diff --git a/src/main/java/com/xujie/sys/modules/rack/controller/RackClosedLoopController.java b/src/main/java/com/xujie/sys/modules/rack/controller/RackClosedLoopController.java index f01cd0b..1ff0cc7 100644 --- a/src/main/java/com/xujie/sys/modules/rack/controller/RackClosedLoopController.java +++ b/src/main/java/com/xujie/sys/modules/rack/controller/RackClosedLoopController.java @@ -5,11 +5,15 @@ import com.xujie.sys.modules.rack.dto.RackBindRequest; import com.xujie.sys.modules.rack.dto.RackBatchCreateRequest; import com.xujie.sys.modules.rack.dto.RackJobAvailableMaterialRequest; import com.xujie.sys.modules.rack.dto.RackJobCreateRequest; +import com.xujie.sys.modules.rack.dto.RackPackagePrintRequest; +import com.xujie.sys.modules.rack.dto.RackPlcIngestRequest; import com.xujie.sys.modules.rack.dto.RackProductionActionRequest; import com.xujie.sys.modules.rack.dto.RackRoutePoolConfigRequest; import com.xujie.sys.modules.rack.dto.RackStationPassRequest; import com.xujie.sys.modules.rack.dto.RackTraceQueryRequest; import com.xujie.sys.modules.rack.entity.*; +import com.xujie.sys.modules.rack.service.impl.RackClosedLoopAddonService; +import org.apache.commons.lang3.StringUtils; import com.xujie.sys.modules.rack.service.RackClosedLoopService; import org.springframework.web.bind.annotation.*; @@ -20,9 +24,14 @@ import java.util.Map; public class RackClosedLoopController { private final RackClosedLoopService rackClosedLoopService; + private final RackClosedLoopAddonService rackClosedLoopAddonService; - public RackClosedLoopController(RackClosedLoopService rackClosedLoopService) { + public RackClosedLoopController( + RackClosedLoopService rackClosedLoopService, + RackClosedLoopAddonService rackClosedLoopAddonService + ) { this.rackClosedLoopService = rackClosedLoopService; + this.rackClosedLoopAddonService = rackClosedLoopAddonService; } @PostMapping("/part/list") @@ -402,6 +411,10 @@ public class RackClosedLoopController { @PostMapping("/production/downhang") public R bindDownHangByCode(@RequestBody RackProductionActionRequest request) { + if (StringUtils.isBlank(request.getRackCode()) && StringUtils.isNotBlank(request.getBatchCode())) { + String resolvedRackCode = rackClosedLoopAddonService.resolveDownHangRackCodeByBatch(request.getJobCode(), request.getBatchCode()); + request.setRackCode(resolvedRackCode); + } rackClosedLoopService.bindDownHangByCode(request); return R.ok(); } @@ -444,6 +457,104 @@ public class RackClosedLoopController { return R.ok(); } + @PostMapping("/device/list") + public R listExternalDevice(@RequestBody(required = false) RackExternalDeviceEntity query) { + return R.ok().put("rows", rackClosedLoopAddonService.listExternalDevice(query)); + } + + @PostMapping("/device/save") + public R saveExternalDevice(@RequestBody RackExternalDeviceEntity entity) { + rackClosedLoopAddonService.saveExternalDevice(entity); + return R.ok(); + } + + @PostMapping("/device/update") + public R updateExternalDevice(@RequestBody RackExternalDeviceEntity entity) { + rackClosedLoopAddonService.updateExternalDevice(entity); + return R.ok(); + } + + @PostMapping("/device/delete/{deviceId}") + public R deleteExternalDevice(@PathVariable Long deviceId) { + rackClosedLoopAddonService.deleteExternalDevice(deviceId); + return R.ok(); + } + + @PostMapping("/device/delete-by-code") + public R deleteExternalDeviceByCode(@RequestBody Map data) { + String deviceCode = data == null ? null : String.valueOf(data.getOrDefault("deviceCode", "")); + rackClosedLoopAddonService.deleteExternalDeviceByCode(deviceCode); + return R.ok(); + } + + @PostMapping("/plc/ingest") + public R ingestPlcEvent(@RequestBody RackPlcIngestRequest request) { + return R.ok().put("result", rackClosedLoopAddonService.ingestPlcEvent(request)); + } + + @GetMapping("/plc/status") + public R listPlcStatus() { + return R.ok().put("rows", rackClosedLoopAddonService.listPlcStatus()); + } + + @PostMapping("/package/order/list") + public R listPackageOrder(@RequestBody(required = false) RackPackageOrderEntity query) { + return R.ok().put("rows", rackClosedLoopAddonService.listPackageOrder(query)); + } + + @PostMapping("/package/order/save") + public R savePackageOrder(@RequestBody RackPackageOrderEntity entity) { + rackClosedLoopAddonService.savePackageOrder(entity); + return R.ok(); + } + + @PostMapping("/package/order/update") + public R updatePackageOrder(@RequestBody RackPackageOrderEntity entity) { + rackClosedLoopAddonService.updatePackageOrder(entity); + return R.ok(); + } + + @PostMapping("/package/order/delete-by-no") + public R deletePackageOrderByNo(@RequestBody Map data) { + String packageNo = data == null ? null : String.valueOf(data.getOrDefault("packageNo", "")); + rackClosedLoopAddonService.deletePackageOrderByNo(packageNo); + return R.ok(); + } + + @PostMapping("/package/template/list") + public R listLabelTemplate(@RequestBody(required = false) RackLabelTemplateEntity query) { + return R.ok().put("rows", rackClosedLoopAddonService.listLabelTemplate(query)); + } + + @PostMapping("/package/template/save") + public R saveLabelTemplate(@RequestBody RackLabelTemplateEntity entity) { + rackClosedLoopAddonService.saveLabelTemplate(entity); + return R.ok(); + } + + @PostMapping("/package/template/update") + public R updateLabelTemplate(@RequestBody RackLabelTemplateEntity entity) { + rackClosedLoopAddonService.updateLabelTemplate(entity); + return R.ok(); + } + + @PostMapping("/package/template/delete-by-code") + public R deleteLabelTemplateByCode(@RequestBody Map data) { + String templateCode = data == null ? null : String.valueOf(data.getOrDefault("templateCode", "")); + rackClosedLoopAddonService.deleteLabelTemplateByCode(templateCode); + return R.ok(); + } + + @PostMapping("/package/label/print") + public R printPackageLabel(@RequestBody RackPackagePrintRequest request) { + return R.ok().put("result", rackClosedLoopAddonService.printPackageLabel(request)); + } + + @PostMapping("/package/print-log/list") + public R listLabelPrintLog(@RequestBody(required = false) RackLabelPrintLogEntity query) { + return R.ok().put("rows", rackClosedLoopAddonService.listLabelPrintLog(query)); + } + @PostMapping("/trace/query") public R traceQuery(@RequestBody RackTraceQueryRequest request) { return R.ok().put("result", rackClosedLoopService.traceQuery(request)); diff --git a/src/main/java/com/xujie/sys/modules/rack/dto/RackPackagePrintRequest.java b/src/main/java/com/xujie/sys/modules/rack/dto/RackPackagePrintRequest.java new file mode 100644 index 0000000..f24c913 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/dto/RackPackagePrintRequest.java @@ -0,0 +1,11 @@ +package com.xujie.sys.modules.rack.dto; + +import lombok.Data; + +@Data +public class RackPackagePrintRequest { + private String packageNo; + private String templateCode; + private Integer copies; + private String operatorName; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/dto/RackPlcIngestRequest.java b/src/main/java/com/xujie/sys/modules/rack/dto/RackPlcIngestRequest.java new file mode 100644 index 0000000..4a658b3 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/dto/RackPlcIngestRequest.java @@ -0,0 +1,21 @@ +package com.xujie.sys.modules.rack.dto; + +import lombok.Data; + +import java.util.Date; + +@Data +public class RackPlcIngestRequest { + private String deviceCode; + private String jobCode; + private String rackCode; + private String lineId; + private String stationId; + private String stepCode; + private String sourceType; + private String payloadJson; + private String operatorName; + private Date eventTime; + private Boolean fallbackManual; + private String missingReason; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/dto/RackProductionActionRequest.java b/src/main/java/com/xujie/sys/modules/rack/dto/RackProductionActionRequest.java index 168ec21..df3ed4a 100644 --- a/src/main/java/com/xujie/sys/modules/rack/dto/RackProductionActionRequest.java +++ b/src/main/java/com/xujie/sys/modules/rack/dto/RackProductionActionRequest.java @@ -6,6 +6,7 @@ import lombok.Data; public class RackProductionActionRequest { private String jobCode; private String rackCode; + private String batchCode; private String lineId; private String stationId; private String stepCode; diff --git a/src/main/java/com/xujie/sys/modules/rack/entity/RackExternalDeviceEntity.java b/src/main/java/com/xujie/sys/modules/rack/entity/RackExternalDeviceEntity.java new file mode 100644 index 0000000..19a698b --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/entity/RackExternalDeviceEntity.java @@ -0,0 +1,33 @@ +package com.xujie.sys.modules.rack.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@TableName("rack_external_device") +public class RackExternalDeviceEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private Long deviceId; + private String deviceCode; + private String deviceName; + private String deviceType; + private String sourceType; + private String lineId; + private String stationId; + private String stepCode; + private String ip; + private Integer port; + private Integer unitId; + private String status; + private String remark; + private Date lastHeartbeatTime; + private Date createTime; + private Date updateTime; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelPrintLogEntity.java b/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelPrintLogEntity.java new file mode 100644 index 0000000..cc34d91 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelPrintLogEntity.java @@ -0,0 +1,28 @@ +package com.xujie.sys.modules.rack.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@TableName("rack_label_print_log") +public class RackLabelPrintLogEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private Long logId; + private Long packageId; + private String packageNo; + private String templateCode; + private String printNo; + private Integer printSeq; + private String printStatus; + private String operatorName; + private String payloadJson; + private Date printTime; + private Date createTime; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelTemplateEntity.java b/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelTemplateEntity.java new file mode 100644 index 0000000..3823bdf --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/entity/RackLabelTemplateEntity.java @@ -0,0 +1,25 @@ +package com.xujie.sys.modules.rack.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@TableName("rack_label_template") +public class RackLabelTemplateEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private Long templateId; + private String templateCode; + private String templateName; + private String templateContent; + private String status; + private String remark; + private Date createTime; + private Date updateTime; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/entity/RackPackageOrderEntity.java b/src/main/java/com/xujie/sys/modules/rack/entity/RackPackageOrderEntity.java new file mode 100644 index 0000000..651a6db --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/entity/RackPackageOrderEntity.java @@ -0,0 +1,34 @@ +package com.xujie.sys.modules.rack.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +@Data +@TableName("rack_package_order") +public class RackPackageOrderEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.INPUT) + private Long packageId; + private String packageNo; + private Long jobId; + private String jobCode; + private String batchCode; + private String rackCode; + private Integer qty; + private String labelTemplateCode; + private String labelContent; + private Integer printCount; + private Date lastPrintTime; + private String status; + private String operatorName; + private Date packedTime; + private String remark; + private Date createTime; + private Date updateTime; +} diff --git a/src/main/java/com/xujie/sys/modules/rack/mapper/RackExternalDeviceMapper.java b/src/main/java/com/xujie/sys/modules/rack/mapper/RackExternalDeviceMapper.java new file mode 100644 index 0000000..504ca1a --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/mapper/RackExternalDeviceMapper.java @@ -0,0 +1,9 @@ +package com.xujie.sys.modules.rack.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xujie.sys.modules.rack.entity.RackExternalDeviceEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RackExternalDeviceMapper extends BaseMapper { +} diff --git a/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelPrintLogMapper.java b/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelPrintLogMapper.java new file mode 100644 index 0000000..8c4f810 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelPrintLogMapper.java @@ -0,0 +1,9 @@ +package com.xujie.sys.modules.rack.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xujie.sys.modules.rack.entity.RackLabelPrintLogEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RackLabelPrintLogMapper extends BaseMapper { +} diff --git a/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelTemplateMapper.java b/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelTemplateMapper.java new file mode 100644 index 0000000..7300bbb --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/mapper/RackLabelTemplateMapper.java @@ -0,0 +1,9 @@ +package com.xujie.sys.modules.rack.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xujie.sys.modules.rack.entity.RackLabelTemplateEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RackLabelTemplateMapper extends BaseMapper { +} diff --git a/src/main/java/com/xujie/sys/modules/rack/mapper/RackPackageOrderMapper.java b/src/main/java/com/xujie/sys/modules/rack/mapper/RackPackageOrderMapper.java new file mode 100644 index 0000000..7266589 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/mapper/RackPackageOrderMapper.java @@ -0,0 +1,9 @@ +package com.xujie.sys.modules.rack.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xujie.sys.modules.rack.entity.RackPackageOrderEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RackPackageOrderMapper extends BaseMapper { +} diff --git a/src/main/java/com/xujie/sys/modules/rack/service/impl/RackClosedLoopAddonService.java b/src/main/java/com/xujie/sys/modules/rack/service/impl/RackClosedLoopAddonService.java new file mode 100644 index 0000000..8dbe889 --- /dev/null +++ b/src/main/java/com/xujie/sys/modules/rack/service/impl/RackClosedLoopAddonService.java @@ -0,0 +1,642 @@ +package com.xujie.sys.modules.rack.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.xujie.sys.common.exception.XJException; +import com.xujie.sys.modules.rack.constant.RackClosedLoopStatus; +import com.xujie.sys.modules.rack.dto.RackPackagePrintRequest; +import com.xujie.sys.modules.rack.dto.RackPlcIngestRequest; +import com.xujie.sys.modules.rack.dto.RackProductionActionRequest; +import com.xujie.sys.modules.rack.entity.*; +import com.xujie.sys.modules.rack.mapper.*; +import com.xujie.sys.modules.rack.service.RackClosedLoopService; +import com.xujie.sys.modules.rack.support.RackIdGenerator; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.text.SimpleDateFormat; +import java.util.*; + +@Service +public class RackClosedLoopAddonService { + + private static final int PLC_OFFLINE_MINUTES = 5; + + private final RackExternalDeviceMapper rackExternalDeviceMapper; + private final RackStationEventMapper rackStationEventMapper; + private final RackJobOrderMapper rackJobOrderMapper; + private final RackJobInboundRelMapper rackJobInboundRelMapper; + private final RackInboundOrderMapper rackInboundOrderMapper; + private final RackBindingMapper rackBindingMapper; + private final RackCarrierMapper rackCarrierMapper; + private final RackPackageOrderMapper rackPackageOrderMapper; + private final RackLabelTemplateMapper rackLabelTemplateMapper; + private final RackLabelPrintLogMapper rackLabelPrintLogMapper; + private final RackClosedLoopService rackClosedLoopService; + private final RackIdGenerator rackIdGenerator; + + public RackClosedLoopAddonService( + RackExternalDeviceMapper rackExternalDeviceMapper, + RackStationEventMapper rackStationEventMapper, + RackJobOrderMapper rackJobOrderMapper, + RackJobInboundRelMapper rackJobInboundRelMapper, + RackInboundOrderMapper rackInboundOrderMapper, + RackBindingMapper rackBindingMapper, + RackCarrierMapper rackCarrierMapper, + RackPackageOrderMapper rackPackageOrderMapper, + RackLabelTemplateMapper rackLabelTemplateMapper, + RackLabelPrintLogMapper rackLabelPrintLogMapper, + RackClosedLoopService rackClosedLoopService, + RackIdGenerator rackIdGenerator + ) { + this.rackExternalDeviceMapper = rackExternalDeviceMapper; + this.rackStationEventMapper = rackStationEventMapper; + this.rackJobOrderMapper = rackJobOrderMapper; + this.rackJobInboundRelMapper = rackJobInboundRelMapper; + this.rackInboundOrderMapper = rackInboundOrderMapper; + this.rackBindingMapper = rackBindingMapper; + this.rackCarrierMapper = rackCarrierMapper; + this.rackPackageOrderMapper = rackPackageOrderMapper; + this.rackLabelTemplateMapper = rackLabelTemplateMapper; + this.rackLabelPrintLogMapper = rackLabelPrintLogMapper; + this.rackClosedLoopService = rackClosedLoopService; + this.rackIdGenerator = rackIdGenerator; + } + + public List listExternalDevice(RackExternalDeviceEntity query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query != null) { + wrapper.like(StringUtils.isNotBlank(query.getDeviceCode()), RackExternalDeviceEntity::getDeviceCode, query.getDeviceCode()); + wrapper.like(StringUtils.isNotBlank(query.getDeviceName()), RackExternalDeviceEntity::getDeviceName, query.getDeviceName()); + wrapper.eq(StringUtils.isNotBlank(query.getDeviceType()), RackExternalDeviceEntity::getDeviceType, query.getDeviceType()); + wrapper.eq(StringUtils.isNotBlank(query.getLineId()), RackExternalDeviceEntity::getLineId, query.getLineId()); + wrapper.eq(StringUtils.isNotBlank(query.getStatus()), RackExternalDeviceEntity::getStatus, query.getStatus()); + } + wrapper.orderByDesc(RackExternalDeviceEntity::getCreateTime); + return rackExternalDeviceMapper.selectList(wrapper); + } + + @Transactional + public void saveExternalDevice(RackExternalDeviceEntity entity) { + if (entity == null || StringUtils.isBlank(entity.getDeviceCode())) { + throw new XJException("设备编码不能为空"); + } + if (StringUtils.isBlank(entity.getStepCode())) { + throw new XJException("工序编码不能为空"); + } + Date now = new Date(); + entity.setDeviceId(rackIdGenerator.nextId()); + entity.setDeviceCode(entity.getDeviceCode().trim()); + entity.setDeviceType(StringUtils.defaultIfBlank(entity.getDeviceType(), "PLC")); + entity.setSourceType(StringUtils.defaultIfBlank(entity.getSourceType(), "PLC")); + entity.setStatus(StringUtils.defaultIfBlank(entity.getStatus(), "启用")); + entity.setCreateTime(now); + entity.setUpdateTime(now); + rackExternalDeviceMapper.insert(entity); + } + + @Transactional + public void updateExternalDevice(RackExternalDeviceEntity entity) { + if (entity == null || entity.getDeviceId() == null) { + throw new XJException("device_id 不能为空"); + } + RackExternalDeviceEntity before = rackExternalDeviceMapper.selectById(entity.getDeviceId()); + if (before == null) { + throw new XJException("设备不存在"); + } + if (StringUtils.isNotBlank(entity.getDeviceCode())) { + entity.setDeviceCode(entity.getDeviceCode().trim()); + } + if (StringUtils.isBlank(entity.getSourceType())) { + entity.setSourceType(before.getSourceType()); + } + if (StringUtils.isBlank(entity.getDeviceType())) { + entity.setDeviceType(before.getDeviceType()); + } + entity.setUpdateTime(new Date()); + rackExternalDeviceMapper.updateById(entity); + } + + @Transactional + public void deleteExternalDevice(Long deviceId) { + rackExternalDeviceMapper.deleteById(deviceId); + } + + @Transactional + public void deleteExternalDeviceByCode(String deviceCode) { + if (StringUtils.isBlank(deviceCode)) { + throw new XJException("设备编码不能为空"); + } + rackExternalDeviceMapper.delete( + new LambdaQueryWrapper() + .eq(RackExternalDeviceEntity::getDeviceCode, deviceCode.trim()) + ); + } + + @Transactional + public Map ingestPlcEvent(RackPlcIngestRequest request) { + if (request == null || StringUtils.isBlank(request.getJobCode())) { + throw new XJException("任务单号不能为空"); + } + RackExternalDeviceEntity device = null; + if (StringUtils.isNotBlank(request.getDeviceCode())) { + device = rackExternalDeviceMapper.selectOne( + new LambdaQueryWrapper() + .eq(RackExternalDeviceEntity::getDeviceCode, request.getDeviceCode().trim()) + ); + if (device == null) { + throw new XJException("未找到外采设备: " + request.getDeviceCode()); + } + if (!StringUtils.equals(device.getStatus(), "启用")) { + throw new XJException("设备未启用: " + request.getDeviceCode()); + } + } + + RackProductionActionRequest action = new RackProductionActionRequest(); + action.setJobCode(request.getJobCode()); + action.setRackCode(request.getRackCode()); + action.setLineId(StringUtils.defaultIfBlank(request.getLineId(), device == null ? null : device.getLineId())); + action.setStationId(StringUtils.defaultIfBlank(request.getStationId(), device == null ? null : device.getStationId())); + action.setStepCode(StringUtils.defaultIfBlank(request.getStepCode(), device == null ? null : device.getStepCode())); + action.setSourceType(StringUtils.defaultIfBlank(request.getSourceType(), device == null ? "PLC" : device.getSourceType())); + action.setOperatorName(StringUtils.defaultIfBlank(request.getOperatorName(), "PLC采集")); + action.setPayloadJson(StringUtils.defaultIfBlank(request.getPayloadJson(), "{}")); + + Date now = request.getEventTime() == null ? new Date() : request.getEventTime(); + if (device != null) { + touchExternalDeviceHeartbeat(device.getDeviceId(), now); + } + + try { + Map result = rackClosedLoopService.stationPassByCode(action); + Map response = new LinkedHashMap<>(); + response.put("ingestStatus", "PASS"); + response.put("msg", "PLC采集过站成功"); + response.put("result", result); + return response; + } catch (Exception e) { + if (Boolean.TRUE.equals(request.getFallbackManual())) { + Map response = fallbackToManualRecord(request, action, now, e); + response.put("ingestStatus", "FALLBACK_MANUAL"); + return response; + } + throw e; + } + } + + public List> listPlcStatus() { + List deviceList = rackExternalDeviceMapper.selectList( + new LambdaQueryWrapper() + .eq(RackExternalDeviceEntity::getDeviceType, "PLC") + .orderByAsc(RackExternalDeviceEntity::getDeviceCode) + ); + Date now = new Date(); + List> rows = new ArrayList<>(); + for (RackExternalDeviceEntity device : deviceList) { + RackStationEventEntity latestEvent = queryLatestEventByDevice(device); + Date heartbeat = device.getLastHeartbeatTime(); + if (heartbeat == null && latestEvent != null) { + heartbeat = latestEvent.getEventTime(); + } + boolean online = false; + String offlineReason = ""; + if (!StringUtils.equals(device.getStatus(), "启用")) { + offlineReason = "设备未启用"; + } else if (heartbeat == null) { + offlineReason = "无心跳"; + } else { + long deltaMs = now.getTime() - heartbeat.getTime(); + online = deltaMs <= PLC_OFFLINE_MINUTES * 60L * 1000L; + if (!online) { + offlineReason = "心跳超时"; + } + } + Map row = new LinkedHashMap<>(); + row.put("deviceId", device.getDeviceId()); + row.put("deviceCode", device.getDeviceCode()); + row.put("deviceName", device.getDeviceName()); + row.put("lineId", device.getLineId()); + row.put("stationId", device.getStationId()); + row.put("stepCode", device.getStepCode()); + row.put("status", device.getStatus()); + row.put("heartbeatTime", heartbeat); + row.put("latestEventTime", latestEvent == null ? null : latestEvent.getEventTime()); + row.put("latestEventStatus", latestEvent == null ? null : latestEvent.getEventStatus()); + row.put("online", online); + row.put("offlineReason", offlineReason); + rows.add(row); + } + return rows; + } + + public String resolveDownHangRackCodeByBatch(String jobCode, String batchCode) { + RackJobOrderEntity job = requireJobByCode(jobCode); + if (StringUtils.isBlank(batchCode)) { + throw new XJException("批次编码不能为空"); + } + RackInboundOrderEntity inbound = rackInboundOrderMapper.selectOne( + new LambdaQueryWrapper() + .eq(RackInboundOrderEntity::getBatchCode, batchCode.trim()) + .orderByDesc(RackInboundOrderEntity::getCreateTime) + .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY") + ); + if (inbound == null) { + throw new XJException("未找到批次编码对应入库单: " + batchCode); + } + long relCount = rackJobInboundRelMapper.selectCount( + new LambdaQueryWrapper() + .eq(RackJobInboundRelEntity::getJobId, job.getJobId()) + .eq(RackJobInboundRelEntity::getInboundId, inbound.getInboundId()) + ); + if (relCount <= 0) { + throw new XJException("批次不属于当前任务: " + batchCode); + } + List activeList = rackBindingMapper.selectList( + new LambdaQueryWrapper() + .eq(RackBindingEntity::getJobId, job.getJobId()) + .eq(RackBindingEntity::getBindStatus, RackClosedLoopStatus.Binding.SHENG_XIAO_ZHONG) + .orderByDesc(RackBindingEntity::getBindTime) + ); + if (activeList.isEmpty()) { + throw new XJException("当前任务不存在生效中的上挂关系"); + } + if (activeList.size() > 1) { + throw new XJException("当前任务有多个挂具在制,请扫码挂具码执行下挂"); + } + RackCarrierEntity rack = rackCarrierMapper.selectById(activeList.get(0).getRackId()); + if (rack == null || StringUtils.isBlank(rack.getRackCode())) { + throw new XJException("生效挂具不存在,请检查绑定数据"); + } + return rack.getRackCode(); + } + + public List listPackageOrder(RackPackageOrderEntity query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query != null) { + wrapper.like(StringUtils.isNotBlank(query.getPackageNo()), RackPackageOrderEntity::getPackageNo, query.getPackageNo()); + wrapper.like(StringUtils.isNotBlank(query.getJobCode()), RackPackageOrderEntity::getJobCode, query.getJobCode()); + wrapper.like(StringUtils.isNotBlank(query.getBatchCode()), RackPackageOrderEntity::getBatchCode, query.getBatchCode()); + wrapper.like(StringUtils.isNotBlank(query.getRackCode()), RackPackageOrderEntity::getRackCode, query.getRackCode()); + wrapper.eq(StringUtils.isNotBlank(query.getStatus()), RackPackageOrderEntity::getStatus, query.getStatus()); + } + wrapper.orderByDesc(RackPackageOrderEntity::getCreateTime); + return rackPackageOrderMapper.selectList(wrapper); + } + + @Transactional + public void savePackageOrder(RackPackageOrderEntity entity) { + if (entity == null) { + throw new XJException("请求参数不能为空"); + } + if (StringUtils.isBlank(entity.getJobCode())) { + throw new XJException("任务单号不能为空"); + } + RackJobOrderEntity job = requireJobByCode(entity.getJobCode()); + Date now = new Date(); + entity.setPackageId(rackIdGenerator.nextId()); + entity.setPackageNo(StringUtils.isNotBlank(entity.getPackageNo()) ? entity.getPackageNo().trim() : generatePackageNo()); + entity.setJobId(job.getJobId()); + if (StringUtils.isBlank(entity.getBatchCode())) { + entity.setBatchCode(resolveBatchCodeByJob(job.getJobId())); + } + entity.setQty(entity.getQty() == null ? 0 : entity.getQty()); + entity.setPrintCount(entity.getPrintCount() == null ? 0 : entity.getPrintCount()); + entity.setStatus(StringUtils.defaultIfBlank(entity.getStatus(), "待打印")); + entity.setPackedTime(entity.getPackedTime() == null ? now : entity.getPackedTime()); + entity.setCreateTime(now); + entity.setUpdateTime(now); + rackPackageOrderMapper.insert(entity); + } + + @Transactional + public void updatePackageOrder(RackPackageOrderEntity entity) { + if (entity == null || entity.getPackageId() == null) { + throw new XJException("package_id 不能为空"); + } + RackPackageOrderEntity before = rackPackageOrderMapper.selectById(entity.getPackageId()); + if (before == null) { + throw new XJException("包装单不存在"); + } + if (StringUtils.isNotBlank(entity.getJobCode())) { + RackJobOrderEntity job = requireJobByCode(entity.getJobCode()); + entity.setJobId(job.getJobId()); + } + entity.setUpdateTime(new Date()); + rackPackageOrderMapper.updateById(entity); + } + + @Transactional + public void deletePackageOrderByNo(String packageNo) { + if (StringUtils.isBlank(packageNo)) { + throw new XJException("包装单号不能为空"); + } + RackPackageOrderEntity order = rackPackageOrderMapper.selectOne( + new LambdaQueryWrapper().eq(RackPackageOrderEntity::getPackageNo, packageNo.trim()) + ); + if (order == null) { + return; + } + rackLabelPrintLogMapper.delete( + new LambdaQueryWrapper().eq(RackLabelPrintLogEntity::getPackageId, order.getPackageId()) + ); + rackPackageOrderMapper.deleteById(order.getPackageId()); + } + + public List listLabelTemplate(RackLabelTemplateEntity query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query != null) { + wrapper.like(StringUtils.isNotBlank(query.getTemplateCode()), RackLabelTemplateEntity::getTemplateCode, query.getTemplateCode()); + wrapper.like(StringUtils.isNotBlank(query.getTemplateName()), RackLabelTemplateEntity::getTemplateName, query.getTemplateName()); + wrapper.eq(StringUtils.isNotBlank(query.getStatus()), RackLabelTemplateEntity::getStatus, query.getStatus()); + } + wrapper.orderByDesc(RackLabelTemplateEntity::getCreateTime); + return rackLabelTemplateMapper.selectList(wrapper); + } + + @Transactional + public void saveLabelTemplate(RackLabelTemplateEntity entity) { + if (entity == null || StringUtils.isBlank(entity.getTemplateCode())) { + throw new XJException("模板编码不能为空"); + } + Date now = new Date(); + entity.setTemplateId(rackIdGenerator.nextId()); + entity.setTemplateCode(entity.getTemplateCode().trim()); + entity.setStatus(StringUtils.defaultIfBlank(entity.getStatus(), "启用")); + entity.setCreateTime(now); + entity.setUpdateTime(now); + rackLabelTemplateMapper.insert(entity); + } + + @Transactional + public void updateLabelTemplate(RackLabelTemplateEntity entity) { + if (entity == null || entity.getTemplateId() == null) { + throw new XJException("template_id 不能为空"); + } + entity.setUpdateTime(new Date()); + rackLabelTemplateMapper.updateById(entity); + } + + @Transactional + public void deleteLabelTemplateByCode(String templateCode) { + if (StringUtils.isBlank(templateCode)) { + throw new XJException("模板编码不能为空"); + } + rackLabelTemplateMapper.delete( + new LambdaQueryWrapper().eq(RackLabelTemplateEntity::getTemplateCode, templateCode.trim()) + ); + } + + @Transactional + public Map printPackageLabel(RackPackagePrintRequest request) { + if (request == null || StringUtils.isBlank(request.getPackageNo())) { + throw new XJException("包装单号不能为空"); + } + if (StringUtils.isBlank(request.getTemplateCode())) { + throw new XJException("标签模板不能为空"); + } + RackPackageOrderEntity order = requirePackageByNo(request.getPackageNo()); + RackLabelTemplateEntity template = requireTemplateByCode(request.getTemplateCode()); + int copies = request.getCopies() == null || request.getCopies() <= 0 ? 1 : request.getCopies(); + Date now = new Date(); + String operatorName = StringUtils.defaultIfBlank(request.getOperatorName(), "PC操作员"); + String renderedContent = renderTemplate(template.getTemplateContent(), order); + int oldPrintCount = order.getPrintCount() == null ? 0 : order.getPrintCount(); + List printNos = new ArrayList<>(); + + for (int i = 1; i <= copies; i++) { + RackLabelPrintLogEntity log = new RackLabelPrintLogEntity(); + log.setLogId(rackIdGenerator.nextId()); + log.setPackageId(order.getPackageId()); + log.setPackageNo(order.getPackageNo()); + log.setTemplateCode(template.getTemplateCode()); + log.setPrintNo(generatePrintNo()); + log.setPrintSeq(oldPrintCount + i); + log.setPrintStatus("成功"); + log.setOperatorName(operatorName); + log.setPayloadJson(JSON.toJSONString(buildPrintPayload(order, template, renderedContent, i, copies))); + log.setPrintTime(now); + log.setCreateTime(now); + rackLabelPrintLogMapper.insert(log); + printNos.add(log.getPrintNo()); + } + + rackPackageOrderMapper.update( + null, + new LambdaUpdateWrapper() + .eq(RackPackageOrderEntity::getPackageId, order.getPackageId()) + .set(RackPackageOrderEntity::getLabelTemplateCode, template.getTemplateCode()) + .set(RackPackageOrderEntity::getLabelContent, renderedContent) + .set(RackPackageOrderEntity::getPrintCount, oldPrintCount + copies) + .set(RackPackageOrderEntity::getLastPrintTime, now) + .set(RackPackageOrderEntity::getStatus, "已打印") + .set(RackPackageOrderEntity::getOperatorName, operatorName) + .set(RackPackageOrderEntity::getUpdateTime, now) + ); + + Map result = new LinkedHashMap<>(); + result.put("packageNo", order.getPackageNo()); + result.put("templateCode", template.getTemplateCode()); + result.put("copies", copies); + result.put("printNos", printNos); + result.put("labelPreview", renderedContent); + return result; + } + + public List listLabelPrintLog(RackLabelPrintLogEntity query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (query != null) { + wrapper.eq(query.getPackageId() != null, RackLabelPrintLogEntity::getPackageId, query.getPackageId()); + wrapper.like(StringUtils.isNotBlank(query.getPackageNo()), RackLabelPrintLogEntity::getPackageNo, query.getPackageNo()); + wrapper.eq(StringUtils.isNotBlank(query.getTemplateCode()), RackLabelPrintLogEntity::getTemplateCode, query.getTemplateCode()); + } + wrapper.orderByDesc(RackLabelPrintLogEntity::getPrintTime); + return rackLabelPrintLogMapper.selectList(wrapper); + } + + private Map fallbackToManualRecord( + RackPlcIngestRequest request, + RackProductionActionRequest action, + Date recordTime, + Exception exception + ) { + RackJobOrderEntity job = requireJobByCode(action.getJobCode()); + RackManualRecordEntity manual = new RackManualRecordEntity(); + manual.setRecordType("过站补录"); + manual.setDocNo(buildManualDocNo(request, action)); + manual.setBatchId(resolveInboundIdByJob(job.getJobId())); + manual.setJobId(job.getJobId()); + manual.setQty(1); + manual.setOperatorName(StringUtils.defaultIfBlank(action.getOperatorName(), "PLC采集")); + manual.setReviewerName("系统自动补录"); + manual.setPayloadJson(JSON.toJSONString(buildFallbackPayload(request, action, exception))); + manual.setRecordTime(recordTime); + rackClosedLoopService.saveManualRecord(manual); + + Map result = new LinkedHashMap<>(); + result.put("msg", "PLC过站失败,已自动写入手工补录台账"); + result.put("error", exception == null ? "未知异常" : exception.getMessage()); + result.put("manualDocNo", manual.getDocNo()); + return result; + } + + private RackStationEventEntity queryLatestEventByDevice(RackExternalDeviceEntity device) { + if (device == null) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(StringUtils.isNotBlank(device.getStationId()), RackStationEventEntity::getStationId, device.getStationId()); + wrapper.eq(StringUtils.isNotBlank(device.getStepCode()), RackStationEventEntity::getStepCode, device.getStepCode()); + wrapper.orderByDesc(RackStationEventEntity::getEventTime); + wrapper.last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"); + List list = rackStationEventMapper.selectList(wrapper); + return list.isEmpty() ? null : list.get(0); + } + + private void touchExternalDeviceHeartbeat(Long deviceId, Date heartbeatTime) { + if (deviceId == null) { + return; + } + rackExternalDeviceMapper.update( + null, + new LambdaUpdateWrapper() + .eq(RackExternalDeviceEntity::getDeviceId, deviceId) + .set(RackExternalDeviceEntity::getLastHeartbeatTime, heartbeatTime == null ? new Date() : heartbeatTime) + .set(RackExternalDeviceEntity::getUpdateTime, new Date()) + ); + } + + private RackPackageOrderEntity requirePackageByNo(String packageNo) { + RackPackageOrderEntity order = rackPackageOrderMapper.selectOne( + new LambdaQueryWrapper() + .eq(RackPackageOrderEntity::getPackageNo, packageNo.trim()) + ); + if (order == null) { + throw new XJException("包装单不存在: " + packageNo); + } + return order; + } + + private RackLabelTemplateEntity requireTemplateByCode(String templateCode) { + RackLabelTemplateEntity template = rackLabelTemplateMapper.selectOne( + new LambdaQueryWrapper() + .eq(RackLabelTemplateEntity::getTemplateCode, templateCode.trim()) + ); + if (template == null) { + throw new XJException("标签模板不存在: " + templateCode); + } + if (!StringUtils.equals(template.getStatus(), "启用")) { + throw new XJException("标签模板未启用: " + templateCode); + } + return template; + } + + private RackJobOrderEntity requireJobByCode(String jobCode) { + RackJobOrderEntity job = rackJobOrderMapper.selectOne( + new LambdaQueryWrapper().eq(RackJobOrderEntity::getJobCode, StringUtils.trimToEmpty(jobCode)) + ); + if (job == null) { + throw new XJException("任务单不存在: " + jobCode); + } + return job; + } + + private Long resolveInboundIdByJob(Long jobId) { + if (jobId == null) { + return null; + } + List relList = rackJobInboundRelMapper.selectList( + new LambdaQueryWrapper() + .eq(RackJobInboundRelEntity::getJobId, jobId) + .orderByAsc(RackJobInboundRelEntity::getCreateTime) + .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY") + ); + if (relList.isEmpty()) { + return null; + } + return relList.get(0).getInboundId(); + } + + private String resolveBatchCodeByJob(Long jobId) { + Long inboundId = resolveInboundIdByJob(jobId); + if (inboundId == null) { + return ""; + } + RackInboundOrderEntity inbound = rackInboundOrderMapper.selectById(inboundId); + return inbound == null ? "" : StringUtils.defaultString(inbound.getBatchCode()); + } + + private String buildManualDocNo(RackPlcIngestRequest request, RackProductionActionRequest action) { + String jobCode = StringUtils.defaultIfBlank(action.getJobCode(), "UNKNOWN"); + String stepCode = StringUtils.defaultIfBlank(action.getStepCode(), "STEP"); + String suffix = String.valueOf(Math.abs(rackIdGenerator.nextId()) % 10000); + return "PLC-MANUAL-" + jobCode + "-" + stepCode + "-" + String.format("%04d", Integer.parseInt(suffix)); + } + + private Map buildFallbackPayload( + RackPlcIngestRequest request, + RackProductionActionRequest action, + Exception exception + ) { + Map payload = new LinkedHashMap<>(); + payload.put("source", "PLC"); + payload.put("deviceCode", request.getDeviceCode()); + payload.put("missingReason", StringUtils.defaultIfBlank(request.getMissingReason(), "PLC采集失败自动补录")); + payload.put("jobCode", action.getJobCode()); + payload.put("rackCode", action.getRackCode()); + payload.put("stepCode", action.getStepCode()); + payload.put("stationId", action.getStationId()); + payload.put("lineId", action.getLineId()); + payload.put("payloadJson", action.getPayloadJson()); + payload.put("errorMsg", exception == null ? "" : exception.getMessage()); + return payload; + } + + private Map buildPrintPayload( + RackPackageOrderEntity order, + RackLabelTemplateEntity template, + String renderedContent, + int index, + int total + ) { + Map payload = new LinkedHashMap<>(); + payload.put("packageNo", order.getPackageNo()); + payload.put("templateCode", template.getTemplateCode()); + payload.put("copyIndex", index); + payload.put("copyTotal", total); + payload.put("labelContent", renderedContent); + return payload; + } + + private String renderTemplate(String templateContent, RackPackageOrderEntity order) { + String content = StringUtils.defaultString(templateContent); + content = content.replace("{{packageNo}}", StringUtils.defaultString(order.getPackageNo())); + content = content.replace("{{jobCode}}", StringUtils.defaultString(order.getJobCode())); + content = content.replace("{{batchCode}}", StringUtils.defaultString(order.getBatchCode())); + content = content.replace("{{rackCode}}", StringUtils.defaultString(order.getRackCode())); + content = content.replace("{{qty}}", String.valueOf(order.getQty() == null ? 0 : order.getQty())); + return content; + } + + private synchronized String generatePackageNo() { + String datePart = new SimpleDateFormat("yyyyMMdd").format(new Date()); + for (int i = 0; i < 8; i++) { + long suffix = Math.abs(rackIdGenerator.nextId()) % 100000; + String candidate = "PKG" + datePart + String.format("%05d", suffix); + long count = rackPackageOrderMapper.selectCount( + new LambdaQueryWrapper().eq(RackPackageOrderEntity::getPackageNo, candidate) + ); + if (count == 0) { + return candidate; + } + } + throw new XJException("包装单号生成失败,请重试"); + } + + private synchronized String generatePrintNo() { + String datePart = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + long suffix = Math.abs(rackIdGenerator.nextId()) % 100000; + return "PRN" + datePart + String.format("%05d", suffix); + } +}