All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m32s
760 lines
30 KiB
Go
760 lines
30 KiB
Go
package iot_card
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
|
|
cardapp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
|
"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/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 注入 IoT 卡身份生命周期的统一审计 Writer。
|
|
func (s *Service) SetAccessAudit(writer *audit.Writer) {
|
|
s.auditWriter = writer
|
|
}
|
|
|
|
// WriteCardStateAudit 将卡观测事务中的人工、回调或 Worker 状态变化写入统一 Audit Event。
|
|
func (s *Service) WriteCardStateAudit(ctx context.Context, tx *gorm.DB, input cardapp.StateAudit) error {
|
|
extraResources := make([]audit.ResourceInput, 0, 1)
|
|
if input.IntegrationID != "" {
|
|
extraResources = append(extraResources, cardStateIntegrationAuditResource(ctx, input.IntegrationID))
|
|
}
|
|
return s.appendCardLifecycleAudit(ctx, tx, input.ActionCode, input.Summary, constants.AuditResultSuccess,
|
|
input.Card, input.BeforeData, input.AfterData, nil, extraResources...)
|
|
}
|
|
|
|
// WriteCardStateFailure 使用独立短事务记录已解析卡资源后的回调或 Worker 失败。
|
|
func (s *Service) WriteCardStateFailure(ctx context.Context, input cardapp.StateAudit, businessErr error) {
|
|
extraResources := make([]audit.ResourceInput, 0, 1)
|
|
if input.IntegrationID != "" {
|
|
extraResources = append(extraResources, cardStateIntegrationAuditResource(ctx, input.IntegrationID))
|
|
}
|
|
s.recordCardLifecycleFailure(ctx, input.ActionCode, input.Summary, constants.AuditResultFailed,
|
|
input.Card, input.Card.ID, businessErr, extraResources...)
|
|
}
|
|
|
|
func cardStateIntegrationAuditResource(ctx context.Context, integrationID string) audit.ResourceInput {
|
|
linkage := auditcontext.From(ctx)
|
|
correlationID := linkage.CorrelationID
|
|
if correlationID == "" {
|
|
correlationID = linkage.RequestID
|
|
}
|
|
direction := constants.IntegrationDirectionInbound
|
|
role := constants.AuditResourceRoleCallbackIntegration
|
|
provider := linkage.ActorID
|
|
if linkage.Source == constants.AuditSourceWorker {
|
|
direction = constants.IntegrationDirectionOutbound
|
|
role = constants.AuditResourceRoleWorkerIntegration
|
|
provider = constants.IntegrationProviderGateway
|
|
}
|
|
return audit.ResourceInput{
|
|
Type: constants.AuditResourceIntegrationLog, Key: integrationID, DisplayName: integrationID,
|
|
Relation: constants.AuditResourceRelationReference, Role: role,
|
|
IdentitySnapshot: map[string]any{
|
|
"integration_id": integrationID, "provider": provider,
|
|
"direction": direction, "correlation_id": correlationID,
|
|
},
|
|
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
|
}
|
|
}
|
|
|
|
func cardRefreshAuditAction(ctx context.Context) (string, bool) {
|
|
switch auditcontext.From(ctx).ActorKind {
|
|
case constants.AuditActorAccount:
|
|
return constants.AuditActionIotCardManualRefreshed, true
|
|
case constants.AuditActorPersonalCustomer:
|
|
return constants.AuditActionIotCardPersonalRefreshed, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func (s *Service) updateCardRefreshCompletion(ctx context.Context, card *model.IotCard, syncTime time.Time, result, summary string) error {
|
|
actionCode, audited := cardRefreshAuditAction(ctx)
|
|
if !audited {
|
|
return s.iotCardStore.UpdateFields(ctx, card.ID, map[string]any{"last_sync_time": syncTime})
|
|
}
|
|
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).Update("last_sync_time", syncTime).Error; err != nil {
|
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡刷新时间失败")
|
|
}
|
|
return s.appendCardLifecycleAudit(ctx, tx, actionCode, summary, result,
|
|
card, map[string]any{"last_sync_time": card.LastSyncTime}, map[string]any{"last_sync_time": syncTime}, nil)
|
|
})
|
|
}
|
|
|
|
func (s *Service) recordCardRefreshFailure(ctx context.Context, card *model.IotCard, result string, businessErr error) {
|
|
actionCode, audited := cardRefreshAuditAction(ctx)
|
|
if !audited || card == nil {
|
|
return
|
|
}
|
|
s.recordCardLifecycleFailure(ctx, actionCode, "人工刷新 IoT 卡未完成", result, card, card.ID, businessErr)
|
|
}
|
|
|
|
func (s *Service) completeCardRefreshAttempt(
|
|
ctx context.Context,
|
|
card *model.IotCard,
|
|
attempt *gatewayAttempt,
|
|
callErr error,
|
|
stateChanged bool,
|
|
message string,
|
|
) error {
|
|
if err := s.completeGatewayCardAttempt(ctx, attempt, callErr, stateChanged); err != nil {
|
|
wrapped := errors.Wrap(errors.CodeDatabaseError, err, message)
|
|
s.recordCardRefreshFailure(ctx, card, constants.AuditResultFailed, wrapped)
|
|
return wrapped
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) appendCardLifecycleAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
actionCode, summary, result string,
|
|
card *model.IotCard,
|
|
beforeData, afterData map[string]any,
|
|
businessErr error,
|
|
extraResources ...audit.ResourceInput,
|
|
) error {
|
|
if s.auditWriter == nil || card == nil || card.ID == 0 {
|
|
return errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置或资源不完整")
|
|
}
|
|
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
|
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
|
resources := []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIotCard, ID: &resourceID,
|
|
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
|
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: beforeData, AfterData: afterData,
|
|
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: summary,
|
|
}}
|
|
resources = append(resources, extraResources...)
|
|
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) recordCardLifecycleFailure(ctx context.Context, actionCode, summary, result string, card *model.IotCard, cardID uint, businessErr error, extraResources ...audit.ResourceInput) {
|
|
if card == nil {
|
|
card = &model.IotCard{}
|
|
card.ID = cardID
|
|
}
|
|
if s.db == nil || s.auditWriter == nil || card.ID == 0 {
|
|
recordCardAuditSecondaryFailure(ctx, actionCode, cardID, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡统一审计接缝未配置"))
|
|
return
|
|
}
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.appendCardLifecycleAudit(ctx, tx, actionCode, summary, result, card, nil, nil, businessErr, extraResources...)
|
|
}); err != nil {
|
|
recordCardAuditSecondaryFailure(ctx, actionCode, card.ID, businessErr, err)
|
|
}
|
|
}
|
|
|
|
func (s *Service) appendBatchDeleteAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, batchTotal int) error {
|
|
linkage := auditcontext.From(ctx)
|
|
if s.auditWriter == nil || linkage.RequestID == "" {
|
|
return errors.New(errors.CodeInvalidStatus, "IoT 卡批量删除审计上下文不完整")
|
|
}
|
|
rootEventID := stableCardBatchEventID("delete", linkage.RequestID)
|
|
result := constants.AuditResultSuccess
|
|
if len(cards) < batchTotal {
|
|
result = constants.AuditResultPartial
|
|
}
|
|
children := make([]audit.AppendInput, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card == nil || card.ID == 0 {
|
|
continue
|
|
}
|
|
resourceID := strconv.FormatUint(uint64(card.ID), 10)
|
|
children = append(children, audit.AppendInput{
|
|
EventID: stableCardBatchEventID("delete-card", linkage.RequestID+":"+resourceID),
|
|
ActionCode: constants.AuditActionIotCardDeleted, Summary: "批量删除 IoT 卡", Result: constants.AuditResultSuccess,
|
|
Resources: []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIotCard, ID: &resourceID,
|
|
Key: audit.IotCardResourceKey(card), DisplayName: card.ICCID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardTarget,
|
|
IdentitySnapshot: audit.IotCardIdentitySnapshot(card), BeforeData: cardSnapshot(card),
|
|
AfterData: map[string]any{"deleted": true},
|
|
SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "IoT 卡已删除",
|
|
}},
|
|
})
|
|
}
|
|
return s.auditWriter.AppendBatch(ctx, tx, audit.BatchInput{
|
|
Root: audit.AppendInput{
|
|
EventID: rootEventID, ActionCode: constants.AuditActionIotCardBatchDeleted,
|
|
Summary: "批量删除 IoT 卡", Result: result,
|
|
BatchTotal: batchTotal, SuccessCount: len(cards), FailCount: batchTotal - len(cards),
|
|
Resources: []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
|
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(cards)},
|
|
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
|
}},
|
|
},
|
|
Children: children,
|
|
})
|
|
}
|
|
|
|
func (s *Service) recordBatchDeleteFailure(ctx context.Context, cardIDs []uint, businessErr error) {
|
|
linkage := auditcontext.From(ctx)
|
|
if s.db == nil || s.auditWriter == nil || linkage.RequestID == "" {
|
|
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardBatchDeleted, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡批量删除审计接缝未配置"))
|
|
return
|
|
}
|
|
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
|
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
|
EventID: stableCardBatchEventID("delete-failed", linkage.RequestID),
|
|
ActionCode: constants.AuditActionIotCardBatchDeleted, Summary: "批量删除 IoT 卡失败",
|
|
Result: constants.AuditResultFailed, ErrorCode: errorCode, ErrorSummary: errorSummary,
|
|
BatchTotal: len(cardIDs), FailCount: len(cardIDs),
|
|
Resources: []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
|
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(cardIDs)},
|
|
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
|
}},
|
|
})
|
|
})
|
|
if err != nil {
|
|
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardBatchDeleted, 0, businessErr, err)
|
|
}
|
|
}
|
|
|
|
type cardAuditOutcome struct {
|
|
Result string
|
|
Summary string
|
|
}
|
|
|
|
func cardAuditOutcomes(cards []*model.IotCard, result, summary string) map[uint]cardAuditOutcome {
|
|
outcomes := make(map[uint]cardAuditOutcome, len(cards))
|
|
for _, card := range cards {
|
|
if card != nil && card.ID > 0 {
|
|
outcomes[card.ID] = cardAuditOutcome{Result: result, Summary: summary}
|
|
}
|
|
}
|
|
return outcomes
|
|
}
|
|
|
|
func setCardAuditOutcomes(outcomes map[uint]cardAuditOutcome, cardIDs []uint, result, summary string) {
|
|
for _, cardID := range cardIDs {
|
|
outcomes[cardID] = cardAuditOutcome{Result: result, Summary: summary}
|
|
}
|
|
}
|
|
|
|
func setCardAuditOutcomeByICCID(outcomes map[uint]cardAuditOutcome, cards []*model.IotCard, iccid, result, summary string) {
|
|
for _, card := range cards {
|
|
if card != nil && (card.ICCID == iccid || card.ICCID19 == iccid || card.ICCID20 != nil && *card.ICCID20 == iccid) {
|
|
outcomes[card.ID] = cardAuditOutcome{Result: result, Summary: summary}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) appendCardTransferAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
rootAction, itemAction, kind, summary, result string,
|
|
cards []*model.IotCard,
|
|
outcomes map[uint]cardAuditOutcome,
|
|
records []*model.AssetAllocationRecord,
|
|
newShopID *uint,
|
|
newStatus, batchTotal, successCount, failCount int,
|
|
businessErr error,
|
|
) error {
|
|
shops, err := loadCardTransferAuditShops(ctx, tx, cards, records, newShopID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recordByCardID := make(map[uint]*model.AssetAllocationRecord, len(records))
|
|
for _, record := range records {
|
|
if record != nil {
|
|
recordByCardID[record.AssetID] = record
|
|
}
|
|
}
|
|
items := make([]cardBatchAuditItem, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card == nil || card.ID == 0 {
|
|
continue
|
|
}
|
|
outcome, ok := outcomes[card.ID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
beforeData := map[string]any{"shop_id": card.ShopID, "status": card.Status}
|
|
var afterData map[string]any
|
|
if outcome.Result == constants.AuditResultSuccess {
|
|
afterData = map[string]any{"shop_id": newShopID, "status": newStatus}
|
|
}
|
|
references := cardTransferAuditReferences(card, recordByCardID[card.ID], newShopID, shops)
|
|
references = append(references, deviceReferences[card.ID]...)
|
|
items = append(items, cardBatchAuditItem{
|
|
Card: card, Result: outcome.Result, Summary: outcome.Summary,
|
|
BeforeData: beforeData, AfterData: afterData,
|
|
References: references,
|
|
})
|
|
}
|
|
allocationNo := ""
|
|
if len(records) > 0 && records[0] != nil {
|
|
allocationNo = records[0].AllocationNo
|
|
}
|
|
return s.appendCardBatchAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
|
batchTotal, successCount, failCount, items,
|
|
map[string]any{"allocation_no": allocationNo, "to_shop_id": newShopID, "new_status": newStatus}, businessErr)
|
|
}
|
|
|
|
func loadCardTransferAuditShops(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, records []*model.AssetAllocationRecord, newShopID *uint) (map[uint]*model.Shop, error) {
|
|
shopIDs := make(map[uint]struct{})
|
|
if newShopID != nil && *newShopID > 0 {
|
|
shopIDs[*newShopID] = struct{}{}
|
|
}
|
|
for _, card := range cards {
|
|
if card != nil && card.ShopID != nil && *card.ShopID > 0 {
|
|
shopIDs[*card.ShopID] = struct{}{}
|
|
}
|
|
}
|
|
for _, record := range records {
|
|
if record == nil {
|
|
continue
|
|
}
|
|
if record.FromOwnerType == constants.OwnerTypeShop && record.FromOwnerID != nil {
|
|
shopIDs[*record.FromOwnerID] = struct{}{}
|
|
}
|
|
if record.ToOwnerType == constants.OwnerTypeShop && record.ToOwnerID > 0 {
|
|
shopIDs[record.ToOwnerID] = 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 cardTransferAuditReferences(card *model.IotCard, 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,
|
|
})
|
|
}
|
|
sourceShopID := card.ShopID
|
|
if record != nil && record.FromOwnerType == constants.OwnerTypeShop {
|
|
sourceShopID = record.FromOwnerID
|
|
}
|
|
if sourceShopID != nil && *sourceShopID > 0 {
|
|
resources = appendShopAuditReference(resources, shops[*sourceShopID], *sourceShopID, constants.AuditResourceRoleTransferSourceShop)
|
|
}
|
|
if record != nil && record.ToOwnerType == constants.OwnerTypeShop && record.ToOwnerID > 0 {
|
|
resources = appendShopAuditReference(resources, shops[record.ToOwnerID], record.ToOwnerID, constants.AuditResourceRoleTransferTargetShop)
|
|
} else if targetShopID != nil && *targetShopID > 0 {
|
|
resources = appendShopAuditReference(resources, shops[*targetShopID], *targetShopID, constants.AuditResourceRoleTransferTargetShop)
|
|
}
|
|
return resources
|
|
}
|
|
|
|
func appendShopAuditReference(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,
|
|
})
|
|
}
|
|
|
|
type cardBatchAuditItem struct {
|
|
Card *model.IotCard
|
|
PrimaryRole string
|
|
Result string
|
|
Summary string
|
|
BeforeData map[string]any
|
|
AfterData map[string]any
|
|
References []audit.ResourceInput
|
|
}
|
|
|
|
func (s *Service) appendCardBatchAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
rootAction, itemAction, kind, summary, result string,
|
|
batchTotal, successCount, failCount int,
|
|
items []cardBatchAuditItem,
|
|
metadata map[string]any,
|
|
businessErr error,
|
|
) error {
|
|
linkage := auditcontext.From(ctx)
|
|
if s.auditWriter == nil || linkage.RequestID == "" {
|
|
return errors.New(errors.CodeInvalidStatus, "IoT 卡批量审计上下文不完整")
|
|
}
|
|
errorCode, errorSummary := assetAuditSvc.BuildErrorInfo(businessErr)
|
|
children := make([]audit.AppendInput, 0, len(items))
|
|
for _, item := range items {
|
|
if item.Card == nil || item.Card.ID == 0 {
|
|
continue
|
|
}
|
|
cardID := strconv.FormatUint(uint64(item.Card.ID), 10)
|
|
primaryRole := item.PrimaryRole
|
|
if primaryRole == "" {
|
|
primaryRole = constants.AuditResourceRoleIotCardTransferTarget
|
|
}
|
|
resources := []audit.ResourceInput{{
|
|
Type: constants.AuditResourceIotCard, ID: &cardID,
|
|
Key: audit.IotCardResourceKey(item.Card), DisplayName: item.Card.ICCID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: primaryRole,
|
|
IdentitySnapshot: audit.IotCardIdentitySnapshot(item.Card), 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, childErrorSummary = errorCode, errorSummary
|
|
}
|
|
children = append(children, audit.AppendInput{
|
|
EventID: stableCardBatchEventID(kind+"-"+item.Result+"-card", linkage.RequestID+":"+cardID),
|
|
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: stableCardBatchEventID(kind+"-"+result, linkage.RequestID),
|
|
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.AuditResourceIotCardBatch, Key: linkage.RequestID, DisplayName: linkage.RequestID,
|
|
Relation: constants.AuditResourceRelationPrimary, Role: constants.AuditResourceRoleIotCardBatch,
|
|
IdentitySnapshot: map[string]any{"request_id": linkage.RequestID, "card_count": len(items)},
|
|
SubjectVisibility: constants.AuditSubjectInternalOnly,
|
|
}},
|
|
},
|
|
Children: children,
|
|
})
|
|
}
|
|
|
|
func (s *Service) recordCardTransferAuditFailure(
|
|
ctx context.Context,
|
|
rootAction, itemAction, kind, summary, result string,
|
|
cards []*model.IotCard,
|
|
outcomes map[uint]cardAuditOutcome,
|
|
newShopID *uint,
|
|
newStatus, batchTotal, successCount, failCount int,
|
|
businessErr error,
|
|
) {
|
|
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
|
recordCardAuditSecondaryFailure(ctx, rootAction, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡批量审计接缝未配置"))
|
|
return
|
|
}
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.appendCardTransferAudit(ctx, tx, rootAction, itemAction, kind, summary, result,
|
|
cards, outcomes, nil, newShopID, newStatus, batchTotal, successCount, failCount, businessErr)
|
|
}); err != nil {
|
|
recordCardAuditSecondaryFailure(ctx, rootAction, 0, businessErr, err)
|
|
}
|
|
}
|
|
|
|
func (s *Service) appendCardSeriesBindingAudit(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
cards []*model.IotCard,
|
|
outcomes map[uint]cardAuditOutcome,
|
|
seriesID *uint,
|
|
result string,
|
|
batchTotal, successCount, failCount int,
|
|
metadata map[string]any,
|
|
businessErr error,
|
|
) error {
|
|
series, err := loadCardSeriesAuditResources(ctx, tx, cards, seriesID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items := make([]cardBatchAuditItem, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card == nil || card.ID == 0 {
|
|
continue
|
|
}
|
|
outcome, ok := outcomes[card.ID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var afterData map[string]any
|
|
if outcome.Result == constants.AuditResultSuccess {
|
|
afterData = map[string]any{"series_id": seriesID}
|
|
}
|
|
references := cardSeriesAuditReferences(card.SeriesID, seriesID, series)
|
|
references = append(references, deviceReferences[card.ID]...)
|
|
items = append(items, cardBatchAuditItem{
|
|
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardSeriesTarget,
|
|
Result: outcome.Result, Summary: outcome.Summary,
|
|
BeforeData: map[string]any{"series_id": card.SeriesID}, AfterData: afterData,
|
|
References: references,
|
|
})
|
|
}
|
|
return s.appendCardBatchAudit(ctx, tx,
|
|
constants.AuditActionIotCardSeriesBindingBatch,
|
|
constants.AuditActionIotCardSeriesBound,
|
|
"series-binding", "批量设置 IoT 卡系列绑定", result,
|
|
batchTotal, successCount, failCount, items, metadata, businessErr)
|
|
}
|
|
|
|
func (s *Service) appendCardRealnamePolicyBatchAudit(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, policy string) error {
|
|
deviceReferences, err := loadCardDeviceAuditReferences(ctx, tx, cards)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items := make([]cardBatchAuditItem, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card == nil || card.ID == 0 || card.RealnamePolicy == policy {
|
|
continue
|
|
}
|
|
items = append(items, cardBatchAuditItem{
|
|
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardTarget,
|
|
Result: constants.AuditResultSuccess, Summary: "更新 IoT 卡实名策略",
|
|
BeforeData: map[string]any{"realname_policy": card.RealnamePolicy},
|
|
AfterData: map[string]any{"realname_policy": policy},
|
|
References: deviceReferences[card.ID],
|
|
})
|
|
}
|
|
return s.appendCardBatchAudit(ctx, tx,
|
|
constants.AuditActionIotCardRealnamePolicyBatchUpdated,
|
|
constants.AuditActionIotCardRealnamePolicyUpdated,
|
|
"realname-policy", "批量更新 IoT 卡实名策略", constants.AuditResultSuccess,
|
|
len(items), len(items), 0, items, map[string]any{"realname_policy": policy, "requested_count": len(cards)}, nil)
|
|
}
|
|
|
|
func (s *Service) recordCardRealnamePolicyBatchFailure(ctx context.Context, cards []*model.IotCard, policy, result string, businessErr error) {
|
|
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
|
return
|
|
}
|
|
items := make([]cardBatchAuditItem, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card == nil || card.ID == 0 {
|
|
continue
|
|
}
|
|
items = append(items, cardBatchAuditItem{
|
|
Card: card, PrimaryRole: constants.AuditResourceRoleIotCardTarget,
|
|
Result: result, Summary: "更新 IoT 卡实名策略未完成",
|
|
BeforeData: map[string]any{"realname_policy": card.RealnamePolicy},
|
|
AfterData: map[string]any{"requested_realname_policy": policy},
|
|
})
|
|
}
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.appendCardBatchAudit(ctx, tx,
|
|
constants.AuditActionIotCardRealnamePolicyBatchUpdated,
|
|
constants.AuditActionIotCardRealnamePolicyUpdated,
|
|
"realname-policy", "批量更新 IoT 卡实名策略未完成", result,
|
|
len(cards), 0, len(cards), items, map[string]any{"realname_policy": policy}, businessErr)
|
|
}); err != nil {
|
|
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardRealnamePolicyBatchUpdated, 0, businessErr, err)
|
|
}
|
|
}
|
|
|
|
func loadCardSeriesAuditResources(ctx context.Context, tx *gorm.DB, cards []*model.IotCard, targetSeriesID *uint) (map[uint]*model.PackageSeries, error) {
|
|
seriesIDs := make(map[uint]struct{})
|
|
if targetSeriesID != nil && *targetSeriesID > 0 {
|
|
seriesIDs[*targetSeriesID] = struct{}{}
|
|
}
|
|
for _, card := range cards {
|
|
if card != nil && card.SeriesID != nil && *card.SeriesID > 0 {
|
|
seriesIDs[*card.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 cardSeriesAuditReferences(previousID, targetID *uint, series map[uint]*model.PackageSeries) []audit.ResourceInput {
|
|
resources := make([]audit.ResourceInput, 0, 2)
|
|
if previousID != nil && *previousID > 0 {
|
|
resources = appendPackageSeriesAuditReference(resources, series[*previousID], *previousID, constants.AuditResourceRolePreviousPackageSeries)
|
|
}
|
|
if targetID != nil && *targetID > 0 {
|
|
resources = appendPackageSeriesAuditReference(resources, series[*targetID], *targetID, constants.AuditResourceRoleTargetPackageSeries)
|
|
}
|
|
return resources
|
|
}
|
|
|
|
func appendPackageSeriesAuditReference(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 loadCardDeviceAuditReferences(ctx context.Context, tx *gorm.DB, cards []*model.IotCard) (map[uint][]audit.ResourceInput, error) {
|
|
cardByID := make(map[uint]*model.IotCard, len(cards))
|
|
cardIDs := make([]uint, 0, len(cards))
|
|
for _, card := range cards {
|
|
if card != nil && card.ID > 0 {
|
|
cardByID[card.ID] = card
|
|
cardIDs = append(cardIDs, card.ID)
|
|
}
|
|
}
|
|
result := make(map[uint][]audit.ResourceInput)
|
|
if len(cardIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
var bindings []*model.DeviceSimBinding
|
|
if err := tx.WithContext(ctx).Where("iot_card_id IN ? AND bind_status = ?", cardIDs, 1).Find(&bindings).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
deviceIDs := make([]uint, 0, len(bindings))
|
|
for _, binding := range bindings {
|
|
deviceIDs = append(deviceIDs, binding.DeviceID)
|
|
}
|
|
var devices []*model.Device
|
|
if len(deviceIDs) > 0 {
|
|
if err := tx.WithContext(ctx).Unscoped().Where("id IN ?", deviceIDs).Find(&devices).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
deviceByID := make(map[uint]*model.Device, len(devices))
|
|
for _, device := range devices {
|
|
deviceByID[device.ID] = device
|
|
}
|
|
for _, binding := range bindings {
|
|
card := cardByID[binding.IotCardID]
|
|
device := deviceByID[binding.DeviceID]
|
|
bindingID := strconv.FormatUint(uint64(binding.ID), 10)
|
|
deviceID := strconv.FormatUint(uint64(binding.DeviceID), 10)
|
|
deviceKey, deviceName := deviceID, deviceID
|
|
deviceIdentity := map[string]any{"id": binding.DeviceID}
|
|
deviceVirtualNo := ""
|
|
if device != nil {
|
|
deviceVirtualNo = device.VirtualNo
|
|
if device.VirtualNo != "" {
|
|
deviceKey = device.VirtualNo
|
|
}
|
|
deviceName = device.DeviceName
|
|
if deviceName == "" {
|
|
deviceName = device.VirtualNo
|
|
}
|
|
deviceIdentity = map[string]any{"id": device.ID, "virtual_no": device.VirtualNo, "imei": device.IMEI, "sn": device.SN, "generation": device.Generation}
|
|
}
|
|
cardICCID, cardVirtualNo := "", ""
|
|
if card != nil {
|
|
cardICCID, cardVirtualNo = card.ICCID, card.VirtualNo
|
|
}
|
|
result[binding.IotCardID] = append(result[binding.IotCardID],
|
|
audit.ResourceInput{
|
|
Type: constants.AuditResourceDeviceSIMBinding, ID: &bindingID,
|
|
Key: bindingID, DisplayName: deviceVirtualNo,
|
|
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleIotCardDeviceBinding,
|
|
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,
|
|
},
|
|
audit.ResourceInput{
|
|
Type: constants.AuditResourceDevice, ID: &deviceID,
|
|
Key: deviceKey, DisplayName: deviceName,
|
|
Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleIotCardRelatedDevice,
|
|
IdentitySnapshot: deviceIdentity, SubjectVisibility: constants.AuditSubjectInternalOnly,
|
|
},
|
|
)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Service) recordCardSeriesBindingAuditFailure(
|
|
ctx context.Context,
|
|
cards []*model.IotCard,
|
|
outcomes map[uint]cardAuditOutcome,
|
|
seriesID *uint,
|
|
result string,
|
|
batchTotal, successCount, failCount int,
|
|
metadata map[string]any,
|
|
businessErr error,
|
|
) {
|
|
if s.db == nil || s.auditWriter == nil || len(cards) == 0 {
|
|
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSeriesBindingBatch, 0, businessErr, errors.New(errors.CodeInvalidStatus, "IoT 卡系列绑定审计接缝未配置"))
|
|
return
|
|
}
|
|
if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return s.appendCardSeriesBindingAudit(ctx, tx, cards, outcomes, seriesID, result,
|
|
batchTotal, successCount, failCount, metadata, businessErr)
|
|
}); err != nil {
|
|
recordCardAuditSecondaryFailure(ctx, constants.AuditActionIotCardSeriesBindingBatch, 0, businessErr, err)
|
|
}
|
|
}
|
|
|
|
func stableCardBatchEventID(kind, key string) string {
|
|
return "evt_" + uuid.NewSHA1(uuid.NameSpaceOID, []byte("iot-card:"+kind+":"+key)).String()
|
|
}
|
|
|
|
func recordCardAuditSecondaryFailure(ctx context.Context, actionCode string, cardID uint, businessErr, auditErr error) {
|
|
errorCode, _ := assetAuditSvc.BuildErrorInfo(businessErr)
|
|
linkage := auditcontext.From(ctx)
|
|
auditfailure.RecordSecondaryWriteFailure(
|
|
actionCode, strconv.FormatUint(uint64(cardID), 10), linkage.RequestID, linkage.CorrelationID, errorCode, auditErr,
|
|
)
|
|
}
|