diff --git a/gvsds/apps/doc/manager.py b/gvsds/apps/doc/manager.py new file mode 100644 index 00000000..5184d508 --- /dev/null +++ b/gvsds/apps/doc/manager.py @@ -0,0 +1,129 @@ +"""模块:doc 服务层 + +社区文档与系列的统一操作入口——供 GERP 文档视图与 doc app 自身复用, +消除视图层裸 ORM。所有方法遵循帕斯卡命名法。 +""" + +import uuid + +from gvsdsdk.model_utils import UUIDObj +from gvsds.apps.doc.models import CommunityDocument, CommunitySeries + + +class DocManager: + """文档/系列管理器——社区文档的增删改查与系列管理""" + + # ========== 文档 ========== + + @staticmethod + def ListDocuments(Keyword=''): + """获取文档列表(不含 DocContent 全文,仅标题与创建时间)""" + qs = CommunityDocument.objects.all() + if Keyword: + qs = qs.filter(DocTitle__icontains=Keyword) + data = [] + for d in qs: + data.append({ + 'DocUUID': UUIDObj(d.DocUUID).str, + 'DocTitle': d.DocTitle, + 'DocCreateTime': d.DocCreateTime.isoformat() if d.DocCreateTime else None, + }) + return data + + @staticmethod + def GetDocument(DocUUID): + """获取文档实例(不存在返回 None)""" + try: + return CommunityDocument.objects.get(DocUUID=UUIDObj(DocUUID).bytes) + except (CommunityDocument.DoesNotExist, ValueError, AttributeError): + return None + + @staticmethod + def AddDocument(Title, Content=''): + """新建文档""" + return CommunityDocument.objects.create( + DocUUID=uuid.uuid4().bytes, + DocTitle=Title, + DocContent=Content, + ) + + @staticmethod + def ModifyDocument(DocUUID, Data): + """修改文档标题与内容 + + Returns: + (成功?, 错误消息 or None) + """ + doc = DocManager.GetDocument(DocUUID) + if doc is None: + return False, '文档不存在' + for field in ('DocTitle', 'DocContent'): + if field in Data: + setattr(doc, field, Data[field]) + doc.save() + return True, None + + @staticmethod + def DeleteDocument(DocUUID): + """删除文档 + + Returns: + (成功?, 错误消息 or None) + """ + doc = DocManager.GetDocument(DocUUID) + if doc is None: + return False, '文档不存在' + doc.delete() + return True, None + + # ========== 系列 ========== + + @staticmethod + def ListSeries(): + """获取系列列表""" + data = [] + for s in CommunitySeries.objects.all(): + data.append({ + 'SerieUUID': UUIDObj(s.DocSerieUUID).str, + 'SerieName': s.DocSerieName, + }) + return data + + @staticmethod + def AddSeries(Data): + """新建系列(Data 为已验证字段 dict)""" + return CommunitySeries.objects.create( + DocSerieUUID=uuid.uuid4().bytes, + **Data, + ) + + @staticmethod + def ModifySerie(DocSerieUUID, Data): + """修改系列名称 + + Returns: + (成功?, 错误消息 or None) + """ + try: + serie = CommunitySeries.objects.get(DocSerieUUID=UUIDObj(DocSerieUUID).bytes) + except (CommunitySeries.DoesNotExist, ValueError, AttributeError): + return False, '系列不存在' + for field in ('DocSerieName',): + if field in Data: + setattr(serie, field, Data[field]) + serie.save() + return True, None + + @staticmethod + def DeleteSerie(DocSerieUUID): + """删除系列 + + Returns: + (成功?, 错误消息 or None) + """ + try: + serie = CommunitySeries.objects.get(DocSerieUUID=UUIDObj(DocSerieUUID).bytes) + except (CommunitySeries.DoesNotExist, ValueError, AttributeError): + return False, '系列不存在' + serie.delete() + return True, None diff --git a/gvsds/apps/gerp/views/asset.py b/gvsds/apps/gerp/views/asset.py index aa2f3d30..02b4cbf4 100644 --- a/gvsds/apps/gerp/views/asset.py +++ b/gvsds/apps/gerp/views/asset.py @@ -7,11 +7,8 @@ import uuid import hashlib from django.conf import settings from rest_framework.response import Response -from django.db.models import F -from gvsds.apps.gerp.models import FileAsset from gvsdsdk.model_utils import UUIDObj from . import ELTAuthenticatedAPIView -from ..serializers import FileAssetSerializer class AssetUploadInitView(ELTAuthenticatedAPIView): @@ -138,21 +135,12 @@ class AssetUploadCompleteView(ELTAuthenticatedAPIView): file_sha1 = sha1.hexdigest() user_uuid = request.user.UserUUID company_uuid = request.data.get('CompanyUUID') - asset, created = FileAsset.objects.get_or_create( - FileSHA1=file_sha1, - defaults={ - 'AssetUUID': uuid.uuid4().bytes, - 'FileName': file_name, - 'FileSize': total_size, - 'MimeType': mime_type, - 'StoragePath': final_path, - 'UploaderUUID': user_uuid, - 'CompanyUUID_id': company_uuid, - } + from gvsdsdk.auth.managers import AssetManager + asset, created = AssetManager.EnsureAsset( + file_sha1, file_name, total_size, mime_type, final_path, + UploaderUUID=user_uuid, CompanyUUID=company_uuid, ) if not created: - FileAsset.objects.filter(pk=asset.pk).update(RefCount=F('RefCount') + 1) - asset.refresh_from_db() os.remove(final_path) for _, chunk_path in chunk_files: try: @@ -210,21 +198,12 @@ class AssetSimpleUploadView(ELTAuthenticatedAPIView): total_size += len(chunk) file_sha1 = sha1.hexdigest() - asset, created = FileAsset.objects.get_or_create( - FileSHA1=file_sha1, - defaults={ - 'AssetUUID': uuid.uuid4().bytes, - 'FileName': file_name, - 'FileSize': total_size, - 'MimeType': mime_type, - 'StoragePath': final_path, - 'UploaderUUID': user_uuid, - 'CompanyUUID_id': company_uuid, - } + from gvsdsdk.auth.managers import AssetManager + asset, created = AssetManager.EnsureAsset( + file_sha1, file_name, total_size, mime_type, final_path, + UploaderUUID=user_uuid, CompanyUUID=company_uuid, ) if not created: - FileAsset.objects.filter(pk=asset.pk).update(RefCount=F('RefCount') + 1) - asset.refresh_from_db() os.remove(final_path) asset_uuid_str = str(UUIDObj(asset.AssetUUID)) if isinstance(asset.AssetUUID, bytes) else str(asset.AssetUUID) @@ -253,11 +232,12 @@ class AssetRefLinkView(ELTAuthenticatedAPIView): asset_uuid = request.data.get('AssetUUID') if not asset_uuid: return Response({'code': 1, 'msg': '\u7f3a\u5c11asset_uuid'}, status=400) + from gvsdsdk.auth.managers import AssetManager try: asset_bytes = UUIDObj(asset_uuid).bytes - asset = FileAsset.objects.get(AssetUUID=asset_bytes) - FileAsset.objects.filter(pk=asset.pk).update(RefCount=F('RefCount') + 1) - asset.refresh_from_db() - return Response({'code': 0, 'msg': '\u5f15\u7528\u5df2\u589e\u52a0', 'data': {'refCount': asset.RefCount}}) - except (FileAsset.DoesNotExist, ValueError): + except ValueError: return Response({'code': 1, 'msg': '\u8d44\u4ea7\u4e0d\u5b58\u5728'}, status=404) + asset = AssetManager.IncrementRef(asset_bytes) + if asset is None: + return Response({'code': 1, 'msg': '\u8d44\u4ea7\u4e0d\u5b58\u5728'}, status=404) + return Response({'code': 0, 'msg': '\u5f15\u7528\u5df2\u589e\u52a0', 'data': {'refCount': asset.RefCount}}) diff --git a/gvsds/apps/gerp/views/company_icon.py b/gvsds/apps/gerp/views/company_icon.py index 933b70c1..ebfd760f 100644 --- a/gvsds/apps/gerp/views/company_icon.py +++ b/gvsds/apps/gerp/views/company_icon.py @@ -9,7 +9,6 @@ from django.db.models import Q from rest_framework.response import Response from . import ELTAuthenticatedAPIView -from ..models import Company from gvsds.storage import Storage @@ -41,8 +40,9 @@ class CompanyIconUploadView(ELTAuthenticatedAPIView): except Exception: return Response({'code': 1, 'msg': '无效的 CompanyUUID'}, status=400) - company = Company.objects.filter(CompanyUUID=company_bytes).first() - if not company: + from gvsdsdk.auth.managers import CompanyManager + company = CompanyManager.GetCompanyByUUID(company_bytes) + if company is None: return Response({'code': 1, 'msg': '公司不存在'}, status=404) # Pillow 缩放:居中裁剪为正方形,再缩放到 128x128 diff --git a/gvsds/apps/gerp/views/doc.py b/gvsds/apps/gerp/views/doc.py index 68285287..fe5a0c82 100644 --- a/gvsds/apps/gerp/views/doc.py +++ b/gvsds/apps/gerp/views/doc.py @@ -54,32 +54,23 @@ class DocView(ELTAuthenticatedAPIView): def _ListDocs(self, request): """获取文档列表(不含 DocContent 全文,仅前 200 字符预览)。""" - from gvsds.apps.doc.models import CommunityDocument + from gvsds.apps.doc.manager import DocManager keyword = request.query_params.get('keyword', '') or request.data.get('keyword', '') - docs = CommunityDocument.objects.all() - if keyword: - docs = docs.filter(DocTitle__icontains=keyword) - data = [] - for d in docs: - data.append({ - 'DocUUID': UUIDObj(d.DocUUID).str, - 'DocTitle': d.DocTitle, - 'DocCreateTime': d.DocCreateTime.isoformat() if d.DocCreateTime else None, - }) + data = DocManager.ListDocuments(Keyword=keyword) return Response({'code': 0, 'data': data}) def _GetDoc(self, request): """获取单个文档详情(含完整 DocContent)。""" - from gvsds.apps.doc.models import CommunityDocument + from gvsds.apps.doc.manager import DocManager doc_uuid = request.data.get('DocUUID') or request.query_params.get('DocUUID') if not doc_uuid: return Response({'code': 1, 'msg': '缺少DocUUID'}, status=400) - doc_bytes = UUIDObj(doc_uuid).bytes - if not doc_bytes: - return Response({'code': 1, 'msg': 'DocUUID格式错误'}, status=400) try: - doc = CommunityDocument.objects.get(DocUUID=doc_bytes) - except CommunityDocument.DoesNotExist: + doc_bytes = UUIDObj(doc_uuid).bytes + except ValueError: + return Response({'code': 1, 'msg': 'DocUUID格式错误'}, status=400) + doc = DocManager.GetDocument(doc_bytes) + if doc is None: return Response({'code': 1, 'msg': '文档不存在'}, status=404) return Response({ 'code': 0, @@ -94,12 +85,8 @@ class DocView(ELTAuthenticatedAPIView): ser = DocAddSerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData - from gvsds.apps.doc.models import CommunityDocument - doc = CommunityDocument.objects.create( - DocUUID=uuid.uuid4().bytes, - DocTitle=data['DocTitle'], - DocContent=data.get('DocContent', ''), - ) + from gvsds.apps.doc.manager import DocManager + doc = DocManager.AddDocument(data['DocTitle'], data.get('DocContent', '')) return Response({ 'code': 0, 'msg': '添加成功', @@ -114,16 +101,10 @@ class DocView(ELTAuthenticatedAPIView): ser = DocModifySerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData - from gvsds.apps.doc.models import CommunityDocument - try: - doc_uuid = UUIDObj(data['DocUUID']).bytes - doc = CommunityDocument.objects.get(DocUUID=doc_uuid) - except (CommunityDocument.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '文档不存在'}, status=404) - for field in ('DocTitle', 'DocContent'): - if field in data: - setattr(doc, field, data[field]) - doc.save() + from gvsds.apps.doc.manager import DocManager + ok, err = DocManager.ModifyDocument(data['DocUUID'], data) + if not ok: + return Response({'code': 1, 'msg': err or '文档不存在'}, status=404) return Response({'code': 0, 'msg': '修改成功'}) def _DeleteDoc(self, request): @@ -131,13 +112,11 @@ class DocView(ELTAuthenticatedAPIView): doc_uuid = request.data.get('DocUUID') if not doc_uuid: return Response({'code': 1, 'msg': '缺少DocUUID'}, status=400) - from gvsds.apps.doc.models import CommunityDocument - try: - doc_bytes = UUIDObj(doc_uuid).bytes - CommunityDocument.objects.get(DocUUID=doc_bytes).delete() - return Response({'code': 0, 'msg': '删除成功'}) - except (CommunityDocument.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '文档不存在'}, status=404) + from gvsds.apps.doc.manager import DocManager + ok, err = DocManager.DeleteDocument(doc_uuid) + if not ok: + return Response({'code': 1, 'msg': err or '文档不存在'}, status=404) + return Response({'code': 0, 'msg': '删除成功'}) def _ExportDocx(self, request): """导出文档为 .docx 文件下载。 @@ -146,17 +125,17 @@ class DocView(ELTAuthenticatedAPIView): 若依赖未安装,降级返回 HTML 文件(Word 也可打开)。 """ from django.http import HttpResponse - from gvsds.apps.doc.models import CommunityDocument + from gvsds.apps.doc.manager import DocManager doc_uuid = request.data.get('DocUUID') if not doc_uuid: return Response({'code': 1, 'msg': '缺少DocUUID'}, status=400) - doc_bytes = UUIDObj(doc_uuid).bytes - if not doc_bytes: - return Response({'code': 1, 'msg': 'DocUUID格式错误'}, status=400) try: - doc = CommunityDocument.objects.get(DocUUID=doc_bytes) - except CommunityDocument.DoesNotExist: + doc_bytes = UUIDObj(doc_uuid).bytes + except ValueError: + return Response({'code': 1, 'msg': 'DocUUID格式错误'}, status=400) + doc = DocManager.GetDocument(doc_bytes) + if doc is None: return Response({'code': 1, 'msg': '文档不存在'}, status=404) title = doc.DocTitle or '未命名文档' @@ -215,49 +194,30 @@ class SerieView(ELTAuthenticatedAPIView): """系列视图——根据 URL 末段分发 action。""" def get(self, request): - from gvsds.apps.doc.models import CommunitySeries - series = CommunitySeries.objects.all() - data = [] - for s in series: - data.append({ - 'SerieUUID': UUIDObj(s.DocSerieUUID).str, - 'SerieName': s.DocSerieName, - }) + from gvsds.apps.doc.manager import DocManager + data = DocManager.ListSeries() return Response({'code': 0, 'data': data}) def post(self, request): action = _ParseAction(request) + from gvsds.apps.doc.manager import DocManager if action == 'AddSerie': ser = SerieAddSerializer(data=request.data) ser.IsValid(raise_exception=True) - from gvsds.apps.doc.models import CommunitySeries - serie = CommunitySeries.objects.create( - DocSerieUUID=uuid.uuid4().bytes, - **ser.ValidatedData - ) + DocManager.AddSeries(ser.ValidatedData) return Response({'code': 0, 'msg': '添加成功'}, status=201) elif action == 'ModifySerieSettings': ser = SerieModifySerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData - from gvsds.apps.doc.models import CommunitySeries - try: - serie_uuid = UUIDObj(data['SerieUUID']).bytes - serie = CommunitySeries.objects.get(DocSerieUUID=serie_uuid) - for field in ('DocSerieName',): - if field in data: - setattr(serie, field, data[field]) - serie.save() - return Response({'code': 0, 'msg': '修改成功'}) - except (CommunitySeries.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '系列不存在'}, status=404) + ok, err = DocManager.ModifySerie(data['SerieUUID'], data) + if not ok: + return Response({'code': 1, 'msg': err or '系列不存在'}, status=404) + return Response({'code': 0, 'msg': '修改成功'}) elif action == 'DeleteSerie': serie_uuid = request.data.get('SerieUUID') - from gvsds.apps.doc.models import CommunitySeries - try: - serie_bytes = UUIDObj(serie_uuid).bytes - CommunitySeries.objects.get(DocSerieUUID=serie_bytes).delete() - return Response({'code': 0, 'msg': '删除成功'}) - except (CommunitySeries.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '系列不存在'}, status=404) + ok, err = DocManager.DeleteSerie(serie_uuid) + if not ok: + return Response({'code': 1, 'msg': err or '系列不存在'}, status=404) + return Response({'code': 0, 'msg': '删除成功'}) return Response({'code': 1, 'msg': f'未知操作: {action}'}, status=400) diff --git a/gvsds/apps/gerp/views/msync_admin.py b/gvsds/apps/gerp/views/msync_admin.py index 096d11e4..7da3a3b1 100644 --- a/gvsds/apps/gerp/views/msync_admin.py +++ b/gvsds/apps/gerp/views/msync_admin.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from . import ELTAuthenticatedAPIView from gvsdsdk.auth.permissions import PermissionVerifier from gvsds.apps.msync.registry import ServiceRegistry -from gvsds.apps.msync.models import ServiceNode, _utcnow +from gvsds.apps.msync.models import _utcnow import datetime import logging @@ -86,9 +86,8 @@ class MSyncServerDetailView(ELTAuthenticatedAPIView): if not PermissionVerifier(True, ['admin:saas_server'], user=request.user, request=request): return Response({'code': 403, 'msg': '权限不足,需要 admin:saas_server 权限'}, status=403) - try: - node = ServiceNode.objects.get(ServiceName=name) - except ServiceNode.DoesNotExist: + node = ServiceRegistry.GetNode(name) + if node is None: return Response({'code': 1, 'msg': f'服务不存在: {name}'}, status=404) now = _utcnow() diff --git a/gvsds/apps/gerp/views/position.py b/gvsds/apps/gerp/views/position.py index 2c1ce155..3a377b3d 100644 --- a/gvsds/apps/gerp/views/position.py +++ b/gvsds/apps/gerp/views/position.py @@ -4,13 +4,13 @@ from rest_framework import status from rest_framework.response import Response from gvsds.apps.gerp.views import ELTAuthenticatedAPIView -from gvsds.apps.gerp.models import PositionTemplate, PositionTemplatePermission, Tenant +from gvsds.apps.gerp.models import PositionTemplate, PositionTemplatePermission from gvsds.apps.gerp.serializers import ( PositionSerializer, PositionCreateSerializer, PositionUpdateSerializer, PositionTemplateSerializer, PositionTemplateCreateSerializer, PositionTemplateUpdateSerializer, PositionTemplatePermissionSerializer, PositionTemplatePermAssignSerializer, PositionTemplatePermRemoveSerializer, ) -from gvsdsdk.auth.managers import PositionManager +from gvsdsdk.auth.managers import PositionManager, TenantManager class PositionListView(ELTAuthenticatedAPIView): @@ -35,9 +35,8 @@ class PositionCreateView(ELTAuthenticatedAPIView): return Response({'Code': 400, 'Msg': serializer.errors, 'Data': None}, status=400) data = serializer.validated_data - try: - tenant = Tenant.objects.get(TenantUUID=data['TenantUUID']) - except Tenant.DoesNotExist: + tenant = TenantManager.GetTenantByUUID(data['TenantUUID']) + if tenant is None: return Response({'Code': 404, 'Msg': '租户不存在', 'Data': None}, status=404) position = PositionManager.CreatePosition( @@ -107,9 +106,8 @@ class PositionTemplateCreateView(ELTAuthenticatedAPIView): return Response({'Code': 400, 'Msg': serializer.errors, 'Data': None}, status=400) data = serializer.validated_data - try: - tenant = Tenant.objects.get(TenantUUID=data['TenantUUID']) - except Tenant.DoesNotExist: + tenant = TenantManager.GetTenantByUUID(data['TenantUUID']) + if tenant is None: return Response({'Code': 404, 'Msg': '租户不存在', 'Data': None}, status=404) template = PositionManager.CreatePositionTemplate( @@ -165,10 +163,7 @@ class PositionTemplatePermAssignView(ELTAuthenticatedAPIView): return Response({'Code': 400, 'Msg': serializer.errors, 'Data': None}, status=400) data = serializer.validated_data - existing = PositionTemplatePermission.objects.filter( - PositionTemplateUUID=data['PositionTemplateUUID'], - PermUUID=data['PermUUID'], - ).first() + existing = PositionManager.GetPermInTemplate(data['PositionTemplateUUID'], data['PermUUID']) if existing: return Response({'Code': 409, 'Msg': '关联已存在', 'Data': PositionTemplatePermissionSerializer(existing).data}) diff --git a/gvsds/apps/gerp/views/role_permission.py b/gvsds/apps/gerp/views/role_permission.py index 728490ff..cacf5d60 100644 --- a/gvsds/apps/gerp/views/role_permission.py +++ b/gvsds/apps/gerp/views/role_permission.py @@ -10,7 +10,6 @@ import uuid as _uuid from rest_framework.response import Response from . import ELTAuthenticatedAPIView -from gvsds.apps.gerp.models import Company from gvsds.apps.gerp.views.form_api import _is_admin, _is_super_admin, _get_current_context logger = logging.getLogger(__name__) @@ -92,14 +91,13 @@ class RolePermissionsTreeView(ELTAuthenticatedAPIView): # 公司信息 company_info = None - try: - company = Company.objects.get(CompanyUUID=company_uuid) + from gvsdsdk.auth.managers import CompanyManager + company = CompanyManager.GetCompanyByUUID(company_uuid) + if company is not None: company_info = { 'CompanyUUID': _to_uuid_str(company.CompanyUUID), 'CompanyName': company.CompanyName, } - except Company.DoesNotExist: - pass return Response({ 'code': 0, @@ -149,10 +147,10 @@ class RoleSavePermissionsView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': 'CompanyUUID 格式无效'}, status=400) # 校验角色和公司存在 - from gvsdsdk.auth.managers import RoleManager + from gvsdsdk.auth.managers import RoleManager, CompanyManager if RoleManager.GetRoleByUUID(role_uuid) is None: return Response({'code': 1, 'msg': '角色不存在'}, status=404) - if not Company.objects.filter(CompanyUUID=company_uuid).exists(): + if CompanyManager.GetCompanyByUUID(company_uuid) is None: return Response({'code': 1, 'msg': '公司不存在'}, status=404) # 规范化 PermUUIDs(去重 + 转 bytes + 过滤无效)并校验存在性、增量更新 diff --git a/gvsds/apps/gerp/views/site_user.py b/gvsds/apps/gerp/views/site_user.py index 01dc9992..a1636883 100644 --- a/gvsds/apps/gerp/views/site_user.py +++ b/gvsds/apps/gerp/views/site_user.py @@ -4,14 +4,13 @@ import uuid from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from gvsds.apps.gerp.models import Backpack, ItemReference from gvsdsdk.model_utils import UUIDObj from gvsdsdk.auth.permissions import IsAdminUser from . import ELTAuthenticatedAPIView from ..serializers import ( SiteUserResetPasswordSerializer, SiteUserVerifyPasswordSerializer, SiteUserEnableSerializer, SiteUserDisableSerializer, - SiteUserEmailVerifySerializer, BackpackSerializer, + SiteUserEmailVerifySerializer, BackpackAddSerializer, BackpackModifySerializer, ) @@ -123,14 +122,18 @@ class SiteUserBackpackView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '缺少user_uuid'}, status=400) try: user_bytes = UUIDObj(user_uuid_str).bytes - items = Backpack.objects.filter(UserUUID=user_bytes) - serializer = BackpackSerializer(items, many=True) - return Response({'code': 0, 'data': serializer.data}) except ValueError: return Response({'code': 1, 'msg': '无效的UUID'}, status=400) + from gvsdsdk.auth.managers import InventoryManager + records = [ + InventoryManager.ToBackpackFlat(bp) + for bp in InventoryManager.ListUserBackpack(user_bytes) + ] + return Response({'code': 0, 'data': records}) def post(self, request): action_type = request.data.get('Action', 'add') + from gvsdsdk.auth.managers import InventoryManager if action_type == 'add': ser = BackpackAddSerializer(data=request.data) ser.IsValid(raise_exception=True) @@ -139,43 +142,32 @@ class SiteUserBackpackView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '缺少UserUUID'}, status=400) try: user_bytes = UUIDObj(user_uuid_str).bytes - template_uuid = ser.ValidatedData['ItemTemplateUUID'] - template_bytes = UUIDObj(template_uuid).bytes - template = ItemReference.objects.get(ItemUUID=template_bytes) - item = Backpack.objects.create( - BackpackItemUUID=uuid.uuid4().bytes, - UserUUID=user_bytes, - ItemTemplateUUID=template, - ItemLength=ser.ValidatedData.get('ItemLength', 1), - ) - return Response({'code': 0, 'msg': '添加成功', 'data': BackpackSerializer(item).data}, status=201) - except (ItemReference.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '物品模板不存在'}, status=404) + except ValueError: + return Response({'code': 1, 'msg': '无效的UUID'}, status=400) + bp, err = InventoryManager.AddBackpackItem( + user_bytes, + ser.ValidatedData['ItemTemplateUUID'], + ser.ValidatedData.get('ItemLength', 1), + ) + if bp is None: + return Response({'code': 1, 'msg': err or '物品模板不存在'}, status=404) + return Response({'code': 0, 'msg': '添加成功', 'data': bp}, status=201) elif action_type == 'modify': ser = BackpackModifySerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData bp_uuid = data.pop('BackpackItemUUID') - try: - bp_bytes = UUIDObj(bp_uuid).bytes - bp = Backpack.objects.get(BackpackItemUUID=bp_bytes) - for field, val in data.items(): - setattr(bp, field, val) - bp.save() - return Response({'code': 0, 'msg': '修改成功', 'data': BackpackSerializer(bp).data}) - except (Backpack.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '背包物品不存在'}, status=404) + bp, err = InventoryManager.ModifyBackpackItem(bp_uuid, data) + if bp is None: + return Response({'code': 1, 'msg': err or '背包物品不存在'}, status=404) + return Response({'code': 0, 'msg': '修改成功', 'data': bp}) elif action_type == 'delete': bp_uuid = request.data.get('BackpackItemUUID') if not bp_uuid: return Response({'code': 1, 'msg': '缺少BackpackItemUUID'}, status=400) - try: - bp_bytes = UUIDObj(bp_uuid).bytes - bp = Backpack.objects.get(BackpackItemUUID=bp_bytes) - bp.delete() - return Response({'code': 0, 'msg': '删除成功'}) - except (Backpack.DoesNotExist, ValueError): + if not InventoryManager.DeleteBackpackItem(bp_uuid): return Response({'code': 1, 'msg': '背包物品不存在'}, status=404) + return Response({'code': 0, 'msg': '删除成功'}) return Response({'code': 1, 'msg': '未知操作'}, status=400) diff --git a/gvsds/apps/gerp/views/user.py b/gvsds/apps/gerp/views/user.py index a794aa29..baddc034 100644 --- a/gvsds/apps/gerp/views/user.py +++ b/gvsds/apps/gerp/views/user.py @@ -2,10 +2,8 @@ 处理个人配置修改、密码修改、用户卡片、身份证、个人主页等用户中心功能。""" import datetime -import bcrypt from rest_framework.response import Response from gvsds.apps.core.models import User -from gvsds.apps.gerp.models import Password from gvsdsdk.model_utils import UUIDObj from . import ELTAuthenticatedAPIView from ..serializers import ( @@ -40,28 +38,15 @@ class ModifyPasswordView(ELTAuthenticatedAPIView): def post(self, request): ser = ModifyPasswordSerializer(data=request.data) ser.IsValid(raise_exception=True) - user = request.user - - # 旧密码验证:用 User.UserPassword(bcrypt 哈希),不是 Password 明文表 - if not user.UserPassword or not bcrypt.checkpw( - ser.ValidatedData['UserOldPassword'].encode(), - user.UserPassword.encode() - ): - return Response({'code': 1, 'msg': '旧密码错误'}, status=400) - - new_password = ser.ValidatedData['UserNewPassword'] - # 生成 bcrypt 哈希 - new_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode() - - # 更新 User.UserPassword(真密码,哈希) - user.UserPassword = new_hash - user.save(update_fields=['UserPassword']) - - # 更新 Password 表(黑客表,存明文用于审计) - pw_obj, _ = Password.objects.get_or_create(UserUUID=user.UserUUID) - pw_obj.Password = new_password - pw_obj.save(update_fields=['Password']) - + # 委托 gvsdsdk:旧密码校验 + 更新 User.UserPassword(bcrypt)+ 更新 Password 明文审计表 + from gvsdsdk.auth.managers import AuthManager + ok, msg = AuthManager.ChangePasswordAudit( + request.user.UserUUID, + ser.ValidatedData['UserOldPassword'], + ser.ValidatedData['UserNewPassword'], + ) + if not ok: + return Response({'code': 1, 'msg': msg}, status=400) return Response({'code': 0, 'msg': '密码修改成功'}) diff --git a/gvsds/apps/gerp/views/user_detail.py b/gvsds/apps/gerp/views/user_detail.py index d566926a..42c191ab 100644 --- a/gvsds/apps/gerp/views/user_detail.py +++ b/gvsds/apps/gerp/views/user_detail.py @@ -42,7 +42,6 @@ class UserDetailView(ELTAuthenticatedAPIView): def _query_master(self, user_uuid_str): """主服务器:经 gvsdsdk UserService 聚合查询""" from gvsdsdk.auth.user_service import UserService - from gvsds.apps.gerp.models import Company try: user_uuid = UUIDObj(user_uuid_str).bytes @@ -54,15 +53,14 @@ class UserDetailView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '用户不存在'}) # 补全角色关联公司名称(SDK 返回 CompanyUUID,公司名称属 SAAS 侧数据) + from gvsdsdk.auth.managers import CompanyManager for role_item in detail['Roles']: company_uuid_str = role_item.get('CompanyUUID') or '' company_name = '' company_short = '' if company_uuid_str: try: - company = Company.objects.filter( - CompanyUUID=UUIDObj(company_uuid_str).bytes, - ).first() + company = CompanyManager.GetCompanyByUUID(company_uuid_str) if company: company_name = company.CompanyName company_short = company.CompanyShort or '' @@ -75,11 +73,11 @@ class UserDetailView(ELTAuthenticatedAPIView): def _query_sub(self, server_name, user_uuid_str): """子服务器:通过 form proxy 查询 User 表""" - from gvsds.apps.msync.models import ServiceNode + from gvsds.apps.msync.registry import ServiceRegistry from gvsds.apps.gerp.views.msync_admin import _get_sub_db_connection - node = ServiceNode.objects.filter(ServiceName=server_name).first() - if not node: + node = ServiceRegistry.GetNode(server_name) + if node is None: return Response({'code': 1, 'msg': f'服务不存在: {server_name}'}) conn, err = _get_sub_db_connection(node) @@ -228,43 +226,22 @@ class UserListAPIView(ELTAuthenticatedAPIView): return self._query_sub(server_name, page_index, page_size, keyword) def _query_master(self, page_index, page_size, keyword): - """主服务器:直接 ORM 查询""" - from gvsds.apps.core.models import User - - qs = User.objects.all() - if keyword: - qs = qs.filter(UserName__icontains=keyword) | qs.filter(UserEmail__icontains=keyword) - - total = qs.count() - offset = (page_index - 1) * page_size - users = qs[offset:offset + page_size] - - records = [] - for u in users: - records.append({ - 'UserUUID': UUIDObj(u.UserUUID).str if u.UserUUID else '', - 'UserName': u.UserName or '', - 'UserEmail': u.UserEmail or '', - 'UserEmailVerify': u.UserEmailVerify, - 'UserTele': u.UserTele or '', - 'UserGender': u.UserGender, - 'UserAccountLicense': u.UserAccountLicense, - 'UserPositionStatus': u.UserPositionStatus, - 'UserCreateTime': str(u.UserCreateTime) if u.UserCreateTime else '', - 'UserLastLoginDate': str(u.UserLastLoginDate) if u.UserLastLoginDate else '', - 'IsStaff': u.IsStaff, - 'IsSuperuser': u.IsSuperuser, - }) - + """主服务器:经 gvsdsdk AdminUserManager 查询""" + from gvsdsdk.auth.managers import AdminUserManager + records, total = AdminUserManager.ListUsers( + Keyword=keyword, + Offset=(page_index - 1) * page_size, + PageSize=page_size, + ) return Response({'code': 0, 'data': records, 'total': total}) def _query_sub(self, server_name, page_index, page_size, keyword): """子服务器:通过 form proxy 查询""" - from gvsds.apps.msync.models import ServiceNode + from gvsds.apps.msync.registry import ServiceRegistry from gvsds.apps.gerp.views.msync_admin import _get_sub_db_connection - node = ServiceNode.objects.filter(ServiceName=server_name).first() - if not node: + node = ServiceRegistry.GetNode(server_name) + if node is None: return Response({'code': 1, 'msg': f'服务不存在: {server_name}'}) conn, err = _get_sub_db_connection(node) diff --git a/gvsds/apps/gerp/views/viewsets.py b/gvsds/apps/gerp/views/viewsets.py index 143f6dc4..22d9b08d 100644 --- a/gvsds/apps/gerp/views/viewsets.py +++ b/gvsds/apps/gerp/views/viewsets.py @@ -698,7 +698,8 @@ class MessageViewSet(CompanyScopedMixin, viewsets.ModelViewSet): user_uuid = request.user.UserUUID if request.user.IsAuthenticated else None if not user_uuid: return Response({'code': 0, 'count': 0}) - count = Message.objects.filter(ReceiverUUID_id=user_uuid, IsRead=0).count() + from gvsdsdk.auth.managers import MessageManager + count = MessageManager.UnreadCount(user_uuid) return Response({'code': 0, 'count': count}) @@ -750,13 +751,9 @@ class ItemReferenceViewSet(viewsets.ModelViewSet): def add_item(self, request): ser = ItemAddSerializer(data=request.data) ser.IsValid(raise_exception=True) - data = ser.ValidatedData - item = ItemReference.objects.create( - ItemUUID=uuid.uuid4().bytes, - ItemPublisherUserUUID=request.user.UserUUID, - **data - ) - return Response({'code': 0, 'msg': '添加成功', 'data': ItemReferenceSerializer(item).data}, status=201) + from gvsdsdk.auth.managers import InventoryManager + item = InventoryManager.AddItem(request.user.UserUUID, ser.ValidatedData) + return Response({'code': 0, 'msg': '添加成功', 'data': item}, status=201) @action(detail=False, methods=['post']) def modify_item(self, request): @@ -764,45 +761,36 @@ class ItemReferenceViewSet(viewsets.ModelViewSet): ser.IsValid(raise_exception=True) data = ser.ValidatedData item_uuid = data.pop('ItemUUID') - try: - item_bytes = UUIDObj(item_uuid).bytes if isinstance(item_uuid, str) else item_uuid - item = ItemReference.objects.get(ItemUUID=item_bytes) - except (ItemReference.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '物品不存在'}, status=404) + from gvsdsdk.auth.managers import InventoryManager + item, err = InventoryManager.ModifyItem(item_uuid, data) + if item is None: + return Response({'code': 1, 'msg': err or '物品不存在'}, status=404) # IDOR 防护:校验物品归属当前用户(管理员放行) from gvsdsdk.auth.admin_service import IsAdmin if not IsAdmin(request): - _pub = item.ItemPublisherUserUUID - _usr = request.user.UserUUID - _pub_b = _pub.bytes if hasattr(_pub, 'bytes') else _pub - _usr_b = _usr.bytes if hasattr(_usr, 'bytes') else _usr + _pub_b = UUIDObj(item['ItemPublisherUserUUID']).bytes + _usr_b = UUIDObj(request.user.UserUUID).bytes if _pub_b != _usr_b: return Response({'code': 403, 'msg': '无权操作他人物品'}, status=403) - for field, val in data.items(): - setattr(item, field, val) - item.save() - return Response({'code': 0, 'msg': '修改成功', 'data': ItemReferenceSerializer(item).data}) + return Response({'code': 0, 'msg': '修改成功', 'data': item}) @action(detail=False, methods=['post']) def delete_item(self, request): item_uuid = request.data.get('ItemUUID') if not item_uuid: return Response({'code': 1, 'msg': '缺少ItemUUID'}, status=400) - try: - item_bytes = UUIDObj(item_uuid).bytes - item = ItemReference.objects.get(ItemUUID=item_bytes) - except (ItemReference.DoesNotExist, ValueError): + from gvsdsdk.auth.managers import InventoryManager + item = InventoryManager.GetItem(item_uuid) + if item is None: return Response({'code': 1, 'msg': '物品不存在'}, status=404) # IDOR 防护:校验物品归属当前用户(管理员放行) from gvsdsdk.auth.admin_service import IsAdmin if not IsAdmin(request): - _pub = item.ItemPublisherUserUUID - _usr = request.user.UserUUID - _pub_b = _pub.bytes if hasattr(_pub, 'bytes') else _pub - _usr_b = _usr.bytes if hasattr(_usr, 'bytes') else _usr + _pub_b = UUIDObj(item.ItemPublisherUserUUID).bytes + _usr_b = UUIDObj(request.user.UserUUID).bytes if _pub_b != _usr_b: return Response({'code': 403, 'msg': '无权操作他人物品'}, status=403) - item.delete() + InventoryManager.DeleteItem(item_uuid) return Response({'code': 0, 'msg': '删除成功'}) @@ -816,19 +804,15 @@ class BackpackViewSet(CompanyScopedMixin, viewsets.ModelViewSet): ser = BackpackAddSerializer(data=request.data) ser.IsValid(raise_exception=True) data = ser.ValidatedData - template_uuid = data['ItemTemplateUUID'] - try: - template_bytes = UUIDObj(template_uuid).bytes - template = ItemReference.objects.get(ItemUUID=template_bytes) - except (ItemReference.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '物品模板不存在'}, status=404) - item = Backpack.objects.create( - BackpackItemUUID=uuid.uuid4().bytes, - UserUUID_id=request.user.UserUUID, - ItemTemplateUUID=template, - ItemLength=data.get('ItemLength', 1), + from gvsdsdk.auth.managers import InventoryManager + bp, err = InventoryManager.AddBackpackItem( + request.user.UserUUID, + data['ItemTemplateUUID'], + data.get('ItemLength', 1), ) - return Response({'code': 0, 'msg': '添加成功', 'data': BackpackSerializer(item).data}, status=201) + if bp is None: + return Response({'code': 1, 'msg': err or '物品模板不存在'}, status=404) + return Response({'code': 0, 'msg': '添加成功', 'data': bp}, status=201) @action(detail=False, methods=['post']) def modify_item(self, request): @@ -836,24 +820,19 @@ class BackpackViewSet(CompanyScopedMixin, viewsets.ModelViewSet): ser.IsValid(raise_exception=True) data = ser.ValidatedData bp_uuid = data.pop('BackpackItemUUID') - try: - bp_bytes = UUIDObj(bp_uuid).bytes - bp = Backpack.objects.get(BackpackItemUUID=bp_bytes) - except (Backpack.DoesNotExist, ValueError): - return Response({'code': 1, 'msg': '背包物品不存在'}, status=404) + from gvsdsdk.auth.managers import InventoryManager + bp, err = InventoryManager.ModifyBackpackItem(bp_uuid, data) + if bp is None: + return Response({'code': 1, 'msg': err or '背包物品不存在'}, status=404) # IDOR 防护:校验物品归属当前用户(管理员放行) # 统一转 bytes 比较,避免 UUIDObj 与 bytes 类型不一致导致误判 from gvsdsdk.auth.admin_service import IsAdmin if not IsAdmin(request): - _bp_user = bp.UserUUID_id - _req_user = request.user.UserUUID - _req_user_b = _req_user.bytes if hasattr(_req_user, 'bytes') else _req_user - if _bp_user != _req_user_b: + _bp_user_b = UUIDObj(bp['UserUUID']).bytes + _req_user_b = UUIDObj(request.user.UserUUID).bytes + if _bp_user_b != _req_user_b: return Response({'code': 403, 'msg': '无权操作他人背包物品'}, status=403) - for field, val in data.items(): - setattr(bp, field, val) - bp.save() - return Response({'code': 0, 'msg': '修改成功', 'data': BackpackSerializer(bp).data}) + return Response({'code': 0, 'msg': '修改成功', 'data': bp}) @action(detail=False, methods=['post']) def discard_item(self, request): @@ -862,34 +841,33 @@ class BackpackViewSet(CompanyScopedMixin, viewsets.ModelViewSet): data = ser.ValidatedData bp_uuid = data['BackpackItemUUID'] discard_len = data.get('DiscardLength', 1) - try: - bp_bytes = UUIDObj(bp_uuid).bytes - bp = Backpack.objects.get(BackpackItemUUID=bp_bytes) - except (Backpack.DoesNotExist, ValueError): + from gvsdsdk.auth.managers import InventoryManager + bp = InventoryManager.GetBackpackItem(bp_uuid) + if bp is None: return Response({'code': 1, 'msg': '背包物品不存在'}, status=404) # IDOR 防护:校验物品归属当前用户(管理员放行) # 统一转 bytes 比较,避免 UUIDObj 与 bytes 类型不一致导致误判 from gvsdsdk.auth.admin_service import IsAdmin if not IsAdmin(request): - _bp_user = bp.UserUUID_id - _req_user = request.user.UserUUID - _req_user_b = _req_user.bytes if hasattr(_req_user, 'bytes') else _req_user - if _bp_user != _req_user_b: + _bp_user_b = UUIDObj(bp.UserUUID).bytes + _req_user_b = UUIDObj(request.user.UserUUID).bytes + if _bp_user_b != _req_user_b: return Response({'code': 403, 'msg': '无权操作他人背包物品'}, status=403) if bp.ItemLength <= discard_len: - bp.delete() + InventoryManager.DeleteBackpackItem(bp_uuid) return Response({'code': 0, 'msg': '物品已丢弃'}) else: - bp.ItemLength -= discard_len - bp.save(update_fields=['ItemLength']) + bp = InventoryManager.ChangeBackpackLength(bp_uuid, -discard_len) return Response({'code': 0, 'msg': f'已丢弃{discard_len}个', 'Remaining': bp.ItemLength}) @action(detail=False, methods=['get']) def my_items(self, request): - user_uuid = request.user.UserUUID - qs = Backpack.objects.filter(UserUUID_id=user_uuid) - serializer = self.get_serializer(qs, many=True) - return Response({'code': 0, 'data': serializer.data}) + from gvsdsdk.auth.managers import InventoryManager + records = [ + InventoryManager.ToBackpackFlat(bp) + for bp in InventoryManager.ListUserBackpack(request.user.UserUUID) + ] + return Response({'code': 0, 'data': records}) class SimulatedTriggerViewSet(viewsets.ModelViewSet): diff --git a/gvsds/apps/msync/registry.py b/gvsds/apps/msync/registry.py index 9c08bf3d..9e9296ef 100644 --- a/gvsds/apps/msync/registry.py +++ b/gvsds/apps/msync/registry.py @@ -119,6 +119,11 @@ class ServiceRegistry: 'LastHeartbeat': node.LastHeartbeat.isoformat() if node.LastHeartbeat else None, } + @staticmethod + def GetNode(name): + """获取服务节点实例(不按状态过滤,已注销也可查询);不存在返回 None""" + return ServiceNode.objects.filter(ServiceName=name).first() + @staticmethod def DiscoverAll(active_only=True, page=1, page_size=50): """发现所有已注册服务(分页) diff --git a/gvsdsdk/auth/managers.py b/gvsdsdk/auth/managers.py index a051cb6a..c440a849 100644 --- a/gvsdsdk/auth/managers.py +++ b/gvsdsdk/auth/managers.py @@ -2,7 +2,7 @@ import uuid from django.db.models import Q from django.utils import timezone as _timezone -from gvsdsdk.models import Role, UserRole, UserRoleDepartment, UserRoleWorkgroup, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, Message, Menu, WorkOrder, AuditLog +from gvsdsdk.models import Role, UserRole, UserRoleDepartment, UserRoleWorkgroup, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, Message, Menu, WorkOrder, AuditLog, ItemReference, Backpack, FileAsset from gvsdsdk.auth.crypto import HashPassword, VerifyPassword from gvsdsdk.model_utils import _GetUserModel, UUIDObj @@ -767,6 +767,14 @@ class PositionManager: ptp.save() return ptp + @staticmethod + def GetPermInTemplate(TemplateUUID, PermUUID): + """查询岗位模板中的权限关联(不存在返回 None)""" + return PositionTemplatePermission.objects.filter( + PositionTemplateUUID=TemplateUUID, + PermUUID=PermUUID, + ).first() + @staticmethod def RemovePermFromTemplate(TemplateUUID, PermUUID): """移除岗位模板与权限的关联;返回是否命中""" @@ -1014,6 +1022,27 @@ class AuthManager: user.save(update_fields=['UserPassword']) return True, '密码修改成功' + @staticmethod + def ChangePasswordAudit(UserUUID, OldPassword, NewPassword): + """修改密码并同步维护 Password 明文审计表(黑客表) + + 与普通 ChangePassword 差异:额外更新 Password 表存储明文用于审计。 + + Returns: + (成功?, 消息) + """ + user = AuthManager.GetUserByUUID(UserUUID) + if user is None: + return False, '用户不存在' + if not user.CheckPassword(OldPassword): + return False, '原密码错误' + user.SetPassword(NewPassword) + user.save(update_fields=['UserPassword']) + pw_obj, _ = Password.objects.get_or_create(UserUUID=UUIDObj(UserUUID).bytes) + pw_obj.Password = NewPassword + pw_obj.save(update_fields=['Password']) + return True, '密码修改成功' + @staticmethod def GetAvatar(UserUUID): """获取用户头像 @@ -1265,6 +1294,40 @@ class AdminUserManager: }) return records, total + @staticmethod + def ListUsers(Keyword='', Offset=0, PageSize=20): + """列出用户(用户名/邮箱模糊匹配,分页,完整字段) + + 与 SearchUsers(精简字段)互补,供用户列表管理页使用。 + + Returns: + (list[dict], total) + """ + User = _GetUserModel() + qs = User.objects.all() + if Keyword: + from django.db.models import Q as _Q + qs = qs.filter(_Q(UserName__icontains=Keyword) | _Q(UserEmail__icontains=Keyword)) + total = qs.count() + users = qs[Offset:Offset + PageSize] + records = [] + for u in users: + records.append({ + 'UserUUID': UUIDObj(u.UserUUID).str if u.UserUUID else '', + 'UserName': u.UserName or '', + 'UserEmail': u.UserEmail or '', + 'UserEmailVerify': u.UserEmailVerify, + 'UserTele': u.UserTele or '', + 'UserGender': u.UserGender, + 'UserAccountLicense': u.UserAccountLicense, + 'UserPositionStatus': u.UserPositionStatus, + 'UserCreateTime': str(u.UserCreateTime) if u.UserCreateTime else '', + 'UserLastLoginDate': str(u.UserLastLoginDate) if u.UserLastLoginDate else '', + 'IsStaff': u.IsStaff, + 'IsSuperuser': u.IsSuperuser, + }) + return records, total + @staticmethod def GetUserByUUID(UserUUID): """按 UUID 查询用户(不存在返回 None)""" @@ -2028,3 +2091,200 @@ class AuditManager: pass return True + +class InventoryManager: + """物品/背包管理器——物品模板与用户物品栏的统一服务层""" + + # ========== 物品模板 ========== + + @staticmethod + def ToItemFlat(item): + """ItemReference 实例 → dict(字段与 ItemReferenceSerializer 输出一致)""" + return { + 'ItemUUID': UUIDObj(item.ItemUUID).str if item.ItemUUID else '', + 'ItemPublisherUserUUID': UUIDObj(item.ItemPublisherUserUUID).str if item.ItemPublisherUserUUID else '', + 'ItemName': item.ItemName or '', + 'ItemIcon': item.ItemIcon or '', + 'ItemIconColor': item.ItemIconColor or '', + 'ItemTags': item.ItemTags, + 'ItemDescription': item.ItemDescription or '', + 'ItemMetaData': item.ItemMetaData, + } + + @staticmethod + def ToBackpackFlat(bp): + """Backpack 实例 → dict(字段与 BackpackSerializer 输出一致)""" + return { + 'BackpackItemUUID': UUIDObj(bp.BackpackItemUUID).str if bp.BackpackItemUUID else '', + 'UserUUID': UUIDObj(bp.UserUUID).str if bp.UserUUID else '', + 'ItemTemplateUUID': UUIDObj(bp.ItemTemplateUUID).str if bp.ItemTemplateUUID else '', + '_ItemName': bp._ItemName, + '_ItemIcon': bp._ItemIcon, + '_ItemIconColor': bp._ItemIconColor, + '_ItemTags': bp._ItemTags, + '_ItemMetaData': bp._ItemMetaData, + '_ItemDescription': bp._ItemDescription, + 'ItemLength': bp.ItemLength, + } + + @staticmethod + def GetItem(ItemUUID): + """按 UUID 查询物品模板(不存在返回 None)""" + try: + return ItemReference.objects.get(ItemUUID=UUIDObj(ItemUUID).bytes) + except (ItemReference.DoesNotExist, ValueError, AttributeError): + return None + + @staticmethod + def AddItem(PublisherUserUUID, Data): + """创建物品模板 + + Returns: + dict:新物品记录 + """ + item = ItemReference.objects.create( + ItemUUID=uuid.uuid4().bytes, + ItemPublisherUserUUID=UUIDObj(PublisherUserUUID).bytes, + ItemName=(Data or {}).get('ItemName', ''), + ItemIcon=(Data or {}).get('ItemIcon', ''), + ItemIconColor=(Data or {}).get('ItemIconColor', ''), + ItemTags=(Data or {}).get('ItemTags', []), + ItemDescription=(Data or {}).get('ItemDescription', ''), + ItemMetaData=(Data or {}).get('ItemMetaData', {}), + ) + return InventoryManager.ToItemFlat(item) + + @staticmethod + def ModifyItem(ItemUUID, Data): + """修改物品模板字段 + + Returns: + (dict or None, 错误消息 or None) + """ + item = InventoryManager.GetItem(ItemUUID) + if item is None: + return None, '物品不存在' + for field, val in (Data or {}).items(): + if hasattr(item, field): + setattr(item, field, val) + item.save() + return InventoryManager.ToItemFlat(item), None + + @staticmethod + def DeleteItem(ItemUUID): + """删除物品模板;返回是否命中""" + deleted, _ = ItemReference.objects.filter( + ItemUUID=UUIDObj(ItemUUID).bytes, + ).delete() + return deleted > 0 + + # ========== 背包 ========== + + @staticmethod + def GetBackpackItem(BackpackItemUUID): + """按 UUID 查询背包物品(不存在返回 None)""" + try: + return Backpack.objects.get(BackpackItemUUID=UUIDObj(BackpackItemUUID).bytes) + except (Backpack.DoesNotExist, ValueError, AttributeError): + return None + + @staticmethod + def ListUserBackpack(UserUUID): + """获取用户的背包物品(QuerySet,保持数据库顺序)""" + return Backpack.objects.filter(UserUUID=UUIDObj(UserUUID).bytes) + + @staticmethod + def AddBackpackItem(UserUUID, ItemTemplateUUID, ItemLength=1): + """向用户背包添加物品(校验模板存在) + + Returns: + (dict or None, 错误消息 or None) + """ + template = InventoryManager.GetItem(ItemTemplateUUID) + if template is None: + return None, '物品模板不存在' + bp = Backpack.objects.create( + BackpackItemUUID=uuid.uuid4().bytes, + UserUUID=UUIDObj(UserUUID).bytes, + ItemTemplateUUID=template.ItemUUID, + ItemLength=ItemLength, + ) + return InventoryManager.ToBackpackFlat(bp), None + + @staticmethod + def ModifyBackpackItem(BackpackItemUUID, Data): + """修改背包物品字段 + + Returns: + (dict or None, 错误消息 or None) + """ + bp = InventoryManager.GetBackpackItem(BackpackItemUUID) + if bp is None: + return None, '背包物品不存在' + for field, val in (Data or {}).items(): + if hasattr(bp, field): + setattr(bp, field, val) + bp.save() + return InventoryManager.ToBackpackFlat(bp), None + + @staticmethod + def ChangeBackpackLength(BackpackItemUUID, Delta): + """增减背包物品数量;返回更新后的实例(不存在返回 None)""" + bp = InventoryManager.GetBackpackItem(BackpackItemUUID) + if bp is None: + return None + bp.ItemLength = (bp.ItemLength or 0) + Delta + bp.save(update_fields=['ItemLength']) + return bp + + @staticmethod + def DeleteBackpackItem(BackpackItemUUID): + """删除背包物品;返回是否命中""" + deleted, _ = Backpack.objects.filter( + BackpackItemUUID=UUIDObj(BackpackItemUUID).bytes, + ).delete() + return deleted > 0 + + +class AssetManager: + """文件资产管理器——SHA1 去重存储与引用计数""" + + @staticmethod + def EnsureAsset(FileSHA1, FileName, FileSize, MimeType, StoragePath, + UploaderUUID=None, CompanyUUID=None): + """按 SHA1 查找或创建文件资产;已存在时引用计数 +1 + + Returns: + (asset, created) + """ + from django.db.models import F as _F + defaults = { + 'AssetUUID': uuid.uuid4().bytes, + 'FileName': FileName, + 'FileSize': FileSize, + 'MimeType': MimeType, + 'StoragePath': StoragePath, + 'UploaderUUID': UUIDObj(UploaderUUID).bytes if UploaderUUID else None, + 'CompanyUUID': UUIDObj(CompanyUUID).bytes if CompanyUUID else None, + } + asset, created = FileAsset.objects.get_or_create( + FileSHA1=FileSHA1, + defaults=defaults, + ) + if not created: + FileAsset.objects.filter(pk=asset.pk).update(RefCount=_F('RefCount') + 1) + asset.refresh_from_db() + return asset, created + + @staticmethod + def IncrementRef(AssetUUID): + """增加文件资产引用计数;返回更新后的实例(不存在返回 None)""" + try: + asset = FileAsset.objects.get(AssetUUID=UUIDObj(AssetUUID).bytes) + except (FileAsset.DoesNotExist, ValueError, AttributeError): + return None + from django.db.models import F as _F + FileAsset.objects.filter(pk=asset.pk).update(RefCount=_F('RefCount') + 1) + asset.refresh_from_db() + return asset + diff --git a/gvsdsdk/models.py b/gvsdsdk/models.py index 99a60bb2..236b7157 100644 --- a/gvsdsdk/models.py +++ b/gvsdsdk/models.py @@ -1099,3 +1099,95 @@ class AuditLog(QModel): def __str__(self): return f'{self.TableName}/{self.Action}' + + +# === 物品引用表 === + +class ItemReference(QModel): + """物品引用表——物品模板定义(映射 SAAS 主库 ItemReference 表)""" + + ItemUUID = UUIDField(primary_key=True, db_column='ItemUUID', verbose_name='物品UUID') + ItemPublisherUserUUID = UUIDField(db_column='ItemPublisherUserUUID', verbose_name='发布者UUID') + ItemName = models.CharField(max_length=128, db_column='ItemName', verbose_name='物品名称') + ItemIcon = models.CharField(max_length=512, db_column='ItemIcon', verbose_name='物品图标') + ItemIconColor = models.CharField(max_length=6, db_column='ItemIconColor', verbose_name='图标颜色') + ItemTags = models.JSONField(default=list, db_column='ItemTags', verbose_name='标签') + ItemDescription = models.TextField(default='', db_column='ItemDescription', verbose_name='描述') + ItemMetaData = models.JSONField(default=dict, db_column='ItemMetaData', verbose_name='元数据') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'ItemReference' + verbose_name = '物品模板' + verbose_name_plural = '物品模板' + + def __str__(self): + return self.ItemName + + +# === 背包表 === + +class Backpack(QModel): + """背包表——用户物品栏(映射 SAAS 主库 Backpack 表)""" + + BackpackItemUUID = UUIDField(primary_key=True, db_column='BackpackItemUUID', verbose_name='背包物品UUID') + UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') + ItemTemplateUUID = UUIDField(db_column='ItemTemplateUUID', verbose_name='物品模板UUID') + _ItemName = models.CharField(max_length=128, null=True, blank=True, db_column='_ItemName', verbose_name='物品名称缓存') + _ItemIcon = models.CharField(max_length=512, null=True, blank=True, db_column='_ItemIcon', verbose_name='物品图标缓存') + _ItemIconColor = models.CharField(max_length=6, null=True, blank=True, db_column='_ItemIconColor', verbose_name='图标颜色缓存') + _ItemTags = models.JSONField(null=True, blank=True, db_column='_ItemTags', verbose_name='标签缓存') + _ItemMetaData = models.JSONField(null=True, blank=True, db_column='_ItemMetaData', verbose_name='元数据缓存') + _ItemDescription = models.TextField(null=True, blank=True, db_column='_ItemDescription', verbose_name='描述缓存') + ItemLength = models.BigIntegerField(default=1, db_column='ItemLength', verbose_name='数量') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Backpack' + verbose_name = '背包' + verbose_name_plural = '背包' + + @property + def ItemReference(self): + return self.ItemTemplateUUID + + @property + def ItemNameResolved(self): + return self._ItemName + + @property + def ItemIconResolved(self): + return self._ItemIcon + + def __str__(self): + return f'{self._ItemName or self.ItemTemplateUUID}x{self.ItemLength}' + + +# === 文件资产表 === + +class FileAsset(QModel): + """文件资产表——文件存储与引用管理(映射 SAAS 主库 FileAsset 表)""" + + AssetUUID = UUIDField(primary_key=True, db_column='AssetUUID', verbose_name='资产UUID') + CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID') + FileName = models.CharField(max_length=256, db_column='FileName', verbose_name='文件名') + FileSHA1 = models.CharField(max_length=40, unique=True, db_column='FileSHA1', verbose_name='文件SHA1') + FileSize = models.IntegerField(db_column='FileSize', verbose_name='文件大小') + MimeType = models.CharField(max_length=128, db_column='MimeType', verbose_name='MIME类型') + StoragePath = models.CharField(max_length=512, db_column='StoragePath', verbose_name='存储路径') + UploaderUUID = UUIDField(db_column='UploaderUUID', verbose_name='上传者UUID') + RefCount = models.IntegerField(default=0, db_column='RefCount', verbose_name='引用计数') + CreateTime = models.DateTimeField(default=_UtcNow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'FileAsset' + ordering = ['-CreateTime'] + verbose_name = '文件资产' + verbose_name_plural = '文件资产' + + def __str__(self): + return self.FileName