feat: 更新支付宝支付回调处理逻辑,支持新旧接口兼容,优化回调数据处理和订单状态更新
This commit is contained in:
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 需要认证的接口
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user