feat: add manual revision and dashboard list apis

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
parent d02fc8565f
commit eeaff269eb
24 changed files with 1950 additions and 64 deletions

View File

@@ -1,10 +1,16 @@
"""Order routes."""
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Depends, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.schemas.order import CreateOrderRequest, CreateOrderResponse, OrderDetailResponse
from app.api.schemas.order import (
CreateOrderRequest,
CreateOrderResponse,
OrderDetailResponse,
OrderListResponse,
)
from app.application.services.order_service import OrderService
from app.domain.enums import OrderStatus
from app.infra.db.session import get_db_session
router = APIRouter(prefix="/orders", tags=["orders"])
@@ -21,6 +27,27 @@ async def create_order(
return await order_service.create_order(session, payload)
@router.get("", response_model=OrderListResponse)
async def list_orders(
page: int = Query(default=1, ge=1),
limit: int = Query(default=20, ge=1, le=100),
query: str | None = Query(default=None, min_length=1),
status_filter: OrderStatus | None = Query(default=None, alias="status"),
order_id: int | None = Query(default=None, ge=1),
session: AsyncSession = Depends(get_db_session),
) -> OrderListResponse:
"""Fetch recent orders for dashboard overview pages."""
return await order_service.list_orders(
session,
page=page,
limit=limit,
query=query,
status_filter=status_filter,
order_id=order_id,
)
@router.get("/{order_id}", response_model=OrderDetailResponse)
async def get_order(
order_id: int,
@@ -29,4 +56,3 @@ async def get_order(
"""Fetch order details."""
return await order_service.get_order(session, order_id)

View File

@@ -3,6 +3,7 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.schemas.revision import ConfirmRevisionRequest, ConfirmRevisionResponse
from app.api.schemas.review import PendingReviewResponse, SubmitReviewRequest, SubmitReviewResponse
from app.application.services.review_service import ReviewService
from app.infra.db.session import get_db_session
@@ -30,3 +31,13 @@ async def submit_review(
return await review_service.submit_review(session, order_id, payload)
@router.post("/{order_id}/confirm-revision", response_model=ConfirmRevisionResponse)
async def confirm_revision(
order_id: int,
payload: ConfirmRevisionRequest,
session: AsyncSession = Depends(get_db_session),
) -> ConfirmRevisionResponse:
"""Confirm a manual revision and resume the workflow."""
return await review_service.confirm_revision_continue(session, order_id, payload)

View File

@@ -0,0 +1,40 @@
"""Manual revision routes."""
from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.schemas.revision import (
RegisterRevisionRequest,
RegisterRevisionResponse,
RevisionChainResponse,
)
from app.application.services.revision_service import RevisionService
from app.infra.db.session import get_db_session
router = APIRouter(prefix="/orders", tags=["revisions"])
revision_service = RevisionService()
@router.post(
"/{order_id}/revisions",
response_model=RegisterRevisionResponse,
status_code=status.HTTP_201_CREATED,
)
async def register_revision(
order_id: int,
payload: RegisterRevisionRequest,
session: AsyncSession = Depends(get_db_session),
) -> RegisterRevisionResponse:
"""Register an offline manual revision asset for an order."""
return await revision_service.register_revision(session, order_id, payload)
@router.get("/{order_id}/revisions", response_model=RevisionChainResponse)
async def list_revisions(
order_id: int,
session: AsyncSession = Depends(get_db_session),
) -> RevisionChainResponse:
"""List the single-line manual revision chain for an order."""
return await revision_service.list_revision_chain(session, order_id)

View File

@@ -1,16 +1,38 @@
"""Workflow routes."""
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.schemas.workflow import WorkflowStatusResponse
from app.api.schemas.workflow import WorkflowListResponse, WorkflowStatusResponse
from app.application.services.workflow_service import WorkflowService
from app.domain.enums import OrderStatus
from app.infra.db.session import get_db_session
router = APIRouter(prefix="/workflows", tags=["workflows"])
workflow_service = WorkflowService()
@router.get("", response_model=WorkflowListResponse)
async def list_workflows(
page: int = Query(default=1, ge=1),
limit: int = Query(default=20, ge=1, le=100),
query: str | None = Query(default=None, min_length=1),
status_filter: OrderStatus | None = Query(default=None, alias="status"),
order_id: int | None = Query(default=None, ge=1),
session: AsyncSession = Depends(get_db_session),
) -> WorkflowListResponse:
"""Fetch recent workflow runs for workflow lookup pages."""
return await workflow_service.list_workflows(
session,
page=page,
limit=limit,
query=query,
status_filter=status_filter,
order_id=order_id,
)
@router.get("/{order_id}", response_model=WorkflowStatusResponse)
async def get_workflow_status(
order_id: int,
@@ -19,4 +41,3 @@ async def get_workflow_status(
"""Fetch persisted workflow status for an order."""
return await workflow_service.get_workflow_status(session, order_id)

View File

@@ -17,7 +17,10 @@ class AssetRead(BaseModel):
order_id: int
asset_type: AssetType
step_name: WorkflowStepName | None
parent_asset_id: int | None = None
root_asset_id: int | None = None
version_no: int = 0
is_current_version: bool = False
uri: str
metadata_json: dict[str, Any] | None
created_at: datetime

View File

@@ -5,7 +5,7 @@ from datetime import datetime
from pydantic import BaseModel
from app.api.schemas.asset import AssetRead
from app.domain.enums import CustomerLevel, OrderStatus, ServiceMode, WorkflowStepName
from app.domain.enums import CustomerLevel, OrderStatus, ReviewTaskStatus, ServiceMode, WorkflowStepName
class CreateOrderRequest(BaseModel):
@@ -41,7 +41,41 @@ class OrderDetailResponse(BaseModel):
final_asset_id: int | None
workflow_id: str | None
current_step: WorkflowStepName | None
current_revision_asset_id: int | None = None
current_revision_version: int | None = None
latest_revision_asset_id: int | None = None
latest_revision_version: int | None = None
revision_count: int = 0
review_task_status: ReviewTaskStatus | None = None
pending_manual_confirm: bool = False
final_asset: AssetRead | None
created_at: datetime
updated_at: datetime
class OrderListItemResponse(BaseModel):
"""Order list item response for overview screens."""
order_id: int
workflow_id: str | None
customer_level: CustomerLevel
service_mode: ServiceMode
status: OrderStatus
current_step: WorkflowStepName | None
updated_at: datetime
final_asset_id: int | None
review_task_status: ReviewTaskStatus | None = None
latest_revision_asset_id: int | None = None
latest_revision_version: int | None = None
revision_count: int = 0
pending_manual_confirm: bool = False
class OrderListResponse(BaseModel):
"""Order list response."""
page: int
limit: int
total: int
total_pages: int
items: list[OrderListItemResponse]

View File

@@ -4,7 +4,7 @@ from datetime import datetime
from pydantic import BaseModel
from app.domain.enums import ReviewDecision, WorkflowStepName
from app.domain.enums import ReviewDecision, ReviewTaskStatus, WorkflowStepName
class SubmitReviewRequest(BaseModel):
@@ -32,5 +32,10 @@ class PendingReviewResponse(BaseModel):
order_id: int
workflow_id: str
current_step: WorkflowStepName | None
review_task_status: ReviewTaskStatus = ReviewTaskStatus.PENDING
latest_revision_asset_id: int | None = None
current_revision_asset_id: int | None = None
latest_revision_version: int | None = None
revision_count: int = 0
pending_manual_confirm: bool = False
created_at: datetime

View File

@@ -0,0 +1,69 @@
"""Revision API schemas."""
from datetime import datetime
from pydantic import BaseModel
from app.domain.enums import ReviewDecision, ReviewTaskStatus
class RegisterRevisionRequest(BaseModel):
"""Request payload for registering a manual revision asset."""
parent_asset_id: int
uploaded_uri: str
reviewer_id: int
comment: str | None = None
class RegisterRevisionResponse(BaseModel):
"""Response returned after a manual revision has been registered."""
order_id: int
workflow_id: str
asset_id: int
parent_asset_id: int
root_asset_id: int
version_no: int
review_task_status: ReviewTaskStatus
latest_revision_asset_id: int
revision_count: int
class RevisionChainItem(BaseModel):
"""One item in the manual revision chain."""
asset_id: int
order_id: int
parent_asset_id: int | None
root_asset_id: int | None
version_no: int
is_current_version: bool
uri: str
created_at: datetime
class RevisionChainResponse(BaseModel):
"""Response returned when listing a revision chain."""
order_id: int
latest_revision_asset_id: int | None = None
revision_count: int = 0
items: list[RevisionChainItem]
class ConfirmRevisionRequest(BaseModel):
"""Request payload for confirming a manual revision."""
reviewer_id: int
comment: str | None = None
class ConfirmRevisionResponse(BaseModel):
"""Response returned after confirming a manual revision."""
order_id: int
workflow_id: str
revision_asset_id: int
decision: ReviewDecision
status: str

View File

@@ -5,7 +5,7 @@ from typing import Any
from pydantic import BaseModel, ConfigDict
from app.domain.enums import OrderStatus, StepStatus, WorkflowStepName
from app.domain.enums import OrderStatus, ReviewTaskStatus, StepStatus, WorkflowStepName
class WorkflowStepRead(BaseModel):
@@ -32,7 +32,40 @@ class WorkflowStatusResponse(BaseModel):
workflow_type: str
workflow_status: OrderStatus
current_step: WorkflowStepName | None
current_revision_asset_id: int | None = None
current_revision_version: int | None = None
latest_revision_asset_id: int | None = None
latest_revision_version: int | None = None
revision_count: int = 0
review_task_status: ReviewTaskStatus | None = None
pending_manual_confirm: bool = False
steps: list[WorkflowStepRead]
created_at: datetime
updated_at: datetime
class WorkflowListItemResponse(BaseModel):
"""Workflow list item response for workflow home screens."""
order_id: int
workflow_id: str
workflow_type: str
workflow_status: OrderStatus
current_step: WorkflowStepName | None
updated_at: datetime
failure_count: int
review_task_status: ReviewTaskStatus | None = None
latest_revision_asset_id: int | None = None
latest_revision_version: int | None = None
revision_count: int = 0
pending_manual_confirm: bool = False
class WorkflowListResponse(BaseModel):
"""Workflow list response."""
page: int
limit: int
total: int
total_pages: int
items: list[WorkflowListItemResponse]