119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
"""SQLite Schema 模块 (T4.1)
|
||
|
||
定义 orders 主表与 order_status_history 审计表的 DDL,并提供幂等的
|
||
apply_schema() 应用函数。所有表使用 IF NOT EXISTS;启用外键约束。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sqlite3
|
||
from pathlib import Path
|
||
|
||
|
||
SCHEMA_SQL: str = """
|
||
-- 订单主表
|
||
CREATE TABLE IF NOT EXISTS orders (
|
||
-- 主键 / 业务标识
|
||
id TEXT PRIMARY KEY,
|
||
source TEXT NOT NULL,
|
||
external_id TEXT,
|
||
service_version TEXT NOT NULL,
|
||
amount_cents INTEGER NOT NULL CHECK(amount_cents >= 0),
|
||
status TEXT NOT NULL CHECK(status IN
|
||
('pending','paid','serving','delivered','completed','refunded')),
|
||
status_updated_at TEXT NOT NULL,
|
||
|
||
-- 客户信息(加密 + 脱敏)
|
||
customer_name TEXT,
|
||
customer_phone_enc TEXT,
|
||
customer_phone_hash TEXT,
|
||
customer_wechat TEXT,
|
||
|
||
-- 考生信息
|
||
candidate_name TEXT,
|
||
candidate_id_card_enc TEXT,
|
||
candidate_province TEXT,
|
||
candidate_score INTEGER,
|
||
candidate_rank INTEGER,
|
||
candidate_subjects TEXT,
|
||
candidate_interests TEXT,
|
||
candidate_strong_subjects TEXT,
|
||
candidate_weak_subjects TEXT,
|
||
candidate_family TEXT,
|
||
|
||
-- 服务信息
|
||
assigned_consultant TEXT,
|
||
plan_file TEXT,
|
||
audit_report TEXT,
|
||
pdf_path TEXT,
|
||
|
||
-- 时间戳(ISO8601 UTC)
|
||
created_at TEXT NOT NULL,
|
||
paid_at TEXT,
|
||
started_at TEXT,
|
||
delivered_at TEXT,
|
||
completed_at TEXT,
|
||
|
||
-- 元数据
|
||
notes TEXT,
|
||
tags TEXT,
|
||
upgrade_from TEXT,
|
||
|
||
FOREIGN KEY (upgrade_from) REFERENCES orders(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
||
CREATE INDEX IF NOT EXISTS idx_orders_source ON orders(source);
|
||
CREATE INDEX IF NOT EXISTS idx_orders_created_at ON orders(created_at);
|
||
CREATE INDEX IF NOT EXISTS idx_orders_phone_hash ON orders(customer_phone_hash);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS uniq_orders_external
|
||
ON orders(source, external_id) WHERE external_id IS NOT NULL;
|
||
|
||
-- 状态历史(审计)
|
||
CREATE TABLE IF NOT EXISTS order_status_history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
order_id TEXT NOT NULL,
|
||
from_status TEXT,
|
||
to_status TEXT NOT NULL,
|
||
actor TEXT,
|
||
reason TEXT,
|
||
changed_at TEXT NOT NULL,
|
||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_status_history_order ON order_status_history(order_id);
|
||
"""
|
||
|
||
|
||
def apply_schema(db_path: str | Path) -> sqlite3.Connection:
|
||
"""应用 schema 到指定 SQLite 文件,返回连接。
|
||
|
||
- 启用外键约束 (PRAGMA foreign_keys = ON)
|
||
- 父目录自动创建
|
||
- 幂等:可重复执行
|
||
"""
|
||
db_path = Path(db_path)
|
||
if db_path.parent and not db_path.parent.exists():
|
||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
conn = sqlite3.connect(str(db_path))
|
||
try:
|
||
conn.execute("PRAGMA foreign_keys = ON")
|
||
conn.executescript(SCHEMA_SQL)
|
||
conn.commit()
|
||
except Exception:
|
||
conn.close()
|
||
raise
|
||
return conn
|
||
|
||
|
||
def get_schema_version(conn: sqlite3.Connection) -> int:
|
||
"""读取当前 schema 版本号(首次运行返回 0)。后续迁移将引入 schema_version 表。"""
|
||
try:
|
||
row = conn.execute(
|
||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='orders'"
|
||
).fetchone()
|
||
return 1 if row else 0
|
||
except sqlite3.DatabaseError:
|
||
return 0
|