# safety-timer
**Repository Path**: rikh/safety-timer
## Basic Information
- **Project Name**: safety-timer
- **Description**: 安全员工作计时系统:H5 计时 + 排班 + 审批 + 工时导出(Vue3 + Express + SQLite)
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-10-19
- **Last Updated**: 2026-10-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: 工时系统
## README
# Safety Timer · 安全员工作计时系统
> 面向试验车队的「安全员计时 + 排班 + 审批 + 工时导出」一体化系统。
>
> 安全员用手机扫码开始/暂停/结束计时,后台自动生成工单号、按班次校验、沉淀可审计的工时明细。
[](LICENSE)






---
## 1. 它解决什么问题
试验车队的安全员工时,常见做法是微信群报点、Excel 手工登记:
- 开始/结束时间靠人记,暂停、等待、换车容易漏;
- 工单号手工编,跨天容易重号;
- 排班在表里、计时在群里,谁该上哪辆车说不清;
- 月底汇总靠人肉拼表,口径不统一;
- 换组织、强制结束、任务延时没有审批留痕。
Safety Timer 把这些收进一个系统:
- **H5 计时**:选车 → 填出车里程 → 开始 → 暂停/恢复 → 结束 → 填收车里程与备注;
- **后台排班**:班次字典、单人/批量/复制/导入、排班看板;
- **审批中心**:换组织/供应商、强制结束、任务延时,流程/节点/会签可配置;
- **工时导出**:标准 12 列 + 详细工时时间轴,Excel / CSV;
- **审计留痕**:工单号、状态变更、备注、登录日志、审批日志全部落库。
## 2. 界面预览
> 以下为界面演示,数据均为示例,不含真实业务数据。
**安全员 H5 · 今日车辆与计时**

**管理后台 · 详细工时**

---
## 3. 30 秒了解架构
```mermaid
flowchart LR
H5["安全员 H5
Vue3 + Vant"] --> API["Express API
Node.js"]
ADMIN["管理后台
Vue3 + Element Plus"] --> API
API --> TIMER["计时服务
状态机 / 互斥 / 工单号"]
API --> SCHED["排班服务
班次 / 排班 / 越权留痕"]
API --> APPR["审批中心
流程 / 节点 / 会签"]
API --> EXP["导出服务
Excel / CSV"]
TIMER --> DB[("SQLite
WAL")]
SCHED --> DB
APPR --> DB
EXP --> DB
```
**两个关键词:**
1. **后端权威**:开始/暂停/结束的时间以服务端为准;前端只展示,不参与最终计时。
2. **单进程同源**:Express 同时提供 API 与前端静态资源,部署只需要一个 Node 进程 + 一个 SQLite 文件。
## 4. 功能清单
### 4.1 安全员 H5
- 账号登录,首次登录强制改密;
- 只显示「今天排给我的车」,也可切换查看全部车辆(非排班车需二次确认并留痕);
- 开始计时必须填**出车里程**,结束必须填**收车里程**;
- 暂停必须填备注;恢复、结束、追加备注;
- 实时显示净时长(自动扣除暂停);
- 我的记录:按日期查看本人工单与时间轴;
- 我的排班:按日期区间查看班次与车辆。
### 4.2 管理后台
| 模块 | 能力 |
|:--|:--|
| 安全员 | 新增/编辑/停用、批量导入、开通 H5 账号、重置密码 |
| 车辆 | 车架号、工位、组织、供应商、默认发起人;批量导入 |
| 字典 | 组织、供应商、发起人维护 |
| 排班 | 班次字典、单人排班、批量排班、复制、导入、排班看板 |
| 审批 | 流程模板、节点/审批人/会签、自定义申请字段、审批/驳回/撤销 |
| 计时记录 | 按日期/安全员/车架号查询,强制结束(走审批) |
| 详细工时 | 含暂停、里程、排班备注、时间轴的明细 |
| 账号与角色 | admin / engineer / fleet_admin 三角色,模块级读写权限 |
| 登录日志 | 成功/失败、来源 IP、账号锁定记录 |
| 导出 | 标准工时 Excel/CSV、详细工时 Excel/CSV |
### 4.3 自动化
- **工单号**:`WO-YYYYMMDD-序号`,按天自增,事务内生成;
- **互斥**:同一安全员同一时间只能有一条进行中的计时(数据库唯一索引兜底);
- **自动结束**:超过班次结束时间 + 缓冲(默认 30 分钟)仍未结束的工单,可自动结束;
- 默认 `observe`(只扫描、只打日志、零写入);
- 显式设置 `AUTO_END_MODE=live` 才真正写库;
- **登录限流**:账号连续失败锁定 + 同 IP 失败上限;
- **改密失效旧 token**:`pwd_ver` 版本号机制,改密/重置后旧 token 立即失效。
## 5. 技术栈
- **前端**:Vue 3 + Vite 5 + TypeScript + Vue Router + Pinia
- H5:Vant 4
- 后台:Element Plus
- **后端**:Node.js ≥ 20 + Express 4 + better-sqlite3(SQLite,WAL)
- **导出**:SheetJS (`xlsx`)
- **鉴权**:HMAC-SHA256 无状态 token + HttpOnly Cookie;口令 scrypt 加盐派生
- **部署**:单进程 Node + Nginx 反代(可选 systemd)
## 6. 快速开始
### 6.1 环境要求
- Node.js **≥ 20**(推荐 22 LTS)
- npm ≥ 9
- Linux / macOS / Windows 均可;生产建议 Linux
### 6.2 安装与启动
```bash
git clone https://gitee.com/rikh/safety-timer.git
cd safety-timer
# 安装前后端依赖
npm run install:all
# 构建前端(输出到 web/dist)
npm run build
# 首次启动:初始化管理员(至少 8 位,初始化后可不再设置)
export ADMIN_INIT_PASSWORD='ChangeMe_123456'
npm start
# 打开
# 后台:http://localhost:19300/admin/login
# H5 :http://localhost:19300/login
```
> 首次启动时,如果数据库里没有任何后台账号,且没有设置 `ADMIN_INIT_PASSWORD` / `ADMIN_PASSWORD`,服务会拒绝启动并提示。
### 6.3 开发模式
```bash
# 终端 1:后端(19300)
npm run dev:server
# 终端 2:前端(5173,自动代理 /api 到 19300)
npm run dev:web
```
开发模式访问 `http://localhost:5173`。
### 6.4 创建第一个安全员账号
1. 用初始管理员登录 `/admin/login`;
2. 「安全员」→ 新增安全员(姓名、工号、组别);
3. 点「开通账号」,设置登录名与初始密码;
4. 安全员用手机打开 `/login`,首次登录会强制改密;
5. 「排班」给该安全员安排今天/某天的车辆与班次;
6. 安全员进入「今日车辆」即可开始计时。
### 6.5 演示数据(可选)
```bash
# 写入安全员/车辆/组织/供应商/发起人示例数据(幂等)
npm run seed
# 生成完整演示数据:账号、排班、最近 6 天 + 今天早间工单、1 条进行中工单
DB_FILE=/tmp/safety-timer-demo.db npm run demo
# H5 账号:E1001~E1004 / demo1234
```
## 7. 配置
所有配置通过环境变量注入;生产建议使用 systemd `EnvironmentFile`,不要把口令写进代码或仓库。
| 变量 | 默认值 | 说明 |
|:--|:--|:--|
| `PORT` | `19300` | 监听端口 |
| `HOST` | `0.0.0.0` | 监听地址 |
| `DB_FILE` | `server/safety-timer.db` | SQLite 文件路径 |
| `ADMIN_USER` | `admin` | 初始管理员用户名 |
| `ADMIN_INIT_PASSWORD` | 无 | 首次启动初始化管理员;仅在账号表为空时生效 |
| `ADMIN_PASSWORD` | 无 | 环境变量单口令兜底通道;**不推荐长期开启** |
| `AUTH_SECRET` | 无 | 固定 HMAC 密钥(字符串);优先级最高 |
| `AUTH_SECRET_FILE` | `~/.safety-timer/auth-secret` | 密钥文件(64 位 hex);不存在会自动生成 |
| `TZ` | 系统时区 | 建议 `Asia/Shanghai` |
| `AUTO_END_MODE` | `observe` | `observe` 只观察 / `live` 真写库 |
| `AUTO_END_BUFFER_MIN` | `30` | 班次结束后缓冲分钟数 |
| `AUTO_END_MAX_HOURS` | `0` | 硬顶小时数;`0` 表示不启用 |
| `AUTO_END_INTERVAL_MS` | `120000` | 扫描间隔(毫秒) |
| `AUTO_END_DISABLED` | `0` | `1` 关闭自动结束 |
| `SCHEDULE_ENFORCE` | 未设置 | `1` 启用排班硬校验;未设置时按代码内默认策略 |
完整示例见 [`.env.example`](.env.example)。
## 8. 目录结构
```
safety-timer/
├─ server/ # Express API + SQLite
│ ├─ app.js # 路由装配 / 静态托管
│ ├─ index.js # 启动入口
│ ├─ db.js # 建表 / 增量迁移
│ ├─ middleware/ # 鉴权 / 限流
│ ├─ migrations/ # 排班 / 审批 / 登录日志
│ ├─ routes/ # REST 路由
│ ├─ services/ # 计时 / 排班 / 导出 / 自动结束
│ ├─ seed.js # 示例数据
│ └─ test/ # Vitest + Supertest
├─ web/ # Vue3 前端(H5 + 后台)
│ ├─ src/views/h5/ # 安全员 H5
│ ├─ src/views/admin/ # 管理后台
│ └─ vite.config.ts
├─ deploy/ # systemd / Nginx / 备份模板
├─ docs/ # 架构 / API / 权限 / 部署 / 运维
└─ assets/ # 截图与赞赏码
```
## 9. 核心业务规则
1. **一人一车**:同一安全员同一时间只能有一条 `running/paused` 的计时;数据库部分唯一索引兜底。
2. **工单号**:`WO-YYYYMMDD-001`,按天重置;使用 `BEGIN IMMEDIATE` 事务,避免并发重号。
3. **净时长**:`试验实际时长 = Σ work 段`,暂停段不计入;暂停秒数单独累计。
4. **排班优先**:H5 默认只列今日排班车辆;非排班车需二次确认并写入 `schedule_override` 留痕。
5. **班次校验**:未到班次开始时间不可开始;同日同人同车已结束不可再次开始。
6. **里程校验**:出车/收车里程必填;收车里程不得小于出车里程,并与上次收车里程做偏差校验。
7. **导出只含已结束工单**:`stopped` 才进入正式报表,进行中不导出。
8. **自动结束安全默认**:默认 `observe`,只记录「将结束」日志,不写库;确认无误后再切 `live`。
## 10. 角色与权限
| 模块 | admin | engineer(工程师) | fleet_admin(车队管理员) |
|:--|:--:|:--:|:--:|
| 安全员 读 | ✅ | ✅ | ✅ |
| 安全员 写 | ✅ | ❌ | ✅ |
| 车辆 读 | ✅ | ✅ | ✅ |
| 车辆 写 | ✅ | ✅ | ❌ |
| 字典 读写 | ✅ | ✅ | ✅ |
| 排班 读写 | ✅ | ✅ | ✅ |
| 审批 读写 | ✅ | ✅ | ✅ |
| 计时/详细工时 | ✅ | ✅ | ✅ |
| 账号管理 | ✅ | ❌ | ❌ |
| 登录日志 | ✅ | ❌ | ❌ |
安全员 H5 使用独立的 `operator` 身份,只能操作**本人**的计时和排班,不能访问后台。
详见 [docs/roles-and-permissions.md](docs/roles-and-permissions.md)。
## 11. API
- 统一前缀:`/api`
- 成功:`{ "ok": true, "data": ... }`
- 失败:`{ "ok": false, "code": "...", "message": "..." }`
- 鉴权:后台 `st_auth` Cookie 或 `Authorization: Bearer `;H5 使用 `st_op` Cookie / Bearer
常用接口:
| 方法 | 路径 | 说明 |
|:--|:--|:--|
| `POST` | `/api/admin/login` | 后台登录 |
| `POST` | `/api/h5/login` | 安全员 H5 登录 |
| `GET` | `/api/h5/my-vehicles` | 今日排班车辆 |
| `POST` | `/api/sessions/start` | 开始计时 |
| `POST` | `/api/sessions/:id/pause` | 暂停(需备注) |
| `POST` | `/api/sessions/:id/resume` | 恢复 |
| `POST` | `/api/sessions/:id/stop` | 结束(需收车里程) |
| `GET` | `/api/admin/export` | 标准工时导出 |
| `GET` | `/api/admin/export/detail` | 详细工时导出 |
完整列表与参数见 [docs/api.md](docs/api.md)。
## 12. 部署
生产建议:
- Node 进程由 systemd 托管;
- Nginx 终止 HTTPS 并反代到 `127.0.0.1:19300`;
- SQLite 放在独立数据目录(如 `/var/lib/safety-timer`),定期热备份;
- 不要直接把 19300 暴露到公网,至少加 HTTPS 与访问控制。
模板与步骤:
- [deploy/safety-timer.service](deploy/safety-timer.service)
- [deploy/nginx.conf.example](deploy/nginx.conf.example)
- [deploy/backup.sh.example](deploy/backup.sh.example)
- [docs/deployment.md](docs/deployment.md)
- [docs/operations.md](docs/operations.md)
## 13. 安全说明
- 口令使用 **scrypt + 随机盐**,只存派生值,不存明文;
- Token 使用 **HMAC-SHA256** 无状态签名,默认 30 天;`pwd_ver` 改密即失效;
- Cookie 为 `HttpOnly`,生产必须启用 HTTPS;
- 登录失败有限流/锁定;
- 首次启动不再内置弱口令,必须显式设置初始化口令;
- 生产请设置固定的 `AUTH_SECRET`,并限制数据库文件权限(`0600`)。
发现安全问题请阅读 [SECURITY.md](SECURITY.md),不要在公开 Issue 中贴敏感信息。
## 14. 许可证与第三方组件
- 本项目代码:**MIT License**(见 [LICENSE](LICENSE))
- [Vue.js](https://github.com/vuejs/core):MIT
- [Vite](https://github.com/vitejs/vite):MIT
- [Element Plus](https://github.com/element-plus/element-plus):MIT
- [Vant](https://github.com/youzan/vant):MIT
- [Express](https://github.com/expressjs/express):MIT
- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3):MIT
- [SheetJS](https://github.com/SheetJS/sheetjs):Apache-2.0
> 仓库内**不包含**任何真实业务数据、账号、密钥或内网地址。示例数据均为虚构。
## 15. 贡献
欢迎 Issue / PR。提交前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md),并确保:
```bash
npm test
npm run build
```
---
## ☕ 赞赏
如果这个项目帮到了你,欢迎请作者喝杯咖啡。**完全自愿**,不影响使用与更新。
左:微信支付(推荐) · 右:支付宝