Files
FruitServer/tools/smoke.sh
T
jianghaifeng b567365460 feat(deploy): D1 后端部署就绪(配置外置 + prod fail-fast + 自包含 Compose + 冒烟/回滚)
为什么:用户下午要部署,但全仓没有任何部署产物;且生产漏配密钥会静默使用代码里的 dev 默认值,
属上线阻塞项(04-部署架构 / 12-上线检查清单)。

- 配置外置(三件):DB_URL、JWT 双密钥(**发现 app-secret 此前没外置,一直用 dev 默认值**)、
  MEDIA_SIGNATURE_SECRET、MEDIA_SIGNATURE_REQUIRED、CORS_ALLOWED_ORIGINS、MEDIA_ROOT_DIR
- 新增三份 application-prod.yml:签名强制开启、CORS 收紧、媒资指向挂载点、日志级别
- 新增 ProdSecretGuard(@Profile("prod")):密钥缺失/仍是 dev 默认值/不足 32 位、签名未开、
  DB_URL 或 CORS 为空 → 一次列全并终止启动(exit 1)
- 新增 /healthz 轻量健康端点:查明 /actuator/health 的"超时"是旧 jar 未重启所致,项目本就
  没有 actuator 依赖,故不引新依赖;同时把 /healthz 加进安全白名单
- Docker 化:多阶段 Dockerfile(非 root、tini、依赖层缓存、ffmpeg 只给 worker)、.dockerignore、
  自包含 docker-compose.yml(含 PostgreSQL 18 + Redis + 命名卷 + 按序初始化 sql + healthcheck)
- tools/smoke.sh(失败非 0 退出)+ DEPLOY.md(Compose 主线 / 裸 jar 兜底 / 回滚 / 故障速查)
- 实测:mvn test 83 条全过;两入口 package 通过;prod 缺密钥退出码 1 且报错明确、配齐后正常启动;
  冒烟脚本全 PASS;本机无 Docker,镜像与 compose 未经构建验证(已在回执如实标注)
2026-10-16 13:01:08 +08:00

106 lines
4.2 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# Author: fruit.com.cn
#
# 发布后冒烟:确认两个入口"活着 + 文档在线 + 未授权路径按统一信封拒绝"。
# 任一检查失败 → 非 0 退出(可直接用在部署流水线里)。
#
# 用法:
# tools/smoke.sh # 默认 127.0.0.1:8081 / 127.0.0.1:8080
# APP_BASE=http://10.0.0.5:8081 ADMIN_BASE=http://10.0.0.5:8080 tools/smoke.sh
set -u -o pipefail
APP_BASE="${APP_BASE:-http://127.0.0.1:8081}"
ADMIN_BASE="${ADMIN_BASE:-http://127.0.0.1:8080}"
TIMEOUT="${SMOKE_TIMEOUT:-8}"
FAILED=0
pass() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; }
# http_code <url> [跟随重定向]:只取状态码,连不上返回 000
http_code() {
local follow="${2:-}"
if [ "$follow" = "follow" ]; then
curl -sL -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
else
curl -s -o /dev/null -m "$TIMEOUT" -w '%{http_code}' "$1" 2>/dev/null || echo 000
fi
}
# body <url>:取响应体(用于断言统一信封)
body() {
curl -s -m "$TIMEOUT" "$1" 2>/dev/null || true
}
# expect_code <期望> <url> <说明> [follow]
expect_code() {
local want="$1" url="$2" label="$3" follow="${4:-}" got
got="$(http_code "$url" "$follow")"
if [ "$got" = "$want" ]; then
pass "${label}(${got})"
else
fail "${label}:期望 ${want},实际 ${got} —— ${url}"
fi
}
# expect_body <url> <必须包含的片段> <说明>
# 只看 HTTP 200 是不够的:本项目未授权时也是 HTTP 200 + 信封 code=401,
# 所以健康检查/文档必须断言响应体的关键字段。
expect_body() {
local url="$1" needle="$2" label="$3" out
out="$(body "$url")"
if printf '%s' "$out" | grep -q "$needle"; then
pass "${label}"
else
fail "${label}:期望包含 ${needle},实际:$(printf '%s' "$out" | head -c 120)"
fi
}
# expect_envelope <url> <说明>:未授权路径必须返回统一响应体(code 字段),而不是挂住/空响应
expect_envelope() {
local url="$1" label="$2" out
out="$(body "$url")"
if printf '%s' "$out" | grep -q '"code"'; then
pass "${label}($(printf '%s' "$out" | head -c 80))"
else
fail "$label:未返回统一响应体 —— $url,实际:$(printf '%s' "$out" | head -c 120)"
fi
}
printf '冒烟开始:APP=%s ADMIN=%s\n' "$APP_BASE" "$ADMIN_BASE"
printf '\n[1/5] 健康检查 /healthz\n'
expect_code 200 "$APP_BASE/healthz" "app-api /healthz 可访问"
expect_body "$APP_BASE/healthz" '"status":"UP"' "app-api /healthz 返回 status=UP"
expect_code 200 "$ADMIN_BASE/healthz" "admin-api /healthz 可访问"
expect_body "$ADMIN_BASE/healthz" '"status":"UP"' "admin-api /healthz 返回 status=UP"
printf '\n[2/5] 客户端目录接口\n'
expect_code 200 "$APP_BASE/app/categories" "GET /app/categories"
expect_code 200 "$APP_BASE/app/home/feed" "GET /app/home/feed"
expect_body "$APP_BASE/app/categories" '"code":0' "GET /app/categories 返回 code=0"
printf '\n[3/5] 接口文档在线\n'
expect_code 200 "$APP_BASE/v3/api-docs" "app-api /v3/api-docs"
expect_code 200 "$ADMIN_BASE/v3/api-docs" "admin-api /v3/api-docs"
expect_body "$APP_BASE/v3/api-docs" '"openapi"' "app-api /v3/api-docs 是 OpenAPI 文档"
# /doc.html 是 springdoc 的 302 跳转,跟随重定向后应落到 Swagger UI(200)
expect_code 200 "$APP_BASE/doc.html" "app-api /doc.html(跟随 302)" follow
printf '\n[4/5] 未授权路径按统一信封拒绝(不挂住)\n'
expect_envelope "$APP_BASE/" "app-api /"
expect_envelope "$ADMIN_BASE/admin/auth/profile" "admin-api 未带令牌访问受保护接口"
printf '\n[5/5] 响应体完整性(/app/categories 必须是 {code,data})\n'
expect_body "$APP_BASE/app/categories" '"data"' "GET /app/categories 带 data 字段"
# 用错误的 HTTP 方法访问 POST-only 接口:只要是"统一信封"就算合格(当前实现返回 code=500 的系统异常信封)
expect_envelope "$ADMIN_BASE/admin/auth/login" "admin-api 用 GET 访问 POST-only 接口仍返回统一信封"
if [ "$FAILED" -eq 0 ]; then
printf '\n\033[32m冒烟通过\033[0m:全部检查通过\n'
exit 0
fi
printf '\n\033[31m冒烟失败\033[0m:存在未通过项,请先看上面的 FAIL 行\n'
exit 1