From f345844df1bf048d2d9f4e46e28b6e7da029f30e Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Mon, 23 Dec 2024 15:06:32 +0800 Subject: [PATCH 01/28] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/service/config_service.py | 12 +- .../module_admin/service/dict_service.py | 21 +--- .../module_admin/service/job_log_service.py | 10 +- .../module_admin/service/job_service.py | 11 +- .../module_admin/service/log_service.py | 20 +--- .../module_admin/service/post_service.py | 12 +- .../module_admin/service/role_service.py | 12 +- .../module_admin/service/user_service.py | 14 +-- ruoyi-fastapi-backend/utils/excel_util.py | 104 ++++++++++++++++++ 9 files changed, 139 insertions(+), 77 deletions(-) create mode 100644 ruoyi-fastapi-backend/utils/excel_util.py diff --git a/ruoyi-fastapi-backend/module_admin/service/config_service.py b/ruoyi-fastapi-backend/module_admin/service/config_service.py index 1d8ec76..312006d 100644 --- a/ruoyi-fastapi-backend/module_admin/service/config_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/config_service.py @@ -7,7 +7,8 @@ from exceptions.exception import ServiceException from module_admin.dao.config_dao import ConfigDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel, DeleteConfigModel -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class ConfigService: @@ -207,17 +208,12 @@ class ConfigService: 'remark': '备注', } - data = config_list - - for item in data: + for item in config_list: if item.get('configType') == 'Y': item['configType'] = '是' else: item['configType'] = '否' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(config_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/dict_service.py b/ruoyi-fastapi-backend/module_admin/service/dict_service.py index 5354083..bfe6489 100644 --- a/ruoyi-fastapi-backend/module_admin/service/dict_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/dict_service.py @@ -15,7 +15,8 @@ from module_admin.entity.vo.dict_vo import ( DictTypeModel, DictTypePageQueryModel, ) -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class DictTypeService: @@ -192,17 +193,12 @@ class DictTypeService: 'remark': '备注', } - data = dict_type_list - - for item in data: + for item in dict_type_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(dict_type_list, mapping_dict) return binary_data @@ -448,9 +444,7 @@ class DictDataService: 'remark': '备注', } - data = dict_data_list - - for item in data: + for item in dict_data_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -459,9 +453,6 @@ class DictDataService: item['isDefault'] = '是' else: item['isDefault'] = '否' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(dict_data_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_log_service.py b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py index f4c9f37..596abe7 100644 --- a/ruoyi-fastapi-backend/module_admin/service/job_log_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py @@ -6,7 +6,7 @@ from module_admin.dao.job_log_dao import JobLogDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.job_vo import DeleteJobLogModel, JobLogModel, JobLogPageQueryModel from module_admin.service.dict_service import DictDataService -from utils.common_util import export_list2excel +from utils.excel_util import ExcelUtil class JobLogService: @@ -115,7 +115,6 @@ class JobLogService: 'createTime': '创建时间', } - data = job_log_list job_group_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_job_group' ) @@ -129,7 +128,7 @@ class JobLogService: ] job_executor_option_dict = {item.get('value'): item for item in job_executor_option} - for item in data: + for item in job_log_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -138,9 +137,6 @@ class JobLogService: item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label') if str(item.get('jobExecutor')) in job_executor_option_dict.keys(): item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label') - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(job_log_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_service.py b/ruoyi-fastapi-backend/module_admin/service/job_service.py index 2d06b6f..55263c1 100644 --- a/ruoyi-fastapi-backend/module_admin/service/job_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/job_service.py @@ -8,8 +8,9 @@ from module_admin.dao.job_dao import JobDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.job_vo import DeleteJobModel, EditJobModel, JobModel, JobPageQueryModel from module_admin.service.dict_service import DictDataService -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil from utils.cron_util import CronUtil +from utils.excel_util import ExcelUtil from utils.string_util import StringUtil @@ -227,7 +228,6 @@ class JobService: 'remark': '备注', } - data = job_list job_group_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_job_group' ) @@ -241,7 +241,7 @@ class JobService: ] job_executor_option_dict = {item.get('value'): item for item in job_executor_option} - for item in data: + for item in job_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -260,9 +260,6 @@ class JobService: item['concurrent'] = '允许' else: item['concurrent'] = '禁止' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(job_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/log_service.py b/ruoyi-fastapi-backend/module_admin/service/log_service.py index 0c80a60..0983b1a 100644 --- a/ruoyi-fastapi-backend/module_admin/service/log_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/log_service.py @@ -14,7 +14,7 @@ from module_admin.entity.vo.log_vo import ( UnlockUser, ) from module_admin.service.dict_service import DictDataService -from utils.common_util import export_list2excel +from utils.excel_util import ExcelUtil class OperationLogService: @@ -122,7 +122,6 @@ class OperationLogService: 'costTime': '消耗时间(毫秒)', } - data = operation_log_list operation_type_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_oper_type' ) @@ -131,18 +130,14 @@ class OperationLogService: ] operation_type_option_dict = {item.get('value'): item for item in operation_type_option} - for item in data: + for item in operation_log_list: if item.get('status') == 0: item['status'] = '成功' else: item['status'] = '失败' if str(item.get('businessType')) in operation_type_option_dict.keys(): item['businessType'] = operation_type_option_dict.get(str(item.get('businessType'))).get('label') - - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(operation_log_list, mapping_dict) return binary_data @@ -253,16 +248,11 @@ class LoginLogService: 'loginTime': '登录日期', } - data = login_log_list - - for item in data: + for item in login_log_list: if item.get('status') == '0': item['status'] = '成功' else: item['status'] = '失败' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(login_log_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/post_service.py b/ruoyi-fastapi-backend/module_admin/service/post_service.py index 5215539..9338a9f 100644 --- a/ruoyi-fastapi-backend/module_admin/service/post_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/post_service.py @@ -5,7 +5,8 @@ from exceptions.exception import ServiceException from module_admin.dao.post_dao import PostDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class PostService: @@ -172,16 +173,11 @@ class PostService: 'remark': '备注', } - data = post_list - - for item in data: + for item in post_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/role_service.py b/ruoyi-fastapi-backend/module_admin/service/role_service.py index 24f9bee..4b633de 100644 --- a/ruoyi-fastapi-backend/module_admin/service/role_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/role_service.py @@ -15,7 +15,8 @@ from module_admin.entity.vo.role_vo import ( from module_admin.entity.vo.user_vo import UserInfoModel, UserRolePageQueryModel from module_admin.dao.role_dao import RoleDao from module_admin.dao.user_dao import UserDao -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil from utils.page_util import PageResponseModel @@ -295,17 +296,12 @@ class RoleService: 'remark': '备注', } - data = role_list - - for item in data: + for item in role_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(role_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/user_service.py b/ruoyi-fastapi-backend/module_admin/service/user_service.py index dc79430..c149b56 100644 --- a/ruoyi-fastapi-backend/module_admin/service/user_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/user_service.py @@ -31,7 +31,8 @@ from module_admin.service.config_service import ConfigService from module_admin.service.dept_service import DeptService from module_admin.service.post_service import PostService from module_admin.service.role_service import RoleService -from utils.common_util import CamelCaseUtil, export_list2excel, get_excel_template +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil from utils.page_util import PageResponseModel from utils.pwd_util import PwdUtil @@ -461,7 +462,7 @@ class UserService: header_list = ['部门编号', '登录名称', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态'] selector_header_list = ['用户性别', '帐号状态'] option_list = [{'用户性别': ['男', '女', '未知']}, {'帐号状态': ['正常', '停用']}] - binary_data = get_excel_template( + binary_data = ExcelUtil.get_excel_template( header_list=header_list, selector_header_list=selector_header_list, option_list=option_list ) @@ -492,9 +493,7 @@ class UserService: 'remark': '备注', } - data = user_list - - for item in data: + for item in user_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -505,10 +504,7 @@ class UserService: item['sex'] = '女' else: item['sex'] = '未知' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(user_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/utils/excel_util.py b/ruoyi-fastapi-backend/utils/excel_util.py new file mode 100644 index 0000000..875a41d --- /dev/null +++ b/ruoyi-fastapi-backend/utils/excel_util.py @@ -0,0 +1,104 @@ +import io +import pandas as pd +from openpyxl import Workbook +from openpyxl.styles import Alignment, PatternFill +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation +from typing import Dict, List + + +class ExcelUtil: + """ + Excel操作类 + """ + + @classmethod + def __mapping_list(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将list数据中的字段名映射为对应的中文字段名 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: 映射后的数据列表 + """ + mapping_data = [{mapping_dict.get(key): item.get(key) for key in mapping_dict} for item in list_data] + + return mapping_data + + @classmethod + def export_list2excel(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: list数据对应excel的二进制数据 + """ + mapping_data = cls.__mapping_list(list_data, mapping_dict) + df = pd.DataFrame(mapping_data) + binary_data = io.BytesIO() + df.to_excel(binary_data, index=False, engine='openpyxl') + binary_data = binary_data.getvalue() + + return binary_data + + @classmethod + def get_excel_template(cls, header_list: List, selector_header_list: List, option_list: List[Dict]): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param header_list: 表头数据列表 + :param selector_header_list: 需要设置为选择器格式的表头数据列表 + :param option_list: 选择器格式的表头预设的选项列表 + :return: 模板excel的二进制数据 + """ + # 创建Excel工作簿 + wb = Workbook() + # 选择默认的活动工作表 + ws = wb.active + + # 设置表头文字 + headers = header_list + + # 设置表头背景样式为灰色,前景色为白色 + header_fill = PatternFill(start_color='ababab', end_color='ababab', fill_type='solid') + + # 将表头写入第一行 + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.value = header + cell.fill = header_fill + # 设置列宽度为16 + ws.column_dimensions[chr(64 + col_num)].width = 12 + # 设置水平居中对齐 + cell.alignment = Alignment(horizontal='center') + + # 设置选择器的预设选项 + options = option_list + + # 获取selector_header的字母索引 + for selector_header in selector_header_list: + column_selector_header_index = headers.index(selector_header) + 1 + + # 创建数据有效性规则 + header_option = [] + for option in options: + if option.get(selector_header): + header_option = option.get(selector_header) + dv = DataValidation(type='list', formula1=f'"{",".join(header_option)}"') + # 设置数据有效性规则的起始单元格和结束单元格 + dv.add( + f'{get_column_letter(column_selector_header_index)}2:{get_column_letter(column_selector_header_index)}1048576' + ) + # 添加数据有效性规则到工作表 + ws.add_data_validation(dv) + + # 保存Excel文件为字节类型的数据 + file = io.BytesIO() + wb.save(file) + file.seek(0) + + # 读取字节数据 + excel_data = file.getvalue() + + return excel_data -- Gitee From 6cd1ea8311f39b67c63bc5cf7ee1dd0fdf6d03d2 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Tue, 24 Dec 2024 08:52:16 +0800 Subject: [PATCH 02/28] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0.env.staging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-frontend/.env.staging | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ruoyi-fastapi-frontend/.env.staging b/ruoyi-fastapi-frontend/.env.staging index 04fbfd8..8798e3c 100644 --- a/ruoyi-fastapi-frontend/.env.staging +++ b/ruoyi-fastapi-frontend/.env.staging @@ -1,6 +1,8 @@ # 页面标题 VUE_APP_TITLE = vfadmin管理系统 +BABEL_ENV = production + NODE_ENV = production # 测试环境配置 -- Gitee From 27cf206967314a5f9afc547251491a11899d21a6 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Tue, 24 Dec 2024 08:52:37 +0800 Subject: [PATCH 03/28] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81http(s)=E9=93=BE?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/HeaderSearch/index.vue | 8 +++---- .../src/components/TopNav/index.vue | 8 +++---- .../src/store/modules/user.js | 7 +++++- ruoyi-fastapi-frontend/src/utils/validate.js | 22 +++++++++++++++++++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/ruoyi-fastapi-frontend/src/components/HeaderSearch/index.vue b/ruoyi-fastapi-frontend/src/components/HeaderSearch/index.vue index 7d6780b..339a688 100644 --- a/ruoyi-fastapi-frontend/src/components/HeaderSearch/index.vue +++ b/ruoyi-fastapi-frontend/src/components/HeaderSearch/index.vue @@ -22,6 +22,7 @@ // make search results more in line with expectations import Fuse from 'fuse.js/dist/fuse.min.js' import path from 'path' +import { isHttp } from '@/utils/validate' export default { name: 'HeaderSearch', @@ -72,7 +73,7 @@ export default { change(val) { const path = val.path; const query = val.query; - if(this.ishttp(val.path)) { + if(isHttp(val.path)) { // http(s):// 路径新窗口打开 const pindex = path.indexOf("http"); window.open(path.substr(pindex, path.length), "_blank"); @@ -115,7 +116,7 @@ export default { if (router.hidden) { continue } const data = { - path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path, + path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path, title: [...prefixTitle] } @@ -149,9 +150,6 @@ export default { } else { this.options = [] } - }, - ishttp(url) { - return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1 } } } diff --git a/ruoyi-fastapi-frontend/src/components/TopNav/index.vue b/ruoyi-fastapi-frontend/src/components/TopNav/index.vue index 86a91c4..f214245 100644 --- a/ruoyi-fastapi-frontend/src/components/TopNav/index.vue +++ b/ruoyi-fastapi-frontend/src/components/TopNav/index.vue @@ -33,6 +33,7 @@ diff --git a/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue b/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue index 96585a5..39ded08 100644 --- a/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue +++ b/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue @@ -133,11 +133,7 @@ export default { const { name } = this.$route if (name) { this.$store.dispatch('tagsView/addView', this.$route) - if (this.$route.meta.link) { - this.$store.dispatch('tagsView/addIframeView', this.$route) - } } - return false }, moveToCurrentTag() { const tags = this.$refs.tag -- Gitee From 649f2dc5c290d3c1743c9a6640ac94bdc0cf61c8 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 26 Dec 2024 08:45:07 +0800 Subject: [PATCH 06/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=BD=93=E5=89=8D=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=A4=B1=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/controller/user_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py index 4af1850..a54b939 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -135,7 +135,7 @@ async def delete_system_user( ): user_id_list = user_ids.split(',') if user_ids else [] if user_id_list: - if current_user.user.user_id in user_id_list: + if current_user.user.user_id in list(map(int, user_id_list)): logger.warning('当前登录用户不能删除') return ResponseUtil.failure(msg='当前登录用户不能删除') -- Gitee From 62bc2111e024771a3314ac19f121e25c98eb61f2 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 26 Dec 2024 08:46:29 +0800 Subject: [PATCH 07/28] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=B8=AD=E6=93=8D=E4=BD=9C=E6=96=B9=E6=B3=95=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py index e038e0b..5ba35a1 100644 --- a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -51,7 +51,7 @@ class Log: # 获取项目根路径 project_root = os.getcwd() # 处理文件路径,去除项目根路径部分 - relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') + relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.').replace('/', '.') # 获取当前被装饰函数所在路径 func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 -- Gitee From b9e83db69e366dd1ee95cd9aa307301415655298 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Mon, 30 Dec 2024 15:22:53 +0800 Subject: [PATCH 08/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E7=9B=AE=E6=A0=87=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E8=A7=84=E5=88=99=E6=A0=A1=E9=AA=8C=E4=B8=8D=E5=85=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/utils/string_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py index 0be9e65..962afdc 100644 --- a/ruoyi-fastapi-backend/utils/string_util.py +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -96,6 +96,5 @@ class StringUtil: :return: 查找结果 """ if search_str and compare_str_list: - for compare_str in compare_str_list: - return cls.startswith_case(search_str, compare_str) + return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list]) return False -- Gitee From 13c1c83ec1d62e1afc21a5a3242a6b35d8733603 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 8 Jan 2025 15:34:25 +0800 Subject: [PATCH 09/28] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=8D=95=E6=AC=A1=E4=BB=BB=E5=8A=A1=E6=97=B6=E4=BC=9A?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=B7=B2=E5=90=AF=E7=94=A8=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/config/get_scheduler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ruoyi-fastapi-backend/config/get_scheduler.py b/ruoyi-fastapi-backend/config/get_scheduler.py index 2c9457b..4473390 100644 --- a/ruoyi-fastapi-backend/config/get_scheduler.py +++ b/ruoyi-fastapi-backend/config/get_scheduler.py @@ -6,7 +6,9 @@ from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.jobstores.redis import RedisJobStore from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger from asyncio import iscoroutinefunction from datetime import datetime, timedelta from sqlalchemy.engine import create_engine @@ -201,8 +203,7 @@ class SchedulerUtil: job_executor = 'default' scheduler.add_job( func=eval(job_info.invoke_target), - trigger='date', - run_date=datetime.now() + timedelta(seconds=1), + trigger=OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)]), args=job_info.job_args.split(',') if job_info.job_args else None, kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, id=str(job_info.job_id), -- Gitee From 10b49c979761737470ac1e31f5a7305fae30dbb6 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 26 Jan 2025 10:02:44 +0800 Subject: [PATCH 10/28] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Etrace=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=E5=BC=BA=E5=8C=96=E6=97=A5=E5=BF=97=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E8=BF=BD=E8=B8=AA=E5=92=8C=E5=93=8D=E5=BA=94=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/middlewares/handle.py | 3 + .../middlewares/trace_middleware/__init__.py | 17 ++++++ .../middlewares/trace_middleware/ctx.py | 23 +++++++ .../middlewares/trace_middleware/middle.py | 47 ++++++++++++++ .../middlewares/trace_middleware/span.py | 52 ++++++++++++++++ ruoyi-fastapi-backend/utils/log_util.py | 61 +++++++++++++++++-- 6 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/span.py diff --git a/ruoyi-fastapi-backend/middlewares/handle.py b/ruoyi-fastapi-backend/middlewares/handle.py index ea447d4..abb2d0d 100644 --- a/ruoyi-fastapi-backend/middlewares/handle.py +++ b/ruoyi-fastapi-backend/middlewares/handle.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from middlewares.cors_middleware import add_cors_middleware from middlewares.gzip_middleware import add_gzip_middleware +from middlewares.trace_middleware import add_trace_middleware def handle_middleware(app: FastAPI): @@ -11,3 +12,5 @@ def handle_middleware(app: FastAPI): add_cors_middleware(app) # 加载gzip压缩中间件 add_gzip_middleware(app) + # 加载trace中间件 + add_trace_middleware(app) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py new file mode 100644 index 0000000..76f8d85 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from .ctx import TraceCtx +from .middle import TraceASGIMiddleware + +__all__ = ('TraceASGIMiddleware', 'TraceCtx') + +__version__ = '0.1.0' + + +def add_trace_middleware(app: FastAPI): + """ + 添加trace中间件 + + :param app: FastAPI对象 + :return: + """ + app.add_middleware(TraceASGIMiddleware) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py new file mode 100644 index 0000000..558a5c9 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: ctx.py +@time: 2025/1/17 16:57 +""" + +import contextvars +from uuid import uuid4 + +CTX_REQUEST_ID: contextvars.ContextVar[str] = contextvars.ContextVar('request-id', default='') + + +class TraceCtx: + @staticmethod + def set_id(): + _id = uuid4().hex + CTX_REQUEST_ID.set(_id) + return _id + + @staticmethod + def get_id(): + return CTX_REQUEST_ID.get() diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py new file mode 100644 index 0000000..a071692 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: middle.py +@time: 2025/1/17 16:57 +""" + +from functools import wraps +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from .span import get_current_span, Span + + +class TraceASGIMiddleware: + """ + fastapi-example: + app = FastAPI() + app.add_middleware(TraceASGIMiddleware) + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + @staticmethod + async def my_receive(receive: Receive, span: Span): + await span.request_before() + + @wraps(receive) + async def my_receive(): + message = await receive() + await span.request_after(message) + return message + + return my_receive + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + async with get_current_span(scope) as span: + handle_outgoing_receive = await self.my_receive(receive, span) + + async def handle_outgoing_request(message: 'Message') -> None: + await span.response(message) + await send(message) + + await self.app(scope, handle_outgoing_receive, handle_outgoing_request) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py new file mode 100644 index 0000000..1e38eab --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: span.py +@time: 2025/1/17 16:57 +""" + +from contextlib import asynccontextmanager +from starlette.types import Scope, Message +from .ctx import TraceCtx + + +class Span: + """ + 整个http生命周期: + request(before) --> request(after) --> response(before) --> response(after) + """ + + def __init__(self, scope: Scope): + self.scope = scope + + async def request_before(self): + """ + request_before: 处理header信息等, 如记录请求体信息 + """ + TraceCtx.set_id() + + async def request_after(self, message: Message): + """ + request_after: 处理请求bytes, 如记录请求参数 + + example: + message: {'type': 'http.request', 'body': b'{\r\n "name": "\xe8\x8b\x8f\xe8\x8b\x8f\xe8\x8b\x8f"\r\n}', 'more_body': False} + """ + return message + + async def response(self, message: Message): + """ + if message['type'] == "http.response.start": -----> request-before + pass + if message['type'] == "http.response.body": -----> request-after + message.get('body', b'') + pass + """ + if message['type'] == 'http.response.start': + message['headers'].append((b'request-id', TraceCtx.get_id().encode())) + return message + + +@asynccontextmanager +async def get_current_span(scope: Scope): + yield Span(scope) diff --git a/ruoyi-fastapi-backend/utils/log_util.py b/ruoyi-fastapi-backend/utils/log_util.py index e42f393..f953f55 100644 --- a/ruoyi-fastapi-backend/utils/log_util.py +++ b/ruoyi-fastapi-backend/utils/log_util.py @@ -1,11 +1,60 @@ import os +import sys import time -from loguru import logger +from loguru import logger as _logger +from typing import Dict +from middlewares.trace_middleware import TraceCtx -log_path = os.path.join(os.getcwd(), 'logs') -if not os.path.exists(log_path): - os.mkdir(log_path) -log_path_error = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_error.log') +class LoggerInitializer: + def __init__(self): + self.log_path = os.path.join(os.getcwd(), 'logs') + self.__ensure_log_directory_exists() + self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') -logger.add(log_path_error, rotation='50MB', encoding='utf-8', enqueue=True, compression='zip') + def __ensure_log_directory_exists(self): + """ + 确保日志目录存在,如果不存在则创建 + """ + if not os.path.exists(self.log_path): + os.mkdir(self.log_path) + + @staticmethod + def __filter(log: Dict): + """ + 自定义日志过滤器,添加trace_id + """ + log['trace_id'] = TraceCtx.get_id() + return log + + def init_log(self): + """ + 初始化日志配置 + """ + # 自定义日志格式 + format_str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' + '{trace_id} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' + ) + _logger.remove() + # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 + _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=True) + _logger.add( + self.log_path_error, + filter=self.__filter, + format=format_str, + rotation='50MB', + encoding='utf-8', + enqueue=True, + compression='zip', + ) + + return _logger + + +# 初始化日志处理器 +log_initializer = LoggerInitializer() +logger = log_initializer.init_log() -- Gitee From 40d10c82ff8294bf31e2a0fe5c97c9e6f43ff2ab Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 26 Jan 2025 16:45:15 +0800 Subject: [PATCH 11/28] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=A3=85=E9=A5=B0=E5=99=A8=E8=8E=B7=E5=8F=96=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=8F=82=E6=95=B0=E7=9A=84=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/annotation/log_annotation.py | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py index 5ba35a1..1d01c1c 100644 --- a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -7,7 +7,8 @@ from datetime import datetime from fastapi import Request from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse from functools import lru_cache, wraps -from typing import Literal, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Any, Callable, Literal, Optional from user_agents import parse from config.enums import BusinessType from config.env import AppConfig @@ -55,9 +56,11 @@ class Log: # 获取当前被装饰函数所在路径 func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 - request: Request = kwargs.get('request') + request_name_list = get_function_parameters_name_by_type(func, Request) + request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs) token = request.headers.get('Authorization') - query_db = kwargs.get('query_db') + session_name_list = get_function_parameters_name_by_type(func, AsyncSession) + query_db = get_function_parameters_value_by_name(func, session_name_list[0], *args, **kwargs) request_method = request.method operator_type = 0 user_agent = request.headers.get('User-Agent') @@ -222,3 +225,37 @@ def get_ip_location(oper_ip: str): oper_location = '未知' print(e) return oper_location + + +def get_function_parameters_name_by_type(func: Callable, param_type: Any): + """ + 获取函数指定类型的参数名称 + + :param func: 函数 + :param arg_type: 参数类型 + :return: 函数指定类型的参数名称 + """ + # 获取函数的参数信息 + parameters = inspect.signature(func).parameters + # 找到指定类型的参数名称 + parameters_name_list = [] + for name, param in parameters.items(): + if param.annotation == param_type: + parameters_name_list.append(name) + return parameters_name_list + + +def get_function_parameters_value_by_name(func: Callable, name: str, *args, **kwargs): + """ + 获取函数指定参数的值 + + :param func: 函数 + :param name: 参数名 + :return: 参数值 + """ + # 获取参数值 + bound_parameters = inspect.signature(func).bind(*args, **kwargs) + bound_parameters.apply_defaults() + parameters_value = bound_parameters.arguments.get(name) + + return parameters_value -- Gitee From df0b82f357617e47888531960fa7608dda6f4e7c Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 19 Feb 2025 00:18:16 +0800 Subject: [PATCH 12/28] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/config/constant.py | 329 ++++++++++ ruoyi-fastapi-backend/config/env.py | 28 + .../controller/gen_controller.py | 158 +++++ .../module_generator/dao/gen_dao.py | 390 ++++++++++++ .../module_generator/entity/do/gen_do.py | 74 +++ .../module_generator/entity/vo/gen_vo.py | 264 ++++++++ .../module_generator/service/gen_service.py | 480 ++++++++++++++ .../templates/js/api.js.jinja2 | 44 ++ .../templates/python/controller.py.jinja2 | 125 ++++ .../templates/python/dao.py.jinja2 | 213 +++++++ .../templates/python/do.py.jinja2 | 41 ++ .../templates/python/service.py.jinja2 | 211 +++++++ .../templates/python/vo.py.jinja2 | 167 +++++ .../module_generator/templates/sql/sql.jinja2 | 47 ++ .../templates/vue/index-tree.vue.jinja2 | 491 +++++++++++++++ .../templates/vue/index.vue.jinja2 | 586 ++++++++++++++++++ .../templates/vue/v3/index-tree.vue.jinja2 | 454 ++++++++++++++ .../templates/vue/v3/index.vue.jinja2 | 571 +++++++++++++++++ ruoyi-fastapi-backend/server.py | 2 + .../sql/ruoyi-fastapi-pg.sql | 80 ++- ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql | 5 +- ruoyi-fastapi-backend/utils/common_util.py | 4 + ruoyi-fastapi-backend/utils/gen_util.py | 223 +++++++ ruoyi-fastapi-backend/utils/string_util.py | 80 ++- ruoyi-fastapi-backend/utils/template_util.py | 468 ++++++++++++++ ruoyi-fastapi-frontend/src/api/tool/gen.js | 9 + .../src/views/tool/gen/createTable.vue | 45 ++ .../src/views/tool/gen/editTable.vue | 30 +- .../src/views/tool/gen/index.vue | 45 +- 29 files changed, 5626 insertions(+), 38 deletions(-) create mode 100644 ruoyi-fastapi-backend/module_generator/controller/gen_controller.py create mode 100644 ruoyi-fastapi-backend/module_generator/dao/gen_dao.py create mode 100644 ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py create mode 100644 ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py create mode 100644 ruoyi-fastapi-backend/module_generator/service/gen_service.py create mode 100644 ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/utils/gen_util.py create mode 100644 ruoyi-fastapi-backend/utils/template_util.py create mode 100644 ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue diff --git a/ruoyi-fastapi-backend/config/constant.py b/ruoyi-fastapi-backend/config/constant.py index 6db32da..1b124a0 100644 --- a/ruoyi-fastapi-backend/config/constant.py +++ b/ruoyi-fastapi-backend/config/constant.py @@ -1,3 +1,6 @@ +from config.env import DataBaseConfig + + class CommonConstant: """ 常用常量 @@ -150,3 +153,329 @@ class MenuConstant: LAYOUT = 'Layout' PARENT_VIEW = 'ParentView' INNER_LINK = 'InnerLink' + + +class GenConstant: + """ + 代码生成常量 + + TPL_CRUD: 单表(增删改查 + TPL_TREE: 树表(增删改查) + TPL_SUB: 主子表(增删改查) + TREE_CODE: 树编码字段 + TREE_PARENT_CODE: 树父编码字段 + TREE_NAME: 树名称字段 + PARENT_MENU_ID: 上级菜单ID字段 + PARENT_MENU_NAME: 上级菜单名称字段 + COLUMNTYPE_STR: 数据库字符串类型 + COLUMNTYPE_TEXT: 数据库文本类型 + COLUMNTYPE_TIME: 数据库时间类型 + COLUMNTYPE_GEOMETRY: 数据库字空间类型 + COLUMNTYPE_NUMBER: 数据库数字类型 + COLUMNNAME_NOT_EDIT: 页面不需要编辑字段 + COLUMNNAME_NOT_LIST: 页面不需要显示的列表字段 + COLUMNNAME_NOT_QUERY: 页面不需要查询字段 + BASE_ENTITY: Entity基类字段 + TREE_ENTITY: Tree基类字段 + HTML_INPUT: 文本框 + HTML_TEXTAREA: 文本域 + HTML_SELECT: 下拉框 + HTML_RADIO: 单选框 + HTML_CHECKBOX: 复选框 + HTML_DATETIME: 日期控件 + HTML_IMAGE_UPLOAD: 图片上传控件 + HTML_FILE_UPLOAD: 文件上传控件 + HTML_EDITOR: 富文本控件 + TYPE_DECIMAL: 高精度计算类型 + TYPE_DATE: 时间类型 + QUERY_LIKE: 模糊查询 + QUERY_EQ: 相等查询 + REQUIRE: 需要 + DB_TO_SQLALCHEMY_TYPE_MAPPING: 数据库类型与sqlalchemy类型映射 + DB_TO_PYTHON_TYPE_MAPPING: 数据库类型与python类型映射 + """ + + TPL_CRUD = 'crud' + TPL_TREE = 'tree' + TPL_SUB = 'sub' + TREE_CODE = 'treeCode' + TREE_PARENT_CODE = 'treeParentCode' + TREE_NAME = 'treeName' + PARENT_MENU_ID = 'parentMenuId' + PARENT_MENU_NAME = 'parentMenuName' + COLUMNTYPE_STR = ( + ['character varying', 'varchar', 'character', 'char'] + if DataBaseConfig.db_type == 'postgresql' + else ['char', 'varchar', 'nvarchar', 'varchar2'] + ) + COLUMNTYPE_TEXT = ( + ['text', 'citext'] if DataBaseConfig.db_type == 'postgresql' else ['tinytext', 'text', 'mediumtext', 'longtext'] + ) + COLUMNTYPE_TIME = ( + [ + 'date', + 'time', + 'time with time zone', + 'time without time zone', + 'timestamp', + 'timestamp with time zone', + 'timestamp without time zone', + 'interval', + ] + if DataBaseConfig.db_type == 'postgresql' + else ['datetime', 'time', 'date', 'timestamp'] + ) + COLUMNTYPE_GEOMETRY = ( + ['point', 'line', 'lseg', 'box', 'path', 'polygon', 'circle'] + if DataBaseConfig.db_type == 'postgresql' + else [ + 'geometry', + 'point', + 'linestring', + 'polygon', + 'multipoint', + 'multilinestring', + 'multipolygon', + 'geometrycollection', + ] + ) + COLUMNTYPE_NUMBER = [ + 'tinyint', + 'smallint', + 'mediumint', + 'int', + 'number', + 'integer', + 'bit', + 'bigint', + 'float', + 'double', + 'decimal', + ] + COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag'] + COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time'] + COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark'] + BASE_ENTITY = ['createBy', 'createTime', 'updateBy', 'updateTime', 'remark'] + TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children'] + HTML_INPUT = 'input' + HTML_TEXTAREA = 'textarea' + HTML_SELECT = 'select' + HTML_RADIO = 'radio' + HTML_CHECKBOX = 'checkbox' + HTML_DATETIME = 'datetime' + HTML_IMAGE_UPLOAD = 'imageUpload' + HTML_FILE_UPLOAD = 'fileUpload' + HTML_EDITOR = 'editor' + TYPE_DECIMAL = 'Decimal' + TYPE_DATE = ['date', 'time', 'datetime'] + QUERY_LIKE = 'LIKE' + QUERY_EQ = 'EQ' + REQUIRE = '1' + DB_TO_SQLALCHEMY_TYPE_MAPPING = ( + { + 'boolean': 'Boolean', + 'smallint': 'SmallInteger', + 'integer': 'Integer', + 'bigint': 'BigInteger', + 'real': 'Float', + 'double precision': 'Float', + 'numeric': 'Numeric', + 'character varying': 'String', + 'character': 'String', + 'text': 'Text', + 'bytea': 'LargeBinary', + 'date': 'Date', + 'time': 'Time', + 'time with time zone': 'Time', + 'time without time zone': 'Time', + 'timestamp': 'DateTime', + 'timestamp with time zone': 'DateTime', + 'timestamp without time zone': 'DateTime', + 'interval': 'Interval', + 'json': 'JSON', + 'jsonb': 'JSONB', + 'uuid': 'Uuid', + 'inet': 'INET', + 'cidr': 'CIDR', + 'macaddr': 'MACADDR', + 'point': 'Geometry', + 'line': 'Geometry', + 'lseg': 'Geometry', + 'box': 'Geometry', + 'path': 'Geometry', + 'polygon': 'Geometry', + 'circle': 'Geometry', + 'bit': 'Bit', + 'bit varying': 'Bit', + 'tsvector': 'TSVECTOR', + 'tsquery': 'TSQUERY', + 'xml': 'String', + 'array': 'ARRAY', + 'composite': 'JSON', + 'enum': 'Enum', + 'range': 'Range', + 'money': 'Numeric', + 'pg_lsn': 'BigInteger', + 'txid_snapshot': 'String', + 'oid': 'BigInteger', + 'regproc': 'String', + 'regclass': 'String', + 'regtype': 'String', + 'regrole': 'String', + 'regnamespace': 'String', + 'int2vector': 'ARRAY', + 'oidvector': 'ARRAY', + 'pg_node_tree': 'Text', + } + if DataBaseConfig.db_type == 'postgresql' + else { + # 数值类型 + 'TINYINT': 'SmallInteger', + 'SMALLINT': 'SmallInteger', + 'MEDIUMINT': 'Integer', + 'INT': 'Integer', + 'INTEGER': 'Integer', + 'BIGINT': 'BigInteger', + 'FLOAT': 'Float', + 'DOUBLE': 'Float', + 'DECIMAL': 'DECIMAL', + 'BIT': 'Integer', + # 日期和时间类型 + 'DATE': 'Date', + 'TIME': 'Time', + 'DATETIME': 'DateTime', + 'TIMESTAMP': 'TIMESTAMP', + 'YEAR': 'Integer', + # 字符串类型 + 'CHAR': 'CHAR', + 'VARCHAR': 'String', + 'TINYTEXT': 'Text', + 'TEXT': 'Text', + 'MEDIUMTEXT': 'Text', + 'LONGTEXT': 'Text', + 'BINARY': 'BINARY', + 'VARBINARY': 'VARBINARY', + 'TINYBLOB': 'LargeBinary', + 'BLOB': 'LargeBinary', + 'MEDIUMBLOB': 'LargeBinary', + 'LONGBLOB': 'LargeBinary', + # 枚举和集合类型 + 'ENUM': 'Enum', + 'SET': 'String', + # JSON 类型 + 'JSON': 'JSON', + # 空间数据类型(需要扩展支持,如 GeoAlchemy2) + 'GEOMETRY': 'Geometry', # 需要安装 geoalchemy2 + 'POINT': 'Geometry', + 'LINESTRING': 'Geometry', + 'POLYGON': 'Geometry', + 'MULTIPOINT': 'Geometry', + 'MULTILINESTRING': 'Geometry', + 'MULTIPOLYGON': 'Geometry', + 'GEOMETRYCOLLECTION': 'Geometry', + } + ) + DB_TO_PYTHON_TYPE_MAPPING = ( + { + 'boolean': 'bool', + 'smallint': 'int', + 'integer': 'int', + 'bigint': 'int', + 'real': 'float', + 'double precision': 'float', + 'numeric': 'Decimal', + 'character varying': 'str', + 'character': 'str', + 'text': 'str', + 'bytea': 'bytes', + 'date': 'date', + 'time': 'time', + 'time with time zone': 'time', + 'time without time zone': 'time', + 'timestamp': 'datetime', + 'timestamp with time zone': 'datetime', + 'timestamp without time zone': 'datetime', + 'interval': 'timedelta', + 'json': 'dict', + 'jsonb': 'dict', + 'uuid': 'str', + 'inet': 'str', + 'cidr': 'str', + 'macaddr': 'str', + 'point': 'list', + 'line': 'list', + 'lseg': 'list', + 'box': 'list', + 'path': 'list', + 'polygon': 'list', + 'circle': 'list', + 'bit': 'int', + 'bit varying': 'int', + 'tsvector': 'str', + 'tsquery': 'str', + 'xml': 'str', + 'array': 'list', + 'composite': 'dict', + 'enum': 'str', + 'range': 'list', + 'money': 'Decimal', + 'pg_lsn': 'int', + 'txid_snapshot': 'str', + 'oid': 'int', + 'regproc': 'str', + 'regclass': 'str', + 'regtype': 'str', + 'regrole': 'str', + 'regnamespace': 'str', + 'int2vector': 'list', + 'oidvector': 'list', + 'pg_node_tree': 'str', + } + if DataBaseConfig.db_type == 'postgresql' + else { + # 数值类型 + 'TINYINT': 'int', + 'SMALLINT': 'int', + 'MEDIUMINT': 'int', + 'INT': 'int', + 'INTEGER': 'int', + 'BIGINT': 'int', + 'FLOAT': 'float', + 'DOUBLE': 'float', + 'DECIMAL': 'Decimal', + 'BIT': 'int', + # 日期和时间类型 + 'DATE': 'date', + 'TIME': 'time', + 'DATETIME': 'datetime', + 'TIMESTAMP': 'datetime', + 'YEAR': 'int', + # 字符串类型 + 'CHAR': 'str', + 'VARCHAR': 'str', + 'TINYTEXT': 'str', + 'TEXT': 'str', + 'MEDIUMTEXT': 'str', + 'LONGTEXT': 'str', + 'BINARY': 'bytes', + 'VARBINARY': 'bytes', + 'TINYBLOB': 'bytes', + 'BLOB': 'bytes', + 'MEDIUMBLOB': 'bytes', + 'LONGBLOB': 'bytes', + # 枚举和集合类型 + 'ENUM': 'str', + 'SET': 'str', + # JSON 类型 + 'JSON': 'dict', + # 空间数据类型(通常需要特殊处理) + 'GEOMETRY': 'bytes', + 'POINT': 'bytes', + 'LINESTRING': 'bytes', + 'POLYGON': 'bytes', + 'MULTIPOINT': 'bytes', + 'MULTILINESTRING': 'bytes', + 'MULTIPOLYGON': 'bytes', + 'GEOMETRYCOLLECTION': 'bytes', + } + ) diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 78378a6..f4d0c60 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/ruoyi-fastapi-backend/config/env.py @@ -64,6 +64,24 @@ class RedisSettings(BaseSettings): redis_database: int = 2 +class GenSettings: + """ + 代码生成配置 + """ + + author = 'insistence' + package_name = 'module_admin.system' + auto_remove_pre = False + table_prefix = 'sys_' + allow_overwrite = True + + GEN_PATH = 'vf_admin/gen_path' + + def __init__(self): + if not os.path.exists(self.GEN_PATH): + os.makedirs(self.GEN_PATH) + + class UploadSettings: """ 上传配置 @@ -159,6 +177,14 @@ class GetConfig: # 实例化Redis配置模型 return RedisSettings() + @lru_cache() + def get_gen_config(self): + """ + 获取代码生成配置 + """ + # 实例化代码生成配置 + return GenSettings() + @lru_cache() def get_upload_config(self): """ @@ -204,5 +230,7 @@ JwtConfig = get_config.get_jwt_config() DataBaseConfig = get_config.get_database_config() # Redis配置 RedisConfig = get_config.get_redis_config() +# 代码生成配置 +GenConfig = get_config.get_gen_config() # 上传配置 UploadConfig = get_config.get_upload_config() diff --git a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py new file mode 100644 index 0000000..4e227c1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py @@ -0,0 +1,158 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Query, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.env import GenConfig +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckRoleInterfaceAuth, CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import DeleteGenTableModel, EditGenTableModel, GenTablePageQueryModel +from module_generator.service.gen_service import GenTableColumnService, GenTableService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +genController = APIRouter(prefix='/tool/gen', dependencies=[Depends(LoginService.get_current_user)]) + + +@genController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.get( + '/db/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_db_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_db_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.post('/importTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))]) +@Log(title='代码生成', business_type=BusinessType.IMPORT) +async def import_gen_table( + request: Request, + tables: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + table_names = tables.split(',') if tables else [] + add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(query_db, table_names) + add_gen_table_result = await GenTableService.import_gen_table_services(query_db, add_gen_table_list, current_user) + logger.info(add_gen_table_result.message) + + return ResponseUtil.success(msg=add_gen_table_result.message) + + +@genController.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@ValidateFields(validate_model='edit_gen_table') +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def edit_gen_table( + request: Request, + edit_gen_table: EditGenTableModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_gen_table.update_by = current_user.user.user_name + edit_gen_table.update_time = datetime.now() + await GenTableService.validate_edit(edit_gen_table) + edit_gen_result = await GenTableService.edit_gen_table_services(query_db, edit_gen_table) + logger.info(edit_gen_result.message) + + return ResponseUtil.success(msg=edit_gen_result.message) + + +@genController.delete('/{table_ids}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:remove'))]) +@Log(title='代码生成', business_type=BusinessType.DELETE) +async def delete_gen_table(request: Request, table_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_gen_table = DeleteGenTableModel(tableIds=table_ids) + delete_gen_table_result = await GenTableService.delete_gen_table_services(query_db, delete_gen_table) + logger.info(delete_gen_table_result.message) + + return ResponseUtil.success(msg=delete_gen_table_result.message) + + +@genController.post('/createTable', dependencies=[Depends(CheckRoleInterfaceAuth('admin'))]) +@Log(title='创建表', business_type=BusinessType.OTHER) +async def create_table( + request: Request, + sql: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + create_table_result = await GenTableService.create_table_services(query_db, sql, current_user) + logger.info(create_table_result.message) + + return ResponseUtil.success(msg=create_table_result.message) + + +@genController.get('/batchGenCode', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def batch_gen_code(request: Request, tables: str = Query(), query_db: AsyncSession = Depends(get_db)): + table_names = tables.split(',') if tables else [] + batch_gen_code_result = await GenTableService.batch_gen_code_services(query_db, table_names) + logger.info('生成代码成功') + + return ResponseUtil.streaming(data=bytes2file_response(batch_gen_code_result)) + + +@genController.get('/genCode/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def gen_code_local(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + if not GenConfig.allow_overwrite: + logger.error('【系统预设】不允许生成文件覆盖到本地') + return ResponseUtil.error('【系统预设】不允许生成文件覆盖到本地') + gen_code_local_result = await GenTableService.generate_code_services(query_db, table_name) + logger.info(gen_code_local_result.message) + + return ResponseUtil.success(msg=gen_code_local_result.message) + + +@genController.get('/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:query'))]) +async def query_detail_gen_table(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + gen_table = await GenTableService.get_gen_table_by_id_services(query_db, table_id) + gen_tables = await GenTableService.get_gen_table_all_services(query_db) + gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(query_db, table_id) + gen_table_detail_result = dict(info=gen_table, rows=gen_columns, tables=gen_tables) + logger.info(f'获取table_id为{table_id}的信息成功') + + return ResponseUtil.success(data=gen_table_detail_result) + + +@genController.get('/preview/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:preview'))]) +async def preview_code(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + preview_code_result = await GenTableService.preview_code_services(query_db, table_id) + logger.info('获取预览代码成功') + + return ResponseUtil.success(data=preview_code_result) + + +@genController.get('/synchDb/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def sync_db(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + sync_db_result = await GenTableService.sync_db_services(query_db, table_name) + logger.info(sync_db_result.message) + + return ResponseUtil.success(data=sync_db_result.message) diff --git a/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py new file mode 100644 index 0000000..d6e74e9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py @@ -0,0 +1,390 @@ +from datetime import datetime, time +from sqlalchemy import delete, func, select, text, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload +from typing import List +from config.env import DataBaseConfig +from module_generator.entity.do.gen_do import GenTable, GenTableColumn +from module_generator.entity.vo.gen_vo import ( + GenTableBaseModel, + GenTableColumnBaseModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from utils.page_util import PageUtil + + +class GenTableDao: + """ + 代码生成业务表模块数据库操作层 + """ + + @classmethod + async def get_gen_table_by_id(cls, db: AsyncSession, table_id: int): + """ + 根据业务表id获取需要生成的业务表信息 + + :param db: orm对象 + :param table_id: 业务表id + :return: 需要生成的业务表信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_id == table_id) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据业务表名称获取需要生成的业务表信息 + + :param db: orm对象 + :param table_name: 业务表名称 + :return: 需要生成的业务表信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_name == table_name) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_all(cls, db: AsyncSession): + """ + 获取所有业务表信息 + + :param db: orm对象 + :return: 所有业务表信息 + """ + gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all() + + return gen_table_all + + @classmethod + async def create_table_by_sql_dao(cls, db: AsyncSession, sql: str): + """ + 根据sql语句创建表结构 + + :param db: orm对象 + :param sql: sql语句 + :return: + """ + await db.execute(text(sql)) + + @classmethod + async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取代码生成业务表列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成业务表列表信息对象 + """ + query = ( + select(GenTable) + .options(selectinload(GenTable.columns)) + .where( + func.lower(GenTable.table_name).like(f'%{query_object.table_name.lower()}%') + if query_object.table_name + else True, + func.lower(GenTable.table_comment).like(f'%{query_object.table_comment.lower()}%') + if query_object.table_comment + else True, + GenTable.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .distinct() + ) + gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_table_list + + @classmethod + async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取数据库列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + list_table + where + table_name not like 'apscheduler_%' + and table_name not like 'gen_%' + and table_name not in (select table_name from gen_table) + """ + else: + query_sql = """ + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_schema = (select database()) + and table_name not like 'apscheduler\_%' + and table_name not like 'gen\_%' + and table_name not in (select table_name from gen_table) + """ + if query_object.table_name: + query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))""" + if query_object.table_comment: + query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" + if query_object.begin_time: + if DataBaseConfig.db_type == 'postgresql': + query_sql += """and create_time::date >= to_date(:begin_time, 'yyyy-MM-dd')""" + else: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')""" + if query_object.end_time: + if DataBaseConfig.db_type == 'postgresql': + query_sql += """and create_time::date <= to_date(:end_time, 'yyyy-MM-dd')""" + else: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')""" + query_sql += """order by create_time desc""" + query = select( + text(query_sql).bindparams( + **{ + k: v + for k, v in query_object.model_dump(exclude_none=True, exclude={'page_num', 'page_size'}).items() + } + ) + ) + gen_db_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_db_table_list + + @classmethod + async def get_gen_db_table_list_by_names(cls, db: AsyncSession, table_names: List[str]): + """ + 根据业务表名称组获取数据库列表信息 + + :param db: orm对象 + :param table_names: 业务表名称组 + :return: 数据库列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + select + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + list_table + where + table_name not like 'qrtz_%' + and table_name not like 'gen_%' + and table_name = any(:table_names) + """ + else: + query_sql = """ + select + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_name not like 'qrtz\_%' + and table_name not like 'gen\_%' + and table_schema = (select database()) + and table_name in :table_names + """ + query = text(query_sql).bindparams(table_names=tuple(table_names)) + gen_db_table_list = (await db.execute(query)).fetchall() + + return gen_db_table_list + + @classmethod + async def add_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 新增业务表数据库操作 + + :param db: orm对象 + :param gen_table: 业务表对象 + :return: + """ + db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump()) + db.add(db_gen_table) + await db.flush() + + return db_gen_table + + @classmethod + async def edit_gen_table_dao(cls, db: AsyncSession, gen_table: dict): + """ + 编辑业务表数据库操作 + + :param db: orm对象 + :param gen_table: 需要更新的业务表字典 + :return: + """ + await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) + + @classmethod + async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 删除业务表数据库操作 + + :param db: orm对象 + :param gen_table: 业务表对象 + :return: + """ + await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id]))) + + +class GenTableColumnDao: + """ + 代码生成业务表字段模块数据库操作层 + """ + + @classmethod + async def get_gen_table_column_list_by_table_id(cls, db: AsyncSession, table_id: int): + """ + 根据业务表id获取需要生成的业务表字段列表信息 + + :param db: orm对象 + :param table_id: 业务表id + :return: 需要生成的业务表字段列表信息对象 + """ + gen_table_column_list = ( + ( + await db.execute( + select(GenTableColumn).where(GenTableColumn.table_id == table_id).order_by(GenTableColumn.sort) + ) + ) + .scalars() + .all() + ) + + return gen_table_column_list + + @classmethod + async def get_gen_db_table_columns_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据业务表名称获取业务表字段列表信息 + + :param db: orm对象 + :param table_name: 业务表名称 + :return: 业务表字段列表信息对象 + """ + if DataBaseConfig.db_type == 'postgresql': + query_sql = """ + select + column_name, is_required, is_pk, sort, column_comment, is_increment, column_type + from + list_column + where + table_name = :table_name + """ + else: + query_sql = """ + select + column_name as column_name, + case + when is_nullable = 'no' and column_key != 'PRI' then '1' + else '0' + end as is_required, + case + when column_key = 'PRI' then '1' + else '0' + end as is_pk, + ordinal_position as sort, + column_comment as column_comment, + case + when extra = 'auto_increment' then '1' + else '0' + end as is_increment, + column_type as column_type + from + information_schema.columns + where + table_schema = (select database()) + and table_name = :table_name + order by + ordinal_position + """ + query = text(query_sql).bindparams(table_name=table_name) + gen_db_table_columns = (await db.execute(query)).fetchall() + + return gen_db_table_columns + + @classmethod + async def add_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 新增业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 岗位对象 + :return: + """ + db_gen_table_column = GenTableColumn( + **GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump() + ) + db.add(db_gen_table_column) + await db.flush() + + return db_gen_table_column + + @classmethod + async def edit_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: dict): + """ + 编辑业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 需要更新的业务表字段字典 + :return: + """ + await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) + + @classmethod + async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 通过业务表id删除业务表字段数据库操作 + + :param db: orm对象 + :param gen_table_column: 业务表字段对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id]))) + + @classmethod + async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 通过业务字段id删除业务表字段数据库操作 + + :param db: orm对象 + :param post: 业务表字段对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id]))) diff --git a/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py new file mode 100644 index 0000000..e64d0bf --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py @@ -0,0 +1,74 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from config.database import Base + + +class GenTable(Base): + """ + 代码生成业务表 + """ + + __tablename__ = 'gen_table' + + table_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_name = Column(String(200), nullable=True, default='', comment='表名称') + table_comment = Column(String(500), nullable=True, default='', comment='表描述') + sub_table_name = Column(String(64), nullable=True, comment='关联子表的表名') + sub_table_fk_name = Column(String(64), nullable=True, comment='子表关联的外键名') + class_name = Column(String(100), nullable=True, default='', comment='实体类名称') + tpl_category = Column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type = Column( + String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)' + ) + package_name = Column(String(100), nullable=True, comment='生成包路径') + module_name = Column(String(30), nullable=True, comment='生成模块名') + business_name = Column(String(30), nullable=True, comment='生成业务名') + function_name = Column(String(100), nullable=True, comment='生成功能名') + function_author = Column(String(100), nullable=True, comment='生成功能作者') + gen_type = Column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)') + gen_path = Column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)') + options = Column(String(1000), nullable=True, comment='其它生成选项') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') + + columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables') + + +class GenTableColumn(Base): + """ + 代码生成业务表字段 + """ + + __tablename__ = 'gen_table_column' + + column_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号') + column_name = Column(String(200), nullable=True, comment='列名称') + column_comment = Column(String(500), nullable=True, comment='列描述') + column_type = Column(String(100), nullable=True, comment='列类型') + python_type = Column(String(500), nullable=True, comment='PYTHON类型') + python_field = Column(String(200), nullable=True, comment='PYTHON字段名') + is_pk = Column(String(1), nullable=True, comment='是否主键(1是)') + is_increment = Column(String(1), nullable=True, comment='是否自增(1是)') + is_required = Column(String(1), nullable=True, comment='是否必填(1是)') + is_unique = Column(String(1), nullable=True, comment='是否唯一(1是)') + is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)') + is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)') + is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)') + is_query = Column(String(1), nullable=True, comment='是否查询字段(1是)') + query_type = Column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)') + html_type = Column( + String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type = Column(String(200), nullable=True, default='', comment='字典类型') + sort = Column(Integer, nullable=True, comment='排序') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + + tables = relationship('GenTable', back_populates='columns') diff --git a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py new file mode 100644 index 0000000..e5d7917 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py @@ -0,0 +1,264 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import List, Literal, Optional +from config.constant import GenConstant +from module_admin.annotation.pydantic_annotation import as_query +from utils.string_util import StringUtil + + +class GenTableBaseModel(BaseModel): + """ + 代码生成业务表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + table_id: Optional[int] = Field(default=None, description='编号') + table_name: Optional[str] = Field(default=None, description='表名称') + table_comment: Optional[str] = Field(default=None, description='表描述') + sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名') + sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名') + class_name: Optional[str] = Field(default=None, description='实体类名称') + tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)') + package_name: Optional[str] = Field(default=None, description='生成包路径') + module_name: Optional[str] = Field(default=None, description='生成模块名') + business_name: Optional[str] = Field(default=None, description='生成业务名') + function_name: Optional[str] = Field(default=None, description='生成功能名') + function_author: Optional[str] = Field(default=None, description='生成功能作者') + gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)') + gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)') + options: Optional[str] = Field(default=None, description='其它生成选项') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='table_name', message='表名称不能为空') + def get_table_name(self): + return self.table_name + + @NotBlank(field_name='table_comment', message='表描述不能为空') + def get_table_comment(self): + return self.table_comment + + @NotBlank(field_name='class_name', message='实体类名称不能为空') + def get_class_name(self): + return self.class_name + + @NotBlank(field_name='package_name', message='生成包路径不能为空') + def get_package_name(self): + return self.package_name + + @NotBlank(field_name='module_name', message='生成模块名不能为空') + def get_module_name(self): + return self.module_name + + @NotBlank(field_name='business_name', message='生成业务名不能为空') + def get_business_name(self): + return self.business_name + + @NotBlank(field_name='function_name', message='生成功能名不能为空') + def get_function_name(self): + return self.function_name + + @NotBlank(field_name='function_author', message='生成功能作者不能为空') + def get_function_author(self): + return self.function_author + + def validate_fields(self): + self.get_table_name() + self.get_table_comment() + self.get_class_name() + self.get_package_name() + self.get_module_name() + self.get_business_name() + self.get_function_name() + self.get_function_author() + + +class GenTableModel(GenTableBaseModel): + """ + 代码生成业务表模型 + """ + + pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息') + sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息') + columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息') + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段') + sub: Optional[bool] = Field(default=None, description='是否为子表') + tree: Optional[bool] = Field(default=None, description='是否为树表') + crud: Optional[bool] = Field(default=None, description='是否为单表') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False + self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False + self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False + return self + + +class EditGenTableModel(GenTableModel): + """ + 修改代码生成业务表模型 + """ + + params: Optional['GenTableParamsModel'] = Field(default=None, description='业务表参数') + + +class GenTableParamsModel(BaseModel): + """ + 代码生成业务表参数模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + + +class GenTableQueryModel(GenTableBaseModel): + """ + 代码生成业务表不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTablePageQueryModel(GenTableQueryModel): + """ + 代码生成业务表分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableModel(BaseModel): + """ + 删除代码生成业务表模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + table_ids: str = Field(description='需要删除的代码生成业务表ID') + + +class GenTableColumnBaseModel(BaseModel): + """ + 代码生成业务表字段对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + column_id: Optional[int] = Field(default=None, description='编号') + table_id: Optional[int] = Field(default=None, description='归属表编号') + column_name: Optional[str] = Field(default=None, description='列名称') + column_comment: Optional[str] = Field(default=None, description='列描述') + column_type: Optional[str] = Field(default=None, description='列类型') + python_type: Optional[str] = Field(default=None, description='PYTHON类型') + python_field: Optional[str] = Field(default=None, description='PYTHON字段名') + is_pk: Optional[str] = Field(default=None, description='是否主键(1是)') + is_increment: Optional[str] = Field(default=None, description='是否自增(1是)') + is_required: Optional[str] = Field(default=None, description='是否必填(1是)') + is_unique: Optional[str] = Field(default=None, description='是否唯一(1是)') + is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)') + is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)') + is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)') + is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)') + query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)') + html_type: Optional[str] = Field( + default=None, description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type: Optional[str] = Field(default=None, description='字典类型') + sort: Optional[int] = Field(default=None, description='排序') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + + @NotBlank(field_name='python_field', message='Python属性不能为空') + def get_python_field(self): + return self.python_field + + def validate_fields(self): + self.get_python_field() + + +class GenTableColumnModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段模型 + """ + + cap_python_field: Optional[str] = Field(default=None, description='字段大写形式') + pk: Optional[bool] = Field(default=None, description='是否主键') + increment: Optional[bool] = Field(default=None, description='是否自增') + required: Optional[bool] = Field(default=None, description='是否必填') + unique: Optional[bool] = Field(default=None, description='是否唯一') + insert: Optional[bool] = Field(default=None, description='是否为插入字段') + edit: Optional[bool] = Field(default=None, description='是否编辑字段') + list: Optional[bool] = Field(default=None, description='是否列表字段') + query: Optional[bool] = Field(default=None, description='是否查询字段') + super_column: Optional[bool] = Field(default=None, description='是否为基类字段') + usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None + self.pk = True if self.is_pk and self.is_pk == '1' else False + self.increment = True if self.is_increment and self.is_increment == '1' else False + self.required = True if self.is_required and self.is_required == '1' else False + self.unique = True if self.is_unique and self.is_unique == '1' else False + self.insert = True if self.is_insert and self.is_insert == '1' else False + self.edit = True if self.is_edit and self.is_edit == '1' else False + self.list = True if self.is_list and self.is_list == '1' else False + self.query = True if self.is_query and self.is_query == '1' else False + self.super_column = ( + True + if StringUtil.equals_any_ignore_case(self.python_field, GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY) + else False + ) + self.usable_column = ( + True if StringUtil.equals_any_ignore_case(self.python_field, ['parentId', 'orderNum', 'remark']) else False + ) + return self + + +class GenTableColumnQueryModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTableColumnPageQueryModel(GenTableColumnQueryModel): + """ + 代码生成业务表字段分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableColumnModel(BaseModel): + """ + 删除代码生成业务表字段模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + column_ids: str = Field(description='需要删除的代码生成业务表字段ID') diff --git a/ruoyi-fastapi-backend/module_generator/service/gen_service.py b/ruoyi-fastapi-backend/module_generator/service/gen_service.py new file mode 100644 index 0000000..c22019d --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/service/gen_service.py @@ -0,0 +1,480 @@ +import io +import json +import os +import re +import zipfile +from datetime import datetime +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import GenConstant +from config.env import GenConfig +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import ( + DeleteGenTableModel, + EditGenTableModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from module_generator.dao.gen_dao import GenTableColumnDao, GenTableDao +from utils.common_util import CamelCaseUtil +from utils.gen_util import GenUtils +from utils.template_util import TemplateInitializer, TemplateUtils + + +class GenTableService: + """ + 代码生成业务表服务层 + """ + + @classmethod + async def get_gen_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取代码生成业务表列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成业务列表信息对象 + """ + gen_table_list_result = await GenTableDao.get_gen_table_list(query_db, query_object, is_page) + + return gen_table_list_result + + @classmethod + async def get_gen_db_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取数据库列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list(query_db, query_object, is_page) + + return gen_db_table_list_result + + @classmethod + async def get_gen_db_table_list_by_name_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 根据表名称组获取数据库列表信息service + + :param query_db: orm对象 + :param table_names: 表名称组 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list_by_names(query_db, table_names) + + return [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)] + + @classmethod + async def import_gen_table_services( + cls, query_db: AsyncSession, gen_table_list: List[GenTableModel], current_user: CurrentUserModel + ): + """ + 导入表结构service + + :param query_db: orm对象 + :param gen_table_list: 导入表列表 + :param current_user: 当前用户信息对象 + :return: 导入结果 + """ + try: + for table in gen_table_list: + table_name = table.table_name + GenUtils.init_table(table, current_user.user.user_name) + add_gen_table = await GenTableDao.add_gen_table_dao(query_db, table) + if add_gen_table: + table.table_id = add_gen_table.table_id + gen_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + for column in [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns) + ]: + GenUtils.init_column_field(column, table) + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='导入成功') + except Exception as e: + await query_db.rollback() + raise ServiceException(message=f'导入失败, {str(e)}') + + @classmethod + async def edit_gen_table_services(cls, query_db: AsyncSession, page_object: EditGenTableModel): + """ + 编辑业务表信息service + + :param query_db: orm对象 + :param page_object: 编辑业务表对象 + :return: 编辑业务表校验结果 + """ + edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True) + gen_table_info = await cls.get_gen_table_by_id_services(query_db, page_object.table_id) + if gen_table_info.table_id: + try: + edit_gen_table['options'] = json.dumps(edit_gen_table.get('params')) + await GenTableDao.edit_gen_table_dao(query_db, edit_gen_table) + for gen_table_column in page_object.columns: + gen_table_column.update_by = page_object.update_by + gen_table_column.update_time = datetime.now() + await GenTableColumnDao.edit_gen_table_column_dao( + query_db, gen_table_column.model_dump(by_alias=True) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='业务表不存在') + + @classmethod + async def delete_gen_table_services(cls, query_db: AsyncSession, page_object: DeleteGenTableModel): + """ + 删除业务表信息service + + :param query_db: orm对象 + :param page_object: 删除业务表对象 + :return: 删除业务表校验结果 + """ + if page_object.table_ids: + table_id_list = page_object.table_ids.split(',') + try: + for table_id in table_id_list: + await GenTableDao.delete_gen_table_dao(query_db, GenTableModel(tableId=table_id)) + await GenTableColumnDao.delete_gen_table_column_by_table_id_dao( + query_db, GenTableColumnModel(tableId=table_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入业务表id为空') + + @classmethod + async def get_gen_table_by_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取需要生成的业务表详细信息service + + :param query_db: orm对象 + :param table_id: 需要生成的业务表id + :return: 需要生成的业务表id对应的信息 + """ + gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id) + result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) + + return result + + @classmethod + async def get_gen_table_all_services(cls, query_db: AsyncSession): + """ + 获取所有业务表信息service + + :param query_db: orm对象 + :return: 所有业务表信息 + """ + gen_table_all = await GenTableDao.get_gen_table_all(query_db) + result = [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)] + + return result + + @classmethod + async def create_table_services(cls, query_db: AsyncSession, sql: str, current_user: CurrentUserModel): + """ + 创建表结构service + + :param query_db: orm对象 + :param sql: 建表语句 + :param current_user: 当前用户信息对象 + :return: 创建表结构结果 + """ + if cls.__is_valid_create_table(sql): + try: + table_names = re.findall(r'create\s+table\s+(\w+)', sql, re.IGNORECASE) + await GenTableDao.create_table_by_sql_dao(query_db, sql) + gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names) + await cls.import_gen_table_services(query_db, gen_table_list, current_user) + + return CrudResponseModel(is_success=True, message='创建表结构成功') + except Exception as e: + raise ServiceException(message=f'创建表结构异常,详细错误信息:{str(e)}') + else: + raise ServiceException(message='建表语句不合法') + + @classmethod + def __is_valid_create_table(cls, sql: str): + """ + 校验sql语句是否为合法的建表语句 + + :param sql: sql语句 + :return: 校验结果 + """ + create_table_pattern = r'^\s*CREATE\s+TABLE\s+' + if not re.search(create_table_pattern, sql, re.IGNORECASE): + return False + forbidden_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE'] + for keyword in forbidden_keywords: + if re.search(rf'\b{keyword}\b', sql, re.IGNORECASE): + return False + return True + + @classmethod + async def preview_code_services(cls, query_db: AsyncSession, table_id: int): + """ + 预览代码service + + :param query_db: orm对象 + :param table_id: 业务表id + :return: 预览数据列表 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_id(query_db, table_id)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + env = TemplateInitializer.init_jinja2() + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + preview_code_result = {} + for template in template_list: + render_content = env.get_template(template).render(**context) + preview_code_result[template] = render_content + return preview_code_result + + @classmethod + async def generate_code_services(cls, query_db: AsyncSession, table_name: str): + """ + 生成代码至指定路径service + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 生成代码结果 + """ + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template in render_info[0]: + try: + render_content = env.get_template(template).render(**render_info[2]) + gen_path = cls.__get_gen_path(render_info[3], template) + os.makedirs(os.path.dirname(gen_path), exist_ok=True) + with open(gen_path, 'w', encoding='utf-8') as f: + f.write(render_content) + except Exception as e: + raise ServiceException( + message=f'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}' + ) + + return CrudResponseModel(is_success=True, message='生成代码成功') + + @classmethod + async def batch_gen_code_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 批量生成代码service + + :param query_db: orm对象 + :param table_names: 业务表名称组 + :return: 下载代码结果 + """ + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for table_name in table_names: + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template_file, output_file in zip(render_info[0], render_info[1]): + render_content = env.get_template(template_file).render(**render_info[2]) + zip_file.writestr(output_file, render_content) + + zip_data = zip_buffer.getvalue() + zip_buffer.close() + return zip_data + + @classmethod + async def __get_gen_render_info(cls, query_db: AsyncSession, table_name: str): + """ + 获取生成代码渲染模板相关信息 + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 生成代码渲染模板相关信息 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_name(query_db, table_name)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + output_files = [TemplateUtils.get_file_name(template, gen_table) for template in template_list] + + return [template_list, output_files, context, gen_table] + + @classmethod + def __get_gen_path(cls, gen_table: GenTableModel, template: str): + """ + 根据GenTableModel对象和模板名称生成路径 + + :param gen_table: GenTableModel对象 + :param template: 模板名称 + :return: 生成的路径 + """ + gen_path = gen_table.gen_path + if gen_path == '/': + return os.path.join(os.getcwd(), GenConfig.GEN_PATH, TemplateUtils.get_file_name(template, gen_table)) + else: + return os.path.join(gen_path, TemplateUtils.get_file_name(template, gen_table)) + + @classmethod + async def sync_db_services(cls, query_db: AsyncSession, table_name: str): + """ + 同步数据库service + + :param query_db: orm对象 + :param table_name: 业务表名称 + :return: 同步数据库结果 + """ + gen_table = await GenTableDao.get_gen_table_by_name(query_db, table_name) + table = GenTableModel(**CamelCaseUtil.transform_result(gen_table)) + table_columns = table.columns + table_column_map = {column.column_name: column for column in table_columns} + query_db_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + db_table_columns = [ + GenTableColumnModel(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns) + ] + if not db_table_columns: + raise ServiceException('同步数据失败,原表结构不存在') + db_table_column_names = [column.column_name for column in db_table_columns] + try: + for column in db_table_columns: + GenUtils.init_column_field(column, table) + if column.column_name in table_column_map: + prev_column = table_column_map[column.column_name] + column.column_id = prev_column.column_id + if column.list: + column.dict_type = prev_column.dict_type + column.query_type = prev_column.query_type + if ( + prev_column.is_required != '' + and not column.pk + and (column.insert or column.edit) + and (column.usable_column or column.super_column) + ): + column.is_required = prev_column.is_required + column.html_type = prev_column.html_type + await GenTableColumnDao.edit_gen_table_column_dao(query_db, column.model_dump(by_alias=True)) + else: + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] + if del_columns: + for column in del_columns: + await GenTableColumnDao.delete_gen_table_column_by_column_id_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='同步成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def set_sub_table(cls, query_db: AsyncSession, gen_table: GenTableModel): + """ + 设置主子表信息 + + :param query_db: orm对象 + :param gen_table: 业务表信息 + :return: + """ + if gen_table.sub_table_name: + sub_table = await GenTableDao.get_gen_table_by_name(query_db, gen_table.sub_table_name) + gen_table.sub_table = GenTableModel(**CamelCaseUtil.transform_result(sub_table)) + + @classmethod + async def set_pk_column(cls, gen_table: GenTableModel): + """ + 设置主键列信息 + + :param gen_table: 业务表信息 + :return: + """ + for column in gen_table.columns: + if column.pk: + gen_table.pk_column = column + break + if gen_table.pk_column is None: + gen_table.pk_column = gen_table.columns[0] + if gen_table.tpl_category == GenConstant.TPL_SUB: + for column in gen_table.sub_table.columns: + if column.pk: + gen_table.sub_table.pk_column = column + break + if gen_table.sub_table.columns is None: + gen_table.sub_table.pk_column = gen_table.sub_table.columns[0] + + @classmethod + async def set_table_from_options(cls, gen_table: GenTableModel): + """ + 设置代码生成其他选项值 + + :param gen_table: 生成对象 + :return: 设置后的生成对象 + """ + params_obj = json.loads(gen_table.options) if gen_table.options else None + if params_obj: + gen_table.tree_code = params_obj.get(GenConstant.TREE_CODE) + gen_table.tree_parent_code = params_obj.get(GenConstant.TREE_PARENT_CODE) + gen_table.tree_name = params_obj.get(GenConstant.TREE_NAME) + gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID) + gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME) + + return gen_table + + @classmethod + async def validate_edit(cls, edit_gen_table: EditGenTableModel): + """ + 编辑保存参数校验 + + :param edit_gen_table: 编辑业务表对象 + """ + if edit_gen_table.tpl_category == GenConstant.TPL_TREE: + params_obj = edit_gen_table.params.model_dump(by_alias=True) + + if GenConstant.TREE_CODE not in params_obj: + raise ServiceException(message='树编码字段不能为空') + elif GenConstant.TREE_PARENT_CODE not in params_obj: + raise ServiceException(message='树父编码字段不能为空') + elif GenConstant.TREE_NAME not in params_obj: + raise ServiceException(message='树名称字段不能为空') + elif edit_gen_table.tpl_category == GenConstant.TPL_SUB: + if not edit_gen_table.sub_table_name: + raise ServiceException(message='关联子表的表名不能为空') + elif not edit_gen_table.sub_table_fk_name: + raise ServiceException(message='子表关联的外键名不能为空') + + +class GenTableColumnService: + """ + 代码生成业务表字段服务层 + """ + + @classmethod + async def get_gen_table_column_list_by_table_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取业务表字段列表信息service + + :param query_db: orm对象 + :param table_id: 业务表格id + :return: 业务表字段列表信息对象 + """ + gen_table_column_list_result = await GenTableColumnDao.get_gen_table_column_list_by_table_id(query_db, table_id) + + return [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result) + ] diff --git a/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 new file mode 100644 index 0000000..3a2a5a9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询{{ functionName }}列表 +export function list{{ BusinessName }}(query) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/list', + method: 'get', + params: query + }) +} + +// 查询{{ functionName }}详细 +export function get{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'get' + }) +} + +// 新增{{ functionName }} +export function add{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'post', + data: data + }) +} + +// 修改{{ functionName }} +export function update{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'put', + data: data + }) +} + +// 删除{{ functionName }} +export function del{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'delete' + }) +} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 new file mode 100644 index 0000000..4d5a12e --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 @@ -0,0 +1,125 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% for column in columns %} +{% if column.python_field == "createTime" %} +from datetime import datetime +{% endif %} +{% endfor %} +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_admin.service.login_service import LoginService +from {{ packageName }}.service.{{ businessName }}_service import {{ BusinessName }}Service +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +{{ businessName }}Controller = APIRouter(prefix='/{{ moduleName }}/{{ businessName }}', dependencies=[Depends(LoginService.get_current_user)]) + + +@{{ businessName }}Controller.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:list'))] +) +async def get_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {% if table.crud or table.sub %}{{ businessName }}_page_query{% elif table.tree %}{{ businessName }}_query{% endif %}: {{ BusinessName }}PageQueryModel = Depends({{ BusinessName }}PageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + {% if table.crud or table.sub %} + # 获取分页数据 + {{ businessName }}_page_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content={{ businessName }}_page_query_result) + {% elif table.tree %} + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_query) + logger.info('获取成功') + + return ResponseUtil.success(data={{ businessName }}_query_result) + {% endif %} + + +@{{ businessName }}Controller.post('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:add'))]) +@ValidateFields(validate_model='add_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.INSERT) +async def add_{{ moduleName }}_{{ businessName }}( + request: Request, + add_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + {% for column in columns %} + {% if column.python_field == "createBy" %} + add_{{ businessName }}.create_by = current_user.user.user_name + {% elif column.python_field == "createTime" %} + add_{{ businessName }}.create_time = datetime.now() + {% elif column.python_field == "updateBy" %} + add_{{ businessName }}.update_by = current_user.user.user_name + {% elif column.python_field == "updateTime" %} + add_{{ businessName }}.update_time = datetime.now() + {% endif %} + {% endfor %} + add_{{ businessName }}_result = await {{ BusinessName }}Service.add_{{ businessName }}_services(query_db, add_{{ businessName }}) + logger.info(add_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=add_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.put('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:edit'))]) +@ValidateFields(validate_model='edit_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.UPDATE) +async def edit_{{ moduleName }}_{{ businessName }}( + request: Request, + edit_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_{{ businessName }}.update_by = current_user.user.user_name + edit_{{ businessName }}.update_time = datetime.now() + edit_{{ businessName }}_result = await {{ BusinessName }}Service.edit_{{ businessName }}_services(query_db, edit_{{ businessName }}) + logger.info(edit_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=edit_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.delete('/{% raw %}{{% endraw %}{{ pk_field }}s{% raw %}}{% endraw %}', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:remove'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.DELETE) +async def delete_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}s: str, query_db: AsyncSession = Depends(get_db)): + delete_{{ businessName }} = Delete{{ BusinessName }}Model({{ pkField }}s={{ pk_field }}s) + delete_{{ businessName }}_result = await {{ BusinessName }}Service.delete_{{ businessName }}_services(query_db, delete_{{ businessName }}) + logger.info(delete_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=delete_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.get( + '/{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}', response_model={{ BusinessName }}Model, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:query'))] +) +async def query_detail_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}: int, query_db: AsyncSession = Depends(get_db)): + {{ businessName }}_detail_result = await {{ BusinessName }}Service.{{ businessName }}_detail_services(query_db, {{ pk_field }}) + logger.info(f'获取{{ pk_field }}为{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}的信息成功') + + return ResponseUtil.success(data={{ businessName }}_detail_result) + + +@{{ businessName }}Controller.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:export'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.EXPORT) +async def export_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {{ businessName }}_page_query: {{ BusinessName }}PageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=False) + {{ businessName }}_export_result = await {{ BusinessName }}Service.export_{{ businessName }}_list_services({% if dicts %}request, {% endif %}{{ businessName }}_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response({{ businessName }}_export_result)) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 new file mode 100644 index 0000000..cb9ba81 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -0,0 +1,213 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% for column in columns %} +{% if column.query and column.query_type == 'BETWEEN' and column.python_field == "createTime" %} +from datetime import datetime, time +{% endif %} +{% endfor %} +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +{% if table.sub %} +from sqlalchemy.orm import selectinload +{% endif %} +{% if table.sub %} +from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }}, {{ subClassName }} +from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel, {{ subTable.business_name | capitalize }}Model +{% else %} +from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }} +from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +{% endif %} +from utils.page_util import PageUtil + + +class {{ BusinessName }}Dao: + """ + {{ functionName }}模块数据库操作层 + """ + + @classmethod + async def get_{{ businessName }}_detail_by_id(cls, db: AsyncSession, {{ pk_field }}: int): + """ + 根据{{ pk_field_comment }}获取{{ functionName }}详细信息 + + :param db: orm对象 + :param {{ pk_field }}: {{ pk_field_comment }} + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = ( + ( + await db.execute( + {% if table.sub %} + select({{ ClassName }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) + {% else %} + select({{ ClassName }}) + {% endif %} + .where( + {{ ClassName }}.{{ pk_field }} == {{ pk_field }} + ) + ) + ) + .scalars() + .first() + ) + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_detail_by_info(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 根据{{ functionName }}参数获取{{ functionName }}信息 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}参数对象 + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = ( + ( + await db.execute( + select({{ ClassName }}).where( + {% for column in columns %} + {% if column.required %} + {{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True, + {% endif %} + {% endfor %} + ) + ) + ) + .scalars() + .first() + ) + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_list(cls, db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False): + """ + 根据查询参数获取{{ functionName }}列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + query = ( + {% if table.sub %} + select({{ ClassName }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) + {% else %} + select({{ ClassName }}) + {% endif %} + .where( + {% for column in columns %} + {% set field = column.python_field | camel_to_snake %} + {% if column.query %} + {% if column.query_type == "EQ" %} + {{ ClassName }}.{{ field }} == query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "NE" %} + {{ ClassName }}.{{ field }} != query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GT" %} + {{ ClassName }}.{{ field }} > query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GTE" %} + {{ ClassName }}.{{ field }} >= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LT" %} + {{ ClassName }}.{{ field }} < query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LTE" %} + {{ ClassName }}.{{ field }} <= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LIKE" %} + {{ ClassName }}.{{ field }}.like(f'%{% raw %}{{% endraw %}query_object.{{ field }}{% raw %}}{% endraw %}%') if query_object.{{ field }} else True, + {% elif column.query_type == "BETWEEN" %} + {{ ClassName }}.{{ field }}.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + {% endif %} + {% endif %} + {% endfor %} + ) + .order_by({{ ClassName }}.{{ pk_field }}) + .distinct() + ) + {{ businessName }}_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return {{ businessName }}_list + + @classmethod + async def add_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 新增{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}对象 + :return: + """ + db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump(exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.insert %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %})) + db.add(db_{{ businessName }}) + await db.flush() + + return db_{{ businessName }} + + @classmethod + async def edit_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: dict): + """ + 编辑{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: 需要更新的{{ functionName }}字典 + :return: + """ + await db.execute(update({{ ClassName }}), [{{ businessName }}]) + + @classmethod + async def delete_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 删除{{ functionName }}数据库操作 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}对象 + :return: + """ + await db.execute(delete({{ ClassName }}).where({{ ClassName }}.{{ pk_field }}.in_([{{ businessName }}.{{ pk_field }}]))) + + {% if table.sub %} + @classmethod + async def add_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 新增{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + db_{{ subTable.business_name }} = {{ subClassName }}(**{{ subTable.business_name }}.model_dump()) + db.add(db_{{ subTable.business_name }}) + await db.flush() + + return db_{{ subTable.business_name }} + + @classmethod + async def edit_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: dict): + """ + 编辑{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: 需要更新的{{ subTable.function_name }}字典 + :return: + """ + await db.execute(update({{ subClassName }}), [{{ subTable.business_name }}]) + + @classmethod + async def delete_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 删除{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + await db.execute(delete({{ subClassName }}).where({{ subClassName }}.{{ subTable.pk_column.python_field | camel_to_snake }}.in_([{{ subTable.business_name }}.{{ subTable.pk_column.python_field | camel_to_snake }}]))) + {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 new file mode 100644 index 0000000..22c23c4 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -0,0 +1,41 @@ +{% for do_import in doImportList %} +{{ do_import }} +{% endfor %} +{% if table.sub %} +from sqlalchemy.orm import relationship +{% endif %} +from config.database import Base + + +class {{ ClassName }}(Base): + """ + {{ functionName }}表 + """ + + __tablename__ = '{{ tableName }}' + + {% for column in columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ subclassName }}_list = relationship('{{ subClassName }}', back_populates='{{ businessName }}') + {% endif %} + + +{% if table.sub %} +class {{ subClassName }}(Base): + """ + {{ subTable.function_name }}表 + """ + + __tablename__ = '{{ subTableName }}' + + {% for column in subTable.columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.column_name == subTableFkName %}ForeignKey('{{ tableName }}.{{ subTableFkName }}'), {% endif %}{% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ subclassName }}_list') + {% endif %} +{% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 new file mode 100644 index 0000000..27d6979 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -0,0 +1,211 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% if dicts %} +from fastapi import Request +{% endif %} +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +{% if dicts %} +from module_admin.service.dict_service import DictDataService +{% endif %} +from {{ packageName }}.dao.{{ businessName }}_dao import {{ BusinessName }}Dao +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class {{ BusinessName }}Service: + """ + {{ functionName }}模块服务层 + """ + + @classmethod + async def get_{{ businessName }}_list_services( + cls, query_db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False + ): + """ + 获取{{ functionName }}列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + {{ businessName }}_list_result = await {{ BusinessName }}Dao.get_{{ businessName }}_list(query_db, query_object, is_page) + + return {{ businessName }}_list_result + + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + @classmethod + async def check_{{ column.python_field | camel_to_snake }}_unique_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 检查{{ comment }}是否唯一service + + :param query_db: orm对象 + :param page_object: {{ functionName }}对象 + :return: 校验结果 + """ + {{ pk_field }} = -1 if page_object.{{ pk_field }} is None else page_object.{{ pk_field }} + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_info(query_db, {{ BusinessName }}Model({{ column.python_field }}=page_object.{{ column.python_field | camel_to_snake }})) + if {{ businessName }} and {{ businessName }}.{{ pk_field }} != {{ pk_field }}: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + @classmethod + async def add_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 新增{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 新增{{ functionName }}对象 + :return: 新增{{ functionName }}校验结果 + """ + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): + raise ServiceException(message=f'新增{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + {% if table.sub %} + add_{{ businessName }} = await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + if add_{{ businessName }}: + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.add_{{ subTable.business_name }}_dao(query_db, sub_table) + {% else %} + await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 编辑{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 编辑{{ functionName }}对象 + :return: 编辑{{ functionName }}校验结果 + """ + edit_{{ businessName }} = page_object.model_dump(exclude_unset=True, exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.edit and not column.pk %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %}) + {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }}) + if {{ businessName }}_info.{{ pk_field }}: + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.unique %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): + raise ServiceException(message=f'修改{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + await {{ BusinessName }}Dao.edit_{{ businessName }}_dao(query_db, edit_{{ businessName }}) + {% if table.sub %} + for sub_table in {{ businessName }}_info.{{ subclassName }}_list: + await {{ BusinessName }}Dao.delete_{{ subTable.business_name }}_dao(query_db, sub_table) + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.add_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='{{ functionName }}不存在') + + @classmethod + async def delete_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: Delete{{ BusinessName }}Model): + """ + 删除{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 删除{{ functionName }}对象 + :return: 删除{{ functionName }}校验结果 + """ + if page_object.{{ pk_field }}s: + {{ pk_field }}_list = page_object.{{ pk_field }}s.split(',') + try: + for {{ pk_field }} in {{ pk_field }}_list: + {% if table.sub %} + {{ businessName }} = await cls.{{ businessName }}_detail_services(query_db, int({{ pk_field }})) + for sub_table in {{ businessName }}.{{ subclassName }}_list: + await {{ BusinessName }}Dao.delete_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} + await {{ BusinessName }}Dao.delete_{{ businessName }}_dao(query_db, {{ BusinessName }}Model({{ pkField }}={{ pk_field }})) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入{{ pk_field_comment }}为空') + + @classmethod + async def {{ businessName }}_detail_services(cls, query_db: AsyncSession, {{ pk_field }}: int): + """ + 获取{{ functionName }}详细信息service + + :param query_db: orm对象 + :param {{ pk_field }}: {{ pk_field_comment }} + :return: {{ pk_field_comment }}对应的信息 + """ + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_id(query_db, {{ pk_field }}={{ pk_field }}) + if {{ businessName }}: + result = {{ BusinessName }}Model(**CamelCaseUtil.transform_result({{ businessName }})) + else: + result = {{ BusinessName }}Model(**dict()) + + return result + + @staticmethod + async def export_{{ businessName }}_list_services({% if dicts %}request: Request, {% endif %}{{ businessName }}_list: List): + """ + 导出{{ functionName }}信息service + + :param {{ businessName }}_list: {{ functionName }}信息列表 + :return: {{ functionName }}信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + '{{ column.python_field }}': '{{ comment }}', + {% endfor %} + } + {% if dicts %} + {% for dict_type in dicts.split(", ") %} + {{ dict_type[1:-1] }}_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type={{ dict_type }} + ) + {{ dict_type[1:-1] }}_option = [dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in {{ dict_type[1:-1] }}_list] + {{ dict_type[1:-1] }}_option_dict = {item.get('value'): item for item in {{ dict_type[1:-1] }}_option} + {% endfor %} + for item in {{ businessName }}_list: + {% for column in columns %} + {% if column.dict_type %} + if str(item.get('{{ column.python_field }}')) in {{ column.dict_type }}_option_dict.keys(): + item['{{ column.python_field }}'] = {{ column.dict_type }}_option_dict.get(str(item.get('{{ column.python_field }}'))).get('label') + {% endif %} + {% endfor %} + {% endif %} + binary_data = ExcelUtil.export_list2excel({{ businessName }}_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 new file mode 100644 index 0000000..2b8d5b9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 @@ -0,0 +1,167 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% set vo_field_required = namespace(has_required=False) %} +{% for column in columns %} +{% if column.required %} + {% set vo_field_required.has_required = True %} +{% endif %} +{% endfor %} +{% if table.sub %} +{% set sub_vo_field_required = namespace(has_required=False) %} +{% for sub_column in subTable.columns %} +{% if sub_column.required %} + {% set sub_vo_field_required.has_required = True %} +{% endif %} +{% endfor %} +{% endif %} +{% for vo_import in voImportList %} +{{ vo_import }} +{% endfor %} +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +{% if vo_field_required.has_required %} +from pydantic_validation_decorator import NotBlank +{% endif %} +{% if table.sub %} +from typing import List, Optional +{% else %} +from typing import Optional +{% endif %} +from module_admin.annotation.pydantic_annotation import as_query + + +{% if table.sub %} +class {{ BusinessName }}BaseModel(BaseModel): + """ + {{ functionName }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for column in columns %} + {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') + {% endfor %} + + {% for column in columns %} + {% if column.required %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + @NotBlank(field_name='{{ column.column_name }}', message='{{ comment }}不能为空') + def get_{{ column.column_name }}(self): + return self.{{ column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if vo_field_required.has_required %} + def validate_fields(self): + {% for column in columns %} + {% if column.required %} + self.get_{{ column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + + +class {{ BusinessName }}Model({% if table.sub %}{{ BusinessName }}BaseModel{% else %}BaseModel{% endif %}): + """ + {{ functionName }}表对应pydantic模型 + """ + {% if not table.sub %} + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for column in columns %} + {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') + {% endfor %} + {% endif %} + {% if table.sub %} + {{ subclassName }}_list: Optional[List['{{ subTable.business_name | capitalize }}Model']] = Field(default=None, description='子表列信息') + {% endif %} + + {% if not table.sub %} + {% for column in columns %} + {% if column.required %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + @NotBlank(field_name='{{ column.column_name }}', message='{{ comment }}不能为空') + def get_{{ column.column_name }}(self): + return self.{{ column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if vo_field_required.has_required %} + def validate_fields(self): + {% for column in columns %} + {% if column.required %} + self.get_{{ column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} + {% endif %} + + +{% if table.sub %} +class {{ subTable.business_name | capitalize }}Model(BaseModel): + """ + {{ subTable.function_name }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for sub_column in subTable.columns %} + {{ sub_column.column_name }}: Optional[{{ sub_column.python_type }}] = Field(default=None, description='{{ sub_column.column_comment}}') + {% endfor %} + + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + {% set parentheseIndex = sub_column.column_comment.find("(") %} + {% set comment = sub_column.column_comment[:parentheseIndex] if parentheseIndex != -1 else sub_column.column_comment %} + @NotBlank(field_name='{{ sub_column.column_name }}', message='{{ comment }}不能为空') + def get_{{ sub_column.column_name }}(self): + return self.{{ sub_column.column_name }} + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + {% if sub_vo_field_required.has_required %} + def validate_fields(self): + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + self.get_{{ sub_column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + + +class {{ BusinessName }}QueryModel({% if table.sub %}{{ BusinessName }}BaseModel{% else %}{{ BusinessName }}Model{% endif %}): + """ + {{ functionName }}不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class {{ BusinessName }}PageQueryModel({{ BusinessName }}QueryModel): + """ + {{ functionName }}分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class Delete{{ BusinessName }}Model(BaseModel): + """ + 删除{{ functionName }}模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + {{ pk_field }}s: str = Field(description='需要删除的{{ pk_field_comment }}') diff --git a/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 new file mode 100644 index 0000000..dd4099f --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 @@ -0,0 +1,47 @@ +{% if dbType == 'postgresql' %} +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}', '{{ parentMenuId }}', '1', '{{ businessName }}', '{{ moduleName }}/{{ businessName }}/index', 1, 0, 'C', '0', '0', '{{ permissionPrefix }}:list', '#', 'admin', current_timestamp, '', null, '{{ functionName }}菜单'); + +-- 按钮父菜单ID +select max(menu_id) from sys_menu; + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}查询', max(menu_id), '1', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:query', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}新增', max(menu_id), '2', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:add', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}修改', max(menu_id), '3', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:edit', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}删除', max(menu_id), '4', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:remove', '#', 'admin', current_timestamp, '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}导出', max(menu_id), '5', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:export', '#', 'admin', current_timestamp, '', null, ''); +{% else %} +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}', '{{ parentMenuId }}', '1', '{{ businessName }}', '{{ moduleName }}/{{ businessName }}/index', 1, 0, 'C', '0', '0', '{{ permissionPrefix }}:list', '#', 'admin', sysdate(), '', null, '{{ functionName }}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:export', '#', 'admin', sysdate(), '', null, ''); +{% endif %} \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 new file mode 100644 index 0000000..741f132 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 @@ -0,0 +1,491 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 new file mode 100644 index 0000000..673b5b0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 @@ -0,0 +1,586 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 new file mode 100644 index 0000000..433c162 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 @@ -0,0 +1,454 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 new file mode 100644 index 0000000..981124f --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 @@ -0,0 +1,571 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/server.py b/ruoyi-fastapi-backend/server.py index 00b4661..5c8ad9c 100644 --- a/ruoyi-fastapi-backend/server.py +++ b/ruoyi-fastapi-backend/server.py @@ -22,6 +22,7 @@ from module_admin.controller.post_controler import postController from module_admin.controller.role_controller import roleController from module_admin.controller.server_controller import serverController from module_admin.controller.user_controller import userController +from module_generator.controller.gen_controller import genController from sub_applications.handle import handle_sub_applications from utils.common_util import worship from utils.log_util import logger @@ -77,6 +78,7 @@ controller_list = [ {'router': serverController, 'tags': ['系统监控-菜单管理']}, {'router': cacheController, 'tags': ['系统监控-缓存监控']}, {'router': commonController, 'tags': ['通用模块']}, + {'router': genController, 'tags': ['代码生成']}, ] for controller in controller_list: diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql index 79b7767..b3731aa 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql @@ -909,15 +909,16 @@ comment on table gen_table is '代码生成业务表'; drop table if exists gen_table_column; create table gen_table_column ( column_id bigserial not null, - table_id varchar(64), + table_id bigint, column_name varchar(200), column_comment varchar(500), column_type varchar(100), - java_type varchar(500), - java_field varchar(200), + python_type varchar(500), + python_field varchar(200), is_pk char(1), is_increment char(1), is_required char(1), + is_unique char(1), is_insert char(1), is_edit char(1), is_list char(1), @@ -937,11 +938,12 @@ comment on column gen_table_column.table_id is '归属表编号'; comment on column gen_table_column.column_name is '列名称'; comment on column gen_table_column.column_comment is '列描述'; comment on column gen_table_column.column_type is '列类型'; -comment on column gen_table_column.java_type is 'JAVA类型'; -comment on column gen_table_column.java_field is 'JAVA字段名'; +comment on column gen_table_column.python_type is 'PYTHON类型'; +comment on column gen_table_column.python_field is 'PYTHON字段名'; comment on column gen_table_column.is_pk is '是否主键(1是)'; comment on column gen_table_column.is_increment is '是否自增(1是)'; comment on column gen_table_column.is_required is '是否必填(1是)'; +comment on column gen_table_column.is_unique is '是否唯一(1是)'; comment on column gen_table_column.is_insert is '是否为插入字段(1是)'; comment on column gen_table_column.is_edit is '是否编辑字段(1是)'; comment on column gen_table_column.is_list is '是否列表字段(1是)'; @@ -975,3 +977,71 @@ END; $BODY$ LANGUAGE plpgsql VOLATILE COST 100; + +create or replace view list_column as +SELECT c.relname AS table_name, + a.attname AS column_name, + d.description AS column_comment, + CASE + WHEN a.attnotnull AND con.conname IS NULL THEN '1' + ELSE '0' + END AS is_required, + CASE + WHEN con.conname IS NOT NULL THEN '1' + ELSE '0' + END AS is_pk, + a.attnum AS sort, + CASE + WHEN "position"(pg_get_expr(ad.adbin, ad.adrelid), ((c.relname::text || '_'::text) || a.attname + ::text) || '_seq'::text) > 0 THEN '1' + ELSE '0' + END AS is_increment, + btrim( + CASE + WHEN t.typelem <> 0::oid AND t.typlen = '-1'::integer THEN 'ARRAY'::text + ELSE + CASE + WHEN t.typtype = 'd'::"char" THEN format_type(t.typbasetype, NULL::integer) + ELSE format_type(a.atttypid, NULL::integer) + END + END, '"'::text) AS column_type +FROM pg_attribute a + JOIN (pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid) ON a.attrelid = c.oid + LEFT JOIN pg_description d ON d.objoid = c.oid AND a.attnum = d.objsubid + LEFT JOIN pg_constraint con ON con.conrelid = c.oid AND (a.attnum = ANY (con.conkey)) + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND a.attnum > 0 + AND n.nspname = 'public'::name + AND not a.attisdropped + ORDER BY c.relname, a.attnum; + +create or replace view list_table as +SELECT c.relname AS table_name, + obj_description(c.oid) AS table_comment, + CURRENT_TIMESTAMP AS create_time, + CURRENT_TIMESTAMP AS update_time +FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND c.relname !~~ 'spatial_%'::text AND n.nspname = 'public'::name AND n.nspname <> ''::name; + +CREATE OR REPLACE FUNCTION substring_index(varchar, varchar, integer) +RETURNS varchar AS $$ +DECLARE +tokens varchar[]; +length integer ; +indexnum integer; +BEGIN +tokens := pg_catalog.string_to_array($1, $2); +length := pg_catalog.array_upper(tokens, 1); +indexnum := length - ($3 * -1) + 1; +IF $3 >= 0 THEN +RETURN pg_catalog.array_to_string(tokens[1:$3], $2); +ELSE +RETURN pg_catalog.array_to_string(tokens[indexnum:length], $2); +END IF; +END; +$$ IMMUTABLE STRICT LANGUAGE PLPGSQL; diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql index ef5a8de..d0ce11a 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql @@ -691,11 +691,12 @@ create table gen_table_column ( column_name varchar(200) comment '列名称', column_comment varchar(500) comment '列描述', column_type varchar(100) comment '列类型', - java_type varchar(500) comment 'JAVA类型', - java_field varchar(200) comment 'JAVA字段名', + python_type varchar(500) comment 'PYTHON类型', + python_field varchar(200) comment 'PYTHON字段名', is_pk char(1) comment '是否主键(1是)', is_increment char(1) comment '是否自增(1是)', is_required char(1) comment '是否必填(1是)', + is_unique char(1) comment '是否唯一(1是)', is_insert char(1) comment '是否为插入字段(1是)', is_edit char(1) comment '是否编辑字段(1是)', is_list char(1) comment '是否列表字段(1是)', diff --git a/ruoyi-fastapi-backend/utils/common_util.py b/ruoyi-fastapi-backend/utils/common_util.py index 86f502e..b56000e 100644 --- a/ruoyi-fastapi-backend/utils/common_util.py +++ b/ruoyi-fastapi-backend/utils/common_util.py @@ -7,6 +7,7 @@ from openpyxl.styles import Alignment, PatternFill from openpyxl.utils import get_column_letter from openpyxl.worksheet.datavalidation import DataValidation from sqlalchemy.engine.row import Row +from sqlalchemy.orm.collections import InstrumentedList from typing import Any, Dict, List, Literal, Union from config.database import Base from config.env import CachePathConfig @@ -58,6 +59,9 @@ class SqlalchemyUtil: if isinstance(obj, Base): base_dict = obj.__dict__.copy() base_dict.pop('_sa_instance_state', None) + for name, value in base_dict.items(): + if isinstance(value, InstrumentedList): + base_dict[name] = cls.serialize_result(value, 'snake_to_camel') elif isinstance(obj, dict): base_dict = obj.copy() if transform_case == 'snake_to_camel': diff --git a/ruoyi-fastapi-backend/utils/gen_util.py b/ruoyi-fastapi-backend/utils/gen_util.py new file mode 100644 index 0000000..355e5d0 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/gen_util.py @@ -0,0 +1,223 @@ +import re +from datetime import datetime +from typing import List +from config.constant import GenConstant +from config.env import GenConfig +from module_generator.entity.vo.gen_vo import GenTableColumnModel, GenTableModel +from utils.string_util import StringUtil + + +class GenUtils: + """代码生成器工具类""" + + @classmethod + def init_table(cls, gen_table: GenTableModel, oper_name: str) -> None: + """ + 初始化表信息 + + param gen_table: 业务表对象 + param oper_name: 操作人 + :return: + """ + gen_table.class_name = cls.convert_class_name(gen_table.table_name) + gen_table.package_name = GenConfig.package_name + gen_table.module_name = cls.get_module_name(GenConfig.package_name) + gen_table.business_name = cls.get_business_name(gen_table.table_name) + gen_table.function_name = cls.replace_text(gen_table.table_comment) + gen_table.function_author = GenConfig.author + gen_table.create_by = oper_name + gen_table.create_time = datetime.now() + gen_table.update_by = oper_name + gen_table.update_time = datetime.now() + + @classmethod + def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel) -> None: + """ + 初始化列属性字段 + + param column: 业务表字段对象 + param table: 业务表对象 + :return: + """ + data_type = cls.get_db_type(column.column_type) + column_name = column.column_name + column.table_id = table.table_id + column.create_by = table.create_by + # 设置Python字段名 + column.python_field = cls.to_camel_case(column_name) + # 设置默认类型 + column.python_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_PYTHON_TYPE_MAPPING, data_type + ) + column.query_type = GenConstant.QUERY_EQ + + if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains( + GenConstant.COLUMNTYPE_TEXT, data_type + ): + # 字符串长度超过500设置为文本域 + column_length = cls.get_column_length(column.column_type) + html_type = ( + GenConstant.HTML_TEXTAREA + if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type) + else GenConstant.HTML_INPUT + ) + column.html_type = html_type + elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type): + column.html_type = GenConstant.HTML_DATETIME + elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type): + column.html_type = GenConstant.HTML_INPUT + + # 插入字段(默认所有字段都需要插入) + column.is_insert = GenConstant.REQUIRE + + # 编辑字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.pk: + column.is_edit = GenConstant.REQUIRE + # 列表字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.pk: + column.is_list = GenConstant.REQUIRE + # 查询字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.pk: + column.is_query = GenConstant.REQUIRE + + # 查询字段类型 + if column_name.lower().endswith('name'): + column.query_type = GenConstant.QUERY_LIKE + # 状态字段设置单选框 + if column_name.lower().endswith('status'): + column.html_type = GenConstant.HTML_RADIO + # 类型&性别字段设置下拉框 + elif column_name.lower().endswith('type') or column_name.lower().endswith('sex'): + column.html_type = GenConstant.HTML_SELECT + # 图片字段设置图片上传控件 + elif column_name.lower().endswith('image'): + column.html_type = GenConstant.HTML_IMAGE_UPLOAD + # 文件字段设置文件上传控件 + elif column_name.lower().endswith('file'): + column.html_type = GenConstant.HTML_FILE_UPLOAD + # 内容字段设置富文本控件 + elif column_name.lower().endswith('content'): + column.html_type = GenConstant.HTML_EDITOR + + column.create_by = table.create_by + column.create_time = datetime.now() + column.update_by = table.update_by + column.update_time = datetime.now() + + @classmethod + def arrays_contains(cls, arr: List[str], target_value: str) -> bool: + """ + 校验数组是否包含指定值 + + param arr: 数组 + param target_value: 需要校验的值 + :return: 校验结果 + """ + return target_value in arr + + @classmethod + def get_module_name(cls, package_name: str) -> str: + """ + 获取模块名 + + param package_name: 包名 + :return: 模块名 + """ + return package_name.split('.')[-1] + + @classmethod + def get_business_name(cls, table_name: str) -> str: + """ + 获取业务名 + + param table_name: 业务表名 + :return: 业务名 + """ + return table_name.split('_')[-1] + + @classmethod + def convert_class_name(cls, table_name: str) -> str: + """ + 表名转换成Python类名 + + param table_name: 业务表名 + :return: Python类名 + """ + auto_remove_pre = GenConfig.auto_remove_pre + table_prefix = GenConfig.table_prefix + if auto_remove_pre and table_prefix: + search_list = table_prefix.split(',') + table_name = cls.replace_first(table_name, search_list) + return StringUtil.convert_to_camel_case(table_name) + + @classmethod + def replace_first(cls, replacement: str, search_list: List[str]) -> str: + """ + 批量替换前缀 + + param replacement: 需要被替换的字符串 + param search_list: 可替换的字符串列表 + :return: 替换后的字符串 + """ + for search_string in search_list: + if replacement.startswith(search_string): + return replacement.replace(search_string, '', 1) + return replacement + + @classmethod + def replace_text(cls, text: str) -> str: + """ + 关键字替换 + + param text: 需要被替换的字符串 + :return: 替换后的字符串 + """ + return re.sub(r'(?:表|若依)', '', text) + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def get_column_length(cls, column_type: str) -> int: + """ + 获取字段长度 + + param column_type: 字段类型 + :return: 字段长度 + """ + if '(' in column_type: + length = len(column_type.split('(')[1].split(')')[0]) + return length + return 0 + + @classmethod + def split_column_type(cls, column_type: str) -> List[str]: + """ + 拆分列类型 + + param column_type: 字段类型 + :return: 拆分结果 + """ + if '(' in column_type and ')' in column_type: + return column_type.split('(')[1].split(')')[0].split(',') + return [] + + @classmethod + def to_camel_case(cls, text: str) -> str: + """ + 将字符串转换为驼峰命名 + + param text: 需要转换的字符串 + :return: 驼峰命名 + """ + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py index 962afdc..7196bcf 100644 --- a/ruoyi-fastapi-backend/utils/string_util.py +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict, List from config.constant import CommonConstant @@ -36,6 +36,16 @@ class StringUtil: """ return string is None or len(string) == 0 + @classmethod + def is_not_empty(cls, string: str) -> bool: + """ + 校验字符串是否不是''和None + + :param string: 需要校验的字符串 + :return: 校验结果 + """ + return not cls.is_empty(string) + @classmethod def is_http(cls, link: str): """ @@ -49,7 +59,7 @@ class StringUtil: @classmethod def contains_ignore_case(cls, search_str: str, compare_str: str): """ - 查找指定字符串是否包含指定字符串同时串忽略大小写 + 查找指定字符串是否包含指定字符串同时忽略大小写 :param search_str: 查找的字符串 :param compare_str: 比对的字符串 @@ -62,15 +72,40 @@ class StringUtil: @classmethod def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): """ - 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时忽略大小写 :param search_str: 查找的字符串 :param compare_str_list: 比对的字符串列表 :return: 查找结果 """ if search_str and compare_str_list: - for compare_str in compare_str_list: - return cls.contains_ignore_case(search_str, compare_str) + return any([cls.contains_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) + return False + + @classmethod + def equals_ignore_case(cls, search_str: str, compare_str: str): + """ + 比较两个字符串是否相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 比较结果 + """ + if search_str and compare_str: + return search_str.lower() == compare_str.lower() + return False + + @classmethod + def equals_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): + """ + 比较指定字符串是否与指定字符串列表中的任意一个字符串相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 比较结果 + """ + if search_str and compare_str_list: + return any([cls.equals_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) return False @classmethod @@ -98,3 +133,38 @@ class StringUtil: if search_str and compare_str_list: return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list]) return False + + @classmethod + def convert_to_camel_case(cls, name: str) -> str: + """ + 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串 + + :param name: 转换前的下划线大写方式命名的字符串 + :return: 转换后的驼峰式命名的字符串 + """ + if not name: + return '' + if '_' not in name: + return name[0].upper() + name[1:] + parts = name.split('_') + result = [] + for part in parts: + if not part: + continue + result.append(part[0].upper() + part[1:].lower()) + return ''.join(result) + + @classmethod + def get_mapping_value_by_key_ignore_case(cls, mapping: Dict[str, str], key: str) -> str: + """ + 根据忽略大小写的键获取字典中的对应的值 + + param mapping: 字典 + param key: 字典的键 + :return: 字典键对应的值 + """ + for k, v in mapping.items(): + if key.lower() == k.lower(): + return v + + return '' diff --git a/ruoyi-fastapi-backend/utils/template_util.py b/ruoyi-fastapi-backend/utils/template_util.py new file mode 100644 index 0000000..55a8f87 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/template_util.py @@ -0,0 +1,468 @@ +import json +import os +from datetime import datetime +from jinja2 import Environment, FileSystemLoader +from typing import Dict, List, Set +from config.constant import GenConstant +from config.env import DataBaseConfig +from exceptions.exception import ServiceWarning +from module_generator.entity.vo.gen_vo import GenTableModel, GenTableColumnModel +from utils.common_util import CamelCaseUtil, SnakeCaseUtil +from utils.string_util import StringUtil + + +class TemplateInitializer: + """ + 模板引擎初始化类 + """ + + @classmethod + def init_jinja2(cls): + """ + 初始化 Jinja2 模板引擎 + + :return: Jinja2 环境对象 + """ + try: + template_dir = os.path.join(os.getcwd(), 'module_generator', 'templates') + env = Environment( + loader=FileSystemLoader(template_dir), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters.update( + { + 'camel_to_snake': SnakeCaseUtil.camel_to_snake, + 'snake_to_camel': CamelCaseUtil.snake_to_camel, + 'get_sqlalchemy_type': TemplateUtils.get_sqlalchemy_type, + } + ) + return env + except Exception as e: + raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}') + + +class TemplateUtils: + """ + 模板工具类 + """ + + # 项目路径 + FRONTEND_PROJECT_PATH = 'frontend' + BACKEND_PROJECT_PATH = 'backend' + DEFAULT_PARENT_MENU_ID = '3' + + @classmethod + def prepare_context(cls, gen_table: GenTableModel): + """ + 准备模板变量 + + :param gen_table: 生成表的配置信息 + :return: 模板上下文字典 + """ + if not gen_table.options: + raise ServiceWarning(message='请先完善生成配置信息') + class_name = gen_table.class_name + module_name = gen_table.module_name + business_name = gen_table.business_name + package_name = gen_table.package_name + tpl_category = gen_table.tpl_category + function_name = gen_table.function_name + + context = { + 'tplCategory': tpl_category, + 'tableName': gen_table.table_name, + 'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】', + 'ClassName': class_name, + 'className': class_name.lower(), + 'moduleName': module_name, + 'BusinessName': business_name.capitalize(), + 'businessName': business_name, + 'basePackage': cls.get_package_prefix(package_name), + 'packageName': package_name, + 'author': gen_table.function_author, + 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'pkColumn': gen_table.pk_column, + 'doImportList': cls.get_do_import_list(gen_table), + 'voImportList': cls.get_vo_import_list(gen_table), + 'permissionPrefix': cls.get_permission_prefix(module_name, business_name), + 'columns': gen_table.columns, + 'table': gen_table, + 'dicts': cls.get_dicts(gen_table), + 'dbType': DataBaseConfig.db_type, + } + + # 设置菜单、树形结构、子表的上下文 + cls.set_menu_context(context, gen_table) + if tpl_category == GenConstant.TPL_TREE: + cls.set_tree_context(context, gen_table) + if tpl_category == GenConstant.TPL_SUB: + cls.set_sub_context(context, gen_table) + + return context + + @classmethod + def set_menu_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置菜单上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + options = gen_table.options + params_obj = json.loads(options) + context['parentMenuId'] = cls.get_parent_menu_id(params_obj) + + @classmethod + def set_tree_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置树形结构上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + options = gen_table.options + params_obj = json.loads(options) + context['treeCode'] = cls.get_tree_code(params_obj) + context['treeParentCode'] = cls.get_tree_parent_code(params_obj) + context['treeName'] = cls.get_tree_name(params_obj) + context['expandColumn'] = cls.get_expand_column(gen_table) + + @classmethod + def set_sub_context(cls, context: Dict, gen_table: GenTableModel): + """ + 设置子表上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ + sub_table = gen_table.sub_table + sub_table_name = gen_table.sub_table_name + sub_table_fk_name = gen_table.sub_table_fk_name + sub_class_name = sub_table.class_name + sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name) + context['subTable'] = sub_table + context['subTableName'] = sub_table_name + context['subTableFkName'] = sub_table_fk_name + context['subTableFkClassName'] = sub_table_fk_class_name + context['subTableFkclassName'] = sub_table_fk_class_name.lower() + context['subClassName'] = sub_class_name + context['subclassName'] = sub_class_name.lower() + + @classmethod + def get_template_list(cls, tpl_category: str, tpl_web_type: str): + """ + 获取模板列表 + + :param tpl_category: 生成模板类型 + :param tpl_web_type: 前端类型 + :return: 模板列表 + """ + use_web_type = 'vue' + if tpl_web_type == 'element-plus': + use_web_type = 'vue/v3' + templates = [ + 'python/controller.py.jinja2', + 'python/dao.py.jinja2', + 'python/do.py.jinja2', + 'python/service.py.jinja2', + 'python/vo.py.jinja2', + 'sql/sql.jinja2', + 'js/api.js.jinja2', + ] + if tpl_category == GenConstant.TPL_CRUD: + templates.append(f'{use_web_type}/index.vue.jinja2') + elif tpl_category == GenConstant.TPL_TREE: + templates.append(f'{use_web_type}/index-tree.vue.jinja2') + elif tpl_category == GenConstant.TPL_SUB: + templates.append(f'{use_web_type}/index.vue.jinja2') + # templates.append('python/sub-domain.python.jinja2') + return templates + + @classmethod + def get_file_name(cls, template: List[str], gen_table: GenTableModel): + """ + 根据模板生成文件名 + + :param template: 模板列表 + :param gen_table: 生成表的配置信息 + :return: 模板生成文件名 + """ + package_name = gen_table.package_name + module_name = gen_table.module_name + business_name = gen_table.business_name + + vue_path = cls.FRONTEND_PROJECT_PATH + python_path = f'{cls.BACKEND_PROJECT_PATH}/{package_name.replace(".", "/")}' + + if 'controller.py.jinja2' in template: + return f'{python_path}/controller/{business_name}_controller.py' + elif 'dao.py.jinja2' in template: + return f'{python_path}/dao/{business_name}_dao.py' + elif 'do.py.jinja2' in template: + return f'{python_path}/entity/do/{business_name}_do.py' + elif 'service.py.jinja2' in template: + return f'{python_path}/service/{business_name}_service.py' + elif 'vo.py.jinja2' in template: + return f'{python_path}/entity/vo/{business_name}_vo.py' + elif 'sql.jinja2' in template: + return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql' + elif 'api.js.jinja2' in template: + return f'{vue_path}/api/{module_name}/{business_name}.js' + elif 'index.vue.jinja2' in template or 'index-tree.vue.jinja2' in template: + return f'{vue_path}/views/{module_name}/{business_name}/index.vue' + return '' + + @classmethod + def get_package_prefix(cls, package_name: str): + """ + 获取包前缀 + + :param package_name: 包名 + :return: 包前缀 + """ + return package_name[: package_name.rfind('.')] + + @classmethod + def get_vo_import_list(cls, gen_table: GenTableModel): + """ + 获取vo模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ + columns = gen_table.columns or [] + import_list = set() + for column in columns: + if column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {column.python_type}') + elif column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + if gen_table.sub: + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + if sub_column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {sub_column.python_type}') + elif sub_column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + return cls.merge_same_imports(list(import_list), 'from datetime import') + + @classmethod + def get_do_import_list(cls, gen_table: GenTableModel): + """ + 获取do模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ + columns = gen_table.columns or [] + import_list = set() + import_list.add('from sqlalchemy import Column') + for column in columns: + data_type = cls.get_db_type(column.column_type) + if data_type in GenConstant.COLUMNTYPE_GEOMETRY: + import_list.add('from geoalchemy2 import Geometry') + import_list.add( + f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}' + ) + if gen_table.sub: + import_list.add('from sqlalchemy import ForeignKey') + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + data_type = cls.get_db_type(sub_column.column_type) + import_list.add( + f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}' + ) + return cls.merge_same_imports(list(import_list), 'from sqlalchemy import') + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]: + """ + 合并相同的导入语句 + + :param imports: 导入语句列表 + :param import_start: 导入语句的起始字符串 + :return: 合并后的导入语句列表 + """ + merged_imports = [] + _imports = [] + for import_stmt in imports: + if import_stmt.startswith(import_start): + imported_items = import_stmt.split('import')[1].strip() + _imports.extend(imported_items.split(', ')) + else: + merged_imports.append(import_stmt) + + if _imports: + merged_datetime_import = f'{import_start} {", ".join(_imports)}' + merged_imports.append(merged_datetime_import) + + return merged_imports + + @classmethod + def get_dicts(cls, gen_table: GenTableModel): + """ + 获取字典列表 + + :param gen_table: 生成表的配置信息 + :return: 字典列表 + """ + columns = gen_table.columns or [] + dicts = set() + cls.add_dicts(dicts, columns) + if gen_table.sub_table is not None: + cls.add_dicts(dicts, gen_table.sub_table.columns) + return ', '.join(dicts) + + @classmethod + def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnModel]): + """ + 添加字典列表 + + :param dicts: 字典列表 + :param columns: 字段列表 + :return: 新的字典列表 + """ + for column in columns: + if ( + not column.super_column + and StringUtil.is_not_empty(column.dict_type) + and StringUtil.equals_any_ignore_case( + column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX] + ) + ): + dicts.add(f"'{column.dict_type}'") + + @classmethod + def get_permission_prefix(cls, module_name: str, business_name: str): + """ + 获取权限前缀 + + :param module_name: 模块名 + :param business_name: 业务名 + :return: 权限前缀 + """ + return f'{module_name}:{business_name}' + + @classmethod + def get_parent_menu_id(cls, params_obj: Dict): + """ + 获取上级菜单ID + + :param params_obj: 菜单参数字典 + :return: 上级菜单ID + """ + if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID): + return params_obj.get(GenConstant.PARENT_MENU_ID) + return cls.DEFAULT_PARENT_MENU_ID + + @classmethod + def get_tree_code(cls, params_obj: Dict): + """ + 获取树编码 + + :param params_obj: 菜单参数字典 + :return: 树编码 + """ + if GenConstant.TREE_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE)) + return '' + + @classmethod + def get_tree_parent_code(cls, params_obj: Dict): + """ + 获取树父编码 + + :param params_obj: 菜单参数字典 + :return: 树父编码 + """ + if GenConstant.TREE_PARENT_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE)) + return '' + + @classmethod + def get_tree_name(cls, params_obj: Dict): + """ + 获取树名称 + + :param params_obj: 菜单参数字典 + :return: 树名称 + """ + if GenConstant.TREE_NAME in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME)) + return '' + + @classmethod + def get_expand_column(cls, gen_table: GenTableModel): + """ + 获取展开列 + + :param gen_table: 生成表的配置信息 + :return: 展开列 + """ + options = gen_table.options + params_obj = json.loads(options) + tree_name = params_obj.get(GenConstant.TREE_NAME) + num = 0 + for column in gen_table.columns or []: + if column.list: + num += 1 + if column.column_name == tree_name: + break + return num + + @classmethod + def to_camel_case(cls, text: str) -> str: + """ + 将字符串转换为驼峰命名 + + :param text: 待转换的字符串 + :return: 转换后的驼峰命名字符串 + """ + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) + + @classmethod + def get_sqlalchemy_type(cls, column_type: str): + """ + 获取SQLAlchemy类型 + + :param column_type: 列类型 + :return: SQLAlchemy类型 + """ + if '(' in column_type: + column_type_list = column_type.split('(') + if column_type_list[0] in GenConstant.COLUMNTYPE_STR: + sqlalchemy_type = ( + StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0] + ) + + '(' + + column_type_list[1] + ) + else: + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0] + ) + else: + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.DB_TO_SQLALCHEMY_TYPE_MAPPING, column_type + ) + + return sqlalchemy_type diff --git a/ruoyi-fastapi-frontend/src/api/tool/gen.js b/ruoyi-fastapi-frontend/src/api/tool/gen.js index 4506927..2075677 100644 --- a/ruoyi-fastapi-frontend/src/api/tool/gen.js +++ b/ruoyi-fastapi-frontend/src/api/tool/gen.js @@ -43,6 +43,15 @@ export function importTable(data) { }) } +// 创建表 +export function createTable(data) { + return request({ + url: '/tool/gen/createTable', + method: 'post', + params: data + }) +} + // 预览生成代码 export function previewTable(tableId) { return request({ diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue new file mode 100644 index 0000000..f914b5d --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue @@ -0,0 +1,45 @@ + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue index 951497a..f2c78ad 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue @@ -24,22 +24,25 @@ min-width="10%" :show-overflow-tooltip="true" /> - + - + @@ -82,6 +85,11 @@ + + +