fix(代理分销注册): 验证码改为落库成功后消费并细化失败原因
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m54s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m54s
公开扫码注册原先在审批准备前就消费短信验证码,落库前的任何失败都会烧掉验证码, 客户重试只能得到统一的“分销码不可用”,掩盖了真实失败原因。 - 验证码校验与消费拆分为 CheckCode 与 ConsumeCode,注册记录与审批实例落库成功后才原子消费; 校验不消费、消费一次性,落库前失败时同一验证码可直接重试 - 分销码无效、所属店铺已停用、上级店铺缺少启用的主账号、验证码错误分别返回各自错误码与提示 - 注册必填字段缺失与请求参数校验失败提示定位到具体字段 - 同步公开接口描述与 agent-distribution-withdrawal 主 Spec 行为契约
This commit is contained in:
@@ -2,6 +2,7 @@ package distributionwithdrawal
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -45,7 +46,8 @@ func NewRegistrationService(
|
||||
}
|
||||
|
||||
// Register 创建待审批注册记录。
|
||||
// 无效分销码、停用上级、验证码无效或已消费统一返回“分销码不可用”,且不落库。
|
||||
// 分销码无效、上级店铺停用、上级店铺缺少启用的主账号、短信验证码无效分别返回各自的错误码与提示。
|
||||
// 短信验证码只在注册记录与审批实例落库成功后消费:落库前的任何失败都不消费验证码,重试无需重新获取。
|
||||
// 手机号、用户名或店铺编号与既有账号/店铺重复时返回稳定冲突错误。
|
||||
func (s *RegistrationService) Register(
|
||||
ctx context.Context,
|
||||
@@ -61,24 +63,19 @@ func (s *RegistrationService) Register(
|
||||
}
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "分销码不可用")
|
||||
return nil, errors.New(errors.CodeInvalidParam, "短信验证码不能为空")
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(normalized.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.CodeInternalError, err, "密码哈希失败")
|
||||
}
|
||||
var parent *model.Shop
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("distribution_code = ? AND status = ?", normalized.DistributionCode, constants.ShopStatusEnabled).
|
||||
First(&parent).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "分销码不可用")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询分销码所属店铺失败")
|
||||
parent, err := s.findDistributionParent(ctx, normalized.DistributionCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 验证码校验成功即消费;无效或已消费与无效分销码返回同一对外结果。
|
||||
if err := s.verifier.VerifyCode(ctx, normalized.Phone, code); err != nil {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "分销码不可用")
|
||||
// 只校验不消费:验证码在注册记录与审批实例落库成功后才消费,落库前的失败不消耗验证码。
|
||||
if err := s.verifier.CheckCode(ctx, normalized.Phone, code); err != nil {
|
||||
return nil, verificationFailure(err)
|
||||
}
|
||||
submitter, err := resolveRegistrationSubmitter(ctx, s.db, parent.ID)
|
||||
if err != nil {
|
||||
@@ -132,9 +129,39 @@ func (s *RegistrationService) Register(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 注册记录与审批实例已落库:此后消费验证码失败只记录日志,不回滚既有事实,也不改变对外成功结果。
|
||||
// 消费失败(验证码已过期或已被并发的另一次提交消费)由验证码实现侧记录。
|
||||
_ = s.verifier.ConsumeCode(ctx, normalized.Phone, code)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// findDistributionParent 按分销码定位上级店铺;未命中与已停用返回各自的可定位错误。
|
||||
// 软删除店铺不参与匹配,与店铺唯一索引的生效范围一致。
|
||||
func (s *RegistrationService) findDistributionParent(ctx context.Context, distributionCode string) (*model.Shop, error) {
|
||||
var parent model.Shop
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("distribution_code = ?", distributionCode).
|
||||
First(&parent).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "分销码无效")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询分销码所属店铺失败")
|
||||
}
|
||||
if parent.Status != constants.ShopStatusEnabled {
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "分销码所属店铺已停用")
|
||||
}
|
||||
return &parent, nil
|
||||
}
|
||||
|
||||
// verificationFailure 把验证码校验失败转换为对外错误,保留验证码服务给出的可定位提示。
|
||||
func verificationFailure(err error) error {
|
||||
var appErr *errors.AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
return errors.Wrap(errors.CodeVerificationCodeInvalid, err, appErr.Message)
|
||||
}
|
||||
return errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||||
}
|
||||
|
||||
// resolveRegistrationSubmitter 解析扫码注册的审批发起身份。
|
||||
// 公开接口没有登录账号,使用分销码所属店铺的启用主账号作为发起主体;
|
||||
// 该账号非平台/超管身份,企业微信侧按既有规则回落到应用默认审批发起人。
|
||||
@@ -144,7 +171,7 @@ func resolveRegistrationSubmitter(ctx context.Context, db *gorm.DB, parentShopID
|
||||
Where("shop_id = ? AND status = ? AND is_primary = TRUE", parentShopID, constants.StatusEnabled).
|
||||
First(&account).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "分销码不可用")
|
||||
return nil, errors.New(errors.CodeInvalidStatus, "上级店铺未配置启用的主账号,请联系平台处理")
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询上级店铺主账号失败")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user