diff --git a/docs/MIGRATIONS.md b/docs/MIGRATIONS.md new file mode 100644 index 0000000..965c007 --- /dev/null +++ b/docs/MIGRATIONS.md @@ -0,0 +1,134 @@ +# 数据库迁移 + +## 概述 + +所有数据库结构变更通过 SQL 迁移脚本管理,存储在 `migrations/` 目录,按序号命名。 + +部署时 `deploy.sh` 自动检测并执行未运行的迁移: + +``` +upload_dir "迁移文件" migrations → 上传到服务器 +run_migrations → 逐条执行未迁移的脚本 +``` + +已执行的迁移记录在数据库 `_migrations` 表中。 + +## 迁移文件清单 + +| 序号 | 文件 | 操作 | 说明 | +|------|------|------|------| +| 001 | `001_add_payment_fields.sql` | `ALTER TABLE users ADD COLUMN` | 添加 `is_member`, `is_admin`, `membership_expires_at` 字段 | +| 002a | `002_add_payment_orders.sql` | `CREATE TABLE` | 创建 `payment_orders` 表 | +| 002b | `002_add_user_profile_fields.sql` | `ALTER TABLE users ADD COLUMN` | 添加 `avatar_url`, `nickname` 字段 | +| 003 | `003_set_all_users_paid.sql` | `SELECT 1`(空操作) | **历史遗留**:原将所有用户设为付费,已改为空操作 | +| 004 | `004_add_refresh_tokens.sql` | `CREATE TABLE` | 创建 `refresh_tokens` 表(双 Token 机制) | +| 005 | `005_add_web_login_codes.sql` | `CREATE TABLE` | 创建 `web_login_codes` 表(网页端扫码登录) | +| 006 | `006_add_performance_indexes.sql` | `CREATE INDEX` | 添加性能索引(`user_id`, `date`, `is_favorite`) | +| 007 | `007_add_spot_check_count.sql` | `ALTER TABLE` | `weather_data` 添加 `spotcheckcount` 字段 | +| 009 | `009_add_payment_audit_log.sql` | `CREATE TABLE` | 创建 `payment_audit_log` 表(支付审计日志) | +| 010 | `010_rename_paid_fields.sql` | `ALTER TABLE RENAME COLUMN` | `is_paid` → `is_member`, `paid_expires_at` → `membership_expires_at` | +| 011 | `011_add_notifications.sql` | `CREATE TABLE` | 创建 `notifications` 表(通知系统) | + +> **注意**:序号不连续(无 008)是因为跳过了一个已撤销的迁移。新迁移请继续使用下一可用序号(如 012)。 + +## 迁移说明 + +### 003 — 历史遗留 + +此迁移**最初**执行了 `UPDATE users SET is_paid = true`,将所有已有用户设为永久付费会员(用于早期测试)。发现后已改为空操作 `SELECT 1 WHERE 1 = 1`。**已有的被修改的数据需要用脚本手动回退**。 + +### 009 — 自动建表 + +此迁移对应的代码中有幂等 `CREATE TABLE IF NOT EXISTS` 逻辑,即使未通过迁移系统执行,代码也会自动建表。迁移文件本身保留以确保正式部署时 `_migrations` 表记录完整。 + +### 010 — 字段重命名 + +`is_paid` → `is_member`(会员身份凭证,即使过期也保留) +`paid_expires_at` → `membership_expires_at`(NULL 表示永久会员) + +## 如何创建新迁移 + +```bash +# 1. 创建迁移文件 +touch migrations/012_your_description.sql + +# 2. 写入 SQL +vim migrations/012_your_description.sql + +# 3. 部署时自动执行 +./deploy.sh production # 会自动运行新迁移 +./deploy.sh development # 开发环境同理 +``` + +### 迁移编写规范 + +| 规则 | 说明 | +|------|------| +| **幂等** | 使用 `IF NOT EXISTS` / `IF EXISTS`,可重复执行 | +| **事务** | 关键操作使用 `BEGIN` / `COMMIT` | +| **回滚** | 考虑失败场景,避免不可逆操作 | +| **注释** | 文件头部写清目的、日期、依赖关系 | + +示例: + +```sql +-- ============================================ +-- 迁移: 012_add_example.sql +-- 目的: 创建示例表 +-- 日期: 2026-05-26 +-- ============================================ + +CREATE TABLE IF NOT EXISTS example ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); +``` + +## 如何手动执行迁移 + +在服务器上直接运行: + +```bash +ssh root@1panel-server +docker exec -i 1Panel-postgresql-FtMo psql -U milkydata -d milkydata \ + < /root/rust/rust_backend/migrations/012_your_migration.sql + +# 记录到 _migrations 表 +docker exec 1Panel-postgresql-FtMo psql -U milkydata -d milkydata \ + -c "INSERT INTO _migrations (name) VALUES ('012_your_migration.sql');" +``` + +## 迁移测试 + +部署后 `test_migration_status` 会自动检查: + +- `_migrations` 表是否存在 +- 已执行的迁移数量 +- 最后一次执行的迁移文件名 + +## 当前执行状态 + +### 开发库 `milkydata_dev` + +``` +001_add_payment_fields.sql +002_add_payment_orders.sql +002_add_user_profile_fields.sql +003_set_all_users_paid.sql +004_add_refresh_tokens.sql +005_add_web_login_codes.sql +006_add_performance_indexes.sql +007_add_spot_check_count.sql +009_add_payment_audit_log.sql +010_rename_paid_fields.sql +``` + +### 生产库 `milkydata` + +`_migrations` 表尚未创建。缺失的迁移: + +- 004 (refresh_tokens) +- 005 (web_login_codes) +- 009 (payment_audit_log) +- 011 (notifications) diff --git a/lib/test.sh b/lib/test.sh index 23c8895..dda8357 100644 --- a/lib/test.sh +++ b/lib/test.sh @@ -103,9 +103,58 @@ test_response_content() { log_info "响应内容验证通过"; return 0 } +# ---------- 新增测试 ---------- + +test_mock_login() { + if [ "${APP_ENV}" != "development" ]; then + log_info " mock-login 仅在开发环境中测试" + return 0 + fi + local key; key=$(remote "grep 'MOCK_LOGIN_KEY' /etc/systemd/system/${SERVICE_NAME} 2>/dev/null | head -1 | sed 's/.*MOCK_LOGIN_KEY=//'" 2>/dev/null || echo "") + [ -z "$key" ] && { log_warn " 未配置 MOCK_LOGIN_KEY,跳过"; return 0; } + + local resp; resp=$(remote "curl -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/api/mock-login' -H 'X-Mock-Key: ${key}'" 2>/dev/null || echo "") + local token; token=$(echo "$resp" | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") + if [ -n "$token" ]; then + log_info " mock-login 返回有效 JWT ✅" + + # 用获取到的 token 测试通知接口 + local notif; notif=$(remote "curl -s --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/api/notifications/unread-count' -H 'Authorization: Bearer ${token}'" 2>/dev/null || echo "") + echo "$notif" | grep -q '"success":true' && log_info " /api/notifications/unread-count 正常 ✅" || log_warn " 通知接口响应异常" + else + log_warn " mock-login 未返回 token" + fi + return 0 +} + +test_payment_endpoints() { + local all=true + local c; c=$(remote "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/payment'" 2>/dev/null || echo "000") + [ "$c" = "200" ] && log_info " /payment 返回 200 ✅" || { log_warn " /payment 返回 ${c}"; all=false; } + + c=$(remote "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/payment/page?package=monthly&jwt=bad'" 2>/dev/null || echo "000") + [ "$c" = "401" ] && log_info " /payment/page (bad JWT) 返回 401 ✅" || { log_warn " /payment/page 返回 ${c}"; all=false; } + + c=$(remote "curl -s -o /dev/null -w '%{http_code}' --max-time 10 'http://127.0.0.1:${BACKEND_PORT}/api/user/quota' -H 'Authorization: Bearer bad'" 2>/dev/null || echo "000") + [ "$c" = "401" ] && log_info " /api/user/quota (bad JWT) 返回 401 ✅" || { log_warn " /api/user/quota 返回 ${c}"; all=false; } + + $all && log_info "支付端点测试通过" && return 0 + log_warn "支付端点测试有异常(部分失败)"; return 0 +} + +test_migration_status() { + local r; r=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT COUNT(*) FROM _migrations'" 2>/dev/null || echo "0") + [ "$r" -gt "0" ] 2>/dev/null && log_info " _migrations 表存在,已执行 ${r} 个迁移 ✅" || { log_warn " _migrations 表不存在或无记录"; return 0; } + + local last; last=$(remote "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -tAc 'SELECT name FROM _migrations ORDER BY applied_at DESC LIMIT 1'" 2>/dev/null || echo "") + [ -n "$last" ] && log_info " 最新迁移: ${last} ✅" + return 0 +} + +# ---------- 主测试入口 ---------- run_tests() { log_step "执行部署后测试..." - [ "$DRY_RUN" = true ] && { log_dry "完整测试套件(6 项)"; return 0; } + [ "$DRY_RUN" = true ] && { log_dry "完整测试套件(9 项)"; return 0; } [ "$SKIP_TESTS" = true ] && { log_warn "跳过测试"; return 0; } local tests=( @@ -115,6 +164,9 @@ run_tests() { "test_weather_details_jwt:weather/details JWT" "test_invalid_token_401:无效 Token 401" "test_response_content:响应内容验证" + "test_mock_login:Mock 登录 + 通知接口" + "test_payment_endpoints:支付端点" + "test_migration_status:迁移状态" ) local i=1 total=${#tests[@]} for entry in "${tests[@]}"; do