yxh
6 天以前 68ff33beff11310e830b0615b5a81f83fad0f683
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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;
    }
}