富友支付支持

This commit is contained in:
2026-08-18 17:13:20 +08:00
parent 46c8e819df
commit 656a921ff0
17 changed files with 622 additions and 16 deletions

View File

@@ -0,0 +1,172 @@
package payment
import (
"context"
"strconv"
"strings"
"time"
"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"
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
)
// FuiouScanAdapter 按富友主扫统一下单生成微信扫码支付链接并主动查单。
type FuiouScanAdapter struct {
integration *integrationlog.Repository
logger *zap.Logger
}
// NewFuiouScanAdapter 创建富友扫码支付适配器。
func NewFuiouScanAdapter(integration *integrationlog.Repository, logger *zap.Logger) *FuiouScanAdapter {
return &FuiouScanAdapter{integration: integration, logger: logger}
}
// Available 判断富友主扫下单、验签与查单所需的配置是否完整。
func (a *FuiouScanAdapter) Available(config *model.WechatConfig) bool {
return fuiouConfigComplete(config, true)
}
// CreatePaymentURL 调用富友主扫统一下单并返回二维码链接。
func (a *FuiouScanAdapter) 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()
client, err := a.newClient(request.Config)
if err != nil {
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, err)
}
expireMinutes := int(time.Until(request.ExpireAt).Minutes())
if expireMinutes < 1 {
expireMinutes = 1
}
resp, callErr := client.PreCreate(
request.PaymentNo, strconv.FormatInt(request.Amount, 10), request.Description,
fuiou.GetServerIP(), fuiou.OrderTypeWechat, strconv.Itoa(expireMinutes),
)
if callErr != nil {
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
if strings.TrimSpace(resp.QrCode) == "" {
return agentrecharge.OnlinePaymentResult{}, a.completeFailed(ctx, attempt.IntegrationID, startedAt, "empty_qr_code", "富友主扫下单未返回二维码链接")
}
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: resp.QrCode}, nil
}
// Query 调用富友订单查询并按 trans_stat 返回统一查询结果。
func (a *FuiouScanAdapter) 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()
client, err := a.newClient(request.Config)
if err != nil {
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, err)
}
resp, callErr := client.CommonQuery(request.PaymentNo, fuiou.OrderTypeWechat)
if callErr != nil {
return agentrecharge.OnlinePaymentQueryResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
}
result := agentrecharge.OnlinePaymentQueryResult{State: mapFuiouTransStat(resp.TransStat)}
if result.State == agentrecharge.OnlinePaymentStatePaid {
result.ThirdPartyTradeNo = strings.TrimSpace(resp.TransactionId)
result.Amount, _ = strconv.ParseInt(strings.TrimSpace(resp.OrderAmt), 10, 64)
if paidAt, ok := parseWechatPaidAt(strings.TrimSpace(resp.ReservedTxnFinTs)); ok {
result.PaidAt = &paidAt
}
}
providerCode := resp.TransStat
if strings.TrimSpace(providerCode) == "" {
providerCode = "unknown"
}
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
Result: constants.IntegrationResultSuccess, ProviderCode: providerCode,
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 fuiouConfigComplete(config *model.WechatConfig, requireActive bool) bool {
return config != nil && (!requireActive || config.IsActive) && config.ProviderType == model.ProviderTypeFuiou &&
config.FyInsCd != "" && config.FyMchntCd != "" && config.FyTermID != "" &&
config.FyPrivateKey != "" && config.FyPublicKey != "" && config.FyAPIURL != "" && config.FyNotifyURL != ""
}
func (a *FuiouScanAdapter) newClient(config *model.WechatConfig) (*fuiou.Client, error) {
if !fuiouConfigComplete(config, false) {
return nil, apperrors.New(apperrors.CodeNoPaymentConfig, "富友扫码支付配置不可用")
}
return fuiou.NewClient(
config.FyInsCd, config.FyMchntCd, config.FyTermID, config.FyAPIURL, config.FyNotifyURL,
config.FyPrivateKey, config.FyPublicKey, a.logger,
)
}
func mapFuiouTransStat(transStat string) string {
switch transStat {
case "SUCCESS":
return agentrecharge.OnlinePaymentStatePaid
case "PAYERROR", "CLOSED", "REVOKED":
return agentrecharge.OnlinePaymentStateClosed
case "USERPAYING", "NOTPAY":
return agentrecharge.OnlinePaymentStatePending
default:
return agentrecharge.OnlinePaymentStateUnknown
}
}
func (a *FuiouScanAdapter) 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.IntegrationProviderFuiou, 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 *FuiouScanAdapter) 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 (a *FuiouScanAdapter) completeFailed(ctx context.Context, integrationID string, startedAt time.Time, providerCode, providerMessage string) error {
_, err := a.integration.Complete(ctx, integrationID, integrationlog.Completion{
Result: constants.IntegrationResultFailed, ProviderCode: providerCode, SafeProviderMessage: providerMessage,
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
})
if err != nil {
return err
}
return apperrors.New(apperrors.CodeServiceUnavailable, providerMessage)
}