激光材料数据平台 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
+72
View File
@@ -0,0 +1,72 @@
"""接口测试用的独立环境:必须在导入 app 之前写好环境变量,配置是冻结的 dataclass。"""
from __future__ import annotations
import os
import struct
import tempfile
import zlib
from pathlib import Path
import pytest
os.environ.setdefault("APP_SECRET", "test-secret-0123456789abcdefghijkl")
os.environ.setdefault("ADMIN_USERNAME", "admin")
os.environ.setdefault("ADMIN_PASSWORD", "TestAdmin12345")
os.environ["DATABASE_URL"] = f"sqlite:///{Path(tempfile.mkdtemp(prefix='laser-test-')) / 'test.db'}"
def png_bytes(width: int = 24, height: int = 16) -> bytes:
"""不依赖 Pillow 手工拼一张真实可解码的 PNG。"""
def chunk(tag: bytes, payload: bytes) -> bytes:
return struct.pack(">I", len(payload)) + tag + payload + struct.pack(">I", zlib.crc32(tag + payload))
raw = b"".join(b"\x00" + b"\x20\x60\x90" * width for _ in range(height))
header = chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
return b"\x89PNG\r\n\x1a\n" + header + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")
def jpeg_bytes(width: int = 32, height: int = 20) -> bytes:
"""只含 SOI/APP0/SOF0/EOI 的最小 JPEG,足以走完服务端的文件头校验。"""
app0 = b"\xff\xe0" + struct.pack(">H", 16) + b"JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00"
sof0 = b"\xff\xc0" + struct.pack(">HBHHB", 11, 8, height, width, 1) + b"\x01\x11\x00"
return b"\xff\xd8" + app0 + sof0 + b"\xff\xd9"
def record_payload(experiment_id: str = "LAS-TEST-001", **overrides) -> dict:
payload = {
"experiment_id": experiment_id, "test_date": "2026-07-01", "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": 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,
"finished_image_filename": "LAS-TEST-001.jpg", "remarks": "测试记录",
}
payload.update(overrides)
return payload
@pytest.fixture(scope="session")
def client():
from fastapi.testclient import TestClient
from app.main import app
with TestClient(app) as test_client:
yield test_client
@pytest.fixture()
def admin(client):
"""返回带管理员会话的客户端和对应 CSRF 令牌。"""
import hashlib
import hmac
from app.config import settings
from app.main import SESSION_COOKIE
client.cookies.clear()
response = client.post("/api/auth/login", data={"username": settings.admin_username, "password": settings.admin_password})
assert response.status_code == 200, response.text
token = client.cookies.get(SESSION_COOKIE)
csrf = hmac.new(settings.secret.encode(), token.encode(), hashlib.sha256).hexdigest()
return client, {"X-CSRF-Token": csrf}