All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m41s
468 lines
22 KiB
Go
468 lines
22 KiB
Go
package distributionwithdrawal
|
||
|
||
import (
|
||
"context"
|
||
stderrors "errors"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/google/uuid"
|
||
"golang.org/x/crypto/bcrypt"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
|
||
approvalapp "github.com/break/junhong_cmp_fiber/internal/application/approval"
|
||
distributiondomain "github.com/break/junhong_cmp_fiber/internal/domain/distribution"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||
)
|
||
|
||
// RegistrationResult 返回已落库的待审批注册记录与审批实例引用。
|
||
type RegistrationResult struct {
|
||
RegistrationID uint
|
||
Status int
|
||
ApprovalInstanceID uint
|
||
ApprovalStatus int
|
||
}
|
||
|
||
// RegistrationService 受理公开扫码注册。
|
||
// 只创建待审批注册记录与审批实例,不创建店铺、账号、钱包或上下级归属。
|
||
type RegistrationService struct {
|
||
db *gorm.DB
|
||
verifier VerificationCodeVerifier
|
||
approval approvalapp.Port
|
||
recovery approvalapp.RecoveryPort
|
||
audit AuditWriter
|
||
}
|
||
|
||
// NewRegistrationService 创建公开扫码注册用例。
|
||
func NewRegistrationService(db *gorm.DB, verifier VerificationCodeVerifier, approval approvalapp.Port, audit AuditWriter) *RegistrationService {
|
||
return &RegistrationService{db: db, verifier: verifier, approval: approval, audit: audit}
|
||
}
|
||
|
||
// SetRecoveryPort 注入通用审批原实例恢复接缝。
|
||
func (s *RegistrationService) SetRecoveryPort(recovery approvalapp.RecoveryPort) {
|
||
s.recovery = recovery
|
||
}
|
||
|
||
// RecoverApproval 恢复代理注册原审批提交事实,不创建新审批实例或业务实体。
|
||
func (s *RegistrationService) RecoverApproval(ctx context.Context, id uint) error {
|
||
userType := middleware.GetUserTypeFromContext(ctx)
|
||
if userType == constants.UserTypeEnterprise || (userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent) {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
if middleware.GetUserIDFromContext(ctx) == 0 {
|
||
return errors.New(errors.CodeUnauthorized)
|
||
}
|
||
if s == nil || s.db == nil || s.recovery == nil {
|
||
return errors.New(errors.CodeServiceUnavailable, "代理注册审批恢复能力尚未配置")
|
||
}
|
||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
registration, err := lockRegistrationForUpdate(ctx, tx, id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if registration.Status != constants.AgentDistributionRegistrationStatusPending {
|
||
return errors.New(errors.CodeConflict, "终态申请不可恢复")
|
||
}
|
||
var parent model.Shop
|
||
if err := tx.Where("id = ?", registration.ParentShopID).First(&parent).Error; err != nil || parent.Status != constants.StatusEnabled {
|
||
return errors.New(errors.CodeConflict, "上级店铺已停用,不能恢复审批")
|
||
}
|
||
if userType == constants.UserTypeAgent && (middleware.GetShopIDFromContext(ctx) == 0 || middleware.GetShopIDFromContext(ctx) != registration.ParentShopID) {
|
||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||
}
|
||
if registration.ApprovalInstanceID == nil || *registration.ApprovalInstanceID == 0 {
|
||
return errors.New(errors.CodeConflict, "注册申请未关联审批实例")
|
||
}
|
||
instanceID := *registration.ApprovalInstanceID
|
||
var instance model.ApprovalInstance
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND business_type = ? AND business_id = ?", instanceID, constants.ApprovalBusinessTypeAgentDistribution, registration.ID).First(&instance).Error; err != nil {
|
||
return errors.New(errors.CodeConflict, "审批实例业务关联不一致")
|
||
}
|
||
var wc model.WeComApprovalContext
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("approval_instance_id = ? AND business_type = ?", instanceID, constants.ApprovalBusinessTypeAgentDistribution).First(&wc).Error; err != nil {
|
||
return errors.New(errors.CodeConflict, "企业微信审批上下文不存在")
|
||
}
|
||
branch := "active"
|
||
var recoveryErr error
|
||
if wc.SPNo != "" {
|
||
branch = "sp_no"
|
||
recoveryErr = s.recovery.EnqueueSubmittedSync(ctx, tx, instanceID)
|
||
} else if wc.SubmissionStatus == constants.WeComSubmissionStatusUnknown || instance.Status == constants.ApprovalStatusSubmissionUnknown {
|
||
branch = "unknown"
|
||
recoveryErr = s.recovery.EnqueueUnknownConfirm(ctx, tx, instanceID)
|
||
} else if instance.Status == constants.ApprovalStatusSubmissionFailed && wc.SubmissionStatus == constants.WeComSubmissionStatusFailed {
|
||
branch = "replay"
|
||
_, recoveryErr = s.recovery.RecoverSubmissionEvent(ctx, tx, instanceID)
|
||
} else if instance.Status == constants.ApprovalStatusSubmitting || instance.Status == constants.ApprovalStatusPending || wc.SubmissionStatus == constants.WeComSubmissionStatusReady || wc.SubmissionStatus == constants.WeComSubmissionStatusSending {
|
||
} else {
|
||
return errors.New(errors.CodeConflict, "当前审批提交状态不允许恢复")
|
||
}
|
||
if recoveryErr != nil {
|
||
return errors.Wrap(errors.CodeConflict, recoveryErr, "恢复审批提交失败")
|
||
}
|
||
if s.audit != nil {
|
||
if err := s.audit.WriteDistributionWithdrawal(ctx, tx, AuditChange{EventID: "agent-distribution:" + uintText(id) + ":recover:" + branch, ActionCode: constants.AuditActionApprovalSubmissionRecovered, Summary: "人工恢复代理注册审批提交", Registration: registration, ParentShop: &parent, BeforeData: map[string]any{"submission_status": wc.SubmissionStatus, "branch": branch}, AfterData: map[string]any{"result": "accepted"}}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// 已通过或已驳回的终态记录不阻塞重新注册;同一关键字段的并发提交由事务级 advisory lock 串行裁决。
|
||
func (s *RegistrationService) Register(
|
||
ctx context.Context,
|
||
input distributiondomain.RegistrationInput,
|
||
code string,
|
||
) (*RegistrationResult, error) {
|
||
if s == nil || s.db == nil || s.verifier == nil || s.approval == nil || s.audit == nil {
|
||
return nil, errors.New(errors.CodeServiceUnavailable, "代理分销注册能力尚未配置")
|
||
}
|
||
normalized, err := distributiondomain.ValidateRegistrationInput(input)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
code = strings.TrimSpace(code)
|
||
if code == "" {
|
||
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, "密码哈希失败")
|
||
}
|
||
parent, err := s.findDistributionParent(ctx, normalized.DistributionCode)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 只校验不消费:验证码在注册记录与审批实例落库成功后才消费,落库前的失败不消耗验证码。
|
||
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 {
|
||
return nil, err
|
||
}
|
||
correlationID := "agent_distribution:registration:" + uuid.NewString()
|
||
preparation, err := s.approval.Prepare(ctx, approvalapp.PrepareRequest{
|
||
BusinessType: constants.ApprovalBusinessTypeAgentDistribution,
|
||
SubmitterAccountID: submitter.ID, CorrelationID: correlationID,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
registration := &model.AgentDistributionRegistration{
|
||
DistributionCode: normalized.DistributionCode, ParentShopID: parent.ID,
|
||
Phone: normalized.Phone, PasswordHash: string(passwordHash),
|
||
ShopName: normalized.ShopName, ShopCode: normalized.ShopCode, Username: normalized.Username,
|
||
ContactName: normalized.ContactName, Province: normalized.Province,
|
||
City: normalized.City, District: normalized.District, Address: normalized.Address,
|
||
Status: constants.AgentDistributionRegistrationStatusPending,
|
||
}
|
||
submitterSnapshot, requestSnapshot, err := approvalSnapshots(submitter.ID, submitter.Username,
|
||
registrationApprovalForm(normalized, parent, s.parentBusinessOwnerName(ctx, parent)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
result := &RegistrationResult{}
|
||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||
// 关键字段门禁与注册记录插入必须同处一个串行化区间:先取 advisory lock,
|
||
// 再在同一事务内校验并落库,避免并发提交落下两条指向同一手机号/用户名/店铺编号的待审批申请。
|
||
if err := lockRegistrationKeyScopes(ctx, tx, normalized.Phone, normalized.Username, normalized.ShopCode); err != nil {
|
||
return err
|
||
}
|
||
if err := ensureRegistrationKeysAvailable(ctx, tx, normalized.Phone, normalized.Username, normalized.ShopCode); err != nil {
|
||
return err
|
||
}
|
||
if err := ensureNoPendingRegistration(ctx, tx, normalized.Phone, normalized.Username, normalized.ShopCode); err != nil {
|
||
return err
|
||
}
|
||
if err := tx.WithContext(ctx).Create(registration).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "创建待审批注册记录失败")
|
||
}
|
||
reference, err := createApprovalInTx(ctx, tx, s.approval, preparation,
|
||
constants.ApprovalBusinessTypeAgentDistribution, registration.ID, submitter.ID,
|
||
submitterSnapshot, requestSnapshot, correlationID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := attachRegistrationInstance(ctx, tx, registration, reference.InstanceID); err != nil {
|
||
return err
|
||
}
|
||
// 公开注册提交不写审计:该链路在 personal.go 的 Use() 之前注册,不经任何认证中间件,
|
||
// 因而没有可信的 actor/source(Append 会以「审计操作者或入口不符合动作注册规则」失败)。
|
||
// tasks 1.7 只要求分销码生成、注册通过/驳回与资格相关审计,提交动作不在其列,
|
||
// 故移除该非必需审计而不是伪造操作者身份。
|
||
result.RegistrationID = registration.ID
|
||
result.Status = registration.Status
|
||
result.ApprovalInstanceID = reference.InstanceID
|
||
result.ApprovalStatus = reference.Status
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 注册记录与审批实例已落库:此后消费验证码失败只记录日志,不回滚既有事实,也不改变对外成功结果。
|
||
// 消费失败(验证码已过期或已被并发的另一次提交消费)由验证码实现侧记录。
|
||
_ = s.verifier.ConsumeCode(ctx, normalized.Phone, code)
|
||
return result, nil
|
||
}
|
||
|
||
// registrationKeyScopePrefix 是注册关键字段串行化点的键前缀,与其它用例的 advisory lock 键空间隔离。
|
||
const registrationKeyScopePrefix = "agent-distribution-registration:"
|
||
|
||
// lockRegistrationKeyScopes 在事务内为注册关键字段(手机号、用户名、店铺编号)取稳定串行化点。
|
||
// 目标关键字段的待审批记录可能尚不存在,行锁无法覆盖「首次并发提交」,
|
||
// 因此按 key 字符串升序取事务级 advisory lock;升序保证并发提交不会形成 A→B / B→A 死锁环。
|
||
// 锁随本次事务提交或回滚自动释放。
|
||
func lockRegistrationKeyScopes(ctx context.Context, tx *gorm.DB, phone, username, shopCode string) error {
|
||
keys := []string{
|
||
registrationKeyScopePrefix + "phone:" + phone,
|
||
registrationKeyScopePrefix + "username:" + username,
|
||
registrationKeyScopePrefix + "shop_code:" + shopCode,
|
||
}
|
||
slices.Sort(keys)
|
||
for _, key := range keys {
|
||
if err := tx.WithContext(ctx).Exec("SELECT pg_advisory_xact_lock(hashtext(?))", key).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定注册关键字段串行化点失败")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ensureRegistrationKeysAvailable 校验注册关键字段未被既有账号或店铺占用。
|
||
// 手机号与用户名对应 tb_account 的条件唯一索引,店铺编号对应 tb_shop 的条件唯一索引;
|
||
// 查询沿用 GORM 默认软删除范围,软删除账号或店铺占用的关键字段可被重新注册。
|
||
func ensureRegistrationKeysAvailable(ctx context.Context, tx *gorm.DB, phone, username, shopCode string) error {
|
||
if exists, err := registrationKeyTaken(ctx, tx, &model.Account{}, "phone", phone); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "校验注册手机号失败")
|
||
} else if exists {
|
||
return errors.New(errors.CodePhoneExists, "手机号已被使用")
|
||
}
|
||
if exists, err := registrationKeyTaken(ctx, tx, &model.Account{}, "username", username); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "校验注册用户名失败")
|
||
} else if exists {
|
||
return errors.New(errors.CodeUsernameExists, "用户名已存在")
|
||
}
|
||
if exists, err := registrationKeyTaken(ctx, tx, &model.Shop{}, "shop_code", shopCode); err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "校验注册店铺编号失败")
|
||
} else if exists {
|
||
return errors.New(errors.CodeShopCodeExists, "店铺编号已存在")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// registrationKeyTaken 判断目标表(默认软删除范围)是否已存在占用该关键字段的记录。
|
||
func registrationKeyTaken(ctx context.Context, tx *gorm.DB, target any, column, value string) (bool, error) {
|
||
var count int64
|
||
if err := tx.WithContext(ctx).Model(target).Where(column+" = ?", value).Count(&count).Error; err != nil {
|
||
return false, err
|
||
}
|
||
return count > 0, nil
|
||
}
|
||
|
||
// ensureNoPendingRegistration 校验关键字段没有正在等待审批的注册申请。
|
||
// 已通过或已驳回的终态记录不阻塞重新注册:资料填错后重新扫码必须能形成新的申请与新审批实例。
|
||
func ensureNoPendingRegistration(ctx context.Context, tx *gorm.DB, phone, username, shopCode string) error {
|
||
var pending model.AgentDistributionRegistration
|
||
err := tx.WithContext(ctx).
|
||
Where("status = ? AND (phone = ? OR username = ? OR shop_code = ?)",
|
||
constants.AgentDistributionRegistrationStatusPending, phone, username, shopCode).
|
||
Order("id ASC").First(&pending).Error
|
||
switch {
|
||
case err == nil:
|
||
return pendingKeyConflict(&pending, phone, username)
|
||
case stderrors.Is(err, gorm.ErrRecordNotFound):
|
||
return nil
|
||
default:
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "校验待审批注册申请失败")
|
||
}
|
||
}
|
||
|
||
// pendingKeyConflict 把命中的待审批记录映射为指明冲突字段的冲突错误。
|
||
// 查询条件保证三个关键字段至少一个命中,店铺编号作为兜底分支。
|
||
func pendingKeyConflict(pending *model.AgentDistributionRegistration, phone, username string) error {
|
||
switch {
|
||
case pending.Phone == phone:
|
||
return errors.New(errors.CodeConflict, "该手机号已有待审批的注册申请,请等待审批结果")
|
||
case pending.Username == username:
|
||
return errors.New(errors.CodeConflict, "该用户名已有待审批的注册申请,请等待审批结果")
|
||
default:
|
||
return errors.New(errors.CodeConflict, "该店铺编号已有待审批的注册申请,请等待审批结果")
|
||
}
|
||
}
|
||
|
||
// 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) {
|
||
if appErr.Code == errors.CodeTooManyRequests {
|
||
return appErr
|
||
}
|
||
return errors.Wrap(errors.CodeVerificationCodeInvalid, err, appErr.Message)
|
||
}
|
||
return errors.Wrap(errors.CodeVerificationCodeInvalid, err)
|
||
}
|
||
|
||
// resolveRegistrationSubmitter 解析扫码注册的审批发起身份。
|
||
// 公开接口没有登录账号,使用分销码所属店铺的启用主账号作为发起主体;
|
||
// 该账号非平台/超管身份,企业微信侧按既有规则回落到应用默认审批发起人。
|
||
func resolveRegistrationSubmitter(ctx context.Context, db *gorm.DB, parentShopID uint) (*model.Account, error) {
|
||
var account model.Account
|
||
if err := db.WithContext(ctx).
|
||
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.CodeInvalidStatus, "上级店铺未配置启用的主账号,请联系平台处理")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询上级店铺主账号失败")
|
||
}
|
||
return &account, nil
|
||
}
|
||
|
||
// registrationApprovalForm 生成企业微信审批表单业务快照。
|
||
// 手机号按脱敏值写入,禁止把完整手机号或密码写入审批表单与审计。
|
||
//
|
||
// 业务员取上级店铺当前业务员的名称快照:上级无业务员时以空值提交并显式标记为「无」,
|
||
// MUST NOT 以提交人、上级店铺主账号或其它账号填充。该字段是只读快照,审批通过时写入新店铺的
|
||
// 初始业务员仍按既有规则取上级业务员当时值,不因本快照与通过时值不同而改写通过结果。
|
||
func registrationApprovalForm(input distributiondomain.RegistrationInput, parent *model.Shop, businessOwnerName string) map[string]any {
|
||
owner := strings.TrimSpace(businessOwnerName)
|
||
absenceMark := ""
|
||
if owner == "" {
|
||
absenceMark = agentDistributionBusinessOwnerAbsenceMark
|
||
}
|
||
return map[string]any{
|
||
constants.ApprovalFieldDistributionCode: distributiondomain.MaskDistributionCode(input.DistributionCode),
|
||
constants.ApprovalFieldDistributionParentShopID: parent.ID,
|
||
constants.ApprovalFieldDistributionParentShopName: parent.ShopName,
|
||
constants.ApprovalFieldDistributionShopName: input.ShopName,
|
||
constants.ApprovalFieldDistributionShopCode: input.ShopCode,
|
||
constants.ApprovalFieldDistributionUsername: input.Username,
|
||
constants.ApprovalFieldDistributionPhoneMasked: distributiondomain.MaskPhone(input.Phone),
|
||
constants.ApprovalFieldDistributionContactName: input.ContactName,
|
||
constants.ApprovalFieldDistributionRegion: strings.TrimSpace(
|
||
input.Province + input.City + input.District + input.Address),
|
||
constants.ApprovalFieldDistributionBusinessOwner: owner,
|
||
constants.ApprovalFieldDistributionBusinessOwnerAbsence: absenceMark,
|
||
}
|
||
}
|
||
|
||
// agentDistributionBusinessOwnerAbsenceMark 是注册审批材料中「上级店铺无业务员」的显式标记。
|
||
// 与「字段未填」区分开,使审批人看到的是确认无业务员而非漏填。
|
||
const agentDistributionBusinessOwnerAbsenceMark = "无"
|
||
|
||
// parentBusinessOwnerName 读取上级店铺当前业务员的名称快照。
|
||
// 判定与店铺业务员投影的可用口径一致:账号存在、为平台账号、启用且未删除;
|
||
// 任一条件不满足时返回空串,由审批材料显式标记为无,MUST NOT 用其它账号填充。
|
||
func (s *RegistrationService) parentBusinessOwnerName(ctx context.Context, parent *model.Shop) string {
|
||
if parent == nil || parent.BusinessOwnerAccountID == nil || *parent.BusinessOwnerAccountID == 0 {
|
||
return ""
|
||
}
|
||
var account model.Account
|
||
if err := s.db.WithContext(ctx).
|
||
Select("id", "username", "user_type", "status").
|
||
First(&account, *parent.BusinessOwnerAccountID).Error; err != nil {
|
||
return ""
|
||
}
|
||
if account.UserType != constants.UserTypePlatform || account.Status != constants.StatusEnabled || account.DeletedAt.Valid {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(account.Username)
|
||
}
|
||
|
||
// attachRegistrationInstance 回写注册记录关联的审批实例,写入一次后不可修改。
|
||
func attachRegistrationInstance(
|
||
ctx context.Context,
|
||
tx *gorm.DB,
|
||
registration *model.AgentDistributionRegistration,
|
||
instanceID uint,
|
||
) error {
|
||
result := tx.WithContext(ctx).Model(&model.AgentDistributionRegistration{}).
|
||
Where("id = ? AND approval_instance_id IS NULL", registration.ID).
|
||
Update("approval_instance_id", instanceID)
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "关联扫码注册审批实例失败")
|
||
}
|
||
if result.RowsAffected != 1 {
|
||
return errors.New(errors.CodeConflict, "扫码注册审批实例关联已变化")
|
||
}
|
||
registration.ApprovalInstanceID = &instanceID
|
||
return nil
|
||
}
|
||
|
||
// registrationAuditSnapshot 生成注册记录审计快照,手机号按脱敏值记录,不含密码哈希。
|
||
func registrationAuditSnapshot(registration *model.AgentDistributionRegistration) map[string]any {
|
||
instanceID := uint(0)
|
||
if registration.ApprovalInstanceID != nil {
|
||
instanceID = *registration.ApprovalInstanceID
|
||
}
|
||
return map[string]any{
|
||
"id": registration.ID, "parent_shop_id": registration.ParentShopID,
|
||
"distribution_code_masked": distributiondomain.MaskDistributionCode(registration.DistributionCode),
|
||
"phone_masked": distributiondomain.MaskPhone(registration.Phone),
|
||
"username": registration.Username, "shop_code": registration.ShopCode,
|
||
"status": registration.Status, "approval_instance_id": instanceID,
|
||
}
|
||
}
|
||
|
||
// uintText 将无符号整数转换为审计标识与键的十进制文本。
|
||
func uintText(value uint) string {
|
||
return strconv.FormatUint(uint64(value), 10)
|
||
}
|
||
|
||
// intText 将整数转换为审计标识与键的十进制文本。
|
||
func intText(value int) string {
|
||
return strconv.Itoa(value)
|
||
}
|
||
|
||
// composeAuditEventID 拼接审计事件标识,并约束在审计列宽内。
|
||
func composeAuditEventID(parts ...string) (string, error) {
|
||
eventID := strings.Join(parts, ":")
|
||
if len(eventID) > 128 {
|
||
return "", errors.New(errors.CodeInternalError, "审计事件标识超出长度限制")
|
||
}
|
||
return eventID, nil
|
||
}
|
||
|
||
// lockRegistrationForUpdate 以行锁读取注册记录,未找到返回稳定不存在错误。
|
||
func lockRegistrationForUpdate(
|
||
ctx context.Context,
|
||
tx *gorm.DB,
|
||
id uint,
|
||
) (*model.AgentDistributionRegistration, error) {
|
||
var registration model.AgentDistributionRegistration
|
||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
First(®istration, id).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
return nil, errors.New(errors.CodeNotFound, "扫码注册记录不存在")
|
||
}
|
||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定扫码注册记录失败")
|
||
}
|
||
return ®istration, nil
|
||
}
|