将 platform + CVAT 合并为单文件 docker-compose.yml,完善 .env 与 init/dev_up 脚本; 新增 docs/DEPLOY.md 与更新 README 以支持新机器部署;含数据湖示例、车队地图、 紧凑表格 UI、ADAS det_7cls 路径与批次台账等近期改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
685 lines
28 KiB
Python
685 lines
28 KiB
Python
"""ORM 模型。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime, timezone
|
||
|
||
from sqlalchemy import (
|
||
Boolean,
|
||
Column,
|
||
DateTime,
|
||
Float,
|
||
ForeignKey,
|
||
Integer,
|
||
JSON,
|
||
String,
|
||
Table,
|
||
Text,
|
||
UniqueConstraint,
|
||
)
|
||
from sqlalchemy.orm import relationship
|
||
|
||
from as_platform.db.engine import Base
|
||
|
||
user_roles = Table(
|
||
"user_roles",
|
||
Base.metadata,
|
||
Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
|
||
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
||
)
|
||
|
||
role_permissions = Table(
|
||
"role_permissions",
|
||
Base.metadata,
|
||
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
||
Column("permission_id", Integer, ForeignKey("permissions.id", ondelete="CASCADE"), primary_key=True),
|
||
)
|
||
|
||
|
||
def _utcnow() -> datetime:
|
||
return datetime.now(timezone.utc)
|
||
|
||
|
||
class User(Base):
|
||
__tablename__ = "users"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
feishu_open_id = Column(String(64), unique=True, nullable=True, index=True)
|
||
feishu_union_id = Column(String(64), unique=True, nullable=True, index=True)
|
||
feishu_user_id = Column(String(64), unique=True, nullable=True, index=True)
|
||
feishu_tenant_key = Column(String(128), nullable=True)
|
||
feishu_department_ids_json = Column(Text, nullable=True)
|
||
name = Column(String(128), nullable=False, default="")
|
||
email = Column(String(256), nullable=True)
|
||
avatar_url = Column(String(512), nullable=True)
|
||
is_active = Column(Boolean, default=True, nullable=False)
|
||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||
updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
|
||
|
||
roles = relationship("Role", secondary=user_roles, back_populates="users")
|
||
|
||
def feishu_department_ids(self) -> list[str]:
|
||
if not self.feishu_department_ids_json:
|
||
return []
|
||
try:
|
||
data = json.loads(self.feishu_department_ids_json)
|
||
if isinstance(data, list):
|
||
return [str(x) for x in data]
|
||
except Exception:
|
||
return []
|
||
return []
|
||
|
||
|
||
class Role(Base):
|
||
__tablename__ = "roles"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
code = Column(String(32), unique=True, nullable=False)
|
||
name = Column(String(64), nullable=False)
|
||
|
||
users = relationship("User", secondary=user_roles, back_populates="roles")
|
||
permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")
|
||
|
||
|
||
class Permission(Base):
|
||
__tablename__ = "permissions"
|
||
|
||
id = Column(Integer, primary_key=True)
|
||
code = Column(String(64), unique=True, nullable=False)
|
||
name = Column(String(128), nullable=False)
|
||
|
||
roles = relationship("Role", secondary=role_permissions, back_populates="permissions")
|
||
|
||
|
||
class Approval(Base):
|
||
__tablename__ = "approvals"
|
||
|
||
id = Column(String(64), primary_key=True)
|
||
status = Column(String(32), nullable=False, default="pending", index=True)
|
||
action = Column(String(64), nullable=False)
|
||
action_label = Column(String(128), nullable=True)
|
||
params_json = Column(Text, nullable=False, default="{}")
|
||
note = Column(Text, nullable=True)
|
||
submitted_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||
submitted_by_name = Column(String(128), nullable=True)
|
||
submitted_at = Column(DateTime(timezone=True), default=_utcnow)
|
||
reviewed_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||
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)
|
||
|
||
def params(self) -> dict:
|
||
return json.loads(self.params_json or "{}")
|
||
|
||
def set_params(self, data: dict) -> None:
|
||
self.params_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def result(self) -> dict | None:
|
||
if not self.result_json:
|
||
return None
|
||
return json.loads(self.result_json)
|
||
|
||
def set_result(self, data: dict) -> None:
|
||
self.result_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"id": self.id,
|
||
"status": self.status,
|
||
"action": self.action,
|
||
"action_label": self.action_label,
|
||
"params": self.params(),
|
||
"note": self.note,
|
||
"submitted_by": self.submitted_by_name,
|
||
"submitted_by_user_id": self.submitted_by_user_id,
|
||
"submitted_at": self.submitted_at.isoformat() if self.submitted_at else None,
|
||
"reviewed_by": self.reviewed_by_name,
|
||
"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(),
|
||
}
|
||
|
||
|
||
class Job(Base):
|
||
__tablename__ = "jobs"
|
||
|
||
id = Column(String(64), primary_key=True)
|
||
status = Column(String(32), nullable=False, default="queued", index=True)
|
||
action = Column(String(64), nullable=False)
|
||
params_json = Column(Text, nullable=False, default="{}")
|
||
approval_id = Column(String(64), ForeignKey("approvals.id"), nullable=True)
|
||
created_at = Column(DateTime(timezone=True), default=_utcnow)
|
||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||
result_json = Column(Text, nullable=True)
|
||
|
||
def params(self) -> dict:
|
||
return json.loads(self.params_json or "{}")
|
||
|
||
def set_params(self, data: dict) -> None:
|
||
self.params_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def result(self) -> dict | None:
|
||
if not self.result_json:
|
||
return None
|
||
return json.loads(self.result_json)
|
||
|
||
def set_result(self, data: dict) -> None:
|
||
self.result_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"id": self.id,
|
||
"status": self.status,
|
||
"action": self.action,
|
||
"params": self.params(),
|
||
"approval_id": self.approval_id,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
||
"result": self.result(),
|
||
}
|
||
|
||
|
||
class DatasetCandidate(Base):
|
||
__tablename__ = "dataset_candidates"
|
||
|
||
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)
|
||
upload_path = Column(String(1024), nullable=False)
|
||
analyzed_source_path = Column(String(1024), nullable=True)
|
||
format_id = Column(String(64), nullable=True)
|
||
split_counts_json = Column(Text, nullable=False, default="{}")
|
||
quality_json = Column(Text, nullable=True)
|
||
error_message = Column(Text, nullable=True)
|
||
upload_size_bytes = Column(Integer, nullable=False, default=0)
|
||
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)
|
||
|
||
def split_counts(self) -> dict:
|
||
return json.loads(self.split_counts_json or "{}")
|
||
|
||
def set_split_counts(self, data: dict) -> None:
|
||
self.split_counts_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def quality(self) -> dict | None:
|
||
if not self.quality_json:
|
||
return None
|
||
return json.loads(self.quality_json)
|
||
|
||
def set_quality(self, data: dict) -> None:
|
||
self.quality_json = json.dumps(data, ensure_ascii=False)
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"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,
|
||
"upload_path": self.upload_path,
|
||
"analyzed_source_path": self.analyzed_source_path,
|
||
"format_id": self.format_id,
|
||
"split_counts": self.split_counts(),
|
||
"quality": self.quality(),
|
||
"error_message": self.error_message,
|
||
"upload_size_bytes": self.upload_size_bytes,
|
||
"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 BatchIndex(Base):
|
||
"""批次列表索引:由扫盘/登记/阶段变更写入,列表 API 只读此表。"""
|
||
|
||
__tablename__ = "batch_index"
|
||
|
||
campaign_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 = Column(String(128), nullable=False, index=True)
|
||
pack = Column(String(64), nullable=True)
|
||
location = Column(String(32), nullable=False, default="inbox")
|
||
stage = Column(String(32), nullable=False, index=True)
|
||
batch_path = Column(String(1024), nullable=True)
|
||
scope_key = Column(String(128), nullable=True)
|
||
engineer = Column(String(128), nullable=True)
|
||
format = Column(String(32), nullable=True)
|
||
image_count = Column(Integer, nullable=False, default=0)
|
||
label_count = Column(Integer, nullable=False, default=0)
|
||
has_meta = Column(Boolean, nullable=False, default=False)
|
||
registry_only = Column(Boolean, nullable=False, default=False)
|
||
next_cli = Column(String(512), nullable=True)
|
||
domain = Column(String(32), nullable=True)
|
||
domain_label = Column(String(64), nullable=True)
|
||
task_label = Column(String(128), nullable=True)
|
||
mode_label = Column(String(128), nullable=True)
|
||
labeling_profile = Column(String(128), nullable=True)
|
||
export_default = Column(String(128), nullable=True)
|
||
ml_adapter = Column(String(128), nullable=True)
|
||
indexed_at = Column(DateTime(timezone=True), nullable=False)
|
||
archived = Column(Boolean, nullable=False, default=False, index=True)
|
||
|
||
def to_list_row(self) -> dict:
|
||
return {
|
||
"project": self.project,
|
||
"task": self.task,
|
||
"mode": self.mode,
|
||
"batch": self.batch,
|
||
"pack": self.pack,
|
||
"stage": self.stage,
|
||
"location": self.location,
|
||
"path": self.batch_path,
|
||
"engineer": self.engineer,
|
||
"format": self.format,
|
||
"counts": {"images": self.image_count, "labels": self.label_count},
|
||
"has_meta": self.has_meta,
|
||
"next_cli": self.next_cli,
|
||
"scope_key": self.scope_key,
|
||
"domain": self.domain,
|
||
"domain_label": self.domain_label,
|
||
"task_label": self.task_label,
|
||
"mode_label": self.mode_label,
|
||
"labeling_profile": self.labeling_profile,
|
||
"export_default": self.export_default,
|
||
"ml_adapter": self.ml_adapter,
|
||
"campaign_id": self.campaign_id,
|
||
"registry_only": self.registry_only,
|
||
"indexed_at": self.indexed_at.isoformat() if self.indexed_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)
|
||
cvat_task_id = Column(Integer, nullable=True, index=True)
|
||
cvat_job_url = Column(String(512), nullable=True)
|
||
annotation_types = Column(JSON, 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,
|
||
"cvat_task_id": self.cvat_task_id,
|
||
"cvat_job_url": self.cvat_job_url,
|
||
"annotation_types": self.annotation_types,
|
||
"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,
|
||
}
|
||
|
||
|