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 cee583ca..73a0eb9d 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 @@ -159,6 +159,7 @@ open class DeliveryOrderService( } } + /** FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 */ open fun searchDoLiteByPage( code: String?, shopName: String?, @@ -203,10 +204,14 @@ open class DeliveryOrderService( val preFilteredContent = allResult.content val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val ticketLaneByDoId = loadTicketTruckLanesByDoId( + deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id }, + ) - // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录) + // 未放單才依供應商樓層查 truck;已放單用票據車線 val shopIdAndDatePairs = preFilteredContent.mapNotNull { info -> val deliveryOrder = deliveryOrdersMap[info.id] + if (isReleasedDeliveryOrder(deliveryOrder)) return@mapNotNull null val shopId = deliveryOrder?.shop?.id val estimatedArrivalDate = info.estimatedArrivalDate if (shopId != null && estimatedArrivalDate != null) { @@ -234,23 +239,16 @@ open class DeliveryOrderService( // 处理预过滤后的记录:计算 truckLanceCode 并过滤 val processedRecords = preFilteredContent.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] - val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) - val shop = deliveryOrder?.shop - val shopId = shop?.id val estimatedArrivalDate = info.estimatedArrivalDate - val calculatedTruckLanceCode = - if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) { - val targetDate = estimatedArrivalDate.toLocalDate() - val dayAbbr = getDayOfWeekAbbr(targetDate) - - // 从缓存中获取 Truck - val matchedTruck = truckCache[Triple(shopId, preferredFloor, dayAbbr)] - matchedTruck?.truckLanceCode - } else { - null - } + val calculatedTruckLanceCode = displayedTruckLanceCode( + deliveryOrder = deliveryOrder, + estimatedArrivalDate = estimatedArrivalDate, + floorSuppliers2F = floorSuppliers2F, + floorSuppliers4F = floorSuppliers4F, + truckCache = truckCache, + ticketLaneByDoId = ticketLaneByDoId, + ) DeliveryOrderInfoLiteDto( id = info.id, @@ -299,22 +297,22 @@ open class DeliveryOrderService( ) val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val loadedOrders = result.content.associate { info -> + info.id to deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id) + } + val ticketLaneByDoId = loadTicketTruckLanesByDoId( + loadedOrders.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it?.id }, + ) val records = result.content.map { info -> - val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id) - val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) - val shop = deliveryOrder?.shop - val shopId = shop?.id - val estimatedArrivalDate = info.estimatedArrivalDate - - val calculatedTruckLanceCode = - if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) { - val targetDate = estimatedArrivalDate.toLocalDate() - val dayAbbr = getDayOfWeekAbbr(targetDate) - resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode - } else { - null - } + val deliveryOrder = loadedOrders[info.id] + val calculatedTruckLanceCode = displayedTruckLanceCode( + deliveryOrder = deliveryOrder, + estimatedArrivalDate = info.estimatedArrivalDate, + floorSuppliers2F = floorSuppliers2F, + floorSuppliers4F = floorSuppliers4F, + truckCache = null, + ticketLaneByDoId = ticketLaneByDoId, + ) DeliveryOrderInfoLiteDto( id = info.id, @@ -391,8 +389,9 @@ open class DeliveryOrderService( } /** - * 僅回傳依店鋪/ETA 從 truck 排程**推算後** `truckLanceCode` 為 null 或空白的送貨單(畫面上對應「車線-X」)。 - * 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。 + * FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 + * 僅回傳顯示車線為 null 或空白的送貨單(畫面上對應「車線-X」)。 + * 已放單(receiving/completed)用票據車線;未放單才依店鋪/ETA 從 truck 排程推算。 */ open fun searchDoLiteUnassignedTruckByPage( code: String?, @@ -425,23 +424,20 @@ open class DeliveryOrderService( val deliveryOrderIds = allResult.content.mapNotNull { it.id } val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id } val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val ticketLaneByDoId = loadTicketTruckLanesByDoId( + deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id }, + ) val processedRecords = allResult.content.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] - val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) - val shop = deliveryOrder?.shop - val shopId = shop?.id - val infoEta = info.estimatedArrivalDate - - val calculatedTruckLanceCode = - if (deliveryOrder != null && shopId != null && infoEta != null) { - val targetDate = infoEta.toLocalDate() - val dayAbbr = getDayOfWeekAbbr(targetDate) - resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode - } else { - null - } + val calculatedTruckLanceCode = displayedTruckLanceCode( + deliveryOrder = deliveryOrder, + estimatedArrivalDate = info.estimatedArrivalDate, + floorSuppliers2F = floorSuppliers2F, + floorSuppliers4F = floorSuppliers4F, + truckCache = null, + ticketLaneByDoId = ticketLaneByDoId, + ) DeliveryOrderInfoLiteDto( id = info.id, @@ -2502,6 +2498,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } return out } + /** FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 */ private fun buildResolvedTruckDtosForLiteRows( rows: List, ): List { @@ -2509,9 +2506,13 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } if (ids.isEmpty()) return emptyList() val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id } val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() + val ticketLaneByDoId = loadTicketTruckLanesByDoId( + deliveryOrdersMap.values.filter { isReleasedDeliveryOrder(it) }.mapNotNull { it.id }, + ) val shopIdAndDatePairs = rows.mapNotNull { info -> val d = deliveryOrdersMap[info.id] + if (isReleasedDeliveryOrder(d)) return@mapNotNull null val shopId = d?.shop?.id val eta = info.estimatedArrivalDate if (shopId != null && eta != null) { @@ -2533,18 +2534,14 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } return rows.map { info -> val deliveryOrder = deliveryOrdersMap[info.id] - val supplierCode = deliveryOrder?.supplier?.code - val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F) - 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 - } + val calculatedTruckLanceCode = displayedTruckLanceCode( + deliveryOrder = deliveryOrder, + estimatedArrivalDate = info.estimatedArrivalDate, + floorSuppliers2F = floorSuppliers2F, + floorSuppliers4F = floorSuppliers4F, + truckCache = truckCache, + ticketLaneByDoId = ticketLaneByDoId, + ) DeliveryOrderInfoLiteDto( id = info.id, code = info.code, @@ -2560,6 +2557,139 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } } } + private class TicketLaneHit( + val lane: String?, + val ticketId: Long, + val srcRank: Int, + ) + + /** + * FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 + * 放單後狀態:receiving 表示已發布,completed 表示已完成。 + */ + private fun isReleasedDeliveryOrder(deliveryOrder: DeliveryOrder?): Boolean = + deliveryOrder?.status == DeliveryOrderStatus.RECEIVING || + deliveryOrder?.status == DeliveryOrderStatus.COMPLETED + + /** + * FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 + * 已放單讀票據車線(工作台 `delivery_order_pick_order`,否則 `do_pick_order`/record)。 + * 未放單才依供應商樓層 + 店鋪 + 星期從 truck 推算。 + * [truckCache] 為 null 時,未放單即時查 truck。 + */ + private fun displayedTruckLanceCode( + deliveryOrder: DeliveryOrder?, + estimatedArrivalDate: LocalDateTime?, + floorSuppliers2F: List, + floorSuppliers4F: List, + truckCache: Map, Truck?>?, + ticketLaneByDoId: Map, + ): String? { + if (isReleasedDeliveryOrder(deliveryOrder)) { + return deliveryOrder?.id?.let { ticketLaneByDoId[it] } + } + val shopId = deliveryOrder?.shop?.id ?: return null + val eta = estimatedArrivalDate ?: return null + val preferredFloor = preferredStoreFloorForSupplier( + deliveryOrder.supplier?.code, + floorSuppliers2F, + floorSuppliers4F, + ) + val dayAbbr = getDayOfWeekAbbr(eta.toLocalDate()) + val key = Triple(shopId, preferredFloor, dayAbbr) + if (truckCache != null) { + return truckCache[key]?.truckLanceCode + } + return resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode + } + + /** + * FP-MTMS Version Checklist | Functions Ref. No. 88 | v1.0.1 | 2026-10-28 + * 一批送貨單對應的票據車線。同一張單有多張票時,工作台票優先,同來源取 id 最大的一張。 + * 票上的車線為空白時仍回傳 null,呼叫端不再改查 truck。 + */ + private fun loadTicketTruckLanesByDoId(doIds: Collection): Map { + val ids = doIds.distinct() + if (ids.isEmpty()) return emptyMap() + val best = HashMap() + val sql = """ + SELECT do_id, truck_lance_code, ticket_id, src_rank + FROM ( + SELECT po.doId AS do_id, + NULLIF(TRIM(dop.truckLanceCode), '') AS truck_lance_code, + dop.id AS ticket_id, + 1 AS src_rank + FROM fpsmsdb.delivery_order_pick_order dop + INNER JOIN fpsmsdb.pick_order po + ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 + WHERE dop.deleted = 0 + AND po.doId IN (:doIds) + + UNION ALL + + SELECT dpo.do_order_id AS do_id, + NULLIF(TRIM(dpo.TruckLanceCode), '') AS truck_lance_code, + dpo.id AS ticket_id, + 2 AS src_rank + FROM fpsmsdb.do_pick_order dpo + WHERE dpo.deleted = 0 + AND dpo.do_order_id IN (:doIds) + + UNION ALL + + SELECT dpol.do_order_id AS do_id, + NULLIF(TRIM(dpo.TruckLanceCode), '') AS truck_lance_code, + dpo.id AS ticket_id, + 2 AS src_rank + FROM fpsmsdb.do_pick_order dpo + INNER JOIN fpsmsdb.do_pick_order_line dpol + ON dpol.do_pick_order_id = dpo.id AND dpol.deleted = 0 + WHERE dpo.deleted = 0 + AND dpol.do_order_id IN (:doIds) + + UNION ALL + + SELECT rec.do_order_id AS do_id, + NULLIF(TRIM(rec.TruckLanceCode), '') AS truck_lance_code, + rec.id AS ticket_id, + 3 AS src_rank + FROM fpsmsdb.do_pick_order_record rec + WHERE rec.deleted = 0 + AND rec.do_order_id IN (:doIds) + ) lanes + """.trimIndent() + + ids.chunked(400).forEach { chunk -> + val rows = jdbcDao.queryForList(sql, mapOf("doIds" to chunk)) + for (row in rows) { + val doId = jdbcNumber(row, "do_id")?.toLong() ?: continue + val ticketId = jdbcNumber(row, "ticket_id")?.toLong() ?: 0L + val srcRank = jdbcNumber(row, "src_rank")?.toInt() ?: 9 + val lane = jdbcText(row, "truck_lance_code") + val hit = TicketLaneHit(lane, ticketId, srcRank) + val current = best[doId] + if ( + current == null || + hit.srcRank < current.srcRank || + (hit.srcRank == current.srcRank && hit.ticketId > current.ticketId) + ) { + best[doId] = hit + } + } + } + return best.mapValues { it.value.lane } + } + + private fun jdbcNumber(row: Map<*, *>, name: String): Number? { + val value = row.entries.firstOrNull { it.key?.toString().equals(name, true) }?.value ?: return null + return value as? Number + } + + private fun jdbcText(row: Map<*, *>, name: String): String? { + val value = row.entries.firstOrNull { it.key?.toString().equals(name, true) }?.value ?: return null + return value.toString().trim().takeIf { it.isNotEmpty() } + } + /** * 依店鋪 + 揀貨樓層解析當日應顯示之車線。 * - **2F**(P07/P06D/P06Y):`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫,不依 `LIKE '%Mon%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。