Files
gaokao-volunteer-system/data/orders/models.py
2026-06-12 15:24:20 +08:00

158 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""订单数据模型 (T4.1)
Order dataclass 覆盖 TECH_ARCHITECTURE §3.4 所有字段;敏感字段按加密/明文分字段存储。
to_dict / from_dict 负责明文↔密文自动转换DAO 层直接调用)。
"""
from __future__ import annotations
import json
import random
import string
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from typing import Any, List, Optional
from .crypto import encrypt, decrypt, hash_for_index
def utc_now_iso() -> str:
"""返回当前 UTC 时间的 ISO8601 字符串(秒精度)。"""
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def generate_order_id() -> str:
"""生成订单号 GKO-YYYYMMDD-XXXX4 位大写字母+数字)。"""
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
rand = "".join(random.choices(string.ascii_uppercase + string.digits, k=4))
return f"GKO-{date_part}-{rand}"
@dataclass
class Order:
"""订单数据模型。
加密约定:
- customer_phone / candidate_id_card 在 from_dict 时加密to_dict 时解密;
数据库落盘只看到 _enc 后缀字段。
- customer_phone_hash 仅用于去重查询SHA-256 hex
"""
id: str
source: str # 'xianyu'|'wechat'|'web'|'school'
external_id: Optional[str] = None
service_version: str = "basic" # 'audit'|'basic'|'standard'|'premium'
amount_cents: int = 0
status: str = "pending"
status_updated_at: Optional[str] = None
# 客户(明文/加密分字段)
customer_name: Optional[str] = None
customer_phone: Optional[str] = None # 明文API 入口接收)
customer_phone_hash: Optional[str] = None # 自动派生
customer_wechat: Optional[str] = None
# 考生
candidate_name: Optional[str] = None
candidate_id_card: Optional[str] = None # 明文API 入口接收)
candidate_province: Optional[str] = None
candidate_score: Optional[int] = None
candidate_rank: Optional[int] = None
candidate_subjects: List[str] = field(default_factory=list)
candidate_interests: Optional[str] = None
candidate_strong_subjects: Optional[str] = None
candidate_weak_subjects: Optional[str] = None
candidate_family: Optional[str] = None
# 服务
assigned_consultant: Optional[str] = None
plan_file: Optional[str] = None
audit_report: Optional[str] = None
pdf_path: Optional[str] = None
# 时间戳
created_at: Optional[str] = None
paid_at: Optional[str] = None
started_at: Optional[str] = None
delivered_at: Optional[str] = None
completed_at: Optional[str] = None
# 元数据
notes: Optional[str] = None
tags: List[str] = field(default_factory=list)
upgrade_from: Optional[str] = None
def __post_init__(self) -> None:
"""自动派生 hash 与时间戳。"""
if self.customer_phone and not self.customer_phone_hash:
self.customer_phone_hash = hash_for_index(self.customer_phone)
if not self.created_at:
self.created_at = utc_now_iso()
if not self.status_updated_at:
self.status_updated_at = self.created_at
# tags/subjects 入库为 JSON 字符串
if isinstance(self.tags, list):
self._tags_json = json.dumps(self.tags, ensure_ascii=False)
if isinstance(self.candidate_subjects, list):
self._subjects_json = json.dumps(
self.candidate_subjects, ensure_ascii=False
)
# 序列化到 DB加密敏感字段
def to_db_row(self) -> dict[str, Any]:
"""返回可直接写入 orders 表的字典(敏感字段已加密)。"""
data = asdict(self)
# 移除明文敏感字段
data.pop("customer_phone", None)
data.pop("candidate_id_card", None)
# 加密落盘字段
if self.customer_phone:
data["customer_phone_enc"] = encrypt(self.customer_phone)
if self.candidate_id_card:
data["candidate_id_card_enc"] = encrypt(self.candidate_id_card)
# tags/subjects JSON 化
data["tags"] = json.dumps(self.tags, ensure_ascii=False)
data["candidate_subjects"] = json.dumps(
self.candidate_subjects, ensure_ascii=False
)
return data
# 从 DB 反序列化(解密敏感字段)
@classmethod
def from_db_row(cls, row: dict[str, Any]) -> "Order":
"""从数据库行构造 Order自动解密敏感字段。"""
data = dict(row)
if data.get("customer_phone_enc"):
data["customer_phone"] = decrypt(data["customer_phone_enc"])
else:
data["customer_phone"] = None
if data.get("candidate_id_card_enc"):
data["candidate_id_card"] = decrypt(data["candidate_id_card_enc"])
else:
data["candidate_id_card"] = None
# JSON 列表字段
for key in ("tags", "candidate_subjects"):
raw = data.get(key)
if raw:
try:
data[key] = json.loads(raw)
except (json.JSONDecodeError, TypeError):
data[key] = []
else:
data[key] = []
# 移除 DB-only 加密字段
data.pop("customer_phone_enc", None)
data.pop("candidate_id_card_enc", None)
return cls(**data)
def to_dict(self, decrypt_sensitive: bool = True) -> dict[str, Any]:
"""导出为字典。
decrypt_sensitive=True 时敏感字段以明文返回API 响应False 时只返回 hash。
"""
data = asdict(self)
if not decrypt_sensitive:
data.pop("customer_phone", None)
data.pop("candidate_id_card", None)
return data