Files
laser-data-hub/docs/数据库接口文档.md
T
broccoliandClaude Fable 5 edfb216561 激光材料数据平台 1.3.0
公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、
PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 22:06:02 +08:00

269 lines
15 KiB
Markdown
Raw 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.
# 数据库与接口文档
版本: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`