This commit is contained in:
2026-03-13 15:51:59 +08:00
parent 4db2386bbf
commit 4e91f4cede
133 changed files with 19502 additions and 37 deletions

View File

@@ -0,0 +1,268 @@
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
}