公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、 PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。 图片相关: - 录入表单支持相册选图与手机端直接拍照,每条记录最多 6 张 - 浏览器内压缩到最长边 1600 并剥离 EXIF,入库统一为 JPG/PNG - 二进制直接入库,现有备份自动覆盖图片,部署无需新增卷 - 详情页缩略图宫格与全屏 lightbox,公开看板同样可见 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
数据库与接口文档
版本:1.0 基础路径:/api
1. 技术与一致性
生产数据库为 PostgreSQL 17,开发环境可使用 SQLite。访问层使用 SQLAlchemy 2;所有手动录入以事务提交,校验失败时整次回滚。时间字段保存带时区时间,业务日期使用 DATE。外键在 PostgreSQL 和 SQLite 中均启用。
主要关系:
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-SHA256(310,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 | 可选、0–100 |
material_thickness |
material_thickness | double | 必填、>0 |
moisture_content |
moisture_content | double | 可选、0–100 |
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 | 可选、0–100 |
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 | 必填、0–10 |
presentation_balance_score |
presentation_balance_score | double | 必填、0–10 |
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。
成功响应:
{"ok": true, "message": "实验数据已保存", "data": {}}
失败响应:
{"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 或省略。示例:
{
"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。
{"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 会复用被删除行的 rowid,URL 必须带 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 API,PostgreSQL 使用 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。