Files
junhong_cmp_fiber/internal/service/device/binding_audit.go
break 5e552d99bc 收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
2026-08-05 14:30:54 +08:00

237 lines
9.0 KiB
Go

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
}