// Package distribution 收口代理分销注册、提现资格与提现审批的领域不变量。 // 本包不依赖 Fiber、GORM、Redis、Asynq 或具体第三方 SDK。 package distribution import ( "crypto/rand" "encoding/hex" "strconv" "strings" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) const ( distributionCodeBytes = 16 phoneMaskedKeepPrefix = 3 phoneMaskedKeepSuffix = 4 codeMaskedKeepPrefix = 4 codeMaskedKeepSuffix = 4 ) // GenerateDistributionCode 生成 32 位十六进制随机分销码。 // 唯一性由数据库条件唯一索引兜底,调用方在冲突时重新生成。 func GenerateDistributionCode() (string, error) { buf := make([]byte, distributionCodeBytes) if _, err := rand.Read(buf); err != nil { return "", errors.Wrap(errors.CodeInternalError, err, "生成分销码失败") } return hex.EncodeToString(buf), nil } // ValidateRegistrationInput 规范化并校验扫码注册输入。 // 手机号、用户名、店铺编号与店铺名称由公开接口必填;密码长度沿用账号体系既有下限。 func ValidateRegistrationInput(input RegistrationInput) (RegistrationInput, error) { input.DistributionCode = strings.TrimSpace(input.DistributionCode) input.Phone = strings.TrimSpace(input.Phone) input.Username = strings.TrimSpace(input.Username) input.ShopName = strings.TrimSpace(input.ShopName) input.ShopCode = strings.TrimSpace(input.ShopCode) input.ContactName = strings.TrimSpace(input.ContactName) input.Province = strings.TrimSpace(input.Province) input.City = strings.TrimSpace(input.City) input.District = strings.TrimSpace(input.District) input.Address = strings.TrimSpace(input.Address) if input.DistributionCode == "" || input.Phone == "" || input.Username == "" || input.ShopName == "" || input.ShopCode == "" { return RegistrationInput{}, errors.New(errors.CodeInvalidParam, "分销码不可用") } if len(input.Phone) != 11 { return RegistrationInput{}, errors.New(errors.CodeInvalidParam, "手机号格式不正确") } if len(input.Username) < 3 || len(input.Username) > 50 { return RegistrationInput{}, errors.New(errors.CodeInvalidParam, "用户名长度必须为 3 至 50 个字符") } if len(input.Password) < 6 || len(input.Password) > 64 { return RegistrationInput{}, errors.New(errors.CodeInvalidParam, "密码长度必须为 6 至 64 个字符") } return input, nil } // ValidateQualificationInput 规范化并校验提现资料资格输入。 // 企业主体必须填写统一社会信用代码,个人主体必须填写法人身份证号; // 发票仅企业可选,且抬头与统一社会信用代码必须与签约主体一致。 func ValidateQualificationInput(input QualificationInput) (QualificationInput, error) { input.SubjectCode = strings.TrimSpace(input.SubjectCode) input.LegalPersonIDCard = strings.TrimSpace(input.LegalPersonIDCard) input.ContractFileKey = strings.TrimSpace(input.ContractFileKey) input.IDCardFrontFileKey = strings.TrimSpace(input.IDCardFrontFileKey) input.IDCardBackFileKey = strings.TrimSpace(input.IDCardBackFileKey) input.BusinessLicenseFileKey = strings.TrimSpace(input.BusinessLicenseFileKey) input.ShopFrontFileKey = strings.TrimSpace(input.ShopFrontFileKey) input.InvoiceFileKey = strings.TrimSpace(input.InvoiceFileKey) input.InvoiceTitle = strings.TrimSpace(input.InvoiceTitle) input.InvoiceSubjectCode = strings.TrimSpace(input.InvoiceSubjectCode) switch input.SubjectType { case constants.WithdrawalQualificationSubjectTypeEnterprise: if input.SubjectCode == "" || input.LegalPersonIDCard == "" { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "企业主体必须填写统一社会信用代码与法人身份证号") } case constants.WithdrawalQualificationSubjectTypePersonal: if input.LegalPersonIDCard == "" { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "个人主体必须填写法人身份证号") } if input.SubjectCode == "" { // 个人主体的签约主体代码即法人身份证号。 input.SubjectCode = input.LegalPersonIDCard } default: return QualificationInput{}, errors.New(errors.CodeInvalidParam, "签约主体类型无效") } if input.SubjectCode != input.LegalPersonIDCard && input.SubjectType == constants.WithdrawalQualificationSubjectTypePersonal { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "个人主体的签约主体代码必须与法人身份证号一致") } if input.ContractFileKey == "" || input.IDCardFrontFileKey == "" || input.IDCardBackFileKey == "" { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "合同与法人身份证正反面附件必须填写") } if input.SubjectType == constants.WithdrawalQualificationSubjectTypePersonal && (input.InvoiceFileKey != "" || input.InvoiceTitle != "" || input.InvoiceSubjectCode != "") { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "发票资料仅企业主体可提交") } if input.InvoiceFileKey != "" { if input.InvoiceTitle == "" || input.InvoiceSubjectCode == "" { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "提交发票时必须填写抬头与统一社会信用代码") } if input.InvoiceSubjectCode != input.SubjectCode { return QualificationInput{}, errors.New(errors.CodeInvalidParam, "发票统一社会信用代码必须与合同主体一致") } } // 附件上限由结构保证:资格只有合同、法人身份证正反面、营业执照、门头照、发票共 6 个 // 单对象键字段,天然不超过企业微信单张审批单 6 个附件上限,无需运行时计数校验。 return input, nil } // MaskPhone 生成脱敏手机号,仅保留前 3 位与后 4 位。 // 日志与审计不得记录完整手机号。 func MaskPhone(phone string) string { phone = strings.TrimSpace(phone) if len(phone) < phoneMaskedKeepPrefix+phoneMaskedKeepSuffix { return "" } return phone[:phoneMaskedKeepPrefix] + "****" + phone[len(phone)-phoneMaskedKeepSuffix:] } // MaskSubjectCode 生成脱敏证件号或统一社会信用代码,仅保留前 4 位与后 4 位。 // 日志与审计不得记录完整证件号。 func MaskSubjectCode(code string) string { code = strings.TrimSpace(code) if len(code) < codeMaskedKeepPrefix+codeMaskedKeepSuffix { return "" } return code[:codeMaskedKeepPrefix] + "**********" + code[len(code)-codeMaskedKeepSuffix:] } // MaskDistributionCode 生成脱敏分销码,仅保留首尾片段。 // 分销码是可枚举的公开入口标识,日志与审计只记录脱敏值。 func MaskDistributionCode(code string) string { code = strings.TrimSpace(code) if len(code) < codeMaskedKeepPrefix+codeMaskedKeepSuffix { return "" } return code[:codeMaskedKeepPrefix] + "****" + code[len(code)-codeMaskedKeepSuffix:] } // FormatCentYuan 将分金额格式化为两位小数的元字符串,仅用于审批表单与展示。 func FormatCentYuan(amount int64) string { return strconv.FormatInt(amount/100, 10) + "." + pad2(strconv.FormatInt(amount%100, 10)) } // pad2 将 0 至 99 的十进制文本左补零到两位。 func pad2(value string) string { if len(value) >= 2 { return value } return "0" + value } // RegistrationInput 是公开扫码注册的规范化输入。 type RegistrationInput struct { DistributionCode string Phone string Username string Password string ShopName string ShopCode string ContactName string Province string City string District string Address string } // QualificationInput 是提现资料资格的规范化输入。 type QualificationInput struct { SubjectType string SubjectCode string LegalPersonIDCard string ContractFileKey string IDCardFrontFileKey string IDCardBackFileKey string BusinessLicenseFileKey string ShopFrontFileKey string InvoiceFileKey string InvoiceTitle string InvoiceSubjectCode string }