Compare commits

...

2 Commits

Author SHA1 Message Date
32fa2c89ed Merge remote-tracking branch 'origin/main' 2025-12-31 17:59:53 +08:00
5c7db39940 写入逻辑 2025-12-31 17:59:19 +08:00
29 changed files with 1262 additions and 0 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()) {

View File

@@ -0,0 +1,31 @@
package com.fizz.business.controller;
import com.fizz.business.domain.vo.BizSendTemplateVO;
import com.fizz.business.service.IBizSendTemplateService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 发送模板配置 Controller
*/
@RestController
@RequestMapping("/business/sendTemplate")
public class BizSendTemplateController extends BaseController {
@Autowired
private IBizSendTemplateService templateService;
/**
* 按模板编码获取模板(含明细)
*/
@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);
}
}

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

@@ -0,0 +1,51 @@
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;
}

View File

@@ -0,0 +1,40 @@
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;
}

View File

@@ -0,0 +1,58 @@
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;
}

View File

@@ -0,0 +1,35 @@
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;
/** 模板编码 */
private String templateCode;
/** 模板名称 */
private String templateName;
/** 默认设备名称 */
private String deviceName;
/** 组类型 DRIVE / FURNACE */
private String groupType;
/** 是否启用 */
private Integer enabled;
}

View File

@@ -0,0 +1,43 @@
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;
}

View File

@@ -0,0 +1,76 @@
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;
/** 设定的原始值 */
@NotBlank(message = "valueRaw不能为空")
private String valueRaw;
/** 参数的设定时间 */
private Date setTime;
}
}

View File

@@ -0,0 +1,18 @@
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;
}

View File

@@ -0,0 +1,19 @@
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;
}

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,38 @@
package com.fizz.business.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
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,23 @@
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;
}

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

@@ -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,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

@@ -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,77 @@
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.vo.SendJobLastSuccessVO;
import com.fizz.business.mapper.BizSendJobGroupMapper;
import com.fizz.business.mapper.BizSendJobItemMapper;
import com.fizz.business.mapper.BizSendJobMapper;
import com.fizz.business.service.ISendJobQueryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class SendJobQueryServiceImpl implements ISendJobQueryService {
@Autowired
private BizSendJobMapper jobMapper;
@Autowired
private BizSendJobGroupMapper groupMapper;
@Autowired
private BizSendJobItemMapper itemMapper;
@Override
public SendJobLastSuccessVO getLastSuccess(String groupType) {
// 1) 先找最近一次 COMPLETED 的 job倒序
BizSendJob lastJob = jobMapper.selectOne(
new LambdaQueryWrapper<BizSendJob>()
.eq(BizSendJob::getStatus, "COMPLETED")
.orderByDesc(BizSendJob::getFinishTime)
.last("LIMIT 1")
);
if (lastJob == null) {
return null;
}
// 2) 找该 job 下对应 groupType 的 groups
List<BizSendJobGroup> groups = groupMapper.selectList(
new LambdaQueryWrapper<BizSendJobGroup>()
.eq(BizSendJobGroup::getJobId, lastJob.getJobId())
.eq(groupType != null && !groupType.trim().isEmpty(), BizSendJobGroup::getGroupType, groupType)
);
if (groups == null || groups.isEmpty()) {
return null;
}
List<Integer> groupIds = groups.stream().map(BizSendJobGroup::getGroupId).collect(java.util.stream.Collectors.toList());
// 3) 取这些 group 的 item按 paramCode 聚合最后一次值(同 paramCode 取最后一条 itemId 最大)
List<BizSendJobItem> items = itemMapper.selectList(
new LambdaQueryWrapper<BizSendJobItem>()
.in(BizSendJobItem::getGroupId, groupIds)
.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);
return vo;
}
}

View File

@@ -0,0 +1,320 @@
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);
return baseMapper.selectList(qw);
}
@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,17 @@ 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();