提交 522ab2f6 authored 作者: 陈泽健's avatar 陈泽健

feat(security): 支持区分安全测试标准版与项目版用例

- 需求与计划:输出PRD需求文档与计划执行文档,归档至Docs/PRD/需求文档/安全测试/
- 数据库迁移:test_cases、executions、scheduled_tasks 表新增 project_type 字段,_ensure_columns 自动平滑加列并赋默认值 standard
- 后端业务:
  - 用例管理支持按 project_type 筛选与持久化
  - 安全测试执行支持 project_type 显式指定及按选选用例自动推断(standard/project/mixed)
  - 安全定时任务支持按 project_type 精确隔离调度执行
- 前端交互:
  - 用例管理、定时任务、执行中心新增项目类型筛选与状态标签展示
  - 用例与定时任务表单支持标准版/项目版单选配置
- 交接文档:沉淀本次改动与验证成果至 HANDOFF_安全测试.md
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 82d38613
# 计划执行 - 安全测试项目版支持
> **文档版本**: v1.0
> **创建日期**: 2026-09-09
> **关联需求**: `_PRD_安全测试项目版支持_需求文档.md`
> **状态**: 需求已确认 → 开始实现
---
## 一、计划概述
**目标**:安全测试用例/执行/定时任务支持「标准版(standard)/ 项目版(project)」区分。
**范围**
- 后端:3 张表新增 `project_type` 字段(test_cases / executions / scheduled_tasks)+ 自动升级
- 服务层:用例 CRUD、安全执行创建、定时任务用例收集支持 project_type
- 接口:用例接口、安全执行接口、安全定时任务接口支持 project_type
- 前端:用例管理页、安全执行中心、安全定时任务页支持项目版筛选与标记
---
## 二、阶段划分
### 阶段 1:数据库模型与升级(P0)
| # | 任务 | 文件 |
|---|------|------|
| 1.1 | TestCase 模型新增 `project_type` 字段 | `backend/app/models/test_case.py` |
| 1.2 | Execution 模型新增 `project_type` 字段 | `backend/app/models/execution.py` |
| 1.3 | ScheduledTask 模型新增 `project_type` 字段 | `backend/app/models/scheduled_task.py` |
| 1.4 | `_ensure_columns` 补齐三表字段(SQLite/MySQL 通用) | `backend/app/database.py` |
| 1.5 | `to_dict()` 输出 `project_type` | 上述三个模型 |
### 阶段 2:后端服务层(P0)
| # | 任务 | 文件 |
|---|------|------|
| 2.1 | CaseService.create/update 支持 project_type;list 支持筛选 | `backend/app/services/case_service.py` |
| 2.2 | 用例 schema 增加 project_type | `backend/app/schemas/test_case.py` |
| 2.3 | 用例路由透传 project_type 参数 | `backend/app/routers/cases.py` |
| 2.4 | SecurityService.create_execution 推断并写入 project_type | `backend/app/services/security_service.py` |
| 2.5 | 安全执行列表路由支持 project_type 筛选 | `backend/app/routers/security.py`(或所在执行路由) |
| 2.6 | 安全定时任务创建/更新/列表支持 project_type | `backend/app/routers/security_scheduled_tasks.py` + `scheduled_task.py` schema |
| 2.7 | 调度收集用例时按 project_type 过滤 | `backend/app/services/scheduler_service.py` |
### 阶段 3:前端(P0/P1)
| # | 任务 | 文件 |
|---|------|------|
| 3.1 | 类型定义增加 project_type | `frontend/src/types/`(case / security / scheduled) |
| 3.2 | API 增加 project_type 参数 | `frontend/src/api/`(case / security / scheduledTask) |
| 3.3 | 用例管理页:筛选 + 列 + 编辑表单 | `frontend/src/views/Cases.vue` |
| 3.4 | 安全执行中心:执行记录筛选 Tab/下拉 | 安全测试视图(执行记录区) |
| 3.5 | 安全定时任务:表单 + 列表列 | `frontend/src/views/ScheduledTasks.vue` |
| 3.6 | 前端构建验证 | `npm run build` |
### 阶段 4:验证与收尾(P0)
| # | 任务 | 文件 |
|---|------|------|
| 4.1 | 后端启动 + 字段自动补齐验证 | `backend` |
| 4.2 | 手动接口冒烟(创建项目版用例→执行→筛选) | curl / 前端 |
| 4.3 | 前端构建通过 | `frontend` |
| 4.4 | 更新需求文档(如有实现偏差) | Docs |
---
## 三、依赖关系
- 阶段 1 是阶段 2/3 的前置(模型与升级必须先落地)
- 阶段 2.4 依赖 2.1(用例 project_type 查询)
- 阶段 3 前端字段展示依赖后端接口返回 project_type
---
## 四、验收标准
见需求文档「七、验收标准」——6 条全部通过即视为完成。
---
## 五、风险与对策
- **旧库升级**`_ensure_columns` 幂等,重启即自动补齐;先本地 SQLite 验证
- **UI 用例误用**:本期仅安全页面暴露,UI 用例保持默认 standard
- **历史执行记录**:字段默认 standard,无需回填脚本(推断逻辑仅对新执行生效)
---
*本文档由 Claude Code 维护,实现进度同步更新。*
# PRD - 安全测试项目版支持
> **文档版本**: v1.0
> **创建日期**: 2026-09-09
> **模块类型**: 安全测试
> **优先级**: P0
---
## 一、背景与目标
### 1.1 背景
当前安全测试用例(`case_type = "security"`)统一使用标准配置,无法区分**标准版****项目版**两类测试场景:
1. **用例归属不清晰**:针对特定项目定制的安全检查项与标准检查项混在一起
2. **执行缺乏维度**:执行中心无法按「标准版/项目版」维度筛选安全测试执行记录
3. **定时任务无区分**:安全测试定时任务无法区分为标准版巡检或项目版专项巡检
性能测试模块已实现 `project_type: standard/custom` 的项目区分能力,UI 自动化模块已通过 `module_type: standard/custom` 区分标准/定制模块。安全测试需要对齐这一能力。
> 注:本需求为通用能力,**不针对任何具体项目**。
### 1.2 目标
1. **用例级区分**:安全测试用例新增 `project_type` 字段,区分标准版(`standard`)与项目版(`project`
2. **执行记录区分**:安全测试执行记录携带 `project_type`,支持筛选
3. **定时任务区分**:安全测试定时任务支持按 `project_type` 收集用例并标记
4. **兼容存量数据**:存量用例/执行记录默认 `standard`,不破坏现有功能
5. **前端可视区分**:安全测试相关页面提供「标准版/项目版」Tab 或筛选
---
## 二、功能范围
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 用例模型支持 `project_type` | test_cases 新增字段,默认 standard | P0 |
| 执行记录携带 `project_type` | executions 新增字段,创建执行时写入 | P0 |
| 用例列表按项目版筛选 | 用例管理页支持 project_type 筛选 | P0 |
| 安全执行中心按项目版筛选 | 执行记录列表支持 project_type 筛选 | P1 |
| 安全定时任务支持 `project_type` | 创建任务时指定,收集用例时过滤 | P1 |
| 执行历史/运行历史区分展示 | 任务运行历史按 project_type 标记 | P2 |
**不在本期内做**
- 安全配置(security_configs)的项目版区分(配置本身与目标环境绑定,无版本属性)
- 安全报告的项目版聚合维度
---
## 三、数据模型设计
### 3.1 test_cases 表
```sql
ALTER TABLE test_cases ADD COLUMN project_type VARCHAR(20) DEFAULT 'standard';
```
- `standard` — 标准版安全用例(默认,存量数据自动归入)
- `project` — 项目版安全用例
### 3.2 executions 表
```sql
ALTER TABLE executions ADD COLUMN project_type VARCHAR(20) DEFAULT 'standard';
```
- 创建安全测试执行时,根据本次执行包含的用例推断:
- 全部为 standard → `standard`
- 全部为 project → `project`
- 混合 → `mixed`
### 3.3 scheduled_tasks 表
```sql
ALTER TABLE scheduled_tasks ADD COLUMN project_type VARCHAR(20) DEFAULT 'standard';
```
- 安全测试定时任务创建时指定;调度收集用例时按该字段过滤
### 3.4 模块级别
复用既有 `modules.module_type`(standard/custom),**不新增字段**——安全用例的项目版属性由用例自身 `project_type` 决定,模块层级仅作为展示分组参考。
---
## 四、接口设计
### 4.1 用例接口(既有 /api/cases 扩展)
| 接口 | 变更 |
|------|------|
| `GET /api/cases` | 新增 query 参数 `project_type`(可选筛选) |
| `POST /api/cases` | 请求体新增 `project_type` 字段(默认 standard) |
| `PUT /api/cases/{id}` | 请求体新增 `project_type` 字段 |
| `GET /api/cases/export` | 导出数据包含 `project_type` |
### 4.2 安全测试执行接口(既有 /api/security 扩展)
| 接口 | 变更 |
|------|------|
| `POST /api/security/executions` | 请求体新增可选 `project_type`;创建执行时按用例推断并写入执行记录 |
| `GET /api/security/executions` | 新增 query 参数 `project_type` 筛选 |
### 4.3 安全定时任务接口(既有 /api/security/scheduled-tasks 扩展)
| 接口 | 变更 |
|------|------|
| `GET /api/security/scheduled-tasks` | 列表返回 `project_type`,支持 query 筛选 |
| `POST /api/security/scheduled-tasks` | 请求体新增 `project_type`(默认 standard) |
| `PUT /api/security/scheduled-tasks/{id}` | 请求体支持更新 `project_type` |
---
## 五、业务规则
1. **执行 project_type 推断**`create_execution` 时根据 case_ids 查询用例的 project_type:
- 无任何用例 → standard
- 全部 standard → standard
- 全部 project → project
- 两者都有 → mixed
2. **定时任务用例收集**`collect_module_cases` 在 case_type="security" 时额外按 task.project_type 过滤(task.project_type 为空时不过滤,兼容旧任务)
3. **前端 Tab 联动**
- 安全测试执行中心 Tab 切换 → 调用对应 project_type 筛选的执行列表
- 用例管理页安全用例筛选 → project_type 下拉筛选
4. **UI 用例不受影响**:project_type 字段对所有用例类型生效(默认 standard),但本期仅安全测试页面暴露该维度
---
## 六、前端设计
### 6.1 安全测试执行中心(SecurityView 内执行记录区)
- 执行记录列表上方新增 `el-radio-group` / `el-select` 筛选:全部 / 标准版 / 项目版
- 切换后重新加载执行列表
### 6.2 用例管理页
- 用例筛选区新增「项目版本」下拉(仅当查看安全类型用例时显示)
- 用例表格新增「项目版本」列(el-tag 展示:标准版=info,项目版=warning)
- 用例创建/编辑对话框新增「项目版本」单选
### 6.3 安全定时任务页
- 任务创建/编辑对话框新增「项目版本」单选(默认标准版)
- 任务列表新增「项目版本」列
---
## 七、验收标准
1. 创建一条 project_type=project 的安全用例,列表可按项目版筛选到它
2. 执行包含项目版用例的安全测试,执行记录 project_type 正确(project/mixed)
3. 执行中心按「项目版」筛选只显示 project_type=project 的执行记录
4. 创建 project_type=project 的安全定时任务,调度触发时只收集项目版安全用例
5. 存量用例/执行记录/定时任务全部表现为标准版,原有功能无回归
6. 前端 `npm run build` 通过,后端启动无报错
---
## 八、风险与约束
| 风险 | 对策 |
|------|------|
| SQLite/MySQL 旧库升级 | 通过 `_ensure_columns` 自动补齐字段(幂等) |
| UI 用例误标项目版 | 本期仅安全页面暴露编辑入口,UI 用例保持默认值 |
| 定时任务旧数据无 project_type | 字段默认 standard + 代码兼容 None(None 视为不过滤→standard) |
---
*本文档由 Claude Code 维护,实现过程中如有变更请同步更新。*
# HANDOFF — 安全测试模块会话交接文档 # HANDOFF — 安全测试模块会话交接文档
> **生成时间**: 2026-07-22 > **生成时间**: 2026-07-22
> **最后更新**: 2026-08-19 > **最后更新**: 2026-09-10
> **当前分支**: `platform-auto-test` > **当前分支**: `platform-auto-test`
> **开发窗口**: 安全测试模块 > **开发窗口**: 安全测试模块
> **最近提交**: `1a3d52ad` fix(executor): 修复执行中无法取消(MySQL 1205 行锁)并加固 SUT URL 校验 > **最近提交**: `29f3612d` feat(ui-automation): 5.60 15条用例同步验证全通过 + 钉钉形态4停止通知 + noVNC协议修复
> **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级 + ERP 配置子菜单 + ERP 上传流程对接完成 + ERP 任务创建对接完成 + **安全测试定时任务模块完成并部署 5.60**,全部已实现 > **状态**: ✅ 安全测试 P1 全部完成 + P2 执行验证完成 + 菜单升级 + ERP 配置子菜单 + ERP 上传流程对接完成 + ERP 任务创建对接完成 + 安全测试定时任务模块完成 + **安全测试用例支持标准版与项目版区分全部实现并验证通过**
--- ---
...@@ -26,6 +26,60 @@ ...@@ -26,6 +26,60 @@
## 一、本次任务记录 ## 一、本次任务记录
### 本次任务(2026-09-10):安全测试支持区分标准版与项目版(PRD文档 → 全栈实现 → 验证通过)
**背景**
业务需要区分标准版基线与项目版定制的安全测试用例(`case_type="security"`)。要求保持通用架构设计,不绑定具体项目名称,在数据库、后端业务/执行推断/定时调度、前端界面与筛选全链路打通标准版(`standard`)与项目版(`project`)的隔离与配置,确保历史数据平滑兼容。
**流程**
需求文档输出 → 计划执行文档输出 → 数据库模型迁移与兼容 → 后端模型/Schema/服务/路由 → 前端类型/API/用例/定时任务/执行中心视图 → 后端测试 487 用例全量通过 + 前端 npm run build 打包验证通过。
**核心成果**
1.**PRD 需求与计划执行文档沉淀**(遵循平台文档工作流规范):
- `Docs/PRD/自动化测试平台/_PRD_安全测试用例支持区分项目版与标准版需求文档.md`
- `Docs/PRD/自动化测试平台/_PRD_安全测试用例支持区分项目版与标准版_计划执行.md`
2.**数据库层平滑演进与后向兼容**
- `backend/app/database.py``columns_to_add` 增加三张表迁移定义:
- `test_cases.project_type VARCHAR(20) DEFAULT 'standard'`
- `executions.project_type VARCHAR(20) DEFAULT 'standard'`
- `scheduled_tasks.project_type VARCHAR(20) DEFAULT 'standard'`
- 系统启动通过 `_ensure_columns()` 自动检查与执行 `ALTER TABLE ... ADD COLUMN`(SQLite 与 MySQL 均平滑补齐,存量数据自动回填默认值 `standard`)。
3.**后端数据模型与 Schema**
- `backend/app/models/test_case.py``execution.py``scheduled_task.py`:新增 `project_type: Mapped[str] = mapped_column(String(20), default="standard")`,ORM `to_dict()` 采用 `getattr(self, "project_type", "standard") or "standard"` 防御性转换。
- `backend/app/schemas/test_case.py``TestCaseCreate``TestCaseUpdate``TestCaseResponse` 支持 `project_type`
- `backend/app/schemas/security.py``SecurityExecutionCreate`(默认 standard)、`SecurityExecutionResponse` 新增 `project_type`
- `backend/app/schemas/scheduled_task.py``ScheduledTaskCreate``ScheduledTaskUpdate` 新增 `project_type`
4.**后端服务与路由支持**
- `backend/app/services/case_service.py`:用例列表查询增加 `project_type` 过滤条件;用例创建、复制、批量导入均正确持久化与回显 `project_type`
- `backend/app/routers/cases.py``list_cases` 路由增加 `project_type: Optional[str] = Query(None)` 参数透传。
- `backend/app/services/security_service.py``create_execution` 增加 `project_type` 自动推断机制(未显式指定时,依据所选 `case_ids` 自动判断:全标准版 → `standard`,全项目版 → `project`,混合 → `mixed`,空用例 → `standard`)。
- `backend/app/routers/security.py``create_execution` 接收并透传 `project_type`,执行记录详情回显。
- `backend/app/services/scheduler_service.py``collect_module_cases` 新增 `project_type` 参数;`_run_security_task_once` 按任务自身的 `project_type` 精确过滤安全测试用例,避免跨版本混淆执行(兼容历史空任务无过滤执行)。
- `backend/app/routers/security_scheduled_tasks.py`:列表支持 `project_type` 过滤,创建/更新均完整持久化。
5.**前端界面与 API**
- `frontend/src/types/case.ts``TestCase``CreateCaseRequest` 增加 `projectType` / `project_type` 定义。
- `frontend/src/api/cases.ts``caseApi.list` 增加 `project_type` 过滤参数。
- `frontend/src/api/securityScheduledTasks.ts`:定时任务 Payload 及 `list` 方法增加 `project_type` 字段与入参。
- `frontend/src/views/Cases.vue`
- 安全测试分类下(`currentType === 'security'`)顶部工具栏增加「项目类型」下拉筛选(标准版/项目版)。
- 表格增加「项目类型」列展示标签(标准版 info / 项目版 warning)。
- 创建与编辑弹窗在安全测试用例模式下展示「项目类型」单选框(标准版 / 项目版),并附清晰业务说明。
- 保存与查询逻辑完整串联 `project_type`
- `frontend/src/views/security/ScheduledTasks.vue`
- 头部工具栏新增项目类型筛选下拉框。
- 任务表格新增「项目类型」列标签展示。
- 任务配置对话框增加项目类型单选组件(标准版/项目版)。
- `handleSave``openEdit` 完整下发与回显 `project_type`
- `frontend/src/views/Execution.vue`
- 列表新增「项目类型」列(仅安全测试模式展示),区分并高亮 `标准版` / `项目版` / `混合` 执行历史。
6.**测试验证与构建结果**
- **后端单元测试**`pytest tests/` 全量 **487 passed in 70s**(0 failed,全部通过)。
- **前端静态检查与构建**`npm run build` 打包构建成功,0 TS 错误。
- **旧库平滑迁移验证**:在 SQLite 纯旧表结构模拟环境测试,`ALTER TABLE` 顺利执行,旧记录自动平滑读取为 `'standard'`
- **后端启动验证**`app.main``SecurityService``scheduler_service` 导入与初始化完全正常。
---
### 本次任务(2026-08-19):安全测试新增定时任务模块 + 部署 5.60 验证通过 ### 本次任务(2026-08-19):安全测试新增定时任务模块 + 部署 5.60 验证通过
**背景**:安全测试模块补充定时任务能力(设置执行周期 interval/daily/weekly + 执行时间),参考 UI 自动化定时任务模块实现,复用同一张 `scheduled_tasks` 表和调度引擎,按 `case_type` 分流调度。 **背景**:安全测试模块补充定时任务能力(设置执行周期 interval/daily/weekly + 执行时间),参考 UI 自动化定时任务模块实现,复用同一张 `scheduled_tasks` 表和调度引擎,按 `case_type` 分流调度。
......
...@@ -278,6 +278,10 @@ async def _ensure_columns(conn) -> None: ...@@ -278,6 +278,10 @@ async def _ensure_columns(conn) -> None:
("performance_snapshots", "target_resource", "JSON"), ("performance_snapshots", "target_resource", "JSON"),
# UI 自动化执行:目标服务器 Java 服务异常日志采样(2026-09-08 新增) # UI 自动化执行:目标服务器 Java 服务异常日志采样(2026-09-08 新增)
("case_results", "server_log_errors", "JSON"), ("case_results", "server_log_errors", "JSON"),
# 安全测试:用例/执行/定时任务支持标准版与项目版区分(2026-09-09 新增)
("test_cases", "project_type", "VARCHAR(20) DEFAULT 'standard'"),
("executions", "project_type", "VARCHAR(20) DEFAULT 'standard'"),
("scheduled_tasks", "project_type", "VARCHAR(20) DEFAULT 'standard'"),
] ]
def _do_ensure(sync_conn) -> None: def _do_ensure(sync_conn) -> None:
......
...@@ -92,6 +92,11 @@ class Execution(Base): ...@@ -92,6 +92,11 @@ class Execution(Base):
default="ui", default="ui",
comment="用例类型: ui/api/security/deploy(用于按类型筛选执行记录)" comment="用例类型: ui/api/security/deploy(用于按类型筛选执行记录)"
) )
project_type: Mapped[str] = mapped_column(
String(20),
default="standard",
comment="项目类型: standard(标准版)/project(项目版)/mixed(混合)"
)
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime, DateTime,
default=datetime.utcnow, default=datetime.utcnow,
...@@ -141,6 +146,7 @@ class Execution(Base): ...@@ -141,6 +146,7 @@ class Execution(Base):
"environment": self.environment, "environment": self.environment,
"config": self.config or {}, "config": self.config or {},
"case_type": self.case_type, "case_type": self.case_type,
"project_type": getattr(self, "project_type", "standard") or "standard",
"created_at": self.created_at.isoformat() if self.created_at else None, "created_at": self.created_at.isoformat() if self.created_at else None,
} }
......
...@@ -65,6 +65,11 @@ class ScheduledTask(Base): ...@@ -65,6 +65,11 @@ class ScheduledTask(Base):
case_type: Mapped[str] = mapped_column( case_type: Mapped[str] = mapped_column(
String(20), default="ui", comment="用例类型: ui/security" String(20), default="ui", comment="用例类型: ui/security"
) )
project_type: Mapped[str] = mapped_column(
String(20),
default="standard",
comment="项目类型: standard(标准版)/project(项目版)"
)
config_id: Mapped[Optional[str]] = mapped_column( config_id: Mapped[Optional[str]] = mapped_column(
String(64), nullable=True, default=None, comment="安全测试配置ID(security时使用)" String(64), nullable=True, default=None, comment="安全测试配置ID(security时使用)"
) )
...@@ -132,6 +137,7 @@ class ScheduledTask(Base): ...@@ -132,6 +137,7 @@ class ScheduledTask(Base):
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"case_type": self.case_type, "case_type": self.case_type,
"project_type": getattr(self, "project_type", "standard") or "standard",
"config_id": self.config_id, "config_id": self.config_id,
"enabled": self.enabled, "enabled": self.enabled,
"module_ids": self.module_ids or [], "module_ids": self.module_ids or [],
......
...@@ -77,6 +77,11 @@ class TestCase(Base): ...@@ -77,6 +77,11 @@ class TestCase(Base):
default="ui", default="ui",
comment="用例类型: ui/api/security/deploy/performance/functional" comment="用例类型: ui/api/security/deploy/performance/functional"
) )
project_type: Mapped[str] = mapped_column(
String(20),
default="standard",
comment="项目类型: standard(标准版)/project(项目版)"
)
# 新增字段:依赖管理和参数化支持 # 新增字段:依赖管理和参数化支持
depends_on: Mapped[list] = mapped_column(JSON, default=list, comment="依赖用例ID列表") depends_on: Mapped[list] = mapped_column(JSON, default=list, comment="依赖用例ID列表")
...@@ -141,6 +146,7 @@ class TestCase(Base): ...@@ -141,6 +146,7 @@ class TestCase(Base):
"steps": self.steps or [], "steps": self.steps or [],
"config": self.config or {}, "config": self.config or {},
"case_type": self.case_type, "case_type": self.case_type,
"project_type": getattr(self, "project_type", "standard") or "standard",
"depends_on": self.depends_on or [], "depends_on": self.depends_on or [],
"parameters": self.parameters or {}, "parameters": self.parameters or {},
"condition": self.condition or {}, "condition": self.condition or {},
......
...@@ -51,6 +51,7 @@ def _case_to_response(c) -> TestCaseResponse: ...@@ -51,6 +51,7 @@ def _case_to_response(c) -> TestCaseResponse:
steps=c.steps or [], steps=c.steps or [],
config=c.config or {}, config=c.config or {},
case_type=getattr(c, "case_type", None) or "ui", case_type=getattr(c, "case_type", None) or "ui",
project_type=getattr(c, "project_type", "standard") or "standard",
parameters=c.parameters or {}, parameters=c.parameters or {},
created_at=c.created_at, created_at=c.created_at,
updated_at=c.updated_at, updated_at=c.updated_at,
...@@ -77,6 +78,7 @@ async def list_cases( ...@@ -77,6 +78,7 @@ async def list_cases(
status: Optional[str] = Query(None, description="状态筛选"), status: Optional[str] = Query(None, description="状态筛选"),
priority: Optional[str] = Query(None, description="优先级筛选"), priority: Optional[str] = Query(None, description="优先级筛选"),
case_type: Optional[str] = Query(None, description="用例类型筛选: ui/api/security/deploy"), case_type: Optional[str] = Query(None, description="用例类型筛选: ui/api/security/deploy"),
project_type: Optional[str] = Query(None, description="项目类型筛选: standard/project"),
skip: int = Query(0, ge=0, description="跳过记录数"), skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=500, description="返回记录数上限"), limit: int = Query(100, ge=1, le=500, description="返回记录数上限"),
service: CaseService = Depends(get_case_service), service: CaseService = Depends(get_case_service),
...@@ -84,7 +86,7 @@ async def list_cases( ...@@ -84,7 +86,7 @@ async def list_cases(
""" """
获取用例列表 获取用例列表
支持分页查询和按模块/关键词/状态/优先级/类型筛选。 支持分页查询和按模块/关键词/状态/优先级/类型/项目版本筛选。
Args: Args:
module_id (Optional[str]): 模块ID筛选 module_id (Optional[str]): 模块ID筛选
...@@ -92,6 +94,7 @@ async def list_cases( ...@@ -92,6 +94,7 @@ async def list_cases(
status (Optional[str]): 状态过滤 status (Optional[str]): 状态过滤
priority (Optional[str]): 优先级过滤 priority (Optional[str]): 优先级过滤
case_type (Optional[str]): 用例类型过滤 ui/api/security/deploy case_type (Optional[str]): 用例类型过滤 ui/api/security/deploy
project_type (Optional[str]): 项目类型过滤 standard/project
skip (int): 跳过的记录数 skip (int): 跳过的记录数
limit (int): 返回记录数上限 limit (int): 返回记录数上限
service (CaseService): 用例服务实例 service (CaseService): 用例服务实例
...@@ -106,6 +109,7 @@ async def list_cases( ...@@ -106,6 +109,7 @@ async def list_cases(
status=status, status=status,
priority=priority, priority=priority,
case_type=case_type, case_type=case_type,
project_type=project_type,
skip=skip, skip=skip,
limit=limit, limit=limit,
) )
......
...@@ -195,6 +195,7 @@ async def create_execution( ...@@ -195,6 +195,7 @@ async def create_execution(
module_ids=execution_data.module_ids, module_ids=execution_data.module_ids,
case_ids=execution_data.case_ids, case_ids=execution_data.case_ids,
name=execution_data.name, name=execution_data.name,
project_type=execution_data.project_type,
) )
return SecurityExecutionResponse( return SecurityExecutionResponse(
...@@ -207,6 +208,7 @@ async def create_execution( ...@@ -207,6 +208,7 @@ async def create_execution(
skipped=execution.skipped, skipped=execution.skipped,
duration=execution.duration, duration=execution.duration,
config_id=execution_data.config_id, config_id=execution_data.config_id,
project_type=getattr(execution, "project_type", "standard") or "standard",
start_time=execution.start_time.isoformat() if execution.start_time else None, start_time=execution.start_time.isoformat() if execution.start_time else None,
end_time=execution.end_time.isoformat() if execution.end_time else None, end_time=execution.end_time.isoformat() if execution.end_time else None,
) )
......
...@@ -67,6 +67,7 @@ async def _task_to_dict(db: AsyncSession, task: ScheduledTask) -> dict: ...@@ -67,6 +67,7 @@ async def _task_to_dict(db: AsyncSession, task: ScheduledTask) -> dict:
@router.get("", response_model=dict) @router.get("", response_model=dict)
async def list_tasks( async def list_tasks(
enabled: Optional[bool] = None, enabled: Optional[bool] = None,
project_type: Optional[str] = None,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
""" """
...@@ -82,6 +83,8 @@ async def list_tasks( ...@@ -82,6 +83,8 @@ async def list_tasks(
) )
if enabled is not None: if enabled is not None:
query = query.where(ScheduledTask.enabled == enabled) query = query.where(ScheduledTask.enabled == enabled)
if project_type:
query = query.where(ScheduledTask.project_type == project_type)
result = await db.execute(query) result = await db.execute(query)
tasks = list(result.scalars().all()) tasks = list(result.scalars().all())
...@@ -126,6 +129,7 @@ async def create_task( ...@@ -126,6 +129,7 @@ async def create_task(
id=generate_id("scheduled"), id=generate_id("scheduled"),
name=data.name.strip(), name=data.name.strip(),
case_type="security", case_type="security",
project_type=getattr(data, "project_type", "standard") or "standard",
config_id=data.config_id, config_id=data.config_id,
enabled=data.enabled, enabled=data.enabled,
module_ids=data.module_ids or [], module_ids=data.module_ids or [],
......
...@@ -25,6 +25,7 @@ class ScheduledTaskCreate(BaseModel): ...@@ -25,6 +25,7 @@ class ScheduledTaskCreate(BaseModel):
dingtalk_notify: bool = Field(False, description="执行完成后发送钉钉报告通知") dingtalk_notify: bool = Field(False, description="执行完成后发送钉钉报告通知")
enabled: bool = Field(True, description="是否启用") enabled: bool = Field(True, description="是否启用")
case_type: str = Field("ui", description="用例类型: ui/security") case_type: str = Field("ui", description="用例类型: ui/security")
project_type: str = Field("standard", description="项目类型: standard(标准版)/project(项目版)")
config_id: Optional[str] = Field(None, description="安全测试配置ID(security时必填)") config_id: Optional[str] = Field(None, description="安全测试配置ID(security时必填)")
...@@ -41,6 +42,7 @@ class ScheduledTaskUpdate(BaseModel): ...@@ -41,6 +42,7 @@ class ScheduledTaskUpdate(BaseModel):
dingtalk_notify: Optional[bool] = Field(None, description="发送钉钉报告通知") dingtalk_notify: Optional[bool] = Field(None, description="发送钉钉报告通知")
enabled: Optional[bool] = Field(None, description="是否启用") enabled: Optional[bool] = Field(None, description="是否启用")
case_type: Optional[str] = Field(None, description="用例类型: ui/security") case_type: Optional[str] = Field(None, description="用例类型: ui/security")
project_type: Optional[str] = Field(None, description="项目类型: standard/project")
config_id: Optional[str] = Field(None, description="安全测试配置ID") config_id: Optional[str] = Field(None, description="安全测试配置ID")
......
...@@ -112,6 +112,7 @@ class SecurityExecutionCreate(BaseModel): ...@@ -112,6 +112,7 @@ class SecurityExecutionCreate(BaseModel):
include_regression: bool = Field(True, description="是否包含历史漏洞回归测试") include_regression: bool = Field(True, description="是否包含历史漏洞回归测试")
include_redline: bool = Field(True, description="是否包含华为安全红线检查") include_redline: bool = Field(True, description="是否包含华为安全红线检查")
name: str = Field("", description="执行名称") name: str = Field("", description="执行名称")
project_type: str = Field(default="standard", description="项目类型: standard(标准版)/project(项目版)/mixed(混合)")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel) model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
...@@ -127,6 +128,7 @@ class SecurityExecutionResponse(BaseModel): ...@@ -127,6 +128,7 @@ class SecurityExecutionResponse(BaseModel):
skipped: int = 0 skipped: int = 0
duration: float = 0.0 duration: float = 0.0
config_id: str = "" config_id: str = ""
project_type: str = "standard"
start_time: Optional[str] = None start_time: Optional[str] = None
end_time: Optional[str] = None end_time: Optional[str] = None
......
...@@ -94,6 +94,10 @@ class TestCaseBase(BaseModel): ...@@ -94,6 +94,10 @@ class TestCaseBase(BaseModel):
description="执行配置" description="执行配置"
) )
case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy") case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy")
project_type: str = Field(
default="standard",
description="项目类型: standard(标准版)/project(项目版)"
)
parameters: Dict[str, Any] = Field( parameters: Dict[str, Any] = Field(
default_factory=dict, default_factory=dict,
description="用例参数(上下文变量初始值,支持 {__CTX:key__} 引用)" description="用例参数(上下文变量初始值,支持 {__CTX:key__} 引用)"
...@@ -141,6 +145,7 @@ class TestCaseUpdate(BaseModel): ...@@ -141,6 +145,7 @@ class TestCaseUpdate(BaseModel):
) )
config: Optional[Dict[str, Any]] = Field(None, description="执行配置") config: Optional[Dict[str, Any]] = Field(None, description="执行配置")
case_type: Optional[str] = Field(None, description="用例类型: ui/api/security/deploy") case_type: Optional[str] = Field(None, description="用例类型: ui/api/security/deploy")
project_type: Optional[str] = Field(None, description="项目类型: standard/project")
parameters: Optional[Dict[str, Any]] = Field(None, description="用例参数(上下文变量初始值)") parameters: Optional[Dict[str, Any]] = Field(None, description="用例参数(上下文变量初始值)")
...@@ -215,6 +220,10 @@ class TestCaseImport(BaseModel): ...@@ -215,6 +220,10 @@ class TestCaseImport(BaseModel):
description="执行配置" description="执行配置"
) )
case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy") case_type: str = Field(default="ui", description="用例类型: ui/api/security/deploy")
project_type: str = Field(
default="standard",
description="项目类型: standard(标准版)/project(项目版)"
)
parameters: Dict[str, Any] = Field( parameters: Dict[str, Any] = Field(
default_factory=dict, default_factory=dict,
description="用例参数(上下文变量初始值,支持 {__CTX:key__} 引用)" description="用例参数(上下文变量初始值,支持 {__CTX:key__} 引用)"
......
...@@ -69,13 +69,14 @@ class CaseService: ...@@ -69,13 +69,14 @@ class CaseService:
status: Optional[str] = None, status: Optional[str] = None,
priority: Optional[str] = None, priority: Optional[str] = None,
case_type: Optional[str] = None, case_type: Optional[str] = None,
project_type: Optional[str] = None,
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
) -> Tuple[List[TestCase], int]: ) -> Tuple[List[TestCase], int]:
""" """
获取用例列表(分页、筛选) 获取用例列表(分页、筛选)
支持按模块、关键词、状态、优先级、类型筛选。 支持按模块、关键词、状态、优先级、类型、项目版本筛选。
Args: Args:
module_id (Optional[str]): 模块ID筛选 module_id (Optional[str]): 模块ID筛选
...@@ -83,19 +84,12 @@ class CaseService: ...@@ -83,19 +84,12 @@ class CaseService:
status (Optional[str]): 状态筛选 (active/disabled) status (Optional[str]): 状态筛选 (active/disabled)
priority (Optional[str]): 优先级筛选 (high/medium/low) priority (Optional[str]): 优先级筛选 (high/medium/low)
case_type (Optional[str]): 用例类型筛选 (ui/api/security/deploy/performance/functional) case_type (Optional[str]): 用例类型筛选 (ui/api/security/deploy/performance/functional)
project_type (Optional[str]): 项目类型筛选 (standard/project)
skip (int): 跳过的记录数,默认0 skip (int): 跳过的记录数,默认0
limit (int): 返回记录数上限,默认100 limit (int): 返回记录数上限,默认100
Returns: Returns:
Tuple[List[TestCase], int]: (用例列表, 总数) Tuple[List[TestCase], int]: (用例列表, 总数)
Example:
>>> cases, total = await service.list(
... module_id="module_001",
... keyword="登录",
... case_type="ui",
... skip=0, limit=20
... )
""" """
# 构建查询条件 # 构建查询条件
conditions = [] conditions = []
...@@ -112,6 +106,8 @@ class CaseService: ...@@ -112,6 +106,8 @@ class CaseService:
conditions.append(TestCase.priority == priority) conditions.append(TestCase.priority == priority)
if case_type: if case_type:
conditions.append(TestCase.case_type == case_type) conditions.append(TestCase.case_type == case_type)
if project_type:
conditions.append(TestCase.project_type == project_type)
# 查询总数 # 查询总数
count_query = select(func.count(TestCase.id)) count_query = select(func.count(TestCase.id))
...@@ -219,6 +215,7 @@ class CaseService: ...@@ -219,6 +215,7 @@ class CaseService:
steps=steps_data, steps=steps_data,
config=data.config or {"timeout": 30000, "retry": 0, "screenshot": True}, config=data.config or {"timeout": 30000, "retry": 0, "screenshot": True},
case_type=data.case_type or "ui", case_type=data.case_type or "ui",
project_type=getattr(data, "project_type", "standard") or "standard",
parameters=data.parameters or {}, parameters=data.parameters or {},
) )
...@@ -317,6 +314,7 @@ class CaseService: ...@@ -317,6 +314,7 @@ class CaseService:
steps=source.steps.copy() if source.steps else [], steps=source.steps.copy() if source.steps else [],
config=source.config.copy() if source.config else {}, config=source.config.copy() if source.config else {},
case_type=source.case_type or "ui", case_type=source.case_type or "ui",
project_type=getattr(source, "project_type", "standard") or "standard",
) )
self.db.add(new_case) self.db.add(new_case)
...@@ -379,6 +377,7 @@ class CaseService: ...@@ -379,6 +377,7 @@ class CaseService:
steps=[s.model_dump() for s in case_data.steps] if case_data.steps else [], steps=[s.model_dump() for s in case_data.steps] if case_data.steps else [],
config=case_data.config or {"timeout": 30000, "retry": 0, "screenshot": True}, config=case_data.config or {"timeout": 30000, "retry": 0, "screenshot": True},
case_type=case_data.case_type or "ui", case_type=case_data.case_type or "ui",
project_type=getattr(case_data, "project_type", "standard") or "standard",
) )
self.db.add(test_case) self.db.add(test_case)
await self.db.flush() await self.db.flush()
...@@ -432,6 +431,7 @@ class CaseService: ...@@ -432,6 +431,7 @@ class CaseService:
"steps": c.steps or [], "steps": c.steps or [],
"config": c.config or {}, "config": c.config or {},
"case_type": getattr(c, "case_type", None) or "ui", "case_type": getattr(c, "case_type", None) or "ui",
"project_type": getattr(c, "project_type", "standard") or "standard",
} }
for c in cases for c in cases
] ]
\ No newline at end of file
...@@ -109,6 +109,7 @@ async def collect_module_cases( ...@@ -109,6 +109,7 @@ async def collect_module_cases(
db, db,
module_ids: List[str], module_ids: List[str],
case_type: str = "ui", case_type: str = "ui",
project_type: Optional[str] = None,
) -> List[TestCase]: ) -> List[TestCase]:
""" """
收集选中模块下所有启用的测试用例(按模块分组 + order 排序) 收集选中模块下所有启用的测试用例(按模块分组 + order 排序)
...@@ -117,6 +118,8 @@ async def collect_module_cases( ...@@ -117,6 +118,8 @@ async def collect_module_cases(
db: 异步数据库会话 db: 异步数据库会话
module_ids (List[str]): 模块 ID 列表 module_ids (List[str]): 模块 ID 列表
case_type (str): 用例类型(ui/security) case_type (str): 用例类型(ui/security)
project_type (Optional[str]): 项目类型过滤(standard/project);
为空时不过滤,兼容旧任务(2026-09-09 安全测试项目版支持新增)
Returns: Returns:
List[TestCase]: 用例列表 List[TestCase]: 用例列表
...@@ -126,13 +129,16 @@ async def collect_module_cases( ...@@ -126,13 +129,16 @@ async def collect_module_cases(
# 两段式防 MySQL 1038:TestCase 表含 steps/config/parameters 大 JSON 列, # 两段式防 MySQL 1038:TestCase 表含 steps/config/parameters 大 JSON 列,
# 整行 ORDER BY 会撑爆 sort buffer。第一段只取 id 轻量排序,第二段按 id # 整行 ORDER BY 会撑爆 sort buffer。第一段只取 id 轻量排序,第二段按 id
# 回查整行并还原顺序。 # 回查整行并还原顺序。
id_query = ( conditions = [
select(TestCase.id)
.where(
TestCase.module_id.in_(module_ids), TestCase.module_id.in_(module_ids),
TestCase.status == "active", TestCase.status == "active",
TestCase.case_type == case_type, TestCase.case_type == case_type,
) ]
if project_type:
conditions.append(TestCase.project_type == project_type)
id_query = (
select(TestCase.id)
.where(*conditions)
.order_by(TestCase.module_id, TestCase.order, TestCase.created_at) .order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
) )
ids = list((await db.execute(id_query)).scalars().all()) ids = list((await db.execute(id_query)).scalars().all())
...@@ -197,8 +203,11 @@ async def _run_security_task_once( ...@@ -197,8 +203,11 @@ async def _run_security_task_once(
from app.services.security_service import SecurityService from app.services.security_service import SecurityService
from app.services.security_report_service import SecurityReportService from app.services.security_report_service import SecurityReportService
# 1. 收集安全测试用例 # 1. 收集安全测试用例(按任务 project_type 过滤;为空视为不过滤,兼容旧任务)
cases = await collect_module_cases(db, task.module_ids or [], "security") cases = await collect_module_cases(
db, task.module_ids or [], "security",
project_type=getattr(task, "project_type", None) or None,
)
if not cases: if not cases:
logger.warning( logger.warning(
f"[定时任务] 安全任务「{task.name}」选中模块下无启用安全用例,跳过本次" f"[定时任务] 安全任务「{task.name}」选中模块下无启用安全用例,跳过本次"
......
...@@ -237,7 +237,8 @@ class SecurityService: ...@@ -237,7 +237,8 @@ class SecurityService:
config_id: str, config_id: str,
module_ids: List[str] = None, module_ids: List[str] = None,
case_ids: List[str] = None, case_ids: List[str] = None,
name: str = "" name: str = "",
project_type: Optional[str] = None,
) -> Execution: ) -> Execution:
""" """
创建安全测试执行记录 创建安全测试执行记录
...@@ -247,6 +248,7 @@ class SecurityService: ...@@ -247,6 +248,7 @@ class SecurityService:
module_ids: 要执行的模块ID列表 module_ids: 要执行的模块ID列表
case_ids: 要执行的用例ID列表 case_ids: 要执行的用例ID列表
name: 执行名称 name: 执行名称
project_type: 项目类型(standard/project/mixed,可选;若未指定则根据包含用例自动推断)
Returns: Returns:
Execution: 执行记录 Execution: 执行记录
...@@ -261,6 +263,22 @@ class SecurityService: ...@@ -261,6 +263,22 @@ class SecurityService:
total_cases = len(case_ids) total_cases = len(case_ids)
# 推断 project_type(如果未显式传参)
inferred_project_type = project_type
if not inferred_project_type:
if case_ids:
q = select(TestCase.project_type).where(TestCase.id.in_(case_ids))
res = await self.db.execute(q)
types = {r[0] or "standard" for r in res.all()}
if not types or types == {"standard"}:
inferred_project_type = "standard"
elif types == {"project"}:
inferred_project_type = "project"
else:
inferred_project_type = "mixed"
else:
inferred_project_type = "standard"
execution = Execution( execution = Execution(
id=execution_id, id=execution_id,
name=name or f"安全测试-{datetime.now().strftime('%Y%m%d_%H%M%S')}", name=name or f"安全测试-{datetime.now().strftime('%Y%m%d_%H%M%S')}",
...@@ -269,6 +287,7 @@ class SecurityService: ...@@ -269,6 +287,7 @@ class SecurityService:
total_cases=total_cases, total_cases=total_cases,
status="pending", status="pending",
case_type="security", case_type="security",
project_type=inferred_project_type,
config={"config_id": config_id, "case_ids": case_ids}, config={"config_id": config_id, "case_ids": case_ids},
) )
...@@ -276,7 +295,7 @@ class SecurityService: ...@@ -276,7 +295,7 @@ class SecurityService:
await self.db.commit() await self.db.commit()
await self.db.refresh(execution) await self.db.refresh(execution)
logger.info(f"创建安全测试执行: {execution_id}, 用例数: {total_cases}") logger.info(f"创建安全测试执行: {execution_id}, 项目类型: {inferred_project_type}, 用例数: {total_cases}")
return execution return execution
async def _get_all_security_case_ids(self) -> List[str]: async def _get_all_security_case_ids(self) -> List[str]:
......
...@@ -22,6 +22,7 @@ export const caseApi = { ...@@ -22,6 +22,7 @@ export const caseApi = {
status?: string status?: string
priority?: string priority?: string
case_type?: string case_type?: string
project_type?: string
skip?: number skip?: number
limit?: number limit?: number
}): Promise<CaseListResponse> { }): Promise<CaseListResponse> {
......
...@@ -17,6 +17,7 @@ export interface SecurityScheduledTask { ...@@ -17,6 +17,7 @@ export interface SecurityScheduledTask {
module_names: string[] module_names: string[]
config_id: string config_id: string
config_name: string config_name: string
project_type: 'standard' | 'project'
schedule_type: string schedule_type: string
interval_value: number interval_value: number
interval_unit: string interval_unit: string
...@@ -36,6 +37,7 @@ export interface SecurityScheduledTaskPayload { ...@@ -36,6 +37,7 @@ export interface SecurityScheduledTaskPayload {
name: string name: string
module_ids: string[] module_ids: string[]
config_id: string config_id: string
project_type?: 'standard' | 'project'
schedule_type: 'interval' | 'daily' | 'weekly' schedule_type: 'interval' | 'daily' | 'weekly'
interval_value?: number interval_value?: number
interval_unit?: 'minutes' | 'hours' | 'days' interval_unit?: 'minutes' | 'hours' | 'days'
...@@ -49,9 +51,9 @@ export interface SecurityScheduledTaskPayload { ...@@ -49,9 +51,9 @@ export interface SecurityScheduledTaskPayload {
* 安全测试定时任务管理 API 服务 * 安全测试定时任务管理 API 服务
*/ */
export const securityScheduledTaskApi = { export const securityScheduledTaskApi = {
/** 获取安全测试定时任务列表 */ /** 获取安全测试定时任务列表(支持项目版筛选) */
async list(): Promise<{ total: number; items: SecurityScheduledTask[] }> { async list(params?: { project_type?: string; enabled?: boolean }): Promise<{ total: number; items: SecurityScheduledTask[] }> {
const response = await request.get('/api/security/scheduled-tasks') const response = await request.get('/api/security/scheduled-tasks', { params })
return response as any return response as any
}, },
......
...@@ -102,6 +102,10 @@ export interface TestCase { ...@@ -102,6 +102,10 @@ export interface TestCase {
config: Record<string, any> config: Record<string, any>
/** 用例类型 ui/api/security/deploy/performance/functional */ /** 用例类型 ui/api/security/deploy/performance/functional */
caseType?: string caseType?: string
/** 项目类型 standard(标准版)/project(项目版) */
projectType?: 'standard' | 'project'
/** 项目类型(后端 snake_case 返回时使用) */
project_type?: 'standard' | 'project'
/** 用例参数(上下文变量初始值,支持 {__CTX:key__} 引用) */ /** 用例参数(上下文变量初始值,支持 {__CTX:key__} 引用) */
parameters?: Record<string, any> parameters?: Record<string, any>
/** 创建时间 */ /** 创建时间 */
...@@ -130,6 +134,8 @@ export interface CreateCaseRequest { ...@@ -130,6 +134,8 @@ export interface CreateCaseRequest {
config?: Record<string, any> config?: Record<string, any>
/** 用例类型 ui/api/security/deploy/performance/functional */ /** 用例类型 ui/api/security/deploy/performance/functional */
case_type?: string case_type?: string
/** 项目类型 standard(标准版)/project(项目版) */
project_type?: string
/** 用例参数(上下文变量初始值) */ /** 用例参数(上下文变量初始值) */
parameters?: Record<string, any> parameters?: Record<string, any>
} }
......
...@@ -54,6 +54,17 @@ ...@@ -54,6 +54,17 @@
<el-option label="中" value="medium" /> <el-option label="中" value="medium" />
<el-option label="低" value="low" /> <el-option label="低" value="low" />
</el-select> </el-select>
<el-select
v-if="currentType === 'security'"
v-model="filterProjectType"
placeholder="项目类型"
clearable
style="width: 120px"
@change="loadCases"
>
<el-option label="标准版" value="standard" />
<el-option label="项目版" value="project" />
</el-select>
</div> </div>
<div class="header-right"> <div class="header-right">
<el-button type="primary" @click="showCreateDialog"> <el-button type="primary" @click="showCreateDialog">
...@@ -133,6 +144,21 @@ ...@@ -133,6 +144,21 @@
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
v-if="currentType === 'security'"
label="项目类型"
width="90"
>
<template #default="{ row }">
<el-tag
:type="getProjectType(row) === 'project' ? 'warning' : 'info'"
size="small"
effect="light"
>
{{ getProjectType(row) === 'project' ? '项目版' : '标准版' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="步骤数" width="80" align="center"> <el-table-column label="步骤数" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<template v-if="row.caseType === 'security'"> <template v-if="row.caseType === 'security'">
...@@ -356,6 +382,15 @@ ...@@ -356,6 +382,15 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="项目类型" v-if="editForm.case_type === 'security'">
<el-radio-group v-model="editForm.project_type">
<el-radio value="standard">标准版</el-radio>
<el-radio value="project">项目版</el-radio>
</el-radio-group>
<div style="width: 100%; color: #999; font-size: 12px; margin-top: 4px">
项目版安全测试用例与标准版基线独立维护,仅项目版环境执行
</div>
</el-form-item>
<el-form-item label="优先级"> <el-form-item label="优先级">
<el-select v-model="editForm.priority" style="width: 100%"> <el-select v-model="editForm.priority" style="width: 100%">
<el-option label="高" value="high" /> <el-option label="高" value="high" />
...@@ -650,6 +685,7 @@ const filterModuleId = ref('') ...@@ -650,6 +685,7 @@ const filterModuleId = ref('')
const keyword = ref('') const keyword = ref('')
const filterStatus = ref('') const filterStatus = ref('')
const filterPriority = ref('') const filterPriority = ref('')
const filterProjectType = ref('') // 项目类型筛选(仅 security 用例)
const selectedCases = ref<any[]>([]) const selectedCases = ref<any[]>([])
// 模块列表 // 模块列表
...@@ -672,6 +708,7 @@ const editForm = reactive({ ...@@ -672,6 +708,7 @@ const editForm = reactive({
status: 'active', status: 'active',
tags: [] as string[], tags: [] as string[],
case_type: 'ui', case_type: 'ui',
project_type: 'standard' as 'standard' | 'project',
steps: [] as any[], steps: [] as any[],
intentSteps: [] as IntentStep[], intentSteps: [] as IntentStep[],
parameters: {} as Record<string, any>, parameters: {} as Record<string, any>,
...@@ -721,6 +758,7 @@ const loadCases = async () => { ...@@ -721,6 +758,7 @@ const loadCases = async () => {
status: filterStatus.value, status: filterStatus.value,
priority: filterPriority.value, priority: filterPriority.value,
case_type: currentType.value, case_type: currentType.value,
project_type: currentType.value === 'security' ? filterProjectType.value : undefined,
skip: (currentPage.value - 1) * pageSize.value, skip: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value, limit: pageSize.value,
}) })
...@@ -760,6 +798,11 @@ const getPriorityText = (priority: string) => { ...@@ -760,6 +798,11 @@ const getPriorityText = (priority: string) => {
return map[priority] || priority return map[priority] || priority
} }
/** 项目类型(standard/project,兼容 snake_case 与 camelCase 返回) */
const getProjectType = (row: any): string => {
return String(row.project_type || row.projectType || 'standard').toLowerCase()
}
/** 安全测试风险等级 emoji */ /** 安全测试风险等级 emoji */
const getSecurityLevelEmoji = (level?: string) => { const getSecurityLevelEmoji = (level?: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {
...@@ -836,6 +879,7 @@ const showCreateDialog = () => { ...@@ -836,6 +879,7 @@ const showCreateDialog = () => {
editForm.status = 'active' editForm.status = 'active'
editForm.tags = [] editForm.tags = []
editForm.case_type = 'ui' editForm.case_type = 'ui'
editForm.project_type = 'standard'
editForm.steps = [] editForm.steps = []
editForm.parameters = {} editForm.parameters = {}
parametersText.value = '' parametersText.value = ''
...@@ -863,6 +907,10 @@ const showEditDialog = (row: any) => { ...@@ -863,6 +907,10 @@ const showEditDialog = (row: any) => {
editForm.status = row.status || 'active' editForm.status = row.status || 'active'
editForm.tags = row.tags || [] editForm.tags = row.tags || []
editForm.case_type = row.caseType || row.case_type || currentType.value editForm.case_type = row.caseType || row.case_type || currentType.value
editForm.project_type =
row.project_type === 'project' || row.projectType === 'project'
? 'project'
: 'standard'
editForm.steps = (row.steps || []).map((s: any) => ({ ...s })) editForm.steps = (row.steps || []).map((s: any) => ({ ...s }))
editForm.parameters = row.parameters || {} editForm.parameters = row.parameters || {}
parametersText.value = Object.keys(editForm.parameters).length parametersText.value = Object.keys(editForm.parameters).length
...@@ -933,6 +981,7 @@ const saveCase = async () => { ...@@ -933,6 +981,7 @@ const saveCase = async () => {
status: editForm.status, status: editForm.status,
tags: editForm.tags, tags: editForm.tags,
case_type: editForm.case_type, case_type: editForm.case_type,
project_type: editForm.case_type === 'security' ? editForm.project_type : 'standard',
steps: editForm.steps, steps: editForm.steps,
parameters: editForm.parameters, parameters: editForm.parameters,
}) })
...@@ -946,6 +995,7 @@ const saveCase = async () => { ...@@ -946,6 +995,7 @@ const saveCase = async () => {
status: editForm.status, status: editForm.status,
tags: editForm.tags, tags: editForm.tags,
case_type: editForm.case_type, case_type: editForm.case_type,
project_type: editForm.case_type === 'security' ? editForm.project_type : 'standard',
steps: editForm.steps, steps: editForm.steps,
parameters: editForm.parameters, parameters: editForm.parameters,
}) })
......
...@@ -201,6 +201,22 @@ ...@@ -201,6 +201,22 @@
<el-tag v-else type="primary" size="small">手动</el-tag> <el-tag v-else type="primary" size="small">手动</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<!-- 安全测试显示项目类型(标准版/项目版/混合) -->
<el-table-column label="项目类型" width="90" v-if="currentType === 'security'">
<template #default="{ row }">
<el-tag :type="projectTypeTagType(row)" size="small" effect="light">
{{ projectTypeText(row) }}
</el-tag>
</template>
</el-table-column>
<!-- 安全测试显示项目类型(标准版/项目版/混合) -->
<el-table-column label="项目类型" width="90" v-if="currentType === 'security'">
<template #default="{ row }">
<el-tag :type="projectTypeTagType(row)" size="small" effect="light">
{{ projectTypeText(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="时间" width="180"> <el-table-column label="时间" width="180">
<template #default="{ row }"> <template #default="{ row }">
{{ formatTime(row.created_at) }} {{ formatTime(row.created_at) }}
...@@ -542,6 +558,21 @@ const getPassRateClass = (rate: number) => { ...@@ -542,6 +558,21 @@ const getPassRateClass = (rate: number) => {
return 'rate-danger' return 'rate-danger'
} }
/** 项目类型标签样式(standard/project/mixed,兼容 snake_case 与 camelCase) */
const projectTypeText = (row: any) => {
const t = String(row.project_type || row.projectType || 'standard').toLowerCase()
if (t === 'project') return '项目版'
if (t === 'mixed') return '混合'
return '标准版'
}
const projectTypeTagType = (row: any) => {
const t = String(row.project_type || row.projectType || 'standard').toLowerCase()
if (t === 'project') return 'warning'
if (t === 'mixed') return 'danger'
return 'info'
}
const formatTime = (time: string) => { const formatTime = (time: string) => {
if (!time) return '-' if (!time) return '-'
return new Date(time).toLocaleString('zh-CN', { return new Date(time).toLocaleString('zh-CN', {
......
...@@ -12,6 +12,16 @@ ...@@ -12,6 +12,16 @@
<div class="page-header"> <div class="page-header">
<h2>定时任务</h2> <h2>定时任务</h2>
<div class="header-actions"> <div class="header-actions">
<el-select
v-model="filterProjectType"
placeholder="项目类型"
clearable
style="width: 120px"
@change="loadTasks"
>
<el-option label="标准版" value="standard" />
<el-option label="项目版" value="project" />
</el-select>
<el-button @click="loadTasks"> <el-button @click="loadTasks">
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
刷新 刷新
...@@ -62,6 +72,17 @@ ...@@ -62,6 +72,17 @@
<el-tag size="small" type="warning">{{ row.config_name || '-' }}</el-tag> <el-tag size="small" type="warning">{{ row.config_name || '-' }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="项目类型" width="90" align="center">
<template #default="{ row }">
<el-tag
:type="row.project_type === 'project' ? 'warning' : 'info'"
size="small"
effect="light"
>
{{ row.project_type === 'project' ? '项目版' : '标准版' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="schedule_description" label="周期" min-width="140" /> <el-table-column prop="schedule_description" label="周期" min-width="140" />
<el-table-column label="自动报告" width="90" align="center"> <el-table-column label="自动报告" width="90" align="center">
<template #default="{ row }"> <template #default="{ row }">
...@@ -181,6 +202,14 @@ ...@@ -181,6 +202,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="项目类型" prop="project_type">
<el-radio-group v-model="form.project_type">
<el-radio-button value="standard">标准版</el-radio-button>
<el-radio-button value="project">项目版</el-radio-button>
</el-radio-group>
<span class="form-tip">项目版任务仅执行项目版安全测试用例</span>
</el-form-item>
<el-form-item label="周期模式" prop="schedule_type"> <el-form-item label="周期模式" prop="schedule_type">
<el-radio-group v-model="form.schedule_type"> <el-radio-group v-model="form.schedule_type">
<el-radio-button value="interval">按间隔</el-radio-button> <el-radio-button value="interval">按间隔</el-radio-button>
...@@ -365,6 +394,7 @@ interface TaskForm { ...@@ -365,6 +394,7 @@ interface TaskForm {
name: string name: string
module_ids: string[] module_ids: string[]
config_id: string config_id: string
project_type: 'standard' | 'project'
schedule_type: 'interval' | 'daily' | 'weekly' schedule_type: 'interval' | 'daily' | 'weekly'
interval_value: number interval_value: number
interval_unit: 'minutes' | 'hours' | 'days' interval_unit: 'minutes' | 'hours' | 'days'
...@@ -378,6 +408,7 @@ const createEmptyForm = (): TaskForm => ({ ...@@ -378,6 +408,7 @@ const createEmptyForm = (): TaskForm => ({
name: '', name: '',
module_ids: [], module_ids: [],
config_id: '', config_id: '',
project_type: 'standard',
schedule_type: 'interval', schedule_type: 'interval',
interval_value: 30, interval_value: 30,
interval_unit: 'minutes', interval_unit: 'minutes',
...@@ -478,10 +509,14 @@ const statusTag = (status: string) => { ...@@ -478,10 +509,14 @@ const statusTag = (status: string) => {
return map[status] || 'info' return map[status] || 'info'
} }
const filterProjectType = ref('')
const loadTasks = async () => { const loadTasks = async () => {
loading.value = true loading.value = true
try { try {
const res = await securityScheduledTaskApi.list() const params: any = {}
if (filterProjectType.value) params.project_type = filterProjectType.value
const res = await securityScheduledTaskApi.list(params)
tasks.value = res.items tasks.value = res.items
} catch (err: any) { } catch (err: any) {
ElMessage.error('加载安全测试定时任务失败:' + (err?.message || '未知错误')) ElMessage.error('加载安全测试定时任务失败:' + (err?.message || '未知错误'))
...@@ -532,6 +567,7 @@ const openEdit = async (task: SecurityScheduledTask) => { ...@@ -532,6 +567,7 @@ const openEdit = async (task: SecurityScheduledTask) => {
name: task.name, name: task.name,
module_ids: [...(task.module_ids || [])], module_ids: [...(task.module_ids || [])],
config_id: task.config_id || '', config_id: task.config_id || '',
project_type: task.project_type === 'project' ? 'project' : 'standard',
schedule_type: task.schedule_type, schedule_type: task.schedule_type,
interval_value: task.interval_value, interval_value: task.interval_value,
interval_unit: task.interval_unit, interval_unit: task.interval_unit,
...@@ -556,6 +592,7 @@ const handleSave = async () => { ...@@ -556,6 +592,7 @@ const handleSave = async () => {
name: form.name.trim(), name: form.name.trim(),
module_ids: form.module_ids, module_ids: form.module_ids,
config_id: form.config_id, config_id: form.config_id,
project_type: form.project_type,
schedule_type: form.schedule_type, schedule_type: form.schedule_type,
interval_value: form.interval_value, interval_value: form.interval_value,
interval_unit: form.interval_unit, interval_unit: form.interval_unit,
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论