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"的合并部署方案)
|
*
|
* <h3>作用</h3>
|
* 前端是 Vue Router 单页应用。用户直接访问或按 F5 刷新一个深层路由
|
* (例如 {@code /ecg/queue})时,jar 里并不存在对应的物理文件,需要把请求
|
* 转发回 {@code index.html},由前端路由接管,否则会 404。
|
* <p>
|
* 原先这段逻辑由 nginx 的 {@code try_files $uri $uri/ /index.html} 承担;
|
* 前端并入 jar 之后改由后端负责,nginx 只需保留 {@code /admin-api/} 反向代理。
|
*
|
* <h3>为什么显式枚举路径,而不是用通配兜底</h3>
|
* 只映射前端实际存在的 5 类页面路径(见 {@code src/router/modules/remaining.ts}
|
* 与动态菜单表 {@code system_menu}):
|
* <ul>
|
* <li>{@code /} 首页</li>
|
* <li>{@code /ecg/**} 心电业务(预约、排队、医生工作台、诊室、设备、记录…)</li>
|
* <li>{@code /clinic/**} 诊室相关</li>
|
* <li>{@code /system/**} 系统管理(用户、角色、菜单、字典、日志…)</li>
|
* <li>{@code /infra/**} 基础设施(文件、定时任务、代码生成、监控…)</li>
|
* </ul>
|
* 这样可避免"把所有 404 都吞成 index.html",保留后端真实的 404 语义
|
* (例如 {@code /admin-api/typo} 仍会正确返回 404)。
|
*
|
* <h3>安全性说明</h3>
|
* {@code @PermitAll} 是必须的:刷新页面时浏览器尚未携带 token,若要求认证
|
* 会直接返回 401,导致用户"一刷新就掉线"。此处放行的仅是**前端页面外壳**,
|
* 真实数据仍由 {@code /admin-api/**} 走 token 鉴权,未授权拿不到任何数据,
|
* 与原先 nginx 公开静态页面的行为一致,未降低安全等级。
|
*
|
* <h3>与静态资源的优先级</h3>
|
* 静态资源由 {@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。
|
* <p>
|
* 必须与 {@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/**} 通配!
|
*
|
* <h4>为什么不能通配(这是一个已实测复现的真实故障)</h4>
|
* ECG 的后端接口与前端页面**共用 /ecg/ 命名空间**:
|
* <pre>
|
* 接口(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
|
* </pre>
|
* 若用 {@code /ecg/**} 转发到 index.html,则大屏/诊间屏请求
|
* {@code /ecg/screen/big-screen-data} 会拿到**HTML 而不是 JSON**,
|
* 叫号屏直接失效(HTTP 仍是 200,极具迷惑性)。
|
*
|
* <h4>精确路径为何安全</h4>
|
* 在 Spring 中 {@code /ecg/queue} 与 {@code /ecg/queue/page} 是两个不同映射,
|
* 精确路径不会匹配带子路径的接口,因此二者天然区分。
|
*
|
* <h4>路径来源</h4>
|
* 取自 {@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;
|
}
|
|
/**
|
* 系统管理与基础设施页面。
|
* <p>
|
* 这两个前缀**可以**用通配,因为 {@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;
|
}
|
}
|