package exchange import ( "context" "time" "gorm.io/gorm" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" "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/middleware" "github.com/break/junhong_cmp_fiber/pkg/sanitizer" ) // exchangeNewAssetPhoneBoundMessage 新资产已绑定手机号时的拒绝提示,不含号码。 const exchangeNewAssetPhoneBoundMessage = "新资产已绑定手机号,请先在后台解除该资产的手机号绑定,再重新提交换货申请" // SetPhoneAssetAssociationStore 注入手机号—资产关联存储,用于换货校验与换货完成失效旧资产关联。 func (s *Service) SetPhoneAssetAssociationStore(store *postgres.PhoneAssetAssociationStore) { s.phoneAssociationStore = store } // ensureNewAssetPhoneUnboundWithTx 新资产存在有效手机号关联时拒绝换货。 // 完整号码只放在错误响应 data.phone 中,错误信息与日志不含号码。 func (s *Service) ensureNewAssetPhoneUnboundWithTx(ctx context.Context, tx *gorm.DB, newAsset *resolvedExchangeAsset) error { if s.phoneAssociationStore == nil { return errors.New(errors.CodeInvalidStatus, "手机号资产关联存储未配置") } phones, err := s.phoneAssociationStore.WithTx(tx).ListValidByAsset(ctx, newAsset.AssetType, newAsset.AssetID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询新资产手机号关联失败") } if len(phones) == 0 { return nil } return errors.NewWithData(errors.CodeExchangeStatusInvalid, map[string]any{"phone": phones[0]}, exchangeNewAssetPhoneBoundMessage) } // invalidateOldAssetPhoneAssociationsWithTx 换货完成时在同一事务内失效旧资产全部有效手机号关联,释放号码名额。 // 旧资产无有效关联时跳过;操作人为确认换货完成的账号,审计只写脱敏手机号。 func (s *Service) invalidateOldAssetPhoneAssociationsWithTx(ctx context.Context, tx *gorm.DB, oldAsset *resolvedExchangeAsset) error { if s.phoneAssociationStore == nil { return errors.New(errors.CodeInvalidStatus, "手机号资产关联存储未配置") } store := s.phoneAssociationStore.WithTx(tx) rows, err := store.LockValidByAsset(ctx, tx, oldAsset.AssetType, oldAsset.AssetID) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定旧资产手机号关联失败") } if len(rows) == 0 { return nil } if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "换货审计接缝未配置") } ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ID) } now := time.Now() method, reason := constants.PhoneAssetAssociationInvalidateMethodExchangeCompleted, constants.PhoneAssetAssociationExchangeCompletedReason operatorID, operatorName := middleware.GetUserIDFromContext(ctx), middleware.GetUsernameFromContext(ctx) affected, err := store.InvalidateByIDs(ctx, tx, ids, method, reason, operatorID, operatorName, now) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "失效旧资产手机号关联失败") } if int(affected) != len(rows) { return errors.New(errors.CodeConflict, "旧资产手机号关联已变化,请重试") } changes := make([]accessauditapp.PhoneAssetAssociationChange, 0, len(rows)) for _, row := range rows { changes = append(changes, accessauditapp.PhoneAssetAssociationChange{ AssociationID: row.ID, PhoneMasked: sanitizer.MaskPhone(row.Phone), AssetType: row.AssetType, AssetID: row.AssetID, AssetDisplayName: oldAsset.Identifier, Status: constants.PhoneAssetAssociationStatusInvalid, Source: row.Source, InvalidatedAt: &now, InvalidationMethod: method, InvalidationReason: reason, BeforeData: map[string]any{"status": constants.PhoneAssetAssociationStatusValid}, AfterData: map[string]any{ "status": constants.PhoneAssetAssociationStatusInvalid, "invalidated_at": now, "invalidation_method": method, "invalidation_reason": reason, }, }) } return s.auditWriter.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionPhoneAssetAssociationUnbound, Summary: "换货完成失效旧资产手机号关联", OperatorID: operatorID, ActorKind: constants.AuditActorAccount, ActorName: operatorName, Source: constants.AuditSourceAdminAPI, ScopeType: constants.AuditScopePlatform, PhoneAssociations: changes, }) }