diff --git a/app/api/v1/module_energy/__init__.py b/app/api/v1/module_energy/__init__.py index c792d77..f851b6d 100644 --- a/app/api/v1/module_energy/__init__.py +++ b/app/api/v1/module_energy/__init__.py @@ -1,8 +1,13 @@ from fastapi import APIRouter +from app.api.v1.module_energy.chapter.controller import ChapterRouter +from app.api.v1.module_energy.config.controller import TemplateConfigRouter from app.api.v1.module_energy.report_template.controller import EnergyReportTemplateRouter +from app.api.v1.module_energy.variable.controller import VariableRouter energy_router = APIRouter() energy_router.include_router(EnergyReportTemplateRouter) - +energy_router.include_router(TemplateConfigRouter) +energy_router.include_router(ChapterRouter) +energy_router.include_router(VariableRouter) diff --git a/app/api/v1/module_energy/chapter/__init__.py b/app/api/v1/module_energy/chapter/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/api/v1/module_energy/chapter/__init__.py @@ -0,0 +1 @@ + diff --git a/app/api/v1/module_energy/chapter/controller.py b/app/api/v1/module_energy/chapter/controller.py new file mode 100644 index 0000000..b8839ef --- /dev/null +++ b/app/api/v1/module_energy/chapter/controller.py @@ -0,0 +1,101 @@ +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path, Query +from fastapi.responses import JSONResponse + +from app.common.response import ResponseSchema, SuccessResponse +from app.core.base_params import PaginationQueryParam +from app.core.base_schema import AuthSchema, PageResultSchema +from app.core.dependencies import AuthPermission +from app.core.router_class import OperationLogRoute + +from .schema import ChapterCreateSchema, ChapterOutSchema, ChapterQueryParam, ChapterUpdateSchema +from .service import ChapterService + +ChapterRouter = APIRouter(route_class=OperationLogRoute, prefix="/chapter", tags=["能源报告章节"]) + + +@ChapterRouter.get( + "/list", + summary="查询章节列表", + response_model=ResponseSchema[PageResultSchema[ChapterOutSchema]], +) +async def get_chapter_list_controller( + page: Annotated[PaginationQueryParam, Depends()], + search: Annotated[ChapterQueryParam, Depends()], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:query"]))], +) -> JSONResponse: + result = await ChapterService(auth).page( + page_no=page.page_no, + page_size=page.page_size, + search=search, + order_by=page.order_by or [{"chapter_order": "asc"}, {"id": "asc"}], + ) + return SuccessResponse(data=result, msg="查询章节列表成功") + + +@ChapterRouter.get( + "/list_by_template", + summary="查询模板下全部章节(不分页)", + response_model=ResponseSchema[list[ChapterOutSchema]], +) +async def get_chapter_list_by_template_controller( + template_id: Annotated[int, Query(ge=1, description="模板ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:query"]))], +) -> JSONResponse: + result = await ChapterService(auth).list_by_template(template_id=template_id) + return SuccessResponse(data=result, msg="查询模板章节成功") + + +@ChapterRouter.get( + "/detail/{id}", + summary="查询章节详情", + response_model=ResponseSchema[ChapterOutSchema], +) +async def get_chapter_detail_controller( + id: Annotated[int, Path(description="章节ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:detail"]))], +) -> JSONResponse: + result = await ChapterService(auth).detail(id=id) + return SuccessResponse(data=result, msg="查询章节详情成功") + + +@ChapterRouter.post( + "/create", + summary="创建章节", + response_model=ResponseSchema[ChapterOutSchema], +) +async def create_chapter_controller( + data: ChapterCreateSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:create"]))], +) -> JSONResponse: + result = await ChapterService(auth).create(data=data) + return SuccessResponse(data=result, msg="创建章节成功") + + +@ChapterRouter.put( + "/update/{id}", + summary="修改章节", + response_model=ResponseSchema[ChapterOutSchema], +) +async def update_chapter_controller( + data: ChapterUpdateSchema, + id: Annotated[int, Path(description="章节ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:update"]))], +) -> JSONResponse: + result = await ChapterService(auth).update(id=id, data=data) + return SuccessResponse(data=result, msg="修改章节成功") + + +@ChapterRouter.delete( + "/delete", + summary="删除章节", + response_model=ResponseSchema[None], +) +async def delete_chapter_controller( + ids: Annotated[list[int], Body(description="ID列表")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:chapter:delete"]))], +) -> JSONResponse: + await ChapterService(auth).delete(ids=ids) + return SuccessResponse(msg="删除章节成功") + diff --git a/app/api/v1/module_energy/chapter/crud.py b/app/api/v1/module_energy/chapter/crud.py new file mode 100644 index 0000000..ee15465 --- /dev/null +++ b/app/api/v1/module_energy/chapter/crud.py @@ -0,0 +1,13 @@ +from app.core.base_crud import CRUDBase +from app.core.base_schema import AuthSchema + +from .model import EnergyReportChapterModel +from .schema import ChapterCreateSchema, ChapterUpdateSchema + + +class ChapterCRUD(CRUDBase[EnergyReportChapterModel, ChapterCreateSchema, ChapterUpdateSchema]): + """能源报告章节数据层。""" + + def __init__(self, auth: AuthSchema) -> None: + super().__init__(model=EnergyReportChapterModel, auth=auth) + diff --git a/app/api/v1/module_energy/chapter/model.py b/app/api/v1/module_energy/chapter/model.py new file mode 100644 index 0000000..5670eb3 --- /dev/null +++ b/app/api/v1/module_energy/chapter/model.py @@ -0,0 +1,40 @@ +from sqlalchemy import ForeignKey, Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.base_model import ModelMixin, TenantMixin, UserMixin + + +class EnergyReportChapterModel(ModelMixin, TenantMixin, UserMixin): + """能源报告章节模型。""" + + __tablename__: str = "energy_report_chapter" + __table_args__: dict[str, str] = {"comment": "能源报告章节标注表"} + __loader_options__: list[str] = [ + "template", + "rule", + "created_by", + "updated_by", + "deleted_by", + "tenant_by", + ] + + template_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("energy_report_template.id", ondelete="CASCADE", onupdate="CASCADE"), + nullable=False, + index=True, + comment="能源报告模板ID", + ) + chapter_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="章节名称", index=True) + chapter_order: Mapped[int] = mapped_column(Integer, default=999, nullable=False, comment="章节排序") + rule_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("rule_list.id", ondelete="RESTRICT", onupdate="CASCADE"), + nullable=False, + index=True, + comment="关联规则ID", + ) + description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="描述") + + template = relationship("EnergyReportTemplateModel", lazy="selectin", foreign_keys=[template_id], uselist=False) + rule = relationship("RuleModel", lazy="selectin", foreign_keys=[rule_id], uselist=False) diff --git a/app/api/v1/module_energy/chapter/schema.py b/app/api/v1/module_energy/chapter/schema.py new file mode 100644 index 0000000..04c15d9 --- /dev/null +++ b/app/api/v1/module_energy/chapter/schema.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass + +from fastapi import Query +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from app.common.enums import QueueEnum +from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam +from app.core.base_schema import BaseSchema, CommonSchema, TenantBySchema, UserBySchema + + +class ChapterCreateSchema(BaseModel): + """章节创建模型。""" + + template_id: int = Field(..., ge=1, description="能源报告模板ID") + chapter_name: str = Field(..., min_length=1, max_length=128, description="章节名称") + chapter_order: int = Field(default=999, ge=0, description="章节排序") + rule_id: int = Field(..., ge=1, description="关联规则ID") + description: str | None = Field(default=None, max_length=500, description="描述") + + @field_validator("chapter_name") + @classmethod + def validate_chapter_name(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("章节名称不能为空") + return value + + +class ChapterUpdateSchema(BaseModel): + """章节更新模型。""" + + chapter_name: str = Field(..., min_length=1, max_length=128, description="章节名称") + chapter_order: int = Field(default=999, ge=0, description="章节排序") + rule_id: int = Field(..., ge=1, description="关联规则ID") + description: str | None = Field(default=None, max_length=500, description="描述") + + @field_validator("chapter_name") + @classmethod + def validate_chapter_name(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("章节名称不能为空") + return value + + +class ChapterOutSchema(ChapterCreateSchema, BaseSchema, UserBySchema, TenantBySchema): + """章节响应模型。""" + + model_config = ConfigDict(from_attributes=True) + + rule_name: str | None = Field(default=None, description="关联规则名称") + + +@dataclass +class ChapterQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam): + """章节查询参数。""" + + template_id: int | None = Query(None, ge=1, description="模板ID") + chapter_name: str | None = Query(None, description="章节名称") + rule_id: int | None = Query(None, ge=1, description="规则ID") + + def __post_init__(self) -> None: + if isinstance(self.template_id, int): + self.template_id = (QueueEnum.eq.value, self.template_id) + if self.chapter_name: + self.chapter_name = (QueueEnum.like.value, self.chapter_name) + if isinstance(self.rule_id, int): + self.rule_id = (QueueEnum.eq.value, self.rule_id) + diff --git a/app/api/v1/module_energy/chapter/service.py b/app/api/v1/module_energy/chapter/service.py new file mode 100644 index 0000000..6a34961 --- /dev/null +++ b/app/api/v1/module_energy/chapter/service.py @@ -0,0 +1,76 @@ +from app.api.v1.module_rule.rule.crud import RuleCRUD +from app.core.base_schema import AuthSchema +from app.core.exceptions import CustomException + +from .crud import ChapterCRUD +from .schema import ChapterOutSchema, ChapterQueryParam, ChapterCreateSchema, ChapterUpdateSchema + + +class ChapterService: + """能源报告章节服务。""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def detail(self, id: int) -> ChapterOutSchema: + chapter = await ChapterCRUD(self.auth).get_or_404(id=id) + return self._to_out_schema(chapter) + + async def list_by_template(self, template_id: int) -> list[ChapterOutSchema]: + chapters = await ChapterCRUD(self.auth).get_list( + search={"template_id": template_id}, + order_by=[{"chapter_order": "asc"}, {"id": "asc"}], + ) + return [self._to_out_schema(chapter) for chapter in chapters] + + async def page( + self, + page_no: int, + page_size: int, + search: ChapterQueryParam | None = None, + order_by: list[dict[str, str]] | None = None, + ) -> dict: + offset = (page_no - 1) * page_size + page_result = await ChapterCRUD(self.auth).page( + offset=offset, + limit=page_size, + order_by=order_by or [{"chapter_order": "asc"}, {"id": "asc"}], + search=vars(search) if search else None, + out_schema=ChapterOutSchema, + ) + for item in page_result.items: + rule_info = item.get("rule") or {} + item["rule_name"] = rule_info.get("name") + return page_result + + async def create(self, data: ChapterCreateSchema) -> ChapterOutSchema: + await self._validate_rule(rule_id=data.rule_id) + chapter = await ChapterCRUD(self.auth).create(data=data) + return self._to_out_schema(chapter) + + async def update(self, id: int, data: ChapterUpdateSchema) -> ChapterOutSchema: + await ChapterCRUD(self.auth).get_or_404(id=id, msg="更新失败,章节标注不存在") + await self._validate_rule(rule_id=data.rule_id) + chapter = await ChapterCRUD(self.auth).update(id=id, data=data) + return self._to_out_schema(chapter) + + async def delete(self, ids: list[int]) -> None: + if len(ids) < 1: + raise CustomException(msg="删除失败,删除对象不能为空") + chapters = await ChapterCRUD(self.auth).get_list(search={"id": ("in", ids)}) + chapter_map = {chapter.id: chapter for chapter in chapters} + for chapter_id in ids: + if chapter_id not in chapter_map: + raise CustomException(msg="删除失败,章节标注不存在") + await ChapterCRUD(self.auth).delete(ids=ids) + + def _to_out_schema(self, chapter) -> ChapterOutSchema: + chapter_out = ChapterOutSchema.model_validate(chapter) + if hasattr(chapter, "rule") and chapter.rule: + chapter_out.rule_name = chapter.rule.name + return chapter_out + + async def _validate_rule(self, rule_id: int) -> None: + rule = await RuleCRUD(self.auth).get(id=rule_id) + if not rule: + raise CustomException(msg="关联规则不存在") diff --git a/app/api/v1/module_energy/config/__init__.py b/app/api/v1/module_energy/config/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/api/v1/module_energy/config/__init__.py @@ -0,0 +1 @@ + diff --git a/app/api/v1/module_energy/config/controller.py b/app/api/v1/module_energy/config/controller.py new file mode 100644 index 0000000..1188ad1 --- /dev/null +++ b/app/api/v1/module_energy/config/controller.py @@ -0,0 +1,40 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query +from fastapi.responses import JSONResponse + +from app.common.response import ResponseSchema, SuccessResponse +from app.core.base_schema import AuthSchema +from app.core.dependencies import AuthPermission +from app.core.router_class import OperationLogRoute + +from .schema import TemplateConfigSaveSchema +from .service import TemplateConfigService + +TemplateConfigRouter = APIRouter(route_class=OperationLogRoute, prefix="/attachment-template/config", tags=["能源报告模板配置"]) + + +@TemplateConfigRouter.get( + "/detail", + summary="查询模板章节规则和变量关联配置", + response_model=ResponseSchema[dict], +) +async def get_template_config_detail_controller( + template_id: Annotated[int, Query(ge=1, description="模板ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:detail"]))], +) -> JSONResponse: + result = await TemplateConfigService(auth).detail(template_id=template_id) + return SuccessResponse(data=result, msg="查询模板配置成功") + + +@TemplateConfigRouter.post( + "/save", + summary="统一保存模板章节规则和变量关联配置", + response_model=ResponseSchema[dict], +) +async def save_template_config_controller( + data: TemplateConfigSaveSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:update"]))], +) -> JSONResponse: + result = await TemplateConfigService(auth).save(data=data) + return SuccessResponse(data=result, msg="保存模板配置成功") diff --git a/app/api/v1/module_energy/config/schema.py b/app/api/v1/module_energy/config/schema.py new file mode 100644 index 0000000..4de940a --- /dev/null +++ b/app/api/v1/module_energy/config/schema.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel, Field, field_validator + +from app.api.v1.module_energy.variable.schema import VariableItemSchema + + +class TemplateChapterRuleSchema(BaseModel): + """模板章节规则配置项。""" + + id: int | None = Field(default=None, ge=1, description="章节规则标注ID,存在则更新,不传则新增") + chapter_name: str = Field(..., min_length=1, max_length=128, description="章节名称") + chapter_order: int = Field(default=999, ge=0, description="章节排序") + rule_id: int = Field(..., ge=1, description="关联规则ID") + description: str | None = Field(default=None, max_length=500, description="描述") + + @field_validator("chapter_name") + @classmethod + def validate_chapter_name(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("章节名称不能为空") + return value + + +class TemplateConfigSaveSchema(BaseModel): + """模板章节规则和变量关联统一保存模型。""" + + template_id: int = Field(..., ge=1, description="模板ID") + chapters: list[TemplateChapterRuleSchema] = Field(default_factory=list, description="章节规则标注列表") + variables: list[VariableItemSchema] = Field(default_factory=list, description="变量关联列表") diff --git a/app/api/v1/module_energy/config/service.py b/app/api/v1/module_energy/config/service.py new file mode 100644 index 0000000..dc0c949 --- /dev/null +++ b/app/api/v1/module_energy/config/service.py @@ -0,0 +1,82 @@ +from app.api.v1.module_energy.chapter.crud import ChapterCRUD +from app.api.v1.module_energy.report_template.crud import EnergyReportTemplateCRUD +from app.api.v1.module_energy.variable.service import VariableService +from app.api.v1.module_rule.rule.crud import RuleCRUD +from app.core.base_schema import AuthSchema +from app.core.exceptions import CustomException + +from .schema import TemplateChapterRuleSchema, TemplateConfigSaveSchema + + +class TemplateConfigService: + """能源报告模板配置服务。""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def save(self, data: TemplateConfigSaveSchema) -> dict: + await self._validate_template(template_id=data.template_id) + await self._validate_rules(chapters=data.chapters) + await self._save_chapters(template_id=data.template_id, chapters=data.chapters) + variable_result = await VariableService(self.auth).save(data=data) + return { + "template_id": data.template_id, + "chapters": await self._list_chapters(template_id=data.template_id), + "variables": variable_result, + } + + async def detail(self, template_id: int) -> dict: + await self._validate_template(template_id=template_id) + return { + "template_id": template_id, + "chapters": await self._list_chapters(template_id=template_id), + "variables": await VariableService(self.auth).list_by_template(template_id=template_id), + } + + async def _save_chapters(self, template_id: int, chapters: list[TemplateChapterRuleSchema]) -> None: + for chapter in chapters: + chapter_data = chapter.model_dump(exclude={"id"}) + chapter_data["template_id"] = template_id + if chapter.id: + old_chapter = await ChapterCRUD(self.auth).get(id=chapter.id, template_id=template_id) + if not old_chapter: + raise CustomException(msg=f"章节规则标注不存在或不属于当前模板: {chapter.id}") + await ChapterCRUD(self.auth).update(id=chapter.id, data=chapter_data) + else: + await ChapterCRUD(self.auth).create(data=chapter_data) + + async def _validate_template(self, template_id: int) -> None: + template = await EnergyReportTemplateCRUD(self.auth).get(id=template_id) + if not template: + raise CustomException(msg="能源报告模板不存在") + + async def _validate_rules(self, chapters: list[TemplateChapterRuleSchema]) -> None: + rule_ids = list(dict.fromkeys(chapter.rule_id for chapter in chapters)) + if not rule_ids: + return + rules = await RuleCRUD(self.auth).get_list(search={"id": ("in", rule_ids)}) + exists_ids = {rule.id for rule in rules} + invalid_ids = [rule_id for rule_id in rule_ids if rule_id not in exists_ids] + if invalid_ids: + raise CustomException(msg=f"关联规则不存在: {invalid_ids}") + + async def _list_chapters(self, template_id: int) -> list[dict]: + chapters = await ChapterCRUD(self.auth).get_list( + search={"template_id": template_id}, + order_by=[{"chapter_order": "asc"}, {"id": "asc"}], + ) + result: list[dict] = [] + for chapter in chapters: + rule_name = chapter.rule.name if hasattr(chapter, "rule") and chapter.rule else None + result.append( + { + "id": chapter.id, + "template_id": chapter.template_id, + "chapter_name": chapter.chapter_name, + "chapter_order": chapter.chapter_order, + "rule_id": chapter.rule_id, + "rule_name": rule_name, + "description": chapter.description, + } + ) + return result diff --git a/app/api/v1/module_energy/report_template/schema.py b/app/api/v1/module_energy/report_template/schema.py index 5ba9434..09bbceb 100644 --- a/app/api/v1/module_energy/report_template/schema.py +++ b/app/api/v1/module_energy/report_template/schema.py @@ -13,14 +13,14 @@ class EnergyReportTemplateCreateSchema(BaseModel): """能源报告模板创建模型。""" template_name: str = Field(..., min_length=1, max_length=128, description="模板名称") - template_type: str = Field(..., min_length=1, max_length=64, description="模板类型") + template_type: str = Field(default="", max_length=64, description="模板类型") version: str | None = Field(default=None, max_length=32, description="模板版本") file_id: int = Field(..., ge=1, description="文件记录ID") status: int = Field(default=0, ge=0, le=1, description="状态 0:启用 1:停用") order: int = Field(default=999, ge=0, description="显示排序") description: str | None = Field(default=None, max_length=500, description="描述") - @field_validator("template_name", "template_type") + @field_validator("template_name") @classmethod def validate_required_text(cls, value: str) -> str: value = value.strip() diff --git a/app/api/v1/module_energy/variable/__init__.py b/app/api/v1/module_energy/variable/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/api/v1/module_energy/variable/__init__.py @@ -0,0 +1 @@ + diff --git a/app/api/v1/module_energy/variable/controller.py b/app/api/v1/module_energy/variable/controller.py new file mode 100644 index 0000000..bde1595 --- /dev/null +++ b/app/api/v1/module_energy/variable/controller.py @@ -0,0 +1,67 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Path, Query +from fastapi.responses import JSONResponse + +from app.common.response import ResponseSchema, SuccessResponse +from app.core.base_schema import AuthSchema +from app.core.dependencies import AuthPermission +from app.core.router_class import OperationLogRoute + +from .schema import VariableSaveSchema +from .service import VariableService + +VariableRouter = APIRouter(route_class=OperationLogRoute, prefix="/variable", tags=["能源报告变量关联"]) + + +@VariableRouter.get( + "/list", + summary="查询模板的变量-章节关联列表", + response_model=ResponseSchema[list[dict]], +) +async def get_variable_list_controller( + template_id: Annotated[int, Query(ge=1, description="模板ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:variable:query"]))], +) -> JSONResponse: + result = await VariableService(auth).list_by_template(template_id=template_id) + return SuccessResponse(data=result, msg="查询变量关联列表成功") + + +@VariableRouter.get( + "/chapter/{chapter_id}", + summary="查询章节标注详情和变量勾选状态", + response_model=ResponseSchema[dict], +) +async def get_variable_list_by_chapter_controller( + chapter_id: Annotated[int, Path(ge=1, description="章节标注ID")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:variable:query"]))], +) -> JSONResponse: + result = await VariableService(auth).list_by_chapter(chapter_id=chapter_id) + return SuccessResponse(data=result, msg="查询章节标注变量配置成功") + + +@VariableRouter.post( + "/save", + summary="批量保存变量-章节关联", + response_model=ResponseSchema[list[dict]], +) +async def save_variable_controller( + data: VariableSaveSchema, + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:variable:save"]))], +) -> JSONResponse: + result = await VariableService(auth).save(data=data) + return SuccessResponse(data=result, msg="保存变量关联成功") + + +@VariableRouter.delete( + "/delete", + summary="删除某个变量的全部关联", + response_model=ResponseSchema[None], +) +async def delete_variable_controller( + template_id: Annotated[int, Query(ge=1, description="模板ID")], + variable_name: Annotated[str, Query(description="变量名称")], + auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:variable:delete"]))], +) -> JSONResponse: + await VariableService(auth).delete_by_variable(template_id=template_id, variable_name=variable_name) + return SuccessResponse(msg="删除变量关联成功") diff --git a/app/api/v1/module_energy/variable/crud.py b/app/api/v1/module_energy/variable/crud.py new file mode 100644 index 0000000..42696eb --- /dev/null +++ b/app/api/v1/module_energy/variable/crud.py @@ -0,0 +1,25 @@ +from sqlalchemy import delete + +from app.core.base_crud import CRUDBase +from app.core.base_schema import AuthSchema +from app.core.exceptions import CustomException + +from .model import EnergyReportVariableModel + + +class VariableCRUD(CRUDBase[EnergyReportVariableModel, dict, dict]): + """能源报告变量关联数据层。""" + + def __init__(self, auth: AuthSchema) -> None: + super().__init__(model=EnergyReportVariableModel, auth=auth) + + async def hard_delete(self, ids: list[int]) -> None: + try: + pk = self._get_pk_col() + sql = self._tenant_dml_where(delete(self.model).where(pk.in_(ids))) + await self.db.execute(sql) + await self.db.flush() + except CustomException: + raise + except Exception as e: + raise CustomException(msg=f"删除变量关联失败: {e!s}") diff --git a/app/api/v1/module_energy/variable/model.py b/app/api/v1/module_energy/variable/model.py new file mode 100644 index 0000000..300489b --- /dev/null +++ b/app/api/v1/module_energy/variable/model.py @@ -0,0 +1,30 @@ +from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.base_model import ModelMixin, TenantMixin, UserMixin + + +class EnergyReportVariableModel(ModelMixin, TenantMixin, UserMixin): + """能源报告变量-章节关联模型。""" + + __tablename__: str = "energy_report_variable" + __table_args__ = (UniqueConstraint("tenant_id", "template_id", "variable_name", "chapter_name"), {"comment": "能源报告变量章节关联表"}) + __loader_options__: list[str] = [ + "template", + "created_by", + "updated_by", + "deleted_by", + "tenant_by", + ] + + template_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("energy_report_template.id", ondelete="CASCADE", onupdate="CASCADE"), + nullable=False, + index=True, + comment="能源报告模板ID", + ) + variable_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="变量名称") + chapter_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="章节名称", index=True) + + template = relationship("EnergyReportTemplateModel", lazy="selectin", foreign_keys=[template_id], uselist=False) diff --git a/app/api/v1/module_energy/variable/schema.py b/app/api/v1/module_energy/variable/schema.py new file mode 100644 index 0000000..814e997 --- /dev/null +++ b/app/api/v1/module_energy/variable/schema.py @@ -0,0 +1,53 @@ +from pydantic import BaseModel, ConfigDict, Field, field_validator + + +class VariableItemSchema(BaseModel): + """单个变量-章节关联项。""" + + variable_name: str = Field(..., min_length=1, max_length=128, description="变量名称") + chapter_names: list[str] = Field(default_factory=list, description="关联章节名称列表") + + @field_validator("variable_name") + @classmethod + def validate_required(cls, value: str) -> str: + value = value.strip() + if not value: + raise ValueError("必填字段不能为空") + return value + + @field_validator("chapter_names") + @classmethod + def validate_chapter_names(cls, values: list[str]) -> list[str]: + result: list[str] = [] + for value in values: + chapter_name = value.strip() + if not chapter_name: + raise ValueError("章节名称不能为空") + result.append(chapter_name) + return result + + +class VariableSaveSchema(BaseModel): + """批量保存变量-章节关联。""" + + template_id: int = Field(..., ge=1, description="模板ID") + variables: list[VariableItemSchema] = Field(default_factory=list, description="变量列表") + + +class VariableOutSchema(BaseModel): + """变量-章节关联响应模型。""" + + model_config = ConfigDict(from_attributes=True) + + id: int = Field(description="主键ID") + template_id: int = Field(description="模板ID") + variable_name: str = Field(description="变量名称") + chapter_name: str = Field(description="章节名称") + + +class VariableGroupOutSchema(BaseModel): + """按变量分组的响应模型。""" + + variable_name: str = Field(description="变量名称") + chapter_names: list[str] = Field(default_factory=list, description="关联章节名称列表") + chapters: list[dict] = Field(default_factory=list, description="关联章节详情列表") diff --git a/app/api/v1/module_energy/variable/service.py b/app/api/v1/module_energy/variable/service.py new file mode 100644 index 0000000..0564068 --- /dev/null +++ b/app/api/v1/module_energy/variable/service.py @@ -0,0 +1,135 @@ +from app.api.v1.module_energy.chapter.crud import ChapterCRUD +from app.core.base_schema import AuthSchema +from app.core.exceptions import CustomException + +from .crud import VariableCRUD +from .schema import VariableGroupOutSchema, VariableSaveSchema + + +class VariableService: + """能源报告变量关联服务。""" + + def __init__(self, auth: AuthSchema) -> None: + self.auth = auth + + async def list_by_template(self, template_id: int) -> list[dict]: + records = await VariableCRUD(self.auth).get_list( + search={"template_id": template_id}, + order_by=[{"variable_name": "asc"}, {"id": "asc"}], + ) + chapter_map = await self._chapter_summary_map(template_id=template_id) + group_map: dict[str, VariableGroupOutSchema] = {} + for record in records: + key = record.variable_name + if key not in group_map: + group_map[key] = VariableGroupOutSchema( + variable_name=record.variable_name, + chapter_names=[], + chapters=[], + ) + group_map[key].chapter_names.append(record.chapter_name) + group_map[key].chapters.append( + chapter_map.get(record.chapter_name, {"chapter_name": record.chapter_name}) + ) + return [group.model_dump() for group in group_map.values()] + + async def list_by_chapter(self, chapter_id: int) -> dict: + chapter = await ChapterCRUD(self.auth).get_or_404(id=chapter_id, msg="章节标注不存在") + records = await VariableCRUD(self.auth).get_list( + search={"template_id": chapter.template_id}, + order_by=[{"variable_name": "asc"}, {"id": "asc"}], + ) + rule_name = chapter.rule.name if hasattr(chapter, "rule") and chapter.rule else None + variable_map: dict[str, dict] = {} + for record in records: + if record.variable_name not in variable_map: + variable_map[record.variable_name] = { + "variable_name": record.variable_name, + "chapter_names": [], + "checked": False, + } + variable_map[record.variable_name]["chapter_names"].append(record.chapter_name) + + for variable in variable_map.values(): + variable["chapter_names"] = self._unique_names(variable["chapter_names"]) + variable["checked"] = chapter.chapter_name in variable["chapter_names"] + + return { + "chapter_annotation": { + "id": chapter.id, + "chapter_name": chapter.chapter_name, + "rule_id": chapter.rule_id, + "rule_name": rule_name, + }, + "variables": list(variable_map.values()), + } + + async def save(self, data: VariableSaveSchema) -> list[dict]: + chapter_names = self._collect_chapter_names(data) + await self._validate_chapters(template_id=data.template_id, chapter_names=chapter_names) + + existing = await VariableCRUD(self.auth).get_list(search={"template_id": data.template_id}) + if existing: + await VariableCRUD(self.auth).hard_delete(ids=[record.id for record in existing]) + + for variable in data.variables: + for chapter_name in self._unique_names(variable.chapter_names): + await VariableCRUD(self.auth).create( + data={ + "template_id": data.template_id, + "variable_name": variable.variable_name, + "chapter_name": chapter_name, + } + ) + + return await self.list_by_template(template_id=data.template_id) + + async def delete_by_variable(self, template_id: int, variable_name: str) -> None: + records = await VariableCRUD(self.auth).get_list( + search={"template_id": template_id, "variable_name": variable_name}, + ) + if not records: + raise CustomException(msg="变量关联不存在") + await VariableCRUD(self.auth).hard_delete(ids=[record.id for record in records]) + + def _collect_chapter_names(self, data: VariableSaveSchema) -> list[str]: + chapter_names: list[str] = [] + for variable in data.variables: + chapter_names.extend(variable.chapter_names) + return self._unique_names(chapter_names) + + def _unique_names(self, names: list[str]) -> list[str]: + return list(dict.fromkeys(name.strip() for name in names if name.strip())) + + async def _validate_chapters(self, template_id: int, chapter_names: list[str]) -> None: + if not chapter_names: + return + chapters = await ChapterCRUD(self.auth).get_list(search={"template_id": template_id}) + exists_names = {chapter.chapter_name for chapter in chapters} + invalid_names = [chapter_name for chapter_name in chapter_names if chapter_name not in exists_names] + if invalid_names: + raise CustomException(msg=f"章节不存在或不属于当前模板: {invalid_names}") + + async def _chapter_summary_map(self, template_id: int) -> dict[str, dict]: + chapters = await ChapterCRUD(self.auth).get_list( + search={"template_id": template_id}, + order_by=[{"chapter_order": "asc"}, {"id": "asc"}], + ) + result: dict[str, dict] = {} + for chapter in chapters: + if chapter.chapter_name not in result: + result[chapter.chapter_name] = { + "chapter_name": chapter.chapter_name, + "annotation_ids": [], + "rules": [], + } + rule_name = chapter.rule.name if hasattr(chapter, "rule") and chapter.rule else None + result[chapter.chapter_name]["annotation_ids"].append(chapter.id) + result[chapter.chapter_name]["rules"].append( + { + "chapter_annotation_id": chapter.id, + "rule_id": chapter.rule_id, + "rule_name": rule_name, + } + ) + return result diff --git a/app/config/setting.py b/app/config/setting.py index 4331cc0..cdc0f4f 100644 --- a/app/config/setting.py +++ b/app/config/setting.py @@ -200,8 +200,13 @@ class Settings(BaseSettings): ".svg", ".xls", ".xlsx", + ".doc", + ".docx", + ".pdf", + ".txt", + ".csv", ] - MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 最大文件大小(10MB) + MAX_FILE_SIZE: int = 100 * 1024 * 1024 # 最大文件大小(100MB) # ================================================= # # ***************** MinIO对象存储配置 **************** # diff --git a/app/scripts/initialize.py b/app/scripts/initialize.py index da3456a..920d927 100644 --- a/app/scripts/initialize.py +++ b/app/scripts/initialize.py @@ -15,7 +15,9 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.api.v1.module_common.file.model import FileModel +from app.api.v1.module_energy.chapter.model import EnergyReportChapterModel from app.api.v1.module_energy.report_template.model import EnergyReportTemplateModel +from app.api.v1.module_energy.variable.model import EnergyReportVariableModel from app.api.v1.module_platform.email.model import EmailConfigModel, EmailTemplateModel from app.api.v1.module_platform.invoice.model import InvoiceModel from app.api.v1.module_platform.menu.model import MenuModel @@ -80,6 +82,8 @@ class InitializeData: TicketModel, FileModel, EnergyReportTemplateModel, + EnergyReportChapterModel, + EnergyReportVariableModel, # ── 日志表(追加写入) ── LoginLogModel, OperationLogModel,