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

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,60 @@
package export_task
import (
"context"
"strconv"
"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/pkg/auditfailure"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
func (s *Service) writeTaskAudit(ctx context.Context, tx *gorm.DB, actionCode, summary string, task *model.ExportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error {
scopeType, scopeID := constants.AuditScopePlatform, ""
if task.CreatorShopID != nil {
scopeType, scopeID = constants.AuditScopeShop, strconv.FormatUint(uint64(*task.CreatorShopID), 10)
}
return s.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
EventID: audit.TaskEventID(constants.AuditResourceExportTask, task.ID, phase),
ActionCode: actionCode, Summary: summary, TaskID: task.ID, TaskNo: task.TaskNo,
Actor: audit.ActorInput{
Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(middleware.GetUserIDFromContext(ctx)), 10),
Name: middleware.GetUsernameFromContext(ctx), ShopID: task.CreatorShopID, EnterpriseID: task.CreatorEnterpriseID,
},
Source: constants.AuditSourceAdminAPI, ScopeType: scopeType, ScopeID: scopeID,
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
IdentitySnapshot: map[string]any{
"id": task.ID, "task_no": task.TaskNo, "scene": task.Scene, "format": task.Format,
"creator_user_id": task.CreatorUserID, "creator_user_type": task.CreatorUserType,
"creator_shop_id": task.CreatorShopID, "creator_enterprise_id": task.CreatorEnterpriseID,
"scope_shop_ids": task.ScopeShopIDs,
},
BeforeData: before, AfterData: after,
})
}
func (s *Service) recordTaskAudit(ctx context.Context, actionCode, summary string, task *model.ExportTask, before, after map[string]any, result, phase string, errorCode int) {
if s == nil || s.auditWriter == nil || s.db == nil || task == nil || task.TaskNo == "" {
return
}
code := strconv.Itoa(errorCode)
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.writeTaskAudit(ctx, tx, actionCode, summary, task, before, after, result, phase, code, summary)
})
if err != nil {
auditfailure.RecordSecondaryWriteFailure(actionCode, task.TaskNo, "", task.TaskNo, code, err)
}
}
func exportTaskState(task *model.ExportTask) map[string]any {
if task == nil {
return nil
}
return map[string]any{
"status": task.Status, "cancel_requested": task.CancelRequested, "progress": task.Progress,
}
}

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
}