收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -1,117 +0,0 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
func (s *Service) appendCSVBatchAllocationAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
devices []*model.Device,
|
||||
succeededIDs []uint,
|
||||
failedItems []dto.AllocationDeviceFailedItem,
|
||||
targetShopID uint,
|
||||
) error {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if s.auditWriter == nil || linkage.ActorKind != constants.AuditActorSystemTask ||
|
||||
linkage.ActorID != constants.TaskTypeDeviceImport || linkage.Source != constants.AuditSourceWorker ||
|
||||
linkage.CorrelationID == "" {
|
||||
return nil
|
||||
}
|
||||
devicesByID := make(map[uint]*model.Device, len(devices))
|
||||
for _, device := range devices {
|
||||
if device != nil {
|
||||
devicesByID[device.ID] = device
|
||||
}
|
||||
}
|
||||
rootEventID := stableBatchEventID("root", linkage.CorrelationID)
|
||||
children := make([]audit.AppendInput, 0, len(succeededIDs)+len(failedItems))
|
||||
for _, deviceID := range succeededIDs {
|
||||
if device := devicesByID[deviceID]; device != nil {
|
||||
children = append(children, deviceBatchChild(device, rootEventID, linkage.CorrelationID, targetShopID, true, ""))
|
||||
}
|
||||
}
|
||||
for _, item := range failedItems {
|
||||
if device := devicesByID[item.DeviceID]; device != nil {
|
||||
children = append(children, deviceBatchChild(device, rootEventID, linkage.CorrelationID, targetShopID, false, item.Reason))
|
||||
}
|
||||
}
|
||||
result := constants.AuditResultSuccess
|
||||
if len(succeededIDs) > 0 && len(failedItems) > 0 {
|
||||
result = constants.AuditResultPartial
|
||||
}
|
||||
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
|
||||
Root: audit.AppendInput{
|
||||
EventID: rootEventID, ActionCode: constants.AuditActionDeviceBatchAllocationCompleted,
|
||||
Summary: "设备CSV批量分配完成", Result: result,
|
||||
CorrelationID: linkage.CorrelationID,
|
||||
BatchTotal: len(succeededIDs) + len(failedItems), SuccessCount: len(succeededIDs), FailCount: len(failedItems),
|
||||
Metadata: map[string]any{"operation_type": constants.DeviceImportOperationAssignShop, "target_shop_id": targetShopID},
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDeviceBatchTask, Key: linkage.CorrelationID, DisplayName: linkage.CorrelationID,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleBatchTask,
|
||||
IdentitySnapshot: map[string]any{"task_no": linkage.CorrelationID, "operation_type": constants.DeviceImportOperationAssignShop},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
},
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
func deviceBatchChild(
|
||||
device *model.Device,
|
||||
parentEventID string,
|
||||
correlationID string,
|
||||
targetShopID uint,
|
||||
succeeded bool,
|
||||
reason string,
|
||||
) audit.AppendInput {
|
||||
resourceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
before := map[string]any{"shop_id": device.ShopID, "status": device.Status}
|
||||
after := before
|
||||
result := constants.AuditResultFailed
|
||||
summary := "设备批量分配失败"
|
||||
if succeeded {
|
||||
after = map[string]any{"shop_id": targetShopID, "status": constants.DeviceStatusDistributed}
|
||||
result = constants.AuditResultSuccess
|
||||
summary = "设备批量分配成功"
|
||||
}
|
||||
return audit.AppendInput{
|
||||
EventID: stableBatchEventID("device", correlationID+":"+resourceID),
|
||||
ActionCode: constants.AuditActionDeviceBatchAllocationItem, Summary: summary,
|
||||
Result: result, ErrorSummary: reason, CorrelationID: correlationID, ParentEventID: parentEventID,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &resourceID, Key: deviceAuditKey(device), DisplayName: deviceAuditKey(device),
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleBatchItem,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": device.ID, "virtual_no": device.VirtualNo, "imei": device.IMEI, "sn": device.SN,
|
||||
"shop_id": device.ShopID, "series_id": device.SeriesID, "generation": device.Generation,
|
||||
},
|
||||
BeforeData: before, AfterData: after,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func stableBatchEventID(kind, key string) string {
|
||||
return "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("device-batch:"+kind+":"+key)).String()
|
||||
}
|
||||
|
||||
func deviceAuditKey(device *model.Device) string {
|
||||
for _, value := range []string{device.VirtualNo, device.IMEI, device.SN} {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return strconv.FormatUint(uint64(device.ID), 10)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"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/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
@@ -80,105 +81,29 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"device_id": deviceID,
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return nil, appErr
|
||||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"device_id": deviceID,
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
metadata := map[string]any{"iot_card_id": req.IotCardID, "slot_position": req.SlotPosition}
|
||||
|
||||
if req.SlotPosition > device.MaxSimSlots {
|
||||
appErr := errors.New(errors.CodeInvalidParam, "插槽位置超出设备最大插槽数")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied,
|
||||
device, nil, nil, metadata, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
existingBinding, err := s.deviceSimBindingStore.GetByDeviceAndSlot(ctx, device.ID, req.SlotPosition)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, nil, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
if existingBinding != nil {
|
||||
appErr := errors.New(errors.CodeConflict, "该插槽已有绑定的卡")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied,
|
||||
device, nil, nil, metadata, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
@@ -186,88 +111,30 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeIotCardNotFound)
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, nil, metadata, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, nil, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
item := deviceBindingAuditItem{
|
||||
Card: card, CardRole: constants.AuditResourceRoleDeviceBindingTargetCard,
|
||||
CardBefore: map[string]any{"device_id": nil, "slot_position": nil},
|
||||
CardAfter: map[string]any{"device_id": device.ID, "slot_position": req.SlotPosition},
|
||||
}
|
||||
|
||||
activeBinding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, card.ID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, []deviceBindingAuditItem{item}, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
if activeBinding != nil {
|
||||
appErr := errors.New(errors.CodeIotCardBoundToDevice, "该卡已绑定到其他设备")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{
|
||||
"device": deviceSnapshot(device),
|
||||
"card": map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"status": card.Status,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"iot_card_id": req.IotCardID,
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡被拒绝", constants.AuditResultDenied,
|
||||
device, nil, []deviceBindingAuditItem{item}, metadata, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
@@ -278,29 +145,26 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
|
||||
BindStatus: 1,
|
||||
}
|
||||
|
||||
if err := s.deviceSimBindingStore.Create(ctx, binding); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := postgres.NewDeviceSimBindingStore(tx, nil).Create(ctx, binding); err != nil {
|
||||
return err
|
||||
}
|
||||
item.Binding = binding
|
||||
item.BindingRole = constants.AuditResourceRoleDeviceCreatedBinding
|
||||
item.BindingAfter = bindingStateData(binding, constants.BindStatusBound, false)
|
||||
return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCardBound, "设备绑定 IoT 卡", constants.AuditResultSuccess,
|
||||
device,
|
||||
map[string]any{
|
||||
"device": deviceSnapshot(device),
|
||||
"card": map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"status": card.Status,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"slot_position": req.SlotPosition,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
map[string]any{"slot_position": req.SlotPosition, "iot_card_id": nil},
|
||||
map[string]any{"slot_position": req.SlotPosition, "iot_card_id": card.ID},
|
||||
[]deviceBindingAuditItem{item}, metadata, nil)
|
||||
})
|
||||
if err != nil {
|
||||
result := constants.AuditResultFailed
|
||||
if appErr, ok := err.(*errors.AppError); ok && (appErr.Code == errors.CodeConflict || appErr.Code == errors.CodeIotCardBoundToDevice) {
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardBound, "设备绑卡失败", result,
|
||||
device, nil, []deviceBindingAuditItem{{Card: card, CardRole: constants.AuditResourceRoleDeviceBindingTargetCard}}, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -313,32 +177,6 @@ func (s *Service) BindCard(ctx context.Context, deviceID uint, req *dto.BindCard
|
||||
)
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceBindCard,
|
||||
"设备绑卡",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{
|
||||
"device": deviceSnapshot(device),
|
||||
"card": map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"status": card.Status,
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"binding_id": binding.ID,
|
||||
"slot_position": req.SlotPosition,
|
||||
"iot_card_id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
},
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
|
||||
return &dto.BindCardToDeviceResponse{
|
||||
BindingID: binding.ID,
|
||||
Message: "绑定成功",
|
||||
@@ -349,111 +187,54 @@ func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeNotFound, "设备不存在")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"device_id": deviceID,
|
||||
"iot_card_id": cardID,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return nil, appErr
|
||||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"device_id": deviceID,
|
||||
"iot_card_id": cardID,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
metadata := map[string]any{"iot_card_id": cardID}
|
||||
|
||||
binding, err := s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, cardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeNotFound, "该卡未绑定到此设备")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"iot_card_id": cardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡被拒绝", constants.AuditResultDenied,
|
||||
device, nil, nil, metadata, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"iot_card_id": cardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, nil, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cardAudit map[string]any
|
||||
if card, cardErr := s.iotCardStore.GetByID(ctx, binding.IotCardID); cardErr == nil {
|
||||
cardAudit = map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"status": card.Status,
|
||||
card, cardErr := s.iotCardStore.GetByID(ctx, binding.IotCardID)
|
||||
if cardErr != nil {
|
||||
card = &model.IotCard{}
|
||||
card.ID = binding.IotCardID
|
||||
}
|
||||
item := deviceBindingAuditItem{
|
||||
Card: card, Binding: binding,
|
||||
CardRole: constants.AuditResourceRoleDeviceBindingTargetCard, BindingRole: constants.AuditResourceRoleDeviceRemovedBinding,
|
||||
CardBefore: map[string]any{"device_id": device.ID, "slot_position": binding.SlotPosition},
|
||||
CardAfter: map[string]any{"device_id": nil, "slot_position": nil},
|
||||
BindingBefore: bindingStateData(binding, constants.BindStatusBound, binding.IsCurrent),
|
||||
BindingAfter: bindingStateData(binding, constants.BindStatusUnbound, false),
|
||||
}
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := postgres.NewDeviceSimBindingStore(tx, nil).Unbind(ctx, binding.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
beforeAuditData := map[string]any{
|
||||
"device": deviceSnapshot(device),
|
||||
"binding_id": binding.ID,
|
||||
"iot_card_id": binding.IotCardID,
|
||||
}
|
||||
if cardAudit != nil {
|
||||
beforeAuditData["card"] = cardAudit
|
||||
}
|
||||
|
||||
if err := s.deviceSimBindingStore.Unbind(ctx, binding.ID); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}).Where("id = ?", binding.ID).Update("is_current", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCardUnbound, "设备解绑 IoT 卡", constants.AuditResultSuccess,
|
||||
device,
|
||||
beforeAuditData,
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
map[string]any{"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID},
|
||||
map[string]any{"slot_position": binding.SlotPosition, "iot_card_id": nil},
|
||||
[]deviceBindingAuditItem{item}, metadata, nil)
|
||||
})
|
||||
if err != nil {
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCardUnbound, "设备解绑卡失败", constants.AuditResultFailed,
|
||||
device, nil, []deviceBindingAuditItem{item}, metadata, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -466,28 +247,6 @@ func (s *Service) UnbindCard(ctx context.Context, deviceID uint, cardID uint) (*
|
||||
)
|
||||
}
|
||||
|
||||
afterAuditData := map[string]any{
|
||||
"iot_card_id": cardID,
|
||||
"unbind": true,
|
||||
}
|
||||
if cardAudit != nil {
|
||||
afterAuditData["card"] = cardAudit
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceUnbindCard,
|
||||
"设备解绑卡",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
beforeAuditData,
|
||||
afterAuditData,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
|
||||
return &dto.UnbindCardFromDeviceResponse{
|
||||
Message: "解绑成功",
|
||||
}, nil
|
||||
|
||||
236
internal/service/device/binding_audit.go
Normal file
236
internal/service/device/binding_audit.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"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/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
type deviceBindingAuditItem struct {
|
||||
Card *model.IotCard
|
||||
Binding *model.DeviceSimBinding
|
||||
CardRole string
|
||||
BindingRole string
|
||||
CardBefore map[string]any
|
||||
CardAfter map[string]any
|
||||
BindingBefore map[string]any
|
||||
BindingAfter map[string]any
|
||||
}
|
||||
|
||||
type deviceBindingState struct {
|
||||
bindings []*model.DeviceSimBinding
|
||||
cards map[uint]*model.IotCard
|
||||
target *model.DeviceSimBinding
|
||||
current *model.DeviceSimBinding
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceBindingAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
device *model.Device,
|
||||
deviceBefore, deviceAfter map[string]any,
|
||||
items []deviceBindingAuditItem,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备卡槽统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
deviceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &deviceID,
|
||||
Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(device), BeforeData: deviceBefore, AfterData: deviceAfter,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
for index, item := range items {
|
||||
if item.Card != nil && item.Card.ID > 0 {
|
||||
cardID := strconv.FormatUint(uint64(item.Card.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID,
|
||||
Key: audit.IotCardResourceKey(item.Card), DisplayName: item.Card.ICCID,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: item.CardRole,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(item.Card), BeforeData: item.CardBefore, AfterData: item.CardAfter,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary, SortOrder: index*2 + 1,
|
||||
})
|
||||
}
|
||||
if item.Binding != nil && item.Binding.ID > 0 {
|
||||
bindingID := strconv.FormatUint(uint64(item.Binding.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
||||
Key: bindingID, DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: item.BindingRole,
|
||||
IdentitySnapshot: deviceBindingIdentity(device, item.Card, item.Binding),
|
||||
BeforeData: item.BindingBefore, AfterData: item.BindingAfter,
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly, SortOrder: index*2 + 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Metadata: metadata, Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceBindingAuditFailure(
|
||||
ctx context.Context,
|
||||
actionCode, summary, result string,
|
||||
device *model.Device,
|
||||
deviceBefore map[string]any,
|
||||
items []deviceBindingAuditItem,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
deviceID := uint(0)
|
||||
if device != nil {
|
||||
deviceID = device.ID
|
||||
}
|
||||
if s.db == nil || s.auditWriter == nil || deviceID == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, deviceID, businessErr, errors.New(errors.CodeInvalidStatus, "设备卡槽统一审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceBindingAudit(ctx, tx, actionCode, summary, result, device, deviceBefore, nil, items, metadata, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, deviceID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func deviceBindingIdentity(device *model.Device, card *model.IotCard, binding *model.DeviceSimBinding) map[string]any {
|
||||
identity := map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "slot_position": binding.SlotPosition,
|
||||
"iot_card_id": binding.IotCardID, "is_current": binding.IsCurrent,
|
||||
}
|
||||
if device != nil {
|
||||
identity["device_virtual_no"] = device.VirtualNo
|
||||
}
|
||||
if card != nil {
|
||||
identity["iccid"] = card.ICCID
|
||||
identity["virtual_no"] = card.VirtualNo
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
func bindingStateData(binding *model.DeviceSimBinding, bindStatus int, isCurrent bool) map[string]any {
|
||||
return map[string]any{
|
||||
"slot_position": binding.SlotPosition,
|
||||
"bind_status": bindStatus,
|
||||
"is_current": isCurrent,
|
||||
}
|
||||
}
|
||||
|
||||
func loadDeviceBindingState(ctx context.Context, db *gorm.DB, deviceID uint, targetICCID string, lock bool) (*deviceBindingState, error) {
|
||||
query := db.WithContext(ctx).Where("device_id = ? AND bind_status = ?", deviceID, constants.BindStatusBound).Order("slot_position ASC")
|
||||
if lock {
|
||||
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
}
|
||||
state := &deviceBindingState{cards: make(map[uint]*model.IotCard)}
|
||||
if err := query.Find(&state.bindings).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备卡槽关系失败")
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(state.bindings))
|
||||
for _, binding := range state.bindings {
|
||||
cardIDs = append(cardIDs, binding.IotCardID)
|
||||
if binding.IsCurrent {
|
||||
state.current = binding
|
||||
}
|
||||
}
|
||||
if len(cardIDs) > 0 {
|
||||
var cards []*model.IotCard
|
||||
if err := db.WithContext(ctx).Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询设备绑定卡失败")
|
||||
}
|
||||
for _, card := range cards {
|
||||
state.cards[card.ID] = card
|
||||
}
|
||||
}
|
||||
targetICCID = strings.TrimSpace(targetICCID)
|
||||
for _, binding := range state.bindings {
|
||||
if card := state.cards[binding.IotCardID]; card != nil && cardMatchesICCID(card, targetICCID) {
|
||||
state.target = binding
|
||||
break
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func switchCardAuditItems(state *deviceBindingState) []deviceBindingAuditItem {
|
||||
items := make([]deviceBindingAuditItem, 0, 2)
|
||||
if state.current != nil {
|
||||
oldCurrentAfter := false
|
||||
if state.target != nil && state.current.ID == state.target.ID {
|
||||
oldCurrentAfter = true
|
||||
}
|
||||
items = append(items, deviceBindingAuditItem{
|
||||
Card: state.cards[state.current.IotCardID], Binding: state.current,
|
||||
CardRole: constants.AuditResourceRoleDeviceOldCurrentCard, BindingRole: constants.AuditResourceRoleDeviceOldCurrentBinding,
|
||||
CardBefore: map[string]any{"is_current": true}, CardAfter: map[string]any{"is_current": oldCurrentAfter},
|
||||
BindingBefore: bindingStateData(state.current, constants.BindStatusBound, true),
|
||||
BindingAfter: bindingStateData(state.current, constants.BindStatusBound, oldCurrentAfter),
|
||||
})
|
||||
}
|
||||
if state.target != nil {
|
||||
wasCurrent := state.target.IsCurrent
|
||||
items = append(items, deviceBindingAuditItem{
|
||||
Card: state.cards[state.target.IotCardID], Binding: state.target,
|
||||
CardRole: constants.AuditResourceRoleDeviceNewCurrentCard, BindingRole: constants.AuditResourceRoleDeviceNewCurrentBinding,
|
||||
CardBefore: map[string]any{"is_current": wasCurrent}, CardAfter: map[string]any{"is_current": true},
|
||||
BindingBefore: bindingStateData(state.target, constants.BindStatusBound, wasCurrent),
|
||||
BindingAfter: bindingStateData(state.target, constants.BindStatusBound, true),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func currentCardID(state *deviceBindingState) uint {
|
||||
if state == nil || state.current == nil {
|
||||
return 0
|
||||
}
|
||||
return state.current.IotCardID
|
||||
}
|
||||
|
||||
func loadDeviceUnbindAuditReferences(ctx context.Context, tx *gorm.DB, device *model.Device) ([]audit.ResourceInput, error) {
|
||||
referencesByDevice, _, err := loadDeviceCardAuditReferences(ctx, tx, []*model.Device{device}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
references := referencesByDevice[device.ID]
|
||||
for index := range references {
|
||||
resource := &references[index]
|
||||
resource.Relation = constants.AuditResourceRelationAffected
|
||||
switch resource.Type {
|
||||
case constants.AuditResourceIotCard:
|
||||
resource.Role = constants.AuditResourceRoleDeviceBindingTargetCard
|
||||
resource.BeforeData = map[string]any{"device_id": device.ID}
|
||||
resource.AfterData = map[string]any{"device_id": nil}
|
||||
resource.SubjectVisibility = constants.AuditSubjectResult
|
||||
resource.SubjectSummary = "设备删除并解绑 IoT 卡"
|
||||
case constants.AuditResourceDeviceSIMBinding:
|
||||
resource.Role = constants.AuditResourceRoleDeviceRemovedBinding
|
||||
resource.BeforeData = map[string]any{
|
||||
"slot_position": resource.IdentitySnapshot["slot_position"],
|
||||
"bind_status": constants.BindStatusBound,
|
||||
"is_current": resource.IdentitySnapshot["is_current"],
|
||||
}
|
||||
resource.AfterData = map[string]any{
|
||||
"slot_position": resource.IdentitySnapshot["slot_position"],
|
||||
"bind_status": constants.BindStatusUnbound,
|
||||
"is_current": false,
|
||||
}
|
||||
}
|
||||
}
|
||||
return references, nil
|
||||
}
|
||||
294
internal/service/device/gateway_audit.go
Normal file
294
internal/service/device/gateway_audit.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
|
||||
"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/pkg/auditcontext"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
type deviceGatewayIntegrationLog interface {
|
||||
Start(ctx context.Context, input integrationlog.Attempt) (*model.IntegrationLog, error)
|
||||
Complete(ctx context.Context, integrationID string, completion integrationlog.Completion) (*model.IntegrationLog, error)
|
||||
}
|
||||
|
||||
// SetGatewayIntegrationLog 注入设备外部命令的 Integration Log 接缝。
|
||||
func (s *Service) SetGatewayIntegrationLog(integration deviceGatewayIntegrationLog) {
|
||||
s.gatewayIntegration = integration
|
||||
}
|
||||
|
||||
type deviceGatewayResource struct {
|
||||
Type string
|
||||
ID string
|
||||
Key string
|
||||
ExternalID string
|
||||
RequestSummary map[string]any
|
||||
}
|
||||
|
||||
type deviceGatewayAttempt struct {
|
||||
log *model.IntegrationLog
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
type deviceGatewayAttemptObserver struct {
|
||||
service *Service
|
||||
operation string
|
||||
scene string
|
||||
seriesKey string
|
||||
resource deviceGatewayResource
|
||||
current *deviceGatewayAttempt
|
||||
successful *deviceGatewayAttempt
|
||||
integration string
|
||||
unknown bool
|
||||
}
|
||||
|
||||
func (o *deviceGatewayAttemptObserver) BeforeAttempt(ctx context.Context, attempt int) error {
|
||||
started, err := o.service.startDeviceGatewayAttempt(ctx, o.operation, o.scene, o.seriesKey, attempt, o.resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.current = started
|
||||
o.integration = started.log.IntegrationID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *deviceGatewayAttemptObserver) AfterAttempt(ctx context.Context, _ int, callErr error) error {
|
||||
if isDeviceGatewayTimeout(callErr) {
|
||||
o.unknown = true
|
||||
}
|
||||
if callErr == nil {
|
||||
o.successful = o.current
|
||||
o.current = nil
|
||||
return nil
|
||||
}
|
||||
err := o.service.completeDeviceGatewayAttempt(ctx, o.current, callErr, false)
|
||||
o.current = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *deviceGatewayAttemptObserver) completeSuccess(ctx context.Context, stateChanged bool) error {
|
||||
return o.service.completeDeviceGatewayAttempt(ctx, o.successful, nil, stateChanged)
|
||||
}
|
||||
|
||||
func (s *Service) startDeviceGatewayAttempt(
|
||||
ctx context.Context,
|
||||
operation, scene, seriesKey string,
|
||||
attempt int,
|
||||
resource deviceGatewayResource,
|
||||
) (*deviceGatewayAttempt, error) {
|
||||
if s == nil || s.gatewayIntegration == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "设备 Gateway Integration Log 接缝未配置")
|
||||
}
|
||||
linkage := auditcontext.From(ctx)
|
||||
triggerSource := linkage.Source
|
||||
if triggerSource == "" {
|
||||
triggerSource = "service"
|
||||
}
|
||||
triggerSeries := uuid.NewSHA1(uuid.NameSpaceOID, []byte("gateway-device-command:"+seriesKey+":"+operation+":"+resource.Type+":"+resource.ID)).String()
|
||||
var requestID, correlationID *string
|
||||
if linkage.RequestID != "" {
|
||||
requestID = &linkage.RequestID
|
||||
}
|
||||
if linkage.CorrelationID != "" {
|
||||
correlationID = &linkage.CorrelationID
|
||||
} else {
|
||||
correlationID = requestID
|
||||
}
|
||||
log, err := s.gatewayIntegration.Start(ctx, integrationlog.Attempt{
|
||||
Provider: constants.IntegrationProviderGateway, Direction: constants.IntegrationDirectionOutbound,
|
||||
Operation: operation, ExternalID: &resource.ExternalID,
|
||||
ResourceType: resource.Type, ResourceID: &resource.ID, ResourceKey: &resource.Key,
|
||||
TriggerSource: &triggerSource, TriggerScene: &scene, TriggerSeries: &triggerSeries,
|
||||
Attempt: attempt, RequestID: requestID, CorrelationID: correlationID,
|
||||
RequestSummary: resource.RequestSummary,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &deviceGatewayAttempt{log: log, startedAt: time.Now()}, nil
|
||||
}
|
||||
|
||||
func (s *Service) completeDeviceGatewayAttempt(ctx context.Context, attempt *deviceGatewayAttempt, callErr error, stateChanged bool) error {
|
||||
if attempt == nil || attempt.log == nil {
|
||||
return nil
|
||||
}
|
||||
completion := integrationlog.Completion{
|
||||
Result: constants.IntegrationResultSuccess, DurationMS: time.Since(attempt.startedAt).Milliseconds(),
|
||||
StateChanged: stateChanged, ResponseSummary: map[string]any{"result": "success"},
|
||||
}
|
||||
if callErr != nil {
|
||||
completion.Result = constants.IntegrationResultFailed
|
||||
completion.SafeProviderMessage = "Gateway 设备命令失败"
|
||||
completion.ResponseSummary = map[string]any{"result": "failed"}
|
||||
if isDeviceGatewayTimeout(callErr) {
|
||||
completion.Result = constants.IntegrationResultUnknown
|
||||
completion.SafeProviderMessage = "Gateway 设备命令结果未知"
|
||||
completion.ResponseSummary = map[string]any{"result": "unknown"}
|
||||
completion.RecoveryStrategy = constants.GatewayDeviceCommandUnknownRecoveryStrategy
|
||||
}
|
||||
}
|
||||
_, err := s.gatewayIntegration.Complete(ctx, attempt.log.IntegrationID, completion)
|
||||
return err
|
||||
}
|
||||
|
||||
type deviceGatewayCommand struct {
|
||||
ActionCode string
|
||||
Summary string
|
||||
Operation string
|
||||
Scene string
|
||||
RequestSummary map[string]any
|
||||
Metadata map[string]any
|
||||
TargetCard *model.IotCard
|
||||
Call func(context.Context) error
|
||||
}
|
||||
|
||||
func (s *Service) executeDeviceGatewayCommand(ctx context.Context, device *model.Device, command deviceGatewayCommand) error {
|
||||
deviceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
seriesKey := deviceCommandSeriesKey(ctx)
|
||||
observer := &deviceGatewayAttemptObserver{
|
||||
service: s, operation: command.Operation, scene: command.Scene, seriesKey: seriesKey,
|
||||
resource: deviceGatewayResource{
|
||||
Type: constants.AuditResourceDevice, ID: deviceID, Key: audit.DeviceResourceKey(device),
|
||||
ExternalID: device.IMEI, RequestSummary: command.RequestSummary,
|
||||
},
|
||||
}
|
||||
callErr := command.Call(gateway.WithAttemptObserver(ctx, observer))
|
||||
metadata := cloneDeviceCommandMetadata(command.Metadata)
|
||||
metadata["integration_id"] = observer.integration
|
||||
if callErr != nil {
|
||||
result := constants.AuditResultFailed
|
||||
summary := command.Summary + "失败"
|
||||
if observer.unknown {
|
||||
result = constants.AuditResultUnknown
|
||||
summary = command.Summary + "结果未知"
|
||||
}
|
||||
s.recordDeviceCommandAudit(ctx, command.ActionCode, summary, result, device, command.TargetCard, nil, nil, metadata, callErr)
|
||||
return callErr
|
||||
}
|
||||
if err := observer.completeSuccess(ctx, false); err != nil {
|
||||
s.recordDeviceCommandAudit(ctx, command.ActionCode, command.Summary+"结果未知", constants.AuditResultUnknown,
|
||||
device, command.TargetCard, nil, nil, metadata, err)
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "终结设备 Gateway Integration Log 失败")
|
||||
}
|
||||
s.recordDeviceCommandAudit(ctx, command.ActionCode, command.Summary, constants.AuditResultSuccess,
|
||||
device, command.TargetCard, nil, nil, metadata, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneDeviceCommandMetadata(source map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(source)+1)
|
||||
for key, value := range source {
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func deviceCommandSeriesKey(ctx context.Context) string {
|
||||
linkage := auditcontext.From(ctx)
|
||||
if linkage.CorrelationID != "" {
|
||||
return linkage.CorrelationID
|
||||
}
|
||||
if linkage.RequestID != "" {
|
||||
return linkage.RequestID
|
||||
}
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
func isDeviceGatewayTimeout(err error) bool {
|
||||
var appErr *errors.AppError
|
||||
return stderrors.As(err, &appErr) && appErr != nil && appErr.Code == errors.CodeGatewayTimeout
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceCommandAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
device *model.Device,
|
||||
targetCard *model.IotCard,
|
||||
cardBefore, cardAfter, metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备命令统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
cardReferences, _, err := loadDeviceCardAuditReferences(ctx, tx, []*model.Device{device}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetCard != nil && targetCard.ID > 0 {
|
||||
targetID := strconv.FormatUint(uint64(targetCard.ID), 10)
|
||||
found := false
|
||||
for i := range cardReferences[device.ID] {
|
||||
resource := &cardReferences[device.ID][i]
|
||||
if resource.Type == constants.AuditResourceIotCard && resource.ID != nil && *resource.ID == targetID {
|
||||
found = true
|
||||
if cardBefore != nil || cardAfter != nil {
|
||||
resource.Relation = constants.AuditResourceRelationAffected
|
||||
resource.BeforeData = cardBefore
|
||||
resource.AfterData = cardAfter
|
||||
resource.SubjectVisibility = constants.AuditSubjectResult
|
||||
resource.SubjectSummary = summary
|
||||
} else {
|
||||
resource.Role = constants.AuditResourceRoleDeviceCommandTargetCard
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cardReferences[device.ID] = append(cardReferences[device.ID], audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &targetID,
|
||||
Key: audit.IotCardResourceKey(targetCard), DisplayName: targetCard.ICCID,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleDeviceCommandTargetCard,
|
||||
IdentitySnapshot: audit.IotCardIdentitySnapshot(targetCard),
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
}
|
||||
deviceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &deviceID,
|
||||
Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(device),
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
resources = append(resources, cardReferences[device.ID]...)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Metadata: metadata, Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceCommandAudit(
|
||||
ctx context.Context,
|
||||
actionCode, summary, result string,
|
||||
device *model.Device,
|
||||
targetCard *model.IotCard,
|
||||
cardBefore, cardAfter, metadata map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, 0, businessErr, errors.New(errors.CodeInvalidStatus, "设备命令统一审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceCommandAudit(ctx, tx, actionCode, summary, result, device, targetCard, cardBefore, cardAfter, metadata, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, device.ID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var _ deviceGatewayIntegrationLog = (*integrationlog.Repository)(nil)
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"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/pkg/constants"
|
||||
@@ -51,73 +52,31 @@ func (s *Service) GatewayGetSlotInfo(ctx context.Context, identifier string) (*g
|
||||
func (s *Service) GatewaySetWiFi(ctx context.Context, identifier string, req *dto.SetWiFiRequest) error {
|
||||
device, imei, err := s.getGatewayDevice(ctx, identifier)
|
||||
if err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSetWiFi,
|
||||
"设备设置WiFi失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"identifier": identifier,
|
||||
"ssid": req.SSID,
|
||||
"enabled": req.Enabled,
|
||||
"password": req.Password,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "")
|
||||
if err = s.gatewayClient.SetWiFi(ctx, &gateway.WiFiReq{
|
||||
CardNo: imei,
|
||||
Params: gateway.WiFiParams{
|
||||
SSIDName: req.SSID,
|
||||
SSIDPassword: req.Password,
|
||||
err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{
|
||||
ActionCode: constants.AuditActionDeviceWiFiSet,
|
||||
Summary: "设置设备 Wi-Fi",
|
||||
Operation: constants.IntegrationOperationGatewaySetWiFi,
|
||||
Scene: constants.CardObservationSceneDeviceSetWiFi,
|
||||
RequestSummary: map[string]any{
|
||||
"device_id": device.ID, "imei": imei, "ssid": req.SSID,
|
||||
"enabled_requested": req.Enabled, "credentials_configured": req.Password != "",
|
||||
},
|
||||
}); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSetWiFi,
|
||||
"设备设置WiFi失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"imei": imei,
|
||||
"ssid": req.SSID,
|
||||
"enabled": req.Enabled,
|
||||
"password": req.Password,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
Metadata: map[string]any{
|
||||
"ssid": req.SSID, "enabled_requested": req.Enabled, "credentials_configured": req.Password != "",
|
||||
},
|
||||
Call: func(callCtx context.Context) error {
|
||||
return s.gatewayClient.SetWiFi(callCtx, &gateway.WiFiReq{
|
||||
CardNo: imei,
|
||||
Params: gateway.WiFiParams{SSIDName: req.SSID, SSIDPassword: req.Password},
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSetWiFi,
|
||||
"设备设置WiFi",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"imei": imei,
|
||||
"ssid": req.SSID,
|
||||
"enabled": req.Enabled,
|
||||
"password": req.Password,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSetWiFi, observation, false)
|
||||
return nil
|
||||
}
|
||||
@@ -126,58 +85,92 @@ func (s *Service) GatewaySetWiFi(ctx context.Context, identifier string, req *dt
|
||||
func (s *Service) GatewaySwitchCard(ctx context.Context, identifier string, req *dto.SwitchCardRequest) error {
|
||||
device, imei, err := s.getGatewayDevice(ctx, identifier)
|
||||
if err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchCard,
|
||||
"设备切卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"identifier": identifier,
|
||||
"target_iccid": req.TargetICCID,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
state, err := loadDeviceBindingState(ctx, s.db, device.ID, req.TargetICCID, false)
|
||||
metadata := map[string]any{"target_iccid": req.TargetICCID}
|
||||
if err != nil {
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡失败", constants.AuditResultFailed,
|
||||
device, nil, nil, metadata, err)
|
||||
return err
|
||||
}
|
||||
if state.target == nil {
|
||||
appErr := errors.New(errors.CodeForbidden, "目标卡未绑定到当前设备")
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡被拒绝", constants.AuditResultDenied,
|
||||
device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr)
|
||||
return appErr
|
||||
}
|
||||
targetCard := state.cards[state.target.IotCardID]
|
||||
if targetCard == nil {
|
||||
appErr := errors.New(errors.CodeNotFound, "目标卡资产不存在或无权限访问")
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡失败", constants.AuditResultFailed,
|
||||
device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr)
|
||||
return appErr
|
||||
}
|
||||
metadata["target_iot_card_id"] = targetCard.ID
|
||||
metadata["target_slot_position"] = state.target.SlotPosition
|
||||
observation := s.captureDeviceControlObservation(ctx, device.ID, 0, req.TargetICCID)
|
||||
if err = s.gatewayClient.SwitchCard(ctx, &gateway.SwitchCardReq{
|
||||
deviceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
observer := &deviceGatewayAttemptObserver{
|
||||
service: s, operation: constants.IntegrationOperationGatewaySwitchCard,
|
||||
scene: constants.CardObservationSceneDeviceSwitchCard, seriesKey: deviceCommandSeriesKey(ctx),
|
||||
resource: deviceGatewayResource{
|
||||
Type: constants.AuditResourceDevice, ID: deviceID, Key: audit.DeviceResourceKey(device), ExternalID: imei,
|
||||
RequestSummary: map[string]any{
|
||||
"device_id": device.ID, "imei": imei, "target_iot_card_id": targetCard.ID,
|
||||
"target_iccid": targetCard.ICCID, "target_slot_position": state.target.SlotPosition,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err = s.gatewayClient.SwitchCard(gateway.WithAttemptObserver(ctx, observer), &gateway.SwitchCardReq{
|
||||
CardNo: imei,
|
||||
ICCID: req.TargetICCID,
|
||||
}); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchCard,
|
||||
"设备切卡失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"target_iccid": req.TargetICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
result, summary := constants.AuditResultFailed, "设备切卡失败"
|
||||
if observer.unknown {
|
||||
result, summary = constants.AuditResultUnknown, "设备切卡结果未知"
|
||||
}
|
||||
metadata["integration_id"] = observer.integration
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, summary, result,
|
||||
device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, err)
|
||||
return err
|
||||
}
|
||||
metadata["integration_id"] = observer.integration
|
||||
if err := observer.completeSuccess(ctx, false); err != nil {
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "终结设备切卡 Integration Log 失败")
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡结果未知", constants.AuditResultUnknown,
|
||||
device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, appErr)
|
||||
return appErr
|
||||
}
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedState, err := loadDeviceBindingState(ctx, tx, device.ID, targetCard.ICCID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lockedState.target == nil {
|
||||
return errors.New(errors.CodeConflict, "切卡期间目标卡绑定关系已变化")
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}).
|
||||
Where("device_id = ? AND bind_status = ?", device.ID, constants.BindStatusBound).
|
||||
Update("is_current", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&model.DeviceSimBinding{}).
|
||||
Where("id = ? AND bind_status = ?", lockedState.target.ID, constants.BindStatusBound).
|
||||
Update("is_current", true).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.appendDeviceBindingAudit(ctx, tx, constants.AuditActionDeviceCurrentCardSwitched, "切换设备当前卡", constants.AuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"current_iot_card_id": currentCardID(lockedState)},
|
||||
map[string]any{"current_iot_card_id": lockedState.target.IotCardID},
|
||||
switchCardAuditItems(lockedState), metadata, nil)
|
||||
})
|
||||
if err != nil {
|
||||
s.recordDeviceBindingAuditFailure(ctx, constants.AuditActionDeviceCurrentCardSwitched, "设备切卡结果未知", constants.AuditResultUnknown,
|
||||
device, map[string]any{"current_iot_card_id": currentCardID(state)}, switchCardAuditItems(state), metadata, err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchCard,
|
||||
"设备切卡",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"target_iccid": req.TargetICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSwitchCard, observation, true)
|
||||
return nil
|
||||
}
|
||||
@@ -186,54 +179,21 @@ func (s *Service) GatewaySwitchCard(ctx context.Context, identifier string, req
|
||||
func (s *Service) GatewayRebootDevice(ctx context.Context, identifier string) error {
|
||||
device, imei, err := s.getGatewayDevice(ctx, identifier)
|
||||
if err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReboot,
|
||||
"设备重启失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{"identifier": identifier},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "")
|
||||
if err = s.gatewayClient.RebootDevice(ctx, &gateway.DeviceOperationReq{
|
||||
DeviceID: imei,
|
||||
}); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReboot,
|
||||
"设备重启失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{
|
||||
ActionCode: constants.AuditActionDeviceRebooted, Summary: "重启设备",
|
||||
Operation: constants.IntegrationOperationGatewayReboot, Scene: constants.CardObservationSceneDeviceReboot,
|
||||
RequestSummary: map[string]any{"device_id": device.ID, "imei": imei},
|
||||
Metadata: map[string]any{"requested_action": "reboot"},
|
||||
Call: func(callCtx context.Context) error {
|
||||
return s.gatewayClient.RebootDevice(callCtx, &gateway.DeviceOperationReq{DeviceID: imei})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReboot,
|
||||
"设备重启",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceReboot, observation, false)
|
||||
return nil
|
||||
}
|
||||
@@ -242,54 +202,21 @@ func (s *Service) GatewayRebootDevice(ctx context.Context, identifier string) er
|
||||
func (s *Service) GatewayResetDevice(ctx context.Context, identifier string) error {
|
||||
device, imei, err := s.getGatewayDevice(ctx, identifier)
|
||||
if err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReset,
|
||||
"设备恢复出厂失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{"identifier": identifier},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
observation := s.captureDeviceControlObservation(ctx, device.ID, 0, "")
|
||||
if err = s.gatewayClient.ResetDevice(ctx, &gateway.DeviceOperationReq{
|
||||
DeviceID: imei,
|
||||
}); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReset,
|
||||
"设备恢复出厂失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{
|
||||
ActionCode: constants.AuditActionDeviceReset, Summary: "恢复设备出厂设置",
|
||||
Operation: constants.IntegrationOperationGatewayReset, Scene: constants.CardObservationSceneDeviceReset,
|
||||
RequestSummary: map[string]any{"device_id": device.ID, "imei": imei},
|
||||
Metadata: map[string]any{"requested_action": "factory_reset"},
|
||||
Call: func(callCtx context.Context) error {
|
||||
return s.gatewayClient.ResetDevice(callCtx, &gateway.DeviceOperationReq{DeviceID: imei})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceReset,
|
||||
"设备恢复出厂",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
nil,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceReset, observation, false)
|
||||
return nil
|
||||
}
|
||||
@@ -303,244 +230,77 @@ func (s *Service) GatewaySwitchMode(ctx context.Context, identifier string, req
|
||||
|
||||
device, imei, err := s.getGatewayDevice(ctx, identifier)
|
||||
if err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{
|
||||
"identifier": identifier,
|
||||
"switch_mode": switchMode,
|
||||
"iot_card_id": req.IotCardID,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
return err
|
||||
}
|
||||
recordRejected := func(summary, result string, businessErr error, targetCard *model.IotCard) error {
|
||||
s.recordDeviceCommandAudit(ctx, constants.AuditActionDeviceSwitchModeSet, summary, result,
|
||||
device, targetCard, nil, nil,
|
||||
map[string]any{"requested_switch_mode": switchMode, "iot_card_id": req.IotCardID}, businessErr)
|
||||
return businessErr
|
||||
}
|
||||
if req.SwitchMode == nil {
|
||||
appErr := errors.New(errors.CodeInvalidParam, "切卡模式不能为空")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil)
|
||||
}
|
||||
if switchMode != 0 && switchMode != 1 {
|
||||
appErr := errors.New(errors.CodeInvalidParam, "切卡模式仅支持0或1")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil)
|
||||
}
|
||||
if req.IotCardID == 0 {
|
||||
appErr := errors.New(errors.CodeInvalidParam, "目标卡资产ID不能为空")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil)
|
||||
}
|
||||
|
||||
targetCard, err := s.iotCardStore.GetByID(ctx, req.IotCardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeNotFound, "目标卡资产不存在或无权限访问")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, nil)
|
||||
}
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询目标卡资产失败")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式失败", constants.AuditResultFailed, appErr, nil)
|
||||
}
|
||||
if _, err = s.deviceSimBindingStore.GetByDeviceAndCard(ctx, device.ID, targetCard.ID); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
appErr := errors.New(errors.CodeForbidden, "目标卡未绑定到当前设备,禁止设置切卡模式")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard)
|
||||
}
|
||||
appErr := errors.Wrap(errors.CodeDatabaseError, err, "查询设备卡绑定关系失败")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式失败", constants.AuditResultFailed, appErr, targetCard)
|
||||
}
|
||||
if targetCard.ICCID == "" {
|
||||
appErr := errors.New(errors.CodeConflict, "目标卡资产缺少ICCID,无法设置切卡模式")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard)
|
||||
}
|
||||
if targetCard.NetworkStatus != constants.NetworkStatusOnline {
|
||||
appErr := errors.New(errors.CodeForbidden, "目标卡状态异常,仅正常状态的卡允许设置切卡模式")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"switch_mode": switchMode,
|
||||
"iot_card_id": req.IotCardID,
|
||||
"iccid": targetCard.ICCID,
|
||||
"network_status": targetCard.NetworkStatus,
|
||||
"real_name_status": targetCard.RealNameStatus,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard)
|
||||
}
|
||||
if targetCard.RealNameStatus != constants.RealNameStatusVerified {
|
||||
appErr := errors.New(errors.CodeForbidden, "目标卡未实名,禁止设置切卡模式")
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换被拒绝",
|
||||
constants.AssetAuditResultDenied,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{
|
||||
"switch_mode": switchMode,
|
||||
"iot_card_id": req.IotCardID,
|
||||
"iccid": targetCard.ICCID,
|
||||
"network_status": targetCard.NetworkStatus,
|
||||
"real_name_status": targetCard.RealNameStatus,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
appErr,
|
||||
)
|
||||
return appErr
|
||||
return recordRejected("设置设备切卡模式被拒绝", constants.AuditResultDenied, appErr, targetCard)
|
||||
}
|
||||
observation := s.captureDeviceControlObservation(ctx, device.ID, targetCard.ID, targetCard.ICCID)
|
||||
if err = s.gatewayClient.SwitchMode(ctx, &gateway.SwitchModeReq{
|
||||
CardNo: imei,
|
||||
SwitchMode: strconv.Itoa(switchMode),
|
||||
ICCID: targetCard.ICCID,
|
||||
}); err != nil {
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换失败",
|
||||
constants.AssetAuditResultFailed,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
err,
|
||||
)
|
||||
err = s.executeDeviceGatewayCommand(ctx, device, deviceGatewayCommand{
|
||||
ActionCode: constants.AuditActionDeviceSwitchModeSet, Summary: "设置设备切卡模式",
|
||||
Operation: constants.IntegrationOperationGatewaySwitchMode, Scene: constants.CardObservationSceneDeviceSwitchMode,
|
||||
RequestSummary: map[string]any{
|
||||
"device_id": device.ID, "imei": imei, "switch_mode": switchMode,
|
||||
"iot_card_id": targetCard.ID, "iccid": targetCard.ICCID,
|
||||
},
|
||||
Metadata: map[string]any{
|
||||
"requested_switch_mode": switchMode, "iot_card_id": targetCard.ID, "iccid": targetCard.ICCID,
|
||||
},
|
||||
TargetCard: targetCard,
|
||||
Call: func(callCtx context.Context) error {
|
||||
return s.gatewayClient.SwitchMode(callCtx, &gateway.SwitchModeReq{
|
||||
CardNo: imei, SwitchMode: strconv.Itoa(switchMode), ICCID: targetCard.ICCID,
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpDeviceSwitchMode,
|
||||
"设备切卡模式切换",
|
||||
constants.AssetAuditResultSuccess,
|
||||
device,
|
||||
map[string]any{"device": deviceSnapshot(device)},
|
||||
map[string]any{"switch_mode": switchMode, "iot_card_id": req.IotCardID, "iccid": targetCard.ICCID},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
s.dispatchDeviceControlObservation(ctx, device.ID, constants.CardObservationSceneDeviceSwitchMode, observation, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ func (s *Service) BatchUpdateRealnamePolicy(ctx context.Context, req *dto.BatchU
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var devices []*model.Device
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var devices []model.Device
|
||||
query := middleware.ApplyShopFilter(ctx, tx.Model(&model.Device{})).Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
if err := query.Where("id IN ?", ids).Find(&devices).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询批量设备资产失败")
|
||||
@@ -30,31 +30,31 @@ func (s *Service) BatchUpdateRealnamePolicy(ctx context.Context, req *dto.BatchU
|
||||
if len(devices) != len(ids) {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
result := tx.Model(&model.Device{}).Where("id IN ?", ids).Update("realname_policy", req.RealnamePolicy)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量更新设备实名认证策略失败")
|
||||
changedIDs := make([]uint, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device != nil && device.RealnamePolicy != req.RealnamePolicy {
|
||||
changedIDs = append(changedIDs, device.ID)
|
||||
}
|
||||
}
|
||||
if result.RowsAffected != int64(len(ids)) {
|
||||
return errors.New(errors.CodeConflict, "设备资产状态已变化,请刷新后重试")
|
||||
if len(changedIDs) > 0 {
|
||||
result := tx.Model(&model.Device{}).Where("id IN ?", changedIDs).Update("realname_policy", req.RealnamePolicy)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量更新设备实名认证策略失败")
|
||||
}
|
||||
if result.RowsAffected != int64(len(changedIDs)) {
|
||||
return errors.New(errors.CodeConflict, "设备资产状态已变化,请刷新后重试")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return s.appendDeviceRealnamePolicyBatchAudit(ctx, tx, devices, req.RealnamePolicy)
|
||||
})
|
||||
if err != nil {
|
||||
result := constants.AuditResultFailed
|
||||
if appErr, ok := err.(*errors.AppError); ok && appErr.Code == errors.CodeForbidden {
|
||||
result = constants.AuditResultDenied
|
||||
}
|
||||
s.recordDeviceRealnamePolicyBatchFailure(ctx, devices, req.RealnamePolicy, result, err)
|
||||
return nil, err
|
||||
}
|
||||
s.logDeviceOperation(
|
||||
ctx,
|
||||
constants.AssetAuditOpAssetRealnamePolicy,
|
||||
"批量更新设备实名认证策略",
|
||||
constants.AssetAuditResultSuccess,
|
||||
nil,
|
||||
nil,
|
||||
map[string]any{"asset_ids": ids, "realname_policy": req.RealnamePolicy},
|
||||
len(ids),
|
||||
len(ids),
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
return &dto.BatchUpdateAssetRealnamePolicyResponse{SuccessCount: len(ids), RealnamePolicy: req.RealnamePolicy}, nil
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
673
internal/service/device/unified_audit.go
Normal file
673
internal/service/device/unified_audit.go
Normal file
@@ -0,0 +1,673 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
assetAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/asset_audit"
|
||||
"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"
|
||||
)
|
||||
|
||||
// SetAccessAudit 注入设备身份生命周期的统一审计 Writer。
|
||||
func (s *Service) SetAccessAudit(writer *audit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceLifecycleAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
actionCode, summary, result string,
|
||||
device *model.Device,
|
||||
beforeData, afterData map[string]any,
|
||||
references []audit.ResourceInput,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备统一审计接缝未配置或资源不完整")
|
||||
}
|
||||
resourceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
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), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
resources = append(resources, references...)
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform,
|
||||
Result: result, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceLifecycleFailure(ctx context.Context, actionCode, summary, result string, device *model.Device, deviceID uint, businessErr error) {
|
||||
if device == nil {
|
||||
device = &model.Device{}
|
||||
device.ID = deviceID
|
||||
}
|
||||
if s.db == nil || s.auditWriter == nil || device.ID == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, deviceID, businessErr, errors.New(errors.CodeInvalidStatus, "设备统一审计接缝未配置"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceLifecycleAudit(ctx, tx, actionCode, summary, result, device, nil, nil, nil, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, actionCode, device.ID, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func recordDeviceAuditSecondaryFailure(ctx context.Context, actionCode string, deviceID uint, businessErr, auditErr error) {
|
||||
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
linkage := auditcontext.From(ctx)
|
||||
auditfailure.RecordSecondaryWriteFailure(
|
||||
actionCode, strconv.FormatUint(uint64(deviceID), 10),
|
||||
linkage.RequestID, linkage.CorrelationID, errorCode, auditErr,
|
||||
)
|
||||
}
|
||||
|
||||
type deviceAuditOutcome struct {
|
||||
Result string
|
||||
Summary string
|
||||
}
|
||||
|
||||
type deviceBatchAuditItem struct {
|
||||
Device *model.Device
|
||||
PrimaryRole string
|
||||
Result string
|
||||
Summary string
|
||||
ErrorSummary string
|
||||
BeforeData map[string]any
|
||||
AfterData map[string]any
|
||||
References []audit.ResourceInput
|
||||
}
|
||||
|
||||
type deviceCardAuditChange struct {
|
||||
ShopID *uint
|
||||
Status int
|
||||
}
|
||||
|
||||
func deviceAuditOutcomes(devices []*model.Device, result, summary string) map[uint]deviceAuditOutcome {
|
||||
outcomes := make(map[uint]deviceAuditOutcome, len(devices))
|
||||
for _, device := range devices {
|
||||
if device != nil && device.ID > 0 {
|
||||
outcomes[device.ID] = deviceAuditOutcome{Result: result, Summary: summary}
|
||||
}
|
||||
}
|
||||
return outcomes
|
||||
}
|
||||
|
||||
func setDeviceAuditOutcomes(outcomes map[uint]deviceAuditOutcome, ids []uint, result, summary string) {
|
||||
for _, id := range ids {
|
||||
if _, ok := outcomes[id]; ok {
|
||||
outcomes[id] = deviceAuditOutcome{Result: result, Summary: summary}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setDeviceAuditFailedItems(outcomes map[uint]deviceAuditOutcome, items []dto.AllocationDeviceFailedItem) {
|
||||
for _, item := range items {
|
||||
if _, ok := outcomes[item.DeviceID]; ok {
|
||||
outcomes[item.DeviceID] = deviceAuditOutcome{Result: constants.AuditResultDenied, Summary: item.Reason}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deviceModelsByIDs(devices []*model.Device, ids []uint) []*model.Device {
|
||||
wanted := make(map[uint]struct{}, len(ids))
|
||||
for _, id := range ids {
|
||||
wanted[id] = struct{}{}
|
||||
}
|
||||
result := make([]*model.Device, 0, len(ids))
|
||||
for _, device := range devices {
|
||||
if device != nil {
|
||||
if _, ok := wanted[device.ID]; ok {
|
||||
result = append(result, device)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceTransferAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
devices []*model.Device,
|
||||
outcomes map[uint]deviceAuditOutcome,
|
||||
records []*model.AssetAllocationRecord,
|
||||
targetShopID *uint,
|
||||
newStatus, batchTotal, successCount, failCount int,
|
||||
cardReferences map[uint][]audit.ResourceInput,
|
||||
businessErr error,
|
||||
) error {
|
||||
if cardReferences == nil {
|
||||
var err error
|
||||
cardReferences, _, err = loadDeviceCardAuditReferences(ctx, tx, devices, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
shops, err := loadDeviceTransferAuditShops(ctx, tx, devices, targetShopID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordByDeviceID := make(map[uint]*model.AssetAllocationRecord, len(records))
|
||||
for _, record := range records {
|
||||
if record != nil {
|
||||
recordByDeviceID[record.AssetID] = record
|
||||
}
|
||||
}
|
||||
items := make([]deviceBatchAuditItem, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.ID == 0 {
|
||||
continue
|
||||
}
|
||||
outcome, ok := outcomes[device.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var afterData map[string]any
|
||||
if outcome.Result == constants.AuditResultSuccess {
|
||||
afterData = map[string]any{"shop_id": targetShopID, "status": newStatus}
|
||||
}
|
||||
references := deviceTransferAuditReferences(device, recordByDeviceID[device.ID], targetShopID, shops)
|
||||
references = append(references, cardReferences[device.ID]...)
|
||||
items = append(items, deviceBatchAuditItem{
|
||||
Device: device, PrimaryRole: constants.AuditResourceRoleDeviceTransferTarget,
|
||||
Result: outcome.Result, Summary: outcome.Summary, ErrorSummary: outcome.Summary,
|
||||
BeforeData: map[string]any{"shop_id": device.ShopID, "status": device.Status}, AfterData: afterData,
|
||||
References: references,
|
||||
})
|
||||
}
|
||||
allocationNo := ""
|
||||
if len(records) > 0 && records[0] != nil {
|
||||
allocationNo = records[0].AllocationNo
|
||||
}
|
||||
return s.appendDeviceBatchAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
||||
batchTotal, successCount, failCount, items,
|
||||
map[string]any{"allocation_no": allocationNo, "to_shop_id": targetShopID, "new_status": newStatus}, businessErr)
|
||||
}
|
||||
|
||||
func loadDeviceTransferAuditShops(ctx context.Context, tx *gorm.DB, devices []*model.Device, targetShopID *uint) (map[uint]*model.Shop, error) {
|
||||
shopIDs := make(map[uint]struct{})
|
||||
if targetShopID != nil && *targetShopID > 0 {
|
||||
shopIDs[*targetShopID] = struct{}{}
|
||||
}
|
||||
for _, device := range devices {
|
||||
if device != nil && device.ShopID != nil && *device.ShopID > 0 {
|
||||
shopIDs[*device.ShopID] = struct{}{}
|
||||
}
|
||||
}
|
||||
ids := make([]uint, 0, len(shopIDs))
|
||||
for id := range shopIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var rows []*model.Shop
|
||||
if len(ids) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shops := make(map[uint]*model.Shop, len(rows))
|
||||
for _, shop := range rows {
|
||||
shops[shop.ID] = shop
|
||||
}
|
||||
return shops, nil
|
||||
}
|
||||
|
||||
func deviceTransferAuditReferences(device *model.Device, record *model.AssetAllocationRecord, targetShopID *uint, shops map[uint]*model.Shop) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, 3)
|
||||
if record != nil && record.ID > 0 {
|
||||
recordID := strconv.FormatUint(uint64(record.ID), 10)
|
||||
resources = append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceAssetAllocationRecord, ID: &recordID,
|
||||
Key: recordID, DisplayName: record.AllocationNo,
|
||||
Relation: constants.AuditResourceRelationAffected, Role: constants.AuditResourceRoleAssetAllocationRecord,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": record.ID, "allocation_no": record.AllocationNo, "asset_type": record.AssetType,
|
||||
"asset_id": record.AssetID, "asset_identifier": record.AssetIdentifier,
|
||||
"from_owner_type": record.FromOwnerType, "from_owner_id": record.FromOwnerID,
|
||||
"to_owner_type": record.ToOwnerType, "to_owner_id": record.ToOwnerID,
|
||||
},
|
||||
AfterData: map[string]any{"created": true}, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
if device.ShopID != nil && *device.ShopID > 0 {
|
||||
resources = appendDeviceShopAuditReference(resources, shops[*device.ShopID], *device.ShopID, constants.AuditResourceRoleTransferSourceShop)
|
||||
}
|
||||
if targetShopID != nil && *targetShopID > 0 {
|
||||
resources = appendDeviceShopAuditReference(resources, shops[*targetShopID], *targetShopID, constants.AuditResourceRoleTransferTargetShop)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func appendDeviceShopAuditReference(resources []audit.ResourceInput, shop *model.Shop, shopID uint, role string) []audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(shopID), 10)
|
||||
name := id
|
||||
identity := map[string]any{"id": shopID}
|
||||
if shop != nil {
|
||||
name = shop.ShopName
|
||||
identity = map[string]any{"id": shop.ID, "shop_code": shop.ShopCode, "shop_name": shop.ShopName, "parent_id": shop.ParentID, "level": shop.Level}
|
||||
}
|
||||
return append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourceShop, ID: &id, Key: id, DisplayName: name,
|
||||
Relation: constants.AuditResourceRelationReference, Role: role,
|
||||
IdentitySnapshot: identity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceBatchAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
items []deviceBatchAuditItem,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
linkage := auditcontext.From(ctx)
|
||||
batchKey := linkage.RequestID
|
||||
if batchKey == "" {
|
||||
batchKey = linkage.CorrelationID
|
||||
}
|
||||
if s.auditWriter == nil || batchKey == "" {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备批量审计上下文不完整")
|
||||
}
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
children := make([]audit.AppendInput, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Device == nil || item.Device.ID == 0 {
|
||||
continue
|
||||
}
|
||||
deviceID := strconv.FormatUint(uint64(item.Device.ID), 10)
|
||||
primaryRole := item.PrimaryRole
|
||||
if primaryRole == "" {
|
||||
primaryRole = constants.AuditResourceRoleDeviceTarget
|
||||
}
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &deviceID,
|
||||
Key: audit.DeviceResourceKey(item.Device), DisplayName: item.Device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: primaryRole,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(item.Device), BeforeData: item.BeforeData, AfterData: item.AfterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: item.Summary,
|
||||
}}
|
||||
resources = append(resources, item.References...)
|
||||
childErrorCode, childErrorSummary := "", ""
|
||||
if item.Result == constants.AuditResultFailed || item.Result == constants.AuditResultDenied {
|
||||
childErrorCode = errorCode
|
||||
childErrorSummary = item.ErrorSummary
|
||||
if childErrorSummary == "" {
|
||||
childErrorSummary = errorSummary
|
||||
}
|
||||
}
|
||||
children = append(children, audit.AppendInput{
|
||||
EventID: stableDeviceBatchEventID(kind+"-"+item.Result+"-device", batchKey+":"+deviceID),
|
||||
ActionCode: itemAction, Summary: item.Summary, ScopeType: constants.AuditScopePlatform, Result: item.Result,
|
||||
ErrorCode: childErrorCode, ErrorSummary: childErrorSummary, Resources: resources,
|
||||
})
|
||||
}
|
||||
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
|
||||
Root: audit.AppendInput{
|
||||
EventID: stableDeviceBatchEventID(kind+"-"+result, batchKey),
|
||||
ActionCode: rootAction, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: result,
|
||||
ErrorCode: errorCode, ErrorSummary: errorSummary,
|
||||
BatchTotal: batchTotal, SuccessCount: successCount, FailCount: failCount, Metadata: metadata,
|
||||
Resources: []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDeviceBatch, Key: batchKey, DisplayName: batchKey,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceBatch,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"request_id": linkage.RequestID, "correlation_id": linkage.CorrelationID,
|
||||
"device_count": len(items), "operation_type": kind,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
}},
|
||||
},
|
||||
Children: children,
|
||||
})
|
||||
}
|
||||
|
||||
func loadDeviceCardAuditReferences(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
devices []*model.Device,
|
||||
change *deviceCardAuditChange,
|
||||
) (map[uint][]audit.ResourceInput, []uint, error) {
|
||||
deviceByID := make(map[uint]*model.Device, len(devices))
|
||||
deviceIDs := make([]uint, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device != nil && device.ID > 0 {
|
||||
deviceByID[device.ID] = device
|
||||
deviceIDs = append(deviceIDs, device.ID)
|
||||
}
|
||||
}
|
||||
result := make(map[uint][]audit.ResourceInput)
|
||||
if len(deviceIDs) == 0 {
|
||||
return result, nil, nil
|
||||
}
|
||||
var bindings []*model.DeviceSimBinding
|
||||
if err := tx.WithContext(ctx).Where("device_id IN ? AND bind_status = ?", deviceIDs, 1).Find(&bindings).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
cardIDs := make([]uint, 0, len(bindings))
|
||||
seenCards := make(map[uint]struct{}, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
if _, exists := seenCards[binding.IotCardID]; !exists {
|
||||
seenCards[binding.IotCardID] = struct{}{}
|
||||
cardIDs = append(cardIDs, binding.IotCardID)
|
||||
}
|
||||
}
|
||||
var cards []*model.IotCard
|
||||
if len(cardIDs) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", cardIDs).Find(&cards).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
cardByID := make(map[uint]*model.IotCard, len(cards))
|
||||
for _, card := range cards {
|
||||
cardByID[card.ID] = card
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
device := deviceByID[binding.DeviceID]
|
||||
card := cardByID[binding.IotCardID]
|
||||
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
||||
cardID := strconv.FormatUint(uint64(binding.IotCardID), 10)
|
||||
deviceVirtualNo, cardICCID, cardVirtualNo := "", "", ""
|
||||
if device != nil {
|
||||
deviceVirtualNo = device.VirtualNo
|
||||
}
|
||||
cardIdentity := map[string]any{"id": binding.IotCardID}
|
||||
cardKey, cardName := cardID, cardID
|
||||
if card != nil {
|
||||
cardICCID, cardVirtualNo = card.ICCID, card.VirtualNo
|
||||
cardKey, cardName = audit.IotCardResourceKey(card), card.ICCID
|
||||
cardIdentity = audit.IotCardIdentitySnapshot(card)
|
||||
}
|
||||
cardRelation := constants.AuditResourceRelationReference
|
||||
var beforeData, afterData map[string]any
|
||||
if change != nil {
|
||||
cardRelation = constants.AuditResourceRelationAffected
|
||||
if card != nil {
|
||||
beforeData = map[string]any{"shop_id": card.ShopID, "status": card.Status}
|
||||
}
|
||||
afterData = map[string]any{"shop_id": change.ShopID, "status": change.Status}
|
||||
}
|
||||
result[binding.DeviceID] = append(result[binding.DeviceID],
|
||||
audit.ResourceInput{
|
||||
Type: constants.AuditResourceIotCard, ID: &cardID, Key: cardKey, DisplayName: cardName,
|
||||
Relation: cardRelation, Role: constants.AuditResourceRoleDeviceBoundCard,
|
||||
IdentitySnapshot: cardIdentity, BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
},
|
||||
audit.ResourceInput{
|
||||
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
||||
Key: bindingID, DisplayName: deviceVirtualNo,
|
||||
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleDeviceCardBinding,
|
||||
IdentitySnapshot: map[string]any{
|
||||
"id": binding.ID, "device_id": binding.DeviceID, "device_virtual_no": deviceVirtualNo,
|
||||
"slot_position": binding.SlotPosition, "iot_card_id": binding.IotCardID,
|
||||
"iccid": cardICCID, "virtual_no": cardVirtualNo, "is_current": binding.IsCurrent,
|
||||
},
|
||||
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
},
|
||||
)
|
||||
}
|
||||
return result, cardIDs, nil
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceTransferAuditFailure(
|
||||
ctx context.Context,
|
||||
rootAction, itemAction, kind, summary, result string,
|
||||
devices []*model.Device,
|
||||
outcomes map[uint]deviceAuditOutcome,
|
||||
targetShopID *uint,
|
||||
newStatus, batchTotal, successCount, failCount int,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || len(devices) == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, rootAction, 0, businessErr, errors.New(errors.CodeInvalidStatus, "设备批量审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceTransferAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
||||
devices, outcomes, nil, targetShopID, newStatus, batchTotal, successCount, failCount, nil, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, rootAction, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func stableDeviceBatchEventID(kind, key string) string {
|
||||
return "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("device:"+kind+":"+key)).String()
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceSeriesBindingAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
devices []*model.Device,
|
||||
outcomes map[uint]deviceAuditOutcome,
|
||||
seriesID *uint,
|
||||
result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
series, err := loadDeviceSeriesAuditResources(ctx, tx, devices, seriesID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cardReferences, _, err := loadDeviceCardAuditReferences(ctx, tx, devices, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]deviceBatchAuditItem, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.ID == 0 {
|
||||
continue
|
||||
}
|
||||
outcome, ok := outcomes[device.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var afterData map[string]any
|
||||
if outcome.Result == constants.AuditResultSuccess {
|
||||
afterData = map[string]any{"series_id": seriesID}
|
||||
}
|
||||
references := deviceSeriesAuditReferences(device.SeriesID, seriesID, series)
|
||||
references = append(references, cardReferences[device.ID]...)
|
||||
items = append(items, deviceBatchAuditItem{
|
||||
Device: device, PrimaryRole: constants.AuditResourceRoleDeviceSeriesTarget,
|
||||
Result: outcome.Result, Summary: outcome.Summary, ErrorSummary: outcome.Summary,
|
||||
BeforeData: map[string]any{"series_id": device.SeriesID}, AfterData: afterData,
|
||||
References: references,
|
||||
})
|
||||
}
|
||||
return s.appendDeviceBatchAudit(ctx, tx,
|
||||
constants.AuditActionDeviceSeriesBindingBatch,
|
||||
constants.AuditActionDeviceSeriesBound,
|
||||
"series-binding", "批量设置设备系列绑定", result,
|
||||
batchTotal, successCount, failCount, items, metadata, businessErr)
|
||||
}
|
||||
|
||||
func loadDeviceSeriesAuditResources(ctx context.Context, tx *gorm.DB, devices []*model.Device, targetSeriesID *uint) (map[uint]*model.PackageSeries, error) {
|
||||
seriesIDs := make(map[uint]struct{})
|
||||
if targetSeriesID != nil && *targetSeriesID > 0 {
|
||||
seriesIDs[*targetSeriesID] = struct{}{}
|
||||
}
|
||||
for _, device := range devices {
|
||||
if device != nil && device.SeriesID != nil && *device.SeriesID > 0 {
|
||||
seriesIDs[*device.SeriesID] = struct{}{}
|
||||
}
|
||||
}
|
||||
ids := make([]uint, 0, len(seriesIDs))
|
||||
for id := range seriesIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var rows []*model.PackageSeries
|
||||
if len(ids) > 0 {
|
||||
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", ids).Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
series := make(map[uint]*model.PackageSeries, len(rows))
|
||||
for _, item := range rows {
|
||||
series[item.ID] = item
|
||||
}
|
||||
return series, nil
|
||||
}
|
||||
|
||||
func deviceSeriesAuditReferences(previousID, targetID *uint, series map[uint]*model.PackageSeries) []audit.ResourceInput {
|
||||
resources := make([]audit.ResourceInput, 0, 2)
|
||||
if previousID != nil && *previousID > 0 {
|
||||
resources = appendDevicePackageSeriesAuditReference(resources, series[*previousID], *previousID, constants.AuditResourceRolePreviousPackageSeries)
|
||||
}
|
||||
if targetID != nil && *targetID > 0 {
|
||||
resources = appendDevicePackageSeriesAuditReference(resources, series[*targetID], *targetID, constants.AuditResourceRoleTargetPackageSeries)
|
||||
}
|
||||
return resources
|
||||
}
|
||||
|
||||
func appendDevicePackageSeriesAuditReference(resources []audit.ResourceInput, series *model.PackageSeries, seriesID uint, role string) []audit.ResourceInput {
|
||||
id := strconv.FormatUint(uint64(seriesID), 10)
|
||||
name := id
|
||||
identity := map[string]any{"id": seriesID}
|
||||
if series != nil {
|
||||
name = series.SeriesName
|
||||
identity = map[string]any{"id": series.ID, "series_code": series.SeriesCode, "series_name": series.SeriesName, "status": series.Status}
|
||||
}
|
||||
return append(resources, audit.ResourceInput{
|
||||
Type: constants.AuditResourcePackageSeries, ID: &id, Key: id, DisplayName: name,
|
||||
Relation: constants.AuditResourceRelationReference, Role: role,
|
||||
IdentitySnapshot: identity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceSeriesBindingAuditFailure(
|
||||
ctx context.Context,
|
||||
devices []*model.Device,
|
||||
outcomes map[uint]deviceAuditOutcome,
|
||||
seriesID *uint,
|
||||
result string,
|
||||
batchTotal, successCount, failCount int,
|
||||
metadata map[string]any,
|
||||
businessErr error,
|
||||
) {
|
||||
if s.db == nil || s.auditWriter == nil || len(devices) == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceSeriesBindingBatch, 0, businessErr, errors.New(errors.CodeInvalidStatus, "设备系列绑定审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceSeriesBindingAudit(ctx, tx, devices, outcomes, seriesID, result,
|
||||
batchTotal, successCount, failCount, metadata, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceSeriesBindingBatch, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceRealnamePolicyBatchAudit(ctx context.Context, tx *gorm.DB, devices []*model.Device, policy string) error {
|
||||
cardReferences, _, err := loadDeviceCardAuditReferences(ctx, tx, devices, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]deviceBatchAuditItem, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.ID == 0 || device.RealnamePolicy == policy {
|
||||
continue
|
||||
}
|
||||
items = append(items, deviceBatchAuditItem{
|
||||
Device: device, PrimaryRole: constants.AuditResourceRoleDeviceTarget,
|
||||
Result: constants.AuditResultSuccess, Summary: "更新设备实名策略",
|
||||
BeforeData: map[string]any{"realname_policy": device.RealnamePolicy},
|
||||
AfterData: map[string]any{"realname_policy": policy},
|
||||
References: cardReferences[device.ID],
|
||||
})
|
||||
}
|
||||
return s.appendDeviceBatchAudit(ctx, tx,
|
||||
constants.AuditActionDeviceRealnamePolicyBatchUpdated,
|
||||
constants.AuditActionDeviceRealnamePolicyUpdated,
|
||||
"realname-policy", "批量更新设备实名策略", constants.AuditResultSuccess,
|
||||
len(items), len(items), 0, items,
|
||||
map[string]any{"realname_policy": policy, "requested_count": len(devices)}, nil)
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceRealnamePolicyBatchFailure(ctx context.Context, devices []*model.Device, policy, result string, businessErr error) {
|
||||
if s.db == nil || s.auditWriter == nil || len(devices) == 0 {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceRealnamePolicyBatchUpdated, 0, businessErr, errors.New(errors.CodeInvalidStatus, "设备实名策略批量审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
items := make([]deviceBatchAuditItem, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
if device == nil || device.ID == 0 {
|
||||
continue
|
||||
}
|
||||
items = append(items, deviceBatchAuditItem{
|
||||
Device: device, PrimaryRole: constants.AuditResourceRoleDeviceTarget,
|
||||
Result: result, Summary: "更新设备实名策略未完成",
|
||||
BeforeData: map[string]any{"realname_policy": device.RealnamePolicy},
|
||||
AfterData: map[string]any{"requested_realname_policy": policy},
|
||||
})
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceBatchAudit(ctx, tx,
|
||||
constants.AuditActionDeviceRealnamePolicyBatchUpdated,
|
||||
constants.AuditActionDeviceRealnamePolicyUpdated,
|
||||
"realname-policy", "批量更新设备实名策略未完成", result,
|
||||
len(devices), 0, len(devices), items, map[string]any{"realname_policy": policy}, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceRealnamePolicyBatchUpdated, 0, businessErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendDeviceRealnamePolicyAudit(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
summary, result string,
|
||||
device *model.Device,
|
||||
beforeData, afterData map[string]any,
|
||||
businessErr error,
|
||||
) error {
|
||||
if s.auditWriter == nil || device == nil || device.ID == 0 {
|
||||
return errors.New(errors.CodeInvalidStatus, "设备实名策略审计接缝未配置或资源不完整")
|
||||
}
|
||||
cardReferences, _, err := loadDeviceCardAuditReferences(ctx, tx, []*model.Device{device}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deviceID := strconv.FormatUint(uint64(device.ID), 10)
|
||||
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
||||
resources := []audit.ResourceInput{{
|
||||
Type: constants.AuditResourceDevice, ID: &deviceID,
|
||||
Key: audit.DeviceResourceKey(device), DisplayName: device.VirtualNo,
|
||||
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleDeviceTarget,
|
||||
IdentitySnapshot: audit.DeviceIdentitySnapshot(device), BeforeData: beforeData, AfterData: afterData,
|
||||
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
||||
}}
|
||||
resources = append(resources, cardReferences[device.ID]...)
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
ActionCode: constants.AuditActionDeviceRealnamePolicyUpdated, Summary: summary,
|
||||
ScopeType: constants.AuditScopePlatform, Result: result,
|
||||
ErrorCode: errorCode, ErrorSummary: errorSummary, Resources: resources,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) recordDeviceRealnamePolicyFailure(ctx context.Context, device *model.Device, deviceID uint, businessErr error) {
|
||||
if device == nil || device.ID == 0 || s.db == nil || s.auditWriter == nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceRealnamePolicyUpdated, deviceID, businessErr, errors.New(errors.CodeInvalidStatus, "设备实名策略审计接缝未配置或资源不完整"))
|
||||
return
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.appendDeviceRealnamePolicyAudit(ctx, tx, "更新设备实名策略失败", constants.AuditResultFailed,
|
||||
device, map[string]any{"realname_policy": device.RealnamePolicy}, nil, businessErr)
|
||||
}); err != nil {
|
||||
recordDeviceAuditSecondaryFailure(ctx, constants.AuditActionDeviceRealnamePolicyUpdated, deviceID, businessErr, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user