diff --git a/README.md b/README.md
index 73dfae62625878d8575a228cdac2fa0c46243ae6..6862dc7deb3476315bb928d86f945329d0973f79 100644
--- a/README.md
+++ b/README.md
@@ -1,397 +1,21 @@
-
-
-
-
-
FastApiAdmin v2.0.0
-
现代化全栈快速开发平台
-
如果你喜欢这个项目,给个 ⭐️ 支持一下吧!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+## 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 端
-
-| 模块名 | 截图 |
-| ----- | --- |
-| 登录 |  |
-| 仪表盘 |  |
-| 分析页 |  |
-| 菜单管理 |  |
-| 部门管理 |  |
-| 岗位管理 |  |
-| 角色管理 |  |
-| 用户管理 |  |
-| 日志管理 |  |
-| 配置管理 |  |
-| 在线用户 |  |
-| 服务器监控 |  |
-| 缓存监控 |  |
-| 任务管理 |  |
-| 字典管理 |  |
-| 接口管理 |  |
-| 系统主题 |  |
-| 在线文档 |  |
-| 系统锁屏 |  |
-| 表单构建 |  |
-| 代码生成 |  |
-| 流程管理 |  |
-| 文件管理 |  |
-| 我的应用 |  |
-| 配置中心 |  |
-| 智能助手 |  |
-
-### 移动端
-
-| 模块 | 详情 | 模块 | 详情 | 模块 | 详情 |
-|----------|------|----------|------|----------|------|
-| 登录 |  | 首页 |  | 我的 |  |
-| 个人 |  | 设置 |  | 工作台 |  |
-
-## 🛠️ 二开教程
-
-### 后端开发
-
-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/)
-
-## 🎨 社区交流
-
-| 微信二维码 | 群组二维码 | 微信支付二维码 |
-| --- | --- | --- |
-|  |  |  |
-
-## ❤️ 支持项目
-
-如果你喜欢这个项目,请给我一个 ⭐️ Star 支持一下吧!非常感谢!
-
-[](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 %}
-
+
批量启用
-
+
批量停用
@@ -344,24 +348,27 @@
{% for column in columns %}
- {% set python_field = column.column_name %}
{% 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.python_field == 'status' %}
+ {% if column.column_name == 'status' %}
{{ '{{' }} detailFormData.status == '0' ? "启用" : "停用" {{ '}}' }}
- {% elif column.python_field == 'created_id' %}
+ {% elif column.column_name == 'created_id' %}
{{ '{{' }} detailFormData.created_by?.name {{ '}}' }}
- {% elif column.python_field == 'updated_id' %}
+ {% elif column.column_name == 'updated_id' %}
{{ '{{' }} detailFormData.updated_by?.name {{ '}}' }}
+ {% else %}
+
+ {{ '{{' }} detailFormData.{{ column.column_name }} {{ '}}' }}
+
{% endif %}
{% endfor %}
@@ -377,14 +384,25 @@
{% set parentheseIndex = column_comment.find("(") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% set required = 'true' if column.is_nullable == '1' else 'false' %}
- {% if column.column_name not in ['uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
+ {% if column.column_name not in ['id', 'uuid', 'created_time', 'updated_time', 'created_id', 'updated_id'] %}
{% if column.column_name == "status" %}
- 启用
- 停用
+ 启用
+ 停用
+ {% elif column.column_name == "description" %}
+
+
+
{% elif column.html_type == "input" %}
diff --git a/backend/app/api/v1/module_generator/gencode/tools/gen_util.py b/backend/app/api/v1/module_generator/gencode/tools/gen_util.py
index 769f8eaa01ae20902c937b19562c6813930ca301..086411a80a76d95b24be45cb18ee04a00778953e 100644
--- a/backend/app/api/v1/module_generator/gencode/tools/gen_util.py
+++ b/backend/app/api/v1/module_generator/gencode/tools/gen_util.py
@@ -24,8 +24,8 @@ class GenUtils:
"""
# 只有当字段为None时才设置默认值
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
- gen_table.package_name = 'module_gencode'
- gen_table.module_name = gen_table.package_name.split('.')[-1]
+ gen_table.package_name = 'gencode'
+ gen_table.module_name = f'module_{gen_table.package_name}'
gen_table.business_name = gen_table.table_name
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py
index c9770fb250b02c4a7f1f0ecf361b575de4e2476e..e93445e190a72c483c5487f4b08eb830a7996f21 100755
--- a/backend/app/config/setting.py
+++ b/backend/app/config/setting.py
@@ -87,10 +87,9 @@ class Settings(BaseSettings):
EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期
# 数据库类型
- DATABASE_TYPE: Literal['mysql', 'postgres'] = 'mysql'
-
+ DATABASE_TYPE: Literal['sqlite','mysql', 'postgres', 'sqlserver'] = 'mysql'
- # MySQL/PostgreSQL数据库连接
+ # MySQL/PostgreSQL/SQLite数据库连接
DATABASE_HOST: str = 'localhost'
DATABASE_PORT: int = 3306
DATABASE_USER: str = 'root'
@@ -194,20 +193,30 @@ class Settings(BaseSettings):
"""获取异步数据库连接"""
if self.DATABASE_TYPE == "mysql":
return f"mysql+asyncmy://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
- elif self.DATABASE_TYPE == "postgres":
+ elif self.DATABASE_TYPE == "postgresql":
return f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
+ elif self.DATABASE_TYPE == "sqlite":
+ return f"sqlite+aiosqlite:///{BASE_DIR.joinpath(self.DATABASE_NAME + '.db')}?characterEncoding=UTF-8"
+ elif self.DATABASE_TYPE == "sqlserver":
+ # 使用兼容SQL Server 2008的驱动
+ return f"mssql+aioodbc://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?driver={self.DATABASE_DRIVER}"
else:
- raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 请选择 mysql、postgres")
+ raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 mysql、postgresql、sqlite、sqlserver")
@property
def DB_URI(self) -> str:
"""获取同步数据库连接"""
if self.DATABASE_TYPE == "mysql":
return f"mysql+pymysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
- elif self.DATABASE_TYPE == "postgres":
+ elif self.DATABASE_TYPE == "postgresql":
return f"postgresql+psycopg2://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
+ elif self.DATABASE_TYPE == "sqlite":
+ return f"sqlite:///{BASE_DIR.joinpath(self.DATABASE_NAME + '.db')}?charset=utf8"
+ elif self.DATABASE_TYPE == "sqlserver":
+ return f"mssql+pymssql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
else:
- raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 请选择 mysql、postgres")
+ raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 请选择 mysql、postgresql、sqlite、sqlserver")
+
@property
def REDIS_URI(self) -> str:
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
index 4ceddc5e153fdff2591d87c240a8748a491dc75e..5f2e1c16463ce5b39391b90905447a92d1bd7bbb 100644
--- a/backend/app/core/database.py
+++ b/backend/app/core/database.py
@@ -55,18 +55,28 @@ def create_async_engine_and_session(
if not settings.SQL_DB_ENABLE:
raise CustomException(msg="请先开启数据库连接", data="请启用 app/config/setting.py: SQL_DB_ENABLE")
# 异步数据库引擎
- async_engine: AsyncEngine = create_async_engine(
- url=db_url,
- echo=settings.DATABASE_ECHO,
- echo_pool=settings.ECHO_POOL,
- pool_pre_ping=settings.POOL_PRE_PING,
- future=settings.FUTURE,
- pool_recycle=settings.POOL_RECYCLE,
- pool_size=settings.POOL_SIZE,
- max_overflow=settings.MAX_OVERFLOW,
- pool_timeout=settings.POOL_TIMEOUT,
- pool_use_lifo=settings.POOL_USE_LIFO,
- )
+ if settings.DATABASE_TYPE == 'sqlite':
+ async_engine: AsyncEngine = create_async_engine(
+ url=db_url,
+ echo=settings.DATABASE_ECHO,
+ echo_pool=settings.ECHO_POOL,
+ pool_pre_ping=settings.POOL_PRE_PING,
+ future=settings.FUTURE,
+ pool_recycle=settings.POOL_RECYCLE,
+ )
+ else:
+ async_engine: AsyncEngine = create_async_engine(
+ url=db_url,
+ echo=settings.DATABASE_ECHO,
+ echo_pool=settings.ECHO_POOL,
+ pool_pre_ping=settings.POOL_PRE_PING,
+ future=settings.FUTURE,
+ pool_recycle=settings.POOL_RECYCLE,
+ pool_use_lifo=settings.POOL_USE_LIFO,
+ pool_size=settings.POOL_SIZE,
+ max_overflow=settings.MAX_OVERFLOW,
+ pool_timeout=settings.POOL_TIMEOUT,
+ )
except Exception as e:
log.error(f'❌ 数据库连接失败 {e}')
raise
diff --git a/backend/env/.env.dev b/backend/env/.env.dev
index 94987f5280a0464df2ee17ca0da1850d94301643..5f772384735f991225e6d9e1238facfd8c9f5e3b 100644
--- a/backend/env/.env.dev
+++ b/backend/env/.env.dev
@@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi
DEMO_ENABLE = False
# 数据库配置
-DATABASE_TYPE = "postgres" # mysql、postgres
+DATABASE_TYPE = "sqlite" # mysql、postgres、sqlite
# 数据库配置
DATABASE_HOST = "localhost"
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 65f6d7660f1d3723b3adcc376bbac28ad7d04402..7ef7d5208a9af87b677a8b66b74cec22b09be4bc 100755
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -24,6 +24,7 @@ bcrypt==4.0.1 # 密码加密解析,切勿升级,如果升级,
itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等
aiofiles==24.1.0 # 文件操作
redis==5.2.1 # redis 同/异步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本
+aiosqlite==0.17.0 # sqlite 异步操作数据库
asyncmy==0.2.9 # mysql 异步操作数据库:基于 mysqlclient:asyncmy 是 mysqlclient 的异步版本,mysqlclient 是一个 C 语言编写的 MySQL 客户端,性能较高。性能:asyncmy 通常在性能上优于 aiomysql,特别是在高并发和大数据量的场景下。
PyMySQL==1.1.2 # mysql 同步步操作数据库基于 pymysql:aiomysql 是 pymysql 的异步版本,pymysql 是一个纯 Python 实现的 MySQL 客户端。成熟度:aiomysql 相对较为成熟,社区支持较好,文档也比较完善。
asyncpg==0.30.0 # postgresql 异步操作数据库基于 psycopg2:asyncpg 是 psycopg2 的异步版本,psycopg2 是一个 pure-Python PostgreSQL 数据库适配器。性能:asyncpg 通常在性能上优于 psycopg2,特别是在高并发和大数据量的场景下。
diff --git a/frontend/src/views/module_generator/gencode/index.vue b/frontend/src/views/module_generator/gencode/index.vue
index 5f229f46c71f7473f8a9733e12d2af906fcdd00d..4d81552c22c028a4f94b8c43fdebe31a4a46737f 100644
--- a/frontend/src/views/module_generator/gencode/index.vue
+++ b/frontend/src/views/module_generator/gencode/index.vue
@@ -516,9 +516,6 @@
backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/model.py
-
- backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/param.py
-
backend/app/api/v1/{{ info.module_name }}/{{ info.business_name }}/schema.py
@@ -644,38 +641,22 @@
-
+
-
+
-
+
-
+
@@ -704,30 +685,22 @@
-
+
-
+
-
+
-
+
diff --git a/frontend/src/views/module_system/menu/index.vue b/frontend/src/views/module_system/menu/index.vue
index f90ff4db7ce57067768a4586e35290fa80c3fa09..7107bb47460f7c242a7a07d3d9996dd6fead89f4 100644
--- a/frontend/src/views/module_system/menu/index.vue
+++ b/frontend/src/views/module_system/menu/index.vue
@@ -390,7 +390,6 @@
v-if="formData.type !== MenuTypeEnum.CATALOG"
label="父级菜单"
prop="parent_id"
- if
>