| @@ -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<DeliveryOrderInfoLite>, | |||
| ): List<DeliveryOrderInfoLiteDto> { | |||
| @@ -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<String>, | |||
| floorSuppliers4F: List<String>, | |||
| truckCache: Map<Triple<Long, String, String>, Truck?>?, | |||
| ticketLaneByDoId: Map<Long, String?>, | |||
| ): 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<Long>): Map<Long, String?> { | |||
| val ids = doIds.distinct() | |||
| if (ids.isEmpty()) return emptyMap() | |||
| val best = HashMap<Long, TicketLaneHit>() | |||
| 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%'` 篩選;取該店該樓層未刪除車輛中出發時間最早者。 | |||