190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
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))
|
|
}
|