mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Update the code generator to plugin (#578)
* Update the code generator to plugin * Fix get all tables return type
This commit is contained in:
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user