Commit 73c9b8d7 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
parents c2c74064 c6774944
......@@ -532,8 +532,8 @@ public class MacroPlannerResultController {
description = "执行数据库转换→数据验证→MIP建模→分层求解→JSON结果保存的完整流程。"
+ "耗时通常数秒到数分钟, 取决于数据规模。")
public R<Map<String, Object>> runOptimization(
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId,
@RequestParam("strategyId") @Parameter(description = "kpi设置", required = true) Integer strategyId) {
@RequestParam("sceneId") @Parameter(description = "场景ID", required = true) String sceneId
, @RequestParam("strategyId") @Parameter(description = "kpi设置", required = true) Integer strategyId) {
if (sceneId == null || sceneId.trim().isEmpty()) {
return R.failed("sceneId不能为空");
}
......@@ -561,7 +561,7 @@ public class MacroPlannerResultController {
long solveStart = System.currentTimeMillis();
MacroPlannerOptimizer optimizer = new MacroPlannerOptimizer(data);
optimizer.buildModel();
optimizer.solve(sid);
optimizer.solve();
long solveEnd = System.currentTimeMillis();
// 4. 保存结果到 JSON 文件
......
package com.aps.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.aps.common.util.R;
import com.aps.entity.MacroSceneConfig;
import com.aps.entity.PlanPeriod;
import com.aps.macroplanner.scene.MacroSceneContext;
import com.aps.macroplanner.scene.MacroSceneService;
import com.aps.service.PlanPeriodService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
/**
* <p>
* 前端控制器
* </p>
*
* @author MyBatis-Plus
* @since 2026-09-20
*/
@RestController
@RequestMapping("/planPeriod")
@RequiredArgsConstructor
public class PlanPeriodController {
private final PlanPeriodService planPeriodService;
private final MacroSceneService macroSceneService;
@PostMapping("/create")
@Operation(summary = "新增场景计划周期")
public R<PlanPeriod> create(@RequestParam(required = false) String sceneId,
@RequestBody PlanPeriod period) {
String targetScene = resolveScene(sceneId);
return MacroSceneContext.execute(targetScene, () -> {
period.setId(UUID.randomUUID().toString());
period.setMpSceneId(targetScene);
period.setIsdeleted(0L);
period.setDeletiontime(null);
period.setDeleteruserid(null);
period.setCreationtime(LocalDateTime.now());
if (!planPeriodService.save(period)) {
return R.failed("新增计划周期失败");
}
return R.ok(period);
});
}
@GetMapping("/list")
@Operation(summary = "查询场景计划周期")
public R<List<PlanPeriod>> list(@RequestParam(required = false) String sceneId) {
String targetScene = resolveScene(sceneId);
return MacroSceneContext.execute(targetScene, () ->
R.ok(planPeriodService.list(new LambdaQueryWrapper<PlanPeriod>()
.eq(PlanPeriod::getIsdeleted, 0))));
}
private String resolveScene(String sceneId) {
String targetScene = sceneId == null ? MacroSceneContext.getSceneId() : sceneId.trim();
if (targetScene == null || targetScene.isEmpty()) return null;
MacroSceneConfig scene = macroSceneService.getScene(targetScene);
if (scene == null || !targetScene.equals(scene.getSceneId())
|| !"READY".equals(scene.getSceneStatus())) {
throw new IllegalArgumentException("场景不存在或尚未就绪: " + targetScene);
}
return targetScene;
}
}
package com.aps.entity;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;
......@@ -8,6 +12,7 @@ import lombok.Data;
@Data
public class PlanPeriod {
@TableId(type = IdType.INPUT)
private String id;
private LocalDateTime creationtime;
private Long creatoruserid;
......@@ -34,5 +39,6 @@ private Long isvisable;
private Long top;
private LocalDateTime basetime;
private Short enabled;
@TableField(value = "MP_SCENE_ID", fill = FieldFill.INSERT)
private String mpSceneId;
}
......@@ -724,7 +724,7 @@ public class SolutionPrinter {
KpiLib kpi = KpiLib.ofEn(key);
double val= model.getKpi(key).solutionValue();
writeLog(" %s: %.2f (权重%.0f × %.2f)",
kpi.getCn(),val * weight, val, val);
kpi.getCn(),val * weight, val, weight);
}
......
......@@ -31,7 +31,8 @@ public class MacroSceneDataPermissionHandler implements MultiDataPermissionHandl
"PLAN_RESOURCE",
"EQUIPINFO",
"EQUIP_SHIFT_CAPACITY",
"APS_TIME_CONFIG"
"APS_TIME_CONFIG",
"PLAN_PERIOD"
));
@Override
......
......@@ -6,7 +6,7 @@ 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 static final boolean DEFAULT_ONLY = false;
private final boolean defaultOnly;
public MacroSceneMode() {
......
......@@ -71,6 +71,7 @@ public class MacroSceneService {
private final EquipinfoMapper equipinfoMapper;
private final EquipShiftCapacityMapper equipShiftCapacityMapper;
private final ApsTimeConfigMapper apsTimeConfigMapper;
private final PlanPeriodMapper planPeriodMapper;
public List<MacroSceneConfig> listScenes() {
if (sceneMode.isDefaultOnly()) return java.util.Collections.singletonList(sceneMode.defaultScene());
......@@ -183,6 +184,7 @@ public class MacroSceneService {
sjzPfWhStockMapper.delete(null);
materialInfoMapper.delete(null);
apsTimeConfigMapper.delete(null);
planPeriodMapper.delete(null);
});
}
......@@ -214,6 +216,8 @@ public class MacroSceneService {
copyStocksAndSupplies(sourceSceneId, targetSceneId, materialIds, routingIds);
copyEquipCapacity(sourceSceneId, targetSceneId, equipIds, resourceIds);
copySimpleRows(sourceSceneId, targetSceneId, apsTimeConfigMapper, ApsTimeConfig.class, null);
copySimpleRows(sourceSceneId, targetSceneId, planPeriodMapper, PlanPeriod.class,
row -> row.setId(UUID.randomUUID().toString()));
}
private Map<Integer, Integer> copyEquipinfo(String source, String target) {
......@@ -1027,7 +1031,8 @@ public class MacroSceneService {
|| mapper == routingDetailEquipMapper || mapper == stockMapper
|| mapper == materialPurchaseMapper || mapper == erpPurchaseOrderMapper
|| mapper == purchaseReceiptMapper || mapper == sjzPfWhStockMapper
|| mapper == planResourceMapper || mapper == equipinfoMapper) {
|| mapper == planResourceMapper || mapper == equipinfoMapper
|| mapper == planPeriodMapper) {
return "ISDELETED";
}
if (mapper == routingDetailMapper || mapper == equipShiftCapacityMapper) {
......
package com.aps.macroplanner.data;
import com.aps.entity.RoutingHeader;
import com.aps.entity.Routingsupporting;
import com.aps.mapper.RoutingHeaderMapper;
import com.aps.mapper.RoutingsupportingMapper;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
class MacroPlannerBomLoadingTest {
private MacroPlannerDataConverter converter;
private RoutingHeaderMapper headers;
private RoutingsupportingMapper inputs;
private Object context;
@BeforeEach
void setUp() throws Exception {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "bom-test"), RoutingHeader.class);
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "bom-test"), Routingsupporting.class);
converter = new MacroPlannerDataConverter();
headers = mock(RoutingHeaderMapper.class);
inputs = mock(RoutingsupportingMapper.class);
ReflectionTestUtils.setField(converter, "routingHeaderMapper", headers);
ReflectionTestUtils.setField(converter, "routingsupportingMapper", inputs);
Class<?> contextType = Class.forName(MacroPlannerDataConverter.class.getName() + "$ConvertContext");
Constructor<?> constructor = contextType.getDeclaredConstructor();
constructor.setAccessible(true);
context = constructor.newInstance();
ReflectionTestUtils.setField(context, "routingHeaders", new ArrayList<>(Collections.singletonList(header(1, "A"))));
}
@Test
@Timeout(5)
void eachLevelLoadsOnlyNewRoutingIds() {
List<Set<Object>> queriedRoutes = new ArrayList<>();
when(inputs.selectList(any())).thenAnswer(call -> {
Set<Object> ids = values(call.getArgument(0));
ids.remove(0);
queriedRoutes.add(ids);
if (ids.equals(Collections.singleton(1))) return Collections.singletonList(input(1, "B"));
if (ids.equals(Collections.singleton(2))) return Collections.singletonList(input(2, "C"));
return Collections.emptyList();
});
when(headers.selectList(any())).thenAnswer(call -> values(call.getArgument(0)).contains("B")
? Collections.singletonList(header(2, "B")) : Collections.emptyList());
Set<String> materials = new HashSet<>(Collections.singleton("A"));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context, materials);
assertEquals(Arrays.asList(1, 2), result);
assertEquals(Arrays.asList(Collections.singleton(1), Collections.singleton(2)), queriedRoutes);
assertEquals(new HashSet<>(Arrays.asList("A", "B", "C")), materials);
verify(headers, times(2)).selectList(any());
}
@Test
@Timeout(5)
void cyclicBomTerminatesWithoutDiscardingReachableHeaders() {
when(inputs.selectList(any())).thenAnswer(call -> values(call.getArgument(0)).contains(1)
? Collections.singletonList(input(1, "B")) : Collections.singletonList(input(2, "A")));
when(headers.selectList(any())).thenReturn(Collections.singletonList(header(2, "B")));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Arrays.asList(1, 2), result);
verify(inputs, times(2)).selectList(any());
verify(headers, times(1)).selectList(any());
}
@Test
void duplicateChildHeadersAreLoadedOnlyOnce() {
when(inputs.selectList(any())).thenReturn(Arrays.asList(input(1, "B"), input(1, "B")), Collections.emptyList());
when(headers.selectList(any())).thenReturn(Arrays.asList(header(2, "B"), header(2, "B"), header(3, "B")));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Arrays.asList(1, 2, 3), result);
verify(headers, times(1)).selectList(any());
verify(inputs, times(2)).selectList(any());
}
@Test
void nullInputMaterialDoesNotProduceUnboundedHeaderQuery() {
when(inputs.selectList(any())).thenReturn(Collections.singletonList(input(1, null)));
List<Integer> result = ReflectionTestUtils.invokeMethod(converter, "expandRoutingHeaders", context,
new HashSet<>(Collections.singleton("A")));
assertEquals(Collections.singletonList(1), result);
verifyNoInteractions(headers);
}
@Test
void oracleInGroupsAreBoundedDeduplicatedAndParenthesized() {
List<Integer> ids = IntStream.range(1, 2002).boxed().collect(Collectors.toList());
ids.add(1);
LambdaQueryWrapper<RoutingHeader> wrapper = MacroPlannerDataConverter.whereIn(RoutingHeader::getId, ids);
String predicate = wrapper.getSqlSegment();
assertEquals(2001, wrapper.getParamNameValuePairs().size());
assertEquals(3, predicate.split(" IN ", -1).length - 1);
assertEquals(2, predicate.split(" OR ", -1).length - 1);
java.util.regex.Matcher groups = java.util.regex.Pattern.compile(" IN [(]([^)]+)[)]").matcher(predicate);
List<Integer> sizes = new ArrayList<>();
while (groups.find()) sizes.add(groups.group(1).split(",").length);
assertEquals(Arrays.asList(1000, 1000, 1), sizes);
wrapper.eq(RoutingHeader::getIsDeleted, false);
assertTrue(wrapper.getSqlSegment().contains(") AND is_deleted ="));
assertTrue(MacroPlannerDataConverter.whereIn(RoutingHeader::getId, Collections.emptyList())
.getSqlSegment().contains("1 = 0"));
}
private Set<Object> values(LambdaQueryWrapper<?> wrapper) {
wrapper.getSqlSegment();
return new HashSet<>(wrapper.getParamNameValuePairs().values());
}
private RoutingHeader header(int id, String material) {
RoutingHeader header = new RoutingHeader();
header.setId(id);
header.setMaterialId(material);
return header;
}
private Routingsupporting input(int routingId, String material) {
Routingsupporting input = new Routingsupporting();
input.setRoutingHeaderId(routingId);
input.setMaterialId(material);
return input;
}
}
......@@ -45,4 +45,14 @@ class MacroSceneDataPermissionHandlerTest {
assertNull(expression);
}
@Test
void planPeriodsAreScopedToBaselineOrSelectedScene() {
Table table = new Table("PLAN_PERIOD");
assertEquals("PLAN_PERIOD.MP_SCENE_ID IS NULL",
handler.getSqlSegment(table, null, "test.select").toString());
MacroSceneContext.setSceneId("scene-001");
assertEquals("PLAN_PERIOD.MP_SCENE_ID = 'scene-001'",
handler.getSqlSegment(table, null, "test.select").toString());
}
}
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