refactor(gerp): FormService 继承 gvsdsdk FormServiceBase 复用字段工具并委托 code_display

This commit is contained in:
TermiNexus committed 2026-08-07 22:55:24 +08:00
1 parent 010ff46765
commit 187aebe811
2 files changed
+26 -138

No files matched your search

+20 -136
View File
@@ -4,7 +4,6 @@ import re
import logging
from django.db import connection, transaction, IntegrityError
from django.db.models import Q
from django.db.models.fields import NOT_PROVIDED
logger = logging.getLogger(__name__)
@@ -18,6 +17,13 @@ from gvsds.apps.gerp.models import (
AuditLog,
)
from gvsds.apps.core.models import User, UserAvatar, User2FA, UserPending2FA
from gvsdsdk.form_service import (
FormServiceBase,
_PascalizeRecord,
_BuildFieldMap,
_ResolveFieldName,
_ConvertValueForField,
)
from gvsdsdk.order.models import Order, OrderItem, OrderLevel, OrderTemplate, OrderTemplateItem
from gvsds.apps.gerp.i18n import translate_field, translate_table
@@ -87,35 +93,7 @@ def create_tenant_admin_role(tenant):
return RoleManager.EnsureTenantAdminRole(tenant.TenantUUID)
def _pascalize_record(record, model=None):
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)
# UUIDObj 处理:gvsdsdk UUIDField 返回 UUIDObj 对象(非 bytes),
# 需转为字符串以便 JSON 序列化,否则前端收不到数据
if val is not None and val.__class__.__name__ == 'UUIDObj':
val = val.str
elif 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 FormService:
class FormService(FormServiceBase):
TABLE_MAP = {
'User': User,
'UserAvatar': UserAvatar,
@@ -182,6 +160,7 @@ class FormService:
HIDE_PK_UUID_IN_TABLE = {'Company', 'Department', 'Workgroup'}
def __init__(self, user, current_company_uuid=None, admin_verify=False, current_role_uuid=None, current_tenant_uuid=None, ip_address='', role_scope=None):
super().__init__(user=user)
self.user = user
self.ip_address = ip_address or ''
self.role_scope = role_scope
@@ -254,14 +233,6 @@ class FormService:
from gvsdsdk.auth.admin_service import IsSuperAdmin
return IsSuperAdmin(user=user, role_uuid=self.current_role_uuid)
def _build_field_map(self, model):
pascal_map = {}
for f in model._meta.get_fields():
if not hasattr(f, 'name'):
continue
pascal_map[f.name] = f
return pascal_map
def _get_entity_picker_fields(self, model):
"""获取模型中所有 entityPicker 类型的字段信息"""
result = []
@@ -435,74 +406,6 @@ class FormService:
pass
return records
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
# 非空字段:数值类型返回默认值或0,其他类型返回空字符串
field_type = field.__class__.__name__
if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField',
'PositiveIntegerField', 'PositiveSmallIntegerField',
'FloatField', 'DecimalField'):
default = getattr(field, 'default', None)
if default is not None and default is not NOT_PROVIDED:
return default
return 0
if field_type == 'BooleanField':
return False
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:
dt = datetime.datetime.fromisoformat(val)
# USE_TZ=False + MySQL 不接受 aware datetime:
# 统一转 naive UTC(UTC-0 存储,展示时由前端按用户时区转换)
if dt.tzinfo is not None:
dt = dt.astimezone(datetime.timezone.utc).replace(tzinfo=None)
return dt
except (ValueError, TypeError):
return val
if field_type in ('BooleanField', 'NullBooleanField') and isinstance(val, str):
low = val.strip().lower()
if low in ('true', '1', 't', 'yes'):
return True
if low in ('false', '0', 'f', 'no'):
return False
return val
max_length = getattr(field, 'max_length', None)
if max_length and isinstance(val, str) and len(val) > max_length:
val = val[:max_length]
return val
def get_tables(self):
tables = []
for name, model in self.TABLE_MAP.items():
@@ -1110,7 +1013,7 @@ class FormService:
f_name = f.attname # e.g. UserUUID_id -> 但实际 attname 是字段名_id
val = getattr(obj, f_name, None)
row[f.name] = val
records.append(_pascalize_record(row, model))
records.append(_PascalizeRecord(row, model))
# 为 entityPicker 字段附带关联实体的 label
if ep_fields:
records = self._enrich_entity_labels(obj_list, ep_fields, records)
@@ -1183,12 +1086,12 @@ class FormService:
# 订单明细由订单统一管理,禁止直接新增(防绕过 OrderManager 破坏金额一致性)
if table_name == 'OrderItem':
return {'error': '订单明细由订单统一管理,不允许直接新增', 'field': 'Table'}
pascal_map = self._build_field_map(model)
pascal_map = self._BuildFieldMap(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)
resolved = self._ResolveFieldName(key, pascal_map)
if resolved is None:
continue
field = pascal_map.get(resolved) or pascal_map.get(key)
@@ -1196,7 +1099,7 @@ class FormService:
continue
if isinstance(val, list) and len(val) == 1:
val = val[0]
val = self._convert_value_for_field(field, val)
val = self._ConvertValueForField(field, val)
# 状态字段校验:只允许 STATUS_FIELD_OPTIONS 中的合法状态码(防手动提交非法值)
if resolved in self.STATUS_FIELD_OPTIONS:
valid_values = [opt['value'] for opt in self.STATUS_FIELD_OPTIONS[resolved]]
@@ -1468,7 +1371,7 @@ class FormService:
# 使用 pk 字段名显式查找,避免自定义 BinaryField pk 的 lookup 问题
pk_field_name = model._meta.pk.name
pk_field = model._meta.pk
pk_value = self._convert_value_for_field(pk_field, pk)
pk_value = self._ConvertValueForField(pk_field, pk)
# 构造查询条件:附加租户/公司过滤防止跨域越权
lookup = {pk_field_name: pk_value}
if table_name not in self.COMPANY_SCOPE_EXEMPT_TABLES:
@@ -1511,12 +1414,12 @@ class FormService:
old_snapshot = self._get_record_snapshot(obj) if table_name in self.AUDIT_TABLES else None
# 工单状态流转:保存修改前的 Status,用于流转判定(时间戳 + 通知)
_wo_old_status = getattr(obj, 'Status', None) if table_name == 'WorkOrder' else None
pascal_map = self._build_field_map(model)
pascal_map = self._BuildFieldMap(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)
resolved = self._ResolveFieldName(key, pascal_map)
if resolved is None:
continue
if resolved == model._meta.pk.name:
@@ -1530,7 +1433,7 @@ class FormService:
continue
if isinstance(val, list) and len(val) == 1:
val = val[0]
val = self._convert_value_for_field(field, val)
val = self._ConvertValueForField(field, val)
# 状态字段校验:只允许 STATUS_FIELD_OPTIONS 中的合法状态码(防手动提交非法值)
if resolved in self.STATUS_FIELD_OPTIONS:
valid_values = [opt['value'] for opt in self.STATUS_FIELD_OPTIONS[resolved]]
@@ -1634,7 +1537,7 @@ class FormService:
# 使用 pk 字段名显式查找,避免自定义 BinaryField pk 的 lookup 问题
pk_field_name = model._meta.pk.name
pk_field = model._meta.pk
pk_value = self._convert_value_for_field(pk_field, pk)
pk_value = self._ConvertValueForField(pk_field, pk)
# 构造查询条件:附加租户/公司过滤防止跨域越权
lookup = {pk_field_name: pk_value}
if table_name not in self.COMPANY_SCOPE_EXEMPT_TABLES:
@@ -1817,24 +1720,5 @@ class FormService:
logger.warning('[AuditLog] 记录审计日志失败: table=%s action=%s', table_name, action, exc_info=True)
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
# 委托 gvsdsdk FormServiceBase.CodeDisplay(values() 两列查询,更优)
return self.CodeDisplay(data)
+6 -2
View File
@@ -23,6 +23,7 @@ import datetime
import logging
from django.db import DatabaseError
from django.db.models import Q
from django.db.models.fields import NOT_PROVIDED
logger = logging.getLogger(__name__)
@@ -39,7 +40,10 @@ def _PascalizeRecord(record, model=None):
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:
# UUIDObj 处理:gvsdsdk UUIDField 返回 UUIDObj 对象(非 bytes),需转为字符串
if val is not None and val.__class__.__name__ == 'UUIDObj':
val = val.str
elif isinstance(val, bytes) and len(val) == 16:
try:
val = str(uuid.UUID(bytes=val))
except ValueError:
@@ -92,7 +96,7 @@ class FormServiceBase:
'PositiveIntegerField', 'PositiveSmallIntegerField',
'FloatField', 'DecimalField'):
default = getattr(field, 'default', None)
if default is not None:
if default is not None and default is not NOT_PROVIDED:
return default
return 0
if field_type == 'BooleanField':