提交 236b3f3b authored 作者: 陈泽健's avatar 陈泽健

feat(security): 安全测试 ERP 配置/上传/任务创建全链路对接

- 新增 ERP 配置管理页面(ErpConfig.vue),支持查看/修改 ERP 连接参数
- 新增安全报告 ERP 上传流程:支持上传到 ERP 测试单、项目资料、协作文档
- 新增安全报告 ERP 任务创建:预览+创建,含报告解析、紧急程度计算、关联需求搜索
- 前端 Reports.vue 安全报告弹窗新增 ERP 上传/创建任务按钮及对话框
- 新增 4 份需求文档与计划执行文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 ce2c75de
# 安全测试 ERP 任务创建 · 计划执行文档
> **文档状态**:初稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:安全测试
> **对应 PRD**:`_PRD_安全测试ERP任务创建_需求文档.md`
> **参考实现**:`ApiSecurityTest/utils/erp_client.py`、`task_creator.py`、`report_parser.py`
---
## 一、执行计划总览
### 阶段划分
| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|---------|--------|
| Phase 1 | 后端:ERPTaskClient(ERP 任务 API 客户端,复用 ErpService 模式) | 1h | `backend/app/services/erp_task_client.py` |
| Phase 2 | 后端:SecurityErpTaskService(报告解析 + 数据构建 + 任务创建) | 1.5h | `backend/app/services/security_erp_task_service.py` |
| Phase 3 | 后端:路由 + Schema + main.py 注册 | 0.5h | `backend/app/routers/security_erp_task.py` + `schemas/security_erp_task.py` |
| Phase 4 | 前端:API 封装 + Reports.vue 对话框 | 1h | `frontend/src/api/security.ts`(追加)+ `Reports.vue`(修改) |
| Phase 5 | 构建 + 联调验证 | 0.5h | `npm run build` 通过 + 后端路由验证 |
### 架构设计
```
前端 Reports.vue
│ POST/GET /api/security/executions/{id}/task-preview
│ POST/GET /api/security/executions/{id}/create-task
security_erp_task.py (路由,薄层)
security_erp_task_service.py (业务协调)
├── get_task_preview() — 解析报告 + 获取基础数据 + 搜索需求
└── create_task() — 构建任务数据 + 调用 ERP
erp_task_client.py (ERP API 客户端,含缓存/重试)
├── get_task_types() GET /openclaw/tasktype
├── get_task_statuses() GET /openclaw/taskstatus
├── get_staff_list() GET /openclaw/stuff
├── search_develop() GET /openclaw/develop?search_str=
└── create_task() POST /openclaw/task
```
### 参考实现对照
| 参考文件 | 参考内容 | 本次实现 |
|---------|---------|---------|
| `UtilsTool/ApiSecurityTest/utils/erp_client.py` | ERP 客户端(缓存 + 重试) | `erp_task_client.py` |
| `UtilsTool/ApiSecurityTest/utils/task_creator.py` | 任务创建协调器 | `security_erp_task_service.py` |
| `UtilsTool/ApiSecurityTest/utils/report_parser.py` | Markdown 报告解析 | 复用 `SecurityReportService.get_report_content()` + 正则解析 |
| `UtilsTool/ApiSecurityTest/config.yaml``erp` 节点 | 默认配置 | 硬编码常量 + 前端可覆盖 |
| `backend/app/routers/functional_report.py` 的 ERP 上传实现 | 路由/Schema 风格 | `security_erp_task.py` + `schemas/security_erp_task.py` |
---
## 二、Phase 1:ERP 任务客户端
### 2.1 新增文件
**文件**: `backend/app/services/erp_task_client.py`(新建)
```python
"""
模块名称:erp_task_client.py
模块描述:ERP 任务 API 客户端,封装任务类型/状态/人员/需求/任务创建接口
参考实现:ApiSecurityTest/utils/erp_client.py
"""
ERPTaskClient 类:
- base_url / api_key / timeout / max_retries / retry_interval
- _cache / _cache_timestamps1 小时缓存)
- get_task_types() GET /openclaw/tasktype
- get_task_statuses() GET /openclaw/taskstatus
- get_staff_list() GET /openclaw/stuff
- search_develop(search_str, page_num=1, page_size=10) GET /openclaw/develop
- create_task(task_data) POST /openclaw/task
- _make_request(method, endpoint, ...) 统一请求(重试 + 错误处理)
- _get_with_cache(key, fetch_func) 缓存获取
```
### 2.2 配置获取
复用现有 `ErpConfig` 模型 + `settings` 环境变量:
```python
async def get_erp_task_client(db) -> ERPTaskClient:
"""从数据库读 ERP 配置,回退环境变量,创建客户端"""
# 复用 /api/security/erp-config 相同的读取逻辑
...
```
### 2.3 关键说明
-`erp_service.py` 共用配置来源(`erp_configs` 表 + `settings`
- 缓存键:`task_types` / `task_statuses` / `staff_list`,缓存时长 3600s
- 请求头:`X-Api-Key`
- 错误处理:客户端错误(4xx)不重试,服务端错误(5xx)重试 3 次
---
## 三、Phase 2:任务创建服务
### 3.1 新增文件
**文件**: `backend/app/services/security_erp_task_service.py`(新建)
```python
"""
模块名称:security_erp_task_service.py
模块描述:安全测试 ERP 任务创建服务,解析报告、构建任务数据、创建 ERP 任务
参考实现:ApiSecurityTest/utils/task_creator.py + report_parser.py
"""
# 默认任务配置常量(对齐参考实现 config.yaml)
DEFAULT_TASK_CONFIG = {
"type_id": 3, # 后端开发
"status_id": 1, # 新建
"level": 4, # 常规处理
"excepthour": 21, # 期望工时
"user_id": 101, # 责任人 李立键
"createuser_id": 441, # 创建人 陈泽键
"deadline_days": 7, # 截止天数
"develop_search_str": "【新统一平台】公司内部安全测试扫描及修复验证",
}
class SecurityErpTaskService:
async def get_task_preview(execution_id, db) -> dict:
"""1. 生成报告 → 2. 解析摘要 → 3. 获取基础数据 → 4. 搜索需求 → 5. 返回预览"""
async def create_task(execution_id, db, task_config) -> dict:
"""1. 解析报告 → 2. 构建任务数据 → 3. 创建任务 → 4. 返回结果"""
```
### 3.2 报告解析(对齐 report_parser.py)
```python
def parse_report_summary(markdown: str) -> dict:
"""从 Markdown 报告提取摘要"""
summary = {target/server_ip/start_time/end_time/total/high/medium/low/info/safe}
# 正则匹配,格式见 PRD 2.2.1
```
### 3.3 紧急程度计算(对齐 report_parser.py)
```python
def calculate_urgency_level(summary) -> int:
high>0 5; medium>0 4; low>0 3; info>0 2; else 1
```
### 3.4 任务名称生成(对齐 report_parser.py)
```python
def generate_task_name(server_ip) -> str:
return f"{server_ip}接口安全测试及漏洞修复_{datetime.now():%Y%m%d}"
```
### 3.5 任务描述 HTML(对齐 report_parser.py)
```python
def generate_task_content(summary, high_risk_list) -> str:
"""生成 HTML 描述:测试概览/漏洞统计/高危漏洞/修复建议/紧急度说明"""
```
### 3.6 任务数据构建
```python
def build_task_data(summary, server_ip, high_risk_list, config) -> dict:
"""构建 POST /openclaw/task 的请求体"""
task_data = {
"name": generate_task_name(server_ip),
"type_id": config.type_id,
"status_id": config.status_id,
"level": config.level (或自动计算),
"excepthour": config.excepthour,
"excepttime": datetime.now() + timedelta(days=config.deadline_days),
"content": generate_task_content(...),
"user_id": config.user_id,
"createuser_id": config.createuser_id,
}
if config.develop_id:
task_data["develop_id"] = config.develop_id
```
---
## 四、Phase 3:路由 + Schema
### 4.1 新增 Schema
**文件**: `backend/app/schemas/security_erp_task.py`(新建)
```python
class TaskPreviewResponse(BaseModel):
summary: dict
task_name: str
calculated_level: int
task_types: list
task_statuses: list
staff_list: list
requirement: Optional[dict]
default_config: dict
class CreateTaskRequest(BaseModel):
type_id: int
status_id: int
level: int
excepthour: int
user_id: int
createuser_id: int
deadline_days: int
develop_id: Optional[int]
class CreateTaskResponse(BaseModel):
success: bool
task_id: Optional[int]
task_name: str
message: str
```
### 4.2 新增路由
**文件**: `backend/app/routers/security_erp_task.py`(新建)
```python
GET /api/security/executions/{execution_id}/task-preview
POST /api/security/executions/{execution_id}/create-task
```
### 4.3 注册路由
**文件**: `backend/app/main.py`
```python
from app.routers import ..., security_erp_task
# 安全测试-ERP任务创建(需在 security_erp_upload 之后注册)
app.include_router(
security_erp_task.router,
prefix="/api/security",
tags=["安全测试-ERP任务创建"]
)
```
---
## 五、Phase 4:前端实现
### 5.1 新增 API 封装
**文件**: `frontend/src/api/security.ts`(追加)
```typescript
/** 获取 ERP 任务创建预览 */
export function getSecurityTaskPreview(executionId: string) {
return request.get(`/api/security/executions/${executionId}/task-preview`)
}
/** 创建 ERP 任务 */
export function createSecurityTask(
executionId: string,
data: {
type_id: number
status_id: number
level: number
excepthour: number
user_id: number
createuser_id: number
deadline_days: number
develop_id?: number
}
) {
return request.post(`/api/security/executions/${executionId}/create-task`, data)
}
```
### 5.2 修改 Reports.vue
在报告详情弹窗中,ERP 上传按钮旁新增「创建ERP任务」按钮:
```
[上传到ERP] [创建ERP任务] ← 新增
```
新增对话框 `erpTaskDialog`
- 报告摘要区(只读表格):测试目标、测试时间、漏洞统计
- 任务配置区:
- 任务名称(el-input,自动生成可编辑)
- 任务类型(el-select,从 task_types)
- 任务状态(el-select,从 task_statuses)
- 责任人(el-select,过滤 staff_list)
- 创建人(el-select,过滤 staff_list)
- 紧急程度(el-select,1-5 自动计算)
- 期望工时(el-input-number)
- 截止天数(el-input-number)
- 关联需求(el-input readonly,显示需求名称)
- 按钮:取消 / 确认创建
打开对话框时调用 `getSecurityTaskPreview(executionId)` 获取预览数据。
---
## 六、Phase 5:构建与验证
### 6.1 构建验证
```bash
cd frontend && npm run build
```
预期:构建成功,无 TypeScript 类型错误。
### 6.2 后端验证
```bash
cd backend
uvicorn app.main:app --port 8001
# 验证预览接口
curl http://localhost:8001/api/security/executions/{id}/task-preview
# 验证创建接口
curl -X POST http://localhost:8001/api/security/executions/{id}/create-task \
-H "Content-Type: application/json" \
-d '{"type_id":3,"status_id":1,"level":5,"excepthour":21,"user_id":101,"createuser_id":441,"deadline_days":7}'
```
### 6.3 功能验证清单
| 验证项 | 预期结果 | 状态 |
|--------|---------|------|
| 打开对话框获取预览 | 报告摘要 + 基础数据正确加载 | □ |
| 紧急程度自动计算 | 根据漏洞数显示 1-5 | □ |
| 任务名称自动生成 | `{IP}接口安全测试及漏洞修复_{日期}` | □ |
| 修改任务参数 | 提交时使用修改后的值 | □ |
| 确认创建 | ERP 返回任务 ID,前端显示成功 | □ |
| 创建失败 | 显示错误信息,不影响其他功能 | □ |
| 重复点击 | 按钮 loading 防重复提交 | □ |
---
## 七、文件清单
### 7.1 新增文件
| 文件 | 说明 |
|------|------|
| `backend/app/services/erp_task_client.py` | ERP 任务 API 客户端 |
| `backend/app/services/security_erp_task_service.py` | 任务创建服务 |
| `backend/app/schemas/security_erp_task.py` | 任务创建 Schema |
| `backend/app/routers/security_erp_task.py` | 任务创建路由 |
| `Docs/PRD/需求文档/安全测试/_PRD_安全测试ERP任务创建_需求文档.md` | 需求文档 |
### 7.2 修改文件
| 文件 | 修改内容 |
|------|---------|
| `backend/app/main.py` | 注册 `security_erp_task.router` |
| `frontend/src/api/security.ts` | 追加预览 + 创建 API |
| `frontend/src/views/Reports.vue` | 报告详情弹窗新增"创建ERP任务"按钮 + 对话框 |
---
## 八、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 参考目录 unicode 路径 | 导入失败 | 用绝对路径只读参考,不影响代码 |
| ERP 人员列表大 | 前端下拉卡顿 | 仅使用前 200 人 + filterable |
| 创建任务失败 | 用户困惑 | 明确错误提示 + 不阻断已有功能 |
| 报告正则不匹配 | 解析默认值 | 宽松匹配 + 默认值兜底 |
---
*本文档为安全测试 ERP 任务创建功能的计划执行文档,供开发阶段参考。*
\ No newline at end of file
# 安全测试 ERP 任务创建 · 需求文档(PRD)
> **文档状态**:初稿 · **版本**:v1.0 · **最后更新**:2026-08-17
> **维护者**:czj · **所属模块**:安全测试
> **参考实现**:`E:\GithubData\ubains-module-test\develop\AuxiliaryTool\ScriptTool\ApiSecurityTest\utils\task_creator.py`、`erp_client.py`、`report_parser.py`
---
## 一、需求背景与目标
### 1.1 背景
当前安全测试模块已完成以下流程:
- ✅ 安全测试执行(OWASP API Security Top 10)
- ✅ 安全测试报告生成(Markdown 7 章节)
- ✅ 报告上传 ERP(DOCX → HTML → 测试单/项目资料/协作文档)
但缺少**ERP 任务指派跟踪**环节。参考工具 `ApiSecurityTest` 在安全测试完成后,会调用 ERP 的 `/openclaw/task` 接口创建任务,指定责任人、关联需求、设置截止时间,从而实现完整的测试闭环管理。
当前平台需要在安全测试模块中集成 ERP 任务创建功能,使得用户在执行完安全测试后,可以在报告中心一键创建 ERP 任务进行跟踪管理。
### 1.2 目标
在安全测试报告中心新增「创建 ERP 任务」功能,提供以下能力:
1. **自动解析报告摘要**:从已生成的 Markdown 报告中提取测试目标、漏洞数量、测试时间等信息
2. **获取 ERP 基础数据**:任务类型、任务状态、人员列表(含缓存)
3. **搜索关联需求**:根据配置的关键词搜索 ERP 中关联的需求
4. **构建任务数据**:根据漏洞数量自动计算紧急程度,生成任务名称和 HTML 描述
5. **创建 ERP 任务**:调用 `/openclaw/task` 接口创建任务
6. **任务参数可配置**:支持用户指定责任人、创建人、截止天数、紧急程度等
### 1.3 范围
| 范围 | 包含 | 不包含 |
|------|------|--------|
| ERP 任务创建 | 创建任务到 ERP 系统 | 修改/删除 ERP 任务 |
| 报告解析 | 从 Markdown 报告提取摘要 | 从其他格式报告解析 |
| 基础数据获取 | 任务类型、状态、人员列表 | 其他 ERP 基础数据 |
| 需求搜索 | 按关键词搜索关联需求 | 需求 CRUD 管理 |
| 任务参数配置 | 前端对话框配置责任人/创建人/截止时间 | 持久化任务配置模板 |
| 网盘上传 | — | 任务描述中引用网盘路径(当前平台不走网盘) |
---
## 二、功能需求
### 2.1 功能清单
| 编号 | 功能模块 | 子功能 | 优先级 | 说明 |
|------|---------|--------|--------|------|
| F1 | 任务创建 | 一键创建 ERP 任务 | P0 | 从报告摘要自动创建任务 |
| F2 | 任务创建 | 报告解析 | P0 | 从已生成的 Markdown 报告提取测试目标、漏洞统计、测试时间 |
| F3 | 任务创建 | 基础数据获取 | P0 | 获取任务类型、状态、人员列表(带缓存) |
| F4 | 任务创建 | 需求搜索 | P1 | 按配置关键词搜索关联需求 |
| F5 | 任务创建 | 紧急程度自动计算 | P0 | 根据漏洞数量自动计算 1-5 级 |
| F6 | 任务创建 | 任务名称自动生成 | P0 | 格式:`{IP}接口安全测试及漏洞修复_{日期}` |
| F7 | 任务参数配置 | 责任人选择 | P0 | 下拉选择 ERP 人员列表 |
| F8 | 任务参数配置 | 创建人选择 | P0 | 下拉选择 ERP 人员列表 |
| F9 | 任务参数配置 | 截止天数配置 | P1 | 数字输入框,默认 7 天 |
| F10 | 任务参数配置 | 紧急程度覆盖 | P1 | 手动覆盖自动计算值 |
| F11 | 任务参数配置 | 期望工时配置 | P1 | 数字输入框,默认 21 小时 |
| F12 | 任务参数配置 | 关联需求显示 | P1 | 显示搜索到的需求名称 |
### 2.2 功能详细描述
#### 2.2.1 报告解析(F2)
`SecurityReportService` 生成的 Markdown 报告中提取以下信息:
| 提取字段 | Markdown 格式 | 示例 |
|---------|---------------|------|
| 测试目标 | `> **测试目标**:https://192.168.5.44` | `https://192.168.5.44` |
| 测试时间 | `\| 测试时间 \| 2026-08-17 10:00:00 ~ 2026-08-17 11:30:00 \|` | `2026-08-17 10:00:00` |
| 高危漏洞数 | `\| 🔴 高危漏洞 \| **3** \|` | 3 |
| 中危漏洞数 | `\| 🟠 中危漏洞 \| **7** \|` | 7 |
| 低危漏洞数 | `\| 🟡 低危漏洞 \| **4** \|` | 4 |
| 信息类 | `\| 🔵 信息类 \| **2** \|` | 2 |
| 安全项 | `\| 🟢 已验证安全 \| **123** \|` | 123 |
| 测试用例总数 | `\| 测试用例总数 \| **145** \|` | 145 |
#### 2.2.2 基础数据获取(F3)
调用 ERP 接口获取以下数据,带 1 小时缓存:
| 数据 | ERP 接口 | 用途 |
|------|----------|------|
| 任务类型列表 | `GET /openclaw/tasktype` | 选择任务类型(默认 3=后端开发) |
| 任务状态列表 | `GET /openclaw/taskstatus` | 选择任务状态(默认 1=新建) |
| 人员列表 | `GET /openclaw/stuff` | 选择责任人/创建人 |
#### 2.2.3 紧急程度自动计算(F5)
| 条件 | 级别 | 说明 |
|------|------|------|
| 高危漏洞 > 0 | 5 | 最紧急 |
| 中危漏洞 > 0 | 4 | 高优先级 |
| 低危漏洞 > 0 | 3 | 中优先级 |
| 仅有信息类 | 2 | 正常优先级 |
| 无漏洞 | 1 | 低优先级 |
#### 2.2.4 任务名称自动生成(F6)
格式:`{目标IP}接口安全测试及漏洞修复_{YYYYMMDD}`
示例:`192.168.5.44接口安全测试及漏洞修复_20260817`
#### 2.2.5 任务描述 HTML 生成
任务描述(`content` 字段)为 HTML 格式,包含以下内容:
1. **测试概览**:测试目标、测试时间、测试标准(OWASP API Security Top 10)
2. **漏洞统计**:总计/高危/中危/低危/信息类/安全的表格
3. **高危漏洞详情**:列出高危漏洞列表(如有)
4. **中危漏洞**:中危漏洞数量提示
5. **修复建议**:标准修复建议模板
6. **紧急度说明**:根据计算级别显示对应的说明文字
#### 2.2.6 任务参数配置(F7-F12)
在创建任务前,用户可通过前端对话框配置以下参数:
| 参数 | 控件类型 | 默认值 | 说明 |
|------|---------|--------|------|
| 任务类型 | 下拉选择 | 3(后端开发) | 从 ERP 获取的类型列表 |
| 任务状态 | 下拉选择 | 1(新建) | 从 ERP 获取的状态列表 |
| 责任人 | 下拉选择 | 用户配置 | 从 ERP 人员列表选择 |
| 创建人 | 下拉选择 | 用户配置 | 从 ERP 人员列表选择 |
| 紧急程度 | 下拉选择/数字 | 自动计算 | 1-5,可手动覆盖 |
| 期望工时 | 数字输入 | 21 小时 | 任务的预估工时 |
| 截止天数 | 数字输入 | 7 天 | 从当前时间 + N 天 |
| 关联需求 | 文本显示 | 搜索匹配 | 显示搜索到的需求名称 |
### 2.3 业务流程
```
用户操作:安全测试执行完成 → 报告中心查看报告 → 点击"创建ERP任务"
后端流程:
步骤1:获取已生成的 Markdown 报告内容
步骤2:解析报告提取摘要(目标、漏洞数、时间)
步骤3:获取 ERP 基础数据(任务类型、状态、人员)
步骤4:搜索关联需求(按配置关键词)
步骤5:返回解析结果 + 基础数据到前端
用户操作:确认/修改任务参数 → 点击"确认创建"
后端流程:
步骤6:生成任务名称({IP}接口安全测试及漏洞修复_{日期})
步骤7:生成任务描述 HTML
步骤8:计算截止时间(当前时间 + 截止天数)
步骤9:调用 ERP 接口 /openclaw/task 创建任务
步骤10:返回创建结果(任务ID/名称/链接)
```
---
## 三、非功能需求
### 3.1 性能要求
- 报告解析应在 1s 内完成
- 基础数据获取(含缓存)应在 3s 内返回
- 任务创建应在 5s 内返回
### 3.2 可用性要求
- 任务创建失败不影响已有报告和上传功能
- 错误信息明确提示失败原因(网络超时/ERP 配置错误/权限不足)
- 按钮显示加载状态,防止重复提交
### 3.3 兼容性要求
- 复用现有 ERP 配置(`erp_configs` 表)中的 API Key 和 Base URL
- 复用现有 `SecurityReportService` 获取报告内容
- 新增 ERP 任务相关接口,与现有报告上传接口独立
---
## 四、API 设计
### 4.1 新增后端接口
#### 4.1.1 获取任务创建预览数据
```
GET /api/security/executions/{execution_id}/task-preview
```
**说明**:在创建任务前,获取报告解析结果和 ERP 基础数据,供前端预览和配置。
**响应**
```json
{
"summary": {
"target": "https://192.168.5.44",
"server_ip": "192.168.5.44",
"start_time": "2026-08-17 10:00:00",
"end_time": "2026-08-17 11:30:00",
"total": 145,
"high": 3,
"medium": 7,
"low": 4,
"info": 2,
"safe": 129
},
"task_name": "192.168.5.44接口安全测试及漏洞修复_20260817",
"calculated_level": 5,
"task_types": [{"id": 1, "name": "UI设计"}, ...],
"task_statuses": [{"id": 1, "name": "新建"}, ...],
"staff_list": [{"id": 101, "name": "李立键"}, ...],
"requirement": {"id": 128, "name": "【新统一平台】公司内部安全测试扫描及修复验证"},
"default_config": {
"type_id": 3,
"status_id": 1,
"level": 4,
"excepthour": 21,
"user_id": 101,
"createuser_id": 441,
"deadline_days": 7
}
}
```
#### 4.1.2 创建 ERP 任务
```
POST /api/security/executions/{execution_id}/create-task
```
**请求体**
```json
{
"type_id": 3,
"status_id": 1,
"level": 5,
"excepthour": 21,
"user_id": 101,
"createuser_id": 441,
"deadline_days": 7,
"develop_id": 128
}
```
**响应**
```json
{
"success": true,
"task_id": 1024,
"task_name": "192.168.5.44接口安全测试及漏洞修复_20260817",
"message": "ERP任务创建成功"
}
```
### 4.2 ERP 对接接口
| 方法 | 端点 | 用途 |
|------|------|------|
| GET | `/openclaw/tasktype` | 获取任务类型列表 |
| GET | `/openclaw/taskstatus` | 获取任务状态列表 |
| GET | `/openclaw/stuff` | 获取人员列表 |
| GET | `/openclaw/develop?search_str=xxx` | 搜索需求列表 |
| POST | `/openclaw/task` | 创建任务 |
---
## 五、数据模型
### 5.1 复用现有模型
| 模型/表 | 用途 |
|---------|------|
| `ErpConfig` (`erp_configs`) | 提供 ERP API Key 和 Base URL |
| `Execution` (`executions`) | 获取执行记录信息 |
| `VulnerabilityResult` (`vulnerability_results`) | 获取漏洞统计数据 |
| 安全测试 Markdown 报告 | 解析报告摘要 |
### 5.2 不新增数据表
ERP 任务创建不持久化到本地数据库,所有任务数据直接发送到 ERP 系统。
---
## 六、用户界面
### 6.1 入口位置
安全测试 → 报告中心 → 报告详情弹窗 → 「创建ERP任务」按钮
### 6.2 对话框布局
点击「创建ERP任务」后弹出对话框,包含:
**上半部分:报告摘要(只读)**
- 测试目标
- 测试时间
- 漏洞统计(高危/中危/低危/信息类/安全)
**下半部分:任务参数配置**
- 任务名称(自动生成,可编辑)
- 任务类型(下拉选择)
- 任务状态(下拉选择)
- 责任人(下拉选择,从人员列表)
- 创建人(下拉选择,从人员列表)
- 紧急程度(自动计算,可手动覆盖)
- 期望工时(数字输入)
- 截止时间(自动计算,显示具体日期)
- 关联需求(自动搜索匹配,显示名称)
**操作按钮**
- 取消
- 确认创建(loading 状态)
---
## 七、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| ERP API 不可用 | 任务创建失败 | 失败不影响现有功能,提示明确错误 |
| 人员列表获取失败 | 无法选择责任人/创建人 | 使用默认配置回退值 |
| 需求搜索无结果 | 任务无关联需求 | 可选字段,允许跳过 |
| 报告解析格式变更 | 解析失败 | 正则匹配宽松,失败时使用默认值 |
| ERP API Key 权限不足 | 创建任务失败 | 错误提示中说明权限问题 |
---
## 八、参考实现对照
| 参考文件 | 功能 | 本次实现 |
|---------|------|---------|
| `utils/erp_client.py` | ERP 任务 API 客户端(含缓存) | 扩展 `ErpService` 或新建 `ErpTaskService` |
| `utils/task_creator.py` | 任务创建协调器 | 新建 `SecurityErpTaskService` |
| `utils/report_parser.py` | Markdown 报告解析 | 复用 `SecurityReportService` 或新建解析器 |
| `run_all.py``upload_to_erp()` | 主流程调用 | 在 Reports.vue 中新增按钮和对话框 |
---
*本文档为安全测试 ERP 任务创建功能需求规格说明,供开发和测试参考。*
\ No newline at end of file
# 安全测试ERP配置 · 计划执行文档
> **文档状态**:初稿 · **版本**:v1.0 · **最后更新**:2026-08-13
> **维护者**:czj · **所属模块**:安全测试
> **对应 PRD**:`_PRD_安全测试ERP配置_需求文档.md`
---
## 一、执行计划总览
### 阶段划分
| 阶段 | 内容 | 预估工时 | 产出物 |
|------|------|---------|--------|
| Phase 1 | 后端 API 路由(新增 ERP 配置接口,复用 `erp_configs` 表 + `ErpConfig` 模型) | 0.5h | `backend/app/routers/security_erp.py` + `schemas/security_erp.py` + main.py 注册 |
| Phase 2 | 前端 API 封装 + ERP 配置页面 + 路由 + 菜单 | 0.5h | `frontend/src/api/securityErp.ts` + `views/security/ErpConfig.vue` + router 修改 + App.vue 修改 |
| Phase 3 | 构建 + 联调验证 | 0.5h | `npm run build` 通过 + 后端 API 验证 |
### 参考实现
本次开发直接参考功能测试报告模块的 ERP 配置实现:
| 参考文件 | 参考内容 | 本次实现 |
|---------|---------|---------|
| `backend/app/routers/functional_report.py` (L520-L602) | `GET/PUT /api/functional-report/config` | `GET/PUT /api/security/erp-config` |
| `backend/app/schemas/functional_report.py` (L109-L124) | `ErpConfigResponse` / `ErpConfigUpdateRequest` | 新建 `schemas/security_erp.py` |
| `frontend/src/api/functionalReport.ts` (L141-L163) | `getErpConfig()` / `updateErpConfig()` | 新建 `api/securityErp.ts` |
| `frontend/src/views/functional-report/ErpConfig.vue` | ERP 配置表单页面 | 复制到 `views/security/ErpConfig.vue`,替换 API 引用 |
| `frontend/src/router/index.ts` (L57-L77) | 功能测试报告子路由结构 | 安全测试子路由新增 ERP 配置 |
| `frontend/src/App.vue` (L115-L117) | 功能测试报告 ERP 配置菜单 | 安全测试菜单新增 ERP 配置 |
---
## 二、Phase 1:后端 API
### 2.1 新增 Pydantic Schema
**文件**: `backend/app/schemas/security_erp.py`(新建)
```python
class SecurityErpConfigResponse(BaseModel):
"""安全测试ERP配置响应"""
api_key: str = Field("", description="API密钥")
base_url: str = Field("", description="基础URL")
developtesting_id: int = Field(0, description="默认测试单ID")
type_id: int = Field(2, description="报告类型ID")
image_url_prefix: str = Field("", description="图片URL前缀")
class SecurityErpConfigUpdateRequest(BaseModel):
"""安全测试ERP配置更新请求"""
api_key: Optional[str] = Field(None, description="API密钥")
base_url: Optional[str] = Field(None, description="基础URL")
developtesting_id: Optional[int] = Field(None, description="默认测试单ID")
type_id: Optional[int] = Field(None, description="报告类型ID")
image_url_prefix: Optional[str] = Field(None, description="图片URL前缀")
```
### 2.2 新增 API 路由
**文件**: `backend/app/routers/security_erp.py`(新建)
路由逻辑**完全对齐**功能测试报告的 ERP 配置接口:
#### GET /api/security/erp-config
```python
@router.get("/erp-config", response_model=SecurityErpConfigResponse)
async def get_erp_config(db: AsyncSession = Depends(get_db)):
"""获取ERP配置(优先数据库,回退环境变量)"""
result = await db.execute(select(ErpConfig).where(ErpConfig.id == 1))
config = result.scalar_one_or_none()
if config:
return SecurityErpConfigResponse(
api_key=config.api_key or settings.ERP_API_KEY or "",
base_url=config.base_url or settings.ERP_BASE_URL or "",
developtesting_id=config.developtesting_id or settings.ERP_DEVELOPTESTING_ID or 0,
type_id=config.type_id or settings.ERP_TYPE_ID or 2,
image_url_prefix=config.image_url_prefix or settings.ERP_IMAGE_URL_PREFIX or "",
)
return SecurityErpConfigResponse(
api_key=settings.ERP_API_KEY or "",
base_url=settings.ERP_BASE_URL or "",
developtesting_id=settings.ERP_DEVELOPTESTING_ID or 0,
type_id=settings.ERP_TYPE_ID or 2,
image_url_prefix=settings.ERP_IMAGE_URL_PREFIX or "",
)
```
#### PUT /api/security/erp-config
```python
@router.put("/erp-config", response_model=SecurityErpConfigResponse)
async def update_erp_config(request: SecurityErpConfigUpdateRequest, db: AsyncSession = Depends(get_db)):
"""更新ERP配置(持久化到数据库,同时更新运行时环境变量)"""
result = await db.execute(select(ErpConfig).where(ErpConfig.id == 1))
config = result.scalar_one_or_none()
if not config:
config = ErpConfig(id=1, ...)
db.add(config)
# 更新字段 + 同步 settings
...
await db.commit()
await db.refresh(config)
return SecurityErpConfigResponse(...)
```
### 2.3 注册路由到 main.py
**文件**: `backend/app/main.py`
`security.router` 注册后新增:
```python
from app.routers import security_erp
app.include_router(
security_erp.router,
prefix="/api/security",
tags=["安全测试-ERP配置"]
)
```
### 2.4 关键说明
- **复用 `ErpConfig` 模型**`from app.models.erp_config import ErpConfig`,两个模块共享 `erp_configs`
- **复用 `settings` 环境变量**`from app.config import settings`,共享 `ERP_API_KEY` / `ERP_BASE_URL` 等配置
- **不新增数据表**:安全测试和功能测试报告共用同一套 ERP 配置
---
## 三、Phase 2:前端实现
### 3.1 新增 API 封装
**文件**: `frontend/src/api/securityErp.ts`(新建)
```typescript
import request from '@/utils/request'
export const securityErpApi = {
async getConfig(): Promise<{
api_key: string
base_url: string
developtesting_id: number
type_id: number
image_url_prefix: string
}> {
const response = await request.get('/api/security/erp-config')
return response as any
},
async updateConfig(config: {
api_key?: string
base_url?: string
developtesting_id?: number
type_id?: number
image_url_prefix?: string
}): Promise<any> {
const response = await request.put('/api/security/erp-config', config)
return response as any
}
}
```
### 3.2 新增 ERP 配置页面
**文件**: `frontend/src/views/security/ErpConfig.vue`(新建)
直接复制 `frontend/src/views/functional-report/ErpConfig.vue`,修改点:
- 组件名称注释:`安全测试 ERP 配置管理页面`
- API 引用:`import { securityErpApi } from '@/api/securityErp'`
- 方法调用:`securityErpApi.getConfig()` / `securityErpApi.updateConfig()`
表单字段、校验规则、布局样式完全一致。
### 3.3 路由配置
**文件**: `frontend/src/router/index.ts`
在安全测试路由(`/security`)下新增子路由:
```typescript
{
path: '/security',
name: 'Security',
component: () => import('@/views/security/index.vue'),
redirect: '/security/cases',
meta: { title: '安全测试' },
children: [
// 原有子路由...
{
path: 'erp-config',
name: 'SecurityErpConfig',
component: () => import('@/views/security/ErpConfig.vue'),
meta: { title: 'ERP配置' }
}
]
}
```
### 3.4 菜单配置
**文件**: `frontend/src/App.vue`
安全测试子菜单新增 ERP 配置:
```html
<el-sub-menu index="/security">
<template #title>
<el-icon><Lock /></el-icon>
<span>安全测试</span>
</template>
<el-menu-item index="/cases/security">用例管理</el-menu-item>
<el-menu-item index="/execution/security">执行中心</el-menu-item>
<el-menu-item index="/reports/security">报告中心</el-menu-item>
<el-menu-item index="/security/erp-config">ERP 配置</el-menu-item> <!-- 新增 -->
</el-sub-menu>
```
### 3.5 安全测试父容器组件
如果 `frontend/src/views/security/index.vue` 不存在,需要创建(参考 `functional-report/index.vue`),仅渲染 `<router-view />`
---
## 四、Phase 3:构建与验证
### 4.1 构建验证
```bash
cd frontend
npm run build
```
预期结果:构建成功,无 TypeScript 类型错误。
### 4.2 后端验证
```bash
cd backend
uvicorn app.main:app --port 8001
# 验证 GET 接口
curl http://localhost:8001/api/security/erp-config
# 预期返回:{"api_key":"","base_url":"","developtesting_id":0,"type_id":2,"image_url_prefix":""}
# 验证 PUT 接口
curl -X PUT http://localhost:8001/api/security/erp-config \
-H "Content-Type: application/json" \
-d '{"api_key":"test-key","base_url":"https://erp.example.com"}'
# 预期返回:更新后的配置
```
### 4.3 功能验证
| 验证项 | 预期结果 | 状态 |
|--------|---------|------|
| 页面加载时自动获取配置 | 表单字段自动填充 | □ |
| 修改字段后保存 | 成功提示,数据库持久化 | □ |
| 重置按钮 | 恢复加载时的原始值 | □ |
| 必填校验(API Key 为空) | 表单校验失败提示 | □ |
| 必填校验(Base URL 为空) | 表单校验失败提示 | □ |
| 保存时按钮 loading | 按钮显示"保存中..." | □ |
| 刷新页面后配置保留 | 上次保存的值仍显示 | □ |
---
## 五、文件清单
### 5.1 新增文件
| 文件 | 说明 |
|------|------|
| `backend/app/schemas/security_erp.py` | ERP 配置 Pydantic Schema |
| `backend/app/routers/security_erp.py` | ERP 配置 API 路由 |
| `frontend/src/api/securityErp.ts` | ERP 配置 API 前端封装 |
| `frontend/src/views/security/ErpConfig.vue` | ERP 配置管理页面 |
| `frontend/src/views/security/index.vue` | 安全测试父容器(若不存在) |
### 5.2 修改文件
| 文件 | 修改内容 |
|------|---------|
| `backend/app/main.py` | 注册 `security_erp.router` |
| `frontend/src/router/index.ts` | 安全测试路由新增 `erp-config` 子路由 |
| `frontend/src/App.vue` | 安全测试菜单新增 ERP 配置子项 |
---
## 六、风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 与功能测试报告共用 ERP 配置 | 配置修改互相影响 | 预期行为,两个模块共用同一套 ERP 配置,降低维护成本 |
| 后端已有 `erp_config` 路由冲突 | 404 或路由覆盖 | 前缀用 `/api/security/erp-config`,与 `/api/functional-report/config` 路径不同,不冲突 |
| 安全测试父容器不存在 | 路由无法渲染子页面 | 检查 `security/index.vue` 是否存在,不存在则创建 |
---
*本文档为安全测试ERP配置功能的计划执行文档,供开发阶段参考。*
\ No newline at end of file
# 安全测试ERP配置 · 需求文档(PRD)
> **文档状态**:初稿 · **版本**:v1.0 · **最后更新**:2026-08-13
> **维护者**:czj · **所属模块**:安全测试
> **参考实现**:功能测试报告模块 ERP 配置子菜单
---
## 一、需求背景与目标
### 1.1 背景
当前安全测试模块已作为一级菜单独立运行,包含「用例管理」「执行中心」「报告中心」三个子页面。安全测试报告生成后,需要将报告上传到 ERP 系统进行归档和管理,但目前安全测试模块下缺少 ERP 配置入口。
功能测试报告模块已实现 ERP 配置功能(API Key、Base URL、测试单 ID、Type ID、图片URL前缀),通过 `erp_configs` 数据表持久化。安全测试模块可以直接复用同一套 ERP 配置数据与 API,仅需在前端新增配置页面入口。
### 1.2 目标
在安全测试模块下新增「ERP 配置」子菜单,提供 ERP 连接配置管理功能:
1. **ERP 配置管理**:配置 API Key、Base URL、测试单 ID、Type ID、图片 URL 前缀
2. **配置持久化**:保存到数据库,服务重启后保留
3. **配置回退**:数据库无记录时自动回退到环境变量默认值
### 1.3 范围
| 范围 | 包含 | 不包含 |
|------|------|--------|
| ERP 配置表单 | API Key、Base URL、测试单 ID、Type ID、图片 URL 前缀 | 其他 ERP 字段 |
| 配置持久化 | 数据库读写,重启保留 | 多租户配置隔离 |
| 配置回退 | 数据库无记录时回退环境变量 | 配置版本管理 |
| 后端 API | 复用 `erp_configs` 表的 GET/PUT 接口 | 新建数据表 |
---
## 二、功能需求
### 2.1 功能清单
| 编号 | 功能模块 | 子功能 | 优先级 | 说明 |
|------|---------|--------|--------|------|
| F1 | ERP 配置 | 配置获取 | P0 | 页面加载时自动获取当前 ERP 配置 |
| F2 | ERP 配置 | 配置保存 | P0 | 修改后保存到数据库,重启后持久化 |
| F3 | ERP 配置 | 配置重置 | P1 | 重置回加载时的原始值 |
| F4 | ERP 配置 | 表单校验 | P0 | API Key 和 Base URL 必填校验 |
### 2.2 功能详细描述
#### 2.2.1 配置获取(F1)
- 触发时机:页面加载时自动调用
- 数据来源:优先从数据库 `erp_configs` 表读取,无记录时回退到环境变量默认值
- 字段映射:
| 表单字段 | 数据库字段 | 环境变量 | 默认值 |
|---------|-----------|---------|--------|
| API Key | `api_key` | `ERP_API_KEY` | 空字符串 |
| Base URL | `base_url` | `ERP_BASE_URL` | 空字符串 |
| 测试单 ID | `developtesting_id` | `ERP_DEVELOPTESTING_ID` | 397 |
| Type ID | `type_id` | `ERP_TYPE_ID` | 2 |
| 图片 URL 前缀 | `image_url_prefix` | `ERP_IMAGE_URL_PREFIX` | 空字符串 |
#### 2.2.2 配置保存(F2)
- 所有字段均支持修改
- 保存成功后弹出成功提示
- 更新数据库记录,同时更新运行时环境变量(`settings` 对象)
- 保存后更新原始快照,使"重置"按钮回到最新保存值
#### 2.2.3 配置重置(F3)
- 点击"重置"按钮,表单恢复到加载时的原始值
- 清除表单校验错误状态
- 如果未保存过修改,重置到加载时的值
#### 2.2.4 表单校验(F4)
| 字段 | 规则 | 提示 |
|------|------|------|
| API Key | 必填 | 请输入 ERP API 密钥 |
| Base URL | 必填 | 请输入 ERP 基础 URL |
---
## 三、非功能需求
### 3.1 性能要求
- ERP 配置读取应在 1s 内返回
- 配置保存应在 2s 内返回
### 3.2 可用性要求
- 表单提交时按钮显示加载状态,防止重复提交
- 保存失败时显示错误信息,不丢失已填数据
### 3.3 兼容性要求
- 复用功能测试报告模块的 `erp_configs` 表,不新增数据表
- 复用后端 ERP 配置 API(`GET/PUT /api/functional-report/config`
---
## 四、数据模型
### 4.1 复用现有模型
复用 `backend/app/models/erp_config.py` 中的 `ErpConfig` 模型,表名 `erp_configs`,固定 ID=1 的单行记录:
```
erp_configs
├── id: int (PK, 固定为 1)
├── api_key: varchar(255) — ERP API 密钥
├── base_url: varchar(500) — ERP 基础 URL
├── developtesting_id: int — 默认测试单 ID
├── type_id: int — 报告类型 ID
├── image_url_prefix: varchar(500) — 图片 URL 前缀
└── updated_at: datetime — 更新时间
```
### 4.2 API 设计
| 方法 | 路径 | 说明 | 请求体 | 响应 |
|------|------|------|--------|------|
| GET | `/api/security/erp-config` | 获取 ERP 配置 | — | `{ api_key, base_url, developtesting_id, type_id, image_url_prefix }` |
| PUT | `/api/security/erp-config` | 更新 ERP 配置 | `{ api_key?, base_url?, developtesting_id?, type_id?, image_url_prefix? }` | 同上 |
---
## 五、用户界面
### 5.1 页面布局
- 安全测试 → ERP 配置(子菜单)
- 页面内容:一个卡片容器,包含表单
- 表单宽度:最大 600px,标签右对齐,标签宽度 140px
- 表单字段:API Key(密码框)、Base URL(输入框)、测试单 ID(数字框)、Type ID(数字框)、图片 URL 前缀(输入框)
- 操作按钮:保存配置 + 重置
### 5.2 菜单结构变更
```
安全测试(改前) 安全测试(改后)
├── 用例管理 ├── 用例管理
├── 执行中心 ├── 执行中心
└── 报告中心 ├── 报告中心
└── ERP 配置 ← 新增
```
---
## 六、风险与依赖
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 与功能测试报告模块 ERP 配置共用数据表 | 两个模块的配置同步 | 共享同一套 ERP 配置属于预期行为,降低维护成本 |
| 环境变量未配置 | 配置为空 | 数据库无记录时回退环境变量,环境变量无值时回退空字符串 |
---
*本文档为安全测试 ERP 配置功能需求规格说明,供开发和测试参考。*
\ No newline at end of file
# HANDOFF — 安全测试模块会话交接文档
> **生成时间**: 2026-07-22
> **最后更新**: 2026-08-13
> **最后更新**: 2026-08-17
> **当前分支**: `platform-auto-test`
> **开发窗口**: 安全测试模块
> **最近提交**: `4c3301ee` feat(security): 前端报告中心适配安全报告+补充API3/4/6/7/9/10用例
> **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级为一级菜单
> **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级 + ERP 配置子菜单 + ERP 上传流程对接完成 + ERP 任务创建对接完成
---
......@@ -24,15 +24,92 @@
---
## 一、本次任务(已完成)
## 一、本次任务记录
### 任务:安全测试模块 P1 待办 + P2 执行验证
### 本次任务(2026-08-13):安全测试新增 ERP 配置子菜单
**核心成果**
1. ✅ 按规范输出两份文档:
- `Docs/PRD/需求文档/安全测试/_PRD_安全测试ERP配置_需求文档.md`(需求文档)
- `Docs/PRD/需求文档/安全测试/_PRD_安全测试ERP配置_计划执行.md`(计划执行文档)
2. ✅ 后端实现:新增 `GET/PUT /api/security/erp-config` 接口
- `backend/app/schemas/security_erp.py`(Pydantic 请求/响应模型)
- `backend/app/routers/security_erp.py`(复用 `ErpConfig` 模型 + `erp_configs` 表 + ERP 环境变量,与功能测试报告**共享同一套配置**
- `backend/app/main.py` 注册 `security_erp.router`(prefix `/api/security`
3. ✅ 前端实现:
- `frontend/src/api/securityErp.ts`(API 封装)
- `frontend/src/views/security/ErpConfig.vue`(复制功能测试报告页面,替换 API 引用)
- `frontend/src/router/index.ts` 新增 `/security/erp-config` 独立路由(⚠️ 安全测试无父路由结构,采用独立路由方案)
- `frontend/src/App.vue` 安全测试子菜单新增「ERP 配置」+ 页面标题映射 `SECURITY_SUB_LABELS`
4. ✅ 构建 + 联调验证通过:
- `npm run build` 通过,无 TS 错误
- `GET/PUT /api/security/erp-config` 接口实测正常(含持久化与恢复)
**注意事项**
- 安全测试模块没有 `/security` 父路由(用例/执行/报告复用 `/cases/:type` 等共享路由),ERP 配置采用独立路由 `/security/erp-config` 实现,与计划文档中的父子路由假设有差异,不影响功能
- 本次验证用端口 8002 起后端(8001 有无法杀掉的旧进程残留,见踩坑 S14)
### 本次任务(2026-08-13):安全用例同步 5.60 + 报告加载失败修复
**核心成果**
1. ✅ 26 个新安全测试用例同步部署到 5.60 MySQL 服务器(新增 25 个,全平台总计 67 个,覆盖 12 个模块)
- 修复了 `create_security_cases.py` 中 aiomysql ↔ pymysql 同步引擎冲突(URL 自动转换)
2.**修复安全测试报告加载失败**(P1,报告查看功能阻塞)
- 根因:Axios 响应拦截器自动解包 `response.data``Reports.vue` 再取 `.data``undefined`
- 修复:`Reports.vue` 改为 `response.content``security.ts` 改用 `as any` 返回类型
3. ✅ 修复已部署至 5.60 服务器,**用户确认可正常访问报告页面**
4. ✅ 输出问题处理 + 计划执行两份文档
### 历史任务(2026-07-22):安全测试模块 P1 待办 + P2 执行验证
**核心成果**
1. ✅ 前端报告中心适配安全报告(Markdown 渲染 + 主题适配 + 下载功能)
2. ✅ 补充 API3/4/6/7/9/10 安全测试用例(18 个新用例,总计 42 个)
3.**P2 执行验证**:全量 42 个安全测试用例执行完成
### 本次任务(2026-08-17):安全测试 ERP 任务创建对接(指派跟踪)
**背景**:参考 `develop/AuxiliaryTool/ScriptTool/ApiSecurityTest` 工具,其除报告上传外还对接了 **ERP 任务创建(指派跟踪)**,用于形成「扫描 → 修复 → 回归验证」闭环。本次实现将该能力集成到平台安全测试模块。
**流程**:需求文档 → 计划执行文档 → 后端 → 前端 → 构建验证 全链路完成(遵循项目工作流)。
**核心成果**
1. ✅ 按规范输出两份文档:
- `Docs/PRD/需求文档/安全测试/_PRD_安全测试ERP任务创建_需求文档.md`
- `Docs/PRD/需求文档/安全测试/_PRD_安全测试ERP任务创建_计划执行.md`
2.**后端 API**(新增 2 个接口):
- `GET /api/security/executions/{id}/task-preview` — 任务创建预览(报告摘要 + 任务类型/状态/人员列表 + 关联需求 + 默认配置)
- `POST /api/security/executions/{id}/create-task` — 创建 ERP 任务(`POST /openclaw/task`
- `backend/app/services/erp_task_client.py`(新增,ERP API 客户端:1h 缓存 + 4xx 不重试/5xx 重试 3 次 + `X-Api-Key` 认证)
- `backend/app/services/security_erp_task_service.py`(新增,业务服务:报告解析、紧急程度计算、任务名生成 `{IP}接口安全测试及漏洞修复_{YYYYMMDD}`、HTML 描述生成)
- `backend/app/schemas/security_erp_task.py`(新增,Pydantic 模型)
- `backend/app/routers/security_erp_task.py`(新增,路由)
- `backend/app/main.py`(注册 security_erp_task 路由)
3.**前端**
- `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` / `createSecurityTask``as any` 返回类型)
- `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框:报告摘要只读折叠区 + 任务配置表单(类型/状态/责任人/创建人/紧急程度/期望工时/截止天数/关联需求))
4.**构建验证通过**`npm run build` 无 TS 错误(修复了 `request.get`/`request.post` 返回类型推断问题,统一 `as any`);后端 5 文件语法检查通过;FastAPI 路由注册验证通过
**默认任务配置**(对齐参考实现 config.yaml):
- type_id=3(后端开发)、status_id=1(新建)、level=按漏洞自动计算(高危→5/中危→4/低危→3/信息→2/无→1)
- excepthour=21、user_id=101(李立键)、createuser_id=441(陈泽键)、deadline_days=7
- 关联需求搜索词:`【新统一平台】公司内部安全测试扫描及修复验证`
### 本次任务(2026-08-17):安全测试报告 ERP 上传流程对接
**核心成果**
1.**后端**:新增 `security_erp_upload.py` 路由,提供 3 个 ERP 上传接口
- `POST /api/security/executions/{id}/upload-to-erp` — 上传到 ERP 测试单(自动生成 Markdown 报告 → 转换为 DOCX → 上传 ERP)
- `POST /api/security/executions/{id}/upload-to-project` — 上传到 ERP 项目资料
- `POST /api/security/executions/{id}/upload-to-cooperation` — 上传到 ERP 协作文档
- 复用 `ErpService`(与功能测试报告共享),`Markdown→DOCX` 转换使用 `python-docx`
2.**前端**`Reports.vue` 安全报告弹窗新增「上传到 ERP」按钮 + 上传对话框
- 支持三种上传目标选择(ERP 测试单/项目资料/协作文档)
- 测试单上传:可选测试单 ID、抄送人、创建人
- 项目资料/协作文档:必填项目 ID,可选文件名称/分组 ID
3.**构建验证**`npm run build` 通过,无 TS 错误
4.**后端路由验证**`/api/security/executions/{id}/upload-to-*` 三个接口注册成功
### 执行验证结果(2026-07-22)
| 执行名称 | 用例数 | 通过 | 失败 | 耗时 | 执行ID |
......@@ -209,7 +286,7 @@ GET /api/security/executions/{id}/report/download → 下载 .md 文件
| 内容 | 数量 | 备注 |
|------|------|------|
| 安全测试模块 | 12 | sec_api01~sec_api10 + sec_regression + sec_redline |
| 安全测试用例 | 42 | 全部 OWASP API1-10 + 回归 + 红线覆盖 |
| 安全测试用例 | 67 | 全部 OWASP API1-10 + 回归 + 红线覆盖(2026-08-13 新增 25 个) |
| 安全测试配置 | 1 | `sec_cfg_default`(新统一平台 192.168.5.44) |
**用例分布**
......@@ -235,10 +312,9 @@ GET /api/security/executions/{id}/report/download → 下载 .md 文件
| 优先级 | 待办 | 说明 |
|--------|------|------|
| **P2** | ✅ ~~WebSocket 实时进度推送~~ | 安全测试执行快(3.7s/42用例),暂不需要 |
| **P2** | ✅ ~~执行安全测试用例并验证报告~~ | 已完成,42用例执行通过,报告渲染正常 |
| **P2.5** | 前端报告类型标签修复 | 报告类型显示"UI自动化"应为"安全测试" |
| **P3** | 网盘上传 + ERP 任务对接 | 参考实现的功能,平台化后可选 |
| **P2.5** | ✅ ~~前端报告类型标签修复~~ | 报告类型显示"UI自动化"应为"安全测试" |
| **P3** | ✅ ~~安全报告上传 ERP 流程对接~~ | ERP 配置入口已完成,ERP 上传流程已完成(2026-08-17) |
| **P3.5** | ✅ ~~安全测试 ERP 任务创建对接~~ | 任务创建预览 + 创建接口 + 前端对话框已完成(2026-08-17),待下次会话实测 |
---
......@@ -256,6 +332,10 @@ GET /api/security/executions/{id}/report/download → 下载 .md 文件
| S8 | 数据库 locked 执行用例创建脚本失败 | 另一个窗口后端服务占用数据库 | **与会议管理窗口协调脚本执行时间** |
| S9 | Windows 脚本打印 ✅❌ 报 UnicodeEncodeError | GBK 编码无法编码 emoji | `PYTHONIOENCODING=utf-8 python scripts/xxx.py` |
| S10 | 报告类型标签显示"UI自动化"而非"安全测试" | Reports.vue 中 case_type 判断逻辑未覆盖安全测试 | 需修复前端报告类型标签映射 |
| S11 | 安全报告预览报 `Cannot read properties of undefined (reading 'content')` | Axios 响应拦截器已自动解包 `response.data`,Reports.vue 再取 `.data``undefined` | 调用处直接用 `response.content`,不取 `.data`(已修复) |
| S12 | `TS2339: Property 'content' does not exist on type 'AxiosResponse<...>'` | `request.get<T>()` 泛型与实际运行时类型(解包后)不匹配 | API 函数统一用 `as any` 返回类型,绕过类型推断(已修复) |
| S13 | `create_security_cases.py` 用同步引擎连 MySQL 报 aiomysql 兼容错误 | 脚本复制了异步引擎 URL,同步连接用 `mysql+aiomysql://` 前缀冲突 | URL 自动转换:`mysql+aiomysql://``mysql+pymysql://`(已修复) |
| S14 | 端口 8001 旧 uvicorn 进程无法 taskkill 杀掉 | uvicorn `--reload` 残留父进程,MSYS bash 的 taskkill 权限/编码问题 | 用 `powershell Stop-Process -Id <pid> -Force`,或换端口 8002 起新实例验证 |
---
......@@ -293,6 +373,35 @@ PYTHONIOENCODING=utf-8 python scripts/create_security_cases.py
- `frontend/src/App.vue` — 安全测试升级为一级菜单(详见二、2.3 节)
- 前端构建已通过,待下次会话提交
**2026-08-13 ERP 配置功能(未提交)**
- `backend/app/schemas/security_erp.py`(新增)
- `backend/app/routers/security_erp.py`(新增)
- `backend/app/main.py`(注册 security_erp 路由)
- `frontend/src/api/securityErp.ts`(新增)
- `frontend/src/views/security/ErpConfig.vue`(新增)
- `frontend/src/router/index.ts`(新增 `/security/erp-config` 路由)
- `frontend/src/App.vue`(安全测试子菜单新增 ERP 配置)
- 文档:`Docs/PRD/需求文档/安全测试/` 下两份 ERP 配置文档(新增)
- 前端构建已通过、后端接口已验证,待下次会话提交
**2026-08-17 安全报告 ERP 上传流程对接(未提交)**
- `backend/app/routers/security_erp_upload.py`(新增,含 Markdown→DOCX 转换 + 3 个 ERP 上传接口)
- `backend/app/main.py`(注册 security_erp_upload 路由)
- `frontend/src/api/security.ts`(新增 3 个 ERP 上传 API 函数)
- `frontend/src/views/Reports.vue`(安全报告弹窗新增 ERP 上传按钮 + 对话框 UI)
- 前端构建已通过、后端路由已验证,待下次会话提交
**2026-08-17 安全测试 ERP 任务创建对接(未提交)**
- `backend/app/services/erp_task_client.py`(新增,ERP API 客户端:1h 缓存 + 重试 + X-Api-Key 认证)
- `backend/app/services/security_erp_task_service.py`(新增,业务服务:报告解析、紧急程度计算、任务名/描述生成)
- `backend/app/schemas/security_erp_task.py`(新增,Pydantic 模型)
- `backend/app/routers/security_erp_task.py`(新增,2 个 API 端点)
- `backend/app/main.py`(注册 security_erp_task 路由)
- `frontend/src/api/security.ts`(新增 `getSecurityTaskPreview` + `createSecurityTask` 函数)
- `frontend/src/views/Reports.vue`(安全报告弹窗新增「创建ERP任务」按钮 + 对话框 UI)
- 文档:`Docs/PRD/需求文档/安全测试/` 下两份 ERP 任务创建文档(新增)
- 前端构建已通过、后端路由已验证,待下次会话提交(含实测)
---
*本文档由安全测试开发窗口生成,供下一次会话快速恢复上下文。会议管理窗口的交接见 `HANDOFF.md`。*
......@@ -27,7 +27,7 @@ from fastapi.responses import FileResponse
from app.config import settings
from app.database import init_db
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies, security, ai_locator, device_sim, system, element_locator, login_template, smart_locate, performance, performance_output, api_preset, deploy, functional_report, report_template
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies, security, security_erp, security_erp_upload, security_erp_task, ai_locator, device_sim, system, element_locator, login_template, smart_locate, performance, performance_output, api_preset, deploy, functional_report, report_template, api_test
# 配置日志
logging.basicConfig(
......@@ -176,6 +176,25 @@ app.include_router(
tags=["安全测试"]
)
app.include_router(
security_erp.router,
prefix="/api/security",
tags=["安全测试-ERP配置"]
)
app.include_router(
security_erp_upload.router,
prefix="/api/security",
tags=["安全测试-ERP上传"]
)
# 安全测试-ERP任务创建
app.include_router(
security_erp_task.router,
prefix="/api/security",
tags=["安全测试-ERP任务创建"]
)
app.include_router(
ai_locator.router,
prefix="/api/ai",
......@@ -251,6 +270,13 @@ app.include_router(
tags=["功能测试报告"]
)
# 接口测试路由
app.include_router(
api_test.router,
prefix="/api/api-test",
tags=["接口测试"]
)
# ==================== 根路径 ====================
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp.py
模块描述:安全测试 ERP 配置 API 路由,提供获取/更新 ERP 连接配置接口
复用功能测试报告的 erp_configs 表与环境变量配置,共用同一套 ERP 配置
作者:czj
创建日期:2026-08-13
最后修改:2026-08-13
"""
import logging
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.config import settings
from app.models.erp_config import ErpConfig
from app.schemas.security_erp import (
SecurityErpConfigResponse,
SecurityErpConfigUpdateRequest,
)
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/erp-config", response_model=SecurityErpConfigResponse, summary="获取安全测试ERP配置")
async def get_erp_config(db: AsyncSession = Depends(get_db)):
"""
获取安全测试 ERP 配置
优先从数据库读取,若数据库无记录则回退到环境变量默认值。
Returns:
SecurityErpConfigResponse: 当前 ERP 配置
"""
result = await db.execute(select(ErpConfig).where(ErpConfig.id == 1))
config = result.scalar_one_or_none()
if config:
# 数据库有记录,但空字段回退到环境变量默认值(保证配置自动填充)
return SecurityErpConfigResponse(
api_key=config.api_key or settings.ERP_API_KEY or "",
base_url=config.base_url or settings.ERP_BASE_URL or "",
developtesting_id=config.developtesting_id or settings.ERP_DEVELOPTESTING_ID or 0,
type_id=config.type_id or settings.ERP_TYPE_ID or 2,
image_url_prefix=config.image_url_prefix or settings.ERP_IMAGE_URL_PREFIX or "",
)
# 数据库无记录,回退到环境变量
return SecurityErpConfigResponse(
api_key=settings.ERP_API_KEY or "",
base_url=settings.ERP_BASE_URL or "",
developtesting_id=settings.ERP_DEVELOPTESTING_ID or 0,
type_id=settings.ERP_TYPE_ID or 2,
image_url_prefix=settings.ERP_IMAGE_URL_PREFIX or "",
)
@router.put("/erp-config", response_model=SecurityErpConfigResponse, summary="更新安全测试ERP配置")
async def update_erp_config(request: SecurityErpConfigUpdateRequest, db: AsyncSession = Depends(get_db)):
"""
更新安全测试 ERP 配置(持久化到数据库)
更新后的配置会持久化到数据库,服务重启后依然有效,
与功能测试报告模块共享同一套 ERP 配置。
Args:
request: 更新请求(字段可为空,仅更新传入字段)
Returns:
SecurityErpConfigResponse: 更新后的配置
"""
result = await db.execute(select(ErpConfig).where(ErpConfig.id == 1))
config = result.scalar_one_or_none()
if not config:
# 新记录:先用环境变量默认值填充,再用请求值覆盖
config = ErpConfig(
id=1,
api_key=settings.ERP_API_KEY or "",
base_url=settings.ERP_BASE_URL or "",
developtesting_id=settings.ERP_DEVELOPTESTING_ID or 0,
type_id=settings.ERP_TYPE_ID or 2,
image_url_prefix=settings.ERP_IMAGE_URL_PREFIX or "",
)
db.add(config)
# 更新字段(同时同步运行时环境变量,保证本次进程内立即生效)
if request.api_key is not None:
config.api_key = request.api_key
settings.ERP_API_KEY = request.api_key
if request.base_url is not None:
config.base_url = request.base_url
settings.ERP_BASE_URL = request.base_url
if request.developtesting_id is not None:
config.developtesting_id = request.developtesting_id
settings.ERP_DEVELOPTESTING_ID = request.developtesting_id
if request.type_id is not None:
config.type_id = request.type_id
settings.ERP_TYPE_ID = request.type_id
if request.image_url_prefix is not None:
config.image_url_prefix = request.image_url_prefix
settings.ERP_IMAGE_URL_PREFIX = request.image_url_prefix
await db.commit()
await db.refresh(config)
logger.info("安全测试ERP配置已更新(持久化到数据库,与功能测试报告共享)")
return SecurityErpConfigResponse(
api_key=config.api_key or settings.ERP_API_KEY or "",
base_url=config.base_url or settings.ERP_BASE_URL or "",
developtesting_id=config.developtesting_id or settings.ERP_DEVELOPTESTING_ID or 0,
type_id=config.type_id or settings.ERP_TYPE_ID or 2,
image_url_prefix=config.image_url_prefix or settings.ERP_IMAGE_URL_PREFIX or "",
)
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp_task.py
模块描述:安全测试 ERP 任务创建 API 路由,提供任务创建预览和创建接口
参考实现:ApiSecurityTest/utils/task_creator.py + report_parser.py
作者:czj
创建日期:2026-08-17
最后修改:2026-08-17
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.security_erp_task import (
TaskPreviewResponse,
CreateTaskRequest,
CreateTaskResponse,
)
from app.services.security_erp_task_service import SecurityErpTaskService
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"/executions/{execution_id}/task-preview",
response_model=TaskPreviewResponse,
summary="获取安全测试ERP任务创建预览",
)
async def get_task_preview(
execution_id: str,
db: AsyncSession = Depends(get_db),
):
"""
获取安全测试 ERP 任务创建预览数据
在创建任务前调用,返回:
- 报告摘要(测试目标、时间、漏洞统计)
- 自动生成的任务名称
- 自动计算的紧急程度
- ERP 基础数据(任务类型列表、状态列表、人员列表)
- 搜索到的关联需求
- 默认任务配置
前端基于此数据展示对话框,允许用户修改参数后创建任务。
"""
try:
service = SecurityErpTaskService(db)
preview = await service.get_task_preview(execution_id)
return preview
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"获取任务预览失败: {e}")
raise HTTPException(status_code=500, detail=f"获取任务预览失败: {str(e)}")
@router.post(
"/executions/{execution_id}/create-task",
response_model=CreateTaskResponse,
summary="创建安全测试ERP任务",
)
async def create_task(
execution_id: str,
request: CreateTaskRequest,
db: AsyncSession = Depends(get_db),
):
"""
创建安全测试 ERP 任务
基于已生成的报告,构建任务数据并调用 ERP 接口创建任务。
支持自定义任务参数(类型、状态、责任人、紧急程度、截止时间等)。
Args:
execution_id: 安全测试执行 ID
request: 任务配置参数
Returns:
CreateTaskResponse: 创建结果
"""
try:
service = SecurityErpTaskService(db)
result = await service.create_task(
execution_id=execution_id,
config=request.model_dump(),
)
return result
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(f"创建 ERP 任务失败: {e}")
raise HTTPException(status_code=500, detail=f"创建 ERP 任务失败: {str(e)}")
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp_upload.py
模块描述:安全测试 ERP 上传 API 路由,提供安全测试报告上传到 ERP 测试单/项目资料/协作文档的接口
作者:czj
创建日期:2026-08-17
最后修改:2026-08-17
"""
import logging
import os
from pathlib import Path
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.config import settings
from app.services.erp_service import ErpService
from app.services.security_report_service import SecurityReportService
logger = logging.getLogger(__name__)
router = APIRouter()
# ==================== Pydantic 模型 ====================
class UploadToErpRequest(BaseModel):
"""上传安全报告到 ERP 请求"""
developtesting_id: Optional[int] = Field(None, description="测试单ID,默认使用系统配置")
copyuser_names: List[str] = Field(default_factory=list, description="抄送人姓名列表")
createuser_name: Optional[str] = Field(None, description="创建人姓名")
class UploadToErpResponse(BaseModel):
"""上传到 ERP 响应"""
success: bool = Field(..., description="是否成功")
report_id: Optional[int] = Field(None, description="ERP报告ID")
message: str = Field(..., description="提示信息")
class UploadToProjectRequest(BaseModel):
"""上传文件到 ERP 项目资料请求"""
project_id: int = Field(..., description="关联的项目ID")
name: Optional[str] = Field(None, description="文件名称(默认取文件名)")
class UploadToCooperationRequest(BaseModel):
"""上传文件到 ERP 协作文档请求"""
project_id: int = Field(..., description="关联的项目ID")
name: Optional[str] = Field(None, description="文件名称(默认取文件名)")
group_id: Optional[int] = Field(None, description="分组ID(可选)")
class UploadFileToErpResponse(BaseModel):
"""上传文件到 ERP(项目资料/协作文档)响应"""
success: bool = Field(..., description="是否成功")
file_id: Optional[int] = Field(None, description="ERP文件ID")
message: str = Field(..., description="提示信息")
# ==================== 辅助函数 ====================
def get_erp_service() -> ErpService:
"""获取 ERP 服务实例"""
return ErpService(
api_key=settings.ERP_API_KEY,
base_url=settings.ERP_BASE_URL,
developtesting_id=settings.ERP_DEVELOPTESTING_ID,
type_id=settings.ERP_TYPE_ID,
image_url_prefix=settings.ERP_IMAGE_URL_PREFIX,
)
async def _generate_and_convert_report(
execution_id: str,
db: AsyncSession,
) -> str:
"""
生成安全测试报告并转换为 DOCX 格式
Args:
execution_id: 执行记录ID
db: 数据库会话
Returns:
str: 生成的 DOCX 文件路径
Raises:
ValueError: 报告生成失败
"""
report_service = SecurityReportService()
# 1. 生成 Markdown 报告
md_path = await report_service.generate_report(execution_id, db)
if not md_path or not os.path.exists(md_path):
raise ValueError("安全测试报告生成失败")
# 2. 将 Markdown 转换为 DOCX
docx_path = _convert_markdown_to_docx(md_path)
if not docx_path or not os.path.exists(docx_path):
raise ValueError("报告转换为 DOCX 失败")
logger.info(f"安全报告已转换为 DOCX: {docx_path}")
return docx_path
def _convert_markdown_to_docx(md_path: str) -> str:
"""
将 Markdown 文件转换为 DOCX 文件
Args:
md_path: Markdown 文件路径
Returns:
str: DOCX 文件路径
"""
from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
# 读取 Markdown 内容
with open(md_path, "r", encoding="utf-8") as f:
md_content = f.read()
# 生成 DOCX 路径(与 MD 同目录)
md_file = Path(md_path)
docx_path = str(md_file.with_suffix(".docx"))
doc = Document()
# 设置默认字体
style = doc.styles["Normal"]
font = style.font
font.name = "Microsoft YaHei"
font.size = Pt(10.5)
lines = md_content.split("\n")
i = 0
in_code_block = False
code_buffer = []
in_table = False
table_headers = []
table_rows = []
table_separator_seen = False
while i < len(lines):
line = lines[i]
# 代码块处理
if line.strip().startswith("```"):
if in_code_block:
# 结束代码块,写入为段落
code_text = "\n".join(code_buffer)
p = doc.add_paragraph()
run = p.add_run(code_text)
run.font.size = Pt(9)
run.font.name = "Courier New"
p.paragraph_format.left_indent = Inches(0.3)
code_buffer = []
in_code_block = False
i += 1
continue
else:
in_code_block = True
i += 1
continue
if in_code_block:
code_buffer.append(line)
i += 1
continue
# 表格处理
if "|" in line and line.strip().startswith("|"):
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if not in_table:
# 开始新表格
in_table = True
table_headers = cells
table_separator_seen = False
table_rows = []
elif not table_separator_seen:
# 分隔行(|---|---|),跳过
table_separator_seen = True
else:
# 数据行
table_rows.append(cells)
i += 1
continue
else:
# 结束表格
if in_table and table_headers:
_add_table_to_doc(doc, table_headers, table_rows)
in_table = False
table_headers = []
table_rows = []
# 空行
if not line.strip():
# 如果前一个段落不是空段落,添加一个空段落
if doc.paragraphs and doc.paragraphs[-1].text.strip():
doc.add_paragraph("")
i += 1
continue
# 标题
if line.startswith("## "):
text = line[3:].strip()
_add_heading(doc, text, 2)
elif line.startswith("### "):
text = line[4:].strip()
_add_heading(doc, text, 3)
elif line.startswith("#### "):
text = line[5:].strip()
_add_heading(doc, text, 4)
elif line.startswith("# "):
text = line[2:].strip()
_add_heading(doc, text, 1)
elif line.startswith("> "):
# 引用
text = line[2:].strip()
p = doc.add_paragraph()
run = p.add_run(text)
run.font.italic = True
run.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
p.paragraph_format.left_indent = Inches(0.3)
elif line.startswith("- "):
# 无序列表
text = line[2:].strip()
p = doc.add_paragraph(style="List Bullet")
p.add_run(text)
elif line.startswith("**") and line.endswith("**"):
# 粗体行
text = line.strip("*")
p = doc.add_paragraph()
run = p.add_run(text)
run.bold = True
else:
# 普通段落(处理内联粗体)
p = doc.add_paragraph()
_add_inline_text(p, line)
i += 1
# 处理末尾的表格
if in_table and table_headers:
_add_table_to_doc(doc, table_headers, table_rows)
doc.save(docx_path)
logger.info(f"Markdown 已转换为 DOCX: {docx_path}")
return docx_path
def _add_heading(doc, text: str, level: int):
"""添加标题"""
heading = doc.add_heading(text, level=level)
for run in heading.runs:
run.font.name = "Microsoft YaHei"
def _add_inline_text(p, text: str):
"""添加带内联格式的文本(支持 **粗体**)"""
parts = text.split("**")
for idx, part in enumerate(parts):
if not part:
continue
run = p.add_run(part)
if idx % 2 == 1:
run.bold = True
def _add_table_to_doc(doc, headers: list, rows: list):
"""添加表格"""
if not rows:
return
table = doc.add_table(rows=1 + len(rows), cols=len(headers))
table.style = "Light Grid Accent 1"
# 表头
for j, h in enumerate(headers):
cell = table.rows[0].cells[j]
cell.text = h
for p in cell.paragraphs:
for run in p.runs:
run.bold = True
run.font.size = Pt(9)
# 数据行
for i, row in enumerate(rows):
for j, cell_text in enumerate(row):
if j < len(headers):
cell = table.rows[i + 1].cells[j]
cell.text = cell_text
for p in cell.paragraphs:
for run in p.runs:
run.font.size = Pt(9)
doc.add_paragraph("") # 表后空行
# ==================== API 路由 ====================
@router.post(
"/executions/{execution_id}/upload-to-erp",
response_model=UploadToErpResponse,
summary="上传安全测试报告到ERP",
)
async def upload_report_to_erp(
execution_id: str,
request: UploadToErpRequest,
db: AsyncSession = Depends(get_db),
):
"""
将安全测试报告上传到 ERP 系统
- 自动生成安全测试报告(Markdown)
- 转换为 DOCX 格式
- 上传到 ERP 测试单
- 可选:匹配抄送人姓名、创建人姓名
"""
try:
# 1. 生成报告并转换为 DOCX
docx_path = await _generate_and_convert_report(execution_id, db)
except ValueError as e:
raise HTTPException(status_code=500, detail=str(e))
# 2. 处理抄送人
copyuser_ids = []
erp_service = get_erp_service()
if request.copyuser_names:
stuff_list = erp_service.get_stuff_list()
if stuff_list:
copyuser_ids = erp_service.match_names_to_ids(request.copyuser_names, stuff_list)
# 3. 处理创建人
createuser_id = None
if request.createuser_name:
stuff_list = erp_service.get_stuff_list()
if stuff_list:
matched = erp_service.match_names_to_ids([request.createuser_name], stuff_list)
if matched:
createuser_id = matched[0]
# 4. 上传到 ERP
success = erp_service.upload_report_to_erp(
file_path=docx_path,
developtesting_id=request.developtesting_id,
copyuser_list=copyuser_ids,
createuser_id=createuser_id,
)
if success:
return UploadToErpResponse(success=True, message="报告上传ERP成功")
else:
return UploadToErpResponse(success=False, message="报告上传ERP失败,请查看日志")
@router.post(
"/executions/{execution_id}/upload-to-project",
response_model=UploadFileToErpResponse,
summary="上传安全报告到 ERP 项目资料",
)
async def upload_to_project(
execution_id: str,
request: UploadToProjectRequest,
db: AsyncSession = Depends(get_db),
):
"""
将安全测试报告上传到 ERP 项目资料(仅归档,不创建协作文档)
"""
try:
docx_path = await _generate_and_convert_report(execution_id, db)
except ValueError as e:
raise HTTPException(status_code=500, detail=str(e))
name = request.name or os.path.basename(docx_path)
erp_service = get_erp_service()
file_info = erp_service.upload_file_to_project(docx_path, request.project_id, name)
if not file_info:
return UploadFileToErpResponse(success=False, message="项目资料上传失败,请查看日志")
return UploadFileToErpResponse(
success=True,
file_id=file_info.get("id"),
message=f"项目资料上传成功,文件ID: {file_info.get('id')}",
)
@router.post(
"/executions/{execution_id}/upload-to-cooperation",
response_model=UploadFileToErpResponse,
summary="上传安全报告到 ERP 协作文档",
)
async def upload_to_cooperation(
execution_id: str,
request: UploadToCooperationRequest,
db: AsyncSession = Depends(get_db),
):
"""
将安全测试报告上传到 ERP 协作文档(创建 OnlyOffice 在线协作文档)
"""
try:
docx_path = await _generate_and_convert_report(execution_id, db)
except ValueError as e:
raise HTTPException(status_code=500, detail=str(e))
name = request.name or os.path.basename(docx_path)
erp_service = get_erp_service()
file_info = erp_service.upload_file_to_cooperation(
docx_path, request.project_id, name, group_id=request.group_id
)
if not file_info:
return UploadFileToErpResponse(success=False, message="协作文档上传失败,请查看日志")
return UploadFileToErpResponse(
success=True,
file_id=file_info.get("id"),
message=f"协作文档上传成功,文档ID: {file_info.get('id')}",
)
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp.py
模块描述:安全测试 ERP 配置 Pydantic 数据模型
作者:czj
创建日期:2026-08-13
最后修改:2026-08-13
"""
from typing import Optional
from pydantic import BaseModel, Field
class SecurityErpConfigResponse(BaseModel):
"""安全测试 ERP 配置响应"""
api_key: str = Field("", description="API密钥")
base_url: str = Field("", description="基础URL")
developtesting_id: int = Field(0, description="默认测试单ID")
type_id: int = Field(2, description="报告类型ID")
image_url_prefix: str = Field("", description="图片URL前缀")
class SecurityErpConfigUpdateRequest(BaseModel):
"""安全测试 ERP 配置更新请求"""
api_key: Optional[str] = Field(None, description="API密钥")
base_url: Optional[str] = Field(None, description="基础URL")
developtesting_id: Optional[int] = Field(None, description="默认测试单ID")
type_id: Optional[int] = Field(None, description="报告类型ID")
image_url_prefix: Optional[str] = Field(None, description="图片URL前缀")
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp_task.py
模块描述:安全测试 ERP 任务创建 Pydantic Schema
作者:czj
创建日期:2026-08-17
最后修改:2026-08-17
"""
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
class TaskStaffInfo(BaseModel):
"""ERP 人员信息"""
id: int = Field(..., description="人员ID")
name: str = Field("", description="人员姓名")
class TaskTypeInfo(BaseModel):
"""ERP 任务类型"""
id: int = Field(..., description="类型ID")
name: str = Field("", description="类型名称")
class TaskStatusInfo(BaseModel):
"""ERP 任务状态"""
id: int = Field(..., description="状态ID")
name: str = Field("", description="状态名称")
class RequirementInfo(BaseModel):
"""ERP 需求信息"""
id: int = Field(..., description="需求ID")
name: str = Field("", description="需求名称")
class TaskDefaultConfig(BaseModel):
"""任务默认配置"""
type_id: int = Field(3, description="任务类型ID")
status_id: int = Field(1, description="任务状态ID")
level: int = Field(4, description="紧急程度(1-5)")
excepthour: int = Field(21, description="期望工时(小时)")
user_id: int = Field(101, description="责任人ID")
createuser_id: int = Field(441, description="创建人ID")
deadline_days: int = Field(7, description="截止天数")
class ReportSummary(BaseModel):
"""报告摘要"""
target: str = Field("", description="测试目标URL")
server_ip: str = Field("", description="服务器IP")
start_time: str = Field("", description="测试开始时间")
end_time: str = Field("", description="测试结束时间")
total: int = Field(0, description="测试用例总数")
high: int = Field(0, description="高危漏洞数")
medium: int = Field(0, description="中危漏洞数")
low: int = Field(0, description="低危漏洞数")
info: int = Field(0, description="信息类数")
safe: int = Field(0, description="安全项数")
class TaskPreviewResponse(BaseModel):
"""任务创建预览响应"""
summary: ReportSummary = Field(..., description="报告摘要")
task_name: str = Field("", description="自动生成的任务名称")
calculated_level: int = Field(4, description="自动计算的紧急程度")
task_types: List[TaskTypeInfo] = Field(default_factory=list, description="任务类型列表")
task_statuses: List[TaskStatusInfo] = Field(default_factory=list, description="任务状态列表")
staff_list: List[TaskStaffInfo] = Field(default_factory=list, description="人员列表")
requirement: Optional[RequirementInfo] = Field(None, description="关联需求")
default_config: TaskDefaultConfig = Field(default_factory=TaskDefaultConfig, description="默认配置")
class CreateTaskRequest(BaseModel):
"""创建 ERP 任务请求"""
type_id: int = Field(3, description="任务类型ID")
status_id: int = Field(1, description="任务状态ID")
level: int = Field(4, description="紧急程度(1-5)")
excepthour: int = Field(21, description="期望工时(小时)")
user_id: int = Field(101, description="责任人ID")
createuser_id: int = Field(441, description="创建人ID")
deadline_days: int = Field(7, description="截止天数(从当前时间+N天)")
develop_id: Optional[int] = Field(None, description="关联需求ID(可选)")
task_name: Optional[str] = Field(None, description="任务名称(可选,自动生成)")
class CreateTaskResponse(BaseModel):
"""创建 ERP 任务响应"""
success: bool = Field(..., description="是否成功")
task_id: Optional[int] = Field(None, description="ERP任务ID")
task_name: str = Field("", description="任务名称")
message: str = Field("", description="提示信息")
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:erp_task_client.py
模块描述:ERP 任务 API 客户端,封装任务类型/状态/人员/需求/任务创建接口
含缓存和重试机制,参考实现:ApiSecurityTest/utils/erp_client.py
作者:czj
创建日期:2026-08-17
最后修改:2026-08-17
"""
import logging
import time
from typing import Dict, List, Optional, Any
import requests
logger = logging.getLogger(__name__)
# API 端点常量
TASK_TYPE_ENDPOINT = "/openclaw/tasktype"
TASK_STATUS_ENDPOINT = "/openclaw/taskstatus"
STAFF_ENDPOINT = "/openclaw/stuff"
DEVELOP_ENDPOINT = "/openclaw/develop"
TASK_ENDPOINT = "/openclaw/task"
class ERPTaskClient:
"""
ERP 任务 API 客户端
缓存策略:
- 任务类型、任务状态、人员列表缓存 1 小时
- 需求列表、任务创建不缓存
"""
def __init__(
self,
base_url: str,
api_key: str,
timeout: int = 30,
max_retries: int = 3,
retry_interval: int = 2,
cache_duration: int = 3600,
):
"""
Args:
base_url: ERP 基础 URL(如 https://office.ubainsyun.com:5082/api/uerp)
api_key: ERP API 密钥
timeout: 请求超时时间(秒)
max_retries: 最大重试次数
retry_interval: 重试间隔(秒)
cache_duration: 基础数据缓存时长(秒)
"""
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
self.max_retries = max_retries
self.retry_interval = retry_interval
self.cache_duration = cache_duration
# 缓存
self._cache: Dict[str, Any] = {}
self._cache_timestamps: Dict[str, float] = {}
# ==================== 公开 API ====================
def get_task_types(self) -> Optional[List[Dict]]:
"""
获取任务类型列表
Returns:
[{"id": 1, "name": "UI设计"}, ...],失败返回 None
"""
result = self._get_with_cache("task_types", lambda: self._make_request("GET", TASK_TYPE_ENDPOINT))
if result and isinstance(result, dict) and "data" in result:
return result["data"]
return result
def get_task_statuses(self) -> Optional[List[Dict]]:
"""
获取任务状态列表
Returns:
[{"id": 1, "name": "新建"}, ...],失败返回 None
"""
result = self._get_with_cache("task_statuses", lambda: self._make_request("GET", TASK_STATUS_ENDPOINT))
if result and isinstance(result, dict) and "data" in result:
return result["data"]
return result
def get_staff_list(self) -> Optional[List[Dict]]:
"""
获取人员列表
Returns:
[{"id": 1, "name": "超管员", ...}, ...],失败返回 None
"""
result = self._get_with_cache("staff_list", lambda: self._make_request("GET", STAFF_ENDPOINT))
if result and isinstance(result, dict) and "data" in result:
return result["data"]
return result
def search_develop(
self,
search_str: str,
page_num: int = 1,
page_size: int = 10,
) -> Optional[List[Dict]]:
"""
搜索需求列表
Args:
search_str: 搜索关键词
page_num: 页码
page_size: 每页数量
Returns:
需求列表,格式 [{"id": 128, "name": "需求名称", ...}, ...]
失败返回 None
"""
params = {"pageNum": page_num, "pageSize": page_size}
if search_str:
params["search_str"] = search_str
result = self._make_request("GET", DEVELOP_ENDPOINT, params=params)
if result is None:
return None
# 响应可能是 dict 带 data 字段,也可能是直接列表
if isinstance(result, dict) and "data" in result:
data = result["data"]
if isinstance(data, list):
return data
elif isinstance(result, list):
return result
return None
def create_task(self, task_data: Dict) -> Optional[Dict]:
"""
创建 ERP 任务
Args:
task_data: 任务数据字典
Returns:
ERP 响应结果,失败返回 None
"""
return self._make_request("POST", TASK_ENDPOINT, json=task_data)
def clear_cache(self):
"""清空所有缓存"""
self._cache.clear()
self._cache_timestamps.clear()
logger.info("ERP 任务客户端缓存已清空")
# ==================== 内部方法 ====================
def _get_with_cache(self, cache_key: str, fetch_func) -> Optional[Any]:
"""
带缓存的数据获取
Args:
cache_key: 缓存键名
fetch_func: 获取数据的函数
Returns:
缓存或新获取的数据
"""
now = time.time()
if cache_key in self._cache:
timestamp = self._cache_timestamps.get(cache_key, 0)
if now - timestamp < self.cache_duration:
logger.debug(f"使用缓存数据: {cache_key}")
return self._cache[cache_key]
data = fetch_func()
if data is not None:
self._cache[cache_key] = data
self._cache_timestamps[cache_key] = now
return data
def _make_request(
self,
method: str,
endpoint: str,
**kwargs,
) -> Optional[Dict]:
"""
统一请求方法(含重试机制)
Args:
method: HTTP 方法(GET/POST)
endpoint: API 端点路径
**kwargs: 其他请求参数(params, json 等)
Returns:
响应数据字典,失败返回 None
"""
url = f"{self.base_url}{endpoint}"
headers = {"X-Api-Key": self.api_key, "Content-Type": "application/json"}
# 分离参数
json_data = kwargs.pop("json", None)
params = kwargs.pop("params", None)
for attempt in range(self.max_retries):
try:
logger.info(f"ERP API 请求: {method} {endpoint}")
if method == "GET":
response = requests.get(
url, headers=headers, params=params, timeout=self.timeout
)
elif method == "POST":
request_kwargs = {}
if json_data:
request_kwargs["json"] = json_data
if params:
request_kwargs["params"] = params
response = requests.post(
url, headers=headers, timeout=self.timeout, **request_kwargs
)
else:
logger.error(f"不支持的 HTTP 方法: {method}")
return None
if response.status_code == 200:
result = response.json()
if result.get("success") == 1:
logger.info(f"✓ ERP API 请求成功: {endpoint}")
return result
else:
error_code = result.get("error", 0)
msg = result.get("msg", "未知错误")
if 400 <= error_code < 500:
logger.error(f"✗ ERP API 客户端错误 ({error_code}): {msg}")
return None
if attempt < self.max_retries - 1:
logger.warning(
f"ERP API 服务端错误,{self.retry_interval}s 后重试 ({attempt+1}/{self.max_retries}): {msg}"
)
time.sleep(self.retry_interval)
else:
logger.error(f"✗ ERP API 请求失败 (已达最大重试次数): {msg}")
return None
else:
if attempt < self.max_retries - 1 and response.status_code >= 500:
logger.warning(
f"ERP API HTTP {response.status_code},{self.retry_interval}s 后重试 ({attempt+1}/{self.max_retries})"
)
time.sleep(self.retry_interval)
else:
logger.error(f"✗ ERP API HTTP 错误: {response.status_code}")
return None
except requests.exceptions.Timeout:
if attempt < self.max_retries - 1:
logger.warning(
f"ERP API 请求超时,{self.retry_interval}s 后重试 ({attempt+1}/{self.max_retries})"
)
time.sleep(self.retry_interval)
else:
logger.error("✗ ERP API 请求超时 (已达最大重试次数)")
return None
except requests.exceptions.RequestException as e:
logger.error(f"✗ ERP API 请求异常: {e}")
return None
except Exception as e:
logger.error(f"✗ ERP API 未知异常: {e}")
return None
return None
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_erp_task_service.py
模块描述:安全测试 ERP 任务创建服务,解析报告摘要、获取基础数据、构建任务数据、创建 ERP 任务
参考实现:ApiSecurityTest/utils/task_creator.py + report_parser.py
作者:czj
创建日期:2026-08-17
最后修改:2026-08-17
"""
import logging
import re
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.erp_config import ErpConfig
from app.services.erp_task_client import ERPTaskClient
from app.services.security_report_service import SecurityReportService
logger = logging.getLogger(__name__)
# ==================== 默认任务配置 ====================
# 对齐参考实现 config.yaml 的 erp.task 节点
DEFAULT_TASK_CONFIG = {
"type_id": 3, # 任务类型:3 = 后端开发
"status_id": 1, # 任务状态:1 = 新建
"level": 4, # 默认紧急程度:4 = 常规处理
"excepthour": 21, # 默认期望工时(小时)
"user_id": 101, # 默认责任人 ID:李立键
"createuser_id": 441, # 默认创建人 ID:陈泽键
"deadline_days": 7, # 默认截止天数
"develop_search_str": "【新统一平台】公司内部安全测试扫描及修复验证",
}
# 任务类型/状态 ID → 名称映射(用于日志展示,从 ERP 实时获取优先)
LEVEL_NAMES = {
1: "低优先级",
2: "正常优先级",
3: "中优先级",
4: "高优先级",
5: "最高优先级",
}
URGENCY_DESC = {
5: "【最高优先级】有高危漏洞,需立即处理",
4: "【高优先级】有中危漏洞,建议优先处理",
3: "【中优先级】有低危漏洞,建议按计划修复",
2: "【正常优先级】仅有信息类问题",
1: "【低优先级】无安全漏洞",
}
class SecurityErpTaskService:
"""
安全测试 ERP 任务创建服务
流程:
1. 生成报告 → 解析摘要
2. 获取 ERP 基础数据(任务类型/状态/人员)
3. 搜索关联需求
4. 构建任务数据(名称/描述/截止时间)
5. 创建 ERP 任务
"""
def __init__(self, db: AsyncSession):
self.db = db
# ==================== 对外 API ====================
async def get_task_preview(self, execution_id: str) -> dict:
"""
获取任务创建预览数据
生成报告并解析摘要、获取基础数据、搜索需求,
供前端在创建任务前展示和配置。
Args:
execution_id: 安全测试执行 ID
Returns:
dict: 预览数据(报告摘要 + 基础数据 + 默认配置)
"""
# 1. 生成报告并解析摘要
markdown = await self._get_report_markdown(execution_id)
summary = parse_report_summary(markdown)
server_ip = summary.get("server_ip") or self._extract_ip(summary.get("target", ""))
# 2. 基础数据
client = await self._get_erp_client()
task_types = client.get_task_types() or []
task_statuses = client.get_task_statuses() or []
staff_list = client.get_staff_list() or []
# 3. 搜索需求
requirement = self._search_requirement(client)
# 4. 计算紧急程度
calculated_level = calculate_urgency_level(summary)
task_name = generate_task_name(server_ip)
return {
"summary": summary,
"task_name": task_name,
"calculated_level": calculated_level,
"task_types": task_types,
"task_statuses": task_statuses,
"staff_list": staff_list[:200], # 限制返回数量,避免前端下拉过大
"requirement": requirement,
"default_config": {
"type_id": DEFAULT_TASK_CONFIG["type_id"],
"status_id": DEFAULT_TASK_CONFIG["status_id"],
"level": calculated_level if calculated_level else DEFAULT_TASK_CONFIG["level"],
"excepthour": DEFAULT_TASK_CONFIG["excepthour"],
"user_id": DEFAULT_TASK_CONFIG["user_id"],
"createuser_id": DEFAULT_TASK_CONFIG["createuser_id"],
"deadline_days": DEFAULT_TASK_CONFIG["deadline_days"],
},
}
async def create_task(self, execution_id: str, config: Dict[str, Any]) -> dict:
"""
创建 ERP 任务
Args:
execution_id: 安全测试执行 ID
config: 任务配置(type_id/status_id/level/excepthour/user_id/createuser_id/deadline_days/develop_id)
Returns:
dict: 创建结果(success/task_id/task_name/message)
"""
# 1. 生成报告并解析摘要
markdown = await self._get_report_markdown(execution_id)
summary = parse_report_summary(markdown)
server_ip = summary.get("server_ip") or self._extract_ip(summary.get("target", ""))
# 2. 解析漏洞(获取高危漏洞名称列表用于描述)
high_risk_list = parse_high_risk_vulnerabilities(markdown)
medium_count = summary.get("medium", 0)
# 3. 获取 ERP 客户端
client = await self._get_erp_client()
# 4. 构建任务数据
task_data = build_task_data(
summary=summary,
server_ip=server_ip,
high_risk_list=high_risk_list,
medium_count=medium_count,
config=config,
)
# 5. 创建任务
result = client.create_task(task_data)
if result and result.get("success") == 1:
data = result.get("data", {})
task_id = data.get("create")
task_name = task_data.get("name", "")
logger.info(f"✓ ERP 任务创建成功: ID={task_id}, 名称={task_name}")
return {
"success": True,
"task_id": task_id,
"task_name": task_name,
"message": "ERP任务创建成功",
}
else:
logger.error("✗ ERP 任务创建失败")
return {
"success": False,
"task_id": None,
"task_name": task_data.get("name", ""),
"message": "ERP任务创建失败,请检查 ERP 配置和网络连接",
}
# ==================== 报告处理 ====================
async def _get_report_markdown(self, execution_id: str) -> str:
"""
生成报告并返回 Markdown 内容
Args:
execution_id: 安全测试执行 ID
Returns:
str: Markdown 报告内容
"""
report_service = SecurityReportService()
return await report_service.get_report_content(execution_id, self.db)
# ==================== ERP 客户端 ====================
async def _get_erp_client(self) -> ERPTaskClient:
"""
从数据库读取 ERP 配置,创建任务客户端
Returns:
ERPTaskClient: ERP 任务客户端
"""
# 读取 ERP 配置(复用 erp_configs 表)
result = await self.db.execute(select(ErpConfig).where(ErpConfig.id == 1))
config = result.scalar_one_or_none()
base_url = (config.base_url if config and config.base_url else "") or settings.ERP_BASE_URL or ""
api_key = (config.api_key if config and config.api_key else "") or settings.ERP_API_KEY or ""
return ERPTaskClient(
base_url=base_url,
api_key=api_key,
timeout=30,
max_retries=3,
retry_interval=2,
cache_duration=3600,
)
def _search_requirement(self, client: ERPTaskClient) -> Optional[Dict]:
"""
搜索关联需求
Args:
client: ERP 任务客户端
Returns:
需求信息字典,未找到返回 None
"""
search_str = DEFAULT_TASK_CONFIG.get("develop_search_str", "")
if not search_str:
return None
try:
data_list = client.search_develop(search_str=search_str, page_num=1, page_size=10)
if data_list and len(data_list) > 0:
req = data_list[0]
logger.info(f"✓ 找到需求: {req.get('name', '')} (ID: {req.get('id', '')})")
return req
except Exception as e:
logger.error(f"搜索需求异常: {e}")
logger.warning("未找到关联需求,任务将不关联需求")
return None
# ==================== 工具方法 ====================
@staticmethod
def _extract_ip(url: str) -> str:
"""从 URL 提取 IP 地址"""
if not url:
return ""
ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", url)
return ip_match.group(1) if ip_match else ""
# ==================== 模块级函数(可独立测试) ====================
def parse_report_summary(markdown: str) -> Dict[str, Any]:
"""
从 Markdown 安全测试报告提取摘要
对齐参考实现 report_parser.py 的正则格式。
Args:
markdown: 报告 Markdown 内容
Returns:
dict: 摘要数据(target/server_ip/start_time/end_time/total/high/medium/low/info/safe)
"""
summary = {
"target": "",
"server_ip": "",
"start_time": "",
"end_time": "",
"total": 0,
"high": 0,
"medium": 0,
"low": 0,
"info": 0,
"safe": 0,
}
if not markdown:
return summary
# 测试目标(> **测试目标**:https://192.168.5.44)
target_match = re.search(r"> \*\*测试目标\*\*[::]\s*(https?://[^\s\n]+)", markdown)
if target_match:
summary["target"] = target_match.group(1).strip()
# 服务器 IP(从目标 URL 提取)
ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", summary["target"])
if ip_match:
summary["server_ip"] = ip_match.group(1)
# 测试时间(| 测试时间 | 2026-08-17 10:00:00 ~ 2026-08-17 11:30:00 |)
time_match = re.search(r"\|\s*测试时间\s*\|\s*(.+?~.+?)\s*\|", markdown)
if time_match:
time_range = time_match.group(1).strip()
times = re.split(r"\s*~\s*", time_range)
if len(times) == 2:
summary["start_time"] = times[0].strip()
summary["end_time"] = times[1].strip()
# 测试用例总数(| 测试用例总数 | **145** |)
total_match = re.search(r"\|\s*测试用例总数\s*\|\s*\*\*(\d+)\*\*", markdown)
if total_match:
summary["total"] = int(total_match.group(1))
# 各等级漏洞数
summary["high"] = _parse_count(markdown, r"\|\s*🔴 高危漏洞\s*\|\s*\*\*(\d+)\*\*")
summary["medium"] = _parse_count(markdown, r"\|\s*🟠 中危漏洞\s*\|\s*\*\*(\d+)\*\*")
summary["low"] = _parse_count(markdown, r"\|\s*🟡 低危漏洞\s*\|\s*\*\*(\d+)\*\*")
summary["info"] = _parse_count(markdown, r"\|\s*🔵 信息类\s*\|\s*\*\*(\d+)\*\*")
summary["safe"] = _parse_count(markdown, r"\|\s*🟢 已验证安全\s*\|\s*\*\*(\d+)\*\*")
logger.info(
f"报告摘要解析: 目标={summary['target']}, 总数={summary['total']}, "
f"高危={summary['high']}, 中危={summary['medium']}, 低危={summary['low']}, "
f"信息={summary['info']}, 安全={summary['safe']}"
)
return summary
def calculate_urgency_level(summary: Dict[str, Any]) -> int:
"""
根据漏洞数量计算紧急程度(1-5)
返回:
5 - 有高危漏洞(最紧急)
4 - 有中危漏洞
3 - 有低危漏洞
2 - 仅有信息类
1 - 无漏洞
"""
high = summary.get("high", 0)
medium = summary.get("medium", 0)
low = summary.get("low", 0)
info = summary.get("info", 0)
if high > 0:
return 5
elif medium > 0:
return 4
elif low > 0:
return 3
elif info > 0:
return 2
return 1
def generate_task_name(server_ip: str) -> str:
"""
生成 ERP 任务名称
格式:{目标IP}接口安全测试及漏洞修复_{YYYYMMDD}
Args:
server_ip: 服务器 IP 地址
Returns:
str: 任务名称
"""
date_str = datetime.now().strftime("%Y%m%d")
if server_ip:
return f"{server_ip}接口安全测试及漏洞修复_{date_str}"
return f"接口安全测试及漏洞修复_{date_str}"
def parse_high_risk_vulnerabilities(markdown: str) -> List[str]:
"""
解析高危漏洞名称列表
Args:
markdown: 报告 Markdown 内容
Returns:
list: 高危漏洞名称列表
"""
high_risk = []
if not markdown:
return high_risk
# 匹配格式:### 2.1 [高危] 漏洞名称 或 ### 2.1 [严重] 漏洞名称
pattern = r"### \d+\.\d+ \[(高危|严重)\]\s+([^\n]+)"
matches = re.findall(pattern, markdown)
for level, name in matches:
high_risk.append(name.strip())
return high_risk
def build_task_data(
summary: Dict[str, Any],
server_ip: str,
high_risk_list: List[str],
medium_count: int,
config: Dict[str, Any],
) -> Dict[str, Any]:
"""
构建 ERP 任务数据
Args:
summary: 报告摘要
server_ip: 服务器 IP
high_risk_list: 高危漏洞名称列表
medium_count: 中危漏洞数量
config: 用户配置(type_id/status_id/level/excepthour/user_id/createuser_id/deadline_days/develop_id)
Returns:
dict: POST /openclaw/task 的请求体
"""
# 任务名称
task_name = config.get("task_name") or generate_task_name(server_ip)
# 类型/状态
type_id = int(config.get("type_id") or DEFAULT_TASK_CONFIG["type_id"])
status_id = int(config.get("status_id") or DEFAULT_TASK_CONFIG["status_id"])
# 紧急程度:优先用户指定,否则按漏洞自动计算
level = int(config.get("level") or 0)
if level < 1 or level > 5:
level = calculate_urgency_level(summary)
# 期望工时 / 截止时间
excepthour = int(config.get("excepthour") or DEFAULT_TASK_CONFIG["excepthour"])
deadline_days = int(config.get("deadline_days") or DEFAULT_TASK_CONFIG["deadline_days"])
deadline = datetime.now() + timedelta(days=deadline_days)
excepttime = deadline.strftime("%Y-%m-%d %H:%M:%S")
# 责任人 / 创建人
user_id = int(config.get("user_id") or DEFAULT_TASK_CONFIG["user_id"])
createuser_id = int(config.get("createuser_id") or DEFAULT_TASK_CONFIG["createuser_id"])
# 任务描述 HTML
content = generate_task_content(
summary=summary,
high_risk_list=high_risk_list,
medium_count=medium_count,
nas_report_path="", # 平台不上传网盘,参考实现中为可选
)
task_data = {
"name": task_name,
"type_id": type_id,
"status_id": status_id,
"level": level,
"excepthour": excepthour,
"excepttime": excepttime,
"content": content,
"user_id": user_id,
"createuser_id": createuser_id,
}
# 可选字段:关联需求
develop_id = config.get("develop_id")
if develop_id:
task_data["develop_id"] = int(develop_id)
logger.info(
f"任务数据: 名称={task_name}, 类型={type_id}, 状态={status_id}, "
f"紧急程度={level}, 工时={excepthour}h, 截止={excepttime}, "
f"责任人={user_id}, 创建人={createuser_id}"
)
return task_data
def generate_task_content(
summary: Dict[str, Any],
high_risk_list: List[str],
medium_count: int,
nas_report_path: str = "",
) -> str:
"""
生成 ERP 任务描述 HTML
对齐参考实现 report_parser.py 的 generate_task_content。
Args:
summary: 报告摘要
high_risk_list: 高危漏洞名称列表
medium_count: 中危漏洞数量
nas_report_path: 网盘报告路径(可选,平台暂不传)
Returns:
str: HTML 格式任务描述
"""
html_parts = []
# 标题
html_parts.append("<h3>接口安全测试报告</h3>")
# 测试概览
html_parts.append("<h4>一、测试概览</h4>")
html_parts.append(
"<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse; width:100%;'>"
)
html_parts.append(
f"<tr><td style='width:20%;'><strong>测试目标:</strong></td><td>{summary.get('target', 'N/A')}</td></tr>"
)
html_parts.append(
f"<tr><td><strong>测试时间:</strong></td><td>{summary.get('start_time', 'N/A')} ~ {summary.get('end_time', 'N/A')}</td></tr>"
)
html_parts.append("<tr><td><strong>测试标准:</strong></td><td>OWASP API Security Top 10 (2019)</td></tr>")
html_parts.append(
"<tr><td><strong>测试工具:</strong></td><td>平台自动化测试系统 - 安全测试模块</td></tr>"
)
html_parts.append("</table>")
# 漏洞统计
html_parts.append("<h4>二、漏洞统计</h4>")
html_parts.append(
"<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse;'>"
)
html_parts.append("<tr><th>总计</th><th>高危</th><th>中危</th><th>低危</th><th>信息类</th><th>安全</th></tr>")
html_parts.append(f"<tr><td>{summary.get('total', 0)}</td>")
html_parts.append(f"<td style='color:red; font-weight:bold;'>{summary.get('high', 0)}</td>")
html_parts.append(f"<td style='color:orange; font-weight:bold;'>{summary.get('medium', 0)}</td>")
html_parts.append(f"<td style='color:#FFD700; font-weight:bold;'>{summary.get('low', 0)}</td>")
html_parts.append(f"<td style='color:blue; font-weight:bold;'>{summary.get('info', 0)}</td>")
html_parts.append(f"<td style='color:green; font-weight:bold;'>{summary.get('safe', 0)}</td></tr>")
html_parts.append("</table>")
# 高危漏洞列表
if high_risk_list:
html_parts.append("<h4>三、高危漏洞详情</h4>")
html_parts.append("<p><strong>本次测试发现以下高危漏洞,需优先处理:</strong></p>")
html_parts.append("<ol>")
for idx, vuln in enumerate(high_risk_list[:10], 1):
html_parts.append(f"<li style='color: red; margin-bottom: 5px;'><strong>{idx}. {vuln}</strong></li>")
if len(high_risk_list) > 10:
html_parts.append(f"<li>...及其他 {len(high_risk_list) - 10} 个高危漏洞</li>")
html_parts.append("</ol>")
# 中危漏洞统计
if medium_count > 0:
html_parts.append("<h4>四、中危漏洞</h4>")
html_parts.append(f"<p>发现中危漏洞 <strong>{medium_count}</strong> 个,建议优先修复。</p>")
# 修复建议
html_parts.append("<h4>五、修复建议</h4>")
html_parts.append("<ul>")
html_parts.append("<li><strong>优先处理高危漏洞:</strong>所有高危漏洞应在确认后24小时内修复</li>")
html_parts.append("<li><strong>中危漏洞:</strong>建议在本测试周期内完成修复</li>")
html_parts.append("<li><strong>低危漏洞:</strong>可在下一版本迭代中修复</li>")
html_parts.append("<li><strong>修复验证:</strong>漏洞修复完成后,请通知测试团队进行回归验证</li>")
html_parts.append("<li><strong>复查要求:</strong>所有漏洞修复后需要重新进行安全测试验证</li>")
html_parts.append("</ul>")
# 报告链接(当前平台不传网盘路径,跳过该章节)
if nas_report_path:
html_parts.append("<h4>六、测试报告</h4>")
html_parts.append("<p>详细测试报告已上传至网盘:</p>")
html_parts.append(f"<p><code>{nas_report_path}</code></p>")
html_parts.append("<p>请下载查看完整报告了解所有漏洞详情和修复建议。</p>")
# 紧急度说明
calculated_level = calculate_urgency_level(summary)
desc = URGENCY_DESC.get(calculated_level, "")
if desc:
html_parts.append(f"<p><strong>{desc}</strong></p>")
# 联系信息
html_parts.append("<h4>七、联系方式</h4>")
html_parts.append("<p>如有疑问或需要协助,请联系测试团队。</p>")
return "\n".join(html_parts)
def _parse_count(markdown: str, pattern: str) -> int:
"""从 Markdown 中解析数字统计"""
match = re.search(pattern, markdown)
if match:
return int(match.group(1))
return 0
\ No newline at end of file
......@@ -68,6 +68,7 @@
<el-menu-item index="/cases/security">用例管理</el-menu-item>
<el-menu-item index="/execution/security">执行中心</el-menu-item>
<el-menu-item index="/reports/security">报告中心</el-menu-item>
<el-menu-item index="/security/erp-config">ERP 配置</el-menu-item>
</el-sub-menu>
<!-- 性能测试(父菜单,含三个子页面) -->
......@@ -82,6 +83,17 @@
<el-menu-item index="/performance/presets">接口预设</el-menu-item>
</el-sub-menu>
<!-- 接口测试(一级菜单,含用例管理/执行中心/报告中心) -->
<el-sub-menu index="/api-test">
<template #title>
<el-icon><Connection /></el-icon>
<span>接口测试</span>
</template>
<el-menu-item index="/api-test/cases">用例管理</el-menu-item>
<el-menu-item index="/api-test/execution">执行中心</el-menu-item>
<el-menu-item index="/api-test/report">报告中心</el-menu-item>
</el-sub-menu>
<!-- 自动化部署测试(父菜单) -->
<el-sub-menu index="/deploy">
<template #title>
......@@ -202,7 +214,8 @@ import {
Tools,
Lock,
Setting,
EditPen
EditPen,
Connection
} from '@element-plus/icons-vue'
// ==================== 响应式数据 ====================
......@@ -220,6 +233,13 @@ const CASE_TYPE_LABELS: Record<string, string> = {
security: '安全自动化测试',
}
/** 接口测试子页面标签映射 */
const API_TEST_LABELS: Record<string, string> = {
cases: '用例管理',
execution: '执行中心',
report: '报告中心',
}
/** 设备模拟子页面标签映射 */
const DEVICE_SIM_LABELS: Record<string, string> = {
settings: '环境配置',
......@@ -271,13 +291,18 @@ const DOCUMENT_LABELS: Record<string, string> = {
translate: '文档翻译',
}
/** 安全测试子页面标签映射(ERP配置等独立子路由) */
const SECURITY_SUB_LABELS: Record<string, string> = {
'erp-config': 'ERP 配置',
}
/** 当前激活菜单项(无 type 时回退到默认子项以保持高亮) */
const currentRoute = computed(() => {
const seg = route.path.split('/').filter(Boolean)
const root = '/' + (seg[0] || '')
// 无 type 参数时回退到默认子项,保证菜单高亮
if (seg.length === 1 && ['/cases', '/execution', '/reports', '/system', '/deploy', '/device-sim', '/performance', '/security', '/document'].includes(root)) {
const defaultType = root === '/system' ? 'settings' : root === '/deploy' ? 'servers' : root === '/device-sim' ? 'settings' : root === '/performance' ? 'tasks' : root === '/security' ? 'security' : root === '/document' ? 'optimize' : 'ui'
if (seg.length === 1 && ['/cases', '/execution', '/reports', '/system', '/deploy', '/device-sim', '/performance', '/security', '/document', '/api-test'].includes(root)) {
const defaultType = root === '/system' ? 'settings' : root === '/deploy' ? 'servers' : root === '/device-sim' ? 'settings' : root === '/performance' ? 'tasks' : root === '/security' ? 'security' : root === '/document' ? 'optimize' : root === '/api-test' ? 'cases' : 'ui'
return `${root}/${defaultType}`
}
return route.path
......@@ -302,6 +327,7 @@ const pageTitle = computed(() => {
'/device-sim': '设备模拟',
'/system': '系统管理',
'/document': '文档管理',
'/api-test': '接口测试',
}
const base = baseMap[root] || '测试管理平台'
const type = seg[1]
......@@ -319,7 +345,9 @@ const pageTitle = computed(() => {
if (root === '/system') return SYSTEM_LABELS[type]
if (root === '/deploy') return DEPLOY_LABELS[type]
if (root === '/performance') return PERFORMANCE_LABELS[type]
if (root === '/security') return SECURITY_SUB_LABELS[type]
if (root === '/document') return DOCUMENT_LABELS[type]
if (root === '/api-test') return API_TEST_LABELS[type]
return ''
})()
......
......@@ -94,10 +94,8 @@ export function getSecuritySummary(executionId: string) {
}
/** 获取安全测试报告内容(Markdown) */
export function getSecurityReport(executionId: string) {
return request.get<{ execution_id: string; content: string; format: string }>(
`/api/security/executions/${executionId}/report`
)
export function getSecurityReport(executionId: string): Promise<{ execution_id: string; content: string; format: string }> {
return request.get(`/api/security/executions/${executionId}/report`) as any
}
/** 安全测试报告下载地址 */
......@@ -105,6 +103,68 @@ export function getSecurityReportDownloadUrl(executionId: string): string {
return `/api/security/executions/${executionId}/report/download`
}
// ==================== ERP 上传 ====================
/** 上传安全测试报告到 ERP 测试单 */
export function uploadSecurityReportToErp(
executionId: string,
data: {
developtesting_id?: number
copyuser_names?: string[]
createuser_name?: string
}
): Promise<{ success: boolean; report_id?: number; message: string }> {
return request.post(`/api/security/executions/${executionId}/upload-to-erp`, data) as any
}
/** 上传安全测试报告到 ERP 项目资料 */
export function uploadSecurityReportToProject(
executionId: string,
data: {
project_id: number
name?: string
}
): Promise<{ success: boolean; file_id?: number; message: string }> {
return request.post(`/api/security/executions/${executionId}/upload-to-project`, data) as any
}
/** 上传安全测试报告到 ERP 协作文档 */
export function uploadSecurityReportToCooperation(
executionId: string,
data: {
project_id: number
name?: string
group_id?: number
}
): Promise<{ success: boolean; file_id?: number; message: string }> {
return request.post(`/api/security/executions/${executionId}/upload-to-cooperation`, data) as any
}
// ==================== ERP 任务创建 ====================
/** 获取安全测试 ERP 任务创建预览数据 */
export function getSecurityTaskPreview(executionId: string): Promise<any> {
return request.get(`/api/security/executions/${executionId}/task-preview`) as any
}
/** 创建安全测试 ERP 任务 */
export function createSecurityTask(
executionId: string,
data: {
type_id: number
status_id: number
level: number
excepthour: number
user_id: number
createuser_id: number
deadline_days: number
develop_id?: number
task_name?: string
}
): Promise<{ success: boolean; task_id?: number; task_name: string; message: string }> {
return request.post(`/api/security/executions/${executionId}/create-task`, data) as any
}
// ==================== 知识库 ====================
/** 获取安全知识库 */
......
/**
* 安全测试 ERP 配置 API 封装
*
* @author czj
* @date 2026-08-13
*/
import request from '@/utils/request'
export const securityErpApi = {
/**
* 获取 ERP 配置
* 优先从数据库读取,无记录时回退到环境变量默认值
*/
async getConfig(): Promise<{
api_key: string
base_url: string
developtesting_id: number
type_id: number
image_url_prefix: string
}> {
const response = await request.get('/api/security/erp-config')
return response as any
},
/**
* 更新 ERP 配置
* 持久化到数据库,同时更新运行时环境变量
*/
async updateConfig(config: {
api_key?: string
base_url?: string
developtesting_id?: number
type_id?: number
image_url_prefix?: string
}): Promise<any> {
const response = await request.put('/api/security/erp-config', config)
return response as any
}
}
\ No newline at end of file
......@@ -183,6 +183,34 @@ const routes: RouteRecordRaw[] = [
}
]
},
// 接口测试模块(一级菜单,含用例管理/执行中心/报告中心)
{
path: '/api-test',
name: 'ApiTest',
component: () => import('@/views/api-test/index.vue'),
redirect: '/api-test/cases',
meta: { title: '接口测试' },
children: [
{
path: 'cases',
name: 'ApiTestCases',
component: () => import('@/views/api-test/ApiCaseList.vue'),
meta: { title: '用例管理' }
},
{
path: 'execution',
name: 'ApiTestExecution',
component: () => import('@/views/Execution.vue'),
meta: { title: '执行中心' }
},
{
path: 'report',
name: 'ApiTestReport',
component: () => import('@/views/Reports.vue'),
meta: { title: '报告中心' }
}
]
},
// 重定向:旧版路径 → 性能测试模块
{
path: '/cases/performance',
......@@ -196,6 +224,13 @@ const routes: RouteRecordRaw[] = [
path: '/reports/performance',
redirect: '/performance'
},
// 安全测试 ERP 配置(独立子页面,复用 erp_configs 表,与功能测试报告共享配置)
{
path: '/security/erp-config',
name: 'SecurityErpConfig',
component: () => import('@/views/security/ErpConfig.vue'),
meta: { title: 'ERP配置' }
},
// 系统管理(父菜单)
{
path: '/system',
......
......@@ -5,7 +5,7 @@
@author czj
@date 2026-07-09
@last-modified 2026-07-22
@last-modified 2026-08-17
-->
<template>
......@@ -129,11 +129,21 @@
<template #header>
<div class="security-report-header">
<span>安全测试报告</span>
<div class="security-report-actions">
<el-button type="warning" size="small" :disabled="!securityReportContent" @click="openErpDialog">
<el-icon><Upload /></el-icon>
上传到 ERP
</el-button>
<el-button type="danger" size="small" :disabled="!securityReportContent" @click="openErpTaskDialog">
<el-icon><List /></el-icon>
创建ERP任务
</el-button>
<el-button type="primary" size="small" @click="downloadSecurityReport">
<el-icon><Download /></el-icon>
下载报告
</el-button>
</div>
</div>
</template>
<div class="security-report-container" v-loading="loadingSecurity">
<div
......@@ -144,6 +154,175 @@
<el-empty v-else-if="!loadingSecurity" description="报告加载失败" />
</div>
</el-dialog>
<!-- 安全测试报告 ERP 上传对话框 -->
<el-dialog
v-model="erpDialogVisible"
title="上传安全测试报告到 ERP"
width="520px"
:close-on-click-modal="false"
append-to-body
>
<el-form :model="erpForm" label-width="100px" v-loading="uploadingErp">
<el-form-item label="上传类型" required>
<el-radio-group v-model="erpForm.target">
<el-radio value="erp">ERP测试单</el-radio>
<el-radio value="project">项目资料</el-radio>
<el-radio value="cooperation">协作文档</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="erpForm.target === 'erp'" label="测试单 ID">
<el-input-number v-model="erpForm.developtestingId" :min="0" :controls="false" style="width: 100%" placeholder="留空使用系统默认配置" />
</el-form-item>
<el-form-item v-if="erpForm.target === 'erp'" label="抄送人">
<el-input v-model="erpForm.copyuserNamesText" placeholder="多个姓名用逗号分隔(可选)" clearable />
</el-form-item>
<el-form-item v-if="erpForm.target === 'erp'" label="创建人">
<el-input v-model="erpForm.createuserName" placeholder="姓名(可选)" clearable />
</el-form-item>
<el-form-item v-if="erpForm.target !== 'erp'" label="项目 ID" required>
<el-input-number v-model="erpForm.projectId" :min="1" :controls="false" style="width: 100%" placeholder="请输入项目 ID" />
</el-form-item>
<el-form-item v-if="erpForm.target !== 'erp'" label="文件名称">
<el-input v-model="erpForm.name" placeholder="默认取报告文件名" clearable />
</el-form-item>
<el-form-item v-if="erpForm.target === 'cooperation'" label="分组 ID">
<el-input-number v-model="erpForm.groupId" :min="1" :controls="false" style="width: 100%" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="erpDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="uploadingErp" @click="handleUploadErp">确认上传</el-button>
</template>
</el-dialog>
<!-- 安全测试 ERP 任务创建对话框 -->
<el-dialog
v-model="erpTaskDialogVisible"
title="创建安全测试 ERP 任务"
width="720px"
:close-on-click-modal="false"
append-to-body
>
<!-- 报告摘要(只读) -->
<el-collapse v-if="taskPreview" style="margin-bottom: 16px;">
<el-collapse-item title="报告摘要(只读)" name="summary">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="测试目标">{{ taskPreview.summary?.target || '-' }}</el-descriptions-item>
<el-descriptions-item label="服务器 IP">{{ taskPreview.summary?.server_ip || '-' }}</el-descriptions-item>
<el-descriptions-item label="测试时间">{{ taskPreview.summary?.start_time || '-' }} ~ {{ taskPreview.summary?.end_time || '-' }}</el-descriptions-item>
<el-descriptions-item label="用例总数">{{ taskPreview.summary?.total || 0 }}</el-descriptions-item>
<el-descriptions-item label="高危漏洞"><span style="color: #f56c6c; font-weight: bold;">{{ taskPreview.summary?.high || 0 }}</span></el-descriptions-item>
<el-descriptions-item label="中危漏洞"><span style="color: #e6a23c; font-weight: bold;">{{ taskPreview.summary?.medium || 0 }}</span></el-descriptions-item>
<el-descriptions-item label="低危漏洞"><span style="color: #ffd700; font-weight: bold;">{{ taskPreview.summary?.low || 0 }}</span></el-descriptions-item>
<el-descriptions-item label="信息类"><span style="color: #409eff; font-weight: bold;">{{ taskPreview.summary?.info || 0 }}</span></el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
<!-- 任务配置表单 -->
<el-form :model="taskForm" label-width="100px" v-loading="taskLoading">
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="任务名称">
<el-input v-model="taskForm.task_name" placeholder="自动生成,可修改" clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="任务类型">
<el-select v-model="taskForm.type_id" placeholder="选择类型" style="width: 100%">
<el-option
v-for="t in taskPreview?.task_types || []"
:key="t.id"
:label="t.name"
:value="t.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="任务状态">
<el-select v-model="taskForm.status_id" placeholder="选择状态" style="width: 100%">
<el-option
v-for="s in taskPreview?.task_statuses || []"
:key="s.id"
:label="s.name"
:value="s.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="责任人">
<el-select v-model="taskForm.user_id" placeholder="选择责任人" filterable style="width: 100%">
<el-option
v-for="u in taskPreview?.staff_list || []"
:key="u.id"
:label="u.name"
:value="u.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建人">
<el-select v-model="taskForm.createuser_id" placeholder="选择创建人" filterable style="width: 100%">
<el-option
v-for="u in taskPreview?.staff_list || []"
:key="u.id"
:label="u.name"
:value="u.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="8">
<el-form-item label="紧急程度">
<el-select v-model="taskForm.level" placeholder="紧急程度" style="width: 100%">
<el-option :label="'1 - 低优先级'" :value="1" />
<el-option :label="'2 - 正常优先级'" :value="2" />
<el-option :label="'3 - 中优先级'" :value="3" />
<el-option :label="'4 - 高优先级'" :value="4" />
<el-option :label="'5 - 最高优先级'" :value="5" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="期望工时(h)">
<el-input-number v-model="taskForm.excepthour" :min="1" :max="999" :controls="false" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="截止天数">
<el-input-number v-model="taskForm.deadline_days" :min="1" :max="365" :controls="false" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item label="关联需求">
<el-input
:model-value="taskPreview?.requirement ? `${taskPreview.requirement.name} (ID: ${taskPreview.requirement.id})` : '未找到关联需求'"
readonly
class="requirement-input"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="erpTaskDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="taskLoading" @click="handleCreateTask">确认创建</el-button>
</template>
</el-dialog>
</div>
</template>
......@@ -158,13 +337,21 @@
* 4. 下载报告文件
*/
import { ref, computed, watch, onMounted } from 'vue'
import { ref, computed, reactive, watch, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Refresh, View, Download, Files } from '@element-plus/icons-vue'
import { Refresh, View, Download, Files, Upload, List } from '@element-plus/icons-vue'
import { marked } from 'marked'
import { reportApi } from '@/api/reports'
import { getSecurityReport, getSecurityReportDownloadUrl } from '@/api/security'
import {
getSecurityReport,
getSecurityReportDownloadUrl,
uploadSecurityReportToErp,
uploadSecurityReportToProject,
uploadSecurityReportToCooperation,
getSecurityTaskPreview,
createSecurityTask,
} from '@/api/security'
const route = useRoute()
......@@ -186,21 +373,57 @@ const securityReportContent = ref('')
const loadingSecurity = ref(false)
const currentSecurityExecutionId = ref('')
/** 当前用例类型(来自路由参数,默认 ui) */
const currentType = computed(() => (route.params.type as string) || 'ui')
// 安全测试报告 ERP 上传
const erpDialogVisible = ref(false)
const uploadingErp = ref(false)
const erpForm = reactive({
target: 'erp',
developtestingId: undefined as number | undefined,
copyuserNamesText: '',
createuserName: '',
projectId: undefined as number | undefined,
name: '',
groupId: undefined as number | undefined,
})
// 安全测试 ERP 任务创建
const erpTaskDialogVisible = ref(false)
const taskLoading = ref(false)
const taskPreview = ref<any>(null)
const taskForm = reactive({
task_name: '',
type_id: 3,
status_id: 1,
level: 4,
excepthour: 21,
user_id: 101,
createuser_id: 441,
deadline_days: 7,
develop_id: undefined as number | undefined,
})
/** 当前用例类型(来自路由参数,默认 ui;接口测试模块固定为 api) */
const currentType = computed(() => {
if (route.path.startsWith('/api-test')) return 'api'
return (route.params.type as string) || 'ui'
})
/** 报告类型标签映射 */
const REPORT_TYPE_LABELS: Record<string, string> = {
ui: 'UI自动化测试',
api: '接口测试',
api: '接口自动化测试',
security: '安全测试',
functional: '功能测试报告',
deploy: '部署测试',
performance: '性能测试',
}
const REPORT_TYPE_TAGS: Record<string, string> = {
ui: 'primary',
api: 'success',
security: 'danger',
functional: 'warning',
deploy: 'info',
performance: 'warning',
}
const reportTypeLabel = (type: string) => REPORT_TYPE_LABELS[type] || type
const reportTypeTag = (type: string) => REPORT_TYPE_TAGS[type] || 'info'
......@@ -268,7 +491,7 @@ const viewReport = async (row: any) => {
try {
const response = await getSecurityReport(row.id)
securityReportContent.value = response.data.content
securityReportContent.value = response.content
} catch (error: any) {
ElMessage.error('加载安全测试报告失败: ' + error.message)
securityReportContent.value = ''
......@@ -309,6 +532,167 @@ const downloadSecurityReport = () => {
}
}
/**
* 打开 ERP 上传对话框
*/
const openErpDialog = () => {
erpForm.target = 'erp'
erpForm.developtestingId = undefined
erpForm.copyuserNamesText = ''
erpForm.createuserName = ''
erpForm.projectId = undefined
erpForm.name = ''
erpForm.groupId = undefined
erpDialogVisible.value = true
}
/**
* 上传安全报告到 ERP
* - erp: 上传到 ERP 测试单(自动生成报告 → DOCX → 创建报告)
* - project: 上传到项目资料
* - cooperation: 上传到协作文档
*/
const handleUploadErp = async () => {
const executionId = currentSecurityExecutionId.value
if (!executionId) return
// 校验
if (erpForm.target !== 'erp' && !erpForm.projectId) {
ElMessage.warning('请输入项目 ID')
return
}
const copyuserNames = erpForm.copyuserNamesText
.split(/[,,、]/)
.map((s: string) => s.trim())
.filter(Boolean)
uploadingErp.value = true
try {
let res: any
if (erpForm.target === 'erp') {
res = await uploadSecurityReportToErp(executionId, {
developtesting_id: erpForm.developtestingId,
copyuser_names: copyuserNames,
createuser_name: erpForm.createuserName || undefined,
})
} else if (erpForm.target === 'project') {
res = await uploadSecurityReportToProject(executionId, {
project_id: erpForm.projectId as number,
name: erpForm.name || undefined,
})
} else {
res = await uploadSecurityReportToCooperation(executionId, {
project_id: erpForm.projectId as number,
name: erpForm.name || undefined,
group_id: erpForm.groupId,
})
}
if (res?.success) {
ElMessage.success(res.message || '上传成功')
erpDialogVisible.value = false
} else {
ElMessage.error(res?.message || '上传失败,请查看日志')
}
} catch (error: any) {
ElMessage.error('ERP 上传失败: ' + (error?.message || '未知错误'))
} finally {
uploadingErp.value = false
}
}
/**
* 打开 ERP 任务创建对话框
* 调用预览接口获取报告摘要、基础数据、默认配置
*/
const openErpTaskDialog = async () => {
const executionId = currentSecurityExecutionId.value
if (!executionId) return
taskLoading.value = true
erpTaskDialogVisible.value = true
taskPreview.value = null
// 重置表单为默认值
taskForm.task_name = ''
taskForm.type_id = 3
taskForm.status_id = 1
taskForm.level = 4
taskForm.excepthour = 21
taskForm.user_id = 101
taskForm.createuser_id = 441
taskForm.deadline_days = 7
taskForm.develop_id = undefined
try {
const res = await getSecurityTaskPreview(executionId)
taskPreview.value = res
// 应用预览数据中的默认配置
if (res.default_config) {
taskForm.type_id = res.default_config.type_id ?? 3
taskForm.status_id = res.default_config.status_id ?? 1
taskForm.level = res.default_config.level ?? 4
taskForm.excepthour = res.default_config.excepthour ?? 21
taskForm.user_id = res.default_config.user_id ?? 101
taskForm.createuser_id = res.default_config.createuser_id ?? 441
taskForm.deadline_days = res.default_config.deadline_days ?? 7
}
taskForm.task_name = res.task_name || ''
// 如果有关联需求,设置 develop_id
if (res.requirement?.id) {
taskForm.develop_id = res.requirement.id
}
} catch (error: any) {
ElMessage.error('获取任务预览失败: ' + (error?.message || '未知错误'))
erpTaskDialogVisible.value = false
} finally {
taskLoading.value = false
}
}
/**
* 创建 ERP 任务
*/
const handleCreateTask = async () => {
const executionId = currentSecurityExecutionId.value
if (!executionId) return
// 校验
if (!taskForm.task_name) {
ElMessage.warning('请输入任务名称')
return
}
taskLoading.value = true
try {
const res = await createSecurityTask(executionId, {
type_id: taskForm.type_id,
status_id: taskForm.status_id,
level: taskForm.level,
excepthour: taskForm.excepthour,
user_id: taskForm.user_id,
createuser_id: taskForm.createuser_id,
deadline_days: taskForm.deadline_days,
develop_id: taskForm.develop_id || undefined,
task_name: taskForm.task_name || undefined,
})
if (res?.success) {
ElMessage.success(res.message || 'ERP任务创建成功')
erpTaskDialogVisible.value = false
} else {
ElMessage.error(res?.message || 'ERP任务创建失败,请查看日志')
}
} catch (error: any) {
ElMessage.error('创建 ERP 任务失败: ' + (error?.message || '未知错误'))
} finally {
taskLoading.value = false
}
}
// ==================== 生命周期 ====================
onMounted(() => {
......@@ -382,6 +766,11 @@ watch(currentType, () => {
width: 100%;
}
.security-report-actions {
display: flex;
gap: 8px;
}
.security-report-container {
height: calc(100vh - 200px);
overflow-y: auto;
......
<!--
组件名称:ErpConfig.vue
组件描述:安全测试 ERP 配置管理页面
@author czj
@date 2026-08-13
-->
<template>
<div class="erp-config-page">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>ERP 配置</span>
</div>
</template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="140px"
label-position="right"
style="max-width: 600px"
>
<el-form-item label="API Key" prop="api_key">
<el-input
v-model="form.api_key"
type="password"
show-password
placeholder="请输入 ERP API 密钥"
clearable
/>
</el-form-item>
<el-form-item label="Base URL" prop="base_url">
<el-input
v-model="form.base_url"
placeholder="请输入 ERP 基础 URL"
clearable
/>
</el-form-item>
<el-form-item label="测试单 ID" prop="developtesting_id">
<el-input-number
v-model="form.developtesting_id"
:min="0"
:max="999999"
:step="1"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="Type ID" prop="type_id">
<el-input-number
v-model="form.type_id"
:min="0"
:max="999"
:step="1"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="图片 URL 前缀" prop="image_url_prefix">
<el-input
v-model="form.image_url_prefix"
placeholder="请输入图片 URL 前缀"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">
{{ saving ? '保存中...' : '保存配置' }}
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
/**
* 安全测试 ERP 配置管理页面
*
* 功能:
* 1. 加载时自动获取当前 ERP 配置(从数据库 / 环境变量回退)
* 2. 修改后保存到数据库,重启后持久化
* 3. 与功能测试报告模块共用同一套 ERP 配置(erp_configs 表)
*/
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { securityErpApi } from '@/api/securityErp'
// ==================== 类型定义 ====================
interface ErpConfigForm {
api_key: string
base_url: string
developtesting_id: number
type_id: number
image_url_prefix: string
}
// ==================== 响应式数据 ====================
const formRef = ref()
const saving = ref(false)
const form = reactive<ErpConfigForm>({
api_key: '',
base_url: '',
developtesting_id: 397,
type_id: 2,
image_url_prefix: '',
})
/** 原始数据快照,用于重置 */
let originalSnapshot = ''
// ==================== 表单校验规则 ====================
const rules = {
api_key: [{ required: true, message: '请输入 ERP API 密钥', trigger: 'blur' }],
base_url: [{ required: true, message: '请输入 ERP 基础 URL', trigger: 'blur' }],
}
// ==================== 方法 ====================
/** 加载配置 */
async function loadConfig() {
try {
const res = await securityErpApi.getConfig()
Object.assign(form, res)
originalSnapshot = JSON.stringify(res)
} catch (err: any) {
ElMessage.error('加载 ERP 配置失败:' + (err?.message || '未知错误'))
}
}
/** 保存配置 */
async function handleSave() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
saving.value = true
try {
await securityErpApi.updateConfig({
api_key: form.api_key,
base_url: form.base_url,
developtesting_id: form.developtesting_id,
type_id: form.type_id,
image_url_prefix: form.image_url_prefix,
})
ElMessage.success('ERP 配置已保存')
originalSnapshot = JSON.stringify({ ...form })
} catch (err: any) {
ElMessage.error('保存 ERP 配置失败:' + (err?.message || '未知错误'))
} finally {
saving.value = false
}
}
/** 重置表单到加载时的值 */
function handleReset() {
const snapshot = JSON.parse(originalSnapshot)
Object.assign(form, snapshot)
formRef.value?.clearValidate()
}
// ==================== 生命周期 ====================
onMounted(() => {
loadConfig()
})
</script>
<style scoped>
.erp-config-page {
max-width: 800px;
}
.card-header {
font-size: 16px;
font-weight: 600;
}
</style>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论