feat: 对齐规则引擎
This commit is contained in:
297
miniprogram/utils/ruleEngine.js
Normal file
297
miniprogram/utils/ruleEngine.js
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户旅程规则引擎
|
||||
* 从后端 /api/miniprogram/user-rules 读取启用的规则,按场景触发引导
|
||||
* 稳定版兼容:readCount 用 getReadCount(),hasPurchasedFull 用 hasFullBook,完善头像跳 avatar-nickname
|
||||
*
|
||||
* trigger → scene 映射:
|
||||
* 注册 → after_login
|
||||
* 点击收费章节 → before_read
|
||||
* 完成匹配 → after_match
|
||||
* 完成付款 → after_pay
|
||||
* 累计浏览5章节 → page_show
|
||||
* 加入派对房 → before_join_party
|
||||
* 绑定微信 → after_bindwechat
|
||||
* 收益满50元 → earnings_check
|
||||
* 手动触发 → manual
|
||||
* 浏览导师页 → browse_mentor
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
const RULE_COOLDOWN_KEY = 'rule_engine_cooldown'
|
||||
const COOLDOWN_MS = 60 * 1000
|
||||
let _cachedRules = null
|
||||
let _cacheTs = 0
|
||||
const CACHE_TTL = 5 * 60 * 1000
|
||||
|
||||
const TRIGGER_SCENE_MAP = {
|
||||
'注册': 'after_login',
|
||||
'点击收费章节': 'before_read',
|
||||
'完成匹配': 'after_match',
|
||||
'完成付款': 'after_pay',
|
||||
'累计浏览5章节': 'page_show',
|
||||
'加入派对房': 'before_join_party',
|
||||
'绑定微信': 'after_bindwechat',
|
||||
'收益满50元': 'earnings_check',
|
||||
'手动触发': 'manual',
|
||||
'浏览导师页': 'browse_mentor',
|
||||
}
|
||||
|
||||
function isInCooldown(ruleId) {
|
||||
try {
|
||||
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
|
||||
const ts = map[ruleId]
|
||||
if (!ts) return false
|
||||
return Date.now() - ts < COOLDOWN_MS
|
||||
} catch (e) {
|
||||
console.warn('[RuleEngine] 读取冷却状态失败:', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setCooldown(ruleId) {
|
||||
try {
|
||||
const map = wx.getStorageSync(RULE_COOLDOWN_KEY) || {}
|
||||
map[ruleId] = Date.now()
|
||||
wx.setStorageSync(RULE_COOLDOWN_KEY, map)
|
||||
} catch (e) {
|
||||
console.warn('[RuleEngine] 写入冷却状态失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
return app.globalData.userInfo || {}
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
if (_cachedRules && Date.now() - _cacheTs < CACHE_TTL) return _cachedRules
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/user-rules', method: 'GET', silent: true })
|
||||
if (res && res.success && res.rules) {
|
||||
_cachedRules = res.rules
|
||||
_cacheTs = Date.now()
|
||||
return _cachedRules
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[RuleEngine] 加载规则失败,继续使用缓存:', e)
|
||||
}
|
||||
return _cachedRules || []
|
||||
}
|
||||
|
||||
function isRuleEnabled(rules, triggerName) {
|
||||
return rules.some(r => r.trigger === triggerName)
|
||||
}
|
||||
|
||||
function getRuleInfo(rules, triggerName) {
|
||||
return rules.find(r => r.trigger === triggerName)
|
||||
}
|
||||
|
||||
// 稳定版:跳转 avatar-nickname(与 _ensureProfileCompletedAfterLogin 一致)
|
||||
function checkRule_FillAvatar(rules) {
|
||||
if (!isRuleEnabled(rules, '注册')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const avatar = user.avatar || user.avatarUrl || ''
|
||||
const nickname = user.nickname || ''
|
||||
if (avatar && !avatar.includes('default') && nickname && nickname !== '微信用户' && !nickname.startsWith('微信用户')) return null
|
||||
if (isInCooldown('fill_avatar')) return null
|
||||
setCooldown('fill_avatar')
|
||||
const info = getRuleInfo(rules, '注册')
|
||||
return {
|
||||
ruleId: 'fill_avatar',
|
||||
title: info?.title || '完善个人信息',
|
||||
message: info?.description || '设置头像和昵称,让其他创业者更容易认识你',
|
||||
action: 'navigate',
|
||||
target: '/pages/avatar-nickname/avatar-nickname'
|
||||
}
|
||||
}
|
||||
|
||||
function checkRule_BindPhone(rules) {
|
||||
if (!isRuleEnabled(rules, '点击收费章节')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (user.phone) return null
|
||||
if (isInCooldown('bind_phone')) return null
|
||||
setCooldown('bind_phone')
|
||||
const info = getRuleInfo(rules, '点击收费章节')
|
||||
return {
|
||||
ruleId: 'bind_phone',
|
||||
title: info?.title || '绑定手机号',
|
||||
message: info?.description || '绑定手机号解锁更多功能,保障账户安全',
|
||||
action: 'bind_phone',
|
||||
target: null
|
||||
}
|
||||
}
|
||||
|
||||
function checkRule_FillProfile(rules) {
|
||||
if (!isRuleEnabled(rules, '完成匹配')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (user.mbti && user.industry) return null
|
||||
if (isInCooldown('fill_profile')) return null
|
||||
setCooldown('fill_profile')
|
||||
const info = getRuleInfo(rules, '完成匹配')
|
||||
return {
|
||||
ruleId: 'fill_profile',
|
||||
title: info?.title || '完善创业档案',
|
||||
message: info?.description || '填写 MBTI 和行业信息,帮你精准匹配创业伙伴',
|
||||
action: 'navigate',
|
||||
target: '/pages/profile-edit/profile-edit'
|
||||
}
|
||||
}
|
||||
|
||||
// 稳定版兼容:readCount 用 getReadCount()
|
||||
function checkRule_ShareAfter5Chapters(rules) {
|
||||
if (!isRuleEnabled(rules, '累计浏览5章节')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const readCount = (typeof app.getReadCount === 'function' ? app.getReadCount() : (app.globalData.readCount || 0))
|
||||
if (readCount < 5) return null
|
||||
if (isInCooldown('share_after_5')) return null
|
||||
setCooldown('share_after_5')
|
||||
const info = getRuleInfo(rules, '累计浏览5章节')
|
||||
return {
|
||||
ruleId: 'share_after_5',
|
||||
title: info?.title || '邀请好友一起看',
|
||||
message: info?.description || '你已阅读 ' + readCount + ' 个章节,分享给好友可获得分销收益',
|
||||
action: 'navigate',
|
||||
target: '/pages/referral/referral'
|
||||
}
|
||||
}
|
||||
|
||||
// 稳定版兼容:hasPurchasedFull 用 hasFullBook
|
||||
function checkRule_FillVipInfo(rules) {
|
||||
if (!isRuleEnabled(rules, '完成付款')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (!(app.globalData.hasFullBook || app.globalData.hasPurchasedFull)) return null
|
||||
if (user.wechatId && user.address) return null
|
||||
if (isInCooldown('fill_vip_info')) return null
|
||||
setCooldown('fill_vip_info')
|
||||
const info = getRuleInfo(rules, '完成付款')
|
||||
return {
|
||||
ruleId: 'fill_vip_info',
|
||||
title: info?.title || '填写完整信息',
|
||||
message: info?.description || '购买全书后,需填写完整信息以进入 VIP 群',
|
||||
action: 'navigate',
|
||||
target: '/pages/profile-edit/profile-edit'
|
||||
}
|
||||
}
|
||||
|
||||
function checkRule_JoinParty(rules) {
|
||||
if (!isRuleEnabled(rules, '加入派对房')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (user.projectIntro) return null
|
||||
if (isInCooldown('join_party')) return null
|
||||
setCooldown('join_party')
|
||||
const info = getRuleInfo(rules, '加入派对房')
|
||||
return {
|
||||
ruleId: 'join_party',
|
||||
title: info?.title || '填写项目介绍',
|
||||
message: info?.description || '进入派对房前,引导填写项目介绍和核心需求',
|
||||
action: 'navigate',
|
||||
target: '/pages/profile-edit/profile-edit'
|
||||
}
|
||||
}
|
||||
|
||||
function checkRule_BindWechat(rules) {
|
||||
if (!isRuleEnabled(rules, '绑定微信')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
if (user.wechatId) return null
|
||||
if (isInCooldown('bind_wechat')) return null
|
||||
setCooldown('bind_wechat')
|
||||
const info = getRuleInfo(rules, '绑定微信')
|
||||
return {
|
||||
ruleId: 'bind_wechat',
|
||||
title: info?.title || '绑定微信号',
|
||||
message: info?.description || '绑定微信后,引导开启分销功能',
|
||||
action: 'navigate',
|
||||
target: '/pages/settings/settings'
|
||||
}
|
||||
}
|
||||
|
||||
function checkRule_Withdraw(rules) {
|
||||
if (!isRuleEnabled(rules, '收益满50元')) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const earnings = app.globalData.totalEarnings || 0
|
||||
if (earnings < 50) return null
|
||||
if (isInCooldown('withdraw_50')) return null
|
||||
setCooldown('withdraw_50')
|
||||
const info = getRuleInfo(rules, '收益满50元')
|
||||
return {
|
||||
ruleId: 'withdraw_50',
|
||||
title: info?.title || '可以提现了',
|
||||
message: info?.description || '累计分销收益超过 50 元,快去申请提现吧',
|
||||
action: 'navigate',
|
||||
target: '/pages/referral/referral'
|
||||
}
|
||||
}
|
||||
|
||||
function checkRulesSync(scene, rules) {
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
|
||||
switch (scene) {
|
||||
case 'after_login':
|
||||
return checkRule_FillAvatar(rules)
|
||||
case 'before_read':
|
||||
return checkRule_BindPhone(rules) || checkRule_FillAvatar(rules)
|
||||
case 'after_match':
|
||||
return checkRule_FillProfile(rules) || checkRule_JoinParty(rules)
|
||||
case 'after_pay':
|
||||
return checkRule_FillVipInfo(rules) || checkRule_FillProfile(rules)
|
||||
case 'page_show':
|
||||
return checkRule_FillAvatar(rules) || checkRule_ShareAfter5Chapters(rules) || checkRule_BindWechat(rules) || checkRule_Withdraw(rules)
|
||||
case 'before_join_party':
|
||||
return checkRule_JoinParty(rules)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function executeRule(rule, pageInstance) {
|
||||
if (!rule) return
|
||||
|
||||
wx.showModal({
|
||||
title: rule.title,
|
||||
content: rule.message,
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
if (rule.action === 'navigate' && rule.target) {
|
||||
wx.navigateTo({ url: rule.target })
|
||||
} else if (rule.action === 'bind_phone' && pageInstance) {
|
||||
if (typeof pageInstance.showPhoneBinding === 'function') {
|
||||
pageInstance.showPhoneBinding()
|
||||
}
|
||||
}
|
||||
}
|
||||
_trackRuleAction(rule.ruleId, res.confirm ? 'confirm' : 'cancel')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function _trackRuleAction(ruleId, action) {
|
||||
const userId = getUserInfo().id
|
||||
if (!userId) return
|
||||
app.request({
|
||||
url: '/api/miniprogram/track',
|
||||
method: 'POST',
|
||||
data: { userId, action: 'rule_trigger', target: ruleId, extraData: { result: action } },
|
||||
silent: true
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function checkAndExecute(scene, pageInstance) {
|
||||
const rules = await loadRules()
|
||||
const rule = checkRulesSync(scene, rules)
|
||||
if (rule) {
|
||||
setTimeout(() => executeRule(rule, pageInstance), 800)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkRules: checkRulesSync, executeRule, checkAndExecute, loadRules }
|
||||
Reference in New Issue
Block a user