diff --git a/README.md b/README.md index 73dfae62625878d8575a228cdac2fa0c46243ae6..6862dc7deb3476315bb928d86f945329d0973f79 100644 --- a/README.md +++ b/README.md @@ -1,397 +1,21 @@ -
-

- logo -

-

FastApiAdmin v2.0.0

-

现代化全栈快速开发平台

-

如果你喜欢这个项目,给个 ⭐️ 支持一下吧!

-

- - Gitee Stars - - - GitHub Stars - - - License - - - - - - - - -

+## 2025-12-05 +### 目标: 支持 SqlLite 数据库 [sqlserver 未测试,应该也可以支持] +- requirements.txt 新增 + - aiosqlite==0.17.0 # sqlite 异步操作数据库 -简体中文 | [English](./README.en.md) +- backend/app/api/v1/module_generator/gencode/crud.py + - 替换原有的直接SQL查询方式,改用SQLAlchemy Inspector来获取数据库表信息和列信息,提高代码的可维护性和数据库兼容性。 + - 新增同步查询方法并通过run_sync在异步环境中调用,统一处理不同数据库类型的差异。 -
+- backend/app/config/setting.py + - 增加对 sqlite 和 sqlserver 数据库的支持。 -## 📘 项目介绍 +- backend/app/core/database.py + - 对 sqlite 数据库引擎进行特殊配置,如禁用连接池。 -**FastApiAdmin** 是一套 **完全开源、高度模块化、技术先进的现代化快速开发平台**,旨在帮助开发者高效搭建高质量的企业级中后台系统。该项目采用 **前后端分离架构**,融合 Python 后端框架 `FastAPI` 和前端主流框架 `Vue3` 实现多端统一开发,提供了一站式开箱即用的开发体验。 +- env/.env.dev + - DATABASE_TYPE 配置项,支持 sqlite、mysql、postgres 等数据库类型。 -> **设计初心**: 以模块化、松耦合为核心,追求丰富的功能模块、简洁易用的接口、详尽的开发文档和便捷的维护方式。通过统一框架和组件,降低技术选型成本,遵循开发规范和设计模式,构建强大的代码分层模型,搭配完善的本地中文化支持,专为团队和企业开发场景量身定制。 - -## 🔗 源码仓库 - -| 平台 | 仓库地址 | -|------|----------| -| GitHub | [FastapiAdmin主工程](https://github.com/1014TaoTao/FastapiAdmin.git) \| [FastDocs官网](https://github.com/1014TaoTao/FastDocs.git) \| [FastApp移动端](https://github.com/1014TaoTao/FastApp.git) | -| Gitee | [FastapiAdmin主工程](https://gitee.com/tao__tao/FastapiAdmin.git) \| [FastDocs官网](https://gitee.com/tao__tao/FastDocs.git) \| [FastApp移动端](https://gitee.com/tao__tao/FastApp.git) | - -## 🎯 核心优势 - -| 优势 | 描述 | -| ---- | ---- | -| 🔥 **现代化技术栈** | 基于 FastAPI + Vue3 + TypeScript 等前沿技术构建 | -| ⚡ **高性能异步** | 利用 FastAPI 异步特性和 Redis 缓存优化响应速度 | -| 🔐 **安全可靠** | JWT + OAuth2 认证机制,RBAC 权限控制模型 | -| 🧱 **模块化设计** | 高度解耦的系统架构,便于扩展和维护 | -| 🌐 **全栈支持** | Web端 + 移动端(H5) + 后端一体化解决方案 | -| 🚀 **快速部署** | Docker 一键部署,支持生产环境快速上线 | -| 📖 **完善文档** | 详细的开发文档和教程,降低学习成本 | - -## 📦 工程结构概览 - -```sh -FastapiAdmin -├─ backend # 后端工程 (FastAPI + Python) -├─ frontend # Web前端工程 (Vue3 + Element Plus) -├─ fastapp # 移动端工程 (UniApp + Wot Design Uni) -├─ fastdocs # 官网文档工程 (VitePress) -├─ devops # 部署配置 -├─ docker-compose.yaml # Docker编排文件 -├─ deploy.sh # 一键部署脚本 -├─ LICENSE # 开源协议 -|─ README.en.md # 英文文档 -└─ README.md # 中文文档 -``` - -## 🛠️ 技术栈概览 - -| 类型 | 技术选型 | 描述 | -|------|----------|------| -| **后端框架** | FastAPI / Uvicorn / Pydantic 2.0 / Alembic | 现代、高性能的异步框架,强制类型约束,数据迁移 | -| **ORM** | SQLAlchemy 2.0 | 强大的 ORM 库 | -| **定时任务** | APScheduler | 轻松实现定时任务 | -| **权限认证** | PyJWT | 实现 JWT 认证 | -| **前端框架** | Vue3 / Vite5 / Pinia / TypeScript | 快速开发 Vue3 应用 | -| **Web UI** | ElementPlus | 企业级 UI 组件库 | -| **移动端** | UniApp / Wot Design Uni | 跨端移动应用框架 | -| **数据库** | MySQL / MongoDB | 关系型和文档型数据库支持 | -| **缓存** | Redis | 高性能缓存数据库 | -| **文档** | Swagger / Redoc | 自动生成 API 文档 | -| **部署** | Docker / Nginx / Docker Compose | 容器化部署方案 | - -## 📌 内置功能模块 - -| 模块 | 功能 | 描述 | -|------|------|------| -| 📊 **仪表盘** | 工作台、分析页 | 系统概览和数据分析 | -| ⚙️ **系统管理** | 用户、角色、菜单、部门、岗位、字典、配置、公告 | 核心系统管理功能 | -| 👀 **监控管理** | 在线用户、服务器监控、缓存监控 | 系统运行状态监控 | -| 📋 **任务管理** | 定时任务 | 异步任务调度管理 | -| 📝 **日志管理** | 操作日志 | 用户行为审计 | -| 🧰 **开发工具** | 代码生成、表单构建、接口文档 | 提升开发效率的工具 | -| 📁 **文件管理** | 文件存储 | 统一文件管理 | - -## 🍪 演示环境 - -- 🌐 官网地址:[https://service.fastapiadmin.com](https://service.fastapiadmin.com) -- 💻 Web演示:[https://service.fastapiadmin.com/web](https://service.fastapiadmin.com/web) -- 📱 移动端:[https://service.fastapiadmin.com/app](https://service.fastapiadmin.com/app) -- 👤 登录账号:`admin` 密码:`123456` - -## 🚀 快速开始 - -### 环境要求 - -| 类型 | 技术栈 | 版本 | -|------|--------|------| -| 后端 | Python | ≥ 3.10 | -| 后端 | FastAPI | 0.109+ | -| 前端 | Node.js | ≥ 20.0 | -| 前端 | Vue3 | 3.3+ | -| 数据库 | MySQL | 8.0+ | -| 缓存 | Redis | 7.0+ | - -### 获取代码 - -```bash -# 克隆代码到本地 -git clone https://gitee.com/tao__tao/FastapiAdmin.git -# 或者 -git clone https://github.com/1014TaoTao/FastapiAdmin.git -``` - -### 后端启动 - -```bash -# 进入后端工程目录 -cd backend - -# 安装依赖 -pip3 install -r requirements.txt - -# 启动后端服务:启动之前保证mysql中创建好了数据库、redis服务 -python main.py run -# 或指定环境 -python main.py run --env=dev - -# 生成迁移文件 -python main.py revision --env=dev -# 应用迁移 -python main.py upgrade --env=dev -``` - -### 前端启动 - -```bash -# 进入前端工程目录 -cd frontend - -# 安装依赖 -pnpm install - -# 启动开发服务器 -pnpm run dev - -# 构建生产版本 -pnpm run build -``` - -### 移动端启动 - -```bash -# 进入移动端工程目录 -cd fastapp - -# 安装依赖 -pnpm install - -# 启动H5开发服务器 -pnpm run dev:h5 - -# 构建H5生产版本 -pnpm run build:h5 -``` - -### 文档启动 - -```bash -# 进入文档工程目录 -cd fastdocs - -# 安装依赖 -pnpm install - -# 启动文档开发服务器 -pnpm run docs:dev - -# 构建文档生产版本 -pnpm run docs:build -``` - -### 访问地址 - -- 🏠 项目官网:[http://localhost:5180](http://localhost:5180) -- 🖥️ Web端:[http://localhost:5180/web](http://localhost:5180/web) -- 📱 移动端:[http://localhost:5180/app](http://localhost:5180/app) - -默认账号: -- 管理员:`admin` / `123456` - -## 🐳 Docker 部署 - -```bash -# 复制部署脚本到服务器并赋予执行权限 -chmod +x deploy.sh - -# 执行一键部署 -./deploy.sh - -# 常用 Docker 命令 -# 查看运行中的容器 -docker compose ps - -# 查看容器日志 -docker logs -f <容器名> - -# 停止服务 -docker compose down -``` - -## 🔧 模块展示 - -### web 端 - -| 模块名
| 截图 | -| ----- | --- | -| 登录 | ![登录](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/login.png) | -| 仪表盘 | ![仪表盘](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/dashboard.png) | -| 分析页 | ![分析页](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/analysis.png) | -| 菜单管理 | ![菜单管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/menu.png) | -| 部门管理 | ![部门管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/dept.png) | -| 岗位管理 | ![岗位管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/position.png) | -| 角色管理 | ![角色管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/role.png) | -| 用户管理 | ![用户管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/user.png) | -| 日志管理 | ![日志管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/log.png) | -| 配置管理 | ![配置管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/config.png) | -| 在线用户 | ![在线用户](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/online.png) | -| 服务器监控 | ![服务器监控](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/service.png) | -| 缓存监控 | ![缓存监控](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/cache.png) | -| 任务管理 | ![任务管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/job.png) | -| 字典管理 | ![字典管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/dict.png) | -| 接口管理 | ![接口管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/docs.png) | -| 系统主题 | ![系统主题](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/theme.png) | -| 在线文档 | ![在线文档](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/help.png) | -| 系统锁屏 | ![系统锁屏](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/lock.png) | -| 表单构建 | ![表单构建](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/form.png) | -| 代码生成 | ![代码生成](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/gencode.png) | -| 流程管理 | ![流程管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/workflow.png) | -| 文件管理 | ![文件管理](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/file.png) | -| 我的应用 | ![我的应用](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/myapp.png) | -| 配置中心 | ![配置中心](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/setting.png) | -| 智能助手 | ![智能助手](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/ai.png) | - -### 移动端 - -| 模块
| 详情 | 模块
| 详情 | 模块
| 详情 | -|----------|------|----------|------|----------|------| -| 登录 | ![移动端登录](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_login.png) | 首页 | ![移动端首页](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_home.png) | 我的 | ![移动端个人中心](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_mine.png) | -| 个人 | ![移动端个人信息](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_profile.png) | 设置 | ![移动端设置](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_setting.png) | 工作台 | ![移动端工作台](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/app_work.png) | - -## 🛠️ 二开教程 - -### 后端开发 - -1. **编写实体类层**:在 `backend/app/api/v1/module_generator/demo/model.py` 中创建 example 的 ORM 模型(对应 Spring Boot 中的实体类层) -2. **编写数据模型层**:在 `backend/app/api/v1/module_generator/demo/schema.py` 中创建 example 数据模型(对应 Spring Boot 中的 DTO 层) -3. **编写查询参数模型层**:在 `backend/app/api/v1/module_generator/demo/param.py` 中创建 example 的查询参数模型(对应 Spring Boot 中的 DTO 层) -4. **编写持久化层**:在 `backend/app/api/v1/cruds/module_generator/crud.py` 中创建 example 数据层(对应 Spring Boot 中的 Mapper 或 DAO 层) -5. **编写业务层**:在 `backend/app/api/v1/services/module_generator/service.py` 中创建 example 数据层(对应 Spring Boot 中的 Service 层) -6. **编写接口层**:在 `backend/app/api/v1/controllers/module_generator/controller.py` 中创建 example 数据层(对应 Spring Boot 中的 Controller 层) -7. **将 demo 模块添加至系统初始化脚本**:在 `backend/app/scripts/initialize.py` 中添加(如果需要可以把 demo 的菜单权限,配置到 `backend/app/scripts/data/system_menu.json` 和 `backend/app/scripts/data/system_role_menus.json` 或从前端页面菜单中新增) -8. **将 demo 模块添加至数据库迁移脚本中**:在 `backend/app/alembic/env.py` 中添加 - -### 代码生成教程 - -代码生成模块是本项目的核心功能之一,可以帮助开发者快速生成完整的 CRUD 代码,大幅提升开发效率。该模块基于 Jinja2 模板引擎,可生成前后端一体化的完整功能模块。 - -#### 代码生成流程 - -1. **创建或导入数据表**: - - 方式一:通过"创建表"功能直接在系统中创建新表 - - 方式二:通过"导入"功能将现有数据库表导入到代码生成器中 - -2. **配置生成参数**: - - 基础配置: - - 表名称、表描述、实体类名称 - - 生成配置: - - 生成包路径(package_name):如 `student` - - 生成模块名(module_name):如 `student` - - 生成业务名(business_name):如 `student` - - 生成功能名(function_name):如 `学生管理` - - 生成代码方式:zip压缩包下载 或 项目目录写入 - - 上级菜单:选择生成功能所属的菜单分类 - -3. **字段配置**: - - 配置每个字段的: - - 字段列类型、Python类型、Python字段名 - - 是否为主键、是否自增、是否必填 - - 是否为插入字段、编辑字段、列表字段、查询字段 - - 查询方式(等于、不等于、大于、小于、范围) - - 显示类型(文本框、文本域、下选框、复选框、单选框、日期控件) - - 字典类型(用于下拉框数据源) - -4. **代码预览**: - - 预览将要生成的代码内容 - - 支持预览后端(Python)、前端(Vue/TS)、数据库(SQL)代码 - - 可按类型筛选预览内容 - -5. **生成代码**: - - 点击"下载代码"生成并下载zip压缩包 - - 点击"写入本地"直接写入到项目目录中 - -#### 生成的文件结构 - -代码生成器会生成完整的前后端代码结构: - -**后端文件**: -- 控制器层:`backend/app/api/v1/module_{module_name}/{business_name}/controller.py` -- 服务层:`backend/app/api/v1/module_{module_name}/{business_name}/service.py` -- 数据访问层:`backend/app/api/v1/module_{module_name}/{business_name}/crud.py` -- 数据模型层:`backend/app/api/v1/module_{module_name}/{business_name}/model.py` -- 数据模式层:`backend/app/api/v1/module_{module_name}/{business_name}/schema.py` - -**前端文件**: -- API接口文件:`frontend/src/api/module_{module_name}/{business_name}.ts` -- 页面组件文件:`frontend/src/views/module_{module_name}/{business_name}/index.vue` - -**数据库文件**: -- 菜单SQL文件:`backend/sql/module_{module_name}/{business_name}_menu.sql` - -#### 使用示例 - -1. 在数据库中创建新表,如 `sys_student` -2. 登录系统,进入**代码生成**模块 -3. 点击"导入",选择 `sys_student` 表 -4. 配置生成参数: - - 生成包路径:`student` - - 生成模块名:`student` - - 生成业务名:`student` - - 生成功能名:`学生管理` - - 上级菜单:系统管理 -5. 配置字段属性(如设置哪些字段需要显示、查询、编辑等) -6. 点击"预览代码"查看生成的代码 -7. 点击"下载代码"或"写入本地"生成完整功能模块 -8. 重启服务,新功能模块即可使用 - -### 前端部分 - -1. **前端接入后端接口地址**:在 `frontend/src/api/demo/example.ts` 中配置 -2. **编写前端页面**:在 `frontend/src/views/demo/example/index.vue` 中编写 - -### 移动端部分 - -1. **移动端接入后端接口地址**:在 `fastapp/src/api` 中编写 -2. **编写移动端页面**:在 `fastapp/src/pages` 中编写 - -## ℹ️ 帮助 - -更多详情请查看 [官方文档](https://service.fastapiadmin.com) - -## 👥 贡献者 - - - - - -## 🙏 特别鸣谢 - -感谢以下开源项目的贡献和支持: - -- [FastAPI](https://fastapi.tiangolo.com/) -- [Pydantic](https://docs.pydantic.dev/) -- [SQLAlchemy](https://www.sqlalchemy.org/) -- [APScheduler](https://github.com/agronholm/apscheduler) -- [Vue3](https://cn.vuejs.org/) -- [TypeScript](https://www.typescriptlang.org/) -- [Vite](https://github.com/vitejs/vite) -- [Element Plus](https://element-plus.org/) -- [UniApp](https://uniapp.dcloud.net.cn/) -- [Wot-Design-UI](https://wot-ui.cn/) - -## 🎨 社区交流 - -| 微信二维码 | 群组二维码 | 微信支付二维码 | -| --- | --- | --- | -| ![微信二维码](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/wechat.jpg) | ![群组二维码](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/group.jpg) | ![微信支付二维码](https://gitee.com/tao__tao/FastDocs/raw/main/src/public/wechatPay.jpg) | - -## ❤️ 支持项目 - -如果你喜欢这个项目,请给我一个 ⭐️ Star 支持一下吧!非常感谢! - -[![Stargazers over time](https://starchart.cc/1014TaoTao/FastapiAdmin.svg?variant=adaptive)](https://starchart.cc/1014TaoTao/FastapiAdmin) +- Todo: + - 测试 sqlserver 数据库连接 + - 数据库方言相关的代码 \ No newline at end of file diff --git a/backend/app/api/v1/module_generator/gencode/crud.py b/backend/app/api/v1/module_generator/gencode/crud.py index f2a95a0cf174c48056aee96d61badf4cf7bdd5df..e540d53aab3d440ff6803ef8a1ec61be9617902c 100644 --- a/backend/app/api/v1/module_generator/gencode/crud.py +++ b/backend/app/api/v1/module_generator/gencode/crud.py @@ -2,6 +2,7 @@ from sqlalchemy.engine.row import Row from sqlalchemy import and_, select, text +from sqlalchemy import inspect as sa_inspect from typing import Sequence from sqlglot.expressions import Expression @@ -118,6 +119,42 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): """ await self.delete(ids=ids) + @staticmethod + def _sync_get_table_info(session, bind, database_type): + """ + 同步函数:获取数据库表信息 + + 参数: + - session: 数据库会话对象(由run_sync自动传入) + - bind: 数据库绑定对象 + - database_type: 数据库类型 + + 返回: + - tuple: (table_names, table_comments) + """ + # 使用SQLAlchemy Inspector获取表信息 + inspector = sa_inspect(bind) + + # 获取所有表名 + if database_type == "postgres": + table_names = inspector.get_table_names(schema='public') + else: + table_names = inspector.get_table_names() + + # 获取表注释信息 + table_comments = {} + for table_name in table_names: + try: + # 获取表选项(包括注释) + table_options = inspector.get_table_options(table_name) + comment = table_options.get('comment', '') + table_comments[table_name] = comment + except Exception: + # 如果无法获取表选项,设置为空字符串 + table_comments[table_name] = '' + + return table_names, table_comments + async def get_db_table_list(self, search: GenTableQueryParam | None = None) -> list[dict]: """ 根据查询参数获取数据库表列表信息。 @@ -128,79 +165,40 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): 返回: - list[dict]: 数据库表列表信息(已转为可序列化字典)。 """ - - # 使用更健壮的方式检测数据库方言 - if settings.DATABASE_TYPE == "postgres": - query_sql = ( - select( - text("t.table_catalog as database_name"), - text("t.table_name as table_name"), - text("t.table_type as table_type"), - text("pd.description as table_comment"), - ) - .select_from(text( - "information_schema.tables t \n" - "LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name \n" - "LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid \n" - "LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0" - )) - .where( - and_( - text("t.table_catalog = (select current_database())"), - text("t.is_insertable_into = 'YES'"), - text("t.table_schema = 'public'"), - ) - ) - ) - else: - query_sql = ( - select( - text("table_schema as database_name"), - text("table_name as table_name"), - text("table_type as table_type"), - text("table_comment as table_comment"), - ) - .select_from(text("information_schema.tables")) - .where( - and_( - text("table_schema = (select database())"), - ) - ) - ) + # 使用run_sync包装同步的inspect操作 + table_names, table_comments = await self.auth.db.run_sync( + GenTableCRUD._sync_get_table_info, + self.auth.db.get_bind(), + settings.DATABASE_TYPE + ) - # 动态条件构造 - params = {} - if search and search.table_name: - query_sql = query_sql.where( - text("lower(table_name) like lower(:table_name)") - ) - params['table_name'] = f"%{search.table_name}%" - if search and search.table_comment: - # 对于PostgreSQL,表注释字段是pd.description,而不是table_comment - if settings.DATABASE_TYPE == "postgres": - query_sql = query_sql.where( - text("lower(pd.description) like lower(:table_comment)") - ) - else: - query_sql = query_sql.where( - text("lower(table_comment) like lower(:table_comment)") - ) - params['table_comment'] = f"%{search.table_comment}%" - - # 执行查询并绑定参数 - all_data = (await self.auth.db.execute(query_sql, params)).fetchall() - - # 将Row对象转换为字典列表,解决JSON序列化问题 + # 构造结果数据 dict_data = [] - for row in all_data: - # 检查row是否为Row对象 - if isinstance(row, Row): - # 使用._mapping获取字典 - dict_row = GenDBTableSchema(**dict(row._mapping)).model_dump() - dict_data.append(dict_row) - else: - dict_row = GenDBTableSchema(**dict(row)).model_dump() - dict_data.append(dict_row) + database_name = self.auth.db.get_bind().url.database or 'default' + + for table_name in table_names: + # 应用搜索过滤器 + if search: + # 过滤表名 + if search.table_name and search.table_name.lower() not in table_name.lower(): + continue + + # 过滤表注释 + if search.table_comment and search.table_comment.lower() not in table_comments[table_name].lower(): + continue + + # 构造表信息字典 + table_info = { + "database_name": database_name, + "table_name": table_name, + "table_type": "BASE TABLE", # SQLAlchemy Inspector不直接提供表类型,我们假设都是BASE TABLE + "table_comment": table_comments[table_name] + } + + # 使用GenDBTableSchema验证并转换为字典 + dict_row = GenDBTableSchema(**table_info).model_dump() + dict_data.append(dict_row) + return dict_data async def get_db_table_list_by_names(self, table_names: list[str]) -> list[GenDBTableSchema]: @@ -216,68 +214,19 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): # 处理空列表情况 if not table_names: return [] - - # 使用更健壮的方式检测数据库方言 - if settings.DATABASE_TYPE == "postgres": - # PostgreSQL使用ANY操作符和正确的参数绑定 - query_sql = """ - SELECT - t.table_catalog as database_name, - t.table_name as table_name, - t.table_type as table_type, - pd.description as table_comment - FROM - information_schema.tables t - LEFT JOIN pg_catalog.pg_class c ON c.relname = t.table_name - LEFT JOIN pg_catalog.pg_namespace n ON n.nspname = t.table_schema AND c.relnamespace = n.oid - LEFT JOIN pg_catalog.pg_description pd ON pd.objoid = c.oid AND pd.objsubid = 0 - WHERE - t.table_catalog = (select current_database()) - AND t.is_insertable_into = 'YES' - AND t.table_schema = 'public' - AND t.table_name = ANY(:table_names) - """ - else: - query_sql = """ - SELECT - table_schema as database_name, - table_name as table_name, - table_type as table_type, - table_comment as table_comment - FROM - information_schema.tables - WHERE - table_schema = (select database()) - AND table_name IN :table_names - """ - # 创建新的数据库会话上下文来执行查询,避免受外部事务状态影响 - try: - # 去重表名列表,避免重复查询 - unique_table_names = list(set(table_names)) - - # 使用只读事务执行查询,不影响主事务 - if settings.DATABASE_TYPE == "postgres": - gen_db_table_list = (await self.auth.db.execute(text(query_sql), {"table_names": unique_table_names})).fetchall() - else: - gen_db_table_list = (await self.auth.db.execute(text(query_sql), {"table_names": tuple(unique_table_names)})).fetchall() - except Exception as e: - log.error(f"查询表信息时发生错误: {e}") - # 查询错误时直接抛出,不需要事务处理 - raise + # 调用get_db_table_list获取所有表信息 + all_tables = await self.get_db_table_list() - # 将Row对象转换为字典列表,解决JSON序列化问题 - dict_data = [] - for row in gen_db_table_list: - # 检查row是否为Row对象 - if isinstance(row, Row): - # 使用._mapping获取字典 - dict_row = GenDBTableSchema(**dict(row._mapping)) - dict_data.append(dict_row) - else: - dict_row = GenDBTableSchema(**dict(row)) - dict_data.append(dict_row) - return dict_data + # 过滤出指定名称的表 + table_names_set = set(table_names) # 转换为集合以提高查找效率 + filtered_tables = [ + GenDBTableSchema(**table) + for table in all_tables + if table["table_name"] in table_names_set + ] + + return filtered_tables async def check_table_exists(self, table_name: str) -> bool: """ @@ -290,14 +239,15 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): - bool: 如果表存在返回True,否则返回False。 """ try: - # 根据不同数据库类型使用不同的查询方式 - if settings.DATABASE_TYPE.lower() == 'mysql': - query = text("SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table_name") - else: - query = text("SELECT 1 FROM pg_tables WHERE tablename = :table_name") + # 使用SQLAlchemy的inspect功能检查表是否存在,这是数据库无关的方法 + bind = self.auth.db.get_bind() + inspector = sa_inspect(bind) - result = await self.auth.db.execute(query, {"table_name": table_name}) - return result.scalar() is not None + # 对于PostgreSQL,需要指定schema + if settings.DATABASE_TYPE.lower() == 'postgres': + return inspector.has_table(table_name, schema='public') + else: + return inspector.has_table(table_name) except Exception as e: log.error(f"检查表格存在性时发生错误: {e}") # 出错时返回False,避免误报表已存在 @@ -325,6 +275,24 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): log.error(f"创建表时发生错误: {e}") return False + async def execute_sql(self, sql: str) -> bool: + """ + 执行SQL语句。 + + 参数: + - sql (str): 要执行的SQL语句。 + + 返回: + - bool: 是否执行成功。 + """ + try: + # 执行SQL但不手动提交事务,由框架管理事务生命周期 + await self.auth.db.execute(text(sql)) + return True + except Exception as e: + log.error(f"执行SQL时发生错误: {e}") + return False + class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, GenTableColumnSchema]): """代码生成业务表字段模块数据库操作层""" @@ -337,6 +305,81 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen - auth (AuthSchema): 认证信息模型 """ super().__init__(model=GenTableColumnModel, auth=auth) + + @staticmethod + def _sync_get_table_columns(session, bind, database_type, table_name): + """ + 同步函数:获取数据库表的列信息 + + 参数: + - session: 数据库会话对象(由run_sync自动传入) + - bind: 数据库绑定对象 + - database_type: 数据库类型 + - table_name: 表名 + + 返回: + - list: 列信息列表 + """ + # 使用SQLAlchemy Inspector获取表列信息 + inspector = sa_inspect(bind) + + # 获取列信息 + columns = inspector.get_columns(table_name) + + # 获取主键信息 + try: + pk_constraint = inspector.get_pk_constraint(table_name) + primary_keys = set(pk_constraint.get("constrained_columns", [])) if pk_constraint else set() + except Exception: + primary_keys = set() + + # 获取唯一约束信息 + unique_columns = set() + try: + unique_constraints = inspector.get_unique_constraints(table_name) + for constraint in unique_constraints: + unique_columns.update(constraint.get("column_names", [])) + except Exception: + pass + + # 处理列信息 + columns_list = [] + for idx, column in enumerate(columns): + # 获取列的基本信息 + column_name = column['name'] + column_type = str(column['type']) + is_nullable = column.get('nullable', True) + column_default = column.get('default', None) + # 获取列注释(如果有的话) + column_comment = column.get('comment', '') + # 判断是否为主键 + is_pk = column_name in primary_keys + # 判断是否为唯一约束 + is_unique = column_name in unique_columns + # 判断是否为自增列(基于数据库类型和列类型) + is_increment = column.get('autoincrement', False) in (True, 'auto') + # 获取列长度(如果适用) + column_length = None + if hasattr(column['type'], 'length') and column['type'].length: + column_length = str(column['type'].length) + + # 构造列信息字典 + column_info = { + "column_name": column_name, + "column_comment": column_comment or '', + "column_type": column_type, + "column_length": column_length or '', + "column_default": str(column_default) if column_default is not None else '', + "sort": idx + 1, # 序号从1开始 + "is_pk": 1 if is_pk else 0, + "is_increment": 1 if is_increment else 0, + "is_nullable": 1 if is_nullable else 0, + "is_unique": 1 if is_unique else 0 + } + + columns_list.append(column_info) + + return columns_list async def get_gen_table_column_by_id(self, id: int, preload: list | None = None) -> GenTableColumnModel | None: """根据业务表字段ID获取业务表字段信息。 @@ -388,101 +431,21 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen # 检查表名是否为空 if not table_name: raise ValueError("数据表名称不能为空") - + try: - if settings.DATABASE_TYPE == "mysql": - query_sql = """ - SELECT - c.column_name AS column_name, - c.column_comment AS column_comment, - c.column_type AS column_type, - c.character_maximum_length AS column_length, - c.column_default AS column_default, - c.ordinal_position AS sort, - (CASE WHEN c.column_key = 'PRI' THEN 1 ELSE 0 END) AS is_pk, - (CASE WHEN c.extra = 'auto_increment' THEN 1 ELSE 0 END) AS is_increment, - (CASE WHEN (c.is_nullable = 'NO' AND c.column_key != 'PRI') THEN 1 ELSE 0 END) AS is_nullable, - (CASE - WHEN c.column_name IN ( - SELECT k.column_name - FROM information_schema.key_column_usage k - JOIN information_schema.table_constraints t - ON k.constraint_name = t.constraint_name - WHERE k.table_schema = c.table_schema - AND k.table_name = c.table_name - AND t.constraint_type = 'UNIQUE' - ) THEN 1 ELSE 0 - END) AS is_unique - FROM - information_schema.columns c - WHERE c.table_schema = (SELECT DATABASE()) - AND c.table_name = :table_name - ORDER BY - c.ordinal_position - """ - else: - query_sql = """ - SELECT - c.column_name AS column_name, - COALESCE(pgd.description, '') AS column_comment, - c.udt_name AS column_type, - c.character_maximum_length AS column_length, - c.column_default AS column_default, - c.ordinal_position AS sort, - (CASE WHEN EXISTS ( - SELECT 1 FROM information_schema.table_constraints tc - JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name - WHERE tc.table_name = c.table_name - AND tc.constraint_type = 'PRIMARY KEY' - AND ccu.column_name = c.column_name - ) THEN 1 ELSE 0 END) AS is_pk, - (CASE WHEN c.column_default LIKE 'nextval%' THEN 1 ELSE 0 END) AS is_increment, - (CASE WHEN c.is_nullable = 'NO' THEN 1 ELSE 0 END) AS is_nullable, - (CASE WHEN EXISTS ( - SELECT 1 FROM information_schema.table_constraints tc - JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name - WHERE tc.table_name = c.table_name - AND tc.constraint_type = 'UNIQUE' - AND ccu.column_name = c.column_name - ) THEN 1 ELSE 0 END) AS is_unique - FROM - information_schema.columns c - LEFT JOIN pg_catalog.pg_description pgd ON - pgd.objoid = (SELECT oid FROM pg_class WHERE relname = c.table_name) - AND pgd.objsubid = c.ordinal_position - WHERE c.table_catalog = current_database() - AND c.table_schema = 'public' - AND c.table_name = :table_name - ORDER BY - c.ordinal_position - """ - - query = text(query_sql).bindparams(table_name=table_name) - result = await self.auth.db.execute(query) - rows = result.fetchall() if result else [] - - # 确保rows是可迭代对象 - if not rows: - return [] + # 使用run_sync包装同步的inspect操作 + columns_info = await self.auth.db.run_sync( + GenTableColumnCRUD._sync_get_table_columns, + self.auth.db.get_bind(), + settings.DATABASE_TYPE, + table_name + ) + # 转换为GenTableColumnOutSchema对象列表 columns_list = [] - for row in rows: - # 防御性编程:检查row是否有足够的元素 - if len(row) >= 10: - columns_list.append( - GenTableColumnOutSchema( - column_name=row[0], - column_comment=row[1], - column_type=row[2], - column_length=str(row[3]) if row[3] is not None else '', - column_default=str(row[4]) if row[4] is not None else '', - sort=row[5], - is_pk=row[6], - is_increment=row[7], - is_nullable=row[8], - is_unique=row[9], - ) - ) + for column_info in columns_info: + columns_list.append(GenTableColumnOutSchema(**column_info)) + return columns_list except Exception as e: log.error(f"获取表{table_name}的字段列表时出错: {str(e)}") diff --git a/backend/app/api/v1/module_generator/gencode/service.py b/backend/app/api/v1/module_generator/gencode/service.py index 5383ad1df8ebadbb0326064d917bcda66683659c..51bc1329bfd9c3c2cba1433efd43b814a85560cc 100644 --- a/backend/app/api/v1/module_generator/gencode/service.py +++ b/backend/app/api/v1/module_generator/gencode/service.py @@ -2,6 +2,7 @@ import io import os +from pathlib import Path import zipfile from typing import Any from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update @@ -185,6 +186,43 @@ class GenTableService: except Exception as e: raise CustomException(msg=f'创建表结构失败: {str(e)}') + @classmethod + @handle_service_exception + async def execute_sql_service(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> bool: + """ + 执行菜单 SQL(INSERT / DO 块)并写入 sys_menu。 + - 仅处理菜单 SQL,不再混杂建表逻辑; + - 文件不存在时给出友好提示; + - 统一异常信息,日志与业务提示分离。 + """ + sql_path = f'{BASE_DIR}/sql/menu/{gen_table.module_name}/{gen_table.business_name}.sql' + + # 文件存在性前置检查,避免多余解析开销 + if not os.path.isfile(sql_path): + raise CustomException(msg=f'菜单 SQL 文件不存在: {sql_path}') + + sql = Path(sql_path).read_text(encoding='utf-8').strip() + if not sql: + raise CustomException(msg='菜单 SQL 文件内容为空') + + # 仅做语法校验,不限制关键字;真正的语义安全由数据库权限控制 + try: + statements = sqlglot_parse(sql, dialect=settings.DATABASE_TYPE) + if not statements: + raise CustomException(msg='菜单 SQL 语法解析失败,请检查文件内容') + except Exception as e: + log.error(f'菜单 SQL 解析异常: {e}') + raise CustomException(msg='菜单 SQL 语法错误,请检查文件内容') + + # 执行 SQL + try: + await GenTableCRUD(auth).execute_sql(sql) + log.info(f'成功执行菜单 SQL: {sql_path}') + return True + except Exception as e: + log.error(f'菜单 SQL 执行失败: {e}') + raise CustomException(msg='菜单 SQL 执行失败,请确认语句及数据库状态') + @classmethod def __is_valid_create_table(cls, sql_statements: list[Expression | None]) -> bool: """ @@ -349,6 +387,8 @@ class GenTableService: f.write(render_content) except Exception as e: raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}') + + await cls.execute_sql_service(auth, gen_table_schema) return True @classmethod diff --git a/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 b/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 index 17bd906072d536a8527dcbc93f4a06f4a451ccad..70d0aa225cbdee4e69ae1ba8d504a8a2b66753db 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/python/schema.py.j2 @@ -53,7 +53,7 @@ class {{ class_name }}QueryParam: {% endif %} {% endfor %} {% for column in columns %} - {% if column.query_type == 'EQ' %} + {% if column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %} {{ column.column_name }}: {{ column.python_type }} | None = Query(None, description="{{ column.column_comment }}"), {% endif %} {% endfor %} @@ -70,7 +70,7 @@ class {{ class_name }}QueryParam: {% if column.query_type == 'LIKE' %} # 模糊查询字段 self.{{ column.column_name }} = ("like", {{ column.column_name }}) - {% elif column.query_type == 'EQ' and column.column_name %} + {% elif column.query_type == 'EQ' and column.column_name not in ['created_time', 'updated_time'] %} # 精确查询字段 self.{{ column.column_name }} = {{ column.column_name }} {% endif %} diff --git a/backend/app/api/v1/module_generator/gencode/templates/sql/sql.sql.j2 b/backend/app/api/v1/module_generator/gencode/templates/sql/sql.sql.j2 index d9511e0af88b5731829875dbd047087b22c35bf2..b05cd4b40a92fa4f5fabd66bbd49c99c9a89a83e 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/sql/sql.sql.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/sql/sql.sql.j2 @@ -12,7 +12,7 @@ INSERT INTO {{ sys_menu }} (`name`, `type`, {{ order_col }}, `permission`, `icon`, `route_name`, `route_path`, `component_path`, `redirect`, `hidden`, `keep_alive`, `always_show`, `title`, `params`, `affix`, `parent_id`, `uuid`, `status`, `description`, `created_time`, `updated_time`) VALUES -('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '/{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()); +('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()); -- 获取父菜单ID(MySQL) SELECT @parentId := LAST_INSERT_ID(); @@ -39,7 +39,7 @@ BEGIN INSERT INTO {{ sys_menu }} (name, type, {{ order_col }}, permission, icon, route_name, route_path, component_path, redirect, hidden, keep_alive, always_show, title, params, affix, parent_id, uuid, status, description, created_time, updated_time ) VALUES -('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '/{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()) +('{{ function_name }}', 2, 9999, '{{ permission_prefix }}:query', '{{ icon }}', '{{ business_name|snake_to_camel }}', '/{{ module_name }}/{{ business_name }}', '{{ module_name }}/{{ business_name }}/index', NULL, {{ b_false }}, {{ b_true }}, {{ b_false }}, '{{ function_name }}', NULL, {{ b_false }}, {{ parent_menu_id }}, {{ set_uuid }}, '0', '{{ function_name }}菜单', NOW(), NOW()) RETURNING id INTO parent_id; -- 按钮权限(类型=3:按钮/权限) diff --git a/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 b/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 index 14520ce22257147aa9680a03ab4db299ea8334ac..57b28ce3bc2240aae7ab1baaedaeced58d15c844 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/ts/api.ts.j2 @@ -1,6 +1,6 @@ import request from "@/utils/request"; -const API_PATH = "/{{ module_name }}/{{ business_name|lower }}"; +const API_PATH = "/{{ package_name }}/{{ business_name|lower }}"; const {{ class_name }}API = { // 列表查询 @@ -95,7 +95,7 @@ export default {{ class_name }}API; // 列表查询参数 export interface {{ class_name }}PageQuery extends PageQuery { {% for column in columns %} - {% if column.is_query and column.column != "BETWEEN" %} + {% if column.is_query and column.column != "BETWEEN" and column.column_name not in ['created_time', 'updated_time'] %} {{ column.column_name }}?: {{ 'string' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio') else 'number' if column.is_pk == '1' @@ -110,7 +110,6 @@ export interface {{ class_name }}PageQuery extends PageQuery { // 列表展示项 export interface {{ class_name }}Table extends BaseType{ - index?: number; {% for column in columns %} {% if column.column_name not in ['id', 'uuid', 'status', 'description', 'created_time', 'updated_time'] %} {{ column.column_name }}?: {{ diff --git a/backend/app/api/v1/module_generator/gencode/templates/vue/index.vue.j2 b/backend/app/api/v1/module_generator/gencode/templates/vue/index.vue.j2 index f23f50abaa51a25cf9a926d6acf5f570b415830f..2b3b4860212bf3fd9444527519330c41872c81e2 100644 --- a/backend/app/api/v1/module_generator/gencode/templates/vue/index.vue.j2 +++ b/backend/app/api/v1/module_generator/gencode/templates/vue/index.vue.j2 @@ -16,29 +16,7 @@ {% set column_comment = column.column_comment if column.column_comment else '' %} {% set parentheseIndex = column_comment.find("(") %} {% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %} - {% if column.html_type == "input" %} - - - - {% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %} - - - - - - {% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %} - - - - - - {% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %} - - - - {% endif %} - {% endif %} - {% endfor %} + {% if column.column_name == "status" %} + {% elif column.column_name == "created_id"%} + {% elif column.column_name == "updated_id"%} - + {% elif column.column_name == "created_time"%} + {% elif column.column_name == "updated_time"%} + {% elif column.html_type == "input" %} + + + + {% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %} + + + + + + {% elif (column.html_type == "select" or column.html_type == "radio") and dict_type %} + + + + + + {% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %} + + + + {% endif %} + {% endif %} + {% endfor %}