激光材料数据平台 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:
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import CustomFieldDefinition, CustomFieldValue, MaterialRecord
|
||||
|
||||
|
||||
def options_for(definition: CustomFieldDefinition) -> list[str]:
|
||||
try:
|
||||
return json.loads(definition.options_json) if definition.options_json else []
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
|
||||
|
||||
def definition_json(definition: CustomFieldDefinition, usage_count: int | None = None) -> dict[str, Any]:
|
||||
data = {
|
||||
"id": definition.id,
|
||||
"field_key": definition.field_key,
|
||||
"label": definition.label,
|
||||
"input_type": definition.input_type,
|
||||
"is_required": definition.is_required,
|
||||
"is_active": definition.is_active,
|
||||
"is_public": definition.is_public,
|
||||
"unit": definition.unit,
|
||||
"options": options_for(definition),
|
||||
"sort_order": definition.sort_order,
|
||||
"created_at": definition.created_at.isoformat(),
|
||||
"updated_at": definition.updated_at.isoformat(),
|
||||
}
|
||||
if usage_count is not None:
|
||||
data["usage_count"] = usage_count
|
||||
return data
|
||||
|
||||
|
||||
def active_definitions(db: Session) -> list[CustomFieldDefinition]:
|
||||
return list(db.scalars(select(CustomFieldDefinition).where(CustomFieldDefinition.is_active.is_(True)).order_by(CustomFieldDefinition.sort_order, CustomFieldDefinition.id)).all())
|
||||
|
||||
|
||||
def _empty(value: Any) -> bool:
|
||||
return value is None or (isinstance(value, str) and not value.strip())
|
||||
|
||||
|
||||
def validate_raw_value(definition: CustomFieldDefinition, value: Any) -> str | None:
|
||||
if _empty(value):
|
||||
if definition.is_required:
|
||||
raise HTTPException(422, f"{definition.label}:该字段为必填项")
|
||||
return None
|
||||
raw = str(value).strip()
|
||||
try:
|
||||
if definition.input_type == "number":
|
||||
if not Decimal(raw).is_finite():
|
||||
raise ValueError
|
||||
elif definition.input_type == "integer":
|
||||
number = Decimal(raw)
|
||||
if isinstance(value, bool) or not number.is_finite() or number != number.to_integral_value():
|
||||
raise ValueError
|
||||
elif definition.input_type == "date":
|
||||
date.fromisoformat(raw)
|
||||
elif definition.input_type == "boolean":
|
||||
if raw.lower() not in {"true", "false", "1", "0"}:
|
||||
raise ValueError
|
||||
raw = "true" if raw.lower() in {"true", "1"} else "false"
|
||||
elif definition.input_type == "select" and raw not in options_for(definition):
|
||||
raise ValueError
|
||||
except (ValueError, InvalidOperation):
|
||||
raise HTTPException(422, f"{definition.label}:数据格式不符合当前字段类型")
|
||||
if len(raw) > 10000:
|
||||
raise HTTPException(422, f"{definition.label}:内容不能超过 10000 个字符")
|
||||
return raw
|
||||
|
||||
|
||||
def validate_custom_values(db: Session, submitted: dict[str, Any], existing_record: MaterialRecord | None = None) -> dict[CustomFieldDefinition, str | None]:
|
||||
definitions = active_definitions(db)
|
||||
by_key = {item.field_key: item for item in definitions}
|
||||
unknown = set(submitted) - set(by_key)
|
||||
if unknown:
|
||||
raise HTTPException(422, "包含未知或已停用的自定义字段")
|
||||
existing_keys = {value.definition.field_key for value in existing_record.custom_values} if existing_record else set()
|
||||
validated: dict[CustomFieldDefinition, str | None] = {}
|
||||
for definition in definitions:
|
||||
if definition.field_key not in submitted and existing_record is not None:
|
||||
if definition.is_required and definition.field_key not in existing_keys:
|
||||
raise HTTPException(422, f"{definition.label}:该字段为必填项")
|
||||
continue
|
||||
validated[definition] = validate_raw_value(definition, submitted.get(definition.field_key))
|
||||
return validated
|
||||
|
||||
|
||||
def apply_custom_values(db: Session, record: MaterialRecord, values: dict[CustomFieldDefinition, str | None]) -> None:
|
||||
existing = {item.field_id: item for item in record.custom_values}
|
||||
for definition, raw_value in values.items():
|
||||
item = existing.get(definition.id)
|
||||
if raw_value is None:
|
||||
if item:
|
||||
db.delete(item)
|
||||
continue
|
||||
if item:
|
||||
item.raw_value = raw_value
|
||||
else:
|
||||
db.add(CustomFieldValue(record_id=record.id, field_id=definition.id, raw_value=raw_value))
|
||||
|
||||
|
||||
def render_custom_value(value: CustomFieldValue) -> dict[str, Any]:
|
||||
definition, raw = value.definition, value.raw_value or ""
|
||||
compatible = True
|
||||
processed: Any = raw
|
||||
try:
|
||||
if definition.input_type == "number":
|
||||
number = Decimal(raw)
|
||||
if not number.is_finite():
|
||||
raise ValueError
|
||||
processed = float(number)
|
||||
elif definition.input_type == "integer":
|
||||
number = Decimal(raw)
|
||||
if number != number.to_integral_value():
|
||||
raise ValueError
|
||||
processed = int(number)
|
||||
elif definition.input_type == "date":
|
||||
processed = date.fromisoformat(raw).isoformat()
|
||||
elif definition.input_type == "boolean":
|
||||
if raw.lower() not in {"true", "false", "1", "0"}:
|
||||
raise ValueError
|
||||
processed = raw.lower() in {"true", "1"}
|
||||
elif definition.input_type == "select" and raw not in options_for(definition):
|
||||
compatible = False
|
||||
except (ValueError, InvalidOperation):
|
||||
compatible = False
|
||||
processed = raw
|
||||
display = "是" if processed is True else "否" if processed is False else str(processed)
|
||||
if definition.unit and display:
|
||||
display = f"{display} {definition.unit}"
|
||||
return {
|
||||
"field_key": definition.field_key,
|
||||
"label": definition.label,
|
||||
"input_type": definition.input_type,
|
||||
"unit": definition.unit,
|
||||
"raw_value": raw,
|
||||
"value": processed,
|
||||
"display_value": display,
|
||||
"compatible": compatible,
|
||||
"is_active": definition.is_active,
|
||||
"is_public": definition.is_public,
|
||||
}
|
||||
|
||||
|
||||
def definition_usage_counts(db: Session) -> dict[int, int]:
|
||||
rows = db.execute(select(CustomFieldValue.field_id, func.count(CustomFieldValue.id)).group_by(CustomFieldValue.field_id)).all()
|
||||
return {field_id: count for field_id, count in rows}
|
||||
Reference in New Issue
Block a user