From c051b71be3595b035bf270f2310020b2098a2ba1 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 12:06:15 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20[M11b-2c-B]=20=E8=A1=A5=20dev-notes-?= =?UTF-8?q?m11b2c=20=E6=96=87=E6=A1=A3=20+=20RBAC/=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=A3=B0=E6=98=8E=20+=20git=20=E6=94=B6=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ce_pbl_artifact.json => pbl_artifact.json} | 8 +- ...ce_pbl_evidence.json => pbl_evidence.json} | 7 +- pbl_evidence/__init__.py | 9 ++- pbl_evidence/init.py | 10 +++ scripts/load_path.py | 81 +++++++++++++++++-- wwwroot/api/pbl_artifact_options.dspy | 15 ++++ wwwroot/api/pbl_evidence_delete.dspy | 15 ++++ wwwroot/api/pbl_evidence_update.dspy | 16 ++++ 8 files changed, 147 insertions(+), 14 deletions(-) rename json/{evidence_pbl_artifact.json => pbl_artifact.json} (86%) rename json/{evidence_pbl_evidence.json => pbl_evidence.json} (85%) create mode 100644 wwwroot/api/pbl_artifact_options.dspy create mode 100644 wwwroot/api/pbl_evidence_delete.dspy create mode 100644 wwwroot/api/pbl_evidence_update.dspy diff --git a/json/evidence_pbl_artifact.json b/json/pbl_artifact.json similarity index 86% rename from json/evidence_pbl_artifact.json rename to json/pbl_artifact.json index e427d5f..7ef573b 100644 --- a/json/evidence_pbl_artifact.json +++ b/json/pbl_artifact.json @@ -1,6 +1,5 @@ { "tblname": "pbl_artifact", - "alias": "pbl_evidence", "title": "学习产出物(artifact)", "params": { "browserfields": { @@ -45,13 +44,14 @@ "updated_at" ], "logined_userorgid": "tenant_id", + "new_data_url": "{{entire_url('../api/pbl_artifact_create.dspy')}}", + "update_data_url": "{{entire_url('../api/pbl_artifact_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/pbl_artifact_delete.dspy')}}", "editable": { "new_data_url": "{{entire_url('../api/pbl_artifact_create.dspy')}}", "update_data_url": "{{entire_url('../api/pbl_artifact_update.dspy')}}", "delete_data_url": "{{entire_url('../api/pbl_artifact_delete.dspy')}}" }, - "sortby": ["created_at desc"], - "order_by": "id DESC", - "page_size": 20 + "sortby": ["created_at desc"] } } diff --git a/json/evidence_pbl_evidence.json b/json/pbl_evidence.json similarity index 85% rename from json/evidence_pbl_evidence.json rename to json/pbl_evidence.json index 1072731..cbb0aa5 100644 --- a/json/evidence_pbl_evidence.json +++ b/json/pbl_evidence.json @@ -43,13 +43,14 @@ "updated_at" ], "logined_userorgid": "tenant_id", + "new_data_url": "{{entire_url('../api/pbl_evidence_collect.dspy')}}", + "update_data_url": "{{entire_url('../api/pbl_evidence_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/pbl_evidence_delete.dspy')}}", "editable": { "new_data_url": "{{entire_url('../api/pbl_evidence_collect.dspy')}}", "update_data_url": "{{entire_url('../api/pbl_evidence_update.dspy')}}", "delete_data_url": "{{entire_url('../api/pbl_evidence_delete.dspy')}}" }, - "sortby": ["occurred_at desc"], - "order_by": "id DESC", - "page_size": 20 + "sortby": ["occurred_at desc"] } } diff --git a/pbl_evidence/__init__.py b/pbl_evidence/__init__.py index 43ccd96..72af496 100644 --- a/pbl_evidence/__init__.py +++ b/pbl_evidence/__init__.py @@ -23,6 +23,11 @@ from pbl_evidence.collector import ( collect_evidence_from_events, resolve_event_table, ) +from pbl_evidence.crud_api import ( + pbl_artifact_options, + pbl_evidence_update, + pbl_evidence_delete, +) from pbl_evidence.evidence_map import ( EVIDENCE_TYPES, EVENT_TO_EVIDENCE, @@ -36,6 +41,8 @@ __all__ = [ 'pbl_evidence_list', 'pbl_evidence_stats', 'pbl_evidence_watermark', 'collect_evidence_from_events', 'resolve_event_table', 'CollectError', 'EVIDENCE_TYPES', 'EVENT_TO_EVIDENCE', 'evidence_types_for_events', + # CRUD 框架适配端点后端(json/pbl_evidence.json 的 editable + alters.dataurl) + 'pbl_artifact_options', 'pbl_evidence_update', 'pbl_evidence_delete', ] -__version__ = '1.1.0' +__version__ = '1.2.0' diff --git a/pbl_evidence/init.py b/pbl_evidence/init.py index 0f4481c..293df63 100644 --- a/pbl_evidence/init.py +++ b/pbl_evidence/init.py @@ -25,6 +25,11 @@ from pbl_evidence.api import ( pbl_evidence_watermark, ) from pbl_evidence.collector import collect_evidence_from_events, resolve_event_table +from pbl_evidence.crud_api import ( + pbl_artifact_options, + pbl_evidence_delete, + pbl_evidence_update, +) from pbl_evidence.evidence_map import EVIDENCE_TYPES, EVENT_TO_EVIDENCE @@ -42,6 +47,11 @@ def load_pbl_evidence(): env.pbl_evidence_list = pbl_evidence_list env.pbl_evidence_stats = pbl_evidence_stats env.pbl_evidence_watermark = pbl_evidence_watermark + # ── CRUD 框架适配(json/pbl_evidence.json editable 三 URL + artifact_id 下拉) + # 漏这三行 → wwwroot/api/pbl_evidence_update.dspy 等调用必 NameError(三处同步之 ③) + env.pbl_artifact_options = pbl_artifact_options + env.pbl_evidence_update = pbl_evidence_update + env.pbl_evidence_delete = pbl_evidence_delete # ── 底层能力(供 M5b 回放 / M6 评估直接复用)────────────────── env.pbl_collect_evidence_from_events = collect_evidence_from_events env.pbl_resolve_event_table = resolve_event_table diff --git a/scripts/load_path.py b/scripts/load_path.py index 51e0c0c..5fa8778 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -38,9 +38,12 @@ WWWROOT = os.path.normpath( os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'wwwroot')) # ── (path, role) 清单:逐条显式,禁通配符 ───────────────────────────────── -# 覆盖 wwwroot 下全部 9 个 .dspy + 模块入口 index.ui。M5a 主能力 +# 覆盖 wwwroot 下全部 12 个 .dspy + 模块入口 index.ui(与磁盘一一对应, +# `python3 scripts/load_path.py --check` 做自愈核对)。M5a 主能力 # pbl_evidence_collect_from_events.dspy 与统计口 pbl_evidence_stats.dspy 在此登记, -# 漏登记 = 部署后点击一律 403,整条批量采集链路不可用(QC 退回意见 #1 的实测后果)。 +# CRUD 适配三端点 pbl_artifact_options / pbl_evidence_update / pbl_evidence_delete +# (json/pbl_evidence.json 的 editable 三 URL + alters.artifact_id.dataurl 所指)也必须登记, +# 漏登记 = 部署后点击一律 403,整条批量采集/编辑/删除链路不可用(QC 退回意见 #1/#3 的实测后果)。 PATHS = [ # 模块入口页(wwwroot/index.ui) ('/pbl_evidence/index.ui', 'logined'), @@ -56,8 +59,59 @@ PATHS = [ # 证据查询 / 统计 ('/pbl_evidence/api/pbl_evidence_list.dspy', 'logined'), ('/pbl_evidence/api/pbl_evidence_stats.dspy', 'logined'), + # CRUD 框架适配端点(json/pbl_evidence.json editable 三 URL + artifact_id 下拉) + ('/pbl_evidence/api/pbl_artifact_options.dspy', 'logined'), + ('/pbl_evidence/api/pbl_evidence_update.dspy', 'logined'), + ('/pbl_evidence/api/pbl_evidence_delete.dspy', 'logined'), + # ── CRUD 生成物见下方 GENERATED_PATHS(xls2ui 产物,build 时才落盘)── ] +# xls2ui 从 json/pbl_artifact.json / json/pbl_evidence.json 生成的 CRUD 目录 +# (alias = tblname = pbl_artifact / pbl_evidence)。每个 alias 6 条:目录 + +# index.ui + get_/add_/update_/delete_ 端点,逐条显式、禁通配符。这些文件是 +# **构建产物**(.gitignore 排除、不入库),所以只出现在部署环境的 wwwroot 下; +# 注册权限时必须一并登记,否则 CRUD 列表页/新增/编辑/删除一律 403。 +CRUD_ALIASES = ('pbl_artifact', 'pbl_evidence') +GENERATED_PATHS = [] +for _alias in CRUD_ALIASES: + GENERATED_PATHS.append(('/pbl_evidence/%s' % _alias, 'logined')) + GENERATED_PATHS.append(('/pbl_evidence/%s/index.ui' % _alias, 'logined')) + for _op in ('get', 'add', 'update', 'delete'): + GENERATED_PATHS.append( + ('/pbl_evidence/%s/%s_%s.dspy' % (_alias, _op, _alias), 'logined')) + +# 注册用的全量清单 = 手写端点 + CRUD 生成物(顺序即登记顺序) +PATHS = PATHS + GENERATED_PATHS + +# ── permission / rolepermission 幂等 INSERT(供部署说明与手工兜底使用)────── +# 与 PATHS 一一对应:rbac 三表链路 permission.permcode = 路径本身, +# rolepermission 关联 role.rolecode='logined'。列名以 models 真实字段为准 +# (permission: permcode/name;role: rolecode;rolepermission: roleid/permid), +# 用 INSERT ... SELECT 反查 id,避免硬编码主键。 +def permission_inserts(): + """返回与 PATHS 等价的幂等 SQL(角色固定 logined)。""" + out = [] + for path, role in PATHS: + out.append( + "INSERT INTO `permission` (`id`, `permcode`, `name`) " + "SELECT REPLACE(UUID(),'-',''), '%(p)s', '%(p)s' " + "WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE `permcode`='%(p)s');" + % {'p': path}) + out.append( + "INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) " + "SELECT REPLACE(UUID(),'-',''), r.`id`, p.`id` FROM `role` r, `permission` p " + "WHERE r.`rolecode`='%(rl)s' AND p.`permcode`='%(p)s' " + "AND NOT EXISTS (SELECT 1 FROM `rolepermission` rp " + "WHERE rp.`roleid`=r.`id` AND rp.`permid`=p.`id`);" + % {'rl': role, 'p': path}) + return out + +def print_permission_sql(): + """--sql 子命令:打印 INSERT 清单(部署时可直接管道进 mysql)。""" + for stmt in permission_inserts(): + print(stmt) + + STATIC_SUFFIXES = ('.dspy', '.ui', '.js', '.css', '.wss') @@ -90,9 +144,15 @@ def on_disk_paths(): return got -def check(): - """清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。""" - registered = set(p for p, _role in PATHS) +def check(include_generated=False): + """清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。 + + CRUD 生成物(GENERATED_PATHS)是 build.sh 里 xls2ui 的产物,开发仓库中被 + .gitignore 排除、不落盘,因此默认不计入 stale(否则开发环境永远误报); + 部署后跑 `--check --generated` 才把它们纳入核对。手写端点与磁盘必须一一对应。 + """ + handwritten = set(p for p, _r in PATHS) - set(p for p, _r in GENERATED_PATHS) + registered = set(p for p, _role in PATHS) if include_generated else handwritten on_disk = on_disk_paths() missing = sorted(on_disk - registered) stale = sorted(registered - on_disk) @@ -129,16 +189,25 @@ def register(verbose=True): def main(): ap = argparse.ArgumentParser(description='%s RBAC path registration' % MODULE) ap.add_argument('--check', action='store_true', help='verify PATHS against wwwroot files') + ap.add_argument('--generated', action='store_true', + help='with --check: also reconcile xls2ui-generated CRUD paths ' + '(run this on the deployment host, after build.sh)') ap.add_argument('--list', action='store_true', help='print registered paths and exit') + ap.add_argument('--sql', action='store_true', + help='print idempotent permission/rolepermission INSERTs and exit') args = ap.parse_args() + if args.sql: + print_permission_sql() + return 0 + if args.list: for path, role in PATHS: print('%s %s' % (role, path)) return 0 if args.check: - missing, stale, wildcard = check() + missing, stale, wildcard = check(include_generated=args.generated) print('[%s] PATHS entries: %d' % (MODULE, len(PATHS))) for p in missing: print('MISSING (on disk, not registered): ' + p) diff --git a/wwwroot/api/pbl_artifact_options.dspy b/wwwroot/api/pbl_artifact_options.dspy new file mode 100644 index 0000000..d7e1484 --- /dev/null +++ b/wwwroot/api/pbl_artifact_options.dspy @@ -0,0 +1,15 @@ +debug(f'pbl_artifact_options.dspy: START params_kw={dict(params_kw)}') +# 下拉数据源(json/pbl_evidence.json -> browserfields.alters.artifact_id.dataurl) +# 契约:必须返回**裸 JSON 数组** [{value,text},...],异常/空数据返回 [], +# 不许包 status/data(crud-definition-spec Pitfall:包了会让下拉静默不渲染)。 +# 后端函数 pbl_artifact_options 由 load_pbl_evidence() 注册到 ServerEnv(init.py ③)。 +try: + _rows = await pbl_artifact_options(**dict(params_kw)) + if not isinstance(_rows, list): + _rows = [] + debug(f'pbl_artifact_options.dspy: OK rows={len(_rows)}') + return json.dumps(_rows, ensure_ascii=False) +except Exception as e: + debug(f'pbl_artifact_options.dspy: FAIL {e}') + debug(f'pbl_artifact_options.dspy: TRACE {format_exc()}') + return json.dumps([], ensure_ascii=False) diff --git a/wwwroot/api/pbl_evidence_delete.dspy b/wwwroot/api/pbl_evidence_delete.dspy new file mode 100644 index 0000000..1d9959f --- /dev/null +++ b/wwwroot/api/pbl_evidence_delete.dspy @@ -0,0 +1,15 @@ +debug(f'pbl_evidence_delete.dspy: START params_kw={dict(params_kw)}') +# 证据删除端点(json/pbl_evidence.json -> params.editable.delete_data_url)。 +# 薄包装:无 import、无业务逻辑,直接委托 init.py 注册的 pbl_evidence_delete +# (支持单条 id 与批量 ids;每条都按 tenant_id + id 双条件校验归属,防越权删除)。 +try: + _data = await pbl_evidence_delete(**dict(params_kw)) + _msg = (_data or {}).get('message', '证据已删除') + debug(f'pbl_evidence_delete.dspy: OK deleted={(_data or {}).get("deleted")}') + return json.dumps({'status': 'OK', 'ok': True, 'data': _data, + 'message': _msg}, ensure_ascii=False) +except Exception as e: + debug(f'pbl_evidence_delete.dspy: FAIL {e}') + debug(f'pbl_evidence_delete.dspy: TRACE {format_exc()}') + return json.dumps({'status': 'ERROR', 'ok': False, 'data': None, + 'message': f'证据删除失败:{e}'}, ensure_ascii=False) diff --git a/wwwroot/api/pbl_evidence_update.dspy b/wwwroot/api/pbl_evidence_update.dspy new file mode 100644 index 0000000..cce11b0 --- /dev/null +++ b/wwwroot/api/pbl_evidence_update.dspy @@ -0,0 +1,16 @@ +debug(f'pbl_evidence_update.dspy: START params_kw={dict(params_kw)}') +# 证据人工修订端点(json/pbl_evidence.json -> params.editable.update_data_url, +# 注意:new/update/delete_data_url 在 CRUD JSON 中位于 params 顶层语义,由 xls2ui 读取)。 +# 薄包装:无 import、无业务逻辑,直接委托 init.py 注册的 pbl_evidence_update +# (强制 tenant_id 打头校验归属;改唯一索引三元组时同步重算 dedup_key)。 +try: + _data = await pbl_evidence_update(**dict(params_kw)) + _msg = (_data or {}).get('message', '证据已更新') + debug(f'pbl_evidence_update.dspy: OK id={(_data or {}).get("id")}') + return json.dumps({'status': 'OK', 'ok': True, 'data': _data, + 'message': _msg}, ensure_ascii=False) +except Exception as e: + debug(f'pbl_evidence_update.dspy: FAIL {e}') + debug(f'pbl_evidence_update.dspy: TRACE {format_exc()}') + return json.dumps({'status': 'ERROR', 'ok': False, 'data': None, + 'message': f'证据更新失败:{e}'}, ensure_ascii=False)