approve: [M11b-2c-A3] 业务实现修复(条件触发,仅当 A1 判定 PBL-RTX-0004 为业务缺陷)
This commit is contained in:
parent
c051b71be3
commit
2da3cd39f8
39
README.md
39
README.md
@ -112,8 +112,10 @@ cd models && json2ddl mysql . > mysql.ddl.sql
|
||||
mysql -h <db_host> -u<user> -p<pwd> <dbname> < mysql.ddl.sql
|
||||
# (手工建表务必带 COLLATE utf8mb4_unicode_ci,否则与 xls2ddl 产物 collation 混用报 1267)
|
||||
|
||||
# 3) 生成 CRUD 前端(可选;已有手写 api/ dspy 时按 build.sh 流程执行)
|
||||
# 3) 生成 CRUD 前端(build.sh 步骤;json/ 下两个文件按 tblname 命名)
|
||||
cd json && xls2ui -m ../models -o ../wwwroot pbl_evidence *.json
|
||||
# -> 生成 wwwroot/pbl_artifact/ 与 wwwroot/pbl_evidence/ 两个 CRUD 目录(构建产物,
|
||||
# 已在 .gitignore 中排除,部署后由 build.sh 重新生成,禁止手改其中文件)
|
||||
|
||||
# 4) 导入编码种子(3 组 appcodes:pbl_artifact_type / pbl_artifact_status / pbl_evidence_type)
|
||||
# Format B,由宿主 scripts/init_data.py(或 dbloader)读入 init/data.json 落 appcodes + appcodes_kv
|
||||
@ -122,10 +124,25 @@ python3 -c "import json;d=json.load(open('init/data.json'));print('groups:',[g['
|
||||
# 5) 登记 RBAC 权限(双层:模块层 + 中央宿主层,见下)
|
||||
python3 scripts/load_path.py --check # 清单与 wwwroot 磁盘一致性核对(不连库)
|
||||
RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py # 实际写权限
|
||||
python3 scripts/load_path.py --check --generated # 部署机上(xls2ui 已跑)连生成物一起核对
|
||||
python3 scripts/load_path.py --sql | mysql -h <db_host> -u<user> -p<pwd> <rbac_dbname>
|
||||
```
|
||||
|
||||
`scripts/load_path.py` 逐条显式登记(**禁通配符**):`/pbl_evidence/index.ui` 与
|
||||
`wwwroot/api/` 下全部 9 个 `.dspy`,角色均为 `logined`。中央宿主 `apps/pbls/scripts/load_path.py`
|
||||
**permission / rolepermission 同步(新增路径必做)**:`--sql` 输出与 PATHS 一一对应的幂等
|
||||
INSERT —— `permission.permcode` = 路径本身,`rolepermission` 用 `role.rolecode='logined'`
|
||||
反查 id 关联(不含任何硬编码主键,重复执行不产生脏数据)。rbac CLI(set_role_perm.py)不在位时,
|
||||
这条管道就是部署说明给出的登记手段。**新增 .dspy 若忘记登记,登录后访问必 403**
|
||||
(QC 硬门禁 #3 的成因)。
|
||||
|
||||
`scripts/load_path.py` 逐条显式登记(**禁通配符**),角色均为 `logined`:
|
||||
- 手写资源 13 条:`/pbl_evidence/index.ui` + `wwwroot/api/` 下全部 **12** 个 `.dspy`
|
||||
(产出物 CRUD 5 + 证据采集 2 + 查询/统计 2 + CRUD 适配 3:
|
||||
`pbl_artifact_options` / `pbl_evidence_update` / `pbl_evidence_delete`);
|
||||
- CRUD 生成物 12 条(`GENERATED_PATHS`):`/pbl_evidence/{pbl_artifact,pbl_evidence}` 目录 +
|
||||
`index.ui` + `get_/add_/update_/delete_*.dspy`(每 alias 6 条)。这些是 xls2ui 产物、开发仓库
|
||||
不落盘,故 `--check` 默认不计 stale,部署后加 `--generated` 再核。
|
||||
|
||||
中央宿主 `apps/pbls/scripts/load_path.py`
|
||||
另有一份等价的硬编码兜底清单(不同代码路径,模块脚本 import 失败时仍能登记)——
|
||||
双层 RBAC 回退,两层都必须在册。
|
||||
|
||||
@ -143,9 +160,16 @@ pip install .
|
||||
python3 -c "import json;d=json.load(open('init/data.json'));print([g['parentid'] for g in d['appcodes']])"
|
||||
grep -o "parentid='[a-z_]*'" models/*.json | sort -u
|
||||
|
||||
# ③ 三处同步:api.py 契约定义数 == init.py env 契约注册数(对外契约应为 10)
|
||||
grep -c "^async def pbl_" pbl_evidence/api.py
|
||||
# ③ 三处同步:api.py(10) + crud_api.py(3) 契约定义 == __init__.py 导出 == init.py env 注册(共 13)
|
||||
grep -c "^async def pbl_" pbl_evidence/api.py pbl_evidence/crud_api.py
|
||||
grep -c "^ env.pbl_" pbl_evidence/init.py
|
||||
grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py' # 定义/导出/__all__/import/env 全命中
|
||||
|
||||
# ③b CRUD 端点存在性:json/*.json 引用的每个 ../api/*.dspy 必须真实落盘(否则编辑/删除/下拉 404)
|
||||
python3 scripts/check_crud_endpoints.py
|
||||
|
||||
# ③c CRUD JSON 合法键 + alias 唯一性自检
|
||||
python3 scripts/check_crud_json.py
|
||||
|
||||
# ④ RBAC 清单与 wwwroot 磁盘一致性核对(不连库)
|
||||
python3 scripts/load_path.py --check
|
||||
@ -192,13 +216,14 @@ pbl_evidence/
|
||||
├── pbl_evidence/ # Python 包(= 模块名)
|
||||
│ ├── __init__.py # 导出(三处同步之 ②)
|
||||
│ ├── init.py # load_pbl_evidence()(三处同步之 ③)
|
||||
│ ├── api.py # 10 个契约函数(三处同步之 ①)
|
||||
│ ├── api.py # 10 个业务契约函数(三处同步之 ①)
|
||||
│ ├── crud_api.py # 3 个 CRUD 框架适配契约(options/update/delete,三处同步之 ①)
|
||||
│ ├── collector.py # 事件扫描 → 映射 → 幂等落库;list/stats/watermark
|
||||
│ ├── evidence_map.py # 纯函数层:事件→证据类型映射、dedup_key、时间规范化
|
||||
│ └── db.py # 数据访问层:get_module_dbname、列名发现、q_all/q_one/q_exec
|
||||
├── models/ # pbl_artifact.json / pbl_evidence.json(四段式表定义)
|
||||
├── json/ # CRUD 定义(tblname + params + editable)
|
||||
├── wwwroot/ # index.ui + api/ 9 个 .dspy(自动路由 /pbl_evidence/...)
|
||||
├── wwwroot/ # index.ui + api/ 12 个 .dspy(自动路由 /pbl_evidence/...)
|
||||
├── init/data.json # 编码种子(Format B:3 组 appcodes + appcodes_kv 子项)
|
||||
├── sql/pbl_evidence.sql # 建表 SQL 快照
|
||||
├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对
|
||||
|
||||
@ -4,10 +4,30 @@
|
||||
|
||||
为什么单独成文件(不改 api.py)
|
||||
------------------------------
|
||||
api.py 已有 390 行契约面(产出物 CRUD + 证据幂等采集)。本文件只补
|
||||
``json/evidence_pbl_evidence.json`` 里 ``params.editable`` 三个 URL 与
|
||||
``browserfields.alters[].dataurl`` 所需的 4 个后端函数,保持 api.py 不动、
|
||||
职责清晰:api.py = 业务契约,crud_api.py = CRUD 框架适配。
|
||||
api.py 已有 390 行业务契约面(产出物 CRUD + 证据幂等采集)。本文件只补
|
||||
``json/pbl_evidence.json`` 里 ``params.editable`` 的 update/delete 两个 URL 与
|
||||
``browserfields.alters[].dataurl`` 所需的 **3 个**后端函数(数量以本文件
|
||||
``async def`` 为准:pbl_artifact_options / pbl_evidence_update / pbl_evidence_delete),
|
||||
保持 api.py 不动、职责清晰:api.py = 业务契约,crud_api.py = CRUD 框架适配。
|
||||
|
||||
与既有 api.py 契约的分工(避免误解为「artifact CRUD 也走本文件」)
|
||||
----------------------------------------------------------------
|
||||
- ``json/pbl_artifact.json`` 的 new/update/delete_data_url 指向
|
||||
``wwwroot/api/pbl_artifact_{create,update,delete}.dspy``,这三个薄包装委托
|
||||
**api.py** 的 ``pbl_artifact_create/update/delete``(含 artifact_uid 自动生成、
|
||||
version_no 递增、租户归属校验),**不经过本文件**;本文件只为 artifact 提供
|
||||
下拉数据源 ``pbl_artifact_options``(只读 id/label 两列)。
|
||||
- ``json/pbl_evidence.json`` 的 new_data_url 指向 ``pbl_evidence_collect.dspy``
|
||||
(委托 api.py 的幂等采集,服务端权威、只增不改),update/delete 才委托本文件。
|
||||
|
||||
注册位置(三处同步,缺一必 NameError)
|
||||
------------------------------------
|
||||
① 定义:本文件;② 导出:``pbl_evidence/__init__.py`` 的
|
||||
``from pbl_evidence.crud_api import ...`` + ``__all__``;
|
||||
③ 挂载:``pbl_evidence/init.py`` 的 ``load_pbl_evidence()`` 内
|
||||
``env.pbl_artifact_options / env.pbl_evidence_update / env.pbl_evidence_delete``。
|
||||
对外契约清单见 ``skill/SKILL.md``「契约接口」表第 11~13 行(CRUD 适配契约),
|
||||
RBAC 路径见 ``scripts/load_path.py`` PATHS。
|
||||
|
||||
规范依据(crud-definition-spec / module-development-spec)
|
||||
--------------------------------------------------------
|
||||
|
||||
105
scripts/check_crud_endpoints.py
Normal file
105
scripts/check_crud_endpoints.py
Normal file
@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""CRUD 端点存在性自检(QC 硬门禁 #2 的 CI 化)。
|
||||
|
||||
json/*.json 里 `params.editable` / `new|update|delete_data_url` /
|
||||
`browserfields.alters[].dataurl` 引用的每个 `../api/xxx.dspy`,都必须在
|
||||
`wwwroot/api/xxx.dspy` 真实落盘 —— 少一个文件,前端点了就是 404。
|
||||
|
||||
用法:
|
||||
python3 scripts/check_crud_endpoints.py # 只核对手写 api/ 端点
|
||||
python3 scripts/check_crud_endpoints.py --generated # 连 xls2ui 生成物一起核(部署机用)
|
||||
|
||||
退出码:0=全部命中;1=存在缺失端点。不连库、不依赖第三方包,可直接放 CI。
|
||||
"""
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
JSON_DIR = os.path.join(ROOT, 'json')
|
||||
API_DIR = os.path.join(ROOT, 'wwwroot', 'api')
|
||||
|
||||
# 匹配 ../api/xxx.dspy 与 /pbl_evidence/api/xxx.dspy 两种写法
|
||||
URL_RE = re.compile(r'api/([A-Za-z0-9_]+\.dspy)')
|
||||
|
||||
|
||||
def referenced_endpoints():
|
||||
"""返回 {dspy 文件名: [引用它的 json 文件, ...]}。"""
|
||||
refs = {}
|
||||
if not os.path.isdir(JSON_DIR):
|
||||
return refs
|
||||
for fn in sorted(os.listdir(JSON_DIR)):
|
||||
if not fn.endswith('.json'):
|
||||
continue
|
||||
path = os.path.join(JSON_DIR, fn)
|
||||
try:
|
||||
json.loads(io.open(path, encoding='utf-8').read())
|
||||
except Exception as exc:
|
||||
refs.setdefault('__INVALID_JSON__:%s' % fn, []).append(str(exc))
|
||||
continue
|
||||
text = io.open(path, encoding='utf-8').read()
|
||||
for name in URL_RE.findall(text):
|
||||
refs.setdefault(name, []).append('json/' + fn)
|
||||
return refs
|
||||
|
||||
|
||||
def generated_endpoints():
|
||||
"""xls2ui 为每个 CRUD 定义生成 wwwroot/{alias}/{get,add,update,delete}_{alias}.dspy。
|
||||
|
||||
注意落点是 **wwwroot/{alias}/** 子目录(不是 wwwroot/api/)——那是构建产物目录,
|
||||
开发仓库被 .gitignore 排除,只有部署机跑过 build.sh 后才存在。
|
||||
"""
|
||||
out = []
|
||||
for fn in sorted(os.listdir(JSON_DIR)):
|
||||
if not fn.endswith('.json'):
|
||||
continue
|
||||
d = json.loads(io.open(os.path.join(JSON_DIR, fn), encoding='utf-8').read())
|
||||
alias = d.get('alias') or d.get('tblname')
|
||||
if not alias:
|
||||
continue
|
||||
for op in ('get', 'add', 'update', 'delete'):
|
||||
out.append(os.path.join(alias, '%s_%s.dspy' % (op, alias)))
|
||||
out.append(os.path.join(alias, 'index.ui'))
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description='verify CRUD-referenced .dspy endpoints exist')
|
||||
ap.add_argument('--generated', action='store_true',
|
||||
help='also check xls2ui-generated get_/add_/update_/delete_{alias}.dspy '
|
||||
'(build artifacts; run on the deployment host after build.sh)')
|
||||
args = ap.parse_args()
|
||||
|
||||
refs = referenced_endpoints()
|
||||
bad = []
|
||||
for name, srcs in sorted(refs.items()):
|
||||
if name.startswith('__INVALID_JSON__:'):
|
||||
bad.append('%s 不是合法 JSON:%s' % (name, srcs[0]))
|
||||
continue
|
||||
if not os.path.exists(os.path.join(API_DIR, name)):
|
||||
bad.append('%s 缺失(被 %s 引用)' % (name, ', '.join(srcs)))
|
||||
|
||||
gen_missing = []
|
||||
if args.generated:
|
||||
for name in generated_endpoints():
|
||||
if not os.path.exists(os.path.join(ROOT, 'wwwroot', name)):
|
||||
gen_missing.append(name)
|
||||
|
||||
print('[pbl_evidence] json/ 引用的端点: %d' % len(refs))
|
||||
for line in bad:
|
||||
print('MISSING ' + line)
|
||||
if args.generated:
|
||||
print('[pbl_evidence] xls2ui 生成物端点: %d' % len(generated_endpoints()))
|
||||
for name in gen_missing:
|
||||
print('MISSING(gen) wwwroot/' + name)
|
||||
ok = not bad and not gen_missing
|
||||
print('[pbl_evidence] crud endpoints: %s' % ('PASS' if ok else 'FAIL'))
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@ -20,10 +20,12 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
|
||||
- `pbl_artifact`:学生产出物(版本内联 `version_no`,唯一索引 `uk_art_uid(tenant_id, artifact_uid)`)
|
||||
- `pbl_evidence`:学习证据(唯一索引 `uk_ev_dedup(tenant_id, source_event_id, evidence_type)` 防重)
|
||||
|
||||
表定义:`models/{table}.json`(四段式 summary/fields/indexes/codes);CRUD:`json/evidence_*.json`。
|
||||
表定义:`models/{table}.json`(四段式 summary/fields/indexes/codes);CRUD:`json/pbl_artifact.json` +
|
||||
`json/pbl_evidence.json`(文件名 = tblname;**不要**给两张不同的表共用同一 alias,
|
||||
xls2ui 会生成到同一目录互相覆盖)。
|
||||
编码种子:`init/data.json`(Format B,3 组 appcodes,见下「编码字典」)。
|
||||
|
||||
## 契约接口(10 个,与 `pbl_evidence/init.py` 契约段 env 注册一一对应)
|
||||
## 契约接口(13 个,与 `pbl_evidence/init.py` 的 `load_pbl_evidence()` env 注册一一对应)
|
||||
|
||||
| # | 契约函数 | .dspy 路径(`/pbl_evidence/api/...`) | 说明 |
|
||||
|---|----------|--------------------------------------|------|
|
||||
@ -37,6 +39,9 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
|
||||
| 8 | `pbl_evidence_list` | `pbl_evidence_list.dspy` | 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 |
|
||||
| 9 | `pbl_evidence_stats` | `pbl_evidence_stats.dspy` | 证据类型分布统计,供 M6 评估与概览页 |
|
||||
| 10 | `pbl_evidence_watermark` | **无独立 .dspy**(见下) | 返回某会话/蓝图已采集到的时间水位,供 M5b/cron 增量续采 |
|
||||
| 11 | `pbl_artifact_options` | `pbl_artifact_options.dspy` | **CRUD 适配**(`crud_api.py`):产出物下拉数据源,返回**裸数组** `[{value,text}]`,异常降级 `[]`;供 `json/pbl_evidence.json` 的 `alters.artifact_id.dataurl` |
|
||||
| 12 | `pbl_evidence_update` | `pbl_evidence_update.dspy` | **CRUD 适配**(`crud_api.py`):人工修订证据,强制 `tenant_id` 归属校验;改唯一索引三元组时同步重算 `dedup_key`;供 `editable.update_data_url` |
|
||||
| 13 | `pbl_evidence_delete` | `pbl_evidence_delete.dspy` | **CRUD 适配**(`crud_api.py`):按 id/ids 删除证据,逐条校验归属;供 `editable.delete_data_url` |
|
||||
|
||||
**watermark 的调用方式(第 10 条契约刻意不生成 .dspy)**:
|
||||
- 内部调用:宿主/cron/M5b 通过 `ServerEnv().pbl_evidence_watermark(...)` 直调;
|
||||
@ -46,10 +51,20 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序
|
||||
- 若将来需要前端直接轮询,再新增 `wwwroot/api/pbl_evidence_watermark.dspy`,并**同步登记**
|
||||
`scripts/load_path.py` + 中央 `apps/pbls/scripts/load_path.py`(双层 RBAC),否则 403。
|
||||
|
||||
> 除上述 10 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、
|
||||
> 第 11~13 条是 **CRUD 框架适配契约**(定义在 `pbl_evidence/crud_api.py`,不改 `api.py`),
|
||||
> 同样走「三处同步」:`crud_api.py` 定义 → `__init__.py` 导出 → `init.py` `env.xxx = xxx`,
|
||||
> 并各自有 `wwwroot/api/*.dspy` 薄包装与 `scripts/load_path.py` 登记。漏任一处 = dspy 调用
|
||||
> NameError / 端点 404 / 登录后 403(QC 硬门禁三条,实测都踩过)。
|
||||
> `json/pbl_artifact.json` 的 new/update/delete 指向既有 `pbl_artifact_{create,update,delete}.dspy`
|
||||
> (委托 `api.py` 第 1/3/4 条契约),**不经过 crud_api**。
|
||||
|
||||
> 除上述 13 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、
|
||||
> 无 .dspy):`pbl_collect_evidence_from_events`、`pbl_resolve_event_table`、`pbl_evidence_types`、
|
||||
> `pbl_event_to_evidence_map`。核对口径:`api.py` 契约定义 10 == `__init__.py` 契约导出 10
|
||||
> == `init.py` 契约 env 注册 10 == `wwwroot/api/*.dspy` 9(watermark 无 .dspy)。
|
||||
> `pbl_event_to_evidence_map`。核对口径:
|
||||
> `api.py` 契约定义 10 + `crud_api.py` 适配定义 3 == `__init__.py` 契约导出 13
|
||||
> == `init.py` 契约 env 注册 13 == `wwwroot/api/*.dspy` 12(watermark 无 .dspy)。
|
||||
> 自查命令:`grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py'`
|
||||
> 必须命中 定义(crud_api.py) / __init__.py import / __all__ / init.py import / env 注册。
|
||||
|
||||
## 编码字典(init/data.json,Format B)
|
||||
| parentid(≤22 字符) | 含义 | 子项 |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user