feat(#150): PostgreSQL users 表 DDL + 映射(对齐 #30 schema,role CHECK 约束)

This commit is contained in:
2026-08-05 02:06:06 +00:00
parent a1acc6e63b
commit f6f41913dc
+84
View File
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
"""④-1 PostgreSQL users 表 DDL + 映射 —— issue #150 / #30 / PRD 8.2。
定义本地账号在 PostgreSQL 中的存储结构(#30 users 表 schema),并提供
`UserStore` 与 PG 之间的映射。DDL 仅用标准库拼装(无 psycopg2 依赖),
实际接库时上层注入连接即可。
表结构(对齐 core/data-bus/postgres_schema.py 的命名风格):
users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- pbkdf2_sha256$<iter>$<salt>$<hash>
role VARCHAR(16) NOT NULL DEFAULT 'readonly', -- readonly/engineer/admin
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ
)
索引:username 唯一索引(登录走 username);role 普通索引(用户管理筛选)。
"""
from __future__ import annotations
from typing import Any, Dict, List
from .users import User, VALID_ROLES
USERS_TABLE_DDL = """CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(16) NOT NULL DEFAULT 'readonly',
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ,
CONSTRAINT users_role_chk CHECK (role IN ('readonly','engineer','admin'))
);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
"""
def row_to_user(row: Any) -> User:
"""PG 行(dict/tuple-like)→ User。row 需含 keys: id/username/password_hash/role/active/created_at/last_login_at。"""
def g(k, default=None):
if isinstance(row, dict):
return row.get(k, default)
return getattr(row, k, default)
created = g("created_at")
last = g("last_login_at")
# PG TIMESTAMPTZ → epoch(若为 datetime 有 timestamp())
def to_epoch(v):
if v is None:
return None
if hasattr(v, "timestamp"):
return v.timestamp()
return v
return User(
id=int(g("id")),
username=str(g("username")),
password_hash=str(g("password_hash")),
role=str(g("role", "readonly")) or "readonly",
active=bool(g("active", True)),
created_at=to_epoch(created) or 0.0,
last_login_at=to_epoch(last),
)
def user_to_row(user: User) -> Dict[str, Any]:
"""User → PG 列字典(不含 id,用于 INSERT;UPDATE 时按 id 定位)。"""
return {
"username": user.username,
"password_hash": user.password_hash,
"role": user.role,
"active": user.active,
}
def validate_role(role: str) -> str:
if role not in VALID_ROLES:
raise ValueError("role must be one of %r" % (VALID_ROLES,))
return role
__all__ = ["USERS_TABLE_DDL", "row_to_user", "user_to_row", "validate_role"]