From ee6cd4ae668913da928f98a286fbc2929e54d83e Mon Sep 17 00:00:00 2001 From: qiaoxinjiu Date: Thu, 7 May 2026 19:21:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=A1=B9=E7=9B=AE=E7=9A=84?= =?UTF-8?q?=E5=90=84=E4=B8=AA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/RBAC_API.md | 807 ++++++++++ .agents/rbac_init.sql | 503 +++++++ .plan/3onvvJGzAx9Dhi05JkVpx.md | 212 +++ .plan/YCGiVLWod2rghU8nT3fEv.md | 856 +++++++++++ Jenkinsfile | 18 +- __pycache__/const.cpython-38.pyc | Bin 0 -> 1662 bytes __pycache__/logger.cpython-38.pyc | Bin 0 -> 1140 bytes api_test_document.md | 416 ++++++ app/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 685 bytes app/api/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 135 bytes app/api/__pycache__/views.cpython-38.pyc | Bin 0 -> 37160 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 146 bytes .../baseCrudController.cpython-38.pyc | Bin 0 -> 2340 bytes .../__pycache__/bugController.cpython-38.pyc | Bin 0 -> 9396 bytes .../__pycache__/caseController.cpython-38.pyc | Bin 0 -> 13001 bytes .../dataBuilderController.cpython-38.pyc | Bin 0 -> 3543 bytes .../__pycache__/planController.cpython-38.pyc | Bin 0 -> 8132 bytes .../productController.cpython-38.pyc | Bin 0 -> 2577 bytes .../projectController.cpython-38.pyc | Bin 0 -> 7609 bytes .../projectHookController.cpython-38.pyc | Bin 0 -> 6378 bytes .../__pycache__/rbacController.cpython-38.pyc | Bin 0 -> 9084 bytes .../reportController.cpython-38.pyc | Bin 0 -> 2221 bytes .../updateSqlProjectController.cpython-38.pyc | Bin 0 -> 6054 bytes .../__pycache__/userController.cpython-38.pyc | Bin 0 -> 4865 bytes app/api/controller/baseCrudController.py | 57 + app/api/controller/bugController.py | 325 ++++ app/api/controller/caseController.py | 413 ++++++ app/api/controller/dataBuilderController.py | 80 + app/api/controller/planController.py | 192 +++ app/api/controller/productController.py | 64 + app/api/controller/projectController.py | 198 +++ app/api/controller/projectHookController.py | 225 +++ app/api/controller/rbacController.py | 257 ++++ app/api/controller/reportController.py | 47 + app/api/controller/userController.py | 128 ++ .../dao/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 139 bytes app/api/dao/__pycache__/bugDao.cpython-38.pyc | Bin 0 -> 6155 bytes .../dao/__pycache__/caseDao.cpython-38.pyc | Bin 0 -> 3954 bytes .../__pycache__/dataBuilderDao.cpython-38.pyc | Bin 0 -> 2500 bytes .../dao/__pycache__/planDao.cpython-38.pyc | Bin 0 -> 3818 bytes .../dao/__pycache__/productDao.cpython-38.pyc | Bin 0 -> 2302 bytes .../dao/__pycache__/projectDao.cpython-38.pyc | Bin 0 -> 3664 bytes .../__pycache__/projectHookDao.cpython-38.pyc | Bin 0 -> 2580 bytes .../dao/__pycache__/rbacDao.cpython-38.pyc | Bin 0 -> 7680 bytes .../dao/__pycache__/reportDao.cpython-38.pyc | Bin 0 -> 1614 bytes .../updateSqlProjectDao.cpython-38.pyc | Bin 0 -> 2728 bytes .../dao/__pycache__/userDao.cpython-38.pyc | Bin 0 -> 4905 bytes app/api/dao/bugDao.py | 224 +++ app/api/dao/caseDao.py | 90 ++ app/api/dao/dataBuilderDao.py | 59 + app/api/dao/planDao.py | 88 ++ app/api/dao/productDao.py | 55 + app/api/dao/projectDao.py | 79 + app/api/dao/projectHookDao.py | 62 + app/api/dao/rbacDao.py | 185 +++ app/api/dao/reportDao.py | 36 + app/api/dao/updateSqlProjectDao.py | 9 +- app/api/dao/userDao.py | 109 ++ .../model/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 141 bytes .../model/__pycache__/bugModel.cpython-38.pyc | Bin 0 -> 2306 bytes .../__pycache__/caseModel.cpython-38.pyc | Bin 0 -> 3246 bytes .../dataBuilderModel.cpython-38.pyc | Bin 0 -> 2125 bytes .../__pycache__/planModel.cpython-38.pyc | Bin 0 -> 2795 bytes .../__pycache__/productModel.cpython-38.pyc | Bin 0 -> 1257 bytes .../projectHookModel.cpython-38.pyc | Bin 0 -> 1484 bytes .../__pycache__/projectModel.cpython-38.pyc | Bin 0 -> 2503 bytes .../__pycache__/rbacModel.cpython-38.pyc | Bin 0 -> 3554 bytes .../__pycache__/reportModel.cpython-38.pyc | Bin 0 -> 1840 bytes .../updateSqlProjectModel.cpython-38.pyc | Bin 0 -> 1263 bytes .../__pycache__/userModel.cpython-38.pyc | Bin 0 -> 1846 bytes app/api/model/bugModel.py | 58 + app/api/model/caseModel.py | 62 + app/api/model/dataBuilderModel.py | 38 + app/api/model/planModel.py | 50 + app/api/model/productModel.py | 19 + app/api/model/projectHookModel.py | 23 + app/api/model/projectModel.py | 44 + app/api/model/rbacModel.py | 72 + app/api/model/reportModel.py | 34 + app/api/model/updateSqlProjectModel.py | 3 - app/api/model/userModel.py | 33 + .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 143 bytes .../__pycache__/bugService.cpython-38.pyc | Bin 0 -> 2489 bytes .../__pycache__/caseService.cpython-38.pyc | Bin 0 -> 1954 bytes .../dataBuilderService.cpython-38.pyc | Bin 0 -> 2424 bytes .../__pycache__/planService.cpython-38.pyc | Bin 0 -> 1763 bytes .../__pycache__/productService.cpython-38.pyc | Bin 0 -> 1360 bytes .../projectHookService.cpython-38.pyc | Bin 0 -> 1818 bytes .../__pycache__/projectService.cpython-38.pyc | Bin 0 -> 1799 bytes .../__pycache__/rbacService.cpython-38.pyc | Bin 0 -> 3695 bytes .../__pycache__/reportService.cpython-38.pyc | Bin 0 -> 1912 bytes .../updateSqlProjectService.cpython-38.pyc | Bin 0 -> 1816 bytes .../__pycache__/userService.cpython-38.pyc | Bin 0 -> 2996 bytes app/api/service/bugService.py | 55 + app/api/service/caseService.py | 38 + app/api/service/dataBuilderService.py | 63 + app/api/service/planService.py | 34 + app/api/service/productService.py | 24 + app/api/service/projectHookService.py | 36 + app/api/service/projectService.py | 34 + app/api/service/rbacService.py | 103 ++ app/api/service/reportService.py | 46 + app/api/service/userService.py | 58 + .../utils/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 141 bytes .../__pycache__/authMiddleware.cpython-38.pyc | Bin 0 -> 4802 bytes app/api/utils/authMiddleware.py | 152 ++ app/api/views.py | 1312 ++++++++++++++++- attachment/用例导入模版.xlsx | Bin 0 -> 14899 bytes bug_api_document.md | 539 +++++++ common/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 134 bytes common/__pycache__/apiResponse.cpython-38.pyc | Bin 0 -> 1886 bytes common/__pycache__/cronRequest.cpython-38.pyc | Bin 0 -> 4284 bytes .../dataBuilderExecutor.cpython-38.pyc | Bin 0 -> 3019 bytes common/__pycache__/getRequest.cpython-38.pyc | Bin 0 -> 1233 bytes common/__pycache__/getUserInfo.cpython-38.pyc | Bin 0 -> 1078 bytes common/__pycache__/sqlSession.cpython-38.pyc | Bin 0 -> 3614 bytes common/dataBuilderExecutor.py | 57 + common/sqlSession.py | 47 +- const.py | 10 +- manage.py | 17 +- 数据库语句 | 534 +++++++ 121 files changed, 9346 insertions(+), 43 deletions(-) create mode 100644 .agents/RBAC_API.md create mode 100644 .agents/rbac_init.sql create mode 100644 .plan/3onvvJGzAx9Dhi05JkVpx.md create mode 100644 .plan/YCGiVLWod2rghU8nT3fEv.md create mode 100644 __pycache__/const.cpython-38.pyc create mode 100644 __pycache__/logger.cpython-38.pyc create mode 100644 api_test_document.md create mode 100644 app/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/__pycache__/views.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/baseCrudController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/bugController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/caseController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/dataBuilderController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/planController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/productController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/projectController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/projectHookController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/rbacController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/reportController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/updateSqlProjectController.cpython-38.pyc create mode 100644 app/api/controller/__pycache__/userController.cpython-38.pyc create mode 100644 app/api/controller/baseCrudController.py create mode 100644 app/api/controller/bugController.py create mode 100644 app/api/controller/caseController.py create mode 100644 app/api/controller/dataBuilderController.py create mode 100644 app/api/controller/planController.py create mode 100644 app/api/controller/productController.py create mode 100644 app/api/controller/projectController.py create mode 100644 app/api/controller/projectHookController.py create mode 100644 app/api/controller/rbacController.py create mode 100644 app/api/controller/reportController.py create mode 100644 app/api/controller/userController.py create mode 100644 app/api/dao/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/bugDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/caseDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/dataBuilderDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/planDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/productDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/projectDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/projectHookDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/rbacDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/reportDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/updateSqlProjectDao.cpython-38.pyc create mode 100644 app/api/dao/__pycache__/userDao.cpython-38.pyc create mode 100644 app/api/dao/bugDao.py create mode 100644 app/api/dao/caseDao.py create mode 100644 app/api/dao/dataBuilderDao.py create mode 100644 app/api/dao/planDao.py create mode 100644 app/api/dao/productDao.py create mode 100644 app/api/dao/projectDao.py create mode 100644 app/api/dao/projectHookDao.py create mode 100644 app/api/dao/rbacDao.py create mode 100644 app/api/dao/reportDao.py create mode 100644 app/api/dao/userDao.py create mode 100644 app/api/model/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/model/__pycache__/bugModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/caseModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/planModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/productModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/projectHookModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/projectModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/rbacModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/reportModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc create mode 100644 app/api/model/__pycache__/userModel.cpython-38.pyc create mode 100644 app/api/model/bugModel.py create mode 100644 app/api/model/caseModel.py create mode 100644 app/api/model/dataBuilderModel.py create mode 100644 app/api/model/planModel.py create mode 100644 app/api/model/productModel.py create mode 100644 app/api/model/projectHookModel.py create mode 100644 app/api/model/projectModel.py create mode 100644 app/api/model/rbacModel.py create mode 100644 app/api/model/reportModel.py create mode 100644 app/api/model/userModel.py create mode 100644 app/api/service/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/service/__pycache__/bugService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/caseService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/dataBuilderService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/planService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/productService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/projectHookService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/projectService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/rbacService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/reportService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/updateSqlProjectService.cpython-38.pyc create mode 100644 app/api/service/__pycache__/userService.cpython-38.pyc create mode 100644 app/api/service/bugService.py create mode 100644 app/api/service/caseService.py create mode 100644 app/api/service/dataBuilderService.py create mode 100644 app/api/service/planService.py create mode 100644 app/api/service/productService.py create mode 100644 app/api/service/projectHookService.py create mode 100644 app/api/service/projectService.py create mode 100644 app/api/service/rbacService.py create mode 100644 app/api/service/reportService.py create mode 100644 app/api/service/userService.py create mode 100644 app/api/utils/__pycache__/__init__.cpython-38.pyc create mode 100644 app/api/utils/__pycache__/authMiddleware.cpython-38.pyc create mode 100644 app/api/utils/authMiddleware.py create mode 100644 attachment/用例导入模版.xlsx create mode 100644 bug_api_document.md create mode 100644 common/__pycache__/__init__.cpython-38.pyc create mode 100644 common/__pycache__/apiResponse.cpython-38.pyc create mode 100644 common/__pycache__/cronRequest.cpython-38.pyc create mode 100644 common/__pycache__/dataBuilderExecutor.cpython-38.pyc create mode 100644 common/__pycache__/getRequest.cpython-38.pyc create mode 100644 common/__pycache__/getUserInfo.cpython-38.pyc create mode 100644 common/__pycache__/sqlSession.cpython-38.pyc create mode 100644 common/dataBuilderExecutor.py create mode 100644 数据库语句 diff --git a/.agents/RBAC_API.md b/.agents/RBAC_API.md new file mode 100644 index 0000000..74e1550 --- /dev/null +++ b/.agents/RBAC_API.md @@ -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、过期时间和刷新机制说明。 + +--- + +## 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: +``` + +或 + +```text +accesstoken: +``` + +或 + +```text +Authorization: Bearer +``` + +当前机制: +- 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. 初始化权限菜单角色的更完整种子数据 diff --git a/.agents/rbac_init.sql b/.agents/rbac_init.sql new file mode 100644 index 0000000..7e2b5de --- /dev/null +++ b/.agents/rbac_init.sql @@ -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; diff --git a/.plan/3onvvJGzAx9Dhi05JkVpx.md b/.plan/3onvvJGzAx9Dhi05JkVpx.md new file mode 100644 index 0000000..a127336 --- /dev/null +++ b/.plan/3onvvJGzAx9Dhi05JkVpx.md @@ -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 脚本沙箱执行 + +这些会影响架构和依赖,需要单独确认后再做。 \ No newline at end of file diff --git a/.plan/YCGiVLWod2rghU8nT3fEv.md b/.plan/YCGiVLWod2rghU8nT3fEv.md new file mode 100644 index 0000000..80d941f --- /dev/null +++ b/.plan/YCGiVLWod2rghU8nT3fEv.md @@ -0,0 +1,856 @@ +# 角色/用户/菜单管理详细设计文档 + +## 1. 目标 + +基于当前项目已有的分层框架(model / dao / service / controller / views)新增以下能力: + +1. 角色管理 + - 角色表 + - 权限表 + - 用户角色关联表 + - 角色权限关联能力 +2. 用户管理 + - 用户表 + - 用户与角色分配 +3. 菜单管理 + - 菜单表 + - 角色分配菜单 + +本次仅输出详细设计,不直接改业务代码。 + +--- + +## 2. 当前项目框架观察 + +### 2.1 现有分层风格 + +当前项目采用如下结构: + +- `app/api/model/*.py` +- `app/api/dao/*.py` +- `app/api/service/*.py` +- `app/api/controller/*.py` +- `app/api/views.py` + +### 2.2 当前职责边界 + +#### model +- 定义 SQLAlchemy 表模型 +- 每个领域通常独立一个 model 文件 + +#### dao +- 负责数据库增删改查 +- 已有通用风格: + - `create` + - `update_by_id` + - `get_by_id` + - `list_by_filters` + - `delete_by_id` + +#### service +- 作为 controller 与 dao 之间的业务入口 +- 目前多数是对 dao 的薄封装 +- 适合放跨表组合查询、业务校验、聚合逻辑 + +#### controller +- 负责参数获取、必填校验、字段映射、返回结构拼装 +- 不应直接 `session.query(...)` + +#### views +- Flask 路由层 +- 负责 request 参数读取、ApiResponse 包装、session 生命周期控制 + +### 2.3 当前接口命名风格 + +已有接口风格: +- `/project/list` +- `/project/detail` +- `/project/create` +- `/project/update` +- `/project/delete` +- `/product/list` +- `/product/detail` +- `/product/create` +- `/product/update` +- `/product/delete` + +建议新增模块保持一致: +- `/role/list` +- `/role/detail` +- `/role/create` +- `/role/update` +- `/role/delete` +- `/user/list` +- `/user/detail` +- `/user/create` +- `/user/update` +- `/user/delete` +- `/menu/tree` +- `/menu/create` +- `/menu/update` +- `/menu/delete` +- `/role/permission/*` +- `/role/menu/*` +- `/user/role/*` + +--- + +## 3. 总体设计原则 + +1. 严格按当前框架分层 +2. controller 不直接查数据库 +3. 跨表聚合通过 service + dao 实现 +4. 保持与现有字段风格一致: + - 主键 `id` + - 软删字段 `is_delete` + - 时间字段 `created_time` / `updated_time` +5. 优先支持后台管理能力,不先引入复杂 RBAC 继承体系 +6. 菜单、权限、角色、用户四个域解耦,但通过关联表关联 + +--- + +## 4. 业务对象与关系 + +### 4.1 核心对象 + +1. 用户 `user` +2. 角色 `role` +3. 权限 `permission` +4. 菜单 `menu` + +### 4.2 关联对象 + +1. 用户角色关联 `user_role` +2. 角色权限关联 `role_permission` +3. 角色菜单关联 `role_menu` + +### 4.3 关系说明 + +- 用户 : 角色 = 多对多 +- 角色 : 权限 = 多对多 +- 角色 : 菜单 = 多对多 +- 菜单自身 = 树形父子关系 + +### 4.4 推荐鉴权思路 + +后续登录后: +- 用户拥有多个角色 +- 角色聚合出权限集合 +- 角色聚合出菜单集合 +- 前端基于菜单渲染导航 +- 后端基于权限点做接口校验 + +--- + +## 5. 数据库表结构设计 + +以下采用 PostgreSQL 风格,保持与现有模型一致。 + +--- + +## 5.1 用户表 `user` + +### 用途 +存储后台系统用户基础信息。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| username | VARCHAR(64) | UNIQUE NOT NULL | 登录用户名 | +| real_name | VARCHAR(64) | | 真实姓名 | +| password_hash | VARCHAR(255) | NOT NULL | 密码哈希 | +| mobile | VARCHAR(32) | | 手机号 | +| email | VARCHAR(128) | | 邮箱 | +| avatar | VARCHAR(255) | | 头像地址 | +| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 | +| last_login_time | TIMESTAMP | | 最后登录时间 | +| created_by | BIGINT | | 创建人 | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +### 索引建议 +- unique(username) +- index(status) +- index(is_delete) + +--- + +## 5.2 角色表 `role` + +### 用途 +定义系统角色。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| code | VARCHAR(64) | UNIQUE NOT NULL | 角色编码,如 admin/test_manager | +| name | VARCHAR(64) | NOT NULL | 角色名称 | +| description | TEXT | | 角色描述 | +| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 | +| is_system | SMALLINT | DEFAULT 0 | 是否系统内置角色 | +| created_by | BIGINT | | 创建人 | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +### 索引建议 +- unique(code) +- index(status) +- index(is_delete) + +--- + +## 5.3 权限表 `permission` + +### 用途 +定义后端权限点。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| code | VARCHAR(128) | UNIQUE NOT NULL | 权限编码,如 project:create | +| name | VARCHAR(128) | NOT NULL | 权限名称 | +| module | VARCHAR(64) | | 所属模块,如 project/user/menu | +| action | VARCHAR(64) | | 动作,如 list/create/update/delete | +| description | TEXT | | 描述 | +| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +### 索引建议 +- unique(code) +- index(module) +- index(status) + +--- + +## 5.4 菜单表 `menu` + +### 用途 +定义前端菜单树。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| parent_id | BIGINT | DEFAULT 0 | 父菜单ID,0表示根节点 | +| name | VARCHAR(64) | NOT NULL | 菜单名称 | +| code | VARCHAR(64) | UNIQUE | 菜单编码 | +| type | SMALLINT | DEFAULT 1 | 1目录 2菜单 3按钮 | +| path | VARCHAR(255) | | 路由路径 | +| component | VARCHAR(255) | | 前端组件路径 | +| icon | VARCHAR(64) | | 图标 | +| permission_code | VARCHAR(128) | | 对应权限编码,可选 | +| sort | INTEGER | DEFAULT 0 | 排序 | +| visible | SMALLINT | DEFAULT 1 | 1显示 0隐藏 | +| status | SMALLINT | DEFAULT 1 | 1启用 0禁用 | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| updated_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +### 索引建议 +- index(parent_id) +- unique(code) +- index(sort) + +--- + +## 5.5 用户角色关联表 `user_role` + +### 用途 +维护用户与角色多对多关系。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| user_id | BIGINT | NOT NULL | 用户ID | +| role_id | BIGINT | NOT NULL | 角色ID | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +### 约束建议 +- unique(user_id, role_id) +- index(user_id) +- index(role_id) + +--- + +## 5.6 角色权限关联表 `role_permission` + +### 用途 +维护角色与权限多对多关系。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| role_id | BIGINT | NOT NULL | 角色ID | +| permission_id | BIGINT | NOT NULL | 权限ID | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +### 约束建议 +- unique(role_id, permission_id) +- index(role_id) +- index(permission_id) + +--- + +## 5.7 角色菜单关联表 `role_menu` + +### 用途 +维护角色与菜单多对多关系。 + +### 建表字段 + +| 字段 | 类型 | 约束 | 说明 | +|---|---|---|---| +| id | BIGSERIAL | PK | 主键 | +| role_id | BIGINT | NOT NULL | 角色ID | +| menu_id | BIGINT | NOT NULL | 菜单ID | +| is_delete | INTEGER | DEFAULT 0 | 软删标识 | +| created_time | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +### 约束建议 +- unique(role_id, menu_id) +- index(role_id) +- index(menu_id) + +--- + +## 6. Model 设计 + +建议新增单独 model 文件: + +### 6.1 `app/api/model/userModel.py` +包含: +- `User` +- `UserRole` + +### 6.2 `app/api/model/rbacModel.py` +包含: +- `Role` +- `Permission` +- `RolePermission` +- `Menu` +- `RoleMenu` + +> 也可以拆成 `roleModel.py` / `menuModel.py`,但按当前项目规模,两个文件足够。 + +### 6.3 Model 设计原则 +- 继承与现有一致:`declarative_base()` + `to_dict` +- 字段命名与数据库列一致 +- 关联关系不强制写 SQLAlchemy relationship,保持当前项目风格简洁 + +--- + +## 7. DAO 设计 + +--- + +## 7.1 `app/api/dao/userDao.py` + +### 基础方法 +- `create(session, model_cls, add_info)` +- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)` +- `get_by_id(session, model_cls, obj_id, soft_delete=True)` +- `list_by_filters(session, model_cls, filter_list, page=1, limit=20, order_column=None)` +- `delete_by_id(session, model_cls, obj_id)` + +### 用户专项方法 +- `get_user_role_ids(session, user_id)` +- `replace_user_roles(session, user_id, role_ids)` +- `get_user_roles(session, user_ids)` + +--- + +## 7.2 `app/api/dao/rbacDao.py` + +### 基础方法 +同现有通用 DAO 风格。 + +### 角色/权限/菜单专项方法 +- `get_role_permission_ids(session, role_id)` +- `replace_role_permissions(session, role_id, permission_ids)` +- `get_role_menu_ids(session, role_id)` +- `replace_role_menus(session, role_id, menu_ids)` +- `get_role_names_map(session, role_ids)` +- `get_menu_tree_items(session, filter_list)` +- `get_permission_list(session, filter_list, page, limit)` + +--- + +## 8. Service 设计 + +--- + +## 8.1 `app/api/service/userService.py` + +### 职责 +- 用户基本 CRUD +- 用户与角色分配 +- 聚合用户角色名称 + +### 方法建议 +- `create(session, model_cls, add_info)` +- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)` +- `get_by_id(session, model_cls, obj_id, soft_delete=True)` +- `list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None)` +- `delete_by_id(session, model_cls, obj_id)` +- `assign_roles(session, user_id, role_ids)` +- `get_user_role_ids(session, user_id)` +- `get_user_roles_map(session, user_ids)` + +--- + +## 8.2 `app/api/service/rbacService.py` + +### 职责 +- 角色 CRUD +- 权限 CRUD +- 菜单 CRUD +- 角色绑定权限 +- 角色绑定菜单 +- 菜单树构建 + +### 方法建议 +- `create(session, model_cls, add_info)` +- `update_by_id(session, model_cls, obj_id, update_info, soft_delete=True)` +- `get_by_id(session, model_cls, obj_id, soft_delete=True)` +- `list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None)` +- `delete_by_id(session, model_cls, obj_id)` +- `assign_permissions(session, role_id, permission_ids)` +- `assign_menus(session, role_id, menu_ids)` +- `get_role_permission_ids(session, role_id)` +- `get_role_menu_ids(session, role_id)` +- `build_menu_tree(session, filters)` + +--- + +## 9. Controller 设计 + +controller 仅负责: +- 参数获取 `_get(...)` +- 必填校验 +- 字段映射 +- 调用 service +- 序列化返回 + +不得直接 `session.query(...)` + +--- + +## 9.1 `app/api/controller/userController.py` + +### 建议方法 +- `user_list` +- `user_detail` +- `user_create` +- `user_update` +- `user_delete` +- `user_role_assign` +- `user_role_list` + +### 返回增强建议 +`user_list` 每项增加: +- `role_ids` +- `role_names` + +--- + +## 9.2 `app/api/controller/rbacController.py` + +### 角色相关 +- `role_list` +- `role_detail` +- `role_create` +- `role_update` +- `role_delete` +- `role_permission_assign` +- `role_permission_list` +- `role_menu_assign` +- `role_menu_list` + +### 权限相关 +- `permission_list` +- `permission_detail` +- `permission_create` +- `permission_update` +- `permission_delete` + +### 菜单相关 +- `menu_tree` +- `menu_detail` +- `menu_create` +- `menu_update` +- `menu_delete` + +--- + +## 10. 接口清单设计 + +以下接口路径保持当前项目风格。 + +--- + +## 10.1 角色管理接口 + +### 角色列表 +- `GET /role/list` +- 参数:`keyword`, `status`, `pageNo`, `pageSize` +- 返回:角色分页列表 + +### 角色详情 +- `GET /role/detail` +- 参数:`roleId` + +### 创建角色 +- `POST /role/create` +- 参数: + - `code` + - `name` + - `description` + - `status` + - `isSystem` + +### 更新角色 +- `POST /role/update` +- 参数: + - `roleId` + - 可选更新字段 + +### 删除角色 +- `POST /role/delete` +- 参数:`roleId` + +--- + +## 10.2 权限管理接口 + +### 权限列表 +- `GET /permission/list` +- 参数:`keyword`, `module`, `status`, `pageNo`, `pageSize` + +### 权限详情 +- `GET /permission/detail` +- 参数:`permissionId` + +### 创建权限 +- `POST /permission/create` + +### 更新权限 +- `POST /permission/update` + +### 删除权限 +- `POST /permission/delete` + +--- + +## 10.3 菜单管理接口 + +### 菜单树 +- `GET /menu/tree` +- 参数:可选 `status` +- 返回树形结构 + +### 菜单详情 +- `GET /menu/detail` +- 参数:`menuId` + +### 创建菜单 +- `POST /menu/create` + +### 更新菜单 +- `POST /menu/update` + +### 删除菜单 +- `POST /menu/delete` + +--- + +## 10.4 角色权限分配接口 + +### 查询角色权限 +- `GET /role/permission/list` +- 参数:`roleId` +- 返回:`permissionIds` + 权限明细列表 + +### 分配角色权限 +- `POST /role/permission/assign` +- 参数: + - `roleId` + - `permissionIds: []` + +建议语义:覆盖式保存 + +--- + +## 10.5 角色菜单分配接口 + +### 查询角色菜单 +- `GET /role/menu/list` +- 参数:`roleId` +- 返回:`menuIds` + 菜单树 + +### 分配角色菜单 +- `POST /role/menu/assign` +- 参数: + - `roleId` + - `menuIds: []` + +建议语义:覆盖式保存 + +--- + +## 10.6 用户管理接口 + +### 用户列表 +- `GET /user/list` +- 参数:`keyword`, `status`, `pageNo`, `pageSize` + +### 用户详情 +- `GET /user/detail` +- 参数:`userId` + +### 创建用户 +- `POST /user/create` +- 参数: + - `username` + - `realName` + - `password` + - `mobile` + - `email` + - `status` + +### 更新用户 +- `POST /user/update` +- 参数: + - `userId` + - 可选更新字段 + +### 删除用户 +- `POST /user/delete` +- 参数:`userId` + +--- + +## 10.7 用户角色分配接口 + +### 查询用户角色 +- `GET /user/role/list` +- 参数:`userId` +- 返回:`roleIds` + 角色列表 + +### 分配用户角色 +- `POST /user/role/assign` +- 参数: + - `userId` + - `roleIds: []` + +建议语义:覆盖式保存 + +--- + +## 11. 返回结构建议 + +### 11.1 角色列表项 + +```json +{ + "id": 1, + "code": "admin", + "name": "管理员", + "description": "系统管理员", + "status": 1, + "is_system": 1, + "created_time": "2025-01-01 10:00:00" +} +``` + +### 11.2 用户列表项 + +```json +{ + "id": 1, + "username": "zhangsan", + "real_name": "张三", + "mobile": "13800000000", + "email": "a@test.com", + "status": 1, + "role_ids": [1, 2], + "role_names": ["管理员", "测试经理"] +} +``` + +### 11.3 菜单树项 + +```json +{ + "id": 1, + "parent_id": 0, + "name": "系统管理", + "code": "system", + "type": 1, + "path": "/system", + "component": "Layout", + "icon": "setting", + "sort": 1, + "children": [] +} +``` + +--- + +## 12. 文件清单 + +本次功能落地预计新增/修改以下文件。 + +### 12.1 新增 model +- `app/api/model/userModel.py` +- `app/api/model/rbacModel.py` + +### 12.2 新增 dao +- `app/api/dao/userDao.py` +- `app/api/dao/rbacDao.py` + +### 12.3 新增 service +- `app/api/service/userService.py` +- `app/api/service/rbacService.py` + +### 12.4 新增 controller +- `app/api/controller/userController.py` +- `app/api/controller/rbacController.py` + +### 12.5 修改 views +- `app/api/views.py` + +### 12.6 可能新增 SQL 脚本(如果你需要) +- 数据库建表 SQL +- 初始化菜单/权限/角色种子数据 SQL + +--- + +## 13. 分层职责清单 + +### model +- 只定义表结构 + +### dao +- 只做数据库查询/写入 +- 负责关联表的增删查 + +### service +- 负责组合查询、覆盖式分配逻辑、聚合返回结构前的数据准备 + +### controller +- 参数校验、字段映射、调用 service、返回结构组装 + +### views +- 路由注册、request/response 包装 + +--- + +## 14. 推荐实现顺序 + +### 第一阶段:基础 RBAC 数据层 +1. 建表 SQL +2. model +3. dao +4. service + +### 第二阶段:角色/权限/菜单接口 +1. role CRUD +2. permission CRUD +3. menu CRUD +4. role-permission assign +5. role-menu assign + +### 第三阶段:用户接口 +1. user CRUD +2. user-role assign +3. user list/detail 聚合角色信息 + +### 第四阶段:初始化与联调 +1. 初始化菜单 +2. 初始化权限 +3. 初始化管理员角色 +4. 初始化管理员用户 + +--- + +## 15. 关键设计决策 + +### 15.1 分配类接口采用覆盖式保存 +原因: +- 前端实现简单 +- 后端一致性更好 +- 避免增删混合接口过多 + +### 15.2 菜单与权限分离 +原因: +- 菜单是导航资源 +- 权限是操作资源 +- 一些按钮权限不一定出现在菜单树中 + +### 15.3 用户表单独建设,不复用现有 project_member +原因: +- `project_member` 是项目成员关系,不是系统账户主表 +- 用户管理需要登录凭据、状态、手机号、邮箱等字段 + +### 15.4 先不引入复杂组织架构 +原因: +- 当前需求聚焦 RBAC + 用户 + 菜单 +- 可后续扩展部门/租户/产品维度数据权限 + +--- + +## 16. 后续可扩展项 + +1. 登录认证 + - JWT / Session +2. 密码重置 +3. 数据权限 + - 按产品/项目/部门的数据访问范围 +4. 操作审计日志 +5. 菜单与接口权限自动同步 +6. 超级管理员保护机制 + +--- + +## 17. 需要你确认的点 + +请你确认以下设计取舍: + +1. 用户表是否需要登录密码字段(默认:需要) +2. 权限是否按 `module:action` 编码(默认:是) +3. 菜单是否需要按钮类型 `type=3`(默认:需要) +4. 分配接口是否采用覆盖式保存(默认:是) +5. 是否需要初始化内置角色: + - 超级管理员 + - 测试经理 + - 测试工程师 +6. 用户是否允许多角色(默认:允许) +7. 菜单是否只做系统后台菜单,不含项目业务模块动态菜单(默认:只做后台菜单) + +--- + +## 18. 下一步建议 + +如果你确认这份设计,我下一步可以继续给你输出两部分: + +1. **完整建表 SQL** +2. **按当前项目风格拆好的代码落地清单** + - 每个文件该写哪些类、哪些方法、哪些接口 + diff --git a/Jenkinsfile b/Jenkinsfile index 79af542..63d5231 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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 """ diff --git a/__pycache__/const.cpython-38.pyc b/__pycache__/const.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea7ac1c7b6b48796ef0a123be02bbd10223319d6 GIT binary patch literal 1662 zcmZ`&+jA3D7|(7t?KVwH%UzM1Ry4wHcQ?sy(s3A48fdY!*t9BVnAzFnoHScCX`A5|xD~z$-)eH*6*qjl>AwxW1K%Y!oq~Z14OYPS z;C8qJa*&_!fKJ#0y70FWF;F;8fmKiht09$e1*Anhr)xMs+1?|if`!y#wq#<>)6~j7 zfp5OOVL=oClPye`r42Y>d~|63!CB|~>kHM}Pac2SsNHw2f98C90G{t$%XSb=YKl0@OZX9bALI%lzAy~{^zPQ{Zf20b2I+c%!DyhD34xU zfq-9+Ra`E;kHBsM{RH9!_7K=hV1U3Nfqeu%ATUH=KZf$=V!^Z~3}hb3ZZDcssX}o= z>h0-yZtcbAR!>i9x+n!hVY$41S1`(pioi=UFNWpbNKlr0EM!_`zbJ)xNfP*=#D~KA zZv!qDV4tTccSoWrC7cu$PE>*sE|gRtmz2XI7fQ)tEv(71q$R;BGp8Arn$z;y1j?bj zrJ7d2V>21tqslmefq7pTHH%-Dycyv?PphiXeo!kRqkI zND8I76sen%qhd- z+%}DSazpf~?Z&#%hBpbcOctgxrDVAq1?5l@Nl{J~B2g|Rivkx_V3-qxprT0;C@4Y@ zbS-(SM^Vz$@s-1~(l!GI4j#1K{X@X;;gp8InRnmF!GZBUK##16xQg#NUVqKX*sV}E@>&krtysh3#ptUcSi1!bU z+RS7@&x6*Xk-aKW+0l4H-96GD2c0;?IILMk6`wb?WaywP{z-iI_*h)+@53&`dj|Ic zc03VR$KtX5z?&Gup$S}m5VU=WeMa`j28RKAAeKmcJTlr3+D36b!VleY8EeUU5%_B> z>^)S@Oidj^=`=cQaeCfDMp{duL#aaEwD{tb&Ay*2z)}|NBqN)X7+A_j`8*80n(km| o>VMupTRJ=cWuG$a3%}*^@67ra3`IX>{-zr(+d5hpoS&us0hX{k`2YX_ literal 0 HcmV?d00001 diff --git a/__pycache__/logger.cpython-38.pyc b/__pycache__/logger.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cde0fbb82eb658d427e7803cb28c6d9b4d95c268 GIT binary patch literal 1140 zcmZuv&ubGw6rP#gO*WgPtx;188qybOkJ49W!q88h1V$UJF_??AGWi%$M)#qifKbdbmC& z-Xbi{@D~5!Elzni#so8_FNs_KiT@(|E>%khaSHrJE&~~7ovk>5ogm{06nIzNd%o>& z?7j#!d3G36Aa0@oL`&kaa}rTbIXkmG@+>5jr5DOLTDSzu#NEOvSa7?fY4fg6}KG_tTvB6Kywq z|6M;wCO4&D)tqJ%LX!A?F^3T*OG8^)obsPG8bZrwvl0(>msbLT5~tvb+qaijA}pfz z>$dMd5Uc= z@;Fm0>Z%4l`b$$z!1=VjRr94RAV@DE3%ja*6o@!{nrBhmnUTLNgqB~~4=43|L=lO5 hxY*RD4c0JPRfjgva;gk>M%nL*)u0Y_7~boh{sE~#FWCS9 literal 0 HcmV?d00001 diff --git a/api_test_document.md b/api_test_document.md new file mode 100644 index 0000000..bcfe64f --- /dev/null +++ b/api_test_document.md @@ -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 接口的稳定性、可靠性和安全性,为前端应用提供可靠的后端支持。 \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-38.pyc b/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d87475cbd4d4cf88f0d51548e435ccf3905055f9 GIT binary patch literal 685 zcmZWm&2AGh5VrT9Es080A&xn+ho(i`P*jn&LF%Cefw-(9SL+rKpYr5s#J-wJpRV>_j&w&Fh~)|==mP~i4po`jr-z(@e;P4f})6Gfohy% zY_TW2+RuH5eG$}Q9y%O|sE+aoa7d%CD359U4rR%2sDLuuhp$ATF9G<4tSSq4Wd2`Y|GmKTE{e5dJq%R z2UBFCZ!t~31MLk0Dmxcle8A1Ywtu=(f=IcEAwPmy^l-b`(mMA;-J|UzjYDP#uQf$dJa3UYXib=&ALfcY1x{jvHcI57g`k0*@ODlR)%i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Gcid9TiMrCD8 zYFb)qc8P9gUP)?ET4Hi)OkzO+5NF23$7kkcmc+;F6;$5hu*uC&Da}c>1DWy}h#3G= C<{r2J literal 0 HcmV?d00001 diff --git a/app/api/__pycache__/views.cpython-38.pyc b/app/api/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb2dbbff32dea0363925ac7809d95b803d1d9e9f GIT binary patch literal 37160 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 N%}KQb8Tc89835~_BVzyn literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/baseCrudController.cpython-38.pyc b/app/api/controller/__pycache__/baseCrudController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d12cd0eff3b796e6f574c3681888e23e1d56e970 GIT binary patch literal 2340 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 QzC#x|5mf15)-3M-U!BHi1ONa4 literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/caseController.cpython-38.pyc b/app/api/controller/__pycache__/caseController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5579f8e9478c2d9f5b7e71a6ddb56d76913fad7c GIT binary patch literal 13001 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 literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/productController.cpython-38.pyc b/app/api/controller/__pycache__/productController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a6ed771a3b2a661540f80637d99f650d484c2b5 GIT binary patch literal 2577 zcmaJ@-HRMW5by5k`PjYLjUF+?uW+D=4w9%3N{}OlL=Xj+KoDd(9VXko=j3KT($gEc za~8~(6)tU{{5CeH7mX?JnMI{&BcD2v>?i5J3}CCxQtpp)FQtln5^D#A@+6&vhrUTTb1f z?zZ;sCWvVP(6=;BZ77p{!X|2q2a>fJ9+H}EKv zp96{y*I~HQt00z1pJrr0GIE>>I%It|y<>1^WlSw*bilNAoDXba4V;V(DgG(REWuY? z_0ibwYA>_S&=s&#qDzA^#GKPs2Jw#<@$>Tu?<+o?Dd7om2IBieXNmQB--aDjGA{gO z*iUAQ$|VM;uhxvaB3D1_C}EsbN4l4;@!g;IcL%Iyh;h2nvaEFs$5A2R*QjxYY(~9A z8xLoX#%t2>6y(Nsu$xkxhC(J%%YYf4#Hlv6?&v59s)mQl65NzL5sFAh#)-OJ*%r!1 zN8XNF5GjdQq;Z-Z)rzz#p}~qFNf2VQ4a0@UR>NkTXsJ@;rZP?APTTkmB_l0GsAC9H zO{I#X1ojl7N8rE)Wzsb6*iU&7tq5+%TDDS!Tfaus3o!h1P`tIY_UY>Cl595Rirx#$ zOVx}TatXEzSG?4irtMO|J7j){>z1AeG4n^Zz`) zwrfiY?%N#VzGSl?5(1Vd+xwtQQ;P~Vv>@?l`dmze)dYey8(5GQT-zB18_QU_A8Z_L z;0I|ZY)lB@;7s7wOliFEd&00-{QWd6Fr8Vrck}k>w{!RI3`Q4EuYY@^_bgb~Fa5m! z!{tA(|2R5-V|4x2==L{7<@|N$l$r;-!bTIg>M7KBpvbu_K`J1Xu~5(BWS#@1DRF_N z7GTTmo7LIHss+fUnMaL}|WA5b` ze*(%3V<=xlc!xj)sfH3BZ~-MdWM^h2Zx0zDJ}FRWKTwQN!~zQQlHF(88Mt8ebP3Ad z0^H|nYA37`FokWUhuMhL@F3pt;7dy4w-0S2W=_boq2jKF?9VefuTE6~&pbw8=MfEW zbc9s^Dqy-0{KZbP|Z={$^< z!X2$C9*g7!z!`(lhazDAI_~4hDctWP2h#_yoSZr+GjgT{uu}l6G3hhG-yt9GfevW( zp+6QKIkSL78+ssFDXE3b&RE75$%%!{#R9}|AjYn#J+Oj$3B|r zp6Er%*y4tv=rqDm?S|-TFN!x%;F9V9idhNd8n!3pf2n-M6`yNoW@itrWyP@rNkm8>%VF*mq>M31fC>;3V#2Z5EUWF2JklO= W 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 literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/projectHookController.cpython-38.pyc b/app/api/controller/__pycache__/projectHookController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d155797bf8fca547358827a7d3be542ddae8f96 GIT binary patch literal 6378 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 literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/rbacController.cpython-38.pyc b/app/api/controller/__pycache__/rbacController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ada444225f4360ee543eb1e327a36aed3700d4c GIT binary patch literal 9084 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 KHkh-E?!N(~LNBBM literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/reportController.cpython-38.pyc b/app/api/controller/__pycache__/reportController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..426367244c7aa50dd1c94e7cc8dfcc8d7991f30e GIT binary patch literal 2221 zcmZ`)TW=Fr5Z<$QJ9ZKRRP=`YP_=bM^HixVLM;NMQmI50(3e%J)#7uUu32Alc3sKQ zS|VIpLj$B-RB=)9whhoqB}f$r(p2?^Eso{;*;Q)hN<=R&*Ij_1sonKSdv%-Jup z*|Y#p_R4enZ(R@`<6`Y2!(unQ?mi$?pu`c%#2`eVB9$DmEE!VF%Z^-D3?=3jM=fiH zMublUs#5KwKs82ANQO@JLqa~aLcmp?gkyUwm+j?sy0=>Kc-3(jUlEfn{V;RA&mF4* zYU)8&t8x#xY{DI4{A;_w3ZS~~LEGK%x^DwvfFpGpoAx9hVkjjsXjG@Ev(jLlrbD=fScU?1Qfw+s z6=K$WWuNfn{-zEwWqds#&Dc{r>kS8_J>m_QMt!}cVchLafzV9*Y5tF&E{#=0n(ud1=8=Y3v>lIeH`H9_ zx^}f<3{SBNDgr#rd-Bt%Yk4*WxCFIgX1KIYGN3Qke5=lIfNv~0S<+-%ba4U z^*qNhtR5dKw#UjY-;7R%ICCLiGtNlHjWC7Oq;|nGWX$?kfxii=&p)O+CuhEDG$vWG z$c}pNLQaeqtpb~beBrex3xnaENL{mtBy2TReTQ%{!L(fQ+@%XjOq;*r;XOHPei zccaCJ(bA=4RGux*h7cCV2IX6k#C?^5X2iAv|1YC3?!s4q%S$|mf;o0qc11^5P1xLj5Hm$wjacaARmNdwUv2X0QN2^U4OKjgkJgnII73ofi|7 z<3Y!{Sp)D$52?Q0JFxA-mlhyKaAh)+`#*5|CKGur)J+rT+%&_qX_n#kbCA!N=5*b1 zdOMnF(rUppIeq~74kY79RwIfztUK3$U=(pEI*~}VKw~RhV)&`XbQ9k*bt-jr1udX7>AdE>i~Yz Ezxb)CG5`Po literal 0 HcmV?d00001 diff --git a/app/api/controller/__pycache__/updateSqlProjectController.cpython-38.pyc b/app/api/controller/__pycache__/updateSqlProjectController.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e46e0da536a6537b559e194c18be6747c0fd98a7 GIT binary patch literal 6054 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 g= 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') diff --git a/app/api/controller/dataBuilderController.py b/app/api/controller/dataBuilderController.py new file mode 100644 index 0000000..bbafb75 --- /dev/null +++ b/app/api/controller/dataBuilderController.py @@ -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), '' diff --git a/app/api/controller/planController.py b/app/api/controller/planController.py new file mode 100644 index 0000000..a866284 --- /dev/null +++ b/app/api/controller/planController.py @@ -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), '' diff --git a/app/api/controller/productController.py b/app/api/controller/productController.py new file mode 100644 index 0000000..678e726 --- /dev/null +++ b/app/api/controller/productController.py @@ -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) diff --git a/app/api/controller/projectController.py b/app/api/controller/projectController.py new file mode 100644 index 0000000..06179f3 --- /dev/null +++ b/app/api/controller/projectController.py @@ -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, '' diff --git a/app/api/controller/projectHookController.py b/app/api/controller/projectHookController.py new file mode 100644 index 0000000..171d182 --- /dev/null +++ b/app/api/controller/projectHookController.py @@ -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) diff --git a/app/api/controller/rbacController.py b/app/api/controller/rbacController.py new file mode 100644 index 0000000..8c51c9f --- /dev/null +++ b/app/api/controller/rbacController.py @@ -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) diff --git a/app/api/controller/reportController.py b/app/api/controller/reportController.py new file mode 100644 index 0000000..3a7cea4 --- /dev/null +++ b/app/api/controller/reportController.py @@ -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')) diff --git a/app/api/controller/userController.py b/app/api/controller/userController.py new file mode 100644 index 0000000..48f4565 --- /dev/null +++ b/app/api/controller/userController.py @@ -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, '' diff --git a/app/api/dao/__pycache__/__init__.cpython-38.pyc b/app/api/dao/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9603d4a860d7ce791f02a989f388ece27783971 GIT binary patch literal 139 zcmWIL<>g`k0*@ODlR)%i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!GPid9TiMrCD8 zYFb)qc8P9gUP)?ET4Hi)OkzO+5NF1uB<9D&$7kkcmc+;F6;$5hu*uC&Da}c>1DW(0 Gh#3IhFCa_+ literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/bugDao.cpython-38.pyc b/app/api/dao/__pycache__/bugDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba8a5f03fc0e447e7efaae88d42b2c6df04831d8 GIT binary patch literal 6155 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 IY=xioAMG&^x&QzG literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/caseDao.cpython-38.pyc b/app/api/dao/__pycache__/caseDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ad399c6be91a96c33ba492e7114f155062e1cee GIT binary patch literal 3954 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 PcfElGD&%yAHk^L|**I6& literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/planDao.cpython-38.pyc b/app/api/dao/__pycache__/planDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2661a23f02e03f30a01c1718a0b1d833c3f9cb5 GIT binary patch literal 3818 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 Hww!+f3F<5W literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/projectDao.cpython-38.pyc b/app/api/dao/__pycache__/projectDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f67a2f95347034057a597af3ff1ec728bd9d5a5 GIT binary patch literal 3664 zcmcgv&5s;M74PcL`P%i`+1QR_U=b0dBN!qg5wMIw!7<_B5yApQP*S!?}dmQbK>`^=c8xr$T_8V>+P$q zdatV9@AqEyKDD$|((rsD{~P?TtZ5%oC;yn}{0J>3!8Oi8tFKFEMogJfI;La({E$r%W<1*>|OhmlrC9WaMk9U3*h^W){ zJBfO~>3fM>!UR%I`t8%c*o76D*N)5w@nvOIfWC*XD7^@MFW;y167+lcs?y8Q@8$cI zUV(mqA5{7hFX6n0WodkluaeoK#WsulhGMo zVB2PDiYF4iZ=`I~yo^YvkT0uQI9JxwTly(jZd%+T&9!SMt}$tOVHEqlJy>J(mm9-Z z|319>)~&Z+`}qCmgpGF+^Q6(_T$()U_`;!g&aa!I6LdCYq0#6x+kT@V^?+}v?6Ilr#GeH3vo>o`{C!)@9-n*TW8LnU-w%r|7`NDpp*Eb z)%5)JX19wLtn+5H-p!y!h^pQ5()NVkO#A}u<7EgDusgb_rdAeGuAkMjkoT>W4cMlgT9>rG zLqKfX0HQ$T1`tl_h-Z>wzXZQ!0AZy%q@6l|!y*q{=a_?D28SI#&kLuDeFX$q0S$-@hoPJJ**cITMBdJ$qA+=gA=Fo_ zcnrz{E6miGV}<#dtT2fc0}bhA)KtV&^brXPtT;r1kPwGSP(;O-NPHPWTCG6DiEGWG zCca98UnB7ViG@HEH~gfbqOgY1n7BQO!q#f;+|y~h3^R6DA!;7+YVr`frsd{obX1Wc zo+C3ZY1{TRBOXfaK0|bjEF-{uyKfFO@ZRFa*`jy?eG6G*4>Xk-cHc<~sjM`N!HcMb?IIyfnK0&gi9g|8w(|kN)}W*!0Tl!;Aghzg-=@cw_X!^$(x@%?yv4 zaIuP8o&`TcC8M|(^}zo^B)BgSUQ|9m_CVlPEA|tGp|rxF9V94>VdxfTc*%0c;YJw5 zi8Q-S+(zHVO0tqU^P;fV?x(;PgsxhegQpp&w=5Fm+&h6!lc*Aq~#Z&K-nW^okjflegb z)=^N63%DDP<0i)u%$mwLkC;ArXe~fE&91W~=lYKWWK z{w#NurtVatk=ld$aC;m^lMFtXTY7HN-PaVf1FTB8=75>4T4}5A_l8>Zr@2M*w|0Kc z=U>zKtTp%gvwtYEg-_-dKAb!4Z4+JgB>fud_7D%jtmZDOPaRomkgr}y-@=tf<6N&9 zj=zj6aneizuk9yiA}$Wnwy30v2T6RF1SO^TA&JLG94A2th!Z64fR-xERLx^@QeZkO z6?IlwLbK7z=o@IzRU;iuS=HQPeV+<<6=2f;SSRUAT~OhXvjL()HlX6DXroZ1Rn@l~ a<(j%5?yx^uIGP3H7`attvBFka#rQw@a)5#W literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/projectHookDao.cpython-38.pyc b/app/api/dao/__pycache__/projectHookDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0e1358a8e3b7e1214e50bc4185802d3b030008e GIT binary patch literal 2580 zcmZ`*&2Jk;6rb5|ubnidZD?r)q2i-PC45K}s1QPcN-wn#DVHdcwcS}Ko2{<|O zgM?BcF7$waOZjjrdMF24q+USqkIWUNYEIk{2YzqXu^m&!nWs0q@6F8qe!n-94`*h4 zhSt0EPu!_6_9qP{n}xw!=-F8q!30lOn=fiaTXkHY+7nr`gj5vz0MBb5jcxN~ z3;q$k7{>_9iKD_1?ll@Y7#$N;QPU$AqZu))vpo2R#GLjk@DGbewC@Wa8=Y6P6FYo2 zNJDdCOE!G!@1SQNz!bd23gbH8G7C0fg}DPT3bxK^k6-XDtFYw7+!&YzU$<^BVHU98 z=_u^G92qTR37PA*u*s`CjSIlZ4wE#CdWW#a{crB>-u!;|$6p@YyY<)apGty7Ax@cr z5Xus1Cz7-&<$e^%PTW~lUbEQ=+EKHqj9Aod*hwno3{B@r zl9!yLLS8xpR7ewnqxFoZTSAmCMGYvRkpt^13}nPCh{|pCogd3{}ts7?ss1Z)zkcE+c*K zBhf!T_3fk3J&ve7nyC>wlrd~10>VLK@G%^H=#bt~A=lZKGpdZ|3TME9kXcp+YQPy- zI}G}_1>2~|ix}Ie9%qN?3ULN*?iJ=X7v^yW&)Vik@n%pdeRw{yy}~P8T?5txa`Fsz zQm>9tD&Z^j7^Xdl)k~#_ByhlS6=fmR)@o%@uEADz61U?Vr&*HJD`Q|)wG>=4iL+c; z-2hj_B34qhaSZ!6q>#x*GSXV1!pWidI&VmtXaXa>u&!Hw4R>-=-w>}LW`D&KQ>cui z4gf++jOz1c45qYcPH59SpiQ$dNt;VdK8-Zg)~MXZLXt1gD{BI^7xAEsoPzF%rVV16 z;QVz2k7|!N@1LIfs)r=i`DOYuwZ3H?Q#7D*Iysf!B%FE)U9q)h^J*_hh8J!v%Y!@) z+flxj3P}f_CR{#C9>FTlk$IKOYh=c|A2FeILR!h_%<;Iv{ffbBGw2SwkFi;H9ofJQ sKd)(9?2hl`OLWsql4*JH(*ckFyGji{WKre4QGU{!B#?}#tnr%pFJUceGynhq literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/rbacDao.cpython-38.pyc b/app/api/dao/__pycache__/rbacDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..586e1e3661f8ecb7f5b8d40c9e374988534163da GIT binary patch literal 7680 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 literal 0 HcmV?d00001 diff --git a/app/api/dao/__pycache__/reportDao.cpython-38.pyc b/app/api/dao/__pycache__/reportDao.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14fc90657cd8b925d1976f84dfc62c6015616146 GIT binary patch literal 1614 zcma)6L66%+6rML@J9e7vZq>512#`QM5J6YvS|L=4svsnC=!#1e$#Om8t+S4ujweD{ zc@HRig(DI|MG1mKZy<5xm&_Fvu_x|5@!pu-EM*T&G|%tly*KZ@Z{C~F`~5D%)&KTI z{?udacM>ix{EywkUEe_>nS_E(VFXh?;=m-Aq7YLt62LxY(v$uPlfLqnV&uzUpA9;v z9Nh-ocJHf9rDlIorZ#w}W|cOls9^)JK~WtXD4mjcarr3R!(D%hM6nS_HsVT1ko<%# zVdNn%q~~}a`E41=?g^kKKsZ(nM{+oGKniImopRjD*3WjkLay3ET1 z8^&>&OjR6Pp38x6JGH9oyee%pt)wdAw5V-}&T(F5l@-;ai4{ugXG~wib6&hJ?~aci zFBW5!W$Mt}&P$_omZWN&%x1Xru}rG5c2|T5?adZ8NVQ6g>Y#hQg@o}gbfE_x?7|it z?VQ88*Bbr{RYLe2)i+UaSi%}sfVgoZ8oq=>jf4O?hYn&<0pxoJ&_-ORm%3(Sb7YqXlZNqF7TOGJbSzlk2(u6i{_aPQwQ& z6?wL3GR{+fTOJYWOWl@`yq$ZKQ0S%PDCi|l*tiD^~Lr=@<20#a1#o601G z^|Uhe%lK~7Ap~%HX8j5})IB7O??8yZ*WevEy1BvoCHw!(JKcgCJsJUnt)O?28Gxn^ zx)7bZSi39kwOty!zNgkYyXD_ddg5=c CG>QG~ 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 H48#loD6JtY literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/bugModel.cpython-38.pyc b/app/api/model/__pycache__/bugModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..901f1273bddd989ac08593e9d87f266464bdb9eb GIT binary patch literal 2306 zcmb7FOLN>r5SFyNl2+0_>?DM|A3Tyh;6sXn!lWQLl|WKuL&eTTm=f~Nc&$V)k4A>g z)g1AOU*SK%kC`hc{{@NzUypX}O#vygs_FT@o>x!zbno+C&ol7*t@Bs%d)F}jAhP&t zgFHa1euZELGgBioLo>5ND{F-&j(TeYlC%|{fohDJ~(WJLvV+=;I8IN;9cf{dzvpZf72KRC$PvE zxyoBhCQow7CtN7|VUgBZuAI3d*v#TIoilq=iX@+?_7?wHs_xd4^+%gqU#)+q+7gP9 zsX9+Lzui~^7&1OiV-d^bfJZxV#i4U#5wT<}$8_-G&jxvbR;`(a!Hv*lMrbiJZ1Fbl zFbiMns1@2^4r?EpxU9u&Xk4vvw8lNM!Y;UXXof!e0JCsu%<9dUuUY>!Hkh$Mj}5^( zSieL)*3+KL3;5guK0lj#Vb;Gh>o20$R$`xbfytgrv;OkJ*cD(p>ni#H{WbL0`3=^? zZTdL(CipEDumSLG;P)^##F)jFaHsdrG^hIIqjvZKJBQGoSFN>r61H~g$=0!@{8A)Y zET+**K2^QAmPL|}1LEODV8ze1ke~+8Y_qK z-MCKWi3O#gkyrkm?|-IyeP^FR;Pr6DB7>%bT-n9B1xBMsA$pV zWTvtbCg_zZ2R_k$=*UE-TsWlk8L!47DP>aRqC*;&rpal^m0NM#C6QC*!i|MSb|qt3 zSIUp8Dw*V*M+sBD;AJ6TFcGg5uy8Er_yuopcV;-IIAdf5jXPx;=XwX8A0(p4DKM&2 zNnTbs9tlZyse$0Y$R?OmK@s9Wtzgm4H0NbX-tX5-3QxTr6cg-lI(`LFGtw zlM1m(Ii|V913U(-VUbKSu7bwz4Ldf(XS5TEKku>C?N@u#={DcpHhi~EV5OXj9v1p&)yEL7**5#;vN<$e^v&m*f&N_R2%pIz*s`g2_(#Q{ zAU&XMe}s5r#ptj$>mY|5WQ;q*R43+o57=imb1)|W?(sfzb)I#RX9MOTpM2EFdh)Qy z$cdp(Z)4n$EzySokHrN@oeSbJp(`Y=LTrqN;yRHVByN(pMdI{jirb*drY-SA^=ov- z1ssbf;MB+kQd88#CnP9dI;`STLZ`85&?@OZhq#MYk!6j?8xIGj9{X=!mRRr>zO><` z#<%28m!S@$j*&m-nCkG{&u{kaH%TQ6F>QRi*O1qIy9C=31G)URz7_YevdMjKH{mJM zRYY|Qpo2KAx$5Tpry2B%k|TWz?e+iR>o2kQJ+x{FG50mSTlxfzZj8<;ui8)JbiBv2 zsjfy%^mS0_9c$xs=+DBduAi-0VM&~#epD-EQOOCiem~Xutu8N2!vF8e!hf!yq*F`u zOpAO4^ENqZ70O#c`t}-XkIpRpB($af_L!7=zbRN>P@UJeyi0ep0?Rh-*6`ZU`x|Lf BIN<;Q literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/caseModel.cpython-38.pyc b/app/api/model/__pycache__/caseModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58bf4b7945497c3360da851767ca6534283123bc GIT binary patch literal 3246 zcmb7H-ES1v72ny<+1=R>e*xhuP7@VYOv1QcCmdB*6^skpNhhVJoV1=M9I-Vgqi3icwX;r6&nY3ESq$lUp5u8V_iBj6 zSbQnO;yk|?(R%8$FI$+JcR`&QD z7czdZV%MCqOmsW6DaW+!E{aci!Ya?ntK^mN{hR;d0LI{P7r^k4uCS0EW{Mu+ zQ66Jq_(fQBIjYAA!w#^-azsxMQt^ucseqC!y{NzrY2;5qPnxMLLw=3?S@7premSgX zI^G`gW?3)nOrMM&uCSWTW!#~Er)Ah;D-oHl3d=D?-FS=FWyY*|6{}nl+~H+UCQ20t z8?QW4VfF2+pSLbFU{F}9<(h4qM{F+o!66hdwKSO4soTx#SN-?TQ&fWSS+iz)6x2SB zK^mAK!vD*1>)K~E+@I!|zxu0Ztx))hfb~1K{0B=b3XOiW3*v^qy#HSF;tl`QmG(bA z`ttsT5y1YPe+GEGkdvxmlud^lhD;eo@R`uo4C83cv^$=RVR*Rm0E!6AE8xW;kq0AF zRZ)49mpsE_GFdeRd^|J-u2M#Eh`@r7DYqg#qaqm4D_S+Zd6~3agK?XCd?h6I;CyJl z9%aRuh534YhR@FOx4a))z)3M{miUZWt-@o?z%sc#Qvw=6ez-kUt&1#-#NBjpNvaZ6 z@=6wdz3}7}@_vVA35XoSAv+-#Pfs%-O+c~~ku1GIvLw@}Cqq4HAeP3mEZHHM286?Q zmiqJ5&OtlRdRQ;(1M2n=>SqIldI9wTmEkRm3h!rIVD7DK8^jHC;+~d3M7*VfHK=ghJZ+xNpPr9Q(m?Oy9+xheCKks?*%{4#I}Z zqz5en_kNnvFM41LUl9Xf#TGC%qTJjrL*Y4gNd64B}Nf_gwy5QeiU0YohI|0d_gRf6c9vYuEHl+9w3PupsJb$}={@*JRfiDwV(V+Oy zHm*HvG|zn=L=>VFA#h!%7N{p;H=1YB45E2b3&eBKCzBl+>571g zY$2_Td6owPh!l~Ts^FzcnOUAyDZ4V}dc5k&Jb$|iTEdwjxC=VyiWml~ptu!bd3Bkj z$-H`%3p|AdZV4Pa=A0|zu4j5RH;|hIhzlySOo1}Pr!eD4UHlM7VBJft+v`~lms;?- z%s}*5ih|Z&98>^<^mbnW)72LZ^rctP=!a^vois-S+po}w?`RIvd<>^~6UG7m|~V;{(GFQu@D^+AgI zWeQU?$&Xq5cY2z(M-bSY9`OrUZ;%#ben(_L3`pr-gs$PD|KLQ>50V%t4oFjb_478Pv literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc b/app/api/model/__pycache__/dataBuilderModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0d27cc1fbf6e83c792265ff2546bbda5cdcd43d GIT binary patch literal 2125 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 ZK)Iby>Pz`ly`o(0^da$dPq&C={0C06aVY=* literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/projectHookModel.cpython-38.pyc b/app/api/model/__pycache__/projectHookModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..820a69873c09d49186cdeb3605609d0725888afa GIT binary patch literal 1484 zcmYjR%WvF7824-KeRyAKnwCHzw{#Dz8aS~+sHAG4NC{Opy^L0}oSoT?o3Xte+l4Jc zq6(n)uxeEmRgi)}Kt+*ATZBNNX?y2Sm@9F@o_a?d_+~aQ+w=MReb4#6d0Z$K^9np~ zocYgtC8H>R3vlo;0GxyoQ6N~sDphkT#kP&`7KkIeR z1~KUp#?4b9P5gjcy8^rEyOi!Cvl%ll=yGF$d>HfG!r8gg&4st;&T%6KK~3eE_nPm| z&jM4yq)lDs#@=P(EV>Z^nH7f)_S$h0>ev_< z$dnjL!PY=W7N9H?mX#<6D35b1DoFBDQjn4YNNijjXvmiKC0xc8@GkCYt5RD6TqYGf z2KI;WI9OCcGKLT132A=_&^YLifL_B#@e44Y!~3#5%_M$tkU2Sap(nYMtl;)+-iruV9q32Xw2}t_3hO) zaME}-NGNp|DPdJmB$Gg98pH3u*<8Oj^;%pFZ>uQxU~uBK1dJVgVWBG`OD7Dejx z?!)x!Plk#LVjINUzdTKE4$=po4{!ZgQ)S2@=#)eRpJ}8wA8g5=&kGb(??$nKO1a6Sp&yXMA&7*oP+*M zraA^ePCVcEYU}RL>9uc%U;glXV|4~_`p2&$%&Vs^oIihhe!buvSGZ8d%T=hmIy zhj)If>1+bxurWboXGD3M&(^kXukTnG(t?+FCK3io54$nhcbTsp;aSHCT%R}&&pVDE z;)Dv`cAQIzOLsIy$BD%Xj3CoRO6aq~N{T^YWU%arh7EZz_ zHzNW|4(kokASC7mOrkdPda)M-xxF3?uLTto+S;i=?A$pa zsQ3CzY@z~;=qQk^YN!>p1n-#qSKwb#$5m-HU#s$bbcwpOy+r)}s5P=lV)C*lvJp(2 zeObV$#FzJd>ezEBoSmrN3!}Kp2&iN;_~Zd2xOeIY?iwbh$E)Jtgh3tbnk0%u7sXEC l#w^@Ka(?>_8F9yjI{W5mvELF~G7=ugRa><+TQgNt{~w)1yD0zw literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/projectModel.cpython-38.pyc b/app/api/model/__pycache__/projectModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3130e1123caffcc49654901812f5d43325797b78 GIT binary patch literal 2503 zcmb7G-ES0C6yMqJ@2{5cuhpnz6D?E_lhv470tS;-Ld(mfaWdU|TLxxkmpe0!i_w4; zsA6r67*KquM46$ow@h^&iU;g zPo?5MxW1orwO?XB-){&yuONUvxUE?*#789EXGpSS_+`HlkOM|g4jLgjWQ66g5s@QC zRE`=kIVNGQUymCJIU)Rjo-|T&O87xNZDiz(YQWS%9NC?Bu~Vzq;4B zw&-47TD^O=K6i0#=|VBU!;Yz)b|{aUj;^btI%VCMdKQe6Y)_hR zc&p2|+)vMGq$qi@_JsP(%-Vz7%|1wS!eE+{^UW*sO$l)hOmE4ZU20sOFYGNfZk_{Y z!OtUvj;W4ri)Wol?0);5-KEk=t749dmDE32aKHVmd49U_^J3A@(-vhDlqsE)9B3<_ zJ9W*yzr4D<%U<*gj^gK3J?@zUO5WgCc`xTFa`KPL-%OZBH32ZsAcyF;#;q z3Xdy_Q6Y|weo|3RJF4CqNhykrM0k*)Dlk8qEE+6%9zeo_n7~4a<4_pHDcg!h7#;yn z5Nef~R<*T?$>W4pRc2!~Fyw-VEnBr6izNUs)Fd7*SIjYOoX4SjV9`Vwt?`&;P(6KRk%6?7*tzctnnU1K{D%{!# zY@F&NtzsTP&{M6kG)%@7SzEzGrJ$6Rx~067gilsV3oi{3k@puRz|*ET-90@Xek^^9-8ZX#QMJ zqI6JPpmcb|f#Ss)5>*mpAnk&BuSOClL0+IVknlF$p0^*L`~sM5a9g+@kH9n#mzKoH zKU4NDr2j9$Vx9v^^CS;NoPu*k4T9X#j8$4l<@NmmHu!qZFQ1@B%@ZwrtygoXRHCxHMT%FVSc&(_JKGa`h^FdLl2%W(V%g)2!jQL} ztsP15zoDn&zXHQU<~jV;tC&5IH$*K9)Q_bw9_)vI5<)pdEsEv--7b0bWlxn~K{CWv SpG5`oC;ee59LQ#~u|EJTIl}e; literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/rbacModel.cpython-38.pyc b/app/api/model/__pycache__/rbacModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d424969bf9b4de6b482826a03f7c57f20c5695a GIT binary patch literal 3554 zcmcgvTW=Fb6!zL{dwsWaPbej{YD;=a2~=vERuu(7#RCYD@Un`!T08@+*tN~csSz6W8O8 zrfUlNh3$ls)RSTyu~SZ3Pb(psJQUJ1G)*%``il@zN%U}tL}}(wM9%__kvQPEz&XGg zNdQg=oClmFDZnX#3xLxk12`ja8_AA@267EpWoSU-$@SLm%_WcSri{lnlx^QB@%W-5 zJ>rW zNlrxc1ZZH_Bs~$lwijp?rmxFPh5Kh>uX!t9t|viA9uYbD>d&7C%7ZQ#`68B9&oFJ7Gq zvo6p-YOGC8&fmK_qX>ON0gs0T5JSVk)Rp?NOZ}^d>lY5fU?$AtgzhqZ+Y_z~UHu*q zwUbwZsSEW#=IV351mAvBzjIkG&`*FdIC3VKo4r3f`*1m+)eml;uATiYI5OQhb^gKa zGedxbo7W}gg$+BlZ{N6e)M#eQL)@->MD4_l#)&^+yIGgA{gfGti)n63IN!f_xpwT< zg0sq2!hD`vm{BhI6=HgH=^VBWbaBlvN~S{%gC`BcDHGquaoRBU`KG;K$ry%*Co3U~ zz*AWmMHWR2U!oSv1j33QOhZMAP`Ajeif5HeEQ*?#>zSVK>Ius=+^XwQhbJM-up(mY ztTHSI9BqW!6jq7p&hQL*deO_X9Lm#j7sNm`6JkT?hvM(GWO(fBJ=N+M-L;GE_1?39 zrEHg3q+@2K0zYdE_DAh8w$m(b!O>u)%J7J>PPurHnxZOsr9){`(n=P-C_5D4^K?4i zhKVnQj&YI#DghCJl1U(v#94tQMRQ_|1kC~s3#1L`DoF`66ljr@or08Iq`QTbpaW7$ zOAqN4r0fGyF5}5hid86{kf3Y;4R3?sF`4B&W)?+qV!E<-P+*#}cTr$6vOyHhG-U51 zT#cfca&oi=b!$Q3-X6)LJmiziCCDT6c`T3cEY{(~FF`ybkD?@sB^{z3ha5np354Z6 zfrXu~qA1$Hn_C!ZS;2FyPrw4ZxGm{zF(5rhtoNmK;X*W)+5($4r z7dml`XmF;B=Zq~>YQloG3n!0lR752$?0J}hObv_1su9HP0%L2H^O3t@!DLPFvrFJS z*b5NjZt=7>Kq+{3k3j7Us8=9tyXe^`P$!@++D(>;_`gDyL;QP|bWO{vWChfKUeXVl zv68D>Xvv2w7swXg%RZNaW2_&!F+X`Wm^cp65%ETTFLY#~Z{fb|zCtVO#6XyJg1c;; z$a5cgJ~UiEb2+$kynoHGe02ZX;o1-5jiZ+v|DX?sbN(ju;cFm+yN6}*!1f-DPtXwnDfyMe5p55>A0yBI zTdp)LS0XVJqEI}NotxN4u*Rj*)J)KqP@34s@Ms>#H@+uQ0rTm*sM45d21{=;jQW z%hmPp*FxN4L<EfyrKH2@a7>9s@>%#M^6mcuEd+)M literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/reportModel.cpython-38.pyc b/app/api/model/__pycache__/reportModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6739eade13c6885ce40506e66a1a309e91bab4a0 GIT binary patch literal 1840 zcmb7E&2QX96t}%z+q-K!n~#1_pvnOW_CO?%xI_q*lopCeK-ulZiex!Ev#F`=-Hz?5 z%PCcWR)L^UN~?-O6+zYJKok`e)CBlL=1RQF{sCNi;=OUw1PKnTHE-YVy*D#&=KbEB zx2&=RKkLpD|C%OAe;_#gQvsZU5nlm8Bq9e=C_8efI7(P>3Zd$#VbLjunxlofqsthh z1V&hLN2B-@_wNsN~2*augyv z1**~_QQ#{ObwzbFf#F0%Unw~H9x?B#D=*RAe#)4 z>WbnFfnFuULLV0MMqu8kz$1W*WK8IzppVfbZwz|@PusM*?`aFnNCbNz2 zdbhW7@A#{e*>5*`Yv1Rvrf{9mmX`*J*hZ|=-?;~Q^uFHgt!%8zxg8n&PGkDL?9)%O zjmN&E?C*K&+V0L)c6T%Tb}=nMQ2*w=-u2bp?QK4M`uyzdnVE+BlqLD=v4C3xkA%2nAG-v~$(zA6k6_21ovugw4I zE`u1NgARJi5It2PCiG2N^oAvL?5^Br3v2?mpuaHACRON>VP1ZVwxAC6Zq#%tP<5gT;5@8^7+sSUAb!NZck&zf TM8#Oz7m&J&sc5oRs1E%Ns~Zy* literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc b/app/api/model/__pycache__/updateSqlProjectModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..333b98319d6397f96912cc22fa6025edde4c2c61 GIT binary patch literal 1263 zcmY*ZOK%)S5bk;J?8|FAJcQr?x0Q$mSA>#~C_*{din8m=Xr^_tSONsHW6A%cPm##@ampnpxKQ1@T5r15wf)s=s;aAYwb7^&Tu*;} z8m-ub{G-6^LLRw-C+{L*gc&hOjKE0Dz)Y;bO6=Wn8re4hf_}di^X>m*|B-Na3sUY{aK~zm;G9 z*fw|*@!4G@MIFiM(O0Jr@1K0JSN^b7xJvYJ|77=Zv4GcdZ(kW6?V?7x{n^Qrd(pef zqf#p$eSZAMPuh98y<0vxcy@5WQAmonki7T&@Z00>ekr%VKl|p<^TXZu5to1eKE`cR zdJG0(5eprRoWHhu>(=#^E}b@m4sGoB_`zRi5B|iAn>p}J;B;atD`^qO;q4f-6K(PA z=<)H_`x6VBs~qIq#4Jn4#pH{~)g~+LMOmRV4HKYLRw+#~R>bOFr*xwTy2@ohMSBIDAJa!e%#0o+l3 zR8eJJOMy`lB@i?)plA?H;Ln7=trQjc_cHsS_vxL{s0V`qtc!~g&J`boedvY5A)cs* zwh;HmId`TlwXC>H!x2|qq5dB7caT&J+i1PCo5lsh#O>n7-~Y7!O1mzrm}waI??5t| zse~%ROd4;TD=pzrFU?FeR}(AeRflHj66$pz&!a5m6~(u&;=Jo`o##5fsW4^b!|@Yc SQjM0Y@ZU7*X5GXnt^WaI2zylk literal 0 HcmV?d00001 diff --git a/app/api/model/__pycache__/userModel.cpython-38.pyc b/app/api/model/__pycache__/userModel.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..87657603f88a2d2349e04e85aa63f7dfdf10ca49 GIT binary patch literal 1846 zcmah}&2QX96t}(p*bnblnx-ZFil7`;(9nuogivVJq8<`e`M6jSma{WStlI0%*bXeG zlpvG}LL*vKl?LdcqJ%?Qs)|5b#Q!l@>P?qRPrYz~_r}R42qbLH)BN82X5PFv@4d-l zF)zV)shzkbLy~?+u>Vs548sUL5JV!fC;74^`--Lb87t$fmg;Mk=IfU38NAa?L z&dP~-#>@K!s~}4>wNqvle^DgxGt0oV|@M6%;jE!P2yQqAD` zv+nH0Mnq>Rrflb_cR7XquKZr!?^&c>yk|h(#QiyD2s7f`Wz@H)NifZYA4Wcc{ zpaBm^7IfLJE=Te!ij@<%034l|q_~o?@kv5sF(2US zWC&qaV~`VEamjjy7n;oV9oDih(-to}aTK_XI-@>qMBJzcK2AEr5U2}r8RYh@$H}i> zZr}L6y}X&MuGBJIj~niMOnJ5ud!94xQC0+hpULz z$6HUgwiurG;uwh2&!2qNUi%?gzSFsN_xY3CX8d0T@`*j{cWRt?UryvB>n?i3!a5mU%u>x$z))yWHk(E`sBKN}Rfq{PseiKHoESUf55E!3f-2NVj z|LG7K(Xcx}?HZz5HIfwwJ4KE_mmr0mt^nPUn4(^bqy+U^BxR_|3eRFyjs+gIa_EH3 zU^`^&x05Ss6OY`cYuovg0UDfEcZn*vklBL2(kr zt0?}N5!?`#UBV5tZo{egXzVo<_`u@L*y}j!K216sMBN)8j=%^p{nL13j7zdu8LgFh zKAiU)uRcfpR+@<70>TJplSs)uOIH6LeGppw;J*IOus853_NJgQ2)uC`hHlVc_~2YU i3*BG5sxbXux8>Ql(yDnIBPXtWQZ^M+(PceTss0V{>' % self.id diff --git a/app/api/model/userModel.py b/app/api/model/userModel.py new file mode 100644 index 0000000..ed7c445 --- /dev/null +++ b/app/api/model/userModel.py @@ -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='创建时间') diff --git a/app/api/service/__pycache__/__init__.cpython-38.pyc b/app/api/service/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f6027ebb7e8cca1beeb84cd5f4813f814171b3d4 GIT binary patch literal 143 zcmWIL<>g`k0*@ODlR)%i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!GXid9TiMrCD8 zYFb)qc8P9gUP)?ET4Hi)OkzO+5NE~|rxulE0{QXrnR%Hd@$q^EmA5!-a`RJ4b5iX< JrhNut1^{(iA`Jil literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/bugService.cpython-38.pyc b/app/api/service/__pycache__/bugService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12417b34cc8fa27804a7f41f35835ef082071cfd GIT binary patch literal 2489 zcmbVO%Wm676y=Z<_4F%ooW{;WNznoX8UtOli=rrkrbt)O#_2{dK+v3#%!Cq^A(=u3 zSGCb~KOl>0=}m$D&TNY!@Gqn&&~t~PMX}v9W8c9U&gs@W&9ezXO4)9IBt8o|j7T?pj2Yj1gF1XB2^a4pqLM3RK@RJA8D7m*i4iE89w!CUWOq25O9z|Vkc2KZ%(ERRQBD^O zUIawAY=jXYg#tGKRtn@+ZZF_+Noz_qdfALa4ew;`h1_jO+ACY=loOx4#5dJlt{`nx zH;G572Q3BYlVU~JCDcQ=0V%28u28Ms5>T*~jWFq`4ZaOf<-4$JY};dzE~(lps*-M% zTh!Gk(SbXVlG1L4()CeS3@fg7(G8vjq<`56lkGLP`v5C$shL~n4848M@<#3~nk7@K z2#>8OOf=O|7{OGDvfy#IxgxE)#f(P_Rcq#tg*VM6s;yBHo(a{9B^S~Q;wYO;mE|Wv zUB8vBT4_+2y=L)!u(?xVR1JdaL4l5EETl%50zn&^ z!l*R#!Ys|Y%(@oWgmroXfko zr+z<0%)`q4dllzY_Wmm!DEqV}sAd(GK%k)ltwzI9DWQa?>#IZAoe znaP-EK}yw+@Jpd_&0_Lnto^*gu3lX9nE@5IvJt-aE)|?I&dImAoi`Rv%~IMc%OG=| z!bsZfolz(v?oIuPQ0*A#jUQ!zQ}N>xcHHkBAjUP0!Qe4<3m&s{PrnI_ojZv>C4F*vk|06qW4AfpQ+EUWY{xL0hCv>y(l&h;RfuAc9F}$m&ewwuqNMbimA`rg*{E z@}eIFpw5lg{&rzok~$Tn?g(0EGMD%p(s~}`4@6!R4k%~^&`ye?C|Rut+Ny9x&1xmk zj)NW^y`t^6mJWKgaj)jsN{+a`UIj?OV@(_;vEz?Pq^|H@-7^ zG5mXc=liGE_XCy*7-dK+xd2lifh8u7=zt99z!|YE@|bRVj>4RoLZGA{OOKhNmRg5V z$CTTVkS!jxv?=*Q@UYX29urjt^Z!2+pEvfl`~8M&HswzIY1oOSYWjg}_}wndu%VOi z8o?~ojc#8dO$nyPTZyx`oJqw-!f+`-qjaELglR)q24NP$fDLk^{1#DI)ie;d*A);5 zzuf1c@banvX9^vdLUj2m?3wD+0^>1N9W`SPaY_gPLrWIyN>zo$g_*LhW?8e>1PG80 zl<`svd=!K%@Ce#;4!3+o##7eZj8);a3JbQ#v;=$TG-X=JGQIY7NWtXr1T32YvZMoL z%)Vv&5QG_9>L4H(7Ia$&lORZ2_&}P_zOliZQtI#e=BTYj+j{!_p#f8rZ39U<;SOOOMZG+f52RO*os;CkbDu|0>|% zOp)g5pY;^jW(G?fC}R)U9Ah2R9FLqdr_*n)){$SwJb({`@5nwhFqX$ux+=OVgBV(M zF-L1~j#W#6*0MR8bM~G@E7pnb_+7mn#e5eE4KG1TRwe5bQ~Z*xg6dGKQjKQwg=mQAl_N<6uf zn+i`MxN$kZ*7IAIhMOYiA__Qc>)4OOpe^I=NZ3cuz9tGkUxlr%u(1>tRAKrQGG@4K zC)!7d!2!O3`eRs1H0L<9!W`;mChF-_Xus*@>Q&)K8(1*-8=OAz@cuzxS&O$_WlCq0 M9G=4#wms&uS6C3luK)l5 literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/dataBuilderService.cpython-38.pyc b/app/api/service/__pycache__/dataBuilderService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fcf62fa1b1a7bb21e6dbfa32c54fdd54a2499af GIT binary patch literal 2424 zcmb7GOOG2x5bo}IdhC5<^8lhiLf|q7a^n)A2!(`~#9nq$g0VDArrXJEW;~{64B6Ef zfi?$_;06bjMXVzt2$uwj3y5FRr$}g5yC-fOAV^g8*zv^dg|WL`)sLF5s;j>C+jDas zf#=;@58?!*-%+VQCMYjM%U_3%6HXJ7k~X0zvxKIs%|JG|nHZ_rHnnUeR%*BH)M-1E zyhgaqotuO^!re654eqWGzp)S2h)~2nA?HEd>t=U}L^FS#<7Yb? z*VflNqT3bMix=WvA!Ij90#n0c<%&o3M?i{_Dt@3ejqW z_$Cj^4m9o15(ra=N~1JK)(Vjg7@oM*8uS5x2(GLLG50OmggM!QPTAR&H5gOP@e1@# zHS?@n1mLv*chJ$QTR>?MIvkonK6wD-`r9H*gvo=l4Nc=lOLg2Ma|U(eswl>g%_(Gg z2=>Xt*v&vY0%I7A3A7UjKzn3zpTf$0+_D@Yq{)M_3r#~j0m2kw!AfJ3LT_%;(i+(l zxJH#b3rU=7yxll~xaG=#bnxvda@DM60$ixvet1;`y+NuPTFT=Mp<0=QA`GG|8Kk{g z|DFIe$DmF8JAHtEGZ_9)89bgb)#;DJZ%m!2!ao!FrvcUR8nT0uqHgf9q`+jvrX&6k zgeu|?XF4*5W@+ZfW8l9oTOlRupNh#4PGAkve9bHwhcn^EmNm3smsOI&hLbTmY-kUi z(vhn%DIMIo@n+$`4!8eaOk%RhhHmL@H=zFMuu+gTPw%w3SvHF1c56hqwPhe5ePK~j z+9j~(7UPXByF^xqNs77ap8S)Np-0Ka+1h@um@hrZk;+DMY6CoImEJA#4eK(STq5s~ z+klFLg^#?;p!L{g(16Y6mIu#340gAE-2L{>-o4v9pWffO_sh<&U+#arz5CVuov(lR zYc3yRA{>rWVh#F+d=`FGQ;K|$ zK&b;esre{N`&jZkz>DEkpXw?D%C`$(_&2yR^*XX|)J)6?DKitJ_h~WPkTS#0o?wAE)|JoC$Z8Li@7|B8q%OdPLpU|d0cekUR)ppJ3?PdLUWHq zs#HcV`%pD5-iSnBFER2$`pqg!F?{kAcBgTvGmcTafnI3(R!${yFf9RlepR{EJ2EMs z!%cLS_te4|L@+joZ^_R?N7y_ysY_jE!Y{+m-?qD?#~53r9(-`r{>L=gL)%)UI0pKo zk&S~2dFFLKUB$1VPhKJmz@4?c?0d=$f?k-4AW&Wqz+E*+P;Ldm`-3nU&$KWP;wTlx zwTxGxM)s}F0ck~I1y_!pVCSStVk literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/planService.cpython-38.pyc b/app/api/service/__pycache__/planService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa2a99c871f6e9102646f977b351da6de104d2fd GIT binary patch literal 1763 zcmbVNy>A;g6eo`-oxWUu#jXJth1VPEH9!yqMiL~OQNZpZ2q4Z!%D(iZQ^`{R8yb?x z0rE8!=+X|Z(8)v6b|_Gw?LWfRRc%ilx^-#axhb2S2T zviw){k4wlI21S7}vIVL)zz8B}OggklDS1eQBiJz!Ogcl>WWwDce(BPIWj;0Dr*Y7| zA0#1!SEK~d7N~v%Mv^8Kr0EFSWYW#{8`5S8d{>l?3HW93SA-`j#}pzDqE%59H5*kR z`cTY^1shc%S`Vh)h4nL0w+k?L_2%KlwA{g0nsUwr@7#k~P%el@xn{x*K~&DoP@A%kslK_9cb z2dX~+lag=gfDGus8L=I5O1FJSAw8xXDjB5GXU5Y~>nQ1(Y9|pg=3%T&B@lu~-F9+H zR27#0eN)_O9UUAVwq(04KTki2x~Wv{Ae613*8`1OI?Jlnn`XG#J5-3K0;=&;;v6j% z7}(4(yiW!SYa!2}u@|J_0nam0WtdYQ7&D*EU5TlKI{tkAz<;04yhak6_HyCo2%Csq3Vja<~Ht zc}`<<>~fevRAM9^!Xd4lW3 z1ZNQZ|0>Qdd_p{%rjQXoQwQq+=c59oE0tT^DYOj-D^bwLTCh}O;pbG3oSfLnyHM*v z|8kuMsfISg+A*FTio`5C2x9{@XJ@s@PGK9m>%*U`uwm3PnC%CC!&Er$1|7+{sc{Z3 zPd`S#!TA^cAfC*4oQovHVFVO~9hn{^!oDfCg%x&%!ZK8NRSL;b2&(|q@@Jc#!31`& z##*aT*Kuf#W!ixMx#F0@wcEbitcxJo#9ZKaWb$xA=gKg(E3P}nRBmV4+(FVUHx2eL D&j*xA literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/productService.cpython-38.pyc b/app/api/service/__pycache__/productService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93c789195f0cd104bd82e89a97001c1cadb2ca5b GIT binary patch literal 1360 zcmb7E&5qMB5Vqr_P1|huM}SrWA+9;F4}cIt0*PCN)a6oDva+$e8%UdAr$}Ap)_o9q z+y~&mtN4V(X3>w2SjNTsD2rj`K(h^~z~5S@yF2yE1a=tvChSH9>2fHO0!PaaG4Es13e zq_%>-2l@_F55W}VJ1t2`OJ~EzjOwh2%2EyhhOPf?6VH;OvN$taLZtm?$iBLnm0~0_gCo6;DLyDLhZ%2Bwo; zeTFqNs56k28R%?Qa9}He1`@0cPKp|UL8pQHv;}wnq!=+Yf|=h3y~i{;$(tf!M(X@X1%sd${F z%lV?s4I7NwaC_e3);j-xM*g7YLDliP>}*G%Dtl`X&H>SrJyh*MIM|F{hVU?jeOISo zMq$tRoG+reMNn zW$*$oipFO9)S-Rm&|ps$W{g62G89pEk#GN4^)CQ!hKviF^soJm|GL8b1~0DdO~C#F Dc_u3Z literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/projectHookService.cpython-38.pyc b/app/api/service/__pycache__/projectHookService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59f3fd8e1d936ddb5be3d2551e2eff4ec90dd184 GIT binary patch literal 1818 zcmb7F&2Hm15ay30%eIp2Ch5S!_oH*hvQ!l4h96q)w{p5KYT*|M58-A zFusFnz5yYLq&b<>F{P-poXz=|Q}QE`TzYpzddj=yV_*8`Bn}=J9Eqtt{6!bnDlLC1 zirEiI0V~7KN(xR-#^VDg^yh9?55Jr!NP9@DqF70dlVNhb&bo@1Q{)vO>OxX%0b@B_8yhB4ifl10J`#=j{_CxLJ-NABuP16c zRkQNjY*8vbO;R;UmP?q~#ME#m%Wd)+glEgO4j@PmrooRBcJt!DM9ykN?^7hu4%G3S zBXkVPmQckjZvz*n(Wj*qt(FpKiHo(!WbA7^t3wbrC@!vH%?=w6WM_w_n3e)qE1-i! zYcWJE0)a{k`O7_!yI)JO5ZZw{a(1|%fzrjj@pe%6FICw<_O_7q0i4rED0ZM>KwNL3 zz1#!sG2{aU&xcbGqZA>v19i-dgZL7ZEyR*l{FZ|BZfWIjf)*~XYv(A>jKe$4a>$#p z9LgaMw#e|CQg#Tvwj29Gy5zJL?1H4z@skcR(wen?O0&DVSGc;ES$zQ;fAm}+bZqZr!Sz=lFB zlDQJX4upVKw#reDg!pZh6(&z<{BTA#){K;!TJ@7?ae#Q^@R3W zo6o@`q>K(Yqfuv~tXVZ%2y OySLa&4Sp;F4F3YB!ls}A literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/projectService.cpython-38.pyc b/app/api/service/__pycache__/projectService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d71ff9e40fe3fab0e1d01808710070593a413bc4 GIT binary patch literal 1799 zcmbVNOKTKC5bmDd_f8(h#0QEjc$$Ox0V<*>c#A^fWm#w@)4j>W-JP{(R!G=GH1Q%q z?~3RmD!C}$6a@cAUz643f6S9ZS z_{xEE9VD!Q5=77zY12BTGAO6S+m=_@b_MJianjgTQ3Y_P^<;u-QL2Z&Cwo@_z~SKQ&zB#EuV2@a-P-W;<6qx)_P@XS zvGZvE%ZuUD!SKnu;q$lqU!K)&-MCXb`0(~%^ZnlDV~>sTLK_N5Zi0l9pd#{y_DP@i ztpQskyL7>^6jnzUJSE*oI!qTs8HRq)(dBj^WQ%*PP?uaGxZi08yF`^C{P$&Xxv{aj zzTS|{ro10r@;i}KP1lnRx7!8r8(~bR(T!`K@R;weD+E*m3GrZJZOk1lzrfdpfPQ>dz-Y>*W0xeVO#lRU(bF0}DngLxW^am)jl%p-`)D>6#p=0R@0i?~ z@Xi6W+8DfxnO+{7mo7Q*V+sdcodC7q*t*2ygSoD74sTPhB{|nN=MQ>r%Ru6~$hinS z+zul*^1Zfi>DCcSXE8i&DCe{0bic*0;m$rSr)A@i`rv? qK8nKF1t(XZ6mBpN?MnV=lGha;9Vna$I_@N0x*8L@hO=TiX|q4nSD~%| literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/rbacService.cpython-38.pyc b/app/api/service/__pycache__/rbacService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b539cddf1de8a34167d2b465d3689639e1e2293 GIT binary patch literal 3695 zcmbVPOHUj}5bo~TnVsF81&r|nLmcJhcu7nw9}){8Vksz*a^PT`99CGPWx9cZebGHw z0`*E!;FyDzTp~r0*c>A9NAeSL=xa{=0UvxyRnI<#!IlyRs;9cUy5{TZ>Z;oBs?{=q z=h2_9H>C!m` zMCU-{h%GG`8p1YQ)osQ}tzag*LbhV1;!Zn^I$W5k+<-EbR)-%nMLIp*ZAj5h!pzti zkpU=w{RLmzINCWp+z?w^VlR0Rw-X_^8j;v&bh|+Dh7$5c41EbUTu=GM?xC63X{d1g zSr?S4_Yp5G4E}t0r!++;ET6vR1V}|Fntet`{40zKQl}h_RXCh-c0!J6y#V~2J!A?! zIA%W2JzhM)o(H{mc!dYHSA<@bPuNvH@RNMX@+IJ}@oCGKfxpgYEMEcs2ES?f0QkH7 z7QbyZRiQV_@7S3MUWOxj*HqA#26l*?%np!8D_DQREhglYq7P}-ch<=nU8^yP4rshc zibf)8jwvc(Ptla%XhbuNn#%YM&cnF9)v*VZHdRbIi}DSj;VJ05PDLxI_Ysw zHe+RhZpBTolrj!nvYI!BY%1wYh=RuNHnv6BK4_Ve%8KX}Pp0o9P0B0cs>B9MEHC+`?eXapFXS&Gcnmiy82Dv-Z3!detqMX(_r*2(=b;*b?o75%S^y zx$$urw~)si41m~Wk1}X4m^v$TA^TwrK~J~tjE6UL=RRbj3kQD-uZD$l!G`LLr9Ob3 zmjfqjF!njn&?7&{TJSCUt_bhiNZrU%=y-WVpTO8s2GKijL^M=!Au2&ZG7h6_keA1% z>J<`pOnR-efpUnuv*fg#YM3Q#td70XbN#6TDdbfrDY|OsMG4M zST9er3nNF1*|Xm~h@0GAzA%wOEG{=YQKP9=7DtOOp+iL$9cGCb(FG|wO@A5Vlb^z@ z#5j`2RbJ+Clmhz}rWK|Zk1)88N1y#SXruMUk9Gh*RCl#Xnr*J*eXxFv+X>Mwka55e z#!K_A#H(NmHrGIgv4tU|b_dE1`2eSi5JvF>#)A@2w0UjPUUX9&SdHmQ%AjMc?iOfamKg3R-)wbjA2I zJBm-4@QFpcg0jn2Sk|1m{P_enB!Gqi^rNg_yj>%GR)c@X)-V!lMW{K~)@qZ+55sn& zCBo2@!{N_>&;(()f6!&3@QrmZQ%Gb{{p^lPQ(BJ literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/reportService.cpython-38.pyc b/app/api/service/__pycache__/reportService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..603009bbe6971a508e2e85a763cce00965351ff1 GIT binary patch literal 1912 zcmb7F&2QX96d!-AKX$!I3sF%)1tj2L0g2!ST1AyW>cJ35zJHh+hog#Y~U?wE4~4IoBFi5ZGgss*%WQj zK+Rn00>Ek6*5@oS{$-Z3)Ph|n5At~22Qb7Gkc#XYHK~o-99c``p0N;`90Mqwl(VE_ zp(S0xgviRC^ox?RJWg{Vy@b*@>vhX}gtx)`>3j73<&CxBaG7joKN(ZE2{M4*7JR35B(r4V*7kanI@)Sv zP#42io7B7GZMs!!H-J;xwTqb>`jEL}fJ6o_0olLv^ZpNa4)nh zw0Gm%$B({(#CY+)&BmjL2jAZZvjKLG&RSLE^PQEF4(B^-@62D_?99QZ)1TkJ_S?a& zpZ@y&>&K67qqL90@wMBBe_TKK=DHHTp`>$q@toR8wyLs9po9{rDZDlzVh&X{z^Go` zjQ4l$?%mws=V2Cl((OavfFza8zV0s*o(|FqdApYsOty4I#nrISpjZb*k?^5((-KOs zSIJR(Nk(@ZAIMq%j-9VfJ3lA_hEj($ng~)Z&k^IjcLB=e1PZYB^qzYI^XSQV0JV VUg>pUCAdG2Ng}XY2FB5A{R1cb_$dGY literal 0 HcmV?d00001 diff --git a/app/api/service/__pycache__/updateSqlProjectService.cpython-38.pyc b/app/api/service/__pycache__/updateSqlProjectService.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..544e0bc784b23dae7e0f5a15dd13974b33614766 GIT binary patch literal 1816 zcmbtV&2HN`5ay30%Z`&~-KGi9>>}8kiZ(5JToj81TA;U}sF90cfKWu*Q7c;xDK8L1 zJ|%~JjOLhEB6{0XUSUt2p`65a*4x7p;CLtwXTF*FNFQ%*wg`-azrJN#J|TbOVsUt| z_yeYS0YVZb- zyJuA1*A4jU#6R!kZ@ua1`FT(E`|3;iG8>gj_mfohlJOX3)-!d8-ejd9^U#Oma~leg zjj~b*^lPw!gia6EU_HDwPMskd!+2~u3#8HY(yd4-6r_22fC@yzs;Z6@Ml5gSHXQXxNCIW z8K}~g0!q-&a$u^l92grlTrq4oP6kShCPUkFn#rchZq$|{&rIox6C(|Zlh3x97L!ql z_N(RDy$ytiu;BA(Pay{rf_bS40xbf=D9`}iJk-fTC?IIw$!1H5@{tN) zftqZf?br^TI#{FsC9a*kbnMWf==U8(ky4W)@j>6=cgK7DzUS$AyWJ$v{>uIg{5m0j z;Go)c7<>&9e*zsRoQ7mT9ZC_ikPS3PqvTt{HLhO}t_%HKa|~`A65G6Hu*IfX?RhNZ zQ!hgJ-I3=5^x?}=TU_%SObg;rP8=pQPC2_E=hV>w?{b|R7ZgSYjP7%j*VM>_(F5M% zmKxPy)aGkyR~_&=-%z*#c$0S&ZUVl=Z!6payv4T_wgBJZI|{b}-{todUgJ$r=wa4E zO%8>88Ti82DhePrK)wNq*Pu(tB~3|6Q#R8M$rU}YnM8ABrZ0t;2wTf)v54a!8fJ|_ z#6{@(VeHgB&fQ?xkFJPp!1BMptCPc4s+^)QGP(S8turQ%Ejm)mAZ*=glpTwLhXN8Yh;hvR|M zP$&*2!m%RBg>?NW91n)8ifsa$TOfsEyH&+jE`OUCUk;Ak(Q|`qCHvgqE4D^DcPf;Y z{9irkrhJ@^6r&Kb#vMbJxH1YwZ0D@9nwkoDR>x_G+jzxdndL#A9uM zYq*_5S5lg?gymH?WtZBFJca6ePN!O;Um8%*v>E%E1Z1kiR};U!c+_K4V``@6kEBo4 zGf+=W+{KOGA;?o>kDNY9Asub=z{hkc8eWceIRuICL3ct@@}e`PDTQO}Qvdu@{TPj|QXt_wfSEMiYF2GWqPari$HxH|p+% zk?)1^es^)k5_9w@X1GeD9RSD3d}B&~B9~C}IhnCv81jY}4@CoSK-s(tD<#Iy6?NK* z_0Ss#yfEjkXgBvzE5#dM+&Hgt!1?nx=Daun?%aYioaW-P*siHEw!;q_jPMgF_FVrZ74V zhHesoaWM2(34IK6UsVXbTXo#4&#~z<{Am*3TZu3EfxVv9U00=tn>Ae*{vnN_IJ=qU zx~F3=oPV+6#7hEyAd-`atG_(zzl6lcTjE0}F-7Gzc33Xu2iTz#B^pj5*@^$8b}nzycQk0^>=Uga7~l literal 0 HcmV?d00001 diff --git a/app/api/service/bugService.py b/app/api/service/bugService.py new file mode 100644 index 0000000..90a0d4c --- /dev/null +++ b/app/api/service/bugService.py @@ -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) \ No newline at end of file diff --git a/app/api/service/caseService.py b/app/api/service/caseService.py new file mode 100644 index 0000000..724d026 --- /dev/null +++ b/app/api/service/caseService.py @@ -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) diff --git a/app/api/service/dataBuilderService.py b/app/api/service/dataBuilderService.py new file mode 100644 index 0000000..b1e3b50 --- /dev/null +++ b/app/api/service/dataBuilderService.py @@ -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}' diff --git a/app/api/service/planService.py b/app/api/service/planService.py new file mode 100644 index 0000000..845a837 --- /dev/null +++ b/app/api/service/planService.py @@ -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) diff --git a/app/api/service/productService.py b/app/api/service/productService.py new file mode 100644 index 0000000..591c82b --- /dev/null +++ b/app/api/service/productService.py @@ -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) diff --git a/app/api/service/projectHookService.py b/app/api/service/projectHookService.py new file mode 100644 index 0000000..874e8d3 --- /dev/null +++ b/app/api/service/projectHookService.py @@ -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) \ No newline at end of file diff --git a/app/api/service/projectService.py b/app/api/service/projectService.py new file mode 100644 index 0000000..b64686c --- /dev/null +++ b/app/api/service/projectService.py @@ -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) diff --git a/app/api/service/rbacService.py b/app/api/service/rbacService.py new file mode 100644 index 0000000..0a8abf7 --- /dev/null +++ b/app/api/service/rbacService.py @@ -0,0 +1,103 @@ +# encoding: UTF-8 +from ..dao.rbacDao import RbacDao + + +def has_permission(permission_code, permission_codes): + if not permission_code: + return True + if not permission_codes: + return False + if permission_code in permission_codes: + return True + if '*:*' in permission_codes: + return True + if ':' in permission_code: + module_code = permission_code.split(':', 1)[0] + if f'{module_code}:*' in permission_codes: + return True + if '_' in module_code: + parent_module_code = module_code.split('_', 1)[0] + if f'{parent_module_code}:*' in permission_codes: + return True + return False + + +class RbacService(object): + @staticmethod + def create(session, model_cls, add_info): + return RbacDao.create(session, model_cls, add_info) + + @staticmethod + def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True): + return RbacDao.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 RbacDao.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 RbacDao.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 RbacDao.delete_by_id(session, model_cls, obj_id) + + @staticmethod + def assign_permissions(session, role_ids, permission_id): + return RbacDao.assign_permissions_to_roles(session, role_ids, permission_id) + + @staticmethod + def assign_menus(session, role_id, menu_ids): + return RbacDao.replace_role_menus(session, role_id, menu_ids) + + @staticmethod + def get_role_permission_ids(session, role_id): + return RbacDao.get_role_permission_ids(session, role_id) + + @staticmethod + def get_role_menu_ids(session, role_id): + return RbacDao.get_role_menu_ids(session, role_id) + + @staticmethod + def build_menu_tree(session, filters, role_ids=None, menu_ids=None): + items = RbacDao.get_menu_tree_items(session, filters) + visible_ids = set() + if not role_ids and not menu_ids: + visible_ids = {item.id for item in items} + else: + role_menu_ids = set(menu_ids or []) + if role_ids: + for role_id in role_ids: + role_menu_ids.update(RbacDao.get_role_menu_ids(session, role_id)) + visible_ids = set(role_menu_ids) + item_by_id = {item.id: item for item in items} + for item_id in list(visible_ids): + if item_id not in item_by_id: + continue + parent_id = item_by_id[item_id].parent_id + while parent_id and parent_id in item_by_id: + if parent_id in visible_ids: + break + visible_ids.add(parent_id) + parent_id = item_by_id[parent_id].parent_id + item_map = {} + roots = [] + for item in items: + if item.id not in visible_ids: + continue + item_dict = item.to_dict() + item_dict['children'] = [] + item_map[item.id] = item_dict + for item in items: + if item.id not in item_map: + continue + if item.parent_id and item.parent_id in item_map: + item_map[item.parent_id]['children'].append(item_map[item.id]) + else: + roots.append(item_map[item.id]) + return roots + + @staticmethod + def get_role_permission_codes(session, role_ids): + return RbacDao.get_role_permission_codes(session, role_ids) diff --git a/app/api/service/reportService.py b/app/api/service/reportService.py new file mode 100644 index 0000000..6a8141a --- /dev/null +++ b/app/api/service/reportService.py @@ -0,0 +1,46 @@ +# encoding: UTF-8 +from ..dao.planDao import PlanDao +from ..dao.projectDao import ProjectDao +from ..dao.reportDao import ReportDao +from ..model.planModel import TestPlan +from ..model.reportModel import Report + + +class ReportService(object): + @staticmethod + def create(session, model_cls, add_info): + return ReportDao.create(session, model_cls, add_info) + + @staticmethod + def get_by_id(session, model_cls, obj_id): + return ReportDao.get_by_id(session, model_cls, obj_id) + + @staticmethod + def list_by_filters(session, model_cls, filter_list, page_num=1, page_size=20, order_column=None, asc=False): + return ReportDao.list_by_filters(session, model_cls, filter_list, int(page_num), int(page_size), order_column, asc) + + @staticmethod + def generate_report(session, plan_id, generated_by=None): + plan = PlanDao.get_by_id(session, TestPlan, plan_id) + if not plan: + return 0, '未查询到对应计划!' + project = ProjectDao.get_by_id(session, ProjectDao.project_model(), plan.project_id) + if not project: + return 0, '未查询到对应项目!' + # 复用计划统计,保证计划详情和报告中的指标口径一致。 + stats = PlanDao.plan_stats(session, plan_id) + # MVP 阶段先生成简单 HTML,后续可替换为模板渲染器。 + content = '

{}

总用例:{}

通过率:{}%

'.format( + plan.name, stats['total_cases'], stats['pass_rate'] + ) + add_info = { + 'plan_id': int(plan_id), + 'project_id': plan.project_id, + 'product_id': project.product_id, + 'name': '{}_报告'.format(plan.name), + 'report_type': 1, + 'summary': stats, + 'content': content, + 'generated_by': generated_by + } + return ReportDao.create(session, Report, add_info) diff --git a/app/api/service/userService.py b/app/api/service/userService.py new file mode 100644 index 0000000..a06cb7a --- /dev/null +++ b/app/api/service/userService.py @@ -0,0 +1,58 @@ +# encoding: UTF-8 +from ..dao.userDao import UserDao +from ..dao.rbacDao import RbacDao + + +class UserService(object): + @staticmethod + def create(session, model_cls, add_info): + return UserDao.create(session, model_cls, add_info) + + @staticmethod + def update_by_id(session, model_cls, obj_id, update_info, soft_delete=True): + return UserDao.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 UserDao.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 UserDao.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 UserDao.delete_by_id(session, model_cls, obj_id) + + @staticmethod + def assign_roles(session, user_id, role_ids): + return UserDao.replace_user_roles(session, user_id, role_ids) + + @staticmethod + def get_user_role_ids(session, user_id): + return UserDao.get_user_role_ids(session, user_id) + + @staticmethod + def get_user_roles_map(session, user_ids): + user_role_map = UserDao.get_user_roles(session, user_ids) + role_ids = list({role_id for role_list in user_role_map.values() for role_id in role_list}) + role_name_map = RbacDao.get_role_names_map(session, role_ids) + ret = {} + for user_id, ids in user_role_map.items(): + ret[user_id] = { + 'role_ids': ids, + 'role_names': [role_name_map.get(role_id, '') for role_id in ids] + } + return ret + + @staticmethod + def get_by_username(session, username): + return UserDao.get_by_username(session, username) + + @staticmethod + def get_user_info_map(session, user_ids): + return UserDao.get_user_info_map(session, user_ids) + + @staticmethod + def update_last_login_time(session, user_id): + return UserDao.update_last_login_time(session, user_id) diff --git a/app/api/utils/__pycache__/__init__.cpython-38.pyc b/app/api/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7db1d4c12025101ad91a3788ab0eb10f9bec3b0 GIT binary patch literal 141 zcmWIL<>g`k0*@ODlR)%i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!Gfid9TiMrCD8 zYFb)qc8P9gUP)?ET4Hi)OkzO+5NF1emSpA>$Hd2H=4F<|$LkeT-r}&y%}*)KNwou+ I^%;m604(hxO8@`> literal 0 HcmV?d00001 diff --git a/app/api/utils/__pycache__/authMiddleware.cpython-38.pyc b/app/api/utils/__pycache__/authMiddleware.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..464478e757da57bbdb3d2a0983cadf6fa0324514 GIT binary patch literal 4802 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 K;tV=Hj`cq&lZf8{ literal 0 HcmV?d00001 diff --git a/app/api/utils/authMiddleware.py b/app/api/utils/authMiddleware.py new file mode 100644 index 0000000..35a7ce4 --- /dev/null +++ b/app/api/utils/authMiddleware.py @@ -0,0 +1,152 @@ +# encoding: UTF-8 +import json +import uuid +from functools import wraps + +import redis +from sqlalchemy.exc import OperationalError +from flask import request, g + +from const import REDIS_URL +from common.apiResponse import ApiResponse +from ..service.userService import UserService +from ..service.rbacService import RbacService +from ..model.userModel import User +from common.sqlSession import SqlSession + +TOKEN_PREFIX = 'effekt:token:' +TOKEN_CONTEXT_PREFIX = 'effekt:token:ctx:' +TOKEN_EXPIRE_SECONDS = 7200 +TOKEN_REFRESH_THRESHOLD_SECONDS = 1800 +TOKEN_CONTEXT_EXPIRE_SECONDS = 300 +WHITELIST_PATHS = ['/it/api/auth/login', '/it/api/auth/register'] + +_redis_client = redis.from_url(REDIS_URL, decode_responses=True) +_redis_client.ping() + + +def create_token(user_id): + token = uuid.uuid4().hex + key = TOKEN_PREFIX + token + _redis_client.setex(key, TOKEN_EXPIRE_SECONDS, str(user_id)) + return token, TOKEN_EXPIRE_SECONDS + + +def get_token_ttl(token): + return _redis_client.ttl(TOKEN_PREFIX + token) + + +def refresh_token_if_needed(token): + ttl = get_token_ttl(token) + if ttl != -2 and ttl < TOKEN_REFRESH_THRESHOLD_SECONDS: + _redis_client.expire(TOKEN_PREFIX + token, TOKEN_EXPIRE_SECONDS) + return TOKEN_EXPIRE_SECONDS + return ttl + + +def get_current_user_id(token): + user_id = _redis_client.get(TOKEN_PREFIX + token) + return int(user_id) if user_id else 0 + + +def parse_token(): + return request.headers.get('accessToken') or request.headers.get('accesstoken') or request.headers.get('Authorization', '').replace('Bearer ', '') + + +def get_token_context(token): + context_str = _redis_client.get(TOKEN_CONTEXT_PREFIX + token) + return json.loads(context_str) if context_str else None + + +def cache_token_context(token, user, role_ids, permission_codes): + _redis_client.setex(TOKEN_CONTEXT_PREFIX + token, TOKEN_CONTEXT_EXPIRE_SECONDS, json.dumps({ + 'user': user.to_dict(), + 'role_ids': role_ids, + 'permission_codes': permission_codes + }, default=str)) + + +def login_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + token = parse_token() + if not token: + return ApiResponse.build_failure(40004, msg='未登录或缺少token!') + user_id = get_current_user_id(token) + if not user_id: + return ApiResponse.build_failure(40004, msg='token无效或已过期!') + session = None + try: + token_context = get_token_context(token) + if token_context: + g.current_user_id = user_id + g.current_user = token_context.get('user', {}) + g.current_role_ids = token_context.get('role_ids', []) + g.current_permission_codes = token_context.get('permission_codes', []) + g.current_token = token + g.current_token_ttl = refresh_token_if_needed(token) + return func(*args, **kwargs) + session = SqlSession() + user = UserService.get_by_id(session, User, user_id) + if not user: + return ApiResponse.build_failure(40011, msg='未查询到对应用户!') + role_ids = UserService.get_user_role_ids(session, user_id) + permission_codes = RbacService.get_role_permission_codes(session, role_ids) + cache_token_context(token, user, role_ids, permission_codes) + g.current_user_id = user_id + g.current_user = user + g.current_role_ids = role_ids + g.current_permission_codes = permission_codes + g.current_token = token + g.current_token_ttl = refresh_token_if_needed(token) + return func(*args, **kwargs) + except OperationalError: + return ApiResponse.build_failure(40008, msg='数据库连接超时,请稍后重试!') + finally: + if session: + session.close() + return wrapper + + +def has_permission(permission_code, permission_codes): + if not permission_code: + return True + if not permission_codes: + return False + if permission_code in permission_codes: + return True + if '*:*' in permission_codes: + return True + if ':' in permission_code: + module_code = permission_code.split(':', 1)[0] + if f'{module_code}:*' in permission_codes: + return True + if '_' in module_code: + parent_module_code = module_code.split('_', 1)[0] + if f'{parent_module_code}:*' in permission_codes: + return True + return False + + +def permission_required(permission_code): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if not getattr(g, 'current_user_id', None): + return ApiResponse.build_failure(40004, msg='未登录或缺少token!') + current_permission_codes = getattr(g, 'current_permission_codes', []) + if not has_permission(permission_code, current_permission_codes): + return ApiResponse.build_failure(40004, msg='无权限访问该接口!') + return func(*args, **kwargs) + return wrapper + return decorator + + +def should_skip_auth(path): + return path in WHITELIST_PATHS + + +def logout_token(token): + if token: + _redis_client.delete(TOKEN_PREFIX + token) + _redis_client.delete(TOKEN_CONTEXT_PREFIX + token) diff --git a/app/api/views.py b/app/api/views.py index f2c6412..c0b73b0 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -1,20 +1,47 @@ # encoding: UTF-8 +from sqlalchemy.exc import OperationalError from flask import Blueprint, request +import traceback from common.apiResponse import ApiResponse +from logger import logger +from .utils.authMiddleware import login_required, permission_required, should_skip_auth from .controller.updateSqlProjectController import UpdateSqlProjectController - +from .controller.projectController import ProjectController +from .controller.caseController import CaseController +from .controller.planController import PlanController +from .controller.reportController import ReportController +from .controller.dataBuilderController import DataBuilderController +from .controller.productController import ProductController +from .controller.rbacController import RbacController +from .controller.userController import UserController +from .controller.bugController import BugController, BugUploadController +from .controller.projectHookController import ProjectHookController api = Blueprint('api', __name__) +@api.before_request +def api_before_request(): + if request.method == 'OPTIONS' or should_skip_auth(request.path): + return None + token = request.headers.get('accessToken') or request.headers.get('accesstoken') or request.headers.get('Authorization') + if not token: + return ApiResponse.build_failure(40004, msg='未登录或缺少token!') + return None + + @api.route('/list', methods=['GET']) +@login_required +@permission_required('sql_project:list') def get_list(): request_args = request.args controller = UpdateSqlProjectController(request_args) try: ret = controller.query_smart_manage_sql_data() return ApiResponse.build_success(20000, data=ret) + except OperationalError: + return ApiResponse.build_failure(40008, msg='数据库连接超时,请稍后重试!') except Exception as e: from logger import logger logger.exception(f'get_list failed, args={dict(request_args)}, err={e}') @@ -22,6 +49,8 @@ def get_list(): @api.route('/create', methods=['POST']) +@login_required +@permission_required('sql_project:create') def create_sql_project(): req_json = request.get_json() or {} controller = UpdateSqlProjectController(req_json) @@ -32,6 +61,8 @@ def create_sql_project(): @api.route('/detail', methods=['GET']) +@login_required +@permission_required('sql_project:detail') def get_sql_project_detail(): request_args = request.args controller = UpdateSqlProjectController(request_args) @@ -42,6 +73,8 @@ def get_sql_project_detail(): @api.route('/delete', methods=['POST']) +@login_required +@permission_required('sql_project:delete') def delete_sql_project(): req_json = request.get_json() or {} controller = UpdateSqlProjectController(req_json) @@ -52,10 +85,1287 @@ def delete_sql_project(): @api.route('/execute', methods=['POST']) +@login_required +@permission_required('sql_project:execute') def execute_sql_project(): + """按 SQL 配置中的项目和环境执行目标 SQL。""" req_json = request.get_json() or {} controller = UpdateSqlProjectController(req_json) ret, err_msg = controller.execute_sql_project() if err_msg: return ApiResponse.build_failure(40009, msg=err_msg) return ApiResponse.build_success(20000, data=ret) + + +@api.route('/project/list', methods=['GET']) +@login_required +@permission_required('project:list') +def project_list(): + controller = ProjectController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.project_list()) + finally: + controller.close_session() + + +@api.route('/project/detail', methods=['GET']) +@login_required +@permission_required('project:detail') +def project_detail(): + controller = ProjectController(request.args) + try: + ret, err_msg = controller.project_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/project/create', methods=['POST']) +@login_required +@permission_required('project:create') +def project_create(): + controller = ProjectController(request.get_json() or {}) + try: + create_id, err_msg = controller.project_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/project/update', methods=['POST']) +@login_required +@permission_required('project:update') +def project_update(): + controller = ProjectController(request.get_json() or {}) + try: + update_id, err_msg = controller.project_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/project/delete', methods=['POST']) +@login_required +@permission_required('project:delete') +def project_delete(): + controller = ProjectController(request.get_json() or {}) + try: + delete_id, err_msg = controller.project_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/environment/list', methods=['GET']) +@login_required +@permission_required('environment:list') +def environment_list(): + """分页查询环境配置列表。""" + controller = ProjectController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.environment_list()) + finally: + controller.close_session() + + +@api.route('/environment/create', methods=['POST']) +@login_required +@permission_required('environment:create') +def environment_create(): + controller = ProjectController(request.get_json() or {}) + try: + create_id, err_msg = controller.environment_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/environment/update', methods=['POST']) +@login_required +@permission_required('environment:update') +def environment_update(): + controller = ProjectController(request.get_json() or {}) + try: + update_id, err_msg = controller.environment_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/environment/delete', methods=['POST']) +@login_required +@permission_required('environment:delete') +def environment_delete(): + controller = ProjectController(request.get_json() or {}) + try: + delete_id, err_msg = controller.environment_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/project/member/list', methods=['GET']) +@login_required +@permission_required('project_member:list') +def project_member_list(): + controller = ProjectController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.member_list()) + finally: + controller.close_session() + + +@api.route('/project/member/create', methods=['POST']) +@login_required +@permission_required('project_member:create') +def project_member_create(): + """批量添加项目成员。""" + controller = ProjectController(request.get_json() or {}) + try: + result, err_msg = controller.member_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': result}) + finally: + controller.close_session() + + +@api.route('/project/hook/list', methods=['GET']) +@login_required +@permission_required('project_hook:list') +def project_hook_list(): + controller = ProjectHookController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.hook_list()) + finally: + controller.close_session() + + +@api.route('/project/hook/detail', methods=['GET']) +@login_required +@permission_required('project_hook:detail') +def project_hook_detail(): + controller = ProjectHookController(request.args) + try: + ret, err_msg = controller.hook_detail() + if err_msg: + return ApiResponse.build_failure(40016, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/project/hook/create', methods=['POST']) +@login_required +@permission_required('project_hook:create') +def project_hook_create(): + controller = ProjectHookController(request.get_json() or {}) + try: + hook_id, err_msg = controller.hook_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': hook_id}) + finally: + controller.close_session() + + +@api.route('/project/hook/update', methods=['POST']) +@login_required +@permission_required('project_hook:update') +def project_hook_update(): + controller = ProjectHookController(request.get_json() or {}) + try: + hook_id, err_msg = controller.hook_update() + if err_msg: + return ApiResponse.build_failure(40010, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': hook_id}) + finally: + controller.close_session() + + +@api.route('/project/hook/delete', methods=['POST']) +@login_required +@permission_required('project_hook:delete') +def project_hook_delete(): + controller = ProjectHookController(request.get_json() or {}) + try: + hook_id, err_msg = controller.hook_delete() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': hook_id}) + finally: + controller.close_session() + + +@api.route('/project/hook/send', methods=['POST']) +@login_required +@permission_required('project_hook:send') +def project_hook_send(): + controller = ProjectHookController(request.get_json() or {}) + try: + success, result = controller.hook_send() + if not success: + if isinstance(result, str): + return ApiResponse.build_failure(40012, msg=result) + elif isinstance(result, list) and result: + errors = [r.get('error') for r in result if not r.get('success') and r.get('error')] + error_msg = errors[0] if errors else '发送失败' + return ApiResponse.build_failure(40012, msg=error_msg, data=result) + else: + return ApiResponse.build_failure(40012, msg='发送失败', data=result) + return ApiResponse.build_success(20000, data=result) + finally: + controller.close_session() + + +@api.route('/product/list', methods=['GET']) +@login_required +@permission_required('product:list') +def product_list(): + controller = ProductController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.product_list()) + finally: + controller.close_session() + + +@api.route('/product/detail', methods=['GET']) +@login_required +@permission_required('product:detail') +def product_detail(): + controller = ProductController(request.args) + try: + ret, err_msg = controller.product_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/product/create', methods=['POST']) +@login_required +@permission_required('product:create') +def product_create(): + controller = ProductController(request.get_json() or {}) + try: + create_id, err_msg = controller.product_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/product/update', methods=['POST']) +@login_required +@permission_required('product:update') +def product_update(): + controller = ProductController(request.get_json() or {}) + try: + update_id, err_msg = controller.product_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/product/delete', methods=['POST']) +@login_required +@permission_required('product:delete') +def product_delete(): + controller = ProductController(request.get_json() or {}) + try: + delete_id, err_msg = controller.product_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/module/tree', methods=['GET']) +@login_required +@permission_required('module:list') +def module_tree(): + try: + return ApiResponse.build_success(20000, data=CaseController(request.args).module_list()) + except Exception as e: + logger.error(f'module_tree异常:{str(e)}, 请求参数:{dict(request.args)}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40009, msg=f'查询失败:{str(e)[:100]}') + + +@api.route('/module/create', methods=['POST']) +@login_required +@permission_required('module:create') +def module_create(): + try: + create_id, err_msg = CaseController(request.get_json() or {}).module_create() + if err_msg: + logger.warning(f'module_create失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + except Exception as e: + logger.error(f'module_create异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40009, msg=f'创建失败:{str(e)[:100]}') + + +@api.route('/module/update', methods=['POST']) +@login_required +@permission_required('module:update') +def module_update(): + try: + update_id, err_msg = CaseController(request.get_json() or {}).module_update() + if err_msg: + logger.warning(f'module_update失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + except Exception as e: + logger.error(f'module_update异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40012, msg=f'更新失败:{str(e)[:100]}') + + +@api.route('/module/delete', methods=['POST']) +@login_required +@permission_required('module:delete') +def module_delete(): + try: + delete_id, err_msg = CaseController(request.get_json() or {}).module_delete() + if err_msg: + logger.warning(f'module_delete失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + except Exception as e: + logger.error(f'module_delete异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40012, msg=f'删除失败:{str(e)[:100]}') + + +@api.route('/case/list', methods=['GET']) +@login_required +@permission_required('case:list') +def case_list(): + try: + controller = CaseController(request.args) + return ApiResponse.build_success(20000, data=controller.case_list()) + except Exception as e: + logger.error(f'case_list异常:{str(e)}, 请求参数:{dict(request.args)}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40009, msg=f'查询失败:{str(e)[:100]}') + + +@api.route('/case/detail', methods=['GET']) +@login_required +@permission_required('case:detail') +def case_detail(): + ret, err_msg = CaseController(request.args).case_detail() + if err_msg: + logger.warning(f'case_detail失败:{err_msg}, 请求参数:{dict(request.args)}') + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +@api.route('/case/create', methods=['POST']) +@login_required +@permission_required('case:create') +def case_create(): + try: + create_id, err_msg = CaseController(request.get_json() or {}).case_create() + if err_msg: + logger.warning(f'case_create失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + except Exception as e: + logger.error(f'case_create异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40009, msg=f'创建失败:{str(e)[:100]}') + + +@api.route('/case/update', methods=['POST']) +@login_required +@permission_required('case:update') +def case_update(): + try: + update_id, err_msg = CaseController(request.get_json() or {}).case_update() + if err_msg: + logger.warning(f'case_update失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + except Exception as e: + logger.error(f'case_update异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40012, msg=f'更新失败:{str(e)[:100]}') + + +@api.route('/case/delete', methods=['POST']) +@login_required +@permission_required('case:delete') +def case_delete(): + try: + delete_id, err_msg = CaseController(request.get_json() or {}).case_delete() + if err_msg: + logger.warning(f'case_delete失败:{err_msg}, 请求参数:{request.get_json()}') + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + except Exception as e: + logger.error(f'case_delete异常:{str(e)}, 请求参数:{request.get_json()}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40012, msg=f'删除失败:{str(e)[:100]}') + + +@api.route('/case/import', methods=['POST']) +@login_required +@permission_required('case:create') +def case_import(): + import os + from flask import send_file + + try: + if 'file' not in request.files: + logger.warning('case_import失败:请上传文件') + return ApiResponse.build_failure(40009, msg='请上传文件') + + file = request.files['file'] + if file.filename == '': + logger.warning('case_import失败:请选择文件') + return ApiResponse.build_failure(40009, msg='请选择文件') + + project_id = request.form.get('projectId') + if not project_id: + logger.warning('case_import失败:projectId 为必传参数') + return ApiResponse.build_failure(40009, msg='projectId 为必传参数') + + # 获取项目根目录 + root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + attachment_dir = os.path.join(root_dir, 'attachment') + # 确保 attachment 目录存在 + os.makedirs(attachment_dir, exist_ok=True) + temp_path = os.path.join(attachment_dir, 'temp_import.xlsx') + file.save(temp_path) + + controller = CaseController({}) + try: + success_count, err_msg = controller.case_import(temp_path, project_id) + if err_msg and ('失败' in err_msg or success_count == 0): + logger.warning(f'case_import失败:{err_msg}, projectId={project_id}') + return ApiResponse.build_failure(40009, msg=err_msg) + logger.info(f'case_import成功:成功{success_count}条, projectId={project_id}') + return ApiResponse.build_success(20000, data={'successCount': success_count, 'message': err_msg}) + finally: + controller.close_session() + if os.path.exists(temp_path): + os.remove(temp_path) + except Exception as e: + logger.error(f'case_import异常:{str(e)}, projectId={request.form.get("projectId")}, 堆栈:{traceback.format_exc()}') + return ApiResponse.build_failure(40009, msg=f'导入失败:{str(e)[:100]}') + + +@api.route('/import/template', methods=['GET']) +@login_required +def import_template(): + import os + from flask import send_file + + template_path = CaseController.get_template_path() + if not os.path.exists(template_path): + return ApiResponse.build_failure(40011, msg='模板文件不存在') + + return send_file(template_path, as_attachment=True, attachment_filename='测试用例模版.xlsx') + + +@api.route('/case/snapshot/create', methods=['POST']) +@login_required +@permission_required('case_snapshot:create') +def case_snapshot_create(): + create_id, err_msg = CaseController(request.get_json() or {}).snapshot_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + + +@api.route('/case/snapshot/list', methods=['GET']) +@login_required +@permission_required('case_snapshot:list') +def case_snapshot_list(): + return ApiResponse.build_success(20000, data=CaseController(request.args).snapshot_list()) + + +@api.route('/case/review/create', methods=['POST']) +@login_required +@permission_required('case_review:create') +def case_review_create(): + create_id, err_msg = CaseController(request.get_json() or {}).review_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + + +@api.route('/case/review/update', methods=['POST']) +@login_required +@permission_required('case_review:update') +def case_review_update(): + update_id, err_msg = CaseController(request.get_json() or {}).review_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + + +@api.route('/case/review/list', methods=['GET']) +@login_required +@permission_required('case_review:list') +def case_review_list(): + return ApiResponse.build_success(20000, data=CaseController(request.args).review_list()) + + +@api.route('/plan/list', methods=['GET']) +@login_required +@permission_required('plan:list') +def plan_list(): + return ApiResponse.build_success(20000, data=PlanController(request.args).plan_list()) + + +@api.route('/plan/detail', methods=['GET']) +@login_required +@permission_required('plan:detail') +def plan_detail(): + ret, err_msg = PlanController(request.args).plan_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +@api.route('/plan/create', methods=['POST']) +@login_required +@permission_required('plan:create') +def plan_create(): + create_id, err_msg = PlanController(request.get_json() or {}).plan_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + + +@api.route('/plan/update', methods=['POST']) +@login_required +@permission_required('plan:update') +def plan_update(): + update_id, err_msg = PlanController(request.get_json() or {}).plan_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + + +@api.route('/plan/delete', methods=['POST']) +@login_required +@permission_required('plan:delete') +def plan_delete(): + delete_id, err_msg = PlanController(request.get_json() or {}).plan_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + + +@api.route('/plan/round/create', methods=['POST']) +@login_required +@permission_required('plan_round:create') +def plan_round_create(): + create_id, err_msg = PlanController(request.get_json() or {}).round_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + + +@api.route('/plan/round/list', methods=['GET']) +@login_required +@permission_required('plan_round:list') +def plan_round_list(): + return ApiResponse.build_success(20000, data=PlanController(request.args).round_list()) + + +@api.route('/plan/case/add', methods=['POST']) +@login_required +@permission_required('plan_case:add') +def plan_case_add(): + added_count, err_msg = PlanController(request.get_json() or {}).plan_case_add() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'addedCount': added_count}) + + +@api.route('/plan/case/list', methods=['GET']) +@login_required +@permission_required('plan_case:list') +def plan_case_list(): + return ApiResponse.build_success(20000, data=PlanController(request.args).plan_case_list()) + + +@api.route('/plan/case/execute', methods=['POST']) +@login_required +@permission_required('plan_case:execute') +def plan_case_execute(): + update_id, err_msg = PlanController(request.get_json() or {}).plan_case_execute() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + + +@api.route('/plan/progress', methods=['GET']) +@login_required +@permission_required('plan:progress') +def plan_progress(): + ret, err_msg = PlanController(request.args).progress() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +# ========================= +# 报告接口 +# ========================= + + +@api.route('/report/list', methods=['GET']) +@login_required +@permission_required('report:list') +def report_list(): + """分页查询测试报告列表。""" + controller = ReportController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.report_list()) + finally: + controller.close_session() + + +@api.route('/report/detail', methods=['GET']) +@login_required +@permission_required('report:detail') +def report_detail(): + controller = ReportController(request.args) + try: + ret, err_msg = controller.report_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/report/generate', methods=['POST']) +@login_required +@permission_required('report:generate') +def report_generate(): + controller = ReportController(request.get_json() or {}) + try: + create_id, err_msg = controller.report_generate() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +# ========================= +# 造数器与造数任务接口 +# ========================= + + +@api.route('/data/builder/list', methods=['GET']) +@login_required +@permission_required('data_builder:list') +def data_builder_list(): + """分页查询造数器列表。""" + return ApiResponse.build_success(20000, data=DataBuilderController(request.args).builder_list()) + + +@api.route('/data/builder/detail', methods=['GET']) +@login_required +@permission_required('data_builder:detail') +def data_builder_detail(): + ret, err_msg = DataBuilderController(request.args).builder_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +@api.route('/data/builder/create', methods=['POST']) +@login_required +@permission_required('data_builder:create') +def data_builder_create(): + create_id, err_msg = DataBuilderController(request.get_json() or {}).builder_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + + +@api.route('/data/builder/update', methods=['POST']) +@login_required +@permission_required('data_builder:update') +def data_builder_update(): + update_id, err_msg = DataBuilderController(request.get_json() or {}).builder_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + + +@api.route('/data/builder/delete', methods=['POST']) +@login_required +@permission_required('data_builder:delete') +def data_builder_delete(): + delete_id, err_msg = DataBuilderController(request.get_json() or {}).builder_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + + +@api.route('/data/builder/execute', methods=['POST']) +@login_required +@permission_required('data_builder:execute') +def data_builder_execute(): + ret, err_msg = DataBuilderController(request.get_json() or {}).builder_execute() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +@api.route('/data/task/status', methods=['GET']) +@login_required +@permission_required('data_task:status') +def data_task_status(): + ret, err_msg = DataBuilderController(request.args).task_status() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + + +@api.route('/role/list', methods=['GET']) +@login_required +@permission_required('role:list') +def role_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.role_list()) + finally: + controller.close_session() + + +@api.route('/role/page/list', methods=['GET']) +@login_required +@permission_required('role:list') +def role_page_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.role_page_list()) + finally: + controller.close_session() + + +@api.route('/role/detail', methods=['GET']) +@login_required +@permission_required('role:detail') +def role_detail(): + controller = RbacController(request.args) + try: + ret, err_msg = controller.role_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/role/create', methods=['POST']) +@login_required +@permission_required('role:create') +def role_create(): + controller = RbacController(request.get_json() or {}) + try: + create_id, err_msg = controller.role_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/role/update', methods=['POST']) +@login_required +@permission_required('role:update') +def role_update(): + controller = RbacController(request.get_json() or {}) + try: + update_id, err_msg = controller.role_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/role/delete', methods=['POST']) +@login_required +@permission_required('role:delete') +def role_delete(): + controller = RbacController(request.get_json() or {}) + try: + delete_id, err_msg = controller.role_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/permission/list', methods=['GET']) +@login_required +@permission_required('permission:list') +def permission_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.permission_list()) + finally: + controller.close_session() + + +@api.route('/permission/detail', methods=['GET']) +@login_required +@permission_required('permission:detail') +def permission_detail(): + controller = RbacController(request.args) + try: + ret, err_msg = controller.permission_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/permission/create', methods=['POST']) +@login_required +@permission_required('permission:create') +def permission_create(): + controller = RbacController(request.get_json() or {}) + try: + create_id, err_msg = controller.permission_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/permission/update', methods=['POST']) +@login_required +@permission_required('permission:update') +def permission_update(): + controller = RbacController(request.get_json() or {}) + try: + update_id, err_msg = controller.permission_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/permission/delete', methods=['POST']) +@login_required +@permission_required('permission:delete') +def permission_delete(): + controller = RbacController(request.get_json() or {}) + try: + delete_id, err_msg = controller.permission_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/menu/tree', methods=['GET']) +@login_required +@permission_required('menu:list') +def menu_tree(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.menu_tree()) + finally: + controller.close_session() + + +@api.route('/menu/current/list', methods=['GET']) +@login_required +def current_menu_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.current_menu_list()) + finally: + controller.close_session() + + +@api.route('/role/menu/tree', methods=['GET']) +@login_required +@permission_required('role_menu:list') +def role_menu_tree(): + controller = RbacController(request.args) + try: + ret, err_msg = controller.role_menu_tree() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/menu/detail', methods=['GET']) +@login_required +@permission_required('menu:detail') +def menu_detail(): + controller = RbacController(request.args) + try: + ret, err_msg = controller.menu_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/menu/create', methods=['POST']) +@login_required +@permission_required('menu:create') +def menu_create(): + controller = RbacController(request.get_json() or {}) + try: + create_id, err_msg = controller.menu_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/menu/update', methods=['POST']) +@login_required +@permission_required('menu:update') +def menu_update(): + controller = RbacController(request.get_json() or {}) + try: + update_id, err_msg = controller.menu_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/menu/delete', methods=['POST']) +@login_required +@permission_required('menu:delete') +def menu_delete(): + controller = RbacController(request.get_json() or {}) + try: + delete_id, err_msg = controller.menu_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/role/permission/list', methods=['GET']) +@login_required +@permission_required('role_permission:list') +def role_permission_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.role_permission_list()) + finally: + controller.close_session() + + +@api.route('/role/permission/assign', methods=['POST']) +@login_required +@permission_required('role_permission:assign') +def role_permission_assign(): + controller = RbacController(request.get_json() or {}) + try: + role_id, err_msg = controller.role_permission_assign() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': role_id}) + finally: + controller.close_session() + + +@api.route('/role/menu/list', methods=['GET']) +@login_required +@permission_required('role_menu:list') +def role_menu_list(): + controller = RbacController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.role_menu_list()) + finally: + controller.close_session() + + +@api.route('/role/menu/assign', methods=['POST']) +@login_required +@permission_required('role_menu:assign') +def role_menu_assign(): + controller = RbacController(request.get_json() or {}) + try: + role_id, err_msg = controller.role_menu_assign() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': role_id}) + finally: + controller.close_session() + + +@api.route('/user/list', methods=['GET']) +@login_required +@permission_required('user:list') +def user_list(): + controller = UserController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.user_list()) + finally: + controller.close_session() + + +@api.route('/user/detail', methods=['GET']) +@login_required +@permission_required('user:detail') +def user_detail(): + controller = UserController(request.args) + try: + ret, err_msg = controller.user_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/user/create', methods=['POST']) +@login_required +@permission_required('user:create') +def user_create(): + controller = UserController(request.get_json() or {}) + try: + create_id, err_msg = controller.user_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/user/update', methods=['POST']) +@login_required +@permission_required('user:update') +def user_update(): + controller = UserController(request.get_json() or {}) + try: + update_id, err_msg = controller.user_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': update_id}) + finally: + controller.close_session() + + +@api.route('/user/delete', methods=['POST']) +@login_required +@permission_required('user:delete') +def user_delete(): + controller = UserController(request.get_json() or {}) + try: + delete_id, err_msg = controller.user_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': delete_id}) + finally: + controller.close_session() + + +@api.route('/user/role/list', methods=['GET']) +@login_required +@permission_required('user_role:list') +def user_role_list(): + controller = UserController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.user_role_list()) + finally: + controller.close_session() + + +@api.route('/user/role/assign', methods=['POST']) +@login_required +@permission_required('user_role:assign') +def user_role_assign(): + controller = UserController(request.get_json() or {}) + try: + user_id, err_msg = controller.user_role_assign() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': user_id}) + finally: + controller.close_session() + + +@api.route('/auth/register', methods=['POST']) +def auth_register(): + controller = UserController(request.get_json() or {}) + try: + create_id, err_msg = controller.register() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': create_id}) + finally: + controller.close_session() + + +@api.route('/auth/login', methods=['POST']) +def auth_login(): + controller = UserController(request.get_json() or {}) + try: + ret, err_msg = controller.login() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + except OperationalError: + return ApiResponse.build_failure(40011, msg='数据库连接失败,请稍后重试!') + finally: + controller.close_session() + + +@api.route('/bug/list', methods=['GET']) +@login_required +@permission_required('bug:list') +def bug_list(): + controller = BugController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.bug_list()) + finally: + controller.close_session() + + +@api.route('/bug/detail', methods=['GET']) +@login_required +@permission_required('bug:detail') +def bug_detail(): + controller = BugController(request.args) + try: + ret, err_msg = controller.bug_detail() + if err_msg: + return ApiResponse.build_failure(40011, msg=err_msg) + return ApiResponse.build_success(20000, data=ret) + finally: + controller.close_session() + + +@api.route('/bug/create', methods=['POST']) +@login_required +@permission_required('bug:create') +def bug_create(): + controller = BugController(request.get_json() or {}) + try: + bug_id, err_msg = controller.bug_create() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': bug_id}) + finally: + controller.close_session() + + +@api.route('/bug/update', methods=['POST']) +@login_required +@permission_required('bug:update') +def bug_update(): + controller = BugController(request.get_json() or {}) + try: + bug_id, err_msg = controller.bug_update() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': bug_id}) + finally: + controller.close_session() + + +@api.route('/bug/delete', methods=['POST']) +@login_required +@permission_required('bug:delete') +def bug_delete(): + controller = BugController(request.get_json() or {}) + try: + bug_id, err_msg = controller.bug_delete() + if err_msg: + return ApiResponse.build_failure(40012, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': bug_id}) + finally: + controller.close_session() + + +@api.route('/bug/history/add', methods=['POST']) +@login_required +@permission_required('bug:update') +def bug_history_add(): + controller = BugController(request.get_json() or {}) + try: + success, err_msg = controller.bug_history_add() + if err_msg: + return ApiResponse.build_failure(40015, msg=err_msg) + return ApiResponse.build_success(20000, data={'success': success}) + finally: + controller.close_session() + + +@api.route('/bug/comment/add', methods=['POST']) +@login_required +@permission_required('bug:comment') +def bug_comment_add(): + controller = BugController(request.get_json() or {}) + try: + comment_id, err_msg = controller.bug_comment_add() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'id': comment_id}) + finally: + controller.close_session() + + +@api.route('/bug/stats', methods=['GET']) +@login_required +@permission_required('bug:stats') +def bug_stats(): + controller = BugController(request.args) + try: + return ApiResponse.build_success(20000, data=controller.bug_stats()) + finally: + controller.close_session() + + +@api.route('/bug/upload', methods=['POST']) +@login_required +@permission_required('bug:create') +def bug_upload(): + controller = BugUploadController(request) + try: + file_url, err_msg = controller.bug_upload() + if err_msg: + return ApiResponse.build_failure(40009, msg=err_msg) + return ApiResponse.build_success(20000, data={'url': file_url}) + finally: + controller.close_session() + diff --git a/attachment/用例导入模版.xlsx b/attachment/用例导入模版.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..45f306d48f34228d56b427ef4fcfe462b4040e3c GIT binary patch literal 14899 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%0DtmL~Puc&KZm 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 literal 0 HcmV?d00001 diff --git a/bug_api_document.md b/bug_api_document.md new file mode 100644 index 0000000..de189b7 --- /dev/null +++ b/bug_api_document.md @@ -0,0 +1,539 @@ +# Bug 管理系统接口文档 + +## 一、接口清单 + +| 接口路径 | 方法 | 权限 | 说明 | +| ------------------ | ---- | ------------- | ---------- | +| `/bug/list` | GET | `bug:list` | Bug 列表(分页) | +| `/bug/detail` | GET | `bug:detail` | Bug 详情 | +| `/bug/create` | POST | `bug:create` | 创建 Bug | +| `/bug/update` | POST | `bug:update` | 更新 Bug | +| `/bug/delete` | POST | `bug:delete` | 删除 Bug | +| `/bug/comment/add` | POST | `bug:comment` | 添加评论 | +| `/bug/stats` | GET | `bug:stats` | Bug 统计 | +| `/bug/upload` | POST | `bug:create` | 图片上传 | + +*** + +## 二、接口详细说明 + +### 1. Bug 列表 + +**GET /bug/list** + +查询 Bug 列表,支持多维度筛选。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ------------------------- | ------ | -- | ---------- | +| productId / product\_id | Number | 否 | 产品 ID | +| projectId / project\_id | Number | 否 | 项目 ID | +| moduleId / module\_id | Number | 否 | 模块 ID | +| bugType / bug\_type | Number | 否 | Bug 类型 | +| severity | Number | 否 | 严重程度 | +| priority | Number | 否 | 优先级 | +| status | Number | 否 | 状态 | +| assigneeId / assignee\_id | Number | 否 | 负责人 ID | +| keyword | String | 否 | 关键词(标题/描述) | +| pageNo / page | Number | 否 | 页码,默认 1 | +| pageSize / size | Number | 否 | 每页数量,默认 20 | + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "list": [ + { + "id": 1, + "bug_key": "BUG-001", + "title": "登录页面无法加载", + "description": "点击登录按钮后页面无响应", + "bug_type": 1, + "severity": 1, + "priority": 1, + "status": 2, + "assignee_id": 1, + "reporter_id": 2, + "product_id": 1, + "project_id": 1, + "module_id": 1, + "case_id": 101, + "plan_id": 5, + "environment": "test", + "created_time": "2026-05-06 10:00:00", + "updated_time": "2026-05-06 11:00:00" + } + ], + "total": 1 + } +} +``` + +*** + +### 2. Bug 详情 + +**GET /bug/detail** + +查询 Bug 详细信息,包含评论和历史记录。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ---------- | ------ | -- | ------ | +| bugId / id | Number | 是 | Bug ID | + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "id": 1, + "bug_key": "BUG-001", + "title": "登录页面无法加载", + "description": "点击登录按钮后页面无响应...", + "bug_type": 1, + "severity": 1, + "priority": 1, + "status": 2, + "assignee_id": 1, + "reporter_id": 2, + "product_id": 1, + "project_id": 1, + "module_id": 1, + "case_id": 101, + "plan_id": 5, + "environment": "test", + "steps": "1. 打开登录页面\n2. 输入用户名密码\n3. 点击登录", + "attachments": [], + "created_time": "2026-05-06 10:00:00", + "updated_time": "2026-05-06 11:00:00", + "comments": [ + { + "id": 1, + "bug_id": 1, + "content": "已收到,正在处理", + "user_id": 1, + "created_time": "2026-05-06 10:30:00" + } + ], + "history": [ + { + "id": 1, + "bug_id": 1, + "field_name": "status", + "old_value": "0", + "new_value": "2", + "operator_id": 1, + "created_time": "2026-05-06 10:30:00" + } + ] + } +} +``` + +*** + +### 3. 创建 Bug + +**POST /bug/create** + +创建新的 Bug 报告。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ----------------------- | ------ | -- | ----------- | +| title | String | 是 | Bug 标题 | +| description | String | 否 | Bug 描述 | +| bugType / bug\_type | Number | 否 | Bug 类型,默认 1 | +| severity | Number | 否 | 严重程度,默认 2 | +| priority | Number | 否 | 优先级,默认 2 | +| productId / product\_id | Number | 是 | 产品 ID | +| projectId / project\_id | Number | 是 | 项目 ID | +| moduleId / module\_id | Number | 否 | 模块 ID | +| caseId / case\_id | Number | 否 | 关联测试用例 ID | +| planId / plan\_id | Number | 否 | 关联测试计划 ID | +| environment | String | 否 | 测试环境 | +| steps | String | 否 | 复现步骤 | + +#### 请求体示例 + +```json +{ + "title": "登录页面无法加载", + "description": "点击登录按钮后页面无响应", + "bugType": 1, + "severity": 1, + "priority": 1, + "productId": 1, + "projectId": 1, + "moduleId": 1, + "environment": "test", + "steps": "1. 打开登录页面\n2. 输入用户名密码\n3. 点击登录" +} +``` + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "id": 1 + } +} +``` + +*** + +### 4. 更新 Bug + +**POST /bug/update** + +更新 Bug 信息。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ------------------------- | ------ | -- | --------- | +| bugId / id | Number | 是 | Bug ID | +| title | String | 否 | Bug 标题 | +| description | String | 否 | Bug 描述 | +| bugType / bug\_type | Number | 否 | Bug 类型 | +| severity | Number | 否 | 严重程度 | +| priority | Number | 否 | 优先级 | +| status | Number | 否 | 状态 | +| assigneeId / assignee\_id | Number | 否 | 负责人 ID | +| moduleId / module\_id | Number | 否 | 模块 ID | +| caseId / case\_id | Number | 否 | 关联测试用例 ID | +| planId / plan\_id | Number | 否 | 关联测试计划 ID | +| environment | String | 否 | 测试环境 | +| steps | String | 否 | 复现步骤 | + +#### 请求体示例 + +```json +{ + "bugId": 1, + "status": 3, + "assigneeId": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "id": 1 + } +} +``` + +*** + +### 5. 删除 Bug + +**POST /bug/delete** + +软删除 Bug。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ---------- | ------ | -- | ------ | +| bugId / id | Number | 是 | Bug ID | + +#### 请求体示例 + +```json +{ + "bugId": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "id": 1 + } +} +``` + +*** + +### 6. 添加评论 + +**POST /bug/comment/add** + +为 Bug 添加评论。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ---------- | ------ | -- | ------ | +| bugId / id | Number | 是 | Bug ID | +| content | String | 是 | 评论内容 | + +#### 请求体示例 + +```json +{ + "bugId": 1, + "content": "已收到,正在处理" +} +``` + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "id": 1 + } +} +``` + +*** + +### 7. Bug 统计 + +**GET /bug/stats** + +获取 Bug 统计信息。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +| ----------------------- | ------ | -- | ----- | +| productId / product\_id | Number | 否 | 产品 ID | +| projectId / project\_id | Number | 否 | 项目 ID | + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "total": 100, + "new": 10, + "pending": 20, + "in_progress": 30, + "resolved": 25, + "closed": 10, + "rejected": 5, + "by_severity": { + "critical": 15, + "major": 30, + "medium": 40, + "minor": 15 + }, + "by_priority": { + "high": 40, + "medium": 45, + "low": 15 + }, + "by_type": { + "functional": 40, + "ui": 25, + "performance": 15, + "security": 10, + "compatibility": 10 + } + } +} +``` + +*** + +### 8. 图片上传 + +**POST /bug/upload** + +上传 Bug 相关图片,返回图片访问 URL。 + +#### 请求参数 + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | File | 是 | 图片文件 | + +#### 支持的文件格式 + +- png +- jpg / jpeg +- gif +- bmp + +#### 调用示例(curl) + +```bash +curl -X POST "http://39.170.26.156:8888/it/api/bug/upload" \ + -H "accessToken: your_token" \ + -F "file=@screenshot.png" +``` + +#### 响应示例 + +```json +{ + "code": 20000, + "message": "success", + "data": { + "url": "http://39.170.26.156:8888/uploads/bug/bug-20260506100000-abc12345.png" + } +} +``` + +#### 错误响应 + +```json +{ + "code": 40009, + "message": "未找到上传文件" +} +``` + +```json +{ + "code": 40009, + "message": "不支持的文件格式,仅支持:png, jpg, jpeg, gif, bmp" +} +``` + +*** + +## 三、枚举值说明 + +### Bug 类型 (bug\_type) + +| 值 | 名称 | 说明 | +| - | ----- | ---------- | +| 1 | 功能缺陷 | 核心功能不能正常工作 | +| 2 | UI 问题 | 界面显示、交互问题 | +| 3 | 性能问题 | 响应慢、资源占用高 | +| 4 | 安全漏洞 | 安全相关问题 | +| 5 | 兼容性问题 | 浏览器/平台兼容问题 | + +### 严重程度 (severity) + +| 值 | 名称 | 说明 | +| - | -- | --------- | +| 1 | 致命 | 系统崩溃、数据丢失 | +| 2 | 严重 | 核心功能不可用 | +| 3 | 中等 | 功能受限但可用 | +| 4 | 轻微 | 小问题,不影响使用 | + +### 优先级 (priority) + +| 值 | 名称 | 说明 | +| - | -- | ------ | +| 1 | 高 | 需要立即处理 | +| 2 | 中 | 按计划处理 | +| 3 | 低 | 空闲时处理 | + +### 状态 (status) + +| 值 | 名称 | 说明 | +| - | --- | ------------- | +| 0 | 新建 | Bug 刚创建,待审核 | +| 1 | 待处理 | 已确认,等待分配 | +| 2 | 进行中 | 已分配,正在修复 | +| 3 | 已解决 | 修复完成,待验证 | +| 4 | 已关闭 | 验证通过,已关闭 | +| 5 | 已拒绝 | 非 Bug 或重复,已拒绝 | + +*** + +## 四、状态流转规则 + +``` + ┌─────────────────┐ + │ 0-新建 │ + └────────┬────────┘ + │ + ┌──────────────┴──────────────┐ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ 1-待处理 │ │ 5-已拒绝 │ + └──────┬──────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ 2-进行中 │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 3-已解决 │ + └──────┬──────┘ + │ + ▼ + ┌─────────────┐ + │ 4-已关闭 │ + └─────────────┘ +``` + +**转换规则:** + +- 新建 → 待处理 / 已拒绝 +- 待处理 → 进行中 / 已拒绝 +- 进行中 → 已解决 / 待处理 +- 已解决 → 已关闭 / 待处理 +- 已关闭 → 待处理(重新打开) +- 已拒绝 → 待处理(重新打开) + +*** + +## 五、通用状态码 + +| 状态码 | 说明 | +| ----- | ------------ | +| 20000 | 成功 | +| 40004 | 未登录或缺少 token | +| 40009 | 参数错误或新增失败 | +| 40011 | 未查询到对应记录 | +| 40012 | 更新或删除失败 | +| 40013 | 权限不足 | + +*** + +## 六、认证方式 + +所有接口(除登录/注册外)需在请求头携带 `accessToken`: + +```bash +curl -H "accessToken: your_token" http://localhost:8081/it/api/bug/list +``` + +*** + +## 七、权限配置 + +| 权限编码 | 权限名称 | +| ----------- | --------- | +| bug:list | 查看 Bug 列表 | +| bug:detail | 查看 Bug 详情 | +| bug:create | 创建 Bug | +| bug:update | 更新 Bug | +| bug:delete | 删除 Bug | +| bug:comment | 添加评论 | +| bug:stats | 查看统计 | + diff --git a/common/__pycache__/__init__.cpython-38.pyc b/common/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2acdf605eb9bfbf351346b6b2baff28c49778531 GIT binary patch literal 134 zcmWIL<>g`k0+r<3nIQTxh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o2Bd#VV#Mqp~t4 zH7zYQyF@oLuOzi7EipMYCOJPhH$N{XK0Y%qvm`!Vub}c4hfQvNN@-529mtT+K+FID D3ECbJ literal 0 HcmV?d00001 diff --git a/common/__pycache__/apiResponse.cpython-38.pyc b/common/__pycache__/apiResponse.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32b4d99ed5c2fea75e94d3ee300682a34815d500 GIT binary patch literal 1886 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 W((>GzDyHJFtCG^nHE|}E7XJ$Xcp0Jq literal 0 HcmV?d00001 diff --git a/common/__pycache__/dataBuilderExecutor.cpython-38.pyc b/common/__pycache__/dataBuilderExecutor.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57f276cb2341b1396a7395d54cd545bc04c240f8 GIT binary patch literal 3019 zcmbVO-ESOM6`wm_JKori>x4Fi0v3&`jG!h_ODM&%A_(FE$uCy>U^Wcvz2nU|`;|L) zL$cnL?Aj)=#ghUbg=KLQ)vBkJsaOmt45h2H_;8fOJbR1FH;7e6`g=c=+Ss(*&O&UCZ=&R?VVR-(5T+c!UGFZ^Y* zwTSMc>9^A90X)(DYth2xjfG3?8y`nYA4N0E(XEAOer02MuD#M~|KVYK?bG(3u0>ZD zqhHT&+`rXXUTWX?EV_6fV*hz>aeeJ_B6H*VO8e67&ADaB!FXrpVKjd^y7x{r|H1m* zyY0E9^}FwHG-r~)wr zU$5(xfq2H?=ua8}!>F03wHDU^j4`6DQ2Lcn8Gh-UYpZ?Z2}U0GCzO%*rJ&;kou^W{ za!LBRW;?AU%tXKFD-C%_~S zHCP=0Ou{FJ2rAapx!z&~Q3Ftkqjjxm)G;!O_0q%&YnE~226>QKCFk3^oE?|BwY+|3 zA-eOo&4<^c-~VyT5#GK6*BB^QWu+={gUYI8MYgEyWW?2#9S?*tvaT&s5S$=rgHwH; z@JsMgPx{45AuD}l6#P=M*brT`ct4R|B7{&v_JLC=KXz^ZIdnn?MAIFXW_^0?YkOh0 zwfZsW2u9L_;0u2VA_)vOtBV1tHx14%Y7;CDUZ>7l#%MBcV*W%tV{&T_Gfk_doz!Ll zWxU_Ud?uc^d5Y$fxjKctQ~e>1=}cdZ?qA)4@aDT4AAh*M_UWVPnds)b(fk@1gww{| zx%IW1gznT-wDh~q#id3A2-o&!rZQ7Ue*TNbsc*ZPMy8Qt5V-viPX--83lD)v?Gl0n zca0pLVRm?3&65rBG=P?(dz;7OK9o@}gX=P^^fc;xfgGt1P*T_y%I| zouNXuIL@=f--3{Tp=q__d!hd1{SFQl%DHSI90rpJcuaq~h#mc3K!*bQ)WMA2VDMT7 zTQu_7Yx-GpH1P$f)`xY7PJQsGVrLXPC!WW^E=NfZ@gOG=iTF>w+cB9AzUiB6p^IGKy zH)r7v+-jPfB21H3U7wAkEJZp8D4b)(r+H(RV>l)7ba-Kd+qf7ZZygZH0t#3IwU zHL9ImtRqXf2dY1VF`HLzM@zp=>gHx^W%I#<=-v_nI-0(Rn%KVnNqceT=`xDiy7AVX z&D)Vxq1Y>meNVCP zEB2aVKX}S^Ix)8WtBLLXU)sLgYB7ju*A(ByOB^L~jL0iQsQf))mpG0wTDxOWCk_%e zIO6VCX@r)L;jaRDS~;j_31{F-Ks~>@@=IMNbw?g zL4hwQPpK3T%1S9*D+x-Ipj9@iWFBXwa#eu}DwT;cE1y*PD-htKg@lBrA7qE{bJ$U4 z>%9osZhU7r_|?AgpRdM+f8g)X2TLhi^gR#d!Yh_}wLpBg=bf!)3tKZO&*SBs=ZS6{ zDf)>}eio$5F8y{YiJ;_3_QxMU4l}KEI_)^=cpOH@*K?RQ)AiuPVtmri$#`}zampD= O6dNYFajGX19pm5KMrB6; literal 0 HcmV?d00001 diff --git a/common/__pycache__/getRequest.cpython-38.pyc b/common/__pycache__/getRequest.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1c0de664a099664ba23db025c6befb18b844201 GIT binary patch literal 1233 zcmZ8h&2QX96rV4DWOvg<1u6&#9as;CGOhdnexfECFyp2;TJwY{COr6zKv zrtJmPR7ez%8Vb9jJ#eV13M4)h!IcAl!H65pHaX?SffMg}({#mH^Ze$$pMUSoyf=P- zdb&Yi=reZCsGj#O5P<>NarSz zj&g4B(3NzN1nxdCkaj@r%*8y*R8p+w**jY28cc34?{t(-5HdNA!q0(K5eykJ#ib*; z6gTM&8j3Nw^)9K)r7}f(A%T4y4Iea|7h#&?l#q?EqLOx*e!U=Tv|<&ngk+L$iK}jQ z4txT97r@6VR`7wS_>k5Jr$)S7I0IL@72PJ%y9Yi>K7hS1g3R6%r0}{8eT@=)YNTJW zZE}wx?xPw97#}eACooh0$9zaCcWp|tF4uq7EDc;k#3sBOz$cI|d%JQ6d`PZ(m81Wv zyhT!Z11eiXYW}IU=^>TJy6iKm$DWxX)B*V_55LE(aR#$(%R8#r{{zk_5SEv^XWf3R zR)5uxZi294&E%PNWSRDj=4f#gcBMV>yiG5u<f;IhEMLfV8W)&^>F1eI;S_2S#{Zt^R{;9&DS2*b zV`Y7PNi8p{ZgFZgE0kW26Sb7&X_{wCP^ZI2Kiyxqtdrv&%t;W0GwRb9Va~DRv_X&2 zS?bo(ht+G$=Yk40M;oj`HyY1KI(fj_3xQ+(D9YkgMUibpQJTxLhk7fDE|+od(9=wM zu`!4Hdlf>~Nlys8r5f)nc7#F2XN=aCz|%A6odprT@T?A})s;l`@lRmta~&7J%|Bj) e|3m%V1UsIP#t_y&KhE$itc9uXIPAN{TKqqsd{f&1 literal 0 HcmV?d00001 diff --git a/common/__pycache__/getUserInfo.cpython-38.pyc b/common/__pycache__/getUserInfo.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3382b064f0b67ad5c692c033b3c088628d596e4c GIT binary patch literal 1078 zcmZ`&&2AGh5VpNuCxne?3zVuSxFAKM$qgZdP$k4qFF7>rWfjTVterM(cDJ>6B@%5< z?Ui?+$36mYf_&w~D;B3OS zF8~x#bV?E$Qi?X4vcwBLNHX>3{V~ zc8-*Of4<%uMe*T=PF2t5MqArM=H3LVaNeHI-Tf$z;dm!I)aeCxBGLJNrkq!psS`($ zi4yA?`#MtE*b8p3V8Z|$+sh1=+Yv6PbI2T%xE6%UBcsQY&yH6_t(saEW@7LOoq$S; zX?{V>BXECp)$8GWe>NNH@mL?`8$<^gwHU*2MGffcz*$s;0Q+m literal 0 HcmV?d00001 diff --git a/common/__pycache__/sqlSession.cpython-38.pyc b/common/__pycache__/sqlSession.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2a95ab1f4c84c861d71d2e85767aea9fc1f4012 GIT binary patch literal 3614 zcmbVP-EZ8+5#J?wU)0@6wo^5Uo76yoBGsi>3MNs;6wv0F$*lfXq9^tEk*{vUjer9@9f-wHnz{mt@DqCWa!9(Fmq zoSoU7`OWOyE47-<@Z8+~ckn`$vCnCce@qO{qb0NG1QR@DZQkN-qh)X!8=={@T9&5G zu+T2HN}9I9a=X&1Xu1&EZM$W27O>U`TJ;JOMNwL3qO@kTYN9ME>#S85_B3mZ>_87| z7-`LuzN>u4kLH8OPc7xIs0L4~iJv4v9JSpgUt(-8cVlpM!ft}mSkiIjlB+sSvK%^H z8DOp$#`E*O^hj?0**)27nDtqU3)V7xbB+rxjCJ0!{DLrrwa(Uft0)Shh&`4>NtE%% zj)jePMU03l-nOWTI^H8Bq&_aPsFBzjV zoPn*<@{odEt;7%KBsu*dlXbivpB9rdy@l1)8GmlhUs9)oNcnQk_52wxZnxuT1~LcR zn&_-Tm=i>Sa-5U29Sa@fMczBHxA}=7hgYDlgNJBRt55MQ5Q6uO`#_LliucVmzQ!>! z`;6xLL|c99KKnhPqR{M(cj83NOCOtgv6HNNacBOe$unmj^U25j{9}FwM=N(_sNs;B ziIR;%YAs+*YIS0XzMJ?mEqB}`xfM&1mW8X_Sr?OJgsd9{l^&n%2BC0<>Ig+&$9$qa zV)UNNo%1lK$H6*$2w@{=`otX&3LfjTMPrlS=6&F8sVodQd;T6 zap)vL&rhpucg2Z+0lb9qt+b{;bR^<2i00F(9!lR^^+K@f(NJ8*3ZFP(Fc+uQp*mO2 zCzk0G>U z@kf_Z6X4enPixMl=9R0>OW6lWK9mI6hAC@!rK5uoae#Lf%hj})<(mVbkmSa1pd~M$ zW4y}Cx*6Q!#@^2W{a@=p#hT6Q+~&RU99DAixDO=5;J!lAk%7s9Y!R+}6AqLWOf(Eh z^J!tZ>&sP)^v*4^)WL0@1>YoE23Fo1%WXifK9pAoqZ*%MAV@XP$TvCV?E~sfSBRnV z0%%_x)cz7gyqwcs&uPbk|F8R-SOny9#n%k^G$DAy%ytpk%GxN1-5oWIW|wxl_12%i zT>I?NryD!3z8wY$fx!-vAWD=QdAKcTF0)z*njJR`WeLmD(k)j;z~qhVR{u7bhea@ulCx>c_lDy2Ko9qaC7BsFOa_!E8%D;=Xm(h-L zq+a*LYKvL)$nQb=L9GOxt2wR5a>vbaZYjmG8ry>V$}sN zjFIM#Ixr=}YdO97c+sm4VV07|fp}54t=53hVZ4|F;Q1@Q*Tq#zAvO#gGg%G~TAf1X zdTtYWkPT$q*tZW7xOdI(xccR%SNrI>(x22U! zE1s|KBpurFJM@JTr6e>B5`4zW4_)#W?dcWpC6rBA!9YT{kkZEx=SbY6Ip7oLA?#70 zYZy>+pdcSQpJ!V*0&J+w2DKfQ46BK}uLqx-4Su6>g37GOZTpUs+K$tXMK>h6>Nv|? zH_X0BQZDHLD2tV=0g;+?c%=DEd&k{v>S*J5eX(clu*+?5l68z)^bSREw-0`Ja zl|P`ly?m%CDvo3s-Lq&3RTQ`bErZ);`Kx6UzoPDmGVQv-HSZzA5q7C1=&$Znl;0VyDx7Lmj));pNX5!C-4sk*^H`bNwr;CKIu6DV O0ab&37Pm(0&Hn+(!9-gC literal 0 HcmV?d00001 diff --git a/common/dataBuilderExecutor.py b/common/dataBuilderExecutor.py new file mode 100644 index 0000000..bb10f97 --- /dev/null +++ b/common/dataBuilderExecutor.py @@ -0,0 +1,57 @@ +# encoding: UTF-8 +import random +import re +import string + + +class DataBuilderExecutor(object): + """造数器同步执行器。 + + 当前版本只做安全的模板渲染和内置随机函数,不执行用户脚本,避免引入任意代码执行风险。 + """ + + def __init__(self, builder_def, env=None): + # builder_def 对应 data_builder.definition 字段,约定为 JSON 对象。 + self.builder_def = builder_def or {} + # 保留 steps 字段,后续扩展 http/db 流程编排时继续复用。 + self.steps = self.builder_def.get('steps', []) + self.env = env or {} + # context 是模板变量来源,支持 {{env.xxx}} 和 {{param.xxx}}。 + self.context = {'env': self.env} + self.results = [] + + def execute(self, params=None): + """执行造数器定义并返回渲染后的 output。""" + params = params or {} + self.context['param'] = params + output = self.builder_def.get('output') or {} + # 如果未配置 output,返回基础执行信息,方便前端判断定义是否为空。 + return self._render_template(output) if output else {'params': params, 'steps': len(self.steps)} + + def _render_template(self, obj): + """递归渲染字符串、字典、数组中的 {{变量}}。""" + if isinstance(obj, str): + return re.sub(r'\{\{([^}]+)\}\}', lambda m: str(self._get_value(m.group(1).strip())), obj) + if isinstance(obj, dict): + return {k: self._render_template(v) for k, v in obj.items()} + if isinstance(obj, list): + return [self._render_template(item) for item in obj] + return obj + + def _get_value(self, expr): + """获取模板表达式的值,支持内置随机函数和点路径取值。""" + if expr.startswith('random_string(') and expr.endswith(')'): + length = int(expr[len('random_string('):-1] or 8) + return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) + if expr == 'random_phone()': + return '1{}{}'.format(random.choice(['3', '5', '7', '8', '9']), ''.join(random.choice(string.digits) for _ in range(9))) + current = self.context + # 点路径示例:param.amount、env.base_url。 + for part in expr.split('.'): + if isinstance(current, dict): + current = current.get(part) + else: + current = getattr(current, part, None) + if current is None: + return '' + return current diff --git a/common/sqlSession.py b/common/sqlSession.py index 1fd2b20..3895627 100644 --- a/common/sqlSession.py +++ b/common/sqlSession.py @@ -8,6 +8,7 @@ from const import sparkatp_sql_uri from logger import logger _ENGINE_CACHE = {} +_SESSION_FACTORY_CACHE = {} """ @@ -32,27 +33,30 @@ class SqlSession: return f"postgresql+psycopg2://{user}:{urlquote(str(password))}@{host}:{port}/{database}" def get_session(self): - engine = _ENGINE_CACHE.get(self.sql_uri) - if engine is None: - engine = create_engine( - self.sql_uri, - pool_size=5, - max_overflow=10, - pool_pre_ping=True, - pool_recycle=1800, - pool_timeout=30, - connect_args={ - 'connect_timeout': 20, - 'options': '-c timezone=Asia/Shanghai' - } - ) - _ENGINE_CACHE[self.sql_uri] = engine - Session = sessionmaker(bind=engine) - session = Session() - return session + session_factory = _SESSION_FACTORY_CACHE.get(self.sql_uri) + if session_factory is None: + engine = _ENGINE_CACHE.get(self.sql_uri) + if engine is None: + engine = create_engine( + self.sql_uri, + pool_size=20, + max_overflow=30, + pool_pre_ping=True, + pool_recycle=1200, + pool_timeout=60, + pool_use_lifo=True, + connect_args={ + 'connect_timeout': 10, + 'options': '-c timezone=Asia/Shanghai' + } + ) + _ENGINE_CACHE[self.sql_uri] = engine + session_factory = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) + _SESSION_FACTORY_CACHE[self.sql_uri] = session_factory + return session_factory() - def query(self, obj): - return self._session.query(obj) + def query(self, *args): + return self._session.query(*args) def add(self, added): self._session.add(added) @@ -69,6 +73,9 @@ class SqlSession: def commit(self): self._session.commit() + def rollback(self): + self._session.rollback() + def close(self): self._session.close() diff --git a/const.py b/const.py index 1277fb3..09baac8 100644 --- a/const.py +++ b/const.py @@ -29,18 +29,18 @@ RES_CODE = { 40013: 'scene_id不能为空!' } -sparkatp_sql_uri = f'postgresql+psycopg2://postgres:{urlquote("dffa3866-dac8-49b1-a59e-725302bdfa4a")}@172.18.0.1:18366/postgres' +sparkatp_sql_uri = f'postgresql+psycopg2://postgres:{urlquote("difyai123456")}@39.170.26.156:8366/test' EXECUTE_DB_CONFIG = { 'ZHYY': { 'st': { - 'host': '172.18.0.1', + 'host': '124.220.32.45', 'port': 18666, 'user': 'postgres', 'password': '89c75b17-1738-4b7d-b651-4c65a5a662ab', 'database': 'smart_management_st' }, 'dev': { - 'host': '172.18.0.1', + 'host': '124.220.32.45', 'port': 18566, 'user': 'postgres', 'password': 'f267abd8-7005-472f-8cef-c1738c691c6c', @@ -56,7 +56,7 @@ EXECUTE_DB_CONFIG = { }, 'DLZ': { 'st': { - 'host': '172.18.0.1', + 'host': '124.220.32.45', 'port': 18666, 'user': 'joyhub', 'password': 'e364be29-6089-4610-97d5-0037a28d0703', @@ -82,4 +82,4 @@ STRESS_URI = 'https://qe.bg.huohua.cn' QE_DOMAIN = 'https://qe.bg.huohua.cn' PASSWORD = quote('AcUVeRb8lN') -REDIS_URL = "redis://:{}@redis.qa.cn:6379/30".format(PASSWORD) +REDIS_URL = 'redis://124.220.32.45:7379/15' diff --git a/manage.py b/manage.py index f9f4e25..7b2e9f8 100644 --- a/manage.py +++ b/manage.py @@ -1,11 +1,26 @@ # encoding: UTF-8 from flask_cors import CORS -from flask import make_response, jsonify, request, redirect +from flask import make_response, jsonify, request, redirect, send_from_directory, safe_join from app import create_app +import os app = create_app() CORS(app, resources=r'/*') +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'attachment', 'bug_picture') +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +@app.route('/uploads/') +def uploaded_file(filename): + try: + safe_path = safe_join(app.config['UPLOAD_FOLDER'], filename) + if not os.path.exists(safe_path): + return f"File not found: {filename}", 404 + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + except Exception as e: + return f"Error: {str(e)}", 500 + if __name__ == '__main__': app.run(host='0.0.0.0', port=5010, debug=True) diff --git a/数据库语句 b/数据库语句 new file mode 100644 index 0000000..0502364 --- /dev/null +++ b/数据库语句 @@ -0,0 +1,534 @@ +-- ========================================================= +-- 测试管理模块数据库初始化脚本(_time 字段版本) +-- 适用数据库:PostgreSQL +-- 说明: +-- 1. 本脚本与当前后端代码字段保持一致 +-- 2. 所有时间字段统一使用 *_time 后缀 +-- 3. 主表包含 is_delete 逻辑删除字段 +-- ========================================================= + + +-- ========================================================= +-- 一、项目相关 +-- ========================================================= + +-- ------------------------- +-- 1. 项目表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS project ( + id BIGSERIAL PRIMARY KEY, + key VARCHAR(32) UNIQUE NOT NULL, + name VARCHAR(128) NOT NULL, + description TEXT, + department VARCHAR(64), + status SMALLINT DEFAULT 1, + config JSONB DEFAULT '{}'::jsonb, + created_by BIGINT, + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE project IS '项目表'; +COMMENT ON COLUMN project.id IS '主键ID'; +COMMENT ON COLUMN project.key IS '项目唯一标识,如 XP2024'; +COMMENT ON COLUMN project.name IS '项目名称'; +COMMENT ON COLUMN project.description IS '项目描述'; +COMMENT ON COLUMN project.department IS '所属部门'; +COMMENT ON COLUMN project.status IS '项目状态:1启用 0禁用'; +COMMENT ON COLUMN project.config IS '扩展配置,JSON格式'; +COMMENT ON COLUMN project.created_by IS '创建人'; +COMMENT ON COLUMN project.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN project.created_time IS '创建时间'; +COMMENT ON COLUMN project.updated_time IS '更新时间'; + +CREATE INDEX IF NOT EXISTS idx_project_status ON project(status); +CREATE INDEX IF NOT EXISTS idx_project_is_delete ON project(is_delete); + + +-- ------------------------- +-- 2. 项目成员表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS project_member ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL, + role SMALLINT NOT NULL, + joined_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE project_member IS '项目成员表'; +COMMENT ON COLUMN project_member.id IS '主键ID'; +COMMENT ON COLUMN project_member.project_id IS '项目ID'; +COMMENT ON COLUMN project_member.user_id IS '用户ID'; +COMMENT ON COLUMN project_member.role IS '角色:1测试经理 2测试工程师 3开发工程师 4访客'; +COMMENT ON COLUMN project_member.joined_time IS '加入时间'; + +CREATE UNIQUE INDEX IF NOT EXISTS uk_project_member ON project_member(project_id, user_id); +CREATE INDEX IF NOT EXISTS idx_member_user ON project_member(user_id); +CREATE INDEX IF NOT EXISTS idx_member_project_id ON project_member(project_id); + + +-- ------------------------- +-- 3. 环境配置表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS environment ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + name VARCHAR(64) NOT NULL, + variables JSONB NOT NULL, + is_encrypted BOOLEAN DEFAULT FALSE, + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE environment IS '环境配置表'; +COMMENT ON COLUMN environment.id IS '主键ID'; +COMMENT ON COLUMN environment.project_id IS '项目ID'; +COMMENT ON COLUMN environment.name IS '环境名称,如 dev/test/staging/prod'; +COMMENT ON COLUMN environment.variables IS '环境变量配置,JSON格式'; +COMMENT ON COLUMN environment.is_encrypted IS '敏感信息是否已加密'; +COMMENT ON COLUMN environment.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN environment.created_time IS '创建时间'; + +CREATE INDEX IF NOT EXISTS idx_environment_project_id ON environment(project_id); +CREATE INDEX IF NOT EXISTS idx_environment_is_delete ON environment(is_delete); + + +-- ------------------------- +-- 4. 产品表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS product ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + code VARCHAR(64) UNIQUE NOT NULL, + description TEXT, + status SMALLINT DEFAULT 1, + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE product IS '产品表'; +COMMENT ON COLUMN product.id IS '主键ID'; +COMMENT ON COLUMN product.name IS '产品名称'; +COMMENT ON COLUMN product.code IS '产品编码'; +COMMENT ON COLUMN product.description IS '产品描述'; +COMMENT ON COLUMN product.status IS '产品状态:1启用 0禁用'; +COMMENT ON COLUMN product.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN product.created_time IS '创建时间'; +COMMENT ON COLUMN product.updated_time IS '更新时间'; + +CREATE INDEX IF NOT EXISTS idx_product_status ON product(status); +CREATE INDEX IF NOT EXISTS idx_product_is_delete ON product(is_delete); + + +-- ========================================================= +-- 二、用例相关 +-- ========================================================= + +-- ------------------------- +-- 4. 模块表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS module ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + parent_id BIGINT DEFAULT 0, + name VARCHAR(128) NOT NULL, + sort_order INTEGER DEFAULT 0, + path VARCHAR(512), + is_delete INTEGER DEFAULT 0 +); + +COMMENT ON TABLE module IS '模块树表,支持多层级模块结构'; +COMMENT ON COLUMN module.id IS '主键ID'; +COMMENT ON COLUMN module.project_id IS '项目ID'; +COMMENT ON COLUMN module.parent_id IS '父模块ID,0表示根节点'; +COMMENT ON COLUMN module.name IS '模块名称'; +COMMENT ON COLUMN module.sort_order IS '排序值'; +COMMENT ON COLUMN module.path IS '模块路径,如 /1/23/45'; +COMMENT ON COLUMN module.is_delete IS '逻辑删除标记:0未删除 1已删除'; + +CREATE INDEX IF NOT EXISTS idx_module_project ON module(project_id); +CREATE INDEX IF NOT EXISTS idx_module_parent_id ON module(parent_id); +CREATE INDEX IF NOT EXISTS idx_module_is_delete ON module(is_delete); + + +-- ------------------------- +-- 5. 用例表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS test_case ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + module_id BIGINT REFERENCES module(id) ON DELETE SET NULL, + case_key VARCHAR(64) NOT NULL, + title VARCHAR(255) NOT NULL, + preconditions TEXT, + steps JSONB NOT NULL DEFAULT '[]'::jsonb, + priority SMALLINT DEFAULT 2, + case_type SMALLINT DEFAULT 1, + tags VARCHAR(64)[] DEFAULT '{}'::varchar[], + status SMALLINT DEFAULT 1, + created_by BIGINT, + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE test_case IS '测试用例表'; +COMMENT ON COLUMN test_case.id IS '主键ID'; +COMMENT ON COLUMN test_case.project_id IS '项目ID'; +COMMENT ON COLUMN test_case.module_id IS '所属模块ID'; +COMMENT ON COLUMN test_case.case_key IS '项目内唯一编号,如 TC-001'; +COMMENT ON COLUMN test_case.title IS '用例标题'; +COMMENT ON COLUMN test_case.preconditions IS '前置条件'; +COMMENT ON COLUMN test_case.steps IS '测试步骤,JSON数组'; +COMMENT ON COLUMN test_case.priority IS '优先级:0P0 1P1 2P2 3P3'; +COMMENT ON COLUMN test_case.case_type IS '用例类型:1功能 2性能 3安全 4接口'; +COMMENT ON COLUMN test_case.tags IS '标签数组'; +COMMENT ON COLUMN test_case.status IS '状态:1正常 2已废弃 3评审中'; +COMMENT ON COLUMN test_case.created_by IS '创建人'; +COMMENT ON COLUMN test_case.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN test_case.created_time IS '创建时间'; +COMMENT ON COLUMN test_case.updated_time IS '更新时间'; + +CREATE INDEX IF NOT EXISTS idx_case_project ON test_case(project_id); +CREATE INDEX IF NOT EXISTS idx_case_module ON test_case(module_id); +CREATE INDEX IF NOT EXISTS idx_case_priority ON test_case(priority); +CREATE INDEX IF NOT EXISTS idx_case_status ON test_case(status); +CREATE INDEX IF NOT EXISTS idx_case_is_delete ON test_case(is_delete); +CREATE UNIQUE INDEX IF NOT EXISTS uk_test_case_project_case_key ON test_case(project_id, case_key); + + +-- ------------------------- +-- 6. 用例快照表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS case_snapshot ( + id BIGSERIAL PRIMARY KEY, + case_id BIGINT NOT NULL REFERENCES test_case(id) ON DELETE CASCADE, + version INTEGER NOT NULL, + snapshot JSONB NOT NULL, + created_by BIGINT, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE case_snapshot IS '用例版本快照表'; +COMMENT ON COLUMN case_snapshot.id IS '主键ID'; +COMMENT ON COLUMN case_snapshot.case_id IS '用例ID'; +COMMENT ON COLUMN case_snapshot.version IS '版本号'; +COMMENT ON COLUMN case_snapshot.snapshot IS '完整快照内容'; +COMMENT ON COLUMN case_snapshot.created_by IS '创建人'; +COMMENT ON COLUMN case_snapshot.created_time IS '创建时间'; + +CREATE INDEX IF NOT EXISTS idx_case_snapshot_case_id ON case_snapshot(case_id); +CREATE UNIQUE INDEX IF NOT EXISTS uk_case_snapshot_case_version ON case_snapshot(case_id, version); + + +-- ------------------------- +-- 7. 用例评审表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS case_review ( + id BIGSERIAL PRIMARY KEY, + case_id BIGINT NOT NULL REFERENCES test_case(id) ON DELETE CASCADE, + reviewer_id BIGINT NOT NULL, + status SMALLINT DEFAULT 0, + comments TEXT, + diff_content TEXT, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reviewed_time TIMESTAMP +); + +COMMENT ON TABLE case_review IS '用例评审表'; +COMMENT ON COLUMN case_review.id IS '主键ID'; +COMMENT ON COLUMN case_review.case_id IS '用例ID'; +COMMENT ON COLUMN case_review.reviewer_id IS '评审人ID'; +COMMENT ON COLUMN case_review.status IS '评审状态:0待评审 1通过 2驳回 3建议修改'; +COMMENT ON COLUMN case_review.comments IS '评审意见'; +COMMENT ON COLUMN case_review.diff_content IS '变更差异内容,通常为JSON diff字符串'; +COMMENT ON COLUMN case_review.created_time IS '创建时间'; +COMMENT ON COLUMN case_review.reviewed_time IS '评审时间'; + +CREATE INDEX IF NOT EXISTS idx_case_review_case_id ON case_review(case_id); +CREATE INDEX IF NOT EXISTS idx_case_review_reviewer_id ON case_review(reviewer_id); + + +-- ========================================================= +-- 三、测试计划相关 +-- ========================================================= + +-- ------------------------- +-- 8. 测试计划表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS test_plan ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + name VARCHAR(128) NOT NULL, + version VARCHAR(32), + description TEXT, + start_date DATE, + end_date DATE, + owner_id BIGINT, + status SMALLINT DEFAULT 0, + environment_id BIGINT REFERENCES environment(id), + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE test_plan IS '测试计划表'; +COMMENT ON COLUMN test_plan.id IS '主键ID'; +COMMENT ON COLUMN test_plan.project_id IS '项目ID'; +COMMENT ON COLUMN test_plan.name IS '计划名称'; +COMMENT ON COLUMN test_plan.version IS '测试版本号'; +COMMENT ON COLUMN test_plan.description IS '计划描述'; +COMMENT ON COLUMN test_plan.start_date IS '开始日期'; +COMMENT ON COLUMN test_plan.end_date IS '结束日期'; +COMMENT ON COLUMN test_plan.owner_id IS '负责人ID'; +COMMENT ON COLUMN test_plan.status IS '计划状态:0草稿 1进行中 2已完成 3已归档'; +COMMENT ON COLUMN test_plan.environment_id IS '关联环境ID'; +COMMENT ON COLUMN test_plan.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN test_plan.created_time IS '创建时间'; +COMMENT ON COLUMN test_plan.updated_time IS '更新时间'; + +CREATE INDEX IF NOT EXISTS idx_test_plan_project_id ON test_plan(project_id); +CREATE INDEX IF NOT EXISTS idx_test_plan_status ON test_plan(status); +CREATE INDEX IF NOT EXISTS idx_test_plan_is_delete ON test_plan(is_delete); + + +-- ------------------------- +-- 9. 计划用例表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS plan_case ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL REFERENCES test_plan(id) ON DELETE CASCADE, + case_id BIGINT NOT NULL REFERENCES test_case(id), + assignee_id BIGINT, + round_no INTEGER DEFAULT 1, + status SMALLINT DEFAULT 0, + actual_result TEXT, + defect_links JSONB DEFAULT '[]'::jsonb, + attachments JSONB DEFAULT '[]'::jsonb, + executed_time TIMESTAMP, + execution_duration INTEGER +); + +COMMENT ON TABLE plan_case IS '计划与用例关联表,同时存储执行结果'; +COMMENT ON COLUMN plan_case.id IS '主键ID'; +COMMENT ON COLUMN plan_case.plan_id IS '计划ID'; +COMMENT ON COLUMN plan_case.case_id IS '用例ID'; +COMMENT ON COLUMN plan_case.assignee_id IS '执行人ID'; +COMMENT ON COLUMN plan_case.round_no IS '执行轮次'; +COMMENT ON COLUMN plan_case.status IS '执行状态:0未开始 1通过 2失败 3阻塞'; +COMMENT ON COLUMN plan_case.actual_result IS '实际执行结果'; +COMMENT ON COLUMN plan_case.defect_links IS '缺陷链接数组'; +COMMENT ON COLUMN plan_case.attachments IS '附件数组'; +COMMENT ON COLUMN plan_case.executed_time IS '执行时间'; +COMMENT ON COLUMN plan_case.execution_duration IS '执行耗时,单位秒'; + +CREATE UNIQUE INDEX IF NOT EXISTS uk_plan_case_round ON plan_case(plan_id, case_id, round_no); +CREATE INDEX IF NOT EXISTS idx_plan_case_plan ON plan_case(plan_id); +CREATE INDEX IF NOT EXISTS idx_plan_case_assignee ON plan_case(assignee_id); +CREATE INDEX IF NOT EXISTS idx_plan_case_status ON plan_case(status); + + +-- ------------------------- +-- 10. 测试轮次表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS test_round ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL REFERENCES test_plan(id) ON DELETE CASCADE, + round_no INTEGER NOT NULL, + name VARCHAR(64), + start_date DATE, + end_date DATE +); + +COMMENT ON TABLE test_round IS '测试轮次表'; +COMMENT ON COLUMN test_round.id IS '主键ID'; +COMMENT ON COLUMN test_round.plan_id IS '计划ID'; +COMMENT ON COLUMN test_round.round_no IS '轮次编号'; +COMMENT ON COLUMN test_round.name IS '轮次名称'; +COMMENT ON COLUMN test_round.start_date IS '开始日期'; +COMMENT ON COLUMN test_round.end_date IS '结束日期'; + +CREATE UNIQUE INDEX IF NOT EXISTS uk_test_round_plan_round_no ON test_round(plan_id, round_no); +CREATE INDEX IF NOT EXISTS idx_test_round_plan_id ON test_round(plan_id); + + +-- ========================================================= +-- 四、报告相关 +-- ========================================================= + +-- ------------------------- +-- 11. 报告表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS report ( + id BIGSERIAL PRIMARY KEY, + plan_id BIGINT NOT NULL REFERENCES test_plan(id) ON DELETE CASCADE, + name VARCHAR(128) NOT NULL, + report_type SMALLINT DEFAULT 1, + summary JSONB, + content TEXT, + file_url VARCHAR(512), + generated_by BIGINT, + generated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE report IS '测试报告表'; +COMMENT ON COLUMN report.id IS '主键ID'; +COMMENT ON COLUMN report.plan_id IS '计划ID'; +COMMENT ON COLUMN report.name IS '报告名称'; +COMMENT ON COLUMN report.report_type IS '报告类型:1实时报告 2归档报告'; +COMMENT ON COLUMN report.summary IS '报告统计摘要,JSON格式'; +COMMENT ON COLUMN report.content IS '报告HTML内容'; +COMMENT ON COLUMN report.file_url IS '导出文件地址'; +COMMENT ON COLUMN report.generated_by IS '生成人'; +COMMENT ON COLUMN report.generated_time IS '生成时间'; + +CREATE INDEX IF NOT EXISTS idx_report_plan_id ON report(plan_id); +CREATE INDEX IF NOT EXISTS idx_report_generated_time ON report(generated_time); + + +-- ------------------------- +-- 12. 缺陷同步表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS defect_sync ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id), + external_id VARCHAR(64) NOT NULL, + external_system VARCHAR(32), + plan_case_id BIGINT REFERENCES plan_case(id), + status VARCHAR(32), + last_sync_time TIMESTAMP +); + +COMMENT ON TABLE defect_sync IS '缺陷同步表,用于记录外部缺陷系统关联关系'; +COMMENT ON COLUMN defect_sync.id IS '主键ID'; +COMMENT ON COLUMN defect_sync.project_id IS '项目ID'; +COMMENT ON COLUMN defect_sync.external_id IS '外部缺陷ID,如 JIRA-123'; +COMMENT ON COLUMN defect_sync.external_system IS '外部系统,如 jira/tapd/zentao'; +COMMENT ON COLUMN defect_sync.plan_case_id IS '计划用例执行ID'; +COMMENT ON COLUMN defect_sync.status IS '外部缺陷状态'; +COMMENT ON COLUMN defect_sync.last_sync_time IS '最后同步时间'; + +CREATE INDEX IF NOT EXISTS idx_defect_sync_project_id ON defect_sync(project_id); +CREATE INDEX IF NOT EXISTS idx_defect_sync_plan_case_id ON defect_sync(plan_case_id); +CREATE INDEX IF NOT EXISTS idx_defect_sync_external_id ON defect_sync(external_id); + + +-- ========================================================= +-- 五、造数相关 +-- ========================================================= + +-- ------------------------- +-- 13. 造数器表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS data_builder ( + id BIGSERIAL PRIMARY KEY, + project_id BIGINT NOT NULL REFERENCES project(id) ON DELETE CASCADE, + name VARCHAR(128) NOT NULL, + description TEXT, + builder_type SMALLINT DEFAULT 1, + definition JSONB NOT NULL, + input_schema JSONB, + output_example JSONB, + created_by BIGINT, + is_delete INTEGER DEFAULT 0, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +COMMENT ON TABLE data_builder IS '数据构造器表'; +COMMENT ON COLUMN data_builder.id IS '主键ID'; +COMMENT ON COLUMN data_builder.project_id IS '项目ID'; +COMMENT ON COLUMN data_builder.name IS '造数器名称'; +COMMENT ON COLUMN data_builder.description IS '造数器描述'; +COMMENT ON COLUMN data_builder.builder_type IS '造数器类型:1流程编排 2SQL 3脚本'; +COMMENT ON COLUMN data_builder.definition IS '造数逻辑定义,JSON格式'; +COMMENT ON COLUMN data_builder.input_schema IS '输入参数结构定义'; +COMMENT ON COLUMN data_builder.output_example IS '输出示例'; +COMMENT ON COLUMN data_builder.created_by IS '创建人'; +COMMENT ON COLUMN data_builder.is_delete IS '逻辑删除标记:0未删除 1已删除'; +COMMENT ON COLUMN data_builder.created_time IS '创建时间'; +COMMENT ON COLUMN data_builder.updated_time IS '更新时间'; + +CREATE INDEX IF NOT EXISTS idx_data_builder_project_id ON data_builder(project_id); +CREATE INDEX IF NOT EXISTS idx_data_builder_is_delete ON data_builder(is_delete); + + +-- ------------------------- +-- 14. 造数任务表 +-- ------------------------- +CREATE TABLE IF NOT EXISTS data_task ( + id BIGSERIAL PRIMARY KEY, + builder_id BIGINT NOT NULL REFERENCES data_builder(id), + project_id BIGINT NOT NULL, + params JSONB, + status SMALLINT DEFAULT 0, + result_data JSONB, + error_message TEXT, + created_by BIGINT, + created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_time TIMESTAMP +); + +COMMENT ON TABLE data_task IS '数据生成任务表'; +COMMENT ON COLUMN data_task.id IS '主键ID'; +COMMENT ON COLUMN data_task.builder_id IS '造数器ID'; +COMMENT ON COLUMN data_task.project_id IS '项目ID'; +COMMENT ON COLUMN data_task.params IS '任务入参,JSON格式'; +COMMENT ON COLUMN data_task.status IS '任务状态:0等待 1执行中 2成功 3失败'; +COMMENT ON COLUMN data_task.result_data IS '生成结果数据'; +COMMENT ON COLUMN data_task.error_message IS '错误信息'; +COMMENT ON COLUMN data_task.created_by IS '创建人'; +COMMENT ON COLUMN data_task.created_time IS '创建时间'; +COMMENT ON COLUMN data_task.completed_time IS '完成时间'; + +CREATE INDEX IF NOT EXISTS idx_task_status ON data_task(status); +CREATE INDEX IF NOT EXISTS idx_data_task_builder_id ON data_task(builder_id); +CREATE INDEX IF NOT EXISTS idx_data_task_project_id ON data_task(project_id); + + +-- ========================================================= +-- 六、更新时间自动维护触发器 +-- 说明: +-- PostgreSQL 需要借助触发器维护 updated_time +-- ========================================================= + +CREATE OR REPLACE FUNCTION update_updated_time_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_time = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_project_updated_time ON project; +CREATE TRIGGER trg_project_updated_time +BEFORE UPDATE ON project +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); + +DROP TRIGGER IF EXISTS trg_product_updated_time ON product; +CREATE TRIGGER trg_product_updated_time +BEFORE UPDATE ON product +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); + +DROP TRIGGER IF EXISTS trg_test_case_updated_time ON test_case; +CREATE TRIGGER trg_test_case_updated_time +BEFORE UPDATE ON test_case +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); + +DROP TRIGGER IF EXISTS trg_test_plan_updated_time ON test_plan; +CREATE TRIGGER trg_test_plan_updated_time +BEFORE UPDATE ON test_plan +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); + +DROP TRIGGER IF EXISTS trg_data_builder_updated_time ON data_builder; +CREATE TRIGGER trg_data_builder_updated_time +BEFORE UPDATE ON data_builder +FOR EACH ROW +EXECUTE FUNCTION update_updated_time_column(); \ No newline at end of file