Files
soul-yongping/miniprogram/utils/ruleEngine.js
2026-03-17 12:21:33 +08:00

298 lines
9.6 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.

/**
* 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 }