"""Standard library queries and administrator maintenance.""" from __future__ import annotations import hashlib import math from datetime import UTC, datetime from typing import Any from sqlalchemy import func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppException from app.models.entities import OperationAudit, StandardRule, UserAccount from app.schemas.standards import ( StandardCategoryCount, StandardRuleCreate, StandardRuleDetail, StandardRuleListData, StandardRuleSummary, StandardRuleUpdate, ) class StandardLibraryService: categories = ("内料", "包材", "包装", "法规") def __init__(self, session: AsyncSession) -> None: self.session = session async def list( self, *, keyword: str | None = None, category: str = "all", status: str = "启用", page: int = 1, page_size: int = 20, ) -> StandardRuleListData: filters = self._filters(keyword=keyword, category=category, status=status) total = int(await self.session.scalar(select(func.count()).select_from(StandardRule).where(*filters)) or 0) statement = ( select(StandardRule) .where(*filters) .order_by(StandardRule.category_lv1, StandardRule.id) .offset((page - 1) * page_size) .limit(page_size) ) rows = list(await self.session.scalars(statement)) category_counts = await self._category_counts(keyword=keyword, status=status) return StandardRuleListData( items=[self.to_summary(row) for row in rows], total=total, page=page, page_size=page_size, total_pages=math.ceil(total / page_size) if total else 0, category_counts=category_counts, ) async def get(self, rule_id: int) -> StandardRuleDetail: row = await self.session.get(StandardRule, rule_id) if row is None: raise AppException("标准条目不存在", code=404, status_code=404) return self.to_detail(row) async def create(self, payload: StandardRuleCreate, user: UserAccount) -> StandardRuleDetail: row = StandardRule( **payload.model_dump(), import_fingerprint=self._manual_fingerprint(payload), updated_at=datetime.now(UTC), ) self.session.add(row) await self.session.flush() await self._audit(user, "standard.create", row.id) return self.to_detail(row) async def update(self, rule_id: int, payload: StandardRuleUpdate, user: UserAccount) -> StandardRuleDetail: row = await self.session.get(StandardRule, rule_id) if row is None: raise AppException("标准条目不存在", code=404, status_code=404) for key, value in payload.model_dump(exclude_unset=True).items(): setattr(row, key, value) row.updated_at = datetime.now(UTC) await self._audit(user, "standard.update", row.id, {"fields": list(payload.model_fields_set)}) await self.session.flush() return self.to_detail(row) async def _category_counts(self, *, keyword: str | None, status: str) -> list[StandardCategoryCount]: filters = self._filters(keyword=keyword, category="all", status=status) rows = await self.session.execute( select(StandardRule.category_lv1, func.count()) .where(*filters) .group_by(StandardRule.category_lv1) ) counts = dict(rows.all()) return [ StandardCategoryCount(category=category, count=int(counts.get(category, 0))) for category in self.categories ] @staticmethod def _filters(*, keyword: str | None, category: str, status: str) -> list[Any]: filters: list[Any] = [] if category != "all": if category not in StandardLibraryService.categories: raise AppException("无效的标准类目", code=422, status_code=422) filters.append(StandardRule.category_lv1 == category) if status != "all": if status not in {"启用", "停用"}: raise AppException("无效的标准状态", code=422, status_code=422) filters.append(StandardRule.status == status) if keyword and keyword.strip(): pattern = f"%{keyword.strip()}%" filters.append( or_( StandardRule.category_lv1.ilike(pattern), StandardRule.category_lv2.ilike(pattern), StandardRule.category_lv3.ilike(pattern), StandardRule.risk_point.ilike(pattern), StandardRule.judge_standard.ilike(pattern), StandardRule.regulation.ilike(pattern), ) ) return filters async def _audit(self, user: UserAccount, action: str, rule_id: int, metadata: dict | None = None) -> None: self.session.add( OperationAudit( actor_user_id=user.id, actor_emp_no=user.emp_no, action=action, resource_type="standard_rule", resource_id=str(rule_id), audit_metadata=metadata or {}, ) ) @staticmethod def _manual_fingerprint(payload: StandardRuleCreate) -> str: content = "\x1f".join(str(value or "") for value in payload.model_dump().values()) return hashlib.sha256(f"manual\x1f{content}".encode()).hexdigest() @staticmethod def to_summary(row: StandardRule) -> StandardRuleSummary: return StandardRuleSummary.model_validate(row, from_attributes=True) @staticmethod def to_detail(row: StandardRule) -> StandardRuleDetail: return StandardRuleDetail.model_validate(row, from_attributes=True)