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:
314
platform/as_platform/deliveries/service.py
Normal file
314
platform/as_platform/deliveries/service.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""批次送标申请 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}
|
||||
Reference in New Issue
Block a user