diff --git a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java index b073c30..417604f 100644 --- a/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java +++ b/auto-solution-admin/src/main/java/com/solution/web/controller/rule/RuleController.java @@ -5,8 +5,10 @@ import com.solution.common.core.controller.BaseController; import com.solution.common.core.domain.AjaxResult; import com.solution.common.core.page.TableDataInfo; import com.solution.common.enums.BusinessType; +import com.solution.common.utils.poi.ExcelUtil; import com.solution.rule.domain.Rule; import com.solution.rule.domain.config.RuleConfig; +import com.solution.rule.domain.config.RuleConfigExcelRow; import com.solution.rule.domain.config.RuleConfigQuery; import com.solution.rule.domain.config.RuleParamMeta; import com.solution.rule.domain.config.vo.RuleFourBlocksGraphVO; @@ -18,7 +20,9 @@ import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; @Api("红蓝对抗规则管理") @@ -142,4 +146,26 @@ import java.util.List; List metas = ruleConfigService.selectParamMetaList(); return success(metas); } -} \ No newline at end of file + + @PreAuthorize("@ss.hasPermi('system:rule:query')") + @Log(title = "Rule Config", businessType = BusinessType.EXPORT) + @PostMapping("/config/export") + @ApiOperation("Export rule config") + public void exportConfig(HttpServletResponse response, @RequestBody(required = false) RuleConfigQuery query) { + List rows = ruleConfigService.exportRuleConfigRows(query == null ? new RuleConfigQuery() : query); + ExcelUtil util = new ExcelUtil<>(RuleConfigExcelRow.class); + util.exportExcel(response, rows, "rule-config"); + } + + @PreAuthorize("@ss.hasPermi('system:rule:edit')") + @Log(title = "Rule Config", businessType = BusinessType.IMPORT) + @PostMapping("/config/importData") + @ApiOperation("Import rule config") + public AjaxResult importConfig(MultipartFile file, + @RequestParam(value = "updateSupport", defaultValue = "true") boolean updateSupport) throws Exception { + ExcelUtil util = new ExcelUtil<>(RuleConfigExcelRow.class); + List rows = util.importExcel(file.getInputStream()); + String result = ruleConfigService.importRuleConfigRows(rows, updateSupport); + return success(result); + } +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigExcelRow.java b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigExcelRow.java new file mode 100644 index 0000000..4830b38 --- /dev/null +++ b/auto-solution-rule/src/main/java/com/solution/rule/domain/config/RuleConfigExcelRow.java @@ -0,0 +1,65 @@ +package com.solution.rule.domain.config; + +import com.solution.common.annotation.Excel; +import lombok.Data; + +@Data +public class RuleConfigExcelRow { + + @Excel(name = "规则编码") + private String ruleCode; + + @Excel(name = "规则名称") + private String ruleName; + + @Excel(name = "层级编码") + private String levelCode; + + @Excel(name = "类别编码") + private String kindCode; + + @Excel(name = "模块编码") + private String moduleCode; + + @Excel(name = "优先级", cellType = Excel.ColumnType.NUMERIC) + private Integer priorityNo; + + @Excel(name = "条件表达式", width = 24) + private String conditionExpr; + + @Excel(name = "动作表达式", width = 24) + private String actionExpr; + + @Excel(name = "版本号", cellType = Excel.ColumnType.NUMERIC) + private Integer versionNo; + + @Excel(name = "规则启用", cellType = Excel.ColumnType.NUMERIC, prompt = "1=启用,0=停用") + private Integer enabled; + + @Excel(name = "备注", width = 20) + private String remark; + + @Excel(name = "任务类型列表", width = 20, prompt = "多个任务类型用英文逗号分隔") + private String taskTypesCsv; + + @Excel(name = "参数名称") + private String paramName; + + @Excel(name = "参数键") + private String paramKey; + + @Excel(name = "参数值", width = 24) + private String paramVal; + + @Excel(name = "参数值类型") + private String valType; + + @Excel(name = "参数排序", cellType = Excel.ColumnType.NUMERIC) + private Integer sortNo; + + @Excel(name = "参数启用", cellType = Excel.ColumnType.NUMERIC, prompt = "1=启用,0=停用") + private Integer paramEnabled; + + @Excel(name = "参数备注", width = 20) + private String paramRemark; +} diff --git a/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java b/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java index 9e88dc0..06428ab 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/service/IRuleConfigService.java @@ -1,6 +1,7 @@ package com.solution.rule.service; import com.solution.rule.domain.config.RuleConfig; +import com.solution.rule.domain.config.RuleConfigExcelRow; import com.solution.rule.domain.config.RuleConfigQuery; import com.solution.rule.domain.config.RuleDictItem; import com.solution.rule.domain.config.RuleParamMeta; @@ -28,6 +29,10 @@ public interface IRuleConfigService { List selectParamMetaList(); + List exportRuleConfigRows(RuleConfigQuery query); + + String importRuleConfigRows(List rows, boolean updateSupport); + /** * 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。 */ diff --git a/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java index 9d09408..c573f0c 100644 --- a/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java +++ b/auto-solution-rule/src/main/java/com/solution/rule/service/impl/RuleConfigServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.solution.common.constant.ExceptionConstants; import com.solution.rule.domain.config.RuleConfig; +import com.solution.rule.domain.config.RuleConfigExcelRow; import com.solution.rule.domain.config.RuleConfigParam; import com.solution.rule.domain.config.RuleConfigQuery; import com.solution.rule.domain.config.RuleConfigTaskTypeRow; @@ -131,6 +132,118 @@ public class RuleConfigServiceImpl implements IRuleConfigService { return new ArrayList<>(metaMap().values()); } + @Override + public List exportRuleConfigRows(RuleConfigQuery query) { + List configs = selectRuleConfigList(query == null ? new RuleConfigQuery() : query); + List rows = new ArrayList<>(); + for (RuleConfig config : configs) { + if (config == null || ObjectUtil.isEmpty(config.getRuleCode())) { + continue; + } + RuleConfig detail = selectRuleConfigByCode(config.getRuleCode()); + if (detail == null) { + continue; + } + List params = CollUtil.isNotEmpty(detail.getParams()) ? detail.getParams() : Collections.singletonList(new RuleConfigParam()); + String taskTypesCsv = joinTaskTypes(detail.getTaskTypes()); + for (RuleConfigParam param : params) { + RuleConfigExcelRow row = new RuleConfigExcelRow(); + row.setRuleCode(detail.getRuleCode()); + row.setRuleName(detail.getRuleName()); + row.setLevelCode(detail.getLevelCode()); + row.setKindCode(detail.getKindCode()); + row.setModuleCode(detail.getModuleCode()); + row.setPriorityNo(detail.getPriorityNo()); + row.setConditionExpr(detail.getConditionExpr()); + row.setActionExpr(detail.getActionExpr()); + row.setVersionNo(detail.getVersionNo()); + row.setEnabled(detail.getEnabled()); + row.setRemark(detail.getRemark()); + row.setTaskTypesCsv(taskTypesCsv); + if (param != null) { + row.setParamName(param.getParamName()); + row.setParamKey(param.getParamKey()); + row.setParamVal(param.getParamVal()); + row.setValType(param.getValType()); + row.setSortNo(param.getSortNo()); + row.setParamEnabled(param.getEnabled()); + row.setParamRemark(param.getRemark()); + } + rows.add(row); + } + } + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String importRuleConfigRows(List rows, boolean updateSupport) { + if (CollUtil.isEmpty(rows)) { + throw new RuntimeException("Import data cannot be empty"); + } + Map grouped = new LinkedHashMap<>(); + for (RuleConfigExcelRow row : rows) { + if (row == null || isEmptyImportRow(row)) { + continue; + } + String ruleCode = trimmed(row.getRuleCode()); + if (ObjectUtil.isEmpty(ruleCode)) { + throw new RuntimeException("Import failed: ruleCode is required"); + } + RuleConfig config = grouped.computeIfAbsent(ruleCode, code -> { + RuleConfig rc = new RuleConfig(); + rc.setRuleCode(code); + rc.setParams(new ArrayList<>()); + rc.setTaskTypes(new ArrayList<>()); + return rc; + }); + mergeRuleBase(config, row); + if (ObjectUtil.isNotEmpty(trimmed(row.getParamKey()))) { + RuleConfigParam param = new RuleConfigParam(); + param.setRuleCode(ruleCode); + param.setParamName(trimmed(row.getParamName())); + param.setParamKey(trimmed(row.getParamKey())); + param.setParamVal(trimmed(row.getParamVal())); + param.setValType(trimmed(row.getValType())); + param.setSortNo(row.getSortNo()); + param.setEnabled(row.getParamEnabled()); + param.setRemark(trimmed(row.getParamRemark())); + config.getParams().add(param); + } + } + if (grouped.isEmpty()) { + throw new RuntimeException("Import data cannot be empty"); + } + + int inserted = 0; + int updated = 0; + for (RuleConfig config : grouped.values()) { + fillImportDefaults(config); + validateBase(config); + if (CollUtil.isEmpty(config.getParams())) { + throw new RuntimeException("Rule " + config.getRuleCode() + " must contain at least one param row"); + } + boolean exists = ruleConfigMapper.countByRuleCode(config.getRuleCode()) > 0; + if (exists) { + if (!updateSupport) { + throw new RuntimeException("Rule already exists: " + config.getRuleCode()); + } + ruleConfigMapper.updateRuleConfig(fillDefault(config)); + String[] ruleCodes = {config.getRuleCode()}; + ruleConfigMapper.deleteParamsByRuleCodes(ruleCodes); + ruleConfigMapper.deleteTaskTypesByRuleCodes(ruleCodes); + saveChildren(config); + updated++; + } else { + ruleConfigMapper.insertRuleConfig(fillDefault(config)); + saveChildren(config); + inserted++; + } + } + syncDrlAfterCrud(); + return "Import completed. inserted=" + inserted + ", updated=" + updated; + } + @Override public RuleGraphVO buildKnowledgeGraph(List ruleConfigs) { RuleGraphVO graph = new RuleGraphVO(); @@ -595,6 +708,107 @@ public class RuleConfigServiceImpl implements IRuleConfigService { return fallback; } + private boolean isEmptyImportRow(RuleConfigExcelRow row) { + return ObjectUtil.isAllEmpty( + trimmed(row.getRuleCode()), + trimmed(row.getRuleName()), + trimmed(row.getParamKey()), + trimmed(row.getParamVal()), + trimmed(row.getTaskTypesCsv())); + } + + private void mergeRuleBase(RuleConfig config, RuleConfigExcelRow row) { + config.setRuleName(mergeStringField(config.getRuleCode(), "ruleName", config.getRuleName(), row.getRuleName())); + config.setLevelCode(mergeStringField(config.getRuleCode(), "levelCode", config.getLevelCode(), row.getLevelCode())); + config.setKindCode(mergeStringField(config.getRuleCode(), "kindCode", config.getKindCode(), row.getKindCode())); + config.setModuleCode(mergeStringField(config.getRuleCode(), "moduleCode", config.getModuleCode(), row.getModuleCode())); + config.setConditionExpr(mergeStringField(config.getRuleCode(), "conditionExpr", config.getConditionExpr(), row.getConditionExpr())); + config.setActionExpr(mergeStringField(config.getRuleCode(), "actionExpr", config.getActionExpr(), row.getActionExpr())); + config.setRemark(mergeStringField(config.getRuleCode(), "remark", config.getRemark(), row.getRemark())); + config.setPriorityNo(mergeIntegerField(config.getRuleCode(), "priorityNo", config.getPriorityNo(), row.getPriorityNo())); + config.setVersionNo(mergeIntegerField(config.getRuleCode(), "versionNo", config.getVersionNo(), row.getVersionNo())); + config.setEnabled(mergeIntegerField(config.getRuleCode(), "enabled", config.getEnabled(), row.getEnabled())); + String taskTypesCsv = trimmed(row.getTaskTypesCsv()); + if (ObjectUtil.isNotEmpty(taskTypesCsv)) { + List parsed = parseTaskTypes(taskTypesCsv); + if (CollUtil.isNotEmpty(config.getTaskTypes()) && !config.getTaskTypes().equals(parsed)) { + throw new RuntimeException("Rule " + config.getRuleCode() + " has conflicting taskTypesCsv values"); + } + config.setTaskTypes(parsed); + } + } + + private void fillImportDefaults(RuleConfig config) { + if (config.getParams() == null) { + config.setParams(new ArrayList<>()); + } + if (config.getTaskTypes() == null) { + config.setTaskTypes(new ArrayList<>()); + } + int sortNo = 0; + for (RuleConfigParam param : config.getParams()) { + if (param.getSortNo() == null) { + param.setSortNo(sortNo); + } + if (param.getEnabled() == null) { + param.setEnabled(1); + } + sortNo++; + } + } + + private String mergeStringField(String ruleCode, String fieldName, String currentValue, String newValue) { + String current = trimmed(currentValue); + String incoming = trimmed(newValue); + if (ObjectUtil.isEmpty(incoming)) { + return current; + } + if (ObjectUtil.isEmpty(current)) { + return incoming; + } + if (!Objects.equals(current, incoming)) { + throw new RuntimeException("Rule " + ruleCode + " has conflicting values for field " + fieldName); + } + return current; + } + + private Integer mergeIntegerField(String ruleCode, String fieldName, Integer currentValue, Integer newValue) { + if (newValue == null) { + return currentValue; + } + if (currentValue == null) { + return newValue; + } + if (!Objects.equals(currentValue, newValue)) { + throw new RuntimeException("Rule " + ruleCode + " has conflicting values for field " + fieldName); + } + return currentValue; + } + + private List parseTaskTypes(String taskTypesCsv) { + if (ObjectUtil.isEmpty(taskTypesCsv)) { + return new ArrayList<>(); + } + return Arrays.stream(taskTypesCsv.split(",")) + .map(this::trimmed) + .filter(ObjectUtil::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + } + + private String joinTaskTypes(List taskTypes) { + if (CollUtil.isEmpty(taskTypes)) { + return null; + } + return taskTypes.stream() + .filter(ObjectUtil::isNotEmpty) + .collect(Collectors.joining(",")); + } + + private String trimmed(String value) { + return value == null ? null : value.trim(); + } + private void saveChildren(RuleConfig ruleConfig) { if (CollUtil.isNotEmpty(ruleConfig.getParams())) { Set keys = new HashSet<>(); diff --git a/modeler/src/style.less b/modeler/src/style.less index 8ba69bf..b04b742 100644 --- a/modeler/src/style.less +++ b/modeler/src/style.less @@ -1930,6 +1930,13 @@ .ks-sidebar-header { background: url('@/assets/icons/bg-fk-title.png') center / 100% 100%; + min-height: 104px; + padding: 8px 10px 12px; + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 10px; + line-height: normal; } .ant-tree { @@ -1973,6 +1980,9 @@ align-items: stretch; overflow: hidden; padding-right: 0; + background: + linear-gradient(180deg, rgba(1, 12, 28, 0.94) 0%, rgba(2, 16, 36, 0.92) 100%), + url('@/assets/icons/page-card-body.png') center / 100% 100%; } .rule-config-graph-placeholder { @@ -2008,11 +2018,61 @@ } } +.rule-config-sidebar .ks-sidebar-actions { + width: 100%; + display: flex; + justify-content: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid rgba(71, 95, 113, 0.78); + border-radius: 2px; + background: linear-gradient(180deg, rgba(8, 29, 54, 0.88) 0%, rgba(6, 21, 40, 0.92) 100%); + box-shadow: inset 0 0 0 1px rgba(18, 70, 120, 0.18); +} + +.rule-config-sidebar .ks-sidebar-add { + position: static; + right: auto; + top: auto; + font-size: 12px; + + .anticon { + display: block; + float: left; + line-height: 16px; + } +} + +.rule-config-sidebar-action.ant-btn { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 62px; + padding-inline: 10px; + border-color: #1c468b; + background: linear-gradient(0deg, #14223b 0, #1c468b 100%); + color: #eee; + + .anticon { + margin: 0; + line-height: 1; + } + + &:not(:disabled):hover, + &:hover, + &:active { + border-color: #166094; + background: linear-gradient(90deg, #3687bc 0%, #074375 100%); + color: #fff; + } +} + .rule-config-right-cluster { display: flex; flex: 1 1 auto; min-width: 0; align-items: stretch; + background: transparent; } .rule-config-graph-placeholder__text { @@ -2039,6 +2099,7 @@ } .rule-config-right-panel { + position: relative; flex: 0 0 min(480px, 42vw); width: min(480px, 42vw); min-width: 320px; @@ -2046,6 +2107,9 @@ transition: flex-basis 0.2s ease, width 0.2s ease, opacity 0.2s ease, min-width 0.2s ease; overflow: hidden; border-left: 1px solid rgba(71, 95, 113, 0.6); + background: + linear-gradient(180deg, rgba(4, 24, 50, 0.96) 0%, rgba(3, 17, 37, 0.94) 100%); + box-shadow: inset 0 0 0 1px rgba(18, 70, 120, 0.18); } .rule-config-right-panel--collapsed { @@ -2058,8 +2122,26 @@ pointer-events: none; } +.rule-config-right-panel-edge { + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 8px; + padding: 0; + border: 0; + cursor: col-resize; + background: linear-gradient(180deg, rgba(55, 126, 173, 0.15) 0%, rgba(55, 126, 173, 0.5) 50%, rgba(55, 126, 173, 0.15) 100%); + z-index: 2; + + &:hover { + background: linear-gradient(180deg, rgba(55, 126, 173, 0.3) 0%, rgba(55, 126, 173, 0.72) 50%, rgba(55, 126, 173, 0.3) 100%); + } +} + .rule-config-right-panel__inner { height: 100%; overflow-y: auto; - padding: 10px 10px 10px 4px; + padding: 10px 10px 10px 12px; + background: transparent; } diff --git a/modeler/src/views/decision/rule-config/api.ts b/modeler/src/views/decision/rule-config/api.ts index 7956c0d..6d82f7d 100644 --- a/modeler/src/views/decision/rule-config/api.ts +++ b/modeler/src/views/decision/rule-config/api.ts @@ -7,8 +7,9 @@ * that was distributed with this source code. */ -import { HttpRequestClient } from '@/utils/request'; +import { HttpRequestClient, originalAxios } from '@/utils/request'; import type { ApiDataResponse, BasicResponse } from '@/types'; +import type { AxiosResponse } from 'axios'; import type { RuleConfig, RuleConfigPageableResponse, RuleConfigRequest, RuleDictItem, RuleFourBlocksPayload, RuleGraphPayload, RuleParamMeta } from './types'; const req = HttpRequestClient.create({ @@ -50,3 +51,24 @@ export const findRuleConfigGraph = (query: Partial = {}): Pro export const findRuleFourBlocksGraph = (): Promise> => { return req.get('/system/rule/config/graph/four-blocks'); }; + +export const exportRuleConfig = (query: Partial = {}): Promise> => { + return originalAxios.post('/api/system/rule/config/export', query, { + responseType: 'blob', + headers: { + 'Content-Type': 'application/json;charset=utf-8', + 'Accept': 'application/octet-stream', + }, + }); +}; + +export const importRuleConfig = (file: File, updateSupport: boolean = true): Promise => { + const formData = new FormData(); + formData.append('file', file); + formData.append('updateSupport', String(updateSupport)); + return originalAxios.post('/api/system/rule/config/importData', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }).then((response) => response.data as BasicResponse); +}; diff --git a/modeler/src/views/decision/rule-config/management.vue b/modeler/src/views/decision/rule-config/management.vue index bfb3448..54e719f 100644 --- a/modeler/src/views/decision/rule-config/management.vue +++ b/modeler/src/views/decision/rule-config/management.vue @@ -6,10 +6,31 @@ 规则聚合测试 - - - 新增 - + + + + 新增 + + + + + 导入 + + + + + + 导出 + + + + +