|
|
@@ -0,0 +1,419 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController;
|
|
|
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.DailyLimitSaveRequest;
|
|
|
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.DimensionRatio;
|
|
|
+import com.etotem.cfc.controller.admin.AdminEnergyConfigController.EnergySourceConfigSaveRequest;
|
|
|
+import com.etotem.cfc.controller.admin.DimensionAdminController;
|
|
|
+import com.etotem.cfc.controller.admin.DimensionConfigController;
|
|
|
+import com.etotem.cfc.controller.admin.DimensionWeightController;
|
|
|
+import com.etotem.cfc.entity.DimensionDailyLimit;
|
|
|
+import com.etotem.cfc.entity.EnergySourceConfig;
|
|
|
+import com.etotem.cfc.entity.ProductDimensionConfig;
|
|
|
+import com.etotem.cfc.mapper.DimensionDailyLimitMapper;
|
|
|
+import com.etotem.cfc.mapper.EnergySourceConfigMapper;
|
|
|
+import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
|
|
|
+import com.etotem.cfc.service.DimensionWeightService;
|
|
|
+import com.etotem.cfc.service.ProductDimensionConfigService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:管理端维度配置治理(能力键 {@code admin.dimension.config})。
|
|
|
+ *
|
|
|
+ * <p><b>需求/设计锚点</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-A-09(维度配置与知识库管理)、
|
|
|
+ * SC-A-25(维度/权重/每日上限配置 CRUD 与生效)、SC-A-26(能量来源比例与归因权重治理);
|
|
|
+ * {@code tests/user-scenarios/07-five-dimension.md} ED-13 / ED-14;</li>
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 + §4.60.9 + §4.60.16 ——
|
|
|
+ * {@code DimensionAdminController} /api/admin/dimension(norm/list/save/delete、data-source)、
|
|
|
+ * {@code DimensionConfigController} /api/admin/dimension-config(list/get/save/delete/tree)、
|
|
|
+ * {@code DimensionWeightController} /api/admin/dimension-weight(save/list)、
|
|
|
+ * {@code AdminEnergyConfigController} /api/admin/energy-config(source-list/source-save、
|
|
|
+ * daily-limits/daily-limit-save);</li>
|
|
|
+ * <li>用例:{@code tests/test-cases/admin.md} TC-A-25 / TC-A-25-E / TC-A-25-P /
|
|
|
+ * TC-A-26 / TC-A-26-E / TC-A-26-P。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>被校验的契约行为</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li><b>正常流(结构锚点)</b>:四个控制器存在并暴露约定端点(SC-A-25/26 主流程 1:CRUD 可用);</li>
|
|
|
+ * <li><b>异常流(TC-A-25-E / TC-A-26-E)</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>维度权重非法(负/超界/和≠100%)→ 400 拦截,不落库;</li>
|
|
|
+ * <li>每日上限非法(负/超界)→ 400 拦截;</li>
|
|
|
+ * <li>能量来源比例非法(负/超 1/和≠1 即和≠100)→ 400 拦截;</li>
|
|
|
+ * <li>删除被引用维度 → 需确认/拒绝,不级联破坏存量;</li>
|
|
|
+ * <li>并发改同配置 → 乐观锁/版本控制({@code ProductDimensionConfig} 应含 {@code version} 字段);</li>
|
|
|
+ * </ul></li>
|
|
|
+ * <li><b>权限流(TC-A-25-P / TC-A-26-P)</b>:非 admin 操作 403 —— 控制器方法应带
|
|
|
+ * {@code @RequestAttribute("role")} 守卫(仿 {@code FileRecordController}:{@code !"admin".equals(role) → 403})。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>实现现状(2026-11-13 核实)</b>:四个控制器类均已存在且 CRUD 端点已落地,
|
|
|
+ * 但<b>异常流 / 边界流 / 权限流契约行为均未实现</b>:
|
|
|
+ * <ul>
|
|
|
+ * <li>{@code DimensionWeightController.save} 仅校验非空,无权重和/范围校验;</li>
|
|
|
+ * <li>{@code AdminEnergyConfigController.dailyLimitSave} 仅校验非空,无上限范围校验;</li>
|
|
|
+ * <li>{@code AdminEnergyConfigController.sourceSave} 无比例和/范围校验;</li>
|
|
|
+ * <li>{@code ProductDimensionConfigService.deleteConfig} 直接 {@code removeById},无「被引用维度」拒绝;</li>
|
|
|
+ * <li>{@code ProductDimensionConfig} 无 {@code version} 字段(无乐观锁);</li>
|
|
|
+ * <li>四个控制器方法均无 {@code @RequestAttribute("role")} 权限守卫(非 admin 不 403)。</li>
|
|
|
+ * </ul>
|
|
|
+ * 因此<b>异常流 / 边界流 / 权限流用例应失败(红)</b>,GREEN 阶段按上述契约实现后转绿。</p>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯契约级单测,仅 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:结构锚点用例通过(绿,基线已落地);异常流 / 边界流 / 权限流用例失败(红),
|
|
|
+ * 断言原文为契约要求的「400 拦截 / 拒绝删除 / version 字段 / role 守卫」缺失。</p>
|
|
|
+ */
|
|
|
+class DimensionConfigAdminContractTest extends EnergyContractTestBase {
|
|
|
+
|
|
|
+ // ==================== 被校验对象 ====================
|
|
|
+
|
|
|
+ private DimensionWeightController weightController;
|
|
|
+ private DimensionWeightService weightService;
|
|
|
+
|
|
|
+ private AdminEnergyConfigController configController;
|
|
|
+ private EnergySourceConfigMapper sourceConfigMapper;
|
|
|
+ private DimensionDailyLimitMapper dailyLimitMapper;
|
|
|
+
|
|
|
+ private ProductDimensionConfigService dimensionConfigService;
|
|
|
+ private ProductDimensionConfigMapper dimensionConfigMapper;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ // --- DimensionWeightController(权重治理,SC-A-25) ---
|
|
|
+ weightController = new DimensionWeightController();
|
|
|
+ weightService = mock(DimensionWeightService.class);
|
|
|
+ inject(weightController, "dimensionWeightService", weightService);
|
|
|
+
|
|
|
+ // --- AdminEnergyConfigController(来源比例 + 每日上限,SC-A-25/A-26) ---
|
|
|
+ configController = new AdminEnergyConfigController();
|
|
|
+ sourceConfigMapper = mock(EnergySourceConfigMapper.class);
|
|
|
+ dailyLimitMapper = mock(DimensionDailyLimitMapper.class);
|
|
|
+ inject(configController, "energySourceConfigMapper", sourceConfigMapper);
|
|
|
+ inject(configController, "dimensionDailyLimitMapper", dailyLimitMapper);
|
|
|
+
|
|
|
+ // --- ProductDimensionConfigService(维度配置 CRUD,SC-A-25) ---
|
|
|
+ dimensionConfigService = new ProductDimensionConfigService();
|
|
|
+ dimensionConfigMapper = mock(ProductDimensionConfigMapper.class);
|
|
|
+ inject(dimensionConfigService, "baseMapper", dimensionConfigMapper);
|
|
|
+
|
|
|
+ // MyBatis-Plus TableInfo 预注册:使 LambdaQueryWrapper 无 Spring 上下文可解析列名
|
|
|
+ initTable(EnergySourceConfig.class);
|
|
|
+ initTable(DimensionDailyLimit.class);
|
|
|
+ initTable(ProductDimensionConfig.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 正常流(结构锚点,SC-A-25/26 主流程 1:CRUD 可用) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-25 正常流(结构锚点):DimensionConfigController 暴露 list/get/save/delete/tree")
|
|
|
+ void dimensionConfigController_exposesRequiredEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(DimensionConfigController.class, "/list"),
|
|
|
+ "DimensionConfigController 应暴露 POST /api/admin/dimension-config/list");
|
|
|
+ assertTrue(hasEndpoint(DimensionConfigController.class, "/get"),
|
|
|
+ "DimensionConfigController 应暴露 POST /api/admin/dimension-config/get");
|
|
|
+ assertTrue(hasEndpoint(DimensionConfigController.class, "/save"),
|
|
|
+ "DimensionConfigController 应暴露 POST /api/admin/dimension-config/save");
|
|
|
+ assertTrue(hasEndpoint(DimensionConfigController.class, "/delete"),
|
|
|
+ "DimensionConfigController 应暴露 POST /api/admin/dimension-config/delete");
|
|
|
+ assertTrue(hasEndpoint(DimensionConfigController.class, "/tree"),
|
|
|
+ "DimensionConfigController 应暴露 POST /api/admin/dimension-config/tree");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-09/SC-A-25 正常流(结构锚点):DimensionAdminController 暴露 norm + data-source")
|
|
|
+ void dimensionAdminController_exposesNormAndDataSourceEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(DimensionAdminController.class, "/norm/list")
|
|
|
+ && hasEndpoint(DimensionAdminController.class, "/norm/save")
|
|
|
+ && hasEndpoint(DimensionAdminController.class, "/norm/delete"),
|
|
|
+ "DimensionAdminController 应暴露 /api/admin/dimension/norm/{list,save,delete}");
|
|
|
+ assertTrue(hasEndpoint(DimensionAdminController.class, "/data-source/list")
|
|
|
+ && hasEndpoint(DimensionAdminController.class, "/data-source/save")
|
|
|
+ && hasEndpoint(DimensionAdminController.class, "/data-source/delete")
|
|
|
+ && hasEndpoint(DimensionAdminController.class, "/data-source/detail"),
|
|
|
+ "DimensionAdminController 应暴露 /api/admin/dimension/data-source/{list,save,delete,detail}");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-25 正常流(结构锚点):DimensionWeightController 暴露 save/list")
|
|
|
+ void dimensionWeightController_exposesSaveAndList() {
|
|
|
+ assertTrue(hasEndpoint(DimensionWeightController.class, "/save")
|
|
|
+ && hasEndpoint(DimensionWeightController.class, "/list"),
|
|
|
+ "DimensionWeightController 应暴露 /api/admin/dimension-weight/{save,list}");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("SC-A-25/SC-A-26 正常流(结构锚点):AdminEnergyConfigController 暴露来源比例 + 每日上限")
|
|
|
+ void adminEnergyConfigController_exposesSourceAndDailyLimitEndpoints() {
|
|
|
+ assertTrue(hasEndpoint(AdminEnergyConfigController.class, "/source-list")
|
|
|
+ && hasEndpoint(AdminEnergyConfigController.class, "/source-save")
|
|
|
+ && hasEndpoint(AdminEnergyConfigController.class, "/source-delete"),
|
|
|
+ "AdminEnergyConfigController 应暴露 /api/admin/energy-config/source-{list,save,delete}");
|
|
|
+ assertTrue(hasEndpoint(AdminEnergyConfigController.class, "/daily-limits")
|
|
|
+ && hasEndpoint(AdminEnergyConfigController.class, "/daily-limit-save"),
|
|
|
+ "AdminEnergyConfigController 应暴露 /api/admin/energy-config/daily-limit{,-save}");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(TC-A-25-E):维度权重非法(负/超界/和≠100%)→ 400 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 异常:权重和≠100% → 400 拦截,不落库")
|
|
|
+ void weightSave_sumNot100_returns400() {
|
|
|
+ // 60 + 30 = 90 ≠ 100
|
|
|
+ Result<Void> r = weightController.save(weightParams(Arrays.asList(
|
|
|
+ weight("body", 60), weight("mind", 30))));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 权重和≠100%(60+30=90)应 400 拦截(TC-A-25-E),"
|
|
|
+ + "当前实现未校验、直接落库返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 异常:权重为负 → 400 拦截")
|
|
|
+ void weightSave_negativeWeight_returns400() {
|
|
|
+ Result<Void> r = weightController.save(weightParams(
|
|
|
+ Collections.singletonList(weight("body", -10))));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 负权重(-10)应 400 拦截(TC-A-25-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 异常:权重超界(>100)→ 400 拦截")
|
|
|
+ void weightSave_overBoundWeight_returns400() {
|
|
|
+ Result<Void> r = weightController.save(weightParams(
|
|
|
+ Collections.singletonList(weight("body", 120))));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 超界权重(120>100)应 400 拦截(TC-A-25-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(TC-A-25-E):每日上限非法(负/超界)→ 400 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 异常:每日上限为负 → 400 拦截")
|
|
|
+ void dailyLimitSave_negativeLimit_returns400() {
|
|
|
+ DailyLimitSaveRequest req = new DailyLimitSaveRequest();
|
|
|
+ req.setLimits(Collections.singletonList(limit("body", -5)));
|
|
|
+ Result<String> r = configController.dailyLimitSave(req);
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 每日上限(-5)应 400 拦截(TC-A-25-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 异常:每日上限超界 → 400 拦截")
|
|
|
+ void dailyLimitSave_overBoundLimit_returns400() {
|
|
|
+ DailyLimitSaveRequest req = new DailyLimitSaveRequest();
|
|
|
+ req.setLimits(Collections.singletonList(limit("body", 100000)));
|
|
|
+ Result<String> r = configController.dailyLimitSave(req);
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 每日上限(100000)超界应 400 拦截(TC-A-25-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 异常流(TC-A-26-E):来源比例非法(负/超 1/和≠100)→ 400 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-26-E 异常:来源比例和≠100 → 400 拦截")
|
|
|
+ void sourceSave_ratioSumNot100_returns400() {
|
|
|
+ // 60 + 30 = 90 ≠ 100
|
|
|
+ Result<String> r = configController.sourceSave(ratioRequest(60, 30));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 来源比例和≠100(60+30=90)应 400 拦截(TC-A-26-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-26-E 异常:来源比例为负 → 400 拦截")
|
|
|
+ void sourceSave_negativeRatio_returns400() {
|
|
|
+ Result<String> r = configController.sourceSave(ratioRequest(-10, 0));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 负比例(-10)应 400 拦截(TC-A-26-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-26-E 异常:来源比例超 1(>100)→ 400 拦截")
|
|
|
+ void sourceSave_ratioOver100_returns400() {
|
|
|
+ Result<String> r = configController.sourceSave(ratioRequest(150, 0));
|
|
|
+ assertEquals(400, r.getCode(),
|
|
|
+ "RED[admin.dimension.config] 超界比例(150>100)应 400 拦截(TC-A-26-E),"
|
|
|
+ + "当前实现未校验,返回 " + r.getCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流(TC-A-25-E):删除被引用维度 → 拒绝,不级联破坏存量 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 边界:删除被引用维度(含子节点)应被拒绝,不级联删除")
|
|
|
+ void deleteConfig_referencedDimension_rejectedNotSilentDelete() {
|
|
|
+ // 维度 id=1 被引用(存在子节点,selectCount=1);若未被拦截则删除会成功(deleteById=1)
|
|
|
+ when(dimensionConfigMapper.selectCount(any())).thenReturn(1L);
|
|
|
+ when(dimensionConfigMapper.deleteById(any())).thenReturn(1);
|
|
|
+
|
|
|
+ boolean result = dimensionConfigService.deleteConfig(1L);
|
|
|
+
|
|
|
+ assertFalse(result,
|
|
|
+ "RED[admin.dimension.config] 删除被引用维度(含子节点)应被拒绝(返回 false),"
|
|
|
+ + "不得级联破坏存量(TC-A-25-E);当前实现未做引用检查,直接 removeById 返回 "
|
|
|
+ + result);
|
|
|
+ // 契约要求:删除前必须先做「被引用」检查(selectCount),且被引用时不执行删除(deleteById 不被调用)
|
|
|
+ verify(dimensionConfigMapper).selectCount(any());
|
|
|
+ verify(dimensionConfigMapper, never()).deleteById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 边界流(TC-A-25-E):并发改同配置 → 乐观锁/版本控制 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-E 边界:ProductDimensionConfig 应含 version 字段(乐观锁,并发不互相覆盖)")
|
|
|
+ void productDimensionConfig_hasVersionOptimisticLock() {
|
|
|
+ assertTrue(hasField(ProductDimensionConfig.class, "version"),
|
|
|
+ "RED[admin.dimension.config] ProductDimensionConfig 应含 version 字段"
|
|
|
+ + "(乐观锁版本控制,TC-A-25-E 并发改同配置不互相覆盖);当前实体无 version 字段");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 权限流(TC-A-25-P / TC-A-26-P):非 admin 操作 403 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-P/TC-A-26-P 权限:DimensionConfigController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void dimensionConfigController_hasRoleGuard_forNonAdmin403() {
|
|
|
+ assertTrue(hasRoleGuard(DimensionConfigController.class),
|
|
|
+ "RED[admin.dimension.config] DimensionConfigController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(TC-A-25-P);"
|
|
|
+ + "当前方法均无 role 参数");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-25-P 权限:DimensionWeightController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void dimensionWeightController_hasRoleGuard() {
|
|
|
+ assertTrue(hasRoleGuard(DimensionWeightController.class),
|
|
|
+ "RED[admin.dimension.config] DimensionWeightController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 操作 403(TC-A-25-P);"
|
|
|
+ + "当前方法均无 role 参数");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("TC-A-26-P 权限:AdminEnergyConfigController 应带 @RequestAttribute(\"role\") 守卫")
|
|
|
+ void adminEnergyConfigController_hasRoleGuard() {
|
|
|
+ assertTrue(hasRoleGuard(AdminEnergyConfigController.class),
|
|
|
+ "RED[admin.dimension.config] AdminEnergyConfigController 方法应带"
|
|
|
+ + " @RequestAttribute(\"role\") 守卫,非 admin 改全局 403(TC-A-26-P);"
|
|
|
+ + "当前方法均无 role 参数");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
+
|
|
|
+ private Map<String, Object> weightParams(List<Map<String, Object>> weights) {
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
+ params.put("targetType", "member");
|
|
|
+ params.put("targetId", 50L);
|
|
|
+ params.put("weights", weights);
|
|
|
+ return params;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> weight(String dimension, int weight) {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("dimension", dimension);
|
|
|
+ m.put("weight", weight);
|
|
|
+ m.put("enabled", true);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private EnergySourceConfigSaveRequest ratioRequest(int ratio1, int ratio2) {
|
|
|
+ EnergySourceConfigSaveRequest req = new EnergySourceConfigSaveRequest();
|
|
|
+ req.setSourceType("task");
|
|
|
+ req.setSourceId(100L);
|
|
|
+ req.setSourceName("任务A");
|
|
|
+ List<DimensionRatio> ratios = new ArrayList<>();
|
|
|
+ ratios.add(ratio(5L, ratio1));
|
|
|
+ if (ratio2 != 0) {
|
|
|
+ ratios.add(ratio(3L, ratio2));
|
|
|
+ }
|
|
|
+ req.setRatios(ratios);
|
|
|
+ return req;
|
|
|
+ }
|
|
|
+
|
|
|
+ private DimensionRatio ratio(Long dimensionId, int ratio) {
|
|
|
+ DimensionRatio r = new DimensionRatio();
|
|
|
+ r.setDimensionId(dimensionId);
|
|
|
+ r.setRatio(ratio);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+
|
|
|
+ private DimensionDailyLimit limit(String code, Integer dailyLimit) {
|
|
|
+ DimensionDailyLimit l = new DimensionDailyLimit();
|
|
|
+ l.setDimensionCode(code);
|
|
|
+ l.setDailyLimit(dailyLimit);
|
|
|
+ Date now = new Date();
|
|
|
+ l.setCreatedAt(now);
|
|
|
+ l.setUpdatedAt(now);
|
|
|
+ return l;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 反射辅助 ====================
|
|
|
+
|
|
|
+ /** 控制器是否暴露指定 POST 路径端点 */
|
|
|
+ private boolean hasEndpoint(Class<?> controller, String path) {
|
|
|
+ for (Method m : controller.getDeclaredMethods()) {
|
|
|
+ PostMapping pm = m.getAnnotation(PostMapping.class);
|
|
|
+ if (pm != null) {
|
|
|
+ for (String v : pm.value()) {
|
|
|
+ if (path.equals(v)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 控制器是否含 @RequestAttribute("role") 参数守卫 */
|
|
|
+ private boolean hasRoleGuard(Class<?> controller) {
|
|
|
+ for (Method m : controller.getDeclaredMethods()) {
|
|
|
+ for (java.lang.reflect.Parameter p : m.getParameters()) {
|
|
|
+ RequestAttribute ra = p.getAnnotation(RequestAttribute.class);
|
|
|
+ if (ra != null && "role".equals(ra.value())) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 实体是否含指定字段 */
|
|
|
+ private boolean hasField(Class<?> clazz, String fieldName) {
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ if (f.getName().equals(fieldName)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|