fix: 回填目标机现场好代码——远端main残缺/空导致部署后模块不可用
This commit is contained in:
parent
c40e2ce8a0
commit
eebd79d9d7
11
.gitignore
vendored
11
.gitignore
vendored
@ -1 +1,10 @@
|
||||
忽略生成物/CRUD 目录
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
models/mysql.ddl.sql
|
||||
wwwroot/entity/
|
||||
wwwroot/entity_import/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
36
README.md
36
README.md
@ -1 +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`:下拉数据源
|
||||
|
||||
@ -1 +1,6 @@
|
||||
仓库根标记(双层结构说明)
|
||||
"""entity 模块:实体管理,含实体导入。"""
|
||||
|
||||
from entity.init import load_entity # noqa: F401
|
||||
|
||||
__all__ = ['load_entity']
|
||||
__version__ = '1.0.0'
|
||||
|
||||
@ -1 +0,0 @@
|
||||
i18n 提取说明
|
||||
@ -1 +0,0 @@
|
||||
工作日志(范围/决策/验证/遗留)
|
||||
@ -1 +1,17 @@
|
||||
包导出(防 dspy NameError)
|
||||
"""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',
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
176
entity/init.py
176
entity/init.py
@ -1 +1,175 @@
|
||||
后端实现:create/update/delete/get/list_entities/import_entities/entity_import_guard/reject_import_write + load_entity() 注册;_err() 统一错误结构;两阶段导入+回滚
|
||||
"""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.serverenv 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.serverenv 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
|
||||
|
||||
@ -1 +1,32 @@
|
||||
entity_type/common_status/import_status 字典种子(Format B)
|
||||
{
|
||||
"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": "失败"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +1,33 @@
|
||||
实体 CRUD 定义(editable + data_filter + alters 下拉)
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,16 @@
|
||||
导入记录只读 CRUD 定义
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,33 @@
|
||||
实体表四段式定义(codes 逻辑关联 scene/world,entity_code 唯一)
|
||||
{
|
||||
"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'"}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +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'"}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +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'"}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +1,13 @@
|
||||
打包配置 name=entity
|
||||
[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*"]
|
||||
|
||||
Binary file not shown.
@ -1 +1,64 @@
|
||||
RBAC 显式路径注册(无通配符)
|
||||
#!/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()
|
||||
|
||||
@ -1 +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)
|
||||
|
||||
@ -1 +1,2 @@
|
||||
新增
|
||||
result = await create_entity(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1 +1,2 @@
|
||||
删除
|
||||
result = await delete_entity(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1 +0,0 @@
|
||||
详情
|
||||
@ -1 +1,3 @@
|
||||
文件导入(两阶段+回滚)
|
||||
debug('entity_import.dspy: START')
|
||||
result = await entity_import(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1 +1 @@
|
||||
导入预校验(干跑)
|
||||
return {'success': False, 'message': '导入记录为只读,不支持手工增删改'}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
导入记录只读分页
|
||||
@ -1 +0,0 @@
|
||||
导入记录禁手工增删改
|
||||
@ -1 +0,0 @@
|
||||
分页列表 {list,total}
|
||||
@ -1 +1,2 @@
|
||||
更新
|
||||
result = await update_entity(request, params_kw)
|
||||
return result
|
||||
|
||||
@ -1 +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)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
导入状态下拉
|
||||
@ -1 +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)
|
||||
|
||||
@ -1 +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)
|
||||
|
||||
@ -1 +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)
|
||||
|
||||
@ -1 +0,0 @@
|
||||
英文 i18n(全部新文案)
|
||||
@ -1 +0,0 @@
|
||||
简体中文 i18n(全部新文案)
|
||||
@ -1 +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}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +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"}}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user