chore: fork from IRIS track 供 LICA 部门独立运行
- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update) - 组织隔离目标: LICA - 端口规划: 前端 8030 / 后端 8031 / 数据库 8032 - 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本) - 已排除工作区未提交改动,取干净的 192c8ee 状态
This commit is contained in:
146
backend/app/services/product_finalize_service.py
Normal file
146
backend/app/services/product_finalize_service.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""产品收口服务 — 管理员一键把产品修正为「已入库」或「已出库」(可反向互切纠错)。
|
||||
|
||||
落库语义与 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)
|
||||
Reference in New Issue
Block a user