Files
danyang-match/tests/uat/run-uat.sh
T
WorkBuddy f96dcb8aa6 fix(admin): 管理端入口收敛到 /admin/ 子路径,staging 端口 13521,修复生产 /admin/ 假成功
承接 rc.15 的管理端端口收敛(3000 → 13520),本轮把「入口」也收敛到一处:

- staging admin 发布端口 3001 → 13521(保持「生产 + 1」错开),同步
  tests/uat/run-uat.sh 的默认 UAT_ADMIN_URL。
- 管理端产物改为 /admin/ 子路径部署:vite 构建 base=/admin/(dev 仍为 /,开发与
  排查习惯不变)、router history base 跟随 import.meta.env.BASE_URL、三处容器化
  入口(生产 nginx / 本地整套栈 nginx / admin-web 容器内 nginx)统一为「不剥离
  前缀」转发,形态一致。
- 修复生产 nginx /admin/ 的假成功:原为 alias /usr/share/nginx/html/,而那正是
  nginx:alpine 纯净镜像自带的默认页目录(管理端产物不在该容器内),访问返回
  200 + nginx 欢迎页 —— 浏览器不报错、状态码正常,只是打开的不是管理端,比 404
  更难发现。改为 upstream admin_web + proxy_pass,并以真实两跳容器实测:
  /admin/ → 200 且为管理端标题与 /admin/assets/*、/admin/dashboard → 200(SPA
  兜底)、/admin/brand/logo-mark.png → 200、根路径 → 显式 404(用 location = /
  钉住,不依赖「本配置恰好没有 server 级 root」这一偶然事实)。
- 品牌资源引用统一走新增的 src/brand.ts(public/ 资源不受 vite base 影响,原 5 处
  写死根路径,子路径部署下会裂图);favicon 由仓库内并不存在的 /favicon.svg 改为
  存在的品牌标。
- 补全 tests/uat/run-uat.sh 被截断的尾部:原文件停在 `if [ -`,bash -n 不通过、
  脚本根本无法运行;顺带把此前「定义了却无人消费」的 ADMIN_URL 接到管理端可达性
  实测上,并补结果汇总与非零退出码。
- ui-tests:ui-document-baseline-contracts 新增两类一致性断言(staging 发布端口 ⇄
  run-uat.sh 默认入口、子路径部署的五处约束);admin-reference-contracts 的资产
  断言适配 brand 模块(断言「组件经 brand 模块引用」+「brand 模块指向正式资产」)。
  6 条新增断言全部经负向注入验证(注入对应缺陷 → 守卫变红且信息指向正确对象)。

验证:UI 门控 14/14 绿;vue-tsc EXIT=0;vite build EXIT=0 且产物资源均为
/admin/ 前缀;三份 nginx 配置 nginx -t 通过。

注:docs/STATUS.md 含此前的在途文档改动(非本次工作,且其中引用了已被删除的
plans 文件),故未纳入本提交;本轮的口径更新已写入该文件待一并处理。
2026-09-18 20:51:12 +08:00

288 lines
11 KiB
Bash
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.
#!/bin/bash
# ============================================
# 丹阳公益婚恋服务平台 — UAT 自动化检查脚本
# 此脚本在 UAT 环境启动后运行,自动化验证各角色核心流程
# ============================================
set -euo pipefail
BASE_URL="${UAT_BASE_URL:-http://localhost:8080}"
# 管理端 UAT 入口。
# 13521 = staging admin 容器发布端口(infrastructure/docker-compose.staging.yml),
# 按「生产 admin 端口 + 1」错开;/admin/ 是管理端产物的部署子路径
# (base 由 admin-web/vite.config.ts 决定)——根路径不提供服务,访问根会 301 到 /admin/。
# 此默认值由 ui-tests/ui-document-baseline-contracts.mjs 钉住,改端口时须两处同步。
ADMIN_URL="${UAT_ADMIN_URL:-http://localhost:13521/admin/}"
PLATFORM_URL="${UAT_PLATFORM_URL:-https://match.danyang.gov.cn}"
TOKEN_FILE="/tmp/uat_token.txt"
PASS=0
FAIL=0
SKIP=0
log_result() {
local test_name="$1"
local status="$2"
local detail="${3:-}"
if [ "$status" = "PASS" ]; then
PASS=$((PASS + 1))
echo " ✅ $test_name"
elif [ "$status" = "FAIL" ]; then
FAIL=$((FAIL + 1))
echo " ❌ $test_name — $detail"
else
SKIP=$((SKIP + 1))
echo " ⏭ $test_name (跳过)"
fi
}
echo "============================================"
echo " 丹阳公益婚恋平台 — UAT 自动化检查"
echo " $(date '+%Y-%m-%d %H:%M:%S')"
echo "============================================"
echo ""
# ========================================
# 1. 基础设施检查
# ========================================
echo "【1. 基础设施检查】"
# 健康检查
if curl -sf "${BASE_URL}/api/v1/health/live" > /dev/null 2>&1; then
log_result "后端健康检查" "PASS"
else
log_result "后端健康检查" "FAIL" "后端服务不可达"
fi
# 版本信息
VERSION=$(curl -sf "${BASE_URL}/version" 2>/dev/null || echo "unknown")
log_result "版本信息接口" "PASS" "version=${VERSION}"
# PostgreSQL
if docker exec danyang-match-postgres pg_isready -U danyang -d danyang_match 2>/dev/null; then
log_result "PostgreSQL 连接" "PASS"
else
log_result "PostgreSQL 连接" "FAIL" "数据库不可用"
fi
# Redis
if docker exec danyang-match-redis redis-cli -a "${REDIS_PASSWORD:-staging}" ping 2>/dev/null | grep -q PONG; then
log_result "Redis 连接" "PASS"
else
log_result "Redis 连接" "FAIL" "Redis 不可用"
fi
# MinIO
if curl -sf "http://localhost:9000/minio/health/live" 2>/dev/null | grep -q online; then
log_result "MinIO 连接" "PASS"
else
log_result "MinIO 连接" "FAIL" "对象存储不可用"
fi
echo ""
# ========================================
# 2. 认证流程
# ========================================
echo "【2. 认证流程】"
# Mock WeChat登录获取Token
LOGIN_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
-H "Content-Type: application/json" \
-d '{"code":"uat-test-code"}' 2>/dev/null || echo '{}')
if echo "$LOGIN_RESP" | grep -q "accessToken"; then
ACCESS_TOKEN=$(echo "$LOGIN_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('accessToken',''))" 2>/dev/null || echo "")
if [ -n "$ACCESS_TOKEN" ]; then
echo "$ACCESS_TOKEN" > "$TOKEN_FILE"
log_result "Mock微信登录" "PASS"
else
log_result "Mock微信登录" "FAIL" "未能获取Token"
fi
else
log_result "Mock微信登录" "SKIP" "Mock Provider 不可用(生产环境需真实WeChat)"
fi
# Token过期检查
if [ -f "$TOKEN_FILE" ]; then
EXPIRE_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/refresh" \
-H "Content-Type: application/json" \
-d "{\"refreshToken\":\"$(cat $TOKEN_FILE)\"}" 2>/dev/null || echo '{}')
if echo "$EXPIRE_RESP" | grep -q "accessToken"; then
log_result "Token刷新" "PASS"
else
log_result "Token刷新" "FAIL" "刷新失败"
fi
fi
# 无效Token拒绝
INVALID_RESP=$(curl -s -o /dev/null -w "%{http_code}" \
"${BASE_URL}/api/v1/users/me" \
-H "Authorization: Bearer invalid-token-here" 2>/dev/null || echo "000")
if [ "$INVALID_RESP" = "401" ]; then
log_result "无效Token拒绝" "PASS"
else
log_result "无效Token拒绝" "FAIL" "HTTP ${INVALID_RESP}"
fi
echo ""
# ========================================
# 3. 核心业务流程
# ========================================
echo "【3. 核心业务流程】"
# 3.1 用户资料创建
if [ -f "$TOKEN_FILE" ]; then
CREATE_PROFILE=$(curl -sf -X POST "${BASE_URL}/api/v1/profile" \
-H "Authorization: Bearer $(cat $TOKEN_FILE)" \
-H "Content-Type: application/json" \
-d '{"displayName":"UAT测试用户","gender":"male","birthDate":"1990-01-01",
"regionCode":"321100","regionName":"丹阳市","maritalStatus":"single",
"childrenStatus":"none","educationCode":"bachelor","occupationCode":"other"}' \
2>/dev/null || echo '{}')
if echo "$CREATE_PROFILE" | grep -q "success\|profileId"; then
log_result "用户资料创建" "PASS"
else
log_result "用户资料创建" "FAIL" "资料创建失败"
fi
fi
# 3.2 个人资料完整性
if [ -f "$TOKEN_FILE" ]; then
GET_PROFILE=$(curl -sf "${BASE_URL}/api/v1/profile" \
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
if echo "$GET_PROFILE" | grep -q "completion"; then
COMPLETION=$(echo "$GET_PROFILE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('completion',0))" 2>/dev/null || echo "0")
if [ "$COMPLETION" -ge 60 ]; then
log_result "资料完整度≥60%" "PASS" "completion=${COMPLETION}%"
else
log_result "资料完整度≥60%" "FAIL" "completion=${COMPLETION}%"
fi
else
log_result "资料完整度≥60%" "SKIP" "无法获取资料"
fi
fi
# 3.3 推荐列表获取
if [ -f "$TOKEN_FILE" ]; then
REC_RESP=$(curl -sf "${BASE_URL}/api/v1/matches/recommendations?limit=6" \
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
if echo "$REC_RESP" | grep -q "list\|items"; then
log_result "推荐列表获取" "PASS"
else
log_result "推荐列表获取" "FAIL" "推荐接口异常"
fi
fi
# 3.4 喜欢操作幂等
if [ -f "$TOKEN_FILE" ]; then
LIKE_RESP=$(curl -sf -X POST "${BASE_URL}/api/v1/matches/like/test-user-123" \
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
# 第二次 Like 应返回 ALREADY_LIKED
log_result "Like幂等性" "SKIP" "需真实Match数据验证"
fi
echo ""
# ========================================
# 4. 安全测试
# ========================================
echo "【4. 安全测试】"
# 4.1 SQL注入防护(参数化查询验证)
INJECTION_TEST=$(curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
-H "Content-Type: application/json" \
-d '{"code":"'$(echo "'; DROP TABLE users; --")'"}' \
-w "%{http_code}" 2>/dev/null || echo "000")
if [ "$INJECTION_TEST" = "400" ] || [ "$INJECTION_TEST" = "401" ]; then
log_result "SQL注入防护" "PASS"
else
log_result "SQL注入防护" "FAIL" "HTTP ${INJECTION_TEST}"
fi
# 4.2 敏感信息脱敏
if [ -f "$TOKEN_FILE" ]; then
USER_RESP=$(curl -sf "${BASE_URL}/api/v1/users/me" \
-H "Authorization: Bearer $(cat $TOKEN_FILE)" 2>/dev/null || echo '{}')
PHONE=$(echo "$USER_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('data',{}).get('phone',''))" 2>/dev/null || echo "")
if echo "$PHONE" | grep -q '\*'; then
log_result "手机号脱敏" "PASS"
elif [ -z "$PHONE" ]; then
log_result "手机号脱敏" "PASS" "接口不返回phone字段(已脱敏)"
else
log_result "手机号脱敏" "FAIL" "手机号明文暴露: ${PHONE}"
fi
fi
# 4.3 HTTPS强制
HTTPS_CHECK=$(curl -sf -o /dev/null -w "%{http_code}" "http://localhost/" 2>/dev/null || echo "000")
if [ "$HTTPS_CHECK" = "301" ] || [ "$HTTPS_CHECK" = "308" ] || [ "$HTTPS_CHECK" = "200" ]; then
log_result "HTTPS重定向" "PASS" "HTTP→HTTPS 301"
else
log_result "HTTPS重定向" "SKIP" "HTTP直接访问未配置"
fi
# 4.4 Rate Limit 登录接口
for i in $(seq 1 15); do
curl -sf -X POST "${BASE_URL}/api/v1/auth/wechat/login" \
-H "Content-Type: application/json" \
-d '{"code":"spam-test"}' > /dev/null 2>&1
done
RATE_LIMIT_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "${BASE_URL}/api/v1/auth/wechat/login" \
-H "Content-Type: application/json" \
-d '{"code":"spam-test"}' 2>/dev/null || echo "200")
if [ "$RATE_LIMIT_CODE" = "429" ]; then
log_result "登录限流" "PASS"
else
log_result "登录限流" "SKIP" "开发环境未启用严格限流(生产已配置)"
fi
echo ""
# ========================================
# 5. 部署验证
# ========================================
echo "【5. 部署验证】"
# 5.1 容器状态
if docker compose -f infrastructure/docker-compose.prod.yml ps 2>/dev/null | grep -q "healthy"; then
log_result "所有容器健康" "PASS"
else
log_result "所有容器健康" "SKIP" "Docker Compose 未运行(使用其他方式部署)"
fi
# 5.2 备份文件存在
# 备份由 infrastructure/scripts/backup.sh 产出(容器内 /app/backups,宿主机为 infrastructure/backups)。
if ls infrastructure/backups/*.sql.gz > /dev/null 2>&1; then
BACKUP_COUNT=$(ls -1 infrastructure/backups/*.sql.gz 2>/dev/null | wc -l | tr -d ' ')
log_result "数据库备份文件" "PASS" "发现 ${BACKUP_COUNT} 个备份"
else
log_result "数据库备份文件" "SKIP" "未发现备份文件(首次部署前正常)"
fi
# 5.3 管理端页面可达
# 2026-09-18 前 ${ADMIN_URL} 是个定义了却无人消费的常量 —— 端口/子路径改了、
# 脚本没同步,也不会有任何检查变红。此处把它接到实测上:
# 期望 200,且期望的是 `/admin/` 子路径(管理端产物 base 为 /admin/,访问根路径会 301)。
ADMIN_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${ADMIN_URL}" 2>/dev/null || echo "000")
if [ "$ADMIN_CODE" = "200" ]; then
log_result "管理端页面可达" "PASS" "${ADMIN_URL}"
else
log_result "管理端页面可达" "FAIL" "${ADMIN_URL} → HTTP ${ADMIN_CODE}"
fi
echo ""
# ========================================
# 6. 结果汇总
# ========================================
echo "============================================"
echo " UAT 检查结果:PASS=${PASS} FAIL=${FAIL} SKIP=${SKIP}"
echo "============================================"
# 有 FAIL 时以非零退出:让 CI/人工调用方靠退出码就能发现,而不是必须滚屏找 ❌。
if [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0