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

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -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)
}

View File

@@ -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

View 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
}

View 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)

View File

@@ -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
}

View File

@@ -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

View 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)
}
}