diff --git a/docs/注册登录对接文档.md b/docs/注册登录对接文档.md index bb8da04..87ed98a 100644 --- a/docs/注册登录对接文档.md +++ b/docs/注册登录对接文档.md @@ -31,12 +31,12 @@ Content-Type: application/json "username": "alice01", "phone": "13800138000", "password": "Secret123", - "referral_code": "ABCD2345", + "referral_code": "123456", "privacy_accepted": true } ``` -`referral_code` 可选;传入时必须匹配当前租户下正常会员的 `invite_code`。 +`referral_code` 必填,必须是 6 位数字,且匹配当前租户下正常会员的 `invite_code`。邀请链接可通过 `inviteCode`、`invite_code` 或 `referral_code` 参数预填注册表单。 ## 4 登录 @@ -64,7 +64,7 @@ Content-Type: application/json "member_no": "MABCDEFGHIJKLMNOPQRST", "username": "alice01", "phone": "13800138000", - "invite_code": "EFGH6789", + "invite_code": "654321", "token": "...", "token_type": "Bearer", "expires_at": "2026-10-23T10:00:00Z" @@ -92,7 +92,7 @@ Authorization: Bearer | `USERNAME_INVALID_FORMAT` | 400 | 账户名格式错误 | | `PHONE_INVALID_FORMAT` | 400 | 手机号格式错误 | | `PASSWORD_INVALID_FORMAT` | 400 | 密码格式错误 | -| `REFERRAL_CODE_INVALID` | 400 | 推荐码无效 | +| `REFERRAL_CODE_INVALID` | 400 | 邀请码为空、格式错误或不存在 | | `PRIVACY_POLICY_NOT_ACCEPTED` | 400 | 未同意隐私政策 | | `ACCOUNT_ALREADY_REGISTERED` | 409 | 账户名或手机号已注册 | | `INVALID_CREDENTIALS` | 401 | 手机号或密码错误 | diff --git a/docs/移动端任务与账户平台前后端对接文档.md b/docs/移动端任务与账户平台前后端对接文档.md index c8c191e..e5e6778 100644 --- a/docs/移动端任务与账户平台前后端对接文档.md +++ b/docs/移动端任务与账户平台前后端对接文档.md @@ -167,7 +167,7 @@ Content-Type: application/json "username": "alice01", "phone": "13800138000", "password": "Secret123", - "referral_code": "ABCD2345", + "referral_code": "123456", "privacy_accepted": true } ``` @@ -177,7 +177,7 @@ Content-Type: application/json | `username` | string | 是 | 1–10 位小写字母或数字;后端去空格并转小写 | | `phone` | string | 是 | 中国大陆 11 位手机号,不带 `+86` | | `password` | string | 是 | 6–72 字节,不做 MD5 | -| `referral_code` | string | 否 | 1–32 位字母或数字,必须匹配正常会员邀请码 | +| `referral_code` | string | 是 | 6 位数字,必须匹配正常会员邀请码 | | `privacy_accepted` | boolean | 是 | 必须为 `true` | 注册事务同时写入: @@ -196,7 +196,7 @@ Content-Type: application/json "member_no": "MABCDEFGHIJKLMNOPQRST", "username": "alice01", "phone": "13800138000", - "invite_code": "EFGH6789", + "invite_code": "654321", "token": "random-base64url-token", "token_type": "Bearer", "expires_at": "2026-10-23T10:00:00Z" @@ -241,7 +241,7 @@ Authorization: Bearer | 400 | `USERNAME_INVALID_FORMAT` | 账户名格式错误 | | 400 | `PHONE_INVALID_FORMAT` | 手机号格式错误 | | 400 | `PASSWORD_INVALID_FORMAT` | 密码格式错误 | -| 400 | `REFERRAL_CODE_INVALID` | 推荐码无效 | +| 400 | `REFERRAL_CODE_INVALID` | 邀请码为空、格式错误或不存在 | | 400 | `PRIVACY_POLICY_NOT_ACCEPTED` | 未同意隐私政策 | | 401 | `INVALID_CREDENTIALS` | 手机号或密码错误 | | 409 | `ACCOUNT_ALREADY_REGISTERED` | 账户名、手机号或其他唯一字段冲突 | @@ -265,7 +265,7 @@ GET /v1/me "username": "alice01", "phone": "13800138000", "avatar_url": "", - "invite_code": "EFGH6789", + "invite_code": "654321", "level": { "id": 1, "code": "normal", diff --git a/internal/accounts/service.go b/internal/accounts/service.go index e911343..5bee648 100644 --- a/internal/accounts/service.go +++ b/internal/accounts/service.go @@ -8,6 +8,8 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" + "math/big" "regexp" "strconv" "strings" @@ -39,7 +41,7 @@ var ( var ( phonePattern = regexp.MustCompile(`^1[3-9][0-9]{9}$`) - referralPattern = regexp.MustCompile(`^[A-Za-z0-9]{1,32}$`) + referralPattern = regexp.MustCompile(`^[0-9]{6}$`) tradePasswordPattern = regexp.MustCompile(`^[0-9]{6}$`) ) @@ -170,26 +172,40 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e if err != nil { return nil, err } - inviteCode, err := newPublicID("", 8) - if err != nil { - return nil, err - } - account, err := s.repository.RegisterAccount(ctx, store.AccountRegistration{ - TenantID: s.cfg.DefaultTenantID, - MemberNo: memberNo, - Username: input.Username, - Mobile: input.Phone, - PasswordHash: string(passwordHash), - ReferralCode: input.ReferralCode, - InviteCode: inviteCode, - }) - if err != nil { + for attempt := 0; attempt < 10; attempt++ { + inviteCode, err := newInviteCode() + if err != nil { + return nil, err + } + account, err := s.repository.RegisterAccount(ctx, store.AccountRegistration{ + TenantID: s.cfg.DefaultTenantID, + MemberNo: memberNo, + Username: input.Username, + Mobile: input.Phone, + PasswordHash: string(passwordHash), + ReferralCode: input.ReferralCode, + InviteCode: inviteCode, + }) + if errors.Is(err, store.ErrInviteCodeExists) { + continue + } if errors.Is(err, store.ErrReferralInvalid) { return nil, ErrReferralInvalid } - return nil, err + if err != nil { + return nil, err + } + return s.issueSession(ctx, account, input.DeviceID) } - return s.issueSession(ctx, account, input.DeviceID) + return nil, ErrUnavailable +} + +func newInviteCode() (string, error) { + number, err := rand.Int(rand.Reader, big.NewInt(1000000)) + if err != nil { + return "", err + } + return fmt.Sprintf("%06d", number.Int64()), nil } func validMemberName(value string) bool { diff --git a/internal/accounts/service_test.go b/internal/accounts/service_test.go index 398a665..3ba0674 100644 --- a/internal/accounts/service_test.go +++ b/internal/accounts/service_test.go @@ -48,7 +48,8 @@ func TestRegisterValidation(t *testing.T) { {name: "password", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "123", PrivacyAccepted: true}, want: ErrPasswordInvalid}, {name: "referral required", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrReferralInvalid}, {name: "referral", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "bad!", PrivacyAccepted: true}, want: ErrReferralInvalid}, - {name: "privacy", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "ABC123"}, want: ErrPrivacyNotAccepted}, + {name: "referral length", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "12345", PrivacyAccepted: true}, want: ErrReferralInvalid}, + {name: "privacy", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "123456"}, want: ErrPrivacyNotAccepted}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -60,6 +61,15 @@ func TestRegisterValidation(t *testing.T) { } } +func TestNewInviteCodeIsSixDigits(t *testing.T) { + for i := 0; i < 100; i++ { + code, err := newInviteCode() + if err != nil || !referralPattern.MatchString(code) { + t.Fatalf("newInviteCode() = %q, %v", code, err) + } + } +} + func TestValidMemberNameSupportsChinese(t *testing.T) { for _, name := range []string{"杨情", "欧阳娜娜", "Alicia", "阿依古丽·买买提"} { if !validMemberName(name) { diff --git a/internal/store/accounts.go b/internal/store/accounts.go index 364808c..09bbead 100644 --- a/internal/store/accounts.go +++ b/internal/store/accounts.go @@ -67,6 +67,9 @@ func (s *Store) RegisterAccount(ctx context.Context, input AccountRegistration) levelID, parentID, teamLevel, tree, input.InviteCode) if err != nil { if isDuplicate(err) { + if strings.Contains(err.Error(), "uk_app_member_invite_code") { + return nil, ErrInviteCodeExists + } return nil, ErrAccountExists } return nil, err diff --git a/internal/store/errors.go b/internal/store/errors.go index b9a3703..4c484cc 100644 --- a/internal/store/errors.go +++ b/internal/store/errors.go @@ -4,6 +4,7 @@ import "errors" var ( ErrAccountExists = errors.New("account already exists") + ErrInviteCodeExists = errors.New("invite code already exists") ErrAccountNotFound = errors.New("account not found") ErrAccountRestricted = errors.New("account restricted") ErrAttachmentInvalid = errors.New("attachment invalid") diff --git a/web/src/mobile/views/AuthView.vue b/web/src/mobile/views/AuthView.vue index ea355dd..e37f31b 100644 --- a/web/src/mobile/views/AuthView.vue +++ b/web/src/mobile/views/AuthView.vue @@ -47,7 +47,8 @@ @@ -134,8 +135,8 @@ if (mode.value === 'register' && !/^[\p{L}\p{M}·]{1,10}$/u.test(form.username)) { return void (error.value = '名字须为 1 至 10 个字符,支持汉字'); } - if (mode.value === 'register' && !/^[A-Za-z0-9]{1,32}$/.test(form.referral_code)) { - return void (error.value = '请输入有效的邀请码'); + if (mode.value === 'register' && !/^\d{6}$/.test(form.referral_code)) { + return void (error.value = '请输入 6 位数字邀请码'); } submitting.value = true; try {