feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史 - H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列 - 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案 - 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容 - 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏 - 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
This commit is contained in:
102
internal/service/client_auth/phone_asset_association.go
Normal file
102
internal/service/client_auth/phone_asset_association.go
Normal file
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||||
associationSvc "github.com/break/junhong_cmp_fiber/internal/service/phone_asset_association"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/verification"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -46,6 +48,7 @@ type Service struct {
|
||||
openidStore *postgres.PersonalCustomerOpenIDStore
|
||||
customerStore *postgres.PersonalCustomerStore
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore
|
||||
associationStore *postgres.PhoneAssetAssociationStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
wechatConfigService *wechatConfigSvc.Service
|
||||
@@ -56,6 +59,7 @@ type Service struct {
|
||||
wechatCache kernel.CacheInterface
|
||||
customerBinding *customerBinding.Service
|
||||
accessAudit accessauditapp.Writer
|
||||
associationWriter *associationSvc.AssociationWriter
|
||||
}
|
||||
|
||||
// New 创建 C 端认证服务实例
|
||||
@@ -64,6 +68,7 @@ func New(
|
||||
openidStore *postgres.PersonalCustomerOpenIDStore,
|
||||
customerStore *postgres.PersonalCustomerStore,
|
||||
phoneStore *postgres.PersonalCustomerPhoneStore,
|
||||
associationStore *postgres.PhoneAssetAssociationStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
wechatConfigService *wechatConfigSvc.Service,
|
||||
@@ -79,6 +84,7 @@ func New(
|
||||
openidStore: openidStore,
|
||||
customerStore: customerStore,
|
||||
phoneStore: phoneStore,
|
||||
associationStore: associationStore,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
wechatConfigService: wechatConfigService,
|
||||
@@ -89,6 +95,7 @@ func New(
|
||||
wechatCache: wechat.NewRedisCache(redisClient),
|
||||
customerBinding: binding,
|
||||
accessAudit: accessAudit,
|
||||
associationWriter: associationSvc.NewAssociationWriter(associationStore, accessAudit),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,26 +298,28 @@ func (s *Service) SendCode(ctx context.Context, req *dto.ClientSendCodeRequest,
|
||||
}
|
||||
|
||||
// BindPhone A5 绑定手机号
|
||||
func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindPhoneRequest) (*dto.BindPhoneResponse, error) {
|
||||
// POST /api/c/v1/auth/bind-phone
|
||||
// 无主手机号时建立账号手机号;已有主手机号且提交号码与主号一致、验证码有效时,
|
||||
// 幂等建立当前访问资产与该手机号的关联且不修改账号手机号;提交号码与主号不一致仍拒绝。
|
||||
// 请求不含当前访问资产身份时只完成账号手机号绑定,不建立关联。
|
||||
func (s *Service) BindPhone(ctx context.Context, customerID uint, assetType string, assetID uint, req *dto.BindPhoneRequest) (*dto.BindPhoneResponse, error) {
|
||||
if req == nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if s.db == nil || s.accessAudit == nil {
|
||||
if s.db == nil || s.accessAudit == nil || s.associationStore == nil {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "个人客户审计接缝未配置")
|
||||
}
|
||||
if _, err := s.phoneStore.GetPrimaryPhone(ctx, customerID); err == nil {
|
||||
appErr := errors.New(errors.CodeAlreadyBoundPhone)
|
||||
if customer, loadErr := s.customerStore.GetByID(ctx, customerID); loadErr == nil {
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, nil, appErr)
|
||||
}
|
||||
return nil, appErr
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
}
|
||||
customer, err := s.customerStore.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
primary, primaryErr := s.phoneStore.GetPrimaryPhone(ctx, customerID)
|
||||
if primaryErr == nil {
|
||||
return s.bindExistingPrimaryPhone(ctx, customer, primary, assetType, assetID, req)
|
||||
}
|
||||
if primaryErr != gorm.ErrRecordNotFound {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, primaryErr, "查询主手机号失败")
|
||||
}
|
||||
if err := s.verificationService.VerifyCode(ctx, req.Phone, req.Code); err != nil {
|
||||
appErr := errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, nil, appErr)
|
||||
@@ -338,6 +347,10 @@ func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindP
|
||||
Status: 1,
|
||||
}
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 固定加锁次序:先取手机号串行化点(advisory),再按 id ASC 锁行。
|
||||
if err := s.lockPhoneScopesInOrder(ctx, tx, req.Phone); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(customer, customerID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询个人客户失败")
|
||||
}
|
||||
@@ -361,10 +374,17 @@ func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindP
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "创建手机号绑定记录失败")
|
||||
}
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
if err := s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号", customer, record, nil,
|
||||
map[string]any{"phone": record.Phone}, "手机号已绑定", constants.AuditResultSuccess,
|
||||
))
|
||||
map[string]any{"phone": sanitizer.MaskPhone(record.Phone)}, "手机号已绑定", constants.AuditResultSuccess,
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
// 建联与账号手机号同事务:超限或写入失败时账号手机号一并回滚。
|
||||
if _, err := s.establishAssociation(ctx, tx, customer, record.Phone, assetType, assetID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号失败", customer, nil, err)
|
||||
@@ -377,6 +397,63 @@ func (s *Service) BindPhone(ctx context.Context, customerID uint, req *dto.BindP
|
||||
}, nil
|
||||
}
|
||||
|
||||
// bindExistingPrimaryPhone 处理已有主手机号的绑定请求。
|
||||
// 提交号码等于主号且验证码有效时幂等建立关联且不改账号手机号;号码不一致仍拒绝换号。
|
||||
func (s *Service) bindExistingPrimaryPhone(
|
||||
ctx context.Context,
|
||||
customer *model.PersonalCustomer,
|
||||
primary *model.PersonalCustomerPhone,
|
||||
assetType string,
|
||||
assetID uint,
|
||||
req *dto.BindPhoneRequest,
|
||||
) (*dto.BindPhoneResponse, error) {
|
||||
if primary.Phone != req.Phone {
|
||||
appErr := errors.New(errors.CodeAlreadyBoundPhone)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, primary, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
if err := s.verificationService.VerifyCode(ctx, req.Phone, req.Code); err != nil {
|
||||
appErr := errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号被拒绝", customer, primary, appErr)
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 固定加锁次序:先取手机号串行化点(advisory),再按 id ASC 锁行。
|
||||
if err := s.lockPhoneScopesInOrder(ctx, tx, primary.Phone); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.lockPhoneRowsInIDOrder(ctx, tx, []uint{primary.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
var locked model.PersonalCustomerPhone
|
||||
if err := tx.WithContext(ctx).
|
||||
Where("id = ? AND customer_id = ? AND is_primary = ? AND status = ?", primary.ID, customer.ID, true, 1).
|
||||
First(&locked).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeAlreadyBoundPhone)
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询主手机号失败")
|
||||
}
|
||||
if locked.Phone != req.Phone {
|
||||
// 并发换绑已把主号改成其他号码:提交号码不再等于账号手机号,按既有语义拒绝。
|
||||
return errors.New(errors.CodeAlreadyBoundPhone)
|
||||
}
|
||||
_, err := s.establishAssociation(ctx, tx, customer, locked.Phone, assetType, assetID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
s.recordPersonalFailure(ctx, constants.AuditActionPersonalCustomerPhoneBound, "绑定个人手机号失败", customer, primary, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.BindPhoneResponse{
|
||||
Phone: req.Phone,
|
||||
BoundAt: now.Format("2006-01-02 15:04:05"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangePhone A6 换绑手机号
|
||||
func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.ChangePhoneRequest) (*dto.ChangePhoneResponse, error) {
|
||||
if req == nil {
|
||||
@@ -421,8 +498,22 @@ func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.Cha
|
||||
now := time.Now()
|
||||
var beforeData map[string]any
|
||||
var failurePhone *model.PersonalCustomerPhone
|
||||
var migrationChanges []accessauditapp.PhoneAssetAssociationChange
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
// 统一加锁顺序:先按手机号行 id ASC 锁定旧、新手机号行,再由关联迁移按 id ASC 锁定两侧有效关系行。
|
||||
// 换绑两行的加锁顺序与 bind-phone 的单行加锁一致,两条路径不会形成 A→B / B→A 死锁环。
|
||||
if idErr := s.lockPhoneScopesInOrder(ctx, tx, primary.Phone, req.NewPhone); idErr != nil {
|
||||
return idErr
|
||||
}
|
||||
phoneIDs, idErr := s.phoneRowIDsByPhone(ctx, tx, primary.Phone, req.NewPhone)
|
||||
if idErr != nil {
|
||||
return idErr
|
||||
}
|
||||
if idErr := s.lockPhoneRowsInIDOrder(ctx, tx, phoneIDs); idErr != nil {
|
||||
return idErr
|
||||
}
|
||||
// 手机号行已在本事务内锁定,此处只复核主号归属与号码,不重复加锁。
|
||||
if err := tx.WithContext(ctx).
|
||||
Where("id = ? AND customer_id = ? AND is_primary = ? AND status = ?", primary.ID, customerID, true, 1).
|
||||
First(primary).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -435,13 +526,19 @@ func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.Cha
|
||||
}
|
||||
current := *primary
|
||||
failurePhone = ¤t
|
||||
beforeData = map[string]any{"phone": primary.Phone}
|
||||
beforeData = map[string]any{"phone": sanitizer.MaskPhone(primary.Phone)}
|
||||
var existed model.PersonalCustomerPhone
|
||||
if err := tx.Where("phone = ? AND status = ?", req.NewPhone, 1).First(&existed).Error; err == nil && existed.CustomerID != customerID {
|
||||
return errors.New(errors.CodePhoneAlreadyBound)
|
||||
} else if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "查询新手机号绑定关系失败")
|
||||
}
|
||||
// 关联迁移先于账号手机号改写:超限或与既有关系冲突时整次失败,旧、新关系均保持原状。
|
||||
changes, migrateErr := s.migrateAssociations(ctx, tx, primary.Phone, req.NewPhone)
|
||||
if migrateErr != nil {
|
||||
return migrateErr
|
||||
}
|
||||
migrationChanges = changes
|
||||
if err := tx.Model(primary).Updates(map[string]any{
|
||||
"phone": req.NewPhone,
|
||||
"verified_at": now,
|
||||
@@ -451,10 +548,22 @@ func (s *Service) ChangePhone(ctx context.Context, customerID uint, req *dto.Cha
|
||||
}
|
||||
primary.Phone = req.NewPhone
|
||||
primary.VerifiedAt = &now
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
if err := s.accessAudit.WriteAccessChange(ctx, tx, personalPhoneAudit(
|
||||
constants.AuditActionPersonalCustomerPhoneChanged, "更换个人手机号", customer, primary, beforeData,
|
||||
map[string]any{"phone": primary.Phone}, "手机号已更换", constants.AuditResultSuccess,
|
||||
))
|
||||
map[string]any{"phone": sanitizer.MaskPhone(primary.Phone)}, "手机号已更换", constants.AuditResultSuccess,
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(migrationChanges) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{
|
||||
ActionCode: constants.AuditActionPhoneAssetAssociationMigrated,
|
||||
Summary: "换绑手机号并迁移资产关联",
|
||||
OperatorID: customer.ID, ActorKind: constants.AuditActorPersonalCustomer, ActorName: customer.Nickname,
|
||||
Source: constants.AuditSourcePersonalAPI, ScopeType: constants.AuditScopePersonalCustomer,
|
||||
PhoneAssociations: migrationChanges,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
if failurePhone == nil {
|
||||
@@ -889,6 +998,11 @@ func (s *Service) bindAsset(ctx context.Context, tx *gorm.DB, customerID uint, a
|
||||
return s.customerBinding.Bind(ctx, tx, customerID, assetType, assetID)
|
||||
}
|
||||
|
||||
// issueLoginToken 签发登录令牌并判定是否需要手机号验证。
|
||||
// 三支判定:无主手机号 → true;有主手机号但未与当前访问资产存在有效关系 → true;
|
||||
// 已存在有效关系 → false。全局开关关闭时恒为 false,且不查询、不创建、不删除任何关系。
|
||||
// 关联查询使用当前访问资产的 asset_type/asset_id,不使用 phone claim:phone 是登录时快照,
|
||||
// 换绑后到下次登录前仍是旧号。该字段只作前端提示,不改变任何资源授权。
|
||||
func (s *Service) issueLoginToken(ctx context.Context, customerID uint, assetType string, assetID uint) (string, bool, error) {
|
||||
// 查询用户已绑定的主手机号,写入 JWT,供后续接口直接从 context 取用
|
||||
var boundPhone string
|
||||
@@ -903,8 +1017,19 @@ func (s *Service) issueLoginToken(ctx context.Context, customerID uint, assetTyp
|
||||
boundPhone = primaryPhone.Phone
|
||||
} else if phoneErr != gorm.ErrRecordNotFound {
|
||||
return "", false, errors.Wrap(errors.CodeInternalError, phoneErr, "查询手机号绑定关系失败")
|
||||
} else if requirePhoneBinding {
|
||||
needBindPhone = true
|
||||
}
|
||||
|
||||
// 开关关闭必须完全短路:既不查询也不写任何关系,否则会出现「关闭开关却写库」的越权写入。
|
||||
if requirePhoneBinding {
|
||||
if boundPhone == "" {
|
||||
needBindPhone = true
|
||||
} else {
|
||||
associated, err := s.associationStore.ExistsValid(ctx, boundPhone, assetType, assetID)
|
||||
if err != nil {
|
||||
return "", false, errors.Wrap(errors.CodeInternalError, err, "查询手机号资产关联失败")
|
||||
}
|
||||
needBindPhone = !associated
|
||||
}
|
||||
}
|
||||
|
||||
token, err := s.jwtManager.GeneratePersonalCustomerToken(customerID, boundPhone, assetType, assetID)
|
||||
|
||||
Reference in New Issue
Block a user