Files
xiaotutongxue/deploy/nginx/xt.conf
T
lovife 2ff17cd0c9 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 仍能起来
2026-10-19 17:35:33 +08:00

96 lines
5.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ═══════════════════════════════════════════════════════════
# 深学在线 · 学生端 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;
}
}