Files
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

224 lines
9.1 KiB
YAML
Raw Permalink 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.
# ═══════════════════════════════════════════════════════════
# GitLab CI(M0-04 / M7-10)
#
# 设计原则:**流水线跑的必须是我们真正依赖的那套验证**。
# 这个仓库的单元测试很少(`src/test` 只有 12 条纯函数),验证靠三样东西:
# ① 五个机械校验脚本(契约对账 / 实体列 / 文档一致性 / 手册文案 / **接线**)
# ② 编译与构建(后端 jar、PC、H5)
# ③ **`scripts/e2e-*.py` 打真实接口 + 查库断言**(1500+ 条)
# 所以 e2e 阶段不是"锦上添花",它是这个项目的主验证手段 ——
# 一个只跑编译的流水线在这里等于没跑。
#
# ⚠️ 密钥策略:e2e 阶段**每次随机生成** JWT/AES/HMAC/RSA 密钥,
# 不存任何 CI 变量。理由是它能顺带证明一件事:
# **应用没有偷偷依赖某个 dev 默认值** —— 如果它依赖了,这里就会红。
# (PG/Redis 的密码是流水线内一次性容器的,随机即可。)
# ═══════════════════════════════════════════════════════════
stages:
- check # 不需要数据库:契约对账、文档一致性、类型检查
- build # 编译与打包
- e2e # 起真库、起服务、跑全套接口断言
variables:
# 依赖缓存放仓库内(GitLab Runner 的默认缓存目录不一定可写)
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
XT_PG_IMAGE: "postgres:16-alpine"
XT_REDIS_IMAGE: "redis:7-alpine"
# E2E 的库与缓存
PG_HOST: "postgres"
PG_PORT: "5432"
PG_DB: "filldb"
PG_USER: "fill"
REDIS_HOST: "redis"
REDIS_PORT: "6379"
# ── 缓存:Maven 仓库与 pnpm store ─────────────────────────
.maven-cache: &maven-cache
key: "maven-$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
.pnpm-cache: &pnpm-cache
key: "pnpm-$CI_COMMIT_REF_SLUG"
paths:
- .pnpm-store/
# ══════════════ 阶段 1:机械校验 ══════════════
契约与实体对账:
stage: check
image: python:3.12-slim
script:
- echo "── 契约 §4 的 121 个接口 vs 代码里注册的路由 ──"
- python scripts/check-api-parity.py --quiet
- echo "── 实体字段 vs 迁移 DDL ──"
- python scripts/check-entity-columns.py --quiet
- echo "── 文档自身的一致性(表格列数 / FR 编号) ──"
- python scripts/verify-docs.py
- echo "── 手册里引用的错误码与文案是否真的存在(客服照着它回答) ──"
- python scripts/check-manual.py
- echo "── 声明了但没接上的东西(权限码 / 错误码 / 通知模板 / 菜单键) ──"
- python scripts/check-wiring.py --quiet
- echo "── 全部 e2e 脚本的语法(引号写错会当场红,而不是跑到一半才红) ──"
- |
python - <<'PY'
import io, os, sys
bad = 0
for fn in sorted(os.listdir('scripts')):
if fn.startswith('e2e-') and fn.endswith('.py'):
try:
compile(io.open('scripts/' + fn, encoding='utf-8').read(), fn, 'exec')
except SyntaxError as e:
bad += 1
print(' [语法错] %s:%s %s' % (fn, e.lineno, (e.text or '').strip()[:80]))
sys.exit(1 if bad else 0)
PY
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
后端编译:
stage: build
image: maven:3.9-eclipse-temurin-21
cache: *maven-cache
script:
- cd backend
- mvn -B -DskipTests clean package
# 产物留一份,后面 e2e 阶段直接用(不用再编译一次)
- cp xt-server/target/xt-server-*.jar ../xt-server.jar
artifacts:
paths:
- xt-server.jar
expire_in: 1 day
管理端类型检查与构建:
stage: build
image: node:20
cache: *pnpm-cache
before_script:
- corepack enable
- pnpm config set store-dir .pnpm-store
script:
- cd frontend-pc
- pnpm install --frozen-lockfile
- pnpm typecheck
# lint 只卡**本次改动涉及的文件**:模板自带的文件里有一批既有告警,
# 全量 lint 会让流水线永远是红的,从而失去意义(红久了就没人看了)。
#
# ⚠️ 这段只能在 Linux 上跑 —— 本机 Windows 的 cmd.exe 解析不了单引号 pathspec,
# 所以它是照 Linux 语义写的、本地验不到。首次上 runner 时留意,
# 见 docs/09-部署与运维.md §6「首次接入 CI 的检查单」。
- |
BASE="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-HEAD~1}"
FILES=$(git diff --name-only --diff-filter=ACMR "$BASE" HEAD -- frontend-pc/ 2>/dev/null \
| grep -E '\.(vue|ts)$' | sed 's|^frontend-pc/||' || true)
if [ -n "$FILES" ]; then
echo "只检查本次改动的文件:"; echo "$FILES"
npx eslint $FILES
else
echo "本次没有改前端文件,跳过 lint"
fi
- pnpm build
学生端 H5 构建:
stage: build
image: node:20
cache: *pnpm-cache
before_script:
- corepack enable
- pnpm config set store-dir .pnpm-store
script:
- cd frontend-mp
- pnpm install --frozen-lockfile
# ⚠️ 2026-10 起 H5 **不再由后端托管**(改由 nginx/网关托管,见 deploy/nginx/xt.conf),
# 所以这个 job 的定位变了:它不再是 e2e 的输入,而是**编译检查** ——
# 学生端没有单独的 typecheck job(vue-tsc 有 2 条已知报错),
# "能不能 build 出 H5"就是它最实际的把关点。
# (产物也不留 artifact:e2e 不消费它,发版时由部署流程自己构建。)
- pnpm build:h5
# ══════════════ 阶段 3:全套接口断言 ══════════════
.e2e-base: &e2e-base
stage: e2e
image: eclipse-temurin:21-jre
services:
- name: $XT_PG_IMAGE
alias: postgres
- name: $XT_REDIS_IMAGE
alias: redis
variables:
# 这两个容器只在本次流水线里活着,密码随机即可
POSTGRES_DB: "filldb"
POSTGRES_USER: "fill"
POSTGRES_PASSWORD: "ci_pg_pwd"
PG_PASSWORD: "ci_pg_pwd"
REDIS_PASSWORD: ""
before_script:
# e2e 脚本要 curl 发 multipart、要 python3 跑断言、要 psql 查库
- apt-get update -qq && apt-get install -y -qq python3 python3-pip curl postgresql-client
- pip3 install --quiet --break-system-packages openpyxl requests
# ⚠️ 每次随机的密钥:见文件头"密钥策略"
- export XT_JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n')
- export XT_AES_KEY=$(openssl rand -base64 32 | tr -d '\n')
- export XT_HMAC_KEY=$(openssl rand -base64 48 | tr -d '\n')
- export XT_RSA_PRIVATE_KEY=$(openssl rand -base64 512 | tr -d '\n')
- export REDIS_PASSWORD=""
script:
- echo "── 先单独验一次 test profile 能不能起来(失败了就是配置问题,不是脚本问题) ──"
- python3 scripts/check-test-profile.py
- echo "── 起后端(test profile) ──"
- |
java -jar xt-server.jar --spring.profiles.active=test \
--server.port=8080 > /tmp/xt-server.log 2>&1 &
for i in $(seq 1 90); do
if curl -sf http://localhost:8080/api/v1/auth/captcha > /dev/null; then
echo "后端已就绪(第 ${i} 次探测)"; break
fi
sleep 3
done
curl -sf http://localhost:8080/api/v1/auth/captcha > /dev/null \
|| { echo "后端没起来,日志尾部:"; tail -80 /tmp/xt-server.log; exit 1; }
- echo "── 依次跑全部 e2e 脚本 ──"
- |
fail=0
total=0
for f in $(ls scripts/e2e-*.py | sort); do
out=$(python3 "$f" 2>&1)
line=$(echo "$out" | grep -E '^通过 [0-9]+ 失败 [0-9]+ 共 [0-9]+' | tail -1)
if [ -z "$line" ]; then
echo " [无汇总] $f"; echo "$out" | tail -20; fail=1; continue
fi
n=$(echo "$line" | sed -E 's/^通过 ([0-9]+).*/\1/')
bad=$(echo "$line" | sed -E 's/.*失败 ([0-9]+).*/\1/')
total=$((total + n))
echo " $f:$n 条,失败 $bad"
if [ "$bad" != "0" ]; then fail=1; echo "$out" | tail -30; fi
done
echo "断言合计:$total"
[ "$fail" = "0" ] || exit 1
artifacts:
when: always
paths:
- /tmp/xt-server.log
接口断言(全套 E2E):
extends: .e2e-base
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
# ⚠️ 生产自检单独一个 job:**它验的是"配错时拒绝启动"**,
# 而这恰好是最容易被后来的改动悄悄破坏、且没人会发现的一件事
# (自检不生效时,一切"看起来都正常")
生产环境自检:
stage: e2e
image: eclipse-temurin:21-jre
script:
- apt-get update -qq && apt-get install -y -qq python3
- python3 scripts/e2e-prodguard.py
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH