Merge remote-tracking branch 'origin/qyqy_develop' into nl-merge-colleague
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Prepare a fresh MySQL database for the advisor demonstration.
|
||||
|
||||
This is deliberately an orchestrator, not a database dump tool. The legacy
|
||||
``jr_agent`` database has schema drift and must not be copied or upgraded in
|
||||
place. The command is dry-run by default and requires two explicit flags
|
||||
before it writes the configured target database.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Step:
|
||||
label: str
|
||||
command: tuple[str, ...]
|
||||
display: str
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Bootstrap an isolated advisor demo database")
|
||||
parser.add_argument("--apply", action="store_true", help="Run commands that write the target database")
|
||||
parser.add_argument(
|
||||
"--confirm-demo-database",
|
||||
action="store_true",
|
||||
help="Confirm that MYSQL_DSN points to a disposable demo database",
|
||||
)
|
||||
parser.add_argument("--suitability-csv", type=Path, help="Reviewed suitability disclosure CSV")
|
||||
parser.add_argument("--contracts-csv", type=Path, help="Reviewed fund-contract disclosure CSV")
|
||||
parser.add_argument("--market-days", type=int, default=400, help="Days of history to synchronize")
|
||||
parser.add_argument("--skip-market-sync", action="store_true", help="Do not call public market sources")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def build_steps(args: argparse.Namespace) -> tuple[Step, ...]:
|
||||
python = sys.executable
|
||||
steps = [
|
||||
Step("Apply current Alembic migrations", (python, "-m", "alembic", "upgrade", "head"), "python -m alembic upgrade head"),
|
||||
Step("Seed baseline demo RBAC", (python, "tools/seed_test_rbac.py"), "python tools/seed_test_rbac.py"),
|
||||
Step("Set customer login", (python, "tools/create_test_user.py", "--id", "9001", "--username", "cust_t", "--role", "customer", "--password", "123456"), "python tools/create_test_user.py --id 9001 --username cust_t --role customer --password <demo-password>"),
|
||||
Step("Set administrator login", (python, "tools/create_test_user.py", "--id", "9003", "--username", "admin_t", "--role", "admin", "--password", "88888888"), "python tools/create_test_user.py --id 9003 --username admin_t --role admin --password <demo-password>"),
|
||||
Step("Create advisor role and permissions", (python, "tools/grant_advisor_role.py"), "python tools/grant_advisor_role.py"),
|
||||
Step("Set advisor login", (python, "tools/create_test_user.py", "--id", "9020", "--username", "advisor_t", "--role", "advisor", "--password", "abc12345"), "python tools/create_test_user.py --id 9020 --username advisor_t --role advisor --password <demo-password>"),
|
||||
Step("Seed compliance baseline", (python, "tools/seed_compliance_baseline.py"), "python tools/seed_compliance_baseline.py"),
|
||||
Step("Seed customer assessment and advisor assignment", (python, "tools/seed_advisor_demo.py"), "python tools/seed_advisor_demo.py"),
|
||||
]
|
||||
if args.suitability_csv is not None:
|
||||
steps.append(
|
||||
Step(
|
||||
"Import reviewed product evidence",
|
||||
(python, "tools/import_product_governance_reference.py", "--suitability", str(args.suitability_csv), "--contracts", str(args.contracts_csv)),
|
||||
"python tools/import_product_governance_reference.py --suitability <reviewed.csv> --contracts <reviewed.csv>",
|
||||
)
|
||||
)
|
||||
steps.append(
|
||||
Step("Classify product assets", (python, "tools/import_product_asset_classifications.py"), "python tools/import_product_asset_classifications.py")
|
||||
)
|
||||
if not args.skip_market_sync:
|
||||
steps.append(
|
||||
Step("Refresh market history and quality snapshots", (python, "tools/sync_advisor_market_data.py", "--days", str(args.market_days)), f"python tools/sync_advisor_market_data.py --days {args.market_days}"),
|
||||
)
|
||||
steps.extend((
|
||||
Step("Publish advisor tool allowlist", (python, "tools/publish_advisor_demo_config.py", "--apply"), "python tools/publish_advisor_demo_config.py --apply"),
|
||||
Step("Audit database schema", (python, "tools/audit_schema.py"), "python tools/audit_schema.py"),
|
||||
Step("Audit database constraints", (python, "tools/audit_constraints.py"), "python tools/audit_constraints.py"),
|
||||
))
|
||||
return tuple(steps)
|
||||
|
||||
|
||||
def validate_args(args: argparse.Namespace) -> str | None:
|
||||
if (args.suitability_csv is None) != (args.contracts_csv is None):
|
||||
return "--suitability-csv and --contracts-csv must be supplied together"
|
||||
if args.market_days < 20:
|
||||
return "--market-days must be at least 20"
|
||||
if args.apply and not args.confirm_demo_database:
|
||||
return "--apply requires --confirm-demo-database"
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
error = validate_args(args)
|
||||
if error:
|
||||
print(f"error: {error}")
|
||||
return 2
|
||||
steps = build_steps(args)
|
||||
print("Target database must be newly created and disposable; never use legacy jr_agent.")
|
||||
if args.suitability_csv is None:
|
||||
print("warning: no reviewed product evidence supplied; recommendations remain fail-closed.")
|
||||
for index, step in enumerate(steps, start=1):
|
||||
print(f"[{index}/{len(steps)}] {step.label}\n $ {step.display}")
|
||||
if args.apply and subprocess.run(step.command, check=False).returncode != 0:
|
||||
print(f"failed: {step.label}")
|
||||
return 1
|
||||
if not args.apply:
|
||||
print("dry run complete; use --apply --confirm-demo-database after checking MYSQL_DSN and .env.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -45,6 +45,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
GRANT_SCRIPTS: tuple[tuple[str, str, str], ...] = (
|
||||
("tools/grant_advisor_role.py", "ADVISOR_PERMISSIONS", "投顾"),
|
||||
("tools/grant_customer_service_phase2_permissions.py", "PHASE2_PERMISSIONS", "客服二期"),
|
||||
("tools/grant_risk_permissions.py", "PERMISSIONS", "风控"),
|
||||
)
|
||||
|
||||
|
||||
@@ -94,6 +95,28 @@ def collect_findings() -> list[str]:
|
||||
if int(permission_id) not in seed_map:
|
||||
problems.append(f"CUSTOMER_PERMISSIONS 引用了不存在的 id {permission_id}")
|
||||
|
||||
for permission_id in seed.RISK_PERMISSIONS:
|
||||
if int(permission_id) not in seed_map:
|
||||
problems.append(f"RISK_PERMISSIONS 引用了不存在的 id {permission_id}")
|
||||
|
||||
required_risk_codes = {
|
||||
"risk:alert:read",
|
||||
"risk:alert:write",
|
||||
"risk:alert:scan",
|
||||
"risk:report:mail",
|
||||
}
|
||||
risk_codes = {
|
||||
seed_map[int(permission_id)]
|
||||
for permission_id in seed.RISK_PERMISSIONS
|
||||
if int(permission_id) in seed_map
|
||||
}
|
||||
missing_risk_codes = required_risk_codes - risk_codes
|
||||
if missing_risk_codes:
|
||||
problems.append(
|
||||
"RISK_PERMISSIONS 缺少风控权限码:"
|
||||
+ ", ".join(sorted(missing_risk_codes))
|
||||
)
|
||||
|
||||
return problems
|
||||
|
||||
|
||||
|
||||
@@ -29,13 +29,18 @@ import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.contracts import RequestContext
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.auth_service import hash_password
|
||||
from app.service.identity_service import IdentityService
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.core.contracts import RequestContext # noqa: E402
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.auth_service import hash_password # noqa: E402
|
||||
from app.service.identity_service import IdentityService # noqa: E402
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
||||
|
||||
@@ -46,12 +46,12 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from app.core.config import get_settings # noqa: E402
|
||||
|
||||
# (permission_code, resource, action, 授予的角色)
|
||||
PERMISSIONS: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
|
||||
("risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
|
||||
("risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
|
||||
("risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
|
||||
("risk:report:mail", "risk", "report", ("risk_operator", "admin")),
|
||||
# (permission_id, permission_code, resource, action, 授予的角色)
|
||||
PERMISSIONS: tuple[tuple[int, str, str, str, tuple[str, ...]], ...] = (
|
||||
(9047, "risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
|
||||
(9048, "risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
|
||||
(9049, "risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
|
||||
(9050, "risk:report:mail", "risk", "report", ("risk_operator", "admin")),
|
||||
)
|
||||
DATA_SCOPE = "all"
|
||||
|
||||
@@ -75,7 +75,7 @@ async def main() -> int:
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
|
||||
for code, resource, action, roles in PERMISSIONS:
|
||||
for permission_id, code, resource, action, roles in PERMISSIONS:
|
||||
await cursor.execute(
|
||||
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
|
||||
)
|
||||
@@ -83,18 +83,21 @@ async def main() -> int:
|
||||
if row is None:
|
||||
await cursor.execute(
|
||||
"INSERT INTO sys_permission"
|
||||
" (permission_code, resource, `action`, data_scope, created_at, updated_at)"
|
||||
" VALUES (%s, %s, %s, %s, %s, %s)",
|
||||
(code, resource, action, DATA_SCOPE, now, now),
|
||||
" (id, permission_code, resource, `action`, data_scope,"
|
||||
" created_at, updated_at)"
|
||||
" VALUES (%s, %s, %s, %s, %s, %s, %s)",
|
||||
(permission_id, code, resource, action, DATA_SCOPE, now, now),
|
||||
)
|
||||
await cursor.execute(
|
||||
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
print(f"[权限] 已创建 {code}(scope={DATA_SCOPE})")
|
||||
else:
|
||||
existing_id = int(row[0])
|
||||
if existing_id != permission_id:
|
||||
raise RuntimeError(
|
||||
f"权限码 {code} 已存在但主键为 {existing_id},"
|
||||
f"与约定主键 {permission_id} 不一致;请先执行 "
|
||||
"python tools/seed_test_rbac.py"
|
||||
)
|
||||
print(f"[权限] {code} 已存在")
|
||||
permission_id = int(row[0])
|
||||
|
||||
for role_code in roles:
|
||||
await cursor.execute("SELECT id FROM sys_role WHERE role_code = %s", (role_code,))
|
||||
@@ -122,5 +125,5 @@ async def main() -> int:
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
sys.exit(asyncio.run(main()))
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Publish the advisor Agent tool allowlist for an isolated demo environment.
|
||||
|
||||
The active ``config_release`` is environment data. A new demo database has no
|
||||
release by default, so AdvisorAgent tool calls would correctly fail closed even
|
||||
though the Agent code is present. This script creates a new release, carries
|
||||
forward every existing item, and replaces only advisor tool entries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import datetime as dt
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import asyncmy
|
||||
import httpx
|
||||
import jwt
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.core.config import get_settings # noqa: E402
|
||||
from app.main import create_app # noqa: E402
|
||||
|
||||
ADMIN_ID = "9003"
|
||||
AGENT_TYPE = "advisor"
|
||||
INTENT_TOOLS: dict[str, tuple[str, ...]] = {
|
||||
"fund_quote": ("query_fund_quote",),
|
||||
"investment_goal": ("query_investment_goal",),
|
||||
"portfolio_analysis": ("analyze_portfolio",),
|
||||
"asset_allocation": ("generate_asset_allocation",),
|
||||
"product_recommend": ("recommend_products",),
|
||||
"comparison": ("compare_products",),
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Publish advisor demo tool allowlist")
|
||||
parser.add_argument("--apply", action="store_true", help="Create, review, and activate release")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def issue_token(subject: str) -> str:
|
||||
settings = get_settings()
|
||||
private_key = Path(settings.jwt_private_key_path).read_text(encoding="utf-8")
|
||||
now = dt.datetime.now(dt.UTC)
|
||||
return jwt.encode(
|
||||
{
|
||||
"sub": subject,
|
||||
"iss": settings.jwt_issuer,
|
||||
"aud": settings.jwt_audience,
|
||||
"exp": now + dt.timedelta(minutes=30),
|
||||
"nbf": now - dt.timedelta(seconds=5),
|
||||
"jti": str(uuid.uuid4()),
|
||||
},
|
||||
private_key,
|
||||
algorithm="RS256",
|
||||
)
|
||||
|
||||
|
||||
async def active_items() -> list[dict[str, Any]]:
|
||||
settings = get_settings()
|
||||
dsn = settings.mysql_dsn.split("://", 1)[1]
|
||||
credentials, location = dsn.split("@", 1)
|
||||
user, password = credentials.split(":", 1)
|
||||
host_port, database = location.split("/", 1)
|
||||
host, _, port = host_port.partition(":")
|
||||
connection = await asyncmy.connect(
|
||||
host=host, port=int(port or 3306), user=user, password=password, db=database
|
||||
)
|
||||
try:
|
||||
cursor = connection.cursor()
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT i.namespace, i.config_key, i.value_json, i.schema_version
|
||||
FROM platform_config_item i
|
||||
JOIN config_release r ON r.id = i.release_id
|
||||
WHERE r.status = 'active'
|
||||
"""
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
return [
|
||||
{
|
||||
"namespace": namespace,
|
||||
"item_key": key,
|
||||
"value_json": json.loads(value) if isinstance(value, str) else value,
|
||||
"schema_version": schema_version,
|
||||
}
|
||||
for namespace, key, value, schema_version in rows
|
||||
]
|
||||
|
||||
|
||||
async def request(
|
||||
client: httpx.AsyncClient,
|
||||
path: str,
|
||||
*,
|
||||
auth: dict[str, str],
|
||||
payload: dict[str, object] | None = None,
|
||||
if_match: str | None = None,
|
||||
) -> httpx.Response:
|
||||
headers = {**auth, "Idempotency-Key": uuid.uuid4().hex}
|
||||
if if_match:
|
||||
headers["If-Match"] = if_match
|
||||
return await client.post(path, json=payload, headers=headers)
|
||||
|
||||
|
||||
async def etag(client: httpx.AsyncClient, path: str, auth: dict[str, str]) -> str | None:
|
||||
return (await client.get(path, headers=auth)).headers.get("ETag")
|
||||
|
||||
|
||||
def advisor_items() -> list[dict[str, object]]:
|
||||
return [
|
||||
{
|
||||
"namespace": "agent_tools",
|
||||
"item_key": f"{AGENT_TYPE}:{intent}",
|
||||
"value_json": {"allowed_tools": list(tools)},
|
||||
"schema_version": "1",
|
||||
}
|
||||
for intent, tools in INTENT_TOOLS.items()
|
||||
]
|
||||
|
||||
|
||||
async def publish() -> int:
|
||||
app = create_app()
|
||||
auth = {"Authorization": f"Bearer {issue_token(ADMIN_ID)}"}
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
|
||||
inherited = await active_items()
|
||||
replacement_keys = {("agent_tools", str(item["item_key"])) for item in advisor_items()}
|
||||
carried = [
|
||||
item
|
||||
for item in inherited
|
||||
if (str(item["namespace"]), str(item["item_key"])) not in replacement_keys
|
||||
]
|
||||
created = await request(
|
||||
client,
|
||||
"/api/v1/admin/config-releases",
|
||||
auth=auth,
|
||||
payload={
|
||||
"release_no": f"advisor-demo-{uuid.uuid4().hex[:12]}",
|
||||
"title": "Advisor demo tool allowlist",
|
||||
"change_summary": "Publish advisor tool allowlists for the isolated demo environment",
|
||||
},
|
||||
)
|
||||
if created.status_code != 201:
|
||||
print(f"create release failed: {created.status_code} {created.text[:240]}")
|
||||
return 1
|
||||
release_id = int(created.json()["data"]["id"])
|
||||
base = f"/api/v1/admin/config-releases/{release_id}"
|
||||
for item in [*carried, *advisor_items()]:
|
||||
response = await request(
|
||||
client, f"{base}/platform-config-items", auth=auth, payload=item
|
||||
)
|
||||
if response.status_code != 201:
|
||||
print(f"write {item['namespace']}/{item['item_key']} failed: {response.status_code}")
|
||||
return 1
|
||||
for suffix, payload in (
|
||||
("validations", {}),
|
||||
("reviews", {"decision": "approved", "comment": "isolated demo setup"}),
|
||||
("activations", {}),
|
||||
):
|
||||
response = await request(
|
||||
client,
|
||||
f"{base}/{suffix}",
|
||||
auth=auth,
|
||||
payload=payload,
|
||||
if_match=await etag(client, base, auth),
|
||||
)
|
||||
if response.status_code not in (200, 201):
|
||||
print(f"{suffix} failed: {response.status_code} {response.text[:240]}")
|
||||
return 1
|
||||
print(f"published advisor demo release id={release_id}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
for intent, tools in INTENT_TOOLS.items():
|
||||
print(f"advisor:{intent} -> {', '.join(tools)}")
|
||||
if not args.apply:
|
||||
print("dry run only; pass --apply to publish a new config release")
|
||||
return 0
|
||||
return asyncio.run(publish())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -5,11 +5,17 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.infrastructure.db import SessionFactory
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
|
||||
ADMIN_USER_ID = 9003
|
||||
ENDPOINT_CODE = "deepseek-flash"
|
||||
@@ -53,7 +59,12 @@ async def seed() -> None:
|
||||
status='active', reviewer_id=:admin_id, reviewed_at=:now, updated_at=:now
|
||||
"""), {
|
||||
"endpoint_code": ENDPOINT_CODE,
|
||||
"capabilities": json.dumps(["chat", "intent_classification", "risk_answer"]),
|
||||
"capabilities": json.dumps([
|
||||
"chat",
|
||||
"intent_classification",
|
||||
"risk_answer",
|
||||
"text_generation",
|
||||
]),
|
||||
"data_levels": json.dumps(["internal"]),
|
||||
"admin_id": ADMIN_USER_ID,
|
||||
"now": now,
|
||||
|
||||
+39
-4
@@ -26,13 +26,19 @@ id BETWEEN 9001 AND 9099` 会删掉该号段内**所有**权限(包括别的
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.contracts import RequestContext
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.identity_service import IdentityService
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.core.contracts import RequestContext # noqa: E402
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.identity_service import IdentityService # noqa: E402
|
||||
|
||||
CUSTOMER_USER = "9001"
|
||||
RISK_USER = "9002"
|
||||
@@ -94,6 +100,14 @@ PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = (
|
||||
(9044, "memory:candidate:confirm", "memory:candidate", "confirm", "self"),
|
||||
(9045, "memory:candidate:review", "memory:candidate", "review", "all"),
|
||||
(9046, "handover:read", "handover", "read", "all"),
|
||||
# ---- 9047-9050:风控模块四个权限码 ----
|
||||
# 风控 Service 和 Agent 工具都按这四个权限码失败关闭。此前依赖
|
||||
# `grant_risk_permissions.py` 临时补种,重跑本脚本时会删除 9001-9099 号段内的
|
||||
# 风控权限,导致登录成功后风控接口全部 403。这里纳入唯一权限定义源。
|
||||
(9047, "risk:alert:read", "risk", "alert", "all"),
|
||||
(9048, "risk:alert:write", "risk", "alert", "all"),
|
||||
(9049, "risk:alert:scan", "risk", "alert", "all"),
|
||||
(9050, "risk:report:mail", "risk", "report", "all"),
|
||||
)
|
||||
|
||||
# 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。
|
||||
@@ -104,7 +118,10 @@ CUSTOMER_PERMISSIONS = (
|
||||
9044,
|
||||
)
|
||||
# 风控专员:业务侧只读 + 跨客户记忆 + 审计只读,不含配置写权限。
|
||||
RISK_PERMISSIONS = (9001, 9002, 9003, 9010, 9011, 9012)
|
||||
RISK_PERMISSIONS = (
|
||||
9001, 9002, 9003, 9010, 9011, 9012,
|
||||
9047, 9048, 9049, 9050,
|
||||
)
|
||||
# 平台管理员:管理面全套(配置发布四态 + 模型端点 + 审计)。
|
||||
ADMIN_PERMISSIONS = tuple(row[0] for row in PERMISSIONS)
|
||||
|
||||
@@ -159,6 +176,24 @@ async def seed() -> None:
|
||||
text("DELETE FROM sys_role_permission WHERE role_id IN (9001,9002,9003)")
|
||||
)
|
||||
await session.execute(text("DELETE FROM sys_user_role WHERE user_id IN (9001,9002,9003)"))
|
||||
# 风控权限在旧环境中可能由 `grant_risk_permissions.py` 自动分配了任意主键。
|
||||
# 先按 permission_code 清掉角色绑定和旧主键,保证本次能用固定号段重建;
|
||||
# 否则 `risk:report:mail` 已存在于 9001-9099 之外时会触发 permission_code 唯一键冲突。
|
||||
await session.execute(text("""
|
||||
DELETE rp FROM sys_role_permission rp
|
||||
JOIN sys_permission p ON p.id = rp.permission_id
|
||||
WHERE p.permission_code IN (
|
||||
'risk:alert:read', 'risk:alert:write',
|
||||
'risk:alert:scan', 'risk:report:mail'
|
||||
)
|
||||
"""))
|
||||
await session.execute(text("""
|
||||
DELETE FROM sys_permission
|
||||
WHERE permission_code IN (
|
||||
'risk:alert:read', 'risk:alert:write',
|
||||
'risk:alert:scan', 'risk:report:mail'
|
||||
)
|
||||
"""))
|
||||
await session.execute(text("DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099"))
|
||||
await session.execute(text("DELETE FROM sys_role WHERE id IN (9001,9002,9003)"))
|
||||
# 不再 DELETE sys_user:投顾域的 advisor_profile_tag 等表用 FK 引用它,库里有数据引用
|
||||
|
||||
@@ -28,11 +28,16 @@ import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.service.auth_service import hash_password
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
from app.service.auth_service import hash_password # noqa: E402
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
|
||||
|
||||
Reference in New Issue
Block a user