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,220 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
from fastapi.responses import JSONResponse
from redis.asyncio.client import Redis
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, redis_getter
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from .schema import (
DictDataCreateSchema,
DictDataOutSchema,
DictDataQueryParam,
DictDataUpdateSchema,
DictTypeCreateSchema,
DictTypeOutSchema,
DictTypeQueryParam,
DictTypeUpdateSchema,
)
from .service import DictDataService, DictTypeService
DictRouter = APIRouter(route_class=OperationLogRoute, prefix="/dict", tags=["字典管理"])
@DictRouter.get("/type/detail/{id}", summary="获取字典类型详情", response_model=ResponseSchema[DictTypeOutSchema])
async def get_type_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:detail"]))],
id: Annotated[int, Path(description="字典类型ID", ge=1)],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
result_dict = await DictTypeService(auth, db).detail(id=id)
return SuccessResponse(data=result_dict, msg="获取字典类型详情成功")
@DictRouter.get("/type/list", summary="查询字典类型", response_model=ResponseSchema[PageResultSchema[DictTypeOutSchema]])
async def get_type_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictTypeQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
result_dict = await DictTypeService(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="查询字典类型列表成功")
@DictRouter.post("/type/export", summary="导出字典类型")
async def export_type_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:export"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictTypeQueryParam, Body()],
) -> StreamResponse:
dict_type_list = await DictTypeService(auth, db).get_list(search=search, order_by=page.order_by)
export_result = DictTypeService.export_list(dict_type_list=[item.model_dump() for item in dict_type_list])
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=dict_type.xlsx"},
)
@DictRouter.get("/type/optionselect", summary="获取全部字典类型", response_model=ResponseSchema[list[DictTypeOutSchema]])
async def get_type_optionselect_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
result_dict_list = await DictTypeService(auth, db).get_list()
return SuccessResponse(data=result_dict_list, msg="获取字典类型列表成功")
@DictRouter.post("/type/create", status_code=status.HTTP_201_CREATED, summary="创建字典类型", response_model=ResponseSchema[DictTypeOutSchema])
async def create_type_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:create"]))],
data: Annotated[DictTypeCreateSchema, Body(description="字典类型创建参数")],
) -> JSONResponse:
result_dict = await DictTypeService(auth, db).create(redis=redis, data=data)
return SuccessResponse(data=result_dict, msg="创建字典类型成功")
@DictRouter.put("/type/update/{id}", summary="修改字典类型", response_model=ResponseSchema[DictTypeOutSchema])
async def update_type_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:update"]))],
id: Annotated[int, Path(description="字典类型ID", ge=1)],
data: Annotated[DictTypeUpdateSchema, Body(description="字典类型修改参数")],
) -> JSONResponse:
result_dict = await DictTypeService(auth, db).update(redis=redis, id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改字典类型成功")
@DictRouter.delete("/type/delete", summary="删除字典类型", response_model=ResponseSchema[None])
async def delete_type_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:delete"]))],
ids: Annotated[list[int], Body(description="字典类型ID列表")],
) -> JSONResponse:
await DictTypeService(auth, db).delete(redis=redis, ids=ids)
return SuccessResponse(msg="删除字典类型成功")
@DictRouter.patch("/type/status/batch", summary="批量修改字典类型状态", response_model=ResponseSchema[None])
async def batch_set_available_dict_type_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_type:patch"]))],
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
await DictTypeService(auth, db).set_available(data=data)
return SuccessResponse(msg="批量修改字典类型状态成功")
@DictRouter.get("/data/detail/{id}", summary="获取字典数据详情", response_model=ResponseSchema[DictDataOutSchema])
async def get_data_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:detail"]))],
id: Annotated[int, Path(description="字典数据ID", ge=1)],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
result_dict = await DictDataService(auth, db).detail(id=id)
return SuccessResponse(data=result_dict, msg="获取字典数据详情成功")
@DictRouter.get("/data/list", summary="查询字典数据", response_model=ResponseSchema[PageResultSchema[DictDataOutSchema]])
async def get_data_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:query"]))],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictDataQueryParam, Query()],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
order_by = [{"order": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict = await DictDataService(auth, db).page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=order_by,
)
return SuccessResponse(data=result_dict, msg="查询字典数据列表成功")
@DictRouter.post("/data/create", status_code=status.HTTP_201_CREATED, summary="创建字典数据", response_model=ResponseSchema[DictDataOutSchema])
async def create_data_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:create"]))],
data: Annotated[DictDataCreateSchema, Body(description="字典数据创建参数")],
) -> JSONResponse:
result_dict = await DictDataService(auth, db).create(redis=redis, data=data)
return SuccessResponse(data=result_dict, msg="创建字典数据成功")
@DictRouter.put("/data/update/{id}", summary="修改字典数据", response_model=ResponseSchema[DictDataOutSchema])
async def update_data_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:update"]))],
id: Annotated[int, Path(description="字典数据ID", ge=1)],
data: Annotated[DictDataUpdateSchema, Body(description="字典数据修改参数")],
) -> JSONResponse:
result_dict = await DictDataService(auth, db).update(redis=redis, id=id, data=data)
return SuccessResponse(data=result_dict, msg="修改字典数据成功")
@DictRouter.delete("/data/delete", summary="删除字典数据", response_model=ResponseSchema[None])
async def delete_data_controller(
redis: Annotated[Redis, Depends(redis_getter)],
db: Annotated[AsyncSession, Depends(db_getter)],
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:delete"]))],
ids: Annotated[list[int], Body(description="ID列表")],
) -> JSONResponse:
await DictDataService(auth, db).delete(redis=redis, ids=ids)
return SuccessResponse(msg="删除字典数据成功")
@DictRouter.patch("/data/status/batch", summary="批量修改字典数据状态", response_model=ResponseSchema[None])
async def batch_set_available_dict_data_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:patch"]))],
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
db: Annotated[AsyncSession, Depends(db_getter)],
) -> JSONResponse:
await DictDataService(auth, db).set_available(data=data)
return SuccessResponse(msg="批量修改字典数据状态成功")
@DictRouter.post("/data/export", summary="导出字典数据")
async def export_data_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:dict_data:export"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[DictDataQueryParam, Body()],
) -> StreamResponse:
dict_data_list = await DictDataService(auth, db).get_list(search=search, order_by=page.order_by)
export_result = DictDataService.export_list(dict_data_list=[item.model_dump() for item in dict_data_list])
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=dict_data.xlsx"},
)
@DictRouter.get("/data/info/{dict_type}", summary="根据字典类型获取数据", response_model=ResponseSchema[list[DictDataOutSchema]])
async def get_init_dict_data_controller(
redis: Annotated[Redis, Depends(redis_getter)],
dict_type: Annotated[str, Path(description="字典类型")],
) -> JSONResponse:
dict_data_query_result = await DictDataService.get_init_cache(redis=redis, dict_type=dict_type)
return SuccessResponse(data=dict_data_query_result, msg="获取初始化字典数据成功")
@@ -0,0 +1,85 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_system.dict.model import DictDataModel, DictTypeModel
from app.api.v1.module_system.dict.schema import (
DictDataCreateSchema,
DictDataUpdateSchema,
DictTypeCreateSchema,
DictTypeUpdateSchema,
)
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateSchema]):
"""数据字典类型数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
"""初始化数据字典类型数据层。
参数:
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
- db (AsyncSession): 数据库会话。
返回:
- None
"""
super().__init__(model=DictTypeModel, auth=auth, db=db)
class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateSchema]):
"""数据字典数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
"""初始化数据字典项数据层。
参数:
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
- db (AsyncSession): 数据库会话。
返回:
- None
"""
super().__init__(model=DictDataModel, auth=auth, db=db)
async def batch_delete(self, ids: list[int], exclude_system: bool = True) -> int:
"""批量删除数据字典数据
参数:
- ids (list[int]): 数据字典数据ID列表
- exclude_system (bool): 是否排除系统默认数据,默认为True
返回:
- int: 删除的记录数量
"""
if exclude_system:
system_data = await self.get_list(
search={
"id__in": ids,
"remark__contains": "系统默认",
},
)
system_ids = [item.id for item in system_data]
ids = [id for id in ids if id not in system_ids]
if ids:
await self.delete(ids=ids)
return len(ids)
async def get_list_by_dict_type(self, dict_type: str, status: int | None = 0) -> Sequence[DictDataModel]:
"""根据字典类型获取字典数据列表
参数:
- dict_type (str): 字典类型
- status (str | None): 状态过滤,None表示不过滤
返回:
- Sequence[DictDataModel]: 数据字典数据模型序列
"""
search: dict[str, Any] = {"dict_type": dict_type}
if status is not None:
search["status"] = status
return await self.get_list(search=search, order_by=[{"id": "asc"}])
@@ -0,0 +1,45 @@
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import ModelMixin
class DictTypeModel(ModelMixin):
"""字典类型表"""
__tablename__: str = "sys_dict_type"
__table_args__: dict[str, str] = {"comment": "字典类型表"}
dict_name: Mapped[str] = mapped_column(String(100), nullable=False, index=True, comment="字典名称")
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, index=True, unique=True, 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="备注")
dict_data_list: Mapped[list["DictDataModel"]] = relationship("DictDataModel", back_populates="dict_type_obj")
class DictDataModel(ModelMixin):
"""字典数据表"""
__tablename__: str = "sys_dict_data"
__table_args__: dict[str, str] = {"comment": "字典数据表"}
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, index=True, comment="状态(0:启动 1:停用)")
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
dict_sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="字典排序")
dict_label: Mapped[str] = mapped_column(String(255), nullable=False, comment="字典标签")
dict_value: Mapped[str] = mapped_column(String(255), nullable=False, comment="字典键值")
css_class: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="样式属性(其他样式扩展)")
list_class: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="表格回显样式")
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否默认(True是 False否)")
dict_type: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="字典类型")
# 添加外键关系,同时保留dict_type字段用于业务查询
dict_type_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("sys_dict_type.id", ondelete="CASCADE"),
nullable=False,
comment="字典类型ID",
)
# 关系定义
dict_type_obj: Mapped[DictTypeModel] = relationship("DictTypeModel", back_populates="dict_data_list")
@@ -0,0 +1,153 @@
import re
from pydantic import (
BaseModel,
ConfigDict,
Field,
field_validator,
model_validator,
)
from app.core.base_schema import BaseQueryParam, BaseSchema
class DictTypeCreateSchema(BaseModel):
"""字典类型表对应pydantic模型
"""
dict_name: str = Field(..., min_length=1, max_length=64, description="字典名称")
dict_type: str = Field(..., min_length=1, max_length=255, description="字典类型编码")
status: int = Field(default=0, ge=0, le=1, description="状态(0:正常 1:停用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("status")
@classmethod
def validate_status(cls, value: int):
if value not in {0, 1}:
raise ValueError("状态仅支持 0(正常) 或 1(停用)")
return value
@field_validator("dict_name")
@classmethod
def validate_dict_name(cls, value: str):
"""校验字典名称为非空字符串。
参数:
- value (str): 字典名称。
返回:
- str: 去首尾空格后的字典名称。
异常:
- ValueError: 字典名称为空时抛出。
"""
if not value or value.strip() == "":
raise ValueError("字典名称不能为空")
return value.strip()
@field_validator("dict_type")
@classmethod
def validate_dict_type(cls, value: str):
"""校验字典类型:小写字母开头,仅包含小写字母/数字/下划线。
参数:
- value (str): 字典类型。
返回:
- str: 去首尾空格后的字典类型。
异常:
- ValueError: 字典类型为空或不满足格式要求时抛出。
"""
if not value or value.strip() == "":
raise ValueError("字典类型不能为空")
regexp = r"^[a-z][a-z0-9_]*$"
if not re.match(regexp, value):
raise ValueError("字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)")
return value.strip()
class DictTypeUpdateSchema(DictTypeCreateSchema):
"""字典类型更新模型"""
class DictTypeOutSchema(DictTypeCreateSchema, BaseSchema):
"""字典类型响应模型"""
model_config = ConfigDict(from_attributes=True)
class DictTypeQueryParam(BaseQueryParam):
"""字典类型查询参数"""
dict_name: str | None = Field(default=None, description="字典名称", max_length=100, json_schema_extra={"q": "like"})
dict_type: str | None = Field(default=None, description="字典类型", max_length=100, json_schema_extra={"q": "eq"})
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:启动 1:停用)", json_schema_extra={"q": "eq"})
class DictDataCreateSchema(BaseModel):
"""字典数据表对应pydantic模型
"""
dict_sort: int = Field(..., ge=1, le=999, description="排序")
dict_label: str = Field(..., min_length=1, max_length=255, description="字典标签")
dict_value: str = Field(..., min_length=1, max_length=255, description="字典键值")
dict_type: str = Field(..., max_length=255, description="字典类型")
dict_type_id: int = Field(..., gt=0, description="字典类型ID")
css_class: str | None = Field(default=None, max_length=255, description="样式属性")
list_class: str | None = Field(default=None, max_length=255, description="表格回显样式")
is_default: bool = Field(default=False, description="是否默认")
status: int = Field(default=0, ge=0, le=1, description="状态(0:正常 1:停用)")
description: str | None = Field(default=None, max_length=255, description="描述")
@field_validator("status")
@classmethod
def validate_status(cls, value: int) -> int:
if value not in (0, 1):
raise ValueError("状态仅支持 0(正常) 或 1(停用)")
return value
@model_validator(mode="after")
def validate_after(self):
"""校验并规范化字典数据字段(标签/键值/类型/类型ID)。
返回:
- DictDataCreateSchema: 校验与去空格后的同一实例。
异常:
- ValueError: 必填字段为空或类型ID非法时抛出。
"""
if not self.dict_label or not self.dict_label.strip():
raise ValueError("字典标签不能为空")
if not self.dict_value or not self.dict_value.strip():
raise ValueError("字典键值不能为空")
if not self.dict_type or not self.dict_type.strip():
raise ValueError("字典类型不能为空")
if not hasattr(self, "dict_type_id") or self.dict_type_id <= 0:
raise ValueError("字典类型ID不能为空且必须大于0")
# 确保字符串字段被正确处理
self.dict_label = self.dict_label.strip()
self.dict_value = self.dict_value.strip()
self.dict_type = self.dict_type.strip()
return self
class DictDataUpdateSchema(DictDataCreateSchema):
"""字典数据更新模型"""
class DictDataOutSchema(DictDataCreateSchema, BaseSchema):
"""字典数据响应模型"""
model_config = ConfigDict(from_attributes=True)
class DictDataQueryParam(BaseQueryParam):
"""字典数据查询参数"""
dict_label: str | None = Field(default=None, description="字典标签", max_length=255, json_schema_extra={"q": "like"})
dict_type: str | None = Field(default=None, description="字典类型", max_length=255, json_schema_extra={"q": "eq"})
dict_type_id: int | None = Field(default=None, description="字典类型ID", json_schema_extra={"q": "eq"})
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:启动 1:停用)", json_schema_extra={"q": "eq"})
@@ -0,0 +1,566 @@
import json
from typing import Any
from redis.asyncio.client import Redis
from sqlalchemy import update as sa_update
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_system.dict.model import DictDataModel
from app.common.enums import RedisInitKeyConfig
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.database import async_db_session
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.core.redis_crud import RedisCURD
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from .crud import DictDataCRUD, DictTypeCRUD
from .schema import (
DictDataCreateSchema,
DictDataOutSchema,
DictDataQueryParam,
DictDataUpdateSchema,
DictTypeCreateSchema,
DictTypeOutSchema,
DictTypeQueryParam,
DictTypeUpdateSchema,
)
class DictTypeService:
"""字典类型管理服务
设计:实例方法承载「当前用户上下文 (auth)」,``redis`` 仍是方法参数
(因为不是每个端点都用到)。调用方写法由 ``XxxService.method_service(auth=...)``
改为 ``XxxService(auth).method(...)``。
"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def detail(self, id: int) -> DictTypeOutSchema:
"""获取数据字典类型详情
参数:
- id (int): 数据字典类型ID
返回:
- DictTypeOutSchema: 字典类型响应模型
"""
obj = await DictTypeCRUD(self.auth, self.db).get_or_404(id=id)
return DictTypeOutSchema.model_validate(obj)
async def get_list(
self,
search: DictTypeQueryParam | None = None,
order_by: list[dict] | None = None,
) -> list[DictTypeOutSchema]:
"""获取数据字典类型列表
参数:
- search (DictTypeQueryParam | None): 搜索条件模型
- order_by (list[dict] | None): 排序字段列表
返回:
- list[DictTypeOutSchema]: 字典类型响应模型列表
"""
obj_list = await DictTypeCRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by)
return [DictTypeOutSchema.model_validate(obj) for obj in obj_list]
@staticmethod
def export_list(dict_type_list: list[dict[str, Any]]) -> bytes:
"""导出字典类型列表为 Excel。"""
if not dict_type_list:
raise CustomException(msg="没有数据可导出")
mapping_dict = {
"id": "字典类型ID",
"dict_name": "字典名称",
"dict_type": "字典类型编码",
"status": "状态",
"description": "描述",
"created_time": "创建时间",
"updated_time": "更新时间",
}
data = dict_type_list.copy()
for item in data:
item["status"] = "正常" if item.get("status") == 0 else "停用"
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
async def page(
self,
page_no: int,
page_size: int,
search: DictTypeQueryParam | None = None,
order_by: list[dict] | None = None,
) -> PageResultSchema[DictTypeOutSchema]:
"""分页查询字典类型(数据库 OFFSET/LIMIT)。
参数:
- page_no (int): 页码(从 1 开始)
- page_size (int): 每页条数
- search (DictTypeQueryParam | None): 查询条件
- order_by (list[dict] | None): 排序字段列表
返回:
- PageResultSchema[DictTypeOutSchema]: 分页结果
"""
offset = (page_no - 1) * page_size
return await DictTypeCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search),
out_schema=DictTypeOutSchema,
)
async def create(self, redis: Redis, data: DictTypeCreateSchema) -> DictTypeOutSchema:
"""创建数据字典类型
参数:
- redis (Redis): Redis客户端
- data (DictTypeCreateSchema): 数据字典类型创建模型
返回:
- DictTypeOutSchema: 字典类型响应模型
"""
exist_obj = await DictTypeCRUD(self.auth, self.db).get(dict_name=data.dict_name)
if exist_obj:
raise CustomException(msg="创建失败,该数据已存在")
obj = await DictTypeCRUD(self.auth, self.db).create(data=data)
new_obj_dict = DictTypeOutSchema.model_validate(obj)
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{data.dict_type}"
try:
await RedisCURD(redis).set(
key=redis_key,
value="[]",
expire=None,
)
logger.info(f"创建字典类型成功: {new_obj_dict}")
except Exception as e:
logger.error(f"创建字典类型失败: {e}")
raise CustomException(msg="同步字典类型缓存失败") from e
return new_obj_dict
async def update(
self,
redis: Redis,
id: int,
data: DictTypeUpdateSchema,
) -> DictTypeOutSchema:
"""更新数据字典类型
参数:
- redis (Redis): Redis客户端
- id (int): 数据字典类型ID
- data (DictTypeUpdateSchema): 数据字典类型更新模型
返回:
- DictTypeOutSchema: 字典类型响应模型
"""
exist_obj = await DictTypeCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在")
if exist_obj.dict_name != data.dict_name:
raise CustomException(msg="更新失败,数据字典类型名称不可以修改")
# 如果字典类型修改或状态变更,则修改对应字典数据的类型和状态
if exist_obj.dict_type != data.dict_type or exist_obj.status != data.status:
# 批量更新所有关联字典数据
update_data: dict[str, Any] = {"status": data.status}
if exist_obj.dict_type != data.dict_type:
update_data["dict_type"] = data.dict_type
stmt = (
sa_update(DictDataModel)
.where(DictDataModel.dict_type == exist_obj.dict_type)
.values(**update_data)
)
await self.db.execute(stmt)
await self.db.flush()
obj = await DictTypeCRUD(self.auth, self.db).update(id=id, data=data)
new_obj_dict = DictTypeOutSchema.model_validate(obj)
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{data.dict_type}"
try:
# 获取当前字典类型的所有字典数据,确保包含最新状态
dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": data.dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
expire=None,
)
logger.info(f"更新字典类型成功并刷新缓存: {new_obj_dict}")
except Exception as e:
logger.error(f"更新字典类型缓存失败: {e}")
raise CustomException(msg="同步字典类型缓存失败") from e
return new_obj_dict
async def delete(self, redis: Redis, ids: list[int]) -> None:
"""删除数据字典类型
参数:
- redis (Redis): Redis客户端
- ids (list[int]): 数据字典类型ID列表
返回:
- None
"""
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await DictTypeCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
existing_map = {obj.id: obj for obj in existing}
# 收集所有 dict_type 去重后批量查询是否有字典数据
dict_types = {obj.dict_type for obj in existing if obj.id in ids}
dict_type_has_data: set[str] = set()
for dt in dict_types:
if await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": dt}):
dict_type_has_data.add(dt)
for nid in ids:
if nid not in existing_map:
raise CustomException(msg="删除失败,该数据不存在")
exist_obj = existing_map[nid]
if exist_obj.dict_type in dict_type_has_data:
raise CustomException(msg="删除失败,该数据字典类型下存在字典数据")
# 验证通过后统一删除 Redis 缓存
existing_dict_types = {obj.dict_type for obj in existing if obj.id in ids}
for dt in existing_dict_types:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{dt}"
try:
await RedisCURD(redis).delete(redis_key)
logger.info(f"删除字典类型缓存: {dt}")
except Exception as e:
logger.error(f"删除字典类型缓存失败: {e}")
raise CustomException(msg="同步删除字典缓存失败") from e
await DictTypeCRUD(self.auth, self.db).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
"""设置数据字典类型状态
参数:
- data (BatchSetAvailable): 批量设置状态模型
返回:
- None
"""
await DictTypeCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
class DictDataService:
"""字典数据管理服务
设计同 DictTypeService:实例方法 + ``__init__(auth)``。
"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def detail(self, id: int) -> DictDataOutSchema:
"""获取数据字典数据详情
参数:
- id (int): 数据字典数据ID
返回:
- DictDataOutSchema: 字典数据响应模型
"""
obj = await DictDataCRUD(self.auth, self.db).get_or_404(id=id)
return DictDataOutSchema.model_validate(obj)
async def get_list(
self,
search: DictDataQueryParam | None = None,
order_by: list[dict] | None = None,
) -> list[DictDataOutSchema]:
"""获取数据字典数据列表
参数:
- search (DictDataQueryParam | None): 搜索条件模型
- order_by (list[dict] | None): 排序字段列表
返回:
- list[DictDataOutSchema]: 字典数据响应模型列表
"""
obj_list = await DictDataCRUD(self.auth, self.db).get_list(search=search_to_dict(search), order_by=order_by)
return [DictDataOutSchema.model_validate(obj) for obj in obj_list]
@staticmethod
def export_list(dict_data_list: list[dict[str, Any]]) -> bytes:
"""导出字典数据列表为 Excel。"""
if not dict_data_list:
raise CustomException(msg="没有数据可导出")
mapping_dict = {
"id": "字典数据ID",
"dict_type": "字典类型编码",
"dict_sort": "排序",
"dict_label": "字典标签",
"dict_value": "字典键值",
"is_default": "是否默认",
"status": "状态",
"description": "描述",
"created_time": "创建时间",
"updated_time": "更新时间",
}
data = dict_data_list.copy()
for item in data:
item["status"] = "正常" if item.get("status") == 0 else "停用"
item["is_default"] = "" if item.get("is_default") else ""
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
async def page(
self,
page_no: int,
page_size: int,
search: DictDataQueryParam | None = None,
order_by: list[dict] | None = None,
) -> PageResultSchema[DictDataOutSchema]:
"""分页查询字典数据(数据库 OFFSET/LIMIT)。
参数:
- page_no (int): 页码(从 1 开始)
- page_size (int): 每页条数
- search (DictDataQueryParam | None): 查询条件
- order_by (list[dict] | None): 排序字段列表
返回:
- PageResultSchema[DictDataOutSchema]: 分页结果
"""
offset = (page_no - 1) * page_size
return await DictDataCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search),
out_schema=DictDataOutSchema,
)
@staticmethod
async def init_cache(redis: Redis) -> None:
"""应用初始化: 获取所有字典类型对应的字典数据信息并按租户缓存(无 auth)。
参数:
- redis (Redis): Redis客户端
返回:
- None
"""
try:
async with async_db_session() as session, session.begin():
init_auth = AuthSchema()
obj_list = await DictTypeCRUD(init_auth, session).get_list()
if not obj_list:
logger.warning("未找到任何字典类型数据")
return
for obj in obj_list:
dict_type = obj.dict_type
try:
dict_data_list = await DictDataCRUD(init_auth, session).get_list(search={"dict_type": dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{dict_type}"
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(
key=redis_key,
value=value,
expire=None,
)
except Exception as e:
logger.error(f"❌ 初始化字典数据失败 [{dict_type}]: {e}")
except Exception as e:
logger.error(f"❌️ 字典初始化过程发生错误: {e}")
raise CustomException(msg="字典数据初始化失败") from e
@staticmethod
async def get_init_cache(redis: Redis, dict_type: str) -> list[dict]:
"""从缓存获取字典数据列表信息(无 auth)。
参数:
- redis (Redis): Redis客户端
- dict_type (str): 字典类型
返回:
- list[dict]: 字典数据列表
"""
def _parse(data: str | list | None) -> list[dict] | None:
"""尝试反序列化 Redis 返回的字典缓存数据"""
if data is None:
return None
if isinstance(data, list):
return data
try:
return json.loads(data)
except (json.JSONDecodeError, TypeError):
return None
try:
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{dict_type}"
obj_list_dict = await RedisCURD(redis).get(redis_key)
result = _parse(obj_list_dict)
if result is not None:
return result
# 缓存未命中或格式异常,重新初始化
logger.warning(f"字典缓存未命中或格式异常,重新初始化缓存: {dict_type}")
await DictDataService.init_cache(redis)
obj_list_dict = await RedisCURD(redis).get(redis_key)
result = _parse(obj_list_dict)
if result is None:
raise CustomException(msg="该数据不存在")
return result
except CustomException:
raise
except Exception as e:
logger.error(f"获取字典缓存失败: {e!s}")
raise CustomException(msg="获取字典数据失败") from e
async def _refresh_dict_cache(self, redis: Redis, dict_type: str) -> None:
"""刷新指定字典类型的 Redis 缓存
从数据库拉取全量数据,序列化后写入 Redis。
参数:
- redis (Redis): Redis 客户端
- dict_type (str): 字典类型
"""
redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:1:{dict_type}"
dict_data_list = await DictDataCRUD(self.auth, self.db).get_list(search={"dict_type": dict_type})
dict_data = [DictDataOutSchema.model_validate(row).model_dump(mode="json") for row in dict_data_list if row]
value = json.dumps(dict_data, ensure_ascii=False)
await RedisCURD(redis).set(key=redis_key, value=value, expire=None)
async def create(self, redis: Redis, data: DictDataCreateSchema) -> DictDataOutSchema:
"""创建数据字典数据
参数:
- redis (Redis): Redis客户端
- data (DictDataCreateSchema): 数据字典数据创建模型
返回:
- DictDataOutSchema: 字典数据响应模型
"""
# 检查相同字典类型下dict_label是否已存在
exist_label_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_label=data.dict_label)
if exist_label_obj:
raise CustomException(msg=f'创建失败,该字典类型下的字典标签"{data.dict_label}"已存在')
# 检查相同字典类型下dict_value是否已存在
exist_value_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_value=data.dict_value)
if exist_value_obj:
raise CustomException(msg=f'创建失败,该字典类型下的字典键值"{data.dict_value}"已存在')
obj = await DictDataCRUD(self.auth, self.db).create(data=data)
try:
await self._refresh_dict_cache(redis, data.dict_type)
logger.info(f"创建字典数据写入缓存成功: {obj}")
except Exception as e:
logger.error(f"创建字典数据写入缓存失败: {e}")
raise CustomException(msg="同步字典数据缓存失败") from e
return DictDataOutSchema.model_validate(obj)
async def update(
self,
redis: Redis,
id: int,
data: DictDataUpdateSchema,
) -> DictDataOutSchema:
"""更新数据字典数据
参数:
- redis (Redis): Redis客户端
- id (int): 数据字典数据ID
- data (DictDataUpdateSchema): 数据字典数据更新模型
返回:
- DictDataOutSchema: 字典数据响应模型
"""
exist_obj = await DictDataCRUD(self.auth, self.db).get_or_404(id=id, msg="更新失败,该数据不存在")
# 检查相同字典类型下dict_label是否已存在(排除当前记录)
if exist_obj.dict_label != data.dict_label:
exist_label_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_label=data.dict_label)
if exist_label_obj:
raise CustomException(msg=f'更新失败,该字典类型下的字典标签"{data.dict_label}"已存在')
# 检查相同字典类型下dict_value是否已存在(排除当前记录)
if exist_obj.dict_value != data.dict_value:
exist_value_obj = await DictDataCRUD(self.auth, self.db).get(dict_type=data.dict_type, dict_value=data.dict_value)
if exist_value_obj:
raise CustomException(msg=f'更新失败,该字典类型下的字典键值"{data.dict_value}"已存在')
# 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态
if exist_obj.dict_type != data.dict_type:
dict_type = await DictTypeCRUD(self.auth, self.db).get(dict_type=exist_obj.dict_type)
if dict_type:
try:
await self._refresh_dict_cache(redis, dict_type.dict_type)
except Exception as e:
logger.error(f"刷新旧字典缓存失败: {e}")
raise CustomException(msg="同步旧字典数据缓存失败") from e
obj = await DictDataCRUD(self.auth, self.db).update(id=id, data=data)
# 刷新新/当前字典类型缓存(仅一次)
try:
await self._refresh_dict_cache(redis, data.dict_type)
logger.info(f"更新字典数据写入缓存成功: {obj}")
except Exception as e:
logger.error(f"更新字典数据写入缓存失败: {e}")
raise CustomException(msg="同步字典数据缓存失败") from e
return DictDataOutSchema.model_validate(obj)
async def delete(self, redis: Redis, ids: list[int]) -> None:
"""删除数据字典数据
参数:
- redis (Redis): Redis客户端
- ids (list[int]): 数据字典数据ID列表
返回:
- None
"""
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
existing = await DictDataCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
existing_map = {obj.id: obj for obj in existing}
for nid in ids:
if nid not in existing_map:
raise CustomException(msg="删除失败,该数据不存在")
# 按 dict_type 分组,每组的缓存只刷新一次
type_groups: dict[str, list[int]] = {}
for obj in existing:
if obj.id in ids:
type_groups.setdefault(obj.dict_type, []).append(obj.id)
for dt in type_groups:
try:
await self._refresh_dict_cache(redis, dt)
logger.info(f"删除字典数据并刷新缓存: dict_type={dt}")
except Exception as e:
logger.error(f"删除字典数据刷新缓存失败: {e}")
raise CustomException(msg="同步删除字典数据缓存失败") from e
await DictDataCRUD(self.auth, self.db).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
"""设置数据字典数据状态
参数:
- data (BatchSetAvailable): 批量设置状态模型
返回:
- None
"""
await DictDataCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)