package client_auth import ( "context" "sort" "gorm.io/gorm" "gorm.io/gorm/clause" 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/config" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // lockPhoneRowsInIDOrder 在事务内按手机号行 id ASC 固定顺序加锁。 // bind-phone 只锁一行、change-phone 锁两行;统一升序后两条路径不会形成 A→B / B→A 死锁环。 // 调用方必须已先取手机号 advisory lock:行锁无法覆盖「尚无手机号行」的新号。 func (s *Service) lockPhoneRowsInIDOrder(ctx context.Context, tx *gorm.DB, ids []uint) error { ordered := make([]uint, 0, len(ids)) seen := make(map[uint]struct{}, len(ids)) for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} ordered = append(ordered, id) } sort.Slice(ordered, func(i, j int) bool { return ordered[i] < ordered[j] }) for _, id := range ordered { var row model.PersonalCustomerPhone if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil { if err == gorm.ErrRecordNotFound { continue } return errors.Wrap(errors.CodeInternalError, err, "锁定手机号行失败") } } return nil } // phoneRowIDsByPhone 查询指定手机号当前启用的手机号行 id。 func (s *Service) phoneRowIDsByPhone(ctx context.Context, tx *gorm.DB, phones ...string) ([]uint, error) { var ids []uint if err := tx.WithContext(ctx).Model(&model.PersonalCustomerPhone{}). Where("phone IN ? AND status = ?", phones, 1). Order("id ASC").Pluck("id", &ids).Error; err != nil { return nil, errors.Wrap(errors.CodeInternalError, err, "查询手机号行失败") } return ids, nil } // requirePhoneBinding 返回全局强制绑定开关取值,默认开启。 // 开关关闭时本特性必须完全惰性:不查询、不建立、不迁移、不失效任何关联。 func requirePhoneBinding() bool { cfg := config.Get() if cfg == nil { return true } return cfg.Client.RequirePhoneBinding } // lockPhoneScopesInOrder 在事务内、任何行锁之前为手机号取稳定串行化点。 // 按号码字符串升序取事务级 advisory lock,等价于「手机号」这一逻辑实体的固定加锁次序。 func (s *Service) lockPhoneScopesInOrder(ctx context.Context, tx *gorm.DB, phones ...string) error { if err := s.associationStore.WithTx(tx).LockPhoneScopes(ctx, phones...); err != nil { return errors.Wrap(errors.CodeInternalError, err, "锁定手机号串行化点失败") } return nil } // establishAssociation 委托关联写入规则完成幂等建联与十项上限判定。 // 已建立有效关系时返回 false 且不产生第二条关系;无当前访问资产身份时不建立关联。 // 开关关闭时完全短路,只保留账号手机号绑定语义。 func (s *Service) establishAssociation( ctx context.Context, tx *gorm.DB, customer *model.PersonalCustomer, phone, assetType string, assetID uint, ) (bool, error) { if !requirePhoneBinding() { return false, nil } return s.associationWriter.Establish(ctx, tx, customer, phone, assetType, assetID) } // migrateAssociations 委托关联写入规则完成换绑原子迁移与上限/冲突判定。 // 开关关闭时完全短路,不迁移任何关联。 func (s *Service) migrateAssociations( ctx context.Context, tx *gorm.DB, oldPhone, newPhone string, ) ([]accessauditapp.PhoneAssetAssociationChange, error) { if !requirePhoneBinding() { return nil, nil } return s.associationWriter.Migrate(ctx, tx, oldPhone, newPhone) }