f
This commit is contained in:
@@ -86,6 +86,10 @@ service main {
|
|||||||
// 代理手机号修改
|
// 代理手机号修改
|
||||||
@handler AdminUpdateAgentMobile
|
@handler AdminUpdateAgentMobile
|
||||||
post /mobile/update (AdminUpdateAgentMobileReq) returns (AdminUpdateAgentMobileResp)
|
post /mobile/update (AdminUpdateAgentMobileReq) returns (AdminUpdateAgentMobileResp)
|
||||||
|
|
||||||
|
// 代理等级降级(管理端)
|
||||||
|
@handler AdminDowngradeAgent
|
||||||
|
post /level/downgrade (AdminDowngradeAgentReq) returns (AdminDowngradeAgentResp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -443,5 +447,14 @@ type (
|
|||||||
AdminUpdateAgentMobileResp {
|
AdminUpdateAgentMobileResp {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
// 代理等级降级
|
||||||
|
AdminDowngradeAgentReq {
|
||||||
|
AgentId string `json:"agent_id"` // 代理ID
|
||||||
|
ToLevel int64 `json:"to_level"` // 目标等级:1=普通,2=黄金,3=钻石(须低于当前等级)
|
||||||
|
Remark string `json:"remark,optional"` // 备注(可选)
|
||||||
|
}
|
||||||
|
AdminDowngradeAgentResp {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package admin_agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/rest/httpx"
|
||||||
|
"ycc-server/app/main/api/internal/logic/admin_agent"
|
||||||
|
"ycc-server/app/main/api/internal/svc"
|
||||||
|
"ycc-server/app/main/api/internal/types"
|
||||||
|
"ycc-server/common/result"
|
||||||
|
"ycc-server/pkg/lzkit/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AdminDowngradeAgentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req types.AdminDowngradeAgentReq
|
||||||
|
if err := httpx.Parse(r, &req); err != nil {
|
||||||
|
result.ParamErrorResult(r, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validator.Validate(req); err != nil {
|
||||||
|
result.ParamValidateErrorResult(r, w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l := admin_agent.NewAdminDowngradeAgentLogic(r.Context(), svcCtx)
|
||||||
|
resp, err := l.AdminDowngradeAgent(&req)
|
||||||
|
result.HttpResult(r, w, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/invite_code/list",
|
Path: "/invite_code/list",
|
||||||
Handler: admin_agent.AdminGetInviteCodeListHandler(serverCtx),
|
Handler: admin_agent.AdminGetInviteCodeListHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/level/downgrade",
|
||||||
|
Handler: admin_agent.AdminDowngradeAgentHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/link/list",
|
Path: "/link/list",
|
||||||
@@ -79,6 +84,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/list",
|
Path: "/list",
|
||||||
Handler: admin_agent.AdminGetAgentListHandler(serverCtx),
|
Handler: admin_agent.AdminGetAgentListHandler(serverCtx),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/mobile/update",
|
||||||
|
Handler: admin_agent.AdminUpdateAgentMobileHandler(serverCtx),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/order/list",
|
Path: "/order/list",
|
||||||
@@ -94,11 +104,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||||||
Path: "/product_config/update",
|
Path: "/product_config/update",
|
||||||
Handler: admin_agent.AdminUpdateAgentProductConfigHandler(serverCtx),
|
Handler: admin_agent.AdminUpdateAgentProductConfigHandler(serverCtx),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Method: http.MethodPost,
|
|
||||||
Path: "/mobile/update",
|
|
||||||
Handler: admin_agent.AdminUpdateAgentMobileHandler(serverCtx),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Path: "/real_name/list",
|
Path: "/real_name/list",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package admin_agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"ycc-server/app/main/model"
|
||||||
|
"ycc-server/common/xerr"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"ycc-server/app/main/api/internal/svc"
|
||||||
|
"ycc-server/app/main/api/internal/types"
|
||||||
|
|
||||||
|
"github.com/zeromicro/go-zero/core/logx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AdminDowngradeAgentLogic struct {
|
||||||
|
logx.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminDowngradeAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDowngradeAgentLogic {
|
||||||
|
return &AdminDowngradeAgentLogic{
|
||||||
|
Logger: logx.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AdminDowngradeAgentLogic) AdminDowngradeAgent(req *types.AdminDowngradeAgentReq) (resp *types.AdminDowngradeAgentResp, err error) {
|
||||||
|
if req.AgentId == "" {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("代理ID不能为空"), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = l.svcCtx.AgentModel.FindOne(l.ctx, req.AgentId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg("代理不存在"), "")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理失败, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.svcCtx.AgentService.ProcessDowngrade(l.ctx, req.AgentId, req.ToLevel, req.Remark); err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrMsg(err.Error()), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.AdminDowngradeAgentResp{
|
||||||
|
Success: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -876,6 +876,102 @@ func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId string, toLev
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpgradeTypeAdminDowngrade 管理端降级
|
||||||
|
const UpgradeTypeAdminDowngrade int64 = 4
|
||||||
|
|
||||||
|
// ProcessDowngrade 管理端代理降级(仅降低等级,不退款、不收回历史返佣)
|
||||||
|
func (s *AgentService) ProcessDowngrade(ctx context.Context, agentId string, toLevel int64, remark string) error {
|
||||||
|
return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
|
||||||
|
agent, err := s.AgentModel.FindOne(transCtx, agentId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
return errors.New("代理不存在")
|
||||||
|
}
|
||||||
|
return errors.Wrapf(err, "查询代理失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
fromLevel := agent.Level
|
||||||
|
if fromLevel == 3 {
|
||||||
|
return errors.New("钻石代理不允许降级")
|
||||||
|
}
|
||||||
|
if fromLevel == 1 {
|
||||||
|
return errors.New("普通代理无法降级")
|
||||||
|
}
|
||||||
|
if fromLevel != 2 || toLevel != 1 {
|
||||||
|
return errors.New("仅支持将黄金代理降级为普通代理")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.validateDowngradeDirectChildren(transCtx, agentId, toLevel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if remark == "" {
|
||||||
|
remark = "管理端降级"
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeRecord := &model.AgentUpgrade{
|
||||||
|
Id: uuid.NewString(),
|
||||||
|
AgentId: agent.Id,
|
||||||
|
FromLevel: fromLevel,
|
||||||
|
ToLevel: toLevel,
|
||||||
|
UpgradeType: UpgradeTypeAdminDowngrade,
|
||||||
|
UpgradeFee: 0,
|
||||||
|
Status: 2, // 已完成
|
||||||
|
Remark: lzUtils.StringToNullString(remark),
|
||||||
|
}
|
||||||
|
if _, err := s.AgentUpgradeModel.Insert(transCtx, session, upgradeRecord); err != nil {
|
||||||
|
return errors.Wrapf(err, "创建降级记录失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Level = toLevel
|
||||||
|
|
||||||
|
teamLeaderId, err := s.findTeamLeaderId(transCtx, agentId)
|
||||||
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
||||||
|
return errors.Wrapf(err, "查找团队首领失败")
|
||||||
|
}
|
||||||
|
if teamLeaderId != "" {
|
||||||
|
agent.TeamLeaderId = sql.NullString{String: teamLeaderId, Valid: true}
|
||||||
|
} else {
|
||||||
|
agent.TeamLeaderId = sql.NullString{Valid: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.AgentModel.UpdateWithVersion(transCtx, session, agent); err != nil {
|
||||||
|
return errors.Wrapf(err, "更新代理等级失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateDowngradeDirectChildren 校验直接下级是否允许上级降到 toLevel
|
||||||
|
func (s *AgentService) validateDowngradeDirectChildren(ctx context.Context, agentId string, toLevel int64) error {
|
||||||
|
builder := s.AgentRelationModel.SelectBuilder().
|
||||||
|
Where("parent_id = ? AND relation_type = ? AND del_state = ?", agentId, 1, globalkey.DelStateNo)
|
||||||
|
relations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "查询直接下级失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, relation := range relations {
|
||||||
|
child, err := s.AgentModel.FindOne(ctx, relation.ChildId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, model.ErrNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return errors.Wrapf(err, "查询下级代理失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.Level > toLevel {
|
||||||
|
return fmt.Errorf("存在等级高于目标等级的直接下级(下级ID: %s),无法降级", child.Id)
|
||||||
|
}
|
||||||
|
if child.Level == toLevel && toLevel >= 2 {
|
||||||
|
return fmt.Errorf("存在与目标等级相同的黄金/钻石直接下级(下级ID: %s),无法降级", child.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// needDetachFromParent 检查是否需要脱离直接上级关系
|
// needDetachFromParent 检查是否需要脱离直接上级关系
|
||||||
func (s *AgentService) needDetachFromParent(ctx context.Context, agent *model.Agent, newLevel int64) (bool, error) {
|
func (s *AgentService) needDetachFromParent(ctx context.Context, agent *model.Agent, newLevel int64) (bool, error) {
|
||||||
parent, err := s.findDirectParent(ctx, agent.Id)
|
parent, err := s.findDirectParent(ctx, agent.Id)
|
||||||
|
|||||||
@@ -230,6 +230,16 @@ type AdminDeleteUserResp struct {
|
|||||||
Success bool `json:"success"` // 是否成功
|
Success bool `json:"success"` // 是否成功
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminDowngradeAgentReq struct {
|
||||||
|
AgentId string `json:"agent_id"` // 代理ID
|
||||||
|
ToLevel int64 `json:"to_level"` // 目标等级:1=普通,2=黄金,3=钻石(须低于当前等级)
|
||||||
|
Remark string `json:"remark,optional"` // 备注(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminDowngradeAgentResp struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type AdminGenerateDiamondInviteCodeReq struct {
|
type AdminGenerateDiamondInviteCodeReq struct {
|
||||||
Count int64 `json:"count"` // 生成数量
|
Count int64 `json:"count"` // 生成数量
|
||||||
ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期)
|
ExpireDays int64 `json:"expire_days,optional"` // 过期天数(可选,0表示不过期)
|
||||||
@@ -870,6 +880,15 @@ type AdminUpdateAgentConfigResp struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminUpdateAgentMobileReq struct {
|
||||||
|
AgentId string `json:"agent_id"` // 代理ID
|
||||||
|
Mobile string `json:"mobile"` // 新手机号
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminUpdateAgentMobileResp struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
}
|
||||||
|
|
||||||
type AdminUpdateAgentProductConfigReq struct {
|
type AdminUpdateAgentProductConfigReq struct {
|
||||||
Id string `json:"id"` // 主键
|
Id string `json:"id"` // 主键
|
||||||
BasePrice float64 `json:"base_price"` // 基础底价
|
BasePrice float64 `json:"base_price"` // 基础底价
|
||||||
@@ -882,16 +901,6 @@ type AdminUpdateAgentProductConfigResp struct {
|
|||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 代理手机号修改
|
|
||||||
type AdminUpdateAgentMobileReq struct {
|
|
||||||
AgentId string `json:"agent_id"` // 代理ID
|
|
||||||
Mobile string `json:"mobile"` // 新手机号
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminUpdateAgentMobileResp struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminUpdateApiReq struct {
|
type AdminUpdateApiReq struct {
|
||||||
Id string `path:"id"`
|
Id string `path:"id"`
|
||||||
ApiName string `json:"api_name"`
|
ApiName string `json:"api_name"`
|
||||||
|
|||||||
109
docs/memo-agent-refund-commission-clawback.md
Normal file
109
docs/memo-agent-refund-commission-clawback.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 备忘录:代理退款扣回佣金/冻结优化(策略 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` 等
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本:初稿(按对话需求整理),实施时可增删小节并与产品确认返佣分配。*
|
||||||
Reference in New Issue
Block a user