feat:新增能源报告模板接口

master
HuangHuiKang 3 days ago
parent a1eb2315fa
commit 3f961146ed

@ -21,7 +21,7 @@ from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.utils.upload_util import UploadUtil
from .schema import FileOutSchema, FileQueryParam
from .schema import FileOutSchema, FileQueryParam, FileUploadOutSchema
from .service import FileService
FileRouter = APIRouter(route_class=OperationLogRoute, prefix="/file", tags=["文件管理"])
@ -63,7 +63,7 @@ async def get_file_detail_controller(
@FileRouter.post(
"/upload",
summary="上传文件",
response_model=ResponseSchema[FileOutSchema],
response_model=ResponseSchema[FileUploadOutSchema],
)
async def upload_controller(
file: UploadFile,
@ -86,7 +86,17 @@ async def upload_controller(
business_type=business_type,
business_id=business_id,
)
return SuccessResponse(data=result, msg="上传文件成功")
return SuccessResponse(
data=FileUploadOutSchema(
id=result.id,
original_name=result.original_name,
file_name=result.file_name,
file_ext=result.file_ext,
file_size=result.file_size,
file_url=result.file_url,
),
msg="上传文件成功",
)
@FileRouter.get(

@ -38,6 +38,19 @@ class FileOutSchema(FileCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
origin_name: str | None = Field(default=None, description="兼容旧接口的原始文件名")
class FileUploadOutSchema(BaseModel):
"""文件上传精简响应模型。"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description="文件记录ID")
original_name: str = Field(description="原始文件名")
file_name: str = Field(description="存储文件名")
file_ext: str | None = Field(default=None, description="文件扩展名")
file_size: int = Field(default=0, description="文件大小,单位字节")
file_url: str | None = Field(default=None, description="文件访问地址")
@dataclass
class FileQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
"""文件记录查询参数。"""
@ -65,4 +78,3 @@ class FileQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
self.business_type = (QueueEnum.eq.value, self.business_type)
if isinstance(self.business_id, int):
self.business_id = (QueueEnum.eq.value, self.business_id)

@ -0,0 +1,8 @@
from fastapi import APIRouter
from app.api.v1.module_energy.report_template.controller import EnergyReportTemplateRouter
energy_router = APIRouter()
energy_router.include_router(EnergyReportTemplateRouter)

@ -0,0 +1,93 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Path
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 (
EnergyReportTemplateCreateSchema,
EnergyReportTemplateOutSchema,
EnergyReportTemplateQueryParam,
EnergyReportTemplateUpdateSchema,
)
from .service import EnergyReportTemplateService
EnergyReportTemplateRouter = APIRouter(route_class=OperationLogRoute, prefix="/attachment-template", tags=["能源报告模板"])
@EnergyReportTemplateRouter.get(
"/list",
summary="查询能源报告模板列表",
response_model=ResponseSchema[PageResultSchema[EnergyReportTemplateOutSchema]],
)
async def get_energy_report_template_list_controller(
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[EnergyReportTemplateQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:query"]))],
) -> JSONResponse:
result = await EnergyReportTemplateService(auth).page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by or [{"order": "asc"}, {"id": "desc"}],
)
return SuccessResponse(data=result, msg="查询能源报告模板列表成功")
@EnergyReportTemplateRouter.get(
"/detail/{id}",
summary="查询能源报告模板详情",
response_model=ResponseSchema[EnergyReportTemplateOutSchema],
)
async def get_energy_report_template_detail_controller(
id: Annotated[int, Path(description="能源报告模板ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:detail"]))],
) -> JSONResponse:
result = await EnergyReportTemplateService(auth).detail(id=id)
return SuccessResponse(data=result, msg="查询能源报告模板详情成功")
@EnergyReportTemplateRouter.post(
"/create",
summary="创建能源报告模板",
response_model=ResponseSchema[EnergyReportTemplateOutSchema],
)
async def create_energy_report_template_controller(
data: EnergyReportTemplateCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:create"]))],
) -> JSONResponse:
result = await EnergyReportTemplateService(auth).create(data=data)
return SuccessResponse(data=result, msg="创建能源报告模板成功")
@EnergyReportTemplateRouter.put(
"/update/{id}",
summary="修改能源报告模板",
response_model=ResponseSchema[EnergyReportTemplateOutSchema],
)
async def update_energy_report_template_controller(
data: EnergyReportTemplateUpdateSchema,
id: Annotated[int, Path(description="能源报告模板ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:update"]))],
) -> JSONResponse:
result = await EnergyReportTemplateService(auth).update(id=id, data=data)
return SuccessResponse(data=result, msg="修改能源报告模板成功")
@EnergyReportTemplateRouter.delete(
"/delete",
summary="删除能源报告模板",
response_model=ResponseSchema[None],
)
async def delete_energy_report_template_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_energy:attachment_template:delete"]))],
) -> JSONResponse:
await EnergyReportTemplateService(auth).delete(ids=ids)
return SuccessResponse(msg="删除能源报告模板成功")

@ -0,0 +1,13 @@
from app.core.base_crud import CRUDBase
from app.core.base_schema import AuthSchema
from .model import EnergyReportTemplateModel
from .schema import EnergyReportTemplateCreateSchema, EnergyReportTemplateUpdateSchema
class EnergyReportTemplateCRUD(CRUDBase[EnergyReportTemplateModel, EnergyReportTemplateCreateSchema, EnergyReportTemplateUpdateSchema]):
"""能源报告模板数据层。"""
def __init__(self, auth: AuthSchema) -> None:
super().__init__(model=EnergyReportTemplateModel, auth=auth)

@ -0,0 +1,35 @@
from sqlalchemy import ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
class EnergyReportTemplateModel(ModelMixin, TenantMixin, UserMixin):
"""能源报告模板模型。"""
__tablename__: str = "energy_report_template"
__table_args__ = (UniqueConstraint("tenant_id", "template_name"), {"comment": "能源报告模板表"})
__loader_options__: list[str] = [
"file",
"created_by",
"updated_by",
"deleted_by",
"tenant_by",
]
template_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="模板名称", index=True)
template_type: Mapped[str] = mapped_column(String(64), nullable=False, comment="模板类型", index=True)
version: Mapped[str | None] = mapped_column(String(32), default=None, nullable=True, comment="模板版本")
file_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("common_file.id", ondelete="RESTRICT", onupdate="CASCADE"),
nullable=False,
index=True,
comment="文件记录ID",
)
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态 0:启用 1:停用", index=True)
order: Mapped[int] = mapped_column(Integer, default=999, nullable=False, comment="显示排序")
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="描述")
file = relationship("FileModel", lazy="selectin", foreign_keys=[file_id], uselist=False)

@ -0,0 +1,72 @@
from dataclasses import dataclass
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, field_validator
from app.api.v1.module_common.file.schema import FileOutSchema
from app.common.enums import QueueEnum
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
from app.core.base_schema import BaseSchema, TenantBySchema, UserBySchema
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="模板类型")
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")
@classmethod
def validate_required_text(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("必填字段不能为空")
return value
@field_validator("status")
@classmethod
def validate_status(cls, value: int) -> int:
if value not in {0, 1}:
raise ValueError("状态仅支持 0(启用) 或 1(停用)")
return value
class EnergyReportTemplateUpdateSchema(EnergyReportTemplateCreateSchema):
"""能源报告模板更新模型。"""
class EnergyReportTemplateOutSchema(EnergyReportTemplateCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
"""能源报告模板响应模型。"""
model_config = ConfigDict(from_attributes=True)
file: FileOutSchema | None = Field(default=None, description="文件记录")
file_url: str | None = Field(default=None, description="文件访问地址")
original_name: str | None = Field(default=None, description="原始文件名")
object_name: str | None = Field(default=None, description="MinIO对象名称")
@dataclass
class EnergyReportTemplateQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
"""能源报告模板查询参数。"""
template_name: str | None = Query(None, description="模板名称")
template_type: str | None = Query(None, description="模板类型")
version: str | None = Query(None, description="模板版本")
status: int | None = Query(None, ge=0, le=1, description="状态 0:启用 1:停用")
def __post_init__(self) -> None:
if self.template_name:
self.template_name = (QueueEnum.like.value, self.template_name)
if self.template_type:
self.template_type = (QueueEnum.eq.value, self.template_type)
if self.version:
self.version = (QueueEnum.like.value, self.version)
if isinstance(self.status, int):
self.status = (QueueEnum.eq.value, self.status)

@ -0,0 +1,112 @@
from app.api.v1.module_common.file.crud import FileCRUD
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from .crud import EnergyReportTemplateCRUD
from .schema import (
EnergyReportTemplateCreateSchema,
EnergyReportTemplateOutSchema,
EnergyReportTemplateQueryParam,
EnergyReportTemplateUpdateSchema,
)
ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE = "energy_report_template"
class EnergyReportTemplateService:
"""能源报告模板服务。"""
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
async def detail(self, id: int) -> EnergyReportTemplateOutSchema:
template = await EnergyReportTemplateCRUD(self.auth).get_or_404(id=id)
return self._to_out_schema(template)
async def page(
self,
page_no: int,
page_size: int,
search: EnergyReportTemplateQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> dict:
offset = (page_no - 1) * page_size
page_result = await EnergyReportTemplateCRUD(self.auth).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"order": "asc"}, {"id": "desc"}],
search=vars(search) if search else None,
out_schema=EnergyReportTemplateOutSchema,
)
for item in page_result.items:
file_info = item.get("file") or {}
item["file_url"] = file_info.get("file_url")
item["original_name"] = file_info.get("original_name")
item["object_name"] = file_info.get("object_name")
return page_result
async def create(self, data: EnergyReportTemplateCreateSchema) -> EnergyReportTemplateOutSchema:
await self._validate_unique_name(template_name=data.template_name)
await self._validate_file(file_id=data.file_id)
template = await EnergyReportTemplateCRUD(self.auth).create(data=data)
await self._bind_file(file_id=data.file_id, template_id=template.id)
return self._to_out_schema(template)
async def update(self, id: int, data: EnergyReportTemplateUpdateSchema) -> EnergyReportTemplateOutSchema:
old_template = await EnergyReportTemplateCRUD(self.auth).get_or_404(id=id, msg="更新失败,能源报告模板不存在")
await self._validate_unique_name(template_name=data.template_name, exclude_id=id)
await self._validate_file(file_id=data.file_id, current_template_id=id)
template = await EnergyReportTemplateCRUD(self.auth).update(id=id, data=data)
if old_template.file_id != data.file_id:
await self._unbind_file(file_id=old_template.file_id, template_id=id)
await self._bind_file(file_id=data.file_id, template_id=id)
return self._to_out_schema(template)
async def delete(self, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg="删除失败,删除对象不能为空")
templates = await EnergyReportTemplateCRUD(self.auth).get_list(search={"id": ("in", ids)})
template_map = {template.id: template for template in templates}
for template_id in ids:
if template_id not in template_map:
raise CustomException(msg="删除失败,能源报告模板不存在")
for template in templates:
await self._unbind_file(file_id=template.file_id, template_id=template.id)
await EnergyReportTemplateCRUD(self.auth).delete(ids=ids)
def _to_out_schema(self, template) -> EnergyReportTemplateOutSchema:
template_out = EnergyReportTemplateOutSchema.model_validate(template)
if template_out.file:
template_out.file_url = template_out.file.file_url
template_out.original_name = template_out.file.original_name
template_out.object_name = template_out.file.object_name
return template_out
async def _validate_unique_name(self, template_name: str, exclude_id: int | None = None) -> None:
template = await EnergyReportTemplateCRUD(self.auth).get(template_name=template_name)
if template and template.id != exclude_id:
raise CustomException(msg="能源报告模板名称已存在")
async def _validate_file(self, file_id: int, current_template_id: int | None = None) -> None:
file_record = await FileCRUD(self.auth).get(id=file_id)
if not file_record:
raise CustomException(msg="文件记录不存在")
if file_record.business_type and file_record.business_type != ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE:
raise CustomException(msg="文件已绑定到其他业务类型")
if file_record.business_id and file_record.business_id != current_template_id:
raise CustomException(msg="文件已绑定到其他能源报告模板")
async def _bind_file(self, file_id: int, template_id: int) -> None:
await FileCRUD(self.auth).set(
ids=[file_id],
business_type=ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE,
business_id=template_id,
)
async def _unbind_file(self, file_id: int, template_id: int) -> None:
file_record = await FileCRUD(self.auth).get(id=file_id)
if not file_record:
return
if file_record.business_type == ENERGY_REPORT_TEMPLATE_BUSINESS_TYPE and file_record.business_id == template_id:
await FileCRUD(self.auth).set(ids=[file_id], business_id=None)

@ -7,7 +7,7 @@ from app.api.v1.module_common.file.service import FileService
from app.common.request import PaginationService
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse, UploadFileResponse
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import UploadResponseSchema
from app.core.base_schema import AuthSchema, UploadResponseSchema
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
@ -49,20 +49,21 @@ async def get_directory_list_controller(
"/upload",
summary="上传文件",
response_model=ResponseSchema[UploadResponseSchema],
dependencies=[Depends(AuthPermission(["module_monitor:resource:upload"]))],
)
async def upload_file_controller(
file: UploadFile,
request: Request,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_monitor:resource:upload"]))],
target_path: Annotated[str | None, Form(description="目标目录路径")] = None,
) -> JSONResponse:
result = await FileService.upload_service(
auth=auth,
base_url=str(request.base_url),
file=file,
upload_type="resource",
target_path=target_path,
)
return SuccessResponse(data=result, msg="上传文件成功")
return SuccessResponse(data=FileService.to_upload_response(result), msg="上传文件成功")
@ResourceRouter.get(

@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse
from app.common.response import ResponseSchema, SuccessResponse
from app.core import cache_util
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
from app.core.cache_util import cache
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
@ -19,6 +19,23 @@ RuleRouter = APIRouter(route_class=OperationLogRoute, prefix="", tags=["规则
_RULE_NS = "rule"
@RuleRouter.get(
"/all",
summary="查询全部规则(不分页,下拉选择用)",
response_model=ResponseSchema[list[RuleOutSchema]],
)
@cache(expire=300, namespace=_RULE_NS)
async def get_rule_all_controller(
search: Annotated[RuleQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:query"]))],
) -> JSONResponse:
result = await RuleService(auth).get_list(
search=search,
order_by=[{"id": "desc"}],
)
return SuccessResponse(data=result, msg="查询全部规则成功")
@RuleRouter.get(
"/list",
summary="查询规则列表",
@ -93,3 +110,17 @@ async def delete_rule_controller(
await RuleService(auth).delete(ids=ids)
await cache_util.clear(namespace=_RULE_NS)
return SuccessResponse(msg="删除规则成功")
@RuleRouter.put(
"/status/batch",
summary="批量启用停用规则",
response_model=ResponseSchema[None],
)
async def batch_set_rule_status_controller(
data: BatchSetAvailable,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:patch"]))],
) -> JSONResponse:
await RuleService(auth).set_available(data=data)
await cache_util.clear(namespace=_RULE_NS)
return SuccessResponse(msg="批量修改规则状态成功")

@ -1,5 +1,5 @@
from app.api.v1.module_rule.rule_category.crud import RuleCategoryCRUD
from app.core.base_schema import AuthSchema
from app.core.base_schema import AuthSchema, BatchSetAvailable
from app.core.exceptions import CustomException
from .crud import RuleCRUD
@ -66,6 +66,14 @@ class RuleService:
raise CustomException(msg="删除失败,该规则不存在")
await RuleCRUD(self.auth).delete(ids=ids)
async def set_available(self, data: BatchSetAvailable) -> None:
rules = await RuleCRUD(self.auth).get_list(search={"id": ("in", data.ids)})
rule_map = {rule.id: rule for rule in rules}
for rule_id in data.ids:
if rule_id not in rule_map:
raise CustomException(msg="该规则不存在")
await RuleCRUD(self.auth).set(ids=data.ids, status=data.status)
def _to_out_schema(self, rule) -> RuleOutSchema:
rule_out = RuleOutSchema.model_validate(rule)
rule_out.category_name = rule_out.category_name_snapshot

@ -94,12 +94,14 @@ def register_exceptions(app: FastAPI) -> None:
def register_routers(app: FastAPI) -> None:
from app.api.v1.module_common import common_router
from app.api.v1.module_energy import energy_router
from app.api.v1.module_monitor import monitor_router
from app.api.v1.module_platform import platform_router
from app.api.v1.module_rule import rule_router
from app.api.v1.module_system import system_router
app.include_router(common_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))])
app.include_router(energy_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))])
app.include_router(monitor_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))])
app.include_router(platform_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))])
app.include_router(rule_router, dependencies=[Depends(RateLimiter(times=200, seconds=10))])

@ -15,6 +15,7 @@ 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.report_template.model import EnergyReportTemplateModel
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
@ -78,6 +79,7 @@ class InitializeData:
NoticeReadModel,
TicketModel,
FileModel,
EnergyReportTemplateModel,
# ── 日志表(追加写入) ──
LoginLogModel,
OperationLogModel,

Loading…
Cancel
Save