chore: 以本地为准同步最新改动

提交当前本地未入库的前后端与发布脚本改动,作为本地主版本基线,用于全量覆盖同步到 GitHub。

Made-with: Cursor
This commit is contained in:
卡若
2026-04-06 23:12:18 +08:00
parent 6d11fb295d
commit d48aba5047
8 changed files with 116 additions and 60 deletions

View File

@@ -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) => {

View File

@@ -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 } })
},

View File

@@ -47,12 +47,17 @@
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
</view>
</view>
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
<view class="banner-card banner-skeleton" wx:elif="{{bannerLoading}}" bindtap="goToChapters">
<view class="banner-glow"></view>
<view class="banner-tag">{{mpUiBannerTag}}</view>
<view class="banner-title">加载中...</view>
<view class="banner-action"><text class="banner-action-text">{{mpUiBannerReadMore}}</text><icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon></view>
</view>
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
<view class="banner-glow"></view>
<view class="banner-tag">{{mpUiBannerTag}}</view>
<view class="banner-title">暂无推荐,去目录看看 →</view>
<view class="banner-action"><text class="banner-action-text">{{mpUiBannerReadMore}}</text><icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon></view>
</view>
<!-- 超级个体:与匹配页一致,仅 VIP 横向列表(无首位特例) -->

View File

@@ -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()
},

View File

@@ -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}"

View File

@@ -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="提交审核说明,默认同上传说明")

View File

@@ -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)

View File

@@ -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)
}