第 6 / 8 篇
整理于 · 9 分钟
支付与退款怎样保证金额和状态正确
退款计价基数、收款身份和并发状态如何保持一致
费用规则与九月交易完整性修订合并叙述,不代表真实支付接入。
贯穿项目的约定 本篇涉及 2 条
在教培管理系统中,金额与课时需要对应到同一轮报名。
一次看似平常的“学员退课”,在底层牵动着极其复杂的业务链路:需要核查已上课时比例、计算违约金阶梯、校验当时报名是全款还是中途插班、确认是由学员自己付款还是家长代付,并在数据库层面按特定顺序锁定多张表,确保钱包退款、选课作废与名额释放原子提交。
这使退款规则、付款记录和并发控制成为同一个问题的不同部分。九月的验证围绕这些关系展开,检查指定流程能否一致提交或回滚。
1. 钱包资金模型与四档退课阶梯
为了规范机构内的资金进出,系统建立了个人钱包与账户流水体系:
- 可用余额(Available Balance):可自由用于购买课程或申请提现;
- 冻结余额(Frozen Balance):提现申请处理中暂时冻结的金额,不能再用于购课;
- 账目流水闭环:充值增加可用余额,购课生成消费流水,退费回退资金,提现转出资金,每笔变动均具备明确的业务凭据。
1.1 当前四档退课计算模型
在退课核算上,系统废弃了早期粗糙的“过半不退”旧口径,演进为精细的四档梯度计算器(RefundCalculator)。计算逻辑首先提取学员在当前课程中的已消耗课时比例:
| 已消耗课时比例 x | 剩余价值违约金比例(普通班课) | 一对一定制课程违约金 | 普通班课计算逻辑 |
|---|---|---|---|
| < 10% | 5% | 15% | 扣除 5% 违约金,退还 95% 剩余价值 |
| 10% ≤ x < 50% | 30% | 40% | 扣除 30% 违约金,退还 70% 剩余价值 |
| 50% ≤ x < 70% | 50% | 60% | 扣除 50% 违约金,退还 50% 剩余价值 |
| ≥ 70% | 100%(不可退) | 100%(不可退) | 剩余价值全部作为违约金,可退金额为 0 |
1.2 请假退费的时间差率与病假规则
对于单个课次的请假退费,系统依据申请发起时刻距离开课时刻的**提前量 h(小时)**计算手续费:
- 提前 ≥ 24 小时申请:扣除单课价值的 5%;
- 提前 2 ≤ h < 24 小时申请:扣除 30%;
- 临近开课不足 2 小时:扣除 100%(不予退费)。
特别规则(病假取低原则):对于附带有效就医证明的病假申请,系统规定其扣费率取“上述时间费率”与“固定 10% 费率”中的较低者。例如:提前 30 小时提交的病假,时间费率本身已是 5%,系统严谨执行 5% 的较低费率,绝不因病假类型反而上浮扣除 10%。
2. 经典缺陷复盘:插班退款基数的统一
9 月在隔离的临时 MySQL 中使用合成数据验证事务时,发现了一个报价与申请不一致的缺陷:
2.1 缺陷场景重现
合成场景中的标准班课共 10 节,整课售价 100 元。
- 插班学员只购买剩余 8 节课,实际支付 80 元,随后发起退课;该用例按 5% 违约金计算。
- 旧报价沿用整课售价 100 元,得出 100 × 95% = 95 元。
- 提交时又按真实实付上限检查,因为 95 元超过实付 80 元,申请被拒绝。
- 修复后,报价和申请统一以本轮实付 80 元为基数,该用例应退 80 × 95% = 76 元,审批退款也为 76 元。
问题出在两个入口用了不同基数:报价给出的金额,无法通过申请自身的校验。这是测试中暴露的缺陷,不是机构真实学员的退款记录。
2.2 治理方案
报价与申请共用本轮支付记录的解析规则。存在可信已付记录时,以该轮实付金额为基数;只有没有付款记录的历史选课,才保留使用课程价格的兼容。重新报名等场景也要确认付款与本轮选课的对应关系,不能随意取一笔历史付款。
3. 资金流向确权:退款原路退回付款人
在家长代付场景下,选课学员是小明,但出资付款的是父亲大明的钱包。
系统优先依据可信付款记录确认退款接收人:
- 退费审批通过后,系统直接通过支付凭证中的
Payment.payerId定位真实付款人账号; - 退款回到原付款人的钱包并生成流水,前端请求不能自行指定其他收款账号。
历史记录缺少付款人时,退课路径仍保留退给申请人的兼容。它需要与有可信付款人的路径区分,不能概括为所有历史退款都能原路返回。
4. 并发控制:共享行锁的相对顺序
多人争抢最后一个名额、付款与提现同时发生或重复审批退款时,仅声明事务还不足以协调这些竞争。还需要统一加锁顺序,并在获得锁后重新检查余额、名额和业务状态。
已核对的交易路径遵循以下既有共享行锁的相对顺序:
这不是要求每条路径都锁四张表,而是要求一条路径获取多个既有共享行锁时,不能持有后面的锁再反向等待前面的锁。等待锁后,还要基于锁定的当前读重新检查业务条件:
- 钱包可用余额在扣减瞬间依然足额;
- 班级名额在锁定瞬间未被他人抢占;
- 事务内步骤失败时,该事务内的数据库变更回滚,避免扣款、选课和人数只完成一部分。
这些调整把金额计算、收款身份和状态提交连在了一起。验证使用原有表结构和合成数据,覆盖指定交易的竞争与回滚;真实第三方支付及既有业务库兼容仍不在这批验收范围内。