feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation
Major changes: - New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind - 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理 - Data catalog with charts (DMS/ADAS/Lane 3-tab view) - Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance - Audit enhancements: batch operations, rejection categories, Feishu notifications - Operation audit log (操作日志) - World model simulation studio (仿真工坊) - Dataset version management with snapshots and diff - ADAS 7-class dataset integration (138K images organized + compressed) - User management with Feishu integration and pagination - CRUD/search/filter on all pages, card layout redesign - PIL-optimized image overlay rendering - Auto-snapshot on build, in_review workflow stage - Removed embedded algorithm code (now in workspace)
This commit is contained in:
@@ -17,23 +17,31 @@ from as_platform.config import (
|
||||
MANIFESTS,
|
||||
)
|
||||
from as_platform.db.engine import Base, engine, session_scope
|
||||
from as_platform.db.models import Approval, Job, Permission, Role, User
|
||||
from as_platform.db.models import Approval, FleetVehicle, Job, OperationLog, Permission, Role, User
|
||||
|
||||
# 角色 → 权限
|
||||
ROLE_DEFS: dict[str, tuple[str, list[str]]] = {
|
||||
"admin": ("管理员", ["*"]),
|
||||
"reviewer": ("审核员", [
|
||||
"read:catalog", "read:pending", "read:jobs", "read:audit",
|
||||
"write:approval_review",
|
||||
"read:catalog", "read:pending", "read:jobs", "read:audit", "read:fleet", "read:deliveries",
|
||||
"write:approval_review", "write:approval_submit",
|
||||
]),
|
||||
"engineer": ("算法工程师", [
|
||||
"read:catalog", "read:pending", "read:jobs", "read:audit",
|
||||
"write:approval_submit",
|
||||
"read:catalog", "read:pending", "read:jobs", "read:audit", "read:fleet", "write:fleet",
|
||||
"write:approval_submit", "write:delivery_submit", "read:deliveries",
|
||||
"write:labeling_vendor", "write:labeling_assign",
|
||||
]),
|
||||
"labeler": ("标注协调", [
|
||||
"read:catalog", "read:pending", "write:approval_submit:register",
|
||||
"read:catalog", "read:pending", "read:fleet", "write:approval_submit:register",
|
||||
"write:delivery_submit", "read:deliveries", "write:labeling_assign",
|
||||
]),
|
||||
"viewer": ("只读访客", ["read:catalog", "read:pending"]),
|
||||
"internal_labeler": ("内部标注员", [
|
||||
"read:catalog", "read:pending", "read:jobs",
|
||||
]),
|
||||
"vendor_labeler": ("第三方标注", [
|
||||
"read:catalog", "read:pending", "read:jobs", "write:labeling_vendor",
|
||||
]),
|
||||
"viewer": ("只读访客", ["read:catalog", "read:pending", "read:fleet"]),
|
||||
}
|
||||
|
||||
PERMISSION_NAMES: dict[str, str] = {
|
||||
@@ -42,19 +50,37 @@ PERMISSION_NAMES: dict[str, str] = {
|
||||
"read:pending": "查看送标/批次",
|
||||
"read:jobs": "查看 Job 队列",
|
||||
"read:audit": "查看审核记录",
|
||||
"read:fleet": "查看车队地图",
|
||||
"write:fleet": "管理车队与轨迹",
|
||||
"write:approval_submit": "提交审核(训练/build 等)",
|
||||
"write:approval_submit:register": "提交批次登记审核",
|
||||
"write:approval_review": "批准/驳回审核",
|
||||
"admin:users": "用户与角色管理",
|
||||
"write:labeling_vendor": "导入第三方标注回传包",
|
||||
"write:labeling_assign": "分配标注子任务",
|
||||
"write:delivery_submit": "提交数据送标申请",
|
||||
"read:deliveries": "查看批次送标台账",
|
||||
}
|
||||
|
||||
|
||||
def init_database() -> None:
|
||||
MANIFESTS.mkdir(parents=True, exist_ok=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
# 标注质检表(audit/review.py,不通过 Base 自动发现)
|
||||
try:
|
||||
from as_platform.audit.review import LabelingReview
|
||||
LabelingReview.__table__.create(bind=engine, checkfirst=True)
|
||||
except Exception:
|
||||
pass
|
||||
with session_scope() as db:
|
||||
_ensure_user_columns(db)
|
||||
_ensure_dataset_candidate_columns(db)
|
||||
_ensure_labeling_campaign_columns(db)
|
||||
_ensure_feishu_bitable_columns(db)
|
||||
_ensure_approval_columns(db)
|
||||
_ensure_operation_log_columns(db)
|
||||
_seed_roles_permissions(db)
|
||||
_seed_fleet_demo(db)
|
||||
_import_jsonl_if_empty(db)
|
||||
_sync_postgres_sequences(db)
|
||||
|
||||
@@ -78,6 +104,20 @@ def _seed_roles_permissions(db: Session) -> None:
|
||||
role.permissions = [perm_map[c] for c in perm_codes if c in perm_map]
|
||||
|
||||
|
||||
|
||||
|
||||
def _seed_fleet_demo(db: Session) -> None:
|
||||
if db.query(FleetVehicle).count() > 0:
|
||||
return
|
||||
try:
|
||||
from as_platform.config import FLEET_MOCK_SEED
|
||||
if not FLEET_MOCK_SEED:
|
||||
return
|
||||
from as_platform.fleet.mock_seed import seed_demo_fleet
|
||||
seed_demo_fleet(db)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _import_jsonl_if_empty(db: Session) -> None:
|
||||
if db.query(Approval).count() == 0 and APPROVAL_QUEUE.is_file():
|
||||
for line in APPROVAL_QUEUE.read_text(encoding="utf-8").strip().splitlines():
|
||||
@@ -153,6 +193,8 @@ def user_has_permission(user: User, permission: str) -> bool:
|
||||
# register_batch 专用权限
|
||||
if permission == "write:approval_submit:register" and p.code == "write:approval_submit":
|
||||
return True
|
||||
if permission == "write:delivery_submit" and p.code == "write:approval_submit:register":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -222,6 +264,42 @@ def _sync_postgres_sequences(db: Session) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _ensure_dataset_candidate_columns(db: Session) -> None:
|
||||
columns = {
|
||||
"mode": "VARCHAR(64)",
|
||||
"inbox_path": "VARCHAR(1024)",
|
||||
"promoted_batch": "VARCHAR(128)",
|
||||
"external_id": "VARCHAR(128)",
|
||||
"feishu_record_id": "VARCHAR(64)",
|
||||
}
|
||||
_ensure_table_columns(db, "dataset_candidates", columns)
|
||||
|
||||
|
||||
def _ensure_feishu_bitable_columns(db: Session) -> None:
|
||||
_ensure_dataset_candidate_columns(db)
|
||||
|
||||
|
||||
def _ensure_labeling_campaign_columns(db: Session) -> None:
|
||||
columns = {
|
||||
"assigned_to_user_id": "INTEGER",
|
||||
"assigned_to_name": "VARCHAR(128)",
|
||||
}
|
||||
_ensure_table_columns(db, "labeling_campaigns", columns)
|
||||
|
||||
|
||||
def _ensure_table_columns(db: Session, table: str, columns: dict[str, str]) -> None:
|
||||
if IS_POSTGRES:
|
||||
for column, column_type in columns.items():
|
||||
db.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {column} {column_type}"))
|
||||
return
|
||||
if IS_SQLITE:
|
||||
rows = db.execute(text(f"PRAGMA table_info({table})")).fetchall()
|
||||
existing = {str(row[1]) for row in rows}
|
||||
for column, column_type in columns.items():
|
||||
if column not in existing:
|
||||
db.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}"))
|
||||
|
||||
|
||||
def _ensure_user_columns(db: Session) -> None:
|
||||
user_columns = {
|
||||
"feishu_user_id": "VARCHAR(64)",
|
||||
@@ -240,3 +318,20 @@ def _ensure_user_columns(db: Session) -> None:
|
||||
for column, column_type in user_columns.items():
|
||||
if column not in existing:
|
||||
db.execute(text(f"ALTER TABLE users ADD COLUMN {column} {column_type}"))
|
||||
db.commit()
|
||||
|
||||
|
||||
def _ensure_approval_columns(db: Session) -> None:
|
||||
_ensure_table_columns(db, "approvals", {"rejection_category": "VARCHAR(32) DEFAULT ''"})
|
||||
|
||||
|
||||
def _ensure_operation_log_columns(db: Session) -> None:
|
||||
"""确保 operation_logs 表存在并包含所有列。"""
|
||||
inspector_args = {}
|
||||
bind = db.get_bind()
|
||||
if hasattr(bind, "url") and "postgresql" in str(getattr(bind, "url", "")):
|
||||
inspector_args["schema"] = "public"
|
||||
try:
|
||||
OperationLog.__table__.create(bind=db.get_bind(), checkfirst=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -8,11 +8,13 @@ from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Table,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
@@ -104,6 +106,7 @@ class Approval(Base):
|
||||
reviewed_by_name = Column(String(128), nullable=True)
|
||||
reviewed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
review_comment = Column(Text, nullable=True)
|
||||
rejection_category = Column(String(32), nullable=True, default="")
|
||||
job_id = Column(String(64), nullable=True)
|
||||
executed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
result_json = Column(Text, nullable=True)
|
||||
@@ -137,6 +140,7 @@ class Approval(Base):
|
||||
"reviewed_by_user_id": self.reviewed_by_user_id,
|
||||
"reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None,
|
||||
"review_comment": self.review_comment,
|
||||
"rejection_category": self.rejection_category or "",
|
||||
"job_id": self.job_id,
|
||||
"executed_at": self.executed_at.isoformat() if self.executed_at else None,
|
||||
"result": self.result(),
|
||||
@@ -190,6 +194,7 @@ class DatasetCandidate(Base):
|
||||
id = Column(String(64), primary_key=True)
|
||||
project = Column(String(32), nullable=False, index=True)
|
||||
task = Column(String(64), nullable=True, index=True)
|
||||
mode = Column(String(64), nullable=True)
|
||||
status = Column(String(32), nullable=False, default="uploaded", index=True)
|
||||
source_type = Column(String(32), nullable=False, default="upload")
|
||||
original_name = Column(String(255), nullable=True)
|
||||
@@ -203,6 +208,10 @@ class DatasetCandidate(Base):
|
||||
submitted_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
submitted_by_name = Column(String(128), nullable=True)
|
||||
analysis_job_id = Column(String(64), ForeignKey("jobs.id"), nullable=True)
|
||||
inbox_path = Column(String(1024), nullable=True)
|
||||
promoted_batch = Column(String(128), nullable=True)
|
||||
external_id = Column(String(128), nullable=True, index=True)
|
||||
feishu_record_id = Column(String(64), nullable=True, index=True)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
@@ -225,6 +234,7 @@ class DatasetCandidate(Base):
|
||||
"id": self.id,
|
||||
"project": self.project,
|
||||
"task": self.task,
|
||||
"mode": self.mode,
|
||||
"status": self.status,
|
||||
"source_type": self.source_type,
|
||||
"original_name": self.original_name,
|
||||
@@ -238,6 +248,369 @@ class DatasetCandidate(Base):
|
||||
"submitted_by_user_id": self.submitted_by_user_id,
|
||||
"submitted_by_name": self.submitted_by_name,
|
||||
"analysis_job_id": self.analysis_job_id,
|
||||
"inbox_path": self.inbox_path,
|
||||
"promoted_batch": self.promoted_batch,
|
||||
"external_id": self.external_id,
|
||||
"feishu_record_id": self.feishu_record_id,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class BatchDelivery(Base):
|
||||
"""平台内数据送标申请(替代飞书多维表格拉取)。"""
|
||||
|
||||
__tablename__ = "batch_deliveries"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("project", "task", "mode", "batch_name", name="uq_batch_deliveries_batch"),
|
||||
)
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
project = Column(String(32), nullable=False, index=True)
|
||||
task = Column(String(64), nullable=True, index=True)
|
||||
mode = Column(String(64), nullable=True)
|
||||
batch_name = Column(String(128), nullable=False, index=True)
|
||||
source_type = Column(String(64), nullable=True)
|
||||
vehicle_scene = Column(String(256), nullable=True)
|
||||
collection_start = Column(String(32), nullable=True)
|
||||
collection_end = Column(String(32), nullable=True)
|
||||
data_path = Column(String(1024), nullable=False)
|
||||
estimated_count = Column(Integer, nullable=True)
|
||||
remark = Column(Text, nullable=True)
|
||||
status = Column(String(32), nullable=False, default="draft", index=True)
|
||||
owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
owner_name = Column(String(128), nullable=True)
|
||||
submitted_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
submitted_by_name = Column(String(128), nullable=True)
|
||||
approval_id = Column(String(64), nullable=True, index=True)
|
||||
candidate_id = Column(String(64), nullable=True, index=True)
|
||||
inbox_path = Column(String(1024), nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"project": self.project,
|
||||
"task": self.task,
|
||||
"mode": self.mode,
|
||||
"batch_name": self.batch_name,
|
||||
"source_type": self.source_type,
|
||||
"vehicle_scene": self.vehicle_scene,
|
||||
"collection_start": self.collection_start,
|
||||
"collection_end": self.collection_end,
|
||||
"data_path": self.data_path,
|
||||
"estimated_count": self.estimated_count,
|
||||
"remark": self.remark,
|
||||
"status": self.status,
|
||||
"owner_user_id": self.owner_user_id,
|
||||
"owner_name": self.owner_name,
|
||||
"submitted_by_user_id": self.submitted_by_user_id,
|
||||
"submitted_by_name": self.submitted_by_name,
|
||||
"submitted_by": self.submitted_by_name,
|
||||
"approval_id": self.approval_id,
|
||||
"candidate_id": self.candidate_id,
|
||||
"inbox_path": self.inbox_path,
|
||||
"error_message": self.error_message,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class FeishuBitableLink(Base):
|
||||
"""HSAP 批次与飞书多维表格行的对应关系。"""
|
||||
|
||||
__tablename__ = "feishu_bitable_links"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
batch_key = Column(String(256), unique=True, nullable=False, index=True)
|
||||
record_id = Column(String(64), unique=True, nullable=True, index=True)
|
||||
delivery_id = Column(String(128), nullable=True, index=True)
|
||||
project = Column(String(32), nullable=False)
|
||||
task = Column(String(64), nullable=True)
|
||||
mode = Column(String(64), nullable=True)
|
||||
batch = Column(String(128), nullable=False)
|
||||
campaign_id = Column(String(64), nullable=True)
|
||||
inbox_path = Column(String(1024), nullable=True)
|
||||
last_sync_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class FleetVehicle(Base):
|
||||
__tablename__ = "fleet_vehicles"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
plate_no = Column(String(32), nullable=False, index=True)
|
||||
tbox_device_id = Column(String(64), unique=True, nullable=False, index=True)
|
||||
name = Column(String(128), nullable=False, default="")
|
||||
team = Column(String(64), nullable=True)
|
||||
status = Column(String(32), nullable=False, default="active")
|
||||
last_lat = Column(Float, nullable=True)
|
||||
last_lng = Column(Float, nullable=True)
|
||||
last_speed_kmh = Column(Float, nullable=True)
|
||||
last_ts = Column(DateTime(timezone=True), nullable=True)
|
||||
online = Column(Boolean, nullable=False, default=False)
|
||||
meta_json = Column(Text, nullable=False, default="{}")
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
def meta(self) -> dict:
|
||||
return json.loads(self.meta_json or "{}")
|
||||
|
||||
def set_meta(self, data: dict) -> None:
|
||||
self.meta_json = json.dumps(data, ensure_ascii=False)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"plate_no": self.plate_no,
|
||||
"tbox_device_id": self.tbox_device_id,
|
||||
"name": self.name or self.plate_no,
|
||||
"team": self.team,
|
||||
"status": self.status,
|
||||
"last_lat": self.last_lat,
|
||||
"last_lng": self.last_lng,
|
||||
"last_speed_kmh": self.last_speed_kmh,
|
||||
"last_ts": self.last_ts.isoformat() if self.last_ts else None,
|
||||
"online": self.online,
|
||||
"meta": self.meta(),
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class FleetCollectionRun(Base):
|
||||
__tablename__ = "fleet_collection_runs"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
vehicle_id = Column(Integer, ForeignKey("fleet_vehicles.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
run_no = Column(String(64), nullable=False, index=True)
|
||||
engineer = Column(String(128), nullable=True)
|
||||
project = Column(String(32), nullable=True)
|
||||
batch = Column(String(128), nullable=True)
|
||||
started_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
ended_at = Column(DateTime(timezone=True), nullable=True)
|
||||
status = Column(String(32), nullable=False, default="active", index=True)
|
||||
mileage_km = Column(Float, nullable=False, default=0.0)
|
||||
source = Column(String(32), nullable=False, default="tbox")
|
||||
note = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"vehicle_id": self.vehicle_id,
|
||||
"run_no": self.run_no,
|
||||
"engineer": self.engineer,
|
||||
"project": self.project,
|
||||
"batch": self.batch,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"ended_at": self.ended_at.isoformat() if self.ended_at else None,
|
||||
"status": self.status,
|
||||
"mileage_km": round(self.mileage_km or 0.0, 3),
|
||||
"source": self.source,
|
||||
"note": self.note,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class FleetTrackPoint(Base):
|
||||
__tablename__ = "fleet_track_points"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
run_id = Column(Integer, ForeignKey("fleet_collection_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
ts = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
lat = Column(Float, nullable=False)
|
||||
lng = Column(Float, nullable=False)
|
||||
speed_kmh = Column(Float, nullable=True)
|
||||
heading = Column(Float, nullable=True)
|
||||
alt_m = Column(Float, nullable=True)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"run_id": self.run_id,
|
||||
"ts": self.ts.isoformat() if self.ts else None,
|
||||
"lat": self.lat,
|
||||
"lng": self.lng,
|
||||
"speed_kmh": self.speed_kmh,
|
||||
"heading": self.heading,
|
||||
"alt_m": self.alt_m,
|
||||
}
|
||||
|
||||
|
||||
class FleetRunMilestone(Base):
|
||||
__tablename__ = "fleet_run_milestones"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
run_id = Column(Integer, ForeignKey("fleet_collection_runs.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
name = Column(String(128), nullable=False, default="")
|
||||
type = Column(String(32), nullable=False, default="waypoint")
|
||||
lat = Column(Float, nullable=False)
|
||||
lng = Column(Float, nullable=False)
|
||||
mileage_km = Column(Float, nullable=True)
|
||||
occurred_at = Column(DateTime(timezone=True), nullable=True)
|
||||
payload_json = Column(Text, nullable=False, default="{}")
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"run_id": self.run_id,
|
||||
"name": self.name,
|
||||
"type": self.type,
|
||||
"lat": self.lat,
|
||||
"lng": self.lng,
|
||||
"mileage_km": self.mileage_km,
|
||||
"occurred_at": self.occurred_at.isoformat() if self.occurred_at else None,
|
||||
"payload": json.loads(self.payload_json or "{}"),
|
||||
}
|
||||
|
||||
|
||||
class LabelingCampaignAccess(Base):
|
||||
"""Campaign 级访问授权(内部/第三方),非派单。"""
|
||||
|
||||
__tablename__ = "labeling_campaign_access"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
campaign_id = Column(String(64), nullable=False, index=True)
|
||||
principal_type = Column(String(32), nullable=False, default="role")
|
||||
principal_id = Column(String(128), nullable=False, index=True)
|
||||
access_role = Column(String(32), nullable=False, default="vendor")
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"campaign_id": self.campaign_id,
|
||||
"principal_type": self.principal_type,
|
||||
"principal_id": self.principal_id,
|
||||
"access_role": self.access_role,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class LabelingExportJob(Base):
|
||||
"""标注活动关联的 export / ml-predict 任务记录。"""
|
||||
|
||||
__tablename__ = "labeling_export_jobs"
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
campaign_id = Column(String(64), nullable=False, index=True)
|
||||
action = Column(String(64), nullable=False, index=True)
|
||||
job_id = Column(String(64), nullable=True, index=True)
|
||||
status = Column(String(32), nullable=False, default="queued", index=True)
|
||||
result_json = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"campaign_id": self.campaign_id,
|
||||
"action": self.action,
|
||||
"job_id": self.job_id,
|
||||
"status": self.status,
|
||||
"result": json.loads(self.result_json or "null") if self.result_json else None,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
||||
}
|
||||
|
||||
|
||||
class LabelingCampaign(Base):
|
||||
"""标注活动:与 pending 批次 (project, task, mode, batch, location) 一一对应。"""
|
||||
|
||||
__tablename__ = "labeling_campaigns"
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
project = Column(String(32), nullable=False, index=True)
|
||||
task = Column(String(64), nullable=False, index=True)
|
||||
mode = Column(String(64), nullable=True, index=True)
|
||||
batch = Column(String(128), nullable=False, index=True)
|
||||
pack = Column(String(64), nullable=True)
|
||||
location = Column(String(32), nullable=False, default="inbox")
|
||||
status = Column(String(32), nullable=False, default="not_opened", index=True)
|
||||
assigned_to_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
assigned_to_name = Column(String(128), nullable=True)
|
||||
config_xml = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"project": self.project,
|
||||
"task": self.task,
|
||||
"mode": self.mode,
|
||||
"batch": self.batch,
|
||||
"pack": self.pack,
|
||||
"location": self.location,
|
||||
"status": self.status,
|
||||
"assigned_to_user_id": self.assigned_to_user_id,
|
||||
"assigned_to_name": self.assigned_to_name,
|
||||
"config_xml": self.config_xml,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
class LabelingTaskAssignment(Base):
|
||||
"""Campaign 内单张图(task_id)分包给标注员。"""
|
||||
|
||||
__tablename__ = "labeling_task_assignments"
|
||||
__table_args__ = (UniqueConstraint("campaign_id", "task_id", name="uq_labeling_campaign_task"),)
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
campaign_id = Column(String(64), ForeignKey("labeling_campaigns.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
task_id = Column(String(32), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
assigned_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
assigned_at = Column(DateTime(timezone=True), default=_utcnow)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"campaign_id": self.campaign_id,
|
||||
"task_id": self.task_id,
|
||||
"user_id": self.user_id,
|
||||
"assigned_by_user_id": self.assigned_by_user_id,
|
||||
"assigned_at": self.assigned_at.isoformat() if self.assigned_at else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
||||
"completed_by_user_id": self.completed_by_user_id,
|
||||
}
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
__tablename__ = "operation_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
timestamp = Column(DateTime(timezone=True), default=_utcnow, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
user_name = Column(String(128), nullable=True)
|
||||
category = Column(String(32), nullable=False, index=True)
|
||||
action = Column(String(64), nullable=False)
|
||||
target_type = Column(String(32), nullable=True)
|
||||
target_id = Column(String(128), nullable=True)
|
||||
summary = Column(String(512), nullable=True)
|
||||
detail_json = Column(Text, nullable=True)
|
||||
ip_address = Column(String(64), nullable=True)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
|
||||
"user_id": self.user_id,
|
||||
"user_name": self.user_name,
|
||||
"category": self.category,
|
||||
"action": self.action,
|
||||
"target_type": self.target_type,
|
||||
"target_id": self.target_id,
|
||||
"summary": self.summary,
|
||||
"detail": json.loads(self.detail_json) if self.detail_json else None,
|
||||
"ip_address": self.ip_address,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user