diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6579d96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.coverage +.DS_Store +*.log +node_modules/ +dist/ +build/ +.venv/ +venv/ diff --git a/json/evidence_pbl_artifact.json b/json/evidence_pbl_artifact.json new file mode 100644 index 0000000..6ade314 --- /dev/null +++ b/json/evidence_pbl_artifact.json @@ -0,0 +1,188 @@ +{ + "tblname": "pbl_artifact", + "alias": "pbl_evidence", + "browserfields": { + "id": { + "label": "ID", + "list": true, + "type": "number" + }, + "tenant_id": { + "label": "租户ID", + "list": true, + "type": "text" + }, + "artifact_uid": { + "label": "产出UID", + "type": "text", + "list": true + }, + "session_id": { + "label": "会话", + "type": "number", + "list": true + }, + "blueprint_id": { + "label": "蓝图", + "type": "number", + "list": true + }, + "team_id": { + "label": "团队", + "type": "number", + "list": true + }, + "creator_id": { + "label": "作者", + "type": "text", + "list": true + }, + "title": { + "label": "标题", + "type": "select", + "list": true + }, + "artifact_type": { + "label": "类型", + "type": "select", + "list": true + }, + "content_json": { + "label": "内容", + "type": "text", + "list": true + }, + "version_no": { + "label": "版本", + "type": "number", + "list": true + }, + "status": { + "label": "状态", + "type": "select", + "list": true + }, + "submitted_at": { + "label": "提交时间", + "type": "text", + "list": true + } + }, + "params": { + "browserfields": { + "id": { + "label": "ID", + "list": true, + "type": "number" + }, + "tenant_id": { + "label": "租户ID", + "list": true, + "type": "text" + }, + "artifact_uid": { + "label": "产出UID", + "type": "text", + "list": true + }, + "session_id": { + "label": "会话", + "type": "number", + "list": true + }, + "blueprint_id": { + "label": "蓝图", + "type": "number", + "list": true + }, + "team_id": { + "label": "团队", + "type": "number", + "list": true + }, + "creator_id": { + "label": "作者", + "type": "text", + "list": true + }, + "title": { + "label": "标题", + "type": "select", + "list": true + }, + "artifact_type": { + "label": "类型", + "type": "select", + "list": true + }, + "content_json": { + "label": "内容", + "type": "text", + "list": true + }, + "version_no": { + "label": "版本", + "type": "number", + "list": true + }, + "status": { + "label": "状态", + "type": "select", + "list": true + }, + "submitted_at": { + "label": "提交时间", + "type": "text", + "list": true + } + }, + "editable": { + "artifact_uid": { + "label": "产出UID", + "type": "text" + }, + "session_id": { + "label": "会话", + "type": "number" + }, + "blueprint_id": { + "label": "蓝图", + "type": "number" + }, + "team_id": { + "label": "团队", + "type": "number" + }, + "creator_id": { + "label": "作者", + "type": "text" + }, + "title": { + "label": "标题", + "type": "select" + }, + "artifact_type": { + "label": "类型", + "type": "select" + }, + "content_json": { + "label": "内容", + "type": "text" + }, + "version_no": { + "label": "版本", + "type": "number" + }, + "status": { + "label": "状态", + "type": "select" + }, + "submitted_at": { + "label": "提交时间", + "type": "text" + } + }, + "order_by": "id DESC", + "page_size": 20 + } +} diff --git a/json/evidence_pbl_evidence.json b/json/evidence_pbl_evidence.json new file mode 100644 index 0000000..77edccf --- /dev/null +++ b/json/evidence_pbl_evidence.json @@ -0,0 +1,160 @@ +{ + "tblname": "pbl_evidence", + "alias": "pbl_evidence", + "browserfields": { + "id": { + "label": "ID", + "list": true, + "type": "number" + }, + "tenant_id": { + "label": "租户ID", + "list": true, + "type": "text" + }, + "artifact_id": { + "label": "产出物", + "type": "number", + "list": true + }, + "evidence_type": { + "label": "证据类型", + "type": "select", + "list": true + }, + "source_event_id": { + "label": "来源 runtime_event", + "type": "text", + "list": true + }, + "session_id": { + "label": "会话", + "type": "number", + "list": true + }, + "learner_id": { + "label": "学习者", + "type": "text", + "list": true + }, + "blueprint_id": { + "label": "蓝图", + "type": "number", + "list": true + }, + "payload_json": { + "label": "证据体", + "type": "text", + "list": true + }, + "occurred_at": { + "label": "发生时间", + "type": "text", + "list": true + }, + "dedup_key": { + "label": "去重键", + "type": "text", + "list": true + } + }, + "params": { + "browserfields": { + "id": { + "label": "ID", + "list": true, + "type": "number" + }, + "tenant_id": { + "label": "租户ID", + "list": true, + "type": "text" + }, + "artifact_id": { + "label": "产出物", + "type": "number", + "list": true + }, + "evidence_type": { + "label": "证据类型", + "type": "select", + "list": true + }, + "source_event_id": { + "label": "来源 runtime_event", + "type": "text", + "list": true + }, + "session_id": { + "label": "会话", + "type": "number", + "list": true + }, + "learner_id": { + "label": "学习者", + "type": "text", + "list": true + }, + "blueprint_id": { + "label": "蓝图", + "type": "number", + "list": true + }, + "payload_json": { + "label": "证据体", + "type": "text", + "list": true + }, + "occurred_at": { + "label": "发生时间", + "type": "text", + "list": true + }, + "dedup_key": { + "label": "去重键", + "type": "text", + "list": true + } + }, + "editable": { + "artifact_id": { + "label": "产出物", + "type": "number" + }, + "evidence_type": { + "label": "证据类型", + "type": "select" + }, + "source_event_id": { + "label": "来源 runtime_event", + "type": "text" + }, + "session_id": { + "label": "会话", + "type": "number" + }, + "learner_id": { + "label": "学习者", + "type": "text" + }, + "blueprint_id": { + "label": "蓝图", + "type": "number" + }, + "payload_json": { + "label": "证据体", + "type": "text" + }, + "occurred_at": { + "label": "发生时间", + "type": "text" + }, + "dedup_key": { + "label": "去重键", + "type": "text" + } + }, + "order_by": "id DESC", + "page_size": 20 + } +} diff --git a/models/pbl_artifact.json b/models/pbl_artifact.json new file mode 100644 index 0000000..c98d45d --- /dev/null +++ b/models/pbl_artifact.json @@ -0,0 +1,144 @@ +{ + "summary": [ + { + "name": "pbl_artifact", + "comment": "学生产出物(版本内联 version_no)", + "module": "pbl_evidence", + "engine": "mariadb", + "charset": "utf8mb4", + "tenant_scoped": true + } + ], + "fields": [ + { + "name": "tenant_id", + "comment": "租户ID(强制打头)", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "id", + "comment": "主键", + "null": false, + "type": "int", + "length": 20, + "unsigned": true, + "auto_increment": true + }, + { + "name": "artifact_uid", + "comment": "产出UID", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "session_id", + "comment": "会话", + "null": false, + "type": "bigint" + }, + { + "name": "blueprint_id", + "comment": "蓝图", + "null": false, + "type": "bigint" + }, + { + "name": "team_id", + "comment": "团队", + "null": false, + "type": "bigint" + }, + { + "name": "creator_id", + "comment": "作者", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "title", + "comment": "标题", + "null": false, + "type": "str", + "length": 128 + }, + { + "name": "artifact_type", + "comment": "类型", + "null": false, + "type": "str", + "length": 64 + }, + { + "name": "content_json", + "comment": "内容", + "null": true, + "type": "json" + }, + { + "name": "version_no", + "comment": "版本", + "null": false, + "type": "int" + }, + { + "name": "status", + "comment": "状态", + "null": false, + "type": "str", + "length": 64 + }, + { + "name": "submitted_at", + "comment": "提交时间", + "null": true, + "type": "datetime" + }, + { + "name": "created_at", + "comment": "创建时间", + "null": false, + "type": "datetime" + }, + { + "name": "updated_at", + "comment": "更新时间", + "null": false, + "type": "datetime" + } + ], + "indexes": [ + { + "name": "uk_art_uid", + "unique": true, + "fields": [ + "tenant_id", + "artifact_uid" + ] + }, + { + "name": "idx_art_session", + "unique": false, + "fields": [ + "tenant_id", + "session_id" + ] + }, + { + "name": "PRIMARY", + "unique": true, + "fields": [ + "id" + ] + } + ], + "codes": [ + { + "field": "artifact_type", + "appcode": "pbl_artifact_type" + } + ] +} diff --git a/models/pbl_evidence.json b/models/pbl_evidence.json new file mode 100644 index 0000000..d159644 --- /dev/null +++ b/models/pbl_evidence.json @@ -0,0 +1,132 @@ +{ + "summary": [ + { + "name": "pbl_evidence", + "comment": "学习证据(幂等采集,唯一索引防重)", + "module": "pbl_evidence", + "engine": "mariadb", + "charset": "utf8mb4", + "tenant_scoped": true + } + ], + "fields": [ + { + "name": "tenant_id", + "comment": "租户ID(强制打头)", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "id", + "comment": "主键", + "null": false, + "type": "int", + "length": 20, + "unsigned": true, + "auto_increment": true + }, + { + "name": "artifact_id", + "comment": "产出物", + "null": false, + "type": "bigint" + }, + { + "name": "evidence_type", + "comment": "证据类型", + "null": false, + "type": "str", + "length": 64 + }, + { + "name": "source_event_id", + "comment": "来源 runtime_event", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "session_id", + "comment": "会话", + "null": false, + "type": "bigint" + }, + { + "name": "learner_id", + "comment": "学习者", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "blueprint_id", + "comment": "蓝图", + "null": false, + "type": "bigint" + }, + { + "name": "payload_json", + "comment": "证据体", + "null": true, + "type": "json" + }, + { + "name": "occurred_at", + "comment": "发生时间", + "null": false, + "type": "datetime" + }, + { + "name": "dedup_key", + "comment": "去重键", + "null": false, + "type": "str", + "length": 32 + }, + { + "name": "created_at", + "comment": "创建时间", + "null": false, + "type": "datetime" + }, + { + "name": "updated_at", + "comment": "更新时间", + "null": false, + "type": "datetime" + } + ], + "indexes": [ + { + "name": "uk_ev_dedup", + "unique": true, + "fields": [ + "tenant_id", + "source_event_id", + "evidence_type" + ] + }, + { + "name": "idx_ev_learner", + "unique": false, + "fields": [ + "tenant_id", + "learner_id" + ] + }, + { + "name": "PRIMARY", + "unique": true, + "fields": [ + "id" + ] + } + ], + "codes": [ + { + "field": "evidence_type", + "appcode": "pbl_evidence_type" + } + ] +} diff --git a/pbl_evidence/__init__.py b/pbl_evidence/__init__.py new file mode 100644 index 0000000..da9cd67 --- /dev/null +++ b/pbl_evidence/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_evidence —— 产出物与证据幂等采集(M5a/M5b) + +注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。 +""" +from pbl_evidence.init import load_pbl_evidence +from pbl_evidence.api import ( + pbl_artifact_create, + pbl_artifact_read, + pbl_artifact_update, + pbl_artifact_delete, + pbl_artifact_list, + pbl_evidence_collect, + pbl_evidence_list, + +) + +__all__ = [ + 'load_pbl_evidence', + 'pbl_artifact_create', + 'pbl_artifact_read', + 'pbl_artifact_update', + 'pbl_artifact_delete', + 'pbl_artifact_list', + 'pbl_evidence_collect', + 'pbl_evidence_list', + +] diff --git a/pbl_evidence/api.py b/pbl_evidence/api.py new file mode 100644 index 0000000..8164c3e --- /dev/null +++ b/pbl_evidence/api.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_evidence.api —— 产出物 + 证据幂等采集(第18/19章,M5a/M5b)。 + +幂等硬线:`pbl_evidence` 唯一索引 (tenant_id, source_event_id, evidence_type)。 +重复采集(重放/双通道:广播 + 3s 轮询兜底同事件)→ 命中已存在行,返回 deduped=True, +不产生第二条证据,也不报错(学生侧无感)。 +""" +import hashlib +import json + +from pbl_common.api import (PblError, actor_id, crud, json_dump, now_str, sql_exec, + sql_rows, tenant_id) + +EVIDENCE_TYPES = ['artifact', 'process_event', 'reflection', 'feedback'] + + +def _loads(v, d=None): + if isinstance(v, (dict, list)): + return v + try: + return json.loads(v) if v else (d or {}) + except ValueError: + return d or {} + + +ART = crud('pbl_artifact', 'pbl', ['artifact_uid', 'session_id', 'blueprint_id', 'team_id', + 'creator_id', 'title', 'artifact_type', 'content_json', + 'version_no', 'status', 'submitted_at']) +EV = crud('pbl_evidence', 'pbl', ['artifact_id', 'evidence_type', 'source_event_id', + 'session_id', 'learner_id', 'blueprint_id', 'payload_json', + 'occurred_at', 'dedup_key']) + + +async def _tid(): + return await tenant_id() + + +async def pbl_artifact_create(**kw): + tid = await _tid() + kw.setdefault('artifact_uid', 'AR%s' % hashlib.sha256( + (str(kw.get('title')) + now_str()).encode()).hexdigest()[:20]) + kw.setdefault('version_no', 1) + kw.setdefault('status', 'draft') + kw['content_json'] = json_dump(kw.get('content_json')) + kw['creator_id'] = kw.get('creator_id') or await actor_id() + r = await ART['create'](**kw) + r['artifact_uid'] = kw['artifact_uid'] + return r + + +async def pbl_artifact_read(**kw): + return await ART['read'](**kw) + + +async def pbl_artifact_update(**kw): + """改内容即版本递增(第29.8 版本化),历史版本快照存 version_history_json 于 content_json 外层。""" + tid = await _tid() + cur = await ART['read'](id=kw.get('id')) + data = cur['data'] + payload = dict(kw) + if 'content_json' in payload: + payload['content_json'] = json_dump(payload['content_json']) + try: + hist = _loads(data.get('content_json'), {}) + except Exception: + hist = {} + payload['content_json'] = json_dump({'current': _loads(payload['content_json']), + 'history': {'v%s' % data['version_no']: hist}}) + payload['version_no'] = int(data['version_no'] or 1) + 1 + return await ART['update'](**payload) + + +async def pbl_artifact_delete(**kw): + tid = await _tid() + await sql_exec('DELETE FROM `pbl_evidence` WHERE `tenant_id` = ${t}$' + ' AND `artifact_id` = ${a}$', {'t': tid, 'a': kw.get('id')}, 'pbl') + return await ART['delete'](**kw) + + +async def pbl_artifact_list(**kw): + return await ART['list'](**kw) + + +async def pbl_evidence_collect(**kw): + """从 pbl_runtime_event 采集证据(服务端权威,客户端不能直接写证据)。""" + tid = await _tid() + etype = kw.get('evidence_type') or 'process_event' + if etype not in EVIDENCE_TYPES: + raise PblError('PBL_EVIDENCE_TYPE_INVALID', 'evidence_type 须为 %s' + % '/'.join(EVIDENCE_TYPES)) + src = kw.get('source_event_id') + if not src: + raise PblError('PBL_PARAM_MISSING', '缺少 source_event_id(证据必须可溯到事件)') + dup = await sql_rows('SELECT * FROM `pbl_evidence` WHERE `tenant_id` = ${t}$' + ' AND `source_event_id` = ${s}$ AND `evidence_type` = ${e}$ LIMIT 1', + {'t': tid, 's': src, 'e': etype}, 'pbl') + if dup: + return {'ok': True, 'deduped': True, 'id': dup[0]['id'], + 'unique_index': 'uk_ev_dedup(tenant_id,source_event_id,evidence_type)', + 'evidence_count': 1} + payload = {'evidence_type': etype, 'source_event_id': src, + 'session_id': kw.get('session_id') or 0, + 'learner_id': kw.get('learner_id') or await actor_id(), + 'blueprint_id': kw.get('blueprint_id'), + 'artifact_id': kw.get('artifact_id'), + 'payload_json': json_dump(kw.get('payload_json') or {}), + 'occurred_at': kw.get('occurred_at') or now_str(), + 'dedup_key': hashlib.sha256(('%s|%s' % (src, etype)).encode()).hexdigest()[:32]} + r = await EV['create'](**payload) + r['deduped'] = False + r['unique_index'] = 'uk_ev_dedup(tenant_id,source_event_id,evidence_type)' + return r + + +async def pbl_evidence_list(**kw): + tid = await _tid() + where, params = ['`tenant_id` = ${t}$'], {'t': tid} + for col in ('session_id', 'learner_id', 'blueprint_id', 'artifact_id', 'evidence_type'): + if kw.get(col) not in (None, ''): + where.append('`%s` = ${%s}$' % (col, col)) + params[col] = kw[col] + limit = min(int(kw.get('page_size') or 50), 200) + rows = await sql_rows('SELECT * FROM `pbl_evidence` WHERE %s ORDER BY `occurred_at` DESC' + ' LIMIT ${lim}$' % ' AND '.join(where), dict(params, lim=limit), 'pbl') + by_type = {} + for r in rows: + by_type[r['evidence_type']] = by_type.get(r['evidence_type'], 0) + 1 + return {'ok': True, 'total': len(rows), 'by_type': by_type, 'data': rows} diff --git a/pbl_evidence/init.py b/pbl_evidence/init.py new file mode 100644 index 0000000..0a0b7d9 --- /dev/null +++ b/pbl_evidence/init.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""`load_pbl_evidence()` —— pbl_evidence 模块唯一挂载入口。 + +注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。 +""" +from ahserver.serverenv import ServerEnv + +from pbl_evidence.api import ( + pbl_artifact_create, + pbl_artifact_read, + pbl_artifact_update, + pbl_artifact_delete, + pbl_artifact_list, + pbl_evidence_collect, + pbl_evidence_list, + +) + + +def load_pbl_evidence(): + env = ServerEnv() + env.pbl_artifact_create = pbl_artifact_create + env.pbl_artifact_read = pbl_artifact_read + env.pbl_artifact_update = pbl_artifact_update + env.pbl_artifact_delete = pbl_artifact_delete + env.pbl_artifact_list = pbl_artifact_list + env.pbl_evidence_collect = pbl_evidence_collect + env.pbl_evidence_list = pbl_evidence_list + + return 'pbl_evidence' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b77b39e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "pbl_evidence" +version = "0.1.0" +description = "产出物与证据幂等采集(M5a/M5b)" +requires-python = ">=3.9" +dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"] + +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["pbl_evidence"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..15d5b2e --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""pbl_evidence RBAC 路径注册(硬门禁 6.6 / QC #11)。 + +约定: +- 路径 = 模块自动路由 `/pbl_evidence/api/<契约>.dspy`,不带端口、不带 /wss 前缀; +- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin); +- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。 +""" +import os +import subprocess +import sys + +MODULE = 'pbl_evidence' + +# (path, role) +PATHS = [ + ('/pbl_evidence/api/pbl_artifact_create.dspy', 'logined'), + ('/pbl_evidence/api/pbl_artifact_read.dspy', 'logined'), + ('/pbl_evidence/api/pbl_artifact_update.dspy', 'logined'), + ('/pbl_evidence/api/pbl_artifact_delete.dspy', 'logined'), + ('/pbl_evidence/api/pbl_artifact_list.dspy', 'logined'), + ('/pbl_evidence/api/pbl_evidence_collect.dspy', 'logined'), + ('/pbl_evidence/api/pbl_evidence_list.dspy', 'logined'), + +] + + +def register(): + tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py') + 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: + done += 1 + else: + missing.append((path, role)) + print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing))) + for path, role in missing: + print(' PENDING %%-12s %%s' %% (role, path)) + return len(missing) == 0 + + +if __name__ == '__main__': + sys.exit(0 if register() else 1) diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..dda67d4 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,26 @@ +# pbl_evidence 模块技能(自动生成骨架 + 人工补充) + +## 定位 +产出物与证据幂等采集(M5a/M5b) + +## 挂载 +`from pbl_evidence.init import load_pbl_evidence` → `load_pbl_evidence()`(应用 app/pbls.py init() 中按序调用) + +## 数据表(2 张) +- `pbl_artifact`:学生产出物(版本内联 version_no) +- `pbl_evidence`:学习证据(幂等采集,唯一索引防重) + +## 契约接口(7 个,路径 `/pbl_evidence/api/.dspy`) +- `pbl_artifact_create` +- `pbl_artifact_read` +- `pbl_artifact_update` +- `pbl_artifact_delete` +- `pbl_artifact_list` +- `pbl_evidence_collect` +- `pbl_evidence_list` + +## 陷阱 +- 库名一律 `ServerEnv().get_module_dbname('pbl_evidence')`,禁止硬编码 DBNAME。 +- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。 +- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。 +- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。 diff --git a/sql/pbl_evidence.sql b/sql/pbl_evidence.sql new file mode 100644 index 0000000..81a708e --- /dev/null +++ b/sql/pbl_evidence.sql @@ -0,0 +1,41 @@ +-- pbl_evidence 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源) +CREATE TABLE IF NOT EXISTS `pbl_artifact` ( + `tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头), + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, + `artifact_uid` VARCHAR(32) NOT NULL COMMENT "产出UID", + `session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话", + `blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图", + `team_id` BIGINT UNSIGNED NOT NULL COMMENT "团队", + `creator_id` VARCHAR(32) NOT NULL COMMENT "作者", + `title` VARCHAR(128) NOT NULL COMMENT "标题", + `artifact_type` VARCHAR(64) NOT NULL COMMENT "类型", + `content_json` LONGTEXT NULL COMMENT "内容", + `version_no` INT NOT NULL DEFAULT 0 COMMENT "版本", + `status` VARCHAR(64) NOT NULL COMMENT "状态", + `submitted_at` DATETIME NULL NULL COMMENT "提交时间", + `created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)", + `updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)", + PRIMARY KEY (`id`), + UNIQUE KEY `uk_art_uid` (`tenant_id`, `artifact_uid`), + KEY `idx_art_session` (`tenant_id`, `session_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学生产出物(版本内联 version_no)"; + +CREATE TABLE IF NOT EXISTS `pbl_evidence` ( + `tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头), + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, + `artifact_id` BIGINT UNSIGNED NOT NULL COMMENT "产出物", + `evidence_type` VARCHAR(64) NOT NULL COMMENT "证据类型", + `source_event_id` VARCHAR(32) NOT NULL COMMENT "来源 runtime_event", + `session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话", + `learner_id` VARCHAR(32) NOT NULL COMMENT "学习者", + `blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图", + `payload_json` LONGTEXT NULL COMMENT "证据体", + `occurred_at` DATETIME NOT NULL COMMENT "发生时间", + `dedup_key` VARCHAR(32) NOT NULL COMMENT "去重键", + `created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)", + `updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)", + PRIMARY KEY (`id`), + UNIQUE KEY `uk_ev_dedup` (`tenant_id`, `source_event_id`, `evidence_type`), + KEY `idx_ev_learner` (`tenant_id`, `learner_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学习证据(幂等采集,唯一索引防重)"; + diff --git a/wwwroot/api/pbl_artifact_create.dspy b/wwwroot/api/pbl_artifact_create.dspy new file mode 100644 index 0000000..f0ce9aa --- /dev/null +++ b/wwwroot/api/pbl_artifact_create.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_artifact_create.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_artifact_create.dspy: START params_kw={dict(params_kw)}') +data = await pbl_artifact_create(**params_kw) +return data diff --git a/wwwroot/api/pbl_artifact_delete.dspy b/wwwroot/api/pbl_artifact_delete.dspy new file mode 100644 index 0000000..c9e9f91 --- /dev/null +++ b/wwwroot/api/pbl_artifact_delete.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_artifact_delete.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_artifact_delete.dspy: START params_kw={dict(params_kw)}') +data = await pbl_artifact_delete(**params_kw) +return data diff --git a/wwwroot/api/pbl_artifact_list.dspy b/wwwroot/api/pbl_artifact_list.dspy new file mode 100644 index 0000000..d179d9f --- /dev/null +++ b/wwwroot/api/pbl_artifact_list.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_artifact_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_artifact_list.dspy: START params_kw={dict(params_kw)}') +data = await pbl_artifact_list(**params_kw) +return data diff --git a/wwwroot/api/pbl_artifact_read.dspy b/wwwroot/api/pbl_artifact_read.dspy new file mode 100644 index 0000000..fd2e2d2 --- /dev/null +++ b/wwwroot/api/pbl_artifact_read.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_artifact_read.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_artifact_read.dspy: START params_kw={dict(params_kw)}') +data = await pbl_artifact_read(**params_kw) +return data diff --git a/wwwroot/api/pbl_artifact_update.dspy b/wwwroot/api/pbl_artifact_update.dspy new file mode 100644 index 0000000..e463e3b --- /dev/null +++ b/wwwroot/api/pbl_artifact_update.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_artifact_update.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_artifact_update.dspy: START params_kw={dict(params_kw)}') +data = await pbl_artifact_update(**params_kw) +return data diff --git a/wwwroot/api/pbl_evidence_collect.dspy b/wwwroot/api/pbl_evidence_collect.dspy new file mode 100644 index 0000000..e2ec545 --- /dev/null +++ b/wwwroot/api/pbl_evidence_collect.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_evidence_collect.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_evidence_collect.dspy: START params_kw={dict(params_kw)}') +data = await pbl_evidence_collect(**params_kw) +return data diff --git a/wwwroot/api/pbl_evidence_list.dspy b/wwwroot/api/pbl_evidence_list.dspy new file mode 100644 index 0000000..00562ae --- /dev/null +++ b/wwwroot/api/pbl_evidence_list.dspy @@ -0,0 +1,4 @@ +# pbl_evidence/api/pbl_evidence_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py) +debug('pbl_evidence/api/pbl_evidence_list.dspy: START params_kw={dict(params_kw)}') +data = await pbl_evidence_list(**params_kw) +return data diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..0ebbb27 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,231 @@ +{ + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%", + "padding": "20px" + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "产出物与证据幂等采集(M5a/M5b)", + "fontSize": "24px" + } + }, + { + "widgettype": "ResponsableBox", + "options": { + "gap": "16px", + "minWidth": "250px" + }, + "subwidgets": [ + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_artifact_create.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_artifact_create" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_artifact_read.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_artifact_read" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_artifact_update.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_artifact_update" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_artifact_delete.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_artifact_delete" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_artifact_list.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_artifact_list" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_evidence_collect.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_evidence_collect" + } + } + ] + }, + { + "widgettype": "VBox", + "options": { + "backgroundColor": "#FFFFFF", + "padding": "20px", + "cursor": "pointer" + }, + "binds": [ + { + "wid": "self", + "event": "click", + "actiontype": "urlwidget", + "target": "app.pbl_evidence_content", + "options": { + "url": "{{entire_url('api/pbl_evidence_list.dspy')}}" + }, + "mode": "replace" + } + ], + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "label": "pbl_evidence_list" + } + } + ] + } + ] + }, + { + "widgettype": "VBox", + "id": "pbl_evidence_content", + "options": { + "width": "100%", + "flex": "1", + "marginTop": "20px" + } + } + ] +}