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

15 KiB
Raw Blame History

数据库与接口文档

版本: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-SHA256310,000 次)结果,不保存明文。roleadminuploadervieweris_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 为 byteaSQLite 为 BLOB),因此现有数据库备份天然包含图片,恢复后不会出现"数据在、图片丢"的情况,部署也无需额外挂载卷。

字段 类型 说明
record_id int,可空 为空表示"已上传但尚未提交"的临时图片,超过 2 小时由下一次上传顺手清理
token varchar(64) 上传后返回给前端的一次性引用,提交记录时凭它完成挂载;挂载后即失效
content_type varchar(40) 服务端按文件头判定,只允许 image/jpegimage/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_typetextlong_textnumberintegerdatebooleanselectis_required 控制新录入和后续编辑校验,is_active 控制是否出现在表单,is_public 控制公开详情,options_json 保存下拉选项,sort_order 控制顺序。

custom_field_values

扩展字段原始值表,record_id + field_id 唯一。raw_value 始终保存用户最后提交的原始文本,字段类型修改时不做数据库迁移或批量转换。读取接口根据当前字段定义生成 valuedisplay_valuecompatible;转换失败时 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 表单:usernamepassword
POST /api/auth/logout 已登录 注销当前会话
GET /api/me 已登录 当前用户
PUT /api/me/password 已登录 JSONold_passwordnew_password

5. 实验数据接口

POST /api/records

权限:adminuploader。请求体为上表全部字段,可选字段可传 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

权限:adminuploader,需要 X-CSRF-Tokenmultipart/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}

参数:variantfullthumb)、v(内容指纹,仅用于绕开浏览器缓存)。已挂到记录上的图片公开可读;尚未挂载的临时图片只有上传者本人能读,其他人一律 404

响应带 X-Content-Type-Options: nosniffContent-Security-Policy: default-src 'none'; sandboxContent-Disposition: inlineETag,已挂载图片按 immutable 缓存一年。因为 SQLite 会复用被删除行的 rowidURL 必须带 v 参数,否则浏览器可能把旧缓存图误认成新图。

GET /api/records

参数:q(实验 ID/材料名称模糊查询)、category(精确)、pagepage_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 只包含 idwidthheighturlthumb_url 五个字段;original_namesize_bytescontent_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