Browse Source

proudctionprocesslist efficnet import and tab 0 no limit date

master
CANCERYS\kw093 6 hours ago
parent
commit
708bac101e
3 changed files with 160 additions and 121 deletions
  1. +23
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt
  2. +38
    -0
      src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt
  3. +99
    -121
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt

+ 23
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt View File

@@ -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<ProductProcessLine, Long> {
fun findByProductProcess_Id(productProcessId: Long): List<ProductProcessLine>
@@ -23,6 +29,23 @@ interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long>
""")
fun findByProductProcess_IdInWithOperatorAndEquipment(@Param("ids") ids: List<Long>): List<ProductProcessLine>

@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<Long>): List<JobOrderLineStatusAggregate>

@Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId")
fun findByProductProcess_IdWithEquipment(productProcessId: Long): List<ProductProcessLine>



+ 38
- 0
src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessRepository.kt View File

@@ -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<ProductProcess, Long>, JpaSpecificationExecutor<ProductProcess> {
fun findByProductProcessCode(code: String): Optional<ProductProcess>
@@ -17,4 +25,34 @@ interface ProductProcessRepository : JpaRepository<ProductProcess, Long>, JpaSpe
fun findByBom_Id(bomId: Long): List<ProductProcess>
fun findAllByDeletedIsFalseAndDate(date: LocalDate): List<ProductProcess>
fun findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds: List<Long>): List<ProductProcess>
fun findByJobOrder_IdInAndDeletedIsFalseOrderByDateDescProductionPriorityAsc(jobOrderIds: Collection<Long>): List<ProductProcess>

@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<Long>,
): List<JobOrderProcessAggregate>
}

+ 99
- 121
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt View File

@@ -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<Long>()
}.toSet()
val pickOrderByJobOrderId = mutableMapOf<Long, com.ffii.fpsms.modules.pickOrder.entity.PickOrder>()
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<Long, com.ffii.fpsms.modules.master.entity.ItemUom>()
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<ProductProcess> { 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()



Loading…
Cancel
Save