diff --git a/docs/development-standards-v2.md b/docs/development-standards-v2.md
index 11a9948..c047da6 100644
--- a/docs/development-standards-v2.md
+++ b/docs/development-standards-v2.md
@@ -159,6 +159,18 @@ server: {
- CRMEB 客户端**任何异常都优雅降级**(返回空昵称/头像,不阻断登录),并 `logger.warning` 记录,便于排查。
- `DEV_MODE=1` 才启用 `POST /api/auth/dev-login`,生产必须为 `0`。
- **CRMEB 匹配口径**:openid 按 appid 隔离。若 CRMEB 与小程序非同一 appid,应改用 **unionid** 匹配(小程序传 unionid,CRMEB 内部接口按 unionid 查;openid 仍作本系统主键)。
+- **H5 端 openid 一致性检查**(分享场景必做):当 H5 被其他用户分享的链接打开时,web-view 的 `localStorage` 可能残留原用户的 token。H5 的路由守卫(如 `ProtectedRoute`)必须比对**当前 URL 中的 openid** 与 **本地缓存 token 中的 openid**,不一致时清除缓存重新走登录流程:
+ ```tsx
+ const urlOpenid = new URLSearchParams(window.location.search).get('openid');
+ if (urlOpenid && token) {
+ const payload = decodeToken(token);
+ if (payload.openid !== urlOpenid) {
+ clearToken(); // 清除原用户缓存
+ return ; // 重新登录
+ }
+ }
+ ```
+ > 否则会出现:B 打开 A 分享的链接,URL 中已是 B 的 openid,但 H5 读取到 A 的缓存 token,仍然展示 A 的数据。
---
@@ -776,3 +788,4 @@ async function handleCopyLink(url: string) {
- [ ] WebView Bridge: H5 复制链接优先用 `document.execCommand('copy')`,bridge 兜底
- [ ] WebView Bridge: 确认微信 JS-SDK 已加载(`jweixin-1.2.0.js`),否则 `wx.miniProgram` 不可用
- [ ] WebView Bridge: 生成分享图片场景使用 `html-to-image` 等截图库
+- [ ] H5 身份一致性: 路由守卫中比对 URL openid 与缓存 token openid,不一致时清除缓存重新登录
diff --git a/frontend/src/app/routes.tsx b/frontend/src/app/routes.tsx
index 782e35e..1364019 100644
--- a/frontend/src/app/routes.tsx
+++ b/frontend/src/app/routes.tsx
@@ -1,134 +1,149 @@
-import { createBrowserRouter, Navigate, Outlet } from "react-router";
-import { LoginPage } from "./pages/LoginPage";
-import { OnboardingPage } from "./pages/OnboardingPage";
-import { HomePage } from "./pages/HomePage";
-import { CameraCapturePage } from "./pages/CameraCapturePage";
-import { DataVerificationPage } from "./pages/DataVerificationPage";
-import { AIAnalysisPage } from "./pages/AIAnalysisPage";
-import { ReportDetailPage } from "./pages/ReportDetailPage";
-import { DevNavigationPage } from "./pages/DevNavigationPage";
-import { AIChatPage } from "./pages/AIChatPage";
-import { HealthRecordsPage } from "./pages/HealthRecordsPage";
-import { GrowthCurvePage } from "./pages/GrowthCurvePage";
-import { TimelinePage } from "./pages/TimelinePage";
-import { CalendarPage } from "./pages/CalendarPage";
-import { DoctorQuestionsPage } from "./pages/DoctorQuestionsPage";
-import { SharePage } from "./pages/SharePage";
-import { ExpertConsultationPage } from "./pages/ExpertConsultationPage";
-import { ProfilePage } from "./pages/ProfilePage";
-import { VoiceRecordPage } from "./pages/VoiceRecordPage";
-import { NotificationPage } from "./pages/NotificationPage";
-import { MobileLayout } from "./components/MobileLayout";
-import { getToken } from "./services/api";
-
-/** 路由守卫:未登录跳转登录页 */
-function ProtectedRoute() {
- const token = getToken();
- const urlToken = new URLSearchParams(window.location.search).get('token');
-
- // 来自小程序的 token → 让 LoginPage 处理
- if (urlToken) {
- return ;
- }
-
- if (!token) {
- return ;
- }
- return ;
-}
-
-export const router = createBrowserRouter([
- {
- // 全局 H5 移动端布局容器
- element: ,
- children: [
- // 公开路由
- {
- path: "/dev",
- Component: DevNavigationPage,
- },
- {
- path: "/",
- Component: LoginPage,
- },
- {
- path: "/onboarding",
- Component: OnboardingPage,
- },
- // 需要登录的路由
- {
- element: ,
- children: [
- {
- path: "/home",
- Component: HomePage,
- },
- {
- path: "/profile",
- Component: ProfilePage,
- },
- {
- path: "/camera",
- Component: CameraCapturePage,
- },
- {
- path: "/review",
- Component: DataVerificationPage,
- },
- {
- path: "/verify",
- Component: DataVerificationPage,
- },
- {
- path: "/analysis/:reportId",
- Component: AIAnalysisPage,
- },
- {
- path: "/report/:reportId",
- Component: ReportDetailPage,
- },
- {
- path: "/chat/:reportId",
- Component: AIChatPage,
- },
- {
- path: "/voice-record",
- Component: VoiceRecordPage,
- },
- {
- path: "/health-records",
- Component: HealthRecordsPage,
- },
- {
- path: "/growth-curve",
- Component: GrowthCurvePage,
- },
- {
- path: "/timeline",
- Component: TimelinePage,
- },
- {
- path: "/calendar",
- Component: CalendarPage,
- },
- {
- path: "/doctor-questions/:reportId",
- Component: DoctorQuestionsPage,
- },
- {
- path: "/share/:reportId",
- Component: SharePage,
- },
- {
- path: "/expert-consultation",
- Component: ExpertConsultationPage,
- },
- {
- path: "/notifications",
- Component: NotificationPage,
- },
- ],
- },
- ],
- },
-], { basename: "/app_03" });
+import { createBrowserRouter, Navigate, Outlet } from "react-router";
+import { LoginPage } from "./pages/LoginPage";
+import { OnboardingPage } from "./pages/OnboardingPage";
+import { HomePage } from "./pages/HomePage";
+import { CameraCapturePage } from "./pages/CameraCapturePage";
+import { DataVerificationPage } from "./pages/DataVerificationPage";
+import { AIAnalysisPage } from "./pages/AIAnalysisPage";
+import { ReportDetailPage } from "./pages/ReportDetailPage";
+import { DevNavigationPage } from "./pages/DevNavigationPage";
+import { AIChatPage } from "./pages/AIChatPage";
+import { HealthRecordsPage } from "./pages/HealthRecordsPage";
+import { GrowthCurvePage } from "./pages/GrowthCurvePage";
+import { TimelinePage } from "./pages/TimelinePage";
+import { CalendarPage } from "./pages/CalendarPage";
+import { DoctorQuestionsPage } from "./pages/DoctorQuestionsPage";
+import { SharePage } from "./pages/SharePage";
+import { ExpertConsultationPage } from "./pages/ExpertConsultationPage";
+import { ProfilePage } from "./pages/ProfilePage";
+import { VoiceRecordPage } from "./pages/VoiceRecordPage";
+import { NotificationPage } from "./pages/NotificationPage";
+import { MobileLayout } from "./components/MobileLayout";
+import { getToken, clearToken, decodeToken } from "./services/api";
+
+/** 路由守卫:未登录跳转登录页 */
+function ProtectedRoute() {
+ const token = getToken();
+ const urlToken = new URLSearchParams(window.location.search).get('token');
+ const urlOpenid = new URLSearchParams(window.location.search).get('openid');
+
+ // 来自小程序的 token → 让 LoginPage 处理
+ if (urlToken) {
+ return ;
+ }
+
+ // URL 中有 openid 且与本地缓存的 token 中的 openid 不一致 → 清除缓存重新登录
+ if (urlOpenid && token) {
+ try {
+ const payload = decodeToken(token);
+ if (payload.openid !== urlOpenid) {
+ console.log('[auth] openid 不一致: URL=' + urlOpenid + ', cache=' + payload.openid + ',清除缓存');
+ clearToken();
+ return ;
+ }
+ } catch (e) {
+ console.warn('[auth] token 解码失败', e);
+ }
+ }
+
+ if (!token) {
+ return ;
+ }
+ return ;
+}
+
+export const router = createBrowserRouter([
+ {
+ // 全局 H5 移动端布局容器
+ element: ,
+ children: [
+ // 公开路由
+ {
+ path: "/dev",
+ Component: DevNavigationPage,
+ },
+ {
+ path: "/",
+ Component: LoginPage,
+ },
+ {
+ path: "/onboarding",
+ Component: OnboardingPage,
+ },
+ // 需要登录的路由
+ {
+ element: ,
+ children: [
+ {
+ path: "/home",
+ Component: HomePage,
+ },
+ {
+ path: "/profile",
+ Component: ProfilePage,
+ },
+ {
+ path: "/camera",
+ Component: CameraCapturePage,
+ },
+ {
+ path: "/review",
+ Component: DataVerificationPage,
+ },
+ {
+ path: "/verify",
+ Component: DataVerificationPage,
+ },
+ {
+ path: "/analysis/:reportId",
+ Component: AIAnalysisPage,
+ },
+ {
+ path: "/report/:reportId",
+ Component: ReportDetailPage,
+ },
+ {
+ path: "/chat/:reportId",
+ Component: AIChatPage,
+ },
+ {
+ path: "/voice-record",
+ Component: VoiceRecordPage,
+ },
+ {
+ path: "/health-records",
+ Component: HealthRecordsPage,
+ },
+ {
+ path: "/growth-curve",
+ Component: GrowthCurvePage,
+ },
+ {
+ path: "/timeline",
+ Component: TimelinePage,
+ },
+ {
+ path: "/calendar",
+ Component: CalendarPage,
+ },
+ {
+ path: "/doctor-questions/:reportId",
+ Component: DoctorQuestionsPage,
+ },
+ {
+ path: "/share/:reportId",
+ Component: SharePage,
+ },
+ {
+ path: "/expert-consultation",
+ Component: ExpertConsultationPage,
+ },
+ {
+ path: "/notifications",
+ Component: NotificationPage,
+ },
+ ],
+ },
+ ],
+ },
+], { basename: "/app_03" });