2026-09-04 10:20:40 +08:00

150 lines
5.8 KiB
Python

"""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)