2026-03-07 22:58:43 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Soul创业派对 - 小程序入口
|
|
|
|
|
|
* 开发: 卡若
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const { parseScene } = require('./utils/scene.js')
|
2026-03-17 12:21:33 +08:00
|
|
|
|
const { checkAndExecute } = require('./utils/ruleEngine.js')
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
2026-03-17 18:22:06 +08:00
|
|
|
|
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
|
|
|
|
|
const DEFAULT_MCH_ID = '1318592501'
|
|
|
|
|
|
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
App({
|
|
|
|
|
|
globalData: {
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// API 基础地址:开发时修改下面一行切换环境
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// baseUrl: "https://soulapi.quwanzhi.com",
|
|
|
|
|
|
baseUrl: 'http://localhost:8080', // 开发
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// baseUrl: 'https://souldev.quwanzhi.com', // 测试
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 小程序配置 - 真实AppID
|
2026-03-18 12:40:51 +08:00
|
|
|
|
appId: DEFAULT_APP_ID,
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
2026-03-18 12:40:51 +08:00
|
|
|
|
withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID,
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 微信支付配置
|
2026-03-18 12:40:51 +08:00
|
|
|
|
mchId: DEFAULT_MCH_ID,
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 用户信息
|
|
|
|
|
|
userInfo: null,
|
|
|
|
|
|
openId: null, // 微信openId,支付必需
|
|
|
|
|
|
isLoggedIn: false,
|
|
|
|
|
|
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// 书籍数据(bookData 由 chapters-by-part 等逐步填充,不再预加载 all-chapters)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
bookData: null,
|
|
|
|
|
|
totalSections: 62,
|
|
|
|
|
|
|
|
|
|
|
|
// 购买记录
|
|
|
|
|
|
purchasedSections: [],
|
|
|
|
|
|
hasFullBook: false,
|
2026-03-10 11:04:34 +08:00
|
|
|
|
// VIP 会员(365天,包含增值版免费;与 hasFullBook=9.9 买断不同)
|
|
|
|
|
|
isVip: false,
|
|
|
|
|
|
vipExpireDate: '',
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
|
|
|
|
|
|
readSectionIds: [],
|
|
|
|
|
|
|
|
|
|
|
|
// 推荐绑定
|
|
|
|
|
|
pendingReferralCode: null, // 待绑定的推荐码
|
|
|
|
|
|
|
|
|
|
|
|
// 主题配置
|
|
|
|
|
|
theme: {
|
|
|
|
|
|
brandColor: '#00CED1',
|
|
|
|
|
|
brandSecondary: '#20B2AA',
|
|
|
|
|
|
goldColor: '#FFD700',
|
|
|
|
|
|
bgColor: '#000000',
|
|
|
|
|
|
cardBg: '#1c1c1e'
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 系统信息
|
|
|
|
|
|
systemInfo: null,
|
|
|
|
|
|
statusBarHeight: 44,
|
|
|
|
|
|
navBarHeight: 88,
|
|
|
|
|
|
|
|
|
|
|
|
// TabBar相关
|
|
|
|
|
|
currentTab: 0,
|
|
|
|
|
|
|
2026-03-10 20:20:03 +08:00
|
|
|
|
// 是否处于「单页模式」(如朋友圈文章里的单页预览)
|
|
|
|
|
|
// 用于在受限环境下给出引导文案,提示用户点击底部「前往小程序」进入完整体验
|
|
|
|
|
|
isSinglePageMode: false,
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 更新检测:上次检测时间戳,避免频繁请求
|
2026-03-17 18:22:06 +08:00
|
|
|
|
lastUpdateCheck: 0,
|
|
|
|
|
|
// mpConfig 上次刷新时间戳(onShow 节流,避免频繁请求)
|
|
|
|
|
|
lastMpConfigCheck: 0,
|
|
|
|
|
|
|
|
|
|
|
|
// 审核模式:后端 /api/miniprogram/config 返回 auditMode=true 时隐藏所有支付相关UI
|
|
|
|
|
|
auditMode: false,
|
|
|
|
|
|
// 客服/微信:mp_config 返回 supportWechat
|
2026-03-18 16:00:57 +08:00
|
|
|
|
supportWechat: '',
|
|
|
|
|
|
// config 统一缓存(5min),减少重复请求
|
|
|
|
|
|
configCache: null,
|
|
|
|
|
|
configCacheExpires: 0
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
onLaunch(options) {
|
|
|
|
|
|
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
|
|
|
|
|
// 获取系统信息
|
|
|
|
|
|
this.getSystemInfo()
|
2026-03-10 20:20:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 场景值兜底:1154 为「朋友圈单页模式」进入
|
|
|
|
|
|
try {
|
|
|
|
|
|
const launchOpts = wx.getLaunchOptionsSync ? wx.getLaunchOptionsSync() : null
|
|
|
|
|
|
if (launchOpts && launchOpts.scene === 1154) {
|
|
|
|
|
|
this.globalData.isSinglePageMode = true
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[App] 读取 LaunchOptions 失败:', e)
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查登录状态
|
|
|
|
|
|
this.checkLoginStatus()
|
|
|
|
|
|
|
|
|
|
|
|
// 加载书籍数据
|
|
|
|
|
|
this.loadBookData()
|
2026-03-17 14:02:09 +08:00
|
|
|
|
// 加载 mpConfig(appId、mchId、withdrawSubscribeTmplId 等,失败时保留默认值)
|
|
|
|
|
|
this.loadMpConfig()
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查更新
|
|
|
|
|
|
this.checkUpdate()
|
|
|
|
|
|
|
|
|
|
|
|
// 处理分享参数(推荐码绑定)
|
|
|
|
|
|
this.handleReferralCode(options)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// 小程序显示时:处理分享参数、检测更新、刷新审核模式(从后台切回时)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
onShow(options) {
|
|
|
|
|
|
this.handleReferralCode(options)
|
|
|
|
|
|
this.checkUpdate()
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// 从后台切回时仅刷新审核模式(轻量接口 /config/audit-mode),节流 30 秒
|
2026-03-17 18:22:06 +08:00
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
if (!this.globalData.lastMpConfigCheck || now - this.globalData.lastMpConfigCheck > 30 * 1000) {
|
|
|
|
|
|
this.globalData.lastMpConfigCheck = now
|
2026-03-18 16:00:57 +08:00
|
|
|
|
this.getAuditMode()
|
2026-03-17 18:22:06 +08:00
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
|
|
|
|
|
|
handleReferralCode(options) {
|
|
|
|
|
|
const query = options?.query || {}
|
|
|
|
|
|
let refCode = query.ref || query.referralCode
|
|
|
|
|
|
const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || ''
|
|
|
|
|
|
if (sceneStr) {
|
|
|
|
|
|
const parsed = parseScene(sceneStr)
|
|
|
|
|
|
if (parsed.mid) this.globalData.initialSectionMid = parsed.mid
|
|
|
|
|
|
if (parsed.id) this.globalData.initialSectionId = parsed.id
|
|
|
|
|
|
if (parsed.ref) refCode = parsed.ref
|
|
|
|
|
|
}
|
|
|
|
|
|
if (refCode) {
|
|
|
|
|
|
console.log('[App] 检测到推荐码:', refCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 立即记录访问(不需要登录,用于统计"通过链接进的人数")
|
|
|
|
|
|
this.recordReferralVisit(refCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制,让后端根据30天规则判断续期/抢夺)
|
|
|
|
|
|
this.globalData.pendingReferralCode = refCode
|
|
|
|
|
|
wx.setStorageSync('pendingReferralCode', refCode)
|
|
|
|
|
|
// 同步写入 referral_code,供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
|
|
|
|
|
|
wx.setStorageSync('referral_code', refCode)
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
|
|
|
|
|
|
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
|
|
|
|
|
|
this.bindReferralCode(refCode)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 记录推荐访问(不需要登录,用于统计)
|
|
|
|
|
|
async recordReferralVisit(refCode) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取openId(如果有)
|
|
|
|
|
|
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
|
|
|
|
|
|
const userId = this.globalData.userInfo?.id || ''
|
|
|
|
|
|
|
|
|
|
|
|
await this.request('/api/miniprogram/referral/visit', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
referralCode: refCode,
|
|
|
|
|
|
visitorOpenId: openId,
|
|
|
|
|
|
visitorId: userId,
|
|
|
|
|
|
source: 'miniprogram',
|
|
|
|
|
|
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
|
|
|
|
|
|
},
|
|
|
|
|
|
silent: true
|
|
|
|
|
|
})
|
|
|
|
|
|
console.log('[App] 记录推荐访问成功')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('[App] 记录推荐访问失败:', e.message)
|
|
|
|
|
|
// 忽略错误,不影响用户体验
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-08 08:00:39 +08:00
|
|
|
|
// 绑定推荐码到用户(自己的推荐码不请求接口,避免 400 与控制台报错)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
async bindReferralCode(refCode) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const userId = this.globalData.userInfo?.id
|
|
|
|
|
|
if (!userId || !refCode) return
|
2026-03-08 08:00:39 +08:00
|
|
|
|
|
|
|
|
|
|
const myCode = this.getMyReferralCode()
|
|
|
|
|
|
if (myCode && this._normalizeReferralCode(refCode) === this._normalizeReferralCode(myCode)) {
|
|
|
|
|
|
console.log('[App] 跳过绑定:不能使用自己的推荐码')
|
|
|
|
|
|
this.globalData.pendingReferralCode = null
|
|
|
|
|
|
wx.removeStorageSync('pendingReferralCode')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
|
2026-03-08 08:00:39 +08:00
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const res = await this.request('/api/miniprogram/referral/bind', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
userId,
|
|
|
|
|
|
referralCode: refCode
|
|
|
|
|
|
},
|
|
|
|
|
|
silent: true
|
|
|
|
|
|
})
|
2026-03-08 08:00:39 +08:00
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
if (res.success) {
|
|
|
|
|
|
console.log('[App] 推荐码绑定成功')
|
|
|
|
|
|
wx.setStorageSync('boundReferralCode', refCode)
|
|
|
|
|
|
this.globalData.pendingReferralCode = null
|
|
|
|
|
|
wx.removeStorageSync('pendingReferralCode')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-03-08 08:00:39 +08:00
|
|
|
|
const msg = (e && e.message) ? String(e.message) : ''
|
|
|
|
|
|
if (msg.indexOf('不能使用自己的推荐码') !== -1) {
|
|
|
|
|
|
console.log('[App] 跳过绑定:不能使用自己的推荐码')
|
|
|
|
|
|
this.globalData.pendingReferralCode = null
|
|
|
|
|
|
wx.removeStorageSync('pendingReferralCode')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('[App] 绑定推荐码失败:', e)
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-08 08:00:39 +08:00
|
|
|
|
// 推荐码归一化后比较(忽略大小写、短横线等)
|
|
|
|
|
|
_normalizeReferralCode(code) {
|
|
|
|
|
|
if (!code || typeof code !== 'string') return ''
|
|
|
|
|
|
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取当前用户的邀请码(用于分享带 ref,未登录返回空字符串)
|
|
|
|
|
|
getMyReferralCode() {
|
|
|
|
|
|
const user = this.globalData.userInfo
|
|
|
|
|
|
if (!user) return ''
|
|
|
|
|
|
if (user.referralCode) return user.referralCode
|
|
|
|
|
|
if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
|
|
|
|
|
|
return ''
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 自定义导航栏「返回」:有上一页则返回,否则跳转首页(解决从分享进入时点返回无效的问题)
|
|
|
|
|
|
*/
|
|
|
|
|
|
goBackOrToHome() {
|
|
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
if (pages.length <= 1) {
|
|
|
|
|
|
wx.switchTab({ url: '/pages/index/index' })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
wx.navigateBack()
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 获取系统信息
|
|
|
|
|
|
getSystemInfo() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const systemInfo = wx.getSystemInfoSync()
|
|
|
|
|
|
this.globalData.systemInfo = systemInfo
|
|
|
|
|
|
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
|
2026-03-10 20:20:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 微信在单页模式下会在 systemInfo.mode 标记 singlePage
|
|
|
|
|
|
if (systemInfo.mode === 'singlePage') {
|
|
|
|
|
|
this.globalData.isSinglePageMode = true
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算导航栏高度
|
|
|
|
|
|
const menuButton = wx.getMenuButtonBoundingClientRect()
|
|
|
|
|
|
if (menuButton) {
|
|
|
|
|
|
this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('获取系统信息失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-10 20:20:03 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 若当前处于朋友圈等「单页模式」,在尝试登录/购买前给用户友好提示,
|
|
|
|
|
|
* 引导用户点击底部「前往小程序」进入完整小程序再操作。
|
|
|
|
|
|
* 返回 false 表示应中断当前操作。
|
|
|
|
|
|
*/
|
|
|
|
|
|
ensureFullAppForAuth() {
|
|
|
|
|
|
// 每次调用时再做一次兜底检测,避免全局标记遗漏
|
|
|
|
|
|
try {
|
|
|
|
|
|
const sys = wx.getSystemInfoSync()
|
|
|
|
|
|
if (sys && sys.mode === 'singlePage') {
|
|
|
|
|
|
this.globalData.isSinglePageMode = true
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[App] ensureFullAppForAuth getSystemInfoSync error:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.globalData.isSinglePageMode) return true
|
|
|
|
|
|
|
|
|
|
|
|
wx.showModal({
|
|
|
|
|
|
title: '请前往完整小程序',
|
|
|
|
|
|
content: '当前为朋友圈单页,仅支持部分浏览。如需登录和解锁内容,请点击底部「前往小程序」后再操作。',
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
confirmText: '我知道了',
|
|
|
|
|
|
})
|
|
|
|
|
|
return false
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 检查登录状态
|
|
|
|
|
|
checkLoginStatus() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const userInfo = wx.getStorageSync('userInfo')
|
|
|
|
|
|
const token = wx.getStorageSync('token')
|
|
|
|
|
|
|
|
|
|
|
|
if (userInfo && token) {
|
|
|
|
|
|
this.globalData.userInfo = userInfo
|
|
|
|
|
|
this.globalData.isLoggedIn = true
|
|
|
|
|
|
this.globalData.purchasedSections = userInfo.purchasedSections || []
|
|
|
|
|
|
this.globalData.hasFullBook = userInfo.hasFullBook || false
|
2026-03-10 11:04:34 +08:00
|
|
|
|
this.globalData.isVip = userInfo.isVip || false
|
|
|
|
|
|
this.globalData.vipExpireDate = userInfo.vipExpireDate || ''
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('检查登录状态失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// 加载书籍元数据(totalSections),不再预加载 all-chapters
|
2026-03-07 22:58:43 +08:00
|
|
|
|
async loadBookData() {
|
|
|
|
|
|
try {
|
2026-03-18 16:00:57 +08:00
|
|
|
|
const res = await this.request({ url: '/api/miniprogram/book/parts', silent: true })
|
|
|
|
|
|
if (res?.success && res.totalSections != null) {
|
|
|
|
|
|
this.globalData.totalSections = res.totalSections
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
2026-03-18 16:00:57 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const statsRes = await this.request({ url: '/api/miniprogram/book/stats', silent: true })
|
|
|
|
|
|
if (statsRes?.success && statsRes?.data?.totalChapters != null) {
|
|
|
|
|
|
this.globalData.totalSections = statsRes.data.totalChapters
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取 config(统一缓存 5min,各页优先读缓存)
|
|
|
|
|
|
* 使用拆分接口 core + audit-mode,体积更小、审核模式独立刷新
|
|
|
|
|
|
* @param {boolean} forceRefresh - 强制刷新,跳过缓存
|
|
|
|
|
|
* @returns {Promise<object|null>} 完整 config 或 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getConfig(forceRefresh = false) {
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const CACHE_TTL = 5 * 60 * 1000
|
|
|
|
|
|
if (!forceRefresh && this.globalData.configCache && now < this.globalData.configCacheExpires) {
|
|
|
|
|
|
return this.globalData.configCache
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [coreRes, auditRes] = await Promise.all([
|
|
|
|
|
|
this.request({ url: '/api/miniprogram/config/core', silent: true, timeout: 5000 }),
|
|
|
|
|
|
this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 })
|
|
|
|
|
|
])
|
|
|
|
|
|
if (coreRes) {
|
|
|
|
|
|
const auditMode = auditRes && typeof auditRes.auditMode === 'boolean' ? auditRes.auditMode : false
|
|
|
|
|
|
const mp = (coreRes.mpConfig && typeof coreRes.mpConfig === 'object') ? { ...coreRes.mpConfig } : {}
|
|
|
|
|
|
mp.auditMode = auditMode
|
|
|
|
|
|
const res = {
|
|
|
|
|
|
success: coreRes.success,
|
|
|
|
|
|
prices: coreRes.prices,
|
|
|
|
|
|
features: coreRes.features,
|
|
|
|
|
|
userDiscount: coreRes.userDiscount,
|
|
|
|
|
|
mpConfig: mp
|
|
|
|
|
|
}
|
|
|
|
|
|
this.globalData.configCache = res
|
|
|
|
|
|
this.globalData.configCacheExpires = now + CACHE_TTL
|
|
|
|
|
|
return res
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-03-18 16:00:57 +08:00
|
|
|
|
if (this.globalData.configCache) return this.globalData.configCache
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取阅读页扩展配置(linkTags、linkedMiniprograms),懒加载
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getReadExtras() {
|
|
|
|
|
|
if (Array.isArray(this.globalData.linkTagsConfig) && this.globalData.linkTagsConfig.length > 0) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
linkTags: this.globalData.linkTagsConfig,
|
|
|
|
|
|
linkedMiniprograms: this.globalData.linkedMiniprograms || []
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
2026-03-18 16:00:57 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const res = await this.request({ url: '/api/miniprogram/config/read-extras', silent: true, timeout: 5000 })
|
|
|
|
|
|
if (res) {
|
|
|
|
|
|
if (Array.isArray(res.linkTags)) this.globalData.linkTagsConfig = res.linkTags
|
|
|
|
|
|
if (Array.isArray(res.linkedMiniprograms)) this.globalData.linkedMiniprograms = res.linkedMiniprograms
|
|
|
|
|
|
return res
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
return { linkTags: [], linkedMiniprograms: [] }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 仅刷新审核模式(从后台切回时用,轻量)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getAuditMode() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await this.request({ url: '/api/miniprogram/config/audit-mode', silent: true, timeout: 3000 })
|
|
|
|
|
|
if (res && typeof res.auditMode === 'boolean') {
|
|
|
|
|
|
this.globalData.auditMode = res.auditMode
|
|
|
|
|
|
if (this.globalData.configCache && this.globalData.configCache.mpConfig) {
|
|
|
|
|
|
this.globalData.configCache.mpConfig.auditMode = res.auditMode
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
pages.forEach(p => {
|
|
|
|
|
|
if (p && p.data && 'auditMode' in p.data) {
|
|
|
|
|
|
p.setData({ auditMode: res.auditMode })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
return res.auditMode
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
return this.globalData.auditMode
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 加载 mpConfig(appId、mchId、withdrawSubscribeTmplId、auditMode、supportWechat 等),失败时保留 globalData 默认值
|
2026-03-17 14:02:09 +08:00
|
|
|
|
async loadMpConfig() {
|
|
|
|
|
|
try {
|
2026-03-18 16:00:57 +08:00
|
|
|
|
const res = await this.getConfig()
|
|
|
|
|
|
if (!res) return
|
2026-03-17 14:02:09 +08:00
|
|
|
|
const mp = (res && res.mpConfig) || (res && res.configs && res.configs.mp_config)
|
|
|
|
|
|
if (mp && typeof mp === 'object') {
|
|
|
|
|
|
if (mp.appId) this.globalData.appId = mp.appId
|
|
|
|
|
|
if (mp.mchId) this.globalData.mchId = mp.mchId
|
|
|
|
|
|
if (mp.withdrawSubscribeTmplId) this.globalData.withdrawSubscribeTmplId = mp.withdrawSubscribeTmplId
|
2026-03-17 18:22:06 +08:00
|
|
|
|
this.globalData.auditMode = !!mp.auditMode
|
|
|
|
|
|
this.globalData.supportWechat = mp.supportWechat || mp.customerWechat || mp.serviceWechat || ''
|
|
|
|
|
|
// 通知当前已加载的页面刷新 auditMode(从后台切回时配置更新后立即生效)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
|
|
pages.forEach(p => {
|
|
|
|
|
|
if (p && p.data && 'auditMode' in p.data) {
|
|
|
|
|
|
p.setData({ auditMode: this.globalData.auditMode || false })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (_) {}
|
2026-03-17 14:02:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[App] loadMpConfig 失败,使用默认值:', e?.message || e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 小程序更新检测(基于 wx.getUpdateManager)
|
|
|
|
|
|
* - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求)
|
|
|
|
|
|
*/
|
|
|
|
|
|
checkUpdate() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!wx.canIUse('getUpdateManager')) return
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const lastCheck = this.globalData.lastUpdateCheck || 0
|
|
|
|
|
|
if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测
|
|
|
|
|
|
this.globalData.lastUpdateCheck = now
|
|
|
|
|
|
|
|
|
|
|
|
const updateManager = wx.getUpdateManager()
|
|
|
|
|
|
updateManager.onCheckForUpdate((res) => {
|
|
|
|
|
|
if (res.hasUpdate) {
|
|
|
|
|
|
console.log('[App] 发现新版本,正在下载...')
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
updateManager.onUpdateReady(() => {
|
|
|
|
|
|
wx.showModal({
|
|
|
|
|
|
title: '更新提示',
|
|
|
|
|
|
content: '新版本已准备好,重启后即可使用',
|
|
|
|
|
|
confirmText: '立即重启',
|
|
|
|
|
|
cancelText: '稍后',
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
if (res.confirm) {
|
|
|
|
|
|
updateManager.applyUpdate()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
updateManager.onUpdateFailed(() => {
|
|
|
|
|
|
wx.showToast({
|
|
|
|
|
|
title: '更新失败,请稍后重试',
|
|
|
|
|
|
icon: 'none',
|
|
|
|
|
|
duration: 2500
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.warn('[App] checkUpdate failed:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段)
|
|
|
|
|
|
*/
|
|
|
|
|
|
_getApiErrorMsg(data, defaultMsg = '请求失败') {
|
|
|
|
|
|
if (!data || typeof data !== 'object') return defaultMsg
|
|
|
|
|
|
const msg = data.message || data.error
|
|
|
|
|
|
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
|
2026-03-18 16:00:57 +08:00
|
|
|
|
* GET 请求 200ms 内相同 url 去重,避免并发重复请求。
|
2026-03-07 22:58:43 +08:00
|
|
|
|
* @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
|
|
|
|
|
|
* @param {object} options - { method, data, header, silent }
|
|
|
|
|
|
* @param {boolean} options.silent - 为 true 时不弹窗,仅 reject(用于静默请求如访问统计)
|
|
|
|
|
|
*/
|
|
|
|
|
|
request(urlOrOptions, options = {}) {
|
|
|
|
|
|
let url
|
|
|
|
|
|
if (typeof urlOrOptions === 'string') {
|
|
|
|
|
|
url = urlOrOptions
|
|
|
|
|
|
} else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) {
|
|
|
|
|
|
url = urlOrOptions.url
|
|
|
|
|
|
options = { ...urlOrOptions, url: undefined }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
url = ''
|
|
|
|
|
|
}
|
2026-03-18 16:00:57 +08:00
|
|
|
|
const method = (options.method || 'GET').toUpperCase()
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const silent = !!options.silent
|
|
|
|
|
|
const showError = (msg) => {
|
|
|
|
|
|
if (!silent && msg) {
|
|
|
|
|
|
wx.showToast({ title: msg, icon: 'none', duration: 2500 })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 16:00:57 +08:00
|
|
|
|
// GET 短时去重:相同 url 的并发请求共享同一 promise
|
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
|
const dedupKey = url + (options.data ? JSON.stringify(options.data) : '')
|
|
|
|
|
|
const pending = this._requestPending || (this._requestPending = {})
|
|
|
|
|
|
if (pending[dedupKey]) {
|
|
|
|
|
|
return pending[dedupKey].promise
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
2026-03-07 22:58:43 +08:00
|
|
|
|
const token = wx.getStorageSync('token')
|
|
|
|
|
|
wx.request({
|
|
|
|
|
|
url: this.globalData.baseUrl + url,
|
|
|
|
|
|
method: options.method || 'GET',
|
|
|
|
|
|
data: options.data || {},
|
2026-03-17 18:22:06 +08:00
|
|
|
|
timeout: options.timeout || 15000,
|
2026-03-07 22:58:43 +08:00
|
|
|
|
header: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': token ? `Bearer ${token}` : '',
|
|
|
|
|
|
...options.header
|
|
|
|
|
|
},
|
|
|
|
|
|
success: (res) => {
|
|
|
|
|
|
const data = res.data
|
|
|
|
|
|
if (res.statusCode === 200) {
|
|
|
|
|
|
// 业务失败:success === false,soul-api 用 message 或 error 返回原因
|
|
|
|
|
|
if (data && data.success === false) {
|
|
|
|
|
|
const msg = this._getApiErrorMsg(data, '操作失败')
|
2026-03-10 14:32:20 +08:00
|
|
|
|
// 登录态不一致:本地有 token/userInfo,但后端查不到该用户
|
|
|
|
|
|
// 典型原因:切换环境(baseUrl)、换库/清库、用户被删除、token 与用户不匹配
|
|
|
|
|
|
if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
|
|
|
|
|
|
this.logout()
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
showError(msg)
|
|
|
|
|
|
reject(new Error(msg))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve(data)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (res.statusCode === 401) {
|
|
|
|
|
|
this.logout()
|
|
|
|
|
|
showError('未授权,请重新登录')
|
|
|
|
|
|
reject(new Error('未授权'))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
// 4xx/5xx:优先用返回体的 message/error
|
|
|
|
|
|
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
|
|
|
|
|
|
showError(msg)
|
|
|
|
|
|
reject(new Error(msg))
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
|
|
|
|
|
|
showError(msg)
|
|
|
|
|
|
reject(new Error(msg))
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-03-18 16:00:57 +08:00
|
|
|
|
|
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
|
const dedupKey = url + (options.data ? JSON.stringify(options.data) : '')
|
|
|
|
|
|
const pending = this._requestPending || (this._requestPending = {})
|
|
|
|
|
|
pending[dedupKey] = { promise, ts: Date.now() }
|
|
|
|
|
|
promise.finally(() => { delete pending[dedupKey] })
|
|
|
|
|
|
}
|
|
|
|
|
|
return promise
|
2026-03-07 22:58:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
|
|
|
|
|
|
async login() {
|
2026-03-10 20:20:03 +08:00
|
|
|
|
if (!this.ensureFullAppForAuth()) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const loginRes = await new Promise((resolve, reject) => {
|
|
|
|
|
|
wx.login({ success: resolve, fail: reject })
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!loginRes || !loginRes.code) {
|
|
|
|
|
|
console.warn('[App] wx.login 未返回 code')
|
|
|
|
|
|
wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await this.request('/api/miniprogram/login', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
data: { code: loginRes.code }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res.success && res.data) {
|
|
|
|
|
|
// 保存openId
|
|
|
|
|
|
if (res.data.openId) {
|
|
|
|
|
|
this.globalData.openId = res.data.openId
|
|
|
|
|
|
wx.setStorageSync('openId', res.data.openId)
|
|
|
|
|
|
console.log('[App] 获取openId成功')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 保存用户信息
|
|
|
|
|
|
if (res.data.user) {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
const user = res.data.user
|
|
|
|
|
|
this.globalData.userInfo = user
|
2026-03-07 22:58:43 +08:00
|
|
|
|
this.globalData.isLoggedIn = true
|
2026-03-12 11:36:50 +08:00
|
|
|
|
this.globalData.purchasedSections = user.purchasedSections || []
|
|
|
|
|
|
this.globalData.hasFullBook = user.hasFullBook || false
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
2026-03-12 11:36:50 +08:00
|
|
|
|
wx.setStorageSync('userInfo', user)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
wx.setStorageSync('token', res.data.token || '')
|
|
|
|
|
|
|
|
|
|
|
|
// 登录成功后,检查待绑定的推荐码并执行绑定
|
|
|
|
|
|
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
|
|
|
|
|
if (pendingRef) {
|
|
|
|
|
|
console.log('[App] 登录后自动绑定推荐码:', pendingRef)
|
|
|
|
|
|
this.bindReferralCode(pendingRef)
|
|
|
|
|
|
}
|
2026-03-12 11:36:50 +08:00
|
|
|
|
|
2026-03-17 12:21:33 +08:00
|
|
|
|
// 登录后引导完善资料(规则引擎接管,完善头像吸收到规则引擎)
|
|
|
|
|
|
checkAndExecute('after_login', null)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return res.data
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (apiError) {
|
|
|
|
|
|
console.log('[App] API登录失败:', apiError.message)
|
|
|
|
|
|
// 不使用模拟登录,提示用户网络问题
|
|
|
|
|
|
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[App] 登录失败:', e)
|
|
|
|
|
|
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 获取openId (支付必需)
|
|
|
|
|
|
async getOpenId() {
|
2026-03-10 20:20:03 +08:00
|
|
|
|
if (!this.ensureFullAppForAuth()) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 先检查缓存
|
|
|
|
|
|
const cachedOpenId = wx.getStorageSync('openId')
|
|
|
|
|
|
if (cachedOpenId) {
|
|
|
|
|
|
this.globalData.openId = cachedOpenId
|
|
|
|
|
|
return cachedOpenId
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 没有缓存则登录获取
|
|
|
|
|
|
try {
|
|
|
|
|
|
const loginRes = await new Promise((resolve, reject) => {
|
|
|
|
|
|
wx.login({ success: resolve, fail: reject })
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const res = await this.request('/api/miniprogram/login', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
data: { code: loginRes.code }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res.success && res.data?.openId) {
|
|
|
|
|
|
this.globalData.openId = res.data.openId
|
|
|
|
|
|
wx.setStorageSync('openId', res.data.openId)
|
|
|
|
|
|
// 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
|
|
|
|
|
|
if (res.data.user) {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
const user = res.data.user
|
|
|
|
|
|
this.globalData.userInfo = user
|
2026-03-07 22:58:43 +08:00
|
|
|
|
this.globalData.isLoggedIn = true
|
2026-03-12 11:36:50 +08:00
|
|
|
|
this.globalData.purchasedSections = user.purchasedSections || []
|
|
|
|
|
|
this.globalData.hasFullBook = user.hasFullBook || false
|
|
|
|
|
|
wx.setStorageSync('userInfo', user)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
wx.setStorageSync('token', res.data.token || '')
|
|
|
|
|
|
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
|
|
|
|
|
if (pendingRef) {
|
|
|
|
|
|
console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
|
|
|
|
|
|
this.bindReferralCode(pendingRef)
|
|
|
|
|
|
}
|
2026-03-12 11:36:50 +08:00
|
|
|
|
|
2026-03-17 12:21:33 +08:00
|
|
|
|
// 登录后引导完善资料(规则引擎接管)
|
|
|
|
|
|
checkAndExecute('after_login', null)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
return res.data.openId
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('[App] 获取openId失败:', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟登录已废弃 - 不再使用
|
|
|
|
|
|
// 现在必须使用真实的微信登录获取openId作为唯一标识
|
|
|
|
|
|
mockLogin() {
|
|
|
|
|
|
console.warn('[App] mockLogin已废弃,请使用真实登录')
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
|
|
|
|
|
|
async loginWithPhone(phoneCode) {
|
2026-03-10 20:20:03 +08:00
|
|
|
|
if (!this.ensureFullAppForAuth()) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-03-07 22:58:43 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const loginRes = await new Promise((resolve, reject) => {
|
|
|
|
|
|
wx.login({ success: resolve, fail: reject })
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!loginRes.code) {
|
|
|
|
|
|
wx.showToast({ title: '获取登录态失败', icon: 'none' })
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await this.request('/api/miniprogram/phone-login', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
data: { code: loginRes.code, phoneCode }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (res.success && res.data) {
|
2026-03-12 11:36:50 +08:00
|
|
|
|
const user = res.data.user
|
|
|
|
|
|
this.globalData.userInfo = user
|
2026-03-07 22:58:43 +08:00
|
|
|
|
this.globalData.isLoggedIn = true
|
2026-03-12 11:36:50 +08:00
|
|
|
|
this.globalData.purchasedSections = user.purchasedSections || []
|
|
|
|
|
|
this.globalData.hasFullBook = user.hasFullBook || false
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
2026-03-12 11:36:50 +08:00
|
|
|
|
wx.setStorageSync('userInfo', user)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
wx.setStorageSync('token', res.data.token)
|
|
|
|
|
|
|
|
|
|
|
|
// 登录成功后绑定推荐码
|
|
|
|
|
|
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
|
|
|
|
|
if (pendingRef) {
|
|
|
|
|
|
console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef)
|
|
|
|
|
|
this.bindReferralCode(pendingRef)
|
|
|
|
|
|
}
|
2026-03-12 11:36:50 +08:00
|
|
|
|
|
2026-03-17 12:21:33 +08:00
|
|
|
|
// 登录后引导完善资料(规则引擎接管)
|
|
|
|
|
|
checkAndExecute('after_login', null)
|
2026-03-07 22:58:43 +08:00
|
|
|
|
|
|
|
|
|
|
return res.data
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log('[App] 手机号登录失败:', e)
|
|
|
|
|
|
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 退出登录
|
|
|
|
|
|
logout() {
|
|
|
|
|
|
this.globalData.userInfo = null
|
|
|
|
|
|
this.globalData.isLoggedIn = false
|
|
|
|
|
|
this.globalData.purchasedSections = []
|
|
|
|
|
|
this.globalData.hasFullBook = false
|
|
|
|
|
|
|
|
|
|
|
|
wx.removeStorageSync('userInfo')
|
|
|
|
|
|
wx.removeStorageSync('token')
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已购买章节
|
|
|
|
|
|
hasPurchased(sectionId) {
|
|
|
|
|
|
if (this.globalData.hasFullBook) return true
|
|
|
|
|
|
return this.globalData.purchasedSections.includes(sectionId)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计)
|
|
|
|
|
|
markSectionAsRead(sectionId) {
|
|
|
|
|
|
if (!sectionId) return
|
|
|
|
|
|
const list = this.globalData.readSectionIds || []
|
|
|
|
|
|
if (list.includes(sectionId)) return
|
|
|
|
|
|
list.push(sectionId)
|
|
|
|
|
|
this.globalData.readSectionIds = list
|
|
|
|
|
|
wx.setStorageSync('readSectionIds', list)
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 已读章节数(用于首页展示)
|
|
|
|
|
|
getReadCount() {
|
|
|
|
|
|
return (this.globalData.readSectionIds || []).length
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 获取章节总数
|
|
|
|
|
|
getTotalSections() {
|
|
|
|
|
|
return this.globalData.totalSections
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 切换TabBar
|
|
|
|
|
|
switchTab(index) {
|
|
|
|
|
|
this.globalData.currentTab = index
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 显示Toast
|
|
|
|
|
|
showToast(title, icon = 'none') {
|
|
|
|
|
|
wx.showToast({
|
|
|
|
|
|
title,
|
|
|
|
|
|
icon,
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 显示Loading
|
|
|
|
|
|
showLoading(title = '加载中...') {
|
|
|
|
|
|
wx.showLoading({
|
|
|
|
|
|
title,
|
|
|
|
|
|
mask: true
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏Loading
|
|
|
|
|
|
hideLoading() {
|
|
|
|
|
|
wx.hideLoading()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|