feat: add tenant-scoped rule and permission management

This commit is contained in:
wren
2026-05-21 22:03:08 +08:00
parent a2c2bf1969
commit 1f1bccf3b3
193 changed files with 64463 additions and 1771 deletions
+591
View File
@@ -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
View File
@@ -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 @@
-- 预留:页级图片质量模块路由初始化脚本
+4
View File
@@ -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;