From 708bac101eed331b502087692399c93a14ed1e53 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 17 Apr 2026 20:31:12 +0800 Subject: [PATCH] proudctionprocesslist efficnet import and tab 0 no limit date --- .../entity/ProductProcessLineRepository.kt | 23 ++ .../entity/ProductProcessRepository.kt | 38 +++ .../service/ProductProcessService.kt | 220 ++++++++---------- 3 files changed, 160 insertions(+), 121 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt index 9cd98d3..d121b4b 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt @@ -6,6 +6,12 @@ import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import java.time.LocalDateTime +interface JobOrderLineStatusAggregate { + val jobOrderId: Long + val totalLines: Long + val doneLines: Long +} + @Repository interface ProductProcessLineRepository : JpaRepository { fun findByProductProcess_Id(productProcessId: Long): List @@ -23,6 +29,23 @@ interface ProductProcessLineRepository : JpaRepository """) fun findByProductProcess_IdInWithOperatorAndEquipment(@Param("ids") ids: List): List + @Query( + """ + SELECT + p.jobOrder.id AS jobOrderId, + COUNT(l.id) AS totalLines, + SUM(CASE WHEN l.status IN ('Completed', 'Pass') THEN 1 ELSE 0 END) AS doneLines + FROM ProductProcessLine l + JOIN l.productProcess p + WHERE l.deleted = false + AND p.deleted = false + AND p.jobOrder IS NOT NULL + AND p.jobOrder.id IN :jobOrderIds + GROUP BY p.jobOrder.id + """ + ) + fun countLineStatusByJobOrderIds(@Param("jobOrderIds") jobOrderIds: Collection): List + @Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId") fun findByProductProcess_IdWithEquipment(productProcessId: Long): List diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt index a257a18..46845ce 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt @@ -2,10 +2,18 @@ package com.ffii.fpsms.modules.productProcess.entity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaSpecificationExecutor +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository import java.util.* import java.time.LocalDate +interface JobOrderProcessAggregate { + val jobOrderId: Long + val maxDate: LocalDate? + val minPriority: Int? +} + @Repository interface ProductProcessRepository : JpaRepository, JpaSpecificationExecutor { fun findByProductProcessCode(code: String): Optional @@ -17,4 +25,34 @@ interface ProductProcessRepository : JpaRepository, JpaSpe fun findByBom_Id(bomId: Long): List fun findAllByDeletedIsFalseAndDate(date: LocalDate): List fun findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds: List): List + fun findByJobOrder_IdInAndDeletedIsFalseOrderByDateDescProductionPriorityAsc(jobOrderIds: Collection): List + + @Query( + """ + SELECT + p.jobOrder.id AS jobOrderId, + MAX(p.date) AS maxDate, + MIN(COALESCE(p.productionPriority, 2147483647)) AS minPriority + FROM ProductProcess p + WHERE p.deleted = false + AND p.jobOrder IS NOT NULL + AND COALESCE(p.jobOrder.isHidden, false) = false + AND p.jobOrder.status <> com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus.PLANNING + AND (:date IS NULL OR p.date = :date) + AND (:itemCode IS NULL OR LOWER(COALESCE(p.item.code, '')) LIKE LOWER(CONCAT('%', :itemCode, '%'))) + AND (:jobOrderCode IS NULL OR LOWER(COALESCE(p.jobOrder.code, '')) LIKE LOWER(CONCAT('%', :jobOrderCode, '%'))) + AND (:bomType IS NULL OR LOWER(COALESCE(p.bom.type, '')) = LOWER(:bomType)) + AND (:useBomIds = false OR p.bom.id IN :bomIds) + GROUP BY p.jobOrder.id + ORDER BY MAX(p.date) DESC, MIN(COALESCE(p.productionPriority, 2147483647)) ASC + """ + ) + fun findCandidateJobOrderAggregates( + @Param("date") date: LocalDate?, + @Param("itemCode") itemCode: String?, + @Param("jobOrderCode") jobOrderCode: String?, + @Param("bomType") bomType: String?, + @Param("useBomIds") useBomIds: Boolean, + @Param("bomIds") bomIds: Collection, + ): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index 145cf8d..e9347d6 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -1517,39 +1517,21 @@ open class ProductProcessService( val trimmedItemCode = itemCode?.trim()?.takeIf { it.isNotBlank() } val trimmedJobOrderCode = jobOrderCode?.trim()?.takeIf { it.isNotBlank() } - - // 1) 找出候选 jobOrder:用 date/itemCode/jobOrderCode/bomIds/type 先把数据缩小到今天等范围 - val candidateProcesses = if (date != null) { - productProcessRepository.findAllByDeletedIsFalseAndDate(date) - } else { - productProcessRepository.findAllByDeletedIsFalse() - } - - val filteredCandidateProcesses = candidateProcesses.filter { p -> - if (p.jobOrder?.isHidden == true) return@filter false - val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } - if (normalizedType != null) { - val currentType = p.bom?.type?.trim()?.lowercase() - if (currentType != normalizedType) return@filter false - } - if (trimmedItemCode != null) { - val code = p.item?.code - if (code == null || !code.contains(trimmedItemCode, ignoreCase = true)) return@filter false - } - if (trimmedJobOrderCode != null) { - val code = p.jobOrder?.code - if (code == null || !code.contains(trimmedJobOrderCode, ignoreCase = true)) return@filter false - } - if (bomIds != null && bomIds.isNotEmpty()) { - val bid = p.bom?.id - if (bid == null || !bomIds.contains(bid)) return@filter false - } - true - } - - val candidateJobOrderIds = filteredCandidateProcesses - .mapNotNull { it.jobOrder?.id } - .distinct() + val normalizedType = bomType?.trim()?.lowercase()?.takeIf { it.isNotBlank() } + val normalizedBomIds = bomIds?.distinct().orEmpty() + val useBomIds = normalizedBomIds.isNotEmpty() + val bomIdsForQuery = if (useBomIds) normalizedBomIds else listOf(-1L) + + // 1) DB 端先完成候選 jobOrder 篩選 + 分組排序,避免全表拉回 JVM + val candidateAggregates = productProcessRepository.findCandidateJobOrderAggregates( + date = date, + itemCode = trimmedItemCode, + jobOrderCode = trimmedJobOrderCode, + bomType = normalizedType, + useBomIds = useBomIds, + bomIds = bomIdsForQuery, + ) + val candidateJobOrderIds = candidateAggregates.map { it.jobOrderId } if (candidateJobOrderIds.isEmpty()) { return JobOrderProductProcessPageResponse( @@ -1560,29 +1542,22 @@ open class ProductProcessService( ) } - // 2) 把候选 jobOrder 下全部 productProcess / lines 取出来用于 qcReady 判断 - val allCandidateProcesses = productProcessRepository.findByJobOrder_IdInAndDeletedIsFalse(candidateJobOrderIds) - val allCandidateProcessIds = allCandidateProcesses.mapNotNull { it.id } - - val lines = if (allCandidateProcessIds.isNotEmpty()) { - productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(allCandidateProcessIds) - } else { - emptyList() - } + // 2) 批量查詢替代 N+1:stockIn / line 狀態都一次取回 + val stockInLineByJobOrderId = stockInLineRepository + .findAllByJobOrder_IdInAndDeletedFalse(candidateJobOrderIds) + .groupBy { it.jobOrder?.id } + .mapNotNull { (jobOrderId, rows) -> + if (jobOrderId == null) null else jobOrderId to rows.firstOrNull() + } + .toMap() - val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } - val processesByJobOrderId = allCandidateProcesses - .filter { it.jobOrder?.id != null } - .groupBy { it.jobOrder!!.id!! } + val lineStatusByJobOrderId = productProcessLineRepository + .countLineStatusByJobOrderIds(candidateJobOrderIds) + .associateBy { it.jobOrderId } - val jobOrders = jobOrderRepository.findAllById(candidateJobOrderIds) - val jobOrderById = jobOrders.associateBy { it.id } + val putawayFilter = putawayStatus?.trim()?.lowercase() + val allowPutaway = includePutaway == true - val stockInLineByJobOrderId = candidateJobOrderIds.associateWith { jobOrderId -> - stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId) - } - - // 3) 计算每个 jobOrder 的 qcReady,并做排序/分页 data class JobOrderQcKey( val jobOrderId: Long, val maxDate: LocalDate, @@ -1590,53 +1565,36 @@ open class ProductProcessService( val ready: Boolean ) - val qcKeys = candidateJobOrderIds.mapNotNull { jobOrderId -> - val jobOrder = jobOrderById[jobOrderId] ?: return@mapNotNull null - if (jobOrder.status == JobOrderStatus.PLANNING) return@mapNotNull null - - val processes = processesByJobOrderId[jobOrderId].orEmpty() - if (processes.isEmpty()) return@mapNotNull null - - val jobOrderLines = processes.flatMap { p -> - linesByProcessId[p.id ?: 0L].orEmpty() - } - - val allLinesDone = jobOrderLines.isNotEmpty() && - jobOrderLines.all { it.status == "Completed" || it.status == "Pass" } - + val qcKeys = candidateAggregates.mapNotNull { agg -> + val jobOrderId = agg.jobOrderId val stockInLine = stockInLineByJobOrderId[jobOrderId] - val stockStatus = stockInLine?.status - //val stockInEligibleForQc = stockStatus != "completed" && stockStatus != "rejected" - val allowPutaway = includePutaway == true - // 列表只排除 completed/rejected(和 planning),null stockInLine 也会显示在 tab0 + val stockStatus = stockInLine?.status?.trim()?.lowercase() + val includedInList = - // rejected 一般還是要排除(維持原規則) - stockStatus != "rejected" && - // completed 只有在不允許 putaway 時才排除 - (allowPutaway || stockStatus != "completed") + stockStatus != "rejected" && + (allowPutaway || stockStatus != "completed") - val putawayFilter = putawayStatus?.trim()?.lowercase() val includedByPutawayStatus = when (putawayFilter) { null, "", "all" -> true - "completed" -> stockStatus?.lowercase() == "completed" - "notcompleted", "not_completed", "not-completed", "waiting" -> stockStatus?.lowercase() != "completed" + "completed" -> stockStatus == "completed" + "notcompleted", "not_completed", "not-completed", "waiting" -> stockStatus != "completed" else -> true } - val ready = includedInList && allLinesDone && stockInLine != null - if (!includedInList || !includedByPutawayStatus) { - return@mapNotNull null - } + if (!includedInList || !includedByPutawayStatus) return@mapNotNull null - val maxDate = processes.mapNotNull { it.date }.maxOrNull() ?: LocalDate.MIN - val minPriority = processes.mapNotNull { it.productionPriority }.minOrNull() ?: Int.MAX_VALUE + val lineAggregate = lineStatusByJobOrderId[jobOrderId] + val totalLines = lineAggregate?.totalLines ?: 0L + val doneLines = lineAggregate?.doneLines ?: 0L + val allLinesDone = totalLines > 0 && doneLines == totalLines + val ready = includedInList && allLinesDone && stockInLine != null JobOrderQcKey( jobOrderId = jobOrderId, - maxDate = maxDate, - minPriority = minPriority, - ready = ready + maxDate = agg.maxDate ?: LocalDate.MIN, + minPriority = agg.minPriority ?: Int.MAX_VALUE, + ready = ready, ) } @@ -1656,59 +1614,79 @@ open class ProductProcessService( val pagedJobOrderIds = sortedQcKeys.subList(from, to).map { it.jobOrderId } val pagedJobOrderIdSet = pagedJobOrderIds.toSet() - // 4) 构造 content:返回 paged jobOrders 下的全部 productProcess - val pickOrdersByJobOrderId = pagedJobOrderIds.associateWith { joId -> - pickOrderRepository.findAllByJobOrder_Id(joId).firstOrNull() - } - val joPickOrdersByPickOrderId = pickOrdersByJobOrderId.mapNotNull { (joId, pick) -> - val pickId = pick?.id ?: return@mapNotNull null - joId to pickId - }.map { it.second }.distinct().associateWith { pickOrderId -> - joPickOrderRepository.findByPickOrderId(pickOrderId) + // 4) 只對當前頁資料做批量關聯查詢,並且排序/資料縮小盡量下推 DB + val contentProcesses = productProcessRepository + .findByJobOrder_IdInAndDeletedIsFalseOrderByDateDescProductionPriorityAsc(pagedJobOrderIds) + .filter { p -> p.jobOrder?.id != null && pagedJobOrderIdSet.contains(p.jobOrder!!.id!!) } + + val contentProcessIds = contentProcesses.mapNotNull { it.id } + val lines = if (contentProcessIds.isNotEmpty()) { + productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(contentProcessIds) + } else { + emptyList() } + val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } + + val jobOrderById = jobOrderRepository.findAllById(pagedJobOrderIds).associateBy { it.id } - val timeNeedToCompleteByBomId = pagedJobOrderIds.flatMap { _ -> - // placeholder, real cache built below - emptyList() - }.toSet() + val pickOrderByJobOrderId = mutableMapOf() + pickOrderRepository + .findAllByJobOrder_IdInAndDeletedFalseOrderByJobOrder_IdAscCreatedDesc(pagedJobOrderIds) + .forEach { po -> + val joId = po.jobOrder?.id ?: return@forEach + pickOrderByJobOrderId.putIfAbsent(joId, po) + } + + val pickOrderIds = pickOrderByJobOrderId.values.mapNotNull { it.id }.distinct() + val joPickOrdersByPickOrderId = if (pickOrderIds.isNotEmpty()) { + joPickOrderRepository.findByPickOrderIdIn(pickOrderIds) + .filter { !it.deleted && it.pickOrderId != null } + .groupBy { it.pickOrderId!! } + } else { + emptyMap() + } // 缓存:按 bomId 计算 timeNeedToComplete - val bomIdsInScope = allCandidateProcesses - .filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } + val bomIdsInScope = contentProcesses .mapNotNull { it.bom?.id } .distinct() - val timeNeedToCompleteCache = bomIdsInScope.associateWith { bomId -> - bomProcessRepository.findByBomId(bomId) - .filter { !it.deleted } - .sumOf { it.durationInMinute ?: 0 } + val timeNeedToCompleteCache = if (bomIdsInScope.isNotEmpty()) { + bomProcessRepository.sumDurationByBomIds(bomIdsInScope) + .associate { it.bomId to it.totalMinutes.toInt() } + } else { + emptyMap() } // 缓存:Uom(itemUom -> uomConversion.udfudesc) - val itemIdsInScope = allCandidateProcesses - .filter { it.jobOrder?.id != null && pagedJobOrderIdSet.contains(it.jobOrder!!.id!!) } + val itemIdsInScope = contentProcesses .mapNotNull { it.item?.id } .distinct() - val uomByItemId = itemIdsInScope.associateWith { itemId -> - val itemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId) - val bomUom = uomConversionRepository.findById(itemUom?.uom?.id ?: 0L).orElse(null) - bomUom?.udfudesc + val stockItemUoms = if (itemIdsInScope.isNotEmpty()) { + itemUomRepository.findAllByItemIdInAndStockUnitIsTrueAndDeletedIsFalse(itemIdsInScope) + } else { + emptyList() + } + val itemUomByItemId = mutableMapOf() + stockItemUoms.forEach { iu -> + val itemId = iu.item?.id ?: return@forEach + itemUomByItemId.putIfAbsent(itemId, iu) } - val contentProcesses = allCandidateProcesses - .filter { p -> p.jobOrder?.id != null && pagedJobOrderIdSet.contains(p.jobOrder!!.id!!) } - - val sortedContentProcesses = contentProcesses.sortedWith( - compareByDescending { it.date ?: LocalDate.MIN } - .thenBy { it.productionPriority ?: Int.MAX_VALUE } - ) + val uomIds = itemUomByItemId.values.mapNotNull { it.uom?.id }.distinct() + val uomById = if (uomIds.isNotEmpty()) { + uomConversionRepository.findAllByIdInAndDeletedFalse(uomIds).associateBy { it.id } + } else { + emptyMap() + } + val uomByItemId = itemUomByItemId.mapValues { (_, iu) -> uomById[iu.uom?.id]?.udfudesc } - val content = sortedContentProcesses.map { productProcess -> + val content = contentProcesses.map { productProcess -> val jobOrderId = productProcess.jobOrder?.id ?: 0L val jobOrder = jobOrderById[jobOrderId] val stockInLine = stockInLineByJobOrderId[jobOrderId] - val pickOrder = pickOrdersByJobOrderId[jobOrderId] + val pickOrder = pickOrderByJobOrderId[jobOrderId] val pickOrderId = pickOrder?.id val joPickOrdersList = if (pickOrderId != null) joPickOrdersByPickOrderId[pickOrderId].orEmpty() else emptyList()