init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -0,0 +1,132 @@
import urllib.parse
from typing import Annotated
from fastapi import APIRouter, Body, Depends, File, Path, Query, UploadFile, status
from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from .schema import DemoCreateSchema, DemoOutSchema, DemoQueryParam, DemoUpdateSchema
from .service import DemoService
DemoRouter = APIRouter(route_class=OperationLogRoute, prefix="/demo", tags=["示例管理"])
@DemoRouter.get("/detail/{id}", summary="获取示例详情", response_model=ResponseSchema[DemoOutSchema])
async def get_obj_detail_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:detail"]))],
id: Annotated[int, Path(description="示例ID")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
result_dict = await service.detail(id=id)
return SuccessResponse(data=result_dict, msg="获取示例详情成功")
@DemoRouter.get("/list", summary="分页查询示例", response_model=ResponseSchema[PageResultSchema[DemoOutSchema]])
async def get_obj_list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DemoQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
result_dict = await service.page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by,
)
return SuccessResponse(data=result_dict, msg="查询示例列表成功")
@DemoRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建示例", response_model=ResponseSchema[DemoOutSchema])
async def create_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:create"]))],
data: Annotated[DemoCreateSchema, Body(description="创建参数")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
result_dict = await service.create(data=data)
return SuccessResponse(data=result_dict, msg="创建示例成功")
@DemoRouter.put("/update/{id}", summary="修改示例", response_model=ResponseSchema[DemoOutSchema])
async def update_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:update"]))],
id: Annotated[int, Path(description="示例ID")],
data: Annotated[DemoUpdateSchema, Body(description="修改参数")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
result_dict = await service.update(id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改示例成功")
@DemoRouter.delete("/delete", summary="删除示例", response_model=ResponseSchema[None])
async def delete_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:delete"]))],
ids: Annotated[list[int], Body(description="ID列表")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
await service.delete(ids=ids)
return SuccessResponse(msg="删除示例成功")
@DemoRouter.patch("/status/batch", summary="批量修改示例状态", response_model=ResponseSchema[None])
async def batch_set_available_obj_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:patch"]))],
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
await service.set_available(data=data)
return SuccessResponse(msg="批量修改示例状态成功")
@DemoRouter.post("/export", summary="导出示例")
async def export_obj_list_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:export"]))],
search: Annotated[DemoQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> StreamingResponse:
service = DemoService(auth, db)
result_dict_list = await service.get_list(search=search)
export_result = DemoService.batch_export(obj_list=[item.model_dump() for item in result_dict_list])
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=demo.xlsx"},
)
@DemoRouter.post("/import", summary="导入示例", response_model=ResponseSchema[str])
async def import_obj_list_controller(
file: Annotated[UploadFile, File(description="导入文件")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_example:demo:import"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
service = DemoService(auth, db)
batch_import_result = await service.batch_import(file=file, update_support=True)
return SuccessResponse(data=batch_import_result, msg="导入示例成功")
@DemoRouter.post("/download/template", summary="获取示例导入模板", dependencies=[Depends(AuthPermission(["module_example:demo:download"]))])
async def export_obj_template_controller() -> StreamingResponse:
import_template_result = DemoService.import_template_download()
return StreamResponse(
data=bytes2file_response(import_template_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={
"Content-Disposition": f"attachment; filename={urllib.parse.quote('示例导入模板.xlsx')}",
"Access-Control-Expose-Headers": "Content-Disposition",
},
)
@@ -0,0 +1,20 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import DemoModel
from .schema import DemoCreateSchema, DemoUpdateSchema
class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]):
"""示例数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
"""初始化CRUD数据层
参数:
- auth (AuthSchema): 认证信息模型
- db (AsyncSession): 数据库会话
"""
super().__init__(model=DemoModel, auth=auth, db=db)
@@ -0,0 +1,27 @@
from datetime import date, datetime, time
from sqlalchemy import BIGINT, JSON, Boolean, Date, DateTime, Float, Integer, String, Text, Time
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class DemoModel(ModelMixin, UserMixin):
"""示例表 - 涵盖大多数常用数据类型
"""
__tablename__: str = "example_demo"
__table_args__: dict[str, str] = {"comment": "示例表"}
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="名称")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
int_val: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="整数")
bigint_val: Mapped[int | None] = mapped_column(BIGINT, nullable=True, comment="大整数")
float_val: Mapped[float | None] = mapped_column(Float, nullable=True, comment="浮点数")
bool_val: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="布尔型")
date_val: Mapped[date | None] = mapped_column(Date, nullable=True, comment="日期")
time_val: Mapped[time | None] = mapped_column(Time, nullable=True, comment="时间")
datetime_val: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, comment="日期时间")
text_val: Mapped[str | None] = mapped_column(Text, nullable=True, comment="长文本")
json_val: Mapped[dict | None] = mapped_column(JSON, nullable=True, comment="元数据(JSON格式)")
@@ -0,0 +1,95 @@
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
from app.core.validator import DateStr, DateTimeStr, TimeStr
class DemoCreateSchema(BaseModel):
"""新增模型"""
name: str = Field(..., description="名称")
status: int = Field(default=0, ge=0, le=1, description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, description="描述")
int_val: int | None = Field(default=None, description="整数")
bigint_val: int | None = Field(default=None, description="大整数")
float_val: float | None = Field(default=None, description="浮点数")
bool_val: bool = Field(default=True, description="布尔型")
date_val: DateStr | None = Field(default=None, description="日期")
time_val: TimeStr | None = Field(default=None, description="时间")
datetime_val: DateTimeStr | None = Field(default=None, description="日期时间")
text_val: str | None = Field(default=None, description="长文本")
json_val: dict | None = Field(default=None, description="元数据(JSON格式)")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""验证名称字段的格式和内容。
参数:
- v (str): 原始名称。
返回:
- str: 去空白后的名称。
异常:
- ValueError: 名称为空时抛出。
"""
# 去除首尾空格
v = v.strip()
if not v:
raise ValueError("名称不能为空")
return v
@model_validator(mode="after")
def _after_validation(self):
"""核心业务规则校验
"""
# 长度校验:名称最小长度
if len(self.name) < 2 or len(self.name) > 50:
raise ValueError("名称长度必须在2-50个字符之间")
# 格式校验:名称只能包含字母、数字、下划线和中划线
if not all(c.isalnum() or c in "-_" for c in self.name):
raise ValueError("名称只能包含字母、数字、下划线和中划线")
if self.status not in [0, 1]:
raise ValueError("是否启用必须为0或1")
# 描述校验:描述最大长度
if self.description and len(self.description) > 255:
raise ValueError("描述长度不能超过255个字符")
return self
class DemoUpdateSchema(BaseModel):
"""更新模型"""
name: str | None = Field(default=None, description="名称")
status: int | None = Field(default=None, ge=0, le=1, description="是否启用(0:启用 1:禁用)")
description: str | None = Field(default=None, description="描述")
int_val: int | None = Field(default=None, description="整数")
bigint_val: int | None = Field(default=None, description="大整数")
float_val: float | None = Field(default=None, description="浮点数")
bool_val: bool | None = Field(default=None, description="布尔型")
date_val: DateStr | None = Field(default=None, description="日期")
time_val: TimeStr | None = Field(default=None, description="时间")
datetime_val: DateTimeStr | None = Field(default=None, description="日期时间")
text_val: str | None = Field(default=None, description="长文本")
json_val: dict | None = Field(default=None, description="元数据(JSON格式)")
class DemoOutSchema(DemoCreateSchema, BaseSchema, UserBySchema):
"""响应模型"""
model_config = ConfigDict(from_attributes=True)
class DemoQueryParam(BaseQueryParam, UserByQueryParam):
"""示例查询参数(演示 Mixin 继承用法)"""
name: str | None = Field(None, description="名称", json_schema_extra={"q": "like"})
description: str | None = Field(None, description="描述", json_schema_extra={"q": "like"})
status: int | None = Field(None, description="是否启用", json_schema_extra={"q": "eq"})
@@ -0,0 +1,202 @@
from typing import Any
from fastapi import UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from .crud import DemoCRUD
from .schema import (
DemoCreateSchema,
DemoOutSchema,
DemoQueryParam,
DemoUpdateSchema,
)
class DemoService:
"""示例管理模块服务层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def detail(self, id: int) -> DemoOutSchema:
obj = await DemoCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="该数据不存在")
return DemoOutSchema.model_validate(obj)
async def get_list(
self,
search: DemoQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[DemoOutSchema]:
obj_list = await DemoCRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by)
return [DemoOutSchema.model_validate(obj) for obj in obj_list]
async def page(
self,
page_no: int,
page_size: int,
search: DemoQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[DemoOutSchema]:
offset = (page_no - 1) * page_size
return await DemoCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search, {}),
out_schema=DemoOutSchema,
)
async def create(self, data: DemoCreateSchema) -> DemoOutSchema:
obj = await DemoCRUD(self.auth, self.db).get(name=data.name)
if obj:
raise CustomException(msg="创建失败,名称已存在")
obj = await DemoCRUD(self.auth, self.db).create(data=data)
return DemoOutSchema.model_validate(obj)
async def update(self, id: int, data: DemoUpdateSchema) -> DemoOutSchema:
obj = await DemoCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="更新失败,该数据不存在")
exist_obj = await DemoCRUD(self.auth, self.db).get(name=data.name)
if exist_obj and exist_obj.id != id:
raise CustomException(msg="更新失败,名称重复")
obj = await DemoCRUD(self.auth, self.db).update(id=id, data=data)
return DemoOutSchema.model_validate(obj)
async def delete(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
objs = await DemoCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
obj_map = {o.id: o for o in objs}
for id_ in ids:
if id_ not in obj_map:
raise CustomException(msg="删除失败,该数据不存在")
await DemoCRUD(self.auth, self.db).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
await DemoCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
@staticmethod
def batch_export(obj_list: list[dict[str, Any]]) -> bytes:
mapping_dict = {
"id": "编号",
"name": "名称",
"status": "状态",
"description": "备注",
"created_time": "创建时间",
"updated_time": "更新时间",
"created_id": "创建者",
}
data = obj_list.copy()
for item in data:
item["status"] = "启用" if item.get("status") == 0 else "停用"
creator_info = item.get("created_id")
if isinstance(creator_info, dict):
item["created_id"] = creator_info.get("name", "未知")
else:
item["created_id"] = "未知"
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
async def batch_import(self, file: UploadFile, update_support: bool = False) -> str:
header_dict = {"名称": "name", "状态": "status", "描述": "description"}
try:
contents = await file.read()
rows = ExcelUtil.read_excel_to_dicts(contents)
await file.close()
if not rows:
raise CustomException(msg="导入文件为空")
missing_headers = [h for h in header_dict if h not in rows[0]]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
# 将中文字段名映射为英文字段
mapped_rows = []
for row in rows:
mapped_rows.append({en: row.get(ch) for ch, en in header_dict.items()})
required_fields = ["name", "status"]
errors = []
for field in required_fields:
missing_indices = [i + 1 for i, r in enumerate(mapped_rows) if r.get(field) is None]
if missing_indices:
field_name = next(k for k, v in header_dict.items() if v == field)
rows_str = "".join(str(i) for i in missing_indices)
errors.append(f"{field_name}不能为空,第{rows_str}")
if errors:
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
error_msgs = []
success_count = 0
for i, row in enumerate(mapped_rows, start=1):
try:
status_str = str(row["status"]).strip()
if status_str == "正常":
status = 0
elif status_str == "停用":
status = 1
else:
error_msgs.append(f"{i}行: 状态必须是'正常''停用'")
continue
create_data = DemoCreateSchema(
name=str(row["name"]),
status=status,
description=str(row["description"] or ""),
)
exists_obj = await DemoCRUD(self.auth, self.db).get(name=create_data.name)
if exists_obj:
if update_support:
update_data = DemoUpdateSchema(
name=create_data.name,
status=create_data.status,
description=create_data.description,
)
await DemoCRUD(self.auth, self.db).update(id=exists_obj.id, data=update_data)
success_count += 1
else:
error_msgs.append(f"{i}行: 对象 {create_data.name} 已存在")
else:
await DemoCRUD(self.auth, self.db).create(data=create_data)
success_count += 1
except Exception as e:
error_msgs.append(f"{i}行: {e!s}")
continue
result = f"成功导入 {success_count} 条数据"
if error_msgs:
result += "\n错误信息:\n" + "\n".join(error_msgs)
return result
except Exception as e:
logger.error(f"批量导入用户失败: {e!s}")
raise CustomException(msg=f"导入失败: {e!s}")
@staticmethod
def import_template_download() -> bytes:
header_list = ["名称", "状态", "描述"]
selector_header_list = ["状态"]
option_list = [{"状态": ["正常", "停用"]}]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list,
)
@@ -0,0 +1,9 @@
# 插件元数据(可选):供文档、运维与 module_application.portal 展示使用。
# 不用于运行时 pip 安装依赖;依赖见仓库根目录 pyproject.toml / uv.lock。
name = "example"
title = "示例插件"
version = "1.0.0"
description = "演示 module_* 目录约定与动态路由注册(demo)。"
optional = true
tags = ["demo", "sample"]