Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 40s
feat(pay): 添加支付API基础结构 feat(miniapp): 创建支付测试小程序页面与配置 feat(wechatpay): 配置微信支付参数与证书 fix(guild): 修复成员列表查询条件 docs: 更新代码规范文档与需求文档 style: 统一前后端枚举显示与注释格式 refactor(admin): 重构用户奖励发放接口参数处理 test(title): 添加称号效果参数验证测试
135 lines
6.4 KiB
Go
135 lines
6.4 KiB
Go
package pay
|
||
|
||
import (
|
||
"net/http"
|
||
"time"
|
||
"encoding/json"
|
||
|
||
"bindbox-game/configs"
|
||
"bindbox-game/internal/code"
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
|
||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||
)
|
||
|
||
type notifyAck struct {
|
||
Code string `json:"code"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
// WechatNotify 微信支付回调通知处理
|
||
// 入参:微信官方通知,验签并解密resource,推进订单状态为已支付
|
||
// 幂等:若订单已为已支付则直接ACK
|
||
func (h *handler) WechatNotify() core.HandlerFunc {
|
||
return func(ctx core.Context) {
|
||
c := configs.Get()
|
||
if c.WechatPay.ApiV3Key == "" {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
|
||
return
|
||
}
|
||
var handler *notify.Handler
|
||
if c.WechatPay.PublicKeyID != "" && c.WechatPay.PublicKeyPath != "" {
|
||
pubKey, err := utils.LoadPublicKeyWithPath(c.WechatPay.PublicKeyPath)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, err.Error()))
|
||
return
|
||
}
|
||
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(c.WechatPay.PublicKeyID, *pubKey))
|
||
} else {
|
||
if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete"))
|
||
return
|
||
}
|
||
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.WechatPay.PrivateKeyPath)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, err.Error()))
|
||
return
|
||
}
|
||
if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext(), mchPrivateKey, c.WechatPay.SerialNo, c.WechatPay.MchID, c.WechatPay.ApiV3Key); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, err.Error()))
|
||
return
|
||
}
|
||
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(c.WechatPay.MchID)
|
||
handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
|
||
}
|
||
|
||
var transaction payments.Transaction
|
||
notification, err := handler.ParseNotifyRequest(ctx.RequestContext(), ctx.Request(), &transaction)
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error()))
|
||
return
|
||
}
|
||
// 事件去重处理
|
||
var existed *model.PaymentNotifyEvents
|
||
if notification != nil && notification.ID != "" {
|
||
existed, _ = h.readDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).First()
|
||
if existed != nil && existed.Processed {
|
||
ctx.Payload(¬ifyAck{Code: "SUCCESS", Message: "OK"})
|
||
return
|
||
}
|
||
}
|
||
if transaction.OutTradeNo == nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "missing out_trade_no"))
|
||
return
|
||
}
|
||
paidAt := time.Now()
|
||
if transaction.SuccessTime != nil {
|
||
if t, err := time.Parse(time.RFC3339, *transaction.SuccessTime); err == nil {
|
||
paidAt = t
|
||
}
|
||
}
|
||
rawStr := func() string { b, _ := json.Marshal(transaction); return string(b) }()
|
||
order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(*transaction.OutTradeNo)).First()
|
||
if err != nil || order == nil {
|
||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "order not found for out_trade_no"))
|
||
return
|
||
}
|
||
tx := &model.PaymentTransactions{
|
||
OrderID: order.ID,
|
||
OrderNo: *transaction.OutTradeNo,
|
||
Channel: "wechat_jsapi",
|
||
TransactionID: func() string { if transaction.TransactionId!=nil { return *transaction.TransactionId }; return "" }(),
|
||
AmountTotal: func() int64 { if transaction.Amount!=nil && transaction.Amount.Total!=nil { return *transaction.Amount.Total }; return 0 }(),
|
||
SuccessTime: paidAt,
|
||
Raw: rawStr,
|
||
}
|
||
if err := h.writeDB.PaymentTransactions.WithContext(ctx.RequestContext()).Create(tx); err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150006, err.Error()))
|
||
return
|
||
}
|
||
// 记录事件
|
||
if notification != nil && notification.ID != "" {
|
||
if existed == nil {
|
||
_ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Create(&model.PaymentNotifyEvents{
|
||
NotifyID: notification.ID,
|
||
ResourceType: notification.ResourceType,
|
||
EventType: notification.EventType,
|
||
Summary: notification.Summary,
|
||
Raw: rawStr,
|
||
Processed: false,
|
||
})
|
||
}
|
||
}
|
||
_, err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(*transaction.OutTradeNo), h.writeDB.Orders.Status.Eq(1)).Updates(map[string]any{
|
||
h.writeDB.Orders.Status.ColumnName().String(): 2,
|
||
h.writeDB.Orders.PaidAt.ColumnName().String(): paidAt,
|
||
})
|
||
if err != nil {
|
||
ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150005, err.Error()))
|
||
return
|
||
}
|
||
// 标记事件处理完成
|
||
if notification != nil && notification.ID != "" {
|
||
_, _ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).Updates(map[string]any{
|
||
h.writeDB.PaymentNotifyEvents.Processed.ColumnName().String(): true,
|
||
})
|
||
}
|
||
ctx.Payload(¬ifyAck{Code: "SUCCESS", Message: "OK"})
|
||
}
|
||
}
|