feat: checkpoint user state tracking service with PostgreSQL

- RESTful API: POST /heartbeat, POST /checkpoints, GET /status, GET /summaries
- State-change-only checkpoint model with extensible StateType enum
- PostgreSQL backend with sqlx, auto-migration on startup
- pg_cron scheduled aggregation (state_summaries) and offline detection
- Heartbeat-based liveness with 60s timeout auto-offline
- LEAD() window function for state duration calculation
- JSONB content field for extensible checkpoint metadata

BREAKING CHANGE: Complete rewrite from Hello World to full API service.
This commit is contained in:
2026-05-31 22:36:20 +08:00
parent 766b8a84c9
commit 13f7c1326a
20 changed files with 3315 additions and 2 deletions
+142
View File
@@ -0,0 +1,142 @@
# ck-rs — Checkpoint 用户状态追踪服务
基于 [Axum](https://github.com/tokio-rs/axum) 的 RESTful API 服务,用于记录用户在不同状态下的持续时间。客户端定时上报检查点(checkpoint),服务端自动计算各状态累计时长。类比手机屏幕使用时间统计。
## 快速开始
```bash
# 启动服务(默认监听 127.0.0.1:3000,可通过 .env 中 LISTEN_ADDR 修改)
cargo run
```
## API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| `GET` | `/health` | 服务健康检查 |
| `POST` | `/users/{user_id}/checkpoints` | 上报当前状态(心跳) |
| `GET` | `/users/{user_id}/checkpoints` | 查询检查点历史(`?from=&to=&limit=` |
| `GET` | `/users/{user_id}/checkpoints/{id}` | 查询单个检查点 |
| `GET` | `/users/{user_id}/status` | 当前状态 + 各状态累计时长 |
### 状态类型
| 内置状态 | 说明 |
|----------|------|
| `Online` | 在线 |
| `Offline` | 离线 |
| `Idle` | 空闲 |
| `Working` | 工作中 |
| `Sleeping` | 睡眠 |
| `"任意字符串"` | 自定义状态,如 `Gaming``Meeting``Driving` 等 |
### 请求示例
```bash
BASE=http://localhost:3000
# 健康检查
curl $BASE/health
# 上报状态(自动记录服务端当前时间)
curl -X POST $BASE/users/alice/checkpoints \
-H "Content-Type: application/json" \
-d '{"state":"Working"}'
# 上报状态 + 附带元数据(设备、坐标等)
curl -X POST $BASE/users/alice/checkpoints \
-H "Content-Type: application/json" \
-d '{"state":"Idle","content":{"device":"MacBook","battery":85}}'
# 上报自定义状态
curl -X POST $BASE/users/alice/checkpoints \
-H "Content-Type: application/json" \
-d '{"state":"Gaming"}'
# 等待几秒后切换状态(产生时长数据)
sleep 3
curl -X POST $BASE/users/alice/checkpoints \
-H "Content-Type: application/json" \
-d '{"state":"Offline"}'
# 查询检查点历史
curl $BASE/users/alice/checkpoints
# 按时间范围查询
curl "$BASE/users/alice/checkpoints?from=1717161600&to=1717248000&limit=10"
# 查询单个检查点
curl $BASE/users/alice/checkpoints/1
# 查询状态汇总(当前状态 + 各状态累计时长)
curl $BASE/users/alice/status
# → {"user_id":"alice","current_state":"Offline","since":1717248000,
# "durations":[{"state":"Working","duration_secs":3}]}
```
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `LISTEN_ADDR` | `127.0.0.1:3000` | 监听地址 |
| `DATABASE_URL` | (无) | 数据库连接字符串(接入真实 DB 时设置) |
## 项目结构
```
src/
├── main.rs # 入口:加载配置 → 初始化 DB → 构建路由 → 启动
├── config.rs # 配置层(环境变量 + 默认值)
├── state.rs # AppState(全局共享状态,持有 DB)
├── error.rs # 统一错误类型 AppError(实现 IntoResponse
├── router.rs # 路由组装
├── models/
│ ├── mod.rs
│ └── checkpoint.rs # StateType / Checkpoint / UserStatusResponse 等
├── handlers/
│ ├── mod.rs
│ ├── health.rs # GET /health
│ └── checkpoints.rs # POST/GET /users/{id}/checkpoints
└── db/
├── mod.rs # Db trait(数据库抽象接口)
└── memory.rs # MemoryDb(内存模拟,开发期使用)
```
## 核心设计
- **状态可自由扩充**`StateType` 内置 5 种状态 + `Custom(String)` 变体,传入任意字符串自动作为新状态
- **content 可扩展**:每个检查点可附带 `Option<serde_json::Value>` 元数据
- **时长自动计算**:相邻检查点的时间差归属于前一个状态,`/status` 返回各状态累计秒数
- **timestamp 可选**:请求可带时间戳,不传则服务端取当前时间
## 接入真实数据库
当前使用内存模拟存储(`MemoryDb`),切换为 PostgreSQL 仅需 3 步:
### 1. 添加依赖
取消 `Cargo.toml` 中的注释:
```toml
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
```
### 2. 实现 Db trait
新建 `src/db/postgres.rs`,对 `PgPool` 实现 `Db` trait。
### 3. 修改入口
`src/main.rs` 中将 `MemoryDb::new()` 替换为 `PgPool::connect(...).await`
## 技术栈
- [Axum 0.8](https://crates.io/crates/axum) — Web 框架
- [Tokio](https://crates.io/crates/tokio) — 异步运行时
- [Serde](https://crates.io/crates/serde) — 序列化 / 反序列化
- [Chrono](https://crates.io/crates/chrono) — 日期时间处理
- [Dotenvy](https://crates.io/crates/dotenvy) — .env 文件加载
## License
MIT