diff --git a/entity/__init__.py b/entity/__init__.py index 530e674..d0e62e9 100644 --- a/entity/__init__.py +++ b/entity/__init__.py @@ -1,17 +1,26 @@ -"""entity 模块包。""" - -from .init import ( # noqa: F401 - create_entity, - delete_entity, - entity_import, +# -*- coding: utf-8 -*- +"""entity 模块包导出——所有 async 函数必须在 __init__.py 导出,否则 dspy 调用报 NameError。""" +from .init import ( + EntityError, load_entity, + list_entities, + get_entity, + create_entity, update_entity, + delete_entity, + import_entities, + parse_entity_file, + get_world_options, + get_scene_options, + get_entity_type_options, + get_entity_status_options, + get_import_status_options, ) __all__ = [ - 'load_entity', - 'create_entity', - 'update_entity', - 'delete_entity', - 'entity_import', + 'EntityError', 'load_entity', + 'list_entities', 'get_entity', 'create_entity', 'update_entity', + 'delete_entity', 'import_entities', 'parse_entity_file', + 'get_world_options', 'get_scene_options', 'get_entity_type_options', + 'get_entity_status_options', 'get_import_status_options', ] diff --git a/entity/init.py b/entity/init.py index 100128f..50630c8 100644 --- a/entity/init.py +++ b/entity/init.py @@ -1,175 +1,505 @@ -"""entity 模块:实体管理 + 实体导入。 +# -*- coding: utf-8 -*- +"""entity 模块后端逻辑——实体 CRUD / 列表查询 / 文件导入(W-03 实体管理)。 -通过 load_entity() 挂载到 ServerEnv,函数被 .dspy 以全局方式直接调用。 +库名禁止硬编码:统一经 ServerEnv().get_module_dbname('entity') 获取; +world/scene/appbase 表分别经 get_module_dbname('world'/'scene'/'appbase') 获取。 """ - -import csv -import io import json -from appPublic.timeUtils import curDateString, timestampstr -from appPublic.uniqueID import getID -from sqlor.dbpools import DBPools +from ahserver import ServerEnv +from appPublic.dbPools import DBPools +from appPublic.futils import getID +from appPublic.timeutils import curDateString + +__all__ = [ + 'EntityError', 'load_entity', + 'list_entities', 'get_entity', 'create_entity', 'update_entity', + 'delete_entity', 'import_entities', 'parse_entity_file', + 'get_world_options', 'get_scene_options', 'get_entity_type_options', + 'get_entity_status_options', 'get_import_status_options', +] + +_FIELDS = ('id', 'world_id', 'scene_id', 'name', 'code', 'entity_type', + 'status', 'attributes_json', 'created_at', 'updated_at') -def _get_dbname(): - """取库名(由宿主应用决定,禁止硬编码)。""" - from ahserver.serverenv import ServerEnv - return ServerEnv().get_module_dbname('entity') +class EntityError(Exception): + """业务校验/处理异常,统一错误结构 {code,message,field,detail}。""" + def __init__(self, code, message, field='', detail=''): + super().__init__(message) + self.code = code + self.message = message + self.field = field + self.detail = detail -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(), + def to_dict(self): + return { + 'code': self.code, + 'message': self.message, + 'field': self.field, + 'detail': self.detail, } - await sor.C('entity_import', import_ns) + +# ---------------------------------------------------------------- 工具 + +def _module_dbname(m): + try: + return ServerEnv().get_module_dbname(m) + except Exception: + return None + + +def _dbname(): + return _module_dbname('entity') + + +def _world_dbname(): + return _module_dbname('world') + + +def _scene_dbname(): + return _module_dbname('scene') + + +def _appbase_dbname(): + return _module_dbname('appbase') + + +def _row_to_dict(r): + if isinstance(r, dict): + return {f: r.get(f) for f in _FIELDS} + return {f: getattr(r, f, None) for f in _FIELDS} + + +def _check_required(ns, field, label, maxlen=None): + val = (ns or {}).get(field) + if val is None or (isinstance(val, str) and not val.strip()): + raise EntityError('FIELD_REQUIRED', f'{label}不能为空', field) + val = str(val).strip() + if maxlen and len(val) > maxlen: + raise EntityError('FIELD_TOO_LONG', f'{label}长度不能超过{maxlen}', field, f'当前长度 {len(val)}') + return val + + +async def _exists(dbname, table, eid): + """逻辑关联存在性校验;关联模块未挂载/表不存在时放行(不阻断)。""" + if not eid or not dbname: + return True + try: + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R(table, {'id': eid}) + return bool(recs) + except Exception: + return True + + +async def _world_exists(world_id): + return await _exists(_world_dbname(), 'world', world_id) + + +async def _scene_exists(scene_id): + return await _exists(_scene_dbname(), 'scene', scene_id) + + +def _check_attributes_json(attrs): + if not attrs: + return '' + if isinstance(attrs, (dict, list)): + return json.dumps(attrs, ensure_ascii=False) + text = str(attrs).strip() + if not text: + return '' + try: + json.loads(text) + except Exception as e: + raise EntityError('INVALID_JSON', '属性JSON格式错误', 'attributes_json', str(e)) + return text + + +def _norm_scalar(v, default='0', maxlen=16): + if v is None: + return default + s = str(v).strip() + if not s: + return default + return s[:maxlen] + + +# ---------------------------------------------------------------- 查询 + +async def list_entities(params): + """分页列表查询,返回 {list, total}。支持 world_id/scene_id/entity_type/status/name/code 过滤 + sort/order。""" + ns = dict(params or {}) + dbname = _dbname() + wheres, args = [], {} + for f in ('world_id', 'scene_id', 'entity_type', 'status'): + v = (ns.get(f) or '').strip() + if v: + wheres.append(f"`{f}` = ${{{f}}}$") + args[f] = v + name = (ns.get('name') or '').strip() + if name: + wheres.append('`name` LIKE ${name}$') + args['name'] = f'%{name}%' + code = (ns.get('code') or '').strip() + if code: + wheres.append('`code` LIKE ${code}$') + args['code'] = f'%{code}%' + where_sql = (' WHERE ' + ' AND '.join(wheres)) if wheres else '' + sort = (ns.get('sort') or 'created_at').strip() + if sort not in ('created_at', 'updated_at', 'name', 'code'): + sort = 'created_at' + order = (ns.get('order') or 'desc').strip().lower() + if order not in ('asc', 'desc'): + order = 'desc' + try: + page = int(ns.get('page') or 1) + rows_n = int(ns.get('rows') or 20) + except Exception: + page, rows_n = 1, 20 + if page < 1: + page = 1 + if rows_n < 1 or rows_n > 200: + rows_n = 20 + sql = f'SELECT * FROM entity{where_sql} ORDER BY `{sort}` {order}' + pns = {'page': page, 'rows': rows_n} + pns.update(args) + async with DBPools().sqlorContext(dbname) as sor: + rec = await sor.sqlPaging(sql, pns) + if isinstance(rec, dict): + total, rows = rec.get('total', 0), rec.get('rows', []) + else: + total, rows = rec.total, rec.rows + return {'list': [_row_to_dict(r) for r in rows], 'total': total} + + +async def get_entity(ns): + """按 id 查询单条,返回 {data}。""" + eid = (ns or {}).get('id') + if not eid: + raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id') + dbname = _dbname() + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('entity', {'id': eid}) + if not recs: + raise EntityError('NOT_FOUND', '实体不存在', 'id') + return {'data': _row_to_dict(recs[0])} + + +# ---------------------------------------------------------------- CRUD + +async def create_entity(ns): + """创建实体。非法输入 100% 拦截不落库;返回 {success, id}。""" + ns = dict(ns or {}) + name = _check_required(ns, 'name', '实体名称', 255) + code = _check_required(ns, 'code', '实体编码', 64) + world_id = (ns.get('world_id') or '').strip() + scene_id = (ns.get('scene_id') or '').strip() + entity_type = _norm_scalar(ns.get('entity_type'), '0', 16) + status = _norm_scalar(ns.get('status'), '0', 16) + attributes_json = _check_attributes_json(ns.get('attributes_json')) + if world_id and not await _world_exists(world_id): + raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', world_id) + if scene_id and not await _scene_exists(scene_id): + raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', scene_id) + now = curDateString() + rec = { + 'id': getID(), + 'world_id': world_id, + 'scene_id': scene_id, + 'name': name, + 'code': code, + 'entity_type': entity_type, + 'status': status, + 'attributes_json': attributes_json, + 'created_at': now, + 'updated_at': now, + } + dbname = _dbname() + try: + async with DBPools().sqlorContext(dbname) as sor: + await sor.C('entity', rec) + except Exception as e: + msg = str(e) + if 'duplicate' in msg.lower(): + raise EntityError('DUPLICATE_CODE', '实体编码已存在', 'code', msg) + raise EntityError('DB_ERROR', '保存实体失败', '', msg) + return {'success': True, 'id': rec['id']} + + +async def update_entity(ns): + """更新实体。剔除 _text 后缀字段;返回 {success, id}。""" + ns = dict(ns or {}) + eid = ns.get('id') + if not eid: + raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id') + clean = {k: v for k, v in ns.items() if not str(k).endswith('_text')} + dbname = _dbname() + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('entity', {'id': eid}) + if not recs: + raise EntityError('NOT_FOUND', '实体不存在', 'id') + upd = {} + if 'name' in clean and clean['name'] is not None: + upd['name'] = _check_required(clean, 'name', '实体名称', 255) + if 'code' in clean and clean['code'] is not None: + upd['code'] = _check_required(clean, 'code', '实体编码', 64) + if 'world_id' in clean and clean['world_id'] is not None: + w = (clean['world_id'] or '').strip() + if w and not await _world_exists(w): + raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', w) + upd['world_id'] = w + if 'scene_id' in clean and clean['scene_id'] is not None: + s = (clean['scene_id'] or '').strip() + if s and not await _scene_exists(s): + raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', s) + upd['scene_id'] = s + if 'entity_type' in clean and clean['entity_type'] is not None: + upd['entity_type'] = _norm_scalar(clean['entity_type'], '0', 16) + if 'status' in clean and clean['status'] is not None: + upd['status'] = _norm_scalar(clean['status'], '0', 16) + if 'attributes_json' in clean and clean['attributes_json'] is not None: + upd['attributes_json'] = _check_attributes_json(clean['attributes_json']) + if not upd: + raise EntityError('PARAM_REQUIRED', '没有可更新的字段', '') + upd['id'] = eid + upd['updated_at'] = curDateString() + try: + await sor.U('entity', upd) + except Exception as e: + msg = str(e) + if 'duplicate' in msg.lower(): + raise EntityError('DUPLICATE_CODE', '实体编码已存在', 'code', msg) + raise EntityError('DB_ERROR', '更新实体失败', '', msg) + return {'success': True, 'id': eid} + + +async def delete_entity(ns): + """删除实体,返回 {success, id}。""" + eid = (ns or {}).get('id') + if not eid: + raise EntityError('PARAM_REQUIRED', '缺少实体ID', 'id') + dbname = _dbname() + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('entity', {'id': eid}) + if not recs: + raise EntityError('NOT_FOUND', '实体不存在', 'id') + await sor.D('entity', {'id': eid}) + return {'success': True, 'id': eid} + + +# ---------------------------------------------------------------- 导入 + +async def parse_entity_file(content, filename=''): + """解析导入文件内容(JSON 数组 / JSON Lines / CSV 表头),返回 {rows: [...]}。""" + if not content: + raise EntityError('PARSE_ERROR', '文件内容为空', 'file') + text = content + if isinstance(text, (bytes, bytearray)): + text = text.decode('utf-8', errors='ignore') + text = text.strip() + if not text: + raise EntityError('PARSE_ERROR', '文件内容为空', 'file') + try: + if text.startswith('['): + data = json.loads(text) + if not isinstance(data, list): + raise EntityError('PARSE_ERROR', 'JSON 顶层必须是数组', 'file') + return {'rows': data} + lines = [ln.strip() for ln in text.splitlines() if ln.strip()] + if lines and lines[0].startswith('{'): + rows = [] + for ln in lines: + obj = json.loads(ln) + if not isinstance(obj, dict): + raise EntityError('PARSE_ERROR', 'JSON Lines 每行必须是对象', 'file', ln[:80]) + rows.append(obj) + return {'rows': rows} + if lines and ',' in lines[0]: + header = [h.strip().strip('\ufeff') for h in lines[0].split(',')] + rows = [] + for ln in lines[1:]: + vals = [v.strip() for v in ln.split(',')] + row = {} + for idx, h in enumerate(header): + row[h] = vals[idx] if idx < len(vals) else '' + rows.append(row) + return {'rows': rows} + data = json.loads(text) + if isinstance(data, dict): + return {'rows': [data]} + raise EntityError('PARSE_ERROR', '无法识别的文件格式', 'file') + except EntityError: + raise + except Exception as e: + raise EntityError('PARSE_ERROR', '解析失败', 'file', str(e)) + + +async def import_entities(ns): + """实体文件导入。 + + 事务批量:先全量解析+校验(含编码查重),再逐条插入;任一条失败整批回滚无脏数据。 + 返回 {success, total, success_count, fail, file_name}(success_count 避免与布尔 success 冲突)。 + """ + ns = dict(ns or {}) + world_id = (ns.get('world_id') or '').strip() + scene_id = (ns.get('scene_id') or '').strip() + file_name = (ns.get('file_name') or '').strip() + rows = ns.get('rows') + if not isinstance(rows, list) or not rows: + raise EntityError('PARAM_REQUIRED', '没有可导入的数据', 'rows') + total = len(rows) + + # 第一阶段:全量解析 + 校验(不落库) + parsed, seen_codes = [], set() + for i, row in enumerate(rows): + seq = i + 1 + if not isinstance(row, dict): + raise EntityError('PARSE_ERROR', f'第{seq}行不是有效对象', 'rows', str(row)[:100]) + try: + name = _check_required(row, 'name', '实体名称', 255) + code = _check_required(row, 'code', '实体编码', 64) + except EntityError as e: + e.detail = f'第{seq}行: {e.message}' + (f'({e.detail})' if e.detail else '') + raise + if code in seen_codes: + raise EntityError('DUPLICATE_CODE', f'第{seq}行编码重复: {code}', 'code', f'第{seq}行') + seen_codes.add(code) + w = (row.get('world_id') or world_id or '').strip() + s = (row.get('scene_id') or scene_id or '').strip() + attrs = _check_attributes_json(row.get('attributes_json')) + parsed.append({ + 'world_id': w, + 'scene_id': s, + 'name': name, + 'code': code, + 'entity_type': _norm_scalar(row.get('entity_type'), '0', 16), + 'status': _norm_scalar(row.get('status'), '0', 16), + 'attributes_json': attrs, + }) + + if world_id and not await _world_exists(world_id): + raise EntityError('WORLD_NOT_FOUND', '所属世界不存在', 'world_id', world_id) + if scene_id and not await _scene_exists(scene_id): + raise EntityError('SCENE_NOT_FOUND', '所属场景不存在', 'scene_id', scene_id) + + # 第二阶段:事务批量插入,任一条失败整批回滚 + dbname = _dbname() + now = curDateString() + imp_id = getID() + try: + async with DBPools().sqlorContext(dbname) as sor: + await sor.C('entity_import', { + 'id': imp_id, + 'world_id': world_id, + 'scene_id': scene_id, + 'file_name': file_name, + 'total': total, + 'success': 0, + 'fail': 0, + 'status': '0', + 'created_at': now, + }) + for item in parsed: + rec = dict(item) + rec['id'] = getID() + rec['created_at'] = now + rec['updated_at'] = now + await sor.C('entity', rec) + await sor.U('entity_import', { + 'id': imp_id, + 'status': '1', + 'success': total, + 'fail': 0, + }) + except Exception as e: + msg = str(e) + if 'duplicate' in msg.lower(): + raise EntityError('DUPLICATE_CODE', '导入失败:实体编码已存在,已整批回滚', 'code', msg) + raise EntityError('DB_ERROR', '导入失败,已整批回滚', '', msg) return { 'success': True, 'total': total, - 'success_count': success, - 'fail': fail, + 'success_count': total, + 'fail': 0, 'file_name': file_name, } -async def load_entity(): - from ahserver.serverenv import ServerEnv +# ---------------------------------------------------------------- 下拉 + +async def get_world_options(): + """世界下拉 [{value,text}](world 模块未挂载时返回空)。""" + dbname = _world_dbname() + if not dbname: + return [] + try: + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('world', {}) + return [{'value': r.id, 'text': getattr(r, 'name', None) or r.id} for r in recs] + except Exception: + return [] + + +async def get_scene_options(): + """场景下拉 [{value,text}](scene 模块未挂载时返回空)。""" + dbname = _scene_dbname() + if not dbname: + return [] + try: + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('scene', {}) + return [{'value': r.id, 'text': getattr(r, 'name', None) or r.id} for r in recs] + except Exception: + return [] + + +async def _appcodes_options(parentid): + """appcodes 字典下拉 [{value,text}]。""" + dbname = _appbase_dbname() + if not dbname: + return [] + try: + async with DBPools().sqlorContext(dbname) as sor: + recs = await sor.R('appcodes_kv', {'parentid': parentid}) + return [{'value': r.k, 'text': r.v} for r in recs] + except Exception: + return [] + + +async def get_entity_type_options(): + return await _appcodes_options('entity_type') + + +async def get_entity_status_options(): + return await _appcodes_options('entity_status') + + +async def get_import_status_options(): + return await _appcodes_options('import_status') + + +# ---------------------------------------------------------------- 注册 + +def load_entity(): + """注册 entity 模块全部函数到 ServerEnv(含复数别名,供 CRUD dspy 调用)。""" env = ServerEnv() env.create_entity = create_entity - env.create_entitys = create_entity + env.create_entities = create_entity env.update_entity = update_entity - env.update_entitys = update_entity + env.update_entities = update_entity env.delete_entity = delete_entity - env.delete_entitys = delete_entity - env.entity_import = entity_import + env.delete_entities = delete_entity + env.list_entities = list_entities + env.get_entity = get_entity + env.import_entities = import_entities + env.parse_entity_file = parse_entity_file + env.entity_import = import_entities + env.get_world_options = get_world_options + env.get_scene_options = get_scene_options + env.get_entity_type_options = get_entity_type_options + env.get_entity_status_options = get_entity_status_options + env.get_import_status_options = get_import_status_options + return env diff --git a/wwwroot/api/entity_create.dspy b/wwwroot/api/entity_create.dspy index 34aeb6e..577bbec 100644 --- a/wwwroot/api/entity_create.dspy +++ b/wwwroot/api/entity_create.dspy @@ -1,2 +1,9 @@ -result = await create_entity(request, params_kw) -return result +# entity_create.dspy —— 实体新增(REST /api/*)。非法输入 100% 拦截不落库。 +try: + result = await create_entity(params_kw) + return {'code': 'OK', 'message': 'success', 'data': result} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '保存实体失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/entity_delete.dspy b/wwwroot/api/entity_delete.dspy index 7bcd6ba..b03a5b5 100644 --- a/wwwroot/api/entity_delete.dspy +++ b/wwwroot/api/entity_delete.dspy @@ -1,2 +1,9 @@ -result = await delete_entity(request, params_kw) -return result +# entity_delete.dspy —— 实体删除(REST /api/*) +try: + result = await delete_entity(params_kw) + return {'code': 'OK', 'message': 'success', 'data': result} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '删除实体失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/entity_get.dspy b/wwwroot/api/entity_get.dspy new file mode 100644 index 0000000..e322273 --- /dev/null +++ b/wwwroot/api/entity_get.dspy @@ -0,0 +1,9 @@ +# entity_get.dspy —— 实体单条查询(REST /api/*) +try: + result = await get_entity(params_kw) + return {'code': 'OK', 'message': 'success', 'data': result.get('data')} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '查询实体失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/entity_import.dspy b/wwwroot/api/entity_import.dspy index 9670b0b..548d327 100644 --- a/wwwroot/api/entity_import.dspy +++ b/wwwroot/api/entity_import.dspy @@ -1,3 +1,35 @@ -debug('entity_import.dspy: START') -result = await entity_import(request, params_kw) -return result +# entity_import.dspy —— 实体文件导入(REST /api/*) +# 入参:world_id / scene_id / file_name / file(文件内容,FileStorage 或文本) +# 事务批量:全量校验 → 整批插入;任一条失败回滚无脏数据。 +# 返回 {success, total, success_count, fail, file_name} +try: + content = None + fname = (params_kw.get('file_name') or '').strip() + file_obj = params_kw.get('file') + if file_obj is not None: + if hasattr(file_obj, 'read'): + content = file_obj.read() + else: + content = file_obj + if content is None: + content = params_kw.get('content') + if content is None and not fname: + raise EntityError('PARAM_REQUIRED', '缺少导入文件', 'file') + if not fname: + fname = (getattr(file_obj, 'filename', '') or '') if file_obj is not None else '' + parsed = await parse_entity_file(content, fname) + rows = parsed.get('rows', []) + imp_ns = { + 'world_id': params_kw.get('world_id'), + 'scene_id': params_kw.get('scene_id'), + 'file_name': fname or 'entity_import', + 'rows': rows, + } + result = await import_entities(imp_ns) + result['success'] = True + return {'code': 'OK', 'message': 'success', 'data': result} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '导入失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/entity_list.dspy b/wwwroot/api/entity_list.dspy new file mode 100644 index 0000000..7b2b349 --- /dev/null +++ b/wwwroot/api/entity_list.dspy @@ -0,0 +1,12 @@ +# entity_list.dspy —— 实体分页列表查询(REST /api/*,宿主挂载后 /entity/api/entity_list.dspy) +# 返回 {list, total};错误统一 {code, message, field, detail} +try: + result = await list_entities(params_kw) + if isinstance(result, dict) and 'list' in result and 'total' in result: + return {'code': 'OK', 'message': 'success', 'data': result} + return {'code': 'OK', 'message': 'success', 'data': {'list': result.get('list', []), 'total': result.get('total', 0)}} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '查询实体列表失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/entity_update.dspy b/wwwroot/api/entity_update.dspy index 6ff8ad2..742101e 100644 --- a/wwwroot/api/entity_update.dspy +++ b/wwwroot/api/entity_update.dspy @@ -1,2 +1,9 @@ -result = await update_entity(request, params_kw) -return result +# entity_update.dspy —— 实体更新(REST /api/*) +try: + result = await update_entity(params_kw) + return {'code': 'OK', 'message': 'success', 'data': result} +except Exception as e: + if hasattr(e, 'to_dict'): + err = e.to_dict() + return {'code': err['code'], 'message': err['message'], 'field': err['field'], 'detail': err['detail']} + return {'code': 'DB_ERROR', 'message': '更新实体失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/get_search_entity_type.dspy b/wwwroot/api/get_search_entity_type.dspy index ec400e4..7aa6efd 100644 --- a/wwwroot/api/get_search_entity_type.dspy +++ b/wwwroot/api/get_search_entity_type.dspy @@ -1,8 +1,6 @@ -result = [{'value': '', 'text': '全部'}] +# get_search_entity_type.dspy —— 实体类型字典下拉(appcodes entity_type)[{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) + opts = await get_entity_type_options() + return {'code': 'OK', 'message': 'success', 'data': opts} except Exception as e: - debug('get_search_entity_type error: %s' % str(e)) -return json.dumps(result, ensure_ascii=False) + return {'code': 'DB_ERROR', 'message': '查询实体类型下拉失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/get_search_import_status.dspy b/wwwroot/api/get_search_import_status.dspy new file mode 100644 index 0000000..b230ef7 --- /dev/null +++ b/wwwroot/api/get_search_import_status.dspy @@ -0,0 +1,6 @@ +# get_search_import_status.dspy —— 导入状态字典下拉(appcodes import_status)[{value, text}] +try: + opts = await get_import_status_options() + return {'code': 'OK', 'message': 'success', 'data': opts} +except Exception as e: + return {'code': 'DB_ERROR', 'message': '查询导入状态下拉失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/get_search_scene_id.dspy b/wwwroot/api/get_search_scene_id.dspy index a4e79d4..8ccdc2e 100644 --- a/wwwroot/api/get_search_scene_id.dspy +++ b/wwwroot/api/get_search_scene_id.dspy @@ -1,8 +1,6 @@ -result = [{'value': '', 'text': '全部'}] +# get_search_scene_id.dspy —— 场景下拉数据源 [{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) + opts = await get_scene_options() + return {'code': 'OK', 'message': 'success', 'data': opts} except Exception as e: - debug('get_search_scene_id error: %s' % str(e)) -return json.dumps(result, ensure_ascii=False) + return {'code': 'DB_ERROR', 'message': '查询场景下拉失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/get_search_status.dspy b/wwwroot/api/get_search_status.dspy index f81a7b5..c31116e 100644 --- a/wwwroot/api/get_search_status.dspy +++ b/wwwroot/api/get_search_status.dspy @@ -1,8 +1,6 @@ -result = [{'value': '', 'text': '全部'}] +# get_search_status.dspy —— 实体状态字典下拉(appcodes entity_status)[{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) + opts = await get_entity_status_options() + return {'code': 'OK', 'message': 'success', 'data': opts} except Exception as e: - debug('get_search_status error: %s' % str(e)) -return json.dumps(result, ensure_ascii=False) + return {'code': 'DB_ERROR', 'message': '查询状态下拉失败', 'field': '', 'detail': str(e)} diff --git a/wwwroot/api/get_search_world_id.dspy b/wwwroot/api/get_search_world_id.dspy index ad91998..44e97da 100644 --- a/wwwroot/api/get_search_world_id.dspy +++ b/wwwroot/api/get_search_world_id.dspy @@ -1,8 +1,6 @@ -result = [{'value': '', 'text': '全部'}] +# get_search_world_id.dspy —— 世界下拉数据源 [{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) + opts = await get_world_options() + return {'code': 'OK', 'message': 'success', 'data': opts} except Exception as e: - debug('get_search_world_id error: %s' % str(e)) -return json.dumps(result, ensure_ascii=False) + return {'code': 'DB_ERROR', 'message': '查询世界下拉失败', 'field': '', 'detail': str(e)}