新增webcad生产单导入:1、cad拆单同步新结构优化适配;2、cad拆单支持基于配置限制板件数量和是否异步拆单,新增异步拆单;

This commit is contained in:
gaoqr
2025-05-13 17:32:39 +08:00
parent b9e7481bdf
commit 341fd485b3
31 changed files with 3485 additions and 1393 deletions
@@ -13,7 +13,7 @@ import org.springframework.context.annotation.Bean;
public class ChenfengIDAutoConfiguration {
@Bean
public SnowflakeIdWorker3rd ipQueryService() {
public SnowflakeIdWorker3rd idWorker() {
return new SnowflakeIdWorker3rd();
}
}
@@ -1,6 +1,8 @@
package com.cf.imes.framework.organ.core.mq.rabbitmq;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
@@ -8,6 +10,7 @@ import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import static com.cf.imes.framework.web.core.util.WebFrameworkUtils.HEADER_ORGAN_ID;
import static com.cf.imes.framework.web.core.util.WebFrameworkUtils.HEADER_PEEK;
/**
* RabbitMQ 消息队列的多组织 {@link ProducerInterceptor} 实现类
@@ -25,6 +28,11 @@ public class OrganRabbitMQMessagePostProcessor implements MessagePostProcessor {
if (organId != null) {
message.getMessageProperties().getHeaders().put(HEADER_ORGAN_ID, organId);
}
String peek = DynamicDataSourceContextHolder.peek();
if (StringUtils.isNotEmpty(peek)) {
message.getMessageProperties().getHeaders().put(HEADER_PEEK, peek);
}
return message;
}
@@ -16,6 +16,8 @@
package org.springframework.messaging.handler.invocation;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.organ.core.util.OrganUtils;
import org.springframework.core.DefaultParameterNameDiscoverer;
@@ -33,6 +35,7 @@ import java.lang.reflect.Type;
import java.util.Arrays;
import static com.cf.imes.framework.web.core.util.WebFrameworkUtils.HEADER_ORGAN_ID;
import static com.cf.imes.framework.web.core.util.WebFrameworkUtils.HEADER_PEEK;
/**
* Extension of {@link HandlerMethod} that invokes the underlying method with
@@ -122,6 +125,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
// 注意:如下是本类的改动点!!!
// 情况一:无组织编号的情况
Long organId= parseorganId(message);
parsePeek(message);
if (organId == null) {
return doInvoke(args);
}
@@ -149,6 +153,18 @@ public class InvocableHandlerMethod extends HandlerMethod {
throw new IllegalArgumentException("未知的数据类型:" + organId);
}
/**
* 头部带有多数据源,透传
*
* @param message
*/
private void parsePeek(Message<?> message) {
Object peek = message.getHeaders().get(HEADER_PEEK);
if (ObjectUtil.isNotNull(peek)) {
DynamicDataSourceContextHolder.push(peek.toString());
}
}
/**
* Get the method argument values for the current message, checking the provided
* argument values and falling back to the configured argument resolvers.
@@ -1,20 +1,12 @@
package com.cf.imes.framework.mq.rabbitmq.config;
import com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import java.util.HashMap;
import java.util.Map;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.*;
/**
* RabbitMQ 消息队列配置类
*
@@ -31,137 +23,4 @@ public class ChenfengRabbitMQAutoConfiguration {
public MessageConverter createMessageConverter() {
return new Jackson2JsonMessageConverter();
}
/**
* 生产单excel导入交换机、队列、绑定声明
*
* @return
*/
@Bean
public FanoutExchange orderImportDefaultExcelFanoutExchange() {
return new FanoutExchange(RabbitMqConstants.ORDER_IMPORT_DEFAULT_EXCEL_EXCHANGE);
}
/**
* 生产单excel导入队列声明
*
* @return
*/
@Bean
public Queue orderImportDefaultExcelQueue() {
Map<String, Object> args = new HashMap<>(1);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
args.put("x-dead-letter-routing-key", ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
return QueueBuilder.durable(RabbitMqConstants.ORDER_IMPORT_DEFAULT_EXCEL_QUEUE).withArguments(args).build();
}
/**
* 生产单excel导入交换机声明
*
* @return
*/
@Bean
public Binding orderImportDefaultExcelBinding() {
return BindingBuilder.bind(orderImportDefaultExcelQueue()).to(orderImportDefaultExcelFanoutExchange());
}
/**
* 生产单xml导入交换机、队列、绑定声明
*
* @return
*/
@Bean
public FanoutExchange orderImportDefaultXmlFanoutExchange() {
return new FanoutExchange(RabbitMqConstants.ORDER_IMPORT_DEFAULT_XML_EXCHANGE);
}
/**
* 生产单xml导入队列声明
*
* @return
*/
@Bean
public Queue orderImportDefaultXmlQueue() {
Map<String, Object> args = new HashMap<>(1);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
args.put("x-dead-letter-routing-key", ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
return QueueBuilder.durable(RabbitMqConstants.ORDER_IMPORT_DEFAULT_XML_QUEUE).withArguments(args).build();
}
/**
* 生产单xml导入交换机声明
*
* @return
*/
@Bean
public Binding orderImportDefaultXmlBinding() {
return BindingBuilder.bind(orderImportDefaultXmlQueue()).to(orderImportDefaultXmlFanoutExchange());
}
/**
* 生产单导入死信交换机声明
*
* @return
*/
@Bean
public DirectExchange orderImportDeadLetterExchange() {
return new DirectExchange(ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
}
/**
* 生产单导入死信队列声明
*
* @return
*/
@Bean
public Queue orderImportDeadLetterQueue() {
return new Queue(ORDER_IMPORT_DEAD_LETTER_QUEUE);
}
/**
* 生产单导入死信队列、交换机、绑定声明
*
* @return
*/
@Bean
public Binding deadLetterBindingA() {
return BindingBuilder.bind(orderImportDeadLetterQueue()).to(orderImportDeadLetterExchange()).with(ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
}
/**
* 广告状态修改 延迟队列
*/
@Bean
public Queue advertisementUpdateDelayedQueue(){
return QueueBuilder.durable(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_QUEUE).build();
}
/**
* 广告状态修改 定义延迟交换机
*/
@Bean
public CustomExchange advertisementUpdateDelayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
/**
* 广告状态修改 队列与交换机绑定
*/
@Bean
public Binding advertisementUpdateQueueABindingX(@Qualifier("advertisementUpdateDelayedQueue") Queue queue,
@Qualifier("advertisementUpdateDelayExchange") CustomExchange delayedExchange){
return BindingBuilder.bind(queue)
.to(delayedExchange)
.with(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_ROUTING_KEY)
.noargs();
}
}
@@ -27,6 +27,17 @@ public class RabbitMqConstants {
*/
public static final String ORDER_IMPORT_DEFAULT_XML_QUEUE = "order_import_default_xml_queue";
/**
* webcad拆单交换机
*/
public static final String ORDER_IMPORT_WEBCAD_EXCHANGE = "order_import_webcad_exchange";
/**
* webcad拆单队列
*/
public static final String ORDER_IMPORT_WEBCAD_QUEUE = "order_import_webcad_queue_%s";
/**
* 生产单导入死信交换机
*/
@@ -29,6 +29,8 @@ public class WebFrameworkUtils {
public static final String HEADER_ORGAN_ID = "organ-id";
public static final String HEADER_PEEK = "peek";
/**
* 终端的 Header
*
@@ -119,7 +119,7 @@ public class DefaultXmlOrderImportHandler implements AbstractXmlOrderImportHandl
// 创建导入任务
OrderImportTaskDO orderImportTaskDO = OrderImportTaskDO.builder()
.id((Long) snowFlakeGenerator.nextId(null))
.type(OrderImportTypeEnum.EXCEL.getType())
.type(OrderImportTypeEnum.XML.getType())
.importStatus(OrderImportStatusEnum.IMPORT_NOT_YET.getStatus())
.status(OrderImportTaskStatusEnum.CREATE_NOT_CONSUME.getStatus())
.fileName(fileName)
@@ -18,7 +18,7 @@ public class ErrorCodeConstants {
public static final ErrorCode WEBCAD_ORDER_IMPORT_SAVEDATA_ERROR = new ErrorCode(1_005_000_008, "导入失败:数据保存异常,请检查拆单数据");
public static final ErrorCode WEBCAD_ORDER_IMPORT_GET_ORG_SEALEDGE_ERROR = new ErrorCode(1_005_000_009, "导入失败:获取组织封边对应配置异常,请检查服务");
public static final ErrorCode WEBCAD_ORDER_IMPORT_ORG_SEALEDGE_ANALYZE_ERROR = new ErrorCode(1_005_000_010, "导入失败:组织封边对应配置解析异常,请检查配置");
public static final ErrorCode WEBCAD_ORDER_IMPORT_ORDER_PLATENUM_CHECK_ERROR = new ErrorCode(1_005_000_011, "导入失败:生产单【{}】下板件数量已达到20000片,无法继续导入");
public static final ErrorCode WEBCAD_ORDER_IMPORT_ORDER_PLATENUM_REACH_THRESHOLD_ERROR = new ErrorCode(1_005_000_012, "部分导入成功,生产单【{}】下板件数量已达到20000片,本次拆单超出部分不计入生产单");
public static final ErrorCode WEBCAD_ORDER_IMPORT_ORDER_PLATENUM_CHECK_ERROR = new ErrorCode(1_005_000_011, "导入失败:生产单【{}】下板件数量已达到{}片,无法继续导入");
public static final ErrorCode WEBCAD_ORDER_IMPORT_ORDER_PLATENUM_REACH_THRESHOLD_ERROR = new ErrorCode(1_005_000_012, "部分导入成功,生产单【{}】下板件数量已达到{}片,本次拆单超出部分不计入生产单");
public static final ErrorCode ORDER_IMPORT_PLATE_GOODS_NOMATCH_ERROR = new ErrorCode(1_005_000_013, "导入失败,板材库中无板材匹配,商品编码:{},商品名称:{},材质:{},颜色:{},厚度:{},品牌:{}");
}
@@ -0,0 +1,173 @@
package com.cf.imes.module.plan.config;
import com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.ORDER_IMPORT_DEAD_LETTER_EXCHANGE;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.ORDER_IMPORT_DEAD_LETTER_QUEUE;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.ORDER_IMPORT_WEBCAD_QUEUE;
/**
* @author Gqr
* @since 2025/5/12 12:00
*/
@Configuration
public class ChenfengRabbitMQPlanConfiguration {
@Value("${chenfeng.plan.webcad.import.node:1}")
private String instanceId;
private static final String X_DEAD_LETTER_EXCHANGE_KEY = "x-dead-letter-exchange";
private static final String X_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
/**
* webcad拆单导入交换机、队列、绑定声明
*
* @return
*/
@Bean
public DirectExchange orderImportWebCadFanoutExchange() {
return new DirectExchange(RabbitMqConstants.ORDER_IMPORT_WEBCAD_EXCHANGE);
}
@Bean("orderImportWebCadQueueName")
public String dynamicOrderImportWebCadQueueName() {
return String.format(ORDER_IMPORT_WEBCAD_QUEUE, instanceId);
}
/**
* webcad拆单导入队列声明
*
* @return
*/
@Bean
public Queue orderImportWebCadQueue() {
Map<String, Object> args = new HashMap<>(1);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put(X_DEAD_LETTER_EXCHANGE_KEY, ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
args.put(X_DEAD_LETTER_ROUTING_KEY, ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
return QueueBuilder.durable(dynamicOrderImportWebCadQueueName()).withArguments(args).build();
}
/**
* 生产单xml导入交换机声明
*
* @return
*/
@Bean
public Binding orderImportWebCadBinding() {
return BindingBuilder.bind(orderImportWebCadQueue()).to(orderImportWebCadFanoutExchange()).with(instanceId);
}
/**
* 生产单excel导入交换机、队列、绑定声明
*
* @return
*/
@Bean
public FanoutExchange orderImportDefaultExcelFanoutExchange() {
return new FanoutExchange(RabbitMqConstants.ORDER_IMPORT_DEFAULT_EXCEL_EXCHANGE);
}
/**
* 生产单excel导入队列声明
*
* @return
*/
@Bean
public Queue orderImportDefaultExcelQueue() {
Map<String, Object> args = new HashMap<>(1);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put(X_DEAD_LETTER_EXCHANGE_KEY, ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
args.put(X_DEAD_LETTER_ROUTING_KEY, ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
return QueueBuilder.durable(RabbitMqConstants.ORDER_IMPORT_DEFAULT_EXCEL_QUEUE).withArguments(args).build();
}
/**
* 生产单excel导入交换机声明
*
* @return
*/
@Bean
public Binding orderImportDefaultExcelBinding() {
return BindingBuilder.bind(orderImportDefaultExcelQueue()).to(orderImportDefaultExcelFanoutExchange());
}
/**
* 生产单xml导入交换机、队列、绑定声明
*
* @return
*/
@Bean
public FanoutExchange orderImportDefaultXmlFanoutExchange() {
return new FanoutExchange(RabbitMqConstants.ORDER_IMPORT_DEFAULT_XML_EXCHANGE);
}
/**
* 生产单xml导入队列声明
*
* @return
*/
@Bean
public Queue orderImportDefaultXmlQueue() {
Map<String, Object> args = new HashMap<>(1);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put(X_DEAD_LETTER_EXCHANGE_KEY, ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
args.put(X_DEAD_LETTER_ROUTING_KEY, ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
return QueueBuilder.durable(RabbitMqConstants.ORDER_IMPORT_DEFAULT_XML_QUEUE).withArguments(args).build();
}
/**
* 生产单xml导入交换机声明
*
* @return
*/
@Bean
public Binding orderImportDefaultXmlBinding() {
return BindingBuilder.bind(orderImportDefaultXmlQueue()).to(orderImportDefaultXmlFanoutExchange());
}
/**
* 生产单导入死信交换机声明
*
* @return
*/
@Bean
public DirectExchange orderImportDeadLetterExchange() {
return new DirectExchange(ORDER_IMPORT_DEAD_LETTER_EXCHANGE);
}
/**
* 生产单导入死信队列声明
*
* @return
*/
@Bean
public Queue orderImportDeadLetterQueue() {
return new Queue(ORDER_IMPORT_DEAD_LETTER_QUEUE);
}
/**
* 生产单导入死信队列、交换机、绑定声明
*
* @return
*/
@Bean
public Binding deadLetterBindingA() {
return BindingBuilder.bind(orderImportDeadLetterQueue()).to(orderImportDeadLetterExchange()).with(ORDER_IMPORT_DEAD_LETTER_ROUTING_KEY);
}
}
@@ -2,19 +2,18 @@ package com.cf.imes.module.plan.controller.admin.orderImport;
import com.cf.imes.framework.common.pojo.CommonResult;
import com.cf.imes.framework.operatelog.core.annotations.OperateLog;
import com.cf.imes.module.plan.controller.admin.orderImport.webcad.vo.WebCadDataReqVO;
import com.cf.imes.module.plan.service.orderImport.WebCadOrderImportService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.validation.Valid;
import static com.cf.imes.framework.operatelog.core.enums.OperateTypeEnum.IMPORT;
@@ -34,8 +33,8 @@ public class OrderImportController {
@PostMapping("/webcad/import")
@Operation(summary = "webcad拆单导入生产单")
@OperateLog(type = IMPORT)
public CommonResult<Boolean> webCadOrderImport(@RequestBody @Valid WebCadDataReqVO webCadDataReqVO) {
webCadOrderImportService.webCadOrderImport(webCadDataReqVO);
public CommonResult<Boolean> webCadOrderImport(@RequestParam("file") MultipartFile file) {
webCadOrderImportService.webCadOrderImport(file);
return CommonResult.success(true);
}
}
@@ -63,59 +63,33 @@ public class WebCadDataBlockReqVO {
private Double kaiLiaoWidth;
@Valid
private InfoDTO info;
// private InfoDTO info;
private String cabinetName;
private String roomName;
private Double cabinetDepth;
private Double cabinetWidth;
private Double cabinetHeight;
private BoadrData boardData;
private PointInfoDTO pointInfo;
private Long plateGoodsId;
private String goodsId;
private Double sealLeft;
private Double sealRight;
private Double sealUp;
private Double sealDown;
private Boolean isRect;
private String spliteHeight;
private String spliteThickness;
private String spliteWidth;
private boolean has2DModel;
private boolean has3DModel;
private String boardTypeName;
private List<List<String>> allRemarkList;
// private Long plateGoodsId;
private String goodsCode;
private List<Integer> groupIds = new ArrayList<>();
@NoArgsConstructor
@Data
public static class GroupInfo {
private String groupTypeName;
private String groupName;
private Double width;
private Double height;
private Double depth;
}
@NoArgsConstructor
@Data
public static class InfoDTO {
private Integer id;
private String name;
private String roomName;
private String cabinetName;
private String material;
private String boardName;
private String color;
private Integer lines;
private String drillType;
private Integer thickness;
private String spliteHeight;
private String spliteThickness;
private String spliteWidth;
private Boolean isRect;
private List<List<String>> remarks;
@Max(value = 99999, message = "板件开料宽度不能超过99999")
@Min(value = 0, message = "板件开料宽度最小值为0")
private Double kaiLiaoWidth;
@Max(value = 99999, message = "板件开料高度不能超过99999")
@Min(value = 0, message = "板件开料高度最小值为0")
private Double kaiLiaoHeight;
private Integer openDir;
}
@NoArgsConstructor
@Data
public static class FrontBackHolesDTO {
@@ -166,295 +140,6 @@ public class WebCadDataBlockReqVO {
}
}
@NoArgsConstructor
@Data
public static class BoadrData {
@JsonProperty("originOutlin")
private OriginOutlinDTO originOutlin;
@JsonProperty("outline")
private OutlineDTO outline;
@JsonProperty("sealing")
private List<SealingDTO> sealing;
@JsonProperty("boardEdgeRemark")
private List<BoardEdgeRemarkDTO> boardEdgeRemark;
@JsonProperty("reservedEdge")
private List<ReservedEdgeDTO> reservedEdge;
@JsonProperty("modeling")
private List<ModelingDTO> modeling;
@JsonProperty("curveBoardModeling")
private List<?> curveBoardModeling;
@JsonProperty("holes")
private HolesDTO holes;
@JsonProperty("sideModeling")
private List<SideModelingDTO> sideModeling;
@JsonProperty("offsetTanslation")
private OffsetTanslationDTO offsetTanslation;
@JsonProperty("metalsData")
private MetalsDataDTO metalsData;
@JsonProperty("modeling2D")
private List<Modeling2DDTO> modeling2D;
@JsonProperty("modeling3D")
private List<?> modeling3D;
@JsonProperty("isReverse")
private Boolean isReverse;
private Object info;
@NoArgsConstructor
@Data
public static class OriginOutlinDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Integer y;
}
}
@NoArgsConstructor
@Data
public static class OutlineDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Integer y;
}
}
@NoArgsConstructor
@Data
public static class HolesDTO {
@JsonProperty("frontBackHoles")
private List<?> frontBackHoles;
@JsonProperty("sideHoles")
private List<?> sideHoles;
}
@NoArgsConstructor
@Data
public static class OffsetTanslationDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Double y;
@JsonProperty("z")
private Integer z;
}
@NoArgsConstructor
@Data
public static class MetalsDataDTO {
@JsonProperty("metals")
private Integer metals;
@JsonProperty("comp")
private Integer comp;
}
@NoArgsConstructor
@Data
public static class SealingDTO {
@JsonProperty("length")
private Double length;
@JsonProperty("size")
private Integer size;
@JsonProperty("sealColor")
private String sealColor;
}
@NoArgsConstructor
@Data
public static class BoardEdgeRemarkDTO {
@JsonProperty("description")
private String description;
}
@NoArgsConstructor
@Data
public static class ReservedEdgeDTO {
@JsonProperty("size")
private Integer size;
}
@NoArgsConstructor
@Data
public static class ModelingDTO {
@JsonProperty("feeding")
private List<FeedingDTO> feeding;
@JsonProperty("thickness")
private Integer thickness;
@JsonProperty("dir")
private Integer dir;
@JsonProperty("knifeRadius")
private Integer knifeRadius;
@JsonProperty("origin")
private OriginDTO origin;
@NoArgsConstructor
@Data
public static class OriginDTO {
@JsonProperty("outline")
private OutlineDTO outline;
@JsonProperty("holes")
private List<?> holes;
@JsonProperty("addLen")
private Integer addLen;
@JsonProperty("addWidth")
private Integer addWidth;
@JsonProperty("addDepth")
private Integer addDepth;
@NoArgsConstructor
@Data
public static class OutlineDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Double y;
}
}
}
@NoArgsConstructor
@Data
public static class FeedingDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Double y;
}
}
}
@NoArgsConstructor
@Data
public static class SideModelingDTO {
@JsonProperty("thickness")
private Integer thickness;
@JsonProperty("dir")
private Integer dir;
@JsonProperty("knifeRadius")
private Integer knifeRadius;
@JsonProperty("outline")
private OutlineDTO outline;
@JsonProperty("holes")
private List<?> holes;
@JsonProperty("addLen")
private Integer addLen;
@JsonProperty("addWidth")
private Integer addWidth;
@JsonProperty("addDepth")
private Integer addDepth;
@JsonProperty("chaiDanName")
private String chaiDanName;
@JsonProperty("modelType")
private Integer modelType;
@NoArgsConstructor
@Data
public static class OutlineDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Double y;
}
}
}
@NoArgsConstructor
@Data
public static class Modeling2DDTO {
@JsonProperty("path")
private PathDTO path;
@JsonProperty("dir")
private Integer dir;
@JsonProperty("items")
private List<ItemsDTO> items;
@NoArgsConstructor
@Data
public static class PathDTO {
@JsonProperty("pts")
private List<PtsDTO> pts;
@JsonProperty("buls")
private List<Integer> buls;
@NoArgsConstructor
@Data
public static class PtsDTO {
@JsonProperty("x")
private Double x;
@JsonProperty("y")
private Double y;
}
}
@NoArgsConstructor
@Data
public static class ItemsDTO {
@JsonProperty("depth")
private Integer depth;
@JsonProperty("offset")
private Integer offset;
@JsonProperty("knife")
private KnifeDTO knife;
@JsonProperty("depthExpr")
private String depthExpr;
@NoArgsConstructor
@Data
public static class KnifeDTO {
@JsonProperty("id")
private String id;
@JsonProperty("radius")
private Integer radius;
@JsonProperty("angle")
private Double angle;
@JsonProperty("name")
private String name;
}
}
}
}
@NoArgsConstructor
@Data
@@ -7,6 +7,7 @@ import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
/**
@@ -35,5 +36,9 @@ public class WebCadDataDoubleRoomTreeReqVO {
@Max(value = 5000, message = "拆单倍数不能超过5000")
@Min(value = 1, message = "拆单倍数最小值为1")
private Integer count;
private List<WebCadDataBlockReqVO> blockList;
private List<WebCadDataPartsReqVO.BlockObjectListDTO> objList;
}
}
@@ -0,0 +1,18 @@
package com.cf.imes.module.plan.controller.admin.orderImport.webcad.vo;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author Gqr
* @since 2025/5/6 15:27
*/
@NoArgsConstructor
@Data
public class WebCadDataGroupInfoReqVO {
private String groupTypeName;
private String groupName;
private Double width;
private Double height;
private Double depth;
}
@@ -13,7 +13,11 @@ import java.util.List;
@Data
public class WebCadDataMaterialReqVO {
private Integer thickness;
private Double height;
private Double width;
private Double thickness;
private String material;
@@ -27,6 +31,14 @@ public class WebCadDataMaterialReqVO {
private String name;
private boolean texture;
public boolean getTexture() {
return texture;
}
private String spec;
/**
* imes板材id
*/
@@ -22,8 +22,6 @@ public class WebCadDataPartsReqVO {
private List<BlockObjectListDTO> blockObjectList;
private List<LeftEdgesDTO> leftEdges;
@NoArgsConstructor
@Data
public static class BlockObjectListDTO {
@@ -74,16 +72,6 @@ public class WebCadDataPartsReqVO {
private GroupDataDTO groupData;
@NoArgsConstructor
@Data
public static class GroupInfo {
private String groupTypeName;
private String groupName;
private Double width;
private Double height;
private Double depth;
}
@NoArgsConstructor
@Data
public static class GroupDataDTO {
@@ -99,37 +87,4 @@ public class WebCadDataPartsReqVO {
private String color;
}
}
@NoArgsConstructor
@Data
public static class LeftEdgesDTO {
@JsonProperty("thickness")
private Double thickness;
@JsonProperty("width")
private Double width;
@JsonProperty("height")
private Double height;
@JsonProperty("color")
private String color;
@JsonProperty("material")
private String material;
@JsonProperty("name")
private String name;
@JsonProperty("goodsId")
private String goodsId;
@JsonProperty("id")
private String id;
@JsonProperty("objectIds")
private List<Integer> objectIds;
@JsonProperty("cabinetNames")
private List<String> cabinetNames;
@JsonProperty("roomNames")
private List<String> roomNames;
@JsonProperty("roomName")
private String roomName;
@JsonProperty("cabinetName")
private String cabinetName;
@JsonProperty("num")
private Double num;
}
}
@@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.List;
/**
@@ -16,20 +15,14 @@ import java.util.List;
@Data
public class WebCadDataReqVO {
@JsonProperty(value = "Orders")
@Valid
private WebCadDataOrderReqVO orders;
@JsonProperty(value = "OrderNo")
private String orderNo;
@JsonProperty(value = "Materials")
private List<WebCadDataMaterialReqVO> Materials;
@JsonProperty(value = "Blocks")
@Size(max = 20000, message = "拆单板件片数单次不能超过2万片")
@Valid
private List<WebCadDataBlockReqVO> Blocks;
@JsonProperty(value = "Parts")
private WebCadDataPartsReqVO Parts;
private List<WebCadDataDoubleRoomTreeReqVO> Parts;
@JsonProperty(value = "ProcessGroup")
@Valid
@@ -37,4 +30,7 @@ public class WebCadDataReqVO {
@Valid
private List<WebCadDataDoubleRoomTreeReqVO> douleRoomTree;
@JsonProperty(value = "Blocks")
private List<WebCadDataDoubleRoomTreeReqVO> blocks;
}
@@ -64,4 +64,10 @@ public class OrderImportTaskDO extends BaseDO {
* 导入结果0
*/
private String result;
/**
* 导入文件名称
*/
private String fileName;
}
@@ -17,8 +17,8 @@ public class PlanThreadPoolConfiguration {
@Bean(PLAN_IMPOT_THREAD_POOL_TASK_EXECUTOR)
public ThreadPoolTaskExecutor planThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8); // 设置核心线程数
executor.setMaxPoolSize(16); // 设置最大线程数
executor.setCorePoolSize(3); // 设置核心线程数
executor.setMaxPoolSize(6); // 设置最大线程数
executor.setKeepAliveSeconds(60); // 设置空闲时间
executor.setQueueCapacity(100); // 设置队列大小
executor.setThreadNamePrefix("plan-import-Executor-"); // 配置线程池的前缀
@@ -3,6 +3,7 @@ package com.cf.imes.module.plan.service.order;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.ElasticsearchException;
import co.elastic.clients.elasticsearch.core.BulkRequest;
@@ -35,6 +36,9 @@ public class OrderInputProcessor {
@Resource
private ElasticsearchClient elasticsearchClient;
@Resource
private ElasticsearchAsyncClient elasticsearchAsyncClient;
private Snowflake snowflake = IdUtil.getSnowflake();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -61,7 +65,21 @@ public class OrderInputProcessor {
throw new ServiceException(500, "未知异常");
}
} catch (IOException | ElasticsearchException e) {
e.printStackTrace();
log.error(e.getMessage());
throw new RuntimeException(e);
}
}
/**
* 异步保存生产单造型数据
*
* @param orderModelDOs
*/
public void batchAsyncSaveModel(List<OrderModelDO> orderModelDOs) {
try {
bulkAsyncCreate(ORDER_PLATE_MODEL, orderModelDOs);
} catch (IOException | ElasticsearchException e) {
log.error(e.getMessage());
throw new RuntimeException(e);
}
@@ -160,4 +178,22 @@ public class OrderInputProcessor {
});
return elasticsearchClient.bulk(br.build());
}
public void bulkAsyncCreate(String idxName, List<? extends ESDocument> documents) throws IOException, ElasticsearchException {
BulkRequest.Builder br = new BulkRequest.Builder();
Date date = new Date();
documents.forEach(esDocument -> {
if (CharSequenceUtil.isBlank(esDocument.getId())) {
esDocument.setId(snowflake.nextIdStr());
}
esDocument.setCreateTime(simpleDateFormat.format(date));
esDocument.setUpdateTime(simpleDateFormat.format(date));
br.operations(op -> op.index(idx -> idx
.index(idxName)
.id(esDocument.getId())
.document(esDocument)));
});
elasticsearchAsyncClient.bulk(br.build());
}
}
@@ -0,0 +1,24 @@
package com.cf.imes.module.plan.service.orderImport;
/**
* @author Gqr
* @since 2025/5/13 16:41
*/
public interface OrderImportTaskService {
/**
* 更新业务异常状态
*
* @param taskId
* @param organId
* @param serviceExceptionMessage
*/
void updateServiceExceptionTaskStatus(Long taskId, Long organId, String serviceExceptionMessage);
/**
* 更新异常状态
*
* @param taskId
* @param organId
*/
void updateExceptionTaskStatus(Long taskId, Long organId);
}
@@ -0,0 +1,44 @@
package com.cf.imes.module.plan.service.orderImport;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.module.executor.enums.OrderImportStatusEnum;
import com.cf.imes.module.executor.enums.OrderImportTaskStatusEnum;
import com.cf.imes.module.plan.dal.dataobject.orderImport.OrderImportTaskDO;
import com.cf.imes.module.plan.dal.mysql.orderImport.OrderImportTaskMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.ORDER_IMPORT_FAIL;
/**
* @author Gqr
* @since 2025/5/13 16:41
*/
@Service
public class OrderImportTaskServiceImpl implements OrderImportTaskService {
@Resource
private OrderImportTaskMapper orderImportTaskMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void updateServiceExceptionTaskStatus(Long taskId, Long organId, String serviceExceptionMessage) {
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>().eq(OrderImportTaskDO::getId, taskId)
.set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_SUCCESS.getStatus())
.set(OrderImportTaskDO::getImportStatus, OrderImportStatusEnum.IMPORT_FAIL.getStatus())
.set(OrderImportTaskDO::getOrganId, organId)
.set(OrderImportTaskDO::getResult, serviceExceptionMessage));
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void updateExceptionTaskStatus(Long taskId, Long organId) {
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>().eq(OrderImportTaskDO::getId, taskId)
.set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_SUCCESS.getStatus())
.set(OrderImportTaskDO::getImportStatus, OrderImportStatusEnum.IMPORT_FAIL.getStatus())
.set(OrderImportTaskDO::getOrganId, organId)
.set(OrderImportTaskDO::getResult, ORDER_IMPORT_FAIL.getMsg()));
}
}
@@ -1,6 +1,6 @@
package com.cf.imes.module.plan.service.orderImport;
import com.cf.imes.module.plan.controller.admin.orderImport.webcad.vo.WebCadDataReqVO;
import org.springframework.web.multipart.MultipartFile;
/**
* 生产单导入服务Service
@@ -13,7 +13,7 @@ public interface WebCadOrderImportService {
/**
* webcad生产单导入
*
* @param webCadDataReqVO
* @param file
*/
void webCadOrderImport(WebCadDataReqVO webCadDataReqVO);
void webCadOrderImport(MultipartFile file);
}
@@ -1,42 +1,63 @@
package com.cf.imes.module.plan.service.orderImport;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.nacos.common.utils.StringUtils;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.cf.imes.framework.common.exception.ErrorCode;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
import com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants;
import com.cf.imes.framework.mybatis.core.generator.SnowFlakeGenerator;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.redis.config.ChenfengCacheProperties;
import com.cf.imes.framework.redis.constants.RedisKeyConstants;
import com.cf.imes.framework.redis.util.RedisLockUtil;
import com.cf.imes.module.executor.enums.OrderImportStatusEnum;
import com.cf.imes.module.executor.enums.OrderImportTaskStatusEnum;
import com.cf.imes.module.executor.enums.OrderImportTypeEnum;
import com.cf.imes.module.plan.controller.admin.orderImport.webcad.vo.WebCadDataReqVO;
import com.cf.imes.module.plan.dal.dataobject.orderImport.OrderImportTaskDO;
import com.cf.imes.module.plan.dal.mysql.goods.GoodsMapper;
import com.cf.imes.module.plan.dal.mysql.order.OrderMapper;
import com.cf.imes.module.plan.dal.mysql.orderBody.OrderBodyMapper;
import com.cf.imes.module.plan.dal.mysql.orderGroup.OrderGroupMapper;
import com.cf.imes.module.plan.dal.mysql.orderItem.OrderItemMapper;
import com.cf.imes.module.plan.dal.mysql.orderParts.OrderPartsMapper;
import com.cf.imes.module.plan.dal.mysql.plate.PlateGoodMapper;
import com.cf.imes.module.plan.dal.mysql.orderImport.OrderImportTaskMapper;
import com.cf.imes.module.plan.dal.mysql.plate.PlateMapper;
import com.cf.imes.module.plan.dal.mysql.rawgoods.RawGoodsMapper;
import com.cf.imes.module.plan.service.customplateno.CustomPlateNoGenerateService;
import com.cf.imes.module.plan.service.order.OrderInputProcessor;
import com.cf.imes.module.plan.service.orderImport.factory.WebCadOrderImportFactory;
import com.cf.imes.module.system.api.systemconfig.SystemConfigApi;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import static com.cf.imes.module.executor.enums.ErrorCodeConstants.ORDER_IMPORT_ORGAN_LOCK_ERROR;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.WEBCAD_ORDER_IMPORT_FAILED;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.WEBCAD_ORDER_IMPORT_ORGANID_EMPTY_ERROR;
import static com.cf.imes.module.plan.framework.executor.config.PlanThreadPoolConfiguration.PLAN_IMPOT_THREAD_POOL_TASK_EXECUTOR;
/**
* 生产单导入服务Service实现类
@@ -60,21 +81,12 @@ public class WebCadOrderImportServiceImpl implements WebCadOrderImportService {
@Resource
private PlateMapper plateMapper;
@Resource
private OrderItemMapper orderItemMapper;
@Resource
private OrderGroupMapper orderGroupMapper;
@Resource
private SnowflakeIdWorker3rd idWorker;
@Resource
private OrderInputProcessor orderInputProcessor;
@Resource
private OrderPartsMapper orderPartsMapper;
@Resource
private CustomPlateNoGenerateService customPlateNoGenerateService;
@@ -90,50 +102,219 @@ public class WebCadOrderImportServiceImpl implements WebCadOrderImportService {
@Resource
private RawGoodsMapper rawGoodsMapper;
@Resource
private PlateGoodMapper plateGoodMapper;
@Resource
private DataSourceTransactionManager transactionManager;
@Resource
private SystemConfigApi systemConfigApi;
@Resource(name = PLAN_IMPOT_THREAD_POOL_TASK_EXECUTOR)
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Value("${chenfeng.plan.impot.cadImportPlateNumThreshold:20000}")
@Value("${chenfeng.plan.webcad.import.cadImportPlateNumThreshold:10000}")
private int cadImportPlateNumThreshold;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private OrderImportTaskMapper orderImportTaskMapper;
@Resource
private RabbitTemplate rabbitTemplate;
/**
* cad拆单请求缓存地址
*/
@Value("${chenfeng.plan.webcad.import.temp-file-path}")
private String webcadJsonCachePath;
/**
* cad拆单是否使用异步方式的板件阈值
*/
@Value("${chenfeng.plan.webcad.import.async-plate-threshold:5000}")
private int webcadAsyncPlateThreshold;
@Value("${chenfeng.plan.webcad.import.node:1}")
private String instanceId;
private static final String REDIS_UNIQUEKEY = "webcad";
@Override
public void webCadOrderImport(WebCadDataReqVO webCadDataReqVO) {
public void webCadOrderImport(MultipartFile file) {
Long organId = OrganContextHolder.getOrganId();
String redisUniqueKey = "webcad";
if (ObjectUtil.isNull(organId)) {
throw new ServiceException(WEBCAD_ORDER_IMPORT_ORGANID_EMPTY_ERROR);
}
try {
String importLockKey = String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId);
// 检查机构导入锁
boolean lockResult = redisLockUtil.lock(importLockKey, redisUniqueKey, chenfengCacheProperties.getLockTimeout());
if (!lockResult) {
throw new ServiceException(ORDER_IMPORT_ORGAN_LOCK_ERROR);
// 上锁
addLock(organId);
TransactionStatus transactionStatus = transactionManager.getTransaction(TransactionDefinition.withDefaults());
// 是否同步处理
boolean sync = true;
Long taskId = null;
JsonFactory factory = new JsonFactory();
try (GZIPInputStream gzipInputStream = new GZIPInputStream(file.getInputStream());
JsonParser parser = factory.createParser(gzipInputStream)) {
factory.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
int plateCount = 0;
String fileName = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
String fieldName = parser.getCurrentName();
if (StringUtils.isEmpty(fieldName)) {
continue;
}
parser.nextToken(); // 移动到字段值位置
if ("plateCount".equals(fieldName)) {
plateCount = parser.getIntValue();
} else if ("fileName".equals(fieldName)) {
fileName = parser.getText();
} else if ("blockData".equals(fieldName)) {
if (plateCount < webcadAsyncPlateThreshold) {
// 在异步阈值范围内,直接解析整个对象
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
WebCadDataReqVO webCadDataReqVO = mapper.readValue(parser, WebCadDataReqVO.class);
WebCadOrderImportFactory webCadOrderImportFactory = new WebCadOrderImportFactory(organId, orderMapper, snowFlakeGenerator, orderBodyMapper, plateMapper,
idWorker, orderInputProcessor, goodsMapper, rawGoodsMapper, customPlateNoGenerateService, systemConfigApi, cadImportPlateNumThreshold, jdbcTemplate);
webCadOrderImportFactory.analyzeTempData(webCadDataReqVO);
} else {
sync = false;
// plateCount 大于阈值时将 blockData 写入文件
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 读取结构
JsonNode node = mapper.readTree(parser);
// 缓存请求,创建导入任务
taskId = initiateTask(node, organId, fileName);
// 发送异步消息
sendMessage(taskId, organId);
// 跳过当前 blockData 解析步骤
parser.skipChildren(); // 忽略 blockData
}
} else {
parser.skipChildren(); // 忽略其他字段
}
}
WebCadOrderImportFactory webCadOrderImportFactory = new WebCadOrderImportFactory(organId, orderMapper, snowFlakeGenerator, orderBodyMapper, plateMapper, orderItemMapper, orderGroupMapper,
idWorker, orderInputProcessor, orderPartsMapper, goodsMapper, rawGoodsMapper, plateGoodMapper, customPlateNoGenerateService, transactionManager, systemConfigApi, threadPoolTaskExecutor, cadImportPlateNumThreshold, jdbcTemplate);
webCadOrderImportFactory.analyzeTempData(webCadDataReqVO);
// 事务提交
transactionManager.commit(transactionStatus);
} catch (ServiceException se) {
throw se;
// 事务回滚
transactionManager.rollback(transactionStatus);
// 移除临时文件
removeCacheFileWhenException(sync, taskId);
throw se;
} catch (Exception e) {
// 事务回滚
transactionManager.rollback(transactionStatus);
// 移除临时文件
removeCacheFileWhenException(sync, taskId);
ErrorCode webcadOrderImportFailed = WEBCAD_ORDER_IMPORT_FAILED;
log.error(webcadOrderImportFailed.getMsg(), e);
throw new ServiceException(webcadOrderImportFailed);
} finally {
// 导入锁解锁
redisLockUtil.unlock(String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId), redisUniqueKey);
if (sync) {
// 导入锁解锁
redisLockUtil.unlock(String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId), REDIS_UNIQUEKEY);
}
}
}
/**
* 缓存请求,发起异步
*
* @param node
* @param organId
* @return
* @throws IOException
*/
private Long initiateTask(JsonNode node, Long organId, String fileName) throws IOException {
// 创建任务
Long taskId = createImportTask(organId, fileName);
// 缓存请求到本地磁盘
ObjectMapper objectMapper = new ObjectMapper();
File file = new File(webcadJsonCachePath + File.separator + taskId + ".json");
objectMapper.writeValue(file, node);
return taskId;
}
/**
* 发送异步消息
*
* @param taskId
*/
private void sendMessage(Long taskId, Long organId) {
Message message = MessageBuilder
.withBody(taskId.toString().getBytes())
.setHeader("peek", DynamicDataSourceContextHolder.peek())
.setExpiration("60000") // 设置消息级 TTL60 秒)
.build();
//如果处于事务中,待事务提交成功了再执行后续操作,防止事务未提交完成导致消费端查不到数据
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
// 数据库操作全部完成发送消息
rabbitTemplate.convertAndSend(RabbitMqConstants.ORDER_IMPORT_WEBCAD_EXCHANGE, instanceId, message);
}
});
} else {
// 数据库操作全部完成发送消息
rabbitTemplate.convertAndSend(RabbitMqConstants.ORDER_IMPORT_WEBCAD_EXCHANGE, instanceId, message);
}
}
/**
* 加导入锁
*
* @param organId
*/
private void addLock(Long organId) {
String importLockKey = String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId);
// 检查机构导入锁
boolean lockResult = redisLockUtil.lock(importLockKey, REDIS_UNIQUEKEY, chenfengCacheProperties.getLockTimeout());
if (!lockResult) {
throw new ServiceException(ORDER_IMPORT_ORGAN_LOCK_ERROR);
}
}
/**
* 异常时移除临时文件
*/
private void removeCacheFileWhenException(boolean sync, Long taskId) {
if (!sync && ObjectUtil.isNotNull(taskId)) {
FileUtil.del(webcadJsonCachePath + File.separator + taskId + ".json");
}
}
/**
* 创建导入任务
*
* @return taskId
*/
private Long createImportTask(Long organId, String fileName) {
Long taskId = (Long) snowFlakeGenerator.nextId(null);
// 创建导入任务
OrderImportTaskDO orderImportTaskDO = OrderImportTaskDO.builder()
.id(taskId)
.type(OrderImportTypeEnum.API.getType())
.importStatus(OrderImportStatusEnum.IMPORT_NOT_YET.getStatus())
.status(OrderImportTaskStatusEnum.CREATE_NOT_CONSUME.getStatus())
.fileName(fileName)
.organId(organId)
.build();
orderImportTaskMapper.insert(orderImportTaskDO);
return taskId;
}
}
@@ -1,7 +1,6 @@
package com.cf.imes.module.plan.service.orderImport.consumer;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.redis.constants.RedisKeyConstants;
@@ -23,7 +22,6 @@ import java.util.List;
import java.util.Map;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.ORDER_IMPORT_DEAD_LETTER_QUEUE;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.ORDER_IMPORT_FAIL;
/**
* 生产单导入死信队列消费
@@ -45,7 +43,6 @@ public class OrderImportDeadLetterConsumer {
private static final String ERROR_REASON = "rejected";
@RabbitListener(queues = ORDER_IMPORT_DEAD_LETTER_QUEUE)
@DS("imes_prod")
public void receive(String message,
Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@@ -54,24 +51,28 @@ public class OrderImportDeadLetterConsumer {
// 手动确认消息
channel.basicAck(deliveryTag, false);
Long organId = OrganContextHolder.getOrganId();
String deadLetterReason = getDeadLetterReason(xDeath);
long taskId = Long.parseLong(message);
if(TTL_REASON.equals(deadLetterReason)) {
// 更新状态为消费超时
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>().eq(OrderImportTaskDO::getId, taskId)
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>()
.eq(OrderImportTaskDO::getId, taskId)
.set(OrderImportTaskDO::getOrganId, organId)
.set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_FAIL.getStatus())
);
} else if (ERROR_REASON.equals(deadLetterReason)) {
// 更新状态为消费成功、导入失败
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>().eq(OrderImportTaskDO::getId, taskId)
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>()
.eq(OrderImportTaskDO::getId, taskId)
.set(OrderImportTaskDO::getOrganId, organId)
.set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_SUCCESS.getStatus())
.set(OrderImportTaskDO::getImportStatus, OrderImportStatusEnum.IMPORT_FAIL.getStatus())
.set(OrderImportTaskDO::getResult, ORDER_IMPORT_FAIL.getMsg()));
.set(OrderImportTaskDO::getImportStatus, OrderImportStatusEnum.IMPORT_FAIL.getStatus()));
}
// 导入锁解锁
Long organId = OrganContextHolder.getOrganId();
if (ObjectUtil.isNotNull(organId)) {
redisLockUtil.unlock(String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId), message);
}
@@ -0,0 +1,216 @@
package com.cf.imes.module.plan.service.orderImport.consumer;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.cf.imes.framework.common.exception.ServiceException;
import com.cf.imes.framework.common.exception.util.ServiceExceptionUtil;
import com.cf.imes.framework.id.core.util.SnowflakeIdWorker3rd;
import com.cf.imes.framework.mybatis.core.generator.SnowFlakeGenerator;
import com.cf.imes.framework.organ.core.context.OrganContextHolder;
import com.cf.imes.framework.redis.constants.RedisKeyConstants;
import com.cf.imes.framework.redis.util.RedisLockUtil;
import com.cf.imes.module.executor.enums.OrderImportStatusEnum;
import com.cf.imes.module.executor.enums.OrderImportTaskStatusEnum;
import com.cf.imes.module.plan.dal.dataobject.orderImport.OrderImportTaskDO;
import com.cf.imes.module.plan.dal.mysql.goods.GoodsMapper;
import com.cf.imes.module.plan.dal.mysql.order.OrderMapper;
import com.cf.imes.module.plan.dal.mysql.orderBody.OrderBodyMapper;
import com.cf.imes.module.plan.dal.mysql.orderGroup.OrderGroupMapper;
import com.cf.imes.module.plan.dal.mysql.orderImport.OrderImportTaskMapper;
import com.cf.imes.module.plan.dal.mysql.plate.PlateMapper;
import com.cf.imes.module.plan.dal.mysql.rawgoods.RawGoodsMapper;
import com.cf.imes.module.plan.service.customplateno.CustomPlateNoGenerateService;
import com.cf.imes.module.plan.service.order.OrderInputProcessor;
import com.cf.imes.module.plan.service.orderImport.OrderImportTaskService;
import com.cf.imes.module.plan.service.orderImport.factory.WebCadOrderImportAsyncFactory;
import com.cf.imes.module.system.api.systemconfig.SystemConfigApi;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import javax.annotation.Resource;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.ORDER_IMPORT_FAIL;
import static com.cf.imes.module.plan.enums.ErrorCodeConstants.ORDER_IMPORT_ORGANID_NOT_EXISTS;
/**
* webcad拆单消费者
*
* @author Gqr
* @since 2025/4/21 13:46
*/
@Component
@Slf4j
public class WebCadOrderImportConsumer {
@Resource
private OrderMapper orderMapper;
@Resource
private SnowFlakeGenerator snowFlakeGenerator;
@Resource
private OrderBodyMapper orderBodyMapper;
@Resource
private PlateMapper plateMapper;
@Resource
private OrderGroupMapper orderGroupMapper;
@Resource
private SnowflakeIdWorker3rd idWorker;
@Resource
private OrderInputProcessor orderInputProcessor;
@Resource
private CustomPlateNoGenerateService customPlateNoGenerateService;
@Resource
private RedisLockUtil redisLockUtil;
@Resource
private GoodsMapper goodsMapper;
@Resource
private RawGoodsMapper rawGoodsMapper;
@Resource
private SystemConfigApi systemConfigApi;
@Value("${chenfeng.plan.webcad.import.cadImportPlateNumThreshold:10000}")
private int cadImportPlateNumThreshold;
@Resource
private JdbcTemplate jdbcTemplate;
@Resource
private OrderImportTaskMapper orderImportTaskMapper;
@Resource
private OrderImportTaskService orderImportTaskService;
/**
* cad拆单请求缓存地址
*/
@Value("${chenfeng.plan.webcad.import.temp-file-path}")
private String webcadJsonCachePath;
private static final String REDIS_UNIQUEKEY = "webcad";
@Resource
private DataSourceTransactionManager transactionManager;
@RabbitListener(queues = "#{@orderImportWebCadQueueName}")
public void orderImport(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
log.info("====================【webcad异步拆单收到消息:{}】====================", message);
Long organId = null;
Long taskId = Long.parseLong(message);
boolean nackSent = false;
TransactionStatus transactionStatus = null;
try {
transactionStatus = transactionManager.getTransaction(TransactionDefinition.withDefaults());
organId = OrganContextHolder.getOrganId();
if (ObjectUtil.isNull(organId)) {
throw ServiceExceptionUtil.exception(ORDER_IMPORT_ORGANID_NOT_EXISTS);
}
// 确认任务,更新状态为消费成功,没有找到任务中断后续消费
OrderImportTaskDO orderImportTaskDO = confirmOrderImport(taskId);
if (ObjectUtil.isNull(orderImportTaskDO)) {
return;
}
log.info("====================【webcad异步拆单处理临时数据开始】====================");
WebCadOrderImportAsyncFactory webCadOrderImportAsyncFactory =
new WebCadOrderImportAsyncFactory(organId, orderMapper, snowFlakeGenerator, orderBodyMapper, plateMapper, orderGroupMapper, idWorker,
orderImportTaskMapper, orderInputProcessor, goodsMapper, rawGoodsMapper, customPlateNoGenerateService, systemConfigApi, orderImportTaskDO.getCreator(), cadImportPlateNumThreshold, jdbcTemplate);
webCadOrderImportAsyncFactory.analyzeTempData(taskId, webcadJsonCachePath);
log.info("====================【webcad异步拆单处理临时数据结束】====================");
// 提交事务
transactionManager.commit(transactionStatus);
} catch (ServiceException se) {
// 回滚事务
if (transactionStatus != null) {
transactionManager.rollback(transactionStatus);
}
// 更新任务状态
orderImportTaskService.updateServiceExceptionTaskStatus(taskId, organId, se.getMessage());
nackSent = true;
// 确认消息进入死信队列
channel.basicNack(deliveryTag, false, false);
} catch (Exception e) {
// 回滚事务
if (transactionStatus != null) {
transactionManager.rollback(transactionStatus);
}
// 更新任务状态
orderImportTaskService.updateExceptionTaskStatus(taskId, organId);
nackSent = true;
// 确认消息进入死信队列
channel.basicNack(deliveryTag, false, false);
log.error(ORDER_IMPORT_FAIL.getMsg(), e);
} finally {
// 手动确认消息接收
if (!nackSent) {
channel.basicAck(deliveryTag, false);
}
// 导入锁解锁
redisLockUtil.unlock(String.format(RedisKeyConstants.ORDER_IMPORT_LOCK_KEY, organId), REDIS_UNIQUEKEY);
}
}
/**
* 确认导入任务,更新任务状态
*
* @param taskId
*/
private OrderImportTaskDO confirmOrderImport(Long taskId) {
boolean taskExist = false;
OrderImportTaskDO orderImportTaskDO = null;
// 线程每1s查询一次,查三次没有就提示任务不存在
for (int i = 0; i < 3; i++) {
orderImportTaskDO = orderImportTaskMapper.selectById(taskId);
if (ObjectUtil.isNotNull(orderImportTaskDO)) {
taskExist = true;
break;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("====================【线程中断异常】====================");
}
}
}
if (taskExist) {
// 更新状态为消费成功
orderImportTaskMapper.update(new LambdaUpdateWrapper<OrderImportTaskDO>().eq(OrderImportTaskDO::getId, taskId).set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_SUCCESS.getStatus()));
} else {
String taskNotExistNotify = String.format("%s不存在的导入任务,消费中止", taskId);
// 更新状态为消费成功,导入失败
orderImportTaskMapper.update(
new LambdaUpdateWrapper<OrderImportTaskDO>()
.eq(OrderImportTaskDO::getId, taskId)
.set(OrderImportTaskDO::getStatus, OrderImportTaskStatusEnum.CONSUME_SUCCESS.getStatus())
.set(OrderImportTaskDO::getImportStatus, OrderImportStatusEnum.IMPORT_FAIL.getStatus())
.set(OrderImportTaskDO::getResult, taskNotExistNotify)
);
log.error(String.format("====================【%s】====================", taskNotExistNotify));
}
return orderImportTaskDO;
}
}
@@ -279,7 +279,7 @@ public class DefaultXmlOrderImportFactory {
log.info("====================【生产单默认xml导入处理回显结束】====================");
// 删除临时数据
// deleteTempData();
deleteTempData();
// 完成所有流程后更新任务为导入成功
updateTaskSuccess();
} catch (Exception e) {
@@ -72,6 +72,12 @@ spring:
port: 5672 # RabbitMQ 服务的端口
username: guest # RabbitMQ 服务的账号
password: guest # RabbitMQ 服务的密码
listener:
simple:
concurrency: 2
max-concurrency: 5
prefetch: 5
acknowledge-mode: manual
# Kafka 配置项,对应 KafkaProperties 配置类
kafka:
bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
@@ -172,4 +172,11 @@ chenfeng:
ignore-tables:
encrypt:
enable: false
publicKey: cfimes
publicKey: cfimes
plan:
webcad:
import:
node: 1
async-plate-threshold: 5000
cadImportPlateNumThreshold: 20000
temp-file-path: /data/imes-server/cf-prod-plan/local
@@ -0,0 +1,54 @@
package com.cf.imes.module.system.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_EXCHANGE;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_QUEUE;
import static com.cf.imes.framework.mq.rabbitmq.constant.RabbitMqConstants.SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_ROUTING_KEY;
/**
* @author Gqr
* @since 2025/5/13 11:42
*/
@Configuration
public class ChenfengRabbitMQSystemConfiguration {
/**
* 广告状态修改 延迟队列
*/
@Bean
public Queue advertisementUpdateDelayedQueue(){
return QueueBuilder.durable(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_QUEUE).build();
}
/**
* 广告状态修改 定义延迟交换机
*/
@Bean
public CustomExchange advertisementUpdateDelayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
}
/**
* 广告状态修改 队列与交换机绑定
*/
@Bean
public Binding advertisementUpdateQueueABindingX(@Qualifier("advertisementUpdateDelayedQueue") Queue queue,
@Qualifier("advertisementUpdateDelayExchange") CustomExchange delayedExchange){
return BindingBuilder.bind(queue)
.to(delayedExchange)
.with(SYSTEM_ADVERTISEMENT_UPDATE_DELAYED_ROUTING_KEY)
.noargs();
}
}