feat: 完成20260315用户管理3全部5个功能

1. 链接人和事:补充CKB_OPEN_API_KEY/ACCOUNT配置,新增fix-ckb批量创建获客计划API
2. 规则配置:打通DB规则与ruleEngine,新增/api/miniprogram/user-rules接口,
   ruleEngine改为从API动态加载规则并按enabled状态执行
3. 获客计划:修复获客数统计中personId/token不匹配导致永远为0的bug,
   管理端新增"修复CKB密钥"按钮
4. 支付问题:修复钱包充值和代付分享中openId缺失导致400错误,
   添加getOpenId()兜底逻辑
5. 朋友圈分享:shareToMoments改为复制文章前200字+省略号+手指箭头emoji

Made-with: Cursor
This commit is contained in:
卡若
2026-03-15 23:00:42 +08:00
parent 2ebcd0fd70
commit aca006e1b2
58 changed files with 1008 additions and 327 deletions

View File

@@ -1,19 +1,40 @@
/**
* Soul创业派对 - 用户旅程规则引擎
* 在关键页面自动检查用户状态,引导完善信息
*
* 规则逻辑
* 1. 注册后 → 引导填写头像和昵称
* 2. 浏览付费章节 → 引导绑定手机号
* 3. 完成匹配 → 引导填写 MBTI / 行业等
* 4. 购买全书 → 引导填写完整信息(进 VIP 群)
* 5. 累计浏览 5 章 → 引导分享推广
* 从后端 /api/miniprogram/user-rules 读取启用的规则,按场景触发引导
*
* 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 {
@@ -36,104 +57,193 @@ function getUserInfo() {
return app.globalData.userInfo || {}
}
function checkRule_FillAvatar() {
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 {}
return _cachedRules || []
}
function isRuleEnabled(rules, triggerName) {
return rules.some(r => r.trigger === triggerName)
}
function getRuleInfo(rules, triggerName) {
return rules.find(r => r.trigger === triggerName)
}
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 (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: '完善个人信息',
message: '设置头像和昵称,让其他创业者更容易认识你',
title: info?.title || '完善个人信息',
message: info?.description || '设置头像和昵称,让其他创业者更容易认识你',
action: 'navigate',
target: '/pages/profile-edit/profile-edit'
}
}
function checkRule_BindPhone() {
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: '绑定手机号',
message: '绑定手机号解锁更多功能,保障账户安全',
title: info?.title || '绑定手机号',
message: info?.description || '绑定手机号解锁更多功能,保障账户安全',
action: 'bind_phone',
target: null
}
}
function checkRule_FillProfile() {
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: '完善创业档案',
message: '填写 MBTI 和行业信息,帮你精准匹配创业伙伴',
title: info?.title || '完善创业档案',
message: info?.description || '填写 MBTI 和行业信息,帮你精准匹配创业伙伴',
action: 'navigate',
target: '/pages/profile-edit/profile-edit'
}
}
function checkRule_ShareAfter5Chapters() {
function checkRule_ShareAfter5Chapters(rules) {
if (!isRuleEnabled(rules, '累计浏览5章节')) return null
const user = getUserInfo()
if (!user.id) return null
const readCount = 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: '邀请好友一起看',
message: '你已阅读 ' + readCount + ' 个章节,分享给好友可获得分销收益',
title: info?.title || '邀请好友一起看',
message: info?.description || '你已阅读 ' + readCount + ' 个章节,分享给好友可获得分销收益',
action: 'navigate',
target: '/pages/referral/referral'
}
}
/**
* 在指定场景下检查规则
* @param {string} scene - 场景after_login, before_read, after_match, after_pay, page_show
* @returns {object|null} - 触发的规则null 表示无需触发
*/
function checkRules(scene) {
function checkRule_FillVipInfo(rules) {
if (!isRuleEnabled(rules, '完成付款')) return null
const user = getUserInfo()
if (!user.id) return null
if (!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()
return checkRule_FillAvatar(rules)
case 'before_read':
return checkRule_BindPhone() || checkRule_FillAvatar()
return checkRule_BindPhone(rules) || checkRule_FillAvatar(rules)
case 'after_match':
return checkRule_FillProfile()
return checkRule_FillProfile(rules) || checkRule_JoinParty(rules)
case 'after_pay':
return checkRule_FillProfile()
return checkRule_FillVipInfo(rules) || checkRule_FillProfile(rules)
case 'page_show':
return checkRule_FillAvatar() || checkRule_ShareAfter5Chapters()
return checkRule_FillAvatar(rules) || checkRule_ShareAfter5Chapters(rules) || checkRule_BindWechat(rules) || checkRule_Withdraw(rules)
case 'before_join_party':
return checkRule_JoinParty(rules)
default:
return null
}
}
/**
* 执行规则动作
* @param {object} rule - checkRules 返回的规则对象
* @param {object} pageInstance - 页面实例(用于绑定手机号等操作)
*/
function executeRule(rule, pageInstance) {
if (!rule) return
wx.showModal({
title: rule.title,
content: rule.message,
@@ -157,23 +267,20 @@ function executeRule(rule, pageInstance) {
function _trackRuleAction(ruleId, action) {
const userId = getUserInfo().id
if (!userId) return
app.request('/api/miniprogram/track', {
app.request({
url: '/api/miniprogram/track',
method: 'POST',
data: { userId, action: 'rule_trigger', target: ruleId, extraData: { result: action } },
silent: true
}).catch(() => {})
}
/**
* 在页面 onShow 中调用的便捷方法
* @param {string} scene
* @param {object} pageInstance
*/
function checkAndExecute(scene, pageInstance) {
const rule = checkRules(scene)
async function checkAndExecute(scene, pageInstance) {
const rules = await loadRules()
const rule = checkRulesSync(scene, rules)
if (rule) {
setTimeout(() => executeRule(rule, pageInstance), 800)
}
}
module.exports = { checkRules, executeRule, checkAndExecute }
module.exports = { checkRules: checkRulesSync, executeRule, checkAndExecute, loadRules }