From 95f5879c9ade5a9d9a3f3cba3dc0337fa46e304f Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Thu, 25 Jun 2026 18:00:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20H5=20=E7=AB=AF=20o?= =?UTF-8?q?penid=20=E4=B8=80=E8=87=B4=E6=80=A7=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B7=AF=E7=94=B1=E5=AE=88=E5=8D=AB=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A5=E6=B8=85=E9=99=A4=E4=B8=8D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/development-standards-v2.md | 13 ++ frontend/src/app/routes.tsx | 283 ++++++++++++++++--------------- 2 files changed, 162 insertions(+), 134 deletions(-) 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" });