diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index b41122a..38fee20 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -2092,97 +2092,112 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List { println("=== getAllJoPickOrders ===") + val wallStartNs = System.nanoTime() + val timing = linkedMapOf() + fun timed(name: String, block: () -> T): T { + val t0 = System.nanoTime() + val v = block() + timing[name] = ((System.nanoTime() - t0) / 1_000_000) + return v + } return try { - val releasedPickOrders = pickOrderRepository.findAllByStatusAndDeletedFalse( - PickOrderStatus.RELEASED - ).filter { pickOrder -> - pickOrder.jobOrder != null + val releasedPickOrders = timed("loadReleasedPickOrdersMs") { + pickOrderRepository.findAllReleasedJoWorkbenchPickOrders( + status = PickOrderStatus.RELEASED, + completedStatus = JobOrderStatus.COMPLETED, + ) } - println("Found ${releasedPickOrders.size} released job order pick orders") + // println("Found ${releasedPickOrders.size} released job order pick orders") val pickOrderIds = releasedPickOrders.mapNotNull { it.id } val normalizedFloorFilter = floor?.let { normalizeFloor(it) }?.takeIf { it.isNotBlank() } // 2. 批量查询每个 pick order 的按楼层统计(若没有则跳过) - val floorCountsByPickOrderId: Map>> = if (pickOrderIds.isNotEmpty()) { - val sql = """ - SELECT - po.id AS pickOrderId, - COALESCE(NULLIF(TRIM(w.store_id), ''), SUBSTRING_INDEX(w.code, '-', 1)) AS floorKey, - COUNT(DISTINCT pol.id) AS totalCount, - COUNT(DISTINCT CASE - WHEN LOWER(sol.status) = 'completed' THEN pol.id - WHEN pol.status = 'COMPLETED' THEN pol.id - END) AS finishedCount - FROM fpsmsdb.pick_order po - JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false - LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId AND spl.deleted = false - LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id AND ill.deleted = false - LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId - LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false - WHERE po.deleted = false - AND po.status = 'RELEASED' - AND po.joId IS NOT NULL - AND po.id IN (${pickOrderIds.joinToString(",")}) - AND w.id IS NOT NULL - GROUP BY po.id, floorKey - """.trimIndent() - jdbcDao.queryForList(sql, emptyMap()) - .groupBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L } - } else { - emptyMap() + val floorCountsByPickOrderId: Map>> = timed("queryFloorCountsMs") { + if (pickOrderIds.isNotEmpty()) { + val sql = """ + SELECT + po.id AS pickOrderId, + COALESCE(NULLIF(TRIM(w.store_id), ''), SUBSTRING_INDEX(w.code, '-', 1)) AS floorKey, + COUNT(DISTINCT pol.id) AS totalCount, + COUNT(DISTINCT CASE + WHEN LOWER(sol.status) = 'completed' THEN pol.id + WHEN pol.status = 'COMPLETED' THEN pol.id + END) AS finishedCount + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId AND spl.deleted = false + LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id AND ill.deleted = false + LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + WHERE po.deleted = false + AND po.status = 'RELEASED' + AND po.joId IS NOT NULL + AND po.id IN (${pickOrderIds.joinToString(",")}) + AND w.id IS NOT NULL + GROUP BY po.id, floorKey + """.trimIndent() + jdbcDao.queryForList(sql, emptyMap()) + .groupBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L } + } else { + emptyMap() + } } // 2b. no-lot counts (lines with stock_out_line.inventoryLotLineId IS NULL) - val noLotCountsByPickOrderId: Map> = if (pickOrderIds.isNotEmpty()) { - val sql = """ - SELECT - po.id AS pickOrderId, - COUNT(DISTINCT pol.id) AS totalCount, - COUNT(DISTINCT CASE - WHEN LOWER(sol.status) = 'completed' THEN pol.id - WHEN pol.status = 'COMPLETED' THEN pol.id - END) AS finishedCount - FROM fpsmsdb.pick_order po - JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false - JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false - WHERE po.deleted = false - AND po.status = 'RELEASED' - AND po.joId IS NOT NULL - AND po.id IN (${pickOrderIds.joinToString(",")}) - AND sol.inventoryLotLineId IS NULL - GROUP BY po.id - """.trimIndent() - jdbcDao.queryForList(sql, emptyMap()) - .associateBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L } - } else emptyMap() + val noLotCountsByPickOrderId: Map> = timed("queryNoLotCountsMs") { + if (pickOrderIds.isNotEmpty()) { + val sql = """ + SELECT + po.id AS pickOrderId, + COUNT(DISTINCT pol.id) AS totalCount, + COUNT(DISTINCT CASE + WHEN LOWER(sol.status) = 'completed' THEN pol.id + WHEN pol.status = 'COMPLETED' THEN pol.id + END) AS finishedCount + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + WHERE po.deleted = false + AND po.status = 'RELEASED' + AND po.joId IS NOT NULL + AND po.id IN (${pickOrderIds.joinToString(",")}) + AND sol.inventoryLotLineId IS NULL + GROUP BY po.id + """.trimIndent() + jdbcDao.queryForList(sql, emptyMap()) + .associateBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L } + } else emptyMap() + } // 2c. suggested fail (rejected) counts per pick order - val suggestedFailCountByPickOrderId: Map = if (pickOrderIds.isNotEmpty()) { - val sql = """ - SELECT - po.id AS pickOrderId, - COUNT(DISTINCT sol.id) AS rejectedCount - FROM fpsmsdb.pick_order po - JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false - JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false - WHERE po.deleted = false - AND po.status = 'RELEASED' - AND po.joId IS NOT NULL - AND po.id IN (${pickOrderIds.joinToString(",")}) - AND LOWER(sol.status) = 'rejected' - GROUP BY po.id - """.trimIndent() - jdbcDao.queryForList(sql, emptyMap()).associate { row -> - val id = (row["pickOrderId"] as? Number)?.toLong() ?: 0L - val cnt = (row["rejectedCount"] as? Number)?.toInt() ?: 0 - id to cnt - } - } else emptyMap() + val suggestedFailCountByPickOrderId: Map = timed("querySuggestedFailCountsMs") { + if (pickOrderIds.isNotEmpty()) { + val sql = """ + SELECT + po.id AS pickOrderId, + COUNT(DISTINCT sol.id) AS rejectedCount + FROM fpsmsdb.pick_order po + JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id AND pol.deleted = false + JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = false + WHERE po.deleted = false + AND po.status = 'RELEASED' + AND po.joId IS NOT NULL + AND po.id IN (${pickOrderIds.joinToString(",")}) + AND LOWER(sol.status) = 'rejected' + GROUP BY po.id + """.trimIndent() + jdbcDao.queryForList(sql, emptyMap()).associate { row -> + val id = (row["pickOrderId"] as? Number)?.toLong() ?: 0L + val cnt = (row["rejectedCount"] as? Number)?.toInt() ?: 0 + id to cnt + } + } else emptyMap() + } - val linesByPickOrderId = + val linesByPickOrderId = timed("loadPickOrderLinesMs") { if (pickOrderIds.isNotEmpty()) { pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(pickOrderIds) .mapNotNull { pol -> @@ -2193,138 +2208,148 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List - val joid = sil.jobOrder?.id ?: return@mapNotNull null - joid to sil - } - .groupBy({ it.first }, { it.second }) - .mapValues { (_, sils) -> - sils.minByOrNull { it.id ?: Long.MAX_VALUE }?.lotNo - } + stockInLineRepository.findFirstLotNoByJobOrderIds(jobOrderIdsForStockIn) + .associate { it.jobOrderId to it.lotNo } } else { emptyMap() } + } val jobTypeIds = releasedPickOrders.mapNotNull { it.jobOrder?.jobTypeId }.distinct() - val jobTypesById = + val jobTypesById = timed("loadJobTypesMs") { if (jobTypeIds.isNotEmpty()) { jobTypeRepository.findAllById(jobTypeIds).associateBy { it.id } } else { emptyMap() } - val jobOrderPickOrders = releasedPickOrders.mapNotNull { pickOrder -> - println("Processing pick order: ${pickOrder.id}, code: ${pickOrder.code}") - val jobOrder = pickOrder.jobOrder - if (jobOrder == null) { - println("❌ Pick order ${pickOrder.id} has no job order") - return@mapNotNull null - } - if (jobOrder.isHidden == true) { - return@mapNotNull null - } - if (jobOrder.status == JobOrderStatus.COMPLETED) { - return@mapNotNull null - } + } + val jobOrderPickOrders = timed("buildResponseMs") { + releasedPickOrders.mapNotNull { pickOrder -> + // println("Processing pick order: ${pickOrder.id}, code: ${pickOrder.code}") + val jobOrder = pickOrder.jobOrder + if (jobOrder == null) { + println("❌ Pick order ${pickOrder.id} has no job order") + return@mapNotNull null + } + if (jobOrder.isHidden == true) { + return@mapNotNull null + } + if (jobOrder.status == JobOrderStatus.COMPLETED) { + return@mapNotNull null + } - println("Job order found: ${jobOrder.id}, code: ${jobOrder.code}") + //println("Job order found: ${jobOrder.id}, code: ${jobOrder.code}") - val bom = jobOrder.bom + val bom = jobOrder.bom - // 按 bom.type 过滤:null / blank 表示不过滤(全部) - val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } - if (normalizedType != null) { - val currentType = bom?.type?.trim()?.lowercase() - if (currentType != normalizedType) return@mapNotNull null - } - println("BOM found: ${bom?.id}") + // 按 bom.type 过滤:null / blank 表示不过滤(全部) + val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } + if (normalizedType != null) { + val currentType = bom?.type?.trim()?.lowercase() + if (currentType != normalizedType) return@mapNotNull null + } + // println("BOM found: ${bom?.id}") - val item = bom?.item - if (item == null) { - println("❌ BOM ${bom?.id} has no item") - return@mapNotNull null - } + val item = bom?.item + if (item == null) { + println("❌ BOM ${bom?.id} has no item") + return@mapNotNull null + } - println("Item found: ${item.id}, name: ${item.name}") + //println("Item found: ${item.id}, name: ${item.name}") - val uom = bom.outputQtyUom - if (uom == null) { - println("❌ BOM ${bom.id} has no uom") - return@mapNotNull null - } + val uom = bom.outputQtyUom + if (uom == null) { + println("❌ BOM ${bom.id} has no uom") + return@mapNotNull null + } - // println("UOM found: ${uom.id}, code: ${uom.code}") + // println("UOM found: ${uom.id}, code: ${uom.code}") - val poId = pickOrder.id ?: return@mapNotNull null - val pickOrderLines = linesByPickOrderId[poId].orEmpty() - val finishedLines = pickOrderLines.count { it.status == PickOrderLineStatus.COMPLETED } - val jobOrderType = jobOrder.jobTypeId?.let { jobTypesById[it] } - val joId = jobOrder.id ?: return@mapNotNull null - val lotNo = lotNoByJobOrderId[joId] - println("✅ Building response for pick order ${pickOrder.id}") - val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row -> - com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( - floor = normalizeFloor((row["floorKey"] as? String).orEmpty()), - finishedCount = (row["finishedCount"] as? Number)?.toInt() ?: 0, - totalCount = (row["totalCount"] as? Number)?.toInt() ?: 0 - ) - }.orEmpty() - - val noLotRow = noLotCountsByPickOrderId[pickOrder.id] - val noLotPickCount = if (noLotRow != null) { - com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( - floor = "NO_LOT", - finishedCount = (noLotRow["finishedCount"] as? Number)?.toInt() ?: 0, - totalCount = (noLotRow["totalCount"] as? Number)?.toInt() ?: 0 - ) - } else null - - // Backend filtering by floor: - // - When selecting a specific floor (2F/3F/4F), show ONLY remaining on that floor. - // - When selecting NO_LOT, show ONLY remaining for no-lot items. - if (normalizedFloorFilter != null) { - if (normalizedFloorFilter == "NO_LOT") { - val hasNoLotRemaining = (noLotPickCount?.let { it.totalCount - it.finishedCount } ?: 0) > 0 - if (!hasNoLotRemaining) return@mapNotNull null - } else { - val hasRemainingOnFloor = floorPickCounts.any { c -> - c.floor == normalizedFloorFilter && (c.totalCount - c.finishedCount) > 0 + val poId = pickOrder.id ?: return@mapNotNull null + val pickOrderLines = linesByPickOrderId[poId].orEmpty() + val finishedLines = pickOrderLines.count { it.status == PickOrderLineStatus.COMPLETED } + val jobOrderType = jobOrder.jobTypeId?.let { jobTypesById[it] } + val joId = jobOrder.id ?: return@mapNotNull null + val lotNo = lotNoByJobOrderId[joId] + // println("✅ Building response for pick order ${pickOrder.id}") + val floorPickCounts = floorCountsByPickOrderId[pickOrder.id]?.map { row -> + com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( + floor = normalizeFloor((row["floorKey"] as? String).orEmpty()), + finishedCount = (row["finishedCount"] as? Number)?.toInt() ?: 0, + totalCount = (row["totalCount"] as? Number)?.toInt() ?: 0 + ) + }.orEmpty() + + val noLotRow = noLotCountsByPickOrderId[pickOrder.id] + val noLotPickCount = if (noLotRow != null) { + com.ffii.fpsms.modules.jobOrder.web.model.FloorPickCountDto( + floor = "NO_LOT", + finishedCount = (noLotRow["finishedCount"] as? Number)?.toInt() ?: 0, + totalCount = (noLotRow["totalCount"] as? Number)?.toInt() ?: 0 + ) + } else null + + // Backend filtering by floor: + // - When selecting a specific floor (2F/3F/4F), show ONLY remaining on that floor. + // - When selecting NO_LOT, show ONLY remaining for no-lot items. + if (normalizedFloorFilter != null) { + if (normalizedFloorFilter == "NO_LOT") { + val hasNoLotRemaining = (noLotPickCount?.let { it.totalCount - it.finishedCount } ?: 0) > 0 + if (!hasNoLotRemaining) return@mapNotNull null + } else { + val hasRemainingOnFloor = floorPickCounts.any { c -> + c.floor == normalizedFloorFilter && (c.totalCount - c.finishedCount) > 0 + } + if (!hasRemainingOnFloor) return@mapNotNull null } - if (!hasRemainingOnFloor) return@mapNotNull null } - } - val suggestedFailCount = suggestedFailCountByPickOrderId[pickOrder.id] ?: 0 - AllJoPickOrderResponse( - id = pickOrder.id ?: 0L, - pickOrderId = pickOrder.id, - pickOrderCode = pickOrder.code, - jobOrderId = jobOrder.id, - jobOrderCode = jobOrder.code, - jobOrderTypeId = jobOrder.jobTypeId, - jobOrderType = jobOrderType?.name, - itemId = item.id ?: 0L, - itemName = item.name ?: "", - reqQty = jobOrder.reqQty ?: BigDecimal.ZERO, - //uomId = bom.outputQtyUom?.id : 0L, - uomId = 0, - uomName = bom?.outputQtyUom ?: "", - lotNo = lotNo, - jobOrderStatus = jobOrder.status?.value ?: "", - finishedPickOLineCount = finishedLines, - floorPickCounts = floorPickCounts, - noLotPickCount = noLotPickCount, - suggestedFailCount = suggestedFailCount - ) + val suggestedFailCount = suggestedFailCountByPickOrderId[pickOrder.id] ?: 0 + AllJoPickOrderResponse( + id = pickOrder.id ?: 0L, + pickOrderId = pickOrder.id, + pickOrderCode = pickOrder.code, + jobOrderId = jobOrder.id, + jobOrderCode = jobOrder.code, + jobOrderTypeId = jobOrder.jobTypeId, + jobOrderType = jobOrderType?.name, + itemId = item.id ?: 0L, + itemName = item.name ?: "", + reqQty = jobOrder.reqQty ?: BigDecimal.ZERO, + //uomId = bom.outputQtyUom?.id : 0L, + uomId = 0, + uomName = bom?.outputQtyUom ?: "", + lotNo = lotNo, + jobOrderStatus = jobOrder.status?.value ?: "", + finishedPickOLineCount = finishedLines, + floorPickCounts = floorPickCounts, + noLotPickCount = noLotPickCount, + suggestedFailCount = suggestedFailCount + ) + } } - - println("Returning ${jobOrderPickOrders.size} released job order pick orders") - jobOrderPickOrders.sortedByDescending { it.id } + val sorted = timed("sortResponseMs") { + jobOrderPickOrders.sortedByDescending { it.id } + } + val totalMs = (System.nanoTime() - wallStartNs) / 1_000_000 + println( + "JO_ALL_PICK_ORDERS_TIMING totalMs=$totalMs released=${releasedPickOrders.size} " + + timing.entries.joinToString(" ") { "${it.key}=${it.value}" }, + ) + // println("Returning ${jobOrderPickOrders.size} released job order pick orders") + sorted } catch (e: Exception) { + val totalMs = (System.nanoTime() - wallStartNs) / 1_000_000 + println( + "JO_ALL_PICK_ORDERS_TIMING totalMs=$totalMs (error) " + + timing.entries.joinToString(" ") { "${it.key}=${it.value}" }, + ) println("❌ Error in getAllJoPickOrders: ${e.message}") e.printStackTrace() emptyList() diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index e8ec7c0..44900d7 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -316,15 +316,15 @@ open class JobOrderService( // ✅ 使用 base unit 进行比较 if (baseAvailableQty >= baseReqQty) { sufficientCount++ - println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") + //println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } else { insufficientCount++ - println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") + //println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } } else { // 如果没有 itemId,视为不足 insufficientCount++ - println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") + //println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") } }