feat: 对齐规则引擎
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { parseScene } = require('./utils/scene.js')
|
const { parseScene } = require('./utils/scene.js')
|
||||||
|
const { checkAndExecute } = require('./utils/ruleEngine.js')
|
||||||
|
|
||||||
App({
|
App({
|
||||||
globalData: {
|
globalData: {
|
||||||
@@ -209,31 +210,6 @@ App({
|
|||||||
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
|
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
|
||||||
},
|
},
|
||||||
|
|
||||||
// 判断用户资料是否完善(昵称 + 头像)
|
|
||||||
_isProfileIncomplete(user) {
|
|
||||||
if (!user) return true
|
|
||||||
const nickname = (user.nickname || '').trim()
|
|
||||||
const avatar = (user.avatar || '').trim()
|
|
||||||
const isDefaultNickname = !nickname || nickname === '微信用户'
|
|
||||||
const noAvatar = !avatar
|
|
||||||
return isDefaultNickname || noAvatar
|
|
||||||
},
|
|
||||||
|
|
||||||
// 登录后若资料未完善,引导跳转到头像昵称引导页
|
|
||||||
_ensureProfileCompletedAfterLogin(user) {
|
|
||||||
try {
|
|
||||||
if (!user || !this._isProfileIncomplete(user)) return
|
|
||||||
const pages = getCurrentPages()
|
|
||||||
const current = pages[pages.length - 1]
|
|
||||||
// 避免在头像昵称页或资料编辑页内重复跳转
|
|
||||||
if (current && (current.route === 'pages/avatar-nickname/avatar-nickname' || current.route === 'pages/profile-edit/profile-edit')) return
|
|
||||||
wx.showToast({ title: '请先完善头像和昵称', icon: 'none', duration: 2000 })
|
|
||||||
wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[App] 跳转资料编辑页失败:', e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 根据业务 id 从 bookData 查 mid(用于跳转)
|
// 根据业务 id 从 bookData 查 mid(用于跳转)
|
||||||
getSectionMid(sectionId) {
|
getSectionMid(sectionId) {
|
||||||
const list = this.globalData.bookData || []
|
const list = this.globalData.bookData || []
|
||||||
@@ -521,8 +497,8 @@ App({
|
|||||||
this.bindReferralCode(pendingRef)
|
this.bindReferralCode(pendingRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录后引导完善资料
|
// 登录后引导完善资料(规则引擎接管,完善头像吸收到规则引擎)
|
||||||
this._ensureProfileCompletedAfterLogin(user)
|
checkAndExecute('after_login', null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.data
|
return res.data
|
||||||
@@ -583,8 +559,8 @@ App({
|
|||||||
this.bindReferralCode(pendingRef)
|
this.bindReferralCode(pendingRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录后引导完善资料
|
// 登录后引导完善资料(规则引擎接管)
|
||||||
this._ensureProfileCompletedAfterLogin(user)
|
checkAndExecute('after_login', null)
|
||||||
}
|
}
|
||||||
return res.data.openId
|
return res.data.openId
|
||||||
}
|
}
|
||||||
@@ -637,8 +613,8 @@ App({
|
|||||||
this.bindReferralCode(pendingRef)
|
this.bindReferralCode(pendingRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录后引导完善资料
|
// 登录后引导完善资料(规则引擎接管)
|
||||||
this._ensureProfileCompletedAfterLogin(user)
|
checkAndExecute('after_login', null)
|
||||||
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||||
|
|
||||||
// 默认匹配类型配置
|
// 默认匹配类型配置
|
||||||
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
||||||
@@ -511,6 +512,8 @@ Page({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 匹配后规则:引导填写 MBTI/行业信息
|
||||||
|
checkAndExecute('after_match', this)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('上报匹配失败:', e)
|
console.log('上报匹配失败:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
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 }
|
||||||
@@ -108,6 +108,9 @@ func Init(dsn string) error {
|
|||||||
if err := db.AutoMigrate(&model.GiftPayRequest{}); err != nil {
|
if err := db.AutoMigrate(&model.GiftPayRequest{}); err != nil {
|
||||||
log.Printf("database: gift_pay_requests migrate warning: %v", err)
|
log.Printf("database: gift_pay_requests migrate warning: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := db.AutoMigrate(&model.UserRule{}); err != nil {
|
||||||
|
log.Printf("database: user_rules migrate warning: %v", err)
|
||||||
|
}
|
||||||
log.Println("database: connected")
|
log.Println("database: connected")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ func DBUserRulesList(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"success": true, "rules": rules})
|
c.JSON(http.StatusOK, gin.H{"success": true, "rules": rules})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MiniprogramUserRulesGet GET /api/miniprogram/user-rules 小程序规则引擎:返回启用的规则,无需鉴权
|
||||||
|
func MiniprogramUserRulesGet(c *gin.Context) {
|
||||||
|
db := database.DB()
|
||||||
|
var rules []model.UserRule
|
||||||
|
if err := db.Where("enabled = ?", true).Order("sort ASC, id ASC").Find(&rules).Error; err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"success": true, "rules": rules})
|
||||||
|
}
|
||||||
|
|
||||||
// DBUserRulesAction POST/PUT/DELETE /api/db/user-rules
|
// DBUserRulesAction POST/PUT/DELETE /api/db/user-rules
|
||||||
func DBUserRulesAction(c *gin.Context) {
|
func DBUserRulesAction(c *gin.Context) {
|
||||||
db := database.DB()
|
db := database.DB()
|
||||||
|
|||||||
@@ -180,6 +180,10 @@ func Setup(cfg *config.Config) *gin.Engine {
|
|||||||
db.DELETE("/link-tags", handler.DBLinkTagDelete)
|
db.DELETE("/link-tags", handler.DBLinkTagDelete)
|
||||||
db.GET("/ckb-leads", handler.DBCKBLeadList)
|
db.GET("/ckb-leads", handler.DBCKBLeadList)
|
||||||
db.GET("/ckb-plan-stats", handler.CKBPlanStats)
|
db.GET("/ckb-plan-stats", handler.CKBPlanStats)
|
||||||
|
db.GET("/user-rules", handler.DBUserRulesList)
|
||||||
|
db.POST("/user-rules", handler.DBUserRulesAction)
|
||||||
|
db.PUT("/user-rules", handler.DBUserRulesAction)
|
||||||
|
db.DELETE("/user-rules", handler.DBUserRulesAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- 分销 -----
|
// ----- 分销 -----
|
||||||
@@ -322,6 +326,8 @@ func Setup(cfg *config.Config) *gin.Engine {
|
|||||||
miniprogram.GET("/about/author", handler.MiniprogramAboutAuthor)
|
miniprogram.GET("/about/author", handler.MiniprogramAboutAuthor)
|
||||||
// 埋点
|
// 埋点
|
||||||
miniprogram.POST("/track", handler.MiniprogramTrackPost)
|
miniprogram.POST("/track", handler.MiniprogramTrackPost)
|
||||||
|
// 规则引擎(用户旅程引导)
|
||||||
|
miniprogram.GET("/user-rules", handler.MiniprogramUserRulesGet)
|
||||||
// 余额
|
// 余额
|
||||||
miniprogram.GET("/balance", handler.BalanceGet)
|
miniprogram.GET("/balance", handler.BalanceGet)
|
||||||
miniprogram.GET("/balance/transactions", handler.BalanceTransactionsGet)
|
miniprogram.GET("/balance/transactions", handler.BalanceTransactionsGet)
|
||||||
|
|||||||
8
soul-api/scripts/add-user-rules-default.sql
Normal file
8
soul-api/scripts/add-user-rules-default.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-- 规则引擎默认数据:插入「注册」规则,供登录后完善头像引导
|
||||||
|
-- 执行:mysql -u user -p db < soul-api/scripts/add-user-rules-default.sql
|
||||||
|
-- 幂等:若已存在 trigger='注册' 则跳过
|
||||||
|
|
||||||
|
INSERT INTO user_rules (title, description, `trigger`, sort, enabled, created_at, updated_at)
|
||||||
|
SELECT '完善个人信息', '设置头像和昵称,让其他创业者更容易认识你', '注册', 1, 1, NOW(), NOW()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM user_rules WHERE `trigger` = '注册' LIMIT 1);
|
||||||
77
开发文档/规则引擎迁移-影响分析.md
Normal file
77
开发文档/规则引擎迁移-影响分析.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# 规则引擎迁移 - 影响分析
|
||||||
|
|
||||||
|
> 乘风严格分析,确保不破坏现有功能。2026-03-17
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、现有逻辑梳理
|
||||||
|
|
||||||
|
### 1. 完善头像逻辑(将被吸收)
|
||||||
|
|
||||||
|
| 调用位置 | 文件 | 行号 | 触发时机 |
|
||||||
|
|----------|------|------|----------|
|
||||||
|
| login 成功 | app.js | 525 | 微信 code 登录成功后 |
|
||||||
|
| getOpenId 登录 | app.js | 587 | getOpenId 时接口返回 user |
|
||||||
|
| loginWithPhone | app.js | 641 | 手机号登录成功后 |
|
||||||
|
|
||||||
|
**逻辑**:`_ensureProfileCompletedAfterLogin(user)` → 若头像/昵称未完善 → Toast + `navigateTo` avatar-nickname
|
||||||
|
|
||||||
|
### 2. 规则引擎将接管
|
||||||
|
|
||||||
|
- **场景**:after_login(对应 trigger「注册」)
|
||||||
|
- **规则**:checkRule_FillAvatar
|
||||||
|
- **行为**:Modal(去完善/稍后再说)→ 确认则 navigateTo
|
||||||
|
- **目标页**:改为 avatar-nickname(与稳定版一致)
|
||||||
|
|
||||||
|
### 3. 破坏性风险点
|
||||||
|
|
||||||
|
| 风险 | 条件 | 后果 | 缓解 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| 规则 API 404 | 后端未部署 user-rules | loadRules 返回 [],无引导 | 先部署后端 |
|
||||||
|
| user_rules 表空 | 无「注册」规则 | isRuleEnabled 为 false,无引导 | SQL 脚本插入默认规则 |
|
||||||
|
| globalData 不兼容 | readCount/hasPurchasedFull | 部分规则不触发 | ruleEngine 内做兼容 |
|
||||||
|
| 目标页错误 | target=profile-edit | 跳错页 | 改为 avatar-nickname |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、兼容性修改清单
|
||||||
|
|
||||||
|
### ruleEngine.js
|
||||||
|
|
||||||
|
| 修改项 | 原值 | 新值 | 原因 |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| checkRule_FillAvatar target | profile-edit | avatar-nickname | 稳定版用 avatar-nickname |
|
||||||
|
| readCount | globalData.readCount | getReadCount() | 稳定版无 readCount |
|
||||||
|
| hasPurchasedFull | globalData.hasPurchasedFull | globalData.hasFullBook | 稳定版用 hasFullBook |
|
||||||
|
|
||||||
|
### 空规则兜底
|
||||||
|
|
||||||
|
当 `rules` 为空时,after_login 不触发 → 用户无引导。**方案**:SQL 脚本插入默认「注册」规则,部署时执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、执行顺序(防破坏)
|
||||||
|
|
||||||
|
1. **soul-api**:新增 GET /api/miniprogram/user-rules
|
||||||
|
2. **soul-api**:确保 user_rules 表存在(GORM AutoMigrate)
|
||||||
|
3. **SQL**:插入默认「注册」规则(幂等)
|
||||||
|
4. **miniprogram**:复制 ruleEngine.js 并做兼容
|
||||||
|
5. **miniprogram**:app.js 替换 _ensureProfileCompletedAfterLogin 为 checkAndExecute
|
||||||
|
6. **miniprogram**:match.js 接入 after_match
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、回滚方案
|
||||||
|
|
||||||
|
若规则引擎导致问题:
|
||||||
|
1. app.js:恢复 _ensureProfileCompletedAfterLogin 调用,注释 checkAndExecute
|
||||||
|
2. match.js:移除 checkAndExecute 调用
|
||||||
|
3. 规则引擎仅新增不删,可随时停用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、部署步骤(2026-03-17 已执行)
|
||||||
|
|
||||||
|
1. **soul-api**:重启后 AutoMigrate 会创建 user_rules 表
|
||||||
|
2. **数据库**:执行 `soul-api/scripts/add-user-rules-default.sql` 插入默认「注册」规则
|
||||||
|
3. **miniprogram**:已接入 ruleEngine,无需额外配置
|
||||||
@@ -83,14 +83,21 @@
|
|||||||
|
|
||||||
原搁置项富文本/打包引导/存客宝均已确认稳定版已有,无新增搁置。
|
原搁置项富文本/打包引导/存客宝均已确认稳定版已有,无新增搁置。
|
||||||
|
|
||||||
### 规则与埋点(待补充,2026-03-17)
|
### 规则引擎(2026-03-17 已迁移)
|
||||||
|
|
||||||
|
| 项 | 状态 |
|
||||||
|
|----|:----:|
|
||||||
|
| ruleEngine.js | ✅ 已接入,兼容 readCount/hasFullBook |
|
||||||
|
| GET /api/miniprogram/user-rules | ✅ soul-api 已新增 |
|
||||||
|
| after_login(完善头像) | ✅ 吸收 _ensureProfileCompletedAfterLogin,跳 avatar-nickname |
|
||||||
|
| after_match(匹配后引导) | ✅ match.js reportMatch 后触发 |
|
||||||
|
| user_rules 表 + 默认规则 | ✅ AutoMigrate + add-user-rules-default.sql |
|
||||||
|
|
||||||
|
### 埋点(待补充)
|
||||||
|
|
||||||
| 项 | 当前状态 | 待办 |
|
| 项 | 当前状态 | 待办 |
|
||||||
|----|----------|------|
|
|----|----------|------|
|
||||||
| **埋点 trackClick** | 已接入:chapters、read、wallet | 遗漏:index、my、match、vip、search、referral 等页的关键操作 |
|
| **埋点 trackClick** | 已接入:chapters、read、wallet | 遗漏:index、my、match、vip、search、referral 等页 |
|
||||||
| **规则引擎 ruleEngine** | 迁移方案「不迁移」,稳定版无 | 若需迁移:ruleEngine.js + GET /api/miniprogram/user-rules(soul-api 需新增 miniprogram 路由) |
|
|
||||||
|
|
||||||
**埋点遗漏页**:match(加好友、加入提交)、index(链接卡若、VIP 等)、my、vip、search、referral、gift-pay 等。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,4 +114,4 @@
|
|||||||
|
|
||||||
**核心迁移已全部完成**:余额体系、代付美团式、埋点、首页/目录/阅读/VIP 相关功能均已落地。
|
**核心迁移已全部完成**:余额体系、代付美团式、埋点、首页/目录/阅读/VIP 相关功能均已落地。
|
||||||
|
|
||||||
**剩余**:导师预约支付(暂不处理);规则与埋点待补充(见上表)。富文本、打包购买引导、存客宝对接均已确认稳定版已有。
|
**剩余**:导师预约支付(暂不处理);埋点待补充(见上表)。规则引擎已迁移,富文本、打包购买引导、存客宝对接均已确认稳定版已有。
|
||||||
|
|||||||
Reference in New Issue
Block a user