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

@@ -83,14 +83,16 @@ Page({
async onLoad(options) {
wx.showShareMenu({ withShareTimeline: true })
// 预加载 linkTags、linkedMiniprograms供 onLinkTagTap 用密钥查 appId
if (!app.globalData.linkTagsConfig || !app.globalData.linkedMiniprograms) {
app.request({ url: '/api/miniprogram/config', silent: true }).then(cfg => {
// 预加载 linkTags、linkedMiniprograms、persons(供 onLinkTagTap / onMentionTap 和内容自动匹配用
if (!app.globalData.linkTagsConfig || !app.globalData.linkedMiniprograms || !app.globalData.personsConfig) {
try {
const cfg = await app.request({ url: '/api/miniprogram/config', silent: true })
if (cfg) {
if (Array.isArray(cfg.linkTags)) app.globalData.linkTagsConfig = cfg.linkTags
if (Array.isArray(cfg.linkedMiniprograms)) app.globalData.linkedMiniprograms = cfg.linkedMiniprograms
if (Array.isArray(cfg.persons)) app.globalData.personsConfig = cfg.persons
}
}).catch(() => {})
} catch (e) {}
}
// 支持 scene扫码、mid、id、ref
@@ -270,7 +272,8 @@ Page({
// 已解锁用 data.content完整内容未解锁用 content预览先 determineAccessState 再 loadContent 保证顺序正确
const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content
if (res && displayContent) {
const { lines, segments } = contentParser.parseContent(displayContent)
const parserConfig = { persons: app.globalData.personsConfig || [], linkTags: app.globalData.linkTagsConfig || [] }
const { lines, segments } = contentParser.parseContent(displayContent, parserConfig)
// 预览内容由后端统一截取比例,这里展示全部预览内容
const previewCount = lines.length
const updates = {
@@ -294,7 +297,8 @@ Page({
try {
const cached = wx.getStorageSync(cacheKey)
if (cached && cached.content) {
const { lines, segments } = contentParser.parseContent(cached.content)
const cachedParserConfig = { persons: app.globalData.personsConfig || [], linkTags: app.globalData.linkTagsConfig || [] }
const { lines, segments } = contentParser.parseContent(cached.content, cachedParserConfig)
// 预览内容由后端统一截取比例,这里展示全部预览内容
const previewCount = lines.length
this.setData({
@@ -551,15 +555,30 @@ Page({
}
const linked = (app.globalData.linkedMiniprograms || []).find(m => m.key === mpKey)
const targetAppId = (linked && linked.appId) ? linked.appId : (appId || mpKey || '')
const selfAppId = (app.globalData.config?.mpConfig?.appId || app.globalData.appId || 'wxb8bbb2b10dec74aa')
const targetPath = pagePath || (linked && linked.path) || ''
if (targetAppId === selfAppId || !targetAppId) {
if (targetPath) {
const navPath = targetPath.startsWith('/') ? targetPath : '/' + targetPath
wx.navigateTo({ url: navPath, fail: () => wx.switchTab({ url: navPath }) })
} else {
wx.switchTab({ url: '/pages/index/index' })
}
return
}
if (targetAppId) {
wx.navigateToMiniProgram({
appId: targetAppId,
path: pagePath || (linked && linked.path) || '',
path: targetPath,
envVersion: 'release',
success: () => {},
fail: (err) => {
console.warn('[LinkTag] 小程序跳转失败:', err)
wx.showToast({ title: '跳转失败,请检查小程序配置', icon: 'none' })
if (targetPath) {
wx.navigateTo({ url: targetPath.startsWith('/') ? targetPath : '/' + targetPath, fail: () => {} })
} else {
wx.showToast({ title: '跳转失败,请检查小程序配置', icon: 'none' })
}
},
})
return
@@ -596,9 +615,17 @@ Page({
// 点击正文中的 @某人:确认弹窗 → 登录/资料校验 → 调用 ckb/lead 加好友留资
onMentionTap(e) {
const userId = e.currentTarget.dataset.userId
let userId = e.currentTarget.dataset.userId
const nickname = (e.currentTarget.dataset.nickname || '').trim() || 'TA'
if (!userId) return
if (!userId && nickname !== 'TA') {
const persons = app.globalData.personsConfig || []
const match = persons.find(p => p.name === nickname || (p.aliases || '').split(',').map(a => a.trim()).includes(nickname))
if (match) userId = match.personId || ''
}
if (!userId) {
wx.showToast({ title: `暂无 @${nickname} 的信息`, icon: 'none' })
return
}
wx.showModal({
title: '添加好友',
content: `是否添加 @${nickname} `,
@@ -629,19 +656,21 @@ Page({
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
let avatar = (app.globalData.userInfo.avatar || app.globalData.userInfo.avatarUrl || '').trim()
if (!phone && !wechatId) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || '').trim()
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
if (!avatar) avatar = (profileRes.data.avatar || '').trim()
}
} catch (e) {}
}
if (!phone && !wechatId) {
if ((!phone && !wechatId) || !avatar) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号或微信号,以便对方联系您',
content: !avatar ? '请先设置头像和填写联系方式,以便对方联系您' : '请先填写手机号或微信号,以便对方联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
@@ -706,19 +735,21 @@ Page({
}
let phone = (app.globalData.userInfo.phone || '').trim()
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
let avatar = (app.globalData.userInfo.avatar || app.globalData.userInfo.avatarUrl || '').trim()
if (!phone && !wechatId) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || '').trim()
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
if (!avatar) avatar = (profileRes.data.avatar || '').trim()
}
} catch (e) {}
}
if (!phone && !wechatId) {
if ((!phone && !wechatId) || !avatar) {
wx.showModal({
title: '完善资料',
content: '请先填写手机号或微信号,以便对方联系您',
content: !avatar ? '请先设置头像和填写联系方式,以便对方联系您' : '请先填写手机号或微信号,以便对方联系您',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
@@ -832,11 +863,18 @@ Page({
},
shareToMoments() {
wx.showModal({
title: '分享到朋友圈',
content: '点击右上角「···」菜单,选择「分享到朋友圈」即可。\n\n朋友圈分享文案已自动生成。',
showCancel: false,
confirmText: '知道了',
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
const raw = (this.data.content || '').replace(/[#@]\S+/g, '').replace(/\s+/g, ' ').trim()
const excerpt = raw.length > 200 ? raw.slice(0, 200) + '……' : raw.length > 100 ? raw + '……' : raw
const copyText = `${title}\n\n${excerpt}\n\n👉 来自「Soul创业派对」`
wx.setClipboardData({
data: copyText,
success: () => {
wx.showToast({ title: '文案已复制,去朋友圈粘贴发布', icon: 'none', duration: 2500 })
},
fail: () => {
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
}
})
},
@@ -1511,104 +1549,125 @@ Page({
wx.showModal({
title: '代付分享',
content: `为好友代付本章 ¥${price}\n\n支付后将生成代付链接,好友点击即可免费阅读`,
confirmText: '微信支付',
cancelText: '用余额',
content: `为好友代付本章 ¥${price}\n支付后将生成代付链接,好友点击即可免费阅读`,
confirmText: '确认代付',
cancelText: '取消',
success: async (res) => {
if (!res.confirm && !res.cancel) return
if (res.confirm) {
// Direct WeChat Pay
wx.showLoading({ title: '创建订单...' })
try {
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: app.globalData.openId,
productType: 'gift',
productId: sectionId,
amount: price,
description: `代付解锁:${this.data.section?.title || sectionId}`,
userId: userId,
}
})
wx.hideLoading()
if (payRes && payRes.payParams) {
wx.requestPayment({
...payRes.payParams,
success: async () => {
// After payment, create gift code via balance gift API
// First confirm recharge to add to balance, then deduct for gift
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已为好友代付 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
}
} catch (e) {
wx.showToast({ title: '生成分享链接失败', icon: 'none' })
}
},
fail: () => { wx.showToast({ title: '支付取消', icon: 'none' }) }
})
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
if (!res.confirm) return
wx.showActionSheet({
itemList: ['微信支付', '用余额支付'],
success: async (actionRes) => {
if (actionRes.tapIndex === 0) {
this._giftPayViaWechat(sectionId, userId, price)
} else if (actionRes.tapIndex === 1) {
this._giftPayViaBalance(sectionId, userId, price)
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '支付失败', icon: 'none' })
}
} else {
// Use balance (existing flow)
const balRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }).catch(() => null)
const balance = (balRes && balRes.data) ? balRes.data.balance : 0
if (balance < price) {
wx.showModal({
title: '余额不足',
content: `当前余额 ¥${balance.toFixed(2)},需要 ¥${price}\n请先充值`,
confirmText: '去充值',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/wallet/wallet' }) }
})
return
}
wx.showLoading({ title: '处理中...' })
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
wx.hideLoading()
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已从余额扣除 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '代付失败', icon: 'none' })
}
}
})
}
})
},
async _giftPayViaWechat(sectionId, userId, price) {
let openId = app.globalData.openId || wx.getStorageSync('openId')
if (!openId) { openId = await app.getOpenId() }
if (!openId) { wx.showToast({ title: '获取支付凭证失败,请重新登录', icon: 'none' }); return }
wx.showLoading({ title: '创建订单...' })
try {
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: openId,
productType: 'gift',
productId: sectionId,
amount: price,
description: `代付解锁:${this.data.section?.title || sectionId}`,
userId: userId,
}
})
wx.hideLoading()
const params = (payRes && payRes.data && payRes.data.payParams) ? payRes.data.payParams : (payRes && payRes.payParams ? payRes.payParams : null)
if (params) {
wx.requestPayment({
...params,
success: async () => {
wx.showLoading({ title: '生成分享链接...' })
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId, paidViaWechat: true }
})
wx.hideLoading()
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功',
content: `已为好友代付 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
cancelText: '稍后分享',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
} else {
wx.showToast({ title: '支付成功,请手动分享', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '支付成功,生成链接失败', icon: 'none' })
}
},
fail: () => { wx.showToast({ title: '支付已取消', icon: 'none' }) }
})
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
console.error('[GiftPay] WeChat pay error:', e)
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
}
},
async _giftPayViaBalance(sectionId, userId, price) {
const balRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }).catch(() => null)
const balance = (balRes && balRes.data) ? balRes.data.balance : 0
if (balance < price) {
wx.showModal({
title: '余额不足',
content: `当前余额 ¥${balance.toFixed(2)},需要 ¥${price}\n请先充值`,
confirmText: '去充值',
cancelText: '取消',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/wallet/wallet' }) }
})
return
}
wx.showLoading({ title: '处理中...' })
try {
const giftRes = await app.request({
url: '/api/miniprogram/balance/gift',
method: 'POST',
data: { giverId: userId, sectionId }
})
wx.hideLoading()
if (giftRes && giftRes.data && giftRes.data.giftCode) {
this._giftCodeToShare = giftRes.data.giftCode
wx.showModal({
title: '代付成功',
content: `已从余额扣除 ¥${price},分享后好友可免费阅读`,
confirmText: '分享给好友',
cancelText: '稍后分享',
success: (r) => { if (r.confirm) wx.shareAppMessage() }
})
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '代付失败', icon: 'none' })
}
},
// 领取礼物码解锁
async _redeemGiftCode(giftCode) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return

View File

@@ -348,17 +348,22 @@
display: flex;
gap: 24rpx;
margin-bottom: 48rpx;
overflow: hidden;
}
.nav-btn {
flex: 1;
min-width: 0;
padding: 24rpx;
border-radius: 24rpx;
max-width: 48%;
box-sizing: border-box;
overflow: hidden;
}
.nav-btn-placeholder {
flex: 1;
min-width: 0;
max-width: 48%;
}
@@ -433,6 +438,7 @@
.action-btn-inline {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
@@ -466,11 +472,11 @@
}
.action-icon-small {
font-size: 28rpx;
font-size: 40rpx;
}
.action-text-small {
font-size: 24rpx;
font-size: 22rpx;
color: #ffffff;
font-weight: 500;
}