增加项目的各个功能
This commit is contained in:
807
.agents/RBAC_API.md
Normal file
807
.agents/RBAC_API.md
Normal file
@@ -0,0 +1,807 @@
|
||||
# RBAC / 用户 / 菜单管理接口文档
|
||||
|
||||
本文档基于当前已落地代码整理,适合直接给前端联调使用。
|
||||
|
||||
## 1. 通用说明
|
||||
|
||||
### 1.1 响应结构
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 20000,
|
||||
"message": "",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 40009,
|
||||
"message": "具体错误信息",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 错误码使用习惯
|
||||
|
||||
| code | 说明 |
|
||||
|---|---|
|
||||
| 20000 | 成功 |
|
||||
| 40009 | 创建类失败 / 参数校验失败 |
|
||||
| 40011 | 详情查询失败 |
|
||||
| 40012 | 更新 / 删除 / 分配失败 |
|
||||
|
||||
### 1.3 当前实现注意事项
|
||||
|
||||
- 用户密码字段当前直接写入 `password_hash`,还未做真正加密
|
||||
- 分配类接口均为覆盖式保存
|
||||
- 当前密码字段是占位实现,后续建议替换为真实 hash
|
||||
|
||||
---
|
||||
|
||||
## 2. 角色管理
|
||||
|
||||
### 2.1 角色列表
|
||||
- 方法:`GET`
|
||||
- 路径:`/role/list`
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| keyword | string | 否 | 角色名称模糊搜索 |
|
||||
| status | int | 否 | 1启用 0禁用 |
|
||||
| pageNo | int | 否 | 页码,默认1 |
|
||||
| pageSize | int | 否 | 每页条数,默认20 |
|
||||
|
||||
返回 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"code": "admin",
|
||||
"name": "超级管理员",
|
||||
"description": "系统内置超级管理员",
|
||||
"status": 1,
|
||||
"is_system": 1,
|
||||
"created_by": 1,
|
||||
"created_time": "2025-01-01 10:00:00",
|
||||
"updated_time": "2025-01-01 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 角色详情
|
||||
- 方法:`GET`
|
||||
- 路径:`/role/detail`
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| roleId | int | 是 | 角色ID |
|
||||
|
||||
返回:单个角色对象。
|
||||
|
||||
### 2.3 创建角色
|
||||
- 方法:`POST`
|
||||
- 路径:`/role/create`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "test_manager",
|
||||
"name": "测试经理",
|
||||
"description": "测试经理角色",
|
||||
"status": 1,
|
||||
"isSystem": 0,
|
||||
"createdBy": 1
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 更新角色
|
||||
- 方法:`POST`
|
||||
- 路径:`/role/update`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 2,
|
||||
"name": "高级测试经理",
|
||||
"description": "升级后的测试经理角色"
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 删除角色
|
||||
- 方法:`POST`
|
||||
- 路径:`/role/delete`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 2
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 权限管理
|
||||
|
||||
### 3.1 权限列表
|
||||
- 方法:`GET`
|
||||
- 路径:`/permission/list`
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| keyword | string | 否 | 权限名称模糊搜索 |
|
||||
| module | string | 否 | 模块名 |
|
||||
| status | int | 否 | 状态 |
|
||||
| pageNo | int | 否 | 页码 |
|
||||
| pageSize | int | 否 | 每页条数 |
|
||||
|
||||
返回 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"code": "user:create",
|
||||
"name": "创建用户",
|
||||
"module": "user",
|
||||
"action": "create",
|
||||
"description": "创建用户权限",
|
||||
"status": 1,
|
||||
"created_time": "2025-01-01 10:00:00",
|
||||
"updated_time": "2025-01-01 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 权限详情
|
||||
- 方法:`GET`
|
||||
- 路径:`/permission/detail`
|
||||
- 参数:`permissionId`
|
||||
|
||||
### 3.3 创建权限
|
||||
- 方法:`POST`
|
||||
- 路径:`/permission/create`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "user:create",
|
||||
"name": "创建用户",
|
||||
"module": "user",
|
||||
"action": "create",
|
||||
"description": "创建用户权限",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 更新权限
|
||||
- 方法:`POST`
|
||||
- 路径:`/permission/update`
|
||||
|
||||
### 3.5 删除权限
|
||||
- 方法:`POST`
|
||||
- 路径:`/permission/delete`
|
||||
|
||||
---
|
||||
|
||||
## 4. 菜单管理
|
||||
|
||||
### 4.1 菜单树
|
||||
- 方法:`GET`
|
||||
- 路径:`/menu/tree`
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| status | int | 否 | 状态过滤 |
|
||||
|
||||
返回 `data`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"parent_id": 0,
|
||||
"name": "系统管理",
|
||||
"code": "system",
|
||||
"type": 1,
|
||||
"path": "/system",
|
||||
"component": "Layout",
|
||||
"icon": "setting",
|
||||
"permission_code": null,
|
||||
"sort": 1,
|
||||
"visible": 1,
|
||||
"status": 1,
|
||||
"created_time": "2025-01-01 10:00:00",
|
||||
"updated_time": "2025-01-01 10:00:00",
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"parent_id": 1,
|
||||
"name": "用户管理",
|
||||
"code": "user_manage",
|
||||
"type": 2,
|
||||
"path": "/system/user",
|
||||
"component": "system/user/index",
|
||||
"icon": "user",
|
||||
"permission_code": "user:list",
|
||||
"sort": 1,
|
||||
"visible": 1,
|
||||
"status": 1,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4.2 菜单详情
|
||||
- 方法:`GET`
|
||||
- 路径:`/menu/detail`
|
||||
- 参数:`menuId`
|
||||
|
||||
### 4.3 创建菜单
|
||||
- 方法:`POST`
|
||||
- 路径:`/menu/create`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"parentId": 1,
|
||||
"name": "角色管理",
|
||||
"code": "role_manage",
|
||||
"type": 2,
|
||||
"path": "/system/role",
|
||||
"component": "system/role/index",
|
||||
"icon": "peoples",
|
||||
"permissionCode": "role:list",
|
||||
"sort": 2,
|
||||
"visible": 1,
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 更新菜单
|
||||
- 方法:`POST`
|
||||
- 路径:`/menu/update`
|
||||
|
||||
### 4.5 删除菜单
|
||||
- 方法:`POST`
|
||||
- 路径:`/menu/delete`
|
||||
|
||||
---
|
||||
|
||||
## 5. 角色权限分配
|
||||
|
||||
### 5.1 查询角色权限
|
||||
- 方法:`GET`
|
||||
- 路径:`/role/permission/list`
|
||||
- 参数:`roleId`
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissionIds": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 分配角色权限
|
||||
- 方法:`POST`
|
||||
- 路径:`/role/permission/assign`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 2,
|
||||
"permissionIds": [1, 2, 3, 4]
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 角色菜单分配
|
||||
|
||||
### 6.1 查询角色菜单
|
||||
- 方法:`GET`
|
||||
- 路径:`/role/menu/list`
|
||||
- 参数:`roleId`
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"menuIds": [1, 2, 3, 4]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 分配角色菜单
|
||||
- 方法:`POST`
|
||||
- 路径:`/role/menu/assign`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 2,
|
||||
"menuIds": [1, 2, 10, 11]
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 用户管理
|
||||
|
||||
### 7.1 用户列表
|
||||
- 方法:`GET`
|
||||
- 路径:`/user/list`
|
||||
|
||||
请求参数:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| keyword | string | 否 | 用户名模糊搜索 |
|
||||
| status | int | 否 | 状态 |
|
||||
| pageNo | int | 否 | 页码 |
|
||||
| pageSize | int | 否 | 每页条数 |
|
||||
|
||||
返回 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"real_name": "管理员",
|
||||
"mobile": "13800000000",
|
||||
"email": "admin@test.com",
|
||||
"avatar": "",
|
||||
"status": 1,
|
||||
"last_login_time": "2025-01-01 10:00:00",
|
||||
"created_by": 1,
|
||||
"created_time": "2025-01-01 10:00:00",
|
||||
"updated_time": "2025-01-01 10:00:00",
|
||||
"role_ids": [1, 2],
|
||||
"role_names": ["管理员", "测试经理"]
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 用户详情
|
||||
- 方法:`GET`
|
||||
- 路径:`/user/detail`
|
||||
- 参数:`userId`
|
||||
|
||||
返回会额外包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"role_ids": [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 创建用户
|
||||
- 方法:`POST`
|
||||
- 路径:`/user/create`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "zhangsan",
|
||||
"password": "123456",
|
||||
"realName": "张三",
|
||||
"mobile": "13800001111",
|
||||
"email": "zhangsan@test.com",
|
||||
"avatar": "",
|
||||
"status": 1,
|
||||
"createdBy": 1
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 更新用户
|
||||
- 方法:`POST`
|
||||
- 路径:`/user/update`
|
||||
|
||||
### 7.5 删除用户
|
||||
- 方法:`POST`
|
||||
- 路径:`/user/delete`
|
||||
|
||||
---
|
||||
|
||||
## 8. 用户角色分配
|
||||
|
||||
### 8.1 查询用户角色
|
||||
- 方法:`GET`
|
||||
- 路径:`/user/role/list`
|
||||
- 参数:`userId`
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"roleIds": [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 分配用户角色
|
||||
- 方法:`POST`
|
||||
- 路径:`/user/role/assign`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": 10,
|
||||
"roleIds": [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 认证接口
|
||||
|
||||
### 9.1 注册
|
||||
- 方法:`POST`
|
||||
- 路径:`/auth/register`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "zhangsan",
|
||||
"password": "123456",
|
||||
"realName": "张三",
|
||||
"mobile": "13800001111",
|
||||
"email": "zhangsan@test.com",
|
||||
"avatar": "",
|
||||
"createdBy": 1
|
||||
}
|
||||
```
|
||||
|
||||
请求参数说明:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| username | string | 是 | 登录用户名 |
|
||||
| password | string | 是 | 登录密码,当前直接写入 `password_hash` |
|
||||
| realName | string | 否 | 真实姓名 |
|
||||
| mobile | string | 否 | 手机号 |
|
||||
| email | string | 否 | 邮箱 |
|
||||
| avatar | string | 否 | 头像 |
|
||||
| createdBy | int | 否 | 创建人 |
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 11
|
||||
}
|
||||
```
|
||||
|
||||
失败场景:
|
||||
- `username、password 为必传参数`
|
||||
- `用户名已存在!`
|
||||
|
||||
### 9.2 登录
|
||||
- 方法:`POST`
|
||||
- 路径:`/auth/login`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "zhangsan",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
请求参数说明:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| username | string | 是 | 登录用户名 |
|
||||
| password | string | 是 | 登录密码 |
|
||||
|
||||
成功返回 `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 11,
|
||||
"username": "zhangsan",
|
||||
"real_name": "张三",
|
||||
"mobile": "13800001111",
|
||||
"email": "zhangsan@test.com",
|
||||
"avatar": "",
|
||||
"status": 1,
|
||||
"last_login_time": null,
|
||||
"created_by": 1,
|
||||
"created_time": "2025-01-01 10:00:00",
|
||||
"updated_time": "2025-01-01 10:00:00",
|
||||
"role_ids": [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
失败场景:
|
||||
- `username、password 为必传参数`
|
||||
- `用户名或密码错误!`
|
||||
- `用户已禁用!`
|
||||
|
||||
登录成功额外返回:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| token | string | 登录令牌,存入 Redis |
|
||||
| token_type | string | 固定为 `Bearer` |
|
||||
| expires_in | int | token 过期时间,单位秒,当前为 7200 |
|
||||
| refresh_threshold_seconds | int | 自动续期阈值,单位秒,当前为 1800 |
|
||||
| refresh_mechanism | string | 刷新机制说明 |
|
||||
|
||||
当前 token 机制:
|
||||
- token 存储位置:Redis
|
||||
- Redis key 前缀:`effekt:token:`
|
||||
- token 过期时间:`7200` 秒(2小时)
|
||||
- 刷新机制:访问任意需要登录的接口时,如果 token 剩余有效期小于 `1800` 秒,则自动续期到完整 2 小时
|
||||
- 请求头支持:
|
||||
- `accessToken`
|
||||
- `accesstoken`
|
||||
- `Authorization: Bearer <token>`
|
||||
|
||||
> 当前登录接口已返回 token、过期时间和刷新机制说明。
|
||||
|
||||
---
|
||||
|
||||
## 10. 一组联调示例
|
||||
|
||||
### 9.1 创建角色
|
||||
|
||||
```http
|
||||
POST /role/create
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "tester",
|
||||
"name": "测试人员",
|
||||
"description": "普通测试角色",
|
||||
"status": 1,
|
||||
"isSystem": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 创建权限
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "case:list",
|
||||
"name": "查看用例列表",
|
||||
"module": "case",
|
||||
"action": "list",
|
||||
"description": "查看测试用例列表",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 创建菜单
|
||||
|
||||
```json
|
||||
{
|
||||
"parentId": 1,
|
||||
"name": "权限管理",
|
||||
"code": "permission_manage",
|
||||
"type": 2,
|
||||
"path": "/system/permission",
|
||||
"component": "system/permission/index",
|
||||
"icon": "lock",
|
||||
"permissionCode": "permission:list",
|
||||
"sort": 3,
|
||||
"visible": 1,
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 给角色分配权限
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 5,
|
||||
"permissionIds": [1, 2, 3, 4]
|
||||
}
|
||||
```
|
||||
|
||||
### 9.5 给角色分配菜单
|
||||
|
||||
```json
|
||||
{
|
||||
"roleId": 5,
|
||||
"menuIds": [1, 2, 8, 9]
|
||||
}
|
||||
```
|
||||
|
||||
### 9.6 创建用户
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "lisi",
|
||||
"password": "123456",
|
||||
"realName": "李四",
|
||||
"mobile": "13800002222",
|
||||
"email": "lisi@test.com",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 10.7 给用户分配角色
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": 10,
|
||||
"roleIds": [5]
|
||||
}
|
||||
```
|
||||
|
||||
### 10.8 注册
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "new_user",
|
||||
"password": "123456",
|
||||
"realName": "新用户",
|
||||
"mobile": "13800009999",
|
||||
"email": "new_user@test.com"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.9 登录
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "new_user",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.10 鉴权说明
|
||||
|
||||
请求受保护接口时,请在请求头中携带以下任意一种:
|
||||
|
||||
```text
|
||||
accessToken: <token>
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```text
|
||||
accesstoken: <token>
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```text
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
当前机制:
|
||||
- token 存 Redis
|
||||
- 默认有效期:2 小时
|
||||
- 剩余有效期小于 30 分钟时,访问受保护接口会自动续期
|
||||
- 注册、登录接口不需要 token
|
||||
- 其他接口已逐步接入登录鉴权与权限限制
|
||||
|
||||
---
|
||||
|
||||
## 11. 当前初始化 SQL 已包含的业务菜单
|
||||
|
||||
已补入以下可直接录入的菜单数据:
|
||||
|
||||
### 系统管理
|
||||
- `system` 系统管理
|
||||
- `role_manage` 角色管理
|
||||
- `user_manage` 用户管理
|
||||
- `permission_manage` 权限管理
|
||||
- `menu_manage` 菜单管理
|
||||
|
||||
### 测试平台
|
||||
- `test_platform` 测试平台
|
||||
- `product_manage` 产品管理
|
||||
- `project_manage` 项目管理
|
||||
- `case_manage` 用例管理
|
||||
- `plan_manage` 测试计划
|
||||
- `report_manage` 测试报告
|
||||
|
||||
### 造数工具
|
||||
- `data_tools` 造数工具
|
||||
- `data_builder_manage` 数据库造数
|
||||
- `data_factory_manage` 造数工厂
|
||||
|
||||
如果后续你要,我可以继续补:
|
||||
|
||||
1. Swagger/OpenAPI 版本
|
||||
2. Apifox / Postman 导入版
|
||||
3. 初始化权限菜单角色的更完整种子数据
|
||||
503
.agents/rbac_init.sql
Normal file
503
.agents/rbac_init.sql
Normal file
@@ -0,0 +1,503 @@
|
||||
-- RBAC / 用户 / 菜单 管理建表与初始化 SQL
|
||||
-- PostgreSQL
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user" (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
real_name VARCHAR(64),
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
mobile VARCHAR(32),
|
||||
email VARCHAR(128),
|
||||
avatar VARCHAR(255),
|
||||
status SMALLINT DEFAULT 1,
|
||||
last_login_time TIMESTAMP NULL,
|
||||
created_by BIGINT,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_status ON "user" (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_is_delete ON "user" (is_delete);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code VARCHAR(64) NOT NULL UNIQUE,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
description TEXT,
|
||||
status SMALLINT DEFAULT 1,
|
||||
is_system SMALLINT DEFAULT 0,
|
||||
created_by BIGINT,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_status ON role (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_is_delete ON role (is_delete);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
module VARCHAR(64),
|
||||
action VARCHAR(64),
|
||||
description TEXT,
|
||||
status SMALLINT DEFAULT 1,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_permission_module ON permission (module);
|
||||
CREATE INDEX IF NOT EXISTS idx_permission_status ON permission (status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS menu (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
parent_id BIGINT DEFAULT 0,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
code VARCHAR(64) UNIQUE,
|
||||
type SMALLINT DEFAULT 1,
|
||||
path VARCHAR(255),
|
||||
component VARCHAR(255),
|
||||
icon VARCHAR(64),
|
||||
permission_code VARCHAR(128),
|
||||
sort INTEGER DEFAULT 0,
|
||||
visible SMALLINT DEFAULT 1,
|
||||
status SMALLINT DEFAULT 1,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_menu_parent_id ON menu (parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_menu_sort ON menu (sort);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role (role_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS role_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uk_role_permission UNIQUE (role_id, permission_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON role_permission (role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON role_permission (permission_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS role_menu (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
menu_id BIGINT NOT NULL,
|
||||
is_delete INTEGER DEFAULT 0,
|
||||
created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uk_role_menu UNIQUE (role_id, menu_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_menu_role_id ON role_menu (role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_menu_menu_id ON role_menu (menu_id);
|
||||
|
||||
INSERT INTO role (code, name, description, status, is_system, created_by, is_delete)
|
||||
SELECT 'admin', '超级管理员', '系统内置超级管理员', 1, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM role WHERE code = 'admin');
|
||||
|
||||
INSERT INTO role (code, name, description, status, is_system, created_by, is_delete)
|
||||
SELECT 'test_manager', '测试经理', '系统内置测试经理角色', 1, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM role WHERE code = 'test_manager');
|
||||
|
||||
INSERT INTO role (code, name, description, status, is_system, created_by, is_delete)
|
||||
SELECT 'test_engineer', '测试工程师', '系统内置测试工程师角色', 1, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM role WHERE code = 'test_engineer');
|
||||
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:list', '角色列表', 'role', 'list', '查看角色列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:create', '创建角色', 'role', 'create', '创建角色', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:update', '更新角色', 'role', 'update', '更新角色', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:delete', '删除角色', 'role', 'delete', '删除角色', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:list', '用户列表', 'user', 'list', '查看用户列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:create', '创建用户', 'user', 'create', '创建用户', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:update', '更新用户', 'user', 'update', '更新用户', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:delete', '删除用户', 'user', 'delete', '删除用户', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:list', '权限列表', 'permission', 'list', '查看权限列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:list', '菜单列表', 'menu', 'list', '查看菜单树', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:list');
|
||||
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:detail', '角色详情', 'role', 'detail', '查看角色详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:detail', '权限详情', 'permission', 'detail', '查看权限详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:create', '创建权限', 'permission', 'create', '创建权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:update', '更新权限', 'permission', 'update', '更新权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:delete', '删除权限', 'permission', 'delete', '删除权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:detail', '菜单详情', 'menu', 'detail', '查看菜单详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:create', '创建菜单', 'menu', 'create', '创建菜单', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:update', '更新菜单', 'menu', 'update', '更新菜单', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:delete', '删除菜单', 'menu', 'delete', '删除菜单', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:detail', '用户详情', 'user', 'detail', '查看用户详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:list', '项目列表', 'project', 'list', '查看项目列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:detail', '项目详情', 'project', 'detail', '查看项目详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:create', '创建项目', 'project', 'create', '创建项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:update', '更新项目', 'project', 'update', '更新项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:delete', '删除项目', 'project', 'delete', '删除项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project_member:list', '项目成员列表', 'project_member', 'list', '查看项目成员列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project_member:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project_member:create', '创建项目成员', 'project_member', 'create', '创建项目成员', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project_member:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'product:list', '产品列表', 'product', 'list', '查看产品列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'product:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'product:detail', '产品详情', 'product', 'detail', '查看产品详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'product:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'product:create', '创建产品', 'product', 'create', '创建产品', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'product:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'product:update', '更新产品', 'product', 'update', '更新产品', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'product:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'product:delete', '删除产品', 'product', 'delete', '删除产品', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'product:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'module:list', '模块列表', 'module', 'list', '查看模块列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'module:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'module:create', '创建模块', 'module', 'create', '创建模块', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'module:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'module:update', '更新模块', 'module', 'update', '更新模块', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'module:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'module:delete', '删除模块', 'module', 'delete', '删除模块', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'module:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:list', '用例列表', 'case', 'list', '查看用例列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:detail', '用例详情', 'case', 'detail', '查看用例详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:create', '创建用例', 'case', 'create', '创建用例', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:update', '更新用例', 'case', 'update', '更新用例', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:delete', '删除用例', 'case', 'delete', '删除用例', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case_snapshot:create', '创建用例快照', 'case_snapshot', 'create', '创建用例快照', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case_snapshot:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case_snapshot:list', '用例快照列表', 'case_snapshot', 'list', '查看用例快照列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case_snapshot:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case_review:create', '创建用例评审', 'case_review', 'create', '创建用例评审', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case_review:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case_review:update', '更新用例评审', 'case_review', 'update', '更新用例评审', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case_review:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case_review:list', '用例评审列表', 'case_review', 'list', '查看用例评审列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case_review:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:list', '计划列表', 'plan', 'list', '查看计划列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:detail', '计划详情', 'plan', 'detail', '查看计划详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:create', '创建计划', 'plan', 'create', '创建计划', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:update', '更新计划', 'plan', 'update', '更新计划', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:update');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:delete', '删除计划', 'plan', 'delete', '删除计划', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:progress', '计划进度', 'plan', 'progress', '查看计划进度', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:progress');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan_round:create', '创建计划轮次', 'plan_round', 'create', '创建计划轮次', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan_round:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan_round:list', '计划轮次列表', 'plan_round', 'list', '查看计划轮次列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan_round:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan_case:add', '添加计划用例', 'plan_case', 'add', '添加计划用例', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan_case:add');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan_case:list', '计划用例列表', 'plan_case', 'list', '查看计划用例列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan_case:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan_case:execute', '执行计划用例', 'plan_case', 'execute', '执行计划用例', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan_case:execute');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'report:list', '报告列表', 'report', 'list', '查看报告列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'report:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'report:detail', '报告详情', 'report', 'detail', '查看报告详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'report:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'report:generate', '生成报告', 'report', 'generate', '生成报告', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'report:generate');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'data_builder:list', '造数器列表', 'data_builder', 'list', '查看造数器列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'data_builder:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'sql_project:list', 'SQL项目列表', 'sql_project', 'list', '查看SQL项目列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'sql_project:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'sql_project:create', '创建SQL项目', 'sql_project', 'create', '创建SQL项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'sql_project:create');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'sql_project:detail', 'SQL项目详情', 'sql_project', 'detail', '查看SQL项目详情', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'sql_project:detail');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'sql_project:delete', '删除SQL项目', 'sql_project', 'delete', '删除SQL项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'sql_project:delete');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'sql_project:execute', '执行SQL项目', 'sql_project', 'execute', '执行SQL项目', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'sql_project:execute');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role_permission:list', '角色权限列表', 'role_permission', 'list', '查看角色权限列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role_permission:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role_permission:assign', '分配角色权限', 'role_permission', 'assign', '分配角色权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role_permission:assign');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role_menu:list', '角色菜单列表', 'role_menu', 'list', '查看角色菜单列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role_menu:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role_menu:assign', '分配角色菜单', 'role_menu', 'assign', '分配角色菜单', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role_menu:assign');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user_role:list', '用户角色列表', 'user_role', 'list', '查看用户角色列表', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user_role:list');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user_role:assign', '分配用户角色', 'user_role', 'assign', '分配用户角色', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user_role:assign');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'role:*', '角色全部权限', 'role', '*', '角色模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'role:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'user:*', '用户全部权限', 'user', '*', '用户模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'user:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'menu:*', '菜单全部权限', 'menu', '*', '菜单模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'menu:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'permission:*', '权限全部权限', 'permission', '*', '权限模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'permission:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'project:*', '项目全部权限', 'project', '*', '项目模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'project:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'environment:*', '环境全部权限', 'environment', '*', '环境模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'environment:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'case:*', '用例全部权限', 'case', '*', '用例模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'case:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'plan:*', '计划全部权限', 'plan', '*', '计划模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'plan:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'report:*', '报告全部权限', 'report', '*', '报告模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'report:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT 'data_builder:*', '造数器全部权限', 'data_builder', '*', '造数器模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = 'data_builder:*');
|
||||
INSERT INTO permission (code, name, module, action, description, status, is_delete)
|
||||
SELECT '*:*', '全部权限', '*', '*', '所有模块全部权限', 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permission WHERE code = '*:*');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT 0, '系统管理', 'system', 1, '/system', 'Layout', 'setting', NULL, 1, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE code = 'system');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '角色管理', 'role_manage', 2, '/system/role', 'system/role/index', 'peoples', 'role:list', 1, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'system'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'role_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '用户管理', 'user_manage', 2, '/system/user', 'system/user/index', 'user', 'user:list', 2, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'system'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'user_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '权限管理', 'permission_manage', 2, '/system/permission', 'system/permission/index', 'lock', 'permission:list', 3, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'system'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'permission_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '菜单管理', 'menu_manage', 2, '/system/menu', 'system/menu/index', 'menu', 'menu:list', 4, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'system'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'menu_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT 0, '测试平台', 'test_platform', 1, '/test-platform', 'Layout', 'platform', NULL, 2, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE code = 'test_platform');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '产品管理', 'product_manage', 2, '/test-platform/product', 'test-platform/product/index', 'product', 'product:list', 1, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'test_platform'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'product_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '项目管理', 'project_manage', 2, '/test-platform/project', 'test-platform/project/index', 'project', 'project:list', 2, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'test_platform'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'project_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '用例管理', 'case_manage', 2, '/test-platform/case', 'test-platform/case/index', 'case', 'case:list', 3, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'test_platform'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'case_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '测试计划', 'plan_manage', 2, '/test-platform/plan', 'test-platform/plan/index', 'plan', 'plan:list', 4, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'test_platform'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'plan_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '测试报告', 'report_manage', 2, '/test-platform/report', 'test-platform/report/index', 'report', 'report:list', 5, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'test_platform'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'report_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT 0, '造数工具', 'data_tools', 1, '/data-tools', 'Layout', 'data', NULL, 3, 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE code = 'data_tools');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '数据库造数', 'data_builder_manage', 2, '/data-tools/db-builder', 'data-tools/db-builder/index', 'database', 'data_builder:list', 1, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'data_tools'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'data_builder_manage');
|
||||
|
||||
INSERT INTO menu (parent_id, name, code, type, path, component, icon, permission_code, sort, visible, status, is_delete)
|
||||
SELECT m.id, '造数工厂', 'data_factory_manage', 2, '/data-tools/factory', 'data-tools/factory/index', 'factory', NULL, 2, 1, 1, 0
|
||||
FROM menu m
|
||||
WHERE m.code = 'data_tools'
|
||||
AND NOT EXISTS (SELECT 1 FROM menu WHERE code = 'data_factory_manage');
|
||||
|
||||
INSERT INTO "user" (username, real_name, password_hash, mobile, email, avatar, status, created_by, is_delete)
|
||||
SELECT 'admin', '系统管理员', 'admin123', '13800000000', 'admin@example.com', '', 1, 1, 0
|
||||
WHERE NOT EXISTS (SELECT 1 FROM "user" WHERE username = 'admin');
|
||||
|
||||
INSERT INTO user_role (user_id, role_id, is_delete)
|
||||
SELECT u.id, r.id, 0
|
||||
FROM "user" u, role r
|
||||
WHERE u.username = 'admin' AND r.code = 'admin'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_role ur WHERE ur.user_id = u.id AND ur.role_id = r.id
|
||||
);
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, is_delete)
|
||||
SELECT r.id, p.id, 0
|
||||
FROM role r, permission p
|
||||
WHERE r.code = 'admin'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
INSERT INTO role_permission (role_id, permission_id, is_delete)
|
||||
SELECT r.id, p.id, 0
|
||||
FROM role r, permission p
|
||||
WHERE r.code = 'admin'
|
||||
AND p.code IN ('user_role:list', 'user_role:assign', 'role_permission:list', 'role_permission:assign', 'role_menu:list', 'role_menu:assign')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_permission rp WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||
);
|
||||
|
||||
UPDATE menu SET permission_code = 'role:list' WHERE code = 'role_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'user:list' WHERE code = 'user_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'permission:list' WHERE code = 'permission_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'menu:list' WHERE code = 'menu_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'product:list' WHERE code = 'product_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'project:list' WHERE code = 'project_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'case:list' WHERE code = 'case_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'plan:list' WHERE code = 'plan_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'report:list' WHERE code = 'report_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
UPDATE menu SET permission_code = 'data_builder:list' WHERE code = 'data_builder_manage' AND (permission_code IS NULL OR permission_code = '');
|
||||
|
||||
INSERT INTO role_menu (role_id, menu_id, is_delete)
|
||||
SELECT r.id, m.id, 0
|
||||
FROM role r, menu m
|
||||
WHERE r.code = 'admin'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_menu rm WHERE rm.role_id = r.id AND rm.menu_id = m.id
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
212
.plan/3onvvJGzAx9Dhi05JkVpx.md
Normal file
212
.plan/3onvvJGzAx9Dhi05JkVpx.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 实施计划:按现有架构接入测试管理模块
|
||||
|
||||
## 目标
|
||||
在当前 Flask + SQLAlchemy 手写分层架构下,按现有模式扩展测试管理能力。保持 `model/dao/service/controller/views` 分层,不引入 FastAPI、异步框架、Celery、WebSocket 等大改架构内容。先实现同步 CRUD 与核心数据流,异步任务/实时推送/外部缺陷系统作为后续阶段。
|
||||
|
||||
## 当前架构约束
|
||||
- 路由集中在 `app/api/views.py` 的 Flask Blueprint `api` 下。
|
||||
- Controller 位于 `app/api/controller/*Controller.py`。
|
||||
- Service 位于 `app/api/service/*Service.py`。
|
||||
- DAO 位于 `app/api/dao/*Dao.py`。
|
||||
- Model 位于 `app/api/model/*Model.py`,使用 SQLAlchemy declarative。
|
||||
- 数据库连接使用 `common/sqlSession.py` 的 `SqlSession`。
|
||||
- 响应统一使用 `common/apiResponse.py`。
|
||||
|
||||
## 命名与接口策略
|
||||
当前已有接口路径是 `/it/api/...` 风格,不采用设计稿中的 `/api/v1/projects/{project_id}/...` 作为硬切换。
|
||||
|
||||
新增接口建议挂在同一个 Blueprint 下,保持风格:
|
||||
- 项目:`/project/list`、`/project/detail`、`/project/create`、`/project/update`、`/project/delete`
|
||||
- 环境:`/environment/list`、`/environment/create`、`/environment/update`、`/environment/delete`
|
||||
- 模块:`/module/tree`、`/module/create`、`/module/update`、`/module/delete`
|
||||
- 用例:`/case/list`、`/case/detail`、`/case/create`、`/case/update`、`/case/delete`
|
||||
- 用例快照:`/case/snapshot/create`、`/case/snapshot/list`
|
||||
- 用例评审:`/case/review/create`、`/case/review/update`、`/case/review/list`
|
||||
- 测试计划:`/plan/list`、`/plan/detail`、`/plan/create`、`/plan/update`、`/plan/delete`
|
||||
- 计划用例:`/plan/case/add`、`/plan/case/list`、`/plan/case/execute`
|
||||
- 轮次:`/plan/round/list`、`/plan/round/create`
|
||||
- 报告:`/report/list`、`/report/detail`、`/report/generate`
|
||||
- 造数:`/data/builder/list`、`/data/builder/detail`、`/data/builder/create`、`/data/builder/update`、`/data/builder/delete`、`/data/builder/execute`、`/data/task/status`
|
||||
|
||||
## 阶段 1:模型层与基础 DAO
|
||||
新增 model 文件:
|
||||
- `projectModel.py`
|
||||
- `Project`
|
||||
- `ProjectMember`
|
||||
- `Environment`
|
||||
- `caseModel.py`
|
||||
- `Module`
|
||||
- `TestCase`
|
||||
- `CaseSnapshot`
|
||||
- `CaseReview`
|
||||
- `planModel.py`
|
||||
- `TestPlan`
|
||||
- `PlanCase`
|
||||
- `TestRound`
|
||||
- `reportModel.py`
|
||||
- `Report`
|
||||
- `DefectSync`
|
||||
- `dataBuilderModel.py`
|
||||
- `DataBuilder`
|
||||
- `DataTask`
|
||||
|
||||
实现细节:
|
||||
- PostgreSQL `JSONB` 使用 `sqlalchemy.dialects.postgresql.JSONB`。
|
||||
- PostgreSQL 数组 `tags VARCHAR(64)[]` 使用 `ARRAY(String(64))`。
|
||||
- `BIGSERIAL` 模型上用 `BigInteger` + `Sequence` 或直接 `BigInteger primary_key=True autoincrement=True`,保持 SQLAlchemy 兼容。
|
||||
- 所有 `created_time/updated_time` 使用 `TIMESTAMP, server_default=text('CURRENT_TIMESTAMP')`,风格参考现有 `UpdateSqlProject`。
|
||||
- 如需软删除,设计稿多数表未含 `is_delete`。为了保持删除接口一致性,优先给业务主表补 `is_delete` 字段:`project`、`module`、`test_case`、`test_plan`、`data_builder`。关联/历史表可物理保留不删。
|
||||
|
||||
新增 DAO 文件:
|
||||
- `projectDao.py`
|
||||
- `caseDao.py`
|
||||
- `planDao.py`
|
||||
- `reportDao.py`
|
||||
- `dataBuilderDao.py`
|
||||
|
||||
DAO 公共实现规则:
|
||||
- 列表方法统一接收 `filter_list, page, limit`。
|
||||
- 详情方法统一按 `id` + `is_delete=0` 查询。
|
||||
- 删除方法统一更新 `is_delete=1`。
|
||||
- 创建/更新方法返回 `(id, err_msg)` 或 `(obj, err_msg)`,与现有 `UpdateSqlProjectDao` 保持一致。
|
||||
|
||||
## 阶段 2:项目、环境、模块、用例基础 CRUD
|
||||
新增 Service:
|
||||
- `projectService.py`
|
||||
- `caseService.py`
|
||||
|
||||
新增 Controller:
|
||||
- `projectController.py`
|
||||
- `caseController.py`
|
||||
|
||||
先实现:
|
||||
- 项目 CRUD
|
||||
- 项目成员新增/删除/列表
|
||||
- 环境 CRUD
|
||||
- 模块树 CRUD
|
||||
- 用例 CRUD
|
||||
- 用例详情返回不暴露 `is_delete`
|
||||
- 用例列表支持:`projectId/moduleId/priority/caseType/status/tags/keyword/pageNo/pageSize`
|
||||
|
||||
关键逻辑:
|
||||
- 创建用例时生成 `case_key`,项目内递增,如 `TC-001`。初版可按当前项目最大 id 或 count 生成,后续再优化并发锁。
|
||||
- 更新用例时可选生成快照,初版提供单独 `/case/snapshot/create`。
|
||||
- 删除用例只更新 `test_case.is_delete=1`。
|
||||
|
||||
## 阶段 3:评审与快照
|
||||
在 `caseDao/service/controller` 内补:
|
||||
- 创建快照
|
||||
- 查询快照列表
|
||||
- 创建评审记录
|
||||
- 更新评审状态/评论
|
||||
|
||||
接口:
|
||||
- `POST /case/snapshot/create` body: `{caseId, createdBy}`
|
||||
- `GET /case/snapshot/list?caseId=...`
|
||||
- `POST /case/review/create` body: `{caseId, reviewerId, comments?}`
|
||||
- `POST /case/review/update` body: `{reviewId, status, comments?}`
|
||||
- `GET /case/review/list?caseId=...`
|
||||
|
||||
## 阶段 4:测试计划与执行闭环
|
||||
新增 Service/Controller:
|
||||
- `planService.py`
|
||||
- `planController.py`
|
||||
|
||||
实现:
|
||||
- 测试计划 CRUD
|
||||
- 轮次创建/列表
|
||||
- 批量添加用例到计划
|
||||
- 执行计划用例,更新状态、实际结果、缺陷链接、附件、执行时间、执行耗时
|
||||
- 计划详情聚合统计:`total_cases/completed/pass_rate/passed/failed/blocked`
|
||||
- 进度接口:按轮次、执行人聚合
|
||||
|
||||
接口:
|
||||
- `GET /plan/list`
|
||||
- `POST /plan/create`
|
||||
- `GET /plan/detail?planId=...`
|
||||
- `POST /plan/delete`
|
||||
- `POST /plan/case/add`
|
||||
- `GET /plan/case/list?planId=...&roundNo=...`
|
||||
- `POST /plan/case/execute`
|
||||
- `GET /plan/progress?planId=...`
|
||||
|
||||
初版不自动创建外部缺陷,只保存 `defect_links`。外部 JIRA/TAPD/禅道集成后续单独做。
|
||||
|
||||
## 阶段 5:报告
|
||||
新增:
|
||||
- `reportService.py`
|
||||
- `reportController.py`
|
||||
- `reportDao.py`
|
||||
|
||||
实现同步版报告生成:
|
||||
- 根据 `plan_id` 聚合 `plan_case` 状态
|
||||
- 保存 `report.summary` 和简单 HTML `content`
|
||||
- 列表/详情查询
|
||||
|
||||
接口:
|
||||
- `GET /report/list?planId=&pageNo=&pageSize=`
|
||||
- `POST /report/generate` body: `{planId, generatedBy}`
|
||||
- `GET /report/detail?reportId=...`
|
||||
|
||||
不实现 PDF 导出、异步任务、模板编辑器;这些需后续引入任务队列/文件服务。
|
||||
|
||||
## 阶段 6:造数模块 MVP
|
||||
新增:
|
||||
- `dataBuilderService.py`
|
||||
- `dataBuilderController.py`
|
||||
- `dataBuilderDao.py`
|
||||
- `common/dataBuilderExecutor.py`
|
||||
|
||||
实现:
|
||||
- 造数器 CRUD
|
||||
- 同步执行 JSON 定义
|
||||
- 支持 step type:`http`、`db`
|
||||
- 先不支持不安全的 `script exec`,避免安全风险
|
||||
- 执行结果保存到 `data_task`
|
||||
|
||||
接口:
|
||||
- `GET /data/builder/list?projectId=...`
|
||||
- `POST /data/builder/create`
|
||||
- `GET /data/builder/detail?builderId=...`
|
||||
- `POST /data/builder/update`
|
||||
- `POST /data/builder/delete`
|
||||
- `POST /data/builder/execute` body: `{builderId, params, async:false}`
|
||||
- `GET /data/task/status?taskId=...`
|
||||
|
||||
## 阶段 7:路由注册
|
||||
修改 `app/api/views.py`:
|
||||
- 保留现有 SQL 管理接口。
|
||||
- 追加新模块路由。
|
||||
- 每个路由实例化对应 Controller,返回 `ApiResponse`。
|
||||
|
||||
为避免 `views.py` 过大,若当前应用允许多 Blueprint,后续可拆 `projectViews.py/caseViews.py/...`;本次先保持单文件,符合现有架构。
|
||||
|
||||
## 阶段 8:数据库初始化 SQL
|
||||
新增建表 SQL 文件需要用户确认放置位置。建议:
|
||||
- `sql/test_management_schema.sql`
|
||||
|
||||
注意:当前仓库不应主动创建文档,但 schema SQL 属于代码资产,可在用户确认后创建。
|
||||
|
||||
## 阶段 9:验证
|
||||
最低验证:
|
||||
- Python 语法检查:`python -m py_compile` 对新增/改动 py 文件。
|
||||
- 手动接口链路:
|
||||
1. 创建项目
|
||||
2. 创建模块
|
||||
3. 创建用例
|
||||
4. 查询用例列表/详情
|
||||
5. 创建计划
|
||||
6. 添加用例到计划
|
||||
7. 执行用例
|
||||
8. 生成报告
|
||||
|
||||
## 非本次直接实现范围
|
||||
- WebSocket 实时推送
|
||||
- Celery 异步任务
|
||||
- PDF 导出/OSS 上传
|
||||
- 外部缺陷系统自动创建
|
||||
- 前端页面
|
||||
- Docker Compose
|
||||
- Python 脚本沙箱执行
|
||||
|
||||
这些会影响架构和依赖,需要单独确认后再做。
|
||||
856
.plan/YCGiVLWod2rghU8nT3fEv.md
Normal file
856
.plan/YCGiVLWod2rghU8nT3fEv.md
Normal file
@@ -0,0 +1,856 @@
|
||||
# 角色/用户/菜单管理详细设计文档
|
||||
|
||||
## 1. 目标
|
||||
|
||||
基于当前项目已有的分层框架(model / dao / service / controller / views)新增以下能力:
|
||||
|
||||
1. 角色管理
|
||||
- 角色表
|
||||
- 权限表
|
||||
- 用户角色关联表
|
||||
- 角色权限关联能力
|
||||
2. 用户管理
|
||||
- 用户表
|
||||
- 用户与角色分配
|
||||
3. 菜单管理
|
||||
- 菜单表
|
||||
- 角色分配菜单
|
||||
|
||||
本次仅输出详细设计,不直接改业务代码。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前项目框架观察
|
||||
|
||||
### 2.1 现有分层风格
|
||||
|
||||
当前项目采用如下结构:
|
||||
|
||||
- `app/api/model/*.py`
|
||||
- `app/api/dao/*.py`
|
||||
- `app/api/service/*.py`
|
||||
- `app/api/controller/*.py`
|
||||
- `app/api/views.py`
|
||||
|
||||
### 2.2 当前职责边界
|
||||
|
||||
#### model
|
||||
- 定义 SQLAlchemy 表模型
|
||||
- 每个领域通常独立一个 model 文件
|
||||
|
||||
#### dao
|
||||
- 负责数据库增删改查
|
||||
- 已有通用风格:
|
||||
- `create`
|
||||
- `update_by_id`
|
||||
- `get_by_id`
|
||||
- `list_by_filters`
|
||||
- `delete_by_id`
|
||||
|
||||
#### service
|
||||
- 作为 controller 与 dao 之间的业务入口
|
||||
- 目前多数是对 dao 的薄封装
|
||||
- 适合放跨表组合查询、业务校验、聚合逻辑
|
||||
|
||||
#### controller
|
||||
- 负责参数获取、必填校验、字段映射、返回结构拼装
|
||||
- 不应直接 `session.query(...)`
|
||||
|
||||
#### views
|
||||
- Flask 路由层
|
||||
- 负责 request 参数读取、ApiResponse 包装、session 生命周期控制
|
||||
|
||||
### 2.3 当前接口命名风格
|
||||
|
||||
已有接口风格:
|
||||
- `/project/list`
|
||||
- `/project/detail`
|
||||
- `/project/create`
|
||||
- `/project/update`
|
||||
- `/project/delete`
|
||||
- `/product/list`
|
||||
- `/product/detail`
|
||||
- `/product/create`
|
||||
- `/product/update`
|
||||
- `/product/delete`
|
||||
|
||||
建议新增模块保持一致:
|
||||
- `/role/list`
|
||||
- `/role/detail`
|
||||
- `/role/create`
|
||||
- `/role/update`
|
||||
- `/role/delete`
|
||||
- `/user/list`
|
||||
- `/user/detail`
|
||||
- `/user/create`
|
||||
- `/user/update`
|
||||
- `/user/delete`
|
||||
- `/menu/tree`
|
||||
- `/menu/create`
|
||||
- `/menu/update`
|
||||
- `/menu/delete`
|
||||
- `/role/permission/*`
|
||||
- `/role/menu/*`
|
||||
- `/user/role/*`
|
||||
|
||||
---
|
||||
|
||||
## 3. 总体设计原则
|
||||
|
||||
1. 严格按当前框架分层
|
||||
2. controller 不直接查数据库
|
||||
3. 跨表聚合通过 service + dao 实现
|
||||
4. 保持与现有字段风格一致:
|
||||
- 主键 `id`
|
||||
- 软删字段 `is_delete`
|
||||
- 时间字段 `created_time` / `updated_time`
|
||||
5. 优先支持后台管理能力,不先引入复杂 RBAC 继承体系
|
||||
6. 菜单、权限、角色、用户四个域解耦,但通过关联表关联
|
||||
|
||||
---
|
||||
|
||||
## 4. 业务对象与关系
|
||||
|
||||
### 4.1 核心对象
|
||||
|
||||
1. 用户 `user`
|
||||
2. 角色 `role`
|
||||
3. 权限 `permission`
|
||||
4. 菜单 `menu`
|
||||
|
||||
### 4.2 关联对象
|
||||
|
||||
1. 用户角色关联 `user_role`
|
||||
2. 角色权限关联 `role_permission`
|
||||
3. 角色菜单关联 `role_menu`
|
||||
|
||||
### 4.3 关系说明
|
||||
|
||||
- 用户 : 角色 = 多对多
|
||||
- 角色 : 权限 = 多对多
|
||||
- 角色 : 菜单 = 多对多
|
||||
- 菜单自身 = 树形父子关系
|
||||
|
||||
### 4.4 推荐鉴权思路
|
||||
|
||||
后续登录后:
|
||||
- 用户拥有多个角色
|
||||
- 角色聚合出权限集合
|
||||
- 角色聚合出菜单集合
|
||||
- 前端基于菜单渲染导航
|
||||
- 后端基于权限点做接口校验
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库表结构设计
|
||||
|
||||
以下采用 PostgreSQL 风格,保持与现有模型一致。
|
||||
|
||||
---
|
||||
|
||||
## 5.1 用户表 `user`
|
||||
|
||||
### 用途
|
||||
存储后台系统用户基础信息。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| username | VARCHAR(64) | UNIQUE NOT NULL | 登录用户名 |
|
||||
| real_name | VARCHAR(64) | | 真实姓名 |
|
||||
| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 |
|
||||
| mobile | VARCHAR(32) | | 手机号 |
|
||||
| email | VARCHAR(128) | | 邮箱 |
|
||||
| avatar | VARCHAR(255) | | 头像地址 |
|
||||
| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 |
|
||||
| last_login_time | TIMESTAMP | | 最后登录时间 |
|
||||
| created_by | BIGINT | | 创建人 |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引建议
|
||||
- unique(username)
|
||||
- index(status)
|
||||
- index(is_delete)
|
||||
|
||||
---
|
||||
|
||||
## 5.2 角色表 `role`
|
||||
|
||||
### 用途
|
||||
定义系统角色。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| code | VARCHAR(64) | UNIQUE NOT NULL | 角色编码,如 admin/test_manager |
|
||||
| name | VARCHAR(64) | NOT NULL | 角色名称 |
|
||||
| description | TEXT | | 角色描述 |
|
||||
| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 |
|
||||
| is_system | SMALLINT | DEFAULT 0 | 是否系统内置角色 |
|
||||
| created_by | BIGINT | | 创建人 |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引建议
|
||||
- unique(code)
|
||||
- index(status)
|
||||
- index(is_delete)
|
||||
|
||||
---
|
||||
|
||||
## 5.3 权限表 `permission`
|
||||
|
||||
### 用途
|
||||
定义后端权限点。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| code | VARCHAR(128) | UNIQUE NOT NULL | 权限编码,如 project:create |
|
||||
| name | VARCHAR(128) | NOT NULL | 权限名称 |
|
||||
| module | VARCHAR(64) | | 所属模块,如 project/user/menu |
|
||||
| action | VARCHAR(64) | | 动作,如 list/create/update/delete |
|
||||
| description | TEXT | | 描述 |
|
||||
| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引建议
|
||||
- unique(code)
|
||||
- index(module)
|
||||
- index(status)
|
||||
|
||||
---
|
||||
|
||||
## 5.4 菜单表 `menu`
|
||||
|
||||
### 用途
|
||||
定义前端菜单树。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| parent_id | BIGINT | DEFAULT 0 | 父菜单ID,0表示根节点 |
|
||||
| name | VARCHAR(64) | NOT NULL | 菜单名称 |
|
||||
| code | VARCHAR(64) | UNIQUE | 菜单编码 |
|
||||
| type | SMALLINT | DEFAULT 1 | 1目录 2菜单 3按钮 |
|
||||
| path | VARCHAR(255) | | 路由路径 |
|
||||
| component | VARCHAR(255) | | 前端组件路径 |
|
||||
| icon | VARCHAR(64) | | 图标 |
|
||||
| permission_code | VARCHAR(128) | | 对应权限编码,可选 |
|
||||
| sort | INTEGER | DEFAULT 0 | 排序 |
|
||||
| visible | SMALLINT | DEFAULT 1 | 1显示 0隐藏 |
|
||||
| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 |
|
||||
|
||||
### 索引建议
|
||||
- index(parent_id)
|
||||
- unique(code)
|
||||
- index(sort)
|
||||
|
||||
---
|
||||
|
||||
## 5.5 用户角色关联表 `user_role`
|
||||
|
||||
### 用途
|
||||
维护用户与角色多对多关系。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| user_id | BIGINT | NOT NULL | 用户ID |
|
||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
|
||||
### 约束建议
|
||||
- unique(user_id, role_id)
|
||||
- index(user_id)
|
||||
- index(role_id)
|
||||
|
||||
---
|
||||
|
||||
## 5.6 角色权限关联表 `role_permission`
|
||||
|
||||
### 用途
|
||||
维护角色与权限多对多关系。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
||||
| permission_id | BIGINT | NOT NULL | 权限ID |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
|
||||
### 约束建议
|
||||
- unique(role_id, permission_id)
|
||||
- index(role_id)
|
||||
- index(permission_id)
|
||||
|
||||
---
|
||||
|
||||
## 5.7 角色菜单关联表 `role_menu`
|
||||
|
||||
### 用途
|
||||
维护角色与菜单多对多关系。
|
||||
|
||||
### 建表字段
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|---|---|---|---|
|
||||
| id | BIGSERIAL | PK | 主键 |
|
||||
| role_id | BIGINT | NOT NULL | 角色ID |
|
||||
| menu_id | BIGINT | NOT NULL | 菜单ID |
|
||||
| is_delete | INTEGER | DEFAULT 0 | 软删标识 |
|
||||
| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
|
||||
### 约束建议
|
||||
- unique(role_id, menu_id)
|
||||
- index(role_id)
|
||||
- index(menu_id)
|
||||
|
||||
---
|
||||
|
||||
## 6. Model 设计
|
||||
|
||||
建议新增单独 model 文件:
|
||||
|
||||
### 6.1 `app/api/model/userModel.py`
|
||||
包含:
|
||||
- `User`
|
||||
- `UserRole`
|
||||
|
||||
### 6.2 `app/api/model/rbacModel.py`
|
||||
包含:
|
||||
- `Role`
|
||||
- `Permission`
|
||||
- `RolePermission`
|
||||
- `Menu`
|
||||
- `RoleMenu`
|
||||
|
||||
> 也可以拆成 `roleModel.py` / `menuModel.py`,但按当前项目规模,两个文件足够。
|
||||
|
||||
### 6.3 Model 设计原则
|
||||
- 继承与现有一致:`declarative_base()` + `to_dict`
|
||||
- 字段命名与数据库列一致
|
||||
- 关联关系不强制写 SQLAlchemy relationship,保持当前项目风格简洁
|
||||
|
||||
---
|
||||
|
||||
## 7. DAO 设计
|
||||
|
||||
---
|
||||
|
||||
## 7.1 `app/api/dao/userDao.py`
|
||||
|
||||
### 基础方法
|
||||
- `create(session, model_cls, add_info)`
|
||||
- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)`
|
||||
- `get_by_id(session, model_cls, obj_id, soft_delete=True)`
|
||||
- `list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None)`
|
||||
- `delete_by_id(session, model_cls, obj_id)`
|
||||
|
||||
### 用户专项方法
|
||||
- `get_user_role_ids(session, user_id)`
|
||||
- `replace_user_roles(session, user_id, role_ids)`
|
||||
- `get_user_roles(session, user_ids)`
|
||||
|
||||
---
|
||||
|
||||
## 7.2 `app/api/dao/rbacDao.py`
|
||||
|
||||
### 基础方法
|
||||
同现有通用 DAO 风格。
|
||||
|
||||
### 角色/权限/菜单专项方法
|
||||
- `get_role_permission_ids(session, role_id)`
|
||||
- `replace_role_permissions(session, role_id, permission_ids)`
|
||||
- `get_role_menu_ids(session, role_id)`
|
||||
- `replace_role_menus(session, role_id, menu_ids)`
|
||||
- `get_role_names_map(session, role_ids)`
|
||||
- `get_menu_tree_items(session, filter_list)`
|
||||
- `get_permission_list(session, filter_list, page, limit)`
|
||||
|
||||
---
|
||||
|
||||
## 8. Service 设计
|
||||
|
||||
---
|
||||
|
||||
## 8.1 `app/api/service/userService.py`
|
||||
|
||||
### 职责
|
||||
- 用户基本 CRUD
|
||||
- 用户与角色分配
|
||||
- 聚合用户角色名称
|
||||
|
||||
### 方法建议
|
||||
- `create(session, model_cls, add_info)`
|
||||
- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)`
|
||||
- `get_by_id(session, model_cls, obj_id, soft_delete=True)`
|
||||
- `list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None)`
|
||||
- `delete_by_id(session, model_cls, obj_id)`
|
||||
- `assign_roles(session, user_id, role_ids)`
|
||||
- `get_user_role_ids(session, user_id)`
|
||||
- `get_user_roles_map(session, user_ids)`
|
||||
|
||||
---
|
||||
|
||||
## 8.2 `app/api/service/rbacService.py`
|
||||
|
||||
### 职责
|
||||
- 角色 CRUD
|
||||
- 权限 CRUD
|
||||
- 菜单 CRUD
|
||||
- 角色绑定权限
|
||||
- 角色绑定菜单
|
||||
- 菜单树构建
|
||||
|
||||
### 方法建议
|
||||
- `create(session, model_cls, add_info)`
|
||||
- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)`
|
||||
- `get_by_id(session, model_cls, obj_id, soft_delete=True)`
|
||||
- `list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None)`
|
||||
- `delete_by_id(session, model_cls, obj_id)`
|
||||
- `assign_permissions(session, role_id, permission_ids)`
|
||||
- `assign_menus(session, role_id, menu_ids)`
|
||||
- `get_role_permission_ids(session, role_id)`
|
||||
- `get_role_menu_ids(session, role_id)`
|
||||
- `build_menu_tree(session, filters)`
|
||||
|
||||
---
|
||||
|
||||
## 9. Controller 设计
|
||||
|
||||
controller 仅负责:
|
||||
- 参数获取 `_get(...)`
|
||||
- 必填校验
|
||||
- 字段映射
|
||||
- 调用 service
|
||||
- 序列化返回
|
||||
|
||||
不得直接 `session.query(...)`
|
||||
|
||||
---
|
||||
|
||||
## 9.1 `app/api/controller/userController.py`
|
||||
|
||||
### 建议方法
|
||||
- `user_list`
|
||||
- `user_detail`
|
||||
- `user_create`
|
||||
- `user_update`
|
||||
- `user_delete`
|
||||
- `user_role_assign`
|
||||
- `user_role_list`
|
||||
|
||||
### 返回增强建议
|
||||
`user_list` 每项增加:
|
||||
- `role_ids`
|
||||
- `role_names`
|
||||
|
||||
---
|
||||
|
||||
## 9.2 `app/api/controller/rbacController.py`
|
||||
|
||||
### 角色相关
|
||||
- `role_list`
|
||||
- `role_detail`
|
||||
- `role_create`
|
||||
- `role_update`
|
||||
- `role_delete`
|
||||
- `role_permission_assign`
|
||||
- `role_permission_list`
|
||||
- `role_menu_assign`
|
||||
- `role_menu_list`
|
||||
|
||||
### 权限相关
|
||||
- `permission_list`
|
||||
- `permission_detail`
|
||||
- `permission_create`
|
||||
- `permission_update`
|
||||
- `permission_delete`
|
||||
|
||||
### 菜单相关
|
||||
- `menu_tree`
|
||||
- `menu_detail`
|
||||
- `menu_create`
|
||||
- `menu_update`
|
||||
- `menu_delete`
|
||||
|
||||
---
|
||||
|
||||
## 10. 接口清单设计
|
||||
|
||||
以下接口路径保持当前项目风格。
|
||||
|
||||
---
|
||||
|
||||
## 10.1 角色管理接口
|
||||
|
||||
### 角色列表
|
||||
- `GET /role/list`
|
||||
- 参数:`keyword`, `status`, `pageNo`, `pageSize`
|
||||
- 返回:角色分页列表
|
||||
|
||||
### 角色详情
|
||||
- `GET /role/detail`
|
||||
- 参数:`roleId`
|
||||
|
||||
### 创建角色
|
||||
- `POST /role/create`
|
||||
- 参数:
|
||||
- `code`
|
||||
- `name`
|
||||
- `description`
|
||||
- `status`
|
||||
- `isSystem`
|
||||
|
||||
### 更新角色
|
||||
- `POST /role/update`
|
||||
- 参数:
|
||||
- `roleId`
|
||||
- 可选更新字段
|
||||
|
||||
### 删除角色
|
||||
- `POST /role/delete`
|
||||
- 参数:`roleId`
|
||||
|
||||
---
|
||||
|
||||
## 10.2 权限管理接口
|
||||
|
||||
### 权限列表
|
||||
- `GET /permission/list`
|
||||
- 参数:`keyword`, `module`, `status`, `pageNo`, `pageSize`
|
||||
|
||||
### 权限详情
|
||||
- `GET /permission/detail`
|
||||
- 参数:`permissionId`
|
||||
|
||||
### 创建权限
|
||||
- `POST /permission/create`
|
||||
|
||||
### 更新权限
|
||||
- `POST /permission/update`
|
||||
|
||||
### 删除权限
|
||||
- `POST /permission/delete`
|
||||
|
||||
---
|
||||
|
||||
## 10.3 菜单管理接口
|
||||
|
||||
### 菜单树
|
||||
- `GET /menu/tree`
|
||||
- 参数:可选 `status`
|
||||
- 返回树形结构
|
||||
|
||||
### 菜单详情
|
||||
- `GET /menu/detail`
|
||||
- 参数:`menuId`
|
||||
|
||||
### 创建菜单
|
||||
- `POST /menu/create`
|
||||
|
||||
### 更新菜单
|
||||
- `POST /menu/update`
|
||||
|
||||
### 删除菜单
|
||||
- `POST /menu/delete`
|
||||
|
||||
---
|
||||
|
||||
## 10.4 角色权限分配接口
|
||||
|
||||
### 查询角色权限
|
||||
- `GET /role/permission/list`
|
||||
- 参数:`roleId`
|
||||
- 返回:`permissionIds` + 权限明细列表
|
||||
|
||||
### 分配角色权限
|
||||
- `POST /role/permission/assign`
|
||||
- 参数:
|
||||
- `roleId`
|
||||
- `permissionIds: []`
|
||||
|
||||
建议语义:覆盖式保存
|
||||
|
||||
---
|
||||
|
||||
## 10.5 角色菜单分配接口
|
||||
|
||||
### 查询角色菜单
|
||||
- `GET /role/menu/list`
|
||||
- 参数:`roleId`
|
||||
- 返回:`menuIds` + 菜单树
|
||||
|
||||
### 分配角色菜单
|
||||
- `POST /role/menu/assign`
|
||||
- 参数:
|
||||
- `roleId`
|
||||
- `menuIds: []`
|
||||
|
||||
建议语义:覆盖式保存
|
||||
|
||||
---
|
||||
|
||||
## 10.6 用户管理接口
|
||||
|
||||
### 用户列表
|
||||
- `GET /user/list`
|
||||
- 参数:`keyword`, `status`, `pageNo`, `pageSize`
|
||||
|
||||
### 用户详情
|
||||
- `GET /user/detail`
|
||||
- 参数:`userId`
|
||||
|
||||
### 创建用户
|
||||
- `POST /user/create`
|
||||
- 参数:
|
||||
- `username`
|
||||
- `realName`
|
||||
- `password`
|
||||
- `mobile`
|
||||
- `email`
|
||||
- `status`
|
||||
|
||||
### 更新用户
|
||||
- `POST /user/update`
|
||||
- 参数:
|
||||
- `userId`
|
||||
- 可选更新字段
|
||||
|
||||
### 删除用户
|
||||
- `POST /user/delete`
|
||||
- 参数:`userId`
|
||||
|
||||
---
|
||||
|
||||
## 10.7 用户角色分配接口
|
||||
|
||||
### 查询用户角色
|
||||
- `GET /user/role/list`
|
||||
- 参数:`userId`
|
||||
- 返回:`roleIds` + 角色列表
|
||||
|
||||
### 分配用户角色
|
||||
- `POST /user/role/assign`
|
||||
- 参数:
|
||||
- `userId`
|
||||
- `roleIds: []`
|
||||
|
||||
建议语义:覆盖式保存
|
||||
|
||||
---
|
||||
|
||||
## 11. 返回结构建议
|
||||
|
||||
### 11.1 角色列表项
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"code": "admin",
|
||||
"name": "管理员",
|
||||
"description": "系统管理员",
|
||||
"status": 1,
|
||||
"is_system": 1,
|
||||
"created_time": "2025-01-01 10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 用户列表项
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"username": "zhangsan",
|
||||
"real_name": "张三",
|
||||
"mobile": "13800000000",
|
||||
"email": "a@test.com",
|
||||
"status": 1,
|
||||
"role_ids": [1, 2],
|
||||
"role_names": ["管理员", "测试经理"]
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 菜单树项
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"parent_id": 0,
|
||||
"name": "系统管理",
|
||||
"code": "system",
|
||||
"type": 1,
|
||||
"path": "/system",
|
||||
"component": "Layout",
|
||||
"icon": "setting",
|
||||
"sort": 1,
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 文件清单
|
||||
|
||||
本次功能落地预计新增/修改以下文件。
|
||||
|
||||
### 12.1 新增 model
|
||||
- `app/api/model/userModel.py`
|
||||
- `app/api/model/rbacModel.py`
|
||||
|
||||
### 12.2 新增 dao
|
||||
- `app/api/dao/userDao.py`
|
||||
- `app/api/dao/rbacDao.py`
|
||||
|
||||
### 12.3 新增 service
|
||||
- `app/api/service/userService.py`
|
||||
- `app/api/service/rbacService.py`
|
||||
|
||||
### 12.4 新增 controller
|
||||
- `app/api/controller/userController.py`
|
||||
- `app/api/controller/rbacController.py`
|
||||
|
||||
### 12.5 修改 views
|
||||
- `app/api/views.py`
|
||||
|
||||
### 12.6 可能新增 SQL 脚本(如果你需要)
|
||||
- 数据库建表 SQL
|
||||
- 初始化菜单/权限/角色种子数据 SQL
|
||||
|
||||
---
|
||||
|
||||
## 13. 分层职责清单
|
||||
|
||||
### model
|
||||
- 只定义表结构
|
||||
|
||||
### dao
|
||||
- 只做数据库查询/写入
|
||||
- 负责关联表的增删查
|
||||
|
||||
### service
|
||||
- 负责组合查询、覆盖式分配逻辑、聚合返回结构前的数据准备
|
||||
|
||||
### controller
|
||||
- 参数校验、字段映射、调用 service、返回结构组装
|
||||
|
||||
### views
|
||||
- 路由注册、request/response 包装
|
||||
|
||||
---
|
||||
|
||||
## 14. 推荐实现顺序
|
||||
|
||||
### 第一阶段:基础 RBAC 数据层
|
||||
1. 建表 SQL
|
||||
2. model
|
||||
3. dao
|
||||
4. service
|
||||
|
||||
### 第二阶段:角色/权限/菜单接口
|
||||
1. role CRUD
|
||||
2. permission CRUD
|
||||
3. menu CRUD
|
||||
4. role-permission assign
|
||||
5. role-menu assign
|
||||
|
||||
### 第三阶段:用户接口
|
||||
1. user CRUD
|
||||
2. user-role assign
|
||||
3. user list/detail 聚合角色信息
|
||||
|
||||
### 第四阶段:初始化与联调
|
||||
1. 初始化菜单
|
||||
2. 初始化权限
|
||||
3. 初始化管理员角色
|
||||
4. 初始化管理员用户
|
||||
|
||||
---
|
||||
|
||||
## 15. 关键设计决策
|
||||
|
||||
### 15.1 分配类接口采用覆盖式保存
|
||||
原因:
|
||||
- 前端实现简单
|
||||
- 后端一致性更好
|
||||
- 避免增删混合接口过多
|
||||
|
||||
### 15.2 菜单与权限分离
|
||||
原因:
|
||||
- 菜单是导航资源
|
||||
- 权限是操作资源
|
||||
- 一些按钮权限不一定出现在菜单树中
|
||||
|
||||
### 15.3 用户表单独建设,不复用现有 project_member
|
||||
原因:
|
||||
- `project_member` 是项目成员关系,不是系统账户主表
|
||||
- 用户管理需要登录凭据、状态、手机号、邮箱等字段
|
||||
|
||||
### 15.4 先不引入复杂组织架构
|
||||
原因:
|
||||
- 当前需求聚焦 RBAC + 用户 + 菜单
|
||||
- 可后续扩展部门/租户/产品维度数据权限
|
||||
|
||||
---
|
||||
|
||||
## 16. 后续可扩展项
|
||||
|
||||
1. 登录认证
|
||||
- JWT / Session
|
||||
2. 密码重置
|
||||
3. 数据权限
|
||||
- 按产品/项目/部门的数据访问范围
|
||||
4. 操作审计日志
|
||||
5. 菜单与接口权限自动同步
|
||||
6. 超级管理员保护机制
|
||||
|
||||
---
|
||||
|
||||
## 17. 需要你确认的点
|
||||
|
||||
请你确认以下设计取舍:
|
||||
|
||||
1. 用户表是否需要登录密码字段(默认:需要)
|
||||
2. 权限是否按 `module:action` 编码(默认:是)
|
||||
3. 菜单是否需要按钮类型 `type=3`(默认:需要)
|
||||
4. 分配接口是否采用覆盖式保存(默认:是)
|
||||
5. 是否需要初始化内置角色:
|
||||
- 超级管理员
|
||||
- 测试经理
|
||||
- 测试工程师
|
||||
6. 用户是否允许多角色(默认:允许)
|
||||
7. 菜单是否只做系统后台菜单,不含项目业务模块动态菜单(默认:只做后台菜单)
|
||||
|
||||
---
|
||||
|
||||
## 18. 下一步建议
|
||||
|
||||
如果你确认这份设计,我下一步可以继续给你输出两部分:
|
||||
|
||||
1. **完整建表 SQL**
|
||||
2. **按当前项目风格拆好的代码落地清单**
|
||||
- 每个文件该写哪些类、哪些方法、哪些接口
|
||||
|
||||
18
Jenkinsfile
vendored
18
Jenkinsfile
vendored
@@ -122,8 +122,11 @@ pipeline {
|
||||
|
||||
sshCommand remote: remote, sudo: false, command: """
|
||||
set -e
|
||||
docker ps --filter name=${CONTAINER_NAME}
|
||||
curl -I http://127.0.0.1:${HOST_PORT}
|
||||
sleep 10
|
||||
docker ps -a --filter name=${CONTAINER_NAME}
|
||||
docker inspect ${CONTAINER_NAME} --format '{{.State.Status}}'
|
||||
docker logs --tail 100 ${CONTAINER_NAME} || true
|
||||
curl -v http://127.0.0.1:${HOST_PORT}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -168,14 +171,15 @@ def deploy_node_by_password(Map args) {
|
||||
remote.password = DEPLOY_PASS
|
||||
|
||||
def deployCommand = """
|
||||
echo '${HARBOR_PASS}' | docker login '${REGISTRY}' -u '${HARBOR_USER}' --password-stdin &&
|
||||
docker pull '${IMAGE_NAME}:latest' &&
|
||||
docker rm -f '${CONTAINER_NAME}' || true &&
|
||||
set -e
|
||||
echo '${HARBOR_PASS}' | docker login '${REGISTRY}' -u '${HARBOR_USER}' --password-stdin
|
||||
docker pull '${IMAGE_NAME}:latest'
|
||||
docker rm -f '${CONTAINER_NAME}' || true
|
||||
docker run -d \
|
||||
--name '${CONTAINER_NAME}' \
|
||||
--restart always \
|
||||
-p '${HOST_PORT}:${CONTAINER_PORT}' \
|
||||
'${IMAGE_NAME}:latest' &&
|
||||
--network host \
|
||||
'${IMAGE_NAME}:latest'
|
||||
docker logout '${REGISTRY}' || true
|
||||
"""
|
||||
|
||||
|
||||
BIN
__pycache__/const.cpython-38.pyc
Normal file
BIN
__pycache__/const.cpython-38.pyc
Normal file
Binary file not shown.
BIN
__pycache__/logger.cpython-38.pyc
Normal file
BIN
__pycache__/logger.cpython-38.pyc
Normal file
Binary file not shown.
416
api_test_document.md
Normal file
416
api_test_document.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# API 测试文档
|
||||
|
||||
## 1. 接口概览
|
||||
|
||||
本项目基于 Flask 框架实现,提供了完整的测试平台 API,包括项目管理、用例管理、计划执行、报告生成、权限管理等功能。
|
||||
|
||||
### 基础信息
|
||||
- 基础 URL: `http://localhost:8081/it/api`
|
||||
- 认证方式: `accessToken` 头部
|
||||
- 响应格式: JSON
|
||||
|
||||
## 2. 模块分类
|
||||
|
||||
| 模块 | 前缀 | 主要功能 |
|
||||
|------|------|----------|
|
||||
| SQL 项目 | `/` | SQL 项目管理 |
|
||||
| 项目管理 | `/project` | 项目 CRUD、环境管理 |
|
||||
| 项目成员 | `/project/member` | 项目成员管理 |
|
||||
| 产品管理 | `/product` | 产品 CRUD |
|
||||
| 模块管理 | `/module` | 测试模块管理 |
|
||||
| 用例管理 | `/case` | 测试用例 CRUD、快照、评审 |
|
||||
| 计划管理 | `/plan` | 测试计划、轮次、执行 |
|
||||
| 报告管理 | `/report` | 测试报告生成与查询 |
|
||||
| Bug 管理 | `/bug` | Bug 追踪与管理 |
|
||||
| 造数器 | `/data` | 数据构建与任务管理 |
|
||||
| 权限管理 | `/role`, `/permission`, `/menu` | 角色、权限、菜单管理 |
|
||||
| 用户管理 | `/user` | 用户 CRUD、角色分配 |
|
||||
| 认证 | `/auth` | 登录、注册 |
|
||||
|
||||
## 3. 详细接口测试文档
|
||||
|
||||
### 3.1 SQL 项目模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/list` | GET | `sql_project:list` | 查询 SQL 项目列表 | pageNo, pageSize | `{"code": 20000, "data": {...}}` |
|
||||
| `/create` | POST | `sql_project:create` | 创建 SQL 项目 | 详见控制器 | `{"code": 20000, "data": {"sqlId": 1}}` |
|
||||
| `/detail` | GET | `sql_project:detail` | 查询 SQL 项目详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/delete` | POST | `sql_project:delete` | 删除 SQL 项目 | id | `{"code": 20000, "data": {"sqlId": 1}}` |
|
||||
| `/execute` | POST | `sql_project:execute` | 执行 SQL 项目 | id, envId | `{"code": 20000, "data": {...}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 正常查询:`GET /list?pageNo=1&pageSize=10`
|
||||
2. 成功创建:`POST /create` 传入完整参数
|
||||
3. 详情查询:`GET /detail?id=1`
|
||||
4. 成功删除:`POST /delete` 传入 id
|
||||
5. 执行 SQL:`POST /execute` 传入 id 和环境
|
||||
|
||||
### 3.2 项目管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/project/list` | GET | `project:list` | 查询项目列表 | pageNo, pageSize, productId | `{"code": 20000, "data": {"list": [...], "total": 10}}` |
|
||||
| `/project/detail` | GET | `project:detail` | 查询项目详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/project/create` | POST | `project:create` | 创建项目 | name, productId, desc | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/project/update` | POST | `project:update` | 更新项目 | id, name, desc | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/project/delete` | POST | `project:delete` | 删除项目 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 列表查询:`GET /project/list?pageNo=1&pageSize=20`
|
||||
2. 详情查询:`GET /project/detail?id=1`
|
||||
3. 创建项目:`POST /project/create` 传入 name, productId
|
||||
4. 更新项目:`POST /project/update` 传入 id, name
|
||||
5. 删除项目:`POST /project/delete` 传入 id
|
||||
|
||||
### 3.3 环境管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/environment/list` | GET | `environment:list` | 查询环境列表 | pageNo, pageSize, projectId | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/environment/create` | POST | `environment:create` | 创建环境 | projectId, name, url, config | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/environment/update` | POST | `environment:update` | 更新环境 | id, name, url, config | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/environment/delete` | POST | `environment:delete` | 删除环境 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 环境列表:`GET /environment/list?projectId=1`
|
||||
2. 创建环境:`POST /environment/create` 传入完整参数
|
||||
3. 更新环境:`POST /environment/update` 传入 id 和更新字段
|
||||
4. 删除环境:`POST /environment/delete` 传入 id
|
||||
|
||||
### 3.4 项目成员模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/project/member/list` | GET | `project_member:list` | 查询项目成员列表 | project_id, pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 3}}` |
|
||||
| `/project/member/create` | POST | `project_member:create` | 批量添加项目成员 | project_id, user_ids | `{"code": 20000, "data": {"id": [1, 2, 3]}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 成员列表:`GET /project/member/list?project_id=1&pageNo=1&pageSize=10`
|
||||
2. 批量添加:`POST /project/member/create` 传入 `{"project_id": 1, "user_ids": [2, 3]}`
|
||||
3. 边界测试:user_ids 为空数组
|
||||
4. 错误测试:user_ids 不是数组
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 20000,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"project_id": 1,
|
||||
"user_id": 2,
|
||||
"role": 1,
|
||||
"role_name": "测试经理",
|
||||
"project_name": "示例项目",
|
||||
"username": "zhangsan",
|
||||
"real_name": "张三",
|
||||
"joined_time": "2026-04-22T10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 产品管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/product/list` | GET | `product:list` | 查询产品列表 | pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/product/detail` | GET | `product:detail` | 查询产品详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/product/create` | POST | `product:create` | 创建产品 | name, desc | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/product/update` | POST | `product:update` | 更新产品 | id, name, desc | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/product/delete` | POST | `product:delete` | 删除产品 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 产品列表:`GET /product/list?pageNo=1&pageSize=20`
|
||||
2. 详情查询:`GET /product/detail?id=1`
|
||||
3. 创建产品:`POST /product/create` 传入 name
|
||||
4. 更新产品:`POST /product/update` 传入 id, name
|
||||
5. 删除产品:`POST /product/delete` 传入 id
|
||||
|
||||
### 3.6 模块管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/module/tree` | GET | `module:list` | 获取模块树 | projectId | `{"code": 20000, "data": [...]}` |
|
||||
| `/module/create` | POST | `module:create` | 创建模块 | projectId, name, parentId | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/module/update` | POST | `module:update` | 更新模块 | id, name | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/module/delete` | POST | `module:delete` | 删除模块 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 模块树:`GET /module/tree?projectId=1`
|
||||
2. 创建模块:`POST /module/create` 传入 projectId, name
|
||||
3. 更新模块:`POST /module/update` 传入 id, name
|
||||
4. 删除模块:`POST /module/delete` 传入 id
|
||||
|
||||
### 3.7 用例管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/case/list` | GET | `case:list` | 查询用例列表 | moduleId, pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 20}}` |
|
||||
| `/case/detail` | GET | `case:detail` | 查询用例详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/case/create` | POST | `case:create` | 创建用例 | moduleId, title, steps | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/update` | POST | `case:update` | 更新用例 | id, title, steps | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/delete` | POST | `case:delete` | 删除用例 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/snapshot/create` | POST | `case_snapshot:create` | 创建用例快照 | caseId, reason | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/snapshot/list` | GET | `case_snapshot:list` | 查询用例快照 | caseId | `{"code": 20000, "data": [...]}` |
|
||||
| `/case/review/create` | POST | `case_review:create` | 创建用例评审 | caseId, reviewerId | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/review/update` | POST | `case_review:update` | 更新评审状态 | id, status | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/case/review/list` | GET | `case_review:list` | 查询评审列表 | projectId | `{"code": 20000, "data": [...]}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 用例列表:`GET /case/list?moduleId=1&pageNo=1&pageSize=10`
|
||||
2. 详情查询:`GET /case/detail?id=1`
|
||||
3. 创建用例:`POST /case/create` 传入完整参数
|
||||
4. 更新用例:`POST /case/update` 传入 id, title, steps
|
||||
5. 删除用例:`POST /case/delete` 传入 id
|
||||
6. 创建快照:`POST /case/snapshot/create` 传入 caseId
|
||||
7. 快照列表:`GET /case/snapshot/list?caseId=1`
|
||||
8. 创建评审:`POST /case/review/create` 传入 caseId, reviewerId
|
||||
9. 更新评审:`POST /case/review/update` 传入 id, status
|
||||
10. 评审列表:`GET /case/review/list?projectId=1`
|
||||
|
||||
### 3.8 计划管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/plan/list` | GET | `plan:list` | 查询计划列表 | projectId, pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/plan/detail` | GET | `plan:detail` | 查询计划详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/plan/create` | POST | `plan:create` | 创建计划 | projectId, name, startDate, endDate | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/plan/update` | POST | `plan:update` | 更新计划 | id, name, startDate, endDate | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/plan/delete` | POST | `plan:delete` | 删除计划 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/plan/round/create` | POST | `plan_round:create` | 创建轮次 | planId, name | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/plan/round/list` | GET | `plan_round:list` | 查询轮次列表 | planId | `{"code": 20000, "data": [...]}` |
|
||||
| `/plan/case/add` | POST | `plan_case:add` | 添加用例到计划 | planId, caseIds | `{"code": 20000, "data": {"addedCount": 5}}` |
|
||||
| `/plan/case/list` | GET | `plan_case:list` | 查询计划用例列表 | planId, roundId | `{"code": 20000, "data": [...]}` |
|
||||
| `/plan/case/execute` | POST | `plan_case:execute` | 执行计划用例 | id, status, comment | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/plan/progress` | GET | `plan:progress` | 查询计划进度 | planId | `{"code": 20000, "data": {...}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 计划列表:`GET /plan/list?projectId=1&pageNo=1&pageSize=10`
|
||||
2. 详情查询:`GET /plan/detail?id=1`
|
||||
3. 创建计划:`POST /plan/create` 传入完整参数
|
||||
4. 更新计划:`POST /plan/update` 传入 id, name
|
||||
5. 删除计划:`POST /plan/delete` 传入 id
|
||||
6. 创建轮次:`POST /plan/round/create` 传入 planId, name
|
||||
7. 轮次列表:`GET /plan/round/list?planId=1`
|
||||
8. 添加用例:`POST /plan/case/add` 传入 planId, caseIds
|
||||
9. 用例列表:`GET /plan/case/list?planId=1&roundId=1`
|
||||
10. 执行用例:`POST /plan/case/execute` 传入 id, status
|
||||
11. 进度查询:`GET /plan/progress?planId=1`
|
||||
|
||||
### 3.9 报告管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/report/list` | GET | `report:list` | 查询报告列表 | projectId, pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/report/detail` | GET | `report:detail` | 查询报告详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/report/generate` | POST | `report:generate` | 生成测试报告 | planId, roundId, title | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 报告列表:`GET /report/list?projectId=1&pageNo=1&pageSize=10`
|
||||
2. 详情查询:`GET /report/detail?id=1`
|
||||
3. 生成报告:`POST /report/generate` 传入 planId, roundId, title
|
||||
|
||||
### 3.10 造数器模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/data/builder/list` | GET | `data_builder:list` | 查询造数器列表 | pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/data/builder/detail` | GET | `data_builder:detail` | 查询造数器详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/data/builder/create` | POST | `data_builder:create` | 创建造数器 | name, config | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/data/builder/update` | POST | `data_builder:update` | 更新造数器 | id, name, config | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/data/builder/delete` | POST | `data_builder:delete` | 删除造数器 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/data/builder/execute` | POST | `data_builder:execute` | 执行造数器 | id, envId | `{"code": 20000, "data": {...}}` |
|
||||
| `/data/task/status` | GET | `data_task:status` | 查询任务状态 | taskId | `{"code": 20000, "data": {...}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 造数器列表:`GET /data/builder/list?pageNo=1&pageSize=10`
|
||||
2. 详情查询:`GET /data/builder/detail?id=1`
|
||||
3. 创建造数器:`POST /data/builder/create` 传入 name, config
|
||||
4. 更新造数器:`POST /data/builder/update` 传入 id, name, config
|
||||
5. 删除造数器:`POST /data/builder/delete` 传入 id
|
||||
6. 执行造数器:`POST /data/builder/execute` 传入 id, envId
|
||||
7. 任务状态:`GET /data/task/status?taskId=1`
|
||||
|
||||
### 3.11 权限管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/role/list` | GET | `role:list` | 查询角色列表 | - | `{"code": 20000, "data": [...]}` |
|
||||
| `/role/page/list` | GET | `role:list` | 分页查询角色 | pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 5}}` |
|
||||
| `/role/detail` | GET | `role:detail` | 查询角色详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/role/create` | POST | `role:create` | 创建角色 | name, status | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/role/update` | POST | `role:update` | 更新角色 | id, name, status | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/role/delete` | POST | `role:delete` | 删除角色 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/permission/list` | GET | `permission:list` | 查询权限列表 | - | `{"code": 20000, "data": [...]}` |
|
||||
| `/permission/detail` | GET | `permission:detail` | 查询权限详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/permission/create` | POST | `permission:create` | 创建权限 | name, code | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/permission/update` | POST | `permission:update` | 更新权限 | id, name, code | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/permission/delete` | POST | `permission:delete` | 删除权限 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/menu/tree` | GET | `menu:list` | 获取菜单树 | - | `{"code": 20000, "data": [...]}` |
|
||||
| `/menu/current/list` | GET | - | 获取当前用户菜单 | - | `{"code": 20000, "data": [...]}` |
|
||||
| `/role/menu/tree` | GET | `role_menu:list` | 获取角色菜单树 | roleId | `{"code": 20000, "data": [...]}` |
|
||||
| `/menu/detail` | GET | `menu:detail` | 查询菜单详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/menu/create` | POST | `menu:create` | 创建菜单 | name, path, parentId | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/menu/update` | POST | `menu:update` | 更新菜单 | id, name, path | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/menu/delete` | POST | `menu:delete` | 删除菜单 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/role/permission/list` | GET | `role_permission:list` | 查询角色权限 | roleId | `{"code": 20000, "data": [...]}` |
|
||||
| `/role/permission/assign` | POST | `role_permission:assign` | 分配角色权限 | roleId, permissionIds | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/role/menu/list` | GET | `role_menu:list` | 查询角色菜单 | roleId | `{"code": 20000, "data": [...]}` |
|
||||
| `/role/menu/assign` | POST | `role_menu:assign` | 分配角色菜单 | roleId, menuIds | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 角色列表:`GET /role/list`
|
||||
2. 分页角色:`GET /role/page/list?pageNo=1&pageSize=10`
|
||||
3. 角色详情:`GET /role/detail?id=1`
|
||||
4. 创建角色:`POST /role/create` 传入 name, status
|
||||
5. 分配权限:`POST /role/permission/assign` 传入 roleId, permissionIds
|
||||
6. 分配菜单:`POST /role/menu/assign` 传入 roleId, menuIds
|
||||
7. 菜单树:`GET /menu/tree`
|
||||
8. 当前菜单:`GET /menu/current/list`
|
||||
|
||||
### 3.12 用户管理模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/user/list` | GET | `user:list` | 查询用户列表 | pageNo, pageSize | `{"code": 20000, "data": {"list": [...], "total": 10}}` |
|
||||
| `/user/detail` | GET | `user:detail` | 查询用户详情 | id | `{"code": 20000, "data": {...}}` |
|
||||
| `/user/create` | POST | `user:create` | 创建用户 | username, real_name, password | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/user/update` | POST | `user:update` | 更新用户 | id, real_name, password | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/user/delete` | POST | `user:delete` | 删除用户 | id | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/user/role/list` | GET | `user_role:list` | 查询用户角色 | userId | `{"code": 20000, "data": [...]}` |
|
||||
| `/user/role/assign` | POST | `user_role:assign` | 分配用户角色 | userId, roleIds | `{"code": 20000, "data": {"id": 1}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 用户列表:`GET /user/list?pageNo=1&pageSize=20`
|
||||
2. 详情查询:`GET /user/detail?id=1`
|
||||
3. 创建用户:`POST /user/create` 传入 username, real_name, password
|
||||
4. 更新用户:`POST /user/update` 传入 id, real_name
|
||||
5. 删除用户:`POST /user/delete` 传入 id
|
||||
6. 用户角色:`GET /user/role/list?userId=1`
|
||||
7. 分配角色:`POST /user/role/assign` 传入 userId, roleIds
|
||||
|
||||
### 3.14 认证模块
|
||||
|
||||
| 接口 | 方法 | 权限 | 描述 | 请求参数 | 响应 |
|
||||
|------|------|------|------|----------|------|
|
||||
| `/auth/register` | POST | - | 用户注册 | username, password, real_name | `{"code": 20000, "data": {"id": 1}}` |
|
||||
| `/auth/login` | POST | - | 用户登录 | username, password | `{"code": 20000, "data": {"token": "...", "user": {...}}}` |
|
||||
|
||||
**测试用例:**
|
||||
1. 用户注册:`POST /auth/register` 传入 username, password, real_name
|
||||
2. 用户登录:`POST /auth/login` 传入 username, password
|
||||
3. 登录失败:传入错误的用户名或密码
|
||||
4. 注册失败:传入已存在的用户名
|
||||
|
||||
## 4. 认证与权限
|
||||
|
||||
### 4.1 认证方式
|
||||
所有接口(除了 `/auth/register` 和 `/auth/login`)都需要在请求头中携带 `accessToken`:
|
||||
|
||||
```bash
|
||||
curl -H "accessToken: your_token" http://localhost:8081/it/api/project/list
|
||||
```
|
||||
|
||||
### 4.2 权限控制
|
||||
每个接口都有对应的权限标识,例如:
|
||||
- `project:list` - 查看项目列表权限
|
||||
- `project:create` - 创建项目权限
|
||||
- `project:update` - 更新项目权限
|
||||
- `project:delete` - 删除项目权限
|
||||
|
||||
## 5. 响应格式
|
||||
|
||||
### 5.1 成功响应
|
||||
```json
|
||||
{
|
||||
"code": 20000,
|
||||
"data": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 失败响应
|
||||
```json
|
||||
{
|
||||
"code": 40009,
|
||||
"msg": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 错误码说明
|
||||
| 错误码 | 描述 |
|
||||
|--------|------|
|
||||
| 40004 | 未登录或缺少token |
|
||||
| 40008 | 数据库连接超时 |
|
||||
| 40009 | 参数错误或业务逻辑错误 |
|
||||
| 40011 | 资源不存在或查询失败 |
|
||||
| 40012 | 更新/删除操作失败 |
|
||||
|
||||
## 6. 测试工具与脚本
|
||||
|
||||
### 6.1 使用 curl 测试
|
||||
|
||||
**获取项目列表:**
|
||||
```bash
|
||||
curl 'http://localhost:8081/it/api/project/list?pageNo=1&pageSize=10' \
|
||||
-H 'accessToken: your_token'
|
||||
```
|
||||
|
||||
**添加项目成员:**
|
||||
```bash
|
||||
curl 'http://localhost:8081/it/api/project/member/create' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'accessToken: your_token' \
|
||||
--data-raw '{"project_id": 1, "user_ids": [2, 3]}'
|
||||
```
|
||||
|
||||
### 6.2 使用 Postman 测试
|
||||
1. 导入本测试文档
|
||||
2. 设置环境变量 `base_url` 为 `http://localhost:8081/it/api`
|
||||
3. 设置环境变量 `token` 为登录后获取的 accessToken
|
||||
4. 运行测试集合
|
||||
|
||||
## 7. 性能测试建议
|
||||
|
||||
1. **并发测试**:使用 JMeter 模拟 50-100 并发用户
|
||||
2. **响应时间**:目标平均响应时间 < 500ms
|
||||
3. **吞吐量**:目标 QPS > 100
|
||||
4. **数据库性能**:监控 SQL 执行时间,优化慢查询
|
||||
5. **内存使用**:监控服务内存占用,避免内存泄漏
|
||||
|
||||
## 8. 安全测试建议
|
||||
|
||||
1. **认证绕过**:测试无 token 访问受保护接口
|
||||
2. **权限越权**:测试低权限用户访问高权限接口
|
||||
3. **SQL 注入**:测试输入参数中的 SQL 注入攻击
|
||||
4. **XSS 攻击**:测试输入参数中的 XSS 攻击
|
||||
5. **CSRF 攻击**:测试跨站请求伪造防护
|
||||
|
||||
## 9. 测试环境配置
|
||||
|
||||
### 9.1 本地环境
|
||||
- Python 3.8+
|
||||
- Flask 2.0+
|
||||
- MySQL 5.7+
|
||||
- Redis(可选,用于缓存)
|
||||
|
||||
### 9.2 环境变量
|
||||
- `FLASK_APP`: `app.py`
|
||||
- `FLASK_ENV`: `development` 或 `production`
|
||||
- `DATABASE_URL`: 数据库连接字符串
|
||||
- `SECRET_KEY`: 用于 JWT 签名
|
||||
|
||||
## 10. 总结
|
||||
|
||||
本测试文档涵盖了项目所有 API 接口的测试用例,包括:
|
||||
- 功能测试:验证接口正常功能
|
||||
- 边界测试:测试参数边界情况
|
||||
- 错误测试:测试错误处理
|
||||
- 性能测试:验证系统性能
|
||||
- 安全测试:验证系统安全性
|
||||
|
||||
通过系统性的测试,可以确保 API 接口的稳定性、可靠性和安全性,为前端应用提供可靠的后端支持。
|
||||
BIN
app/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/__pycache__/views.cpython-38.pyc
Normal file
BIN
app/api/__pycache__/views.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/baseCrudController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/baseCrudController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/bugController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/bugController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/caseController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/caseController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/planController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/planController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/productController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/productController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/projectController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/projectController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/rbacController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/rbacController.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/controller/__pycache__/reportController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/reportController.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/controller/__pycache__/userController.cpython-38.pyc
Normal file
BIN
app/api/controller/__pycache__/userController.cpython-38.pyc
Normal file
Binary file not shown.
57
app/api/controller/baseCrudController.py
Normal file
57
app/api/controller/baseCrudController.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# encoding: UTF-8
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from common.sqlSession import SqlSession
|
||||
|
||||
|
||||
class BaseCrudController(object):
|
||||
"""通用 Controller 基类,封装公共的请求取值与序列化逻辑。"""
|
||||
|
||||
def __init__(self, req_data):
|
||||
# 每个 controller 持有一个独立 session,沿用当前项目的使用方式。
|
||||
self.session = SqlSession()
|
||||
# req_data 兼容 request.args 和 request.get_json() 两种来源。
|
||||
self.req_data = req_data
|
||||
|
||||
def close_session(self):
|
||||
if self.session:
|
||||
self.session.close()
|
||||
|
||||
@staticmethod
|
||||
def _get(req_data, *keys, default=None):
|
||||
"""按顺序读取多个候选参数名,兼容前后端字段别名。"""
|
||||
for key in keys:
|
||||
value = req_data.get(key)
|
||||
if value not in (None, ''):
|
||||
return value
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def _format_value(value):
|
||||
"""将数据库对象中的特殊类型转成可直接返回给前端的值。"""
|
||||
if isinstance(value, datetime):
|
||||
return value.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(value, date):
|
||||
return value.strftime('%Y-%m-%d')
|
||||
if isinstance(value, Decimal):
|
||||
return float(value)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, item, exclude=None):
|
||||
"""单对象序列化,可按需排除不希望暴露给前端的字段。"""
|
||||
if not item:
|
||||
return {}
|
||||
exclude = exclude or []
|
||||
item_dict = item.to_dict()
|
||||
for key in exclude:
|
||||
item_dict.pop(key, None)
|
||||
for key, value in item_dict.items():
|
||||
item_dict[key] = cls._format_value(value)
|
||||
return item_dict
|
||||
|
||||
@classmethod
|
||||
def serialize_list(cls, items, exclude=None):
|
||||
"""列表对象序列化。"""
|
||||
return [cls.serialize(item, exclude) for item in items]
|
||||
325
app/api/controller/bugController.py
Normal file
325
app/api/controller/bugController.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# encoding: UTF-8
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from flask import current_app
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.bugModel import Bug, BugComment
|
||||
from ..model.productModel import Product
|
||||
from ..model.projectModel import Project
|
||||
from ..model.userModel import User
|
||||
from ..model.caseModel import Module
|
||||
from ..service.bugService import BugService
|
||||
from ..service.userService import UserService
|
||||
|
||||
|
||||
class BugUploadController(BaseCrudController):
|
||||
UPLOAD_FOLDER = 'attachment/bug_picture'
|
||||
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
|
||||
|
||||
def allowed_file(self, filename):
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in self.ALLOWED_EXTENSIONS
|
||||
|
||||
def bug_upload(self):
|
||||
if 'file' not in self.req_data.files:
|
||||
return '', '未找到上传文件'
|
||||
|
||||
file = self.req_data.files['file']
|
||||
if file.filename == '':
|
||||
return '', '文件名不能为空'
|
||||
|
||||
if not self.allowed_file(file.filename):
|
||||
return '', '不支持的文件格式,仅支持:png, jpg, jpeg, gif, bmp'
|
||||
|
||||
try:
|
||||
os.makedirs(self.UPLOAD_FOLDER, exist_ok=True)
|
||||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
ext = file.filename.rsplit('.', 1)[1].lower()
|
||||
new_filename = f'bug-{timestamp}-{uuid.uuid4().hex[:8]}.{ext}'
|
||||
file_path = os.path.join(self.UPLOAD_FOLDER, new_filename)
|
||||
file.save(file_path)
|
||||
|
||||
file_url = f'/uploads/{new_filename}'
|
||||
return file_url, ''
|
||||
except Exception as e:
|
||||
return '', f'文件上传失败:{str(e)}'
|
||||
|
||||
|
||||
class BugController(BaseCrudController):
|
||||
def bug_list(self):
|
||||
filters = []
|
||||
product_id = self._get(self.req_data, 'productId', 'product_id')
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
module_id = self._get(self.req_data, 'moduleId', 'module_id')
|
||||
bug_type = self._get(self.req_data, 'bugType', 'bug_type')
|
||||
severity = self._get(self.req_data, 'severity')
|
||||
priority = self._get(self.req_data, 'priority')
|
||||
status = self._get(self.req_data, 'status')
|
||||
assignee_id = self._get(self.req_data, 'assigneeId', 'assignee_id')
|
||||
reporter_id = self._get(self.req_data, 'reporterId', 'reporter_id')
|
||||
resolved_by = self._get(self.req_data, 'resolvedBy', 'resolved_by')
|
||||
reproduce_rate = self._get(self.req_data, 'reproduceRate', 'reproduce_rate')
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
|
||||
if product_id:
|
||||
filters.append(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
filters.append(Bug.project_id == int(project_id))
|
||||
if module_id:
|
||||
filters.append(Bug.module_id == int(module_id))
|
||||
if bug_type not in (None, ''):
|
||||
filters.append(Bug.bug_type == int(bug_type))
|
||||
if severity not in (None, ''):
|
||||
filters.append(Bug.severity == int(severity))
|
||||
if priority not in (None, ''):
|
||||
filters.append(Bug.priority == int(priority))
|
||||
if status not in (None, ''):
|
||||
filters.append(Bug.status == int(status))
|
||||
if assignee_id:
|
||||
filters.append(Bug.assignee_id == int(assignee_id))
|
||||
if reporter_id:
|
||||
filters.append(Bug.reporter_id == int(reporter_id))
|
||||
if resolved_by:
|
||||
filters.append(Bug.resolved_by == int(resolved_by))
|
||||
if reproduce_rate not in (None, ''):
|
||||
filters.append(Bug.reproduce_rate == int(reproduce_rate))
|
||||
if keyword:
|
||||
filters.append(Bug.title.like(f'%{keyword}%') | Bug.description.like(f'%{keyword}%'))
|
||||
|
||||
items, total = BugService.list_by_filters(
|
||||
self.session, Bug, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
Bug.created_time
|
||||
)
|
||||
|
||||
user_ids = []
|
||||
for item in items:
|
||||
if item.assignee_id:
|
||||
user_ids.append(item.assignee_id)
|
||||
if item.reporter_id:
|
||||
user_ids.append(item.reporter_id)
|
||||
if item.resolved_by:
|
||||
user_ids.append(item.resolved_by)
|
||||
|
||||
user_info_map = UserService.get_user_info_map(self.session, user_ids) if user_ids else {}
|
||||
|
||||
result_list = []
|
||||
for item in items:
|
||||
bug_dict = item.to_dict()
|
||||
if item.assignee_id and item.assignee_id in user_info_map:
|
||||
bug_dict['assignee_name'] = user_info_map[item.assignee_id].get('real_name', '')
|
||||
else:
|
||||
bug_dict['assignee_name'] = ''
|
||||
if item.reporter_id and item.reporter_id in user_info_map:
|
||||
bug_dict['reporter_name'] = user_info_map[item.reporter_id].get('real_name', '')
|
||||
else:
|
||||
bug_dict['reporter_name'] = ''
|
||||
if item.resolved_by and item.resolved_by in user_info_map:
|
||||
bug_dict['resolved_by_name'] = user_info_map[item.resolved_by].get('real_name', '')
|
||||
else:
|
||||
bug_dict['resolved_by_name'] = ''
|
||||
result_list.append(bug_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def bug_detail(self):
|
||||
bug_id = self._get(self.req_data, 'bugId', 'id')
|
||||
if not bug_id:
|
||||
return {}, 'bugId 为必传参数'
|
||||
item = BugService.get_by_id(self.session, Bug, bug_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应 Bug!'
|
||||
ret = self.serialize(item, ['is_delete'])
|
||||
|
||||
if item.product_id:
|
||||
product = self.session.query(Product).filter(Product.id == item.product_id, Product.is_delete == 0).first()
|
||||
ret['product_name'] = product.name if product else ''
|
||||
|
||||
if item.project_id:
|
||||
project = self.session.query(Project).filter(Project.id == item.project_id, Project.is_delete == 0).first()
|
||||
ret['project_name'] = project.name if project else ''
|
||||
|
||||
if item.reporter_id:
|
||||
reporter = self.session.query(User).filter(User.id == item.reporter_id, User.is_delete == 0).first()
|
||||
ret['reporter_name'] = reporter.real_name if reporter else ''
|
||||
|
||||
if item.assignee_id:
|
||||
assignee = self.session.query(User).filter(User.id == item.assignee_id, User.is_delete == 0).first()
|
||||
ret['assignee_name'] = assignee.real_name if assignee else ''
|
||||
|
||||
if item.module_id:
|
||||
module = self.session.query(Module).filter(Module.id == item.module_id, Module.is_delete == 0).first()
|
||||
ret['module_name'] = module.name if module else ''
|
||||
|
||||
if item.resolved_by:
|
||||
resolved_by_user = self.session.query(User).filter(User.id == item.resolved_by, User.is_delete == 0).first()
|
||||
ret['resolved_by_name'] = resolved_by_user.real_name if resolved_by_user else ''
|
||||
|
||||
comments = BugService.get_comments(self.session, bug_id)
|
||||
comment_user_ids = [c.user_id for c in comments if c.user_id]
|
||||
user_info_map = UserService.get_user_info_map(self.session, comment_user_ids) if comment_user_ids else {}
|
||||
serialized_comments = []
|
||||
for comment in comments:
|
||||
comment_dict = comment.to_dict()
|
||||
if comment.user_id and comment.user_id in user_info_map:
|
||||
comment_dict['user_name'] = user_info_map[comment.user_id].get('real_name', '')
|
||||
else:
|
||||
comment_dict['user_name'] = ''
|
||||
serialized_comments.append(comment_dict)
|
||||
ret['comments'] = serialized_comments
|
||||
|
||||
history_items = BugService.get_history(self.session, bug_id)
|
||||
user_ids = set()
|
||||
for h in history_items:
|
||||
if h.operator_id:
|
||||
user_ids.add(h.operator_id)
|
||||
if h.field_name in ('assignee_id', 'reporter_id', 'user_id', 'resolved_by'):
|
||||
if h.old_value:
|
||||
try:
|
||||
user_ids.add(int(h.old_value))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if h.new_value:
|
||||
try:
|
||||
user_ids.add(int(h.new_value))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
user_info_map = UserService.get_user_info_map(self.session, list(user_ids)) if user_ids else {}
|
||||
|
||||
serialized_history = []
|
||||
for h in history_items:
|
||||
h_dict = h.to_dict()
|
||||
if h.operator_id:
|
||||
h_dict['operator_id'] = user_info_map.get(h.operator_id, {}).get('real_name', h.operator_id)
|
||||
if h.field_name in ('assignee_id', 'reporter_id', 'user_id', 'resolved_by'):
|
||||
if h.old_value:
|
||||
try:
|
||||
old_uid = int(h.old_value)
|
||||
h_dict['old_value'] = user_info_map.get(old_uid, {}).get('real_name', h.old_value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if h.new_value:
|
||||
try:
|
||||
new_uid = int(h.new_value)
|
||||
h_dict['new_value'] = user_info_map.get(new_uid, {}).get('real_name', h.new_value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
serialized_history.append(h_dict)
|
||||
|
||||
ret['history'] = serialized_history
|
||||
return ret, ''
|
||||
|
||||
def bug_create(self):
|
||||
title = self._get(self.req_data, 'title')
|
||||
product_id = self._get(self.req_data, 'productId', 'product_id')
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
if not title or not product_id or not project_id:
|
||||
return 0, 'title、productId、projectId 为必传参数'
|
||||
|
||||
bug_key = BugService.generate_bug_key(self.session)
|
||||
add_info = {
|
||||
'bug_key': bug_key,
|
||||
'title': title,
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'bug_type': int(self._get(self.req_data, 'bugType', 'bug_type', default=1)),
|
||||
'severity': int(self._get(self.req_data, 'severity', default=2)),
|
||||
'priority': int(self._get(self.req_data, 'priority', default=2)),
|
||||
'status': 0,
|
||||
'reporter_id': self._get(self.req_data, 'reporterId', 'reporter_id'),
|
||||
'assignee_id': self._get(self.req_data, 'assigneeId', 'assignee_id'),
|
||||
'product_id': product_id,
|
||||
'project_id': project_id,
|
||||
'module_id': self._get(self.req_data, 'moduleId', 'module_id'),
|
||||
'case_id': self._get(self.req_data, 'caseId', 'case_id'),
|
||||
'plan_id': self._get(self.req_data, 'planId', 'plan_id'),
|
||||
'environment': self._get(self.req_data, 'environment'),
|
||||
'steps': self._get(self.req_data, 'steps'),
|
||||
'solution': self._get(self.req_data, 'solution'),
|
||||
'resolve_version': self._get(self.req_data, 'resolveVersion', 'resolve_version'),
|
||||
'resolved_by': self._get(self.req_data, 'resolvedBy', 'resolved_by'),
|
||||
'reproduce_rate': self._get(self.req_data, 'reproduceRate', 'reproduce_rate'),
|
||||
'is_delete': 0
|
||||
}
|
||||
return BugService.create(self.session, Bug, add_info)
|
||||
|
||||
def bug_update(self):
|
||||
bug_id = self._get(self.req_data, 'bugId', 'id')
|
||||
if not bug_id:
|
||||
return 0, 'bugId 为必传参数'
|
||||
|
||||
update_info = {}
|
||||
field_mapping = [
|
||||
(('title',), 'title'),
|
||||
(('description',), 'description'),
|
||||
(('bugType', 'bug_type'), 'bug_type'),
|
||||
(('severity',), 'severity'),
|
||||
(('priority',), 'priority'),
|
||||
(('status',), 'status'),
|
||||
(('assigneeId', 'assignee_id'), 'assignee_id'),
|
||||
(('reporterId', 'reporter_id'), 'reporter_id'),
|
||||
(('moduleId', 'module_id'), 'module_id'),
|
||||
(('caseId', 'case_id'), 'case_id'),
|
||||
(('planId', 'plan_id'), 'plan_id'),
|
||||
(('environment',), 'environment'),
|
||||
(('steps',), 'steps'),
|
||||
(('solution',), 'solution'),
|
||||
(('resolveVersion', 'resolve_version'), 'resolve_version'),
|
||||
(('resolvedBy', 'resolved_by'), 'resolved_by'),
|
||||
(('reproduceRate', 'reproduce_rate'), 'reproduce_rate')
|
||||
]
|
||||
|
||||
for req_keys, column_key in field_mapping:
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
|
||||
result = BugService.update_by_id(self.session, Bug, bug_id, update_info)
|
||||
|
||||
comment = self._get(self.req_data, 'comment')
|
||||
user_id = self._get(self.req_data, 'user_id', 'userId')
|
||||
if comment and user_id:
|
||||
BugService.add_comment(self.session, bug_id, comment, user_id)
|
||||
|
||||
return result
|
||||
|
||||
def bug_delete(self):
|
||||
bug_id = self._get(self.req_data, 'bugId', 'id')
|
||||
if not bug_id:
|
||||
return 0, 'bugId 为必传参数'
|
||||
return BugService.delete_by_id(self.session, Bug, bug_id)
|
||||
|
||||
def bug_history_add(self):
|
||||
bug_id = self._get(self.req_data, 'bugId', 'id')
|
||||
field_name = self._get(self.req_data, 'fieldName', 'field_name')
|
||||
old_value = self._get(self.req_data, 'oldValue', 'old_value')
|
||||
new_value = self._get(self.req_data, 'newValue', 'new_value')
|
||||
operator_id = self._get(self.req_data, 'operatorId', 'operator_id', 'user_id', 'userId')
|
||||
|
||||
if not bug_id:
|
||||
return 0, 'bugId 为必传参数'
|
||||
if not field_name:
|
||||
return 0, 'fieldName 为必传参数'
|
||||
if not operator_id:
|
||||
return 0, 'operatorId 为必传参数'
|
||||
|
||||
success = BugService.add_history(self.session, bug_id, field_name, old_value, new_value, operator_id)
|
||||
return 1 if success else 0, '' if success else '添加历史记录失败'
|
||||
|
||||
def bug_comment_add(self):
|
||||
user_id = self._get(self.req_data, 'user_id', 'reporter_id', 'reporterId')
|
||||
bug_id = self._get(self.req_data, 'bugId')
|
||||
content = self._get(self.req_data, 'content')
|
||||
if not bug_id:
|
||||
return 0, 'bugId 为必传参数'
|
||||
if not content:
|
||||
return 0, 'content 为必传参数'
|
||||
return BugService.add_comment(self.session, bug_id, content, user_id)
|
||||
|
||||
def bug_stats(self):
|
||||
product_id = self._get(self.req_data, 'productId', 'product_id')
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
return BugService.get_stats(self.session, product_id, project_id)
|
||||
413
app/api/controller/caseController.py
Normal file
413
app/api/controller/caseController.py
Normal file
@@ -0,0 +1,413 @@
|
||||
# encoding: UTF-8
|
||||
import os
|
||||
import json
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from flask import g
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.caseModel import CaseReview, CaseSnapshot, Module, TestCase
|
||||
from ..model.projectModel import Project
|
||||
from ..model.userModel import User
|
||||
from ..service.caseService import CaseService
|
||||
from logger import logger
|
||||
|
||||
|
||||
class CaseController(BaseCrudController):
|
||||
def module_list(self):
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
parent_id = self._get(self.req_data, 'parentId')
|
||||
filters = []
|
||||
|
||||
if project_id:
|
||||
filters.append(Module.project_id == int(project_id))
|
||||
if parent_id not in (None, ''):
|
||||
filters.append(Module.parent_id == int(parent_id))
|
||||
|
||||
parent_module = Module.__table__.alias('parent')
|
||||
query = self.session.query(Module, parent_module.c.name.label('parent_name')).\
|
||||
outerjoin(parent_module, Module.parent_id == parent_module.c.id).\
|
||||
filter(*filters)
|
||||
|
||||
if hasattr(Module, 'is_delete'):
|
||||
query = query.filter(Module.is_delete == 0)
|
||||
|
||||
total = query.count()
|
||||
|
||||
page_num = int(self._get(self.req_data, 'pageNo', default=1))
|
||||
page_size = int(self._get(self.req_data, 'pageSize', default=200))
|
||||
|
||||
query = query.order_by(Module.id)
|
||||
items = query.offset((page_num - 1) * page_size).limit(page_size).all()
|
||||
|
||||
result_list = []
|
||||
for module, parent_name in items:
|
||||
module_dict = self.serialize(module, ['is_delete'])
|
||||
module_dict['parent_name'] = parent_name or ''
|
||||
result_list.append(module_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def module_create(self):
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not project_id or not name:
|
||||
return 0, 'projectId、name 为必传参数'
|
||||
add_info = {'project_id': project_id, 'parent_id': int(self._get(self.req_data, 'parentId', default=0)), 'name': name, 'sort_order': int(self._get(self.req_data, 'sortOrder', default=0)), 'path': self._get(self.req_data, 'path'), 'is_delete': 0}
|
||||
return CaseService.create(self.session, Module, add_info)
|
||||
|
||||
def module_update(self):
|
||||
module_id = self._get(self.req_data, 'moduleId', 'id')
|
||||
if not module_id:
|
||||
return 0, 'moduleId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('parentId', 'parent_id'), ('name', 'name'), ('sortOrder', 'sort_order'), ('path', 'path')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return CaseService.update_by_id(self.session, Module, module_id, update_info)
|
||||
|
||||
def module_delete(self):
|
||||
module_id = self._get(self.req_data, 'moduleId', 'id')
|
||||
if not module_id:
|
||||
return 0, 'moduleId 为必传参数'
|
||||
return CaseService.delete_by_id(self.session, Module, module_id)
|
||||
|
||||
def case_list(self):
|
||||
"""分页查询用例列表,支持项目名称、用例标题、优先级、类型、状态、是否自动化、标签过滤。"""
|
||||
filters = []
|
||||
|
||||
project_name = self._get(self.req_data, 'projectName')
|
||||
if project_name:
|
||||
filters.append(Project.name.like('%{}%'.format(project_name)))
|
||||
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
if project_id:
|
||||
filters.append(TestCase.project_id == int(project_id))
|
||||
|
||||
module_name = self._get(self.req_data, 'moduleName', 'module_name')
|
||||
if module_name:
|
||||
filters.append(Module.name.like('%{}%'.format(module_name)))
|
||||
|
||||
for req_key, column in [('moduleId', TestCase.module_id), ('priority', TestCase.priority),
|
||||
('caseType', TestCase.case_type), ('status', TestCase.status),
|
||||
('isAuto', TestCase.is_auto)]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value not in (None, ''):
|
||||
filters.append(column == int(value))
|
||||
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
if keyword:
|
||||
filters.append(TestCase.title.like('%{}%'.format(keyword)))
|
||||
|
||||
tag = self._get(self.req_data, 'tag')
|
||||
if tag:
|
||||
filters.append(TestCase.tags.any(tag))
|
||||
|
||||
created_by_name = self._get(self.req_data, 'createdBy')
|
||||
if created_by_name:
|
||||
filters.append(User.real_name.like('%{}%'.format(created_by_name)))
|
||||
|
||||
query = self.session.query(TestCase, Project.name.label('project_name'), Module.name.label('module_name'), User.real_name.label('created_by_name')).\
|
||||
join(Project, TestCase.project_id == Project.id, isouter=True).\
|
||||
join(Module, TestCase.module_id == Module.id, isouter=True).\
|
||||
join(User, TestCase.created_by == User.id, isouter=True).\
|
||||
filter(*filters)
|
||||
|
||||
if hasattr(TestCase, 'is_delete'):
|
||||
query = query.filter(TestCase.is_delete == 0)
|
||||
if hasattr(Project, 'is_delete'):
|
||||
query = query.filter(Project.is_delete == 0)
|
||||
if hasattr(Module, 'is_delete'):
|
||||
query = query.filter(or_(Module.is_delete == 0, Module.is_delete.is_(None)))
|
||||
if hasattr(User, 'is_delete'):
|
||||
query = query.filter(or_(User.is_delete == 0, User.is_delete.is_(None)))
|
||||
|
||||
total = query.count()
|
||||
|
||||
page_num = int(self._get(self.req_data, 'pageNo', 'page', default=1))
|
||||
page_size = int(self._get(self.req_data, 'pageSize', 'size', default=20))
|
||||
|
||||
query = query.order_by(TestCase.id.desc())
|
||||
items = query.offset((page_num - 1) * page_size).limit(page_size).all()
|
||||
|
||||
result_list = []
|
||||
for case, project_name, module_name, created_by_name in items:
|
||||
case_dict = self.serialize(case, ['is_delete'])
|
||||
case_dict['project_name'] = project_name or ''
|
||||
case_dict['module_name'] = module_name or ''
|
||||
case_dict['case_key'] = case_dict.get('case_key', '')
|
||||
case_dict['created_by_name'] = created_by_name or ''
|
||||
if not case_dict.get('steps'):
|
||||
case_dict['steps'] = ''
|
||||
result_list.append(case_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def case_detail(self):
|
||||
case_id = self._get(self.req_data, 'caseId', 'id')
|
||||
if not case_id:
|
||||
return {}, 'caseId 为必传参数'
|
||||
item = CaseService.get_by_id(self.session, TestCase, case_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应用例!'
|
||||
result = self.serialize(item, ['is_delete'])
|
||||
if not result.get('steps'):
|
||||
result['steps'] = ''
|
||||
if item.module_id:
|
||||
module = self.session.query(Module).filter(Module.id == item.module_id).first()
|
||||
result['module_name'] = module.name if module else ''
|
||||
else:
|
||||
result['module_name'] = ''
|
||||
return result, ''
|
||||
|
||||
def case_create(self):
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
title = self._get(self.req_data, 'title')
|
||||
if not project_id or not title:
|
||||
return 0, 'projectId、title 为必传参数'
|
||||
steps_value = self._get(self.req_data, 'steps', default='')
|
||||
if isinstance(steps_value, (list, dict)):
|
||||
steps_value = ''
|
||||
add_info = {
|
||||
'project_id': project_id,
|
||||
'module_id': self._get(self.req_data, 'moduleId'),
|
||||
'case_key': self._get(self.req_data, 'caseKey') or CaseService.next_case_key(self.session, project_id),
|
||||
'title': title,
|
||||
'preconditions': self._get(self.req_data, 'preconditions'),
|
||||
'steps': steps_value,
|
||||
'expected_results': self._get(self.req_data, 'expectedResults'),
|
||||
'priority': int(self._get(self.req_data, 'priority', default=2)),
|
||||
'case_type': int(self._get(self.req_data, 'caseType', default=1)),
|
||||
'tags': self._get(self.req_data, 'tags', default=[]),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'is_auto': int(self._get(self.req_data, 'isAuto', default=0)),
|
||||
'created_by': getattr(g, 'current_user_id', None),
|
||||
'is_delete': 0
|
||||
}
|
||||
return CaseService.create(self.session, TestCase, add_info)
|
||||
|
||||
def case_update(self):
|
||||
"""更新用例内容,只更新请求中传入的字段。"""
|
||||
case_id = self._get(self.req_data, 'caseId', 'id')
|
||||
if not case_id:
|
||||
return 0, 'caseId 为必传参数'
|
||||
update_info = {}
|
||||
mapping = [('moduleId', 'module_id'), ('caseKey', 'case_key'), ('title', 'title'), ('preconditions', 'preconditions'), ('expectedResults', 'expected_results'), ('priority', 'priority'), ('caseType', 'case_type'), ('tags', 'tags'), ('status', 'status'), ('isAuto', 'is_auto')]
|
||||
for req_key, column_key in mapping:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
steps_value = self._get(self.req_data, 'steps')
|
||||
if steps_value is not None:
|
||||
if isinstance(steps_value, (list, dict)):
|
||||
steps_value = ''
|
||||
update_info['steps'] = steps_value
|
||||
return CaseService.update_by_id(self.session, TestCase, case_id, update_info)
|
||||
|
||||
def case_delete(self):
|
||||
case_id = self._get(self.req_data, 'caseId', 'id')
|
||||
if not case_id:
|
||||
return 0, 'caseId 为必传参数'
|
||||
return CaseService.delete_by_id(self.session, TestCase, case_id)
|
||||
|
||||
def snapshot_create(self):
|
||||
case_id = self._get(self.req_data, 'caseId')
|
||||
if not case_id:
|
||||
return 0, 'caseId 为必传参数'
|
||||
case_obj = CaseService.get_by_id(self.session, TestCase, case_id)
|
||||
if not case_obj:
|
||||
return 0, '未查询到对应用例!'
|
||||
version = CaseService.next_snapshot_version(self.session, case_id)
|
||||
snapshot = self.serialize(case_obj, ['is_delete'])
|
||||
if not snapshot.get('steps'):
|
||||
snapshot['steps'] = ''
|
||||
add_info = {'case_id': case_id, 'version': version, 'snapshot': snapshot, 'created_by': self._get(self.req_data, 'createdBy')}
|
||||
return CaseService.create(self.session, CaseSnapshot, add_info)
|
||||
|
||||
def snapshot_list(self):
|
||||
"""查询指定用例的快照历史。"""
|
||||
case_id = self._get(self.req_data, 'caseId')
|
||||
filters = [CaseSnapshot.case_id == int(case_id)] if case_id else []
|
||||
items, total = CaseService.list_by_filters(self.session, CaseSnapshot, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), CaseSnapshot.created_time)
|
||||
return {'list': self.serialize_list(items), 'total': total}
|
||||
|
||||
def review_create(self):
|
||||
case_id = self._get(self.req_data, 'caseId')
|
||||
reviewer_id = self._get(self.req_data, 'reviewerId')
|
||||
if not case_id or not reviewer_id:
|
||||
return 0, 'caseId、reviewerId 为必传参数'
|
||||
return CaseService.create(self.session, CaseReview, {'case_id': case_id, 'reviewer_id': reviewer_id, 'comments': self._get(self.req_data, 'comments')})
|
||||
|
||||
def review_update(self):
|
||||
review_id = self._get(self.req_data, 'reviewId', 'id')
|
||||
if not review_id:
|
||||
return 0, 'reviewId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('status', 'status'), ('comments', 'comments'), ('diffContent', 'diff_content'), ('reviewedTime', 'reviewed_time')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return CaseService.update_by_id(self.session, CaseReview, review_id, update_info, soft_delete=False)
|
||||
|
||||
def review_list(self):
|
||||
"""查询用例评审记录列表。"""
|
||||
case_id = self._get(self.req_data, 'caseId')
|
||||
filters = [CaseReview.case_id == int(case_id)] if case_id else []
|
||||
items, total = CaseService.list_by_filters(self.session, CaseReview, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), CaseReview.created_time)
|
||||
return {'list': self.serialize_list(items), 'total': total}
|
||||
|
||||
def case_import(self, file_path, project_id):
|
||||
"""批量导入用例"""
|
||||
try:
|
||||
from openpyxl import load_workbook
|
||||
except ImportError:
|
||||
return 0, '请先安装 openpyxl 依赖'
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return 0, '文件不存在'
|
||||
|
||||
if not project_id:
|
||||
return 0, 'projectId 为必传参数'
|
||||
|
||||
wb = load_workbook(file_path)
|
||||
sheet = wb.active
|
||||
|
||||
headers = {}
|
||||
for col in range(1, sheet.max_column + 1):
|
||||
header = str(sheet.cell(row=1, column=col).value).strip() if sheet.cell(row=1, column=col).value else ''
|
||||
if header:
|
||||
headers[header] = col
|
||||
|
||||
required_columns = ['所属模块', '用例标题', '前置条件', '步骤', '预期', '关键词', '优先级', '用例类型']
|
||||
for col in required_columns:
|
||||
if col not in headers:
|
||||
return 0, f'缺少必要列: {col}'
|
||||
|
||||
module_name_to_id = {}
|
||||
existing_modules = self.session.query(Module).filter(Module.project_id == int(project_id), Module.is_delete == 0).all()
|
||||
for module in existing_modules:
|
||||
module_name_to_id[module.name] = module.id
|
||||
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
fail_messages = []
|
||||
|
||||
for row in range(2, sheet.max_row + 1):
|
||||
try:
|
||||
module_path_str = str(sheet.cell(row=row, column=headers['所属模块']).value).strip() if sheet.cell(row=row, column=headers['所属模块']).value else ''
|
||||
|
||||
if not module_path_str:
|
||||
fail_count += 1
|
||||
fail_messages.append(f'第{row}行:所属模块为空')
|
||||
continue
|
||||
|
||||
module_names = [m.strip() for m in module_path_str.split('/') if m.strip()]
|
||||
|
||||
if not module_names:
|
||||
fail_count += 1
|
||||
fail_messages.append(f'第{row}行:所属模块格式不正确')
|
||||
continue
|
||||
|
||||
parent_id = 0
|
||||
module_id = None
|
||||
|
||||
for idx, module_name in enumerate(module_names):
|
||||
if module_name in module_name_to_id:
|
||||
parent_id = module_name_to_id[module_name]
|
||||
else:
|
||||
path_parts = module_names[:idx+1]
|
||||
module_path = '/' + '/'.join(path_parts)
|
||||
new_module = Module(
|
||||
project_id=int(project_id),
|
||||
parent_id=parent_id,
|
||||
name=module_name,
|
||||
path=module_path,
|
||||
is_delete=0
|
||||
)
|
||||
self.session.add(new_module)
|
||||
self.session.flush()
|
||||
module_name_to_id[module_name] = new_module.id
|
||||
parent_id = new_module.id
|
||||
|
||||
module_id = parent_id
|
||||
|
||||
title = str(sheet.cell(row=row, column=headers['用例标题']).value).strip() if sheet.cell(row=row, column=headers['用例标题']).value else ''
|
||||
preconditions = str(sheet.cell(row=row, column=headers['前置条件']).value).strip() if sheet.cell(row=row, column=headers['前置条件']).value else ''
|
||||
steps = str(sheet.cell(row=row, column=headers['步骤']).value).strip() if sheet.cell(row=row, column=headers['步骤']).value else ''
|
||||
expected_results = str(sheet.cell(row=row, column=headers['预期']).value).strip() if sheet.cell(row=row, column=headers['预期']).value else ''
|
||||
keywords = str(sheet.cell(row=row, column=headers['关键词']).value).strip() if sheet.cell(row=row, column=headers['关键词']).value else ''
|
||||
priority_str = str(sheet.cell(row=row, column=headers['优先级']).value).strip() if sheet.cell(row=row, column=headers['优先级']).value else '2'
|
||||
case_type_str = str(sheet.cell(row=row, column=headers['用例类型']).value).strip() if sheet.cell(row=row, column=headers['用例类型']).value else '1'
|
||||
|
||||
if not title:
|
||||
fail_count += 1
|
||||
fail_messages.append(f'第{row}行:用例标题为空')
|
||||
continue
|
||||
|
||||
priority_map = {'P0': 0, 'P1': 1, 'P2': 2, 'P3': 3}
|
||||
priority = priority_map.get(priority_str, int(priority_str) if priority_str.isdigit() else 2)
|
||||
|
||||
case_type_map = {'功能': 1, '性能': 2, '安全': 3, '接口': 4}
|
||||
case_type = case_type_map.get(case_type_str, int(case_type_str) if case_type_str.isdigit() else 1)
|
||||
|
||||
tags = [k.strip() for k in keywords.split(',')] if keywords else []
|
||||
|
||||
retry_count = 0
|
||||
max_retries = 5
|
||||
case_key = CaseService.next_case_key(self.session, project_id)
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
case = TestCase(
|
||||
project_id=int(project_id),
|
||||
module_id=module_id,
|
||||
case_key=case_key,
|
||||
title=title,
|
||||
preconditions=preconditions,
|
||||
steps=steps,
|
||||
expected_results=expected_results,
|
||||
priority=priority,
|
||||
case_type=case_type,
|
||||
tags=tags,
|
||||
status=1,
|
||||
is_auto=0,
|
||||
created_by=getattr(g, 'current_user_id', None),
|
||||
is_delete=0
|
||||
)
|
||||
self.session.add(case)
|
||||
self.session.flush()
|
||||
success_count += 1
|
||||
break
|
||||
except Exception as e:
|
||||
if 'duplicate key' in str(e).lower() or 'already exists' in str(e).lower():
|
||||
logger.warning(f'case_import case_key冲突,重新生成:{case_key}, 错误:{str(e)}')
|
||||
case_key = CaseService.next_case_key(self.session, project_id)
|
||||
retry_count += 1
|
||||
else:
|
||||
raise
|
||||
|
||||
if retry_count >= max_retries:
|
||||
fail_count += 1
|
||||
fail_messages.append(f'第{row}行:用例标题[{title}]导入失败:case_key生成失败')
|
||||
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
fail_messages.append(f'第{row}行:导入失败 - {str(e)}')
|
||||
|
||||
try:
|
||||
self.session.commit()
|
||||
msg = f'导入完成:成功{success_count}条,失败{fail_count}条'
|
||||
if fail_messages:
|
||||
msg += f'。失败详情:{"; ".join(fail_messages[:10])}'
|
||||
if len(fail_messages) > 10:
|
||||
msg += f'...(共{len(fail_messages)}条)'
|
||||
return success_count, msg
|
||||
except Exception as e:
|
||||
self.session.rollback()
|
||||
return 0, f'提交失败:{str(e)}'
|
||||
|
||||
@staticmethod
|
||||
def get_template_path():
|
||||
"""获取模板文件路径"""
|
||||
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))), 'attachment', '用例导入模版.xlsx')
|
||||
80
app/api/controller/dataBuilderController.py
Normal file
80
app/api/controller/dataBuilderController.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# encoding: UTF-8
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.dataBuilderModel import DataBuilder, DataTask
|
||||
from ..service.dataBuilderService import DataBuilderService
|
||||
|
||||
|
||||
class DataBuilderController(BaseCrudController):
|
||||
"""造数器与造数任务相关接口控制器。"""
|
||||
|
||||
def builder_list(self):
|
||||
"""分页查询造数器列表,可按项目过滤。"""
|
||||
filters = []
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
if project_id:
|
||||
filters.append(DataBuilder.project_id == int(project_id))
|
||||
items, total = DataBuilderService.list_by_filters(self.session, DataBuilder, filters,
|
||||
self._get(self.req_data, 'pageNo', default=1),
|
||||
self._get(self.req_data, 'pageSize', default=20),
|
||||
DataBuilder.created_time)
|
||||
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
|
||||
|
||||
def builder_detail(self):
|
||||
"""查询造数器详情。"""
|
||||
builder_id = self._get(self.req_data, 'builderId', 'id')
|
||||
if not builder_id:
|
||||
return {}, 'builderId 为必传参数'
|
||||
item = DataBuilderService.get_by_id(self.session, DataBuilder, builder_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应造数器!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def builder_create(self):
|
||||
"""创建造数器,definition 保存流程编排或模板定义。"""
|
||||
project_id = self._get(self.req_data, 'projectId')
|
||||
name = self._get(self.req_data, 'name')
|
||||
definition = self._get(self.req_data, 'definition')
|
||||
if not project_id or not name or definition is None:
|
||||
return 0, 'projectId、name、definition 为必传参数'
|
||||
add_info = {'project_id': project_id, 'name': name, 'description': self._get(self.req_data, 'description'),
|
||||
'builder_type': int(self._get(self.req_data, 'builderType', default=1)), 'definition': definition,
|
||||
'input_schema': self._get(self.req_data, 'inputSchema'),
|
||||
'output_example': self._get(self.req_data, 'outputExample'),
|
||||
'created_by': self._get(self.req_data, 'createdBy'), 'is_delete': 0}
|
||||
return DataBuilderService.create(self.session, DataBuilder, add_info)
|
||||
|
||||
def builder_update(self):
|
||||
builder_id = self._get(self.req_data, 'builderId', 'id')
|
||||
if not builder_id:
|
||||
return 0, 'builderId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('name', 'name'), ('description', 'description'), ('builderType', 'builder_type'),
|
||||
('definition', 'definition'), ('inputSchema', 'input_schema'),
|
||||
('outputExample', 'output_example')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return DataBuilderService.update_by_id(self.session, DataBuilder, builder_id, update_info)
|
||||
|
||||
def builder_delete(self):
|
||||
builder_id = self._get(self.req_data, 'builderId', 'id')
|
||||
if not builder_id:
|
||||
return 0, 'builderId 为必传参数'
|
||||
return DataBuilderService.delete_by_id(self.session, DataBuilder, builder_id)
|
||||
|
||||
def builder_execute(self):
|
||||
builder_id = self._get(self.req_data, 'builderId')
|
||||
if not builder_id:
|
||||
return {}, 'builderId 为必传参数'
|
||||
return DataBuilderService.execute_builder(self.session, builder_id,
|
||||
self._get(self.req_data, 'params', default={}),
|
||||
self._get(self.req_data, 'createdBy'))
|
||||
|
||||
def task_status(self):
|
||||
task_id = self._get(self.req_data, 'taskId')
|
||||
if not task_id:
|
||||
return {}, 'taskId 为必传参数'
|
||||
item = DataBuilderService.get_by_id(self.session, DataTask, task_id, soft_delete=False)
|
||||
if not item:
|
||||
return {}, '未查询到对应任务!'
|
||||
return self.serialize(item), ''
|
||||
192
app/api/controller/planController.py
Normal file
192
app/api/controller/planController.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# encoding: UTF-8
|
||||
from datetime import datetime
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.planModel import PlanCase, TestPlan, TestRound
|
||||
from ..model.caseModel import Module, TestCase
|
||||
from ..service.planService import PlanService
|
||||
from ..service.userService import UserService
|
||||
|
||||
|
||||
class PlanController(BaseCrudController):
|
||||
def plan_list(self):
|
||||
filters = []
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
status = self._get(self.req_data, 'status')
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
owner_id = self._get(self.req_data, 'ownerId', 'owner_id', 'owner')
|
||||
if project_id:
|
||||
filters.append(TestPlan.project_id == int(project_id))
|
||||
if status not in (None, ''):
|
||||
filters.append(TestPlan.status == int(status))
|
||||
if keyword:
|
||||
filters.append(TestPlan.name.like('%{}%'.format(keyword)))
|
||||
if owner_id:
|
||||
filters.append(TestPlan.owner_id == int(owner_id))
|
||||
|
||||
items, total = PlanService.list_by_filters(self.session, TestPlan, filters, self._get(self.req_data, 'pageNo', 'page', default=1), self._get(self.req_data, 'pageSize', 'size', default=20), TestPlan.created_time)
|
||||
|
||||
owner_ids = [item.owner_id for item in items if item.owner_id]
|
||||
user_info_map = UserService.get_user_info_map(self.session, owner_ids) if owner_ids else {}
|
||||
|
||||
result_list = []
|
||||
for item in items:
|
||||
plan_dict = item.to_dict()
|
||||
if item.owner_id and item.owner_id in user_info_map:
|
||||
plan_dict['owner_name'] = user_info_map[item.owner_id].get('real_name', '')
|
||||
else:
|
||||
plan_dict['owner_name'] = ''
|
||||
result_list.append(plan_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def plan_detail(self):
|
||||
plan_id = self._get(self.req_data, 'planId', 'id')
|
||||
if not plan_id:
|
||||
return {}, 'planId 为必传参数'
|
||||
item = PlanService.get_by_id(self.session, TestPlan, plan_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应计划!'
|
||||
ret = self.serialize(item, ['is_delete'])
|
||||
ret.update(PlanService.plan_stats(self.session, plan_id))
|
||||
return ret, ''
|
||||
|
||||
def plan_create(self):
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not project_id or not name:
|
||||
return 0, 'projectId、name 为必传参数'
|
||||
add_info = {'project_id': project_id, 'name': name, 'version': self._get(self.req_data, 'version'), 'description': self._get(self.req_data, 'description'), 'start_date': self._get(self.req_data, 'startDate', 'start_time'), 'end_date': self._get(self.req_data, 'endDate', 'end_time'), 'owner_id': self._get(self.req_data, 'ownerId', 'owner_id'), 'status': int(self._get(self.req_data, 'status', default=0)), 'environment_id': self._get(self.req_data, 'environmentId', 'environment_id'), 'is_delete': 0}
|
||||
return PlanService.create(self.session, TestPlan, add_info)
|
||||
|
||||
def plan_update(self):
|
||||
"""更新测试计划,只更新请求中传入的字段。"""
|
||||
plan_id = self._get(self.req_data, 'planId', 'id')
|
||||
if not plan_id:
|
||||
return 0, 'planId 为必传参数'
|
||||
update_info = {}
|
||||
for req_keys, column_key in [(('name', 'name'), 'name'), (('version', 'version'), 'version'), (('description', 'description'), 'description'), (('startDate', 'start_time', 'start_date'), 'start_date'), (('endDate', 'end_time', 'end_date'), 'end_date'), (('ownerId', 'owner_id'), 'owner_id'), (('status', 'status'), 'status'), (('environmentId', 'environment_id'), 'environment_id')]:
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return PlanService.update_by_id(self.session, TestPlan, plan_id, update_info)
|
||||
|
||||
def plan_delete(self):
|
||||
plan_id = self._get(self.req_data, 'planId', 'id')
|
||||
if not plan_id:
|
||||
return 0, 'planId 为必传参数'
|
||||
return PlanService.delete_by_id(self.session, TestPlan, plan_id)
|
||||
|
||||
def round_create(self):
|
||||
plan_id = self._get(self.req_data, 'planId')
|
||||
round_no = self._get(self.req_data, 'roundNo')
|
||||
if not plan_id or not round_no:
|
||||
return 0, 'planId、roundNo 为必传参数'
|
||||
return PlanService.create(self.session, TestRound, {'plan_id': plan_id, 'round_no': round_no, 'name': self._get(self.req_data, 'name'), 'start_date': self._get(self.req_data, 'startDate'), 'end_date': self._get(self.req_data, 'endDate')})
|
||||
|
||||
def round_list(self):
|
||||
plan_id = self._get(self.req_data, 'planId')
|
||||
filters = [TestRound.plan_id == int(plan_id)] if plan_id else []
|
||||
items, total = PlanService.list_by_filters(self.session, TestRound, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=50), TestRound.id)
|
||||
return {'list': self.serialize_list(items), 'total': total}
|
||||
|
||||
def plan_case_add(self):
|
||||
plan_id = self._get(self.req_data, 'planId')
|
||||
case_ids = self._get(self.req_data, 'caseIds', default=[])
|
||||
if not plan_id or not case_ids:
|
||||
return 0, 'planId、caseIds 为必传参数'
|
||||
batch_info_list = [{'plan_id': plan_id, 'case_id': case_id, 'assignee_id': self._get(self.req_data, 'assigneeId'), 'round_no': int(self._get(self.req_data, 'roundNo', default=1)), 'status': 0} for case_id in case_ids]
|
||||
return PlanService.batch_create(self.session, PlanCase, batch_info_list)
|
||||
|
||||
def plan_case_list(self):
|
||||
plan_id = self._get(self.req_data, 'planId')
|
||||
filters = [PlanCase.plan_id == int(plan_id)] if plan_id else []
|
||||
round_no = self._get(self.req_data, 'roundNo')
|
||||
if round_no not in (None, ''):
|
||||
filters.append(PlanCase.round_no == int(round_no))
|
||||
items, total = PlanService.list_by_filters(self.session, PlanCase, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), PlanCase.id, asc=True)
|
||||
|
||||
case_ids = [item.case_id for item in items if item.case_id]
|
||||
case_info_map = {}
|
||||
module_info_map = {}
|
||||
if case_ids:
|
||||
cases = self.session.query(TestCase).filter(TestCase.id.in_(case_ids), TestCase.is_delete == 0).all()
|
||||
case_info_map = {case.id: {'case_key': case.case_key, 'title': case.title, 'module_id': case.module_id} for case in cases}
|
||||
|
||||
module_ids = [case.module_id for case in cases if case.module_id]
|
||||
if module_ids:
|
||||
modules = self.session.query(Module).filter(Module.id.in_(module_ids), Module.is_delete == 0).all()
|
||||
module_info_map = {module.id: module.name for module in modules}
|
||||
|
||||
result_list = []
|
||||
for item in items:
|
||||
case_dict = item.to_dict()
|
||||
if item.case_id and item.case_id in case_info_map:
|
||||
case_dict['case_key'] = case_info_map[item.case_id]['case_key']
|
||||
case_dict['case_title'] = case_info_map[item.case_id]['title']
|
||||
module_id = case_info_map[item.case_id].get('module_id')
|
||||
if module_id and module_id in module_info_map:
|
||||
case_dict['module_name'] = module_info_map[module_id]
|
||||
else:
|
||||
case_dict['module_name'] = ''
|
||||
else:
|
||||
case_dict['case_key'] = ''
|
||||
case_dict['case_title'] = ''
|
||||
case_dict['module_name'] = ''
|
||||
result_list.append(case_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def plan_case_execute(self):
|
||||
plan_case_id = self._get(self.req_data, 'planCaseId', 'id')
|
||||
if not plan_case_id:
|
||||
return 0, 'planCaseId 为必传参数'
|
||||
|
||||
plan_case = PlanService.get_by_id(self.session, PlanCase, plan_case_id, soft_delete=False)
|
||||
if not plan_case:
|
||||
return 0, '未查询到对应计划用例!'
|
||||
|
||||
plan_id = plan_case.plan_id
|
||||
|
||||
update_info = {'status': int(self._get(self.req_data, 'status', default=0)), 'actual_result': self._get(self.req_data, 'actualResult'), 'defect_links': self._get(self.req_data, 'defectLinks', default=[]), 'attachments': self._get(self.req_data, 'attachments', default=[]), 'executed_time': datetime.now(), 'execution_duration': self._get(self.req_data, 'executionDuration')}
|
||||
result = PlanService.update_by_id(self.session, PlanCase, plan_case_id, update_info, soft_delete=False)
|
||||
|
||||
self._update_plan_status(plan_id)
|
||||
|
||||
return result
|
||||
|
||||
def _update_plan_status(self, plan_id):
|
||||
total = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id).count()
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
unexecuted_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 0).count()
|
||||
passed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status == 1).count()
|
||||
failed_count = self.session.query(PlanCase).filter(PlanCase.plan_id == plan_id, PlanCase.status.in_([2, 3])).count()
|
||||
|
||||
plan = PlanService.get_by_id(self.session, TestPlan, plan_id)
|
||||
if not plan:
|
||||
return
|
||||
|
||||
if plan.status == 3:
|
||||
return
|
||||
|
||||
if unexecuted_count == 0:
|
||||
if failed_count == 0:
|
||||
new_status = 4
|
||||
else:
|
||||
new_status = 2
|
||||
elif unexecuted_count < total:
|
||||
new_status = 1
|
||||
else:
|
||||
new_status = plan.status
|
||||
|
||||
if new_status != plan.status:
|
||||
PlanService.update_by_id(self.session, TestPlan, plan_id, {'status': new_status})
|
||||
|
||||
def progress(self):
|
||||
"""查询计划进度统计。"""
|
||||
plan_id = self._get(self.req_data, 'planId', 'plan_id')
|
||||
if not plan_id:
|
||||
return {}, 'planId 为必传参数'
|
||||
return PlanService.plan_stats(self.session, plan_id), ''
|
||||
64
app/api/controller/productController.py
Normal file
64
app/api/controller/productController.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# encoding: UTF-8
|
||||
import random
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.productModel import Product
|
||||
from ..service.productService import ProductService
|
||||
|
||||
|
||||
class ProductController(BaseCrudController):
|
||||
"""产品相关接口控制器。"""
|
||||
|
||||
def product_list(self):
|
||||
filters = []
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
status = self._get(self.req_data, 'status')
|
||||
if keyword:
|
||||
filters.append(Product.name.like('%{}%'.format(keyword)))
|
||||
if status not in (None, ''):
|
||||
filters.append(Product.status == int(status))
|
||||
items, total = ProductService.list_by_filters(self.session, Product, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
Product.created_time)
|
||||
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
|
||||
|
||||
def product_detail(self):
|
||||
product_id = self._get(self.req_data, 'productId', 'id')
|
||||
if not product_id:
|
||||
return {}, 'productId 为必传参数'
|
||||
item = ProductService.get_by_id(self.session, Product, product_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应产品!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def product_create(self):
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not name:
|
||||
return 0, 'name 为必传参数'
|
||||
add_info = {
|
||||
'name': name,
|
||||
'code': str(random.randint(100000, 999999)),
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'is_delete': 0
|
||||
}
|
||||
return ProductService.create(self.session, Product, add_info)
|
||||
|
||||
def product_update(self):
|
||||
product_id = self._get(self.req_data, 'productId', 'id')
|
||||
if not product_id:
|
||||
return 0, 'productId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('name', 'name'), ('code', 'code'), ('description', 'description'),
|
||||
('status', 'status')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return ProductService.update_by_id(self.session, Product, product_id, update_info)
|
||||
|
||||
def product_delete(self):
|
||||
product_id = self._get(self.req_data, 'productId', 'id')
|
||||
if not product_id:
|
||||
return 0, 'productId 为必传参数'
|
||||
return ProductService.delete_by_id(self.session, Product, product_id)
|
||||
198
app/api/controller/projectController.py
Normal file
198
app/api/controller/projectController.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# encoding: UTF-8
|
||||
import random
|
||||
import string
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.projectModel import Environment, Project, ProjectMember
|
||||
from ..service.projectService import ProjectService
|
||||
from ..service.userService import UserService
|
||||
from ..dao.rbacDao import RbacDao
|
||||
|
||||
|
||||
class ProjectController(BaseCrudController):
|
||||
"""项目、项目成员、环境配置相关接口控制器。"""
|
||||
|
||||
def project_list(self):
|
||||
"""分页查询项目列表。"""
|
||||
page_num = self._get(self.req_data, 'pageNo', 'page', default=1)
|
||||
page_size = self._get(self.req_data, 'pageSize', 'size', default=20)
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
status = self._get(self.req_data, 'status')
|
||||
filter_list = []
|
||||
# 关键字先按项目名称模糊匹配。
|
||||
if keyword:
|
||||
filter_list.append(Project.name.like('%{}%'.format(keyword)))
|
||||
# 状态字段是枚举数字,查询时显式转 int。
|
||||
if status not in (None, ''):
|
||||
filter_list.append(Project.status == int(status))
|
||||
items, total = ProjectService.list_by_filters(self.session, Project, filter_list, page_num, page_size,
|
||||
Project.created_time)
|
||||
product_ids = list({item.product_id for item in items if item.product_id})
|
||||
product_map = ProjectService.get_product_map(self.session, product_ids)
|
||||
result_list = self.serialize_list(items, ['is_delete'])
|
||||
for item in result_list:
|
||||
item['product_name'] = product_map.get(item.get('product_id'), '')
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def project_detail(self):
|
||||
"""查询项目详情。"""
|
||||
project_id = self._get(self.req_data, 'projectId', 'id')
|
||||
if not project_id:
|
||||
return {}, 'projectId 为必传参数'
|
||||
item = ProjectService.get_by_id(self.session, Project, project_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应项目!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def project_create(self):
|
||||
"""创建项目。"""
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not name:
|
||||
return 0, 'name 为必传参数'
|
||||
add_info = {
|
||||
'key': ''.join(random.choices(string.ascii_letters + string.digits, k=6)),
|
||||
'name': name,
|
||||
'product_id': self._get(self.req_data, 'productId', 'product_id'),
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'department': self._get(self.req_data, 'department'),
|
||||
# 默认状态为启用。
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'config': self._get(self.req_data, 'config', default={}),
|
||||
'created_by': self._get(self.req_data, 'createdBy'),
|
||||
'is_delete': 0
|
||||
}
|
||||
return ProjectService.create(self.session, Project, add_info)
|
||||
|
||||
def project_update(self):
|
||||
"""更新项目。"""
|
||||
project_id = self._get(self.req_data, 'projectId', 'id')
|
||||
if not project_id:
|
||||
return 0, 'projectId 为必传参数'
|
||||
update_info = {}
|
||||
# 仅更新前端实际传入的字段,避免把未传字段覆盖为空。
|
||||
for req_key, column_key in [('key', 'key'), ('name', 'name'), ('productId', 'product_id'),
|
||||
('product_id', 'product_id'), ('description', 'description'),
|
||||
('department', 'department'), ('status', 'status'), ('config', 'config')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return ProjectService.update_by_id(self.session, Project, project_id, update_info)
|
||||
|
||||
def project_delete(self):
|
||||
"""软删除项目。"""
|
||||
project_id = self._get(self.req_data, 'projectId', 'id')
|
||||
if not project_id:
|
||||
return 0, 'projectId 为必传参数'
|
||||
return ProjectService.delete_by_id(self.session, Project, project_id)
|
||||
|
||||
def environment_list(self):
|
||||
"""按项目查询环境配置列表。"""
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
if not project_id:
|
||||
return {'list': [], 'total': 0}
|
||||
items, total = ProjectService.list_by_filters(self.session, Environment,
|
||||
[Environment.project_id == int(project_id)],
|
||||
self._get(self.req_data, 'pageNo', default=1),
|
||||
self._get(self.req_data, 'pageSize', default=20),
|
||||
Environment.created_time)
|
||||
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
|
||||
|
||||
def environment_create(self):
|
||||
"""新增环境配置。"""
|
||||
project_id = self._get(self.req_data, 'project_id')
|
||||
name = self._get(self.req_data, 'name')
|
||||
variables = self._get(self.req_data, 'variables')
|
||||
if not project_id or not name or variables is None:
|
||||
return 0, 'projectId、name、variables 为必传参数'
|
||||
return ProjectService.create(self.session, Environment, {
|
||||
'project_id': project_id,
|
||||
'name': name,
|
||||
'variables': variables,
|
||||
# 兼容是否加密开关。
|
||||
'is_encrypted': bool(self._get(self.req_data, 'isEncrypted', default=False)),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def environment_update(self):
|
||||
"""更新环境配置。"""
|
||||
env_id = self._get(self.req_data, 'environmentId', 'id')
|
||||
if not env_id:
|
||||
return 0, 'environmentId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('name', 'name'), ('variables', 'variables'), ('isEncrypted', 'is_encrypted')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return ProjectService.update_by_id(self.session, Environment, env_id, update_info)
|
||||
|
||||
def environment_delete(self):
|
||||
"""软删除环境配置。"""
|
||||
env_id = self._get(self.req_data, 'environmentId', 'id')
|
||||
if not env_id:
|
||||
return 0, 'environmentId 为必传参数'
|
||||
return ProjectService.delete_by_id(self.session, Environment, env_id)
|
||||
|
||||
def member_list(self):
|
||||
"""查询项目成员列表(带用户名、角色名称、项目名称)。"""
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
filters = [ProjectMember.project_id == int(project_id)] if project_id else []
|
||||
items, total = ProjectService.list_by_filters(self.session, ProjectMember, filters,
|
||||
self._get(self.req_data, 'pageNo', default=1),
|
||||
self._get(self.req_data, 'pageSize', default=20),
|
||||
ProjectMember.joined_time)
|
||||
result_list = self.serialize_list(items)
|
||||
if not result_list:
|
||||
return {'list': result_list, 'total': total}
|
||||
user_ids = [item.get('user_id') for item in result_list]
|
||||
project_ids = [item.get('project_id') for item in result_list]
|
||||
user_map = UserService.get_user_info_map(self.session, user_ids)
|
||||
project_map = ProjectService.get_project_name_map(self.session, project_ids)
|
||||
user_role_map = UserService.get_user_roles_map(self.session, user_ids)
|
||||
|
||||
for item in result_list:
|
||||
user_id = item.get('user_id')
|
||||
user_info = user_map.get(user_id, {})
|
||||
item['real_name'] = user_info.get('real_name', '')
|
||||
item['username'] = user_info.get('username', '')
|
||||
project_info = project_map.get(item.get('project_id'), {})
|
||||
item['project_name'] = project_info.get('name', '')
|
||||
|
||||
role_info = user_role_map.get(user_id, {})
|
||||
role_names = role_info.get('role_names', [])
|
||||
item['role_names'] = role_names
|
||||
item['role_name'] = ','.join(role_names) if role_names else ''
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def member_create(self):
|
||||
"""批量新增项目成员(根据用户系统角色自动映射项目成员角色)。"""
|
||||
project_id = self._get(self.req_data, 'project_id')
|
||||
user_ids = self._get(self.req_data, 'user_ids')
|
||||
if not project_id or not user_ids:
|
||||
return 0, 'project_id、user_ids 为必传参数'
|
||||
if not isinstance(user_ids, list):
|
||||
return 0, 'user_ids 必须为数组'
|
||||
if not user_ids:
|
||||
return 0, 'user_ids 不能为空'
|
||||
user_role_map = UserService.get_user_roles_map(self.session, user_ids)
|
||||
role_name_map = RbacDao.get_role_name_map(self.session)
|
||||
name_to_project_role = {name: role_id for role_id, name in role_name_map.items()}
|
||||
created_ids = []
|
||||
for user_id in user_ids:
|
||||
role_info = user_role_map.get(user_id, {})
|
||||
role_names = role_info.get('role_names', [])
|
||||
project_role = 0
|
||||
for role_name in role_names:
|
||||
if role_name in name_to_project_role:
|
||||
project_role = name_to_project_role[role_name]
|
||||
break
|
||||
if project_role == 0:
|
||||
return 0, f'用户 {user_id} 未分配有效角色,无法添加为项目成员'
|
||||
create_id, err_msg = ProjectService.create(self.session, ProjectMember, {
|
||||
'project_id': project_id,
|
||||
'user_id': user_id,
|
||||
'role': project_role
|
||||
})
|
||||
if err_msg:
|
||||
return 0, f'用户 {user_id} 添加失败:{err_msg}'
|
||||
created_ids.append(create_id)
|
||||
return created_ids[0] if len(created_ids) == 1 else created_ids, ''
|
||||
225
app/api/controller/projectHookController.py
Normal file
225
app/api/controller/projectHookController.py
Normal file
@@ -0,0 +1,225 @@
|
||||
# encoding: UTF-8
|
||||
import time
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.projectHookModel import ProjectHook
|
||||
from ..service.projectHookService import ProjectHookService
|
||||
|
||||
|
||||
class ProjectHookController(BaseCrudController):
|
||||
def hook_list(self):
|
||||
filters = []
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
|
||||
|
||||
if project_id:
|
||||
filters.append(ProjectHook.project_id == int(project_id))
|
||||
if hook_type not in (None, ''):
|
||||
filters.append(ProjectHook.hook_type == int(hook_type))
|
||||
|
||||
items, total = ProjectHookService.list_by_filters(
|
||||
self.session, ProjectHook, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
ProjectHook.created_time
|
||||
)
|
||||
|
||||
result_list = []
|
||||
hook_type_map = {1: '飞书', 2: '钉钉', 3: '企微'}
|
||||
for item in items:
|
||||
hook_dict = item.to_dict()
|
||||
hook_dict['hook_type_name'] = hook_type_map.get(item.hook_type, '')
|
||||
result_list.append(hook_dict)
|
||||
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def hook_detail(self):
|
||||
hook_id = self._get(self.req_data, 'hookId', 'id')
|
||||
if not hook_id:
|
||||
return {}, 'hookId 为必传参数'
|
||||
item = ProjectHookService.get_by_id(self.session, ProjectHook, hook_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应Hook配置!'
|
||||
ret = item.to_dict()
|
||||
hook_type_map = {1: '飞书', 2: '钉钉', 3: '企微'}
|
||||
ret['hook_type_name'] = hook_type_map.get(item.hook_type, '')
|
||||
return ret, ''
|
||||
|
||||
def hook_create(self):
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
|
||||
webhook_url = self._get(self.req_data, 'webhookUrl', 'webhook_url')
|
||||
|
||||
if not project_id:
|
||||
return 0, 'projectId 为必传参数'
|
||||
if not hook_type:
|
||||
return 0, 'hookType 为必传参数'
|
||||
if not webhook_url:
|
||||
return 0, 'webhookUrl 为必传参数'
|
||||
|
||||
add_info = {
|
||||
'project_id': project_id,
|
||||
'hook_type': int(hook_type),
|
||||
'webhook_url': webhook_url,
|
||||
'secret': self._get(self.req_data, 'secret'),
|
||||
'enabled': int(self._get(self.req_data, 'enabled', default=1)),
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'config': self._get(self.req_data, 'config', default={}),
|
||||
'is_delete': 0
|
||||
}
|
||||
return ProjectHookService.create(self.session, ProjectHook, add_info)
|
||||
|
||||
def hook_update(self):
|
||||
hook_id = self._get(self.req_data, 'hookId', 'id')
|
||||
if not hook_id:
|
||||
return 0, 'hookId 为必传参数'
|
||||
|
||||
update_info = {}
|
||||
field_mapping = [
|
||||
(('hookType', 'hook_type'), 'hook_type'),
|
||||
(('webhookUrl', 'webhook_url'), 'webhook_url'),
|
||||
(('secret',), 'secret'),
|
||||
(('enabled',), 'enabled'),
|
||||
(('description',), 'description'),
|
||||
(('config',), 'config')
|
||||
]
|
||||
|
||||
for req_keys, column_key in field_mapping:
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
|
||||
return ProjectHookService.update_by_id(self.session, ProjectHook, hook_id, update_info)
|
||||
|
||||
def hook_delete(self):
|
||||
hook_id = self._get(self.req_data, 'hookId', 'id')
|
||||
if not hook_id:
|
||||
return 0, 'hookId 为必传参数'
|
||||
return ProjectHookService.delete_by_id(self.session, ProjectHook, hook_id)
|
||||
|
||||
def hook_send(self):
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
title = self._get(self.req_data, 'title')
|
||||
content = self._get(self.req_data, 'content')
|
||||
hook_type = self._get(self.req_data, 'hookType', 'hook_type')
|
||||
hook_id = self._get(self.req_data, 'hookId', 'id')
|
||||
real_name = self._get(self.req_data, 'real_name', 'realName')
|
||||
|
||||
if not project_id:
|
||||
return 0, 'projectId 为必传参数'
|
||||
if not title:
|
||||
return 0, 'title 为必传参数'
|
||||
if not content:
|
||||
return 0, 'content 为必传参数'
|
||||
|
||||
at_prefix = f'@{real_name} ' if real_name else ''
|
||||
final_content = f'{at_prefix}{content}'
|
||||
|
||||
if hook_id:
|
||||
hook = ProjectHookService.get_by_id(self.session, ProjectHook, hook_id)
|
||||
if not hook or hook.is_delete == 1 or hook.enabled != 1:
|
||||
return 0, '未找到对应的Hook或Hook未启用'
|
||||
hooks = [hook]
|
||||
else:
|
||||
hooks = ProjectHookService.get_hooks_by_project(self.session, project_id, hook_type)
|
||||
if not hooks:
|
||||
return 0, '未配置对应的Hook'
|
||||
|
||||
results = []
|
||||
for hook in hooks:
|
||||
if hook.hook_type == 1:
|
||||
success, err_msg = self._send_feishu_message(hook.webhook_url, hook.secret, title, final_content)
|
||||
elif hook.hook_type == 2:
|
||||
success, err_msg = self._send_dingtalk_message(hook.webhook_url, hook.secret, title, final_content)
|
||||
elif hook.hook_type == 3:
|
||||
success, err_msg = self._send_wecom_message(hook.webhook_url, hook.secret, title, final_content)
|
||||
else:
|
||||
success, err_msg = False, '未知Hook类型'
|
||||
|
||||
results.append({
|
||||
'hook_id': hook.id,
|
||||
'hook_type': hook.hook_type,
|
||||
'success': success,
|
||||
'error': err_msg
|
||||
})
|
||||
|
||||
all_success = all(r['success'] for r in results)
|
||||
return 1 if all_success else 0, results
|
||||
|
||||
def _send_feishu_message(self, webhook_url, secret, title, content):
|
||||
timestamp = str(int(time.time()))
|
||||
sign = ''
|
||||
if secret:
|
||||
string_to_sign = f'{timestamp}\n{secret}'
|
||||
hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256).digest()
|
||||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||||
|
||||
separator = '&' if '?' in webhook_url else '?'
|
||||
url = f'{webhook_url}{separator}timestamp={timestamp}&sign={sign}' if sign else webhook_url
|
||||
|
||||
payload = {
|
||||
'msg_type': 'text',
|
||||
'content': {
|
||||
'text': f'【{title}】\n\n{content}'
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, timeout=10)
|
||||
result = response.json()
|
||||
if result.get('code') == 0:
|
||||
return True, ''
|
||||
else:
|
||||
return False, result.get('msg', '发送失败')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def _send_dingtalk_message(self, webhook_url, secret, title, content):
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
sign = ''
|
||||
if secret:
|
||||
string_to_sign = f'{timestamp}\n{secret}'
|
||||
hmac_code = hmac.new(secret.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha256).digest()
|
||||
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||||
|
||||
separator = '&' if '?' in webhook_url else '?'
|
||||
url = f'{webhook_url}{separator}timestamp={timestamp}&sign={sign}' if sign else webhook_url
|
||||
|
||||
payload = {
|
||||
'msgtype': 'text',
|
||||
'text': {
|
||||
'content': f'【{title}】\n\n{content}'
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, timeout=10)
|
||||
result = response.json()
|
||||
if result.get('errcode') == 0:
|
||||
return True, ''
|
||||
else:
|
||||
return False, result.get('errmsg', '发送失败')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def _send_wecom_message(self, webhook_url, secret, title, content):
|
||||
payload = {
|
||||
'msgtype': 'text',
|
||||
'text': {
|
||||
'content': f'【{title}】\n\n{content}'
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(webhook_url, json=payload, timeout=10)
|
||||
result = response.json()
|
||||
if result.get('errcode') == 0:
|
||||
return True, ''
|
||||
else:
|
||||
return False, result.get('errmsg', '发送失败')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
257
app/api/controller/rbacController.py
Normal file
257
app/api/controller/rbacController.py
Normal file
@@ -0,0 +1,257 @@
|
||||
# encoding: UTF-8
|
||||
from flask import g
|
||||
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.rbacModel import Role, Permission, Menu, RolePermission
|
||||
from ..service.rbacService import RbacService
|
||||
|
||||
|
||||
class RbacController(BaseCrudController):
|
||||
def role_list(self):
|
||||
filters = []
|
||||
status = self._get(self.req_data, 'status')
|
||||
if status not in (None, ''):
|
||||
filters.append(Menu.status == int(status))
|
||||
return RbacService.build_menu_tree(
|
||||
self.session,
|
||||
filters,
|
||||
role_ids=getattr(g, 'current_role_ids', [])
|
||||
)
|
||||
|
||||
def role_page_list(self):
|
||||
filters = []
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
status = self._get(self.req_data, 'status')
|
||||
if keyword:
|
||||
filters.append(Role.name.like('%{}%'.format(keyword)))
|
||||
if status not in (None, ''):
|
||||
filters.append(Role.status == int(status))
|
||||
items, total = RbacService.list_by_filters(self.session, Role, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
Role.created_time)
|
||||
return {'list': self.serialize_list(items, ['is_delete']), 'total': total}
|
||||
|
||||
def role_detail(self):
|
||||
role_id = self._get(self.req_data, 'roleId', 'id')
|
||||
if not role_id:
|
||||
return {}, 'roleId 为必传参数'
|
||||
item = RbacService.get_by_id(self.session, Role, role_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应角色!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def role_create(self):
|
||||
code = self._get(self.req_data, 'code')
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not code or not name:
|
||||
return 0, 'code、name 为必传参数'
|
||||
return RbacService.create(self.session, Role, {
|
||||
'code': code,
|
||||
'name': name,
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'is_system': int(self._get(self.req_data, 'isSystem', 'is_system', default=0)),
|
||||
'created_by': self._get(self.req_data, 'createdBy'),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def role_update(self):
|
||||
role_id = self._get(self.req_data, 'roleId', 'id')
|
||||
if not role_id:
|
||||
return 0, 'roleId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('code', 'code'), ('name', 'name'), ('description', 'description'),
|
||||
('status', 'status'), ('isSystem', 'is_system'), ('is_system', 'is_system')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return RbacService.update_by_id(self.session, Role, role_id, update_info)
|
||||
|
||||
def role_delete(self):
|
||||
role_id = self._get(self.req_data, 'roleId', 'id')
|
||||
if not role_id:
|
||||
return 0, 'roleId 为必传参数'
|
||||
return RbacService.delete_by_id(self.session, Role, role_id)
|
||||
|
||||
def permission_list(self):
|
||||
filters = []
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
module = self._get(self.req_data, 'module')
|
||||
status = self._get(self.req_data, 'status')
|
||||
if keyword:
|
||||
filters.append(Permission.name.like('%{}%'.format(keyword)))
|
||||
if module:
|
||||
filters.append(Permission.module == module)
|
||||
if status not in (None, ''):
|
||||
filters.append(Permission.status == int(status))
|
||||
items, total = RbacService.list_by_filters(self.session, Permission, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
Permission.created_time)
|
||||
role_permission_items = self.session.query(RolePermission).filter(RolePermission.is_delete == 0).all()
|
||||
permission_role_map = {}
|
||||
for rp in role_permission_items:
|
||||
if rp.permission_id not in permission_role_map:
|
||||
permission_role_map[rp.permission_id] = []
|
||||
permission_role_map[rp.permission_id].append(rp.role_id)
|
||||
role_items = self.session.query(Role).filter(Role.is_delete == 0).all()
|
||||
role_map = {r.id: {'id': r.id, 'name': r.name} for r in role_items}
|
||||
result_list = []
|
||||
for item in items:
|
||||
item_dict = self.serialize(item, ['is_delete'])
|
||||
role_ids = permission_role_map.get(item.id, [])
|
||||
item_dict['roles'] = [role_map.get(rid) for rid in role_ids if role_map.get(rid)]
|
||||
result_list.append(item_dict)
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def permission_detail(self):
|
||||
permission_id = self._get(self.req_data, 'permissionId', 'id')
|
||||
if not permission_id:
|
||||
return {}, 'permissionId 为必传参数'
|
||||
item = RbacService.get_by_id(self.session, Permission, permission_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应权限!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def permission_create(self):
|
||||
code = self._get(self.req_data, 'code')
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not code or not name:
|
||||
return 0, 'code、name 为必传参数'
|
||||
return RbacService.create(self.session, Permission, {
|
||||
'code': code,
|
||||
'name': name,
|
||||
'module': self._get(self.req_data, 'module'),
|
||||
'action': self._get(self.req_data, 'action'),
|
||||
'description': self._get(self.req_data, 'description'),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def permission_update(self):
|
||||
permission_id = self._get(self.req_data, 'permissionId', 'id')
|
||||
if not permission_id:
|
||||
return 0, 'permissionId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('code', 'code'), ('name', 'name'), ('module', 'module'), ('action', 'action'),
|
||||
('description', 'description'), ('status', 'status')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return RbacService.update_by_id(self.session, Permission, permission_id, update_info)
|
||||
|
||||
def permission_delete(self):
|
||||
permission_id = self._get(self.req_data, 'permissionId', 'id')
|
||||
if not permission_id:
|
||||
return 0, 'permissionId 为必传参数'
|
||||
return RbacService.delete_by_id(self.session, Permission, permission_id)
|
||||
|
||||
def menu_tree(self):
|
||||
return RbacService.build_menu_tree(self.session, [])
|
||||
|
||||
def current_menu_list(self):
|
||||
filters = []
|
||||
status = self._get(self.req_data, 'status')
|
||||
if status not in (None, ''):
|
||||
filters.append(Menu.status == int(status))
|
||||
return RbacService.build_menu_tree(
|
||||
self.session,
|
||||
filters,
|
||||
role_ids=getattr(g, 'current_role_ids', [])
|
||||
)
|
||||
|
||||
def role_menu_tree(self):
|
||||
role_id = self._get(self.req_data, 'roleId')
|
||||
if not role_id:
|
||||
return {'tree': [], 'checkedKeys': []}, 'roleId 为必传参数'
|
||||
return {
|
||||
'tree': RbacService.build_menu_tree(self.session, []),
|
||||
'checkedKeys': RbacService.get_role_menu_ids(self.session, role_id)
|
||||
}, ''
|
||||
|
||||
def menu_detail(self):
|
||||
menu_id = self._get(self.req_data, 'menuId', 'id')
|
||||
if not menu_id:
|
||||
return {}, 'menuId 为必传参数'
|
||||
item = RbacService.get_by_id(self.session, Menu, menu_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应菜单!'
|
||||
return self.serialize(item, ['is_delete']), ''
|
||||
|
||||
def menu_create(self):
|
||||
name = self._get(self.req_data, 'name')
|
||||
if not name:
|
||||
return 0, 'name 为必传参数'
|
||||
return RbacService.create(self.session, Menu, {
|
||||
'parent_id': int(self._get(self.req_data, 'parentId', 'parent_id', default=0)),
|
||||
'name': name,
|
||||
'code': self._get(self.req_data, 'code'),
|
||||
'type': int(self._get(self.req_data, 'type', default=1)),
|
||||
'path': self._get(self.req_data, 'path'),
|
||||
'component': self._get(self.req_data, 'component'),
|
||||
'icon': self._get(self.req_data, 'icon'),
|
||||
'permission_code': self._get(self.req_data, 'permissionCode', 'permission_code'),
|
||||
'sort': int(self._get(self.req_data, 'sort', default=0)),
|
||||
'visible': int(self._get(self.req_data, 'visible', default=1)),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def menu_update(self):
|
||||
menu_id = self._get(self.req_data, 'menuId', 'id')
|
||||
if not menu_id:
|
||||
return 0, 'menuId 为必传参数'
|
||||
update_info = {}
|
||||
field_pairs = [
|
||||
(('parentId', 'parent_id'), 'parent_id'),
|
||||
(('name',), 'name'),
|
||||
(('code',), 'code'),
|
||||
(('type',), 'type'),
|
||||
(('path',), 'path'),
|
||||
(('component',), 'component'),
|
||||
(('icon',), 'icon'),
|
||||
(('permissionCode', 'permission_code'), 'permission_code'),
|
||||
(('sort',), 'sort'),
|
||||
(('visible',), 'visible'),
|
||||
(('status',), 'status')
|
||||
]
|
||||
for req_keys, column_key in field_pairs:
|
||||
value = self._get(self.req_data, *req_keys)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return RbacService.update_by_id(self.session, Menu, menu_id, update_info)
|
||||
|
||||
def menu_delete(self):
|
||||
menu_id = self._get(self.req_data, 'menuId', 'id')
|
||||
if not menu_id:
|
||||
return 0, 'menuId 为必传参数'
|
||||
return RbacService.delete_by_id(self.session, Menu, menu_id)
|
||||
|
||||
def role_permission_list(self):
|
||||
role_id = self._get(self.req_data, 'roleId')
|
||||
if not role_id:
|
||||
return {'permissionIds': []}
|
||||
return {'permissionIds': RbacService.get_role_permission_ids(self.session, role_id)}
|
||||
|
||||
def role_permission_assign(self):
|
||||
role_ids = self._get(self.req_data, 'roleIds', default=[])
|
||||
permission_id = self._get(self.req_data, 'permissionId')
|
||||
if not role_ids:
|
||||
return 0, 'roleIds 为必传参数'
|
||||
if not permission_id:
|
||||
return 0, 'permissionId 为必传参数'
|
||||
return RbacService.assign_permissions(self.session, role_ids, permission_id)
|
||||
|
||||
def role_menu_list(self):
|
||||
role_id = self._get(self.req_data, 'roleId')
|
||||
if not role_id:
|
||||
return {'menuIds': []}
|
||||
return {'menuIds': RbacService.get_role_menu_ids(self.session, role_id)}
|
||||
|
||||
def role_menu_assign(self):
|
||||
role_id = self._get(self.req_data, 'roleId')
|
||||
menu_ids = self._get(self.req_data, 'menuIds', default=[])
|
||||
if not role_id:
|
||||
return 0, 'roleId 为必传参数'
|
||||
return RbacService.assign_menus(self.session, role_id, menu_ids)
|
||||
47
app/api/controller/reportController.py
Normal file
47
app/api/controller/reportController.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# encoding: UTF-8
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.planModel import TestPlan
|
||||
from ..model.reportModel import Report
|
||||
from ..service.reportService import ReportService
|
||||
|
||||
|
||||
class ReportController(BaseCrudController):
|
||||
"""测试报告相关接口控制器。"""
|
||||
|
||||
def report_list(self):
|
||||
"""分页查询报告列表,可按产品、项目、计划过滤。"""
|
||||
filters = []
|
||||
product_id = self._get(self.req_data, 'productId', 'product_id')
|
||||
if product_id:
|
||||
filters.append(Report.product_id == int(product_id))
|
||||
project_id = self._get(self.req_data, 'projectId', 'project_id')
|
||||
if project_id:
|
||||
filters.append(Report.project_id == int(project_id))
|
||||
plan_id = self._get(self.req_data, 'planId', 'plan_id')
|
||||
if plan_id:
|
||||
filters.append(Report.plan_id == int(plan_id))
|
||||
items, total = ReportService.list_by_filters(self.session, Report, filters, self._get(self.req_data, 'pageNo', default=1), self._get(self.req_data, 'pageSize', default=20), Report.generated_time)
|
||||
result_list = []
|
||||
for item in items:
|
||||
item_dict = self.serialize(item)
|
||||
plan = self.session.query(TestPlan).filter(TestPlan.id == item.plan_id).first()
|
||||
item_dict['plan_name'] = plan.name if plan else None
|
||||
result_list.append(item_dict)
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def report_detail(self):
|
||||
"""查询报告详情,返回 summary 和 HTML content。"""
|
||||
report_id = self._get(self.req_data, 'reportId', 'report_id', 'id')
|
||||
if not report_id:
|
||||
return {}, 'reportId 为必传参数'
|
||||
item = ReportService.get_by_id(self.session, Report, report_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应报告!'
|
||||
return self.serialize(item), ''
|
||||
|
||||
def report_generate(self):
|
||||
"""同步生成报告:聚合计划执行数据并落库。"""
|
||||
plan_id = self._get(self.req_data, 'planId', 'plan_id')
|
||||
if not plan_id:
|
||||
return 0, 'planId 为必传参数'
|
||||
return ReportService.generate_report(self.session, plan_id, self._get(self.req_data, 'generatedBy', 'generated_by'))
|
||||
128
app/api/controller/userController.py
Normal file
128
app/api/controller/userController.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# encoding: UTF-8
|
||||
from .baseCrudController import BaseCrudController
|
||||
from ..model.userModel import User
|
||||
from ..service.userService import UserService
|
||||
from ..utils.authMiddleware import TOKEN_REFRESH_THRESHOLD_SECONDS, create_token
|
||||
|
||||
|
||||
class UserController(BaseCrudController):
|
||||
def user_list(self):
|
||||
filters = []
|
||||
keyword = self._get(self.req_data, 'keyword')
|
||||
status = self._get(self.req_data, 'status')
|
||||
if keyword:
|
||||
filters.append(User.username.like('%{}%'.format(keyword)))
|
||||
if status not in (None, ''):
|
||||
filters.append(User.status == int(status))
|
||||
items, total = UserService.list_by_filters(self.session, User, filters,
|
||||
self._get(self.req_data, 'pageNo', 'page', default=1),
|
||||
self._get(self.req_data, 'pageSize', 'size', default=20),
|
||||
User.created_time)
|
||||
result_list = self.serialize_list(items, ['is_delete', 'password_hash'])
|
||||
role_map = UserService.get_user_roles_map(self.session, [item.id for item in items])
|
||||
for item in result_list:
|
||||
role_info = role_map.get(item.get('id'), {'role_ids': [], 'role_names': []})
|
||||
item['role_ids'] = role_info['role_ids']
|
||||
item['role_names'] = role_info['role_names']
|
||||
return {'list': result_list, 'total': total}
|
||||
|
||||
def user_detail(self):
|
||||
user_id = self._get(self.req_data, 'userId', 'id')
|
||||
if not user_id:
|
||||
return {}, 'userId 为必传参数'
|
||||
item = UserService.get_by_id(self.session, User, user_id)
|
||||
if not item:
|
||||
return {}, '未查询到对应用户!'
|
||||
ret = self.serialize(item, ['is_delete', 'password_hash'])
|
||||
ret['role_ids'] = UserService.get_user_role_ids(self.session, user_id)
|
||||
return ret, ''
|
||||
|
||||
def user_create(self):
|
||||
username = self._get(self.req_data, 'username')
|
||||
password = self._get(self.req_data, 'password')
|
||||
if not username or not password:
|
||||
return 0, 'username、password 为必传参数'
|
||||
return UserService.create(self.session, User, {
|
||||
'username': username,
|
||||
'real_name': self._get(self.req_data, 'realName', 'real_name'),
|
||||
'password_hash': password,
|
||||
'mobile': self._get(self.req_data, 'mobile'),
|
||||
'email': self._get(self.req_data, 'email'),
|
||||
'avatar': self._get(self.req_data, 'avatar'),
|
||||
'status': int(self._get(self.req_data, 'status', default=1)),
|
||||
'created_by': self._get(self.req_data, 'createdBy'),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def user_update(self):
|
||||
user_id = self._get(self.req_data, 'userId', 'id')
|
||||
if not user_id:
|
||||
return 0, 'userId 为必传参数'
|
||||
update_info = {}
|
||||
for req_key, column_key in [('username', 'username'), ('realName', 'real_name'), ('real_name', 'real_name'),
|
||||
('password', 'password_hash'), ('mobile', 'mobile'), ('email', 'email'),
|
||||
('avatar', 'avatar'), ('status', 'status')]:
|
||||
value = self._get(self.req_data, req_key)
|
||||
if value is not None:
|
||||
update_info[column_key] = value
|
||||
return UserService.update_by_id(self.session, User, user_id, update_info)
|
||||
|
||||
def user_delete(self):
|
||||
user_id = self._get(self.req_data, 'userId', 'id')
|
||||
if not user_id:
|
||||
return 0, 'userId 为必传参数'
|
||||
return UserService.delete_by_id(self.session, User, user_id)
|
||||
|
||||
def user_role_list(self):
|
||||
user_id = self._get(self.req_data, 'userId')
|
||||
if not user_id:
|
||||
return {'roleIds': []}
|
||||
return {'roleIds': UserService.get_user_role_ids(self.session, user_id)}
|
||||
|
||||
def user_role_assign(self):
|
||||
user_id = self._get(self.req_data, 'userId')
|
||||
role_ids = self._get(self.req_data, 'roleIds', default=[])
|
||||
if not user_id:
|
||||
return 0, 'userId 为必传参数'
|
||||
return UserService.assign_roles(self.session, user_id, role_ids)
|
||||
|
||||
def register(self):
|
||||
username = self._get(self.req_data, 'username')
|
||||
password = self._get(self.req_data, 'password')
|
||||
if not username or not password:
|
||||
return 0, 'username、password 为必传参数'
|
||||
exist_user = UserService.get_by_username(self.session, username)
|
||||
if exist_user:
|
||||
return 0, '用户名已存在!'
|
||||
return UserService.create(self.session, User, {
|
||||
'username': username,
|
||||
'real_name': self._get(self.req_data, 'realName', 'real_name'),
|
||||
'password_hash': password,
|
||||
'mobile': self._get(self.req_data, 'mobile'),
|
||||
'email': self._get(self.req_data, 'email'),
|
||||
'avatar': self._get(self.req_data, 'avatar'),
|
||||
'status': 1,
|
||||
'created_by': self._get(self.req_data, 'createdBy'),
|
||||
'is_delete': 0
|
||||
})
|
||||
|
||||
def login(self):
|
||||
username = self._get(self.req_data, 'username')
|
||||
password = self._get(self.req_data, 'password')
|
||||
if not username or not password:
|
||||
return {}, 'username、password 为必传参数'
|
||||
user = UserService.get_by_username(self.session, username)
|
||||
if not user or user.password_hash != password:
|
||||
return {}, '用户名或密码错误!'
|
||||
if int(user.status) != 1:
|
||||
return {}, '用户已禁用!'
|
||||
UserService.update_last_login_time(self.session, user.id)
|
||||
token, expire_seconds = create_token(user.id)
|
||||
ret = self.serialize(user, ['is_delete', 'password_hash'])
|
||||
ret['role_ids'] = UserService.get_user_role_ids(self.session, user.id)
|
||||
ret['token'] = token
|
||||
ret['token_type'] = 'Bearer'
|
||||
ret['expires_in'] = expire_seconds
|
||||
ret['refresh_threshold_seconds'] = TOKEN_REFRESH_THRESHOLD_SECONDS
|
||||
ret['refresh_mechanism'] = '请求任意已登录接口时,若token剩余有效期小于阈值则自动续期到完整有效期'
|
||||
return ret, ''
|
||||
BIN
app/api/dao/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/bugDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/bugDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/caseDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/caseDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/dataBuilderDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/dataBuilderDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/planDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/planDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/productDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/productDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/projectDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/projectDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/projectHookDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/projectHookDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/rbacDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/rbacDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/reportDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/reportDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/updateSqlProjectDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/updateSqlProjectDao.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/dao/__pycache__/userDao.cpython-38.pyc
Normal file
BIN
app/api/dao/__pycache__/userDao.cpython-38.pyc
Normal file
Binary file not shown.
224
app/api/dao/bugDao.py
Normal file
224
app/api/dao/bugDao.py
Normal file
@@ -0,0 +1,224 @@
|
||||
# encoding: UTF-8
|
||||
from sqlalchemy import func, cast, Date
|
||||
|
||||
from ..model.bugModel import Bug, BugComment, BugHistory
|
||||
from ..model.userModel import User
|
||||
from ..model.caseModel import Module
|
||||
from logger import logger
|
||||
|
||||
|
||||
class BugDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.asc() if asc else order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return BugDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def generate_bug_key(session):
|
||||
max_key = session.query(func.max(Bug.bug_key)).filter(Bug.bug_key.like('BUG-%')).scalar()
|
||||
if max_key:
|
||||
num = int(max_key.split('-')[1]) + 1
|
||||
else:
|
||||
num = 1
|
||||
return f'BUG-{num:03d}'
|
||||
|
||||
@staticmethod
|
||||
def get_comments(session, bug_id):
|
||||
return session.query(BugComment).filter(
|
||||
BugComment.bug_id == int(bug_id),
|
||||
BugComment.is_delete == 0
|
||||
).order_by(BugComment.created_time.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def get_history(session, bug_id):
|
||||
return session.query(BugHistory).filter(
|
||||
BugHistory.bug_id == int(bug_id)
|
||||
).order_by(BugHistory.created_time.desc()).all()
|
||||
|
||||
@staticmethod
|
||||
def add_history(session, bug_id, field_name, old_value, new_value, operator_id):
|
||||
session.add(BugHistory(
|
||||
bug_id=bug_id,
|
||||
field_name=field_name,
|
||||
old_value=str(old_value) if old_value else None,
|
||||
new_value=str(new_value) if new_value else None,
|
||||
operator_id=operator_id
|
||||
))
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'BugHistory新增失败!{err}')
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_stats(session, product_id=None, project_id=None):
|
||||
query = session.query(Bug).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
query = query.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
query = query.filter(Bug.project_id == int(project_id))
|
||||
|
||||
total = query.count()
|
||||
new_count = query.filter(Bug.status == 0).count()
|
||||
pending_count = query.filter(Bug.status == 1).count()
|
||||
in_progress_count = query.filter(Bug.status == 2).count()
|
||||
resolved_count = query.filter(Bug.status == 3).count()
|
||||
closed_count = query.filter(Bug.status == 4).count()
|
||||
rejected_count = query.filter(Bug.status == 5).count()
|
||||
|
||||
by_status = {}
|
||||
for status in range(6):
|
||||
by_status[str(status)] = query.filter(Bug.status == status).count()
|
||||
|
||||
by_solution = {}
|
||||
solution_results = session.query(
|
||||
Bug.solution, func.count(Bug.id)
|
||||
).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
solution_results = solution_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
solution_results = solution_results.filter(Bug.project_id == int(project_id))
|
||||
solution_results = solution_results.filter(Bug.solution.isnot(None)).group_by(Bug.solution).all()
|
||||
for solution, count in solution_results:
|
||||
by_solution[solution] = count
|
||||
|
||||
by_reporter = {}
|
||||
reporter_results = session.query(
|
||||
User.real_name, func.count(Bug.id)
|
||||
).join(User, Bug.reporter_id == User.id).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
reporter_results = reporter_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
reporter_results = reporter_results.filter(Bug.project_id == int(project_id))
|
||||
reporter_results = reporter_results.group_by(User.real_name).all()
|
||||
for name, count in reporter_results:
|
||||
by_reporter[name] = count
|
||||
|
||||
by_assignee = {}
|
||||
assignee_results = session.query(
|
||||
User.real_name, func.count(Bug.id)
|
||||
).outerjoin(User, Bug.assignee_id == User.id).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
assignee_results = assignee_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
assignee_results = assignee_results.filter(Bug.project_id == int(project_id))
|
||||
assignee_results = assignee_results.group_by(User.real_name).all()
|
||||
for name, count in assignee_results:
|
||||
by_assignee[name or '未指派'] = count
|
||||
|
||||
by_resolver = {}
|
||||
resolver_results = session.query(
|
||||
User.real_name, func.count(Bug.id)
|
||||
).outerjoin(User, Bug.resolved_by == User.id).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
resolver_results = resolver_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
resolver_results = resolver_results.filter(Bug.project_id == int(project_id))
|
||||
resolver_results = resolver_results.group_by(User.real_name).all()
|
||||
for name, count in resolver_results:
|
||||
by_resolver[name or '未解决'] = count
|
||||
|
||||
by_module = {}
|
||||
module_results = session.query(
|
||||
Module.name, func.count(Bug.id)
|
||||
).outerjoin(Module, Bug.module_id == Module.id).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
module_results = module_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
module_results = module_results.filter(Bug.project_id == int(project_id))
|
||||
module_results = module_results.group_by(Module.name).all()
|
||||
for name, count in module_results:
|
||||
by_module[name or '未分类'] = count
|
||||
|
||||
by_version = {}
|
||||
version_results = session.query(
|
||||
Bug.resolve_version, func.count(Bug.id)
|
||||
).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
version_results = version_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
version_results = version_results.filter(Bug.project_id == int(project_id))
|
||||
version_results = version_results.filter(Bug.resolve_version.isnot(None)).group_by(Bug.resolve_version).all()
|
||||
for version, count in version_results:
|
||||
by_version[version] = count
|
||||
|
||||
by_activation = {}
|
||||
|
||||
daily_new = {}
|
||||
daily_new_results = session.query(
|
||||
cast(Bug.created_time, Date).label('stat_date'),
|
||||
func.count(Bug.id)
|
||||
).filter(Bug.is_delete == 0)
|
||||
if product_id:
|
||||
daily_new_results = daily_new_results.filter(Bug.product_id == int(product_id))
|
||||
if project_id:
|
||||
daily_new_results = daily_new_results.filter(Bug.project_id == int(project_id))
|
||||
daily_new_results = daily_new_results.group_by('stat_date').order_by('stat_date').all()
|
||||
for date, count in daily_new_results:
|
||||
daily_new[str(date)] = count
|
||||
|
||||
daily_resolved = {}
|
||||
|
||||
daily_closed = {}
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'new': new_count,
|
||||
'pending': pending_count,
|
||||
'in_progress': in_progress_count,
|
||||
'resolved': resolved_count,
|
||||
'closed': closed_count,
|
||||
'rejected': rejected_count,
|
||||
'by_status': by_status,
|
||||
'by_solution': by_solution,
|
||||
'by_reporter': by_reporter,
|
||||
'by_assignee': by_assignee,
|
||||
'by_resolver': by_resolver,
|
||||
'by_module': by_module,
|
||||
'by_version': by_version,
|
||||
'by_activation': by_activation,
|
||||
'daily_new': daily_new,
|
||||
'daily_resolved': daily_resolved,
|
||||
'daily_closed': daily_closed
|
||||
}
|
||||
90
app/api/dao/caseDao.py
Normal file
90
app/api/dao/caseDao.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# encoding: UTF-8
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..model.caseModel import CaseReview, CaseSnapshot, Module, TestCase
|
||||
from logger import logger
|
||||
|
||||
|
||||
class CaseDao(object):
|
||||
"""用例域通用 DAO,复用模块、用例、快照、评审的基础操作。"""
|
||||
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
"""创建记录并提交事务。"""
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return CaseDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def next_case_key(session, project_id):
|
||||
count_num = session.query(func.count(TestCase.id)).filter(TestCase.project_id == int(project_id)).scalar() or 0
|
||||
return 'TC-{:03d}'.format(count_num + 1)
|
||||
|
||||
@staticmethod
|
||||
def next_snapshot_version(session, case_id):
|
||||
"""生成用例快照版本号。"""
|
||||
max_version = session.query(func.max(CaseSnapshot.version)).filter(CaseSnapshot.case_id == int(case_id)).scalar() or 0
|
||||
return int(max_version) + 1
|
||||
|
||||
@staticmethod
|
||||
def module_model():
|
||||
return Module
|
||||
|
||||
@staticmethod
|
||||
def case_model():
|
||||
return TestCase
|
||||
|
||||
@staticmethod
|
||||
def snapshot_model():
|
||||
return CaseSnapshot
|
||||
|
||||
@staticmethod
|
||||
def review_model():
|
||||
return CaseReview
|
||||
|
||||
@staticmethod
|
||||
def get_module_name_map(session, module_ids):
|
||||
if not module_ids:
|
||||
return {}
|
||||
module_items = session.query(Module).filter(Module.id.in_(module_ids), Module.is_delete == 0).all()
|
||||
return {module.id: module.name for module in module_items}
|
||||
59
app/api/dao/dataBuilderDao.py
Normal file
59
app/api/dao/dataBuilderDao.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.dataBuilderModel import DataBuilder, DataTask
|
||||
from logger import logger
|
||||
|
||||
|
||||
class DataBuilderDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return DataBuilderDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def builder_model():
|
||||
return DataBuilder
|
||||
|
||||
@staticmethod
|
||||
def task_model():
|
||||
return DataTask
|
||||
88
app/api/dao/planDao.py
Normal file
88
app/api/dao/planDao.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# encoding: UTF-8
|
||||
from sqlalchemy import func
|
||||
|
||||
from ..model.planModel import PlanCase, TestPlan, TestRound
|
||||
from logger import logger
|
||||
|
||||
|
||||
class PlanDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def batch_create(session, model_cls, batch_info_list):
|
||||
if not batch_info_list:
|
||||
return 0, ''
|
||||
objs = [model_cls(**info) for info in batch_info_list]
|
||||
session.add_all(objs)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}批量新增失败!{err}')
|
||||
return 0, f'批量新增失败!{err}'
|
||||
return len(objs), ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.asc() if asc else order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return PlanDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def plan_stats(session, plan_id):
|
||||
"""聚合计划执行进度,用于计划详情、进度看板和报告生成。"""
|
||||
total = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id)).scalar() or 0
|
||||
passed = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 1).scalar() or 0
|
||||
failed = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 2).scalar() or 0
|
||||
blocked = session.query(func.count(PlanCase.id)).filter(PlanCase.plan_id == int(plan_id), PlanCase.status == 3).scalar() or 0
|
||||
completed = passed + failed + blocked
|
||||
pass_rate = round(passed / total * 100, 2) if total else 0
|
||||
return {'total_cases': total, 'completed': completed, 'passed': passed, 'failed': failed, 'blocked': blocked, 'pass_rate': pass_rate}
|
||||
|
||||
@staticmethod
|
||||
def plan_model():
|
||||
return TestPlan
|
||||
|
||||
@staticmethod
|
||||
def plan_case_model():
|
||||
return PlanCase
|
||||
|
||||
@staticmethod
|
||||
def round_model():
|
||||
return TestRound
|
||||
55
app/api/dao/productDao.py
Normal file
55
app/api/dao/productDao.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.productModel import Product
|
||||
from logger import logger
|
||||
|
||||
|
||||
class ProductDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProductDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def product_model():
|
||||
return Product
|
||||
79
app/api/dao/projectDao.py
Normal file
79
app/api/dao/projectDao.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.productModel import Product
|
||||
from ..model.projectModel import Environment, Project, ProjectMember
|
||||
from logger import logger
|
||||
|
||||
|
||||
class ProjectDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
"""按过滤条件分页查询;存在 is_delete 字段时统一过滤未删除数据。"""
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProjectDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def get_product_map(session, product_ids):
|
||||
if not product_ids:
|
||||
return {}
|
||||
product_items = session.query(Product).filter(Product.id.in_(product_ids), Product.is_delete == 0).all()
|
||||
return {product.id: product.name for product in product_items}
|
||||
|
||||
@staticmethod
|
||||
def get_project_name_map(session, project_ids):
|
||||
if not project_ids:
|
||||
return {}
|
||||
project_items = session.query(Project).filter(Project.id.in_(project_ids), Project.is_delete == 0).all()
|
||||
return {project.id: {'name': project.name} for project in project_items}
|
||||
|
||||
@staticmethod
|
||||
def project_model():
|
||||
return Project
|
||||
|
||||
@staticmethod
|
||||
def member_model():
|
||||
return ProjectMember
|
||||
|
||||
@staticmethod
|
||||
def environment_model():
|
||||
return Environment
|
||||
62
app/api/dao/projectHookDao.py
Normal file
62
app/api/dao/projectHookDao.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.projectHookModel import ProjectHook
|
||||
from logger import logger
|
||||
|
||||
|
||||
class ProjectHookDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProjectHookDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def hook_model():
|
||||
return ProjectHook
|
||||
|
||||
@staticmethod
|
||||
def list_all_by_filters(session, model_cls, filter_list):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
return query.all()
|
||||
185
app/api/dao/rbacDao.py
Normal file
185
app/api/dao/rbacDao.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.rbacModel import Role, Permission, RolePermission, Menu, RoleMenu
|
||||
from logger import logger
|
||||
|
||||
|
||||
class RbacDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return RbacDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def get_role_permission_ids(session, role_id):
|
||||
items = session.query(RolePermission).filter(RolePermission.role_id == int(role_id), RolePermission.is_delete == 0).all()
|
||||
return [item.permission_id for item in items]
|
||||
|
||||
@staticmethod
|
||||
def replace_role_permissions(session, role_id, permission_ids):
|
||||
role_id = int(role_id)
|
||||
normalized_permission_ids = []
|
||||
for permission_id in permission_ids:
|
||||
permission_id = int(permission_id)
|
||||
if permission_id not in normalized_permission_ids:
|
||||
normalized_permission_ids.append(permission_id)
|
||||
session.query(RolePermission).filter(RolePermission.role_id == role_id, RolePermission.is_delete == 0).update({'is_delete': 1})
|
||||
session.flush()
|
||||
if normalized_permission_ids:
|
||||
existing_items = session.query(RolePermission).filter(
|
||||
RolePermission.role_id == role_id,
|
||||
RolePermission.permission_id.in_(normalized_permission_ids)
|
||||
).all()
|
||||
existing_map = {item.permission_id: item for item in existing_items}
|
||||
for permission_id in normalized_permission_ids:
|
||||
existing_item = existing_map.get(permission_id)
|
||||
if existing_item:
|
||||
existing_item.is_delete = 0
|
||||
else:
|
||||
session.add(RolePermission(role_id=role_id, permission_id=permission_id, is_delete=0))
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
return 0, f'分配权限失败!{err}'
|
||||
return role_id, ''
|
||||
|
||||
@staticmethod
|
||||
def assign_permissions_to_roles(session, role_ids, permission_id):
|
||||
permission_id = int(permission_id)
|
||||
normalized_role_ids = []
|
||||
for role_id in role_ids:
|
||||
role_id = int(role_id)
|
||||
if role_id not in normalized_role_ids:
|
||||
normalized_role_ids.append(role_id)
|
||||
existing_items = session.query(RolePermission).filter(
|
||||
RolePermission.role_id.in_(normalized_role_ids),
|
||||
RolePermission.permission_id == permission_id
|
||||
).all()
|
||||
existing_role_ids = {item.role_id for item in existing_items}
|
||||
for role_id in normalized_role_ids:
|
||||
if role_id not in existing_role_ids:
|
||||
session.add(RolePermission(role_id=role_id, permission_id=permission_id, is_delete=0))
|
||||
else:
|
||||
existing_item = next(item for item in existing_items if item.role_id == role_id)
|
||||
if existing_item.is_delete == 1:
|
||||
existing_item.is_delete = 0
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
return 0, f'分配权限失败!{err}'
|
||||
return len(normalized_role_ids), ''
|
||||
|
||||
@staticmethod
|
||||
def get_role_menu_ids(session, role_id):
|
||||
items = session.query(RoleMenu).filter(RoleMenu.role_id == int(role_id), RoleMenu.is_delete == 0).all()
|
||||
return [item.menu_id for item in items]
|
||||
|
||||
@staticmethod
|
||||
def replace_role_menus(session, role_id, menu_ids):
|
||||
role_id = int(role_id)
|
||||
normalized_menu_ids = []
|
||||
for menu_id in menu_ids:
|
||||
menu_id = int(menu_id)
|
||||
if menu_id not in normalized_menu_ids:
|
||||
normalized_menu_ids.append(menu_id)
|
||||
session.query(RoleMenu).filter(RoleMenu.role_id == role_id, RoleMenu.is_delete == 0).update({'is_delete': 1})
|
||||
if normalized_menu_ids:
|
||||
existing_items = session.query(RoleMenu).filter(
|
||||
RoleMenu.role_id == role_id,
|
||||
RoleMenu.menu_id.in_(normalized_menu_ids)
|
||||
).all()
|
||||
existing_map = {item.menu_id: item for item in existing_items}
|
||||
for menu_id in normalized_menu_ids:
|
||||
existing_item = existing_map.get(menu_id)
|
||||
if existing_item:
|
||||
existing_item.is_delete = 0
|
||||
else:
|
||||
session.add(RoleMenu(role_id=role_id, menu_id=menu_id, is_delete=0))
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
return 0, f'分配菜单失败!{err}'
|
||||
return role_id, ''
|
||||
|
||||
@staticmethod
|
||||
def get_role_names_map(session, role_ids):
|
||||
if not role_ids:
|
||||
return {}
|
||||
items = session.query(Role).filter(Role.id.in_(role_ids), Role.is_delete == 0).all()
|
||||
return {item.id: item.name for item in items}
|
||||
|
||||
@staticmethod
|
||||
def get_role_permission_codes(session, role_ids):
|
||||
if not role_ids:
|
||||
return []
|
||||
permission_items = session.query(Permission.code).join(
|
||||
RolePermission, RolePermission.permission_id == Permission.id
|
||||
).filter(
|
||||
RolePermission.role_id.in_(role_ids), RolePermission.is_delete == 0,
|
||||
Permission.is_delete == 0, Permission.status == 1
|
||||
).all()
|
||||
menu_items = session.query(Menu.permission_code).join(
|
||||
RoleMenu, RoleMenu.menu_id == Menu.id
|
||||
).filter(
|
||||
RoleMenu.role_id.in_(role_ids), RoleMenu.is_delete == 0,
|
||||
Menu.is_delete == 0, Menu.status == 1
|
||||
).all()
|
||||
return sorted(list({item[0] for item in permission_items if item[0]} | {item[0] for item in menu_items if item[0]}))
|
||||
|
||||
@staticmethod
|
||||
def get_menu_tree_items(session, filter_list):
|
||||
return session.query(Menu).filter(*filter_list, Menu.is_delete == 0).order_by(Menu.sort.asc(), Menu.id.asc()).all()
|
||||
|
||||
@staticmethod
|
||||
def role_model():
|
||||
return Role
|
||||
|
||||
@staticmethod
|
||||
def permission_model():
|
||||
return Permission
|
||||
|
||||
@staticmethod
|
||||
def menu_model():
|
||||
return Menu
|
||||
|
||||
@staticmethod
|
||||
def get_role_name_map(session):
|
||||
items = session.query(Role).filter(Role.is_delete == 0, Role.status == 1).all()
|
||||
return {item.id: item.name for item in items}
|
||||
36
app/api/dao/reportDao.py
Normal file
36
app/api/dao/reportDao.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# encoding: UTF-8
|
||||
from ..model.reportModel import DefectSync, Report
|
||||
from logger import logger
|
||||
|
||||
|
||||
class ReportDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id):
|
||||
return session.query(model_cls).filter(model_cls.id == int(obj_id)).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None, asc=False):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.asc() if asc else order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def report_model():
|
||||
return Report
|
||||
|
||||
@staticmethod
|
||||
def defect_model():
|
||||
return DefectSync
|
||||
@@ -43,15 +43,12 @@ class UpdateSqlProjectDao(object):
|
||||
|
||||
@staticmethod
|
||||
def get_sql_by_filters(session, filter_list, page=1, limit=20):
|
||||
rets = session.query(UpdateSqlProject)\
|
||||
.filter(*filter_list) \
|
||||
.filter(UpdateSqlProject.is_delete == 0) \
|
||||
.order_by(UpdateSqlProject.created_time.desc()) \
|
||||
query = session.query(UpdateSqlProject).filter(*filter_list).filter(UpdateSqlProject.is_delete == 0)
|
||||
total = query.count()
|
||||
rets = query.order_by(UpdateSqlProject.created_time.desc()) \
|
||||
.offset((int(page) - 1) * int(limit)) \
|
||||
.limit(limit) \
|
||||
.all()
|
||||
total = session.query(UpdateSqlProject).filter(*filter_list).filter(
|
||||
UpdateSqlProject.is_delete == 0).count()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
|
||||
109
app/api/dao/userDao.py
Normal file
109
app/api/dao/userDao.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# encoding: UTF-8
|
||||
from datetime import datetime
|
||||
|
||||
from ..model.userModel import User, UserRole
|
||||
from logger import logger
|
||||
|
||||
|
||||
class UserDao(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
obj = model_cls(**add_info)
|
||||
session.add(obj)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.warning(f'{model_cls.__name__}新增失败!{err}')
|
||||
return 0, f'新增失败!{err}'
|
||||
return obj.id, ''
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
update_res = session.query(model_cls).filter(*filters).update(update_info)
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
logger.error(f'{model_cls.__name__}更新失败!id: {obj_id}, err: {err}')
|
||||
return 0, f'更新失败!{err}'
|
||||
if not update_res:
|
||||
return 0, '未查询到对应记录!'
|
||||
return int(obj_id), ''
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
filters = [model_cls.id == int(obj_id)]
|
||||
if soft_delete and hasattr(model_cls, 'is_delete'):
|
||||
filters.append(model_cls.is_delete == 0)
|
||||
return session.query(model_cls).filter(*filters).first()
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None):
|
||||
query = session.query(model_cls).filter(*filter_list)
|
||||
if hasattr(model_cls, 'is_delete'):
|
||||
query = query.filter(model_cls.is_delete == 0)
|
||||
total = query.count()
|
||||
if order_column is not None:
|
||||
query = query.order_by(order_column.desc())
|
||||
rets = query.offset((int(page) - 1) * int(limit)).limit(int(limit)).all()
|
||||
return rets, total
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return UserDao.update_by_id(session, model_cls, obj_id, {'is_delete': 1})
|
||||
|
||||
@staticmethod
|
||||
def get_user_role_ids(session, user_id):
|
||||
items = session.query(UserRole).filter(UserRole.user_id == int(user_id), UserRole.is_delete == 0).all()
|
||||
return [item.role_id for item in items]
|
||||
|
||||
@staticmethod
|
||||
def replace_user_roles(session, user_id, role_ids):
|
||||
user_id = int(user_id)
|
||||
role_ids = [int(role_id) for role_id in role_ids]
|
||||
session.query(UserRole).filter(UserRole.user_id == user_id, UserRole.is_delete == 0).update({'is_delete': 1})
|
||||
existing_items = session.query(UserRole).filter(UserRole.user_id == user_id).all()
|
||||
existing_map = {item.role_id: item for item in existing_items}
|
||||
for role_id in role_ids:
|
||||
existing_item = existing_map.get(role_id)
|
||||
if existing_item:
|
||||
existing_item.is_delete = 0
|
||||
else:
|
||||
session.add(UserRole(user_id=user_id, role_id=role_id, is_delete=0))
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
return 0, f'分配角色失败!{err}'
|
||||
return user_id, ''
|
||||
|
||||
@staticmethod
|
||||
def get_user_roles(session, user_ids):
|
||||
if not user_ids:
|
||||
return {}
|
||||
items = session.query(UserRole).filter(UserRole.user_id.in_(user_ids), UserRole.is_delete == 0).all()
|
||||
ret = {}
|
||||
for item in items:
|
||||
ret.setdefault(item.user_id, []).append(item.role_id)
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def get_by_username(session, username):
|
||||
return session.query(User).filter(User.username == username, User.is_delete == 0).first()
|
||||
|
||||
@staticmethod
|
||||
def update_last_login_time(session, user_id):
|
||||
session.query(User).filter(User.id == int(user_id), User.is_delete == 0).update({'last_login_time': datetime.now()})
|
||||
err = session.done(close=False)
|
||||
if err:
|
||||
return 0, f'更新登录时间失败!{err}'
|
||||
return int(user_id), ''
|
||||
|
||||
@staticmethod
|
||||
def user_model():
|
||||
return User
|
||||
|
||||
@staticmethod
|
||||
def get_user_info_map(session, user_ids):
|
||||
if not user_ids:
|
||||
return {}
|
||||
items = session.query(User).filter(User.id.in_(user_ids), User.is_delete == 0).all()
|
||||
return {item.id: {'username': item.username, 'real_name': item.real_name} for item in items}
|
||||
BIN
app/api/model/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/bugModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/bugModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/caseModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/caseModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/planModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/planModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/productModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/productModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/projectHookModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/projectHookModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/projectModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/projectModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/rbacModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/rbacModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/reportModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/reportModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/model/__pycache__/userModel.cpython-38.pyc
Normal file
BIN
app/api/model/__pycache__/userModel.cpython-38.pyc
Normal file
Binary file not shown.
58
app/api/model/bugModel.py
Normal file
58
app/api/model/bugModel.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, Text, TIMESTAMP, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Bug(Base):
|
||||
__tablename__ = 'bug'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
bug_key = Column(String(64), nullable=False, unique=True)
|
||||
title = Column(String(256), nullable=False)
|
||||
description = Column(Text)
|
||||
bug_type = Column(SmallInteger, nullable=False, default=1)
|
||||
severity = Column(SmallInteger, nullable=False, default=2)
|
||||
priority = Column(SmallInteger, nullable=False, default=2)
|
||||
status = Column(SmallInteger, nullable=False, default=0)
|
||||
assignee_id = Column(BigInteger)
|
||||
reporter_id = Column(BigInteger, nullable=False)
|
||||
product_id = Column(BigInteger, nullable=False)
|
||||
project_id = Column(BigInteger, nullable=False)
|
||||
module_id = Column(BigInteger)
|
||||
case_id = Column(BigInteger)
|
||||
plan_id = Column(BigInteger)
|
||||
environment = Column(String(64))
|
||||
steps = Column(Text)
|
||||
solution = Column(Text)
|
||||
resolve_version = Column(String(64))
|
||||
resolved_by = Column(BigInteger)
|
||||
reproduce_rate = Column(SmallInteger)
|
||||
attachments = Column(JSONB, server_default=text("'[]'::jsonb"))
|
||||
is_delete = Column(Integer, default=0)
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True)
|
||||
|
||||
|
||||
class BugComment(Base):
|
||||
__tablename__ = 'bug_comment'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
bug_id = Column(BigInteger, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
is_delete = Column(Integer, default=0)
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)
|
||||
|
||||
|
||||
class BugHistory(Base):
|
||||
__tablename__ = 'bug_history'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
bug_id = Column(BigInteger, nullable=False)
|
||||
field_name = Column(String(64), nullable=False)
|
||||
old_value = Column(String(512))
|
||||
new_value = Column(String(512))
|
||||
operator_id = Column(BigInteger, nullable=False)
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True)
|
||||
62
app/api/model/caseModel.py
Normal file
62
app/api/model/caseModel.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Module(Base):
|
||||
__tablename__ = 'module'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
parent_id = Column(BigInteger, default=0, comment='父模块id')
|
||||
name = Column(String(128), nullable=False, comment='模块名称')
|
||||
sort_order = Column(Integer, default=0, comment='排序')
|
||||
path = Column(String(512), comment='模块路径')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
|
||||
|
||||
class TestCase(Base):
|
||||
__tablename__ = 'test_case'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
module_id = Column(BigInteger, comment='模块id')
|
||||
case_key = Column(String(64), nullable=False, comment='项目内唯一编号')
|
||||
title = Column(String(255), nullable=False, comment='标题')
|
||||
preconditions = Column(Text, comment='前置条件')
|
||||
steps = Column(Text, comment='步骤')
|
||||
expected_results = Column(Text, comment='预期结果')
|
||||
priority = Column(SmallInteger, default=2, comment='0:P0 1:P1 2:P2 3:P3')
|
||||
case_type = Column(SmallInteger, default=1, comment='1:功能 2:性能 3:安全 4:接口')
|
||||
tags = Column(ARRAY(String(64)), server_default=text("'{}'::varchar[]"), comment='标签')
|
||||
status = Column(SmallInteger, default=1, comment='1:正常 2:已废弃 3:评审中 4:评审通过')
|
||||
is_auto = Column(Integer, default=0, comment='0:未实现自动化;1:已实现自动化')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class CaseSnapshot(Base):
|
||||
__tablename__ = 'case_snapshot'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
case_id = Column(BigInteger, nullable=False, comment='用例id')
|
||||
version = Column(Integer, nullable=False, comment='版本')
|
||||
snapshot = Column(JSONB, nullable=False, comment='快照')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
|
||||
|
||||
class CaseReview(Base):
|
||||
__tablename__ = 'case_review'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
case_id = Column(BigInteger, nullable=False, comment='用例id')
|
||||
reviewer_id = Column(BigInteger, nullable=False, comment='评审人')
|
||||
status = Column(SmallInteger, default=0, comment='0:待评审 1:通过 2:驳回 3:建议修改')
|
||||
comments = Column(Text, comment='评论')
|
||||
diff_content = Column(Text, comment='JSON diff')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
reviewed_time = Column(TIMESTAMP, nullable=True, comment='评审时间')
|
||||
38
app/api/model/dataBuilderModel.py
Normal file
38
app/api/model/dataBuilderModel.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class DataBuilder(Base):
|
||||
__tablename__ = 'data_builder'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
name = Column(String(128), nullable=False, comment='造数器名称')
|
||||
description = Column(Text, comment='描述')
|
||||
builder_type = Column(SmallInteger, default=1, comment='1:流程编排 2:SQL 3:脚本')
|
||||
definition = Column(JSONB, nullable=False, comment='构造定义')
|
||||
input_schema = Column(JSONB, comment='输入定义')
|
||||
output_example = Column(JSONB, comment='输出示例')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class DataTask(Base):
|
||||
__tablename__ = 'data_task'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
builder_id = Column(BigInteger, nullable=False, comment='造数器id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
params = Column(JSONB, comment='任务参数')
|
||||
status = Column(SmallInteger, default=0, comment='0:等待 1:执行中 2:成功 3:失败')
|
||||
result_data = Column(JSONB, comment='生成数据')
|
||||
error_message = Column(Text, comment='错误信息')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
completed_time = Column(TIMESTAMP, nullable=True, comment='完成时间')
|
||||
50
app/api/model/planModel.py
Normal file
50
app/api/model/planModel.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from sqlalchemy import BigInteger, Column, Date, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class TestPlan(Base):
|
||||
__tablename__ = 'test_plan'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
name = Column(String(128), nullable=False, comment='计划名称')
|
||||
version = Column(String(32), comment='测试版本')
|
||||
description = Column(Text, comment='描述')
|
||||
start_date = Column(Date, comment='开始日期')
|
||||
end_date = Column(Date, comment='结束日期')
|
||||
owner_id = Column(BigInteger, comment='负责人')
|
||||
status = Column(SmallInteger, default=0, comment='0:草稿 1:进行中 2:已完成 3:已归档 4:已通过')
|
||||
environment_id = Column(BigInteger, comment='环境id')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class PlanCase(Base):
|
||||
__tablename__ = 'plan_case'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
plan_id = Column(BigInteger, nullable=False, comment='计划id')
|
||||
case_id = Column(BigInteger, nullable=False, comment='用例id')
|
||||
assignee_id = Column(BigInteger, comment='执行人')
|
||||
round_no = Column(Integer, default=1, comment='执行轮次')
|
||||
status = Column(SmallInteger, default=0, comment='0:未开始 1:通过 2:失败 3:阻塞')
|
||||
actual_result = Column(Text, comment='实际结果')
|
||||
defect_links = Column(JSONB, server_default=text("'[]'::jsonb"), comment='缺陷链接')
|
||||
attachments = Column(JSONB, server_default=text("'[]'::jsonb"), comment='附件')
|
||||
executed_time = Column(TIMESTAMP, comment='执行时间')
|
||||
execution_duration = Column(Integer, comment='执行耗时')
|
||||
|
||||
role_name_map = {1: '测试经理', 2: '测试工程师', 3: '开发工程师', 4: '访客'}
|
||||
class TestRound(Base):
|
||||
__tablename__ = 'test_round'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
plan_id = Column(BigInteger, nullable=False, comment='计划id')
|
||||
round_no = Column(Integer, nullable=False, comment='轮次')
|
||||
name = Column(String(64), comment='轮次名称')
|
||||
start_date = Column(Date, comment='开始日期')
|
||||
end_date = Column(Date, comment='结束日期')
|
||||
19
app/api/model/productModel.py
Normal file
19
app/api/model/productModel.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = 'product'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
name = Column(String(128), nullable=False, comment='产品名称')
|
||||
code = Column(String(64), unique=True, nullable=False, comment='产品编码')
|
||||
description = Column(Text, comment='产品描述')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
23
app/api/model/projectHookModel.py
Normal file
23
app/api/model/projectHookModel.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, Text, TIMESTAMP, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class ProjectHook(Base):
|
||||
__tablename__ = 'project_hook'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
hook_type = Column(SmallInteger, nullable=False, comment='1:飞书 2:钉钉 3:企微')
|
||||
webhook_url = Column(String(512), nullable=False, comment='webhook地址')
|
||||
secret = Column(String(256), comment='签名密钥')
|
||||
enabled = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
description = Column(String(256), comment='描述')
|
||||
config = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展配置')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
44
app/api/model/projectModel.py
Normal file
44
app/api/model/projectModel.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from sqlalchemy import BigInteger, Boolean, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = 'project'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
key = Column(String(32), unique=True, nullable=False, comment='项目唯一标识')
|
||||
name = Column(String(128), nullable=False, comment='项目名称')
|
||||
product_id = Column(Integer, comment='产品id')
|
||||
description = Column(Text, comment='项目描述')
|
||||
department = Column(String(64), comment='部门')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
config = Column(JSONB, server_default=text("'{}'::jsonb"), comment='扩展配置')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class ProjectMember(Base):
|
||||
__tablename__ = 'project_member'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
user_id = Column(BigInteger, nullable=False, comment='用户id')
|
||||
role = Column(SmallInteger, nullable=False, comment='1:测试经理 2:测试工程师 3:开发工程师 4:访客')
|
||||
joined_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='加入时间')
|
||||
|
||||
|
||||
class Environment(Base):
|
||||
__tablename__ = 'environment'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
name = Column(String(64), nullable=False, comment='环境名称,如 dev/st/pre/prod')
|
||||
variables = Column(JSONB, nullable=False, comment='环境变量')
|
||||
is_encrypted = Column(Boolean, default=False, comment='是否加密')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
72
app/api/model/rbacModel.py
Normal file
72
app/api/model/rbacModel.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'role'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
code = Column(String(64), unique=True, nullable=False, comment='角色编码')
|
||||
name = Column(String(64), nullable=False, comment='角色名称')
|
||||
description = Column(Text, comment='角色描述')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
is_system = Column(SmallInteger, default=0, comment='是否系统内置角色')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = 'permission'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
code = Column(String(128), unique=True, nullable=False, comment='权限编码')
|
||||
name = Column(String(128), nullable=False, comment='权限名称')
|
||||
module = Column(String(64), comment='所属模块')
|
||||
action = Column(String(64), comment='动作')
|
||||
description = Column(Text, comment='描述')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class RolePermission(Base):
|
||||
__tablename__ = 'role_permission'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
role_id = Column(BigInteger, nullable=False, comment='角色id')
|
||||
permission_id = Column(BigInteger, nullable=False, comment='权限id')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
|
||||
|
||||
class Menu(Base):
|
||||
__tablename__ = 'menu'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
parent_id = Column(BigInteger, default=0, comment='父菜单id')
|
||||
name = Column(String(64), nullable=False, comment='菜单名称')
|
||||
code = Column(String(64), unique=True, comment='菜单编码')
|
||||
type = Column(SmallInteger, default=1, comment='1:目录 2:菜单 3:按钮')
|
||||
path = Column(String(255), comment='路由路径')
|
||||
component = Column(String(255), comment='前端组件路径')
|
||||
icon = Column(String(64), comment='图标')
|
||||
permission_code = Column(String(128), comment='对应权限编码')
|
||||
sort = Column(Integer, default=0, comment='排序')
|
||||
visible = Column(SmallInteger, default=1, comment='1:显示 0:隐藏')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class RoleMenu(Base):
|
||||
__tablename__ = 'role_menu'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
role_id = Column(BigInteger, nullable=False, comment='角色id')
|
||||
menu_id = Column(BigInteger, nullable=False, comment='菜单id')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
34
app/api/model/reportModel.py
Normal file
34
app/api/model/reportModel.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from sqlalchemy import BigInteger, Column, SmallInteger, String, TIMESTAMP, Text, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class Report(Base):
|
||||
__tablename__ = 'report'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
plan_id = Column(BigInteger, nullable=False, comment='计划id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
product_id = Column(BigInteger, nullable=False, comment='产品id')
|
||||
name = Column(String(128), nullable=False, comment='报告名称')
|
||||
report_type = Column(SmallInteger, default=1, comment='1:实时报告 2:归档报告')
|
||||
summary = Column(JSONB, comment='统计数据')
|
||||
content = Column(Text, comment='HTML内容')
|
||||
file_url = Column(String(512), comment='文件地址')
|
||||
generated_by = Column(BigInteger, comment='生成人')
|
||||
generated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='生成时间')
|
||||
|
||||
|
||||
class DefectSync(Base):
|
||||
__tablename__ = 'defect_sync'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
project_id = Column(BigInteger, nullable=False, comment='项目id')
|
||||
external_id = Column(String(64), nullable=False, comment='外部缺陷id')
|
||||
external_system = Column(String(32), comment='外部系统')
|
||||
plan_case_id = Column(BigInteger, comment='计划用例id')
|
||||
status = Column(String(32), comment='外部状态')
|
||||
last_sync_time = Column(TIMESTAMP, comment='最后同步时间')
|
||||
@@ -25,6 +25,3 @@ class UpdateSqlProject(Base):
|
||||
nullable=True,
|
||||
comment='修改时间'
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return '<update_sql_project %r>' % self.id
|
||||
|
||||
33
app/api/model/userModel.py
Normal file
33
app/api/model/userModel.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from sqlalchemy import BigInteger, Column, Integer, SmallInteger, String, TIMESTAMP, text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
from common.sqlSession import to_dict
|
||||
|
||||
Base = declarative_base()
|
||||
Base.to_dict = to_dict
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
username = Column(String(64), unique=True, nullable=False, comment='登录用户名')
|
||||
real_name = Column(String(64), comment='真实姓名')
|
||||
password_hash = Column(String(255), nullable=False, comment='密码哈希')
|
||||
mobile = Column(String(32), comment='手机号')
|
||||
email = Column(String(128), comment='邮箱')
|
||||
avatar = Column(String(255), comment='头像地址')
|
||||
status = Column(SmallInteger, default=1, comment='1:启用 0:禁用')
|
||||
last_login_time = Column(TIMESTAMP, nullable=True, comment='最后登录时间')
|
||||
created_by = Column(BigInteger, comment='创建人')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
updated_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), server_onupdate=text('CURRENT_TIMESTAMP'), nullable=True, comment='修改时间')
|
||||
|
||||
|
||||
class UserRole(Base):
|
||||
__tablename__ = 'user_role'
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, comment='id')
|
||||
user_id = Column(BigInteger, nullable=False, comment='用户id')
|
||||
role_id = Column(BigInteger, nullable=False, comment='角色id')
|
||||
is_delete = Column(Integer, default=0, comment='0:未删除;1:已删除')
|
||||
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), nullable=True, comment='创建时间')
|
||||
BIN
app/api/service/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/bugService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/bugService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/caseService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/caseService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/dataBuilderService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/dataBuilderService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/planService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/planService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/productService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/productService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/projectHookService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/projectHookService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/projectService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/projectService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/rbacService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/rbacService.cpython-38.pyc
Normal file
Binary file not shown.
BIN
app/api/service/__pycache__/reportService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/reportService.cpython-38.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/api/service/__pycache__/userService.cpython-38.pyc
Normal file
BIN
app/api/service/__pycache__/userService.cpython-38.pyc
Normal file
Binary file not shown.
55
app/api/service/bugService.py
Normal file
55
app/api/service/bugService.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.bugDao import BugDao
|
||||
from ..model.bugModel import BugComment
|
||||
|
||||
|
||||
class BugService(object):
|
||||
"""Bug 管理 Service 层"""
|
||||
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return BugDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return BugDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return BugDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False):
|
||||
return BugDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return BugDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def get_comments(session, bug_id):
|
||||
return BugDao.get_comments(session, bug_id)
|
||||
|
||||
@staticmethod
|
||||
def get_history(session, bug_id):
|
||||
return BugDao.get_history(session, bug_id)
|
||||
|
||||
@staticmethod
|
||||
def add_comment(session, bug_id, content, user_id):
|
||||
return BugDao.create(session, BugComment, {
|
||||
'bug_id': bug_id,
|
||||
'content': content,
|
||||
'user_id': user_id
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def generate_bug_key(session):
|
||||
return BugDao.generate_bug_key(session)
|
||||
|
||||
@staticmethod
|
||||
def get_stats(session, product_id=None, project_id=None):
|
||||
return BugDao.get_stats(session, product_id, project_id)
|
||||
|
||||
@staticmethod
|
||||
def add_history(session, bug_id, field_name, old_value, new_value, operator_id):
|
||||
return BugDao.add_history(session, bug_id, field_name, old_value, new_value, operator_id)
|
||||
38
app/api/service/caseService.py
Normal file
38
app/api/service/caseService.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.caseDao import CaseDao
|
||||
|
||||
|
||||
class CaseService(object):
|
||||
"""用例域 Service 层,封装用例编号和快照版本等业务能力。"""
|
||||
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return CaseDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return CaseDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return CaseDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
|
||||
return CaseDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return CaseDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def next_case_key(session, project_id):
|
||||
return CaseDao.next_case_key(session, project_id)
|
||||
|
||||
@staticmethod
|
||||
def next_snapshot_version(session, case_id):
|
||||
return CaseDao.next_snapshot_version(session, case_id)
|
||||
|
||||
@staticmethod
|
||||
def get_module_name_map(session, module_ids):
|
||||
return CaseDao.get_module_name_map(session, module_ids)
|
||||
63
app/api/service/dataBuilderService.py
Normal file
63
app/api/service/dataBuilderService.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# encoding: UTF-8
|
||||
from datetime import datetime
|
||||
|
||||
from common.dataBuilderExecutor import DataBuilderExecutor
|
||||
from ..dao.dataBuilderDao import DataBuilderDao
|
||||
from ..model.dataBuilderModel import DataBuilder, DataTask
|
||||
|
||||
|
||||
class DataBuilderService(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return DataBuilderDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return DataBuilderDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return DataBuilderDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
|
||||
return DataBuilderDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return DataBuilderDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def execute_builder(session, builder_id, params=None, created_by=None):
|
||||
builder = DataBuilderDao.get_by_id(session, DataBuilder, builder_id)
|
||||
if not builder:
|
||||
return {}, '未查询到对应造数器!'
|
||||
params = params or {}
|
||||
task_info = {
|
||||
'builder_id': builder.id,
|
||||
'project_id': builder.project_id,
|
||||
'params': params,
|
||||
'status': 1,
|
||||
'created_by': created_by
|
||||
}
|
||||
# 先写入执行中任务,保证失败时也能追踪任务记录。
|
||||
task_id, err_msg = DataBuilderDao.create(session, DataTask, task_info)
|
||||
if err_msg:
|
||||
return {}, err_msg
|
||||
try:
|
||||
# 当前 MVP 只做同步模板渲染执行,后续可在 executor 内扩展 http/db step。
|
||||
executor = DataBuilderExecutor(builder.definition or {}, {})
|
||||
result_data = executor.execute(params)
|
||||
DataBuilderDao.update_by_id(session, DataTask, task_id, {
|
||||
'status': 2,
|
||||
'result_data': result_data,
|
||||
'completed_time': datetime.now()
|
||||
}, soft_delete=False)
|
||||
return {'taskId': task_id, 'data': result_data}, ''
|
||||
except Exception as e:
|
||||
DataBuilderDao.update_by_id(session, DataTask, task_id, {
|
||||
'status': 3,
|
||||
'error_message': str(e),
|
||||
'completed_time': datetime.now()
|
||||
}, soft_delete=False)
|
||||
return {}, f'执行造数失败!{e}'
|
||||
34
app/api/service/planService.py
Normal file
34
app/api/service/planService.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.planDao import PlanDao
|
||||
|
||||
|
||||
class PlanService(object):
|
||||
"""测试计划域 Service 层,封装计划统计等业务能力。"""
|
||||
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return PlanDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def batch_create(session, model_cls, batch_info_list):
|
||||
return PlanDao.batch_create(session, model_cls, batch_info_list)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return PlanDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return PlanDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False):
|
||||
return PlanDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return PlanDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def plan_stats(session, plan_id):
|
||||
return PlanDao.plan_stats(session, plan_id)
|
||||
24
app/api/service/productService.py
Normal file
24
app/api/service/productService.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.productDao import ProductDao
|
||||
|
||||
|
||||
class ProductService(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return ProductDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return ProductDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return ProductDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
|
||||
return ProductDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProductDao.delete_by_id(session, model_cls, obj_id)
|
||||
36
app/api/service/projectHookService.py
Normal file
36
app/api/service/projectHookService.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.projectHookDao import ProjectHookDao
|
||||
from ..model.projectHookModel import ProjectHook
|
||||
|
||||
|
||||
class ProjectHookService(object):
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return ProjectHookDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return ProjectHookDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return ProjectHookDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
|
||||
return ProjectHookDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProjectHookDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def get_hooks_by_project(session, project_id, hook_type=None):
|
||||
filters = [
|
||||
ProjectHook.project_id == int(project_id),
|
||||
ProjectHook.is_delete == 0,
|
||||
ProjectHook.enabled == 1
|
||||
]
|
||||
if hook_type not in (None, ''):
|
||||
filters.append(ProjectHook.hook_type == int(hook_type))
|
||||
return ProjectHookDao.list_all_by_filters(session, ProjectHook, filters)
|
||||
34
app/api/service/projectService.py
Normal file
34
app/api/service/projectService.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# encoding: UTF-8
|
||||
from ..dao.projectDao import ProjectDao
|
||||
|
||||
|
||||
class ProjectService(object):
|
||||
"""项目域 Service 层,保持业务入口与 DAO 解耦。"""
|
||||
|
||||
@staticmethod
|
||||
def create(session, model_cls, add_info):
|
||||
return ProjectDao.create(session, model_cls, add_info)
|
||||
|
||||
@staticmethod
|
||||
def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True):
|
||||
return ProjectDao.update_by_id(session, model_cls, obj_id, update_info, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(session, model_cls, obj_id, soft_delete=True):
|
||||
return ProjectDao.get_by_id(session, model_cls, obj_id, soft_delete)
|
||||
|
||||
@staticmethod
|
||||
def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None):
|
||||
return ProjectDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column)
|
||||
|
||||
@staticmethod
|
||||
def delete_by_id(session, model_cls, obj_id):
|
||||
return ProjectDao.delete_by_id(session, model_cls, obj_id)
|
||||
|
||||
@staticmethod
|
||||
def get_product_map(session, product_ids):
|
||||
return ProjectDao.get_product_map(session, product_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_project_name_map(session, project_ids):
|
||||
return ProjectDao.get_project_name_map(session, project_ids)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user