package service import ( "context" "errors" "fmt" "log/slog" "time" "github.com/go-redis/redis/v8" "pay-bridge/internal/channel" "pay-bridge/internal/errcode" "pay-bridge/internal/model" "pay-bridge/internal/repository" "pay-bridge/pkg/sequence" ) const ( sharingLockPrefix = "lock:sharing:" sharingLockTTL = 30 * time.Second ) // ProfitSharingService 分润服务 type ProfitSharingService struct { sharingRepo *repository.ProfitSharingRepository tradeRepo *repository.TradeOrderRepository channelSvc *ChannelService seqSvc *sequence.Service rdb *redis.Client } func NewProfitSharingService( sharingRepo *repository.ProfitSharingRepository, tradeRepo *repository.TradeOrderRepository, channelSvc *ChannelService, seqSvc *sequence.Service, rdb *redis.Client, ) *ProfitSharingService { return &ProfitSharingService{ sharingRepo: sharingRepo, tradeRepo: tradeRepo, channelSvc: channelSvc, seqSvc: seqSvc, rdb: rdb, } } // TriggerSharing 支付成功后触发分润(幂等) func (s *ProfitSharingService) TriggerSharing(ctx context.Context, tradeNo string) error { // 分布式锁防止并发重复触发 lockKey := sharingLockPrefix + tradeNo ok, err := s.rdb.SetNX(ctx, lockKey, "1", sharingLockTTL).Result() if err != nil { return fmt.Errorf("acquire sharing lock: %w", err) } if !ok { return nil // 已有进程在处理 } defer s.rdb.Del(ctx, lockKey) // 查询交易 order, err := s.tradeRepo.GetByTradeNo(ctx, tradeNo) if err != nil || order == nil { return errors.New(errcode.ErrOrderNotFound) } if order.ProfitSharingAmount <= 0 { return nil // 无需分润 } // 幂等检查:是否已有分润记录 existing, err := s.sharingRepo.GetOrderByTradeNo(ctx, tradeNo) if err != nil { return err } if existing != nil { return nil // 已触发过 } // 获取应用分润配置 cfg, err := s.sharingRepo.GetConfigByAppID(ctx, order.AppID) if err != nil { return err } if cfg == nil { return errors.New(errcode.ErrSharingNotConfig) } // 校验分润比例 maxAmount := int64(float64(order.Amount) * cfg.MaxSharingRatio) if order.ProfitSharingAmount > maxAmount { return errors.New(errcode.ErrSharingAmountExceed) } // 生成分润单号 sharingNo, err := s.seqSvc.NextSharingNo(ctx, order.AppID) if err != nil { return err } // 创建分润记录 sharingOrder := &model.ProfitSharingOrder{ SharingNo: sharingNo, TradeNo: tradeNo, AppID: order.AppID, ReceiverMerchantID: cfg.ReceiverMerchantID, SharingAmount: order.ProfitSharingAmount, Status: model.ProfitSharingStatusPending, } if err := s.sharingRepo.CreateOrder(ctx, sharingOrder); err != nil { return err } // 调用渠道分账 ch, err := s.channelSvc.GetChannel(ctx, order.AppID, order.ChannelCode) if err != nil { return err } resp, err := ch.ProfitSharing(ctx, &channel.ProfitSharingReq{ TradeNo: tradeNo, ChannelTradeNo: order.ChannelTradeNo, SharingNo: sharingNo, ReceiverMerchantID: cfg.ReceiverMerchantID, Amount: order.ProfitSharingAmount, }) if err != nil { s.sharingRepo.UpdateOrderStatus(ctx, sharingNo, model.ProfitSharingStatusPending, model.ProfitSharingStatusFailed, map[string]any{"fail_reason": err.Error()}, ) s.sharingRepo.CreateLog(ctx, &model.ProfitSharingLog{ SharingNo: sharingNo, Action: "SPLIT", Amount: order.ProfitSharingAmount, Status: "FAILED", }) return fmt.Errorf("profit sharing failed: %w", err) } now := time.Now() s.sharingRepo.UpdateOrderStatus(ctx, sharingNo, model.ProfitSharingStatusPending, model.ProfitSharingStatusProcessing, map[string]any{ "channel_sharing_no": resp.ChannelSharingNo, "sharing_time": now, }, ) s.sharingRepo.CreateLog(ctx, &model.ProfitSharingLog{ SharingNo: sharingNo, Action: "SPLIT", Amount: order.ProfitSharingAmount, Status: "PROCESSING", }) slog.InfoContext(ctx, "profit sharing triggered", "trade_no", tradeNo, "sharing_no", sharingNo, "amount", order.ProfitSharingAmount, ) return nil } // HandleSharingNotify 处理分账回调(上游分账完成通知) func (s *ProfitSharingService) HandleSharingNotify(ctx context.Context, sharingNo, channelSharingNo string, status model.ProfitSharingStatus) error { now := time.Now() updates := map[string]any{ "channel_sharing_no": channelSharingNo, "sharing_time": now, } ok, err := s.sharingRepo.UpdateOrderStatus(ctx, sharingNo, model.ProfitSharingStatusProcessing, status, updates) if err != nil { return err } if !ok { return nil // 幂等 } logStatus := string(status) s.sharingRepo.CreateLog(ctx, &model.ProfitSharingLog{ SharingNo: sharingNo, Action: "SPLIT", Amount: 0, Status: logStatus, }) return nil } // RollbackSharing 退款前回退分润 func (s *ProfitSharingService) RollbackSharing(ctx context.Context, tradeNo string) error { sharingOrder, err := s.sharingRepo.GetOrderByTradeNo(ctx, tradeNo) if err != nil { return err } if sharingOrder == nil { return nil // 无分润,直接跳过 } if sharingOrder.Status == model.ProfitSharingStatusRollback { return nil // 已回退,幂等 } if sharingOrder.Status != model.ProfitSharingStatusSuccess { return fmt.Errorf("sharing not success, cannot rollback, status=%s", sharingOrder.Status) } order, err := s.tradeRepo.GetByTradeNo(ctx, tradeNo) if err != nil || order == nil { return errors.New(errcode.ErrOrderNotFound) } ch, err := s.channelSvc.GetChannel(ctx, order.AppID, order.ChannelCode) if err != nil { return err } if err := ch.RollbackProfitSharing(ctx, &channel.RollbackSharingReq{ SharingNo: sharingOrder.SharingNo, ChannelSharingNo: sharingOrder.ChannelSharingNo, TradeNo: tradeNo, }); err != nil { return fmt.Errorf("rollback sharing failed: %w", err) } s.sharingRepo.UpdateOrderStatus(ctx, sharingOrder.SharingNo, model.ProfitSharingStatusSuccess, model.ProfitSharingStatusRollback, nil, ) s.sharingRepo.CreateLog(ctx, &model.ProfitSharingLog{ SharingNo: sharingOrder.SharingNo, Action: "ROLLBACK", Amount: sharingOrder.SharingAmount, Status: "SUCCESS", }) return nil } // QuerySharing 查询分润状态 func (s *ProfitSharingService) QuerySharing(ctx context.Context, sharingNo string) (*model.ProfitSharingOrder, error) { order, err := s.sharingRepo.GetOrderBySharingNo(ctx, sharingNo) if err != nil { return nil, err } if order == nil { return nil, errors.New(errcode.ErrOrderNotFound) } return order, nil } // ValidateSharingAmount 下单时校验分润金额是否合法 func (s *ProfitSharingService) ValidateSharingAmount(ctx context.Context, appID string, orderAmount, sharingAmount int64) error { if sharingAmount <= 0 { return nil } cfg, err := s.sharingRepo.GetConfigByAppID(ctx, appID) if err != nil { return err } if cfg == nil { return errors.New(errcode.ErrSharingNotConfig) } maxAmount := int64(float64(orderAmount) * cfg.MaxSharingRatio) if sharingAmount > maxAmount { return errors.New(errcode.ErrSharingAmountExceed) } return nil }