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

@@ -0,0 +1,167 @@
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
statusBarHeight: 0,
balance: 0,
balanceText: '0.00',
totalRecharged: '0.00',
totalGifted: '0.00',
totalRefunded: '0.00',
transactions: [],
loading: true,
rechargeAmounts: [10, 30, 50, 1000],
selectedAmount: 30,
},
onLoad() {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
this.loadBalance()
this.loadTransactions()
},
async loadBalance() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
if (res && res.data) {
this.setData({
balance: res.data.balance || 0,
balanceText: (res.data.balance || 0).toFixed(2),
totalRecharged: (res.data.totalRecharged || 0).toFixed(2),
totalGifted: (res.data.totalGifted || 0).toFixed(2),
totalRefunded: (res.data.totalRefunded || 0).toFixed(2),
loading: false,
})
}
} catch (e) {
this.setData({ loading: false })
}
},
async loadTransactions() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance/transactions?userId=${userId}`, silent: true })
if (res && res.data) {
const list = (res.data || []).map(t => ({
...t,
amountText: (t.amount || 0).toFixed(2),
amountSign: (t.amount || 0) >= 0 ? '+' : '',
description: t.description || (t.type === 'recharge' ? '充值' : t.type === 'gift' ? '赠送' : t.type === 'refund' ? '退款' : t.type === 'consume' ? '阅读消费' : '其他'),
}))
this.setData({ transactions: list })
}
} catch (e) {
console.warn('[Wallet] load transactions failed', e)
}
},
selectAmount(e) {
trackClick('wallet', 'tab_click', '选择金额' + (e.currentTarget.dataset.amount || ''))
this.setData({ selectedAmount: parseInt(e.currentTarget.dataset.amount) })
},
async handleRecharge() {
trackClick('wallet', 'btn_click', '充值')
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
const userId = app.globalData.userInfo.id
const amount = this.data.selectedAmount
wx.showLoading({ title: '创建订单...' })
try {
const res = await app.request({
url: '/api/miniprogram/balance/recharge',
method: 'POST',
data: { userId, amount }
})
wx.hideLoading()
if (res && res.data && res.data.orderSn) {
// Trigger WeChat Pay for the recharge order
const payRes = await app.request({
url: '/api/miniprogram/pay',
method: 'POST',
data: {
openId: app.globalData.openId,
productType: 'balance_recharge',
productId: res.data.orderSn,
amount: amount,
description: `余额充值 ¥${amount}`,
userId: userId,
}
})
if (payRes && payRes.payParams) {
wx.requestPayment({
...payRes.payParams,
success: async () => {
// Confirm the recharge
await app.request({
url: '/api/miniprogram/balance/recharge/confirm',
method: 'POST',
data: { orderSn: res.data.orderSn }
})
wx.showToast({ title: '充值成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
},
fail: () => {
wx.showToast({ title: '支付取消', icon: 'none' })
}
})
} else {
wx.showToast({ title: '创建支付失败', icon: 'none' })
}
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '充值失败', icon: 'none' })
}
},
async handleRefund() {
trackClick('wallet', 'btn_click', '退款')
if (this.data.balance <= 0) {
wx.showToast({ title: '余额为零', icon: 'none' })
return
}
const userId = app.globalData.userInfo.id
const balance = this.data.balance
const refundAmount = (balance * 0.9).toFixed(2)
wx.showModal({
title: '余额退款',
content: `退回全部余额 ¥${balance.toFixed(2)}\n实际到账 ¥${refundAmount}9折\n\n退款将在1-3个工作日内原路返回`,
confirmText: '确认退款',
success: async (res) => {
if (!res.confirm) return
wx.showLoading({ title: '处理中...' })
try {
const result = await app.request({
url: '/api/miniprogram/balance/refund',
method: 'POST',
data: { userId, amount: balance }
})
wx.hideLoading()
if (result && result.data) {
wx.showToast({ title: result.data.message || '退款成功', icon: 'success' })
this.loadBalance()
this.loadTransactions()
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: '退款失败', icon: 'none' })
}
}
})
},
goBack() {
wx.navigateBack()
},
})

View File

@@ -0,0 +1,4 @@
{
"navigationBarTitleText": "我的余额",
"navigationStyle": "custom"
}

View File

@@ -0,0 +1,88 @@
<!-- Soul创业派对 - 我的余额 -->
<view class="page">
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-back" bindtap="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">我的余额</text>
<view class="nav-placeholder"></view>
</view>
<view class="nav-placeholder-block" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 余额卡片 -->
<view class="balance-card">
<view class="balance-main" wx:if="{{!loading}}">
<text class="balance-label">当前余额</text>
<text class="balance-value">¥{{balanceText}}</text>
<text class="balance-tip">充值后可直接用于解锁付费内容,消费记录会展示在下方。</text>
</view>
<view class="balance-skeleton" wx:else>
<text class="skeleton-text">加载中...</text>
</view>
</view>
<!-- 充值金额选择 -->
<view class="section">
<view class="section-head">
<text class="section-title">选择充值金额</text>
<text class="section-note">当前已选 ¥{{selectedAmount}}</text>
</view>
<view class="amount-grid">
<view
class="amount-card {{selectedAmount === item ? 'amount-card-active' : ''}}"
wx:for="{{rechargeAmounts}}"
wx:key="*this"
bindtap="selectAmount"
data-amount="{{item}}"
>
<view class="amount-card-top">
<text class="amount-card-value">¥{{item}}</text>
<view class="amount-card-check {{selectedAmount === item ? 'amount-card-check-active' : ''}}">
<view class="amount-card-check-dot" wx:if="{{selectedAmount === item}}"></view>
</view>
</view>
<text class="amount-card-desc">{{selectedAmount === item ? '已选中,点击充值' : '点击选择此金额'}}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-row">
<view class="btn btn-recharge" bindtap="handleRecharge">充值</view>
<view class="btn btn-refund" bindtap="handleRefund">退款(9折)</view>
</view>
<!-- 充值与消费记录 -->
<view class="section">
<view class="section-head">
<text class="section-title">充值/消费记录</text>
<text class="section-note">按时间倒序显示</text>
</view>
<view class="transactions" wx:if="{{transactions.length > 0}}">
<view
class="tx-item"
wx:for="{{transactions}}"
wx:key="id"
>
<view class="tx-icon {{item.type}}">
<text wx:if="{{item.type === 'recharge'}}">💰</text>
<text wx:elif="{{item.type === 'gift'}}">🎁</text>
<text wx:elif="{{item.type === 'refund'}}">↩️</text>
<text wx:elif="{{item.type === 'consume'}}">📖</text>
<text wx:else>•</text>
</view>
<view class="tx-info">
<text class="tx-desc">{{item.description}}</text>
<text class="tx-time">{{item.createdAt || item.created_at || '--'}}</text>
</view>
<text class="tx-amount {{item.amount >= 0 ? 'tx-amount-plus' : 'tx-amount-minus'}}">{{item.amountSign}}¥{{item.amountText}}</text>
</view>
</view>
<view class="tx-empty" wx:else>
<text>暂无充值或消费记录</text>
</view>
</view>
<view class="bottom-space"></view>
</view>

View File

@@ -0,0 +1,267 @@
/* Soul创业派对 - 我的余额 - 深色主题 */
.page {
min-height: 100vh;
background: #0a0a0a;
padding-bottom: 64rpx;
}
/* 导航栏 */
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(40rpx);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
height: 88rpx;
}
.nav-back {
width: 64rpx;
height: 64rpx;
background: #1c1c1e;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 40rpx;
color: rgba(255, 255, 255, 0.6);
font-weight: 300;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.nav-placeholder {
width: 64rpx;
}
.nav-placeholder-block {
width: 100%;
}
/* 余额卡片 - 渐变背景 */
.balance-card {
margin: 24rpx 24rpx 32rpx;
background: linear-gradient(135deg, #1c1c1e 0%, rgba(56, 189, 172, 0.15) 100%);
border-radius: 32rpx;
padding: 40rpx 32rpx;
border: 2rpx solid rgba(56, 189, 172, 0.2);
}
.balance-main {
min-height: 220rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.balance-label {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 8rpx;
}
.balance-value {
font-size: 72rpx;
font-weight: 700;
color: #38bdac;
letter-spacing: 2rpx;
}
.balance-tip {
margin-top: 18rpx;
font-size: 24rpx;
line-height: 1.7;
color: rgba(255, 255, 255, 0.58);
}
.balance-skeleton {
padding: 40rpx 0;
}
.skeleton-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
}
/* 区块标题 */
.section {
margin: 0 24rpx 32rpx;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 20rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.section-note {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.45);
}
/* 金额选择卡片 */
.amount-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20rpx;
}
.amount-card {
padding: 26rpx 24rpx;
background: linear-gradient(180deg, #19191b 0%, #151517 100%);
border-radius: 28rpx;
border: 2rpx solid rgba(255, 255, 255, 0.1);
box-sizing: border-box;
}
.amount-card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.amount-card-value {
font-size: 40rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
}
.amount-card-desc {
display: block;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.46);
}
.amount-card-check {
width: 34rpx;
height: 34rpx;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.18);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.amount-card-check-active {
border-color: #38bdac;
background: rgba(56, 189, 172, 0.18);
}
.amount-card-check-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background: #38bdac;
}
.amount-card-active {
background: linear-gradient(180deg, rgba(56, 189, 172, 0.22) 0%, rgba(15, 30, 29, 0.95) 100%);
border-color: rgba(56, 189, 172, 0.95);
box-shadow: 0 0 0 2rpx rgba(56, 189, 172, 0.1);
}
.amount-card-active .amount-card-value {
color: #52d8c7;
}
.amount-card-active .amount-card-desc {
color: rgba(213, 255, 250, 0.72);
}
/* 操作按钮 */
.action-row {
display: flex;
gap: 24rpx;
margin: 0 24rpx 40rpx;
}
.btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
}
.btn-recharge {
background: #38bdac;
color: #0a0a0a;
}
.btn-refund {
background: #1c1c1e;
color: rgba(255, 255, 255, 0.9);
border: 2rpx solid rgba(56, 189, 172, 0.4);
}
/* 交易记录 */
.transactions {
background: #1c1c1e;
border-radius: 24rpx;
overflow: hidden;
border: 2rpx solid rgba(255, 255, 255, 0.04);
}
.tx-item {
display: flex;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
}
.tx-item:last-child {
border-bottom: none;
}
.tx-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 24rpx;
}
.tx-icon.recharge {
background: rgba(56, 189, 172, 0.2);
}
.tx-icon.gift {
background: rgba(255, 215, 0, 0.15);
}
.tx-icon.refund {
background: rgba(255, 255, 255, 0.1);
}
.tx-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.tx-desc {
font-size: 28rpx;
color: #fff;
}
.tx-time {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.45);
}
.tx-amount {
font-size: 30rpx;
font-weight: 600;
}
.tx-amount-plus {
color: #38bdac;
}
.tx-amount-minus {
color: rgba(255, 255, 255, 0.6);
}
.tx-empty {
padding: 60rpx;
text-align: center;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
background: #1c1c1e;
border-radius: 24rpx;
}
.bottom-space {
height: 48rpx;
}