feat: 数据概览简化 + 用户管理增加余额/提现列

- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额
- 数据概览:移除余额统计区块(余额改在用户管理中展示)
- 数据概览:恢复转化率卡片(唯一付费用户/总用户)
- 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额
- 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段
- 后端:User model 添加 WalletBalance 非数据库字段
- 包含之前的小程序埋点和管理后台点击统计面板

Made-with: Cursor
This commit is contained in:
卡若
2026-03-15 15:57:09 +08:00
parent 991e17698c
commit 708547d0dd
52 changed files with 3161 additions and 1103 deletions

View File

@@ -18,6 +18,8 @@ import readingTracker from '../../utils/readingTracker'
const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
const app = getApp()
Page({
@@ -181,6 +183,21 @@ Page({
// 5. 加载导航
this.loadNavigation(id)
// 6. 规则引擎:阅读前检查(填头像、绑手机等)
checkAndExecute('before_read', this)
// 7. 记录浏览行为到 user_tracks
const userId = app.globalData.userInfo?.id
if (userId) {
app.request('/api/miniprogram/track', {
method: 'POST',
data: { userId, action: 'view_chapter', target: id, extraData: { sectionId: id, mid: mid || '' } },
silent: true
}).catch(() => {})
// 更新全局阅读计数
app.globalData.readCount = (app.globalData.readCount || 0) + 1
}
} catch (e) {
console.error('[Read] 初始化失败:', e)
wx.showToast({ title: '加载失败,请重试', icon: 'none' })
@@ -783,6 +800,7 @@ Page({
// 分享到微信 - 自动带分享人ID优先用 mid扫码/海报闭环),无则用 id
onShareAppMessage() {
trackClick('read', 'btn_click', '分享_' + this.data.sectionId)
const { section, sectionId, sectionMid } = this.data
const ref = app.getMyReferralCode()
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
@@ -808,11 +826,20 @@ Page({
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
const articleTitle = (section?.title || chapterTitle || '').trim()
const title = articleTitle
? (articleTitle.length > 28 ? articleTitle.slice(0, 28) + '...' : articleTitle)
: 'Soul创业派对 - 真实商业故事'
? `📚 ${articleTitle.length > 24 ? articleTitle.slice(0, 24) + '...' : articleTitle}Soul创业派对`
: '📚 Soul创业派对 - 真实商业故事'
return { title, query: ref ? `${q}&ref=${ref}` : q }
},
shareToMoments() {
wx.showModal({
title: '分享到朋友圈',
content: '点击右上角「···」菜单,选择「分享到朋友圈」即可。\n\n朋友圈分享文案已自动生成。',
showCancel: false,
confirmText: '知道了',
})
},
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
showLoginModal() {
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
@@ -939,6 +966,7 @@ Page({
// 购买章节 - 直接调起支付
async handlePurchaseSection() {
trackClick('read', 'btn_click', '购买章节_' + this.data.sectionId)
console.log('[Pay] 点击购买章节按钮')
wx.showLoading({ title: '处理中...', mask: true })
@@ -1297,6 +1325,11 @@ Page({
wx.navigateTo({ url: '/pages/referral/referral' })
},
showPosterModal() {
this.setData({ showPosterModal: true })
this.generatePoster()
},
// 生成海报
async generatePoster() {
wx.showLoading({ title: '生成中...' })
@@ -1466,7 +1499,7 @@ Page({
this.setData({ showShareTip: false })
},
// 代付分享:余额帮好友解锁当前章节
// 代付分享:微信支付或余额帮好友解锁当前章节
async handleGiftPay() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({ title: '提示', content: '请先登录', confirmText: '去登录', success: (r) => { if (r.confirm) this.showLoginModal() } })
@@ -1476,46 +1509,101 @@ Page({
const userId = app.globalData.userInfo.id
const price = (this.data.section && this.data.section.price != null) ? this.data.section.price : (this.data.sectionPrice || 1)
const balRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }).catch(() => null)
const balance = (balRes && balRes.data) ? balRes.data.balance : 0
wx.showModal({
title: '代付分享',
content: `为好友代付本章 ¥${price}\n当前余额: ¥${balance.toFixed(2)}\n${balance < price ? '余额不足,请先充值' : '确认后将从余额扣除'}`,
confirmText: balance >= price ? '确认代付' : '去充值',
content: `为好友代付本章 ¥${price}\n\n支付后将生成代付链接,好友点击即可免费阅读`,
confirmText: '微信支付',
cancelText: '用余额',
success: async (res) => {
if (!res.confirm) return
if (balance < price) {
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) {
const giftCode = giftRes.data.giftCode
wx.showModal({
title: '代付成功!',
content: `已为好友代付 ¥${price},分享链接后好友可免费阅读`,
confirmText: '分享给好友',
success: (r) => {
if (r.confirm) {
this._giftCodeToShare = giftCode
wx.shareAppMessage()
}
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,
}
})
} else {
wx.showToast({ title: (giftRes && giftRes.error) || '代付失败', icon: 'none' })
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' })
}
} 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' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '代付失败', icon: 'none' })
}
}
})

View File

@@ -93,7 +93,7 @@
<text class="action-icon-small">🎁</text>
<text class="action-text-small">代付分享</text>
</view>
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
<view class="action-btn-inline btn-poster-inline" bindtap="showPosterModal">
<text class="action-icon-small">🖼️</text>
<text class="action-text-small">生成海报</text>
</view>
@@ -312,8 +312,8 @@
</view>
</view>
<!-- 右下角悬浮分享按钮 -->
<button class="fab-share" open-type="share">
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
</button>
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
<view class="fab-share" bindtap="shareToMoments">
<text class="fab-moments-icon">🌐</text>
</view>
</view>

View File

@@ -454,10 +454,16 @@
}
.btn-poster-inline {
background: rgba(255, 215, 0, 0.15);
border: 2rpx solid rgba(255, 215, 0, 0.3);
background: linear-gradient(135deg, #2d2d30 0%, #3d3d40 100%);
}
.btn-moments-inline {
background: linear-gradient(135deg, #1a4a2e, #0d3320);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.btn-moments-inline:active {
opacity: 0.7;
}
.action-icon-small {
font-size: 28rpx;
@@ -1003,6 +1009,10 @@
display: block;
}
.fab-moments-icon {
font-size: 48rpx;
}
/* ===== 分享提示文字(底部导航上方) ===== */
.share-tip-inline {
text-align: center;