refactor(h5)!: H5 改由 nginx 托管,后端退回纯 API
用户决定:后端不再对外发静态文件,学生端 H5 交给网关。两者**仍然同源**
(微信只给一个授权回调域名;H5 的接口地址取自 location.origin),
所以前端业务代码、微信后台、application-dev.yml 的 h5-base-url 都不用改。
后端
- 删掉 dev/test/prod 三份 profile 里的 spring.web.resources.static-locations
- 主配置加 spring.web.resources.add-mappings=false —— 用开关而不是"指到空目录":
后者会连带启用 Spring Boot 的默认位置(classpath:/static 等),谁丢个文件进去就悄悄对外可见
- StaticCacheHeaderFilter → ApiNoStoreFilter,只剩 /api/** 的 no-store
(静态资源的缓存策略整体归 nginx,避免"改一处漏一处")
- 补 NoHandlerFoundException → 404:关掉静态映射后未匹配路径不再经过
ResourceHttpRequestHandler,抛的是 NoHandlerFoundException,只处理
NoResourceFoundException 时访问 / 会变成 500「系统繁忙」并刷 ERROR 日志
deploy
- 新增 xt-nginx(80):发 frontend-mp/dist/build/h5 + /api 反代 8080(host.docker.internal,
Linux 靠 extra_hosts host-gateway),端口与产物目录走 .env 的 H5_PORT/H5_DIR
- 三条"删了就会出事"的都已带上理由:client_max_body_size 50m(默认 1m,超了回 413
而后端日志一个字都没有)、/assets 的 immutable 与 *.html 的 no-cache(§一·十 那个
"发版后学生看到旧页面"的坑)、健康检查写 127.0.0.1(容器里 localhost 先解析到 IPv6,
nginx 只听 IPv4 → 永远 starting 而首页是好的)
CI 与文档
- CI:学生端 H5 构建从"e2e 的输入"改为编译检查,不再留 artifact(e2e 不消费它)
- 同步 AGENTS.md / README.md / docs/09(启动步骤、CI 表、发布注意、排障表)/ docs/11
- 交接摘要新增 §一·四十三(含理由、验证数字、要人工做的一步与环境限制)
验证
- mvn clean package 通过(必须 clean:删掉的旧类会留在 target/classes 被组件扫描)
- 后端直连:/ 与 /assets/* → 404({"code":40001,"msg":"接口不存在"})、
/api/.../captcha → 200 + no-store、/doc.html → 302→200、/v3/api-docs 与 /actuator/health → 200
- 经 nginx(80):首页 200 + no-cache、/assets/*.js 200 + immutable、/nope.js 404(无回退)、
/api 200 + no-store、gzip 生效;2MB multipart 上传 → 401(与直连一致 ⇒ 未踩 1m 的坑)、
55MB → 413
- e2e(XT_BASE=http://localhost,全程走 nginx):e2e-m0.py 37/37、e2e-m3-h5-auth.py 24/24、
e2e-m2-scope.py 38/38
- verify-all.py --quick 6/6;check-test-profile.py:test profile 仍能起来
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 深学在线 · 学生端 H5 站点(本地开发 / 开发的"生产形态")
|
||||
#
|
||||
# 它做两件事,**而且必须在同一个 server 上做**:
|
||||
# ① 把 H5 构建产物(frontend-mp/dist/build/h5)当静态站发出去;
|
||||
# ② 把 /api 反代到后端 8080。
|
||||
#
|
||||
# ⚠️ 为什么"同源"是硬要求,不是审美:
|
||||
# · 微信网页授权的「授权回调页面域名」**只能配一个**,而回调落在
|
||||
# /api/v1/mp/h5/callback;学生授权后被 302 回页面,页面必须与回调**同域**
|
||||
# (两个源就要配两个域名,微信不给配)。
|
||||
# · H5 的接口地址取自 `location.origin`(见 frontend-mp/src/config/index.ts),
|
||||
# 跨源时它会把请求打到 nginx 自己身上,表现是"页面能开、接口全 404"。
|
||||
# · 同源还顺带省掉 CORS。
|
||||
#
|
||||
# 用法:
|
||||
# cd frontend-mp && pnpm build:h5 # 先出产物(产物不入库)
|
||||
# cd deploy && docker compose up -d nginx
|
||||
# → http://localhost/ H5
|
||||
# → http://localhost/api/... 后端 8080
|
||||
#
|
||||
# ⚠️ 内网穿透要指到**本服务的端口**(默认 80),不是后端的 8080 ——
|
||||
# 指到 8080 的表现是"接口好用、页面 404"。
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# ⚠️⚠️ 这一条必须有。nginx 默认只收 1MB,而学生从聊天记录里选一张照片就可能超过;
|
||||
# 超了 nginx 直接回 413 —— 而**后端日志里一个字都没有**,
|
||||
# 排查时会一路怀疑"请求根本没发出来"。
|
||||
# 后端自己的上限是 50MB/60MB(application.yml 的 spring.servlet.multipart),
|
||||
# 这里对齐到 50m 即可,网关不该比应用更严。
|
||||
client_max_body_size 50m;
|
||||
|
||||
# 上传与导出都是长连接(导出打包是长事务,见 application-prod.yml 的说明),
|
||||
# 别用默认的 60s 把还在跑的请求掐掉
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# H5 的包有好几 MB,穿透/真机下压缩收益很直接
|
||||
gzip on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||
|
||||
# ── /api:反代到后端 ────────────────────────────────────
|
||||
#
|
||||
# ⚠️ 后端是**宿主机上的 jar**,不在这个 compose 里,所以用 host.docker.internal
|
||||
# (Linux 上靠 compose 里的 extra_hosts: host-gateway 才有这个名字)。
|
||||
#
|
||||
# ⚠️ 这里**故意不写** Cache-Control:后端的 ApiNoStoreFilter 已经发了 no-store。
|
||||
# 再加一条会变成两个 Cache-Control 头 —— 虽然合法,但"这个头谁负责"就有了两个来源。
|
||||
location /api/ {
|
||||
proxy_pass http://host.docker.internal:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# 上传是流式的,别在 nginx 侧攒完再转发(大文件时表现为"进度条卡住不动")
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
# ── 带内容哈希的资源:可以按"永久"缓存 ──────────────────
|
||||
# 文件名里带内容哈希,内容变了名字就变 —— 这是唯一值得缓存的东西
|
||||
# (这些包加起来好几 MB,学生不该每次发版都重下)
|
||||
#
|
||||
# ⚠️ 不加 `always`:那会让 404 也带上 immutable,浏览器就把"文件不存在"缓存一年
|
||||
location /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
# ── 其余:HTML 与没有内容哈希的静态文件(uni-app 的 /static/**)──
|
||||
#
|
||||
# ⚠️ 这两行不能省:`no-cache` ≠ 不缓存,而是"用之前先问一下" ——
|
||||
# 配合 Last-Modified 走 304,只花一个往返、不重传正文。
|
||||
# 没有显式过期时间时浏览器会**启发式缓存**(RFC 9111 §4.2.2:按
|
||||
# Last-Modified 年龄的 10% 自行推断),后果是发版后学生继续打开旧页面
|
||||
# (旧 index.html 里写死了旧 chunk 名),且只在"打开过页面的设备"上复现。
|
||||
# 这个坑的完整来龙去脉见 backend/xt-common/.../ApiNoStoreFilter.java 的类注释。
|
||||
#
|
||||
# ⚠️ 这里**故意不做** history 回退(try_files ... /index.html):
|
||||
# uni-app H5 是 hash 路由(#/pages/...),服务端只需要提供 / 与静态文件。
|
||||
# 加了回退反而有害:缺失的 chunk 会返回 200 + HTML,
|
||||
# 浏览器报的是"Unexpected token '<'",而真正的原因只是文件不存在。
|
||||
location / {
|
||||
add_header Cache-Control "no-cache";
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user