Files
Django/gvsdsdk/form_service.py

418 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块:通用 FormService 引擎
提供子服务器开箱即用的 ORM 表管理 API。
子服务器只需继承 FormServiceBase 并注册 TABLE_MAP 即可。
使用方式:
# 子服务器 form_service.py
from gvsdsdk.form_service import FormServiceBase
from .models import MyModel1, MyModel2
class FormService(FormServiceBase):
TABLE_MAP = {
'MyModel1': MyModel1,
'MyModel2': MyModel2,
}
# 子服务器 views.py
from gvsdsdk.form_service import FormServiceAPIView
from .form_service import FormService
"""
import uuid
import datetime
import logging
from django.db.models import Q
logger = logging.getLogger(__name__)
def _pascalize_record(record, model=None):
"""将数据库记录的键名转为 PascalCase使用 db_column"""
result = {}
column_map = {}
if model is not None:
for f in model._meta.get_fields():
if hasattr(f, 'db_column') and f.db_column:
column_map[f.name] = f.db_column
if hasattr(f, 'attname') and f.attname != f.name:
column_map[f.attname] = f.db_column
for key, val in record.items():
out_key = column_map.get(key, key)
if isinstance(val, bytes) and len(val) == 16:
try:
val = str(uuid.UUID(bytes=val))
except ValueError:
pass
elif isinstance(val, datetime.datetime):
val = val.strftime('%Y-%m-%d %H:%M:%S')
elif isinstance(val, datetime.date):
val = val.isoformat()
result[out_key] = val
return result
class FormServiceBase:
"""通用 FormService 基类
子类需要定义:
TABLE_MAP: dict — 表名到 Django Model 的映射
ENTITY_PICKER_MAP: dict — ForeignKey 到实体选择器的映射(可选)
READONLY_TABLES_FOR_NORMAL: set — 普通用户只读的表(可选)
ADMIN_ONLY_TABLES: set — 仅管理员可访问的表(可选)
"""
TABLE_MAP = {}
ENTITY_PICKER_MAP = {}
READONLY_TABLES_FOR_NORMAL = set()
ADMIN_ONLY_TABLES = set()
def __init__(self, user=None):
self.user = user
def _build_field_map(self, model):
pascal_map = {}
for f in model._meta.get_fields():
if hasattr(f, 'name'):
pascal_map[f.name] = f
return pascal_map
def _resolve_field_name(self, key, pascal_map):
if key in pascal_map:
return pascal_map[key].name
return None
def _convert_value_for_field(self, field, val):
if val is None or val == '':
if getattr(field, 'null', False):
return None
return val
field_type = field.__class__.__name__
if field_type == 'UUIDField' and isinstance(val, str):
try:
return uuid.UUID(val).bytes
except ValueError:
return val
if field_type == 'BinaryField' and isinstance(val, str):
try:
return uuid.UUID(val).bytes
except ValueError:
return val
if field_type == 'ForeignKey' and isinstance(val, str):
try:
return uuid.UUID(val).bytes
except ValueError:
return val
if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField',
'PositiveIntegerField', 'PositiveSmallIntegerField',
'FloatField', 'DecimalField') and isinstance(val, str):
try:
if field_type == 'FloatField':
return float(val)
return int(val)
except (ValueError, TypeError):
return val
if field_type == 'DateTimeField' and isinstance(val, str):
try:
return datetime.datetime.fromisoformat(val)
except (ValueError, TypeError):
return val
if hasattr(field, 'max_length') and isinstance(val, str) and len(val) > field.max_length:
val = val[:field.max_length]
return val
@staticmethod
def _map_field_type(field):
t = field.__class__.__name__
mapping = {
'UUIDField': 'uuid',
'BinaryField': 'uuid',
'FixedBinaryField': 'uuid',
'CharField': 'text',
'TextField': 'textarea',
'JSONField': 'textarea',
'SmallIntegerField': 'number',
'IntegerField': 'number',
'BigIntegerField': 'number',
'PositiveIntegerField': 'number',
'PositiveSmallIntegerField': 'number',
'FloatField': 'number',
'DecimalField': 'number',
'BooleanField': 'boolean',
'NullBooleanField': 'boolean',
'DateTimeField': 'datetime',
'DateField': 'date',
'TimeField': 'time',
'ForeignKey': 'uuid',
'OneToOneField': 'uuid',
'ManyToManyField': 'text',
'ImageField': 'text',
'FileField': 'text',
'SlugField': 'text',
'EmailField': 'text',
'URLField': 'text',
'AutoField': 'number',
'BigAutoField': 'number',
}
if t == 'CharField' and hasattr(field, 'choices') and field.choices:
return 'select'
return mapping.get(t, 'text')
def get_tables(self):
tables = []
for name, model in self.TABLE_MAP.items():
try:
count = model.objects.count()
except Exception:
count = -1
tables.append({
'name': name,
'title': name,
'count': count,
'type': 'dynamic',
'source': 'model',
})
return tables
def get_columns(self, table_name):
model = self.TABLE_MAP.get(table_name)
if not model:
return []
try:
columns = []
for f in model._meta.get_fields():
if not hasattr(f, 'name') or f.name in ('id',):
continue
if f.auto_created and not f.concrete:
continue
if f.is_relation and not f.concrete:
continue
is_pk = (f.name == model._meta.pk.name) if model._meta.pk else False
pascal_name = f.name
field_type = self._map_field_type(f)
col = {
'field': pascal_name,
'title': pascal_name,
'type': field_type,
'editable': getattr(f, 'editable', True) and not is_pk,
'sortable': True,
'filterable': True,
'primaryKey': is_pk,
'hideInForm': is_pk,
}
if field_type == 'select' and hasattr(f, 'choices') and f.choices:
col['options'] = [{'value': k, 'label': str(v)} for k, v in f.choices]
if field_type == 'number':
col['min'] = None
col['max'] = None
for v in getattr(f, 'validators', []):
vname = v.__class__.__name__
if vname == 'MinValueValidator':
col['min'] = v.limit_value
elif vname == 'MaxValueValidator':
col['max'] = v.limit_value
if col['min'] is None and col['max'] is None:
col.pop('min', None)
col.pop('max', None)
if field_type == 'text' and hasattr(f, 'max_length') and f.max_length:
col['maxLength'] = f.max_length
if getattr(f, 'null', False):
col['required'] = False
# ForeignKey 字段:检测关联模型,自动使用实体选择器
if f.__class__.__name__ == 'ForeignKey':
related_model = getattr(f, 'related_model', None)
if related_model:
related_name = related_model.__name__
ep_config = self.ENTITY_PICKER_MAP.get(related_name)
if ep_config:
col['type'] = 'entityPicker'
col['entityPicker'] = ep_config
columns.append(col)
return columns
except Exception:
return []
def get_table(self, table_name, params):
model = self.TABLE_MAP.get(table_name)
if not model:
return {'records': [], 'total': 0}
qs = model.objects.all()
keyword = params.get('keyword', '')
if keyword:
or_conditions = {}
for f in model._meta.get_fields():
if hasattr(f, 'name') and f.__class__.__name__ in ('CharField', 'TextField'):
or_conditions[f'{f.name}__icontains'] = keyword
if or_conditions:
q = Q()
for k, v in or_conditions.items():
q |= Q(**{k: v})
qs = qs.filter(q)
try:
page = int(params.get('PageIndex', 1))
except (ValueError, TypeError):
page = 1
try:
page_size = int(params.get('PageSize', 20))
except (ValueError, TypeError):
page_size = 20
total = qs.count()
start = (page - 1) * page_size
records = [_pascalize_record(r, model) for r in qs[start:start + page_size].values()]
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
def add_record(self, data):
table_name = data.get('Table', '')
model = self.TABLE_MAP.get(table_name)
if not model:
return None
pascal_map = self._build_field_map(model)
create_data = {}
for key, val in data.items():
if key in ('table', 'action', 'Table', 'Action', 'pk', 'PK', 'uuid', 'UUID'):
continue
resolved = self._resolve_field_name(key, pascal_map)
if resolved is None:
continue
field = pascal_map.get(resolved) or pascal_map.get(key)
if field is None:
continue
if isinstance(val, list) and len(val) == 1:
val = val[0]
val = self._convert_value_for_field(field, val)
is_fk = field.__class__.__name__ == 'ForeignKey'
attr_name = field.attname if is_fk else resolved
create_data[attr_name] = val
pk_field_name = model._meta.pk.name if model._meta.pk else None
if pk_field_name and pk_field_name not in create_data:
pk_field = model._meta.pk
if pk_field.__class__.__name__ in ('UUIDField', 'BinaryField', 'FixedBinaryField'):
create_data[pk_field_name] = uuid.uuid4().bytes
obj = model.objects.create(**create_data)
return {'PK': str(obj.pk) if obj.pk else None}
def modify_record(self, data):
table_name = data.get('Table', '')
model = self.TABLE_MAP.get(table_name)
if not model:
return None
pk = data.get('PK')
if not pk:
return None
pk_field_name = model._meta.pk.name
pk_field = model._meta.pk
pk_value = self._convert_value_for_field(pk_field, pk)
try:
obj = model.objects.get(**{pk_field_name: pk_value})
except model.DoesNotExist:
return None
pascal_map = self._build_field_map(model)
update_fields = []
for key, val in data.items():
if key in ('table', 'action', 'pk', 'uuid', 'Table', 'Action', 'PK', 'UUID'):
continue
resolved = self._resolve_field_name(key, pascal_map)
if resolved is None:
continue
if resolved == model._meta.pk.name:
continue
field = pascal_map.get(resolved) or pascal_map.get(key)
if field is None:
continue
if isinstance(val, list) and len(val) == 1:
val = val[0]
val = self._convert_value_for_field(field, val)
is_fk = field.__class__.__name__ == 'ForeignKey'
attr_name = field.attname if is_fk else resolved
setattr(obj, attr_name, val)
update_fields.append(attr_name)
if update_fields:
obj.save(update_fields=update_fields)
return {'PK': str(obj.pk) if obj.pk else None}
def modify_item(self, data):
return self.modify_record(data)
def delete_record(self, data):
table_name = data.get('Table', '')
model = self.TABLE_MAP.get(table_name)
if not model:
return False
pk = data.get('PK')
if not pk:
return False
pk_field_name = model._meta.pk.name
pk_field = model._meta.pk
pk_value = self._convert_value_for_field(pk_field, pk)
try:
model.objects.get(**{pk_field_name: pk_value}).delete()
return True
except model.DoesNotExist:
return False
def code_display(self, data):
table_name = data.get('Table', '')
code_field = data.get('CodeField', 'PermCode')
name_field = data.get('NameField', 'PermName')
model = self.TABLE_MAP.get(table_name)
if not model:
return []
fields = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')}
if code_field not in fields or name_field not in fields:
return []
qs = model.objects.all()
keyword = data.get('Keyword', '')
if keyword:
filter_kw = {f'{code_field}__icontains': keyword}
qs = qs.filter(**filter_kw)
results = []
for obj in qs[:50]:
results.append({
'code': getattr(obj, code_field, ''),
'name': getattr(obj, name_field, ''),
})
return results
def handle_request(self, action, data):
"""统一请求入口
Args:
action: 操作类型 (tables/columns/list/add/modify/delete/code_display)
data: 请求数据
Returns:
dict 响应数据
"""
if action == 'tables':
return {'code': 0, 'msg': 'success', 'data': self.get_tables()}
elif action == 'columns':
table_name = data.get('Table', '')
if table_name not in self.TABLE_MAP:
return {'code': 1, 'msg': f'{table_name} 不存在'}
return {'code': 0, 'msg': 'success', 'data': self.get_columns(table_name)}
elif action == 'list':
table_name = data.get('Table', '')
if table_name not in self.TABLE_MAP:
return {'code': 1, 'msg': f'{table_name} 不存在'}
return {'code': 0, 'msg': 'success', 'data': self.get_table(table_name, data)}
elif action == 'add':
result = self.add_record(data)
if result is None:
return {'code': 1, 'msg': '添加失败'}
return {'code': 0, 'msg': 'success', 'data': result}
elif action == 'modify':
result = self.modify_record(data)
if result is None:
return {'code': 1, 'msg': '修改失败'}
return {'code': 0, 'msg': 'success', 'data': result}
elif action == 'delete':
result = self.delete_record(data)
if not result:
return {'code': 1, 'msg': '删除失败'}
return {'code': 0, 'msg': 'success'}
elif action == 'code_display':
result = self.code_display(data)
return {'code': 0, 'msg': 'success', 'data': result}
else:
return {'code': 1, 'msg': f'未知操作: {action}'}