Ver código fonte

DO 放單 relate created need write in username

production
CANCERYS\kw093 1 semana atrás
pai
commit
d5b88deb09
2 arquivos alterados com 142 adições e 66 exclusões
  1. +24
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt
  2. +118
    -65
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt

+ 24
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReleaseCoordinatorService.kt Ver arquivo

@@ -28,6 +28,8 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByDoPickOrderIdRequ
import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import jakarta.persistence.OptimisticLockException
import org.hibernate.StaleObjectStateException
@@ -491,6 +493,7 @@ class DoReleaseCoordinatorService(
return ids
}
}
/** FP-MTMS Version Checklist | Functions Ref. No. 77 | v1.0.0 | 2026-09-08 */
fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse {
if (ids.isEmpty()) {
return MessageResponse(
@@ -502,10 +505,13 @@ class DoReleaseCoordinatorService(
val jobId = UUID.randomUUID().toString()
val status = BatchReleaseJobStatus(jobId = jobId, total = ids.size)
jobs[jobId] = status
val workerSecurityContext = copySecurityContextForWorker()

executor.submit {
batchReleaseConcurrencyGate.acquireUninterruptibly()
try {
SecurityContextHolder.setContext(workerSecurityContext)
batchReleaseConcurrencyGate.acquireUninterruptibly()
try {
try {
println("Starting batch release for ${ids.size} orders (job $jobId)")
val sortedIds = getOrderedDeliveryOrderIds(ids)
@@ -643,6 +649,9 @@ class DoReleaseCoordinatorService(
} finally {
batchReleaseConcurrencyGate.release()
}
} finally {
SecurityContextHolder.clearContext()
}
}

return MessageResponse(
@@ -851,6 +860,7 @@ class DoReleaseCoordinatorService(
)
}

/** FP-MTMS Version Checklist | Functions Ref. No. 77 | v1.0.0 | 2026-09-08 */
fun startBatchReleaseAsyncSingle(doId: Long, userId: Long): MessageResponse {
val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
?: return MessageResponse(
@@ -858,8 +868,11 @@ class DoReleaseCoordinatorService(
message = "Delivery Order not found", errorPosition = null, entity = null
)
val workerSecurityContext = copySecurityContextForWorker()

executor.submit {
try {
SecurityContextHolder.setContext(workerSecurityContext)
println("📦 Starting single release for DO $doId")
// 调用 releaseDeliveryOrderWithoutTicket 创建 pick order
@@ -958,6 +971,8 @@ class DoReleaseCoordinatorService(
} catch (e: Exception) {
println("❌ Single release exception: ${e.message}")
e.printStackTrace()
} finally {
SecurityContextHolder.clearContext()
}
}
@@ -968,6 +983,14 @@ class DoReleaseCoordinatorService(
)
}
/** FP-MTMS Version Checklist | Functions Ref. No. 77 | v1.0.0 | 2026-09-08 */
private fun copySecurityContextForWorker(): SecurityContext {
val parent = SecurityContextHolder.getContext()
val copy = SecurityContextHolder.createEmptyContext()
copy.authentication = parent.authentication
return copy
}

private fun updateSingleTicketNumbers() {
try {
// 1. 查找所有 TEMP- 开头的 single release type 订单


+ 118
- 65
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt Ver arquivo

@@ -9,6 +9,9 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchMergeTicketCandi
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService
import com.ffii.fpsms.modules.user.entity.UserRepository
import org.springframework.security.core.context.SecurityContext
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
@@ -95,6 +98,7 @@ open class DoWorkbenchReleaseService(
private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
private val stockOutLineWorkbenchService: StockOutLineWorkbenchService,
private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
private val userRepository: UserRepository,
) {
private val poolSize = Runtime.getRuntime().availableProcessors()
private val executor = Executors.newFixedThreadPool(kotlin.math.min(poolSize, 4))
@@ -142,6 +146,7 @@ open class DoWorkbenchReleaseService(
open fun startBatchReleaseAsyncSingleV2(ids: List<Long>, userId: Long): MessageResponse =
startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "single")

/** FP-MTMS Version Checklist | Functions Ref. No. 78 | v1.0.0 | 2026-09-08 */
private fun startBatchReleaseAsyncInternal(
ids: List<Long>,
userId: Long,
@@ -164,75 +169,83 @@ open class DoWorkbenchReleaseService(
val jobId = UUID.randomUUID().toString()
val status = WorkbenchBatchReleaseJobStatus(jobId = jobId, total = ids.size)
jobs[jobId] = status
val actor = resolveReleaseActor(userId)
// Capture auth before worker thread: BaseEntity @PrePersist needs SecurityContext for pick_order / lines.
val workerSecurityContext = copySecurityContextForWorker()

executor.submit {
batchReleaseConcurrencyGate.acquireUninterruptibly()
try {
val orderedIds = getOrderedDeliveryOrderIds(ids)
val successResults = mutableListOf<ReleaseDoResult>()
SecurityContextHolder.setContext(workerSecurityContext)
batchReleaseConcurrencyGate.acquireUninterruptibly()
try {
val orderedIds = getOrderedDeliveryOrderIds(ids)
val successResults = mutableListOf<ReleaseDoResult>()

orderedIds.forEach { deliveryOrderId ->
try {
val statusRows = jdbcDao.queryForList(
"""
SELECT status
FROM fpsmsdb.delivery_order
WHERE id = :id AND deleted = 0
""".trimIndent(),
mapOf("id" to deliveryOrderId)
)
val currentStatus = statusRows.firstOrNull()?.get("status")?.toString()?.lowercase()
if (currentStatus == DeliveryOrderStatus.COMPLETED.value || currentStatus == DeliveryOrderStatus.RECEIVING.value) {
return@forEach
}
orderedIds.forEach { deliveryOrderId ->
try {
val statusRows = jdbcDao.queryForList(
"""
SELECT status
FROM fpsmsdb.delivery_order
WHERE id = :id AND deleted = 0
""".trimIndent(),
mapOf("id" to deliveryOrderId)
)
val currentStatus = statusRows.firstOrNull()?.get("status")?.toString()?.lowercase()
if (currentStatus == DeliveryOrderStatus.COMPLETED.value || currentStatus == DeliveryOrderStatus.RECEIVING.value) {
return@forEach
}

val released = releaseDeliveryOrderWorkbenchWithRetries(deliveryOrderId, userId, useV2)
val released = releaseDeliveryOrderWorkbenchWithRetries(deliveryOrderId, userId, useV2)

if (released != null) {
successResults += released
status.success.incrementAndGet()
} else {
if (released != null) {
successResults += released
status.success.incrementAndGet()
} else {
synchronized(status.failed) {
status.failed.add(deliveryOrderId to "Release returned null")
}
}
} catch (e: Exception) {
synchronized(status.failed) {
status.failed.add(deliveryOrderId to "Release returned null")
status.failed.add(deliveryOrderId to (e.message ?: "Unknown error"))
}
}
} catch (e: Exception) {
synchronized(status.failed) {
status.failed.add(deliveryOrderId to (e.message ?: "Unknown error"))
}
}
}

try {
createAndLinkDeliveryOrderPickOrders(successResults, dopReleaseType, mergeExtraIntoLaneTicket)
} catch (e: Exception) {
// header-link failure shouldn't crash job; status.failed already includes per-DO failures
println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}")
}

val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults)
try {
createAndLinkDeliveryOrderPickOrders(successResults, dopReleaseType, mergeExtraIntoLaneTicket, actor)
} catch (e: Exception) {
// header-link failure shouldn't crash job; status.failed already includes per-DO failures
println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}")
}

if (!useV2) {
val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet()
pickOrdersForDownstream.forEach { pickOrderId ->
try {
suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId)
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId)
} catch (e: Exception) {
val deliveryOrderId = successResults.firstOrNull { it.pickOrderId == pickOrderId }?.deliveryOrderId
?: 0L
synchronized(status.failed) {
status.failed.add(
deliveryOrderId to ("Downstream workbench step failed for pick order $pickOrderId: ${e.message}")
)
val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults)

if (!useV2) {
val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet()
pickOrdersForDownstream.forEach { pickOrderId ->
try {
suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId)
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId)
} catch (e: Exception) {
val deliveryOrderId = successResults.firstOrNull { it.pickOrderId == pickOrderId }?.deliveryOrderId
?: 0L
synchronized(status.failed) {
status.failed.add(
deliveryOrderId to ("Downstream workbench step failed for pick order $pickOrderId: ${e.message}")
)
}
}
}
}
} finally {
status.running = false
status.finishedAt = Instant.now().toEpochMilli()
batchReleaseConcurrencyGate.release()
}
} finally {
status.running = false
status.finishedAt = Instant.now().toEpochMilli()
batchReleaseConcurrencyGate.release()
SecurityContextHolder.clearContext()
}
}

@@ -318,6 +331,7 @@ open class DoWorkbenchReleaseService(
)
}

val actor = resolveReleaseActor(userId)
val orderedIds = getOrderedDeliveryOrderIds(ids)
val successResults = mutableListOf<ReleaseDoResult>()
val failed = mutableListOf<Pair<Long, String>>()
@@ -350,7 +364,7 @@ open class DoWorkbenchReleaseService(
}
}

val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch", mergeExtraIntoLaneTicket)
val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch", mergeExtraIntoLaneTicket, actor)
val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults)
if (!useV2) {
val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet()
@@ -504,6 +518,37 @@ open class DoWorkbenchReleaseService(
}
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 78 | v1.0.0 | 2026-09-08
* Username for ticket createdBy/modifiedBy. Capture on the request thread, then copy onto the worker.
*/
private fun resolveReleaseActor(userId: Long): String {
val fromSecurity = SecurityContextHolder.getContext().authentication?.name
?.trim()
?.takeIf { it.isNotEmpty() && !it.equals("anonymousUser", ignoreCase = true) }
?.take(30)
if (fromSecurity != null) return fromSecurity
if (userId > 0L) {
val fromUser = userRepository.findById(userId).orElse(null)?.username
?.trim()
?.takeIf { it.isNotEmpty() }
?.take(30)
if (fromUser != null) return fromUser
}
return "system"
}

/**
* FP-MTMS Version Checklist | Functions Ref. No. 78 | v1.0.0 | 2026-09-08
* Copy auth for the worker thread so BaseEntity @PrePersist matches JO / consumable pick-order creation.
*/
private fun copySecurityContextForWorker(): SecurityContext {
val parent = SecurityContextHolder.getContext()
val copy = SecurityContextHolder.createEmptyContext()
copy.authentication = parent.authentication
return copy
}

private fun resolveTicketFloorSegment(storeId: String?, isDefaultTruckLane: Boolean): String =
if (isDefaultTruckLane) {
WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK
@@ -837,7 +882,7 @@ open class DoWorkbenchReleaseService(
}
}

private fun setRelationshipIdSelf(headerId: Long) {
private fun setRelationshipIdSelf(headerId: Long, actor: String) {
jdbcDao.executeUpdate(
"""
UPDATE fpsmsdb.delivery_order_pick_order
@@ -849,7 +894,7 @@ open class DoWorkbenchReleaseService(
mapOf(
"headerId" to headerId,
"modified" to LocalDateTime.now(),
"modifiedBy" to "system",
"modifiedBy" to actor,
),
)
}
@@ -914,10 +959,11 @@ open class DoWorkbenchReleaseService(
isSingleRelease: Boolean,
requiredDate: LocalDate,
ticketFloorSegment: String,
actor: String,
): Long? {
val ticketNo = nextDeliveryOrderPickOrderMergeTicketNo(requiredDate, ticketFloorSegment)
val releaseType = WorkbenchReleaseTypeSupport.mergeTicketReleaseType(isSingleRelease)
return insertNewDeliveryOrderPickOrderHeader(first, storeId, releaseType, ticketNo)
return insertNewDeliveryOrderPickOrderHeader(first, storeId, releaseType, ticketNo, actor)
}

private fun linkPickOrdersToHeader(headerId: Long, group: List<ReleaseDoResult>) {
@@ -941,6 +987,7 @@ open class DoWorkbenchReleaseService(
storeId: String?,
releaseTypeCol: String,
ticketNo: String,
actor: String,
): Long? {
val now = LocalDateTime.now()
jdbcDao.executeUpdate(
@@ -970,9 +1017,9 @@ open class DoWorkbenchReleaseService(
"ticketNo" to ticketNo,
"releaseType" to releaseTypeCol,
"created" to now,
"createdBy" to "system",
"createdBy" to actor,
"modified" to now,
"modifiedBy" to "system",
"modifiedBy" to actor,
)
)
return jdbcDao.queryForList(
@@ -984,7 +1031,7 @@ open class DoWorkbenchReleaseService(
LIMIT 1
""".trimIndent(),
mapOf("ticketNo" to ticketNo)
).firstOrNull()?.get("id")?.let { (it as Number).toLong() }?.also { setRelationshipIdSelf(it) }
).firstOrNull()?.get("id")?.let { (it as Number).toLong() }?.also { setRelationshipIdSelf(it, actor) }
}

/**
@@ -997,6 +1044,7 @@ open class DoWorkbenchReleaseService(
isSingleRelease: Boolean,
requiredDate: LocalDate,
ticketFloorSegment: String,
actor: String,
): Int {
val activeMerge = findActiveMergeTicket(first, storeId, isSingleRelease)
if (activeMerge != null) {
@@ -1010,14 +1058,14 @@ open class DoWorkbenchReleaseService(
val sourceToRetire = plainBatch ?: legacyUpgraded

if (sourceToRetire != null) {
val mergeHeaderId = createNewMergeHeader(first, storeId, isSingleRelease, requiredDate, ticketFloorSegment)
val mergeHeaderId = createNewMergeHeader(first, storeId, isSingleRelease, requiredDate, ticketFloorSegment, actor)
?: return 0
retireSourceTicketIntoMergeHeader(sourceToRetire.id, mergeHeaderId)
linkPickOrdersToHeader(mergeHeaderId, group)
return 1
}

val mergeHeaderId = createNewMergeHeader(first, storeId, isSingleRelease, requiredDate, ticketFloorSegment)
val mergeHeaderId = createNewMergeHeader(first, storeId, isSingleRelease, requiredDate, ticketFloorSegment, actor)
?: return 0
linkPickOrdersToHeader(mergeHeaderId, group)
return 1
@@ -1033,6 +1081,7 @@ open class DoWorkbenchReleaseService(
results: List<ReleaseDoResult>,
dopReleaseType: String = "batch",
mergeExtraIntoLaneTicket: Boolean = true,
actor: String,
): Int {
if (results.isEmpty()) return 0

@@ -1064,6 +1113,7 @@ open class DoWorkbenchReleaseService(
isSingleRelease,
requiredDate,
ticketFloorSegment,
actor,
)
}

@@ -1079,6 +1129,7 @@ open class DoWorkbenchReleaseService(
storeId,
WorkbenchReleaseTypeSupport.LEGACY_IS_EXTRA,
ticketNo,
actor,
) ?: return 0
linkPickOrdersToHeader(headerId, group)
return 1
@@ -1104,7 +1155,7 @@ open class DoWorkbenchReleaseService(
} else {
nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment)
}
val headerId = insertNewDeliveryOrderPickOrderHeader(first, storeId, releaseTypeCol, ticketNo)
val headerId = insertNewDeliveryOrderPickOrderHeader(first, storeId, releaseTypeCol, ticketNo, actor)
?: return 0
linkPickOrdersToHeader(headerId, group)
return 1
@@ -1206,6 +1257,7 @@ open class DoWorkbenchReleaseService(
isSingleRelease = isSingleRelease,
requiredDate = requiredDate,
ticketFloorSegment = ticketFloorSegment,
actor = resolveReleaseActor(0L),
) ?: return mergeTicketsError("CREATE_FAILED", "Failed to create TI-M merge ticket")
}

@@ -1473,6 +1525,7 @@ open class DoWorkbenchReleaseService(
isSingleRelease: Boolean,
requiredDate: LocalDate,
ticketFloorSegment: String,
actor: String,
): Long? {
val ticketNo = nextDeliveryOrderPickOrderMergeTicketNo(requiredDate, ticketFloorSegment)
val releaseType = WorkbenchReleaseTypeSupport.mergeTicketReleaseType(isSingleRelease)
@@ -1504,9 +1557,9 @@ open class DoWorkbenchReleaseService(
"ticketNo" to ticketNo,
"releaseType" to releaseType,
"created" to now,
"createdBy" to "system",
"createdBy" to actor,
"modified" to now,
"modifiedBy" to "system",
"modifiedBy" to actor,
),
)
return jdbcDao.queryForList(
@@ -1515,6 +1568,6 @@ open class DoWorkbenchReleaseService(
WHERE ticketNo = :ticketNo ORDER BY id DESC LIMIT 1
""".trimIndent(),
mapOf("ticketNo" to ticketNo),
).firstOrNull()?.get("id")?.let { (it as Number).toLong() }?.also { setRelationshipIdSelf(it) }
).firstOrNull()?.get("id")?.let { (it as Number).toLong() }?.also { setRelationshipIdSelf(it, actor) }
}
}

Carregando…
Cancelar
Salvar