提灯喵 Rust 后端使用说明

环境要求

配置

复制 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 数据到本地

  1. Xshell 连接 dev,隧道:本地 13307127.0.0.1:3306
  2. 启动本地库:docker compose up -d mysql
  3. 同步:
cd TdmServerRust
$env:REMOTE_DATABASE_URL = "mysql://tdm:密码@127.0.0.1:13307/tdm?ssl-mode=disabled"
.\scripts\sync_remote_db.ps1
  1. .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_URLRUN_INTEGRATION_TESTS=1)。

RustRover Run 窗口:

  1. Run 配置 取消「在输出控制台中模拟终端」
  2. Ctrl+点击独立行 --> src/repository/member_repo.rs:75:1

输出示例:

prof │ ├─ repository::episode_repo::page_list  self  23.3ms ...
   --> src/repository/member_repo.rs:75:1

Server-Timing(前后端联调)

--profile dev 时响应头携带 Server-Timingtotal(墙钟)、hot(self 最大 span)、s1…(其余热点)。

JSON 性能

新增 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_DIRDEV_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.rsassert_manga_card_fieldsassert_episode_list_fields 等)。

curl 接口测试

依赖:curljq、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.shassert_field_existsassert_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.jsonoperationsSorter=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_IDBASE_URL

写接口默认不自动跑。

API 响应格式

{"code":200,"msg":"成功!","data":...}

分页:{"total":100,"rows":[...]}(仅一层 Result 包裹,勿再嵌套 code/data

列表项主键 JSON 字段为 id(小写,对齐 Java/OpenAPI)

鉴权头:tokenAuthorization(JWT,密钥 yuri.tdm

Query 与路由约定

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 同名):developmentproduction

类型 变量/密钥
Variables SSH_HOSTSSH_USERNAMESSH_PORT(默认 22)、PG_APP_USER(应用数据库用户,默认 tdm)、SENTRY_DSNSECRET_ID
Secrets SSH_KEY(完整私钥,含 BEGIN/END 行,无 passphrase)、PG_APP_PASSWORD(应用数据库密码)、PG_SUPER_PASSWORD(PostgreSQL 管理员密码)、SECRET_KEYCDN_KEY

PG_SUPER_PASSWORD 建议使用 Environment Secret,并按环境配置不同值:

同名的 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。状态值:waitingrunningsuccessfailed

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_HOSTSSH_USERNAMESSH_PORTSSH_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-Timingtotal(非 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

输出:

scripts/generate_performance_report.pyrust_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 已迁移