diff --git a/gvsds/apps/gerp/form_service.py b/gvsds/apps/gerp/form_service.py index 2c3c4efe..c586e16f 100644 --- a/gvsds/apps/gerp/form_service.py +++ b/gvsds/apps/gerp/form_service.py @@ -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) diff --git a/gvsdsdk/form_service.py b/gvsdsdk/form_service.py index c4488fd4..70d3a277 100644 --- a/gvsdsdk/form_service.py +++ b/gvsdsdk/form_service.py @@ -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':