提灯喵 Rust 后端使用说明
环境要求
- Rust 1.75+
- MySQL 8(库名
tdm) - Docker(可选,测试库)
配置
复制 config/dev.toml,或通过环境变量覆盖:
| 变量 | 说明 |
|---|---|
DATABASE_URL |
MySQL 连接串 |
DB_USER / DB_PASSWORD |
数据库账号 |
SECRET_ID |
腾讯云 SecretId(默认 dev.toml,TDM OSS 用 manga-trans 密钥对) |
SECRET_KEY |
腾讯云 SecretKey(见 keychain_20260427/keys.txt,勿用 backup 密钥);写入 TdmServerRust/.env 即可,若 shell 残留 ci-placeholder 会自动被 .env 覆盖 |
CDN_KEY |
CDN 签名密钥(ossdev.yuriful.top) |
启动测试数据库
cd TdmServerRust
docker compose up -d
迁移脚本位于 migrations/(自 Java Flyway 复制)。
CI/本地 Docker 基础库结构见 docker/mysql/00_schema.sql(同步自 WebBack-end local/tdm.sql)。
同步远程 dev 数据到本地
- Xshell 连接 dev,隧道:
本地 13307→127.0.0.1:3306 - 启动本地库:
docker compose up -d mysql - 同步:
cd TdmServerRust
$env:REMOTE_DATABASE_URL = "mysql://tdm:密码@127.0.0.1:13307/tdm?ssl-mode=disabled"
.\scripts\sync_remote_db.ps1
.env改本地连接:
DATABASE_URL=mysql://root:root@127.0.0.1:3307/tdm
| 参数 | 说明 |
|---|---|
-SkipImport |
仅导出到 tmp/tdm_remote_dump.sql |
LOCAL_DATABASE_URL |
覆盖本地连接串,默认 root:root@127.0.0.1:3307/tdm |
启动服务
cargo run -- --profile dev
默认端口 8090,与 Java 一致。
HTTP/2(本地 / 生产)
| 场景 | 做法 |
|---|---|
| 本地 dev | cargo run -- --profile dev-h2(TLS + ALPN 协商 h2;首次自动生成 config/certs/dev/*.pem) |
| RustRover | Cargo 配置文件选 dev-h2;程序实参填 --profile dev-h2(前者是编译 profile,后者是应用配置档) |
| 前端 | .env.local-development 改为 VITE_BASE_API=https://localhost:8090,浏览器信任自签证书 |
| 生产 | nginx listen 443 ssl http2,示例见 deploy/nginx/back-end.http2.conf.example |
Chrome Network 协议列应显示 h2;dev 控制台 [api-timing] 的 queue 应明显下降。
OSS 下载
| 场景 | 接口 | 说明 |
|---|---|---|
| 单文件 | GET /api/oss/downloadCredential + CDN 直链,或 GET /api/oss/downloadFile 302(非浏览器客户端) |
|
| ZIP/CORS 回退 | GET /api/oss/downloadFile/proxy |
服务端代理,需连接池 |
| CDN CORS | 见 deploy/cdn-cors.md |
配好后 ZIP 直拉 CDN |
开发期双端对比:Java 8090 / Rust 8091(修改 config/dev.toml 中 port)。
dev 模式自动记录每个 HTTP 请求/响应(body 超 64KB 截断),并在控制台输出调用栈耗时树(prof │ 前缀)。
调用栈耗时分析(仅 dev)
--profile dev 时,每个 API 请求结束后在 stderr 打印各层方法 self(自身)与 incl(含子调用)耗时。
| 环境变量 | 说明 |
|---|---|
RUST_LOG |
默认 tdm_server_rust=debug |
DEV_PROFILE_MIN_MS |
隐藏 self 低于该阈值的 span(毫秒),默认 0.1 |
DEV_PROFILE_NO_LINKS |
设为任意值则禁用源码链接 |
DEV_PROFILE_LINK_SCHEME |
jetbrains(默认)/cursor/vscode/file/none |
DEV_PROFILE_JB_STYLE |
rustc(默认)/stack |
DEV_PROFILE_JSONL |
每请求追加一行 JSON profile(如 tmp/member_profile.jsonl) |
DEV_SERVER_TIMING |
设为 0 关闭 Server-Timing 响应头(dev 默认开启) |
MANGA_NAME_CACHE_SECS |
译名/原名列表缓存 TTL(秒),默认 60 |
MEMBER_ALL_CACHE_SECS |
全量组员列表缓存 TTL(秒),默认 60 |
LIST_PAGE_CACHE_SECS |
列表分页 JSON 缓存 TTL(秒),默认 60 |
缓存突变 ↔ 失效(src/cache/invalidate.rs)
写操作后须调用语义化 helper,禁止在 service 层散落多个 invalidate_*。
| 突变 | Helper | 失效范围 |
|---|---|---|
| 话数 CRUD / 接稿 / 交稿 / OSS / 上传 / 回退 | on_episode_mutated |
话数 epoch + 藏宝处 epoch + 任务追踪 + 统计 + 组员话数(可选) |
| 漫画增删改 | on_manga_mutated |
译名 + 漫画列表 epoch + 杂志漫画 epoch + 删漫画时话数相关 |
| 组员增删改 | on_member_mutated |
全量组员 + 组员分页 epoch + 鉴权快照 + 组员话数 |
| 常驻变更且影响话数岗位 | on_station_mutated |
常驻漫画 + on_episode_mutated |
| 作者/杂志改名(影响列表筛选) | on_author_or_magazine_renamed |
漫画列表 epoch + 杂志漫画 epoch |
列表分页键含世代号(list_page_epochs / manga_episode_cache_epoch),失效时 epoch += 1,不用 invalidate_entries_if。
集成测试:cargo test --test cache_invalidation_test(需 DATABASE_URL 或 RUN_INTEGRATION_TESTS=1)。
RustRover Run 窗口:
- Run 配置 取消「在输出控制台中模拟终端」
- Ctrl+点击独立行
--> src/repository/member_repo.rs:75:1
- Cursor:
DEV_PROFILE_LINK_SCHEME=cursor - self 最大 的行加粗黄色,末尾
◀ hot;报告末尾有hotspot摘要行
输出示例:
prof │ ├─ repository::episode_repo::page_list self 23.3ms ...
--> src/repository/member_repo.rs:75:1
Server-Timing(前后端联调)
--profile dev 时响应头携带 Server-Timing:total(墙钟)、hot(self 最大 span)、s1…(其余热点)。
- 跨域 dev 已暴露
Access-Control-Expose-Headers: server-timing - 关闭:
DEV_SERVER_TIMING=0 - 前端 dev 控制台
[api-timing]合并 Network Timing 与server=[...]
JSON 性能
- 请求/响应 JSON 使用 simd-json(x86 AVX2 / aarch64),否则自动回退 serde_json
- Controller 请求体提取器:
AppJson<T>
新增 service/repository/web handler 的 async fn 后:
python scripts/add_profile_instrument.py
dev 控制台(日志与错误检查)
仅 dev / dev-h2 profile 注册,不经 /api 鉴权。
| URL | 说明 |
|---|---|
/dev/console.html |
Web 控制台(健康 / 错误 / 应用日志) |
/dev/api/health |
JSON 健康检查 |
/dev/api/logs/errors?limit=100 |
内存错误列表 |
/dev/api/logs/app?lines=200 |
tail app.log |
本地:http://localhost:8090/dev/console.html
远程 dev:https://back.dev.yuriful.top/dev/console.html
配置 config/dev.toml [dev_console];部署时 log_dir / app_log 指向 /data/TdmServerRust/。环境变量 DEV_LOG_DIR、DEV_APP_LOG 可覆盖。
RSS 订阅(事件驱动,无轮询)
输出目录:{folder.base}/src(dev 部署常为 /data/WebBack-end/local/tdm/src),由 Caddy rss.dev.yuriful.top 静态托管。
| Feed | 内容 |
|---|---|
rssManga.xml |
新开坑 + 漫画信息更新 + 最新话(合并后最多 40 条,新替旧) |
rssEpisode.xml |
最新 40 话新单话提醒 |
rssEpisode_{post}.xml |
各岗位最新 40 条交稿提醒 |
rssMember.xml |
三月未交稿组员(无条数上限) |
rssPublishLink.xml |
发布链接 |
业务状态变更后异步写盘;rss_file_lock 按文件名互斥。
| 事件 | 刷新 |
|---|---|
| 增/删/改话、接稿、回退 | EpisodePipeline |
| 交稿 | 对应岗位 + rssMember.xml |
| 新发/更新漫画 | rssManga.xml |
| 发布链接 | rssPublishLink.xml |
| 启动 / 每日 12:00 | 全量 / rssMember.xml |
dev 验证:curl -sI https://rss.dev.yuriful.top/rssManga.xml 交稿/改漫画后 Last-Modified 即时变化。
production 订阅入口为 https://rss.prod.yuriful.top/rssEpisode_letterer.xml。生产部署会安装并验证独立 Caddy 站点,只允许访问根目录下的 XML 文件;该订阅文件同时作为 TLS、响应头和 XML 完整性检查目标。
测试:cargo test rss_service;集成 cargo test --test rss_event_test(需 DB)。
单元测试
cargo test
集成测试(需数据库)
PowerShell:
$env:RUN_INTEGRATION_TESTS = "1"
$env:DATABASE_URL = "mysql://root:root@127.0.0.1:3307/tdm"
cargo test --test episode_api_test
cargo test --test member_api_test
cargo test --test manga_api_test
cargo test --test manga_card_repo_test
cargo test --test oss_api_test
cargo test --test task_tracking_api_test
cargo test --test cos_presign_test --test cos_sts_test --test oss_entity_test
/task-tracking 页面 API 冒烟(需后端 8090 + admin 账号):
powershell -File scripts/test_task_tracking_apis.ps1
Git Bash / WSL:
export RUN_INTEGRATION_TESTS=1
export DATABASE_URL=mysql://root:root@127.0.0.1:3307/tdm
cargo test --test episode_api_test
cargo test --test member_api_test
cargo test --test manga_api_test
cargo test --test station_episode_test
cargo test --test oss_api_test
cargo test --test manga_repo_parity_test
cargo test --test manga_service_parity_test
cargo test --test task_tracking_repo_parity_test
exe 被占用导致 link 失败时:
$env:CARGO_TARGET_DIR = "target/test-build"
cargo test --test episode_api_test
常驻组员填充/清空单话
| 接口 | 行为 |
|---|---|
POST /api/mangas/station/admin |
fillEpisodes: true 时,将该漫画该岗位所有 空位且未交稿 的单话指派给新组员,并写入 *SetupTime |
DELETE /api/mangas/station/:stationId |
删除常驻前,清空该组员在该漫画、该岗位上 未交稿 的单话分配(已交稿不动) |
岗位支持:翻译/校对/嵌字/审稿/时轴(post 1–4、6);图源(post 0)仅按空位填充,无 detail 交稿字段。
集成测试:cargo test --test station_episode_test
公共断言见 tests/common/mod.rs(assert_manga_card_fields、assert_episode_list_fields 等)。
curl 接口测试
依赖:curl、jq、Git Bash 或 WSL(Windows 原生 PowerShell 无 bash 时需 WSL)。
# 单模块 smoke
bash tests/curl/login/01_login.sh
# 字段契约(需 TOKEN)
TOKEN=your_jwt bash tests/curl/manga/02_manga_fields.sh
TOKEN=your_jwt bash tests/curl/episode/02_episode_fields.sh
TOKEN=your_jwt bash tests/curl/member/02_stationed.sh
TOKEN=your_jwt bash tests/curl/reward/02_reward_fields.sh
bash tests/curl/oss/02_oss_credential.sh
# 全量(01 + 02)
TOKEN=your_jwt bash tests/curl/run_all.sh
# Java vs Rust 双端 key 对比
JAVA_URL=http://127.0.0.1:8090 RUST_URL=http://127.0.0.1:8091 \
TOKEN=xxx MANGA_ID=1 MEMBER_ID=1 bash tests/curl/compare_module.sh manga
bash tests/curl/compare_module.sh episode
bash tests/curl/compare_module.sh glossary
字段断言库:tests/curl/lib/assert_json.sh(assert_field_exists、assert_no_field)。
OpenAPI 文档(dev / dev-h2)
| 路径 | 说明 |
|---|---|
/doc/openapi.json |
OpenAPI 3.1 契约,与 Java static/doc/openapi.json 一致 |
/swagger-ui.html |
Swagger UI,url=/doc/openapi.json,operationsSorter=method |
契约源:assets/doc/openapi.json(由 Java smart-doc 同步)。pro 环境不暴露上述路由。
OpenAPI parity 全量验收
契约:tests/fixtures/openapi.json(与 https://back-docs.dev.yuriful.top/ 一致;Java 基准 API 为 https://back.dev.yuriful.top)
用例:tests/fixtures/parity_cases.yaml(101 operation,含 json / multipart / form-urlencoded)
前置:数据库
默认读取 .env / 环境变量中的 DATABASE_URL。如需严格对比业务数据值,请先同步 dev 库:
cd TdmServerRust
$env:REMOTE_DATABASE_URL = "mysql://tdm:密码@127.0.0.1:13307/tdm?ssl-mode=disabled"
.\scripts\sync_remote_db.ps1
$env:DATABASE_URL = "mysql://root:root@127.0.0.1:3307/tdm"
启动 Rust(hotspot 采集必选)
$env:DEV_PROFILE_JSONL = "tmp/openapi_profile.jsonl"
cargo run -- --profile dev
run_openapi_parity.ps1 会在 benchmark 前清空 JSONL;Rust 服务端必须带 DEV_PROFILE_JSONL 启动,否则 hotspot 列显示 none。
启动本地 Java(与 Rust 共用 DATABASE_URL)
$env:DATABASE_URL = "mysql://root:root@127.0.0.1:3307/tdm"
.\scripts\start_local_java_backend.ps1 -Port 8091
跑 Java vs Rust parity + 性能报告
$env:RUN_INTEGRATION_TESTS = "1"
$env:JAVA_BASE_URL = "http://127.0.0.1:8091"
.\scripts\run_openapi_parity.ps1
# 远程 Java 基准:$env:JAVA_BASE_URL = "https://back.dev.yuriful.top"
# 仅 Rust 单测(不调 Java):$env:SKIP_JAVA_PARITY = "1"
# 严格对比业务数据值:$env:STRICT_OPENAPI_PARITY = "1"
单独跑集成测试
cargo test --test openapi_parity_test -- --nocapture
cargo test --test evaluation_api_test
cargo test --test questionnaire_api_test
cargo test --test reward_api_test
cargo test --test task_tracking_api_test
重新生成 parity_cases.yaml
python scripts/generate_parity_cases.py
报告输出:docs/reports/api_performance_YYYY-MM-DD.md
| 列 | 含义 |
|---|---|
| hotspot | 最耗时 span 标签 |
| self_ms | hotspot 自身耗时(ms) |
| pct | hotspot 占 wall 比例 |
| source | jsonl / server-timing / none |
parity 集成测试覆盖全部 37 个 POST/PUT(default_body 自动补请求体);reg 为 Rust-only(skip_java)。
账号:gum979 / 123456(member);Gum979 / 123456(admin task-tracking)
Member 性能巡检
一键(自动启停 dev 服务 + curl + hotspot 表):
cd TdmServerRust
.\scripts\run_member_profile.ps1
# 保持服务运行:.\scripts\run_member_profile.ps1 -KeepServer
手动分步:
$env:DEV_PROFILE_JSONL = "tmp/member_profile.jsonl"
$env:DATABASE_URL = "mysql://root:root@127.0.0.1:3307/tdm"
cargo run -- --profile dev
.\scripts\profile_member_api.ps1
# 或 Git Bash:bash tests/curl/member/03_profile_all.sh
python scripts/parse_profile_report.py tmp/member_profile.jsonl
环境变量:DEV_USER/DEV_PWD(默认 gum979/123456,memberId=18)、MEMBER_ID、BASE_URL。
写接口默认不自动跑。
API 响应格式
{"code":200,"msg":"成功!","data":...}
分页:{"total":100,"rows":[...]}(仅一层 Result 包裹,勿再嵌套 code/data)
列表项主键 JSON 字段为 id(小写,对齐 Java/OpenAPI)
鉴权头:token 或 Authorization(JWT,密钥 yuri.tdm)
Query 与路由约定
- 空 query 字符串(如
email=、mangaOriName=)视为未传参,不参与过滤(对齐 Java/Spring) NaN/undefined的 query 数字参数返回{"code":500,...}业务 JSON,非 axum 400- 动态路径参数使用 axum 0.7 语法
/:id(如/api/members/:id、/api/members/stationedMangas/:id) - 工作台任务分页使用
GET /api/members/workbench/:id/tasks?page=&pageSize=&view=plan|doing&tab=&keyword=;分页单位为漫画组,total为匹配漫画组数,rows[].tasks为该漫画当前筛选下的全部话数;view为空时按all兼容旧调用。 - 工作台总览
GET /api/members/workbench/:id/overview返回planCount、doingCount,前端视图切换按钮使用后端计数。
CI/CD
| 工作流 | 触发 | 说明 |
|---|---|---|
CI |
后端代码 push/PR → dev;后端代码 PR → master | 裸 SQL 检查、clippy、单元测试;push dev 额外跑集成测试 |
Deploy |
push → dev / v* tag / 手动 |
dev:Actions 上传源码包并触发服务器后台构建;prod:Actions 编译二进制 → SSH 上传 → Flyway → 重启 |
Migrate Dev PostgreSQL |
手动 workflow_dispatch | dev MySQL→PG 全量恢复;也支持仅 post-import 收尾 |
Migrate Prod PostgreSQL |
手动 workflow_dispatch | prod MySQL→PG:full_cutover / post_import / full_cutover_and_deploy |
分支:日常开发用 dev,不直接提交 master;任务完成后开 PR。
CI 省钱规则:CI 只监听 src/**、tests/**、Cargo.*、config/**、migrations/**、docker/postgres/**、scripts/check_no_raw_sql.sh 和自身 workflow;纯 docs / openapi-site 改动不触发后端 CI。dev -> master PR 不重复跑 PR 事件的 check,以同一 SHA 的 push dev 结果作为合并前验证。
GitHub Environments(与 WebBack-end 同名):development、production
| 类型 | 变量/密钥 |
|---|---|
| Variables | SSH_HOST、SSH_USERNAME、SSH_PORT(默认 22)、PG_APP_USER(应用数据库用户,默认 tdm)、SENTRY_DSN、SECRET_ID |
| Secrets | SSH_KEY(完整私钥,含 BEGIN/END 行,无 passphrase)、PG_APP_PASSWORD(应用数据库密码)、PG_SUPER_PASSWORD(PostgreSQL 管理员密码)、SECRET_KEY、CDN_KEY |
PG_SUPER_PASSWORD 建议使用 Environment Secret,并按环境配置不同值:
development环境:PG_SUPER_PASSWORD=...(开发库/实例)production环境:PG_SUPER_PASSWORD=...(生产库/实例)
同名的 PG_SUPER_PASSWORD 在不同环境是独立的,仓库级 Secret 为全局兜底,不要用它存放环境差异值。
dev 自动部署:提交到 dev 后只运行 Deploy,Actions 只上传源码包并通过 SSH 触发 /data/TdmServerRust/scripts/deploy_dev_async.sh 后台任务;Rust 编译、Flyway、PG/Redis/SkyWalking 检查、重启和健康检查都在 dev 服务器完成,减少 Actions 计费时间。
dev 部署状态:服务器状态文件为 /data/TdmServerRust/logs/dev-deploy-latest.status;单次日志为 /data/TdmServerRust/logs/dev-deploy-<run_id>-<attempt>.log。状态值:waiting、running、success、failed。
ssh -p "${SSH_PORT:-22}" ubuntu@43.155.139.176 'cat /data/TdmServerRust/logs/dev-deploy-latest.status && tail -n 120 /data/TdmServerRust/logs/dev-deploy-<run_id>-<attempt>.log'
Actions 若停在 SSH 连接阶段,先确认 development 环境的 SSH_HOST、SSH_USERNAME、SSH_PORT、SSH_KEY 与服务器安全组入站端口一致。
Deploy 前置:dev 应用构建使用服务器本地 Docker builder 缓存;prod 应用部署不使用 Docker。PG、Redis、Flyway、SkyWalking 仍沿用服务器现有 Docker 维护脚本。prod Deploy 不再维护 MySQL 链路,仅做 PostgreSQL 运维与迁移。
SSH_KEY 配置(Windows 用文件导入,勿手粘贴):
gh secret set SSH_KEY --env development --repo Tideng-Cat/TdmServerRust < C:\path\to\tdm_tencent_lighthouse
gh secret set SSH_KEY --env production --repo Tideng-Cat/TdmServerRust < C:\path\to\prod.pem
服务器 ~/.ssh/authorized_keys 需有对应公钥。
手动部署:Actions → Deploy → Run workflow → 选择 development / production。
dev 数据恢复:Actions → Migrate Dev PostgreSQL → Run workflow。
| mode | 用途 |
|---|---|
full_restore |
停 Rust → 备份 dev MySQL → pgloader 全量导入 PG → UTC/序列/Flyway → init 用户 → 清 Redis → 重启 Rust |
post_import |
PG 已导入数据时仅做 UTC/序列/Flyway/init 用户 |
full_restore_and_deploy |
全量恢复后触发 Deploy development |
首次全量恢复保持默认 full_restore;如果当前 PG 已做过 UTC 修正,勾选 skip_utc。MySQL 密码优先读 development 环境密钥 MYSQL_PASSWORD / MYSQL_PWD,也可在运行工作流时填写 mysql_password。
PG 收尾:Actions → Migrate Dev PostgreSQL / Migrate Prod PostgreSQL → Run workflow。
生产 MySQL→PG 完整步骤见 POSTGRESQL_MIGRATION.md。
Server-Timing 全接口扫描
验收口径:响应头 Server-Timing 的 total(非 hot、非浏览器 TTFB)。普通接口 pass 目标 p95 ≤ 20ms,warn 上限 p95 ≤ 30ms;bcrypt、OSS STS/预签名、文件上传下载代理按特殊成本单独列出。
cd TdmServerRust
# dev 远程
BASE_URL=https://back.dev.yuriful.top THRESHOLD_MS=20 SOFT_THRESHOLD_MS=30 ALLOW_DESTRUCTIVE=1 python3 scripts/scan_server_timing.py
# 本地(需先 cargo run -- --profile dev)
BASE_URL=http://127.0.0.1:8090 THRESHOLD_MS=20 SOFT_THRESHOLD_MS=30 python3 scripts/scan_server_timing.py
| 变量 | 默认 | 说明 |
|---|---|---|
BASE_URL |
http://127.0.0.1:8090 |
目标服务 |
THRESHOLD_MS |
20 |
pass 阈值(p95) |
SOFT_THRESHOLD_MS |
30 |
warn 上限;超过即 fail |
WARMUP_ROUNDS |
2 |
采样前预热次数 |
SAMPLE_ROUNDS |
5 |
普通接口采样次数 |
ALLOW_DESTRUCTIVE / INCLUDE_DESTRUCTIVE |
unset | 设为 1 后包含 POST/PUT/PATCH/DELETE |
DESTRUCTIVE_WARMUP_ROUNDS |
0 |
destructive case 预热次数 |
DESTRUCTIVE_SAMPLE_ROUNDS |
1 |
destructive case 采样次数,避免重复删除同一 fixture |
TAGS / OPERATIONS |
unset | 逗号分隔,只扫描指定 tag 或 operationId |
PROFILE_JSONL / DEV_PROFILE_JSONL |
unset | 读取服务端 JSONL profile,补充源码级 hotspot |
DEV_USER / DEV_PWD |
gum979 / 123456 | 组员 token |
DEV_ADMIN_USER |
Gum979 | 管理员 token |
输出:
docs/reports/server_timing_scan_{date}.json— 全量采样docs/reports/server_timing_slow_{date}.md— 超标清单
scripts/generate_performance_report.py的rust_ms为客户端墙钟,不能作 20ms 验收依据。
模块迁移状态
| 模块 | 端点 | 状态 |
|---|---|---|
| Login | 5 | 已迁移 |
| Member | 12 | 已迁移 |
| Author | 7 | 已迁移 |
| Magazine | 7 | 已迁移 |
| Evaluation | 5 | 已迁移 |
| Questionnaire | 3 | 已迁移 |
| Manga | 27 | 已迁移 |
| Episode | 14 | 已迁移 |
| MangaBenefit | 4 | 已迁移 |
| Reward | 10 | 已迁移 |
| OSS | 4 | 已迁移 |
| TaskTracking | 3 | 已迁移 |