269 lines
7.1 KiB
Go
269 lines
7.1 KiB
Go
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
|
|
}
|