diff --git a/.workbuddy/memory/2026-07-24.md b/.workbuddy/memory/2026-07-24.md index 368e032..54ce369 100644 --- a/.workbuddy/memory/2026-07-24.md +++ b/.workbuddy/memory/2026-07-24.md @@ -60,3 +60,22 @@ - 同一个手机号前 3 次直接调用 `sendSmsCaptcha`;第 4 次起弹出原有图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。 - 失败时自动刷新图形验证码;补充了 `.catch`/`.finally`,避免发送异常时按钮一直 loading。 - `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-login` 编译通过。 + +## 修复:邮箱登录图形验证码未校验 +- **问题**:`src/views/passport/login/index.vue` 第 651-555 行的验证码比对逻辑被注释掉,导致用户输入任意非空字符串即可通过前端校验。 +- **修复**:取消注释 `form.code?.toLowerCase() !== text.value` 的校验块,恢复「输入错误则提示"验证码错误"并刷新验证码」的逻辑。 +- **说明**:当前后端 `/captcha` 接口返回明文答案 `data.text`,前端直接比对即可。后续若后端改为返回 key 模式,需同步改为传 key 给后端校验。 + +## 新增:手机号登录多租户选择 +- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`。 +- `src/api/passport/login/model/index.ts`: + - `LoginResult` 新增 `tenants?: TenantOption[]`; + - 新增 `TenantOption` 类型与 `LoginOutcome` 联合类型(`ok` / `selectTenant`)。 +- `src/api/passport/login/index.ts`: + - 新增 `saveLoginState` 统一保存 token、TenantId、UserId; + - `loginBySuperAdminSms` 改为返回 `LoginOutcome`,检测 `data.tenants` 时返回 `selectTenant`; + - 新增 `loginBySelectTenant({ phone, code, tenantId, remember })` 二次登录接口。 +- `src/views/passport/login/index.vue`: + - `onLoginBySms` 根据 `outcome.status` 处理:单租户直接登录成功;多租户弹出「请选择要登录的租户」弹窗。 + - 新增弹窗与 `onSelectTenant`:选择租户后调用 `loginBySelectTenant` 完成二次登录并跳转。 +- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-tenant` 编译通过。 diff --git a/src/api/passport/login/index.ts b/src/api/passport/login/index.ts index e8ea358..a504c49 100644 --- a/src/api/passport/login/index.ts +++ b/src/api/passport/login/index.ts @@ -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 { const res = await request.post>( 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 { + const res = await request.post>( + 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 || '登录失败')); } /** diff --git a/src/api/passport/login/model/index.ts b/src/api/passport/login/model/index.ts index b87c7c4..d7f3e8c 100644 --- a/src/api/passport/login/model/index.ts +++ b/src/api/passport/login/model/index.ts @@ -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; message: string } + | { status: 'selectTenant'; tenants: TenantOption[]; message: string }; + /** * 图形验证码返回结果 */ diff --git a/src/views/passport/login/index.vue b/src/views/passport/login/index.vue index 71521ac..0a123fd 100644 --- a/src/views/passport/login/index.vue +++ b/src/views/passport/login/index.vue @@ -270,6 +270,35 @@ + + + +
+ + + +
+
@@ -291,8 +320,10 @@ loginBySuperAdminSms, getCaptcha, loginByEmail, - loginByUserId + loginByUserId, + loginBySelectTenant } from '@/api/passport/login'; + import type { TenantOption } from '@/api/passport/login/model'; import QrLogin from '@/components/QrLogin/index.vue'; import { User } from '@/api/system/user/model'; @@ -467,6 +498,10 @@ // 多用户选择账号登录 const showSelectLoginUser = ref(false); const admins = ref([]); + // 多租户选择登录 + const showSelectTenantModal = ref(false); + const pendingTenants = ref([]); + const selectingTenant = ref(false); // const tenantId = getTenantId(); // 表格选中数据 @@ -585,23 +620,18 @@ loading.value = true; form.code = form.smsCode?.toLowerCase(); loginBySuperAdminSms(form) - .then((msg) => { - if (msg == '请选择登录用户') { - showSelectLoginUser.value = true; - listAdminsByPhoneAll({ - phone: form.phone, - templateId: TEMPLATE_ID - }).then((data) => { - admins.value = data; - }); - return false; - } else { - message.success(msg); + .then((outcome) => { + if (outcome.status === 'selectTenant') { + pendingTenants.value = outcome.tenants; + showSelectTenantModal.value = true; loading.value = false; - resetFields(); - cleanPageTabs(); - goHome(); + return; } + message.success(outcome.message); + loading.value = false; + resetFields(); + cleanPageTabs(); + goHome(); }) .catch((e: Error) => { message.error(e.message); @@ -616,6 +646,29 @@ onLoginBySms(); }; + /* 选择租户后二次登录 */ + const onSelectTenant = (item: TenantOption) => { + if (selectingTenant.value || item.tenantId == null) return; + selectingTenant.value = true; + loginBySelectTenant({ + phone: form.phone, + code: form.smsCode || '', + tenantId: item.tenantId, + remember: form.remember + }) + .then((outcome) => { + showSelectTenantModal.value = false; + message.success(outcome.message); + resetFields(); + cleanPageTabs(); + goHome(); + }) + .catch((e: Error) => message.error(e.message)) + .finally(() => { + selectingTenant.value = false; + }); + }; + /* 保存编辑 */ const submit = () => { if (!formRef.value) { @@ -624,11 +677,11 @@ formRef.value .validate() .then(() => { - // if (form.code?.toLowerCase() !== text.value) { - // message.error('验证码错误'); - // changeCaptcha(); - // return; - // } + if (form.code?.toLowerCase() !== text.value) { + message.error('验证码错误'); + changeCaptcha(); + return; + } const account = (form.account || '').trim(); // 邮箱格式判断 const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(account);