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

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,11 +2,17 @@ package device_import
import (
"context"
"strconv"
"time"
"gorm.io/gorm"
infraAudit "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"
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
"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"
)
// AssetAuditService 资产审计服务接口。
@@ -14,62 +20,62 @@ type AssetAuditService interface {
LogOperation(ctx context.Context, log *model.AssetOperationLog)
}
func (s *Service) logDeviceImportAudit(ctx context.Context, p assetAuditSvc.BuildLogParams) {
if s == nil || s.assetAudit == nil {
func (s *Service) writeDeviceImportTaskAudit(ctx context.Context, tx *gorm.DB, task *model.DeviceImportTask, before, after map[string]any, result, phase, errorCode, errorSummary string) error {
scopeType, scopeID := constants.AuditScopePlatform, ""
if task.OperatorShopID != nil {
scopeType, scopeID = constants.AuditScopeShop, strconv.FormatUint(uint64(*task.OperatorShopID), 10)
}
return s.auditWriter.WriteTask(ctx, tx, infraAudit.TaskInput{
EventID: infraAudit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, phase),
ActionCode: constants.AuditActionDeviceImportTaskCreated, Summary: "创建设备导入任务",
TaskID: task.ID, TaskNo: task.TaskNo,
Actor: infraAudit.ActorInput{
Kind: constants.AuditActorAccount, ID: strconv.FormatUint(uint64(middleware.GetUserIDFromContext(ctx)), 10),
Name: middleware.GetUsernameFromContext(ctx), ShopID: task.OperatorShopID,
},
Source: constants.AuditSourceAdminAPI, ScopeType: scopeType, ScopeID: scopeID,
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
IdentitySnapshot: map[string]any{
"id": task.ID, "task_no": task.TaskNo, "file_name": task.FileName,
"operation_type": task.OperationType, "target_id": task.TargetID,
"batch_no": task.BatchNo, "realname_policy": task.RealnamePolicy,
},
BeforeData: before, AfterData: after,
})
}
func (s *Service) recordDeviceImportTaskAudit(ctx context.Context, task *model.DeviceImportTask, before, after map[string]any, result, phase string, errorCode int, summary string) {
if s == nil || s.db == nil || s.auditWriter == nil || task == nil || task.TaskNo == "" {
return
}
if p.Operator.Type == "" {
p.Operator = assetAuditSvc.OperatorFromContext(ctx)
}
if p.OperationType == "" {
p.OperationType = constants.AssetAuditOpDeviceImportTaskCreate
}
if p.AssetType == "" {
p.AssetType = constants.AssetTypeDevice
}
p.BeforeData, p.AfterData = assetAuditSvc.WrapOperationContent(p.BeforeData, p.AfterData, nil)
s.assetAudit.LogOperation(ctx, assetAuditSvc.BuildLog(ctx, p))
}
func newDeviceImportAuditParams(
taskID uint,
taskNo string,
req *dto.ImportDeviceRequest,
resultStatus string,
err error,
) assetAuditSvc.BuildLogParams {
afterData := map[string]any{}
if req != nil {
afterData["batch_no"] = req.BatchNo
afterData["file_key"] = req.FileKey
afterData["realname_policy"] = req.RealnamePolicy
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
return assetAuditSvc.BuildLogParams{
AssetID: taskID,
AssetIdentifier: taskNo,
OperationDesc: "创建设备导入任务",
ResultStatus: resultStatus,
ErrorCode: errorCode,
ErrorMsg: errorMsg,
AfterData: afterData,
code := strconv.Itoa(errorCode)
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return s.writeDeviceImportTaskAudit(ctx, tx, task, before, after, result, phase, code, summary)
}); err != nil {
auditfailure.RecordSecondaryWriteFailure(constants.AuditActionDeviceImportTaskCreated, task.TaskNo, "", task.TaskNo, code, err)
}
}
func newDeviceBatchAllocationAuditParams(taskID uint, taskNo string, req *dto.CreateDeviceBatchAllocationRequest, resultStatus string, err error) assetAuditSvc.BuildLogParams {
afterData := map[string]any{}
if req != nil {
afterData["file_key"] = req.FileKey
afterData["operation_type"] = req.OperationType
if req.OperationType != constants.DeviceImportOperationRecall {
afterData["target_id"] = req.TargetID
func (s *Service) failEnqueueWithAudit(ctx context.Context, task *model.DeviceImportTask, summary string) error {
before := deviceImportTaskState(task)
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
now := time.Now()
if err := tx.WithContext(ctx).Model(&model.DeviceImportTask{}).Where("id = ?", task.ID).Updates(map[string]any{
"status": model.ImportTaskStatusFailed, "error_message": summary, "completed_at": now, "updated_at": now,
}).Error; err != nil {
return err
}
task.Status, task.ErrorMessage = model.ImportTaskStatusFailed, summary
return s.writeDeviceImportTaskAudit(ctx, tx, task, before, deviceImportTaskState(task), constants.AuditResultFailed, "enqueue_failed", strconv.Itoa(errors.CodeTaskQueueError), summary)
})
}
func deviceImportTaskState(task *model.DeviceImportTask) map[string]any {
if task == nil {
return nil
}
errorCode, errorMsg := assetAuditSvc.BuildErrorInfo(err)
return assetAuditSvc.BuildLogParams{
AssetID: taskID, AssetIdentifier: taskNo,
OperationType: constants.AssetAuditOpDeviceBatchTaskCreate,
OperationDesc: "创建设备CSV批量操作任务", ResultStatus: resultStatus,
ErrorCode: errorCode, ErrorMsg: errorMsg, AfterData: afterData,
return map[string]any{
"status": task.Status, "total_count": task.TotalCount, "success_count": task.SuccessCount,
"skip_count": task.SkipCount, "fail_count": task.FailCount, "warning_count": task.WarningCount,
}
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"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"
@@ -23,6 +24,7 @@ type Service struct {
importTaskStore *postgres.DeviceImportTaskStore
queueClient *queue.Client
assetAudit AssetAuditService
auditWriter *audit.Writer
}
type DeviceImportPayload struct {
@@ -34,21 +36,24 @@ func New(
importTaskStore *postgres.DeviceImportTaskStore,
queueClient *queue.Client,
assetAudit AssetAuditService,
auditWriters ...*audit.Writer,
) *Service {
return &Service{
service := &Service{
db: db,
importTaskStore: importTaskStore,
queueClient: queueClient,
assetAudit: assetAudit,
}
if len(auditWriters) > 0 {
service.auditWriter = auditWriters[0]
}
return service
}
func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceRequest) (*dto.ImportDeviceResponse, error) {
userID := middleware.GetUserIDFromContext(ctx)
if userID == 0 {
appErr := errors.New(errors.CodeUnauthorized, "未授权访问")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr))
return nil, appErr
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
}
taskNo := s.importTaskStore.GenerateTaskNo(ctx)
@@ -67,9 +72,17 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceReq
task.Creator = userID
task.Updater = userID
if err := s.importTaskStore.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 := tx.WithContext(ctx).Create(task).Error; err != nil {
return err
}
return s.writeDeviceImportTaskAudit(ctx, tx, task, nil, deviceImportTaskState(task), constants.AuditResultSuccess, "created", "", "")
}); err != nil {
appErr := errors.Wrap(errors.CodeInternalError, err, "创建导入任务失败")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr))
s.recordDeviceImportTaskAudit(ctx, task, nil, deviceImportTaskState(task), constants.AuditResultFailed, "create_failed", errors.CodeDatabaseError, "创建设备导入任务失败")
return nil, appErr
}
@@ -81,14 +94,13 @@ func (s *Service) CreateImportTask(ctx context.Context, req *dto.ImportDeviceReq
asynq.Queue(constants.QueueForTaskType(constants.TaskTypeDeviceImport)),
)
if err != nil {
s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败: "+err.Error())
if secondaryErr := s.failEnqueueWithAudit(ctx, task, "设备导入任务入队失败"); secondaryErr != nil {
s.recordDeviceImportTaskAudit(ctx, task, nil, deviceImportTaskState(task), constants.AuditResultFailed, "enqueue_audit_failed", errors.CodeTaskQueueError, "设备导入任务入队失败")
}
appErr := errors.Wrap(errors.CodeInternalError, err, "任务入队失败")
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
s.logDeviceImportAudit(ctx, newDeviceImportAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil))
return &dto.ImportDeviceResponse{
TaskID: task.ID,
TaskNo: taskNo,
@@ -101,9 +113,7 @@ func (s *Service) CreateBatchAllocationTask(ctx context.Context, req *dto.Create
userID := middleware.GetUserIDFromContext(ctx)
userType := middleware.GetUserTypeFromContext(ctx)
if userID == 0 || (userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent) {
appErr := errors.New(errors.CodeForbidden, "仅平台和代理后台账号可创建设备CSV批量任务")
s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(0, "", req, constants.AssetAuditResultDenied, appErr))
return nil, appErr
return nil, errors.New(errors.CodeForbidden, "仅平台和代理后台账号可创建设备CSV批量任务")
}
if req == nil || !constants.IsDeviceImportOperation(req.OperationType) || req.OperationType == constants.DeviceImportOperationCreate {
return nil, errors.New(errors.CodeInvalidParam, "设备CSV批量任务参数不合法")
@@ -136,20 +146,28 @@ func (s *Service) CreateBatchAllocationTask(ctx context.Context, req *dto.Create
CreatorName: middleware.GetUsernameFromContext(ctx),
}
task.Creator, task.Updater = userID, userID
if err := s.importTaskStore.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 := tx.WithContext(ctx).Create(task).Error; err != nil {
return err
}
return s.writeDeviceImportTaskAudit(ctx, tx, task, nil, deviceImportTaskState(task), constants.AuditResultSuccess, "created", "", "")
}); err != nil {
appErr := errors.Wrap(errors.CodeDatabaseError, err, "创建设备CSV批量任务失败")
s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(0, taskNo, req, constants.AssetAuditResultFailed, appErr))
s.recordDeviceImportTaskAudit(ctx, task, nil, deviceImportTaskState(task), constants.AuditResultFailed, "create_failed", errors.CodeDatabaseError, "创建设备 CSV 批量任务失败")
return nil, appErr
}
if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeDeviceImport, DeviceImportPayload{TaskID: task.ID},
asynq.Queue(constants.QueueForTaskType(constants.TaskTypeDeviceImport)),
asynq.Timeout(constants.DeviceBatchAllocationTaskTimeout)); err != nil {
_ = s.importTaskStore.UpdateStatus(ctx, task.ID, model.ImportTaskStatusFailed, "任务入队失败")
if secondaryErr := s.failEnqueueWithAudit(ctx, task, "设备 CSV 批量任务入队失败"); secondaryErr != nil {
s.recordDeviceImportTaskAudit(ctx, task, nil, deviceImportTaskState(task), constants.AuditResultFailed, "enqueue_audit_failed", errors.CodeTaskQueueError, "设备 CSV 批量任务入队失败")
}
appErr := errors.Wrap(errors.CodeInternalError, err, "设备CSV批量任务入队失败")
s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(task.ID, taskNo, req, constants.AssetAuditResultFailed, appErr))
return nil, appErr
}
s.logDeviceImportAudit(ctx, newDeviceBatchAllocationAuditParams(task.ID, taskNo, req, constants.AssetAuditResultSuccess, nil))
return &dto.CreateDeviceBatchAllocationResponse{
TaskID: task.ID, TaskNo: task.TaskNo, Message: "设备CSV批量任务已创建Worker 将异步处理CSV文件",
}, nil