feat(passport): 增加多租户手机号登录支持并修复邮箱验证码校验

- 修复邮箱登录验证码未校验问题,恢复输入错误提示及刷新验证码逻辑
- 新增多租户选项登录流程,支持单租户直接登录及多租户选择弹窗
- 扩展登录接口及模型,新增 TenantOption 类型和 LoginOutcome 联合结果类型
- 新增 loginBySelectTenant 二次登录接口及对应组件逻辑
- 统一保存登录态,包括 token、租户ID和用户ID到本地存储
- 调整前端短信登录逻辑,根据返回状态决定流程,支持多租户弹窗选择
- 确保新增代码无类型错误并通过构建验证
This commit is contained in:
2026-07-24 22:33:39 +08:00
parent 55e90c2434
commit 45fc45680f
4 changed files with 188 additions and 33 deletions
+71 -12
View File
@@ -9,6 +9,30 @@ import type {
} from './model';
import { User } from '@/api/system/user/model';
import { SERVER_API_URL } from '@/config/setting';
import type { LoginOutcome, TenantOption } from './model';
/**
* 登录态落地:把后端返回的 access_token / 租户 / 用户 持久化到本地
*/
function saveLoginState(result: LoginResult | undefined, remember?: boolean) {
if (!result) return;
if (result.access_token) {
setToken(result.access_token, remember);
}
const user = result.user;
if (user) {
if (user.tenantId != null) {
try {
localStorage.setItem('TenantId', String(user.tenantId));
} catch { /* ignore */ }
}
if (user.userId != null) {
try {
localStorage.setItem('UserId', String(user.userId));
} catch { /* ignore */ }
}
}
}
/**
* 登录
@@ -106,23 +130,58 @@ export async function loginBySms(data: LoginParam) {
/**
* 超管手机号短信登录
* 后端对应 POST /loginBySuperAdminSms,参数为 { phone, code, tenantId }
* - 单租户:直接返回 ok
* - 多租户:返回 selectTenant,前端需调用 loginBySelectTenant 二次登录
*/
export async function loginBySuperAdminSms(data: LoginParam) {
export async function loginBySuperAdminSms(
data: LoginParam
): Promise<LoginOutcome> {
const res = await request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/loginBySuperAdminSms',
data
{ phone: data.phone, code: data.smsCode, smsCode: data.smsCode, tenantId: data.tenantId }
);
if (res.data.code === 0) {
setToken(res.data.data?.access_token, data.remember);
if (res.data.data?.user) {
const user = res.data.data?.user;
localStorage.setItem('TenantId', String(user.tenantId));
localStorage.setItem('UserId', String(user.userId));
}
return res.data.message;
if (res.data.code !== 0) {
return Promise.reject(new Error(res.data.message));
}
return Promise.reject(new Error(res.data.message));
const result = res.data.data;
if (result?.tenants && result.tenants.length > 0) {
return {
status: 'selectTenant',
tenants: result.tenants as TenantOption[],
message: res.data.message || '请选择要登录的租户'
};
}
if (result?.user) {
saveLoginState(result, data.remember);
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
}
return Promise.reject(new Error(res.data.message || '登录失败'));
}
/**
* 选择租户后登录(多租户短信登录专用二次登录接口)
* 后端对应 POST /loginBySelectTenant,参数为 { phone, code, tenantId }
*/
export async function loginBySelectTenant(data: {
phone: string;
code: string;
tenantId: number;
remember?: boolean;
}): Promise<LoginOutcome> {
const res = await request.post<ApiResult<LoginResult>>(
SERVER_API_URL + '/loginBySelectTenant',
{ phone: data.phone, code: data.code, smsCode: data.code, tenantId: data.tenantId }
);
if (res.data.code !== 0) {
return Promise.reject(new Error(res.data.message));
}
const result = res.data.data;
if (result?.user) {
saveLoginState(result, data.remember);
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
}
return Promise.reject(new Error(res.data.message || '登录失败'));
}
/**
+24
View File
@@ -19,6 +19,8 @@ export interface LoginParam {
phone?: string;
// 短信验证码
code?: string;
// 短信验证码(别名,与后端 LoginParam.code 对应)
smsCode?: string;
}
/**
@@ -29,8 +31,30 @@ export interface LoginResult {
access_token?: string;
// 用户信息
user?: User;
// 多租户场景下返回,此时 access_token / user 为 null
tenants?: TenantOption[];
}
/**
* 多租户登录时后端返回的租户选项
*/
export interface TenantOption {
tenantId?: number;
tenantName?: string;
userId?: number;
username?: string;
nickname?: string;
}
/**
* 登录接口统一返回结果
* - ok:单租户直接登录成功
* - selectTenant:多租户,需要前端弹出选择租户列表
*/
export type LoginOutcome =
| { status: 'ok'; user: NonNullable<LoginResult['user']>; message: string }
| { status: 'selectTenant'; tenants: TenantOption[]; message: string };
/**
* 图形验证码返回结果
*/