feat: multi-region rule isolation — region column + config + queries
- DB: add region column to leaudit_rule_sets + leaudit_rule_type_bindings - DB: change UNIQUE constraint from (rule_type) to (rule_type, region) - Config: add APP_REGION to app.toml + AppSettings + __init__.pyi - AuditServiceImpl: filter bindings by APP_REGION - RuleServiceImpl: ListBindings/CreateBinding use APP_REGION - Seed script: accept --region arg, tag rules by region - OssPathUtils: BuildRuleYamlKey already accepts Region parameter Each region can now have its own independent copy of the same rule_type, stored in separate OSS paths and DB rows, keyed by region.
This commit is contained in:
+24
-15
@@ -2,10 +2,11 @@
|
||||
|
||||
用法: cd /home/wren-dev/Porject/leaudit-platform
|
||||
PYTHONPATH=src:/home/wren-dev/Porject/docauditai \
|
||||
python scripts/m4_seed_rules.py
|
||||
python scripts/m4_seed_rules.py [--region default]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import os
|
||||
@@ -23,6 +24,8 @@ from sqlalchemy import text
|
||||
|
||||
RULES_DIR = Path(__file__).resolve().parent.parent / "rules"
|
||||
|
||||
REGION = "default"
|
||||
|
||||
|
||||
def _read_metadata(yaml_path: Path) -> dict:
|
||||
import yaml
|
||||
@@ -37,11 +40,11 @@ def _type_id_to_rule_type(type_id: str) -> str:
|
||||
|
||||
|
||||
async def _get_or_create_rule_set(
|
||||
session, rule_type: str, rule_name: str, domain_type: str, description: str
|
||||
session, rule_type: str, rule_name: str, domain_type: str, description: str, region: str
|
||||
) -> dict:
|
||||
result = await session.execute(
|
||||
text("SELECT * FROM leaudit_rule_sets WHERE rule_type = :rt AND deleted_at IS NULL LIMIT 1"),
|
||||
{"rt": rule_type},
|
||||
text("SELECT * FROM leaudit_rule_sets WHERE rule_type = :rt AND region = :rgn AND deleted_at IS NULL LIMIT 1"),
|
||||
{"rt": rule_type, "rgn": region},
|
||||
)
|
||||
row = result.mappings().first()
|
||||
if row:
|
||||
@@ -49,11 +52,11 @@ async def _get_or_create_rule_set(
|
||||
|
||||
result = await session.execute(
|
||||
text(
|
||||
"""INSERT INTO leaudit_rule_sets (rule_type, rule_name, domain_type, description, status, is_builtin)
|
||||
VALUES (:rt, :rn, :dt, :desc, 'draft', false)
|
||||
"""INSERT INTO leaudit_rule_sets (rule_type, rule_name, domain_type, description, region, status, is_builtin)
|
||||
VALUES (:rt, :rn, :dt, :desc, :rgn, 'draft', false)
|
||||
RETURNING id, rule_type, rule_name, domain_type, current_version_id, status"""
|
||||
),
|
||||
{"rt": rule_type, "rn": rule_name, "dt": domain_type, "desc": description or ""},
|
||||
{"rt": rule_type, "rn": rule_name, "dt": domain_type, "desc": description or "", "rgn": region},
|
||||
)
|
||||
return dict(result.mappings().first())
|
||||
|
||||
@@ -66,7 +69,7 @@ async def _version_exists(session, rule_set_id: int, version_no: str) -> bool:
|
||||
return result.mappings().first() is not None
|
||||
|
||||
|
||||
async def upload_one(yaml_path: Path, oss: OssClient) -> dict:
|
||||
async def upload_one(yaml_path: Path, oss: OssClient, region: str) -> dict:
|
||||
meta = _read_metadata(yaml_path)
|
||||
type_id = meta.get("type_id", "")
|
||||
rule_type = _type_id_to_rule_type(type_id)
|
||||
@@ -80,13 +83,13 @@ async def upload_one(yaml_path: Path, oss: OssClient) -> dict:
|
||||
file_size = len(yaml_text.encode())
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
rs = await _get_or_create_rule_set(session, rule_type, rule_name, domain_type, description)
|
||||
rs = await _get_or_create_rule_set(session, rule_type, rule_name, domain_type, description, region)
|
||||
|
||||
if await _version_exists(session, rs["id"], version_no):
|
||||
return {"rule_type": rule_type, "status": "skipped", "reason": f"version {version_no} exists"}
|
||||
|
||||
# Upload to OSS
|
||||
object_key = OssPathUtils.BuildRuleYamlKey(rule_type, version_no)
|
||||
object_key = OssPathUtils.BuildRuleYamlKey(rule_type, version_no, Region=region)
|
||||
oss_url = oss.UploadText(
|
||||
ObjectKey=object_key,
|
||||
Content=yaml_text,
|
||||
@@ -122,11 +125,11 @@ async def upload_one(yaml_path: Path, oss: OssClient) -> dict:
|
||||
return {"rule_type": rule_type, "status": "created", "version": version_no, "oss_url": oss_url}
|
||||
|
||||
|
||||
async def publish_one(rule_type: str, version_no: str | None = None) -> dict:
|
||||
async def publish_one(rule_type: str, region: str, version_no: str | None = None) -> dict:
|
||||
async with GetAsyncSession() as session:
|
||||
rs = await session.execute(
|
||||
text("SELECT id FROM leaudit_rule_sets WHERE rule_type = :rt AND deleted_at IS NULL LIMIT 1"),
|
||||
{"rt": rule_type},
|
||||
text("SELECT id FROM leaudit_rule_sets WHERE rule_type = :rt AND region = :rgn AND deleted_at IS NULL LIMIT 1"),
|
||||
{"rt": rule_type, "rgn": region},
|
||||
)
|
||||
rs_row = rs.mappings().first()
|
||||
if not rs_row:
|
||||
@@ -161,9 +164,15 @@ async def publish_one(rule_type: str, version_no: str | None = None) -> dict:
|
||||
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="M4 种子数据初始化")
|
||||
parser.add_argument("--region", default="default", help="地区标识(默认: default)")
|
||||
args = parser.parse_args()
|
||||
region = args.region
|
||||
|
||||
oss = OssClient()
|
||||
rules_dirs = sorted(d for d in RULES_DIR.iterdir() if d.is_dir() and (d / "rules.yaml").exists())
|
||||
|
||||
print(f"地区: {region}")
|
||||
print(f"找到 {len(rules_dirs)} 套规则\n")
|
||||
|
||||
# Step 1: Upload all
|
||||
@@ -173,7 +182,7 @@ async def main():
|
||||
for d in rules_dirs:
|
||||
yaml_path = d / "rules.yaml"
|
||||
try:
|
||||
result = await upload_one(yaml_path, oss)
|
||||
result = await upload_one(yaml_path, oss, region)
|
||||
icon = "✓" if result["status"] != "skipped" else "⊙"
|
||||
print(f" {icon} {result['rule_type']:40s} {result['status']:8s} v{result.get('version', '?')}")
|
||||
except Exception as e:
|
||||
@@ -189,7 +198,7 @@ async def main():
|
||||
type_id = meta.get("type_id", "")
|
||||
rule_type = _type_id_to_rule_type(type_id)
|
||||
try:
|
||||
result = await publish_one(rule_type)
|
||||
result = await publish_one(rule_type, region)
|
||||
icon = "✓" if result["status"] == "published" else "✗"
|
||||
print(f" {icon} {rule_type:40s} {result['status']}")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user