@@ -1,12 +1,20 @@
package com.ffii.fpsms.modules.jobOrder.service
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.jobOrder.entity.JobOrder
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.PrintRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import com.ffii.fpsms.modules.jobOrder.web.model.OnPackQrJobOrderRequest
import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.py.PyJobOrderListItem
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.awt.Color
import java.awt.Font
import java.awt.Graphics2D
@@ -32,6 +40,7 @@ import java.net.ConnectException
import java.net.SocketTimeoutException
import org.springframework.core.io.ClassPathResource
import org.slf4j.LoggerFactory
import java.time.LocalDate
// Data class to store bitmap bytes + width (for XML)
data class BitmapResult(val bytes: ByteArray, val width: Int)
@@ -41,9 +50,197 @@ open class PlasticBagPrinterService(
val jobOrderRepository: JobOrderRepository,
private val jdbcDao: JdbcDao,
private val stockInLineRepository: StockInLineRepository,
private val settingsService: SettingsService,
) {
private val logger = LoggerFactory.getLogger(javaClass)
companion object {
private const val DEFAULT_LASER_BAG2_HOST = "192.168.18.77"
private const val DEFAULT_LASER_BAG2_PORT = 45678
private const val DEFAULT_LASER_ITEM_CODES = "PP1175"
private const val PACKAGING_PROCESS_NAME = "包裝"
}
fun getLaserBag2Settings(): LaserBag2SettingsResponse {
val hostRaw = settingsService.findByName(SettingNames.LASER_PRINT_HOST)
.map { it.value }
.orElse(null)
?.trim()
val host = if (hostRaw.isNullOrBlank()) DEFAULT_LASER_BAG2_HOST else hostRaw
val portStr = settingsService.findByName(SettingNames.LASER_PRINT_PORT)
.map { it.value }
.orElse(null)
?.trim()
.orEmpty()
val port = portStr.toIntOrNull() ?: DEFAULT_LASER_BAG2_PORT
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)
}
/**
* Same criteria as [com.ffii.fpsms.py.PyController.listJobOrders], filtered by [SettingNames.LASER_PRINT_ITEM_CODES]
* (comma-separated item codes). Blank / whitespace-only setting value means no filter (all packaging job orders).
*/
@Transactional(readOnly = true)
open fun listLaserPrintJobOrders(planStart: LocalDate): List<PyJobOrderListItem> {
val dayStart = planStart.atStartOfDay()
val dayEndExclusive = planStart.plusDays(1).atStartOfDay()
val orders = jobOrderRepository.findByDeletedFalseAndPlanStartBetweenAndBomProcessNameOrderByIdAsc(
dayStart,
dayEndExclusive,
PACKAGING_PROCESS_NAME,
)
val opt = settingsService.findByName(SettingNames.LASER_PRINT_ITEM_CODES)
val raw = if (opt.isPresent) opt.get().value else null
val allowed = when {
raw == null -> parseLaserItemCodeFilters(DEFAULT_LASER_ITEM_CODES)
raw.trim().isEmpty() -> emptySet()
else -> parseLaserItemCodeFilters(raw.trim())
}
val filtered = if (allowed.isEmpty()) {
orders
} else {
orders.filter { jo ->
val code = (jo.bom?.item?.code ?: jo.bom?.code)?.trim().orEmpty()
allowed.contains(code)
}
}
return filtered.map { jo -> toPyJobOrderListItem(jo) }
}
private fun parseLaserItemCodeFilters(raw: String?): Set<String> {
if (raw.isNullOrBlank()) return emptySet()
return raw.split(',')
.map { it.trim() }
.filter { it.isNotEmpty() }
.toSet()
}
private fun toPyJobOrderListItem(jo: JobOrder): PyJobOrderListItem {
val itemCode = jo.bom?.item?.code ?: jo.bom?.code
val itemName = jo.bom?.name ?: jo.bom?.item?.name
val itemId = jo.bom?.item?.id
val stockInLine = jo.id?.let { stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(it) }
val stockInLineId = stockInLine?.id
val lotNo = stockInLine?.lotNo
return PyJobOrderListItem(
id = jo.id!!,
code = jo.code,
planStart = jo.planStart,
itemCode = itemCode,
itemName = itemName,
reqQty = jo.reqQty,
stockInLineId = stockInLineId,
itemId = itemId,
lotNo = lotNo,
)
}
/**
* Bag2.py [send_job_to_laser] / [send_job_to_laser_with_retry]: UTF-8 TCP payload and optional ack read.
*/
fun sendLaserBag2Job(request: LaserBag2SendRequest): LaserBag2SendResponse {
val ip = (request.printerIp?.trim()?.takeIf { it.isNotEmpty() }
?: resolveLaserBag2Host()).trim()
val port = request.printerPort ?: resolveLaserBag2Port()
val first = sendLaserBag2TcpOnce(
ip = ip,
port = port,
itemId = request.itemId,
stockInLineId = request.stockInLineId,
itemCode = request.itemCode,
itemName = request.itemName,
)
if (first.first) {
return LaserBag2SendResponse(success = true, message = first.second, payloadSent = first.third)
}
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,
)
}
private fun resolveLaserBag2Host(): String {
val v = settingsService.findByName(SettingNames.LASER_PRINT_HOST)
.map { it.value }
.orElse(null)
?.trim()
.orEmpty()
return v.ifBlank { DEFAULT_LASER_BAG2_HOST }
}
private fun resolveLaserBag2Port(): Int {
val v = settingsService.findByName(SettingNames.LASER_PRINT_PORT)
.map { it.value }
.orElse(null)
?.trim()
.orEmpty()
return v.toIntOrNull() ?: DEFAULT_LASER_BAG2_PORT
}
private fun sendLaserBag2TcpOnce(
ip: String,
port: Int,
itemId: Long?,
stockInLineId: Long?,
itemCode: String?,
itemName: String?,
): Triple<Boolean, String, String> {
val codeStr = (itemCode ?: "").trim().replace(";", ",")
val nameStr = (itemName ?: "").trim().replace(";", ",")
val payload = if (itemId != null && stockInLineId != null) {
"{\"itemID\":$itemId,\"stockInLineId\":$stockInLineId};$codeStr;$nameStr;;"
} else {
"0;$codeStr;$nameStr;;"
}
val bytes = payload.toByteArray(StandardCharsets.UTF_8)
var socket: Socket? = null
try {
socket = Socket()
socket.soTimeout = 3000
socket.connect(InetSocketAddress(ip, port), 3000)
val out = socket.getOutputStream()
out.write(bytes)
out.flush()
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)
}
}
} catch (_: SocketTimeoutException) {
// Same as Python: ignore read timeout, treat as sent
}
return Triple(true, "已送出激光機:$payload", payload)
} catch (e: ConnectException) {
return Triple(false, "無法連線至 $ip:$port,請確認激光機已開機且 IP 正確。", payload)
} catch (e: SocketTimeoutException) {
return Triple(false, "連線逾時 ($ip:$port),請檢查網路與連接埠。", payload)
} catch (e: Exception) {
return Triple(false, "激光機送出失敗:${e.message}", payload)
} finally {
try {
socket?.close()
} catch (_: Exception) {
}
}
}
fun generatePrintJobBundle(
itemCode: String,
lotNo: String,