schemas.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. """请求契约拒绝未知字段,避免拼错参数时服务端静默采用默认配置。"""
  2. from decimal import Decimal
  3. from typing import Any, Literal
  4. from pydantic import BaseModel, ConfigDict, Field, field_validator
  5. from app.shared.common import cents
  6. class Strict(BaseModel):
  7. model_config = ConfigDict(extra="forbid")
  8. class Login(Strict):
  9. username: str = Field(min_length=1, max_length=50, description="已初始化登录名")
  10. password: str = Field(min_length=1, max_length=200, description="仅传输用于校验,不写入日志")
  11. class Refresh(Strict):
  12. refresh_token: str = Field(min_length=20, max_length=500, description="轮换或撤销的刷新凭据")
  13. class UserCreate(Strict):
  14. username: str = Field(pattern=r"^[A-Za-z][A-Za-z0-9_]{2,49}$", description="唯一登录名")
  15. real_name: str = Field(min_length=1, max_length=80, description="真实显示名称")
  16. password: str = Field(min_length=10, max_length=200, description="至少大小写、数字和符号")
  17. department_id: int = Field(gt=0, description="有效部门主键")
  18. role: Literal["employee", "supervisor", "auditor", "manager", "admin"] = Field(description="五种固定业务身份,不接受任意角色名")
  19. class UserPatch(Strict):
  20. real_name: str | None = Field(default=None, min_length=1, max_length=80, description="新显示名称")
  21. department_id: int | None = Field(default=None, gt=0, description="新部门,历史单据部门不变")
  22. role: Literal["employee", "supervisor", "auditor", "manager", "admin"] | None = Field(default=None, description="管理员授权的固定身份;禁止降级自己")
  23. active: bool | None = Field(default=None, description="停用后立即拒绝现有访问令牌")
  24. class DepartmentBody(Strict):
  25. name: str = Field(min_length=1, max_length=100, description="不重复的部门名称")
  26. active: bool = Field(default=True, description="停用仅影响新业务")
  27. class ConfigPatch(Strict):
  28. expected_version: int = Field(gt=0, description="乐观并发版本,不覆盖他人的修改")
  29. enabled: bool | None = Field(default=None, description="可选能力开关,核心规则和决策不能停用")
  30. config: dict[str, Any] | None = Field(default=None, description="节点白名单参数,禁止密钥或代码")
  31. class RulePatch(Strict):
  32. expected_version: int = Field(gt=0, description="当前规则版本")
  33. enabled: bool | None = Field(default=None, description="可选规则生效开关,核心检查不允许关闭")
  34. severity: Literal["warning", "error", "critical"] | None = Field(default=None, description="命中时的处理级别,核心严重程度有下限")
  35. params: dict[str, Any] | None = Field(default=None, description="白名单阈值,不接受表达式")
  36. scope: dict[str, Any] | None = Field(default=None, description="可选规则的部门/类型范围")
  37. class RuleTest(Strict):
  38. invoice: dict[str, Any] = Field(description="待试算字段,不写正式审核")
  39. context: dict[str, Any] = Field(default_factory=dict, description="显式试算上下文,不冒充生产历史")
  40. class ReimbursementCreate(Strict):
  41. title: str = Field(min_length=1, max_length=200, description="报销目的")
  42. description: str = Field(default="", max_length=5000, description="业务补充说明")
  43. type: Literal[1, 2, 3] = Field(default=1, description="1差旅/2招待/3办公")
  44. total_amount: Decimal = Field(description="含税总额,以两位小数传输")
  45. @field_validator("total_amount")
  46. @classmethod
  47. def money_precision(cls, value: Decimal):
  48. """金额契约在请求入口验证,与数据库整数分口径一致。"""
  49. cents(value)
  50. return value
  51. class ReimbursementPatch(Strict):
  52. revision: int = Field(gt=0, description="必须等于当前可编辑版本")
  53. title: str | None = Field(default=None, min_length=1, max_length=200, description="修改业务目的,不能空字符串")
  54. description: str | None = Field(default=None, max_length=5000, description="修订补充说明")
  55. type: Literal[1, 2, 3] | None = Field(default=None, description="修改1差旅/2招待/3办公类型,影响下一轮匹配")
  56. total_amount: Decimal | None = Field(default=None, description="修订含税额,服务层校验非负及两位精度")
  57. class Revision(Strict):
  58. revision: int = Field(gt=0, description="预期单据版本,过期返回409")
  59. class InvoicePatch(Strict):
  60. revision: int = Field(gt=0, description="所属单据当前版本")
  61. fields: dict[str, Any] = Field(description="用户填报字段;缺失为未知,不伪造默认金额")
  62. class Rerun(Strict):
  63. reason: str = Field(min_length=2, max_length=1000, description="重跑理由记录在审计日志")
  64. class Feedback(Strict):
  65. approval_record_id: int = Field(gt=0, description="本人已处理审批记录")
  66. corrected_fields: dict[str, Any] = Field(default_factory=dict, description="纠正建议,不覆盖原证据")
  67. reason: str = Field(min_length=2, max_length=2000, description="判断依据")
  68. class Decide(Strict):
  69. action: Literal["approve", "reject", "return"] = Field(description="通过、驳回或退回草稿,不接受任意状态")
  70. opinion: str = Field(min_length=2, max_length=2000, description="所有审批动作必须留可读意见")
  71. class ChainNode(Strict):
  72. approver_id: int = Field(gt=0, description="实际有效用户,而非角色占位符")
  73. role: Literal["supervisor", "auditor", "manager", "admin"] = Field(description="必须与指定人员当前真实身份一致")
  74. class ChainBody(Strict):
  75. name: str = Field(min_length=1, max_length=100, description="审批链显示名")
  76. route: Literal["to_supervisor", "to_finance", "to_manual"] = Field(description="对应确定性审核路由;系统批准不配置人工链")
  77. type: Literal[1, 2, 3] | None = Field(default=None, description="适用报销类型,空表示全部")
  78. department_id: int | None = Field(default=None, gt=0, description="匹配历史单据所属有效部门,空表示全部")
  79. amount_min: Decimal | None = Field(default=None, description="含税金额下限,空不限制")
  80. amount_max: Decimal | None = Field(default=None, description="含税金额上限,必须大于等于下限")
  81. priority: int = Field(default=0, ge=0, le=100, description="同具体度链的选择优先级,越大越优先")
  82. nodes: list[ChainNode] = Field(min_length=1, max_length=10, description="有序且非空审批链")
  83. active: bool = Field(default=True, description="仅影响新轮次,已有冻结链不替换")
  84. expected_version: int | None = Field(default=None, gt=0, description="更新时必填")
  85. class KnowledgeBody(Strict):
  86. title: str = Field(min_length=1, max_length=200, description="政策原文标题,用作引用出处")
  87. category: str = Field(default="policy", pattern=r"^[a-zA-Z0-9_-]{1,40}$", description="policy分类参与正式政策审核")
  88. content: str = Field(min_length=1, max_length=200000, description="用户提供的政策原文")
  89. active: bool = Field(default=True, description="停用原文不参与新审核检索")
  90. expected_version: int | None = Field(default=None, gt=0, description="修订时必须等于当前正文版本")
  91. class SearchBody(Strict):
  92. query: str = Field(min_length=1, max_length=2000, description="实际检索问题,不用作系统指令")
  93. mode: Literal["keyword", "semantic"] = Field(default="keyword", description="显式本地词项或真实向量语义模式")
  94. top_k: int = Field(default=5, ge=1, le=20, description="最多返回的真实片段数量")
  95. category: str | None = Field(default=None, max_length=40, description="可选分类过滤")
  96. class Opinion(Strict):
  97. opinion: str = Field(min_length=2, max_length=2000, description="处理意见,不得仅置状态无依据")
  98. class ExtractedInvoice(Strict):
  99. """视觉模型只输出允许字段,无法识别留空;金额不靠默认值补齐。"""
  100. invoice_code: str | None = Field(default=None, max_length=100, description="原票代码,未知留空")
  101. invoice_no: str | None = Field(default=None, max_length=100, description="原票号码,非文件去重键")
  102. invoice_date: str | None = Field(default=None, max_length=30, description="开票日期,规则检查格式及有效期")
  103. amount: Decimal | None = Field(default=None, description="不含税金额;非负且最多两位小数,缺失未知")
  104. tax_amount: Decimal | None = Field(default=None, description="原票税额,非负且最多两位小数")
  105. tax_rate: Decimal | None = Field(default=None, ge=0, le=1, description="原票声明税率,不能推测法定税率")
  106. buyer_name: str | None = Field(default=None, max_length=200, description="购方完整名称")
  107. seller_name: str | None = Field(default=None, max_length=200, description="销方完整名称")
  108. tax_number: str | None = Field(default=None, max_length=100, description="发票税号,仅格式检查")
  109. invoice_type: str | None = Field(default=None, max_length=100, description="原票类型描述")
  110. items: list[dict] = Field(default_factory=list, max_length=100, description="最多100条原票明细,不作为自动金额补齐依据")
  111. @field_validator("amount", "tax_amount")
  112. @classmethod
  113. def extraction_money(cls, value):
  114. if value is not None:
  115. cents(value)
  116. return value
  117. class RiskAssessment(Strict):
  118. risk_level: int = Field(ge=1, le=3, description="只接受低/中/高三级")
  119. risk_score: Decimal = Field(ge=0, le=1, description="模型结构化归一分值,不保证是校准概率")
  120. risk_factors: list[str] = Field(description="模型输出的风险因素,记录依据但不能单独批准")
  121. reasoning: str = Field(min_length=1, max_length=5000)
  122. class PolicyAssessment(Strict):
  123. status: Literal["compliant", "non_compliant", "partial", "unknown"] = Field(description="结构化政策结论;未知和部分完成不会汇总为全通过")
  124. reason: str = Field(min_length=1, max_length=5000)
  125. chunk_ids: list[int] = Field(default_factory=list, description="只能引用本次检索返回的真实块 ID")