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:
张胜宇
2026-09-17 15:15:22 +08:00
commit e239eb778b
874 changed files with 188418 additions and 0 deletions
@@ -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)
)
"""
)
)