"""MSYNC ORM 中转处理器 子服务器端注册 Socket.IO 事件处理器,接收主服务器发来的 ORM 查询请求, 在本地数据库执行 SQL 并返回结果。 用法(子服务器端): from gvsdsdk.msync.orm_handler import ORMRelayHandler handler = ORMRelayHandler() handler.Register(sio_client) # 注册到 MSyncSocketClient 实例 """ import datetime import logging import uuid logger = logging.getLogger(__name__) class ORMRelayHandler: """ORM 中转处理器——在子服务器端执行本地 SQL 查询 通过 MSYNC Socket.IO 事件协议,主服务器将 ORM 请求转发给子服务器, 子服务器在本地数据库执行查询后返回结果。 """ def Register(self, sio_client): """将 ORM 事件处理器注册到 MSyncSocketClient 实例 Args: sio_client: MSyncSocketClient 实例,需要有 _sio 属性 """ sio = sio_client._sio @sio.on('msync:orm:tables', namespace='/msync') def on_orm_tables(data): return self.HandleTables(data) @sio.on('msync:orm:columns', namespace='/msync') def on_orm_columns(data): return self.HandleColumns(data) @sio.on('msync:orm:list', namespace='/msync') def on_orm_list(data): return self.HandleList(data) @sio.on('msync:orm:add', namespace='/msync') def on_orm_add(data): return self.HandleAdd(data) @sio.on('msync:orm:modify', namespace='/msync') def on_orm_modify(data): return self.HandleModify(data) @sio.on('msync:orm:delete', namespace='/msync') def on_orm_delete(data): return self.HandleDelete(data) logger.info('[ORM] ORM 中转处理器已注册') def _GetConnection(self): """获取本地数据库连接(Django default)""" from django.db import connections conn = connections['default'] conn.ensure_connection() return conn # ─── 事件处理 ────────────────────────────────────────────── def HandleTables(self, data): """列出本地数据库中所有用户表""" try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: tables = self._ListTables(cursor) return {'code': 0, 'msg': 'success', 'data': tables} except Exception as e: logger.error(f'[ORM] HandleTables 异常: {e}') return {'code': 1, 'msg': f'查询失败: {e}'} def HandleColumns(self, data): """列出指定表的列信息""" table_name = (data or {}).get('Table', '') if not table_name: return {'code': 1, 'msg': '缺少 Table 参数'} try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: columns = self._ListColumns(cursor, table_name) return {'code': 0, 'msg': 'success', 'data': columns} except Exception as e: logger.error(f'[ORM] HandleColumns 异常: {e}') return {'code': 1, 'msg': f'查询失败: {e}'} def HandleList(self, data): """分页查询表数据""" table_name = (data or {}).get('Table', '') if not table_name: return {'code': 1, 'msg': '缺少 Table 参数'} try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: result = self._QueryTable(cursor, table_name, data or {}) if result.get('error'): return {'code': 1, 'msg': result['error'], 'data': None} return {'code': 0, 'msg': 'success', 'data': result} except Exception as e: logger.error(f'[ORM] HandleList 异常: {e}') return {'code': 1, 'msg': f'查询失败: {e}'} def HandleAdd(self, data): """新增记录""" table_name = (data or {}).get('Table', '') record = (data or {}).get('Record', {}) if not table_name: return {'code': 1, 'msg': '缺少 Table 参数'} try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: result = self._AddRecord(cursor, table_name, record) return result except Exception as e: logger.error(f'[ORM] HandleAdd 异常: {e}') return {'code': 1, 'msg': f'新增失败: {e}'} def HandleModify(self, data): """修改记录""" table_name = (data or {}).get('Table', '') record = (data or {}).get('Record', {}) if not table_name: return {'code': 1, 'msg': '缺少 Table 参数'} try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: result = self._ModifyRecord(cursor, table_name, record) return result except Exception as e: logger.error(f'[ORM] HandleModify 异常: {e}') return {'code': 1, 'msg': f'修改失败: {e}'} def HandleDelete(self, data): """删除记录""" table_name = (data or {}).get('Table', '') record = (data or {}).get('Record', {}) if not table_name: return {'code': 1, 'msg': '缺少 Table 参数'} try: conn = self._GetConnection() except Exception as e: return {'code': 1, 'msg': f'数据库连接失败: {e}'} try: with conn.cursor() as cursor: result = self._DeleteRecord(cursor, table_name, record) return result except Exception as e: logger.error(f'[ORM] HandleDelete 异常: {e}') return {'code': 1, 'msg': f'删除失败: {e}'} # ─── SQL 执行 ────────────────────────────────────────────── def _ListTables(self, cursor): """列出所有用户表,含 i18n 标题""" cursor.execute( "SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' " "ORDER BY TABLE_NAME" ) from django.apps import apps # 构建表名→中文标题映射 model_verbose = {} for model in apps.get_models(): meta = model._meta # 优先使用 table_title(自定义 Meta 属性),其次 verbose_name table_title = getattr(meta, 'table_title', None) title = str(table_title) if table_title else (str(meta.verbose_name) if meta.verbose_name else '') if title: model_verbose[meta.object_name] = title model_verbose[meta.db_table] = title result = [] for row in cursor.fetchall(): table_name, table_rows, table_comment = row # 优先级:TABLE_COMMENT > table_title/verbose_name > 表名 title = table_comment or model_verbose.get(table_name) or table_name result.append({'name': table_name, 'title': title, 'count': table_rows or 0}) return result def _ListColumns(self, cursor, table_name): """列出指定表的列信息""" cursor.execute( "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA, COLUMN_TYPE, COLUMN_COMMENT " "FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s " "ORDER BY ORDINAL_POSITION", [table_name] ) model_verbose = self._FindModelVerboseNames(table_name) logger.info(f'[ORM] _ListColumns table={table_name} model_verbose={model_verbose}') columns = [] for row in cursor.fetchall(): col_name, data_type, is_nullable, col_key, extra, col_type, col_comment = row is_pk = col_key == 'PRI' is_auto = 'auto_increment' in (extra or '').lower() if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'): ftype = 'number' elif data_type in ('float', 'double', 'decimal'): ftype = 'number' elif data_type in ('datetime', 'timestamp'): ftype = 'datetime' elif data_type in ('date',): ftype = 'date' elif data_type in ('time',): ftype = 'time' elif data_type in ('json',): ftype = 'textarea' elif data_type in ('text', 'mediumtext', 'longtext', 'tinytext'): ftype = 'textarea' elif data_type in ('binary', 'varbinary', 'blob'): ftype = 'uuid' else: ftype = 'text' title = col_comment or model_verbose.get(col_name) or col_name columns.append({ 'field': col_name, 'title': title, 'type': ftype, 'editable': not is_pk and not is_auto, 'sortable': True, 'filterable': True, 'primaryKey': is_pk, 'hideInForm': is_pk, 'required': is_nullable == 'NO' and not is_auto, 'dbType': col_type, }) return columns def _QueryTable(self, cursor, table_name, params): """分页查询表数据""" cursor.execute( "SELECT COUNT(*) FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", [table_name] ) if cursor.fetchone()[0] == 0: return {'records': [], 'total': 0, 'error': f'表不存在: {table_name}'} col_info = self._GetTableColumns(cursor, table_name) pk_cols = {k: v for k, v in col_info.items() if v['is_pk']} where_parts = [] where_values = [] for pk_name in pk_cols: pk_val = params.get(pk_name, '') if pk_val: where_parts.append(f'`{pk_name}` = %s') where_values.append(self._ConvertValueForDB(pk_val, pk_cols[pk_name])) keyword = params.get('keyword', '') if not where_parts and keyword: cursor.execute( "SELECT COLUMN_NAME FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s " "AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext')", [table_name] ) text_cols = [row[0] for row in cursor.fetchall()] if text_cols: conditions = [f'`{c}` LIKE %s' for c in text_cols] or_clause = ' OR '.join(conditions) where_parts.append(f'({or_clause})') where_values.extend([f'%{keyword}%'] * len(text_cols)) where_clause = (' WHERE ' + ' AND '.join(where_parts)) if where_parts else '' if where_clause: cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`{where_clause}', where_values) total = cursor.fetchone()[0] else: cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`') total = cursor.fetchone()[0] try: page = max(int(params.get('PageIndex', 1)), 1) except (ValueError, TypeError): page = 1 try: page_size = min(max(int(params.get('PageSize', 20)), 1), 200) except (ValueError, TypeError): page_size = 20 offset = (page - 1) * page_size cursor.execute( f'SELECT * FROM `{table_name}`{where_clause} ORDER BY 1 LIMIT %s OFFSET %s', where_values + [page_size, offset] ) desc = [d[0] for d in cursor.description] records = [] for row in cursor.fetchall(): record = {} for i, val in enumerate(row): val = self._SerializeValue(val) record[desc[i]] = val records.append(record) return {'records': records, 'total': total, 'page': page, 'PageSize': page_size} def _AddRecord(self, cursor, table_name, record): """新增记录""" if not self._ValidateTableName(cursor, table_name): return {'code': 1, 'msg': f'表 {table_name} 不存在'} col_info = self._GetTableColumns(cursor, table_name) fields = [] values = [] for key, val in record.items(): if key not in col_info: continue if col_info[key]['is_auto']: continue fields.append(f'`{key}`') values.append(self._ConvertValueForDB(val, col_info[key])) if not fields: return {'code': 1, 'msg': '没有可插入的字段'} placeholders = ', '.join(['%s'] * len(fields)) sql = f'INSERT INTO `{table_name}` ({", ".join(fields)}) VALUES ({placeholders})' cursor.execute(sql, values) return {'code': 0, 'msg': '新增成功'} def _ModifyRecord(self, cursor, table_name, record): """修改记录""" if not self._ValidateTableName(cursor, table_name): return {'code': 1, 'msg': f'表 {table_name} 不存在'} col_info = self._GetTableColumns(cursor, table_name) pk_cols = {k: v for k, v in col_info.items() if v['is_pk']} if not pk_cols: return {'code': 1, 'msg': '表没有主键,无法定位记录'} set_parts = [] set_values = [] for key, val in record.items(): if key not in col_info or key in pk_cols: continue set_parts.append(f'`{key}` = %s') set_values.append(self._ConvertValueForDB(val, col_info[key])) if not set_parts: return {'code': 0, 'msg': '没有需要更新的字段'} where_parts = [] where_values = [] for pk_name in pk_cols: if pk_name not in record: return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'} where_parts.append(f'`{pk_name}` = %s') where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name])) sql = f'UPDATE `{table_name}` SET {", ".join(set_parts)} WHERE {" AND ".join(where_parts)}' cursor.execute(sql, set_values + where_values) if cursor.rowcount == 0: return {'code': 1, 'msg': '记录不存在或未变更'} return {'code': 0, 'msg': '修改成功'} def _DeleteRecord(self, cursor, table_name, record): """删除记录""" if not self._ValidateTableName(cursor, table_name): return {'code': 1, 'msg': f'表 {table_name} 不存在'} col_info = self._GetTableColumns(cursor, table_name) pk_cols = {k: v for k, v in col_info.items() if v['is_pk']} if not pk_cols: return {'code': 1, 'msg': '表没有主键,无法定位记录'} where_parts = [] where_values = [] for pk_name in pk_cols: if pk_name not in record: return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'} where_parts.append(f'`{pk_name}` = %s') where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name])) sql = f'DELETE FROM `{table_name}` WHERE {" AND ".join(where_parts)}' cursor.execute(sql, where_values) if cursor.rowcount == 0: return {'code': 1, 'msg': '记录不存在'} return {'code': 0, 'msg': '删除成功'} # ─── 工具方法 ────────────────────────────────────────────── def _ValidateTableName(self, cursor, table_name): """验证表名存在""" cursor.execute( "SELECT COUNT(*) FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s", [table_name] ) return cursor.fetchone()[0] > 0 def _GetTableColumns(self, cursor, table_name): """获取表的列信息""" cursor.execute( "SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE " "FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s " "ORDER BY ORDINAL_POSITION", [table_name] ) cols = {} for row in cursor.fetchall(): col_name, data_type, column_type, col_key, extra, is_nullable = row cols[col_name] = { 'type': data_type, 'column_type': column_type or '', 'is_pk': col_key == 'PRI', 'is_auto': 'auto_increment' in (extra or '').lower(), 'is_nullable': is_nullable == 'YES', } return cols def _ConvertValueForDB(self, value, col_info): """将前端传来的值转换为数据库可接受的类型""" if value is None or value == '': if col_info['is_nullable']: return None return '' data_type = col_info['type'] if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'): return int(value) elif data_type in ('float', 'double', 'decimal'): return float(value) elif data_type in ('binary', 'varbinary'): import re hex_str = re.sub(r'[^0-9a-fA-F]', '', str(value)) if len(hex_str) == 32: return uuid.UUID(hex_str).bytes return value return value def _SerializeValue(self, val): """将数据库值序列化为 JSON 安全格式""" if isinstance(val, bytes): if len(val) == 16: try: return str(uuid.UUID(bytes=val)) except ValueError: return val.hex() return val.hex() elif isinstance(val, datetime.datetime): return val.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(val, datetime.date): return val.isoformat() elif isinstance(val, datetime.time): return val.strftime('%H:%M:%S') elif isinstance(val, (bytearray, memoryview)): return bytes(val).hex() return val def _FindModelVerboseNames(self, table_name): """从 Django 模型中获取字段的 verbose_name""" from django.apps import apps verbose_map = {} try: for model in apps.get_models(): if model._meta.db_table == table_name: logger.info(f'[ORM] _FindModelVerboseNames matched model={model.__name__} db_table={model._meta.db_table}') for f in model._meta.get_fields(): # 获取数据库列名 col_name = None if hasattr(f, 'column') and f.column: col_name = f.column elif hasattr(f, 'db_column') and f.db_column: col_name = f.db_column elif hasattr(f, 'attname'): col_name = f.attname if not col_name: continue vn = getattr(f, 'verbose_name', None) if vn and str(vn) != col_name: verbose_map[col_name] = str(vn) logger.debug(f'[ORM] {col_name} -> {str(vn)}') break else: # 没有匹配的模型 available = [m._meta.db_table for m in apps.get_models()] logger.warning(f'[ORM] _FindModelVerboseNames: 未找到 db_table={table_name} 的模型, 可用表: {available[:20]}...') except Exception as e: logger.error(f'[ORM] _FindModelVerboseNames 异常: {e}') return verbose_map