@@ -122,6 +122,22 @@ open class DeliveryOrderService(
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val itemsRepository: ItemsRepository,
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<String> {
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(
open fun searchDoLiteByPage(
code: String?,
code: String?,
shopName: String?,
shopName: String?,
@@ -129,7 +145,8 @@ open class DeliveryOrderService(
estimatedArrivalDate: LocalDateTime?,
estimatedArrivalDate: LocalDateTime?,
pageNum: Int?,
pageNum: Int?,
pageSize: Int?,
pageSize: Int?,
truckLanceCode: String?
truckLanceCode: String?,
floor: String? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
): RecordsRes<DeliveryOrderInfoLiteDto> {
val page = (pageNum ?: 1) - 1
val page = (pageNum ?: 1) - 1
@@ -142,81 +159,26 @@ open class DeliveryOrderService(
val searchTruckLanceCode = truckLanceCode?.ifBlank { null }?.lowercase()
val searchTruckLanceCode = truckLanceCode?.ifBlank { null }?.lowercase()
if (searchTruckLanceCode != null) {
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<String?>()
val supplierCodesToExclude = mutableSetOf<String?>()
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 },
code = code?.ifBlank { null },
shopName = shopName?.ifBlank { null },
shopName = shopName?.ifBlank { null },
status = statusEnum,
status = statusEnum,
etaStart = etaStart,
etaStart = etaStart,
etaEnd = etaEnd,
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 deliveryOrderIds = allResult.content.mapNotNull { it.id }
val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds)
val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds)
.associateBy { it.id }
.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 和日期组合(只处理预过滤后的记录)
// ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录)
val shopIdAndDatePairs = preFilteredContent.mapNotNull { info ->
val shopIdAndDatePairs = preFilteredContent.mapNotNull { info ->
@@ -243,15 +205,8 @@ open class DeliveryOrderService(
// ✅ 优化4: 批量查询所有需要的 Truck
// ✅ 优化4: 批量查询所有需要的 Truck
val truckCache = mutableMapOf<Triple<Long, String, String>, Truck?>()
val truckCache = mutableMapOf<Triple<Long, String, String>, Truck?>()
shopIdAndDatePairs.forEach { (shopId, preferredFloor, dayAbbr) ->
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")
println("DEBUG: Cached ${truckCache.size} truck lookups")
@@ -263,7 +218,7 @@ open class DeliveryOrderService(
val preferredFloor = when (supplierCode) {
val preferredFloor = when (supplierCode) {
"P06B" -> "4F"
"P06B" -> "4F"
"P07", "P06D" -> "2F"
"P07", "P06D" -> "2F"
else -> null
else -> "2F"
}
}
val shop = deliveryOrder?.shop
val shop = deliveryOrder?.shop
val shopId = shop?.id
val shopId = shop?.id
@@ -314,7 +269,7 @@ open class DeliveryOrderService(
return RecordsRes(paginatedRecords, totalCount)
return RecordsRes(paginatedRecords, totalCount)
} else {
} else {
// 未提供 truckLanceCode:在 DB 層依允許的供應商分頁,避免先取 10 筆再過濾導致每頁顯示少於 pageSize
// 未提供 truckLanceCode:在 DB 層依允許的供應商分頁,避免先取 10 筆再過濾導致每頁顯示少於 pageSize
val allowedSupplierCodes = listOf("P06B", "P07", "P06D" )
val allowedSupplierCodes = allowedSupplierCodesForFloor(floor )
val result = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
val result = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
code = code?.ifBlank { null },
code = code?.ifBlank { null },
shopName = shopName?.ifBlank { null },
shopName = shopName?.ifBlank { null },
@@ -341,17 +296,7 @@ open class DeliveryOrderService(
if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
val targetDate = estimatedArrivalDate.toLocalDate()
val targetDate = estimatedArrivalDate.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
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 {
} else {
null
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<DeliveryOrderInfoLiteDto> {
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」)。
* 僅回傳依店鋪/ETA 從 truck 排程**推算後** `truckLanceCode` 為 null 或空白的送貨單(畫面上對應「車線-X」)。
* 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。
* 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。
@@ -386,13 +382,14 @@ open class DeliveryOrderService(
estimatedArrivalDate: LocalDateTime?,
estimatedArrivalDate: LocalDateTime?,
pageNum: Int?,
pageNum: Int?,
pageSize: Int?,
pageSize: Int?,
floor: String? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
): RecordsRes<DeliveryOrderInfoLiteDto> {
val page = (pageNum ?: 1) - 1
val page = (pageNum ?: 1) - 1
val size = pageSize ?: 10
val size = pageSize ?: 10
val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } }
val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } }
val etaStart = estimatedArrivalDate
val etaStart = estimatedArrivalDate
val etaEnd = estimatedArrivalDate?.plusDays(1)
val etaEnd = estimatedArrivalDate?.plusDays(1)
val allowedSupplierCodes = listOf("P06B", "P07", "P06D" )
val allowedSupplierCodes = allowedSupplierCodesForFloor(floor )
val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
code = code?.ifBlank { null },
code = code?.ifBlank { null },
@@ -423,15 +420,7 @@ open class DeliveryOrderService(
if (deliveryOrder != null && shopId != null && infoEta != null) {
if (deliveryOrder != null && shopId != null && infoEta != null) {
val targetDate = infoEta.toLocalDate()
val targetDate = infoEta.toLocalDate()
val dayAbbr = getDayOfWeekAbbr(targetDate)
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 {
} else {
null
null
}
}
@@ -447,7 +436,7 @@ open class DeliveryOrderService(
shopAddress = info.shopAddress,
shopAddress = info.shopAddress,
truckLanceCode = calculatedTruckLanceCode,
truckLanceCode = calculatedTruckLanceCode,
)
)
}.filter { dto -> dto.truckLanceCode.isNullOrBlank( ) }
}.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane( dto.truckLanceCode) }
val totalCount = processedRecords.size
val totalCount = processedRecords.size
val startIndex = page * 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<String>,
lanePredicate: (String?) -> Boolean,
): List<DeliveryOrderInfoLiteDto> {
val out = ArrayList<DeliveryOrderInfoLiteDto>()
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<DeliveryOrderInfoLite>,
): List<DeliveryOrderInfoLiteDto> {
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<Triple<Long, String, String>, 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 =
private fun getDayOfWeekAbbr(date: LocalDate): String =
when (date.dayOfWeek) {
when (date.dayOfWeek) {
java.time.DayOfWeek.MONDAY -> "Mon"
java.time.DayOfWeek.MONDAY -> "Mon"