15 KiB
pbl_evidence
PBL(项目式学习)的产出物与学习证据模块。它做两件事:维护学生产出物(pbl_artifact),
以及把运行时产生的学习事件幂等地采集成学习证据(pbl_evidence)。
- 模块类型:业务模块(Python 包),不是独立部署单元 —— 没有 app.py、没有自己的端口、
没有 Dockerfile;由宿主应用(
apps/pbls)调用load_pbl_evidence()挂载后运行。 - 交付里程碑:M5a(事件 → 证据 幂等采集 + 产出物 CRUD)、M5b(回放 / 增量游标 / 统计)。
- 下游:M6 评估(Rubric 加权打分)以本模块的证据表为唯一事实源。
Features(能力清单)
| 能力 | 契约函数 | DSPY 路径 | 说明 |
|---|---|---|---|
| 产出物新增 | pbl_artifact_create |
/pbl_evidence/api/pbl_artifact_create.dspy |
自动补 tenant_id / artifact_uid / created_at |
| 产出物读取 | pbl_artifact_read |
/pbl_evidence/api/pbl_artifact_read.dspy |
按 id 读,强制租户过滤 |
| 产出物更新 | pbl_artifact_update |
/pbl_evidence/api/pbl_artifact_update.dspy |
白名单列更新,禁改 tenant_id |
| 产出物删除 | pbl_artifact_delete |
/pbl_evidence/api/pbl_artifact_delete.dspy |
带归属校验 |
| 产出物列表 | pbl_artifact_list |
/pbl_evidence/api/pbl_artifact_list.dspy |
分页 + 会话/蓝图/作者过滤 |
| 单事件落证据 | pbl_evidence_collect |
/pbl_evidence/api/pbl_evidence_collect.dspy |
一条事件 → 一条证据,重复调用返回 deduped=True |
| 批量采集(M5a 主能力) | pbl_evidence_collect_from_events |
/pbl_evidence/api/pbl_evidence_collect_from_events.dspy |
扫 pbl_runtime_event → 映射 → 幂等落库,返回 scanned/created/skipped/updated/ignored/failed + watermark |
| 证据列表 | pbl_evidence_list |
/pbl_evidence/api/pbl_evidence_list.dspy |
学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 |
| 证据统计 | pbl_evidence_stats |
/pbl_evidence/api/pbl_evidence_stats.dspy |
证据类型分布,供 M6 与概览页 |
| 增量游标 | pbl_evidence_watermark |
(无独立 dspy,供 M5b 回放 / cron 与内部调用) | 已采集最大事件时间 + 总量,3s 轮询兜底的起点 |
模块入口页:/pbl_evidence/index.ui(bricks 卡片导航,聚合上述 9 个 .dspy)。
幂等语义(本模块的核心承诺)
重复采集绝不产生第二条证据,也绝不报错(学生侧无感):
- 数据库层:唯一索引
uk_ev_dedup(tenant_id, source_event_id, evidence_type)—— 同一租户下「同一来源事件 + 同一证据类型」最多一行。这是唯一可信的防重屏障。 - 应用层双保险:写入前先按
(tenant_id, source_event_id, evidence_type)预查, 命中即skipped(或update_existing=True时刷新 payload);未命中才INSERT ... ON DUPLICATE KEY UPDATE,因此并发/重放同时命中也不会抛 IntegrityError。 - 去重键:
dedup_key = md5(tenant_id|source_event_id|evidence_type)(32 位定长, 与 VARCHAR(32) 严格对齐),便于外部对账与排障。 - 事件 → 证据映射是配置化常量表(
evidence_map.EVENT_TO_EVIDENCE,37 条同义词映射 + 7 条忽略黑名单),未知事件类型降级为observation而不丢事件;不靠 SQL 硬编码、不靠猜。
硬约束 C-3(上线顺序,必须遵守)
本模块批量采集读取的 pbl_runtime_event 表由 scense_runtime 的 M11b 建表并写入。
- M5 不得早于 M11b 上线,且 M5 与 M11 不得并行开发联调。
- 事件表缺失时
collector.resolve_event_table()返回 None → 抛CollectError→ 契约层转成明确错误码PBL_E_DB_UNAVAILABLE,不会静默返回空成功(fail-closed,便于运维定位)。 - 兼容层:事件表名按候选顺序发现
pbl_runtime_event→runtime_event→pbl_event; 列名一律经information_schema发现,不猜列名(分区表/改名不会导致整链 500)。
Data tables(数据表)
表定义在 models/{table}.json(四段式 summary/fields/indexes/codes),DDL 由 json2ddl 生成,
建表 SQL 快照在 sql/pbl_evidence.sql。
pbl_artifact —— 学生产出物
| 列 | 类型 | 说明 |
|---|---|---|
id |
str(32) PK | 主键 |
tenant_id |
str(32) NOT NULL | 租户(强制打头,缺失 fail-closed) |
artifact_uid |
str(32) | 业务唯一码,与 tenant 组成唯一键 |
session_id / blueprint_id / team_id |
str(32) | 会话 / 蓝图 / 团队归属 |
creator_id |
str(32) | 作者(学习者) |
title |
str(128) | 标题 |
artifact_type |
str(64) | 类型(编码 pbl_artifact_type,默认 other) |
content_json |
text | 产出物内容(序列化 JSON) |
version_no |
int | 版本号(内联,默认 1;版本快照由蓝图侧维护) |
status |
str(64) | 状态(编码 pbl_artifact_status,默认 draft) |
submitted_at / created_at / updated_at |
datetime | 时间与审计列(应用层写入,sqlor 不自动填) |
索引:uk_art_uid(tenant_id, artifact_uid) 唯一;idx_art_session / idx_art_blueprint /
idx_art_creator(均以 tenant_id 打头)。
pbl_evidence —— 学习证据
| 列 | 类型 | 说明 |
|---|---|---|
id |
str(32) PK | 主键 |
tenant_id |
str(32) NOT NULL | 租户 |
artifact_id |
str(32) | 归属产出物('0' = 无强归属的观测类证据) |
evidence_type |
str(64) | 证据类型(编码 pbl_evidence_type,8 类,源自 evidence_map.EVIDENCE_TYPES) |
source_event_id |
str(32) NOT NULL | 来源 runtime_event 事件 ID(幂等锚点) |
session_id / learner_id / blueprint_id |
str(32) | 查询维度 |
payload_json |
text | 证据体(事件摘要序列化 JSON) |
occurred_at |
datetime | 事件发生时间 |
dedup_key |
str(32) | md5(tenant|event|type) |
collect_batch_id |
str(32) | 采集批次(排障用) |
created_at / updated_at |
datetime | 审计列(应用层写入) |
索引:
uk_ev_dedupUNIQUE(tenant_id, source_event_id, evidence_type)—— 幂等的唯一事实源。 含义:同一租户内,一条来源事件的一种证据类型只允许一行;重放、双通道(广播 + 3s 轮询兜底) 命中同一事件时第二次写入被唯一键吸收,不产生重复证据。idx_ev_learner(tenant_id, learner_id, occurred_at)、idx_ev_artifact(tenant_id, artifact_id)、idx_ev_type_time(tenant_id, evidence_type, occurred_at)、idx_ev_batch(tenant_id, collect_batch_id)。
CRUD 定义在 json/evidence_pbl_artifact.json / json/evidence_pbl_evidence.json
(tblname + params,editable 的 new/update/delete_data_url 指向 wwwroot/api/ 自定义 dspy)。
Installation
作为模块,它随宿主应用一起安装(不单独起服务):
# 1) 安装 Python 包(在宿主 pkgs/ 或机构 modules/ 下)
cd pbl_evidence && pip install -e .
# 2) 生成建表 DDL 并执行(models/ 为四段式 JSON,用 json2ddl)
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 前端(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
python3 -c "import json;d=json.load(open('init/data.json'));print('groups:',[g['parentid'] for g in d['appcodes']])"
# 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>
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 回退,两层都必须在册。
自检(无需连库,可放 CI):
占位说明:离线自检脚本(三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 / 幂等键语义) 由后续子任务 M5a-2 产出,当前磁盘上尚不存在,故本 README 不引用其文件名。 在它落地前,用下列当前磁盘上真实存在的命令完成同等自检:
# ① 依赖可安装(pyproject 只声明 sqlor,不拉基础包)
pip install .
# ② 编码种子 JSON 合法 + parentid 与 models/*.json 的 codes.cond 一致
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(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
Integration(宿主挂载方式)
模块只依赖:基础包(sqlor、ahserver ServerEnv、appPublic 工具)、pbl_common(租户上下文/
错误码/时间序列化)、以及自己的两张表。不依赖任何宿主入口文件或宿主的 wwwroot 路径。
# apps/pbls/app/pbls.py
from ahserver.serverenv import ServerEnv
from appbase.init import load_appbase
from rbac.init import load_rbac
from bricks_for_python.init import load_pybricks
def get_module_dbname(m):
# 库名由宿主决定;本模块绝不硬编码 DBNAME
return MODULE_DBNAME_MAP.get(m, DEFAULT_DBNAME)
def init():
env = ServerEnv() # 必须在 load_rbac() 之前
env.get_module_dbname = get_module_dbname # ← 宿主必须先注册,否则本模块 fail-closed
load_appbase(); load_rbac(); load_pybricks()
from pbl_evidence.init import load_pbl_evidence
load_pbl_evidence() # 唯一集成点
前置条件:宿主必须在 ServerEnv 上注册 get_module_dbname。取不到库名时本模块直接抛
PblError(fail-closed),不会退化成查错库。租户上下文由 pbl_common.api.tenant_id() 提供,
所有读写强制带 tenant_id,缺失即报错。
函数注册三处同步(改一处必须改三处):
① pbl_evidence/api.py 定义 → ② pbl_evidence/__init__.py 导出 → ③ pbl_evidence/init.py
env.<name> = <name>。漏 ② → import 期 ImportError;漏 ③ → .dspy 调用 NameError。
新增对外路径还要同步 ④ scripts/load_path.py(+ 中央 apps/pbls/scripts/load_path.py)。
目录结构
pbl_evidence/
├── pbl_evidence/ # Python 包(= 模块名)
│ ├── __init__.py # 导出(三处同步之 ②)
│ ├── init.py # load_pbl_evidence()(三处同步之 ③)
│ ├── 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/ 12 个 .dspy(自动路由 /pbl_evidence/...)
├── init/data.json # 编码种子(Format B:3 组 appcodes + appcodes_kv 子项)
├── sql/pbl_evidence.sql # 建表 SQL 快照
├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对
├── skill/SKILL.md # 面向 agent 的模块技能文档
└── pyproject.toml
已知边界(如实说明)
- 批量采集(
pbl_evidence_collect_from_events)的真实回放未在本地环境跑通:该环境information_schema.tables中不存在pbl_runtime_event(M11b 未上线),采集会按设计抛CollectError → PBL_E_DB_UNAVAILABLE。已在 M11b 落库的联调环境验证前,此项标注为「待联调」。 pbl_evidence_watermark只作为契约函数注册(供 M5b/cron 内部调用),未单独暴露 .dspy; 如需外部轮询调用,新增 .dspy 后必须同步登记 RBAC。