收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -46,6 +48,8 @@ type ClientWalletHandler struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
paymentMethodPolicy ClientPaymentMethodPolicy
|
||||
auditWriter *audit.Writer
|
||||
paymentIntegration *integrationlog.Repository
|
||||
}
|
||||
|
||||
// SetPaymentMethodPolicy 注入 C 端支付方式策略。
|
||||
@@ -53,6 +57,12 @@ func (h *ClientWalletHandler) SetPaymentMethodPolicy(policy ClientPaymentMethodP
|
||||
h.paymentMethodPolicy = policy
|
||||
}
|
||||
|
||||
// SetPaymentAudit 注入充值支付审计与外部交互日志接缝。
|
||||
func (h *ClientWalletHandler) SetPaymentAudit(writer *audit.Writer, integration *integrationlog.Repository) {
|
||||
h.auditWriter = writer
|
||||
h.paymentIntegration = integration
|
||||
}
|
||||
|
||||
// NewClientWalletHandler 创建 C 端钱包处理器
|
||||
func NewClientWalletHandler(
|
||||
assetService *asset.Service,
|
||||
@@ -352,6 +362,10 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
|
||||
// 先初始化生效支付通道并创建预支付订单,确认支付通道可用
|
||||
// 避免先写入充值记录后支付初始化失败,导致产生孤儿记录
|
||||
attempt, startedAt, err := h.startRechargePaymentAttempt(resolved.SkipPermissionCtx, config, paymentNo, rechargeNo, req.Amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payConfig, err := h.createClientRechargePayConfig(
|
||||
resolved.SkipPermissionCtx,
|
||||
config,
|
||||
@@ -363,6 +377,12 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
int(req.Amount),
|
||||
)
|
||||
if err != nil {
|
||||
if completeErr := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "充值支付预下单结果未知"); completeErr != nil {
|
||||
return completeErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := h.completeRechargePaymentAttempt(resolved.SkipPermissionCtx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -381,10 +401,6 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
OperatorType: constants.OperatorTypePersonalCustomer,
|
||||
Generation: resolved.Generation,
|
||||
}
|
||||
if err := h.rechargeOrderStore.Create(resolved.SkipPermissionCtx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
|
||||
payment := &model.Payment{
|
||||
PaymentNo: paymentNo,
|
||||
OrderID: rechargeOrder.ID,
|
||||
@@ -394,8 +410,17 @@ func (h *ClientWalletHandler) createWechatRecharge(
|
||||
Status: model.PaymentRecordStatusPending,
|
||||
PaymentConfigID: &config.ID,
|
||||
}
|
||||
if err := h.paymentStore.Create(resolved.SkipPermissionCtx, payment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
if err := h.db.WithContext(resolved.SkipPermissionCtx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := h.rechargeOrderStore.CreateWithTx(resolved.SkipPermissionCtx, tx, rechargeOrder); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
|
||||
}
|
||||
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.Success(c, &dto.ClientRechargeResponse{
|
||||
@@ -456,14 +481,17 @@ func (h *ClientWalletHandler) createAlipayRecharge(
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
|
||||
}
|
||||
payment.OrderID = rechargeOrder.ID
|
||||
return h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment)
|
||||
if err := h.paymentStore.CreateWithTx(resolved.SkipPermissionCtx, tx, payment); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.appendRechargePaymentCreatedAudit(resolved.SkipPermissionCtx, tx, payment, rechargeOrder)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wapURL, err := alipay.BuildWapPayURL(resolved.SkipPermissionCtx, config, payment, "资产钱包充值")
|
||||
if err != nil {
|
||||
if updateErr := h.paymentStore.UpdateStatus(resolved.SkipPermissionCtx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil {
|
||||
if updateErr := h.markRechargePaymentFailed(resolved.SkipPermissionCtx, payment); updateErr != nil {
|
||||
h.logger.Warn("标记支付宝支付单 failed 失败",
|
||||
zap.String("payment_no", paymentNo),
|
||||
zap.Error(updateErr),
|
||||
|
||||
100
internal/handler/app/client_wallet_payment_audit.go
Normal file
100
internal/handler/app/client_wallet_payment_audit.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *ClientWalletHandler) appendRechargePaymentCreatedAudit(ctx context.Context, tx *gorm.DB, payment *model.Payment, recharge *model.RechargeOrder) error {
|
||||
if h.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "充值支付统一审计接缝未配置")
|
||||
}
|
||||
rechargeID := strconv.FormatUint(uint64(recharge.ID), 10)
|
||||
resources := []audit.ResourceInput{
|
||||
audit.PaymentResource(payment, constants.AuditResourceRelationPrimary, constants.AuditResourceRolePaymentTarget, nil, map[string]any{"status": payment.Status}),
|
||||
{
|
||||
Type: constants.AuditResourceRechargeOrder, ID: &rechargeID, Key: recharge.RechargeOrderNo, DisplayName: recharge.RechargeOrderNo,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRolePaymentBusinessOrder,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": recharge.ID, "recharge_order_no": recharge.RechargeOrderNo, "user_id": recharge.UserID,
|
||||
"asset_wallet_id": recharge.AssetWalletID, "resource_type": recharge.ResourceType,
|
||||
"resource_id": recharge.ResourceID, "amount": recharge.Amount, "status": recharge.Status,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "资产充值支付已创建",
|
||||
},
|
||||
}
|
||||
references, err := audit.AssetRechargeReferences(ctx, tx, recharge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources = append(resources, references...)
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: constants.AuditActionPaymentCreated, Summary: "创建资产充值支付记录",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
CorrelationID: payment.PaymentNo,
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) startRechargePaymentAttempt(ctx context.Context, config *model.WechatConfig, paymentNo, rechargeNo string, amount int64) (*model.IntegrationLog, time.Time, error) {
|
||||
if h.paymentIntegration == nil {
|
||||
return nil, time.Time{}, errors.New(errors.CodeInvalidStatus, "充值支付 Integration Log 接缝未配置")
|
||||
}
|
||||
provider := constants.IntegrationProviderWechatPay
|
||||
if config.ProviderType == model.ProviderTypeFuiou {
|
||||
provider = constants.IntegrationProviderFuiou
|
||||
}
|
||||
series := "payment:" + paymentNo + ":" + constants.IntegrationOperationPaymentPreCreate
|
||||
log, err := h.paymentIntegration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: provider, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: constants.IntegrationOperationPaymentPreCreate,
|
||||
ResourceType: constants.IntegrationResourceTypePayment, ResourceKey: &paymentNo, ExternalID: &paymentNo,
|
||||
TriggerSeries: &series, CorrelationID: &rechargeNo,
|
||||
RequestSummary: map[string]any{"payment_config_id": config.ID, "amount": amount},
|
||||
})
|
||||
return log, time.Now(), err
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) completeRechargePaymentAttempt(ctx context.Context, log *model.IntegrationLog, startedAt time.Time, result, providerCode, safeMessage string) error {
|
||||
completion := integrationlog.Completion{
|
||||
Result: result, ProviderCode: providerCode, SafeProviderMessage: safeMessage,
|
||||
ResponseSummary: map[string]any{"success": result == constants.IntegrationResultSuccess},
|
||||
DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
}
|
||||
if result == constants.IntegrationResultUnknown {
|
||||
completion.RecoveryStrategy = "使用原支付单号向支付渠道查单,确认结果后再推进本地充值状态"
|
||||
}
|
||||
_, err := h.paymentIntegration.Complete(ctx, log.IntegrationID, completion)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ClientWalletHandler) markRechargePaymentFailed(ctx context.Context, payment *model.Payment) error {
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Payment{}).Where("id = ? AND status = ?", payment.ID, model.PaymentRecordStatusPending).
|
||||
Update("status", model.PaymentRecordStatusFailed)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "关闭失败支付记录失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
after := *payment
|
||||
after.Status = model.PaymentRecordStatusFailed
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: constants.AuditActionPaymentFailed, Summary: "支付宝支付链接生成失败,关闭支付记录",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
CorrelationID: payment.PaymentNo,
|
||||
Resources: []audit.ResourceInput{audit.PaymentResource(&after, constants.AuditResourceRelationPrimary, constants.AuditResourceRolePaymentTarget,
|
||||
map[string]any{"status": payment.Status}, map[string]any{"status": after.Status})},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -7,11 +7,19 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
apperrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
)
|
||||
|
||||
func carrierCallbackContext(ctx context.Context, provider string) context.Context {
|
||||
return auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorExternalSystem, ActorID: provider,
|
||||
ActorName: provider, Source: constants.AuditSourceCallback,
|
||||
})
|
||||
}
|
||||
|
||||
// SystemConfigReader 提供运营商回调运行时开关读取能力。
|
||||
type SystemConfigReader interface {
|
||||
Get(ctx context.Context, key string) (string, error)
|
||||
|
||||
@@ -60,6 +60,7 @@ func (h *CMCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
if h == nil || h.translator == nil || h.resolver == nil || h.integration == nil || h.observation == nil {
|
||||
return apperrors.New(apperrors.CodeInternalError, "移动实名回调能力未完整配置")
|
||||
}
|
||||
ctx = carrierCallbackContext(ctx, constants.IntegrationProviderCMCC)
|
||||
translated, translateErr := h.translator.Translate(body)
|
||||
idempotencyKey := cmccIdempotencyKey(body, translated)
|
||||
integrationID := "cmcc-realname:" + shortHash(idempotencyKey)
|
||||
@@ -108,6 +109,7 @@ func (h *CMCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
h.observation.RecordCarrierCallbackFailure(ctx, &card, log.IntegrationID, err)
|
||||
return h.fail(ctx, log.IntegrationID, err)
|
||||
}
|
||||
if h.series != nil {
|
||||
|
||||
@@ -82,6 +82,7 @@ func (h *CTCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
if h == nil || h.translator == nil || h.resolver == nil || h.integration == nil || h.observation == nil {
|
||||
return apperrors.New(apperrors.CodeInternalError, "电信实名回调能力未完整配置")
|
||||
}
|
||||
ctx = carrierCallbackContext(ctx, constants.IntegrationProviderCTCC)
|
||||
translated, translateErr := h.translator.Translate(body)
|
||||
idempotencyKey := ctccIdempotencyKey(body, translated)
|
||||
integrationID := "ctcc-realname:" + shortHash(idempotencyKey)
|
||||
@@ -134,6 +135,7 @@ func (h *CTCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
h.observation.RecordCarrierCallbackFailure(ctx, &card, log.IntegrationID, err)
|
||||
return h.failPending(ctx, log.IntegrationID, err)
|
||||
}
|
||||
if h.series != nil {
|
||||
|
||||
@@ -61,6 +61,7 @@ func (h *CUCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
if h == nil || h.translator == nil || h.resolver == nil || h.integration == nil || h.observation == nil {
|
||||
return apperrors.New(apperrors.CodeInternalError, "联通实名回调能力未完整配置")
|
||||
}
|
||||
ctx = carrierCallbackContext(ctx, constants.IntegrationProviderCUCC)
|
||||
translated, translateErr := h.translator.Translate(body)
|
||||
key := cuccRealnameIdempotencyKey(body, translated, translateErr == nil)
|
||||
integrationID := "cucc-realname:" + shortHash(key)
|
||||
@@ -107,6 +108,7 @@ func (h *CUCCRealnameHandler) process(ctx context.Context, body []byte, contentT
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
h.observation.RecordCarrierCallbackFailure(ctx, &card, log.IntegrationID, err)
|
||||
return h.fail(ctx, log.IntegrationID, err)
|
||||
}
|
||||
if h.series != nil {
|
||||
|
||||
@@ -123,6 +123,12 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "微信 v2 回调验签失败")
|
||||
}
|
||||
if result.TradeState != "SUCCESS" {
|
||||
if err := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID,
|
||||
Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}, result.TradeState); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.wechatV2SuccessResponse(c)
|
||||
}
|
||||
// TotalFee 为字符串格式的分,解析失败则降级为 0(后续 handlePaymentCallback 会记录日志)
|
||||
@@ -145,7 +151,10 @@ func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error {
|
||||
fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true)
|
||||
_, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error {
|
||||
if result.TradeState != "SUCCESS" {
|
||||
return nil
|
||||
return h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID,
|
||||
Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}, result.TradeState)
|
||||
}
|
||||
paidAt, _ := time.Parse(time.RFC3339, result.SuccessTime)
|
||||
return h.dispatchWechatCallback(ctx, verifiedPaymentCallback{
|
||||
@@ -170,19 +179,35 @@ func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, call
|
||||
if h.paymentStore != nil {
|
||||
payment, err := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo)
|
||||
if err == nil {
|
||||
log, err := h.recordPaymentCallback(ctx, callback, payment)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
var processErr error
|
||||
switch payment.OrderType {
|
||||
case model.PaymentOrderTypePackage:
|
||||
return true, h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
|
||||
processErr = h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
|
||||
case model.PaymentOrderTypeRecharge:
|
||||
if h.rechargeOrderService != nil {
|
||||
return true, h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
|
||||
processErr = h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
|
||||
} else {
|
||||
processErr = errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
}
|
||||
return true, errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
return true, h.confirmAgentRechargePayment(ctx, callback)
|
||||
return true, h.confirmAgentRechargePayment(ctx, callback, log)
|
||||
default:
|
||||
return true, errors.New(errors.CodeInvalidStatus, "未知支付记录类型")
|
||||
processErr = errors.New(errors.CodeInvalidStatus, "未知支付记录类型")
|
||||
}
|
||||
completion := integrationlog.Completion{Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"confirmed": true}}
|
||||
if processErr != nil {
|
||||
completion.Result = constants.IntegrationResultFailed
|
||||
completion.SafeProviderMessage = "支付回调业务确认失败"
|
||||
completion.ResponseSummary = map[string]any{"confirmed": false}
|
||||
} else if current, currentErr := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo); currentErr == nil {
|
||||
completion.StateChanged = payment.Status != model.PaymentRecordStatusPaid && current.Status == model.PaymentRecordStatusPaid
|
||||
}
|
||||
h.completePaymentCallbackLog(ctx, log, completion)
|
||||
return true, processErr
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return true, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败")
|
||||
@@ -198,31 +223,14 @@ func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, callback ve
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(callback.PaymentNo, "ORD"):
|
||||
return h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.Amount)
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix):
|
||||
if h.rechargeOrderService != nil {
|
||||
return h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentByWechat, callback.TransactionID)
|
||||
}
|
||||
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
return h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, model.PaymentMethodWechat, callback.TransactionID, callback.Amount)
|
||||
}
|
||||
return errors.New(errors.CodeInternalError, "代理充值服务未配置")
|
||||
default:
|
||||
return errors.New(errors.CodeInvalidStatus, "未知订单号前缀")
|
||||
}
|
||||
return h.dispatchLegacyPaymentCallback(ctx, callback)
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
if h.agentPaymentConfirm == nil || h.integration == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置")
|
||||
func (h *PaymentHandler) dispatchLegacyPaymentCallback(ctx context.Context, callback verifiedPaymentCallback) error {
|
||||
if h.integration == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置")
|
||||
}
|
||||
resourceKey, correlationID := callback.PaymentNo, callback.PaymentNo
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID})
|
||||
linkage := auditcontext.From(ctx)
|
||||
idempotencyKey := callback.TransactionID
|
||||
if idempotencyKey == "" {
|
||||
idempotencyKey = callback.PaymentNo
|
||||
@@ -230,13 +238,51 @@ func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callba
|
||||
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
|
||||
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
|
||||
ResourceType: constants.IntegrationResourceTypeAgentRechargePayment, ResourceKey: &resourceKey,
|
||||
ResourceType: constants.IntegrationResourceTypePayment, ResourceKey: &resourceKey,
|
||||
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
|
||||
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.preparePaymentCallbackRetry(ctx, log); err != nil {
|
||||
return err
|
||||
}
|
||||
var processErr error
|
||||
switch {
|
||||
case strings.HasPrefix(callback.PaymentNo, "ORD"):
|
||||
processErr = h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.Amount)
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix):
|
||||
if h.rechargeOrderService != nil {
|
||||
processErr = h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID)
|
||||
} else {
|
||||
processErr = errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
}
|
||||
case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
processErr = h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount)
|
||||
} else {
|
||||
processErr = errors.New(errors.CodeInternalError, "代理充值服务未配置")
|
||||
}
|
||||
default:
|
||||
processErr = errors.New(errors.CodeInvalidStatus, "未知订单号前缀")
|
||||
}
|
||||
completion := integrationlog.Completion{Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"confirmed": processErr == nil}}
|
||||
if processErr != nil {
|
||||
completion.Result = constants.IntegrationResultFailed
|
||||
completion.SafeProviderMessage = "旧支付回调业务确认失败"
|
||||
}
|
||||
h.completePaymentCallbackLog(ctx, log, completion)
|
||||
return processErr
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback, log *model.IntegrationLog) error {
|
||||
if h.agentPaymentConfirm == nil || h.integration == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置")
|
||||
}
|
||||
correlationID := callback.PaymentNo
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID})
|
||||
linkage := auditcontext.From(ctx)
|
||||
result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{
|
||||
PaymentNo: callback.PaymentNo, PaymentMethod: callback.PaymentMethod, ConfigID: callback.ConfigID,
|
||||
MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID,
|
||||
@@ -269,6 +315,69 @@ func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callba
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) recordPaymentCallback(ctx context.Context, callback verifiedPaymentCallback, payment *model.Payment) (*model.IntegrationLog, error) {
|
||||
if h.integration == nil || payment == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置")
|
||||
}
|
||||
resourceID, resourceKey, correlationID := strconv.FormatUint(uint64(payment.ID), 10), payment.PaymentNo, payment.PaymentNo
|
||||
idempotencyKey := callback.TransactionID
|
||||
if idempotencyKey == "" {
|
||||
idempotencyKey = callback.PaymentNo
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID})
|
||||
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
|
||||
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
|
||||
ResourceType: constants.IntegrationResourceTypePayment, ResourceID: &resourceID, ResourceKey: &resourceKey,
|
||||
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
|
||||
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := h.preparePaymentCallbackRetry(ctx, log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) preparePaymentCallbackRetry(ctx context.Context, log *model.IntegrationLog) error {
|
||||
if log == nil || log.Result != constants.IntegrationResultFailed {
|
||||
return nil
|
||||
}
|
||||
claimed, err := h.integration.ClaimFailedInbound(ctx, log.IntegrationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if claimed {
|
||||
log.Result = constants.IntegrationResultPending
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) recordIgnoredPaymentCallback(ctx context.Context, callback verifiedPaymentCallback, providerCode string) error {
|
||||
if h.integration == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置")
|
||||
}
|
||||
resourceKey, correlationID := callback.PaymentNo, callback.PaymentNo
|
||||
idempotencyKey := callback.PaymentNo + ":" + providerCode
|
||||
log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{
|
||||
IdempotencyKey: idempotencyKey, Provider: callback.Provider,
|
||||
Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID,
|
||||
ResourceType: constants.IntegrationResourceTypePayment, ResourceKey: &resourceKey,
|
||||
RawPayload: callback.RawPayload, ContentType: callback.ContentType,
|
||||
RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID,
|
||||
})
|
||||
if err != nil || log.Result != constants.IntegrationResultPending {
|
||||
return err
|
||||
}
|
||||
_, err = h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultIgnored, ProviderCode: providerCode,
|
||||
ResponseSummary: map[string]any{"confirmed": false},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *PaymentHandler) completePaymentCallbackLog(ctx context.Context, log *model.IntegrationLog, completion integrationlog.Completion) {
|
||||
if log == nil || log.Result != constants.IntegrationResultPending {
|
||||
return
|
||||
@@ -346,6 +455,12 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_status", tradeStatus),
|
||||
)
|
||||
if err := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: outTradeNo, TransactionID: notification.TradeNo,
|
||||
Provider: constants.IntegrationProviderAlipay, RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
|
||||
}, tradeStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.SendString("success")
|
||||
}
|
||||
|
||||
@@ -409,57 +524,13 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付金额校验失败")
|
||||
}
|
||||
|
||||
// 新代理充值由统一确认事务原子写入交易号,旧业务保持原处理方式。
|
||||
if payment.OrderType != model.PaymentOrderTypeAgentRecharge {
|
||||
if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil {
|
||||
h.logger.Error("支付宝回调:写入 third_party_trade_no 失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
// 不中断存量业务,继续由原幂等业务层处理状态。
|
||||
}
|
||||
callback := verifiedPaymentCallback{
|
||||
PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay,
|
||||
TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID,
|
||||
MerchantIdentity: notification.AppId, Provider: constants.IntegrationProviderAlipay,
|
||||
RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// 按支付单 order_type 分发业务
|
||||
switch payment.OrderType {
|
||||
case model.PaymentOrderTypePackage:
|
||||
if err := h.orderService.HandlePaymentRecordCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo, payment.Amount); err != nil {
|
||||
h.logger.Error("支付宝回调:推进套餐订单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝支付回调失败")
|
||||
}
|
||||
h.logger.Info("支付宝回调:套餐订单支付成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.String("order_type", payment.OrderType),
|
||||
)
|
||||
|
||||
case model.PaymentOrderTypeRecharge:
|
||||
if h.rechargeOrderService == nil {
|
||||
h.logger.Error("支付宝回调:充值订单服务未配置",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
)
|
||||
return errors.New(errors.CodeInternalError, "充值订单服务未配置")
|
||||
}
|
||||
if err := h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo); err != nil {
|
||||
h.logger.Error("支付宝回调:推进充值订单失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝充值回调失败")
|
||||
}
|
||||
h.logger.Info("支付宝回调:充值订单支付成功",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("trade_no", notification.TradeNo),
|
||||
zap.String("order_type", payment.OrderType),
|
||||
)
|
||||
|
||||
case model.PaymentOrderTypeAgentRecharge:
|
||||
if payment.OrderType == model.PaymentOrderTypeAgentRecharge {
|
||||
paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local)
|
||||
if parseErr != nil {
|
||||
h.logger.Error("支付宝回调:付款时间格式无效",
|
||||
@@ -468,25 +539,13 @@ func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error {
|
||||
)
|
||||
return errors.New(errors.CodeWechatCallbackInvalid, "支付宝付款时间格式错误")
|
||||
}
|
||||
if err := h.confirmAgentRechargePayment(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay,
|
||||
TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID,
|
||||
MerchantIdentity: notification.AppId, PaidAt: paidAt, Provider: constants.IntegrationProviderAlipay,
|
||||
RawPayload: c.Body(), ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
h.logger.Error("支付宝回调:确认代理充值支付失败",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "处理支付宝代理充值回调失败")
|
||||
}
|
||||
|
||||
default:
|
||||
h.logger.Error("支付宝回调:未知支付记录类型",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.String("order_type", payment.OrderType),
|
||||
)
|
||||
return errors.New(errors.CodeInternalError, "未知支付记录类型")
|
||||
callback.PaidAt = paidAt
|
||||
}
|
||||
if handled, dispatchErr := h.dispatchPaymentRecordCallback(ctx, callback); dispatchErr != nil {
|
||||
h.logger.Error("支付宝回调:确认支付失败", zap.String("out_trade_no", outTradeNo), zap.Error(dispatchErr))
|
||||
return errors.Wrap(errors.CodeInternalError, dispatchErr, "处理支付宝支付回调失败")
|
||||
} else if !handled {
|
||||
return errors.New(errors.CodeInternalError, "支付记录分发失败")
|
||||
}
|
||||
|
||||
return c.SendString("success")
|
||||
@@ -563,6 +622,12 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
h.logger.Warn("富友回调:非成功结果",
|
||||
zap.String("result_code", notify.ResultCode),
|
||||
zap.String("result_msg", notify.ResultMsg))
|
||||
if recordErr := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: notify.MchntOrderNo, TransactionID: notify.TransactionId,
|
||||
Provider: constants.IntegrationProviderFuiou, RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}, notify.ResultCode); recordErr != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse("integration log failed"))
|
||||
}
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
}
|
||||
h.logger.Error("富友回调:验签或解析失败",
|
||||
@@ -581,9 +646,10 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
orderNo := notify.MchntOrderNo
|
||||
// OrderAmt 为字符串格式的分,解析失败则降级为 0
|
||||
orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64)
|
||||
paidAt, _ := time.ParseInLocation("20060102150405", notify.TxnFinTs, time.Local)
|
||||
if handled, err := h.dispatchPaymentRecordCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: orderNo, PaymentMethod: "fuiou", TransactionID: notify.TransactionId, Amount: orderAmt,
|
||||
ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, Provider: model.ProviderTypeFuiou,
|
||||
ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, PaidAt: paidAt, Provider: constants.IntegrationProviderFuiou,
|
||||
RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
@@ -591,27 +657,11 @@ func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error {
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(orderNo, "ORD"):
|
||||
if err := h.orderService.HandlePaymentCallback(ctx, orderNo, "fuiou", orderAmt); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
}
|
||||
case strings.HasPrefix(orderNo, constants.AssetRechargeOrderPrefix):
|
||||
if h.rechargeOrderService != nil {
|
||||
if err := h.rechargeOrderService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
}
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
}
|
||||
return c.Send(fuiou.BuildNotifyFailResponse("充值订单服务未配置"))
|
||||
case strings.HasPrefix(orderNo, constants.AgentRechargeOrderPrefix):
|
||||
if h.agentRechargeService != nil {
|
||||
if err := h.agentRechargeService.HandlePaymentCallback(ctx, orderNo, model.ProviderTypeFuiou, notify.TransactionId, orderAmt); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
}
|
||||
}
|
||||
default:
|
||||
return c.Send(fuiou.BuildNotifyFailResponse("unknown order prefix"))
|
||||
if err := h.dispatchLegacyPaymentCallback(ctx, verifiedPaymentCallback{
|
||||
PaymentNo: orderNo, PaymentMethod: model.ProviderTypeFuiou, TransactionID: notify.TransactionId, Amount: orderAmt,
|
||||
Provider: constants.IntegrationProviderFuiou, RawPayload: body, ContentType: c.Get("Content-Type"),
|
||||
}); err != nil {
|
||||
return c.Send(fuiou.BuildNotifyFailResponse(err.Error()))
|
||||
}
|
||||
|
||||
return c.Send(fuiou.BuildNotifySuccessResponse())
|
||||
|
||||
Reference in New Issue
Block a user