外观
余额充值和消费系统设计文档
设计思路
本系统采用预付费余额模式,用户先充值到账户余额,然后在使用产品时按动作消耗余额。这种设计有以下优势:
- 简化计费逻辑:只需要关注每个动作的费用和用户余额是否足够
- 统一管理:所有产品的计费都通过余额系统,避免为每个产品单独设计支付方案
- 灵活扩展:新增产品只需定义动作费用,无需修改支付流程
- 完整记录:所有充值和消费记录都有完整的流水,便于对账和查询
系统配置
系统通过环境变量配置提现和退款相关的参数,这些配置在 src/scripts/ConstantUtils.mts 中定义:
提现配置
MIN_WITHDRAW_AMOUNT:最小可提现金额(单位:元)WITHDRAW_FEE_PERCENT:提现手续费费率(百分比,如0.01表示1%)MIN_WITHDRAW_FEE:单笔提现最低手续费(单位:元)WITHDRAW_TO_ACCOUNT_TIME:提现到账时间(工作日数,单位:天)
退款配置
MIN_REFUND_AMOUNT:最小可退款金额(单位:元)REFUND_FEE_PERCENT:退款手续费费率(百分比,如0.01表示1%)MIN_REFUND_FEE:单笔退款最低手续费(单位:元)REFUND_TO_ACCOUNT_TIME:退款到账时间(工作日数,单位:天)
注意:这些配置值通过环境变量传入,可在不同环境(开发、测试、生产)中设置不同的值。
数据库表设计
1. balance_account(余额账户表)
存储用户当前余额信息。
字段说明:
user_id: 用户ID(唯一,关联user表)balance: 账户余额(单位:元,保留2位小数,默认0)frozen_balance: 冻结余额(用于处理中的订单等场景)last_updated: 最后更新时间
特点:
- 一个用户只有一个余额账户
- 余额通过交易流水表记录,账户表只存储当前快照
2. balance_transaction(余额交易流水表)
记录所有余额变动记录,包括充值、消费、退款、调整、提现。
字段说明:
user_id: 用户IDtransaction_type: 交易类型recharge: 充值(通过支付订单充值到余额)consume: 消费(使用余额支付)refund: 退款(消费退款)adjust: 调整(管理员手动调整余额)withdraw: 提现(余额提现)
amount: 交易金额(正数表示增加,负数表示减少)balance_before: 交易前余额balance_after: 交易后余额status: 交易状态(pending-处理中,success-成功,failed-失败,canceled-已取消)order_id: 关联的支付订单ID(如果是充值,关联order表)business_type: 业务类型(ppt_generate-PPT生成,design_create-设计创建,other-其他)business_id: 业务ID(关联具体的业务记录,如PPT项目ID)remark: 备注信息original_transaction_id: 原交易ID(如果是退款交易,关联被退款的交易记录)withdraw_account_id: 提现账户ID(如果是提现交易,关联提现账户表)fee: 手续费(单位:元,主要用于提现场景)actual_amount: 实际到账金额(单位:元,提现金额减去手续费)
索引:
(user_id, created_at): 用于查询用户的收支列表(user_id, transaction_type): 用于按类型查询(status): 用于查询处理中的交易
3. balance_alert_settings(余额提醒设置表)
存储用户的余额提醒配置。
字段说明:
user_id: 用户ID(唯一)enable_sms_alert: 是否启用短信提醒(默认true)enable_email_alert: 是否启用邮件提醒(默认true)threshold_amount: 临界金额(单位:元,当余额低于此值时触发提醒,默认10元)
4. balance_alert_log(余额提醒记录表)
记录已发送的提醒,避免重复提醒。
字段说明:
user_id: 用户IDalert_type: 提醒类型(sms-短信提醒,email-邮件提醒)balance_at_alert: 提醒时的余额success: 提醒是否发送成功error_message: 如果发送失败,记录错误信息
索引:
(user_id, created_at): 用于查询用户的提醒历史
5. balance_withdraw_account(余额提现账户表)
存储用户的提现账户信息。
字段说明:
user_id: 用户ID(关联user表)account_type: 账户类型(bank_card-银行卡,alipay-支付宝,wechat-微信)account_name: 账户名称(如持卡人姓名、支付宝账号名等)account_number: 账户号码(如银行卡号、支付宝账号等,加密存储)bank_name: 银行名称(仅银行卡类型需要)bank_branch: 开户行(仅银行卡类型需要)is_default: 是否为默认提现账户is_verified: 是否已验证(暂时不支持验证)remark: 备注信息
索引:
(user_id): 用于查询用户的提现账户列表(user_id, is_default): 用于查询默认账户
6. balance_withdraw_audit(余额提现审批表)
记录提现申请的审批信息。
字段说明:
transaction_id: 提现交易ID(关联balance_transaction表)user_id: 用户ID(关联user表)audit_status: 审批状态(pending-待审批,approved-已批准,rejected-已驳回,deleted-已删除)is_withdrawn: 是否已提现(仅当审批状态为approved时有效)is_email_notified: 是否已邮件通知(仅当is_withdrawn为true时有效)auditor_id: 审批人ID(最后操作的审批人)audited_at: 审批时间(最后审批操作的时间)original_amount: 原始提现金额(用户申请的金额)adjusted_amount: 调整后的金额(管理员改价后的金额,如果未改价则为null)remark: 备注信息(审批备注,驳回或改价时必填)admin_remark: 管理员备注(内部备注,不对外显示)
索引:
(transaction_id): 用于查询交易对应的审批记录(user_id): 用于查询用户的提现审批记录(audit_status): 用于按状态筛选(audit_status, is_withdrawn): 用于查询已批准但未提现的记录(created_at): 用于按时间排序
核心业务流程
1. 用户充值流程
1. 用户发起充值请求
2. 创建支付订单(order表)
3. 支付成功后:
- 创建余额交易记录(transaction_type='recharge', status='success')
- 更新余额账户表(balance += 充值金额)
- 检查是否需要发送余额提醒1
2
3
4
5
6
2
3
4
5
6
2. 用户消费流程
1. 用户发起消费请求(如生成PPT)
2. 检查用户余额是否足够
3. 如果余额足够:
- 创建余额交易记录(transaction_type='consume', status='pending')
- 冻结相应金额(frozen_balance += 消费金额)
- 执行业务逻辑(如生成PPT)
- 业务成功后:
- 更新交易记录状态为success
- 更新余额(balance -= 消费金额,frozen_balance -= 消费金额)
- 业务失败后:
- 更新交易记录状态为failed
- 解冻金额(frozen_balance -= 消费金额)
4. 检查是否需要发送余额不足提醒1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
3. 用户退款流程
退款分为两种场景:
场景1:消费退款(退还余额到账户)
1. 用户申请退款(如PPT生成失败)
2. 验证原消费交易是否可退款(如状态为success)
3. 创建退款交易记录:
- transaction_type='refund'
- amount=退款金额(正数,表示增加余额)
- original_transaction_id=原消费交易ID
- status='pending'
4. 退款审批通过后:
- 更新余额账户(balance += 退款金额)
- 更新退款交易记录状态为success
- 可选:更新原消费交易记录的备注,标记已退款1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
场景2:充值退款(退回到原支付渠道)
充值退款支持两种操作方式:
方式1:用户侧操作(针对支付订单退款)
1. 用户在"支付订单"页面查看订单列表
2. 用户选择已支付的订单,点击"退款"按钮
3. 系统自动查找对应的充值交易记录
4. 用户填写退款金额(可选,留空则全额退款)和退款原因
5. 系统验证:
- 原充值交易状态为success
- 支付订单状态为已支付
- 退款金额不超过可退金额(需扣除已退款部分)
6. 调用支付渠道退款接口(蓝兔支付)
7. 创建退款交易记录:
- transaction_type='refund'
- amount=退款金额(负数,表示减少余额)
- original_transaction_id=原充值交易ID
- order_id=原支付订单ID
- status='success'(如果退款接口调用成功)或'pending'(如果调用失败)
8. 支付渠道退款成功后:
- 更新余额账户(balance -= 退款金额,注意:如果余额不足,需要特殊处理)
- 更新退款交易记录状态为success
9. 支付渠道退款失败后:
- 更新退款交易记录状态为pending或failed
- 记录失败原因1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
方式2:管理员侧操作(针对充值交易记录退款)
1. 超级管理员在余额管理界面查看交易记录
2. 管理员选择交易类型为"充值"且状态为"成功"的记录
3. 点击"退款"按钮
4. 填写退款金额(可选,留空则全额退款)和退款原因
5. 系统验证:
- 原充值交易状态为success
- 支付订单状态为已支付
- 退款金额不超过可退金额
6. 调用支付渠道退款接口
7. 创建退款交易记录(同方式1)
8. 更新余额和交易状态(同方式1)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
退款重试功能(管理员侧)
对于失败的退款交易,管理员可以进行退款重试:
1. 超级管理员在余额管理界面查看交易记录
2. 管理员选择交易类型为"退款"且状态为"失败"或"处理中"的记录
3. 点击"退款重试"按钮
4. 系统验证:
- 退款交易状态为failed或pending
- 原交易和支付订单存在且状态正确
5. 重新生成退款单号
6. 调用支付渠道退款接口
7. 如果成功:
- 更新退款交易记录状态为success
- 更新余额账户(如果之前未成功扣除)
- 更新备注信息(标记重试成功)
8. 如果失败:
- 更新退款交易记录状态为failed
- 更新备注信息(记录重试失败原因)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意事项:
- 退款必须关联原交易(通过
original_transaction_id) - 同一笔交易可能多次退款(部分退款),需要记录累计退款金额
- 充值退款时,如果余额已被消费,需要先扣减余额,不足部分需要特殊处理
- 用户侧退款:用户只能对自己的订单进行退款操作
- 管理员侧退款:仅超级管理员可以操作,可以对任意用户的充值记录进行退款
- 退款重试:仅超级管理员可以操作,用于处理失败的退款交易
- 退款接口调用失败时,交易状态为pending,管理员可以通过退款重试功能重新发起退款
4. 余额提醒流程
1. 每次余额变动后,查询用户的提醒设置
2. 如果余额 < 临界金额:
- 检查是否在最近一段时间内已发送过提醒(避免频繁提醒)
- 如果启用短信提醒,发送短信
- 如果启用邮件提醒,发送邮件
- 记录提醒日志1
2
3
4
5
6
2
3
4
5
6
5. 用户提现流程
1. 用户发起提现请求
2. 验证提现金额:
- 检查提现金额是否大于0
- 检查提现金额是否 >= MIN_WITHDRAW_AMOUNT(最小可提现金额)
3. 检查可用余额是否足够(可用余额 = 总余额 - 冻结余额)
4. 计算手续费:
- 手续费 = max(提现金额 × WITHDRAW_FEE_PERCENT, MIN_WITHDRAW_FEE)
- 使用BigNumber确保计算精度
- 实际到账金额 = 提现金额 - 手续费
- 验证实际到账金额 >= 0
5. 如果可用余额足够:
- 创建提现交易记录:
- transaction_type='withdraw'
- amount=-提现金额(负数,表示减少余额)
- status='pending'(待审核或处理)
- fee=手续费
- actual_amount=实际到账金额
- withdraw_account_id=提现账户ID(可选)
- 创建提现审批记录:
- audit_status='pending'(待审批)
- original_amount=提现金额
- is_withdrawn=false
- is_email_notified=false
- 直接从余额中扣除提现金额(balance -= 提现金额)
- 注意:提现会立即扣除余额,但交易状态为pending,需要管理员审批
6. 管理员审批处理:
- 批准:更新审批状态为approved,交易状态为success
- 驳回:退回余额,创建退款交易记录,更新审批状态为rejected,交易状态为failed
- 改价:调整提现金额,自动处理余额差额,更新审批记录
- 删除:如果是待审批状态,退回余额;已提现的不能删除
7. 标记已提现:
- 管理员在完成实际转账后,标记审批记录为已提现(is_withdrawn=true)
8. 发送邮件通知:
- 标记已提现后,可以发送邮件通知用户
- 邮件发送成功后,记录is_email_notified=true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
注意事项:
- 提现使用可用余额(余额 - 冻结余额),确保不会影响冻结中的订单
- 最小提现金额由
MIN_WITHDRAW_AMOUNT配置决定 - 手续费计算:
max(提现金额 × WITHDRAW_FEE_PERCENT, MIN_WITHDRAW_FEE),使用BigNumber确保精度 - 提现申请创建后,余额立即扣除,但资金实际到账需要等待管理员审批和转账
- 提现交易状态为pending时,如果需要取消提现,需要将余额退回并更新交易状态
- 提现到账时间由
WITHDRAW_TO_ACCOUNT_TIME配置决定(工作日数) - 驳回和改价操作时,备注为必填项
- 管理员可以添加备注(审批备注和管理员备注)
6. 提现账户管理流程
1. 用户添加提现账户:
- 选择账户类型(银行卡或支付宝)
- 填写账户信息:
- 银行卡:账户名称、账户号码、银行名称、开户行(必填)
- 支付宝:账户名称、账户号码(必填)
- 可选择设为默认账户
- 前端提示用户确认信息准确,否则造成的金额损失由用户自行负责
2. 用户编辑提现账户:
- 修改账户信息
- 可以切换默认账户
3. 用户删除提现账户:
- 检查账户是否已被使用(有提现交易使用此账户)
- 如果已被使用,不允许删除
- 如果未被使用,允许删除
4. 设置默认账户:
- 用户可以将任意账户设为默认账户
- 设置新默认账户时,自动取消其他账户的默认状态1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意事项:
- 目前暂不支持账户验证,用户需要自行确认信息准确
- 账户号码建议加密存储
- 银行卡类型必须填写银行名称和开户行
- 一个用户可以有多个提现账户,但只能有一个默认账户
7. 提现审批流程
1. 管理员查看提现审批列表:
- 支持按用户ID筛选
- 支持按审批状态筛选(待审批、已批准、已驳回、已删除)
- 支持分页查询
2. 批准提现:
- 更新审批状态为approved
- 更新交易状态为success
- 可以添加备注和管理员备注
3. 驳回提现:
- 更新审批状态为rejected
- 更新交易状态为failed
- 退回余额到用户账户
- 创建退款交易记录
- 备注必填(驳回原因)
4. 改价(调整提现金额):
- 只能调整待审批状态的提现申请
- 如果新金额小于原金额,退回差额
- 如果新金额大于原金额,从余额中扣除差额
- 重新计算手续费和实际到账金额
- 备注必填(改价原因)
5. 删除提现申请:
- 如果是待审批状态,退回余额
- 已提现的申请不能删除
- 更新审批状态为deleted
- 更新交易状态为canceled(如果是pending状态)
6. 标记已提现:
- 只能标记已批准状态的提现申请
- 更新is_withdrawn=true
- 管理员完成实际转账后使用此功能
7. 发送邮件通知:
- 只能对已标记为已提现的申请发送邮件
- 邮件发送成功后,更新is_email_notified=true
- 如果用户未设置邮箱,无法发送1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
注意事项:
- 只有超级管理员可以进行审批操作
- 驳回和改价操作时,备注为必填项
- 删除待审批的申请会自动退回余额
- 已提现的申请不能删除
- 邮件通知功能需要用户已设置邮箱
使用示例
查询用户余额
typescript
const account = await knex("balance_account").where({ user_id }).first();
const balance = account?.balance || 0;1
2
2
查询收支列表
typescript
const transactions = await knex("balance_transaction")
.where({ user_id })
.orderBy("created_at", "desc")
.limit(20)
.offset(0);1
2
3
4
5
2
3
4
5
消费扣款
typescript
await knex.transaction(async (trx) => {
// 1. 查询并锁定账户
const account = await trx("balance_account")
.where({ user_id })
.forUpdate()
.first();
if (!account || account.balance < amount) {
throw new Error("余额不足");
}
// 2. 创建交易记录
await trx("balance_transaction").insert({
user_id,
transaction_type: "consume",
amount: -amount,
balance_before: account.balance,
balance_after: account.balance - amount,
status: "pending",
business_type: "ppt_generate",
business_id: projectId,
remark: "生成PPT",
});
// 3. 更新余额
await trx("balance_account")
.where({ user_id })
.update({
balance: account.balance - amount,
last_updated: trx.fn.now(),
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
充值到余额
typescript
await knex.transaction(async (trx) => {
// 1. 查询账户
const account = await trx("balance_account")
.where({ user_id })
.forUpdate()
.first();
const balanceBefore = account?.balance || 0;
const balanceAfter = balanceBefore + rechargeAmount;
// 2. 创建交易记录
await trx("balance_transaction").insert({
user_id,
transaction_type: "recharge",
amount: rechargeAmount,
balance_before: balanceBefore,
balance_after: balanceAfter,
status: "success",
order_id: orderId,
remark: "充值到余额",
});
// 3. 更新或创建账户
if (account) {
await trx("balance_account").where({ user_id }).update({
balance: balanceAfter,
last_updated: trx.fn.now(),
});
} else {
await trx("balance_account").insert({
user_id,
balance: balanceAfter,
frozen_balance: 0,
});
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
消费退款(退还余额)
typescript
await knex.transaction(async (trx) => {
// 1. 查询原消费交易
const originalTransaction = await trx("balance_transaction")
.where({ id: originalTransactionId, user_id, transaction_type: "consume" })
.first();
if (!originalTransaction || originalTransaction.status !== "success") {
throw new Error("原交易不存在或状态不正确");
}
// 检查是否已退款(查询已成功的退款记录)
const existingRefunds = await trx("balance_transaction")
.where({
original_transaction_id: originalTransactionId,
transaction_type: "refund",
status: "success",
})
.sum("amount as total_refunded");
const totalRefunded = existingRefunds[0]?.total_refunded || 0;
const refundableAmount = Math.abs(originalTransaction.amount) - totalRefunded;
if (refundAmount > refundableAmount) {
throw new Error("退款金额超过可退金额");
}
// 2. 查询并锁定账户
const account = await trx("balance_account")
.where({ user_id })
.forUpdate()
.first();
const balanceBefore = account?.balance || 0;
const balanceAfter = balanceBefore + refundAmount;
// 3. 创建退款交易记录
const [refundTransaction] = await trx("balance_transaction")
.insert({
user_id,
transaction_type: "refund",
amount: refundAmount, // 正数,表示增加余额
balance_before: balanceBefore,
balance_after: balanceAfter,
status: "success",
original_transaction_id: originalTransactionId,
business_type: originalTransaction.business_type,
business_id: originalTransaction.business_id,
remark: `退款:${refundReason || "用户申请退款"}`,
})
.returning("id");
// 4. 更新余额
await trx("balance_account").where({ user_id }).update({
balance: balanceAfter,
last_updated: trx.fn.now(),
});
return refundTransaction;
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
充值退款(退回到支付渠道)
typescript
await knex.transaction(async (trx) => {
// 1. 查询原充值交易
const originalTransaction = await trx("balance_transaction")
.where({
id: originalTransactionId,
user_id,
transaction_type: "recharge",
})
.first();
if (!originalTransaction || originalTransaction.status !== "success") {
throw new Error("原交易不存在或状态不正确");
}
// 2. 查询支付订单
const order = await trx("order")
.where({ id: originalTransaction.order_id })
.first();
if (!order || order.pay_status !== 1) {
throw new Error("支付订单不存在或未支付成功");
}
// 3. 调用支付渠道退款接口
const [refundErr, refundResult] = await refundPayOrder({
out_trade_no: order.order_no,
out_refund_no: generateRefundOrderNo(), // 生成退款单号
refund_fee: refundAmount.toString(),
refund_desc: refundReason || "用户申请退款",
});
if (refundErr) {
throw refundErr;
}
// 4. 查询并锁定账户
const account = await trx("balance_account")
.where({ user_id })
.forUpdate()
.first();
const balanceBefore = account?.balance || 0;
const balanceAfter = Math.max(0, balanceBefore - refundAmount); // 余额不能为负
// 5. 创建退款交易记录
await trx("balance_transaction").insert({
user_id,
transaction_type: "refund",
amount: -refundAmount, // 负数,表示减少余额
balance_before: balanceBefore,
balance_after: balanceAfter,
status: "success",
original_transaction_id: originalTransactionId,
order_id: order.id,
remark: `充值退款:${refundReason || "用户申请退款"}`,
});
// 6. 更新余额(如果余额不足,需要记录异常)
await trx("balance_account").where({ user_id }).update({
balance: balanceAfter,
last_updated: trx.fn.now(),
});
// 注意:如果 balanceBefore < refundAmount,说明余额已被消费,
// 这部分差额需要记录到异常表中,或通过其他方式处理
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
余额提现
typescript
import BigNumber from "bignumber.js";
import {
MIN_WITHDRAW_AMOUNT,
WITHDRAW_FEE_PERCENT,
MIN_WITHDRAW_FEE,
} from "#scripts/ConstantUtils";
await knex.transaction(async (trx) => {
// 1. 查询并锁定账户
const account = await trx("balance_account")
.where({ user_id })
.forUpdate()
.first();
if (!account) {
throw new Error("账户不存在");
}
const balanceBefore = Number(account.balance) || 0;
const frozenBalance = Number(account.frozen_balance) || 0;
// 可用余额 = 总余额 - 冻结余额(使用BigNumber确保精度)
const availableBalance = new BigNumber(balanceBefore)
.minus(frozenBalance)
.toNumber();
// 2. 验证提现金额
if (amount <= 0) {
throw new Error("提现金额必须大于0");
}
if (amount < MIN_WITHDRAW_AMOUNT) {
throw new Error(`提现金额不能少于${MIN_WITHDRAW_AMOUNT}元`);
}
// 3. 检查可用余额是否足够
if (availableBalance < amount) {
throw new Error(
`可用余额不足,当前余额: ${balanceBefore}元,冻结余额: ${frozenBalance}元,可用余额: ${availableBalance}元`,
);
}
// 4. 计算手续费(使用配置的费率和最低手续费)
const fee = BigNumber.max(
new BigNumber(amount).times(WITHDRAW_FEE_PERCENT),
MIN_WITHDRAW_FEE,
).toNumber();
const actualAmount = new BigNumber(amount).minus(fee).toNumber(); // 实际到账金额
// 验证实际到账金额不能为负
if (actualAmount < 0) {
throw new Error("提现金额不足以支付手续费");
}
// 5. 计算提现后余额
const balanceAfter = new BigNumber(balanceBefore).minus(amount).toNumber();
// 6. 创建提现交易记录
const [transactionId] = await trx("balance_transaction").insert({
user_id,
transaction_type: "withdraw",
amount: -amount, // 负数,表示减少余额
balance_before: balanceBefore,
balance_after: balanceAfter,
status: "pending", // 提现状态为pending,需要后续处理
withdraw_account_id: withdrawAccountId || null,
fee: fee,
actual_amount: actualAmount,
remark: remark || "余额提现",
});
// 7. 创建提现审批记录
await trx("balance_withdraw_audit").insert({
transaction_id: transactionId,
user_id,
audit_status: "pending",
is_withdrawn: false,
is_email_notified: false,
original_amount: amount,
adjusted_amount: null,
remark: null,
admin_remark: null,
});
// 8. 更新余额(直接扣除,因为提现是用户主动操作)
await trx("balance_account").where({ user_id }).update({
balance: balanceAfter,
last_updated: trx.fn.now(),
});
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
注意事项
- 数据一致性:所有余额操作必须在事务中进行,并使用
forUpdate()锁定行,避免并发问题 - 余额计算:余额应该通过交易流水表计算得出,账户表只存储快照,用于快速查询
- 精度处理:使用BigNumber进行金额计算,避免JavaScript浮点数精度问题
- 提醒频率控制:避免频繁发送提醒,建议同一用户同一类型提醒至少间隔1小时
- 业务类型扩展:新增业务类型时,在
business_type枚举中添加新值 - 余额不足处理:消费前必须检查余额是否足够(在服务端和客户端进行双重校验),包括冻结余额
- 提现安全:
- 提现使用可用余额(余额 - 冻结余额),确保不影响冻结中的订单
- 最小提现金额限制:由
MIN_WITHDRAW_AMOUNT配置决定 - 手续费计算:
max(提现金额 × WITHDRAW_FEE_PERCENT, MIN_WITHDRAW_FEE) - 提现申请创建后余额立即扣除,但需要后续审核或自动处理才能完成实际转账
- 提现状态为pending时,如需取消,需要将余额退回
- 提现到账时间:由
WITHDRAW_TO_ACCOUNT_TIME配置决定(工作日数)
- 退款安全:
- 退款前必须验证原交易状态和用户权限
- 防止重复退款,需要检查累计退款金额
- 最小退款金额:由
MIN_REFUND_AMOUNT配置决定 - 退款手续费:由
REFUND_FEE_PERCENT和MIN_REFUND_FEE配置决定 - 退款到账时间:由
REFUND_TO_ACCOUNT_TIME配置决定(工作日数) - 充值退款时,如果余额已被消费,需要特殊处理(记录差额或拒绝退款)
- 退款操作应该有审批流程或限制条件(如时间限制、金额限制)
后续扩展建议
- 余额快照定时任务:定期通过交易流水表计算余额,与账户表对比,确保数据一致性
- 余额统计报表:按时间段统计充值、消费金额、退款金额、提现金额
- 余额冻结机制:支持更复杂的冻结场景(如订单处理中的冻结)
- 提现功能增强:
- ✅ 提现审批流程(已实现):支持管理员审批提现申请,包括批准、驳回、删除、改价等操作
- ✅ 提现账户管理(已实现):支持用户管理多个提现账户(银行卡、支付宝),支持默认账户设置
- ✅ 邮件通知(已实现):标记已提现后可以发送邮件通知用户
- 自动提现:达到一定金额或条件时自动处理
- 提现限制:设置每日/每月提现限额、提现次数限制等
- 提现账户验证机制:通过验证码等方式验证账户真实性
- 提现到账处理:
- 集成支付渠道接口,实现自动转账
- 提现失败处理:转账失败时自动退回余额
- 提现回调处理:接收支付渠道的回调通知
- 退款审批流程:添加退款审批表,支持管理员审批退款申请
- 退款限制:支持设置退款时间限制(如7天内可退款)、退款次数限制等
- 部分退款支持:支持同一笔交易多次部分退款,需要记录累计退款金额
- 账户号码加密:对提现账户号码进行加密存储,提高安全性
退款功能实现
用户侧退款功能
用户可以在余额管理页面的"支付订单"标签页中查看订单列表,并对已支付的订单进行退款操作。
主要功能:
- 查看支付订单列表(支持分页)
- 对已支付的订单发起退款(支持全额或部分退款)
- 自动关联对应的充值交易记录
API接口:
GET /balance/orders- 查询用户的支付订单列表POST /balance/refund-recharge- 充值退款(用户侧)
服务层函数:
getUserOrders()- 查询用户的支付订单列表refundRecharge()- 充值退款处理
管理员侧退款功能
超级管理员可以在余额管理界面中对交易记录进行退款操作。
主要功能:
- 对充值记录进行退款操作(交易类型为
recharge且状态为success) - 对退款记录进行退款重试操作(交易类型为
refund且状态为failed或pending)
API接口:
POST /balance/admin/refund-recharge- 管理员侧充值退款(仅超级管理员)POST /balance/admin/retry-refund- 退款重试(仅超级管理员)
服务层函数:
refundRecharge()- 充值退款处理(管理员侧调用)retryRefund()- 退款重试处理
主要涉及文件
后端文件
Service层
src/services/balance.mts- 余额相关业务逻辑getUserBalance()- 查询用户余额getBalanceTransactions()- 查询交易列表createRechargeOrder()- 创建充值订单handleRechargeSuccess()- 处理充值成功回调consumeBalance()- 消费扣款refundConsume()- 消费退款refundRecharge()- 充值退款(用户侧和管理员侧共用)retryRefund()- 退款重试(管理员侧)getUserOrders()- 查询用户的支付订单列表withdrawBalance()- 余额提现
src/services/balanceWithdrawAudit.mts- 提现审批业务逻辑src/services/balanceWithdrawAccount.mts- 提现账户管理业务逻辑src/services/ltzf.mts- 蓝兔支付服务ltzfRefundWechatPayOrder()- 微信支付退款接口
Controller层
src/controllers/balance.mts- 余额相关控制器cGetBalance()- 查询余额接口cGetTransactions()- 查询交易列表接口cCreateRechargeOrder()- 创建充值订单接口cConsumeBalance()- 消费扣款接口cRefundConsume()- 消费退款接口cRefundRecharge()- 充值退款接口(用户侧)cAdminRefundRecharge()- 管理员侧充值退款接口(仅超级管理员)cAdminRetryRefund()- 退款重试接口(仅超级管理员)cGetUserOrders()- 查询用户的支付订单列表接口cWithdrawBalance()- 余额提现接口
src/controllers/balanceWithdrawAudit.mts- 提现审批控制器(仅超级管理员)src/controllers/balanceWithdrawAccount.mts- 提现账户管理控制器
路由层
src/routes/balance.mts- 余额相关路由定义GET /balance/balance- 查询余额GET /balance/transactions- 查询交易列表GET /balance/orders- 查询用户的支付订单列表POST /balance/recharge- 创建充值订单POST /balance/consume- 消费扣款POST /balance/refund- 消费退款POST /balance/refund-recharge- 充值退款(用户侧)POST /balance/withdraw- 余额提现POST /balance/admin/refund-recharge- 管理员侧充值退款(仅超级管理员)POST /balance/admin/retry-refund- 退款重试(仅超级管理员)
src/routes/balanceWithdrawAudit.mts- 提现审批路由定义(仅超级管理员)src/routes/balanceWithdrawAccount.mts- 提现账户管理路由定义
Model层(数据库表定义)
src/models/BalanceAccount.mts- 余额账户表定义src/models/BalanceTransaction.mts- 余额交易流水表定义src/models/BalanceAlertSettings.mts- 余额提醒设置表定义src/models/BalanceAlertLog.mts- 余额提醒记录表定义src/models/BalanceWithdrawAccount.mts- 余额提现账户表定义src/models/BalanceWithdrawAudit.mts- 余额提现审批表定义
配置常量
src/scripts/ConstantUtils.mts- 系统常量配置
支付相关
src/services/ltzf.mts- 蓝兔支付服务ltzfPayByWechatScan()- 微信扫码支付ltzfRefundWechatPayOrder()- 微信支付退款ltzfGetWechatRefundOrder()- 查询退款订单
前端文件
API接口
apps/account/src/api/balance.ts- 用户端余额相关API调用apiGetBalance()- 查询余额apiGetBalanceTransactions()- 查询交易列表apiGetUserOrders()- 查询用户的支付订单列表apiCreateRechargeOrder()- 创建充值订单apiRefundConsume()- 消费退款apiRefundRecharge()- 充值退款apiWithdrawBalance()- 余额提现
apps/account/src/api/balanceWithdrawAccount.ts- 用户端提现账户管理API调用apps/admin/src/api/balance.ts- 管理端余额相关API调用apiGetBalance()- 查询余额(管理员可查看指定用户)apiGetBalanceTransactions()- 查询交易列表(管理员可查看所有用户或指定用户)apiAdminRefundRecharge()- 管理员侧充值退款(仅超级管理员)apiAdminRetryRefund()- 退款重试(仅超级管理员)
apps/admin/src/api/balanceWithdrawAudit.ts- 管理端提现审批API调用(仅超级管理员)
页面组件
apps/account/src/views/BalanceView/BalanceView.vue- 用户端余额管理页面- 余额展示和充值功能
- 交易记录列表(支持按类型筛选)
- 支付订单列表(支持退款操作)
- 提现功能
apps/account/src/views/WithdrawAccountView/WithdrawAccountView.vue- 用户端提现账户管理页面apps/admin/src/views/BalanceView/BalanceView.vue- 管理端余额管理页面- 交易记录列表(支持按用户ID和交易类型筛选)
- 对充值记录进行退款操作(仅超级管理员)
- 对退款记录进行退款重试操作(仅超级管理员)
apps/admin/src/views/WithdrawAuditView/WithdrawAuditView.vue- 管理端提现审批页面(仅超级管理员)
通用组件
apps/_base/src/components/RechargeDialog/RechargeDialog.vue- 充值对话框组件
类型定义文件
API类型
typings/balance-api.d.ts- 余额相关API类型定义
数据库类型
typings/balance-account.d.ts- 余额账户表类型定义typings/balance-transaction.d.ts- 交易记录类型定义typings/balance-alert-settings.d.ts- 余额提醒设置表类型定义typings/balance-alert-log.d.ts- 余额提醒记录表类型定义typings/balance-withdraw-account.d.ts- 提现账户类型定义typings/balance-withdraw-audit.d.ts- 提现审批表类型定义