1、拉取组件业务数据接口order_plate和order_item批量改为2k;2、新增接入组件排单 相关四个接口:新增/删除排单、新增/删除小板;

This commit is contained in:
gaoqr
2026-07-31 16:09:09 +08:00
parent e77163dcf8
commit 31b3c82317
10 changed files with 477 additions and 9 deletions
@@ -24,6 +24,7 @@ public class ErrorCodeConstants {
public static final ErrorCode ORDER_PLATE_UPDATE_CONCURRENCY_ERROR = new ErrorCode(1_003_107_012, "order.plate.update.concurrency.error");
public static final ErrorCode GOODS_PLATE_UPDATE_CONCURRENCY_ERROR = new ErrorCode(1_003_107_013, "goods.plate.update.concurrency.error");
public static final ErrorCode BODY_PLATE_UPDATE_CONCURRENCY_ERROR = new ErrorCode(1_003_107_014, "body.plate.update.concurrency.error");
public static final ErrorCode PRODUCING_PLAN_ORDER_SYNC_FAILED = new ErrorCode(1_003_107_016, "producing.plan.order.sync.failed");
public static final ErrorCode ORDER_PLAN_LOCKED = new ErrorCode(1_003_107_015, "当前排单已锁定,请稍后再试");
@@ -152,4 +152,9 @@ public class PlanDO extends BaseDO {
* 已开板件数量
*/
private Integer cuttedPlateNum;
}
/**
* 组件生产系统返回的生产排单 ID。
*/
private Long producingPlanOrderId;
}
@@ -2,6 +2,7 @@ package com.cf.imes.module.executor.framework.rpc.config;
import com.cf.imes.module.infra.api.file.FileApi;
import com.cf.imes.module.system.api.application.ApplicationApi;
import com.cf.imes.module.system.api.auth.AuthApi;
import com.cf.imes.module.system.api.assemblyconfig.AssemblyConfigApi;
import com.cf.imes.module.system.api.customplateno.CustomPlateNoSeqApi;
import com.cf.imes.module.system.api.dataSource.DataSourceApi;
@@ -20,6 +21,7 @@ import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableFeignClients(clients = {AdminUserApi.class, MachineApi.class, DictDataApi.class,
FileApi.class, FileApi.class, DataSourceApi.class, ApplicationApi.class,
OrganApi.class, SystemConfigApi.class, CustomPlateNoSeqApi.class, PermissionApi.class, AssemblyConfigApi.class})
OrganApi.class, SystemConfigApi.class, CustomPlateNoSeqApi.class, PermissionApi.class,
AssemblyConfigApi.class, AuthApi.class})
public class RpcConfiguration {
}
@@ -58,6 +58,8 @@ import com.cf.imes.module.executor.dal.mysql.remainplaten.RemainPlateMapper;
import com.cf.imes.module.executor.enums.ErrorCodeConstants;
import com.cf.imes.module.executor.enums.OrderPlanQueryTypeEnum;
import com.cf.imes.module.executor.enums.OrderStatusEnum;
import com.cf.imes.module.executor.service.plan.producing.ProducingPlanOrderClient;
import com.cf.imes.module.system.api.auth.AuthApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.framework.AopContext;
@@ -131,6 +133,12 @@ public class PlanServiceImpl implements PlanService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private AuthApi authApi;
@Resource
private ProducingPlanOrderClient producingPlanOrderClient;
// 重复使用的常量
private static final String FIELD_ORDER_ID = "o.id";
private static final String FIELD_ORDER_TYPE = "o.order_type";
@@ -203,6 +211,7 @@ public class PlanServiceImpl implements PlanService {
}
// 增加排单
planMapper.insertBatch(planDOS);
createProducingPlanOrders(planDOS);
}
}
@@ -319,6 +328,7 @@ public class PlanServiceImpl implements PlanService {
.in(OrderItemDO::getPlateId, plateIdList));
}
deleteProducingPlanOrders(planDOS);
}
/**
@@ -1291,7 +1301,7 @@ public class PlanServiceImpl implements PlanService {
List<Long> deletePlanIds = deleteOrderPlateIds.stream().map(OrderPlateIds::getPlanId).distinct().toList();
validatePlanExists(deletePlanIds);
List<PlanDO> deletePlans = validatePlanExists(deletePlanIds);
List<Integer> plateStatus = Arrays.asList(OrderPlateCutStatusEnum.OPENING.getStatus(),OrderPlateCutStatusEnum.OPENED.getStatus());
@@ -1312,6 +1322,18 @@ public class PlanServiceImpl implements PlanService {
AssertUtils.empty(plateDOList, PLAN_PLATE_IS_CUTTED);
Map<Long, String> sourceBlockIdByPlateId = plateDOS.stream()
.filter(plate -> StringUtils.isNotBlank(plate.getSourceBlockItemId()))
.collect(Collectors.toMap(PlateDO::getId, PlateDO::getSourceBlockItemId));
Map<Long, List<String>> sourceBlockIdsByPlanId = deleteOrderPlateIds.stream()
.filter(item -> sourceBlockIdByPlateId.containsKey(item.getPlateId()))
.collect(Collectors.groupingBy(
OrderPlateIds::getPlanId,
Collectors.mapping(
item -> sourceBlockIdByPlateId.get(item.getPlateId()),
Collectors.collectingAndThen(Collectors.toList(),
list -> list.stream().distinct().toList()))));
// 移除排单板件 & 重新计算订单和材质下的数量和面积
removePlanOrderAndGoodsPlate(plateDOS, deleteOrderIds);
@@ -1341,6 +1363,8 @@ public class PlanServiceImpl implements PlanService {
.set(PlanDO::getOrderNos, orderNos));
}
}
syncDeletedProducingPlanBlocks(
deletePlans, sourceBlockIdsByPlanId, new HashSet<>(deleteEmptyPlanIdList));
}
@@ -1390,6 +1414,12 @@ public class PlanServiceImpl implements PlanService {
// 新增排单板件 & 重新计算订单和材质下的数量和面积
insertPlanOrderAndGoodsPlate(planDO.getId(), insertOrderPlateIds, orderIds);
List<String> sourceBlockIds = getSourceBlockIds(
insertOrderPlateIds.stream().map(OrderPlateIds::getPlateId).toList());
if (CollUtil.isNotEmpty(sourceBlockIds)) {
appendProducingPlanBlocks(planDO, sourceBlockIds);
}
}
/**
@@ -1941,6 +1971,117 @@ public class PlanServiceImpl implements PlanService {
return resultList;
}
/**
* 为包含组件来源板件的本地排单创建组件生产排单。
*/
private void createProducingPlanOrders(List<PlanDO> plans) {
String token = null;
for (PlanDO plan : plans) {
List<String> sourceBlockIds = getSourceBlockIdsByPlanId(plan.getId());
if (CollUtil.isEmpty(sourceBlockIds)) {
continue;
}
if (plan.getProcessId() == null) {
throw new ServiceException(PRODUCING_PLAN_ORDER_SYNC_FAILED,
"workflowSchemeId is required");
}
if (token == null) {
token = getProducingToken();
}
Long producingPlanOrderId = producingPlanOrderClient.create(
token, plan.getProcessId(), sourceBlockIds);
plan.setProducingPlanOrderId(producingPlanOrderId);
planMapper.update(new LambdaUpdateWrapper<PlanDO>()
.eq(PlanDO::getId, plan.getId())
.eq(PlanDO::getOrganId, getUserOrganId())
.set(PlanDO::getProducingPlanOrderId, producingPlanOrderId));
}
}
/**
* 向已有组件生产排单追加板件。
*
* <p>{@code producingPlanOrderId} 为空表示接入组件系统之前创建的历史排单,
* 此类排单不调用组件生产系统,也不补建远端排单。</p>
*/
private void appendProducingPlanBlocks(PlanDO plan, List<String> sourceBlockIds) {
if (plan.getProducingPlanOrderId() == null) {
return;
}
producingPlanOrderClient.appendBlocks(
getProducingToken(), plan.getProducingPlanOrderId(), sourceBlockIds);
}
/**
* 同步删除排单板件;本地排单已变空时直接删除对方排单。
*/
private void syncDeletedProducingPlanBlocks(
List<PlanDO> plans,
Map<Long, List<String>> sourceBlockIdsByPlanId,
Set<Long> deletedPlanIds) {
String token = null;
for (PlanDO plan : plans) {
if (plan.getProducingPlanOrderId() == null) {
continue;
}
List<String> sourceBlockIds = sourceBlockIdsByPlanId.get(plan.getId());
if (!deletedPlanIds.contains(plan.getId()) && CollUtil.isEmpty(sourceBlockIds)) {
continue;
}
if (token == null) {
token = getProducingToken();
}
if (deletedPlanIds.contains(plan.getId())) {
producingPlanOrderClient.delete(token, plan.getProducingPlanOrderId());
} else {
producingPlanOrderClient.deleteBlocks(
token, plan.getProducingPlanOrderId(), sourceBlockIds);
}
}
}
/**
* 删除本地排单时同步删除其组件生产排单。
*/
private void deleteProducingPlanOrders(List<PlanDO> plans) {
String token = null;
for (PlanDO plan : plans) {
if (plan.getProducingPlanOrderId() == null) {
continue;
}
if (token == null) {
token = getProducingToken();
}
producingPlanOrderClient.delete(token, plan.getProducingPlanOrderId());
}
}
private List<String> getSourceBlockIdsByPlanId(Long planId) {
return plateMapper.selectList(new LambdaQueryWrapper<PlateDO>()
.eq(PlateDO::getPlanId, planId)
.eq(PlateDO::getOrganId, getUserOrganId()))
.stream()
.map(PlateDO::getSourceBlockItemId)
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
}
private List<String> getSourceBlockIds(Collection<Long> plateIds) {
if (CollUtil.isEmpty(plateIds)) {
return Collections.emptyList();
}
return plateMapper.selectByIds(plateIds).stream()
.map(PlateDO::getSourceBlockItemId)
.filter(StringUtils::isNotBlank)
.distinct()
.toList();
}
private String getProducingToken() {
return authApi.getMesJwtToken().getCheckedData();
}
@Override
public void getPlanLock(OrderPlanLockReqVO orderPlanLockReqVO) {
Long planId = orderPlanLockReqVO.getPlanId();
@@ -0,0 +1,191 @@
package com.cf.imes.module.executor.service.plan.producing;
import com.cf.imes.framework.common.exception.ServiceException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static com.cf.imes.module.executor.enums.ErrorCodeConstants.PRODUCING_PLAN_ORDER_SYNC_FAILED;
/**
* 组件生产系统生产排单客户端。
*/
@Component
@Slf4j
public class ProducingPlanOrderClient {
private final ProducingPlanOrderProperties properties;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
public ProducingPlanOrderClient(ProducingPlanOrderProperties properties,
ObjectMapper objectMapper) {
this.properties = properties;
this.objectMapper = objectMapper;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(properties.getConnectTimeout())
.build();
}
public Long create(String token, Long workflowSchemeId, List<String> blockIds) {
CreateRequest request = new CreateRequest(blockIds, workflowSchemeId);
HttpResponse<String> response = send(
properties.getBaseUrl(), token, "POST", serialize(request), "create");
try {
JsonNode result = objectMapper.readTree(response.body());
JsonNode planOrderIdNode = result;
if (result != null && result.isObject()) {
JsonNode code = result.get("code");
if (code == null || !code.isIntegralNumber() || code.intValue() != 0) {
String message = result.path("message").asText("business error");
log.error("Producing plan order create failed, responseBody={}",
responseBodyForLog(response.body()));
throw syncError("create: " + message);
}
planOrderIdNode = result.get("data");
}
if (planOrderIdNode == null
|| (!planOrderIdNode.isIntegralNumber() && !planOrderIdNode.isTextual())) {
throw syncError("create: invalid plan order id");
}
return planOrderIdNode.isIntegralNumber()
? planOrderIdNode.longValue() : Long.valueOf(planOrderIdNode.textValue());
} catch (JsonProcessingException | NumberFormatException ex) {
log.error("Producing plan order create response cannot be parsed, responseBody={}",
responseBodyForLog(response.body()), ex);
throw syncError("create: invalid plan order id");
}
}
public void appendBlocks(String token, Long planOrderId, List<String> blockIds) {
HttpResponse<String> response = send(
resourceUrl(planOrderId, "append-block-id"), token, "POST",
serialize(new AppendRequest(blockIds)), "append-block-id");
validateBusinessResponse(response, "append-block-id");
}
public void deleteBlocks(String token, Long planOrderId, List<String> blockIds) {
HttpResponse<String> response = send(
resourceUrl(planOrderId, "delete-block-id"), token, "DELETE",
serialize(new DeleteBlocksRequest(blockIds)), "delete-block-id");
validateBusinessResponse(response, "delete-block-id");
}
public void delete(String token, Long planOrderId) {
HttpResponse<String> response = send(
properties.getBaseUrl() + "/" + planOrderId, token, "DELETE",
null, "delete");
validateBusinessResponse(response, "delete");
}
private String resourceUrl(Long planOrderId, String action) {
return properties.getBaseUrl() + "/" + planOrderId + "/" + action;
}
private String serialize(Object request) {
try {
return objectMapper.writeValueAsString(request);
} catch (JsonProcessingException ex) {
throw syncError("cannot serialize request");
}
}
private HttpResponse<String> send(String url, String token, String method,
String body, String operation) {
HttpRequest.BodyPublisher publisher = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8);
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(properties.getReadTimeout())
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.header("Content-Type", "application/json; charset=utf-8")
.method(method, publisher)
.build();
try {
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
log.error("Producing plan order operation {} failed, url={}, httpStatus={}, "
+ "responseBody={}",
operation, url, response.statusCode(),
responseBodyForLog(response.body()));
throw syncError(operation + ": HTTP " + response.statusCode());
}
return response;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw syncError(operation + ": request interrupted");
} catch (IOException ex) {
log.warn("Producing plan order operation {} failed", operation, ex);
throw syncError(operation + ": " + ex.getMessage());
}
}
/**
* 对方写接口可能以 HTTP 200 返回统一业务响应,此处补充业务码校验。
* 空响应或未包含 code 的响应仍按接口文档视为成功。
*/
private void validateBusinessResponse(HttpResponse<String> response, String operation) {
if (response.body() == null || response.body().isBlank()) {
return;
}
try {
JsonNode result = objectMapper.readTree(response.body());
JsonNode code = result == null || !result.isObject() ? null : result.get("code");
if (code != null && code.isIntegralNumber() && code.intValue() != 0) {
String message = result.path("message").asText("business error");
log.error("Producing plan order operation {} failed, responseBody={}",
operation, responseBodyForLog(response.body()));
throw syncError(operation + ": " + message);
}
} catch (JsonProcessingException ex) {
log.warn("Producing plan order operation {} returned a non-JSON success response, "
+ "responseBody={}",
operation, responseBodyForLog(response.body()));
}
}
private String responseBodyForLog(String body) {
if (body == null) {
return "";
}
int maxLength = 4000;
return body.length() <= maxLength ? body : body.substring(0, maxLength) + "...(truncated)";
}
private ServiceException syncError(String message) {
return new ServiceException(PRODUCING_PLAN_ORDER_SYNC_FAILED, message);
}
@Data
@AllArgsConstructor
private static class CreateRequest {
private List<String> blockIds;
private Long workflowSchemeId;
}
@Data
@AllArgsConstructor
private static class AppendRequest {
private List<String> appendBlockIds;
}
@Data
@AllArgsConstructor
private static class DeleteBlocksRequest {
private List<String> deleteBlockIds;
}
}
@@ -0,0 +1,23 @@
package com.cf.imes.module.executor.service.plan.producing;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* 组件生产系统生产排单接口配置。
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "chenfeng.executor.producing-plan-order")
public class ProducingPlanOrderProperties {
private String baseUrl =
"http://proc.mes.cf/api-branch=opt_split_order/api/v1/producing/plan-order";
private Duration connectTimeout = Duration.ofSeconds(5);
private Duration readTimeout = Duration.ofSeconds(10);
}
@@ -152,3 +152,4 @@ parts.heapsplit.num.not.enough=The number of accessories that can be stacked is
parts.heapsplit.item.source.not.exist=Abnormal accessory stacking data, please refresh and stack again
plan.not.allow.delete=Plan number: {0} has been cut and cannot be deleted
producing.plan.order.sync.failed=Failed to synchronize component production plan order: {0}
@@ -152,3 +152,4 @@ parts.heapsplit.num.not.enough=配件可分堆数量不足,请刷新后再次
parts.heapsplit.item.source.not.exist=配件分堆数据异常,请刷新后再次分堆
plan.not.allow.delete=排单号:{0}已开料无法删除
producing.plan.order.sync.failed=同步组件生产排单失败:{0}
@@ -0,0 +1,93 @@
package com.cf.imes.module.executor.service.plan.producing;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ProducingPlanOrderClientTest {
private final ObjectMapper objectMapper = new ObjectMapper();
private final List<RecordedRequest> requests = new ArrayList<>();
private HttpServer server;
private ProducingPlanOrderClient client;
@BeforeEach
void setUp() throws IOException {
server = HttpServer.create(new InetSocketAddress(0), 0);
server.createContext("/api/v1/producing/plan-order", this::handle);
server.start();
ProducingPlanOrderProperties properties = new ProducingPlanOrderProperties();
properties.setBaseUrl("http://localhost:" + server.getAddress().getPort()
+ "/api/v1/producing/plan-order");
client = new ProducingPlanOrderClient(properties, objectMapper);
}
@AfterEach
void tearDown() {
server.stop(0);
}
@Test
void shouldUseSourceBlockIdsForAllFourOperations() throws Exception {
Long remotePlanId = client.create("mes-token", 36L, List.of("source-1", "source-2"));
client.appendBlocks("mes-token", remotePlanId, List.of("source-3"));
client.deleteBlocks("mes-token", remotePlanId, List.of("source-2"));
client.delete("mes-token", remotePlanId);
assertThat(remotePlanId).isEqualTo(98765L);
assertThat(requests).hasSize(4);
assertThat(requests).extracting(RecordedRequest::method)
.containsExactly("POST", "POST", "DELETE", "DELETE");
assertThat(requests).extracting(RecordedRequest::path)
.containsExactly(
"/api/v1/producing/plan-order",
"/api/v1/producing/plan-order/98765/append-block-id",
"/api/v1/producing/plan-order/98765/delete-block-id",
"/api/v1/producing/plan-order/98765");
assertThat(requests).extracting(RecordedRequest::authorization)
.containsOnly("Bearer mes-token");
JsonNode createBody = objectMapper.readTree(requests.get(0).body());
assertThat(createBody.get("workflowSchemeId").longValue()).isEqualTo(36L);
assertThat(createBody.get("blockIds").toString())
.isEqualTo("[\"source-1\",\"source-2\"]");
assertThat(objectMapper.readTree(requests.get(1).body())
.get("appendBlockIds").get(0).textValue()).isEqualTo("source-3");
assertThat(objectMapper.readTree(requests.get(2).body())
.get("deleteBlockIds").get(0).textValue()).isEqualTo("source-2");
}
private void handle(HttpExchange exchange) throws IOException {
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
requests.add(new RecordedRequest(
exchange.getRequestMethod(),
exchange.getRequestURI().getPath(),
exchange.getRequestHeaders().getFirst("Authorization"),
body));
byte[] response = exchange.getRequestURI().getPath()
.equals("/api/v1/producing/plan-order")
? """
{"data":98765,"code":0,"message":"Request processed successfully."}
""".getBytes(StandardCharsets.UTF_8) : new byte[0];
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
exchange.close();
}
private record RecordedRequest(
String method, String path, String authorization, String body) {
}
}