approve: 测试执行 - world_snapshot 世界快照模块

This commit is contained in:
agent.develop 2026-08-29 15:11:13 +08:00
parent 7ff253692c
commit b2db4764e2
12 changed files with 197 additions and 134 deletions

19
.gitignore vendored
View File

@ -1,10 +1,21 @@
# Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.egg-info/ *$py.class
# Distribution / packaging
build/ build/
dist/ dist/
models/mysql.ddl.sql *.egg-info/
wwwroot/entity/ .eggs/
wwwroot/entity_import/
# CRUD-generated wwwroot subdirs (build artifacts, regenerated by build.sh)
wwwroot/entity_list/
wwwroot/entity_import_list/
# Editors / OS
*.swp *.swp
*.swo *.swo
.DS_Store
.idea/
.vscode/

View File

@ -1,35 +1,44 @@
# entity 模块 # entity 模块(W-03 实体管理)
实体管理模块,含实体导入。依赖 `world`、`scene` 模块。 实体管理模块:实体表(entity)增删改查、分页列表查询、实体文件导入(entity_import 记录表)。依赖 world、scene(逻辑关联,不加物理外键)。
## 功能 ## 功能
- 实体表(entity)CRUD:实体名称/编码/所属世界/所属场景/实体类型/状态/属性 JSON
- 实体导入:上传 CSV/JSON 文件 → 解析 → 逐条校验 → 批量落库 → 写导入记录(entity_import) - **实体 CRUD**:create_entity / update_entity / delete_entity / get_entity / list_entities
- **列表查询**:sqlPaging 分页 `{list, total}`,支持 world_id / scene_id / entity_type / status / name / code 过滤 + sort / order
- **文件导入**:api/entity_import.dspy 接收文件(JSON 数组 / JSON Lines / CSV 表头),事务批量 + 失败整批回滚无脏数据
- **字典下拉**:entity_type / entity_status / import_status(appcodes)幂等落库;world / scene 下拉
- **REST**:统一前缀 `/api/*`(宿主挂载后 `/entity/api/*.dspy`),错误结构 `{code,message,field,detail}`,分页 `{list,total}`,非法输入 100% 拦截不落库
## 数据表 ## 数据表
| 表 | 说明 | | 表 | 说明 |
|----|------| |----|------|
| entity | 实体表 | | entity | 实体表:id / world_id / scene_id / name / code(唯一) / entity_type / status / attributes_json / created_at / updated_at |
| entity_import | 实体导入记录表 | | entity_import | 导入记录表(只读):id / world_id / scene_id / file_name / total / success / fail / status / created_at |
## 目录结构 - `entity.scene_id → scene.id`、`entity.world_id → world.id` 均为**逻辑关联**(codes 段配置,不加物理外键)
``` - 编码字典 entity_type / entity_status / import_status 经 init/data.json 幂等落库到 appbase
entity/
├── entity/ # Python 包(init.py 定义 load_entity 及业务函数) ## 集成
├── models/ # 表定义(entity.json / entity_import.json)
├── json/ # CRUD 定义(entity.json / entity_import.json) 1. `load_entity()` 挂载:在宿主应用 `init()` 中调用(函数注册到 ServerEnv,含复数别名)
├── wwwroot/ # 前端(index.ui / import_page.ui / api/*.dspy) 2. wwwroot 符号链接到宿主 wwwroot/entity/
├── init/data.json # appcodes 字典种子(entity_type / entity_status / import_status) 3. 运行 `scripts/load_path.py` 注册 RBAC(或同步中央 load_path.py)
├── scripts/load_path.py # RBAC 权限注册 4. 构建:`json2ddl mysql . > mysql.ddl.sql`(models/)、`xls2ui -m ../models -o ../wwwroot entity *.json`(json/)
└── skill/SKILL.md # 模块技能文档
## 关键接口(经 load_entity() 注册)
- `list_entities(params)` → `{list, total}`
- `create_entity(ns)` / `update_entity(ns)` / `delete_entity(ns)` / `get_entity(ns)`
- `import_entities({world_id, scene_id, file_name, rows})` → `{success, total, success_count, fail}`
- `parse_entity_file(content, filename)` → `{rows: [...]}`
- `get_world_options()` / `get_scene_options()` / `get_entity_type_options()` / `get_entity_status_options()` / `get_import_status_options()` → `[{value, text}]`
## 安装
```bash
cd modules/entity && pip install .
``` ```
## 集成方式 详见 skill/SKILL.md。
应用入口 `app/{应用名}.py` 中 `from entity.init import load_entity`,并在 `init()` 里调用
`load_entity()`。库名由宿主应用的 `get_module_dbname('entity')` 决定,模块内不硬编码。
## 关键接口
- `load_entity()`:挂载模块函数到 ServerEnv
- `wwwroot/api/entity_import.dspy`:实体导入(world_id + scene_id + file)
- `wwwroot/api/entity_create.dspy` / `entity_update.dspy` / `entity_delete.dspy`:实体 CRUD
- `wwwroot/api/get_search_*.dspy`:下拉数据源

View File

@ -5,6 +5,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "entity" name = "entity"
version = "1.0.0" version = "1.0.0"
description = "实体管理模块(W-03)——实体表 CRUD、列表查询、实体文件导入(entity_import 记录)"
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = ["sqlor", "bricks_for_python"] dependencies = ["sqlor", "bricks_for_python"]

View File

@ -1,63 +1,61 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""entity 模块 RBAC 权限注册脚本(显式路径,禁止通配符)。""" """entity 模块 RBAC 权限注册(scripts/load_path.py)。
每次代码变更如有新 path 出现,需同步更新本脚本。禁止通配符,路径全显式。
"""
import os import os
import sys import sys
MODULE = 'entity' HERE = os.path.dirname(os.path.abspath(__file__))
PATHS_ANY = []
# 显式路径清单(禁止 % / * 通配符)
PATHS_LOGINED = [ PATHS_LOGINED = [
'/%s' % MODULE, # 页面
'/%s/index.ui' % MODULE, '/entity/index.ui',
'/%s/import_page.ui' % MODULE, '/entity/import_page.ui',
'/%s/entity' % MODULE, # CRUD 列表(xls2ui 生成的别名目录 5 件套)
'/%s/entity/index.ui' % MODULE, '/entity/entity_list',
'/%s/entity/get_entity.dspy' % MODULE, '/entity/entity_list/index.ui',
'/%s/entity/add_entity.dspy' % MODULE, '/entity/entity_list/get_entity_list.dspy',
'/%s/entity/update_entity.dspy' % MODULE, '/entity/entity_list/add_entity_list.dspy',
'/%s/entity/delete_entity.dspy' % MODULE, '/entity/entity_list/update_entity_list.dspy',
'/%s/entity_import' % MODULE, '/entity/entity_list/delete_entity_list.dspy',
'/%s/entity_import/index.ui' % MODULE, '/entity/entity_import_list',
'/%s/entity_import/get_entity_import.dspy' % MODULE, '/entity/entity_import_list/index.ui',
'/%s/api/entity_import.dspy' % MODULE, '/entity/entity_import_list/get_entity_import_list.dspy',
'/%s/api/entity_create.dspy' % MODULE, '/entity/entity_import_list/add_entity_import_list.dspy',
'/%s/api/entity_update.dspy' % MODULE, '/entity/entity_import_list/update_entity_import_list.dspy',
'/%s/api/entity_delete.dspy' % MODULE, '/entity/entity_import_list/delete_entity_import_list.dspy',
'/%s/api/entity_import_guard.dspy' % MODULE, # REST API(统一 /api/* 前缀)
'/%s/api/get_search_world_id.dspy' % MODULE, '/entity/api/entity_list.dspy',
'/%s/api/get_search_scene_id.dspy' % MODULE, '/entity/api/entity_get.dspy',
'/%s/api/get_search_entity_type.dspy' % MODULE, '/entity/api/entity_create.dspy',
'/%s/api/get_search_status.dspy' % MODULE, '/entity/api/entity_update.dspy',
'/entity/api/entity_delete.dspy',
'/entity/api/entity_import.dspy',
'/entity/api/get_search_world_id.dspy',
'/entity/api/get_search_scene_id.dspy',
'/entity/api/get_search_entity_type.dspy',
'/entity/api/get_search_status.dspy',
'/entity/api/get_search_import_status.dspy',
] ]
PATHS_ANY = []
def find_sage_root():
candidates = [
os.path.expanduser('~/repos/sage'),
os.path.expanduser('~/sage'),
]
for c in candidates:
if os.path.isdir(os.path.join(c, 'wwwroot')) and os.path.isdir(os.path.join(c, 'py3', 'bin')):
return c
return None
def main(): def main():
sage_root = find_sage_root() sys.path.insert(0, HERE)
if not sage_root: try:
print('Sage root not found, skip entity RBAC registration') from set_role_perm import set_role_perm
except Exception:
# 找不到 set_role_perm 时回退到中央 sage/load_path.py 已注册的路径,仅提示
print('WARN: set_role_perm 不可用,请确保中央 load_path.py 已包含上述路径')
return return
sys.path.insert(0, sage_root) for path in PATHS_LOGINED:
from set_role_perm import set_role_perm # noqa: E402 set_role_perm(path, 'logined')
for path in PATHS_ANY:
for p in PATHS_ANY: set_role_perm(path, 'any')
set_role_perm(p, 'any') print(f'entity: registered {len(PATHS_LOGINED)} logined + {len(PATHS_ANY)} any paths')
for p in PATHS_LOGINED:
set_role_perm(p, 'logined')
print('entity RBAC registered: %d any + %d logined' % (len(PATHS_ANY), len(PATHS_LOGINED)))
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,29 +1,75 @@
--- ---
name: entity name: entity
description: entity 模块技能文档——实体管理(实体表 CRUD + 实体导入 entity_import + api/entity_import.dspy),通过 load_entity() 挂载。 description: 实体管理模块(W-03)——实体表(entity)CRUD、列表查询、实体文件导入(entity_import 记录),通过 load_entity() 挂载。
--- ---
# entity 模块 # entity 模块
## 概述 ## 概述
实体管理模块,提供实体表(entity)增删改查与文件导入(entity_import 记录表)。依赖 world、scene 模块。 实体管理模块,提供实体表(entity)增删改查与文件导入(entity_import 记录表)。依赖 world、scene 模块(逻辑关联,不加物理外键)。
## 数据模型 ## 数据模型
- `entity`:id / world_id(→world.id)/ scene_id(→scene.id)/ name / code / entity_type(appcodes entity_type)/ status(appcodes entity_status)/ attributes_json(text) / created_at / updated_at。唯一索引 code。
- `entity_import`:id / world_id / scene_id / file_name / total / success / fail / status(appcodes import_status)/ created_at。只读记录。
## 关键接口 ### 表 `entity`(models/entity.json)
- `load_entity()`:注册 create_entity/update_entity/delete_entity/entity_import 到 ServerEnv(含复数别名)。 | 字段 | 类型 | 说明 |
- `wwwroot/api/entity_import.dspy`:文件导入 → 解析(JSON 数组或 CSV 表头)→ 逐条校验 name/code → sor.C('entity') → 统计 → sor.C('entity_import')。返回 `{success,total,success_count,fail,file_name}`。 |------|------|------|
- `wwwroot/api/entity_{create,update,delete}.dspy`:实体 CRUD(委托 init.py 函数)。 | id | str(32) | 主键 |
- `wwwroot/api/get_search_{world_id,scene_id,entity_type,status}.dspy`:下拉数据源。 | world_id | str(32) | 所属世界(→world.id 逻辑关联) |
| scene_id | str(32) | 所属场景(→scene.id 逻辑关联,codes 段配置,不加物理外键) |
| name | str(255) | 实体名称 |
| code | str(64) | 实体编码(唯一,idx_entity_code) |
| entity_type | str(16) | 实体类型(appcodes entity_type) |
| status | str(16) | 状态(appcodes entity_status) |
| attributes_json | text | 属性 JSON |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
- 主键 `["id"]`;唯一索引 `idx_entity_code(code)`;索引 `idx_entity_world(world_id)`、`idx_entity_scene(scene_id)`
- codes:world_id → world(id/name)、scene_id → scene(id/name)、entity_type → appcodes_kv(parentid='entity_type')、status → appcodes_kv(parentid='entity_status')
### 表 `entity_import`(models/entity_import.json,只读)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | str(32) | 主键 |
| world_id | str(32) | 目标世界 |
| scene_id | str(32) | 目标场景 |
| file_name | str(255) | 导入文件名 |
| total / success / fail | int | 总数 / 成功 / 失败(默认 0) |
| status | str(16) | 导入状态(appcodes import_status:0 进行中/1 成功/2 失败) |
| created_at | timestamp | 导入时间 |
- 主键 `["id"]`;索引 `idx_entity_import_world(world_id)`;**只读记录,不可手工增删改**
## 关键接口(load_entity() 挂载到 ServerEnv)
- `list_entities(params)` → `{list, total}`(sqlPaging 分页,支持 world_id/scene_id/entity_type/status/name/code 过滤 + sort/order)
- `get_entity({id})` → `{data}` 或错误
- `create_entity(ns)` → 校验 + world/scene 存在校验 + 自动 id/created_at/updated_at;返回 `{success, id}`
- `update_entity(ns)` → 写 updated_at、剔除 `_text` 后缀、world/scene 校验
- `delete_entity({id})` → `{success, id}`
- `import_entities({world_id, scene_id, file_name, rows})` → `{success, total, success_count, fail, file_name}`
(**事务批量**:先全量解析+校验(含文件内编码查重),再逐条插入;任一条失败整批回滚,无脏数据;success_count 避免与布尔 success 冲突)
- `parse_entity_file(content, filename)` → `{rows: [...]}`(JSON 数组 / JSON Lines / CSV 表头)
- `get_world_options()` / `get_scene_options()` / `get_entity_type_options()` / `get_entity_status_options()` / `get_import_status_options()` → `[{value, text}]`
### wwwroot/api/*.dspy(REST 统一前缀 /api/*,宿主挂载后为 /entity/api/*.dspy)
- `entity_list.dspy`(分页 `{list,total}`)、`entity_get.dspy`、`entity_create.dspy`、`entity_update.dspy`、`entity_delete.dspy`
- `entity_import.dspy`(world_id + file → parse_entity_file → import_entities)
- `get_search_world_id.dspy` / `get_search_scene_id.dspy` / `get_search_entity_type.dspy` / `get_search_status.dspy` / `get_search_import_status.dspy`(字典下拉)
### 错误结构(统一)
`{code, message, field, detail}`;code:PARAM_REQUIRED / FIELD_REQUIRED / FIELD_TOO_LONG /
WORLD_NOT_FOUND / SCENE_NOT_FOUND / DUPLICATE_CODE / PARSE_ERROR / INVALID_JSON / DB_ERROR / NOT_FOUND。
非法输入 100% 拦截不落库。
## 陷阱 ## 陷阱
- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname('entity')`,.dspy 用 `get_module_dbname('entity')` / `get_sor_context(request._run_ns, 'entity')`。 - 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname("entity")`,.dspy 用 `get_module_dbname("entity")`(全局);world/scene 表分别在 world/scene 模块库(`get_module_dbname("world"/"scene")`),appcodes 在 `get_module_dbname("appbase")`
- dspy 内禁止 import(仅 `from sqlor.filter import DBFilter` 例外),业务逻辑放 `entity/init.py` 并通过 `load_entity()` 导出。 - dspy 无 import(json/get_sor_context/debug/params_kw 均预加载全局);函数经 load_entity() 注册后为全局,直接调用
- `sor.C()` 不会自动填 created_at,必须显式 `ns['created_at'] = curDateString()`。 - `create_entity` / `import_entities` 必须显式设置 `created_at = curDateString()`,否则 sor.C 静默丢记录
- 实体导入记录(entity_import)只读,不可手工增删改。 - `entity_import` 记录 total/success/fail 必须为 int
- `import_entities` 返回用 `success_count`(避免与布尔 `success` 键冲突)
- 三处同步注册:entity/__init__.py 导出 ← entity/init.py 实现 ← load_entity() 注册(含复数别名 create_entities/update_entities/delete_entities)
- 返回 `{list,total}` 分页用 `sor.sqlPaging`,不要硬编码 LIMIT/OFFSET
- CRUD json `new_data_url` 指向自定义 `api/entity_create.dspy`;entity_import 为只读列表,无 editable
## 依赖 ## 依赖
- world、scene(下拉数据源 world / scene 表) - world(world_id 逻辑关联 + 世界下拉)、scene(scene_id 逻辑关联 + 场景下拉)、appbase(appcodes 字典)、rbac(权限)
- 基础:sqlor、ahserver、bricks、appbase(appcodes)

View File

@ -1,20 +1,21 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"}, "options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [ "subwidgets": [
{"widgettype": "Text", "options": {"label": "实体导入", "fontSize": "24px"}}, {"widgettype": "Text", "options": {"label": "实体文件导入", "fontSize": "20px"}},
{"widgettype": "Text", "options": {"label": "支持 CSV(首行表头)或 JSON 数组,字段:name、code、entity_type、status、attributes_json"}}, {"widgettype": "Text", "options": {"label": "支持 JSON 数组 / JSON Lines / CSV(表头 name,code,world_id,scene_id,entity_type,status,attributes_json)。事务批量:任一条校验失败整批回滚,无脏数据。"}},
{"widgettype": "Form", "options": { {"widgettype": "Form",
"title": "导入实体数据", "options": {
"submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}", "width": "100%",
"method": "POST", "submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}",
"fields": [ "method": "POST",
{"name": "world_id", "label": "目标世界", "uitype": "code", "required": true, "fields": [
"dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"}, {"name": "world_id", "label": "目标世界ID", "uitype": "code", "dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"},
{"name": "scene_id", "label": "目标场景", "uitype": "code", {"name": "scene_id", "label": "目标场景ID", "uitype": "code", "dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"},
"dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"}, {"name": "file_name", "label": "文件名", "uitype": "text"},
{"name": "file", "label": "导入文件", "uitype": "file", "required": true} {"name": "file", "label": "导入文件", "uitype": "file", "required": true}
] ],
}} "buttons": [{"label": "开始导入", "type": "submit"}]
] }}
]
} }

View File

@ -1,22 +1,19 @@
{ {
"widgettype": "VBox", "widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"}, "options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [ "subwidgets": [
{"widgettype": "Text", "options": {"label": "实体管理", "fontSize": "24px"}}, {"widgettype": "Text", "options": {"label": "实体管理(W-03)", "fontSize": "24px"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [ {"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"}, "subwidgets": [
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"}, {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/entity_list')}}"}}],
"options": {"url": "{{entire_url('/entity/entity/index.ui')}}"}, "mode": "replace"}], "subwidgets": [{"widgettype": "Text", "options": {"label": "实体列表"}}]},
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体列表"}}]}, {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"}, "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/import_page.ui')}}"}}],
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "subwidgets": [{"widgettype": "Text", "options": {"label": "实体导入"}}]},
"options": {"url": "{{entire_url('/entity/import_page.ui')}}"}, "mode": "replace"}], {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
"subwidgets": [{"widgettype": "Text", "options": {"label": "实体导入"}}]}, "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", "mode": "replace", "options": {"url": "{{entire_url('/entity/entity_import_list')}}"}}],
{"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"}, "subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]}
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", ]},
"options": {"url": "{{entire_url('/entity/entity_import/index.ui')}}"}, "mode": "replace"}], {"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
"subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]} ]
]},
{"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
} }