draft
This commit is contained in:
268
backend/internal/service/profit_sharing.go
Normal file
268
backend/internal/service/profit_sharing.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user