feat(register): 完善经销商注册功能并优化邀请机制

- 将登录方法替换为注册方法,实现完整的用户注册流程
- 添加短信验证码发送失败的错误处理和提示
- 实现邀请推荐关系绑定功能,支持注册后自动建立推荐关系
- 优化URL参数解析,支持tenantId和inviter参数的灵活获取
- 添加倒计时清理逻辑,防止内存泄漏
- 更新表单验证规则,移除不必要的字段验证
- 在邀请链接中添加tenantId参数,确保未登录用户能正确识别租户
- 添加注册成功后的自动登录和推荐关系建立流程
This commit is contained in:
2026-01-22 11:29:15 +08:00
parent 585b2e95fa
commit 49c8d40e75
2 changed files with 162 additions and 133 deletions

View File

@@ -67,7 +67,7 @@
size="large" size="large"
:maxlength="6" :maxlength="6"
allow-clear allow-clear
@pressEnter="onLoginBySms" @pressEnter="submit"
/> />
<a-button <a-button
class="login-captcha" class="login-captcha"
@@ -120,7 +120,7 @@
size="large" size="large"
:maxlength="6" :maxlength="6"
allow-clear allow-clear
@pressEnter="onLoginBySms" @pressEnter="submit"
/> />
<a-button <a-button
class="login-captcha" class="login-captcha"
@@ -138,7 +138,7 @@
size="large" size="large"
type="primary" type="primary"
:loading="loading" :loading="loading"
@click="onLoginBySms" @click="submit"
> >
{{ loading ? t('login.loading') : t('login.login') }} {{ loading ? t('login.loading') : t('login.login') }}
</a-button> </a-button>
@@ -195,7 +195,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, unref, watch, onMounted } from 'vue'; import { ref, reactive, unref, watch, onMounted, onBeforeUnmount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getTenantId } from '@/utils/domain'; import { getTenantId } from '@/utils/domain';
@@ -203,22 +203,26 @@
import { CheckCircleOutlined } from '@ant-design/icons-vue'; import { CheckCircleOutlined } from '@ant-design/icons-vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util'; import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
import { loginBySms, getCaptcha } from '@/api/passport/login'; import {
import { TEMPLATE_ID, THEME_STORE_NAME } from '@/config/setting'; getCaptcha,
import { sendSmsCaptcha } from '@/api/passport/login'; loginBySms,
registerUser,
sendSmsCaptcha
} from '@/api/passport/login';
import { THEME_STORE_NAME } from '@/config/setting';
import useFormData from '@/utils/use-form-data'; import useFormData from '@/utils/use-form-data';
import { FormInstance } from 'ant-design-vue/es/form'; import { FormInstance } from 'ant-design-vue/es/form';
import { configWebsiteField } from '@/api/cms/cmsWebsiteField'; import { configWebsiteField } from '@/api/cms/cmsWebsiteField';
import { Config } from '@/api/cms/cmsWebsiteField/model'; import { Config } from '@/api/cms/cmsWebsiteField/model';
import { phoneReg } from 'ele-admin-pro'; import { phoneReg } from 'ele-admin-pro';
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { addAdminUser } from '@/api/system/user';
import { User } from '@/api/system/user/model'; import { User } from '@/api/system/user/model';
import { listRoles } from '@/api/system/role'; import { addShopDealerReferee } from '@/api/shop/shopDealerReferee';
import { getToken } from '@/utils/token-util';
const useForm = Form.useForm; const useForm = Form.useForm;
const { currentRoute } = useRouter(); const router = useRouter();
const { currentRoute } = router;
const { t } = useI18n(); const { t } = useI18n();
const { locale } = useI18n(); const { locale } = useI18n();
@@ -237,7 +241,10 @@
// 配置信息 // 配置信息
const { form } = useFormData<User>({ const { form } = useFormData<User>({
phone: '', phone: '',
isAdmin: true code: '',
remember: false, // 这里作为“同意协议”的勾选状态使用
isAdmin: true,
dealerId: undefined
}); });
// 验证码 base64 数据 // 验证码 base64 数据
@@ -262,22 +269,8 @@
// 表格选中数据 // 表格选中数据
const formRef = ref<FormInstance | null>(null); const formRef = ref<FormInstance | null>(null);
// 表单验证规则 // 表单验证规则(只保留本页面实际填写的字段)
const rules = reactive({ const rules = reactive({
companyName: [
{
required: true,
message: '请输入店铺名称',
type: 'string'
}
],
category: [
{
required: true,
message: '请选择行业分类',
type: 'string'
}
],
phone: [ phone: [
{ {
pattern: phoneReg, pattern: phoneReg,
@@ -287,33 +280,12 @@
trigger: 'blur' trigger: 'blur'
} }
], ],
// address: [
// {
// required: true,
// message: '请选择店铺位置',
// type: 'string'
// }
// ],
password: [
{
required: true,
message: t('login.password'),
type: 'string'
}
],
code: [ code: [
{ {
required: true, required: true,
message: t('login.code'), message: t('login.code'),
type: 'string' type: 'string'
} }
],
smsCode: [
{
required: true,
message: t('login.code'),
type: 'string'
}
] ]
}); });
@@ -339,20 +311,26 @@
return; return;
} }
codeLoading.value = true; codeLoading.value = true;
sendSmsCaptcha({ phone: form.phone }).then(() => { sendSmsCaptcha({ phone: form.phone })
message.success('短信验证码发送成功, 请注意查收!'); .then(() => {
visible.value = false; message.success('短信验证码发送成功, 请注意查收!');
codeLoading.value = false; visible.value = false;
countdownTime.value = 30; countdownTime.value = 30;
// 开始对按钮进行倒计时 // 开始对按钮进行倒计时
countdownTimer = window.setInterval(() => { countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) { if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer); countdownTimer && clearInterval(countdownTimer);
countdownTimer = null; countdownTimer = null;
} }
countdownTime.value--; countdownTime.value--;
}, 1000); }, 1000);
}); })
.catch((e: any) => {
message.error(e?.message || '短信验证码发送失败');
})
.finally(() => {
codeLoading.value = false;
});
}; };
const { resetFields } = useForm(form, rules); const { resetFields } = useForm(form, rules);
@@ -363,29 +341,30 @@
localStorage.removeItem(THEME_STORE_NAME); localStorage.removeItem(THEME_STORE_NAME);
}; };
const onLoginBySms = () => { // 建立邀请推荐关系(需要登录态;失败不影响主流程)
if (!formRef.value) { let bindInviterOnce = false;
const tryBindInviteRelation = async () => {
if (bindInviterOnce) {
return; return;
} }
formRef.value if (!inviterId.value) {
.validate() return;
.then(() => { }
loading.value = true; if (!getToken()) {
form.code = form.smsCode?.toLowerCase(); return;
loginBySms(form) }
.then((msg) => { const tenantIdStr = localStorage.getItem('TenantId');
message.success(msg); const userIdStr = localStorage.getItem('UserId');
loading.value = false; if (!userIdStr) {
resetFields(); return;
cleanPageTabs(); }
goHome(); bindInviterOnce = true;
}) await addShopDealerReferee({
.catch((e: Error) => { dealerId: inviterId.value,
message.error(e.message); userId: Number(userIdStr),
loading.value = false; level: 1,
}); tenantId: tenantIdStr ? Number(tenantIdStr) : form.tenantId
}) });
.catch(() => {});
}; };
/* 保存编辑 */ /* 保存编辑 */
@@ -394,52 +373,53 @@
return; return;
} }
formRef.value try {
.validate() await formRef.value.validate();
.then(() => { } catch {
loading.value = true; return;
// addShopDealerUser({ }
// ...form,
// dealerId: Number(dealerId.value),
// }).then((data) => {
// console.log(data);
// });
// createCmsWebSite(form) loading.value = true;
// .then(async (msg) => { try {
// // 如果有邀请人,注册成功后建立推荐关系 // 通过短信验证码完成注册inviter 作为推荐人ID传给后端
// if (inviterId.value) { const msg = await registerUser({
// try { tenantId: form.tenantId,
// const userId = localStorage.getItem('UserId'); phone: form.phone,
// if (userId) { username: form.phone,
// await bindUserReferee({ code: form.code?.trim(),
// dealerId: inviterId.value, // 后端若用 Java boolean 命名为 `isAdmin`,序列化/反序列化可能会被映射成 `admin`
// userId: Number(userId), // 这里双写以兼容不同字段名,避免后端拿到 null 导致 NPE。
// level: 1 isAdmin: true,
// }); ...({ admin: true } as any),
// message.success('注册成功,已建立推荐关系'); dealerId: inviterId.value
// } });
// } catch (e) {
// console.error('建立推荐关系失败:', e); // 注册成功后自动登录,再建立绑定关系(避免未登录时后端取 role 为空)
// // 不影响注册流程,只是推荐关系建立失败 await loginBySms({
// } tenantId: form.tenantId,
// } phone: form.phone,
// code: form.code?.trim(),
// setTimeout(() => { remember: !!form.remember
// // 登录成功 });
// message.success(msg);
// loading.value = false; // 建立绑定关系(失败不影响主流程)
// resetFields(); try {
// cleanPageTabs(); await tryBindInviteRelation();
// goHome(); } catch (e: any) {
// }, 2000) message.warning(
// }) `注册成功,但建立推荐关系失败:${e?.message || '未知错误'}`
// .catch(() => { );
// message.error('该手机号码已经被注册'); }
// loading.value = false;
// }); message.success(msg);
}) resetFields();
.catch(() => {}); cleanPageTabs();
goHome();
} catch (e: any) {
message.error(e?.message || '注册失败');
} finally {
loading.value = false;
}
}; };
/* 获取图形验证码 */ /* 获取图形验证码 */
@@ -469,13 +449,51 @@
// 检查URL参数中的邀请人ID // 检查URL参数中的邀请人ID
onMounted(() => { onMounted(() => {
const urlParams = new URLSearchParams(window.location.search); // 优先从链接参数中拿 tenantId新用户未登录时用于后端识别租户/初始化角色)
const inviterParam = urlParams.get('inviter'); const tenantIdParam = (unref(currentRoute).query.tenantId ??
if (inviterParam) { new URLSearchParams(window.location.search).get('tenantId')) as
inviterId.value = Number(inviterParam); | string
| undefined;
if (tenantIdParam != null && tenantIdParam !== '') {
const tid = Number.parseInt(String(tenantIdParam).trim(), 10);
if (!Number.isNaN(tid) && tid > 0) {
form.tenantId = tid;
// 让 request 拦截器也能带上 TenantId
localStorage.setItem('TenantId', String(tid));
showTenantId.value = false;
}
}
const inviterParam = (unref(currentRoute).query.inviter ??
new URLSearchParams(window.location.search).get('inviter')) as
| string
| undefined;
if (inviterParam != null && inviterParam !== '') {
// 容错:有些场景会把链接后面带上逗号/空格等(例如复制文本),这里提取首段数字
const raw = String(inviterParam).trim();
const id = Number.parseInt(raw.match(/\d+/)?.[0] || '', 10);
if (!Number.isNaN(id) && id > 0) {
inviterId.value = id;
form.dealerId = id;
}
}
// 已登录用户通过邀请链接访问时,补绑一次推荐关系
tryBindInviteRelation();
});
onBeforeUnmount(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
} }
}); });
// 地图选择回调(本页面不使用地图能力,避免模板引用报错)
const onDone = () => {
showMap.value = false;
};
watch( watch(
currentRoute, currentRoute,
() => { () => {

View File

@@ -169,10 +169,21 @@
return props.inviterId || Number(localStorage.getItem('UserId')); return props.inviterId || Number(localStorage.getItem('UserId'));
}); });
// 邀请链接需要带上 tenantId避免未登录用户打开链接时后端无法识别租户导致角色/权限初始化失败
const tenantId = computed(() => {
const tid = localStorage.getItem('TenantId');
return tid ? Number(tid) : undefined;
});
// 生成邀请链接 // 生成邀请链接
const invitationLink = computed(() => { const invitationLink = computed(() => {
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
return `${baseUrl}/dealer/register?inviter=${inviterId.value}`; const params = new URLSearchParams();
params.set('inviter', String(inviterId.value));
if (tenantId.value) {
params.set('tenantId', String(tenantId.value));
}
return `${baseUrl}/dealer/register?${params.toString()}`;
}); });
// 复制链接 // 复制链接