第五幕 集成与总结
运行方式
# 1. 启动 MySQL
cd ~/mysql && docker compose up -d
# 2. 启动 Server(自动建库建表,首次运行约 3s)
cd ~/db_class_design_rs
cargo run -p forum-server
# 3. 新开终端,启动 TUI 客户端
cargo run -p forum-client首次启动后直接注册,第一个注册的用户自动成为管理员。
功能清单
| 功能 | 状态 |
|---|---|
| 用户注册/登录(bcrypt + JWT) | ✅ |
| 首个注册用户自动成为管理员 | ✅ |
| 文章发布/编辑/删除(Markdown) | ✅ |
| 文章列表(分页、分类、搜索) | ✅ |
| 评论与回复(树形结构) | ✅ |
| 成员管理(角色切换、删除) | ✅ |
| 个人中心(修改昵称/密码) | ✅ |
| WebSocket 实时通知 | ✅ |
| TUI 鼠标交互 | ✅ |
| Markdown 终端渲染(语法高亮) | ✅ |
| 软删除 | ✅ |
| Server 三层架构(repository 模式) | ✅ |
技术总结
与 middle 卷的对比
middle 卷的快递驿站是单机 CLI,本课设升级为完整的 C/S 系统:
| 维度 | middle(快递驿站) | end(论坛系统) |
|---|---|---|
| 架构 | 单机 CLI | Client-Server |
| 界面 | println! + stdin | ratatui TUI |
| 通信 | 直连数据库 | HTTP REST + WebSocket |
| 认证 | 无 | JWT |
| 并发 | 单用户 | 多用户 |
| 代码分层 | models / sqls / utils | config / repository / handlers |
关键技术点
axum 中间件链:from_fn_with_state 把 JWT 验证和 CurrentUser 注入解耦,Handler 只需声明 user: CurrentUser 参数即可自动提取,通过 FromRequestParts trait 实现。
repository 模式:ArticleRepo<'a>(&'a MySqlPool) 这类结构体把 sqlx 查询封装为方法,Handler 层零 sqlx 调用。生命周期参数 'a 确保 Repo 不会比它借用的 Pool 活得更长,编译期保证安全。
sqlx 动态查询:文章列表支持分类+搜索的组合过滤。sqlx 不支持完全动态的参数绑定,用 match (category, search) 枚举四种组合分别构造查询,保持类型安全,代价是代码量翻倍。
ratatui 状态管理:所有 UI 状态集中在 App struct,每帧根据 app.page 分发到对应页面的 render() 函数。页面间通过 navigate() / go_back() 切换,只保留一层历史,够用且简单。
页面 → Action → Handler 模式:页面的 handle_key 返回 Option<XxxAction> 枚举,异步操作统一在 main.rs 的 handler 函数里执行。这样页面模块保持纯同步,ratatui 渲染闭包不需要 async。
tui-markdown:基于 pulldown-cmark + syntect,直接返回 ratatui Text 类型,代码块按语言着色。比手写 AST 遍历省了大量工作。
遇到的坑
1. jsonwebtoken 10.x CryptoProvider:10.x 版本引入了可插拔的 crypto backend,必须在 Cargo.toml 中显式启用 rust_crypto feature,否则运行时 panic。
2. 数据库不存在:sqlx 连接时如果数据库不存在会直接报错。解决方案是先连接 mysql 系统库创建目标数据库,再重新连接。
3. 注册 handler 角色硬编码:注册逻辑里判断 user_count == 0 后把 role 正确写入了数据库,但返回给客户端的 token 和 UserPublic 仍然硬编码了 "member",导致客户端拿到的角色始终是普通成员。修复方式是让返回值使用实际写入的 role 变量而不是字面量。
4. ratatui 'static lifetime:Line<'static> 要求所有 Span 的内容是 'static,从 Markdown 解析出的 String 需要 .clone() 或 to_string() 转换,不能直接借用临时变量。tui-markdown 内部处理了这个问题,直接用它省去了手写渲染器的麻烦。
5. 鼠标点击坐标:ratatui 的布局坐标只在 draw() 闭包里才能拿到,但鼠标事件在闭包外处理。解决方案是每帧渲染后把关键区域的坐标存到 App struct,下一帧的鼠标事件用上一帧的坐标做命中检测——对于静态布局完全够用。
6. Workspace 与 Tauri 冲突:项目里同时有 Cargo workspace(server + client)和 Tauri 子项目(tauri-client/src-tauri)。Tauri 的 src-tauri 是独立的 Cargo 项目,不能被 workspace 管理,否则 tauri build 会报错。在根 Cargo.toml 里加 exclude = ["tauri-client/src-tauri"] 解决。
