Files
junhong_cmp_fiber/internal/service/client_auth/phone_asset_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

101 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package client_auth
import (
"context"
"sort"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"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)
}
// assetHasValidAssociation 判断当前访问资产是否已有任一有效手机号关联;开关关闭或无资产身份时返回 false。
func (s *Service) assetHasValidAssociation(ctx context.Context, tx *gorm.DB, assetType string, assetID uint) (bool, error) {
if !requirePhoneBinding() || assetType == "" || assetID == 0 {
return false, nil
}
phones, err := s.associationStore.WithTx(tx).ListValidByAsset(ctx, assetType, assetID)
if err != nil {
return false, errors.Wrap(errors.CodeInternalError, err, "查询资产手机号关联失败")
}
return len(phones) > 0, nil
}