微信小程序:自定义底栏接入 AI 入口(含图标与样式)、AI 聊天相关页面与历史/报告等能力;审核模式与配置联动;资料/企业/首页/授权等页面与 app 入口配合调整。
后端 API:AI 对话/报告/余额告警 等服务与路由;小程序 tabBar 配置与分润相关能力;出站推送 Webhook 与管理端调试;飞书留资、小程序数据分析 等支撑;多份 数据库迁移(AI 聊天、灵魂文章、tabBar、分润等)。 管理端 / 超管:看板、用户/订单/分销/财务、设置与 小程序 tabBar 管理、分润规则、灵魂文章、AI 配置与监控 等大量页面与能力扩展;主题与布局更新。 抖音小程序:与微信侧在 结果页/购买/个人中心 等方向做了同步或优化。 另含 部署/脚本、开发文档 等配套更新。 邀请码与解锁:后端 分销/邀请码 接口与 add_invite_codes 等 SQL;小程序侧 邀请码弹窗组件、个人中心/推广/多类结果页 的填写入口与 解锁门控(inviteCodeGate、unlockGate);并补充 《小程序解锁引导与邀请码方案》 文档。 超管后台:路由与 企业中心(EnterpriseHub) 等交互与结构优化。 小程序体验:在 首页结果、推广、各测评结果页 上继续打磨;与 邀请码门控、审核门控 小步联动。
This commit is contained in:
87
miniprogram/utils/inviteCodeGate.js
Normal file
87
miniprogram/utils/inviteCodeGate.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 支付前邀请码弹框闸门(与开发文档 §3 一致)
|
||||
*/
|
||||
const { shouldHideInviteCodeEntry } = require('./miniprogramAuditGate.js')
|
||||
|
||||
const STORAGE_BOUND = 'inviteCodeBound'
|
||||
const STORAGE_SKIP = 'inviteCodeSkipped'
|
||||
|
||||
function shouldPromptInviteCode() {
|
||||
try {
|
||||
if (wx.getStorageSync(STORAGE_BOUND)) return false
|
||||
if (wx.getStorageSync(STORAGE_SKIP)) return false
|
||||
} catch (e) {}
|
||||
return true
|
||||
}
|
||||
|
||||
function markInviteSkipped() {
|
||||
try {
|
||||
wx.setStorageSync(STORAGE_SKIP, 1)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function markInviteBound() {
|
||||
try {
|
||||
wx.setStorageSync(STORAGE_BOUND, 1)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WechatMiniprogram.Page.TrivialInstance | null} page
|
||||
* @returns {Promise<boolean>} true 表示可继续发起支付
|
||||
*/
|
||||
function ensureInviteCodeGate(page) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const app = getApp()
|
||||
if (shouldHideInviteCodeEntry(app && app.globalData)) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
if (!shouldPromptInviteCode()) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
if (!page || typeof page.setData !== 'function') {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
page._inviteCodeGateResolve = resolve
|
||||
page.setData({ showInviteCodeDialog: true })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WechatMiniprogram.Page.TrivialInstance | null} page
|
||||
* @param {boolean} canProceed
|
||||
*/
|
||||
function finishInviteCodeGate(page, canProceed) {
|
||||
if (page && typeof page.setData === 'function') {
|
||||
page.setData({ showInviteCodeDialog: false })
|
||||
}
|
||||
const fn = page && page._inviteCodeGateResolve
|
||||
if (page) {
|
||||
page._inviteCodeGateResolve = null
|
||||
}
|
||||
if (typeof fn === 'function') {
|
||||
fn(canProceed !== false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 非支付闸门:从推广中心 / 我的 等入口主动打开填写弹框 */
|
||||
function openInviteCodeDialog(page) {
|
||||
if (!page || typeof page.setData !== 'function') return
|
||||
page.setData({ showInviteCodeDialog: true })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldPromptInviteCode,
|
||||
markInviteSkipped,
|
||||
markInviteBound,
|
||||
ensureInviteCodeGate,
|
||||
finishInviteCodeGate,
|
||||
openInviteCodeDialog,
|
||||
STORAGE_BOUND,
|
||||
STORAGE_SKIP
|
||||
}
|
||||
@@ -32,6 +32,16 @@ function isAuditHideAiMode(gd) {
|
||||
return !!(gd.miniprogramAuditMode || gd.maintenanceMode || gd.reviewMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏「填写邀请码 / 展示我的邀请码」等分销向入口(与「我的」页推广区策略一致:
|
||||
* reviewMode / maintenanceMode / miniprogramAuditMode 任一开启即隐藏)
|
||||
*/
|
||||
function shouldHideInviteCodeEntry(gd) {
|
||||
if (TEMP_FORCE_SHOW_ALL_HIDDEN_UI) return false
|
||||
if (!gd) return false
|
||||
return !!(gd.reviewMode || gd.maintenanceMode || gd.miniprogramAuditMode)
|
||||
}
|
||||
|
||||
function redirectIfMiniprogramAudit(message) {
|
||||
const app = getApp()
|
||||
const gd = app && app.globalData
|
||||
@@ -67,6 +77,7 @@ module.exports = {
|
||||
applyAuditUiOverride,
|
||||
shouldHideTabBarHighlightFab,
|
||||
isAuditHideAiMode,
|
||||
shouldHideInviteCodeEntry,
|
||||
redirectIfMiniprogramAudit,
|
||||
ensureRuntimeThenGate
|
||||
}
|
||||
|
||||
110
miniprogram/utils/unlockGate.js
Normal file
110
miniprogram/utils/unlockGate.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 解锁完整报告前的留资门禁:先手机号,再与报告一致的资料齐(isReportProfileComplete)
|
||||
*/
|
||||
const phoneAuth = require('./phoneAuth.js')
|
||||
|
||||
/** scroll-view 的 scroll-into-view 目标:包住解锁/手机号按钮(勿用列表末尾 tail,否则付费墙会被滚出视野) */
|
||||
const GATE_ANCHOR_ID = 'unlock-gate-anchor'
|
||||
|
||||
/** 整页滚动时兜底:可选节点 */
|
||||
const SCROLL_TAIL_ID = 'unlock-scroll-tail'
|
||||
|
||||
function getCurrentPathForRedirect() {
|
||||
const pages = getCurrentPages()
|
||||
const cur = pages[pages.length - 1]
|
||||
if (!cur || !cur.route) return '/pages/index/index'
|
||||
let path = '/' + cur.route
|
||||
const opts = cur.options || {}
|
||||
const keys = Object.keys(opts)
|
||||
if (keys.length === 0) return path
|
||||
const q = keys
|
||||
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(opts[k] == null ? '' : String(opts[k]))}`)
|
||||
.join('&')
|
||||
return `${path}?${q}`
|
||||
}
|
||||
|
||||
function scrollToUnlockAnchor(page, opt) {
|
||||
const scrollIntoView = !!(opt && opt.scrollIntoView)
|
||||
if (scrollIntoView && page && typeof page.setData === 'function') {
|
||||
const key = (opt && opt.scrollTargetDataKey) || 'scrollTarget'
|
||||
const targetId = (opt && opt.scrollIntoViewTargetId) || GATE_ANCHOR_ID
|
||||
page.setData({ [key]: '' }, () => {
|
||||
wx.nextTick(() => {
|
||||
page.setData({ [key]: targetId })
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
const customSelector = opt && opt.anchorSelector
|
||||
wx.nextTick(() => {
|
||||
const query =
|
||||
page && typeof page.createSelectorQuery === 'function'
|
||||
? page.createSelectorQuery()
|
||||
: wx.createSelectorQuery()
|
||||
if (customSelector) {
|
||||
query.select(customSelector).boundingClientRect()
|
||||
} else {
|
||||
query.select('#unlock-gate-anchor').boundingClientRect()
|
||||
query.select(`#${SCROLL_TAIL_ID}`).boundingClientRect()
|
||||
}
|
||||
query.selectViewport().scrollOffset()
|
||||
query.exec((res) => {
|
||||
const viewport = res && res[res.length - 1]
|
||||
let rect = null
|
||||
if (customSelector) {
|
||||
rect = res && res[0]
|
||||
} else {
|
||||
const gate = res && res[0]
|
||||
const tail = res && res[1]
|
||||
const gateOk = gate && typeof gate.top === 'number' && gate.width > 0 && gate.height >= 0
|
||||
const tailOk = tail && typeof tail.top === 'number' && tail.width > 0 && tail.height >= 0
|
||||
rect = gateOk ? gate : tailOk ? tail : null
|
||||
}
|
||||
if (rect && viewport && typeof rect.top === 'number') {
|
||||
const scrollTop = rect.top + viewport.scrollTop - 80
|
||||
wx.pageScrollTo({
|
||||
scrollTop: Math.max(0, scrollTop),
|
||||
duration: 280
|
||||
})
|
||||
} else {
|
||||
wx.pageScrollTo({ scrollTop: 99999, duration: 280 })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {WechatMiniprogram.Page.TrivialInstance} page
|
||||
* @param {object} [opt]
|
||||
* @param {boolean} [opt.scrollIntoView] 结果页用 scroll-view 时为 true
|
||||
* @param {string} [opt.anchorId]
|
||||
* @param {string} [opt.anchorSelector] 整页滚动时用 #id
|
||||
* @param {string} [opt.scrollTargetDataKey]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
function ensureUnlockPrerequisitesBeforePay(page, opt) {
|
||||
if (phoneAuth.hasPhone() && phoneAuth.isReportProfileComplete()) {
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
if (!phoneAuth.hasPhone()) {
|
||||
scrollToUnlockAnchor(page, opt)
|
||||
wx.showToast({ title: '请滑动到解锁区完成手机号授权', icon: 'none' })
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
const redirect = getCurrentPathForRedirect()
|
||||
const url = `/pages/user-profile/index?from=unlock&redirect=${encodeURIComponent(redirect)}`
|
||||
wx.showToast({ title: '请先完善头像与昵称', icon: 'none' })
|
||||
wx.navigateTo({
|
||||
url,
|
||||
fail: () => {
|
||||
wx.showToast({ title: '无法打开资料页', icon: 'none' })
|
||||
}
|
||||
})
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ensureUnlockPrerequisitesBeforePay,
|
||||
scrollToUnlockAnchor,
|
||||
getCurrentPathForRedirect
|
||||
}
|
||||
Reference in New Issue
Block a user