feat(business): 同步G30添加发送任务模板功能并扩展计划实体

- 新增 BizSendJob、BizSendJobGroup、BizSendJobItem 实体类用于发送任务管理
- 新增 BizSendTemplate、BizSendTemplateItem 实体类用于发送模板配置
- 实现发送模板的增删改查和批量保存功能
- 添加 DashboardController 提供首页仪表板统计接口
- 实现发送任务查询和执行服务
- 扩展 PdiPlan 相关实体类增加锌层厚度字段
- 优化 OPC 消息发送功能,支持多种数据类型转换
- 更新日志配置,调整错误日志处理策略
This commit is contained in:
2026-01-04 10:30:38 +08:00
parent 8717c55aca
commit 42c9d12504
58 changed files with 1981 additions and 45 deletions

View File

@@ -61,6 +61,59 @@ public class OpcMessageSend {
}
}
/**
* 通用写入方法,用于向指定 OPC 节点写入一个值
* @param address OPC 节点地址 (e.g., "ns=2;s=ProcessCGL.PLCLine.ExitCut.cutLength")
* @param value 要写入的值
*/
public void writeNode(String address, Object value) {
try {
List<ReadWriteEntity> entities = new ArrayList<>();
entities.add(ReadWriteEntity.builder()
.identifier(address)
.value(value)
.build());
miloService.writeToOpcUa(entities);
log.info("写入 OPC 成功, node={}, value={}", address, value);
} catch (Exception e) {
log.error("写入 OPC 失败, node={}, value={}, 原因: {}", address, value, e.getMessage(), e);
// 抛出运行时异常,以便上层调用者(如 SendJobServiceImpl可以捕获并处理失败状态
throw new RuntimeException("写入 OPC 失败: " + e.getMessage(), e);
}
}
/**
* 写入 OPC 节点(增强版):根据字符串值尝试转换为数值/布尔,再写入
* 规则:
* 1) 先尝试转 Integer
* 2) 再尝试转 Double
* 3) 再尝试转 Booleantrue/false/1/0
* 4) 都不行则按原字符串写入
*/
public void writeNode(String address, String valueRaw) {
Object v = valueRaw;
if (valueRaw != null) {
String s = valueRaw.trim();
// boolean
if ("1".equals(s) || "0".equals(s) || "true".equalsIgnoreCase(s) || "false".equalsIgnoreCase(s)) {
v = ("1".equals(s) || "true".equalsIgnoreCase(s));
} else {
// int
try {
v = Integer.parseInt(s);
} catch (Exception ignore) {
// double
try {
v = Double.parseDouble(s);
} catch (Exception ignore2) {
v = valueRaw;
}
}
}
}
writeNode(address, v);
}
private List<ReadWriteEntity> getWriteEntities(OpcMessage msg, Map<String,String> msgIds) {
List<ReadWriteEntity> entities = new ArrayList<>();
for (String key : msgIds.keySet()) {
@@ -76,7 +129,7 @@ public class OpcMessageSend {
} catch (NoSuchFieldException | IllegalAccessException e) {
// 处理字段不存在或访问异常,可记录日志或设置默认值
e.printStackTrace();
return new ArrayList<>();
return new ArrayList<>();
}
ReadWriteEntity entity = ReadWriteEntity.builder()

View File

@@ -0,0 +1,93 @@
package com.fizz.business.controller;
import com.fizz.business.domain.BizSendTemplate;
import com.fizz.business.domain.BizSendTemplateItem;
import com.fizz.business.domain.dto.SendTemplateItemsBatchSaveDTO;
import com.fizz.business.domain.vo.BizSendTemplateVO;
import com.fizz.business.service.IBizSendTemplateItemService;
import com.fizz.business.service.IBizSendTemplateService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
/**
* 发送模板配置 Controller
*/
@RestController
@RequestMapping("/business/sendTemplate")
public class BizSendTemplateController extends BaseController {
@Autowired
private IBizSendTemplateService templateService;
@Autowired
private IBizSendTemplateItemService templateItemService;
/**
* 按模板编码获取模板(含明细)
*/
@GetMapping("/{templateCode}")
public AjaxResult getByCode(@PathVariable String templateCode) {
BizSendTemplateVO vo = templateService.getTemplateWithItems(templateCode);
if (vo == null) {
return AjaxResult.error("Template not found");
}
return AjaxResult.success(vo);
}
/**
* 更新模板主表(如 deviceName
*/
@PutMapping
public AjaxResult updateTemplate(@RequestBody BizSendTemplate template) {
if (template == null || template.getTemplateId() == null) {
return AjaxResult.error("templateId is required");
}
template.setUpdateBy(SecurityUtils.getUsername());
template.setUpdateTime(new Date());
return toAjax(templateService.updateById(template));
}
/**
* 批量更新模板明细address/defaultValueRaw/enabled等
*/
@PutMapping("/items")
public AjaxResult updateTemplateItems(@RequestBody List<BizSendTemplateItem> items) {
if (items == null || items.isEmpty()) {
return AjaxResult.success();
}
Date now = new Date();
String username = SecurityUtils.getUsername();
for (BizSendTemplateItem it : items) {
it.setUpdateBy(username);
it.setUpdateTime(now);
}
return toAjax(templateItemService.updateItemsBatch(items));
}
/**
* 模板明细批量保存(新增/更新/删除)
*/
@PutMapping("/items/batchSave")
public AjaxResult batchSaveItems(@RequestBody SendTemplateItemsBatchSaveDTO dto) {
if (dto == null || dto.getTemplateId() == null) {
return AjaxResult.error("templateId is required");
}
try {
Boolean ok = templateItemService.batchSave(
dto.getTemplateId(),
dto.getItems(),
dto.getDeleteIds(),
SecurityUtils.getUsername()
);
return toAjax(ok);
} catch (IllegalArgumentException e) {
return AjaxResult.error(e.getMessage());
}
}
}

View File

@@ -0,0 +1,39 @@
package com.fizz.business.controller;
import com.fizz.business.service.DashboardService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 首页仪表板相关接口
*/
@RestController
public class DashboardController extends BaseController {
@Resource
private DashboardService dashboardService;
/**
* 当前生产中的计划信息crm_pdi_plan.status = 'PRODUCING'
*/
@GetMapping("/api/business/dashboard/currentPlan")
public AjaxResult getCurrentProducingPlan() {
return AjaxResult.success(dashboardService.getCurrentProducingPlan());
}
/**
* 当前生产卷的关键工艺参数
* - 从 cpl_segment_total.total_values_json 解析
* - 键名来自 DeviceEnum.paramFields
*/
@GetMapping("/api/business/dashboard/currentProcess")
public AjaxResult getCurrentProcessParams() {
return AjaxResult.success(dashboardService.getCurrentProcessParams());
}
}

View File

@@ -0,0 +1,90 @@
package com.fizz.business.controller;
import com.fizz.business.domain.BizSendJob;
import com.fizz.business.domain.dto.SendJobCreateDTO;
import com.fizz.business.domain.dto.SendJobQueryDTO;
import com.fizz.business.domain.vo.SendJobDetailVO;
import com.fizz.business.service.ISendJobService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.fizz.business.domain.vo.SendJobLastSuccessVO;
import com.fizz.business.service.ISendJobQueryService;
import com.ruoyi.common.annotation.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 发送任务 Controller
*/
@RestController
@RequestMapping("/business/sendJob")
public class SendJobController extends BaseController {
@Autowired
private ISendJobService sendJobService;
@Autowired
private ISendJobQueryService sendJobQueryService;
/**
* 创建发送任务
*/
@Log(title = "发送任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult create(@Validated @RequestBody SendJobCreateDTO dto) {
Integer jobId = sendJobService.createSendJob(dto);
return success(jobId);
}
/**
* 查询发送任务列表
*/
@GetMapping("/list")
public TableDataInfo list(SendJobQueryDTO query) {
startPage(); // 使用BaseController的分页方法
List<BizSendJob> list = sendJobService.selectSendJobList(query);
return getDataTable(list);
}
/**
* 获取发送任务详情
*/
@GetMapping("/{jobId}")
public AjaxResult getDetail(@PathVariable Integer jobId) {
SendJobDetailVO detail = sendJobService.selectSendJobDetail(jobId);
return success(detail);
}
/**
* 删除发送任务(逻辑删除)
*/
@Log(title = "发送任务", businessType = BusinessType.DELETE)
@DeleteMapping("/{jobIds}")
public AjaxResult remove(@PathVariable Integer[] jobIds) {
return toAjax(sendJobService.deleteSendJobByJobIds(jobIds));
}
/**
* 执行发送任务
*/
@Log(title = "发送任务", businessType = BusinessType.UPDATE)
@PostMapping("/{jobId}/execute")
public AjaxResult execute(@PathVariable Integer jobId) {
return toAjax(sendJobService.executeSendJob(jobId));
}
/**
* 查询最近一次成功发送(用于界面显示上次发送时间 + 推荐上次值)
* @param groupType DRIVE / FURNACE
*/
@GetMapping("/lastSuccess")
public AjaxResult lastSuccess(@RequestParam String groupType) {
SendJobLastSuccessVO vo = sendJobQueryService.getLastSuccess(groupType);
return AjaxResult.success(vo);
}
}

View File

@@ -30,57 +30,45 @@ public class SteelGradeInfoController {
@Operation(summary = "查询钢种列表")
public R<List<StdAlloyVO>> list(@RequestParam(value = "keyword", required = false) String keyword) {
// 使用 LambdaQueryWrapper 查询 StdAlloy 表中的数据,支持按名称/编号模糊查询
// 严格按 cgldb.sql查询 std_alloy(GRADEID, NAME)
LambdaQueryWrapper<StdAlloy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.select(StdAlloy::getGradeid, StdAlloy::getName); // 只查询 gradeId 和 name 字段
queryWrapper.select(StdAlloy::getGradeid, StdAlloy::getName);
if (StringUtils.isNotBlank(keyword)) {
queryWrapper.like(StdAlloy::getName, keyword)
.or()
.like(StdAlloy::getGradeid, keyword);
}
queryWrapper.orderByAsc(StdAlloy::getName); // 按 name 排序
queryWrapper.orderByAsc(StdAlloy::getName);
// 查询 StdAlloy 数据
List<StdAlloy> stdAlloyList = steelGradeInfoService.list(queryWrapper);
// 使用 BeanUtils 将 StdAlloy 对象的字段映射到 StdAlloyVO
List<StdAlloyVO> stdAlloyVOList = new ArrayList<>();
for (StdAlloy stdAlloy : stdAlloyList) {
StdAlloyVO stdAlloyVO = new StdAlloyVO();
BeanUtils.copyProperties(stdAlloy, stdAlloyVO); // 将 StdAlloy 属性复制到 StdAlloyVO
BeanUtils.copyProperties(stdAlloy, stdAlloyVO);
stdAlloyVOList.add(stdAlloyVO);
}
// 返回结果
return R.ok(stdAlloyVOList);
}
@GetMapping("/info")
@Operation(summary ="询单个钢种详情")
public R<StdAlloy> getSteelGradeInfo(@RequestParam Integer gradeid) {
// 使用 LambdaQueryWrapper 查询 StdAlloy 表中的数据
LambdaQueryWrapper<StdAlloy> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StdAlloy::getGradeid, gradeid); // 只查询 gradeId 和 name 字段
// 查询 StdAlloy 数据
StdAlloy stdAlloyList = steelGradeInfoService.getById(gradeid);
// 返回结果
return R.ok(stdAlloyList);
return R.ok(steelGradeInfoService.getById(gradeid));
}
@PostMapping("/add")
@Operation(summary ="新增")
public R<Boolean> add(@RequestBody StdAlloy steelGradeInfo) {
return R.ok(steelGradeInfoService.save(steelGradeInfo));
public R<Boolean> add(@RequestBody StdAlloy stdAlloy) {
return R.ok(steelGradeInfoService.save(stdAlloy));
}
@PutMapping("/update")
@Operation(summary ="更新")
public R<Boolean> update(@RequestBody StdAlloy steelGradeInfo) {
return R.ok(steelGradeInfoService.updateById(steelGradeInfo));
public R<Boolean> update(@RequestBody StdAlloy stdAlloy) {
return R.ok(steelGradeInfoService.updateById(stdAlloy));
}
@Operation(summary ="删除")

View File

@@ -0,0 +1,60 @@
package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 业务发送批次表 biz_send_job
*
* @author Cascade
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_send_job")
public class BizSendJob extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 批次ID */
@TableId(type = IdType.AUTO)
private Integer jobId;
/** 业务唯一键, 用于幂等控制 */
private String bizKey;
/** 目标设备/产线名称 (如 CGL_LINE_1, FURNACE_A) */
private String deviceName;
/** 计划发送时间 */
private Date planSendTime;
/** 实际开始发送时间 */
private Date actualSendTime;
/** 发送完成时间 */
private Date finishTime;
/** 批次状态: PENDING, IN_PROGRESS, COMPLETED, PARTIAL_SUCCESS, FAILED */
private String status;
/** 操作员ID */
private Long operatorId;
/** 操作员姓名 */
private String operatorName;
// 解决 BaseEntity 字段导致的未知列问题
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String searchValue;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.Map<String,Object> params;
// GroupType
private String groupType;
}

View File

@@ -0,0 +1,46 @@
package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 业务发送分组表 biz_send_job_group
*
* @author Cascade
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_send_job_group")
public class BizSendJobGroup extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 分组ID */
@TableId(type = IdType.AUTO)
private Integer groupId;
/** 所属批次ID */
private Integer jobId;
/** 在本批次内的组序号 */
private Integer groupNo;
/** 组类型: DRIVE(传动), FURNACE(炉火) */
private String groupType;
/** 组名称 (可选) */
private String groupName;
/** 分组状态: PENDING, IN_PROGRESS, COMPLETED, FAILED */
private String status;
// 解决 BaseEntity 字段导致的未知列问题
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String searchValue;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.Map<String,Object> params;
}

View File

@@ -0,0 +1,64 @@
package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
/**
* 业务发送项历史表 biz_send_job_item
*
* @author Cascade
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_send_job_item")
public class BizSendJobItem extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 发送项ID */
@TableId(type = IdType.AUTO)
private Integer itemId;
/** 所属批次ID (冗余) */
private Integer jobId;
/** 所属分组ID */
private Integer groupId;
/** 参数业务编码 */
private String paramCode;
/** 设定地址 (OPC地址) */
private String address;
/** 设定的原始值 */
private String valueRaw;
/** 设定值的数值形式 */
private BigDecimal valueNum;
/** 参数的设定时间 */
private Date setTime;
/** 发送结果: PENDING, SUCCESS, FAILED */
private String resultStatus;
/** 结果消息 */
private String resultMsg;
/** 重试次数 */
private Integer retryCount;
// 解决 BaseEntity 字段导致的未知列问题
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String searchValue;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.Map<String,Object> params;
}

View File

@@ -0,0 +1,41 @@
package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 发送默认模板主表
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_send_template")
public class BizSendTemplate extends BaseEntity {
@TableId(type = IdType.AUTO)
private Integer templateId;
// 解决 MyBatis-Plus 父类字段映射导致的 search_value 报错
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String searchValue;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.Map<String,Object> params;
/** 模板编码 */
private String templateCode;
/** 模板名称 */
private String templateName;
/** 默认设备名称 */
private String deviceName;
/** 组类型 DRIVE / FURNACE */
private String groupType;
/** 是否启用 */
private Integer enabled;
}

View File

@@ -0,0 +1,49 @@
package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 发送默认模板明细表
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("biz_send_template_item")
public class BizSendTemplateItem extends BaseEntity {
@TableId(type = IdType.AUTO)
private Integer templateItemId;
private Integer templateId;
/** 明细序号 */
private Integer itemNo;
/** 参数编码 */
private String paramCode;
/** 英文显示名 */
private String labelEn;
/** 英文分组名 */
private String groupNameEn;
/** OPC地址 */
private String address;
/** 默认值(字符串) */
private String defaultValueRaw;
/** 是否启用 */
private Integer enabled;
// 解决 BaseEntity 字段导致的未知列问题
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String searchValue;
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.Map<String,Object> params;
}

View File

@@ -313,4 +313,8 @@ public class CrmPdiPlan implements Serializable {
@Schema(description = "原卷号")
private String originCoilid;
//锌层厚度 zinc_coating_thickness
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDateTime;
@@ -130,4 +131,7 @@ public class CrmPdoExcoil implements Serializable {
@Schema(description = "计划来源L3-L3计划MANUAL-人工")
private String planOrigin;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -2,10 +2,12 @@ package com.fizz.business.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("std_alloy")
public class StdAlloy {
@TableId("GRADEID")

View File

@@ -0,0 +1,75 @@
package com.fizz.business.domain.dto;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Date;
import java.util.List;
/**
* 创建发送任务 DTO
*/
@Data
public class SendJobCreateDTO {
/** 目标设备/产线名称 (如 CGL_LINE_1, FURNACE_A) */
@NotBlank(message = "deviceName不能为空")
private String deviceName;
/** 计划发送时间 */
private Date planSendTime;
/** 操作员ID */
private Long operatorId;
/** 操作员姓名 */
private String operatorName;
/** 分组列表 */
@Valid
@NotNull(message = "groups不能为空")
@Size(min = 1, message = "至少需要一个分组")
private List<GroupDTO> groups;
@Data
public static class GroupDTO {
/** 在本批次内的组序号 */
@NotNull(message = "groupNo不能为空")
private Integer groupNo;
/** 组类型: DRIVE(传动), FURNACE(炉火) */
@NotBlank(message = "groupType不能为空")
private String groupType;
/** 组名称 (可选) */
private String groupName;
/** 参数项列表 */
@Valid
@NotNull(message = "items不能为空")
@Size(min = 1, message = "至少需要一个参数项")
private List<ItemDTO> items;
}
@Data
public static class ItemDTO {
/** 参数业务编码 */
private String paramCode;
/** 设定地址 (OPC地址) */
@NotBlank(message = "address不能为空")
private String address;
/** 设定的原始值 */
private String valueRaw;
/** 参数的设定时间 */
private Date setTime;
}
}

View File

@@ -0,0 +1,21 @@
package com.fizz.business.domain.dto;
import lombok.Data;
/**
* 发送任务查询 DTO
* 说明:分页参数沿用 RuoYi BaseController 的 startPage() 机制,从 request 里取 pageNum/pageSize。
*/
@Data
public class SendJobQueryDTO {
/** 设备/产线名称 */
private String deviceName;
/** 状态: PENDING, IN_PROGRESS, COMPLETED, PARTIAL_SUCCESS, FAILED, DELETED */
private String status;
/** 分组类型: DRIVE / FURNACE */
private String groupType;
}

View File

@@ -0,0 +1,23 @@
package com.fizz.business.domain.dto;
import com.fizz.business.domain.BizSendTemplateItem;
import lombok.Data;
import java.util.List;
/**
* 模板明细批量保存(新增/更新/删除)
*/
@Data
public class SendTemplateItemsBatchSaveDTO {
/** 模板ID */
private Integer templateId;
/** 需要新增/更新的明细 */
private List<BizSendTemplateItem> items;
/** 需要删除的明细ID */
private List<Integer> deleteIds;
}

View File

@@ -0,0 +1,22 @@
package com.fizz.business.domain.vo;
import lombok.Data;
/**
* 模板明细 VO
*/
@Data
public class BizSendTemplateItemVO {
private Integer templateItemId;
private Integer templateId;
private Integer itemNo;
private String paramCode;
private String labelEn;
private String groupNameEn;
private String address;
private String defaultValueRaw;
private Boolean enabled;
}

View File

@@ -0,0 +1,21 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 模板主VO含明细
*/
@Data
public class BizSendTemplateVO {
private Integer templateId;
private String templateCode;
private String templateName;
private String deviceName;
private String groupType;
private Integer enabled;
private List<BizSendTemplateItemVO> items;
}

View File

@@ -0,0 +1,26 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
* 发送任务详情 VO
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class SendJobDetailVO extends SendJobVO {
private Date planSendTime;
private Date actualSendTime;
private Date finishTime;
private String remark;
private List<SendJobGroupVO> groups;
}

View File

@@ -0,0 +1,27 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 发送任务分组 VO
*/
@Data
public class SendJobGroupVO {
private Integer groupId;
private Integer jobId;
private Integer groupNo;
private String groupType;
private String groupName;
private String status;
private List<SendJobItemVO> items;
}

View File

@@ -0,0 +1,39 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 发送任务明细项 VO
*/
@Data
public class SendJobItemVO {
private Integer itemId;
private Integer jobId;
private Integer groupId;
private String paramCode;
private String address;
private String valueRaw;
private BigDecimal valueNum;
private Date setTime;
private String resultStatus;
private String resultMsg;
private Integer retryCount;
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,26 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.util.Date;
import java.util.Map;
/**
* 最近一次成功发送结果(用于前端推荐默认值 + 显示上次发送时间)
*/
@Data
public class SendJobLastSuccessVO {
/** 最近一次成功发送时间job.finish_time */
private Date lastSendTime;
/** paramCode -> valueRaw */
private Map<String, String> values;
/** 最近一次成功的jobId可选 */
private Integer jobId;
//IsFromHistory
private Boolean isFromHistory;
}

View File

@@ -0,0 +1,27 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.util.Date;
/**
* 发送任务列表 VO
*/
@Data
public class SendJobVO {
private Integer jobId;
private String bizKey;
private String deviceName;
private String status;
private Long operatorId;
private String operatorName;
private Date createTime;
}

View File

@@ -5,6 +5,7 @@ import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 子计划数据
@@ -57,4 +58,7 @@ public class PdiPlanSubDTO implements Serializable {
@Schema(description = "实际重量")
private Double actualWeight;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -6,6 +6,7 @@ import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
@@ -71,4 +72,7 @@ public class PdoExCoilSubDTO implements Serializable {
@Schema(description = "结束时间")
private LocalDateTime endTime;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -115,5 +115,5 @@ public class CrmPdiPlanForm {
private BigDecimal tailendGaugeLength; // 尾端测厚长度(mm)
private String origin; // 产地
private String originCoilid; // 原卷号
private BigDecimal zincCoatingThickness;//锌层厚度
}

View File

@@ -3,6 +3,7 @@ package com.fizz.business.form;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@@ -18,4 +19,7 @@ public class CrmPdoExcoilForm {
@Schema(description = "结束日期")
private String endDate;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -0,0 +1,10 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.BizSendJobGroup;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BizSendJobGroupMapper extends BaseMapper<BizSendJobGroup> {
}

View File

@@ -0,0 +1,10 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.BizSendJobItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BizSendJobItemMapper extends BaseMapper<BizSendJobItem> {
}

View File

@@ -0,0 +1,10 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.BizSendJob;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BizSendJobMapper extends BaseMapper<BizSendJob> {
}

View File

@@ -0,0 +1,10 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.BizSendTemplateItem;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BizSendTemplateItemMapper extends BaseMapper<BizSendTemplateItem> {
}

View File

@@ -0,0 +1,10 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.BizSendTemplate;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BizSendTemplateMapper extends BaseMapper<BizSendTemplate> {
}

View File

@@ -0,0 +1,20 @@
package com.fizz.business.mapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Map;
/**
* 计划相关仪表板 Mappercrm_pdi_plan
*/
@Mapper
public interface PlanDashboardMapper {
/**
* 查询当前生产中的计划status = 'PRODUCING'
* 返回字段至少包括:
* - coilid, planid, steel_grade, entry_weight, entry_thick, entry_width, start_date 等
*/
Map<String, Object> selectCurrentProducingPlan();
}

View File

@@ -1,18 +1,16 @@
package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.SegmentTotal;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* <p>
* 各机张力,电流等架跟踪表 Mapper 接口
* </p>
*
* @author baomidou
* @since 2023-10-26
* 带钢段工艺参数 Mappercpl_segment_total
*/
@Mapper
public interface SegmentTotalMapper extends BaseMapper<SegmentTotal> {
SegmentTotal getLatestRecord();
public interface SegmentTotalMapper {
/**
* 根据入库钢卷号查询最新一段的 total_values_json
*/
String selectLatestTotalValuesJsonByCoilId(@Param("coilId") String coilId);
}

View File

@@ -3,7 +3,6 @@ package com.fizz.business.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fizz.business.domain.StdAlloy;
import com.fizz.business.domain.SteelGradeInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper

View File

@@ -0,0 +1,27 @@
package com.fizz.business.service;
import java.util.Map;
/**
* 首页仪表板统计服务
*/
public interface DashboardService {
/**
* 当前生产中的计划信息(包含卷号、规格、时间等)
*/
Map<String, Object> getCurrentProducingPlan();
/**
* 当前生产卷的关键工艺参数
* 结构说明(示例):
* {
* "coilId": "...",
* "entrySection": { "POR1": { ... }, "POR2": { ... }, ... },
* "processSection":{ "CLEAN": { ... }, "FUR1": { ... }, ... },
* "exitSection": { "TR": { ... }, "CXL1": { ... }, ... }
* }
*/
Map<String, Object> getCurrentProcessParams();
}

View File

@@ -0,0 +1,23 @@
package com.fizz.business.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fizz.business.domain.BizSendTemplateItem;
import java.util.List;
/**
* 发送模板明细 Service
*/
public interface IBizSendTemplateItemService extends IService<BizSendTemplateItem> {
/**
* 批量更新模板明细仅更新已有ID
*/
Boolean updateItemsBatch(List<BizSendTemplateItem> items);
/**
* 批量保存模板明细(新增/更新/删除)
*/
Boolean batchSave(Integer templateId, List<BizSendTemplateItem> items, List<Integer> deleteIds, String username);
}

View File

@@ -0,0 +1,17 @@
package com.fizz.business.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fizz.business.domain.BizSendTemplate;
import com.fizz.business.domain.vo.BizSendTemplateVO;
/**
* 发送默认模板 Service
*/
public interface IBizSendTemplateService extends IService<BizSendTemplate> {
/**
* 按模板编码获取模板(含明细)
*/
BizSendTemplateVO getTemplateWithItems(String templateCode);
}

View File

@@ -0,0 +1,15 @@
package com.fizz.business.service;
import com.fizz.business.domain.vo.SendJobLastSuccessVO;
/**
* 发送任务查询扩展(用于推荐值、上次发送时间)
*/
public interface ISendJobQueryService {
/**
* 查询最近一次成功发送(按 groupType 过滤DRIVE / FURNACE
*/
SendJobLastSuccessVO getLastSuccess(String groupType);
}

View File

@@ -0,0 +1,41 @@
package com.fizz.business.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fizz.business.domain.BizSendJob;
import com.fizz.business.domain.dto.SendJobCreateDTO;
import com.fizz.business.domain.dto.SendJobQueryDTO;
import com.fizz.business.domain.vo.SendJobDetailVO;
import java.util.List;
/**
* 发送任务 Service
*/
public interface ISendJobService extends IService<BizSendJob> {
/**
* 创建发送任务(包含分组与明细)
*/
Integer createSendJob(SendJobCreateDTO dto);
/**
* 查询发送任务列表(分页由 Controller 的 startPage() 控制)
*/
List<BizSendJob> selectSendJobList(SendJobQueryDTO query);
/**
* 查询发送任务详情(包含分组与明细)
*/
SendJobDetailVO selectSendJobDetail(Integer jobId);
/**
* 删除任务逻辑删除status=DELETED
*/
Boolean deleteSendJobByJobIds(Integer[] jobIds);
/**
* 执行发送:写入 OPC并将发送结果保存为历史更新 job/group/item 状态)
*/
Boolean executeSendJob(Integer jobId);
}

View File

@@ -2,7 +2,6 @@ package com.fizz.business.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.fizz.business.domain.StdAlloy;
import com.fizz.business.domain.SteelGradeInfo;
public interface SteelGradeInfoService extends IService<StdAlloy> {
}

View File

@@ -0,0 +1,98 @@
package com.fizz.business.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fizz.business.domain.BizSendTemplateItem;
import com.fizz.business.mapper.BizSendTemplateItemMapper;
import com.fizz.business.service.IBizSendTemplateItemService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
public class BizSendTemplateItemServiceImpl extends ServiceImpl<BizSendTemplateItemMapper, BizSendTemplateItem> implements IBizSendTemplateItemService {
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateItemsBatch(List<BizSendTemplateItem> items) {
if (items == null || items.isEmpty()) {
return true;
}
// MyBatis-Plus 批量更新:这里简单循环 updateById数量约40~100可接受
for (BizSendTemplateItem it : items) {
this.updateById(it);
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchSave(Integer templateId, List<BizSendTemplateItem> items, List<Integer> deleteIds, String username) {
Date now = new Date();
// 1) 删除(物理删除)。如果你们希望逻辑删除,可在此改为 enabled=0 / deleted 标记。
if (deleteIds != null && !deleteIds.isEmpty()) {
this.removeByIds(deleteIds);
}
// 2) 新增/更新
if (items == null) {
items = Collections.emptyList();
}
// 基础校验templateId 必须一致
for (BizSendTemplateItem it : items) {
if (it.getTemplateId() == null) {
it.setTemplateId(templateId);
}
}
// 校验:同一 templateId 下 paramCode 唯一
// - 先校验本次提交内是否重复
Set<String> seen = new HashSet<>();
for (BizSendTemplateItem it : items) {
if (it.getParamCode() == null || it.getParamCode().trim().isEmpty()) {
throw new IllegalArgumentException("paramCode is required");
}
String code = it.getParamCode().trim();
if (!seen.add(code)) {
throw new IllegalArgumentException("Duplicate paramCode in request: " + code);
}
it.setParamCode(code);
}
// - 再校验数据库里是否已存在同 paramCode排除自身ID
for (BizSendTemplateItem it : items) {
LambdaQueryWrapper<BizSendTemplateItem> qw = new LambdaQueryWrapper<>();
qw.eq(BizSendTemplateItem::getTemplateId, templateId)
.eq(BizSendTemplateItem::getParamCode, it.getParamCode());
if (it.getTemplateItemId() != null) {
qw.ne(BizSendTemplateItem::getTemplateItemId, it.getTemplateItemId());
}
long cnt = this.count(qw);
if (cnt > 0) {
throw new IllegalArgumentException("paramCode already exists: " + it.getParamCode());
}
}
for (BizSendTemplateItem it : items) {
it.setTemplateId(templateId);
if (it.getTemplateItemId() == null) {
it.setCreateBy(username);
it.setCreateTime(now);
it.setUpdateBy(username);
it.setUpdateTime(now);
this.save(it);
} else {
it.setUpdateBy(username);
it.setUpdateTime(now);
this.updateById(it);
}
}
return true;
}
}

View File

@@ -0,0 +1,55 @@
package com.fizz.business.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fizz.business.domain.BizSendTemplate;
import com.fizz.business.domain.BizSendTemplateItem;
import com.fizz.business.domain.vo.BizSendTemplateItemVO;
import com.fizz.business.domain.vo.BizSendTemplateVO;
import com.fizz.business.mapper.BizSendTemplateItemMapper;
import com.fizz.business.mapper.BizSendTemplateMapper;
import com.fizz.business.service.IBizSendTemplateService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class BizSendTemplateServiceImpl extends ServiceImpl<BizSendTemplateMapper, BizSendTemplate> implements IBizSendTemplateService {
@Autowired
private BizSendTemplateItemMapper templateItemMapper;
@Override
public BizSendTemplateVO getTemplateWithItems(String templateCode) {
BizSendTemplate template = this.lambdaQuery()
.eq(BizSendTemplate::getTemplateCode, templateCode)
.eq(BizSendTemplate::getEnabled, 1)
.one();
if (template == null) {
return null;
}
List<BizSendTemplateItem> items = templateItemMapper.selectList(
new LambdaQueryWrapper<BizSendTemplateItem>()
.eq(BizSendTemplateItem::getTemplateId, template.getTemplateId())
.eq(BizSendTemplateItem::getEnabled, 1)
.orderByAsc(BizSendTemplateItem::getItemNo)
);
BizSendTemplateVO vo = new BizSendTemplateVO();
BeanUtils.copyProperties(template, vo);
List<BizSendTemplateItemVO> itemVOs = items.stream().map(item -> {
BizSendTemplateItemVO it = new BizSendTemplateItemVO();
BeanUtils.copyProperties(item, it);
return it;
}).collect(Collectors.toList());
vo.setItems(itemVOs);
return vo;
}
}

View File

@@ -0,0 +1,113 @@
package com.fizz.business.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fizz.business.constants.enums.DeviceEnum;
import com.fizz.business.mapper.PlanDashboardMapper;
import com.fizz.business.mapper.SegmentTotalMapper;
import com.fizz.business.service.DashboardService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
/**
* 首页仪表板统计服务实现
*/
@Service
public class DashboardServiceImpl implements DashboardService {
@Resource
private PlanDashboardMapper planDashboardMapper;
@Resource
private SegmentTotalMapper segmentTotalMapper;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Map<String, Object> getCurrentProducingPlan() {
// 查询当前 PRODUCING 的计划
Map<String, Object> plan = planDashboardMapper.selectCurrentProducingPlan();
if (plan == null) {
return Collections.emptyMap();
}
return plan;
}
@Override
public Map<String, Object> getCurrentProcessParams() {
Map<String, Object> result = new HashMap<>();
// 1. 当前生产计划
Map<String, Object> plan = planDashboardMapper.selectCurrentProducingPlan();
if (plan == null || plan.get("coilid") == null) {
return result;
}
String coilId = String.valueOf(plan.get("coilid"));
result.put("coilId", coilId);
// 2. 查询该卷最新一段的 total_values_json
String totalValuesJson = segmentTotalMapper.selectLatestTotalValuesJsonByCoilId(coilId);
if (totalValuesJson == null || totalValuesJson.isEmpty()) {
return result;
}
try {
// 3. 解析 JSON -> Map<String, Object>
Map<String, Object> valuesMap = objectMapper.readValue(
totalValuesJson,
new TypeReference<Map<String, Object>>() {}
);
// 4. 按设备 + 段类型整理数据
Map<String, Object> entrySection = new LinkedHashMap<>();
Map<String, Object> processSection = new LinkedHashMap<>();
Map<String, Object> exitSection = new LinkedHashMap<>();
for (DeviceEnum device : DeviceEnum.values()) {
List<String> fields = device.getParamFields();
if (fields == null || fields.isEmpty()) {
continue;
}
Map<String, Object> devData = new LinkedHashMap<>();
for (String field : fields) {
if (valuesMap.containsKey(field)) {
devData.put(field, valuesMap.get(field));
}
}
if (devData.isEmpty()) {
continue;
}
// key 用设备英文枚举名,如 POR1/FUR1/TM/TL/COAT 等
String key = device.name();
switch (device.getSectionType()) {
case ENTRY:
entrySection.put(key, devData);
break;
case PROCESS:
processSection.put(key, devData);
break;
case EXIT:
exitSection.put(key, devData);
break;
default:
break;
}
}
result.put("entrySection", entrySection);
result.put("processSection", processSection);
result.put("exitSection", exitSection);
} catch (Exception e) {
// 解析异常时,可按你项目的日志方案记录
e.printStackTrace();
}
return result;
}
}

View File

@@ -50,7 +50,7 @@ public class PdoExCoilServiceImpl implements PdoExCoilService {
double aimWeightTop = 1;
double aimWeightBottom = 1;
pdoExCoilDTO.setZincCoatingThickness(plan.getZincCoatingThickness());
pdoExCoilDTO.setPlanOrigin(plan.getUnitCode());
pdoExCoilDTO.setUnitCode(plan.getUnitCode());
pdoExCoilDTO.setProcessCode(SYSTEM_MODULE);

View File

@@ -0,0 +1,196 @@
package com.fizz.business.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fizz.business.domain.BizSendJob;
import com.fizz.business.domain.BizSendJobGroup;
import com.fizz.business.domain.BizSendJobItem;
import com.fizz.business.domain.CrmPdiPlan;
import com.fizz.business.domain.vo.BizSendTemplateItemVO;
import com.fizz.business.domain.vo.BizSendTemplateVO;
import com.fizz.business.domain.vo.SendJobLastSuccessVO;
import com.fizz.business.mapper.BizSendJobGroupMapper;
import com.fizz.business.mapper.BizSendJobItemMapper;
import com.fizz.business.mapper.BizSendJobMapper;
import com.fizz.business.mapper.CrmPdiPlanMapper;
import com.fizz.business.service.IBizSendTemplateService;
import com.fizz.business.service.ISendJobQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class SendJobQueryServiceImpl implements ISendJobQueryService {
@Autowired
private BizSendJobMapper jobMapper;
@Autowired
private BizSendJobGroupMapper groupMapper;
@Autowired
private BizSendJobItemMapper itemMapper;
@Autowired
private IBizSendTemplateService templateService;
@Autowired
private CrmPdiPlanMapper planMapper;
@Override
public SendJobLastSuccessVO getLastSuccess(String groupType) {
// 1. 根据 groupType 从 group 表中找到所有相关的 job_id
List<BizSendJobGroup> groupsWithType = groupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>().eq(BizSendJobGroup::getGroupType, groupType)
);
if (CollectionUtils.isEmpty(groupsWithType)) {
// 如果一个相关的 group 都没有,直接走 fallback 逻辑
return getFallbackValues(groupType);
}
List<Integer> jobIds = groupsWithType.stream()
.map(BizSendJobGroup::getJobId)
.distinct()
.collect(Collectors.toList());
// 2. 在这些 job_id 中,找到状态为 COMPLETED 且时间最新的一个 job
BizSendJob lastJob = jobMapper.selectOne(
new LambdaQueryWrapper<BizSendJob>()
.in(BizSendJob::getJobId, jobIds)
.eq(BizSendJob::getStatus, "COMPLETED")
.orderByDesc(BizSendJob::getFinishTime)
.last("LIMIT 1")
);
// 如果找到了,直接返回上次成功的值
if (lastJob != null) {
List<BizSendJobGroup> groups = groupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>()
.eq(BizSendJobGroup::getJobId, lastJob.getJobId())
);
if (groups != null && !groups.isEmpty()) {
List<Integer> groupIdsForJob = groups.stream().map(BizSendJobGroup::getGroupId).collect(Collectors.toList());
List<BizSendJobItem> items = itemMapper.selectList(
new LambdaQueryWrapper<BizSendJobItem>()
.in(BizSendJobItem::getGroupId, groupIdsForJob)
.eq(BizSendJobItem::getResultStatus, "SUCCESS")
.orderByAsc(BizSendJobItem::getItemId)
);
Map<String, String> values = new HashMap<>();
for (BizSendJobItem it : items) {
if (it.getParamCode() == null) continue;
values.put(it.getParamCode(), it.getValueRaw());
}
SendJobLastSuccessVO vo = new SendJobLastSuccessVO();
vo.setJobId(lastJob.getJobId());
vo.setLastSendTime(lastJob.getFinishTime());
vo.setValues(values);
// 添加一个标志,告诉前端这是真实发送过的值
vo.setIsFromHistory(true);
return vo;
}
}
// 如果没找到符合条件的 job走 fallback 逻辑
return getFallbackValues(groupType);
}
private SendJobLastSuccessVO getFallbackValues(String groupType) {
if ("FURNACE".equalsIgnoreCase(groupType)) {
return getFurnaceTemplateValues();
} else if ("DRIVE".equalsIgnoreCase(groupType)) {
return getDrivePlanValues();
}
return null; // 其他情况返回 null
}
/**
* 获取炉火工艺模板的默认值
*/
private SendJobLastSuccessVO getFurnaceTemplateValues() {
// "FURNACE_DEFAULT" 是前端写死的模板编码
BizSendTemplateVO template = templateService.getTemplateWithItems("FURNACE_DEFAULT");
if (template == null || template.getItems() == null || template.getItems().isEmpty()) {
return createEmptyResponse("Furnace template 'FURNACE_DEFAULT' not found or is empty.");
}
Map<String, String> values = new HashMap<>();
for (BizSendTemplateItemVO item : template.getItems()) {
if (item.getEnabled() && item.getParamCode() != null) {
values.put(item.getParamCode(), item.getDefaultValueRaw());
}
}
SendJobLastSuccessVO vo = new SendJobLastSuccessVO();
vo.setValues(values);
// 标志为非历史值
vo.setIsFromHistory(false);
return vo;
}
/**
* 获取当前计划的传动参数
*/
private SendJobLastSuccessVO getDrivePlanValues() {
// 优先找 PRODUCING 或 ONLINE 状态的计划,按更新时间倒序取最新的一个
CrmPdiPlan currentPlan = planMapper.selectOne(
new LambdaQueryWrapper<CrmPdiPlan>()
.in(CrmPdiPlan::getStatus, "PRODUCING", "ONLINE")
.orderByDesc(CrmPdiPlan::getUpdateTime)
.last("LIMIT 1")
);
// 如果没有正在生产的,就找第一个 READY 的计划
if (currentPlan == null) {
currentPlan = planMapper.selectOne(
new LambdaQueryWrapper<CrmPdiPlan>()
.eq(CrmPdiPlan::getStatus, "READY")
.orderByAsc(CrmPdiPlan::getSeqid)
.last("LIMIT 1")
);
}
if (currentPlan == null) {
return createEmptyResponse("No active or ready plan found.");
}
// 将 CrmPdiPlan 对象的字段反射为 Map
Map<String, String> values = new HashMap<>();
Field[] fields = CrmPdiPlan.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
Object value = field.get(currentPlan);
if (value != null) {
values.put(field.getName(), String.valueOf(value));
}
} catch (IllegalAccessException e) {
// 忽略无法访问的字段
}
}
SendJobLastSuccessVO vo = new SendJobLastSuccessVO();
vo.setValues(values);
vo.setIsFromHistory(false); // 标志为非历史值
return vo;
}
/**
* 创建一个空的带消息的返回对象
*/
private SendJobLastSuccessVO createEmptyResponse(String message) {
SendJobLastSuccessVO vo = new SendJobLastSuccessVO();
vo.setValues(Collections.singletonMap("_message", message));
vo.setIsFromHistory(false);
return vo;
}
}

View File

@@ -0,0 +1,344 @@
package com.fizz.business.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fizz.business.domain.BizSendJob;
import com.fizz.business.domain.BizSendJobGroup;
import com.fizz.business.domain.BizSendJobItem;
import com.fizz.business.domain.dto.SendJobCreateDTO;
import com.fizz.business.domain.dto.SendJobQueryDTO;
import com.fizz.business.domain.vo.SendJobDetailVO;
import com.fizz.business.domain.vo.SendJobGroupVO;
import com.fizz.business.domain.vo.SendJobItemVO;
import com.fizz.business.mapper.BizSendJobGroupMapper;
import com.fizz.business.mapper.BizSendJobItemMapper;
import com.fizz.business.mapper.BizSendJobMapper;
import com.fizz.business.service.ISendJobService;
import com.fizz.business.service.manager.OpcMessageIdsManager;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.Date;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SendJobServiceImpl extends ServiceImpl<BizSendJobMapper, BizSendJob> implements ISendJobService {
@Autowired
private com.fizz.business.comm.OPC.OpcMessageSend opcMessageSend;
@Autowired
private BizSendJobGroupMapper jobGroupMapper;
@Autowired
private BizSendJobItemMapper jobItemMapper;
@Autowired
private OpcMessageIdsManager opcMessageIdsManager;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer createSendJob(SendJobCreateDTO dto) {
// 1. 创建批次
BizSendJob job = new BizSendJob();
BeanUtils.copyProperties(dto, job);
// 生成业务唯一键
String bizKey = opcMessageIdsManager.generateMessageId("SEND_JOB");
job.setBizKey(bizKey);
job.setStatus("PENDING");
// 设置操作人
String username = SecurityUtils.getUsername();
job.setCreateBy(username);
job.setCreateTime(new Date());
// 保存批次
baseMapper.insert(job);
// 2. 保存分组与明细
if (dto.getGroups() != null) {
for (SendJobCreateDTO.GroupDTO groupDTO : dto.getGroups()) {
// 2.1 保存分组
BizSendJobGroup group = new BizSendJobGroup();
BeanUtils.copyProperties(groupDTO, group);
group.setJobId(job.getJobId());
group.setStatus("PENDING");
group.setCreateBy(username);
group.setCreateTime(new Date());
jobGroupMapper.insert(group);
// 2.2 保存明细项
if (groupDTO.getItems() != null) {
for (SendJobCreateDTO.ItemDTO itemDTO : groupDTO.getItems()) {
BizSendJobItem item = new BizSendJobItem();
BeanUtils.copyProperties(itemDTO, item);
item.setJobId(job.getJobId());
item.setGroupId(group.getGroupId());
item.setResultStatus("PENDING");
item.setCreateBy(username);
item.setCreateTime(new Date());
// 尝试将valueRaw转为数值
try {
item.setValueNum(new BigDecimal(itemDTO.getValueRaw()));
} catch (Exception e) {
log.warn("转换数值失败: {}", itemDTO.getValueRaw(), e);
}
jobItemMapper.insert(item);
}
}
}
}
return job.getJobId();
}
@Override
public List<BizSendJob> selectSendJobList(SendJobQueryDTO query) {
LambdaQueryWrapper<BizSendJob> qw = new LambdaQueryWrapper<>();
qw.eq(StringUtils.isNotBlank(query.getDeviceName()),
BizSendJob::getDeviceName, query.getDeviceName())
.eq(StringUtils.isNotBlank(query.getStatus()),
BizSendJob::getStatus, query.getStatus())
.orderByDesc(BizSendJob::getCreateTime);
List<BizSendJob> jobs = baseMapper.selectList(qw);
if (jobs == null || jobs.isEmpty()) {
return jobs;
}
// 如果传了 groupType如 FURNACE则仅保留包含该 groupType 的 job
if (StringUtils.isNotBlank(query.getGroupType())) {
String gt = query.getGroupType().trim();
List<Integer> jobIds = jobs.stream().map(BizSendJob::getJobId).collect(Collectors.toList());
List<BizSendJobGroup> groups = jobGroupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>()
.in(BizSendJobGroup::getJobId, jobIds)
.eq(BizSendJobGroup::getGroupType, gt)
);
if (groups == null || groups.isEmpty()) {
return Collections.emptyList();
}
Set<Integer> allowedJobIds = groups.stream().map(BizSendJobGroup::getJobId).collect(Collectors.toSet());
return jobs.stream().filter(j -> allowedJobIds.contains(j.getJobId())).collect(Collectors.toList());
}
return jobs;
}
@Override
public SendJobDetailVO selectSendJobDetail(Integer jobId) {
// 1. 查询任务
BizSendJob job = baseMapper.selectById(jobId);
if (job == null) {
return null;
}
// 2. 转换为VO
SendJobDetailVO detailVO = new SendJobDetailVO();
BeanUtils.copyProperties(job, detailVO);
// 3. 查询分组
List<BizSendJobGroup> groups = jobGroupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>()
.eq(BizSendJobGroup::getJobId, jobId)
.orderByAsc(BizSendJobGroup::getGroupNo)
);
if (groups.isEmpty()) {
return detailVO;
}
// 4. 查询所有明细项
List<Integer> groupIds = groups.stream()
.map(BizSendJobGroup::getGroupId)
.collect(Collectors.toList());
List<BizSendJobItem> items = jobItemMapper.selectList(
new LambdaQueryWrapper<BizSendJobItem>()
.in(BizSendJobItem::getGroupId, groupIds)
);
// 5. 按groupId分组
Map<Integer, List<BizSendJobItem>> groupItemsMap = items.stream()
.collect(Collectors.groupingBy(BizSendJobItem::getGroupId));
// 6. 构建分组VO
List<SendJobGroupVO> groupVOs = groups.stream().map(group -> {
SendJobGroupVO groupVO = new SendJobGroupVO();
BeanUtils.copyProperties(group, groupVO);
// 设置分组下的明细项
List<BizSendJobItem> groupItems = groupItemsMap
.getOrDefault(group.getGroupId(), Collections.emptyList());
List<SendJobItemVO> itemVOs = groupItems.stream().map(item -> {
SendJobItemVO itemVO = new SendJobItemVO();
BeanUtils.copyProperties(item, itemVO);
return itemVO;
}).collect(Collectors.toList());
groupVO.setItems(itemVOs);
return groupVO;
}).collect(Collectors.toList());
detailVO.setGroups(groupVOs);
return detailVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteSendJobByJobIds(Integer[] jobIds) {
if (jobIds == null || jobIds.length == 0) {
return true;
}
// 1. 更新任务状态为已删除
BizSendJob updateJob = new BizSendJob();
updateJob.setStatus("DELETED");
updateJob.setUpdateBy(SecurityUtils.getUsername());
updateJob.setUpdateTime(new Date());
LambdaQueryWrapper<BizSendJob> qw = new LambdaQueryWrapper<>();
qw.in(BizSendJob::getJobId, Arrays.asList(jobIds));
return update(updateJob, qw);
}
/**
* 执行发送:写入 OPC并将发送结果保存为历史更新 job/group/item 状态)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean executeSendJob(Integer jobId) {
BizSendJob job = baseMapper.selectById(jobId);
if (job == null) {
return false;
}
if ("DELETED".equalsIgnoreCase(job.getStatus())) {
return false;
}
// 更新 job 状态为发送中
BizSendJob jobUpd = new BizSendJob();
jobUpd.setJobId(jobId);
jobUpd.setStatus("IN_PROGRESS");
jobUpd.setActualSendTime(new Date());
jobUpd.setUpdateBy(SecurityUtils.getUsername());
jobUpd.setUpdateTime(new Date());
baseMapper.updateById(jobUpd);
// 查询该job下所有 PENDING 的 item
List<BizSendJobItem> items = jobItemMapper.selectList(
new LambdaQueryWrapper<BizSendJobItem>()
.eq(BizSendJobItem::getJobId, jobId)
.eq(BizSendJobItem::getResultStatus, "PENDING")
);
// 如果没有 PENDING 的项,检查是否有其他状态的项
if (items == null || items.isEmpty()) {
boolean hasItems = jobItemMapper.selectCount(
new LambdaQueryWrapper<BizSendJobItem>().eq(BizSendJobItem::getJobId, jobId)
) > 0;
BizSendJob finish = new BizSendJob();
finish.setJobId(jobId);
finish.setStatus(hasItems ? "COMPLETED" : "FAILED");
finish.setFinishTime(new Date());
finish.setUpdateBy(SecurityUtils.getUsername());
finish.setUpdateTime(new Date());
finish.setRemark(hasItems ? "没有待发送的项" : "没有找到任何发送项");
baseMapper.updateById(finish);
return hasItems;
}
// 查询该job下group
List<BizSendJobGroup> groups = jobGroupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>().eq(BizSendJobGroup::getJobId, jobId)
);
for (BizSendJobGroup g : groups) {
BizSendJobGroup gu = new BizSendJobGroup();
gu.setGroupId(g.getGroupId());
gu.setStatus("IN_PROGRESS");
gu.setUpdateBy(SecurityUtils.getUsername());
gu.setUpdateTime(new Date());
jobGroupMapper.updateById(gu);
}
boolean allSuccess = true;
// 按 address 分组,只发送最后一条记录
Map<String, List<BizSendJobItem>> itemsByAddr = items.stream()
.collect(Collectors.groupingBy(BizSendJobItem::getAddress));
for (Map.Entry<String, List<BizSendJobItem>> entry : itemsByAddr.entrySet()) {
List<BizSendJobItem> addrItems = entry.getValue();
// 取最后一条(按 itemId 最大 || createTime 最新)
addrItems.sort(Comparator.comparing(BizSendJobItem::getItemId));
BizSendJobItem last = addrItems.get(addrItems.size() - 1);
boolean success = true;
String failMsg = null;
try {
opcMessageSend.writeNode(last.getAddress(), last.getValueRaw());
} catch (Exception ex) {
success = false;
allSuccess = false;
failMsg = ex.getMessage();
}
// 更新该 address 下所有项的状态(统一)
for (BizSendJobItem it : addrItems) {
BizSendJobItem upd = new BizSendJobItem();
upd.setItemId(it.getItemId());
upd.setResultStatus(success ? "SUCCESS" : "FAILED");
upd.setResultMsg(success ? "OK" : failMsg);
upd.setUpdateBy(SecurityUtils.getUsername());
upd.setUpdateTime(new Date());
jobItemMapper.updateById(upd);
}
}
// 更新 group 状态(按 group 下 item 是否全部成功)
Map<Integer, List<BizSendJobItem>> itemByGroup = jobItemMapper.selectList(
new LambdaQueryWrapper<BizSendJobItem>().eq(BizSendJobItem::getJobId, jobId)
).stream().collect(Collectors.groupingBy(BizSendJobItem::getGroupId));
for (BizSendJobGroup g : groups) {
List<BizSendJobItem> gi = itemByGroup.getOrDefault(g.getGroupId(), Collections.emptyList());
boolean groupOk = gi.stream().allMatch(x -> "SUCCESS".equalsIgnoreCase(x.getResultStatus()));
BizSendJobGroup gu = new BizSendJobGroup();
gu.setGroupId(g.getGroupId());
gu.setStatus(groupOk ? "COMPLETED" : "FAILED");
gu.setUpdateBy(SecurityUtils.getUsername());
gu.setUpdateTime(new Date());
jobGroupMapper.updateById(gu);
}
// 更新 job 最终状态
BizSendJob finish = new BizSendJob();
finish.setJobId(jobId);
finish.setFinishTime(new Date());
finish.setUpdateBy(SecurityUtils.getUsername());
finish.setUpdateTime(new Date());
finish.setStatus(allSuccess ? "COMPLETED" : "PARTIAL_SUCCESS");
finish.setRemark(allSuccess ? "全部发送成功" : "部分发送失败");
baseMapper.updateById(finish);
return true;
}
}

View File

@@ -14,6 +14,16 @@ import java.util.Map;
@Component
public class OpcMessageIdsManager {
/**
* 生成业务/消息唯一ID用于发送批次 bizKey 等)
* 规则PREFIX_时间戳_8位随机
*/
public String generateMessageId(String prefix) {
String p = (prefix == null || prefix.trim().isEmpty()) ? "MSG" : prefix.trim();
String random = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
return (p + "_" + System.currentTimeMillis() + "_" + random).toUpperCase();
}
public static List<String> msgTriggers = Lists.newArrayList();
public static Map<String,String> lineMeasureIds = Maps.newHashMap();

View File

@@ -311,4 +311,7 @@ public class CrmPdiPlanVO {
@Schema(description = "原卷号")
private String originCoilid;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@@ -145,4 +146,7 @@ public class PdiPlanSetupInfoVO {
@Schema(description = "设定值列表")
List<ModSetupResultVO> lists;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@@ -157,4 +158,7 @@ public class PdiPlanVO implements Serializable {
@Schema(description = "实验类别")
private String experimentType;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -3,6 +3,7 @@ package com.fizz.business.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -80,5 +81,8 @@ public class Plan2PdoVO {
@Schema(description = "热卷温度")
private Double hotCoilTemp;
@Schema(description = "锌层厚度")
private BigDecimal zincCoatingThickness;
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fizz.business.mapper.PlanDashboardMapper">
<!-- 当前生产中的计划status = 'PRODUCING',按 producing_time / start_date 倒序取最新一条) -->
<select id="selectCurrentProducingPlan" resultType="java.util.Map">
SELECT
id,
coilid,
planid,
steel_grade AS steelGrade,
entry_weight AS entryWeight,
entry_thick AS entryThick,
entry_width AS entryWidth,
entry_length AS entryLength,
status,
start_date AS startDate,
end_date AS endDate,
producing_time AS producingTime,
unit_code AS unitCode
FROM crm_pdi_plan
WHERE status = 'PRODUCING'
ORDER BY
producing_time DESC,
start_date DESC,
id DESC
LIMIT 1
</select>
</mapper>

View File

@@ -1,7 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.fizz.business.mapper.SegmentTotalMapper">
<select id="getLatestRecord" resultType="com.fizz.business.domain.SegmentTotal">
SELECT * FROM cpg_segment_total where id=(SELECT max(id) FROM cpg_segment_total)
<!-- 根据入库钢卷号查询最新一段的 total_values_json -->
<select id="selectLatestTotalValuesJsonByCoilId"
parameterType="java.lang.String"
resultType="java.lang.String">
SELECT
total_values_json
FROM cpl_segment_total
WHERE en_coil_id = #{coilId}
ORDER BY seg_no DESC
LIMIT 1
</select>
</mapper>