SpringSunYY 5 kuukautta sitten
vanhempi
commit
8c9f7c76b2

+ 1 - 1
.gitignore

@@ -37,7 +37,7 @@ npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
 **/*.log
-ruoyi-test/*
+ruoyi_test/*
 tests/**/coverage/
 tests/e2e/reports
 selenium-debug.log

+ 2 - 2
ruoyi-ui/src/views/tool/gen/genInfoForm.vue

@@ -243,7 +243,7 @@ export default {
           { required: true, message: "请选择生成模板", trigger: "blur" }
         ],
         packageName: [
-          { required: true, message: "请输入生成包路径", trigger: "blur" }
+          { required: false, message: "请输入生成包路径", trigger: "blur" }
         ],
         moduleName: [
           { required: true, message: "请输入生成模块名", trigger: "blur" }
@@ -301,4 +301,4 @@ export default {
     }
   }
 };
-</script>
+</script>

+ 0 - 98
ruoyi_generator/BUG_FIXES.md

@@ -1,98 +0,0 @@
-# 代码生成器Bug修复报告
-
-## 修复的问题
-
-### 1. Pydantic验证错误 - Missing required argument
-
-**问题描述:**
-```
-pydantic_core._pydantic_core.ValidationError: 1 validation error for update_gen_table
-dto
-  Missing required argument [type=missing_argument, input_value=ArgsKwargs((), {'tableId': 18}), input_type=ArgsKwargs]
-```
-
-**根本原因:**
-1. 装饰器顺序错误:`@PathValidator()` 在 `@BodyValidator()` 之前
-2. 函数参数顺序错误:Pydantic模型参数必须是第一个参数
-
-**修复方案:**
-
-#### 1.1 修复装饰器顺序
-```python
-# 修复前
-@gen.route('/<int:tableId>', methods=['PUT'])
-@PathValidator()
-@BodyValidator()
-@PreAuthorize(HasPerm('tool:gen:edit'))
-@JsonSerializer()
-def update_gen_table(dto: GenTablePO, tableId: int):
-
-# 修复后
-@gen.route('/<int:tableId>', methods=['PUT'])
-@BodyValidator()
-@PathValidator()
-@PreAuthorize(HasPerm('tool:gen:edit'))
-@JsonSerializer()
-def update_gen_table(dto: GenTablePO, tableId: int):
-```
-
-#### 1.2 修复函数参数顺序
-- Pydantic模型参数(dto)必须是第一个参数
-- 路径参数(tableId)必须是第二个参数
-
-#### 1.3 修复的文件
-- `ruoyi_generator/controller/gen.py` - `update_gen_table` 函数
-- `ruoyi_generator/controller/column.py` - `update_column` 函数
-
-### 2. 装饰器使用规则
-
-**正确的装饰器顺序:**
-```python
-@route_decorator
-@BodyValidator()      # Body验证器在前
-@PathValidator()      # Path验证器在后
-@PreAuthorize()       # 权限验证
-@JsonSerializer()     # 序列化器
-def function_name(dto: ModelClass, path_param: int):
-```
-
-**正确的参数顺序:**
-```python
-def function_name(dto: PydanticModel, path_param: int):
-    # dto 必须是第一个参数(Pydantic模型)
-    # path_param 必须是第二个参数(路径参数)
-```
-
-### 3. 验证器工作原理
-
-1. **BodyValidator**: 验证请求体中的JSON数据,映射到Pydantic模型
-2. **PathValidator**: 验证URL路径中的参数
-3. **装饰器执行顺序**: 从下到上执行,但参数解析需要正确的顺序
-
-### 4. 测试验证
-
-修复后的函数签名:
-```python
-def update_gen_table(dto: GenTablePO, tableId: int):
-    # 参数顺序:dto在前,tableId在后
-    # 装饰器顺序:BodyValidator在前,PathValidator在后
-```
-
-## 预防措施
-
-1. **代码审查**: 确保所有使用BodyValidator和PathValidator的函数都遵循正确的顺序
-2. **模板检查**: 确保生成的代码模板也遵循正确的顺序
-3. **测试覆盖**: 添加单元测试验证装饰器组合的正确性
-
-## 相关文件
-
-- `ruoyi_generator/controller/gen.py` - 主要修复文件
-- `ruoyi_generator/controller/column.py` - 列管理修复文件
-- `ruoyi_generator/vm/py/controller.py.vm` - 模板文件(已正确)
-
-## 状态
-
-✅ 已修复 - 所有Pydantic验证错误已解决
-✅ 已验证 - 装饰器顺序和参数顺序已正确
-✅ 已测试 - 无linter错误
-

+ 0 - 151
ruoyi_generator/CODE_GENERATOR_FEATURES.md

@@ -1,151 +0,0 @@
-# 代码生成器功能说明
-
-## 已实现的功能
-
-### 1. 核心功能
-- ✅ 数据库表导入
-- ✅ 代码生成配置
-- ✅ 模板引擎生成代码
-- ✅ 前后端代码生成
-- ✅ ZIP打包下载
-- ✅ 代码预览
-- ✅ 批量生成代码
-- ✅ 数据库表结构同步
-- ✅ 表信息查询
-
-### 2. 生成的代码文件
-- ✅ Python后端代码
-  - Entity实体类 (`domain/{ClassName}.py`)
-  - PO查询对象 (`domain/po.py`)
-  - Mapper数据访问层 (`mapper/{class_name}_mapper.py`)
-  - Service业务逻辑层 (`service/{class_name}_service.py`)
-  - Controller控制层 (`controller/{class_name}_controller.py`)
-- ✅ Vue前端代码
-  - 页面组件 (`vue/{business_name}/index.vue`)
-  - API接口 (`api/{module_name}/{business_name}.js`)
-- ✅ SQL脚本
-  - 菜单权限SQL (`sql/menu.sql`)
-- ✅ 文档
-  - README说明文档 (`README.md`)
-
-### 3. 模板特性
-- ✅ 支持Jinja2模板引擎
-- ✅ 动态字段生成
-- ✅ 权限控制集成
-- ✅ 分页查询支持
-- ✅ 表单验证集成
-- ✅ 字典数据支持
-- ✅ 查询条件动态生成
-
-### 4. 数据库支持
-- ✅ MySQL数据库表结构读取
-- ✅ 字段类型自动映射
-- ✅ 主键自动识别
-- ✅ 自增字段识别
-- ✅ 字段注释读取
-- ✅ 表注释读取
-
-### 5. API接口
-- ✅ 表列表查询 (`GET /list`)
-- ✅ 数据库表列表 (`GET /db/list`)
-- ✅ 表详情查询 (`GET /{tableId}`)
-- ✅ 表导入 (`POST /importTable`)
-- ✅ 表信息更新 (`PUT /{tableId}`)
-- ✅ 表删除 (`DELETE /{tableIds}`)
-- ✅ 代码预览 (`GET /preview/{tableId}`)
-- ✅ 代码下载 (`GET /download/{table_name}`)
-- ✅ 代码生成 (`GET /genCode/{table_name}`)
-- ✅ 批量生成 (`GET /batchGenCode`)
-- ✅ 数据库同步 (`GET /synchDb/{table_name}`)
-- ✅ 字段列表查询 (`GET /column/list`)
-- ✅ 数据导出 (`GET /export`)
-- ✅ 表信息查询 (`GET /tableInfo/{table_name}`)
-
-### 6. 配置功能
-- ✅ 作者配置
-- ✅ 包名配置
-- ✅ 表前缀配置
-- ✅ 自动移除表前缀
-- ✅ 模板分类支持
-
-### 7. 代码规范
-- ✅ Python代码规范
-- ✅ 类型注解支持
-- ✅ 文档字符串
-- ✅ 异常处理
-- ✅ 日志记录
-- ✅ 权限验证
-
-## 使用说明
-
-### 1. 导入数据库表
-```python
-# 通过API导入表
-POST /tool/gen/importTable?tables=sys_user,sys_role
-```
-
-### 2. 配置生成参数
-- 设置包名、模块名、业务名
-- 配置字段属性(是否查询、编辑、列表显示等)
-- 设置HTML控件类型
-
-### 3. 生成代码
-```python
-# 预览代码
-GET /tool/gen/preview/{tableId}
-
-# 下载代码
-GET /tool/gen/download/{table_name}
-
-# 批量生成
-GET /tool/gen/batchGenCode?tables=sys_user,sys_role
-```
-
-### 4. 同步数据库
-```python
-# 同步表结构
-GET /tool/gen/synchDb/{table_name}
-```
-
-## 模板文件结构
-```
-ruoyi_generator/vm/
-├── py/                    # Python后端模板
-│   ├── entity.py.vm      # 实体类模板
-│   ├── po.py.vm          # 查询对象模板
-│   ├── mapper.py.vm      # 数据访问层模板
-│   ├── service.py.vm     # 业务逻辑层模板
-│   └── controller.py.vm  # 控制层模板
-├── vue/                   # Vue前端模板
-│   └── index.vue.vm      # 页面模板
-├── js/                    # JavaScript模板
-│   └── api.js.vm         # API接口模板
-├── sql/                   # SQL模板
-│   └── menu.sql.vm       # 菜单权限模板
-└── README.md.vm          # 说明文档模板
-```
-
-## 权限配置
-- `tool:gen:list` - 查询权限
-- `tool:gen:query` - 详情查询权限
-- `tool:gen:add` - 新增权限
-- `tool:gen:edit` - 编辑权限
-- `tool:gen:remove` - 删除权限
-- `tool:gen:code` - 代码生成权限
-- `tool:gen:preview` - 代码预览权限
-- `tool:gen:export` - 导出权限
-
-## 技术栈
-- **后端**: Flask + SQLAlchemy + Jinja2
-- **前端**: Vue.js + Element UI
-- **数据库**: MySQL
-- **模板引擎**: Jinja2
-- **权限控制**: 基于注解的权限控制
-
-## 扩展性
-- 支持自定义模板
-- 支持多种数据库
-- 支持多种前端框架
-- 支持自定义字段类型映射
-- 支持自定义生成规则
-

+ 0 - 0
ruoyi_generator/RUOYI_VUE_FEATURES.md


+ 8 - 0
ruoyi_generator/config.py

@@ -10,6 +10,12 @@ class GeneratorConfig:
     # 作者
     author = "YY"
     
+    # 默认前端模块名(用于前端代码路径和SQL component)
+    model_name = "test"
+    
+    # 默认Python模块名(用于后端代码路径)
+    python_model_name = "ruoyi_test"
+    
     # 默认包名
     package_name = "com.yy.project"
     
@@ -27,6 +33,8 @@ class GeneratorConfig:
             gen_config = config_data.get("gen", {})
             
             author = gen_config.get("author", author)
+            model_name = gen_config.get("modelName", model_name)
+            python_model_name = gen_config.get("pythonModelName", python_model_name)
             package_name = gen_config.get("packageName", package_name)
             auto_remove_pre = gen_config.get("autoRemovePre", auto_remove_pre)
             table_prefix = gen_config.get("tablePrefix", table_prefix)

+ 7 - 2
ruoyi_generator/config/generator.yml

@@ -1,5 +1,10 @@
 gen:
+#  作者
   author: YY
-  packageName: com.yy.project
+#  模块,你需要生成python的模块
+  pythonModelName: ruoyi_test
+  #模块,真正的模块名字
+  modelName: test
+  packageName: 
   autoRemovePre: true
-  tablePrefix: sys_
+  tablePrefix: tb_

+ 29 - 0
ruoyi_generator/controller/gen.py

@@ -130,6 +130,35 @@ def update_gen_table(dto: GenTablePO):
     for attr in dto.model_fields.keys():
         if hasattr(gen_table, attr):
             setattr(gen_table, attr, getattr(dto, attr))
+    
+    # 处理 options 字段和 parentMenuId
+    import json
+    options_dict = {}
+    
+    # 如果 options 字段存在,先解析它
+    if hasattr(dto, 'options') and dto.options:
+        try:
+            if isinstance(dto.options, str):
+                options_dict = json.loads(dto.options)
+            else:
+                options_dict = dto.options.copy() if hasattr(dto.options, 'copy') else dto.options
+        except Exception as e:
+            print(f"解析 options 字段出错: {e}")
+            options_dict = {}
+    
+    # 检查 params 中是否有 parentMenuId(前端可能通过 params 传递)
+    if hasattr(dto, 'params') and dto.params:
+        if isinstance(dto.params, dict) and 'parentMenuId' in dto.params:
+            options_dict['parentMenuId'] = dto.params.get('parentMenuId')
+    
+    # 如果 options_dict 中有 parentMenuId,更新 options 字段
+    if 'parentMenuId' in options_dict:
+        gen_table.options = json.dumps(options_dict, ensure_ascii=False)
+        gen_table.parent_menu_id = options_dict.get('parentMenuId')
+        print(f"设置 parentMenuId: {options_dict.get('parentMenuId')}, options: {gen_table.options}")
+    elif options_dict:
+        # 即使没有 parentMenuId,也要保存其他 options
+        gen_table.options = json.dumps(options_dict, ensure_ascii=False)
 
     # 特别处理columns字段
     if hasattr(dto, 'columns') and dto.columns is not None:

+ 6 - 1
ruoyi_generator/domain/entity.py

@@ -45,6 +45,8 @@ class GenTable(BaseEntity):
     tree_code: Optional[str] = Field(None, alias='treeCode')
     tree_parent_code: Optional[str] = Field(None, alias='treeParentCode')
     tree_name: Optional[str] = Field(None, alias='treeName')
+    # 菜单相关字段
+    parent_menu_id: Optional[int] = Field(None, alias='parentMenuId')
 
     # 分页参数
     page_num: Optional[int] = Field(None, alias='pageNum')
@@ -52,13 +54,16 @@ class GenTable(BaseEntity):
 
     @model_validator(mode='after')
     def process_options(self):
-        # 解析options字段以设置tree相关属性
+        # 解析options字段以设置tree相关属性和parent_menu_id
         if self.options and isinstance(self.options, str):
             try:
                 options_dict = json.loads(self.options)
                 self.tree_name = options_dict.get('treeName')
                 self.tree_code = options_dict.get('treeCode')
                 self.tree_parent_code = options_dict.get('treeParentCode')
+                # 从options中解析parent_menu_id
+                if 'parentMenuId' in options_dict:
+                    self.parent_menu_id = options_dict.get('parentMenuId')
             except Exception:
                 pass
         return self

+ 25 - 12
ruoyi_generator/mapper/__init__.py

@@ -89,12 +89,20 @@ class GenTableMapper:
         """
         # 查询真实的数据库表信息
         try:
-            # 查询所有表名
-            result = db.session.execute(text("SHOW TABLES")).fetchall()
-            table_names = [row[0] for row in result]
+            # 查询所有表名、表注释、创建时间和更新时间,按创建时间倒序排列
+            result = db.session.execute(text("""
+                SELECT table_name, table_comment, create_time, update_time 
+                FROM information_schema.tables 
+                WHERE table_schema = DATABASE() 
+                ORDER BY create_time DESC
+            """)).fetchall()
             
             tables = []
-            for table_name in table_names:
+            for row in result:
+                table_name = row[0]
+                table_comment = row[1] if row[1] else table_name
+                create_time = row[2] if len(row) > 2 else None
+                update_time = row[3] if len(row) > 3 else None
                 # 检查是否已导入
                 exists_result = db.session.execute(
                     text("SELECT COUNT(1) FROM gen_table WHERE table_name = :table_name"),
@@ -103,22 +111,22 @@ class GenTableMapper:
                 
                 exists = exists_result[0] > 0 if exists_result else False
                 if not exists:
-                    # 获取表注释
-                    table_comment_result = db.session.execute(
-                        text("SELECT table_comment FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table_name"),
-                        {"table_name": table_name}
-                    ).fetchone()
-                    table_comment = table_comment_result[0] if table_comment_result else table_name
                     
                     table = GenTable()
                     table.table_name = table_name
                     table.table_comment = table_comment
+                    # 设置创建时间和更新时间
+                    if create_time:
+                        table.create_time = create_time
+                    if update_time:
+                        table.update_time = update_time
                     # 设置默认值,以便前端显示
                     clean_table_name = GenUtils.remove_table_prefix(table_name) if GeneratorConfig.auto_remove_pre else table_name
                     # 使用下划线命名法而不是驼峰命名法
                     table.class_name = to_underscore(clean_table_name)
                     table.package_name = GeneratorConfig.package_name
-                    table.module_name = StringUtil.substring_before(clean_table_name, "_") if hasattr(StringUtil, 'substring_before') and "_" in clean_table_name else clean_table_name
+                    # 使用配置中的 modelName 作为模块名
+                    table.module_name = GeneratorConfig.model_name
                     table.business_name = StringUtil.substring_after(clean_table_name, "_") if hasattr(StringUtil, 'substring_after') and "_" in clean_table_name else clean_table_name
                     table.function_name = table.business_name
                     table.function_author = GeneratorConfig.author
@@ -292,13 +300,18 @@ class GenTableMapper:
             table_data = gen_table.model_dump(by_alias=False, exclude_none=True)
             
             # 移除不需要更新的字段
-            exclude_fields = {'table_id', 'page_size', 'page_num', 'columns', 'pk_column', 'tree_name', 'tree_code', 'tree_parent_code'}
+            exclude_fields = {'table_id', 'page_size', 'page_num', 'columns', 'pk_column', 'tree_name', 'tree_code', 'tree_parent_code', 'parent_menu_id'}
             for field in exclude_fields:
                 table_data.pop(field, None)
             
             table_data.pop('create_time', None)
             table_data.pop('create_by', None)
             
+            # 确保 options 字段被正确更新(即使为空也要更新)
+            if 'options' in table_data:
+                # options 字段需要保留,即使可能是空字符串
+                pass
+            
             # 确保必要的字段有默认值
             table_data.setdefault('update_by', 'admin')
             

+ 14 - 1
ruoyi_generator/ruoyi_generator.py

@@ -71,9 +71,21 @@ class RuoYiGenerator:
                 table.tree_name = table.options.get('treeName')
                 table.tree_code = table.options.get('treeCode')
                 table.tree_parent_code = table.options.get('treeParentCode')
+                # 从 options 中提取 parentMenuId
+                if 'parentMenuId' in table.options:
+                    table.parent_menu_id = table.options.get('parentMenuId')
             except Exception:
                 pass
         
+        # 强制使用前端模块名(modelName),而不是 Python 模块名
+        # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
+        # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
+        original_module_name = table.module_name
+        if not table.module_name or table.module_name == GeneratorConfig.python_model_name or (table.module_name and GeneratorConfig.python_model_name in table.module_name):
+            table.module_name = GeneratorConfig.model_name
+            if original_module_name != table.module_name:
+                print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
+        
         # 设置模板数据
         data = {
             'table': table,
@@ -209,7 +221,8 @@ class RuoYiGenerator:
             # 使用下划线命名法而不是驼峰命名法
             table.class_name = to_underscore(clean_table_name)
             table.package_name = GeneratorConfig.package_name
-            table.module_name = StringUtil.substring_before(clean_table_name, "_") if hasattr(StringUtil, 'substring_before') and "_" in clean_table_name else clean_table_name
+            # 使用配置中的 modelName 作为模块名
+            table.module_name = GeneratorConfig.model_name
             table.business_name = StringUtil.substring_after(clean_table_name, "_") if hasattr(StringUtil, 'substring_after') and "_" in clean_table_name else clean_table_name
             table.function_name = table.business_name
             table.function_author = GeneratorConfig.author

+ 2 - 2
ruoyi_generator/service/__init__.py

@@ -126,8 +126,8 @@ class GenTableService:
             table.class_name = to_underscore(clean_table_name)
             table.business_name = GenUtils.get_business_name(clean_table_name)
             table.package_name = GeneratorConfig.package_name
-            table.module_name = StringUtil.substring_before(clean_table_name, "_") if hasattr(StringUtil,
-                                                                                              'substring_before') and "_" in clean_table_name else clean_table_name
+            # 使用配置中的 modelName 作为模块名
+            table.module_name = GeneratorConfig.model_name
 
             # 获取表注释
             try:

+ 408 - 26
ruoyi_generator/util.py

@@ -44,36 +44,48 @@ class GenUtils:
         # 移除.vm后缀
         base_name = template_file[:-3] if template_file.endswith('.vm') else template_file
         
+        # 确定模块路径:后端代码使用 pythonModelName,前端代码使用 modelName
+        if table.package_name:
+            # 使用 package_name 作为路径(如 com.yy.test -> com/yy/test)
+            module_path = table.package_name.replace('.', '/')
+        else:
+            # 如果 package_name 为空,使用 pythonModelName 作为后端模块名
+            # 注意:这里只用于后端 Python 代码路径,前端代码路径在下面单独处理
+            module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
+        
         # 根据模板类型生成文件名和路径
         if 'py/entity.py' in template_file:
-            # 根据包名生成目录结构
-            package_path = table.package_name.replace('.', '/') if table.package_name else ''
-            return f"{package_path}/domain/{table.class_name}.py"
+            # Entity文件放在 domain/entity/ 目录下,使用下划线命名法
+            entity_name = to_underscore(table.class_name)
+            return f"{module_path}/domain/entity/{entity_name}.py"
         elif 'py/controller.py' in template_file:
-            package_path = table.package_name.replace('.', '/') if table.package_name else ''
             # 使用下划线命名法
             controller_name = f"{to_underscore(table.class_name)}_controller"
-            return f"{package_path}/controller/{controller_name}.py"
+            return f"{module_path}/controller/{controller_name}.py"
         elif 'py/service.py' in template_file:
-            package_path = table.package_name.replace('.', '/') if table.package_name else ''
             # 使用下划线命名法
             service_name = f"{to_underscore(table.class_name)}_service"
-            return f"{package_path}/service/{service_name}.py"
+            return f"{module_path}/service/{service_name}.py"
         elif 'py/mapper.py' in template_file:
-            package_path = table.package_name.replace('.', '/') if table.package_name else ''
             # 使用下划线命名法
             mapper_name = f"{to_underscore(table.class_name)}_mapper"
-            return f"{package_path}/mapper/{mapper_name}.py"
+            return f"{module_path}/mapper/{mapper_name}.py"
         elif 'py/po.py' in template_file:
-            package_path = table.package_name.replace('.', '/') if table.package_name else ''
-            # PO文件使用表名作为文件名
-            po_name = f"{table.class_name}PO"
-            return f"{package_path}/domain/{po_name}.py"
-        elif 'vue/index.vue' in template_file:
+            # PO文件放在 domain/po/ 目录下,使用下划线命名法
+            po_name = f"{to_underscore(table.class_name)}_po"
+            return f"{module_path}/domain/po/{po_name}.py"
+        elif 'vue/index.vue' in template_file or 'vue/index-tree.vue' in template_file:
             # 无论是树表还是普通表,Vue文件名都是index.vue
-            return f"vue/{table.business_name}/index.vue"
+            # 使用数据库中的 module_name(前端模块名)
+            frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
+            return f"vue/views/{frontend_module}/{table.business_name}/index.vue"
         elif 'js/api.js' in template_file:
-            return f"js/api/{table.business_name}.js"
+            # 使用数据库中的 module_name(前端模块名)
+            frontend_module = table.module_name if table.module_name else GeneratorConfig.model_name
+            return f"vue/api/{frontend_module}/{table.business_name}.js"
+        elif 'py/__init__.py' in template_file:
+            # 模块根目录的 __init__.py
+            return f"{module_path}/__init__.py"
         elif 'sql/menu.sql' in template_file:
             return f"sql/{table.business_name}_menu.sql"
         elif 'README.md' in template_file:
@@ -144,28 +156,32 @@ class GenUtils:
         return GenUtils.substring_before(clean_table_name, "_") if "_" in clean_table_name else clean_table_name
 
     @staticmethod
-    def get_import_path(package_name: str, module_type: str, class_name: str = None) -> str:
+    def get_import_path(package_name: str, module_name: str, module_type: str, class_name: str = None) -> str:
         """
         生成导入路径
         
         Args:
-            package_name (str): 包名,如 "com.yy.project" 或 "ruoyi_generator"
+            package_name (str): 包名,如 "com.yy.project" 或空字符串
+            module_name (str): 前端模块名(从数据库读取,用于前端代码),但这里应该传入 pythonModelName
             module_type (str): 模块类型,如 "domain", "service", "mapper", "controller"
             class_name (str): 类名(可选,用于PO导入)
             
         Returns:
             str: 导入路径,Python包名保持点分隔格式
         """
+        # Python 后端代码使用 pythonModelName,而不是前端模块名
+        # 如果 package_name 为空,使用 pythonModelName 作为 Python 模块名
         if not package_name:
-            return f"ruoyi_generator.{module_type}"
-        
-        # Python导入路径使用点分隔,保持原样
-        # 例如: "com.yy.project" -> "com.yy.project"
-        # 例如: "ruoyi_generator" -> "ruoyi_generator" (保持不变)
-        python_package = package_name
+            # 使用 pythonModelName 作为 Python 模块名
+            python_package = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (module_name if module_name else 'ruoyi_generator')
+        else:
+            # Python导入路径使用点分隔,保持原样
+            # 例如: "com.yy.project" -> "com.yy.project"
+            python_package = package_name
         
         # 生成导入路径
         if module_type == "domain" and class_name:
+            # PO 文件在 domain/po/ 目录下
             return f"{python_package}.domain.po"
         elif module_type == "domain":
             return f"{python_package}.domain.entity"
@@ -250,6 +266,29 @@ class GenUtils:
         else:
             table.pk_column = None
         
+        # 从 options 中解析 parentMenuId
+        if table.options:
+            import json
+            try:
+                if isinstance(table.options, str):
+                    options_dict = json.loads(table.options)
+                else:
+                    options_dict = table.options
+                # 从 options 中提取 parentMenuId 并设置到 table
+                if 'parentMenuId' in options_dict:
+                    table.parent_menu_id = options_dict.get('parentMenuId')
+            except Exception as e:
+                print(f"解析 options 字段出错: {e}")
+        
+        # 强制使用前端模块名(modelName),而不是 Python 模块名
+        # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
+        # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
+        original_module_name = table.module_name
+        if not table.module_name or table.module_name == GeneratorConfig.python_model_name or (table.module_name and GeneratorConfig.python_model_name in table.module_name):
+            table.module_name = GeneratorConfig.model_name
+            if original_module_name != table.module_name:
+                print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
+        
         # 获取模板目录
         template_dir = os.path.join(os.path.dirname(__file__), 'vm')
         
@@ -273,7 +312,53 @@ class GenUtils:
         # 创建内存中的ZIP文件
         zip_buffer = BytesIO()
         
+        # 确定模块路径,用于生成 __init__.py
+        # 后端 Python 代码使用 pythonModelName,而不是前端模块名
+        if table.package_name:
+            module_path = table.package_name.replace('.', '/')
+        else:
+            # 使用 pythonModelName 作为后端模块路径
+            module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (table.module_name if table.module_name else 'ruoyi_generator')
+        
+        # 收集需要生成 __init__.py 的目录和文件信息
+        init_dirs = set()
+        # 收集每个目录下的文件,用于生成导入语句
+        dir_files = {}  # {dir_path: [file_info]}
+        
         with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
+            # 跟踪已添加的文件名以避免重复
+            added_files = set()
+            
+            # 首先生成模块根目录的 __init__.py
+            init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
+            if os.path.exists(init_template_path):
+                try:
+                    with open(init_template_path, 'r', encoding='utf-8') as f:
+                        template_content = f.read()
+                    
+                    # 准备模板上下文(单个表时,tables 为 None)
+                    context = {
+                        'table': table,
+                        'tables': None,  # 单个表生成时,tables 为 None
+                        'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+                        'underscore': to_underscore,
+                        'get_import_path': GenUtils.get_import_path
+                    }
+                    
+                    # 使用Jinja2渲染模板
+                    template = Template(template_content)
+                    rendered_content = template.render(**context)
+                    
+                    # 生成文件名
+                    init_file_path = f"{module_path}/__init__.py"
+                    
+                    # 将渲染后的内容写入ZIP文件
+                    if rendered_content.strip():
+                        zip_file.writestr(init_file_path, rendered_content)
+                        added_files.add(init_file_path)
+                except Exception as e:
+                    print(f"处理模块 __init__.py 模板时出错: {e}")
+            
             # 处理每个核心模板文件
             for relative_path in core_templates:
                 template_path = os.path.join(template_dir, relative_path)
@@ -284,6 +369,8 @@ class GenUtils:
                             template_content = f.read()
                             
                         # 准备模板上下文
+                        # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
+                        # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
                         context = {
                             'table': table,
                             'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
@@ -295,9 +382,46 @@ class GenUtils:
                         template = Template(template_content)
                         rendered_content = template.render(**context)
                         
+                        # 如果是 SQL 模板,恢复原始 module_name(虽然已经强制设置了,但为了安全)
+                        if 'sql/menu.sql' in relative_path:
+                            pass  # 已经强制设置为 model_name,不需要恢复
+                        
                         # 生成文件名
                         output_file_name = GenUtils.get_file_name(relative_path, table)
                         
+                        # 收集目录路径和文件信息,用于生成 __init__.py
+                        # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
+                        dir_path = os.path.dirname(output_file_name)
+                        if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
+                            init_dirs.add(dir_path)
+                            # 同时收集父目录(但也要跳过 sql 和 vue)
+                            parts = dir_path.split('/')
+                            for i in range(1, len(parts)):
+                                parent_dir = '/'.join(parts[:i])
+                                if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
+                                    init_dirs.add(parent_dir)
+                            
+                            # 收集文件信息用于生成导入语句
+                            file_name = os.path.basename(output_file_name)
+                            file_base = os.path.splitext(file_name)[0]
+                            
+                            if dir_path not in dir_files:
+                                dir_files[dir_path] = []
+                            
+                            # 根据文件类型确定导入的类名
+                            if '/entity/' in output_file_name and '.py' in output_file_name:
+                                # Entity 文件在 domain/entity/ 目录下
+                                dir_files[dir_path].append(('entity', table.class_name, table))
+                            elif '/po/' in output_file_name and '_po.py' in output_file_name:
+                                # PO 文件在 domain/po/ 目录下
+                                dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
+                            elif '_service.py' in output_file_name:
+                                dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
+                            elif '_mapper.py' in output_file_name:
+                                dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
+                            elif '_controller.py' in output_file_name:
+                                dir_files[dir_path].append(('controller', 'gen', table))
+                        
                         # 检查渲染后的内容是否为空
                         if rendered_content.strip():
                             # 将渲染后的内容写入ZIP文件
@@ -306,6 +430,69 @@ class GenUtils:
                             print(f"警告: 模板 {relative_path} 渲染后内容为空")
                     except Exception as e:
                         print(f"处理模板 {relative_path} 时出错: {e}")
+            
+            # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
+            for dir_path in sorted(init_dirs):
+                # 跳过模块根目录,因为已经在开始时生成
+                if dir_path == module_path:
+                    continue
+                
+                # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
+                if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
+                    continue
+                
+                init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
+                
+                # 生成 __init__.py 内容
+                init_lines = ["# -*- coding: utf-8 -*-"]
+                init_lines.append(f"# @Module: {dir_path}")
+                init_lines.append("")
+                
+                # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
+                if 'controller' in dir_path and dir_path.endswith('/controller'):
+                    # 在 controller/__init__.py 中为每个 controller 创建蓝图
+                    if dir_path in dir_files:
+                        # 先导入 Blueprint
+                        init_lines.append("from flask import Blueprint")
+                        init_lines.append("")
+                        
+                        # 为每个 controller 创建蓝图
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'controller':
+                                blueprint_name = to_underscore(table_info.class_name)
+                                url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
+                                init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
+                        
+                        # 导入各个 controller 模块
+                        init_lines.append("")
+                        init_lines.append("")
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'controller':
+                                controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
+                                init_lines.append(f"from . import {controller_module_name}")
+                else:
+                    # 其他目录正常生成导入语句
+                    if dir_path in dir_files:
+                        imports = []
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'entity':
+                                # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
+                                entity_file_name = to_underscore(class_name)
+                                imports.append(f"from .{entity_file_name} import {class_name}")
+                            elif file_type == 'po':
+                                # PO 文件在 domain/po/ 目录下,导入时使用文件名
+                                po_file_name = class_name  # class_name 已经是 address_info_po
+                                imports.append(f"from .{po_file_name} import {class_name}")
+                            elif file_type == 'service':
+                                imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
+                            elif file_type == 'mapper':
+                                imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
+                        
+                        if imports:
+                            init_lines.extend(sorted(set(imports)))
+                
+                init_content = "\n".join(init_lines) + "\n"
+                zip_file.writestr(init_file_path, init_content)
         
         zip_buffer.seek(0)
         return zip_buffer
@@ -330,8 +517,30 @@ class GenUtils:
                 table.pk_column = pk_columns[0]
             else:
                 table.pk_column = None
-        
-        # 定义核心模板文件
+            
+            # 从 options 中解析 parentMenuId
+            if table.options:
+                import json
+                try:
+                    if isinstance(table.options, str):
+                        options_dict = json.loads(table.options)
+                    else:
+                        options_dict = table.options
+                    # 从 options 中提取 parentMenuId 并设置到 table
+                    if 'parentMenuId' in options_dict:
+                        table.parent_menu_id = options_dict.get('parentMenuId')
+                except Exception as e:
+                    print(f"解析 options 字段出错: {e}")
+            
+            # 强制使用前端模块名(modelName),而不是 Python 模块名
+            # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
+            if not table.module_name or table.module_name == GeneratorConfig.python_model_name or table.module_name.strip() == GeneratorConfig.python_model_name:
+                table.module_name = GeneratorConfig.model_name
+            # 额外检查:如果 module_name 等于 python_model_name,强制替换
+            if table.module_name == GeneratorConfig.python_model_name:
+                table.module_name = GeneratorConfig.model_name
+        
+        # 定义核心模板文件(每个表都会生成,但 __init__.py 只生成一次)
         core_templates = [
             'py/entity.py.vm',
             'py/po.py.vm', 
@@ -348,10 +557,59 @@ class GenUtils:
         # 获取模板目录
         template_dir = os.path.join(os.path.dirname(__file__), 'vm')
         
+        # 确定模块路径(使用第一个表的模块路径)
+        if tables and len(tables) > 0:
+            first_table = tables[0]
+            if first_table.package_name:
+                module_path = first_table.package_name.replace('.', '/')
+            else:
+                # 使用 pythonModelName 作为后端模块路径
+                module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else (first_table.module_name if first_table.module_name else 'ruoyi_generator')
+        else:
+            module_path = GeneratorConfig.python_model_name if hasattr(GeneratorConfig, 'python_model_name') else 'ruoyi_generator'
+        
+        # 收集需要生成 __init__.py 的目录和文件信息
+        init_dirs = set()
+        # 收集每个目录下的文件,用于生成导入语句 {dir_path: [(file_type, class_name, table)]}
+        dir_files = {}
+        
         with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
             # 跟踪已添加的文件名以避免重复
             added_files = set()
             
+            # 首先为模块根目录生成 __init__.py(只生成一次,使用第一个表的信息)
+            if tables and len(tables) > 0:
+                init_template_path = os.path.join(template_dir, 'py/__init__.py.vm')
+                if os.path.exists(init_template_path):
+                    try:
+                        with open(init_template_path, 'r', encoding='utf-8') as f:
+                            template_content = f.read()
+                        
+                        # 准备模板上下文(批量生成时,传入所有表)
+                        # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
+                        # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
+                        context = {
+                            'table': first_table,  # 用于兼容模板中的 table 变量
+                            'tables': tables,  # 传入所有表,用于循环注册所有蓝图
+                            'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+                            'underscore': to_underscore,
+                            'get_import_path': GenUtils.get_import_path
+                        }
+                        
+                        # 使用Jinja2渲染模板
+                        template = Template(template_content)
+                        rendered_content = template.render(**context)
+                        
+                        # 生成文件名
+                        init_file_path = f"{module_path}/__init__.py"
+                        
+                        # 将渲染后的内容写入ZIP文件(只写入一次)
+                        if rendered_content.strip() and init_file_path not in added_files:
+                            zip_file.writestr(init_file_path, rendered_content)
+                            added_files.add(init_file_path)
+                    except Exception as e:
+                        print(f"处理模块 __init__.py 模板时出错: {e}")
+            
             # 处理每个表
             for table in tables:
                 # 根据表类型添加相应的Vue模板
@@ -362,6 +620,10 @@ class GenUtils:
                 
                 # 处理每个核心模板文件
                 for relative_path in current_templates:
+                    # 跳过模块根目录的 __init__.py,因为已经在开始时生成
+                    if relative_path == 'py/__init__.py.vm':
+                        continue
+                    
                     template_path = os.path.join(template_dir, relative_path)
                     if os.path.exists(template_path):
                         # 读取模板内容
@@ -370,6 +632,8 @@ class GenUtils:
                                 template_content = f.read()
                                 
                             # 准备模板上下文
+                            # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
+                            # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
                             context = {
                                 'table': table,
                                 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
@@ -384,6 +648,36 @@ class GenUtils:
                             # 生成文件名
                             output_file_name = GenUtils.get_file_name(relative_path, table)
                             
+                            # 收集目录路径和文件信息,用于生成 __init__.py
+                            # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
+                            dir_path = os.path.dirname(output_file_name)
+                            if dir_path and 'sql' not in dir_path and 'vue' not in dir_path:
+                                init_dirs.add(dir_path)
+                                # 同时收集父目录(但也要跳过 sql 和 vue)
+                                parts = dir_path.split('/')
+                                for i in range(1, len(parts)):
+                                    parent_dir = '/'.join(parts[:i])
+                                    if parent_dir and 'sql' not in parent_dir and 'vue' not in parent_dir:
+                                        init_dirs.add(parent_dir)
+                                
+                                # 收集文件信息用于生成导入语句
+                                if dir_path not in dir_files:
+                                    dir_files[dir_path] = []
+                                
+                                # 根据文件类型确定导入的类名
+                                if '/entity/' in output_file_name and '.py' in output_file_name:
+                                    # Entity 文件在 domain/entity/ 目录下
+                                    dir_files[dir_path].append(('entity', table.class_name, table))
+                                elif '/po/' in output_file_name and '_po.py' in output_file_name:
+                                    # PO 文件在 domain/po/ 目录下
+                                    dir_files[dir_path].append(('po', f"{to_underscore(table.class_name)}_po", table))
+                                elif '_service.py' in output_file_name:
+                                    dir_files[dir_path].append(('service', f"{table.class_name}Service", table))
+                                elif '_mapper.py' in output_file_name:
+                                    dir_files[dir_path].append(('mapper', f"{table.class_name}Mapper", table))
+                                elif '_controller.py' in output_file_name:
+                                    dir_files[dir_path].append(('controller', 'gen', table))
+                            
                             # 检查是否已添加同名文件
                             if output_file_name in added_files:
                                 # 为重复文件添加序号
@@ -404,6 +698,69 @@ class GenUtils:
                                 print(f"警告: 模板 {relative_path} 渲染后内容为空")
                         except Exception as e:
                             print(f"处理表 {table.table_name} 的模板 {relative_path} 时出错: {e}")
+            
+            # 为每个目录生成 __init__.py 文件,使其成为完整的 Python 模块
+            for dir_path in sorted(init_dirs):
+                # 跳过模块根目录,因为已经在开始时生成
+                if dir_path == module_path:
+                    continue
+                
+                # 跳过 sql 和 vue 目录,这些目录不需要 __init__.py
+                if 'sql' in dir_path or 'vue' in dir_path or dir_path.startswith('sql/') or dir_path.startswith('vue/'):
+                    continue
+                
+                init_file_path = os.path.join(dir_path, '__init__.py').replace('\\', '/')
+                
+                # 生成 __init__.py 内容
+                init_lines = ["# -*- coding: utf-8 -*-"]
+                init_lines.append(f"# @Module: {dir_path}")
+                init_lines.append("")
+                
+                # 特殊处理 controller 目录:参考 ruoyi_generator/controller/__init__.py 的格式
+                if 'controller' in dir_path and dir_path.endswith('/controller'):
+                    # 在 controller/__init__.py 中为每个 controller 创建蓝图
+                    if dir_path in dir_files:
+                        # 先导入 Blueprint
+                        init_lines.append("from flask import Blueprint")
+                        init_lines.append("")
+                        
+                        # 为每个 controller 创建蓝图
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'controller':
+                                blueprint_name = to_underscore(table_info.class_name)
+                                url_prefix = f"/{table_info.module_name}/{table_info.business_name}"
+                                init_lines.append(f"{blueprint_name} = Blueprint('{blueprint_name}', __name__, url_prefix='{url_prefix}')")
+                        
+                        # 导入各个 controller 模块
+                        init_lines.append("")
+                        init_lines.append("")
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'controller':
+                                controller_module_name = f"{to_underscore(table_info.class_name)}_controller"
+                                init_lines.append(f"from . import {controller_module_name}")
+                else:
+                    # 其他目录正常生成导入语句
+                    if dir_path in dir_files:
+                        imports = []
+                        for file_type, class_name, table_info in dir_files[dir_path]:
+                            if file_type == 'entity':
+                                # Entity 文件在 domain/entity/ 目录下,导入时使用文件名
+                                entity_file_name = to_underscore(class_name)
+                                imports.append(f"from .{entity_file_name} import {class_name}")
+                            elif file_type == 'po':
+                                # PO 文件在 domain/po/ 目录下,导入时使用文件名
+                                po_file_name = class_name  # class_name 已经是 address_info_po
+                                imports.append(f"from .{po_file_name} import {class_name}")
+                            elif file_type == 'service':
+                                imports.append(f"from .{to_underscore(class_name.replace('Service', ''))}_service import {class_name}")
+                            elif file_type == 'mapper':
+                                imports.append(f"from .{to_underscore(class_name.replace('Mapper', ''))}_mapper import {class_name}")
+                        
+                        if imports:
+                            init_lines.extend(sorted(set(imports)))
+                
+                init_content = "\n".join(init_lines) + "\n"
+                zip_file.writestr(init_file_path, init_content)
         
         zip_buffer.seek(0)
         return zip_buffer
@@ -447,6 +804,29 @@ class GenUtils:
         else:
             table.pk_column = None
         
+        # 从 options 中解析 parentMenuId
+        if table.options:
+            import json
+            try:
+                if isinstance(table.options, str):
+                    options_dict = json.loads(table.options)
+                else:
+                    options_dict = table.options
+                # 从 options 中提取 parentMenuId 并设置到 table
+                if 'parentMenuId' in options_dict:
+                    table.parent_menu_id = options_dict.get('parentMenuId')
+            except Exception as e:
+                print(f"解析 options 字段出错: {e}")
+        
+        # 强制使用前端模块名(modelName),而不是 Python 模块名
+        # module_name 必须使用 modelName(test),不能使用 pythonModelName(ruoyi_test)
+        # 如果 module_name 是空的、等于 python_model_name 或包含 python_model_name,强制替换为 model_name
+        original_module_name = table.module_name
+        if not table.module_name or table.module_name == GeneratorConfig.python_model_name or (table.module_name and GeneratorConfig.python_model_name in table.module_name):
+            table.module_name = GeneratorConfig.model_name
+            if original_module_name != table.module_name:
+                print(f"警告:table.module_name 从 '{original_module_name}' 强制替换为 '{table.module_name}'(前端模块名)")
+        
         # 获取模板目录
         template_dir = os.path.join(os.path.dirname(__file__), 'vm')
         
@@ -480,6 +860,8 @@ class GenUtils:
                         template_content = f.read()
                         
                     # 准备模板上下文
+                    # table.module_name 是从数据库读取的前端模块名(真正的模块名,用于权限、前端、SQL)
+                    # GeneratorConfig.python_model_name 是 Python 模块名(只用于 Python 后端代码路径)
                     context = {
                         'table': table,
                         'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),

+ 0 - 56
ruoyi_generator/vm/README.md.vm

@@ -1,56 +0,0 @@
-# {{ table.function_name }}
-
-## 简介
-
-{% if table.remark != '' %}{{ table.remark }}{% else %}{{ table.function_name }}{% endif %}
-
-## 联系方式
-
-- 作者:{{ table.function_author }}
-- 邮箱:example@example.com
-
-## 功能说明
-
-该模块包含以下主要功能:
-
-1. {{ table.function_name }}信息的增删改查操作
-2. 支持分页查询和条件筛选
-3. 提供RESTful API接口
-4. 基于角色的访问控制(RBAC)
-
-## 数据库设计
-
-### 表名
-{{ table.table_name }}
-
-### 表结构
-{% for column in table.columns %}
-- {{ column.column_name }} ({{ column.column_type }}): {{ column.column_comment }}
-{% endfor %}
-
-## API接口
-
-| 接口名称 | 请求方式 | 接口地址 | 说明 |
-|---------|---------|---------|------|
-| 查询列表 | GET | /{{ table.module_name }}/{{ table.business_name }}/list | 分页查询{{ table.function_name }}列表 |
-| 查询详情 | GET | /{{ table.module_name }}/{{ table.business_name }}/{id} | 根据ID查询{{ table.function_name }}详情 |
-| 新增数据 | POST | /{{ table.module_name }}/{{ table.business_name }} | 新增一条{{ table.function_name }}记录 |
-| 修改数据 | PUT | /{{ table.module_name }}/{{ table.business_name }}/{id} | 根据ID修改{{ table.function_name }}记录 |
-| 删除数据 | DELETE | /{{ table.module_name }}/{{ table.business_name }}/{ids} | 根据ID删除{{ table.function_name }}记录 |
-
-## 权限说明
-
-本模块涉及以下权限:
-
-1. {{ table.module_name }}:{{ table.business_name }}:list - 查询权限
-2. {{ table.module_name }}:{{ table.business_name }}:add - 新增权限
-3. {{ table.module_name }}:{{ table.business_name }}:edit - 修改权限
-4. {{ table.module_name }}:{{ table.business_name }}:remove - 删除权限
-5. {{ table.module_name }}:{{ table.business_name }}:export - 导出权限
-
-## 使用说明
-
-1. 确保数据库表 `{{ table.table_name }}` 已创建并包含相应字段
-2. 在系统菜单中添加对应的菜单项和权限配置
-3. 启动服务后可通过API接口进行访问
-4. 前端页面位于 `{{ table.module_name }}/{{ table.business_name }}/index` 路径下

+ 23 - 0
ruoyi_generator/vm/py/__init__.py.vm

@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# @Module: {{ table.module_name }}
+# @Author: {{ table.function_author }}
+
+def init_app(app):
+    """
+    初始化模块,注册蓝图
+    
+    Args:
+        app: Flask应用实例
+    """
+    # 导入 controller 模块,自动注册所有蓝图
+{%- if tables %}
+{%- for table in tables %}
+    # 使用 pythonModelName 生成 Python 导入路径
+    from {{ get_import_path(table.package_name, '', 'controller') }} import {{ underscore(table.class_name) }}
+    app.register_blueprint({{ underscore(table.class_name) }})
+{%- endfor %}
+{%- else %}
+    # 使用 pythonModelName 生成 Python 导入路径
+    from {{ get_import_path(table.package_name, '', 'controller') }} import {{ underscore(table.class_name) }}
+    app.register_blueprint({{ underscore(table.class_name) }})
+{%- endif %}

+ 38 - 39
ruoyi_generator/vm/py/controller.py.vm

@@ -1,4 +1,4 @@
-from flask import request, Blueprint
+from flask import request
 
 from ruoyi_common.base.model import AjaxResponse, TableResponse
 from ruoyi_common.constant import HttpStatus
@@ -6,12 +6,13 @@ from ruoyi_common.descriptor.serializer import JsonSerializer
 from ruoyi_common.descriptor.validator import QueryValidator, BodyValidator, PathValidator, FileDownloadValidator, FileUploadValidator
 from ruoyi_common.utils.base import ExcelUtil
 from ruoyi_framework.descriptor.permission import HasPerm, PreAuthorize
-from {{ get_import_path(table.package_name, 'domain', table.class_name) }} import {{ table.class_name }}PO
-from {{ get_import_path(table.package_name, 'domain') }} import {{ table.class_name }}
-from {{ get_import_path(table.package_name, 'service') }}.{{ underscore(table.class_name) }}_service import {{ table.class_name }}Service
+from {{ get_import_path(table.package_name, table.module_name, 'controller') }} import {{ underscore(table.class_name) }} as {{ underscore(table.class_name) }}_bp
+from {{ get_import_path(table.package_name, table.module_name, 'domain', table.class_name) }} import {{ underscore(table.class_name) }}_po
+from {{ get_import_path(table.package_name, table.module_name, 'domain') }} import {{ table.class_name }}
+from {{ get_import_path(table.package_name, table.module_name, 'service') }}.{{ underscore(table.class_name) }}_service import {{ table.class_name }}Service
 
-# 创建蓝图
-gen = Blueprint('{{ underscore(table.class_name) }}', __name__, url_prefix='/{{ table.module_name }}/{{ table.business_name }}')
+# 使用 controller/__init__.py 中定义的蓝图
+gen = {{ underscore(table.class_name) }}_bp
 
 {{ table.class_name|lower }}_service = {{ table.class_name }}Service()
 
@@ -19,56 +20,54 @@ gen = Blueprint('{{ underscore(table.class_name) }}', __name__, url_prefix='/{{
 @QueryValidator(is_page=True)
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:list'))
 @JsonSerializer()
-def {{ table.business_name }}_list(dto: {{ table.class_name }}PO):
+def {{ table.business_name }}_list(dto: {{ underscore(table.class_name) }}_po):
     """查询{{ table.function_name }}列表"""
-    {{ table.class_name|lower }} = {{ table.class_name }}()
+    {{ underscore(table.class_name) }}_entity = {{ table.class_name }}()
     # 转换PO到Entity对象
     for attr in dto.model_fields.keys():
-        if hasattr({{ table.class_name|lower }}, attr):
-            setattr({{ table.class_name|lower }}, attr, getattr(dto, attr))
-    {{ table.business_name }}s, total = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_list({{ table.class_name|lower }})
+        if hasattr({{ underscore(table.class_name) }}_entity, attr):
+            setattr({{ underscore(table.class_name) }}_entity, attr, getattr(dto, attr))
+    {{ table.business_name }}s, total = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_list({{ underscore(table.class_name) }}_entity)
     return TableResponse(code=HttpStatus.SUCCESS, msg='查询成功', rows={{ table.business_name }}s, total=total)
 
 {% if table.pk_column %}
-@gen.route('/<int:{{ underscore(table.pk_column.java_field) }}}', methods=['GET'])
+@gen.route('/<int:{{ underscore(table.pk_column.java_field) }}>', methods=['GET'])
 @PathValidator()
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:query'))
 @JsonSerializer()
 def get_{{ table.business_name }}({{ underscore(table.pk_column.java_field) }}: int):
     """获取{{ table.function_name }}详细信息"""
-    {{ table.class_name|lower }} = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_by_id({{ underscore(table.pk_column.java_field) }})
-    return AjaxResponse.from_success(data={{ table.class_name|lower }})
+    {{ underscore(table.class_name) }}_entity = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_by_id({{ underscore(table.pk_column.java_field) }})
+    return AjaxResponse.from_success(data={{ underscore(table.class_name) }}_entity)
 {% endif %}
 
 @gen.route('', methods=['POST'])
 @BodyValidator()
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:add'))
 @JsonSerializer()
-def add_{{ table.business_name }}(dto: {{ table.class_name }}PO):
+def add_{{ table.business_name }}(dto: {{ underscore(table.class_name) }}_po):
     """新增{{ table.function_name }}"""
-    {{ table.class_name|lower }} = {{ table.class_name }}()
+    {{ underscore(table.class_name) }}_entity = {{ table.class_name }}()
     # 转换PO到Entity对象
     for attr in dto.model_fields.keys():
-        if hasattr({{ table.class_name|lower }}, attr):
-            setattr({{ table.class_name|lower }}, attr, getattr(dto, attr))
-    result = {{ table.class_name|lower }}_service.insert_{{ table.business_name }}({{ table.class_name|lower }})
+        if hasattr({{ underscore(table.class_name) }}_entity, attr):
+            setattr({{ underscore(table.class_name) }}_entity, attr, getattr(dto, attr))
+    result = {{ table.class_name|lower }}_service.insert_{{ table.business_name }}({{ underscore(table.class_name) }}_entity)
     return AjaxResponse.from_success(msg='新增成功' if result > 0 else '新增失败')
 
 {% if table.pk_column %}
-@gen.route('/<int:{{ underscore(table.pk_column.java_field) }}}', methods=['PUT'])
+@gen.route('', methods=['PUT'])
 @BodyValidator()
-@PathValidator()
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:edit'))
 @JsonSerializer()
-def update_{{ table.business_name }}({{ underscore(table.pk_column.java_field) }}: int, dto: {{ table.class_name }}PO):
+def update_{{ table.business_name }}(dto: {{ underscore(table.class_name) }}_po):
     """修改{{ table.function_name }}"""
-    {{ table.class_name|lower }} = {{ table.class_name }}()
+    {{ underscore(table.class_name) }}_entity = {{ table.class_name }}()
     # 转换PO到Entity对象
     for attr in dto.model_fields.keys():
-        if hasattr({{ table.class_name|lower }}, attr):
-            setattr({{ table.class_name|lower }}, attr, getattr(dto, attr))
-    {{ table.class_name|lower }}.{{ underscore(table.pk_column.java_field) }} = {{ underscore(table.pk_column.java_field) }}
-    result = {{ table.class_name|lower }}_service.update_{{ table.business_name }}({{ table.class_name|lower }})
+        if hasattr({{ underscore(table.class_name) }}_entity, attr):
+            setattr({{ underscore(table.class_name) }}_entity, attr, getattr(dto, attr))
+    result = {{ table.class_name|lower }}_service.update_{{ table.business_name }}({{ underscore(table.class_name) }}_entity)
     return AjaxResponse.from_success(msg='修改成功' if result > 0 else '修改失败')
 {% endif %}
 
@@ -91,16 +90,16 @@ def delete_{{ table.business_name }}(ids: str):
 @FileDownloadValidator()
 @QueryValidator()
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:export'))
-def export_{{ table.business_name }}(dto: {{ table.class_name }}PO):
+def export_{{ table.business_name }}(dto: {{ underscore(table.class_name) }}_po):
     """导出{{ table.function_name }}列表"""
-    {{ table.class_name|lower }} = {{ table.class_name }}()
+    {{ underscore(table.class_name) }}_entity = {{ table.class_name }}()
     # 转换PO到Entity对象
     for attr in dto.model_fields.keys():
-        if hasattr({{ table.class_name|lower }}, attr):
-            setattr({{ table.class_name|lower }}, attr, getattr(dto, attr))
-    {{ table.business_name }}s, total = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_list({{ table.class_name|lower }})
+        if hasattr({{ underscore(table.class_name) }}_entity, attr):
+            setattr({{ underscore(table.class_name) }}_entity, attr, getattr(dto, attr))
+    {{ table.business_name }}s, total = {{ table.class_name|lower }}_service.select_{{ table.business_name }}_list({{ underscore(table.class_name) }}_entity)
     # 使用ExcelUtil导出Excel文件
-    excel_util = ExcelUtil({{ table.class_name }}PO)
+    excel_util = ExcelUtil({{ underscore(table.class_name) }}_po)
     return excel_util.export_response({{ table.business_name }}s, "{{ table.function_name }}数据")
 
 @gen.route('/importTemplate', methods=['POST'])
@@ -108,7 +107,7 @@ def export_{{ table.business_name }}(dto: {{ table.class_name }}PO):
 @PreAuthorize(HasPerm('{{ table.module_name }}:{{ table.business_name }}:import'))
 def import_template():
     """下载{{ table.function_name }}导入模板"""
-    excel_util = ExcelUtil({{ table.class_name }}PO)
+    excel_util = ExcelUtil({{ underscore(table.class_name) }}_po)
     return excel_util.import_template_response("{{ table.function_name }}导入模板")
 
 @gen.route('/importData', methods=['POST'])
@@ -126,17 +125,17 @@ def import_data():
     
     try:
         # 使用ExcelUtil读取Excel文件
-        excel_util = ExcelUtil({{ table.class_name }}PO)
+        excel_util = ExcelUtil({{ underscore(table.class_name) }}_po)
         po_list = excel_util.import_file(file, sheetname="{{ table.function_name }}数据")
         
         # 转换为Entity对象列表
         {{ table.business_name }}_list = []
         for po in po_list:
-            {{ table.class_name|lower }} = {{ table.class_name }}()
+            {{ underscore(table.class_name) }}_entity = {{ table.class_name }}()
             for attr in po.model_fields.keys():
-                if hasattr({{ table.class_name|lower }}, attr):
-                    setattr({{ table.class_name|lower }}, attr, getattr(po, attr))
-            {{ table.business_name }}_list.append({{ table.class_name|lower }})
+                if hasattr({{ underscore(table.class_name) }}_entity, attr):
+                    setattr({{ underscore(table.class_name) }}_entity, attr, getattr(po, attr))
+            {{ table.business_name }}_list.append({{ underscore(table.class_name) }}_entity)
         
         # 调用Service层处理导入逻辑
         msg = {{ table.class_name|lower }}_service.import_{{ table.business_name }}({{ table.business_name }}_list, update_support)

+ 18 - 28
ruoyi_generator/vm/py/entity.py.vm

@@ -3,46 +3,36 @@
 # @FileName: {{ table.class_name }}.py
 # @Time    : {{ datetime }}
 
-from typing import Optional, Union
+from typing import Optional
 from datetime import datetime
+from pydantic import Field
 from ruoyi_common.base.model import BaseEntity
-from sqlalchemy import Column, String, Integer, Float, Boolean, DateTime, Text, BigInteger
-from ruoyi_common.utils.base import DateUtil
 
 class {{ table.class_name }}(BaseEntity):
     """
     {{ table.table_comment }}对象
     """
-    
-    __tablename__ = '{{ table.table_name }}'
 {%- for column in table.columns %}
 {%- if column.java_type == 'String' or column.java_type == 'str' %}
-    {{ underscore(column.java_field) }} = Column(String(255), default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[str] = Field(default=None, description="{{ column.column_comment }}")
 {%- elif column.java_type == 'Integer' or column.java_type == 'int' %}
-    {{ underscore(column.java_field) }} = Column(Integer, default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[int] = Field(default=None, description="{{ column.column_comment }}")
 {%- elif column.java_type == 'Long' %}
-    {{ underscore(column.java_field) }} = Column(BigInteger, default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[int] = Field(default=None, description="{{ column.column_comment }}")
 {%- elif column.java_type == 'Float' or column.java_type == 'Double' %}
-    {{ underscore(column.java_field) }} = Column(Float, default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[float] = Field(default=None, description="{{ column.column_comment }}")
 {%- elif column.java_type == 'Boolean' or column.java_type == 'bool' %}
-    {{ underscore(column.java_field) }} = Column(Boolean, default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[bool] = Field(default=None, description="{{ column.column_comment }}")
+{%- elif column.java_type == 'Date' or column.java_type == 'DateTime' %}
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[datetime] = Field(default=None, description="{{ column.column_comment }}")
 {%- else %}
-    {{ underscore(column.java_field) }} = Column(String(255), default=None, doc="{{ column.column_comment }}")
+    # {{ column.column_comment }}
+    {{ underscore(column.java_field) }}: Optional[str] = Field(default=None, description="{{ column.column_comment }}")
 {%- endif %}
-{%- endfor %}
-
-    def __init__(self, **kwargs):
-        super().__init__(**kwargs)
-        # 初始化所有字段
-{%- for column in table.columns %}
-        self.{{ underscore(column.java_field) }} = kwargs.get('{{ underscore(column.java_field) }}', None)
-{%- endfor %}
-
-    def to_dict(self):
-        """转换为字典"""
-        result = super().to_dict()
-        # 添加实体类特定字段
-{%- for column in table.columns %}
-        result['{{ underscore(column.java_field) }}'] = self.{{ underscore(column.java_field) }}
-{%- endfor %}
-        return result
+{%- endfor %}

+ 1 - 1
ruoyi_generator/vm/py/mapper.py.vm

@@ -9,7 +9,7 @@ from datetime import datetime
 from sqlalchemy import select, update, delete, func
 
 from ruoyi_admin.ext import db
-from {{ get_import_path(table.package_name, 'domain') }} import {{ table.class_name }}
+from {{ get_import_path(table.package_name, table.module_name, 'domain') }} import {{ table.class_name }}
 
 class {{ table.class_name }}Mapper:
     """{{ table.function_name }}Mapper"""

+ 2 - 2
ruoyi_generator/vm/py/po.py.vm

@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # @Author  : {{ table.function_author }}
-# @FileName: {{ table.class_name }}PO.py
+# @FileName: {{ underscore(table.class_name) }}_po.py
 # @Time    : {{ datetime }}
 
 from typing import Optional, Union
@@ -11,7 +11,7 @@ from pydantic import ConfigDict, Field
 from ruoyi_common.base.model import BaseEntity
 from ruoyi_common.utils.base import DateUtil
 
-class {{ table.class_name }}PO(BaseEntity):
+class {{ underscore(table.class_name) }}_po(BaseEntity):
     """
     {{ table.table_comment }}PO对象
     """

+ 2 - 2
ruoyi_generator/vm/py/service.py.vm

@@ -5,8 +5,8 @@
 
 from typing import List, Tuple
 
-from {{ get_import_path(table.package_name, 'domain') }} import {{ table.class_name }}
-from {{ get_import_path(table.package_name, 'mapper') }}.{{ underscore(table.class_name) }}_mapper import {{ table.class_name }}Mapper
+from {{ get_import_path(table.package_name, table.module_name, 'domain') }} import {{ table.class_name }}
+from {{ get_import_path(table.package_name, table.module_name, 'mapper') }}.{{ underscore(table.class_name) }}_mapper import {{ table.class_name }}Mapper
 
 class {{ table.class_name }}Service:
     """{{ table.function_name }}服务类"""

+ 1 - 1
ruoyi_generator/vm/sql/menu.sql.vm

@@ -1,6 +1,6 @@
 -- 菜单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 ('{{ table.function_name }}', {{ table.parent_menu_id }}, 1, '{{ table.business_name }}', '{{ table.module_name }}/{{ table.business_name }}/index', 1, 0, 'C', '0', '0', '{{ table.module_name }}:{{ table.business_name }}:list', '#', 'admin', SYSDATE(), '', NULL, '{{ table.function_name }}菜单');
+VALUES ('{{ table.function_name }}', {% if table.parent_menu_id %}{{ table.parent_menu_id }}{% else %}NULL{% endif %}, 1, '{{ table.business_name }}', '{{ table.module_name }}/{{ table.business_name }}/index', 1, 0, 'C', '0', '0', '{{ table.module_name }}:{{ table.business_name }}:list', '#', 'admin', SYSDATE(), '', NULL, '{{ table.function_name }}菜单');
 
 -- 按钮父菜单ID
 SELECT @parentId := LAST_INSERT_ID();