draft
This commit is contained in:
189
backend/internal/service/service_fee.go
Normal file
189
backend/internal/service/service_fee.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
|
||||
"pay-bridge/internal/channel"
|
||||
"pay-bridge/internal/model"
|
||||
"pay-bridge/internal/repository"
|
||||
)
|
||||
|
||||
// ServiceFeeService 服务费服务
|
||||
type ServiceFeeService struct {
|
||||
feeRepo *repository.ServiceFeeRepository
|
||||
tradeRepo *repository.TradeOrderRepository
|
||||
channelSvc *ChannelService
|
||||
}
|
||||
|
||||
func NewServiceFeeService(
|
||||
feeRepo *repository.ServiceFeeRepository,
|
||||
tradeRepo *repository.TradeOrderRepository,
|
||||
channelSvc *ChannelService,
|
||||
) *ServiceFeeService {
|
||||
return &ServiceFeeService{
|
||||
feeRepo: feeRepo,
|
||||
tradeRepo: tradeRepo,
|
||||
channelSvc: channelSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// ChargeServiceFee 交易完成后扣收服务费
|
||||
func (s *ServiceFeeService) ChargeServiceFee(ctx context.Context, tradeNo string) error {
|
||||
order, err := s.tradeRepo.GetByTradeNo(ctx, tradeNo)
|
||||
if err != nil || order == nil {
|
||||
return fmt.Errorf("order not found: %s", tradeNo)
|
||||
}
|
||||
|
||||
// 幂等检查
|
||||
existing, err := s.feeRepo.GetLog(ctx, tradeNo, "CHARGE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
return nil // 已扣收
|
||||
}
|
||||
|
||||
// 获取服务费配置
|
||||
group := model.PayMethodToGroup(order.PayMethod)
|
||||
cfg, err := s.feeRepo.GetConfig(ctx, order.AppID, group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg == nil || cfg.FeeRate == 0 {
|
||||
return nil // 未配置或费率为0
|
||||
}
|
||||
|
||||
// 计算服务费(四舍五入到分)
|
||||
feeAmount := calculateFee(order.Amount, cfg.FeeRate)
|
||||
if feeAmount <= 0 {
|
||||
return nil // 不足1分不扣收
|
||||
}
|
||||
|
||||
// 更新订单服务费金额快照
|
||||
s.tradeRepo.UpdateStatus(ctx, tradeNo, model.TradeStatusPaid, model.TradeStatusPaid,
|
||||
map[string]any{"service_fee_amount": feeAmount})
|
||||
|
||||
// 创建服务费流水
|
||||
log := &model.ServiceFeeLog{
|
||||
TradeNo: tradeNo,
|
||||
ConfigID: cfg.ID,
|
||||
FeeAmount: feeAmount,
|
||||
FeeRate: cfg.FeeRate,
|
||||
ReceiverMerchantID: cfg.FeeReceiverMerchantID,
|
||||
Action: "CHARGE",
|
||||
Status: "PENDING",
|
||||
}
|
||||
if err := s.feeRepo.CreateLog(ctx, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 调用渠道分账
|
||||
ch, err := s.channelSvc.GetChannel(ctx, order.AppID, order.ChannelCode)
|
||||
if err != nil {
|
||||
s.feeRepo.UpdateLogStatus(ctx, log.ID, "FAILED", "")
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := ch.ProfitSharing(ctx, &channel.ProfitSharingReq{
|
||||
TradeNo: tradeNo,
|
||||
ChannelTradeNo: order.ChannelTradeNo,
|
||||
SharingNo: fmt.Sprintf("FEE%s", tradeNo),
|
||||
ReceiverMerchantID: cfg.FeeReceiverMerchantID,
|
||||
Amount: feeAmount,
|
||||
})
|
||||
if err != nil {
|
||||
s.feeRepo.UpdateLogStatus(ctx, log.ID, "FAILED", "")
|
||||
slog.WarnContext(ctx, "charge service fee failed", "trade_no", tradeNo, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.feeRepo.UpdateLogStatus(ctx, log.ID, "SUCCESS", resp.ChannelSharingNo)
|
||||
slog.InfoContext(ctx, "service fee charged", "trade_no", tradeNo, "fee_amount", feeAmount)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RollbackServiceFee 退款时回退服务费
|
||||
func (s *ServiceFeeService) RollbackServiceFee(ctx context.Context, tradeNo string) error {
|
||||
// 幂等检查
|
||||
existing, err := s.feeRepo.GetLog(ctx, tradeNo, "ROLLBACK")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
return nil // 已回退
|
||||
}
|
||||
|
||||
chargeLog, err := s.feeRepo.GetLog(ctx, tradeNo, "CHARGE")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chargeLog == nil || chargeLog.Status != "SUCCESS" {
|
||||
return nil // 没有成功扣收,无需回退
|
||||
}
|
||||
|
||||
order, err := s.tradeRepo.GetByTradeNo(ctx, tradeNo)
|
||||
if err != nil || order == nil {
|
||||
return fmt.Errorf("order not found: %s", tradeNo)
|
||||
}
|
||||
|
||||
rollbackLog := &model.ServiceFeeLog{
|
||||
TradeNo: tradeNo,
|
||||
ConfigID: chargeLog.ConfigID,
|
||||
FeeAmount: chargeLog.FeeAmount,
|
||||
FeeRate: chargeLog.FeeRate,
|
||||
ReceiverMerchantID: chargeLog.ReceiverMerchantID,
|
||||
Action: "ROLLBACK",
|
||||
Status: "PENDING",
|
||||
}
|
||||
if err := s.feeRepo.CreateLog(ctx, rollbackLog); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch, err := s.channelSvc.GetChannel(ctx, order.AppID, order.ChannelCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sharingNo := fmt.Sprintf("FEE%s", tradeNo)
|
||||
if err := ch.RollbackProfitSharing(ctx, &channel.RollbackSharingReq{
|
||||
SharingNo: sharingNo,
|
||||
ChannelSharingNo: chargeLog.ChannelSharingNo,
|
||||
TradeNo: tradeNo,
|
||||
}); err != nil {
|
||||
s.feeRepo.UpdateLogStatus(ctx, rollbackLog.ID, "FAILED", "")
|
||||
return err
|
||||
}
|
||||
|
||||
s.feeRepo.UpdateLogStatus(ctx, rollbackLog.ID, "SUCCESS", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalculateAndValidate 下单时校验分润+服务费不超过订单金额
|
||||
func (s *ServiceFeeService) CalculateAndValidate(ctx context.Context, appID string, payMethod model.PayMethod, orderAmount, sharingAmount int64) (int64, error) {
|
||||
group := model.PayMethodToGroup(payMethod)
|
||||
cfg, err := s.feeRepo.GetConfig(ctx, appID, group)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var feeAmount int64
|
||||
if cfg != nil && cfg.FeeRate > 0 {
|
||||
feeAmount = calculateFee(orderAmount, cfg.FeeRate)
|
||||
}
|
||||
|
||||
if sharingAmount+feeAmount > orderAmount {
|
||||
return 0, fmt.Errorf(errSharingFeeExceed)
|
||||
}
|
||||
return feeAmount, nil
|
||||
}
|
||||
|
||||
const errSharingFeeExceed = "30007" // errcode.ErrSharingFeeExceed
|
||||
|
||||
// calculateFee 计算服务费(四舍五入到分)
|
||||
func calculateFee(amount int64, rate float64) int64 {
|
||||
fee := float64(amount) * rate
|
||||
return int64(math.Round(fee))
|
||||
}
|
||||
Reference in New Issue
Block a user