deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
e966f801cb
commit
e280d09a99
@ -1,7 +1,7 @@
|
||||
{
|
||||
"module": "pbl_scense_ext",
|
||||
"milestone": "M9(覆盖层前端与页面注册)",
|
||||
"note": "pbl_scense_ext 26 条 RBAC 路径权限种子清单(M9-收口C,消 P-1/P-2 pending=26)。逐条显式登记,禁止 % / * 通配符(module-development-spec 硬门禁 6.6)。由 scripts/m9c_rbac_seed.py 幂等导入 permission/rolepermission,并由 scripts/rbac_set_perm.py 作为 scripts/load_path.py register() 的 RBAC_SET_PERM 工具,使 'rbac paths: total=26 ok=26 pending=0'。traceability 中函数名/文件/行号均为 grep 实测值(沿用 pbl_runtime_ext b54ff00 做法)。",
|
||||
"note": "pbl_scense_ext 26 条 RBAC 路径权限种子清单(M9-收口C,消 P-1/P-2 pending=26)。逐条显式登记,禁止 % / * 通配符(module-development-spec 硬门禁 6.6)。消费端:scripts/m9c_rbac_seed.py(--check 离线三方一致性校验 / --emit-sql 出幂等 SQL / --apply 连库幂等导入 permission+rolepermission,id 用 path/role 的 md5 派生);scripts/rbac_set_perm.py 作为 scripts/load_path.py register() 的 RBAC_SET_PERM 工具,使 'rbac paths: total=26 ok=26 pending=0'。traceability 中函数名/文件/行号均为 grep 实测值(沿用 pbl_runtime_ext b54ff00 做法)。",
|
||||
"source_of_truth": "scripts/load_path.py PATHS(26 条,22 logined + 4 any)",
|
||||
"default_role": "logined",
|
||||
"permissions": [
|
||||
@ -299,9 +299,14 @@
|
||||
]
|
||||
},
|
||||
"superadmin_grant": {
|
||||
"note": "超管授权:运行时从 role 表按 id / name 解析真实超管角色(rbac 伪角色 logined/any 之外的实体角色),命中即授予全部 26 条。role 表列集合按 information_schema 取交集后写,缺列不猜。解析不到任何超管角色时打印 WARN 但不 FAIL——logined/any 两条伪角色已按 rbac userperm.py:204/217/298 的语义覆盖全部登录/匿名主体。",
|
||||
"role_ids": ["pbl_admin", "superadmin", "admin"],
|
||||
"role_names": ["pbl_admin", "superadmin", "admin"],
|
||||
"note": "超管授权:由 scripts/m9c_rbac_seed.py::resolve_superadmin() 在导入时运行时解析——先查 role 表列集合(information_schema 取交集),按 role/name/rolelabel 三列命中 role_names 后回填真实 32 位主键 id,再为全部 26 条写 rolepermission。本段刻意不再放 role_ids:rbac role.id 是 32 位主键,把角色名塞进 role_ids 语义错位、按 id 查必零命中并静默不授权(M9-收口C QC 退回意见 #12)。解析不到任何超管实体角色时打印 WARN 并在 dev-notes 留证,不 FAIL——logined/any 两条伪角色已按 rbac userperm.py:204/217/298 语义覆盖全部登录/匿名主体。",
|
||||
"role_names": [
|
||||
"pbl_admin",
|
||||
"superadmin",
|
||||
"admin",
|
||||
"超级管理员",
|
||||
"系统管理员"
|
||||
],
|
||||
"grant_all": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,13 +82,24 @@ for _key in OVERLAY_PAGES:
|
||||
PATHS.append(('/pbl_scense_ext/pbl_overlay', 'logined'))
|
||||
|
||||
|
||||
def register():
|
||||
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
|
||||
def register(verbose=False):
|
||||
"""把 PATHS 逐条登记进 RBAC permission/rolepermission(真落库,不是打印清单)。
|
||||
|
||||
M9-收口C(QC 退回意见 #10):此前默认工具名 `set_role_perm.py` 在工作空间内
|
||||
零命中,register() 必然 26 条全失败(pending=26)。现默认调用本模块
|
||||
`scripts/rbac_set_perm.py`(内部走 m9c_rbac_seed.py 的幂等 upsert,rc=0 才算
|
||||
登记成功);`RBAC_SET_PERM` 环境变量仍可覆盖为外部工具路径。
|
||||
verbose=True 时透传子进程输出(留验收证据用),默认静默只打汇总行。
|
||||
"""
|
||||
default_tool = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'rbac_set_perm.py')
|
||||
tool = os.environ.get('RBAC_SET_PERM') or default_tool
|
||||
done, missing = 0, []
|
||||
for path, role in PATHS:
|
||||
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
|
||||
tool, role, path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
|
||||
cmd = [sys.executable if os.environ.get('PY') else 'python3', tool, role, path]
|
||||
rc = subprocess.call(cmd, stdout=None if verbose else subprocess.DEVNULL,
|
||||
stderr=None if verbose else subprocess.DEVNULL)
|
||||
if rc == 0:
|
||||
done += 1
|
||||
else:
|
||||
missing.append((path, role))
|
||||
|
||||
536
scripts/m9c_rbac_seed.py
Normal file
536
scripts/m9c_rbac_seed.py
Normal file
@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M9-收口C 权限种子导入器(pbl_scense_ext 覆盖层 26 条路径,消 pending=26)。
|
||||
|
||||
背景:M9-收口B 一致性核验实测 `scripts/load_path.py`(register 分支)rc=1、
|
||||
`rbac paths: total=26 ok=0 pending=26` —— 声明侧合规(--audit rc=0)但权限表零
|
||||
落库,线上覆盖层 403。本脚本把 init/rbac_seed.json 的 26 条 permission /
|
||||
rolepermission 种子**幂等**导入应用库,并作为 scripts/rbac_set_perm.py 的底层
|
||||
实现(load_path.register() 的 RBAC_SET_PERM 工具)。
|
||||
|
||||
实现模式对齐同项目已收口的 modules/pbl_runtime_ext/scripts/m11b34_rbac_seed.py
|
||||
(commit e141be2 幂等种子导入器 + b54ff00 traceability grep 实测)。
|
||||
|
||||
硬规定(module-development-spec 硬门禁 6.6 / rbac-permission-initialization
|
||||
-pattern / sqlor-database-module):
|
||||
1. **逐条显式登记,禁 `%` / `*` / `?` 通配符**——离线校验命中即 FAIL,不写半截。
|
||||
2. **permission.path 唯一语义**:一个 path 一行 permission;多角色靠多条
|
||||
rolepermission 指向同一 permid。rolepermission.id 显式生成(rbac 表无自增)。
|
||||
3. **不猜列名**:permission/rolepermission/role 的实际列集合运行时从
|
||||
information_schema 读取,只写交集列;缺关键列直接 FAIL 而非静默跳过。
|
||||
4. **幂等**:permission 按 path 查重(存在则 UPDATE 非键列),rolepermission 按
|
||||
(roleid, permid) 查重;id 用 md5(path)/md5(role|path) 派生的稳定 32 位串,
|
||||
重复执行零差异、行数不增。
|
||||
5. **logined / any 语义不混用**:种子里每条 permission 的 role 必须与其所在
|
||||
role_grants 分桶一致,交叉即 FAIL。
|
||||
6. **库名/凭据不硬编码**:dbname 依次取 --dbname > $PBL_SCENSE_EXT_DBNAME >
|
||||
应用 conf/config.json 的 module_dbname 映射;连接参数取
|
||||
databases[dbname].kwargs,可被 $PBL_DB_HOST/PORT/USER/PASSWORD 覆盖
|
||||
(沙箱库验收用)。解析不到库名 → 直接 FAIL,不猜。
|
||||
7. **超管授权运行时解析**(QC #12):seed.superadmin_grant.role_names 是**角色
|
||||
名称**候选,导入时查 role 表(列交集,按 role/name/rolelabel 命中)取真实
|
||||
32 位 id 再写 rolepermission;解析不到打印 WARN 并在退出码中保持 0(伪角色
|
||||
logined/any 已按 rbac userperm.py 语义覆盖全部登录/匿名主体),绝不静默。
|
||||
|
||||
用法:
|
||||
python3 scripts/m9c_rbac_seed.py --check # 离线校验,不连库(rc=0 即 errors=0)
|
||||
python3 scripts/m9c_rbac_seed.py --emit-sql x.sql # 出幂等 SQL(交 build.sh 第 9 步 import_rp)
|
||||
python3 scripts/m9c_rbac_seed.py --apply # 连库幂等导入 + 库内回查
|
||||
python3 scripts/m9c_rbac_seed.py --report # 连库只读回查 26 条落库行数
|
||||
python3 scripts/m9c_rbac_seed.py --upsert-one logined /pbl_scense_ext/xxx.dspy
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
MODULE_DIR = os.path.dirname(HERE)
|
||||
MODULE = 'pbl_scense_ext'
|
||||
APP_NAME = 'pbls'
|
||||
SEED_JSON = os.path.join(MODULE_DIR, 'init', 'rbac_seed.json')
|
||||
WWWROOT = os.path.join(MODULE_DIR, 'wwwroot')
|
||||
LOAD_PATH_PY = os.path.join(HERE, 'load_path.py')
|
||||
|
||||
WILDCARD_RE = re.compile(r'[%*?]')
|
||||
# rbac 真实列(information_schema 取交集后才使用,缺列 = FAIL,不静默降级)
|
||||
PERM_REQUIRED = ['id', 'path']
|
||||
PERM_OPTIONAL = ['name', 'description', 'ptype', 'parentid', 'title', 'icon']
|
||||
RP_REQUIRED = ['id', 'roleid', 'permid']
|
||||
# role 表里可能承载「角色名称」的列(按实测 DDL:id/orgtypeid/role/rolelabel/name)
|
||||
ROLE_NAME_COLS = ['role', 'name', 'rolelabel']
|
||||
# permission.ptype 按资源类别区分(api 端点 / page 页面壳 / static 静态资源)
|
||||
PTYPE_BY_KIND = {'api': 'api', 'page': 'page', 'static': 'static'}
|
||||
# rbac 伪角色(不是 role 表实体行,userperm.py 按主体类型直接判定;这里作为
|
||||
# rolepermission.roleid 的授权键,与 pbl_runtime_ext 已收口做法一致)
|
||||
PSEUDO_ROLES = ('logined', 'any')
|
||||
|
||||
|
||||
def load_seed():
|
||||
with open(SEED_JSON, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def stable_id(prefix, *parts):
|
||||
"""32 位稳定 id(幂等;不用 UUID——UUID 每次不同会插重复行)。"""
|
||||
h = hashlib.md5(('|'.join([MODULE] + [str(p) for p in parts])).encode('utf-8'))
|
||||
return ('%s%s' % (prefix, h.hexdigest()))[:32]
|
||||
|
||||
|
||||
def load_path_pairs():
|
||||
"""导入 scripts/load_path.py 读 PATHS(含循环 append 的 12 条页面壳)。
|
||||
|
||||
用 importlib 而非 AST:load_path.PATHS 由字面量 + for 循环 append 构成,
|
||||
ast.literal_eval 只能拿到字面量部分(会漏 13 条),三方一致性校验会假红。
|
||||
load_path.py 有 __main__ 守卫,import 无副作用。
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location('_pbl_scense_ext_load_path', LOAD_PATH_PY)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return [(p, r) for p, r in mod.PATHS], mod
|
||||
|
||||
|
||||
def path_to_fs(path):
|
||||
"""URL 路径 → wwwroot 下期望文件/目录(目录型条目返回目录)。"""
|
||||
marker = '/%s' % MODULE
|
||||
if not path.startswith(marker):
|
||||
return None, None
|
||||
rel = path[len(marker):].lstrip('/')
|
||||
fs = os.path.join(WWWROOT, *rel.split('/')) if rel else WWWROOT
|
||||
return rel, fs
|
||||
|
||||
|
||||
def static_validate(seed):
|
||||
"""离线三方一致性(种子 ↔ load_path.PATHS ↔ 磁盘文件)+ 通配符/语义门禁。"""
|
||||
errors, warns = [], []
|
||||
perms = seed.get('permissions') or []
|
||||
if not perms:
|
||||
return False, ['init/rbac_seed.json permissions 为空'], []
|
||||
|
||||
pairs, _mod = load_path_pairs()
|
||||
registered = set(pairs)
|
||||
names, paths = [], set()
|
||||
|
||||
for p in perms:
|
||||
name, path, role = p.get('name') or '', p.get('path') or '', p.get('role') or ''
|
||||
if name in names:
|
||||
errors.append('重复权限名: %s' % name)
|
||||
names.append(name)
|
||||
if path in paths:
|
||||
errors.append('重复权限路径(permission.path 必须唯一): %s' % path)
|
||||
paths.add(path)
|
||||
if WILDCARD_RE.search(path):
|
||||
errors.append('禁止通配符(硬政策): %s' % path)
|
||||
if not path.startswith('/%s' % MODULE):
|
||||
errors.append('路径前缀不符合模块自动路由 /%s: %s' % (MODULE, path))
|
||||
if role not in PSEUDO_ROLES:
|
||||
errors.append('role 必须是伪角色 %s 之一: %s -> %s' % (PSEUDO_ROLES, name, role))
|
||||
rel, fs = path_to_fs(path)
|
||||
if fs and not (os.path.exists(fs) or os.path.isdir(fs)):
|
||||
errors.append('磁盘缺资源文件: %s (期望 %s)' % (path, os.path.relpath(fs, MODULE_DIR)))
|
||||
for key in ('traceability', 'milestone', 'title', 'kind', 'endpoint'):
|
||||
if not p.get(key):
|
||||
errors.append('缺 %s 字段: %s' % (key, name))
|
||||
|
||||
# 双向集合一致:种子 26 条 == load_path.PATHS 26 条(同 path 同 role)
|
||||
seed_pairs = set((p['path'], p['role']) for p in perms)
|
||||
for path, role in sorted(registered - seed_pairs):
|
||||
errors.append('load_path.py 有登记但种子缺失: %s (%s)' % (path, role))
|
||||
for path, role in sorted(seed_pairs - registered):
|
||||
errors.append('种子有但 load_path.py PATHS 未登记: %s (%s)' % (path, role))
|
||||
|
||||
grants = seed.get('role_grants') or {}
|
||||
granted = set()
|
||||
for role, lst in grants.items():
|
||||
for gname in lst:
|
||||
granted.add(gname)
|
||||
hit = [p for p in perms if p.get('name') == gname]
|
||||
if not hit:
|
||||
errors.append('role_grants[%s] 引用不存在的权限名: %s' % (role, gname))
|
||||
elif hit[0].get('role') != role:
|
||||
# 退回意见 #12:logined / any 语义不得混用
|
||||
errors.append('role_grants 分桶与 permission.role 不一致(语义混用): %s 在 [%s] 桶但 role=%s'
|
||||
% (gname, role, hit[0].get('role')))
|
||||
for p in perms:
|
||||
if p['name'] not in granted:
|
||||
warns.append('权限未进入任何 role_grants 分桶: %s' % p['name'])
|
||||
|
||||
# 超管段语义(退回意见 #12):role_ids 不得塞角色名;要么留真实 id,要么空
|
||||
sup = seed.get('superadmin_grant') or {}
|
||||
for rid in sup.get('role_ids') or []:
|
||||
if not re.match(r'^[0-9a-f]{32}$', str(rid)):
|
||||
errors.append('superadmin_grant.role_ids 含非 32 位主键值(角色名应放 role_names): %s' % rid)
|
||||
if not sup.get('role_names'):
|
||||
warns.append('superadmin_grant.role_names 为空:超管授权只能靠 role 表解析结果,未配置候选名')
|
||||
return (not errors), errors, warns
|
||||
|
||||
|
||||
# ── 库名 / 连接(禁止硬编码 DBNAME 与凭据)──────────────────────────────────
|
||||
def app_conf_candidates():
|
||||
ws = os.path.dirname(os.path.dirname(MODULE_DIR))
|
||||
return [os.path.join(ws, 'apps', APP_NAME, 'conf', 'config.json'),
|
||||
os.environ.get('PBL_APP_CONF', '')]
|
||||
|
||||
|
||||
def read_app_conf():
|
||||
for path in app_conf_candidates():
|
||||
if path and os.path.isfile(path):
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f), path
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return None, None
|
||||
|
||||
|
||||
def resolve_dbname(args):
|
||||
"""dbname 优先级:--dbname > $PBL_SCENSE_EXT_DBNAME > 应用 module_dbname 映射。"""
|
||||
if getattr(args, 'dbname', None):
|
||||
return args.dbname, '--dbname'
|
||||
env_name = os.environ.get('PBL_SCENSE_EXT_DBNAME')
|
||||
if env_name:
|
||||
return env_name, '$PBL_SCENSE_EXT_DBNAME'
|
||||
conf, path = read_app_conf()
|
||||
if conf:
|
||||
m = conf.get('module_dbname') or {}
|
||||
name = m.get(MODULE) or m.get('default')
|
||||
if name:
|
||||
return name, 'conf:%s' % path
|
||||
return None, 'unresolved'
|
||||
|
||||
|
||||
def db_kwargs(dbname):
|
||||
"""连接参数:应用 conf 的 databases[dbname].kwargs,允许 $PBL_DB_* 覆盖。"""
|
||||
conf, _path = read_app_conf()
|
||||
kw = {}
|
||||
entry = ((conf or {}).get('databases') or {}).get(dbname) or {}
|
||||
kw = dict(entry.get('kwargs') or {})
|
||||
kw.pop('dbname', None)
|
||||
kw.pop('db', None)
|
||||
kw.pop('charset', None)
|
||||
if entry.get('password') and 'password' not in kw:
|
||||
kw['password'] = entry.get('password')
|
||||
kw.setdefault('host', os.environ.get('PBL_DB_HOST', '127.0.0.1'))
|
||||
kw.setdefault('port', int(os.environ.get('PBL_DB_PORT', '3306')))
|
||||
for env_key, key in (('PBL_DB_USER', 'user'), ('PBL_DB_PASSWORD', 'password'),
|
||||
('PBL_DB_SCHEMA', 'database')):
|
||||
if os.environ.get(env_key):
|
||||
kw[key] = os.environ[env_key]
|
||||
# conf 里没有该 dbname 的条目时(如沙箱库 --dbname),database 直接取解析出的库名
|
||||
kw.setdefault('database', dbname)
|
||||
kw['charset'] = 'utf8mb4'
|
||||
kw['autocommit'] = False
|
||||
kw['connect_timeout'] = 6
|
||||
return kw
|
||||
|
||||
|
||||
def get_conn(dbname):
|
||||
try:
|
||||
import pymysql
|
||||
except ImportError:
|
||||
return None
|
||||
return pymysql.connect(**db_kwargs(dbname))
|
||||
|
||||
|
||||
def table_columns(cur, dbname, table):
|
||||
cur.execute("SELECT COLUMN_NAME FROM information_schema.COLUMNS "
|
||||
"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", (dbname, table))
|
||||
return [r[0] for r in cur.fetchall()]
|
||||
|
||||
|
||||
# ── 行构造(id 稳定 → 幂等)────────────────────────────────────────────────
|
||||
def perm_row(p, cols):
|
||||
pid = stable_id('rp_', p['path'])
|
||||
vals = {
|
||||
'id': pid,
|
||||
'path': p['path'],
|
||||
'name': p['name'],
|
||||
'title': p.get('title') or p['name'],
|
||||
'ptype': PTYPE_BY_KIND.get(p.get('kind'), 'api'),
|
||||
'parentid': MODULE,
|
||||
'description': '%s | %s | role=%s | %s' % (
|
||||
p.get('title') or p['name'], p.get('milestone') or '',
|
||||
p.get('role') or '', (p.get('traceability') or '')[:120]),
|
||||
'icon': p.get('kind') or 'api',
|
||||
}
|
||||
return pid, {c: vals.get(c) for c in cols}
|
||||
|
||||
|
||||
def rp_row(role, pid, path, cols):
|
||||
rrow = {'id': stable_id('rr_', role, path)}
|
||||
if 'roleid' in cols:
|
||||
rrow['roleid'] = role
|
||||
if 'permid' in cols:
|
||||
rrow['permid'] = pid
|
||||
return rrow
|
||||
|
||||
|
||||
def build_rows(seed, perm_cols, rp_cols, superadmin_ids=()):
|
||||
"""permission 每 path 一行;rolepermission = 伪角色分桶 + 解析到的超管实体角色。"""
|
||||
perm_rows, rp_rows, name_to = [], [], {}
|
||||
for p in seed['permissions']:
|
||||
pid, row = perm_row(p, perm_cols)
|
||||
name_to[p['name']] = (pid, p['path'])
|
||||
perm_rows.append((pid, row))
|
||||
for role, lst in (seed.get('role_grants') or {}).items():
|
||||
for n in lst:
|
||||
hit = name_to.get(n)
|
||||
if hit:
|
||||
rp_rows.append(rp_row(role, hit[0], hit[1], rp_cols))
|
||||
for srole in superadmin_ids:
|
||||
for n, (pid, path) in name_to.items():
|
||||
rp_rows.append(rp_row(srole, pid, path, rp_cols))
|
||||
return perm_rows, rp_rows
|
||||
|
||||
|
||||
def upsert(cur, table, cols, rows, key_col, stats, key_val=None):
|
||||
"""按 key_col 查重:存在 UPDATE 非键列,不存在 INSERT。rows 为 dict 列表。"""
|
||||
for r in rows:
|
||||
kv = key_val(r) if key_val else r[key_col]
|
||||
cur.execute('SELECT id FROM `%s` WHERE `%s` = %%s' % (table, key_col), (kv,))
|
||||
hit = cur.fetchone()
|
||||
present = [c for c in cols if c in r]
|
||||
if hit:
|
||||
sets = [c for c in present if c != 'id']
|
||||
if sets:
|
||||
cur.execute('UPDATE `%s` SET %s WHERE id = %%s'
|
||||
% (table, ', '.join('`%s` = %%s' % c for c in sets)),
|
||||
[r[c] for c in sets] + [hit[0]])
|
||||
stats['%s_updated' % table] = stats.get('%s_updated' % table, 0) + 1
|
||||
else:
|
||||
ph = ', '.join(['%s'] * len(present))
|
||||
cur.execute('INSERT INTO `%s` (%s) VALUES (%s)'
|
||||
% (table, ', '.join('`%s`' % c for c in present), ph),
|
||||
[r[c] for c in present])
|
||||
stats['%s_inserted' % table] = stats.get('%s_inserted' % table, 0) + 1
|
||||
return stats
|
||||
|
||||
|
||||
def resolve_superadmin(cur, dbname, seed, stats):
|
||||
"""退回意见 #12:运行时按 role_names 查 role 表取真实 32 位 id,回填后授权。"""
|
||||
sup = seed.get('superadmin_grant') or {}
|
||||
names = [n for n in (sup.get('role_names') or []) if n]
|
||||
configured_ids = [r for r in (sup.get('role_ids') or []) if re.match(r'^[0-9a-f]{32}$', str(r))]
|
||||
if not sup.get('grant_all'):
|
||||
print(' INFO superadmin_grant.grant_all=false,跳过超管授权')
|
||||
return configured_ids
|
||||
cols = table_columns(cur, dbname, 'role')
|
||||
if not cols:
|
||||
stats['superadmin'] = 'WARN: role 表不存在,未授权超管'
|
||||
print(' WARN role 表不存在(rbac 未建表),超管授权跳过')
|
||||
return configured_ids
|
||||
name_cols = [c for c in ROLE_NAME_COLS if c in cols]
|
||||
found = list(configured_ids)
|
||||
for nc in name_cols:
|
||||
for n in names:
|
||||
cur.execute('SELECT id FROM `role` WHERE `%s` = %%s' % nc, (n,))
|
||||
for row in cur.fetchall():
|
||||
rid = str(row[0])
|
||||
if len(rid) <= 32 and rid not in found:
|
||||
found.append(rid)
|
||||
print(' OK 超管角色解析命中: role.%s=%s -> id=%s' % (nc, n, rid))
|
||||
if not found:
|
||||
stats['superadmin'] = 'WARN: 未解析到超管实体角色(候选名 %s)' % names
|
||||
print(' WARN 未解析到超管实体角色(候选 %s,扫描列 %s);'
|
||||
'logined/any 伪角色已覆盖全部登录/匿名主体,不 FAIL' % (names, name_cols))
|
||||
else:
|
||||
stats['superadmin_resolved'] = len(found)
|
||||
return found
|
||||
|
||||
|
||||
def emit_sql(seed, perm_cols, rp_cols, out_path, superadmin_ids=()):
|
||||
perm_rows, rp_rows = build_rows(seed, perm_cols, rp_cols, superadmin_ids)
|
||||
lines = ['-- M9-收口C pbl_scense_ext 覆盖层权限种子(26 条,逐条显式,无 % / * 通配符)',
|
||||
'-- 由 scripts/m9c_rbac_seed.py 生成;id 为 path/role 的 md5 派生 → 幂等(可重复执行)',
|
||||
'-- 用法:交 apps/pbls/build.sh 第 9 步(import_rp)或直接 mysql < 本文件',
|
||||
'']
|
||||
for _pid, r in perm_rows:
|
||||
present = [c for c in perm_cols if c in r]
|
||||
vals = ', '.join("'%s'" % (str(r[c]).replace("'", "''")) for c in present)
|
||||
lines.append('INSERT INTO `permission` (%s) VALUES (%s)'
|
||||
% (', '.join('`%s`' % c for c in present), vals)
|
||||
+ ' ON DUPLICATE KEY UPDATE '
|
||||
+ ', '.join('`%s`=VALUES(`%s`)' % (c, c) for c in present if c != 'id') + ';')
|
||||
lines.append('')
|
||||
for r in rp_rows:
|
||||
present = [c for c in rp_cols if c in r]
|
||||
vals = ', '.join("'%s'" % r[c] for c in present)
|
||||
lines.append('INSERT INTO `rolepermission` (%s) VALUES (%s)'
|
||||
% (', '.join('`%s`' % c for c in present), vals)
|
||||
+ ' ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);')
|
||||
lines += ['', '-- 校验:permission 应为 26 行,rolepermission 应为 26 行(logined 22 + any 4)',
|
||||
"SELECT COUNT(1) AS perm_cnt FROM `permission` WHERE `parentid` = '%s';" % MODULE]
|
||||
with open(out_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
return len(perm_rows), len(rp_rows)
|
||||
|
||||
|
||||
def verify_db(cur, dbname, seed, stats):
|
||||
paths = [p['path'] for p in seed['permissions']]
|
||||
ph = ', '.join(['%s'] * len(paths))
|
||||
cur.execute('SELECT COUNT(1) FROM `permission` WHERE `path` IN (%s)' % ph, paths)
|
||||
stats['verify_permission_in_db'] = cur.fetchone()[0]
|
||||
cur.execute('SELECT COUNT(1) FROM `rolepermission` rp JOIN `permission` p '
|
||||
'ON rp.permid = p.id WHERE p.path IN (%s)' % ph, paths)
|
||||
stats['verify_rolepermission_in_db'] = cur.fetchone()[0]
|
||||
cur.execute('SELECT COUNT(DISTINCT rp.roleid) FROM `rolepermission` rp JOIN '
|
||||
'`permission` p ON rp.permid = p.id WHERE p.path IN (%s)' % ph, paths)
|
||||
stats['verify_distinct_roles'] = cur.fetchone()[0]
|
||||
return stats
|
||||
|
||||
|
||||
def report_db(cur, dbname, seed):
|
||||
"""逐条打印 26 条路径在 permission / rolepermission 的实测行数(验收证据)。"""
|
||||
print(' %-58s %-8s %6s %6s' % ('path', 'role', 'perm', 'rolem'))
|
||||
total_p = total_r = 0
|
||||
for p in seed['permissions']:
|
||||
cur.execute('SELECT id FROM `permission` WHERE `path` = %s', (p['path'],))
|
||||
rows = cur.fetchall()
|
||||
pid = rows[0][0] if rows else None
|
||||
if pid:
|
||||
cur.execute('SELECT COUNT(1) FROM `rolepermission` WHERE `permid` = %s', (pid,))
|
||||
rc = cur.fetchone()[0]
|
||||
else:
|
||||
rc = 0
|
||||
total_p += len(rows)
|
||||
total_r += rc
|
||||
print(' %-58s %-8s %6d %6d' % (p['path'], p['role'], len(rows), rc))
|
||||
print(' TOTAL permission=%d rolepermission=%d(期望 26 / >=26)' % (total_p, total_r))
|
||||
return total_p, total_r
|
||||
|
||||
|
||||
def apply_seed(args, single=None):
|
||||
"""连库幂等导入。single=(role, path) 时只 upsert 一条(rbac_set_perm.py 用)。"""
|
||||
seed = load_seed()
|
||||
ok, errors, warns = static_validate(seed)
|
||||
for e in errors:
|
||||
print(' FAIL ' + e)
|
||||
if not ok:
|
||||
return 1
|
||||
dbname, src = resolve_dbname(args)
|
||||
if not dbname:
|
||||
print(' FAIL 无法解析 %s 的库名(--dbname / $PBL_SCENSE_EXT_DBNAME / '
|
||||
'应用 conf module_dbname 均缺)' % MODULE)
|
||||
return 1
|
||||
try:
|
||||
conn = get_conn(dbname)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(' SKIP 连库失败(%s):DB 不可达时用 --emit-sql 出 SQL,'
|
||||
'交 build.sh 第 9 步(import_rp)导入' % str(e)[:120])
|
||||
return 2
|
||||
if conn is None:
|
||||
print(' SKIP 无 pymysql:改用 --emit-sql 出 SQL 交 build.sh 第 9 步导入')
|
||||
return 2
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
perm_cols = table_columns(cur, dbname, 'permission')
|
||||
rp_cols = table_columns(cur, dbname, 'rolepermission')
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(' SKIP 无法读取 %s 库的 rbac 表结构(%s)' % (dbname, str(e)[:80]))
|
||||
conn.close()
|
||||
return 2
|
||||
if not perm_cols or not rp_cols:
|
||||
print(' SKIP 库 %s 缺 permission/rolepermission 表(rbac 未建表),本任务不冒充成功' % dbname)
|
||||
conn.close()
|
||||
return 2
|
||||
missing = [c for c in PERM_REQUIRED if c not in perm_cols] \
|
||||
+ [c for c in RP_REQUIRED if c not in rp_cols]
|
||||
if missing:
|
||||
print(' FAIL rbac 表缺关键列: %s(实际 %s / %s)' % (missing, perm_cols, rp_cols))
|
||||
conn.close()
|
||||
return 1
|
||||
use_perm = [c for c in PERM_REQUIRED + PERM_OPTIONAL if c in perm_cols]
|
||||
use_rp = [c for c in RP_REQUIRED if c in rp_cols]
|
||||
|
||||
if single:
|
||||
role, path = single
|
||||
hit = [p for p in seed['permissions'] if p['path'] == path]
|
||||
if not hit:
|
||||
print(' FAIL 种子清单里没有这条路径: %s' % path)
|
||||
conn.close()
|
||||
return 1
|
||||
if hit[0]['role'] != role:
|
||||
print(' FAIL 角色与种子声明不一致: %s 应为 %s(传入 %s)'
|
||||
% (path, hit[0]['role'], role))
|
||||
conn.close()
|
||||
return 1
|
||||
pid, row = perm_row(hit[0], use_perm)
|
||||
stats = {}
|
||||
stats = upsert(cur, 'permission', use_perm, [row], 'path', stats)
|
||||
stats = upsert(cur, 'rolepermission', use_rp,
|
||||
[rp_row(role, pid, path, use_rp)], 'id', stats)
|
||||
conn.commit()
|
||||
print('[%s] upsert-one ok: role=%s path=%s %s' % (MODULE, role, path,
|
||||
json.dumps(stats, ensure_ascii=False)))
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
stats = {}
|
||||
sup_ids = resolve_superadmin(cur, dbname, seed, stats)
|
||||
perm_rows, rp_rows = build_rows(seed, use_perm, use_rp, sup_ids)
|
||||
if not (args.apply or args.report):
|
||||
print(' DRY:加 --apply 才写库,或 --emit-sql 出 SQL')
|
||||
conn.close()
|
||||
return 0
|
||||
stats = upsert(cur, 'permission', use_perm, [r for _, r in perm_rows], 'path', stats)
|
||||
stats = upsert(cur, 'rolepermission', use_rp, rp_rows, 'id', stats)
|
||||
conn.commit()
|
||||
verify_db(cur, dbname, seed, stats)
|
||||
print('[%s] 导入结果 dbname=%s(%s): %s'
|
||||
% (MODULE, dbname, src, json.dumps(stats, ensure_ascii=False)))
|
||||
if args.report:
|
||||
report_db(cur, dbname, seed)
|
||||
expect_p = len(seed['permissions'])
|
||||
good = stats.get('verify_permission_in_db') == expect_p \
|
||||
and stats.get('verify_rolepermission_in_db', 0) >= expect_p
|
||||
print(' %s 库内回查:permission=%s rolepermission=%s(期望 %d / >=%d)'
|
||||
% ('OK' if good else 'FAIL', stats.get('verify_permission_in_db'),
|
||||
stats.get('verify_rolepermission_in_db'), expect_p, expect_p))
|
||||
conn.close()
|
||||
return 0 if good else 1
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--check', action='store_true', help='只做离线校验,不连库')
|
||||
ap.add_argument('--apply', action='store_true', help='连库幂等导入')
|
||||
ap.add_argument('--report', action='store_true', help='连库幂等导入 + 逐条落库行数回查')
|
||||
ap.add_argument('--emit-sql', default=None, help='输出幂等 SQL 文件(不连库)')
|
||||
ap.add_argument('--upsert-one', nargs=2, metavar=('ROLE', 'PATH'),
|
||||
help='单条幂等 upsert(scripts/rbac_set_perm.py 的内部入口)')
|
||||
ap.add_argument('--dbname', default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
seed = load_seed()
|
||||
ok, errors, warns = static_validate(seed)
|
||||
pairs, _mod = load_path_pairs()
|
||||
print('[%s] rbac_seed 静态校验: seed=%d load_path.PATHS=%d errors=%d warns=%d'
|
||||
% (MODULE, len(seed['permissions']), len(pairs), len(errors), len(warns)))
|
||||
for e in errors:
|
||||
print(' FAIL ' + e)
|
||||
for w in warns:
|
||||
print(' WARN ' + w)
|
||||
if not ok:
|
||||
return 1
|
||||
print(' OK 26 条路径逐条显式登记(无 %% / * 通配符);'
|
||||
'种子 ↔ load_path.PATHS ↔ 磁盘资源 三方一致;logined/any 分桶无混用'
|
||||
% ())
|
||||
if args.check:
|
||||
print('[%s] --check 通过(离线,未连库)' % MODULE)
|
||||
return 0
|
||||
if args.upsert_one:
|
||||
return apply_seed(args, single=tuple(args.upsert_one))
|
||||
if args.emit_sql:
|
||||
np_, nr = emit_sql(seed, PERM_REQUIRED + PERM_OPTIONAL, RP_REQUIRED, args.emit_sql)
|
||||
print(' SQL 已落盘: %s (permission=%d rolepermission=%d,列名取 rbac 标准列)'
|
||||
% (args.emit_sql, np_, nr))
|
||||
return 0
|
||||
return apply_seed(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
64
scripts/rbac_set_perm.py
Normal file
64
scripts/rbac_set_perm.py
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""RBAC 单条权限登记 CLI(M9-收口C,QC 退回意见 #10 方案 a)。
|
||||
|
||||
接口(与 scripts/load_path.py register() 的 subprocess 调用契约严格一致):
|
||||
|
||||
python3 scripts/rbac_set_perm.py <role> <path>
|
||||
例:python3 scripts/rbac_set_perm.py logined /pbl_scense_ext/api/pbl_overlay_bootstrap.dspy
|
||||
|
||||
退出码:rc=0 表示该 (role, path) 已幂等落库 permission + rolepermission;
|
||||
rc!=0 表示未落库(参数非法 / 路径不在种子清单 / DB 不可达)。
|
||||
**不猜、不静默成功**——DB 不可达时返回 2,由 load_path.py 计入 pending,
|
||||
使 `total=26 ok=26 pending=0` 只在真实落库时才成立。
|
||||
|
||||
实现下沉到 scripts/m9c_rbac_seed.py 的 --upsert-one(幂等 upsert,id 由
|
||||
path/role 的 md5 派生,重复执行零差异),本文件只做 CLI 适配与参数校验,
|
||||
不重复实现导入逻辑。
|
||||
|
||||
被 apps/pbls/build.sh 第 9 步(import_rp)经 load_path.register() 调用;
|
||||
RBAC_SET_PERM 环境变量可覆盖本工具路径(保持向后兼容)。
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SEED_TOOL = os.path.join(HERE, 'm9c_rbac_seed.py')
|
||||
MODULE = 'pbl_scense_ext'
|
||||
PSEUDO_ROLES = ('logined', 'any')
|
||||
WILDCARD_RE = re.compile(r'[%*?]')
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 3:
|
||||
print('usage: rbac_set_perm.py <role> <path> (role in %s)' % (PSEUDO_ROLES,))
|
||||
return 64
|
||||
role, path = argv[1], argv[2]
|
||||
# 参数硬校验:与 module-development-spec 硬门禁 6.6 一致,通配符零容忍
|
||||
if role not in PSEUDO_ROLES:
|
||||
print('FAIL 角色必须是伪角色 %s(rbac userperm.py 语义),收到 %s' % (PSEUDO_ROLES, role))
|
||||
return 64
|
||||
if WILDCARD_RE.search(path):
|
||||
print('FAIL 禁止通配符(硬政策): %s' % path)
|
||||
return 64
|
||||
if not path.startswith('/%s' % MODULE):
|
||||
print('FAIL 路径必须落在模块自动路由 /%s 下: %s' % (MODULE, path))
|
||||
return 64
|
||||
if not os.path.isfile(SEED_TOOL):
|
||||
print('FAIL 缺少导入器 %s' % SEED_TOOL)
|
||||
return 66
|
||||
cmd = [sys.executable, SEED_TOOL, '--upsert-one', role, path]
|
||||
if os.environ.get('PBL_SCENSE_EXT_DBNAME'):
|
||||
cmd += ['--dbname', os.environ['PBL_SCENSE_EXT_DBNAME']]
|
||||
rc = subprocess.call(cmd)
|
||||
if rc == 0:
|
||||
print('OK %-8s %s' % (role, path))
|
||||
else:
|
||||
print('PENDING(rc=%d) %-8s %s' % (rc, role, path))
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
60
sql/m9c_rbac_seed.sql
Normal file
60
sql/m9c_rbac_seed.sql
Normal file
@ -0,0 +1,60 @@
|
||||
-- M9-收口C pbl_scense_ext 覆盖层权限种子(26 条,逐条显式,无 % / * 通配符)
|
||||
-- 由 scripts/m9c_rbac_seed.py 生成;id 为 path/role 的 md5 派生 → 幂等(可重复执行)
|
||||
-- 用法:交 apps/pbls/build.sh 第 9 步(import_rp)或直接 mysql < 本文件
|
||||
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_1ba8c90c3d0b00842b74b28c90cd2', '/pbl_scense_ext/api/pbl_session_context_get.dspy', 'pbl_session_context_get', '会话上下文读取(租户/世界/角色) | M9 | role=logined | pbl_scense_ext/api.py::pbl_session_context_get(api.py:102)→ __init__.py:9 → init.py:20 env 挂载 → wwwroot/api/pbl_session_', 'api', 'pbl_scense_ext', '会话上下文读取(租户/世界/角色)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_0ab941aff9922b4b10788d072a4ed', '/pbl_scense_ext/api/pbl_page_registry_list.dspy', 'pbl_page_registry_list', '覆盖层 12 页面注册表(含渲染时延预算) | M9 | role=logined | pbl_scense_ext/api.py::pbl_page_registry_list(api.py:46,读 PAGES api.py:16)→ __init__.py:10 → init.py:21 → wwwroot/api/pb', 'api', 'pbl_scense_ext', '覆盖层 12 页面注册表(含渲染时延预算)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_e25b1aac374944df4ec6432449f9a', '/pbl_scense_ext/api/pbl_playtest_feedback_save.dspy', 'pbl_playtest_feedback_save', 'Playtest 反馈/反思落库 | M9 | role=logined | pbl_scense_ext/api.py::pbl_playtest_feedback_save(api.py:113)→ __init__.py:11 → init.py:22 → wwwroot/api/pbl_playtest_fe', 'api', 'pbl_scense_ext', 'Playtest 反馈/反思落库', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_3e9fe75350098fe2f5bfe96e4cb51', '/pbl_scense_ext/api/pbl_overlay_bootstrap.dspy', 'pbl_overlay_bootstrap', '覆盖层启动上下文(bootstrap) | M9 | role=logined | pbl_scense_ext/api.py::pbl_overlay_bootstrap(api.py:62)→ __init__.py:12 → init.py:23 → wwwroot/api/pbl_overlay_bootstrap', 'api', 'pbl_scense_ext', '覆盖层启动上下文(bootstrap)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_2fc7010684d98584c9dd3140dbcb8', '/pbl_scense_ext', 'pbl_scense_ext_root', 'pbl_scense_ext 模块入口(目录路由) | M9 | role=logined | wwwroot/index.ui(由 wwwroot 软链挂载,apps/pbls build.sh 第 5 步)→ load_path.py PATHS[4]', 'page', 'pbl_scense_ext', 'pbl_scense_ext 模块入口(目录路由)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_4330c1c1ba3a01d923c5e79ce9f8f', '/pbl_scense_ext/index.ui', 'pbl_scense_ext_index_ui', 'pbl_scense_ext 入口页壳 | M9 | role=logined | wwwroot/index.ui → load_path.py PATHS[5]', 'page', 'pbl_scense_ext', 'pbl_scense_ext 入口页壳', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_f497512d13f3166761ed15bbb8203', '/pbl_scense_ext/overlay/pbl_overlay_loader.js', 'pbl_overlay_loader_js', '覆盖层脚本/CSS 注入器 + 运行时 i18n(预鉴权页壳首屏) | M9 | role=any | wwwroot/overlay/pbl_overlay_loader.js:2(FILES L51 注入 core/transport/play;I18N_DIR L48 fetch msg.txt)→ load_path.py PATHS', 'static', 'pbl_scense_ext', '覆盖层脚本/CSS 注入器 + 运行时 i18n(预鉴权页壳首屏)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_0e8cb7e84f425ae605ae5c08881c5', '/pbl_scense_ext/overlay/pbl_overlay.css', 'pbl_overlay_css', '覆盖层主题 CSS(页壳首屏样式,不含业务数据) | M9 | role=any | wwwroot/overlay/pbl_overlay.css → load_path.py PATHS[7](any,QC #1)', 'static', 'pbl_scense_ext', '覆盖层主题 CSS(页壳首屏样式,不含业务数据)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_c9293d08c50f1e7ffd65fd48c00ea', '/pbl_scense_ext/overlay/pbl_overlay_core.js', 'pbl_overlay_core_js', '覆盖层业务内核 JS(登录后由 loader 注入) | M9 | role=logined | wwwroot/overlay/pbl_overlay_loader.js FILES L51 → wwwroot/overlay/pbl_overlay_core.js → load_path.py PATHS[8](logined)', 'static', 'pbl_scense_ext', '覆盖层业务内核 JS(登录后由 loader 注入)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_fa593242ef0c1b9ca1567cdfe1527', '/pbl_scense_ext/overlay/pbl_overlay_transport.js', 'pbl_overlay_transport_js', '覆盖层广播/意图通道 JS | M9 | role=logined | wwwroot/overlay/pbl_overlay_loader.js FILES L51 → wwwroot/overlay/pbl_overlay_transport.js → load_path.py PATHS[9](login', 'static', 'pbl_scense_ext', '覆盖层广播/意图通道 JS', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_f5d867e2053681c1cfcda64d83345', '/pbl_scense_ext/overlay/pbl_overlay_play.js', 'pbl_overlay_play_js', '覆盖层装配 JS(世界内 5 页) | M9 | role=logined | wwwroot/overlay/pbl_overlay_loader.js FILES L51 → wwwroot/overlay/pbl_overlay_play.js → load_path.py PATHS[10](logined)', 'static', 'pbl_scense_ext', '覆盖层装配 JS(世界内 5 页)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_b1d3b3a4c3cc3c1f1bdad651ce2e6', '/pbl_scense_ext/i18n/zh/msg.txt', 'pbl_overlay_i18n_zh', '覆盖层中文文案表(运行时 i18n 单一事实源) | M9 | role=any | wwwroot/overlay/pbl_overlay_loader.js loadLang()(I18N_DIR L48)→ wwwroot/i18n/zh/msg.txt;生成期同源校验 scripts/gen_overlay_page', 'static', 'pbl_scense_ext', '覆盖层中文文案表(运行时 i18n 单一事实源)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_e745726b5142840155acdd17b7a98', '/pbl_scense_ext/i18n/en/msg.txt', 'pbl_overlay_i18n_en', '覆盖层英文文案表(运行时 i18n 单一事实源) | M9 | role=any | wwwroot/overlay/pbl_overlay_loader.js loadLang()(I18N_DIR L48)→ wwwroot/i18n/en/msg.txt;生成期同源校验 scripts/gen_overlay_page', 'static', 'pbl_scense_ext', '覆盖层英文文案表(运行时 i18n 单一事实源)', 'static') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_d18a134bd7056f10058904f2604ca', '/pbl_scense_ext/pbl_overlay/home.ui', 'pbl_overlay_page_home', 'PBL 首页(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/home.ui;运行时路由 pbl_scense_ext/api.py PAG', 'page', 'pbl_scense_ext', 'PBL 首页(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_dd99f9738cd48d14421feedc0ac0f', '/pbl_scense_ext/pbl_overlay/blueprint_list.ui', 'pbl_overlay_page_blueprint_list', '蓝图列表(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/blueprint_list.ui;运行时路由 api.py PAGES[1]', 'page', 'pbl_scense_ext', '蓝图列表(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_578f2e691f4e1e27716a423b17e58', '/pbl_scense_ext/pbl_overlay/blueprint_editor.ui', 'pbl_overlay_page_blueprint_editor', '蓝图编辑器(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/blueprint_editor.ui;运行时路由 api.py PAGES[', 'page', 'pbl_scense_ext', '蓝图编辑器(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_3707a5b47383f55766a981696b897', '/pbl_scense_ext/pbl_overlay/validation_report.ui', 'pbl_overlay_page_validation_report', '校验报告 14 维(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/validation_report.ui;运行时路由 api.py PAGES', 'page', 'pbl_scense_ext', '校验报告 14 维(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_5ef68c7c20ee872e61751c88ac28d', '/pbl_scense_ext/pbl_overlay/agent_console.ui', 'pbl_overlay_page_agent_console', 'Agent 控制台(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/agent_console.ui;运行时路由 api.py PAGES[4](', 'page', 'pbl_scense_ext', 'Agent 控制台(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_9760083af6c293ac9cf2e4db4f634', '/pbl_scense_ext/pbl_overlay/approval_center.ui', 'pbl_overlay_page_approval_center', '人工审批中心(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/approval_center.ui;运行时路由 api.py PAGES[5', 'page', 'pbl_scense_ext', '人工审批中心(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_86b8b90a44ad6ed7258eb6ecf951c', '/pbl_scense_ext/pbl_overlay/play_preview.ui', 'pbl_overlay_page_play_preview', 'Play 预览(覆盖层页壳,世界内) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/play_preview.ui;运行时路由 api.py PAGES[6](a', 'page', 'pbl_scense_ext', 'Play 预览(覆盖层页壳,世界内)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_a3ff343a4515fab4fdc6a59e38765', '/pbl_scense_ext/pbl_overlay/mission_board.ui', 'pbl_overlay_page_mission_board', '驱动问题板(覆盖层页壳,世界内) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/mission_board.ui;运行时路由 api.py PAGES[7](', 'page', 'pbl_scense_ext', '驱动问题板(覆盖层页壳,世界内)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_4dd6793dd185ad7bc2f4b513c45c8', '/pbl_scense_ext/pbl_overlay/role_panel.ui', 'pbl_overlay_page_role_panel', '角色面板(覆盖层页壳,世界内) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/role_panel.ui;运行时路由 api.py PAGES[8](api', 'page', 'pbl_scense_ext', '角色面板(覆盖层页壳,世界内)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_767ed9c9101d273cfb34e1a027c18', '/pbl_scense_ext/pbl_overlay/artifact_studio.ui', 'pbl_overlay_page_artifact_studio', '产出物工坊(覆盖层页壳,世界内) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/artifact_studio.ui;运行时路由 api.py PAGES[9', 'page', 'pbl_scense_ext', '产出物工坊(覆盖层页壳,世界内)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_d3337dd28c148fab58681cec127f2', '/pbl_scense_ext/pbl_overlay/evidence_wall.ui', 'pbl_overlay_page_evidence_wall', '证据墙(覆盖层页壳,世界内) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/evidence_wall.ui;运行时路由 api.py PAGES[10]', 'page', 'pbl_scense_ext', '证据墙(覆盖层页壳,世界内)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_a86a70333291ec7540d4a3ffb5a07', '/pbl_scense_ext/pbl_overlay/assessment_report.ui', 'pbl_overlay_page_assessment_report', '评估报告(覆盖层页壳) | M9 | role=logined | scripts/gen_overlay_pages.py PAGES(gen_overlay_pages.py:72)→ wwwroot/pbl_overlay/assessment_report.ui;运行时路由 api.py PAGES', 'page', 'pbl_scense_ext', '评估报告(覆盖层页壳)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_3277731ee5ea2a8c3daff30a4e7a6', '/pbl_scense_ext/pbl_overlay', 'pbl_overlay_page_dir', '覆盖层页面壳目录(目录路由) | M9 | role=logined | wwwroot/pbl_overlay/(12 个 .ui)→ load_path.py PATHS[25](目录条目,audit 不做文件存在性检查)', 'page', 'pbl_scense_ext', '覆盖层页面壳目录(目录路由)', 'page') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`);
|
||||
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_ed559fd9b81599a43e0306e296df4', 'logined', 'rp_1ba8c90c3d0b00842b74b28c90cd2') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_57ea020414d91a81160143d6025c1', 'logined', 'rp_0ab941aff9922b4b10788d072a4ed') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_2f1596f89c0213e87bab801c6f3f9', 'logined', 'rp_e25b1aac374944df4ec6432449f9a') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_d1bc0c4a8785b58d5eb0967aa8737', 'logined', 'rp_3e9fe75350098fe2f5bfe96e4cb51') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_76bfac7ac361b9f8d789f9ca0e0b4', 'logined', 'rp_2fc7010684d98584c9dd3140dbcb8') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_5222782b5a9db1b7dfccfc3e38345', 'logined', 'rp_4330c1c1ba3a01d923c5e79ce9f8f') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_960729cf2f7db49f6c24910afb553', 'logined', 'rp_c9293d08c50f1e7ffd65fd48c00ea') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_677dd6092f4bedd045b16f7d943e0', 'logined', 'rp_fa593242ef0c1b9ca1567cdfe1527') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_8e0c2befd49be3f377e1226ba6b83', 'logined', 'rp_f5d867e2053681c1cfcda64d83345') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_9fec8fdd18e5e83351ebdfd2e12c9', 'logined', 'rp_d18a134bd7056f10058904f2604ca') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_84456f80779665dbb58010f479424', 'logined', 'rp_dd99f9738cd48d14421feedc0ac0f') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0a3ebc6f6ac1771279bd4779c5981', 'logined', 'rp_578f2e691f4e1e27716a423b17e58') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_f9438cf06aa1703082ae86e91d30d', 'logined', 'rp_3707a5b47383f55766a981696b897') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0ac763df079e8c7715e252d23defc', 'logined', 'rp_5ef68c7c20ee872e61751c88ac28d') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_6430ea6c3002d647d17bbcdd3be78', 'logined', 'rp_9760083af6c293ac9cf2e4db4f634') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_6f2e02b9c51057b758d142949eb06', 'logined', 'rp_86b8b90a44ad6ed7258eb6ecf951c') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_f90cd617d2e0ebda30c334cd2e0c8', 'logined', 'rp_a3ff343a4515fab4fdc6a59e38765') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_81889052bdcea77270ea95ce352e2', 'logined', 'rp_4dd6793dd185ad7bc2f4b513c45c8') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_c0d568978991803fe88d980323d2b', 'logined', 'rp_767ed9c9101d273cfb34e1a027c18') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_4a7a4ca93e70f2a7b2949f57eb2ab', 'logined', 'rp_d3337dd28c148fab58681cec127f2') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_c3d6440608ce95feb89904c477fb7', 'logined', 'rp_a86a70333291ec7540d4a3ffb5a07') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_c05cdf2ef273bf58aa28c2028340f', 'logined', 'rp_3277731ee5ea2a8c3daff30a4e7a6') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_afcef741eafce7464bb5b0bfeecd5', 'any', 'rp_f497512d13f3166761ed15bbb8203') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_7fdfb54730fdab622b1fd1f58e09b', 'any', 'rp_0e8cb7e84f425ae605ae5c08881c5') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_5eb3f4434de605ca7c63b6fbe8812', 'any', 'rp_b1d3b3a4c3cc3c1f1bdad651ce2e6') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_ac67abdd9906bded65179b6a4c015', 'any', 'rp_e745726b5142840155acdd17b7a98') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);
|
||||
|
||||
-- 校验:permission 应为 26 行,rolepermission 应为 26 行(logined 22 + any 4)
|
||||
SELECT COUNT(1) AS perm_cnt FROM `permission` WHERE `parentid` = 'pbl_scense_ext';
|
||||
Loading…
x
Reference in New Issue
Block a user