Procházet zdrojové kódy

update jo efficient

stable1
CANCERYS\kw093 před 2 týdny
rodič
revize
c928139ba2
2 změnil soubory, kde provedl 210 přidání a 185 odebrání
  1. +207
    -182
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  2. +3
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt

+ 207
- 182
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Zobrazit soubor

@@ -2092,97 +2092,112 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List<Map<Str


open fun getAllJoPickOrders(bomType: String?, floor: String?): List<AllJoPickOrderResponse> { open fun getAllJoPickOrders(bomType: String?, floor: String?): List<AllJoPickOrderResponse> {
println("=== getAllJoPickOrders ===") println("=== getAllJoPickOrders ===")
val wallStartNs = System.nanoTime()
val timing = linkedMapOf<String, Long>()
fun <T> 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 { 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 pickOrderIds = releasedPickOrders.mapNotNull { it.id }


val normalizedFloorFilter = floor?.let { normalizeFloor(it) }?.takeIf { it.isNotBlank() } val normalizedFloorFilter = floor?.let { normalizeFloor(it) }?.takeIf { it.isNotBlank() }


// 2. 批量查询每个 pick order 的按楼层统计(若没有则跳过) // 2. 批量查询每个 pick order 的按楼层统计(若没有则跳过)
val floorCountsByPickOrderId: Map<Long, List<Map<String, Any?>>> = 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<String, Any?>())
.groupBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L }
} else {
emptyMap()
val floorCountsByPickOrderId: Map<Long, List<Map<String, Any?>>> = 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<String, Any?>())
.groupBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L }
} else {
emptyMap()
}
} }


// 2b. no-lot counts (lines with stock_out_line.inventoryLotLineId IS NULL) // 2b. no-lot counts (lines with stock_out_line.inventoryLotLineId IS NULL)
val noLotCountsByPickOrderId: Map<Long, Map<String, Any?>> = 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<String, Any?>())
.associateBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L }
} else emptyMap()
val noLotCountsByPickOrderId: Map<Long, Map<String, Any?>> = 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<String, Any?>())
.associateBy { (it["pickOrderId"] as? Number)?.toLong() ?: 0L }
} else emptyMap()
}


// 2c. suggested fail (rejected) counts per pick order // 2c. suggested fail (rejected) counts per pick order
val suggestedFailCountByPickOrderId: Map<Long, Int> = 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<String, Any?>()).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<Long, Int> = 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<String, Any?>()).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()) { if (pickOrderIds.isNotEmpty()) {
pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(pickOrderIds) pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(pickOrderIds)
.mapNotNull { pol -> .mapNotNull { pol ->
@@ -2193,138 +2208,148 @@ open fun getCompletedJobOrderPickOrders(completedDate: LocalDate?): List<Map<Str
} else { } else {
emptyMap() emptyMap()
} }
}


val jobOrderIdsForStockIn = releasedPickOrders.mapNotNull { it.jobOrder?.id }.distinct() val jobOrderIdsForStockIn = releasedPickOrders.mapNotNull { it.jobOrder?.id }.distinct()
val lotNoByJobOrderId =
val lotNoByJobOrderId = timed("loadStockInLinesMs") {
if (jobOrderIdsForStockIn.isNotEmpty()) { if (jobOrderIdsForStockIn.isNotEmpty()) {
stockInLineRepository.findAllByJobOrder_IdInAndDeletedFalse(jobOrderIdsForStockIn)
.mapNotNull { sil ->
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 { } else {
emptyMap() emptyMap()
} }
}


val jobTypeIds = releasedPickOrders.mapNotNull { it.jobOrder?.jobTypeId }.distinct() val jobTypeIds = releasedPickOrders.mapNotNull { it.jobOrder?.jobTypeId }.distinct()
val jobTypesById =
val jobTypesById = timed("loadJobTypesMs") {
if (jobTypeIds.isNotEmpty()) { if (jobTypeIds.isNotEmpty()) {
jobTypeRepository.findAllById(jobTypeIds).associateBy { it.id } jobTypeRepository.findAllById(jobTypeIds).associateBy { it.id }
} else { } else {
emptyMap() 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) { } 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}") println("❌ Error in getAllJoPickOrders: ${e.message}")
e.printStackTrace() e.printStackTrace()
emptyList() emptyList()


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Zobrazit soubor

@@ -316,15 +316,15 @@ open class JobOrderService(
// ✅ 使用 base unit 进行比较 // ✅ 使用 base unit 进行比较
if (baseAvailableQty >= baseReqQty) { if (baseAvailableQty >= baseReqQty) {
sufficientCount++ 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 { } else {
insufficientCount++ 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 { } else {
// 如果没有 itemId,视为不足 // 如果没有 itemId,视为不足
insufficientCount++ insufficientCount++
println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId")
//println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId")
} }
} }


Načítá se…
Zrušit
Uložit