150 lines
5.8 KiB
Python
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)
|