Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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,14 +22,14 @@ import java.util.List;
|
||||
@Api("红蓝对抗规则管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/system/rule")
|
||||
public class RuleController extends BaseController {
|
||||
public class RuleController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IRuleService ruleService;
|
||||
@Autowired
|
||||
private IRuleConfigService ruleConfigService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||
/*@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||
@GetMapping("/list")
|
||||
@ApiOperation("查询规则列表")
|
||||
public TableDataInfo list(Rule rule) {
|
||||
@@ -66,7 +67,7 @@ public class RuleController extends BaseController {
|
||||
@ApiOperation("删除规则")
|
||||
public AjaxResult remove(@PathVariable Integer[] ids) {
|
||||
return toAjax(ruleService.deleteRuleByIds(ids));
|
||||
}
|
||||
}*/
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('system:rule:list')")
|
||||
@GetMapping("/config/list")
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ spring:
|
||||
master:
|
||||
url: jdbc:mysql://127.0.0.1:3306/autosolution_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: 123456
|
||||
password: 1234
|
||||
# 从库数据源
|
||||
slave:
|
||||
# 从数据源开关/默认关闭
|
||||
|
||||
@@ -23,9 +23,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</select>
|
||||
|
||||
<sql id="selectBehaviortreeVo">
|
||||
select id, name, description, created_at, updated_at, english_name, xml_content from behaviortree
|
||||
select id, name, description, created_at, updated_at, english_name, xml_content, platform_id, scenario_id from behaviortree
|
||||
</sql>
|
||||
|
||||
|
||||
<select id="selectBehaviortreeList" parameterType="Behaviortree" resultMap="BehaviortreeResult">
|
||||
<include refid="selectBehaviortreeVo"/>
|
||||
<where>
|
||||
@@ -35,6 +36,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="updatedAt != null "> and updated_at = #{updatedAt}</if>
|
||||
<if test="englishName != null and englishName != ''"> and english_name like concat('%', #{englishName}, '%')</if>
|
||||
<if test="xmlContent != null and xmlContent != ''"> and xml_content = #{xmlContent}</if>
|
||||
<if test="xmlContent != null and xmlContent != ''"> and xml_content = #{xmlContent}</if>
|
||||
<if test="platformId != null">platform_id = #{platformId}</if>
|
||||
<if test="scenarioId != null">scenario_id = #{scenarioId}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.solution.rule.service;
|
||||
|
||||
public interface RuleDrlSyncService {
|
||||
|
||||
void syncGlobalParamsToDrl();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -78,8 +78,8 @@ function Map buildParam(){
|
||||
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
|
||||
param.put("missileScore", 1);
|
||||
|
||||
// ===================== 目标分配参数(写入 Tasks.task.execute) =====================
|
||||
// executeTypeDefault:生成 execute[0] 的类型字段
|
||||
// ===================== 目标分配参数(写入 Tasks.task.execute) =====================
|
||||
// executeTypeDefault:生成 execute[0] 的类型字段 取值:strike_test/assault
|
||||
param.put("executeTypeDefault", "assault");
|
||||
// targetPickMode:roundRobin(稳定轮询) / random(伪随机但同输入稳定)
|
||||
param.put("targetPickMode", "roundRobin");
|
||||
@@ -200,16 +200,111 @@ function Map buildParam(){
|
||||
param.put("wingmanAltBaseMeters", 40);
|
||||
param.put("wingmanAltScale", 1.0);
|
||||
|
||||
// ===== 以下由规则配置表自动同步生成(请勿手改 param.put 段) =====
|
||||
param.put("groupRuleEnabled", true);
|
||||
param.put("groupDrawNameSuffix", "编组");
|
||||
param.put("groupDrawNameWithIndex", false);
|
||||
param.put("groupFormationMode", "onePerRed");
|
||||
param.put("groupClusterSize", 3);
|
||||
param.put("groupLeaderPickMode", "byHitRateThenId");
|
||||
param.put("groupMinMembersForWingman", 2);
|
||||
param.put("wingmanDistanceBaseMeters", 100);
|
||||
param.put("wingmanDistanceStepMeters", 50);
|
||||
param.put("wingmanAngleBaseDeg", 50);
|
||||
param.put("wingmanAngleStepDeg", 15);
|
||||
param.put("wingmanAltBaseMeters", 40);
|
||||
param.put("wingmanAltScale", 1.0);
|
||||
param.put("enableTrackWarZoneClamp", true);
|
||||
param.put("trackRuleEnabled", true);
|
||||
param.put("trackRouteAlgorithm", "followBlue");
|
||||
param.put("trackRouteNameSuffix", "航迹");
|
||||
param.put("trackAirDataTypeCsv", "taskPlane,air,plane,flight");
|
||||
param.put("trackAirKeywordsCsv", "机,飞,空,J-,F-,无人机,直升机");
|
||||
param.put("trackGroundTrackType", "routeLineGround");
|
||||
param.put("trackFallbackBearingDeg", 0);
|
||||
param.put("trackExtraNodesMax", 0);
|
||||
param.put("trackShortPathSegments", 3);
|
||||
param.put("trackFlankOffsetMeters", 800);
|
||||
param.put("trackFlankSideMode", "alternate");
|
||||
param.put("trackJamWobbleMeters", 400);
|
||||
param.put("trackJamSegments", 4);
|
||||
param.put("enableWarZoneClamp", true);
|
||||
param.put("positionRuleEnabled", true);
|
||||
param.put("positionAnchorMode", "hybrid");
|
||||
param.put("trackPointDirectionMode", "head2next");
|
||||
param.put("fallbackBearingDeg", 0);
|
||||
param.put("deployDistanceKmMin", 8);
|
||||
param.put("deployDistanceKmMax", 30);
|
||||
param.put("deployDistanceKmDefault", 15);
|
||||
param.put("distanceByPlatformCsv", "");
|
||||
param.put("formationType", "line");
|
||||
param.put("formationSpacingMeters", 300);
|
||||
param.put("formationHeadingOffsetDeg", 15);
|
||||
param.put("defaultDeployHeight", 30);
|
||||
param.put("heightFollowBlueRatio", 0.0);
|
||||
param.put("warZoneClampMode", "nearestInside");
|
||||
param.put("minInterPlatformDistanceMeters", 80);
|
||||
param.put("redHitRateThreshold", 0.6);
|
||||
param.put("maxExtraWeaponsPerTask", 2);
|
||||
param.put("maxSupplementRounds", 2);
|
||||
param.put("extraPickMinScore", 1);
|
||||
param.put("executeTypeDefault", "assault");
|
||||
param.put("targetPickMode", "roundRobin");
|
||||
param.put("minTargetsPerRed", 1);
|
||||
param.put("maxTargetsPerRedCap", 3);
|
||||
param.put("radToTargetsCsv", "0.8:1,0.5:2,0.2:3");
|
||||
param.put("rangeParseRegex", "(\\\\d+(?:\\\\.\\\\d+)?)");
|
||||
param.put("rangeUnit", "km");
|
||||
param.put("minRangeToAllowAssignKm", 0);
|
||||
param.put("weight", 1);
|
||||
param.put("minSelectedScore", 1);
|
||||
param.put("tieBreak", "equipmentId");
|
||||
param.put("outputDrawNameSuffix", "打击任务");
|
||||
param.put("ruleSlotCount", 3);
|
||||
param.put("blueRuleKeywords_1", "F-16,F-35");
|
||||
param.put("redRuleKeywords_1", "防空,导弹,无人机");
|
||||
param.put("ruleScore_1", 5);
|
||||
param.put("blueRuleKeywords_2", "坦克,装甲");
|
||||
param.put("redRuleKeywords_2", "反坦克");
|
||||
param.put("ruleScore_2", 4);
|
||||
param.put("blueRuleKeywords_3", "地面,突击");
|
||||
param.put("redRuleKeywords_3", "远火,榴弹,炮");
|
||||
param.put("ruleScore_3", 2);
|
||||
param.put("bluePlatformKeywords_air", "F-16,J-10,F-35");
|
||||
param.put("redPreferredWhenBlueAir", "防空,导弹,无人机,直升机,空空");
|
||||
param.put("airScore", 2);
|
||||
param.put("airTaskKeywords", "空中,制空,拦截,空战");
|
||||
param.put("airTaskScore", 10);
|
||||
param.put("groundTaskKeywords", "地面,突击,登陆");
|
||||
param.put("redPreferredWhenGround", "远火,榴弹,炮,火箭");
|
||||
param.put("groundScore", 1);
|
||||
param.put("tankKeywords", "坦克,装甲");
|
||||
param.put("redMatchKeywords_tank", "反坦克");
|
||||
param.put("tankScore", 1);
|
||||
param.put("missileKeywords", "导弹,火箭弹,巡航");
|
||||
param.put("redMatchKeywords_missile", "防空,导弹,导弹发射");
|
||||
param.put("missileScore", 1);
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
function void mergeDefaultParams(Map current){
|
||||
Map defaults = buildParam();
|
||||
for (Object k : defaults.keySet()) {
|
||||
if (!current.containsKey(k)) {
|
||||
current.put(k, defaults.get(k));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rule "装备匹配"
|
||||
salience 100
|
||||
when
|
||||
$fact : DroolsFact(task != null)
|
||||
then
|
||||
// 以本文件 buildParam 为真源覆盖同名键,再执行 Java 侧匹配逻辑
|
||||
globalParams.putAll(buildParam());
|
||||
// globalParams.putAll(buildParam());
|
||||
mergeDefaultParams(globalParams);
|
||||
equipmentRule($fact, globalParams);
|
||||
end
|
||||
|
||||
@@ -219,7 +314,8 @@ when
|
||||
$fact : DroolsFact(task != null)
|
||||
then
|
||||
// 显式目标分配规则:填充 Tasks.task.execute.targetList[*].targetId
|
||||
globalParams.putAll(buildParam());
|
||||
// globalParams.putAll(buildParam());
|
||||
mergeDefaultParams(globalParams);
|
||||
target($fact, globalParams);
|
||||
end
|
||||
|
||||
@@ -229,7 +325,8 @@ when
|
||||
$fact : DroolsFact(task != null)
|
||||
then
|
||||
// 显式阵位规则:填充 redWeapons.SubComponents.platform[].positions
|
||||
globalParams.putAll(buildParam());
|
||||
// globalParams.putAll(buildParam());
|
||||
mergeDefaultParams(globalParams);
|
||||
position($fact, globalParams);
|
||||
end
|
||||
|
||||
@@ -239,7 +336,8 @@ when
|
||||
$fact : DroolsFact(task != null)
|
||||
then
|
||||
// 显式航迹规则:填充 TrackParam 下各航迹 id,并绑定 execute[0].targetList[*].moveRouteId
|
||||
globalParams.putAll(buildParam());
|
||||
// globalParams.putAll(buildParam());
|
||||
mergeDefaultParams(globalParams);
|
||||
trackRoute($fact, globalParams);
|
||||
end
|
||||
|
||||
@@ -249,6 +347,7 @@ when
|
||||
$fact : DroolsFact(task != null)
|
||||
then
|
||||
// 显式编组规则:填充 TrackParam.Groups(groupType=addGroup)与 wingmanData
|
||||
globalParams.putAll(buildParam());
|
||||
// globalParams.putAll(buildParam());
|
||||
mergeDefaultParams(globalParams);
|
||||
groupFormation($fact, globalParams);
|
||||
end
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
]
|
||||
44
modeler/src/views/decision/rule-config/api.ts
Normal file
44
modeler/src/views/decision/rule-config/api.ts
Normal 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');
|
||||
};
|
||||
557
modeler/src/views/decision/rule-config/management.vue
Normal file
557
modeler/src/views/decision/rule-config/management.vue
Normal 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>
|
||||
88
modeler/src/views/decision/rule-config/types.ts
Normal file
88
modeler/src/views/decision/rule-config/types.ts
Normal 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> {}
|
||||
8
modeler/types/components.d.ts
vendored
8
modeler/types/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
Reference in New Issue
Block a user