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

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

@@ -2,6 +2,8 @@ package export_task
import (
"context"
stderrors "errors"
"strconv"
"time"
"github.com/bytedance/sonic"
@@ -10,10 +12,12 @@ import (
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/exporter"
"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"
"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"
@@ -28,6 +32,7 @@ type Service struct {
queueClient *queue.Client
storageSvc *storage.Service
sceneRegistry *exporter.Registry
auditWriter *audit.Writer
}
type dispatchPayload struct {
@@ -35,14 +40,18 @@ type dispatchPayload struct {
}
// New 创建导出任务服务。
func New(db *gorm.DB, taskStore *postgres.ExportTaskStore, queueClient *queue.Client, storageSvc *storage.Service) *Service {
return &Service{
func New(db *gorm.DB, taskStore *postgres.ExportTaskStore, queueClient *queue.Client, storageSvc *storage.Service, auditWriters ...*audit.Writer) *Service {
service := &Service{
db: db,
taskStore: taskStore,
queueClient: queueClient,
storageSvc: storageSvc,
sceneRegistry: exporter.NewDefaultRegistry(db),
}
if len(auditWriters) > 0 {
service.auditWriter = auditWriters[0]
}
return service
}
// CreateTask 创建导出任务并入队 dispatch。
@@ -118,7 +127,16 @@ func (s *Service) CreateTask(ctx context.Context, req *dto.CreateExportTaskReque
task.Creator = userID
task.Updater = userID
if err := s.taskStore.Create(ctx, task); err != nil {
if s.auditWriter == nil {
return nil, errors.New(errors.CodeInvalidStatus, "导出任务统一审计接缝未配置")
}
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.taskStore.WithTx(tx).Create(ctx, task); err != nil {
return err
}
return s.writeTaskAudit(ctx, tx, constants.AuditActionExportTaskCreated, "创建业务导出任务", task, nil, exportTaskState(task), constants.AuditResultSuccess, "created", "", "")
}); err != nil {
s.recordTaskAudit(ctx, constants.AuditActionExportTaskCreated, "创建业务导出任务失败", task, nil, exportTaskState(task), constants.AuditResultFailed, "create_failed", errors.CodeDatabaseError)
return nil, errors.Wrap(errors.CodeDatabaseError, err, "创建导出任务失败")
}
@@ -130,7 +148,18 @@ func (s *Service) CreateTask(ctx context.Context, req *dto.CreateExportTaskReque
asynq.Timeout(constants.ExportDispatchTaskTimeout),
asynq.Queue(constants.QueueForTaskType(constants.TaskTypeExportDispatch)),
); err != nil {
_ = s.taskStore.MarkFailed(ctx, task.ID, userID, "导出任务入队失败")
secondaryErr := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.taskStore.WithTx(tx).MarkFailed(ctx, task.ID, userID, "导出任务入队失败"); err != nil {
return err
}
before := exportTaskState(task)
task.Status = constants.ExportTaskStatusFailed
task.ErrorMessage = "导出任务入队失败"
return s.writeTaskAudit(ctx, tx, constants.AuditActionExportTaskCreated, "导出任务入队失败", task, before, exportTaskState(task), constants.AuditResultFailed, "enqueue_failed", strconv.Itoa(errors.CodeTaskQueueError), "导出任务入队失败")
})
if secondaryErr != nil {
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionExportTaskCreated, task.TaskNo, "", task.TaskNo, strconv.Itoa(errors.CodeTaskQueueError), secondaryErr)
}
return nil, errors.Wrap(errors.CodeTaskQueueError, err, "导出任务入队失败")
}
@@ -233,45 +262,68 @@ func (s *Service) CancelTask(ctx context.Context, id uint) (*dto.CancelExportTas
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询导出任务失败")
}
if s.auditWriter == nil {
return nil, errors.New(errors.CodeInvalidStatus, "导出任务统一审计接缝未配置")
}
message := "取消请求已提交"
switch task.Status {
case constants.ExportTaskStatusPending:
ok, err := s.taskStore.CancelPendingTask(ctx, id, userID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "取消导出任务失败")
}
if !ok {
return nil, errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
}
message = "任务已取消"
case constants.ExportTaskStatusProcessing:
if !task.CancelRequested {
ok, err := s.taskStore.SetCancelRequested(ctx, id, userID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "提交取消请求失败")
before := exportTaskState(task)
changed := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txStore := s.taskStore.WithTx(tx)
switch task.Status {
case constants.ExportTaskStatusPending:
ok, updateErr := txStore.CancelPendingTask(ctx, id, userID)
if updateErr != nil {
return updateErr
}
if !ok {
return nil, errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
return errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
}
} else {
message = "取消请求已提交,请稍后刷新状态"
task.Status, task.CancelRequested, task.Progress = constants.ExportTaskStatusCancelled, true, 100
message, changed = "任务已取消", true
case constants.ExportTaskStatusProcessing:
if task.CancelRequested {
message = "取消请求已提交,请稍后刷新状态"
return nil
}
ok, updateErr := txStore.SetCancelRequested(ctx, id, userID)
if updateErr != nil {
return updateErr
}
if !ok {
return errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
}
task.CancelRequested, changed = true, true
case constants.ExportTaskStatusCompleted, constants.ExportTaskStatusFailed, constants.ExportTaskStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
default:
return errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
}
case constants.ExportTaskStatusCompleted, constants.ExportTaskStatusFailed, constants.ExportTaskStatusCancelled:
return nil, errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
default:
return nil, errors.New(errors.CodeInvalidStatus, "当前状态不支持取消")
}
latestTask, err := s.taskStore.GetByID(ctx, id)
if !changed {
return nil
}
return s.writeTaskAudit(ctx, tx, constants.AuditActionExportTaskCancelled, message, task, before, exportTaskState(task), constants.AuditResultSuccess, "cancelled", "", "")
})
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询最新任务状态失败")
result := constants.AuditResultFailed
errorCode := errors.CodeDatabaseError
var appErr *errors.AppError
if stderrors.As(err, &appErr) && appErr.Code == errors.CodeInvalidStatus {
result = constants.AuditResultDenied
errorCode = appErr.Code
}
s.recordTaskAudit(ctx, constants.AuditActionExportTaskCancelled, "取消业务导出任务失败", task, before, exportTaskState(task), result, "", errorCode)
if appErr != nil {
return nil, appErr
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "取消导出任务失败")
}
return &dto.CancelExportTaskResponse{
TaskID: latestTask.ID,
Status: latestTask.Status,
StatusName: constants.GetExportTaskStatusName(latestTask.Status),
CancelRequested: latestTask.CancelRequested,
TaskID: task.ID,
Status: task.Status,
StatusName: constants.GetExportTaskStatusName(task.Status),
CancelRequested: task.CancelRequested,
Message: message,
}, nil
}