deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
d198fb88cb
commit
a026feb25a
@ -1,187 +1,56 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"title": "学习产出物(artifact)",
|
||||
"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
|
||||
"exclouded": [
|
||||
"content_json",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"alters": {
|
||||
"artifact_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "document", "text": "文档报告"},
|
||||
{"value": "code", "text": "程序代码"},
|
||||
{"value": "model", "text": "模型/场景"},
|
||||
{"value": "presentation", "text": "演示汇报"},
|
||||
{"value": "poster", "text": "海报展板"},
|
||||
{"value": "video", "text": "音视频作品"},
|
||||
{"value": "dataset", "text": "数据/素材集"},
|
||||
{"value": "other", "text": "其他"}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "draft", "text": "草稿"},
|
||||
{"value": "submitted", "text": "已提交"},
|
||||
{"value": "reviewing", "text": "评审中"},
|
||||
{"value": "returned", "text": "退回修改"},
|
||||
{"value": "graded", "text": "已评分"},
|
||||
{"value": "archived", "text": "已归档"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"tenant_id",
|
||||
"artifact_uid",
|
||||
"version_no",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"logined_userorgid": "tenant_id",
|
||||
"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"
|
||||
}
|
||||
"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
|
||||
}
|
||||
|
||||
@ -1,159 +1,54 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
},
|
||||
"title": "学习证据(runtime_event 幂等采集)",
|
||||
"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
|
||||
"exclouded": [
|
||||
"payload_json",
|
||||
"dedup_key",
|
||||
"collect_batch_id",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"alters": {
|
||||
"evidence_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "artifact", "text": "产出物"},
|
||||
{"value": "assessment", "text": "测评作答"},
|
||||
{"value": "peer_review", "text": "同伴互评"},
|
||||
{"value": "reflection", "text": "反思日志"},
|
||||
{"value": "milestone", "text": "里程碑达成"},
|
||||
{"value": "collaboration", "text": "协作行为"},
|
||||
{"value": "resource", "text": "资源使用"},
|
||||
{"value": "observation", "text": "一般观测"}
|
||||
]
|
||||
},
|
||||
"artifact_id": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/pbl_artifact_options.dspy')}}",
|
||||
"datamethod": "GET",
|
||||
"valueField": "value",
|
||||
"textField": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"tenant_id",
|
||||
"dedup_key",
|
||||
"collect_batch_id",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"logined_userorgid": "tenant_id",
|
||||
"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"
|
||||
}
|
||||
"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
|
||||
}
|
||||
|
||||
189
pbl_evidence/crud_api.py
Normal file
189
pbl_evidence/crud_api.py
Normal file
@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_evidence.crud_api —— CRUD 定义(json/*.json)所需的自定义端点后端。
|
||||
|
||||
为什么单独成文件(不改 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 框架适配。
|
||||
|
||||
规范依据(crud-definition-spec / module-development-spec)
|
||||
--------------------------------------------------------
|
||||
1. ``editable.new_data_url/update_data_url/delete_data_url`` 必须有对应
|
||||
``wwwroot/api/*.dspy``,且路径不含 ``wwwroot/``(json/ 视角用 ``../api/``)。
|
||||
2. 下拉数据接口(``alters[].dataurl``)**必须返回裸 JSON 数组**
|
||||
``[{value, text}, ...]``,异常/空数据返回 ``[]``,不许包 status/data。
|
||||
3. 多租户铁律:所有读写 ``tenant_id`` 强制打头,缺失即 fail-closed;
|
||||
更新/删除必须校验记录归属当前租户,防越权改他人证据。
|
||||
4. 列名不猜:先 ``table_columns`` 发现真实列,再按交集拼 SQL。
|
||||
5. 证据表的幂等唯一索引是 ``uk_ev_dedup(tenant_id, source_event_id, evidence_type)``;
|
||||
手工编辑若改动这三元组,``dedup_key`` 必须同步重算,否则去重键与唯一索引漂移。
|
||||
"""
|
||||
|
||||
from pbl_common.api import PblError, actor_id, json_dump, now_str, tenant_id
|
||||
|
||||
from pbl_evidence.db import (
|
||||
build_update, build_where, json_loads, pick_columns, q_all, q_exec, q_one,
|
||||
table_columns,
|
||||
)
|
||||
from pbl_evidence.evidence_map import (
|
||||
EVIDENCE_TYPES, build_dedup_key, normalize_dt,
|
||||
)
|
||||
|
||||
# 证据表允许人工修订的列(唯一索引三元组中的 tenant_id 永不开放)
|
||||
EVIDENCE_EDITABLE_COLS = ('artifact_id', 'evidence_type', 'source_event_id',
|
||||
'session_id', 'learner_id', 'blueprint_id',
|
||||
'payload_json', 'occurred_at')
|
||||
|
||||
|
||||
def _require_tenant():
|
||||
tid = tenant_id()
|
||||
if not tid:
|
||||
raise PblError('PBL_E_TENANT_MISSING',
|
||||
'tenant_id 缺失:pbl_evidence 所有读写强制带租户(fail-closed)')
|
||||
return str(tid)
|
||||
|
||||
|
||||
def _blank(value):
|
||||
return value is None or (isinstance(value, str) and value.strip() == '')
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 下拉数据:产出物选项(browserfields.alters.artifact_id.dataurl)
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
async def pbl_artifact_options(**kw):
|
||||
"""返回当前租户可选产出物 ``[{value,text}]``(裸数组,供 bricks UiCode 渲染)。
|
||||
|
||||
只取 id/title 两列(不 SELECT *,避免把 content_json 大字段拖进下拉查询)。
|
||||
任何异常都降级为 ``[]``——下拉数据源报错会让整个列表页 500,宁可空列表。
|
||||
"""
|
||||
try:
|
||||
tid = _require_tenant()
|
||||
cols = await table_columns('pbl_artifact')
|
||||
if not cols or 'id' not in cols:
|
||||
return []
|
||||
text_col = 'title' if 'title' in cols else ('artifact_uid' if 'artifact_uid' in cols else 'id')
|
||||
where = {'tenant_id': tid}
|
||||
for key in ('session_id', 'blueprint_id', 'status', 'creator_id'):
|
||||
if not _blank(kw.get(key)) and key in cols:
|
||||
where[key] = kw[key]
|
||||
where_sql, args = build_where(where, cols)
|
||||
limit = min(500, max(1, int(kw.get('limit') or 200)))
|
||||
args2 = dict(args)
|
||||
args2['limit'] = limit
|
||||
rows = await q_all(
|
||||
'SELECT `id`, `%s` AS label FROM `pbl_artifact` WHERE %s '
|
||||
'ORDER BY `created_at` DESC LIMIT ${limit}$' % (text_col, where_sql), args2)
|
||||
out = []
|
||||
for r in rows:
|
||||
val = r.get('id')
|
||||
if _blank(val):
|
||||
continue
|
||||
label = r.get('label')
|
||||
out.append({'value': str(val),
|
||||
'text': ('%s' % label) if not _blank(label) else str(val)})
|
||||
return out
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 证据人工修订 / 删除(editable.update_data_url / delete_data_url)
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
async def pbl_evidence_update(**kw):
|
||||
"""按 id 修订证据(强制租户校验;payload_json 自动包成合法 JSON 文本)。
|
||||
|
||||
与幂等采集的边界:批量采集走 ``pbl_evidence_collect_from_events``(服务端权威、
|
||||
只增不改);本函数只服务人工纠错/补注,不开放 tenant_id,也不允许改 collect_batch_id
|
||||
(采集批次号是审计线索)。改唯一索引三元组时同步重算 dedup_key。
|
||||
"""
|
||||
tid = _require_tenant()
|
||||
eid = kw.get('id') or kw.get('evidence_id')
|
||||
if _blank(eid):
|
||||
raise PblError('PBL_E_PARAM', '缺少 id(证据主键)')
|
||||
|
||||
cols = await table_columns('pbl_evidence')
|
||||
if not cols:
|
||||
raise PblError('PBL_E_DB_UNAVAILABLE', 'pbl_evidence 表不存在,请先执行 models DDL')
|
||||
|
||||
where_sql, args = build_where({'tenant_id': tid, 'id': eid}, cols)
|
||||
cur = await q_one('SELECT * FROM `pbl_evidence` WHERE %s LIMIT 1' % where_sql, args)
|
||||
if not cur:
|
||||
raise PblError('PBL_E_NOT_FOUND', '证据不存在或不属于当前租户')
|
||||
|
||||
upd = {'updated_at': now_str()}
|
||||
for key in EVIDENCE_EDITABLE_COLS:
|
||||
if key not in cols or _blank(kw.get(key)):
|
||||
continue
|
||||
if key == 'evidence_type':
|
||||
etype = str(kw[key]).strip()
|
||||
if etype not in EVIDENCE_TYPES:
|
||||
raise PblError('PBL_E_PARAM',
|
||||
'evidence_type 须为 %s' % '/'.join(sorted(EVIDENCE_TYPES)))
|
||||
upd['evidence_type'] = etype
|
||||
elif key == 'payload_json':
|
||||
raw = kw[key]
|
||||
if isinstance(raw, str):
|
||||
parsed = json_loads(raw, default=None)
|
||||
if parsed is None:
|
||||
raise PblError('PBL_E_PARAM', 'payload_json 不是合法 JSON')
|
||||
upd['payload_json'] = json_dump(parsed)
|
||||
else:
|
||||
upd['payload_json'] = json_dump(raw)
|
||||
elif key == 'occurred_at':
|
||||
stamp = normalize_dt(kw[key])
|
||||
if not stamp:
|
||||
raise PblError('PBL_E_PARAM', 'occurred_at 格式须为 YYYY-MM-DD HH:MM:SS')
|
||||
upd['occurred_at'] = stamp
|
||||
elif key == 'source_event_id':
|
||||
upd['source_event_id'] = str(kw[key]).strip()[:32]
|
||||
else:
|
||||
upd[key] = str(kw[key])[:32]
|
||||
|
||||
# 唯一索引三元组变化 → dedup_key 同步重算(保持与 uk_ev_dedup 一致)
|
||||
if 'dedup_key' in cols:
|
||||
final_src = str(upd.get('source_event_id', cur.get('source_event_id') or ''))
|
||||
final_type = str(upd.get('evidence_type', cur.get('evidence_type') or ''))
|
||||
if final_src and final_type:
|
||||
upd['dedup_key'] = build_dedup_key(tid, final_src, final_type)
|
||||
|
||||
upd = pick_columns(upd, cols)
|
||||
sql, sql_args = build_update('pbl_evidence', upd, cols, {'tenant_id': tid, 'id': eid})
|
||||
await q_exec(sql, sql_args)
|
||||
return {'ok': True, 'status': 'OK', 'id': eid,
|
||||
'updated_fields': sorted(upd.keys()),
|
||||
'message': '证据已更新(%s 个字段)' % len(upd)}
|
||||
|
||||
|
||||
async def pbl_evidence_delete(**kw):
|
||||
"""按 id 删除证据(强制租户校验;支持 ids 批量,逗号分隔或数组)。"""
|
||||
tid = _require_tenant()
|
||||
raw_ids = kw.get('ids') if not _blank(kw.get('ids')) else (
|
||||
kw.get('id') or kw.get('evidence_id'))
|
||||
if _blank(raw_ids):
|
||||
raise PblError('PBL_E_PARAM', '缺少 id / ids')
|
||||
if isinstance(raw_ids, (list, tuple)):
|
||||
id_list = [str(x).strip() for x in raw_ids if not _blank(x)]
|
||||
else:
|
||||
id_list = [x.strip() for x in str(raw_ids).split(',') if x.strip()]
|
||||
if not id_list:
|
||||
raise PblError('PBL_E_PARAM', 'id / ids 为空')
|
||||
|
||||
cols = await table_columns('pbl_evidence')
|
||||
if not cols:
|
||||
raise PblError('PBL_E_DB_UNAVAILABLE', 'pbl_evidence 表不存在,请先执行 models DDL')
|
||||
|
||||
deleted = 0
|
||||
for eid in id_list[:500]:
|
||||
where_sql, args = build_where({'tenant_id': tid, 'id': eid}, cols)
|
||||
cur = await q_one('SELECT `id` FROM `pbl_evidence` WHERE %s LIMIT 1' % where_sql, args)
|
||||
if not cur or _blank(cur.get('id')):
|
||||
continue
|
||||
await q_exec('DELETE FROM `pbl_evidence` WHERE `tenant_id`=${tenant_id}$ '
|
||||
'AND `id`=${id}$', {'tenant_id': tid, 'id': eid})
|
||||
deleted += 1
|
||||
return {'ok': True, 'status': 'OK', 'deleted': deleted,
|
||||
'requested': len(id_list), 'operator': actor_id() or '0',
|
||||
'message': '已删除 %s 条证据' % deleted}
|
||||
Loading…
x
Reference in New Issue
Block a user