Files
junhong_cmp_fiber/internal/routes/shop.go
break 1aa4eacee2
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s
feat(退款分佣): 佣金回溯明细替换全额失效并补齐读侧与导出
用 PRD 2.14 语义整体替换退款佣金「整单全额失效」实现:原佣金保持已发放不变,
回溯事实落在新表 tb_commission_clawback_record 的负数、不可提现明细上。

- 新增成对迁移 000220 建 tb_commission_clawback_record,唯一约束
  (refund_id, original_commission_id) 为权威幂等键,附店铺+时间/原佣金/订单索引。
- 回溯用例(internal/service/refund/clawback.go):准入仅由退款申请状态、审批异常
  标记与退款方式决定;金额按分整数计算,分母取冻结实收(缺失回落审批尝试)、
  分子原路取渠道成功金额,乘法用 math/big 中间量,舍入差自末条起向前补差;
  终态判据要求订单佣金已离开待计算且不存在 status IN (1,2,99) 的记录。
- 三层幂等:唯一约束兜底、佣金行行锁 + 钱包乐观锁、commission_deducted 仅作投影
  并带 WHERE commission_deducted = false 条件置位;闭合三结果为已回溯、无需回溯、
  审批异常转人工。
- 事务内顺序固定:锁提现申请行 → 锁尝试行 → 解冻冻结 → 置驳回 → 插回溯明细 →
  扣 balance(允许为负)→ 写负数流水 → 审计;删除旧全额失效写入与其两个审计调用点,
  refund.invalidate_commission 仅保留常量与注册供历史审计读取。
- 读侧:佣金明细列表 status 筛选透传,两表 UNION ALL 合并分页并以 source ASC 作
  末位次序键;新增佣金明细详情接口并同步路由与 OpenAPI 装配。
- 导出:新增 commission_record 场景(白名单、exporter 注册、DTO oneof、DataSource
  与列定义),粒度为佣金记录,原佣金与回溯各一行,金额保持分且可为负。
- 新增退款佣金回溯周期补偿任务(@every 1m / MaxRetry(3) / Timeout(10m) /
  Unique(10m),独立队列),保留启动时补偿扫描,判据与既有实现一致。

Refs: AUG26-012
2026-09-14 13:40:34 +08:00

276 lines
12 KiB
Go
Raw 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.
package routes
import (
"github.com/gofiber/fiber/v2"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/openapi"
)
func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
groupPath := basePath + "/shops"
Register(shops, doc, groupPath, "GET", "", coreShopManagement(handler.List), RouteSpec{
Summary: "店铺列表",
Description: constants.ShopManagementAccessDescription + constants.ShopListPaginationDescription,
Tags: []string{"店铺管理"},
Input: new(dto.ShopListRequest),
Output: new(dto.ShopPageResult),
Auth: true,
})
Register(shops, doc, groupPath, "POST", "", coreShopManagement(handler.Create), RouteSpec{
Summary: "创建店铺",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.CreateShopRequest),
Output: new(dto.ShopResponse),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/business-owner-candidates", coreShopManagement(handler.BusinessOwnerCandidates), RouteSpec{
Summary: "查询店铺业务员候选",
Description: "仅超级管理员和平台账号可查询;只返回当前启用、未删除的平台账号最小摘要。",
Tags: []string{"店铺管理"},
Input: new(dto.ShopBusinessOwnerCandidateRequest),
Output: new(dto.ShopBusinessOwnerCandidatePageResult),
Auth: true,
})
Register(shops, doc, groupPath, "PUT", "/:id", coreShopManagement(handler.Update), RouteSpec{
Summary: "更新店铺",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.UpdateShopParams),
Output: new(dto.ShopResponse),
Auth: true,
})
Register(shops, doc, groupPath, "PUT", "/:id/credit-limit", handler.UpdateCreditLimit, RouteSpec{
Summary: "调整既有店铺实际信用额度",
Description: "后端不校验按钮权限shop:credit-limit:manage 仅供前端控制按钮显示。",
Tags: []string{"代理商资金管理"},
Input: new(dto.UpdateShopCreditLimitParams),
Output: new(dto.ShopCreditLimitResponse),
Auth: true,
})
Register(shops, doc, groupPath, "DELETE", "/:id", coreShopManagement(handler.Delete), RouteSpec{
Summary: "删除店铺",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.IDReq),
Output: nil,
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/cascade", coreShopManagement(handler.Cascade), RouteSpec{
Summary: "店铺联级查询",
Description: constants.ShopManagementAccessDescription,
Tags: []string{"店铺管理"},
Input: new(dto.ShopCascadeRequest),
Output: new([]dto.ShopCascadeItem),
Auth: true,
})
}
func registerShopDetailRoute(router fiber.Router, handler *admin.ShopHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
groupPath := basePath + "/shops"
Register(shops, doc, groupPath, "GET", "/:id", coreShopManagement(handler.Detail), RouteSpec{
Summary: "查询店铺详情",
Description: constants.ShopManagementAccessDescription + " 返回与店铺列表一致的业务员归属摘要。",
Tags: []string{"店铺管理"},
Input: new(dto.IDReq),
Output: new(dto.ShopResponse),
Auth: true,
})
}
func coreShopManagement(handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
if middleware.GetUserTypeFromContext(c.UserContext()) == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, constants.ShopManagementForbiddenMessage)
}
return handler(c)
}
}
func registerShopRoleRoutes(router fiber.Router, handler *admin.ShopRoleHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
groupPath := basePath + "/shops"
Register(shops, doc, groupPath, "POST", "/:shop_id/roles", handler.AssignShopRoles, RouteSpec{
Summary: "分配店铺默认角色",
Tags: []string{"店铺管理"},
Input: new(dto.AssignShopRolesRequest),
Output: new(dto.ShopRolesResponse),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/roles", handler.GetShopRoles, RouteSpec{
Summary: "查询店铺默认角色",
Tags: []string{"店铺管理"},
Input: new(dto.GetShopRolesRequest),
Output: new(dto.ShopRolesResponse),
Auth: true,
})
Register(shops, doc, groupPath, "DELETE", "/:shop_id/roles/:role_id", handler.DeleteShopRole, RouteSpec{
Summary: "删除店铺默认角色",
Tags: []string{"店铺管理"},
Input: new(dto.DeleteShopRoleRequest),
Output: nil,
Auth: true,
})
}
func registerShopCommissionRoutes(router fiber.Router, handler *admin.ShopCommissionHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
groupPath := basePath + "/shops"
Register(shops, doc, groupPath, "GET", "/fund-summary", agentFundManagement(handler.ListFundSummary), RouteSpec{
Summary: "代理商资金概况",
Description: "按当前店铺数据范围分页返回主钱包、佣金及信用资金事实;现金可用、总可用和欠款均由服务端计算,读取不代表具有调额权限。",
Tags: []string{"代理商资金管理"},
Input: new(dto.ShopFundSummaryListReq),
Output: new(dto.ShopFundSummaryPageResult),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/withdrawal-requests", handler.ListWithdrawalRequests, RouteSpec{
Summary: "代理商提现记录",
Tags: []string{"代理商资金管理"},
Input: new(dto.ShopWithdrawalRequestListReq),
Output: new(dto.ShopWithdrawalRequestPageResult),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/commission-records", handler.ListCommissionRecords, RouteSpec{
Summary: "代理商佣金明细",
Tags: []string{"代理商资金管理"},
Input: new(dto.ShopCommissionRecordListReq),
Output: new(dto.ShopCommissionRecordPageResult),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/commission-records/:id", handler.GetCommissionRecord, RouteSpec{
Summary: "佣金明细详情",
Description: "返回单条佣金明细source=clawback 时返回回溯明细并附带其原佣金,默认返回原佣金并附带全部回溯明细。仅限有数据范围的后台账号,越权与不存在返回同一结果。",
Tags: []string{"代理商资金管理"},
Input: new(dto.ShopCommissionRecordDetailReq),
Output: new(dto.ShopCommissionRecordDetailResp),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/main-wallet/transactions", handler.ListMainWalletTransactions, RouteSpec{
Summary: "代理商预充值钱包流水",
Tags: []string{"代理商资金管理"},
Input: new(dto.MainWalletTransactionListRequest),
Output: new(dto.MainWalletTransactionListResponse),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/commission-stats", handler.GetCommissionStats, RouteSpec{
Summary: "代理商佣金统计",
Tags: []string{"代理商资金管理"},
Input: new(dto.CommissionStatsRequest),
Output: new(dto.CommissionStatsResponse),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/commission-daily-stats", handler.GetCommissionDailyStats, RouteSpec{
Summary: "代理商每日佣金统计",
Tags: []string{"代理商资金管理"},
Input: new(dto.DailyCommissionStatsRequest),
Output: []dto.DailyCommissionStatsResponse{},
Auth: true,
})
Register(shops, doc, groupPath, "POST", "/:shop_id/withdrawal-requests", handler.CreateWithdrawal, RouteSpec{
Summary: "发起提现申请",
Tags: []string{"代理商资金管理"},
Input: new(dto.CreateMyWithdrawalReq),
Output: new(dto.CreateMyWithdrawalResp),
Auth: true,
})
Register(shops, doc, groupPath, "PUT", "/:shop_id/withdrawal-requests/:id", handler.ResubmitWithdrawal, RouteSpec{
Summary: "重提被驳回的提现申请",
Description: "仅本人代理店铺,且仅已被企业微信驳回的申请可重提。事务内先释放旧未结算冻结再按新金额冻结,新增审批尝试记录与新的审批实例,历史尝试与审批结果不被覆盖。",
Tags: []string{"代理商资金管理"},
Input: new(dto.ResubmitWithdrawalReq),
Output: new(dto.CreateMyWithdrawalResp),
Auth: true,
})
Register(shops, doc, groupPath, "GET", "/:shop_id/withdrawal-requests/:id", handler.WithdrawalDetail, RouteSpec{
Summary: "提现申请详情",
Description: "返回指定提现申请及其全部审批尝试记录与正交异常标记,用于详情展示;仅限有数据范围的后台账号。",
Tags: []string{"代理商资金管理"},
Input: new(dto.ShopWithdrawalRequestDetailReq),
Output: new(dto.ShopWithdrawalRequestDetailResp),
Auth: true,
})
commissionRecords := router.Group("/commission-records")
crPath := basePath + "/commission-records"
Register(commissionRecords, doc, crPath, "POST", "/:id/resolve", handler.ResolveCommissionRecord, RouteSpec{
Summary: "修正待审佣金记录",
Tags: []string{"代理商资金管理"},
Input: new(dto.CommissionRecordResolveRequest),
Auth: true,
})
}
func agentFundManagement(handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
if middleware.GetUserTypeFromContext(c.UserContext()) == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, constants.AgentFundManagementForbiddenMessage)
}
return handler(c)
}
}
// registerWithdrawalQualificationRoutes 注册提现资料资格路由。
// 资格提交与查询沿用既有认证与数据范围校验,绝不公开。
func registerWithdrawalQualificationRoutes(router fiber.Router, handler *admin.WithdrawalQualificationHandler, doc *openapi.Generator, basePath string) {
shops := router.Group("/shops")
shopsPath := basePath + "/shops"
qualifications := router.Group("/withdrawal-qualifications")
qualificationPath := basePath + "/withdrawal-qualifications"
Register(shops, doc, shopsPath, "POST", "/:shop_id/withdrawal-qualifications", agentFundManagement(handler.SubmitWithdrawalQualification), RouteSpec{
Summary: "提交提现资料资格",
Description: "仅本人代理店铺。合同与法人身份证正反面必填;企业必填统一社会信用代码,个人填写法人身份证号。替换合同或法人身份证时同一事务新增资料版本并使旧有效版本失效,需重新审批通过后才可提现。",
Tags: []string{"代理商提现资格"},
Input: new(dto.SubmitWithdrawalQualificationReq),
Output: new(dto.SubmitWithdrawalQualificationResp),
Auth: true,
})
Register(shops, doc, shopsPath, "GET", "/:shop_id/withdrawal-qualifications", agentFundManagement(handler.ListWithdrawalQualifications), RouteSpec{
Summary: "查询提现资料资格版本",
Description: "仅返回当前账号数据范围内店铺的资料版本,按版本从新到旧;证件号脱敏,附件只返回对象存储 Key 引用。",
Tags: []string{"代理商提现资格"},
Input: new(dto.WithdrawalQualificationListReq),
Output: new(dto.WithdrawalQualificationPageResult),
Auth: true,
})
Register(qualifications, doc, qualificationPath, "POST", "/:id/void", agentFundManagement(handler.VoidWithdrawalQualification), RouteSpec{
Summary: "作废提现资料资格",
Description: "仅超级管理员reason 必填。作废后该资格失效且原因可查询,代理提现被拒绝直至重新审批通过。",
Tags: []string{"代理商提现资格"},
Input: new(dto.VoidWithdrawalQualificationParams),
Output: nil,
Auth: true,
})
}