Files
ycc-proxy-server/docs/memo-agent-refund-commission-clawback.md
2026-05-19 20:30:32 +08:00

110 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 备忘录:代理退款扣回佣金/冻结优化(策略 2 + 策略 5 改版)
> 状态:**待实施**
> 目的:后续改造 `CancelAgentCommission` 及后台/回调退款链路时,按本文规则实现,避免共享冻结池误扣、部分退款与扣佣口径不一致。
---
## 一、策略 2按「本笔佣金」扣款先冻结、再余额
### 1.1 问题(现状)
- 撤销时使用钱包级「先扣总 `FrozenBalance`、再扣 `Balance`」,可能动到**其他订单**的冻结。
- 解冻任务与提现占用冻结混在同一字段时,易出现对账失败、负冻结等。
### 1.2 目标行为(实施后)
对**单笔订单**下、待冲回的那笔**直推佣金**`agent_commission`,且状态为已发放、未被整笔撤销):
1. **若该笔佣金存在仍有效的冻结任务**(与 `commission_id` 关联、`agent_freeze_task.status == 1` 待解冻):
- 先从**该任务对应的冻结金额**`FreezeAmount`,且只从**本笔佣金归属的冻结额度**扣,见下文物理解耦建议)中扣减,**最多扣 `FreezeAmount`**
- **剩余冲回金额** = 本次要对这笔佣金冲回的总金额 已从冻结部分扣掉的部分,**全部从 `Balance`(可用余额)扣**。
2. **若该笔佣金没有有效冻结任务**(无任务或未达冻结门槛等):
- **本次冲回金额全部从 `Balance` 扣**。
3. **禁止**再用「整钱包 `FrozenBalance` 池先垫满再扣余额」的方式处理**单笔**佣金冲回(除非产品明确采用单字段且不拆分,也应在文档中标记为技术债)。
### 1.3 工程建议(与策略 2 配套)
- **优先**:将「佣金冻结」与「占用的提现冻结」分字段或子账(如 `commission_frozen` / `withdrawal_frozen`),冲回、到期解冻只动佣金侧。
- **至少**:冲回前按 `commission_id` + 任务锁定本单冻结份额;扣款顺序仅在本份额与 `Balance` 之间分配,不侵占其他 `agent_freeze_task` 对应额度。
### 1.4 部分冲回后的冻结任务(需定义)
- 若本次只冲回部分金额:是否**调减** `agent_freeze_task.FreezeAmount`、或拆任务、或标记「已部分冲回」,需在开发时定表结构和状态机,避免到期解冻仍按原 `FreezeAmount` 全额解冻。
---
## 二、策略 5 改版:部分退款 **不按比例**,而是 **「退多少扣多少」,且封顶佣金**
### 2.1 规则(用户指定)
- **不按**「退款金额 / 订单金额 × 佣金」这类比例。
- 设:
- `R` = **本笔后台/回调确认的本次退款金额**(元);
- `C_direct` = 代理在该订单上**当前仍有效的直推佣金**可冲回上限(已发放未撤销的 `agent_commission.amount`,或按「剩余未冲回」口径若支持多次退款累计,见 2.3
- (返佣侧见 2.4`C_rebate_i` = 各条 `agent_rebate` 可冲回上限。
**对直推佣金** 本次应冲回金额:
```text
claw_direct = min(R, C_direct)
```
**示例**
- 订单 100 元,本次退款 50 → **`claw_direct = min(50, C_direct)`**;若 `C_direct = 80`,则冲 **50**(不是 50%×80
- 订单 100 元,本次退款 95`C_direct = 80`**`claw_direct = min(95, 80) = 80`**(封顶佣金)。
### 2.2 与策略 2 的结合
对计算出的 `claw_direct`
- 再按**第一节**拆成:有冻结任务则先扣满该任务可承担的冻结份额,余下从 `Balance`;无冻结则全部从 `Balance`
### 2.3 多次退款(累计)
- 若同一订单可**多次退款**:需约定 `C_direct` 是「原始佣金」还是「剩余可冲回」。
- **建议**:维护「本单已冲回佣金累计」或等价地更新 `agent_commission` 剩余金额/子状态,使每次:
```text
claw_direct = min(R, max(0, C_direct - 已累计已冲回))
```
具体字段是否新增 `revoked_amount`、是否只依赖多条 `order_refund` 汇总,实施时定案。
### 2.4 返佣(`agent_rebate`
- 用户口头规则主要针对「佣金」;**返佣是否沿用同一公式**需在实施前产品拍板:
- **方案 A**:对每条返佣单独 `claw_rebate_i = min(R, cap_i)`,且注意 `R` 与直推已分摊是否重复扣(通常应 **`R` 全局一次,再在直推与多条返佣间分配**,否则 `min(R,C)` 对多受益方会超标)。
- **方案 B**`R` 先在直推与返佣链上按约定顺序分配(例如先扣直推至封顶,再扣上级返佣至各自封顶),总额不超过 `R` 且各方不超过各自佣金上限。
**备忘录约束**:实施前必须写清 **「一笔退款 R 在直推与多返佣之间的分配顺序与上限」**,避免多扣或少扣。
### 2.5 调用链改造要点
- `CancelAgentCommission` 需接收 **`refund_amount`(或累计上下文)**,不能只传 `order_id` 默认全额冲回。
- 后台 `AdminRefundOrder`、微信退款成功回调等,在退款成功后调用冲回逻辑时传入**本次** `R`
- 支付宝/微信**全额**退款:若 `R >=` 订单剩余实付等,则直推/返佣可按封顶一次性冲完(与现有「整单撤销」行为对齐时,等价于 `claw = min(R, C)``R` 足够大)。
---
## 三、实施检查清单(供开发自用)
- [ ] `giveAgentCommission` / 冻结任务与钱包字段是否需调整以支持「只扣本笔冻结份额」。
- [ ] `deductCommissionFromWallet` 重写:入参含 `claw_amount` + `freeze_task` + 分桶或任务份额。
- [ ] 返佣 `deductRebateFromWallet`:是否按 `min(R_remaining, rebate_cap)` 及分配顺序。
- [ ] 部分冲回后 `agent_commission.status`:仍为已发放 / 部分撤销 / 新状态;与统计、列表过滤一致。
- [ ] 多次退款与 `order_refund` 记录一一对应,冲回幂等与审计日志。
- [ ] 提现、定时解冻与冲回共用冻结时的边界用例(负冻结防护)。
---
## 四、参考现状代码(实施时对照)
- 冲回入口:`AgentService.CancelAgentCommission``deductCommissionFromWallet``deductRebateFromWallet`
- 退款后调用:`admin_order/adminrefundorderlogic.go``pay/wechatpayrefundcallbacklogic.go`
---
*文档版本:初稿(按对话需求整理),实施时可增删小节并与产品确认返佣分配。*