feat: add tenant-scoped rule and permission management
This commit is contained in:
@@ -0,0 +1,591 @@
|
||||
"""Import local rule YAML files into OSS and rule version tables.
|
||||
|
||||
This script is intentionally conservative:
|
||||
- uploads every local rules.yaml to the canonical OSS key;
|
||||
- upserts matching rule version rows by tenant/rule_type/version_no;
|
||||
- switches each rule set to the highest local version for that rule_type;
|
||||
- does not delete old OSS objects or historical DB versions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from sqlalchemy import text
|
||||
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_storage.oss_path_utils import OssPathUtils
|
||||
from fastapi_modules.fastapi_leaudit.leaudit_bridge.ruleValidator import RuleValidator
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ossServiceImpl import OssServiceImpl
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LocalRule:
|
||||
rule_type: str
|
||||
version_no: str
|
||||
rule_name: str
|
||||
description: str | None
|
||||
path: Path
|
||||
yaml_text: str
|
||||
sha256: str
|
||||
size: int
|
||||
|
||||
|
||||
def _version_sort_key(value: str) -> tuple[int, tuple[int | str, ...]]:
|
||||
raw = str(value or "").strip()
|
||||
normalized = raw[1:] if raw.lower().startswith("v") else raw
|
||||
parts: list[int | str] = []
|
||||
for part in re.split(r"([0-9]+)", normalized):
|
||||
if not part:
|
||||
continue
|
||||
parts.append(int(part) if part.isdigit() else part)
|
||||
return (0 if raw.lower().startswith("v") else 1, tuple(parts))
|
||||
|
||||
|
||||
def _domain_type(rule_type: str) -> str:
|
||||
if rule_type.startswith("contract."):
|
||||
return "contract"
|
||||
if rule_type.startswith("govdoc."):
|
||||
return "govdoc"
|
||||
if rule_type.startswith("行政卷宗."):
|
||||
return "case_file"
|
||||
return "custom"
|
||||
|
||||
|
||||
def _legacy_region_for_tenant(tenant_code: str) -> str:
|
||||
normalized = str(tenant_code or "").strip().upper()
|
||||
return normalized or "PUBLIC"
|
||||
|
||||
|
||||
def load_local_rules(root: Path) -> list[LocalRule]:
|
||||
validator = RuleValidator()
|
||||
items: list[LocalRule] = []
|
||||
failures: list[str] = []
|
||||
for path in sorted(root.glob("*/*/rules.yaml")):
|
||||
yaml_text = path.read_text(encoding="utf-8")
|
||||
result = validator.ValidateYaml(yaml_text)
|
||||
if not result.valid:
|
||||
failures.append(f"{path}: {'; '.join(result.errors or [])}")
|
||||
continue
|
||||
data = yaml.safe_load(yaml_text)
|
||||
metadata = data.get("metadata") or {}
|
||||
rule_type = str(metadata.get("type_id") or "").strip()
|
||||
version_no = str(metadata.get("version") or "").strip()
|
||||
if not rule_type or not version_no:
|
||||
failures.append(f"{path}: metadata.type_id/version is required")
|
||||
continue
|
||||
if version_no != path.parent.name:
|
||||
failures.append(f"{path}: metadata.version={version_no!r} does not match directory {path.parent.name!r}")
|
||||
continue
|
||||
items.append(
|
||||
LocalRule(
|
||||
rule_type=rule_type,
|
||||
version_no=version_no,
|
||||
rule_name=str(metadata.get("name") or rule_type),
|
||||
description=metadata.get("description"),
|
||||
path=path,
|
||||
yaml_text=yaml_text,
|
||||
sha256=hashlib.sha256(yaml_text.encode("utf-8")).hexdigest(),
|
||||
size=len(yaml_text.encode("utf-8")),
|
||||
)
|
||||
)
|
||||
if failures:
|
||||
raise RuntimeError("Local YAML validation failed:\n" + "\n".join(failures))
|
||||
return items
|
||||
|
||||
|
||||
async def _load_rule_sets(session) -> list[dict[str, Any]]:
|
||||
rows = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT
|
||||
rs.id,
|
||||
rs.rule_type,
|
||||
rs.rule_name,
|
||||
rs.current_version_id,
|
||||
COALESCE(NULLIF(BTRIM(rs.tenant_code), ''), 'PUBLIC') AS tenant_code,
|
||||
COALESCE(NULLIF(BTRIM(rs.scope_type), ''), 'PUBLIC') AS scope_type
|
||||
FROM leaudit_rule_sets rs
|
||||
WHERE rs.deleted_at IS NULL
|
||||
ORDER BY rs.tenant_code ASC, rs.rule_type ASC, rs.id ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def _tenant_name(session, tenant_code: str) -> str | None:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT tenant_name
|
||||
FROM sys_tenants
|
||||
WHERE tenant_code = :tenant_code
|
||||
AND deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"tenant_code": tenant_code},
|
||||
)
|
||||
).mappings().first()
|
||||
return str(row["tenant_name"]) if row else None
|
||||
|
||||
|
||||
async def _ensure_rule_set(session, local: LocalRule, tenant_code: str) -> dict[str, Any]:
|
||||
row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, rule_type, rule_name, current_version_id, tenant_code, scope_type
|
||||
FROM leaudit_rule_sets
|
||||
WHERE rule_type = :rule_type
|
||||
AND tenant_code = :tenant_code
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"rule_type": local.rule_type, "tenant_code": tenant_code},
|
||||
)
|
||||
).mappings().first()
|
||||
if row:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_sets
|
||||
SET rule_name = :rule_name,
|
||||
domain_type = :domain_type,
|
||||
description = :description,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": int(row["id"]),
|
||||
"rule_name": local.rule_name,
|
||||
"domain_type": _domain_type(local.rule_type),
|
||||
"description": local.description,
|
||||
},
|
||||
)
|
||||
return dict(row)
|
||||
|
||||
tenant_name = await _tenant_name(session, tenant_code)
|
||||
scope_type = "PUBLIC" if tenant_code == "PUBLIC" else "TENANT"
|
||||
created = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_rule_sets (
|
||||
rule_type, rule_name, domain_type, description, entry_module,
|
||||
current_version_id, status, is_builtin, owner_user_id,
|
||||
created_at, updated_at, deleted_at, region,
|
||||
tenant_code, scope_type, source_rule_set_id, tenant_name_snapshot
|
||||
) VALUES (
|
||||
:rule_type, :rule_name, :domain_type, :description, NULL,
|
||||
NULL, 'draft', FALSE, NULL,
|
||||
NOW(), NOW(), NULL, :region,
|
||||
:tenant_code, :scope_type, NULL, :tenant_name
|
||||
)
|
||||
RETURNING id, rule_type, rule_name, current_version_id, tenant_code, scope_type
|
||||
"""
|
||||
),
|
||||
{
|
||||
"rule_type": local.rule_type,
|
||||
"rule_name": local.rule_name,
|
||||
"domain_type": _domain_type(local.rule_type),
|
||||
"description": local.description,
|
||||
"region": _legacy_region_for_tenant(tenant_code),
|
||||
"tenant_code": tenant_code,
|
||||
"scope_type": scope_type,
|
||||
"tenant_name": tenant_name,
|
||||
},
|
||||
)
|
||||
).mappings().first()
|
||||
return dict(created)
|
||||
|
||||
|
||||
async def _upsert_version(session, local: LocalRule, rule_set_id: int, tenant_code: str, oss_url: str) -> tuple[int, str]:
|
||||
existing = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, file_sha256, status, version_seq
|
||||
FROM leaudit_rule_versions
|
||||
WHERE rule_set_id = :rule_set_id
|
||||
AND version_no = :version_no
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"rule_set_id": rule_set_id, "version_no": local.version_no},
|
||||
)
|
||||
).mappings().first()
|
||||
if existing:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_versions
|
||||
SET source_type = 'oss_yaml',
|
||||
dsl_format = 'yaml',
|
||||
oss_url = :oss_url,
|
||||
file_sha256 = :file_sha256,
|
||||
file_size = :file_size,
|
||||
metadata_type_id = :metadata_type_id,
|
||||
metadata_name = :metadata_name,
|
||||
metadata_version = :metadata_version,
|
||||
tenant_code_snapshot = :tenant_code,
|
||||
scope_type_snapshot = :scope_type,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"id": int(existing["id"]),
|
||||
"oss_url": oss_url,
|
||||
"file_sha256": local.sha256,
|
||||
"file_size": local.size,
|
||||
"metadata_type_id": local.rule_type,
|
||||
"metadata_name": local.rule_name,
|
||||
"metadata_version": local.version_no,
|
||||
"tenant_code": tenant_code,
|
||||
"scope_type": "PUBLIC" if tenant_code == "PUBLIC" else "TENANT",
|
||||
},
|
||||
)
|
||||
action = "updated" if existing["file_sha256"] != local.sha256 else "refreshed"
|
||||
return int(existing["id"]), action
|
||||
|
||||
seq_row = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT COALESCE(MAX(version_seq), 0) + 1 AS next_seq
|
||||
FROM leaudit_rule_versions
|
||||
WHERE rule_set_id = :rule_set_id
|
||||
"""
|
||||
),
|
||||
{"rule_set_id": rule_set_id},
|
||||
)
|
||||
).mappings().first()
|
||||
created = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO leaudit_rule_versions (
|
||||
rule_set_id, version_no, version_seq, status, source_type, dsl_format,
|
||||
oss_url, file_sha256, file_size, local_cache_path,
|
||||
metadata_type_id, metadata_name, metadata_version, change_note,
|
||||
editor_user_id, publisher_user_id, published_at,
|
||||
created_at, updated_at, deleted_at,
|
||||
tenant_code_snapshot, scope_type_snapshot, source_version_id
|
||||
) VALUES (
|
||||
:rule_set_id, :version_no, :version_seq, 'draft', 'oss_yaml', 'yaml',
|
||||
:oss_url, :file_sha256, :file_size, NULL,
|
||||
:metadata_type_id, :metadata_name, :metadata_version, :change_note,
|
||||
NULL, NULL, NULL,
|
||||
NOW(), NOW(), NULL,
|
||||
:tenant_code, :scope_type, NULL
|
||||
)
|
||||
RETURNING id
|
||||
"""
|
||||
),
|
||||
{
|
||||
"rule_set_id": rule_set_id,
|
||||
"version_no": local.version_no,
|
||||
"version_seq": int(seq_row["next_seq"] or 1),
|
||||
"oss_url": oss_url,
|
||||
"file_sha256": local.sha256,
|
||||
"file_size": local.size,
|
||||
"metadata_type_id": local.rule_type,
|
||||
"metadata_name": local.rule_name,
|
||||
"metadata_version": local.version_no,
|
||||
"change_note": "从 leaudit-oss-yaml-files 全量导入",
|
||||
"tenant_code": tenant_code,
|
||||
"scope_type": "PUBLIC" if tenant_code == "PUBLIC" else "TENANT",
|
||||
},
|
||||
)
|
||||
).mappings().first()
|
||||
return int(created["id"]), "created"
|
||||
|
||||
|
||||
async def import_rules(root: Path, dry_run: bool) -> None:
|
||||
locals_by_type: dict[str, list[LocalRule]] = {}
|
||||
for local in load_local_rules(root):
|
||||
locals_by_type.setdefault(local.rule_type, []).append(local)
|
||||
for rules in locals_by_type.values():
|
||||
rules.sort(key=lambda item: _version_sort_key(item.version_no))
|
||||
|
||||
oss = OssServiceImpl()
|
||||
async with GetAsyncSession() as session:
|
||||
rule_sets = await _load_rule_sets(session)
|
||||
tenant_codes = sorted({str(row["tenant_code"]).strip().upper() for row in rule_sets if row.get("tenant_code")})
|
||||
tenant_codes = [code for code in tenant_codes if code != "PROVINCIAL"]
|
||||
if "PUBLIC" not in tenant_codes:
|
||||
tenant_codes.insert(0, "PUBLIC")
|
||||
|
||||
print(f"local_rule_files={sum(len(v) for v in locals_by_type.values())}")
|
||||
print(f"local_rule_types={len(locals_by_type)}")
|
||||
print(f"target_tenants={','.join(tenant_codes)}")
|
||||
|
||||
stats = {"uploaded": 0, "created_versions": 0, "updated_versions": 0, "refreshed_versions": 0, "published_sets": 0}
|
||||
for tenant_code in tenant_codes:
|
||||
for rule_type, versions in sorted(locals_by_type.items()):
|
||||
current_version_id: int | None = None
|
||||
rule_set_id: int | None = None
|
||||
for local in versions:
|
||||
object_key = OssPathUtils.BuildRuleYamlKey(local.rule_type, local.version_no)
|
||||
if dry_run:
|
||||
oss_url = object_key
|
||||
else:
|
||||
oss_url = await oss.UploadText(
|
||||
ObjectKey=object_key,
|
||||
Content=local.yaml_text,
|
||||
ContentType="application/x-yaml; charset=utf-8",
|
||||
)
|
||||
stats["uploaded"] += 1
|
||||
rule_set = await _ensure_rule_set(session, local, tenant_code)
|
||||
rule_set_id = int(rule_set["id"])
|
||||
version_id, action = await _upsert_version(session, local, rule_set_id, tenant_code, oss_url)
|
||||
if action == "created":
|
||||
stats["created_versions"] += 1
|
||||
elif action == "updated":
|
||||
stats["updated_versions"] += 1
|
||||
else:
|
||||
stats["refreshed_versions"] += 1
|
||||
if local is versions[-1]:
|
||||
current_version_id = version_id
|
||||
|
||||
if rule_set_id is not None and current_version_id is not None:
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_versions
|
||||
SET status = CASE
|
||||
WHEN id = :current_version_id THEN 'published'
|
||||
WHEN status = 'published' THEN 'deprecated'
|
||||
ELSE status
|
||||
END,
|
||||
published_at = CASE
|
||||
WHEN id = :current_version_id AND published_at IS NULL THEN NOW()
|
||||
ELSE published_at
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE rule_set_id = :rule_set_id
|
||||
AND deleted_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"rule_set_id": rule_set_id, "current_version_id": current_version_id},
|
||||
)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_sets
|
||||
SET current_version_id = :current_version_id,
|
||||
status = 'active',
|
||||
updated_at = NOW()
|
||||
WHERE id = :rule_set_id
|
||||
"""
|
||||
),
|
||||
{"rule_set_id": rule_set_id, "current_version_id": current_version_id},
|
||||
)
|
||||
stats["published_sets"] += 1
|
||||
|
||||
if dry_run:
|
||||
await session.rollback()
|
||||
print("dry_run=true rolled_back=true")
|
||||
else:
|
||||
await session.commit()
|
||||
print("dry_run=false committed=true")
|
||||
for key, value in stats.items():
|
||||
print(f"{key}={value}")
|
||||
|
||||
|
||||
async def _backup_rule_domain(session) -> Path:
|
||||
backup_dir = Path("docs/规则编辑/backups")
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
backup_path = backup_dir / f"rule-domain-before-reset-{stamp}.sql"
|
||||
rule_sets = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT *
|
||||
FROM leaudit_rule_sets
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
rule_versions = (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT *
|
||||
FROM leaudit_rule_versions
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
lines = [
|
||||
"-- Rule domain backup before reset",
|
||||
f"-- generated_at: {datetime.now().isoformat(timespec='seconds')}",
|
||||
f"-- rule_sets: {len(rule_sets)}",
|
||||
f"-- rule_versions: {len(rule_versions)}",
|
||||
"",
|
||||
"-- This file is an audit snapshot, not an automatic restore script.",
|
||||
"-- Use the rows below to inspect pre-reset IDs, current_version_id, oss_url and sha.",
|
||||
"",
|
||||
]
|
||||
for row in rule_sets:
|
||||
lines.append(
|
||||
"-- rule_set "
|
||||
+ " ".join(
|
||||
[
|
||||
f"id={row.get('id')}",
|
||||
f"tenant_code={row.get('tenant_code')}",
|
||||
f"rule_type={row.get('rule_type')}",
|
||||
f"current_version_id={row.get('current_version_id')}",
|
||||
f"status={row.get('status')}",
|
||||
f"deleted_at={row.get('deleted_at')}",
|
||||
]
|
||||
)
|
||||
)
|
||||
lines.append("")
|
||||
for row in rule_versions:
|
||||
lines.append(
|
||||
"-- rule_version "
|
||||
+ " ".join(
|
||||
[
|
||||
f"id={row.get('id')}",
|
||||
f"rule_set_id={row.get('rule_set_id')}",
|
||||
f"version_no={row.get('version_no')}",
|
||||
f"status={row.get('status')}",
|
||||
f"oss_url={row.get('oss_url')}",
|
||||
f"sha={row.get('file_sha256')}",
|
||||
f"deleted_at={row.get('deleted_at')}",
|
||||
]
|
||||
)
|
||||
)
|
||||
backup_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return backup_path
|
||||
|
||||
|
||||
async def reset_and_import_rules(root: Path, *, dry_run: bool, prune_oss: bool) -> None:
|
||||
local_rules = load_local_rules(root)
|
||||
canonical_keys = {
|
||||
OssPathUtils.BuildRuleYamlKey(local.rule_type, local.version_no)
|
||||
for local in local_rules
|
||||
}
|
||||
async with GetAsyncSession() as session:
|
||||
backup_path = await _backup_rule_domain(session)
|
||||
referenced_keys = {
|
||||
str(row["oss_url"])
|
||||
for row in (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT rule_source_oss_url AS oss_url
|
||||
FROM leaudit_audit_runs
|
||||
WHERE COALESCE(rule_source_oss_url, '') <> ''
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
}
|
||||
existing_version_keys = {
|
||||
str(row["oss_url"])
|
||||
for row in (
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT oss_url
|
||||
FROM leaudit_rule_versions
|
||||
WHERE COALESCE(oss_url, '') <> ''
|
||||
"""
|
||||
)
|
||||
)
|
||||
).mappings().all()
|
||||
}
|
||||
deletable_oss_keys = sorted(existing_version_keys - canonical_keys - referenced_keys)
|
||||
print(f"backup_path={backup_path}")
|
||||
print(f"canonical_keys={len(canonical_keys)}")
|
||||
print(f"existing_version_keys={len(existing_version_keys)}")
|
||||
print(f"audit_referenced_keys={len(referenced_keys)}")
|
||||
print(f"deletable_oss_keys={len(deletable_oss_keys)}")
|
||||
for key in deletable_oss_keys[:50]:
|
||||
print(f"deletable_oss_key={key}")
|
||||
|
||||
if dry_run:
|
||||
await session.rollback()
|
||||
print("reset_dry_run=true rolled_back=true")
|
||||
return
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_sets
|
||||
SET current_version_id = NULL,
|
||||
status = 'draft',
|
||||
updated_at = NOW()
|
||||
WHERE deleted_at IS NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE leaudit_rule_versions
|
||||
SET deleted_at = NOW(),
|
||||
status = CASE WHEN status = 'published' THEN 'deprecated' ELSE status END,
|
||||
updated_at = NOW()
|
||||
WHERE deleted_at IS NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
if prune_oss and deletable_oss_keys:
|
||||
oss = OssServiceImpl()
|
||||
client = oss.Client._GetMinioClient()
|
||||
bucket = oss.Client.bucket
|
||||
deleted = 0
|
||||
for key in deletable_oss_keys:
|
||||
try:
|
||||
client.remove_object(bucket, key)
|
||||
deleted += 1
|
||||
except Exception as exc:
|
||||
print(f"delete_oss_failed key={key} error={exc}")
|
||||
print(f"oss_deleted={deleted}")
|
||||
elif prune_oss:
|
||||
print("oss_deleted=0")
|
||||
|
||||
await import_rules(root, dry_run=False)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", default="leaudit-oss-yaml-files")
|
||||
parser.add_argument("--execute", action="store_true")
|
||||
parser.add_argument("--reset-rule-domain", action="store_true")
|
||||
parser.add_argument("--prune-oss", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.reset_rule_domain:
|
||||
asyncio.run(reset_and_import_rules(Path(args.root), dry_run=not args.execute, prune_oss=args.prune_oss))
|
||||
else:
|
||||
asyncio.run(import_rules(Path(args.root), dry_run=not args.execute))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
用法:
|
||||
scripts/run_rag_public_orphan_defaults_migration.sh [连接参数] [--migrate --yes]
|
||||
|
||||
连接方式二选一:
|
||||
1. 使用 DATABASE_URL:
|
||||
DATABASE_URL='postgresql://user:password@host:5432/dbname' scripts/run_rag_public_orphan_defaults_migration.sh
|
||||
|
||||
2. 使用 psql 参数:
|
||||
scripts/run_rag_public_orphan_defaults_migration.sh -h <host> -U <user> -d <db_name> [-p <port>]
|
||||
|
||||
默认行为:
|
||||
只执行迁移前检查,不修改数据库。
|
||||
|
||||
执行迁移:
|
||||
先确认 precheck 输出符合预期,再追加 --migrate --yes:
|
||||
scripts/run_rag_public_orphan_defaults_migration.sh -h <host> -U <user> -d <db_name> --migrate --yes
|
||||
|
||||
环境变量:
|
||||
PGPASSWORD 可用于非交互输入数据库密码。
|
||||
USAGE
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
PRECHECK_SQL="${REPO_ROOT}/scripts/创建sql/precheck_rag_public_orphan_defaults.sql"
|
||||
MIGRATE_SQL="${REPO_ROOT}/scripts/创建sql/migrate_rag_public_orphan_defaults.sql"
|
||||
LOG_DIR="${REPO_ROOT}/logs/rag-public-orphan-defaults"
|
||||
|
||||
PSQL_ARGS=()
|
||||
RUN_MIGRATION=false
|
||||
CONFIRMED=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--host)
|
||||
[[ $# -ge 2 ]] || { echo "缺少 $1 的值" >&2; exit 2; }
|
||||
PSQL_ARGS+=("-h" "$2")
|
||||
shift 2
|
||||
;;
|
||||
-U|--username)
|
||||
[[ $# -ge 2 ]] || { echo "缺少 $1 的值" >&2; exit 2; }
|
||||
PSQL_ARGS+=("-U" "$2")
|
||||
shift 2
|
||||
;;
|
||||
-d|--dbname)
|
||||
[[ $# -ge 2 ]] || { echo "缺少 $1 的值" >&2; exit 2; }
|
||||
PSQL_ARGS+=("-d" "$2")
|
||||
shift 2
|
||||
;;
|
||||
-p|--port)
|
||||
[[ $# -ge 2 ]] || { echo "缺少 $1 的值" >&2; exit 2; }
|
||||
PSQL_ARGS+=("-p" "$2")
|
||||
shift 2
|
||||
;;
|
||||
--migrate)
|
||||
RUN_MIGRATION=true
|
||||
shift
|
||||
;;
|
||||
--yes)
|
||||
CONFIRMED=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v psql >/dev/null 2>&1; then
|
||||
echo "未找到 psql,请先安装 PostgreSQL client。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${PRECHECK_SQL}" ]]; then
|
||||
echo "找不到预检查 SQL: ${PRECHECK_SQL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${MIGRATE_SQL}" ]]; then
|
||||
echo "找不到迁移 SQL: ${MIGRATE_SQL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${DATABASE_URL:-}" && ${#PSQL_ARGS[@]} -gt 0 ]]; then
|
||||
echo "请不要同时使用 DATABASE_URL 和 -h/-U/-d 参数。" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "${DATABASE_URL:-}" && ${#PSQL_ARGS[@]} -eq 0 ]]; then
|
||||
echo "缺少数据库连接参数。请设置 DATABASE_URL,或传入 -h/-U/-d。" >&2
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "${RUN_MIGRATION}" == true && "${CONFIRMED}" != true ]]; then
|
||||
echo "迁移会修改数据库。请先检查 precheck 输出,确认后追加 --yes。" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "${LOG_DIR}"
|
||||
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
PRECHECK_LOG="${LOG_DIR}/precheck-${TIMESTAMP}.log"
|
||||
MIGRATE_LOG="${LOG_DIR}/migrate-${TIMESTAMP}.log"
|
||||
|
||||
run_psql_file() {
|
||||
local sql_file="$1"
|
||||
local log_file="$2"
|
||||
|
||||
if [[ -n "${DATABASE_URL:-}" ]]; then
|
||||
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${sql_file}" 2>&1 | tee "${log_file}"
|
||||
else
|
||||
psql "${PSQL_ARGS[@]}" -v ON_ERROR_STOP=1 -f "${sql_file}" 2>&1 | tee "${log_file}"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "开始执行 RAG 公共知识库未归属默认项预检查..."
|
||||
echo "预检查输出: ${PRECHECK_LOG}"
|
||||
run_psql_file "${PRECHECK_SQL}" "${PRECHECK_LOG}"
|
||||
|
||||
if [[ "${RUN_MIGRATION}" != true ]]; then
|
||||
cat <<EOF
|
||||
|
||||
预检查已完成,未执行迁移。
|
||||
请确认上方第 1 段候选只包含需要从“未分配地区”迁到“公共”的历史公共知识库。
|
||||
确认后执行同一命令并追加: --migrate --yes
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "开始执行迁移..."
|
||||
echo "迁移输出: ${MIGRATE_LOG}"
|
||||
run_psql_file "${MIGRATE_SQL}" "${MIGRATE_LOG}"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
迁移脚本已执行完成。
|
||||
请检查迁移输出中的 public_default_count,应为 1。
|
||||
然后刷新 /chat-with-llm/dataset-manager,原“未分配地区”的默认项应归入“公共”,并且非默认项可以删除。
|
||||
EOF
|
||||
@@ -0,0 +1,129 @@
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- RAG 公共知识库未归属默认项迁移
|
||||
--
|
||||
-- 背景:
|
||||
-- 历史 RAG 知识库存在 tenant_code 为空、area 为空/default/公共、is_public=true 的公共知识库。
|
||||
-- 如果这类记录是默认知识库,后端会禁止直接取消默认/删除;同时 PUBLIC 组另有默认
|
||||
-- 时,页面会出现两个“默认”,但它们属于不同默认组。
|
||||
--
|
||||
-- 目标:
|
||||
-- 1. 将历史未归属公共 RAG 知识库迁移到 tenant_code='PUBLIC'、area='公共'
|
||||
-- 2. 同步其关联 rag_chat_app 的 tenant_code/area
|
||||
-- 3. 迁移后 PUBLIC 组只保留一个默认知识库/默认应用
|
||||
--
|
||||
-- 执行前:
|
||||
-- 先执行 precheck_rag_public_orphan_defaults.sql,确认候选记录符合预期。
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE public.rag_dataset
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
ALTER TABLE public.rag_chat_app
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rag_dataset_tenant_code
|
||||
ON public.rag_dataset(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rag_chat_app_tenant_code
|
||||
ON public.rag_chat_app(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TEMP TABLE tmp_rag_public_orphan_dataset_ids (
|
||||
id BIGINT PRIMARY KEY
|
||||
) ON COMMIT DROP;
|
||||
|
||||
INSERT INTO tmp_rag_public_orphan_dataset_ids (id)
|
||||
SELECT d.id
|
||||
FROM public.rag_dataset d
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND (d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
AND d.is_public IS TRUE
|
||||
AND (
|
||||
d.area IS NULL
|
||||
OR BTRIM(d.area) = ''
|
||||
OR LOWER(BTRIM(d.area)) = 'default'
|
||||
OR BTRIM(d.area) = '公共'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
WITH
|
||||
updated_dataset AS (
|
||||
UPDATE public.rag_dataset d
|
||||
SET tenant_code = 'PUBLIC',
|
||||
area = '公共',
|
||||
is_public = TRUE,
|
||||
updated_at = NOW()
|
||||
FROM tmp_rag_public_orphan_dataset_ids orphan
|
||||
WHERE d.id = orphan.id
|
||||
RETURNING d.id
|
||||
)
|
||||
UPDATE public.rag_chat_app a
|
||||
SET tenant_code = 'PUBLIC',
|
||||
area = '公共',
|
||||
updated_at = NOW()
|
||||
FROM updated_dataset ud
|
||||
WHERE a.dataset_id = ud.id
|
||||
AND a.deleted_at IS NULL;
|
||||
|
||||
WITH ranked_public_dataset AS (
|
||||
SELECT
|
||||
d.id,
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY
|
||||
d.is_default DESC,
|
||||
CASE WHEN orphan.id IS NULL THEN 0 ELSE 1 END,
|
||||
d.created_at DESC,
|
||||
d.id DESC
|
||||
) AS keep_default_rank
|
||||
FROM public.rag_dataset d
|
||||
LEFT JOIN tmp_rag_public_orphan_dataset_ids orphan ON orphan.id = d.id
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND BTRIM(COALESCE(d.tenant_code, '')) = 'PUBLIC'
|
||||
),
|
||||
normalized_public_dataset AS (
|
||||
UPDATE public.rag_dataset d
|
||||
SET is_default = (rpd.keep_default_rank = 1),
|
||||
updated_at = NOW()
|
||||
FROM ranked_public_dataset rpd
|
||||
WHERE d.id = rpd.id
|
||||
RETURNING d.id, d.is_default
|
||||
)
|
||||
UPDATE public.rag_chat_app a
|
||||
SET is_default = npd.is_default,
|
||||
tenant_code = 'PUBLIC',
|
||||
area = '公共',
|
||||
updated_at = NOW()
|
||||
FROM normalized_public_dataset npd
|
||||
WHERE a.dataset_id = npd.id
|
||||
AND a.deleted_at IS NULL;
|
||||
|
||||
-- 验收输出:PUBLIC 组当前默认知识库应只剩 1 条。
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE is_default = TRUE) AS public_default_count,
|
||||
COUNT(*) AS public_dataset_count
|
||||
FROM public.rag_dataset
|
||||
WHERE deleted_at IS NULL
|
||||
AND BTRIM(COALESCE(tenant_code, '')) = 'PUBLIC';
|
||||
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
area,
|
||||
tenant_code,
|
||||
is_public,
|
||||
is_default,
|
||||
CASE
|
||||
WHEN id IN (SELECT id FROM tmp_rag_public_orphan_dataset_ids) THEN 'migrated_orphan'
|
||||
ELSE 'existing_public'
|
||||
END AS source_scope,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM public.rag_dataset
|
||||
WHERE deleted_at IS NULL
|
||||
AND BTRIM(COALESCE(tenant_code, '')) = 'PUBLIC'
|
||||
ORDER BY is_default DESC, created_at DESC, id DESC;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,282 @@
|
||||
-- ============================================================================
|
||||
-- Evaluation Points Tenant Cleanup Precheck
|
||||
-- 目标:
|
||||
-- 1. 在执行 schema_evaluation_points_tenant_cleanup.sql 前先识别风险
|
||||
-- 2. 输出 area 分布、无法映射记录、共享域残留、编码重复情况
|
||||
-- 3. 供 DBA / 开发在评审和落库前人工确认
|
||||
-- 说明:
|
||||
-- - 本脚本只读,不修改任何数据
|
||||
-- - 建议在生产、预发、测试库分别执行并保存结果
|
||||
-- ============================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 0. 当前表结构确认
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'evaluation_points'
|
||||
ORDER BY ordinal_position;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. 基础体量
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT COUNT(*) AS total_points
|
||||
FROM public.evaluation_points;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1.1 构建兼容旧库的预检视图
|
||||
-- - 若旧库还没有 tenant_code / tenant_name,则自动补 NULL 占位
|
||||
-- - 后续所有预检统一只读这个临时视图
|
||||
-- --------------------------------------------------------------------------
|
||||
DO $$
|
||||
DECLARE
|
||||
has_tenant_code BOOLEAN;
|
||||
has_tenant_name BOOLEAN;
|
||||
tenant_code_expr TEXT;
|
||||
tenant_name_expr TEXT;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'evaluation_points'
|
||||
AND column_name = 'tenant_code'
|
||||
) INTO has_tenant_code;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'evaluation_points'
|
||||
AND column_name = 'tenant_name'
|
||||
) INTO has_tenant_name;
|
||||
|
||||
tenant_code_expr := CASE
|
||||
WHEN has_tenant_code THEN 'tenant_code'
|
||||
ELSE 'NULL::VARCHAR(64) AS tenant_code'
|
||||
END;
|
||||
|
||||
tenant_name_expr := CASE
|
||||
WHEN has_tenant_name THEN 'tenant_name'
|
||||
ELSE 'NULL::VARCHAR(128) AS tenant_name'
|
||||
END;
|
||||
|
||||
EXECUTE format(
|
||||
'CREATE TEMP VIEW tmp_evaluation_points_precheck AS
|
||||
SELECT
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
area,
|
||||
%s,
|
||||
%s
|
||||
FROM public.evaluation_points',
|
||||
tenant_code_expr,
|
||||
tenant_name_expr
|
||||
);
|
||||
END $$;
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS total_points,
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS missing_tenant_code,
|
||||
COUNT(*) FILTER (WHERE tenant_name IS NULL OR BTRIM(tenant_name) = '') AS missing_tenant_name,
|
||||
COUNT(*) FILTER (WHERE area IS NULL OR BTRIM(area) = '') AS blank_area
|
||||
FROM tmp_evaluation_points_precheck;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. area 值分布
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
COALESCE(NULLIF(BTRIM(area), ''), '<EMPTY>') AS area_value,
|
||||
COUNT(*) AS point_count
|
||||
FROM tmp_evaluation_points_precheck
|
||||
GROUP BY 1
|
||||
ORDER BY point_count DESC, area_value ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. area 无法映射租户编码的残留清单
|
||||
-- 规则:别名表 -> 租户名 -> 共享域兼容值
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_points AS (
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.code,
|
||||
ep.name,
|
||||
ep.area,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(ep.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN ep.area IS NULL OR BTRIM(ep.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(ep.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code
|
||||
FROM tmp_evaluation_points_precheck ep
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = tn.normalized_tenant_name
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
code,
|
||||
name,
|
||||
area
|
||||
FROM resolved_points
|
||||
WHERE resolved_tenant_code IS NULL
|
||||
ORDER BY id ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. 共享域残留统计
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
CASE
|
||||
WHEN area IS NULL OR BTRIM(area) = '' THEN '<EMPTY>'
|
||||
ELSE BTRIM(area)
|
||||
END AS shared_area_value,
|
||||
COUNT(*) AS point_count
|
||||
FROM tmp_evaluation_points_precheck
|
||||
WHERE area IS NULL
|
||||
OR BTRIM(area) = ''
|
||||
OR LOWER(BTRIM(area)) = 'default'
|
||||
OR BTRIM(area) IN ('公共', '省级', '省局')
|
||||
GROUP BY 1
|
||||
ORDER BY point_count DESC, shared_area_value ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. tenant_name 映射冲突预检
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
COUNT(DISTINCT tenant_code) AS tenant_code_count,
|
||||
ARRAY_AGG(DISTINCT tenant_code ORDER BY tenant_code) AS tenant_codes
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
AND tenant_name IS NOT NULL
|
||||
AND BTRIM(tenant_name) <> ''
|
||||
GROUP BY 1
|
||||
HAVING COUNT(DISTINCT tenant_code) > 1
|
||||
ORDER BY normalized_tenant_name ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. alias 映射冲突预检
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
COUNT(DISTINCT tenant_code) AS tenant_code_count,
|
||||
ARRAY_AGG(DISTINCT tenant_code ORDER BY tenant_code) AS tenant_codes
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
AND alias_value IS NOT NULL
|
||||
AND BTRIM(alias_value) <> ''
|
||||
GROUP BY 1
|
||||
HAVING COUNT(DISTINCT tenant_code) > 1
|
||||
ORDER BY normalized_alias_value ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. 编码唯一性现状预检
|
||||
-- 注意:当前 service 仍按全局唯一校验 code,不是按 tenant_code + code 校验
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
LOWER(BTRIM(code)) AS normalized_code,
|
||||
COUNT(*) AS duplicate_count,
|
||||
ARRAY_AGG(id ORDER BY id) AS point_ids
|
||||
FROM tmp_evaluation_points_precheck
|
||||
WHERE code IS NOT NULL
|
||||
AND BTRIM(code) <> ''
|
||||
GROUP BY 1
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY duplicate_count DESC, normalized_code ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 8. 若后续想把 code 改成“按租户唯一”,先看跨租户重复情况
|
||||
-- 这里用“预估 tenant_code”做模拟,不代表当前表已有 tenant_code
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_points AS (
|
||||
SELECT
|
||||
ep.id,
|
||||
LOWER(BTRIM(ep.code)) AS normalized_code,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(ep.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN ep.area IS NULL OR BTRIM(ep.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(ep.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code
|
||||
FROM tmp_evaluation_points_precheck ep
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = tn.normalized_tenant_name
|
||||
WHERE ep.code IS NOT NULL
|
||||
AND BTRIM(ep.code) <> ''
|
||||
)
|
||||
SELECT
|
||||
normalized_code,
|
||||
COUNT(*) AS duplicate_count,
|
||||
COUNT(DISTINCT resolved_tenant_code) AS tenant_count,
|
||||
ARRAY_AGG(
|
||||
CONCAT(id, ':', COALESCE(resolved_tenant_code, '<UNRESOLVED>'))
|
||||
ORDER BY id
|
||||
) AS point_ids_with_tenant
|
||||
FROM resolved_points
|
||||
GROUP BY normalized_code
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY duplicate_count DESC, normalized_code ASC;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 9. 执行后验收时建议对照的基线统计
|
||||
-- --------------------------------------------------------------------------
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS missing_tenant_code,
|
||||
COUNT(*) FILTER (WHERE tenant_name IS NULL OR BTRIM(tenant_name) = '') AS missing_tenant_name
|
||||
FROM tmp_evaluation_points_precheck;
|
||||
@@ -0,0 +1,134 @@
|
||||
-- ============================================================================
|
||||
-- RAG 公共知识库未归属默认项迁移前检查
|
||||
--
|
||||
-- 用途:
|
||||
-- 检查历史 tenant_code 为空,area 为空/default/公共,且 is_public=true 的 RAG 知识库/应用。
|
||||
-- 这些记录在页面显示为“未分配地区”,且如果 is_default=true,会导致无法删除。
|
||||
--
|
||||
-- 执行:
|
||||
-- psql -h <host> -U <user> -d <db_name> -v ON_ERROR_STOP=1 \
|
||||
-- -f scripts/创建sql/precheck_rag_public_orphan_defaults.sql
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 历史未归属公共知识库迁移候选
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
d.description,
|
||||
d.area,
|
||||
d.tenant_code,
|
||||
d.is_public,
|
||||
d.is_default,
|
||||
d.status,
|
||||
d.created_at,
|
||||
d.updated_at
|
||||
FROM public.rag_dataset d
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND (d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
AND d.is_public IS TRUE
|
||||
AND (
|
||||
d.area IS NULL
|
||||
OR BTRIM(d.area) = ''
|
||||
OR LOWER(BTRIM(d.area)) = 'default'
|
||||
OR BTRIM(d.area) = '公共'
|
||||
)
|
||||
ORDER BY d.is_default DESC, d.created_at DESC, d.id DESC;
|
||||
|
||||
-- 2. 当前 PUBLIC 组默认知识库
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
d.description,
|
||||
d.area,
|
||||
d.tenant_code,
|
||||
d.is_public,
|
||||
d.is_default,
|
||||
d.status,
|
||||
d.created_at,
|
||||
d.updated_at
|
||||
FROM public.rag_dataset d
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND BTRIM(COALESCE(d.tenant_code, '')) = 'PUBLIC'
|
||||
ORDER BY d.is_default DESC, d.created_at DESC, d.id DESC;
|
||||
|
||||
-- 3. 迁移后 PUBLIC 组默认冲突预览
|
||||
WITH candidate AS (
|
||||
SELECT
|
||||
d.id,
|
||||
d.name,
|
||||
d.area,
|
||||
CASE
|
||||
WHEN BTRIM(COALESCE(d.tenant_code, '')) = 'PUBLIC' THEN 'existing_public'
|
||||
ELSE 'orphan_public'
|
||||
END AS source_scope,
|
||||
COALESCE(NULLIF(BTRIM(d.tenant_code), ''), 'PUBLIC') AS target_tenant_code,
|
||||
d.is_default,
|
||||
d.is_public,
|
||||
d.created_at
|
||||
FROM public.rag_dataset d
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND (
|
||||
BTRIM(COALESCE(d.tenant_code, '')) = 'PUBLIC'
|
||||
OR (
|
||||
(d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
AND d.is_public IS TRUE
|
||||
AND (
|
||||
d.area IS NULL
|
||||
OR BTRIM(d.area) = ''
|
||||
OR LOWER(BTRIM(d.area)) = 'default'
|
||||
OR BTRIM(d.area) = '公共'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.*,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY c.target_tenant_code
|
||||
ORDER BY
|
||||
c.is_default DESC,
|
||||
CASE WHEN c.source_scope = 'existing_public' THEN 0 ELSE 1 END,
|
||||
c.created_at DESC,
|
||||
c.id DESC
|
||||
) AS keep_default_rank
|
||||
FROM candidate c
|
||||
)
|
||||
SELECT
|
||||
target_tenant_code,
|
||||
id,
|
||||
name,
|
||||
area,
|
||||
source_scope,
|
||||
is_public,
|
||||
is_default AS current_is_default,
|
||||
keep_default_rank,
|
||||
CASE WHEN keep_default_rank = 1 THEN '迁移后保留默认' ELSE '迁移后取消默认' END AS planned_default_action
|
||||
FROM ranked
|
||||
ORDER BY target_tenant_code, keep_default_rank, id;
|
||||
|
||||
-- 4. 关联应用候选
|
||||
SELECT
|
||||
a.id,
|
||||
a.name,
|
||||
a.area,
|
||||
a.tenant_code,
|
||||
a.dataset_id,
|
||||
a.is_default,
|
||||
a.status,
|
||||
a.created_at,
|
||||
a.updated_at
|
||||
FROM public.rag_chat_app a
|
||||
JOIN public.rag_dataset d ON d.id = a.dataset_id
|
||||
WHERE a.deleted_at IS NULL
|
||||
AND d.deleted_at IS NULL
|
||||
AND (a.tenant_code IS NULL OR BTRIM(a.tenant_code) = '')
|
||||
AND (d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
AND d.is_public IS TRUE
|
||||
AND (
|
||||
d.area IS NULL
|
||||
OR BTRIM(d.area) = ''
|
||||
OR LOWER(BTRIM(d.area)) = 'default'
|
||||
OR BTRIM(d.area) = '公共'
|
||||
)
|
||||
ORDER BY a.is_default DESC, a.created_at DESC, a.id DESC;
|
||||
@@ -0,0 +1,118 @@
|
||||
-- ============================================================================
|
||||
-- Rule Domain Tenant Phase 1 Precheck
|
||||
-- 目标:
|
||||
-- 1. 执行前确认规则域现状
|
||||
-- 2. 识别历史全局规则资产规模
|
||||
-- 3. 识别运行结果快照回填影响范围
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 核心表是否存在
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name IN (
|
||||
'leaudit_rule_sets',
|
||||
'leaudit_rule_versions',
|
||||
'leaudit_rule_group_bindings',
|
||||
'leaudit_rule_type_bindings',
|
||||
'leaudit_audit_runs',
|
||||
'leaudit_rule_results',
|
||||
'leaudit_run_errors',
|
||||
'leaudit_run_metrics'
|
||||
)
|
||||
ORDER BY table_name;
|
||||
|
||||
-- 2. 当前列缺失情况
|
||||
SELECT
|
||||
table_name,
|
||||
column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name IN (
|
||||
'leaudit_rule_sets',
|
||||
'leaudit_rule_versions',
|
||||
'leaudit_rule_group_bindings',
|
||||
'leaudit_rule_type_bindings',
|
||||
'leaudit_audit_runs',
|
||||
'leaudit_rule_results',
|
||||
'leaudit_run_errors',
|
||||
'leaudit_run_metrics'
|
||||
)
|
||||
AND column_name IN (
|
||||
'tenant_code',
|
||||
'scope_type',
|
||||
'tenant_code_snapshot',
|
||||
'scope_type_snapshot',
|
||||
'source_rule_set_id',
|
||||
'source_version_id',
|
||||
'tenant_name_snapshot',
|
||||
'group_id_snapshot',
|
||||
'rule_binding_id_snapshot'
|
||||
)
|
||||
ORDER BY table_name, column_name;
|
||||
|
||||
-- 3. 规则集与规则版本规模
|
||||
SELECT 'leaudit_rule_sets' AS table_name, COUNT(*) AS total
|
||||
FROM leaudit_rule_sets
|
||||
WHERE deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT 'leaudit_rule_versions' AS table_name, COUNT(*) AS total
|
||||
FROM leaudit_rule_versions
|
||||
UNION ALL
|
||||
SELECT 'leaudit_rule_group_bindings' AS table_name, COUNT(*) AS total
|
||||
FROM leaudit_rule_group_bindings
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 4. 历史全局规则资产规模
|
||||
SELECT
|
||||
COUNT(*) AS global_rule_sets_without_tenant_code
|
||||
FROM leaudit_rule_sets
|
||||
WHERE deleted_at IS NULL
|
||||
AND (
|
||||
tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = ''
|
||||
);
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS global_group_bindings_without_tenant_code
|
||||
FROM leaudit_rule_group_bindings
|
||||
WHERE deleted_at IS NULL
|
||||
AND (
|
||||
tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = ''
|
||||
);
|
||||
|
||||
-- 5. 运行主表与结果表快照缺口
|
||||
SELECT
|
||||
COUNT(*) AS audit_runs_missing_tenant_code
|
||||
FROM leaudit_audit_runs
|
||||
WHERE tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = '';
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS rule_results_missing_tenant_code
|
||||
FROM leaudit_rule_results
|
||||
WHERE tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = '';
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS run_errors_missing_tenant_code
|
||||
FROM leaudit_run_errors
|
||||
WHERE tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = '';
|
||||
|
||||
SELECT
|
||||
COUNT(*) AS run_metrics_missing_tenant_code
|
||||
FROM leaudit_run_metrics
|
||||
WHERE tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = '';
|
||||
|
||||
-- 6. 文档租户缺口,会直接影响 audit_runs 回填质量
|
||||
SELECT
|
||||
COUNT(*) AS documents_missing_tenant_code
|
||||
FROM leaudit_documents
|
||||
WHERE deleted_at IS NULL
|
||||
AND (
|
||||
tenant_code IS NULL
|
||||
OR BTRIM(tenant_code) = ''
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
CREATE TABLE IF NOT EXISTS public.leaudit_page_quality_runs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
document_id BIGINT NOT NULL,
|
||||
document_file_id BIGINT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'queued',
|
||||
summary_status VARCHAR(32) NULL,
|
||||
total_pages INTEGER NOT NULL DEFAULT 0,
|
||||
review_page_count INTEGER NOT NULL DEFAULT 0,
|
||||
reject_page_count INTEGER NOT NULL DEFAULT 0,
|
||||
skip_reason VARCHAR(64) NULL,
|
||||
task_id VARCHAR(128) NULL,
|
||||
error_message TEXT NULL,
|
||||
started_at TIMESTAMP NULL,
|
||||
finished_at TIMESTAMP NULL,
|
||||
created_by BIGINT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.leaudit_page_quality_results (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
run_id BIGINT NOT NULL,
|
||||
document_id BIGINT NOT NULL,
|
||||
page_num INTEGER NOT NULL,
|
||||
quality_status VARCHAR(32) NOT NULL,
|
||||
quality_score NUMERIC(10, 4) NULL,
|
||||
reason_text TEXT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_page_quality_runs_document_id
|
||||
ON public.leaudit_page_quality_runs(document_id)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_page_quality_results_run_id
|
||||
ON public.leaudit_page_quality_results(run_id);
|
||||
@@ -0,0 +1,78 @@
|
||||
-- 入口模块租户关系表
|
||||
-- 目的:
|
||||
-- 1. 替代 leaudit_entry_modules.areas 的硬编码地区数组
|
||||
-- 2. 支持新增自定义租户后为入口模块直接分配 tenant_code
|
||||
-- 3. 保留旧 areas 字段作为兼容读写镜像,逐步退出
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS leaudit_entry_module_tenants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
entry_module_id BIGINT NOT NULL,
|
||||
tenant_code VARCHAR(64) NOT NULL,
|
||||
tenant_name VARCHAR(128) NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
UNIQUE (entry_module_id, tenant_code)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_entry_module_tenants_module
|
||||
ON leaudit_entry_module_tenants(entry_module_id, sort_order, id)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_entry_module_tenants_tenant
|
||||
ON leaudit_entry_module_tenants(tenant_code, is_enabled, sort_order)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE leaudit_entry_module_tenants IS '入口模块与租户的多对多配置关系表';
|
||||
COMMENT ON COLUMN leaudit_entry_module_tenants.entry_module_id IS '入口模块ID';
|
||||
COMMENT ON COLUMN leaudit_entry_module_tenants.tenant_code IS '租户编码,引用 sys_tenants.tenant_code';
|
||||
COMMENT ON COLUMN leaudit_entry_module_tenants.tenant_name IS '配置快照名称,便于前端直接展示';
|
||||
|
||||
INSERT INTO leaudit_entry_module_tenants (
|
||||
entry_module_id, tenant_code, tenant_name, is_enabled, sort_order, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
em.id,
|
||||
alias_map.tenant_code,
|
||||
COALESCE(t.tenant_name, area_item->>'area') AS tenant_name,
|
||||
COALESCE((area_item->>'enabled')::boolean, TRUE) AS is_enabled,
|
||||
COALESCE((area_item->>'sort_order')::int, 0) AS sort_order,
|
||||
NOW(),
|
||||
NOW(),
|
||||
NULL
|
||||
FROM leaudit_entry_modules em
|
||||
CROSS JOIN LATERAL jsonb_array_elements(COALESCE(em.areas, '[]'::jsonb)) AS area_item
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT a.tenant_code
|
||||
FROM sys_tenant_aliases a
|
||||
WHERE a.alias_value = area_item->>'area'
|
||||
AND a.deleted_at IS NULL
|
||||
AND a.is_enabled = TRUE
|
||||
ORDER BY
|
||||
CASE a.alias_type
|
||||
WHEN 'DISPLAY' THEN 1
|
||||
WHEN 'LEGACY_AREA' THEN 2
|
||||
WHEN 'LEGACY_REGION' THEN 3
|
||||
ELSE 9
|
||||
END ASC,
|
||||
a.id ASC
|
||||
LIMIT 1
|
||||
) alias_map ON TRUE
|
||||
LEFT JOIN sys_tenants t
|
||||
ON t.tenant_code = alias_map.tenant_code
|
||||
AND t.deleted_at IS NULL
|
||||
WHERE em.deleted_at IS NULL
|
||||
AND alias_map.tenant_code IS NOT NULL
|
||||
ON CONFLICT (entry_module_id, tenant_code) DO UPDATE
|
||||
SET
|
||||
tenant_name = EXCLUDED.tenant_name,
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
updated_at = NOW(),
|
||||
deleted_at = NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,198 @@
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- Evaluation Points Tenant Cleanup
|
||||
-- 目标:
|
||||
-- 1. 为旧表 evaluation_points 补 tenant_code / tenant_name
|
||||
-- 2. 建立基础索引,支持评查点模块彻底切到 tenant_code 主链
|
||||
-- 3. 基于 sys_tenant_aliases / sys_tenants 做首轮历史回填
|
||||
-- 说明:
|
||||
-- - 当前真实运行链路仍使用旧表 evaluation_points
|
||||
-- - 本脚本只做“补列 + 建索引 + 回填”
|
||||
-- - 不在本阶段删除旧 area 字段
|
||||
-- - 不在本阶段强制 NOT NULL
|
||||
-- ============================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. 补字段
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.evaluation_points
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name VARCHAR(128);
|
||||
|
||||
COMMENT ON COLUMN public.evaluation_points.tenant_code IS '所属租户编码:评查点历史收尾阶段新增,后续替代 area 作为真实归属主字段';
|
||||
COMMENT ON COLUMN public.evaluation_points.tenant_name IS '所属租户名称:展示字段,和 tenant_code 配套回填';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. 基础索引
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS idx_evaluation_points_tenant_code
|
||||
ON public.evaluation_points(tenant_code);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_evaluation_points_group_tenant_code
|
||||
ON public.evaluation_points(evaluation_point_groups_id, tenant_code);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_evaluation_points_group_pid_tenant_code
|
||||
ON public.evaluation_points(evaluation_point_groups_pid, tenant_code);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. 公共 / 省级规范租户兜底
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO public.sys_tenants (
|
||||
tenant_code, tenant_name, tenant_short_name, tenant_type,
|
||||
parent_tenant_code, is_enabled, is_public, display_order, ext,
|
||||
created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
'PUBLIC', '公共资源域', '公共', 'PUBLIC',
|
||||
NULL, TRUE, TRUE, 0, '{}'::jsonb,
|
||||
NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.sys_tenants WHERE tenant_code = 'PUBLIC'
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenants (
|
||||
tenant_code, tenant_name, tenant_short_name, tenant_type,
|
||||
parent_tenant_code, is_enabled, is_public, display_order, ext,
|
||||
created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
'PROVINCIAL', '省级统管域', '省级', 'GOV',
|
||||
NULL, TRUE, FALSE, 1, '{}'::jsonb,
|
||||
NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.sys_tenants WHERE tenant_code = 'PROVINCIAL'
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PUBLIC', 'DISPLAY', '公共', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PUBLIC'
|
||||
AND alias_value = '公共'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PUBLIC', 'LEGACY_REGION', 'default', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PUBLIC'
|
||||
AND alias_value = 'default'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PROVINCIAL', 'DISPLAY', '省级', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PROVINCIAL'
|
||||
AND alias_value = '省级'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PROVINCIAL', 'LEGACY_REGION', '省局', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PROVINCIAL'
|
||||
AND alias_value = '省局'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. 历史回填:优先按别名表映射
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_points AS (
|
||||
SELECT
|
||||
ep.id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(ep.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN ep.area IS NULL OR BTRIM(ep.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(ep.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(ep.tenant_name), ''),
|
||||
st.tenant_name,
|
||||
NULLIF(BTRIM(ep.area), ''),
|
||||
CASE
|
||||
WHEN ep.area IS NULL OR BTRIM(ep.area) = '' THEN '公共资源域'
|
||||
WHEN LOWER(BTRIM(ep.area)) = 'default' THEN '公共资源域'
|
||||
WHEN BTRIM(ep.area) = '公共' THEN '公共资源域'
|
||||
WHEN BTRIM(ep.area) IN ('省级', '省局') THEN '省级统管域'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_name
|
||||
FROM public.evaluation_points ep
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(ep.area, ''))) = tn.normalized_tenant_name
|
||||
LEFT JOIN public.sys_tenants st
|
||||
ON st.tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(ep.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN ep.area IS NULL OR BTRIM(ep.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(ep.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(ep.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
AND st.deleted_at IS NULL
|
||||
AND st.is_enabled = TRUE
|
||||
WHERE
|
||||
ep.tenant_code IS NULL
|
||||
OR BTRIM(ep.tenant_code) = ''
|
||||
OR ep.tenant_name IS NULL
|
||||
OR BTRIM(ep.tenant_name) = ''
|
||||
)
|
||||
UPDATE public.evaluation_points ep
|
||||
SET tenant_code = COALESCE(NULLIF(BTRIM(ep.tenant_code), ''), rp.resolved_tenant_code),
|
||||
tenant_name = COALESCE(NULLIF(BTRIM(ep.tenant_name), ''), rp.resolved_tenant_name)
|
||||
FROM resolved_points rp
|
||||
WHERE ep.id = rp.id
|
||||
AND (
|
||||
rp.resolved_tenant_code IS NOT NULL
|
||||
OR rp.resolved_tenant_name IS NOT NULL
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,230 @@
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- Rule Domain Tenant Phase 1
|
||||
-- 目标:
|
||||
-- 1. 为规则域核心表补 tenant_code / scope_type / 运行结果快照字段
|
||||
-- 2. 为运行时 TENANT -> PROVINCIAL -> PUBLIC 解析建立物理载体
|
||||
-- 3. 将历史全局规则资产默认回填到 PROVINCIAL,而不是 PUBLIC
|
||||
-- 说明:
|
||||
-- - 本阶段只做补列、建索引、基础回填
|
||||
-- - 不强制 NOT NULL
|
||||
-- - 不删除旧 region / 全局兼容逻辑
|
||||
-- ============================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. leaudit_rule_sets
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_rule_sets
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS scope_type VARCHAR(32) DEFAULT 'PROVINCIAL',
|
||||
ADD COLUMN IF NOT EXISTS source_rule_set_id BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_rule_sets.tenant_code IS '规则集所属租户编码';
|
||||
COMMENT ON COLUMN public.leaudit_rule_sets.scope_type IS '规则集作用域: TENANT / PROVINCIAL / PUBLIC';
|
||||
COMMENT ON COLUMN public.leaudit_rule_sets.source_rule_set_id IS '派生规则集来源 rule_set_id';
|
||||
COMMENT ON COLUMN public.leaudit_rule_sets.tenant_name_snapshot IS '规则集所属租户名称快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_sets_tenant_code
|
||||
ON public.leaudit_rule_sets(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_sets_scope_type
|
||||
ON public.leaudit_rule_sets(scope_type)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. leaudit_rule_versions
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_rule_versions
|
||||
ADD COLUMN IF NOT EXISTS tenant_code_snapshot VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS scope_type_snapshot VARCHAR(32),
|
||||
ADD COLUMN IF NOT EXISTS source_version_id BIGINT;
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_rule_versions.tenant_code_snapshot IS '规则版本所属租户编码快照';
|
||||
COMMENT ON COLUMN public.leaudit_rule_versions.scope_type_snapshot IS '规则版本作用域快照';
|
||||
COMMENT ON COLUMN public.leaudit_rule_versions.source_version_id IS '派生版本来源 version_id';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_versions_tenant_code_snapshot
|
||||
ON public.leaudit_rule_versions(tenant_code_snapshot);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. leaudit_rule_group_bindings
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_rule_group_bindings
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS scope_type VARCHAR(32) DEFAULT 'PROVINCIAL',
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_rule_group_bindings.tenant_code IS '业务组绑定所属租户编码';
|
||||
COMMENT ON COLUMN public.leaudit_rule_group_bindings.scope_type IS '业务组绑定作用域: TENANT / PROVINCIAL / PUBLIC';
|
||||
COMMENT ON COLUMN public.leaudit_rule_group_bindings.tenant_name_snapshot IS '业务组绑定所属租户名称快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_group_bindings_group_tenant
|
||||
ON public.leaudit_rule_group_bindings(group_id, tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_group_bindings_scope_type
|
||||
ON public.leaudit_rule_group_bindings(scope_type)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. leaudit_rule_type_bindings
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_rule_type_bindings
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS scope_type VARCHAR(32) DEFAULT 'PROVINCIAL';
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_rule_type_bindings.tenant_code IS '旧规则类型绑定所属租户编码,仅用于兼容与迁移';
|
||||
COMMENT ON COLUMN public.leaudit_rule_type_bindings.scope_type IS '旧规则类型绑定作用域,仅用于兼容与迁移';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. leaudit_audit_runs
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_audit_runs
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS scope_type_snapshot VARCHAR(32),
|
||||
ADD COLUMN IF NOT EXISTS group_id_snapshot BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS rule_binding_id_snapshot BIGINT;
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_audit_runs.tenant_code IS '本次运行所属租户编码快照';
|
||||
COMMENT ON COLUMN public.leaudit_audit_runs.tenant_name_snapshot IS '本次运行所属租户名称快照';
|
||||
COMMENT ON COLUMN public.leaudit_audit_runs.scope_type_snapshot IS '本次运行命中的规则作用域快照';
|
||||
COMMENT ON COLUMN public.leaudit_audit_runs.group_id_snapshot IS '本次运行命中的业务组快照';
|
||||
COMMENT ON COLUMN public.leaudit_audit_runs.rule_binding_id_snapshot IS '本次运行命中的规则绑定快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_audit_runs_tenant_code
|
||||
ON public.leaudit_audit_runs(tenant_code, created_at DESC);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. leaudit_rule_results
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_rule_results
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_rule_results.tenant_code IS '规则结果所属租户编码快照';
|
||||
COMMENT ON COLUMN public.leaudit_rule_results.tenant_name_snapshot IS '规则结果所属租户名称快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_rule_results_tenant_code
|
||||
ON public.leaudit_rule_results(tenant_code);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. leaudit_run_errors
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_run_errors
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(255);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_run_errors.tenant_code IS '运行错误所属租户编码快照';
|
||||
COMMENT ON COLUMN public.leaudit_run_errors.tenant_name_snapshot IS '运行错误所属租户名称快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_run_errors_tenant_code
|
||||
ON public.leaudit_run_errors(tenant_code);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 8. leaudit_run_metrics
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_run_metrics
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_run_metrics.tenant_code IS '运行指标所属租户编码快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_run_metrics_tenant_code
|
||||
ON public.leaudit_run_metrics(tenant_code);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 9. 历史资产默认回填到 PROVINCIAL
|
||||
-- --------------------------------------------------------------------------
|
||||
UPDATE public.leaudit_rule_sets
|
||||
SET tenant_code = COALESCE(NULLIF(BTRIM(tenant_code), ''), 'PROVINCIAL'),
|
||||
scope_type = COALESCE(NULLIF(BTRIM(scope_type), ''), 'PROVINCIAL')
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 兼容历史唯一约束 (rule_type, region):租户私有规则集必须使用租户编码作为 legacy region,
|
||||
-- 否则会与省级默认 region=default 冲突。
|
||||
UPDATE public.leaudit_rule_sets
|
||||
SET region = tenant_code
|
||||
WHERE deleted_at IS NULL
|
||||
AND scope_type = 'TENANT'
|
||||
AND tenant_code IS NOT NULL
|
||||
AND BTRIM(tenant_code) <> ''
|
||||
AND region = 'default';
|
||||
|
||||
UPDATE public.leaudit_rule_versions rv
|
||||
SET tenant_code_snapshot = COALESCE(
|
||||
NULLIF(BTRIM(rv.tenant_code_snapshot), ''),
|
||||
NULLIF(BTRIM(rs.tenant_code), ''),
|
||||
'PROVINCIAL'
|
||||
),
|
||||
scope_type_snapshot = COALESCE(
|
||||
NULLIF(BTRIM(rv.scope_type_snapshot), ''),
|
||||
NULLIF(BTRIM(rs.scope_type), ''),
|
||||
'PROVINCIAL'
|
||||
)
|
||||
FROM public.leaudit_rule_sets rs
|
||||
WHERE rv.rule_set_id = rs.id;
|
||||
|
||||
UPDATE public.leaudit_rule_group_bindings rgb
|
||||
SET tenant_code = COALESCE(NULLIF(BTRIM(rgb.tenant_code), ''), 'PROVINCIAL'),
|
||||
scope_type = COALESCE(NULLIF(BTRIM(rgb.scope_type), ''), 'PROVINCIAL')
|
||||
WHERE rgb.deleted_at IS NULL;
|
||||
|
||||
UPDATE public.leaudit_rule_type_bindings
|
||||
SET tenant_code = COALESCE(NULLIF(BTRIM(tenant_code), ''), 'PROVINCIAL'),
|
||||
scope_type = COALESCE(NULLIF(BTRIM(scope_type), ''), 'PROVINCIAL')
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 10. 运行与结果快照回填
|
||||
-- --------------------------------------------------------------------------
|
||||
UPDATE public.leaudit_audit_runs ar
|
||||
SET tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(ar.tenant_code), ''),
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
'PROVINCIAL'
|
||||
),
|
||||
scope_type_snapshot = COALESCE(
|
||||
NULLIF(BTRIM(ar.scope_type_snapshot), ''),
|
||||
'PROVINCIAL'
|
||||
),
|
||||
group_id_snapshot = COALESCE(ar.group_id_snapshot, d.group_id)
|
||||
FROM public.leaudit_documents d
|
||||
WHERE ar.document_id = d.id;
|
||||
|
||||
UPDATE public.leaudit_rule_results rr
|
||||
SET tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(rr.tenant_code), ''),
|
||||
NULLIF(BTRIM(ar.tenant_code), ''),
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
'PROVINCIAL'
|
||||
)
|
||||
FROM public.leaudit_audit_runs ar
|
||||
LEFT JOIN public.leaudit_documents d ON d.id = ar.document_id
|
||||
WHERE rr.run_id = ar.id;
|
||||
|
||||
UPDATE public.leaudit_run_errors re
|
||||
SET tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(re.tenant_code), ''),
|
||||
NULLIF(BTRIM(ar.tenant_code), ''),
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
'PROVINCIAL'
|
||||
)
|
||||
FROM public.leaudit_audit_runs ar
|
||||
LEFT JOIN public.leaudit_documents d ON d.id = ar.document_id
|
||||
WHERE re.run_id = ar.id;
|
||||
|
||||
UPDATE public.leaudit_run_metrics rm
|
||||
SET tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(rm.tenant_code), ''),
|
||||
NULLIF(BTRIM(ar.tenant_code), ''),
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
'PROVINCIAL'
|
||||
)
|
||||
FROM public.leaudit_audit_runs ar
|
||||
LEFT JOIN public.leaudit_documents d ON d.id = ar.document_id
|
||||
WHERE rm.run_id = ar.id;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,453 @@
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- High Risk Tenant Code Phase 1
|
||||
-- 目标:
|
||||
-- 1. 为高风险核心业务表补 tenant_code / tenant 快照字段
|
||||
-- 2. 补基础索引,支持后续 service 改为 tenant_code 主链
|
||||
-- 3. 基于 sys_tenant_aliases / sys_tenants 做第一轮历史回填
|
||||
-- 说明:
|
||||
-- - 本脚本只做“补列 + 建索引 + 回填”
|
||||
-- - 不在本阶段强制 NOT NULL
|
||||
-- - 不在本阶段删除旧 region/area 字段
|
||||
-- ============================================================================
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. leaudit_documents
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.leaudit_documents
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN public.leaudit_documents.tenant_code IS '所属租户编码:高风险阶段新增,后续替代 region 作为真实归属主字段';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_documents_tenant_code
|
||||
ON public.leaudit_documents(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leaudit_documents_type_tenant_name_latest
|
||||
ON public.leaudit_documents(type_id, tenant_code, normalized_name, is_latest_version)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. contract_templates
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.contract_templates
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name VARCHAR(128);
|
||||
|
||||
COMMENT ON COLUMN public.contract_templates.tenant_code IS '所属租户编码:高风险阶段新增,后续替代 region 作为真实归属主字段';
|
||||
COMMENT ON COLUMN public.contract_templates.tenant_name IS '所属租户名称:展示字段,和 tenant_code 配套回填';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contract_templates_tenant_code_active
|
||||
ON public.contract_templates(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_contract_templates_tenant_code_code_active
|
||||
ON public.contract_templates(tenant_code, template_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. usage_login_events
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.usage_login_events
|
||||
ADD COLUMN IF NOT EXISTS tenant_code_snapshot VARCHAR(64),
|
||||
ADD COLUMN IF NOT EXISTS tenant_name_snapshot VARCHAR(128);
|
||||
|
||||
COMMENT ON COLUMN public.usage_login_events.tenant_code_snapshot IS '登录时租户编码快照';
|
||||
COMMENT ON COLUMN public.usage_login_events.tenant_name_snapshot IS '登录时租户名称快照';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_usage_login_events_tenant_code
|
||||
ON public.usage_login_events(tenant_code_snapshot, login_time DESC);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. rag_dataset
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.rag_dataset
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN public.rag_dataset.tenant_code IS '所属租户编码:高风险阶段新增,后续替代 area 作为真实归属主字段';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rag_dataset_tenant_code
|
||||
ON public.rag_dataset(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. rag_chat_app
|
||||
-- --------------------------------------------------------------------------
|
||||
ALTER TABLE public.rag_chat_app
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64);
|
||||
|
||||
COMMENT ON COLUMN public.rag_chat_app.tenant_code IS '所属租户编码:高风险阶段新增,后续替代 area 作为真实归属主字段';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_rag_chat_app_tenant_code
|
||||
ON public.rag_chat_app(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. 公共 / 省级规范租户兜底
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO public.sys_tenants (
|
||||
tenant_code, tenant_name, tenant_short_name, tenant_type,
|
||||
parent_tenant_code, is_enabled, is_public, display_order, ext,
|
||||
created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
'PUBLIC', '公共资源域', '公共', 'PUBLIC',
|
||||
NULL, TRUE, TRUE, 0, '{}'::jsonb,
|
||||
NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.sys_tenants WHERE tenant_code = 'PUBLIC'
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenants (
|
||||
tenant_code, tenant_name, tenant_short_name, tenant_type,
|
||||
parent_tenant_code, is_enabled, is_public, display_order, ext,
|
||||
created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT
|
||||
'PROVINCIAL', '省级统管域', '省级', 'GOV',
|
||||
NULL, TRUE, FALSE, 1, '{}'::jsonb,
|
||||
NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.sys_tenants WHERE tenant_code = 'PROVINCIAL'
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PUBLIC', 'DISPLAY', '公共', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PUBLIC'
|
||||
AND alias_value = '公共'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PUBLIC', 'LEGACY_REGION', 'default', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PUBLIC'
|
||||
AND alias_value = 'default'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PROVINCIAL', 'DISPLAY', '省级', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PROVINCIAL'
|
||||
AND alias_value = '省级'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
INSERT INTO public.sys_tenant_aliases (
|
||||
tenant_code, alias_type, alias_value, is_enabled, created_at, updated_at, deleted_at
|
||||
)
|
||||
SELECT 'PROVINCIAL', 'LEGACY_REGION', '省局', TRUE, NOW(), NOW(), NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE tenant_code = 'PROVINCIAL'
|
||||
AND alias_value = '省局'
|
||||
AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. 第一轮历史回填:优先按别名表映射
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_documents AS (
|
||||
SELECT
|
||||
d.id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN d.region IS NULL OR BTRIM(d.region) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(d.region)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(d.region) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(d.region) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code
|
||||
FROM public.leaudit_documents d
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(d.region, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(d.region, ''))) = tn.normalized_tenant_name
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND (d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
)
|
||||
UPDATE public.leaudit_documents d
|
||||
SET tenant_code = rd.resolved_tenant_code
|
||||
FROM resolved_documents rd
|
||||
WHERE d.id = rd.id
|
||||
AND rd.resolved_tenant_code IS NOT NULL;
|
||||
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_templates AS (
|
||||
SELECT
|
||||
t.id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(t.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN t.region IS NULL OR BTRIM(t.region) = '' THEN 'PROVINCIAL'
|
||||
WHEN LOWER(BTRIM(t.region)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(t.region) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(t.region) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(t.tenant_name), ''),
|
||||
NULLIF(BTRIM(t.region), ''),
|
||||
st.tenant_name,
|
||||
CASE
|
||||
WHEN t.region IS NULL OR BTRIM(t.region) = '' THEN '省级'
|
||||
WHEN LOWER(BTRIM(t.region)) = 'default' THEN '公共'
|
||||
WHEN BTRIM(t.region) = '公共' THEN '公共'
|
||||
WHEN BTRIM(t.region) IN ('省级', '省局') THEN '省级'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_name
|
||||
FROM public.contract_templates t
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(t.region, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(t.region, ''))) = tn.normalized_tenant_name
|
||||
LEFT JOIN public.sys_tenants st
|
||||
ON st.tenant_code = COALESCE(
|
||||
NULLIF(BTRIM(t.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN t.region IS NULL OR BTRIM(t.region) = '' THEN 'PROVINCIAL'
|
||||
WHEN LOWER(BTRIM(t.region)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(t.region) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(t.region) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
)
|
||||
AND st.deleted_at IS NULL
|
||||
AND st.is_enabled = TRUE
|
||||
WHERE t.deleted_at IS NULL
|
||||
AND (
|
||||
t.tenant_code IS NULL
|
||||
OR BTRIM(t.tenant_code) = ''
|
||||
OR t.tenant_name IS NULL
|
||||
OR BTRIM(t.tenant_name) = ''
|
||||
)
|
||||
)
|
||||
UPDATE public.contract_templates t
|
||||
SET tenant_code = COALESCE(t.tenant_code, rt.resolved_tenant_code),
|
||||
tenant_name = COALESCE(NULLIF(BTRIM(t.tenant_name), ''), rt.resolved_tenant_name)
|
||||
FROM resolved_templates rt
|
||||
WHERE t.id = rt.id
|
||||
AND (
|
||||
rt.resolved_tenant_code IS NOT NULL
|
||||
OR rt.resolved_tenant_name IS NOT NULL
|
||||
);
|
||||
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_rag_dataset AS (
|
||||
SELECT
|
||||
d.id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(d.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN d.area IS NULL OR BTRIM(d.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(d.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(d.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(d.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code
|
||||
FROM public.rag_dataset d
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(d.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(d.area, ''))) = tn.normalized_tenant_name
|
||||
WHERE d.deleted_at IS NULL
|
||||
AND (d.tenant_code IS NULL OR BTRIM(d.tenant_code) = '')
|
||||
)
|
||||
UPDATE public.rag_dataset d
|
||||
SET tenant_code = rrd.resolved_tenant_code
|
||||
FROM resolved_rag_dataset rrd
|
||||
WHERE d.id = rrd.id
|
||||
AND rrd.resolved_tenant_code IS NOT NULL;
|
||||
|
||||
WITH alias_map AS (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
),
|
||||
tenant_name_map AS (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
),
|
||||
resolved_rag_chat_app AS (
|
||||
SELECT
|
||||
a.id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(a.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN a.area IS NULL OR BTRIM(a.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(a.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(a.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(a.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code
|
||||
FROM public.rag_chat_app a
|
||||
LEFT JOIN alias_map am
|
||||
ON LOWER(BTRIM(COALESCE(a.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN tenant_name_map tn
|
||||
ON LOWER(BTRIM(COALESCE(a.area, ''))) = tn.normalized_tenant_name
|
||||
WHERE a.deleted_at IS NULL
|
||||
AND (a.tenant_code IS NULL OR BTRIM(a.tenant_code) = '')
|
||||
)
|
||||
UPDATE public.rag_chat_app a
|
||||
SET tenant_code = rca.resolved_tenant_code
|
||||
FROM resolved_rag_chat_app rca
|
||||
WHERE a.id = rca.id
|
||||
AND rca.resolved_tenant_code IS NOT NULL;
|
||||
|
||||
WITH user_tenant_map AS (
|
||||
SELECT
|
||||
u.id AS user_id,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(u.tenant_code), ''),
|
||||
am.tenant_code,
|
||||
tn.tenant_code,
|
||||
CASE
|
||||
WHEN u.area IS NULL OR BTRIM(u.area) = '' THEN 'PUBLIC'
|
||||
WHEN LOWER(BTRIM(u.area)) = 'default' THEN 'PUBLIC'
|
||||
WHEN BTRIM(u.area) = '公共' THEN 'PUBLIC'
|
||||
WHEN BTRIM(u.area) IN ('省级', '省局') THEN 'PROVINCIAL'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_code,
|
||||
COALESCE(
|
||||
NULLIF(BTRIM(u.tenant_name), ''),
|
||||
t.tenant_name,
|
||||
NULLIF(BTRIM(u.area), ''),
|
||||
CASE
|
||||
WHEN u.area IS NULL OR BTRIM(u.area) = '' THEN '公共资源域'
|
||||
ELSE NULL
|
||||
END
|
||||
) AS resolved_tenant_name
|
||||
FROM public.sso_users u
|
||||
LEFT JOIN public.sys_tenant_aliases sa
|
||||
ON LOWER(BTRIM(COALESCE(u.area, ''))) = LOWER(BTRIM(sa.alias_value))
|
||||
AND sa.deleted_at IS NULL
|
||||
AND sa.is_enabled = TRUE
|
||||
LEFT JOIN public.sys_tenants t
|
||||
ON t.tenant_code = COALESCE(NULLIF(BTRIM(u.tenant_code), ''), sa.tenant_code)
|
||||
AND t.deleted_at IS NULL
|
||||
AND t.is_enabled = TRUE
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (LOWER(BTRIM(alias_value)))
|
||||
LOWER(BTRIM(alias_value)) AS normalized_alias_value,
|
||||
tenant_code
|
||||
FROM public.sys_tenant_aliases
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
ORDER BY LOWER(BTRIM(alias_value)), id ASC
|
||||
) am
|
||||
ON LOWER(BTRIM(COALESCE(u.area, ''))) = am.normalized_alias_value
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
LOWER(BTRIM(tenant_name)) AS normalized_tenant_name,
|
||||
tenant_code
|
||||
FROM public.sys_tenants
|
||||
WHERE deleted_at IS NULL
|
||||
AND is_enabled = TRUE
|
||||
) tn
|
||||
ON LOWER(BTRIM(COALESCE(u.area, ''))) = tn.normalized_tenant_name
|
||||
WHERE u.deleted_at IS NULL
|
||||
)
|
||||
UPDATE public.usage_login_events e
|
||||
SET tenant_code_snapshot = COALESCE(e.tenant_code_snapshot, utm.resolved_tenant_code),
|
||||
tenant_name_snapshot = COALESCE(e.tenant_name_snapshot, utm.resolved_tenant_name)
|
||||
FROM user_tenant_map utm
|
||||
WHERE e.deleted_at IS NULL
|
||||
AND e.user_id = utm.user_id
|
||||
AND (
|
||||
e.tenant_code_snapshot IS NULL
|
||||
OR BTRIM(e.tenant_code_snapshot) = ''
|
||||
OR e.tenant_name_snapshot IS NULL
|
||||
OR BTRIM(e.tenant_name_snapshot) = ''
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,163 @@
|
||||
-- 租户主数据底座
|
||||
-- 目的:
|
||||
-- 1. 为“地区 -> 租户”升级提供稳定主数据
|
||||
-- 2. 为后续入口模块、RAG、文档、模板等模块提供 tenant_code
|
||||
-- 3. 保持对现有 area / region / tenant_name 的兼容
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_tenants (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL UNIQUE,
|
||||
tenant_name VARCHAR(128) NOT NULL,
|
||||
tenant_short_name VARCHAR(64) NULL,
|
||||
tenant_type VARCHAR(32) NOT NULL,
|
||||
parent_tenant_code VARCHAR(64) NULL,
|
||||
display_order INT NOT NULL DEFAULT 0,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_builtin BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
can_host_entry_module BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_host_documents BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_host_rag BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
can_host_templates BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ext JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_tenants_enabled_order
|
||||
ON sys_tenants(is_enabled, display_order, id)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_tenant_aliases (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL,
|
||||
alias_value VARCHAR(128) NOT NULL,
|
||||
alias_type VARCHAR(32) NOT NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
UNIQUE (tenant_code, alias_value)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_tenant_aliases_lookup
|
||||
ON sys_tenant_aliases(alias_value, alias_type)
|
||||
WHERE deleted_at IS NULL AND is_enabled = TRUE;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_tenant_feature_flags (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_code VARCHAR(64) NOT NULL,
|
||||
feature_key VARCHAR(64) NOT NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ext JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ NULL,
|
||||
UNIQUE (tenant_code, feature_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_tenant_features_lookup
|
||||
ON sys_tenant_feature_flags(tenant_code, feature_key)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
ALTER TABLE sso_users
|
||||
ADD COLUMN IF NOT EXISTS tenant_code VARCHAR(64) NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_tenant_code
|
||||
ON sso_users(tenant_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE sys_tenants IS '系统租户主数据表';
|
||||
COMMENT ON TABLE sys_tenant_aliases IS '租户历史别名与展示别名映射表';
|
||||
COMMENT ON TABLE sys_tenant_feature_flags IS '租户能力开关表';
|
||||
COMMENT ON COLUMN sso_users.tenant_code IS '用户主归属租户编码,优先于 area 作为租户边界';
|
||||
|
||||
INSERT INTO sys_tenants (
|
||||
tenant_code, tenant_name, tenant_short_name, tenant_type, parent_tenant_code,
|
||||
display_order, is_enabled, is_builtin, is_public,
|
||||
can_host_entry_module, can_host_documents, can_host_rag, can_host_templates, ext
|
||||
)
|
||||
VALUES
|
||||
('MZ', '梅州', '梅州', 'LOCAL', NULL, 10, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb),
|
||||
('YF', '云浮', '云浮', 'LOCAL', NULL, 20, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb),
|
||||
('JY', '揭阳', '揭阳', 'LOCAL', NULL, 30, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb),
|
||||
('CZ', '潮州', '潮州', 'LOCAL', NULL, 40, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb),
|
||||
('PROVINCIAL', '省局', '省局', 'HEADQUARTER', NULL, 90, TRUE, TRUE, FALSE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb),
|
||||
('PUBLIC', '公共资源域', '公共', 'PUBLIC', NULL, 100, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, '{}'::jsonb)
|
||||
ON CONFLICT (tenant_code) DO UPDATE
|
||||
SET
|
||||
tenant_name = EXCLUDED.tenant_name,
|
||||
tenant_short_name = EXCLUDED.tenant_short_name,
|
||||
tenant_type = EXCLUDED.tenant_type,
|
||||
parent_tenant_code = EXCLUDED.parent_tenant_code,
|
||||
display_order = EXCLUDED.display_order,
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
is_builtin = EXCLUDED.is_builtin,
|
||||
is_public = EXCLUDED.is_public,
|
||||
can_host_entry_module = EXCLUDED.can_host_entry_module,
|
||||
can_host_documents = EXCLUDED.can_host_documents,
|
||||
can_host_rag = EXCLUDED.can_host_rag,
|
||||
can_host_templates = EXCLUDED.can_host_templates,
|
||||
ext = EXCLUDED.ext,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO sys_tenant_aliases (tenant_code, alias_value, alias_type, is_enabled)
|
||||
VALUES
|
||||
('MZ', '梅州', 'LEGACY_AREA', TRUE),
|
||||
('YF', '云浮', 'LEGACY_AREA', TRUE),
|
||||
('JY', '揭阳', 'LEGACY_AREA', TRUE),
|
||||
('CZ', '潮州', 'LEGACY_AREA', TRUE),
|
||||
('PROVINCIAL', '省局', 'DISPLAY', TRUE),
|
||||
('PUBLIC', '省级', 'LEGACY_REGION', TRUE),
|
||||
('PUBLIC', 'default', 'LEGACY_REGION', TRUE),
|
||||
('PUBLIC', '', 'LEGACY_REGION', TRUE)
|
||||
ON CONFLICT (tenant_code, alias_value) DO UPDATE
|
||||
SET
|
||||
alias_type = EXCLUDED.alias_type,
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
updated_at = NOW();
|
||||
|
||||
INSERT INTO sys_tenant_feature_flags (tenant_code, feature_key, is_enabled)
|
||||
VALUES
|
||||
('MZ', 'home.entry_module', TRUE),
|
||||
('YF', 'home.entry_module', TRUE),
|
||||
('JY', 'home.entry_module', TRUE),
|
||||
('CZ', 'home.entry_module', TRUE),
|
||||
('PROVINCIAL', 'home.entry_module', TRUE),
|
||||
('PUBLIC', 'home.entry_module', TRUE),
|
||||
('MZ', 'rag.dataset', TRUE),
|
||||
('YF', 'rag.dataset', TRUE),
|
||||
('JY', 'rag.dataset', TRUE),
|
||||
('CZ', 'rag.dataset', TRUE),
|
||||
('PROVINCIAL', 'rag.dataset', TRUE),
|
||||
('PUBLIC', 'rag.dataset', TRUE),
|
||||
('MZ', 'documents.upload', TRUE),
|
||||
('YF', 'documents.upload', TRUE),
|
||||
('JY', 'documents.upload', TRUE),
|
||||
('CZ', 'documents.upload', TRUE),
|
||||
('PROVINCIAL', 'documents.upload', TRUE),
|
||||
('PUBLIC', 'documents.upload', TRUE)
|
||||
ON CONFLICT (tenant_code, feature_key) DO UPDATE
|
||||
SET
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
updated_at = NOW();
|
||||
|
||||
UPDATE sso_users u
|
||||
SET tenant_code = mapped.tenant_code
|
||||
FROM (
|
||||
SELECT DISTINCT ON (a.alias_value)
|
||||
a.alias_value,
|
||||
a.tenant_code
|
||||
FROM sys_tenant_aliases a
|
||||
WHERE a.deleted_at IS NULL
|
||||
AND a.is_enabled = TRUE
|
||||
ORDER BY a.alias_value, a.id ASC
|
||||
) mapped
|
||||
WHERE u.deleted_at IS NULL
|
||||
AND COALESCE(BTRIM(u.tenant_code), '') = ''
|
||||
AND COALESCE(BTRIM(u.area), '') = mapped.alias_value;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1 @@
|
||||
-- 预留:页级图片质量模块权限初始化脚本
|
||||
@@ -0,0 +1 @@
|
||||
-- 预留:页级图片质量模块路由初始化脚本
|
||||
@@ -344,9 +344,13 @@ seed(role_key, permission_key, grant_type, data_scope) AS (
|
||||
('admin', 'rules:version_list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:content:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:validate:execute', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:version_create:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:publish:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:rollback:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_create:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_update:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_delete:delete', 'GRANT', 'DEPT'),
|
||||
('admin', 'cross_review:task:create', 'GRANT', 'DEPT'),
|
||||
('admin', 'cross_review:task:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'cross_review:progress:view', 'GRANT', 'DEPT'),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- ============================================================================
|
||||
-- Rule Domain Tenant Phase 1 Verify
|
||||
-- 目标:
|
||||
-- 1. 验证字段与索引已成功补齐
|
||||
-- 2. 验证历史资产已具备基础 tenant_code / scope_type
|
||||
-- 3. 验证运行结果链路具备租户快照
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 字段检查
|
||||
SELECT
|
||||
table_name,
|
||||
column_name
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND (
|
||||
(table_name = 'leaudit_rule_sets' AND column_name IN ('tenant_code', 'scope_type', 'source_rule_set_id', 'tenant_name_snapshot'))
|
||||
OR (table_name = 'leaudit_rule_versions' AND column_name IN ('tenant_code_snapshot', 'scope_type_snapshot', 'source_version_id'))
|
||||
OR (table_name = 'leaudit_rule_group_bindings' AND column_name IN ('tenant_code', 'scope_type', 'tenant_name_snapshot'))
|
||||
OR (table_name = 'leaudit_rule_type_bindings' AND column_name IN ('tenant_code', 'scope_type'))
|
||||
OR (table_name = 'leaudit_audit_runs' AND column_name IN ('tenant_code', 'tenant_name_snapshot', 'scope_type_snapshot', 'group_id_snapshot', 'rule_binding_id_snapshot'))
|
||||
OR (table_name = 'leaudit_rule_results' AND column_name IN ('tenant_code', 'tenant_name_snapshot'))
|
||||
OR (table_name = 'leaudit_run_errors' AND column_name IN ('tenant_code', 'tenant_name_snapshot'))
|
||||
OR (table_name = 'leaudit_run_metrics' AND column_name IN ('tenant_code'))
|
||||
)
|
||||
ORDER BY table_name, column_name;
|
||||
|
||||
-- 2. 历史规则资产 tenant_code / scope_type 覆盖率
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS rule_sets_missing_tenant_code,
|
||||
COUNT(*) FILTER (WHERE scope_type IS NULL OR BTRIM(scope_type) = '') AS rule_sets_missing_scope_type
|
||||
FROM leaudit_rule_sets
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS rule_group_bindings_missing_tenant_code,
|
||||
COUNT(*) FILTER (WHERE scope_type IS NULL OR BTRIM(scope_type) = '') AS rule_group_bindings_missing_scope_type
|
||||
FROM leaudit_rule_group_bindings
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 3. 运行与结果快照覆盖率
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS audit_runs_missing_tenant_code,
|
||||
COUNT(*) FILTER (WHERE scope_type_snapshot IS NULL OR BTRIM(scope_type_snapshot) = '') AS audit_runs_missing_scope_type
|
||||
FROM leaudit_audit_runs;
|
||||
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS rule_results_missing_tenant_code
|
||||
FROM leaudit_rule_results;
|
||||
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS run_errors_missing_tenant_code
|
||||
FROM leaudit_run_errors;
|
||||
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE tenant_code IS NULL OR BTRIM(tenant_code) = '') AS run_metrics_missing_tenant_code
|
||||
FROM leaudit_run_metrics;
|
||||
|
||||
-- 4. 作用域分布,确认默认回填结果
|
||||
SELECT scope_type, COUNT(*) AS total
|
||||
FROM leaudit_rule_sets
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY scope_type
|
||||
ORDER BY scope_type;
|
||||
|
||||
SELECT scope_type, COUNT(*) AS total
|
||||
FROM leaudit_rule_group_bindings
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY scope_type
|
||||
ORDER BY scope_type;
|
||||
Reference in New Issue
Block a user