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,118 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, Security, 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 RoleCreateSchema, RoleOutSchema, RolePermissionSettingSchema, RoleQueryParam, RoleUpdateSchema
from .service import RoleService
RoleRouter = APIRouter(route_class=OperationLogRoute, prefix="/role", tags=["角色管理"])
@RoleRouter.get("/list", summary="查询角色", response_model=ResponseSchema[PageResultSchema[RoleOutSchema]])
async def get_role_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[RoleQueryParam, Query()],
) -> JSONResponse:
result_dict = await RoleService(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="查询角色成功")
@RoleRouter.get("/detail/{id}", summary="查询角色详情", response_model=ResponseSchema[RoleOutSchema])
async def get_role_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:detail"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="角色ID", ge=1)],
) -> JSONResponse:
result_dict = await RoleService(auth, db).detail(id=id)
return SuccessResponse(data=result_dict, msg="获取角色详情成功")
@RoleRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建角色", response_model=ResponseSchema[RoleOutSchema])
async def create_role_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[RoleCreateSchema, Body(description="角色创建参数")],
) -> JSONResponse:
result_dict = await RoleService(auth, db).create(data=data)
return SuccessResponse(data=result_dict, msg="创建角色成功")
@RoleRouter.put("/update/{id}", summary="修改角色", response_model=ResponseSchema[RoleOutSchema])
async def update_role_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="角色ID", ge=1)],
data: Annotated[RoleUpdateSchema, Body(description="角色修改参数")],
) -> JSONResponse:
result_dict = await RoleService(auth, db).update(id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改角色成功")
@RoleRouter.delete("/delete", summary="删除角色", response_model=ResponseSchema[None])
async def delete_role_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="ID列表")],
) -> JSONResponse:
await RoleService(auth, db).delete(ids=ids)
return SuccessResponse(msg="删除角色成功")
@RoleRouter.patch("/status/batch", summary="批量修改角色状态", response_model=ResponseSchema[None])
async def batch_set_available_role_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:patch"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
) -> JSONResponse:
await RoleService(auth, db).set_available(data=data)
return SuccessResponse(msg="批量修改角色状态成功")
@RoleRouter.put("/permission", summary="角色授权", response_model=ResponseSchema[None])
async def set_role_permission_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:permission"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[RolePermissionSettingSchema, Body(description="角色授权参数")],
) -> JSONResponse:
await RoleService(auth, db).set_permission(data=data)
return SuccessResponse(msg="授权角色成功")
@RoleRouter.get("/options", summary="获取角色下拉选项", response_model=ResponseSchema[list[dict[str, int | str]]])
async def get_role_options_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
options = await RoleService(auth, db).get_options()
return SuccessResponse(data=options, msg="获取角色选项成功")
@RoleRouter.post("/export", summary="导出角色")
async def export_role_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:role:export"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
search: Annotated[RoleQueryParam, Body()],
) -> StreamingResponse:
role_query_result = await RoleService(auth, db).get_list(search=search)
role_export_result = RoleService.export_list(role_list=[item.model_dump() for item in role_query_result])
return StreamResponse(
data=bytes2file_response(role_export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=role.xlsx"},
)
@@ -0,0 +1,74 @@
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_system.dept.crud import DeptCRUD
from app.api.v1.module_system.menu.crud import MenuCRUD
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from .model import RoleModel
from .schema import RoleCreateSchema, RoleUpdateSchema
class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
"""角色模块数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
super().__init__(model=RoleModel, auth=auth, db=db)
async def set_role_menus_crud(self, role_ids: list[int], menu_ids: list[int]) -> None:
"""设置角色的菜单权限
参数:
- role_ids (list[int]): 角色ID列表
- menu_ids (list[int]): 菜单ID列表
返回:
- None
"""
if not role_ids:
raise CustomException(msg="角色ID列表不能为空")
roles = await self.get_list(search={"id": ("in", role_ids)}, preload=["menus"])
if len(roles) != len(set(role_ids)):
missing = sorted(set(role_ids) - {r.id for r in roles})
raise CustomException(msg=f"角色不存在: {missing}")
menus = [] if not menu_ids else await MenuCRUD(self.auth, self.db).get_list(search={"id": ("in", menu_ids)})
if menu_ids and len(menus) != len(set(menu_ids)):
missing = sorted(set(menu_ids) - {m.id for m in menus})
raise CustomException(msg=f"菜单不存在: {missing}")
for obj in roles:
obj.menus.clear()
obj.menus.extend(menus)
await self.db.flush()
async def set_role_depts_crud(self, role_ids: list[int], dept_ids: list[int]) -> None:
"""设置角色的部门权限(含存在性校验)"""
if not role_ids:
raise CustomException(msg="角色ID列表不能为空")
roles = await self.get_list(search={"id": ("in", role_ids)}, preload=["depts"])
if len(roles) != len(set(role_ids)):
missing = sorted(set(role_ids) - {r.id for r in roles})
raise CustomException(msg=f"角色不存在: {missing}")
depts = [] if not dept_ids else await DeptCRUD(self.auth, self.db).get_list(search={"id": ("in", dept_ids)})
if dept_ids and len(depts) != len(set(dept_ids)):
missing = sorted(set(dept_ids) - {d.id for d in depts})
raise CustomException(msg=f"部门不存在: {missing}")
for obj in roles:
relationship = obj.depts
relationship.clear()
relationship.extend(depts)
await self.db.flush()
async def get_options(self) -> list[dict[str, Any]]:
"""获取角色下拉选项,返回 [{value, label}](自动按状态过滤)"""
items = await self.get_list(search={"status": 0})
return [{"value": item.id, "label": item.name} for item in items]
@@ -0,0 +1,76 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import MappedBase, ModelMixin, UserMixin
if TYPE_CHECKING:
from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.menu.model import MenuModel
from app.api.v1.module_system.user.model import UserModel
class RoleMenusModel(MappedBase):
"""角色菜单关联表
定义角色与菜单的多对多关系,用于权限控制
"""
__tablename__: str = "sys_role_menus"
__table_args__: dict[str, str] = {"comment": "角色菜单关联表"}
role_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="角色ID",
)
menu_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="菜单ID",
)
class RoleDeptsModel(MappedBase):
"""角色部门关联表
定义角色与部门的多对多关系,用于数据权限控制
仅当角色的data_scope=5(自定义数据权限)时使用此表
"""
__tablename__: str = "sys_role_depts"
__table_args__: dict[str, str] = {"comment": "角色部门关联表"}
role_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="角色ID",
)
dept_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_dept.id", ondelete="CASCADE", onupdate="CASCADE"),
primary_key=True,
comment="部门ID",
)
class RoleModel(ModelMixin, UserMixin):
"""角色模型"""
__tablename__: str = "sys_role"
__table_args__: dict[str, str] = {"comment": "角色表"}
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="角色名称")
code: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, comment="角色编码")
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)")
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
data_scope: Mapped[int] = mapped_column(Integer, default=1, nullable=False, comment="数据权限范围(1:仅本人 2:本部门及以下 3:全部)")
menus: Mapped[list["MenuModel"]] = relationship(secondary="sys_role_menus", back_populates="roles", order_by="MenuModel.order")
depts: Mapped[list["DeptModel"]] = relationship(secondary="sys_role_depts", back_populates="roles")
users: Mapped[list["UserModel"]] = relationship(secondary="sys_user_roles", back_populates="roles")
@@ -0,0 +1,103 @@
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
from app.api.v1.module_system.dept.schema import DeptOutSchema
from app.api.v1.module_system.menu.schema import MenuOutSchema
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
from app.core.validator import (
role_permission_request_validator,
validate_required_code,
)
class RoleCreateSchema(BaseModel):
"""角色创建模型
"""
name: str = Field(..., min_length=1, max_length=64, description="角色名称")
code: str = Field(..., min_length=2, max_length=64, description="角色编码")
order: int | None = Field(default=1, ge=0, description="显示排序")
data_scope: int | None = Field(
default=1,
ge=1,
le=3,
description="数据权限范围(1:仅本人 2:本部门及以下 3:全部)",
)
status: int = Field(default=0, ge=0, le=1, description="状态(0:启动 1:停用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("code")
@classmethod
def validate_code(cls, value: str):
"""校验角色编码:字母开头,2-64 位,仅含字母/数字/下划线"""
return validate_required_code(value)
@field_validator("status")
@classmethod
def validate_status(cls, value: int):
"""校验状态:仅支持 0(正常)、1(禁用)"""
if value not in {0, 1}:
raise ValueError("状态仅支持 0(正常) 或 1(禁用)")
return value
@field_validator("name")
@classmethod
def validate_name(cls, value: str):
"""校验角色名称:不能为空"""
v = value.strip()
if not v:
raise ValueError("角色名称不能为空")
return v
class RolePermissionSettingSchema(BaseModel):
"""角色权限配置模型
"""
data_scope: int = Field(
default=1,
ge=1,
le=3,
description="数据权限范围(1:仅本人 2:本部门及以下 3:全部)",
)
role_ids: list[int] = Field(default_factory=list, description="角色ID列表")
menu_ids: list[int] = Field(default_factory=list, description="菜单ID列表")
dept_ids: list[int] = Field(default_factory=list, description="部门ID列表")
@model_validator(mode="after")
def validate_fields(self):
"""校验角色权限配置字段(数据范围与关联 ID 等)。
返回:
- RolePermissionSettingSchema: 通过 `role_permission_request_validator` 校验后的同一实例。
"""
return role_permission_request_validator(self)
class RoleUpdateSchema(RoleCreateSchema):
"""角色更新模型
"""
class RoleOutSchema(RoleCreateSchema, BaseSchema, UserBySchema):
"""角色信息响应模型
"""
model_config = ConfigDict(from_attributes=True)
menus: list[MenuOutSchema] = Field(default_factory=list, description="角色菜单列表")
depts: list[DeptOutSchema] = Field(default_factory=list, description="角色部门列表")
class RoleQueryParam(BaseQueryParam, UserByQueryParam):
"""角色管理查询参数
"""
name: str | None = Field(None, description="角色名称", json_schema_extra={"q": "like"})
code: str | None = Field(None, description="角色编码", json_schema_extra={"q": "eq"})
status: int | None = Field(None, description="状态(0:启动 1:停用)", json_schema_extra={"q": "eq"})
@@ -0,0 +1,221 @@
from typing import Any
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 RoleCRUD
from .schema import (
RoleCreateSchema,
RoleOutSchema,
RolePermissionSettingSchema,
RoleQueryParam,
RoleUpdateSchema,
)
_ROLE_PRELOAD = ["menus", "depts"]
class RoleService:
"""角色管理服务
提供角色 CRUD、权限配置、数据权限范围设置、批量启/禁用、Excel 导出等业务能力。
"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def detail(self, id: int) -> RoleOutSchema:
"""获取角色详情
参数:
- id (int): 角色ID
返回:
- RoleOutSchema: 角色详情响应模型
"""
obj = await RoleCRUD(self.auth, self.db).get_or_404(id=id, preload=_ROLE_PRELOAD)
return RoleOutSchema.model_validate(obj)
async def get_options(self) -> list[dict[str, Any]]:
"""获取角色下拉选项,委托给 RoleCRUD"""
return await RoleCRUD(self.auth, self.db).get_options()
async def get_list(
self,
search: RoleQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[RoleOutSchema]:
"""获取角色列表
参数:
- search (RoleQueryParam | None): 查询参数模型
- order_by (list[dict[str, str]] | None): 排序参数列表
返回:
- list[RoleOutSchema]: 角色响应模型列表
"""
role_list = await RoleCRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by, preload=_ROLE_PRELOAD)
return [RoleOutSchema.model_validate(role) for role in role_list]
async def page(
self,
page_no: int,
page_size: int,
search: RoleQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[RoleOutSchema]:
"""分页查询角色(数据库 OFFSET/LIMIT)。
参数:
- page_no (int): 页码(从 1 开始)
- page_size (int): 每页条数
- search (RoleQueryParam | None): 查询条件
- order_by (list[dict[str, str]] | None): 排序字段列表
返回:
- dict: 分页结果(结构由 ``CRUD.page`` 返回约定)
"""
offset = (page_no - 1) * page_size
return await RoleCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search),
out_schema=RoleOutSchema,
preload=_ROLE_PRELOAD,
)
async def create(self, data: RoleCreateSchema) -> RoleOutSchema:
"""
创建角色
参数:
- data (RoleCreateSchema): 创建角色模型
返回:
- RoleOutSchema: 新创建的角色响应模型
"""
role = await RoleCRUD(self.auth, self.db).get(name=data.name)
if role:
raise CustomException(msg="创建失败,该数据已存在")
obj = await RoleCRUD(self.auth, self.db).get(code=data.code)
if obj:
raise CustomException(msg="创建失败,编码已存在")
new_role = await RoleCRUD(self.auth, self.db).create(data=data)
return await self.detail(id=new_role.id)
async def update(self, id: int, data: RoleUpdateSchema) -> RoleOutSchema:
"""更新角色
参数:
- id (int): 角色ID
- data (RoleUpdateSchema): 更新角色模型
返回:
- RoleOutSchema: 更新后的角色响应模型
"""
_ = await RoleCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在")
exist_role = await RoleCRUD(self.auth, self.db).get(name=data.name)
if exist_role and exist_role.id != id:
raise CustomException(msg="更新失败,名称已存在")
exist_code = await RoleCRUD(self.auth, self.db).get(code=data.code)
if exist_code and exist_code.id != id:
raise CustomException(msg="更新失败,角色编码已存在")
await RoleCRUD(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列表
返回:
- None
"""
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
# 批量校验角色存在性
roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
if len(roles) != len(ids):
raise CustomException(msg="删除失败,部分ID不存在")
await RoleCRUD(self.auth, self.db).delete(ids=ids)
async def set_permission(self, data: RolePermissionSettingSchema) -> None:
"""设置角色权限
参数:
- data (RolePermissionSettingSchema): 角色权限设置模型
返回:
- None
"""
# 设置角色菜单权限
await RoleCRUD(self.auth, self.db).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids)
# 设置数据权限范围(自定义部门关联已废弃,直接清空)
await RoleCRUD(self.auth, self.db).set(ids=data.role_ids, data_scope=data.data_scope)
await RoleCRUD(self.auth, self.db).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
async def set_available(self, data: BatchSetAvailable) -> None:
"""设置角色可用状态
参数:
- data (BatchSetAvailable): 批量设置可用状态模型
返回:
- None
"""
roles = await RoleCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)})
role_map = {r.id: r for r in roles}
for rid in data.ids:
if rid not in role_map:
raise CustomException(msg="该数据不存在")
await RoleCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
@staticmethod
def export_list(role_list: list[dict[str, Any]]) -> bytes:
"""导出角色列表
参数:
- role_list (list[dict[str, Any]]): 角色详情字典列表
返回:
- bytes: Excel文件字节流
"""
# 字段映射配置
mapping_dict = {
"id": "角色编号",
"name": "角色名称",
"order": "显示顺序",
"data_scope": "数据权限",
"status": "状态",
"description": "备注",
"created_time": "创建时间",
"updated_time": "更新时间",
"created_id": "创建者ID",
"updated_id": "更新者ID",
}
# 数据权限映射
data_scope_map = {
1: "仅本人数据权限",
2: "本部门及以下数据权限",
3: "全部数据权限",
}
# 处理数据
data = role_list.copy()
for item in data:
item["status"] = "启用" if item.get("status") == 0 else "停用"
item["data_scope"] = data_scope_map.get(item.get("data_scope", 1), "")
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)