refactor(gerp): P1 裸操作委托服务层

物品/背包、文件资产、社区文档、密码修改、服务节点查询改经 InventoryManager/AssetManager/DocManager/ChangePasswordAudit/ServiceRegistry.GetNode;新增 doc 服务层 manager.py。
This commit is contained in:
TermiNexus committed 2026-08-07 19:39:37 +08:00
1 parent cd9ebcddd1
commit d92c163801
14 files changed
+652 -302

No files matched your search

+129
View File
@@ -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
+14 -34
View File
@@ -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}})
+3 -3
View File
@@ -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
+37 -77
View File
@@ -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)
+3 -4
View File
@@ -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()
+7 -12
View File
@@ -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})
+5 -7
View File
@@ -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 + 过滤无效)并校验存在性、增量更新
+24 -32
View File
@@ -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)
+9 -24
View File
@@ -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': '密码修改成功'})
+15 -38
View File
@@ -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)
+48 -70
View File
@@ -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):
+5
View File
@@ -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):
"""发现所有已注册服务(分页)
+261 -1
View File
@@ -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
+92
View File
@@ -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