Files
HSAP/platform/as_platform/deliveries/service.py
Chengfang Lu e72bc061c5 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)
2026-06-03 11:40:21 +08:00

315 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""批次送标申请 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}