Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
237 lines
9.0 KiB
Go
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
|
|
}
|