feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
"""产品服务 — 业务逻辑层:扫码查询、CRUD"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
from fastapi import HTTPException, status
|
2026-08-12 15:52:06 +08:00
|
|
|
|
from sqlalchemy import select, or_, cast, String, delete, update
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.product import Product
|
|
|
|
|
|
from app.models.production_order import ProductionOrder
|
|
|
|
|
|
from app.models.task import Task
|
|
|
|
|
|
from app.schemas.product import ProductCreate, ProductUpdate, ProductResponse, ProductScanResponse
|
2026-08-05 14:00:55 +08:00
|
|
|
|
from app.schemas.task import TaskSummaryResponse, TaskResponse, TaskRecordResponse
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _task_to_response(task: Task) -> TaskResponse:
|
|
|
|
|
|
"""将 Task ORM 对象递归转为 TaskResponse(含子任务树)"""
|
2026-08-06 11:16:07 +08:00
|
|
|
|
product_sn = ""
|
|
|
|
|
|
product_material = ""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if task.product:
|
|
|
|
|
|
product_sn = task.product.serial_number or ""
|
|
|
|
|
|
product_material = (task.product.material_name or task.product.material_id or "")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
return TaskResponse(
|
|
|
|
|
|
id=task.id,
|
|
|
|
|
|
product_id=task.product_id,
|
2026-08-06 11:16:07 +08:00
|
|
|
|
product_sn=product_sn,
|
|
|
|
|
|
product_material=product_material,
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
parent_task_id=task.parent_task_id,
|
|
|
|
|
|
task_name=task.task_name,
|
|
|
|
|
|
assignee_id=task.assignee_id,
|
|
|
|
|
|
status=task.status,
|
|
|
|
|
|
notify_parent_on_complete=task.notify_parent_on_complete,
|
|
|
|
|
|
is_rework=task.is_rework,
|
2026-08-07 11:44:04 +08:00
|
|
|
|
task_type=task.task_type,
|
2026-08-05 17:41:09 +08:00
|
|
|
|
remark=task.remark,
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
reject_reason=task.reject_reason,
|
|
|
|
|
|
received_at=task.received_at,
|
|
|
|
|
|
completed_at=task.completed_at,
|
|
|
|
|
|
created_at=task.created_at,
|
|
|
|
|
|
child_tasks=[_task_to_response(c) for c in task.child_tasks],
|
2026-08-05 14:00:55 +08:00
|
|
|
|
records=[TaskRecordResponse.model_validate(r) for r in (task.records or [])],
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _load_task_tree(db: AsyncSession, product_id: uuid.UUID) -> list[TaskResponse]:
|
2026-08-12 12:03:02 +08:00
|
|
|
|
"""使用 PostgreSQL Recursive CTE 一次性加载产品下完整任务树(消除 N+1)"""
|
|
|
|
|
|
from app.services.task_tree_loader import load_task_trees_by_product
|
|
|
|
|
|
tasks = await load_task_trees_by_product(db, product_id)
|
|
|
|
|
|
return [_task_to_response(t) for t in tasks]
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_product_by_serial(db: AsyncSession, serial_number: str) -> ProductScanResponse:
|
|
|
|
|
|
"""扫码查询:根据 16 位序列号查出产品 + 所属订单 + 完整任务树"""
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(Product)
|
|
|
|
|
|
.options(
|
|
|
|
|
|
selectinload(Product.order),
|
|
|
|
|
|
selectinload(Product.parent_product),
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(Product.serial_number == serial_number)
|
|
|
|
|
|
)
|
|
|
|
|
|
product = result.scalar_one_or_none()
|
|
|
|
|
|
if not product:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
|
detail=f"未找到序列号为 {serial_number} 的产品",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取顶层任务摘要(兼容旧接口)
|
|
|
|
|
|
top_tasks_result = await db.execute(
|
|
|
|
|
|
select(Task)
|
|
|
|
|
|
.where(
|
|
|
|
|
|
Task.product_id == product.id,
|
|
|
|
|
|
Task.parent_task_id.is_(None),
|
|
|
|
|
|
)
|
|
|
|
|
|
.order_by(Task.created_at)
|
|
|
|
|
|
)
|
|
|
|
|
|
top_tasks = top_tasks_result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取完整任务树(递归嵌套,供前端渲染十字矩阵树状图)
|
|
|
|
|
|
task_tree = await _load_task_tree(db, product.id)
|
|
|
|
|
|
|
2026-08-10 11:25:44 +08:00
|
|
|
|
# 🔧 收集任务树中所有 assignee_id → 查中文姓名映射
|
|
|
|
|
|
assignee_ids: set[str] = set()
|
|
|
|
|
|
def _collect_ids(tasks):
|
|
|
|
|
|
for t in tasks:
|
|
|
|
|
|
if t.assignee_id: assignee_ids.add(t.assignee_id)
|
|
|
|
|
|
if t.child_tasks: _collect_ids(t.child_tasks)
|
|
|
|
|
|
for t in top_tasks:
|
|
|
|
|
|
if t.assignee_id: assignee_ids.add(t.assignee_id)
|
|
|
|
|
|
_collect_ids(task_tree)
|
|
|
|
|
|
assignee_names = _lookup_display_names(list(assignee_ids))
|
|
|
|
|
|
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
return ProductScanResponse(
|
|
|
|
|
|
id=product.id,
|
|
|
|
|
|
serial_number=product.serial_number,
|
2026-08-05 14:00:55 +08:00
|
|
|
|
external_serial=product.external_serial,
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
order_id=product.order_id,
|
|
|
|
|
|
order_no=product.order.order_no if product.order else "",
|
|
|
|
|
|
material_id=product.material_id,
|
2026-08-05 14:00:55 +08:00
|
|
|
|
material_name=product.material_name,
|
|
|
|
|
|
spec_model=product.spec_model,
|
|
|
|
|
|
category=product.category,
|
|
|
|
|
|
material_type=product.material_type,
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
parent_product_id=product.parent_product_id,
|
|
|
|
|
|
current_location_id=product.current_location_id,
|
2026-08-05 14:00:55 +08:00
|
|
|
|
overall_status=product.overall_status,
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
status=product.status,
|
|
|
|
|
|
created_at=product.created_at,
|
|
|
|
|
|
top_level_tasks=[
|
|
|
|
|
|
TaskSummaryResponse.model_validate(t) for t in top_tasks
|
|
|
|
|
|
],
|
|
|
|
|
|
task_tree=task_tree,
|
2026-08-10 11:25:44 +08:00
|
|
|
|
assignee_names=assignee_names, # 🔧 username→中文姓名
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_product(db: AsyncSession, product_id: uuid.UUID) -> Product:
|
|
|
|
|
|
"""获取产品,不存在则 404"""
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(Product)
|
|
|
|
|
|
.options(selectinload(Product.order))
|
|
|
|
|
|
.where(Product.id == product_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
product = result.scalar_one_or_none()
|
|
|
|
|
|
if not product:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
|
|
|
|
detail=f"产品不存在: {product_id}",
|
|
|
|
|
|
)
|
|
|
|
|
|
return product
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 15:42:10 +08:00
|
|
|
|
async def create_product(db: AsyncSession, data: ProductCreate, creator_username: str = "") -> ProductResponse:
|
|
|
|
|
|
"""创建产品 — 自动生成 16 位 HEX 序列号,初始位置设为创建者"""
|
2026-08-05 14:00:55 +08:00
|
|
|
|
from app.services.counter_service import ensure_sequence, next_hex_id
|
|
|
|
|
|
from app.models.production_order import ProductionOrder
|
|
|
|
|
|
|
|
|
|
|
|
await ensure_sequence(db)
|
|
|
|
|
|
hex_id = await next_hex_id(db)
|
|
|
|
|
|
|
|
|
|
|
|
# 处理订单: 如果传了 order_no 但没传 order_id,查找或创建
|
|
|
|
|
|
order_id = data.order_id
|
|
|
|
|
|
if not order_id and data.order_no:
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(ProductionOrder).where(ProductionOrder.order_no == data.order_no.strip())
|
|
|
|
|
|
)
|
|
|
|
|
|
existing = result.scalar_one_or_none()
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
order_id = existing.id
|
|
|
|
|
|
else:
|
|
|
|
|
|
new_order = ProductionOrder(order_no=data.order_no.strip())
|
|
|
|
|
|
db.add(new_order)
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
order_id = new_order.id
|
|
|
|
|
|
|
|
|
|
|
|
product = Product(
|
|
|
|
|
|
serial_number=hex_id,
|
|
|
|
|
|
order_id=order_id,
|
|
|
|
|
|
material_id=data.material_id,
|
|
|
|
|
|
material_name=data.material_name or None,
|
|
|
|
|
|
spec_model=data.spec_model or None,
|
|
|
|
|
|
category=data.category or None,
|
|
|
|
|
|
material_type=data.material_type or None,
|
|
|
|
|
|
external_serial=data.external_serial,
|
|
|
|
|
|
parent_product_id=data.parent_product_id,
|
2026-08-07 15:42:10 +08:00
|
|
|
|
current_location_id=creator_username or None, # 谁创建,初始位置就是谁
|
2026-08-05 14:00:55 +08:00
|
|
|
|
)
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
db.add(product)
|
|
|
|
|
|
await db.commit()
|
2026-08-05 14:00:55 +08:00
|
|
|
|
await db.refresh(product, ["order"])
|
2026-08-07 15:42:10 +08:00
|
|
|
|
|
|
|
|
|
|
# 查创建者的真实姓名
|
|
|
|
|
|
creator_display_name = ""
|
|
|
|
|
|
if creator_username:
|
|
|
|
|
|
name_map = _lookup_display_names([creator_username])
|
|
|
|
|
|
creator_display_name = name_map.get(creator_username, "")
|
|
|
|
|
|
|
2026-08-05 14:00:55 +08:00
|
|
|
|
return ProductResponse(
|
|
|
|
|
|
id=product.id,
|
|
|
|
|
|
serial_number=product.serial_number,
|
|
|
|
|
|
external_serial=product.external_serial,
|
|
|
|
|
|
order_id=product.order_id,
|
|
|
|
|
|
order_no=product.order.order_no if product.order else (data.order_no or ""),
|
|
|
|
|
|
material_id=product.material_id,
|
|
|
|
|
|
material_name=product.material_name,
|
|
|
|
|
|
spec_model=product.spec_model,
|
|
|
|
|
|
category=product.category,
|
|
|
|
|
|
material_type=product.material_type,
|
|
|
|
|
|
parent_product_id=product.parent_product_id,
|
|
|
|
|
|
current_location_id=product.current_location_id,
|
2026-08-07 15:42:10 +08:00
|
|
|
|
current_location_name=creator_display_name or None,
|
2026-08-05 14:00:55 +08:00
|
|
|
|
overall_status=product.overall_status,
|
|
|
|
|
|
status=product.status,
|
|
|
|
|
|
created_at=product.created_at,
|
|
|
|
|
|
)
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def update_product(db: AsyncSession, product_id: uuid.UUID, data: ProductUpdate) -> ProductResponse:
|
|
|
|
|
|
"""更新产品"""
|
2026-08-05 14:00:55 +08:00
|
|
|
|
from app.models.production_order import ProductionOrder
|
|
|
|
|
|
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
product = await get_product(db, product_id)
|
|
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
2026-08-05 14:00:55 +08:00
|
|
|
|
|
|
|
|
|
|
# 处理 order_no → order_id 映射
|
|
|
|
|
|
if "order_no" in update_data:
|
|
|
|
|
|
order_no_val = update_data.pop("order_no")
|
|
|
|
|
|
if order_no_val and order_no_val.strip():
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(ProductionOrder).where(ProductionOrder.order_no == order_no_val.strip())
|
|
|
|
|
|
)
|
|
|
|
|
|
existing = result.scalar_one_or_none()
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
product.order_id = existing.id
|
|
|
|
|
|
else:
|
|
|
|
|
|
new_order = ProductionOrder(order_no=order_no_val.strip())
|
|
|
|
|
|
db.add(new_order)
|
|
|
|
|
|
await db.flush()
|
|
|
|
|
|
product.order_id = new_order.id
|
|
|
|
|
|
else:
|
|
|
|
|
|
product.order_id = None
|
|
|
|
|
|
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
for field, value in update_data.items():
|
|
|
|
|
|
setattr(product, field, value)
|
|
|
|
|
|
await db.commit()
|
2026-08-05 14:00:55 +08:00
|
|
|
|
await db.refresh(product, ["order"])
|
|
|
|
|
|
return ProductResponse(
|
|
|
|
|
|
id=product.id,
|
|
|
|
|
|
serial_number=product.serial_number,
|
|
|
|
|
|
external_serial=product.external_serial,
|
|
|
|
|
|
order_id=product.order_id,
|
|
|
|
|
|
order_no=product.order.order_no if product.order else "",
|
|
|
|
|
|
material_id=product.material_id,
|
|
|
|
|
|
material_name=product.material_name,
|
|
|
|
|
|
spec_model=product.spec_model,
|
|
|
|
|
|
category=product.category,
|
|
|
|
|
|
material_type=product.material_type,
|
|
|
|
|
|
parent_product_id=product.parent_product_id,
|
|
|
|
|
|
current_location_id=product.current_location_id,
|
|
|
|
|
|
overall_status=product.overall_status,
|
|
|
|
|
|
status=product.status,
|
|
|
|
|
|
created_at=product.created_at,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VALID_OVERALL_STATUS = {"备货", "生产", "测试", "维修", "在库"}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 15:05:30 +08:00
|
|
|
|
async def update_overall_status(
|
|
|
|
|
|
db: AsyncSession, serial_number: str, status_value: str,
|
|
|
|
|
|
current_user: dict | None = None,
|
|
|
|
|
|
) -> ProductScanResponse:
|
|
|
|
|
|
"""更新产品宏观状态
|
|
|
|
|
|
|
|
|
|
|
|
权限校验:
|
|
|
|
|
|
- SUPER_ADMIN 角色:直接放行
|
|
|
|
|
|
- 当前操作该产品主线任务(WIP/PENDING 状态主干任务)的人:放行
|
|
|
|
|
|
- 其他:403
|
|
|
|
|
|
"""
|
2026-08-05 14:00:55 +08:00
|
|
|
|
if status_value not in VALID_OVERALL_STATUS:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
|
detail=f"无效状态: {status_value},合法值: {', '.join(sorted(VALID_OVERALL_STATUS))}",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
|
select(Product)
|
|
|
|
|
|
.options(selectinload(Product.order))
|
|
|
|
|
|
.where(Product.serial_number == serial_number)
|
|
|
|
|
|
)
|
|
|
|
|
|
product = result.scalar_one_or_none()
|
|
|
|
|
|
if not product:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail=f"未找到序列号 {serial_number} 的产品")
|
|
|
|
|
|
|
2026-08-11 17:39:30 +08:00
|
|
|
|
# ── 权限校验(无 current_user 一律拒绝,杜绝空 dict 绕过)──
|
|
|
|
|
|
if not current_user:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
|
|
detail="请先登录",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
user_role = current_user.get("role", "")
|
|
|
|
|
|
user_username = current_user.get("username", "")
|
|
|
|
|
|
|
|
|
|
|
|
# SUPER_ADMIN 直接放行
|
|
|
|
|
|
if user_role != "SUPER_ADMIN":
|
|
|
|
|
|
# 检查当前用户是否是该产品主线任务的负责人
|
|
|
|
|
|
main_task_result = await db.execute(
|
|
|
|
|
|
select(Task).where(
|
|
|
|
|
|
Task.product_id == product.id,
|
|
|
|
|
|
Task.status.in_(["WIP", "PENDING"]),
|
|
|
|
|
|
or_(
|
|
|
|
|
|
Task.parent_task_id.is_(None),
|
|
|
|
|
|
Task.task_type.in_(["TRANSFER", "RECOVERY"]),
|
|
|
|
|
|
),
|
|
|
|
|
|
).order_by(Task.created_at.desc()).limit(1)
|
|
|
|
|
|
)
|
|
|
|
|
|
main_task = main_task_result.scalar_one_or_none()
|
|
|
|
|
|
has_permission = (
|
|
|
|
|
|
main_task is not None
|
|
|
|
|
|
and main_task.assignee_id == user_username
|
|
|
|
|
|
)
|
|
|
|
|
|
if not has_permission:
|
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
|
|
|
|
detail="只有 SUPER_ADMIN 或当前操作该产品主线任务的人才能修改宏观状态",
|
2026-08-11 15:05:30 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-05 14:00:55 +08:00
|
|
|
|
product.overall_status = status_value
|
|
|
|
|
|
await db.commit()
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
await db.refresh(product)
|
2026-08-05 14:00:55 +08:00
|
|
|
|
|
|
|
|
|
|
return await get_product_by_serial(db, serial_number)
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 15:38:01 +08:00
|
|
|
|
def _lookup_display_names(location_ids: list[str]) -> dict[str, str]:
|
2026-08-12 12:03:02 +08:00
|
|
|
|
"""批量查询 MOM sys_user,将 username 映射为真实姓名(带 2h TTL 缓存)"""
|
|
|
|
|
|
from app.services.mom_cache import get_display_names
|
|
|
|
|
|
return get_display_names(location_ids)
|
2026-08-07 15:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 11:44:04 +08:00
|
|
|
|
async def get_all_products(
|
|
|
|
|
|
db: AsyncSession,
|
|
|
|
|
|
skip: int = 0,
|
|
|
|
|
|
limit: int = 50,
|
|
|
|
|
|
keyword: str | None = None,
|
|
|
|
|
|
status_filter: str | None = None,
|
|
|
|
|
|
) -> list[ProductResponse]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取产品列表 — 支持多维 keyword 搜索 + 状态筛选
|
|
|
|
|
|
|
|
|
|
|
|
keyword: 同时模糊匹配 serial_number (产品身份证)、material_name/id (规格型号)、order_no (订单号)
|
|
|
|
|
|
status_filter: 按产品状态过滤 (如 PENDING / WIP / COMPLETED / ARCHIVED)
|
|
|
|
|
|
"""
|
|
|
|
|
|
stmt = select(Product).options(selectinload(Product.order))
|
|
|
|
|
|
|
|
|
|
|
|
# keyword 多字段 OR 模糊搜索
|
|
|
|
|
|
if keyword and keyword.strip():
|
|
|
|
|
|
kw = f"%{keyword.strip()}%"
|
|
|
|
|
|
stmt = stmt.outerjoin(ProductionOrder, Product.order_id == ProductionOrder.id).where(
|
|
|
|
|
|
or_(
|
|
|
|
|
|
Product.serial_number.ilike(kw),
|
|
|
|
|
|
Product.material_name.ilike(kw),
|
|
|
|
|
|
cast(Product.material_id, String).ilike(kw),
|
|
|
|
|
|
Product.spec_model.ilike(kw),
|
|
|
|
|
|
ProductionOrder.order_no.ilike(kw),
|
|
|
|
|
|
)
|
|
|
|
|
|
).distinct()
|
|
|
|
|
|
|
|
|
|
|
|
# 状态筛选 — 大小写不敏感,支持组合过滤
|
|
|
|
|
|
if status_filter and status_filter.strip():
|
|
|
|
|
|
from sqlalchemy import func
|
|
|
|
|
|
sf = status_filter.strip().upper()
|
|
|
|
|
|
if sf == "DONE":
|
|
|
|
|
|
# "已完成" 匹配 COMPLETED 或 ARCHIVED
|
|
|
|
|
|
stmt = stmt.where(
|
|
|
|
|
|
or_(
|
|
|
|
|
|
func.upper(Product.status) == "COMPLETED",
|
|
|
|
|
|
func.upper(Product.status) == "ARCHIVED",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
elif sf == "PENDING":
|
|
|
|
|
|
# "待流转" — 产品状态 PENDING 且所有顶层任务均未分配人
|
|
|
|
|
|
stmt = (
|
|
|
|
|
|
stmt.outerjoin(Task, Task.product_id == Product.id)
|
|
|
|
|
|
.where(func.upper(Product.status) == "PENDING")
|
|
|
|
|
|
.where(Task.assignee_id.is_(None))
|
|
|
|
|
|
.distinct()
|
|
|
|
|
|
)
|
|
|
|
|
|
elif sf == "PENDING_ASSIGNED":
|
|
|
|
|
|
# "待接收" — 产品状态 PENDING 但已有任务被分配(等待工人扫码)
|
|
|
|
|
|
stmt = (
|
|
|
|
|
|
stmt.outerjoin(Task, Task.product_id == Product.id)
|
|
|
|
|
|
.where(func.upper(Product.status) == "PENDING")
|
|
|
|
|
|
.where(Task.assignee_id.isnot(None))
|
|
|
|
|
|
.distinct()
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
stmt = stmt.where(func.upper(Product.status) == sf)
|
|
|
|
|
|
|
|
|
|
|
|
stmt = stmt.offset(skip).limit(limit).order_by(Product.created_at.desc())
|
|
|
|
|
|
|
|
|
|
|
|
result = await db.execute(stmt)
|
feat(services): 重写核心业务逻辑 — 接收/驳回返工/裂变转交
task_service 新增 3 个核心业务函数:
- receive_task(): PENDING→WIP, 记录 received_at, 仅 PENDING 状态可接收
- reject_task(): →REJECTED, 记录 reject_reason+completed_at,
防呆闭环: 自动查找父任务负责人, 创建 is_rework=True 的返工任务
- transfer_task(): →COMPLETED, 记录 completed_at,
裂变转交: 遍历 next_assignees 批量创建 PENDING 任务,
多路裂变(>1)时子任务挂载形成树状分支,
virtual_warehouse 入库逻辑更新 Product.current_location_id
- complete_task(): 保留兼容, 同步使用新状态常量 TASK_STATUS_*
product_service 重构:
- get_product_by_serial(): 新增 task_tree 返回完整递归任务树
- _load_task_tree(): 递归加载产品关联的所有任务嵌套关系
2026-08-04 17:03:37 +08:00
|
|
|
|
products = result.scalars().all()
|
2026-08-07 15:38:01 +08:00
|
|
|
|
|
2026-08-09 18:23:55 +08:00
|
|
|
|
# 🔧 批量预计算 macro_status:一次性查出所有产品关联的任务状态
|
|
|
|
|
|
product_ids = [p.id for p in products]
|
|
|
|
|
|
macro_map: dict[uuid.UUID, str] = {}
|
|
|
|
|
|
if product_ids:
|
|
|
|
|
|
from sqlalchemy import case, func as sa_func
|
|
|
|
|
|
task_stmt = (
|
|
|
|
|
|
select(
|
|
|
|
|
|
Task.product_id,
|
|
|
|
|
|
sa_func.max(case(
|
|
|
|
|
|
(Task.status == "WIP", 3),
|
|
|
|
|
|
(Task.status == "PENDING", 2),
|
|
|
|
|
|
(Task.status == "COMPLETED", 1),
|
|
|
|
|
|
(Task.status == "ARCHIVED", 1),
|
|
|
|
|
|
else_=0,
|
|
|
|
|
|
)).label("prio"),
|
|
|
|
|
|
)
|
|
|
|
|
|
.where(Task.product_id.in_(product_ids))
|
|
|
|
|
|
.group_by(Task.product_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
task_result = await db.execute(task_stmt)
|
|
|
|
|
|
prio_to_status = {3: "WIP", 2: "PENDING", 1: "COMPLETED", 0: None}
|
|
|
|
|
|
for row in task_result:
|
|
|
|
|
|
macro_map[row[0]] = prio_to_status.get(row[1], None)
|
|
|
|
|
|
|
2026-08-10 11:32:07 +08:00
|
|
|
|
# 🔧 动态主干状态+位置:只从主干任务中获取最高优先级任务的 task_name + assignee_id
|
2026-08-10 11:25:44 +08:00
|
|
|
|
overall_names: dict[uuid.UUID, str] = {}
|
2026-08-10 11:32:07 +08:00
|
|
|
|
main_assignees: dict[uuid.UUID, str] = {}
|
2026-08-10 11:25:44 +08:00
|
|
|
|
if product_ids:
|
2026-08-12 15:05:39 +08:00
|
|
|
|
from sqlalchemy import and_, func as sa_func, case as sa_case
|
2026-08-10 11:41:10 +08:00
|
|
|
|
main_where = and_(
|
2026-08-10 11:32:07 +08:00
|
|
|
|
Task.product_id.in_(product_ids),
|
2026-08-10 11:41:10 +08:00
|
|
|
|
or_(
|
2026-08-10 11:32:07 +08:00
|
|
|
|
Task.parent_task_id.is_(None),
|
|
|
|
|
|
Task.task_type.in_(["TRANSFER", "RECOVERY"]),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-08-10 11:41:10 +08:00
|
|
|
|
prio_expr = sa_case(
|
2026-08-10 11:32:07 +08:00
|
|
|
|
(Task.status == "WIP", 3),
|
|
|
|
|
|
(Task.status == "PENDING", 2),
|
|
|
|
|
|
(Task.status == "COMPLETED", 1),
|
|
|
|
|
|
else_=0,
|
|
|
|
|
|
)
|
|
|
|
|
|
# 子查询:每个产品最高优先级主干任务
|
|
|
|
|
|
max_prio = (
|
2026-08-10 11:41:10 +08:00
|
|
|
|
select(Task.product_id, sa_func.max(prio_expr).label("prio"))
|
2026-08-10 11:32:07 +08:00
|
|
|
|
.where(main_where)
|
2026-08-10 11:25:44 +08:00
|
|
|
|
.group_by(Task.product_id)
|
2026-08-10 11:32:07 +08:00
|
|
|
|
).subquery("mp")
|
|
|
|
|
|
# JOIN 回 tasks 拿 task_name + assignee_id(同优先级取最新创建的)
|
|
|
|
|
|
main_stmt = (
|
|
|
|
|
|
select(Task.product_id, Task.task_name, Task.assignee_id)
|
2026-08-10 11:41:10 +08:00
|
|
|
|
.join(max_prio, and_(
|
2026-08-10 11:32:07 +08:00
|
|
|
|
Task.product_id == max_prio.c.product_id,
|
|
|
|
|
|
prio_expr == max_prio.c.prio,
|
2026-08-10 11:25:44 +08:00
|
|
|
|
))
|
2026-08-10 11:32:07 +08:00
|
|
|
|
.where(main_where)
|
|
|
|
|
|
.order_by(Task.product_id, Task.created_at.desc())
|
2026-08-10 11:41:10 +08:00
|
|
|
|
.distinct(Task.product_id)
|
2026-08-10 11:25:44 +08:00
|
|
|
|
)
|
2026-08-10 11:32:07 +08:00
|
|
|
|
main_result = await db.execute(main_stmt)
|
|
|
|
|
|
for row in main_result:
|
|
|
|
|
|
pid, tname, assignee = row[0], row[1], row[2]
|
|
|
|
|
|
overall_names[pid] = tname
|
|
|
|
|
|
if assignee: main_assignees[pid] = assignee
|
|
|
|
|
|
|
|
|
|
|
|
# 🔧 动态主干的 assignee_id → 查中文姓名
|
|
|
|
|
|
dynamic_location_ids = list(main_assignees.values())
|
|
|
|
|
|
dynamic_name_map = _lookup_display_names(dynamic_location_ids)
|
|
|
|
|
|
|
|
|
|
|
|
# 🔧 合并:静态位置姓名(兜底)+ 动态主干位置姓名(优先)
|
|
|
|
|
|
static_location_ids = [p.current_location_id for p in products if p.current_location_id]
|
|
|
|
|
|
merged_location_ids = list(set(static_location_ids + dynamic_location_ids))
|
|
|
|
|
|
merged_name_map = _lookup_display_names(merged_location_ids)
|
2026-08-10 11:25:44 +08:00
|
|
|
|
|
2026-08-12 15:43:33 +08:00
|
|
|
|
# 🔧 批量查询每个产品活跃任务的最新记录
|
|
|
|
|
|
latest_record_map: dict[uuid.UUID, tuple] = {}
|
|
|
|
|
|
if product_ids:
|
|
|
|
|
|
from app.models.task import TaskRecord as TR
|
|
|
|
|
|
wip_pending_ids = select(Task.id).where(
|
|
|
|
|
|
and_(
|
|
|
|
|
|
Task.product_id.in_(product_ids),
|
|
|
|
|
|
Task.status.in_(["WIP", "PENDING"]),
|
|
|
|
|
|
)
|
|
|
|
|
|
).subquery()
|
|
|
|
|
|
ranked = (
|
2026-08-12 16:01:24 +08:00
|
|
|
|
select(TR.task_id, TR.remark, TR.images, TR.created_at, Task.product_id,
|
2026-08-12 15:52:06 +08:00
|
|
|
|
sa_func.row_number().over(
|
2026-08-12 15:43:33 +08:00
|
|
|
|
partition_by=Task.product_id,
|
|
|
|
|
|
order_by=TR.created_at.desc()
|
|
|
|
|
|
).label("rn"))
|
|
|
|
|
|
.join(Task, TR.task_id == Task.id)
|
|
|
|
|
|
.where(Task.id.in_(select(wip_pending_ids.c.id)))
|
|
|
|
|
|
).subquery()
|
|
|
|
|
|
rec_result = await db.execute(
|
2026-08-12 16:01:24 +08:00
|
|
|
|
select(ranked.c.product_id, ranked.c.created_at, ranked.c.remark, ranked.c.images)
|
2026-08-12 15:43:33 +08:00
|
|
|
|
.where(ranked.c.rn == 1)
|
|
|
|
|
|
)
|
|
|
|
|
|
for row in rec_result:
|
2026-08-12 16:01:24 +08:00
|
|
|
|
has_img = bool(row[3] and row[3] != "[]" and row[3] != "null")
|
|
|
|
|
|
latest_record_map[row[0]] = (row[1], row[2], has_img)
|
2026-08-12 15:43:33 +08:00
|
|
|
|
|
2026-08-05 14:00:55 +08:00
|
|
|
|
return [
|
|
|
|
|
|
ProductResponse(
|
|
|
|
|
|
id=p.id,
|
|
|
|
|
|
serial_number=p.serial_number,
|
|
|
|
|
|
external_serial=p.external_serial,
|
|
|
|
|
|
order_id=p.order_id,
|
|
|
|
|
|
order_no=p.order.order_no if p.order else "",
|
|
|
|
|
|
material_id=p.material_id,
|
|
|
|
|
|
material_name=p.material_name,
|
|
|
|
|
|
spec_model=p.spec_model,
|
|
|
|
|
|
category=p.category,
|
|
|
|
|
|
material_type=p.material_type,
|
|
|
|
|
|
parent_product_id=p.parent_product_id,
|
2026-08-10 11:32:07 +08:00
|
|
|
|
current_location_id=(
|
2026-08-12 15:43:33 +08:00
|
|
|
|
main_assignees.get(p.id) or p.current_location_id
|
2026-08-10 11:32:07 +08:00
|
|
|
|
),
|
2026-08-07 15:38:01 +08:00
|
|
|
|
current_location_name=(
|
2026-08-10 11:32:07 +08:00
|
|
|
|
"仓库" if (main_assignees.get(p.id) or p.current_location_id) == "virtual_warehouse"
|
|
|
|
|
|
else dynamic_name_map.get(main_assignees.get(p.id, ""))
|
|
|
|
|
|
or merged_name_map.get(p.current_location_id) if p.current_location_id
|
2026-08-07 15:38:01 +08:00
|
|
|
|
else None
|
|
|
|
|
|
),
|
2026-08-10 11:25:44 +08:00
|
|
|
|
macro_status=macro_map.get(p.id) or p.status,
|
2026-08-10 11:32:07 +08:00
|
|
|
|
overall_status=overall_names.get(p.id) or p.overall_status,
|
2026-08-05 14:00:55 +08:00
|
|
|
|
status=p.status,
|
|
|
|
|
|
created_at=p.created_at,
|
2026-08-12 16:01:24 +08:00
|
|
|
|
latest_record_time=latest_record_map.get(p.id, (None, None, False))[0],
|
|
|
|
|
|
latest_record_content=latest_record_map.get(p.id, (None, None, False))[1],
|
|
|
|
|
|
latest_record_has_images=latest_record_map.get(p.id, (None, None, False))[2],
|
2026-08-05 14:00:55 +08:00
|
|
|
|
)
|
|
|
|
|
|
for p in products
|
|
|
|
|
|
]
|
2026-08-07 15:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def delete_product(db: AsyncSession, product_id: uuid.UUID) -> None:
|
|
|
|
|
|
"""删除产品及其关联任务"""
|
|
|
|
|
|
product = await get_product(db, product_id)
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.task import TaskRecord
|
2026-08-10 17:36:48 +08:00
|
|
|
|
from app.models.task_log import TaskLog
|
|
|
|
|
|
|
|
|
|
|
|
# 🚀 1. 切断产品自引用:子产品的 parent_product_id 置空
|
|
|
|
|
|
await db.execute(
|
|
|
|
|
|
update(Product).where(Product.parent_product_id == product_id).values(parent_product_id=None)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 查询所有关联任务
|
2026-08-07 15:38:01 +08:00
|
|
|
|
tasks_result = await db.execute(
|
|
|
|
|
|
select(Task).where(Task.product_id == product_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
tasks = tasks_result.scalars().all()
|
2026-08-10 17:36:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 🚀 3. 切断任务自引用:子任务的 parent_task_id 置空
|
2026-08-07 15:38:01 +08:00
|
|
|
|
for task in tasks:
|
|
|
|
|
|
await db.execute(
|
2026-08-10 17:36:48 +08:00
|
|
|
|
update(Task).where(Task.parent_task_id == task.id).values(parent_task_id=None)
|
2026-08-07 15:38:01 +08:00
|
|
|
|
)
|
2026-08-10 17:36:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 4. 删除任务记录、日志、任务本身
|
2026-08-07 15:38:01 +08:00
|
|
|
|
for task in tasks:
|
2026-08-10 17:36:48 +08:00
|
|
|
|
await db.execute(delete(TaskRecord).where(TaskRecord.task_id == task.id))
|
|
|
|
|
|
await db.execute(delete(TaskLog).where(TaskLog.task_id == task.id))
|
2026-08-07 15:38:01 +08:00
|
|
|
|
await db.delete(task)
|
2026-08-10 17:36:48 +08:00
|
|
|
|
|
|
|
|
|
|
# 5. 删除产品(product_messages 有 ON DELETE CASCADE 自动级联)
|
2026-08-07 15:38:01 +08:00
|
|
|
|
await db.delete(product)
|
|
|
|
|
|
await db.commit()
|