Commit 0fd5e9c9 authored by Tong Li's avatar Tong Li

Merge remote-tracking branch 'origin/master' into tl

# Conflicts:
#	src/main/java/com/aps/controller/MacroPlannerResultController.java
#	src/main/java/com/aps/macroplanner/output/ResultWriter.java
parents 4cad6053 5b411bad
......@@ -23,6 +23,17 @@ import java.util.List;
public class MacroSceneController {
private final MacroSceneService macroSceneService;
private final com.aps.macroplanner.scene.MacroSceneMode sceneMode;
@GetMapping("/capabilities")
@Operation(summary = "查询主计划场景管理能力")
public R<java.util.Map<String, Boolean>> capabilities() {
java.util.Map<String, Boolean> result = new java.util.LinkedHashMap<>();
result.put("defaultOnly", sceneMode.isDefaultOnly());
result.put("canCreate", !sceneMode.isDefaultOnly());
result.put("canDelete", !sceneMode.isDefaultOnly());
return R.ok(result);
}
@PostMapping("/create")
@Operation(summary = "创建主计划场景", description = "从原始数据或已有主计划场景复制创建新场景")
......
......@@ -170,7 +170,7 @@ public class MacroPlannerDataConverter {
// 0. 读取时间配置, 计算 horizonEnd
// endCount = 期数 (与 periodDimension 结合决定实际天数)
LambdaQueryWrapper<ApsTimeConfig> wrapper= new LambdaQueryWrapper<ApsTimeConfig>();
if(!sceneId.isEmpty())
if(sceneId != null && !sceneId.isEmpty())
{
// wrapper.eq(ApsTimeConfig::getMpSceneId,sceneId);
}
......@@ -200,19 +200,19 @@ public class MacroPlannerDataConverter {
// 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤)
// 1. 需求订单 (ApsDemandOrder, 按 deliverytime 时间范围过滤)
// ctx.apsDemandOrders = apsDemandOrderMapper.selectList(
// new LambdaQueryWrapper<ApsDemandOrder>().eq(ApsDemandOrder::getIsdeleted, 0)
// .ge(ApsDemandOrder::getDeliverytime, ctx.baseTime)
// .lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime));
ctx.apsDemandOrders = apsDemandOrderMapper.selectList(
new LambdaQueryWrapper<ApsDemandOrder>()
new LambdaQueryWrapper<ApsDemandOrder>().eq(ApsDemandOrder::getIsdeleted, 0)
.ge(ApsDemandOrder::getDeliverytime, ctx.baseTime)
.eq(ApsDemandOrder::getCode,"XQDD_20260812_6")
.lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime)
);
.lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime));
// ctx.apsDemandOrders = apsDemandOrderMapper.selectList(
//
// new LambdaQueryWrapper<ApsDemandOrder>()
// .ge(ApsDemandOrder::getDeliverytime, ctx.baseTime)
// .eq(ApsDemandOrder::getCode,"XQDD_20260812_6")
// .lt(ApsDemandOrder::getDeliverytime, horizonEndDateTime)
// );
log.info("加载需求订单: {} 条 (时间范围过滤)", ctx.apsDemandOrders.size());
// 2. 收集 materialIds (从 ApsDemandOrder.mmid)
......
package com.aps.macroplanner.scene;
import com.aps.entity.MacroSceneConfig;
import org.springframework.stereotype.Component;
@Component
public class MacroSceneMode {
// Temporary test mode. Set to false and rebuild to restore scene management.
private static final boolean DEFAULT_ONLY = true;
private final boolean defaultOnly;
public MacroSceneMode() {
this(DEFAULT_ONLY);
}
MacroSceneMode(boolean defaultOnly) {
this.defaultOnly = defaultOnly;
}
public boolean isDefaultOnly() { return defaultOnly; }
public String resolve(String sceneId) {
if (defaultOnly) return null;
String id = sceneId == null || sceneId.trim().isEmpty() ? null : sceneId.trim();
if (id == null) {
throw new IllegalArgumentException("sceneId不能为空");
}
return id;
}
public void requireSceneManagement() {
if (defaultOnly) {
throw new IllegalArgumentException("当前仅支持默认场景,暂不支持创建或删除");
}
}
public MacroSceneConfig defaultScene() {
MacroSceneConfig scene = new MacroSceneConfig();
scene.setSceneId("default");
scene.setSceneName("默认场景");
scene.setSceneStatus("READY");
return scene;
}
public static String resultFileKey(String sceneId) {
return sceneId == null || sceneId.trim().isEmpty() ? "default" : sceneId.trim();
}
}
......@@ -12,6 +12,12 @@ import java.io.IOException;
@Component
public class MacroSceneRequestFilter extends OncePerRequestFilter {
private final MacroSceneMode sceneMode;
public MacroSceneRequestFilter(MacroSceneMode sceneMode) {
this.sceneMode = sceneMode;
}
public static final String HEADER_NAME = "X-MP-Scene-Id";
@Override
......@@ -36,6 +42,9 @@ public class MacroSceneRequestFilter extends OncePerRequestFilter {
sceneId = request.getParameter("sceneId");
}
try {
if (sceneMode.isDefaultOnly() && request.getServletPath().startsWith("/macroResult")) {
sceneId = null;
}
MacroSceneContext.setSceneId(sceneId);
filterChain.doFilter(request, response);
} finally {
......
......@@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
......@@ -49,6 +50,8 @@ public class MacroSceneService {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final DataSource dataSource;
private final MacroSceneMode sceneMode;
private final JdbcTemplate jdbcTemplate;
private final MacroSceneSelectionService selectionService;
private final MacroSceneConfigMapper sceneConfigMapper;
private final MacroSceneSequenceMapper sequenceMapper;
......@@ -70,18 +73,29 @@ public class MacroSceneService {
private final ApsTimeConfigMapper apsTimeConfigMapper;
public List<MacroSceneConfig> listScenes() {
if (sceneMode.isDefaultOnly()) return java.util.Collections.singletonList(sceneMode.defaultScene());
return sceneConfigMapper.selectList(null);
}
public MacroSceneConfig getScene(String sceneId) {
if (sceneMode.isDefaultOnly()) {
sceneMode.resolve(sceneId);
return sceneMode.defaultScene();
}
return sceneConfigMapper.selectById(requireText(sceneId, "sceneId"));
}
public void selectScene(String sceneId, String userId) {
if (sceneMode.isDefaultOnly()) {
sceneMode.resolve(sceneId);
MacroSceneSelectionService.validateUserId(userId);
return;
}
selectionService.select(sceneId, userId);
}
public synchronized MacroSceneConfig createScene(MacroSceneCreateRequest request) {
sceneMode.requireSceneManagement();
long createStartNanos = System.nanoTime();
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
......@@ -141,6 +155,7 @@ public class MacroSceneService {
@Transactional(rollbackFor = Exception.class)
public void deleteScene(String sceneId) {
sceneMode.requireSceneManagement();
String normalizedSceneId = requireText(sceneId, "sceneId");
if (sceneConfigMapper.selectById(normalizedSceneId) == null) {
return;
......@@ -150,6 +165,7 @@ public class MacroSceneService {
}
private void cleanupSceneData(String sceneId) {
cleanupPispipResults(sceneId);
MacroSceneContext.execute(sceneId, () -> {
apsDemandOrderMapper.delete(null);
routingDetailEquipMapper.delete(null);
......@@ -170,6 +186,14 @@ public class MacroSceneService {
});
}
private void cleanupPispipResults(String sceneId) {
// Foreign keys require children to be removed before the PISPIP parent rows.
jdbcTemplate.update("DELETE FROM MP_PISPIP_BOM WHERE SCENE_ID = ?", sceneId);
jdbcTemplate.update("DELETE FROM MP_PISPIP_PRODUCTION WHERE SCENE_ID = ?", sceneId);
jdbcTemplate.update("DELETE FROM MP_PISPIP_RESULT WHERE SCENE_ID = ?", sceneId);
log.info("已清理主计划排产结果,场景ID:{}", sceneId);
}
private void cloneSceneData(String sourceSceneId, String targetSceneId) {
Map<Integer, Integer> equipIds = copyEquipinfo(sourceSceneId, targetSceneId);
Map<Integer, Integer> resourceIds = copyPlanResources(sourceSceneId, targetSceneId, equipIds);
......
......@@ -47,10 +47,6 @@ public class MacroPlannerResultService {
* @return 反序列化后的优化结果, 或 null (文件不存在/读取失败)
*/
public OptimizationResult getResult(String sceneId) {
if (sceneId == null || sceneId.isEmpty()) {
log.warn("sceneId为空, 无法读取结果");
return null;
}
File file = getOptimizationFile(sceneId);
if (file == null || !file.exists() || file.length() == 0) {
......@@ -73,6 +69,7 @@ public class MacroPlannerResultService {
* 定位结果文件, 优先 .json, 其次 .json.gz。
*/
private File getOptimizationFile(String sceneId) {
sceneId = com.aps.macroplanner.scene.MacroSceneMode.resultFileKey(sceneId);
String workDir = System.getProperty("user.dir");
Path resultDir = Paths.get(workDir, OUTPUT_DIR);
......
package com.aps.macroplanner.scene;
import com.aps.entity.MacroSceneConfig;
import com.aps.mapper.MacroSceneConfigMapper;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class MacroSceneDefaultModeTest {
@Test
void defaultResultsRoundTrip(@org.junit.jupiter.api.io.TempDir java.nio.file.Path dir) {
String oldDir = System.getProperty("user.dir");
try {
System.setProperty("user.dir", dir.toString());
com.aps.macroplanner.output.ResultWriter writer =
new com.aps.macroplanner.output.ResultWriter(null, null, 0);
com.aps.macroplanner.output.dto.OptimizationResult result =
new com.aps.macroplanner.output.dto.OptimizationResult();
assertTrue(writer.saveResultToFile(null, result));
assertTrue(java.nio.file.Files.exists(dir.resolve("mp/result/optimization_result_default.json")));
assertNotNull(writer.getResultToFile(null));
assertNotNull(new com.aps.service.MacroPlannerResultService().getResult(null));
} finally {
System.setProperty("user.dir", oldDir);
}
}
@Test
void resolvesAllIdsToBaselineInDefaultMode() {
MacroSceneMode mode = new MacroSceneMode(true);
assertNull(mode.resolve(null));
assertNull(mode.resolve(" "));
for (String id : new String[]{"null", "1", "default", "scene-001"}) {
assertNull(mode.resolve(id));
}
assertEquals("default", MacroSceneMode.resultFileKey(null));
assertEquals("scene-001", MacroSceneMode.resultFileKey("scene-001"));
}
@Test
void multiSceneModeRestoresNonemptyIdsAndManagement() {
MacroSceneMode mode = new MacroSceneMode(false);
assertEquals("scene-001", mode.resolve(" scene-001 "));
assertThrows(IllegalArgumentException.class, () -> mode.resolve(null));
assertDoesNotThrow(mode::requireSceneManagement);
}
@Test
void defaultSceneDoesNotAccessDatabaseAndManagementIsBlocked() {
MacroSceneService service = mock(MacroSceneService.class, CALLS_REAL_METHODS);
MacroSceneConfigMapper mapper = mock(MacroSceneConfigMapper.class);
ReflectionTestUtils.setField(service, "sceneMode", new MacroSceneMode(true));
ReflectionTestUtils.setField(service, "sceneConfigMapper", mapper);
MacroSceneConfig scene = service.listScenes().get(0);
assertEquals("default", scene.getSceneId());
assertNull(new MacroSceneMode(true).resolve(scene.getSceneId()));
assertEquals("默认场景", scene.getSceneName());
assertEquals("READY", scene.getSceneStatus());
service.selectScene(null, "1111");
assertDoesNotThrow(() -> service.selectScene("1", "1111"));
assertEquals("default", service.getScene("old-scene").getSceneId());
assertThrows(IllegalArgumentException.class, () -> service.createScene(null));
assertThrows(IllegalArgumentException.class, () -> service.deleteScene("scene-001"));
verifyNoInteractions(mapper);
}
@Test
void staleHeaderCannotChangeDefaultResultScope() throws Exception {
MacroSceneRequestFilter filter = new MacroSceneRequestFilter(new MacroSceneMode(true));
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/macroResult/run");
request.setServletPath("/macroResult/run");
request.addHeader("X-MP-Scene-Id", "old-scene");
filter.doFilter(request, new MockHttpServletResponse(), (req, res) -> {
assertNull(MacroSceneContext.getSceneId());
});
assertNull(MacroSceneContext.getSceneId());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment