激光材料数据平台 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
+45
View File
@@ -0,0 +1,45 @@
from datetime import date
import pytest
from pydantic import ValidationError
from app.schemas import EntryRecordSubmission, MaterialRecordInput
def valid_payload():
return {
"experiment_id": "LAS-2026-001", "test_date": date(2026, 7, 17), "material_category": "木材", "material_name": "椴木板",
"material_thickness": 3, "actual_output_power": 8.5, "display_current": 2, "scanning_speed": 100,
"pulse_frequency": 20, "pulse_width": 5, "defocus_amount": 0, "scan_line_spacing": .1,
"filling_method": "双向", "processing_size": "20×20", "is_cut_through": True,
"carbonized_edge_width": .2, "etching_depth": 30, "is_fire_smolder": False,
"pattern_clarity_score": 9, "presentation_balance_score": 8, "finished_image_filename": "LAS-2026-001.jpg", "remarks": "正常",
}
def test_all_optional_physical_properties_may_be_empty():
record = MaterialRecordInput(**valid_payload())
assert record.apparent_density is None
assert record.hardness is None
assert len(record.model_fields) - 3 == 32 # 32 fixed fields plus the dynamic-field map and the two image lists
def test_scores_are_limited_to_ten():
payload = valid_payload()
payload["pattern_clarity_score"] = 10.1
with pytest.raises(ValidationError):
MaterialRecordInput(**payload)
def test_percentage_is_limited_to_hundred():
payload = valid_payload()
payload["moisture_content"] = 101
with pytest.raises(ValidationError):
MaterialRecordInput(**payload)
def test_entry_submission_requires_confirmation_username():
payload = valid_payload()
with pytest.raises(ValidationError):
EntryRecordSubmission(**payload)
assert EntryRecordSubmission(**payload, confirm_username="operator").confirm_username == "operator"