From ec6ddb284fa63376bd9245d0b01a224be59606a1 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Fri, 28 Aug 2026 13:34:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(entity):=20entity=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=8F=90=E4=BA=A4=EF=BC=88=E5=85=83=E6=99=AF=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE-=E5=88=9D=E5=A7=8B=E8=BF=AD=E4=BB=A3=EF=BC=8C?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E4=BA=A7=E7=BA=BF=E4=BA=A7=E5=87=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 ++ README.md | 35 +++++ __init__.py | 6 + entity/__init__.py | 17 +++ entity/init.py | 175 ++++++++++++++++++++++++ init/data.json | 32 +++++ json/entity.json | 33 +++++ json/entity_import.json | 16 +++ models/entity.json | 33 +++++ models/entity_import.json | 29 ++++ models/entity_import_log.json | 20 +++ models/entity_type.json | 1 + pyproject.toml | 13 ++ scripts/load_path.py | 64 +++++++++ skill/SKILL.md | 29 ++++ wwwroot/api/entity_create.dspy | 2 + wwwroot/api/entity_delete.dspy | 2 + wwwroot/api/entity_import.dspy | 3 + wwwroot/api/entity_import_guard.dspy | 1 + wwwroot/api/entity_update.dspy | 2 + wwwroot/api/get_search_entity_type.dspy | 8 ++ wwwroot/api/get_search_scene_id.dspy | 8 ++ wwwroot/api/get_search_status.dspy | 8 ++ wwwroot/api/get_search_world_id.dspy | 8 ++ wwwroot/import_page.ui | 20 +++ wwwroot/index.ui | 22 +++ 26 files changed, 597 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 entity/__init__.py create mode 100644 entity/init.py create mode 100644 init/data.json create mode 100644 json/entity.json create mode 100644 json/entity_import.json create mode 100644 models/entity.json create mode 100644 models/entity_import.json create mode 100644 models/entity_import_log.json create mode 100644 models/entity_type.json create mode 100644 pyproject.toml create mode 100644 scripts/load_path.py create mode 100644 skill/SKILL.md create mode 100644 wwwroot/api/entity_create.dspy create mode 100644 wwwroot/api/entity_delete.dspy create mode 100644 wwwroot/api/entity_import.dspy create mode 100644 wwwroot/api/entity_import_guard.dspy create mode 100644 wwwroot/api/entity_update.dspy create mode 100644 wwwroot/api/get_search_entity_type.dspy create mode 100644 wwwroot/api/get_search_scene_id.dspy create mode 100644 wwwroot/api/get_search_status.dspy create mode 100644 wwwroot/api/get_search_world_id.dspy create mode 100644 wwwroot/import_page.ui create mode 100644 wwwroot/index.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6254bac --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +models/mysql.ddl.sql +wwwroot/entity/ +wwwroot/entity_import/ +*.swp +*.swo diff --git a/README.md b/README.md new file mode 100644 index 0000000..53bdf8f --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# entity 模块 + +实体管理模块,含实体导入。依赖 `world`、`scene` 模块。 + +## 功能 +- 实体表(entity)CRUD:实体名称/编码/所属世界/所属场景/实体类型/状态/属性 JSON +- 实体导入:上传 CSV/JSON 文件 → 解析 → 逐条校验 → 批量落库 → 写导入记录(entity_import) + +## 数据表 +| 表 | 说明 | +|----|------| +| entity | 实体表 | +| entity_import | 实体导入记录表 | + +## 目录结构 +``` +entity/ +├── entity/ # Python 包(init.py 定义 load_entity 及业务函数) +├── models/ # 表定义(entity.json / entity_import.json) +├── json/ # CRUD 定义(entity.json / entity_import.json) +├── wwwroot/ # 前端(index.ui / import_page.ui / api/*.dspy) +├── init/data.json # appcodes 字典种子(entity_type / entity_status / import_status) +├── scripts/load_path.py # RBAC 权限注册 +└── 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`:下拉数据源 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..4e3446f --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +"""entity 模块:实体管理,含实体导入。""" + +from entity.init import load_entity # noqa: F401 + +__all__ = ['load_entity'] +__version__ = '1.0.0' diff --git a/entity/__init__.py b/entity/__init__.py new file mode 100644 index 0000000..530e674 --- /dev/null +++ b/entity/__init__.py @@ -0,0 +1,17 @@ +"""entity 模块包。""" + +from .init import ( # noqa: F401 + create_entity, + delete_entity, + entity_import, + load_entity, + update_entity, +) + +__all__ = [ + 'load_entity', + 'create_entity', + 'update_entity', + 'delete_entity', + 'entity_import', +] diff --git a/entity/init.py b/entity/init.py new file mode 100644 index 0000000..d0ae02b --- /dev/null +++ b/entity/init.py @@ -0,0 +1,175 @@ +"""entity 模块:实体管理 + 实体导入。 + +通过 load_entity() 挂载到 ServerEnv,函数被 .dspy 以全局方式直接调用。 +""" + +import csv +import io +import json + +from appPublic.timeUtils import curDateString, timestampstr +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + + +def _get_dbname(): + """取库名(由宿主应用决定,禁止硬编码)。""" + from ahserver import ServerEnv + return ServerEnv().get_module_dbname('entity') + + +def _clean_ns(ns): + """清理 Tabular/Form 提交的 _text 后缀字段与占位值。""" + for k in list(ns.keys()): + if k.endswith('_text'): + ns.pop(k, None) + elif ns[k] in ('NaN', 'null', ''): + ns[k] = None + return ns + + +async def create_entity(request, params_kw): + dbname = _get_dbname() + ns = _clean_ns(dict(params_kw or {})) + if not ns.get('id'): + ns['id'] = getID() + ns['created_at'] = curDateString() + ns['updated_at'] = timestampstr() + ns.setdefault('entity_type', '0') + ns.setdefault('status', '0') + db = DBPools() + async with db.sqlorContext(dbname) as sor: + await sor.C('entity', ns) + return {'success': True, 'message': '保存成功'} + + +async def update_entity(request, params_kw): + dbname = _get_dbname() + ns = _clean_ns(dict(params_kw or {})) + eid = ns.pop('id', None) + if not eid: + return {'success': False, 'message': '缺少主键 id'} + ns['updated_at'] = timestampstr() + db = DBPools() + async with db.sqlorContext(dbname) as sor: + await sor.U('entity', {'id': eid, **ns}) + return {'success': True, 'message': '更新成功'} + + +async def delete_entity(request, params_kw): + dbname = _get_dbname() + eid = (params_kw or {}).get('id') + if not eid: + return {'success': False, 'message': '缺少主键 id'} + db = DBPools() + async with db.sqlorContext(dbname) as sor: + await sor.D('entity', {'id': eid}) + return {'success': True, 'message': '删除成功'} + + +def _parse_import_content(content): + """解析导入内容,返回记录列表。支持 JSON 数组/对象 或 CSV(首行表头)。""" + text = (content or '').strip() + if not text: + return [] + if text.startswith('[') or text.startswith('{'): + data = json.loads(text) + return data if isinstance(data, list) else [data] + reader = csv.DictReader(io.StringIO(text)) + return [dict(r) for r in reader] + + +async def entity_import(request, params_kw): + dbname = _get_dbname() + p = params_kw or {} + world_id = p.get('world_id', '') + scene_id = p.get('scene_id') or None + file_obj = p.get('file') + + if not world_id: + return {'success': False, 'message': '缺少目标世界 world_id'} + + file_name = '' + content = '' + if file_obj is not None: + file_name = getattr(file_obj, 'filename', '') or 'upload' + try: + raw = file_obj.read() + content = raw.decode('utf-8') if isinstance(raw, bytes) else raw + except Exception as e: + return {'success': False, 'message': '读取文件失败: %s' % str(e)} + elif p.get('file_content'): + content = p.get('file_content') + file_name = p.get('file_name', 'import') + + try: + rows = _parse_import_content(content) + except Exception as e: + return {'success': False, 'message': '文件解析失败: %s' % str(e)} + + if not rows: + return {'success': False, 'message': '文件内容为空'} + + total = len(rows) + success = 0 + fail = 0 + + db = DBPools() + async with db.sqlorContext(dbname) as sor: + for row in rows: + try: + name = str(row.get('name') or '').strip() + code = str(row.get('code') or '').strip() + if not name or not code: + fail += 1 + continue + ns = { + 'id': getID(), + 'world_id': world_id, + 'scene_id': scene_id, + 'name': name, + 'code': code, + 'entity_type': str(row.get('entity_type') or '0'), + 'status': str(row.get('status') or '0'), + 'attributes_json': row.get('attributes_json') or None, + 'created_at': curDateString(), + 'updated_at': timestampstr(), + } + await sor.C('entity', ns) + success += 1 + except Exception: + fail += 1 + + status = '2' if fail == 0 else ('3' if success == 0 else '2') + import_ns = { + 'id': getID(), + 'world_id': world_id, + 'scene_id': scene_id, + 'file_name': file_name, + 'total': total, + 'success': success, + 'fail': fail, + 'status': status, + 'created_at': curDateString(), + } + await sor.C('entity_import', import_ns) + + return { + 'success': True, + 'total': total, + 'success_count': success, + 'fail': fail, + 'file_name': file_name, + } + + +async def load_entity(): + from ahserver import ServerEnv + env = ServerEnv() + env.create_entity = create_entity + env.create_entitys = create_entity + env.update_entity = update_entity + env.update_entitys = update_entity + env.delete_entity = delete_entity + env.delete_entitys = delete_entity + env.entity_import = entity_import diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..9a97353 --- /dev/null +++ b/init/data.json @@ -0,0 +1,32 @@ +{ + "appcodes": [ + { + "parentid": "entity_type", + "parentname": "实体类型", + "items": [ + {"k": "0", "v": "普通实体"}, + {"k": "1", "v": "NPC"}, + {"k": "2", "v": "物品"}, + {"k": "3", "v": "建筑"} + ] + }, + { + "parentid": "entity_status", + "parentname": "实体状态", + "items": [ + {"k": "0", "v": "启用"}, + {"k": "1", "v": "停用"} + ] + }, + { + "parentid": "import_status", + "parentname": "导入状态", + "items": [ + {"k": "0", "v": "待处理"}, + {"k": "1", "v": "处理中"}, + {"k": "2", "v": "完成"}, + {"k": "3", "v": "失败"} + ] + } + ] +} diff --git a/json/entity.json b/json/entity.json new file mode 100644 index 0000000..b8409e9 --- /dev/null +++ b/json/entity.json @@ -0,0 +1,33 @@ +{ + "tblname": "entity", + "title": "实体管理", + "params": { + "sortby": ["created_at desc"], + "new_data_url": "{{entire_url('../api/entity_create.dspy')}}", + "update_data_url": "{{entire_url('../api/entity_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/entity_delete.dspy')}}", + "browserfields": { + "exclouded": ["attributes_json", "updated_at"], + "alters": { + "world_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_world_id.dspy')}}"}, + "scene_id": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_scene_id.dspy')}}"}, + "entity_type": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_entity_type.dspy')}}"}, + "status": {"uitype": "code", "dataurl": "{{entire_url('../api/get_search_status.dspy')}}"} + } + }, + "editexclouded": ["id", "created_at", "updated_at"], + "data_filter": { + "AND": [ + {"field": "name", "op": "LIKE", "var": "name"}, + {"field": "world_id", "op": "=", "var": "world_id"}, + {"field": "scene_id", "op": "=", "var": "scene_id"} + ] + }, + "filter_labels": {"name": "实体名称", "world_id": "所属世界", "scene_id": "所属场景"}, + "editable": { + "new_data_url": "{{entire_url('../api/entity_create.dspy')}}", + "update_data_url": "{{entire_url('../api/entity_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/entity_delete.dspy')}}" + } + } +} diff --git a/json/entity_import.json b/json/entity_import.json new file mode 100644 index 0000000..b14d094 --- /dev/null +++ b/json/entity_import.json @@ -0,0 +1,16 @@ +{ + "tblname": "entity_import", + "title": "实体导入记录", + "params": { + "sortby": ["created_at desc"], + "browserfields": { + "exclouded": [] + }, + "editexclouded": ["id", "world_id", "scene_id", "file_name", "total", "success", "fail", "status", "created_at"], + "editable": { + "new_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}", + "update_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}", + "delete_data_url": "{{entire_url('../api/entity_import_guard.dspy')}}" + } + } +} diff --git a/models/entity.json b/models/entity.json new file mode 100644 index 0000000..a504777 --- /dev/null +++ b/models/entity.json @@ -0,0 +1,33 @@ +{ + "summary": [ + { + "name": "entity", + "title": "实体表", + "primary": ["id"], + "catelog": "entity" + } + ], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界", "type": "str", "length": 32, "nullable": "no"}, + {"name": "scene_id", "title": "所属场景", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "name", "title": "实体名称", "type": "str", "length": 255, "nullable": "no"}, + {"name": "code", "title": "实体编码", "type": "str", "length": 64, "nullable": "no"}, + {"name": "entity_type", "title": "实体类型", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "attributes_json", "title": "实体属性JSON", "type": "text", "nullable": "yes"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"} + ], + "indexes": [ + {"name": "idx_entity_code", "idxtype": "unique", "idxfields": ["code"]}, + {"name": "idx_entity_world", "idxtype": "index", "idxfields": ["world_id"]}, + {"name": "idx_entity_scene", "idxtype": "index", "idxfields": ["scene_id"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"}, + {"field": "entity_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='entity_type'"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='entity_status'"} + ] +} diff --git a/models/entity_import.json b/models/entity_import.json new file mode 100644 index 0000000..d5f0fdb --- /dev/null +++ b/models/entity_import.json @@ -0,0 +1,29 @@ +{ + "summary": [ + { + "name": "entity_import", + "title": "实体导入记录表", + "primary": ["id"], + "catelog": "entity" + } + ], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "目标世界", "type": "str", "length": 32, "nullable": "no"}, + {"name": "scene_id", "title": "目标场景", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"}, + {"name": "total", "title": "总条数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "success", "title": "成功条数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "fail", "title": "失败条数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "0"}, + {"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [ + {"name": "idx_entity_import_world", "idxtype": "index", "idxfields": ["world_id"]} + ], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "scene_id", "table": "scene", "valuefield": "id", "textfield": "name"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"} + ] +} diff --git a/models/entity_import_log.json b/models/entity_import_log.json new file mode 100644 index 0000000..5b427f0 --- /dev/null +++ b/models/entity_import_log.json @@ -0,0 +1,20 @@ +{ + "summary": [{"name": "entity_import_log", "title": "实体导入日志表", "primary": ["id"], "catelog": "relation"}], + "fields": [ + {"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "world_id", "title": "所属世界ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "file_name", "title": "导入文件名", "type": "str", "length": 255, "nullable": "no"}, + {"name": "total", "title": "总记录数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "success", "title": "成功数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "failed", "title": "失败数", "type": "int", "nullable": "no", "default": "0"}, + {"name": "status", "title": "导入状态", "type": "str", "length": 16, "nullable": "no", "default": "processing"}, + {"name": "detail", "title": "失败明细", "type": "text", "nullable": "yes"}, + {"name": "created_by", "title": "导入人ID", "type": "str", "length": 32, "nullable": "yes"}, + {"name": "created_at", "title": "导入时间", "type": "timestamp", "nullable": "no"} + ], + "indexes": [{"name": "idx_entity_ilog_world", "idxtype": "index", "idxfields": ["world_id"]}], + "codes": [ + {"field": "world_id", "table": "world", "valuefield": "id", "textfield": "name"}, + {"field": "status", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='import_status'"} + ] +} diff --git a/models/entity_type.json b/models/entity_type.json new file mode 100644 index 0000000..9780e7e --- /dev/null +++ b/models/entity_type.json @@ -0,0 +1 @@ +entity_type 表定义。 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ce6735a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "entity" +version = "1.0.0" +requires-python = ">=3.8" +dependencies = ["sqlor", "bricks_for_python"] + +[tool.setuptools.packages.find] +where = ["."] +include = ["entity*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..1201a9c --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""entity 模块 RBAC 权限注册脚本(显式路径,禁止通配符)。""" + +import os +import sys + +MODULE = 'entity' + +PATHS_ANY = [] + +PATHS_LOGINED = [ + '/%s' % MODULE, + '/%s/index.ui' % MODULE, + '/%s/import_page.ui' % MODULE, + '/%s/entity' % MODULE, + '/%s/entity/index.ui' % MODULE, + '/%s/entity/get_entity.dspy' % MODULE, + '/%s/entity/add_entity.dspy' % MODULE, + '/%s/entity/update_entity.dspy' % MODULE, + '/%s/entity/delete_entity.dspy' % MODULE, + '/%s/entity_import' % MODULE, + '/%s/entity_import/index.ui' % MODULE, + '/%s/entity_import/get_entity_import.dspy' % MODULE, + '/%s/api/entity_import.dspy' % MODULE, + '/%s/api/entity_create.dspy' % MODULE, + '/%s/api/entity_update.dspy' % MODULE, + '/%s/api/entity_delete.dspy' % MODULE, + '/%s/api/entity_import_guard.dspy' % MODULE, + '/%s/api/get_search_world_id.dspy' % MODULE, + '/%s/api/get_search_scene_id.dspy' % MODULE, + '/%s/api/get_search_entity_type.dspy' % MODULE, + '/%s/api/get_search_status.dspy' % MODULE, +] + + +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(): + sage_root = find_sage_root() + if not sage_root: + print('Sage root not found, skip entity RBAC registration') + return + sys.path.insert(0, sage_root) + from set_role_perm import set_role_perm # noqa: E402 + + for p in PATHS_ANY: + set_role_perm(p, 'any') + 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__': + main() diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..e785e57 --- /dev/null +++ b/skill/SKILL.md @@ -0,0 +1,29 @@ +--- +name: entity +description: entity 模块技能文档——实体管理(实体表 CRUD + 实体导入 entity_import + api/entity_import.dspy),通过 load_entity() 挂载。 +--- + +# entity 模块 + +## 概述 +实体管理模块,提供实体表(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。只读记录。 + +## 关键接口 +- `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 函数)。 +- `wwwroot/api/get_search_{world_id,scene_id,entity_type,status}.dspy`:下拉数据源。 + +## 陷阱 +- 库名禁止硬编码:.py 用 `ServerEnv().get_module_dbname('entity')`,.dspy 用 `get_module_dbname('entity')` / `get_sor_context(request._run_ns, 'entity')`。 +- dspy 内禁止 import(仅 `from sqlor.filter import DBFilter` 例外),业务逻辑放 `entity/init.py` 并通过 `load_entity()` 导出。 +- `sor.C()` 不会自动填 created_at,必须显式 `ns['created_at'] = curDateString()`。 +- 实体导入记录(entity_import)只读,不可手工增删改。 + +## 依赖 +- world、scene(下拉数据源 world / scene 表) +- 基础:sqlor、ahserver、bricks、appbase(appcodes) diff --git a/wwwroot/api/entity_create.dspy b/wwwroot/api/entity_create.dspy new file mode 100644 index 0000000..34aeb6e --- /dev/null +++ b/wwwroot/api/entity_create.dspy @@ -0,0 +1,2 @@ +result = await create_entity(request, params_kw) +return result diff --git a/wwwroot/api/entity_delete.dspy b/wwwroot/api/entity_delete.dspy new file mode 100644 index 0000000..7bcd6ba --- /dev/null +++ b/wwwroot/api/entity_delete.dspy @@ -0,0 +1,2 @@ +result = await delete_entity(request, params_kw) +return result diff --git a/wwwroot/api/entity_import.dspy b/wwwroot/api/entity_import.dspy new file mode 100644 index 0000000..9670b0b --- /dev/null +++ b/wwwroot/api/entity_import.dspy @@ -0,0 +1,3 @@ +debug('entity_import.dspy: START') +result = await entity_import(request, params_kw) +return result diff --git a/wwwroot/api/entity_import_guard.dspy b/wwwroot/api/entity_import_guard.dspy new file mode 100644 index 0000000..01f8ff7 --- /dev/null +++ b/wwwroot/api/entity_import_guard.dspy @@ -0,0 +1 @@ +return {'success': False, 'message': '导入记录为只读,不支持手工增删改'} diff --git a/wwwroot/api/entity_update.dspy b/wwwroot/api/entity_update.dspy new file mode 100644 index 0000000..6ff8ad2 --- /dev/null +++ b/wwwroot/api/entity_update.dspy @@ -0,0 +1,2 @@ +result = await update_entity(request, params_kw) +return result diff --git a/wwwroot/api/get_search_entity_type.dspy b/wwwroot/api/get_search_entity_type.dspy new file mode 100644 index 0000000..ec400e4 --- /dev/null +++ b/wwwroot/api/get_search_entity_type.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'entity') as sor: + rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_type' order by k", {}) + return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False) +except Exception as e: + debug('get_search_entity_type error: %s' % str(e)) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_scene_id.dspy b/wwwroot/api/get_search_scene_id.dspy new file mode 100644 index 0000000..a4e79d4 --- /dev/null +++ b/wwwroot/api/get_search_scene_id.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'entity') as sor: + rows = await sor.sqlExe("select id as value, name as text from scene order by name", {}) + return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False) +except Exception as e: + debug('get_search_scene_id error: %s' % str(e)) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy new file mode 100644 index 0000000..f81a7b5 --- /dev/null +++ b/wwwroot/api/get_search_status.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'entity') as sor: + rows = await sor.sqlExe("select k as value, v as text from appcodes_kv where parentid='entity_status' order by k", {}) + return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False) +except Exception as e: + debug('get_search_status error: %s' % str(e)) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_world_id.dspy b/wwwroot/api/get_search_world_id.dspy new file mode 100644 index 0000000..ad91998 --- /dev/null +++ b/wwwroot/api/get_search_world_id.dspy @@ -0,0 +1,8 @@ +result = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'entity') as sor: + rows = await sor.sqlExe("select id as value, name as text from world order by name", {}) + return json.dumps(result + [{'value': r.value, 'text': r.text} for r in rows], ensure_ascii=False) +except Exception as e: + debug('get_search_world_id error: %s' % str(e)) +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/import_page.ui b/wwwroot/import_page.ui new file mode 100644 index 0000000..4c14d2e --- /dev/null +++ b/wwwroot/import_page.ui @@ -0,0 +1,20 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "20px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"label": "实体导入", "fontSize": "24px"}}, + {"widgettype": "Text", "options": {"label": "支持 CSV(首行表头)或 JSON 数组,字段:name、code、entity_type、status、attributes_json"}}, + {"widgettype": "Form", "options": { + "title": "导入实体数据", + "submit_url": "{{entire_url('/entity/api/entity_import.dspy')}}", + "method": "POST", + "fields": [ + {"name": "world_id", "label": "目标世界", "uitype": "code", "required": true, + "dataurl": "{{entire_url('/entity/api/get_search_world_id.dspy')}}"}, + {"name": "scene_id", "label": "目标场景", "uitype": "code", + "dataurl": "{{entire_url('/entity/api/get_search_scene_id.dspy')}}"}, + {"name": "file", "label": "导入文件", "uitype": "file", "required": true} + ] + }} + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..51c1353 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,22 @@ +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "padding": "20px"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"label": "实体管理", "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.entity_content", + "options": {"url": "{{entire_url('/entity/entity/index.ui')}}"}, "mode": "replace"}], + "subwidgets": [{"widgettype": "Text", "options": {"label": "实体列表"}}]}, + {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", + "options": {"url": "{{entire_url('/entity/import_page.ui')}}"}, "mode": "replace"}], + "subwidgets": [{"widgettype": "Text", "options": {"label": "实体导入"}}]}, + {"widgettype": "VBox", "options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"}, + "binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.entity_content", + "options": {"url": "{{entire_url('/entity/entity_import/index.ui')}}"}, "mode": "replace"}], + "subwidgets": [{"widgettype": "Text", "options": {"label": "导入记录"}}]} + ]}, + {"widgettype": "VBox", "id": "app.entity_content", "options": {"width": "100%", "flex": "1", "marginTop": "20px"}} + ] +}