init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user