/** * Soul创业派对 - 小程序入口 * 开发: 卡若 */ const { parseScene } = require('./utils/scene.js') App({ globalData: { // API基础地址 - 连接真实后端 baseUrl: 'https://soulapi.quwanzhi.com', // baseUrl: 'https://souldev.quwanzhi.com', // baseUrl: 'http://localhost:8080', // 小程序配置 - 真实AppID appId: 'wxb8bbb2b10dec74aa', // 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗 withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE', // 微信支付配置 mchId: '1318592501', // 商户号 // 用户信息 userInfo: null, openId: null, // 微信openId,支付必需 isLoggedIn: false, // 书籍数据 bookData: null, totalSections: 62, // 购买记录 purchasedSections: [], hasFullBook: false, // 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」) readSectionIds: [], // 推荐绑定 pendingReferralCode: null, // 待绑定的推荐码 // 主题配置 theme: { brandColor: '#00CED1', brandSecondary: '#20B2AA', goldColor: '#FFD700', bgColor: '#000000', cardBg: '#1c1c1e' }, // 系统信息 systemInfo: null, statusBarHeight: 44, navBarHeight: 88, // TabBar相关 currentTab: 0 }, onLaunch(options) { this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || [] // 获取系统信息 this.getSystemInfo() // 检查登录状态 this.checkLoginStatus() // 加载书籍数据 this.loadBookData() // 检查更新 this.checkUpdate() // 处理分享参数(推荐码绑定) this.handleReferralCode(options) }, // 小程序显示时也检查分享参数 onShow(options) { this.handleReferralCode(options) }, // 处理推荐码绑定:官方以 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) // 忽略错误,不影响用户体验 } }, // 绑定推荐码到用户 async bindReferralCode(refCode) { try { const userId = this.globalData.userInfo?.id if (!userId || !refCode) return console.log('[App] 绑定推荐码:', refCode, '到用户:', userId) // 调用API绑定推荐关系 const res = await this.request('/api/miniprogram/referral/bind', { method: 'POST', data: { userId, referralCode: refCode }, silent: true }) if (res.success) { console.log('[App] 推荐码绑定成功') // 仅记录当前已绑定的推荐码,用于展示/调试;是否允许更换由后端根据30天规则判断 wx.setStorageSync('boundReferralCode', refCode) this.globalData.pendingReferralCode = null wx.removeStorageSync('pendingReferralCode') } } catch (e) { console.error('[App] 绑定推荐码失败:', e) } }, // 根据业务 id 从 bookData 查 mid(用于跳转) getSectionMid(sectionId) { const list = this.globalData.bookData || [] const ch = list.find(c => c.id === sectionId) return ch?.mid || 0 }, // 获取当前用户的邀请码(用于分享带 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 '' }, // 获取系统信息 getSystemInfo() { try { const systemInfo = wx.getSystemInfoSync() this.globalData.systemInfo = systemInfo this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44 // 计算导航栏高度 const menuButton = wx.getMenuButtonBoundingClientRect() if (menuButton) { this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight } } catch (e) { console.error('获取系统信息失败:', e) } }, // 检查登录状态 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 } } catch (e) { console.error('检查登录状态失败:', e) } }, // 加载书籍数据 async loadBookData() { try { // 先从缓存加载 const cachedData = wx.getStorageSync('bookData') if (cachedData) { this.globalData.bookData = cachedData } // 从服务器获取最新数据 const res = await this.request('/api/miniprogram/book/all-chapters') if (res && (res.data || res.chapters)) { const chapters = res.data || res.chapters || [] this.globalData.bookData = chapters wx.setStorageSync('bookData', chapters) } } catch (e) { console.error('加载书籍数据失败:', e) } }, // 检查更新 checkUpdate() { if (wx.canIUse('getUpdateManager')) { const updateManager = wx.getUpdateManager() updateManager.onCheckForUpdate((res) => { if (res.hasUpdate) { console.log('发现新版本') } }) updateManager.onUpdateReady(() => { wx.showModal({ title: '更新提示', content: '新版本已准备好,是否重启应用?', success: (res) => { if (res.confirm) { updateManager.applyUpdate() } } }) }) updateManager.onUpdateFailed(() => { wx.showToast({ title: '更新失败,请稍后重试', icon: 'none' }) }) } }, /** * 从 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 一致)。 * @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 = '' } const silent = !!options.silent const showError = (msg) => { if (!silent && msg) { wx.showToast({ title: msg, icon: 'none', duration: 2500 }) } } return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: this.globalData.baseUrl + url, method: options.method || 'GET', data: options.data || {}, 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, '操作失败') 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)) } }) }) }, // 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”) async login() { 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) { this.globalData.userInfo = res.data.user this.globalData.isLoggedIn = true this.globalData.purchasedSections = res.data.user.purchasedSections || [] this.globalData.hasFullBook = res.data.user.hasFullBook || false wx.setStorageSync('userInfo', res.data.user) wx.setStorageSync('token', res.data.token || '') // 登录成功后,检查待绑定的推荐码并执行绑定 const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode if (pendingRef) { console.log('[App] 登录后自动绑定推荐码:', pendingRef) this.bindReferralCode(pendingRef) } } 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() { // 先检查缓存 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) { this.globalData.userInfo = res.data.user this.globalData.isLoggedIn = true this.globalData.purchasedSections = res.data.user.purchasedSections || [] this.globalData.hasFullBook = res.data.user.hasFullBook || false wx.setStorageSync('userInfo', res.data.user) wx.setStorageSync('token', res.data.token || '') const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode if (pendingRef) { console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef) this.bindReferralCode(pendingRef) } } 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) { 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) { this.globalData.userInfo = res.data.user this.globalData.isLoggedIn = true this.globalData.purchasedSections = res.data.user.purchasedSections || [] this.globalData.hasFullBook = res.data.user.hasFullBook || false wx.setStorageSync('userInfo', res.data.user) wx.setStorageSync('token', res.data.token) // 登录成功后绑定推荐码 const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode if (pendingRef) { console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef) this.bindReferralCode(pendingRef) } 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() } })