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

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

@@ -14,10 +14,14 @@ import (
"github.com/bytedance/sonic"
"github.com/hibiken/asynq"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
"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/postgres"
"github.com/break/junhong_cmp_fiber/pkg/asynctask"
"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"
@@ -41,11 +45,16 @@ type AssetPackageBatchOrderHandler struct {
orderCreator AssetPackageBatchOrderCreator
storageService *storage.Service
logger *zap.Logger
auditWriter *audit.Writer
}
// NewAssetPackageBatchOrderHandler 创建资产套餐批量订购任务处理器。
func NewAssetPackageBatchOrderHandler(taskStore *postgres.AssetPackageBatchOrderTaskStore, shopStore *postgres.ShopStore, orderCreator AssetPackageBatchOrderCreator, storageService *storage.Service, logger *zap.Logger) *AssetPackageBatchOrderHandler {
return &AssetPackageBatchOrderHandler{taskStore: taskStore, shopStore: shopStore, orderCreator: orderCreator, storageService: storageService, logger: logger}
func NewAssetPackageBatchOrderHandler(taskStore *postgres.AssetPackageBatchOrderTaskStore, shopStore *postgres.ShopStore, orderCreator AssetPackageBatchOrderCreator, storageService *storage.Service, logger *zap.Logger, auditWriters ...*audit.Writer) *AssetPackageBatchOrderHandler {
handler := &AssetPackageBatchOrderHandler{taskStore: taskStore, shopStore: shopStore, orderCreator: orderCreator, storageService: storageService, logger: logger}
if len(auditWriters) > 0 {
handler.auditWriter = auditWriters[0]
}
return handler
}
// Handle 处理资产套餐批量订购任务。
@@ -60,6 +69,12 @@ func (h *AssetPackageBatchOrderHandler) Handle(ctx context.Context, taskMessage
h.logger.Error("查询资产套餐批量订购任务失败", zap.Uint("task_id", payload.TaskID), zap.Error(err))
return asynq.SkipRetry
}
rootEventID := audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed")
ctx = auditcontext.With(ctx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAssetPackageBatchOrder,
ActorName: "资产套餐批量订购任务", Source: constants.AuditSourceWorker,
CorrelationID: taskRecord.TaskNo, ParentEventID: rootEventID,
})
claimed, err := h.taskStore.Claim(ctx, taskRecord.ID)
if err != nil {
return err
@@ -71,17 +86,68 @@ func (h *AssetPackageBatchOrderHandler) Handle(ctx context.Context, taskMessage
rows, err := h.downloadAndParse(ctx, taskRecord.StorageKey)
if err != nil {
h.logger.Error("下载或解析批量订购CSV失败", zap.Uint("task_id", taskRecord.ID), zap.Error(err))
_ = h.taskStore.MarkFailed(ctx, taskRecord.ID, err.Error())
if finishErr := h.finishBatchOrderTask(ctx, taskRecord, nil, 0, 1, asynctask.StatusFailed, err.Error()); finishErr != nil {
h.resetBatchOrderTaskForRetry(ctx, taskRecord.ID)
return finishErr
}
return asynq.SkipRetry
}
items, successCount, failCount := h.processRows(ctx, taskRecord, rows)
if err := h.taskStore.Complete(ctx, taskRecord.ID, items, successCount, failCount); err != nil {
if err := h.finishBatchOrderTask(ctx, taskRecord, items, successCount, failCount, asynctask.StatusCompleted, ""); err != nil {
h.resetBatchOrderTaskForRetry(ctx, taskRecord.ID)
return err
}
h.logger.Info("资产套餐批量订购任务完成", zap.Uint("task_id", taskRecord.ID), zap.Int("success", successCount), zap.Int("fail", failCount))
return nil
}
func (h *AssetPackageBatchOrderHandler) resetBatchOrderTaskForRetry(ctx context.Context, taskID uint) {
_ = h.taskStore.DB().WithContext(ctx).Model(&model.AssetPackageBatchOrderTask{}).
Where("id = ? AND status = ?", taskID, asynctask.StatusProcessing).
Updates(map[string]any{"status": asynctask.StatusPending, "started_at": nil}).Error
}
func (h *AssetPackageBatchOrderHandler) finishBatchOrderTask(ctx context.Context, taskRecord *model.AssetPackageBatchOrderTask, items model.AssetPackageBatchOrderResultItems, successCount, failCount, status int, errorMessage string) error {
if h.auditWriter == nil {
return apperrors.New(apperrors.CodeInvalidStatus, "资产套餐批量订购统一审计接缝未配置")
}
return h.taskStore.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txStore := h.taskStore.WithTx(tx)
if status == asynctask.StatusFailed {
if err := txStore.MarkFailed(ctx, taskRecord.ID, errorMessage); err != nil {
return err
}
} else if err := txStore.Complete(ctx, taskRecord.ID, items, successCount, failCount); err != nil {
return err
}
rootID := audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed")
var childCount int64
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
Where("parent_event_id = ? AND action_code = ?", rootID, constants.AuditActionOrderCreated).
Count(&childCount).Error; err != nil {
return err
}
result := batchAuditResult(int(childCount), failCount)
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
EventID: rootID, ActionCode: constants.AuditActionAssetPackageBatchOrderTaskCompleted,
Summary: "完成资产套餐批量订购任务", TaskID: taskRecord.ID, TaskNo: taskRecord.TaskNo,
Result: result, CorrelationID: taskRecord.TaskNo,
ParentEventID: audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "created"),
BatchTotal: len(items), SuccessCount: int(childCount), FailCount: failCount,
IdentitySnapshot: map[string]any{
"id": taskRecord.ID, "task_no": taskRecord.TaskNo, "file_name": taskRecord.FileName,
"package_id": taskRecord.PackageID, "package_code": taskRecord.PackageCode,
"package_name": taskRecord.PackageName, "payment_method": taskRecord.PaymentMethod,
},
BeforeData: map[string]any{"status": asynctask.StatusProcessing},
AfterData: map[string]any{
"status": status, "total_count": len(items), "success_count": successCount, "fail_count": failCount,
},
Metadata: map[string]any{"task_success_count": successCount, "task_fail_count": failCount},
})
})
}
type assetPackageBatchOrderRow struct {
Line int
Identifier string
@@ -150,6 +216,12 @@ func (h *AssetPackageBatchOrderHandler) processRows(ctx context.Context, taskRec
Username: taskRecord.CreatorName, ShopID: taskRecord.CreatorShopID,
SubordinateShopIDs: subordinateShopIDs,
})
workerCtx = auditcontext.With(workerCtx, auditcontext.Context{
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeAssetPackageBatchOrder,
ActorName: "资产套餐批量订购任务", Source: constants.AuditSourceWorker,
CorrelationID: taskRecord.TaskNo,
ParentEventID: audit.TaskEventID(constants.AuditResourceAssetPackageBatchOrderTask, taskRecord.ID, "completed"),
})
buyerType, buyerID := "", uint(0)
if taskRecord.CreatorUserType == constants.UserTypeAgent {
buyerType, buyerID = model.BuyerTypeAgent, taskRecord.CreatorShopID