# seed - 压测数据准备工具 > 给 prod 凌晨压测灌 1000 个测试用户 + 资产 + JWT,数据用 `star_id=999900` 物理隔离。 --- ## ⭐ 推荐入口:prod_loadtest.sh > **如果你目标是打生产压测,优先用 [`../scripts/prod_loadtest.sh`](../scripts/prod_loadtest.sh) 一站式脚本**。 > 它会把下面的所有步骤(SSH 隧道/seed/preflight/loadgen/report/cleanup)封装成子命令, > 凭据从 `docker/.env.prod` 自动读,避免明文泄露。 ```bash # 一键完整流程 ./../scripts/prod_loadtest.sh pipeline --scenarios=S1,S2,S4 \ --stage=step --step-schedule='5,10,20' --duration=60s ``` 详见 [../scripts/README.md](../scripts/README.md)。 --- ## 一句话总结 跑 `./seed`,数据库里多出 1000 个用户 + 5000 个 assets + 2000 个 exhibitions,本地多出 `users.csv` (含 JWT)。 --- ## 编译 ```bash cd backend go build -o bin/seed ./scripts/loadgen/seed/ # 或 make loadgen-build ``` --- ## 在 prod 上跑 (凌晨 T0 = 02:00,推荐) ```bash ssh root@101.132.250.62 cd /opt/topfans/loadtest bash scripts/prod_seed.sh ``` 这个脚本会自动: 1. 读 `/opt/topfans/docker/.env.prod` 拿 DB_PASSWORD + JWT_SECRET 2. 跑 seed (插入 23k 行测试数据,直接写入 docker 里的 `topfans-postgres`) 3. 自动重置 PG 序列 (CLAUDE.md 规范) 4. 写 `users.csv` (含 1000 个 JWT) **预计耗时**:30-60 秒 > ⚠️ **为什么必须在生产机上跑**:`seed` 默认值是 `localhost:5432/topfans`, > 这正好等于生产机上 docker 暴露的 `5432:5432` + `POSTGRES_DB=topfans`。 > 从本地 Mac 跑默认值会**连到你本机的 Postgres**(可能根本不是 topfans 库), > 后续 `users.csv` 拿去打生产网关会全部 401。详见下一节。 --- ## 从本地 Mac 连到生产 DB (不 ssh 进生产机) 只适合本地紧急补 seed、或者压测脚本调试。**首选仍是上一节 ssh 进生产机**。 ### 方式 A:SSH 端口转发 (推荐,改动最小) ```bash # 1. 建立转发:本地 25432 → 生产机 docker 5432 ssh -L 25432:127.0.0.1:5432 -N -f root@101.132.250.62 # 2. 跑 seed (从 docker/.env.prod 拿密码) cd /Users/liulujian/Documents/code/TopFansByGithub DB_PASSWORD=$(grep '^DB_PASSWORD=' docker/.env.prod | cut -d= -f2) \ JWT_SECRET=$(grep '^JWT_SECRET=' docker/.env.prod | cut -d= -f2) \ go run ./backend/scripts/loadgen/seed \ --db-host=127.0.0.1 \ --db-port=25432 \ --db-name=topfans \ --db-user=postgres # 3. 用完记得关转发 pkill -f 'ssh -L 25432' ``` ### 方式 B:从本地直连 docker 容器 (生产机已开 5432 端口时) 如果生产机 `topfans-postgres` 已经通过 `ports: - "5432:5432"` 暴露到外网, 也可以直接走公网,但**强烈不建议** (生产 PG 暴露公网本身就有风险): ```bash DB_PASSWORD=$(grep '^DB_PASSWORD=' docker/.env.prod | cut -d= -f2) \ JWT_SECRET=$(grep '^JWT_SECRET=' docker/.env.prod | cut -d= -f2) \ go run ./backend/scripts/loadgen/seed \ --db-host=101.132.250.62 \ --db-port=5432 \ --db-name=topfans \ --db-user=postgres ``` --- ## 在本地 docker 跑 (开发联调) ```bash cd backend/scripts/loadgen/seed # 1. 生成 bcrypt 哈希 (与 tokens.go 硬编码的 "Test@123" 匹配) python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \ > loadtest_bcrypt.txt # 2. 跑 seed (假设本地 docker postgres 在 15432) cd /Users/liulujian/Documents/code/TopFansByGithub/backend DB_PASSWORD=123456 \ JWT_SECRET=topfans-secret-key-local-dev-only \ ./bin/seed \ --db-name=top-fans \ --db-host=localhost \ --db-port=15432 \ --db-user=postgres ``` **注意**: `loadtest_bcrypt.txt` 必须在 seed 二进制运行的**当前目录**(代码用相对路径读)。 --- ## 命令行参数 ``` ./bin/seed --help Usage of ./bin/seed: -cleanup # 跑清理 (默认保留 1000 users) -cleanup-star-id int # 要清的 star_id (默认 999900, 防止误删) -full # 配合 -cleanup: 也删用户和 stars -reset # 删旧数据再 seed (隐含 --cleanup 行为) -reset-tokens # 只重签 JWT (数据保留) -jwt-secret string # JWT 密钥 (默认 $JWT_SECRET) -db-host string # PG host (默认 localhost) -db-port int # PG port (默认 5432) -db-name string # PG 数据库 (默认 topfans,即 prod 默认; # 本地 docker dev 用 top-fans,需显式 --db-name=top-fans) -db-user string # PG user (默认 postgres) -db-password string # PG 密码 (默认 $DB_PASSWORD) ``` ### 三种部署对应的 DB 参数速查表 | 部署 | host | port | dbname | user | password | |------|------|------|--------|------|----------| | **生产机直跑** (ssh 进去) | `localhost` | `5432` | `topfans` | `postgres` | `$DB_PASSWORD` (从 `docker/.env.prod` 读) | | **本地连生产** (SSH 端口转发) | `127.0.0.1` | `25432` | `topfans` | `postgres` | `$DB_PASSWORD` (从 `docker/.env.prod` 读) | | **本地 docker 联调** (宿主机 postgresql-database-1) | `localhost` | `15432` | `top-fans` | `postgres` | `123456` (从 `docker/.env.local` 读) | > **默认值设计意图**:`localhost:5432/topfans` 是给"在生产机上直接跑"设计的, > 因为生产 docker 把 5432 暴露到宿主机 5432。从本地 Mac 跑时**必须显式覆盖 > `--db-host` / `--db-port` / `--db-name`**,否则会连到本机 Postgres。 --- ## 三种"清理"模式对比 | 命令 | 删 stars | 删 users | 删 assets/exhibits | 用途 | |------|---------|---------|-------------------|------| | `./seed --cleanup` | ❌ | ❌ | ✅ | 压完一轮,清理资产但保留账号 | | `./seed --cleanup --full` | ✅ | ✅ | ✅ | 全部清,下次重新 seed | | `./seed --reset` | ❌ | ❌ | ✅ | 等同 `--cleanup`(保留用户) | | `./seed --reset-tokens` | ❌ | ❌ | ❌ | 只重新签 JWT,数据不动 | **典型流程**: ```bash # 第 1 轮压测 (02:00-03:00) ./seed # 灌数据 ./loadgen --cmd=run --scenarios=S1,S2,S4 # 压测 ./seed --cleanup # 压完清理资产 # 第 2 轮压测 (下周,JWT 过期了) ./seed --reset-tokens --jwt-secret=$JWT_SECRET # 只重签 JWT ./loadgen --cmd=run --scenarios=S1,S2,S4 # 复测 # 完全重来 (例如改了用户模型) ./seed --cleanup --full # 全删 ./seed # 重新灌 ``` --- ## 数据规模 | 表 | 行数 | 备注 | |----|------|------| | `stars` | +1 | star_id=999900 | | `users` | +1000 | mobile 19900000001 ~ 19900001000 | | `fan_profiles` | +1000 | 每个 user 一个 | | `crystal_transaction_records` | +1000+ | 初始水晶 | | `assets` | +5000 | 每个 user ~5 个 | | `booth_slots` | +3000 | | | `exhibitions` | +2000 | | | `friendships` | +10000 | | | **TOTAL** | **~23k 行** | | --- ## 关键设计 ### 1. star_id 隔离 所有测试数据用 `star_id = 999900`,**不影响**真实业务 (87, 88, 91, 93, 94, 95)。 ### 2. PG max_connections = 50 prod 已将 `POSTGRES_MAX_CONNECTIONS` 从 100 调到 50,避免被测试数据耗尽连接池。 ### 3. CLAUDE.md 序列重置 seed 末尾自动 `setval()` 所有相关表的 sequence,避免后续 GORM 插入报 duplicate key。 ### 4. JWT 7 天过期 跨周第二轮压测前需 `--reset-tokens` 重签。 ### 5. bcrypt 哈希与密码硬编码 - `tokens.go` 硬编码密码为 `"Test@123"`(写到 users.csv 的 password 列) - `loadtest_bcrypt.txt` 是这个密码的 bcrypt(cost=10) 哈希 - 二者必须匹配,否则 login 会报 500 --- ## 常见问题 ### Q: 跑完 seed 但 login 报"密码错误"? A: `loadtest_bcrypt.txt` 没匹配上 `Test@123`。 ```bash python3 -c "import bcrypt; print(bcrypt.hashpw(b'Test@123', bcrypt.gensalt(rounds=10)).decode())" \ > loadtest_bcrypt.txt ./seed --cleanup --full && ./seed ``` ### Q: 想换密码怎么办? A: 同时改两个地方: 1. `tokens.go` 的 `u.Mobile, "Test@123"` → 你的密码 2. `loadtest_bcrypt.txt` 重新生成 ### Q: "loadtest_bcrypt.txt: no such file or directory"? A: seed 用相对路径读这个文件,必须在 seed 目录跑(或者把文件 cp 到当前目录)。 ### Q: --reset 没生效,users 还是旧的? A: 因为 `--reset` 等同 `--cleanup`(保留用户)。要删用户用 `--cleanup --full`。 --- ## 单元测试 ```bash cd backend go test ./scripts/loadgen/seed/ -v ``` 5 个测试: - `TestMobileNumbering`: mobile 编号正确性 - `TestSequenceMapping`: loadtestSeqs 映射 - `TestPKColumnMapping`: pkColumns 映射(关键 stars/star_id, booth_slots/slot_id) - `TestCleanupRejectsInvalidStarID`: cleanup 拒绝非 loadtest star_id - `TestJoinInt64`: CSV 序列化辅助函数 **测试状态**: 5/5 PASS