diff --git a/backend/public/auth-callback.html b/backend/public/auth-callback.html new file mode 100644 index 0000000..ed2e046 --- /dev/null +++ b/backend/public/auth-callback.html @@ -0,0 +1,39 @@ + + + + + + 正在完成登录… + + + +
正在完成登录,请稍候…
+ + + diff --git a/backend/src/router/index.js b/backend/src/router/index.js index d0706b8..ad346af 100644 --- a/backend/src/router/index.js +++ b/backend/src/router/index.js @@ -1,6 +1,6 @@ import { createRouter, createWebHashHistory } from "vue-router"; import { convertMenusToRoutes } from "./dynamicRoutes"; -import { isSSOEnabled, redirectToAuthorize } from "@/utils/authClient"; +import { isSSOEnabled, redirectToAuthorize, ensureUserInfo } from "@/utils/authClient"; // 静态子路由:需要在 Main 框架内显示的页面 const staticMainChildren = [ @@ -335,6 +335,17 @@ router.beforeEach(async (to, from, next) => { next({ path: "/login", query: { redirect: to.path } }); return; } + + // 统一认证模式:补全用户信息(旧会话可能只有 token 没有 userInfo, + // 或迁移后 userInfo 缺失 id,会导致菜单等接口报「用户ID不存在」)。 + // 令牌失效时 ensureUserInfo 会清空登录态并返回 null,此时重新登录。 + if (isSSOEnabled()) { + const info = await ensureUserInfo(); + if (!info) { + await redirectToAuthorize(); + return; + } + } if (!dynamicRoutesAdded) { await loadAndAddDynamicRoutes(); diff --git a/backend/src/utils/authClient.js b/backend/src/utils/authClient.js index 56370f3..67e6149 100644 --- a/backend/src/utils/authClient.js +++ b/backend/src/utils/authClient.js @@ -171,6 +171,48 @@ export async function logoutSSO() { window.location.href = `${AUTH_BASE}/logout?${params.toString()}`; } +/** + * 确保本地存在完整的用户信息(含 id / group_id)。 + * + * 场景:统一认证上线或 ID 全量迁移后,浏览器里可能残留旧会话—— + * 只有 token 没有 userInfo,或 userInfo 缺少 id,菜单等接口会报 + *「用户ID不存在」。这里主动补全;若令牌已失效则清空登录态返回 null, + * 由调用方跳转登录。 + */ +export async function ensureUserInfo() { + let info = null; + try { + const raw = localStorage.getItem('userInfo'); + info = raw ? JSON.parse(raw) : null; + } catch (e) { + info = null; + } + if (info && info.id) { + return info; + } + + try { + const data = await fetchUserInfo(); + const normalized = { + id: data.id || data.sub || '', + account: data.account || data.mobile || '', + name: data.name || data.nickname || '', + group_id: data.group_id || '', + tid: data.tid || '', + tenant_name: (data.tenants || []).map((t) => t.tenant_name).join('、'), + avatar: data.avatar || '', + type: 'backend' + }; + localStorage.setItem('userInfo', JSON.stringify(normalized)); + return normalized; + } catch (e) { + // 令牌无效(例如迁移前的旧令牌):清掉本地状态,让调用方重新登录 + clearTokens(); + localStorage.removeItem('userInfo'); + return null; + } +} + /** 拉取当前登录用户(认证中心视角) */ export async function fetchUserInfo() { const res = await fetch(`${AUTH_BASE}/userinfo`, { diff --git a/backend/src/utils/request.js b/backend/src/utils/request.js index 985a2ed..ce037b7 100644 --- a/backend/src/utils/request.js +++ b/backend/src/utils/request.js @@ -1,5 +1,8 @@ import axios from 'axios'; -import { isSSOEnabled, refreshAccessToken, clearTokens } from '@/utils/authClient'; +import { isSSOEnabled, refreshAccessToken, clearTokens, redirectToAuthorize } from '@/utils/authClient'; + +// 统一认证模式下 401 后正在跳转登录的标志,避免并发请求同时触发跳转造成死循环 +let ssoRedirecting = false; // 获取API基础URL,添加调试信息 const apiBaseURL = import.meta.env.VITE_API_BASE_URL; @@ -56,10 +59,14 @@ service.interceptors.response.use( error.config.headers['Authorization'] = `Bearer ${newToken}`; return service.request(error.config); } catch (e) { + // 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环) clearTokens(); localStorage.removeItem('userInfo'); - window.location.href = '#/login'; - return Promise.reject(new Error('token无效')); + if (!ssoRedirecting) { + ssoRedirecting = true; + redirectToAuthorize(); + } + return Promise.reject(new Error('token无效,请重新登录')); } } console.error('未授权,请重新登录'); diff --git a/backend/src/views/apps/oa/schedule/components/detail.vue b/backend/src/views/apps/oa/schedule/components/detail.vue index 95bae38..30bff3d 100644 --- a/backend/src/views/apps/oa/schedule/components/detail.vue +++ b/backend/src/views/apps/oa/schedule/components/detail.vue @@ -327,6 +327,14 @@ const previewList = computed(() => imageList.value.map(img => resolveUrl(img.url)) ); +// ---------- 绑定提醒的状态回显与反馈 ---------- +// 注意:这两个 ref 必须声明在下方 watch 之前。 +// watch 带 immediate:true,会在 setup 执行到该处时立即回调 loadReminderInfo(), +// 而 loadReminderInfo 同步写入 reminderInfo / reminderLoading, +// 若此时它们尚未初始化,会抛出 "Cannot access 'x' before initialization"(暂时性死区)。 +const reminderInfo = ref(null); +const reminderLoading = ref(false); + // ---------- 备注:创建 / 修改 / 完成后都可编辑 ---------- const remarkText = ref(""); const remarkSaving = ref(false); @@ -374,10 +382,7 @@ async function saveRemark() { } } -// ---------- 绑定提醒的状态回显与反馈 ---------- -const reminderInfo = ref(null); -const reminderLoading = ref(false); - +// ---------- 提醒渠道文案映射 ---------- const channelTextMap = { SMS: "短信", EMAIL: "邮件", diff --git a/backend/src/views/auth/callback.vue b/backend/src/views/auth/callback.vue index 4bc166d..5252988 100644 --- a/backend/src/views/auth/callback.vue +++ b/backend/src/views/auth/callback.vue @@ -16,10 +16,39 @@ + + diff --git a/platform/src/views/system/authConfig/index.vue b/platform/src/views/system/authConfig/index.vue new file mode 100644 index 0000000..3917274 --- /dev/null +++ b/platform/src/views/system/authConfig/index.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/platform/src/views/system/authIdp/index.vue b/platform/src/views/system/authIdp/index.vue new file mode 100644 index 0000000..10875ce --- /dev/null +++ b/platform/src/views/system/authIdp/index.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/platform/vite.config.js b/platform/vite.config.js index ee05411..fabcee2 100644 --- a/platform/vite.config.js +++ b/platform/vite.config.js @@ -28,8 +28,10 @@ export default defineConfig({ }, server: { host: "127.0.0.1", - port: 4000, - // 开发时前端在 4000,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000) + port: 4400, + // 端口被占用时直接报错,不再自动漂移到其他端口(避免回跳地址与实际端口不一致) + strictPort: true, + // 开发时前端在 4400,接口走相对路径 /platform/*、/backend/*,转发到本地 Go(当前 httpport=9000) proxy: { "/platform": { target: "http://127.0.0.1:9000", diff --git a/uniapp/vite.config.js b/uniapp/vite.config.js index d5e65ae..10b7e83 100644 --- a/uniapp/vite.config.js +++ b/uniapp/vite.config.js @@ -13,6 +13,9 @@ export default defineConfig({ }, }, server: { + host: '127.0.0.1', + port: 4403, + strictPort: true, proxy: { // H5 开发:api/config.js 默认 baseURL 为 /proxy-api,转发到 Go 后端 '/proxy-api': { diff --git a/website/vite.config.ts b/website/vite.config.ts index 1deb3d5..c80e7e3 100644 --- a/website/vite.config.ts +++ b/website/vite.config.ts @@ -21,7 +21,8 @@ export default defineConfig({ server: { open: true, host: '127.0.0.1', - port: 4002, + port: 4402, + strictPort: true, hmr: true, proxy: { '/api': {