Browse Source

update do search and jo bom name coe

production
CANCERYS\kw093 1 day ago
parent
commit
d4af229304
9 changed files with 364 additions and 137 deletions
  1. +208
    -96
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  2. +2
    -22
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  3. +55
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt
  4. +21
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt
  5. +3
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt
  6. +18
    -15
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt
  8. +37
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  9. +19
    -0
      src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql

+ 208
- 96
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt View File

@@ -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<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(
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<DeliveryOrderInfoLiteDto> {

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<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 },
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<Triple<Long, String, String>, 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<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」)。
* 與 [searchDoLiteByPage] 帶一般車線關鍵字分開,避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。
@@ -386,13 +382,14 @@ open class DeliveryOrderService(
estimatedArrivalDate: LocalDateTime?,
pageNum: Int?,
pageSize: Int?,
floor: String? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {
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<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 =
when (date.dayOfWeek) {
java.time.DayOfWeek.MONDAY -> "Mon"


+ 2
- 22
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt View File

@@ -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<Map<String, Any?>> {
try {
println(" Starting getFgPickOrdersByUserIdWorkbench with userId: $userId")
//println(" Starting getFgPickOrdersByUserIdWorkbench with userId: $userId")

val sql = """
SELECT


+ 55
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/TruckLaneSearchSpec.kt View File

@@ -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)
}
}
}
}

+ 21
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt View File

@@ -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<DeliveryOrderInfoLiteDto> {
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,
)
}



+ 3
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/ReleaseDoRequest.kt View File

@@ -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,
)

+ 18
- 15
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt View File

@@ -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()
)


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/JobOrderController.kt View File

@@ -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)
}


+ 37
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt View File

@@ -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<PickOrderLineWithLotsWorkbenchResponse>
)

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<LotDetailResponse>,
val stockouts: List<StockOutLineDetailResponse> = 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?,


+ 19
- 0
src/main/resources/db/changelog/changes/20260507_01_search/01_do_truck_search_indexes.sql View File

@@ -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);

Loading…
Cancel
Save