!29 RuoYi-Vue3-FastAPI v1.6.1
Merge pull request !29 from insistence/develop
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
|
||||||
</p>
|
</p>
|
||||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.6.0</h1>
|
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.6.1</h1>
|
||||||
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
|
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a>
|
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a>
|
||||||
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
|
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
|
||||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.6.0-brightgreen.svg"></a>
|
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.6.1-brightgreen.svg"></a>
|
||||||
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
|
||||||
<img src="https://img.shields.io/badge/python-≥3.9-blue">
|
<img src="https://img.shields.io/badge/python-≥3.9-blue">
|
||||||
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
|
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
|
||||||
|
@@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
|
|||||||
# 应用端口
|
# 应用端口
|
||||||
APP_PORT = 9099
|
APP_PORT = 9099
|
||||||
# 应用版本
|
# 应用版本
|
||||||
APP_VERSION= '1.6.0'
|
APP_VERSION= '1.6.1'
|
||||||
# 应用是否开启热重载
|
# 应用是否开启热重载
|
||||||
APP_RELOAD = true
|
APP_RELOAD = true
|
||||||
# 应用是否开启IP归属区域查询
|
# 应用是否开启IP归属区域查询
|
||||||
|
@@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
|
|||||||
# 应用端口
|
# 应用端口
|
||||||
APP_PORT = 9099
|
APP_PORT = 9099
|
||||||
# 应用版本
|
# 应用版本
|
||||||
APP_VERSION= '1.6.0'
|
APP_VERSION= '1.6.1'
|
||||||
# 应用是否开启热重载
|
# 应用是否开启热重载
|
||||||
APP_RELOAD = false
|
APP_RELOAD = false
|
||||||
# 应用是否开启IP归属区域查询
|
# 应用是否开启IP归属区域查询
|
||||||
|
@@ -3,6 +3,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from pydantic import computed_field
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
@@ -51,6 +52,13 @@ class DataBaseSettings(BaseSettings):
|
|||||||
db_pool_recycle: int = 3600
|
db_pool_recycle: int = 3600
|
||||||
db_pool_timeout: int = 30
|
db_pool_timeout: int = 30
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def sqlglot_parse_dialect(self) -> str:
|
||||||
|
if self.db_type == 'postgresql':
|
||||||
|
return 'postgres'
|
||||||
|
return self.db_type
|
||||||
|
|
||||||
|
|
||||||
class RedisSettings(BaseSettings):
|
class RedisSettings(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
@@ -2,10 +2,13 @@ import inspect
|
|||||||
from fastapi import Form, Query
|
from fastapi import Form, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.fields import FieldInfo
|
from pydantic.fields import FieldInfo
|
||||||
from typing import Type
|
from typing import Type, TypeVar
|
||||||
|
|
||||||
|
|
||||||
def as_query(cls: Type[BaseModel]):
|
BaseModelVar = TypeVar('BaseModelVar', bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
def as_query(cls: Type[BaseModelVar]) -> Type[BaseModelVar]:
|
||||||
"""
|
"""
|
||||||
pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数
|
pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数
|
||||||
"""
|
"""
|
||||||
@@ -43,7 +46,7 @@ def as_query(cls: Type[BaseModel]):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
def as_form(cls: Type[BaseModel]):
|
def as_form(cls: Type[BaseModelVar]) -> Type[BaseModelVar]:
|
||||||
"""
|
"""
|
||||||
pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数
|
pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数
|
||||||
"""
|
"""
|
||||||
|
@@ -2,6 +2,7 @@ from datetime import datetime, time
|
|||||||
from sqlalchemy import delete, func, select, text, update
|
from sqlalchemy import delete, func, select, text, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlglot.expressions import Expression
|
||||||
from typing import List
|
from typing import List
|
||||||
from config.env import DataBaseConfig
|
from config.env import DataBaseConfig
|
||||||
from module_generator.entity.do.gen_do import GenTable, GenTableColumn
|
from module_generator.entity.do.gen_do import GenTable, GenTableColumn
|
||||||
@@ -75,15 +76,17 @@ class GenTableDao:
|
|||||||
return gen_table_all
|
return gen_table_all
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_table_by_sql_dao(cls, db: AsyncSession, sql: str):
|
async def create_table_by_sql_dao(cls, db: AsyncSession, sql_statements: List[Expression]):
|
||||||
"""
|
"""
|
||||||
根据sql语句创建表结构
|
根据sql语句创建表结构
|
||||||
|
|
||||||
:param db: orm对象
|
:param db: orm对象
|
||||||
:param sql: sql语句
|
:param sql_statements: sql语句的ast列表
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await db.execute(text(sql))
|
for sql_statement in sql_statements:
|
||||||
|
sql = sql_statement.sql(dialect=DataBaseConfig.sqlglot_parse_dialect)
|
||||||
|
await db.execute(text(sql))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False):
|
async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False):
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlglot import parse as sqlglot_parse
|
||||||
|
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
|
||||||
from typing import List
|
from typing import List
|
||||||
from config.constant import GenConstant
|
from config.constant import GenConstant
|
||||||
from config.env import GenConfig
|
from config.env import DataBaseConfig, GenConfig
|
||||||
from exceptions.exception import ServiceException
|
from exceptions.exception import ServiceException
|
||||||
from module_admin.entity.vo.common_vo import CrudResponseModel
|
from module_admin.entity.vo.common_vo import CrudResponseModel
|
||||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||||
@@ -197,10 +198,11 @@ class GenTableService:
|
|||||||
:param current_user: 当前用户信息对象
|
:param current_user: 当前用户信息对象
|
||||||
:return: 创建表结构结果
|
:return: 创建表结构结果
|
||||||
"""
|
"""
|
||||||
if cls.__is_valid_create_table(sql):
|
sql_statements = sqlglot_parse(sql, dialect=DataBaseConfig.sqlglot_parse_dialect)
|
||||||
|
if cls.__is_valid_create_table(sql_statements):
|
||||||
try:
|
try:
|
||||||
table_names = re.findall(r'create\s+table\s+(\w+)', sql, re.IGNORECASE)
|
table_names = cls.__get_table_names(sql_statements)
|
||||||
await GenTableDao.create_table_by_sql_dao(query_db, sql)
|
await GenTableDao.create_table_by_sql_dao(query_db, sql_statements)
|
||||||
gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names)
|
gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names)
|
||||||
await cls.import_gen_table_services(query_db, gen_table_list, current_user)
|
await cls.import_gen_table_services(query_db, gen_table_list, current_user)
|
||||||
|
|
||||||
@@ -211,22 +213,39 @@ class GenTableService:
|
|||||||
raise ServiceException(message='建表语句不合法')
|
raise ServiceException(message='建表语句不合法')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __is_valid_create_table(cls, sql: str):
|
def __is_valid_create_table(cls, sql_statements: List[Expression]):
|
||||||
"""
|
"""
|
||||||
校验sql语句是否为合法的建表语句
|
校验sql语句是否为合法的建表语句
|
||||||
|
|
||||||
:param sql: sql语句
|
:param sql_statements: sql语句的ast列表
|
||||||
:return: 校验结果
|
:return: 校验结果
|
||||||
"""
|
"""
|
||||||
create_table_pattern = r'^\s*CREATE\s+TABLE\s+'
|
validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements]
|
||||||
if not re.search(create_table_pattern, sql, re.IGNORECASE):
|
validate_forbidden_keywords = [
|
||||||
|
isinstance(
|
||||||
|
sql_statement,
|
||||||
|
(Add, Alter, Delete, Drop, Insert, TruncateTable, Update),
|
||||||
|
)
|
||||||
|
for sql_statement in sql_statements
|
||||||
|
]
|
||||||
|
if not any(validate_create) or any(validate_forbidden_keywords):
|
||||||
return False
|
return False
|
||||||
forbidden_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
|
|
||||||
for keyword in forbidden_keywords:
|
|
||||||
if re.search(rf'\b{keyword}\b', sql, re.IGNORECASE):
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_table_names(cls, sql_statements: List[Expression]):
|
||||||
|
"""
|
||||||
|
获取sql语句中所有的建表表名
|
||||||
|
|
||||||
|
:param sql_statements: sql语句的ast列表
|
||||||
|
:return: 建表表名列表
|
||||||
|
"""
|
||||||
|
table_names = []
|
||||||
|
for sql_statement in sql_statements:
|
||||||
|
if isinstance(sql_statement, Create):
|
||||||
|
table_names.append(sql_statement.find(Table).name)
|
||||||
|
return table_names
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def preview_code_services(cls, query_db: AsyncSession, table_id: int):
|
async def preview_code_services(cls, query_db: AsyncSession, table_id: int):
|
||||||
"""
|
"""
|
||||||
|
@@ -70,7 +70,7 @@ class {{ BusinessName }}Dao:
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
select({{ ClassName }}).where(
|
select({{ ClassName }}).where(
|
||||||
{% for column in columns %}
|
{% for column in columns %}
|
||||||
{% if column.required %}
|
{% if column.unique %}
|
||||||
{{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True,
|
{{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -8,8 +8,8 @@
|
|||||||
{% set vo_field_required.has_required = True %}
|
{% set vo_field_required.has_required = True %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if table.sub %}
|
|
||||||
{% set sub_vo_field_required = namespace(has_required=False) %}
|
{% set sub_vo_field_required = namespace(has_required=False) %}
|
||||||
|
{% if table.sub %}
|
||||||
{% for sub_column in subTable.columns %}
|
{% for sub_column in subTable.columns %}
|
||||||
{% if sub_column.required %}
|
{% if sub_column.required %}
|
||||||
{% set sub_vo_field_required.has_required = True %}
|
{% set sub_vo_field_required.has_required = True %}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from pydantic.alias_generators import to_camel
|
from pydantic.alias_generators import to_camel
|
||||||
{% if vo_field_required.has_required %}
|
{% if vo_field_required.has_required or sub_vo_field_required.has_required %}
|
||||||
from pydantic_validation_decorator import NotBlank
|
from pydantic_validation_decorator import NotBlank
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if table.sub %}
|
{% if table.sub %}
|
||||||
|
@@ -14,4 +14,5 @@ PyMySQL==1.1.1
|
|||||||
redis==5.2.1
|
redis==5.2.1
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
SQLAlchemy[asyncio]==2.0.38
|
SQLAlchemy[asyncio]==2.0.38
|
||||||
|
sqlglot[rs]==26.6.0
|
||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vfadmin",
|
"name": "vfadmin",
|
||||||
"version": "1.6.0",
|
"version": "1.6.1",
|
||||||
"description": "vfadmin管理系统",
|
"description": "vfadmin管理系统",
|
||||||
"author": "insistence",
|
"author": "insistence",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
Reference in New Issue
Block a user