common.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. """金额、时间、响应和异常契约统一,防止各模块用浮点或不同错误格式。"""
  2. from datetime import datetime, timezone
  3. from decimal import Decimal, InvalidOperation
  4. from enum import StrEnum
  5. from typing import Any
  6. class BusinessError(Exception):
  7. """业务错误带稳定机器码,前端不需要解析异常字符串来决定交互。"""
  8. def __init__(self, message: str, status: int = 422, code: str = "BUSINESS_ERROR", details: Any = None):
  9. super().__init__(message)
  10. self.status, self.code, self.details = status, code, details
  11. class Status(StrEnum):
  12. DRAFT = "draft"
  13. PENDING = "pending_audit"
  14. APPROVAL = "in_approval"
  15. APPROVED = "approved"
  16. REJECTED = "rejected"
  17. WITHDRAWN = "withdrawn"
  18. TRANSITIONS = {
  19. Status.DRAFT: {Status.PENDING}, Status.PENDING: {Status.APPROVAL, Status.WITHDRAWN},
  20. Status.APPROVAL: {Status.APPROVED, Status.REJECTED, Status.DRAFT},
  21. Status.REJECTED: {Status.PENDING}, Status.APPROVED: set(), Status.WITHDRAWN: set(),
  22. }
  23. ROLES = {"employee": 1, "supervisor": 2, "auditor": 3, "manager": 4, "admin": 5}
  24. DECISIONS = {"auto_approve", "to_supervisor", "to_finance", "to_manual"}
  25. def transition(record: Any, target: str) -> None:
  26. """每次迁移都查白名单,审批或后台事件不能绕过终态保护。"""
  27. if target not in TRANSITIONS.get(record.status, set()):
  28. raise BusinessError(f"不允许从 {record.status} 变更为 {target}", 409, "INVALID_STATE")
  29. record.status = target
  30. def utcnow() -> datetime:
  31. """SQLite 存无时区 UTC,统一去掉 tzinfo 避免比较 aware/naive 时间出错。"""
  32. return datetime.now(timezone.utc).replace(tzinfo=None)
  33. def iso(value: datetime | None) -> str | None:
  34. return value.isoformat() + "Z" if value else None
  35. def cents(value: Any, allow_zero: bool = True) -> int:
  36. """拒绝 NaN、超精度和超范围;不四舍五入隐藏用户填报错误。"""
  37. try:
  38. number = Decimal(str(value))
  39. if not number.is_finite() or number < 0 or number > Decimal("9999999999.99"):
  40. raise ValueError()
  41. if number != number.quantize(Decimal("0.01")) or (not allow_zero and number <= 0):
  42. raise ValueError()
  43. return int(number * 100)
  44. except (InvalidOperation, ValueError, TypeError):
  45. raise BusinessError("金额必须是非负且最多两位小数,范围不得超过 9999999999.99", code="INVALID_AMOUNT")
  46. def money(value: int | None) -> str | None:
  47. return f"{Decimal(value) / 100:.2f}" if value is not None else None
  48. def envelope(data: Any, request_id: str = "", message: str = "操作成功") -> dict:
  49. return {"data": data, "message": message, "request_id": request_id}
  50. def paginate(items: list, page: int, page_size: int) -> dict:
  51. """本地版统一分页契约,前端无需针对每类资源重写分页逻辑。"""
  52. return {"items": items[(page - 1) * page_size:page * page_size], "total": len(items), "page": page, "page_size": page_size}