package cn.lihu.jh.server.ecg; import io.swagger.v3.oas.annotations.Hidden; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import javax.annotation.security.PermitAll; /** * 前端 SPA 转发控制器(配合"前端打进 jar"的合并部署方案) * *

作用

* 前端是 Vue Router 单页应用。用户直接访问或按 F5 刷新一个深层路由 * (例如 {@code /ecg/queue})时,jar 里并不存在对应的物理文件,需要把请求 * 转发回 {@code index.html},由前端路由接管,否则会 404。 *

* 原先这段逻辑由 nginx 的 {@code try_files $uri $uri/ /index.html} 承担; * 前端并入 jar 之后改由后端负责,nginx 只需保留 {@code /admin-api/} 反向代理。 * *

为什么显式枚举路径,而不是用通配兜底

* 只映射前端实际存在的 5 类页面路径(见 {@code src/router/modules/remaining.ts} * 与动态菜单表 {@code system_menu}): * * 这样可避免"把所有 404 都吞成 index.html",保留后端真实的 404 语义 * (例如 {@code /admin-api/typo} 仍会正确返回 404)。 * *

安全性说明

* {@code @PermitAll} 是必须的:刷新页面时浏览器尚未携带 token,若要求认证 * 会直接返回 401,导致用户"一刷新就掉线"。此处放行的仅是**前端页面外壳**, * 真实数据仍由 {@code /admin-api/**} 走 token 鉴权,未授权拿不到任何数据, * 与原先 nginx 公开静态页面的行为一致,未降低安全等级。 * *

与静态资源的优先级

* 静态资源由 {@link FrontendResourceConfig#addResourceHandlers} 注册, * 其优先级高于 Controller 映射,因此 {@code /assets/*.js}、{@code /logo.gif} * 等真实存在的文件不会被本控制器拦截。 * * @author 心电排队装机系统 */ @Hidden // 不在 Swagger 文档中展示 @Controller @PermitAll public class SpaForwardController { /** 前端页面外壳。位于 jar 内 META-INF/resources/frontend/ 下 */ private static final String SPA_INDEX = "forward:/frontend/index.html"; @GetMapping("/") public String index() { return SPA_INDEX; } /** * remaining.ts 中的顶层单页路由: * /login、/roomselect、/sso、/social-login、/403、/404、/500、 * /calling-screen-big、/calling-screen-room。 *

* 必须与 {@link FrontendAuthorizeRequestsCustomizer#SPA_PAGE_PATTERNS} * 保持同步,否则会出现"控制器有映射、却被安全层拦成 401"的不一致 * (曾实测漏掉 /roomselect 时即为该症状)。 */ @GetMapping({"/index.html", "/login", "/roomselect", "/sso", "/social-login", "/403", "/404", "/500", "/calling-screen-big", "/calling-screen-room"}) public String simplePages() { return SPA_INDEX; } /** 框架自带的功能页(个人中心、字典、代码生成、定时任务、重定向中转) */ @GetMapping({"/user/**", "/dict/**", "/codegen/**", "/job/**", "/redirect/**"}) public String modulePages() { return SPA_INDEX; } /** * ⚠️ ECG 业务页面:必须【逐条精确枚举】,绝不能用 {@code /ecg/**} 通配! * *

为什么不能通配(这是一个已实测复现的真实故障)

* ECG 的后端接口与前端页面**共用 /ecg/ 命名空间**: *
     *   接口(javax @RestController)        前端页面(菜单 path)
     *   /ecg/screen/big-screen-data         /ecg/queue
     *   /ecg/queue/page                     /ecg/room
     *   /ecg/call/next                      /ecg/check-type
     *   /ecg/doctor/finish-next-patient     /ecg/device
     *   /ecg/appointment/page               /ecg/appointment
     * 
* 若用 {@code /ecg/**} 转发到 index.html,则大屏/诊间屏请求 * {@code /ecg/screen/big-screen-data} 会拿到**HTML 而不是 JSON**, * 叫号屏直接失效(HTTP 仍是 200,极具迷惑性)。 * *

精确路径为何安全

* 在 Spring 中 {@code /ecg/queue} 与 {@code /ecg/queue/page} 是两个不同映射, * 精确路径不会匹配带子路径的接口,因此二者天然区分。 * *

路径来源

* 取自 {@code system_menu} 表中 parent_id 为 ECG 根(1254)或系统管理(1) * 的页面 path,以及 {@code ecg_web/src/router} 中的静态路由。 */ @GetMapping({ // —— ECG 根(1254) 下的业务页 —— "/ecg/appointment", "/ecg/appointment-confirm", "/ecg/alter", "/ecg/queue", "/ecg/roomselect", "/ecg/doc/routine", "/ecg/doc/dev-ready", "/ecg/doc/dev-install", "/ecg/dev-dismantle", "/ecg/dev-rent", "/ecg/doctor/dev-data-entry", "/ecg/job-statistic", "/ecg/dev-statistic", // —— 挂系统管理(1) 下但路径在 /ecg 的页面 —— "/ecg/room", "/ecg/roomstatus", "/ecg/dev-model", "/ecg/device", "/ecg/check-type", }) public String ecgPages() { return SPA_INDEX; } /** * 诊室相关页面。注意:接口 {@code /clinic/room/page} 等带子路径, * 与精确路径 {@code /clinic/room} 不冲突。 */ @GetMapping({"/clinic/room"}) public String clinicPages() { return SPA_INDEX; } /** * 系统管理与基础设施页面。 *

* 这两个前缀**可以**用通配,因为 {@code /admin-api/**} 才是接口命名空间, * {@code /system/**}、{@code /infra/**} 仅用于页面,不存在同名接口。 * (曾核查:system/infra 模块所有 Controller 均为 {@code @RequestMapping("/system/...")} 形式, * 但实际请求路径都带 {@code /admin-api} 前缀,故不冲突。) */ @GetMapping({"/system/**", "/infra/**"}) public String adminPages() { return SPA_INDEX; } }