Feat: 后端新增基础资料与业务管理只读汇总接口

为 Web 端改版提供名称、统计和看板数据。只新增只读查询、扩展列表与详情
返回字段;写接口、托盘状态机和幂等键规则均未改动,未新增权限标识。

- 基础资料:列表补充名称与汇总,客户详情返回现场树,托盘详情返回最近流转,
  托盘列表支持按流转阶段筛选和二维码精确匹配;新增现场、产线、隧道的上级
  资料选项接口;Excel 导入支持 preview 只校验不写入
- 发货出库:列表支持 keyword、receiptState 筛选并补充名称、托盘数、包数和
  菌种构成;详情明细补充托盘号、菌种、批次
- 首次入库:列表与详情补充名称、托盘数、托盘当前状态;新增 /summary
- 托盘退回:列表支持 keyword 并补充名称与托盘汇总,详情补充托盘号和隧道;
  新增 /summary、/site-options
- 工厂装填:列表支持 keyword 并补充托盘号、仓库、操作人;新增 /summary、
  /batches、/batches/detail
- 客户现场使用:列表支持 keyword、fifoSkipped 并补充名称和被跳过的先到托盘号;
  新增 /site-options、/site-board 以及 usage-fifo 的 /queue
- 先到先用 DAO 抽出公共候选条件与排序,建议查询生成的 SQL 与改动前一致
- 新增各汇总 DAO 的单元测试,同步更新路由权限与模型字段契约测试

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 05:03:10 -07:00
co-authored by Claude Opus 5.5
parent 80a71b48ce
commit 3050ccdc4e
51 changed files with 6545 additions and 103 deletions
@@ -0,0 +1,89 @@
from collections.abc import Awaitable, Callable
from typing import Any, Literal
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from common.vo import PageModel
from module_business.dao.master_summary_dao import MasterSummaryDao
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.vo.master_summary_vo import (
CustomerDetailModel,
CustomerListItemModel,
LineListItemModel,
MasterParentOptionModel,
PalletDetailModel,
PalletListItemModel,
SiteListItemModel,
StrainListItemModel,
TunnelListItemModel,
WarehouseListItemModel,
)
from module_business.entity.vo.master_vo import CustomerModel, PalletModel
SummaryResource = Literal['strain', 'customer', 'site', 'line', 'tunnel', 'warehouse', 'pallet']
_SummaryLoader = Callable[[AsyncSession, list[int], TestBatchScope], Awaitable[dict[int, dict[str, Any]]]]
_ENRICHERS: dict[SummaryResource, tuple[str, _SummaryLoader, type[BaseModel]]] = {
'strain': ('strain_id', MasterSummaryDao.get_strain_summaries, StrainListItemModel),
'customer': ('customer_id', MasterSummaryDao.get_customer_summaries, CustomerListItemModel),
'site': ('site_id', MasterSummaryDao.get_site_summaries, SiteListItemModel),
'line': ('line_id', MasterSummaryDao.get_line_summaries, LineListItemModel),
'tunnel': ('tunnel_id', MasterSummaryDao.get_tunnel_summaries, TunnelListItemModel),
'warehouse': ('warehouse_id', MasterSummaryDao.get_warehouse_summaries, WarehouseListItemModel),
'pallet': ('pallet_id', MasterSummaryDao.get_pallet_summaries, PalletListItemModel),
}
class MasterSummaryService:
"""为基础资料页面补充只读汇总。导出与写入仍使用原主数据模型。"""
@classmethod
async def enrich_page(
cls,
db: AsyncSession,
resource: SummaryResource,
page: PageModel,
scope: TestBatchScope,
) -> PageModel:
id_field, loader, item_model = _ENRICHERS[resource]
rows = list(page.rows)
summaries = await loader(db, [getattr(row, id_field) for row in rows], scope)
enriched = [
item_model.model_validate({**row.model_dump(), **summaries.get(getattr(row, id_field), {})})
for row in rows
]
return page.model_copy(update={'rows': enriched})
@classmethod
async def customer_detail(
cls,
db: AsyncSession,
customer: CustomerModel,
scope: TestBatchScope,
) -> CustomerDetailModel:
sites = await MasterSummaryDao.get_customer_site_tree(db, customer.customer_id, scope)
return CustomerDetailModel.model_validate({**customer.model_dump(), 'sites': sites})
@classmethod
async def pallet_detail(
cls,
db: AsyncSession,
pallet: PalletModel,
scope: TestBatchScope,
) -> PalletDetailModel:
summaries = await MasterSummaryDao.get_pallet_summaries(db, [pallet.pallet_id], scope)
events = await MasterSummaryDao.get_pallet_recent_events(db, pallet.pallet_id, scope)
return PalletDetailModel.model_validate(
{**pallet.model_dump(), **summaries.get(pallet.pallet_id, {}), 'recent_events': events}
)
@classmethod
async def customer_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[MasterParentOptionModel]:
rows = await MasterSummaryDao.list_customer_options(db, scope)
return [MasterParentOptionModel.model_validate(row) for row in rows]
@classmethod
async def site_options(cls, db: AsyncSession, scope: TestBatchScope) -> list[MasterParentOptionModel]:
rows = await MasterSummaryDao.list_site_options(db, scope)
return [MasterParentOptionModel.model_validate(row) for row in rows]