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
@@ -0,0 +1 @@
@@ -0,0 +1,91 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter, get_current_user
from app.core.router_class import OperationLogRoute
from .schema import NoticeCreateSchema, NoticeOutSchema, NoticeQueryParam, NoticeUpdateSchema
from .service import NoticeService
NoticeRouter = APIRouter(route_class=OperationLogRoute, prefix="/notice", tags=["公告通知"])
@NoticeRouter.get("/detail/{id}", summary="获取公告详情", response_model=ResponseSchema[NoticeOutSchema])
async def get_notice_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:detail"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="公告ID")],
) -> JSONResponse:
result_dict = await NoticeService(auth, db).detail(id=id)
return SuccessResponse(data=result_dict, msg="获取公告详情成功")
@NoticeRouter.get("/list", summary="查询公告", response_model=ResponseSchema[PageResultSchema[NoticeOutSchema]])
async def get_notice_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[NoticeQueryParam, Query()],
) -> JSONResponse:
result_dict = await NoticeService(auth, db).page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by,
)
return SuccessResponse(data=result_dict, msg="查询公告列表成功")
@NoticeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建公告", response_model=ResponseSchema[NoticeOutSchema])
async def create_notice_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[NoticeCreateSchema, Body(description="公告创建参数")],
) -> JSONResponse:
result_dict = await NoticeService(auth, db).create(data=data)
return SuccessResponse(data=result_dict, msg="创建公告成功")
@NoticeRouter.put("/update/{id}", summary="修改公告", response_model=ResponseSchema[NoticeOutSchema])
async def update_notice_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="公告ID", ge=1)],
data: Annotated[NoticeUpdateSchema, Body(description="公告修改参数")],
) -> JSONResponse:
result_dict = await NoticeService(auth, db).update(id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改公告成功")
@NoticeRouter.delete("/delete", summary="删除公告", response_model=ResponseSchema[None])
async def delete_notice_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="ID列表")],
) -> JSONResponse:
await NoticeService(auth, db).delete(ids=ids)
return SuccessResponse(msg="删除公告成功")
@NoticeRouter.patch("/status/batch", summary="批量修改公告状态", response_model=ResponseSchema[None])
async def batch_set_available_notice_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:notice:patch"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
) -> JSONResponse:
await NoticeService(auth, db).set_available(data=data)
return SuccessResponse(msg="批量修改公告状态成功")
@NoticeRouter.get("/available", summary="获取全局启用公告", response_model=ResponseSchema[list[NoticeOutSchema]])
async def get_notice_list_available_controller(
auth: Annotated[AuthSchema, Depends(get_current_user)],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
result_dict = await NoticeService(auth, db).available_page()
return SuccessResponse(data=result_dict.items, msg="查询已启用公告列表成功")
@@ -0,0 +1,14 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import NoticeModel
from .schema import NoticeCreateSchema, NoticeUpdateSchema
class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]):
"""公告数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=NoticeModel, auth=auth, db=db)
@@ -0,0 +1,17 @@
from sqlalchemy import Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class NoticeModel(ModelMixin, UserMixin):
"""通知公告表"""
__tablename__: str = "sys_notice"
__table_args__: dict[str, str] = {"comment": "通知公告表"}
notice_title: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="公告标题")
notice_type: Mapped[str] = mapped_column(String(1), nullable=False, index=True, comment="公告类型(1通知 2公告)")
notice_content: Mapped[str | None] = mapped_column(Text, nullable=True, comment="公告内容")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:草稿 1:已发布 2:已归档)")
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
@@ -0,0 +1,67 @@
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
from app.utils.xss_util import sanitize_html
class NoticeCreateSchema(BaseModel):
"""公告通知创建模型"""
notice_title: str = Field(..., min_length=1, max_length=64, description="公告标题")
notice_type: str = Field(..., max_length=1, description="公告类型(1:通知 2:公告)")
notice_content: str | None = Field(default=None, max_length=65535, description="公告内容")
status: int = Field(default=0, ge=0, le=2, description="状态(0:草稿 1:已发布 2:已归档)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("notice_type")
@classmethod
def _validate_notice_type(cls, value: str):
if value not in {"1", "2"}:
raise ValueError("公告类型仅支持 1(通知) 或 2(公告)")
return value
@field_validator("status")
@classmethod
def _validate_status(cls, value: int):
if value not in {0, 1, 2}:
raise ValueError("状态仅支持 0(草稿) 1(已发布) 2(已归档)")
return value
@field_validator("notice_content")
@classmethod
def _sanitize_notice_content(cls, value: str | None) -> str | None:
if value is None:
return value
return sanitize_html(value)
@model_validator(mode="after")
def _validate_after(self):
if not self.notice_title.strip():
raise ValueError("公告标题不能为空")
if self.notice_content and not self.notice_content.strip():
raise ValueError("公告内容不能为空")
return self
class NoticeUpdateSchema(NoticeCreateSchema):
"""公告通知更新模型"""
class NoticeOutSchema(NoticeCreateSchema, BaseSchema, UserBySchema):
"""公告通知响应模型"""
model_config = ConfigDict(from_attributes=True)
class NoticeQueryParam(BaseQueryParam, UserByQueryParam):
"""公告通知查询参数"""
notice_title: str | None = Field(None, description="公告标题", json_schema_extra={"q": "like"})
notice_type: str | None = Field(None, description="公告类型", json_schema_extra={"q": "eq"})
status: int | None = Field(None, ge=0, le=2, description="状态(0:草稿 1:已发布 2:已归档)", json_schema_extra={"q": "eq"})
@@ -0,0 +1,162 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.exceptions import CustomException
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from .crud import NoticeCRUD
from .schema import NoticeCreateSchema, NoticeOutSchema, NoticeQueryParam, NoticeUpdateSchema
class NoticeService:
"""公告管理服务
提供公告 CRUD、状态切换、已启用公告分页查询、Excel 导出等业务能力。
"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def detail(self, id: int) -> NoticeOutSchema:
"""获取公告详情
参数:
- id (int): 公告 ID
返回:
- NoticeOutSchema: 公告响应模型
"""
obj = await NoticeCRUD(self.auth, self.db).get_or_404(id=id)
return NoticeOutSchema.model_validate(obj)
async def get_list(
self,
search: NoticeQueryParam | None = None,
order_by: list[dict] | None = None,
) -> list[NoticeOutSchema]:
"""获取公告列表
参数:
- search (NoticeQueryParam | None): 查询参数
- order_by (list[dict] | None): 排序规则
返回:
- list[NoticeOutSchema]: 公告列表
"""
notice_obj_list = await NoticeCRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by)
return [NoticeOutSchema.model_validate(notice_obj) for notice_obj in notice_obj_list]
async def page(
self,
page_no: int,
page_size: int,
search: NoticeQueryParam | None = None,
order_by: list[dict] | None = None,
) -> PageResultSchema[NoticeOutSchema]:
"""分页查询公告
参数:
- page_no (int): 当前页码
- page_size (int): 每页条数
- search (NoticeQueryParam | None): 查询参数
- order_by (list[dict] | None): 排序规则
返回:
- PageResultSchema[NoticeOutSchema]: 分页结果
"""
offset = (page_no - 1) * page_size
return await NoticeCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search),
out_schema=NoticeOutSchema,
)
async def available_page(self) -> PageResultSchema[NoticeOutSchema]:
"""获取已启用的公告(首页展示用,最多 10 条)"""
return await NoticeCRUD(self.auth, self.db).page(
offset=0,
limit=10,
order_by=[{"id": "asc"}],
search={"status": ("eq", 0)},
out_schema=NoticeOutSchema,
)
async def create(self, data: NoticeCreateSchema) -> NoticeOutSchema:
"""创建公告
参数:
- data (NoticeCreateSchema): 公告创建模型
返回:
- NoticeOutSchema: 公告响应模型
"""
notice = await NoticeCRUD(self.auth, self.db).get(notice_title=data.notice_title)
if notice:
raise CustomException(msg="创建失败,该数据已存在")
notice_obj = await NoticeCRUD(self.auth, self.db).create(data=data)
return await self.detail(id=notice_obj.id)
async def update(self, id: int, data: NoticeUpdateSchema) -> NoticeOutSchema:
"""更新公告
参数:
- id (int): 公告 ID
- data (NoticeUpdateSchema): 公告更新模型
返回:
- NoticeOutSchema: 公告响应模型
"""
_ = await NoticeCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在")
exist_notice = await NoticeCRUD(self.auth, self.db).get(notice_title=data.notice_title)
if exist_notice and exist_notice.id != id:
raise CustomException(msg="更新失败,标题已存在")
await NoticeCRUD(self.auth, self.db).update(id=id, data=data)
return await self.detail(id=id)
async def delete(self, ids: list[int]) -> None:
"""删除公告
参数:
- ids (list[int]): 公告 ID 列表
"""
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
notices = await NoticeCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
notice_map = {n.id: n for n in notices}
for nid in ids:
if nid not in notice_map:
raise CustomException(msg="删除失败,该数据不存在")
await NoticeCRUD(self.auth, self.db).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
"""批量启用/禁用公告
参数:
- data (BatchSetAvailable): 批量设置状态模型
"""
await NoticeCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
@staticmethod
def export(notice_list: list[dict]) -> bytes:
"""导出公告列表为 Excel
参数:
- notice_list (list[dict]): 公告数据列表(英文字段名)
返回:
- bytes: Excel 文件字节流
"""
mapping_dict = {
"id": "编号",
"notice_title": "公告标题",
"notice_type": "公告类型(1通知 2公告)",
"notice_content": "公告内容",
"status": "状态",
"description": "备注",
"created_time": "创建时间",
}
return ExcelUtil.export_list2excel(notice_list, mapping_dict)