Files
order/service/payment.go

529 lines
16 KiB
Go
Raw Normal View History

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
}