draft
This commit is contained in:
213
backend/internal/service/refund.go
Normal file
213
backend/internal/service/refund.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"time"
|
||||
|
||||
"pay-bridge/internal/channel"
|
||||
"pay-bridge/internal/errcode"
|
||||
"pay-bridge/internal/model"
|
||||
"pay-bridge/internal/repository"
|
||||
"pay-bridge/pkg/sequence"
|
||||
)
|
||||
|
||||
// CreateRefundReq 退款请求
|
||||
type CreateRefundReq struct {
|
||||
AppID string
|
||||
TradeNo string
|
||||
RefundAmount int64
|
||||
Reason string
|
||||
NotifyURL string
|
||||
}
|
||||
|
||||
// RefundService 退款服务
|
||||
type RefundService struct {
|
||||
refundRepo *repository.RefundOrderRepository
|
||||
tradeRepo *repository.TradeOrderRepository
|
||||
channelSvc *ChannelService
|
||||
seqSvc *sequence.Service
|
||||
notifySvc *NotifyService
|
||||
}
|
||||
|
||||
func NewRefundService(
|
||||
refundRepo *repository.RefundOrderRepository,
|
||||
tradeRepo *repository.TradeOrderRepository,
|
||||
channelSvc *ChannelService,
|
||||
seqSvc *sequence.Service,
|
||||
notifySvc *NotifyService,
|
||||
) *RefundService {
|
||||
return &RefundService{
|
||||
refundRepo: refundRepo,
|
||||
tradeRepo: tradeRepo,
|
||||
channelSvc: channelSvc,
|
||||
seqSvc: seqSvc,
|
||||
notifySvc: notifySvc,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRefund 发起退款
|
||||
func (s *RefundService) CreateRefund(ctx context.Context, req *CreateRefundReq) (*model.RefundOrder, error) {
|
||||
// 查询原交易
|
||||
order, err := s.tradeRepo.GetByTradeNo(ctx, req.TradeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if order == nil || order.AppID != req.AppID {
|
||||
return nil, errors.New(errcode.ErrOrderNotFound)
|
||||
}
|
||||
if order.Status != model.TradeStatusPaid && order.Status != model.TradeStatusRefunded {
|
||||
return nil, errors.New(errcode.ErrOrderNotPaid)
|
||||
}
|
||||
|
||||
// 校验可退金额
|
||||
refunded, err := s.refundRepo.SumRefundedAmount(ctx, req.TradeNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if refunded+req.RefundAmount > order.Amount {
|
||||
return nil, errors.New(errcode.ErrRefundAmountExceed)
|
||||
}
|
||||
|
||||
// 生成退款单号
|
||||
refundNo, err := s.seqSvc.NextRefundNo(ctx, req.AppID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建退款记录
|
||||
refund := &model.RefundOrder{
|
||||
RefundNo: refundNo,
|
||||
TradeNo: req.TradeNo,
|
||||
AppID: req.AppID,
|
||||
ChannelCode: order.ChannelCode,
|
||||
RefundAmount: req.RefundAmount,
|
||||
Reason: req.Reason,
|
||||
Status: model.RefundStatusPending,
|
||||
NotifyURL: req.NotifyURL,
|
||||
}
|
||||
if err := s.refundRepo.Create(ctx, refund); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 调用渠道退款
|
||||
ch, err := s.channelSvc.GetChannel(ctx, req.AppID, order.ChannelCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
channelResp, err := ch.Refund(ctx, &channel.RefundReq{
|
||||
TradeNo: req.TradeNo,
|
||||
ChannelTradeNo: order.ChannelTradeNo,
|
||||
RefundNo: refundNo,
|
||||
RefundAmount: req.RefundAmount,
|
||||
TotalAmount: order.Amount,
|
||||
Reason: req.Reason,
|
||||
NotifyURL: req.NotifyURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.refundRepo.UpdateStatus(ctx, refundNo, model.RefundStatusPending, model.RefundStatusFailed, nil)
|
||||
return nil, errors.New(errcode.ErrChannelRefundFail)
|
||||
}
|
||||
|
||||
// 更新渠道退款单号
|
||||
updates := map[string]any{
|
||||
"channel_refund_no": channelResp.ChannelRefundNo,
|
||||
"status": model.RefundStatusProcessing,
|
||||
}
|
||||
s.refundRepo.UpdateStatus(ctx, refundNo, model.RefundStatusPending, model.RefundStatusProcessing, updates)
|
||||
|
||||
refund.ChannelRefundNo = channelResp.ChannelRefundNo
|
||||
refund.Status = model.RefundStatusProcessing
|
||||
return refund, nil
|
||||
}
|
||||
|
||||
// QueryRefund 查询退款状态
|
||||
func (s *RefundService) QueryRefund(ctx context.Context, appID, refundNo string) (*model.RefundOrder, error) {
|
||||
refund, err := s.refundRepo.GetByRefundNo(ctx, refundNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if refund == nil || refund.AppID != appID {
|
||||
return nil, errors.New(errcode.ErrRefundNotFound)
|
||||
}
|
||||
|
||||
// 如果处于处理中,主动查询渠道
|
||||
if refund.Status == model.RefundStatusProcessing {
|
||||
s.syncRefundStatus(ctx, refund)
|
||||
// 重新查询最新状态
|
||||
refund, _ = s.refundRepo.GetByRefundNo(ctx, refundNo)
|
||||
}
|
||||
|
||||
return refund, nil
|
||||
}
|
||||
|
||||
// HandleRefundNotify 处理退款回调
|
||||
func (s *RefundService) HandleRefundNotify(ctx context.Context, refundNo string, channelRefundNo string, status model.RefundStatus) error {
|
||||
refund, err := s.refundRepo.GetByRefundNo(ctx, refundNo)
|
||||
if err != nil || refund == nil {
|
||||
return errors.New(errcode.ErrRefundNotFound)
|
||||
}
|
||||
|
||||
if refund.Status == model.RefundStatusSuccess {
|
||||
return nil // 幂等
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"channel_refund_no": channelRefundNo,
|
||||
}
|
||||
if status == model.RefundStatusSuccess {
|
||||
now := time.Now()
|
||||
updates["refund_time"] = now
|
||||
}
|
||||
|
||||
ok, err := s.refundRepo.UpdateStatus(ctx, refundNo, model.RefundStatusProcessing, status, updates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil // 幂等
|
||||
}
|
||||
|
||||
// 退款成功后通知下游
|
||||
if status == model.RefundStatusSuccess && refund.NotifyURL != "" && s.notifySvc != nil {
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
if err := s.notifySvc.SendNotify(bgCtx, refund.TradeNo, model.NotifyTypeRefund, refund.NotifyURL); err != nil {
|
||||
slog.Error("send refund notify failed", "refund_no", refundNo, "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RefundService) syncRefundStatus(ctx context.Context, refund *model.RefundOrder) {
|
||||
order, err := s.tradeRepo.GetByTradeNo(ctx, refund.TradeNo)
|
||||
if err != nil || order == nil {
|
||||
return
|
||||
}
|
||||
ch, err := s.channelSvc.GetChannel(ctx, refund.AppID, refund.ChannelCode)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := ch.QueryRefund(ctx, &channel.QueryRefundReq{
|
||||
RefundNo: refund.RefundNo,
|
||||
ChannelRefundNo: refund.ChannelRefundNo,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.Status != refund.Status {
|
||||
updates := map[string]any{
|
||||
"channel_refund_no": resp.ChannelRefundNo,
|
||||
}
|
||||
if resp.RefundTime != nil {
|
||||
updates["refund_time"] = resp.RefundTime
|
||||
}
|
||||
s.refundRepo.UpdateStatus(ctx, refund.RefundNo, refund.Status, resp.Status, updates)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user