# -*- 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$$$ 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"]