Files
2026-08-01 23:09:49 +08:00

409 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 运行手册
> 本文档是项目运行的唯一事实来源。后续操作优先按本文档执行,若实际命令与文档不符,先验证真实行为,再更新本文档。
## 技术栈
| 层级 | 技术 | 版本 |
|---|---|---|
| 前端框架 | React + TypeScript | ^19 |
| 构建工具 | Vite | ^6 |
| 后端框架 | Express.js | ^4 |
| 数据库 | SQLiteWAL 模式) | 内置 node:sqlite |
| 包管理器 | npm | >= 18 |
| 图表库 | Recharts | latest |
| 图标库 | Lucide React | latest |
| 进程管理 | concurrently | latest |
| 测试框架 | Jest + Supertest | latest |
## 本地环境
### 环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
| `API_PORT` | `8787` | 后端 API 监听端口 |
### 端口清单
| 服务 | 地址 | 说明 |
|---|---|---|
| 前端开发服务器 | http://localhost:5177 | Vite 热重载 |
| 后端 API | http://localhost:8787 | Express REST API |
| 前端代理 | `/api``http://localhost:8787` | Vite proxy 配置 |
### 依赖服务
- 无外部依赖服务(SQLite 为内置文件数据库)
## 安装
```bash
npm install
```
> 若 `node_modules` 已存在,跳过此步骤。
## 开发命令
### 全栈启动(推荐)
```bash
npm run dev
```
同时启动:
- API 服务(`node --watch server/index.js`)监听 `127.0.0.1:8787`
- 前端 Vite 开发服务器监听 `localhost:5173`API 请求代理至 `8787`
### 单独启动
```bash
# 仅前端(需 API 已在运行)
npm run web
# 仅后端(需前端已在运行)
npm run server
```
### 后端启动(独立,不热重载)
```bash
npm run server:start
```
## 构建
```bash
npm run build
```
- 前端编译输出至 `dist/`
- TypeScript 类型检查:`tsc -b`
- 两者均通过才算构建成功
## 测试
```bash
npm test
```
运行 `server/tests/integration.test.cjs`,覆盖所有 API 端点的集成测试。测试使用独立临时数据库,测试完成后自动清理。
## 数据库
### 数据文件
- 开发数据库:`data/reporter-station.db`SQLite
- WAL 文件:`data/reporter-station.db-wal`Write-Ahead Log
- SHM 文件:`data/reporter-station.db-shm`
### 迁移管理
使用 `migrations/_runner.js` 管理所有 schema 变更:
```bash
# 查看迁移状态
node migrations/_runner.js --status
# 应用所有待执行迁移
node migrations/_runner.js migrate
# 执行指定迁移(幂等,CREATE TABLE IF NOT EXISTS
node migrations/_runner.js migrate --target 005
# 回滚最后一条迁移
node migrations/_runner.js rollback
# 重置数据库(删除并重建所有表,重新 seed)
node migrations/_runner.js reset
# 查看 seed 数据
node migrations/_runner.js --seed
```
迁移按文件名数字前缀顺序执行(001~009)。新增迁移时,使用下一个可用数字前缀。
### 数据库结构
**work_records**V0.1
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | TEXT PK | 主键,格式 `WK-YYYYMMDD-XXXX` |
| `title` | TEXT | 工作标题 |
| `type` | TEXT | 工作类型(文字稿件/视频供稿/...) |
| `reporter` | TEXT | 记者姓名 |
| `station` | TEXT | 所属记者站 |
| `occurred_date` | TEXT | 发生/刊发日期 |
| `platform` | TEXT | 媒体/平台 |
| `status` | TEXT | 状态(draft/station_review/headquarters_review/returned/archived |
| `score` | INTEGER | 考核得分(可为 null |
| `description` | TEXT | 工作说明 |
| `review_note` | TEXT | 审核意见 |
| `updated_at` | TEXT | 更新时间 |
**audit_logs**V0.1
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `record_id` | TEXT FK | 关联 work_records.id |
| `actor_role` | TEXT | 操作者角色 |
| `actor_name` | TEXT | 操作者姓名 |
| `action` | TEXT | 操作类型(submit/pass/return |
| `from_status` | TEXT | 原状态 |
| `to_status` | TEXT | 新状态 |
| `score` | INTEGER | 打分 |
| `note` | TEXT | 意见 |
| `created_at` | TEXT | 操作时间 |
**people**V0.1.1
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `code` | TEXT UNIQUE | 人员编码 |
| `name` | TEXT | 姓名 |
| `role` | TEXT | 角色(headquarters/station/reporter |
| `title` | TEXT | 职务 |
| `phone` | TEXT | 联系电话 |
| `station` | TEXT | 所属记者站 |
| `status` | TEXT | 状态(active/inactive |
| `joined_at` | TEXT | 入职日期 |
**stations**V0.1.1
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `code` | TEXT UNIQUE | 记者站编码 |
| `name` | TEXT | 记者站名称 |
| `region` | TEXT | 所属区域 |
| `established_at` | TEXT | 成立日期 |
**notices**V0.1.1
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `title` | TEXT | 通知标题 |
| `content` | TEXT | 通知内容 |
| `type` | TEXT | 类型(notice/circular/guide |
| `priority` | TEXT | 优先级(normal/important/urgent |
| `created_by` | TEXT | 创建人 |
| `created_at` | TEXT | 创建时间 |
| `expire_at` | TEXT | 过期时间 |
**rules**V0.2
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `code` | TEXT UNIQUE | 规则编码 |
| `name` | TEXT | 规则名称 |
| `description` | TEXT | 规则说明 |
| `period_type` | TEXT | 周期类型(quarterly/custom |
| `period_start` | TEXT | 周期开始日期 |
| `period_end` | TEXT | 周期结束日期 |
| `status` | TEXT | 状态(draft/active/archived |
| `version` | INTEGER | 版本号 |
| `parent_id` | INTEGER FK | 父版本 ID |
| `created_by` | TEXT | 创建人 |
| `created_at` | TEXT | 创建时间 |
| `updated_at` | TEXT | 更新时间 |
**rule_items**V0.2
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `rule_id` | INTEGER FK | 关联 rules.id |
| `category` | TEXT | 指标类别(quantity/quality/efficiency/compliance |
| `name` | TEXT | 指标名称 |
| `metric_key` | TEXT | 指标键名 |
| `weight` | REAL | 权重 |
| `min_score` | REAL | 最低分 |
| `max_score` | REAL | 最高分 |
| `formula_type` | TEXT | 计算公式类型 |
| `formula_params` | TEXT | 公式参数(JSON |
| `display_order` | INTEGER | 排序 |
| `enabled` | INTEGER | 是否启用 |
**scores**V0.2
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | INTEGER PK | 自增主键 |
| `rule_id` | INTEGER FK | 关联 rules.id |
| `station` | TEXT | 站点名称 |
| `period` | TEXT | 考核周期 |
| `period_type` | TEXT | 周期类型 |
| `total_score` | REAL | 综合得分 |
| `quality_score` | REAL | 质量分 |
| `quantity_score` | REAL | 数量分 |
| `efficiency_score` | REAL | 时效分 |
| `compliance_score` | REAL | 合规分 |
| `items` | TEXT | 明细(JSON 数组) |
| `computed_at` | TEXT | 计算时间 |
## API 接口文档
### 认证
所有 API 请求需携带 HTTP header
```
x-user-role: headquarters | station | reporter
```
### 核心接口
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/health` | 公开 | 健康检查 |
| GET | `/api/records` | 全部 | 查询工作记录(按角色隔离) |
| POST | `/api/records` | reporter/station | 新建记录 |
| PATCH | `/api/records/:id/review` | station/hq | 审核(通过/退回) |
| GET | `/api/records/:id/audit` | 全部 | 流转记录(按角色隔离) |
### 人员与站点(V0.1.1
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/people` | 全部 | 查询人员列表 |
| POST | `/api/people` | station | 新建人员 |
| PATCH | `/api/people/:id` | station/hq | 编辑人员 |
| GET | `/api/stations` | 全部 | 查询记者站列表 |
| POST | `/api/stations` | hq | 新建记者站 |
### 通知公告(V0.1.1
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/notices` | 全部 | 查询通知列表 |
| POST | `/api/notices` | hq | 发布通知 |
| POST | `/api/notices/:id/read` | 全部 | 标记已读 |
| GET | `/api/notices/unread-count` | 全部 | 未读数量 |
### 统计(V0.1.1
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/stats/overview` | 全部 | 总览统计(站点角色仅限本站) |
| GET | `/api/stats/records` | hq | 记录统计(支持按周期/站点/类型筛选) |
| GET | `/api/stats/scores` | hq | 评分统计(支持按周期/站点筛选) |
### 考核规则(V0.2
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/rules` | hq | 查询规则列表(支持 status 筛选) |
| GET | `/api/rules/:id` | hq | 查询规则详情(含指标项) |
| POST | `/api/rules` | hq | 创建规则(含指标项) |
| PATCH | `/api/rules/:id` | hq | 编辑规则(草稿状态) |
| POST | `/api/rules/:id/activate` | hq | 激活规则 |
### 评分(V0.2
| 方法 | 路径 | 角色 | 说明 |
|---|---|---|---|
| GET | `/api/scores` | hq | 查询评分列表(支持 period/station 筛选) |
| GET | `/api/scores/:id` | hq | 查询评分详情(含明细) |
| POST | `/api/scores/compute` | hq | 触发评分计算 |
### 请求示例
```bash
# 以总部管理员身份查询全部记录
curl -H "x-user-role: headquarters" http://localhost:8787/api/records
# 以记者身份提交新记录
curl -X POST http://localhost:8787/api/records \
-H "Content-Type: application/json" \
-H "x-user-role: reporter" \
-d '{"title":"测试稿件","type":"文字稿件","date":"2026-08-01","platform":"测试平台"}'
# 以分站负责人身份审核通过
curl -X PATCH http://localhost:8787/api/records/WK-202608-XXXX/review \
-H "Content-Type: application/json" \
-H "x-user-role: station" \
-d '{"decision":"pass","score":8,"note":"审核通过"}'
# 总部创建考核规则
curl -X POST http://localhost:8787/api/rules \
-H "Content-Type: application/json" \
-H "x-user-role: headquarters" \
-d '{"name":"2026年度Q3考核","period_type":"quarterly","items":[{"category":"quantity","name":"发稿数量","metric_key":"count_total","weight":0.4,"formula_type":"count","formula_params":{}},{"category":"quality","name":"审核得分","metric_key":"avg_score","weight":0.6,"formula_type":"avg_score","formula_params":{}}]}'
# 总部触发评分计算
curl -X POST http://localhost:8787/api/scores/compute \
-H "Content-Type: application/json" \
-H "x-user-role: headquarters" \
-d '{"rule_id":1,"period":"2026-Q3","period_type":"quarterly"}'
```
## 常见问题
### Q1:端口 8787 被占用
```bash
# 查找占用进程
lsof -i :8787
# 杀死进程(替换 PID
kill -9 <PID>
# 或换端口
API_PORT=8788 npm run dev
```
### Q2:端口 5173 被占用
```bash
lsof -i :5173
kill -9 <PID>
```
### Q3SQLite WAL 文件导致"database is locked"
- 通常因多个进程同时写入
- 解决:确保只有一个服务实例运行
- WAL 文件异常时,删除 `.db-wal``.db-shm` 后重启服务
### Q4:前端热重载不生效
- 确认 Vite dev server 正在运行(看终端输出 "ready in xxx ms"
- 清除浏览器缓存或使用无痕模式
### Q5:迁移执行失败
```bash
# 查看迁移状态
node migrations/_runner.js --status
# 检查是否有 pending 迁移
# 查看具体错误日志
```
### Q6TypeScript 类型错误
```bash
npx tsc -b --force # 强制重建类型缓存
```
### Q7`npm run build` 失败
1. 先确认 TypeScript 无误:`npx tsc -b`
2. 查看 Vite 构建输出中的具体错误
3. 检查是否有新增依赖未安装
### Q8seed 数据消失
- 原因:`work_records` 表已有数据时,seed 逻辑不执行
- 解决:手动删除数据库文件后重启服务,或使用 `node migrations/_runner.js reset`
## 后续开发参考
- 需求文档:`pmdocs/0-req-RSMS.md`
- 产品文档:`pmdocs/1-prd-RSMS.md`
- 任务文档:`pmdocs/2-task-RSMS.md`
- 架构决策:`pmdocs/adr/`
- 需求变更:`pmdocs/changes/`
- 原始需求参考:`req.md`