feat: require invitation code for registration
This commit is contained in:
@@ -155,7 +155,7 @@ func (s *Service) Register(ctx context.Context, input RegisterInput) (*Result, e
|
|||||||
if len([]rune(input.Password)) < 6 || len([]byte(input.Password)) > 72 {
|
if len([]rune(input.Password)) < 6 || len([]byte(input.Password)) > 72 {
|
||||||
return nil, ErrPasswordInvalid
|
return nil, ErrPasswordInvalid
|
||||||
}
|
}
|
||||||
if input.ReferralCode != "" && !referralPattern.MatchString(input.ReferralCode) {
|
if !referralPattern.MatchString(input.ReferralCode) {
|
||||||
return nil, ErrReferralInvalid
|
return nil, ErrReferralInvalid
|
||||||
}
|
}
|
||||||
if !input.PrivacyAccepted {
|
if !input.PrivacyAccepted {
|
||||||
|
|||||||
@@ -46,8 +46,9 @@ func TestRegisterValidation(t *testing.T) {
|
|||||||
{name: "username", input: RegisterInput{Username: "张 三", Phone: "13800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrUsernameInvalid},
|
{name: "username", input: RegisterInput{Username: "张 三", Phone: "13800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrUsernameInvalid},
|
||||||
{name: "phone", input: RegisterInput{Username: "alice", Phone: "12800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrPhoneInvalid},
|
{name: "phone", input: RegisterInput{Username: "alice", Phone: "12800138000", Password: "secret1", PrivacyAccepted: true}, want: ErrPhoneInvalid},
|
||||||
{name: "password", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "123", PrivacyAccepted: true}, want: ErrPasswordInvalid},
|
{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: "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"}, want: ErrPrivacyNotAccepted},
|
{name: "privacy", input: RegisterInput{Username: "alice", Phone: "13800138000", Password: "secret1", ReferralCode: "ABC123"}, want: ErrPrivacyNotAccepted},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func accountByMobile(ctx context.Context, db queryer, tenantID int64, mobile str
|
|||||||
func referralParent(ctx context.Context, tx *sqlx.Tx, tenantID int64, code string) (int64, int, string, error) {
|
func referralParent(ctx context.Context, tx *sqlx.Tx, tenantID int64, code string) (int64, int, string, error) {
|
||||||
code = strings.TrimSpace(code)
|
code = strings.TrimSpace(code)
|
||||||
if code == "" {
|
if code == "" {
|
||||||
return 0, 1, "", nil
|
return 0, 0, "", ErrReferralInvalid
|
||||||
}
|
}
|
||||||
var parent struct {
|
var parent struct {
|
||||||
ID int64 `db:"id"`
|
ID int64 `db:"id"`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const reasonMessages: Record<string, string> = {
|
|||||||
FORBIDDEN: '当前账户无权执行此操作',
|
FORBIDDEN: '当前账户无权执行此操作',
|
||||||
INVALID_CREDENTIALS: '手机号或密码错误',
|
INVALID_CREDENTIALS: '手机号或密码错误',
|
||||||
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
|
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
|
||||||
|
REFERRAL_CODE_INVALID: '邀请码无效,请检查后重新输入',
|
||||||
RATE_LIMITED: '操作过于频繁,请稍后再试',
|
RATE_LIMITED: '操作过于频繁,请稍后再试',
|
||||||
TASK_DISABLED: '领取功能暂未开放',
|
TASK_DISABLED: '领取功能暂未开放',
|
||||||
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
|
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
<input
|
<input
|
||||||
v-model.trim="form.referral_code"
|
v-model.trim="form.referral_code"
|
||||||
maxlength="32"
|
maxlength="32"
|
||||||
placeholder="请输入推荐码(选填)"
|
autocomplete="off"
|
||||||
|
placeholder="请输入邀请码(必填)"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<router-link v-if="mode === 'login'" class="m-forgot-link" to="/app/password/verify"
|
<router-link v-if="mode === 'login'" class="m-forgot-link" to="/app/password/verify"
|
||||||
@@ -99,15 +100,32 @@
|
|||||||
const error = ref('');
|
const error = ref('');
|
||||||
const form = reactive({ username: '', phone: '', password: '', referral_code: '' });
|
const form = reactive({ username: '', phone: '', password: '', referral_code: '' });
|
||||||
|
|
||||||
watch(mode, () => {
|
function referralCodeFromLink() {
|
||||||
|
const value =
|
||||||
|
route.query.inviteCode ?? route.query.invite_code ?? route.query.referral_code;
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(mode, (value) => {
|
||||||
form.username = '';
|
form.username = '';
|
||||||
form.phone = '';
|
form.phone = '';
|
||||||
form.password = '';
|
form.password = '';
|
||||||
form.referral_code = '';
|
form.referral_code = value === 'register' ? referralCodeFromLink() : '';
|
||||||
accepted.value = true;
|
accepted.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [route.query.inviteCode, route.query.invite_code, route.query.referral_code],
|
||||||
|
() => {
|
||||||
|
const referralCode = referralCodeFromLink();
|
||||||
|
if (!referralCode) return;
|
||||||
|
mode.value = 'register';
|
||||||
|
form.referral_code = referralCode;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
error.value = '';
|
error.value = '';
|
||||||
if (!/^1\d{10}$/.test(form.phone)) return void (error.value = '请输入正确的手机号码');
|
if (!/^1\d{10}$/.test(form.phone)) return void (error.value = '请输入正确的手机号码');
|
||||||
@@ -116,6 +134,9 @@
|
|||||||
if (mode.value === 'register' && !/^[\p{L}\p{M}·]{1,10}$/u.test(form.username)) {
|
if (mode.value === 'register' && !/^[\p{L}\p{M}·]{1,10}$/u.test(form.username)) {
|
||||||
return void (error.value = '名字须为 1 至 10 个字符,支持汉字');
|
return void (error.value = '名字须为 1 至 10 个字符,支持汉字');
|
||||||
}
|
}
|
||||||
|
if (mode.value === 'register' && !/^[A-Za-z0-9]{1,32}$/.test(form.referral_code)) {
|
||||||
|
return void (error.value = '请输入有效的邀请码');
|
||||||
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const data = await request<AuthResponse>({
|
const data = await request<AuthResponse>({
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<n-modal v-model:show="showResult" :mask-closable="false">
|
<n-modal v-model:show="showResult" :mask-closable="false">
|
||||||
<section class="m-claim-result" :class="`is-${resultState}`">
|
<section class="m-claim-result" :class="`is-${resultState}`">
|
||||||
<template v-if="resultState === 'matching'">
|
<template v-if="resultState === 'matching'">
|
||||||
<h2>正在匹配任务</h2>
|
<h2>正在匹配订单</h2>
|
||||||
<p>{{ matchingHint }}</p>
|
<p>{{ matchingHint }}</p>
|
||||||
<div class="m-match-visual">
|
<div class="m-match-visual">
|
||||||
<img class="orbit" :src="mobileAssets.claimSpinner" alt="" />
|
<img class="orbit" :src="mobileAssets.claimSpinner" alt="" />
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<div v-if="isAutoMatching" class="m-match-progress"><i :style="{ width: `${matchProgress}%` }"></i></div>
|
<div v-if="isAutoMatching" class="m-match-progress"><i :style="{ width: `${matchProgress}%` }"></i></div>
|
||||||
<strong v-if="isAutoMatching" class="m-match-percent">{{ matchProgress }}%</strong>
|
<strong v-if="isAutoMatching" class="m-match-percent">{{ matchProgress }}%</strong>
|
||||||
<ol class="m-match-steps m-match-steps--single">
|
<ol class="m-match-steps m-match-steps--single">
|
||||||
<li class="active"><span>✓</span>正在匹配任务</li>
|
<li class="active"><span>✓</span>正在匹配订单</li>
|
||||||
</ol>
|
</ol>
|
||||||
<button class="m-result-secondary" @click="openOrders">稍后去记录查看</button>
|
<button class="m-result-secondary" @click="openOrders">稍后去记录查看</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
const matchingHint = computed(() => {
|
const matchingHint = computed(() => {
|
||||||
if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...';
|
if (order.value?.dispatch_mode === 'manual') return '订单已领取,正在等待后台匹配...';
|
||||||
if (matchRemainingSeconds.value > 0) return `系统正在派单... ${formatCountdown(matchRemainingSeconds.value)}`;
|
if (matchRemainingSeconds.value > 0) return `系统正在派单... ${formatCountdown(matchRemainingSeconds.value)}`;
|
||||||
return pollFinished.value ? '任务仍在匹配,可稍后前往记录查看' : '系统正在为您匹配任务,请稍等...';
|
return pollFinished.value ? '订单仍在匹配,可稍后前往记录查看' : '系统正在为您匹配订单,请稍等...';
|
||||||
});
|
});
|
||||||
const resultIcon = computed(() => resultState.value === 'success' ? mobileAssets.claimSuccess : resultState.value === 'failed' ? mobileAssets.claimFailed : mobileAssets.claimCancelled);
|
const resultIcon = computed(() => resultState.value === 'success' ? mobileAssets.claimSuccess : resultState.value === 'failed' ? mobileAssets.claimFailed : mobileAssets.claimCancelled);
|
||||||
const resultTitle = computed(() => resultState.value === 'success' ? '任务获取成功' : resultState.value === 'failed' ? '领取失败' : '已取消任务');
|
const resultTitle = computed(() => resultState.value === 'success' ? '任务获取成功' : resultState.value === 'failed' ? '领取失败' : '已取消任务');
|
||||||
|
|||||||
Reference in New Issue
Block a user