Files
order/service/payment.go
2026-06-10 16:27:59 +08:00

529 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
"order/consts"
"order/dao"
"order/model/dto"
"order/model/entity"
"gitea.redpowerfuture.com/red-future/common/http"
"github.com/gogf/gf/v2/frame/g"
"go.mongodb.org/mongo-driver/v2/bson"
)
type payment struct{}
// Payment 支付服务
var Payment = new(payment)
// PayOrder 支付订单
func (s *payment) PayOrder(ctx context.Context, req *dto.PayOrderReq) (*dto.PayOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.OrderNo == "" || req.PayMethod == "" || req.PayType == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 验证订单状态(只有待支付订单可以支付)
if status != consts.OrderStatusPending {
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
}
pendingOrder, ok := order.(*entity.OrderPending)
if !ok {
return nil, errors.New("订单类型错误")
}
// 4. 获取支付配置
paymentConfig, err := dao.PaymentConfig.GetByTenantAndMethod(ctx, req.TenantID, req.PayMethod)
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{
OrderID: pendingOrder.Id,
OrderNo: req.OrderNo,
PayMethod: req.PayMethod,
PayType: req.PayType,
Amount: pendingOrder.PayAmount,
OutTradeNo: payResp.OutTradeNo,
Status: "pending",
}
if err := dao.PaymentRecord.Create(ctx, paymentRecord); err != nil {
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,
}
if err := dao.Order.UpdatePayInfo(ctx, req.OrderNo, payInfo); err != nil {
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 调用第三方支付接口
func (s *payment) callThirdPartyPayment(ctx context.Context, config *entity.PaymentConfig, req *dto.PayOrderReq, order *entity.OrderPending) (*PaymentResponse, error) {
// 这里应该是实际的第三方支付接口调用
// 为了演示,我们返回模拟数据
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 生成商户订单号
func (s *payment) generateOutTradeNo(tenantID string) string {
timestamp := time.Now().Format("20060102150405")
random := rand.Intn(10000)
return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random)
}
// generateNonceStr 生成随机字符串
func (s *payment) generateNonceStr() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 16)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// HandlePaymentNotify 处理支付回调
func (s *payment) HandlePaymentNotify(ctx context.Context, req *dto.PaymentNotifyReq) error {
// 1. 验证回调签名
if !s.verifyNotifySignature(req) {
return errors.New("签名验证失败")
}
// 2. 查询支付记录
paymentRecord, err := dao.PaymentRecord.GetByOrderNo(ctx, req.TenantID, req.OrderNo)
if err != nil {
return fmt.Errorf("查询支付记录失败: %w", err)
}
if paymentRecord == nil {
return errors.New("支付记录不存在")
}
// 3. 更新支付记录状态
if err := dao.PaymentRecord.UpdateStatus(ctx, paymentRecord.Id.Hex(), req.Status, req.TransactionID, req.TradeNo); err != nil {
return fmt.Errorf("更新支付记录失败: %w", err)
}
// 4. 如果支付成功,更新订单状态并处理库存
if req.Status == "success" {
// 获取订单信息以处理库存
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)
}
}
}
// 更新订单状态
updateData := bson.M{
"paid_at": time.Now(),
"transaction_id": req.TransactionID,
"trade_no": req.TradeNo,
"payment_channel": req.PayMethod,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusPaid, req.OrderNo, updateData); err != nil {
return fmt.Errorf("更新订单状态失败: %w", err)
}
}
return nil
}
// verifyNotifySignature 验证回调签名
func (s *payment) verifyNotifySignature(req *dto.PaymentNotifyReq) bool {
// 这里应该是实际的签名验证逻辑
// 为了演示我们总是返回true
return true
}
// RefundOrder 退款
func (s *payment) RefundOrder(ctx context.Context, req *dto.RefundOrderReq) (*dto.RefundOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.OrderNo == "" || req.RefundAmount <= 0 {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 验证订单状态(只有已支付订单可以退款)
if status != consts.OrderStatusPaid {
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{
OrderID: paidOrder.Id,
OrderNo: req.OrderNo,
RefundNo: refundResp.RefundNo,
RefundAmount: req.RefundAmount,
Reason: req.Reason,
Status: "pending",
}
if err := dao.RefundRecord.Create(ctx, refundRecord); err != nil {
return nil, fmt.Errorf("创建退款记录失败: %w", err)
}
// 7. 如果是全额退款,更新订单状态
if req.RefundAmount == paidOrder.PayAmount {
updateData := bson.M{
"refund_reason": req.Reason,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPaid, consts.OrderStatusRefunded, req.OrderNo, updateData); err != nil {
return nil, fmt.Errorf("更新订单状态失败: %w", err)
}
}
// 8. 返回退款结果
resp := &dto.RefundOrderResp{
RefundNo: refundResp.RefundNo,
RefundID: refundResp.RefundID,
RefundAmount: req.RefundAmount,
}
return resp, nil
}
// callThirdPartyRefund 调用第三方退款接口
func (s *payment) callThirdPartyRefund(ctx context.Context, req *dto.RefundOrderReq, order *entity.OrderPaid) (*RefundResponse, error) {
// 这里应该是实际的第三方退款接口调用
// 为了演示,我们返回模拟数据
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 生成退款单号
func (s *payment) generateRefundNo(tenantID string) string {
timestamp := time.Now().Format("20060102150405")
random := rand.Intn(10000)
return fmt.Sprintf("R%s%s%04d", tenantID, timestamp, random)
}
// HandleRefundNotify 处理退款回调
func (s *payment) HandleRefundNotify(ctx context.Context, req *dto.RefundNotifyReq) error {
// 1. 验证回调签名
if !s.verifyRefundNotifySignature(req) {
return errors.New("签名验证失败")
}
// 2. 查询退款记录
refundRecord, err := dao.RefundRecord.GetByRefundNo(ctx, req.TenantID, req.RefundNo)
if err != nil {
return fmt.Errorf("查询退款记录失败: %w", err)
}
if refundRecord == nil {
return errors.New("退款记录不存在")
}
// 3. 更新退款记录状态
if err := dao.RefundRecord.UpdateRefundStatus(ctx, refundRecord.Id.Hex(), req.Status, req.RefundID); err != nil {
return fmt.Errorf("更新退款记录失败: %w", err)
}
return nil
}
// verifyRefundNotifySignature 验证退款回调签名
func (s *payment) verifyRefundNotifySignature(req *dto.RefundNotifyReq) bool {
// 这里应该是实际的签名验证逻辑
// 为了演示我们总是返回true
return true
}
// 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
err := http.Get(ctx, fmt.Sprintf("http://assets-service/internal/asset/%s", assetID), nil, &assetResp)
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"`
}
err := http.Post(ctx, "http://assets-service/internal/stock/deduct", nil, &result, req)
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"`
}
err := http.Post(ctx, "http://assets-service/internal/stock/refund", nil, &result, req)
if err != nil {
return err
}
if !result.Success {
return errors.New(result.Message)
}
return nil
}