From d4af229304105598e4aa2ba09b2705047bfc8aea Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Wed, 6 May 2026 16:31:30 +0800 Subject: [PATCH] update do search and jo bom name coe --- .../service/DeliveryOrderService.kt | 304 ++++++++++++------ .../service/DoWorkbenchMainService.kt | 24 +- .../service/TruckLaneSearchSpec.kt | 55 ++++ .../web/DeliveryOrderController.kt | 22 +- .../web/models/ReleaseDoRequest.kt | 4 +- .../service/JoWorkbenchMainService.kt | 33 +- .../jobOrder/web/JobOrderController.kt | 2 +- .../web/model/CreateJobOrderRequest.kt | 38 ++- .../01_do_truck_search_indexes.sql | 19 ++ 9 files changed, 364 insertions(+), 137 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt create mode 100644 src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 1f50f2b..13b9a50 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -122,6 +122,22 @@ open class DeliveryOrderService( private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, private val itemsRepository: ItemsRepository, ) { + /** + * 樓層篩選:2F → P07/P06D(車線- 族);4F → P06B(P06B_ 族);全部 → 三者。 + * 車線-X 仍屬該 DO 的 supplier,故 P06B+車線-X 不會出現在 2F,P06D+車線-X 不會出現在 4F。 + */ + private fun allowedSupplierCodesForFloor(floor: String?): List { + val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty() + if (f.isEmpty() || f == "ALL" || f == "All") { + return listOf("P06B", "P07", "P06D") + } + return when (f) { + "2F" -> listOf("P07", "P06D") + "4F" -> listOf("P06B") + else -> listOf("P06B", "P07", "P06D") + } + } + open fun searchDoLiteByPage( code: String?, shopName: String?, @@ -129,7 +145,8 @@ open class DeliveryOrderService( estimatedArrivalDate: LocalDateTime?, pageNum: Int?, pageSize: Int?, - truckLanceCode: String? + truckLanceCode: String?, + floor: String? = null, ): RecordsRes { val page = (pageNum ?: 1) - 1 @@ -142,81 +159,26 @@ open class DeliveryOrderService( val searchTruckLanceCode = truckLanceCode?.ifBlank { null }?.lowercase() if (searchTruckLanceCode != null) { - println("DEBUG: Filtering by truckLanceCode: $searchTruckLanceCode") - - // ✅ 优化:先从 truck 表找到匹配的 truck,获取 Store_id 和 shopId - val matchingTrucks = truckRepository.findAllByTruckLanceCodeContainingAndDeletedFalse(searchTruckLanceCode) - println("DEBUG: Found ${matchingTrucks.size} matching trucks") - - // 收集所有匹配的 Store_id 和 shopId - val matchingStoreIds = matchingTrucks.mapNotNull { it.storeId }.distinct() - val matchingShopIds = matchingTrucks.mapNotNull { it.shop?.id }.distinct() - - println("DEBUG: Matching storeIds: $matchingStoreIds") - println("DEBUG: Matching shopIds: ${matchingShopIds.size} shops") - - // 根据 Store_id 确定需要过滤的 supplier codes - // Store_id = "2F" → supplier code != "P06B" - // Store_id = "4F" → supplier code = "P06B" - val supplierCodesToInclude = mutableSetOf() - val supplierCodesToExclude = mutableSetOf() - - if (matchingStoreIds.contains("2F")) { - // 2F 只关心 P07 / P06D - supplierCodesToInclude.addAll(listOf("P07", "P06D")) - // 同时排除 P06B,避免混在 2F 结果里 - supplierCodesToExclude.add("P06B") - } - if (matchingStoreIds.contains("4F")) { - // 4F 只关心 P06B - supplierCodesToInclude.add("P06B") - } - - // 查询符合条件的 DeliveryOrder(根据 supplier code 和 shopId 预过滤) - // 注意:这里需要在 Repository 层面添加 supplier code 过滤 - // 或者先查询所有,然后在代码层面过滤 + println("DEBUG: Filtering by truckLanceCode: $searchTruckLanceCode, floor=$floor") - val allResult = deliveryOrderRepository.searchDoLitePage( + val allowedForFloor = allowedSupplierCodesForFloor(floor) + val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes( code = code?.ifBlank { null }, shopName = shopName?.ifBlank { null }, status = statusEnum, etaStart = etaStart, etaEnd = etaEnd, - pageable = PageRequest.of(0, 100000) + allowedSupplierCodes = allowedForFloor, + pageable = PageRequest.of(0, 100_000), ) - println("DEBUG: Total records from DB before filtering: ${allResult.totalElements}") + println("DEBUG: Total records from DB (supplier+floor filter): ${allResult.totalElements}") - // ✅ 优化1: 批量查询所有 DeliveryOrder 实体 val deliveryOrderIds = allResult.content.mapNotNull { it.id } val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds) .associateBy { it.id } - println("DEBUG: Loaded ${deliveryOrdersMap.size} delivery orders in batch") - - // ✅ 优化2: 预过滤 - 根据 supplier code 和 shopId 过滤 - val preFilteredContent = allResult.content.filter { info -> - val deliveryOrder = deliveryOrdersMap[info.id] - val supplierCode = deliveryOrder?.supplier?.code - - // 检查 supplier code 是否匹配 - val supplierMatches = when { - supplierCodesToExclude.contains(supplierCode) -> false - supplierCodesToInclude.isNotEmpty() && !supplierCodesToInclude.contains(supplierCode) -> false - else -> true - } - - // 如果提供了 shopId 过滤,也检查 shopId - val shopMatches = if (matchingShopIds.isNotEmpty()) { - deliveryOrder?.shop?.id in matchingShopIds - } else { - true // 如果没有匹配的 shopId,不过滤 - } - - supplierMatches && shopMatches - } - - println("DEBUG: Pre-filtered records: ${preFilteredContent.size} (from ${allResult.content.size})") + val preFilteredContent = allResult.content // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录) val shopIdAndDatePairs = preFilteredContent.mapNotNull { info -> @@ -243,15 +205,8 @@ open class DeliveryOrderService( // ✅ 优化4: 批量查询所有需要的 Truck val truckCache = mutableMapOf, Truck?>() shopIdAndDatePairs.forEach { (shopId, preferredFloor, dayAbbr) -> - val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, preferredFloor, dayAbbr) - val matchedTruck = if (trucks.isEmpty()) { - truckRepository.findByShopIdAndDeletedFalse(shopId) - .filter { it.storeId == preferredFloor } - .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } else { - trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } - truckCache[Triple(shopId, preferredFloor, dayAbbr)] = matchedTruck + truckCache[Triple(shopId, preferredFloor, dayAbbr)] = + resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr) } println("DEBUG: Cached ${truckCache.size} truck lookups") @@ -263,7 +218,7 @@ open class DeliveryOrderService( val preferredFloor = when (supplierCode) { "P06B" -> "4F" "P07", "P06D" -> "2F" - else -> null + else -> "2F" } val shop = deliveryOrder?.shop val shopId = shop?.id @@ -314,7 +269,7 @@ open class DeliveryOrderService( return RecordsRes(paginatedRecords, totalCount) } else { // 未提供 truckLanceCode:在 DB 層依允許的供應商分頁,避免先取 10 筆再過濾導致每頁顯示少於 pageSize - val allowedSupplierCodes = listOf("P06B", "P07", "P06D") + val allowedSupplierCodes = allowedSupplierCodesForFloor(floor) val result = deliveryOrderRepository.searchDoLitePageWithSupplierCodes( code = code?.ifBlank { null }, shopName = shopName?.ifBlank { null }, @@ -341,17 +296,7 @@ open class DeliveryOrderService( if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) { val targetDate = estimatedArrivalDate.toLocalDate() val dayAbbr = getDayOfWeekAbbr(targetDate) - val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, preferredFloor, dayAbbr) - - val matchedTruck = if (trucks.isEmpty()) { - truckRepository.findByShopIdAndDeletedFalse(shopId) - .filter { it.storeId == preferredFloor } - .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } else { - trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } - - matchedTruck?.truckLanceCode + resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode } else { null } @@ -375,6 +320,57 @@ open class DeliveryOrderService( } + /** + * DO 輕量搜尋 v2:車線關鍵字正規化(`車線-X`/`x`/`車線-` 前綴含未指派)、 + * 以允許供應商 + 分批掃描取代單次 100000 筆載入;無車線條件時等同 [searchDoLiteByPage] 無車線分支。 + */ + open fun searchDoLiteByPageV2( + code: String?, + shopName: String?, + status: String?, + estimatedArrivalDate: LocalDateTime?, + pageNum: Int?, + pageSize: Int?, + truckLanceCode: String?, + floor: String? = null, + ): RecordsRes { + val mode = TruckLaneSearchSpec.parse(truckLanceCode) + if (mode is TruckLaneSearchSpec.Mode.NoFilter) { + return searchDoLiteByPage( + code, + shopName, + status, + estimatedArrivalDate, + pageNum, + pageSize, + null, + floor, + ) + } + val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1 + val size = (pageSize ?: 10).coerceAtLeast(1).coerceAtMost(10_000) + val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } } + val etaStart = estimatedArrivalDate + val etaEnd = estimatedArrivalDate?.plusDays(1) + + val lanePredicate: (String?) -> Boolean = { lane -> TruckLaneSearchSpec.matches(mode, lane) } + val matched = scanDoLiteSupplierFilteredWithLanePredicate( + code = code?.ifBlank { null }, + shopName = shopName?.ifBlank { null }, + statusEnum = statusEnum, + etaStart = etaStart, + etaEnd = etaEnd, + allowedSupplierCodes = allowedSupplierCodesForFloor(floor), + lanePredicate = lanePredicate, + ) + val total = matched.size + val from = pageIdx * size + val pageRecords = + if (from >= total) emptyList() + else matched.subList(from, minOf(from + size, total)) + return RecordsRes(pageRecords, total) + } + /** * 僅回傳依店鋪/ETA 從 truck 排程**推算後** `truckLanceCode` 為 null 或空白的送貨單(畫面上對應「車線-X」)。 * 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。 @@ -386,13 +382,14 @@ open class DeliveryOrderService( estimatedArrivalDate: LocalDateTime?, pageNum: Int?, pageSize: Int?, + floor: String? = null, ): RecordsRes { val page = (pageNum ?: 1) - 1 val size = pageSize ?: 10 val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } } val etaStart = estimatedArrivalDate val etaEnd = estimatedArrivalDate?.plusDays(1) - val allowedSupplierCodes = listOf("P06B", "P07", "P06D") + val allowedSupplierCodes = allowedSupplierCodesForFloor(floor) val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes( code = code?.ifBlank { null }, @@ -423,15 +420,7 @@ open class DeliveryOrderService( if (deliveryOrder != null && shopId != null && infoEta != null) { val targetDate = infoEta.toLocalDate() val dayAbbr = getDayOfWeekAbbr(targetDate) - val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, preferredFloor, dayAbbr) - val matchedTruck = if (trucks.isEmpty()) { - truckRepository.findByShopIdAndDeletedFalse(shopId) - .filter { it.storeId == preferredFloor } - .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } else { - trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } - } - matchedTruck?.truckLanceCode + resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode } else { null } @@ -447,7 +436,7 @@ open class DeliveryOrderService( shopAddress = info.shopAddress, truckLanceCode = calculatedTruckLanceCode, ) - }.filter { dto -> dto.truckLanceCode.isNullOrBlank() } + }.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) } val totalCount = processedRecords.size val startIndex = page * size @@ -2096,6 +2085,129 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } ) } + private fun scanDoLiteSupplierFilteredWithLanePredicate( + code: String?, + shopName: String?, + statusEnum: DeliveryOrderStatus?, + etaStart: LocalDateTime?, + etaEnd: LocalDateTime?, + allowedSupplierCodes: List, + lanePredicate: (String?) -> Boolean, + ): List { + val out = ArrayList() + var dbPage = 0 + var rawScanned = 0 + while (rawScanned < 100_000) { + val slice = deliveryOrderRepository.searchDoLitePageWithSupplierCodes( + code = code, + shopName = shopName, + status = statusEnum, + etaStart = etaStart, + etaEnd = etaEnd, + allowedSupplierCodes = allowedSupplierCodes, + pageable = PageRequest.of(dbPage, 500), + ) + if (slice.content.isEmpty()) break + val dtos = buildResolvedTruckDtosForLiteRows(slice.content) + for (dto in dtos) { + if (lanePredicate(dto.truckLanceCode)) out.add(dto) + } + rawScanned += slice.content.size + dbPage++ + if (!slice.hasNext()) break + } + return out + } + + private fun buildResolvedTruckDtosForLiteRows( + rows: List, + ): List { + val ids = rows.mapNotNull { it.id } + if (ids.isEmpty()) return emptyList() + val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id } + + val shopIdAndDatePairs = rows.mapNotNull { info -> + val d = deliveryOrdersMap[info.id] + val shopId = d?.shop?.id + val eta = info.estimatedArrivalDate + if (shopId != null && eta != null) { + val targetDate = eta.toLocalDate() + val dayAbbr = getDayOfWeekAbbr(targetDate) + val supplierCode = d.supplier?.code + val preferredFloor = when (supplierCode) { + "P06B" -> "4F" + "P07", "P06D" -> "2F" + else -> "2F" + } + Triple(shopId, preferredFloor, dayAbbr) + } else { + null + } + }.distinct() + + val truckCache = mutableMapOf, Truck?>() + shopIdAndDatePairs.forEach { (shopId, preferredFloor, dayAbbr) -> + truckCache[Triple(shopId, preferredFloor, dayAbbr)] = + resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr) + } + + return rows.map { info -> + val deliveryOrder = deliveryOrdersMap[info.id] + val supplierCode = deliveryOrder?.supplier?.code + val preferredFloor = when (supplierCode) { + "P06B" -> "4F" + "P07", "P06D" -> "2F" + else -> "2F" + } + val shopId = deliveryOrder?.shop?.id + val infoEta = info.estimatedArrivalDate + val calculatedTruckLanceCode = + if (deliveryOrder != null && shopId != null && infoEta != null) { + val targetDate = infoEta.toLocalDate() + val dayAbbr = getDayOfWeekAbbr(targetDate) + truckCache[Triple(shopId, preferredFloor, dayAbbr)]?.truckLanceCode + } else { + null + } + DeliveryOrderInfoLiteDto( + id = info.id, + code = info.code, + orderDate = info.orderDate, + estimatedArrivalDate = info.estimatedArrivalDate, + status = info.status, + shopName = info.shopName, + supplierName = info.supplierName, + shopAddress = info.shopAddress, + truckLanceCode = calculatedTruckLanceCode, + ) + } + } + + /** + * 依店鋪 + 揀貨樓層解析當日應顯示之車線。 + * - **2F**(P07/P06D):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。 + * - **4F**(P06B):維持以星期縮寫篩選 [TruckRepository.findByShopIdAndStoreIdAndDayOfWeek];無命中時再退回同樓層最早出發。 + */ + private fun resolveTruckForShopFloorAndDay( + shopId: Long, + preferredFloor: String, + dayAbbr: String, + ): Truck? { + if (preferredFloor == "2F") { + return truckRepository.findByShopIdAndDeletedFalse(shopId) + .filter { it.storeId == preferredFloor } + .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } + val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, preferredFloor, dayAbbr) + return if (trucks.isEmpty()) { + truckRepository.findByShopIdAndDeletedFalse(shopId) + .filter { it.storeId == preferredFloor } + .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } else { + trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) } + } + } + private fun getDayOfWeekAbbr(date: LocalDate): String = when (date.dayOfWeek) { java.time.DayOfWeek.MONDAY -> "Mon" diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt index c832957..d2805fb 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt @@ -477,27 +477,7 @@ open class DoWorkbenchMainService( val solSnapshot = infos.joinToString("; ") { info -> "sol${info.id} st=${info.status} qty=${info.qty} picked=${WorkbenchStockOutLinePickProgress.isCountedAsPicked(info)}" } -/* -log.info( - "WORKBENCH_SCAN_TRACE polId={} solId={} scanLotNo={} scanIllId={} polRequired={} [{}] endedSumOthers={} currentSolQty={} remainingPol={} splMatchQty={} chunkTarget={} stillNeedOnThisSol={} requestedCap={} availScanLot={} plannedDelta={} lotSplit={}", - polId, - sol.id, - lotNo, - scannedIll.id, - required, - solSnapshot, - endedSumOthers, - currentQtyBd, - remainingPol, - splForLot?.qty, - chunkTarget, - stillNeedOnThisSol, - requestedCap, - availablePickable, - plannedDelta, - isLotExhaustedSplit, // initial trace only -) -*/ + val prepMs = lapMs() // retry-related state @@ -1858,7 +1838,7 @@ return MessageResponse( /** DO workbench FG list from [delivery_order_pick_order] + linked [pick_order] (no do_pick_order_line). */ open fun getFgPickOrdersByUserIdWorkbench(userId: Long): List> { try { - println(" Starting getFgPickOrdersByUserIdWorkbench with userId: $userId") + //println(" Starting getFgPickOrdersByUserIdWorkbench with userId: $userId") val sql = """ SELECT diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt new file mode 100644 index 0000000..a407689 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt @@ -0,0 +1,55 @@ +package com.ffii.fpsms.modules.deliveryOrder.service + +import java.util.Locale + +/** + * 車線搜尋正規化(search-do-lite-v2)。 + * 實際 [com.ffii.fpsms.modules.pickOrder.entity.Truck] 編碼主要為 `車線-…` 或 `P06B_…`;未指派預設列為 `車線-X`(shopId 可為 null)。 + */ +object TruckLaneSearchSpec { + const val UNASSIGNED_LANE_LABEL: String = "車線-X" + + sealed interface Mode { + data object NoFilter : Mode + /** 僅未指派:推算為 null/空白/字面量 車線-X(與預設車列一致) */ + data object UnassignedOnly : Mode + /** + * 一般關鍵字:以 [needleLower](trim + [Locale.ROOT] lowercase)對推算車線做 [String.contains]。 + * 不再因「`車線-` 開頭」額外併入未指派,避免搜 `車線-待1` 卻出現 `車線-X`;廣義條件(無車線欄位)仍由 [NoFilter] 帶出含 X 的列。 + */ + data class Keyword( + val needleLower: String, + ) : Mode + } + + fun parse(raw: String?): Mode { + val trimmed = raw?.trim().orEmpty() + if (trimmed.isEmpty()) return Mode.NoFilter + if (isUnassignedSearchToken(trimmed)) return Mode.UnassignedOnly + return Mode.Keyword( + needleLower = trimmed.lowercase(Locale.ROOT), + ) + } + + private fun isUnassignedSearchToken(trimmed: String): Boolean { + if (trimmed.length == 1 && trimmed.equals("x", ignoreCase = true)) return true + val normalized = trimmed.lowercase(Locale.ROOT).replace("车线", "車線") + return normalized == "車線-x" + } + + fun isUnassignedResolvedLane(calculated: String?): Boolean { + if (calculated.isNullOrBlank()) return true + return calculated.trim().equals(UNASSIGNED_LANE_LABEL, ignoreCase = true) + } + + fun matches(mode: Mode, resolvedTruckLanceCode: String?): Boolean { + when (mode) { + Mode.NoFilter -> return true + Mode.UnassignedOnly -> return isUnassignedResolvedLane(resolvedTruckLanceCode) + is Mode.Keyword -> { + val lane = resolvedTruckLanceCode?.trim()?.lowercase(Locale.ROOT).orEmpty() + return lane.contains(mode.needleLower) + } + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index 9a4716f..acba85b 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -70,7 +70,8 @@ class DeliveryOrderController( estimatedArrivalDate = request.estimatedArrivalDate, pageNum = request.pageNum, pageSize = request.pageSize, - truckLanceCode = request.truckLanceCode + truckLanceCode = request.truckLanceCode, + floor = request.floor, ) } @@ -86,6 +87,25 @@ class DeliveryOrderController( estimatedArrivalDate = request.estimatedArrivalDate, pageNum = request.pageNum, pageSize = request.pageSize, + floor = request.floor, + ) + } + + /** + * DO 輕量搜尋 v2:車線關鍵字正規化(`車線-X`/`x`/`車線-` 前綴併入未指派)、 + * 允許供應商條件下分批掃描,避免單次載入過大;請求體同 [searchDoLite]。 + */ + @PostMapping("/search-do-lite-v2") + fun searchDoLiteV2(@RequestBody request: SearchDeliveryOrderInfoRequest): RecordsRes { + return deliveryOrderService.searchDoLiteByPageV2( + code = request.code, + shopName = request.shopName, + status = request.status, + estimatedArrivalDate = request.estimatedArrivalDate, + pageNum = request.pageNum, + pageSize = request.pageSize, + truckLanceCode = request.truckLanceCode, + floor = request.floor, ) } diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt index 5c66b54..478e123 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt @@ -30,5 +30,7 @@ data class SearchDeliveryOrderInfoRequest( val estimatedArrivalDate: LocalDateTime?, val pageSize: Int?, val pageNum: Int?, - val truckLanceCode: String? + val truckLanceCode: String?, + /** `ALL`/`All`/null:P06B+P07+P06D;`2F`:P07+P06D;`4F`:P06B。車線-X 亦依供應商歸屬出現在對應樓層。 */ + val floor: String? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt index 69eebf7..42ed0fd 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt @@ -3,11 +3,12 @@ package com.ffii.fpsms.modules.jobOrder.service import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus -import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse -import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderLotsHierarchicalResponse +import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoWorkbenchResponse +import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderLotsHierarchicalWorkbenchResponse +import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoWorkbenchResponse import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse -import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse +import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsWorkbenchResponse import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository @@ -194,7 +195,7 @@ open class JoWorkbenchMainService( /** * Hierarchical pick UI for JO Workbench: available qty **in − out**; stockouts include **suggestedPickQty** when SPL matches SOL lot line. */ - open fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId: Long): JobOrderLotsHierarchicalResponse { + open fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId: Long): JobOrderLotsHierarchicalWorkbenchResponse { println("=== JoWorkbenchMainService.getJobOrderLotsHierarchicalByPickOrderIdWorkbench ===") println("pickOrderId: $pickOrderId") @@ -299,8 +300,8 @@ open class JoWorkbenchMainService( } val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) - - val pickOrderInfo = PickOrderInfoResponse( + + val pickOrderInfo = PickOrderInfoWorkbenchResponse( id = pickOrder.id, code = pickOrder.code, consoCode = pickOrder.consoCode, @@ -310,10 +311,12 @@ open class JoWorkbenchMainService( type = pickOrder.type?.value, status = pickOrder.status?.value, assignTo = pickOrder.assignTo?.id, - jobOrder = JobOrderBasicInfoResponse( + jobOrder = JobOrderBasicInfoWorkbenchResponse( id = jobOrder.id!!, code = jobOrder.code ?: "", - name = "Job Order ${jobOrder.code}" + name = "Job Order ${jobOrder.code}", + itemCode = jobOrder.bom?.code, + itemName = jobOrder.bom?.name, ) ) @@ -342,7 +345,7 @@ open class JoWorkbenchMainService( val handlerNameInner = jpoInner?.handledBy?.let { uid -> userService.find(uid).orElse(null)?.name } - println("handlerName: $handlerNameInner") + //println("handlerName: $handlerNameInner") val availableQty = if (sol?.status == "rejected") { null } else { @@ -429,7 +432,7 @@ open class JoWorkbenchMainService( ) } - PickOrderLineWithLotsResponse( + PickOrderLineWithLotsWorkbenchResponse( id = pol.id!!, itemId = item?.id, itemCode = item?.code, @@ -445,7 +448,7 @@ open class JoWorkbenchMainService( ) } - JobOrderLotsHierarchicalResponse( + JobOrderLotsHierarchicalWorkbenchResponse( pickOrder = pickOrderInfo, pickOrderLines = pickOrderLinesResult ) @@ -456,10 +459,10 @@ open class JoWorkbenchMainService( } } - private fun emptyHierarchical(message: String): JobOrderLotsHierarchicalResponse { + private fun emptyHierarchical(message: String): JobOrderLotsHierarchicalWorkbenchResponse { println("❌ $message") - return JobOrderLotsHierarchicalResponse( - pickOrder = PickOrderInfoResponse( + return JobOrderLotsHierarchicalWorkbenchResponse( + pickOrder = PickOrderInfoWorkbenchResponse( id = null, code = null, consoCode = null, @@ -467,7 +470,7 @@ open class JoWorkbenchMainService( type = null, status = null, assignTo = null, - jobOrder = JobOrderBasicInfoResponse(0, "", "") + jobOrder = JobOrderBasicInfoWorkbenchResponse(0, "", "",null,null) ), pickOrderLines = emptyList() ) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt index 0c8fc6e..c90b20e 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt @@ -332,7 +332,7 @@ fun getJobOrderPickOrderLotDetails( /** Workbench: available qty uses in−out (matches scan-pick); stockouts include suggested SPL qty when matched. */ @GetMapping("/all-lots-hierarchical-by-pick-order-workbench/{pickOrderId}") - fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalResponse { + fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(@PathVariable pickOrderId: Long): JobOrderLotsHierarchicalWorkbenchResponse { return joWorkbenchMainService.getJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId) } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt index 89a68e4..c7e382a 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt @@ -90,9 +90,45 @@ data class PickOrderInfoResponse( data class JobOrderBasicInfoResponse( val id: Long, val code: String, - val name: String + val name: String, +) +data class JobOrderLotsHierarchicalWorkbenchResponse( + val pickOrder: PickOrderInfoWorkbenchResponse, + val pickOrderLines: List ) +data class PickOrderInfoWorkbenchResponse( + val id: Long?, + val code: String?, + val consoCode: String?, + val targetDate: String?, + val type: String?, + val status: String?, + val assignTo: Long?, + val jobOrder: JobOrderBasicInfoWorkbenchResponse +) +data class PickOrderLineWithLotsWorkbenchResponse( + val id: Long, + val itemId: Long?, + val itemCode: String?, + val itemName: String?, + val requiredQty: Double?, + // Total available qty across all inventory lot lines for this item (used by JO pick UI) + val totalAvailableQty: Double? = null, + val uomCode: String?, + val uomDesc: String?, + val status: String?, + val lots: List, + val stockouts: List = emptyList(), + val handler: String? +) +data class JobOrderBasicInfoWorkbenchResponse( + val id: Long, + val code: String, + val name: String, + val itemCode: String?, + val itemName: String?, +) data class PickOrderLineWithLotsResponse( val id: Long, val itemId: Long?, diff --git a/src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql b/src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql new file mode 100644 index 0000000..75446dc --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql @@ -0,0 +1,19 @@ +-- liquibase formatted sql + +-- changeset fpsms:20260507_idx_do_deleted_status_eta_id +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'delivery_order' AND index_name = 'idx_do_deleted_status_eta_id' + +CREATE INDEX idx_do_deleted_status_eta_id + ON delivery_order (deleted, status, estimatedArrivalDate, id); + +-- changeset fpsms:20260507_idx_do_deleted_shop_eta_id +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'delivery_order' AND index_name = 'idx_do_deleted_shop_eta_id' + +CREATE INDEX idx_do_deleted_shop_eta_id + ON delivery_order (deleted, shopId, estimatedArrivalDate, id); + +-- changeset fpsms:20260507_idx_truck_TruckLanceCode +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'truck' AND index_name = 'idx_truck_TruckLanceCode' + +CREATE INDEX idx_truck_TruckLanceCode + ON truck (TruckLanceCode);