Files
MBTI_wang/miniprogram/pages/promo/index.js
Ghost d7137f9349 微信小程序:自定义底栏接入 AI 入口(含图标与样式)、AI 聊天相关页面与历史/报告等能力;审核模式与配置联动;资料/企业/首页/授权等页面与 app 入口配合调整。
后端 API:AI 对话/报告/余额告警 等服务与路由;小程序 tabBar 配置与分润相关能力;出站推送 Webhook 与管理端调试;飞书留资、小程序数据分析 等支撑;多份 数据库迁移(AI 聊天、灵魂文章、tabBar、分润等)。
管理端 / 超管:看板、用户/订单/分销/财务、设置与 小程序 tabBar 管理、分润规则、灵魂文章、AI 配置与监控 等大量页面与能力扩展;主题与布局更新。
抖音小程序:与微信侧在 结果页/购买/个人中心 等方向做了同步或优化。
另含 部署/脚本、开发文档 等配套更新。
邀请码与解锁:后端 分销/邀请码 接口与 add_invite_codes 等 SQL;小程序侧 邀请码弹窗组件、个人中心/推广/多类结果页 的填写入口与 解锁门控(inviteCodeGate、unlockGate);并补充 《小程序解锁引导与邀请码方案》 文档。
超管后台:路由与 企业中心(EnterpriseHub) 等交互与结构优化。
小程序体验:在 首页结果、推广、各测评结果页 上继续打磨;与 邀请码门控、审核门控 小步联动。
2026-04-23 11:28:16 +08:00

398 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pages/promo/index.js
const app = getApp()
const { request } = require('../../utils/request')
const inviteCodeGate = require('../../utils/inviteCodeGate.js')
const { shouldHideInviteCodeEntry } = require('../../utils/miniprogramAuditGate.js')
Page({
data: {
balance: '0.00',
totalEarned: '0.00',
pendingAmount: '0.00',
bindingCount: 0,
paidCount: 0,
expiringCount: 0,
activeTab: 0,
userList: [],
listTotal: 0,
listPage: 1,
listLoading: false,
listFinished: false,
commissionRate: '',
bindingDays: '',
testCommissionType: '',
testCommissionRate: '',
testCommissionAmount: '',
testNoPayment: false,
withdrawMinYuan: '0.01',
withdrawMaxYuan: '',
withdrawFeePct: 0,
requireWithdrawAudit: true,
showWithdrawDialog: false,
withdrawAmountInput: '',
withdrawError: '',
withdrawFeeYuan: '0.00',
withdrawActualYuan: '0.00',
myInviteCode: '',
myInviteCodeMeta: null,
/** loading | ready | error | nodata — 避免因接口失败整块不渲染 */
myInviteCodeLoadState: 'loading',
myInviteCodeErrMsg: '',
myInviteCodeHint: '',
showInviteCodeDialog: false,
/** 与结果页一致:审核/提审不展示邀请码相关块 */
hideInviteCodeEntry: false
},
onLoad() {
// 分享直达时 silentLogin 可能未完成,先 ensureLogin 再拉推广数据,避免 401 空白
app.ensureLogin()
.then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录后查看推广中心', icon: 'none' })
return
}
const hide = shouldHideInviteCodeEntry(app.globalData || {})
this.setData({ hideInviteCodeEntry: hide })
this.loadStats()
if (!hide) this.loadMyInviteCode()
this.loadBindings(true)
})
.catch(() => {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
})
},
onShow() {
const hide = shouldHideInviteCodeEntry(app.globalData || {})
this.setData({ hideInviteCodeEntry: hide })
const token = app.globalData.token || wx.getStorageSync('token')
if (token) {
this.loadStats()
if (!hide) this.loadMyInviteCode()
}
},
loadMyInviteCode() {
this.setData({
myInviteCodeLoadState: 'loading',
myInviteCodeErrMsg: ''
})
request({
url: '/api/distribution/my-invite-code',
method: 'GET',
success: (res) => {
const payload = res && res.data
const httpOk = res.statusCode >= 200 && res.statusCode < 300
const bizOk = payload && Number(payload.code) === 200
if (!httpOk || !bizOk) {
const msg =
(payload && payload.message) ||
(typeof res.statusCode === 'number' ? `请求失败 (${res.statusCode})` : '加载失败')
this.setData({
myInviteCode: '',
myInviteCodeMeta: null,
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: msg,
myInviteCodeHint: ''
})
return
}
const data = payload.data || {}
const raw = data.code != null ? String(data.code).trim() : ''
const code = raw ? raw.toUpperCase() : ''
const hint = (data.hint && String(data.hint).trim()) || ''
if (code) {
this.setData({
myInviteCode: code,
myInviteCodeMeta: data,
myInviteCodeLoadState: 'ready',
myInviteCodeErrMsg: '',
myInviteCodeHint: ''
})
} else {
this.setData({
myInviteCode: '',
myInviteCodeMeta: data,
myInviteCodeLoadState: 'nodata',
myInviteCodeErrMsg: '',
myInviteCodeHint: hint || '暂无可用邀请码'
})
}
},
fail: () => {
this.setData({
myInviteCode: '',
myInviteCodeMeta: null,
myInviteCodeLoadState: 'error',
myInviteCodeErrMsg: '网络异常,请稍后重试',
myInviteCodeHint: ''
})
}
})
},
retryMyInviteCode() {
this.loadMyInviteCode()
},
copyMyInviteCode() {
const code = (this.data.myInviteCode || '').trim()
if (!code) return
wx.setClipboardData({
data: code,
success: () => wx.showToast({ title: '已复制邀请码', icon: 'success' })
})
},
/** 填写他人邀请码(与结果页支付前弹框同一组件) */
openInviteCodeFill() {
app.ensureLogin().then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
inviteCodeGate.openInviteCodeDialog(this)
})
},
onInviteCodeSkip() {
inviteCodeGate.finishInviteCodeGate(this, true)
},
onInviteCodeSuccess() {
inviteCodeGate.finishInviteCodeGate(this, true)
},
/** 加载推广统计数据 */
loadStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
const title = d.promoCenterTitle || '推广中心'
wx.setNavigationBarTitle({ title })
this.setData({
balance: d.walletBalance,
totalEarned: d.totalEarned,
pendingAmount: d.pendingAmount,
bindingCount: d.bindingCount,
paidCount: d.paidCount,
expiringCount: d.expiringCount,
totalInvite: d.totalInvite,
commissionRate: d.commissionRate,
bindingDays: d.bindingDays,
testCommissionType: d.testCommissionType,
testCommissionRate: d.testCommissionRate,
testCommissionAmount: d.testCommissionAmount,
testNoPayment: d.testNoPayment,
withdrawMinYuan: d.withdrawMinYuan != null ? d.withdrawMinYuan : '0.01',
withdrawMaxYuan: d.withdrawMaxYuan != null && d.withdrawMaxYuan !== '' ? d.withdrawMaxYuan : '',
withdrawFeePct: d.withdrawFeePct != null ? d.withdrawFeePct : 0,
requireWithdrawAudit: d.requireWithdrawAudit !== false,
})
}
}
})
},
/** 加载绑定用户列表tab: 0=绑定中 1=已付款 2=已过期) */
loadBindings(reset = false) {
if (this.data.listLoading) return
if (!reset && this.data.listFinished) return
const page = reset ? 1 : this.data.listPage
this.setData({ listLoading: true })
request({
url: `/api/distribution/bindings?tab=${this.data.activeTab}&page=${page}&pageSize=10`,
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const { list, total } = payload.data
const formatted = (list || []).map(item => ({
...item,
createdAtStr: item.createdAt ? this._fmtTimestamp(item.createdAt) : ''
}))
const newList = reset ? formatted : [...this.data.userList, ...formatted]
this.setData({
userList: newList,
listTotal: total,
listPage: page + 1,
listFinished: newList.length >= total,
})
}
},
complete: () => {
this.setData({ listLoading: false })
}
})
},
/** 时间戳格式化为 YYYY-MM-DD */
_fmtTimestamp(ts) {
if (!ts) return ''
const d = new Date(ts * 1000)
if (isNaN(d.getTime())) return ''
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
},
/** 切换用户列表 Tab */
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index)
if (index === this.data.activeTab) return
this.setData({ activeTab: index, userList: [], listPage: 1, listFinished: false })
this.loadBindings(true)
},
/** 上拉加载更多 */
onReachBottom() {
this.loadBindings(false)
},
/** 申请提现:打开自定义金额弹框 */
handleWithdraw() {
const balance = parseFloat(this.data.balance)
if (!balance || balance <= 0) {
wx.showToast({ title: '暂无可提现余额', icon: 'none' })
return
}
const pct = this.data.withdrawFeePct || 0
const feeYuan = (balance * pct / 100).toFixed(2)
const actualYuan = (balance - balance * pct / 100).toFixed(2)
this.setData({
showWithdrawDialog: true,
withdrawAmountInput: this.data.balance,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 关闭提现弹框 */
closeWithdrawDialog() {
this.setData({
showWithdrawDialog: false,
withdrawError: ''
})
},
/** 输入金额:实时计算手续费与实际到账 */
onWithdrawInput(e) {
const raw = e.detail.value
const val = parseFloat(raw)
const pct = this.data.withdrawFeePct || 0
let feeYuan = '0.00'
let actualYuan = '0.00'
if (raw !== '' && !isNaN(val) && val >= 0) {
const fee = val * pct / 100
feeYuan = fee.toFixed(2)
actualYuan = (val - fee).toFixed(2)
}
this.setData({
withdrawAmountInput: raw,
withdrawFeeYuan: feeYuan,
withdrawActualYuan: actualYuan,
withdrawError: ''
})
},
/** 确认提现(使用用户填写的金额) */
confirmWithdraw() {
const balance = parseFloat(this.data.balance)
const val = parseFloat(this.data.withdrawAmountInput)
const cfgMin = parseFloat(this.data.withdrawMinYuan)
const minYuan = !isNaN(cfgMin) && cfgMin > 0 ? cfgMin : 0.01
const pct = this.data.withdrawFeePct || 0
if (isNaN(val)) {
this.setData({ withdrawError: '请输入正确的金额' })
return
}
if (val + 1e-9 < minYuan) {
this.setData({ withdrawError: `单次提现金额至少 ¥${minYuan.toFixed(2)}` })
return
}
if (val > balance + 1e-9) {
this.setData({ withdrawError: '不可超过当前可提现金额' })
return
}
const actualYuan = val - val * pct / 100
if (actualYuan + 1e-9 < minYuan) {
this.setData({ withdrawError: `实际到账金额不得低于 ¥${minYuan.toFixed(2)}` })
return
}
const amountFen = Math.max(1, Math.round(val * 100))
this.setData({ withdrawError: '' })
try { require('../../utils/analytics').track('tap_promo_withdraw', { amountFen }) } catch (e) {}
request({
url: '/api/distribution/withdraw',
method: 'POST',
data: { amountFen },
success: (r) => {
const payload = r && r.data
if (payload && payload.code === 200) {
const msg = payload.msg || payload.message || ''
wx.showToast({ title: '申请已提交', icon: 'success' })
this.setData({ showWithdrawDialog: false })
this.loadStats()
// 免审核且已自动发起微信转账时,自动进入提现记录页
if (msg.indexOf('已自动发起') !== -1) {
setTimeout(() => {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
}, 500)
}
} else {
const errMsg = (payload && (payload.msg || payload.message)) || '申请失败,请稍后重试'
this.setData({ withdrawError: errMsg })
}
}
})
},
/** 查看提现记录 */
goToWithdrawHistory() {
wx.navigateTo({ url: '/pages/promo/withdrawals' })
},
/** 生成海报 */
generatePoster() {
try { require('../../utils/analytics').track('tap_promo_poster', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/poster' })
},
/** 分享到朋友圈:引导用户使用右上角菜单 */
shareToTimeline() {
try { require('../../utils/analytics').track('tap_promo_share', { channel: 'timeline_hint' }) } catch (e) {}
wx.showToast({ title: '请点击右上角 ··· 选择「分享到朋友圈」', icon: 'none', duration: 2500 })
},
/** 分享给好友 */
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
path: getSharePathByScope('/pages/index/index')
}
},
/** 分享到朋友圈 */
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '神仙团队性格测试 - 发现你的内在潜能',
query: buildShareQuery()
}
}
})