界面优化

This commit is contained in:
MHW
2026-05-07 15:40:57 +08:00
parent c597c3c01f
commit 2509b30382
14 changed files with 1809 additions and 9 deletions

View File

@@ -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<RuleParamMeta> metas = ruleConfigService.selectParamMetaList();
return success(metas);
}
}
@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<RuleConfigExcelRow> rows = ruleConfigService.exportRuleConfigRows(query == null ? new RuleConfigQuery() : query);
ExcelUtil<RuleConfigExcelRow> 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<RuleConfigExcelRow> util = new ExcelUtil<>(RuleConfigExcelRow.class);
List<RuleConfigExcelRow> rows = util.importExcel(file.getInputStream());
String result = ruleConfigService.importRuleConfigRows(rows, updateSupport);
return success(result);
}
}

View File

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

View File

@@ -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<RuleParamMeta> selectParamMetaList();
List<RuleConfigExcelRow> exportRuleConfigRows(RuleConfigQuery query);
String importRuleConfigRows(List<RuleConfigExcelRow> rows, boolean updateSupport);
/**
* 根据当前页规则主数据构建知识图谱(节点与边),参数与任务类型从库批量加载。
*/

View File

@@ -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<RuleConfigExcelRow> exportRuleConfigRows(RuleConfigQuery query) {
List<RuleConfig> configs = selectRuleConfigList(query == null ? new RuleConfigQuery() : query);
List<RuleConfigExcelRow> 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<RuleConfigParam> 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<RuleConfigExcelRow> rows, boolean updateSupport) {
if (CollUtil.isEmpty(rows)) {
throw new RuntimeException("Import data cannot be empty");
}
Map<String, RuleConfig> 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<RuleConfig> 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<String> 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<String> 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<String> 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<String> keys = new HashSet<>();

View File

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

View File

@@ -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<BasicResponse>({
@@ -50,3 +51,24 @@ export const findRuleConfigGraph = (query: Partial<RuleConfigRequest> = {}): Pro
export const findRuleFourBlocksGraph = (): Promise<ApiDataResponse<RuleFourBlocksPayload>> => {
return req.get('/system/rule/config/graph/four-blocks');
};
export const exportRuleConfig = (query: Partial<RuleConfigRequest> = {}): Promise<AxiosResponse<Blob>> => {
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<BasicResponse> => {
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);
};

View File

@@ -6,10 +6,31 @@
<span class="icon"></span>
<span class="text">规则聚合测试</span>
</a-flex>
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
新增
</a-button>
<a-space class="ks-sidebar-actions">
<a-button class="ks-sidebar-add" size="small" @click="handleCreate">
<PlusOutlined />
&#26032;&#22686;
</a-button>
<a-tooltip title="导入 Excel">
<a-button class="rule-config-sidebar-action" size="small" :loading="importLoading" @click="handleImportTrigger">
<UploadOutlined />
导入
</a-button>
</a-tooltip>
<a-tooltip title="导出 Excel">
<a-button class="rule-config-sidebar-action" size="small" :loading="exportLoading" @click="handleExport">
<DownloadOutlined />
导出
</a-button>
</a-tooltip>
</a-space>
<input
ref="importInputRef"
type="file"
accept=".xls,.xlsx"
style="display: none;"
@change="handleImportChange"
/>
</div>
<a-tree
@@ -72,7 +93,15 @@
<div
class="rule-config-right-panel"
:class="{ 'rule-config-right-panel--collapsed': !rightPanelExpanded }"
:style="rightPanelExpanded ? { flexBasis: `${rightPanelWidth}px`, width: `${rightPanelWidth}px` } : undefined"
>
<button
v-if="rightPanelExpanded"
type="button"
class="rule-config-right-panel-edge"
title="左右拖动调整参数配置区域宽度"
@mousedown="onRightPanelResizeMouseDown"
/>
<div class="rule-config-right-panel__inner">
<a-form
ref="formRef"
@@ -182,10 +211,10 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue';
import { type FormInstance, message } from 'ant-design-vue';
import { LeftOutlined, MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons-vue';
import { DownloadOutlined, LeftOutlined, MinusCircleOutlined, PlusCircleOutlined, PlusOutlined, RightOutlined, UploadOutlined } from '@ant-design/icons-vue';
import Layout from '../layout.vue';
import RuleKnowledgeGraph from './RuleKnowledgeGraph.vue';
import { createRuleConfig, deleteRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, updateRuleConfig } from './api';
import { createRuleConfig, deleteRuleConfig, exportRuleConfig, findRuleConfigByCode, findRuleConfigByQuery, findRuleParamMeta, importRuleConfig, updateRuleConfig } from './api';
import type { RuleConfig, RuleConfigParam, RuleConfigRequest, RuleParamMeta } from './types';
const query = ref<RuleConfigRequest>({
@@ -225,6 +254,9 @@ 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 importInputRef = ref<HTMLInputElement | null>(null);
const importLoading = ref(false);
const exportLoading = ref(false);
const treeData = ref<any[]>([]);
const selectedTreeKeys = ref<string[]>([]);
const expandedTreeKeys = ref<string[]>([]);
@@ -250,14 +282,24 @@ const rightPanelExpanded = ref(true);
const GRAPH_PANE_STORAGE_KEY = 'rule-config-graph-pane-percent';
const GRAPH_PANE_MIN = 18;
const GRAPH_PANE_MAX = 70;
const RIGHT_PANEL_WIDTH_STORAGE_KEY = 'rule-config-right-panel-width';
const RIGHT_PANEL_MIN = 360;
const RIGHT_PANEL_MAX = 960;
const graphPanePercent = ref(32);
const graphPanePercentBeforeFourBlocks = ref<number | null>(null);
const splitRootRef = ref<HTMLElement | null>(null);
const graphRevision = ref(0);
const rightPanelWidth = ref(480);
const clampGraphPercent = (n: number) =>
Math.min(GRAPH_PANE_MAX, Math.max(GRAPH_PANE_MIN, Math.round(n)));
const clampRightPanelWidth = (n: number) => {
const rootWidth = splitRootRef.value?.getBoundingClientRect().width ?? window.innerWidth;
const dynamicMax = Math.min(RIGHT_PANEL_MAX, Math.max(420, Math.floor(rootWidth * 0.72)));
return Math.min(dynamicMax, Math.max(RIGHT_PANEL_MIN, Math.round(n)));
};
const onRuleGraphDensityChange = (mode: 'overview' | 'full' | 'four-blocks') => {
if (mode === 'four-blocks') {
if (graphPanePercentBeforeFourBlocks.value === null) {
@@ -295,6 +337,28 @@ const onGraphSplitMouseDown = (e: MouseEvent) => {
window.addEventListener('mouseup', onUp);
};
const onRightPanelResizeMouseDown = (e: MouseEvent) => {
e.preventDefault();
const root = splitRootRef.value;
if (!root) return;
const rect = root.getBoundingClientRect();
const onMove = (ev: MouseEvent) => {
const width = rect.right - ev.clientX;
rightPanelWidth.value = clampRightPanelWidth(width);
};
const onUp = () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
document.body.style.removeProperty('cursor');
document.body.style.removeProperty('user-select');
sessionStorage.setItem(RIGHT_PANEL_WIDTH_STORAGE_KEY, String(rightPanelWidth.value));
};
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
};
const kindOptions = [
{ code: 'select', name: '选择' },
{ code: 'assign', name: '分配' },
@@ -581,6 +645,79 @@ const handleMinusParam = (index: number) => {
selectedRuleConfig.value.params = params;
};
const extractFilename = (contentDisposition?: string) => {
if (!contentDisposition) {
return 'rule-config.xlsx';
}
const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(contentDisposition);
const raw = match?.[1] ?? match?.[2] ?? 'rule-config.xlsx';
try {
return decodeURIComponent(raw.replace(/\+/g, '%20'));
} catch {
return raw;
}
};
const downloadBlob = (blob: Blob, filename: string) => {
const url = window.URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.URL.revokeObjectURL(url);
};
const handleExport = async () => {
exportLoading.value = true;
try {
const response = await exportRuleConfig(query.value as Partial<RuleConfigRequest>);
const filename = extractFilename(response.headers['content-disposition']);
downloadBlob(response.data, filename);
message.success('导出成功');
} catch (err: any) {
message.error(err?.message ?? '导出失败');
} finally {
exportLoading.value = false;
}
};
const handleImportTrigger = () => {
if (importInputRef.value) {
importInputRef.value.value = '';
importInputRef.value.click();
}
};
const handleImportChange = async (event: Event) => {
const input = event.target as HTMLInputElement | null;
const file = input?.files?.[0];
if (!file) {
return;
}
if (!/\.(xls|xlsx)$/i.test(file.name)) {
message.error('请选择 Excel 文件');
if (input) {
input.value = '';
}
return;
}
importLoading.value = true;
try {
const response = await importRuleConfig(file, true);
await load();
message.success(response.msg ?? '导入成功');
} catch (err: any) {
message.error(err?.message ?? '导入失败');
} finally {
importLoading.value = false;
if (input) {
input.value = '';
}
}
};
onMounted(() => {
const raw = sessionStorage.getItem(GRAPH_PANE_STORAGE_KEY);
if (raw) {
@@ -589,6 +726,13 @@ onMounted(() => {
graphPanePercent.value = clampGraphPercent(v);
}
}
const rightPanelRaw = sessionStorage.getItem(RIGHT_PANEL_WIDTH_STORAGE_KEY);
if (rightPanelRaw) {
const width = Number(rightPanelRaw);
if (!Number.isNaN(width)) {
rightPanelWidth.value = clampRightPanelWidth(width);
}
}
loadParamMeta().then(() => load());
});

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET
NS = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
W_P = "{%s}p" % NS["w"]
W_T = "{%s}t" % NS["w"]
def para_text(p):
parts = []
for t in p.iter(W_T):
if t.text:
parts.append(t.text)
if t.tail:
parts.append(t.tail)
return "".join(parts)
def dump(docx_name, out_txt):
root_dir = Path(__file__).resolve().parents[1]
z = zipfile.ZipFile(root_dir / docx_name)
root = ET.fromstring(z.read("word/document.xml"))
body = root.find("w:body", NS)
lines = []
idx = 0
for c in body:
if c.tag != W_P:
continue
t = para_text(c).replace("\n", " ").strip()
lines.append(f"{idx}\t{t[:200]}")
idx += 1
(root_dir / out_txt).write_text("\n".join(lines), encoding="utf-8")
print("wrote", idx, "paragraphs to", out_txt)
if __name__ == "__main__":
dump("xxx控制子系统软件需求规格说明书.docx", "_srs_paragraphs_index.txt")
dump("xxx控制子系统软件设计说明书.docx", "_sdd_paragraphs_index.txt")

View File

@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""
以《规则库设计与管理子系统系统软件需求规格说明书》《规则库设计与管理子系统软件设计说明书》
为版式与行文风格底稿(不修改这两份参考文件),仅更新输出:
方案自动化生成-软件需求规格说明书.docx
方案自动化生成-软件设计说明书.docx
实现zip + lxml仅改 w:t 与顶层段落文字;保留块级结构与域。
生成后在 Word 中「更新域」刷新目录与页码。
"""
from pathlib import Path
from template_inplace_engine import collect_hf_paths, fill_docx_inplace, verify_body_structure_same
from template_inplace_mappings import (
SDD_PARAGRAPH_PREDICATES,
SDD_SUBSTRINGS,
SRS_PARAGRAPH_PREDICATES,
SRS_SUBSTRINGS,
)
ROOT = Path(__file__).resolve().parents[1]
# 参考底稿(只读,脚本不写入)
SRS_TEMPLATE = ROOT / "规则库设计与管理子系统系统软件需求规格说明书.docx"
SDD_TEMPLATE = ROOT / "规则库设计与管理子系统软件设计说明书.docx"
SRS_OUT = ROOT / "方案自动化生成-软件需求规格说明书.docx"
SDD_OUT = ROOT / "方案自动化生成-软件设计说明书.docx"
def main():
if not SRS_TEMPLATE.is_file():
raise FileNotFoundError(SRS_TEMPLATE)
if not SDD_TEMPLATE.is_file():
raise FileNotFoundError(SDD_TEMPLATE)
hf_srs = collect_hf_paths(SRS_TEMPLATE)
hf_sdd = collect_hf_paths(SDD_TEMPLATE)
fill_docx_inplace(SRS_TEMPLATE, SRS_OUT, SRS_SUBSTRINGS, SRS_PARAGRAPH_PREDICATES, hf_srs)
ok, msg = verify_body_structure_same(SRS_TEMPLATE, SRS_OUT)
if not ok:
raise RuntimeError("SRS body 结构不一致: " + msg)
print("SRS OK", SRS_OUT, msg)
fill_docx_inplace(SDD_TEMPLATE, SDD_OUT, SDD_SUBSTRINGS, SDD_PARAGRAPH_PREDICATES, hf_sdd)
ok2, msg2 = verify_body_structure_same(SDD_TEMPLATE, SDD_OUT)
if not ok2:
raise RuntimeError("SDD body 结构不一致: " + msg2)
print("SDD OK", SDD_OUT, msg2)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""
API 与建模器modeler调用分类A=建模器已调用B=服务端提供、建模器未调用。
供软件需求/软件设计正文引用,与 auto-solution-admin 及 modeler 源码一致。
"""
# 键: (方法, 相对 context-path 的路径), 值: A 或 B
# 注意:带 /api 前缀的与 Spring 中 @RequestMapping 一致;无 /api 的与根路径一致。
API_CLASSIFICATION = [
# --- 建模器 decision/api + user ---
("GET", "/getInfo", "A"),
("POST", "/login", "A"),
("POST", "/logout", "A"),
("GET", "/api/system/firerule/platforms/basic", "A"),
("GET", "/api/node/command/all", "A"),
# designer/api
("GET", "/api/system/nodetemplate/all", "A"),
("GET", "/api/system/templateparameterdef/list", "A"),
("GET", "/api/system/nodeconnection/list", "A"),
("GET", "/api/system/nodeparameter/list", "A"),
("GET", "/api/system/treenodeinstance/list", "A"),
("GET", "/api/system/behaviortree/list", "A"),
("GET", "/api/system/behaviortree/platform/{id}", "A"),
("GET", "/api/system/behaviortree/underling/{platformId}", "A"),
("GET", "/api/system/behaviortree/{id}", "A"),
("DELETE", "/api/system/behaviortree/{id}", "A"),
("POST", "/api/system/behaviortree", "A"),
("POST", "/api/system/behaviortree/copy", "A"),
("PUT", "/api/system/behaviortree", "A"),
# communication/api
("GET", "/api/system/scene/list", "A"),
("GET", "/api/system/scene/{id}", "A"),
("DELETE", "/api/system/behaviortree/{id}", "A 注:当前前端实现用于删除时调用此路径,与场景删除语义需与业务确认"),
("GET", "/api/system/firerule/platforms/{id}", "A"),
("GET", "/api/system/scene/getAllRelation/{id}", "A"),
("POST", "/api/system/scene/saveSceneConfig", "A"),
("GET", "/api/system/scene/getAllTree/{sceneId}", "A"),
("PUT", "/api/system/behaviortree/behaviortreeId", "A"),
# rule/api
("GET/POST/PUT/DELETE", "/api/system/rule 根路径(非 config", "需取消 RuleController 块注释后生效;当前未启用"),
("GET/POST/PUT/DELETE", "/api/system/rule/config/…", "A 当前已启用"),
("GET", "/api/system/firerule/platforms", "A"),
# rule-config/api
("GET", "/api/system/rule/config/list", "A"),
("GET", "/api/system/rule/config/{ruleCode}", "A"),
("POST", "/api/system/rule/config", "A"),
("PUT", "/api/system/rule/config", "A"),
("DELETE", "/api/system/rule/config/{ruleCode}", "A"),
("GET", "/api/system/rule/config/dict/{dictType}", "A"),
("GET", "/api/system/rule/config/param-meta", "A"),
("GET", "/api/system/rule/config/graph", "A"),
("GET", "/api/system/rule/config/graph/four-blocks", "A"),
# algorithm/api
("GET", "/api/algo/algorithm/list", "A"),
("POST", "/api/algo/algorithm", "A"),
("POST", "/api/algo/algorithm/run", "A"),
("PUT", "/api/algo/algorithm", "A"),
("DELETE", "/api/algo/algorithm/{id}", "A"),
]
# B 类:仅列举主要分组,与代码中存在的 Controller 一致
B_ONLY_PREFIXES = [
("POST", "/captchaImage", "验证码(建模器未直接调用,可与登录流程配合)"),
("*", "/system/user**", "用户管理(系统管理子模块,管理端/第三方工具可调用)"),
("*", "/system/role**", "角色管理"),
("*", "/system/menu**", "菜单管理"),
("*", "/system/dept**", "部门管理"),
("*", "/system/post**", "岗位管理"),
("*", "/system/dict/**", "字典管理"),
("*", "/system/config**", "参数配置"),
("*", "/system/notice**", "通知公告"),
("*", "/system/user/profile**", "个人中心"),
("*", "/monitor/online**", "在线用户"),
("*", "/monitor/logininfor**", "登录日志"),
("*", "/monitor/operlog**", "操作日志"),
("*", "/monitor/server**", "服务监控"),
("*", "/monitor/cache**", "缓存监控"),
("*", "/monitor/job**", "定时任务quartz 子模块)"),
("*", "/monitor/jobLog**", "任务日志"),
("*", "/common/**", "通用上传下载CommonController"),
("*", "/tool/gen**", "代码生成generator 子模块)"),
("GET", "/register**", "注册(若开放)"),
("*", "/test/user**", "测试接口TestController"),
("POST", "/api/system/firerule/start", "火力规则执行匹配B"),
("GET", "/api/system/firerule/weapon", "武器平台与组件名B"),
("GET", "/api/system/firerule/comm**", "通信相关查询B"),
("GET", "/api/system/behaviortree/commander/{scenarioId}", "按场景查指挥关系树B"),
]

470
scripts/spec_sdd_builder.py Normal file
View File

@@ -0,0 +1,470 @@
# -*- coding: utf-8 -*-
"""
【已废弃】曾配合 clear_body 生成软设,会破坏 xxx 模板版式。
请改用python build_spec_documents.py
"""
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from spec_srs_builder import _add_table
def _h(doc, text, level=1):
doc.add_heading(text, level=level)
def _p(doc, text, bold=False):
p = doc.add_paragraph()
r = p.add_run(text)
r.bold = bold
return p
def _long_para(doc, *parts):
for part in parts:
doc.add_paragraph(part)
def build_sdd_document(doc):
"""向已清空正文的 Document 写入软件设计说明。"""
t = doc.add_paragraph("方案自动化生成系统")
t.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph("软件设计说明").alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph("")
_h(doc, "1 CSCI 体系结构设计", 1)
_h(doc, "1.1 系统 CSCI 层次", 2)
_long_para(
doc,
"本系统为单进程多模块 Maven 工程,版本 3.9.1根描述名「方案自动化生成」。CSCI 在运行期体现为:"
"一个 Spring Boot 可执行 JAR管理端服务子系统模块名对应 auto-solution-admin作为 HTTP 服务入口,"
"在类路径上装配核心框架子模块、系统管理子模块、行为树子模块、场景子模块、规则子模块、算法子模块、"
"定时任务子模块、代码生成子模块与通用基础子模块。持久化使用关系型数据库(以 Druid + MySQL 为典型部署),"
"缓存与会话可依赖 Redis。建模器为独立 Vite+Vue 前端,开发态通过 /api 代理至后端 1777 端口;"
"与后端属于浏览器—服务器架构。",
)
_h(doc, "1.1.1 主要 CSCI/模块划分", 2)
_add_table(
doc,
["名称(中文)", "工程目录/构件", "职责概述"],
[
["管理端服务子系统", "auto-solution-admin", "提供 REST 控制器、应用主类 SolutionApplication、Swagger 等"],
["核心框架子模块", "auto-solution-framework", "安全、Token、MyBatis/数据源/Redis/切面/异常"],
["通用基础子模块", "auto-solution-common", "实体/工具/统一返回/异常体系"],
["系统管理子模块", "auto-solution-system", "组织用户、角色菜单、系统参数、日志等"],
["行为树业务子模块", "auto-solution-behaviour", "行为树、节点、边、参数、命令字典"],
["仿真场景子模块", "auto-solution-scene", "场景、平台通信、与行为树/规则联动"],
["规则与火力业务子模块", "auto-solution-rule", "Drools、规则配置、火力元数据、执行服务"],
["算法脚本子模块", "auto-solution-algo", "算法、参数、与外部脚本执行"],
["定时任务子模块", "auto-solution-quartz", "Quartz 任务与执行日志"],
["代码生成子模块", "auto-solution-generator", "库表扫描与代码生成"],
],
)
_h(doc, "1.1.2 执行方案与部署视图", 2)
_long_para(
doc,
"逻辑执行路径(典型):浏览器加载建模器静态资源→用户 /login 获取 Token→在 axios 请求头中附带 Token→"
"调用 /api/... 业务接口→经 JwtAuthenticationTokenFilter 与 SecurityFilterChain 鉴权后进入各 Controller→"
"由 Service/Mapper 访问 MySQL/Redis。规则计算在规则与火力子模块中实现可调用资源目录下 JSON/规则 DRL 等。",
"物理部署上:可单机部署于 Windows/Linux后端监听端口在 application.yml 中配置,默认 1777"
"Redis 默认 127.0.0.1:6379业务库为 MyBatis 映射的库表behaviortree、scene/afsim 相关、rule、algorithm、sys_* 等)。"
"与软需中 A/B 类能力划分一致,不在此重复追踪表。",
)
try:
doc.add_paragraph("图1 方案自动化生成系统执行方案与模块依赖与软需图1 含义一致,可自绘为逻辑框图)", style="Caption")
except Exception:
doc.add_paragraph("图1 方案自动化生成系统执行方案与模块依赖。")
_h(doc, "1.2 与外部/内部接口", 2)
_long_para(
doc,
"对建模器:外部接口即浏览器通过 HTTP/HTTPS 访问的 REST 集合,建模器在 src/views/decision 下以 HttpRequestClient 组织;"
"以 /api 为业务前缀的接口与 Spring @RequestMapping(\"/api/...\") 对齐。",
"对操作系统:通过 ProcessBuilder/Runtime 在 AlgorithmController 中可调用本机 Python 可执行文件(以代码中配置路径为准,当前实现含固定盘符时须部署环境保持可用)。",
"内部接口为 Spring 的 Service 与 Mapper 依赖注入关系;规则子模块为场景、火力接口提供 findPlatformComponents、execute 等能力。",
)
_h(doc, "1.2.1 接口控制说明(方自-设计-EIF-01", 2)
_add_table(
doc,
["接口标识", "方向", "说明", ""],
[
["EIF-WEB-01", "外部→本系统", "建模器/浏览器 REST 调用", "A 为主、B 为管理扩展"],
["EIF-DB-01", "本系统↔库", "MyBatis 访问表", "全类"],
["EIF-REDIS-01", "本系统↔缓存", "Token 等", "A/B"],
["EIF-OS-01", "本系统→OS", "可选子进程", "A算法 run"],
],
)
# --- 分模块详设(拉长篇幅)---
_h(doc, "2 子系统/模块详细设计", 1)
def module_block(name, key, text_blocks, table_rows):
_h(doc, f"2.{key} {name}", 2)
for blk in text_blocks:
_long_para(doc, blk)
if table_rows:
_add_table(
doc,
["方自-设计-要素", "说明"],
table_rows,
)
module_block(
"管理端服务子系统(方自-设计-0200",
1,
[
"管理端子系统承载所有对外 Controller包路径 com.solution.web.controller下分 system、behaviour、scene、rule、algo、monitor、common 等。",
"统一基类为 com.solution.common.core.controller.BaseController成功与失败返回与 AjaxResult、TableDataInfo 一致;权限注解以 @PreAuthorize 为主(以实际代码为准)。",
"Swagger 通过 auto-solution-admin 的 SwaggerConfig 与 springfox-boot-starter 暴露,便于 B 类接口的联调。",
"应用主类 SolutionApplication 以 @SpringBootApplication 启动并排除 DataSource 自动配置,配合自定义 Druid/MyBatis 配置。",
],
[
["主类", "com.solution.SolutionApplication"],
["静态资源/跨域", "com.solution.framework.config.ResourcesConfig 等"],
],
)
module_block(
"核心框架子模块(方自-设计-0210",
2,
[
"该模块聚合横切与基础设施com.solution.framework.config 包内 SecurityConfig 定义 JWT 与登录退出链Druid 数据源与 druid 监控页;"
"Redis 序列化、MyBatis 映射扫描、I18n、Kaptcha 等。",
"com.solution.framework.web.service 提供 SysLoginService、TokenService、UserDetailsService与登录控制器协作。",
"com.solution.framework.security 提供 JwtAuthenticationTokenFilter、各 AuthenticationEntryPoint/Logout 扩展。",
"日志与防重复等通过 aspectj 包中 LogAspect、RepeatSubmitInterceptor 等实现(以实际类为准)。",
],
[
["安全过滤", "JwtAuthenticationTokenFilter"],
["密码服务", "SysPasswordService"],
],
)
module_block(
"系统管理子模块与通用基础子模块(方自-设计-0211",
3,
[
"系统管理子模块为典型组织权限模型,对应 sys_user、sys_role、sys_menu、sys_dept 等表与 ISys*Service 实现。",
"该部分接口路径多为 /system/...,在建模器侧除登录/会话外多数未用,设计为 B 类能力,供后台管理或未来接入。",
"通用基础子模块提供核心实体、分页、Excel、异常与注解为所有子模块所依赖。",
],
[
["用户服务", "SysUserServiceImpl 等"],
["字典服务", "SysDictType/ Data"],
],
)
module_block(
"行为树业务子模块(方自-设计-0212",
4,
[
"领域对象包括行为树、节点实例、边(父子顺序)、参数值、节点模板、模板参数定义、节点命令字典等;"
"Mapper 位于 auto-solution-behaviour 的 resources/mapper/system表名如 behaviortree、treenodeinstance、nodeconnection、nodeparameter、nodetemplate、templateparameterdef。",
"IBehaviortreeService 实现复制、按平台/场景查询、更新平台 id 等Treenodeinstance 支持 list/saveOrUpdate/批量。",
"与建模器设计器、通信视图的 A 类接口一一对应,见接口汇总表。",
],
[
["树表", "behaviortree"],
["节点实例", "treenodeinstance"],
],
)
module_block(
"仿真场景子模块(方自-设计-0213",
5,
[
"SceneService 提供 insert、update、saveOrUpdate、selectSceneList、findOneById、getAllTree、getAllRelation"
"控制层 SceneController 映射在 /api/system/scene 下。",
"AfsimScenario 等域对象承载场景 JSON/通信图等,与 modeler 侧 Scenario 类型对齐。",
"与规则子模块共同支撑「场景 id 下平台+组件」的火力/通信视角。",
],
[
["场景", "AfsimScenario/Form"],
],
)
module_block(
"规则与火力业务子模块(方自-设计-0214",
6,
[
"规则子模块包含 RuleMapper、RuleDrl 同步、规则配置、火力输入输出 DTO、Drools 工作内存装配等。",
"RuleController 中 **规则主数据** 的 list/get/post/put/delete 映射在源码中为块注释,当前仅 **规则聚合config** 相关 REST 生效;"
"规则配置包括 /config/list、/config/graph、/config/graph/four-blocks、/config/{ruleCode}、/config 的 POST/PUT、/config/{ruleCodes} DELETE、/config/dict、/config/param-meta。",
"FireRuleController 对规则执行、武器/通信元数据、平台-组件、component 子查询及 POST /rule 等暴露;与建模器对接情况见接口表 A/B。",
"资源目录下 JSON 规则样例为工程内可交付资产(如 auto-solution-rule/src/main/resources/json/)。",
],
[
["规则主表", "rule"],
["配置", "规则 config 与 DB/JSON 协同,以实现为准"],
],
)
module_block(
"算法脚本子模块(方自-设计-0215",
7,
[
"Algorithm 实体、AlgorithmParam 与 typeHandler 对 algo_config 进行映射IAlgorithmService 与 AlgorithmController 提供 /api/algo/algorithm 的 CRUD、export、GET id、/run。",
"/run 中通过 Process 调用 Python 并回写输出,部署时需保证可执行文件路径、权限与参数 JSON 与前端一致。",
],
[
["主表", "algorithm"],
],
)
module_block(
"定时任务子模块、代码生成子模块(方自-设计-0216",
8,
[
"定时任务子模块在 /monitor/job、/monitor/jobLog 暴露任务与日志 CRUD/导出,与 Quartz 调度器协同;配置见 ScheduleConfig。",
"代码生成子模块在 /tool/gen 下提供表、列、预览、下载生成代码;为 B 类能力。",
],
[
["Quartz 域", "SysJob、SysJobLog"],
],
)
doc.add_page_break()
_h(doc, "2.9 接口设计汇总(摘录)", 1)
_long_para(
doc,
"下表以 Controller 映射为主键整理A/B 含义同软需;{id} 等为路径参数。",
"完整与最新列表可在运行态通过 Swagger 与各 *Controller 源码核对。",
)
iface = [
["管理端", "POST", "/login", "登录", "A"],
["管理端", "GET", "/getInfo", "当前用户", "A"],
["管理端", "POST", "/logout", "登出", "A"],
["验证码", "GET", "/captchaImage", "验证码", "B"],
["系统管理", "GET/POST/...", "/system/user/...", "用户", "B"],
["系统管理", "*", "/system/role/...", "角色", "B"],
["系统管理", "*", "/system/menu/...", "菜单", "B"],
["系统管理", "*", "/system/dept/...", "部门", "B"],
["系统管理", "*", "/system/dict/...", "字典", "B"],
["系统管理", "*", "/system/config/...", "参数", "B"],
["系统管理", "*", "/system/notice/...", "通知", "B"],
["监控", "*", "/monitor/online/...", "在线", "B"],
["监控", "*", "/monitor/operlog/...", "操作日志", "B"],
["监控", "*", "/monitor/logininfor/...", "登录日志", "B"],
["监控", "*", "/monitor/server", "服务信息", "B"],
["监控", "*", "/monitor/cache/...", "缓存", "B"],
["任务", "*", "/monitor/job/...", "任务", "B"],
["任务", "*", "/monitor/jobLog/...", "任务日志", "B"],
["公共", "*", "/common/...", "上传下载", "B"],
["代码生成", "*", "/tool/gen/...", "生成", "B"],
["行为树", "GET/POST/PUT/DELETE", "/api/system/behaviortree/...", "", "A"],
["行为树", "GET/POST/PUT/DELETE", "/api/system/treenodeinstance/...", "树节点实例", "A+CRUD+saveOrUpdate"],
["行为树", "*", "/api/system/nodeconnection/...", "", "A"],
["行为树", "*", "/api/system/nodeparameter/...", "参数", "A"],
["行为树", "*", "/api/system/nodetemplate/...", "模板", "A"],
["行为树", "*", "/api/system/templateparameterdef/...", "模板参数", "A"],
["行为树", "GET", "/api/node/command/all", "节点命令", "A"],
["场景", "GET/POST", "/api/system/scene/...", "场景", "A"],
["规则", "", "/api/system/rule 根路径 CRUD", "RuleController 中块注释,当前未启用", ""],
["规则", "GET/POST/PUT/DELETE", "/api/system/rule/config/...", "规则聚合(配置)", "A 已启用"],
["火力", "GET/POST", "/api/system/firerule/...", "元数据/执行", "A+B 混合"],
["算法", "GET/POST/PUT/DELETE/POST", "/api/algo/algorithm/...", "算法", "A"],
]
_add_table(
doc,
["子域", "HTTP", "路径特征", "说明", "A/B"],
iface,
)
# 大表2分路径明细再占篇幅
detail = [
(m, pth, a) for m, pth, a in [
("行为树", "/api/system/behaviortree/list", "A"),
("行为树", "/api/system/behaviortree/{id}", "A"),
("行为树", "/api/system/behaviortree", "A POST/PUT"),
("行为树", "/api/system/behaviortree/copy", "A"),
("行为树", "/api/system/behaviortree/platform/{id}", "A"),
("行为树", "/api/system/behaviortree/underling/{platformId}", "A"),
("行为树", "/api/system/behaviortree/commander/{scenarioId}", "B"),
("行为树", "/api/system/behaviortree/behaviortreeId", "A"),
("场景", "/api/system/scene/list", "A"),
("场景", "/api/system/scene/saveSceneConfig", "A"),
("规则", "/api/system/rule/config/graph/four-blocks", "A"),
("火力", "/api/system/firerule/start", "B"),
("火力", "/api/system/firerule/rule", "B"),
]
]
doc.add_paragraph("表2 关键路径补充说明(方自-设计-API-02")
_add_table(
doc,
["子域", "相对路径", "A/B 备注"],
[[a[0], a[1], a[2]] for a in detail],
)
_h(doc, "2.9.1 管理端主要 HTTP 端点细目(方自-设计-API-03按 Controller 归纳)", 2)
_long_para(
doc,
"下列列表用于设计评审时与 Swagger 及源码双核;**规则主表**根路径行标注「未启用」以与 RuleController 块注释状态一致。",
)
fine = [
["POST", "/login", "用户登录,返回 Token 等", "A"],
["GET", "/getInfo", "当前登录用户/权限/角色", "A"],
["GET", "/getRouters", "侧栏路由,若启用", "B 典型为管理端"],
["POST", "/logout", "登出", "A"],
["GET", "/captchaImage", "数学/字符验证码", "B"],
["POST", "/register", "若开放注册", "B"],
["GET", "/system/user/list", "用户列表", "B"],
["GET/POST/PUT/DELETE", "/system/user/**", "用户 CRUD 等", "B"],
["GET/POST/PUT/DELETE", "/system/role/**", "角色", "B"],
["GET/POST/PUT/DELETE", "/system/menu/**", "菜单", "B"],
["GET/POST/PUT/DELETE", "/system/dept/**", "部门", "B"],
["GET/POST/PUT/DELETE", "/system/dict/**", "字典", "B"],
["GET/POST/PUT/DELETE", "/system/config/**", "系统参数", "B"],
["GET/POST/PUT/DELETE", "/system/notice/**", "通知", "B"],
["GET/PUT/POST", "/system/user/profile**", "个人中心", "B"],
["GET/POST/DELETE", "/monitor/operlog**", "操作日志", "B"],
["GET/POST/DELETE", "/monitor/logininfor**", "登录日志", "B"],
["GET/DELETE", "/monitor/online**", "在线用户", "B"],
["GET", "/monitor/server", "服务器信息", "B"],
["GET/DELETE", "/monitor/cache**", "缓存", "B"],
["GET/POST/PUT/DELETE", "/monitor/job**", "Quartz 任务", "B"],
["GET/POST/DELETE", "/monitor/jobLog**", "任务执行日志", "B"],
["GET/POST", "/common/**", "上传/下载/资源", "B"],
["GET/POST/PUT/DELETE", "/test/user/**", "TestController 调试", "B"],
["GET/POST/PUT/DELETE", "/api/system/behaviortree**", "行为树全族", "A建模器用"],
["GET/POST/PUT/DELETE", "/api/system/treenodeinstance**", "节点实例", "A"],
["GET/POST/PUT/DELETE", "/api/system/nodeconnection**", "连接", "A"],
["GET/POST/PUT/DELETE", "/api/system/nodeparameter**", "参数", "A"],
["GET/POST/PUT/DELETE", "/api/system/nodetemplate**", "模板", "A"],
["GET/POST/PUT/DELETE", "/api/system/templateparameterdef**", "模板参数", "A"],
["GET", "/api/node/command/all", "Hb 节点命令", "A"],
["GET/POST", "/api/system/scene**", "场景", "A"],
["GET/POST/…", "/api/system/rule 根下 list、POST、PUT、DELETE", "RuleController 块注释,未启用", ""],
["GET/POST/PUT/DELETE", "/api/system/rule/config**", "规则聚合与图谱", "A 已启用"],
["GET/POST", "/api/system/firerule/**", "火力/平台/执行", "A+B 视路径"],
["GET/POST/PUT/DELETE+run", "/api/algo/algorithm**", "算法", "A"],
["GET/POST/PUT/DELETE", "/tool/gen**", "代码生成", "B"],
]
_add_table(doc, ["方法族", "路径/说明", "含义", ""], fine)
doc.add_paragraph("表3 行为树/场景/火力部分路径展开(方自-设计-API-04")
fine2 = [
["GET", "/api/system/behaviortree/list", "分页/条件查询", "A"],
["GET", "/api/system/behaviortree/{id}", "单条", "A"],
["POST", "/api/system/behaviortree", "更新(实例)", "A"],
["PUT", "/api/system/behaviortree", "新增/更新", "A"],
["DELETE", "/api/system/behaviortree/{ids}", "删除", "A"],
["POST", "/api/system/behaviortree/copy", "复制", "A"],
["GET", "/api/system/behaviortree/platform/{id}", "按平台", "A"],
["GET", "/api/system/behaviortree/underling/{platformId}", "下属平台", "A"],
["GET", "/api/system/behaviortree/commander/{scenarioId}", "指挥关系", "B"],
["PUT", "/api/system/behaviortree/behaviortreeId", "更新树 id", "A"],
["GET", "/api/system/scene/list", "场景列表", "A"],
["GET", "/api/system/scene/{id}", "场景详情", "A"],
["POST", "/api/system/scene/saveSceneConfig", "保存/更新", "A"],
["GET", "/api/system/scene/getAllTree/{id}", "场景下所有树", "A"],
["GET", "/api/system/scene/getAllRelation/{id}", "平台通信关系", "A"],
["GET", "/api/system/firerule/platforms", "全量平台+组件", "A"],
["GET", "/api/system/firerule/platforms/{scenarioId}", "按场景", "A"],
["GET", "/api/system/firerule/platforms/basic", "基础平台列表", "A"],
["GET", "/api/system/firerule/component/{platformId}", "按平台组件", "A"],
["POST", "/api/system/firerule/start", "执行规则匹配", "B"],
["GET", "/api/system/firerule/weapon", "武器", "B"],
["GET", "/api/system/firerule/comm", "通信", "B"],
["POST", "/api/system/firerule/rule", "任务型规则", "B"],
]
_add_table(doc, ["方法", "完整路径", "说明", "A/B"], fine2)
_h(doc, "2.10 数据与持久化设计要点", 1)
_long_para(
doc,
"库表以 MyBatis XML 与实体为准:行为树与节点相关、场景与通信关系、规则/算法/系统表分离在不同子模块的 mapper 目录。",
"重要表摘录behaviortree、platform 相关、afsims 场景、rule 与 config、algorithm/algorithm_param、quartz 任务表、"
"sys_* 组织权限表、gen_table 等。建表与索引策略遵循各自 Mapper 与实体字段。",
"规则资源 JSON 位于规则子模块 resources/json/,在部署时随 JAR 打包。",
)
_h(doc, "2.11 安全、配置与与建模器联调", 1)
_long_para(
doc,
"认证:无 Token 的登录与验证码、静态资源等按 SecurityConfig 放行;业务接口需已认证。",
"服务端口application.yml 中 server.port=1777servlet.context-path 为 /。",
"Redisspring.redis 中 host、port、password 与项目实际一致。",
"文件上传solution.profile 等定义上传根路径。",
"建模器Vite 开发态 proxy 将 /api、/login、/getInfo 转发到 127.0.0.1:1777与本文一致"
"生产可改为同域 Nginx 反向代理。",
)
# 再补充若干长段落以充实软设(论文式展开)
for k in range(1, 9):
_h(doc, f"2.12.{k} 设计说明补充(方自-设计-EXT-0{k}", 2)
_long_para(
doc,
f"本小节对 2.1—2.8 中模块的接口粒度和错误处理作补充性说明。",
"异常时统一经 com.solution.framework.web.exception.GlobalExceptionHandler 等全局处理输出 AjaxResult 结构,前端以 code/msg 进行提示;"
"鉴权失败等由 Security 与入口配置决定;",
"业务上行为树、场景、规则、算法在事务边界上以各 Service/Mapper 实现为准。",
)
_h(doc, "2.13 典型用例文字说明(与建模器页面)", 1)
use_cases = [
(
"用例 UC-01建模器用户登录并拉取行为树列表",
"用户在建模器登录页输入账号口令→POST /login→获得令牌→设计器页 GET /api/system/behaviortree/list 分页查询→"
"将结果展示为卡片或列表。异常:口令错误、账户锁定、网络超时;前端应提示 code/msg。",
),
(
"用例 UC-02在场景下编辑通信并保存",
"用户从设计器带 scenario 进入通信页→GET /api/system/scene/{id} 与 getAllRelation→在画布上编辑关系→"
"saveSceneConfig 提交 JSON 字符串字段;若需平台详情则 GET /api/system/firerule/platforms/{scenarioId}",
),
(
"用例 UC-03配置规则聚合与查看知识图谱",
"用户打开规则配置管理页→/config/list 分页→/config/{ruleCode} 看详情→/config/graph 与 four-blocks 获取可视化数据→"
"新增/修改时 POST/PUT /config。权限由 @PreAuthorize 与 system:rule:* 权限字控制。",
),
(
"用例 UC-04运行算法脚本",
"用户维护 algorithm 行→POST /run 将 Algorithm 实例序列化 JSON 作为子进程标准输入,读取输出写回展示;"
"部署时须校验 codePath 指向的 Python/脚本存在。",
),
(
"用例 UC-05管理员操作定时任务/代码生成B 类)",
"通过 /monitor/job* 与 /tool/gen* 在具备权限时维护任务与生成物;不经过建模器菜单,属于运维/研发工具链。",
),
]
for title, body in use_cases:
_h(doc, title, 2)
for para in body.split(""):
p = para.strip()
if p:
doc.add_paragraph(p + "")
_h(doc, "2.14 附:工程目录与可维护性", 1)
_long_para(
doc,
"代码按 Maven 模块分目录auto-solution-* 与 modeler 前端解耦。后端单测位于各模块 `src/test`"
"若需扩展新 REST应在管理端子系统增 Controller 并视需要暴露 Swagger。",
"建议:规则主数据接口若与建模器火力规则页强依赖,应取消 RuleController 内块注释并同步权限、菜单与联调。",
)
_h(doc, "2.15 附:环境变量与端口一览", 1)
_add_table(
doc,
["", "默认/说明", "备注"],
[
["HTTP 端口", "1777application.yml server.port", "可改"],
["context-path", "/", "可改"],
["Redis", "127.0.0.1:6379密码以配置为准", "缓存/会话"],
["Modeler 开发端口", "8888Vite", "proxy 到 1777"],
["上传根路径", "solution.profile", "Windows 示例为盘符路径"],
],
)
doc.add_page_break()
_h(doc, "3 设计追溯", 1)
_p(
doc,
"本设计说明与《软件需求说明》使用同一批业务表述与方自-需求-xxx/方自-设计-xxx 标识在文字中对应,本版不另附正逆向追踪矩阵。",
)
_p(doc, "(完)")

215
scripts/spec_srs_builder.py Normal file
View File

@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
"""
【已废弃】曾用 python-docx 清空正文再写入,会破坏与 xxx 模板一致的版式与目录域。
请改用python build_spec_documents.py基于 OOXML 仅改 w:t保留块级结构
本文件保留仅为历史参考,不再被 build_spec_documents 引用。
"""
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def _set_cell_shade(cell, fill="D9D9D9"):
"""表头灰底(可选)"""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement("w:shd")
shd.set(qn("w:fill"), fill)
tcPr.append(shd)
def _add_table(doc, headers, rows):
t = doc.add_table(rows=1 + len(rows), cols=len(headers))
t.style = "Table Grid"
for j, h in enumerate(headers):
t.rows[0].cells[j].text = h
_set_cell_shade(t.rows[0].cells[j])
for i, row in enumerate(rows):
for j, val in enumerate(row):
t.rows[i + 1].cells[j].text = str(val)
doc.add_paragraph("")
def clear_body(doc):
body = doc._body._body
for child in list(body):
if "sectPr" in child.tag:
continue
body.remove(child)
def build_srs_document(doc):
"""向已清空正文的 Document 中写入软件需求(方案自动化生成)。"""
t = doc.add_paragraph("方案自动化生成系统")
t.alignment = WD_ALIGN_PARAGRAPH.CENTER
try:
t.runs[0].font.size = t.runs[0].font.size
except Exception:
pass
doc.add_paragraph("软件需求说明").alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph("")
doc.add_heading("1 方案自动化生成系统软件需求", level=1)
doc.add_heading("1.1 方案自动化生成系统CSCI能力需求", level=2)
intro = (
"方案自动化生成系统(软件版本 3.9.1Maven 工程名 auto-solution由管理端服务子系统、"
"核心框架子模块、通用基础子模块、系统管理子模块、行为树业务子模块、仿真场景子模块、"
"规则与火力业务子模块、算法脚本子模块、定时任务子模块、代码生成子模块等共同组成;"
"其中业务数据访问基于 MyBatis服务入口为 Spring Boot 应用,默认 HTTP 服务端口 1777以 application.yml 为准)。\n"
"系统能力分为两类A 类为「建模器modeler 目录)已调用的 HTTP 接口」所支撑的功能;"
"B 类为「管理端/扩展或运维向接口,当前在建模器中未作为页面功能接入」所支撑的能力,仍由同一后端提供。"
"本需求说明中凡涉及具体 URL 的,均与 com.solution.web.controller 及 modeler 下实际请求一致。"
)
doc.add_paragraph(intro)
doc.add_paragraph("系统逻辑范围示意如下(与软设中体系结构一致):")
doc.add_paragraph("图1 方案自动化生成系统软件组成及能力范围A/B 类划分)", style="Caption")
cap = (
"1行为树与节点建模提供行为树、节点实例、节点连接、节点参数、节点模板、模板参数定义等维护与查询"
"支撑建模器行为树设计页;对应 A 类接口 /api/system/behaviortree、/api/system/treenodeinstance 等。\n"
"2仿真场景与平台通信提供仿真场景Afsim 场景)维护、场景下行为树列表、平台间通信关系、"
"场景下带组件的平台信息查询;对应 A 类接口 /api/system/scene、/api/system/firerule/platforms 等。\n"
"3规则与火力提供火力类规则主数据及规则配置含图谱、四块图、字典与参数元数据"
"对应 A 类 /api/system/rule 及 /api/system/rule/config 等。\n"
"4算法脚本提供算法登记、运行外部 Python 进程(具体解释器路径以系统实现为准);"
"对应 A 类 /api/algo/algorithm。\n"
"5身份与系统管理提供登录、会话、菜单路由与完整后台用户/角色/部门/岗位/字典/通知/参数/监控等;"
"其中登录与 /getInfo 为 modeling 所依赖,其余管理项多为 B 类。"
)
doc.add_paragraph(cap)
doc.add_paragraph("方案自动化生成系统软件 CSCI 能力如表1 归纳。")
doc.add_paragraph("表1 方案自动化生成系统软件CSCI 能力表", style="Caption")
_add_table(
doc,
["子系统/模块(中文名)", "主要能力", "A/B 类", "说明"],
[
["管理端服务子系统", "统一发布 REST 接口、Swagger 文档、全局异常与权限控制", "A/B", "依赖核心框架、聚合各子模块"],
["核心框架子模块", "Spring Security、JWT、Redis、MyBatis、数据源、切面日志等", "B", "为全部接口提供横切能力"],
["系统管理子模块", "组织与用户、角色菜单、日志与在线用户等", "A登录/会话)+B", "建模器主要使用登录、取用户信息"],
["行为树业务子模块", "行为树、节点、连接、参数、命令字典等", "A", "与 designer 等页面深度对接"],
["仿真场景子模块", "场景 CRUD、场景-树、场景-通信关系", "A", "与 communication 场景页对接"],
["规则与火力业务子模块", "Drools/规则配置、火力规则、平台/组件元数据", "A+B", "A 为规则与配置B 为规则执行/武器等未接建模器接口"],
["算法脚本子模块", "算法配置、脚本路径、执行 Python", "A", "algorithm 管理页"],
["定时任务子模块", "Quartz 任务、任务日志", "B", "接口位于 /monitor/job、/monitor/jobLog"],
["代码生成子模块", "库表元数据、Velocity 代码生成", "B", "接口 /tool/gen"],
],
)
# 1.1.1
doc.add_heading("1.1.1 身份、会话与基础访问(方自-需求-0101", level=3)
doc.add_paragraph("功能描述")
doc.add_paragraph(
"为建模器提供用户登录、会话与权限所需的基础能力POST /login 提交账密;"
"GET /getInfo 在携带令牌时返回用户、角色、权限等POST /logout 登出(与 Spring Security 实现一致)。"
)
doc.add_paragraph("执行逻辑")
doc.add_paragraph("1功能输入\n用户名/密码、令牌(请求头中携带)。")
doc.add_paragraph("2功能输出\n令牌、用户基本信息、角色权限集合、路由/菜单类数据由后端服务返回。")
doc.add_paragraph("3主要步骤\n用户在前端输入凭证→后端认证→建立会话/签发令牌→后续 /api 与 /getInfo 携带令牌访问。")
_add_table(doc, ["序号", "数据项", "类型", "约束"], [["1", "用户登录表单", "JSON", "A 类"]])
# 1.1.2
doc.add_heading("1.1.2 行为树与节点全量数据访问(方自-需求-0102", level=3)
doc.add_paragraph("功能描述")
doc.add_paragraph(
"按树标识分页或条件查询行为树;按平台查询树;查询下属平台列表;新增、更新、删除、复制行为树;"
"查询节点实例、节点连接、节点参数、节点模板、模板参数定义。对应 GET/POST/PUT/DELETE 与 /api/system/behaviortree、"
"/api/system/treenodeinstance、/api/system/nodeconnection、/api/system/nodeparameter、/api/system/nodetemplate、"
"/api/system/templateparameterdef 等路径(以 Controller 映射为准,建模器以 designer/api 调用)。"
)
doc.add_paragraph("数据需求:行为树主键、平台标识、树名称、根实例信息、边表顺序、参数字段等,均来自各业务表,字段名与实体类/Mapper 一致;建模器以分页参数 pageNum、pageSize 拉取列表与明细。")
_add_table(doc, ["序号", "关键数据", "类型", "说明"], [
["1", "树主键 id", "Long", "A"],
["2", "树 platformId", "Number", "与场景、平台选择联动"],
])
# 1.1.3
doc.add_heading("1.1.3 仿真场景与平台通信关系(方自-需求-0103", level=3)
doc.add_paragraph("功能描述")
doc.add_paragraph(
"提供场景列表与详情、保存场景配置、查询某场景下全部行为树、某场景下平台通信关系、"
"为指定场景 id 拉取带组件的平台信息GET /api/system/firerule/platforms/{scenarioId}"
"并支持在保存场景时写入通信图 JSON 等(以 SceneService、Scene 实体与前端字段为准)。"
)
doc.add_paragraph(
"重要说明:当前 modeler 中“删除”操作实现为 DELETE 调用 /api/system/behaviortree/{id},与按场景删场景语义在字面上可能不一致,"
"本需求以当前代码与软设注记为准,由实施时统一修正或保留。"
)
_add_table(doc, ["序号", "路径", "HTTP 方法", "A/B"], [
["1", "/api/system/scene/list", "GET", "A"],
["2", "/api/system/scene/{id}", "GET", "A"],
["3", "/api/system/scene/saveSceneConfig", "POST", "A"],
["4", "/api/system/scene/getAllTree/{id}", "GET", "A"],
["5", "/api/system/scene/getAllRelation/{id}", "GET", "A"],
])
# 1.1.4
doc.add_heading("1.1.4 规则、火力与规则配置(方自-需求-0104", level=3)
doc.add_paragraph("功能描述")
doc.add_paragraph(
"规则聚合(规则配置)能力:分页列表、按 ruleCode 查询、知识图谱、四块图、字典、参数元数据、增删改规则聚合等。"
"对应 **当前已启用** 的后端路径为 /api/system/rule/config 及其子路径(见 RuleController 未注释部分)。"
)
doc.add_paragraph(
"重要说明火力规则「主表」CRUD 映射(如 GET /api/system/rule/list、POST/PUT /api/system/rule、DELETE /api/system/rule/{ids}"
"在 RuleController 源码中处于块注释内,**当前版本未作为有效接口发布**。建模器 rule/api 若仍请求上述路径,需以取消注释后的代码或分支为准;"
"本需求对「规则配置」类接口按已启用代码描述。"
)
doc.add_paragraph(
"B 类POST /api/system/firerule/start、GET /api/system/firerule/weapon、GET /api/system/firerule/comm、POST /api/system/firerule/rule、"
"以及平台列表等由 FireRuleController 提供;其中建模器已调用部分 platforms 路径,其余为扩展。"
)
# 1.1.5
doc.add_heading("1.1.5 算法与扩展运维(方自-需求-0105", level=3)
doc.add_paragraph("功能描述")
doc.add_paragraph(
"A 类:/api/algo/algorithm 的列表、导出、单条、增删改及 /run 运行脚本。"
"B 类:系统内用户/角色/菜单/部门/岗位/字典/参数/通知、在线、登录/操作日志、服务与缓存监控、"
"定时任务与任务日志、代码生成、文件上传/下载、开发用 Test 接口、验证码等。"
)
doc.add_heading("1.2 非功能需求(与实现一致)", level=2)
doc.add_paragraph("安全性")
doc.add_paragraph(
"采用 Spring Security 与 JWT 过滤器;敏感操作可结合 @PreAuthorize 与权限字;"
"密码错误次数与锁定时长由 application.yml 中 user.password 控制;"
"XSS/重复提交等可依赖 common 与 framework 中已有过滤器/切面(以实际启用类为准)。"
)
doc.add_paragraph("性能与可扩展性")
doc.add_paragraph(
"服务使用内嵌 Tomcat线程与连接数可在 application.yml 的 server.tomcat 中调优;"
"数据库连接池为 Druid详细见 application-druid.yml 与 druid 监控;"
"业务列表接口使用 PageHelper 分页,前端传 pageNum、pageSize。"
)
doc.add_paragraph("可维护性与运行环境")
doc.add_paragraph(
"运行需 JDK 8+(工程编译配置以根 pom 为准、MySQL 与 Redis 可连;"
"建模器为 Vite+Vue 独立工程,与后端分开发布;开发态通过代理对接后端 1777 端口。",
)
doc.add_paragraph("日志与审计")
doc.add_paragraph(
"可启用操作日志、登录日志等(对应 /monitor/operlog、/monitor/logininfor为 B 类能力;"
"与若依系实现一致。",
)
doc.add_heading("2 合格性规定", level=1)
doc.add_paragraph(
"以本系统源代码与可运行环境为准A 类需求以建模器联调能完整走通对应接口为合格;"
"B 类需求以 Swagger/接口测试可访问、服务启动无错误、与数据库和 Redis 配置一致为合格;"
"安全上须满足本系统所采用的 Spring Security、JWT 与配置项的约束。"
)
doc.add_heading("3 需求追溯", level=1)
doc.add_paragraph(
"本版本不展开《正向/逆向需求追溯表》细化填写;在工程上会采用需求标识(方自-需求-xxxx与软设中设计标识一一在文字中对应说明。"
)
doc.add_page_break()

View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""
在保留 word/document.xml 块级结构的前提下,仅修改 w:t / w:instrText 中的可见文本。
不删除 w:p、w:tbl、域、分节等用于与 xxx 控制子系统模板版式一致。
"""
from __future__ import annotations
import io
import zipfile
from pathlib import Path
from typing import Callable, Iterable, List, Optional, Tuple
from lxml import etree
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
NSMAP = {"w": W_NS}
W_P = f"{{{W_NS}}}p"
W_T = f"{{{W_NS}}}t"
def _apply_substrings(text: str, pairs: List[Tuple[str, str]]) -> str:
if not text:
return text
for old, new in pairs:
if old in text:
text = text.replace(old, new)
return text
def _paragraph_plain_text(p: etree._Element) -> str:
parts: List[str] = []
for wt in p.iter(W_T):
if wt.text:
parts.append(wt.text)
if wt.tail:
parts.append(wt.tail)
return "".join(parts)
def _set_paragraph_plain_text(p: etree._Element, new_text: str) -> None:
"""将段落内所有 w:t 合并为单一文本写入第一个 w:t其余 w:t 置空;不增删 w:r/w:p。"""
tels = list(p.iter(W_T))
if not tels:
return
for i, wt in enumerate(tels):
wt.text = new_text if i == 0 else ""
wt.tail = None
def replace_substrings_in_tree(root: etree._Element, pairs: List[Tuple[str, str]]) -> None:
"""整棵树(含表格内)仅 w:t 子串替换;不改 w:instrText避免破坏 TOC/PAGE 等域。"""
for wt in root.iter(W_T):
if wt.text:
wt.text = _apply_substrings(wt.text, pairs)
if wt.tail:
wt.tail = _apply_substrings(wt.tail, pairs)
def replace_direct_paragraphs_by_predicate(
body: etree._Element,
predicate_replace: List[Tuple[Callable[[str], bool], str]],
) -> None:
"""仅处理 body 下一层 w:p与 Word 目录页中段落结构一致;表格内段落不在此列)。"""
for child in body:
if child.tag != W_P:
continue
plain = _paragraph_plain_text(child).strip()
if not plain:
continue
for pred, new_text in predicate_replace:
if pred(plain):
_set_paragraph_plain_text(child, new_text)
break
def process_xml_bytes(xml_bytes: bytes, pairs: List[Tuple[str, str]], paragraph_rules: Optional[List[Tuple[Callable[[str], bool], str]]] = None) -> bytes:
"""先整段替换顶层段落(匹配模板原文),再做全局子串替换,避免子串抢先破坏匹配条件。"""
root = etree.fromstring(xml_bytes)
body = root.find(".//w:body", namespaces=NSMAP)
if body is None:
return xml_bytes
if paragraph_rules:
replace_direct_paragraphs_by_predicate(body, paragraph_rules)
replace_substrings_in_tree(root, pairs)
return etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)
def fill_docx_inplace(
template_path: Path,
output_path: Path,
pairs: List[Tuple[str, str]],
paragraph_rules: Optional[List[Tuple[Callable[[str], bool], str]]] = None,
extra_xml_paths: Optional[Iterable[str]] = None,
) -> None:
"""读取 template zip写出 output zip仅改写列出的 XML 部件,其余二进制原样拷贝。"""
paths = {"word/document.xml"}
if extra_xml_paths:
paths.update(extra_xml_paths)
buf = io.BytesIO()
with zipfile.ZipFile(template_path, "r") as zin, zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename in paths:
pr = paragraph_rules if item.filename == "word/document.xml" else None
data = process_xml_bytes(data, pairs, pr)
zout.writestr(item, data)
output_path.write_bytes(buf.getvalue())
def collect_hf_paths(template_path: Path) -> List[str]:
"""收集可能含占位文字的页眉页脚 XML 路径。"""
with zipfile.ZipFile(template_path, "r") as z:
names = z.namelist()
out = []
for n in names:
if n.startswith("word/header") and n.endswith(".xml"):
out.append(n)
if n.startswith("word/footer") and n.endswith(".xml"):
out.append(n)
return out
def verify_body_structure_same(template_path: Path, output_path: Path) -> Tuple[bool, str]:
"""比对 template 与输出 document.xml 中 w:body 子元素标签序列是否一致。"""
def tag_seq(zpath: Path) -> List[str]:
with zipfile.ZipFile(zpath, "r") as z:
root = etree.fromstring(z.read("word/document.xml"))
body = root.find("w:body", namespaces=NSMAP)
return [c.tag for c in body]
sb = tag_seq(template_path)
sa = tag_seq(output_path)
ok = sb == sa
return ok, f"body_child_tags_equal={ok} len={len(sb)}"

View File

@@ -0,0 +1,240 @@
# -*- coding: utf-8 -*-
"""
以《规则库设计与管理子系统*.docx》为版式与行文风格底稿生成《方案自动化生成-*.docx》。
仅做 w:t 子串替换 + 顶层段落整段替换;不修改两份规则库参考文件本身。
段落谓词匹配 **原文**(子串替换之前)。
auto-solution-rule 子模块内部类设计、Drools 细节本文不展开;管理端 REST 仍按 Controller 描述。
"""
from __future__ import annotations
from typing import Callable, List, Tuple
# 全局子串(长串优先),作用于全文 w:t含表格
SRS_SUBSTRINGS: List[Tuple[str, str]] = [
("规则库设计与管理子系统项目合同", "方案自动化生成系统项目合同"),
("规则库设计与管理子系统软件", "方案自动化生成系统软件"),
("规则库设计与管理子系统", "方案自动化生成系统"),
("规则库设计与子系统", "方案自动化生成系统"),
("方案自动生成", "方案自动化生成"),
]
SDD_SUBSTRINGS: List[Tuple[str, str]] = [
("规则库设计与管理子系统项目合同", "方案自动化生成系统项目合同"),
("规则库设计与管理子系统软件", "方案自动化生成系统软件"),
("规则库设计与管理子系统", "方案自动化生成系统"),
("规则库设计与子系统", "方案自动化生成系统"),
("方案自动生成", "方案自动化生成"),
("规则库软件外部接口", "方案自动化生成软件外部接口"),
("规则系统控制交互接口", "方案自动化生成系统控制交互接口"),
]
SRS_PARAGRAPH_PREDICATES: List[Tuple[Callable[[str], bool], str]] = [
(
lambda s: s.startswith("规则库设计与管理子系统主要由"),
(
"方案自动化生成系统Maven 工程 auto-solution版本 3.9.1)由以下 Maven 子模块协同构成:"
"auto-solution-admin 为 Spring Boot 管理端入口com.solution.SolutionApplication集中注册对外 REST Controller"
"auto-solution-framework 提供安全认证Spring Security、JWT 过滤器链、Redis、MyBatis、Druid 数据源与全局异常、日志切面等横切能力;"
"auto-solution-common 提供 AjaxResult、TableDataInfo、分页、通用注解与工具类"
"auto-solution-system 实现若依风格的系统管理域(用户、角色、菜单、部门、岗位、字典类型/数据、参数配置、通知公告、注册、个人资料等);"
"auto-solution-behaviour 承载行为树与节点建模(行为树 CRUD/复制/导出、按平台与场景查询指挥关系、节点实例 saveOrUpdate、节点连线、节点参数、节点模板与模板参数定义、节点命令枚举等"
"auto-solution-scene 承载 AFSIM 场景与通信关系(场景列表/详情、保存场景 POST /api/system/scene/saveSceneConfig、场景下全部行为树与平台间关系查询"
"auto-solution-algo 承载算法脚本元数据与执行入口(/api/algo/algorithm 列表/导入导出/CRUD 及 /run 调用外部脚本);"
"auto-solution-quartz 提供定时任务与执行日志(/monitor/job、/monitor/jobLog"
"auto-solution-generator 提供基于 Velocity 的代码生成(/tool/gen"
"规则聚合配置、图谱、字典与参数元数据等 REST 由管理端 RuleController/api/system/rule/config/**)发布,火力与平台元数据由 FireRuleController/api/system/firerule/**)发布;"
"持久化、规则引擎与 Drools 协同细节在 auto-solution-rule 子模块内实现,本文不对该子模块作类级展开。"
"前端建模器 modeler 通过 axios 调用 /login、/getInfo、/logout 及 /api/** 业务接口;运维侧另具备 /system/**、/monitor/**、/common、/captchaImage 等管理台能力。"
),
),
(
lambda s: s.startswith("支持对规则聚合数据进行配置与维护"),
(
"用户与会话POST /login、POST /logout、GET /getInfo、GET /getRoutersSysLoginController验证码 GET /captchaImageCaptchaController"
"注册 POST /registerSysRegisterController"
"系统管理(/system/**):用户 SysUserController、角色 SysRoleController、菜单 SysMenuController、部门 SysDeptController、岗位 SysPostController、"
"字典类型 SysDictTypeController、字典数据 SysDictDataController、参数配置 SysConfigController、通知 SysNoticeController、个人中心 SysProfileController。"
"公共文件GET/POST /common/download、/common/upload、/common/uploadsCommonController"
),
),
(
lambda s: s.startswith("2多形态规则表达与可视化展示"),
(
"2行为树与节点建模行为树 GET/POST/PUT/DELETE /api/system/behaviortree/**(含 /list、/{id}、/copy、/export、按平台 /platform/{id}、从属 /underling/{platformId}、指挥员 /commander/{scenarioId}、更新树实例关联 PUT /behaviortreeId"
"节点实例 /api/system/treenodeinstance/list、/saveOrUpdate、导出等节点连线 /api/system/nodeconnection节点参数 /api/system/nodeparameter"
"节点模板 /api/system/nodetemplate含 /all、/listAll模板参数定义 /api/system/templateparameterdef节点命令枚举 GET /api/node/command/allHbNodeCommandController"
),
),
(
lambda s: s.startswith("3知识库源数据接入与全链路管理"),
(
"3仿真场景与通信场景 GET /api/system/scene/list、GET /api/system/scene/{id}、POST /api/system/scene/saveSceneConfig"
"场景下行为树 GET /api/system/scene/getAllTree/{id};场景下平台通信关系 GET /api/system/scene/getAllRelation/{id}"
"火力与平台侧元数据建模器通信用GET /api/system/firerule/platforms/{scenarioId}、/platforms、/platforms/basic、GET /api/system/firerule/component/{platformId},武器/通信字典 GET /weapon、GET /comm推演启动 POST /start规则投递 POST /rule以 FireRuleController 源码为准)。"
),
),
(
lambda s: s.startswith("4领域知识与规则的结构化提取功能需求"),
(
"4规则聚合与算法扩展规则聚合分页/图谱/四块图/详情与保存见 GET/POST/PUT/DELETE /api/system/rule/config/**"
"规则主表 /api/system/rule 的 list/{id}/POST/PUT/DELETE 映射在 RuleController 内为块注释,当前未启用。"
"算法脚本GET/POST/PUT/DELETE /api/algo/algorithm/** 与 POST /api/algo/algorithm/run。"
"auto-solution-rule 子模块内的领域对象、Mapper 与 Drools 规则加载不在本文展开。"
),
),
(
lambda s: s.startswith("5规则库运维与资产化管理"),
(
"5平台运维与扩展操作日志 /monitor/operlog、登录日志 /monitor/logininfor、在线用户 /monitor/online、服务监控 /monitor/server、缓存 /monitor/cache"
"定时任务 /monitor/job、任务日志 /monitor/jobLog代码生成 /tool/gen联调示例 /test/user/**。"
"上述为管理端已装配、建模器未必全部调用的 B 类能力,与 A 类建模接口共同构成完整运维面。"
),
),
(
lambda s: "规则库设计与子系统软件共分为" in s or s.startswith("规则库设计与管理子系统软件共分为"),
(
"方案自动化生成系统软件按业务域可划分为:用户与会话、系统管理、行为树与节点模板、仿真场景与通信、规则聚合配置与火力元数据、算法脚本、"
"定时任务与代码生成、监控与日志等;与表 1 及后续各小节功能需求叙述对应。"
),
),
(
lambda s: s.startswith("规则聚合详情查询功能为规则建模与配置编辑提供数据加载能力"),
(
"规则聚合详情查询功能为规则建模与配置编辑提供数据加载能力;接口 GET /api/system/rule/config/{ruleCode},返回规则聚合主数据及参数、适用任务类型等,供建模器 rule-config 回显。"
),
),
(
lambda s: s.startswith("规则聚合新增与修改保存功能负责将多维度规则建模结果持久化"),
(
"规则聚合新增与修改保存功能负责将多维度规则建模结果持久化;接口 POST /api/system/rule/config新增、PUT /api/system/rule/config修改请求体为 RuleConfig 结构。"
),
),
(
lambda s: s.startswith("规则知识图谱查询与展示功能在规则聚合列表相同筛选"),
(
"规则知识图谱查询与展示功能在规则聚合列表相同筛选与分页条件下生成图谱数据;接口 GET /api/system/rule/config/graph。"
),
),
(
lambda s: s.startswith("四块分区规则图谱查询与展示功能基于当前已启用的规则聚合数据"),
(
"四块分区规则图谱查询与展示功能基于当前已启用的规则聚合数据;接口 GET /api/system/rule/config/graph/four-blocks。"
),
),
(
lambda s: s.startswith("规则参数元数据查询功能提供参数键、显示名称"),
(
"规则参数元数据查询功能提供参数键、显示名称、取值类型及校验约束等;接口 GET /api/system/rule/config/param-meta。"
),
),
(
lambda s: s.startswith("规则字典数据查询功能按字典类型提供字典项列表查询"),
(
"规则字典数据查询功能按字典类型提供字典项列表查询;接口 GET /api/system/rule/config/dict/{dictType}"
),
),
(
lambda s: s.startswith("规则知识条目结构化提取功能用于将规则库中的规则主数据"),
(
"规则知识条目结构化提取功能用于将规则主数据与关联参数、任务类型等结构化分页返回;"
"当前以 GET /api/system/rule/config/list 为准TableDataInfo"
"说明GET /api/system/rule/list 等主表接口在 RuleController 中为块注释未启用。"
),
),
(
lambda s: s.startswith("规则关系结构化提取与展示功能用于将规则条目之间的层级关系"),
(
"规则关系结构化提取与展示功能用于将规则条目之间的关联关系提取为图谱结构;"
"与 GET /api/system/rule/config/graph 及 auto-solution-rule 中图谱构建服务一致,不在本文展开实现类。"
),
),
(
lambda s: s.startswith("规则聚合分页查询运维功能用于按条件分页查询规则聚合数据"),
(
"规则聚合分页查询运维功能用于按条件分页查询规则聚合数据;接口 GET /api/system/rule/config/list与建模器侧栏列表一致。"
),
),
(
lambda s: s.startswith("规则聚合删除运维功能用于按规则编码删除规则聚合数据"),
(
"规则聚合删除运维功能用于按规则编码删除规则聚合数据;接口 DELETE /api/system/rule/config/{ruleCodes}(路径变量以 Controller 定义为准)。"
),
),
(
lambda s: s.startswith("本文与规则库设计与管理子系统合同之间的正向追踪表"),
"本版本不对正向/逆向追踪表作展开填写;需求与软设章节在文字中对应说明即可。",
),
(
lambda s: s.startswith("本文与规则库设计与管理子系统项目合同之间的逆向追踪性见下表"),
"同上。",
),
]
SDD_PARAGRAPH_PREDICATES: List[Tuple[Callable[[str], bool], str]] = [
(
lambda s: s.startswith("系统包含的功能模块有多维度规则建模"),
(
"系统包含的功能模块有:用户登录与会话、系统管理(用户/角色/菜单/部门/岗位/字典/配置/通知/个人中心)、"
"行为树与节点模板建模、仿真场景与通信关系、规则聚合与火力元数据接口、算法脚本、定时任务、代码生成、监控缓存与日志等;"
"主要软件部件与 Maven 子模块 auto-solution-admin、framework、common、system、behaviour、scene、algo、quartz、generator 对应,规则域持久化在 solution-rule 子模块。"
),
),
(
lambda s: s.startswith("规则库设计与管理子系统主要由多维度规则建模"),
(
"方案自动化生成系统由各 Maven 子模块组成:"
"auto-solution-admin 为可执行 Jar 入口,扫描注册 com.solution.web.controller 下各业务 Controller"
"auto-solution-framework 装配 SecurityConfig、RedisConfig、MyBatis、数据源与跨域、重复提交等过滤器"
"auto-solution-system 提供 system 域 Service/Mapper用户、角色、菜单等"
"auto-solution-behaviour 提供行为树、节点实例、连线、参数、模板等域服务;"
"auto-solution-scene 提供场景与平台通信服务;"
"auto-solution-algo 提供算法实体与运行编排;"
"auto-solution-quartz、auto-solution-generator 分别以独立 Jar 被 admin 依赖并暴露 /monitor/job*、/tool/gen"
"规则配置与执行相关 Bean 与 Drools 资源由 auto-solution-rule 提供并由 admin 引入,本文不展开该子模块内部包结构。"
"建模器 modeler 与若依管理端共用同一后端进程时,分别消费 /api/** 与 /system/**、/monitor/** 等路径。"
),
),
(
lambda s: s.startswith("提供统一的规则聚合建模与配置管理能力"),
(
"提供统一的规则聚合建模与配置管理能力,并与行为树、场景、火力元数据、算法脚本等模块在同一管理端 Spring 容器中装配;"
"控制层按包划分com.solution.web.controller.system、behaviour、scene、rule、algo、common、monitor、tool 等;"
"统一响应 AjaxResult、分页 TableDataInfo。"
),
),
(
lambda s: s.startswith("规则库设计与管理子系统交互接口用于承接前端规则配置页面"),
(
"方案自动化生成系统交互接口用于承接建模器与管理端请求。"
"建模器侧典型路径:登录与用户信息;行为树与设计师 /api/system/behaviortree、/api/system/treenodeinstance、nodeconnection、nodeparameter、nodetemplate、templateparameterdef"
"场景与通信 /api/system/scene/**、/api/system/firerule/**;规则配置 /api/system/rule/config/**;算法 /api/algo/algorithm/**。"
"管理端另含 /system/**、/monitor/**、/tool/gen、/common/** 等,详见各 Controller 的 @RequestMapping。"
),
),
(
lambda s: s.startswith("规则库设计与管理子系统内部接口用于完成控制层、服务层"),
(
"方案自动化生成系统内部接口用于完成控制层、服务层、数据访问层之间的数据传递;"
"行为树、场景、算法各自模块内 Service 与 Mapper 交互;"
"规则聚合由 IRuleConfigService 等与 auto-solution-rule 内 Mapper、图谱 VO 构建协同;"
"定时任务由 ISysJobService 等与 Quartz 表交互;代码生成读取数据库元数据并填充 Velocity 模板。"
),
),
(
lambda s: s.startswith("本系统数据处理接口用于规则体现,承接任务数据"),
(
"本系统数据处理接口除规则与任务数据外,还承接行为树节点实例批量保存、场景 JSON 配置、算法运行参数、"
"定时任务调度参数、用户导入导出二进制流等;具体字段以各 Controller 的 Java Bean 与 MyBatis XML 为准。"
),
),
(
lambda s: s.startswith("本软件处理结果接口用于输出规则查询、维护及图谱展示结果"),
(
"本软件处理结果接口用于输出规则查询、维护及图谱展示结果,并输出行为树、场景、火力平台列表、算法执行结果、"
"系统管理分页数据、监控指标等,统一封装为 AjaxResult 或 TableDataInfo。"
),
),
]