收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
60
internal/service/export_task/audit.go
Normal file
60
internal/service/export_task/audit.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user