Files

72 lines
2.3 KiB
Go

package payment
import (
"context"
"crypto/subtle"
"fmt"
"net/http"
"time"
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"github.com/smartwalle/alipay/v3"
)
// CloseExpired 批量关闭已过期的渠道支付单;单笔失败留待下一轮安全重试。
func CloseExpired(ctx context.Context, limit int) (int, error) {
var orders []models.PaymentOrder
if err := impl.DBService.Where("payment_status = ? AND expires_at <= ?", StatusPending, time.Now()).Order("expires_at asc").Limit(limit).Find(&orders).Error; err != nil {
return 0, err
}
closed := 0
for _, order := range orders {
if err := closeChannelOrder(ctx, order); err != nil {
continue
}
now := time.Now()
result := impl.DBService.Model(&models.PaymentOrder{}).Where("id = ? AND payment_status = ?", order.ID, StatusPending).Updates(map[string]any{"payment_status": StatusClosed, "closed_at": &now})
if result.Error == nil && result.RowsAffected == 1 {
closed++
}
}
return closed, nil
}
func closeChannelOrder(ctx context.Context, order models.PaymentOrder) error {
if order.Channel == "alipay" {
client, err := alipayClient()
if err != nil {
return err
}
_, err = client.TradeClose(ctx, alipay.TradeClose{OutTradeNo: order.PaymentNo})
return err
}
if order.Channel == "wechat" {
client, err := wechatClient(ctx)
if err != nil {
return err
}
_, err = client.Post(ctx, fmt.Sprintf("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close", order.PaymentNo), map[string]string{"mchid": config.Spec.Payment.Wechat.MerchantID})
return err
}
return nil
}
// CloseExpiredHandler 只接受 Worker 共享凭证,不暴露为平台用户动作。
func CloseExpiredHandler(ctx *gin.Context) {
expected := config.Spec.Payment.InternalServiceToken
actual := ctx.GetHeader("X-Heqi-Worker-Token")
if expected == "" || len(actual) != len(expected) || subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) != 1 {
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
count, err := CloseExpired(ctx, 100)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "close expired payments failed"})
return
}
ctx.JSON(http.StatusOK, gin.H{"closed": count})
}