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)
315 lines
11 KiB
Python
315 lines
11 KiB
Python
"""批次送标申请 CRUD 与提交审批。"""
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
from as_platform.audit.queue import reject_approval, submit_approval
|
||
from as_platform.db.engine import session_scope
|
||
from as_platform.db.models import Approval, BatchDelivery, Job, User
|
||
from as_platform.integrations.delivery_ingest import validate_delivery_fields
|
||
|
||
EDITABLE_STATUSES = frozenset({"draft", "rejected", "ingest_failed"})
|
||
DELETABLE_STATUSES = frozenset({"draft", "rejected", "ingest_failed"})
|
||
RETRY_INGEST_STATUSES = frozenset({"ingest_failed", "ingesting"})
|
||
|
||
|
||
def _utcnow() -> datetime:
|
||
return datetime.now(timezone.utc)
|
||
|
||
|
||
def _new_delivery_id() -> str:
|
||
return f"del-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}"
|
||
|
||
|
||
def _normalize_task(project: str, task: str | None) -> str | None:
|
||
if project == "dms":
|
||
return (task or "").strip() or None
|
||
return None
|
||
|
||
|
||
def _enrich_delivery_dict(db, rec: BatchDelivery) -> dict[str, Any]:
|
||
d = rec.to_dict()
|
||
d["submitted_by"] = rec.submitted_by_name
|
||
if rec.approval_id:
|
||
ap = db.get(Approval, rec.approval_id)
|
||
if ap:
|
||
d["approval_status"] = ap.status
|
||
d["job_id"] = ap.job_id
|
||
if ap.job_id:
|
||
job = db.get(Job, ap.job_id)
|
||
if job:
|
||
d["job_status"] = job.status
|
||
return d
|
||
|
||
|
||
def list_deliveries(
|
||
*,
|
||
status: str | None = None,
|
||
mine_user_id: int | None = None,
|
||
mine_editable_only: bool = False,
|
||
offset: int = 0,
|
||
limit: int = 20,
|
||
) -> dict[str, Any]:
|
||
with session_scope() as db:
|
||
q = db.query(BatchDelivery).order_by(BatchDelivery.updated_at.desc())
|
||
if status:
|
||
q = q.filter(BatchDelivery.status == status)
|
||
if mine_user_id is not None:
|
||
q = q.filter(
|
||
(BatchDelivery.owner_user_id == mine_user_id)
|
||
| (BatchDelivery.submitted_by_user_id == mine_user_id)
|
||
)
|
||
if mine_editable_only:
|
||
q = q.filter(BatchDelivery.status.in_(("draft", "rejected", "ingest_failed")))
|
||
total = q.count()
|
||
rows = q.offset(max(0, offset)).limit(max(1, limit)).all()
|
||
return {
|
||
"items": [_enrich_delivery_dict(db, r) for r in rows],
|
||
"total": total,
|
||
"offset": offset,
|
||
"limit": limit,
|
||
"count": len(rows),
|
||
}
|
||
|
||
|
||
def get_delivery(delivery_id: str) -> dict[str, Any] | None:
|
||
with session_scope() as db:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
return _enrich_delivery_dict(db, rec) if rec else None
|
||
|
||
|
||
def create_delivery(data: dict[str, Any], user: User) -> dict[str, Any]:
|
||
project = (data.get("project") or "dms").strip()
|
||
task = _normalize_task(project, data.get("task"))
|
||
mode = (data.get("mode") or "").strip() or None
|
||
batch_name = (data.get("batch_name") or "").strip()
|
||
data_path = (data.get("data_path") or "").strip()
|
||
|
||
if not batch_name:
|
||
raise ValueError("批次名必填")
|
||
if data_path:
|
||
err = validate_delivery_fields(
|
||
project=project,
|
||
task=task,
|
||
mode=mode,
|
||
batch_name=batch_name,
|
||
data_path=data_path,
|
||
)
|
||
if err:
|
||
raise ValueError(err)
|
||
|
||
with session_scope() as db:
|
||
dup = (
|
||
db.query(BatchDelivery)
|
||
.filter_by(project=project, task=task, mode=mode, batch_name=batch_name)
|
||
.filter(BatchDelivery.status.notin_(("rejected", "ingest_failed")))
|
||
.first()
|
||
)
|
||
if dup:
|
||
raise ValueError(f"已存在同批次申请: {dup.id} ({dup.status})")
|
||
|
||
rec = BatchDelivery(
|
||
id=_new_delivery_id(),
|
||
project=project,
|
||
task=task,
|
||
mode=mode,
|
||
batch_name=batch_name,
|
||
source_type=(data.get("source_type") or "").strip() or None,
|
||
vehicle_scene=(data.get("vehicle_scene") or "").strip() or None,
|
||
collection_start=(data.get("collection_start") or "").strip() or None,
|
||
collection_end=(data.get("collection_end") or "").strip() or None,
|
||
data_path=data_path,
|
||
estimated_count=int(data["estimated_count"]) if data.get("estimated_count") not in (None, "") else None,
|
||
remark=(data.get("remark") or "").strip() or None,
|
||
status="draft",
|
||
owner_user_id=data.get("owner_user_id") or user.id,
|
||
owner_name=(data.get("owner_name") or user.name),
|
||
submitted_by_user_id=user.id,
|
||
submitted_by_name=user.name,
|
||
)
|
||
db.add(rec)
|
||
db.flush()
|
||
return rec.to_dict()
|
||
|
||
|
||
def update_delivery(delivery_id: str, data: dict[str, Any], user: User) -> dict[str, Any]:
|
||
with session_scope() as db:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
if not rec:
|
||
raise ValueError("送标申请不存在")
|
||
if rec.status not in EDITABLE_STATUSES:
|
||
raise ValueError(f"当前状态不可编辑: {rec.status}")
|
||
|
||
project = (data.get("project") or rec.project).strip()
|
||
task = _normalize_task(project, data.get("task") if "task" in data else rec.task)
|
||
mode = (data.get("mode") if "mode" in data else rec.mode) or None
|
||
if mode:
|
||
mode = mode.strip() or None
|
||
batch_name = (data.get("batch_name") or rec.batch_name).strip()
|
||
data_path = (data.get("data_path") or rec.data_path).strip()
|
||
|
||
err = validate_delivery_fields(
|
||
project=project,
|
||
task=task,
|
||
mode=mode,
|
||
batch_name=batch_name,
|
||
data_path=data_path,
|
||
)
|
||
if err:
|
||
raise ValueError(err)
|
||
|
||
rec.project = project
|
||
rec.task = task
|
||
rec.mode = mode
|
||
rec.batch_name = batch_name
|
||
rec.data_path = data_path
|
||
if "source_type" in data:
|
||
rec.source_type = (data.get("source_type") or "").strip() or None
|
||
if "vehicle_scene" in data:
|
||
rec.vehicle_scene = (data.get("vehicle_scene") or "").strip() or None
|
||
if "collection_start" in data:
|
||
rec.collection_start = (data.get("collection_start") or "").strip() or None
|
||
if "collection_end" in data:
|
||
rec.collection_end = (data.get("collection_end") or "").strip() or None
|
||
if "estimated_count" in data:
|
||
v = data.get("estimated_count")
|
||
rec.estimated_count = int(v) if v not in (None, "") else None
|
||
if "remark" in data:
|
||
rec.remark = (data.get("remark") or "").strip() or None
|
||
if "owner_user_id" in data:
|
||
rec.owner_user_id = data.get("owner_user_id") or rec.owner_user_id
|
||
if "owner_name" in data:
|
||
rec.owner_name = (data.get("owner_name") or "").strip() or rec.owner_name
|
||
if rec.status in ("rejected", "ingest_failed"):
|
||
rec.status = "draft"
|
||
rec.error_message = None
|
||
rec.updated_at = _utcnow()
|
||
db.flush()
|
||
return rec.to_dict()
|
||
|
||
|
||
def submit_delivery_for_review(delivery_id: str, user: User) -> dict[str, Any]:
|
||
with session_scope() as db:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
if not rec:
|
||
raise ValueError("送标申请不存在")
|
||
if rec.status not in EDITABLE_STATUSES:
|
||
raise ValueError(f"当前状态不可提交: {rec.status}")
|
||
|
||
err = validate_delivery_fields(
|
||
project=rec.project,
|
||
task=rec.task,
|
||
mode=rec.mode,
|
||
batch_name=rec.batch_name,
|
||
data_path=rec.data_path,
|
||
)
|
||
if err:
|
||
raise ValueError(err)
|
||
|
||
params = {
|
||
"delivery_id": rec.id,
|
||
"project": rec.project,
|
||
"task": rec.task,
|
||
"mode": rec.mode,
|
||
"batch_name": rec.batch_name,
|
||
"data_path": rec.data_path,
|
||
"source_type": rec.source_type,
|
||
"vehicle_scene": rec.vehicle_scene,
|
||
"estimated_count": rec.estimated_count,
|
||
"remark": rec.remark,
|
||
}
|
||
approval = submit_approval(
|
||
"delivery_ingest",
|
||
params,
|
||
submitted_by=user.name,
|
||
submitted_by_user_id=user.id,
|
||
note=f"数据送标入湖 {rec.batch_name}",
|
||
)
|
||
rec.status = "pending_review"
|
||
rec.approval_id = approval.get("id")
|
||
rec.submitted_by_user_id = user.id
|
||
rec.submitted_by_name = user.name
|
||
rec.updated_at = _utcnow()
|
||
db.flush()
|
||
out = rec.to_dict()
|
||
out["approval"] = approval
|
||
return out
|
||
|
||
|
||
def mark_delivery_rejected_by_approval(approval_id: str) -> None:
|
||
with session_scope() as db:
|
||
rec = db.query(BatchDelivery).filter_by(approval_id=approval_id).first()
|
||
if rec and rec.status == "pending_review":
|
||
rec.status = "rejected"
|
||
rec.updated_at = _utcnow()
|
||
db.flush()
|
||
|
||
|
||
def on_approval_rejected(approval_id: str, **_: Any) -> None:
|
||
mark_delivery_rejected_by_approval(approval_id)
|
||
|
||
|
||
def mark_delivery_ingest_failed(delivery_id: str | None, approval_id: str | None, error: str) -> None:
|
||
"""Job 失败或入湖异常时,确保台账状态为 ingest_failed(避免卡在 ingesting)。"""
|
||
msg = (error or "")[:2000]
|
||
with session_scope() as db:
|
||
rec = None
|
||
if delivery_id:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
if not rec and approval_id:
|
||
rec = db.query(BatchDelivery).filter_by(approval_id=approval_id).first()
|
||
if not rec:
|
||
return
|
||
if rec.status in ("ingesting", "pending_review"):
|
||
rec.status = "ingest_failed"
|
||
rec.error_message = msg
|
||
rec.updated_at = _utcnow()
|
||
db.flush()
|
||
|
||
|
||
def delete_delivery(delivery_id: str, user: User) -> None:
|
||
with session_scope() as db:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
if not rec:
|
||
raise ValueError("送标申请不存在")
|
||
if rec.status not in DELETABLE_STATUSES:
|
||
raise ValueError(f"当前状态不可删除: {rec.status}")
|
||
db.delete(rec)
|
||
db.flush()
|
||
|
||
|
||
def retry_delivery_ingest(delivery_id: str, user: User) -> dict[str, Any]:
|
||
"""入湖失败后重新执行入湖 Job(无需重新走审核)。"""
|
||
with session_scope() as db:
|
||
rec = db.get(BatchDelivery, delivery_id)
|
||
if not rec:
|
||
raise ValueError("送标申请不存在")
|
||
if rec.status not in RETRY_INGEST_STATUSES:
|
||
raise ValueError(f"当前状态不可重新入湖: {rec.status}")
|
||
err = validate_delivery_fields(
|
||
project=rec.project,
|
||
task=rec.task,
|
||
mode=rec.mode,
|
||
batch_name=rec.batch_name,
|
||
data_path=rec.data_path,
|
||
)
|
||
if err:
|
||
raise ValueError(err)
|
||
approval_id = rec.approval_id
|
||
rec.status = "ingesting"
|
||
rec.error_message = None
|
||
rec.updated_at = _utcnow()
|
||
db.flush()
|
||
|
||
from as_platform.jobs.queue import enqueue_job
|
||
|
||
job = enqueue_job(
|
||
"delivery_ingest",
|
||
{"delivery_id": delivery_id},
|
||
approval_id=approval_id,
|
||
async_run=True,
|
||
)
|
||
return {"ok": True, "job_id": job.get("id"), "delivery_id": delivery_id}
|