diff --git a/miniprogram/app.js b/miniprogram/app.js
index 1773762b..74742bf4 100644
--- a/miniprogram/app.js
+++ b/miniprogram/app.js
@@ -10,16 +10,22 @@ const mpPagePopups = require('./utils/mpPagePopups.js')
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
-// baseUrl 手动切换(本地端口以 soul-api/.env 的 PORT 为准,当前仓库多为 8080;发版前改回线上)
-const API_BASE_URL = 'http://127.0.0.1:8080'
-// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
-// const API_BASE_URL = 'https://souldev.quwanzhi.com'
+// 线上正式域名(真机/体验版/正式版强制使用此地址)
+const API_BASE_URL_PROD = 'https://soulapi.quwanzhi.com'
+// 本地开发地址(仅开发者工具模拟器生效;真机自动回退 PROD)
+const API_BASE_URL_DEV = 'http://127.0.0.1:8080'
+// const API_BASE_URL_DEV = 'https://souldev.quwanzhi.com'
+
+// 运行时自动判断:非开发工具(真机/体验版/正式版)强制用 PROD,避免 localhost 打进上传包导致白屏
+const _isDevTools = (typeof __wxConfig !== 'undefined' && __wxConfig.platform === 'devtools')
+ || (wx.getSystemInfoSync && wx.getSystemInfoSync().platform === 'devtools')
+const API_BASE_URL = _isDevTools ? API_BASE_URL_DEV : API_BASE_URL_PROD
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
/** 新用户未完成头像+昵称前,强制留在资料引导(与 avatar-nickname 页配合) */
const PROFILE_GUIDE_REQUIRED_KEY = 'profile_guide_required'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
-const APP_DISPLAY_VERSION = '1.7.2'
+const APP_DISPLAY_VERSION = '1.7.3'
App({
globalData: {
@@ -114,8 +120,20 @@ App({
onLaunch(options) {
- // baseUrl 固定取 API_BASE_URL(通过注释切换)
this.globalData.baseUrl = API_BASE_URL
+ if (!_isDevTools && API_BASE_URL.indexOf('127.0.0.1') !== -1) {
+ console.error('[App] 检测到真机使用 localhost,强制切换为线上地址')
+ this.globalData.baseUrl = API_BASE_URL_PROD
+ }
+ // 体验版/正式版:与上传提审前扫码体验一致,必须走线上正式域名(不依赖其它开关)
+ try {
+ const ai = wx.getAccountInfoSync && wx.getAccountInfoSync()
+ const ev = ai && ai.miniProgram && ai.miniProgram.envVersion
+ if (ev === 'trial' || ev === 'release') {
+ this.globalData.baseUrl = API_BASE_URL_PROD
+ }
+ } catch (_e) {}
+ console.log('[App] 运行环境:', _isDevTools ? '开发工具' : '真机/体验版', '| baseUrl:', this.globalData.baseUrl)
// 昵称等隐私组件需先授权:input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
wx.onNeedPrivacyAuthorization((resolve) => {
diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js
index 9b299492..46da6688 100644
--- a/miniprogram/pages/index/index.js
+++ b/miniprogram/pages/index/index.js
@@ -155,6 +155,8 @@ Page({
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters)
bannerSection: null,
+ /** 首屏 Banner 请求完成前为 true;完成后为 false,避免接口全失败时长期显示「加载中」 */
+ bannerLoading: true,
latestLabel: '最新更新',
// 内容概览
@@ -335,6 +337,7 @@ Page({
// 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法,latest=updated_at)
async loadFeaturedAndLatest() {
+ this.setData({ bannerLoading: true })
try {
const tagClassForTag = (tag) => (tag === '热门' ? 'tag-hot' : 'tag-rec')
const toSectionFromRanking = (s) => {
@@ -429,6 +432,8 @@ Page({
this.setData({ latestChapters, displayLatestChapters: display })
} catch (e) {
console.log('[Index] 从服务端加载推荐失败:', e)
+ } finally {
+ this.setData({ bannerLoading: false })
}
},
@@ -514,35 +519,36 @@ Page({
/** 拉取后台置顶 @人物,合并到首页右上角「链接」区;同时拿到该人的 homeEntryConfig */
async loadHomePinnedPerson() {
+ let pinnedPerson = null
+ let override
try {
const res = await app.request({ url: '/api/miniprogram/ckb/pinned-person', silent: true })
if (res && res.success && res.data && res.data.token) {
const name = cleanSingleLineField(res.data.name) || '好友'
let av = String(res.data.avatar || '').trim()
if (!isSafeImageSrc(av)) av = ''
- this.setData({
- homePinnedPerson: {
- token: String(res.data.token).trim(),
- name,
- avatar: av,
- homeEntryConfig: res.data.homeEntryConfig || null,
- },
- })
+ pinnedPerson = {
+ token: String(res.data.token).trim(),
+ name,
+ avatar: av,
+ homeEntryConfig: res.data.homeEntryConfig || null,
+ }
+ if (pinnedPerson.homeEntryConfig) {
+ override = {}
+ if (pinnedPerson.homeEntryConfig.linkKaruoReward) override.linkKaruoReward = pinnedPerson.homeEntryConfig.linkKaruoReward
+ if (pinnedPerson.homeEntryConfig.liveMicSchedule) override.liveMicSchedule = pinnedPerson.homeEntryConfig.liveMicSchedule
+ }
} else {
- this.setData({ homePinnedPerson: null })
+ pinnedPerson = null
}
} catch (e) {
console.log('[Index] pinned-person:', e)
- this.setData({ homePinnedPerson: null })
+ pinnedPerson = null
}
- const pin = (this.data.homePinnedPerson || {})
- const override = {}
- if (pin.homeEntryConfig) {
- if (pin.homeEntryConfig.linkKaruoReward) override.linkKaruoReward = pin.homeEntryConfig.linkKaruoReward
- if (pin.homeEntryConfig.liveMicSchedule) override.liveMicSchedule = pin.homeEntryConfig.liveMicSchedule
- }
- this._applyHomeMpUi()
- this._refreshHomeEntryFlags(Object.keys(override).length ? override : undefined)
+ this.setData({ homePinnedPerson: pinnedPerson }, () => {
+ this._applyHomeMpUi()
+ this._refreshHomeEntryFlags(override)
+ })
},
async loadFeatureConfig() {
@@ -581,13 +587,19 @@ Page({
await this.loadHomePinnedPerson()
},
- _refreshHomeEntryFlags() {
+ _refreshHomeEntryFlags(override) {
const audit = !!app.globalData.auditMode
- // 优先读置顶超级个体的 per-person 配置,回退到全局 mpUi.homePage
- const personCfg = (this.data.homePinnedPerson && this.data.homePinnedPerson.homeEntryConfig) || null
+ // 有置顶人物时:入口开关严格按该人物配置,不再回退全局,避免“人字拖未开上麦却显示申请上麦”。
+ // 无置顶人物时:沿用全局 mpUi.homePage。
+ const hasPinned = !!(this.data.homePinnedPerson && this.data.homePinnedPerson.token)
+ const personCfg = override || ((this.data.homePinnedPerson && this.data.homePinnedPerson.homeEntryConfig) || null)
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
- const rw = (personCfg && personCfg.linkKaruoReward) || h.linkKaruoReward || {}
- const schReal = (personCfg && personCfg.liveMicSchedule) || h.liveMicSchedule || {}
+ const rw = hasPinned
+ ? ((personCfg && personCfg.linkKaruoReward) || {})
+ : (h.linkKaruoReward || {})
+ const schReal = hasPinned
+ ? ((personCfg && personCfg.liveMicSchedule) || {})
+ : (h.liveMicSchedule || {})
const rewardEnabled = normCfgBool(rw.enabled)
const micEnabled = normCfgBool(schReal.enabled)
const inWindow = isNowInMicWindow(schReal.micStart, schReal.micEnd)
@@ -648,8 +660,9 @@ Page({
avatar = this.data.mpUiLinkKaruoDisplay || DEFAULT_KARUO_LINK_AVATAR
} else if (reward) {
show = true; kind = 'reward'
- label = String(sch.micButtonText || '申请上麦').trim() || '申请上麦'
- emoji = String(sch.micEmoji || '🎤').trim() || '🎤'
+ const rwUi = this.data.linkKaruoRewardUi || {}
+ label = String(rwUi.entryButtonText || '支持一下').trim() || '支持一下'
+ emoji = ''
}
this.setData({ headerCornerAction: { show, kind, label, emoji, avatar } })
},
diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml
index 9256256f..7ba67e96 100644
--- a/miniprogram/pages/index/index.wxml
+++ b/miniprogram/pages/index/index.wxml
@@ -47,12 +47,17 @@
-
+
{{mpUiBannerTag}}
加载中...
{{mpUiBannerReadMore}}
-
+
+
+
+ {{mpUiBannerTag}}
+ 暂无推荐,去目录看看 →
+ {{mpUiBannerReadMore}}
diff --git a/miniprogram/pages/settings/settings.js b/miniprogram/pages/settings/settings.js
index aa0c85ec..b60ba765 100644
--- a/miniprogram/pages/settings/settings.js
+++ b/miniprogram/pages/settings/settings.js
@@ -34,7 +34,7 @@ Page({
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
const displayVersion =
wxPkgVersion ||
- (app.globalData.appDisplayVersion || '1.7.2')
+ (app.globalData.appDisplayVersion || '1.7.3')
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
isLoggedIn: app.globalData.isLoggedIn,
@@ -50,7 +50,7 @@ Page({
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
const displayVersion =
wxPkgVersion ||
- (app.globalData.appDisplayVersion || '1.7.2')
+ (app.globalData.appDisplayVersion || '1.7.3')
this.setData({ version: displayVersion })
this.loadBindingInfo()
},
diff --git a/scripts/miniprogram_upload.sh b/scripts/miniprogram_upload.sh
index c9756607..45f49ee5 100755
--- a/scripts/miniprogram_upload.sh
+++ b/scripts/miniprogram_upload.sh
@@ -13,8 +13,8 @@ if [[ ! -x "$CLI" ]]; then
exit 1
fi
-# 未传参时默认 1.7.2(避免手滑打成 1.17 等与展示不一致)
-DEFAULT_VER="${MINIPROGRAM_DEFAULT_VERSION:-1.7.2}"
+# 未传参时默认 1.7.3(避免手滑打成 1.17 等与展示不一致)
+DEFAULT_VER="${MINIPROGRAM_DEFAULT_VERSION:-1.7.3}"
VERSION="${1:-$DEFAULT_VER}"
DESC="${2:-版本 v$VERSION}"
diff --git a/scripts/wechat_miniprogram_release.py b/scripts/wechat_miniprogram_release.py
index 86d825d9..48db5776 100644
--- a/scripts/wechat_miniprogram_release.py
+++ b/scripts/wechat_miniprogram_release.py
@@ -247,8 +247,8 @@ def main() -> None:
p_up.add_argument(
"--version",
"-v",
- default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.2"),
- help="版本号,默认 1.7.2 或环境变量 MINIPROGRAM_DEFAULT_VERSION",
+ default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.3"),
+ help="版本号,默认 1.7.3 或环境变量 MINIPROGRAM_DEFAULT_VERSION",
)
p_up.add_argument(
"--desc",
@@ -289,7 +289,7 @@ def main() -> None:
p_uo.add_argument(
"--version",
"-v",
- default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.2"),
+ default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.3"),
)
p_uo.add_argument("--desc", "-d", default="", help="默认:版本 v<版本号>")
@@ -300,7 +300,7 @@ def main() -> None:
p_rel.add_argument(
"--version",
"-v",
- default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.2"),
+ default=os.environ.get("MINIPROGRAM_DEFAULT_VERSION", "1.7.3"),
)
p_rel.add_argument("--desc", "-d", default="", help="上传说明,默认:版本 v<版本号>")
p_rel.add_argument("--version-desc", default="", help="提交审核说明,默认同上传说明")
diff --git a/soul-api/internal/handler/ckb.go b/soul-api/internal/handler/ckb.go
index e8c1db9c..1eed77e4 100644
--- a/soul-api/internal/handler/ckb.go
+++ b/soul-api/internal/handler/ckb.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"log"
+ "math"
"net/http"
"net/url"
"sort"
@@ -946,23 +947,35 @@ func CKBLead(c *gin.Context) {
}
}
- // 去重:同一用户对同一 @人物(targetUserId=Person.token)仅允许一条有效留资;已存在则不再写库、不调存客宝(不同人物互不影响)
+ // 去重:同一用户对同一 @人物 1 小时内仅允许一次;超过 1 小时允许再次触发(不同人物互不影响)
targetTok := strings.TrimSpace(body.TargetUserID)
if body.UserID != "" && targetTok != "" {
- var existCount int64
- db.Model(&model.CkbLeadRecord{}).Where("user_id = ? AND target_person_id = ?", body.UserID, targetTok).Count(&existCount)
- if existCount > 0 {
+ var lastRec model.CkbLeadRecord
+ err := db.Model(&model.CkbLeadRecord{}).
+ Where("user_id = ? AND target_person_id = ?", body.UserID, targetTok).
+ Order("created_at DESC").
+ First(&lastRec).Error
+ if err == nil && time.Since(lastRec.CreatedAt) < leadRepeatCooldown {
who := targetName
if who == "" {
who = "对方"
}
- msg := fmt.Sprintf("您已向「%s」留资过,无需重复提交", who)
+ remain := leadRepeatCooldown - time.Since(lastRec.CreatedAt)
+ if remain < 0 {
+ remain = 0
+ }
+ minutes := int(math.Ceil(remain.Minutes()))
+ if minutes < 1 {
+ minutes = 1
+ }
+ msg := fmt.Sprintf("您已向「%s」提交过,请 %d 分钟后再试", who, minutes)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": msg,
"data": gin.H{
- "repeatedSubmit": true,
- "skipped": true,
+ "repeatedSubmit": true,
+ "skipped": true,
+ "cooldownMinutes": minutes,
},
})
return
@@ -1203,26 +1216,28 @@ func leadSourceLabel(source string) string {
var _webhookDedupCache = struct {
sync.Mutex
- m map[string]string
-}{m: make(map[string]string)}
+ m map[string]time.Time
+}{m: make(map[string]time.Time)}
+
+const leadRepeatCooldown = time.Hour
func webhookShouldSkip(userId string, targetMemberID string) bool {
if userId == "" && targetMemberID == "" {
return false
}
- today := time.Now().Format("2006-01-02")
+ now := time.Now()
key := strings.TrimSpace(userId) + "|" + strings.TrimSpace(targetMemberID)
if key == "|" {
return false
}
_webhookDedupCache.Lock()
defer _webhookDedupCache.Unlock()
- if _webhookDedupCache.m[key] == today {
+ if lastAt, ok := _webhookDedupCache.m[key]; ok && now.Sub(lastAt) < leadRepeatCooldown {
return true
}
- _webhookDedupCache.m[key] = today
+ _webhookDedupCache.m[key] = now
if len(_webhookDedupCache.m) > 10000 {
- _webhookDedupCache.m = map[string]string{key: today}
+ _webhookDedupCache.m = map[string]time.Time{key: now}
}
return false
}
@@ -1259,7 +1274,7 @@ func loadLeadWebhookURL(db *gorm.DB, targetMemberID string) string {
func sendLeadWebhook(db *gorm.DB, p leadWebhookPayload) {
if p.LeadUserID != "" && webhookShouldSkip(p.LeadUserID, p.TargetMemberID) {
- log.Printf("webhook: skip duplicate for user %s today", p.LeadUserID)
+ log.Printf("webhook: skip duplicate for user %s within 1h", p.LeadUserID)
return
}
webhookURL := loadLeadWebhookURL(db, p.TargetMemberID)
diff --git a/soul-api/internal/handler/db_person.go b/soul-api/internal/handler/db_person.go
index 36c7e35c..41a051b1 100644
--- a/soul-api/internal/handler/db_person.go
+++ b/soul-api/internal/handler/db_person.go
@@ -858,12 +858,17 @@ func AdminCKBPlanCheck(c *gin.Context) {
"ckbPlanId": p.CkbPlanID,
"status": "unknown",
}
- // 尝试启用计划
- if enableErr := setCkbPlanEnabled(token, p.CkbPlanID, true); enableErr != nil {
- item["status"] = "error"
- item["error"] = enableErr.Error()
- } else {
+ // 先用详情接口做连通性检测(不改动计划配置),避免 update 接口对部分计划返回“更新计划失败”造成误报。
+ if _, detailErr := ckbOpenGetPlanDetail(token, p.CkbPlanID); detailErr == nil {
item["status"] = "online"
+ } else {
+ // 兼容旧数据:详情失败时再降级尝试启用一次,双重探测提高兼容性。
+ if enableErr := setCkbPlanEnabled(token, p.CkbPlanID, true); enableErr != nil {
+ item["status"] = "error"
+ item["error"] = fmt.Sprintf("详情探测失败: %v;启用探测失败: %v", detailErr, enableErr)
+ } else {
+ item["status"] = "online"
+ }
}
out = append(out, item)
}