规则展示,bug修复

This commit is contained in:
MHW
2026-04-15 15:30:20 +08:00
parent a67e3e42ba
commit 4404d0e411
16 changed files with 1231 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ 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.service.IRuleService;
import com.solution.rule.service.IRuleConfigService;
import io.swagger.annotations.Api;
@@ -21,7 +22,7 @@ import java.util.List;
@Api("红蓝对抗规则管理")
@RestController
@RequestMapping("/api/system/rule")
public class RuleController extends BaseController {
public class RuleController extends BaseController {
@Autowired
private IRuleService ruleService;
@@ -113,4 +114,12 @@ public class RuleController extends BaseController {
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

@@ -5,7 +5,7 @@ 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}

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

@@ -35,4 +35,6 @@ public interface RuleConfigMapper {
int insertTaskTypesBatch(@Param("ruleCode") String ruleCode, @Param("taskTypes") List<String> taskTypes);
List<RuleDictItem> selectDictByType(@Param("dictType") String dictType);
List<RuleConfigParam> selectEnabledParamsForGlobal();
}

View File

@@ -3,8 +3,10 @@ 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 java.util.List;
import java.util.Map;
public interface IRuleConfigService {
@@ -19,4 +21,8 @@ public interface IRuleConfigService {
int deleteRuleConfigByCodes(String[] ruleCodes);
List<RuleDictItem> selectDictByType(String dictType);
Map<String, Object> loadEnabledGlobalParams();
List<RuleParamMeta> selectParamMetaList();
}

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

@@ -7,21 +7,27 @@ 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.domain.config.RuleParamMeta;
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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;
@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) {
@@ -48,6 +54,8 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
}
int rows = ruleConfigMapper.insertRuleConfig(fillDefault(ruleConfig));
saveChildren(ruleConfig);
// return rows;
syncDrlAfterCrud();
return rows;
}
@@ -63,6 +71,8 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes);
ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes);
saveChildren(ruleConfig);
// return rows;
syncDrlAfterCrud();
return rows;
}
@@ -74,7 +84,10 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
}
ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes);
ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes);
return ruleConfigMapper.deleteRuleConfigByCodes(ruleCodes);
// return ruleConfigMapper.deleteRuleConfigByCodes(ruleCodes);
int rows = ruleConfigMapper.deleteRuleConfigByCodes(ruleCodes);
syncDrlAfterCrud();
return rows;
}
@Override
@@ -85,6 +98,27 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
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());
}
private void saveChildren(RuleConfig ruleConfig) {
if (CollUtil.isNotEmpty(ruleConfig.getParams())) {
Set<String> keys = new HashSet<>();
@@ -95,6 +129,7 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
if (!keys.add(param.getParamKey())) {
throw new RuntimeException("参数键重复: " + param.getParamKey());
}
validateParam(param);
param.setRuleCode(ruleConfig.getRuleCode());
if (param.getSortNo() == null) {
param.setSortNo(0);
@@ -136,4 +171,200 @@ public class RuleConfigServiceImpl implements IRuleConfigService {
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

@@ -171,4 +171,13 @@
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,7 +78,7 @@ function Map buildParam(){
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
param.put("missileScore", 1);
// ===================== 目标分配参数(写入 Tasks.task.execute =====================
// ===================== 目标分配参数(写入 Tasks.task.execute =====================
// executeTypeDefault生成 execute[0] 的类型字段 取值strike_test/assault
param.put("executeTypeDefault", "assault");
// targetPickModeroundRobin(稳定轮询) / random(伪随机但同输入稳定)
@@ -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

@@ -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

@@ -0,0 +1,44 @@
/*
* This file is part of the kernelstudio package.
*
* (c) 2014-2026 zlin <admin@kernelstudio.com>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
import { HttpRequestClient } from '@/utils/request';
import type { ApiDataResponse, BasicResponse } from '@/types';
import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleParamMeta } from './types';
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
});
export const findRuleConfigByQuery = (query: Partial<RuleConfigRequest> = {}): Promise<RuleConfigPageableResponse> => {
return req.get('/system/rule/config/list', query);
};
export const findRuleConfigByCode = (ruleCode: string): Promise<ApiDataResponse<RuleConfig>> => {
return req.get(`/system/rule/config/${ruleCode}`);
};
export const createRuleConfig = (ruleConfig: RuleConfig): Promise<BasicResponse> => {
return req.postJson('/system/rule/config', ruleConfig);
};
export const updateRuleConfig = (ruleConfig: RuleConfig): Promise<BasicResponse> => {
return req.putJson('/system/rule/config', ruleConfig);
};
export const deleteRuleConfig = (ruleCode: string): Promise<BasicResponse> => {
return req.delete(`/system/rule/config/${ruleCode}`);
};
export const findRuleDictByType = (dictType: string): Promise<ApiDataResponse<RuleDictItem[]>> => {
return req.get(`/system/rule/config/dict/${dictType}`);
};
export const findRuleParamMeta = (): Promise<ApiDataResponse<RuleParamMeta[]>> => {
return req.get('/system/rule/config/param-meta');
};

View File

@@ -0,0 +1,557 @@
<template>
<Layout>
<template #sidebar>
<div class="ks-sidebar-header">
<a-flex class="ks-sidebar-title">
<span class="icon"></span>
<span class="text">规则聚合测试</span>
</a-flex>
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
新增
</a-button>
</div>
<a-tree
class="ks-sidebar-list"
:tree-data="treeData"
v-model:expandedKeys="expandedTreeKeys"
v-model:selectedKeys="selectedTreeKeys"
@select="handleTreeSelect"
/>
<a-pagination
v-model:current="query.pageNum"
:page-size="query.pageSize"
:total="datasourceTotal"
simple
size="small"
@change="handleChange"
/>
</template>
<div class="w-full h-full">
<a-card class="ks-page-card ks-algorithm-card">
<template #title>
<a-space>
<span class="point"></span>
<span class="text">规则聚合配置</span>
</a-space>
</template>
<div class="ks-scrollable" style="height: 80.5vh;overflow-y: auto;padding-right: 10px">
<a-row :gutter="15">
<a-col :span="16">
<a-form
ref="formRef"
:label-col="{ span: 6 }"
:model="selectedRuleConfig"
autocomplete="off"
layout="horizontal"
name="rule-config"
>
<a-alert
type="info"
show-icon
style="margin-bottom: 12px;"
message="仅保留业务参数参数作用、参数Key、参数值"
/>
<a-form-item
label="参数配置"
:rules="[{ required: true, message: '请至少配置一条参数', trigger: ['change'] }]"
name="params"
>
<a-form-item-rest>
<div class="ks-sidebar-list-param-list">
<div class="ks-sidebar-list-param-item" v-for="(item,index) in selectedRuleConfig.params" :key="`param-${index}`">
<a-row :gutter="10">
<a-col :span="6">
<a-input v-model:value="item.paramName" placeholder="参数作用(业务说明)" />
</a-col>
<a-col :span="6">
<a-select
v-model:value="item.paramKey"
placeholder="参数Key"
:options="paramMetaOptions"
show-search
:filter-option="filterParamKey"
@change="() => handleParamKeyChange(item)"
/>
</a-col>
<a-col :span="9">
<template v-if="resolveMeta(item.paramKey)?.valueType === 'bool'">
<a-select
v-model:value="item.paramVal"
placeholder="参数值"
:options="boolOptions"
/>
</template>
<template v-else-if="resolveMeta(item.paramKey)?.valueType === 'enum'">
<a-select
v-model:value="item.paramVal"
placeholder="参数值"
:options="toEnumOptions(resolveMeta(item.paramKey)?.enumOptions)"
/>
</template>
<template v-else-if="resolveMeta(item.paramKey)?.valueType === 'number'">
<a-input-number
v-model:value="item.paramVal"
placeholder="参数值"
style="width: 100%;"
:min="resolveMeta(item.paramKey)?.min ?? undefined"
:max="resolveMeta(item.paramKey)?.max ?? undefined"
/>
</template>
<template v-else>
<a-input v-model:value="item.paramVal" placeholder="参数值" />
</template>
<div class="text-xs text-red-400 mt-1" v-if="!resolveMeta(item.paramKey) && item.paramKey">
未知参数键当前为兼容展示请联系后端补充参数元数据后再保存
</div>
<div class="text-xs text-gray-400 mt-1" v-if="resolveMeta(item.paramKey)?.description">
{{ resolveMeta(item.paramKey)?.description }}<span v-if="resolveMeta(item.paramKey)?.example">示例{{ resolveMeta(item.paramKey)?.example }}</span>
</div>
</a-col>
<a-col :span="3">
<a-space class="ks-sidebar-list-param-actions">
<MinusCircleOutlined @click.stop="() => handleMinusParam(index)" />
<PlusCircleOutlined @click.stop="() => handleAddParam()" v-if="index === 0" />
</a-space>
</a-col>
</a-row>
</div>
</div>
</a-form-item-rest>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 6 }">
<a-space>
<a-button @click="handleSave" type="primary">保存规则</a-button>
<a-popconfirm
v-if="selectedRuleConfig && selectedRuleConfig.id > 0 && selectedRuleConfig.ruleCode"
title="确定删除?"
@confirm="handleDelete"
>
<a-button danger>删除规则</a-button>
</a-popconfirm>
</a-space>
</a-form-item>
</a-form>
</a-col>
</a-row>
</div>
</a-card>
</div>
</Layout>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue';
import { type FormInstance, message } from 'ant-design-vue';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons-vue';
import Layout from '../layout.vue';
import { createRuleConfig, deleteRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, updateRuleConfig } from './api';
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
const query = ref<RuleConfigRequest>({
pageNum: 1,
pageSize: 10,
});
const defaultParam = (): RuleConfigParam => ({
ruleCode: null,
paramKey: null,
paramVal: null,
valType: 'string',
paramName: null,
sortNo: 0,
enabled: 1,
remark: null,
});
const defaultRuleConfig: RuleConfig = {
id: 0,
ruleCode: null,
ruleName: null,
levelCode: null,
kindCode: null,
moduleCode: null,
priorityNo: 0,
conditionExpr: null,
actionExpr: null,
versionNo: 1,
enabled: 1,
remark: null,
params: [defaultParam()],
taskTypes: [],
};
const datasource = ref<RuleConfig[]>([]);
const datasourceTotal = ref<number>(0);
const selectedRuleConfig = ref<RuleConfig>(JSON.parse(JSON.stringify(defaultRuleConfig)));
const formRef = ref<FormInstance | null>(null);
const treeData = ref<any[]>([]);
const selectedTreeKeys = ref<string[]>([]);
const expandedTreeKeys = ref<string[]>([]);
const detailMap = ref<Record<string, RuleConfig>>({});
const paramMetaList = ref<RuleParamMeta[]>([]);
const paramMetaMap = ref<Record<string, RuleParamMeta>>({});
const boolOptions = [
{ label: 'true', value: 'true' },
{ label: 'false', value: 'false' },
];
const paramMetaOptions = ref<{ label: string; value: string }[]>([]);
const levelOptions = [
{ code: 'task', name: '任务级' },
{ code: 'platform', name: '平台级' },
{ code: 'action', name: '行动级' },
];
const kindOptions = [
{ code: 'select', name: '选择' },
{ code: 'assign', name: '分配' },
{ code: 'deploy', name: '部署' },
{ code: 'config', name: '配置' },
{ code: 'mode', name: '工作模式' },
{ code: 'spacetime', name: '时空约束' },
{ code: 'relation', name: '关联关系' },
{ code: 'limit', name: '限制条件' },
];
const buildTreeData = () => {
const group: Record<string, Record<string, any[]>> = {};
datasource.value.forEach((rule) => {
const detail = rule.ruleCode ? detailMap.value[rule.ruleCode] : null;
const levelCode = rule.levelCode;
const kindCode = rule.kindCode;
if (!levelCode || !kindCode) return;
const params = detail?.params?.filter((item) => item.paramKey) ?? [];
if (params.length === 0) return;
group[levelCode] = group[levelCode] ?? {};
group[levelCode][kindCode] = group[levelCode][kindCode] ?? [];
params.forEach((param, index) => {
const paramTitle = param.paramName || param.paramKey || `参数${index + 1}`;
group[levelCode][kindCode].push({
title: paramTitle,
key: `param:${rule.ruleCode}:${param.paramKey || index}`,
isLeaf: true,
nodeType: 'param',
ruleCode: rule.ruleCode,
});
});
});
treeData.value = levelOptions
.map((level) => {
const levelGroup = group[level.code] ?? {};
const kindChildren = kindOptions
.map((kind) => {
const children = levelGroup[kind.code] ?? [];
if (children.length === 0) return null;
return {
title: kind.name,
key: `kind:${level.code}:${kind.code}`,
nodeType: 'kind',
children,
};
})
.filter((item) => !!item);
if (kindChildren.length === 0) return null;
return {
title: level.name,
key: `level:${level.code}`,
nodeType: 'level',
children: kindChildren,
};
})
.filter((item) => !!item);
};
const load = async () => {
datasource.value = [];
datasourceTotal.value = 0;
detailMap.value = {};
if (selectedRuleConfig.value.id <= 0) {
selectedRuleConfig.value = JSON.parse(JSON.stringify(defaultRuleConfig));
nextTick(() => {
formRef.value?.resetFields();
});
}
const r = await findRuleConfigByQuery(query.value as Partial<RuleConfigRequest>);
datasource.value = r.rows ?? [];
datasourceTotal.value = r.total ?? 0;
await Promise.all(
datasource.value
.filter((item) => !!item.ruleCode)
.map(async (item) => {
if (!item.ruleCode) return;
const detailResp = await findRuleConfigByCode(item.ruleCode);
if (detailResp.code === 200 && detailResp.data) {
detailMap.value[item.ruleCode] = detailResp.data;
}
}),
);
buildTreeData();
};
const loadParamMeta = async () => {
const r = await findRuleParamMeta();
if (r.code !== 200 || !Array.isArray(r.data)) {
message.error(r.msg ?? '加载参数元数据失败');
return;
}
paramMetaList.value = r.data;
const map: Record<string, RuleParamMeta> = {};
const options: { label: string; value: string }[] = [];
r.data.forEach((item) => {
if (!item.paramKey) return;
map[item.paramKey] = item;
options.push({
label: item.label ? `${item.label} (${item.paramKey})` : String(item.paramKey),
value: String(item.paramKey),
});
});
paramMetaMap.value = map;
paramMetaOptions.value = options;
};
const resolveMeta = (paramKey: string | null): RuleParamMeta | null => {
if (!paramKey) return null;
return paramMetaMap.value[paramKey] ?? null;
};
const toEnumOptions = (enumOptions: string[] | null | undefined) => {
if (!Array.isArray(enumOptions)) return [];
return enumOptions.map((v) => ({ label: v, value: v }));
};
const filterParamKey = (input: string, option: { label: string; value: string }) => {
const text = `${option.label ?? ''}${option.value ?? ''}`.toLowerCase();
return text.includes(input.toLowerCase());
};
const ensureUnknownParamOptions = (params: RuleConfigParam[]) => {
const exists = new Set(paramMetaOptions.value.map((item) => item.value));
const append: { label: string; value: string }[] = [];
params.forEach((item) => {
if (!item?.paramKey) return;
if (!exists.has(item.paramKey)) {
const opt = { label: `[未知] ${item.paramKey}`, value: item.paramKey };
append.push(opt);
exists.add(item.paramKey);
}
});
if (append.length > 0) {
paramMetaOptions.value = [...paramMetaOptions.value, ...append];
}
};
const handleCreate = () => {
formRef.value?.resetFields();
selectedRuleConfig.value = JSON.parse(JSON.stringify(defaultRuleConfig));
};
const handleSelect = (item: RuleConfig) => {
if (!item.ruleCode) {
return;
}
findRuleConfigByCode(item.ruleCode).then((r) => {
if (r.code === 200 && r.data) {
formRef.value?.resetFields();
nextTick(() => {
const detail = JSON.parse(JSON.stringify(r.data)) as RuleConfig;
if (!Array.isArray(detail.params) || detail.params.length === 0) {
detail.params = [defaultParam()];
}
if (!Array.isArray(detail.taskTypes)) {
detail.taskTypes = [];
}
ensureUnknownParamOptions(detail.params ?? []);
selectedRuleConfig.value = detail;
});
} else {
message.error(r.msg ?? '查询详情失败');
}
});
};
const handleTreeSelect = (_keys: (string | number)[], e: any) => {
const node = e?.node;
if (!node || node.nodeType !== 'param') {
return;
}
const ruleCode = node.ruleCode as string | null;
if (!ruleCode) {
return;
}
selectedTreeKeys.value = [String(node.key)];
handleSelect({
...defaultRuleConfig,
ruleCode,
});
};
const handleDelete = () => {
if (selectedRuleConfig.value?.id > 0 && selectedRuleConfig.value.ruleCode) {
deleteRuleConfig(selectedRuleConfig.value.ruleCode).then((r) => {
if (r.code === 200) {
load();
message.success(r.msg ?? '删除成功');
} else {
message.error(r.msg ?? '删除失败');
}
});
}
};
const handleSave = () => {
formRef.value?.validate().then(() => {
const savedValue: RuleConfig = JSON.parse(JSON.stringify(selectedRuleConfig.value));
savedValue.params = (savedValue.params ?? []).filter((item) => item.paramKey);
if (savedValue.params.length === 0) {
message.error('请至少填写一个参数键');
return;
}
savedValue.params = savedValue.params.map((item, index) => ({
...item,
ruleCode: savedValue.ruleCode,
sortNo: item.sortNo ?? index,
enabled: item.enabled ?? 1,
valType: resolveMeta(item.paramKey)?.valueType === 'bool'
? 'bool'
: resolveMeta(item.paramKey)?.valueType === 'number'
? 'number'
: 'string',
paramVal: normalizeParamVal(item),
}));
const validMsg = validateParamsBeforeSubmit(savedValue.params);
if (validMsg) {
message.error(validMsg);
return;
}
// 保留后端必填字段,前端不展示时统一补默认值
savedValue.ruleCode = savedValue.ruleCode ?? `rule_${Date.now()}`;
savedValue.ruleName = savedValue.ruleName ?? savedValue.ruleCode;
savedValue.levelCode = savedValue.levelCode ?? 'task';
savedValue.kindCode = savedValue.kindCode ?? 'select';
savedValue.moduleCode = savedValue.moduleCode ?? 'equipment';
savedValue.priorityNo = savedValue.priorityNo ?? 100;
savedValue.versionNo = savedValue.versionNo ?? 1;
savedValue.enabled = savedValue.enabled ?? 1;
savedValue.taskTypes = Array.isArray(savedValue.taskTypes) ? savedValue.taskTypes : [];
const request = savedValue.id > 0
? updateRuleConfig(savedValue)
: createRuleConfig(savedValue);
request.then(async (r) => {
if (r.code === 200) {
await load();
message.success(r.msg ?? '操作成功');
} else {
message.error(r.msg ?? '操作失败');
}
}).catch((err) => {
message.error('请求失败:' + err.message);
});
}).catch((err) => {
message.error('表单验证失败:' + err.message);
});
};
const handleChange = async (page: number, pageSize: number) => {
query.value.pageNum = page;
query.value.pageSize = pageSize;
await load();
};
const handleAddParam = () => {
const row = defaultParam();
if (paramMetaOptions.value.length > 0) {
row.paramKey = paramMetaOptions.value[0].value;
handleParamKeyChange(row);
}
selectedRuleConfig.value.params.push(row);
};
const handleMinusParam = (index: number) => {
const params = [...selectedRuleConfig.value.params];
if (params.length <= 1) {
selectedRuleConfig.value.params = [defaultParam()];
return;
}
params.splice(index, 1);
selectedRuleConfig.value.params = params;
};
onMounted(() => {
loadParamMeta().then(() => load());
});
const handleParamKeyChange = (item: RuleConfigParam) => {
const meta = resolveMeta(item.paramKey);
if (!meta) {
item.valType = 'string';
return;
}
if (meta.valueType === 'bool') {
item.valType = 'bool';
if (item.paramVal !== 'true' && item.paramVal !== 'false') {
item.paramVal = 'true';
}
return;
}
if (meta.valueType === 'number') {
item.valType = 'number';
return;
}
item.valType = 'string';
if (meta.valueType === 'enum' && Array.isArray(meta.enumOptions) && meta.enumOptions.length > 0) {
if (!meta.enumOptions.includes(String(item.paramVal ?? ''))) {
item.paramVal = meta.enumOptions[0];
}
}
};
const normalizeParamVal = (item: RuleConfigParam): string | null => {
if (item.paramVal === null || item.paramVal === undefined) return null;
return String(item.paramVal);
};
const validateParamsBeforeSubmit = (params: RuleConfigParam[]): string | null => {
for (const item of params) {
const key = item.paramKey;
if (!key) return '参数Key不能为空';
const meta = resolveMeta(key);
if (!meta) return `不支持的参数Key: ${key}`;
const val = String(item.paramVal ?? '');
if (meta.required && !val) return `参数值不能为空: ${key}`;
if (meta.valueType === 'bool' && val !== 'true' && val !== 'false') {
return `布尔参数仅支持 true/false: ${key}`;
}
if (meta.valueType === 'enum' && Array.isArray(meta.enumOptions) && !meta.enumOptions.includes(val)) {
return `参数值不在可选范围内: ${key}`;
}
if (meta.valueType === 'number') {
const num = Number(val);
if (Number.isNaN(num)) return `数值参数格式错误: ${key}`;
if (meta.min !== null && meta.min !== undefined && num < meta.min) return `参数值小于最小值: ${key}`;
if (meta.max !== null && meta.max !== undefined && num > meta.max) return `参数值大于最大值: ${key}`;
}
if (meta.pattern) {
const regex = new RegExp(meta.pattern);
if (!regex.test(val)) return `参数格式错误: ${key}`;
}
}
return null;
};
</script>

View File

@@ -0,0 +1,88 @@
/*
* This file is part of the kernelstudio package.
*
* (c) 2014-2026 zlin <admin@kernelstudio.com>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
import type { NullableString, PageableResponse } from '@/types';
export interface RuleConfigParam {
// 规则编码
ruleCode: NullableString,
// 参数键
paramKey: NullableString,
// 参数值
paramVal: NullableString | number,
// 值类型(string/number/bool/json)
valType: NullableString,
// 参数名称
paramName: NullableString,
// 排序号
sortNo: number | null,
// 是否启用(1是0否)
enabled: number | null,
// 备注
remark: NullableString,
}
export interface RuleDictItem {
dictType: NullableString,
dictCode: NullableString,
dictName: NullableString,
sortNo: number | null,
enabled: number | null,
remark: NullableString,
}
export interface RuleParamMeta {
paramKey: NullableString,
label: NullableString,
valueType: NullableString,
required: boolean | null,
enumOptions: string[] | null,
min: number | null,
max: number | null,
pattern: NullableString,
example: NullableString,
description: NullableString,
}
export interface RuleConfig {
id: number,
// 规则编码
ruleCode: NullableString,
// 规则名称
ruleName: NullableString,
// 层级编码
levelCode: NullableString,
// 种类编码
kindCode: NullableString,
// 模块编码
moduleCode: NullableString,
// 优先级(数值越小优先级越高)
priorityNo: number | null,
// 条件表达式
conditionExpr: NullableString,
// 动作表达式
actionExpr: NullableString,
// 版本号
versionNo: number | null,
// 是否启用1是0否
enabled: number | null,
// 备注
remark: NullableString,
// 参数列表
params: RuleConfigParam[],
// 适用任务类型编码列表
taskTypes: string[],
}
export interface RuleConfigRequest extends Partial<RuleConfig> {
pageNum: number,
pageSize: number,
}
export interface RuleConfigPageableResponse extends PageableResponse<RuleConfig> {}

View File

@@ -12,6 +12,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
@@ -40,6 +41,8 @@ declare module 'vue' {
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
@@ -49,6 +52,7 @@ declare module 'vue' {
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATree: typeof import('ant-design-vue/es')['Tree']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
@@ -56,6 +60,7 @@ declare module 'vue' {
// For TSX support
declare global {
const AAlert: typeof import('ant-design-vue/es')['Alert']
const ABadge: typeof import('ant-design-vue/es')['Badge']
const AButton: typeof import('ant-design-vue/es')['Button']
const ACard: typeof import('ant-design-vue/es')['Card']
@@ -84,6 +89,8 @@ declare global {
const AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
const APagination: typeof import('ant-design-vue/es')['Pagination']
const APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
const ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
const ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
const ARow: typeof import('ant-design-vue/es')['Row']
const ASelect: typeof import('ant-design-vue/es')['Select']
const ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
@@ -93,6 +100,7 @@ declare global {
const ATabs: typeof import('ant-design-vue/es')['Tabs']
const ATextarea: typeof import('ant-design-vue/es')['Textarea']
const ATooltip: typeof import('ant-design-vue/es')['Tooltip']
const ATree: typeof import('ant-design-vue/es')['Tree']
const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView']
}