Update the code generator to plugin (#578)

* Update the code generator to plugin

* Fix get all tables return type
This commit is contained in:
Wu Clan
2025-04-17 11:06:43 +08:00
committed by GitHub
parent 1cc020efdd
commit fc34c98f0a
40 changed files with 211 additions and 205 deletions
+41
View File
@@ -0,0 +1,41 @@
> [!TIP]
> 当前版本仅包含后端代码生成
> [!WARNING]
> 由于 jinja2 在渲染模版时,文本方式输出可能存在格式问题,所以 `preview` 接口可能无法直观预览代码,这是为前端进行的预设
## 简介
代码生成器使用 api 调用实现,包含两个模组,设计可能存在缺陷,相关问题请直接提交 issues
### 1. 代码生成业务
包含代码生成的相关配置,详情查看:`generator/model/gen_business.py`
### 2. 代码生成模型
包含代码生成所需要的模型列信息,就像正常定义模型列一样,目前支持的功能有限
## 使(食)用
1. 启动后端服务,打开 swagger 文档直接操作
2. 通过第三方 api 调试工具发送接口请求
3. 同时启动前后端,从页面进行操作
接口参数基本都有说明,请注意查看
### F. 纯手动模式
不推荐(手动创建业务接口被标记为「已弃用」)
1. 通过创建业务接口手动添加一项业务数据
2. 通过模型创建接口手动添加模型列
3. 访问 `preview`(预览),`generate`(磁盘写入),`download`(下载)接口,执行后端代码生成相应工作
### S. 自动模式
推荐
1. 访问 `tables` 接口,获取数据库表名列表
2. 通过 `import` 接口,导入数据库已有的数据库表数据,将自动创建业务表数据和模型表数据
3. 访问 `preview`(预览),`generate`(磁盘写入),`download`(下载)接口,执行后端代码生成相应工作
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.core.conf import settings
from backend.plugin.code_generator.api.v1.gen import router as gen_router
from backend.plugin.code_generator.api.v1.gen_business import router as gen_business_router
from backend.plugin.code_generator.api.v1.gen_model import router as gen_model_router
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(gen_business_router, prefix='/businesses')
v1.include_router(gen_model_router, prefix='/models')
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import StreamingResponse
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.conf import generator_settings
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.gen_service import gen_service
router = APIRouter()
@router.get('/tables', summary='获取数据库表')
async def get_all_tables(
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
) -> ResponseSchemaModel[list[str]]:
data = await gen_service.get_tables(table_schema=table_schema)
return response_base.success(data=data)
@router.post(
'/import',
summary='导入代码生成业务和模型列',
dependencies=[
Depends(RequestPermission('gen:code:import')),
DependsRBAC,
],
)
async def import_table(obj: ImportParam) -> ResponseModel:
await gen_service.import_business_and_model(obj=obj)
return response_base.success()
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
data = await gen_service.preview(pk=pk)
return response_base.success(data=data)
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
data = await gen_service.get_generate_path(pk=pk)
return response_base.success(data=data)
@router.post(
'/generate/{pk}',
summary='代码生成',
description='文件磁盘写入,请谨慎操作',
dependencies=[
Depends(RequestPermission('gen:code:generate')),
DependsRBAC,
],
)
async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
await gen_service.generate(pk=pk)
return response_base.success()
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
bio = await gen_service.download(pk=pk)
return StreamingResponse(
bio,
media_type='application/x-zip-compressed',
headers={'Content-Disposition': f'attachment; filename={generator_settings.DOWNLOAD_ZIP_FILENAME}.zip'},
)
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.gen_business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.plugin.code_generator.schema.gen_model import GetGenModelDetail
from backend.plugin.code_generator.service.gen_business_service import gen_business_service
from backend.plugin.code_generator.service.gen_model_service import gen_model_service
router = APIRouter()
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
data = await gen_business_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
async def get_business(
pk: Annotated[int, Path(description='业务 ID')],
) -> ResponseSchemaModel[GetGenBusinessDetail]:
data = await gen_business_service.get(pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
async def get_business_all_models(
pk: Annotated[int, Path(description='业务 ID')],
) -> ResponseSchemaModel[list[GetGenModelDetail]]:
data = await gen_model_service.get_by_business(business_id=pk)
return response_base.success(data=data)
@router.post(
'',
summary='创建代码生成业务',
deprecated=True,
dependencies=[
Depends(RequestPermission('gen:code:business:add')),
DependsRBAC,
],
)
async def create_business(obj: CreateGenBusinessParam) -> ResponseModel:
await gen_business_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新代码生成业务',
dependencies=[
Depends(RequestPermission('gen:code:business:edit')),
DependsRBAC,
],
)
async def update_business(
pk: Annotated[int, Path(description='业务 ID')], obj: UpdateGenBusinessParam
) -> ResponseModel:
count = await gen_business_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'/{pk}',
summary='删除代码生成业务',
dependencies=[
Depends(RequestPermission('gen:code:business:del')),
DependsRBAC,
],
)
async def delete_business(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
count = await gen_business_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.gen_model import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.plugin.code_generator.service.gen_model_service import gen_model_service
router = APIRouter()
@router.get('/types', summary='获取代码生成模型列类型', dependencies=[DependsJwtAuth])
async def get_model_types() -> ResponseSchemaModel[list[str]]:
model_types = await gen_model_service.get_types()
return response_base.success(data=model_types)
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
async def get_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseSchemaModel[GetGenModelDetail]:
data = await gen_model_service.get(pk=pk)
return response_base.success(data=data)
@router.post(
'',
summary='创建代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:add')),
DependsRBAC,
],
)
async def create_model(obj: CreateGenModelParam) -> ResponseModel:
await gen_model_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:edit')),
DependsRBAC,
],
)
async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: UpdateGenModelParam) -> ResponseModel:
count = await gen_model_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'/{pk}',
summary='删除代码生成模型',
dependencies=[
Depends(RequestPermission('gen:code:model:del')),
DependsRBAC,
],
)
async def delete_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseModel:
count = await gen_model_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings):
"""代码生成配置"""
# 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@lru_cache
def get_generator_settings() -> GeneratorSettings:
"""获取代码生成配置"""
return GeneratorSettings()
generator_settings = get_generator_settings()
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Row, text
from sqlalchemy.ext.asyncio import AsyncSession
from backend.core.conf import settings
class CRUDGen:
"""代码生成 CRUD 类"""
@staticmethod
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
"""
获取所有表名
:param db: 数据库会话
:param table_schema: 数据库 schema 名称
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT table_name AS table_name FROM information_schema.tables
WHERE table_name NOT LIKE 'sys_gen_%'
AND table_schema = :table_schema;
"""
else:
sql = """
SELECT table_name AS table_name FROM information_schema.tables
WHERE table_name NOT LIKE 'sys_gen_%'
AND table_catalog = :table_schema
AND table_schema = 'public'; -- schema 通常是 'public'
"""
stmt = text(sql).bindparams(table_schema=table_schema)
result = await db.execute(stmt)
return result.scalars().all()
@staticmethod
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
"""
获取表信息
:param db: 数据库会话
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT table_name AS table_name, table_comment AS table_comment FROM information_schema.tables
WHERE table_name NOT LIKE 'sys_gen_%'
AND table_name = :table_name;
"""
else:
sql = """
SELECT t.tablename AS table_name,
pg_catalog.obj_description(t.tablename::regclass, 'pg_class') AS table_comment
FROM pg_tables t
WHERE t.tablename NOT LIKE 'sys_gen_%'
AND t.tablename = :table_name
AND t.schemaname = 'public'; -- schema 通常是 'public'
"""
stmt = text(sql).bindparams(table_name=table_name)
result = await db.execute(stmt)
return result.fetchone()
@staticmethod
async def get_all_columns(db: AsyncSession, table_schema: str, table_name: str) -> Sequence[Row[tuple]]:
"""
获取所有列信息
:param db: 数据库会话
:param table_schema: 数据库 schema 名称
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT column_name AS column_name,
CASE WHEN column_key = 'PRI' THEN 1 ELSE 0 END AS is_pk,
CASE WHEN is_nullable = 'NO' OR column_key = 'PRI' THEN 0 ELSE 1 END AS is_nullable,
ordinal_position AS sort, column_comment AS column_comment,
column_type AS column_type FROM information_schema.columns
WHERE table_schema = :table_schema
AND table_name = :table_name
AND column_name <> 'id'
AND column_name <> 'created_time'
AND column_name <> 'updated_time'
ORDER BY sort;
"""
stmt = text(sql).bindparams(table_schema=table_schema, table_name=table_name)
else:
sql = """
SELECT a.attname AS column_name,
CASE WHEN EXISTS (
SELECT 1
FROM pg_constraint c
WHERE c.conrelid = t.oid
AND c.contype = 'p'
AND a.attnum = ANY(c.conkey)
) THEN 1 ELSE 0 END AS is_pk,
CASE WHEN a.attnotnull OR EXISTS (
SELECT 1
FROM pg_constraint c
WHERE c.conrelid = t.oid
AND c.contype = 'p'
AND a.attnum = ANY(c.conkey)
) THEN 0 ELSE 1 END AS is_nullable,
a.attnum AS sort,
col_description(t.oid, a.attnum) AS column_comment,
pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type
FROM pg_attribute a
JOIN pg_class t ON a.attrelid = t.oid
JOIN pg_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = 'public' -- 根据你的实际情况修改 schema 名称,通常是 'public'
AND t.relname = :table_name
AND a.attnum > 0
AND NOT a.attisdropped
AND a.attname <> 'id'
AND a.attname <> 'created_time'
AND a.attname <> 'updated_time'
ORDER BY sort;
"""
stmt = text(sql).bindparams(table_name=table_name)
result = await db.execute(stmt)
return result.fetchall()
gen_dao: CRUDGen = CRUDGen()
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
"""代码生成业务 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None:
"""
获取代码生成业务
:param db: 数据库会话
:param pk: 代码生成业务 ID
:return:
"""
return await self.select_model(db, pk)
async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None:
"""
通过 name 获取代码生成业务
:param db: 数据库会话
:param name: 表名
:return:
"""
return await self.select_model_by_column(db, table_name_en=name)
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
"""
获取所有代码生成业务
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None:
"""
创建代码生成业务
:param db: 数据库会话
:param obj: 创建代码生成业务参数
:return:
"""
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenBusinessParam) -> int:
"""
更新代码生成业务
:param db: 数据库会话
:param pk: 代码生成业务 ID
:param obj: 更新代码生成业务参数
:return:
"""
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成业务
:param db: 数据库会话
:param pk: 代码生成业务 ID
:return:
"""
return await self.delete_model(db, pk)
gen_business_dao: CRUDGenBusiness = CRUDGenBusiness(GenBusiness)
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.code_generator.model import GenModel
from backend.plugin.code_generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]):
"""代码生成模型 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
"""
获取代码生成模型列
:param db: 数据库会话
:param pk: 代码生成模型 ID
:return:
"""
return await self.select_model(db, pk)
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
"""
获取所有代码生成模型列
:param db: 数据库会话
:param business_id: 业务 ID
:return:
"""
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None) -> None:
"""
创建代码生成模型
:param db: 数据库会话
:param obj: 创建代码生成模型参数
:param pd_type: Pydantic 类型
:return:
"""
await self.create_model(db, obj, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None) -> int:
"""
更新代码生成模型
:param db: 数据库会话
:param pk: 代码生成模型 ID
:param obj: 更新代码生成模型参数
:param pd_type: Pydantic 类型
:return:
"""
return await self.update_model(db, pk, obj, pd_type=pd_type)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成模型
:param db: 数据库会话
:param pk: 代码生成模型 ID
:return:
"""
return await self.delete_model(db, pk)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel)
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import StrEnum
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
@@ -0,0 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.code_generator.model.gen_business import GenBusiness
from backend.plugin.code_generator.model.gen_model import GenModel
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.plugin.code_generator.model import GenModel
class GenBusiness(Base):
"""代码生成业务表"""
__tablename__ = 'sys_gen_business'
id: Mapped[id_key] = mapped_column(init=False)
app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)')
table_name_en: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)')
table_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文)')
table_simple_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文简称)')
table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述')
# relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键')
schema_name: Mapped[str | None] = mapped_column(String(255), default=None, comment='Schema 名称 (默认为英文表名称)')
default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列')
api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1')
gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)')
remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
# 代码生成业务模型一对多
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business')
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING, Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import DataClassBase, id_key
if TYPE_CHECKING:
from backend.plugin.code_generator.model import GenBusiness
class GenModel(DataClassBase):
"""代码生成模型表"""
__tablename__ = 'sys_gen_model'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), comment='列名称')
comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述')
type: Mapped[str] = mapped_column(String(20), default='str', comment='SQLA 模型列类型')
pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型')
default: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值'
)
sort: Mapped[int | None] = mapped_column(default=1, comment='列排序')
length: Mapped[int] = mapped_column(default=0, comment='列长度')
is_pk: Mapped[bool] = mapped_column(default=False, comment='是否主键')
is_nullable: Mapped[bool] = mapped_column(default=False, comment='是否可为空')
# 代码生成业务模型一对多
gen_business_id: Mapped[int] = mapped_column(
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
)
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model')
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.core.path_conf import PLUGIN_DIR
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = PLUGIN_DIR / 'code_generator' / 'templates'
@@ -0,0 +1,2 @@
[app]
router = ['v1']
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import Field
from backend.common.schema import SchemaBase
class ImportParam(SchemaBase):
"""导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app')
table_schema: str = Field(description='数据库名')
table_name: str = Field(description='数据库表名')
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from backend.common.schema import SchemaBase
class GenBusinessSchemaBase(SchemaBase):
"""代码生成业务基础模型"""
app_name: str = Field(description='应用名称(英文)')
table_name_en: str = Field(description='表名称(英文)')
table_name_zh: str = Field(description='表名称(中文)')
table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_comment: str | None = Field(None, description='表描述')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase):
"""创建代码生成业务参数"""
class UpdateGenBusinessParam(GenBusinessSchemaBase):
"""更新代码生成业务参数"""
class GetGenBusinessDetail(GenBusinessSchemaBase):
"""获取代码生成业务详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='主键 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import ConfigDict, Field, field_validator
from backend.common.schema import SchemaBase
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase):
"""代码生成模型基础模型"""
name: str = Field(description='列名称')
comment: str | None = Field(None, description='列描述')
type: str = Field(description='SQLA 模型列类型')
default: str | None = Field(None, description='列默认值')
sort: int = Field(description='列排序')
length: int = Field(description='列长度')
is_pk: bool = Field(False, description='是否主键')
is_nullable: bool = Field(False, description='是否可为空')
gen_business_id: int = Field(description='代码生成业务ID')
@field_validator('type')
@classmethod
def type_update(cls, v: str) -> str:
"""更新列类型"""
return sql_type_to_sqlalchemy(v)
class CreateGenModelParam(GenModelSchemaBase):
"""创建代码生成模型参数"""
class UpdateGenModelParam(GenModelSchemaBase):
"""更新代码生成模型参数"""
class GetGenModelDetail(GenModelSchemaBase):
"""获取代码生成模型详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='主键 ID')
pd_type: str = Field(description='列类型对应的 pydantic 类型')
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_gen_business import gen_business_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
class GenBusinessService:
"""代码生成业务服务类"""
@staticmethod
async def get(*, pk: int) -> GenBusiness:
"""
获取指定 ID 的业务
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='代码生成业务不存在')
return business
@staticmethod
async def get_all() -> Sequence[GenBusiness]:
"""获取所有业务"""
async with async_db_session() as db:
return await gen_business_dao.get_all(db)
@staticmethod
async def create(*, obj: CreateGenBusinessParam) -> None:
"""
创建业务
:param obj: 创建业务参数
:return:
"""
async with async_db_session.begin() as db:
business = await gen_business_dao.get_by_name(db, obj.table_name_en)
if business:
raise errors.ForbiddenError(msg='代码生成业务已存在')
await gen_business_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int:
"""
更新业务
:param pk: 业务 ID
:param obj: 更新业务参数
:return:
"""
async with async_db_session.begin() as db:
return await gen_business_dao.update(db, pk, obj)
@staticmethod
async def delete(*, pk: int) -> int:
"""
删除业务
:param pk: 业务 ID
:return:
"""
async with async_db_session.begin() as db:
return await gen_business_dao.delete(db, pk)
gen_business_service: GenBusinessService = GenBusinessService()
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_gen_model import gen_model_dao
from backend.plugin.code_generator.enums import GenModelMySQLColumnType
from backend.plugin.code_generator.model import GenModel
from backend.plugin.code_generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenModelService:
"""代码生成模型服务类"""
@staticmethod
async def get(*, pk: int) -> GenModel:
"""
获取指定 ID 的模型
:param pk: 模型 ID
:return:
"""
async with async_db_session() as db:
model = await gen_model_dao.get(db, pk)
if not model:
raise errors.NotFoundError(msg='代码生成模型不存在')
return model
@staticmethod
async def get_types() -> list[str]:
"""获取所有 MySQL 列类型"""
types = GenModelMySQLColumnType.get_member_keys()
types.sort()
return types
@staticmethod
async def get_by_business(*, business_id: int) -> Sequence[GenModel]:
"""
获取指定业务的所有模型
:param business_id: 业务 ID
:return:
"""
async with async_db_session() as db:
return await gen_model_dao.get_all_by_business(db, business_id)
@staticmethod
async def create(*, obj: CreateGenModelParam) -> None:
"""
创建模型
:param obj: 创建模型参数
:return:
"""
async with async_db_session.begin() as db:
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
if obj.name in [gen_model.name for gen_model in gen_models]:
raise errors.ForbiddenError(msg='禁止添加相同列到同一模型表')
pd_type = sql_type_to_pydantic(obj.type)
await gen_model_dao.create(db, obj, pd_type=pd_type)
@staticmethod
async def update(*, pk: int, obj: UpdateGenModelParam) -> int:
"""
更新模型
:param pk: 模型 ID
:param obj: 更新模型参数
:return:
"""
async with async_db_session.begin() as db:
model = await gen_model_dao.get(db, pk)
if obj.name != model.name:
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
if obj.name in [gen_model.name for gen_model in gen_models]:
raise errors.ForbiddenError(msg='模型列名已存在')
pd_type = sql_type_to_pydantic(obj.type)
return await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
@staticmethod
async def delete(*, pk: int) -> int:
"""
删除模型
:param pk: 模型 ID
:return:
"""
async with async_db_session.begin() as db:
return await gen_model_dao.delete(db, pk)
gen_model_service: GenModelService = GenModelService()
@@ -0,0 +1,246 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import os.path
import zipfile
from pathlib import Path
import aiofiles
from pydantic.alias_generators import to_pascal
from backend.common.exception import errors
from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_gen import gen_dao
from backend.plugin.code_generator.crud.crud_gen_business import gen_business_dao
from backend.plugin.code_generator.crud.crud_gen_model import gen_model_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.schema.gen_business import CreateGenBusinessParam
from backend.plugin.code_generator.schema.gen_model import CreateGenModelParam
from backend.plugin.code_generator.service.gen_model_service import gen_model_service
from backend.plugin.code_generator.utils.gen_template import gen_template
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenService:
"""代码生成服务类"""
@staticmethod
async def get_tables(*, table_schema: str) -> list[str]:
"""
获取指定 schema 下的所有表名
:param table_schema: 数据库 schema 名称
:return:
"""
async with async_db_session() as db:
return await gen_dao.get_all_tables(db, table_schema)
@staticmethod
async def import_business_and_model(*, obj: ImportParam) -> None:
"""
导入业务和模型数据
:param obj: 导入参数对象
:return:
"""
async with async_db_session.begin() as db:
table_info = await gen_dao.get_table(db, obj.table_name)
if not table_info:
raise errors.NotFoundError(msg='数据库表不存在')
business_info = await gen_business_dao.get_by_name(db, obj.table_name)
if business_info:
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0]
business_data = {
'app_name': obj.app,
'table_name_en': table_name,
'table_name_zh': table_info[1] or ' '.join(table_name.split('_')),
'table_simple_name_zh': table_info[1] or table_name.split('_')[-1],
'table_comment': table_info[1],
}
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
db.add(new_business)
await db.flush()
column_info = await gen_dao.get_all_columns(db, obj.table_schema, table_name)
for column in column_info:
column_type = column[-1].split('(')[0].upper()
pd_type = sql_type_to_pydantic(column_type)
model_data = {
'name': column[0],
'comment': column[-2],
'type': column_type,
'sort': column[-3],
'length': column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
'is_pk': column[1],
'is_nullable': column[2],
'gen_business_id': new_business.id,
}
await gen_model_dao.create(db, CreateGenModelParam(**model_data), pd_type=pd_type)
@staticmethod
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
"""
渲染模板代码
:param business: 业务对象
:return:
"""
gen_models = await gen_model_service.get_by_business(business_id=business.id)
if not gen_models:
raise errors.NotFoundError(msg='代码生成模型表为空')
gen_vars = gen_template.get_vars(business, gen_models)
return {
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
for tpl_path in gen_template.get_template_files()
}
async def preview(self, *, pk: int) -> dict[str, bytes]:
"""
预览生成的代码
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
codes = {}
for tpl, code in tpl_code_map.items():
if tpl.startswith('python'):
codes[tpl.replace('.jinja', '.py').split('/')[-1]] = code.encode('utf-8')
return codes
@staticmethod
async def get_generate_path(*, pk: int) -> list[str]:
"""
获取代码生成路径
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
gen_path = business.gen_path or 'fba-backend-app-dir'
target_files = gen_template.get_code_gen_paths(business)
return [os.path.join(gen_path, *target_file.split('/')) for target_file in target_files]
async def generate(self, *, pk: int) -> None:
"""
生成代码文件
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
gen_path = business.gen_path or os.path.join(BASE_PATH, 'app')
for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join(
gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/'),
)
# 写入 init 文件
str_code_filepath = str(code_filepath)
code_folder = Path(str_code_filepath).parent
code_folder.mkdir(parents=True, exist_ok=True)
init_filepath = code_folder.joinpath('__init__.py')
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# api __init__.py
if 'api' in str_code_filepath:
api_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# app __init__.py
if 'service' in str_code_filepath:
app_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# model init 文件补充
if code_folder.name == 'model':
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
await f.write(
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
async def download(self, *, pk: int) -> io.BytesIO:
"""
下载生成的代码
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
bio = io.BytesIO()
with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items():
code_filepath = gen_template.get_code_gen_path(tpl_path, business)
# 写入 init 文件
code_dir = os.path.dirname(code_filepath)
init_filepath = os.path.join(code_dir, '__init__.py')
if 'model' not in code_filepath.split('/'):
zf.writestr(init_filepath, gen_template.init_content)
else:
zf.writestr(
init_filepath,
f'{gen_template.init_content}'
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
)
# api __init__.py
if 'api' in code_dir:
api_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content)
# app __init__.py
if 'service' in code_dir:
app_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(app_init_filepath, gen_template.init_content)
# 写入代码文件
zf.writestr(code_filepath, code)
bio.seek(0)
return bio
gen_service: GenService = GenService()
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Get{{ schema_name }}Detail, Update{{ schema_name }}Param
from backend.app.{{ app_name }}.service.{{ table_name_en }}_service import {{ table_name_en }}_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
router = APIRouter()
@router.get('/{pk}', summary='获取{{ table_simple_name_zh }}详情', dependencies=[DependsJwtAuth])
async def get_{{ table_name_en }}(pk: Annotated[int, Path(description='{{ table_simple_name_zh }} ID')]) -> ResponseSchemaModel[Get{{ schema_name }}Detail]:
{{ table_name_en }} = await {{ table_name_en }}_service.get(pk=pk)
return response_base.success(data={{ table_name_en }})
@router.get(
'',
summary='分页获取所有{{ table_simple_name_zh }}',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_{{ table_name_en }}s(db: CurrentSession) -> ResponseSchemaModel[PageData[Get{{ schema_name }}Detail]]:
{{ table_name_en }}_select = await {{ table_name_en }}_service.get_select()
page_data = await paging_data(db, {{ table_name_en }}_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建{{ table_simple_name_zh }}',
dependencies=[
Depends(RequestPermission('{{ permission }}:add')),
DependsRBAC,
],
)
async def create_{{ table_name_en }}(obj: Create{{ schema_name }}Param) -> ResponseModel:
await {{ table_name_en }}_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新{{ table_simple_name_zh }}',
dependencies=[
Depends(RequestPermission('{{ permission }}:edit')),
DependsRBAC,
],
)
async def update_{{ table_name_en }}(pk: Annotated[int, Path(description='{{ table_simple_name_zh }} ID')], obj: Update{{ schema_name }}Param) -> ResponseModel:
count = await {{ table_name_en }}_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='批量删除{{ table_simple_name_zh }}',
dependencies=[
Depends(RequestPermission('{{ permission }}:del')),
DependsRBAC,
],
)
async def delete_{{ table_name_en }}(pk: Annotated[list[int], Query(description='{{ table_simple_name_zh }} ID 列表')]) -> ResponseModel:
count = await {{ table_name_en }}_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.{{ app_name }}.model import {{ table_name_class }}
from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Update{{ schema_name }}Param
class CRUD{{ table_name_class }}(CRUDPlus[{{ schema_name }}]):
async def get(self, db: AsyncSession, pk: int) -> {{ table_name_class }} | None:
"""
获取{{ table_name_zh }}
:param db: 数据库会话
:param pk: {{ table_simple_name_zh }} ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self) -> Select:
"""获取{{ table_name_zh }}列表"""
return await self.select_order('created_time', 'desc')
async def get_all(self, db: AsyncSession) -> Sequence[{{ table_name_class }}]:
"""
获取所有{{ table_name_zh }}
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def create(self, db: AsyncSession, obj: Create{{ schema_name }}Param) -> None:
"""
创建{{ table_name_zh }}
:param db: 数据库会话
:param obj: 创建{{ table_simple_name_zh }} 参数
:return:
"""
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj: Update{{ schema_name }}Param) -> int:
"""
更新{{ table_name_zh }}
:param db: 数据库会话
:param pk: {{ table_simple_name_zh }} ID
:param obj: 更新 {{ table_simple_name_zh }} 参数
:return:
"""
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除{{ table_name_zh }}
:param db: 数据库会话
:param pk: {{ table_simple_name_zh }} ID
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
{{ table_name_en }}_dao: CRUD{{ table_name_class }} = CRUD{{ table_name_class }}({{ table_name_class }})
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from uuid import UUID
import sqlalchemy as sa
{% if database_type == 'mysql' -%}
from sqlalchemy.dialects import mysql
{% else -%}
from sqlalchemy.dialects import postgresql
{% endif -%}
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import {% if default_datetime_column %}Base{% else %}MappedBase{% endif %}, id_key
class {{ table_name_class }}({% if default_datetime_column %}Base{% else %}MappedBase{% endif %}):
"""{{ table_name_zh }}"""
__tablename__ = '{{ table_name_en }}'
id: Mapped[id_key] = mapped_column(init=False)
{% for model in models %}
{{ model.name }}:
{%- if model.is_nullable %} Mapped[{{ model.pd_type }} | None]
{%- else %} Mapped[{{ model.pd_type }}]
{%- endif %} = mapped_column(
{%- if model.type in ['NVARCHAR', 'String', 'Unicode', 'VARCHAR'] -%}
sa.String({{ model.length }})
{%- elif database_type == 'mysql' and model.type in ['BIT', 'ENUM', 'LONGBLOB', 'LONGTEXT', 'MEDIUMBLOB',
'MEDIUMINT', 'MEDIUMTEXT', 'SET', 'TINYBLOB', 'TINYINT', 'TINYTEXT', 'YEAR'] -%}
mysql.{{ model.type }}()
{%- elif database_type == 'postgresql' and model.type in [
'ARRAY', 'BIT', 'BYTEA', 'CIDR', 'CITEXT', 'DATEMULTIRANGE', 'DATERANGE', 'DOMAIN', 'ENUM', 'HSTORE', 'INET',
'INT4MULTIRANGE', 'INT4RANGE', 'INT8MULTIRANGE', 'INT8RANGE', 'INTERVAL', 'JSONB', 'JSONPATH', 'MACADDR',
'MACADDR8', 'MONEY', 'NUMMULTIRANGE', 'NUMRANGE', 'OID', 'REGCLASS', 'REGCONFIG', 'TSMULTIRANGE', 'TSQUERY',
'TSRANGE', 'TSTZMULTIRANGE', 'TSTZRANGE', 'TSVECTOR'] -%}
{%- else -%}
sa.{{ model.type }}()
{%- endif -%}, default=
{%- if model.is_nullable and model.default == None -%}
None
{%- else -%}
{%- if model.default != None -%}
'{{ model.default }}'
{%- else -%}
{%- if model.pd_type == 'str' -%}
''
{%- elif model.pd_type == 'int' -%}
0
{%- elif model.pd_type == 'bytes' -%}
b''
{%- elif model.pd_type == 'bool' -%}
True
{%- elif model.pd_type == 'float' -%}
0.0
{%- elif model.pd_type == 'dict' -%}
{}
{%- elif model.pd_type == 'date' or model.pd_type == 'datetime' -%}
timezone.now()
{%- elif model.pd_type == 'list[str]' -%}
()
{%- else -%}
''
{%- endif -%}
{%- endif -%}
{%- endif -%}{% if model.sort != 0 %}, sort_order={{ model.sort }}{% endif %}, comment=
{%- if model.comment != None -%}
'{{ model.comment }}')
{% else -%}
None)
{%- endif -%}
{% endfor %}
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
class {{ schema_name }}SchemaBase(SchemaBase):
"""{{ table_simple_name_zh }}基础模型"""
{% for model in models %}
{{ model.name }}: {% if model.nullable %}{{ model.pd_type }} | None = Field(None, description='{{ model.comment }}'){% else %}{{ model.pd_type }} = Field(description='{{ model.comment }}'){% endif %}
{% endfor %}
class Create{{ schema_name }}Param({{ schema_name }}SchemaBase):
"""创建{{ table_simple_name_zh }}参数"""
class Update{{ schema_name }}Param({{ schema_name }}SchemaBase):
"""更新{{ table_simple_name_zh }}参数"""
class Get{{ schema_name }}Detail({{ schema_name }}SchemaBase):
"""{{ table_simple_name_zh }}详情"""
model_config = ConfigDict(from_attributes=True)
id: int
{% if default_datetime_column %}
created_time: datetime
updated_time: datetime | None = None
{% endif %}
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select
from backend.app.{{ app_name }}.crud.crud_{{ table_name_en }} import {{ table_name_en }}_dao
from backend.app.{{ app_name }}.model import {{ table_name_class }}
from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Update{{ schema_name }}Param
from backend.common.exception import errors
from backend.database.db import async_db_session
class {{ table_name_class }}Service:
@staticmethod
async def get(*, pk: int) -> {{ table_name_class }}:
"""
获取{{ table_simple_name_zh }}
:param pk: {{ table_simple_name_zh }} ID
:return:
"""
async with async_db_session() as db:
{{ table_name_en }} = await {{ table_name_en }}_dao.get(db, pk)
if not {{ table_name_en }}:
raise errors.NotFoundError(msg='{{ table_simple_name_zh }}不存在')
return {{ table_name_en }}
@staticmethod
async def get_select() -> Select:
"""获取{{ table_simple_name_zh }}查询对象"""
return await {{ table_name_en }}_dao.get_list()
@staticmethod
async def get_all() -> Sequence[{{ table_name_class }}]:
"""获取所有{{ table_simple_name_zh }}"""
async with async_db_session() as db:
{{ table_name_en }}s = await {{ table_name_en }}_dao.get_all(db)
return {{ table_name_en }}s
@staticmethod
async def create(*, obj: Create{{ schema_name }}Param) -> None:
"""
创建{{ table_simple_name_zh }}
:param obj: 创建{{ table_simple_name_zh }}参数
:return:
"""
async with async_db_session.begin() as db:
await {{ table_name_en }}_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: Update{{ schema_name }}Param) -> int:
"""
更新{{ table_simple_name_zh }}
:param pk: {{ table_simple_name_zh }} ID
:param obj: 更新{{ table_simple_name_zh }}参数
:return:
"""
async with async_db_session.begin() as db:
count = await {{ table_name_en }}_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除{{ table_simple_name_zh }}
:param pk: {{ table_simple_name_zh }} ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await {{ table_name_en }}_dao.delete(db, pk)
return count
{{ table_name_en }}_service: {{ table_name_class }}Service = {{ table_name_class }}Service()
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
from pydantic.alias_generators import to_pascal, to_snake
from backend.core.conf import settings
from backend.plugin.code_generator.model import GenBusiness, GenModel
from backend.plugin.code_generator.path_conf import JINJA2_TEMPLATE_DIR
class GenTemplate:
def __init__(self) -> None:
"""初始化模板生成器"""
self.env = Environment(
loader=FileSystemLoader(JINJA2_TEMPLATE_DIR),
autoescape=select_autoescape(enabled_extensions=['jinja']),
trim_blocks=True,
lstrip_blocks=True,
keep_trailing_newline=True,
enable_async=True,
)
self.init_content = '#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n'
def get_template(self, jinja_file: str) -> Template:
"""
获取模板文件
:param jinja_file: Jinja2 模板文件
:return:
"""
return self.env.get_template(jinja_file)
@staticmethod
def get_template_files() -> list[str]:
"""
获取模板文件列表
:return:
"""
files = []
# python
python_template_path = JINJA2_TEMPLATE_DIR / 'python'
files.extend([f'python/{file.name}' for file in python_template_path.iterdir() if file.is_file()])
return files
@staticmethod
def get_code_gen_paths(business: GenBusiness) -> list[str]:
"""
获取代码生成路径列表
:param business: 代码生成业务对象
:return:
"""
app_name = business.app_name
module_name = business.table_name_en
return [
f'{app_name}/api/{business.api_version}/{module_name}.py',
f'{app_name}/crud/crud_{module_name}.py',
f'{app_name}/model/{module_name}.py',
f'{app_name}/schema/{module_name}.py',
f'{app_name}/service/{module_name}_service.py',
]
def get_code_gen_path(self, tpl_path: str, business: GenBusiness) -> str:
"""
获取代码生成路径
:param tpl_path: 模板文件路径
:param business: 代码生成业务对象
:return:
"""
target_files = self.get_code_gen_paths(business)
code_gen_path_mapping = dict(zip(self.get_template_files(), target_files))
return code_gen_path_mapping[tpl_path]
@staticmethod
def get_vars(business: GenBusiness, models: Sequence[GenModel]) -> dict[str, str | Sequence[GenModel]]:
"""
获取模板变量
:param business: 代码生成业务对象
:param models: 代码生成模型对象列表
:return:
"""
return {
'app_name': business.app_name,
'table_name_en': to_snake(business.table_name_en),
'table_name_class': to_pascal(business.table_name_en),
'table_name_zh': business.table_name_zh,
'table_simple_name_zh': business.table_simple_name_zh,
'table_comment': business.table_comment,
'schema_name': to_pascal(business.schema_name),
'default_datetime_column': business.default_datetime_column,
'permission': str(business.table_name_en.replace('_', ':')),
'database_type': settings.DATABASE_TYPE,
'models': models,
}
gen_template: GenTemplate = GenTemplate()
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.core.conf import settings
from backend.plugin.code_generator.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
def sql_type_to_sqlalchemy(typing: str) -> str:
"""
将 SQL 类型转换为 SQLAlchemy 类型
:param typing: SQL 类型字符串
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
if typing in GenModelMySQLColumnType.get_member_keys():
return typing
else:
if typing in GenModelPostgreSQLColumnType.get_member_keys():
return typing
return 'String'
def sql_type_to_pydantic(typing: str) -> str:
"""
将 SQL 类型转换为 Pydantic 类型
:param typing: SQL 类型字符串
:return:
"""
try:
if settings.DATABASE_TYPE == 'mysql':
return GenModelMySQLColumnType[typing].value
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
return 'str'
return GenModelPostgreSQLColumnType[typing].value
except KeyError:
return 'str'