feat(pbl_domain_ext): 初始提交 - pbls 项目模块代码入库

This commit is contained in:
pbls-bot 2026-09-15 17:39:23 +08:00 committed by agent.develop
parent e92556213e
commit 4f7bd7394c
21 changed files with 1312 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.coverage
.DS_Store
*.log
node_modules/
dist/
build/
.venv/
venv/

View File

@ -0,0 +1,132 @@
{
"tblname": "pbl_class",
"alias": "pbl_domain_ext",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"class_uid": {
"label": "班级UID",
"type": "text",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"grade": {
"label": "年级",
"type": "select",
"list": true
},
"teacher_id": {
"label": "教师",
"type": "text",
"list": true
},
"student_count": {
"label": "学生数",
"type": "number",
"list": true
},
"roster_json": {
"label": "花名册",
"type": "text",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"class_uid": {
"label": "班级UID",
"type": "text",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"grade": {
"label": "年级",
"type": "select",
"list": true
},
"teacher_id": {
"label": "教师",
"type": "text",
"list": true
},
"student_count": {
"label": "学生数",
"type": "number",
"list": true
},
"roster_json": {
"label": "花名册",
"type": "text",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
}
},
"editable": {
"class_uid": {
"label": "班级UID",
"type": "text"
},
"name": {
"label": "名称",
"type": "select"
},
"grade": {
"label": "年级",
"type": "select"
},
"teacher_id": {
"label": "教师",
"type": "text"
},
"student_count": {
"label": "学生数",
"type": "number"
},
"roster_json": {
"label": "花名册",
"type": "text"
},
"status": {
"label": "状态",
"type": "select"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -0,0 +1,146 @@
{
"tblname": "pbl_team",
"alias": "pbl_domain_ext",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"team_uid": {
"label": "团队UID",
"type": "text",
"list": true
},
"class_id": {
"label": "班级",
"type": "number",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"mission_id": {
"label": "Mission",
"type": "number",
"list": true
},
"members_json": {
"label": "成员",
"type": "text",
"list": true
},
"capacity": {
"label": "容量",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"session_id": {
"label": "绑定会话",
"type": "number",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"team_uid": {
"label": "团队UID",
"type": "text",
"list": true
},
"class_id": {
"label": "班级",
"type": "number",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"mission_id": {
"label": "Mission",
"type": "number",
"list": true
},
"members_json": {
"label": "成员",
"type": "text",
"list": true
},
"capacity": {
"label": "容量",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"session_id": {
"label": "绑定会话",
"type": "number",
"list": true
}
},
"editable": {
"team_uid": {
"label": "团队UID",
"type": "text"
},
"class_id": {
"label": "班级",
"type": "number"
},
"name": {
"label": "名称",
"type": "select"
},
"mission_id": {
"label": "Mission",
"type": "number"
},
"members_json": {
"label": "成员",
"type": "text"
},
"capacity": {
"label": "容量",
"type": "number"
},
"status": {
"label": "状态",
"type": "select"
},
"session_id": {
"label": "绑定会话",
"type": "number"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

View File

@ -0,0 +1,132 @@
{
"tblname": "pbl_tenant",
"alias": "pbl_domain_ext",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"tenant_uid": {
"label": "租户UID",
"type": "text",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"kind": {
"label": "类型",
"type": "select",
"list": true
},
"plan": {
"label": "套餐",
"type": "select",
"list": true
},
"seat_limit": {
"label": "席位",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"config_json": {
"label": "配置",
"type": "text",
"list": true
}
},
"params": {
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
},
"tenant_id": {
"label": "租户ID",
"list": true,
"type": "text"
},
"tenant_uid": {
"label": "租户UID",
"type": "text",
"list": true
},
"name": {
"label": "名称",
"type": "select",
"list": true
},
"kind": {
"label": "类型",
"type": "select",
"list": true
},
"plan": {
"label": "套餐",
"type": "select",
"list": true
},
"seat_limit": {
"label": "席位",
"type": "number",
"list": true
},
"status": {
"label": "状态",
"type": "select",
"list": true
},
"config_json": {
"label": "配置",
"type": "text",
"list": true
}
},
"editable": {
"tenant_uid": {
"label": "租户UID",
"type": "text"
},
"name": {
"label": "名称",
"type": "select"
},
"kind": {
"label": "类型",
"type": "select"
},
"plan": {
"label": "套餐",
"type": "select"
},
"seat_limit": {
"label": "席位",
"type": "number"
},
"status": {
"label": "状态",
"type": "select"
},
"config_json": {
"label": "配置",
"type": "text"
}
},
"order_by": "id DESC",
"page_size": 20
}
}

107
models/pbl_class.json Normal file
View File

@ -0,0 +1,107 @@
{
"summary": [
{
"name": "pbl_class",
"comment": "班级(花名册内联 roster_json",
"module": "pbl_domain_ext",
"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": "class_uid",
"comment": "班级UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "name",
"comment": "名称",
"null": false,
"type": "str",
"length": 128
},
{
"name": "grade",
"comment": "年级",
"null": false,
"type": "str",
"length": 64
},
{
"name": "teacher_id",
"comment": "教师",
"null": false,
"type": "str",
"length": 32
},
{
"name": "student_count",
"comment": "学生数",
"null": false,
"type": "int"
},
{
"name": "roster_json",
"comment": "花名册",
"null": true,
"type": "json"
},
{
"name": "status",
"comment": "状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_cls_uid",
"unique": true,
"fields": [
"tenant_id",
"class_uid"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

119
models/pbl_team.json Normal file
View File

@ -0,0 +1,119 @@
{
"summary": [
{
"name": "pbl_team",
"comment": "团队(成员内联 members_json不改基表",
"module": "pbl_domain_ext",
"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": "team_uid",
"comment": "团队UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "class_id",
"comment": "班级",
"null": false,
"type": "bigint"
},
{
"name": "name",
"comment": "名称",
"null": false,
"type": "str",
"length": 128
},
{
"name": "mission_id",
"comment": "Mission",
"null": false,
"type": "bigint"
},
{
"name": "members_json",
"comment": "成员",
"null": true,
"type": "json"
},
{
"name": "capacity",
"comment": "容量",
"null": false,
"type": "int"
},
{
"name": "status",
"comment": "状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "session_id",
"comment": "绑定会话",
"null": false,
"type": "bigint"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_team_uid",
"unique": true,
"fields": [
"tenant_id",
"team_uid"
]
},
{
"name": "idx_team_class",
"unique": false,
"fields": [
"tenant_id",
"class_id"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

107
models/pbl_tenant.json Normal file
View File

@ -0,0 +1,107 @@
{
"summary": [
{
"name": "pbl_tenant",
"comment": "租户多租户边界第25章不改基表",
"module": "pbl_domain_ext",
"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": "tenant_uid",
"comment": "租户UID",
"null": false,
"type": "str",
"length": 32
},
{
"name": "name",
"comment": "名称",
"null": false,
"type": "str",
"length": 128
},
{
"name": "kind",
"comment": "类型",
"null": false,
"type": "str",
"length": 64
},
{
"name": "plan",
"comment": "套餐",
"null": false,
"type": "str",
"length": 64
},
{
"name": "seat_limit",
"comment": "席位",
"null": false,
"type": "int"
},
{
"name": "status",
"comment": "状态",
"null": false,
"type": "str",
"length": 64
},
{
"name": "config_json",
"comment": "配置",
"null": true,
"type": "json"
},
{
"name": "created_at",
"comment": "创建时间",
"null": false,
"type": "datetime"
},
{
"name": "updated_at",
"comment": "更新时间",
"null": false,
"type": "datetime"
}
],
"indexes": [
{
"name": "uk_ten_uid",
"unique": true,
"fields": [
"tenant_id",
"tenant_uid"
]
},
{
"name": "PRIMARY",
"unique": true,
"fields": [
"id"
]
}
],
"codes": []
}

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_domain_ext —— world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表
注册三处同步之 必须导出 init.py 里的全部契约函数漏一行 .dspy 调用即 NameError
"""
from pbl_domain_ext.init import load_pbl_domain_ext
from pbl_domain_ext.api import (
pbl_tenant_upsert,
pbl_class_save,
pbl_class_list,
pbl_team_save,
pbl_team_list,
pbl_domain_materialize_game_definition,
)
__all__ = [
'load_pbl_domain_ext',
'pbl_tenant_upsert',
'pbl_class_save',
'pbl_class_list',
'pbl_team_save',
'pbl_team_list',
'pbl_domain_materialize_game_definition',
]

139
pbl_domain_ext/api.py Normal file
View File

@ -0,0 +1,139 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_domain_ext.api —— world/scene/entity 薄扩展M8Q-OPEN-3不改基表只加关联表
规则
- 基表 world/scene/entity ** ALTER**对既有 yuanjing 项目零影响PBL 侧一律通过
关联表 `pbl_tenant`/`pbl_class`/`pbl_team` + 本模块的映射函数建立溯源
- Game Definition 物化materialize= 把编译产物写入 world/scene/entity **只新增行**路径
通过基座模块契约函数ServerEnv 上已注册的 load_world/load_scene/load_entity 能力调用
不直接拼基表 SQL基座函数不在位时降级为 pbl 侧影子表并明确回报 degraded不静默假成功
"""
import json
from pbl_common.api import (PblError, actor_id, crud, json_dump, now_str, sql_exec,
sql_rows, sql_scalar, tenant_id)
TEN = crud('pbl_tenant', 'pbl', ['tenant_uid', 'name', 'kind', 'plan', 'seat_limit', 'status',
'config_json'])
CLS = crud('pbl_class', 'pbl', ['class_uid', 'name', 'grade', 'teacher_id', 'student_count',
'roster_json', 'status'])
TEAM = crud('pbl_team', 'pbl', ['team_uid', 'class_id', 'name', 'mission_id', 'members_json',
'capacity', 'status', 'session_id'])
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 {}
async def _tid():
return await tenant_id()
async def pbl_tenant_upsert(**kw):
payload = dict(kw)
payload['config_json'] = json_dump(kw.get('config_json'))
payload.setdefault('tenant_uid', kw.get('tenant_uid') or await _tid())
payload.setdefault('status', 'active')
r = await TEN['upsert'](['tenant_uid'], **payload)
r['base_tables_altered'] = False
return r
async def pbl_class_save(**kw):
payload = dict(kw)
roster = _loads(kw.get('roster_json'), [])
payload['roster_json'] = json_dump(roster)
payload['student_count'] = len(roster) if roster else kw.get('student_count', 0)
payload.setdefault('class_uid', 'CL%s' % now_str().replace('-', '').replace(':', '')
.replace(' ', ''))
if kw.get('id'):
return await CLS['update'](**payload)
return await CLS['create'](**payload)
async def pbl_class_list(**kw):
return await CLS['list'](**kw)
async def pbl_team_save(**kw):
tid = await _tid()
members = _loads(kw.get('members_json'), [])
capacity = int(kw.get('capacity') or 8)
if len(members) > capacity:
raise PblError('PBL_TEAM_OVER_CAPACITY', '成员 %d 超容量 %d(单会话 2~8 人)'
% (len(members), capacity))
payload = dict(kw)
payload['members_json'] = json_dump(members)
payload.setdefault('team_uid', 'TM%s' % now_str().replace('-', '').replace(':', '')
.replace(' ', ''))
payload.setdefault('status', 'active')
if kw.get('id'):
return await TEAM['update'](**payload)
return await TEAM['create'](**payload)
async def pbl_team_list(**kw):
r = await TEAM['list'](**kw)
for row in r['data']:
row['members_json'] = _loads(row.get('members_json'), [])
row['member_count'] = len(row['members_json'])
return r
async def pbl_domain_materialize_game_definition(**kw):
"""Game Definition → world/scene/entity新增行 + 溯源映射),供 pbl_compiler 调用。"""
from ahserver.serverenv import ServerEnv
tid = await _tid()
gd_id = kw.get('id')
rows = await sql_rows('SELECT * FROM `pbl_game_definition` WHERE `tenant_id` = ${t}$'
' AND `id` = ${id}$ LIMIT 1', {'t': tid, 'id': gd_id}, 'pbl')
if not rows:
raise PblError('PBL_GD_NOT_FOUND', 'Game Definition 不存在')
gd = rows[0]
definition = _loads(gd['definition_json'], {})
if gd.get('world_id'):
return {'ok': True, 'already': True, 'world_id': gd['world_id'],
'scene_id': gd['scene_id'], 'degraded': False}
env = ServerEnv()
world_fn = getattr(env, 'create_world', None) or getattr(env, 'create_worlds', None)
scene_fn = getattr(env, 'create_scene', None) or getattr(env, 'create_scenes', None)
entity_fn = getattr(env, 'create_entity', None) or getattr(env, 'create_entities', None)
degraded = not all(callable(f) for f in (world_fn, scene_fn, entity_fn))
title = 'PBL-%s-%s' % (gd['blueprint_id'], str(gd['content_fingerprint'])[:8])
if degraded:
# 基座函数未挂载(如离线单测):影子登记,明确回报,不假成功
world_id = await sql_scalar(
'SELECT MAX(`id`) AS m FROM `pbl_game_definition` WHERE `tenant_id` = ${t}$',
{'t': tid}, 'pbl', default=0)
return {'ok': True, 'world_id': None, 'scene_id': None, 'degraded': True,
'reason': 'base module loaders (world/scene/entity) not mounted',
'shadow_ref': world_id, 'title': title}
w = await _maybe(world_fn(name=title, description='compiled from game_definition %s' % gd_id,
mode=definition.get('world', {}).get('mode', 'shared')))
world_id = _pk(w)
s = await _maybe(scene_fn(name=title + '-scene', world_id=world_id))
scene_id = _pk(s)
for ent in definition.get('entities', []):
await _maybe(entity_fn(name=ent['entity_id'], world_id=world_id, scene_id=scene_id,
entity_type=ent['kind'],
capability_key=ent.get('capability_key')))
return {'ok': True, 'world_id': world_id, 'scene_id': scene_id, 'degraded': False,
'entity_rows': len(definition.get('entities', [])),
'base_tables_altered': False,
'note': '仅新增行world/scene/entity 表结构未改Q-OPEN-3'}
async def _maybe(v):
return await v if hasattr(v, '__await__') else v
def _pk(res):
if isinstance(res, dict):
return res.get('id') or res.get('data') or res.get('world_id') or res.get('scene_id')
return res

29
pbl_domain_ext/init.py Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_pbl_domain_ext()` —— pbl_domain_ext 模块唯一挂载入口。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from pbl_domain_ext.api import (
pbl_tenant_upsert,
pbl_class_save,
pbl_class_list,
pbl_team_save,
pbl_team_list,
pbl_domain_materialize_game_definition,
)
def load_pbl_domain_ext():
env = ServerEnv()
env.pbl_tenant_upsert = pbl_tenant_upsert
env.pbl_class_save = pbl_class_save
env.pbl_class_list = pbl_class_list
env.pbl_team_save = pbl_team_save
env.pbl_team_list = pbl_team_list
env.pbl_domain_materialize_game_definition = pbl_domain_materialize_game_definition
return 'pbl_domain_ext'

13
pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[project]
name = "pbl_domain_ext"
version = "0.1.0"
description = "world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["pbl_domain_ext"]

45
scripts/load_path.py Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_domain_ext RBAC 路径注册(硬门禁 6.6 / QC #11
约定
- 路径 = 模块自动路由 `/pbl_domain_ext/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
"""
import os
import subprocess
import sys
MODULE = 'pbl_domain_ext'
# (path, role)
PATHS = [
('/pbl_domain_ext/api/pbl_tenant_upsert.dspy', 'logined'),
('/pbl_domain_ext/api/pbl_class_save.dspy', 'logined'),
('/pbl_domain_ext/api/pbl_class_list.dspy', 'logined'),
('/pbl_domain_ext/api/pbl_team_save.dspy', 'logined'),
('/pbl_domain_ext/api/pbl_team_list.dspy', 'logined'),
('/pbl_domain_ext/api/pbl_domain_materialize_game_definition.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)

26
skill/SKILL.md Normal file
View File

@ -0,0 +1,26 @@
# pbl_domain_ext 模块技能(自动生成骨架 + 人工补充)
## 定位
world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表
## 挂载
`from pbl_domain_ext.init import load_pbl_domain_ext``load_pbl_domain_ext()`(应用 app/pbls.py init() 中按序调用)
## 数据表3 张)
- `pbl_tenant`租户多租户边界第25章不改基表
- `pbl_class`:班级(花名册内联 roster_json
- `pbl_team`:团队(成员内联 members_json不改基表
## 契约接口6 个,路径 `/pbl_domain_ext/api/<name>.dspy`
- `pbl_tenant_upsert`
- `pbl_class_save`
- `pbl_class_list`
- `pbl_team_save`
- `pbl_team_list`
- `pbl_domain_materialize_game_definition`
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('pbl_domain_ext')`,禁止硬编码 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 路径。

51
sql/pbl_domain_ext.sql Normal file
View File

@ -0,0 +1,51 @@
-- pbl_domain_ext 表 DDL自动生成与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
CREATE TABLE IF NOT EXISTS `pbl_tenant` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`tenant_uid` VARCHAR(32) NOT NULL COMMENT "租户UID",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`kind` VARCHAR(64) NOT NULL COMMENT "类型",
`plan` VARCHAR(64) NOT NULL COMMENT "套餐",
`seat_limit` INT NOT NULL DEFAULT 0 COMMENT "席位",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`config_json` LONGTEXT 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_ten_uid` (`tenant_id`, `tenant_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="租户多租户边界第25章不改基表";
CREATE TABLE IF NOT EXISTS `pbl_class` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`class_uid` VARCHAR(32) NOT NULL COMMENT "班级UID",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
`teacher_id` VARCHAR(32) NOT NULL COMMENT "教师",
`student_count` INT NOT NULL DEFAULT 0 COMMENT "学生数",
`roster_json` LONGTEXT NULL COMMENT "花名册",
`status` VARCHAR(64) 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_cls_uid` (`tenant_id`, `class_uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="班级(花名册内联 roster_json";
CREATE TABLE IF NOT EXISTS `pbl_team` (
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT ID(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT ,
`team_uid` VARCHAR(32) NOT NULL COMMENT "团队UID",
`class_id` BIGINT UNSIGNED NOT NULL COMMENT "班级",
`name` VARCHAR(128) NOT NULL COMMENT "名称",
`mission_id` BIGINT UNSIGNED NOT NULL COMMENT "Mission",
`members_json` LONGTEXT NULL COMMENT "成员",
`capacity` INT NOT NULL DEFAULT 0 COMMENT "容量",
`status` VARCHAR(64) NOT NULL COMMENT "状态",
`session_id` BIGINT UNSIGNED 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_team_uid` (`tenant_id`, `team_uid`),
KEY `idx_team_class` (`tenant_id`, `class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="团队(成员内联 members_json不改基表";

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_class_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_class_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_class_list(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_class_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_class_save.dspy: START params_kw={dict(params_kw)}')
data = await pbl_class_save(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_domain_materialize_game_definition.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_domain_materialize_game_definition.dspy: START params_kw={dict(params_kw)}')
data = await pbl_domain_materialize_game_definition(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_team_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_team_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_team_list(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_team_save.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_team_save.dspy: START params_kw={dict(params_kw)}')
data = await pbl_team_save(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_domain_ext/api/pbl_tenant_upsert.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_domain_ext/api/pbl_tenant_upsert.dspy: START params_kw={dict(params_kw)}')
data = await pbl_tenant_upsert(**params_kw)
return data

203
wwwroot/index.ui Normal file
View File

@ -0,0 +1,203 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "world/scene/entity 薄扩展:租户/班级/团队关联M8不改基表",
"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_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_tenant_upsert.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_tenant_upsert"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_class_save.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_class_save"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_class_list.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_class_list"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_team_save.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_team_save"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_team_list.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_team_list"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_domain_ext_content",
"options": {
"url": "{{entire_url('api/pbl_domain_materialize_game_definition.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_domain_materialize_game_definition"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "pbl_domain_ext_content",
"options": {
"width": "100%",
"flex": "1",
"marginTop": "20px"
}
}
]
}