激光材料数据平台 1.3.0

公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、
PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。

图片相关:
- 录入表单支持相册选图与手机端直接拍照,每条记录最多 6 张
- 浏览器内压缩到最长边 1600 并剥离 EXIF,入库统一为 JPG/PNG
- 二进制直接入库,现有备份自动覆盖图片,部署无需新增卷
- 详情页缩略图宫格与全屏 lightbox,公开看板同样可见

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 22:06:02 +08:00
co-authored by Claude Fable 5
commit edfb216561
49 changed files with 3922 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
# 激光材料数据平台操作手册
版本:1.0 更新日期:2026-07-17
## 1. 部署前准备
生产环境建议至少 2 核 CPU、4 GB 内存、20 GB 可用磁盘,安装 Docker Engine 24+ 与 Docker Compose v2+。应准备域名、HTTPS 证书和一处异机备份存储。服务器只需对外开放反向代理的 80/443 端口;PostgreSQL 不应直接暴露到公网。
复制环境变量文件:
```bash
cp .env.example .env
```
至少修改以下内容:
```dotenv
APP_SECRET=使用密码生成器创建的32位以上随机字符串
ADMIN_USERNAME=admin
ADMIN_PASSWORD=首次登录使用的高强度密码
POSTGRES_PASSWORD=数据库高强度密码
BACKUP_KEEP_DAYS=30
```
`.env` 不得提交到 Git,也不要通过聊天或邮件发送。
## 2. 启动、升级与停止
首次启动:
```bash
docker compose up -d --build
docker compose ps
docker compose logs --tail=100 web
```
`web``db` 均为运行状态后,访问 `http://服务器IP:5980`。端口由 `.env``APP_PORT` 控制。生产环境应在前方配置 Nginx/Caddy HTTPS 反向代理。只有在明确限制受信代理 IP 后才应启用转发头解析,不能无条件信任公网请求提供的 `X-Forwarded-For`,否则会削弱 Key 尝试限速。
更新代码后:
```bash
docker compose up -d --build
```
停止但保留数据库:
```bash
docker compose down
```
不要执行 `docker compose down -v`,该命令会删除 PostgreSQL 数据卷。
## 3. 首次登录
1. 打开登录页,输入 `.env` 中的管理员账号和密码。
2. 首次登录后点击“立即修改”,设置至少 10 位且同时含字母和数字的新密码。
3. 在“用户与权限”中为实际使用人员创建独立账号。禁止多人共用管理员账号,否则审计日志无法追溯到个人。
登录会话默认 12 小时。退出时点击左下角退出按钮。连续失败登录会记入审计日志;生产环境还应在反向代理层启用限速。
## 4. 手动录入实验数据
系统提供两种手动录入入口:登录管理后台后点击左侧“手动录入”,或访问 `/entry` 使用四位录入 Key。两者使用相同的字段校验和数据库事务。
### 4.1 设置四位录入 Key
管理员或数据录入员登录 `/admin` 后点击右上角“录入 Key”,输入两次四位数字和当前登录密码。只读用户不能设置或使用录入 Key。Key 在所有用户中必须唯一,数据库只保存带应用密钥的摘要,不保存原值。更换或停用 Key 会立即撤销已签发的录入会话。
四位数字只有 10,000 种组合,因此该入口采用以下限制:同一 IP 在 15 分钟内最多失败 5 次;验证成功后授权仅持续 30 分钟;提交时必须再次输入 Key 所属账号的准确用户名。不要把 Key 设置成易猜的生日、年份或连续数字,也不要公开传播。
访问 `/entry`,输入 Key 后进入专用录入面板。数据填写完成后点击“检查并提交”,在弹窗中输入登录用户名(不是显示名称)再次确认。用户名或 Key 会话不匹配时,数据不会写入。
### 4.2 字段填写
表单分为五组:
1. 实验与材料:实验 ID、测试日期、材料大类、名称/型号、厚度。
2. 材料物理特性:原 Excel 红色表头对应的 10 项字段,全部可选。
3. 激光雕刻参数:功率、电流、速度、频率、脉宽、离焦、线间距、填充方式和加工尺寸。
4. 实验结果:切透、碳化边宽度、蚀刻深度、起火/阴燃、两项评分、成品图文件名和备注。
5. 成品图片:可选,最多 6 张,详见 4.3。
`*` 的项目必须填写。实验 ID 在整个系统内唯一;百分比必须位于 0–100,评分必须位于 0–10,是否类字段通过下拉框选择。点击“保存实验数据”后,后端会再次校验;任何字段错误都不会产生半条记录。
建议实验 ID 采用固定规则,例如 `LAS-2026-0001`。“成品图文件名”记录图片在单位文件服务器或对象存储中的归档名;上传第一张图片后系统会自动填入该字段(手机拍照没有有意义的文件名时,用实验 ID 补齐),需要时可以手动改写。
### 4.3 上传成品图片
在录入表单第 5 段“成品图片”里,点击 **选择图片** 从相册或文件中挑选(支持一次多选),手机和平板上还会多出一个 **拍照** 按钮,点击即调起系统相机直接拍摄。电脑上也可以把图片文件直接拖进虚线框。
要点:
- 每条记录最多 6 张。可以选择 JPG、PNG、WebP、HEIC 照片,浏览器会统一转成 JPG 或 PNG 再上传;出于安全考虑不接受 SVG、GIF 及任何伪装成图片的文件。
- 图片在浏览器里就会被压缩到最长边 1600 像素再上传,一般每张 200–400 KB,用手机流量提交也不慢。压缩过程顺带**清除了照片中的 GPS 定位等 EXIF 信息**。
- 图片是逐张依次上传的,缩略图右上角的 × 可以移除,失败的会显示原因并提供“重试”。**上传未完成时提交会被拦下**,请等所有缩略图不再显示进度条。
- iPhone 若拍出 HEIC 格式且浏览器无法转换,页面会提示到「设置 → 相机 → 格式」选「兼容性最佳」后重拍。
- 上传后未提交的图片属于临时状态,只有上传者本人能看到,2 小时后自动清理。所以**填完表单要及时提交**。
- 图片二进制保存在数据库中,日常数据库备份已经包含图片,不需要单独备份图片目录。
在管理后台编辑已有记录时,原有图片会显示在同一位置,可以删除或继续追加,保存后生效。只读账号只能查看,不能增删。
### 4.4 管理扩展数据字段
管理员进入 `/admin`,选择“数据字段配置”,可添加固定 32 项之外的数据种类。支持以下填写形式:
| 填写形式 | 用途 |
|---|---|
| 单行文本 | 型号、方法、短说明 |
| 多行文本 | 较长实验描述 |
| 数字 | 可含小数的测量值 |
| 整数 | 次数、等级编号 |
| 日期 | ISO 日期 |
| 是 / 否 | 二元状态 |
| 下拉选项 | 由管理员维护的固定选项 |
每个字段还可设置名称、单位、排序值、必填/可选、是否在公开看板显示以及启用/停用状态。排序值越小越靠前。
系统不允许从页面物理删除字段。字段名称、类型、选项或启用状态改变时,`custom_field_values` 中的原始值不被改写:
- 新增必填字段不会伪造旧记录的值;旧记录显示为未填写,后续编辑时需要补充。
- 类型改变后,能转换的历史值按新类型显示。
- 无法转换的值显示原文并标记“历史格式”;编辑其他字段时留空不会清除该原值,主动填写符合新类型的新值才会替换。
- 停用字段不再出现在新录入表单,但已有值仍在管理详情中显示;若字段允许公开,公开详情也会标记为历史字段。
- 调整下拉选项不会删除已不在新选项列表中的旧值。
## 5. 公开数据面板
访问 `/` 无需登录。页面显示总记录数、材料分类数、近 30 天新增、平均清晰度,并支持按实验 ID、材料名称和材料分类筛选。点击记录可查看成品图片、材料物性、激光参数和实验结果。详情弹窗顶部是成品图缩略图,点击任意一张即可全屏放大,支持左右方向键、手机左右滑动切换,按 Esc 或点击空白处关闭。
公开接口不会返回录入用户、备注、成品图文件名、审计日志或账号信息。**但成品图片本身是公开的**:任何人都能在公开看板看到并直接访问图片地址。请提醒录入人员不要拍进白板、工牌、屏幕等含内部信息的画面;若图片属于内部资料,应在正式上线前收紧 `GET /api/images/{id}` 的权限。扩展字段只有在管理员启用“公开看板显示”后才会公开。若某些科学参数也属于内部敏感数据,应在正式上线前调整公开字段白名单。
## 6. 查询、编辑与删除
“数据中心”支持按实验 ID、材料名称模糊搜索,或按材料大类精确筛选。
- 管理员:可查看、编辑、删除。
- 录入员:可查看、编辑,不能删除。
- 只读用户:只能查看。
点击“编辑”会将完整数据载入表单。删除前系统要求二次确认,删除动作不可从页面撤销,但会写入审计日志;如需找回,应通过数据库备份恢复到临时实例后导出目标记录,不建议直接覆盖生产库。
## 7. 用户和权限管理
管理员进入“用户与权限”后可创建账号、修改角色、停用账号或重置密码。被重置密码的用户下次登录会收到改密提示。系统阻止停用当前登录账号,并保证至少保留一名启用的管理员。
人员离职时应立即停用而不是复用账号。每季度检查一次管理员列表和长期未使用账号。
## 8. 审计日志
审计日志记录登录成功/失败、退出、密码修改、记录新增/修改/删除、用户管理及备份创建,包含操作者、资源、时间、IP 和摘要。只有管理员可查看。审计表应纳入数据库备份,日常不得手工修改。
## 9. 备份与恢复
管理员可在“备份管理”点击“立即备份”。生产 PostgreSQL 使用 `pg_dump --format=custom` 创建一致性备份;开发 SQLite 使用在线备份 API。每个备份记录大小和 SHA-256 摘要,默认保留 30 天。
建议用宿主机定时任务每日执行:
```bash
docker compose exec -T web python scripts/db_backup.py
```
项目内 `backups/` 只是第一份副本。应每天同步到另一台服务器或对象存储,并至少每季度做一次恢复演练。
成品图片保存在数据库中,上面两种备份都已包含图片,不需要单独备份图片目录,恢复后也不会出现“数据在、图片丢”。代价是备份体积会随图片增长:按每条记录 3 张、单张 300 KB 估算,每 1000 条记录约多出 0.9 GB,且 JPEG 几乎无法再压缩。请据此核算 `backups/` 所在磁盘容量,必要时调小 `BACKUP_KEEP_DAYS` 或缩短异机副本的保留周期。
恢复必须安排维护窗口:停止 Web 写入,确认备份摘要及时间,再执行:
```bash
docker compose stop web
docker compose run --rm web python scripts/db_restore.py database-YYYYMMDD-HHMMSS.dump --confirm
docker compose up -d web
```
恢复会覆盖当前数据库。生产恢复前先额外备份当前状态,并优先在临时数据库验证备份可用性。
## 10. 日常检查与故障处理
- 健康检查:访问 `/health`,应返回 `status: ok`
- 服务日志:`docker compose logs --tail=200 web`
- 数据库日志:`docker compose logs --tail=200 db`
- 磁盘检查:关注 PostgreSQL 数据卷、`backups/` 和 Docker 日志空间。
- 无法登录:确认账号未停用、系统时间正确、Cookie 未被浏览器禁止。
- 保存返回 422:按提示检查必填、数值范围和日期格式。
- 备份失败:确认容器内 `pg_dump` 可用、磁盘空间充足、数据库连接正常。
## 11. 上线安全清单
- 已修改 `APP_SECRET`、管理员密码和数据库密码。
- 已启用 HTTPSHTTP 自动跳转 HTTPS。
- 数据库端口未暴露公网。
- `.env` 权限受限且未进入版本库。
- 已创建个人账号并停用不必要账号。
- 已为需要快速录入的用户单独设置 Key,且未使用弱组合。
- 已确认成品图片可以公开;如属内部资料,已收紧 `GET /api/images/{id}` 权限。
- 已按预期图片量核算备份磁盘容量。
- 每日自动备份已配置,异机副本和恢复演练已完成。
- 服务器与容器镜像有定期补丁计划。
+268
View File
@@ -0,0 +1,268 @@
# 数据库与接口文档
版本:1.0 基础路径:`/api`
## 1. 技术与一致性
生产数据库为 PostgreSQL 17,开发环境可使用 SQLite。访问层使用 SQLAlchemy 2;所有手动录入以事务提交,校验失败时整次回滚。时间字段保存带时区时间,业务日期使用 `DATE`。外键在 PostgreSQL 和 SQLite 中均启用。
主要关系:
```text
users 1 ── n login_sessions
users 1 ── n entry_sessions
users 1 ── n entry_batches(手动录入事务批次)
entry_batches 1 ── n material_records
material_records 1 ── n custom_field_values
custom_field_definitions 1 ── n custom_field_values
material_records 1 ── n record_images(成品图,二进制入库)
users 1 ── n audit_logs
users 1 ── n backup_records
```
## 2. 数据表
### users
账号表。`username` 唯一;`password_hash` 为 PBKDF2-SHA256310,000 次)结果,不保存明文。`role``admin``uploader``viewer``is_active` 控制账号可用性;`must_change_password` 标识初始/重置密码。`entry_key_hash` 是使用 `APP_SECRET` 生成的 HMAC-SHA256 摘要并带唯一索引,`entry_key_updated_at` 记录设置时间。修改 `APP_SECRET` 会使现有录入 Key 全部失效。
### login_sessions
服务端会话表。浏览器只持有随机令牌,库内保存令牌加应用密钥后的 SHA-256 摘要。`expires_at` 控制过期,用户停用后已有会话立即失效。
### entry_sessions
专用录入授权表,与管理登录会话完全隔离。四位 Key 验证成功后签发随机令牌,默认 30 分钟过期;只允许调用录入接口,不能访问管理 API。用户更换/停用 Key 或账号停用后,已有授权失效。
### entry_batches
录入事务批次表。每次成功保存产生一个 `ENT...` 编号并记录创建人、记录数、状态和时间。保留该层用于来源追踪、事务分组及未来的批量手动录入扩展。
### material_records
实验主表。关键字段映射如下:
| API 字段 | 数据库字段 | 类型 | 约束 |
|---|---|---|---|
| `experiment_id` | experiment_id | varchar(100) | 全局唯一约束 + 接口校验 |
| `test_date` | test_date | date | 必填 |
| `material_category` | material_category | varchar(100) | 必填、索引 |
| `material_name` | material_name | varchar(200) | 必填、索引 |
| `apparent_density` | apparent_density | double | 可选、≥0 |
| `uv_absorption_355nm` | uv_absorption_355nm | double | 可选、0100 |
| `material_thickness` | material_thickness | double | 必填、>0 |
| `moisture_content` | moisture_content | double | 可选、0100 |
| `thermal_conductivity` | thermal_conductivity | double | 可选、≥0 |
| `initial_decomposition_temp` | initial_decomposition_temp | double | 可选 |
| `melting_vaporization_temp` | melting_vaporization_temp | double | 可选 |
| `specific_heat_capacity` | specific_heat_capacity | double | 可选、≥0 |
| `carbon_residue_rate` | carbon_residue_rate | double | 可选、0100 |
| `surface_roughness` | surface_roughness | double | 可选、≥0 |
| `hardness` | hardness | double | 可选、≥0 |
| `actual_output_power` | actual_output_power | double | 必填、≥0 |
| `display_current` | display_current | double | 必填、≥0 |
| `scanning_speed` | scanning_speed | double | 必填、>0 |
| `pulse_frequency` | pulse_frequency | double | 必填、≥0 |
| `pulse_width` | pulse_width | double | 必填、≥0 |
| `defocus_amount` | defocus_amount | double | 必填,可为负 |
| `scan_line_spacing` | scan_line_spacing | double | 必填、>0 |
| `filling_method` | filling_method | varchar(100) | 必填 |
| `processing_size` | processing_size | varchar(100) | 必填 |
| `is_cut_through` | is_cut_through | boolean | 必填 |
| `carbonized_edge_width` | carbonized_edge_width | double | 必填、≥0 |
| `etching_depth` | etching_depth | double | 必填、≥0 |
| `is_fire_smolder` | is_fire_smolder | boolean | 必填 |
| `pattern_clarity_score` | pattern_clarity_score | double | 必填、010 |
| `presentation_balance_score` | presentation_balance_score | double | 必填、010 |
| `finished_image_filename` | finished_image_filename | varchar(255) | 必填 |
| `remarks` | remarks | text | 必填、最多 5000 字符(接口) |
组合索引覆盖材料分类+测试日期,实验 ID、日期、材料名称、批次外键均建索引。
### record_images
实验成品图表。图片二进制直接保存在 `data` 列(PostgreSQL 为 `bytea`SQLite 为 `BLOB`),因此现有数据库备份天然包含图片,恢复后不会出现"数据在、图片丢"的情况,部署也无需额外挂载卷。
| 字段 | 类型 | 说明 |
|---|---|---|
| `record_id` | int,可空 | 为空表示"已上传但尚未提交"的临时图片,超过 2 小时由下一次上传顺手清理 |
| `token` | varchar(64) | 上传后返回给前端的一次性引用,提交记录时凭它完成挂载;挂载后即失效 |
| `content_type` | varchar(40) | 服务端按文件头判定,只允许 `image/jpeg``image/png`(WebP 的 RIFF 头几乎不校验内容,不予接收) |
| `original_name` | varchar(255) | 原始文件名,仅作展示,已去除路径部分 |
| `size_bytes` / `width` / `height` | int | 均由服务端解析得出,不信任客户端上报 |
| `sha256` | varchar(64) | 内容指纹,用于 ETag 和图片 URL 的版本参数 |
| `data` / `thumbnail` | 二进制 | 主图与缩略图;缩略图由客户端生成,缺失时接口回落到主图 |
| `sort_order` | int | 按提交时给出的顺序排列 |
单条记录最多 6 张图,单张上传上限 8 MB(前端会先压缩到最长边 1600px 的 JPEG,通常 200–400 KB)。删除实验记录会级联删除其图片。
### custom_field_definitions
动态字段定义表。`field_key` 是创建后永不改变的内部标识;`label` 可重命名;`input_type``text``long_text``number``integer``date``boolean``select``is_required` 控制新录入和后续编辑校验,`is_active` 控制是否出现在表单,`is_public` 控制公开详情,`options_json` 保存下拉选项,`sort_order` 控制顺序。
### custom_field_values
扩展字段原始值表,`record_id + field_id` 唯一。`raw_value` 始终保存用户最后提交的原始文本,字段类型修改时不做数据库迁移或批量转换。读取接口根据当前字段定义生成 `value``display_value``compatible`;转换失败时 `compatible=false` 并返回原文。字段停用不会级联删除值,只有实验记录被管理员删除时才随记录删除。
### audit_logs / backup_records
`audit_logs` 保存动作、资源类型/ID、JSON 摘要、IP、操作者和时间。`backup_records` 保存备份文件元数据、数据库类型、字节数和 SHA-256,不把备份二进制放入数据库。
## 3. 认证与通用约定
登录成功后服务器设置 HttpOnly、SameSite=Lax Cookie `laser_session`。除 GET 外的浏览器操作需要请求头 `X-CSRF-Token`;页面已自动注入该值。未登录返回 401,无权限返回 403,冲突返回 409,字段校验返回 422。
成功响应:
```json
{"ok": true, "message": "实验数据已保存", "data": {}}
```
失败响应:
```json
{"ok": false, "message": "experiment_id:不能为空"}
```
分页接口返回 `pagination: {page, page_size, total}``page_size` 最大 100。
## 4. 认证接口
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| POST | `/api/auth/login` | 公开 | multipart 表单:`username``password` |
| POST | `/api/auth/logout` | 已登录 | 注销当前会话 |
| GET | `/api/me` | 已登录 | 当前用户 |
| PUT | `/api/me/password` | 已登录 | JSON`old_password``new_password` |
## 5. 实验数据接口
### POST `/api/records`
权限:`admin``uploader`。请求体为上表全部字段,可选字段可传 `null` 或省略。示例:
```json
{
"experiment_id": "LAS-2026-0001",
"test_date": "2026-07-17",
"material_category": "木材",
"material_name": "椴木板 A3",
"apparent_density": null,
"uv_absorption_355nm": null,
"material_thickness": 3,
"moisture_content": null,
"thermal_conductivity": null,
"initial_decomposition_temp": null,
"melting_vaporization_temp": null,
"specific_heat_capacity": null,
"carbon_residue_rate": null,
"surface_roughness": null,
"hardness": null,
"actual_output_power": 8.5,
"display_current": 2,
"scanning_speed": 100,
"pulse_frequency": 20,
"pulse_width": 5,
"defocus_amount": 0,
"scan_line_spacing": 0.1,
"filling_method": "双向填充",
"processing_size": "20×20",
"is_cut_through": true,
"carbonized_edge_width": 0.2,
"etching_depth": 30,
"is_fire_smolder": false,
"pattern_clarity_score": 9,
"presentation_balance_score": 8.5,
"finished_image_filename": "LAS-2026-0001.jpg",
"remarks": "加工稳定",
"custom_fields": {
"custom_a1b2c3d4e5f6": "12.5"
},
"image_tokens": ["QnR5c2hvdC10b2tlbg"],
"image_ids": []
}
```
`custom_fields` 的键来自 `GET /api/form-fields`。创建记录时所有启用的必填扩展字段必须提交。更新记录时,无法适配新类型的历史字段可以省略以保留原始值;提交 `null` 表示清空可选字段。
`image_tokens` 是先通过上传接口拿到的新图令牌;`image_ids` 是编辑时要保留的既有图片 ID。两者合计不能超过 6,数组顺序即展示顺序。**编辑记录时,未列入 `image_ids` 的既有图片会被删除**,所以 PUT 请求必须带上完整的保留列表;新建记录不接受 `image_ids`
### POST `/api/uploads/images`
权限:`admin``uploader`,需要 `X-CSRF-Token``multipart/form-data`,字段 `file`(必填)与 `thumbnail`(可选)。
服务端只按文件头判定类型,扩展名和客户端 `Content-Type` 一律不作数;SVG、GIF、HTML 等一律拒绝。单张上限 8 MB,超出的请求在中间件按 `Content-Length` 直接返回 413,不会落盘。同一账号未提交的图片累计 24 张时返回 429。
```json
{"ok": true, "message": "图片已上传", "data": {
"id": 12, "token": "…", "width": 1600, "height": 1067, "size_bytes": 305412,
"content_type": "image/jpeg", "original_name": "IMG_4821.jpg",
"url": "/api/images/12?v=3f9a1c2b7d4e", "thumb_url": "/api/images/12?variant=thumb&v=3f9a1c2b7d4e"}}
```
缩略图只是加速手段:即使它体积超限或格式非法也不会导致整次上传失败,服务端会丢弃并让 `thumb_url` 回落到原图。上传的字节在保存前会截断到图像结束标记(JPEG 的 `FFD9`、PNG 的 `IEND`)为止,防止在合法图片后面附带任意数据;`original_name` 也会做字符清洗后再入库。
对应的 Key 录入入口是 `POST /api/entry/uploads/images`,鉴权改用录入会话 Cookie 与录入 CSRF Token,其余行为一致。
### DELETE `/api/uploads/images/{token}`
撤回一张尚未挂到记录上的图片,及时释放该账号的待提交名额(用户在页面上删掉重拍时自动调用)。只能撤回本人上传且未挂载的图片,其余一律 404。Key 录入入口为 `DELETE /api/entry/uploads/images/{token}`
### GET `/api/images/{id}`
参数:`variant``full``thumb`)、`v`(内容指纹,仅用于绕开浏览器缓存)。已挂到记录上的图片公开可读;**尚未挂载的临时图片只有上传者本人能读,其他人一律 404**。
响应带 `X-Content-Type-Options: nosniff``Content-Security-Policy: default-src 'none'; sandbox``Content-Disposition: inline``ETag`,已挂载图片按 `immutable` 缓存一年。因为 SQLite 会复用被删除行的 rowidURL 必须带 `v` 参数,否则浏览器可能把旧缓存图误认成新图。
### GET `/api/records`
参数:`q`(实验 ID/材料名称模糊查询)、`category`(精确)、`page``page_size`。任何已登录角色可访问。
### GET / PUT / DELETE `/api/records/{id}`
GET 返回完整记录;PUT 请求体与 POST 相同,权限为管理员或录入员;DELETE 仅管理员可用,同时清理对应的单条手动录入批次并写审计日志。
## 6. 公开与 Key 录入接口
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | `/api/public/dashboard` | 公开 | 统计、分类、公开记录分页 |
| GET | `/api/public/records/{id}` | 公开 | 科学参数详情与成品图列表,不返回备注、图片文件名和用户 |
| GET | `/api/images/{id}` | 公开(临时图除外) | 图片二进制,`variant=thumb` 取缩略图 |
| POST | `/api/entry/uploads/images` | 录入会话 | multipart 上传成品图,返回挂载令牌 |
| DELETE | `/api/entry/uploads/images/{token}` | 录入会话 | 撤回尚未提交的图片 |
公开详情里的 `images` 只包含 `id``width``height``url``thumb_url` 五个字段;`original_name``size_bytes``content_type` 仅在登录后的 `GET /api/records/{id}` 返回。
| POST | `/api/entry/auth` | 公开、限速 | JSON `{key}`,成功设置 30 分钟 HttpOnly Cookie |
| GET | `/api/entry/session` | 公开 | 检查当前录入授权并获取 CSRF Token |
| POST | `/api/entry/records` | 录入会话 | 32 个数据字段加 `confirm_username` |
| POST | `/api/entry/logout` | 录入会话 | 撤销当前录入授权 |
| PUT | `/api/me/entry-key` | 管理登录 | 当前密码验证后设置/更换四位 Key |
| DELETE | `/api/me/entry-key` | 管理登录 | 当前密码验证后停用 Key |
| GET | `/api/form-fields` | 公开 | 当前启用的扩展字段定义,供录入表单渲染 |
`/api/entry/records` 会同时验证录入 Cookie、CSRF Token 和 `confirm_username`。用户名必须与 Key 所属账号的 `username` 精确一致。失败 Key 按来源 IP 限制为 15 分钟最多 5 次,失败与成功均写审计日志。
## 7. 管理接口
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | `/api/users` | admin | 用户列表 |
| GET | `/api/field-definitions` | admin | 全部动态字段及已有值数量 |
| POST | `/api/field-definitions` | admin | 新增动态字段 |
| PUT | `/api/field-definitions/{id}` | admin | 修改类型、规则、选项、排序或状态 |
| POST | `/api/users` | admin | 新建用户 |
| PATCH | `/api/users/{id}` | admin | 角色、状态、密码重置 |
| GET | `/api/audits` | admin | 审计日志分页 |
| GET | `/api/backups` | admin | 备份列表 |
| POST | `/api/backups` | admin | 立即创建备份 |
| GET | `/api/backups/{id}/download` | admin | 下载备份 |
| GET | `/health` | 公开 | 存活检查 |
完整请求/响应 Schema 以运行实例的 `/docs``/openapi.json` 为准。
## 8. 备份接口与保留策略
SQLite 使用官方在线 Backup APIPostgreSQL 使用 custom format `pg_dump`;生成后计算 SHA-256 并写 `backup_records`。超过 `BACKUP_KEEP_DAYS` 的本地备份在新备份完成后清理。恢复只通过停机命令行工具执行,不提供 Web 恢复接口,以降低误覆盖风险。
成品图存在数据库里,备份自然包含图片,代价是备份体积随图片增长:按每条记录平均 3 张、单张约 300 KB 估算,每 1000 条记录约增加 0.9 GB。`pg_dump --format=custom` 默认会压缩,但 JPEG 已经是压缩数据,几乎压不动。上线前请按预期记录量核算 `backups/` 所在磁盘容量,必要时调小 `BACKUP_KEEP_DAYS`