增加项目的各个功能

This commit is contained in:
qiaoxinjiu
2026-05-07 19:21:19 +08:00
parent aba1618f89
commit ee6cd4ae66
121 changed files with 9346 additions and 43 deletions

807
.agents/RBAC_API.md Normal file
View 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
View 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;

View 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 脚本沙箱执行
这些会影响架构和依赖,需要单独确认后再做。

View 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 | 父菜单ID0表示根节点 |
| 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
View File

@@ -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
"""

Binary file not shown.

Binary file not shown.

416
api_test_document.md Normal file
View 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 接口的稳定性、可靠性和安全性,为前端应用提供可靠的后端支持。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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]

View 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)

View 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')

View 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), ''

View 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), ''

View 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)

View 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, ''

View 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)

View 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)

View 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'))

View 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, ''

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

224
app/api/dao/bugDao.py Normal file
View 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
View 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}

View 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
View 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
View 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
View 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

View 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
View 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
View 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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

58
app/api/model/bugModel.py Normal file
View 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)

View 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='评审时间')

View 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='完成时间')

View 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='结束日期')

View 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='修改时间')

View 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='修改时间')

View 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='创建时间')

View 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='创建时间')

View 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='最后同步时间')

View File

@@ -25,6 +25,3 @@ class UpdateSqlProject(Base):
nullable=True,
comment='修改时间'
)
def __repr__(self):
return '<update_sql_project %r>' % self.id

View 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='创建时间')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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)

View 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)

View 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}'

View 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)

View 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)

View 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)

View 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