Files
junhong_cmp_fiber/internal/infrastructure/payment/wechat_web.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

209 lines
9.3 KiB
Go

// Package payment 提供代理在线充值使用的支付渠道薄适配器。
package payment
import (
"context"
"strconv"
"time"
"github.com/ArtisanCloud/PowerWeChat/v3/src/kernel"
sdkpayment "github.com/ArtisanCloud/PowerWeChat/v3/src/payment"
"go.uber.org/zap"
agentrecharge "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
wechatpay "github.com/break/junhong_cmp_fiber/pkg/wechat"
)
// WechatWebAdapter 按当前支付配置生成微信 H5/MWEB 支付链接并查单。
type WechatWebAdapter struct {
cache kernel.CacheInterface
integration *integrationlog.Repository
logger *zap.Logger
}
// NewWechatWebAdapter 创建微信 H5/MWEB 支付适配器。
func NewWechatWebAdapter(cache kernel.CacheInterface, integration *integrationlog.Repository, logger *zap.Logger) *WechatWebAdapter {
return &WechatWebAdapter{cache: cache, integration: integration, logger: logger}
}
// Available 判断当前协议配置是否完整支持 H5/MWEB 下单、验签与查单。
func (a *WechatWebAdapter) Available(config *model.WechatConfig) bool {
return wechatConfigComplete(config, true) || wechatV2ConfigComplete(config, true)
}
// CreatePaymentURL 按当前协议生成微信 H5 或 MWEB 支付链接。
func (a *WechatWebAdapter) CreatePaymentURL(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentResult, error) {
attempt, err := a.startAttempt(ctx, request, constants.IntegrationOperationPaymentPreCreate, request.Config.ID, request.Amount)
if err != nil {
return agentrecharge.OnlinePaymentResult{}, err
}
startedAt := time.Now()
response, callErr := a.createH5Order(ctx, request, &wechatpay.H5SceneInfo{
PayerClientIP: request.PayerClientIP,
H5Type: "Wap",
})
if callErr != nil {
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"success": true},
DurationMS: time.Since(startedAt).Milliseconds(), StateChanged: true,
}); err != nil {
return agentrecharge.OnlinePaymentResult{}, err
}
return agentrecharge.OnlinePaymentResult{QRContent: response.H5URL}, nil
}
// Query 按创建支付单时的协议查询微信支付状态。
func (a *WechatWebAdapter) Query(ctx context.Context, request agentrecharge.OnlinePaymentRequest) (agentrecharge.OnlinePaymentQueryResult, error) {
attempt, err := a.startAttempt(ctx, request, constants.IntegrationOperationPaymentQuery, request.Config.ID, 0)
if err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, err
}
startedAt := time.Now()
info, callErr := a.queryOrder(ctx, request.Config, request.PaymentNo)
if callErr != nil {
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
result := agentrecharge.OnlinePaymentQueryResult{
State: mapWechatTradeState(info.TradeState), ThirdPartyTradeNo: info.TransactionID, Amount: info.TotalAmount,
}
if paidAt, ok := parseWechatPaidAt(info.SuccessTime); ok {
result.PaidAt = &paidAt
}
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultSuccess, ProviderCode: info.TradeState,
ResponseSummary: map[string]any{"state": result.State, "has_trade_no": result.ThirdPartyTradeNo != ""},
DurationMS: time.Since(startedAt).Milliseconds(),
}); err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, err
}
return result, nil
}
func parseWechatPaidAt(value string) (time.Time, bool) {
for _, layout := range []string{time.RFC3339, "20060102150405"} {
paidAt, err := time.ParseInLocation(layout, value, time.Local)
if err == nil {
return paidAt, true
}
}
return time.Time{}, false
}
func (a *WechatWebAdapter) paymentApp(config *model.WechatConfig) (*sdkpayment.Payment, error) {
if !wechatConfigComplete(config, false) {
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "微信 H5 支付配置不可用")
}
app, err := wechatpay.NewPaymentAppFromConfig(config, config.OaAppID, a.cache, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 H5 支付配置不可用")
}
return app, nil
}
func (a *WechatWebAdapter) createH5Order(ctx context.Context, request agentrecharge.OnlinePaymentRequest, sceneInfo *wechatpay.H5SceneInfo) (*wechatpay.H5PayResult, error) {
switch request.Config.ProviderType {
case model.ProviderTypeWechat:
app, err := a.paymentApp(request.Config)
if err != nil {
return nil, err
}
return wechatpay.NewPaymentService(app, a.logger).CreateH5Order(ctx, request.PaymentNo, request.Description, int(request.Amount), sceneInfo)
case model.ProviderTypeWechatV2:
service, err := wechatpay.NewPaymentV2ServiceFromConfig(request.Config, request.Config.OaAppID, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 MWEB 支付配置不可用")
}
return service.CreateH5Order(ctx, request.PaymentNo, request.Description, int(request.Amount), sceneInfo)
default:
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "当前支付渠道不支持微信网页支付")
}
}
func (a *WechatWebAdapter) queryOrder(ctx context.Context, config *model.WechatConfig, paymentNo string) (*wechatpay.OrderInfo, error) {
switch config.ProviderType {
case model.ProviderTypeWechat:
app, err := a.queryPaymentApp(config)
if err != nil {
return nil, err
}
return wechatpay.NewPaymentService(app, a.logger).QueryOrder(ctx, paymentNo)
case model.ProviderTypeWechatV2:
service, err := wechatpay.NewPaymentV2ServiceFromConfig(config, config.OaAppID, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信 v2 查单配置不可用")
}
return service.QueryOrder(ctx, paymentNo)
default:
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "当前支付渠道不支持微信查单")
}
}
func (a *WechatWebAdapter) queryPaymentApp(config *model.WechatConfig) (*sdkpayment.Payment, error) {
if !wechatConfigComplete(config, false) {
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "微信查单配置不可用")
}
app, err := wechatpay.NewPaymentAppFromConfig(config, config.OaAppID, a.cache, a.logger)
if err != nil {
return nil, apperrors.Wrap(apperrors.CodeNoPaymentConfig, err, "微信查单配置不可用")
}
return app, nil
}
func wechatConfigComplete(config *model.WechatConfig, requireActive bool) bool {
return config != nil && (!requireActive || config.IsActive) && config.ProviderType == model.ProviderTypeWechat &&
config.OaAppID != "" && config.WxMchID != "" && config.WxAPIV3Key != "" &&
config.WxCertContent != "" && config.WxKeyContent != "" && config.WxSerialNo != "" && config.WxNotifyURL != ""
}
func wechatV2ConfigComplete(config *model.WechatConfig, requireActive bool) bool {
return config != nil && (!requireActive || config.IsActive) && config.ProviderType == model.ProviderTypeWechatV2 &&
config.OaAppID != "" && config.WxMchID != "" && config.WxAPIV2Key != "" && config.WxNotifyURL != ""
}
func (a *WechatWebAdapter) startAttempt(ctx context.Context, request agentrecharge.OnlinePaymentRequest, operation string, configID uint, amount int64) (*model.IntegrationLog, error) {
resourceID, resourceKey := strconv.FormatUint(uint64(request.PaymentID), 10), request.PaymentNo
series := "agent-recharge-payment:" + resourceID + ":" + operation
correlationID := request.CorrelationID
return a.integration.Start(ctx, integrationlog.Attempt{
Provider: constants.IntegrationProviderWechatPay, Direction: constants.IntegrationDirectionOutbound,
Operation: operation, ResourceType: constants.IntegrationResourceTypeAgentRechargePayment,
ResourceID: &resourceID, ResourceKey: &resourceKey, ExternalID: &resourceKey,
TriggerSeries: &series, CorrelationID: &correlationID,
RequestSummary: map[string]any{"payment_config_id": configID, "amount": amount},
})
}
func (a *WechatWebAdapter) completeUnknown(ctx context.Context, integrationID string, startedAt time.Time, cause error) error {
if a.logger != nil {
a.logger.Warn("微信支付请求结果未知", zap.String("integration_id", integrationID), zap.Error(cause))
}
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultUnknown, ProviderCode: "request_unknown", SafeProviderMessage: "微信支付请求结果未知",
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
RecoveryStrategy: "使用原支付单号主动查单,确认不存在或关闭后才允许关闭本地支付单",
})
if err != nil {
return err
}
return apperrors.Wrap(apperrors.CodeTimeout, cause, "微信支付请求结果未知")
}
func mapWechatTradeState(state string) string {
switch state {
case "SUCCESS":
return agentrecharge.OnlinePaymentStatePaid
case "CLOSED", "REVOKED", "PAYERROR":
return agentrecharge.OnlinePaymentStateClosed
case "NOTPAY", "USERPAYING":
return agentrecharge.OnlinePaymentStatePending
default:
return agentrecharge.OnlinePaymentStateUnknown
}
}