mockResult = new HashMap<>();
+ mockResult.put("totalMatched", 5);
+ mockResult.put("totalAssigned", 3);
+ mockResult.put("ruleCount", 2);
+
+ when(service.triggerAssignment()).thenReturn(mockResult);
+
+ mockMvc.perform(post("/assignRule/trigger"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.totalMatched").value(5))
+ .andExpect(jsonPath("$.data.totalAssigned").value(3))
+ .andExpect(jsonPath("$.data.ruleCount").value(2));
+ }
+}
diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java
new file mode 100644
index 0000000..5b48dd5
--- /dev/null
+++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/AssignRuleTriggerIntegrationTest.java
@@ -0,0 +1,58 @@
+package org.springblade.lims;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * 规则触发集成测试 — 验证端到端触发流程
+ *
+ * 场景:
+ * 1. 无规则场景: DB中没有启用规则,返回 totalMatched=0, totalAssigned=0, ruleCount=0
+ * 2. 基础触发: 调用 /assignRule/trigger 返回标准响应结构
+ * 3. 重复触发的幂等性: 连续调用两次返回结构一致
+ *
+ * 注意: 需要完整Spring上下文 + 数据库环境才能真实执行分配逻辑。
+ * 在无数据库的测试环境中,默认走"无规则"或空查询路径。
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class AssignRuleTriggerIntegrationTest {
+
+ @Autowired
+ private TestRestTemplate restTemplate;
+
+ @Test
+ void testTrigger_basicInvocation_shouldReturnResultStructure() {
+ ResponseEntity response = restTemplate.postForEntity("/assignRule/trigger", null, String.class);
+
+ assertThat(response.getStatusCodeValue()).isEqualTo(200);
+ assertThat(response.getBody()).contains("totalMatched");
+ assertThat(response.getBody()).contains("totalAssigned");
+ assertThat(response.getBody()).contains("ruleCount");
+ }
+
+ @Test
+ void testTrigger_noEnabledRules_shouldReturnZeroCounts() {
+ // 无数据库环境或数据库中无启用规则 → 返回0
+ ResponseEntity response = restTemplate.postForEntity("/assignRule/trigger", null, String.class);
+
+ assertThat(response.getStatusCodeValue()).isEqualTo(200);
+ assertThat(response.getBody()).contains("totalMatched");
+ assertThat(response.getBody()).contains("totalAssigned");
+ }
+
+ @Test
+ void testTrigger_idempotent_shouldNotThrowOnRepeatedCall() {
+ // 幂等性: 连续触发不抛异常
+ ResponseEntity first = restTemplate.postForEntity("/assignRule/trigger", null, String.class);
+ ResponseEntity second = restTemplate.postForEntity("/assignRule/trigger", null, String.class);
+
+ assertThat(first.getStatusCodeValue()).isEqualTo(200);
+ assertThat(second.getStatusCodeValue()).isEqualTo(200);
+ }
+
+}
diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java
new file mode 100644
index 0000000..403ece0
--- /dev/null
+++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/BaseLimsTest.java
@@ -0,0 +1,14 @@
+package org.springblade.lims;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class BaseLimsTest {
+ @Test
+ public void contextLoads() {
+ assert true;
+ }
+}
diff --git a/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java b/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java
new file mode 100644
index 0000000..79d94f0
--- /dev/null
+++ b/lab-service/lab-lims/src/test/java/org/springblade/lims/drools/AssignRuleDroolsServiceTest.java
@@ -0,0 +1,193 @@
+package org.springblade.lims.drools;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springblade.lims.entry.AssignRule;
+import org.springblade.lims.entry.Examine;
+import org.springblade.lims.service.IExamineItemService;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * AssignRuleDroolsService 单元测试
+ *
+ * 覆盖6个场景:
+ * 1. generateDrl null条件
+ * 2. generateDrl 空JSON
+ * 3. generateDrl sampleSource条件
+ * 4. generateDrl 全部条件
+ * 5. executeDrl 匹配命中
+ * 6. executeDrl 无匹配
+ */
+class AssignRuleDroolsServiceTest {
+
+ private AssignRuleDroolsService service;
+
+ @MockBean
+ private IExamineItemService examineItemService;
+
+ @BeforeEach
+ void setUp() {
+ service = new AssignRuleDroolsService(new ObjectMapper(), examineItemService);
+ }
+
+ // ── DRL生成测试 ──
+
+ @Test
+ void generateDrl_nullConditions_shouldProduceBasicDrl() {
+ AssignRule rule = new AssignRule();
+ rule.setConditions(null);
+
+ String drl = service.generateDrl(rule, "1");
+
+ assertThat(drl).contains("rule \"rule-1\"");
+ assertThat(drl).contains("$e: Examine(isDistribute == null || isDistribute == 0)");
+ assertThat(drl).contains("matchedIds.add($e.getId())");
+ // 不应包含额外约束
+ assertThat(drl).doesNotContain("simpleSource in");
+ assertThat(drl).doesNotContain("examineItemId in");
+ assertThat(drl).doesNotContain("simpleCount");
+ }
+
+ @Test
+ void generateDrl_emptyConditions_shouldProduceBasicDrl() {
+ AssignRule rule = new AssignRule();
+ rule.setConditions("{}");
+
+ String drl = service.generateDrl(rule, "2");
+
+ assertThat(drl).contains("rule \"rule-2\"");
+ assertThat(drl).contains("$e: Examine(isDistribute == null || isDistribute == 0)");
+ assertThat(drl).doesNotContain("sampleSource in");
+ }
+
+ @Test
+ void generateDrl_sampleSourceCondition_shouldIncludeSimpleSourceConstraint() {
+ AssignRule rule = new AssignRule();
+ rule.setConditions("{\"sampleSource\":[\"血清\",\"全血\"]}");
+
+ String drl = service.generateDrl(rule, "3");
+
+ // 约束现在在Examine()括号内,使用字段名(无$e.前缀)
+ assertThat(drl).contains("Examine(isDistribute == null || isDistribute == 0, simpleSource in (\"血清\", \"全血\"))");
+ assertThat(drl).doesNotContain("examineItemId in");
+ assertThat(drl).doesNotContain("simpleCount");
+ }
+
+ @Test
+ void generateDrl_allConditions_shouldIncludeAllConstraints() {
+ AssignRule rule = new AssignRule();
+ rule.setConditions("{\"sampleSource\":[\"血清\",\"尿样\"],\"examineItem\":[1,2,3],\"sampleCountThreshold\":5}");
+
+ String drl = service.generateDrl(rule, "4");
+
+ assertThat(drl).contains("Examine(isDistribute == null || isDistribute == 0, simpleSource in (\"血清\", \"尿样\"), examineItemId in (1L, 2L, 3L), simpleCount != null && simpleCount >= 5)");
+ }
+
+ // ── DRL执行测试 ──
+
+ @Test
+ void executeDrl_withMatchingRecords_shouldReturnMatchedIds() {
+ // 规则: 匹配所有未分配记录
+ String drl = "" +
+ "package org.springblade.lims.drools;\n" +
+ "import org.springblade.lims.entry.Examine;\n" +
+ "import java.util.Set;\n" +
+ "global java.util.Set matchedIds;\n" +
+ "\n" +
+ "rule \"all-unassigned\"\n" +
+ " when\n" +
+ " $e: Examine(isDistribute == null || isDistribute == 0)\n" +
+ " then\n" +
+ " matchedIds.add($e.getId());\n" +
+ "end\n";
+
+ Examine matched1 = new Examine();
+ matched1.setId(100L);
+ matched1.setIsDistribute(0);
+ Examine matched2 = new Examine();
+ matched2.setId(200L);
+ matched2.setIsDistribute(null);
+ Examine unmatched = new Examine();
+ unmatched.setId(300L);
+ unmatched.setIsDistribute(1);
+
+ List candidates = Arrays.asList(matched1, matched2, unmatched);
+
+ List result = service.executeDrl(drl, candidates);
+
+ assertThat(result).containsExactlyInAnyOrder(100L, 200L);
+ assertThat(result).doesNotContain(300L);
+ }
+
+ @Test
+ void executeDrl_withNoMatchingRecords_shouldReturnEmpty() {
+ String drl = "" +
+ "package org.springblade.lims.drools;\n" +
+ "import org.springblade.lims.entry.Examine;\n" +
+ "import java.util.Set;\n" +
+ "global java.util.Set matchedIds;\n" +
+ "\n" +
+ "rule \"no-match\"\n" +
+ " when\n" +
+ " $e: Examine(isDistribute == 2)\n" +
+ " then\n" +
+ " matchedIds.add($e.getId());\n" +
+ "end\n";
+
+ Examine e = new Examine();
+ e.setId(1L);
+ e.setIsDistribute(0);
+
+ List result = service.executeDrl(drl, Collections.singletonList(e));
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void executeDrl_withEmptyCandidates_shouldReturnEmpty() {
+ String drl = "" +
+ "package org.springblade.lims.drools;\n" +
+ "import org.springblade.lims.entry.Examine;\n" +
+ "import java.util.Set;\n" +
+ "global java.util.Set matchedIds;\n" +
+ "\n" +
+ "rule \"any\"\n" +
+ " when\n" +
+ " $e: Examine()\n" +
+ " then\n" +
+ " matchedIds.add($e.getId());\n" +
+ "end\n";
+
+ List result = service.executeDrl(drl, new ArrayList<>());
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void executeDrl_withNullCandidates_shouldReturnEmpty() {
+ String drl = "" +
+ "package org.springblade.lims.drools;\n" +
+ "import org.springblade.lims.entry.Examine;\n" +
+ "import java.util.Set;\n" +
+ "global java.util.Set matchedIds;\n" +
+ "\n" +
+ "rule \"any\"\n" +
+ " when\n" +
+ " $e: Examine()\n" +
+ " then\n" +
+ " matchedIds.add($e.getId());\n" +
+ "end\n";
+
+ List result = service.executeDrl(drl, null);
+
+ assertThat(result).isEmpty();
+ }
+}
diff --git a/lab-service/lab-lims/src/test/resources/application-test.yml b/lab-service/lab-lims/src/test/resources/application-test.yml
new file mode 100644
index 0000000..17dc4b7
--- /dev/null
+++ b/lab-service/lab-lims/src/test/resources/application-test.yml
@@ -0,0 +1,8 @@
+spring:
+ main:
+ allow-bean-definition-overriding: true
+ autoconfigure:
+ exclude:
+ - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
+ - org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
+ - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
diff --git a/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java b/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java
index bcdbe73..fe79a12 100644
--- a/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java
+++ b/lab-service/lab-system/src/main/java/org/springblade/system/controller/RegionController.java
@@ -148,6 +148,40 @@ public class RegionController extends BladeController {
return R.data(list);
}
+ /**
+ * 根据区划编码批量查询
+ */
+ @GetMapping("/list-by-codes")
+ @ApiOperationSupport(order = 10)
+ @ApiOperation(value = "批量查询", notes = "传入codes(逗号分隔)")
+ public R> listByCodes(@RequestParam String codes) {
+ List list = regionService.list(Wrappers.query().lambda().in(Region::getCode, codes.split(",")));
+ return R.data(list);
+ }
+
+ /**
+ * 获取区划编码的祖先链
+ */
+ @GetMapping("/ancestor-codes")
+ @ApiOperationSupport(order = 11)
+ @ApiOperation(value = "祖先链", notes = "传入code")
+ public R> ancestorCodes(@RequestParam String code) {
+ Region region = regionService.getOne(Wrappers.query().lambda().eq(Region::getCode, code));
+ if (region == null) {
+ return R.data(new ArrayList<>());
+ }
+ String ancestors = region.getAncestors();
+ String[] parts = ancestors.split(",");
+ List result = new ArrayList<>();
+ for (String part : parts) {
+ if (!"0".equals(part) && !part.isEmpty()) {
+ result.add(part);
+ }
+ }
+ result.add(code);
+ return R.data(result);
+ }
+
/**
* 导入行政区划数据
*/