Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
201 lines
7.6 KiB
Go
201 lines
7.6 KiB
Go
package customer_binding
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit"
|
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
|
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
|
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
|
)
|
|
|
|
// SetAccessAudit 注入个人客户资产关系的统一审计接缝。
|
|
func (s *Service) SetAccessAudit(writer accessauditapp.Writer) {
|
|
s.accessAudit = writer
|
|
}
|
|
|
|
func (s *Service) writeBindingAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
actionCode, summary string,
|
|
customerID uint,
|
|
personalDevices []accessauditapp.PersonalCustomerDeviceChange,
|
|
personalICCIDs []accessauditapp.PersonalCustomerICCIDChange,
|
|
cards []accessauditapp.IotCardChange,
|
|
devices []accessauditapp.DeviceChange,
|
|
) error {
|
|
if s.accessAudit == nil {
|
|
return errors.New(errors.CodeInvalidStatus, "个人客户资产审计接缝未配置")
|
|
}
|
|
var customer model.PersonalCustomer
|
|
if err := tx.WithContext(ctx).First(&customer, customerID).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询个人客户审计快照失败")
|
|
}
|
|
|
|
value := auditcontext.From(ctx)
|
|
operatorID := customerID
|
|
actorKind := constants.AuditActorPersonalCustomer
|
|
actorName := customer.Nickname
|
|
source := constants.AuditSourcePersonalAPI
|
|
scopeType := constants.AuditScopePersonalCustomer
|
|
visibility := constants.AuditSubjectDetail
|
|
subjectData := map[string]any(nil)
|
|
if actionCode == constants.AuditActionPersonalCustomerAssetBound {
|
|
assetType, assetID := bindingAssetReference(cards, devices)
|
|
subjectData = map[string]any{"asset_type": assetType, "asset_id": assetID}
|
|
} else {
|
|
operatorID = middleware.GetUserIDFromContext(ctx)
|
|
if parsed, err := strconv.ParseUint(value.ActorID, 10, 64); err == nil && parsed > 0 {
|
|
operatorID = uint(parsed)
|
|
}
|
|
actorKind = value.ActorKind
|
|
actorName = value.ActorName
|
|
source = value.Source
|
|
scopeType = constants.AuditScopePlatform
|
|
visibility = constants.AuditSubjectResult
|
|
}
|
|
if operatorID == 0 {
|
|
return errors.New(errors.CodeInvalidStatus, "个人客户资产审计操作者不完整")
|
|
}
|
|
|
|
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
|
ActionCode: actionCode, Summary: summary, Result: constants.AuditResultSuccess,
|
|
OperatorID: operatorID, ActorKind: actorKind, ActorName: actorName, Source: source, ScopeType: scopeType,
|
|
PersonalCustomer: &customer, PersonalDevices: personalDevices, PersonalICCIDs: personalICCIDs,
|
|
Cards: cards, Devices: devices,
|
|
SubjectVisibility: visibility, SubjectSummary: summary, SubjectData: subjectData,
|
|
})
|
|
}
|
|
|
|
func bindingAssetReference(cards []accessauditapp.IotCardChange, devices []accessauditapp.DeviceChange) (string, uint) {
|
|
if len(cards) > 0 && cards[0].Card != nil {
|
|
return constants.AuditResourceIotCard, cards[0].Card.ID
|
|
}
|
|
if len(devices) > 0 && devices[0].Device != nil {
|
|
return constants.AuditResourceDevice, devices[0].Device.ID
|
|
}
|
|
return "", 0
|
|
}
|
|
|
|
func cardAuditChange(card *model.IotCard, relation, role string, beforeData, afterData map[string]any) accessauditapp.IotCardChange {
|
|
return accessauditapp.IotCardChange{
|
|
Card: card, Relation: relation, Role: role, BeforeData: beforeData, AfterData: afterData,
|
|
SubjectSummary: "个人客户资产关系已更新",
|
|
}
|
|
}
|
|
|
|
func deviceAuditChange(device *model.Device, relation, role string, beforeData, afterData map[string]any) accessauditapp.DeviceChange {
|
|
return accessauditapp.DeviceChange{
|
|
Device: device, Relation: relation, Role: role, BeforeData: beforeData, AfterData: afterData,
|
|
SubjectSummary: "个人客户资产关系已更新",
|
|
}
|
|
}
|
|
|
|
func (s *Service) loadAuditAssets(ctx context.Context, tx *gorm.DB, oldType string, oldID uint, newType string, newID uint) ([]accessauditapp.IotCardChange, []accessauditapp.DeviceChange, error) {
|
|
cards := make([]accessauditapp.IotCardChange, 0, 2)
|
|
devices := make([]accessauditapp.DeviceChange, 0, 2)
|
|
appendAsset := func(assetType string, assetID uint, role string) error {
|
|
switch normalizeAssetType(assetType) {
|
|
case assetTypeIotCard:
|
|
card, err := s.readCard(ctx, tx, assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cards = append(cards, cardAuditChange(card, constants.AuditResourceRelationAffected, role, nil, nil))
|
|
case assetTypeDevice:
|
|
device, err := s.readDevice(ctx, tx, assetID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
devices = append(devices, deviceAuditChange(device, constants.AuditResourceRelationAffected, role, nil, nil))
|
|
default:
|
|
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
|
|
}
|
|
return nil
|
|
}
|
|
if err := appendAsset(oldType, oldID, constants.AuditResourceRolePersonalCustomerOldAsset); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err := appendAsset(newType, newID, constants.AuditResourceRolePersonalCustomerNewAsset); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return cards, devices, nil
|
|
}
|
|
|
|
func (s *Service) writeMigrationAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
customerID uint,
|
|
oldType string,
|
|
oldID uint,
|
|
newType string,
|
|
newID uint,
|
|
personalDevices []accessauditapp.PersonalCustomerDeviceChange,
|
|
personalICCIDs []accessauditapp.PersonalCustomerICCIDChange,
|
|
) error {
|
|
cards, devices, err := s.loadAuditAssets(ctx, tx, oldType, oldID, newType, newID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.writeBindingAudit(
|
|
ctx, tx, constants.AuditActionPersonalCustomerAssetBindingMigrated, "换货迁移个人客户资产绑定",
|
|
customerID, personalDevices, personalICCIDs, cards, devices,
|
|
)
|
|
}
|
|
|
|
// UnbindByVirtualNo 按现有换货重置语义删除设备号绑定,并在同一事务记录实际删除关系。
|
|
func (s *Service) UnbindByVirtualNo(ctx context.Context, tx *gorm.DB, assetType string, assetID uint, virtualNo string) error {
|
|
if s.accessAudit == nil {
|
|
return errors.New(errors.CodeInvalidStatus, "个人客户资产审计接缝未配置")
|
|
}
|
|
if tx == nil {
|
|
tx = s.db
|
|
}
|
|
records, err := s.makePCD(tx).GetByDeviceNo(ctx, virtualNo)
|
|
if err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "查询个人客户资产绑定失败")
|
|
}
|
|
if err := tx.WithContext(ctx).Where("virtual_no = ?", virtualNo).Delete(&model.PersonalCustomerDevice{}).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "清理个人客户绑定失败")
|
|
}
|
|
for _, record := range records {
|
|
if record == nil {
|
|
continue
|
|
}
|
|
cards := []accessauditapp.IotCardChange(nil)
|
|
devices := []accessauditapp.DeviceChange(nil)
|
|
switch normalizeAssetType(assetType) {
|
|
case assetTypeIotCard:
|
|
card, loadErr := s.readCard(ctx, tx, assetID)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
cards = append(cards, cardAuditChange(card, constants.AuditResourceRelationAffected, constants.AuditResourceRolePersonalCustomerBoundAsset, nil, nil))
|
|
case assetTypeDevice:
|
|
device, loadErr := s.readDevice(ctx, tx, assetID)
|
|
if loadErr != nil {
|
|
return loadErr
|
|
}
|
|
devices = append(devices, deviceAuditChange(device, constants.AuditResourceRelationAffected, constants.AuditResourceRolePersonalCustomerBoundAsset, nil, nil))
|
|
default:
|
|
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
|
|
}
|
|
if err := s.writeBindingAudit(
|
|
ctx, tx, constants.AuditActionPersonalCustomerAssetUnbound, "解除个人客户资产绑定", record.CustomerID,
|
|
[]accessauditapp.PersonalCustomerDeviceChange{{
|
|
Binding: record, Role: constants.AuditResourceRolePersonalCustomerAssetBinding,
|
|
BeforeData: map[string]any{"virtual_no": record.VirtualNo, "status": record.Status},
|
|
AfterData: map[string]any{"deleted": true},
|
|
}}, nil, cards, devices,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|