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
@@ -2,12 +2,13 @@ from datetime import datetime
from typing import Any
from pydantic import BaseModel
from sqlalchemy import ColumnElement, Select, func, select, update
from sqlalchemy import ColumnElement, Select, func, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import InstrumentedAttribute
from common.vo import PageModel
from module_admin.entity.do.user_do import SysUser
from module_business.domain.enums import PalletFlowState
from module_business.domain.test_batch_scope import TestBatchScope, batch_scope_condition
from module_business.entity.do.master_do import (
BizCustomer,
@@ -39,6 +40,18 @@ from module_business.entity.vo.master_vo import (
)
from utils.page_util import PageUtil
_PALLET_FLOW_GROUP_STATES = {
'unstocked': (PalletFlowState.UNSTOCKED.value,),
'warehouse': (PalletFlowState.EMPTY_IN_WAREHOUSE.value, PalletFlowState.FILLED_IN_WAREHOUSE.value),
'transit': (PalletFlowState.IN_TRANSIT.value,),
'site': (
PalletFlowState.AT_SITE_UNUSED.value,
PalletFlowState.AT_SITE_PARTIAL.value,
PalletFlowState.AT_SITE_EMPTY.value,
),
'returning': (PalletFlowState.RETURNING.value,),
}
class MasterDao:
"""菌种、客户、现场、产线、隧道和仓库主数据数据库操作层。"""
@@ -494,15 +507,44 @@ class MasterDao:
) -> PageModel | list[dict[str, Any]]:
conditions = [batch_scope_condition(BizPallet.test_batch_id, scope)]
if query_object.pallet_code:
conditions.append(BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'))
# 扫码枪输入的是二维码固定标识,按编号模糊或标识精确匹配
conditions.append(
or_(
BizPallet.pallet_code.like(f'%{query_object.pallet_code}%'),
BizPallet.qr_token == query_object.pallet_code,
)
)
if query_object.asset_status:
conditions.append(BizPallet.asset_status == query_object.asset_status)
if query_object.flow_group:
conditions.append(cls._pallet_flow_group_condition(query_object.flow_group, scope))
ids = cls._parse_id_list(getattr(query_object, 'ids', None))
if ids:
conditions.append(BizPallet.pallet_id.in_(ids))
statement = select(BizPallet).where(*conditions).order_by(BizPallet.pallet_id)
return await cls._paginate(db, statement, query_object, is_page)
@staticmethod
def _pallet_flow_group_condition(flow_group: str, scope: TestBatchScope) -> ColumnElement[bool]:
states = _PALLET_FLOW_GROUP_STATES[flow_group]
in_states = BizPallet.pallet_id.in_(
select(BizPalletInventory.pallet_id).where(
BizPalletInventory.flow_state.in_(states),
batch_scope_condition(BizPalletInventory.test_batch_id, scope),
)
)
if flow_group != 'unstocked':
return in_states
# 历史托盘可能缺少库存行,同样视为未入库
return or_(
in_states,
BizPallet.pallet_id.not_in(
select(BizPalletInventory.pallet_id).where(
batch_scope_condition(BizPalletInventory.test_batch_id, scope)
)
),
)
@classmethod
async def get_pallet(
cls,