Files
track-LICA/backend/app/services/product_finalize_service.py
duxingchen 3286a11bc7 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 状态
2026-09-21 15:56:52 +08:00

147 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""产品收口服务 — 管理员一键把产品修正为「已入库」或「已出库」(可反向互切纠错)。
落库语义与 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)