package distributionwithdrawal import ( "context" "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" ) // RegistrationResult 返回已落库的待审批注册记录与审批实例引用。 type RegistrationResult struct { RegistrationID uint Status int ApprovalInstanceID uint ApprovalStatus int } // RegistrationService 受理公开扫码注册。 // 只创建待审批注册记录与审批实例,不创建店铺、账号、钱包或上下级归属。 type RegistrationService struct { db *gorm.DB verifier VerificationCodeVerifier approval approvalapp.Port 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} } // Register 创建待审批注册记录。 // 无效分销码、停用上级、验证码无效或已消费统一返回“分销码不可用”,且不落库。 // 手机号、用户名或店铺编号与既有账号/店铺重复时返回稳定冲突错误。 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, "密码哈希失败") } 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, "查询分销码所属店铺失败") } // 验证码校验成功即消费;无效或已消费与无效分销码返回同一对外结果。 if err := s.verifier.VerifyCode(ctx, normalized.Phone, code); err != nil { return nil, errors.New(errors.CodeInvalidParam, "分销码不可用") } 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)) if err != nil { return nil, err } result := &RegistrationResult{} err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { 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 } return result, nil } // 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.CodeInvalidParam, "分销码不可用") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询上级店铺主账号失败") } return &account, nil } // registrationApprovalForm 生成企业微信审批表单业务快照。 // 手机号按脱敏值写入,禁止把完整手机号或密码写入审批表单与审计。 func registrationApprovalForm(input distributiondomain.RegistrationInput, parent *model.Shop) map[string]any { 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), } } // 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 }