Parcourir la source

no message

production
[email protected] il y a 4 jours
Parent
révision
5eba1a42f2
6 fichiers modifiés avec 252 ajouts et 22 suppressions
  1. +42
    -5
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  2. +2
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  3. +180
    -16
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  4. +10
    -0
      src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt
  5. +12
    -0
      src/main/resources/application-prod.yml
  6. +6
    -1
      src/main/resources/application.yml

+ 42
- 5
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Voir le fichier

@@ -151,9 +151,13 @@ open class M18DeliveryOrderService(
return deliveryOrder
}

open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult {
open fun saveDeliveryOrders(request: M18CommonRequest, skipExistingDo: Boolean = false): SyncResult {
val deliveryOrdersWithType = getDeliveryOrdersWithType(request)
return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType, syncisExtra = false)
return saveDeliveryOrdersWithPreparedList(
deliveryOrdersWithType,
syncisExtra = false,
skipExistingDo = skipExistingDo,
)
}

/**
@@ -209,16 +213,21 @@ open class M18DeliveryOrderService(
query = conds
)

return saveDeliveryOrdersWithPreparedList(prepared, syncisExtra = isExtraSync)
return saveDeliveryOrdersWithPreparedList(prepared, syncisExtra = isExtraSync, skipExistingDo = newOnly)
}

private fun saveDeliveryOrdersWithPreparedList(
deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?,
syncisExtra: Boolean = false,
skipExistingDo: Boolean = false,
): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------")
if (skipExistingDo) {
logger.info("skipExistingDo=true — local delivery orders will not be updated")
}

val successList = mutableListOf<Long>()
val skippedList = mutableListOf<Long>()
val successDetailList = mutableListOf<Long>()
val failList = mutableListOf<Long>()
val failDetailList = mutableListOf<Long>()
@@ -241,6 +250,22 @@ open class M18DeliveryOrderService(

if (deliveryOrdersValues != null) {
deliveryOrdersValues.forEach { deliveryOrder ->
if (skipExistingDo) {
val latestDeliveryOrderLog =
m18DataLogService.findLatestM18DataLogWithSuccess(deliveryOrder.id, doRefType)
val existingByM18 = latestDeliveryOrderLog?.id?.let {
deliveryOrderService.findByM18DataLogId(it)
}
if (existingByM18 != null && existingByM18.deleted != true) {
logger.info(
"${doRefType}: skipExistingDo — skipping M18 id=${deliveryOrder.id} " +
"code=${existingByM18.code} localId=${existingByM18.id} status=${existingByM18.status}"
)
skippedList.add(deliveryOrder.id)
return@forEach
}
}

val deliveryOrderDetail = getDeliveryOrder(deliveryOrder.id)

var deliveryOrderId: Long? = null //FP-MTMS
@@ -254,6 +279,14 @@ open class M18DeliveryOrderService(

// delivery_order + m18_data_log table
if (mainpo != null) {
if (skipExistingDo && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(mainpo.code)) {
logger.info(
"${doRefType}: skipExistingDo — skipping M18 id=${deliveryOrder.id} code=${mainpo.code} (local DO exists by code)"
)
skippedList.add(deliveryOrder.id)
return@forEach
}

// Find the latest m18 data log by m18 id & type
// logger.info("${doRefType}: Finding For Latest M18 Data Log...")
val latestDeliveryOrderLog =
@@ -573,6 +606,9 @@ open class M18DeliveryOrderService(
// End of save. Check result
logger.info("Total Success (${doRefType}) (${successList.size})")
logger.error("Total Fail (${doRefType}) (${failList.size}): $failList")
if (skippedList.isNotEmpty()) {
logger.info("Total Skipped (${doRefType}) (${skippedList.size}): $skippedList")
}

logger.info("Total Success (${doLineRefType}) (${successDetailList.size})")
logger.error("Total Fail (${doLineRefType}) (${failDetailList.size}): $failDetailList")
@@ -585,11 +621,12 @@ open class M18DeliveryOrderService(

logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------")

val skippedSuffix = if (skippedList.isNotEmpty()) " | skipped=${skippedList.size}" else ""
return SyncResult(
totalProcessed = successList.size + failList.size,
totalProcessed = successList.size + failList.size + skippedList.size,
totalSuccess = successList.size,
totalFail = failList.size,
query = deliveryOrdersWithType?.query ?: ""
query = (deliveryOrdersWithType?.query ?: "") + skippedSuffix,
)
}
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java Voir le fichier

@@ -28,6 +28,8 @@ public abstract class SettingNames {
public static final String SCHEDULE_M18_DO1 = "SCHEDULE.m18.do1";
/** Saturday-only DO1 time (default 03:10). Mon–Fri & Sun use [SCHEDULE_M18_DO1] time via a second trigger. */
public static final String SCHEDULE_M18_DO1_SAT = "SCHEDULE.m18.do1.sat";
/** Comma-separated dDates (yyyy-MM-dd) of completed one-time DO1 catch-ups ([scheduler.do1CatchUp]). */
public static final String SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE = "SCHEDULE.m18.do1.catchup.doneDDate";
public static final String SCHEDULE_M18_DO2 = "SCHEDULE.m18.do2";

/** Daily push FPSMS BOMs → M18 udfBomForShop (default 23:00; requires [M18_BOM_SHOP_SYNC_ENABLED] and scheduler.m18Sync.enabled). */


+ 180
- 16
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Voir le fichier

@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.HashMap
import java.util.concurrent.ScheduledFuture
@@ -44,6 +45,14 @@ open class SchedulerService(
/** When false (default), M18 PO / DO1 / DO2 / master-data cron jobs are not registered — use true in production only. */
@Value("\${scheduler.m18Sync.enabled:false}") val m18SyncEnabled: Boolean,
@Value("\${scheduler.jo.planStart.enabled:true}") val jobOrderPlanStartAutoEnabled: Boolean,
@Value("\${scheduler.do1CatchUp.enabled:false}") val do1CatchUpEnabled: Boolean,
@Value("\${scheduler.do1CatchUp.dDate:}") val do1CatchUpDDate: String,
@Value("\${scheduler.do1CatchUp.runAt:}") val do1CatchUpRunAt: String,
@Value("\${scheduler.do1CatchUp.skipExistingDo:true}") val do1CatchUpSkipExistingDo: Boolean,
@Value("\${scheduler.do1CatchUp2.enabled:false}") val do1CatchUp2Enabled: Boolean,
@Value("\${scheduler.do1CatchUp2.dDate:}") val do1CatchUp2DDate: String,
@Value("\${scheduler.do1CatchUp2.runAt:}") val do1CatchUp2RunAt: String,
@Value("\${scheduler.do1CatchUp2.skipExistingDo:true}") val do1CatchUp2SkipExistingDo: Boolean,
val settingsService: SettingsService,
/**
* Lookback window for GRN code sync: rows with `created` from **start of (today − N days)** through **now**,
@@ -100,6 +109,9 @@ open class SchedulerService(

var scheduledJobOrderPlanStart: ScheduledFuture<*>? = null

var scheduledDo1CatchUp: ScheduledFuture<*>? = null
var scheduledDo1CatchUp2: ScheduledFuture<*>? = null

//@Volatile
//var scheduledRoughProd: ScheduledFuture<*>? = null

@@ -191,10 +203,144 @@ open class SchedulerService(
scheduleGrnCodeSync();
scheduleInventoryLotExpiry();
scheduleJobOrderPlanStartAuto();
scheduleDo1CatchUpOnce();
//scheduleRoughProd();
//scheduleDetailedProd();
}

/**
* One-time DO1 catch-up jobs for fixed dDates (e.g. missed 15/6 → dDate 17/6, 16/6 → dDate 18/6).
* Requires [m18SyncEnabled] (production only). Config: scheduler.do1CatchUp / do1CatchUp2 in application-prod.yml.
* Completed dDates are stored comma-separated in [SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE].
*/
fun scheduleDo1CatchUpOnce() {
scheduledDo1CatchUp?.cancel(false)
scheduledDo1CatchUp = null
scheduledDo1CatchUp2?.cancel(false)
scheduledDo1CatchUp2 = null

if (!m18SyncEnabled) {
logger.info("DO1 catch-up schedulers disabled (scheduler.m18Sync.enabled=false; production only)")
return
}

scheduledDo1CatchUp = scheduleOneDo1CatchUp(
scheduledDo1CatchUp,
do1CatchUpEnabled,
do1CatchUpDDate,
do1CatchUpRunAt,
do1CatchUpSkipExistingDo,
"do1CatchUp",
)
scheduledDo1CatchUp2 = scheduleOneDo1CatchUp(
scheduledDo1CatchUp2,
do1CatchUp2Enabled,
do1CatchUp2DDate,
do1CatchUp2RunAt,
do1CatchUp2SkipExistingDo,
"do1CatchUp2",
)
}

private fun scheduleOneDo1CatchUp(
existing: ScheduledFuture<*>?,
enabled: Boolean,
dDateRaw: String,
runAtRaw: String,
skipExistingDo: Boolean,
configKey: String,
): ScheduledFuture<*>? {
existing?.cancel(false)
if (!enabled) {
return null
}
val dDateStr = dDateRaw.trim()
val runAtStr = runAtRaw.trim()
if (dDateStr.isEmpty() || runAtStr.isEmpty()) {
logger.warn("{} enabled but dDate or runAt is blank — skipped", configKey)
return null
}

val dDate = try {
LocalDate.parse(dDateStr)
} catch (e: Exception) {
logger.error("Invalid scheduler.{}.dDate={}", configKey, dDateStr)
return null
}
val runAt = try {
LocalDateTime.parse(runAtStr)
} catch (e: Exception) {
logger.error("Invalid scheduler.{}.runAt={}", configKey, runAtStr)
return null
}

if (isDo1CatchUpAlreadyDone(dDate)) {
logger.info("DO1 catch-up ({}) already completed for dDate={}", configKey, dDate)
return null
}

val now = LocalDateTime.now()
if (!runAt.isAfter(now)) {
logger.warn(
"DO1 catch-up ({}) runAt={} is not in the future (now={}); use GET /scheduler/trigger/do1-catchup?dDate={}",
configKey,
runAt,
now,
dDate,
)
return null
}

val scheduled = taskScheduler.schedule(
{ runDo1CatchUp(dDate, skipExistingDo) },
runAt.atZone(ZoneId.systemDefault()).toInstant(),
)
logger.info(
"Scheduled one-time DO1 catch-up ({}) for dDate={} at {} skipExistingDo={}",
configKey,
dDate,
runAt,
skipExistingDo,
)
return scheduled
}

private fun getDo1CatchUpDoneDDateSet(): Set<String> {
val done = settingsService.findByName(SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE).getOrNull()?.value
?: return emptySet()
return done.split(",").map { it.trim() }.filter { it.isNotEmpty() }.toSet()
}

private fun isDo1CatchUpAlreadyDone(dDate: LocalDate): Boolean {
return dDate.toString() in getDo1CatchUpDoneDDateSet()
}

private fun markDo1CatchUpDone(dDate: LocalDate) {
val updated = (getDo1CatchUpDoneDDateSet() + dDate.toString()).sorted()
settingsService.update(SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE, updated.joinToString(","))
}

open fun runDo1CatchUp(dDate: LocalDate, skipExistingDo: Boolean = true) {
if (!m18SyncEnabled) {
logger.warn(
"DO1 catch-up refused for dDate={}: production only (scheduler.m18Sync.enabled=false)",
dDate,
)
return
}
if (isDo1CatchUpAlreadyDone(dDate)) {
logger.info("DO1 catch-up already completed for dDate={}", dDate)
return
}
try {
getM18Dos1ForDDate(dDate, syncType = "DO1_CATCHUP", skipExistingDo = skipExistingDo)
markDo1CatchUpDone(dDate)
logger.info("DO1 catch-up completed for dDate={}", dDate)
} catch (e: Exception) {
logger.error("DO1 catch-up failed for dDate={}: {}", dDate, e.message, e)
}
}

// Scheduler
// --------------------------- FP-MTMS --------------------------- //
//fun scheduleRoughProd() {
@@ -480,24 +626,42 @@ open class SchedulerService(
open fun getM18Dos1() {
logger.info("DO Scheduler 1 - DO")
val currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val twoDaysLater = today.plusDays(2L)

var requestDO = M18CommonRequest(
dDateTo = twoDaysLater.format(dateTimeStringFormat),
dDateFrom = twoDaysLater.format(dateTimeStringFormat)
)
val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO);
val today = LocalDateTime.now().toLocalDate().atStartOfDay()
val dDate = today.plusDays(2L).toLocalDate()
getM18Dos1ForDDate(dDate, syncType = "DO1")
}

saveSyncLog(
type = "DO1",
status = "SUCCESS",
result = result,
start = currentTime
/** DO1 sync for an explicit delivery date (normal DO1 uses run-day + 2 days). */
open fun getM18Dos1ForDDate(
dDate: LocalDate,
syncType: String = "DO1",
skipExistingDo: Boolean = syncType == "DO1_CATCHUP",
) {
logger.info("{} sync for dDate={} skipExistingDo={}", syncType, dDate, skipExistingDo)
val currentTime = LocalDateTime.now()
val dDateStart = dDate.atStartOfDay()
val requestDO = M18CommonRequest(
dDateTo = dDateStart.format(dateTimeStringFormat),
dDateFrom = dDateStart.format(dateTimeStringFormat),
)
try {
val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO, skipExistingDo = skipExistingDo)
saveSyncLog(
type = syncType,
status = "SUCCESS",
result = result?.copy(query = "dDate=$dDate ${result.query}".trim()),
start = currentTime,
)
} catch (e: Exception) {
logger.error("{} sync failed for dDate={}: {}", syncType, dDate, e.message, e)
saveSyncLog(
type = syncType,
status = "FAILED",
error = e.message,
start = currentTime,
)
throw e
}
}

private fun saveSyncLog(type: String, status: String, result: SyncResult? = null, error: String? = null, start: LocalDateTime) {


+ 10
- 0
src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt Voir le fichier

@@ -43,6 +43,16 @@ class SchedulerController(
return "M18 DO1 Sync Triggered Successfully"
}

/** Manual DO1 catch-up for a fixed dDate (production only). Skips existing local DOs by default. */
@GetMapping("/trigger/do1-catchup")
fun triggerDo1CatchUp(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) dDate: LocalDate,
@RequestParam(required = false, defaultValue = "true") skipExistingDo: Boolean = true,
): String {
schedulerService.runDo1CatchUp(dDate, skipExistingDo = skipExistingDo)
return "M18 DO1 catch-up triggered for dDate=$dDate skipExistingDo=$skipExistingDo"
}

@GetMapping("/trigger/do2")
fun triggerDo2(): String {
schedulerService.getM18Dos2()


+ 12
- 0
src/main/resources/application-prod.yml Voir le fichier

@@ -40,6 +40,18 @@ scheduler:
syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code
inventoryLotExpiry:
enabled: true
# One-time DO1 catch-ups (only registered when scheduler.m18Sync.enabled=true — production profile).
# skipExistingDo defaults true for catch-up: already-synced/picked DOs are not overwritten.
do1CatchUp:
enabled: true
skipExistingDo: true
dDate: "2026-06-17"
runAt: "2026-06-17T11:30:00"
do1CatchUp2:
enabled: true
skipExistingDo: true
dDate: "2026-06-18"
runAt: "2026-06-16T15:00:00"

# Laser Bag2 (/laserPrint) TCP auto-send; uses LASER_PRINT host/port/itemCodes from DB and sends first matching job only.
laser:


+ 6
- 1
src/main/resources/application.yml Voir le fichier

@@ -12,7 +12,7 @@ server:
# PostCompletedDn GRN: runs daily at 00:01, processes all POs with receipt date = yesterday.
# Set enabled: false to disable. Optional receiptDate: "yyyy-MM-dd" overrides for testing only.
# m18Grn.createEnabled: M18 GRN PUT/create — false outside production so UAT/dev never posts GRNs.
# m18Sync: M18 cron jobs for PO, DO1, DO2, BOM→M18 udfBomForShop ([SCHEDULE.m18.bom.shop], default 23:00), master data — false outside production (manual /trigger/* still works).
# m18Sync: M18 cron jobs for PO, DO1, DO2, DO1 catch-up, BOM→M18 udfBomForShop, master data — false outside production (manual /trigger/* still works except do1-catchup).
truck:
lane:
schedule:
@@ -37,6 +37,11 @@ scheduler:
jo:
planStart:
enabled: true
# One-time DO1 catch-up jobs (production only — requires scheduler.m18Sync.enabled=true in application-prod.yml).
do1CatchUp:
enabled: false
do1CatchUp2:
enabled: false

# Nav: PO stock_in_line pending/receiving within last N days (see ProductProcessService for 工單 QC/上架:今日+昨日).
fpsms:


Chargement…
Annuler
Enregistrer