package handler import ( "context" "encoding/json" "errors" "fmt" logmodel "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/lottery" "gorm.io/gorm" ) // CommissionHandler 发放"抽奖佣金"。写 user.commission 增量 + system_logs // (Type=Commission, CommissionType=339 Lottery) —— 用新增的 CommissionTypeLottery // 常量与 Purchase/Renewal 区分,账目侧对账更清晰。 // // 幂等模型:DispatchRequest.IdempotencyKey → lottery_grant_ledger.external_ref。 // Reserve 命中即幂等,未命中才走真实发放。commission 直接发给中奖者本人,不做 // 家庭组归位(family owner 不代收成员的抽奖佣金)。 type CommissionHandler struct { deps CommissionDeps } // CommissionDeps 是 CommissionHandler 需要的最小依赖集。抽出到接口方便测试。 type CommissionDeps struct { Ledger lottery.LedgerService // UpdateCommission 对齐 UserModel.UpdateCommission 签名。 UpdateCommission func(ctx context.Context, userId int64, delta int64, tx ...*gorm.DB) error // WriteCommissionLog 对齐 common.WriteCommissionLog 签名。 WriteCommissionLog func(tx *gorm.DB, objectID int64, logType uint16, amount int64, orderNo string) error } // NewCommissionHandler 构造真实的 commission handler。 func NewCommissionHandler(deps CommissionDeps) *CommissionHandler { return &CommissionHandler{deps: deps} } func (*CommissionHandler) Type() string { return lottery.PrizeTypeCommission } func (*CommissionHandler) IsAuto() bool { return true } func (*CommissionHandler) ValidateClaim([]byte) error { return nil } type commissionConfig struct { // AmountCents 是"分"级别的金额(与 user.commission 存储单位对齐)。 AmountCents int64 `json:"amount_cents"` } type commissionPayload struct { Amount int64 `json:"amount"` LogType uint16 `json:"log_type"` Message string `json:"message"` OrderNo string `json:"order_no"` } // Dispatch 在 caller 的事务内为中奖人发放佣金。 func (h *CommissionHandler) Dispatch(ctx context.Context, tx *gorm.DB, req lottery.DispatchRequest) (lottery.DispatchResult, error) { if tx == nil { return lottery.DispatchResult{}, errors.New("commission handler requires a transaction") } if req.IdempotencyKey == "" { return lottery.DispatchResult{}, errors.New("commission handler requires DispatchRequest.IdempotencyKey") } if h.deps.UpdateCommission == nil || h.deps.WriteCommissionLog == nil { return lottery.DispatchResult{}, errors.New("commission handler deps not fully wired") } var cfg commissionConfig if err := json.Unmarshal([]byte(req.Prize.Config), &cfg); err != nil { return lottery.DispatchResult{}, fmt.Errorf("decode commission config: %w", err) } if cfg.AmountCents <= 0 { return lottery.DispatchResult{}, fmt.Errorf("commission config amount_cents must be > 0, got %d", cfg.AmountCents) } // 佣金"发给中奖者本人"(不走家庭组归位)。 targetUserID := req.UserId entry := lottery.GrantLedger{ ExternalRef: req.IdempotencyKey, HandlerType: lottery.PrizeTypeCommission, UserId: targetUserID, ActivityId: req.ActivityId, DrawId: req.DrawId, Amount: cfg.AmountCents, } row, alreadyExisted, err := h.deps.Ledger.Reserve(ctx, tx, entry) if err != nil { return lottery.DispatchResult{}, fmt.Errorf("reserve grant ledger: %w", err) } if alreadyExisted { var payload commissionPayload if row.Payload != "" { _ = json.Unmarshal([]byte(row.Payload), &payload) } if payload.Message == "" { payload.Message = "佣金已到账" } return lottery.DispatchResult{State: lottery.DispatchStateAutoClaimed, Message: payload.Message}, nil } // 未存在 → 真实发放。UpdateCommission 用 gorm.Expr 原子累加,避免丢更新。 if err := h.deps.UpdateCommission(ctx, targetUserID, cfg.AmountCents, tx); err != nil { return lottery.DispatchResult{}, fmt.Errorf("update commission for user %d: %w", targetUserID, err) } // 传 external_ref 到 WriteCommissionLog 的 orderNo 位("lottery:*"),与业务 order 命名域天然区分。 if err := h.deps.WriteCommissionLog(tx, targetUserID, logmodel.CommissionTypeLottery, cfg.AmountCents, req.IdempotencyKey); err != nil { return lottery.DispatchResult{}, fmt.Errorf("write commission log: %w", err) } payload := commissionPayload{ Amount: cfg.AmountCents, LogType: logmodel.CommissionTypeLottery, OrderNo: req.IdempotencyKey, Message: fmt.Sprintf("佣金已到账 %d", cfg.AmountCents), } raw, err := json.Marshal(payload) if err != nil { return lottery.DispatchResult{}, fmt.Errorf("marshal ledger payload: %w", err) } if err := tx.WithContext(ctx). Model(&lottery.GrantLedger{}). Where("id = ?", row.Id). UpdateColumn("payload", string(raw)).Error; err != nil { return lottery.DispatchResult{}, fmt.Errorf("update ledger payload: %w", err) } return lottery.DispatchResult{State: lottery.DispatchStateAutoClaimed, Message: payload.Message}, nil }