收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -0,0 +1,103 @@
package client_order
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/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
func (s *Service) appendPaymentCreatedAudit(ctx context.Context, tx *gorm.DB, payment *model.Payment, order *model.Order, recharge *model.RechargeOrder) error {
if s.auditWriter == nil {
return errors.New(errors.CodeInvalidStatus, "支付统一审计接缝未配置")
}
resources := []audit.ResourceInput{audit.PaymentResource(payment, constants.AuditResourceRelationPrimary, constants.AuditResourceRolePaymentTarget, nil, map[string]any{"status": payment.Status})}
if order != nil {
resources = append(resources, audit.OrderResource(order, constants.AuditResourceRelationReference, constants.AuditResourceRolePaymentBusinessOrder))
}
if recharge != nil {
id := strconv.FormatUint(uint64(recharge.ID), 10)
resources = append(resources, audit.ResourceInput{
Type: constants.AuditResourceRechargeOrder, ID: &id, 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,
},
})
}
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
ActionCode: constants.AuditActionPaymentCreated, Summary: "创建第三方支付记录",
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
CorrelationID: payment.PaymentNo, Resources: resources,
})
}
func (s *Service) startPaymentAttempt(ctx context.Context, payment *model.Payment, provider, scene string) (*model.IntegrationLog, time.Time, error) {
if s.paymentIntegration == nil {
return nil, time.Time{}, errors.New(errors.CodeInvalidStatus, "支付 Integration Log 接缝未配置")
}
resourceID := strconv.FormatUint(uint64(payment.ID), 10)
resourceKey, series, correlationID := payment.PaymentNo, "payment:"+resourceID+":"+constants.IntegrationOperationPaymentPreCreate, payment.PaymentNo
triggerSource, triggerScene := auditcontext.From(ctx).Source, scene
log, err := s.paymentIntegration.Start(ctx, integrationlog.Attempt{
Provider: provider, Direction: constants.IntegrationDirectionOutbound,
Operation: constants.IntegrationOperationPaymentPreCreate,
ResourceType: constants.IntegrationResourceTypePayment, ResourceID: &resourceID, ResourceKey: &resourceKey,
ExternalID: &resourceKey, TriggerSource: &triggerSource, TriggerScene: &triggerScene,
TriggerSeries: &series, CorrelationID: &correlationID,
RequestSummary: map[string]any{"payment_config_id": payment.PaymentConfigID, "amount": payment.Amount},
})
return log, time.Now(), err
}
func (s *Service) completePaymentAttempt(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 := s.paymentIntegration.Complete(ctx, log.IntegrationID, completion)
return err
}
func paymentIntegrationProvider(config *model.WechatConfig) string {
if config != nil && config.ProviderType == model.ProviderTypeFuiou {
return constants.IntegrationProviderFuiou
}
return constants.IntegrationProviderWechatPay
}
func (s *Service) markPaymentFailed(ctx context.Context, payment *model.Payment, summary string) error {
return s.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 s.auditWriter.Append(ctx, tx, audit.AppendInput{
ActionCode: constants.AuditActionPaymentFailed, Summary: 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})},
})
})
}

View File

@@ -10,6 +10,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/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
asset "github.com/break/junhong_cmp_fiber/internal/service/asset"
@@ -40,6 +42,8 @@ type WechatConfigServiceInterface interface {
// 用于将钱包扣款、套餐激活、佣金计算等核心逻辑委托给 B 端 order.Service 处理。
type OrderWalletPayServiceInterface interface {
WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error
CreatePendingOrder(ctx context.Context, order *model.Order, items []*model.OrderItem) error
RecordCreateFailure(ctx context.Context, order *model.Order, businessErr error)
}
// PaymentMethodPolicy 提供按资产类型校验支付方式的能力。
@@ -76,6 +80,8 @@ type Service struct {
redis *redis.Client
logger *zap.Logger
paymentMethodPolicy PaymentMethodPolicy
auditWriter *audit.Writer
paymentIntegration *integrationlog.Repository
}
// SetPaymentMethodPolicy 注入 C 端支付方式策略。
@@ -83,6 +89,12 @@ func (s *Service) SetPaymentMethodPolicy(policy PaymentMethodPolicy) {
s.paymentMethodPolicy = policy
}
// SetPaymentAudit 注入支付审计与外部交互日志接缝。
func (s *Service) SetPaymentAudit(writer *audit.Writer, integration *integrationlog.Repository) {
s.auditWriter = writer
s.paymentIntegration = integration
}
// New 创建客户端订单服务。
func New(
assetService *asset.Service,
@@ -131,7 +143,7 @@ func New(
// CreateOrder 创建客户端订单。
// 普通套餐下单:仅创建待支付订单,不发起支付,需后续调用 POST /orders/:id/pay 支付。
// 强充场景:检测到需要强充时,直接创建充值单并发起微信支付(一步完成),此时 app_type 必传。
func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (*dto.ClientCreateOrderResponse, error) {
func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.ClientCreateOrderRequest) (resp *dto.ClientCreateOrderResponse, err error) {
if req == nil {
return nil, errors.New(errors.CodeInvalidParam)
}
@@ -144,6 +156,23 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
if err != nil {
return nil, err
}
auditOrder := &model.Order{
OrderNo: "create:client:" + strings.TrimSpace(req.Identifier), BuyerType: model.BuyerTypePersonal,
BuyerID: customerID, AssetIdentifier: strings.TrimSpace(req.Identifier), PaymentMethod: req.PaymentMethod,
}
if assetInfo.AssetType == "card" || assetInfo.AssetType == constants.AssetTypeIotCard {
auditOrder.OrderType = model.OrderTypeSingleCard
auditOrder.IotCardID = &assetInfo.AssetID
} else {
auditOrder.OrderType = model.OrderTypeDevice
auditOrder.DeviceID = &assetInfo.AssetID
}
orderFlow := true
defer func() {
if orderFlow && s.orderPaymentService != nil {
s.orderPaymentService.RecordCreateFailure(skipCtx, auditOrder, err)
}
}()
if owned, err := s.customerBinding.OwnsAsset(skipCtx, customerID, assetInfo.AssetType, assetInfo.AssetID); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产归属失败")
} else if !owned {
@@ -249,6 +278,7 @@ func (s *Service) CreateOrder(ctx context.Context, customerID uint, req *dto.Cli
}()
if forceRecharge.NeedForceRecharge {
orderFlow = false
if s.paymentMethodPolicy == nil {
return nil, errors.New(errors.CodeNoPaymentConfig)
}
@@ -405,8 +435,11 @@ func (s *Service) createPackageOrder(
return nil, err
}
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
if s.orderPaymentService == nil {
return nil, errors.New(errors.CodeInternalError, "订单创建能力未配置")
}
if err := s.orderPaymentService.CreatePendingOrder(ctx, order, items); err != nil {
return nil, err
}
s.markClientPurchaseCreated(ctx, redisKey, order.OrderNo)
@@ -484,10 +517,6 @@ func (s *Service) createForceRechargeOrder(
AutoPurchaseStatus: model.AutoPurchaseStatusPending,
}
if err := s.rechargeOrderStore.Create(ctx, rechargeOrder); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
paymentNo := generateClientPaymentNo()
payment := &model.Payment{
PaymentNo: paymentNo,
@@ -499,12 +528,34 @@ func (s *Service) createForceRechargeOrder(
PaymentConfigID: &activeConfig.ID,
}
if err := s.paymentStore.Create(ctx, payment); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.rechargeOrderStore.CreateWithTx(ctx, tx, rechargeOrder); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
payment.OrderID = rechargeOrder.ID
if err := s.paymentStore.CreateWithTx(ctx, tx, payment); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
}
return s.appendPaymentCreatedAudit(ctx, tx, payment, nil, rechargeOrder)
}); err != nil {
return nil, err
}
attempt, startedAt, err := s.startPaymentAttempt(ctx, payment, paymentIntegrationProvider(activeConfig), "client_force_recharge")
if err != nil {
return nil, err
}
paymentResult, err := paymentProvider.CreateJSAPIPayment(ctx, paymentNo, "余额充值", openID, int(rechargeOrder.Amount))
if err != nil {
if completeErr := s.completePaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "支付预下单结果未知"); completeErr != nil {
return nil, completeErr
}
if updateErr := s.markPaymentFailed(ctx, payment, "支付预下单失败,关闭支付记录"); updateErr != nil {
return nil, updateErr
}
return nil, err
}
if err := s.completePaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
return nil, err
}
@@ -937,7 +988,7 @@ func (s *Service) getOrBuildAlipayPaymentLink(
} else {
// 过期或不存在,标记旧单 failed 并新建
if existing != nil {
if updateErr := s.paymentStore.UpdateStatus(ctx, existing.ID, model.PaymentRecordStatusFailed); updateErr != nil {
if updateErr := s.markPaymentFailed(ctx, existing, "支付宝支付记录过期关闭"); updateErr != nil {
s.logger.Warn("标记过期支付宝支付单 failed 失败",
zap.Uint("payment_id", existing.ID),
zap.Error(updateErr),
@@ -959,8 +1010,13 @@ func (s *Service) getOrBuildAlipayPaymentLink(
PaymentConfigID: &activeConfig.ID,
ExpireAt: &expireAt,
}
if err := s.paymentStore.Create(ctx, newPayment); err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建支付宝支付单失败")
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.paymentStore.CreateWithTx(ctx, tx, newPayment); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付宝支付单失败")
}
return s.appendPaymentCreatedAudit(ctx, tx, newPayment, nil, nil)
}); err != nil {
return nil, err
}
payment = newPayment
s.logger.Info("创建支付宝支付单",
@@ -977,7 +1033,7 @@ func (s *Service) getOrBuildAlipayPaymentLink(
if err != nil {
// 新建的 payment 生成链接失败,标记 failed
if existing == nil || payment.ID != existing.ID {
_ = s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed)
_ = s.markPaymentFailed(ctx, payment, "支付宝支付链接生成失败,关闭支付记录")
}
return nil, err
}
@@ -1074,14 +1130,17 @@ func (s *Service) createAlipayForceRechargeOrder(
return errors.Wrap(errors.CodeDatabaseError, err, "创建充值订单失败")
}
payment.OrderID = rechargeOrder.ID
return s.paymentStore.CreateWithTx(ctx, tx, payment)
if err := s.paymentStore.CreateWithTx(ctx, tx, payment); err != nil {
return err
}
return s.appendPaymentCreatedAudit(ctx, tx, payment, nil, rechargeOrder)
}); err != nil {
return nil, err
}
wapURL, err := alipay.BuildWapPayURL(ctx, activeConfig, payment, "余额充值")
if err != nil {
if updateErr := s.paymentStore.UpdateStatus(ctx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil {
if updateErr := s.markPaymentFailed(ctx, payment, "支付宝支付链接生成失败,关闭支付记录"); updateErr != nil {
s.logger.Warn("标记支付宝支付单 failed 失败",
zap.String("payment_no", paymentNo),
zap.Error(updateErr),
@@ -1407,14 +1466,21 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
if err := s.paymentStore.CreateWithTx(skipCtx, tx, payment); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建支付记录失败")
}
return nil
return s.appendPaymentCreatedAudit(skipCtx, tx, payment, order, nil)
}); err != nil {
return nil, err
}
attempt, startedAt, err := s.startPaymentAttempt(skipCtx, payment, paymentIntegrationProvider(activeConfig), "client_order")
if err != nil {
return nil, err
}
paymentResult, err := paymentProvider.CreateJSAPIPayment(skipCtx, paymentNo, "套餐购买", openID, int(order.TotalAmount))
if err != nil {
if updateErr := s.paymentStore.UpdateStatus(skipCtx, payment.ID, model.PaymentRecordStatusFailed); updateErr != nil {
if completeErr := s.completePaymentAttempt(skipCtx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "支付预下单结果未知"); completeErr != nil {
return nil, completeErr
}
if updateErr := s.markPaymentFailed(skipCtx, payment, "支付预下单失败,关闭支付记录"); updateErr != nil {
s.logger.Warn("标记支付记录失败状态失败",
zap.Uint("payment_id", payment.ID),
zap.String("payment_no", paymentNo),
@@ -1423,6 +1489,9 @@ func (s *Service) PayOrder(ctx context.Context, customerID uint, orderID uint, r
}
return nil, err
}
if err := s.completePaymentAttempt(skipCtx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
return nil, err
}
return &dto.ClientPayOrderResponse{
PaymentMethod: paymentMethod,
PayConfig: buildClientPayConfigFromResult(paymentResult),