Files
HSAP/platform/as_platform/deliveries/service.py
Chengfang Lu 483e027482 feat: 合并 Docker Compose、标注表格优化与部署文档
将 platform + CVAT 合并为单文件 docker-compose.yml,完善 .env 与 init/dev_up 脚本;
新增 docs/DEPLOY.md 与更新 README 以支持新机器部署;含数据湖示例、车队地图、
紧凑表格 UI、ADAS det_7cls 路径与批次台账等近期改动。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 17:06:31 +08:00

333 lines
12 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:
t = (task or "").strip() or None
if project == "adas":
return t or "cuboid_7cls"
if project == "lane":
return t
return t
def _enrich_delivery_dict(db, rec: BatchDelivery, *, ap_map: dict | None = None, job_map: dict | None = None) -> dict[str, Any]:
d = rec.to_dict()
d["submitted_by"] = rec.submitted_by_name
if rec.approval_id:
ap = (ap_map or {}).get(rec.approval_id) if ap_map is not None else db.get(Approval, rec.approval_id)
if ap:
d["approval_status"] = ap.status
d["job_id"] = ap.job_id
if ap.job_id:
job = (job_map or {}).get(ap.job_id) if job_map is not None else db.get(Job, ap.job_id)
if job:
d["job_status"] = job.status
return d
def _bulk_approval_maps(db, rows: list[BatchDelivery]) -> tuple[dict[str, Approval], dict[str, Job]]:
approval_ids = [r.approval_id for r in rows if r.approval_id]
ap_map: dict[str, Approval] = {}
job_map: dict[str, Job] = {}
if approval_ids:
aps = db.query(Approval).filter(Approval.id.in_(approval_ids)).all()
ap_map = {a.id: a for a in aps}
job_ids = [a.job_id for a in aps if a.job_id]
if job_ids:
jobs = db.query(Job).filter(Job.id.in_(job_ids)).all()
job_map = {j.id: j for j in jobs}
return ap_map, job_map
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()
ap_map, job_map = _bulk_approval_maps(db, rows)
return {
"items": [_enrich_delivery_dict(db, r, ap_map=ap_map, job_map=job_map) 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}