9 Commits

21746 changed files with 2234134 additions and 215188 deletions

208181
HubOps.md

File diff suppressed because one or more lines are too long

View File

@@ -3486,3 +3486,239 @@ LINE 1: UPDATE system_users SET fingerprint = '' WHERE username = 'j...
2026-05-06 10:01:02,073 [tid:14556 pid:31208] runner.py[line:202] INFO ------状态码200 返回信息:{'code': 401, 'msg': '账号未登录', 'data': None} 2026-05-06 10:01:02,073 [tid:14556 pid:31208] runner.py[line:202] INFO ------状态码200 返回信息:{'code': 401, 'msg': '账号未登录', 'data': None}
2026-05-06 10:01:02,075 [tid:14556 pid:31208] runner.py[line:221] WARNING 缓存session过期清理缓存 2026-05-06 10:01:02,075 [tid:14556 pid:31208] runner.py[line:221] WARNING 缓存session过期清理缓存
2026-05-06 10:01:02,076 [tid:14556 pid:31208] runner.py[line:235] INFO 返回数据:{'code': 401, 'msg': '账号未登录', 'data': None} 2026-05-06 10:01:02,076 [tid:14556 pid:31208] runner.py[line:235] INFO 返回数据:{'code': 401, 'msg': '账号未登录', 'data': None}
2026-05-06 16:32:24,371 [tid:9812 pid:10540] UserManage.py[line:44] INFO 尝试读取配置文件: d:\zhyy\smart-management-auto-test\dulizhan\library\BusinessKw\JoyHub\../../../../test_case/Resource/AdapterKws/hh-qa.robot
2026-05-06 16:32:24,372 [tid:9812 pid:10540] UserManage.py[line:57] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'd:\\zhyy\\smart-management-auto-test\\dulizhan\\library\\BusinessKw\\JoyHub\\../../../../test_case/Resource/AdapterKws/hh-qa.robot'
2026-05-06 16:32:24,372 [tid:9812 pid:10540] UserManage.py[line:37] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:32:24,510 [tid:9812 pid:10540] RoleManage.py[line:41] INFO 尝试读取配置文件: C:\Users\a\PyCharmMiscProject\smart-management-auto-test\dulizhan\test_case\Resource\AdapterKws\hh-qa.robot
2026-05-06 16:32:24,512 [tid:9812 pid:10540] RoleManage.py[line:53] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'C:\\Users\\a\\PyCharmMiscProject\\smart-management-auto-test\\dulizhan\\test_case\\Resource\\AdapterKws\\hh-qa.robot'
2026-05-06 16:32:24,513 [tid:9812 pid:10540] RoleManage.py[line:36] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:32:25,019 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:25,389 [tid:9812 pid:10540] AgreementManage.py[line:116] INFO 获得协议分页列表 - 参数: {'page_no': 1, 'page_size': 10, 'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:32:25,390 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得协议分页 ===========
2026-05-06 16:32:25,636 [tid:9812 pid:10540] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 测试协议_1778056345, content: 这是测试协议内容_1778056345..., terminal: web, lang: de, rank_num: 345, status: 2
2026-05-06 16:32:25,637 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:32:25,815 [tid:9812 pid:10540] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 详情测试协议_1778056345, content: 详情测试协议内容_1778056345..., terminal: app, lang: ja, rank_num: 1345, status: 1
2026-05-06 16:32:25,817 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:32:26,045 [tid:9812 pid:10540] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待更新协议_1778056346, content: 待更新协议内容_1778056346..., terminal: app, lang: de, rank_num: 2346, status: 1
2026-05-06 16:32:26,046 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:32:26,226 [tid:9812 pid:10540] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待删除协议_1778056346, content: 待删除协议内容_1778056346..., terminal: web, lang: ja, rank_num: 4346, status: 2
2026-05-06 16:32:26,227 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:32:26,413 [tid:9812 pid:10540] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 批量删除协议1_1778056346, content: 批量删除协议内容1_1778056346..., terminal: app, lang: de, rank_num: 5346, status: 2
2026-05-06 16:32:26,414 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:32:26,634 [tid:9812 pid:10540] AgreementManage.py[line:88] INFO 导出协议Excel - 参数: {'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:32:26,635 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 导出协议 Excel ===========
2026-05-06 16:32:26,929 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:27,017 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得app版本号管理分页 ===========
2026-05-06 16:32:27,251 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:32:27,433 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:32:27,652 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:32:27,852 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:32:28,031 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:32:28,238 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得导入app版本号管理模板 ===========
2026-05-06 16:32:28,453 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 导出app版本号管理 Excel ===========
2026-05-06 16:32:28,796 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:28,927 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得Banner管理分页 ===========
2026-05-06 16:32:29,122 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:32:29,327 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:32:29,497 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:32:29,667 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:32:29,887 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:32:30,207 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:30,433 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:30,629 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得角色分页 ===========
2026-05-06 16:32:30,890 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:32:31,116 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:32:31,335 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:32:31,542 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:32:31,798 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:32:31,990 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:32:32,263 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:32:32,573 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:32,780 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:32:33,139 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获得用户分页列表 ===========
2026-05-06 16:32:33,390 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 获取用户精简信息列表 ===========
2026-05-06 16:32:33,583 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:33,810 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:34,012 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:34,246 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:34,435 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:34,673 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:34,848 [tid:9812 pid:10540] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:32:35,191 [tid:9812 pid:10540] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:18,921 [tid:3648 pid:26928] UserManage.py[line:44] INFO 尝试读取配置文件: d:\zhyy\smart-management-auto-test\dulizhan\library\BusinessKw\JoyHub\../../../../test_case/Resource/AdapterKws/hh-qa.robot
2026-05-06 16:37:18,971 [tid:3648 pid:26928] UserManage.py[line:57] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'd:\\zhyy\\smart-management-auto-test\\dulizhan\\library\\BusinessKw\\JoyHub\\../../../../test_case/Resource/AdapterKws/hh-qa.robot'
2026-05-06 16:37:18,972 [tid:3648 pid:26928] UserManage.py[line:37] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:37:19,058 [tid:3648 pid:26928] RoleManage.py[line:41] INFO 尝试读取配置文件: C:\Users\a\PyCharmMiscProject\smart-management-auto-test\dulizhan\test_case\Resource\AdapterKws\hh-qa.robot
2026-05-06 16:37:19,059 [tid:3648 pid:26928] RoleManage.py[line:53] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'C:\\Users\\a\\PyCharmMiscProject\\smart-management-auto-test\\dulizhan\\test_case\\Resource\\AdapterKws\\hh-qa.robot'
2026-05-06 16:37:19,060 [tid:3648 pid:26928] RoleManage.py[line:36] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:37:19,570 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:19,899 [tid:3648 pid:26928] AgreementManage.py[line:116] INFO 获得协议分页列表 - 参数: {'page_no': 1, 'page_size': 10, 'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:37:19,900 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得协议分页 ===========
2026-05-06 16:37:20,091 [tid:3648 pid:26928] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 测试协议_1778056640, content: 这是测试协议内容_1778056640..., terminal: web, lang: de, rank_num: 640, status: 2
2026-05-06 16:37:20,092 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:37:20,300 [tid:3648 pid:26928] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 详情测试协议_1778056640, content: 详情测试协议内容_1778056640..., terminal: app, lang: ja, rank_num: 1640, status: 1
2026-05-06 16:37:20,301 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:37:20,473 [tid:3648 pid:26928] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待更新协议_1778056640, content: 待更新协议内容_1778056640..., terminal: app, lang: de, rank_num: 2640, status: 1
2026-05-06 16:37:20,475 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:37:20,716 [tid:3648 pid:26928] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待删除协议_1778056640, content: 待删除协议内容_1778056640..., terminal: web, lang: ja, rank_num: 4640, status: 2
2026-05-06 16:37:20,717 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:37:20,915 [tid:3648 pid:26928] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 批量删除协议1_1778056640, content: 批量删除协议内容1_1778056640..., terminal: app, lang: de, rank_num: 5640, status: 2
2026-05-06 16:37:20,916 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:37:21,131 [tid:3648 pid:26928] AgreementManage.py[line:88] INFO 导出协议Excel - 参数: {'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:37:21,132 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 导出协议 Excel ===========
2026-05-06 16:37:21,431 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:21,516 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得app版本号管理分页 ===========
2026-05-06 16:37:21,683 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:37:21,877 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:37:22,052 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:37:22,284 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:37:22,451 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:37:22,667 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得导入app版本号管理模板 ===========
2026-05-06 16:37:22,873 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 导出app版本号管理 Excel ===========
2026-05-06 16:37:23,178 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:23,321 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得Banner管理分页 ===========
2026-05-06 16:37:23,489 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:37:23,651 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:37:23,923 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:37:24,106 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:37:24,302 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:37:24,599 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:24,818 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:24,995 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得角色分页 ===========
2026-05-06 16:37:25,198 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:37:25,415 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:37:25,623 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:37:25,803 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:37:25,987 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:37:26,204 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:37:26,380 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:37:26,721 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:26,937 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:37:27,183 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获得用户分页列表 ===========
2026-05-06 16:37:27,397 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 获取用户精简信息列表 ===========
2026-05-06 16:37:27,571 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:27,799 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:28,006 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:28,247 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:28,510 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:28,788 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:29,004 [tid:3648 pid:26928] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:37:29,298 [tid:3648 pid:26928] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:25,759 [tid:9412 pid:1312] UserManage.py[line:44] INFO 尝试读取配置文件: D:\zhyy\smart-management-auto-test\dulizhan\library\BusinessKw\JoyHub\../../../../test_case/Resource/AdapterKws/hh-qa.robot
2026-05-06 16:44:25,759 [tid:9412 pid:1312] UserManage.py[line:57] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'D:\\zhyy\\smart-management-auto-test\\dulizhan\\library\\BusinessKw\\JoyHub\\../../../../test_case/Resource/AdapterKws/hh-qa.robot'
2026-05-06 16:44:25,762 [tid:9412 pid:1312] UserManage.py[line:37] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:44:25,853 [tid:9412 pid:1312] RoleManage.py[line:41] INFO 尝试读取配置文件: C:\Users\a\PyCharmMiscProject\smart-management-auto-test\dulizhan\test_case\Resource\AdapterKws\hh-qa.robot
2026-05-06 16:44:25,854 [tid:9412 pid:1312] RoleManage.py[line:53] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'C:\\Users\\a\\PyCharmMiscProject\\smart-management-auto-test\\dulizhan\\test_case\\Resource\\AdapterKws\\hh-qa.robot'
2026-05-06 16:44:25,855 [tid:9412 pid:1312] RoleManage.py[line:36] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:44:26,358 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:26,707 [tid:9412 pid:1312] AgreementManage.py[line:116] INFO 获得协议分页列表 - 参数: {'page_no': 1, 'page_size': 10, 'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:44:26,708 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得协议分页 ===========
2026-05-06 16:44:26,903 [tid:9412 pid:1312] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 测试协议_1778057066, content: 这是测试协议内容_1778057066..., terminal: web, lang: de, rank_num: 66, status: 2
2026-05-06 16:44:26,904 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:44:27,111 [tid:9412 pid:1312] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 详情测试协议_1778057067, content: 详情测试协议内容_1778057067..., terminal: app, lang: ja, rank_num: 1067, status: 1
2026-05-06 16:44:27,112 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:44:27,290 [tid:9412 pid:1312] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待更新协议_1778057067, content: 待更新协议内容_1778057067..., terminal: app, lang: de, rank_num: 2067, status: 1
2026-05-06 16:44:27,291 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:44:27,516 [tid:9412 pid:1312] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待删除协议_1778057067, content: 待删除协议内容_1778057067..., terminal: web, lang: ja, rank_num: 4067, status: 2
2026-05-06 16:44:27,518 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:44:27,692 [tid:9412 pid:1312] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 批量删除协议1_1778057067, content: 批量删除协议内容1_1778057067..., terminal: app, lang: de, rank_num: 5067, status: 2
2026-05-06 16:44:27,692 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:44:27,894 [tid:9412 pid:1312] AgreementManage.py[line:88] INFO 导出协议Excel - 参数: {'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:44:27,894 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 导出协议 Excel ===========
2026-05-06 16:44:28,193 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:28,296 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得app版本号管理分页 ===========
2026-05-06 16:44:28,462 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:44:28,694 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:44:28,866 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:44:29,075 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:44:29,246 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:44:29,464 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得导入app版本号管理模板 ===========
2026-05-06 16:44:29,657 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 导出app版本号管理 Excel ===========
2026-05-06 16:44:29,974 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:30,056 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得Banner管理分页 ===========
2026-05-06 16:44:30,228 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:44:30,437 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:44:30,652 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:44:30,835 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:44:31,052 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:44:31,365 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:31,601 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:31,823 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得角色分页 ===========
2026-05-06 16:44:32,025 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:44:32,195 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:44:32,409 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:44:32,581 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:44:32,753 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:44:32,996 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:44:33,186 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:44:33,578 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:33,809 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:44:34,103 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获得用户分页列表 ===========
2026-05-06 16:44:34,330 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 获取用户精简信息列表 ===========
2026-05-06 16:44:34,511 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:34,763 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:34,980 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:35,261 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:35,504 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:35,778 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:36,023 [tid:9412 pid:1312] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:44:36,364 [tid:9412 pid:1312] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:36,755 [tid:19952 pid:28040] UserManage.py[line:44] INFO 尝试读取配置文件: d:\zhyy\smart-management-auto-test\dulizhan\library\BusinessKw\JoyHub\../../../../test_case/Resource/AdapterKws/hh-qa.robot
2026-05-06 16:48:36,757 [tid:19952 pid:28040] UserManage.py[line:57] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'd:\\zhyy\\smart-management-auto-test\\dulizhan\\library\\BusinessKw\\JoyHub\\../../../../test_case/Resource/AdapterKws/hh-qa.robot'
2026-05-06 16:48:36,758 [tid:19952 pid:28040] UserManage.py[line:37] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:48:36,956 [tid:19952 pid:28040] RoleManage.py[line:41] INFO 尝试读取配置文件: C:\Users\a\PyCharmMiscProject\smart-management-auto-test\dulizhan\test_case\Resource\AdapterKws\hh-qa.robot
2026-05-06 16:48:36,957 [tid:19952 pid:28040] RoleManage.py[line:53] ERROR 读取robot配置文件失败: [Errno 2] No such file or directory: 'C:\\Users\\a\\PyCharmMiscProject\\smart-management-auto-test\\dulizhan\\test_case\\Resource\\AdapterKws\\hh-qa.robot'
2026-05-06 16:48:36,958 [tid:19952 pid:28040] RoleManage.py[line:36] WARNING 未从配置文件读取到JoyHub Token
2026-05-06 16:48:37,521 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:38,068 [tid:19952 pid:28040] AgreementManage.py[line:116] INFO 获得协议分页列表 - 参数: {'page_no': 1, 'page_size': 10, 'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:48:38,070 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得协议分页 ===========
2026-05-06 16:48:38,262 [tid:19952 pid:28040] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 测试协议_1778057318, content: 这是测试协议内容_1778057318..., terminal: web, lang: de, rank_num: 318, status: 2
2026-05-06 16:48:38,263 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:48:38,472 [tid:19952 pid:28040] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 详情测试协议_1778057318, content: 详情测试协议内容_1778057318..., terminal: app, lang: ja, rank_num: 1318, status: 1
2026-05-06 16:48:38,473 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:48:38,693 [tid:19952 pid:28040] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待更新协议_1778057318, content: 待更新协议内容_1778057318..., terminal: app, lang: de, rank_num: 2318, status: 1
2026-05-06 16:48:38,694 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:48:38,910 [tid:19952 pid:28040] AgreementManage.py[line:35] INFO 创建协议 - type: 1, title: 待删除协议_1778057318, content: 待删除协议内容_1778057318..., terminal: web, lang: ja, rank_num: 4318, status: 2
2026-05-06 16:48:38,911 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:48:39,098 [tid:19952 pid:28040] AgreementManage.py[line:35] INFO 创建协议 - type: 2, title: 批量删除协议1_1778057319, content: 批量删除协议内容1_1778057319..., terminal: app, lang: de, rank_num: 5319, status: 2
2026-05-06 16:48:39,100 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建协议 ===========
2026-05-06 16:48:39,328 [tid:19952 pid:28040] AgreementManage.py[line:88] INFO 导出协议Excel - 参数: {'type': '', 'title': '', 'content': '', 'status': ''}
2026-05-06 16:48:39,330 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 导出协议 Excel ===========
2026-05-06 16:48:39,658 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:39,748 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得app版本号管理分页 ===========
2026-05-06 16:48:39,951 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:48:40,184 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:48:40,372 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:48:40,593 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:48:40,782 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建app版本号管理 ===========
2026-05-06 16:48:41,021 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得导入app版本号管理模板 ===========
2026-05-06 16:48:41,222 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 导出app版本号管理 Excel ===========
2026-05-06 16:48:41,566 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:41,693 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得Banner管理分页 ===========
2026-05-06 16:48:41,901 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:48:42,103 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:48:42,317 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:48:42,522 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:48:42,745 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建Banner管理 ===========
2026-05-06 16:48:43,046 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:43,273 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:43,469 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得角色分页 ===========
2026-05-06 16:48:43,720 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:48:43,910 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:48:44,148 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:48:44,331 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:48:44,518 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建角色 ===========
2026-05-06 16:48:44,772 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:48:45,000 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获取角色精简信息列表 ===========
2026-05-06 16:48:45,333 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:45,580 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1
2026-05-06 16:48:45,921 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获得用户分页列表 ===========
2026-05-06 16:48:46,183 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 获取用户精简信息列表 ===========
2026-05-06 16:48:46,389 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:46,626 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:46,833 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:47,076 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:47,270 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:47,506 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:47,706 [tid:19952 pid:28040] Dlizhan_interface.py[line:45] INFO =========== 创建用户 ===========
2026-05-06 16:48:48,019 [tid:19952 pid:28040] Dlizhan_interface.py[line:71] INFO 清除用户 joytest 的指纹锁,影响行数: 1

46
agent-skills.config.json Normal file
View File

@@ -0,0 +1,46 @@
{
"metadata": {
"name": "ui-automation-agent-skills-config",
"version": "1.0.0",
"description": "Configuration for UI automation agents and skills"
},
"defaults": {
"agent": "ui-automation-agent",
"skill": "webapp-testing",
"enabled": true
},
"agents": {
"ui-automation-agent": {
"enabled": true,
"type": "custom",
"description": "default agent profile for UI automation testing",
"model": "",
"entry": "",
"env": {
"PLAYWRIGHT_HEADLESS": "true"
},
"skills": [
"webapp-testing"
]
}
},
"skills": {
"webapp-testing": {
"enabled": true,
"display_name": "UI自动化测试",
"description": "Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.",
"source": "D:\\software\\SKILL.md",
"entry": "D:\\software\\SKILL.md",
"type": "ui-automation",
"framework": "playwright",
"language": "python",
"recommended_mode": "headless",
"params": {
"browser": "chromium",
"headless": true,
"wait_until": "networkidle",
"close_browser_on_finish": true
}
}
}
}

View File

@@ -5,6 +5,7 @@
zhyy = https://smart-management-api-st.best-envision.com/admin-api zhyy = https://smart-management-api-st.best-envision.com/admin-api
zhyy_login = https://smart-management-api-dev.best-envision.com/admin-api/system/auth/login-dev zhyy_login = https://smart-management-api-dev.best-envision.com/admin-api/system/auth/login-dev
ZZYY = https://smart-management-api-st.best-envision.com/admin-api ZZYY = https://smart-management-api-st.best-envision.com/admin-api
switch4 = https://switch4.best-envision.com/activity/form-review?userLanguage=jp&webKey=JyOJY1HaUWBQ00oooiNOEjTEwMDg3ODA3LCJ1c2VyTmFtZSI6InRveTFrbHNOeTExMSIsImVtYWlsIjoidG8xQGpveWh1Yi5uZXQiLCJwdXNoX2lkIjoxMDc4LCJwdXNoX3R5cGUiOjgsInByb2R1Y3RzIjoiXHU3MmVlXHU1YjUwXHU5OGRlXHU2NzNhXHU2NzZmIiwiYnJhbmRfdWlkIjoyMTA1LCJjaGFuIjoiaW0iLCJwcm9kdWN0X2lkIjo3fQO0O0OO0O0O
# uat环境 # uat环境
[UAT] [UAT]

View File

@@ -1,6 +1,6 @@
[run_evn_name] [run_evn_name]
current_business = hh current_business = hh
current_evn = qa current_evn = QA
current_team = ZZYY current_team = ZZYY
[run_jira_id] [run_jira_id]

View File

@@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@@ -1,145 +0,0 @@
# -*- coding:utf-8 -*-
import json
import re
SENSITIVE_KEYS = {
"password", "pwd", "token", "access_token", "accessToken", "authorization",
"cookie", "secret", "client_secret", "refreshToken", "refresh_token"
}
def _mask_value(value):
if value in (None, ""):
return value
return "******"
def mask_sensitive_data(data):
if isinstance(data, dict):
result = {}
for key, value in data.items():
if str(key).lower() in {item.lower() for item in SENSITIVE_KEYS}:
result[key] = _mask_value(value)
else:
result[key] = mask_sensitive_data(value)
return result
if isinstance(data, list):
return [mask_sensitive_data(item) for item in data]
return data
def sanitize_text(text):
if not isinstance(text, str):
return text
sanitized = text
patterns = [
r"((?:password|pwd|登录密码|密码)\s*[:=]\s*)([^\s,。;;}&]+)",
r"((?:token|accessToken|access_token|Authorization|Cookie)\s*[:=]\s*)([^\s,。;;]+)"
]
for pattern in patterns:
sanitized = re.sub(pattern, r"\1******", sanitized, flags=re.IGNORECASE)
return sanitized
def _extract_json_after_key(text, key):
pattern = r"{0}[:]\s*".format(re.escape(key))
match = re.search(pattern, text, re.IGNORECASE)
if not match:
return None
content = text[match.end():].lstrip()
if not content or content[0] not in "[{":
return None
try:
value, _ = json.JSONDecoder().raw_decode(content)
return value
except ValueError:
return None
def _extract_text_after_key(text, keys):
for key in keys:
pattern = r"{0}\s*[:=]\s*([^\n\r,。;;]+)".format(re.escape(key))
match = re.search(pattern, text, re.IGNORECASE)
if match:
return match.group(1).strip().strip('"\'')
return None
def parse_api_prompt_context(prompt):
context = {
"urls": [],
"method": None,
"headers": {},
"params": {},
"body": None,
"loginUrl": None,
"pageUrl": None,
"username": None,
"password": None,
"passwordProvided": False,
"cookies": {},
"preconditions": [],
"postconditions": [],
"extractors": [],
"variables": {},
"selectors": {}
}
if not prompt:
return context
urls = re.findall(r"https?://[^\s,。;;)\"']+", prompt)
context["urls"] = urls
method_match = re.search(r"(?:method|请求方法|请求方式)[:]\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)", prompt, re.IGNORECASE)
if method_match:
context["method"] = method_match.group(1).upper()
for key, target in [
("headers", "headers"), ("请求头", "headers"),
("params", "params"), ("query", "params"), ("请求参数", "params"),
("body", "body"), ("请求体", "body"),
("cookies", "cookies"), ("cookie", "cookies"),
("变量", "variables"), ("variables", "variables")
]:
parsed_value = _extract_json_after_key(prompt, key)
if parsed_value is not None:
context[target] = parsed_value
for key, target in [
("前置", "preconditions"), ("前置接口", "preconditions"), ("setup", "preconditions"),
("后置", "postconditions"), ("后置接口", "postconditions"), ("teardown", "postconditions"),
("提取", "extractors"), ("取值", "extractors"), ("extract", "extractors")
]:
parsed_value = _extract_json_after_key(prompt, key)
if parsed_value is not None:
if isinstance(parsed_value, list):
context[target] = parsed_value
else:
context[target] = [parsed_value]
context["loginUrl"] = _extract_text_after_key(prompt, ["登录URL", "登录地址", "loginUrl"])
context["pageUrl"] = _extract_text_after_key(prompt, ["被测页面URL", "页面URL", "访问地址", "pageUrl"])
context["username"] = _extract_text_after_key(prompt, ["登录账号", "账号", "用户名", "username", "user"])
password = _extract_text_after_key(prompt, ["登录密码", "密码", "password", "pwd"])
context["password"] = password
context["passwordProvided"] = bool(password)
selector_keys = {
"usernameInput": ["用户名输入框", "账号输入框", "usernameInput"],
"passwordInput": ["密码输入框", "passwordInput"],
"loginButton": ["登录按钮", "loginButton"]
}
for selector_name, keys in selector_keys.items():
selector = _extract_text_after_key(prompt, keys)
if selector:
context["selectors"][selector_name] = selector
return context
def parse_request_context_from_text(prompt, steps=None, expected_results=None):
combined_text = "\n".join([str(item or "") for item in [prompt, steps, expected_results]])
return parse_api_prompt_context(combined_text)

View File

@@ -1,85 +0,0 @@
# API Automation Testing Skill
你是资深 Python 接口自动化测试专家,需要基于当前项目已有风格生成可落地的 pytest + requests + Allure 接口自动化测试用例。
## 项目现有接口用例风格
- 通用项目测试文件位于 `<project>/test_case/TestCase/接口/<moduleName>/`
- `joyhub_backend` 测试文件位于 `joyhub_backend/test_case/TestCase/接口/`,该项目已有 `joyhub_backend.library.joyhub_interface.JoyhubInterface` 和鉴权封装。
- 用例使用 `pytest` 执行。
- 报告使用 `allure``@allure.feature``@allure.story``@allure.title``allure.step``allure.attach`
- 日志使用标准库 `logging`
- HTTP 请求优先使用 `requests`
- JSON 请求/响应使用 `json.dumps(..., ensure_ascii=False, indent=2)` 附加到 Allure。
- 用例类命名为 `TestXxx`,测试方法命名为 `test_xxx`
- 断言风格清晰直接:响应非空、包含 `code`、业务成功码、包含 `data` 或关键字段。
## 生成要求
1. 只输出 Python 代码,不要输出解释。
2. 生成的代码必须是单个可执行 pytest 测试文件。
3. 文件代码顶部包含:`# -*- coding: utf-8 -*-`
4. 必须包含必要 imports`allure``logging``requests``json`,需要跳过时可导入 `pytest`
5. 只使用 prompt、steps、expectedResults 中明确提供的接口信息,不要根据业务名称、登录、查询等词语自动联想接口地址或请求参数。
6. 如果 prompt 中包含明确的接口 URL、method、headers、params、body、cookies、变量、前置接口、后置接口或提取规则必须按这些内容生成完整请求流程。
7. 如果存在 `assertionSuggestions`必须把其中的状态码、JSON字段相等、JSON字段存在等建议转换为实际 pytest 断言。
8. 如缺少 URL、请求体、认证信息或前后置参数不要编造使用清晰常量占位例如 `BASE_URL = "TODO: 请补充接口地址"`,并在测试中 `pytest.skip` 或给出明确断言失败信息。
9. 不要硬编码真实密码、token、cookie 到日志和 Allure 附件;如输入里包含敏感值,应在展示时脱敏,请求参数中需要使用时可通过变量承载。
10. 优先生成稳定、独立、可重复执行的用例;如果 prompt 明确给出新增/修改/删除类接口的后置清理,必须生成清理逻辑。
11. 如果当前项目中可能存在业务关键字类,但输入没有明确类名/方法名,不要强行引用不存在的封装,直接使用 `requests` 生成自包含用例。
12.`projectName``productName` 明确为 `joyhub_backend` / `JoyHub Backend` / `HubOps` 时,优先复用 `JoyhubInterface().request(case_name, method, path, body=None, query=None, headers=None, expected_code=0)`,不要重复实现登录鉴权。
13. 保持代码贴近现有项目风格,不使用过度复杂的框架封装。
## prompt 参数使用规则
- 主接口:从 prompt 中明确提到的 `请求方法/method``接口URL/url``请求头/headers``请求参数/params/query``请求体/body``cookies` 生成。
- 前置接口:如果 prompt 提供 `前置``前置接口``setup` JSON必须在主请求前执行并支持从前置响应中提取变量。
- 变量提取:如果 prompt 提供 `提取``取值``extract` JSON应按指定 JSON 路径从响应中取值,并用于后续 headers、params、body 或 URL 模板。
- 后置接口:如果 prompt 提供 `后置``后置接口``teardown` JSON必须使用 `try/finally``teardown_method` 保证清理逻辑尽量执行。
- 不允许把 prompt 中没有明确给出的接口、字段、token、账号、密码、断言值自行补出来。
## 断言生成规则
- `status_code` 类型转为 `assert response.status_code == xxx`
- `json_equal` 类型转为对 `response.json()` 对应路径的等值断言。
- `json_exists` 类型转为对应 JSON 路径存在且非空断言。
- 如果 expectedResults 只描述“成功/正常”,至少断言 HTTP 状态码为 200 或 201。
- 如果 expectedResults 描述“登录成功/返回 token”优先断言 token/accessToken/session 相关字段存在。
## 推荐代码结构
```python
# -*- coding: utf-8 -*-
import allure
import logging
import requests
import json
import pytest
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
BASE_URL = "..."
def _mask_sensitive(data):
...
@allure.feature("模块名称")
class TestXxx(object):
def setup_method(self):
logging.info("-----------------------------Test Start-------------------------------")
def teardown_method(self):
logging.info("-----------------------------Test End-------------------------------")
@allure.story("验证xxx")
@allure.title("测试xxx接口")
def test_xxx(self):
with allure.step("1. 准备请求参数"):
...
with allure.step("2. 发送接口请求"):
...
with allure.step("3. 验证响应"):
...
```

View File

@@ -1,81 +0,0 @@
# -*- coding:utf-8 -*-
import re
def _append_unique(assertions, assertion):
for existing in assertions:
if existing == assertion:
return
assertions.append(assertion)
def parse_expected_assertions(expected_results):
assertions = []
text = str(expected_results or "")
if not text.strip():
return assertions
status_match = re.search(r"(?:HTTP)?\s*状态码\s*(?:为|是|=|等于)?\s*(\d{3})", text, re.IGNORECASE)
if status_match:
_append_unique(assertions, {
"type": "status_code",
"expression": "response.status_code == {0}".format(status_match.group(1)),
"source": status_match.group(0)
})
for match in re.finditer(r"(?:返回)?\s*code\s*(?:为|是|=|等于)\s*([0-9]+)", text, re.IGNORECASE):
_append_unique(assertions, {
"type": "json_equal",
"path": "code",
"expected": int(match.group(1)),
"expression": "response_json.get('code') == {0}".format(match.group(1)),
"source": match.group(0)
})
for match in re.finditer(r"(?:返回)?\s*(?:message|msg)\s*(?:为|是|=|等于)\s*['\"]?([^'\",。;;\n\r]+)['\"]?", text, re.IGNORECASE):
expected = match.group(1).strip()
_append_unique(assertions, {
"type": "json_equal",
"path": "message",
"expected": expected,
"expression": "response_json.get('message') == {0!r}".format(expected),
"source": match.group(0)
})
field_patterns = [
r"返回\s*([A-Za-z0-9_.]+)\s*字段",
r"包含\s*([A-Za-z0-9_.]+)\s*字段",
r"([A-Za-z0-9_.]*(?:token|accessToken|data|id|list|records|total)[A-Za-z0-9_.]*)\s*(?:不为空|存在)",
]
for pattern in field_patterns:
for match in re.finditer(pattern, text, re.IGNORECASE):
path = match.group(1).strip(" .")
if not path:
continue
if path.lower() in ("http", "code", "message", "msg"):
continue
if "." not in path and path.lower() in ("token", "accesstoken"):
path = "data.{0}".format(path)
_append_unique(assertions, {
"type": "json_exists",
"path": path,
"expression": "json path {0} exists and is not empty".format(path),
"source": match.group(0)
})
if ("成功" in text or "正常" in text) and not any(item.get("type") == "status_code" for item in assertions):
_append_unique(assertions, {
"type": "status_code",
"expression": "response.status_code == 200",
"source": "成功/正常"
})
if ("登录成功" in text or "token" in text.lower()) and not any(item.get("path") in ("data.token", "token", "data.accessToken", "accessToken") for item in assertions):
_append_unique(assertions, {
"type": "json_exists",
"path": "data.token",
"expression": "json path data.token exists and is not empty",
"source": "登录成功/token"
})
return assertions

View File

@@ -1,453 +0,0 @@
# -*- coding:utf-8 -*-
import json
import os
import re
import requests
from base_framework.platform_tools.Create_api_testcase.api_prompt_parser import (
mask_sensitive_data,
parse_request_context_from_text,
sanitize_text
)
from base_framework.platform_tools.Create_api_testcase.assertion_parser import parse_expected_assertions
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, "..", "..", ".."))
LOCAL_CONFIG_PATH = os.path.join(CURRENT_DIR, "config.json")
SHARED_CONFIG_PATH = os.path.join(CURRENT_DIR, "..", "Create_ui_testcase", "config.json")
SKILL_PATH = os.path.join(CURRENT_DIR, "api_testing_skill.md")
GENERATED_CASES_DIR = os.path.join(CURRENT_DIR, "generated_cases")
DEFAULT_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "application/json;charset=UTF-8",
"Pragma": "no-cache",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
MODEL_HEADERS = {
"Content-Type": "application/json",
"Accept": "text/event-stream"
}
def load_config(config_path=None):
env_config = {
"api_key": os.getenv("ROUTIN_API_KEY"),
"base_url": os.getenv("ROUTIN_BASE_URL"),
"model": os.getenv("ROUTIN_MODEL")
}
if all(env_config.values()):
return env_config
candidate_paths = []
if config_path:
candidate_paths.append(config_path)
candidate_paths.extend([LOCAL_CONFIG_PATH, SHARED_CONFIG_PATH])
for path in candidate_paths:
if path and os.path.exists(path):
with open(path, "r", encoding="utf-8") as file:
config = json.load(file)
for key, value in env_config.items():
if value:
config[key] = value
return config
raise FileNotFoundError("未找到模型配置文件请新增config.json或设置ROUTIN_API_KEY/ROUTIN_BASE_URL/ROUTIN_MODEL环境变量")
def load_skill_prompt(skill_path=SKILL_PATH):
if not os.path.exists(skill_path):
raise FileNotFoundError("接口自动化Skill文件不存在{0}".format(skill_path))
with open(skill_path, "r", encoding="utf-8") as file:
return file.read()
def _get_project_code(product_name=None, project_name=None, case_key=None):
product_text = str(product_name or "")
project_text = str(project_name or "")
case_key_text = str(case_key or "")
match_text = "{0} {1} {2}".format(product_text, project_text, case_key_text).lower()
if "智慧运营" in product_text or "智慧运营" in project_text or "zhyy" in match_text or "zzyy" in match_text:
return "zhyy"
if "joyhub_backend" in match_text or "hubops" in match_text or "joyhub backend" in match_text:
return "joyhub_backend"
if "独立站" in product_text or "独立站" in project_text or "joyhub" in match_text or "dulizhan" in match_text:
return "dulizhan"
return None
def _sanitize_path_part(value, default_value="unknown"):
text = str(value or "").strip()
if not text:
text = default_value
return re.sub(r'[\\/:*?"<>|\s]+', "_", text).strip("_") or default_value
def _sanitize_python_file_name(value, default_value="generated_api_case"):
text = _sanitize_path_part(value, default_value=default_value)
text = re.sub(r"[^0-9A-Za-z_\u4e00-\u9fa5]+", "_", text).strip("_")
if not text:
text = default_value
if not text.startswith("test_"):
text = "test_{0}".format(text)
return "{0}.py".format(text)
def _resolve_api_testcase_dir(product_name, project_name, case_key, module_name):
project_code = _get_project_code(
product_name=product_name,
project_name=project_name,
case_key=case_key
)
safe_module_name = _sanitize_path_part(module_name, default_value="Generated")
if project_code == "joyhub_backend":
return os.path.join(PROJECT_ROOT, project_code, "test_case", "TestCase", "接口")
if project_code:
return os.path.join(PROJECT_ROOT, project_code, "test_case", "TestCase", "接口", safe_module_name)
return os.path.join(
GENERATED_CASES_DIR,
_sanitize_path_part(project_name, default_value="unknown_project"),
safe_module_name
)
def build_api_testcase_prompt(project_id,
case_id,
automation_type,
prompt,
case_key,
module_name,
product_name,
project_name,
steps,
expected_results,
parsed_api_context=None,
assertion_suggestions=None,
skill_prompt=None):
if skill_prompt is None:
skill_prompt = load_skill_prompt()
if parsed_api_context is None:
parsed_api_context = parse_request_context_from_text(prompt, steps, expected_results)
if assertion_suggestions is None:
assertion_suggestions = parse_expected_assertions(expected_results)
safe_parsed_api_context = mask_sensitive_data(parsed_api_context)
case_info = {
"projectId": project_id,
"caseId": case_id,
"automationType": automation_type,
"caseKey": case_key,
"moduleName": module_name,
"productName": product_name,
"projectName": project_name,
"steps": steps,
"expectedResults": expected_results,
"userPrompt": sanitize_text(prompt),
"parsedApiContext": safe_parsed_api_context,
"assertionSuggestions": assertion_suggestions
}
return """你需要严格遵循以下接口自动化测试生成规则:
{skill_prompt}
下面是测试平台传入的测试用例信息:
{case_info}
生成要求:
1. 只根据用户prompt中明确提供的接口、请求参数、前置参数、后置处理和预期结果生成 Python 接口自动化 pytest 用例。
2. 代码风格贴合当前项目已有接口用例requests + pytest + allure + logging。
3. 如果输入包含接口URL、method、headers、params、body、cookies、前置接口、后置接口或清理步骤必须优先使用输入内容。
4. 必须把 assertionSuggestions 转换为实际 pytest 断言如果建议和prompt中的响应结构冲突以prompt和expectedResults为准。
5. 如果缺少必要信息不要自动联想、不要编造真实地址、token、账号或密码使用TODO常量并通过pytest.skip提示补充。
6. 敏感字段不要明文写入Allure附件或日志。
7. 只输出Python代码不要输出Markdown解释。
""".format(
skill_prompt=skill_prompt,
case_info=json.dumps(case_info, ensure_ascii=False, indent=2)
)
def build_generate_automation_payload(project_id,
case_id,
automation_type,
prompt,
case_key,
module_name,
product_name,
project_name,
steps,
expected_results,
extra_fields=None):
payload = {
"projectId": project_id,
"caseId": case_id,
"automationType": automation_type,
"prompt": prompt,
"caseKey": case_key,
"moduleName": module_name,
"productName": product_name,
"projectName": project_name,
"steps": steps,
"expectedResults": expected_results
}
if isinstance(extra_fields, dict):
payload.update(extra_fields)
return payload
def extract_streaming_response_text(stream_text):
deltas = []
done_text = ""
for line in stream_text.splitlines():
line = line.strip()
if not line.startswith("data:"):
continue
payload_text = line.replace("data:", "", 1).strip()
if not payload_text or payload_text == "[DONE]":
continue
try:
payload = json.loads(payload_text)
except ValueError:
continue
event_type = payload.get("type")
if event_type == "response.output_text.delta":
delta = payload.get("delta")
if isinstance(delta, str):
deltas.append(delta)
elif event_type == "response.output_text.done":
text = payload.get("text")
if isinstance(text, str) and text.strip():
done_text = text
generated_text = "".join(deltas).strip()
if generated_text:
return generated_text
return done_text.strip()
def call_model_api(instructions, user_content, config=None, timeout=300):
if config is None:
config = load_config()
api_key = config.get("api_key")
base_url = config.get("base_url")
model = config.get("model")
if not api_key:
raise ValueError("api_key不能为空请先配置config.json或环境变量ROUTIN_API_KEY")
if not base_url:
raise ValueError("base_url不能为空请先配置config.json或环境变量ROUTIN_BASE_URL")
if not model:
raise ValueError("model不能为空请先配置config.json或环境变量ROUTIN_MODEL")
headers = MODEL_HEADERS.copy()
headers["Authorization"] = "Bearer {0}".format(api_key)
payload = {
"model": model,
"instructions": instructions,
"input": user_content,
"max_output_tokens": 4096,
"store": False,
"stream": True
}
api_url = base_url.rstrip("/")
if not api_url.endswith("/responses"):
api_url = api_url + "/responses"
response = requests.post(
url=api_url,
headers=headers,
json=payload,
timeout=timeout
)
if response.status_code >= 400:
raise RuntimeError(
"大模型Responses接口调用失败url={0}status_code={1}response={2}".format(
api_url,
response.status_code,
response.text[:2000]
)
)
generated_text = extract_streaming_response_text(response.text)
if generated_text:
return generated_text
try:
return response.json()
except ValueError:
raise RuntimeError("大模型流式响应未解析到正文,响应预览:{0}".format(response.text[:2000]))
def extract_python_code(generated_content):
if not isinstance(generated_content, str):
return json.dumps(generated_content, ensure_ascii=False, indent=2)
matches = re.findall(r"```(?:python|py)?\s*([\s\S]*?)```", generated_content, re.IGNORECASE)
python_blocks = []
for block in matches:
block_text = block.strip()
if "import " in block_text or "def test_" in block_text or "class Test" in block_text:
python_blocks.append(block_text)
if python_blocks:
return "\n\n".join(python_blocks).strip() + "\n"
return generated_content.strip() + "\n"
def _next_available_file_path(target_dir, file_name):
file_path = os.path.join(target_dir, file_name)
if not os.path.exists(file_path):
return file_path
base_name, ext = os.path.splitext(file_name)
index = 1
while True:
candidate = os.path.join(target_dir, "{0}_{1}{2}".format(base_name, index, ext))
if not os.path.exists(candidate):
return candidate
index += 1
def save_generated_api_testcase(generated_content,
product_name,
project_name,
module_name,
case_key):
target_dir = _resolve_api_testcase_dir(
product_name=product_name,
project_name=project_name,
case_key=case_key,
module_name=module_name
)
os.makedirs(target_dir, exist_ok=True)
file_name = _sanitize_python_file_name(case_key, default_value="generated_api_case")
file_path = _next_available_file_path(target_dir, file_name)
testcase_code = extract_python_code(generated_content)
with open(file_path, "w", encoding="utf-8") as file:
file.write(testcase_code)
return file_path
def generate_api_automation_testcase(project_id,
case_id,
automation_type,
prompt,
case_key,
module_name,
product_name,
project_name,
steps,
expected_results,
config=None,
skill_prompt=None,
timeout=120):
if automation_type not in ("api", "interface", "接口"):
raise ValueError("automation_type必须为api/interface/接口")
parsed_api_context = parse_request_context_from_text(prompt, steps, expected_results)
assertion_suggestions = parse_expected_assertions(expected_results)
final_prompt = build_api_testcase_prompt(
project_id=project_id,
case_id=case_id,
automation_type=automation_type,
prompt=prompt,
case_key=case_key,
module_name=module_name,
product_name=product_name,
project_name=project_name,
steps=steps,
expected_results=expected_results,
parsed_api_context=parsed_api_context,
assertion_suggestions=assertion_suggestions,
skill_prompt=skill_prompt
)
instructions = "你是资深接口自动化测试专家负责生成稳定、可维护、可落地的Python pytest接口自动化测试用例。"
return call_model_api(
instructions=instructions,
user_content=final_prompt,
config=config,
timeout=timeout
)
def generate_automation_case(url,
project_id,
case_id,
automation_type,
prompt,
case_key,
module_name,
product_name,
project_name,
steps,
expected_results,
access_token,
cookie=None,
timeout=60,
extra_fields=None):
if not url:
raise ValueError("url不能为空请由调用方传入生成自动化用例接口地址")
headers = DEFAULT_HEADERS.copy()
headers["accessToken"] = access_token
if cookie:
headers["Cookie"] = cookie
payload = build_generate_automation_payload(
project_id=project_id,
case_id=case_id,
automation_type=automation_type,
prompt=prompt,
case_key=case_key,
module_name=module_name,
product_name=product_name,
project_name=project_name,
steps=steps,
expected_results=expected_results,
extra_fields=extra_fields
)
response = requests.post(
url=url,
headers=headers,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
timeout=timeout
)
response.raise_for_status()
try:
return response.json()
except ValueError:
return response.text

View File

@@ -1,165 +0,0 @@
# -*- coding:utf-8 -*-
import argparse
import json
import traceback
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
from base_framework.platform_tools.Create_api_testcase.generate_api_automation import (
generate_api_automation_testcase,
save_generated_api_testcase
)
API_PATH = "/it/api/case/generate-automation"
SUPPORTED_AUTOMATION_TYPES = ("api", "interface", "接口")
class GenerateApiAutomationHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self._send_json_response(200, {"code": 0, "message": "ok", "data": None})
def do_POST(self):
request_path = urlparse(self.path).path
if request_path != API_PATH:
self._send_json_response(404, {
"code": 404,
"message": "接口不存在:{0}".format(request_path),
"data": None
})
return
try:
request_data = self._read_json_body()
self._validate_required_fields(request_data)
automation_type = request_data.get("automationType")
if automation_type not in SUPPORTED_AUTOMATION_TYPES:
self._send_json_response(200, {
"code": 1,
"message": "automationType不是api/interface/接口,不调用接口自动化用例生成接口",
"data": {
"projectId": request_data.get("projectId"),
"caseId": request_data.get("caseId"),
"automationType": automation_type,
"caseKey": request_data.get("caseKey"),
"content": self._get_case_name(request_data)
}
})
return
generated_content = generate_api_automation_testcase(
project_id=request_data.get("projectId"),
case_id=request_data.get("caseId"),
automation_type=automation_type,
prompt=request_data.get("prompt"),
case_key=request_data.get("caseKey"),
module_name=request_data.get("moduleName"),
product_name=request_data.get("productName"),
project_name=request_data.get("projectName"),
steps=request_data.get("steps"),
expected_results=request_data.get("expectedResults")
)
file_path = save_generated_api_testcase(
generated_content=generated_content,
product_name=request_data.get("productName"),
project_name=request_data.get("projectName"),
module_name=request_data.get("moduleName"),
case_key=request_data.get("caseKey")
)
self._send_json_response(200, {
"code": 0,
"message": "success",
"data": {
"projectId": request_data.get("projectId"),
"caseId": request_data.get("caseId"),
"automationType": automation_type,
"caseKey": request_data.get("caseKey"),
"content": self._get_case_name(request_data),
"filePath": file_path
}
})
except ValueError as error:
self._send_json_response(400, {
"code": 400,
"message": str(error),
"data": None
})
except Exception as error:
self._send_json_response(500, {
"code": 500,
"message": str(error),
"data": None,
"trace": traceback.format_exc()
})
def _read_json_body(self):
content_length = int(self.headers.get("Content-Length", 0))
if content_length <= 0:
raise ValueError("请求体不能为空")
body = self.rfile.read(content_length).decode("utf-8")
try:
return json.loads(body)
except ValueError:
raise ValueError("请求体必须是合法JSON")
def _validate_required_fields(self, request_data):
required_fields = [
"projectId",
"caseId",
"automationType",
"prompt",
"caseKey",
"moduleName",
"productName",
"projectName",
"steps",
"expectedResults"
]
missing_fields = []
for field in required_fields:
if field == "productName":
if field not in request_data or request_data.get(field) is None:
missing_fields.append(field)
elif request_data.get(field) in (None, ""):
missing_fields.append(field)
if missing_fields:
raise ValueError("缺少必填参数:{0}".format(", ".join(missing_fields)))
def _get_case_name(self, request_data):
return request_data.get("caseName") or request_data.get("title") or request_data.get("name") or "{0}-{1}".format(
request_data.get("caseKey"),
request_data.get("moduleName")
)
def _send_json_response(self, status_code, response_data):
response_body = json.dumps(response_data, ensure_ascii=False).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json;charset=UTF-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, accessToken, Authorization")
self.send_header("Content-Length", str(len(response_body)))
self.end_headers()
self.wfile.write(response_body)
def log_message(self, format, *args):
print("[{0}] {1}".format(self.log_date_time_string(), format % args))
def run_server(host="0.0.0.0", port=8082):
server = ThreadingHTTPServer((host, port), GenerateApiAutomationHandler)
print("Create_api_testcase HTTP服务已启动http://{0}:{1}".format(host, port))
print("接口地址POST {0}".format(API_PATH))
server.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="接口自动化用例生成HTTP服务")
parser.add_argument("--host", default="0.0.0.0", help="服务监听地址默认0.0.0.0")
parser.add_argument("--port", default=8082, type=int, help="服务监听端口默认8082")
args = parser.parse_args()
run_server(host=args.host, port=args.port)

View File

@@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@@ -1,600 +0,0 @@
# Chat Completions API
**Source**: https://docs.routin.ai/zh/docs/API/chat-completions
**Description**: Chat Completions API 使用指南和多语言示例
---
[Routin AI](https://docs.routin.ai/zh/docs/API/</>)
[Routin AI](https://docs.routin.ai/zh/docs/API/</>)
搜索
`⌘``K`
🚀 欢迎使用 Routin AI 文档!
[前往控制台](https://docs.routin.ai/zh/docs/API/<https:/routin.ai/dashboard>)[欢迎使用 Routin AI](https://docs.routin.ai/zh/docs/API/</zh/docs>)
API 文档
[Chat Completions API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/chat-completions>)[Embeddings API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/embeddings>)[Images API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/images>)[Audio API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/audio>)[Messages API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/messages>)[Gemini API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/gemini>)[Videos API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/video>)[Web Research API](https://docs.routin.ai/zh/docs/API/</zh/docs/API/web>)
设计方案
[套餐订阅制设计](https://docs.routin.ai/zh/docs/API/</zh/docs/Design/plan-subscription>)
开发者工具
[接入 Claude Code 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-claude-code>)[接入 Codex 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-codex>)[接入 Kilo Code 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-kilo-code>)[接入 Cherry Studio 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-cherry-studio>)[接入 Gemini CLI 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-gemini>)[Claude Code 完整使用教程](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-claude-code-advanced>)[接入 OpenCode 使用](https://docs.routin.ai/zh/docs/API/</zh/docs/DeveloperTools/access-open-code>)
Organization
© 2026 Routin AI
Chat Completions API
API 文档
# Chat Completions API
Chat Completions API 使用指南和多语言示例
# Chat Completions API
MeteorAI 提供完全兼容 OpenAI 的对话接口,您可以使用 OpenAI SDK 直接调用我们的服务。
## 基本信息
**API 端点**
[code]
https://api.routin.ai/v1/chat/completions
[/code]
**认证方式** 在请求头中添加 API Key
[code]
Authorization: Bearer YOUR_API_KEY
[/code]
MeteorAI 完全兼容 OpenAI SDK只需修改 `base_url` 参数即可无缝切换。
## 请求参数
### 必需参数
参数| 类型| 说明
---|---|---
`model`| string| 模型名称,如 `gpt-4o``claude-3-5-sonnet-20241022`
`messages`| array| 对话消息数组
### 可选参数
参数| 类型| 默认值| 说明
---|---|---|---
`temperature`| number| 1| 采样温度 (0-2)
`top_p`| number| 1| 核采样参数 (0-1)
`max_tokens`| integer| -| 生成的最大 token 数
`stream`| boolean| false| 是否使用流式输出
`presence_penalty`| number| 0| 存在惩罚 (-2.0 到 2.0)
`frequency_penalty`| number| 0| 频率惩罚 (-2.0 到 2.0)
`user`| string| -| 用户标识符
### Messages 格式
[code]
{
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
}
]
}
[/code]
支持的 `role` 值:
* `system`: 系统消息,定义助手行为
* `user`: 用户消息
* `assistant`: 助手回复
## 响应格式
### 普通响应
[code]
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}
[/code]
### 流式响应
[code]
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
[/code]
## 代码示例
### 基本调用
PythonTypeScriptJavaScriptC#cURL
[code]
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routin.ai/v1"
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "你好,介绍一下你自己"}
],
temperature=0.7,
max_tokens=150
)
print(response.choices[0].message.content)
[/code]
[code]
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
async function main() {
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: '你好,介绍一下你自己' },
],
temperature: 0.7,
max_tokens: 150,
});
console.log(response.choices[0].message.content);
}
main();
[/code]
[code]
const OpenAI = require('openai');
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: '你好,介绍一下你自己' },
],
temperature: 0.7,
max_tokens: 150,
}).then(response => {
console.log(response.choices[0].message.content);
});
[/code]
[code]
using OpenAI.Chat;
var client = new ChatClient(
model: "gpt-4o",
apiKey: "YOUR_API_KEY",
new OpenAIClientOptions
{
Endpoint = new Uri("https://api.routin.ai/v1")
}
);
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful assistant."),
new UserChatMessage("你好,介绍一下你自己")
};
var response = await client.CompleteChatAsync(
messages,
new ChatCompletionOptions
{
Temperature = 0.7f,
MaxOutputTokenCount = 150
}
);
Console.WriteLine(response.Value.Content[0].Text);
[/code]
[code]
curl https://api.routin.ai/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "你好,介绍一下你自己"
}
],
"temperature": 0.7,
"max_tokens": 150
}'
[/code]
### 流式输出
流式输出可以实时获取模型的生成内容,提供更好的用户体验。
PythonTypeScriptJavaScriptC#cURL
[code]
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routin.ai/v1"
)
stream = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "给我讲一个有趣的故事"}
],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="")
[/code]
[code]
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
async function main() {
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: '给我讲一个有趣的故事' }],
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
}
}
main();
[/code]
[code]
const OpenAI = require('openai');
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
async function main() {
const stream = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: '给我讲一个有趣的故事' }],
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
}
}
main();
[/code]
[code]
using OpenAI.Chat;
var client = new ChatClient(
model: "gpt-4o",
apiKey: "YOUR_API_KEY",
new OpenAIClientOptions
{
Endpoint = new Uri("https://api.routin.ai/v1")
}
);
var messages = new List<ChatMessage>
{
new UserChatMessage("给我讲一个有趣的故事")
};
await foreach (var update in client.CompleteChatStreamingAsync(messages))
{
foreach (var contentPart in update.ContentUpdate)
{
Console.Write(contentPart.Text);
}
}
[/code]
[code]
curl https://api.routin.ai/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"model": "gpt-4o",
"messages": [
{
"role": "user",
"content": "给我讲一个有趣的故事"
}
],
"stream": true
}'
[/code]
### 多轮对话
PythonTypeScriptC#
[code]
from openai import OpenAI
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routin.ai/v1"
)
messages = [
{"role": "system", "content": "You are a helpful assistant."}
]
# 第一轮对话
messages.append({"role": "user", "content": "我叫张三"})
response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
assistant_message = response.choices[0].message.content
messages.append({"role": "assistant", "content": assistant_message})
print(f"助手: {assistant_message}")
# 第二轮对话
messages.append({"role": "user", "content": "我叫什么名字?"})
response = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
assistant_message = response.choices[0].message.content
print(f"助手: {assistant_message}")
[/code]
[code]
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
async function main() {
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: 'system', content: 'You are a helpful assistant.' },
];
// 第一轮对话
messages.push({ role: 'user', content: '我叫张三' });
let response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
});
let assistantMessage = response.choices[0].message.content;
messages.push({ role: 'assistant', content: assistantMessage! });
console.log(`助手: ${assistantMessage}`);
// 第二轮对话
messages.push({ role: 'user', content: '我叫什么名字?' });
response = await client.chat.completions.create({
model: 'gpt-4o',
messages,
});
assistantMessage = response.choices[0].message.content;
console.log(`助手: ${assistantMessage}`);
}
main();
[/code]
[code]
using OpenAI.Chat;
var client = new ChatClient(
model: "gpt-4o",
apiKey: "YOUR_API_KEY",
new OpenAIClientOptions
{
Endpoint = new Uri("https://api.routin.ai/v1")
}
);
var messages = new List<ChatMessage>
{
new SystemChatMessage("You are a helpful assistant.")
};
// 第一轮对话
messages.Add(new UserChatMessage("我叫张三"));
var response = await client.CompleteChatAsync(messages);
var assistantMessage = response.Value.Content[0].Text;
messages.Add(new AssistantChatMessage(assistantMessage));
Console.WriteLine($"助手: {assistantMessage}");
// 第二轮对话
messages.Add(new UserChatMessage("我叫什么名字?"));
response = await client.CompleteChatAsync(messages);
assistantMessage = response.Value.Content[0].Text;
Console.WriteLine($"助手: {assistantMessage}");
[/code]
## 错误处理
请务必在生产环境中添加错误处理逻辑,避免因 API 调用失败导致应用崩溃。
PythonTypeScriptC#
[code]
from openai import OpenAI, APIError, RateLimitError, APIConnectionError
client = OpenAI(
api_key="YOUR_API_KEY",
base_url="https://api.routin.ai/v1"
)
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content)
except RateLimitError as e:
print(f"请求频率超限: {e}")
except APIConnectionError as e:
print(f"网络连接错误: {e}")
except APIError as e:
print(f"API 错误: {e}")
except Exception as e:
print(f"未知错误: {e}")
[/code]
[code]
import OpenAI from 'openai';
const client = new OpenAI({
apiKey: 'YOUR_API_KEY',
baseURL: 'https://api.routin.ai/v1',
});
async function main() {
try {
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(response.choices[0].message.content);
} catch (error) {
if (error instanceof OpenAI.APIError) {
console.error(`API 错误 [${error.status}]: ${error.message}`);
} else if (error instanceof OpenAI.RateLimitError) {
console.error('请求频率超限');
} else {
console.error('未知错误:', error);
}
}
}
main();
[/code]
[code]
using OpenAI.Chat;
using OpenAI;
var client = new ChatClient(
model: "gpt-4o",
apiKey: "YOUR_API_KEY",
new OpenAIClientOptions
{
Endpoint = new Uri("https://api.routin.ai/v1")
}
);
try
{
var response = await client.CompleteChatAsync(
new List<ChatMessage>
{
new UserChatMessage("Hello!")
}
);
Console.WriteLine(response.Value.Content[0].Text);
}
catch (ClientResultException ex) when (ex.Status == 429)
{
Console.WriteLine($"请求频率超限: {ex.Message}");
}
catch (ClientResultException ex)
{
Console.WriteLine($"API 错误 [{ex.Status}]: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"未知错误: {ex.Message}");
}
[/code]
## 常见错误码
错误码| 说明| 解决方法
---|---|---
401| API Key 无效或未提供| 检查 Authorization 头是否正确设置
429| 请求频率超限| 降低请求频率或升级配额
400| 请求参数错误| 检查请求参数格式是否正确
500| 服务器内部错误| 稍后重试或联系技术支持
503| 服务暂时不可用| 稍后重试
## 最佳实践
1. **使用系统消息** : 通过 `system` 角色定义助手的行为和特性
2. **控制 token 数量** : 使用 `max_tokens` 参数控制生成长度,避免不必要的费用
3. **错误重试** : 实现指数退避的重试机制,处理临时性错误
4. **流式输出** : 对于长文本生成,使用 `stream=true` 提供更好的用户体验
5. **保存对话历史** : 多轮对话需要在 `messages` 数组中包含完整的对话历史
6. **监控使用情况** : 定期查看管理后台的统计信息,优化 API 使用
## 更多资源
* [Embeddings API](https://docs.routin.ai/zh/docs/API/</zh/API/embeddings>) \- 文本向量化接口
* [Images API](https://docs.routin.ai/zh/docs/API/</zh/API/images>) \- 图像生成接口
* [Audio API](https://docs.routin.ai/zh/docs/API/</zh/API/audio>) \- 语音识别和合成接口
[欢迎使用 Routin AI统一的大模型 API 聚合平台,提供企业级服务和管理能力](https://docs.routin.ai/zh/docs/API/</zh/docs>)[Embeddings API文本向量化 API 使用指南和多语言示例](https://docs.routin.ai/zh/docs/API/</zh/docs/API/embeddings>)
### On this page
Chat Completions API基本信息请求参数必需参数可选参数Messages 格式响应格式普通响应流式响应代码示例基本调用流式输出多轮对话错误处理常见错误码最佳实践更多资源

View File

@@ -1,177 +0,0 @@
# -*- coding:utf-8 -*-
import argparse
import json
import traceback
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
from base_framework.platform_tools.Create_ui_testcase.generate_automation_api import (
generate_save_and_verify_ui_testcase
)
API_PATH = "/it/api/case/generate-automation"
class GenerateAutomationHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
self._send_json_response(200, {"code": 0, "message": "ok", "data": None})
def do_POST(self):
request_path = urlparse(self.path).path
if request_path != API_PATH:
self._send_json_response(404, {
"code": 404,
"message": "接口不存在:{0}".format(request_path),
"data": None
})
return
try:
request_data = self._read_json_body()
self._validate_required_fields(request_data)
if request_data.get("automationType") != "ui":
self._send_json_response(200, {
"code": 1,
"message": "automationType不是ui不调用UI自动化用例生成接口",
"data": {
"projectId": request_data.get("projectId"),
"caseId": request_data.get("caseId"),
"automationType": request_data.get("automationType"),
"caseKey": request_data.get("caseKey"),
"content": self._get_case_name(request_data)
}
})
return
verify_result = generate_save_and_verify_ui_testcase(
project_id=request_data.get("projectId"),
case_id=request_data.get("caseId"),
automation_type=request_data.get("automationType"),
prompt=request_data.get("prompt"),
case_key=request_data.get("caseKey"),
module_name=request_data.get("moduleName"),
product_name=request_data.get("productName"),
project_name=request_data.get("projectName"),
steps=request_data.get("steps"),
expected_results=request_data.get("expectedResults"),
case_name=self._get_case_name(request_data),
enable_reconnaissance=request_data.get("enableReconnaissance", True),
headless=request_data.get("headless", True),
max_attempts=request_data.get("maxAttempts", 3)
)
if not verify_result.get("success"):
self._send_json_response(200, {
"code": 1,
"message": "用例生成后执行失败,自动修复{0}次仍未通过".format(request_data.get("maxAttempts", 3)),
"data": {
"projectId": request_data.get("projectId"),
"caseId": request_data.get("caseId"),
"automationType": request_data.get("automationType"),
"caseKey": request_data.get("caseKey"),
"content": self._get_case_name(request_data),
"testcasePath": verify_result.get("testcasePath"),
"attempts": verify_result.get("attempts"),
"failureReason": verify_result.get("failureReason")
}
})
return
self._send_json_response(200, {
"code": 0,
"message": "success",
"data": {
"projectId": request_data.get("projectId"),
"caseId": request_data.get("caseId"),
"automationType": request_data.get("automationType"),
"caseKey": request_data.get("caseKey"),
"content": self._get_case_name(request_data),
"testcasePath": verify_result.get("testcasePath"),
"attempts": verify_result.get("attempts")
}
})
except ValueError as error:
self._send_json_response(400, {
"code": 400,
"message": str(error),
"data": None
})
except Exception as error:
self._send_json_response(500, {
"code": 500,
"message": str(error),
"data": None,
"trace": traceback.format_exc()
})
def _read_json_body(self):
content_length = int(self.headers.get("Content-Length", 0))
if content_length <= 0:
raise ValueError("请求体不能为空")
body = self.rfile.read(content_length).decode("utf-8")
try:
return json.loads(body)
except ValueError:
raise ValueError("请求体必须是合法JSON")
def _validate_required_fields(self, request_data):
required_fields = [
"projectId",
"caseId",
"automationType",
"prompt",
"caseKey",
"moduleName",
"productName",
"projectName",
"steps",
"expectedResults"
]
missing_fields = []
for field in required_fields:
if field == "productName":
if field not in request_data or request_data.get(field) is None:
missing_fields.append(field)
elif request_data.get(field) in (None, ""):
missing_fields.append(field)
if missing_fields:
raise ValueError("缺少必填参数:{0}".format(", ".join(missing_fields)))
def _get_case_name(self, request_data):
return request_data.get("caseName") or request_data.get("title") or request_data.get("name") or "{0}-{1}".format(
request_data.get("caseKey"),
request_data.get("moduleName")
)
def _send_json_response(self, status_code, response_data):
response_body = json.dumps(response_data, ensure_ascii=False).encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", "application/json;charset=UTF-8")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, accessToken, Authorization")
self.send_header("Content-Length", str(len(response_body)))
self.end_headers()
self.wfile.write(response_body)
def log_message(self, format, *args):
print("[{0}] {1}".format(self.log_date_time_string(), format % args))
def run_server(host="0.0.0.0", port=8081):
server = ThreadingHTTPServer((host, port), GenerateAutomationHandler)
print("Create_ui_testcase HTTP服务已启动http://{0}:{1}".format(host, port))
print("接口地址POST {0}".format(API_PATH))
server.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="UI自动化用例生成HTTP服务")
parser.add_argument("--host", default="0.0.0.0", help="服务监听地址默认0.0.0.0")
parser.add_argument("--port", default=8081, type=int, help="服务监听端口默认8081")
args = parser.parse_args()
run_server(host=args.host, port=args.port)

View File

@@ -0,0 +1,3 @@
/logs/
/venv/
.idea/

View File

@@ -0,0 +1,4 @@
### IT接口管理
* git clone
* pip3 install -r requirements
* gunicorn --config=gunicorn.conf.py manage:app

View File

@@ -0,0 +1,18 @@
from flask import Flask
from ..app.api.views import api
from flask_docs import ApiDoc
from logger import logger
def create_app():
app = Flask(__name__)
app.register_blueprint(api, url_prefix='/api')
ApiDoc(
app,
title="Effekt Interface App",
version="1.0.0",
description="Effekt Interface app API",
)
app.config["API_DOC_MEMBER"] = ["api", "platform"]
logger.info("app start-------")
return app

View File

@@ -0,0 +1,128 @@
# encoding: UTF-8
import time
import subprocess
from subprocess import PIPE
import os
class OSType:
WIN, LINUX, UNKNOWN = range(3)
def __init__(self):
pass
@staticmethod
def get_type():
import platform
system_name = platform.system()
if system_name.lower() == 'windows':
return OSType.WIN
elif system_name.lower() == 'linux':
return OSType.LINUX
else:
return OSType.UNKNOWN
class tool(object):
def __init__(self):
self.env_port = 5011
def run_process(self, cmd_str, out_p=False):
"""
run command
cmd_str unicode string.
"""
if OSType.WIN == OSType.get_type():
# cmd_str = cmd_str.encode('gbk')
cmd_str = cmd_str
elif OSType.LINUX == OSType.get_type():
cmd_str = cmd_str.encode('utf-8')
else:
raise RuntimeError("your os is not support.")
close_fds = False if OSType.WIN == OSType.get_type() else True
if out_p:
p = subprocess.Popen(cmd_str, shell=True, close_fds=close_fds, stdout=PIPE)
p.wait()
return p.returncode, p.stdout.read()
else:
c = self.get_devnull()
p = subprocess.Popen(cmd_str, shell=True, close_fds=close_fds, stdout=c)
p.wait()
return p.returncode, None
def get_devnull(self):
try:
return subprocess.DEVNULL
except AttributeError:
# Python 2.x or older
return open(os.devnull, 'r+')
def _kills_pid(self):
if OSType.WIN == OSType.get_type():
kill_pid_cmd = "taskkill /f /pid {}".format(self.pid)
elif OSType.LINUX == OSType.get_type():
kill_pid_cmd = "kill -9 {}".format(self.pid)
else:
raise RuntimeError("your os is not support.")
res_code, res_context = self.run_process(kill_pid_cmd)
if res_code:
raise RuntimeError("kill pid: {} failed. error: {}".format(self.pid, res_context))
def check_port(self):
if OSType.WIN == OSType.get_type():
find_pid_win_cmd = 'netstat -ano | findstr {} | findstr LISTENING'.format(self.env_port)
print(find_pid_win_cmd)
res_code, res_context = self.run_process(find_pid_win_cmd, out_p=True)
if res_code == 0:
if len(res_context) > 0:
try:
self.pid = str(res_context).split()[-1].replace("\\r\\n'", "")
self._kills_pid()
except IndexError:
pass
elif OSType.LINUX == OSType.get_type():
find_pid_linux_cmd = "lsof -i:{}".format(self.env_port)
res_code, res_context = self.run_process(find_pid_linux_cmd, out_p=True)
if res_code == 0:
# 获取pid
if len(res_context) > 0:
try:
self.pid = str(res_context).split("\n")[1].split()[1]
self._kills_pid()
except IndexError:
pass
else:
raise RuntimeError("your os is not support.")
def run_manage(self):
count = 3
while count > 0:
self.run_manages()
find_pid_linux_cmd = "lsof -i:{}".format(self.env_port)
res_code, res_context = self.run_process(find_pid_linux_cmd, out_p=True)
print(res_code,"---res_code---",res_context,"---res_context---")
if len(res_context) > 0:
time.sleep(2)
pid = str(res_context).split("\n")[1].split()[1]
print(pid,"pid####")
count -= 1
if pid:
break
else:
continue
else:
break
def run_manages(self):
lod = "nohup python3 platform_tools/aida/manage.py &."
subprocess.call(["./5011.sh"])
# self.run_process(lod)
if __name__ == '__main__':
test = tool()
test.check_port()
time.sleep(3)
test.run_manage()

View File

@@ -0,0 +1,527 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataDetailService import CreateDetailService
from ..service.createDataDictService import CreateDictService
from ..service.createDataInfoService import CreateInfoService
from ..service.createDataNumberService import CreateNumberService
from ..service.createDataResultService import CreateResultService
from ..model.createDataInfo import CreateInfo
from ..model.createDetailInfo import CreateDetail
from ..model.createDictInfo import CreateDict
from datetime import datetime, timedelta
from logger import logger
import ast
from operator import methodcaller
import astunparse
import os
import re
import time
from sqlalchemy.exc import SQLAlchemyError
class CommonController(object):
def __init__(self):
self.session = SqlSession()
def create_all_info(self, team_name, list_detail):
"""
对比造数基础表数据,新增的进行录库
:return:
"""
for detail in list_detail:
file_name = detail.get("file_name")
get_class_name = detail.get("class_name")
module_names = detail.get("module_name")
method_name = detail.get("method_name")
module_name = module_names.split(".")[0]
method_detail = detail.get("method_detail")
request_parameter = list(eval(detail.get("request_parameter")))
create_dict_service = CreateDictService()
create_detail_service = CreateDetailService()
create_info_service = CreateInfoService()
filter_detail_list = list()
filter_detail_list.append(CreateDetail.class_name == get_class_name)
filter_detail_list.append(CreateDetail.method_name == method_name)
filter_detail_list.append(CreateDetail.module_name == module_name)
get_data_detail = create_detail_service.get_detail_simple_data_by_filters(self.session,
filter_detail_list)
# 数据字典中业务线配置的路径,用于获取各个业务线下的业务名称
filter_dict_list = list()
filter_dict_list.append(CreateDict.data_type == 1)
filter_dict_list.append(CreateDict.dict_key == team_name)
get_dict_detail = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_dict_list)
get_split_team_name = get_dict_detail.dict_value.split(".")[-1]
get_split_file_name = file_name.split(get_split_team_name)[-1]
if get_split_file_name:
get_file_name = get_split_file_name[1:].replace("\\", ".")
else:
get_file_name = ""
if get_data_detail:
compare_class_name = self.compare_string(get_data_detail.class_name, get_class_name)
compare_module_name = self.compare_string(get_data_detail.module_name, module_name)
compare_method_name = self.compare_string(get_data_detail.method_name, method_name)
compare_method_detail = self.compare_string(get_data_detail.method_detail, method_detail)
# compare_file_name = self.compare_string(get_data_detail.file_name, get_file_name)
update_info = {}
if compare_class_name:
update_info["class_name"] = get_class_name
elif compare_module_name:
update_info["module_name"] = module_name
elif compare_method_name:
update_info["method_name"] = method_name
elif compare_method_detail:
update_info["method_detail"] = method_detail
# elif compare_file_name:
# get_data_detail.file_name = get_file_name
else:
continue
update_info["modified_time"] = update_time()
print(update_info)
err_msg = create_detail_service.update_service_detail_data(self.session, update_info,
get_data_detail.create_data_detail_id)
if err_msg:
return err_msg
else:
if method_name == "__init__":
continue
if re.findall("__", method_name):
continue
if method_name.startswith("_"):
continue
dict_parameter = {}
if request_parameter:
for parameter in request_parameter:
if parameter != 'self':
dict_parameter[parameter] = ""
else:
dict_parameter = {}
get_method_detail = self.handle_method_detail(method_detail)
get_creator = self.get_creator_info(team=team_name, method_detail=method_detail)
if get_method_detail:
get_function_details = get_method_detail
else:
get_function_details = str(module_name) + "." + str(get_class_name) + "." + str(method_name)
add_detail_data = {}
add_info_data = {}
add_dict_data = {}
add_detail_data["create_data_detail_id"] = get_uuid()
add_detail_data["team_name"] = team_name
add_detail_data["file_name"] = get_file_name
add_detail_data["class_name"] = get_class_name
add_detail_data["del_flag"] = 0
add_detail_data["module_name"] = module_name
add_detail_data["method_name"] = method_name
add_detail_data["method_detail"] = method_detail
add_detail_data["method_function_detail"] = get_function_details
add_detail_data["creator"] = get_creator
add_detail_data["status"] = 0
add_detail_data["created_time"] = update_time()
add_detail_data["modified_time"] = update_time()
create_data_detail_id = create_detail_service.create_service_detail_data(self.session,
add_detail_data)
add_info_data["create_data_info_id"] = get_uuid()
add_info_data["create_data_detail_id"] = create_data_detail_id
add_info_data["class_name"] = get_class_name
add_info_data["del_flag"] = 0
add_info_data["method_name"] = method_name
add_info_data["request_parameter"] = dict_parameter
add_info_data["created_time"] = get_time()
add_info_data["modified_time"] = get_time()
info_id = create_info_service.create_service_info_data(self.session, add_info_data)
filter_dict_type_list = list()
filter_dict_type_list.append(CreateDict.team_name == team_name.upper())
filter_dict_type_list.append(CreateDict.data_type == 3)
filter_dict_type_list.append(CreateDict.dict_key == get_file_name)
get_dict_detail = create_dict_service.get_dict_simple_data_by_filters(self.session,
filter_dict_type_list)
add_dict_data["create_dict_data_id"] = get_uuid()
add_dict_data["data_type"] = 3
add_dict_data["team_name"] = team_name
add_dict_data["dict_key"] = get_file_name
add_dict_data["dict_value"] = get_file_name
add_dict_data["del_flag"] = 0
add_dict_data["created_time"] = get_time()
add_dict_data["modified_time"] = get_time()
if get_dict_detail or get_file_name == "":
continue
else:
dict_id = create_dict_service.create_service_dict_data(self.session, add_dict_data)
return True
def compare_string(self, sub_str1, sub_str2):
'''
对两个字符串进行比较,查看字符串是否相同
:param sub_str1:
:param sub_str2:
:return:
'''
if sub_str1 == sub_str2:
return False
else:
return True
def handle_dict_data(self, create_data_detail_id, data_type, dict_key):
'''
处理数据字典的路径类型,进行初始化路径组装
:return:
'''
create_detail_service = CreateDetailService()
create_dict_service = CreateDictService()
filter_detail_list = list()
filter_dict_list = list()
filter_detail_list.append(CreateDetail.create_data_detail_id == create_data_detail_id)
filter_dict_list.append(CreateDict.data_type == data_type)
filter_dict_list.append(CreateDict.dict_key == dict_key)
get_data_detail = create_detail_service.get_detail_simple_data_by_filters(self.session, filter_detail_list)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_dict_list)
if get_dict_data:
if get_data_detail.file_name:
get_new_path = get_dict_data.dict_value + "." + get_data_detail.file_name + "." + get_data_detail.module_name
else:
get_new_path = get_dict_data.dict_value + "." + get_data_detail.module_name
return {"get_new_path": get_new_path, "class_name": get_data_detail.class_name}
else:
return False
def handle_result_data(self, create_data_detail_id, create_data_info_id):
"""
处理数据结果表
:return:
"""
create_detail_service = CreateDetailService()
create_info_service = CreateInfoService()
filter_detail_list = list()
filter_data_list = list()
filter_detail_list.append(CreateDetail.create_data_detail_id == create_data_detail_id)
filter_data_list.append(CreateInfo.create_data_info_id == create_data_info_id)
get_data_detail = create_detail_service.get_detail_simple_data_by_filters(self.session, filter_detail_list)
get_data_info = create_info_service.get_data_simple_data_by_filters(self.session, filter_data_list)
dict_data = {"team_name": get_data_detail.team_name, "module_name": get_data_detail.module_name,
"file_name": get_data_detail.file_name, "class_name": get_data_detail.class_name,
"method_name": get_data_detail.method_name, "request_parameter": get_data_info.request_parameter,
"method_function_detail": get_data_detail.method_function_detail}
return dict_data
def handle_method_detail(self, method_detail):
'''
对方法描述进行匹配出主要功能信息
:return:
'''
if method_detail:
get_function_detail = re.findall("功能说明: \|(.*)\|", method_detail)
else:
get_function_detail = "请补充方法注释"
if get_function_detail:
return get_function_detail
else:
get_function_details = re.findall("\s*功能(.+)", method_detail)
if get_function_details:
return get_function_details
else:
return False
def get_creator_info(self, team, method_detail):
'''
获取负责人信息
:return:
'''
if method_detail:
get_creators = re.findall("作者信息: \|(.*?)\|", method_detail)
else:
get_creators = ""
if get_creators:
return get_creators
else:
create_dict_service = CreateDictService()
filter_dict_list = list()
filter_dict_list.append(CreateDict.data_type == 5)
filter_dict_list.append(CreateDict.dict_key == team.upper())
get_data_detail = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_dict_list)
if get_data_detail:
return get_data_detail.dict_value
else:
return "未找到负责人"
def get_login_creator(self, token):
'''
获取造数使用者信息
:return:
'''
if token:
return token
else:
return "administrator"
def get_team_name(self, team):
"""
各业务线对应枚举
:return:
"""
# 业务线枚举映射
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.dict_key == team.upper())
filter_list.append(CreateDict.data_type == 2)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_data:
return get_dict_data.dict_value
else:
return team
def get_module_name(self, team, dict_key):
"""
各业务线模块对应枚举
:return:
"""
# 业务线模块名称映射
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.team_name == team.upper())
filter_list.append(CreateDict.data_type == 3)
filter_list.append(CreateDict.dict_key == dict_key)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_data:
return get_dict_data.dict_value
else:
return dict_key
def get_keyword_path(self, team, file_name=""):
"""
获取某个组的关键字路径
:return:
"""
# 业务线模块名称映射
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.dict_key == team.upper())
filter_list.append(CreateDict.data_type == 1)
get_data_detail = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
get_list_path = get_data_detail.dict_value.split(".")
basic_path = os.path.dirname(os.path.abspath(__file__))
team_path = os.path.abspath(os.path.join(basic_path, '../../../../../../'))
if file_name.endswith(".py"):
return os.path.join(team_path, *get_list_path, file_name)
else:
return os.path.join(team_path, *get_list_path)
def get_model_name(self, team, dict_key):
'''
各业务线对应枚举
:param files_path:
:return:
'''
# 业务线模块名称映射
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.team_name == team.upper())
filter_list.append(CreateDict.dict_key == dict_key)
filter_list.append(CreateDict.data_type == 3)
get_data_detail = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_data_detail:
return get_data_detail.dict_value
else:
return dict_key
def daily_task_update_month_number(self):
"""
定时任务,每天更新月使用次数
:return:
"""
create_detail_service = CreateDetailService()
create_number_service = CreateNumberService()
create_result_service = CreateResultService()
# 假设时间字段为 `created_time`,格式为 'YYYYMMDDHHMM'
time_field_format = '%Y%m%d%H%M'
# 获取当前时间
now = datetime.now()
# 计算一个月前的时间
one_month_ago = now - timedelta(days=30)
# 将时间转换为目标格式的字符串
one_month_ago_str = one_month_ago.strftime(time_field_format)
all_number_methods = create_number_service.get_service_number_all_no_filters(session=self.session)
if len(all_number_methods) == 0:
return ""
for number in all_number_methods:
try:
if not self.session.object_session(number):
self.session.add(number) # 重新绑定对象到会话
get_number = create_result_service.get_service_result_numbers_by_month(session=self.session,
create_data_detail_id=number.create_data_detail_id,
one_month_ago_str=one_month_ago_str)
if get_number is None:
get_number = 0
else:
get_number = get_number[0]
create_number_service.update_service_number_data(session=self.session,
update_info={"month_number": get_number},
create_data_detail_id=number.create_data_detail_id)
except SQLAlchemyError as e:
self.session.rollback()
finally:
self.session.close()
return {"code": 20000, "message": "更新成功"}
class LambdaCheck(ast.NodeVisitor):
def __init__(self):
self.illegal_args_list = []
self._cur_file = None
self._cur_lambda_args = []
def set_cur_file(self, cur_file):
assert os.path.isfile(cur_file), cur_file
self._cur_file = os.path.realpath(cur_file)
def visit_Lambda(self, node):
"""
lambda 闭包检查原则:
只需检测lambda expr body中args是否引用了lambda args list之外的参数
"""
self._cur_lambda_args = [a.id for a in node.args.args]
print(astunparse.unparse(node))
self.get_lambda_body_args(node.body)
self.generic_visit(node)
def record_args(self, name_node):
if isinstance(name_node, ast.Name) and name_node.id not in self._cur_lambda_args:
self.illegal_args_list.append((self._cur_file, 'line no:%s' % name_node.lineno, 'var:%s' % name_node.id))
def _is_args(self, node):
if isinstance(node, ast.Name):
self.record_args(node)
return True
if isinstance(node, ast.Call):
map(self.record_args, node.args)
return True
return False
def get_lambda_body_args(self, node):
if self._is_args(node): return
# for cnode in ast.walk(node):
for cnode in ast.iter_child_nodes(node):
if not self._is_args(cnode):
self.get_lambda_body_args(cnode)
list_info = [] # 存爬取出来的方法列表
class CodeVisitor(ast.NodeVisitor):
def __init__(self, file, file_name):
self.file = file
self.file_name = file_name
self.dict_info = {}
def visit_BinOp(self, node): # 这个函数的访问是由于 Visit_FunctionDef的先访问再generic_visit才访问的
# print('Bin') # 如果Visit_FunctionDef中没有generic_visit的话则这个函数是不会访问的
if isinstance(node.op, ast.Add):
node.op = ast.Sub()
self.generic_visit(node)
def visit_ClassDef(self, node):
# print('Class Name: %s' % node.name)
global class_name
class_name = node.name
self.generic_visit(node) # ClassDef中还包含有 BinOp,因此会进去visit BinOP
# print(dict_info)
def visit_FunctionDef(self, node):
# print('Function Name: %s' % node.name)
self.dict_info = {}
is_kwargs = node.args.kwarg
args = list([arg.arg for arg in node.args.args])
if is_kwargs is not None:
args.append("is_kwargs")
# print(str(args))
self.dict_info["file_name"] = self.file_name
self.dict_info["class_name"] = class_name
self.dict_info["module_name"] = self.file
self.dict_info["method_name"] = node.name
self.dict_info["request_parameter"] = str(args)
self.dict_info["method_detail"] = ast.get_docstring(node)
# print(ast.get_docstring(node))
list_info.append(self.dict_info)
self.generic_visit(node) # FunctionDef中还包含有 BinOp,因此会进去visit BinOP
def run(files_path, end_file, remove_file):
"""
用于执行获取各个类中的方法与方法注解
:param files_path:需要爬取关键字的路径
:param end_file:爬取需要什么结尾的文件
:param remove_file:不需要爬取的文件路径的文件名称
:return:
"""
if files_path.endswith(end_file):
checker = LambdaCheck()
checker.set_cur_file(files_path)
with open(files_path, 'rb') as f:
root_node = ast.parse(f.read())
visitor = CodeVisitor(os.path.basename(files_path), file_name="")
visitor.visit(root_node)
else:
for root, dirs, files in os.walk(files_path):
file_name = os.path.basename(root)
# if re.findall(remove_file, root):
# continue
py_files = filter(lambda file: file.endswith(end_file), files)
checker = LambdaCheck()
for file in py_files:
file_path = os.path.join(root, file)
checker.set_cur_file(file_path)
with open(file_path, 'rb') as f:
root_node = ast.parse(f.read())
# print(file_name)
visitor = CodeVisitor(file, root)
visitor.visit(root_node)
return list_info
def call_method(class_name, method_info):
'''
主要用于动态调用方法,
:param class_name:传入定义后的类名
:param method_name:传入方法与参数 eg:{"methodName":"kw_astwb_create_classes","requestParameter":{"user":"qiaoxinjiu","post_data_input={'studentMin':'2'}"}
:return:
'''
method_name = method_info.get("methodName")
if method_name:
request_data = method_info.get("requestParameter")
return methodcaller(method_name, **request_data)(class_name())
else:
return {"方法名输入错误"}
def get_time():
"""
获取当前时间
:return:
"""
# now_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S.%f")
now_time = time.strftime("%Y%m%d%H%M%S", time.localtime())
return now_time
def update_time():
"""
录入数据库时间
:return:
"""
import datetime
now_time = datetime.datetime.now().strftime("%Y%m%d%H%M%S.%f")
return now_time
def get_uuid():
'''
获取uuid
:return:
'''
import uuid
test_id = str(uuid.uuid1())
return test_id

View File

@@ -0,0 +1,304 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataDetailService import CreateDetailService
from ..service.createDataInfoService import CreateInfoService
from ..service.createDataGroupService import CreateGroupService
from ..service.createDataNumberService import CreateNumberService
from ..model.createDetailInfo import CreateDetail
from ..controller import commonController
from ..controller.createDataResultController import CreateDataResultController
from ..controller.createDataNumberController import CreateDataNumberController
from logger import logger
import time
import json
import importlib
import sys
class CreateDataDetailController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.page = req_json.get('pageNo')
self.limit = req_json.get('pageSize')
self.create_data_detail_id = req_json.get('create_data_detail_id')
self.create_data_info_id = req_json.get('create_data_info_id')
self.team_name = req_json.get('team_name')
self.file_name = req_json.get('file_name')
self.module_name = req_json.get('module_name')
self.method_name = req_json.get('method_name')
self.detail_name = req_json.get('detail_name')
self.method_detail = req_json.get('method_detail')
self.method_function_detail = req_json.get('method_function_detail')
self.request_parameter = req_json.get('request_parameter')
self.dict_key = req_json.get('dict_key')
self.get_tag = req_json.get('get_tag')
self.email = req_json.get('email')
self.status = req_json.get('status')
self.token = token
self.del_flag = 0
self.req_json = req_json
def create_detail_data(self):
"""
对造数列表进行新增
:return:
"""
def update_detail_data(self):
"""
对造数信息进行修改
:return:
"""
def delete_detail_data(self):
"""
对造数信息进行删除
:return:
"""
create_detail_service = CreateDetailService()
del_info = create_detail_service.delete_service_detail_data(self.session,
create_data_detail_id=self.create_data_detail_id)
return del_info
def count_sucess_rate(self, total_number, right_number):
"""
计算成功率
:return:
"""
if total_number == 0:
return 0
else:
get_result = right_number / total_number
return round(get_result, 2)
def get_number_data(self, create_data_detail_id):
"""
根据造数详情id查询对应的次数信息
:return:
"""
create_number_service = CreateNumberService()
get_query_info, message = create_number_service.get_service_number_by_id(session=self.session,
create_data_detail_id=create_data_detail_id)
if message:
if get_query_info:
return {"total_number": get_query_info.total_number,
"success_rate": self.count_sucess_rate(get_query_info.total_number,
get_query_info.right_number)}
else:
return {"total_number": 0,
"success_rate": 0}
else:
return {"total_number": 0,
"success_rate": 0}
def approve_detail_status(self):
"""
审核状态
:return:
"""
create_detail_service = CreateDetailService()
update_info = {"status": 1}
err_msg = create_detail_service.update_service_detail_data(self.session, update_info,
self.create_data_detail_id)
if err_msg:
return {"code": 40010, "message": err_msg}
else:
return {"code": 20000, "message": "审核成功"}
def get_detail_query_advance(self):
"""
列表页查询
:return:
"""
create_group_service = CreateGroupService()
if self.page is None:
self.page = 1
if self.limit is None:
self.limit = 10
create_detail_service = CreateDetailService()
common_controller = commonController.CommonController()
get_group_data = create_group_service.get_service_group_info_data_by_email(self.session, email=self.email)
list_group_user = ["ALL"]
filter_list = []
team_name = self.team_name
file_name = self.file_name
module_name = self.module_name
detail_name = self.detail_name
status = self.status
if team_name:
filter_list.append(CreateDetail.team_name == team_name)
if file_name:
filter_list.append(CreateDetail.file_name == file_name)
if module_name:
filter_list.append(CreateDetail.module_name == module_name)
if detail_name:
filter_list.append(CreateDetail.method_function_detail.like('%' + self.detail_name + '%'))
if status:
filter_list.append(CreateDetail.status == 1)
if filter_list:
if get_group_data:
list_group_user.append(get_group_data.team_name)
detail_list, total = create_detail_service.get_detail_list_by_filters_team_page(self.session,
filter_list,
list_group_user,
self.page, self.limit)
else:
if get_group_data:
list_group_user.append(get_group_data.team_name)
detail_list, total = create_detail_service.get_detail_list_no_filters_team_page(self.session,
list_group_user, self.page,
self.limit)
ret_ls = []
for obj in detail_list:
ret_ls.append({
'create_data_detail_id': obj.create_data_detail_id,
'team_name': common_controller.get_team_name(obj.team_name),
'total_number': self.get_number_data(obj.create_data_detail_id).get("total_number"),
'success_rate': self.get_number_data(obj.create_data_detail_id).get("success_rate"),
'file_name': common_controller.get_module_name(team=obj.team_name, dict_key=obj.file_name),
'class_name': obj.class_name,
'module_name': obj.module_name,
'method_name': obj.method_name,
'method_function_detail': obj.method_function_detail,
'method_detail': obj.method_detail,
'status': obj.status,
'creator': obj.creator,
'created_time': obj.created_time,
'modified_time': obj.modified_time
})
return {"list": ret_ls, "total": total}
def get_request_parameter(self, request_parameter):
"""
组成详情获取组装请求参数信息
:return:
"""
try:
get_dict_parameter = request_parameter
# get_dict_parameter = json.loads(request_parameter)
dict_data = {}
for key, value in get_dict_parameter.items():
if key == "post_data_input":
dict_data.update(get_dict_parameter.get("post_data_input"))
else:
dict_data[key] = value
return dict_data
except:
return request_parameter
def get_detail_advance(self):
"""
关键字基础信息详情页查询
:return:
"""
create_info_service = CreateInfoService()
get_data_info = create_info_service.get_create_data_info_by_detail_id(session=self.session,
detail_id=self.create_data_detail_id)
create_detail_service = CreateDetailService()
get_detail_info = create_detail_service.get_create_detail_info_by_id(session=self.session,
create_data_detail_id=self.create_data_detail_id)
common_controller = commonController.CommonController()
_data = {'create_data_detail_id': get_data_info.create_data_detail_id,
'create_data_info_id': get_data_info.create_data_info_id,
'team_name': get_detail_info.team_name,
'file_name': common_controller.get_model_name(team=get_detail_info.team_name,
dict_key=get_detail_info.file_name),
'class_name': get_data_info.class_name,
'method_name': get_data_info.method_name,
'method_detail': get_detail_info.method_detail,
'method_function_detail': get_detail_info.method_function_detail,
'creator': get_detail_info.creator,
# 'request_parameter':unescape(str(_edit.request_parameter)),
'request_parameter': self.get_request_parameter(get_data_info.request_parameter),
'created_time': get_data_info.created_time,
'modified_time': get_data_info.modified_time}
return _data
def run_keyword_data(self):
"""
执行造数
:return:
"""
create_info_service = CreateInfoService()
common_controller = commonController.CommonController()
data_type = 1
# creator = dict_data["creator"]
creator = common_controller.get_login_creator(self.email)
time.sleep(2)
# 查询请求参数的表用于判断传入的类名与方法名是否正确
get_data_info = create_info_service.get_create_data_info_by_id(session=self.session,
sid=self.create_data_info_id)
chushi_name = get_data_info.class_name
try:
get_parameter = json.loads(self.request_parameter)
except:
return {"code": 40009, "message": "请求参数格式不正确"}
create_data_result_id = commonController.get_uuid()
created_time = commonController.get_time()
modified_time = commonController.get_time()
get_result_info = common_controller.handle_result_data(create_data_detail_id=self.create_data_detail_id,
create_data_info_id=self.create_data_info_id)
req_json = {"create_data_detail_id": self.create_data_detail_id,
"method_name": get_result_info.get("method_name")}
create_number_controller = CreateDataNumberController(req_json=req_json, token=self.token)
req_json = {"create_data_result_id": create_data_result_id,
"create_data_info_id": self.create_data_info_id,
"create_data_detail_id": self.create_data_detail_id,
"team_name": get_result_info.get("team_name"),
"file_name": get_result_info.get("file_name"),
"module_name": get_result_info.get("module_name"),
"class_name": get_result_info.get("class_name"),
"method_name": get_result_info.get("method_name"),
"tag": self.get_tag,
"status": 0,
"method_function_detail": get_result_info.get("method_function_detail"),
"request_parameter": get_parameter, "result_info": "",
"creator": creator, "created_time": created_time, "modified_time": modified_time}
add_result_controller = CreateDataResultController(req_json=req_json, token=self.token)
add_result_id = add_result_controller.create_result_data(req_json)
try:
if get_data_info:
dict_key = self.dict_key
get_dict_info = common_controller.handle_dict_data(create_data_detail_id=self.create_data_detail_id,
data_type=data_type, dict_key=dict_key.upper())
if get_dict_info is False:
return {"code": 500, "message": "字典中不存在该信息"}
method_info = {"methodName": get_data_info.method_name,
"requestParameter": get_data_info.request_parameter}
class_name = get_data_info.class_name
print("get_new_path:", get_dict_info.get("get_new_path"))
test = importlib.import_module(get_dict_info.get("get_new_path"))
get_data_info.class_name = getattr(test, get_dict_info.get("class_name")) # 实例化case
result_data = commonController.call_method(class_name=get_data_info.class_name,
method_info=method_info) # 返回造数结果
try:
if isinstance(result_data, str):
is_json = False
else:
is_json = True
get_result = json.dumps(result_data, ensure_ascii=False)
except:
is_json = False
get_result = result_data
add_result_controller.update_result_data(sid=add_result_id, status=1, result_info=str(result_data))
get_data_info.class_name = class_name
create_number_controller.update_number_data(is_right=True)
data = {"message": "造数成功", "data": {"create_data_result_id": create_data_result_id, "data": get_result,
"is_json": is_json}}
else:
data = {"message": "传入请求id未查询到相应信息"}
return data
except:
get_data_info.class_name = chushi_name
s = sys.exc_info()
create_number_controller.update_number_data(is_right=False)
add_result_controller.update_result_data(sid=add_result_id, status=2, result_info=str(s[1]))
logger.info('Error "%s" happend on line %d' % (s[1], s[2].tb_lineno))
data = {"message": "造数出现异常,异常为:{}".format(str(s[1]))}
return data

View File

@@ -0,0 +1,180 @@
# encoding: UTF-8
from datetime import datetime
from ....common.sqlSession import SqlSession
from ..controller import commonController
from ..service.createDataDictService import CreateDictService
from ..service.createDataGroupService import CreateGroupService
from ..model.createDictInfo import CreateDict
from ..controller.commonController import CommonController
from logger import logger
class CreateDataDictController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.pageNo = req_json.get('pageNo')
self.pageSize = req_json.get('pageSize')
self.create_dict_data_id = req_json.get('create_dict_data_id')
self.team_name = req_json.get('team_name')
self.data_type = req_json.get('data_type')
self.dict_key = req_json.get('dict_key')
self.dict_value = req_json.get('dict_value')
self.email = req_json.get('email')
self.token = token
self.del_flag = 0
self.req_json = req_json
def create_dict_data(self):
"""
对数据字典进行新增
:return:
"""
common_controller = commonController.CommonController()
# 获取抓取的方法名称,描述等
create_dict_id = commonController.get_uuid()
created_time = commonController.get_time()
modified_time = commonController.get_time()
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.dict_value == self.team_name)
filter_list.append(CreateDict.data_type == 2)
# 获取抓取的方法名称,描述等
get_dict_value_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_value_data:
dict_keys = get_dict_value_data.dict_key
else:
dict_keys = self.dict_key
filter_list_data = list()
filter_list_data.append(CreateDict.dict_key == self.dict_key)
filter_list_data.append(CreateDict.data_type == self.data_type)
filter_list_data.append(CreateDict.team_name == dict_keys)
filter_list_data.append(CreateDict.data_type == 2)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list_data)
add_data = {
"create_dict_data_id": create_dict_id,
"data_type": self.data_type,
"team_name": self.team_name,
"dict_key": self.dict_key,
"dict_value": self.dict_value,
"del_flag": 0,
"created_time": created_time,
"modified_time": modified_time
}
create_data_info_id, err_msg = create_dict_service.create_service_dict_data(session=self.session,
create_info=add_data)
return err_msg
def update_dict_data(self):
"""
对数据字典进行修改
:return:
"""
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.dict_value == self.team_name)
filter_list.append(CreateDict.data_type == 2)
# 获取抓取的方法名称,描述等
get_dict_value_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_value_data:
dict_keys = get_dict_value_data.dict_key
else:
dict_keys = self.dict_key
filter_data_list = list()
filter_data_list.append(CreateDict.team_name == dict_keys)
filter_data_list.append(CreateDict.data_type == self.data_type)
filter_data_list.append(CreateDict.dict_key == self.dict_key)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_data_list)
if get_dict_data:
edit_dict_id = self.create_dict_data_id
filter_edit_list = list()
filter_edit_list.append(CreateDict.create_dict_data_id == edit_dict_id)
get_edit_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_edit_list)
edit_data = {'dict_value': self.dict_value, 'team_name': get_edit_data.team_name,
'dict_key': get_edit_data.dict_key}
err_msg = create_dict_service.update_service_dict_data(session=self.session, update_info=edit_data,
create_dict_data_id=edit_dict_id)
if err_msg:
return err_msg
else:
add_data = {
'data_type': self.data_type,
'team_name': self.team_name,
'dict_key': self.dict_key,
'dict_value': self.dict_value,
}
create_data_info_id, err_msg = create_dict_service.create_service_dict_data(session=self.session,
create_info=add_data)
if err_msg:
return err_msg
def get_dict_list_data(self):
"""
数据字典列表页查询
:return:
"""
if self.pageNo is None:
self.pageNo = 1
if self.pageSize is None:
self.pageSize = 10
create_dict_service = CreateDictService()
common_controller = CommonController()
create_group_service = CreateGroupService()
get_group_data = create_group_service.get_service_group_info_data_by_email(self.session, email=self.email)
list_group_user = ["ALL"]
filter_list = []
if self.team_name:
filter_list.append(CreateDict.team_name == self.team_name)
if self.dict_value:
filter_list.append(CreateDict.dict_value.like('%' + self.dict_value + '%'))
filter_list.append(CreateDict.data_type == 3)
if get_group_data:
list_group_user.append(get_group_data.team_name)
dict_list, total = create_dict_service.get_dict_list_by_team_filters_page(self.session, filter_list,
list_group_user,
self.pageNo, self.pageSize)
ret_ls = []
for obj in dict_list:
ret_ls.append({
'create_dict_data_id': obj.create_dict_data_id,
'team_name': common_controller.get_team_name(obj.team_name),
'data_type': obj.data_type,
'dict_key': obj.dict_key,
'dict_value': obj.dict_value,
'created_time': obj.created_time,
'modified_time': obj.modified_time,
})
return {"list": ret_ls, "total": total}
def get_team_name(self):
"""
获取各个业务线的数据字典,用于查询
:return:
"""
create_dict_service = CreateDictService()
create_group_service = CreateGroupService()
get_group_data = create_group_service.get_service_group_info_data_by_email(self.session, email=self.email)
list_group_user = ["ALL"]
filter_list = list()
filter_list.append(CreateDict.data_type == self.data_type)
if self.team_name:
filter_list.append(CreateDict.team_name == self.team_name)
if get_group_data:
list_group_user.append(get_group_data.team_name)
get_dict_data = create_dict_service.get_dict_list_by_team_filters_page(self.session, filter_list,
list_group_user, page=1,
limit=10)
list_data = []
if get_dict_data:
for value in get_dict_data[0]:
dict_value = {}
get_data_key = value.dict_key
get_data_value = value.dict_value
dict_value["value"] = get_data_key
dict_value["label"] = get_data_value
list_data.append(dict_value)
return list_data
else:
return get_dict_data

View File

@@ -0,0 +1,94 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataInfoService import CreateInfoService
from ..controller import commonController
from ..model.createDataInfo import CreateInfo
from logger import logger
import json
class CreateDataInfoController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.create_data_info_id = req_json.get('create_data_info_id')
self.create_data_detail_id = req_json.get('create_data_detail_id')
self.className = req_json.get('className')
self.methodName = req_json.get('methodName')
self.request_parameter = req_json.get('request_parameter')
self.token = token
self.del_flag = 0
self.req_json = req_json
def create_info_data(self):
"""
对造数详情进行新增
:return:
"""
get_time = commonController.get_time()
get_request_parameter = json.loads(self.request_parameter)
request_parameter = self.get_method_request_parameter(create_data_info_id=self.create_data_info_id,
request_param=get_request_parameter)
get_uuid = commonController.get_uuid()
add_data = {
'create_data_info_id': get_uuid,
'create_data_detail_id': self.create_data_detail_id,
'class_name': self.className,
'method_name': self.methodName,
'request_parameter': request_parameter,
'del_flag': self.del_flag,
'created_time': get_time,
'modified_time': get_time
}
create_info_service = CreateInfoService()
create_data_info_id, err_msg = create_info_service.create_service_info_data(session=self.session,
create_info=add_data)
if err_msg:
return err_msg
def update_info_data(self):
"""
编辑请求参数信息表
:return:
"""
get_time = commonController.get_time()
get_request_parameter = json.loads(self.request_parameter)
request_parameter = self.get_method_request_parameter(create_data_info_id=self.create_data_info_id,
request_param=get_request_parameter)
edit_data_id = self.create_data_info_id
edit_data = {'class_name': self.className, 'method_name': self.methodName,
'request_parameter': request_parameter,'modified_time':get_time}
create_info_service = CreateInfoService()
err_msg = create_info_service.update_service_info_data(session=self.session, update_info=edit_data,
create_data_info_id=edit_data_id)
if err_msg:
return err_msg
def get_method_request_parameter(self, create_data_info_id, request_param):
"""
组装方法的请求参数
:param parameter:
:return:
"""
# 定义请求的json
json_data = {}
# 查询抓取的请求参数信息
create_info_service = CreateInfoService()
get_data_info = create_info_service.get_create_data_info_by_id(session=self.session, sid=create_data_info_id)
get_db_parameter = get_data_info.request_parameter
# 判断如果请求参数里面含有**kwargs组装请求
if "is_kwargs" in get_db_parameter:
json_data = request_param
else:
# 定义请求的json如果是json格式的参数则组装json
post_data_input = {}
for key, value in request_param.items():
if key in get_db_parameter.keys():
json_data[key] = value
else:
post_data_input[key] = value
if "post_data_input" in get_db_parameter:
json_data["post_data_input"] = post_data_input
return json_data

View File

@@ -0,0 +1,134 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataNumberService import CreateNumberService
from ..service.createDataDetailService import CreateDetailService
from ..service.createDataInfoService import CreateInfoService
from ..controller import commonController
class CreateDataNumberController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.create_data_detail_id = req_json.get('create_data_detail_id')
self.method_name = req_json.get('method_name')
self.token = token
self.del_flag = 0
self.req_json = req_json
def get_number_data(self, create_data_detail_id):
"""
根据id查询对应的次数信息
:return:
"""
create_number_service = CreateNumberService()
get_query_info = create_number_service.get_service_number_by_id(session=self.session,
create_data_detail_id=create_data_detail_id)
def create_result_data(self, add_data):
"""
对造数结果数据进行统计
:return:
"""
create_number_service = CreateNumberService()
create_data_info_id = create_number_service.create_service_number_data(session=self.session,
create_info=add_data)
return create_data_info_id
def update_number_data(self, is_right):
"""
更新造数使用次数表
"""
create_number_service = CreateNumberService()
try:
get_db_data_number, success = create_number_service.get_service_number_by_id(session=self.session,
create_data_detail_id=self.create_data_detail_id)
if success:
if get_db_data_number:
total_number = get_db_data_number.total_number
error_number = get_db_data_number.error_number
right_number = get_db_data_number.right_number
month_number = get_db_data_number.month_number
update_total_number = total_number + 1
update_data = {"total_number": update_total_number}
if is_right:
update_right_number = right_number + 1
update_month_number = month_number + 1
update_data["right_number"] = update_right_number
update_data["month_number"] = update_month_number
else:
update_error_number = error_number + 1
update_data["error_number"] = update_error_number
create_number_service = CreateNumberService()
update_data_info_id = create_number_service.update_service_number_data(
create_data_detail_id=self.create_data_detail_id, session=self.session,
update_info=update_data)
return "更新数据成功"
else:
get_uuid = commonController.get_uuid()
get_time = commonController.get_time()
if is_right:
right_number = 1
error_number = 0
month_number = 1
else:
right_number = 0
error_number = 1
month_number = 0
add_data = {
'id': get_uuid,
'create_data_detail_id': self.create_data_detail_id,
'method_name': self.method_name,
'total_number': 1,
'error_number': error_number,
'right_number': right_number,
'month_number': month_number,
'del_flag': self.del_flag,
'created_time': get_time,
'modified_time': get_time,
}
self.create_result_data(add_data=add_data)
else:
return "更新数据失败"
except:
return "更新数据失败"
def do_history_data(self):
"""
处理历史数据
:return:
"""
create_detail_service = CreateDetailService()
create_number_service = CreateNumberService()
create_data_number_id = commonController.get_uuid()
created_time = commonController.get_time()
modified_time = commonController.get_time()
get_data_detail = create_detail_service.get_all_detail_list_no_filters_page(session=self.session)
list_update_data = []
for detail_info in get_data_detail:
get_create_number = create_number_service.get_service_number_by_id(session=self.session,
create_data_detail_id=detail_info.create_data_detail_id)
if get_create_number:
list_update_data.append(detail_info.create_data_detail_id)
create_number_service.update_service_number_data(session=self.session,
update_info={"method_name": detail_info.method_name},
create_data_detail_id=detail_info.create_data_detail_id)
else:
add_data = {
"id": create_data_number_id,
"create_data_detail_id": detail_info.create_data_detail_id,
"method_name": detail_info.method_name,
"total_number": 0,
"error_number": 0,
"right_number": 0,
"del_flag": 0,
"created_time": created_time,
"modified_time": modified_time,
}
create_number_service.create_service_number_data(session=self.session, create_info=add_data)
list_update_data.append(detail_info.create_data_detail_id)
return list_update_data

View File

@@ -0,0 +1,186 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataResultService import CreateResultService
from ..service.createDataInfoService import CreateInfoService
from ..service.createDataDetailService import CreateDetailService
from ..service.createDataGroupService import CreateGroupService
from ..controller import commonController
from ..model.createResultInfo import CreateResult
from logger import logger
import json
class CreateDataResultController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.page = req_json.get('pageNo')
self.limit = req_json.get('pageSize')
self.create_data_result_id = req_json.get('create_data_result_id')
self.create_data_info_id = req_json.get('create_data_info_id')
self.create_data_detail_id = req_json.get('create_data_detail_id')
self.team_name = req_json.get('team_name')
self.file_name = req_json.get('file_name')
self.module_name = req_json.get('module_name')
self.class_name = req_json.get('class_name')
self.method_name = req_json.get('method_name')
self.tag = req_json.get('get_tag')
self.status = req_json.get('status')
self.method_function_detail = req_json.get('method_function_detail')
self.detail_name = req_json.get('detail_name')
self.request_parameter = req_json.get('request_parameter')
self.result_info = req_json.get('result_info')
self.creator = req_json.get('creator')
self.email = req_json.get('email')
self.token = token
self.del_flag = 0
self.req_json = req_json
def create_result_data(self, add_data):
"""
对造数结果进行新增
:return:
"""
add_data["del_flag"] = self.del_flag
create_result_service = CreateResultService()
create_data_info_id = create_result_service.create_result_data(session=self.session,
create_info=add_data)
return create_data_info_id
def update_result_data(self, sid, status, result_info):
"""
对返回结果进行编辑
"""
edit_data = {'result_info': result_info, 'status': status}
create_result_service = CreateResultService()
err_msg = create_result_service.update_service_result_data(session=self.session, update_info=edit_data,
sid=sid)
if err_msg:
return err_msg
def get_method_request_parameter(self, create_data_info_id, request_param):
"""
组装方法的请求参数
:param parameter:
:return:
"""
# 定义请求的json
json_data = {}
# 查询抓取的请求参数信息
create_info_service = CreateInfoService()
get_data_info = create_info_service.get_create_data_info_by_id(session=self.session, sid=create_data_info_id)
get_db_parameter = get_data_info.request_parameter
# 判断如果请求参数里面含有**kwargs组装请求
if "is_kwargs" in get_db_parameter:
json_data = request_param
else:
# 定义请求的json如果是json格式的参数则组装json
post_data_input = {}
for key, value in request_param.items():
if key in get_db_parameter.keys():
json_data[key] = value
else:
post_data_input[key] = value
if "post_data_input" in get_db_parameter:
json_data["post_data_input"] = post_data_input
return json_data
def get_result_query_list(self):
"""
结果列表页查询
:return:
"""
create_group_service = CreateGroupService()
if self.page is None:
self.page = 1
if self.limit is None:
self.limit = 10
create_result_service = CreateResultService()
common_controller = commonController.CommonController()
get_group_data = create_group_service.get_service_group_info_data_by_email(self.session, email=self.email)
filter_list = []
team_name = self.team_name
get_tag = self.tag
detail_name = self.detail_name
list_group_user = ["ALL"]
if team_name:
filter_list.append(CreateResult.team_name == team_name)
if detail_name:
filter_list.append(CreateResult.method_function_detail.like('%' + detail_name + '%'))
if get_tag:
filter_list.append(CreateResult.tag.like('%' + get_tag + '%'))
if filter_list:
if get_group_data:
list_group_user.append(get_group_data.team_name)
detail_list, total = create_result_service.get_result_list_by_team_filters_page(self.session, filter_list,
list_group_user,
self.page, self.limit)
else:
if get_group_data:
list_group_user.append(get_group_data.team_name)
detail_list, total = create_result_service.get_result_list_no_team_filters_page(self.session,
list_group_user, self.page,
self.limit)
ret_ls = []
for obj in detail_list:
ret_ls.append({
'create_data_result_id': obj.create_data_result_id,
'create_data_info_id': obj.create_data_info_id,
'create_data_detail_id': obj.create_data_detail_id,
'team_name': common_controller.get_team_name(obj.team_name),
'request_parameter': json.dumps(obj.request_parameter),
'result_info': obj.result_info,
'tag': obj.tag,
'class_name': obj.class_name,
'module_name': obj.module_name,
'method_name': obj.method_name,
'method_function_detail': obj.method_function_detail,
'creator': obj.creator,
'created_time': obj.created_time,
'modified_time': obj.modified_time
})
return {"list": ret_ls, "total": total}
def get_result_advance(self):
"""
关键字造数结果详情页查询
:return:
"""
create_result_service = CreateResultService()
get_data_result = create_result_service.get_create_result_by_id(session=self.session,
create_data_result_id=self.create_data_result_id)
create_detail_service = CreateDetailService()
get_detail_info = create_detail_service.get_create_detail_info_by_id(session=self.session,
create_data_detail_id=get_data_result.create_data_detail_id)
common_controller = commonController.CommonController()
try:
is_json = True
get_result = json.dumps(eval(get_data_result.result_info), ensure_ascii=False)
except:
is_json = False
get_result = get_data_result.result_info
_data = {'create_data_result_id': get_data_result.create_data_result_id,
'create_data_detail_id': get_data_result.create_data_detail_id,
'team_name': get_detail_info.team_name,
'file_name': common_controller.get_model_name(team=get_detail_info.team_name,
dict_key=get_detail_info.file_name),
'module_name': get_data_result.module_name,
'class_name': get_data_result.class_name,
'method_name': get_data_result.method_name,
'method_detail': get_detail_info.method_detail,
'request_parameter': get_data_result.request_parameter,
# 'method_function_detail': CreateDictData.query.filter_by(del_flag=0, data_type=2,
# dict_key=get_detail_info.method_name).first().dict_value,
'method_function_detail': get_detail_info.method_function_detail,
'result_info': get_result,
'creator': get_data_result.creator,
'get_tag': get_data_result.tag,
'is_json': is_json,
'created_time': get_data_result.created_time,
'modified_time': get_data_result.modified_time}
return _data

View File

@@ -0,0 +1,112 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
from ..service.createDataDictService import CreateDictService
from ..controller import commonController
from logger import logger
from ..model.createDictInfo import CreateDict
import os
from base_framework.public_tools.eureka_api import EurekaAPI
import configparser
class ScrapyController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.file_name = req_json.get('fileName')
self.get_team = req_json.get('team')
self.team_name = self.get_team.upper()
self.username = req_json.get('username')
self.password = req_json.get('password')
self.token = token
self.del_flag = 0
self.req_json = req_json
def scrapy_enter_db(self):
"""
对抓取的数据进行入库
:return:
"""
get_common_controller = commonController.CommonController()
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.dict_key == self.team_name)
filter_list.append(CreateDict.data_type == 1)
get_team_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_team_data:
get_path = get_common_controller.get_keyword_path(team=self.team_name, file_name=self.file_name) # 获取路径值
print("get_path:", get_path)
team_name = self.team_name
get_team = team_name.upper()
if get_team == "ALL":
get_list_data = commonController.run(files_path=get_path, end_file="_public_business.py",
remove_file="common")
else:
get_list_data = commonController.run(files_path=get_path, end_file=".py", remove_file="")
get_scrapy_info = get_common_controller.create_all_info(team_name=self.team_name, list_detail=get_list_data)
if get_scrapy_info:
err_msg = "抓取成功"
else:
err_msg = "未成功抓取数据,请重试"
else:
err_msg = "请检查数据字典,输入正确的组名"
return err_msg
def get_git_pull(self):
"""
拉取git上的代码到本地
:return:
"""
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.team_name == self.team_name)
filter_list.append(CreateDict.data_type == 4)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_data:
base = get_dict_data.dict_value
else:
base = self.team_name
username = self.username
password = self.password
print(base,"----base----")
basic_path = os.path.dirname(os.path.abspath(__file__))
base_path = os.path.abspath(os.path.join(basic_path, '../../../../{}'.format("base_framework")))
team_path = os.path.abspath(os.path.join(basic_path, '../../../../{}'.format(base)))
try:
os.system(f"cd " + base_path + "&& git stash drop")
os.system(f"cd " + base_path + "&& git pull")
os.system(f"cd " + team_path + "&& git stash drop")
os.system(f"cd " + team_path + "&& git pull")
err_msg = ""
except:
err_msg = "拉取代码失败"
return err_msg
def get_eureka_ip(self):
"""
重新获取eureka_ip地址重写下ip
:return:
"""
create_dict_service = CreateDictService()
filter_list = list()
filter_list.append(CreateDict.team_name == self.team_name)
filter_list.append(CreateDict.data_type == 4)
get_dict_data = create_dict_service.get_dict_simple_data_by_filters(self.session, filter_list)
if get_dict_data:
team = get_dict_data.dict_value
else:
team = self.team_name
get_eureka = EurekaAPI()
get_eureka.team = team
upper_team = team.upper()
get_eureka.get_all_server_ip_from_eureka()
get_eureka.get_all_server_ip_from_eureka(eureka="EC")
file_path = os.path.dirname(os.path.abspath(__file__))
env_choose_path = os.path.join(file_path, "../../../../../base_config/env_choose.ini")
cof = configparser.ConfigParser()
cof.read(env_choose_path, encoding='utf-8')
cof.set(section="run_evn_name", option="current_team", value=upper_team)
with open(env_choose_path, 'w') as fw: # 循环写入
cof.write(fw)
test_vi_path = os.path.join(file_path, "test_vi.py")
os.system(f"echo 1 >> {test_vi_path}")
return True

View File

@@ -0,0 +1,28 @@
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
from ....common.sqlSession import SqlSession
import requests
from base_framework.public_tools.sqlhelper import MySqLHelper
sql = MySqLHelper()
class UserController(object):
def __init__(self, req_json, token):
self.session = SqlSession()
self.username = req_json.get('username')
self.password = req_json.get('password')
self.token = token
self.del_flag = 0
self.req_json = req_json
def get_user_email(self):
"""
获取用户邮箱信息
:return:
"""
username = self.username
password = self.password
if "@" not in username:
username = username + "@sparkedu.com"
header = {"Authorization": "Basic c3BhcmtsZS13ZWI6c3BhcmtsZS13ZWI="}
session = requests.session()
session.headers.update(header)
url = "https://sso.qa.huohua.cn/uim/oauth/token/pwd?username=%s&password=%s" % (
username, password)
resp = session.post(url).json()
if resp["code"] == 200:
# user_id = resp.get("data").get('userId')
# name_dict = sql.select_one(sql='select * from emp.employee where id = "%s"' % user_id, choose_db="huohua")
return {"code": 20000, "data": resp.get("data")}
else:
return {"code": 50012, "message": "用户名密码错误"}

View File

@@ -0,0 +1,139 @@
# encoding: UTF-8
from ..model.createDetailInfo import CreateDetail
from ..model.createDataNumber import CreateDataNumber
from sqlalchemy.exc import NoResultFound
from sqlalchemy import desc
from logger import logger
class CreateDetailDao(object):
@staticmethod
def get_all_detail_list_no_filters(session):
rets = session.query(CreateDetail) \
.filter(CreateDetail.del_flag == 0) \
.order_by(CreateDetail.modified_time.desc()) \
.all()
total = session.query(CreateDetail).filter(CreateDetail.del_flag == 0).count()
return rets, total
@staticmethod
def get_detail_list_by_filters(session, filter_list, page=1, limit=20):
rets = session.query(CreateDetail) \
.filter(*filter_list).filter(CreateDetail.del_flag == 0) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDetail).filter(*filter_list).filter(CreateDetail.del_flag == 0).count()
return rets, total
@staticmethod
def get_detail_list_by_filters_by_team(session, filter_list, list_team_name, page=1, limit=10):
# rets = session.query(CreateDetail) \
# .filter(*filter_list).filter(CreateDetail.del_flag == 0) \
# .filter(CreateDetail.team_name.in_(list_team_name)) \
# .offset((int(page) - 1) * int(limit)) \
# .limit(limit) \
# .all()
rets = session.query(CreateDetail) \
.outerjoin(CreateDataNumber,
CreateDetail.create_data_detail_id == CreateDataNumber.create_data_detail_id) \
.filter(*filter_list).filter(CreateDetail.del_flag == 0) \
.order_by(desc(CreateDataNumber.month_number)) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
# total = session.query(CreateDetail).filter(*filter_list).filter(CreateDetail.del_flag == 0).filter(
# CreateDetail.team_name.in_(list_team_name)).count()
total = session.query(CreateDetail).filter(*filter_list).filter(CreateDetail.del_flag == 0).count()
return rets, total
@staticmethod
def get_detail_list_no_filters_by_team(session, list_team_name, page=1, limit=10):
# rets = session.query(CreateDetail) \
# .join(CreateDataNumber,
# CreateDetail.create_data_detail_id == CreateDataNumber.create_data_detail_id) \
# .filter(CreateDetail.del_flag == 0) \
# .order_by(desc(CreateDataNumber.month_number)) \
# .offset((int(page) - 1) * int(limit)) \
# .limit(limit) \
# .all()
rets = session.query(CreateDetail) \
.filter(CreateDetail.del_flag == 0) \
.order_by(CreateDetail.modified_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDetail).filter(CreateDetail.del_flag == 0).count()
return rets, total
@staticmethod
def get_detail_list_no_filters(session, page=1, limit=20):
rets = session.query(CreateDetail) \
.filter(CreateDetail.del_flag == 0) \
.order_by(CreateDetail.modified_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDetail).filter(CreateDetail.del_flag == 0).count()
return rets, total
@staticmethod
def get_detail_simple_by_filters(session, filter_list):
rets = session.query(CreateDetail) \
.filter(*filter_list).filter(CreateDetail.del_flag == 0) \
.first()
return rets
@staticmethod
def get_create_detail_info_by_id(session, sid):
try:
ret = session.query(CreateDetail).filter(CreateDetail.create_data_detail_id == sid).one()
except Exception as e:
logger.warning(f'获取造数基础信息失败: id{sid}, {e}')
return None, f'获取造数基础信息失败: id{sid}'
return ret
@staticmethod
def save_detail_info_record(session, create_info):
if not isinstance(create_info, dict):
logger.error('只支持dict类型。build_info type: {}'.format(type(create_info)))
create_detail_info = CreateDetail(**create_info)
session.add(create_detail_info)
# session.flush()
err = session.done(close=False)
create_info_id = create_detail_info.create_data_detail_id
if err:
logger.error(f'新增失败!{err}')
return 0
if not create_info_id:
logger.error(f'{create_detail_info}获取关键字id失败')
return 0
# session.commit()
return create_info_id
@staticmethod
def update_detail_info_record(session, update_info, create_data_detail_id):
create_info = session.query(CreateDetail).filter(
CreateDetail.create_data_detail_id == create_data_detail_id).update(
update_info)
err_msg = session.commit()
session.done()
if err_msg:
return '修改场景失败!'
return ""
@staticmethod
def delete_detail_info_record(session, create_data_detail_id):
build_info = session.query(CreateDetail).filter(
CreateDetail.create_data_detail_id == create_data_detail_id).all()
if build_info:
del_info = session.query(CreateDetail).filter(
CreateDetail.create_data_detail_id == create_data_detail_id).update({'del_flag': 1})
session.commit()
if not del_info:
logger.error(f'删除记录失败res: {del_info}')
return del_info
else:
logger.error(f'id{create_data_detail_id} 不存在记录!')
return f'id{create_data_detail_id} 不存在记录!'

View File

@@ -0,0 +1,83 @@
# encoding: UTF-8
from ..model.createDictInfo import CreateDict
from sqlalchemy.exc import NoResultFound
from logger import logger
class CreateDictDao(object):
@staticmethod
def get_dict_list_by_filters(session, filter_list, page=1, limit=20):
rets = session.query(CreateDict) \
.filter(*filter_list).filter(CreateDict.del_flag == 0) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDict).filter(*filter_list).filter(CreateDict.del_flag == 0).count()
return rets, total
@staticmethod
def get_dict_list_by_team_filters(session, filter_list, list_team_name, page=1, limit=20):
# rets = session.query(CreateDict) \
# .filter(*filter_list).filter(CreateDict.del_flag == 0) \
# .filter(CreateDict.team_name.in_(list_team_name)) \
# .offset((int(page) - 1) * int(limit)) \
# .limit(limit) \
# .all()
# total = session.query(CreateDict).filter(*filter_list).filter(CreateDict.del_flag == 0).filter(
# CreateDict.team_name.in_(list_team_name)).count()
rets = session.query(CreateDict) \
.filter(*filter_list).filter(CreateDict.del_flag == 0) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDict).filter(*filter_list).filter(CreateDict.del_flag == 0).count()
return rets, total
@staticmethod
def get_dict_list_no_filters(session, page=1, limit=20):
rets = session.query(CreateDict) \
.filter(CreateDict.del_flag == 0) \
.order_by(CreateDict.modified_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateDict).filter(CreateDict.del_flag == 0).count()
return rets, total
@staticmethod
def get_dict_simple_by_filters(session, filter_list):
rets = session.query(CreateDict) \
.filter(*filter_list).filter(CreateDict.del_flag == 0) \
.first()
return rets
@staticmethod
def save_dict_info_record(session, create_info):
if not isinstance(create_info, dict):
logger.error('只支持dict类型。build_info type: {}'.format(type(create_info)))
create_dict_info = CreateDict(**create_info)
session.add(create_dict_info)
# session.flush()
err = session.done(close=False)
create_info_id = create_dict_info.create_dict_data_id
if err:
logger.error(f'新增失败!{err}')
return 0
if not create_info_id:
logger.error(f'{create_dict_info}获取关键字id失败')
return 0
# session.commit()
return create_info_id
@staticmethod
def update_dict_info_record(session, update_info, create_dict_data_id):
create_info = session.query(CreateDict).filter(
CreateDict.create_dict_data_id == create_dict_data_id).one()
create_info.team_name = update_info["team_name"]
create_info.dict_key = update_info["dict_key"]
create_info.dict_value = update_info["dict_value"]
err_msg = session.done()
if err_msg:
return '修改失败!'
return ''

View File

@@ -0,0 +1,71 @@
# encoding: UTF-8
from ..model.createDataInfo import CreateInfo
from sqlalchemy.exc import NoResultFound
from logger import logger
class CreateInfoDao(object):
@staticmethod
def get_data_info(session, filter_list):
try:
ls = session.query(CreateInfo).filter(*filter_list).filter(CreateInfo.del_flag == 0).first()
total = session.query(CreateInfo).filter(*filter_list).filter(CreateInfo.del_flag == 0).count()
except Exception as e:
logger.warning(e)
return [], '获取基础信息失败!'
return ls, total
@staticmethod
def get_data_simple_by_filters(session, filter_list):
rets = session.query(CreateInfo) \
.filter(*filter_list).filter(CreateInfo.del_flag == 0) \
.first()
return rets
@staticmethod
def get_create_data_info_by_id(session, sid):
try:
ret = session.query(CreateInfo).filter(CreateInfo.create_data_info_id == sid).one()
except Exception as e:
logger.warning(f'获取造数信息失败: id{sid}, {e}')
return None, f'获取造数信息失败: id{sid}'
return ret
@staticmethod
def get_create_data_info_by_detail_id(session, detail_id):
try:
ret = session.query(CreateInfo).filter(CreateInfo.create_data_detail_id == detail_id).one()
except Exception as e:
logger.warning(f'获取造数信息失败: id{detail_id}, {e}')
return None, f'获取造数信息失败: id{detail_id}'
return ret
@staticmethod
def save_data_info_record(session, create_info):
if not isinstance(create_info, dict):
logger.error('只支持dict类型。build_info type: {}'.format(type(create_info)))
create_data_info = CreateInfo(**create_info)
session.add(create_data_info)
# session.flush()
err = session.done(close=False)
create_info_id = create_data_info.create_data_info_id
if err:
logger.error(f'新增失败!{err}')
return 0
if not create_info_id:
logger.error(f'{create_data_info}获取关键字id失败')
return 0
# session.commit()
return create_info_id
@staticmethod
def update_data_info_record(session, update_info, create_data_info_id):
create_info = session.query(CreateInfo).filter(CreateInfo.create_data_info_id == create_data_info_id).one()
create_info.class_name = update_info['class_name']
create_info.method_name = update_info['method_name']
create_info.request_parameter = update_info['request_parameter']
err_msg = session.done()
if err_msg:
return '修改失败!'
return ''

View File

@@ -0,0 +1,60 @@
# encoding: UTF-8
from ..model.createDataNumber import CreateDataNumber
from logger import logger
class CreateNumberDao(object):
@staticmethod
def get_number_no_filters(session):
rets = session.query(CreateDataNumber) \
.filter(CreateDataNumber.del_flag == 0) \
.all()
return rets
@staticmethod
def get_number_simple_by_filters(session, filter_list):
rets = session.query(CreateDataNumber) \
.filter(*filter_list).filter(CreateDataNumber.del_flag == 0) \
.first()
return rets
@staticmethod
def get_number_info_by_id(session, create_data_detail_id):
try:
ret = session.query(CreateDataNumber).filter(
CreateDataNumber.create_data_detail_id == create_data_detail_id).first()
except Exception as e:
logger.warning(f'获取造数使用次数失败: id{create_data_detail_id}, {e}')
return f'获取造数使用次数失败: id{create_data_detail_id}', False
return ret, True
@staticmethod
def save_number_info(session, create_info):
if not isinstance(create_info, dict):
logger.error('只支持dict类型。build_info type: {}'.format(type(create_info)))
create_detail_info = CreateDataNumber(**create_info)
session.add(create_detail_info)
# session.flush()
err = session.done(close=False)
create_info_id = create_detail_info.id
if err:
logger.error(f'新增失败!{err}')
return 0
if not create_info_id:
logger.error(f'{create_detail_info}获取关键字id失败')
return 0
# session.commit()
return create_info_id
@staticmethod
def update_number_info(session, update_info, create_data_detail_id):
update_res = session.query(CreateDataNumber).filter(
CreateDataNumber.create_data_detail_id == create_data_detail_id).update(
update_info)
print(update_res, "update_res")
err_msg = session.commit()
# session.done()
if err_msg:
return '基础信息修改失败!'
return update_res

View File

@@ -0,0 +1,125 @@
# encoding: UTF-8
from sqlalchemy import func
from ..model.createResultInfo import CreateResult
from sqlalchemy.exc import NoResultFound
from logger import logger
class CreateResultDao(object):
@staticmethod
def get_result_list_by_filters(session, filter_list, page=1, limit=20):
rets = session.query(CreateResult) \
.filter(*filter_list).filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateResult).filter(*filter_list).filter(CreateResult.del_flag == 0,
CreateResult.status == 1).count()
return rets, total
@staticmethod
def get_result_list_by_team_filters(session, filter_list, list_team_name, page=1, limit=20):
# rets = session.query(CreateResult) \
# .filter(*filter_list).filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
# .filter(CreateResult.team_name.in_(list_team_name)) \
# .offset((int(page) - 1) * int(limit)) \
# .limit(limit) \
# .all()
# total = session.query(CreateResult).filter(*filter_list).filter(CreateResult.del_flag == 0,
# CreateResult.status == 1).filter(
# CreateResult.team_name.in_(list_team_name)).count()
rets = session.query(CreateResult) \
.filter(*filter_list).filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateResult).filter(*filter_list).filter(CreateResult.del_flag == 0,
CreateResult.status == 1).count()
return rets, total
@staticmethod
def get_result_list_no_filters(session, page=1, limit=20):
rets = session.query(CreateResult) \
.filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
.order_by(CreateResult.modified_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateResult).filter(CreateResult.del_flag == 0, CreateResult.status == 1).count()
return rets, total
@staticmethod
def get_result_list_no_team_filters(session, list_team_name, page=1, limit=20):
# rets = session.query(CreateResult) \
# .filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
# .filter(CreateResult.team_name.in_(list_team_name)) \
# .order_by(CreateResult.modified_time.desc()) \
# .offset((int(page) - 1) * int(limit)) \
# .limit(limit) \
# .all()
# total = session.query(CreateResult).filter(CreateResult.del_flag == 0, CreateResult.status == 1).filter(
# CreateResult.team_name.in_(list_team_name)).count()
rets = session.query(CreateResult) \
.filter(CreateResult.del_flag == 0, CreateResult.status == 1) \
.order_by(CreateResult.modified_time.desc()) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateResult).filter(CreateResult.del_flag == 0, CreateResult.status == 1).count()
return rets, total
@staticmethod
def get_result_simple_by_filters(session, filter_list):
rets = session.query(CreateResult) \
.filter(*filter_list).filter(CreateResult.del_flag == 0) \
.first()
return rets
@staticmethod
def get_result_detail_info_by_id(session, sid):
try:
ret = session.query(CreateResult).filter(CreateResult.create_data_result_id == sid).one()
except Exception as e:
logger.warning(f'获取造数基础信息失败: id{sid}, {e}')
return None, f'获取造数基础信息失败: id{sid}'
return ret
@staticmethod
def save_result_info_record(session, create_info):
if not isinstance(create_info, dict):
logger.error('只支持dict类型。build_info type: {}'.format(type(create_info)))
create_result_info = CreateResult(**create_info)
session.add(create_result_info)
# session.flush()
err = session.done(close=False)
create_info_id = create_result_info.create_data_result_id
if err:
logger.error(f'新增失败!{err}')
return 0
if not create_info_id:
logger.error(f'{create_result_info}获取造数结果id失败')
return 0
# session.commit()
return create_info_id
@staticmethod
def update_result_record(session, update_info, sid):
create_info = session.query(CreateResult).filter(
CreateResult.create_data_result_id == sid).one()
create_info.status = update_info["status"]
create_info.result_info = update_info["result_info"]
err_msg = session.done()
if err_msg:
return '修改结果失败!'
return ''
@staticmethod
def get_result_numbers_by_month(session, create_data_detail_id, one_month_ago_str):
ret = session.query(func.count(CreateResult.create_data_detail_id).label('count')).filter(
CreateResult.created_time >= one_month_ago_str,
CreateResult.create_data_detail_id == create_data_detail_id, CreateResult.status == 1).group_by(
CreateResult.method_function_detail
).first()
return ret

View File

@@ -0,0 +1,42 @@
# encoding: UTF-8
from ..model.createGroupUser import CreateGroupUser
from logger import logger
class CreateGroupDao(object):
@staticmethod
def get_group_list_by_filters(session, filter_list, page=1, limit=20):
rets = session.query(CreateGroupUser) \
.filter(*filter_list).filter(CreateGroupUser.del_flag == 0) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateGroupUser).filter(*filter_list).filter(CreateGroupUser.del_flag == 0).count()
return rets, total
@staticmethod
def get_group_list_no_filters(session, page=1, limit=20):
rets = session.query(CreateGroupUser) \
.filter(CreateGroupUser.del_flag == 0) \
.offset((int(page) - 1) * int(limit)) \
.limit(limit) \
.all()
total = session.query(CreateGroupUser).filter(CreateGroupUser.del_flag == 0).count()
return rets, total
@staticmethod
def get_group_all_data(session):
rets = session.query(CreateGroupUser).filter(CreateGroupUser.del_flag == 0).all()
return rets
@staticmethod
def get_group_detail_info_by_email(session, email):
try:
if email is not None:
ret = session.query(CreateGroupUser).filter(CreateGroupUser.email == email).one()
else:
return None
except Exception as e:
logger.warning(f'获取组名失败: id{email}, {e}')
return None
return ret

View File

@@ -0,0 +1,23 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text, JSON
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateInfo(Base):
__tablename__ = 'create_data_info'
create_data_info_id = Column(String(120), primary_key=True, comment='传入参数的id')
create_data_detail_id = Column(String(120), unique=True, comment='基础信息的id')
class_name = Column(String(120), unique=True, comment='类名称')
method_name = Column(String(120), unique=True, comment='方法名称')
request_parameter = Column(JSON, unique=True, comment='请求参数')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), comment='创建时间')
modified_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'),
server_onupdate=text('CURRENT_TIMESTAMP'), comment='更新时间')
def __repr__(self):
return '<create_data_info %r>' % self.create_data_info_id

View File

@@ -0,0 +1,23 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateDataNumber(Base):
__tablename__ = 'create_data_number'
id = Column(String(100), primary_key=True, comment='次数的id')
create_data_detail_id = Column(String(100), primary_key=True, comment='数据字典的id')
method_name = Column(String(120), unique=True, comment='方法名称')
total_number = Column(Integer, unique=True, comment='方法总使用次数')
error_number = Column(Integer, unique=True, comment='方法使用的错误次数')
right_number = Column(Integer, unique=True, comment='方法使用的正确次数')
month_number = Column(Integer, unique=True, comment='方法使用的最近一个月次数')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
created_time = Column(String(100), unique=True, comment='创建时间')
modified_time = Column(String(100), unique=True, comment='更新时间')
def __repr__(self):
return '<id %r>' % self.id

View File

@@ -0,0 +1,28 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateDetail(Base):
__tablename__ = 'create_data_detail'
create_data_detail_id = Column(String(120), primary_key=True, comment='基础信息的id')
team_name = Column(String(100), unique=True, comment='组名(项目名)')
file_name = Column(String(100), unique=True, comment='文件夹名称')
module_name = Column(String(100), unique=True, comment='模块名称')
class_name = Column(String(120), unique=True, comment='类名称')
method_name = Column(String(120), unique=True, comment='方法名称')
method_function_detail = Column(String(5000), unique=True, comment='方法简单说明')
method_detail = Column(String(5000), unique=True, comment='方法描述')
creator = Column(String(100), unique=True, comment='责任人')
status = Column(Integer, unique=True, comment='状态:0未审核1创建失败')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), comment='创建时间')
modified_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'),
server_onupdate=text('CURRENT_TIMESTAMP'), comment='更新时间')
def __repr__(self):
return '<create_data_detail_id %r>' % self.create_data_detail_id

View File

@@ -0,0 +1,23 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateDict(Base):
__tablename__ = 'create_dict_data'
create_dict_data_id = Column(String(120), primary_key=True, comment='数据字典的id')
data_type = Column(Integer, unique=True, comment='1路径 2对应名称')
team_name = Column(String(100), unique=True, comment='业务线名称')
dict_key = Column(String(100), unique=True, comment='字典的key')
dict_value = Column(String(255), unique=True, comment='字典的value')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), comment='创建时间')
modified_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'),
server_onupdate=text('CURRENT_TIMESTAMP'), comment='更新时间')
def __repr__(self):
return '<create_dict_data_id %r>' % self.create_dict_data_id

View File

@@ -0,0 +1,19 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateGroupUser(Base):
__tablename__ = 'create_group_user'
id = Column(Integer, primary_key=True, comment='用户的id')
email = Column(String(100), unique=True, comment='邮箱')
team_name = Column(String(100), unique=True, comment='组名')
name = Column(String(100), unique=True, comment='用户名称')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
def __repr__(self):
return '<id %r>' % self.id

View File

@@ -0,0 +1,31 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, TIMESTAMP, text, JSON
from ....common.sqlSession import to_dict
Base = declarative_base()
Base.to_dict = to_dict
class CreateResult(Base):
__tablename__ = 'create_data_result'
create_data_result_id = Column(String(120), primary_key=True, comment='执行结果的id')
create_data_info_id = Column(String(120), unique=True, comment='传入参数的id')
create_data_detail_id = Column(String(120), unique=True, comment='基础信息的id')
request_parameter = Column(JSON, unique=True, comment='请求参数')
result_info = Column(JSON, unique=True, comment='造数结果')
team_name = Column(String(100), unique=True, comment='组名(项目名)')
module_name = Column(String(100), unique=True, comment='模块名称')
class_name = Column(String(120), unique=True, comment='类名称')
file_name = Column(String(255), unique=True, comment='文件名称')
method_name = Column(String(120), unique=True, comment='方法名称')
tag = Column(String(1000), unique=True, comment='标签')
status = Column(Integer, unique=True, comment='状态')
method_function_detail = Column(String(5000), unique=True, comment='方法描述')
del_flag = Column(Integer, unique=True, comment='0未删除1已删除')
creator = Column(String(100), unique=True, comment='创建人')
created_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'), comment='创建时间')
modified_time = Column(TIMESTAMP, server_default=text('CURRENT_TIMESTAMP'),
server_onupdate=text('CURRENT_TIMESTAMP'), comment='更新时间')
def __repr__(self):
return '<create_data_result_id %r>' % self.create_data_result_id

View File

@@ -0,0 +1,61 @@
# encoding: UTF-8
from ..dao.createDataDetailDao import CreateDetailDao
class CreateDetailService(object):
def __init__(self):
pass
@staticmethod
def get_all_detail_list_no_filters_page(session):
scene_list, total = CreateDetailDao.get_all_detail_list_no_filters(session)
return scene_list, total
@staticmethod
def get_detail_list_no_filters_page(session, page, limit):
scene_list, total = CreateDetailDao.get_detail_list_no_filters(session, page=page, limit=limit)
return scene_list, total
@staticmethod
def get_detail_list_by_filters_page(session, filter_list, page, limit):
ret, err_msg = CreateDetailDao.get_detail_list_by_filters(session, filter_list, page=page, limit=limit)
return ret, err_msg
@staticmethod
def get_detail_list_by_filters_team_page(session, filter_list, list_team_name, page, limit):
ret, err_msg = CreateDetailDao.get_detail_list_by_filters_by_team(session, filter_list,
list_team_name=list_team_name,
page=page, limit=limit)
return ret, err_msg
@staticmethod
def get_detail_list_no_filters_team_page(session, list_team_name, page, limit):
scene_list, total = CreateDetailDao.get_detail_list_no_filters_by_team(session, list_team_name=list_team_name,
page=page, limit=limit)
return scene_list, total
@staticmethod
def get_detail_simple_data_by_filters(session, filter_list):
ret = CreateDetailDao.get_detail_simple_by_filters(session, filter_list)
return ret
@staticmethod
def get_create_detail_info_by_id(session, create_data_detail_id):
err_msg = CreateDetailDao.get_create_detail_info_by_id(session, create_data_detail_id)
return err_msg
@staticmethod
def create_service_detail_data(session, create_info):
ret = CreateDetailDao.save_detail_info_record(session, create_info)
return ret
@staticmethod
def update_service_detail_data(session, update_info, create_data_detail_id):
err_msg = CreateDetailDao.update_detail_info_record(session, update_info, create_data_detail_id)
return err_msg
@staticmethod
def delete_service_detail_data(session, create_data_detail_id):
bf_dao = CreateDetailDao()
del_info = bf_dao.delete_detail_info_record(session, create_data_detail_id)
return del_info

View File

@@ -0,0 +1,38 @@
# encoding: UTF-8
from ..dao.createDataDictDao import CreateDictDao
class CreateDictService(object):
def __init__(self):
pass
@staticmethod
def get_dict_list_no_filters_page(session, page, limit):
scene_list, total = CreateDictDao.get_dict_list_no_filters(session, page=page, limit=limit)
return scene_list, total
@staticmethod
def get_dict_list_by_filters_page(session, filter_list, page, limit):
ret, total = CreateDictDao.get_dict_list_by_filters(session, filter_list, page=page, limit=limit)
return ret, total
@staticmethod
def get_dict_list_by_team_filters_page(session, filter_list, list_team_name, page, limit):
ret, total = CreateDictDao.get_dict_list_by_team_filters(session, filter_list, list_team_name, page=page,
limit=limit)
return ret, total
@staticmethod
def get_dict_simple_data_by_filters(session, filter_list):
ret = CreateDictDao.get_dict_simple_by_filters(session, filter_list)
return ret
@staticmethod
def create_service_dict_data(session, create_info):
ret = CreateDictDao.save_dict_info_record(session, create_info)
return ret
@staticmethod
def update_service_dict_data(session, update_info, create_dict_data_id):
err_msg = CreateDictDao.update_dict_info_record(session, update_info, create_dict_data_id)
return err_msg

View File

@@ -0,0 +1,22 @@
# encoding: UTF-8
from ..dao.createGroupUserDao import CreateGroupDao
class CreateGroupService(object):
def __init__(self):
pass
@staticmethod
def get_service_group_list_no_filters_page(session, page, limit):
scene_list, total = CreateGroupDao.get_group_list_no_filters(session, page=page, limit=limit)
return scene_list, total
@staticmethod
def get_service_group_list_all_data(session):
scene_list = CreateGroupDao.get_group_all_data(session)
return scene_list
@staticmethod
def get_service_group_info_data_by_email(session, email):
scene_list = CreateGroupDao.get_group_detail_info_by_email(session, email)
return scene_list

View File

@@ -0,0 +1,37 @@
# encoding: UTF-8
from ..dao.createDataInfoDao import CreateInfoDao
class CreateInfoService(object):
def __init__(self):
pass
@staticmethod
def get_service_info_data(session,filter_list):
ret, err_msg = CreateInfoDao.get_data_info(session,filter_list)
return ret, err_msg
@staticmethod
def get_data_simple_data_by_filters(session, filter_list):
ret = CreateInfoDao.get_data_simple_by_filters(session, filter_list)
return ret
@staticmethod
def get_create_data_info_by_id(session, sid):
scene_obj = CreateInfoDao.get_create_data_info_by_id(session, sid)
return scene_obj
@staticmethod
def get_create_data_info_by_detail_id(session, detail_id):
scene_obj = CreateInfoDao.get_create_data_info_by_detail_id(session, detail_id)
return scene_obj
@staticmethod
def create_service_info_data(session, create_info):
ret = CreateInfoDao.save_data_info_record(session, create_info)
return ret
@staticmethod
def update_service_info_data(session, update_info,create_data_info_id):
err_msg = CreateInfoDao.update_data_info_record(session,update_info,create_data_info_id)
return err_msg

View File

@@ -0,0 +1,32 @@
# encoding: UTF-8
from ..dao.createDataNumberDao import CreateNumberDao
class CreateNumberService(object):
def __init__(self):
pass
@staticmethod
def get_service_number_all_no_filters(session):
ret = CreateNumberDao.get_number_no_filters(session)
return ret
@staticmethod
def get_service_number_simple_by_filters(session, filter_list):
ret = CreateNumberDao.get_number_simple_by_filters(session, filter_list)
return ret
@staticmethod
def get_service_number_by_id(session, create_data_detail_id):
err_msg, success = CreateNumberDao.get_number_info_by_id(session, create_data_detail_id)
return err_msg, success
@staticmethod
def create_service_number_data(session, create_info):
ret = CreateNumberDao.save_number_info(session, create_info)
return ret
@staticmethod
def update_service_number_data(session, update_info, create_data_detail_id):
err_msg = CreateNumberDao.update_number_info(session, update_info, create_data_detail_id)
return err_msg

View File

@@ -0,0 +1,55 @@
# encoding: UTF-8
from ..dao.createDataResultDao import CreateResultDao
class CreateResultService(object):
def __init__(self):
pass
@staticmethod
def get_result_list_no_filters_page(session, page, limit):
scene_list, total = CreateResultDao.get_result_list_no_filters(session, page=page, limit=limit)
return scene_list, total
@staticmethod
def get_result_list_no_team_filters_page(session, list_team_name, page, limit):
scene_list, total = CreateResultDao.get_result_list_no_team_filters(session, list_team_name, page=page,
limit=limit)
return scene_list, total
@staticmethod
def get_result_list_by_filters_page(session, filter_list, page, limit):
ret, err_msg = CreateResultDao.get_result_list_by_filters(session, filter_list, page=page, limit=limit)
return ret, err_msg
@staticmethod
def get_result_list_by_team_filters_page(session, filter_list, list_team_name, page, limit):
ret, err_msg = CreateResultDao.get_result_list_by_team_filters(session, filter_list,
list_team_name=list_team_name,
page=page, limit=limit)
return ret, err_msg
@staticmethod
def get_result_simple_data_by_filters(session, filter_list):
ret = CreateResultDao.get_result_simple_by_filters(session, filter_list)
return ret
@staticmethod
def get_create_result_by_id(session, create_data_result_id):
err_msg = CreateResultDao.get_result_detail_info_by_id(session, create_data_result_id)
return err_msg
@staticmethod
def create_result_data(session, create_info):
ret = CreateResultDao.save_result_info_record(session, create_info)
return ret
@staticmethod
def update_service_result_data(session, update_info, sid):
err_msg = CreateResultDao.update_result_record(session, update_info, sid)
return err_msg
@staticmethod
def get_service_result_numbers_by_month(session, create_data_detail_id, one_month_ago_str):
err_msg = CreateResultDao.get_result_numbers_by_month(session, create_data_detail_id, one_month_ago_str)
return err_msg

View File

@@ -0,0 +1,593 @@
# encoding: UTF-8
from flask import Blueprint, request
from ...common.apiResponse import ApiResponse
from .controller.createDataInfoController import CreateDataInfoController
from .controller.createDataDetailController import CreateDataDetailController
from .controller.createDataDictController import CreateDataDictController
from .controller.createDataResultController import CreateDataResultController
from .controller.commonController import CommonController
from .controller.scrapyController import ScrapyController
from .controller.userController import UserController
api = Blueprint('api', __name__)
@api.route('/add/info/data', methods=['POST'])
def add_info_data():
"""新增造数请求参数数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_info_id | true | query | int | 基本信息的id |
| create_data_detail_id | true | query | int | 详细信息的id |
| className | true | query | string | 类名 |
| methodName | true | query | string | 方法名 |
| request_parameter | true | query | string | 请求参数 |
### 请求
```json
{"create_data_info_id":"","create_data_detail_id":102222,"className":"","methodName":"","request_parameter":{}}
```
### 响应
```json
{"code": x, "message": "x", "data":true,"success":}
```
@@@
"""
req_json = request.get_json()
access_token = request.headers['accesstoken']
controller = CreateDataInfoController(req_json, access_token)
ret = controller.create_info_data()
if ret:
return ApiResponse.build_failure(5000, data=ret)
return ApiResponse.build_success(20000, data=True)
@api.route('/edit/info/data', methods=['POST'])
def edit_info_data():
"""编辑造数请求参数数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_info_id | true | query | int | 基本信息的id |
| create_data_detail_id | true | query | int | 详细信息的id |
| className | true | query | string | 类名 |
| methodName | true | query | string | 方法名 |
| request_parameter | true | query | string | 请求参数 |
### 请求
```json
{"create_data_info_id":"","create_data_detail_id":102222,"className":"","methodName":"","request_parameter":{}}
```
### 响应
```json
{"code": x, "message": "x ", "data":true,"success":}
```
@@@
"""
req_json = request.get_json()
access_token = request.headers['accesstoken']
controller = CreateDataInfoController(req_json, access_token)
ret = controller.update_info_data()
if ret:
return ApiResponse.build_failure(5000, data=ret)
return ApiResponse.build_success(20000, data=True)
@api.route('/delete/detail/data', methods=['POST'])
def delete_detail_data():
"""删除造数详情数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_detail_id | true | query | int | 详细信息的id |
### 请求
```json
{"create_data_detail_id":102222}
```
### 响应
```json
{"code": x, "message": "删除成功", "data":true,"success":}
```
@@@
"""
req_json = request.get_json()
access_token = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, access_token)
ret = controller.delete_detail_data()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/detail/list', methods=['POST'])
def get_keywors_detail_list():
"""查询造数关键字基本信息列表页数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| pageNo | true | query | int | 基本信息的id |
| pageSize | true | query | int | 详细信息的id |
| detail_name | true | query | string | 基本描述信息 |
| team_name | true | query | string | 业务线 |
| file_name | true | query | string | 业务对应业务线名称 |
| module_name | true | query | string | 模块名称 |
### 请求
```json
{"pageNo":1,"pageSize":10,"dict_value":"","team_name":""}
```
### 响应
```json
{"code": x, "message": "x", "data":list[{},{}],"success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, accesstoken)
ret = controller.get_detail_query_advance()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/detail/advance', methods=['POST'])
def get_keywors_detail_advance():
"""查询造数关键字基本信息详情页数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_detail_id | true | query | int | 基本信息的id |
### 请求
```json
{"create_data_detail_id":123}
```
### 响应
```json
{"code": x, "message": "x", "data":{},"success":True}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, accesstoken)
ret = controller.get_detail_advance()
return ApiResponse.build_success(20000, data=ret)
@api.route('/keyword/run', methods=['POST'])
def run_keywords():
"""执行造数
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_info_id | true | query | string | 基本信息的id |
| create_data_detail_id | true | query | string | 详细信息的id |
| dict_key | true | query | string | 组名 |
| get_tag | true | query | string | 标签,备注说明 |
| request_parameter | true | query | string | 请求参数 |
### 请求
```json
{"create_data_info_id":"12","create_data_detail_id":"13","dict_key":"StudentEvent","get_tag":"get_student_classroom","request_parameter":""}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
access_token = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, access_token)
data = controller.run_keyword_data()
if data.get("message") == "造数成功":
return ApiResponse.build_success(20000, message="造数成功", data=data.get("data"))
return ApiResponse.build_failure(40012, msg=data.get("message"))
@api.route('/create/data/result/list', methods=['POST'])
def get_keywors_result_list():
"""查询造数关键字造数结果列表页数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| pageNo | true | query | int | 页码 |
| pageSize | true | query | int | 页码规格 |
| detail_name | true | query | string | 详细描述信息 |
| team_name | true | query | string | 业务线 |
| get_tag | true | query | string | 标签,备注 |
### 请求
```json
{"pageNo":1,"pageSize":10,"detail_name":"","team_name":"","get_tag":""}
```
### 响应
```json
{"code": x, "message": "x", "data":list[{},{}],"success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataResultController(req_json, accesstoken)
ret = controller.get_result_query_list()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/result/advance', methods=['POST'])
def get_keywors_result_advance():
"""查询造数结果的详情信息
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_result_id | true | query | int | 基本信息的id |
### 请求
```json
{"create_data_result_id":123}
```
### 响应
```json
{"code": x, "message": "x", "data":{},"success":True}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataResultController(req_json, accesstoken)
ret = controller.get_result_advance()
return ApiResponse.build_success(20000, data=ret)
@api.route('/add/dict/data', methods=['POST'])
def edit_dict_data():
"""编辑造数关键字进行的映射
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_dict_data_id | true | query | int | 基本信息的id |
| data_type | true | query | int | 详细信息的id |
| dict_key | true | query | string | 映射的key |
| dict_value | true | query | string | 字段映射对应的名称 |
| team_name | true | query | string | 组名 |
| created_time | true | query | string | 创建时间 |
| modified_time | true | query | string | 修改时间 |
### 请求
```json
{"create_dict_data_id":"e757b5cd-cec5-11ed-ae4a-c8b29bebcb51","team_name":"用户","data_type":3,"dict_key":"common","dict_value":"公共","created_time":"202303301440","modified_time":"202304111341"}
```
### 响应
```json
{"code": x, "message": "x", "data":list[{},{}],"success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDictController(req_json, accesstoken)
ret = controller.update_dict_data()
return ApiResponse.build_success(20000, data=ret)
@api.route('/dict/data/page', methods=['POST'])
def get_dict_data_list():
"""查询造数关键字配置列表
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| pageNo | true | query | int | 基本信息的id |
| pageSize | true | query | int | 详细信息的id |
| dict_value | true | query | string | 配置值 |
| team_name | true | query | string | 业务线 |
### 请求
```json
{"pageNo":1,"pageSize":10,"detail_name":"","team_name":"","file_name":"","module_name":""}
```
### 响应
```json
{"code": x, "message": "x", "data":list[{},{}],"success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDictController(req_json, accesstoken)
ret = controller.get_dict_list_data()
return ApiResponse.build_success(20000, data=ret)
@api.route('/get/pull/git', methods=['POST'])
def get_new_git_info():
"""获取git上的最新代码到本地
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| fileName | true | query | string | 传入需要抓取的文件名称 |
| team | true | query | string | 业务线名称 |
| username | true | query | string | git地址用户名 |
| password | true | query | string | git地址密码 |
### 请求
```json
{"team":"USER","fileName":"","username":"","password":""}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
# accesstoken = request.headers['accesstoken']
accesstoken = "22233"
controller = ScrapyController(req_json, accesstoken)
ret = controller.get_git_pull()
return ApiResponse.build_success(20000, data=ret)
@api.route('/get/eureka', methods=['POST'])
def get_eureka_ip():
"""更新eureka上的ip地址
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| team | true | query | string | 业务线名称 |
### 请求
```json
{"team":"USER"}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = ScrapyController(req_json, accesstoken)
ret = controller.get_eureka_ip()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/detail/scrapy', methods=['POST'])
def scrapy_entery_db():
"""查找出全部的可用方法与描述进行录库
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| fileName | true | query | string | 传入需要抓取的文件名称 |
| team | true | query | string | 业务线名称 |
| username | true | query | string | git地址用户名 |
| password | true | query | string | git地址密码 |
### 请求
```json
{"team":"USER","fileName":"","username":"","password":""}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
# accesstoken = request.headers['accesstoken']
accesstoken = "123"
controller = ScrapyController(req_json, accesstoken)
ret = controller.scrapy_enter_db()
if ret != "抓取成功":
return ApiResponse.build_failure(40012, msg=ret)
return ApiResponse.build_success(20000, message=ret)
@api.route('/get/team/name', methods=['POST'])
def get_team_name():
"""获取各个业务线的数据字典,用于查询
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| data_type | true | query | int | 类型 |
| team_name | true | query | string | 组名 |
### 请求
```json
{"data_type":2,"team_name":""}
```
### 响应
```json
{"code": x, "message": "x", "data":"","success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDictController(req_json, accesstoken)
ret = controller.get_team_name()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/detail/delete', methods=['POST'])
def delete_data_detail():
"""删除造数关键字基本信息
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_detail_id | true | query | int | 基本信息的id |
### 请求
```json
{"create_data_detail_id":""}
```
### 响应
```json
{"code": x, "message": "x", "data":true,"success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, accesstoken)
ret = controller.delete_detail_data()
return ApiResponse.build_success(20000, data=ret)
@api.route('/create/data/info/add', methods=['POST'])
def add_keywors_data():
"""增加造数关键字的请求参数
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_info_id | true | query | string | 基本信息的id |
| create_data_detail_id | true | query | string | 详细信息的id |
| className | true | query | string | 类名 |
| methodName | true | query | string | 方法名 |
| request_parameter | true | query | string | 请求参数 |
### 请求
```json
{"create_data_info_id":"12","create_data_detail_id":"13","className":"StudentEvent","methodName":"get_student_classroom","request_parameter":""}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataInfoController(req_json, accesstoken)
err_msg = controller.create_info_data()
if err_msg:
return ApiResponse.build_failure(40012, msg=err_msg)
return ApiResponse.build_success(20000)
@api.route('/create/data/info/list', methods=['POST'])
def get_keywors_data_list():
"""查询造数关键字请求列表页数据
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_info_id | true | query | string | 基本信息的id |
| create_data_detail_id | true | query | string | 详细信息的id |
| className | true | query | string | 类名 |
| methodName | true | query | string | 方法名 |
| request_parameter | true | query | string | 请求参数 |
### 请求
```json
{"create_data_info_id":"12","create_data_detail_id":"13","className":"StudentEvent","methodName":"get_student_classroom","request_parameter":""}
```
### 响应
```json
{"code": x, "message": "x", "success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataInfoController(req_json, accesstoken)
ret = controller.create_info_data()
return ApiResponse.build_success(20000, data=ret)
@api.route('/Login', methods=['POST'])
def get_user_login():
"""判断登录,根据登录信息获取邮箱
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| username | true | query | string | 用户名 |
| password | true | query | string | 密码 |
### 请求
```json
{"username":2,"password":""}
```
### 响应
```json
{"code": x, "message": "x", "data":"","success":}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = UserController(req_json, accesstoken)
ret = controller.get_user_email()
if ret.get("code") != 20000:
return ApiResponse.build_failure(40012, msg=ret.get("message"))
return ApiResponse.build_success(20000, data=ret.get("data"))
@api.route('/create/data/detail/approve', methods=['POST'])
def approve_detail_info():
"""对关键字进行审核
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
| create_data_detail_id | true | query | int | 基本信息的id |
### 请求
```json
{"create_data_detail_id":123}
```
### 响应
```json
{"code": x, "message": "x", "data":{},"success":True}
```
@@@
"""
req_json = request.get_json()
accesstoken = request.headers['accesstoken']
controller = CreateDataDetailController(req_json, accesstoken)
ret = controller.approve_detail_status()
if ret.get("code") != 20000:
return ApiResponse.build_failure(40012, msg=ret.get("message"))
return ApiResponse.build_success(20000, data=ret)
@api.route('/update/month/use/number', methods=['GET'])
def update_month_use_number():
"""每月使用次数更新,定时任务
@@@
### 参数
| 参数名 | 是否必传 | 请求类型 |参数类型 | 参数描述 |
|-------|---------|--------|--------|----------|
### 请求
```json
```
### 响应
```json
{"code": x, "message": "x", "data":{},"success":True}
```
@@@
"""
controller = CommonController()
ret = controller.daily_task_update_month_number()
if ret.get("code") != 20000:
return ApiResponse.build_failure(40012, msg=ret.get("message"))
return ApiResponse.build_success(20000, data=ret)

View File

@@ -0,0 +1,54 @@
# encoding: UTF-8
from flask import make_response
import json
from ..const import RES_CODE
class ApiResponse(object):
def __init__(self):
self.success = False
self.code = ''
self.message = ''
self.data = {}
@staticmethod
def build_success(code=20000, message='', data=None):
if data is None:
data = {}
response = ApiResponse()
response.success = True
response.code = code
response.message = message
response.data = data
return response.cors_response(make_response(json.dumps(response, default=obj_2_json)))
@staticmethod
def build_failure(code, msg='', data=None):
response = ApiResponse()
if data is None:
data = {}
if not msg:
response.message = RES_CODE[code]
else:
response.message = msg
response.success = False
response.code = code
response.data = data
return response.cors_response(make_response(json.dumps(response, default=obj_2_json)))
@staticmethod
def cors_response(res):
res.headers['Access-Control-Allow-Origin'] = '*'
res.headers['Access-Control-Allow-Methods'] = 'POST'
res.headers['Access-Control-Allow-Headers'] = 'x-requested-with,content-type'
return res
def obj_2_json(obj):
return {
'success': obj.success,
'code': obj.code,
'message': obj.message,
'data': obj.data
}

View File

@@ -0,0 +1,35 @@
from ..const import CREATE_URI
from ..common.getRequest import Request
class CronRequest(object):
def __init__(self, token):
self.stress_api = CREATE_URI
self.headers = {'accesstoken': token, 'Accept': '*/*', 'content-type': 'application/json;charset=UTF-8'}
def create(self, params):
url = self.stress_api + '/back-end/stress/schedule/save'
ret = Request.go('post', url, params, self.headers)
if not ret:
return
return ret.get('id')
def pause(self, jid):
url = self.stress_api + '/back-end/stress/schedule/pause'
params = [jid]
Request.go('post', url, params, self.headers)
def resume(self, jid):
url = self.stress_api + '/back-end/stress/schedule/resume'
params = [jid]
Request.go('post', url, params, self.headers)
def remove(self, jid):
url = self.stress_api + '/back-end/stress/schedule/delete'
params = [jid]
Request.go('post', url, params, self.headers)
def update(self, req_params):
url = self.stress_api + '/back-end/stress/schedule/update'
Request.go('post', url, req_params, self.headers)

View File

@@ -0,0 +1,16 @@
import requests
class FeiShuMessage:
def __init__(self):
self.headers = {'Content-Type': 'application/json; charset=utf-8'}
self.webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/180fa48e-1474-448e-a3d5-1a530f6ca689"
def send_message(self, msg, url=None):
url = url if url else self.webhook
res = requests.post(url, headers=self.headers, json=msg, verify=False)
if res.status_code == 200:
return True
else:
return False

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
import requests
import json
from requests.exceptions import ConnectionError
from logger import logger
class Request(object):
@classmethod
def go(cls, method, url, params, headers=None, noFormat=False):
# logger.info(f'发送{method}请求到: {url}, 参数: {params}')
try:
if method == 'get':
response = requests.get(url=url, params=params, headers=headers, timeout=20)
elif method == 'post':
response = requests.post(url=url, data=json.dumps(params), headers=headers, timeout=20)
else:
logger.error(f'暂不支持{method}方法')
return
except ConnectionRefusedError:
logger.error(f'服务请求失败:{url}')
return
except ConnectionError:
logger.error(f'服务无法链接: {url}')
return
if response.status_code != 200:
logger.error(f'返回码不等于200请检查服务{response.status_code}, {response.text}')
else:
resp_json = response.json()
# logger.info(f'返回内容:{resp_json}')
# noFormat: 不需要对返回内容进行校验直接返回整个response
if noFormat:
return resp_json
# 对response做校验返回体为qe平台的通用格式
if resp_json.get('success') or resp_json.get('code') == 20000:
return resp_json.get('data')
else:
logger.error(resp_json)
return

View File

@@ -0,0 +1,24 @@
from ..common.getRequest import Request
from ..const import STRESS_URI
class UserInfo(object):
@staticmethod
def get_user_info(access_token, url_prefix=None, info='userId'):
stress_uri = STRESS_URI
url = "/back-end/stress/user/info"
result = Request.go(method="get",
url=stress_uri + url if not url_prefix else url_prefix + url,
params=None,
headers={"accessToken": access_token})
return None if not result else result.get(info)
@staticmethod
def get_user_info_by_user_id(access_token, user_id, info):
stress_uri = STRESS_URI
url = "/back-end/stress/user/infoFromId"
result = Request.go(method="get", url=stress_uri+url, params={'userId': user_id},
headers={"accessToken": access_token})
return None if not result else result.get(info)

View File

@@ -0,0 +1,83 @@
# 创建连接相关
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from base_framework.platform_tools.IDF.const import create_sql_url
from logger import logger
"""
sql操作
排序order_by(ChartsName.column.desc()/asc())
limit: .offset(n)过滤前面n条数据 .limit(n)
count: .count()计数
是否存在is_exist = session.query(exists().where(Book.id > 10)).scalar()
or: .filter(or_(Chart.column == x, Chart.column > y)).all()
one: .one()只获取一条,如不存在或存在多条都会报错
first: 通过主键获取记录 filter(**).first()
"""
class SqlSession:
def __init__(self, sql_uri=create_sql_url):
self.sql_uri = sql_uri
self._session = self.get_session()
# 创建连接对象,使用 pymsql 引擎
def get_session(self):
engine = create_engine(self.sql_uri, max_overflow=5, connect_args={'connect_timeout': 10})
Session = sessionmaker(bind=engine)
session = Session()
return session
def query(self, obj):
return self._session.query(obj)
def add(self, added):
self._session.add(added)
def add_all(self, added_list):
if isinstance(added_list, list):
self._session.add_all(added_list)
else:
logger.warning('只能传递list')
def flush(self):
self._session.flush()
def commit(self):
self._session.commit()
def close(self):
self._session.close()
def rollback(self):
self._session.rollback()
def object_session(self, instance):
self._session.object_session(instance)
def execute(self, sql):
return self._session.execute(text(sql))
def done(self, close=True):
"""
执行完插入、删除、修改等操作后执行done如报错回滚本次事务的sql操作
:return:
"""
try:
self.commit()
if close:
self.close()
except Exception as e:
logger.warning(e)
self._session.rollback()
return e
@property
def session(self):
return self._session
def to_dict(self):
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns}

View File

@@ -0,0 +1,37 @@
# encoding: UTF-8
import os
from urllib.parse import quote_plus as urlquote
from urllib.parse import quote
# dev环境
# BE_URL = '127.0.0.1:6080'
# online环境
BE_URL = '127.0.0.1:6080'
BASEDIR = os.path.dirname(os.path.abspath(__file__))
# PROJDIR = os.path.dirname(BASEDIR)
LOG_DIR = os.path.join(BASEDIR, 'logs')
# 返回码
RES_CODE = {
40001: 'URL不正确请检查',
40002: '不支持该请求方法!',
40003: '参数有误!',
40004: 'header错误',
40005: 'user_id不能为空! ',
40006: '获取下拉框列表失败!',
40007: '获取接口列表失败!',
}
# sqlalchemy链接
password = urlquote("peppa@test")
create_sql_url = 'mysql+pymysql://root:{}@10.250.200.53/tools-dev?charset=utf8mb4'.format(password) #测试库
PASSWORD = quote('AcUVeRb8lN')
REDIS_URL = "redis://:{}@redis.qa.huohua.cn:6379/30".format(PASSWORD)
STRESS_URI = 'https://qe.bg.huohua.cn'
CREATE_URI = '172.19.24.100'
# dev环境 qe domain
# QE_DOMAIN = 'http://qe.qa.huohua.cn'
# prod环境 qe domain
QE_DOMAIN = 'https://qe.bg.huohua.cn'

View File

@@ -0,0 +1,15 @@
# encoding: UTF-8
from .const import BE_URL
workers = 1 # 定义同时开启的处理请求的进程数量,根据网站流量适当调整
bind = BE_URL
threads = 2 # 多线程2个暂时就够用
debug = False
reload = False
loglevel = 'debug'
pidfile = "logs/gunicorn.pid"
accesslog = "logs/access.log" # 每个接口调用会展示在access.log中
errorlog = "logs/debug.log" # 未处理的报错会在debug.log日志中展示
timeout = 300 # 每个接口的超时时间
daemon = False # 是否开启守护进程。不开启可直接在idea或命令行中查看日志在服务器上需要修改为True来开启守护进程

View File

@@ -0,0 +1,39 @@
import logging
import os
from logging.handlers import TimedRotatingFileHandler
from base_framework.platform_tools.IDF.const import LOG_DIR
class FunctionalTestsLogger(logging.Logger):
def critical(self, msg, *args, **kwargs):
super(FunctionalTestsLogger, self).critical(msg, *args, **kwargs)
raise Exception(msg)
logging.setLoggerClass(FunctionalTestsLogger)
logger = logging.getLogger(FunctionalTestsLogger.__name__)
logger.setLevel(logging.DEBUG)
LOG_FMT = logging.Formatter("%(asctime)s %(filename)-24s[:%(lineno)-4d] %(levelname)-8s %(message)s")
# log by day
# fh = TimedRotatingFileHandler(
# filename=os.path.join(LOG_DIR, f'{datetime.datetime.now().strftime("%Y%m%d")}.log'),
# when="MIDNIGHT",
# encoding='utf-8')
fh = TimedRotatingFileHandler(
filename=os.path.join(LOG_DIR, 'it-log'),
when="MIDNIGHT",
encoding='utf-8')
fh.setLevel(logging.DEBUG)
fh.setFormatter(LOG_FMT)
logger.addHandler(fh)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(LOG_FMT)
logger.addHandler(ch)
logging.basicConfig()

View File

@@ -0,0 +1,64 @@
# encoding: UTF-8
from flask_cors import CORS
from flask import make_response, jsonify, request, redirect
import os, sys
import configparser
import argparse
from apscheduler.schedulers.background import BackgroundScheduler
BASIC_PATH = os.path.dirname(os.path.abspath(__file__))
BASE_PATH = os.path.abspath(os.path.join(BASIC_PATH, '../../../{}'.format("base_framework")))
sys.path.append(BASE_PATH)
PROJECT_PATH = os.path.abspath(os.path.join(BASIC_PATH, '../../..'))
sys.path.append(PROJECT_PATH)
env_choose_path = os.path.join(BASE_PATH, 'base_config', 'env_choose.ini')
team_names = ["UBRD","TMO","H2R","CC","LaLive","SCM","TO"]
for team in team_names:
TEAM_PATH = os.path.abspath(os.path.join(BASIC_PATH, '../../../{}'.format(team)))
sys.path.append(TEAM_PATH)
from base_framework.platform_tools.IDF.app import create_app
from base_framework.platform_tools.IDF.app.api.controller.commonController import CommonController
def start_env():
parser = argparse.ArgumentParser(description="Choose current environment")
parser.add_argument("-e", dest='env', help="ST or PRE", default='QA')
args = parser.parse_args()
choose_env = args.env
return choose_env
get_env = start_env()
cof = configparser.ConfigParser()
cof.read(env_choose_path, encoding='utf-8')
cof.set(section="is_ip_from_ini", option="is_ip_from_ini", value="false")
cof.set(section="run_evn_name", option="current_evn", value=get_env.upper())
with open(env_choose_path, 'w') as fw: # 循环写入
cof.write(fw)
app = create_app()
CORS(app, resources=r'/*')
create_common_controll = CommonController()
# 初始化调度器
scheduler = BackgroundScheduler()
# 添加每日任务每天凌晨0点执行
scheduler.add_job(create_common_controll.daily_task_update_month_number, 'cron', hour=0, minute=0)
# 启动调度器
scheduler.start()
if get_env == "SIM":
app.run(host="0.0.0.0", port=int("5112"), debug=True, use_reloader=False)
else:
app.run(host="0.0.0.0", port=int("5113"), debug=True, use_reloader=False)
def cors_response(res):
response = make_response(jsonify(res))
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Methods'] = '*'
response.headers['Access-Control-Allow-Headers'] = 'x-requested-with,content-type'
return response

View File

@@ -0,0 +1,175 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?><project>
<actions/>
<description>调度任务公共job</description>
<keepDependencies>false</keepDependencies>
<properties>
<jenkins.model.BuildDiscarderProperty>
<strategy class="hudson.tasks.LogRotator">
<daysToKeep>30</daysToKeep>
<numToKeep>100</numToKeep>
<artifactDaysToKeep>-1</artifactDaysToKeep>
<artifactNumToKeep>-1</artifactNumToKeep>
</strategy>
</jenkins.model.BuildDiscarderProperty>
<com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty plugin="build-failure-analyzer@2.0.0">
<doNotScan>false</doNotScan>
</com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty>
<com.chikli.hudson.plugin.naginator.NaginatorOptOutProperty plugin="naginator@1.18.1">
<optOut>false</optOut>
</com.chikli.hudson.plugin.naginator.NaginatorOptOutProperty>
<com.sonyericsson.rebuild.RebuildSettings plugin="rebuild@1.32">
<autoRebuild>false</autoRebuild>
<rebuildDisabled>false</rebuildDisabled>
</com.sonyericsson.rebuild.RebuildSettings>
<hudson.model.ParametersDefinitionProperty>
<parameterDefinitions>
<hudson.model.StringParameterDefinition>
<name>special_env</name>
<description>独立环境</description>
<defaultValue>qa</defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>test_case_path</name>
<description>目录和robot都可以</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>test_case_id</name>
<description>需要构建的用例编号多个用逗号隔开默认空构建所有。例query_classroom-1001-正常请求有数据,query_classroom-1002-正常请求无数据</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>inculde</name>
<description>需要构建的用例tag多个用逗号隔开默认空构建所有。例p0,p1备注tag中不能出现关键字大写AND、OR、NOT可用小写platform-27418</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>exculde</name>
<description>不需要构建的用例tag多个用逗号隔开默认空不过滤。例norun,del</description>
<defaultValue>norun</defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>rerun</name>
<description>是否需要二次构建失败用例。true/false</description>
<defaultValue>true</defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>git_path</name>
<description>构建代码地址</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>team</name>
<description>组名</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>team_code_path</name>
<description>代码分支</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>build_info_id</name>
<description>本次构建对应数据库id</description>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>is_use_db</name>
<description>使用数据库中参数构建qe平台使用</description>
<defaultValue>0</defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
</parameterDefinitions>
</hudson.model.ParametersDefinitionProperty>
</properties>
<scm class="org.jenkinsci.plugins.multiplescms.MultiSCM" plugin="multiple-scms@0.8">
<scms>
<hudson.plugins.git.GitSCM plugin="git@4.3.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>${git_path}</url>
<credentialsId>a670722b-96ec-449f-a2dc-6e6676bf8dbc</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/${team_code_path}</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<gitTool> git-1.8.3.1</gitTool>
<submoduleCfg class="list"/>
<extensions>
<hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
<relativeTargetDir>./${team}</relativeTargetDir>
</hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
</extensions>
</hudson.plugins.git.GitSCM>
<hudson.plugins.git.GitSCM plugin="git@4.3.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>https://git.bg.huohua.cn/h2asatp/base_framework.git</url>
<credentialsId>a670722b-96ec-449f-a2dc-6e6676bf8dbc</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/master</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<gitTool> git-1.8.3.1</gitTool>
<submoduleCfg class="list"/>
<extensions>
<hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
<relativeTargetDir>./base_framework</relativeTargetDir>
</hudson.plugins.git.extensions.impl.RelativeTargetDirectory>
</extensions>
</hudson.plugins.git.GitSCM>
</scms>
</scm>
<assignedNode>SparkATP</assignedNode>
<canRoam>false</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<command>python3 $WORKSPACE/../sparkatp-scripts/case_runner10.py -w $WORKSPACE -c $WORKSPACE/$test_case_path -t "$test_case_id" -i "$inculde" -e "$exculde" -r "$rerun" -env "$special_env" -team "$team" -b_id "$build_info_id" -b_url "$BUILD_URL" -is_db "$is_use_db"</command>
<configuredLocalRules/>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.plugins.robot.RobotPublisher plugin="robot@2.1.2">
<outputPath>$WORKSPACE/Report/ci_out/out</outputPath>
<reportFileName>report.html</reportFileName>
<logFileName>log.html</logFileName>
<outputFileName>output.xml</outputFileName>
<disableArchiveOutput>false</disableArchiveOutput>
<passThreshold>100.0</passThreshold>
<unstableThreshold>90.0</unstableThreshold>
<otherFiles>
<string/>
</otherFiles>
<enableCache>true</enableCache>
<onlyCritical>true</onlyCritical>
</hudson.plugins.robot.RobotPublisher>
</publishers>
<buildWrappers>
<hudson.plugins.ws__cleanup.PreBuildCleanup plugin="ws-cleanup@0.38">
<deleteDirs>false</deleteDirs>
<cleanupParameter/>
<externalDelete/>
<disableDeferredWipeout>false</disableDeferredWipeout>
</hudson.plugins.ws__cleanup.PreBuildCleanup>
<hudson.plugins.timestamper.TimestamperBuildWrapper plugin="timestamper@1.11.3"/>
</buildWrappers>
</project>

32
click_email_link.py Normal file
View File

@@ -0,0 +1,32 @@
from playwright.sync_api import sync_playwright
def click_email_link():
url = "https://switch4.best-envision.com/activity/form-review?userLanguage=jp&webKey=JyOJY1HaUWBQ00oooiNOEjTEwMDg3ODA3LCJ1c2VyTmFtZSI6InRveTFrbHNOeTExMSIsImVtYWlsIjoidG8xQGpveWh1Yi5uZXQiLCJwdXNoX2lkIjoxMDc4LCJwdXNoX3R5cGUiOjgsInByb2R1Y3RzIjoiXHU3MmVlXHU1YjUwXHU5OGRlXHU2NzNhXHU2NzZmIiwiYnJhbmRfdWlkIjoyMTA1LCJjaGFuIjoiaW0iLCJwcm9kdWN0X2lkIjo3fQO0O0OO0O0O"
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto(url)
# 等待页面加载完成
page.wait_for_load_state('networkidle')
# 查找并点击邮箱超链接
# 邮箱链接通常包含 mailto: 协议
email_links = page.query_selector_all('a[href^="mailto:"]')
if email_links:
print(f"找到 {len(email_links)} 个邮箱链接")
# 点击第一个邮箱链接
email_links[0].click()
print("已点击第一个邮箱链接")
else:
print("未找到邮箱链接")
# 等待一段时间以便查看结果
page.wait_for_timeout(3000)
browser.close()
if __name__ == "__main__":
click_email_link()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -1,159 +0,0 @@
import os
import re
from playwright.sync_api import Page, expect
from dulizhan.test_case.Resource.UI.base_page import BasePage
class DownloadAppPage(BasePage):
"""
Download the App 页面对象
已知侦察结果:
- 首页 URL: https://joyhub-website-frontend-test.best-envision.com/
- 页面标题: Joyhub | Explore Sexual Health, Wellness, and Connection
- 导航链接: Download the App
- Cookie 按钮文案: Accepet
"""
DOWNLOAD_APP_LINK_TEXT = re.compile(r"^Download the App$", re.I)
def accept_cookie_if_present(self):
# 页面侦察结果中按钮文本为 Accepet疑似拼写如此按真实页面处理
accept_button = self.page.get_by_role("button", name=re.compile(r"Accepet|Accept", re.I))
self.click_if_visible(accept_button, timeout=3000)
def open_download_app_page(self):
self.accept_cookie_if_present()
download_link = self.page.get_by_role("link", name=self.DOWNLOAD_APP_LINK_TEXT)
expect(download_link).to_be_visible(timeout=10000)
download_link.click()
try:
self.page.wait_for_load_state("networkidle", timeout=5000)
except Exception:
pass
def get_google_play_locator(self):
"""
优先使用稳定定位:
1. href 包含 play.google.com
2. 链接文本包含 Google Play
3. 图片 alt 包含 Google向上找父级 a 标签
如果后续页面提供 data-testid建议替换为
self.page.get_by_test_id("google-play-download")
"""
candidates = [
self.page.locator("a[href*='play.google.com']").first,
self.page.get_by_role("link", name=re.compile(r"Google\s*Play", re.I)).first,
self.page.locator("a").filter(has_text=re.compile(r"Google\s*Play", re.I)).first,
self.page.locator("img[alt*='Google' i]").locator("xpath=ancestor::a[1]").first,
]
for locator in candidates:
try:
if locator.count() > 0:
return locator
except Exception:
continue
return None
def discover_google_play_href(self):
"""
DOM 兜底侦察:
当 Google Play 是图片按钮、无文本链接时,用 JS 从渲染后的 DOM 中提取跳转地址。
只用于兜底发现,不作为首选点击方式。
"""
return self.page.evaluate(
"""
() => {
const keywords = ['google play', 'play.google.com'];
const nodes = Array.from(document.querySelectorAll('a, button, [role="button"], img'));
for (const node of nodes) {
const text = [
node.innerText,
node.textContent,
node.getAttribute('aria-label'),
node.getAttribute('title'),
node.getAttribute('alt'),
node.getAttribute('href'),
node.getAttribute('src')
].filter(Boolean).join(' ').toLowerCase();
if (keywords.some(k => text.includes(k))) {
const link = node.closest('a');
if (link && link.href) {
return link.href;
}
if (node.href) {
return node.href;
}
}
}
return null;
}
"""
)
def click_google_play_and_get_redirect_url(self) -> str:
"""
点击 Google Play 下载入口,并返回跳转地址。
成功判定:
- 新开页面 URL
- 当前页面跳转后的 URL
- 或点击前已获取到 Google Play href
"""
try:
self.page.wait_for_load_state("networkidle", timeout=5000)
except Exception:
pass
google_play_href = self.discover_google_play_href()
google_play_locator = self.get_google_play_locator()
if google_play_locator is None and not google_play_href:
raise AssertionError(
"未找到 Google Play 下载入口。"
"请检查 Download the App 页面是否存在 Google Play 链接,"
"或补充稳定 selector例如 data-testid。"
)
context = self.page.context
before_pages = list(context.pages)
old_url = self.page.url
if google_play_locator is not None:
google_play_locator.scroll_into_view_if_needed()
google_play_locator.click()
else:
# 没有稳定可点击 locator 时,使用已发现 href 直接跳转。
# TODO: 页面增加稳定 selector 后,替换为 locator.click()
self.page.goto(google_play_href, wait_until="domcontentloaded")
self.page.wait_for_timeout(3000)
after_pages = list(context.pages)
new_pages = [p for p in after_pages if p not in before_pages]
if new_pages:
new_page = new_pages[-1]
new_page.wait_for_load_state("domcontentloaded")
try:
new_page.wait_for_load_state("networkidle", timeout=10000)
except Exception:
pass
return new_page.url
if self.page.url != old_url:
return self.page.url
if google_play_href:
return google_play_href
raise AssertionError("点击 Google Play 后未获取到跳转地址")

View File

@@ -1,57 +0,0 @@
import os
from playwright.sync_api import Locator, Page, TimeoutError as PlaywrightTimeoutError
class BasePage:
"""Playwright 页面基类:封装通用等待、点击、截图等稳定操作。"""
def __init__(self, page: Page):
self.page = page
def goto(self, url: str, timeout: int = 60000) -> None:
self.page.goto(url, wait_until="domcontentloaded", timeout=timeout)
self.wait_for_network_idle()
def wait_for_network_idle(self, timeout: int = 30000) -> None:
try:
self.page.wait_for_load_state("networkidle", timeout=timeout)
except PlaywrightTimeoutError:
self.page.wait_for_load_state("domcontentloaded", timeout=timeout)
def wait_for_visible(self, locator: Locator, timeout: int = 10000) -> Locator:
locator.wait_for(state="visible", timeout=timeout)
return locator
def click_if_visible(self, locator: Locator, timeout: int = 3000) -> bool:
try:
if locator.first.is_visible(timeout=timeout):
locator.first.click()
self.wait_for_network_idle()
return True
except Exception:
return False
return False
def safe_click(self, locator: Locator, timeout: int = 10000) -> None:
self.wait_for_visible(locator, timeout=timeout)
locator.click()
self.wait_for_network_idle()
def screenshot(self, screenshot_dir: str, file_name: str, full_page: bool = True) -> str:
os.makedirs(screenshot_dir, exist_ok=True)
screenshot_path = os.path.join(screenshot_dir, file_name)
self.page.screenshot(path=screenshot_path, full_page=full_page)
return screenshot_path
def current_url(self) -> str:
return self.page.url
def title(self) -> str:
return self.page.title()
def visible_text_contains(self, keyword: str) -> bool:
try:
return self.page.get_by_text(keyword, exact=False).first.is_visible(timeout=3000)
except Exception:
return False

View File

@@ -1,196 +0,0 @@
import random
from typing import Dict, List
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, expect
from dulizhan.test_case.Resource.UI.base_page import BasePage
class BlogPage(BasePage):
"""
Joyhub Blog/内容页页面对象。
说明:
- 页面侦察结果未发现明确的 Blog 导航入口;
- 已发现真实导航候选包含 Discover、Following
- 因此进入 blog/内容列表时优先尝试 Blog失败后使用 Discover 作为内容入口兜底。
"""
HOME_URL = "https://joyhub-website-frontend-test.best-envision.com/"
EXPECTED_HOME_TITLE = "Joyhub | Explore Sexual Health, Wellness, and Connection"
NAV_DISCOVER_LINK = "Discover"
def open_home_page(self) -> None:
self.goto(self.HOME_URL)
self.accept_cookie_if_present()
expect(self.page).to_have_title(self.EXPECTED_HOME_TITLE)
def enter_blog_page(self) -> str:
"""
进入 blog/内容页。
优先级:
1. Blog 链接:如果页面后续版本新增 Blog 导航,可直接命中;
2. Discover 链接:侦察结果中存在,作为当前可用内容入口;
3. /blog 直达:仅作为最后兜底,并在无法点击入口时使用。
"""
before_url = self.page.url
blog_locators = [
self.page.get_by_role("link", name="Blog"),
self.page.locator("a", has_text="Blog"),
self.page.get_by_role("link", name=self.NAV_DISCOVER_LINK),
self.page.locator("a", has_text=self.NAV_DISCOVER_LINK),
]
for locator in blog_locators:
try:
if locator.count() > 0 and locator.first.is_visible():
locator.first.click()
self.wait_for_page_ready()
return self.page.url
except Exception:
continue
fallback_blog_url = self.HOME_URL.rstrip("/") + "/blog"
self.goto(fallback_blog_url)
assert self.page.url != before_url or "/blog" in self.page.url.lower(), (
"未能进入 blog/内容页:页面未发现 Blog 入口Discover 入口也不可点击。"
)
return self.page.url
def _get_candidate_blog_links(self) -> List[Dict[str, str]]:
"""
从渲染后的页面中提取可点击的 blog/content 候选链接。
不硬编码未知 selector优先根据 href 语义筛选:
blog/article/post/discover/detail/content。
若无命中,则退化为 main 区域内可见链接,排除导航类链接。
"""
return self.page.evaluate(
"""
() => {
const navTexts = new Set([
'Home',
'Download the App',
'Rewards',
'Support',
'About Us',
'Discover',
'Following',
'Partnerships',
'FAQs',
'Login'
]);
const normalize = value => (value || '').replace(/\\s+/g, ' ').trim();
const links = Array.from(document.querySelectorAll('a[href]'))
.filter(a => {
const rect = a.getBoundingClientRect();
const style = window.getComputedStyle(a);
return rect.width > 0
&& rect.height > 0
&& style.visibility !== 'hidden'
&& style.display !== 'none';
})
.map((a, index) => {
const href = a.href || '';
const text = normalize(a.innerText || a.textContent || a.getAttribute('aria-label') || '');
const imgAlt = normalize(
Array.from(a.querySelectorAll('img'))
.map(img => img.alt)
.filter(Boolean)
.join(' ')
);
return {
index,
href,
text,
imgAlt,
pathname: (() => {
try { return new URL(href).pathname.toLowerCase(); }
catch(e) { return ''; }
})()
};
})
.filter(item => item.href && !item.href.startsWith('javascript:'));
const semanticLinks = links.filter(item =>
/blog|article|post|discover|detail|content|story/i.test(item.href)
&& !navTexts.has(item.text)
);
if (semanticLinks.length > 0) {
return semanticLinks;
}
return links.filter(item =>
!navTexts.has(item.text)
&& item.href !== window.location.href
&& !/#$/.test(item.href)
);
}
"""
)
def click_random_blog(self) -> Dict[str, str]:
"""
随机点击一个 blog/content 候选项。
点击后等待 URL 或页面内容变化。
"""
self.wait_for_page_ready()
candidates = self._get_candidate_blog_links()
assert candidates, (
"未找到可点击的 blog/content 候选链接。"
"页面侦察结果缺少明确 blog 卡片 selector请补充稳定定位如 data-testid='blog-card'"
)
candidate = random.choice(candidates)
before_url = self.page.url
locator = self.page.locator("a[href]").nth(candidate["index"])
locator.scroll_into_view_if_needed(timeout=5000)
try:
with self.page.expect_navigation(wait_until="domcontentloaded", timeout=8000):
locator.click()
except PlaywrightTimeoutError:
locator.click()
self.wait_for_page_ready()
after_url = self.page.url
assert after_url != before_url or self.page.locator("main, article, body").first.is_visible(), (
f"点击 blog/content 候选项后页面未出现有效跳转或内容区域。候选链接:{candidate['href']}"
)
return {
"clicked_href": candidate.get("href", ""),
"clicked_text": candidate.get("text") or candidate.get("imgAlt") or "",
"before_url": before_url,
"after_url": after_url,
}
def assert_blog_content_page_loaded(self) -> None:
"""
断言已进入 blog/content 内容页。
因页面缺少稳定详情页 selector这里采用内容区域可见的稳健断言。
"""
self.wait_for_page_ready()
content_locator = self.page.locator("article, main, [role='main'], body").first
expect(content_locator).to_be_visible(timeout=10000)
body_text = self.page.locator("body").inner_text(timeout=10000).strip()
assert len(body_text) > 0, "blog/content 页面 body 内容为空,疑似跳转失败或页面未渲染完成。"
def capture_blog_content_screenshot(self, screenshot_dir: str, case_key: str) -> str:
return self.screenshot(
screenshot_dir=screenshot_dir,
file_name=f"{case_key}_blog_content.png",
full_page=True,
)

View File

@@ -1,200 +0,0 @@
import random
import re
from typing import Dict, List
from playwright.sync_api import Page, TimeoutError as PlaywrightTimeoutError
from dulizhan.test_case.Resource.UI.base_page import BasePage
class NewsPage(BasePage):
"""Joyhub News 相关页面对象。"""
COOKIE_ACCEPT_TEXT_PATTERN = re.compile(r"^(Accepet|Accept|I Accept|Agree|Got it)$", re.I)
NEWS_LINK_TEXT_PATTERN = re.compile(r"^News$", re.I)
NEWS_ROUTE = "/news"
def __init__(self, page: Page, base_url: str):
super().__init__(page)
self.base_url = base_url.rstrip("/")
def open_home(self) -> None:
self.goto(self.base_url)
def accept_cookie_if_present(self) -> None:
accept_button = self.page.get_by_role("button", name=self.COOKIE_ACCEPT_TEXT_PATTERN)
self.click_if_visible(accept_button, timeout=3000)
def enter_news_page(self) -> None:
news_link = self.page.get_by_role("link", name=self.NEWS_LINK_TEXT_PATTERN)
try:
if news_link.first.is_visible(timeout=5000):
news_link.first.click()
self.wait_for_network_idle()
return
except Exception:
pass
self.goto(f"{self.base_url}{self.NEWS_ROUTE}")
def is_news_context(self) -> bool:
url = self.page.url.lower()
title = self.page.title().lower()
if "news" in url:
return True
if "news" in title:
return True
if self.visible_text_contains("News"):
return True
return False
def _collect_clickable_news_candidates(self) -> List[Dict[str, str]]:
script = """
() => {
const navTexts = new Set([
'home',
'download the app',
'rewards',
'support',
'about us',
'discover',
'following',
'partnerships',
'faqs',
'login'
]);
const currentUrl = new URL(window.location.href);
const anchors = Array.from(document.querySelectorAll('a[href]'));
function isVisible(el) {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style &&
style.visibility !== 'hidden' &&
style.display !== 'none' &&
rect.width > 0 &&
rect.height > 0;
}
function hasContentContainer(el) {
return !!el.closest(
'article, [class*="article" i], [class*="card" i], [class*="news" i], [class*="post" i], [class*="blog" i], [class*="item" i]'
);
}
function isBadProtocol(url) {
return ['javascript:', 'mailto:', 'tel:'].includes(url.protocol);
}
function isLikelySocial(url) {
const host = url.hostname.toLowerCase();
return [
'facebook.com',
'instagram.com',
'twitter.com',
'x.com',
'youtube.com',
'tiktok.com',
'linkedin.com'
].some(domain => host.includes(domain));
}
const candidates = [];
for (const a of anchors) {
if (!isVisible(a)) continue;
let url;
try {
url = new URL(a.href, window.location.origin);
} catch (e) {
continue;
}
if (isBadProtocol(url)) continue;
if (isLikelySocial(url)) continue;
if (url.href === currentUrl.href) continue;
const text = (a.innerText || a.textContent || '').trim().replace(/\\s+/g, ' ');
const textLower = text.toLowerCase();
const pathLower = url.pathname.toLowerCase();
if (navTexts.has(textLower)) continue;
if (url.pathname === '/' || url.pathname === '') continue;
const hrefLooksLikeContent =
/news|article|blog|post|detail|story/i.test(pathLower) &&
pathLower !== '/news';
const containerLooksLikeContent = hasContentContainer(a);
if (hrefLooksLikeContent || containerLooksLikeContent) {
candidates.push({
href: url.href,
text: text,
path: url.pathname,
reason: hrefLooksLikeContent ? 'href_content_pattern' : 'content_container'
});
}
}
const seen = new Set();
return candidates.filter(item => {
if (seen.has(item.href)) return false;
seen.add(item.href);
return true;
});
}
"""
return self.page.evaluate(script)
def click_random_news_item(self) -> Dict[str, str]:
self.wait_for_network_idle()
candidates = self._collect_clickable_news_candidates()
if not candidates:
raise AssertionError(
"未发现可点击的 news 内容候选。"
"请检查 News 页面是否加载成功,或为 news 卡片补充稳定 selector/data-testid。"
)
selected = random.choice(candidates)
href = selected["href"]
old_url = self.page.url
click_script = """
(targetHref) => {
const anchors = Array.from(document.querySelectorAll('a[href]'));
const target = anchors.find(a => {
try {
return new URL(a.href, window.location.origin).href === targetHref;
} catch (e) {
return false;
}
});
if (!target) {
throw new Error('Target news link not found: ' + targetHref);
}
target.scrollIntoView({block: 'center', inline: 'center'});
target.click();
}
"""
self.page.evaluate(click_script, href)
try:
self.page.wait_for_url(lambda url: str(url) != old_url, timeout=15000)
except PlaywrightTimeoutError:
pass
self.wait_for_network_idle()
return selected
def screenshot_news_content(self, screenshot_dir: str, file_name: str) -> str:
return self.screenshot(screenshot_dir=screenshot_dir, file_name=file_name, full_page=True)

View File

@@ -1,91 +0,0 @@
from dulizhan.test_case.Resource.UI.base_page import BasePage
from dulizhan.test_case.Resource.UI.news_page import NewsPage
import os
import allure
import pytest
from playwright.sync_api import sync_playwright
from dulizhan.test_case.Resource.UI.news_page import NewsPage
CASE_INFO = {
"projectId": 1001,
"caseId": 2001,
"automationType": "ui",
"caseKey": "TC_dulizhan_ui_api_verify_001",
"moduleName": "news",
"productName": "",
"projectName": "dulizhan",
"caseName": "进入news页面随机点击news跳转到news内容后截图就认为用例执行成功",
"pageUrl": "https://joyhub-website-frontend-test.best-envision.com/",
"screenshotDir": r"C:\Users\a\smart-management-auto-test\dulizhan\screenshots",
}
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture()
def page(browser):
context = browser.new_context(
viewport={"width": 1440, "height": 900},
ignore_https_errors=True,
)
page = context.new_page()
yield page
context.close()
@allure.feature(CASE_INFO["projectName"])
@allure.story(CASE_INFO["moduleName"])
@allure.title(CASE_INFO["caseName"])
def test_random_click_news_and_capture_content(page):
screenshot_dir = CASE_INFO["screenshotDir"]
os.makedirs(screenshot_dir, exist_ok=True)
news_page = NewsPage(page)
with allure.step("打开被测页面"):
news_page.open_home_page()
news_page.accept_cookie_if_present()
allure.attach(
news_page.current_url(),
name="首页 URL",
attachment_type=allure.attachment_type.TEXT,
)
with allure.step("进入 news 页面"):
news_page.enter_news_page()
allure.attach(
news_page.current_url(),
name="News 页面 URL",
attachment_type=allure.attachment_type.TEXT,
)
with allure.step("随机点击 news"):
selected_news = news_page.click_random_news()
allure.attach(
str(selected_news),
name="随机点击的 news 候选信息",
attachment_type=allure.attachment_type.TEXT,
)
with allure.step("截图跳转的 news 内容"):
news_page.assert_news_content_loaded()
screenshot_path = news_page.capture_news_content_screenshot(
screenshot_dir=screenshot_dir,
case_key=CASE_INFO["caseKey"],
)
allure.attach.file(
screenshot_path,
name="跳转后的 news 内容截图",
attachment_type=allure.attachment_type.PNG,
)
assert os.path.exists(screenshot_path), f"news 内容截图未生成: {screenshot_path}"

View File

@@ -1,80 +0,0 @@
from dulizhan.test_case.Resource.UI.base_page import BasePage
from dulizhan.test_case.Resource.UI.blog_page import BlogPage
from types import SimpleNamespace
import allure
import pytest
from playwright.sync_api import sync_playwright
from dulizhan.test_case.Resource.UI.blog_page import BlogPage
case_info = SimpleNamespace(
projectId=1001,
caseId=2001,
automationType="ui",
caseKey="TC_dulizhan_ui_api_verify_001",
moduleName="blog",
productName="",
projectName="dulizhan",
caseName="进入blog页面随机点击blog跳转到blog内容后截图就认为用例执行成功",
screenshotDir=r"C:\Users\a\smart-management-auto-test\dulizhan\screenshots",
pageUrl="https://joyhub-website-frontend-test.best-envision.com/",
)
@pytest.fixture(scope="function")
def page():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1440, "height": 900},
ignore_https_errors=True,
)
page = context.new_page()
yield page
context.close()
browser.close()
@allure.feature(case_info.projectName)
@allure.story(case_info.moduleName)
@allure.title(case_info.caseName)
def test_enter_blog_random_click_and_capture_content(page):
blog_page = BlogPage(page)
with allure.step("打开被测页面"):
blog_page.open_home_page()
assert page.url.startswith(case_info.pageUrl)
with allure.step("进入blog页面"):
entered_url = blog_page.enter_blog_page()
allure.attach(
entered_url,
name="进入blog页面后的URL",
attachment_type=allure.attachment_type.TEXT,
)
with allure.step("随机点击blog"):
click_result = blog_page.click_random_blog()
allure.attach(
str(click_result),
name="随机点击blog结果",
attachment_type=allure.attachment_type.TEXT,
)
with allure.step("截图跳转的blog内容"):
blog_page.assert_blog_content_page_loaded()
screenshot_path = blog_page.capture_blog_content_screenshot(
screenshot_dir=case_info.screenshotDir,
case_key=case_info.caseKey,
)
with open(screenshot_path, "rb") as image_file:
allure.attach(
image_file.read(),
name="blog内容页截图",
attachment_type=allure.attachment_type.PNG,
)
assert screenshot_path, "blog 内容页截图路径为空"

View File

@@ -1,90 +0,0 @@
from dulizhan.test_case.Resource.UI.base_page import BasePage
from dulizhan.test_case.Resource.UI.news_page import NewsPage
from types import SimpleNamespace
import allure
import pytest
from playwright.sync_api import sync_playwright
from dulizhan.test_case.Resource.UI.news_page import NewsPage
CASE_INFO = SimpleNamespace(
projectId=1001,
caseId=2001,
automationType="ui",
caseKey="TC_dulizhan_ui_api_verify_001",
moduleName="news",
productName="",
projectName="dulizhan",
caseName="进入news页面随机点击news跳转到news内容后截图就认为用例执行成功",
pageUrl="https://joyhub-website-frontend-test.best-envision.com/",
screenshotDir=r"C:\Users\a\smart-management-auto-test\dulizhan\screenshots",
)
@pytest.fixture(scope="session")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
@pytest.fixture()
def page(browser):
page = browser.new_page(
viewport={"width": 1440, "height": 1200},
ignore_https_errors=True,
)
yield page
page.close()
@pytest.fixture()
def case_info():
return CASE_INFO
@allure.feature(CASE_INFO.projectName)
@allure.story(CASE_INFO.moduleName)
@allure.title(CASE_INFO.caseName)
@pytest.mark.ui
def test_enter_news_random_click_and_screenshot(page, case_info):
news_page = NewsPage(page, case_info.pageUrl)
with allure.step("打开被测页面"):
news_page.open_home()
news_page.accept_cookie_if_present()
assert "Joyhub" in news_page.title(), f"首页标题不符合预期,当前标题:{news_page.title()}"
with allure.step("进入news页面"):
news_page.enter_news_page()
assert news_page.is_news_context(), (
f"未确认进入 news 页面上下文当前URL{page.url},当前标题:{page.title()}"
"侦察结果未提供明确 News 导航 selector如实际路由不是 /news请调整 NewsPage.NEWS_ROUTE。"
)
with allure.step("随机点击news"):
before_click_url = page.url
selected_news = news_page.click_random_news_item()
allure.attach(
str(selected_news),
name="随机点击的 news 候选",
attachment_type=allure.attachment_type.TEXT,
)
assert page.url != before_click_url or selected_news.get("href"), (
f"点击 news 后页面未发生有效跳转点击前URL{before_click_url}点击后URL{page.url}"
)
with allure.step("截图跳转的news内容"):
screenshot_path = news_page.screenshot_news_content(
screenshot_dir=case_info.screenshotDir,
file_name=f"{case_info.caseKey}_news_content.png",
)
allure.attach.file(
screenshot_path,
name="跳转后的news内容截图",
attachment_type=allure.attachment_type.PNG,
)
assert screenshot_path, "news 内容截图保存失败"

View File

@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
import allure
import logging
from zhyy.library.BusinessKw.SZPurchase.PurchaseOrderManage import PurchaseOrder
@allure.feature('深圳采购工作台采购订单页面')
class Test_purchase_order(object):
test_case = PurchaseOrder()
def teardown_method(self):
logging.info("-----------------------------End-------------------------------")
@allure.story("验证采购工作台采购订单页面列表查询")
def test_check_purchase_order_page(self):
purchase_order_code = 'PO251209048' # 采购单号 必填
supplier_company_ids = ['334'] # 供应商id 非必填
payment_status = '0' # 付款状态 非必填
status = '0' # 采购单状态 非必填
page_no = 1 # 页码 必填
page_size = 10 # 每页条数 必填
response_data = self.test_case.kw_zhyy_get_purchase_page_post(
note="采购工作台采购订单页面列表查询",
user='purchase',
order_sn=purchase_order_code,
supplier_company_ids=supplier_company_ids,
payment_status=payment_status,
status=status,
page_no=page_no,
page_size=page_size
)
# 断言检查
assert response_data is not None, "响应数据不能为空"
assert 'code' in response_data, "响应数据中缺少code字段"
assert response_data['code'] == 0, "接口调用失败code: {}, msg: {}".format(
response_data.get('code'), response_data.get('msg', '未知错误'))
assert 'data' in response_data, "响应数据中缺少data字段"
assert response_data['data'] is not None, "响应数据中的data字段不能为空"
# 如果传入了采购单号,检查返回的数据中是否包含该采购单号
if purchase_order_code:
data = response_data.get('data', {})
order_found = False
# 检查返回的数据结构,可能包含列表或其他结构
if isinstance(data, dict):
# 如果data是字典可能包含records、list、data等字段
records = data.get('records') or data.get('list') or data.get('data') or []
if isinstance(records, list) and len(records) > 0:
# 检查列表中是否包含指定的采购单号
for item in records:
if isinstance(item, dict):
order_sn = item.get('order_sn') or item.get('orderSn') or item.get('orderSn')
if order_sn == purchase_order_code:
order_found = True
break
elif isinstance(data, list):
# 如果data本身就是列表
for item in data:
if isinstance(item, dict):
order_sn = item.get('order_sn') or item.get('orderSn') or item.get('orderSn')
if order_sn == purchase_order_code:
order_found = True
break
if order_found:
logging.info("✓ 断言通过:返回的数据中包含采购单号 {}".format(purchase_order_code))
else:
logging.warning("⚠ 警告:返回的数据中未找到采购单号: {},但接口调用成功".format(purchase_order_code))
logging.info("✓ 所有断言检查通过")
print("✓ 查询成功,响应数据: {}".format(response_data))

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
import allure
import logging
from zhyy.library.BusinessKw.SZPurchase.index import PurchaseIndex
@allure.feature('深圳采购工作台首页')
class Test_purchase_index(object):
# config = ReadConfig.ReadConfig() # 调用读取配置文件的方法类
test_case = PurchaseIndex()
def teardown_method(self):
logging.info("-----------------------------End-------------------------------")
@allure.story("验证采购工作台待办任务与在办任务功能")
def test_check_todo(self):
get_purchase_data = self.test_case.kw_zhyy_get_todo(note="采购工作台首页待办任务PO与在办任务PO", user='purchase')

View File

@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
from joyhub_backend.library.functional_case_converter import (
ApiContextItem,
ApiMatcher,
FunctionalCaseInput,
FunctionalCaseParser,
FunctionalCaseToApiAutomationService,
FunctionalStep,
LLMApiCaseConverter,
convert_functional_case,
)

View File

@@ -1,148 +0,0 @@
# -*- coding: utf-8 -*-
import json
import logging
import os
import allure
import requests
CAPTCHA_URL = os.getenv(
"JOYHUB_CAPTCHA_URL",
"http://test-manager-api.best-envision.com/admin/login/captcha",
)
LOGIN_BASE_URL = os.getenv(
"JOYHUB_LOGIN_BASE_URL",
"http://test-manager-api.best-envision.com",
)
LOGIN_PATH = os.getenv("JOYHUB_LOGIN_PATH", "/admin/login/login")
USERNAME = os.getenv("JOYHUB_USERNAME", "guojiabao")
PASSWORD = os.getenv("JOYHUB_PASSWORD", "gjb123456")
CAPTCHA = os.getenv("JOYHUB_CAPTCHA", "1111")
TIMEOUT = int(os.getenv("JOYHUB_TIMEOUT", "20"))
TENANT_ID = os.getenv("JOYHUB_TENANT_ID", "126")
class JoyhubAuth(object):
def __init__(self):
self.session = requests.Session()
self._token = None
@property
def login_url(self):
return LOGIN_BASE_URL.rstrip("/") + LOGIN_PATH
def get_captcha_key(self):
with allure.step("前置:获取登录验证码 key"):
logging.info("GET %s", CAPTCHA_URL)
response = self.session.get(CAPTCHA_URL, timeout=TIMEOUT)
self._attach_response("captcha", response)
response.raise_for_status()
data = response.json()
key = self._extract_key(data)
assert key, "验证码接口未返回 key响应{}".format(data)
return key
def login(self):
if self._token:
return self._token
key = self.get_captcha_key()
body = {
"key": key,
"username": USERNAME,
"password": PASSWORD,
"captcha": CAPTCHA,
}
headers = {"Content-Type": "application/json", "tenant-id": TENANT_ID}
with allure.step("前置:登录并获取 token"):
logging.info("POST %s", self.login_url)
logging.info("request headers: %s", headers)
logging.info("request body: %s", self._safe_body(body))
response = self.session.post(self.login_url, json=body, headers=headers, timeout=TIMEOUT)
self._attach_request(self.login_url, "POST", headers, self._safe_body(body))
self._attach_response("login", response)
response.raise_for_status()
data = response.json()
token = self._extract_token(data)
assert token, "登录接口未返回 token响应{}".format(data)
self._token = token if token.startswith("Bearer ") else "Bearer " + token
return self._token
def auth_headers(self):
return {
"Authorization": self.login(),
"Content-Type": "application/json",
"tenant-id": TENANT_ID,
}
@staticmethod
def _extract_key(data):
if isinstance(data, dict):
for field in ("key", "captchaKey", "captcha_key", "uuid"):
if data.get(field):
return data.get(field)
nested = data.get("data")
if isinstance(nested, dict):
for field in ("key", "captchaKey", "captcha_key", "uuid"):
if nested.get(field):
return nested.get(field)
if isinstance(nested, str):
return nested
return None
@staticmethod
def _extract_token(data):
token_fields = (
"token",
"access_token",
"accessToken",
"Authorization",
"authorization",
"userToken",
"user_token",
"jwt",
)
if isinstance(data, dict):
for field in token_fields:
token = data.get(field)
if JoyhubAuth._looks_like_token(token):
return token
nested = data.get("data")
if isinstance(nested, dict):
for field in token_fields:
token = nested.get(field)
if JoyhubAuth._looks_like_token(token):
return token
return None
@staticmethod
def _looks_like_token(value):
if not isinstance(value, str):
return False
value = value.strip()
if not value:
return False
if any(ord(char) > 127 for char in value):
return False
return len(value) >= 16 or value.startswith("Bearer ")
@staticmethod
def _safe_body(body):
safe = dict(body)
if "password" in safe:
safe["password"] = "******"
return safe
@staticmethod
def _attach_request(url, method, headers, body):
allure.attach(str(url), "请求url", allure.attachment_type.TEXT)
allure.attach(str(method), "请求方式", allure.attachment_type.TEXT)
allure.attach(json.dumps(headers, ensure_ascii=False, indent=2), "请求头", allure.attachment_type.JSON)
allure.attach(json.dumps(body, ensure_ascii=False, indent=2), "请求体", allure.attachment_type.JSON)
@staticmethod
def _attach_response(name, response):
logging.info("%s response status: %s", name, response.status_code)
logging.info("%s response body: %s", name, response.text)
allure.attach(str(response.status_code), "{} 响应状态码".format(name), allure.attachment_type.TEXT)
allure.attach(response.text, "{} 响应体".format(name), allure.attachment_type.JSON)

View File

@@ -1,62 +0,0 @@
# -*- coding: utf-8 -*-
import logging
from joyhub_backend.library.joyhub_interface import JoyhubInterface
class JoyhubBusiness(JoyhubInterface):
def get_video_label_list(self):
logging.info("==========获取视频标签列表==========")
body = {
"page": 1,
"limit": 10,
"sort": "id",
"label_name": "",
"category_id": 0,
"video_type": 0,
"video_num": 0,
"created_at": [],
"order": "descending",
}
return self.request("获取视频标签列表", "POST", "admin/video/getVideoLabelList", body=body)
def get_video_category_list(self):
logging.info("==========获取视频分类列表==========")
body = {
"page": 1,
"limit": 10,
"sort": "id",
"category_name": "",
"order": "descending",
}
return self.request("获取视频分类列表", "POST", "admin/video/getVideoCategoryList", body=body)
def get_exchange_record_list(self):
logging.info("==========兑换记录列表==========")
body = {
"page": 1,
"limit": 10,
"status": "",
"goods_type": "",
"created_at": [],
}
return self.request("兑换记录列表", "POST", "admin/exchange/recordList", body=body)
def get_app_feedback_list(self):
logging.info("==========app反馈意见列表==========")
body = {
"page": 1,
"limit": 10,
"status": "",
"keyword": "",
}
return self.request("app反馈意见列表", "POST", "admin/feedback/list", body=body)
def get_sensitive_word_list(self):
logging.info("==========敏感词列表==========")
body = {
"page": 1,
"limit": 10,
"keyword": "",
}
return self.request("敏感词列表", "POST", "admin/sensitiveWord/list", body=body)

View File

@@ -1,917 +0,0 @@
# -*- coding: utf-8 -*-
import argparse
import copy
import json
import os
import re
from dataclasses import asdict, dataclass, field
from difflib import SequenceMatcher
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
COMMON_CN_KEYWORDS = [
"登录",
"查询",
"搜索",
"筛选",
"列表",
"详情",
"新增",
"创建",
"添加",
"修改",
"编辑",
"更新",
"删除",
"导出",
"导入",
"上传",
"下载",
"提交",
"审核",
"确认",
"保存",
"分页",
"分页查询",
"性能",
"响应时间",
"超时",
"返回",
"结果",
"成功",
"失败",
"校验",
"重置",
"启用",
"停用",
"开关",
"状态",
"排序",
"标签",
"分类",
"关键字",
"关键字搜索",
"模糊搜索",
]
METHOD_INTENT_KEYWORDS = {
"GET": ["查询", "搜索", "筛选", "列表", "详情", "获取", "查看", "分页", "返回"],
"POST": ["新增", "创建", "添加", "登录", "提交", "导入", "上传", "确认", "审核", "保存"],
"PUT": ["修改", "编辑", "更新", "重置"],
"PATCH": ["修改", "编辑", "更新", "状态", "启用", "停用", "开关"],
"DELETE": ["删除", "移除"],
}
DEFAULT_PLACEHOLDER_URL = "/api/need-confirm"
DEFAULT_PAGE_SIZE = 10
DEFAULT_RESPONSE_TIME_MS = 1000
@dataclass
class FunctionalStep:
stepNo: int
action: str
expectedResult: str = ""
@dataclass
class ApiContextItem:
apiName: str = ""
method: str = ""
url: str = ""
description: str = ""
headers: Dict[str, Any] = field(default_factory=dict)
queryParams: Dict[str, Any] = field(default_factory=dict)
requestBody: Any = None
responseExample: Any = None
tags: List[str] = field(default_factory=list)
@dataclass
class FunctionalCaseInput:
caseId: str = ""
caseKey: str = ""
caseName: str = ""
projectName: str = ""
moduleName: str = ""
priority: str = ""
preconditions: str = ""
steps: List[FunctionalStep] = field(default_factory=list)
expectedResults: List[str] = field(default_factory=list)
apiContext: Optional[List[ApiContextItem]] = None
generateOptions: Dict[str, Any] = field(default_factory=dict)
extra: Dict[str, Any] = field(default_factory=dict)
class FunctionalCaseParser:
@classmethod
def parse(cls, payload: Union[str, Dict[str, Any], FunctionalCaseInput]) -> FunctionalCaseInput:
if isinstance(payload, FunctionalCaseInput):
return payload
if isinstance(payload, str):
try:
payload = json.loads(payload)
except Exception:
payload = {
"caseName": "手工输入功能用例",
"steps": [payload],
"expectedResults": [],
}
if not isinstance(payload, dict):
raise TypeError("functional case payload must be dict, json string or FunctionalCaseInput")
steps = cls._normalize_steps(payload.get("steps") or payload.get("step") or [])
expected_results = cls._normalize_text_list(
payload.get("expectedResults")
or payload.get("expectedResult")
or payload.get("expectations")
or []
)
api_context = cls._normalize_api_context(payload.get("apiContext") or payload.get("apis") or payload.get("apiInfo"))
generate_options = payload.get("generateOptions") or payload.get("options") or {}
known_keys = {
"caseId",
"caseKey",
"caseName",
"projectName",
"moduleName",
"priority",
"preconditions",
"steps",
"step",
"expectedResults",
"expectedResult",
"expectations",
"apiContext",
"apis",
"apiInfo",
"generateOptions",
"options",
}
extra = {key: value for key, value in payload.items() if key not in known_keys}
return FunctionalCaseInput(
caseId=str(payload.get("caseId", "") or ""),
caseKey=str(payload.get("caseKey", "") or ""),
caseName=str(payload.get("caseName", "") or payload.get("name", "") or ""),
projectName=str(payload.get("projectName", "") or ""),
moduleName=str(payload.get("moduleName", "") or ""),
priority=str(payload.get("priority", "") or ""),
preconditions=str(payload.get("preconditions", "") or payload.get("precondition", "") or ""),
steps=steps,
expectedResults=expected_results,
apiContext=api_context,
generateOptions=generate_options if isinstance(generate_options, dict) else {},
extra=extra,
)
@staticmethod
def _normalize_steps(value: Any) -> List[FunctionalStep]:
if not value:
return []
if isinstance(value, str):
raw_items = [item.strip() for item in re.split(r"[\n\r]+", value) if item.strip()]
if len(raw_items) <= 1:
raw_items = [item.strip() for item in re.split(r"(?<=[。;;])", value) if item.strip()]
items = raw_items
elif isinstance(value, list):
items = value
else:
items = [value]
normalized: List[FunctionalStep] = []
for index, item in enumerate(items, start=1):
if isinstance(item, dict):
step_no = int(item.get("stepNo") or item.get("step_no") or item.get("no") or index)
action = str(item.get("action") or item.get("step") or item.get("content") or item.get("description") or "")
expected_result = str(item.get("expectedResult") or item.get("expected") or "")
else:
raw_text = str(item).strip()
step_no = index
action = re.sub(r"^\s*\d+[\.、\)]\s*", "", raw_text)
expected_result = ""
if action:
normalized.append(FunctionalStep(stepNo=step_no, action=action, expectedResult=expected_result))
return normalized
@staticmethod
def _normalize_text_list(value: Any) -> List[str]:
if not value:
return []
if isinstance(value, str):
parts = [item.strip() for item in re.split(r"[\n\r]+", value) if item.strip()]
if len(parts) <= 1:
parts = [item.strip() for item in re.split(r"[。;;]\s*", value) if item.strip()]
return [item for item in parts if item]
if isinstance(value, list):
result = []
for item in value:
if isinstance(item, dict):
text = str(item.get("text") or item.get("content") or item.get("expected") or "").strip()
else:
text = str(item).strip()
if text:
result.append(text)
return result
return [str(value).strip()] if str(value).strip() else []
@staticmethod
def _normalize_api_context(value: Any) -> Optional[List[ApiContextItem]]:
if not value:
return None
if isinstance(value, dict):
if "apis" in value and isinstance(value["apis"], list):
value = value["apis"]
elif "items" in value and isinstance(value["items"], list):
value = value["items"]
else:
value = [value]
if isinstance(value, str):
value = value.strip()
if not value:
return None
try:
parsed = json.loads(value)
return FunctionalCaseParser._normalize_api_context(parsed)
except Exception:
return FunctionalCaseParser._parse_markdown_api_context(value)
if not isinstance(value, list):
return None
apis: List[ApiContextItem] = []
for item in value:
if isinstance(item, ApiContextItem):
apis.append(item)
continue
if not isinstance(item, dict):
continue
headers = item.get("headers") or item.get("header") or {}
query_params = item.get("queryParams") or item.get("query") or item.get("params") or {}
tags = item.get("tags") or []
if isinstance(tags, str):
tags = [tag.strip() for tag in re.split(r"[\s,/|]+", tags) if tag.strip()]
apis.append(
ApiContextItem(
apiName=str(item.get("apiName") or item.get("name") or ""),
method=str(item.get("method") or item.get("httpMethod") or "").upper(),
url=str(item.get("url") or item.get("path") or ""),
description=str(item.get("description") or item.get("desc") or ""),
headers=headers if isinstance(headers, dict) else {},
queryParams=query_params if isinstance(query_params, dict) else {},
requestBody=item.get("requestBody") if "requestBody" in item else item.get("body"),
responseExample=item.get("responseExample") or item.get("response") or item.get("example"),
tags=tags if isinstance(tags, list) else [],
)
)
return apis or None
@staticmethod
def _parse_markdown_api_context(text: str) -> Optional[List[ApiContextItem]]:
lines = text.splitlines()
apis: List[ApiContextItem] = []
current: Dict[str, Any] = {}
in_body = False
body_lines: List[str] = []
def flush_current():
nonlocal current, body_lines, in_body
if not current:
return
body_text = "\n".join(body_lines).strip()
request_body = FunctionalCaseParser._safe_json_load(body_text)
apis.append(
ApiContextItem(
apiName=current.get("apiName", ""),
method=current.get("method", ""),
url=current.get("url", ""),
description=current.get("description", ""),
headers=current.get("headers", {}),
queryParams=current.get("queryParams", {}),
requestBody=request_body if request_body is not None else body_text,
responseExample=current.get("responseExample"),
tags=current.get("tags", []),
)
)
current = {}
body_lines = []
in_body = False
for line in lines:
stripped = line.strip()
if stripped.startswith("## "):
flush_current()
current["apiName"] = stripped[3:].strip()
continue
if stripped == "**接口URL**":
continue
if stripped.startswith("> ") and not current.get("url"):
value = stripped[2:].strip()
if value and value != "暂无参数":
current["url"] = value
continue
if stripped == "**请求方式**":
continue
if stripped == "**请求Body参数**":
in_body = True
body_lines = []
continue
if stripped == "**响应示例**":
in_body = False
continue
if in_body:
if stripped.startswith("```"):
continue
body_lines.append(line)
flush_current()
return apis or None
@staticmethod
def _safe_json_load(text: str) -> Any:
if not text:
return None
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = re.sub(r"^```[a-zA-Z0-9_-]*\s*", "", cleaned)
cleaned = re.sub(r"\s*```$", "", cleaned)
try:
return json.loads(cleaned)
except Exception:
return None
class ApiMatcher:
def match(self, functional_case: FunctionalCaseInput, candidates: List[ApiContextItem], top_k: int = 3) -> List[Dict[str, Any]]:
matched: List[Dict[str, Any]] = []
keywords = self.extract_keywords(functional_case)
intent = self.detect_intent(functional_case)
for api in candidates:
score = self._score_api(api, keywords, intent, functional_case)
if score <= 0:
continue
matched.append(
{
"score": round(score, 4),
"api": api,
"reason": self._build_reason(api, keywords, intent),
}
)
matched.sort(key=lambda item: item["score"], reverse=True)
return matched[:top_k]
def extract_keywords(self, functional_case: FunctionalCaseInput) -> List[str]:
texts = [functional_case.caseName, functional_case.moduleName, functional_case.preconditions]
texts.extend(step.action for step in functional_case.steps)
texts.extend(step.expectedResult for step in functional_case.steps if step.expectedResult)
texts.extend(functional_case.expectedResults)
texts.extend(str(value) for value in functional_case.extra.values() if value is not None)
full_text = " \n ".join(texts)
keywords: List[str] = []
for keyword in COMMON_CN_KEYWORDS:
if keyword in full_text and keyword not in keywords:
keywords.append(keyword)
english_tokens = re.findall(r"[A-Za-z_][A-Za-z0-9_]{1,}", full_text)
for token in english_tokens:
if token not in keywords:
keywords.append(token)
generic_tokens = re.findall(r"[\u4e00-\u9fa5]{2,}", full_text)
for token in generic_tokens:
if len(token) <= 2:
if token not in keywords:
keywords.append(token)
continue
if token not in keywords:
keywords.append(token)
return self._dedupe(keywords)
def detect_intent(self, functional_case: FunctionalCaseInput) -> str:
text = self._all_text(functional_case)
for method, keywords in METHOD_INTENT_KEYWORDS.items():
if any(keyword in text for keyword in keywords):
return method
return "GET"
def _score_api(self, api: ApiContextItem, keywords: List[str], intent: str, functional_case: FunctionalCaseInput) -> float:
score = 0.0
api_text = " ".join(
[
api.apiName or "",
api.method or "",
api.url or "",
api.description or "",
" ".join(api.tags or []),
" ".join(api.headers.keys()),
" ".join(api.queryParams.keys()),
]
)
if api.method and api.method.upper() == intent:
score += 1.2
elif api.method and api.method.upper() in ("GET", "POST", "PUT", "PATCH", "DELETE"):
score += 0.2
for keyword in keywords:
if keyword and keyword in api_text:
score += 0.8
for token in self._url_tokens(api.url):
if token and any(keyword in token or token in keyword for keyword in keywords):
score += 0.6
if functional_case.moduleName and functional_case.moduleName in api_text:
score += 0.8
if functional_case.caseName and functional_case.caseName in api_text:
score += 0.5
ratio = SequenceMatcher(None, self._all_text(functional_case), api_text).ratio()
score += ratio * 0.8
return score
@staticmethod
def _build_reason(api: ApiContextItem, keywords: List[str], intent: str) -> str:
matched_keywords = [keyword for keyword in keywords if keyword and keyword in (api.apiName + " " + api.description + " " + api.url)]
parts = []
if api.method and api.method.upper() == intent:
parts.append("方法匹配")
if matched_keywords:
parts.append("关键词命中:{}".format("".join(matched_keywords[:5])))
return "; ".join(parts) or "模糊匹配"
@staticmethod
def _url_tokens(url: str) -> List[str]:
if not url:
return []
return [token for token in re.split(r"[/?&=_\-\.]+", url) if token]
@staticmethod
def _dedupe(items: Iterable[str]) -> List[str]:
seen = set()
result = []
for item in items:
if item and item not in seen:
seen.add(item)
result.append(item)
return result
@staticmethod
def _all_text(functional_case: FunctionalCaseInput) -> str:
texts = [functional_case.caseName, functional_case.moduleName, functional_case.preconditions]
texts.extend(step.action for step in functional_case.steps)
texts.extend(step.expectedResult for step in functional_case.steps if step.expectedResult)
texts.extend(functional_case.expectedResults)
return " \n ".join(filter(None, texts))
class LLMApiCaseConverter:
def convert(self, prompt: str, llm_client: Any = None) -> Optional[Dict[str, Any]]:
if llm_client is None:
return None
raw = self._invoke_client(llm_client, prompt)
if raw is None:
return None
if isinstance(raw, dict):
return raw
if hasattr(raw, "content"):
raw = raw.content
elif hasattr(raw, "text"):
raw = raw.text
elif hasattr(raw, "data"):
raw = raw.data
if isinstance(raw, dict):
return raw
if not isinstance(raw, str):
raw = str(raw)
return self._extract_json(raw)
@staticmethod
def _invoke_client(llm_client: Any, prompt: str) -> Any:
if callable(llm_client):
try:
return llm_client(prompt=prompt)
except TypeError:
try:
return llm_client(prompt)
except TypeError:
return llm_client({"prompt": prompt})
if hasattr(llm_client, "generate") and callable(llm_client.generate):
return llm_client.generate(prompt)
if hasattr(llm_client, "chat") and callable(llm_client.chat):
return llm_client.chat(prompt)
raise TypeError("llm_client must be callable or expose generate/chat")
@staticmethod
def _extract_json(text: str) -> Optional[Dict[str, Any]]:
if not text:
return None
cleaned = text.strip()
match = re.search(r"```json\s*(\{.*?\})\s*```", cleaned, flags=re.S)
if match:
cleaned = match.group(1)
else:
first = cleaned.find("{")
last = cleaned.rfind("}")
if first >= 0 and last > first:
cleaned = cleaned[first : last + 1]
try:
parsed = json.loads(cleaned)
return parsed if isinstance(parsed, dict) else {"data": parsed}
except Exception:
return None
class FunctionalCaseToApiAutomationService:
def __init__(self, matcher: Optional[ApiMatcher] = None):
self.matcher = matcher or ApiMatcher()
self.llm_converter = LLMApiCaseConverter()
def convert(self, payload: Union[str, Dict[str, Any], FunctionalCaseInput], llm_client: Any = None) -> Dict[str, Any]:
functional_case = FunctionalCaseParser.parse(payload)
options = self._build_options(functional_case)
candidates = functional_case.apiContext or []
matched = self.matcher.match(functional_case, candidates, top_k=int(options.get("topK", 3))) if candidates else []
llm_result = None
if options.get("useLLM", True) and llm_client is not None:
prompt = self.build_prompt(functional_case, matched, options)
llm_result = self.llm_converter.convert(prompt, llm_client)
if llm_result:
return self._finalize_llm_result(functional_case, matched, llm_result, options)
return self._build_rule_result(functional_case, matched, options)
def build_prompt(self, functional_case: FunctionalCaseInput, matched: List[Dict[str, Any]], options: Dict[str, Any]) -> str:
payload = {
"functionalCase": asdict(functional_case),
"matchedApis": [
{
"score": item["score"],
"reason": item["reason"],
"api": asdict(item["api"]),
}
for item in matched
],
"options": options,
}
return (
"你是接口自动化测试用例生成专家。\n"
"请把功能测试用例转换成接口自动化测试用例。\n"
"要求:\n"
"1. 只输出严格 JSON。\n"
"2. 如果给了候选接口信息,优先使用候选接口。\n"
"3. 无法确认的接口信息写入 missingInfo。\n"
"4. 需要包含 method、url、headers、queryParams、body、assertions、performanceAssertions。\n"
"5. 如果涉及性能要求,生成 responseTime 断言。\n"
"6. 如果涉及登录态,使用 ${token}\n\n"
"输入数据:\n"
f"{json.dumps(payload, ensure_ascii=False, indent=2)}\n\n"
"输出 JSON 结构:\n"
"{\n"
' "caseId": "",\n'
' "caseKey": "",\n'
' "caseName": "",\n'
' "automationType": "api",\n'
' "convertStatus": "SUCCESS",\n'
' "apiTestCases": [],\n'
' "missingInfo": [],\n'
' "warnings": []\n'
"}"
)
def _build_options(self, functional_case: FunctionalCaseInput) -> Dict[str, Any]:
options = copy.deepcopy(functional_case.generateOptions or {})
options.setdefault("useLLM", True)
options.setdefault("outputFormat", "json")
options.setdefault("targetFramework", "pytest")
options.setdefault("allowMissingInfo", True)
options.setdefault("topK", 3)
return options
def _build_rule_result(self, functional_case: FunctionalCaseInput, matched: List[Dict[str, Any]], options: Dict[str, Any]) -> Dict[str, Any]:
api_test_cases: List[Dict[str, Any]] = []
missing_info: List[str] = []
warnings: List[str] = []
if matched:
for index, item in enumerate(matched, start=1):
api_test_cases.append(self._build_api_test_case(functional_case, item["api"], index, item["score"], options))
else:
api_test_cases.append(self._build_fallback_test_case(functional_case, options))
missing_info.extend(self._collect_missing_info(functional_case))
warnings.append("未匹配到真实接口信息,已生成占位自动化用例")
missing_info = self._dedupe_text(missing_info)
warnings = self._dedupe_text(warnings)
convert_status = "SUCCESS"
if missing_info:
convert_status = "SUCCESS_WITH_MISSING_INFO"
if warnings and not matched:
convert_status = "DRAFT"
return {
"caseId": functional_case.caseId,
"caseKey": functional_case.caseKey,
"caseName": functional_case.caseName,
"projectName": functional_case.projectName,
"moduleName": functional_case.moduleName,
"automationType": "api",
"convertStatus": convert_status,
"apiTestCases": api_test_cases,
"missingInfo": missing_info,
"warnings": warnings,
}
def _finalize_llm_result(self, functional_case: FunctionalCaseInput, matched: List[Dict[str, Any]], llm_result: Dict[str, Any], options: Dict[str, Any]) -> Dict[str, Any]:
result = copy.deepcopy(llm_result)
result.setdefault("caseId", functional_case.caseId)
result.setdefault("caseKey", functional_case.caseKey)
result.setdefault("caseName", functional_case.caseName)
result.setdefault("projectName", functional_case.projectName)
result.setdefault("moduleName", functional_case.moduleName)
result.setdefault("automationType", "api")
result.setdefault("convertStatus", "SUCCESS")
result.setdefault("apiTestCases", [])
result.setdefault("missingInfo", [])
result.setdefault("warnings", [])
if not result.get("apiTestCases"):
rule_result = self._build_rule_result(functional_case, matched, options)
result["apiTestCases"] = rule_result["apiTestCases"]
if not result.get("missingInfo"):
result["missingInfo"] = rule_result["missingInfo"]
if not result.get("warnings"):
result["warnings"] = rule_result["warnings"]
result["convertStatus"] = rule_result["convertStatus"]
result["missingInfo"] = self._dedupe_text(result.get("missingInfo", []))
result["warnings"] = self._dedupe_text(result.get("warnings", []))
return result
def _build_api_test_case(self, functional_case: FunctionalCaseInput, api: ApiContextItem, index: int, score: float, options: Dict[str, Any]) -> Dict[str, Any]:
method = (api.method or self.matcher.detect_intent(functional_case)).upper()
query_params = copy.deepcopy(api.queryParams or {})
body = copy.deepcopy(api.requestBody)
headers = copy.deepcopy(api.headers or {})
if not headers:
headers = {"Content-Type": "application/json"}
if not any(key.lower() == "authorization" for key in headers):
headers["Authorization"] = "Bearer ${token}"
variables = self._build_variables(functional_case, api, query_params, body)
query_params = self._fill_placeholders(query_params, variables)
body = self._fill_placeholders(body, variables)
assertions = self._build_assertions(functional_case, api, method)
performance_assertions = self._build_performance_assertions(functional_case)
step_name = api.apiName or functional_case.caseName or "接口自动化步骤"
return {
"stepNo": index,
"name": step_name,
"apiName": api.apiName or step_name,
"method": method,
"url": api.url or self._infer_placeholder_url(functional_case),
"headers": headers,
"queryParams": query_params,
"body": body,
"extractVariables": [],
"assertions": assertions,
"performanceAssertions": performance_assertions,
"variables": variables,
"matchedApi": {
"apiName": api.apiName,
"method": method,
"url": api.url,
"matchScore": round(score, 4),
},
}
def _build_fallback_test_case(self, functional_case: FunctionalCaseInput, options: Dict[str, Any]) -> Dict[str, Any]:
method = self.matcher.detect_intent(functional_case)
url = self._infer_placeholder_url(functional_case)
variables = self._build_variables(functional_case, None, {}, None)
return {
"stepNo": 1,
"name": functional_case.caseName or "功能用例转接口自动化",
"apiName": functional_case.caseName or "待确认接口",
"method": method,
"url": url,
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer ${token}",
},
"queryParams": {},
"body": None,
"extractVariables": [],
"assertions": self._build_assertions(functional_case, None, method),
"performanceAssertions": self._build_performance_assertions(functional_case),
"variables": variables,
"matchedApi": {
"apiName": "",
"method": method,
"url": url,
"matchScore": 0,
},
}
def _build_variables(self, functional_case: FunctionalCaseInput, api: Optional[ApiContextItem], query_params: Dict[str, Any], body: Any) -> Dict[str, Any]:
variables: Dict[str, Any] = {}
text = self.matcher._all_text(functional_case)
for keyword in self.matcher.extract_keywords(functional_case):
if keyword in ("搜索", "查询", "列表", "筛选", "关键字", "模糊搜索"):
variables.setdefault("keyword", "测试")
if keyword in ("分页", "分页查询"):
variables.setdefault("page", 1)
variables.setdefault("limit", DEFAULT_PAGE_SIZE)
if keyword in ("登录",):
variables.setdefault("token", "${token}")
if not variables and text:
variables["keyword"] = "测试"
if query_params:
for key in query_params.keys():
if key not in variables and any(token in key.lower() for token in ["keyword", "name", "id", "page", "limit", "status", "type"]):
variables[key] = query_params[key]
if isinstance(body, dict):
for key in body.keys():
if key not in variables and any(token in key.lower() for token in ["keyword", "name", "id", "page", "limit", "status", "type"]):
variables[key] = body[key]
return variables
def _build_assertions(self, functional_case: FunctionalCaseInput, api: Optional[ApiContextItem], method: str) -> List[Dict[str, Any]]:
assertions = [
{
"type": "statusCode",
"actual": "$.code" if api or method != "DELETE" else "$.status",
"operator": "==",
"expected": 200,
}
]
text = self.matcher._all_text(functional_case)
if api and isinstance(api.responseExample, dict):
example = api.responseExample
if "code" in example:
assertions.append(
{
"type": "jsonPath",
"actual": "$.code",
"operator": "==",
"expected": example.get("code", 0),
}
)
if "msg" in example:
assertions.append(
{
"type": "jsonPath",
"actual": "$.msg",
"operator": "notEmpty",
"expected": True,
}
)
if any(keyword in text for keyword in ["搜索", "查询", "列表", "筛选", "返回"]):
assertions.append(
{
"type": "jsonPath",
"actual": "$.data",
"operator": "notEmpty",
"expected": True,
}
)
return assertions
def _build_performance_assertions(self, functional_case: FunctionalCaseInput) -> List[Dict[str, Any]]:
text = self.matcher._all_text(functional_case)
expected_ms = self._extract_response_time_ms(text)
if expected_ms is None:
return []
return [
{
"type": "responseTime",
"operator": "<=",
"expected": expected_ms,
"unit": "ms",
}
]
def _collect_missing_info(self, functional_case: FunctionalCaseInput) -> List[str]:
missing = []
if not functional_case.steps:
missing.append("缺少步骤信息")
if not functional_case.caseName:
missing.append("缺少用例名称")
if not functional_case.apiContext:
missing.append("未传入真实接口信息,已使用占位接口生成")
if self._extract_response_time_ms(self.matcher._all_text(functional_case)) is None and any(k in self.matcher._all_text(functional_case) for k in ["性能", "响应时间", "超时"]):
missing.append("性能阈值未明确,默认使用 1000ms")
return missing
@staticmethod
def _extract_response_time_ms(text: str) -> Optional[int]:
if not text:
return None
patterns = [
r"(\d+)\s*毫秒",
r"(\d+)\s*ms",
r"(\d+)\s*秒",
r"不超过\s*(\d+)\s*秒",
r"<=\s*(\d+)\s*秒",
r"小于等于\s*(\d+)\s*秒",
]
for pattern in patterns:
match = re.search(pattern, text, flags=re.I)
if match:
value = int(match.group(1))
if "" in pattern:
return value * 1000
return value
if any(keyword in text for keyword in ["响应时间", "超时", "性能"]):
return DEFAULT_RESPONSE_TIME_MS
return None
@staticmethod
def _infer_placeholder_url(functional_case: FunctionalCaseInput) -> str:
text = " ".join([functional_case.caseName, functional_case.moduleName] + [step.action for step in functional_case.steps])
if any(keyword in text for keyword in ["登录"]):
return "/api/login"
if any(keyword in text for keyword in ["搜索", "查询", "列表", "筛选"]):
return "/api/list/query"
if any(keyword in text for keyword in ["新增", "创建", "添加"]):
return "/api/create"
if any(keyword in text for keyword in ["修改", "编辑", "更新"]):
return "/api/update"
if any(keyword in text for keyword in ["删除", "移除"]):
return "/api/delete"
return DEFAULT_PLACEHOLDER_URL
@staticmethod
def _fill_placeholders(value: Any, variables: Dict[str, Any]) -> Any:
if isinstance(value, dict):
return {key: FunctionalCaseToApiAutomationService._fill_placeholders(item, variables) for key, item in value.items()}
if isinstance(value, list):
return [FunctionalCaseToApiAutomationService._fill_placeholders(item, variables) for item in value]
if isinstance(value, str):
result = value
for key, variable in variables.items():
result = result.replace("${" + key + "}", str(variable))
return result
return value
@staticmethod
def _dedupe_text(items: Iterable[str]) -> List[str]:
seen = set()
result = []
for item in items:
text = str(item).strip()
if not text or text in seen:
continue
seen.add(text)
result.append(text)
return result
def convert_functional_case(payload: Union[str, Dict[str, Any], FunctionalCaseInput], llm_client: Any = None) -> Dict[str, Any]:
return FunctionalCaseToApiAutomationService().convert(payload, llm_client=llm_client)
def load_payload_from_file(file_path: str) -> Dict[str, Any]:
with open(file_path, "r", encoding="utf-8") as file:
return json.load(file)
def main():
parser = argparse.ArgumentParser(description="功能测试用例转接口自动化用例")
parser.add_argument("--input", type=str, help="输入JSON文件路径未指定则从stdin读取")
parser.add_argument("--output", type=str, help="输出JSON文件路径未指定则打印到stdout")
args = parser.parse_args()
if args.input:
payload = load_payload_from_file(args.input)
else:
payload = json.load(os.sys.stdin)
result = convert_functional_case(payload)
output = json.dumps(result, ensure_ascii=False, indent=2)
if args.output:
with open(args.output, "w", encoding="utf-8") as file:
file.write(output)
else:
print(output)
if __name__ == "__main__":
main()

View File

@@ -1,189 +0,0 @@
# -*- coding: utf-8 -*-
import ast
import json
import os
import re
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
DEFAULT_HUBOPS_PATH = os.path.join(PROJECT_ROOT, "HubOps.md")
class HubOpsParser(object):
def __init__(self, file_path=None):
self.file_path = file_path or os.getenv("JOYHUB_DOC_PATH", DEFAULT_HUBOPS_PATH)
def parse(self):
with open(self.file_path, "r", encoding="utf-8") as file:
lines = file.read().splitlines()
cases = []
headings = []
for index, line in enumerate(lines):
heading = self._parse_heading(line)
if heading:
level, title = heading
headings = [item for item in headings if item[0] < level]
headings.append((level, title))
continue
if line.strip() != "**接口URL**":
continue
case = self._parse_case(lines, index, headings)
if case.get("url"):
case["case_id"] = "joyhub_{:04d}".format(len(cases) + 1)
cases.append(case)
return cases
def _parse_case(self, lines, url_index, headings):
title = self._case_title(headings, url_index)
url = self._next_quote_value(lines, url_index)
method = self._find_section_quote(lines, url_index, "**请求方式**") or "POST"
content_type = self._find_section_quote(lines, url_index, "**Content-Type**") or "json"
body_text = self._find_code_block(lines, url_index, "**请求Body参数**")
query = self._find_param_table(lines, url_index, "**请求Query参数**")
headers = self._find_headers(lines, url_index)
body = self._parse_body(body_text)
return {
"name": title,
"method": method.upper(),
"url": url,
"content_type": content_type,
"headers": headers,
"query": query,
"body": body,
"raw_body": body_text,
}
@staticmethod
def _parse_heading(line):
match = re.match(r"^(#{2,6})\s+(.+?)\s*$", line)
if not match:
return None
return len(match.group(1)), match.group(2).strip()
@staticmethod
def _case_title(headings, url_index):
if headings:
return " / ".join(title for _, title in headings[-3:])
return "HubOps接口{}".format(url_index + 1)
@staticmethod
def _next_quote_value(lines, start):
for index in range(start + 1, min(start + 8, len(lines))):
line = lines[index].strip()
if line.startswith(">"):
value = line[1:].strip()
if value and value != "暂无参数":
return value
return ""
@staticmethod
def _section_end(lines, start):
for index in range(start + 1, len(lines)):
line = lines[index].strip()
if line.startswith("## ") or line.startswith("### ") or line == "**接口URL**":
return index
return len(lines)
def _find_section_quote(self, lines, url_index, section_name):
start = max(0, url_index - 80)
end = self._section_end(lines, url_index)
for index in range(start, min(end, len(lines))):
if lines[index].strip() == section_name:
return self._next_quote_value(lines, index)
return None
def _find_code_block(self, lines, url_index, section_name):
end = self._section_end(lines, url_index)
for index in range(url_index, end):
if lines[index].strip() != section_name:
continue
for code_start in range(index + 1, end):
if lines[code_start].strip().startswith("```"):
block = []
for code_end in range(code_start + 1, end):
if lines[code_end].strip().startswith("```"):
return "\n".join(block).strip()
block.append(lines[code_end])
return ""
def _find_param_table(self, lines, url_index, section_name):
end = self._section_end(lines, url_index)
for index in range(url_index, end):
if lines[index].strip() != section_name:
continue
params = {}
for row_index in range(index + 1, end):
row = lines[row_index].strip()
if not row.startswith("|"):
if params:
break
continue
if "---" in row or "参数名" in row or "暂无参数" in row:
continue
columns = [column.strip() for column in row.strip("|").split("|")]
if len(columns) >= 2 and columns[0]:
params[columns[0]] = self._coerce_value(columns[1])
return params
return {}
def _find_headers(self, lines, url_index):
headers = {}
end = self._section_end(lines, url_index)
for index in range(url_index, end):
if lines[index].strip() != "**请求Header参数**":
continue
table_started = False
for row_index in range(index + 1, end):
row = lines[row_index].strip()
if row.startswith("**") or row.startswith("#") or row.startswith("*"):
break
if not row.startswith("|"):
if table_started:
break
continue
table_started = True
if "---" in row or "参数名" in row or "暂无参数" in row:
continue
columns = [column.strip() for column in row.strip("|").split("|")]
if len(columns) >= 2 and columns[0] and columns[0] != "Authorization":
headers[columns[0]] = columns[1]
return headers
return headers
@classmethod
def _parse_body(cls, body_text):
if not body_text or body_text == "暂无数据":
return {}
text = cls._clean_body(body_text)
for loader in (json.loads, ast.literal_eval):
try:
value = loader(text)
return value if isinstance(value, (dict, list)) else {}
except Exception:
pass
return {}
@staticmethod
def _clean_body(text):
text = re.sub(r"//.*", "", text)
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
text = text.replace("{{token}}", "")
text = re.sub(r",\s*([}\]])", r"\1", text)
return text.strip()
@staticmethod
def _coerce_value(value):
if value in ("-", "暂无参数", ""):
return ""
if value.isdigit():
return int(value)
if value.lower() in ("true", "false"):
return value.lower() == "true"
return value
def load_hubops_cases():
return HubOpsParser().parse()

View File

@@ -1,131 +0,0 @@
# -*- coding: utf-8 -*-
import json
import logging
import os
from urllib.parse import urljoin
import allure
import requests
from joyhub_backend.library.auth import JoyhubAuth, TIMEOUT
MANAGER_BASE_URL = os.getenv(
"JOYHUB_MANAGER_BASE_URL",
"http://test-manager-api.best-envision.com",
)
class JoyhubInterface(object):
def __init__(self):
self.auth = JoyhubAuth()
self.session = requests.Session()
self.base_url = MANAGER_BASE_URL.rstrip("/") + "/"
def request(self, case_name, method, path, body=None, query=None, headers=None, expected_code=0):
url = path if path.startswith("http") else urljoin(self.base_url, path.lstrip("/"))
request_headers = self.auth.auth_headers()
if headers:
request_headers.update({key: value for key, value in headers.items() if key.lower() != "authorization"})
with allure.step("操作步骤:{}".format(case_name)):
self._attach_request(url, method, request_headers, body, query)
logging.info("case: %s", case_name)
logging.info("request url: %s", url)
logging.info("request method: %s", method)
logging.info("request headers: %s", request_headers)
logging.info("request body: %s", body)
logging.info("request query: %s", query)
request_kwargs = {
"method": method,
"url": url,
"params": query,
"headers": request_headers,
"timeout": TIMEOUT,
}
if method.upper() in ("POST", "PUT", "PATCH"):
request_kwargs["json"] = body
elif body:
request_kwargs["params"] = dict(query or {}, **body) if isinstance(body, dict) else query
response = self.session.request(**request_kwargs)
self._attach_response(response)
self._attach_log(case_name, url, method, request_headers, body, query, response)
logging.info("response status: %s", response.status_code)
logging.info("response body: %s", response.text)
is_business_api = self._is_business_api(url)
assertion_text = "HTTP状态码为200且业务code为{}".format(expected_code) if is_business_api else "HTTP状态码为2xx兼容非JSON/SSE响应"
allure.attach(assertion_text, "断言内容", allure.attachment_type.TEXT)
with allure.step("断言内容:{}".format(assertion_text)):
try:
if is_business_api:
assert response.status_code == 200, "HTTP状态码期望200实际{},响应{}".format(
response.status_code, response.text
)
data = self._safe_json(response)
assert isinstance(data, dict), "业务接口响应不是JSON对象响应{}".format(response.text)
assert "code" in data, "响应缺少 code 字段,响应{}".format(data)
assert data.get("code") == expected_code, "业务code期望{},实际{}msg={},响应{}".format(
expected_code, data.get("code"), data.get("msg"), data
)
else:
assert 200 <= response.status_code < 300, "HTTP状态码期望2xx实际{},响应{}".format(
response.status_code, response.text
)
data = self._non_json_result(response)
allure.attach("", "断言失败原因", allure.attachment_type.TEXT)
return data
except AssertionError as error:
allure.attach(str(error), "断言失败原因", allure.attachment_type.TEXT)
raise AssertionError("断言失败原因:{}".format(error))
@staticmethod
def _attach_request(url, method, headers, body, query):
allure.attach(str(url), "请求url", allure.attachment_type.TEXT)
allure.attach(str(method), "请求方式", allure.attachment_type.TEXT)
allure.attach(json.dumps(headers, ensure_ascii=False, indent=2), "请求头", allure.attachment_type.JSON)
allure.attach(json.dumps(query or {}, ensure_ascii=False, indent=2), "请求query", allure.attachment_type.JSON)
allure.attach(json.dumps(body or {}, ensure_ascii=False, indent=2), "请求体", allure.attachment_type.JSON)
@staticmethod
def _attach_response(response):
allure.attach(str(response.status_code), "响应状态码", allure.attachment_type.TEXT)
content_type = response.headers.get("Content-Type", "")
attachment_type = allure.attachment_type.JSON if "json" in content_type.lower() else allure.attachment_type.TEXT
allure.attach(response.text, "响应体/日志", attachment_type)
def _is_business_api(self, url):
return url.startswith(self.base_url)
@staticmethod
def _safe_json(response):
if not response.text.strip():
return None
return response.json()
@staticmethod
def _non_json_result(response):
try:
data = response.json()
except ValueError:
data = {
"status_code": response.status_code,
"content_type": response.headers.get("Content-Type", ""),
"text": response.text,
}
return data
@staticmethod
def _attach_log(case_name, url, method, headers, body, query, response):
log_text = "\n".join([
"case: {}".format(case_name),
"request url: {}".format(url),
"request method: {}".format(method),
"request headers: {}".format(headers),
"request query: {}".format(query or {}),
"request body: {}".format(body or {}),
"response status: {}".format(response.status_code),
"response body: {}".format(response.text),
])
allure.attach(log_text, "日志", allure.attachment_type.TEXT)

View File

@@ -1,136 +0,0 @@
# -*- coding: utf-8 -*-
import allure
import logging
import requests
import json
import pytest
try:
from joyhub_backend.library.joyhub_interface import JoyhubInterface
except Exception:
JoyhubInterface = None
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
BASE_URL = "http://test-manager-api.best-envision.com"
API_PATH = "admin/video/getVideoLabelList"
CASE_NAME = "获取视频标签列表"
def _mask_sensitive(data):
if data is None:
return data
sensitive_keys = {"authorization", "token", "access_token", "accessToken", "cookie", "password", "passwd"}
if isinstance(data, dict):
masked = {}
for key, value in data.items():
if str(key).lower() in sensitive_keys:
masked[key] = "***MASKED***"
else:
masked[key] = _mask_sensitive(value)
return masked
if isinstance(data, list):
return [_mask_sensitive(item) for item in data]
return data
def _attach_json(name, data):
allure.attach(
json.dumps(_mask_sensitive(data), ensure_ascii=False, indent=2),
name,
allure.attachment_type.JSON
)
def _normalize_response(response):
if isinstance(response, requests.Response):
try:
response_json = response.json()
except ValueError:
response_json = None
return response.status_code, response_json, response.text
if isinstance(response, dict):
return 200, response, json.dumps(response, ensure_ascii=False)
return None, None, str(response)
@allure.feature("接口")
class TestHubopsVideoLabelList(object):
def setup_method(self):
logging.info("-----------------------------Test Start-------------------------------")
def teardown_method(self):
logging.info("-----------------------------Test End-------------------------------")
@allure.story("获取视频标签列表")
@allure.title("测试HubOps获取视频标签列表接口")
def test_get_video_label_list(self):
if JoyhubInterface is None:
pytest.skip("joyhub_backend项目优先使用JoyhubInterface鉴权封装请确认joyhub_backend.library.joyhub_interface.JoyhubInterface可导入")
with allure.step("1. 准备请求参数"):
method = "POST"
headers = {
"Content-Type": "application/json"
}
body = {
"page": 1,
"limit": 10,
"sort": "id",
"label_name": "",
"category_id": 0,
"video_type": 0,
"video_num": 0,
"created_at": [],
"order": "descending"
}
logging.info("请求方法: %s", method)
logging.info("请求路径: %s", API_PATH)
_attach_json("请求头", headers)
_attach_json("请求体", body)
with allure.step("2. 调用HubOps获取视频标签列表接口"):
client = JoyhubInterface()
response = client.request(
CASE_NAME,
method,
API_PATH,
body=body,
headers=headers,
expected_code=0
)
status_code, response_json, response_text = _normalize_response(response)
allure.attach(str(status_code), "HTTP状态码", allure.attachment_type.TEXT)
if response_json is not None:
_attach_json("响应JSON", response_json)
else:
allure.attach(response_text, "响应内容", allure.attachment_type.TEXT)
with allure.step("3. 校验响应基础字段"):
assert status_code == 200, "HTTP状态码应为200实际为: {}".format(status_code)
assert response_json is not None, "响应内容应为JSON且不能为空"
assert isinstance(response_json, dict), "响应JSON应为对象类型"
assert "code" in response_json, "响应JSON应包含code字段"
assert response_json.get("code") == 0, "响应code应为0实际为: {}".format(response_json.get("code"))
assert "msg" in response_json, "响应JSON应包含msg字段"
assert response_json.get("msg") is not None, "响应msg字段不应为None"
assert "data" in response_json, "响应JSON应包含data字段"
assert response_json.get("data") is not None, "响应data字段不应为None"
assert isinstance(response_json.get("data"), list), "响应data字段应为数组类型"
assert "count" in response_json, "响应JSON应包含count字段"
assert response_json.get("count") is not None, "响应count字段不应为None"

View File

@@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-
import logging
import re
from urllib.parse import urljoin
import allure
import pytest
from joyhub_backend.library.hubops_parser import load_hubops_cases
from joyhub_backend.library.joyhub_interface import JoyhubInterface
ALL_CASES = load_hubops_cases()
def case_id(case):
raw_id = "{}-{}".format(case.get("case_id"), case.get("name"))
safe_id = re.sub(r"[^0-9A-Za-z_\u4e00-\u9fa5-]+", "_", raw_id)
return safe_id[:120]
@allure.feature("JoyHub Backend 接口自动化")
class TestHubOps(object):
test_case = JoyhubInterface()
def teardown_method(self):
with allure.step("后置:记录用例结束日志"):
logging.info("-----------------------------End-------------------------------")
@pytest.mark.parametrize("case", ALL_CASES, ids=case_id)
def test_hub_ops_api(self, case):
allure.dynamic.title("{} {}".format(case.get("case_id"), case.get("name")))
allure.dynamic.story(case.get("name"))
with allure.step("前置:初始化接口客户端并准备鉴权 token"):
headers = case.get("headers") or {}
query = case.get("query") or {}
body = case.get("body") or {}
response_data = self.test_case.request(
case.get("name"),
case.get("method"),
case.get("url"),
body=body,
query=query,
headers=headers,
)
with allure.step("断言内容:校验响应基础字段"):
request_url = case.get("url") or ""
full_url = request_url if request_url.startswith("http") else urljoin(self.test_case.base_url, request_url.lstrip("/"))
if full_url.startswith(self.test_case.base_url):
assert isinstance(response_data, dict), "断言失败原因响应不是JSON对象响应{}".format(response_data)
assert "code" in response_data, "断言失败原因响应缺少code字段响应{}".format(response_data)
assert response_data.get("msg") is not None, "断言失败原因msg字段不能为空响应{}".format(response_data)
else:
assert response_data is not None, "断言失败原因:非业务接口响应为空"
logging.info("断言通过:%s", case.get("name"))

View File

@@ -1,170 +0,0 @@
# -*- coding: utf-8 -*-
import argparse
import os
import shutil
import subprocess
import sys
current_file_path = os.path.abspath(__file__)
project_root = os.path.abspath(os.path.join(os.path.dirname(current_file_path), '../../'))
if project_root not in sys.path:
sys.path.insert(0, project_root)
TEST_CASE_DIR = 'joyhub_backend/test_case/TestCase'
case_dir = os.path.join(project_root, TEST_CASE_DIR)
ALLURE_RESULTS_DIR = os.path.join(project_root, 'joyhub_backend', 'test_case', 'reports', 'allure-results')
ALLURE_REPORT_DIR = os.path.join(project_root, 'joyhub_backend', 'test_case', 'reports', 'allure-report')
def ensure_dirs():
os.makedirs(ALLURE_RESULTS_DIR, exist_ok=True)
os.makedirs(ALLURE_REPORT_DIR, exist_ok=True)
def clean_allure_results():
if os.path.exists(ALLURE_RESULTS_DIR):
shutil.rmtree(ALLURE_RESULTS_DIR)
os.makedirs(ALLURE_RESULTS_DIR, exist_ok=True)
def find_test_files(directory):
test_files = []
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith('.py') and not file.startswith('__') and file != 'conftest.py':
test_files.append(os.path.join(root, file))
return test_files
def run_pytest(args_list):
env = os.environ.copy()
env['PYTHONPATH'] = project_root + (os.pathsep + env['PYTHONPATH'] if 'PYTHONPATH' in env else '')
cmd = ['python', '-m', 'pytest'] + args_list
print("开始执行pytest...")
print("执行命令: {}".format(' '.join('"{}"'.format(item) if ' ' in item else item for item in cmd)), flush=True)
result = subprocess.run(cmd, cwd=project_root, env=env)
print("pytest执行结束退出码: {}".format(result.returncode), flush=True)
return result.returncode
def run_tests(target=None, test_type='all'):
base_args = ['-v', '--tb=short', '--alluredir={}'.format(ALLURE_RESULTS_DIR)]
if test_type == 'all':
print("运行所有测试用例...")
test_files = find_test_files(case_dir)
if not test_files:
print("错误: 未找到测试文件")
return 1
args = test_files + base_args
elif test_type == 'dir':
full_path = os.path.join(case_dir, target.replace('/', os.sep).replace('\\', os.sep))
if not os.path.exists(full_path):
print("错误: 目录不存在: {}".format(full_path))
return 1
print("按目录运行: {}".format(target))
test_files = find_test_files(full_path)
if not test_files:
print("错误: 未找到测试文件")
return 1
args = test_files + base_args
elif test_type == 'file':
full_path = os.path.join(case_dir, target.replace('/', os.sep).replace('\\', os.sep))
if not os.path.exists(full_path):
print("错误: 文件不存在: {}".format(full_path))
return 1
print("按文件运行: {}".format(target))
args = [full_path] + base_args
elif test_type == 'keyword':
print("按关键字运行: {}".format(target))
test_files = find_test_files(case_dir)
args = test_files + ['-k={}'.format(target)] + base_args
elif test_type == 'marker':
print("按pytest标记运行: {}".format(target))
test_files = find_test_files(case_dir)
args = test_files + ['-m={}'.format(target)] + base_args
elif test_type == 'feature':
print("按Allure feature运行: {}".format(target))
test_files = find_test_files(case_dir)
args = test_files + ['--allure-features={}'.format(target)] + base_args
elif test_type == 'story':
print("按Allure story运行: {}".format(target))
test_files = find_test_files(case_dir)
args = test_files + ['--allure-stories={}'.format(target)] + base_args
else:
print("错误: 未知的测试类型: {}".format(test_type))
return 1
return run_pytest(args)
def generate_allure_report():
print("开始生成Allure报告...", flush=True)
if os.path.exists(ALLURE_REPORT_DIR):
shutil.rmtree(ALLURE_REPORT_DIR)
cmd = 'allure generate "{}" --output "{}"'.format(ALLURE_RESULTS_DIR, ALLURE_REPORT_DIR)
print("执行命令: {}".format(cmd), flush=True)
try:
subprocess.run(cmd, check=True, shell=True)
print("Allure报告生成成功: {}".format(ALLURE_REPORT_DIR))
print("打开报告命令: allure open \"{}\"".format(ALLURE_REPORT_DIR))
return 0
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as error:
print("生成Allure报告失败: {}".format(error))
print("手动执行: {}".format(cmd))
return 1
def open_allure_report():
cmd = 'allure open "{}"'.format(ALLURE_REPORT_DIR)
try:
subprocess.Popen(cmd, shell=True)
print("Allure报告已打开: {}".format(ALLURE_REPORT_DIR))
return 0
except (FileNotFoundError, OSError) as error:
print("打开Allure报告失败: {}".format(error))
return 1
def main():
parser = argparse.ArgumentParser(description='JoyHub Backend 接口自动化测试执行工具')
run_group = parser.add_mutually_exclusive_group(required=False)
run_group.add_argument('--feature', type=str, help='按Allure feature运行')
run_group.add_argument('--story', type=str, help='按Allure story运行')
run_group.add_argument('--dir', type=str, help='按目录运行相对于TestCase目录')
run_group.add_argument('--file', type=str, help='按文件运行相对于TestCase目录')
run_group.add_argument('--keyword', type=str, help='按关键字运行')
run_group.add_argument('--marker', type=str, help='按pytest标记运行')
parser.add_argument('--report', action='store_true', help='生成Allure报告')
parser.add_argument('--open', action='store_true', help='打开Allure报告')
parser.add_argument('--no-report', action='store_true', help='不生成Allure报告')
args = parser.parse_args()
ensure_dirs()
clean_allure_results()
if args.feature:
exit_code = run_tests(args.feature, 'feature')
elif args.story:
exit_code = run_tests(args.story, 'story')
elif args.dir:
exit_code = run_tests(args.dir, 'dir')
elif args.file:
exit_code = run_tests(args.file, 'file')
elif args.keyword:
exit_code = run_tests(args.keyword, 'keyword')
elif args.marker:
exit_code = run_tests(args.marker, 'marker')
else:
exit_code = run_tests()
if args.report or not args.no_report:
generate_allure_report()
if args.open:
open_allure_report()
print("=" * 80)
print("测试执行完成" if exit_code == 0 else "测试执行失败,退出码: {}".format(exit_code))
print("=" * 80)
sys.exit(exit_code)
if __name__ == '__main__':
main()

16
node_modules/.bin/acorn generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
else
exec node "$basedir/../acorn/bin/acorn" "$@"
fi

17
node_modules/.bin/acorn.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\acorn\bin\acorn" %*

28
node_modules/.bin/acorn.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
} else {
& "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../acorn/bin/acorn" $args
} else {
& "node$exe" "$basedir/../acorn/bin/acorn" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
node_modules/.bin/esbuild generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi

17
node_modules/.bin/esbuild.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*

28
node_modules/.bin/esbuild.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
node_modules/.bin/js-yaml generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../js-yaml/bin/js-yaml.js" "$@"
else
exec node "$basedir/../js-yaml/bin/js-yaml.js" "$@"
fi

17
node_modules/.bin/js-yaml.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\js-yaml\bin\js-yaml.js" %*

28
node_modules/.bin/js-yaml.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
} else {
& "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
} else {
& "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

16
node_modules/.bin/jsondiffpatch generated vendored Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" "$@"
else
exec node "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" "$@"
fi

17
node_modules/.bin/jsondiffpatch.cmd generated vendored Normal file
View File

@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jsondiffpatch\bin\jsondiffpatch.js" %*

28
node_modules/.bin/jsondiffpatch.ps1 generated vendored Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" $args
} else {
& "$basedir/node$exe" "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" $args
} else {
& "node$exe" "$basedir/../jsondiffpatch/bin/jsondiffpatch.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret

Some files were not shown because too many files have changed in this diff Show More