部署验证清单
本文档提供完整的部署验证步骤,确保 fulla 全栈系统在 Windows Docker Desktop 或 Linux 生产环境上正确运行。
快速验证(5 分钟)
1. 检查所有容器状态
# Windows
docker compose -f deploy/docker/docker-compose.yml ps
# Linux
docker compose -f deploy/docker/docker-compose.prod.yml --env-file .env.docker ps
预期结果:所有容器状态为 Up 或 Up (healthy)
| 容器名 | 状态 | 端口映射 |
|---|---|---|
| fulla-frontend | Up | 8080:80 |
| fulla-admin | Up | 8081:80 |
| fulla-backend | Up (healthy) | 5555:5555 |
| fulla-postgres | Up (healthy) | 5433:5432 |
| fulla-redis | Up | 6380:6379 |
| fulla-prometheus | Up | 9090:9090 |
2. 健康检查
# 后端健康端点
curl http://localhost:5555/health
# 预期输出
{"status":"healthy","timestamp":"2026-08-26T10:30:00Z"}
3. 数据库连接测试
# 进入 postgres 容器
docker exec -it fulla-postgres psql -U fulla_user -d fulla_db -c "\dt"
# 预期输出:OAuth2 相关表列表
# oauth2_clients, oauth2_codes, oauth2_access_tokens, oauth2_refresh_tokens,
# oauth2_scopes, users, roles, user_roles, organizations, audit_logs 等(V026 后共 21 张)
4. 前端访问测试
在浏览器中打开:
- 用户前端:http://localhost:8080 或 https://your-domain.com
- 管理后台:http://localhost:8081 或 https://your-domain.com/admin
预期结果:页面正常加载,无 404 或 502 错误
完整验证(30 分钟)
阶段一:基础设施验证
1.1 PostgreSQL 验证
# 连接测试
docker exec fulla-postgres pg_isready -U fulla_user
# 预期输出:/var/run/postgresql:5432 - accepting connections
# 表结构检查
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
"
# 预期表列表(V002-V026 实际 schema,均带 oauth2_ 前缀):
# - oauth2_access_tokens, oauth2_refresh_tokens, oauth2_codes
# - oauth2_clients, oauth2_scopes, oauth2_client_scopes
# - oauth2_user_consents, oauth2_subject_mappings, oauth2_device_codes
# - users, roles, permissions, user_roles, role_permissions
# - organizations, audit_logs, webauthn_credentials 等
# 数据库版本检查
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "SELECT version();"
# 预期:PostgreSQL 17.x(deploy compose 默认 postgres:17-alpine;
# 显式钉回 15 的存量部署此处应为 15.x,见 docs/operate/postgresql-major-upgrade.md)
1.2 Redis 验证
# 进入 redis 容器
docker exec -it fulla-redis redis-cli -a redis_secret_pass ping
# 预期输出:PONG
# 测试读写
docker exec fulla-redis redis-cli -a redis_secret_pass SET test_key "hello"
docker exec fulla-redis redis-cli -a redis_secret_pass GET test_key
# 预期输出:"hello"
# 检查内存使用
docker exec fulla-redis redis-cli -a redis_secret_pass INFO memory
# 预期:used_memory_human 显示合理的内存占用
1.3 网络连通性验证
# 从后端容器测试数据库连接
docker exec fulla-backend ping -c 3 fulla-postgres
# 预期:3 packets transmitted, 3 received, 0% packet loss
# 从后端容器测试 Redis 连接
docker exec fulla-backend ping -c 3 fulla-redis
# 预期:3 packets transmitted, 3 received, 0% packet loss
# 检查 DNS 解析
docker exec fulla-backend nslookup fulla-postgres
# 预期:返回 fulla-postgres 的容器 IP 地址(如 172.x.x.x)
阶段二:数据库初始化验证
2.1 检查 Seed 数据
# 检查管理员用户(角色经 user_roles 关联,users 表本身没有 role 列)
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "
SELECT u.username, u.email, r.name AS role, u.created_at
FROM users u
LEFT JOIN user_roles ur ON ur.user_id = u.id
LEFT JOIN roles r ON r.id = ur.role_id
WHERE u.username = 'admin';
"
# 预期输出:
# username | email | role | created_at
# ----------+-------------------+-------+----------------------------
# admin | [email protected] | admin | 2026-xx-xx xx:xx:xx
# 检查默认客户端(表名带 oauth2_ 前缀;名称列是 name)
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "
SELECT client_id, name, client_type, token_endpoint_auth_method
FROM oauth2_clients
WHERE client_id IN ('admin-console', 'vue-client');
"
# 预期输出:admin-console 与 vue-client 均为 PUBLIC(token_endpoint_auth_method = none)
# 检查默认 Scopes(scope 名称列是 name)
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "
SELECT name, description
FROM oauth2_scopes
LIMIT 5;
"
# 预期输出:openid, profile, email, admin 等标准 scope
2.2 验证数据库迁移
# 检查 migrations 表(如果有的话)
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "\d schema_migrations"
# 或检查表结构完整性
docker exec fulla-postgres psql -U fulla_user -d fulla_db -c "
SELECT COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
"
# 预期:table_count >= 15(V026 后实测 21 张 public 表)
阶段三:后端 API 验证
3.1 获取管理员令牌
admin-console 是 PUBLIC 客户端(token_endpoint_auth_method=none,无 client_secret,不支持 password grant),令牌必须走 授权码 + PKCE 两步流程(F-011:PUBLIC 客户端强制 PKCE)。以下等价于 scripts/backend/test-admin-endpoints.sh 的 setup 步骤:
# 1) 登录换取授权码(表单编码;code_challenge = BASE64URL(SHA256(code_verifier)))
CODE_VERIFIER=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=' | tr -d '+/' | head -c 43)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
LOGIN_RESP=$(curl -s -X POST http://localhost:5555/oauth2/login \
-d "username=admin&password=admin" \
-d "client_id=admin-console&redirect_uri=http://localhost:5174/admin/callback" \
-d "scope=openid+profile+admin&state=verify-state" \
-d "code_challenge=$CODE_CHALLENGE&code_challenge_method=S256&json=true")
CODE=$(echo "$LOGIN_RESP" | jq -r '.code')
# 2) 授权码换令牌(表单编码;PUBLIC 客户端只带 client_id,不能携带任何 secret)
curl -s -X POST http://localhost:5555/oauth2/token \
-d "grant_type=authorization_code&code=$CODE" \
-d "redirect_uri=http://localhost:5174/admin/callback" \
-d "client_id=admin-console&code_verifier=$CODE_VERIFIER"
# 预期响应(保存 access_token):
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JH7xN1yQ9X2...",
"scope": "openid profile admin"
}
# 设置环境变量(后续测试使用)
export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Windows 下可使用仓库自带的
scripts/backend/test-admin-endpoints.ps1完成同样的登录+令牌流程。
3.2 验证令牌内省(Token Introspection)
# 内省令牌(RFC 7662,表单编码)
curl -s -X POST http://localhost:5555/oauth2/introspect \
-d "token=$TOKEN" \
-d "token_type_hint=access_token" \
-d "client_id=admin-console"
# 预期响应:
{
"active": true,
"client_id": "admin-console",
"username": "admin",
"scope": "openid profile admin",
"exp": 1719123456,
"iat": 1719119856,
"sub": "admin",
"iss": "http://localhost:5555"
}
# 测试无效令牌
curl -s -X POST http://localhost:5555/oauth2/introspect \
-d "token=invalid_token" \
-d "token_type_hint=access_token" \
-d "client_id=admin-console"
# 预期响应:{"active": false}
3.3 刷新令牌(Refresh Token)
# 使用 refresh_token 获取新的 access_token(表单编码;
# PUBLIC 客户端只带 client_id —— 携带 client_secret 反而会被 F-017 拒绝)
curl -s -X POST http://localhost:5555/oauth2/token \
-d "grant_type=refresh_token" \
-d "refresh_token=tGzv3JH7xN1yQ9X2..." \
-d "client_id=admin-console"
# 预期响应:返回新的 access_token 和 refresh_token
{
"access_token": "新的 access token...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "新的 refresh token...",
"scope": "openid profile admin"
}
3.4 撤销令牌(Token Revocation)
# 撤销令牌(RFC 7009,表单编码;客户端认证方式须与注册的
# token_endpoint_auth_method 一致 —— PUBLIC 客户端仅 client_id)
curl -s -X POST http://localhost:5555/oauth2/revoke \
-d "token=$TOKEN" \
-d "token_type_hint=access_token" \
-d "client_id=admin-console"
# 预期响应:HTTP 200 OK(空响应体)
# 验证令牌已被撤销
curl -s -X POST http://localhost:5555/oauth2/introspect \
-d "token=$TOKEN" \
-d "token_type_hint=access_token" \
-d "client_id=admin-console"
# 预期响应:{"active": false}
阶段四:管理后台 API 验证
4.1 用户管理 API
# 获取用户列表
curl -X GET http://localhost:5555/api/admin/users \
-H "Authorization: Bearer $TOKEN"
# 预期响应:用户列表 JSON
{
"users": [
{
"user_id": 1,
"username": "admin",
"role": "admin",
"created_at": "2026-08-26T10:00:00Z",
"updated_at": "2026-08-26T10:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20
}
# 创建新用户
curl -X POST http://localhost:5555/api/admin/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"email": "[email protected]",
"password": "TestPassword123!",
"role": "user"
}'
# 预期响应:HTTP 201 Created
{
"user_id": 2,
"username": "testuser",
"role": "user",
"created_at": "2026-08-26T10:30:00Z"
}
# 获取单个用户详情
curl -X GET http://localhost:5555/api/admin/users/2 \
-H "Authorization: Bearer $TOKEN"
# 预期响应:显示 testuser 的详细信息
4.2 客户端管理 API
# 获取客户端列表
curl -X GET http://localhost:5555/api/admin/clients \
-H "Authorization: Bearer $TOKEN"
# 预期响应:客户端列表(客户端 secret 一律不回显;哈希仅存于库中)
{
"clients": [
{
"client_id": "admin-console",
"name": "Admin Console",
"client_type": "PUBLIC",
"token_endpoint_auth_method": "none",
"redirect_uris": ["http://localhost:5174/admin/callback"],
"allowed_grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "admin"]
}
],
"total": 1
}
# 创建新客户端
curl -X POST http://localhost:5555/api/admin/clients \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"client_id": "test-client",
"name": "Test Client",
"client_type": "CONFIDENTIAL",
"client_secret": "test-secret",
"redirect_uris": ["http://localhost:8080/callback"],
"allowed_grant_types": ["authorization_code", "refresh_token"],
"scopes": ["openid", "profile", "email"]
}'
# 预期响应:HTTP 201 Created(响应含新生成客户端的元数据;secret 不回显)
4.3 Scope 管理 API
# 获取所有 scopes
curl -X GET http://localhost:5555/api/admin/scopes \
-H "Authorization: Bearer $TOKEN"
# 预期响应:scope 列表(scope 名称字段为 name,与 oauth2_scopes 表一致)
{
"scopes": [
{"name": "openid", "description": "OpenID Connect"},
{"name": "profile", "description": "User profile"},
{"name": "email", "description": "User email"},
{"name": "admin", "description": "Administrative access"}
]
}
# 创建新 scope
curl -X POST http://localhost:5555/api/admin/scopes \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "read",
"description": "Read access to user resources"
}'
# 预期响应:HTTP 201 Created
阶段五:前端功能验证
5.1 用户前端验证
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| 访问首页 | 打开 http://localhost:8080 | 显示登录页面 |
| 用户注册 | 填写注册表单(用户名、邮箱、密码) | 注册成功,跳转到登录页 |
| 用户登录 | 使用刚注册的账号登录 | 登录成功,跳转到个人资料页 |
| 访问个人资料 | 点击"个人资料"菜单 | 显示用户信息(用户名、邮箱) |
| 修改密码 | 输入旧密码和新密码 | 密码修改成功,需要重新登录 |
| 退出登录 | 点击"退出"按钮 | 退出成功,跳转到登录页 |
5.2 管理后台验证
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| 访问管理后台 | 打开 http://localhost:8081/admin | 显示管理后台登录页 |
| 管理员登录 | 使用 admin/admin 登录 | 登录成功,显示仪表板 |
| 应用管理 | 点击"应用"菜单 | 显示客户端列表(至少有 admin-console) |
| 创建应用 | 点击"新建应用",填写表单 | 应用创建成功,出现在列表中 |
| 用户管理 | 点击"用户"菜单 | 显示用户列表(至少有 admin 和刚注册的用户) |
| Token 管理 | 点击"Token"菜单 | 显示 active tokens 列表 |
5.3 OAuth2 授权码流程验证
# 步骤 1:构建授权 URL(在浏览器中访问)
# 注意:redirect_uri 必须与客户端注册值精确匹配(vue-client 种子注册的是
# http://127.0.0.1:8080/callback —— 用 localhost 会被拒绝)
# http://localhost:5555/oauth2/authorize?
# response_type=code&
# client_id=vue-client&
# redirect_uri=http://127.0.0.1:8080/callback&
# scope=openid profile email&
# state=random_state_value
# 预期:重定向到登录页面
# 步骤 2:用户登录
# 使用测试账号登录(如 testuser)
# 预期:显示授权确认页面
# 步骤 3:用户授权
# 点击"授权"按钮
# 预期:重定向到 redirect_uri,携带 authorization code
# http://127.0.0.1:8080/callback?code=xxx&state=random_state_value
# 步骤 4:交换令牌(vue-client 是 PUBLIC 客户端 → 必须带 PKCE code_verifier,
# 且不能携带 client_secret)
curl -s -X POST http://localhost:5555/oauth2/token \
-d "grant_type=authorization_code" \
-d "code=从回调中获取的code" \
-d "redirect_uri=http://127.0.0.1:8080/callback" \
-d "client_id=vue-client" \
-d "code_verifier=登录时使用的PKCE_verifier"
# 预期响应:返回 access_token 和 refresh_token
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xxx",
"scope": "openid profile email"
}
阶段五·补充:邮件服务验证
邮件服务有两种模式,由后端 getEmailService() 根据 FULLA_SMTP_* 环境变量决定。
5.4 确认邮件服务模式
# 查看后端启动日志中的邮件服务模式
docker logs fulla-backend 2>&1 | grep -i "Email service"
预期输出(二选一):
- Console 模式(未配置 SMTP):
Email service: Console (set FULLA_SMTP_* env vars to enable SMTP) - SMTP 模式(已配置):
Email service: SMTP (smtp.163.com:465)
5.5 邮箱验证邮件
操作:登录用户前端 → Profile 页面 → 点击"发送邮箱验证"
| 模式 | 验证方式 |
|---|---|
| Console 模式 | 邮件内容打到后端日志,从中复制验证链接 |
| SMTP 模式 | 收件箱应收到真实邮件 |
Console 模式下查看验证链接:
docker logs fulla-backend --tail 50 2>&1 | grep -A 5 -iE "verify|email"
# 预期:含 "verify-email?token=xxx" 的链接
SMTP 模式下验证邮件发送:
# 触发发送后,检查后端是否有 SMTP 错误
docker logs fulla-backend --tail 50 2>&1 | grep -iE "smtp|email|curl"
# 预期:无 ERROR 级别日志;收件箱收到 "Verify Your Email" 邮件
5.6 密码重置邮件
操作:前端"忘记密码"页面 → 输入邮箱 → 提交
- 预期响应(防枚举):无论邮箱是否存在,统一返回
If the email exists, a reset link has been sent - Console 模式下,重置链接同样打到后端日志