2025-12-10 09:02:41 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
"order/consts"
|
2025-12-10 09:02:41 +08:00
|
|
|
|
"order/dao"
|
|
|
|
|
|
"order/model/dto"
|
|
|
|
|
|
"order/model/entity"
|
2025-12-12 18:16:28 +08:00
|
|
|
|
|
2026-02-24 16:49:31 +08:00
|
|
|
|
"gitea.com/red-future/common/http"
|
2025-12-15 09:02:30 +08:00
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
2025-12-12 18:16:28 +08:00
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
2025-12-10 09:02:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
type payment struct{}
|
2025-12-10 09:02:41 +08:00
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
// Payment 支付服务
|
|
|
|
|
|
var Payment = new(payment)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
|
|
|
|
|
|
// PayOrder 支付订单
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) PayOrder(ctx context.Context, req *dto.PayOrderReq) (*dto.PayOrderResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
|
|
|
|
|
if req.TenantID == "" || req.OrderNo == "" || req.PayMethod == "" || req.PayType == "" {
|
|
|
|
|
|
return nil, errors.New("必填参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if order == nil {
|
|
|
|
|
|
return nil, errors.New("订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 验证订单状态(只有待支付订单可以支付)
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if status != consts.OrderStatusPending {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pendingOrder, ok := order.(*entity.OrderPending)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, errors.New("订单类型错误")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 获取支付配置
|
2025-12-10 13:51:09 +08:00
|
|
|
|
paymentConfig, err := dao.PaymentConfig.GetByTenantAndMethod(ctx, req.TenantID, req.PayMethod)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取支付配置失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if paymentConfig == nil {
|
|
|
|
|
|
return nil, errors.New("支付配置不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 调用第三方支付接口
|
|
|
|
|
|
payResp, err := s.callThirdPartyPayment(ctx, paymentConfig, req, pendingOrder)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("调用支付接口失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 创建支付记录
|
|
|
|
|
|
paymentRecord := &entity.PaymentRecord{
|
2025-12-12 16:20:47 +08:00
|
|
|
|
OrderID: pendingOrder.Id,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: req.OrderNo,
|
|
|
|
|
|
PayMethod: req.PayMethod,
|
|
|
|
|
|
PayType: req.PayType,
|
|
|
|
|
|
Amount: pendingOrder.PayAmount,
|
|
|
|
|
|
OutTradeNo: payResp.OutTradeNo,
|
|
|
|
|
|
Status: "pending",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if err := dao.PaymentRecord.Create(ctx, paymentRecord); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("创建支付记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 更新订单支付信息
|
|
|
|
|
|
payInfo := entity.PayInfo{
|
|
|
|
|
|
OutTradeNo: payResp.OutTradeNo,
|
|
|
|
|
|
QRCode: payResp.QRCode,
|
|
|
|
|
|
PayURL: payResp.PayURL,
|
|
|
|
|
|
PrepayID: payResp.PrepayID,
|
|
|
|
|
|
JSAPIParams: payResp.JSAPIParams,
|
|
|
|
|
|
APPParams: payResp.APPParams,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if err := dao.Order.UpdatePayInfo(ctx, req.OrderNo, payInfo); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("更新订单支付信息失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 返回支付信息
|
|
|
|
|
|
resp := &dto.PayOrderResp{
|
|
|
|
|
|
OrderNo: req.OrderNo,
|
|
|
|
|
|
QRCode: payResp.QRCode,
|
|
|
|
|
|
PayURL: payResp.PayURL,
|
|
|
|
|
|
PrepayID: payResp.PrepayID,
|
|
|
|
|
|
JSAPIParams: payResp.JSAPIParams,
|
|
|
|
|
|
APPParams: payResp.APPParams,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// callThirdPartyPayment 调用第三方支付接口
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) callThirdPartyPayment(ctx context.Context, config *entity.PaymentConfig, req *dto.PayOrderReq, order *entity.OrderPending) (*PaymentResponse, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 这里应该是实际的第三方支付接口调用
|
|
|
|
|
|
// 为了演示,我们返回模拟数据
|
|
|
|
|
|
|
|
|
|
|
|
outTradeNo := s.generateOutTradeNo(req.TenantID)
|
|
|
|
|
|
|
|
|
|
|
|
payResp := &PaymentResponse{
|
|
|
|
|
|
OutTradeNo: outTradeNo,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据支付类型返回不同的支付参数
|
|
|
|
|
|
switch req.PayType {
|
|
|
|
|
|
case "native":
|
|
|
|
|
|
// 扫码支付
|
|
|
|
|
|
payResp.QRCode = fmt.Sprintf("https://api.example.com/qrcode/%s", outTradeNo)
|
|
|
|
|
|
case "jsapi":
|
|
|
|
|
|
// JSAPI支付
|
|
|
|
|
|
payResp.PrepayID = fmt.Sprintf("wxprepay_%s", outTradeNo)
|
|
|
|
|
|
payResp.JSAPIParams = fmt.Sprintf(`{"appId":"%s","timeStamp":"%d","nonceStr":"%s","package":"prepay_id=%s","signType":"MD5","paySign":"signature"}`,
|
|
|
|
|
|
config.AppID, time.Now().Unix(), s.generateNonceStr(), payResp.PrepayID)
|
|
|
|
|
|
case "app":
|
|
|
|
|
|
// APP支付
|
|
|
|
|
|
payResp.APPParams = fmt.Sprintf(`{"appid":"%s","partnerid":"%s","prepayid":"%s","package":"Sign=WXPay","noncestr":"%s","timestamp":"%d","sign":"signature"}`,
|
|
|
|
|
|
config.AppID, config.MchID, payResp.PrepayID, s.generateNonceStr(), time.Now().Unix())
|
|
|
|
|
|
case "h5":
|
|
|
|
|
|
// H5支付
|
|
|
|
|
|
payResp.PayURL = fmt.Sprintf("https://api.example.com/h5pay/%s", outTradeNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return payResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PaymentResponse 支付响应
|
|
|
|
|
|
|
|
|
|
|
|
type PaymentResponse struct {
|
|
|
|
|
|
OutTradeNo string `json:"out_trade_no"` // 商户订单号
|
|
|
|
|
|
QRCode string `json:"qrcode"` // 支付二维码
|
|
|
|
|
|
PayURL string `json:"pay_url"` // 支付链接
|
|
|
|
|
|
PrepayID string `json:"prepay_id"` // 预支付ID
|
|
|
|
|
|
JSAPIParams string `json:"jsapi_params"` // JSAPI参数
|
|
|
|
|
|
APPParams string `json:"app_params"` // APP参数
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateOutTradeNo 生成商户订单号
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) generateOutTradeNo(tenantID string) string {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
timestamp := time.Now().Format("20060102150405")
|
|
|
|
|
|
random := rand.Intn(10000)
|
|
|
|
|
|
return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateNonceStr 生成随机字符串
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) generateNonceStr() string {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
|
|
b := make([]byte, 16)
|
|
|
|
|
|
for i := range b {
|
|
|
|
|
|
b[i] = charset[rand.Intn(len(charset))]
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(b)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HandlePaymentNotify 处理支付回调
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) HandlePaymentNotify(ctx context.Context, req *dto.PaymentNotifyReq) error {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 验证回调签名
|
|
|
|
|
|
if !s.verifyNotifySignature(req) {
|
|
|
|
|
|
return errors.New("签名验证失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询支付记录
|
2025-12-10 13:51:09 +08:00
|
|
|
|
paymentRecord, err := dao.PaymentRecord.GetByOrderNo(ctx, req.TenantID, req.OrderNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("查询支付记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if paymentRecord == nil {
|
|
|
|
|
|
return errors.New("支付记录不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 更新支付记录状态
|
2025-12-12 16:20:47 +08:00
|
|
|
|
if err := dao.PaymentRecord.UpdateStatus(ctx, paymentRecord.Id.Hex(), req.Status, req.TransactionID, req.TradeNo); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return fmt.Errorf("更新支付记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// 4. 如果支付成功,更新订单状态并处理库存
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if req.Status == "success" {
|
2025-12-15 09:02:30 +08:00
|
|
|
|
// 获取订单信息以处理库存
|
|
|
|
|
|
order, err := dao.Order.GetPendingOrder(ctx, req.OrderNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("获取待支付订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if order == nil {
|
|
|
|
|
|
return errors.New("待支付订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理库存扣减
|
|
|
|
|
|
for _, item := range order.OrderItems {
|
|
|
|
|
|
// 判断是否需要扣减库存
|
|
|
|
|
|
shouldDeduct, saleMode, err := s.shouldDeductStock(ctx, paymentRecord.TenantId.(string), item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "检查库存策略失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果需要扣减库存,判断扣减时机
|
|
|
|
|
|
if shouldDeduct {
|
|
|
|
|
|
timing, err := s.getDeductStockTiming(ctx, paymentRecord.TenantId.(string), item.AssetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "获取库存扣减时机失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 常规售卖:支付成功时扣减库存
|
|
|
|
|
|
if timing == "payment_success" {
|
|
|
|
|
|
quantity := len(item.Stocks) // 每个库存项数量为1
|
|
|
|
|
|
if err := s.deductStock(ctx, paymentRecord.TenantId.(string), req.OrderNo, item.AssetID, quantity, fmt.Sprintf("支付成功扣减库存,售卖方式:%s", saleMode)); err != nil {
|
|
|
|
|
|
g.Log().Errorf(ctx, "支付成功扣减库存失败,订单:%s,资产:%s,错误:%v", req.OrderNo, item.AssetID, err)
|
|
|
|
|
|
// 库存扣减失败不影响订单状态更新,但需要记录错误
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "资产 %s 支付成功扣减库存成功,订单:%s,数量:%d", item.AssetID, req.OrderNo, quantity)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新订单状态
|
2025-12-10 09:02:41 +08:00
|
|
|
|
updateData := bson.M{
|
|
|
|
|
|
"paid_at": time.Now(),
|
|
|
|
|
|
"transaction_id": req.TransactionID,
|
|
|
|
|
|
"trade_no": req.TradeNo,
|
|
|
|
|
|
"payment_channel": req.PayMethod,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusPaid, req.OrderNo, updateData); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return fmt.Errorf("更新订单状态失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// verifyNotifySignature 验证回调签名
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) verifyNotifySignature(req *dto.PaymentNotifyReq) bool {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 这里应该是实际的签名验证逻辑
|
|
|
|
|
|
// 为了演示,我们总是返回true
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RefundOrder 退款
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) RefundOrder(ctx context.Context, req *dto.RefundOrderReq) (*dto.RefundOrderResp, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 参数验证
|
|
|
|
|
|
if req.TenantID == "" || req.OrderNo == "" || req.RefundAmount <= 0 {
|
|
|
|
|
|
return nil, errors.New("必填参数不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询订单
|
2025-12-12 18:16:28 +08:00
|
|
|
|
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("获取订单失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if order == nil {
|
|
|
|
|
|
return nil, errors.New("订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 验证订单状态(只有已支付订单可以退款)
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if status != consts.OrderStatusPaid {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
paidOrder, ok := order.(*entity.OrderPaid)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, errors.New("订单类型错误")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 验证退款金额
|
|
|
|
|
|
if req.RefundAmount > paidOrder.PayAmount {
|
|
|
|
|
|
return nil, errors.New("退款金额不能超过支付金额")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 调用第三方退款接口
|
|
|
|
|
|
refundResp, err := s.callThirdPartyRefund(ctx, req, paidOrder)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("调用退款接口失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 创建退款记录
|
|
|
|
|
|
refundRecord := &entity.RefundRecord{
|
2025-12-12 16:20:47 +08:00
|
|
|
|
OrderID: paidOrder.Id,
|
2025-12-10 09:02:41 +08:00
|
|
|
|
OrderNo: req.OrderNo,
|
|
|
|
|
|
RefundNo: refundResp.RefundNo,
|
|
|
|
|
|
RefundAmount: req.RefundAmount,
|
|
|
|
|
|
Reason: req.Reason,
|
|
|
|
|
|
Status: "pending",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-10 13:51:09 +08:00
|
|
|
|
if err := dao.RefundRecord.Create(ctx, refundRecord); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("创建退款记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 如果是全额退款,更新订单状态
|
|
|
|
|
|
if req.RefundAmount == paidOrder.PayAmount {
|
|
|
|
|
|
updateData := bson.M{
|
|
|
|
|
|
"refund_reason": req.Reason,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 18:16:28 +08:00
|
|
|
|
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPaid, consts.OrderStatusRefunded, req.OrderNo, updateData); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return nil, fmt.Errorf("更新订单状态失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 返回退款结果
|
|
|
|
|
|
resp := &dto.RefundOrderResp{
|
|
|
|
|
|
RefundNo: refundResp.RefundNo,
|
|
|
|
|
|
RefundID: refundResp.RefundID,
|
|
|
|
|
|
RefundAmount: req.RefundAmount,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// callThirdPartyRefund 调用第三方退款接口
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) callThirdPartyRefund(ctx context.Context, req *dto.RefundOrderReq, order *entity.OrderPaid) (*RefundResponse, error) {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 这里应该是实际的第三方退款接口调用
|
|
|
|
|
|
// 为了演示,我们返回模拟数据
|
|
|
|
|
|
|
|
|
|
|
|
refundNo := s.generateRefundNo(req.TenantID)
|
|
|
|
|
|
refundID := fmt.Sprintf("refund_%s", refundNo)
|
|
|
|
|
|
|
|
|
|
|
|
return &RefundResponse{
|
|
|
|
|
|
RefundNo: refundNo,
|
|
|
|
|
|
RefundID: refundID,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RefundResponse 退款响应
|
|
|
|
|
|
|
|
|
|
|
|
type RefundResponse struct {
|
|
|
|
|
|
RefundNo string `json:"refund_no"` // 退款单号
|
|
|
|
|
|
RefundID string `json:"refund_id"` // 退款ID
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateRefundNo 生成退款单号
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) generateRefundNo(tenantID string) string {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
timestamp := time.Now().Format("20060102150405")
|
|
|
|
|
|
random := rand.Intn(10000)
|
|
|
|
|
|
return fmt.Sprintf("R%s%s%04d", tenantID, timestamp, random)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HandleRefundNotify 处理退款回调
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) HandleRefundNotify(ctx context.Context, req *dto.RefundNotifyReq) error {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 1. 验证回调签名
|
|
|
|
|
|
if !s.verifyRefundNotifySignature(req) {
|
|
|
|
|
|
return errors.New("签名验证失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询退款记录
|
2025-12-10 13:51:09 +08:00
|
|
|
|
refundRecord, err := dao.RefundRecord.GetByRefundNo(ctx, req.TenantID, req.RefundNo)
|
2025-12-10 09:02:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("查询退款记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if refundRecord == nil {
|
|
|
|
|
|
return errors.New("退款记录不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 更新退款记录状态
|
2025-12-12 16:20:47 +08:00
|
|
|
|
if err := dao.RefundRecord.UpdateRefundStatus(ctx, refundRecord.Id.Hex(), req.Status, req.RefundID); err != nil {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
return fmt.Errorf("更新退款记录失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// verifyRefundNotifySignature 验证退款回调签名
|
2025-12-10 13:51:09 +08:00
|
|
|
|
func (s *payment) verifyRefundNotifySignature(req *dto.RefundNotifyReq) bool {
|
2025-12-10 09:02:41 +08:00
|
|
|
|
// 这里应该是实际的签名验证逻辑
|
|
|
|
|
|
// 为了演示,我们总是返回true
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2025-12-15 09:02:30 +08:00
|
|
|
|
|
|
|
|
|
|
// shouldDeductStock 判断是否需要扣减库存
|
|
|
|
|
|
func (s *payment) shouldDeductStock(ctx context.Context, tenantID, assetID string) (bool, string, error) {
|
|
|
|
|
|
// 调用assets服务获取资产信息
|
|
|
|
|
|
assetResp, err := s.getAssetFromAssetService(ctx, tenantID, assetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, "", fmt.Errorf("获取资产信息失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if assetResp == nil {
|
|
|
|
|
|
return false, "", errors.New("资产不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 无库存限制的资产不需要扣减库存
|
|
|
|
|
|
if assetResp.UnlimitedStock {
|
|
|
|
|
|
return false, "unlimited_stock", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 预售场景不扣减库存
|
|
|
|
|
|
if assetResp.SaleMode == "presale" {
|
|
|
|
|
|
return false, "presale_no_deduct", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有库存限制的常规售卖和秒杀需要扣减库存
|
|
|
|
|
|
return true, assetResp.SaleMode, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getDeductStockTiming 获取库存扣减时机
|
|
|
|
|
|
func (s *payment) getDeductStockTiming(ctx context.Context, tenantID, assetID string) (string, error) {
|
|
|
|
|
|
assetResp, err := s.getAssetFromAssetService(ctx, tenantID, assetID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("获取资产信息失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if assetResp == nil {
|
|
|
|
|
|
return "", errors.New("资产不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据售卖方式确定扣减时机
|
|
|
|
|
|
switch assetResp.SaleMode {
|
|
|
|
|
|
case "flash":
|
|
|
|
|
|
return "order_create", nil // 秒杀:下单时扣减库存
|
|
|
|
|
|
case "regular":
|
|
|
|
|
|
return "payment_success", nil // 常规售卖:支付成功时扣减库存
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "", errors.New("未知售卖方式")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// deductStock 扣减库存
|
|
|
|
|
|
func (s *payment) deductStock(ctx context.Context, tenantID, orderNo, assetID string, quantity int, reason string) error {
|
|
|
|
|
|
// 调用assets服务扣减库存
|
|
|
|
|
|
err := s.deductStockFromAssetService(ctx, tenantID, assetID, quantity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("扣减库存失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "库存扣减成功,订单:%s,资产:%s,数量:%d,原因:%s", orderNo, assetID, quantity, reason)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// refundStock 回充库存
|
|
|
|
|
|
func (s *payment) refundStock(ctx context.Context, tenantID, orderNo, assetID string, quantity int, reason string) error {
|
|
|
|
|
|
// 调用资产服务回充库存
|
|
|
|
|
|
err := s.refundStockFromAssetService(ctx, tenantID, assetID, quantity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("回充库存失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
g.Log().Infof(ctx, "库存回充成功,订单:%s,资产:%s,数量:%d,原因:%s", orderNo, assetID, quantity, reason)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getAssetFromAssetService 从资产服务获取资产信息
|
|
|
|
|
|
func (s *payment) getAssetFromAssetService(ctx context.Context, tenantID, assetID string) (*AssetServiceResponse, error) {
|
|
|
|
|
|
// 使用common/http中的封装方法调用资产服务
|
|
|
|
|
|
// 这里应该调用assets服务的getAsset接口
|
|
|
|
|
|
// 暂时返回模拟数据
|
|
|
|
|
|
var assetResp AssetServiceResponse
|
2025-12-23 17:25:23 +08:00
|
|
|
|
err := http.Get(ctx, fmt.Sprintf("http://assets-service/internal/asset/%s", assetID), nil, &assetResp)
|
2025-12-15 09:02:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &assetResp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// deductStockFromAssetService 从资产服务扣减库存
|
|
|
|
|
|
func (s *payment) deductStockFromAssetService(ctx context.Context, tenantID, assetID string, quantity int) error {
|
|
|
|
|
|
// 调用assets服务的扣减库存接口
|
|
|
|
|
|
req := map[string]interface{}{
|
|
|
|
|
|
"asset_id": assetID,
|
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
|
"reason": "订单扣减",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:25:23 +08:00
|
|
|
|
err := http.Post(ctx, "http://assets-service/internal/stock/deduct", nil, &result, req)
|
2025-12-15 09:02:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Success {
|
|
|
|
|
|
return errors.New(result.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// refundStockFromAssetService 从资产服务回充库存
|
|
|
|
|
|
func (s *payment) refundStockFromAssetService(ctx context.Context, tenantID, assetID string, quantity int) error {
|
|
|
|
|
|
// 调用资产服务的回充库存接口
|
|
|
|
|
|
req := map[string]interface{}{
|
|
|
|
|
|
"asset_id": assetID,
|
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
|
"reason": "订单取消回充",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 17:25:23 +08:00
|
|
|
|
err := http.Post(ctx, "http://assets-service/internal/stock/refund", nil, &result, req)
|
2025-12-15 09:02:30 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !result.Success {
|
|
|
|
|
|
return errors.New(result.Message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|