diff --git a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java index a7ef814..f55335a 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java +++ b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java @@ -6,7 +6,10 @@ 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.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; @@ -22,6 +25,8 @@ public class RuleController extends BaseController { @Autowired private IRuleService ruleService; + @Autowired + private IRuleConfigService ruleConfigService; @PreAuthorize("@ss.hasPermi('system:rule:list')") @GetMapping("/list") @@ -62,4 +67,50 @@ public class RuleController extends BaseController { 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: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)); + } } \ No newline at end of file diff --git a/auto-solution-admin/src/main/java/com/solution/web/controller/scene/SceneController.java b/auto-solution-admin/src/main/java/com/solution/web/controller/scene/SceneController.java index 311cbed..7116aa6 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/controller/scene/SceneController.java +++ b/auto-solution-admin/src/main/java/com/solution/web/controller/scene/SceneController.java @@ -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)); + } } diff --git a/auto-solution-admin/src/main/resources/application-druid.yml b/auto-solution-admin/src/main/resources/application-druid.yml index d9bbb1f..3cfb6ea 100644 --- a/auto-solution-admin/src/main/resources/application-druid.yml +++ b/auto-solution-admin/src/main/resources/application-druid.yml @@ -6,11 +6,9 @@ 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 + password: 123456 # 从库数据源 slave: # 从数据源开关/默认关闭 diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfig.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfig.java new file mode 100644 index 0000000..9836ede --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfig.java @@ -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 params; + + @ApiModelProperty("适用任务类型编码列表") + private List taskTypes; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigParam.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigParam.java new file mode 100644 index 0000000..543906a --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigParam.java @@ -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; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigQuery.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigQuery.java new file mode 100644 index 0000000..60d209a --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigQuery.java @@ -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; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleDictItem.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleDictItem.java new file mode 100644 index 0000000..5f01157 --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleDictItem.java @@ -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; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/mapper/RuleConfigMapper.java b/auto-solution-rule/src/main/java/com/solution/rule/mapper/RuleConfigMapper.java new file mode 100644 index 0000000..9f3575c --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/mapper/RuleConfigMapper.java @@ -0,0 +1,38 @@ +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.RuleDictItem; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface RuleConfigMapper { + + List 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 selectParamsByRuleCode(@Param("ruleCode") String ruleCode); + + int deleteParamsByRuleCodes(@Param("ruleCodes") String[] ruleCodes); + + int insertParamsBatch(@Param("params") List params); + + List selectTaskTypesByRuleCode(@Param("ruleCode") String ruleCode); + + int deleteTaskTypesByRuleCodes(@Param("ruleCodes") String[] ruleCodes); + + int insertTaskTypesBatch(@Param("ruleCode") String ruleCode, @Param("taskTypes") List taskTypes); + + List selectDictByType(@Param("dictType") String dictType); +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java b/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java new file mode 100644 index 0000000..2ef3add --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java @@ -0,0 +1,22 @@ +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 java.util.List; + +public interface IRuleConfigService { + + List selectRuleConfigList(RuleConfigQuery query); + + RuleConfig selectRuleConfigByCode(String ruleCode); + + int insertRuleConfig(RuleConfig ruleConfig); + + int updateRuleConfig(RuleConfig ruleConfig); + + int deleteRuleConfigByCodes(String[] ruleCodes); + + List selectDictByType(String dictType); +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java new file mode 100644 index 0000000..7c68f0d --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java @@ -0,0 +1,139 @@ +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.RuleDictItem; +import com.solution.rule.mapper.RuleConfigMapper; +import com.solution.rule.service.IRuleConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class RuleConfigServiceImpl implements IRuleConfigService { + + @Autowired + private RuleConfigMapper ruleConfigMapper; + + @Override + public List 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; + } + + @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; + } + + @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); + } + + @Override + public List selectDictByType(String dictType) { + if (ObjectUtil.isEmpty(dictType)) { + throw new RuntimeException(ExceptionConstants.PARAMETER_EXCEPTION); + } + return ruleConfigMapper.selectDictByType(dictType); + } + + private void saveChildren(RuleConfig ruleConfig) { + if (CollUtil.isNotEmpty(ruleConfig.getParams())) { + Set 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()); + } + 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); + } + } +} diff --git a/auto-solution-rule/src/main/resources/mapper/rule/FireRuleMapper.xml b/auto-solution-rule/src/main/resources/mapper/rule/FireRuleMapper.xml index 8b45634..78b5264 100644 --- a/auto-solution-rule/src/main/resources/mapper/rule/FireRuleMapper.xml +++ b/auto-solution-rule/src/main/resources/mapper/rule/FireRuleMapper.xml @@ -82,7 +82,7 @@ @@ -112,13 +112,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 - + - - \ No newline at end of file + \ No newline at end of file diff --git a/auto-solution-rule/src/main/resources/mapper/rule/RuleConfigMapper.xml b/auto-solution-rule/src/main/resources/mapper/rule/RuleConfigMapper.xml new file mode 100644 index 0000000..8d9e7bc --- /dev/null +++ b/auto-solution-rule/src/main/resources/mapper/rule/RuleConfigMapper.xml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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()) + + + + UPDATE rule_item + + rule_name = #{ruleName}, + level_code = #{levelCode}, + kind_code = #{kindCode}, + module_code = #{moduleCode}, + priority_no = #{priorityNo}, + condition_expr = #{conditionExpr}, + action_expr = #{actionExpr}, + version_no = #{versionNo}, + enabled = #{enabled}, + remark = #{remark}, + updated_at = NOW() + + WHERE rule_code = #{ruleCode} + + + + DELETE FROM rule_item + WHERE rule_code IN + + #{code} + + + + + + + DELETE FROM rule_item_param + WHERE rule_code IN + + #{code} + + + + + INSERT INTO rule_item_param + (rule_code, param_key, param_val, val_type, param_name, sort_no, enabled, remark, created_at, updated_at) + VALUES + + (#{item.ruleCode}, #{item.paramKey}, #{item.paramVal}, #{item.valType}, #{item.paramName}, + #{item.sortNo}, #{item.enabled}, #{item.remark}, NOW(), NOW()) + + + + + + + DELETE FROM rule_item_task_type + WHERE rule_code IN + + #{code} + + + + + INSERT INTO rule_item_task_type (rule_code, task_type_code, created_at) + VALUES + + (#{ruleCode}, #{taskType}, NOW()) + + + + + + diff --git a/auto-solution-rule/src/main/resources/sql/rule/001_rule_schema.sql b/auto-solution-rule/src/main/resources/sql/rule/001_rule_schema.sql new file mode 100644 index 0000000..365f598 --- /dev/null +++ b/auto-solution-rule/src/main/resources/sql/rule/001_rule_schema.sql @@ -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='规则参数表'; diff --git a/auto-solution-rule/src/main/resources/sql/rule/002_rule_seed_from_drl.sql b/auto-solution-rule/src/main/resources/sql/rule/002_rule_seed_from_drl.sql new file mode 100644 index 0000000..96052b4 --- /dev/null +++ b/auto-solution-rule/src/main/resources/sql/rule/002_rule_seed_from_drl.sql @@ -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 selectAllTreeBySceneId(Integer id); + + /** + * 根据场景id获取场景下所有关系 + * @param id + * @return + */ + List selectAllRelationBySceneId(Integer id); } diff --git a/auto-solution-scene/src/main/java/com/solution/scene/service/SceneService.java b/auto-solution-scene/src/main/java/com/solution/scene/service/SceneService.java index 6f875d9..56644a8 100644 --- a/auto-solution-scene/src/main/java/com/solution/scene/service/SceneService.java +++ b/auto-solution-scene/src/main/java/com/solution/scene/service/SceneService.java @@ -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 getAllTree(Integer id); + + /** + * 根据场景id获取场景下所有关系 + * @param id + * @return + */ + List getAllRelation(Integer id); } diff --git a/auto-solution-scene/src/main/java/com/solution/scene/service/impl/SceneServiceImpl.java b/auto-solution-scene/src/main/java/com/solution/scene/service/impl/SceneServiceImpl.java index d50ed19..91c8125 100644 --- a/auto-solution-scene/src/main/java/com/solution/scene/service/impl/SceneServiceImpl.java +++ b/auto-solution-scene/src/main/java/com/solution/scene/service/impl/SceneServiceImpl.java @@ -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 getAllRelation(Integer id) { + List result = sceneMapper.selectAllRelationBySceneId(id); + if(CollUtil.isEmpty( result)){ + throw new RuntimeException("该场景下不存在关系"); + } + return result; + } + } diff --git a/auto-solution-scene/src/main/resources/mapper/scene/SceneMapper.xml b/auto-solution-scene/src/main/resources/mapper/scene/SceneMapper.xml index 7c9481b..ab0b53e 100644 --- a/auto-solution-scene/src/main/resources/mapper/scene/SceneMapper.xml +++ b/auto-solution-scene/src/main/resources/mapper/scene/SceneMapper.xml @@ -32,6 +32,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" WHERE scenario_id=#{id} + + + + + + + + + + update afsim_scenario set name=#{name}, diff --git a/modeler/src/views/decision/communication/api.ts b/modeler/src/views/decision/communication/api.ts index 8fb5d4f..881dd56 100644 --- a/modeler/src/views/decision/communication/api.ts +++ b/modeler/src/views/decision/communication/api.ts @@ -8,7 +8,7 @@ */ 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'; @@ -33,6 +33,15 @@ export const findPlatformWithComponents = (id: number): Promise(`/system/firerule/platforms/${id}`); }; +/** + * 获取场景的所有通信关系 + * @param id 场景ID + * @returns 通信关系列表 + */ +export const findRelations = (id: number): Promise => { + return req.get(`/system/scene/getAllRelation/${id}`); +}; + export const saveScenario = (scenario: Scenario): Promise => { return req.postJson(`/system/scene/saveSceneConfig`,scenario); }; diff --git a/modeler/src/views/decision/communication/communication.vue b/modeler/src/views/decision/communication/communication.vue index 76e9076..3ec11e0 100644 --- a/modeler/src/views/decision/communication/communication.vue +++ b/modeler/src/views/decision/communication/communication.vue @@ -21,6 +21,14 @@
+ + + 随机生成 + + + + 从后端加载 + 保存 @@ -50,22 +58,24 @@ 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, getAllBehaviorTreesBySceneId } from './api'; +import { findOneScenarioById, saveScenario, findRelations } from './api'; import { resolveConnectionRelation } from './relation'; +import { generateRandomCommunicationData } from './random-data-generator'; +import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter'; const TeleportContainer = defineComponent(getTeleport()); @@ -81,6 +91,8 @@ export default defineComponent({ CheckCircleOutlined, CheckOutlined, RollbackOutlined, + ThunderboltOutlined, + DatabaseOutlined, TeleportContainer, }, setup() { @@ -218,36 +230,67 @@ export default defineComponent({ } 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; - // 将场景ID存储到graph对象中,供子组件访问 - if (graph.value) { - (graph.value as any).currentScenario = currentScenario.value; - - // 加载该场景下的行为树列表 + // 如果场景有ID且没有已保存的图数据,尝试从后端加载通信关系 + if (scenario.id > 0 && !nodeGraph) { try { - const response = await getAllBehaviorTreesBySceneId(scenario.id); - if (response.code === 200 && response.data) { - (graph.value as any).behaviorTrees = response.data; - console.log(`加载场景${scenario.id}的行为树列表:`, response.data.length, '个'); + 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]); + + // 转换为图数据 + const convertedGraph = convertRecordsToGraphContainer(normalizedRelations); + console.log('转换后的图数据:', convertedGraph); + + // 更新当前场景的图数据 + currentScenario.value.graph = convertedGraph; + currentScenario.value.communicationGraph = JSON.stringify(convertedGraph); + + message.success({ content: `成功加载 ${normalizedRelations.length} 条通信关系`, key: 'loading-relations' }); } else { - (graph.value as any).behaviorTrees = []; - console.warn('获取行为树列表失败:', response.msg); + message.warning({ content: '该场景暂无通信关系数据', key: 'loading-relations' }); } } catch (error) { - console.error('获取行为树列表失败:', error); - (graph.value as any).behaviorTrees = []; + console.error('从后端加载通信关系失败:', error); + message.error({ content: '加载通信关系失败,请手动点击"从后端加载"', key: 'loading-relations' }); } } @@ -282,6 +325,7 @@ export default defineComponent({ }, 100); // 延迟一会儿,免得连线错位 } } + }, 100); }); }; @@ -442,6 +486,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(); @@ -471,6 +702,8 @@ export default defineComponent({ handleDrop, isDraggingOver, handleSave, + handleGenerateRandom, + handleLoadFromBackend, handleUpdateElement, handleSelect, }; diff --git a/modeler/src/views/decision/communication/data-converter.ts b/modeler/src/views/decision/communication/data-converter.ts new file mode 100644 index 0000000..01b0880 --- /dev/null +++ b/modeler/src/views/decision/communication/data-converter.ts @@ -0,0 +1,1049 @@ +/* + * This file is part of the kernelstudio package. + * + * (c) 2014-2026 zlin + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +import type { GraphTaskElement, GraphContainer, GraphEdgeElement } from '../graph'; +import { generateKey } from '@/utils/strings'; + +// ==================== 类型定义 ==================== + +/** + * 布局方向类型 + */ +export type LayoutDirection = 'horizontal' | 'vertical'; + +/** + * 布局配置接口 + */ +export interface LayoutConfig { + direction: LayoutDirection; + rootPosition?: number; // 根节点起始位置 + colSpacing?: number; // 列间距 + rowSpacing?: number; // 行间距 + startPosition?: number; // 起始位置 +} + +/** + * 数据库通信关系记录接口 + */ +export interface CommunicationRecord { + id: number; + command_platform: string; // 指挥平台(源节点名称) + subordinate_platform: string; // 下属平台(目标节点名称) + command_comm?: string; // 指挥通信方式 + subordinate_comm?: string; // 下属通信方式 + scenary_id?: number; // 场景ID +} + +interface ParentChildMappings { + parentToChildren: Map; + childToParent: Map; +} + +interface LayoutParams { + ROOT_POSITION: number; // 根节点起始位置(horizontal时为X, vertical时为Y) + LEVEL_SPACING: number; // 层级间距(horizontal时为X间距, vertical时为Y间距) + SIBLING_SPACING: number; // 同级间距(horizontal时为Y间距, vertical时为X间距) + START_POSITION: number; // 起始位置(horizontal时为Y, vertical时为X) + direction: LayoutDirection; +} + +interface NodePosition { + x: number; + y: number; +} + +interface SubtreeRange { + min: number; + max: number; +} + +/** + * 坐标映射器接口 - 用于抽象水平和垂直布局的坐标转换 + */ +interface CoordinateMapper { + /** 将层级索引映射到主轴坐标 */ + mapLevelToAxis(level: number): number; + /** 将同级索引映射到次轴坐标 */ + mapSiblingIndexToAxis(index: number, totalInLevel?: number): number; + /** 获取节点的主轴坐标 */ + getPrimaryCoord(pos: NodePosition): number; + /** 获取节点的次轴坐标 */ + getSecondaryCoord(pos: NodePosition): number; + /** 设置节点坐标 */ + setCoords(primary: number, secondary: number): NodePosition; + /** 获取子树范围的主轴坐标 */ + getSubtreePrimaryCoord(range: SubtreeRange): { min: number; max: number }; + /** 创建子树范围对象 */ + createSubtreeRange(min: number, max: number): SubtreeRange; +} + +/** + * 创建水平布局的坐标映射器 (X轴表示层级,Y轴表示同级分布) + */ +const createHorizontalMapper = (params: LayoutParams): CoordinateMapper => ({ + mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING, + mapSiblingIndexToAxis: (index, totalInLevel) => { + // 均匀分布在START_POSITION开始的区域内 + return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2; + }, + getPrimaryCoord: (pos) => pos.x, + getSecondaryCoord: (pos) => pos.y, + setCoords: (primary, secondary) => ({ x: primary, y: secondary }), + getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }), + createSubtreeRange: (min, max) => ({ min, max }), +}); + +/** + * 创建垂直布局的坐标映射器 (Y轴表示层级,X轴表示同级分布) + */ +const createVerticalMapper = (params: LayoutParams): CoordinateMapper => ({ + mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING, + mapSiblingIndexToAxis: (index, totalInLevel) => { + // 均匀分布在START_POSITION开始的区域内 + return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2; + }, + getPrimaryCoord: (pos) => pos.y, + getSecondaryCoord: (pos) => pos.x, + setCoords: (primary, secondary) => ({ x: secondary, y: primary }), + getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }), + createSubtreeRange: (min, max) => ({ min, max }), +}); + +// ==================== 主函数:数据转换 ==================== + +/** + * 从数据库记录转换为 GraphContainer 格式 + * @param records 数据库通信关系记录数组 + * @param config 布局配置(可选),默认为水平布局 + * @returns GraphContainer 格式的节点和边数据 + */ +export const convertRecordsToGraphContainer = ( + records: CommunicationRecord[], + config?: Partial +): GraphContainer => { + if (!records || records.length === 0) { + return { nodes: [], edges: [] }; + } + + // 合并默认配置 + const finalConfig: LayoutConfig = { + direction: config?.direction || 'horizontal', + rootPosition: typeof config?.rootPosition === 'number' ? config.rootPosition : 50, + colSpacing: typeof config?.colSpacing === 'number' ? config.colSpacing : 280, + rowSpacing: typeof config?.rowSpacing === 'number' ? config.rowSpacing : 140, + startPosition: typeof config?.startPosition === 'number' ? config.startPosition : 80, + }; + + // 收集所有唯一的平台名称(过滤无效值) + const platformSet = new Set(); + records.forEach(record => { + if (record.command_platform && typeof record.command_platform === 'string' && record.command_platform.trim()) { + platformSet.add(record.command_platform); + } + if (record.subordinate_platform && typeof record.subordinate_platform === 'string' && record.subordinate_platform.trim()) { + platformSet.add(record.subordinate_platform); + } + }); + + const allPlatforms = Array.from(platformSet); + + console.log(`收集到 ${allPlatforms.length} 个唯一平台:`, allPlatforms); + + if (allPlatforms.length === 0) { + console.warn('没有有效的平台数据'); + return { nodes: [], edges: [] }; + } + + // 构建父子映射关系 + const { parentToChildren, childToParent } = buildParentChildMappings(records); + + // 检测连通分量(处理断链情况) + const connectedComponents = findConnectedComponents(allPlatforms, parentToChildren, childToParent); + + console.log(`检测到 ${connectedComponents.length} 个连通分量`); + + // 为每个连通分量生成布局 + const allNodes: GraphTaskElement[] = []; + const allEdges: GraphEdgeElement[] = []; + + let offset: number = finalConfig.rootPosition ?? 50; // 第一个分量的起始位置 + + connectedComponents.forEach((component, componentIndex) => { + // 为该分量确定根节点(选择入度为0的节点) + const rootPlatformName = determineRootNode(Array.from(component), childToParent); + + if (!rootPlatformName) { + console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`); + return; + } + + // BFS遍历确定该分量中每个节点的层级 + const levelMap = computeNodeLevels( + Array.from(component), + rootPlatformName, + parentToChildren, + childToParent + ); + + console.log(` 分量 ${componentIndex + 1} 的层级映射:`, Object.fromEntries(levelMap)); + + // 按层级分组 + const levelsMap = groupNodesByLevel(levelMap); + + console.log(` 分量 ${componentIndex + 1} 的层级分组:`, Object.fromEntries(levelsMap)); + + // 根据方向创建布局参数 + const layoutParams: LayoutParams = { + ROOT_POSITION: offset ?? 50, + LEVEL_SPACING: finalConfig.colSpacing ?? 280, + SIBLING_SPACING: finalConfig.rowSpacing ?? 140, + START_POSITION: finalConfig.startPosition ?? 80, + direction: finalConfig.direction, + }; + + // 创建坐标映射器 + const mapper = finalConfig.direction === 'horizontal' + ? createHorizontalMapper(layoutParams) + : createVerticalMapper(layoutParams); + + // 计算节点位置 + const nodePositions = calculateNodePositions( + levelsMap, + parentToChildren, + childToParent, + layoutParams, + mapper + ); + + // 创建节点对象 + const nodeMap = createNodeObjects(levelsMap, nodePositions); + + console.log(` 分量 ${componentIndex + 1} 创建了 ${nodeMap.size} 个节点:`, Array.from(nodeMap.keys())); + + // 创建根节点(如果不存在则添加) + if (!nodeMap.has(rootPlatformName)) { + console.warn(` 根节点 "${rootPlatformName}" 未在 levelsMap 中找到,手动创建`); + } + createRootNode(nodeMap, rootPlatformName, parentToChildren, layoutParams, mapper); + + console.log(` 最终节点数: ${nodeMap.size}`); + + // 过滤出属于当前分量的边 + const componentEdges = filterEdgesForComponent(records, component); + const edges = createEdges(componentEdges, nodeMap); + + // 添加到总结果 + allNodes.push(...Array.from(nodeMap.values())); + allEdges.push(...edges); + + // 计算下一个分量的偏移量(基于当前分量的宽度) + const maxLevel = Math.max(...Array.from(levelsMap.keys())); + offset += (maxLevel + 1) * layoutParams.LEVEL_SPACING + 150; // 分量之间留150px间隔 + }); + + return { + nodes: allNodes, + edges: allEdges, + }; +}; + +// ==================== 辅助函数:检测连通分量 ==================== + +/** + * 使用并查集或DFS检测图中的连通分量 + */ +const findConnectedComponents = ( + platforms: string[], + parentToChildren: Map, + childToParent: Map +): Set[] => { + // 过滤掉undefined、null和空字符串 + const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0); + + const visited = new Set(); + const components: Set[] = []; + + // DFS遍历找出所有连通分量 + const dfs = (platform: string, component: Set) => { + if (visited.has(platform)) return; + + visited.add(platform); + component.add(platform); + + // 访问子节点 + const children = parentToChildren.get(platform) || []; + children.forEach(child => { + // 只处理有效的子节点 + if (child && typeof child === 'string' && child.trim().length > 0) { + dfs(child, component); + } + }); + + // 访问父节点 + const parent = childToParent.get(platform); + if (parent && !visited.has(parent) && typeof parent === 'string' && parent.trim().length > 0) { + dfs(parent, component); + } + }; + + // 对每个未访问的有效节点启动一次DFS + validPlatforms.forEach(platform => { + if (!visited.has(platform)) { + const component = new Set(); + dfs(platform, component); + if (component.size > 0) { + components.push(component); + } + } + }); + + return components; +}; + +/** + * 为连通分量确定根节点 + * 策略:选择入度为0的节点(没有父节点的节点),如果有多个则选择第一个 + */ +const determineRootNode = ( + platforms: string[], + childToParent: Map +): string | null => { + // 过滤掉undefined、null和空字符串 + const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0); + + if (validPlatforms.length === 0) return null; + + // 优先选择入度为0的节点(没有父节点) + const rootCandidates = validPlatforms.filter(platform => !childToParent.has(platform)); + + if (rootCandidates.length > 0) { + return rootCandidates[0] || null; + } + + // 如果所有节点都有父节点(可能是环形结构),返回第一个有效节点 + return validPlatforms[0] || null; +}; + +/** + * 过滤出属于特定连通分量的边 + */ +const filterEdgesForComponent = ( + records: CommunicationRecord[], + component: Set +): CommunicationRecord[] => { + return records.filter(record => + component.has(record.command_platform) && + component.has(record.subordinate_platform) + ); +}; + +// ==================== 辅助函数:构建父子映射 ==================== + +const buildParentChildMappings = (records: CommunicationRecord[]): ParentChildMappings => { + const parentToChildren = new Map(); + const childToParent = new Map(); + + records.forEach(record => { + if (!parentToChildren.has(record.command_platform)) { + parentToChildren.set(record.command_platform, []); + } + parentToChildren.get(record.command_platform)!.push(record.subordinate_platform); + childToParent.set(record.subordinate_platform, record.command_platform); + }); + + return { parentToChildren, childToParent }; +}; + +// ==================== 辅助函数:计算节点层级 ==================== + +const computeNodeLevels = ( + platforms: string[], + rootPlatformName: string, + parentToChildren: Map, + childToParent: Map +): Map => { + const levelMap = new Map(); + const processed = new Set(); + const queue: Array<{ platform: string; level: number }> = []; + + // 找出所有入度为0的节点(没有父节点的节点),作为BFS的起点 + const rootNodes = platforms.filter(p => !childToParent.has(p)); + + console.log(` BFS起点(入度为0的节点): [${rootNodes.join(', ')}]`); + + // 将所有根节点加入队列,层级为0 + rootNodes.forEach(root => { + levelMap.set(root, 0); + processed.add(root); + queue.push({ platform: root, level: 0 }); + }); + + // BFS遍历后续层级 + while (queue.length > 0) { + const { platform: currentPlatform, level: currentLevel } = queue.shift()!; + + const children = parentToChildren.get(currentPlatform) || []; + children.forEach(childPlatform => { + if (!processed.has(childPlatform)) { + levelMap.set(childPlatform, currentLevel + 1); + processed.add(childPlatform); + queue.push({ platform: childPlatform, level: currentLevel + 1 }); + } + }); + } + + // 理论上不应该有未处理的节点,因为所有节点都应该从某个根节点可达 + // 如果有,说明图中存在环或者数据有问题 + const unprocessed = platforms.filter(p => !processed.has(p)); + if (unprocessed.length > 0) { + console.warn(` 警告:以下节点未被BFS遍历到(可能存在环或数据问题): [${unprocessed.join(', ')}]`); + // 给这些节点分配一个合理的层级(基于它们的父节点) + unprocessed.forEach(platform => { + const parent = childToParent.get(platform); + if (parent && levelMap.has(parent)) { + levelMap.set(platform, levelMap.get(parent)! + 1); + } else { + // 如果连父节点都没有,放到最后一层 + const maxLevel = Math.max(...Array.from(levelMap.values()), 0); + levelMap.set(platform, maxLevel + 1); + } + }); + } + + return levelMap; +}; + +// ==================== 辅助函数:按层级分组 ==================== + +const groupNodesByLevel = (levelMap: Map): Map => { + const levelsMap = new Map(); + levelMap.forEach((level, platform) => { + if (!levelsMap.has(level)) { + levelsMap.set(level, []); + } + levelsMap.get(level)!.push(platform); + }); + return levelsMap; +}; + +// ==================== 辅助函数:计算节点位置 ==================== + +const calculateNodePositions = ( + levelsMap: Map, + parentToChildren: Map, + childToParent: Map, + params: LayoutParams, + mapper: CoordinateMapper +): Map => { + const nodePositions = new Map(); + const subtreeRanges = new Map(); + + // 第一步:预计算每列需要的总高度 + precomputeColumnHeights(levelsMap, parentToChildren, params.SIBLING_SPACING, subtreeRanges, mapper); + + // 第二步:初步分配位置 + initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions, mapper); + + // 第三步:调整父节点使其位于子节点中心(处理共享子节点情况) + adjustParentsToCenterOfChildren(levelsMap, parentToChildren, childToParent, nodePositions, mapper); + + // 第四步:全局防重叠检测与修正(跨所有层级) + preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper); + + // 第五步:重新调整父节点恢复等腰三角形 + restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions, mapper); + + // 第六步:最终全局防重叠检查 + preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper); + + return nodePositions; +}; + +// 预计算每列高度 +const precomputeColumnHeights = ( + levelsMap: Map, + parentToChildren: Map, + siblingSpacing: number, + subtreeRanges: Map, + mapper: CoordinateMapper +): void => { + const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a); + + sortedLevels.forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + let totalSize = 0; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + let maxSubtreeSize = 0; + + children.forEach(child => { + const childRange = subtreeRanges.get(child); + if (childRange) { + const range = mapper.getSubtreePrimaryCoord(childRange); + const childSize = range.max - range.min; + maxSubtreeSize = Math.max(maxSubtreeSize, childSize); + } + }); + + const subtreeSize = maxSubtreeSize; + + totalSize += Math.max(siblingSpacing, subtreeSize); + subtreeRanges.set(platform, mapper.createSubtreeRange( + totalSize - subtreeSize, + totalSize + )); + } else { + totalSize += siblingSpacing; + subtreeRanges.set(platform, mapper.createSubtreeRange( + totalSize - siblingSpacing, + totalSize + )); + } + }); + }); +}; + +// 初步分配位置(简化版:所有节点均匀分布,不考虑子树高度) +const initialPositionAssignment = ( + levelsMap: Map, + parentToChildren: Map, + params: LayoutParams, + subtreeRanges: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + forwardLevels.forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach((platform, index) => { + const primaryCoord = mapper.mapLevelToAxis(level); + const secondaryCoord = mapper.mapSiblingIndexToAxis(index, platformsInLevel.length); + nodePositions.set(platform, mapper.setCoords(primaryCoord, secondaryCoord)); + }); + }); +}; + +// 调整父节点到子节点中心 +const adjustParentsToCenterOfChildren = ( + levelsMap: Map, + parentToChildren: Map, + childToParent: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + // 从最底层向上遍历,确保子节点位置已经确定后再调整父节点 + [...forwardLevels].reverse().forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); + + if (childPositions.length > 0) { + // 计算所有子节点的平均次轴坐标(垂直布局时是X,水平布局时是Y) + const avgSecondaryCoord = childPositions.reduce((sum, pos) => + sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length; + + // 将父节点的次轴坐标移动到子节点的中心 + const pos = nodePositions.get(platform)!; + const primaryCoord = mapper.getPrimaryCoord(pos); + nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord)); + } + } + }); + }); +}; + +// 全局防重叠检测与修正(跨所有层级,迭代式确保完全无重叠) +const preventGlobalOverlaps = ( + parentToChildren: Map, + siblingSpacing: number, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + // 收集所有节点并按主轴坐标分组(同层的节点) + const nodesByLevel = new Map>(); + + nodePositions.forEach((pos, platform) => { + const primaryCoord = mapper.getPrimaryCoord(pos); + if (!nodesByLevel.has(primaryCoord)) { + nodesByLevel.set(primaryCoord, []); + } + nodesByLevel.get(primaryCoord)!.push({ platform, pos }); + }); + + // 对每一层进行防重叠处理 + nodesByLevel.forEach(nodes => { + let hasOverlap = true; + let iterations = 0; + const maxIterations = 30; // 防止无限循环 + + while (hasOverlap && iterations < maxIterations) { + hasOverlap = false; + iterations++; + + // 按次轴坐标排序 + nodes.sort((a, b) => mapper.getSecondaryCoord(a.pos) - mapper.getSecondaryCoord(b.pos)); + + for (let i = 1; i < nodes.length; i++) { + const prev = nodes[i - 1]; + const curr = nodes[i]; + + if (!prev || !curr) continue; + + const actualDistance = mapper.getSecondaryCoord(curr.pos) - mapper.getSecondaryCoord(prev.pos); + + if (actualDistance < siblingSpacing) { + hasOverlap = true; + + const offset = siblingSpacing - actualDistance; + const currSecondary = mapper.getSecondaryCoord(curr.pos); + const currPrimary = mapper.getPrimaryCoord(curr.pos); + curr.pos = mapper.setCoords(currPrimary, currSecondary + offset); + + // 递归调整所有后代节点 + const adjustDescendants = (platform: string, secondaryOffset: number) => { + const children = parentToChildren.get(platform) || []; + children.forEach(child => { + const childPos = nodePositions.get(child); + if (childPos) { + const childPrimary = mapper.getPrimaryCoord(childPos); + const childSecondary = mapper.getSecondaryCoord(childPos); + const newPos = mapper.setCoords(childPrimary, childSecondary + secondaryOffset); + nodePositions.set(child, newPos); + adjustDescendants(child, secondaryOffset); + } + }); + }; + + adjustDescendants(curr.platform, offset); + + // 发现重叠并调整后,跳出内层循环,重新开始检测 + break; + } + } + } + + if (iterations >= maxIterations) { + console.warn(`某层防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`); + } + }); +}; + +// 恢复等腰三角形 +const restoreIsoscelesTriangles = ( + levelsMap: Map, + parentToChildren: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + [...forwardLevels].reverse().forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); + + if (childPositions.length > 0) { + const avgSecondaryCoord = childPositions.reduce((sum, pos) => + sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length; + const pos = nodePositions.get(platform)!; + const primaryCoord = mapper.getPrimaryCoord(pos); + nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord)); + } + } + }); + }); +}; + +// ==================== 辅助函数:创建节点对象 ==================== + +const createNodeObjects = ( + levelsMap: Map, + nodePositions: Map +): Map => { + const nodeMap = new Map(); + + levelsMap.forEach((platformsInLevel, level) => { + platformsInLevel.forEach(platform => { + const pos = nodePositions.get(platform)!; + const componentId = 1; + + const node: GraphTaskElement = { + id: 0, + key: generateKey(), + type: 'scenario', + name: platform, + platformId: 0, + scenarioId: 0, + components: [ + { + id: componentId, + name: `${platform}_comm`, + type: 'communication', + description: `通信组件` + } + ], + template: 0, + templateType: null, + category: null, + multiable: false, + group: null, + description: platform, + order: 0, + position: { + x: Math.round(pos.x), + y: Math.round(pos.y), + }, + width: 250, + height: 145, + inputs: null, + outputs: null, + parameters: [], + variables: [], + }; + + nodeMap.set(platform, node); + }); + }); + + return nodeMap; +}; + +// ==================== 辅助函数:创建根节点 ==================== + +const createRootNode = ( + nodeMap: Map, + rootPlatformName: string, + parentToChildren: Map, + params: LayoutParams, + mapper: CoordinateMapper +): void => { + if (nodeMap.size === 0 || !rootPlatformName) return; + + const directChildren = parentToChildren.get(rootPlatformName) || []; + + let centerSecondaryCoord: number; + if (directChildren.length > 0) { + const childSecondaryPositions = directChildren.map(child => { + const childNode = nodeMap.get(child); + return childNode ? mapper.getSecondaryCoord(childNode.position) : 0; + }).filter(coord => coord > 0); + + if (childSecondaryPositions.length > 0) { + centerSecondaryCoord = childSecondaryPositions.reduce((sum, coord) => sum + coord, 0) / childSecondaryPositions.length; + } else { + const allNodes = Array.from(nodeMap.values()); + const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position)); + const minCoord = Math.min(...coords); + const maxCoord = Math.max(...coords); + centerSecondaryCoord = (minCoord + maxCoord) / 2; + } + } else { + const allNodes = Array.from(nodeMap.values()); + const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position)); + const minCoord = Math.min(...coords); + const maxCoord = Math.max(...coords); + centerSecondaryCoord = (minCoord + maxCoord) / 2; + } + + const rootNode: GraphTaskElement = { + id: 0, + key: generateKey(), + type: 'scenario', + name: rootPlatformName, + platformId: 0, + scenarioId: 0, + components: [ + { + id: 1, + name: `${rootPlatformName}_comm`, + type: 'communication', + description: `通信组件` + } + ], + template: 0, + templateType: null, + category: null, + multiable: false, + group: null, + description: rootPlatformName, + order: 0, + position: mapper.setCoords(params.ROOT_POSITION, Math.round(centerSecondaryCoord)), + width: 250, + height: 145, + inputs: null, + outputs: null, + parameters: [], + variables: [], + }; + + nodeMap.set(rootPlatformName, rootNode); +}; + +// ==================== 辅助函数:创建边 ==================== + +const createEdges = ( + records: CommunicationRecord[], + nodeMap: Map +): GraphEdgeElement[] => { + const edges: GraphEdgeElement[] = []; + + records.forEach((record, index) => { + const sourceNode = nodeMap.get(record.command_platform); + const targetNode = nodeMap.get(record.subordinate_platform); + + if (sourceNode && targetNode && sourceNode.key && targetNode.key) { + const sourceCompId = sourceNode.components?.[0]?.id || 1; + const targetCompId = targetNode.components?.[0]?.id || 1; + + edges.push({ + id: index + 1, + key: generateKey(), + source: sourceNode.key, + target: targetNode.key, + sourcePort: `out-${sourceCompId}`, + targetPort: `in-${targetCompId}`, + attrs: {}, + router: { name: 'normal' }, + connector: { name: 'smooth' }, + }); + } + }); + + return edges; +}; + +// ==================== 新增功能:平台数据转通信关系 ==================== + +/** + * 平台组件接口(来自后端API) + */ +export interface PlatformComponent { + id: number; + name: string | null; + type: string | null; + description: string | null; + platformId: number; + [key: string]: unknown; +} + +/** + * 带组件的平台接口(来自后端API) + */ +export interface PlatformWithComponents { + id: number; + name: string | null; + description: string | null; + scenarioId: number; + components: PlatformComponent[]; + [key: string]: unknown; +} + +/** + * 智能推断通信关系的策略类型 + */ +type InferenceStrategy = + | 'by-name-pattern' // 根据名称模式推断(如包含"command"、"cmd"等关键词) + | 'all-to-first' // 所有节点连接到第一个节点 + | 'chain' // 链式连接(A→B→C→D...) + | 'star'; // 星型连接(中心节点连接所有其他节点) + +/** + * 将平台列表转换为通信关系记录 + * @param platforms 平台列表 + * @param strategy 推断策略,默认为 'by-name-pattern' + * @returns 通信关系记录数组 + */ +export const convertPlatformsToCommunicationRecords = ( + platforms: PlatformWithComponents[], + strategy: InferenceStrategy = 'by-name-pattern' +): CommunicationRecord[] => { + if (!platforms || platforms.length === 0) { + return []; + } + + const records: CommunicationRecord[] = []; + + switch (strategy) { + case 'by-name-pattern': + inferByNamPattern(platforms, records); + break; + case 'all-to-first': + connectAllToFirst(platforms, records); + break; + case 'chain': + createChainConnection(platforms, records); + break; + case 'star': + createStarConnection(platforms, records); + break; + default: + console.warn(`未知的推断策略: ${strategy},使用默认策略 'by-name-pattern'`); + inferByNamPattern(platforms, records); + } + + return records; +}; + +/** + * 根据名称模式推断通信关系 + * - 识别指挥节点(包含"command"、"cmd"、"指挥"等关键词) + * - 其他节点连接到最近的指挥节点 + */ +const inferByNamPattern = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + // 识别指挥节点 + const commandKeywords = ['command', 'cmd', '指挥', 'chief', 'leader']; + const commandNodes = platforms.filter(p => { + const name = (p.name || '').toLowerCase(); + const desc = (p.description || '').toLowerCase(); + return commandKeywords.some(kw => name.includes(kw) || desc.includes(kw)); + }); + + // 非指挥节点 + const otherNodes = platforms.filter(p => !commandNodes.includes(p)); + + // 如果有指挥节点,其他节点连接到最近的指挥节点 + if (commandNodes.length > 0) { + // 按ID排序,取第一个作为主要指挥中心 + const mainCommand = commandNodes.sort((a, b) => a.id - b.id)[0]; + + if (!mainCommand) return; // 安全检查 + + // 其他指挥节点也连接到主指挥 + commandNodes.slice(1).forEach(cmd => { + records.push({ + id: records.length + 1, + command_platform: mainCommand.name || `platform_${mainCommand.id}`, + subordinate_platform: cmd.name || `platform_${cmd.id}`, + scenary_id: mainCommand.scenarioId, + }); + }); + + // 非指挥节点连接到主指挥 + otherNodes.forEach(node => { + records.push({ + id: records.length + 1, + command_platform: mainCommand.name || `platform_${mainCommand.id}`, + subordinate_platform: node.name || `platform_${node.id}`, + scenary_id: mainCommand.scenarioId, + }); + }); + } else { + // 没有指挥节点,使用链式连接 + createChainConnection(platforms, records); + } +}; + +/** + * 所有节点连接到第一个节点 + */ +const connectAllToFirst = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + if (platforms.length < 2) return; + + const first = platforms[0]; + if (!first) return; // 安全检查 + + const firstName = first.name || `platform_${first.id}`; + + platforms.slice(1).forEach(platform => { + records.push({ + id: records.length + 1, + command_platform: firstName, + subordinate_platform: platform.name || `platform_${platform.id}`, + scenary_id: first.scenarioId, + }); + }); +}; + +/** + * 创建链式连接 A→B→C→D... + */ +const createChainConnection = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + for (let i = 0; i < platforms.length - 1; i++) { + const current = platforms[i]; + const next = platforms[i + 1]; + + if (!current || !next) continue; // 安全检查 + + records.push({ + id: records.length + 1, + command_platform: current.name || `platform_${current.id}`, + subordinate_platform: next.name || `platform_${next.id}`, + scenary_id: current.scenarioId, + }); + } +}; + +/** + * 创建星型连接(中间节点连接所有其他节点) + */ +const createStarConnection = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + if (platforms.length < 2) return; + + // 选择中间的节点作为中心 + const centerIndex = Math.floor(platforms.length / 2); + const center = platforms[centerIndex]; + + if (!center) return; // 安全检查 + + const centerName = center.name || `platform_${center.id}`; + + platforms.forEach((platform, idx) => { + if (idx !== centerIndex) { + records.push({ + id: records.length + 1, + command_platform: centerName, + subordinate_platform: platform.name || `platform_${platform.id}`, + scenary_id: center.scenarioId, + }); + } + }); +}; + +/** + * 便捷函数:直接将平台列表转换为图容器 + * @param platforms 平台列表 + * @param strategy 推断策略 + * @param layoutConfig 布局配置(可选) + * @returns 图容器对象 + */ +export const convertPlatformsToGraphContainer = ( + platforms: PlatformWithComponents[], + strategy: InferenceStrategy = 'by-name-pattern', + layoutConfig?: Partial +): GraphContainer => { + const records = convertPlatformsToCommunicationRecords(platforms, strategy); + return convertRecordsToGraphContainer(records, layoutConfig); +}; diff --git a/modeler/src/views/decision/communication/random-data-generator.ts b/modeler/src/views/decision/communication/random-data-generator.ts new file mode 100644 index 0000000..9d81e11 --- /dev/null +++ b/modeler/src/views/decision/communication/random-data-generator.ts @@ -0,0 +1,143 @@ +/* + * This file is part of the kernelstudio package. + * + * (c) 2014-2026 zlin + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +import type { GraphContainer } from '../graph'; +import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter'; + +/** + * 生成随机的通信关系数据用于测试 + * @param nodeCount 节点数量(默认5-8个) + * @param edgeDensity 边的密度(0.3-0.7之间,表示连接概率) + * @returns 随机生成的通信关系记录和对应的GraphContainer + */ +export const generateRandomCommunicationData = ( + nodeCount: number = Math.floor(Math.random() * 4) + 5, // 5-8个节点 + edgeDensity: number = 0.5 +): { records: CommunicationRecord[]; graph: GraphContainer } => { + // 生成随机平台名称 + const platformTypes = ['指挥中心', '雷达站', '导弹阵地', '预警机', '战斗机', '驱逐舰', '潜艇', '电子战飞机']; + const platforms: string[] = []; + + for (let i = 0; i < nodeCount; i++) { + const baseName = platformTypes[i % platformTypes.length]; + const suffix = Math.floor(i / platformTypes.length) > 0 ? `-${Math.floor(i / platformTypes.length) + 1}` : ''; + platforms.push(`${baseName}${suffix}`); + } + + // 生成随机通信关系 - 改进版:更符合实际指挥层级 + const records: CommunicationRecord[] = []; + let recordId = 1; + + // 确定根节点(指挥中心) + const rootPlatformName = platforms.find(p => p.includes('指挥')) || platforms[0] || '默认平台'; + const rootIndex = platforms.indexOf(rootPlatformName); + + if (rootIndex === -1) { + console.warn('未找到根节点,使用第一个平台'); + return { records: [], graph: { nodes: [], edges: [] } }; + } + + // 第一层:指挥中心直接管理的单位(通常是主要作战单元) + // 选择2-4个作为一级下属 + const firstLevelCount = Math.min(Math.max(2, Math.floor(platforms.length / 2)), 4); + const firstLevelIndices: number[] = []; + + for (let i = 0; i < platforms.length && firstLevelIndices.length < firstLevelCount; i++) { + if (i !== rootIndex) { + firstLevelIndices.push(i); + } + } + + // 建立第一层连接:指挥中心 -> 一级下属 + firstLevelIndices.forEach(idx => { + const subordinatePlatform = platforms[idx]; + if (subordinatePlatform) { + records.push({ + id: recordId++, + command_platform: rootPlatformName, + subordinate_platform: subordinatePlatform, + command_comm: '加密指挥链路', + subordinate_comm: '接收端', + scenary_id: 1, + }); + } + }); + + // 第二层:一级下属可以有二级下属 + const remainingIndices = platforms.map((_, i) => i).filter(i => + i !== rootIndex && !firstLevelIndices.includes(i) + ); + + remainingIndices.forEach(idx => { + // 随机选择一个一级下属作为父节点 + const parentIdx = firstLevelIndices[Math.floor(Math.random() * firstLevelIndices.length)]; + + if (parentIdx !== undefined) { + const parentPlatform = platforms[parentIdx]; + const childPlatform = platforms[idx]; + + if (parentPlatform && childPlatform) { + records.push({ + id: recordId++, + command_platform: parentPlatform, + subordinate_platform: childPlatform, + command_comm: Math.random() > 0.5 ? '战术数据链' : '无线通信', + subordinate_comm: '双向通信', + scenary_id: 1, + }); + } + } + }); + + // 第三层:添加少量横向协同连接(不超过总边数的20%) + const maxCrossLinks = Math.floor(records.length * 0.2); + let crossLinkCount = 0; + + if (platforms.length > 4 && crossLinkCount < maxCrossLinks) { + // 在同级之间添加协同连接 + for (let i = 0; i < firstLevelIndices.length - 1 && crossLinkCount < maxCrossLinks; i++) { + if (Math.random() < 0.4) { // 40%概率 + const j = i + 1 + Math.floor(Math.random() * 2); + if (j < firstLevelIndices.length) { + const idxI = firstLevelIndices[i]; + const idxJ = firstLevelIndices[j]; + + if (idxI !== undefined && idxJ !== undefined) { + const platformI = platforms[idxI]; + const platformJ = platforms[idxJ]; + + if (platformI && platformJ) { + const exists = records.some(r => + r.command_platform === platformI && + r.subordinate_platform === platformJ + ); + + if (!exists) { + records.push({ + id: recordId++, + command_platform: platformI, + subordinate_platform: platformJ, + command_comm: '协同通信', + subordinate_comm: '双向通信', + scenary_id: 1, + }); + crossLinkCount++; + } + } + } + } + } + } + } + + // 转换为GraphContainer + const graph = convertRecordsToGraphContainer(records); + + return { records, graph }; +}; diff --git a/modeler/src/views/decision/communication/types.ts b/modeler/src/views/decision/communication/types.ts index fba34cd..a265095 100644 --- a/modeler/src/views/decision/communication/types.ts +++ b/modeler/src/views/decision/communication/types.ts @@ -48,3 +48,22 @@ export interface ScenarioDetailsResponse extends ApiDataResponse { } +/** + * 通信关系记录(对应数据库表结构) + */ +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 { + +} +