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