Browse Source

no message

master
DESKTOP-064TTA1\Fai LUK 2 days ago
parent
commit
1d6b1087b9
9 changed files with 206 additions and 25 deletions
  1. +3
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  2. +21
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt
  3. +136
    -25
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt
  4. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt
  5. +8
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt
  6. +4
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt
  7. +2
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt
  8. +22
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt
  9. +8
    -0
      src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java View File

@@ -99,4 +99,7 @@ public abstract class SettingNames {
/** Comma-separated BOM item codes shown on /laserPrint job list (e.g. PP1175); blank = no filter (all packaging JOs) */
public static final String LASER_PRINT_ITEM_CODES = "LASER_PRINT.itemCodes";

/** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */
public static final String LASER_PRINT_LAST_RECEIVE_SUCCESS = "LASER_PRINT.lastReceiveSuccess";

}

+ 21
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/LaserBag2AutoSendService.kt View File

@@ -32,6 +32,17 @@ class LaserBag2AutoSendService(
sendsPerJob: Int = DEFAULT_SENDS_PER_JOB,
delayBetweenSendsMs: Long = DEFAULT_DELAY_BETWEEN_SENDS_MS,
): LaserBag2AutoSendReport {
val (reachable, laserIp, laserPort) = plasticBagPrinterService.probeLaserBag2Tcp()
if (!reachable) {
logger.warn("Connection failed to the laser print: {} / {}", laserIp, laserPort)
return LaserBag2AutoSendReport(
planStart = planStart,
jobOrdersFound = 0,
jobOrdersProcessed = 0,
results = emptyList(),
)
}

val orders = plasticBagPrinterService.listLaserPrintJobOrders(planStart)
val toProcess = if (limitPerRun > 0) orders.take(limitPerRun) else orders
val results = mutableListOf<LaserBag2JobSendResult>()
@@ -47,6 +58,8 @@ class LaserBag2AutoSendService(
for (jo in toProcess) {
var lastMsg = ""
var overallOk = true
var lastPrinterAck: String? = null
var lastReceiveAck = false
for (attempt in 1..sendsPerJob) {
val resp = plasticBagPrinterService.sendLaserBag2Job(
LaserBag2SendRequest(
@@ -54,9 +67,15 @@ class LaserBag2AutoSendService(
stockInLineId = jo.stockInLineId,
itemCode = jo.itemCode,
itemName = jo.itemName,
jobOrderId = jo.id,
jobOrderNo = jo.code,
lotNo = jo.lotNo,
source = "AUTO",
),
)
lastMsg = resp.message
lastPrinterAck = resp.printerAck
lastReceiveAck = resp.receiveAcknowledged
if (!resp.success) {
overallOk = false
logger.warn("Laser send failed jobOrderId={} attempt={}: {}", jo.id, attempt, resp.message)
@@ -79,6 +98,8 @@ class LaserBag2AutoSendService(
itemCode = jo.itemCode,
success = overallOk,
message = lastMsg,
printerAck = lastPrinterAck,
receiveAcknowledged = lastReceiveAck,
),
)
}


+ 136
- 25
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt View File

@@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SettingsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LaserLastReceiveSuccessDto
import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import com.ffii.fpsms.modules.jobOrder.web.model.NgpclPushResponse
@@ -47,12 +48,23 @@ import java.net.ConnectException
import java.net.SocketTimeoutException
import org.springframework.core.io.ClassPathResource
import org.slf4j.LoggerFactory
import com.fasterxml.jackson.databind.ObjectMapper
import java.time.Duration
import java.time.Instant
import java.time.LocalDate

// Data class to store bitmap bytes + width (for XML)
data class BitmapResult(val bytes: ByteArray, val width: Int)

/** One Bag2-style laser TCP attempt (internal to [PlasticBagPrinterService]). */
private data class LaserBag2TcpResult(
val success: Boolean,
val message: String,
val payload: String,
val printerAck: String?,
val receiveAcknowledged: Boolean,
)

@Service
class PlasticBagPrinterService(
val jobOrderRepository: JobOrderRepository,
@@ -61,6 +73,7 @@ class PlasticBagPrinterService(
private val settingsService: SettingsService,
private val pyJobOrderPrintSubmitService: PyJobOrderPrintSubmitService,
private val environment: Environment,
private val objectMapper: ObjectMapper,
) {
private val logger = LoggerFactory.getLogger(javaClass)

@@ -86,7 +99,43 @@ class PlasticBagPrinterService(
val itemCodes = settingsService.findByName(SettingNames.LASER_PRINT_ITEM_CODES)
.map { it.value?.trim() ?: "" }
.orElse(DEFAULT_LASER_ITEM_CODES)
return LaserBag2SettingsResponse(host = host, port = port, itemCodes = itemCodes)
return LaserBag2SettingsResponse(
host = host,
port = port,
itemCodes = itemCodes,
lastReceiveSuccess = readLaserLastReceiveSuccessFromSettings(),
)
}

private fun readLaserLastReceiveSuccessFromSettings(): LaserLastReceiveSuccessDto? {
val raw = settingsService.findByName(SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS)
.map { it.value }
.orElse(null)
?.trim()
.orEmpty()
if (raw.isBlank() || raw == "{}") return null
return try {
val dto = objectMapper.readValue(raw, LaserLastReceiveSuccessDto::class.java)
if (dto.sentAt.isNullOrBlank()) null else dto
} catch (e: Exception) {
logger.warn("Could not parse LASER_PRINT.lastReceiveSuccess: {}", e.message)
null
}
}

private fun persistLaserLastReceiveSuccess(request: LaserBag2SendRequest, printerAck: String?) {
val dto = LaserLastReceiveSuccessDto(
jobOrderId = request.jobOrderId,
jobOrderNo = request.jobOrderNo?.trim()?.takeIf { it.isNotEmpty() },
lotNo = request.lotNo?.trim()?.takeIf { it.isNotEmpty() },
itemId = request.itemId,
stockInLineId = request.stockInLineId,
printerAck = printerAck?.trim()?.takeIf { it.isNotEmpty() },
sentAt = Instant.now().toString(),
source = request.source?.trim()?.takeIf { it.isNotEmpty() } ?: "MANUAL",
)
val json = objectMapper.writeValueAsString(dto)
settingsService.update(SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS, json)
}

/**
@@ -173,22 +222,39 @@ class PlasticBagPrinterService(
itemCode = request.itemCode,
itemName = request.itemName,
)
if (first.first) {
return LaserBag2SendResponse(success = true, message = first.second, payloadSent = first.third)
val response = if (first.success) {
LaserBag2SendResponse(
success = true,
message = first.message,
payloadSent = first.payload,
printerAck = first.printerAck,
receiveAcknowledged = first.receiveAcknowledged,
)
} else {
val second = sendLaserBag2TcpOnce(
ip = ip,
port = port,
itemId = request.itemId,
stockInLineId = request.stockInLineId,
itemCode = request.itemCode,
itemName = request.itemName,
)
LaserBag2SendResponse(
success = second.success,
message = second.message,
payloadSent = second.payload,
printerAck = second.printerAck,
receiveAcknowledged = second.receiveAcknowledged,
)
}
val second = sendLaserBag2TcpOnce(
ip = ip,
port = port,
itemId = request.itemId,
stockInLineId = request.stockInLineId,
itemCode = request.itemCode,
itemName = request.itemName,
)
return LaserBag2SendResponse(
success = second.first,
message = second.second,
payloadSent = second.third,
)
if (response.success && response.receiveAcknowledged) {
try {
persistLaserLastReceiveSuccess(request, response.printerAck)
} catch (e: Exception) {
logger.warn("Could not persist laser last receive success: {}", e.message)
}
}
return response
}

private fun resolveLaserBag2Host(): String {
@@ -209,6 +275,17 @@ class PlasticBagPrinterService(
return v.toIntOrNull() ?: DEFAULT_LASER_BAG2_PORT
}

/**
* TCP connect probe to configured [LASER_PRINT.host] / [LASER_PRINT.port] (same endpoint as [sendLaserBag2Job] / auto-send).
* @return Triple(ok, host, port)
*/
fun probeLaserBag2Tcp(): Triple<Boolean, String, Int> {
val host = resolveLaserBag2Host()
val port = resolveLaserBag2Port()
val (ok, _) = checkTcpPrinter(host, port, "Laser")
return Triple(ok, host, port)
}

private fun sendLaserBag2TcpOnce(
ip: String,
port: Int,
@@ -216,7 +293,7 @@ class PlasticBagPrinterService(
stockInLineId: Long?,
itemCode: String?,
itemName: String?,
): Triple<Boolean, String, String> {
): LaserBag2TcpResult {
val codeStr = (itemCode ?: "").trim().replace(";", ",")
val nameStr = (itemName ?: "").trim().replace(";", ",")
val payload = if (itemId != null && stockInLineId != null) {
@@ -233,27 +310,61 @@ class PlasticBagPrinterService(
val out = socket.getOutputStream()
out.write(bytes)
out.flush()
// Half-close the write side so the peer sees a clean end-of-send before we read the ack.
// Abrupt full socket close right after read often makes EZCAD log "Remote TCP client disconnected".
try {
socket.shutdownOutput()
} catch (_: Exception) {
}
var ackRaw: String? = null
var receiveAck = false
socket.soTimeout = 500
try {
val buf = ByteArray(4096)
val n = socket.getInputStream().read(buf)
if (n > 0) {
val ack = String(buf, 0, n, StandardCharsets.UTF_8).trim().lowercase()
if (ack.contains("receive") && !ack.contains("invalid")) {
return Triple(true, "已送出激光機:$payload(已確認)", payload)
ackRaw = String(buf, 0, n, StandardCharsets.UTF_8).trim()
val ackLower = ackRaw.lowercase()
if (ackLower.contains("receive") && !ackLower.contains("invalid")) {
receiveAck = true
logger.info(
"Laser TCP ack (receive): {}:{} sent={} ack={}",
ip,
port,
payload,
ackRaw,
)
} else if (ackRaw.isNotEmpty()) {
logger.info(
"Laser TCP response: {}:{} sent={} ack={}",
ip,
port,
payload,
ackRaw,
)
}
}
} catch (_: SocketTimeoutException) {
// Same as Python: ignore read timeout, treat as sent
// Same as Python Bag3: ignore read timeout, payload was still sent
}
val msg = if (receiveAck) {
"已送出激光機:$payload(已確認)"
} else {
"已送出激光機:$payload"
}
return Triple(true, "已送出激光機:$payload", payload)
return LaserBag2TcpResult(true, msg, payload, ackRaw, receiveAck)
} catch (e: ConnectException) {
return Triple(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload)
return LaserBag2TcpResult(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload, null, false)
} catch (e: SocketTimeoutException) {
return Triple(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload)
return LaserBag2TcpResult(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload, null, false)
} catch (e: Exception) {
return Triple(false, "激光機送出失敗:${e.message}", payload)
return LaserBag2TcpResult(false, "激光機送出失敗:${e.message}", payload, null, false)
} finally {
try {
Thread.sleep(100)
} catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
try {
socket?.close()
} catch (_: Exception) {


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2AutoSendReport.kt View File

@@ -15,4 +15,6 @@ data class LaserBag2JobSendResult(
val itemCode: String?,
val success: Boolean,
val message: String,
val printerAck: String? = null,
val receiveAcknowledged: Boolean = false,
)

+ 8
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendRequest.kt View File

@@ -3,6 +3,9 @@ package com.ffii.fpsms.modules.jobOrder.web.model
/**
* Body for Bag2.py-style laser TCP send: `json;itemCode;itemName;;` (UTF-8).
* Optional [printerIp] / [printerPort] override system settings [LASER_PRINT.host] / [LASER_PRINT.port].
*
* Optional job metadata is used to persist [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS]
* when the printer returns a receive ack.
*/
data class LaserBag2SendRequest(
val itemId: Long? = null,
@@ -11,4 +14,9 @@ data class LaserBag2SendRequest(
val itemName: String? = null,
val printerIp: String? = null,
val printerPort: Int? = null,
val jobOrderId: Long? = null,
val jobOrderNo: String? = null,
val lotNo: String? = null,
/** AUTO (auto-send) or MANUAL (/laserPrint); optional. */
val source: String? = null,
)

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SendResponse.kt View File

@@ -4,4 +4,8 @@ data class LaserBag2SendResponse(
val success: Boolean,
val message: String,
val payloadSent: String? = null,
/** Raw bytes from the laser TCP peer after our payload (often `receive;;`). */
val printerAck: String? = null,
/** True when [printerAck] contained `receive` and not `invalid` (same rule as Bag3.py). */
val receiveAcknowledged: Boolean = false,
)

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserBag2SettingsResponse.kt View File

@@ -2,9 +2,11 @@ package com.ffii.fpsms.modules.jobOrder.web.model

/**
* @param itemCodes Comma-separated item codes for the laser job list filter (e.g. `PP1175` or `PP1175,AB123`). Empty string means no filter (show all packaging job orders).
* @param lastReceiveSuccess Last job where the laser printer returned a receive ack (from settings JSON); null if never recorded or empty.
*/
data class LaserBag2SettingsResponse(
val host: String,
val port: Int,
val itemCodes: String,
val lastReceiveSuccess: LaserLastReceiveSuccessDto? = null,
)

+ 22
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/LaserLastReceiveSuccessDto.kt View File

@@ -0,0 +1,22 @@
package com.ffii.fpsms.modules.jobOrder.web.model

import com.fasterxml.jackson.annotation.JsonIgnoreProperties

/**
* Persisted in [com.ffii.fpsms.modules.common.SettingNames.LASER_PRINT_LAST_RECEIVE_SUCCESS] when the laser
* TCP peer returns a receive-style ack ([com.ffii.fpsms.modules.jobOrder.web.model.LaserBag2SendResponse.receiveAcknowledged]).
*/
@JsonIgnoreProperties(ignoreUnknown = true)
data class LaserLastReceiveSuccessDto(
val jobOrderId: Long? = null,
/** Job order code (工單號). */
val jobOrderNo: String? = null,
val lotNo: String? = null,
val itemId: Long? = null,
val stockInLineId: Long? = null,
val printerAck: String? = null,
/** ISO-8601 instant string (server UTC). */
val sentAt: String? = null,
/** e.g. AUTO (scheduler/auto-send) or MANUAL (/laserPrint row). */
val source: String? = null,
)

+ 8
- 0
src/main/resources/db/changelog/changes/20260327_laser_last_receive_success/01_laser_print_last_receive_success.sql View File

@@ -0,0 +1,8 @@
--liquibase formatted sql
--changeset fpsms:20260327_laser_print_last_receive_success

INSERT INTO `settings` (`name`, `value`, `category`, `type`)
SELECT 'LASER_PRINT.lastReceiveSuccess', '{}', 'LASER', 'string'
WHERE NOT EXISTS (
SELECT 1 FROM `settings` s WHERE s.name = 'LASER_PRINT.lastReceiveSuccess'
);

Loading…
Cancel
Save