93 Commits

Author SHA1 Message Date
MHW
ec716df946 解决节点参数int默认值sql执行错误问题 2026-04-21 11:26:26 +08:00
abe0315536 调整行为树挂载到平台的逻辑,修复发送平台变量未正确渲染
Co-authored-by: Copilot <copilot@github.com>
2026-04-20 20:06:08 +08:00
4252b3f514 Merge remote-tracking branch 'origin/master' 2026-04-20 19:05:44 +08:00
MHW
c597c3c01f PUT接口改json 2026-04-20 17:10:01 +08:00
MHW
4933c2c14e 智唐任务解析 2026-04-20 16:59:52 +08:00
3d0ba1d77c Merge remote-tracking branch 'origin/master' 2026-04-20 16:22:50 +08:00
MHW
0cd8455c9d 添加behaviortreeId字段返回 2026-04-20 16:02:54 +08:00
ced438c725 Merge remote-tracking branch 'origin/master' 2026-04-20 14:56:46 +08:00
9bea71e414 优化平台参数处理逻辑,支持逗号分隔值的独立分组
Co-authored-by: Copilot <copilot@github.com>
2026-04-20 14:54:21 +08:00
MHW
2ea04b98f8 修改平台关联的行为树id 2026-04-20 14:33:12 +08:00
MHW
8afd87b690 细节修改 2026-04-20 14:16:28 +08:00
MHW
71bb45f6a0 实体补充 2026-04-20 10:31:09 +08:00
MHW
931804555f Merge remote-tracking branch 'origin/master' 2026-04-20 09:39:58 +08:00
MHW
ae01a2aa01 旧接口字段更新BUG修复 2026-04-20 09:38:58 +08:00
c3a6661d2a 化行为树加载
Co-authored-by: Copilot <copilot@github.com>
2026-04-17 21:43:53 +08:00
1de4f9db8d 添加反向树加载功能,重构节点和树的选择逻辑
Co-authored-by: Copilot <copilot@github.com>
2026-04-17 19:30:19 +08:00
65d99bb7a8 Merge remote-tracking branch 'origin/master' 2026-04-17 17:59:12 +08:00
MHW
39b04d8b73 旧接口字段更新BUG修复 2026-04-17 17:55:26 +08:00
4d5f966b0a Merge remote-tracking branch 'origin/master' 2026-04-17 15:47:01 +08:00
MHW
0cd142bb82 旧接口字段更新 2026-04-17 15:45:44 +08:00
bcaaf1da4f Merge remote-tracking branch 'origin/master' 2026-04-17 15:24:08 +08:00
MHW
d310c5ae7b Merge remote-tracking branch 'origin/master' 2026-04-17 14:53:00 +08:00
MHW
1e38170420 接口新增,知识图谱 2026-04-17 14:52:10 +08:00
4330be8d0e 优化节点初始化逻辑 2026-04-16 15:52:36 +08:00
28cd9c131b 补充类型 2026-04-16 13:46:59 +08:00
27a5dc8e02 修复节点添加和删除时的报错 2026-04-16 12:48:25 +08:00
243a42ca5c 优化场景和行为树列表项的选中状态显示 2026-04-16 11:25:10 +08:00
b56d57af44 调整行为树保存信息 2026-04-16 10:44:46 +08:00
012939829c Merge remote-tracking branch 'origin/master' 2026-04-16 10:20:03 +08:00
82027fc6aa 默认收起行为树场景列表 2026-04-16 10:19:53 +08:00
MHW
6610e06991 Merge remote-tracking branch 'origin/master' 2026-04-16 09:59:45 +08:00
MHW
40655dd557 新增行为树增加场景id字段 2026-04-16 09:59:34 +08:00
5079f17df0 更新返回按钮逻辑,添加从场景页面跳转的状态管理 2026-04-15 19:34:19 +08:00
913dea7afa 添加行为树页面的场景选择 2026-04-15 19:24:25 +08:00
6f48a06438 Merge remote-tracking branch 'origin/master' 2026-04-15 17:56:50 +08:00
6941954e14 调整行为树命令位置参数,增加位置标题样式并调整布局 2026-04-15 17:56:02 +08:00
MHW
2c834b9b2f 复制行为树区分多次复制同一行为树 2026-04-15 17:32:38 +08:00
MHW
447aef0fe3 Merge remote-tracking branch 'origin/master' 2026-04-15 17:30:39 +08:00
MHW
873e31970f Merge branch 'refs/heads/develop' 2026-04-15 17:30:30 +08:00
MHW
b39a85a074 复制行为树区分多次复制同一行为树 2026-04-15 17:30:25 +08:00
d8c3429163 Merge remote-tracking branch 'origin/master' 2026-04-15 17:22:30 +08:00
dbbc8a27c3 完善阵位分配命令的变量数据加载 2026-04-15 17:22:22 +08:00
MHW
8e96285c0a Merge remote-tracking branch 'origin/master' 2026-04-15 17:17:54 +08:00
MHW
559a550157 根据平台id获取该行为树的下属:BUG修复 2026-04-15 17:17:23 +08:00
a08e061979 更新 .gitignore 文件以排除新的缓存文件和锁定文件,类型文件项目运行时自动生成 2026-04-15 16:34:20 +08:00
8975777cd5 Merge remote-tracking branch 'origin/master' 2026-04-15 16:32:38 +08:00
MHW
f51619526a 根据平台id获取该行为树的下属:BUG修复 2026-04-15 16:28:49 +08:00
MHW
0691fddbf1 Merge remote-tracking branch 'origin/master' 2026-04-15 16:27:01 +08:00
MHW
2d60a8b90f 根据平台id获取该行为树的下属 2026-04-15 16:26:49 +08:00
e92423ee54 Merge remote-tracking branch 'origin/master' 2026-04-15 15:53:37 +08:00
99dfb6f009 为参数设置标签添加样式,限制标签宽度并处理文本溢出 2026-04-15 15:46:44 +08:00
MHW
4404d0e411 规则展示,bug修复 2026-04-15 15:30:20 +08:00
MHW
a67e3e42ba 优化规则代码,行为树的pagelist请求添加场景id与平台id条件 2026-04-15 13:54:09 +08:00
44ff489c08 调整文件结构 2026-04-15 13:42:07 +08:00
3c06e77c6d Merge remote-tracking branch 'origin/master' 2026-04-15 11:21:32 +08:00
e555c95058 修复转换数据不完整问题 2026-04-15 10:50:02 +08:00
MHW
8839c65d7f Merge remote-tracking branch 'origin/master' 2026-04-15 09:38:37 +08:00
MHW
f9eb10c783 根据场景id获取总指挥的行为树的原数据 2026-04-15 09:38:15 +08:00
949e059c8f 恢复挂载行为树请求 2026-04-14 18:08:03 +08:00
9858787570 Merge branch 'feature-transform-node-data' 2026-04-14 17:30:54 +08:00
5a8c707340 添加行为决策树管理的场景数据到节点数据的自动转换逻辑 2026-04-14 17:27:31 +08:00
e1fe2cf7da Merge remote-tracking branch 'origin/master' into feature-transform-node-data 2026-04-14 17:03:50 +08:00
MHW
3ae6a693e1 BUG修复:字段映射不上 2026-04-14 17:00:49 +08:00
MHW
178bceabbb Merge remote-tracking branch 'origin/master' 2026-04-14 16:46:10 +08:00
MHW
6969fe5744 技术要求:规则展示表格接口 2026-04-14 16:45:49 +08:00
d43effe612 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-14 16:45:10 +08:00
f66d707520 Merge remote-tracking branch 'origin/master' into feature-transform-node-data 2026-04-14 16:30:13 +08:00
MHW
e82455a220 获取场景关系 2026-04-14 15:10:24 +08:00
2b2a11831d 支持在通信节点右键菜单中挂载行为树 2026-04-14 15:09:50 +08:00
a10b16a0ff Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-14 13:35:50 +08:00
85b4a7dd2f feat: 增加通信关系图随机生成及后端数据加载功能
- 新增 data-converter.ts,实现通信记录到GraphContainer 的转换及自动布局算法
- 新增random-data-generator.ts,用于生成测试用的随机通信拓扑数据
- communication.vue中集成“随机生成”与“从后端加载”按钮及相关逻辑
- 支持多连通分量检测、层级布局计算及节点防重叠处理
2026-04-14 11:39:22 +08:00
MHW
835bb56851 Merge branch 'refs/heads/test-platform' 2026-04-14 09:34:42 +08:00
MHW
eff0b87da8 逻辑修改 2026-04-13 17:26:56 +08:00
a277fe22a6 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-13 16:34:30 +08:00
836e42625e Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-13 16:29:49 +08:00
69c01bc62b Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-13 15:30:50 +08:00
bd62efca3f Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-13 15:07:15 +08:00
ee019d3c0f Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-09 16:19:08 +08:00
80fa2de819 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-01 11:33:31 +08:00
94b15ef412 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-04-01 09:14:06 +08:00
4e28782d59 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-31 14:33:13 +08:00
615eac3bfe Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-31 11:14:44 +08:00
e24920acf1 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-31 11:11:50 +08:00
1c562c134c Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-27 13:29:42 +08:00
c6c38332d9 Merge branches 'master' and 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-27 10:06:41 +08:00
f51b27dc7c Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-27 09:33:59 +08:00
a6c6cf2427 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-27 09:23:04 +08:00
373dc390fa Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-26 23:37:03 +08:00
2ed1129fe4 Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-20 16:06:00 +08:00
ce4c23eff8 save 2026-03-17 09:19:00 +08:00
9f4ef1ab94 save 2026-03-17 09:18:50 +08:00
c800811bed Merge branch 'master' of http://101.43.238.71:3000/zouju/auto-solution 2026-03-17 09:18:05 +08:00
235fd9b6e1 更新数据格式 2026-03-16 11:57:20 +08:00
116 changed files with 16242 additions and 697 deletions

14
.gitignore vendored
View File

@@ -51,3 +51,17 @@ nbdist/
!*/build/*.java
!*/build/*.html
!*/build/*.xml
modeler/pnpm-lock.yaml
# Application configuration files (may contain sensitive info)
**/application.yml
**/application-*.yml
!**/application-example.yml
######################################################################
# Mock bundled cache files
**/*.bundled_*.mjs
**/__*.bundled_*.mjs
**/___*.bundled_*.mjs
modeler/types/components.d.ts
modeler/types/auto-imports.d.ts

View File

@@ -81,6 +81,21 @@
<artifactId>solution-scene</artifactId>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security 测试支持(如果接口有权限控制) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -3,19 +3,14 @@ package com.solution.web.controller.behaviour;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.solution.system.domain.PlatFormDTO;
import com.solution.web.core.BehaviortreeProcessor;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.checkerframework.checker.units.qual.A;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import com.solution.common.annotation.Log;
import com.solution.common.core.controller.BaseController;
import com.solution.common.core.domain.AjaxResult;
@@ -92,7 +87,7 @@ public class BehaviortreeController extends BaseController
@PreAuthorize("@ss.hasPermi('system:behaviortree:add')")
@Log(title = "行为树主", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody Behaviortree behaviortree)
public AjaxResult add(@RequestBody Behaviortree behaviortree)
{
return toAjax(behaviortreeProcessor.create(behaviortree));
}
@@ -135,17 +130,36 @@ public class BehaviortreeController extends BaseController
}
/**
* 根据行为树id获取该行为树的下属
* @param treeId
* 根据平台id获取该行为树的下属
* @param platformId
* @return
*/
@ApiOperation("根据行为树id获取该行为树的下属")
@ApiOperation("根据平台id获取该行为树的下属")
@PreAuthorize("@ss.hasPermi('system:behaviortree:query')")
@GetMapping("/underling/{treeId}")
public AjaxResult getUnderling(@PathVariable Integer treeId)
@GetMapping("/underling/{platformId}")
public AjaxResult getUnderling(@PathVariable Integer platformId)
{
return success(behaviortreeService.getUnderling(treeId));
return success(behaviortreeService.getUnderling(platformId));
}
/**
* 根据场景id获取总指挥的行为树的原数据
* @return
*/
@ApiOperation("根据场景id获取总指挥的行为树的原数据")
@GetMapping("/commander/{scenarioId}")
public AjaxResult getCommander(@PathVariable Integer scenarioId){
return success(behaviortreeService.getCommander(scenarioId));
}
/**
* 修改平台关联的行为树id
* @param platFormDTO
* @return
*/
@ApiOperation("修改平台关联的行为树id")
@PutMapping("/behaviortreeId")
public AjaxResult updateBehaviortreeId(@RequestBody PlatFormDTO platFormDTO){
return toAjax(behaviortreeService.updateBehaviortreeId(platFormDTO));
}
}

View File

@@ -6,7 +6,13 @@ import com.solution.common.core.domain.AjaxResult;
import com.solution.common.core.page.TableDataInfo;
import com.solution.common.enums.BusinessType;
import com.solution.rule.domain.Rule;
import com.solution.rule.domain.config.RuleConfig;
import com.solution.rule.domain.config.RuleConfigQuery;
import com.solution.rule.domain.config.RuleParamMeta;
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
import com.solution.rule.domain.config.vo.RuleGraphVO;
import com.solution.rule.service.IRuleService;
import com.solution.rule.service.IRuleConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -18,12 +24,14 @@ import java.util.List;
@Api("红蓝对抗规则管理")
@RestController
@RequestMapping("/api/system/rule")
public class RuleController extends BaseController {
public class RuleController extends BaseController {
@Autowired
private IRuleService ruleService;
@Autowired
private IRuleConfigService ruleConfigService;
@PreAuthorize("@ss.hasPermi('system:rule:list')")
/*@PreAuthorize("@ss.hasPermi('system:rule:list')")
@GetMapping("/list")
@ApiOperation("查询规则列表")
public TableDataInfo list(Rule rule) {
@@ -61,5 +69,77 @@ public class RuleController extends BaseController {
@ApiOperation("删除规则")
public AjaxResult remove(@PathVariable Integer[] ids) {
return toAjax(ruleService.deleteRuleByIds(ids));
}*/
@PreAuthorize("@ss.hasPermi('system:rule:list')")
@GetMapping("/config/list")
@ApiOperation("查询规则聚合列表")
public TableDataInfo configList(RuleConfigQuery query) {
startPage();
return getDataTable(ruleConfigService.selectRuleConfigList(query));
}
@PreAuthorize("@ss.hasPermi('system:rule:list')")
@GetMapping("/config/graph")
@ApiOperation("规则知识图谱(与列表相同的分页与筛选条件)")
public AjaxResult configGraph(RuleConfigQuery query) {
startPage();
List<RuleConfig> list = ruleConfigService.selectRuleConfigList(query);
RuleGraphVO graph = ruleConfigService.buildKnowledgeGraph(list);
return success(graph);
}
@PreAuthorize("@ss.hasPermi('system:rule:list')")
@GetMapping("/config/graph/four-blocks")
@ApiOperation("四块规则知识图谱(装备/目标/阵位/航迹;参数值与运行时 globalParams 一致)")
public AjaxResult configGraphFourBlocks() {
RuleFourBlocksGraphVO vo = ruleConfigService.buildFourBlocksKnowledgeGraph();
return success(vo);
}
@PreAuthorize("@ss.hasPermi('system:rule:query')")
@GetMapping("/config/{ruleCode}")
@ApiOperation("查询规则聚合详情")
public AjaxResult configInfo(@PathVariable String ruleCode) {
return success(ruleConfigService.selectRuleConfigByCode(ruleCode));
}
@PreAuthorize("@ss.hasPermi('system:rule:add')")
@Log(title = "规则聚合管理", businessType = BusinessType.INSERT)
@PostMapping("/config")
@ApiOperation("新增规则聚合")
public AjaxResult addConfig(@RequestBody RuleConfig ruleConfig) {
return toAjax(ruleConfigService.insertRuleConfig(ruleConfig));
}
@PreAuthorize("@ss.hasPermi('system:rule:edit')")
@Log(title = "规则聚合管理", businessType = BusinessType.UPDATE)
@PutMapping("/config")
@ApiOperation("修改规则聚合")
public AjaxResult editConfig(@RequestBody RuleConfig ruleConfig) {
return toAjax(ruleConfigService.updateRuleConfig(ruleConfig));
}
@PreAuthorize("@ss.hasPermi('system:rule:remove')")
@Log(title = "规则聚合管理", businessType = BusinessType.DELETE)
@DeleteMapping("/config/{ruleCodes}")
@ApiOperation("删除规则聚合")
public AjaxResult removeConfig(@PathVariable String[] ruleCodes) {
return toAjax(ruleConfigService.deleteRuleConfigByCodes(ruleCodes));
}
@PreAuthorize("@ss.hasPermi('system:rule:query')")
@GetMapping("/config/dict/{dictType}")
@ApiOperation("按类型查询规则字典")
public AjaxResult dict(@PathVariable String dictType) {
return success(ruleConfigService.selectDictByType(dictType));
}
@PreAuthorize("@ss.hasPermi('system:rule:query')")
@GetMapping("/config/param-meta")
@ApiOperation("查询参数元数据")
public AjaxResult paramMeta() {
List<RuleParamMeta> metas = ruleConfigService.selectParamMetaList();
return success(metas);
}
}

View File

@@ -64,4 +64,15 @@ public class SceneController extends BaseController {
public AjaxResult getAllTree(@PathVariable Integer id){
return success(sceneService.getAllTree(id));
}
/**
* 根据场景id获取场景下所有关系
* @param id
* @return
*/
@GetMapping("/getAllRelation/{id}")
@ApiOperation("根据场景id获取场景下所有关系")
public AjaxResult getAllRelation(@PathVariable Integer id){
return success(sceneService.getAllRelation(id));
}
}

View File

@@ -243,7 +243,28 @@ public class BehaviortreeProcessor {
String newEnglishName = englishName + "_" + behaviortree.getId();
behaviortree.setEnglishName(newEnglishName);
behaviortree.setName(newName);
//不做前置判断必走数据库count
//获取行为树重复名称个数
Integer num = behaviortreeService.getCountName(newName);
if(num > 0){
// 从2开始尝试因为基础名称已经存在
int count = 2;
String finalName;
String finalEnglishName;
do {
finalName = newName + "_" + count;
finalEnglishName = newEnglishName + "_" + count;
// 检查当前生成的名称是否存在
num = behaviortreeService.getCountName(finalName);
count++;
} while(num > 0);
behaviortree.setName(finalName);
behaviortree.setEnglishName(finalEnglishName);
} else {
behaviortree.setName(newName);
behaviortree.setEnglishName(newEnglishName);
}
return this.create(behaviortree);
}
}

View File

@@ -6,10 +6,8 @@ spring:
druid:
# 主库数据源
master:
# url: jdbc:mysql://192.168.166.71:3306/behaviortreedb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/autosolution_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://127.0.0.1:3306/autosolution_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
# password: 123456
password: 1234
# 从库数据源
slave:

View File

@@ -32,7 +32,7 @@ public class Nodeparameter extends BaseEntity
@Excel(name = "节点实例设置的具体参数值 (覆盖模板默认值)")
private String value;
private int groupIndex;
private Integer groupIndex;
public void setId(Long id)
{
@@ -48,6 +48,14 @@ public class Nodeparameter extends BaseEntity
return treeId;
}
public Integer getGroupIndex() {
return groupIndex;
}
public void setGroupIndex(Integer groupIndex) {
this.groupIndex = groupIndex;
}
public void setTreeId(Long treeId) {
this.treeId = treeId;
}
@@ -82,13 +90,6 @@ public class Nodeparameter extends BaseEntity
return value;
}
public int getGroupIndex() {
return groupIndex;
}
public void setGroupIndex(int groupIndex) {
this.groupIndex = groupIndex;
}
@Override
public String toString() {

View File

@@ -0,0 +1,12 @@
package com.solution.system.domain;
import lombok.Data;
@Data
public class PlatFormDTO {
private Integer id;
private Integer behaviortreeId;
}

View File

@@ -0,0 +1,46 @@
package com.solution.system.domain;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 收集数据用来生成总指挥官
*/
@Data
public class PlatformChiefCommander {
/**
* Primary Key 主键ID自增
*/
private Integer id;
/**
* Create Time 创建时间
*/
private LocalDateTime createTime;
/**
* 名称
*/
private String name;
/**
* 时间
*/
private Integer time;
/**
* 指令
*/
private String command;
/**
* 下属指挥官
*/
private String subordinateCommander;
/**
* 场景ID
*/
private Integer scenarioId;
}

View File

@@ -0,0 +1,34 @@
package com.solution.system.domain;
import lombok.Data;
/**
* 平台视图对象 platform
*/
@Data
public class PlatformVO
{
/** 主键 */
private Integer id;
/** 名称 */
private String name;
/** 描述 */
private String description;
/** 平台出现在想定上的想定id */
private Integer scenarioId;
/** 经度 */
private String longitude;
/** 纬度 */
private String latitude;
/** 平台类型 */
private String platformType;
/** 海拔 */
private Float altitude;
}

View File

@@ -39,11 +39,11 @@ public class Treenodeinstance extends BaseEntity
private Long preconditionTempleteId;
@Excel(name = "节点介绍")
private String desciption;
private String nodeValue;
/** $column.columnComment */
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
private String uuid;
@Excel(name = "是否外部节点", readConverterExp = "0=否,1=是")
private String isExtern;
public void setId(Long id)
{
@@ -105,36 +105,33 @@ public class Treenodeinstance extends BaseEntity
return preconditionTempleteId;
}
public void setUuid(String uuid)
{
this.uuid = uuid;
public String getNodeValue() {
return nodeValue;
}
public String getUuid()
{
return uuid;
public void setNodeValue(String nodeValue) {
this.nodeValue = nodeValue;
}
public String getDesciption() {
return desciption;
public String getIsExtern() {
return isExtern;
}
public void setDesciption(String desciption) {
this.desciption = desciption;
public void setIsExtern(String isExtern) {
this.isExtern = isExtern;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("treeId", getTreeId())
.append("templateId", getTemplateId())
.append("instanceName", getInstanceName())
.append("isRoot", getIsRoot())
.append("preconditionTempleteId", getPreconditionTempleteId())
.append("uuid", getUuid())
.append("desciption", getDesciption())
.toString();
return "Treenodeinstance{" +
"id=" + id +
", treeId=" + treeId +
", templateId=" + templateId +
", instanceName='" + instanceName + '\'' +
", isRoot=" + isRoot +
", preconditionTempleteId=" + preconditionTempleteId +
", nodeValue='" + nodeValue + '\'' +
", isExtern='" + isExtern + '\'' +
'}';
}
}

View File

@@ -3,6 +3,7 @@ package com.solution.system.mapper;
import java.util.List;
import com.solution.system.domain.Behaviortree;
import com.solution.system.domain.PlatformChiefCommander;
/**
* 行为树主Mapper接口
@@ -62,4 +63,18 @@ public interface BehaviortreeMapper
* @return 结果
*/
public int deleteBehaviortreeByIds(Long[] ids);
/**
* 根据场景id获取总指挥的行为树的原数据
* @param scenarioId
* @return
*/
List<PlatformChiefCommander> getCommander(Integer scenarioId);
/**
* 获取行为树重复名称个数
* @param newName
* @return
*/
Integer getCountName(String newName);
}

View File

@@ -1,24 +1,43 @@
package com.solution.system.mapper;
import com.solution.system.domain.PlatFormDTO;
import com.solution.system.domain.PlatformTree;
import com.solution.system.domain.PlatformVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PlatformMapper {
/**
* 根据行为树id获取行为树所属平台
* @param id
* @return
*/
PlatformTree getPlatformByTreeId(Integer id);
/**
* 根据下属平台英文名获取中文名返回
* @param underlingEnglishName
* @return
*/
List<String> selectPlatformChineseName(List<String> underlingEnglishName);
List<String> selectPlatformChineseName(@Param("underlingEnglishName") List<String> underlingEnglishName);
/**
* 根据平台id获取平台实体
* @param platformId
* @return
*/
PlatformTree getPlatformById(Integer platformId);
/**
* 根据平台英文名查询该平台实体
* @param underlingEnglishName
* @return
*/
List<PlatformVO> getPlatformByEnglishName(@Param("underlingEnglishName") List<String> underlingEnglishName);
/**
* 修改平台关联的行为树id
* @param platFormDTO
* @return
*/
Integer updateBehaviortreeId(PlatFormDTO platFormDTO);
}

View File

@@ -3,6 +3,9 @@ package com.solution.system.service;
import java.util.List;
import com.solution.system.domain.Behaviortree;
import com.solution.system.domain.PlatFormDTO;
import com.solution.system.domain.PlatformChiefCommander;
import com.solution.system.domain.PlatformVO;
/**
* 行为树主Service接口
@@ -33,7 +36,6 @@ public interface IBehaviortreeService
/**
* 新增行为树主
*
* @param behaviortree 行为树主
* @return 结果
*/
@@ -64,9 +66,30 @@ public interface IBehaviortreeService
public int deleteBehaviortreeById(Long id);
/**
* 根据行为树id获取该行为树的下属
* @param treeId
* 根据平台id获取该行为树的下属
* @param platformId
* @return
*/
List<String> getUnderling(Integer treeId);
List<PlatformVO> getUnderling(Integer platformId);
/**
* 根据场景id获取总指挥的行为树的原数据
* @param scenarioId
* @return
*/
List<PlatformChiefCommander> getCommander(Integer scenarioId);
/**
* 获取行为树重复名称个数
* @param newName
* @return
*/
Integer getCountName(String newName);
/**
* 修改平台关联的行为树id
* @param platFormDTO
* @return
*/
Integer updateBehaviortreeId(PlatFormDTO platFormDTO);
}

View File

@@ -5,13 +5,12 @@ import java.util.List;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.solution.common.constant.ExceptionConstants;
import com.solution.system.domain.PlatformTree;
import com.solution.system.domain.*;
import com.solution.system.mapper.PlatformCommunicationMapper;
import com.solution.system.mapper.PlatformMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.solution.system.mapper.BehaviortreeMapper;
import com.solution.system.domain.Behaviortree;
import com.solution.system.service.IBehaviortreeService;
/**
@@ -111,28 +110,60 @@ public class BehaviortreeServiceImpl implements IBehaviortreeService
}
/**
* 根据行为树id获取该行为树的下属
* @param treeId
* 根据平台id获取该行为树的下属
* @param platformId
* @return
*/
@Override
public List<String> getUnderling(Integer treeId) {
if(null == treeId){
public List<PlatformVO> getUnderling(Integer platformId) {
if(null == platformId){
throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION);
}
Behaviortree behaviortree = behaviortreeMapper.selectBehaviortreeById(Long.valueOf(treeId));
if(ObjectUtil.isEmpty(behaviortree) || null == behaviortree.getId()){
throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION);
}
//根据行为树id获取行为树所属平台
PlatformTree platform = platformMapper.getPlatformByTreeId(behaviortree.getPlatformId());
//根据平台id获取平台实体
PlatformTree platform = platformMapper.getPlatformById(platformId);
//根据平台name获取平台下属英文名
List<String> underlingEnglishName = platformCommunicationMapper.getUnderlingBytreeId(platform.getName());
//根据下属平台英文名获取中文名返回
List<String> underlingChineseName = platformMapper.selectPlatformChineseName(underlingEnglishName);
if(CollUtil.isEmpty(underlingChineseName)){
if(CollUtil.isEmpty(underlingEnglishName)){
throw new RuntimeException("该平台暂无下属");
}
return underlingChineseName;
//根据平台英文名查询该平台实体
List<PlatformVO> resultList = platformMapper.getPlatformByEnglishName(underlingEnglishName);
if(CollUtil.isEmpty(resultList)){
throw new RuntimeException("无法根据平台英文名获取平台实体");
}
return resultList;
}
/**
* 根据场景id获取总指挥的行为树的原数据
* @param scenarioId
* @return
*/
@Override
public List<PlatformChiefCommander> getCommander(Integer scenarioId) {
return behaviortreeMapper.getCommander(scenarioId);
}
/**
* 获取行为树重复名称个数
* @param newName
* @return
*/
@Override
public Integer getCountName(String newName) {
return behaviortreeMapper.getCountName(newName);
}
/**
* 修改平台关联的行为树id
* @param platFormDTO
* @return
*/
@Override
public Integer updateBehaviortreeId(PlatFormDTO platFormDTO) {
if(ObjectUtil.isEmpty(platFormDTO)){
throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION);
}
return platformMapper.updateBehaviortreeId(platFormDTO);
}
}

View File

@@ -14,7 +14,7 @@ import com.solution.system.service.INodeparameterService;
* @date 2026-02-05
*/
@Service
public class NodeparameterServiceImpl implements INodeparameterService
public class NodeparameterServiceImpl implements INodeparameterService
{
@Autowired
private NodeparameterMapper nodeparameterMapper;

View File

@@ -23,9 +23,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<sql id="selectBehaviortreeVo">
select id, name, description, created_at, updated_at, english_name, xml_content from behaviortree
select id, name, description, created_at, updated_at, english_name, xml_content, platform_id, scenario_id from behaviortree
</sql>
<select id="selectBehaviortreeList" parameterType="Behaviortree" resultMap="BehaviortreeResult">
<include refid="selectBehaviortreeVo"/>
<where>
@@ -35,6 +36,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updatedAt != null "> and updated_at = #{updatedAt}</if>
<if test="englishName != null and englishName != ''"> and english_name like concat('%', #{englishName}, '%')</if>
<if test="xmlContent != null and xmlContent != ''"> and xml_content = #{xmlContent}</if>
<if test="xmlContent != null and xmlContent != ''"> and xml_content = #{xmlContent}</if>
<if test="platformId != null">platform_id = #{platformId}</if>
<if test="scenarioId != null">scenario_id = #{scenarioId}</if>
</where>
</select>
@@ -42,6 +46,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
select id, name, description, created_at, updated_at, english_name,graph, xml_content ,platform_id, scenario_id from behaviortree
where id = #{id}
</select>
<select id="getCommander" resultType="com.solution.system.domain.PlatformChiefCommander"
parameterType="java.lang.Integer">
SELECT id,
create_time AS createTime,
name,time,command,
subordinate_commander AS subordinateCommander,
scenario_id AS scenarioId
FROM platform_chief_commander
WHERE scenario_id = #{scenarioId}
</select>
<select id="getCountName" resultType="java.lang.Integer" parameterType="java.lang.String">
SELECT count(*)
FROM behaviortree
WHERE name = #{name}
</select>
<insert id="insertBehaviortree" parameterType="Behaviortree" useGeneratedKeys="true" keyProperty="id">
insert into behaviortree
@@ -53,6 +72,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="englishName != null and englishName != ''">english_name,</if>
<if test="xmlContent != null">xml_content,</if>
<if test="platformId != null">platform_id,</if>
<if test="scenarioId != null">scenario_id,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null and name != ''">#{name},</if>
@@ -62,6 +82,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="englishName != null and englishName != ''">#{englishName},</if>
<if test="xmlContent != null">#{xmlContent},</if>
<if test="platformId != null">#{platformId},</if>
<if test="scenarioId != null">#{scenarioId},</if>
</trim>
</insert>

View File

@@ -18,15 +18,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</delete>
<sql id="selectNodeparameterVo">
select id, treeId, node_instance_id, param_def_id,`value`, group_index from nodeparameter
select id, tree_id, node_instance_id, param_def_id,`value`, group_index from nodeparameter
</sql>
<select id="selectNodeparameterList" parameterType="Nodeparameter" resultMap="NodeparameterResult">
<include refid="selectNodeparameterVo"/>
<where>
<where>
<if test="id != null"> and id = #{id}</if>
<if test="treeId != null"> and tree_id = #{treeId}</if>
<if test="nodeInstanceId != null "> and node_instance_id = #{nodeInstanceId}</if>
<if test="paramDefId != null "> and param_def_id = #{paramDefId}</if>
<if test="value != null and value != ''"> and value = #{value}</if>
<if test="value != null and value != ''"> and `value` = #{value}</if>
<if test="groupIndex != null"> and group_index = #{groupIndex}</if>
</where>
</select>

View File

@@ -5,9 +5,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<mapper namespace="com.solution.system.mapper.PlatformCommunicationMapper">
<select id="getUnderlingBytreeId" resultType="java.lang.String" parameterType="java.lang.Integer">
<select id="getUnderlingBytreeId" resultType="java.lang.String" parameterType="java.lang.String">
SELECT subordinate_platform
FROM platform_communication
WHERE subordinate_platform = #{name}
WHERE command_platform = #{name}
</select>
</mapper>

View File

@@ -3,14 +3,12 @@
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.solution.system.mapper.PlatformMapper">
<select id="getPlatformByTreeId" resultType="com.solution.system.domain.PlatformTree"
parameterType="java.lang.Integer">
SELECT id , name , description, scenario_id
FROM platform
<update id="updateBehaviortreeId">
UPDATE platform
SET behaviortree_id = #{behaviortreeId}
WHERE id = #{id}
</select>
</update>
<select id="selectPlatformChineseName" resultType="java.lang.String"
parameterType="java.util.List">
SELECT description
@@ -20,4 +18,22 @@
#{item}
</foreach>
</select>
<select id="getPlatformById" resultType="com.solution.system.domain.PlatformTree"
parameterType="java.lang.Integer">
SELECT id, name, description, scenario_id AS scenarioId
FROM platform
WHERE id = #{platformId}
</select>
<select id="getPlatformByEnglishName" resultType="com.solution.system.domain.PlatformVO">
SELECT id,name,description,
scenario_id AS scenarioId,
longitude,latitude,
platform_type AS platformType,
altitude
FROM platform
WHERE name IN
<foreach item="item" collection="underlingEnglishName" open="(" separator="," close=")">
#{item}
</foreach>
</select>
</mapper>

View File

@@ -20,7 +20,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectTemplateparameterdefList" parameterType="templateparameterdef" resultMap="TemplateparameterdefResult">
<include refid="selectTemplateparameterdefVo"/>
<where>
<where>
<if test="id != null"> and id = #{id}</if>
<if test="templateId != null "> and template_id = #{templateId}</if>
<if test="paramKey != null and paramKey != ''"> and param_key = #{paramKey}</if>
<if test="dataType != null and dataType != ''"> and data_type = #{dataType}</if>

View File

@@ -11,16 +11,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="instanceName" column="instance_name" />
<result property="isRoot" column="is_root" />
<result property="preconditionTempleteId" column="precondition_templete_id" />
<result property="uuid" column="uuid" />
<result property="desciption" column="desciption" />
<result property="nodeValue" column="node_value" />
<result property="isExtern" column="is_extern" />
</resultMap>
<delete id="deleteByTreeId">
delete from treenodeinstance where tree_id=#{treeId}
delete from treenodeinstance where tree_id = #{treeId}
</delete>
<sql id="selectTreenodeinstanceVo">
select id, tree_id, template_id, instance_name, is_root, precondition_templete_id, uuid,desciption from treenodeinstance
select id, tree_id, template_id, instance_name, is_root, precondition_templete_id, node_value,is_extern from treenodeinstance
</sql>
<select id="selectTreenodeinstanceList" parameterType="Treenodeinstance" resultMap="TreenodeinstanceResult">
@@ -31,8 +31,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="instanceName != null and instanceName != ''"> and instance_name like concat('%', #{instanceName}, '%')</if>
<if test="isRoot != null "> and is_root = #{isRoot}</if>
<if test="preconditionTempleteId != null "> and precondition_templete_id = #{preconditionTempleteId}</if>
<if test="uuid != null and uuid != ''"> and uuid = #{uuid}</if>
<if test="desciption != null and desciption != ''"> and desciption = #{desciption}</if>
<if test="nodeValue != null and nodeValue != ''"> and node_value = #{nodeValue}</if>
<if test="isExtern != null and isExtern != ''"> and is_extern = #{isExtern}</if>
</where>
</select>
@@ -49,8 +49,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="instanceName != null">instance_name,</if>
<if test="isRoot != null">is_root,</if>
<if test="preconditionTempleteId != null">precondition_templete_id,</if>
<if test="uuid != null">uuid,</if>
<if test="desciption != null">desciption,</if>
<if test="nodeValue != null">node_value,</if>
<if test="isExtern != null">is_extern,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="treeId != null">#{treeId},</if>
@@ -58,8 +58,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="instanceName != null">#{instanceName},</if>
<if test="isRoot != null">#{isRoot},</if>
<if test="preconditionTempleteId != null">#{preconditionTempleteId},</if>
<if test="uuid != null">#{uuid},</if>
<if test="desciption != null">#{desciption},</if>
<if test="nodeValue != null">#{node_value},</if>
<if test="isExtern != null">#{is_extern},</if>
</trim>
</insert>
@@ -71,8 +71,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="instanceName != null">instance_name = #{instanceName},</if>
<if test="isRoot != null">is_root = #{isRoot},</if>
<if test="preconditionTempleteId != null">precondition_templete_id = #{preconditionTempleteId},</if>
<if test="uuid != null">uuid = #{uuid},</if>
<if test="desciption != null">desciption = #{desciption},</if>
<if test="nodeValue != null">#{node_value},</if>
<if test="isExtern != null">#{is_extern},</if>
</trim>
where id = #{id}
</update>

View File

@@ -0,0 +1,28 @@
package com.solution.rule.controller;
import com.solution.rule.domain.tasks.Kj3PostResult;
import com.solution.rule.service.Kj3TaskDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/rule/kj3")
public class Kj3TaskDataController {
@Autowired
private Kj3TaskDataService kj3TaskDataService;
/**
* 接收外部传入的 kj-3 JSON筛选红方任务并转换后 POST 到目标地址。
*
* @param kj3Json 外部请求体 JSON
* @return 处理结果
*/
@PostMapping("/parse-and-post")
public Kj3PostResult parseAndPost(@RequestBody String kj3Json) {
return kj3TaskDataService.parseAndPostRedTasks(kj3Json);
}
}

View File

@@ -20,6 +20,16 @@ public class BasicPlatform implements Serializable {
private String description;
private Integer behaviortreeId;
public Integer getBehaviortreeId() {
return behaviortreeId;
}
public void setBehaviortreeId(Integer behaviortreeId) {
this.behaviortreeId = behaviortreeId;
}
public Integer getId() {
return id;
}

View File

@@ -0,0 +1,61 @@
package com.solution.rule.domain.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
@ApiModel("规则聚合对象")
public class RuleConfig {
@ApiModelProperty("主键ID")
private Long id;
@ApiModelProperty("规则编码")
private String ruleCode;
@ApiModelProperty("规则名称")
private String ruleName;
@ApiModelProperty("层级编码(task/action/platform)")
private String levelCode;
@ApiModelProperty("种类编码(select/assign/deploy/config/mode/spacetime/relation/limit)")
private String kindCode;
@ApiModelProperty("模块编码(equipment/target/position/track/group)")
private String moduleCode;
@ApiModelProperty("优先级(数字越小越先执行)")
private Integer priorityNo;
@ApiModelProperty("条件表达式")
private String conditionExpr;
@ApiModelProperty("动作表达式")
private String actionExpr;
@ApiModelProperty("版本号")
private Integer versionNo;
@ApiModelProperty("是否启用(1是0否)")
private Integer enabled;
@ApiModelProperty("备注")
private String remark;
@ApiModelProperty("创建时间")
private Date createdAt;
@ApiModelProperty("更新时间")
private Date updatedAt;
@ApiModelProperty("参数列表")
private List<RuleConfigParam> params;
@ApiModelProperty("适用任务类型编码列表")
private List<String> taskTypes;
}

View File

@@ -0,0 +1,34 @@
package com.solution.rule.domain.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("规则参数对象")
public class RuleConfigParam {
@ApiModelProperty("规则编码")
private String ruleCode;
@ApiModelProperty("参数键")
private String paramKey;
@ApiModelProperty("参数值")
private String paramVal;
@ApiModelProperty("值类型(string/number/bool/json)")
private String valType;
@ApiModelProperty("参数名称")
private String paramName;
@ApiModelProperty("排序号")
private Integer sortNo;
@ApiModelProperty("是否启用(1是0否)")
private Integer enabled;
@ApiModelProperty("备注")
private String remark;
}

View File

@@ -0,0 +1,28 @@
package com.solution.rule.domain.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("规则配置查询对象")
public class RuleConfigQuery {
@ApiModelProperty("规则编码")
private String ruleCode;
@ApiModelProperty("规则名称")
private String ruleName;
@ApiModelProperty("层级编码(task/action/platform)")
private String levelCode;
@ApiModelProperty("种类编码(select/assign/deploy/config/mode/spacetime/relation/limit)")
private String kindCode;
@ApiModelProperty("模块编码(equipment/target/position/track/group)")
private String moduleCode;
@ApiModelProperty("是否启用(1是0否)")
private Integer enabled;
}

View File

@@ -0,0 +1,16 @@
package com.solution.rule.domain.config;
import lombok.Data;
import java.io.Serializable;
/**
* rule_item_task_type 批量查询行
*/
@Data
public class RuleConfigTaskTypeRow implements Serializable {
private String ruleCode;
private String taskTypeCode;
}

View File

@@ -0,0 +1,28 @@
package com.solution.rule.domain.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("规则字典项")
public class RuleDictItem {
@ApiModelProperty("字典类型")
private String dictType;
@ApiModelProperty("字典编码")
private String dictCode;
@ApiModelProperty("字典名称")
private String dictName;
@ApiModelProperty("排序号")
private Integer sortNo;
@ApiModelProperty("是否启用(1是0否)")
private Integer enabled;
@ApiModelProperty("备注")
private String remark;
}

View File

@@ -0,0 +1,42 @@
package com.solution.rule.domain.config;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("规则参数元数据")
public class RuleParamMeta {
@ApiModelProperty("参数键")
private String paramKey;
@ApiModelProperty("参数名称")
private String label;
@ApiModelProperty("值类型(bool/number/enum/csv/string)")
private String valueType;
@ApiModelProperty("是否必填")
private Boolean required;
@ApiModelProperty("枚举可选值")
private List<String> enumOptions;
@ApiModelProperty("数值最小值")
private Double min;
@ApiModelProperty("数值最大值")
private Double max;
@ApiModelProperty("格式正则")
private String pattern;
@ApiModelProperty("示例值")
private String example;
@ApiModelProperty("说明")
private String description;
}

View File

@@ -0,0 +1,107 @@
package com.solution.rule.domain.config.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 与 {@code rules/rule.drl} 中「装备/目标/阵位/航迹」四个主规则一一对应,仅用于知识图谱分块展示(与 module_code 一致)。
*/
public enum RuleFourBlockDefinition {
EQUIPMENT(
"equipment",
"装备匹配",
100,
Arrays.asList(
"抽取蓝方任务与武器文案、红方装备类型与名称等,拼接为可对齐的蓝/红文本特征",
"规则槽关键词与兼容层叠加计分,再乘以全局权重得到每件红装的匹配总分",
"低于最低入选分则丢弃;并列最高分按既定策略决胜,并写入火力输入与任务名后缀"),
"DroolsFact(task != null)",
"mergeDefaultParams(globalParams); equipmentRule($fact, globalParams);"),
TARGET(
"target",
"目标匹配",
90,
Arrays.asList(
"根据命中率映射、射程解析与分配模式,确定每套红方可承载的目标数量与目标标识",
"向任务 execute 写入目标列表与默认行动类型等执行侧字段",
"命中率低于阈值时,在轮次与件数限制内补充选取装备并更新任务"),
"DroolsFact(task != null)",
"mergeDefaultParams(globalParams); target($fact, globalParams);"),
POSITION(
"position",
"阵位匹配",
80,
Arrays.asList(
"结合锚点模式、航向与部署距离区间、编队样式与间距,计算红方平台目标点位",
"按平台最小间距离散多个平台坐标,必要时叠加上述默认高度与跟随比例",
"若启用作战区约束,将越界阵位沿策略回拉到战争区内"),
"DroolsFact(task != null)",
"mergeDefaultParams(globalParams); position($fact, globalParams);"),
TRACK(
"track",
"航迹匹配",
70,
Arrays.asList(
"依据蓝方航迹与数据类型判定空中/地面航迹类别,选择对应变形算法生成航迹折线",
"生成航迹标识并与任务 execute 中的目标绑定移动路由",
"若启用航迹作战区约束,将航迹点约束在战争区多边形内"),
"DroolsFact(task != null)",
"mergeDefaultParams(globalParams); trackRoute($fact, globalParams);");

View File

@@ -0,0 +1,87 @@
package com.solution.rule.domain.config.graph;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
/**
* 冻结「参数键 → 四大展示块」归属(与 rule.drl / {@code 002_rule_seed_from_drl.sql} 中的 rule_item.module_code 一致)。
* 图谱优先按库表 rule_item_param.rule_code 所属模块归类;若某键仅存在于 globalParams 快照且无法关联模块时,再用本映射兜底。
*/
public final class RuleFourBlockParamMapping {
private static final Set<String> EQUIPMENT = set(
"weight", "minSelectedScore", "tieBreak", "outputDrawNameSuffix", "ruleSlotCount",
"blueRuleKeywords_1", "redRuleKeywords_1", "ruleScore_1",
"blueRuleKeywords_2", "redRuleKeywords_2", "ruleScore_2",
"blueRuleKeywords_3", "redRuleKeywords_3", "ruleScore_3",
"bluePlatformKeywords_air", "redPreferredWhenBlueAir", "airScore",
"airTaskKeywords", "airTaskScore",
"groundTaskKeywords", "redPreferredWhenGround", "groundScore",
"tankKeywords", "redMatchKeywords_tank", "tankScore",
"missileKeywords", "redMatchKeywords_missile", "missileScore");
private static final Set<String> TARGET = set(
"executeTypeDefault", "targetPickMode", "minTargetsPerRed", "maxTargetsPerRedCap",
"radToTargetsCsv", "rangeParseRegex", "rangeUnit", "minRangeToAllowAssignKm",
"redHitRateThreshold", "maxExtraWeaponsPerTask", "maxSupplementRounds", "extraPickMinScore");
private static final Set<String> POSITION = set(
"positionRuleEnabled", "positionAnchorMode", "trackPointDirectionMode",
"fallbackBearingDeg", "deployDistanceKmMin", "deployDistanceKmMax", "deployDistanceKmDefault",
"distanceByPlatformCsv", "formationType", "formationSpacingMeters", "formationHeadingOffsetDeg",
"defaultDeployHeight", "heightFollowBlueRatio", "enableWarZoneClamp", "warZoneClampMode",
"minInterPlatformDistanceMeters");
private static final Set<String> TRACK = set(
"trackRuleEnabled", "trackRouteAlgorithm", "trackRouteNameSuffix",
"trackAirDataTypeCsv", "trackAirKeywordsCsv", "trackGroundTrackType",
"trackFallbackBearingDeg", "enableTrackWarZoneClamp",
"trackExtraNodesMax", "trackShortPathSegments",
"trackFlankOffsetMeters", "trackFlankSideMode",
"trackJamWobbleMeters", "trackJamSegments");
private RuleFourBlockParamMapping() {
}
private static Set<String> set(String... keys) {
return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(keys)));
}
/**
* @param paramKey 参数键
* @param slotRuleKeys 形如 blueRuleKeywords_i / redRuleKeywords_i / ruleScore_i视为装备块
*/
public static RuleFourBlockDefinition fallbackBlockForKey(String paramKey, boolean slotRuleKeys) {
if (paramKey == null || paramKey.isEmpty()) {
return RuleFourBlockDefinition.EQUIPMENT;
}
String k = paramKey.trim();
if (slotRuleKeys) {
return RuleFourBlockDefinition.EQUIPMENT;
}
if (EQUIPMENT.contains(k)) {
return RuleFourBlockDefinition.EQUIPMENT;
}
if (TARGET.contains(k)) {
return RuleFourBlockDefinition.TARGET;
}
if (POSITION.contains(k)) {
return RuleFourBlockDefinition.POSITION;
}
if (TRACK.contains(k)) {
return RuleFourBlockDefinition.TRACK;
}
String lower = k.toLowerCase(Locale.ROOT);
if (lower.startsWith("track")) {
return RuleFourBlockDefinition.TRACK;
}
if (lower.startsWith("position") || lower.startsWith("deploy") || lower.startsWith("formation")
|| lower.contains("warzone") || lower.contains("platform")) {
return RuleFourBlockDefinition.POSITION;
}
if (lower.contains("target") || lower.contains("execute") || lower.contains("assign") || lower.contains("supplement")) {
return RuleFourBlockDefinition.TARGET;
}
return RuleFourBlockDefinition.EQUIPMENT;
}
}

View File

@@ -0,0 +1,91 @@
package com.solution.rule.domain.config.graph;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 四块图谱展示用:每条 rule_item 对应的「运行时产出/写回」中文说明(静态目录,与 RuleFunction 行为一致即可)。
*/
public final class RuleFourBlockRuleOutputCatalog {
private static final Map<String, Map<String, List<String>>> BY_MODULE_AND_RULE = new HashMap<>();
static {
Map<String, List<String>> equipment = new HashMap<>();
equipment.put("R_TASK_SELECT_BASE", list(
"汇总匹配结果并写入火力规则输入fireRuleInputs 等匹配行)",
"为任务名称追加输出后缀outputDrawNameSuffix"));
equipment.put("R_TASK_SELECT_SLOT_1", list(
"若槽1蓝红关键词同时命中为当前候选装备累加 ruleScore_1×weight 分值"));
equipment.put("R_TASK_SELECT_SLOT_2", list(
"若槽2蓝红关键词同时命中累加 ruleScore_2×weight 分值"));
equipment.put("R_TASK_SELECT_SLOT_3", list(
"若槽3蓝红关键词同时命中累加 ruleScore_3×weight 分值"));
equipment.put("R_TASK_REL_AIR_PLATFORM", list(
"空中平台关键词命中时累加 airScore×weight 兼容层分值"));
equipment.put("R_TASK_REL_AIR_TASK", list(
"空中任务文案命中时累加 airTaskScore×weight 分值"));
equipment.put("R_TASK_REL_GROUND_TASK", list(
"地面任务与红方偏好同时命中时累加 groundScore×weight 分值"));
equipment.put("R_TASK_REL_TANK", list(
"坦克类关键词匹配时累加 tankScore×weight 分值"));
equipment.put("R_TASK_REL_MISSILE", list(
"导弹类关键词匹配时累加 missileScore×weight 分值"));
BY_MODULE_AND_RULE.put("equipment", Collections.unmodifiableMap(equipment));
Map<String, List<String>> target = new HashMap<>();
target.put("R_TASK_ASSIGN_TARGET", list(
"填充任务 execute 中目标列表与默认行动类型executeTypeDefault 等)",
"按命中率映射与分配模式更新 targetId / 目标数量"));
target.put("R_TASK_LIMIT_SUPPLEMENT", list(
"命中率低于阈值时按轮次与件数上限补充选取装备并回写任务武器列表"));
BY_MODULE_AND_RULE.put("target", Collections.unmodifiableMap(target));
Map<String, List<String>> position = new HashMap<>();
position.put("R_PLATFORM_DEPLOY", list(
"计算并写回红方 SubComponents.platform[].positions部署点、编队间距与高度"));
position.put("R_PLATFORM_SPACETIME", list(
"在启用作战区约束时,将越界阵位回拉到 warZoneLocation 多边形内"));
BY_MODULE_AND_RULE.put("position", Collections.unmodifiableMap(position));
Map<String, List<String>> track = new HashMap<>();
track.put("R_ACTION_TRACK_ROUTE", list(
"生成/更新 TrackParam 中航迹节点,并为 execute 绑定 moveRouteId",
"按算法followBlue 等)变形蓝方航迹并写航迹名称后缀"));
track.put("R_ACTION_TRACK_SPACETIME", list(
"在启用约束时将航迹点限制在作战区边界内"));
BY_MODULE_AND_RULE.put("track", Collections.unmodifiableMap(track));
}
private RuleFourBlockRuleOutputCatalog() {
}
private static List<String> list(String... lines) {
List<String> l = new ArrayList<>();
for (String s : lines) {
if (s != null && !s.isEmpty()) {
l.add(s);
}
}
return Collections.unmodifiableList(l);
}
/**
* @param moduleCode equipment / target / position / track
* @param ruleCode rule_item.rule_code
*/
public static List<String> outputsForRule(String moduleCode, String ruleCode) {
if (moduleCode == null || ruleCode == null) {
return Collections.emptyList();
}
Map<String, List<String>> m = BY_MODULE_AND_RULE.get(moduleCode);
if (m == null) {
return Collections.emptyList();
}
List<String> list = m.get(ruleCode);
return list != null ? list : Collections.emptyList();
}
}

View File

@@ -0,0 +1,72 @@
package com.solution.rule.domain.config.graph;
import cn.hutool.core.util.ObjectUtil;
/**
* 参数在「产出」列中的一行静态说明(与 globalParams 生效值拼接展示)。
*/
public final class RuleParamOutputHint {
private static final String GENERIC = "写入 globalParams 并由当前块对应 Java 算子读取";
private RuleParamOutputHint() {
}
public static String effectLine(String paramKey, String moduleCode) {
if (ObjectUtil.isEmpty(paramKey)) {
return GENERIC;
}
String k = paramKey.trim();
switch (k) {
case "weight":
return "参与装备/目标等环节的评分加权";
case "minSelectedScore":
return "低于该分则本规则对应环节不选中/不生效";
case "tieBreak":
return "同分时的决胜策略(如按装备 ID";
case "outputDrawNameSuffix":
return "影响任务/输出名称后缀展示";
case "ruleSlotCount":
return "规则槽数量上限(装备匹配)";
case "executeTypeDefault":
return "写入任务 execute 的默认行动类型";
case "targetPickMode":
return "目标分配挑选模式(轮询/随机等)";
case "radToTargetsCsv":
return "命中率到目标数量的映射";
case "redHitRateThreshold":
return "命中率阈值,触发补拿等逻辑";
case "maxExtraWeaponsPerTask":
case "maxSupplementRounds":
case "extraPickMinScore":
return "补拿装备轮次与分数约束";
case "positionRuleEnabled":
case "trackRuleEnabled":
case "groupRuleEnabled":
return "控制对应环节是否执行";
case "deployDistanceKmMin":
case "deployDistanceKmMax":
case "deployDistanceKmDefault":
return "部署距离区间与默认值(阵位)";
case "formationType":
case "formationSpacingMeters":
return "编队几何与间距";
case "enableWarZoneClamp":
case "enableTrackWarZoneClamp":
return "作战区约束开关";
case "trackRouteAlgorithm":
return "航迹生成算法选择";
default:
if (k.startsWith("blueRuleKeywords_") || k.startsWith("redRuleKeywords_") || k.startsWith("ruleScore_")) {
return "规则槽关键词/分值,参与装备匹配打分";
}
if (ObjectUtil.isNotEmpty(moduleCode) && "track".equals(moduleCode) && k.toLowerCase().startsWith("track")) {
return "航迹生成与约束相关配置";
}
if (ObjectUtil.isNotEmpty(moduleCode) && "position".equals(moduleCode)) {
return "阵位/部署与空间约束相关配置";
}
return GENERIC;
}
}
}

View File

@@ -0,0 +1,40 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
@ApiModel("单个分块(环形参数 + If/Then 中枢)")
public class RuleFourBlockClusterVO implements Serializable {
@ApiModelProperty("块标识equipment/target/position/track")
private String blockId;
@ApiModelProperty("模块编码,对应 rule_item.module_code")
private String moduleCode;
@ApiModelProperty("界面标题(如 装备匹配)")
private String title;
@ApiModelProperty("Drools 规则名(如 装备匹配)")
private String droolsRuleName;
@ApiModelProperty("salience")
private Integer salience;
@ApiModelProperty("Drools when 表达式摘要")
private String whenExpr;
@ApiModelProperty("then 动作(含 mergeDefaultParams + Java 调用)")
private String thenAction;
@ApiModelProperty("中枢 + 环形参数节点与边")
private RuleGraphVO graph;
@ApiModelProperty("该块内每条规则的每个参数When/Then/产出(表格)")
private List<RuleFourBlockParamRowVO> paramRows;
}

View File

@@ -0,0 +1,33 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel("四块分区内单条参数展示行")
public class RuleFourBlockParamRowVO implements Serializable {
@ApiModelProperty("规则编码")
private String ruleCode;
@ApiModelProperty("规则名称")
private String ruleName;
@ApiModelProperty("参数键")
private String paramKey;
@ApiModelProperty("参数业务名")
private String paramName;
@ApiModelProperty("When规则条件或块级回退")
private String whenText;
@ApiModelProperty("Then规则动作或块级回退")
private String thenText;
@ApiModelProperty("产出说明(生效值 + 静态提示)")
private String outputText;
}

View File

@@ -0,0 +1,28 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
@ApiModel("四块规则知识图谱(装备/目标/阵位/航迹)")
public class RuleFourBlocksGraphVO implements Serializable {
@ApiModelProperty("与运行时 KieSession globalParams 一致的参数快照(启用规则与参数合并后)")
private Map<String, Object> globalParamsPreview;
@ApiModelProperty("四个分块,每块含中枢 If/Then 与子图节点边")
private List<RuleFourBlockClusterVO> blocks;
public Map<String, Object> safeGlobalParamsPreview() {
if (globalParamsPreview == null) {
globalParamsPreview = new LinkedHashMap<>();
}
return globalParamsPreview;
}
}

View File

@@ -0,0 +1,27 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel("规则知识图谱边")
public class RuleGraphEdgeVO implements Serializable {
@ApiModelProperty("边 id")
private String id;
@ApiModelProperty("起点节点 id")
private String source;
@ApiModelProperty("终点节点 id")
private String target;
@ApiModelProperty("边类型")
private String edgeType;
@ApiModelProperty("边上展示文案")
private String label;
}

View File

@@ -0,0 +1,33 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
@Data
@ApiModel("规则知识图谱节点")
public class RuleGraphNodeVO implements Serializable {
@ApiModelProperty("全局唯一节点 id")
private String id;
@ApiModelProperty("展示标题")
private String label;
@ApiModelProperty("节点类型level/kind/module/rule/param/taskType")
private String nodeType;
@ApiModelProperty("附加属性(如 ruleCode、paramKey、字典编码等")
private Map<String, Object> payload;
public Map<String, Object> safePayload() {
if (payload == null) {
payload = new HashMap<>();
}
return payload;
}
}

View File

@@ -0,0 +1,40 @@
package com.solution.rule.domain.config.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
@ApiModel("规则知识图谱数据")
public class RuleGraphVO implements Serializable {
@ApiModelProperty("节点列表")
private List<RuleGraphNodeVO> nodes;
@ApiModelProperty("边列表")
private List<RuleGraphEdgeVO> edges;
@ApiModelProperty("前端布局提示:如 radial_hub 使用径向圆心布局")
private String layoutHint;
@ApiModelProperty("径向布局时的中心节点 id通常为 drools_facade")
private String focusNodeId;
public List<RuleGraphNodeVO> safeNodes() {
if (nodes == null) {
nodes = new ArrayList<>();
}
return nodes;
}
public List<RuleGraphEdgeVO> safeEdges() {
if (edges == null) {
edges = new ArrayList<>();
}
return edges;
}
}

View File

@@ -0,0 +1,16 @@
package com.solution.rule.domain.tasks;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class AssignTargetItem {
private String task;
private String type;
private List<String> target = new ArrayList<>();
}

View File

@@ -0,0 +1,16 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class DataOutput {
@JsonProperty("assign_target")
private List<AssignTargetItem> assignTarget = new ArrayList<>();
private List<TaskItem> task = new ArrayList<>();
}

View File

@@ -0,0 +1,13 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3ExecuteBlock {
private List<Kj3TargetItem> targetList;
}

View File

@@ -0,0 +1,16 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3ForceSide {
@JsonProperty("ObjectHandle")
private String objectHandle;
@JsonProperty("ForceSideName")
private String forceSideName;
}

View File

@@ -0,0 +1,25 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3MilitaryItem {
@JsonProperty("Name")
private String name;
@JsonProperty("OwnerForceSide")
private String ownerForceSide;
@JsonProperty("EquipmentID")
private String equipmentId;
@JsonProperty("Platform_type")
private String platformType;
@JsonProperty("SubComponents")
private Kj3SubComponents subComponents;
}

View File

@@ -0,0 +1,17 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3MilitaryPlatformComponent {
@JsonProperty("TrackParamId")
private String trackParamId;
private List<Double> positions;
}

View File

@@ -0,0 +1,24 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3MilitaryScenario {
@JsonProperty("Tasks")
private List<Kj3TaskNode> tasks;
@JsonProperty("ForceSides")
private List<Kj3ForceSide> forceSides;
@JsonProperty("Military")
private List<Kj3MilitaryItem> military;
@JsonProperty("RefAttributeObject")
private Kj3RefAttributeObject refAttributeObject;
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import lombok.Data;
@Data
public class Kj3PostResult {
private boolean success;
private String statusMessage;
private String postUrl;
private int taskCount;
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Map;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3RefAttributeObject {
@JsonProperty("TrackParam")
private Map<String, Kj3TrackParam> trackParam;
}

View File

@@ -0,0 +1,13 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3Root {
@JsonProperty("MilitaryScenario")
private Kj3MilitaryScenario militaryScenario;
}

View File

@@ -0,0 +1,13 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3SubComponents {
private List<Kj3MilitaryPlatformComponent> platform;
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3TargetItem {
private String targetId;
@JsonProperty("cruiseRouteId")
private String cruiseRouteId;
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3TaskNode {
private String name;
private String side;
private Kj3TaskPayload task;
}

View File

@@ -0,0 +1,23 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3TaskPayload {
private String name;
private String side;
private String type;
@JsonProperty("at_time")
private String atTime;
private List<Kj3ExecuteBlock> execute;
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3TrackParam {
@JsonProperty("TrackPoints")
private List<Kj3TrackPoint> trackPoints;
}

View File

@@ -0,0 +1,17 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class Kj3TrackPoint {
private String longitude;
private String latitude;
private String height;
private String speed;
}

View File

@@ -0,0 +1,24 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class PlatformItem {
private String name;
@JsonProperty("platform_type")
private String platformType;
private String longitude;
private String latitude;
private Integer altitude;
private List<RoutePoint> route = new ArrayList<>();
}

View File

@@ -0,0 +1,15 @@
package com.solution.rule.domain.tasks;
import lombok.Data;
@Data
public class RoutePoint {
private String longitude;
private String latitude;
private Integer altitude;
private Integer speed;
}

View File

@@ -0,0 +1,18 @@
package com.solution.rule.domain.tasks;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TaskItem {
@JsonProperty("task_name")
private String taskName;
private List<PlatformItem> platform = new ArrayList<>();
private TaskTime time;
}

View File

@@ -0,0 +1,11 @@
package com.solution.rule.domain.tasks;
import lombok.Data;
@Data
public class TaskTime {
private Integer begin;
private Integer end;
}

View File

@@ -0,0 +1,45 @@
package com.solution.rule.mapper;
import com.solution.rule.domain.config.RuleConfig;
import com.solution.rule.domain.config.RuleConfigParam;
import com.solution.rule.domain.config.RuleConfigQuery;
import com.solution.rule.domain.config.RuleConfigTaskTypeRow;
import com.solution.rule.domain.config.RuleDictItem;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface RuleConfigMapper {
List<RuleConfig> selectRuleConfigList(RuleConfigQuery query);
RuleConfig selectRuleConfigByCode(@Param("ruleCode") String ruleCode);
int countByRuleCode(@Param("ruleCode") String ruleCode);
int insertRuleConfig(RuleConfig ruleConfig);
int updateRuleConfig(RuleConfig ruleConfig);
int deleteRuleConfigByCodes(@Param("ruleCodes") String[] ruleCodes);
List<RuleConfigParam> selectParamsByRuleCode(@Param("ruleCode") String ruleCode);
List<RuleConfigParam> selectParamsByRuleCodes(@Param("ruleCodes") List<String> ruleCodes);
int deleteParamsByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
int insertParamsBatch(@Param("params") List<RuleConfigParam> params);
List<String> selectTaskTypesByRuleCode(@Param("ruleCode") String ruleCode);
List<RuleConfigTaskTypeRow> selectTaskTypesByRuleCodes(@Param("ruleCodes") List<String> ruleCodes);
int deleteTaskTypesByRuleCodes(@Param("ruleCodes") String[] ruleCodes);
int insertTaskTypesBatch(@Param("ruleCode") String ruleCode, @Param("taskTypes") List<String> taskTypes);
List<RuleDictItem> selectDictByType(@Param("dictType") String dictType);
List<RuleConfigParam> selectEnabledParamsForGlobal();
}

View File

@@ -0,0 +1,40 @@
package com.solution.rule.service;
import com.solution.rule.domain.config.RuleConfig;
import com.solution.rule.domain.config.RuleConfigQuery;
import com.solution.rule.domain.config.RuleDictItem;
import com.solution.rule.domain.config.RuleParamMeta;
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
import com.solution.rule.domain.config.vo.RuleGraphVO;
import java.util.List;
import java.util.Map;
public interface IRuleConfigService {
List<RuleConfig> selectRuleConfigList(RuleConfigQuery query);
RuleConfig selectRuleConfigByCode(String ruleCode);
int insertRuleConfig(RuleConfig ruleConfig);
int updateRuleConfig(RuleConfig ruleConfig);
int deleteRuleConfigByCodes(String[] ruleCodes);
List<RuleDictItem> selectDictByType(String dictType);
Map<String, Object> loadEnabledGlobalParams();
List<RuleParamMeta> selectParamMetaList();
/**
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
*/
RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs);
/**
* 四块规则知识图谱(装备/目标/阵位/航迹):中枢 If/Then + 环形参数,参数值与 {@link #loadEnabledGlobalParams()} 一致。
*/
RuleFourBlocksGraphVO buildFourBlocksKnowledgeGraph();
}

View File

@@ -0,0 +1,23 @@
package com.solution.rule.service;
import com.solution.rule.domain.tasks.Kj3PostResult;
import com.solution.rule.domain.tasks.Kj3Root;
public interface Kj3TaskDataService {
/**
* 解析接口入参 JSON仅提取红方任务转换为 data 结构后发送 HTTP POST。
*
* @param kj3Json 传入的 kj-3 JSON 字符串
* @return 发送结果
*/
Kj3PostResult parseAndPostRedTasks(String kj3Json);
/**
* 解析接口入参对象,仅提取红方任务,转换为 data 结构后发送 HTTP POST。
*
* @param root 传入的 kj-3 对象
* @return 发送结果
*/
Kj3PostResult parseAndPostRedTasks(Kj3Root root);
}

View File

@@ -0,0 +1,6 @@
package com.solution.rule.service;
public interface RuleDrlSyncService {
void syncGlobalParamsToDrl();
}

View File

@@ -21,6 +21,7 @@ import com.solution.rule.domain.vo.PlatformWeaponAggregateVO;
import com.solution.rule.domain.vo.WeaponModelVO;
import com.solution.rule.mapper.FireRuleMapper;
import com.solution.rule.service.FireRuleService;
import com.solution.rule.service.IRuleConfigService;
import com.solution.rule.simpstrategy.FireRUleType;
import com.solution.rule.simpstrategy.FireRuleStrategy;
import com.solution.rule.simpstrategy.FireRuleStrategyFactory;
@@ -51,6 +52,8 @@ public class FireRuleServiceImpl implements FireRuleService {
@Autowired
private KieBase kieBase;
@Autowired
private IRuleConfigService ruleConfigService;
/* @Override
public WeaponModelVO execute(Integer sceneType, WeaponModelDTO weaponModelDTO) {
@@ -209,7 +212,8 @@ public class FireRuleServiceImpl implements FireRuleService {
}
//创建KieSession
KieSession kieSession = kieBase.newKieSession();
Map<String, Object> globalParams = new HashMap<>();
// Map<String, Object> globalParams = new HashMap<>();
Map<String, Object> globalParams = ruleConfigService.loadEnabledGlobalParams();
// globalParams.putAll(FireRuleMatchDefaultParams.defaults());
kieSession.setGlobal("globalParams", globalParams);
//获取红方阵营id

View File

@@ -0,0 +1,418 @@
package com.solution.rule.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.solution.rule.domain.tasks.AssignTargetItem;
import com.solution.rule.domain.tasks.DataOutput;
import com.solution.rule.domain.tasks.Kj3ExecuteBlock;
import com.solution.rule.domain.tasks.Kj3ForceSide;
import com.solution.rule.domain.tasks.Kj3MilitaryItem;
import com.solution.rule.domain.tasks.Kj3MilitaryPlatformComponent;
import com.solution.rule.domain.tasks.Kj3MilitaryScenario;
import com.solution.rule.domain.tasks.Kj3PostResult;
import com.solution.rule.domain.tasks.Kj3RefAttributeObject;
import com.solution.rule.domain.tasks.Kj3Root;
import com.solution.rule.domain.tasks.Kj3TaskNode;
import com.solution.rule.domain.tasks.Kj3TaskPayload;
import com.solution.rule.domain.tasks.Kj3TrackParam;
import com.solution.rule.domain.tasks.Kj3TrackPoint;
import com.solution.rule.domain.tasks.Kj3TargetItem;
import com.solution.rule.domain.tasks.PlatformItem;
import com.solution.rule.domain.tasks.RoutePoint;
import com.solution.rule.domain.tasks.TaskItem;
import com.solution.rule.domain.tasks.TaskTime;
import com.solution.rule.service.Kj3TaskDataService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.UnknownHostException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class Kj3TaskDataServiceImpl implements Kj3TaskDataService {
private static final Logger log = LoggerFactory.getLogger(Kj3TaskDataServiceImpl.class);
private static final String DATA_POST_URL = "http://localhost:5000/bhtree/zhitangCompanyData";
private static final String URL_PLACEHOLDER = "http://TODO-REPLACE";
private static final String RED_SIDE = "红方";
private static final String DEFAULT_ASSIGN_TYPE = "fire";
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Kj3PostResult parseAndPostRedTasks(String kj3Json) {
if (ObjectUtil.isEmpty(kj3Json)) {
throw new IllegalArgumentException("kj3Json 不能为空");
}
Kj3Root root = parseKj3Json(kj3Json);
return parseAndPostRedTasks(root);
}
@Override
public Kj3PostResult parseAndPostRedTasks(Kj3Root root) {
if (ObjectUtil.isEmpty(root)) {
throw new IllegalArgumentException("kj3 入参对象不能为空");
}
DataOutput output = transformToDataOutput(root);
String payload = writeJson(output);
log.info("kj3 转换后的 data 输出: {}", payload);
Kj3PostResult result = new Kj3PostResult();
result.setPostUrl(DATA_POST_URL);
result.setTaskCount(output.getTask().size());
if (!isPostUrlConfigured()) {
result.setSuccess(false);
result.setStatusMessage("POST URL 未配置,请将 DATA_POST_URL 从占位值替换为真实地址");
return result;
}
try {
String response = postJson(payload);
result.setSuccess(true);
result.setStatusMessage(response);
} catch (RuntimeException ex) {
result.setSuccess(false);
result.setStatusMessage(ex.getMessage());
}
return result;
}
private boolean isPostUrlConfigured() {
return ObjectUtil.isNotEmpty(DATA_POST_URL) && !URL_PLACEHOLDER.equalsIgnoreCase(DATA_POST_URL);
}
private Kj3Root parseKj3Json(String kj3Json) {
try {
return objectMapper.readValue(kj3Json, Kj3Root.class);
} catch (IOException e) {
throw new RuntimeException("解析接口入参 kj-3 JSON 失败", e);
}
}
private DataOutput transformToDataOutput(Kj3Root root) {
DataOutput output = new DataOutput();
Kj3MilitaryScenario scenario = root == null ? null : root.getMilitaryScenario();
List<Kj3TaskNode> tasks = scenario == null ? Collections.emptyList() : scenario.getTasks();
if (CollUtil.isEmpty(tasks)) {
return output;
}
String redForceSideId = getRedForceSideId(scenario);
List<Kj3TaskNode> redTasks = tasks.stream()
.filter(this::isRedTask)
.collect(Collectors.toList());
for (Kj3TaskNode redTask : redTasks) {
String taskName = resolveTaskName(redTask);
Kj3TaskPayload payload = redTask.getTask();
TaskItem taskItem = new TaskItem();
taskItem.setTaskName(taskName);
taskItem.setTime(buildTaskTime(payload));
taskItem.setPlatform(buildPlatformsForTask(scenario, redForceSideId, payload));
output.getTask().add(taskItem);
AssignTargetItem assignTargetItem = new AssignTargetItem();
assignTargetItem.setTask(taskName);
assignTargetItem.setType(resolveTaskType(payload));
assignTargetItem.setTarget(extractTargets(payload));
output.getAssignTarget().add(assignTargetItem);
}
return output;
}
private boolean isRedTask(Kj3TaskNode taskNode) {
if (taskNode == null) {
return false;
}
if (RED_SIDE.equals(taskNode.getSide())) {
return true;
}
Kj3TaskPayload payload = taskNode.getTask();
return payload != null && RED_SIDE.equals(payload.getSide());
}
private String resolveTaskName(Kj3TaskNode taskNode) {
Kj3TaskPayload payload = taskNode.getTask();
if (payload != null && ObjectUtil.isNotEmpty(payload.getName())) {
return payload.getName();
}
return taskNode.getName();
}
private String resolveTaskType(Kj3TaskPayload payload) {
if (payload != null && ObjectUtil.isNotEmpty(payload.getType())) {
return payload.getType();
}
return DEFAULT_ASSIGN_TYPE;
}
private String getRedForceSideId(Kj3MilitaryScenario scenario) {
if (scenario == null || CollUtil.isEmpty(scenario.getForceSides())) {
return "";
}
for (Kj3ForceSide forceSide : scenario.getForceSides()) {
if (forceSide != null && RED_SIDE.equals(forceSide.getForceSideName())) {
return forceSide.getObjectHandle();
}
}
return "";
}
private List<PlatformItem> buildRedPlatforms(Kj3MilitaryScenario scenario, String redForceSideId) {
if (scenario == null || CollUtil.isEmpty(scenario.getMilitary()) || ObjectUtil.isEmpty(redForceSideId)) {
return new ArrayList<>();
}
Map<String, Kj3TrackParam> trackParamMap = getTrackParamMap(scenario);
List<PlatformItem> platforms = new ArrayList<>();
for (Kj3MilitaryItem militaryItem : scenario.getMilitary()) {
if (militaryItem == null || !redForceSideId.equals(militaryItem.getOwnerForceSide())) {
continue;
}
PlatformItem platformItem = mapMilitaryToPlatform(militaryItem, trackParamMap);
platforms.add(platformItem);
}
return platforms;
}
private List<PlatformItem> buildPlatformsForTask(Kj3MilitaryScenario scenario, String redForceSideId, Kj3TaskPayload payload) {
List<PlatformItem> redPlatforms = buildRedPlatforms(scenario, redForceSideId);
if (CollUtil.isEmpty(redPlatforms)) {
return redPlatforms;
}
Map<String, Kj3TrackParam> trackParamMap = getTrackParamMap(scenario);
String fallbackRouteId = resolveFallbackRouteId(payload);
if (ObjectUtil.isEmpty(fallbackRouteId)) {
return redPlatforms;
}
for (PlatformItem platform : redPlatforms) {
if (CollUtil.isNotEmpty(platform.getRoute())) {
continue;
}
platform.setRoute(buildRoute(fallbackRouteId, trackParamMap));
}
return redPlatforms;
}
private Map<String, Kj3TrackParam> getTrackParamMap(Kj3MilitaryScenario scenario) {
Kj3RefAttributeObject refAttributeObject = scenario.getRefAttributeObject();
if (refAttributeObject == null || refAttributeObject.getTrackParam() == null) {
return new HashMap<>();
}
return refAttributeObject.getTrackParam();
}
private PlatformItem mapMilitaryToPlatform(Kj3MilitaryItem militaryItem, Map<String, Kj3TrackParam> trackParamMap) {
PlatformItem platformItem = new PlatformItem();
platformItem.setName(militaryItem.getName());
platformItem.setPlatformType(militaryItem.getPlatformType());
Kj3MilitaryPlatformComponent positionSourceComponent = resolvePositionSourceComponent(militaryItem);
fillPosition(platformItem, positionSourceComponent);
String trackParamId = resolveTrackParamId(militaryItem);
platformItem.setRoute(buildRoute(trackParamId, trackParamMap));
return platformItem;
}
private String resolveFallbackRouteId(Kj3TaskPayload payload) {
if (payload == null || CollUtil.isEmpty(payload.getExecute())) {
return "";
}
for (Kj3ExecuteBlock executeBlock : payload.getExecute()) {
if (executeBlock == null || CollUtil.isEmpty(executeBlock.getTargetList())) {
continue;
}
for (Kj3TargetItem targetItem : executeBlock.getTargetList()) {
if (targetItem != null && ObjectUtil.isNotEmpty(targetItem.getCruiseRouteId())) {
return targetItem.getCruiseRouteId();
}
}
}
return "";
}
private Kj3MilitaryPlatformComponent resolvePositionSourceComponent(Kj3MilitaryItem militaryItem) {
if (militaryItem.getSubComponents() == null || CollUtil.isEmpty(militaryItem.getSubComponents().getPlatform())) {
return null;
}
for (Kj3MilitaryPlatformComponent component : militaryItem.getSubComponents().getPlatform()) {
if (component != null && CollUtil.isNotEmpty(component.getPositions())) {
return component;
}
}
return militaryItem.getSubComponents().getPlatform().get(0);
}
private String resolveTrackParamId(Kj3MilitaryItem militaryItem) {
if (militaryItem.getSubComponents() == null || CollUtil.isEmpty(militaryItem.getSubComponents().getPlatform())) {
return "";
}
for (Kj3MilitaryPlatformComponent component : militaryItem.getSubComponents().getPlatform()) {
if (component != null && ObjectUtil.isNotEmpty(component.getTrackParamId())) {
return component.getTrackParamId();
}
}
return "";
}
private void fillPosition(PlatformItem platformItem, Kj3MilitaryPlatformComponent component) {
if (component == null || CollUtil.isEmpty(component.getPositions())) {
return;
}
List<Double> positions = component.getPositions();
if (positions.size() > 0 && positions.get(0) != null) {
platformItem.setLongitude(String.valueOf(positions.get(0)));
}
if (positions.size() > 1 && positions.get(1) != null) {
platformItem.setLatitude(String.valueOf(positions.get(1)));
}
if (positions.size() > 2 && positions.get(2) != null) {
platformItem.setAltitude((int) Math.round(positions.get(2)));
}
}
private List<RoutePoint> buildRoute(String trackParamId, Map<String, Kj3TrackParam> trackParamMap) {
if (ObjectUtil.isEmpty(trackParamId)) {
return new ArrayList<>();
}
Kj3TrackParam trackParam = trackParamMap.get(trackParamId);
if (trackParam == null || CollUtil.isEmpty(trackParam.getTrackPoints())) {
return new ArrayList<>();
}
List<RoutePoint> routes = new ArrayList<>();
for (Kj3TrackPoint trackPoint : trackParam.getTrackPoints()) {
if (trackPoint == null) {
continue;
}
RoutePoint routePoint = new RoutePoint();
routePoint.setLongitude(trackPoint.getLongitude());
routePoint.setLatitude(trackPoint.getLatitude());
routePoint.setAltitude(parseInteger(trackPoint.getHeight()));
routePoint.setSpeed(parseInteger(trackPoint.getSpeed()));
routes.add(routePoint);
}
return routes;
}
private TaskTime buildTaskTime(Kj3TaskPayload payload) {
int begin = parseAtTime(payload);
TaskTime taskTime = new TaskTime();
taskTime.setBegin(begin);
taskTime.setEnd(begin);
return taskTime;
}
private int parseAtTime(Kj3TaskPayload payload) {
if (payload == null || ObjectUtil.isEmpty(payload.getAtTime())) {
return 0;
}
try {
return Integer.parseInt(payload.getAtTime().trim());
} catch (NumberFormatException ex) {
return 0;
}
}
private Integer parseInteger(String value) {
if (ObjectUtil.isEmpty(value)) {
return null;
}
try {
return (int) Math.round(Double.parseDouble(value.trim()));
} catch (NumberFormatException ex) {
return null;
}
}
private List<String> extractTargets(Kj3TaskPayload payload) {
if (payload == null || CollUtil.isEmpty(payload.getExecute())) {
return new ArrayList<>();
}
Set<String> uniqueTargets = new LinkedHashSet<>();
for (Kj3ExecuteBlock executeBlock : payload.getExecute()) {
if (executeBlock == null || CollUtil.isEmpty(executeBlock.getTargetList())) {
continue;
}
for (Kj3TargetItem targetItem : executeBlock.getTargetList()) {
if (targetItem == null || ObjectUtil.isEmpty(targetItem.getTargetId())) {
continue;
}
uniqueTargets.add(targetItem.getTargetId());
}
}
return new ArrayList<>(uniqueTargets);
}
private String writeJson(DataOutput output) {
try {
return objectMapper.writeValueAsString(output);
} catch (Exception e) {
throw new RuntimeException("序列化 data 输出失败", e);
}
}
private String postJson(String payload) {
HttpURLConnection connection = null;
try {
URL url = new URL(DATA_POST_URL);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
connection.setDoOutput(true);
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(payload.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
int status = connection.getResponseCode();
try (InputStream responseStream = status >= 400 ? connection.getErrorStream() : connection.getInputStream()) {
String responseBody = readResponse(responseStream);
return "HTTP " + status + " " + responseBody;
}
} catch (IOException e) {
if (e instanceof UnknownHostException) {
throw new RuntimeException("POST data 输出失败: 无法解析主机,请检查 DATA_POST_URL=" + DATA_POST_URL, e);
}
throw new RuntimeException("POST data 输出失败: " + e.getMessage(), e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private String readResponse(InputStream inputStream) throws IOException {
if (inputStream == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}
}

View File

@@ -0,0 +1,846 @@
package com.solution.rule.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.solution.common.constant.ExceptionConstants;
import com.solution.rule.domain.config.RuleConfig;
import com.solution.rule.domain.config.RuleConfigParam;
import com.solution.rule.domain.config.RuleConfigQuery;
import com.solution.rule.domain.config.RuleConfigTaskTypeRow;
import com.solution.rule.domain.config.RuleDictItem;
import com.solution.rule.domain.config.RuleParamMeta;
import com.solution.rule.domain.config.graph.RuleFourBlockDefinition;
import com.solution.rule.domain.config.graph.RuleFourBlockRuleOutputCatalog;
import com.solution.rule.domain.config.graph.RuleParamOutputHint;
import com.solution.rule.domain.config.vo.RuleFourBlockClusterVO;
import com.solution.rule.domain.config.vo.RuleFourBlockParamRowVO;
import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO;
import com.solution.rule.domain.config.vo.RuleGraphEdgeVO;
import com.solution.rule.domain.config.vo.RuleGraphNodeVO;
import com.solution.rule.domain.config.vo.RuleGraphVO;
import com.solution.rule.mapper.RuleConfigMapper;
import com.solution.rule.service.IRuleConfigService;
import com.solution.rule.service.RuleDrlSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
public class RuleConfigServiceImpl implements IRuleConfigService {
private static final Pattern RULE_SLOT_KEYS = Pattern.compile("^(blueRuleKeywords|redRuleKeywords|ruleScore)_\\d+$");
private static final boolean ALLOW_UNKNOWN_PARAM_KEY = false;
@Autowired
private RuleConfigMapper ruleConfigMapper;
@Autowired
private RuleDrlSyncService ruleDrlSyncService;
@Override
public List<RuleConfig> selectRuleConfigList(RuleConfigQuery query) {
return ruleConfigMapper.selectRuleConfigList(query);
}
@Override
public RuleConfig selectRuleConfigByCode(String ruleCode) {
RuleConfig config = ruleConfigMapper.selectRuleConfigByCode(ruleCode);
if (config == null) {
return null;
}
config.setParams(ruleConfigMapper.selectParamsByRuleCode(ruleCode));
config.setTaskTypes(ruleConfigMapper.selectTaskTypesByRuleCode(ruleCode));
return config;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int insertRuleConfig(RuleConfig ruleConfig) {
validateBase(ruleConfig);
if (ruleConfigMapper.countByRuleCode(ruleConfig.getRuleCode()) > 0) {
throw new RuntimeException("规则编码已存在");
}
int rows = ruleConfigMapper.insertRuleConfig(fillDefault(ruleConfig));
saveChildren(ruleConfig);
// return rows;
syncDrlAfterCrud();
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateRuleConfig(RuleConfig ruleConfig) {
validateBase(ruleConfig);
if (ruleConfigMapper.countByRuleCode(ruleConfig.getRuleCode()) <= 0) {
throw new RuntimeException("规则编码不存在");
}
int rows = ruleConfigMapper.updateRuleConfig(fillDefault(ruleConfig));
String[] ruleCodes = {ruleConfig.getRuleCode()};
ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes);
ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes);
saveChildren(ruleConfig);
// return rows;
syncDrlAfterCrud();
return rows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteRuleConfigByCodes(String[] ruleCodes) {
if (ruleCodes == null || ruleCodes.length == 0) {
return 0;
}
ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes);
ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes);
// return ruleConfigMapper.deleteRuleConfigByCodes(ruleCodes);
int rows = ruleConfigMapper.deleteRuleConfigByCodes(ruleCodes);
syncDrlAfterCrud();
return rows;
}
@Override
public List<RuleDictItem> selectDictByType(String dictType) {
if (ObjectUtil.isEmpty(dictType)) {
throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION);
}
return ruleConfigMapper.selectDictByType(dictType);
}
@Override
public Map<String, Object> loadEnabledGlobalParams() {
Map<String, Object> map = new HashMap<>();
List<RuleConfigParam> params = ruleConfigMapper.selectEnabledParamsForGlobal();
if (CollUtil.isEmpty(params)) {
return map;
}
for (RuleConfigParam param : params) {
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
continue;
}
map.put(param.getParamKey(), parseValue(param.getParamVal(), param.getValType()));
}
return map;
}
@Override
public List<RuleParamMeta> selectParamMetaList() {
return new ArrayList<>(metaMap().values());
}
@Override
public RuleGraphVO buildKnowledgeGraph(List<RuleConfig> ruleConfigs) {
RuleGraphVO graph = new RuleGraphVO();
List<RuleGraphNodeVO> nodes = graph.safeNodes();
List<RuleGraphEdgeVO> edges = graph.safeEdges();
if (CollUtil.isEmpty(ruleConfigs)) {
return graph;
}
List<String> ruleCodes = ruleConfigs.stream()
.map(RuleConfig::getRuleCode)
.filter(ObjectUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList());
List<RuleConfigParam> allParams = ruleCodes.isEmpty()
? Collections.emptyList()
: ruleConfigMapper.selectParamsByRuleCodes(ruleCodes);
List<RuleConfigTaskTypeRow> allTaskRows = ruleCodes.isEmpty()
? Collections.emptyList()
: ruleConfigMapper.selectTaskTypesByRuleCodes(ruleCodes);
Map<String, List<RuleConfigParam>> paramsByRule = allParams.stream()
.filter(p -> p != null && ObjectUtil.isNotEmpty(p.getRuleCode()))
.collect(Collectors.groupingBy(RuleConfigParam::getRuleCode, LinkedHashMap::new, Collectors.toList()));
Map<String, List<String>> taskTypesByRule = new LinkedHashMap<>();
for (RuleConfigTaskTypeRow row : allTaskRows) {
if (row == null || ObjectUtil.isEmpty(row.getRuleCode()) || ObjectUtil.isEmpty(row.getTaskTypeCode())) {
continue;
}
taskTypesByRule.computeIfAbsent(row.getRuleCode(), k -> new ArrayList<>()).add(row.getTaskTypeCode());
}
List<RuleDictItem> levelDict = safeDict("level");
List<RuleDictItem> kindDict = safeDict("kind");
List<RuleDictItem> taskTypeDict = safeDict("task_type");
Map<String, RuleGraphNodeVO> nodeById = new LinkedHashMap<>();
Set<String> edgeIds = new LinkedHashSet<>();
for (RuleConfig rule : ruleConfigs) {
if (rule == null || ObjectUtil.isEmpty(rule.getRuleCode())) {
continue;
}
String ruleCode = rule.getRuleCode();
String levelCode = ObjectUtil.defaultIfBlank(rule.getLevelCode(), "unknown");
String kindCode = ObjectUtil.defaultIfBlank(rule.getKindCode(), "unknown");
String moduleCode = ObjectUtil.defaultIfBlank(rule.getModuleCode(), "unknown");
String levelId = "level:" + levelCode;
Map<String, Object> levelPayload = new LinkedHashMap<>();
levelPayload.put("dictType", "level");
levelPayload.put("dictCode", levelCode);
levelPayload.put("dictName", dictLabel(levelDict, levelCode, null));
putNode(nodeById, levelId, node(levelId, levelCode, "level", levelPayload));
String kindId = "kind:" + levelCode + ":" + kindCode;
Map<String, Object> kindPayload = new LinkedHashMap<>();
kindPayload.put("levelCode", levelCode);
kindPayload.put("kindCode", kindCode);
kindPayload.put("dictName", dictLabel(kindDict, kindCode, null));
putNode(nodeById, kindId, node(kindId, kindCode, "kind", kindPayload));
String moduleId = "module:" + moduleCode;
Map<String, Object> modulePayload = new LinkedHashMap<>();
modulePayload.put("moduleCode", moduleCode);
putNode(nodeById, moduleId, node(moduleId, moduleCode, "module", modulePayload));
String ruleId = "rule:" + ruleCode;
String ruleLabel = ObjectUtil.defaultIfBlank(rule.getRuleName(), ruleCode);
Map<String, Object> rulePayload = new LinkedHashMap<>();
rulePayload.put("ruleCode", ruleCode);
rulePayload.put("priorityNo", rule.getPriorityNo());
rulePayload.put("moduleCode", moduleCode);
rulePayload.put("levelCode", levelCode);
rulePayload.put("kindCode", kindCode);
if (ObjectUtil.isNotEmpty(rule.getConditionExpr())) {
rulePayload.put("conditionExpr", rule.getConditionExpr());
}
if (ObjectUtil.isNotEmpty(rule.getActionExpr())) {
rulePayload.put("actionExpr", rule.getActionExpr());
}
RuleGraphNodeVO ruleNode = node(ruleId, ruleLabel, "rule", rulePayload);
putNode(nodeById, ruleId, ruleNode);
addEdge(edges, edgeIds, "belongs_level:" + ruleCode, ruleId, levelId, "rule_belongs_level", null);
addEdge(edges, edgeIds, "belongs_kind:" + ruleCode, ruleId, kindId, "rule_belongs_kind", null);
addEdge(edges, edgeIds, "has_module:" + ruleCode, ruleId, moduleId, "rule_has_module", null);
List<RuleConfigParam> plist = paramsByRule.getOrDefault(ruleCode, Collections.emptyList());
for (RuleConfigParam param : plist) {
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
continue;
}
String pk = param.getParamKey();
String paramId = "param:" + ruleCode + ":" + pk;
String pLabel = ObjectUtil.defaultIfBlank(param.getParamName(), pk);
Map<String, Object> paramPayload = new LinkedHashMap<>();
paramPayload.put("ruleCode", ruleCode);
paramPayload.put("paramKey", pk);
paramPayload.put("paramVal", param.getParamVal());
paramPayload.put("valType", param.getValType());
RuleGraphNodeVO pNode = node(paramId, pLabel, "param", paramPayload);
putNode(nodeById, paramId, pNode);
addEdge(edges, edgeIds, "has_param:" + ruleCode + ":" + pk, ruleId, paramId, "rule_has_param", null);
}
List<String> tlist = taskTypesByRule.getOrDefault(ruleCode, Collections.emptyList());
for (String tt : tlist) {
if (ObjectUtil.isEmpty(tt)) {
continue;
}
String taskId = "task:" + tt;
Map<String, Object> taskPayload = new LinkedHashMap<>();
taskPayload.put("taskTypeCode", tt);
taskPayload.put("dictName", dictLabel(taskTypeDict, tt, null));
putNode(nodeById, taskId, node(taskId, tt, "taskType", taskPayload));
addEdge(edges, edgeIds, "applies_task:" + ruleCode + ":" + tt, ruleId, taskId, "rule_applies_task", null);
}
}
Comparator<RuleConfig> byPriority = Comparator.comparing(RuleConfig::getPriorityNo,
Comparator.nullsLast(Integer::compareTo));
Map<String, List<RuleConfig>> execGroups = ruleConfigs.stream()
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getRuleCode()))
.collect(Collectors.groupingBy(
r -> ObjectUtil.defaultIfBlank(r.getLevelCode(), "") + "\0" + ObjectUtil.defaultIfBlank(r.getModuleCode(), ""),
LinkedHashMap::new,
Collectors.toList()));
for (List<RuleConfig> group : execGroups.values()) {
List<RuleConfig> sorted = new ArrayList<>(group);
sorted.sort(byPriority);
for (int i = 0; i < sorted.size() - 1; i++) {
String from = "rule:" + sorted.get(i).getRuleCode();
String to = "rule:" + sorted.get(i + 1).getRuleCode();
if (from.equals(to)) {
continue;
}
addEdge(edges, edgeIds, "exec:" + sorted.get(i).getRuleCode() + "->" + sorted.get(i + 1).getRuleCode(),
from, to, "rule_exec_before", "执行顺序");
}
}
nodes.addAll(nodeById.values());
return graph;
}
@Override
public RuleFourBlocksGraphVO buildFourBlocksKnowledgeGraph() {
RuleFourBlocksGraphVO out = new RuleFourBlocksGraphVO();
Map<String, Object> globalPreview = loadEnabledGlobalParams();
out.setGlobalParamsPreview(new LinkedHashMap<>(globalPreview));
RuleConfigQuery q = new RuleConfigQuery();
q.setEnabled(1);
List<RuleConfig> allEnabled = ruleConfigMapper.selectRuleConfigList(q);
if (CollUtil.isEmpty(allEnabled)) {
out.setBlocks(new ArrayList<>());
return out;
}
Set<String> fourModules = new HashSet<>(Arrays.asList("equipment", "target", "position", "track"));
Map<String, List<RuleConfig>> byModule = allEnabled.stream()
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getModuleCode()) && fourModules.contains(r.getModuleCode()))
.collect(Collectors.groupingBy(RuleConfig::getModuleCode, LinkedHashMap::new, Collectors.toList()));
List<RuleFourBlockClusterVO> blocks = new ArrayList<>();
for (RuleFourBlockDefinition def : RuleFourBlockDefinition.ordered()) {
List<RuleConfig> moduleRules = new ArrayList<>(byModule.getOrDefault(def.getModuleCode(), Collections.emptyList()));
moduleRules.sort(Comparator.comparing(RuleConfig::getPriorityNo, Comparator.nullsLast(Integer::compareTo)));
List<String> ruleCodes = moduleRules.stream().map(RuleConfig::getRuleCode).filter(ObjectUtil::isNotEmpty).collect(Collectors.toList());
List<RuleConfigParam> plist = ruleCodes.isEmpty()
? Collections.emptyList()
: ruleConfigMapper.selectParamsByRuleCodes(ruleCodes);
Map<String, Integer> priByRule = moduleRules.stream()
.filter(r -> ObjectUtil.isNotEmpty(r.getRuleCode()))
.collect(Collectors.toMap(RuleConfig::getRuleCode, RuleConfig::getPriorityNo, (a, b) -> a));
plist.sort(Comparator
.comparing((RuleConfigParam p) -> priByRule.getOrDefault(p.getRuleCode(), Integer.MAX_VALUE))
.thenComparing(p -> p.getSortNo() == null ? Integer.MAX_VALUE : p.getSortNo())
.thenComparing(RuleConfigParam::getParamKey, Comparator.nullsLast(String::compareTo)));
Map<String, RuleConfig> ruleByCode = moduleRules.stream()
.filter(r -> r != null && ObjectUtil.isNotEmpty(r.getRuleCode()))
.collect(Collectors.toMap(RuleConfig::getRuleCode, Function.identity(), (a, b) -> a));
RuleFourBlockClusterVO cluster = new RuleFourBlockClusterVO();
cluster.setBlockId(def.getBlockId());
cluster.setModuleCode(def.getModuleCode());
cluster.setTitle(def.getDroolsRuleName());
cluster.setDroolsRuleName(def.getDroolsRuleName());
cluster.setSalience(def.getSalience());
cluster.setWhenExpr(def.getWhenExpr());
cluster.setThenAction(def.getThenAction());
cluster.setParamRows(buildFourBlockParamRows(def, ruleByCode, plist, globalPreview));
cluster.setGraph(buildFourBlockNeoStyleGraph(def, moduleRules, plist, globalPreview, ruleByCode));
blocks.add(cluster);
}
out.setBlocks(blocks);
return out;
}
private List<RuleFourBlockParamRowVO> buildFourBlockParamRows(RuleFourBlockDefinition def,
Map<String, RuleConfig> ruleByCode,
List<RuleConfigParam> plist,
Map<String, Object> globalPreview) {
List<RuleFourBlockParamRowVO> rows = new ArrayList<>();
if (CollUtil.isEmpty(plist)) {
return rows;
}
String moduleCode = def.getModuleCode();
for (RuleConfigParam p : plist) {
if (p == null || ObjectUtil.isEmpty(p.getParamKey()) || ObjectUtil.isEmpty(p.getRuleCode())) {
continue;
}
if (p.getEnabled() != null && p.getEnabled() == 0) {
continue;
}
RuleConfig r = ruleByCode.get(p.getRuleCode());
if (r == null) {
continue;
}
RuleFourBlockParamRowVO row = new RuleFourBlockParamRowVO();
row.setRuleCode(r.getRuleCode());
row.setRuleName(r.getRuleName());
row.setParamKey(p.getParamKey());
row.setParamName(ObjectUtil.defaultIfBlank(p.getParamName(), p.getParamKey()));
row.setWhenText(formatParamWhenForRow(r, def));
row.setThenText(formatParamThenForRow(r, def));
row.setOutputText(formatParamOutputForRow(p, globalPreview, moduleCode));
rows.add(row);
}
return rows;
}
private static String formatParamWhenForRow(RuleConfig r, RuleFourBlockDefinition def) {
if (r != null && ObjectUtil.isNotEmpty(r.getConditionExpr())) {
return r.getConditionExpr();
}
return ObjectUtil.defaultIfBlank(def.getWhenExpr(), "");
}
private static String formatParamThenForRow(RuleConfig r, RuleFourBlockDefinition def) {
if (r != null && ObjectUtil.isNotEmpty(r.getActionExpr())) {
return r.getActionExpr();
}
return ObjectUtil.defaultIfBlank(def.getThenAction(), "");
}
private static String formatParamOutputForRow(RuleConfigParam p, Map<String, Object> globalPreview, String moduleCode) {
Object ev = globalPreview != null ? globalPreview.get(p.getParamKey()) : null;
String hint = RuleParamOutputHint.effectLine(p.getParamKey(), moduleCode);
if (ev == null) {
return hint;
}
return "生效值:" + String.valueOf(ev) + "" + hint;
}
/**
* Neo4j 风格子图:门面节点、多条 rule_item、参数按归属规则展开含 drools_contains / rule_priority_next / rule_has_param 边。
*/
private RuleGraphVO buildFourBlockNeoStyleGraph(RuleFourBlockDefinition def,
List<RuleConfig> moduleRules,
List<RuleConfigParam> plist,
Map<String, Object> globalPreview,
Map<String, RuleConfig> ruleByCode) {
RuleGraphVO graph = new RuleGraphVO();
List<RuleGraphNodeVO> nodes = graph.safeNodes();
List<RuleGraphEdgeVO> edges = graph.safeEdges();
Set<String> edgeIds = new LinkedHashSet<>();
String bid = def.getBlockId();
String droolsId = "four:block:" + bid + ":drools";
List<String> stepLabels = def.getComputationSteps();
String lastStepId = null;
for (int i = 0; i < stepLabels.size(); i++) {
String text = stepLabels.get(i);
if (ObjectUtil.isEmpty(text)) {
continue;
}
String sid = "four:block:" + bid + ":step:" + i;
Map<String, Object> sp = new LinkedHashMap<>();
sp.put("stepIndex", i);
sp.put("blockId", bid);
nodes.add(node(sid, text, "compute_step", sp));
if (lastStepId != null) {
addEdge(edges, edgeIds, "four:compute_flow:" + bid + ":" + (i - 1),
lastStepId, sid, "compute_flow", "");
}
lastStepId = sid;
}
Map<String, Object> droolsPayload = new LinkedHashMap<>();
droolsPayload.put("blockId", bid);
droolsPayload.put("salience", def.getSalience());
droolsPayload.put("droolsRuleName", def.getDroolsRuleName());
droolsPayload.put("computationHint", def.getDroolsRuleName() + ":业务步骤汇总后与库表规则项、参数一并展示");
nodes.add(node(droolsId, def.getDroolsRuleName(), "drools_facade", droolsPayload));
if (lastStepId != null) {
addEdge(edges, edgeIds, "four:compute_flow:to_drools:" + bid,
lastStepId, droolsId, "compute_flow", "汇总");
}
Set<String> ruleCodeSet = new HashSet<>();
List<String> ruleNodeIds = new ArrayList<>();
for (RuleConfig r : moduleRules) {
if (r == null || ObjectUtil.isEmpty(r.getRuleCode())) {
continue;
}
ruleCodeSet.add(r.getRuleCode());
String rid = "four:block:" + bid + ":rule:" + r.getRuleCode();
ruleNodeIds.add(rid);
String ruleLabel = ObjectUtil.defaultIfBlank(r.getRuleName(), r.getRuleCode());
Map<String, Object> rp = new LinkedHashMap<>();
rp.put("ruleCode", r.getRuleCode());
rp.put("ruleName", r.getRuleName());
rp.put("priorityNo", r.getPriorityNo());
rp.put("levelCode", r.getLevelCode());
rp.put("kindCode", r.getKindCode());
rp.put("moduleCode", r.getModuleCode());
if (ObjectUtil.isNotEmpty(r.getConditionExpr())) {
rp.put("conditionExpr", r.getConditionExpr());
}
if (ObjectUtil.isNotEmpty(r.getActionExpr())) {
rp.put("actionExpr", r.getActionExpr());
}
if (ObjectUtil.isNotEmpty(r.getRemark())) {
rp.put("remark", r.getRemark());
}
nodes.add(node(rid, ruleLabel, "rule_row", rp));
addEdge(edges, edgeIds, "four:drools_contains:" + bid + ":" + r.getRuleCode(),
droolsId, rid, "drools_contains", "包含");
}
for (int i = 0; i < ruleNodeIds.size() - 1; i++) {
addEdge(edges, edgeIds, "four:priority_next:" + bid + ":" + i,
ruleNodeIds.get(i), ruleNodeIds.get(i + 1), "rule_priority_next", "优先级顺序");
}
for (RuleConfig r : moduleRules) {
if (r == null || ObjectUtil.isEmpty(r.getRuleCode()) || !ruleCodeSet.contains(r.getRuleCode())) {
continue;
}
String rid = "four:block:" + bid + ":rule:" + r.getRuleCode();
List<String> outs = RuleFourBlockRuleOutputCatalog.outputsForRule(def.getModuleCode(), r.getRuleCode());
for (int j = 0; j < outs.size(); j++) {
String full = outs.get(j);
if (ObjectUtil.isEmpty(full)) {
continue;
}
String oid = "four:block:" + bid + ":out:" + r.getRuleCode() + ":" + j;
Map<String, Object> op = new LinkedHashMap<>();
op.put("detail", full);
op.put("ruleCode", r.getRuleCode());
op.put("outputIndex", j);
String lab = full.length() > 42 ? full.substring(0, 42) + "" : full;
nodes.add(node(oid, lab, "rule_output", op));
addEdge(edges, edgeIds, "four:produces:" + bid + ":" + r.getRuleCode() + ":" + j,
rid, oid, "rule_produces", "产出");
}
}
for (RuleConfigParam p : plist) {
if (p == null || ObjectUtil.isEmpty(p.getParamKey()) || ObjectUtil.isEmpty(p.getRuleCode())) {
continue;
}
if (p.getEnabled() != null && p.getEnabled() == 0) {
continue;
}
if (!ruleCodeSet.contains(p.getRuleCode())) {
continue;
}
String pk = p.getParamKey();
String pid = "four:block:" + bid + ":param:" + p.getRuleCode() + ":" + pk;
RuleParamMeta pm = resolveMeta(pk);
String pLabel = ObjectUtil.defaultIfBlank(p.getParamName(), pk);
Map<String, Object> pl = new LinkedHashMap<>();
pl.put("paramKey", pk);
pl.put("storedRaw", p.getParamVal());
pl.put("effectiveValue", globalPreview != null ? globalPreview.get(pk) : null);
pl.put("ruleCode", p.getRuleCode());
if (pm != null) {
pl.put("metaLabel", pm.getLabel());
pl.put("valueType", pm.getValueType());
pl.put("description", pm.getDescription());
}
nodes.add(node(pid, pLabel, "param", pl));
String rid = "four:block:" + bid + ":rule:" + p.getRuleCode();
addEdge(edges, edgeIds, "four:has_param:" + bid + ":" + p.getRuleCode() + ":" + pk,
rid, pid, "rule_has_param", "参数");
RuleConfig rr = ruleByCode.get(p.getRuleCode());
String fullWhen = formatParamWhenForRow(rr, def);
String cid = "four:block:" + bid + ":cond:" + p.getRuleCode() + ":" + pk;
String shortLab;
if (ObjectUtil.isEmpty(fullWhen)) {
shortLab = "(无条件)";
} else {
shortLab = fullWhen.length() > 36 ? fullWhen.substring(0, 36) + "" : fullWhen;
}
Map<String, Object> cp = new LinkedHashMap<>();
cp.put("fullWhenText", fullWhen);
cp.put("paramKey", pk);
cp.put("ruleCode", p.getRuleCode());
nodes.add(node(cid, shortLab, "param_condition", cp));
addEdge(edges, edgeIds, "four:cond_to_param:" + bid + ":" + p.getRuleCode() + ":" + pk,
cid, pid, "condition_applies", "条件");
}
graph.setLayoutHint("radial_hub");
graph.setFocusNodeId(droolsId);
return graph;
}
private List<RuleDictItem> safeDict(String dictType) {
List<RuleDictItem> list = ruleConfigMapper.selectDictByType(dictType);
return list != null ? list : Collections.emptyList();
}
private static void putNode(Map<String, RuleGraphNodeVO> map, String id, RuleGraphNodeVO n) {
map.putIfAbsent(id, n);
}
private static RuleGraphNodeVO node(String id, String label, String nodeType, Map<String, Object> payload) {
RuleGraphNodeVO n = new RuleGraphNodeVO();
n.setId(id);
n.setLabel(label);
n.setNodeType(nodeType);
n.setPayload(payload);
return n;
}
private static void addEdge(List<RuleGraphEdgeVO> edges, Set<String> edgeIds, String id,
String source, String target, String edgeType, String label) {
if (!edgeIds.add(id)) {
return;
}
RuleGraphEdgeVO e = new RuleGraphEdgeVO();
e.setId(id);
e.setSource(source);
e.setTarget(target);
e.setEdgeType(edgeType);
e.setLabel(label);
edges.add(e);
}
private static String dictLabel(List<RuleDictItem> dict, String code, String fallback) {
if (CollUtil.isEmpty(dict) || ObjectUtil.isEmpty(code)) {
return fallback;
}
for (RuleDictItem item : dict) {
if (item != null && code.equals(item.getDictCode())) {
return ObjectUtil.defaultIfBlank(item.getDictName(), fallback);
}
}
return fallback;
}
private void saveChildren(RuleConfig ruleConfig) {
if (CollUtil.isNotEmpty(ruleConfig.getParams())) {
Set<String> keys = new HashSet<>();
for (RuleConfigParam param : ruleConfig.getParams()) {
if (param == null || ObjectUtil.isEmpty(param.getParamKey())) {
throw new RuntimeException("参数键不能为空");
}
if (!keys.add(param.getParamKey())) {
throw new RuntimeException("参数键重复: " + param.getParamKey());
}
validateParam(param);
param.setRuleCode(ruleConfig.getRuleCode());
if (param.getSortNo() == null) {
param.setSortNo(0);
}
if (param.getEnabled() == null) {
param.setEnabled(1);
}
if (ObjectUtil.isEmpty(param.getValType())) {
param.setValType("string");
}
}
ruleConfigMapper.insertParamsBatch(ruleConfig.getParams());
}
if (CollUtil.isNotEmpty(ruleConfig.getTaskTypes())) {
ruleConfigMapper.insertTaskTypesBatch(ruleConfig.getRuleCode(), ruleConfig.getTaskTypes());
}
}
private RuleConfig fillDefault(RuleConfig ruleConfig) {
if (ruleConfig.getPriorityNo() == null) {
ruleConfig.setPriorityNo(100);
}
if (ruleConfig.getVersionNo() == null) {
ruleConfig.setVersionNo(1);
}
if (ruleConfig.getEnabled() == null) {
ruleConfig.setEnabled(1);
}
return ruleConfig;
}
private void validateBase(RuleConfig ruleConfig) {
if (ruleConfig == null
|| ObjectUtil.isEmpty(ruleConfig.getRuleCode())
|| ObjectUtil.isEmpty(ruleConfig.getRuleName())
|| ObjectUtil.isEmpty(ruleConfig.getLevelCode())
|| ObjectUtil.isEmpty(ruleConfig.getKindCode())
|| ObjectUtil.isEmpty(ruleConfig.getModuleCode())) {
throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION);
}
}
private void syncDrlAfterCrud() {
ruleDrlSyncService.syncGlobalParamsToDrl();
}
private Object parseValue(String val, String valType) {
if ("bool".equalsIgnoreCase(valType) || "boolean".equalsIgnoreCase(valType)) {
return Boolean.parseBoolean(val);
}
if ("number".equalsIgnoreCase(valType)) {
if (val == null || val.trim().isEmpty()) {
return 0;
}
String t = val.trim();
if (t.contains(".")) {
try {
return Double.parseDouble(t);
} catch (Exception ignore) {
return t;
}
}
try {
return Integer.parseInt(t);
} catch (Exception ignore) {
return t;
}
}
// 多值按英文逗号分隔时保持字符串原样,不做拆分
return val;
}
private void validateParam(RuleConfigParam param) {
RuleParamMeta meta = resolveMeta(param.getParamKey());
if (meta == null) {
if (ALLOW_UNKNOWN_PARAM_KEY) {
param.setValType("string");
return;
}
throw new RuntimeException("不支持的参数键: " + param.getParamKey());
}
String val = param.getParamVal();
if (Boolean.TRUE.equals(meta.getRequired()) && ObjectUtil.isEmpty(val)) {
throw new RuntimeException("参数值不能为空: " + param.getParamKey());
}
if ("bool".equalsIgnoreCase(meta.getValueType())) {
if (!"true".equalsIgnoreCase(String.valueOf(val)) && !"false".equalsIgnoreCase(String.valueOf(val))) {
throw new RuntimeException("布尔参数仅支持 true/false: " + param.getParamKey());
}
param.setValType("bool");
return;
}
if ("enum".equalsIgnoreCase(meta.getValueType())) {
if (CollUtil.isEmpty(meta.getEnumOptions()) || !meta.getEnumOptions().contains(val)) {
throw new RuntimeException("参数值不在可选范围内: " + param.getParamKey());
}
param.setValType("string");
return;
}
if ("number".equalsIgnoreCase(meta.getValueType())) {
try {
double d = Double.parseDouble(String.valueOf(val));
if (meta.getMin() != null && d < meta.getMin()) {
throw new RuntimeException("参数值小于最小值: " + param.getParamKey());
}
if (meta.getMax() != null && d > meta.getMax()) {
throw new RuntimeException("参数值大于最大值: " + param.getParamKey());
}
} catch (NumberFormatException e) {
throw new RuntimeException("数值参数格式错误: " + param.getParamKey());
}
param.setValType("number");
return;
}
if ("csv".equalsIgnoreCase(meta.getValueType())) {
if (meta.getPattern() != null && !Pattern.matches(meta.getPattern(), String.valueOf(val))) {
throw new RuntimeException("CSV 参数格式错误(英文逗号分隔): " + param.getParamKey());
}
param.setValType("string");
return;
}
param.setValType("string");
}
private RuleParamMeta resolveMeta(String key) {
RuleParamMeta direct = metaMap().get(key);
if (direct != null) {
return direct;
}
if (RULE_SLOT_KEYS.matcher(key).matches()) {
RuleParamMeta slotMeta = new RuleParamMeta();
slotMeta.setParamKey(key);
slotMeta.setLabel("规则槽动态参数");
slotMeta.setValueType(key.startsWith("ruleScore_") ? "number" : "csv");
slotMeta.setRequired(Boolean.TRUE);
slotMeta.setPattern("^[^,]+(?:,[^,]+)*$");
slotMeta.setDescription("支持 blueRuleKeywords_i/redRuleKeywords_i/ruleScore_i");
if (key.startsWith("ruleScore_")) {
slotMeta.setMin(0d);
}
return slotMeta;
}
return null;
}
private Map<String, RuleParamMeta> metaMap() {
Map<String, RuleParamMeta> map = new LinkedHashMap<>();
map.put("executeTypeDefault", meta("executeTypeDefault", "执行类型", "enum", true, Arrays.asList("assault", "strike_test", "my_test_type"), null, null, null, "assault", "execute[0].type"));
map.put("positionRuleEnabled", meta("positionRuleEnabled", "阵位规则开关", "bool", true, null, null, null, null, "true", "是否执行阵位生成"));
map.put("trackRuleEnabled", meta("trackRuleEnabled", "航迹规则开关", "bool", true, null, null, null, null, "true", "是否执行航迹生成"));
map.put("groupRuleEnabled", meta("groupRuleEnabled", "编组规则开关", "bool", true, null, null, null, null, "true", "是否执行编组生成"));
map.put("enableTrackWarZoneClamp", meta("enableTrackWarZoneClamp", "航迹作战区约束开关", "bool", true, null, null, null, null, "true", "是否对航迹点进行作战区约束"));
map.put("enableWarZoneClamp", meta("enableWarZoneClamp", "阵位作战区约束开关", "bool", true, null, null, null, null, "true", "是否对阵位点进行作战区约束"));
map.put("targetPickMode", meta("targetPickMode", "目标分配模式", "enum", true, Arrays.asList("roundRobin", "random"), null, null, null, "roundRobin", "目标挑选方式"));
map.put("formationType", meta("formationType", "编队样式", "enum", true, Arrays.asList("line", "wedge", "circle"), null, null, null, "line", "平台编队样式"));
map.put("trackRouteAlgorithm", meta("trackRouteAlgorithm", "航迹算法", "enum", true, Arrays.asList("followBlue", "shortestPath", "flank", "jam"), null, null, null, "followBlue", "航迹变形算法"));
map.put("trackFlankSideMode", meta("trackFlankSideMode", "flank侧向模式", "enum", true, Arrays.asList("alternate", "left", "right"), null, null, null, "alternate", "侧向策略"));
map.put("groupFormationMode", meta("groupFormationMode", "编组模式", "enum", true, Arrays.asList("onePerRed", "clusterByCount", "singleGroup"), null, null, null, "onePerRed", "编组策略"));
map.put("groupLeaderPickMode", meta("groupLeaderPickMode", "领队选择模式", "enum", true, Arrays.asList("byHitRateThenId", "byId"), null, null, null, "byHitRateThenId", "领队策略"));
map.put("weight", meta("weight", "全局权重", "number", true, null, 0d, 100d, null, "1", "评分乘数"));
map.put("minSelectedScore", meta("minSelectedScore", "最小选中分", "number", true, null, 0d, 100000d, null, "1", "低于该分不选中"));
map.put("minTargetsPerRed", meta("minTargetsPerRed", "每红装最少目标数", "number", true, null, 1d, 20d, null, "1", "目标分配下限"));
map.put("maxTargetsPerRedCap", meta("maxTargetsPerRedCap", "每红装最多目标数", "number", true, null, 1d, 50d, null, "3", "目标分配上限"));
map.put("redHitRateThreshold", meta("redHitRateThreshold", "命中率阈值", "number", true, null, 0d, 1d, null, "0.6", "低于阈值触发补拿"));
map.put("maxExtraWeaponsPerTask", meta("maxExtraWeaponsPerTask", "补拿装备上限", "number", true, null, 0d, 20d, null, "2", "每任务补拿数量"));
map.put("maxSupplementRounds", meta("maxSupplementRounds", "补拿轮次上限", "number", true, null, 0d, 20d, null, "2", "补拿循环轮次"));
map.put("extraPickMinScore", meta("extraPickMinScore", "补拿最低分", "number", true, null, 0d, 100000d, null, "1", "补拿分数门槛"));
map.put("deployDistanceKmMin", meta("deployDistanceKmMin", "部署距离最小值(km)", "number", true, null, 0d, 1000d, null, "8", "部署距离下限"));
map.put("deployDistanceKmMax", meta("deployDistanceKmMax", "部署距离最大值(km)", "number", true, null, 0d, 1000d, null, "30", "部署距离上限"));
map.put("deployDistanceKmDefault", meta("deployDistanceKmDefault", "默认部署距离(km)", "number", true, null, 0d, 1000d, null, "15", "默认部署距离"));
map.put("formationSpacingMeters", meta("formationSpacingMeters", "编队间距(米)", "number", true, null, 1d, 100000d, null, "300", "编队间距"));
map.put("formationHeadingOffsetDeg", meta("formationHeadingOffsetDeg", "编队偏转角(度)", "number", true, null, 0d, 360d, null, "15", "编队航向偏移"));
map.put("defaultDeployHeight", meta("defaultDeployHeight", "默认部署高度(米)", "number", true, null, -10000d, 100000d, null, "30", "默认部署高度"));
map.put("heightFollowBlueRatio", meta("heightFollowBlueRatio", "高度跟随比例", "number", true, null, 0d, 100d, null, "0.0", "高度跟随系数"));
map.put("minInterPlatformDistanceMeters", meta("minInterPlatformDistanceMeters", "最小平台间距(米)", "number", true, null, 0d, 100000d, null, "80", "平台最小间距"));
map.put("trackFallbackBearingDeg", meta("trackFallbackBearingDeg", "航迹默认回退方位角", "number", true, null, 0d, 360d, null, "0", "航迹回退方位"));
map.put("fallbackBearingDeg", meta("fallbackBearingDeg", "默认回退方位角", "number", true, null, 0d, 360d, null, "0", "阵位回退方位"));
map.put("trackExtraNodesMax", meta("trackExtraNodesMax", "航迹额外插点数", "number", true, null, 0d, 1000d, null, "0", "额外插值节点"));
map.put("trackShortPathSegments", meta("trackShortPathSegments", "短路径分段数", "number", true, null, 1d, 1000d, null, "3", "最短路径分段"));
map.put("trackFlankOffsetMeters", meta("trackFlankOffsetMeters", "flank偏移(米)", "number", true, null, 0d, 100000d, null, "800", "侧向偏移"));
map.put("trackJamWobbleMeters", meta("trackJamWobbleMeters", "jam摆动振幅(米)", "number", true, null, 0d, 100000d, null, "400", "正弦扰动振幅"));
map.put("trackJamSegments", meta("trackJamSegments", "jam周期数", "number", true, null, 1d, 1000d, null, "4", "正弦周期"));
map.put("groupClusterSize", meta("groupClusterSize", "编组人数上限", "number", true, null, 1d, 1000d, null, "3", "固定人数编组上限"));
map.put("groupMinMembersForWingman", meta("groupMinMembersForWingman", "生成僚机最小人数", "number", true, null, 1d, 1000d, null, "2", "僚机人数阈值"));
map.put("wingmanDistanceBaseMeters", meta("wingmanDistanceBaseMeters", "僚机基础距离(米)", "number", true, null, 0d, 100000d, null, "100", "僚机基础距离"));
map.put("wingmanDistanceStepMeters", meta("wingmanDistanceStepMeters", "僚机距离步长(米)", "number", true, null, 0d, 100000d, null, "50", "僚机距离步长"));
map.put("wingmanAngleBaseDeg", meta("wingmanAngleBaseDeg", "僚机基础角度(度)", "number", true, null, 0d, 360d, null, "50", "僚机基础角度"));
map.put("wingmanAngleStepDeg", meta("wingmanAngleStepDeg", "僚机角度步长(度)", "number", true, null, 0d, 360d, null, "15", "僚机角度步长"));
map.put("wingmanAltBaseMeters", meta("wingmanAltBaseMeters", "僚机基础高度(米)", "number", true, null, -10000d, 100000d, null, "40", "僚机基础高度"));
map.put("wingmanAltScale", meta("wingmanAltScale", "僚机高度缩放", "number", true, null, 0d, 100d, null, "1.0", "僚机高度系数"));
map.put("minRangeToAllowAssignKm", meta("minRangeToAllowAssignKm", "允许分配最小射程(km)", "number", true, null, 0d, 100000d, null, "0", "射程过滤阈值"));
map.put("tieBreak", meta("tieBreak", "并列决策方式", "enum", true, Arrays.asList("equipmentId"), null, null, null, "equipmentId", "并列评分决策"));
map.put("positionAnchorMode", meta("positionAnchorMode", "阵位锚点模式", "enum", true, Arrays.asList("hybrid"), null, null, null, "hybrid", "当前仅支持hybrid"));
map.put("trackPointDirectionMode", meta("trackPointDirectionMode", "航向计算模式", "enum", true, Arrays.asList("head2next", "tail2prev"), null, null, null, "head2next", "航向计算方式"));
map.put("warZoneClampMode", meta("warZoneClampMode", "作战区约束模式", "enum", true, Arrays.asList("nearestInside"), null, null, null, "nearestInside", "当前仅支持nearestInside"));
map.put("trackRouteNameSuffix", meta("trackRouteNameSuffix", "航迹名称后缀", "string", true, null, null, null, null, "航迹", "航迹名称后缀"));
map.put("groupDrawNameSuffix", meta("groupDrawNameSuffix", "编组名称后缀", "string", true, null, null, null, null, "编组", "编组名称后缀"));
map.put("groupDrawNameWithIndex", meta("groupDrawNameWithIndex", "编组名称带序号", "bool", true, null, null, null, null, "false", "是否追加序号"));
map.put("outputDrawNameSuffix", meta("outputDrawNameSuffix", "任务名称后缀", "string", true, null, null, null, null, "打击任务", "装备匹配后的名称后缀"));
map.put("trackGroundTrackType", meta("trackGroundTrackType", "地面航迹类型", "string", true, null, null, null, null, "routeLineGround", "非飞行航迹类型"));
map.put("rangeUnit", meta("rangeUnit", "射程单位", "enum", true, Arrays.asList("km", "m"), null, null, null, "km", "射程单位"));
map.put("bluePlatformKeywords_air", meta("bluePlatformKeywords_air", "蓝方空中平台关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "F-16,J-10,F-35", "英文逗号分隔"));
map.put("redPreferredWhenBlueAir", meta("redPreferredWhenBlueAir", "红方空中偏好关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "防空,导弹,无人机", "英文逗号分隔"));
map.put("airTaskKeywords", meta("airTaskKeywords", "空中任务关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "空中,制空,拦截,空战", "英文逗号分隔"));
map.put("groundTaskKeywords", meta("groundTaskKeywords", "地面任务关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "地面,突击,登陆", "英文逗号分隔"));
map.put("redPreferredWhenGround", meta("redPreferredWhenGround", "红方地面偏好关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "远火,榴弹,炮,火箭", "英文逗号分隔"));
map.put("tankKeywords", meta("tankKeywords", "坦克关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "坦克,装甲", "英文逗号分隔"));
map.put("redMatchKeywords_tank", meta("redMatchKeywords_tank", "红方反坦克关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "反坦克", "英文逗号分隔"));
map.put("missileKeywords", meta("missileKeywords", "导弹关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "导弹,火箭弹,巡航", "英文逗号分隔"));
map.put("redMatchKeywords_missile", meta("redMatchKeywords_missile", "红方导弹匹配关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "防空,导弹,导弹发射", "英文逗号分隔"));
map.put("radToTargetsCsv", meta("radToTargetsCsv", "命中率映射", "string", true, null, null, null, "^\\d+(?:\\.\\d+)?:\\d+(?:,\\d+(?:\\.\\d+)?:\\d+)*$", "0.8:1,0.5:2,0.2:3", "阈值:目标数,英文逗号分隔"));
map.put("distanceByPlatformCsv", meta("distanceByPlatformCsv", "按平台部署距离映射", "string", false, null, null, null, "^(|[^,:]+:\\d+(?:\\.\\d+)?(?:,[^,:]+:\\d+(?:\\.\\d+)?)*?)$", "防空:18,反坦克:10", "关键词:距离,英文逗号分隔"));
map.put("trackAirDataTypeCsv", meta("trackAirDataTypeCsv", "空中dataType关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "taskPlane,air,plane,flight", "英文逗号分隔"));
map.put("trackAirKeywordsCsv", meta("trackAirKeywordsCsv", "空中关键词", "csv", true, null, null, null, "^[^,]+(?:,[^,]+)*$", "机,飞,空,J-,F-", "英文逗号分隔"));
map.put("rangeParseRegex", meta("rangeParseRegex", "射程提取正则", "string", true, null, null, null, null, "(\\\\d+(?:\\\\.\\\\d+)?)", "Java正则表达式"));
return map;
}
private RuleParamMeta meta(String key, String label, String valueType, boolean required, List<String> enumOptions,
Double min, Double max, String pattern, String example, String description) {
RuleParamMeta m = new RuleParamMeta();
m.setParamKey(key);
m.setLabel(label);
m.setValueType(valueType);
m.setRequired(required);
m.setEnumOptions(enumOptions);
m.setMin(min);
m.setMax(max);
m.setPattern(pattern);
m.setExample(example);
m.setDescription(description);
return m;
}
}

View File

@@ -0,0 +1,105 @@
package com.solution.rule.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.solution.rule.domain.config.RuleConfigParam;
import com.solution.rule.mapper.RuleConfigMapper;
import com.solution.rule.service.RuleDrlSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@Service
public class RuleDrlSyncServiceImpl implements RuleDrlSyncService {
@Autowired
private RuleConfigMapper ruleConfigMapper;
@Override
public void syncGlobalParamsToDrl() {
Path drlPath = resolveDrlPath();
try {
String content = Files.readString(drlPath, StandardCharsets.UTF_8);
String generated = generateParamPutLines(ruleConfigMapper.selectEnabledParamsForGlobal());
String newContent = replaceBuildParamBody(content, generated);
Files.writeString(drlPath, newContent, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("同步 rule.drl 失败: " + e.getMessage(), e);
}
}
private Path resolveDrlPath() {
Path root = Paths.get(System.getProperty("user.dir"));
Path path = root.resolve("auto-solution-rule/src/main/resources/rules/rule.drl");
if (Files.exists(path)) {
return path;
}
Path fallback = root.resolve("src/main/resources/rules/rule.drl");
if (Files.exists(fallback)) {
return fallback;
}
throw new RuntimeException("未找到 rule.drl 文件路径");
}
private String generateParamPutLines(List<RuleConfigParam> params) {
StringBuilder sb = new StringBuilder();
sb.append(" // ===== 以下由规则配置表自动同步生成(请勿手改 param.put 段) =====\n");
if (CollUtil.isEmpty(params)) {
return sb.toString();
}
for (RuleConfigParam param : params) {
if (param == null || param.getParamKey() == null) {
continue;
}
String valueExpr = toDrlValueExpr(param.getParamVal(), param.getValType());
sb.append(" param.put(\"")
.append(escapeJavaString(param.getParamKey()))
.append("\", ")
.append(valueExpr)
.append(");\n");
}
return sb.toString();
}
private String toDrlValueExpr(String val, String valType) {
if ("bool".equalsIgnoreCase(valType) || "boolean".equalsIgnoreCase(valType)) {
return String.valueOf(Boolean.parseBoolean(val));
}
if ("number".equalsIgnoreCase(valType)) {
if (val == null || val.trim().isEmpty()) {
return "0";
}
return val.trim();
}
// string/json 统一按字符串写入,多个值用英文逗号分隔时保持原样,不做拆分
return "\"" + escapeJavaString(val == null ? "" : val) + "\"";
}
private String replaceBuildParamBody(String content, String generatedLines) {
String marker = " // ===== 以下由规则配置表自动同步生成(请勿手改 param.put 段) =====";
int buildParamStart = content.indexOf("function Map buildParam(){");
int returnPos = content.indexOf(" return param;", buildParamStart);
if (buildParamStart < 0 || returnPos < 0) {
throw new RuntimeException("rule.drl 中未找到 buildParam 函数结构");
}
int oldMarker = content.indexOf(marker, buildParamStart);
int insertFrom;
if (oldMarker > 0 && oldMarker < returnPos) {
insertFrom = oldMarker;
} else {
// 首次同步:保留原内容,追加自动生成段
insertFrom = returnPos;
}
return content.substring(0, insertFrom) + generatedLines + "\n" + content.substring(returnPos);
}
private String escapeJavaString(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}

View File

@@ -82,12 +82,13 @@
</resultMap>
<select id="findComponentsByPlatformId" resultMap="VPlatformComponentMap">
SELECT * FROM platform_component
WHERE platform_id=#{platformId}
WHERE platform_id=#{platformId} AND platform_component.type = "comm"
</select>
<resultMap id="VPBasicPlatformMap" type="com.solution.rule.domain.BasicPlatform">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="behaviortreeId" column="behaviortree_id"/>
<result property="description" column="description"/>
</resultMap>
<select id="findAllBasicPlatformComponents" resultMap="VPBasicPlatformMap">
@@ -112,13 +113,21 @@
FROM platform p
LEFT JOIN platform_component pc ON p.id = pc.platform_id
WHERE pc.type = 'comm'
AND p.scenario_id = #{scenarioId}
AND p.scenario_id = #{scenarioId}
ORDER BY p.name,pc.name
</select>
<!-- SELECT
p.id,
p.name,
p.description
FROM platform p
LEFT JOIN platform_component pc ON p.id = pc.platform_id
WHERE pc.type = 'comm'
AND p.scenario_id = #{scenarioId}
ORDER BY p.name,pc.name
</select>
<select id="findAllPlatformComponents" resultMap="VPlatformMap">
SELECT *
FROM platform
<select id="findAllPlatformComponents" resultMap="VPlatformMap">
SELECT *
FROM platform-->
</select>
</mapper>
</mapper>

View File

@@ -0,0 +1,222 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.solution.rule.mapper.RuleConfigMapper">
<resultMap id="RuleConfigMap" type="com.solution.rule.domain.config.RuleConfig">
<id property="id" column="id"/>
<result property="ruleCode" column="rule_code"/>
<result property="ruleName" column="rule_name"/>
<result property="levelCode" column="level_code"/>
<result property="kindCode" column="kind_code"/>
<result property="moduleCode" column="module_code"/>
<result property="priorityNo" column="priority_no"/>
<result property="conditionExpr" column="condition_expr"/>
<result property="actionExpr" column="action_expr"/>
<result property="versionNo" column="version_no"/>
<result property="enabled" column="enabled"/>
<result property="remark" column="remark"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<resultMap id="RuleConfigParamMap" type="com.solution.rule.domain.config.RuleConfigParam">
<result property="ruleCode" column="rule_code"/>
<result property="paramKey" column="param_key"/>
<result property="paramVal" column="param_val"/>
<result property="valType" column="val_type"/>
<result property="paramName" column="param_name"/>
<result property="sortNo" column="sort_no"/>
<result property="enabled" column="enabled"/>
<result property="remark" column="remark"/>
</resultMap>
<resultMap id="RuleDictItemMap" type="com.solution.rule.domain.config.RuleDictItem">
<result property="dictType" column="dict_type"/>
<result property="dictCode" column="dict_code"/>
<result property="dictName" column="dict_name"/>
<result property="sortNo" column="sort_no"/>
<result property="enabled" column="enabled"/>
<result property="remark" column="remark"/>
</resultMap>
<resultMap id="RuleConfigTaskTypeRowMap" type="com.solution.rule.domain.config.RuleConfigTaskTypeRow">
<result property="ruleCode" column="rule_code"/>
<result property="taskTypeCode" column="task_type_code"/>
</resultMap>
<select id="selectRuleConfigList" resultMap="RuleConfigMap">
SELECT id, rule_code, rule_name, level_code, kind_code, module_code, priority_no,
condition_expr, action_expr, version_no, enabled, remark, created_at, updated_at
FROM rule_item
<where>
<if test="ruleCode != null and ruleCode != ''">
AND rule_code = #{ruleCode}
</if>
<if test="ruleName != null and ruleName != ''">
AND rule_name LIKE CONCAT('%', #{ruleName}, '%')
</if>
<if test="levelCode != null and levelCode != ''">
AND level_code = #{levelCode}
</if>
<if test="kindCode != null and kindCode != ''">
AND kind_code = #{kindCode}
</if>
<if test="moduleCode != null and moduleCode != ''">
AND module_code = #{moduleCode}
</if>
<if test="enabled != null">
AND enabled = #{enabled}
</if>
</where>
ORDER BY priority_no ASC, updated_at DESC
</select>
<select id="selectRuleConfigByCode" resultMap="RuleConfigMap">
SELECT id, rule_code, rule_name, level_code, kind_code, module_code, priority_no,
condition_expr, action_expr, version_no, enabled, remark, created_at, updated_at
FROM rule_item
WHERE rule_code = #{ruleCode}
</select>
<select id="countByRuleCode" resultType="int">
SELECT COUNT(1)
FROM rule_item
WHERE rule_code = #{ruleCode}
</select>
<insert id="insertRuleConfig" parameterType="com.solution.rule.domain.config.RuleConfig">
INSERT INTO rule_item
(rule_code, rule_name, level_code, kind_code, module_code, priority_no, condition_expr,
action_expr, version_no, enabled, remark, created_at, updated_at)
VALUES
(#{ruleCode}, #{ruleName}, #{levelCode}, #{kindCode}, #{moduleCode}, #{priorityNo}, #{conditionExpr},
#{actionExpr}, #{versionNo}, #{enabled}, #{remark}, NOW(), NOW())
</insert>
<update id="updateRuleConfig" parameterType="com.solution.rule.domain.config.RuleConfig">
UPDATE rule_item
<set>
<if test="ruleName != null">rule_name = #{ruleName},</if>
<if test="levelCode != null">level_code = #{levelCode},</if>
<if test="kindCode != null">kind_code = #{kindCode},</if>
<if test="moduleCode != null">module_code = #{moduleCode},</if>
<if test="priorityNo != null">priority_no = #{priorityNo},</if>
<if test="conditionExpr != null">condition_expr = #{conditionExpr},</if>
<if test="actionExpr != null">action_expr = #{actionExpr},</if>
<if test="versionNo != null">version_no = #{versionNo},</if>
<if test="enabled != null">enabled = #{enabled},</if>
<if test="remark != null">remark = #{remark},</if>
updated_at = NOW()
</set>
WHERE rule_code = #{ruleCode}
</update>
<delete id="deleteRuleConfigByCodes">
DELETE FROM rule_item
WHERE rule_code IN
<foreach item="code" collection="ruleCodes" open="(" separator="," close=")">
#{code}
</foreach>
</delete>
<select id="selectParamsByRuleCode" resultMap="RuleConfigParamMap">
SELECT rule_code, param_key, param_val, val_type, param_name, sort_no, enabled, remark
FROM rule_item_param
WHERE rule_code = #{ruleCode}
ORDER BY sort_no ASC, id ASC
</select>
<select id="selectParamsByRuleCodes" resultMap="RuleConfigParamMap">
SELECT rule_code, param_key, param_val, val_type, param_name, sort_no, enabled, remark
FROM rule_item_param
<where>
<if test="ruleCodes != null and ruleCodes.size() &gt; 0">
rule_code IN
<foreach collection="ruleCodes" item="c" open="(" separator="," close=")">
#{c}
</foreach>
</if>
<if test="ruleCodes == null or ruleCodes.size() == 0">
1 = 0
</if>
</where>
ORDER BY rule_code ASC, sort_no ASC, id ASC
</select>
<delete id="deleteParamsByRuleCodes">
DELETE FROM rule_item_param
WHERE rule_code IN
<foreach item="code" collection="ruleCodes" open="(" separator="," close=")">
#{code}
</foreach>
</delete>
<insert id="insertParamsBatch">
INSERT INTO rule_item_param
(rule_code, param_key, param_val, val_type, param_name, sort_no, enabled, remark, created_at, updated_at)
VALUES
<foreach item="item" collection="params" separator=",">
(#{item.ruleCode}, #{item.paramKey}, #{item.paramVal}, #{item.valType}, #{item.paramName},
#{item.sortNo}, #{item.enabled}, #{item.remark}, NOW(), NOW())
</foreach>
</insert>
<select id="selectTaskTypesByRuleCode" resultType="string">
SELECT task_type_code
FROM rule_item_task_type
WHERE rule_code = #{ruleCode}
ORDER BY id ASC
</select>
<select id="selectTaskTypesByRuleCodes" resultMap="RuleConfigTaskTypeRowMap">
SELECT rule_code, task_type_code
FROM rule_item_task_type
<where>
<if test="ruleCodes != null and ruleCodes.size() &gt; 0">
rule_code IN
<foreach collection="ruleCodes" item="c" open="(" separator="," close=")">
#{c}
</foreach>
</if>
<if test="ruleCodes == null or ruleCodes.size() == 0">
1 = 0
</if>
</where>
ORDER BY rule_code ASC, id ASC
</select>
<delete id="deleteTaskTypesByRuleCodes">
DELETE FROM rule_item_task_type
WHERE rule_code IN
<foreach item="code" collection="ruleCodes" open="(" separator="," close=")">
#{code}
</foreach>
</delete>
<insert id="insertTaskTypesBatch">
INSERT INTO rule_item_task_type (rule_code, task_type_code, created_at)
VALUES
<foreach item="taskType" collection="taskTypes" separator=",">
(#{ruleCode}, #{taskType}, NOW())
</foreach>
</insert>
<select id="selectDictByType" resultMap="RuleDictItemMap">
SELECT dict_type, dict_code, dict_name, sort_no, enabled, remark
FROM rule_dict
WHERE dict_type = #{dictType}
ORDER BY sort_no ASC, id ASC
</select>
<select id="selectEnabledParamsForGlobal" resultMap="RuleConfigParamMap">
SELECT p.rule_code, p.param_key, p.param_val, p.val_type, p.param_name, p.sort_no, p.enabled, p.remark
FROM rule_item_param p
INNER JOIN rule_item r ON p.rule_code = r.rule_code
WHERE r.enabled = 1
AND p.enabled = 1
ORDER BY r.priority_no ASC, p.sort_no ASC, p.id ASC
</select>
</mapper>

View File

@@ -78,8 +78,8 @@ function Map buildParam(){
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
param.put("missileScore", 1);
// ===================== 目标分配参数(写入 Tasks.task.execute =====================
// executeTypeDefault生成 execute[0] 的类型字段
// ===================== 目标分配参数(写入 Tasks.task.execute =====================
// executeTypeDefault生成 execute[0] 的类型字段 取值strike_test/assault
param.put("executeTypeDefault", "assault");
// targetPickModeroundRobin(稳定轮询) / random(伪随机但同输入稳定)
param.put("targetPickMode", "roundRobin");
@@ -200,16 +200,111 @@ function Map buildParam(){
param.put("wingmanAltBaseMeters", 40);
param.put("wingmanAltScale", 1.0);
// ===== 以下由规则配置表自动同步生成(请勿手改 param.put 段) =====
param.put("groupRuleEnabled", true);
param.put("groupDrawNameSuffix", "编组");
param.put("groupDrawNameWithIndex", false);
param.put("groupFormationMode", "onePerRed");
param.put("groupClusterSize", 3);
param.put("groupLeaderPickMode", "byHitRateThenId");
param.put("groupMinMembersForWingman", 2);
param.put("wingmanDistanceBaseMeters", 100);
param.put("wingmanDistanceStepMeters", 50);
param.put("wingmanAngleBaseDeg", 50);
param.put("wingmanAngleStepDeg", 15);
param.put("wingmanAltBaseMeters", 40);
param.put("wingmanAltScale", 1.0);
param.put("enableTrackWarZoneClamp", true);
param.put("trackRuleEnabled", true);
param.put("trackRouteAlgorithm", "followBlue");
param.put("trackRouteNameSuffix", "航迹");
param.put("trackAirDataTypeCsv", "taskPlane,air,plane,flight");
param.put("trackAirKeywordsCsv", "机,飞,空,J-,F-,无人机,直升机");
param.put("trackGroundTrackType", "routeLineGround");
param.put("trackFallbackBearingDeg", 0);
param.put("trackExtraNodesMax", 0);
param.put("trackShortPathSegments", 3);
param.put("trackFlankOffsetMeters", 800);
param.put("trackFlankSideMode", "alternate");
param.put("trackJamWobbleMeters", 400);
param.put("trackJamSegments", 4);
param.put("enableWarZoneClamp", true);
param.put("positionRuleEnabled", true);
param.put("positionAnchorMode", "hybrid");
param.put("trackPointDirectionMode", "head2next");
param.put("fallbackBearingDeg", 0);
param.put("deployDistanceKmMin", 8);
param.put("deployDistanceKmMax", 30);
param.put("deployDistanceKmDefault", 15);
param.put("distanceByPlatformCsv", "");
param.put("formationType", "line");
param.put("formationSpacingMeters", 300);
param.put("formationHeadingOffsetDeg", 15);
param.put("defaultDeployHeight", 30);
param.put("heightFollowBlueRatio", 0.0);
param.put("warZoneClampMode", "nearestInside");
param.put("minInterPlatformDistanceMeters", 80);
param.put("redHitRateThreshold", 0.6);
param.put("maxExtraWeaponsPerTask", 2);
param.put("maxSupplementRounds", 2);
param.put("extraPickMinScore", 1);
param.put("executeTypeDefault", "assault");
param.put("targetPickMode", "roundRobin");
param.put("minTargetsPerRed", 1);
param.put("maxTargetsPerRedCap", 3);
param.put("radToTargetsCsv", "0.8:1,0.5:2,0.2:3");
param.put("rangeParseRegex", "(\\\\d+(?:\\\\.\\\\d+)?)");
param.put("rangeUnit", "km");
param.put("minRangeToAllowAssignKm", 0);
param.put("weight", 1);
param.put("minSelectedScore", 1);
param.put("tieBreak", "equipmentId");
param.put("outputDrawNameSuffix", "打击任务");
param.put("ruleSlotCount", 3);
param.put("blueRuleKeywords_1", "F-16,F-35");
param.put("redRuleKeywords_1", "防空,导弹,无人机");
param.put("ruleScore_1", 5);
param.put("blueRuleKeywords_2", "坦克,装甲");
param.put("redRuleKeywords_2", "反坦克");
param.put("ruleScore_2", 4);
param.put("blueRuleKeywords_3", "地面,突击");
param.put("redRuleKeywords_3", "远火,榴弹,炮");
param.put("ruleScore_3", 2);
param.put("bluePlatformKeywords_air", "F-16,J-10,F-35");
param.put("redPreferredWhenBlueAir", "防空,导弹,无人机,直升机,空空");
param.put("airScore", 2);
param.put("airTaskKeywords", "空中,制空,拦截,空战");
param.put("airTaskScore", 10);
param.put("groundTaskKeywords", "地面,突击,登陆");
param.put("redPreferredWhenGround", "远火,榴弹,炮,火箭");
param.put("groundScore", 1);
param.put("tankKeywords", "坦克,装甲");
param.put("redMatchKeywords_tank", "反坦克");
param.put("tankScore", 1);
param.put("missileKeywords", "导弹,火箭弹,巡航");
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
param.put("missileScore", 1);
return param;
}
function void mergeDefaultParams(Map current){
Map defaults = buildParam();
for (Object k : defaults.keySet()) {
if (!current.containsKey(k)) {
current.put(k, defaults.get(k));
}
}
}
rule "装备匹配"
salience 100
when
$fact : DroolsFact(task != null)
then
// 以本文件 buildParam 为真源覆盖同名键,再执行 Java 侧匹配逻辑
globalParams.putAll(buildParam());
// globalParams.putAll(buildParam());
mergeDefaultParams(globalParams);
equipmentRule($fact, globalParams);
end
@@ -219,7 +314,8 @@ when
$fact : DroolsFact(task != null)
then
// 显式目标分配规则:填充 Tasks.task.execute.targetList[*].targetId
globalParams.putAll(buildParam());
// globalParams.putAll(buildParam());
mergeDefaultParams(globalParams);
target($fact, globalParams);
end
@@ -229,7 +325,8 @@ when
$fact : DroolsFact(task != null)
then
// 显式阵位规则:填充 redWeapons.SubComponents.platform[].positions
globalParams.putAll(buildParam());
// globalParams.putAll(buildParam());
mergeDefaultParams(globalParams);
position($fact, globalParams);
end
@@ -239,7 +336,8 @@ when
$fact : DroolsFact(task != null)
then
// 显式航迹规则:填充 TrackParam 下各航迹 id并绑定 execute[0].targetList[*].moveRouteId
globalParams.putAll(buildParam());
// globalParams.putAll(buildParam());
mergeDefaultParams(globalParams);
trackRoute($fact, globalParams);
end
@@ -249,6 +347,7 @@ when
$fact : DroolsFact(task != null)
then
// 显式编组规则:填充 TrackParam.GroupsgroupType=addGroup与 wingmanData
globalParams.putAll(buildParam());
// globalParams.putAll(buildParam());
mergeDefaultParams(globalParams);
groupFormation($fact, globalParams);
end

View File

@@ -0,0 +1,67 @@
-- 规则主数据表结构MySQL 8+
-- 说明:用于前端按“层级->种类->规则项”进行展示与增删改查。
CREATE TABLE IF NOT EXISTS `rule_dict` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`dict_type` VARCHAR(32) NOT NULL COMMENT '字典类型(level/kind/task_type/status)',
`dict_code` VARCHAR(64) NOT NULL COMMENT '字典编码',
`dict_name` VARCHAR(64) NOT NULL COMMENT '字典名称',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '排序号',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(1是0否)',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rule_dict_type_code` (`dict_type`, `dict_code`),
KEY `idx_rule_dict_type_enabled` (`dict_type`, `enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='规则字典表';
CREATE TABLE IF NOT EXISTS `rule_item` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`rule_code` VARCHAR(64) NOT NULL COMMENT '规则编码',
`rule_name` VARCHAR(128) NOT NULL COMMENT '规则名称',
`level_code` VARCHAR(32) NOT NULL COMMENT '规则层级(task/action/platform)',
`kind_code` VARCHAR(32) NOT NULL COMMENT '规则种类(select/assign/deploy/config/mode/spacetime/relation/limit)',
`module_code` VARCHAR(32) NOT NULL COMMENT '规则模块(equipment/target/position/track/group)',
`priority_no` INT NOT NULL DEFAULT 100 COMMENT '优先级(数字越小越先执行)',
`condition_expr` VARCHAR(1024) DEFAULT NULL COMMENT '条件表达式(展示用)',
`action_expr` VARCHAR(1024) DEFAULT NULL COMMENT '动作表达式(展示用)',
`version_no` INT NOT NULL DEFAULT 1 COMMENT '版本号',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(1是0否)',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rule_item_code` (`rule_code`),
KEY `idx_rule_item_level_kind` (`level_code`, `kind_code`),
KEY `idx_rule_item_module_enabled` (`module_code`, `enabled`),
KEY `idx_rule_item_priority` (`priority_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='规则主表';
CREATE TABLE IF NOT EXISTS `rule_item_task_type` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`rule_code` VARCHAR(64) NOT NULL COMMENT '规则编码',
`task_type_code` VARCHAR(32) NOT NULL COMMENT '任务类型编码',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rule_task_type` (`rule_code`, `task_type_code`),
KEY `idx_rule_task_type` (`task_type_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='规则适用任务类型关联表';
CREATE TABLE IF NOT EXISTS `rule_item_param` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`rule_code` VARCHAR(64) NOT NULL COMMENT '规则编码',
`param_key` VARCHAR(128) NOT NULL COMMENT '参数键',
`param_val` TEXT NOT NULL COMMENT '参数值(统一文本存储)',
`val_type` VARCHAR(16) NOT NULL DEFAULT 'string' COMMENT '值类型(string/number/bool/json)',
`param_name` VARCHAR(128) DEFAULT NULL COMMENT '参数名称',
`sort_no` INT NOT NULL DEFAULT 0 COMMENT '排序号',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(1是0否)',
`remark` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_rule_param` (`rule_code`, `param_key`),
KEY `idx_rule_param_key` (`param_key`),
KEY `idx_rule_param_enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='规则参数表';

View File

@@ -0,0 +1,211 @@
-- 从 rules/rule.drl 提取的初始化数据
-- 说明:本文件提供字典、规则主数据、规则参数与任务类型关联的初始记录。
-- 1) 字典数据
INSERT INTO `rule_dict` (`dict_type`, `dict_code`, `dict_name`, `sort_no`, `enabled`, `remark`) VALUES
('level', 'task', '任务级', 10, 1, '任务层面的选择/分配/限制'),
('level', 'action', '行动级', 20, 1, '行动层面的航迹/编组/模式'),
('level', 'platform', '平台级', 30, 1, '平台层面的部署/时空约束'),
('kind', 'select', '选择', 10, 1, '对象选择与评分'),
('kind', 'assign', '分配', 20, 1, '目标分配'),
('kind', 'deploy', '部署', 30, 1, '阵位与部署'),
('kind', 'config', '配置', 40, 1, '通用配置参数'),
('kind', 'mode', '工作模式', 50, 1, '算法与模式选择'),
('kind', 'spacetime', '时空约束', 60, 1, '空间和时间相关约束'),
('kind', 'relation', '关联关系', 70, 1, '蓝红关键词关联'),
('kind', 'limit', '限制条件', 80, 1, '阈值和边界'),
('task_type', 'strike', '打击任务', 10, 1, '典型任务类型'),
('task_type', 'recon', '侦察任务', 20, 1, '典型任务类型'),
('task_type', 'intercept', '拦截任务', 30, 1, '典型任务类型'),
('task_type', 'support', '支援任务', 40, 1, '典型任务类型'),
('task_type', 'jamming', '干扰任务', 50, 1, '典型任务类型'),
('status', 'enabled', '启用', 10, 1, '通用状态'),
('status', 'disabled', '停用', 20, 1, '通用状态')
ON DUPLICATE KEY UPDATE
`dict_name` = VALUES(`dict_name`),
`sort_no` = VALUES(`sort_no`),
`enabled` = VALUES(`enabled`),
`remark` = VALUES(`remark`);
-- 2) 规则主数据(层级->种类->规则项)
INSERT INTO `rule_item`
(`rule_code`, `rule_name`, `level_code`, `kind_code`, `module_code`, `priority_no`, `condition_expr`, `action_expr`, `version_no`, `enabled`, `remark`)
VALUES
('R_TASK_SELECT_BASE', '任务级-装备选择基础规则', 'task', 'select', 'equipment', 100, 'task!=null', 'equipmentRule(fact, params)', 1, 1, '来自装备匹配主流程'),
('R_TASK_SELECT_SLOT_1', '任务级-规则槽1', 'task', 'relation', 'equipment', 101, 'containsAny(blue,slot1Blue)&&containsAny(red,slot1Red)', 'score += ruleScore_1*weight', 1, 1, '蓝红关键词槽位匹配'),
('R_TASK_SELECT_SLOT_2', '任务级-规则槽2', 'task', 'relation', 'equipment', 102, 'containsAny(blue,slot2Blue)&&containsAny(red,slot2Red)', 'score += ruleScore_2*weight', 1, 1, '蓝红关键词槽位匹配'),
('R_TASK_SELECT_SLOT_3', '任务级-规则槽3', 'task', 'relation', 'equipment', 103, 'containsAny(blue,slot3Blue)&&containsAny(red,slot3Red)', 'score += ruleScore_3*weight', 1, 1, '蓝红关键词槽位匹配'),
('R_TASK_REL_AIR_PLATFORM', '任务级-关联关系-空中平台', 'task', 'relation', 'equipment', 104, 'bluePlatformKeywords_air && redPreferredWhenBlueAir', 'score += airScore*weight', 1, 1, '兼容层空中平台关联'),
('R_TASK_REL_AIR_TASK', '任务级-关联关系-空中任务', 'task', 'relation', 'equipment', 105, 'airTaskKeywords && redPreferredWhenBlueAir', 'score += airTaskScore*weight', 1, 1, '兼容层空中任务关联'),
('R_TASK_REL_GROUND_TASK', '任务级-关联关系-地面任务', 'task', 'relation', 'equipment', 106, 'groundTaskKeywords && redPreferredWhenGround', 'score += groundScore*weight', 1, 1, '兼容层地面任务关联'),
('R_TASK_REL_TANK', '任务级-关联关系-坦克装甲', 'task', 'relation', 'equipment', 107, 'tankKeywords && redMatchKeywords_tank', 'score += tankScore*weight', 1, 1, '兼容层坦克关联'),
('R_TASK_REL_MISSILE', '任务级-关联关系-导弹火箭', 'task', 'relation', 'equipment', 108, 'missileKeywords && redMatchKeywords_missile', 'score += missileScore*weight', 1, 1, '兼容层导弹关联'),
('R_TASK_ASSIGN_TARGET', '任务级-目标分配规则', 'task', 'assign', 'target', 90, 'task!=null', 'target(fact, params)', 1, 1, '目标分配与execute填充'),
('R_TASK_LIMIT_SUPPLEMENT', '任务级-低命中率补拿限制', 'task', 'limit', 'target', 89, 'hitRate<threshold', '补拿装备并更新任务', 1, 1, '补拿轮次和数量限制'),
('R_PLATFORM_DEPLOY', '平台级-阵位部署规则', 'platform', 'deploy', 'position', 80, 'positionRuleEnabled=true', 'position(fact, params)', 1, 1, '平台部署与编队参数'),
('R_PLATFORM_SPACETIME', '平台级-时空约束规则', 'platform', 'spacetime', 'position', 79, 'enableWarZoneClamp=true', '平台点位约束到作战区', 1, 1, '阵位空间约束'),
('R_ACTION_TRACK_ROUTE', '行动级-航迹生成规则', 'action', 'mode', 'track', 70, 'trackRuleEnabled=true', 'trackRoute(fact, params)', 1, 1, '航迹算法与路由绑定'),
('R_ACTION_TRACK_SPACETIME', '行动级-航迹时空约束', 'action', 'spacetime', 'track', 69, 'enableTrackWarZoneClamp=true', '航迹点约束到作战区', 1, 1, '航迹空间约束'),
('R_ACTION_GROUP_FORMATION', '行动级-编组规则', 'action', 'mode', 'group', 60, 'groupRuleEnabled=true', 'groupFormation(fact, params)', 1, 1, '编组与wingman规则')
ON DUPLICATE KEY UPDATE
`rule_name` = VALUES(`rule_name`),
`level_code` = VALUES(`level_code`),
`kind_code` = VALUES(`kind_code`),
`module_code` = VALUES(`module_code`),
`priority_no` = VALUES(`priority_no`),
`condition_expr` = VALUES(`condition_expr`),
`action_expr` = VALUES(`action_expr`),
`version_no` = VALUES(`version_no`),
`enabled` = VALUES(`enabled`),
`remark` = VALUES(`remark`);
-- 3) 规则参数(来自 rule.drl 的 buildParam
INSERT INTO `rule_item_param`
(`rule_code`, `param_key`, `param_val`, `val_type`, `param_name`, `sort_no`, `enabled`, `remark`)
VALUES
-- R_TASK_SELECT_BASE
('R_TASK_SELECT_BASE','weight','1','number','全局权重',10,1,'评分乘数'),
('R_TASK_SELECT_BASE','minSelectedScore','1','number','最小选中分',20,1,'低于该分不选中'),
('R_TASK_SELECT_BASE','tieBreak','equipmentId','string','并列决策方式',30,1,'equipmentId字典序'),
('R_TASK_SELECT_BASE','outputDrawNameSuffix','打击任务','string','输出任务后缀',40,1,'匹配成功后缀'),
('R_TASK_SELECT_BASE','ruleSlotCount','3','number','规则槽数量',50,1,'槽位匹配条数'),
-- R_TASK_SELECT_SLOT_1~3
('R_TASK_SELECT_SLOT_1','blueRuleKeywords_1','F-16,F-35','string','蓝方关键词1',10,1,'规则槽1'),
('R_TASK_SELECT_SLOT_1','redRuleKeywords_1','防空,导弹,无人机','string','红方关键词1',20,1,'规则槽1'),
('R_TASK_SELECT_SLOT_1','ruleScore_1','5','number','规则槽1分值',30,1,'规则槽1'),
('R_TASK_SELECT_SLOT_2','blueRuleKeywords_2','坦克,装甲','string','蓝方关键词2',10,1,'规则槽2'),
('R_TASK_SELECT_SLOT_2','redRuleKeywords_2','反坦克','string','红方关键词2',20,1,'规则槽2'),
('R_TASK_SELECT_SLOT_2','ruleScore_2','4','number','规则槽2分值',30,1,'规则槽2'),
('R_TASK_SELECT_SLOT_3','blueRuleKeywords_3','地面,突击','string','蓝方关键词3',10,1,'规则槽3'),
('R_TASK_SELECT_SLOT_3','redRuleKeywords_3','远火,榴弹,炮','string','红方关键词3',20,1,'规则槽3'),
('R_TASK_SELECT_SLOT_3','ruleScore_3','2','number','规则槽3分值',30,1,'规则槽3'),
-- R_TASK_REL_AIR_PLATFORM
('R_TASK_REL_AIR_PLATFORM','bluePlatformKeywords_air','F-16,J-10,F-35','string','蓝方空中平台关键词',10,1,'兼容层'),
('R_TASK_REL_AIR_PLATFORM','redPreferredWhenBlueAir','防空,导弹,无人机,直升机,空空','string','红方空中偏好关键词',20,1,'兼容层'),
('R_TASK_REL_AIR_PLATFORM','airScore','2','number','空中平台分值',30,1,'兼容层'),
-- R_TASK_REL_AIR_TASK
('R_TASK_REL_AIR_TASK','airTaskKeywords','空中,制空,拦截,空战','string','空中任务关键词',10,1,'兼容层'),
('R_TASK_REL_AIR_TASK','airTaskScore','10','number','空中任务分值',20,1,'兼容层'),
-- R_TASK_REL_GROUND_TASK
('R_TASK_REL_GROUND_TASK','groundTaskKeywords','地面,突击,登陆','string','地面任务关键词',10,1,'兼容层'),
('R_TASK_REL_GROUND_TASK','redPreferredWhenGround','远火,榴弹,炮,火箭','string','红方地面偏好关键词',20,1,'兼容层'),
('R_TASK_REL_GROUND_TASK','groundScore','1','number','地面任务分值',30,1,'兼容层'),
-- R_TASK_REL_TANK
('R_TASK_REL_TANK','tankKeywords','坦克,装甲','string','坦克关键词',10,1,'兼容层'),
('R_TASK_REL_TANK','redMatchKeywords_tank','反坦克','string','红方反坦克关键词',20,1,'兼容层'),
('R_TASK_REL_TANK','tankScore','1','number','坦克分值',30,1,'兼容层'),
-- R_TASK_REL_MISSILE
('R_TASK_REL_MISSILE','missileKeywords','导弹,火箭弹,巡航','string','导弹关键词',10,1,'兼容层'),
('R_TASK_REL_MISSILE','redMatchKeywords_missile','防空,导弹,导弹发射','string','红方导弹匹配关键词',20,1,'兼容层'),
('R_TASK_REL_MISSILE','missileScore','1','number','导弹分值',30,1,'兼容层'),
-- R_TASK_ASSIGN_TARGET
('R_TASK_ASSIGN_TARGET','executeTypeDefault','assault','string','执行类型默认值',10,1,'目标分配'),
('R_TASK_ASSIGN_TARGET','targetPickMode','roundRobin','string','目标选择模式',20,1,'roundRobin/random'),
('R_TASK_ASSIGN_TARGET','minTargetsPerRed','1','number','每红装最少目标数',30,1,'目标分配'),
('R_TASK_ASSIGN_TARGET','maxTargetsPerRedCap','3','number','每红装最多目标数',40,1,'目标分配'),
('R_TASK_ASSIGN_TARGET','radToTargetsCsv','0.8:1,0.5:2,0.2:3','string','命中率映射目标数',50,1,'阈值映射'),
('R_TASK_ASSIGN_TARGET','rangeParseRegex','(\\\\d+(?:\\\\.\\\\d+)?)','string','射程提取正则',60,1,'提取首个数字'),
('R_TASK_ASSIGN_TARGET','rangeUnit','km','string','射程单位',70,1,'km/m'),
('R_TASK_ASSIGN_TARGET','minRangeToAllowAssignKm','0','number','允许分配最小射程',80,1,'射程过滤'),
-- R_TASK_LIMIT_SUPPLEMENT
('R_TASK_LIMIT_SUPPLEMENT','redHitRateThreshold','0.6','number','红装命中率阈值',10,1,'低于阈值触发补拿'),
('R_TASK_LIMIT_SUPPLEMENT','maxExtraWeaponsPerTask','2','number','每任务最大补拿装备数',20,1,'限制条件'),
('R_TASK_LIMIT_SUPPLEMENT','maxSupplementRounds','2','number','补拿最大轮次',30,1,'防止死循环'),
('R_TASK_LIMIT_SUPPLEMENT','extraPickMinScore','1','number','补拿最小匹配分',40,1,'限制条件'),
-- R_PLATFORM_DEPLOY
('R_PLATFORM_DEPLOY','positionRuleEnabled','true','bool','是否启用阵位规则',10,1,'部署开关'),
('R_PLATFORM_DEPLOY','positionAnchorMode','hybrid','string','阵位锚点模式',20,1,'当前使用hybrid'),
('R_PLATFORM_DEPLOY','trackPointDirectionMode','head2next','string','航向计算模式',30,1,'head2next/tail2prev'),
('R_PLATFORM_DEPLOY','fallbackBearingDeg','0','number','默认航向角',40,1,'无法计算时回退'),
('R_PLATFORM_DEPLOY','deployDistanceKmMin','8','number','部署距离最小值(km)',50,1,'部署约束'),
('R_PLATFORM_DEPLOY','deployDistanceKmMax','30','number','部署距离最大值(km)',60,1,'部署约束'),
('R_PLATFORM_DEPLOY','deployDistanceKmDefault','15','number','默认部署距离(km)',70,1,'部署距离'),
('R_PLATFORM_DEPLOY','distanceByPlatformCsv','','string','按平台覆盖部署距离',80,1,'关键词:距离'),
('R_PLATFORM_DEPLOY','formationType','line','string','编队样式',90,1,'line/wedge/circle'),
('R_PLATFORM_DEPLOY','formationSpacingMeters','300','number','编队间距(米)',100,1,'部署参数'),
('R_PLATFORM_DEPLOY','formationHeadingOffsetDeg','15','number','编队偏转角(度)',110,1,'部署参数'),
('R_PLATFORM_DEPLOY','defaultDeployHeight','30','number','默认部署高度(米)',120,1,'部署参数'),
('R_PLATFORM_DEPLOY','heightFollowBlueRatio','0.0','number','高度跟随比例',130,1,'部署参数'),
('R_PLATFORM_DEPLOY','warZoneClampMode','nearestInside','string','作战区约束模式',140,1,'部署约束'),
('R_PLATFORM_DEPLOY','minInterPlatformDistanceMeters','80','number','平台最小间距(米)',150,1,'部署约束'),
-- R_PLATFORM_SPACETIME
('R_PLATFORM_SPACETIME','enableWarZoneClamp','true','bool','是否启用作战区约束',10,1,'平台时空约束'),
-- R_ACTION_TRACK_ROUTE
('R_ACTION_TRACK_ROUTE','trackRuleEnabled','true','bool','是否启用航迹规则',10,1,'航迹开关'),
('R_ACTION_TRACK_ROUTE','trackRouteAlgorithm','followBlue','string','航迹算法',20,1,'followBlue/shortestPath/flank/jam'),
('R_ACTION_TRACK_ROUTE','trackRouteNameSuffix','航迹','string','航迹名称后缀',30,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackAirDataTypeCsv','taskPlane,air,plane,flight','string','空中航迹dataType关键词',40,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackAirKeywordsCsv','机,飞,空,J-,F-,无人机,直升机','string','空中航迹关键词',50,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackGroundTrackType','routeLineGround','string','地面航迹类型',60,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackFallbackBearingDeg','0','number','航迹默认回退航向角',70,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackExtraNodesMax','0','number','航迹额外插点上限',80,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackShortPathSegments','3','number','最短路径插值分段',90,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackFlankOffsetMeters','800','number','flank偏移距离(米)',100,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackFlankSideMode','alternate','string','flank侧向模式',110,1,'alternate/left/right'),
('R_ACTION_TRACK_ROUTE','trackJamWobbleMeters','400','number','jam摆动振幅(米)',120,1,'行动模式'),
('R_ACTION_TRACK_ROUTE','trackJamSegments','4','number','jam摆动周期数',130,1,'行动模式'),
-- R_ACTION_TRACK_SPACETIME
('R_ACTION_TRACK_SPACETIME','enableTrackWarZoneClamp','true','bool','是否启用航迹作战区约束',10,1,'行动时空约束'),
-- R_ACTION_GROUP_FORMATION
('R_ACTION_GROUP_FORMATION','groupRuleEnabled','true','bool','是否启用编组规则',10,1,'编组开关'),
('R_ACTION_GROUP_FORMATION','groupDrawNameSuffix','编组','string','编组名称后缀',20,1,'编组规则'),
('R_ACTION_GROUP_FORMATION','groupDrawNameWithIndex','false','bool','编组名称是否加序号',30,1,'编组规则'),
('R_ACTION_GROUP_FORMATION','groupFormationMode','onePerRed','string','编组模式',40,1,'onePerRed/clusterByCount/singleGroup'),
('R_ACTION_GROUP_FORMATION','groupClusterSize','3','number','按人数编组的每组上限',50,1,'编组规则'),
('R_ACTION_GROUP_FORMATION','groupLeaderPickMode','byHitRateThenId','string','领队选择模式',60,1,'byHitRateThenId/byId'),
('R_ACTION_GROUP_FORMATION','groupMinMembersForWingman','2','number','生成僚机的最小人数',70,1,'编组规则'),
('R_ACTION_GROUP_FORMATION','wingmanDistanceBaseMeters','100','number','僚机基础距离(米)',80,1,'编组几何'),
('R_ACTION_GROUP_FORMATION','wingmanDistanceStepMeters','50','number','僚机距离步长(米)',90,1,'编组几何'),
('R_ACTION_GROUP_FORMATION','wingmanAngleBaseDeg','50','number','僚机基础角度(度)',100,1,'编组几何'),
('R_ACTION_GROUP_FORMATION','wingmanAngleStepDeg','15','number','僚机角度步长(度)',110,1,'编组几何'),
('R_ACTION_GROUP_FORMATION','wingmanAltBaseMeters','40','number','僚机基础高度(米)',120,1,'编组几何'),
('R_ACTION_GROUP_FORMATION','wingmanAltScale','1.0','number','僚机高度缩放系数',130,1,'编组几何')
ON DUPLICATE KEY UPDATE
`param_val` = VALUES(`param_val`),
`val_type` = VALUES(`val_type`),
`param_name` = VALUES(`param_name`),
`sort_no` = VALUES(`sort_no`),
`enabled` = VALUES(`enabled`),
`remark` = VALUES(`remark`);
-- 4) 规则适用任务类型(默认全部规则覆盖五类任务,后续可在前端按需调整)
INSERT IGNORE INTO `rule_item_task_type` (`rule_code`, `task_type_code`)
SELECT r.rule_code, t.task_type_code
FROM (
SELECT 'R_TASK_SELECT_BASE' AS rule_code
UNION ALL SELECT 'R_TASK_SELECT_SLOT_1'
UNION ALL SELECT 'R_TASK_SELECT_SLOT_2'
UNION ALL SELECT 'R_TASK_SELECT_SLOT_3'
UNION ALL SELECT 'R_TASK_REL_AIR_PLATFORM'
UNION ALL SELECT 'R_TASK_REL_AIR_TASK'
UNION ALL SELECT 'R_TASK_REL_GROUND_TASK'
UNION ALL SELECT 'R_TASK_REL_TANK'
UNION ALL SELECT 'R_TASK_REL_MISSILE'
UNION ALL SELECT 'R_TASK_ASSIGN_TARGET'
UNION ALL SELECT 'R_TASK_LIMIT_SUPPLEMENT'
UNION ALL SELECT 'R_PLATFORM_DEPLOY'
UNION ALL SELECT 'R_PLATFORM_SPACETIME'
UNION ALL SELECT 'R_ACTION_TRACK_ROUTE'
UNION ALL SELECT 'R_ACTION_TRACK_SPACETIME'
UNION ALL SELECT 'R_ACTION_GROUP_FORMATION'
) r
CROSS JOIN (
SELECT 'strike' AS task_type_code
UNION ALL SELECT 'recon'
UNION ALL SELECT 'intercept'
UNION ALL SELECT 'support'
UNION ALL SELECT 'jamming'
) t;

View File

@@ -0,0 +1,204 @@
{
"assign_target": [
{
"task": "task1",
"type": "fire",
"target": [
"enemy_p1",
"enemyp2"
]
},
{
"task": "task2",
"type": "fire",
"target": [
"enemy_p1333",
"enemyp2333"
]
},
{
"task": "task3",
"type": "radar",
"target": [
"enemy_p1444",
"enemyp2444"
]
}
],
"task": [
{
"task_name": "task1",
"platform": [
{
"name": "platform1",
"platform_type": "WSF_PLATFORM",
"longitude": "120:49:24.79e",
"latitude": "23:47:26.60n",
"altitude": 0,
"route": [
{
"longitude": "120:49:24.79e",
"latitude": "23:47:26.60n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:49:42.46e",
"latitude": "23:47:42.80n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:19.01e",
"latitude": "23:47:51.84n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
}
]
},
{
"name": "platform2",
"platform_type": "WSF_PLATFORM",
"longitude": "120:50:19.01e",
"latitude": "23:47:51.84n",
"altitude": 0,
"route": [
{
"longitude": "120:50:19.01e",
"latitude": "23:47:51.84n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
}
]
}
],
"time": {
"begin": 50,
"end": 300
}
},
{
"task_name": "task2",
"platform": [
{
"name": "platform3",
"platform_type": "SENSOR_TYPE",
"longitude": "120:50:19.01e",
"latitude": "23:47:51.84n",
"altitude": 0,
"route": [
{
"longitude": "120:50:19.01e",
"latitude": "23:47:51.84n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
},
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
}
]
},
{
"name": "platform5",
"platform_type": "SENSOR_TYPE",
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"route": [
{
"longitude": "120:50:42.73e",
"latitude": "23:48:09.86n",
"altitude": 0,
"speed": 100
}
]
},
{
"name": "platform4",
"platform_type": "JAM_TYPE",
"longitude": "120:49:55.28e",
"latitude": "23:48:38.68n",
"altitude": 0,
"route": [
{
"longitude": "120:49:55.28e",
"latitude": "23:48:38.68n",
"altitude": 0,
"speed": 100
}
]
},
{
"name": "platform9",
"platform_type": "JAM_TYPE",
"longitude": "120:49:55.28e",
"latitude": "23:48:38.68n",
"altitude": 0,
"route": [
{
"longitude": "120:49:55.28e",
"latitude": "23:48:38.68n",
"altitude": 0,
"speed": 100
}
]
}
],
"time": {
"begin": 110,
"end": 300
}
},
{
"task_name": "task3",
"platform": [
{
"name": "platform6",
"platform_type": "WSF_PLATFORM",
"longitude": "120:49:45.11e",
"latitude": "23:48:53.09n",
"altitude": 0,
"route": [
{
"longitude": "120:49:45.11e",
"latitude": "23:48:53.09n",
"altitude": 0,
"speed": 100
}
]
}
],
"time": {
"begin": 50,
"end": 300
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
package com.solution.system.domain;
package com.solution.scene.domain;
import lombok.Data;

View File

@@ -2,6 +2,7 @@ package com.solution.scene.mapper;
import com.solution.scene.domain.AfsimScenario;
import com.solution.scene.domain.AfsimScenarioForm;
import com.solution.scene.domain.PlatformCommunication;
import com.solution.system.domain.Behaviortree;
import org.apache.ibatis.annotations.Mapper;
@@ -34,4 +35,11 @@ public interface SceneMapper {
* @return
*/
List<Behaviortree> selectAllTreeBySceneId(Integer id);
/**
* 根据场景id获取场景下所有关系
* @param id
* @return
*/
List<PlatformCommunication> selectAllRelationBySceneId(Integer id);
}

View File

@@ -2,6 +2,7 @@ package com.solution.scene.service;
import com.solution.scene.domain.AfsimScenario;
import com.solution.scene.domain.AfsimScenarioForm;
import com.solution.scene.domain.PlatformCommunication;
import com.solution.system.domain.Behaviortree;
import java.util.List;
@@ -33,4 +34,11 @@ public interface SceneService {
* @return
*/
List<Behaviortree> getAllTree(Integer id);
/**
* 根据场景id获取场景下所有关系
* @param id
* @return
*/
List<PlatformCommunication> getAllRelation(Integer id);
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.solution.common.constant.ExceptionConstants;
import com.solution.scene.domain.AfsimScenario;
import com.solution.scene.domain.AfsimScenarioForm;
import com.solution.scene.domain.PlatformCommunication;
import com.solution.scene.mapper.PlatFormCommunicationMapper;
import com.solution.scene.mapper.SceneMapper;
import com.solution.scene.service.SceneService;
@@ -90,4 +91,18 @@ public class SceneServiceImpl implements SceneService {
return allTree;
}
/**
* 根据场景id获取场景下所有关系
* @param id
* @return
*/
@Override
public List<PlatformCommunication> getAllRelation(Integer id) {
List<PlatformCommunication> result = sceneMapper.selectAllRelationBySceneId(id);
if(CollUtil.isEmpty( result)){
throw new RuntimeException("该场景下不存在关系");
}
return result;
}
}

View File

@@ -32,6 +32,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
WHERE scenario_id=#{id}
</select>
<resultMap id="PlatformCommunicationResultMap" type="com.solution.scene.domain.PlatformCommunication">
<id property="id" column="id"/>
<result property="commandPlatform" column="command_platform"/>
<result property="subordinatePlatform" column="subordinate_platform"/>
<result property="commandComm" column="command_comm"/>
<result property="subordinateComm" column="subordinate_comm"/>
<result property="scenaryId" column="scenary_id"/>
</resultMap>
<select id="selectAllRelationBySceneId" resultMap="PlatformCommunicationResultMap"
parameterType="java.lang.Integer">
SELECT id,command_platform,subordinate_platform,command_comm,subordinate_comm,scenary_id
FROM platform_communication
WHERE scenary_id = #{id}
</select>
<insert id="update" parameterType="com.solution.scene.domain.AfsimScenario">
update afsim_scenario
set name=#{name},

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
"@antv/g6": "^5.0.49",
"@ant-design/icons-vue": "^7.0.1",
"@antv/x6": "^3.1.2",
"@antv/x6-vue-shape": "^3.0.2",
"ant-design-vue": "^4.2.6",

View File

@@ -58,4 +58,12 @@ export const routes: RouteRecordRaw[] = [
},
component: () => import('@/views/decision/rule/management.vue'),
},
{
name: 'decision-rule-config',
path: '/app/decision/rule-config',
meta: {
title: '规则聚合测试',
},
component: () => import('@/views/decision/rule-config/management.vue'),
},
]

View File

@@ -853,6 +853,7 @@
.ks-model-builder-content,
.ks-model-builder-right {
height: calc(100vh - 90px);
overflow: auto;
.ant-card-body {
padding: 6px;
@@ -987,6 +988,7 @@
.ant-collapse-content-box {
max-height: 37vh;
height: fit-content;
overflow: auto;
}
}
@@ -1555,6 +1557,44 @@
border: 1px solid #0f4a7c;
color: #eee;
}
// 修复 mini 模式分页滚动条问题
&.ant-pagination-mini {
height: 24px;
line-height: 24px;
.ant-pagination-simple-pager {
height: 24px;
line-height: 24px;
input {
height: 22px;
line-height: 20px;
padding: 0 4px;
margin: 0 2px;
box-sizing: border-box;
}
}
.ant-pagination-total-text {
height: 24px;
line-height: 24px;
}
.ant-pagination-prev,
.ant-pagination-next,
.ant-pagination-jump-prev,
.ant-pagination-jump-next {
height: 24px;
line-height: 22px;
.ant-pagination-item-link {
height: 22px;
line-height: 20px;
padding: 0;
}
}
}
}
@@ -1877,3 +1917,149 @@
padding-left: 5px;
}
}
/* rule-config 页:侧栏与主内容区视觉统一(仅带 .rule-config-sidebar 时生效) */
.rule-config-sidebar.ks-layout-sidebar {
background-color: #020a14;
background-image:
linear-gradient(180deg, rgba(2, 10, 20, 0.94) 0%, rgba(2, 12, 24, 0.9) 50%, rgba(1, 8, 18, 0.92) 100%),
url('@/assets/icons/bg-fk.png');
background-size: 100% 100%, 100% 100%;
background-position: center, center;
border-inline-end: 1px solid rgba(55, 126, 173, 0.35);
.ks-sidebar-header {
background: url('@/assets/icons/bg-fk-title.png') center / 100% 100%;
}
.ant-tree {
background: transparent;
color: #eee;
.ant-tree-node-content-wrapper {
color: #eee;
&:hover {
background: rgba(9, 38, 75, 0.55);
}
}
.ant-tree-node-content-wrapper.ant-tree-node-selected {
background: rgba(17, 55, 126, 0.65);
}
.ant-tree-switcher {
color: #a2b1ba;
}
.ant-tree-title {
color: inherit;
}
}
.ant-pagination {
color: #a2b1ba;
.ant-pagination-item-link,
.ant-pagination-item a {
color: #a2b1ba;
}
}
}
.rule-config-main-split {
display: flex;
flex-direction: row;
align-items: stretch;
overflow: hidden;
padding-right: 0;
}
.rule-config-graph-placeholder {
flex-shrink: 0;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed rgba(55, 126, 173, 0.45);
margin: 8px 0 8px 8px;
border-radius: 2px;
color: #a2b1ba;
background: rgba(8, 29, 54, 0.25);
}
.rule-config-graph-placeholder--chart {
align-items: stretch;
justify-content: stretch;
padding: 6px;
}
.rule-config-main-split-resizer {
flex: 0 0 6px;
width: 6px;
margin: 8px 0;
cursor: col-resize;
flex-shrink: 0;
border-radius: 2px;
background: rgba(55, 126, 173, 0.25);
&:hover {
background: rgba(55, 126, 173, 0.5);
}
}
.rule-config-right-cluster {
display: flex;
flex: 1 1 auto;
min-width: 0;
align-items: stretch;
}
.rule-config-graph-placeholder__text {
font-size: 14px;
}
.rule-config-panel-toggle {
flex: 0 0 20px;
width: 20px;
margin: 8px 0;
padding: 0;
border: 0;
border-radius: 2px 0 0 2px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #eee;
background: rgba(9, 38, 75, 0.75);
&:hover {
background: rgba(17, 55, 126, 0.85);
}
}
.rule-config-right-panel {
flex: 0 0 min(480px, 42vw);
width: min(480px, 42vw);
min-width: 320px;
max-width: 55%;
transition: flex-basis 0.2s ease, width 0.2s ease, opacity 0.2s ease, min-width 0.2s ease;
overflow: hidden;
border-left: 1px solid rgba(71, 95, 113, 0.6);
}
.rule-config-right-panel--collapsed {
flex: 0 0 0;
width: 0;
min-width: 0;
max-width: 0;
opacity: 0;
border-left: 0;
pointer-events: none;
}
.rule-config-right-panel__inner {
height: 100%;
overflow-y: auto;
padding: 10px 10px 10px 4px;
}

View File

@@ -8,9 +8,10 @@
*/
import { HttpRequestClient } from '@/utils/request';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest, CommunicationRelationsResponse } from './types';
import type { PlatformWithComponentsResponse } from '../types';
import type { BasicResponse } from '@/types';
import type { BehaviorTree } from '../designer/tree';
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
@@ -32,6 +33,29 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
};
/**
* 获取场景的所有通信关系
* @param id 场景ID
* @returns 通信关系列表
*/
export const findRelations = (id: number): Promise<CommunicationRelationsResponse> => {
return req.get<CommunicationRelationsResponse>(`/system/scene/getAllRelation/${id}`);
};
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
};
// 获取场景下的所有行为树列表
export const getAllBehaviorTreesBySceneId = (sceneId: number): Promise<{ code: number; msg: string; data: BehaviorTree[] }> => {
return req.get<{ code: number; msg: string; data: BehaviorTree[] }>(`/system/scene/getAllTree/${sceneId}`);
};
// 更新平台的行为树id
export const updateBehaviorTreeIdOfPlatform = (query: { id: number, behaviortreeId: number}): Promise<BasicResponse> => {
return req.putJson<BasicResponse>(`/system/behaviortree/behaviortreeId`, query);
};
// 更新行为树的平台id
export const updateBehaviorTree = (behaviorTree: BehaviorTree): Promise<BasicResponse> => {
return req.putJson<BasicResponse>(`/system/behaviortree`, behaviorTree);
};

View File

@@ -21,6 +21,14 @@
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
<div class="ks-model-builder-actions">
<a-space>
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleGenerateRandom">
<ThunderboltOutlined />
<span>随机生成</span>
</a-button>
<a-button v-if="graph && currentScenario && currentScenario.id > 0" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleLoadFromBackend">
<DatabaseOutlined />
<span>从后端加载</span>
</a-button>
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
<CheckOutlined />
<span>保存</span>
@@ -50,22 +58,26 @@ import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import { getTeleport } from '@antv/x6-vue-shape';
import { Graph, Node, type NodeProperties } from '@antv/x6';
import { CheckCircleOutlined, CheckOutlined, RollbackOutlined, SaveOutlined } from '@ant-design/icons-vue';
import { CheckCircleOutlined, CheckOutlined, DatabaseOutlined, RollbackOutlined, SaveOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
import { Wrapper } from '@/components/wrapper';
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
import Header from '../header.vue';
import type { Scenario } from './types';
import type { PlatformWithComponents } from '../types';
import { createLineOptions, type GraphContainer, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
import { createLineOptions, type GraphContainer, type GraphEdgeElement, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
import { registerScenarioElement } from './register';
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue';
import { findOneScenarioById, saveScenario } from './api';
import { findOneScenarioById, saveScenario, findRelations, getAllBehaviorTreesBySceneId, findPlatformWithComponents } from './api';
import { resolveConnectionRelation } from './relation';
import { generateRandomCommunicationData } from '../graph/random-data-generator';
import { convertRecordsToGraphContainer, type CommunicationRecord } from '../graph/data-converter';
import { log } from 'echarts/types/src/util/log.js';
import { router } from '@/router';
const TeleportContainer = defineComponent(getTeleport());
@@ -81,6 +93,8 @@ export default defineComponent({
CheckCircleOutlined,
CheckOutlined,
RollbackOutlined,
ThunderboltOutlined,
DatabaseOutlined,
TeleportContainer,
},
setup() {
@@ -92,7 +106,6 @@ export default defineComponent({
const isDraggingOver = ref(false);
const currentScenarioEditing = ref<boolean>(false);
const currentScenario = ref<Scenario | null>(null);
const currentGraph = ref<GraphContainer | null>(null);
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
const selectedNodeTaskElement = ref<GraphTaskElement | null>(null);
const changed = ref<boolean>(false);
@@ -211,25 +224,132 @@ export default defineComponent({
}
};
const handleSelect = (scenario: Scenario) => {
const handleSelect = async (scenario: Scenario) => {
let nodeGraph: GraphContainer | null = null;
try {
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
} catch (e: any) {
console.error('parse error,cause:', e);
}
if (!nodeGraph) {
nodeGraph = {
nodes: [],
edges: [],
};
}
// 设置当前场景
currentScenario.value = {
...scenario,
graph: nodeGraph,
graph: nodeGraph || { nodes: [], edges: [] },
relations: []
};
console.log('选中场景:', currentScenario.value);
currentScenarioEditing.value = true;
// 并行加载通信关系和行为树列表
if (scenario.id > 0) {
try {
// 1. 加载通信关系(如果没有已保存的图数据)
if (!nodeGraph) {
message.loading({ content: '正在加载通信关系...', key: 'loading-relations' });
const response = await findRelations(scenario.id);
console.log('API完整响应:', response);
// 解析API响应支持多种格式
let relations: any[] = [];
if (Array.isArray(response.data)) {
relations = response.data;
} else if (response.data && Array.isArray((response.data as any).data)) {
relations = (response.data as any).data;
} else if (response.data && Array.isArray((response.data as any).rows)) {
relations = (response.data as any).rows;
} else if (response.data && Array.isArray((response.data as any).list)) {
relations = (response.data as any).list;
}
console.log('解析后的通信关系数量:', relations.length);
if (relations.length > 0) {
// 字段名标准化(驼峰转下划线)
const normalizedRelations = relations.map((item: any) => ({
id: item.id,
command_platform: item.commandPlatform || item.command_platform,
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
command_comm: item.commandComm || item.command_comm,
subordinate_comm: item.subordinateComm || item.subordinate_comm,
scenary_id: item.scenaryId || item.scenary_id,
}));
console.log('标准化后的第一条记录:', normalizedRelations[0]);
// 获取平台列表(包含完整的组件信息)
message.loading({ content: '正在获取平台信息...', key: 'loading-platforms' });
const platformResponse = await findPlatformWithComponents(currentScenario.value.id);
if (!platformResponse.data || platformResponse.data.length === 0) {
console.warn('未能获取平台列表,将使用简化的组件信息');
message.destroy('loading-platforms');
// 即使没有平台数据也继续转换传入场景ID
const convertedGraph = convertRecordsToGraphContainer(
normalizedRelations,
undefined,
undefined,
currentScenario.value.id
);
console.log('转换后的图数据:', convertedGraph);
currentScenario.value.graph = convertedGraph;
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
message.success({ content: `成功加载 ${normalizedRelations.length} 条通信关系(无详细组件信息)`, key: 'loading-relations' });
return;
}
console.log(`获取到 ${platformResponse.data.length} 个平台的完整数据`);
platformResponse.data.forEach(platform => {
console.log(` 平台 "${platform.name}" (ID=${platform.id}) 有 ${platform.components?.length || 0} 个组件:`,
platform.components?.map(c => c.name));
});
// 转换为图数据传入完整的平台数据数组和场景ID
const convertedGraph = convertRecordsToGraphContainer(
normalizedRelations,
undefined,
platformResponse.data,
currentScenario.value.id
);
// 验证转换后的节点数据结构
console.log('转换后的图数据:', convertedGraph);
// 更新当前场景的图数据
currentScenario.value.graph = convertedGraph;
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
message.success({ content: `成功加载 ${normalizedRelations.length} 条通信关系`, key: 'loading-relations' });
} else {
message.warning({ content: '该场景暂无通信关系数据', key: 'loading-relations' });
}
}
// 2. 加载行为树列表并缓存到graph对象
const treesResponse = await getAllBehaviorTreesBySceneId(scenario.id);
if (treesResponse.code === 200 && treesResponse.data) {
console.log('[communication] 行为树列表加载完成:', treesResponse.data.length, '个');
// 将行为树列表存储到graph对象中供node.vue使用
if (graph.value) {
(graph.value as any).behaviorTrees = treesResponse.data;
}
} else {
console.warn('[communication] 行为树列表加载失败或为空');
if (graph.value) {
(graph.value as any).behaviorTrees = [];
}
}
} catch (error) {
console.error('从后端加载数据失败:', error);
message.error({ content: '加载数据失败', key: 'loading-relations' });
}
}
createElements();
};
@@ -261,6 +381,7 @@ export default defineComponent({
}, 100); // 延迟一会儿,免得连线错位
}
}
}, 100);
});
};
@@ -277,10 +398,12 @@ export default defineComponent({
nodes: [],
},
};
currentGraph.value = {
edges: [],
nodes: [],
};
// 清空graph中的场景信息
if (graph.value) {
(graph.value as any).currentScenario = null;
}
selectedModelNode.value = null;
selectedNodeTaskElement.value = null;
@@ -313,13 +436,19 @@ export default defineComponent({
handleGraphEvent('node:dblclick', (args: any) => {
const node = args.node as Node<NodeProperties>;
const element = node.getData() as GraphTaskElement;
console.error('element',element)
window.location.href = `/app/decision/designer?scenario=${currentScenario.value?.id}&platform=${element?.platformId?? ''}`
window.location.href = `/app/decision/designer?scenarioId=${currentScenario.value?.id}&behaviortreeId=${element?.behaviortreeId ?? ''}`
// destroy()
// window.location.href = '/app/decision/designer'
// 通过router跳转在初始化节点上存在渲染时机的问题导致节点未渲染
// router.push({
// path: '/app/decision/designer'
// path: '/app/decision/designer',
// query: {
// scenarioId: currentScenario.value?.id?.toString() ?? '',
// behaviortreeId: element?.behaviortreeId?.toString() ?? '',
// },
// })
});
@@ -380,10 +509,12 @@ export default defineComponent({
const handleSave = () => {
const graphData: GraphContainer = resolveGraph(graph.value as Graph);
console.log(graph.value);
const relations = resolveConnectionRelation(graph.value as Graph);
console.error('relations',relations)
console.info('handleSave', graphData);
if (!currentScenario.value) {
message.error('当前决策树不存在');
@@ -415,6 +546,193 @@ export default defineComponent({
});
};
// 随机生成节点流图
const handleGenerateRandom = () => {
if (!graph.value) {
message.error('画布未初始化');
return;
}
try {
// 生成随机数据
const { records, graph: randomGraph } = generateRandomCommunicationData(30);
console.log('生成的随机数据:', records);
console.log('转换后的图数据:', randomGraph);
// 清空现有内容
graph.value.clearCells();
// 设置当前场景
if (!currentScenario.value) {
currentScenario.value = {
id: 0,
name: `随机场景_${Date.now()}`,
description: '自动生成的测试场景',
communicationGraph: null,
relations: [],
graph: randomGraph,
};
} else {
currentScenario.value.graph = randomGraph;
currentScenario.value.communicationGraph = JSON.stringify(randomGraph);
}
// 渲染节点
setTimeout(() => {
if (randomGraph.nodes) {
randomGraph.nodes.forEach(ele => {
const node = createGraphScenarioElement(ele as GraphTaskElement);
graph.value?.addNode(node as Node);
});
}
// 延迟添加边,确保节点已渲染
setTimeout(() => {
if (randomGraph.edges) {
randomGraph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
});
}
// 自动适应视图
fitToScreen();
message.success(`已生成 ${randomGraph.nodes.length} 个节点和 ${randomGraph.edges.length} 条连接线`);
}, 100);
}, 50);
} catch (error) {
console.error('随机生成时出错:', error);
message.error('生成失败,请重试');
}
};
// 从后端加载平台数据并转换为通信关系图(当前使用模拟数据)
const handleLoadFromBackend = async () => {
if (!graph.value || !currentScenario.value) {
message.error('请先选择场景');
return;
}
try {
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
// 调用真实API获取通信关系
console.log(`正在从后端加载场景 ${currentScenario.value.id} 的通信关系...`);
const response = await findRelations(currentScenario.value.id);
console.log('API完整响应:', response);
console.log('response.data类型:', typeof response.data, Array.isArray(response.data) ? 'Array' : 'Object');
// API返回的是 CommunicationRelationRecord[],与 CommunicationRecord 结构兼容
// 处理可能的多种返回格式
let relations: any[] = [];
if (Array.isArray(response.data)) {
relations = response.data;
} else if (response.data && Array.isArray((response.data as any).data)) {
relations = (response.data as any).data;
} else if (response.data && Array.isArray((response.data as any).rows)) {
relations = (response.data as any).rows;
} else if (response.data && Array.isArray((response.data as any).list)) {
relations = (response.data as any).list;
}
console.log('解析后的通信关系数量:', relations.length);
if (relations.length > 0) {
console.log('第一条记录:', JSON.stringify(relations[0], null, 2));
}
// 后端返回的是驼峰命名,需要转换为下划线命名以匹配前端类型
const normalizedRelations = relations.map((item: any) => ({
id: item.id,
command_platform: item.commandPlatform || item.command_platform,
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
command_comm: item.commandComm || item.command_comm,
subordinate_comm: item.subordinateComm || item.subordinate_comm,
scenary_id: item.scenaryId || item.scenary_id,
}));
console.log('标准化后的第一条记录:', normalizedRelations[0]);
if (normalizedRelations.length === 0) {
console.warn('API未返回任何通信关系数据使用模拟数据作为fallback');
// Fallback到模拟数据保留以便测试
relations.push(
{ id: 6, command_platform: 'chief', subordinate_platform: 'task1_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 7, command_platform: 'chief', subordinate_platform: 'task2_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 8, command_platform: 'chief', subordinate_platform: 'task3_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 9, command_platform: 'task1_commander', subordinate_platform: 'platform1', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 10, command_platform: 'task1_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 11, command_platform: 'task1_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 12, command_platform: 'task1_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 13, command_platform: 'task1_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 14, command_platform: 'task2_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 15, command_platform: 'task2_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 16, command_platform: 'task2_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 17, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 18, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 19, command_platform: 'task3_commander', subordinate_platform: 'platform7', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 20, command_platform: 'task3_commander', subordinate_platform: 'platform8', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
);
}
console.log('最终使用的通信记录:', normalizedRelations);
// 使用数据进行转换
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
console.log('转换后的图数据:', convertedGraph);
// 清空现有内容
graph.value.clearCells();
// 更新当前场景
currentScenario.value.graph = convertedGraph;
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
// 渲染节点
setTimeout(() => {
if (convertedGraph.nodes) {
convertedGraph.nodes.forEach(ele => {
const node = createGraphScenarioElement(ele as GraphTaskElement);
graph.value?.addNode(node as Node);
});
}
// 延迟添加边,确保节点已渲染
setTimeout(() => {
if (convertedGraph.edges) {
convertedGraph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
});
}
// 自动适应视图
fitToScreen();
message.success({
content: `已从后端加载 ${convertedGraph.nodes.length} 个平台和 ${convertedGraph.edges.length} 条连接关系`,
key: 'loading'
});
}, 100);
}, 50);
} catch (error) {
console.error('从后端加载时出错:', error);
message.error({
content: error instanceof Error ? error.message : '加载失败,请重试',
key: 'loading'
});
}
};
// 初始化
onMounted(() => {
init();
@@ -428,7 +746,6 @@ export default defineComponent({
handleCreate,
currentScenarioEditing,
currentScenario,
currentGraph,
selectedNodeTaskElement,
selectedModelNode,
graph,
@@ -444,6 +761,8 @@ export default defineComponent({
handleDrop,
isDraggingOver,
handleSave,
handleGenerateRandom,
handleLoadFromBackend,
handleUpdateElement,
handleSelect,
};

View File

@@ -1,84 +1,116 @@
<template>
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
<a-card
:class="[
'ks-scenario-node',
`ks-scenario-${element?.category ?? 'model'}-node`
]"
hoverable
<div>
<a-dropdown
:trigger="['contextmenu']"
:getPopupContainer="getPopupContainer"
@openChange="handleVisibleChange"
>
<template #title>
<a-space>
<span class="ks-scenario-node-title">{{ element?.description ?? element?.name ?? '-' }}</span>
</a-space>
</template>
<!-- 节点内容区域 -->
<div class="w-full">
<div class="ks-scenario-node-content">
<div
v-for="(item, index) in element?.components || []"
:key="item.id || index"
class="ks-scenario-node-row"
>
<a-card
:class="[
'ks-scenario-node',
`ks-scenario-${element?.category ?? 'model'}-node`
]"
hoverable
>
<template #title>
<a-space>
<span class="ks-scenario-node-title">{{ element?.description ?? element?.name ?? '-' }}</span>
</a-space>
</template>
<!-- 节点内容区域 -->
<div class="w-full">
<div class="ks-scenario-node-content">
<div
:data-port="`in-${item.id || index}`"
:port="`in-${item.id || index}`"
:title="`入桩: ${item.name}`"
class="port port-in"
magnet="passive"
:data-item="JSON.stringify(item)"
v-for="(item, index) in element?.components || []"
:key="item.id || index"
class="ks-scenario-node-row"
>
<div class="triangle-left"></div>
</div>
<!-- child名称 -->
<div class="ks-scenario-node-name">
{{ substring(item.description ?? item.name, 20) }}
</div>
<!-- 右侧出桩只能作为连线源 -->
<div
:data-port="`out-${item.id || index}`"
:port="`out-${item.id || index}`"
:title="`出桩: ${item.name}`"
class="port port-out"
magnet="active"
:data-item="JSON.stringify(item)"
>
<div class="triangle-right" ></div>
<div
:data-port="`in-${item.id || index}`"
:port="`in-${item.id || index}`"
:title="`入桩: ${item.name}`"
class="port port-in"
magnet="passive"
:data-item="JSON.stringify(item)"
>
<div class="triangle-left"></div>
</div>
<!-- child名称 -->
<div class="ks-scenario-node-name">
{{ substring(item.description ?? item.name, 20) }}
</div>
<!-- 右侧出桩只能作为连线源 -->
<div
:data-port="`out-${item.id || index}`"
:port="`out-${item.id || index}`"
:title="`出桩: ${item.name}`"
class="port port-out"
magnet="active"
:data-item="JSON.stringify(item)"
>
<div class="triangle-right" ></div>
</div>
</div>
</div>
</div>
</div>
</a-card>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-card>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-sub-menu key="mount">
<template #icon>
<LinkOutlined />
</template>
<template #title>挂载</template>
<a-menu-item
v-for="tree in availableTrees"
:key="`tree-${tree.id}`"
:disabled="isTreeMounted(tree.id)"
@click="() => handleMountTree(tree)"
>
<template #icon>
<CheckOutlined v-if="isTreeMounted(tree.id)" />
</template>
{{ tree.name }}
</a-menu-item>
<a-menu-item v-if="availableTrees.length === 0" disabled>
暂无可用行为树
</a-menu-item>
</a-sub-menu>
<a-menu-divider />
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import { elementProps, type ModelElement } from '../graph';
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
import { DeleteOutlined, LinkOutlined, CheckOutlined, SettingOutlined } from '@ant-design/icons-vue';
import type { Graph } from '@antv/x6';
import { message } from 'ant-design-vue';
import { substring } from '@/utils/strings';
import { updateBehaviorTree, updateBehaviorTreeIdOfPlatform } from './api';
import type { BehaviorTree } from '../designer/tree';
export default defineComponent({
name: 'ModelElement',
components: {
SettingOutlined,
DeleteOutlined,
LinkOutlined,
CheckOutlined,
},
props: elementProps,
setup(_props) {
@@ -87,6 +119,17 @@ export default defineComponent({
);
const updateKey = ref(0);
const isMenuVisible = ref(false);
// 挂载行为树相关状态
const availableTrees = ref<BehaviorTree[]>([]);
// 获取 popup 容器
const getPopupContainer = () => {
if (typeof document !== 'undefined') {
return document.body;
}
return undefined;
};
// 获取画布实例
const getGraph = (): Graph | null => {
@@ -103,8 +146,35 @@ export default defineComponent({
updateKey.value++;
};
// 获取行为树名称
const getBehaviorTreeName = (treeId: number | undefined | null): string => {
if (!treeId) return '';
const tree = availableTrees.value.find(t => t.id === treeId);
return tree?.name || `行为树${treeId}`;
};
// 判断行为树是否已挂载到当前节点
const isTreeMounted = (treeId: number): boolean => {
if (!element.value) return false;
const currentTreeId = (element.value as any).behaviortreeId as number | undefined;
return currentTreeId === treeId;
};
// 处理挂载行为树 - 当右键菜单打开时从graph中读取已缓存的行为树列表
const handleVisibleChange = (visible: boolean) => {
isMenuVisible.value = visible;
if (!visible || !element.value) return;
// 从graph对象中获取已缓存的行为树列表
const graph = _props.graph as any;
if (graph?.behaviorTrees) {
availableTrees.value = graph.behaviorTrees;
console.log('从缓存中读取行为树列表:', availableTrees.value.length, '个');
} else {
availableTrees.value = [];
console.warn('未找到缓存的行为树列表');
}
};
const handleMenuClick = ({ key }: { key: string }) => {
@@ -113,6 +183,35 @@ export default defineComponent({
}
};
// 处理挂载具体的行为树
const handleMountTree = async (tree: BehaviorTree) => {
if (!element.value) return;
try {
// 更新节点的behaviortreeId属性
const updatedElement = { ...(element.value as any), behaviortreeId: tree.id };
// 调用后端API同时更新平台表的 behaviortreeId 和行为树表的 platformId
const platformIdValue = (element.value as any).platformId as number;
const [platformRes, treeRes] = await Promise.all([
updateBehaviorTreeIdOfPlatform({ id: platformIdValue, behaviortreeId: tree.id }),
updateBehaviorTree({ ...tree, platformId: platformIdValue }),
]);
if (platformRes.code === 200 && treeRes.code === 200) {
// 更新本地节点数据
if (_props.node) {
_props.node.setData(updatedElement);
}
message.success(`已成功挂载行为树: ${tree.name}`);
} else {
message.error(platformRes.msg || treeRes.msg || '挂载失败');
}
} catch (error) {
console.error('挂载行为树失败:', error);
message.error('挂载行为树失败');
}
};
const handleDelete = () => {
if (!_props.node) return;
@@ -134,10 +233,37 @@ export default defineComponent({
onMounted(() => {
_props.node?.on('change:data', handleDataChange);
// 监听画布各种事件,操作时立即关闭菜单
const graph = getGraph();
if (graph) {
const closeMenuHandler = () => {
if (isMenuVisible.value) {
isMenuVisible.value = false;
}
};
// 监听多种可能导致菜单位置变化的事件
graph.on('pan', closeMenuHandler);
graph.on('translate', closeMenuHandler);
graph.on('scale', closeMenuHandler);
graph.on('zoom', closeMenuHandler);
graph.on('resize', closeMenuHandler);
}
});
onUnmounted(() => {
_props.node?.off('change:data', handleDataChange);
// 清理事件监听
const graph = getGraph();
if (graph) {
graph.off('pan');
graph.off('translate');
graph.off('scale');
graph.off('zoom');
graph.off('resize');
}
});
return {
@@ -145,6 +271,11 @@ export default defineComponent({
substring,
handleMenuClick,
handleVisibleChange,
availableTrees,
getBehaviorTreeName,
isTreeMounted,
handleMountTree,
getPopupContainer,
};
},
});

View File

@@ -26,7 +26,7 @@ export function resolveConnectionRelation(graph: Graph): PlatformRelation[] {
// 过滤无效/临时边
const validEdges = edges.filter(edge => {
// 过滤临时边X6 拖拽连线时生成的未完成边)
const isTempEdge = edge?.attr('line/stroke') === 'transparent' || edge.id.includes('temp');
const isTempEdge = edge?.attr('line/stroke') === 'transparent' || String(edge.id).includes('temp');
if (isTempEdge) {
tempEdgeIds.add(edge.id);
return false;

View File

@@ -48,3 +48,22 @@ export interface ScenarioDetailsResponse extends ApiDataResponse<Scenario> {
}
/**
* 通信关系记录(对应数据库表结构)
*/
export interface CommunicationRelationRecord {
id: number;
command_platform: string; // 指挥平台名称
subordinate_platform: string; // 下属平台名称
command_comm?: string; // 指挥端通信方式
subordinate_comm?: string; // 下属端通信方式
scenary_id?: number; // 场景ID
}
/**
* 获取场景所有通信关系的响应
*/
export interface CommunicationRelationsResponse extends ApiDataResponse<CommunicationRelationRecord[]> {
}

View File

@@ -10,7 +10,53 @@
import { HttpRequestClient } from '@/utils/request';
import type { NodeTemplatesResponse } from './template';
import type { BehaviorTree, BehaviorTreeDetailsResponse, BehaviorTreePageResponse, BehaviorTreeRequest } from './tree';
import type { BasicResponse } from '@/types';
import type { BasicResponse, PageableResponse } from '@/types';
import type { PlatformListableResponse } from '../types';
export interface TreeTemplateRow {
id: number;
type: string;
name: string;
multiable?: boolean;
}
export interface TreeNodeInstanceRow {
id: number;
treeId: number;
templateId: number;
instanceName: string;
isRoot: number;
}
export interface TreeConnectionRow {
id: number;
treeId: number;
parentNodeId: number;
childNodeId: number;
orderIndex: number;
}
export interface TreeTemplateParameterDefRow {
id: number;
templateId: number;
paramKey: string;
dataType: string;
defaultValue: string;
description: string;
templateType: string;
}
export interface TreeNodeParameterRow {
id: number;
treeId: number;
nodeInstanceId: number;
paramDefId: number;
value: string;
groupIndex: number;
}
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
@@ -19,6 +65,20 @@ const req = HttpRequestClient.create<BasicResponse>({
export const findNodeTemplates = (): Promise<NodeTemplatesResponse> => {
return req.get('/system/nodetemplate/all');
};
export const findTemplateParameterDefs = (query={pageSize: 1000, pageNum: 1}): Promise<PageableResponse<TreeTemplateParameterDefRow>> => {
return req.get('/system/templateparameterdef/list', query);
}
export const findNodeConnections = (query: {treeId: number}): Promise<PageableResponse<TreeConnectionRow>> => {
return req.get('/system/nodeconnection/list', {pageSize: 1000, pageNum: 1, ...query});
};
export const findNodeParameters = (query: {treeId: number}): Promise<PageableResponse<TreeNodeParameterRow>> => {
return req.get('/system/nodeparameter/list', {pageSize: 1000, pageNum: 1, ...query});
}
export const findTreeNodeInstances = (query: {treeId: number}): Promise<PageableResponse<TreeNodeInstanceRow>> => {
return req.get('/system/treenodeinstance/list', {pageSize: 1000, pageNum: 1, ...query});
}
export const findTreesByQuery = (query: Partial<BehaviorTreeRequest> = {}): Promise<BehaviorTreePageResponse> => {
return req.get<BehaviorTreePageResponse>('/system/behaviortree/list', query);
@@ -28,8 +88,8 @@ export const findOneTreeByPlatformId = (platformId: number): Promise<BehaviorTre
return req.get(`/system/behaviortree/platform/${platformId}`);
};
export const findSubPlatforms = (treeId: number): Promise<BehaviorTreeDetailsResponse> => {
return req.get(`/system/behaviortree/underling/${treeId}`);
export const findSubPlatforms = (platformId: number): Promise<PlatformListableResponse> => {
return req.get(`/system/behaviortree/underling/${platformId}`);
};
export const findOneTreeById = (id: number): Promise<BehaviorTreeDetailsResponse> => {

View File

@@ -5,8 +5,15 @@
<a-layout class="ks-layout-body">
<div class="ks-model-builder-body">
<div class="ks-model-builder-left">
<ScenariosCard
ref="scenariosCardRef"
:scenarioId="currentScenarioId"
@select-scenario="handleSelectScenario"
/>
<TressCard
ref="treesCardRef"
:scenarioId="currentScenarioId"
:treeId="currentTreeId"
@create-tree="handleCreateTree"
@select-tree="handleSelectTree"
/>
@@ -24,7 +31,7 @@
<span>保存</span>
</a-button>
<a-button v-if="currentScenarioId" class="ks-model-builder-save" size="small" @click="handleGoback">
<a-button v-if="fromScenarioPage" class="ks-model-builder-save" size="small" @click="handleGoback">
<BackwardFilled />
<span>返回</span>
</a-button>
@@ -71,6 +78,7 @@ import Properties from './properties.vue';
import type { NodeDragTemplate } from './template';
import type { BehaviorTree } from './tree';
import { createGraphTaskElementFromTemplate } from './utils';
import { resolveBehaviorTreeGraph } from './reverse-tree/tree-graph-resolver';
import { createGraphTaskElement, createLineOptions, type GraphContainer, type GraphTaskElement, hasElements, hasRootElementNode, resolveGraph, useGraphCanvas } from '../graph';
import { registerNodeElement } from './register';
@@ -78,7 +86,9 @@ import { findAllBasicPlatforms, findAllNodeCommands } from '../api';
import type { NodeCommand, Platform } from '../types';
import { createTree, findOneTreeById, findOneTreeByPlatformId, updateTree, findSubPlatforms } from './api';
import TressCard from './trees-card.vue';
import ScenariosCard from './scenarios-card.vue';
import NodesCard from './nodes-card.vue';
import { generateKey } from '@/utils/strings';
const TeleportContainer = defineComponent(getTeleport());
@@ -86,6 +96,7 @@ registerNodeElement();
export default defineComponent({
components: {
ScenariosCard,
TressCard,
NodesCard,
Wrapper,
@@ -107,16 +118,18 @@ export default defineComponent({
const isDraggingOver = ref(false);
const currentTreeEditing = ref<boolean>(false);
const currentBehaviorTree = ref<BehaviorTree | null>(null);
const currentGraph = ref<GraphContainer | null>(null);
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
const selectedNodeTaskElement = ref<GraphTaskElement | null>(null);
const changed = ref<boolean>(false);
const scenariosCardRef = ref<InstanceType<typeof ScenariosCard> | null>(null);
const treesCardRef = ref<InstanceType<typeof TressCard> | null>(null);
const platforms = ref<Platform[]>([]);
const subPlatforms = ref<Platform[]>([]);
const nodeCommands = ref<NodeCommand[]>([])
const currentScenarioId = ref<number | null>(null);
const currentScenarioId = ref<number | undefined>();
const currentPlatformId = ref<number | null>(null);
const currentTreeId = ref<number | null>(null);
const fromScenarioPage = ref<boolean>(false);
const {
handleGraphEvent,
@@ -156,20 +169,14 @@ export default defineComponent({
}
// 加载下属平台
const loadSubPlatforms = (treeId: number) => {
console.log(treeId);
if (!treeId || treeId <= 0) {
const loadSubPlatforms = (platformId: number | null) => {
if (!platformId || platformId <= 0) {
subPlatforms.value = [];
return;
}
findSubPlatforms(treeId).then(r => {
if (r.data && Array.isArray(r.data)) {
subPlatforms.value = r.data as Platform[];
} else {
subPlatforms.value = [];
}
findSubPlatforms(platformId).then(r => {
subPlatforms.value = r.data ?? [];
}).catch(err => {
console.error('加载下属平台失败:', err);
subPlatforms.value = [];
@@ -265,42 +272,24 @@ export default defineComponent({
}
};
const handleSelectTree = (tree: BehaviorTree) => {
destroyGraph();
currentPlatformId.value = null;
currentScenarioId.value = null;
const initGraphConfig = (_graph?: GraphContainer) => {
const graph: GraphContainer = _graph ? _graph : {
nodes: [],
edges: [],
};
console.info('handleSelectTree', tree);
findOneTreeById(tree.id).then(r => {
if (r.data) {
let nodeGraph: GraphContainer | null = null;
try {
nodeGraph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as GraphContainer;
} catch (e: any) {
console.error('parse error,cause:', e);
}
if (!nodeGraph) {
nodeGraph = {
nodes: [],
edges: [],
};
}
currentBehaviorTree.value = {
...r.data,
graph: nodeGraph,
};
currentTreeEditing.value = true;
// 加载下属平台
loadSubPlatforms(r.data.id);
nextTick(() => {
initGraph();
});
} else {
message.error(r.msg ?? '行为树不存在.');
}
});
currentBehaviorTree.value = {
id: 0,
name: '行为树',
description: null,
englishName: generateKey('scenario'),
xmlContent: null,
createdAt: null,
platformId: currentPlatformId.value,
scenarioId: currentScenarioId.value,
graph: { ...graph },
updatedAt: null,
};
};
const createElements = () => {
@@ -311,72 +300,107 @@ export default defineComponent({
if (currentBehaviorTree.value?.graph.nodes) {
currentBehaviorTree.value?.graph.nodes.forEach(ele => {
const node = createGraphTaskElement(ele as GraphTaskElement);
console.info('create node: ', ele);
// 将节点添加到画布
graph.value?.addNode(node as Node);
});
}
if (currentBehaviorTree.value?.graph.edges) {
setTimeout(() => {
// 然后添加所有边,确保包含桩点信息
setTimeout(() => {
currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
}, 100); // 延迟一会儿,免得连线错位
}
});
});
fitToScreen();
console.info('create elements: ', currentBehaviorTree.value?.graph);
}
}, 100);
});
});
};
const resolveQuery = ()=> {
let scenarioId = Number(currentRoute.query.scenario);
if (!isNaN(scenarioId)) {
currentScenarioId.value = scenarioId;
} else {
currentScenarioId.value = null;
}
let platformId = Number(currentRoute.query.platform);
if (!isNaN(platformId)) {
currentPlatformId.value = platformId;
} else {
currentPlatformId.value = null;
}
}
const handleCreateTree = () => {
const applyBehaviorTree = (tree: BehaviorTree) => {
destroyGraph();
currentBehaviorTree.value = {
id: 0,
name: '行为树',
description: null,
englishName: null,
xmlContent: null,
createdAt: null,
platformId: currentPlatformId.value,
graph: {
edges: [],
nodes: [],
},
updatedAt: null,
};
currentGraph.value = {
edges: [],
nodes: [],
};
currentPlatformId.value = tree.platformId ?? null;
currentBehaviorTree.value = tree;
currentTreeEditing.value = true;
selectedModelNode.value = null;
selectedNodeTaskElement.value = null;
subPlatforms.value = []; // 重置下属平台
loadSubPlatforms(currentPlatformId.value);
nextTick(() => {
initGraph();
});
};
const loadTargetTree = (tree: BehaviorTree) => {
resolveBehaviorTreeGraph(tree.id, tree.xmlContent).then(nodeGraph => {
applyBehaviorTree({
...tree,
graph: nodeGraph,
});
}).catch(error => {
console.error('resolve tree graph error:', error);
message.error('加载行为树图失败');
});
};
const handleCreateTree = () => {
destroyGraph();
selectedModelNode.value = null;
selectedNodeTaskElement.value = null;
subPlatforms.value = []; // 重置下属平台
initGraphConfig();
nextTick(() => {
initGraph();
});
};
const handleSelectTree = (tree: BehaviorTree) => {
console.info('handleSelectTree', tree);
findOneTreeById(tree.id).then(r => {
if (r.data) {
currentTreeId.value = tree.id;
loadTargetTree(r.data);
} else {
message.error(r.msg ?? '行为树不存在.');
}
});
};
const resolveQuery = ()=> {
console.log(currentRoute);
if (!currentRoute.query.scenarioId) {
return
}
let scenarioId = Number(currentRoute.query.scenarioId);
if (!isNaN(scenarioId)) {
currentScenarioId.value = scenarioId;
fromScenarioPage.value = true;
} else {
fromScenarioPage.value = false;
}
let behaviortreeId = Number(currentRoute.query.behaviortreeId);
if (!isNaN(behaviortreeId)) {
currentTreeId.value = behaviortreeId;
} else {
currentTreeId.value = null;
}
}
// 处理选择场景
const handleSelectScenario = (scenario: any) => {
currentScenarioId.value = scenario.id;
};
// 初始化X6画布
const initGraph = () => {
if (!canvas.value) {
@@ -387,8 +411,15 @@ export default defineComponent({
try {
graph.value = createCanvas(canvas.value);
console.log('画布初始化成功');
createElements();
graph.value?.on('edge:click', (args: any) => {
const edge = args.edge;
console.info('点击了连线:', args);
// 这里可以添加选中连线的逻辑,比如显示属性面板等
});
// 监听缩放变化
handleGraphEvent('scale', ({ sx }: { sx: number }) => {
currentZoom.value = sx;
@@ -403,7 +434,6 @@ export default defineComponent({
handleGraphEvent('node:click', (args: any) => {
const node = args.node as Node<NodeProperties>;
const newElement = node.getData() as GraphTaskElement;
console.error('ddd', args);
selectedModelNode.value = node;
selectedNodeTaskElement.value = JSON.parse(JSON.stringify(newElement || {})) as GraphTaskElement;
@@ -429,23 +459,28 @@ export default defineComponent({
const init = () => {
console.info('init');
resolveQuery();
nextTick(() => {
initGraphConfig();
initGraph();
window.addEventListener('resize', handleResize);
console.log('节点挂载完成');
resolveQuery();
window.addEventListener('resize', handleResize);
if (currentPlatformId.value) {
findOneTreeByPlatformId(currentPlatformId.value).then(r => {
if (currentTreeId.value) {
findOneTreeById(currentTreeId.value).then(r => {
if (r.data) {
handleSelectTree(r.data);
currentPlatformId.value = r.data.platformId ?? null;
loadTargetTree(r.data);
} else {
handleCreateTree();
}
});
} else {
currentPlatformId.value = null;
handleCreateTree();
}
});
@@ -473,6 +508,7 @@ export default defineComponent({
message.error('当前决策树不存在');
return;
}
// return;
const newTree: BehaviorTree = {
...currentBehaviorTree.value,
graph: graphData,
@@ -508,19 +544,23 @@ export default defineComponent({
loadDatasource();
});
// 清理
onBeforeUnmount(() => destroy());
return {
nodeCommands,
currentScenarioId,
currentPlatformId,
currentTreeId,
platforms,
subPlatforms,
scenariosCardRef,
treesCardRef,
handleCreateTree,
handleSelectScenario,
currentTreeEditing,
currentBehaviorTree,
currentGraph,
selectedNodeTaskElement,
selectedModelNode,
graph,
@@ -539,6 +579,7 @@ export default defineComponent({
handleUpdateElement,
handleSelectTree,
handleGoback,
fromScenarioPage,
};
},
});

View File

@@ -1,49 +1,53 @@
<template>
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
<a-card
:class="[
'ks-designer-node',
`ks-designer-${element?.category ?? 'model'}-node`,
`ks-designer-group-${element?.group ?? 'general'}`
]"
hoverable
>
<template #title>
<a-space>
<!-- <span class="ks-designer-node-icon"></span>-->
<span class="ks-designer-node-title">{{ element?.name ?? '-' }}</span>
</a-space>
<div>
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
<a-card
:class="[
'ks-designer-node',
`ks-designer-${element?.category ?? 'model'}-node`,
`ks-designer-group-${element?.group ?? 'general'}`
]"
hoverable
>
<template #title>
<a-space>
<!-- <span class="ks-designer-node-icon"></span>-->
<span class="ks-designer-node-title">{{ element?.name ?? '-' }}</span>
</a-space>
</template>
<div class="port port-in" data-port="in-0" port="in-0" magnet="passive">
<div class="triangle-left"></div>
</div>
<div class="w-full ks-designer-node-text">
<a-tooltip >
<template #title>
{{ element?.description ?? element?.name }}
</template>
<p class="ks-designer-node-label">
{{ substring(element?.name ?? (element?.name ?? '-'), 40) }}
</p>
</a-tooltip>
</div>
<div class="port port-out" data-port="out-0" port="out-0" magnet="active">
<div class="triangle-right" ></div>
</div>
</a-card>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-menu-item>
</a-menu>
</template>
<div class="port port-in" data-port="in-0" magnet="passive">
<div class="triangle-left"></div>
</div>
<div class="w-full ks-designer-node-text">
<a-tooltip >
<template #title>
{{ element?.description ?? element?.name }}
</template>
<p class="ks-designer-node-label">
{{ substring(element?.name ?? (element?.name ?? '-'), 40) }}
</p>
</a-tooltip>
</div>
<div class="port port-out" data-port="out-0" magnet="active">
<div class="triangle-right" ></div>
</div>
</a-card>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-dropdown>
</div>
</template>
<script lang="ts">

View File

@@ -72,8 +72,8 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import type { NodeDragTemplate, NodeTemplate } from './template';
import { findNodeTemplates } from './api';
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
import { loadNodeTemplatesOnce } from './template-metadata-loader';
export default defineComponent({
emits: ['drag-item-start', 'drag-item-end'],
@@ -95,7 +95,7 @@ export default defineComponent({
conditionTemplates.value = [];
actionsTemplates.value = [];
findNodeTemplates().then(r => {
loadNodeTemplatesOnce().then(r => {
templateData.value = r.data;
if (r.data) {
r.data.forEach(tpl => {

View File

@@ -108,12 +108,13 @@
:placeholder="setting.description" size="small" style="width:100%;" />
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'platforms'" v-model:value="setting.defaultValue">
v-else-if="setting.paramKey === 'platforms'"
v-model:value="setting.defaultValue">
<a-select-option v-for="pl in getAvailablePlatforms()" :value="pl.name">{{pl.description}}</a-select-option>
</a-select>
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
v-else-if="isNodeCommandParameter(setting.paramKey as string | null | undefined)" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
</a-select>
<a-input v-else v-model:value="setting.defaultValue"
@@ -129,21 +130,31 @@
</a-tabs>
</template>
<template v-else>
<a-form-item v-for="setting in currentElement.parameters" :label="setting.description">
<a-input-number v-if="setting.dataType === 'double'" v-model:value="setting.defaultValue"
:placeholder="setting.description" size="small" style="width:100%;" />
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'platforms'" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in platforms" :value="pl.name">{{ pl.description }}</a-select-option>
</a-select>
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
</a-select>
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
</a-form-item>
<template v-for="setting in currentElement.parameters">
<div v-if="['lon','lat'].includes(setting.paramKey as string)">
<div v-if="setting.paramKey==='lon'" class="ks-location-title">位置</div>
<a-form-item class="ks-location-item" labelAlign="left" :label="setting.description">
<a-input-number v-if="setting.dataType === 'double'" v-model:value="setting.defaultValue"
:placeholder="setting.description" size="small" style="width:100%;" />
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
</a-form-item>
</div>
<a-form-item v-else :label="setting.description">
<a-input-number v-if="setting.dataType === 'double'" v-model:value="setting.defaultValue"
:placeholder="setting.description" size="small" style="width:100%;" />
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="setting.paramKey === 'platforms'" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in getAvailablePlatforms()" :value="pl.name">{{ pl.description }}</a-select-option>
</a-select>
<a-select :placeholder="`请选择${setting.description}`"
allow-clear
v-else-if="isNodeCommandParameter(setting.paramKey as string | null | undefined)" v-model:value="setting.defaultValue">
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
</a-select>
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
</a-form-item>
</template>
</template>
</a-form>
</template>
@@ -199,6 +210,10 @@ export default defineComponent({
},
emits: ['update-element', 'update-tree'],
setup(props, { emit }) {
const isNodeCommandParameter = (paramKey: string | null | undefined): boolean => {
return ['command', 'should_task'].includes(paramKey ?? '');
};
const platforms = ref<Platform[]>(props.platforms ?? []);
const subPlatforms = ref<Platform[]>(props.subPlatforms ?? []);
const nodeCommands = ref<NodeCommand[]>(props.nodeCommands ?? []);
@@ -231,7 +246,16 @@ export default defineComponent({
const addParameterTab = () => {
let newParameters = dumpParameters();
// 新增一个空的参数分组
// 如果有下属平台,预填对应索引的平台名称
const nextIndex = groupedParameters.value.length;
const subPlatform = subPlatforms.value[nextIndex];
if (subPlatform) {
const platformParam = newParameters.find(p => p.paramKey === 'platforms');
if (platformParam) {
platformParam.defaultValue = subPlatform.name;
}
}
// 新增一个参数分组
groupedParameters.value.push(newParameters);
// 自动切换到新增的分组
groupedParametersActiveTab.value = groupedParameters.value.length - 1;
@@ -292,33 +316,51 @@ export default defineComponent({
});
// 第二步:将 Map 转换为二维数组(按 groupIndex 升序排序)
groupedParameters.value = Array.from(groupMap.entries())
// 按分组索引从小到大排序(保证分组顺序正确)
const rawGroups = Array.from(groupMap.entries())
.sort((a, b) => a[0] - b[0])
// 只保留分组后的参数数组,丢弃 key
.map(item => item[1]);
// 第三步:展开逗号分隔的 platforms 值 —— 每个平台拆成独立分组
const expandedGroups: Array<ElementParameter[]> = [];
for (const group of rawGroups) {
const platformParam = group.find(p => p.paramKey === 'platforms');
if (platformParam) {
const platformValues = String(platformParam.defaultValue ?? '')
.split(',')
.map(v => v.trim())
.filter(Boolean);
if (platformValues.length > 1) {
// 每个平台值生成独立分组,其余参数保持一致
for (const pv of platformValues) {
const newGroup = JSON.parse(JSON.stringify(group)) as ElementParameter[];
const pp = newGroup.find(p => p.paramKey === 'platforms');
if (pp) pp.defaultValue = pv;
expandedGroups.push(newGroup);
}
continue;
}
}
expandedGroups.push(group);
}
groupedParameters.value = expandedGroups;
multiableParameters.value = multiable === true && groupedParameters.value.length > 0;
};
// 获取平台Tab显示名称
// 获取平台Tab显示名称(优先取该分组的 platforms 参数值,其次下属平台名称)
const getPlatformTabName = (index: number): string => {
if (!currentTree.value?.platformId) {
return `平台 ${index + 1}`;
const group = groupedParameters.value[index];
if (group) {
const platformParam = group.find(p => p.paramKey === 'platforms');
if (platformParam?.defaultValue) {
return String(platformParam.defaultValue);
}
}
// 查找当前行为树绑定的平台
const currentPlatform = platforms.value.find(p => p.id === currentTree.value?.platformId);
if (!currentPlatform) {
return `平台 ${index + 1}`;
const sub = subPlatforms.value[index];
if (sub) {
return sub.name || sub.description || `平台 ${index + 1}`;
}
// 如果有多个分组,显示平台名称和索引
if (groupedParameters.value.length > 1) {
return `${currentPlatform.name || '未知平台'}-${index + 1}`;
}
return currentPlatform.name || '平台';
return `平台 ${index + 1}`;
};
// 获取可用的平台列表(包括当前平台和其下属平台)
@@ -440,6 +482,7 @@ export default defineComponent({
groupedParameters,
getPlatformTabName,
getAvailablePlatforms,
isNodeCommandParameter,
actionSpaceColumns,
activeTopTabsKey,
activeBottomTabsKey,
@@ -454,3 +497,45 @@ export default defineComponent({
},
});
</script>
<style scoped lang="less">
.ks-location-title {
font-size: 16px;
font-weight: 600;
color: rgba(255, 255, 255, 0.65);
margin-top: 6px;
margin-bottom: 6px;
}
:deep(.ks-location-item) {
.ant-form-item-row {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
}
.ant-form-item-label {
width: 48px;
flex-shrink: 0;
padding-bottom: 0;
> label {
height: 24px;
}
}
.ant-form-item-control {
flex: 1;
}
}
.ks-parameter-setting-tabs {
:deep(.ant-tabs-tab) {
width: 120px;
max-width: 120px;
}
:deep(.ant-tabs-tab-btn) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@@ -0,0 +1,49 @@
import type { GraphContainer } from '../../graph';
import { findNodeConnections, findNodeParameters, findTreeNodeInstances } from '../api';
import type { TreeTemplateRow } from '../api';
import { buildGraphFromReverseTreeRows } from './reverse-tree';
import type { ReverseTreeRows } from './reverse-tree';
import { loadNodeTemplatesOnce, loadTemplateParameterDefsOnce } from '../template-metadata-loader';
// 已反演过的树直接返回缓存图,无需重复请求
const graphCache = new Map<number, GraphContainer>();
export const loadReverseTreeGraph = async (treeId: number): Promise<GraphContainer | null> => {
const cached = graphCache.get(treeId);
if (cached) {
return cached;
}
// 模板类接口只请求一次,树级接口每次切树请求
const [nodeTemplatesResponse, templateParameterDefs, instancesResponse, connectionsResponse] =
await Promise.all([
loadNodeTemplatesOnce(),
loadTemplateParameterDefsOnce(),
findTreeNodeInstances({ treeId }),
findNodeConnections({ treeId }),
]);
const parametersResponse = await findNodeParameters({ treeId });
const rows: ReverseTreeRows = {
templates: (nodeTemplatesResponse.data ?? []).map(t => ({
id: t.id,
type: t.type ?? 'action',
name: t.name ?? '',
multiable: t.multiable,
})) as TreeTemplateRow[],
templateParameterDefs: templateParameterDefs,
nodeInstances: instancesResponse.rows ?? [],
connections: connectionsResponse.rows ?? [],
nodeParameters: parametersResponse.rows ?? [],
};
const graph = buildGraphFromReverseTreeRows(treeId, rows);
if (graph) {
graphCache.set(treeId, graph);
}
return graph;
};
export const invalidateReverseTreeCache = (treeId: number): void => {
graphCache.delete(treeId);
};

View File

@@ -0,0 +1,267 @@
import type { ElementParameter, GraphContainer, GraphEdgeElement, GraphTaskElement } from '../../graph';
import type {
TreeConnectionRow as ReverseTreeConnectionRow,
TreeNodeInstanceRow as ReverseTreeNodeInstanceRow,
TreeNodeParameterRow as ReverseTreeNodeParameterRow,
TreeTemplateParameterDefRow as ReverseTreeTemplateParameterDefRow,
TreeTemplateRow as ReverseTreeTemplateRow,
} from '../api';
export interface ReverseTreeRows {
templates: ReverseTreeTemplateRow[];
nodeInstances: ReverseTreeNodeInstanceRow[];
connections: ReverseTreeConnectionRow[];
templateParameterDefs: ReverseTreeTemplateParameterDefRow[];
nodeParameters: ReverseTreeNodeParameterRow[];
}
const NODE_WIDTH = 250;
const CONTROL_HEIGHT = 60;
const ACTION_HEIGHT = 120;
const ROOT_X = 80;
const ROOT_Y = 50;
const LEVEL_Y_SPACING = 120;
const SIBLING_X_SPACING = 280;
const createNodeKey = (treeId: number, nodeId: number): string => {
return `reverse-tree-${treeId}-node-${nodeId}`;
};
const createEdgeKey = (treeId: number, edgeId: number): string => {
return `reverse-tree-${treeId}-edge-${edgeId}`;
};
const createBehaviorTreeEdgeEndpoints = (sourceCell: string, targetCell: string) => {
return {
source: {
cell: sourceCell,
port: 'out-0',
},
target: {
cell: targetCell,
port: 'in-0',
},
};
};
const resolveGroup = (type: string): 'control' | 'condition' | 'action' => {
if (type === 'root' || type === 'parallel' || type === 'select' || type === 'sequence') {
return 'control';
}
if (type === 'condition') {
return 'condition';
}
return 'action';
};
const resolveTemplateType = (type: string): string => {
if (type === 'select') {
return 'selector';
}
return type;
};
const resolveNodeHeight = (group: 'control' | 'condition' | 'action'): number => {
return group === 'action' ? ACTION_HEIGHT : CONTROL_HEIGHT;
};
const findRootNodeId = (
instances: ReverseTreeNodeInstanceRow[],
connections: ReverseTreeConnectionRow[],
): number | null => {
const rootNode = instances.find(item => item.isRoot === 1);
if (rootNode) {
return rootNode.id;
}
const childIds = new Set(connections.map(item => item.childNodeId));
const topNode = instances.find(item => !childIds.has(item.id));
return topNode?.id ?? null;
};
export const buildGraphFromReverseTreeRows = (
treeId: number,
rows: ReverseTreeRows,
): GraphContainer | null => {
const templates = rows.templates;
const templateParameterDefs = rows.templateParameterDefs;
const nodeParameters = rows.nodeParameters;
const instances = rows.nodeInstances.filter(item => item.treeId === treeId);
const connections = rows.connections
.filter(item => item.treeId === treeId)
.sort((left, right) => left.orderIndex - right.orderIndex);
const treeNodeParameters = nodeParameters.filter(item => item.treeId === treeId);
if (instances.length === 0) {
return null;
}
const templateMap = new Map(templates.map(item => [item.id, item]));
const templateParameterDefsMap = new Map<number, ReverseTreeTemplateParameterDefRow[]>();
const instanceMap = new Map(instances.map(item => [item.id, item]));
const childrenMap = new Map<number, ReverseTreeConnectionRow[]>();
const nodeParametersMap = new Map<number, ReverseTreeNodeParameterRow[]>();
templateParameterDefs.forEach(definition => {
const currentDefinitions = templateParameterDefsMap.get(definition.templateId) ?? [];
currentDefinitions.push(definition);
currentDefinitions.sort((left, right) => left.id - right.id);
templateParameterDefsMap.set(definition.templateId, currentDefinitions);
});
treeNodeParameters.forEach(parameter => {
const currentParameters = nodeParametersMap.get(parameter.nodeInstanceId) ?? [];
currentParameters.push(parameter);
currentParameters.sort((left, right) => {
if (left.groupIndex !== right.groupIndex) {
return left.groupIndex - right.groupIndex;
}
return left.id - right.id;
});
nodeParametersMap.set(parameter.nodeInstanceId, currentParameters);
});
connections.forEach(connection => {
const siblingConnections = childrenMap.get(connection.parentNodeId) ?? [];
siblingConnections.push(connection);
siblingConnections.sort((left, right) => left.orderIndex - right.orderIndex);
childrenMap.set(connection.parentNodeId, siblingConnections);
});
const rootNodeId = findRootNodeId(instances, connections);
if (!rootNodeId) {
return null;
}
const leafCountCache = new Map<number, number>();
const countLeaves = (nodeId: number): number => {
const cachedCount = leafCountCache.get(nodeId);
if (cachedCount) {
return cachedCount;
}
const children = childrenMap.get(nodeId) ?? [];
if (children.length === 0) {
leafCountCache.set(nodeId, 1);
return 1;
}
const totalLeaves = children.reduce((total, connection) => {
return total + countLeaves(connection.childNodeId);
}, 0);
leafCountCache.set(nodeId, totalLeaves);
return totalLeaves;
};
const nodes: GraphTaskElement[] = [];
const edges: GraphEdgeElement[] = [];
const buildNodeParameters = (instance: ReverseTreeNodeInstanceRow): ElementParameter[] => {
const parameterDefinitions = templateParameterDefsMap.get(instance.templateId) ?? [];
if (parameterDefinitions.length === 0) {
return [];
}
const parameterRows = nodeParametersMap.get(instance.id) ?? [];
const groupIndexes = parameterRows.length > 0
? Array.from(new Set(parameterRows.map(item => item.groupIndex))).sort((left, right) => left - right)
: [0];
return groupIndexes.flatMap(groupIndex => {
return parameterDefinitions.map(definition => {
const parameterRow = parameterRows.find(item => item.paramDefId === definition.id && item.groupIndex === groupIndex);
return {
id: definition.id,
templateId: definition.templateId,
paramKey: definition.paramKey,
dataType: definition.dataType,
defaultValue: parameterRow?.value ?? definition.defaultValue,
description: definition.description,
templateType: definition.templateType,
groupIndex,
} as ElementParameter;
});
});
};
const walk = (nodeId: number, startX: number, y: number): number => {
const instance = instanceMap.get(nodeId);
if (!instance) {
return startX;
}
const template = templateMap.get(instance.templateId);
const templateType = template?.type ?? 'action';
const group = resolveGroup(templateType);
const parameters = buildNodeParameters(instance);
const leafCount = countLeaves(nodeId);
const totalWidth = leafCount * SIBLING_X_SPACING;
const centerX = startX + totalWidth / 2 - SIBLING_X_SPACING / 2;
const currentNode: GraphTaskElement = {
id: instance.id,
key: createNodeKey(treeId, instance.id),
type: 'task',
template: instance.templateId,
templateType: resolveTemplateType(templateType),
name: instance.instanceName || template?.name || `节点 ${instance.id}`,
category: group,
group,
description: instance.instanceName || template?.name || `节点 ${instance.id}`,
multiable: template?.multiable === true,
order: 0,
position: {
x: Math.round(centerX),
y: Math.round(y),
},
width: NODE_WIDTH,
height: resolveNodeHeight(group),
inputs: null,
outputs: null,
parameters,
variables: [],
};
nodes.push(currentNode);
const children = childrenMap.get(nodeId) ?? [];
let childStartX = startX;
children.forEach(connection => {
const childLeafCount = countLeaves(connection.childNodeId);
walk(connection.childNodeId, childStartX, y + LEVEL_Y_SPACING + currentNode.height);
const childInstance = instanceMap.get(connection.childNodeId);
const edge: GraphEdgeElement = {
id: connection.id,
key: createEdgeKey(treeId, connection.id),
...createBehaviorTreeEdgeEndpoints(
currentNode.key as string,
createNodeKey(treeId, connection.childNodeId),
),
attrs: {},
router: {},
connector: null,
};
edges.push(edge);
const currentEdges = ((currentNode as GraphTaskElement & { edges?: GraphEdgeElement[] }).edges ?? []).slice();
currentEdges.push({
...edge,
sourceName: currentNode.name,
targetName: childInstance?.instanceName ?? childInstance?.id ?? connection.childNodeId,
});
(currentNode as GraphTaskElement & { edges?: GraphEdgeElement[] }).edges = currentEdges;
childStartX += childLeafCount * SIBLING_X_SPACING;
});
return centerX;
};
walk(rootNodeId, ROOT_X, ROOT_Y);
return {
nodes,
edges,
};
};

View File

@@ -0,0 +1,43 @@
import type { GraphContainer } from '../../graph';
import { loadReverseTreeGraph } from './reverse-tree-loader';
const EMPTY_GRAPH: GraphContainer = {
nodes: [],
edges: [],
};
export const parseBehaviorTreeGraph = (xmlContent: string | null | undefined): GraphContainer | null => {
if (!xmlContent) {
return null;
}
try {
return JSON.parse(xmlContent) as GraphContainer;
} catch (error) {
console.error('parse behavior tree graph error:', error);
return null;
}
};
export const hasBehaviorTreeNodes = (graph: GraphContainer | null | undefined): boolean => {
return Boolean(graph?.nodes && graph.nodes.length > 0);
};
export const loadBehaviorTreeGraphById = async (treeId: number): Promise<GraphContainer> => {
return (await loadReverseTreeGraph(treeId)) ?? { ...EMPTY_GRAPH };
};
export const resolveBehaviorTreeGraph = async (
treeId: number,
xmlContent: string | null | undefined,
): Promise<GraphContainer> => {
// void xmlContent;
// 如需恢复动态选择逻辑,可切回下面这段:
const savedGraph = parseBehaviorTreeGraph(xmlContent);
if (hasBehaviorTreeNodes(savedGraph)) {
return savedGraph as GraphContainer;
}
return loadBehaviorTreeGraphById(treeId);
};

View File

@@ -0,0 +1,138 @@
<template>
<a-collapse v-model:activeKey="activeKey" :accordion="false" class="ks-trees-collapse">
<a-collapse-panel key="1" style="position: relative">
<template #header>
<a-flex>
<span class="ks-model-builder-title-icon icon-model"></span>
<span style="color: #82c4e9;font-size: 16px;">我的场景</span>
</a-flex>
</template>
<div class="w-full" style="padding: 5px;">
<a-flex>
<a-input-search v-model:value="scenarioQuery.name" allowClear placeholder="场景名称" size="small" @search="loadScenarios()" />
</a-flex>
</div>
<a-list :data-source="scenarios || []" size="small" style="min-height: 25vh">
<template #renderItem="{ item }">
<a-list-item :class="{ 'ks-item-selected': scenarioId === item.id }" @click="()=> handleSelect(item)">
<a-flex>
<a-tooltip placement="bottom">
<template #title>
<p>名称: {{ item.name }}</p>
<p>说明: {{ item.description }}</p>
</template>
<span>{{ substring(item.name, 15) }}</span>
</a-tooltip>
</a-flex>
</a-list-item>
</template>
</a-list>
<a-pagination
v-model:current="scenarioQuery.pageNum"
:page-size="scenarioQuery.pageSize"
:total="totalScenarios"
style="position: unset; margin: 5px 0;"
simple size="small" @change="handleChange" />
</a-collapse-panel>
</a-collapse>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, watch, type PropType } from 'vue';
import { CopyOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
import type { Scenario, ScenarioRequest } from '../communication/types';
import { findScenarioByQuery } from '../communication/api';
import { substring } from '@/utils/strings';
export default defineComponent({
props: {
scenarioId: Number as PropType<number>,
},
emits: ['select-scenario'],
components: {
CopyOutlined,
PlusOutlined,
DeleteOutlined,
},
setup(_props, { emit }) {
const scenarios = ref<Scenario[]>([]);
const scenarioQuery = ref<Partial<ScenarioRequest>>({
name: null,
pageNum: 1,
pageSize: 8,
});
const activeKey = ref<number>(0);
const totalScenarios = ref<number>(0);
const loadScenarios = (cb?: () => void) => {
findScenarioByQuery(scenarioQuery.value).then(r => {
scenarios.value = r.rows;
totalScenarios.value = r.total ?? 0;
if(cb) cb();
});
};
const handleChange = (page: number, pageSize: number) => {
scenarioQuery.value.pageNum = page;
scenarioQuery.value.pageSize = pageSize;
loadScenarios();
};
const handleSelect = (record: Scenario) => {
emit('select-scenario', record);
};
const refresh = () => loadScenarios();
onMounted(() => {
loadScenarios(() => {
if (_props.scenarioId) {
return;
}
// 默认选中第一个场景
const selectedScenario = scenarios.value[0];
if (selectedScenario) {
emit('select-scenario', selectedScenario);
}
});
});
return {
refresh,
totalScenarios,
substring,
activeKey,
scenarios,
scenarioQuery,
loadScenarios,
handleSelect,
handleChange,
};
},
});
</script>
<style scoped>
.a-list-item {
cursor: pointer;
padding: 8px 12px;
transition: background-color 0.3s;
}
.a-list-item:hover {
background-color: #f5f5f5;
}
.icon-scenario::before {
content: '\e6b8';
font-family: 'iconfont';
margin-right: 8px;
color: #82c4e9;
}
:deep(.ant-list-item.ks-item-selected) {
background-color: rgba(130, 196, 233, 0.15);
border-left: 3px solid #82c4e9;
padding-left: 9px;
}
</style>

View File

@@ -0,0 +1,32 @@
import { findNodeTemplates, findTemplateParameterDefs } from './api';
import type { TreeTemplateParameterDefRow } from './api';
import type { NodeTemplatesResponse } from './template';
// 模板列表:全局只请求一次
let nodeTemplatesPromise: Promise<NodeTemplatesResponse> | null = null;
// 模板参数定义:全局只请求一次
let templateParameterDefsPromise: Promise<TreeTemplateParameterDefRow[]> | null = null;
export const loadNodeTemplatesOnce = (): Promise<NodeTemplatesResponse> => {
if (!nodeTemplatesPromise) {
nodeTemplatesPromise = findNodeTemplates().catch(err => {
// 请求失败时清除缓存,下次可以重试
nodeTemplatesPromise = null;
return Promise.reject(err);
});
}
return nodeTemplatesPromise;
};
export const loadTemplateParameterDefsOnce = (): Promise<TreeTemplateParameterDefRow[]> => {
if (!templateParameterDefsPromise) {
templateParameterDefsPromise = findTemplateParameterDefs()
.then(r => r.rows ?? [])
.catch(err => {
templateParameterDefsPromise = null;
return Promise.reject(err);
});
}
return templateParameterDefsPromise;
};

Some files were not shown because too many files have changed in this diff Show More