个人站点

第 6 / 8 篇

整理于 · 9 分钟

支付与退款怎样保证金额和状态正确

退款计价基数、收款身份和并发状态如何保持一致

费用规则与九月交易完整性修订合并叙述,不代表真实支付接入。

贯穿项目的约定 本篇涉及 2 条

贯穿项目的约定

  1. 课时属于具体课程,班级、课次与教室容量分别判断。

    查看规则说明
  2. 各角色只在授权范围内操作,家长代办以确认绑定为前提。

    查看规则说明
  3. 账号、关系、教学与订单各有状态,一处变化不代替另一处判断。

    本篇涉及 查看规则说明
  4. 课程课时、订单金额和钱包资金分开,费用变化保留业务依据。

    本篇涉及 查看规则说明

本篇如何落实

退款以本轮实付金额为基数,优先回到原付款人;九月通过相关共享行的相对锁序校验并发状态,案例来自合成环境。

在教培管理系统中,金额与课时需要对应到同一轮报名。

一次看似平常的“学员退课”,在底层牵动着极其复杂的业务链路:需要核查已上课时比例、计算违约金阶梯、校验当时报名是全款还是中途插班、确认是由学员自己付款还是家长代付,并在数据库层面按特定顺序锁定多张表,确保钱包退款、选课作废与名额释放原子提交。

这使退款规则、付款记录和并发控制成为同一个问题的不同部分。九月的验证围绕这些关系展开,检查指定流程能否一致提交或回滚。

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 元。

问题出在两个入口用了不同基数:报价给出的金额,无法通过申请自身的校验。这是测试中暴露的缺陷,不是机构真实学员的退款记录。

旧系统逻辑(基数错位)
插班实付:80 元
错误读取课程标价:100 元
计算可退:100 × 95% = 95 元
提交退课申请:95 元超过实付 80 元,申请被拒绝
9 月修复后逻辑(同源实付)
插班实付:80 元
报价与申请读取本轮可信实付:80 元
该合成用例可退:80 × 95% = 76 元
申请通过,审批退款为 76 元
同一笔插班,退费基数取错一次就够让申请被自己拒掉

2.2 治理方案

报价与申请共用本轮支付记录的解析规则。存在可信已付记录时,以该轮实付金额为基数;只有没有付款记录的历史选课,才保留使用课程价格的兼容。重新报名等场景也要确认付款与本轮选课的对应关系,不能随意取一笔历史付款。

3. 资金流向确权:退款原路退回付款人

在家长代付场景下,选课学员是小明,但出资付款的是父亲大明的钱包。

系统优先依据可信付款记录确认退款接收人:

  • 退费审批通过后,系统直接通过支付凭证中的 Payment.payerId 定位真实付款人账号;
  • 退款回到原付款人的钱包并生成流水,前端请求不能自行指定其他收款账号。

历史记录缺少付款人时,退课路径仍保留退给申请人的兼容。它需要与有可信付款人的路径区分,不能概括为所有历史退款都能原路返回。

4. 并发控制:共享行锁的相对顺序

多人争抢最后一个名额、付款与提现同时发生或重复审批退款时,仅声明事务还不足以协调这些竞争。还需要统一加锁顺序,并在获得锁后重新检查余额、名额和业务状态。

已核对的交易路径遵循以下既有共享行锁的相对顺序:

1 支付订单级Payment 锁
2 开班实例与名额级CourseInstance 锁
3 选课权益级StudentCourse 锁
4 用户钱包级UserWallet 锁
相关交易都按这个相对顺序拿锁,顺序本身就是约定

这不是要求每条路径都锁四张表,而是要求一条路径获取多个既有共享行锁时,不能持有后面的锁再反向等待前面的锁。等待锁后,还要基于锁定的当前读重新检查业务条件:

  1. 钱包可用余额在扣减瞬间依然足额;
  2. 班级名额在锁定瞬间未被他人抢占;
  3. 事务内步骤失败时,该事务内的数据库变更回滚,避免扣款、选课和人数只完成一部分。

这些调整把金额计算、收款身份和状态提交连在了一起。验证使用原有表结构和合成数据,覆盖指定交易的竞争与回滚;真实第三方支付及既有业务库兼容仍不在这批验收范围内。