fix: 三致命Bug—macro_status预计算+排除已完工脏读+flush落盘+递归深度限制10
This commit is contained in:
@ -51,6 +51,7 @@ class ProductResponse(BaseModel):
|
|||||||
parent_product_id: uuid.UUID | None
|
parent_product_id: uuid.UUID | None
|
||||||
current_location_id: str | None = None
|
current_location_id: str | None = None
|
||||||
current_location_name: str | None = None
|
current_location_name: str | None = None
|
||||||
|
macro_status: str | None = None # 🔧 后端预计算的任务树状态(免前端逐条展开)
|
||||||
overall_status: str | None = None
|
overall_status: str | None = None
|
||||||
status: str
|
status: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@ -384,6 +384,30 @@ async def get_all_products(
|
|||||||
location_ids = [p.current_location_id for p in products if p.current_location_id]
|
location_ids = [p.current_location_id for p in products if p.current_location_id]
|
||||||
name_map = _lookup_display_names(location_ids)
|
name_map = _lookup_display_names(location_ids)
|
||||||
|
|
||||||
|
# 🔧 批量预计算 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)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
ProductResponse(
|
ProductResponse(
|
||||||
id=p.id,
|
id=p.id,
|
||||||
@ -403,6 +427,7 @@ async def get_all_products(
|
|||||||
else name_map.get(p.current_location_id) if p.current_location_id
|
else name_map.get(p.current_location_id) if p.current_location_id
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
macro_status=macro_map.get(p.id) or p.status, # 优先任务树状态,兜底产品状态
|
||||||
overall_status=p.overall_status,
|
overall_status=p.overall_status,
|
||||||
status=p.status,
|
status=p.status,
|
||||||
created_at=p.created_at,
|
created_at=p.created_at,
|
||||||
|
|||||||
@ -74,6 +74,7 @@ async def _recalc_product_location(db: AsyncSession, product_id: uuid.UUID, comp
|
|||||||
sa_select(Task).where(
|
sa_select(Task).where(
|
||||||
Task.product_id == product_id,
|
Task.product_id == product_id,
|
||||||
Task.status == TASK_STATUS_WIP,
|
Task.status == TASK_STATUS_WIP,
|
||||||
|
Task.id != completed_task_id, # 🔧 排除刚刚完工的任务(避免脏读)
|
||||||
).order_by(Task.created_at.desc())
|
).order_by(Task.created_at.desc())
|
||||||
)
|
)
|
||||||
wip_tasks = task_result.scalars().all()
|
wip_tasks = task_result.scalars().all()
|
||||||
@ -94,6 +95,7 @@ async def _recalc_product_location(db: AsyncSession, product_id: uuid.UUID, comp
|
|||||||
|
|
||||||
if product.current_location_id != new_location:
|
if product.current_location_id != new_location:
|
||||||
product.current_location_id = new_location
|
product.current_location_id = new_location
|
||||||
|
await db.flush() # 🔧 确保位置更新落盘到当前事务
|
||||||
|
|
||||||
|
|
||||||
def _check_permission(task_assignee_id: str | None, operator_id: str | None, operator_role: str | None = None) -> None:
|
def _check_permission(task_assignee_id: str | None, operator_id: str | None, operator_role: str | None = None) -> None:
|
||||||
|
|||||||
@ -271,12 +271,24 @@ interface CommonProps {
|
|||||||
onViewRecords: (t: TaskResponse) => void;
|
onViewRecords: (t: TaskResponse) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_DEPTH = 10; // 🔧 防爆栈:递归深度上限
|
||||||
|
|
||||||
const TaskNode = memo(function TaskNode({
|
const TaskNode = memo(function TaskNode({
|
||||||
task, isBranch, common,
|
task, isBranch, common, depth = 0,
|
||||||
}: {
|
}: {
|
||||||
task: TaskResponse; isBranch: boolean;
|
task: TaskResponse; isBranch: boolean;
|
||||||
common: CommonProps;
|
common: CommonProps; depth?: number;
|
||||||
}) {
|
}) {
|
||||||
|
// 🔧 防循环引用/脏数据导致死循环
|
||||||
|
if (depth > MAX_DEPTH) {
|
||||||
|
return (
|
||||||
|
<div className="w-56 shrink-0 rounded-xl border-2 border-red-300 bg-red-50 p-3 text-center">
|
||||||
|
<p className="text-[10px] font-bold text-red-500">⚠ 树深度超限</p>
|
||||||
|
<p className="mt-1 text-[9px] text-red-400">请检查数据完整性</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const isActive = task.status?.toUpperCase() === TASK_STATUS.PENDING
|
const isActive = task.status?.toUpperCase() === TASK_STATUS.PENDING
|
||||||
|| task.status?.toUpperCase() === TASK_STATUS.WIP;
|
|| task.status?.toUpperCase() === TASK_STATUS.WIP;
|
||||||
|
|
||||||
@ -320,7 +332,7 @@ const TaskNode = memo(function TaskNode({
|
|||||||
{/* 横线连接 */}
|
{/* 横线连接 */}
|
||||||
<div className="absolute left-[108px] top-6 w-6 h-0.5 bg-gray-200" />
|
<div className="absolute left-[108px] top-6 w-6 h-0.5 bg-gray-200" />
|
||||||
<div className="ml-6">
|
<div className="ml-6">
|
||||||
<TaskNode task={child} isBranch={false} common={common} />
|
<TaskNode task={child} isBranch={false} common={common} depth={depth + 1} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -81,15 +81,18 @@ export default function AdminTasksPage() {
|
|||||||
loadProducts(keyword);
|
loadProducts(keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 🔧 本地计算产品综合状态(与表格列渲染逻辑100%统一) */
|
/** 🔧 产品综合状态:优先后端预计算的 macro_status,兜底本地 taskTree */
|
||||||
function calcProductStatus(p: ProductResponse): string {
|
function calcProductStatus(p: ProductResponse): string {
|
||||||
|
// 后端已批量预计算 macro_status(WIP/PENDING/COMPLETED)
|
||||||
|
if (p.macro_status) return p.macro_status;
|
||||||
|
// 兜底:展开过的流转树
|
||||||
const tree = taskTrees[p.serial_number];
|
const tree = taskTrees[p.serial_number];
|
||||||
if (tree?.task_tree?.length) {
|
if (tree?.task_tree?.length) {
|
||||||
if (tree.task_tree.some(t => t.status === "WIP")) return "WIP";
|
if (tree.task_tree.some(t => t.status === "WIP")) return "WIP";
|
||||||
if (tree.task_tree.every(t => t.status === "COMPLETED" || t.status === "ARCHIVED")) return "COMPLETED";
|
if (tree.task_tree.every(t => t.status === "COMPLETED" || t.status === "ARCHIVED")) return "COMPLETED";
|
||||||
return tree.task_tree[0].status;
|
return tree.task_tree[0].status;
|
||||||
}
|
}
|
||||||
return p.status; // 未展开流转树时兜底产品状态
|
return p.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 按订单分组 + 本地状态过滤 ----
|
// ---- 按订单分组 + 本地状态过滤 ----
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export interface ProductResponse {
|
|||||||
parent_product_id: string | null;
|
parent_product_id: string | null;
|
||||||
current_location_id: string | null;
|
current_location_id: string | null;
|
||||||
current_location_name: string | null;
|
current_location_name: string | null;
|
||||||
|
macro_status: string | null;
|
||||||
overall_status: string | null;
|
overall_status: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user