feat: 更新支付宝支付回调处理逻辑,支持新旧接口兼容,优化回调数据处理和订单状态更新

This commit is contained in:
wood911010 committed 2026-01-17 00:00:23 +08:00
1 parent 973b549298
commit 612bf9090e
4 files changed
+237 -47

No files matched your search

@@ -228,7 +228,7 @@ notification, err := client.GetTradeNotification(request)
### 3. 支付回调处理
**接口**: `POST /api/v1/payment/alipay/callback`
**接口**: `POST /api/payment/alipay/callback`(兼容:`POST /api/v1/payment/alipay/callback`)
支付宝服务器回调,系统验证签名后更新订单状态。
@@ -316,7 +316,7 @@ payment:
private_key: "your_private_key"
public_key: "alipay_public_key"
gateway_url: "https://openapi.alipay.com/gateway.do"
notify_url: "https://your-domain.com/api/v1/payment/alipay/callback"
notify_url: "https://your-domain.com/api/payment/alipay/callback"
return_url: "https://your-domain.com/payment/success"
sign_type: "RSA2"
charset: "utf-8"
@@ -154,5 +154,20 @@ func (h *PaymentHandler) AlipayCallback(c *gin.Context) {
// 暂时使用简化的处理方式
// 实际项目中应该使用service中的client来验证和处理
if err := c.Request.ParseForm(); err != nil {
utils.SendNotificationResponse(c.Writer, false)
return
}
values := c.Request.PostForm
if values == nil || len(values) == 0 {
values = c.Request.Form
}
if err := h.paymentService.ProcessAlipayCallback(c.Request.Context(), values); err != nil {
utils.SendNotificationResponse(c.Writer, false)
return
}
utils.SendNotificationResponse(c.Writer, true)
}
+2 -1
View File
@@ -90,7 +90,7 @@ func SetupRouter() *gin.Engine {
authCodeService := service.NewAuthorizationCodeService(authCodeRepo, customerRepo, cuUserRepo, licenseRepo)
cuOrderService := service.NewCuOrderService(cuOrderRepo, cuUserRepo, authCodeRepo, db)
cuDeviceService := service.NewCuDeviceService(licenseRepo)
paymentService := service.NewPaymentService(paymentRepo, cuOrderRepo, convertPaymentConfig(cfg.Payment))
paymentService := service.NewPaymentService(paymentRepo, cuOrderRepo, convertPaymentConfig(cfg.Payment), db)
enumService := service.NewEnumService()
licenseService := service.NewLicenseService(licenseRepo, db, log)
dashboardService := service.NewDashboardService(dashboardRepo)
@@ -130,6 +130,7 @@ func SetupRouter() *gin.Engine {
// 支付回调接口
public.POST("/payment/alipay/callback", paymentHandler.AlipayCallback)
public.POST("/v1/payment/alipay/callback", paymentHandler.AlipayCallback)
}
// 需要认证的接口
+218 -44
View File
@@ -2,20 +2,26 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
"license-manager/internal/models"
"license-manager/internal/repository"
"license-manager/pkg/utils"
"time"
"github.com/smartwalle/alipay/v3"
"gorm.io/gorm"
)
type PaymentService interface {
CreatePayment(ctx context.Context, req *models.PaymentCreateRequest) (*models.Payment, error)
GetPayment(ctx context.Context, paymentNo string) (*models.Payment, error)
GetPaymentStatus(ctx context.Context, paymentNo string) (*models.PaymentStatusResponse, error)
ProcessPaymentCallback(ctx context.Context, notification interface{}) error
ProcessAlipayCallback(ctx context.Context, values url.Values) error
GetUserPayments(ctx context.Context, customerID, cuUserID string, page, pageSize int) (*models.PaymentListResponse, error)
CancelExpiredPayments(ctx context.Context) error
}
@@ -25,6 +31,7 @@ type paymentService struct {
cuOrderRepo repository.CuOrderRepository
alipayClient *alipay.Client
config *PaymentConfig
db *gorm.DB
}
type PaymentConfig struct {
@@ -50,36 +57,33 @@ func NewPaymentService(
paymentRepo repository.PaymentRepository,
cuOrderRepo repository.CuOrderRepository,
config *PaymentConfig,
db *gorm.DB,
) PaymentService {
var alipayClient *alipay.Client
fmt.Printf("PaymentService: Checking providers, config.Providers is nil: %v\n", config.Providers == nil)
if config.Providers != nil {
fmt.Printf("PaymentService: Found %d providers\n", len(config.Providers))
for providerName := range config.Providers {
fmt.Printf("PaymentService: Provider: %s\n", providerName)
}
if alipayConfig, exists := config.Providers["alipay"]; exists && alipayConfig.Enabled {
fmt.Printf("Initializing Alipay client with APPID: %s\n", alipayConfig.AppID)
fmt.Printf("Private key length: %d\n", len(alipayConfig.PrivateKey))
fmt.Printf("Public key length: %d\n", len(alipayConfig.PublicKey))
// 使用官方SDK创建客户端
fmt.Printf("Creating Alipay client with APPID: %s\n", alipayConfig.AppID)
client, err := alipay.New(alipayConfig.AppID, alipayConfig.PrivateKey, false)
if config != nil && config.Providers != nil {
if alipayConfig, exists := config.Providers["alipay"]; exists && alipayConfig.Enabled {
isProduction := strings.Contains(strings.ToLower(alipayConfig.GatewayURL), "openapi.alipay.com") &&
!strings.Contains(strings.ToLower(alipayConfig.GatewayURL), "sandbox")
client, err := alipay.New(
alipayConfig.AppID,
alipayConfig.PrivateKey,
isProduction,
alipay.WithSandboxGateway(alipayConfig.GatewayURL),
alipay.WithProductionGateway(alipayConfig.GatewayURL),
)
if err != nil {
fmt.Printf("Failed to create Alipay client: %v\n", err)
fmt.Printf("Private key length: %d\n", len(alipayConfig.PrivateKey))
} else {
fmt.Printf("Alipay client created successfully\n")
// 先不加载公钥,测试基本功能
if alipayConfig.PublicKey != "" {
if err := client.LoadAliPayPublicKey(alipayConfig.PublicKey); err != nil {
fmt.Printf("Failed to load Alipay public key: %v\n", err)
}
}
alipayClient = client
fmt.Println("Alipay client initialized successfully")
}
} else {
fmt.Println("Alipay provider not found or disabled")
}
} else {
fmt.Println("No payment providers configured")
}
return &paymentService{
@@ -87,6 +91,7 @@ func NewPaymentService(
cuOrderRepo: cuOrderRepo,
alipayClient: alipayClient,
config: config,
db: db,
}
}
@@ -95,7 +100,7 @@ func (s *paymentService) CreatePayment(ctx context.Context, req *models.PaymentC
if req.Currency == "" {
req.Currency = "CNY"
}
if req.PaymentMethod == "" {
if s.config != nil && req.PaymentMethod == "" {
req.PaymentMethod = s.config.DefaultMethod
}
@@ -103,7 +108,11 @@ func (s *paymentService) CreatePayment(ctx context.Context, req *models.PaymentC
paymentNo := utils.GeneratePaymentNo()
// 计算过期时间
expireTime := time.Now().Add(time.Duration(s.config.ExpireMinutes) * time.Minute)
expireMinutes := 30
if s.config != nil && s.config.ExpireMinutes > 0 {
expireMinutes = s.config.ExpireMinutes
}
expireTime := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
// 创建支付单
payment := &models.Payment{
@@ -121,39 +130,36 @@ func (s *paymentService) CreatePayment(ctx context.Context, req *models.PaymentC
}
// 如果是支付宝,生成支付URL
if req.PaymentMethod == "alipay" {
fmt.Printf("Payment method is alipay, checking client...\n")
if req.PaymentMethod == models.PaymentMethodAlipay {
if s.alipayClient == nil {
fmt.Printf("Alipay client is nil!\n")
return nil, fmt.Errorf("alipay client not initialized")
}
fmt.Printf("Alipay client is available, creating payment request...\n")
var notifyURL, returnURL string
if s.config != nil && s.config.Providers != nil {
if provider, ok := s.config.Providers["alipay"]; ok && provider != nil {
notifyURL = strings.TrimSpace(provider.NotifyURL)
returnURL = strings.TrimSpace(provider.ReturnURL)
}
}
// 使用官方SDK创建支付请求
// TradePagePay继承了Trade,需要通过Trade字段设置
payRequest := alipay.TradePagePay{
Trade: alipay.Trade{
OutTradeNo: paymentNo,
TotalAmount: fmt.Sprintf("%.2f", req.Amount),
Subject: "产品套餐购买",
ProductCode: "FAST_INSTANT_TRADE_PAY",
NotifyURL: notifyURL,
ReturnURL: returnURL,
},
}
fmt.Printf("Creating Alipay payment request: OutTradeNo=%s, TotalAmount=%s\n", paymentNo, fmt.Sprintf("%.2f", req.Amount))
// 生成支付URL
payURL, err := s.alipayClient.TradePagePay(payRequest)
if err != nil {
fmt.Printf("TradePagePay failed: %v\n", err)
return nil, fmt.Errorf("generate payment URL failed: %w", err)
}
payURLStr := payURL.String()
fmt.Printf("Generated payment URL: %s\n", payURLStr[:100]+"...")
payment.PaymentURL = &payURLStr
} else {
fmt.Printf("Payment method is: %s\n", req.PaymentMethod)
}
// 保存到数据库
@@ -199,13 +205,181 @@ func (s *paymentService) GetPaymentStatus(ctx context.Context, paymentNo string)
return response, nil
}
func (s *paymentService) ProcessPaymentCallback(ctx context.Context, notification interface{}) error {
// 这里暂时简化处理回调逻辑
// 实际项目中应该进行完整的验证和处理
// 包括签名验证、状态检查、业务逻辑处理等
func (s *paymentService) ProcessAlipayCallback(ctx context.Context, values url.Values) error {
if s.alipayClient == nil {
return errors.New("alipay client not initialized")
}
if s.db == nil {
return errors.New("db not initialized")
}
if values == nil || len(values) == 0 {
return errors.New("empty callback values")
}
// 暂时返回成功,表示回调处理完成
return nil
notification, err := s.alipayClient.DecodeNotification(values)
if err != nil {
return err
}
paymentNo := strings.TrimSpace(notification.OutTradeNo)
if paymentNo == "" {
return errors.New("missing out_trade_no")
}
notifyDataBytes, err := json.Marshal(values)
if err != nil {
return err
}
notifyDataStr := string(notifyDataBytes)
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var payment models.Payment
if err := tx.Where("payment_no = ?", paymentNo).First(&payment).Error; err != nil {
return err
}
// 金额二次校验
expectedAmount := fmt.Sprintf("%.2f", payment.Amount)
if notification.TotalAmount != "" && expectedAmount != notification.TotalAmount {
return fmt.Errorf("payment amount mismatch: expected %s, got %s", expectedAmount, notification.TotalAmount)
}
updates := map[string]interface{}{
"notify_data": notifyDataStr,
}
if strings.TrimSpace(notification.TradeNo) != "" {
updates["trade_no"] = strings.TrimSpace(notification.TradeNo)
}
isPaid := false
switch notification.TradeStatus {
case alipay.TradeStatusSuccess, alipay.TradeStatusFinished:
isPaid = true
updates["status"] = models.PaymentStatusPaid
if notification.GmtPayment != "" {
if t, err := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local); err == nil {
updates["payment_time"] = t
}
}
case alipay.TradeStatusClosed:
// 已支付的订单不允许被回调覆盖为取消
if payment.Status != models.PaymentStatusPaid {
updates["status"] = models.PaymentStatusCancelled
}
default:
// WAIT_BUYER_PAY / 未知状态:只记录回调数据,不改变业务状态
}
if err := tx.Model(&models.Payment{}).Where("payment_no = ?", paymentNo).Updates(updates).Error; err != nil {
return err
}
if !isPaid {
return nil
}
// 支付成功:根据业务类型处理
if payment.BusinessType != models.BusinessTypePackageOrder || payment.BusinessID == nil || strings.TrimSpace(*payment.BusinessID) == "" {
return nil
}
var order models.CuOrder
if err := tx.Where("id = ? AND deleted_at IS NULL", *payment.BusinessID).First(&order).Error; err != nil {
return err
}
// 幂等:订单已生成授权码则不重复创建
if order.AuthorizationCode != nil && strings.TrimSpace(*order.AuthorizationCode) != "" && order.Status == "paid" {
return nil
}
// 生成授权码并写入授权码表 & 订单(沿用当前 free order 的规则)
authCode, err := utils.GenerateLegacyAuthorizationCode(order.CustomerID)
if err != nil {
return err
}
now := time.Now()
var startDate, endDate time.Time
var expiredAt *time.Time
if order.PackageID == "trial" {
trialExpiry := time.Date(now.Year(), now.Month(), 25, 23, 59, 59, 0, now.Location())
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endDate = trialExpiry
expiredAt = &trialExpiry
} else {
startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endDate = startDate.AddDate(1000, 0, 0)
expiredAt = nil
}
var featureConfigMap, usageLimitsMap map[string]interface{}
if order.PackageID == "basic" {
usageLimitsMap = map[string]interface{}{
"type": "standard",
}
}
customParametersMap := map[string]interface{}{
"package_id": order.PackageID,
"package_name": order.PackageName,
"license_count": order.LicenseCount,
}
var featureConfig, usageLimits, customParameters models.JSON
if featureConfigMap != nil {
b, err := json.Marshal(featureConfigMap)
if err != nil {
return err
}
featureConfig = models.JSON(b)
}
if usageLimitsMap != nil {
b, err := json.Marshal(usageLimitsMap)
if err != nil {
return err
}
usageLimits = models.JSON(b)
}
b, err := json.Marshal(customParametersMap)
if err != nil {
return err
}
customParameters = models.JSON(b)
description := fmt.Sprintf("%s - %d个授权", order.PackageName, order.LicenseCount)
encryptionType := "standard"
authCodeEntity := &models.AuthorizationCode{
Code: authCode,
CustomerID: order.CustomerID,
CreatedBy: order.CuUserID,
Description: &description,
StartDate: startDate,
EndDate: endDate,
DeploymentType: "cloud",
EncryptionType: &encryptionType,
MaxActivations: order.LicenseCount,
IsLocked: false,
FeatureConfig: featureConfig,
UsageLimits: usageLimits,
CustomParameters: customParameters,
}
if err := tx.Create(authCodeEntity).Error; err != nil {
return err
}
orderUpdates := map[string]interface{}{
"status": "paid",
"authorization_code": authCode,
"expired_at": expiredAt,
"updated_at": now,
}
if err := tx.Model(&models.CuOrder{}).Where("id = ?", order.ID).Updates(orderUpdates).Error; err != nil {
return err
}
return nil
})
}
func (s *paymentService) GetUserPayments(ctx context.Context, customerID, cuUserID string, page, pageSize int) (*models.PaymentListResponse, error) {