收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,10 +15,13 @@ import (
|
||||
"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/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/storage"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
)
|
||||
@@ -45,6 +49,7 @@ type DeviceImportHandler struct {
|
||||
assetWalletStore *postgres.AssetWalletStore
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
storageService *storage.Service
|
||||
auditWriter *audit.Writer
|
||||
logger *zap.Logger
|
||||
allocationExecutor DeviceBatchAllocationExecutor
|
||||
}
|
||||
@@ -59,6 +64,7 @@ func NewDeviceImportHandler(
|
||||
assetWalletStore *postgres.AssetWalletStore,
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore,
|
||||
storageSvc *storage.Service,
|
||||
auditWriter *audit.Writer,
|
||||
logger *zap.Logger,
|
||||
allocationExecutor DeviceBatchAllocationExecutor,
|
||||
) *DeviceImportHandler {
|
||||
@@ -72,6 +78,7 @@ func NewDeviceImportHandler(
|
||||
assetWalletStore: assetWalletStore,
|
||||
assetIdentifierStore: assetIdentifierStore,
|
||||
storageService: storageSvc,
|
||||
auditWriter: auditWriter,
|
||||
logger: logger,
|
||||
allocationExecutor: allocationExecutor,
|
||||
}
|
||||
@@ -95,6 +102,12 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
)
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
ctx = auditcontext.With(ctx, auditcontext.Context{
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.TaskTypeDeviceImport,
|
||||
ActorName: "设备导入任务", Source: constants.AuditSourceWorker,
|
||||
CorrelationID: importTask.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, importTask.ID, "completed"),
|
||||
})
|
||||
|
||||
switch importTask.Status {
|
||||
case model.ImportTaskStatusPending:
|
||||
@@ -132,7 +145,9 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
zap.Uint("task_id", importTask.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, err.Error())
|
||||
if finishErr := h.finishDeviceImportTask(ctx, importTask, 0, 0, 1, model.ImportTaskStatusFailed, err.Error()); finishErr != nil {
|
||||
return finishErr
|
||||
}
|
||||
return asynq.SkipRetry
|
||||
}
|
||||
|
||||
@@ -147,12 +162,13 @@ func (h *DeviceImportHandler) HandleDeviceImport(ctx context.Context, task *asyn
|
||||
result.failCount++
|
||||
}
|
||||
|
||||
h.importTaskStore.UpdateResult(ctx, importTask.ID, parseResult.TotalCount, result.successCount, result.skipCount, result.failCount, 0, result.skippedItems, result.failedItems, nil)
|
||||
|
||||
importTask.TotalCount = parseResult.TotalCount
|
||||
status, errorMessage := model.ImportTaskStatusCompleted, ""
|
||||
if result.failCount > 0 && result.successCount == 0 {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusFailed, "所有导入均失败")
|
||||
} else {
|
||||
h.importTaskStore.UpdateStatus(ctx, importTask.ID, model.ImportTaskStatusCompleted, "")
|
||||
status, errorMessage = model.ImportTaskStatusFailed, "所有导入均失败"
|
||||
}
|
||||
if err := h.finishDeviceImportTask(ctx, importTask, result.successCount, result.skipCount, result.failCount, status, errorMessage, result.skippedItems, result.failedItems); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("设备导入任务完成",
|
||||
@@ -393,7 +409,7 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return h.appendDeviceCreateAudit(ctx, tx, task, device)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -423,4 +439,137 @@ func (h *DeviceImportHandler) processBatch(ctx context.Context, task *model.Devi
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DeviceImportHandler) appendDeviceCreateAudit(ctx context.Context, tx *gorm.DB, task *model.DeviceImportTask, device *model.Device) error {
|
||||
if h.auditWriter == nil || task == nil || device == nil || device.ID == 0 {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "设备导入统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &resourceID,
|
||||
Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(device), AfterData: map[string]any{"created": true},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备已导入",
|
||||
}}
|
||||
var bindings []*model.DeviceSimBinding
|
||||
if err := tx.WithContext(ctx).Where("device_id = ? AND bind_status = ?", device.ID, constants.BindStatusBound).Order("slot_position ASC").Find(&bindings).Error; err != nil {
|
||||
return pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询设备导入卡槽关系失败")
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
cardIDs = append(cardIDs, binding.IotCardID)
|
||||
}
|
||||
cardByID := make(map[uint]*model.IotCard, len(cardIDs))
|
||||
if len(cardIDs) > 0 {
|
||||
var cards []*model.IotCard
|
||||
if err := tx.WithContext(ctx).Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
|
||||
return pkgerrors.Wrap(pkgerrors.CodeDatabaseError, err, "查询设备导入绑定卡失败")
|
||||
}
|
||||
for _, card := range cards {
|
||||
cardByID[card.ID] = card
|
||||
}
|
||||
}
|
||||
for index, binding := range bindings {
|
||||
card := cardByID[binding.IotCardID]
|
||||
if card != nil {
|
||||
cardID := strconv.FormatUint(uint64(card.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID,
|
||||
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleDeviceBindingTargetCard,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(card),
|
||||
AfterData: map[string]any{"device_id": device.ID, "slot_position": binding.SlotPosition},
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备导入并绑定 IoT 卡", SortOrder: index*2 + 1,
|
||||
})
|
||||
}
|
||||
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
||||
identity := map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": device.VirtualNo,
|
||||
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID, "is_current": binding.IsCurrent,
|
||||
}
|
||||
if card != nil {
|
||||
identity["iccid"] = card.ICCID
|
||||
identity["virtual_no"] = card.VirtualNo
|
||||
}
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
||||
Key: bindingID, DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleDeviceCreatedBinding,
|
||||
IdentitySnapshot: identity,
|
||||
AfterData: map[string]any{
|
||||
"slot_position": binding.SlotPosition, "bind_status": constants.BindStatusBound, "is_current": binding.IsCurrent,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index*2 + 2,
|
||||
})
|
||||
}
|
||||
return h.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, device.ID, "item"),
|
||||
ActionCode: constants.AuditActionDeviceCreated, Summary: "导入创建设备",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
Metadata: map[string]any{"import_task_id": task.ID, "import_task_no": task.TaskNo},
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *DeviceImportHandler) finishDeviceImportTask(ctx context.Context, task *model.DeviceImportTask, successCount, skipCount, failCount, status int, errorMessage string, items ...model.ImportResultItems) error {
|
||||
if h.auditWriter == nil {
|
||||
return pkgerrors.New(pkgerrors.CodeInvalidStatus, "设备导入任务统一审计接缝未配置")
|
||||
}
|
||||
return h.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
updates := map[string]any{
|
||||
"status": status, "total_count": task.TotalCount, "success_count": successCount,
|
||||
"skip_count": skipCount, "fail_count": failCount, "error_message": errorMessage,
|
||||
"completed_at": now, "updated_at": now,
|
||||
}
|
||||
if len(items) > 0 {
|
||||
updates["skipped_items"] = items[0]
|
||||
}
|
||||
if len(items) > 1 {
|
||||
updates["failed_items"] = items[1]
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.DeviceImportTask{}).Where("id = ?", task.ID).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
rootID := audit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, "completed")
|
||||
var childCount int64
|
||||
if err := tx.WithContext(ctx).Model(&model.AuditEvent{}).
|
||||
Where("correlation_id = ? AND action_code = ? AND result = ?", task.TaskNo, deviceImportItemAction(task.OperationType), constants.AuditResultSuccess).
|
||||
Count(&childCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return h.auditWriter.WriteTask(ctx, tx, audit.TaskInput{
|
||||
EventID: rootID, ActionCode: constants.AuditActionDeviceImportTaskCompleted,
|
||||
Summary: "完成设备导入任务", TaskID: task.ID, TaskNo: task.TaskNo,
|
||||
Result: batchAuditResult(int(childCount), failCount), CorrelationID: task.TaskNo,
|
||||
ParentEventID: audit.TaskEventID(constants.AuditResourceDeviceImportTask, task.ID, "created"),
|
||||
BatchTotal: task.TotalCount, SuccessCount: int(childCount), FailCount: failCount,
|
||||
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: map[string]any{"status": model.ImportTaskStatusProcessing},
|
||||
AfterData: map[string]any{
|
||||
"status": status, "total_count": task.TotalCount, "success_count": successCount,
|
||||
"skip_count": skipCount, "fail_count": failCount,
|
||||
},
|
||||
Metadata: map[string]any{"skip_count": skipCount},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func deviceImportItemAction(operationType string) string {
|
||||
switch operationType {
|
||||
case constants.DeviceImportOperationAssignShop:
|
||||
return constants.AuditActionDeviceAllocated
|
||||
case constants.DeviceImportOperationAssignSeries:
|
||||
return constants.AuditActionDeviceSeriesBound
|
||||
case constants.DeviceImportOperationRecall:
|
||||
return constants.AuditActionDeviceRecalled
|
||||
default:
|
||||
return constants.AuditActionDeviceCreated
|
||||
}
|
||||
}
|
||||
|
||||
var ErrMissingDeviceNoColumn = stderrors.New("CSV 缺少 virtual_no 列")
|
||||
|
||||
Reference in New Issue
Block a user