package asset import ( "context" stderrors "errors" "strconv" infraAudit "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/auditfailure" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "gorm.io/gorm" ) var deactivatableAssetStatuses = []int{constants.AssetStatusInStock, constants.AssetStatusSold} // LifecycleService 资产生命周期服务。 type LifecycleService struct { db *gorm.DB iotCardStore *postgres.IotCardStore deviceStore *postgres.DeviceStore auditWriter *infraAudit.Writer } // NewLifecycleService 创建资产生命周期服务。 func NewLifecycleService(db *gorm.DB, iotCardStore *postgres.IotCardStore, deviceStore *postgres.DeviceStore, auditWriter *infraAudit.Writer) *LifecycleService { return &LifecycleService{db: db, iotCardStore: iotCardStore, deviceStore: deviceStore, auditWriter: auditWriter} } // DeactivateIotCard 手动停用 IoT 卡。 func (s *LifecycleService) DeactivateIotCard(ctx context.Context, id uint) error { card, err := s.iotCardStore.GetByID(ctx, id) if err != nil { appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败") if stderrors.Is(err, gorm.ErrRecordNotFound) { appErr = errors.New(errors.CodeIotCardNotFound) } result := constants.AuditResultFailed if stderrors.Is(err, gorm.ErrRecordNotFound) { result = constants.AuditResultDenied } s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", result, cardResourceStub(id), nil, appErr) return appErr } beforeData := map[string]any{"asset_status": card.AssetStatus} if !canDeactivateAsset(card.AssetStatus) { appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用") s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡被拒绝", constants.AuditResultDenied, card, beforeData, appErr) return appErr } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { result := tx.Model(&model.IotCard{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用IoT卡失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") } return s.appendCardDeactivationAudit(ctx, tx, card, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil) }) if err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionIotCardDeactivated, "停用 IoT 卡失败", constants.AuditResultFailed, card, beforeData, err) } return err } // DeactivateDevice 手动停用设备。 func (s *LifecycleService) DeactivateDevice(ctx context.Context, id uint) error { device, err := s.deviceStore.GetByID(ctx, id) if err != nil { appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败") if stderrors.Is(err, gorm.ErrRecordNotFound) { appErr = errors.New(errors.CodeNotFound, "设备不存在") } result := constants.AuditResultFailed if stderrors.Is(err, gorm.ErrRecordNotFound) { result = constants.AuditResultDenied } s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", result, deviceResourceStub(id), nil, appErr) return appErr } beforeData := map[string]any{"asset_status": device.AssetStatus} if !canDeactivateAsset(device.AssetStatus) { appErr := errors.New(errors.CodeForbidden, "当前状态不允许停用") s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备被拒绝", constants.AuditResultDenied, device, beforeData, appErr) return appErr } err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { result := tx.Model(&model.Device{}).Where("id = ? AND asset_status IN ?", id, deactivatableAssetStatuses).Update("asset_status", constants.AssetStatusDeactivated) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "停用设备失败") } if result.RowsAffected == 0 { return errors.New(errors.CodeConflict, "状态已变更,请刷新后重试") } return s.appendDeviceDeactivationAudit(ctx, tx, device, beforeData, map[string]any{"asset_status": constants.AssetStatusDeactivated}, constants.AuditResultSuccess, nil) }) if err != nil { s.recordLifecycleFailure(ctx, constants.AuditActionDeviceDeactivated, "停用设备失败", constants.AuditResultFailed, device, beforeData, err) } return err } func (s *LifecycleService) appendCardDeactivationAudit(ctx context.Context, tx *gorm.DB, card *model.IotCard, beforeData, afterData map[string]any, result string, businessErr error) error { if s.auditWriter == nil || card == nil || card.ID == 0 { return errors.New(errors.CodeInvalidStatus, "IoT 卡资产统一审计接缝未配置或资源不完整") } id := strconv.FormatUint(uint64(card.ID), 10) errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{ ActionCode: constants.AuditActionIotCardDeactivated, Summary: "停用 IoT 卡资产", Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform, Resources: []infraAudit.ResourceInput{{ Type: constants.AuditResourceIotCard, ID: &id, Key: infraAudit.IotCardResourceKey(card), DisplayName: card.ICCID, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget, IdentitySnapshot: infraAudit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡资产已停用", }}, }) } func (s *LifecycleService) appendDeviceDeactivationAudit(ctx context.Context, tx *gorm.DB, device *model.Device, beforeData, afterData map[string]any, result string, businessErr error) error { if s.auditWriter == nil || device == nil || device.ID == 0 { return errors.New(errors.CodeInvalidStatus, "设备资产统一审计接缝未配置或资源不完整") } id := strconv.FormatUint(uint64(device.ID), 10) errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr) return s.auditWriter.Append(ctx, tx, infraAudit.AppendInput{ ActionCode: constants.AuditActionDeviceDeactivated, Summary: "停用设备资产", Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary, ScopeType: constants.AuditScopePlatform, Resources: []infraAudit.ResourceInput{{ Type: constants.AuditResourceDevice, ID: &id, Key: infraAudit.DeviceResourceKey(device), DisplayName: device.VirtualNo, Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget, IdentitySnapshot: infraAudit.DeviceIdentitySnapshot(device), BeforeData: beforeData, AfterData: afterData, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "设备资产已停用", }}, }) } func (s *LifecycleService) recordLifecycleFailure(ctx context.Context, actionCode, summary, result string, resource any, beforeData map[string]any, businessErr error) { if s == nil || s.db == nil || s.auditWriter == nil { return } err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { switch value := resource.(type) { case *model.IotCard: return s.appendCardDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr) case *model.Device: return s.appendDeviceDeactivationAudit(ctx, tx, value, beforeData, nil, result, businessErr) default: return errors.New(errors.CodeInvalidStatus, "资产审计资源类型无效") } }) if err == nil { return } linkage := auditcontext.From(ctx) errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr) auditfailure.RecordSecondaryWriteFailure(actionCode, summary, linkage.RequestID, linkage.CorrelationID, errorCode, err) } func cardResourceStub(id uint) *model.IotCard { return &model.IotCard{Model: gorm.Model{ID: id}} } func deviceResourceStub(id uint) *model.Device { return &model.Device{Model: gorm.Model{ID: id}} } func canDeactivateAsset(assetStatus int) bool { return assetStatus == constants.AssetStatusInStock || assetStatus == constants.AssetStatusSold }