Files
junhong_cmp_fiber/internal/service/exchange/phone_association.go
邹进 9ced14fa1b fix: 第1批 F1、M12 一资产一号与换货按手机号关联校验
- F1 一个资产同时只能有一个有效手机号关联,他号绑定返回 1192(不含号码),同号幂等
- 登录 need_bind_phone 改看资产是否已有有效关联,新增 asset_phone_bound、asset_phone_is_own;H5 绑定按客户/资产两侧状态分别写入
- 客户换号不再迁移资产关联
- M12 换货新资产有有效手机号关联即拒绝,号码只在 data.phone;换货完成清空旧资产客户绑定并失效旧资产关联(exchange_completed),均不迁移
- OpenSpec Change enforce-single-phone-per-asset:11/16,H5 短信与换货端到端验证待测试部署后完成
2026-10-12 00:10:41 +09:00

94 lines
4.5 KiB
Go

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,
})
}