refactor(server): 单号规则收紧为 A/R/D+8 位紧凑格式
- DOCUMENT_NUMBER_PREFIXES 改为 A/R/D,新增短格式与旧格式正则并存识别,提取正则加边界锚定避免误匹配 - build_document_number 去掉时间戳段,统一生成 A+token 等紧凑单号,is_application_claim_no 兼容旧 AP-/APP- 前缀 - access_policy/status_registry/reimbursements/expense_claims/budget_support 统一复用 is_application_claim_no 判定申请单 - 同步 document_numbering 单元测试覆盖新旧两种格式
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import secrets
|
||||
from datetime import UTC, datetime
|
||||
from datetime import datetime
|
||||
from typing import Callable, Literal
|
||||
|
||||
from sqlalchemy import select
|
||||
@@ -13,20 +13,29 @@ from app.models.financial_record import ExpenseClaim
|
||||
DocumentNumberKind = Literal["application", "reimbursement", "audit"]
|
||||
|
||||
DOCUMENT_NUMBER_PREFIXES: dict[DocumentNumberKind, str] = {
|
||||
"application": "AP",
|
||||
"reimbursement": "RE",
|
||||
"audit": "AD",
|
||||
"application": "A",
|
||||
"reimbursement": "R",
|
||||
"audit": "D",
|
||||
}
|
||||
DOCUMENT_NUMBER_TOKEN_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
DOCUMENT_NUMBER_TOKEN_LENGTH = 8
|
||||
DOCUMENT_NUMBER_SHORT_BODY = (
|
||||
rf"(?:A|R|D)[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}"
|
||||
)
|
||||
DOCUMENT_NUMBER_LEGACY_BODY = (
|
||||
rf"(?:AP|RE|AD)-\d{{14}}-[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}"
|
||||
)
|
||||
DOCUMENT_NUMBER_PATTERN = re.compile(
|
||||
rf"^(?:AP|RE|AD)-\d{{14}}-[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}$",
|
||||
rf"^(?:{DOCUMENT_NUMBER_SHORT_BODY}|{DOCUMENT_NUMBER_LEGACY_BODY})$",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
DOCUMENT_NUMBER_EXTRACT_PATTERN = re.compile(
|
||||
rf"(?:AP|RE|AD)-\d{{14}}-[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}"
|
||||
rf"(?<![A-Z0-9])(?:"
|
||||
rf"{DOCUMENT_NUMBER_SHORT_BODY}"
|
||||
rf"|{DOCUMENT_NUMBER_LEGACY_BODY}"
|
||||
r"|APP-\d{8}-[A-Z0-9]{6}"
|
||||
r"|EXP-\d{6}-\d{3}",
|
||||
r"|EXP-\d{6}-\d{3}"
|
||||
r")(?![A-Z0-9])",
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -45,16 +54,13 @@ def build_document_number(
|
||||
token: str | None = None,
|
||||
) -> str:
|
||||
prefix = DOCUMENT_NUMBER_PREFIXES[kind]
|
||||
generated_at = timestamp or datetime.now(UTC)
|
||||
if generated_at.tzinfo is None:
|
||||
generated_at = generated_at.replace(tzinfo=UTC)
|
||||
normalized_token = (token or generate_document_token()).strip().upper()
|
||||
if not re.fullmatch(
|
||||
rf"[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}",
|
||||
normalized_token,
|
||||
):
|
||||
raise ValueError("document number token must be 8 chars from the configured alphabet")
|
||||
return f"{prefix}-{generated_at.astimezone(UTC):%Y%m%d%H%M%S}-{normalized_token}"
|
||||
return f"{prefix}{normalized_token}"
|
||||
|
||||
|
||||
def generate_unique_expense_claim_no(
|
||||
@@ -83,4 +89,9 @@ def generate_unique_expense_claim_no(
|
||||
|
||||
def is_application_claim_no(value: object) -> bool:
|
||||
normalized = str(value or "").strip().upper()
|
||||
return normalized.startswith(("AP-", "APP-"))
|
||||
return bool(
|
||||
re.fullmatch(
|
||||
rf"A[{DOCUMENT_NUMBER_TOKEN_ALPHABET}]{{{DOCUMENT_NUMBER_TOKEN_LENGTH}}}",
|
||||
normalized,
|
||||
)
|
||||
) or normalized.startswith(("AP-", "APP-"))
|
||||
|
||||
Reference in New Issue
Block a user