feat: 同步今日小程序与后台迭代版本
集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。 Made-with: Cursor
This commit is contained in:
105
miniprogram/utils/aiChatIntent.js
Normal file
105
miniprogram/utils/aiChatIntent.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 神仙 AI:根据用户输入识别跳转「我的」同源能力(匹配工作 / 了解自己 / 性格测试)
|
||||
*/
|
||||
|
||||
function detectJobIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return (
|
||||
/工作|求职|就业|职业|岗位|职位|面试|跳槽|转行|offer|薪资|薪水|上班|打工|应聘|招聘|简历|适合.+工作|找.+工作|干什么工作|做什么工作|哪类工作|啥工作|职场|事业|工种/i.test(
|
||||
s
|
||||
) || /适合什么样|适合什么类型|MBTI.+工作|性格.+工作/i.test(s)
|
||||
)
|
||||
}
|
||||
|
||||
function detectSelfKnowIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return /了解自己|认识自己|认识自我|内在|潜能|我是什么样|我是谁|读懂自己|深度了解|真实的我|本心|自我认知|探索自己|看清自己|个性|人格|我的特点|我是怎样的人/i.test(
|
||||
s
|
||||
)
|
||||
}
|
||||
|
||||
/** 用户明确想做题 / 测评(与「了解自己」泛聊区分) */
|
||||
function detectPersonalityTestIntent(text) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return false
|
||||
return /性格测试|人格测试|MBTI测试|测MBTI|做测评|做测试|测一测|问卷|量表|测题|测一下|重新测|再测/i.test(s)
|
||||
}
|
||||
|
||||
/**
|
||||
* 求职 > 测评 > 了解自己
|
||||
* @returns {'job'|'test'|'self'|null}
|
||||
*/
|
||||
function resolveChatFeatureIntent(text, hasMbtiType) {
|
||||
const s = String(text || '').trim()
|
||||
if (!s) return null
|
||||
if (detectJobIntent(s)) return 'job'
|
||||
if (detectPersonalityTestIntent(s)) return 'test'
|
||||
if (detectSelfKnowIntent(s)) return 'self'
|
||||
return null
|
||||
}
|
||||
|
||||
/** 助手回复是否与该功能意图相关(避免文不对题的推荐卡片) */
|
||||
function replyMatchesFeatureIntent(intent, assistantText) {
|
||||
const t = String(assistantText || '').trim()
|
||||
if (!t) return false
|
||||
if (intent === 'job') {
|
||||
return /工作|职业|岗位|求职|就业|职场|行业|方向|规划|面试|转行|技能|发展|适配|资源|管理|团队|领导|MBTI|性格.*工作|适合.*工作/i.test(t)
|
||||
}
|
||||
if (intent === 'test') {
|
||||
return /测试|测评|MBTI|问卷|维度|类型|题目|结果|人格|性格|指标|信度|效度/i.test(t)
|
||||
}
|
||||
if (intent === 'self') {
|
||||
return /性格|人格|特质|优势|盲点|内在|情绪|状态|成长|了解自己|认识|适合|关系|伴侣|沟通|需求|模式/i.test(t)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 「了解自己」类:随机打开 了解自己(购买页) 或 性格测试(选测页)。
|
||||
* 尚无 MBTI 结果时略提高去「性格测试」的概率,便于先测评。
|
||||
* @param {boolean} hasMbtiType 是否已有 MBTI 类型(与 ai-chat 页 mbtiType 一致)
|
||||
* @returns {string} 小程序 path
|
||||
*/
|
||||
function pickSelfServicePath(hasMbtiType) {
|
||||
try {
|
||||
const app = getApp()
|
||||
if (app && app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
return '/pages/test-select/index'
|
||||
}
|
||||
} catch (e) {}
|
||||
const r = Math.random()
|
||||
if (!hasMbtiType) {
|
||||
return r < 0.65 ? '/pages/test-select/index' : '/pages/purchase/index'
|
||||
}
|
||||
return r < 0.5 ? '/pages/purchase/index' : '/pages/test-select/index'
|
||||
}
|
||||
|
||||
/**
|
||||
* 阅读数展示:8630 + 距发布日天数×50;≥1万 显示为「x万+」
|
||||
* @param {string} publishedAt 如 2026-04-17
|
||||
*/
|
||||
function formatInlineReadLabel(publishedAt) {
|
||||
const s = String(publishedAt || '').trim()
|
||||
let ts = s ? Date.parse(s.replace(/\./g, '-')) : NaN
|
||||
if (Number.isNaN(ts)) ts = Date.now()
|
||||
const days = Math.max(0, Math.floor((Date.now() - ts) / 86400000))
|
||||
const raw = 8630 + days * 50
|
||||
if (raw >= 10000) {
|
||||
const wan = raw / 10000
|
||||
const rounded = Math.round(wan * 10) / 10
|
||||
return `${rounded}万+`
|
||||
}
|
||||
return `${raw}阅读`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectJobIntent,
|
||||
detectSelfKnowIntent,
|
||||
detectPersonalityTestIntent,
|
||||
resolveChatFeatureIntent,
|
||||
replyMatchesFeatureIntent,
|
||||
pickSelfServicePath,
|
||||
formatInlineReadLabel
|
||||
}
|
||||
101
miniprogram/utils/deepPricingFilter.js
Normal file
101
miniprogram/utils/deepPricingFilter.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 「了解自己」深度服务页:按 enterprisePermissions(与 /api/config/runtime 一致)过滤套餐权益文案。
|
||||
* permissions 为 null 时视为全开(个人未绑定企业)。
|
||||
*/
|
||||
|
||||
const TEST_ORDER = [
|
||||
{ key: 'mbti', label: 'MBTI' },
|
||||
{ key: 'disc', label: 'DISC' },
|
||||
{ key: 'pdp', label: 'PDP' },
|
||||
{ key: 'sbti', label: 'SBTI' },
|
||||
{ key: 'face', label: '面相' }
|
||||
]
|
||||
|
||||
/** @param {Record<string, any>|null|undefined} p */
|
||||
function isPermOn(key, p) {
|
||||
if (!p || typeof p !== 'object') return true
|
||||
return p[key] !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* 顶部副标题里「结合 xxx 的综合解读」动态片段
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function buildTestsLabelForHero(perms) {
|
||||
const parts = []
|
||||
for (const { key, label } of TEST_ORDER) {
|
||||
if (isPermOn(key, perms)) parts.push(label)
|
||||
}
|
||||
if (parts.length === 0) return '定制测评组合'
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整 Hero 说明(第二行长文案)
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function buildDeepHeroDesc(perms) {
|
||||
const label = buildTestsLabelForHero(perms)
|
||||
return `结合 ${label} 的综合解读;个人 1v1、团队工作坊、VIP 职业发展三档方案。提交后顾问会主动与你联系。`
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否至少有一项性格/面相类能力开启(用于「综合报告」「职业」等泛化权益)
|
||||
* @param {Record<string, any>|null|undefined} p
|
||||
*/
|
||||
function anyPersonalityOn(p) {
|
||||
if (!p || typeof p !== 'object') return true
|
||||
return TEST_ORDER.some(({ key }) => p[key] !== false)
|
||||
}
|
||||
|
||||
// 单行权益与权限键:命中正则则受对应开关控制
|
||||
const FEATURE_LINE_RULES = [
|
||||
{ re: /面相|面部分析|面部|三张照片|照片\+问卷/i, key: 'face' },
|
||||
{ re: /\bMBTI\b|MBTI性格|16型/i, key: 'mbti' },
|
||||
{ re: /\bDISC\b|DISC沟通/i, key: 'disc' },
|
||||
{ re: /\bPDP\b|PDP行为/i, key: 'pdp' },
|
||||
{ re: /盖洛普|SBTI|sbti/i, key: 'sbti' }
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {string[]} features
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function filterFeatureLines(features, perms) {
|
||||
if (!Array.isArray(features) || !features.length) return []
|
||||
return features.filter((line) => {
|
||||
const s = String(line || '').trim()
|
||||
if (!s) return false
|
||||
for (const { re, key } of FEATURE_LINE_RULES) {
|
||||
if (re.test(s)) return isPermOn(key, perms)
|
||||
}
|
||||
if (/多维度|综合.*性格|优势解读|潜在盲区/i.test(s)) return anyPersonalityOn(perms)
|
||||
if (/职业|发展方向|匹配.*职业/i.test(s)) return anyPersonalityOn(perms)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any[]} categories
|
||||
* @param {Record<string, any>|null|undefined} perms
|
||||
*/
|
||||
function filterCategories(categories, perms) {
|
||||
if (!Array.isArray(categories)) return []
|
||||
return categories.map((cat) => {
|
||||
const copy = Object.assign({}, cat)
|
||||
if (Array.isArray(copy.features)) {
|
||||
copy.features = filterFeatureLines(copy.features, perms)
|
||||
}
|
||||
return copy
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildTestsLabelForHero,
|
||||
buildDeepHeroDesc,
|
||||
filterFeatureLines,
|
||||
filterCategories,
|
||||
isPermOn,
|
||||
anyPersonalityOn
|
||||
}
|
||||
46
miniprogram/utils/miniprogramAuditGate.js
Normal file
46
miniprogram/utils/miniprogramAuditGate.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 超管审核相关:隐藏神仙 AI Tab/入口;提审模式下另由后端/各页关闭虚拟商品与「了解自己」深度套餐。
|
||||
* - miniprogramAuditMode:提审专用(含虚拟支付合规)
|
||||
* - maintenanceMode / reviewMode:面相审核(用户口语「审核模式」常指其一)
|
||||
*/
|
||||
|
||||
function isAuditHideAiMode(gd) {
|
||||
if (!gd) return false
|
||||
return !!(gd.miniprogramAuditMode || gd.maintenanceMode || gd.reviewMode)
|
||||
}
|
||||
|
||||
function redirectIfMiniprogramAudit(message) {
|
||||
const app = getApp()
|
||||
const gd = app && app.globalData
|
||||
if (!isAuditHideAiMode(gd)) return false
|
||||
wx.showToast({ title: message || '功能升级中', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 300)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 先拉 runtime(若存在),再若提审则踢回首页;否则执行 callback。
|
||||
*/
|
||||
function ensureRuntimeThenGate(callback) {
|
||||
const app = getApp()
|
||||
const run = () => {
|
||||
if (isAuditHideAiMode(app.globalData)) {
|
||||
redirectIfMiniprogramAudit()
|
||||
return
|
||||
}
|
||||
if (typeof callback === 'function') callback()
|
||||
}
|
||||
if (app && typeof app.getRuntimeConfig === 'function') {
|
||||
app.getRuntimeConfig().then(run).catch(run)
|
||||
return
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAuditHideAiMode,
|
||||
redirectIfMiniprogramAudit,
|
||||
ensureRuntimeThenGate
|
||||
}
|
||||
@@ -71,6 +71,13 @@ function generateOrderId(productType) {
|
||||
function wxPay(options) {
|
||||
const { orderId, amount = 0, description, productType, testResultId, deepProductId, enterpriseId, success, fail } = options
|
||||
|
||||
if (app.globalData && app.globalData.miniprogramAuditMode) {
|
||||
const msg = '版本审核期间不可发起支付'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
if (typeof fail === 'function') fail(new Error(msg))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const analyticsMod = require('./analytics')
|
||||
if (analyticsMod && typeof analyticsMod.track === 'function') {
|
||||
|
||||
@@ -3,6 +3,67 @@
|
||||
* 手机号与个人资料:详见 isProfileComplete 规则
|
||||
*/
|
||||
|
||||
/** 本会话/本机「已用手机号授权登录」标记(与是否库里已有手机无关;换 userId 自动失效) */
|
||||
const PHONE_LOGIN_SESSION_KEY = 'mbti_phone_login_v1'
|
||||
const PHONE_LOGIN_TTL_MS = 90 * 86400000
|
||||
|
||||
function getLoginUserId() {
|
||||
try {
|
||||
const app = getApp()
|
||||
const u = (app && app.globalData && app.globalData.userInfo) || wx.getStorageSync('userInfo')
|
||||
if (!u || typeof u !== 'object') return ''
|
||||
const id = u.id != null ? u.id : u.userId
|
||||
return id != null && id !== '' ? String(id) : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 与当前登录用户一致且未过期时,视为已在结果页完成过「手机登录」授权
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasPhoneLoginSession() {
|
||||
const uid = getLoginUserId()
|
||||
if (!uid) return false
|
||||
try {
|
||||
const row = wx.getStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
if (!row || typeof row !== 'object') return false
|
||||
if (String(row.uid || '') !== uid) return false
|
||||
const at = Number(row.at || 0)
|
||||
if (at > 0 && Date.now() - at > PHONE_LOGIN_TTL_MS) return false
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function markPhoneLoginSession() {
|
||||
const uid = getLoginUserId()
|
||||
if (!uid) return
|
||||
try {
|
||||
wx.setStorageSync(PHONE_LOGIN_SESSION_KEY, { uid, at: Date.now() })
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function clearPhoneLoginSession() {
|
||||
try {
|
||||
wx.removeStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/** 静默登录写入新用户后调用:userId 变化则清掉旧会话标记 */
|
||||
function syncPhoneLoginStorageWithUser(user) {
|
||||
const uid = user && (user.id != null ? String(user.id) : (user.userId != null ? String(user.userId) : ''))
|
||||
if (!uid) return
|
||||
try {
|
||||
const row = wx.getStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
if (row && row.uid && String(row.uid) !== uid) {
|
||||
wx.removeStorageSync(PHONE_LOGIN_SESSION_KEY)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人资料是否已满足业务门禁(避免反复跳转「完善资料」)
|
||||
* - 已绑定手机号:视为可用(付费/深度服务以手机为准;仅首字头像无 URL 不再卡死)
|
||||
@@ -33,6 +94,25 @@ function isReportProfileComplete() {
|
||||
return nickname.length > 0 && avatar.length > 0 && phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 问卷/结果页预览门禁:分享落地不遮挡;否则需手机号+昵称+头像齐全后才可看全文
|
||||
*/
|
||||
function needsResultProfileGate(fromShare) {
|
||||
if (fromShare) return false
|
||||
return !isReportProfileComplete()
|
||||
}
|
||||
|
||||
/**
|
||||
* 结果页 getPhoneNumber 成功后:若资料未齐,进入「我的-资料」与查看全文同一套规则
|
||||
*/
|
||||
function navigateToCompleteProfileAfterPhoneIfNeeded() {
|
||||
if (isReportProfileComplete()) return
|
||||
wx.showToast({ title: '请完善头像与昵称', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index?from=result_gate', fail: () => {} })
|
||||
}, 450)
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
@@ -104,6 +184,7 @@ function bindPhoneByCode(code) {
|
||||
const newUser = { ...user, phone }
|
||||
app.globalData.userInfo = newUser
|
||||
wx.setStorageSync('userInfo', newUser)
|
||||
markPhoneLoginSession()
|
||||
wx.showToast({ title: '授权成功', icon: 'success' })
|
||||
resolve(newUser)
|
||||
} else {
|
||||
@@ -121,10 +202,26 @@ function bindPhoneByCode(code) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 测评提交后直接进结果页;完整报告是否在结果页展示由「资料齐全」门禁控制(见 needsResultProfileGate / isReportProfileComplete)
|
||||
* @param {string} targetUrl 必须以 / 开头的本地路径,如 /pages/result/mbti?id=1&type=mbti
|
||||
*/
|
||||
function afterTestSubmitNavigate(targetUrl) {
|
||||
const url = (targetUrl && String(targetUrl).trim()) || '/pages/index/index'
|
||||
wx.redirectTo({ url, fail: () => wx.reLaunch({ url: '/pages/index/index' }) })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
isReportProfileComplete,
|
||||
needsResultProfileGate,
|
||||
navigateToCompleteProfileAfterPhoneIfNeeded,
|
||||
hasPhoneLoginSession,
|
||||
markPhoneLoginSession,
|
||||
clearPhoneLoginSession,
|
||||
syncPhoneLoginStorageWithUser,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
afterTestSubmitNavigate,
|
||||
}
|
||||
|
||||
27
miniprogram/utils/recoMiniProgram.js
Normal file
27
miniprogram/utils/recoMiniProgram.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 精选推荐 → 跳转其他小程序 path(与「一场 soul」阅读页 pages/read/read?id= 对齐)
|
||||
* @param {string} basePath 后台配置的 path,无 sourceId 时使用
|
||||
* @param {{ id?: string|number, title?: string, sourceId?: string|number, fromTag?: string }} opts
|
||||
*/
|
||||
function buildRecoMiniProgramPath(basePath, opts) {
|
||||
const id = opts && opts.id != null ? String(opts.id).trim() : ''
|
||||
const title = opts && opts.title != null ? String(opts.title).trim() : ''
|
||||
const sourceId = opts && opts.sourceId != null ? String(opts.sourceId).trim() : ''
|
||||
const fromTag = (opts && opts.fromTag) || 'mbti_reco'
|
||||
const parts = ['from=' + encodeURIComponent(fromTag)]
|
||||
if (id) parts.push('mbtiArticleId=' + encodeURIComponent(id))
|
||||
if (title) parts.push('title=' + encodeURIComponent(title.slice(0, 200)))
|
||||
|
||||
if (sourceId) {
|
||||
let path = 'pages/read/read?id=' + encodeURIComponent(sourceId)
|
||||
return path + '&' + parts.join('&')
|
||||
}
|
||||
let path = (basePath || 'pages/index/index').replace(/^\//, '')
|
||||
if (!path) path = 'pages/index/index'
|
||||
const sep = path.indexOf('?') >= 0 ? '&' : '?'
|
||||
return path + sep + parts.join('&')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildRecoMiniProgramPath
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 统一请求封装:自动拼接 baseURL、携带 token、处理 401
|
||||
* 使用前需已执行 App(),否则 getApp() 在 require 时可能未就绪,这里在请求时再取 app
|
||||
* 401 时先尝试静默登录一次并重试,减少 token 过期/首屏竞态导致的误 401
|
||||
*/
|
||||
function getAppSafe() {
|
||||
try {
|
||||
@@ -12,7 +12,17 @@ function getAppSafe() {
|
||||
|
||||
function getToken() {
|
||||
const app = getAppSafe()
|
||||
return (app && app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
|
||||
const g = app && app.globalData && app.globalData.token
|
||||
if (g) return g
|
||||
try {
|
||||
const st = wx.getStorageSync('token')
|
||||
if (st && app && app.globalData) {
|
||||
app.globalData.token = st
|
||||
}
|
||||
return st || ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getApiBase() {
|
||||
@@ -20,9 +30,6 @@ function getApiBase() {
|
||||
return (app && app.globalData && app.globalData.apiBase) || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除登录态(401 或主动退出时调用)
|
||||
*/
|
||||
function clearLoginState() {
|
||||
const app = getAppSafe()
|
||||
if (app && app.globalData) {
|
||||
@@ -37,17 +44,29 @@ function clearLoginState() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起请求
|
||||
* @param {Object} options - 同 wx.request,url 可为相对路径(自动加 apiBase)
|
||||
* @param {boolean} options.needAuth - 是否携带 Authorization,默认 true
|
||||
* @param {boolean} options.allow401 - 401 时是否静默清除登录态而不 fail,默认 true
|
||||
* @param {Object} options - 同 wx.request
|
||||
* @param {boolean} options.needAuth - 是否带 Authorization,默认 true
|
||||
* @param {boolean} options.optionalAuth
|
||||
* @param {boolean} options.allow401 - 401 且已带 Bearer 时是否清登录态(重试失败后),默认 true
|
||||
*/
|
||||
function request(options) {
|
||||
const apiBase = getApiBase()
|
||||
const url = options.url
|
||||
const fullUrl = url.startsWith('http') ? url : `${apiBase.replace(/\/$/, '')}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
const {
|
||||
__didAuthRetry,
|
||||
success: userSuccess,
|
||||
fail: userFail,
|
||||
complete: userComplete,
|
||||
url: optUrl,
|
||||
...rest
|
||||
} = options
|
||||
|
||||
const fullUrl = optUrl.startsWith('http')
|
||||
? optUrl
|
||||
: `${apiBase.replace(/\/$/, '')}${optUrl.startsWith('/') ? '' : '/'}${optUrl}`
|
||||
const needAuth = options.needAuth !== false
|
||||
const optionalAuth = options.optionalAuth === true
|
||||
const allow401 = options.allow401 !== false
|
||||
const didRetry = __didAuthRetry === true
|
||||
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -56,34 +75,58 @@ function request(options) {
|
||||
if (needAuth) {
|
||||
const token = getToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
} else if (optionalAuth) {
|
||||
const token = getToken()
|
||||
if (token) header['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const success = options.success
|
||||
const fail = options.fail
|
||||
const complete = options.complete
|
||||
const authHeaderSent = !!header['Authorization']
|
||||
|
||||
return wx.request({
|
||||
...options,
|
||||
wx.request({
|
||||
...rest,
|
||||
url: fullUrl,
|
||||
header,
|
||||
success(res) {
|
||||
if (res.statusCode === 401 && allow401) {
|
||||
if (
|
||||
res.statusCode === 401 &&
|
||||
allow401 &&
|
||||
authHeaderSent &&
|
||||
needAuth &&
|
||||
!didRetry
|
||||
) {
|
||||
const app = getAppSafe()
|
||||
if (app && typeof app.silentLogin === 'function') {
|
||||
app
|
||||
.silentLogin()
|
||||
.then((loginOk) => {
|
||||
if (loginOk && getToken()) {
|
||||
request({ ...options, __didAuthRetry: true })
|
||||
} else {
|
||||
clearLoginState()
|
||||
if (userSuccess) userSuccess(res)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
clearLoginState()
|
||||
if (userSuccess) userSuccess(res)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if (res.statusCode === 401 && allow401 && authHeaderSent) {
|
||||
clearLoginState()
|
||||
}
|
||||
if (success) success(res)
|
||||
if (userSuccess) userSuccess(res)
|
||||
},
|
||||
fail(err) {
|
||||
if (fail) fail(err)
|
||||
if (userFail) userFail(err)
|
||||
},
|
||||
complete(res) {
|
||||
if (complete) complete(res)
|
||||
if (userComplete) userComplete(res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise 版 request,便于 async/await
|
||||
*/
|
||||
function requestPromiseOnce(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request({
|
||||
@@ -110,7 +153,6 @@ function requestPromiseOnce(options) {
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
// wx.request fail:网络不可达 / CONNECTION_CLOSED / TLS / 超时
|
||||
const e = new Error((err && err.errMsg) || '网络异常')
|
||||
e.isNetworkError = true
|
||||
reject(e)
|
||||
@@ -119,23 +161,20 @@ function requestPromiseOnce(options) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 带指数退避重试(默认 2 次):仅对"网络异常"或 5xx 重试,4xx 不重试
|
||||
* 用法:requestPromise({ url, ...opts, retry: 2, retryDelayMs: 400 })
|
||||
*/
|
||||
function requestPromise(options) {
|
||||
const retry = options.retry == null ? 2 : Number(options.retry) || 0
|
||||
const retryDelayMs = options.retryDelayMs == null ? 400 : Number(options.retryDelayMs) || 0
|
||||
let attempt = 0
|
||||
const run = () => requestPromiseOnce(options).catch((err) => {
|
||||
const retriable = err && (err.isNetworkError || (err.statusCode >= 500 && err.statusCode < 600))
|
||||
if (!retriable || attempt >= retry) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
attempt += 1
|
||||
const wait = retryDelayMs * Math.pow(2, attempt - 1)
|
||||
return new Promise((r) => setTimeout(r, wait)).then(run)
|
||||
})
|
||||
const run = () =>
|
||||
requestPromiseOnce(options).catch((err) => {
|
||||
const retriable = err && (err.isNetworkError || (err.statusCode >= 500 && err.statusCode < 600))
|
||||
if (!retriable || attempt >= retry) {
|
||||
return Promise.reject(err)
|
||||
}
|
||||
attempt += 1
|
||||
const wait = retryDelayMs * Math.pow(2, attempt - 1)
|
||||
return new Promise((r) => setTimeout(r, wait)).then(run)
|
||||
})
|
||||
return run()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 结果页「按步骤解锁」状态:
|
||||
* Step 1 · 看全文(需完善资料或付费)
|
||||
* Step 1 · 看全文(需手机号+头像+昵称齐全,或已付费)
|
||||
* Step 2 · 分享朋友圈(Step 1 完成后启用)
|
||||
* Step 3 · AI 拍照测试(Step 2 完成后启用)
|
||||
*
|
||||
@@ -27,7 +27,7 @@ function write(obj) {
|
||||
|
||||
/**
|
||||
* Step 1 是否已解锁
|
||||
* 条件:资料完善(profileGate===false)且(免费或已付费)
|
||||
* 条件:资料已齐(profileGate===false,与 isReportProfileComplete 一致)且(免费或已付费)
|
||||
*/
|
||||
function isStep1Unlocked({ profileGate, payRequired, isPaid }) {
|
||||
if (profileGate) return false
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 结果页资料门禁:未完善头像+昵称+手机时,客户端展示约 30% 预览(与后端 locked 互补)
|
||||
* 结果页门禁:非分享落地且资料未齐(手机+头像+昵称)时展示约 30% 预览
|
||||
*/
|
||||
const { isReportProfileComplete } = require('./phoneAuth.js')
|
||||
const { needsResultProfileGate } = require('./phoneAuth.js')
|
||||
|
||||
/** @param {WechatMiniprogram.Page.TrivialInstance} page */
|
||||
function setProfileGateOnPage(page) {
|
||||
const fromShare = !!(page.data && page.data.fromShare)
|
||||
const gate = !fromShare && !isReportProfileComplete()
|
||||
const gate = needsResultProfileGate(fromShare)
|
||||
page.setData({ profileGate: gate })
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ function openTimelineShareHint() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isReportProfileComplete,
|
||||
setProfileGateOnPage,
|
||||
slicePreviewText,
|
||||
slicePreviewList,
|
||||
|
||||
Reference in New Issue
Block a user