- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update) - 组织隔离目标: LICA - 端口规划: 前端 8030 / 后端 8031 / 数据库 8032 - 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本) - 已排除工作区未提交改动,取干净的 192c8ee 状态
147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
"""产品收口服务 — 管理员一键把产品修正为「已入库」或「已出库」(可反向互切纠错)。
|
||
|
||
落库语义与 MOM 仓储回调(webhooks.mom_inbound / mom_outbound)保持一致:
|
||
1) 改 product.overall_status + product.status(映射为 ARCHIVED / OUTBOUND);
|
||
2) 写一条 warehouse_inbound / warehouse_outbound 的 task_log;
|
||
3) 幂等追加「扫码入库 / 扫码出库」WAREHOUSE 主线节点 + TaskRecord。
|
||
|
||
与 update_overall_status(仅改状态、面向主线负责人)不同:本服务面向管理员做收口,
|
||
会补全流转树收尾节点,使任务全景/详情/矩阵能正确显示入库/出库收口。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from fastapi import HTTPException
|
||
from sqlalchemy import select, update
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.time_utils import get_beijing_time
|
||
from app.models.product import Product
|
||
from app.models.task import Task, TaskRecord
|
||
from app.models.task_log import TaskLog
|
||
from app.services.product_service import get_product_by_serial
|
||
|
||
# target -> (product.status 映射码, 追加节点名, task_log.action_type)
|
||
_FINALIZE_MAP = {
|
||
"已入库": ("ARCHIVED", "扫码入库", "warehouse_inbound"),
|
||
"已出库": ("OUTBOUND", "扫码出库", "warehouse_outbound"),
|
||
}
|
||
|
||
|
||
async def _append_warehouse_node(db: AsyncSession, product: Product, task_name: str) -> None:
|
||
"""在流转树主干末尾幂等追加仓储收口主线节点 + TaskRecord(对齐 webhooks._append_warehouse_task)"""
|
||
existing = (
|
||
await db.execute(
|
||
select(Task.id).where(
|
||
Task.product_id == product.id,
|
||
Task.task_name == task_name,
|
||
).limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
if existing:
|
||
return
|
||
|
||
last_main = (
|
||
await db.execute(
|
||
select(Task)
|
||
.where(
|
||
Task.product_id == product.id,
|
||
(Task.parent_task_id.is_(None))
|
||
| (Task.task_type.in_(["TRANSFER", "RECOVERY"])),
|
||
)
|
||
.order_by(Task.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
|
||
node = Task(
|
||
product_id=product.id,
|
||
parent_task_id=last_main.id if last_main else None,
|
||
task_name=task_name,
|
||
assignee_id=None, # ★ 不能填非 UUID,否则前端头像解析报错导致节点跳过渲染
|
||
status="COMPLETED",
|
||
task_type="WAREHOUSE",
|
||
completed_at=get_beijing_time(),
|
||
remark=f"管理员收口:追加{task_name}收尾节点",
|
||
)
|
||
db.add(node)
|
||
await db.flush()
|
||
db.add(TaskRecord(task_id=node.id, remark=f"管理员收口:追加{task_name}收尾节点"))
|
||
|
||
|
||
async def finalize_product_status(
|
||
db: AsyncSession,
|
||
serial_number: str,
|
||
target: str,
|
||
current_user: dict | None,
|
||
note: str | None = None,
|
||
):
|
||
"""把产品整体收口到 target(已入库 / 已出库)。已处于目标态则幂等直接返回。"""
|
||
target = (target or "").strip()
|
||
if target not in _FINALIZE_MAP:
|
||
raise HTTPException(status_code=400, detail="收口目标状态仅支持:已入库 / 已出库")
|
||
|
||
product = (
|
||
await db.execute(select(Product).where(Product.serial_number == serial_number))
|
||
).scalar_one_or_none()
|
||
if product is None:
|
||
raise HTTPException(status_code=404, detail=f"未找到序列号为 {serial_number} 的产品")
|
||
|
||
# 幂等:已在目标状态则不动(避免重复写日志/节点)
|
||
if product.overall_status == target:
|
||
return await get_product_by_serial(db, serial_number)
|
||
|
||
# 管理员强收口:仍有在产/待接收任务的,一并结束为完工收口,避免宏观口径分裂
|
||
res = await db.execute(
|
||
update(Task)
|
||
.where(Task.product_id == product.id, Task.status.in_(["WIP", "PENDING"]))
|
||
.values(
|
||
status="COMPLETED",
|
||
received_at=get_beijing_time(),
|
||
completed_at=get_beijing_time(),
|
||
)
|
||
)
|
||
ended_active = res.rowcount or 0
|
||
|
||
status_code, node_name, action_type = _FINALIZE_MAP[target]
|
||
product.overall_status = target
|
||
product.status = status_code
|
||
product.current_location_id = "virtual_warehouse"
|
||
|
||
# task_log 绑目标任务:优先「在库」任务,其次该产品最新任务(对齐 webhook)
|
||
log_task = (
|
||
await db.execute(
|
||
select(Task)
|
||
.where(Task.product_id == product.id, Task.task_name.ilike("%在库%"))
|
||
.order_by(Task.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
if log_task is None:
|
||
log_task = (
|
||
await db.execute(
|
||
select(Task).where(Task.product_id == product.id)
|
||
.order_by(Task.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
|
||
operator = (current_user or {}).get("username") or "virtual_warehouse"
|
||
remark = f"管理员[{operator}]收口为{target}"
|
||
if ended_active:
|
||
remark += f";一并结束{ended_active}个进行中/待接收任务"
|
||
if note and note.strip():
|
||
remark += f";备注:{note.strip()}"
|
||
if log_task is not None:
|
||
db.add(
|
||
TaskLog(
|
||
task_id=log_task.id,
|
||
operator_id=str(operator)[:64],
|
||
action_type=action_type,
|
||
remark=remark,
|
||
)
|
||
)
|
||
|
||
await _append_warehouse_node(db, product, node_name)
|
||
await db.commit()
|
||
return await get_product_by_serial(db, serial_number)
|