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

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

@@ -5,6 +5,7 @@ package wechat_config
import (
"context"
"fmt"
"strconv"
"time"
"github.com/bytedance/sonic"
@@ -12,10 +13,12 @@ import (
"go.uber.org/zap"
"gorm.io/gorm"
systemconfigapp "github.com/break/junhong_cmp_fiber/internal/application/systemconfig"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/auditfailure"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
@@ -24,11 +27,6 @@ import (
// Redis 缓存键
const redisActiveConfigKey = "wechat:config:active"
// AuditServiceInterface 审计日志服务接口
type AuditServiceInterface interface {
LogOperation(ctx context.Context, log *model.AccountOperationLog)
}
// Service 微信参数配置业务服务
type Service struct {
store *postgres.WechatConfigStore
@@ -36,7 +34,7 @@ type Service struct {
rechargeOrderStore *postgres.RechargeOrderStore
agentRechargeStore *postgres.AgentRechargeStore
paymentStore *postgres.PaymentStore
auditService AuditServiceInterface
audit systemconfigapp.AuditWriter
redis *redis.Client
logger *zap.Logger
}
@@ -48,7 +46,7 @@ func New(
rechargeOrderStore *postgres.RechargeOrderStore,
agentRechargeStore *postgres.AgentRechargeStore,
paymentStore *postgres.PaymentStore,
auditService AuditServiceInterface,
audit systemconfigapp.AuditWriter,
rdb *redis.Client,
logger *zap.Logger,
) *Service {
@@ -58,7 +56,7 @@ func New(
rechargeOrderStore: rechargeOrderStore,
agentRechargeStore: agentRechargeStore,
paymentStore: paymentStore,
auditService: auditService,
audit: audit,
redis: rdb,
logger: logger,
}
@@ -69,8 +67,17 @@ func New(
func (s *Service) Create(ctx context.Context, req *dto.CreateWechatConfigRequest) (*dto.WechatConfigResponse, error) {
// 根据 provider_type 校验必填字段
if err := s.validateProviderFields(req); err != nil {
s.recordAuditFailure(ctx, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: constants.AuditOperationPaymentConfigCreate,
Description: "拒绝创建非法支付连接配置", ConfigKey: "payment_config.new:" + req.Name,
DisplayName: req.Name, Identity: map[string]any{"name": req.Name, "provider_type": req.ProviderType, "credentials_configured": paymentRequestCredentialsConfigured(req)},
Result: constants.AuditResultDenied, ErrorCode: strconv.Itoa(errors.CodeInvalidParam), ErrorSummary: "支付连接配置字段校验失败",
})
return nil, err
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
var desc *string
if req.Description != "" {
@@ -118,28 +125,17 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateWechatConfigRequest
}
config.Creator = middleware.GetUserIDFromContext(ctx)
if err := s.store.Create(ctx, config); err != nil {
err := s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Create(ctx, config); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigCreate, "创建支付连接配置", nil, config)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigCreate, "创建支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "创建微信支付配置失败")
}
// 审计日志
afterData := model.JSONB{
"id": config.ID,
"name": config.Name,
"provider_type": config.ProviderType,
}
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: middleware.GetUserIDFromContext(ctx),
OperatorType: middleware.GetUserTypeFromContext(ctx),
OperatorName: "",
OperationType: "create",
OperationDesc: fmt.Sprintf("创建微信支付配置:%s", config.Name),
AfterData: afterData,
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
return dto.FromWechatConfigModel(config), nil
}
@@ -200,6 +196,10 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateWechatConf
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败")
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
// 合并字段:指针非 nil 时更新,敏感字段空字符串表示保持原值
if req.Name != nil {
@@ -256,7 +256,14 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateWechatConf
config.Updater = middleware.GetUserIDFromContext(ctx)
if err := s.store.Update(ctx, config); err != nil {
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Update(ctx, config); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigUpdate, "更新支付连接配置", &before, config)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigUpdate, "更新支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "更新微信支付配置失败")
}
@@ -265,24 +272,6 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateWechatConf
s.clearActiveConfigCache(ctx)
}
afterData := model.JSONB{
"id": config.ID,
"name": config.Name,
"provider_type": config.ProviderType,
"is_active": config.IsActive,
}
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: middleware.GetUserIDFromContext(ctx),
OperatorType: middleware.GetUserTypeFromContext(ctx),
OperatorName: "",
OperationType: "update",
OperationDesc: fmt.Sprintf("更新微信支付配置:%s", config.Name),
AfterData: afterData,
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
return dto.FromWechatConfigModel(config), nil
}
@@ -296,9 +285,13 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
}
return errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败")
}
if s.audit == nil {
return errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
// 不允许删除正在激活的配置
if config.IsActive {
s.recordPaymentDenied(ctx, constants.AuditOperationPaymentConfigDelete, "拒绝删除生效中的支付连接配置", config, errors.CodeWechatConfigActive)
return errors.New(errors.CodeWechatConfigActive)
}
@@ -314,32 +307,23 @@ func (s *Service) Delete(ctx context.Context, id uint) error {
}
if pendingOrders > 0 || pendingRecharges > 0 {
s.recordPaymentDenied(ctx, constants.AuditOperationPaymentConfigDelete, "拒绝删除存在在途业务的支付连接配置", config, errors.CodeWechatConfigHasPendingOrders)
return errors.New(errors.CodeWechatConfigHasPendingOrders)
}
if err := s.store.SoftDelete(ctx, id); err != nil {
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).SoftDelete(ctx, id); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDelete, "删除支付连接配置", config, nil)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigDelete, "删除支付连接配置失败", config, err)
return errors.Wrap(errors.CodeInternalError, err, "删除微信支付配置失败")
}
s.clearActiveConfigCache(ctx)
beforeData := model.JSONB{
"id": config.ID,
"name": config.Name,
"provider_type": config.ProviderType,
}
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: middleware.GetUserIDFromContext(ctx),
OperatorType: middleware.GetUserTypeFromContext(ctx),
OperatorName: "",
OperationType: "delete",
OperationDesc: fmt.Sprintf("删除微信支付配置:%s", config.Name),
BeforeData: beforeData,
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
return nil
}
@@ -353,19 +337,32 @@ func (s *Service) Activate(ctx context.Context, id uint) (*dto.WechatConfigRespo
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败")
}
// 记录旧的激活配置名称
oldActiveName := ""
oldActive, oldErr := s.store.GetActive(ctx)
if oldErr == nil && oldActive != nil {
oldActiveName = oldActive.Name
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
// 保留原激活配置快照,确保自动停用也进入该配置自身时间线。
oldActive, oldErr := s.store.GetActive(ctx)
// 事务内激活
db := s.store.DB()
if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.store.ActivateInTx(ctx, tx, id)
if err := s.store.ActivateInTx(ctx, tx, id); err != nil {
return err
}
after := before
after.IsActive = true
if oldErr == nil && oldActive != nil && oldActive.ID != id {
oldAfter := *oldActive
oldAfter.IsActive = false
if err := s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDeactivate, "激活其他配置时停用原支付连接配置", oldActive, &oldAfter); err != nil {
return err
}
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigActivate, "激活支付连接配置", &before, &after)
}); err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigActivate, "激活支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "激活微信支付配置失败")
}
@@ -374,22 +371,6 @@ func (s *Service) Activate(ctx context.Context, id uint) (*dto.WechatConfigRespo
// 重新查询最新状态
config, _ = s.store.GetByID(ctx, id)
desc := fmt.Sprintf("激活微信支付配置:%s", config.Name)
if oldActiveName != "" && oldActiveName != config.Name {
desc = fmt.Sprintf("激活微信支付配置:%s原激活配置%s", config.Name, oldActiveName)
}
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: middleware.GetUserIDFromContext(ctx),
OperatorType: middleware.GetUserTypeFromContext(ctx),
OperatorName: "",
OperationType: "activate",
OperationDesc: desc,
AfterData: model.JSONB{"id": config.ID, "name": config.Name, "is_active": true},
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
return dto.FromWechatConfigModel(config), nil
}
@@ -403,8 +384,21 @@ func (s *Service) Deactivate(ctx context.Context, id uint) (*dto.WechatConfigRes
}
return nil, errors.Wrap(errors.CodeInternalError, err, "获取微信支付配置失败")
}
if s.audit == nil {
return nil, errors.New(errors.CodeInvalidStatus, "支付配置审计接缝未配置")
}
before := *config
after := before
after.IsActive = false
if err := s.store.Deactivate(ctx, id); err != nil {
err = s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.store.WithTx(tx).Deactivate(ctx, id); err != nil {
return err
}
return s.writeAudit(ctx, tx, constants.AuditOperationPaymentConfigDeactivate, "停用支付连接配置", &before, &after)
})
if err != nil {
s.recordPaymentFailure(ctx, constants.AuditOperationPaymentConfigDeactivate, "停用支付连接配置失败", config, err)
return nil, errors.Wrap(errors.CodeInternalError, err, "停用微信支付配置失败")
}
@@ -413,21 +407,109 @@ func (s *Service) Deactivate(ctx context.Context, id uint) (*dto.WechatConfigRes
// 重新查询最新状态
config, _ = s.store.GetByID(ctx, id)
go s.auditService.LogOperation(ctx, &model.AccountOperationLog{
OperatorID: middleware.GetUserIDFromContext(ctx),
OperatorType: middleware.GetUserTypeFromContext(ctx),
OperatorName: "",
OperationType: "deactivate",
OperationDesc: fmt.Sprintf("停用微信支付配置:%s", config.Name),
AfterData: model.JSONB{"id": config.ID, "name": config.Name, "is_active": false},
RequestID: middleware.GetRequestIDFromContext(ctx),
IPAddress: middleware.GetIPFromContext(ctx),
UserAgent: middleware.GetUserAgentFromContext(ctx),
})
return dto.FromWechatConfigModel(config), nil
}
func (s *Service) writeAudit(ctx context.Context, tx *gorm.DB, operation, description string, before, after *model.WechatConfig) error {
config := after
if config == nil {
config = before
}
resourceID := strconv.FormatUint(uint64(config.ID), 10)
requestID := ""
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
requestID = *value
}
return s.audit.WriteConfigChange(ctx, tx, systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description,
ConfigKey: "payment_config." + resourceID, Module: "payment", ResourceID: &resourceID,
DisplayName: config.Name, Identity: paymentConfigIdentity(config),
BeforeData: paymentConfigAuditSnapshot(before), AfterData: paymentConfigAuditSnapshot(after),
RequestID: requestID, CorrelationID: requestID,
})
}
func (s *Service) recordPaymentDenied(ctx context.Context, operation, description string, config *model.WechatConfig, code int) {
s.recordAuditFailure(ctx, paymentFailureAudit(ctx, operation, description, config, constants.AuditResultDenied, code))
}
func (s *Service) recordPaymentFailure(ctx context.Context, operation, description string, config *model.WechatConfig, _ error) {
s.recordAuditFailure(ctx, paymentFailureAudit(ctx, operation, description, config, constants.AuditResultFailed, errors.CodeDatabaseError))
}
func (s *Service) recordAuditFailure(ctx context.Context, audit systemconfigapp.ChangeAudit) {
if s.audit == nil || s.store == nil || s.store.DB() == nil || audit.OperatorID == 0 || audit.ConfigKey == "" {
return
}
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
audit.RequestID = *value
audit.CorrelationID = *value
}
if err := s.store.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.audit.WriteConfigChange(ctx, tx, audit)
}); err != nil {
auditfailure.RecordSecondaryWriteFailure(audit.OperationType, audit.ConfigKey, audit.RequestID, audit.CorrelationID, audit.ErrorCode, err)
}
}
func paymentFailureAudit(ctx context.Context, operation, description string, config *model.WechatConfig, result string, code int) systemconfigapp.ChangeAudit {
resourceID := strconv.FormatUint(uint64(config.ID), 10)
return systemconfigapp.ChangeAudit{
OperatorID: middleware.GetUserIDFromContext(ctx), OperationType: operation, Description: description,
ConfigKey: "payment_config." + resourceID, Module: "payment", ResourceID: &resourceID,
DisplayName: config.Name, Identity: paymentConfigIdentity(config), BeforeData: paymentConfigAuditSnapshot(config),
Result: result, ErrorCode: strconv.Itoa(code), ErrorSummary: description,
}
}
func paymentConfigIdentity(config *model.WechatConfig) map[string]any {
if config == nil {
return nil
}
return map[string]any{
"id": config.ID, "name": config.Name, "provider_type": config.ProviderType,
"is_active": config.IsActive, "credentials_configured": paymentConfigCredentialsConfigured(config),
}
}
func paymentConfigAuditSnapshot(config *model.WechatConfig) map[string]any {
if config == nil {
return nil
}
return map[string]any{
"id": config.ID, "name": config.Name, "description": config.Description,
"provider_type": config.ProviderType, "is_active": config.IsActive,
"oa_app_id": config.OaAppID, "oa_oauth_redirect_url": config.OaOAuthRedirectURL,
"miniapp_app_id": config.MiniappAppID, "wx_mch_id": config.WxMchID,
"wx_serial_no": config.WxSerialNo, "wx_notify_url": config.WxNotifyURL,
"fy_ins_cd": config.FyInsCd, "fy_mchnt_cd": config.FyMchntCd, "fy_term_id": config.FyTermID,
"fy_api_url": config.FyAPIURL, "fy_notify_url": config.FyNotifyURL,
"ali_app_id": config.AliAppID, "ali_notify_url": config.AliNotifyURL,
"ali_return_url": config.AliReturnURL, "ali_production": config.AliProduction,
"ali_pay_expire_minutes": config.AliPayExpireMinutes,
"credentials_configured": paymentConfigCredentialsConfigured(config),
"oauth_configured": config.OaAppSecret != "" || config.OaToken != "" || config.OaAesKey != "" || config.MiniappAppSecret != "",
"wechat_payment_configured": config.WxAPIV3Key != "" || config.WxAPIV2Key != "" || config.WxCertContent != "" || config.WxKeyContent != "",
"fuiou_configured": config.FyPrivateKey != "" || config.FyPublicKey != "",
"alipay_configured": config.AliPrivateKey != "" || config.AliPublicKey != "",
}
}
func paymentConfigCredentialsConfigured(config *model.WechatConfig) bool {
if config == nil {
return false
}
return config.OaAppSecret != "" || config.OaToken != "" || config.OaAesKey != "" || config.MiniappAppSecret != "" ||
config.WxAPIV3Key != "" || config.WxAPIV2Key != "" || config.WxCertContent != "" || config.WxKeyContent != "" ||
config.FyPrivateKey != "" || config.FyPublicKey != "" || config.AliPrivateKey != "" || config.AliPublicKey != ""
}
func paymentRequestCredentialsConfigured(request *dto.CreateWechatConfigRequest) bool {
return request != nil && (request.OaAppSecret != "" || request.OaToken != "" || request.OaAesKey != "" || request.MiniappAppSecret != "" ||
request.WxAPIV3Key != "" || request.WxAPIV2Key != "" || request.WxCertContent != "" || request.WxKeyContent != "" ||
request.FyPrivateKey != "" || request.FyPublicKey != "" || request.AliPrivateKey != "" || request.AliPublicKey != "")
}
// GetActiveConfig 获取当前生效的支付配置(带 Redis 缓存)
// 缓存策略:命中直接返回,未命中查 DB 后缓存 5 分钟,无记录缓存 "none" 1 分钟
func (s *Service) GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) {