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}) }