富友支付支持
This commit is contained in:
@@ -49,17 +49,18 @@ type OnlineCreationService struct {
|
||||
db *gorm.DB
|
||||
wechat OnlinePaymentPort
|
||||
alipay OnlinePaymentPort
|
||||
fuiou OnlinePaymentPort
|
||||
audit PaymentAuditWriter
|
||||
}
|
||||
|
||||
// NewOnlineCreationService 创建代理在线充值用例并以结构体字段注入两个渠道 Adapter。
|
||||
func NewOnlineCreationService(db *gorm.DB, wechat, alipay OnlinePaymentPort, audit PaymentAuditWriter) *OnlineCreationService {
|
||||
return &OnlineCreationService{db: db, wechat: wechat, alipay: alipay, audit: audit}
|
||||
// NewOnlineCreationService 创建代理在线充值用例并以结构体字段注入三个渠道 Adapter。
|
||||
func NewOnlineCreationService(db *gorm.DB, wechat, alipay, fuiou OnlinePaymentPort, audit PaymentAuditWriter) *OnlineCreationService {
|
||||
return &OnlineCreationService{db: db, wechat: wechat, alipay: alipay, fuiou: fuiou, audit: audit}
|
||||
}
|
||||
|
||||
// Execute 以短事务建单,事务外生成支付链接,再条件保存链接或关闭失败订单。
|
||||
func (s *OnlineCreationService) Execute(ctx context.Context, command CreateOnlineCommand) (*CreateOnlineResult, error) {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.audit == nil {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.fuiou == nil || s.audit == nil {
|
||||
return nil, apperrors.New(apperrors.CodeServiceUnavailable, "代理在线充值能力未配置")
|
||||
}
|
||||
command.PaymentMethod = strings.TrimSpace(command.PaymentMethod)
|
||||
@@ -139,7 +140,7 @@ func (s *OnlineCreationService) AvailablePaymentMethods(ctx context.Context, use
|
||||
}
|
||||
return result, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败")
|
||||
}
|
||||
if s.wechat.Available(&config) {
|
||||
if s.wechat.Available(&config) || s.fuiou.Available(&config) {
|
||||
result.Methods = append(result.Methods, constants.RechargeMethodWechat)
|
||||
}
|
||||
if s.alipay.Available(&config) {
|
||||
@@ -180,7 +181,7 @@ func (s *OnlineCreationService) loadCreationFacts(
|
||||
}
|
||||
return nil, nil, nil, nil, nil, apperrors.Wrap(apperrors.CodeDatabaseError, err, "查询生效支付配置失败")
|
||||
}
|
||||
adapter := s.adapter(command.PaymentMethod)
|
||||
adapter := s.adapter(command.PaymentMethod, &config)
|
||||
if adapter == nil || !adapter.Available(&config) {
|
||||
return nil, nil, nil, nil, nil, apperrors.New(apperrors.CodeNoPaymentConfig)
|
||||
}
|
||||
@@ -209,7 +210,7 @@ func (s *OnlineCreationService) createLocalFacts(
|
||||
expireMinutes = model.DefaultAliPayExpireMinutes
|
||||
}
|
||||
expireAt := time.Now().Add(time.Duration(expireMinutes) * time.Minute)
|
||||
channel, requestID := command.PaymentMethod, command.RequestID
|
||||
channel, requestID := paymentChannel(command.PaymentMethod, config), command.RequestID
|
||||
record := &model.AgentRechargeRecord{
|
||||
UserID: account.ID, AgentWalletID: wallet.ID, ShopID: shop.ID, RechargeNo: rechargeNo,
|
||||
Amount: command.Amount, PaymentMethod: command.PaymentMethod, PaymentChannel: &channel,
|
||||
@@ -247,6 +248,9 @@ func paymentMerchantIdentity(paymentMethod string, config *model.WechatConfig) s
|
||||
return ""
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
if config.ProviderType == model.ProviderTypeFuiou {
|
||||
return config.FyMchntCd
|
||||
}
|
||||
return config.WxMchID
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
@@ -255,6 +259,14 @@ func paymentMerchantIdentity(paymentMethod string, config *model.WechatConfig) s
|
||||
return ""
|
||||
}
|
||||
|
||||
// paymentChannel 返回实际支付渠道:富友配置下微信业务方式落库为 fuiou,其余与业务方式一致。
|
||||
func paymentChannel(paymentMethod string, config *model.WechatConfig) string {
|
||||
if paymentMethod == constants.RechargeMethodWechat && config != nil && config.ProviderType == model.ProviderTypeFuiou {
|
||||
return model.ProviderTypeFuiou
|
||||
}
|
||||
return paymentMethod
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) loadReplay(
|
||||
ctx context.Context,
|
||||
command CreateOnlineCommand,
|
||||
@@ -316,8 +328,11 @@ func (s *OnlineCreationService) closeFailedCreation(ctx context.Context, result
|
||||
})
|
||||
}
|
||||
|
||||
func (s *OnlineCreationService) adapter(paymentMethod string) OnlinePaymentPort {
|
||||
func (s *OnlineCreationService) adapter(paymentMethod string, config *model.WechatConfig) OnlinePaymentPort {
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
if config != nil && config.ProviderType == model.ProviderTypeFuiou {
|
||||
return s.fuiou
|
||||
}
|
||||
return s.wechat
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
|
||||
@@ -16,19 +16,20 @@ type RecoverOnlinePaymentService struct {
|
||||
db *gorm.DB
|
||||
wechat OnlinePaymentPort
|
||||
alipay OnlinePaymentPort
|
||||
fuiou OnlinePaymentPort
|
||||
confirm *ConfirmOnlinePaymentService
|
||||
audit PaymentAuditWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewRecoverOnlinePaymentService 创建代理在线充值支付恢复用例。
|
||||
func NewRecoverOnlinePaymentService(db *gorm.DB, wechat, alipay OnlinePaymentPort, confirm *ConfirmOnlinePaymentService, audit PaymentAuditWriter) *RecoverOnlinePaymentService {
|
||||
return &RecoverOnlinePaymentService{db: db, wechat: wechat, alipay: alipay, confirm: confirm, audit: audit, now: time.Now}
|
||||
func NewRecoverOnlinePaymentService(db *gorm.DB, wechat, alipay, fuiou OnlinePaymentPort, confirm *ConfirmOnlinePaymentService, audit PaymentAuditWriter) *RecoverOnlinePaymentService {
|
||||
return &RecoverOnlinePaymentService{db: db, wechat: wechat, alipay: alipay, fuiou: fuiou, confirm: confirm, audit: audit, now: time.Now}
|
||||
}
|
||||
|
||||
// ProcessBatch 按固定批次读取本地待处理事实并调用对应渠道收敛状态。
|
||||
func (s *RecoverOnlinePaymentService) ProcessBatch(ctx context.Context) (int, error) {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.confirm == nil || s.audit == nil {
|
||||
if s == nil || s.db == nil || s.wechat == nil || s.alipay == nil || s.fuiou == nil || s.confirm == nil || s.audit == nil {
|
||||
return 0, errors.New(errors.CodeServiceUnavailable, "代理在线充值支付恢复能力未配置")
|
||||
}
|
||||
now := s.now().UTC()
|
||||
@@ -70,7 +71,7 @@ func (s *RecoverOnlinePaymentService) ProcessBatch(ctx context.Context) (int, er
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) recoverOne(ctx context.Context, payment *model.Payment, recharge *model.AgentRechargeRecord, config *model.WechatConfig, now time.Time) error {
|
||||
adapter := s.adapter(payment.PaymentMethod)
|
||||
adapter := s.adapter(payment.PaymentMethod, config)
|
||||
if adapter == nil {
|
||||
return errors.New(errors.CodeNoPaymentConfig, "代理充值创建时支付配置不可用")
|
||||
}
|
||||
@@ -200,8 +201,11 @@ func (s *RecoverOnlinePaymentService) closePending(ctx context.Context, payment
|
||||
})
|
||||
}
|
||||
|
||||
func (s *RecoverOnlinePaymentService) adapter(paymentMethod string) OnlinePaymentPort {
|
||||
func (s *RecoverOnlinePaymentService) adapter(paymentMethod string, config *model.WechatConfig) OnlinePaymentPort {
|
||||
if paymentMethod == constants.RechargeMethodWechat {
|
||||
if config != nil && config.ProviderType == model.ProviderTypeFuiou {
|
||||
return s.fuiou
|
||||
}
|
||||
return s.wechat
|
||||
}
|
||||
if paymentMethod == constants.RechargeMethodAlipay {
|
||||
|
||||
@@ -288,6 +288,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.DB,
|
||||
paymentInfra.NewWechatWebAdapter(wechat.NewRedisCache(deps.Redis), paymentIntegration, deps.Logger),
|
||||
paymentInfra.NewAlipayWapAdapter(paymentIntegration, deps.Logger),
|
||||
paymentInfra.NewFuiouScanAdapter(paymentIntegration, deps.Logger),
|
||||
auditWriter,
|
||||
)
|
||||
agentRechargePaymentConfirm := agentrechargeApp.NewConfirmOnlinePaymentService(
|
||||
|
||||
@@ -3,6 +3,7 @@ package agentrecharge
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
@@ -51,8 +52,11 @@ func ValidatePaymentConfirmation(facts PaymentConfirmationFacts) (bool, error) {
|
||||
return false, errors.New(errors.CodeConflict, "支付单与代理充值单关联不一致")
|
||||
}
|
||||
method := strings.TrimSpace(facts.PaymentMethod)
|
||||
if method == "" || method != strings.TrimSpace(facts.RechargePaymentMethod) ||
|
||||
method != strings.TrimSpace(facts.RechargePaymentChannel) {
|
||||
if method == "" || method != strings.TrimSpace(facts.RechargePaymentMethod) {
|
||||
return false, errors.New(errors.CodeConflict, "支付渠道与代理充值单不一致")
|
||||
}
|
||||
channel := strings.TrimSpace(facts.RechargePaymentChannel)
|
||||
if channel != method && !(method == constants.RechargeMethodWechat && channel == model.ProviderTypeFuiou) {
|
||||
return false, errors.New(errors.CodeConflict, "支付渠道与代理充值单不一致")
|
||||
}
|
||||
identity := strings.TrimSpace(facts.MerchantIdentity)
|
||||
|
||||
@@ -283,8 +283,13 @@ func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callba
|
||||
correlationID := callback.PaymentNo
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID})
|
||||
linkage := auditcontext.From(ctx)
|
||||
// 富友本质是微信支付上游通道,回调渠道归一化为业务方式 wechat 后再进入确认用例。
|
||||
paymentMethod := callback.PaymentMethod
|
||||
if paymentMethod == model.ProviderTypeFuiou {
|
||||
paymentMethod = constants.RechargeMethodWechat
|
||||
}
|
||||
result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{
|
||||
PaymentNo: callback.PaymentNo, PaymentMethod: callback.PaymentMethod, ConfigID: callback.ConfigID,
|
||||
PaymentNo: callback.PaymentNo, PaymentMethod: paymentMethod, ConfigID: callback.ConfigID,
|
||||
MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID,
|
||||
Amount: callback.Amount, PaidAt: callback.PaidAt, RequestID: linkage.RequestID,
|
||||
CorrelationID: correlationID, ParentEventID: linkage.ParentEventID,
|
||||
|
||||
172
internal/infrastructure/payment/fuiou_scan.go
Normal file
172
internal/infrastructure/payment/fuiou_scan.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user