docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富 统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」; 同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。 2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本), 新增《文档规整方案与开发前待决事项-2026-09-17》。 3) 客服agent 四份交付文档首次纳入本分支。
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
MIGRATION_PATH = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "alembic"
|
||||
/ "versions"
|
||||
/ "20260910_offsite_recognition_attempt.py"
|
||||
)
|
||||
|
||||
|
||||
def test_offsite_recognition_attempt_migration_is_forward_compatible() -> None:
|
||||
module = _load_migration()
|
||||
|
||||
assert module.revision == "20260910_recog_attempt"
|
||||
assert module.down_revision == "20260910_offsite_worker_identity"
|
||||
assert len(module.revision) <= 32
|
||||
source = MIGRATION_PATH.read_text(encoding="utf-8")
|
||||
assert "CREATE TABLE IF NOT EXISTS offsite_recognition_attempt" in source
|
||||
assert "DROP TABLE" not in source.upper()
|
||||
assert "ALTER TABLE offsite_fund_" not in source
|
||||
|
||||
|
||||
def _load_migration() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"offsite_recognition_attempt_migration", MIGRATION_PATH
|
||||
)
|
||||
assert spec is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
MIGRATION_PATH = (
|
||||
Path(__file__).resolve().parents[3]
|
||||
/ "alembic"
|
||||
/ "versions"
|
||||
/ "20260910_offsite_worker_identity.py"
|
||||
)
|
||||
|
||||
|
||||
def test_offsite_worker_identity_helpers_create_minimal_identity() -> None:
|
||||
module = _load_migration()
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as connection:
|
||||
_create_rbac_tables(connection)
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
worker_user_id = module._ensure_worker_user(connection, now)
|
||||
role_id = module._ensure_operator_role(connection, now)
|
||||
permission_id = module._ensure_offsite_write_permission(connection, now)
|
||||
module._ensure_operator_role_is_minimal(connection, role_id)
|
||||
module._ensure_role_permission(connection, role_id, permission_id, worker_user_id, now)
|
||||
module._ensure_user_role(connection, worker_user_id, role_id, now)
|
||||
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_user")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_role")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_permission")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_user_role")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_role_permission")) == 1
|
||||
|
||||
|
||||
def test_offsite_worker_identity_helpers_are_idempotent() -> None:
|
||||
module = _load_migration()
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as connection:
|
||||
_create_rbac_tables(connection)
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
for _ in range(2):
|
||||
worker_user_id = module._ensure_worker_user(connection, now)
|
||||
role_id = module._ensure_operator_role(connection, now)
|
||||
permission_id = module._ensure_offsite_write_permission(connection, now)
|
||||
module._ensure_operator_role_is_minimal(connection, role_id)
|
||||
module._ensure_role_permission(connection, role_id, permission_id, worker_user_id, now)
|
||||
module._ensure_user_role(connection, worker_user_id, role_id, now)
|
||||
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_user")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_role")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_permission")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_user_role")) == 1
|
||||
assert connection.scalar(text("SELECT COUNT(*) FROM sys_role_permission")) == 1
|
||||
|
||||
|
||||
def test_offsite_worker_identity_rejects_broad_operator_role() -> None:
|
||||
module = _load_migration()
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as connection:
|
||||
_create_rbac_tables(connection)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO sys_role (id, role_code, role_name, status, created_at, updated_at)
|
||||
VALUES (1, 'operator', '运营人员', 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO sys_permission
|
||||
(id, permission_code, resource, action, data_scope, field_policy, created_at,
|
||||
updated_at)
|
||||
VALUES
|
||||
(1, 'offsite:write', 'offsite_fund', 'write', 'all', NULL, CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP),
|
||||
(2, 'offsite:notify', 'offsite_fund', 'notify', 'all', NULL, CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO sys_role_permission (role_id, permission_id, created_at)
|
||||
VALUES (1, 1, CURRENT_TIMESTAMP), (1, 2, CURRENT_TIMESTAMP)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="额外权限"):
|
||||
module._ensure_operator_role_is_minimal(connection, 1)
|
||||
|
||||
|
||||
def test_offsite_worker_identity_rejects_permission_broadening() -> None:
|
||||
module = _load_migration()
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
with engine.begin() as connection:
|
||||
_create_rbac_tables(connection)
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
worker_user_id = module._ensure_worker_user(connection, now)
|
||||
role_id = module._ensure_operator_role(connection, now)
|
||||
permission_id = module._ensure_offsite_write_permission(connection, now)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO sys_user
|
||||
(id, user_no, username, password_hash, user_type, employee_role,
|
||||
professional_investor_status, fund_account_status, status, created_at,
|
||||
updated_at)
|
||||
VALUES
|
||||
(999, 'OTHER', 'other_operator', 'x', '员工', 'operator', '未申请', '未开户',
|
||||
'正常', :now, :now)
|
||||
"""
|
||||
),
|
||||
{"now": now},
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO sys_user_role (user_id, role_id, assigned_at, expires_at)
|
||||
VALUES (999, :role_id, :now, NULL)
|
||||
"""
|
||||
),
|
||||
{"role_id": role_id, "now": now},
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="扩大 offsite:write 权限"):
|
||||
module._ensure_role_permission(
|
||||
connection, role_id, permission_id, worker_user_id, now
|
||||
)
|
||||
|
||||
|
||||
def _load_migration() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"offsite_worker_identity_migration", MIGRATION_PATH
|
||||
)
|
||||
assert spec is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _create_rbac_tables(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE sys_user (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_no VARCHAR(32) NOT NULL UNIQUE,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
email VARCHAR(128) NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
user_type VARCHAR(16) NOT NULL,
|
||||
employee_role VARCHAR(32) NULL,
|
||||
professional_investor_status VARCHAR(16) NOT NULL,
|
||||
fund_account_status VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE sys_role (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role_code VARCHAR(32) NOT NULL UNIQUE,
|
||||
role_name VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE sys_permission (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
permission_code VARCHAR(64) NOT NULL UNIQUE,
|
||||
resource VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
data_scope VARCHAR(32) NOT NULL,
|
||||
field_policy JSON NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE sys_user_role (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
assigned_at DATETIME NOT NULL,
|
||||
expires_at DATETIME NULL,
|
||||
UNIQUE (user_id, role_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE sys_role_permission (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role_id INTEGER NOT NULL,
|
||||
permission_id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE (role_id, permission_id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user