69 lines
2.2 KiB
Go
69 lines
2.2 KiB
Go
|
|
package payment
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"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) {
|
||
|
|
if config.Spec.Payment.InternalServiceToken == "" || ctx.GetHeader("X-Heqi-Worker-Token") != config.Spec.Payment.InternalServiceToken {
|
||
|
|
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})
|
||
|
|
}
|