"refactor(gerp): 菜单与工单 API 委托 gvsdsdk

menu.py 四视图与 WorkOrderViewSet 自定义 action、WorkOrderStatsView 改经
MenuManager/WorkOrderManager,移除视图层裸 ORM 查询。"
This commit is contained in:
TermiNexus committed 2026-08-07 17:17:29 +08:00
1 parent f6b057bc19
commit e2015164ea
12 files changed
+856 -447

No files matched your search

+19 -39
View File
@@ -6,18 +6,18 @@ GET /API/Dashboard/Stats
- dept_manager: 本部门统计
- workgroup_lead: 本工作组统计
- member: 个人统计
统计逻辑委托 gvsdsdk:用户数经 RoleManager/AuthManager,订单数经 OrderManager。
"""
import logging
import datetime
from rest_framework.response import Response
from . import ELTAuthenticatedAPIView
from gvsds.apps.gerp.views.form_api import _is_admin, _get_current_context
from gvsds.apps.gerp.views.form_api import _get_current_context
from gvsds.apps.gerp.role_scope import GetUserScope, _get_scope_members
from gvsds.apps.gerp.models import UserRole
from gvsds.apps.core.models import User
from gvsdsdk.order.models import Order
from gvsdsdk.auth.managers import AuthManager, RoleManager
from gvsdsdk.order.manager import OrderManager
logger = logging.getLogger(__name__)
@@ -29,11 +29,6 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
_, current_company_uuid, current_tenant_uuid = _get_current_context(request)
scope = GetUserScope(request)
# 时间范围
now = datetime.datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - datetime.timedelta(days=today_start.weekday())
stats = {
'Scope': scope.Type,
'TotalUsers': 0,
@@ -48,17 +43,11 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
try:
# 用户统计
if scope.Type == 'admin':
# 管理员:全公司用户
user_qs = User.objects.all()
# 管理员:全公司用户(无公司上下文时统计全量)
company_user_uuids = None
if current_company_uuid:
# 通过 UserRole 关联查找本公司用户
company_user_uuids = set(
UserRole.objects.filter(
CompanyUUID=current_company_uuid
).values_list('UserUUID', flat=True)
)
user_qs = user_qs.filter(UserUUID__in=company_user_uuids) if company_user_uuids else user_qs.none()
stats['TotalUsers'] = user_qs.count()
company_user_uuids = RoleManager.GetCompanyUserUUIDs(current_company_uuid)
stats['TotalUsers'] = AuthManager.CountUsers(company_user_uuids)
elif scope.Type in ('dept_manager', 'workgroup_lead'):
member_uuids = _get_scope_members(scope)
if member_uuids:
@@ -67,28 +56,19 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
stats['TotalUsers'] = 1
# 订单统计
order_qs = Order.objects.all()
if current_tenant_uuid:
order_qs = order_qs.filter(TenantUUID=current_tenant_uuid)
if scope.Type == 'admin':
if current_company_uuid:
order_qs = order_qs.filter(CompanyUUID=current_company_uuid)
elif scope.Type == 'member':
order_qs = order_qs.filter(BuyerUserUUID=scope.UserUUID)
buyer_uuids = None
if scope.Type == 'member':
buyer_uuids = [scope.UserUUID]
elif scope.Type in ('dept_manager', 'workgroup_lead'):
# 无成员上下文视为空集合(统计全 0),与 None 区分
member_uuids = _get_scope_members(scope)
if member_uuids:
order_qs = order_qs.filter(BuyerUserUUID__in=member_uuids)
else:
order_qs = order_qs.none()
buyer_uuids = member_uuids if member_uuids is not None else []
stats['TotalOrders'] = order_qs.count()
stats['TodayOrders'] = order_qs.filter(CreateTime__gte=today_start).count()
stats['WeekOrders'] = order_qs.filter(CreateTime__gte=week_start).count()
stats['PendingOrders'] = order_qs.filter(OrderStatus=Order.ORD_PENDING).count()
stats['PaidOrders'] = order_qs.filter(OrderStatus__in=[Order.ORD_PAID, Order.ORD_PROCESSING, Order.ORD_COMPLETED]).count()
stats['CompletedOrders'] = order_qs.filter(OrderStatus=Order.ORD_COMPLETED).count()
stats.update(OrderManager.GetStats(
tenant_uuid=current_tenant_uuid,
company_uuid=current_company_uuid if scope.Type == 'admin' else None,
buyer_user_uuids=buyer_uuids,
))
except Exception:
logger.warning('[DashboardStats] 统计计算失败', exc_info=True)
+6 -11
View File
@@ -464,22 +464,17 @@ class WorkOrderStatsView(ELTAuthenticatedAPIView):
"""
def get(self, request):
from django.db.models import Count
from gvsds.apps.gerp.models import WorkOrder
from gvsdsdk.auth.managers import WorkOrderManager
from gvsds.apps.gerp.form_service import FormService
qs = WorkOrder.objects.all()
def _group(field_name):
return {row[field_name]: row['c'] for row in qs.values(field_name).annotate(c=Count('pk'))}
stats = WorkOrderManager.GetStats()
return Response({
'code': 0,
'data': {
'total': qs.count(),
'byStatus': _group('Status'),
'byPriority': _group('Priority'),
'byCategory': _group('Category'),
'total': stats['total'],
'byStatus': stats['byStatus'],
'byPriority': stats['byPriority'],
'byCategory': stats['byCategory'],
# 附状态/优先级中文标签,前端可直接渲染
'statusOptions': FormService.WORKORDER_STATUS_OPTIONS['Status'],
'priorityOptions': FormService.WORKORDER_PRIORITY_OPTIONS['Priority'],
+16 -173
View File
@@ -15,9 +15,8 @@
import uuid as _uuid
import logging
from django.db.models import Q
from rest_framework.response import Response
from gvsds.apps.gerp.models import Menu, Company
from gvsdsdk.auth.managers import MenuManager
from . import ELTAuthenticatedAPIView
logger = logging.getLogger(__name__)
@@ -79,43 +78,13 @@ class MenuListCompatView(ELTAuthenticatedAPIView):
# 管理员特权:仅跳过权限过滤,公司过滤对所有用户生效(防止跨公司私有菜单泄漏)
is_admin = _IsAdminUser(user, role_uuid_bytes)
# 公司过滤(对所有用户生效,包括管理员)
qs = Menu.objects.filter(MenuStatus=1)
if current_company:
# 公有菜单 + 当前公司私有菜单
qs = qs.filter(Q(CompanyUUID__isnull=True) | Q(CompanyUUID=current_company))
else:
# 无公司上下文:仅公有菜单
qs = qs.filter(CompanyUUID__isnull=True)
menus = qs.select_related('ParentMenuUUID', 'CompanyUUID').order_by('MenuSort', 'MenuName')
# 构建扁平映射
# 公司过滤(对所有用户生效,包括管理员)——经 MenuManager 委托查询
menu_map = {}
for menu in menus:
menu_uuid_str = str(menu.MenuUUID) if menu.MenuUUID else ''
for item in MenuManager.ListVisible(current_company):
menu_uuid_str = item['MenuUUID']
if not menu_uuid_str:
continue
parent_uuid_str = ''
if menu.ParentMenuUUID and menu.ParentMenuUUID.MenuUUID:
parent_uuid_str = str(menu.ParentMenuUUID.MenuUUID)
company_uuid = None
if menu.CompanyUUID:
company_uuid = str(menu.CompanyUUID.CompanyUUID)
menu_map[menu_uuid_str] = {
'Id': menu.MenuName,
'Title': menu.MenuTitle,
'Icon': menu.MenuIcon or '',
'Page': menu.MenuLink or '',
'ParentUUID': parent_uuid_str,
'MenuUUID': menu_uuid_str,
'CompanyUUID': company_uuid,
'Sort': menu.MenuSort or 0,
'Type': menu.MenuType or 0,
'Children': [],
'PermLevel': menu.PermissionLevel or 0,
'PermAttr': menu.PermissionAttr or '',
}
menu_map[menu_uuid_str] = item
# 祖先链校验:任何祖先不属于当前公司 → 整个分支移除
# 对所有用户生效(包括管理员),防止公有子节点挂在私有父节点下导致结构泄漏
@@ -248,31 +217,9 @@ class MenuAdminListView(ELTAuthenticatedAPIView):
return Response({'code': 403, 'msg': '无权限', 'data': None})
company_uuid_str = (request.query_params.get('CompanyUUID') or '').strip()
qs = Menu.objects.all()
if company_uuid_str:
company_uuid_bytes = _ParseUUID(company_uuid_str)
if company_uuid_bytes:
qs = qs.filter(Q(CompanyUUID__isnull=True) | Q(CompanyUUID=company_uuid_bytes))
else:
qs = qs.filter(CompanyUUID__isnull=True)
menus = qs.select_related('ParentMenuUUID', 'CompanyUUID').order_by('MenuSort', 'MenuName')
records = []
for menu in menus:
records.append({
'MenuUUID': str(menu.MenuUUID) if menu.MenuUUID else '',
'ParentMenuUUID': str(menu.ParentMenuUUID.MenuUUID) if menu.ParentMenuUUID and menu.ParentMenuUUID.MenuUUID else '',
'CompanyUUID': str(menu.CompanyUUID.CompanyUUID) if menu.CompanyUUID else '',
'MenuName': menu.MenuName or '',
'MenuTitle': menu.MenuTitle or '',
'MenuIcon': menu.MenuIcon or '',
'MenuLink': menu.MenuLink or '',
'MenuSort': menu.MenuSort if menu.MenuSort is not None else 0,
'MenuType': menu.MenuType if menu.MenuType is not None else 0,
'MenuStatus': menu.MenuStatus if menu.MenuStatus is not None else 0,
'PermissionLevel': menu.PermissionLevel if menu.PermissionLevel is not None else 0,
'PermissionAttr': menu.PermissionAttr or '',
})
company_provided = bool(company_uuid_str)
company_uuid_bytes = _ParseUUID(company_uuid_str) if company_provided else None
records = MenuManager.AdminList(company_uuid_bytes, company_provided)
return Response({'code': 0, 'data': records, 'total': len(records)})
@@ -292,100 +239,11 @@ class MenuSaveView(ELTAuthenticatedAPIView):
if user is None:
return Response({'code': 403, 'msg': '无权限', 'data': None})
def _get(key, default=''):
v = request.data.get(key, default)
return '' if v is None else v
menu_uuid_str = _get('MenuUUID', '').strip()
company_uuid_str = _get('CompanyUUID', '').strip()
parent_uuid_str = _get('ParentMenuUUID', '').strip()
# 解析公司(空则不关联公司,即公有菜单)
company = None
if company_uuid_str:
company_uuid_bytes = _ParseUUID(company_uuid_str)
if not company_uuid_bytes:
return Response({'code': 1, 'msg': 'CompanyUUID 格式无效', 'data': None})
try:
company = Company.objects.get(CompanyUUID=company_uuid_bytes)
except Company.DoesNotExist:
return Response({'code': 1, 'msg': '公司不存在', 'data': None})
# 解析父菜单(空则根菜单)
parent = None
if parent_uuid_str:
parent_uuid_bytes = _ParseUUID(parent_uuid_str)
if not parent_uuid_bytes:
return Response({'code': 1, 'msg': 'ParentMenuUUID 格式无效', 'data': None})
try:
parent = Menu.objects.get(MenuUUID=parent_uuid_bytes)
except Menu.DoesNotExist:
return Response({'code': 1, 'msg': '父菜单不存在', 'data': None})
# 通用字段
menu_name = _get('MenuName', '').strip()
menu_title = _get('MenuTitle', '').strip()
menu_icon = _get('MenuIcon', '')
menu_link = _get('MenuLink', '')
try:
menu_sort = int(_get('MenuSort', '0'))
except (ValueError, TypeError):
menu_sort = 0
try:
menu_type = int(_get('MenuType', '0'))
except (ValueError, TypeError):
menu_type = 0
try:
menu_status = int(_get('MenuStatus', '1'))
except (ValueError, TypeError):
menu_status = 1
try:
perm_level = float(_get('PermissionLevel', '1.0'))
except (ValueError, TypeError):
perm_level = 1.0
perm_attr = _get('PermissionAttr', '')
if menu_uuid_str:
# 修改
menu_uuid_bytes = _ParseUUID(menu_uuid_str)
if not menu_uuid_bytes:
return Response({'code': 1, 'msg': 'MenuUUID 格式无效', 'data': None})
try:
menu = Menu.objects.get(MenuUUID=menu_uuid_bytes)
except Menu.DoesNotExist:
return Response({'code': 1, 'msg': '菜单不存在', 'data': None})
menu.CompanyUUID = company
menu.ParentMenuUUID = parent
menu.MenuName = menu_name
menu.MenuTitle = menu_title
menu.MenuIcon = menu_icon
menu.MenuLink = menu_link
menu.MenuSort = menu_sort
menu.MenuType = menu_type
menu.MenuStatus = menu_status
menu.PermissionLevel = perm_level
menu.PermissionAttr = perm_attr
menu.save()
return Response({'code': 0, 'id': menu_uuid_str, 'data': {'MenuUUID': menu_uuid_str}})
# 新增
new_uuid_bytes = _uuid.uuid4().bytes
menu = Menu.objects.create(
MenuUUID=new_uuid_bytes,
CompanyUUID=company,
ParentMenuUUID=parent,
MenuName=menu_name or 'new-menu',
MenuTitle=menu_title or '新菜单',
MenuIcon=menu_icon,
MenuLink=menu_link,
MenuSort=menu_sort,
MenuType=menu_type,
MenuStatus=menu_status,
PermissionLevel=perm_level,
PermissionAttr=perm_attr,
)
new_uuid_str = str(_uuid.UUID(bytes=new_uuid_bytes))
return Response({'code': 0, 'id': new_uuid_str, 'data': {'MenuUUID': new_uuid_str}})
record, err = MenuManager.Save(dict(request.data))
if err:
return Response({'code': 1, 'msg': err, 'data': None})
menu_uuid_str = record['MenuUUID']
return Response({'code': 0, 'id': menu_uuid_str, 'data': {'MenuUUID': menu_uuid_str}})
class MenuDeleteView(ELTAuthenticatedAPIView):
@@ -404,22 +262,7 @@ class MenuDeleteView(ELTAuthenticatedAPIView):
if not menu_uuid_str:
return Response({'code': 1, 'msg': '缺少 MenuUUID', 'data': None})
menu_uuid_bytes = _ParseUUID(menu_uuid_str)
if not menu_uuid_bytes:
return Response({'code': 1, 'msg': 'MenuUUID 格式无效', 'data': None})
try:
menu = Menu.objects.get(MenuUUID=menu_uuid_bytes)
except Menu.DoesNotExist:
return Response({'code': 1, 'msg': '菜单不存在', 'data': None})
# 递归删除子菜单,避免孤儿节点
def _DeleteDescendants(parent_uuid):
children = Menu.objects.filter(ParentMenuUUID=parent_uuid)
for child in children:
_DeleteDescendants(child.MenuUUID)
child.delete()
_DeleteDescendants(menu.MenuUUID)
menu.delete()
ok, err = MenuManager.Delete(menu_uuid_str)
if not ok:
return Response({'code': 1, 'msg': err, 'data': None})
return Response({'code': 0, 'data': None})
+73 -141
View File
@@ -1,127 +1,74 @@
import uuid
import datetime
import logging
from rest_framework.response import Response
from gvsds.apps.core.models import User
from gvsds.apps.gerp.models import Message
from gvsdsdk.models import Role
from . import ELTAuthenticatedAPIView
logger = logging.getLogger(__name__)
class MessageCompatView(ELTAuthenticatedAPIView):
"""消息兼容视图——站内信收发与查询"""
"""消息兼容视图——站内信收发与查询(委托 gvsdsdk.MessageManager)"""
def get(self, request):
action = request.query_params.get('action', 'unread_count')
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
if not user:
return Response({'code': 0, 'UnreadCount': 0, 'RecentMessages': []})
# request.user 是 gvsdsdk.User,但 Message.ReceiverUUID/SenderUUID 是 FK 指向 core.User
# 使用 _id 后缀直接传列值避免类型不匹配
from gvsdsdk.auth.managers import MessageManager
user_uuid = user.UserUUID
if action == 'unread_count':
count = Message.objects.filter(ReceiverUUID_id=user_uuid, IsRead=0).count()
recent = Message.objects.filter(ReceiverUUID_id=user_uuid).select_related('SenderUUID').order_by('-CreateTime')[:5]
recent_data = []
for msg in recent:
sender_name = "\u7cfb\u7edf"
if msg.SenderUUID:
sender_name = msg.SenderUUID.UserName
recent_data.append({
'MessageUUID': msg.MessageUUID or '',
'Title': msg.Title or '',
'Content': (msg.Content or '')[:100],
'MsgType': msg.MsgType or 0,
'IsRead': msg.IsRead or 0,
'SenderName': sender_name,
'RelatedUUID': msg.RelatedUUID or '',
'RelatedType': msg.RelatedType or '',
'CreateTime': msg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if msg.CreateTime else '',
})
return Response({'code': 0, 'UnreadCount': count, 'RecentMessages': recent_data})
count = MessageManager.UnreadCount(user_uuid)
recent = MessageManager.RecentMessages(user_uuid, Limit=5)
return Response({'code': 0, 'UnreadCount': count, 'RecentMessages': recent})
elif action == 'list':
if action == 'list':
# 分页参数:兼容前端协议 PageIndex/PageSize(原生 page/pageSize 兜底)
page = int(request.query_params.get('PageIndex', request.query_params.get('page', 1)))
page_size = int(request.query_params.get('PageSize', request.query_params.get('pageSize', 20)))
qs = Message.objects.filter(ReceiverUUID_id=user_uuid).select_related('SenderUUID').order_by('-CreateTime')
# 筛选:IsRead(0 未读 / 1 已读)、MsgType(0 系统 / 1 审核 / 2 工单 / 3 个人)
is_read = request.query_params.get('IsRead')
if is_read in ('0', '1'):
qs = qs.filter(IsRead=int(is_read))
msg_type = request.query_params.get('MsgType')
if msg_type and msg_type.isdigit():
qs = qs.filter(MsgType=int(msg_type))
total = qs.count()
items = qs[(page - 1) * page_size:page * page_size]
data = []
for msg in items:
sender_name = "\u7cfb\u7edf"
if msg.SenderUUID:
sender_name = msg.SenderUUID.UserName
data.append({
'MessageUUID': str(msg.MessageUUID or ''),
'Title': msg.Title or '',
'Content': msg.Content or '',
'MsgType': msg.MsgType or 0,
'IsRead': msg.IsRead or 0,
'SenderName': sender_name,
'RelatedUUID': msg.RelatedUUID or '',
'RelatedType': msg.RelatedType or '',
'CreateTime': msg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if msg.CreateTime else '',
})
try:
page = int(request.query_params.get('PageIndex', request.query_params.get('page', 1)))
except (TypeError, ValueError):
page = 1
try:
page_size = int(request.query_params.get('PageSize', request.query_params.get('pageSize', 20)))
except (TypeError, ValueError):
page_size = 20
is_read_raw = request.query_params.get('IsRead')
is_read = int(is_read_raw) if is_read_raw in ('0', '1') else None
msg_type_raw = request.query_params.get('MsgType')
msg_type = int(msg_type_raw) if msg_type_raw and msg_type_raw.isdigit() else None
total, data = MessageManager.ListMessages(
user_uuid, Page=page, PageSize=page_size,
IsRead=is_read, MsgType=msg_type,
)
return Response({'code': 0, 'total': total, 'data': data})
elif action == 'detail':
if action == 'detail':
# 消息UUID:兼容前端协议 MessageUUID(原生 uuid 兜底)
message_uuid_str = request.query_params.get('MessageUUID', '') or request.query_params.get('uuid', '')
if not message_uuid_str:
return Response({'code': 1, 'msg': '\u7f3a\u5c11MessageUUID\u53c2\u6570'}, status=400)
msg = Message.objects.filter(MessageUUID=message_uuid_str, ReceiverUUID_id=user_uuid).select_related('SenderUUID').first()
if not msg:
return Response({'code': 1, 'msg': '\u6d88\u606f\u4e0d\u5b58\u5728'}, status=404)
msg.IsRead = 1
msg.save(update_fields=['IsRead'])
sender_name = "\u7cfb\u7edf"
if msg.SenderUUID:
sender_name = msg.SenderUUID.UserName
return Response({'code': 0, 'data': {
'MessageUUID': str(msg.MessageUUID or ''),
'Title': msg.Title or '',
'Content': msg.Content or '',
'MsgType': msg.MsgType or 0,
'IsRead': msg.IsRead or 0,
'SenderName': sender_name,
'RelatedUUID': msg.RelatedUUID or '',
'RelatedType': msg.RelatedType or '',
'CreateTime': msg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if msg.CreateTime else '',
}})
elif action == 'search_recipients':
keyword = request.query_params.get('keyword', '')
limit = int(request.query_params.get('limit', 10))
data = []
# 用户:按用户名搜索
uqs = User.objects.all()
if keyword:
uqs = uqs.filter(UserName__icontains=keyword)
for u in uqs[:limit]:
data.append({'type': 'user', 'uuid': str(u.UserUUID or ''), 'name': u.UserName, 'label': u.UserName})
# 角色:按角色名搜索(发送弹窗支持按角色群发)
rqs = Role.objects.all()
if keyword:
rqs = rqs.filter(RoleName__icontains=keyword)
for r in rqs[:limit]:
data.append({'type': 'role', 'uuid': str(r.RoleUUID or ''), 'name': r.RoleName, 'label': r.RoleName})
return Response({'code': 1, 'msg': '缺少MessageUUID参数'}, status=400)
data = MessageManager.GetMessageDetail(user_uuid, message_uuid_str)
if data is None:
return Response({'code': 1, 'msg': '消息不存在'}, status=404)
return Response({'code': 0, 'data': data})
return Response({'code': 1, 'msg': '\u672a\u77e5\u64cd\u4f5c'}, status=400)
if action == 'search_recipients':
keyword = request.query_params.get('keyword', '')
try:
limit = int(request.query_params.get('limit', 10))
except (TypeError, ValueError):
limit = 10
data = MessageManager.SearchRecipients(Keyword=keyword, Limit=limit)
return Response({'code': 0, 'data': data})
return Response({'code': 1, 'msg': '未知操作'}, status=400)
def post(self, request):
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
if not user:
return Response({'code': 1, 'msg': '\u672a\u767b\u5f55'}, status=401)
return Response({'code': 1, 'msg': '未登录'}, status=401)
from gvsdsdk.auth.managers import MessageManager
user_uuid = user.UserUUID
title = request.data.get('Title', '')
content = request.data.get('Content', '')
@@ -131,67 +78,52 @@ class MessageCompatView(ELTAuthenticatedAPIView):
related_uuid_str = request.data.get('RelatedUUID', '')
related_type = request.data.get('RelatedType', '')
if not title or not receiver_uuid_str:
return Response({'code': 1, 'msg': '\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570'}, status=400)
def _make_message(receiver):
return Message(
MessageUUID=uuid.uuid4().bytes,
SenderUUID_id=user_uuid,
ReceiverUUID=receiver,
Title=title,
Content=content,
MsgType=msg_type,
IsRead=0,
RelatedUUID=related_uuid_str,
RelatedType=related_type,
CreateTime=datetime.datetime.utcnow(),
)
return Response({'code': 1, 'msg': '缺少必要参数'}, status=400)
if receiver_type == 'role':
# 角色群发:给持有该角色的所有用户各发一条(复用 UserRole 关联,去重)
role_uuid_str = receiver_uuid_str
from gvsdsdk.auth.managers import RoleManager
role = RoleManager.GetRoleByUUID(role_uuid_str)
if role is None:
# 角色群发:给持有该角色的所有用户各发一条
sent = MessageManager.SendRoleMessage(
user_uuid, receiver_uuid_str, title, content,
msg_type, related_uuid_str, related_type,
)
if sent < 0:
return Response({'code': 1, 'msg': '角色不存在'}, status=400)
receivers = RoleManager.GetUsersForRole(role.RoleUUID)
for receiver in receivers:
_make_message(receiver).save()
return Response({'code': 0, 'msg': '\u53d1\u9001\u6210\u529f'})
try:
receiver = User.objects.get(UserUUID=receiver_uuid_str)
except User.DoesNotExist:
return Response({'code': 1, 'msg': '\u63a5\u6536\u8005\u4e0d\u5b58\u5728'}, status=400)
_make_message(receiver).save()
return Response({'code': 0, 'msg': '\u53d1\u9001\u6210\u529f'})
return Response({'code': 0, 'msg': '发送成功'})
msg = MessageManager.SendMessage(
user_uuid, receiver_uuid_str, title, content,
msg_type, related_uuid_str, related_type,
)
if msg is None:
return Response({'code': 1, 'msg': '接收者不存在'}, status=400)
return Response({'code': 0, 'msg': '发送成功'})
def patch(self, request):
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
if not user:
return Response({'code': 1, 'msg': '\u672a\u767b\u5f55'}, status=401)
return Response({'code': 1, 'msg': '未登录'}, status=401)
from gvsdsdk.auth.managers import MessageManager
# 全部已读:body 携带 action=read_all
if request.data.get('action') == 'read_all':
Message.objects.filter(ReceiverUUID_id=user.UserUUID, IsRead=0).update(IsRead=1)
return Response({'code': 0, 'msg': '\u5df2\u5168\u90e8\u6807\u8bb0\u4e3a\u5df2\u8bfb'})
MessageManager.MarkAllRead(user.UserUUID)
return Response({'code': 0, 'msg': '已全部标记为已读'})
uuid_str = request.data.get('UUID', '')
if not uuid_str:
return Response({'code': 1, 'msg': '\u7f3a\u5c11uuid\u53c2\u6570'}, status=400)
msg = Message.objects.filter(MessageUUID=uuid_str, ReceiverUUID_id=user.UserUUID).first()
if not msg:
return Response({'code': 1, 'msg': '\u6d88\u606f\u4e0d\u5b58\u5728'}, status=404)
msg.IsRead = 1
msg.save(update_fields=['IsRead'])
return Response({'code': 0, 'msg': '\u5df2\u6807\u8bb0\u4e3a\u5df2\u8bfb'})
return Response({'code': 1, 'msg': '缺少uuid参数'}, status=400)
if not MessageManager.MarkRead(user.UserUUID, uuid_str):
return Response({'code': 1, 'msg': '消息不存在'}, status=404)
return Response({'code': 0, 'msg': '已标记为已读'})
def delete(self, request):
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
if not user:
return Response({'code': 1, 'msg': '\u672a\u767b\u5f55'}, status=401)
return Response({'code': 1, 'msg': '未登录'}, status=401)
from gvsdsdk.auth.managers import MessageManager
# 消息UUID:兼容前端协议(DELETE body MessageUUID),原生 uuid(query)/UUID(body)兜底
uuid_str = (request.data.get('MessageUUID', '')
or request.query_params.get('uuid', '')
or request.data.get('UUID', ''))
if not uuid_str:
return Response({'code': 1, 'msg': '\u7f3a\u5c11uuid\u53c2\u6570'}, status=400)
Message.objects.filter(MessageUUID=uuid_str, ReceiverUUID_id=user.UserUUID).delete()
return Response({'code': 0, 'msg': '\u5220\u9664\u6210\u529f'})
return Response({'code': 1, 'msg': '缺少uuid参数'}, status=400)
MessageManager.DeleteMessage(user.UserUUID, uuid_str)
return Response({'code': 0, 'msg': '删除成功'})
+7 -14
View File
@@ -1,7 +1,6 @@
"""用户设置 API——基于 UserSettings 模型提供存取"""
"""用户设置 API——基于 gvsdsdk UserService 提供存取"""
import json
from rest_framework.response import Response
from gvsds.apps.gerp.models import UserSettings
from . import ELTAuthenticatedAPIView
@@ -13,14 +12,10 @@ class SettingsAPIView(ELTAuthenticatedAPIView):
if not user:
return Response({'code': 1, 'msg': '未登录'}, status=401)
from gvsdsdk.auth.user_service import UserService
try:
# request.user 是 gvsdsdk.User,UserSettings.UserUUID 是 FK 指向 core.User
# 使用 _id 后缀直接传列值避免类型不匹配
record = UserSettings.objects.filter(UserUUID_id=user.UserUUID).first()
if record and record.SettingsData:
settings = json.loads(record.SettingsData)
else:
settings = {}
settings_text = UserService.GetUserSettings(user.UserUUID)
settings = json.loads(settings_text) if settings_text else {}
except Exception:
settings = {}
@@ -39,12 +34,10 @@ class SettingsAPIView(ELTAuthenticatedAPIView):
elif not isinstance(settings_data, str):
return Response({'code': 1, 'msg': '无效的设置数据'}, status=400)
from gvsdsdk.auth.user_service import UserService
try:
record, created = UserSettings.objects.update_or_create(
UserUUID_id=user.UserUUID,
defaults={'SettingsData': settings_data}
)
UserService.SaveUserSettings(user.UserUUID, settings_data)
except Exception as e:
return Response({'code': 1, 'msg': f'保存失败: {str(e)}'}, status=500)
return Response({'code': 0, 'msg': '保存成功'})
return Response({'code': 0, 'msg': '保存成功'})
+12 -50
View File
@@ -5,7 +5,7 @@ import datetime
import bcrypt
from rest_framework.response import Response
from gvsds.apps.core.models import User
from gvsds.apps.gerp.models import Password, UserIDCard, PersonalPage
from gvsds.apps.gerp.models import Password
from gvsdsdk.model_utils import UUIDObj
from . import ELTAuthenticatedAPIView
from ..serializers import (
@@ -121,9 +121,9 @@ class UserIdCardView(ELTAuthenticatedAPIView):
def get(self, request):
user = request.user
try:
idcard = UserIDCard.objects.get(UserUUID=user.UserUUID)
except UserIDCard.DoesNotExist:
from gvsdsdk.auth.user_service import UserService
idcard = UserService.GetUserIDCard(user.UserUUID)
if idcard is None:
return Response({'code': 0, 'count': 1000, 'data': {}})
born = idcard.Born
return Response({
@@ -166,9 +166,9 @@ class PersonalPageConfigView(ELTAuthenticatedAPIView):
def get(self, request):
user = request.user
try:
page = PersonalPage.objects.get(PersonalPageUserUUID=user.UserUUID)
except PersonalPage.DoesNotExist:
from gvsdsdk.auth.user_service import UserService
page = UserService.GetPersonalPage(user.UserUUID)
if page is None:
return Response({'code': 1, 'msg': '未注册个人主页'})
return Response({
'code': 0,
@@ -214,52 +214,14 @@ class PersonalPageConfigView(ELTAuthenticatedAPIView):
return Response({'code': 1, 'msg': 'PersonalPageName 不能为空'})
if not re.fullmatch(r'^[a-z0-9_]+$', personal_page_name):
return Response({'code': 1, 'msg': 'PersonalPageName 仅允许小写字母、数字和下划线'})
field_map = {
'PersonalPageName': 'PersonalPageName',
'Subtitle': 'Subtitle',
'BackgroundLink': 'BackgroundLink',
'Classification': 'Classification',
'Github': 'Github',
'WeChat': 'Wechat',
'QQ': 'QQ',
'SelfIntroduction': 'SelfIntroduction',
'ProjectExperience': 'ProjectExperience',
'SatisfiedCustomers': 'SatisfiedCustomers',
'ReceiveAward': 'ReceiveAward',
'CoffeeCups': 'CoffeeCups',
'Achievement1Icon': 'Achievement1Icon',
'Achievement1Name': 'Achievement1Name',
'Achievement1Time': 'Achievement1Time',
'Achievement1Introduction': 'Achievement1Introduction',
'Achievement2Icon': 'Achievement2Icon',
'Achievement2Name': 'Achievement2Name',
'Achievement2Time': 'Achievement2Time',
'Achievement2Introduction': 'Achievement2Introduction',
'Achievement3Icon': 'Achievement3Icon',
'Achievement3Name': 'Achievement3Name',
'Achievement3Time': 'Achievement3Time',
'Achievement3Introduction': 'Achievement3Introduction',
'Achievement4Icon': 'Achievement4Icon',
'Achievement4Name': 'Achievement4Name',
'Achievement4Time': 'Achievement4Time',
'Achievement4Introduction': 'Achievement4Introduction',
'Tags': 'Tags',
'MarkDown': 'Markdown',
}
defaults = {}
for form_key, model_key in field_map.items():
val = request.data.get(form_key)
if val is not None:
defaults[model_key] = val
defaults['PersonalPageName'] = personal_page_name
from gvsdsdk.auth.user_service import UserService
try:
page, created = PersonalPage.objects.update_or_create(
PersonalPageUserUUID=user.UserUUID,
defaults=defaults,
)
return Response({'code': 0, 'msg': '修改成功'})
page, err = UserService.SavePersonalPage(user.UserUUID, request.data)
if page is None:
return Response({'code': 1, 'msg': err})
except Exception as e:
return Response({'code': 1, 'msg': str(e)}, status=400)
return Response({'code': 0, 'msg': '修改成功'})
class MemberCardView(ELTAuthenticatedAPIView):
+13 -17
View File
@@ -655,9 +655,9 @@ class WorkOrderViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
user_uuid = request.user.UserUUID if request.user.IsAuthenticated else None
if not user_uuid:
return Response({'code': 401, 'msg': '未登录'}, status=401)
qs = WorkOrder.objects.filter(CreatorUUID_id=user_uuid)
serializer = self.get_serializer(qs, many=True)
return Response({'code': 0, 'data': serializer.data})
from gvsdsdk.auth.managers import WorkOrderManager
data = WorkOrderManager.ListForUser(user_uuid)
return Response({'code': 0, 'data': data})
@action(detail=False, methods=['post'])
def create_order(self, request):
@@ -665,31 +665,27 @@ class WorkOrderViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
ser.IsValid(raise_exception=True)
data = ser.ValidatedData
user_uuid = request.user.UserUUID
assignee = data.get('AssigneeUUID')
order = WorkOrder.objects.create(
OrderUUID=uuid.uuid4().bytes,
from gvsdsdk.auth.managers import WorkOrderManager
record = WorkOrderManager.Create(
CreatorUUID=user_uuid,
Title=data['Title'],
Description=data.get('Description', ''),
Category=data.get('Category', 'other'),
Priority=data.get('Priority', 0),
CreatorUUID_id=user_uuid,
AssigneeUUID_id=assignee,
AssigneeUUID=data.get('AssigneeUUID'),
)
return Response({'code': 0, 'msg': '创建成功', 'data': WorkOrderSerializer(order).data}, status=201)
return Response({'code': 0, 'msg': '创建成功', 'data': record}, status=201)
@action(detail=True, methods=['post'])
def update_order(self, request, pk=None):
order = self.get_object()
ser = WorkOrderUpdateSerializer(data=request.data)
ser.IsValid(raise_exception=True)
data = ser.ValidatedData
field_map = {'Status': 'Status', 'AssigneeUUID': 'AssigneeUUID', 'Resolution': 'Resolution', 'Description': 'Description', 'Priority': 'Priority'}
for key in field_map:
if key in data:
setattr(order, field_map[key], data[key])
order.UpdateTime = datetime.datetime.utcnow()
order.save()
return Response({'code': 0, 'msg': '更新成功', 'data': WorkOrderSerializer(order).data})
from gvsdsdk.auth.managers import WorkOrderManager
record = WorkOrderManager.Update(pk, data)
if record is None:
return Response({'code': 404, 'msg': '工单不存在'}, status=404)
return Response({'code': 0, 'msg': '更新成功', 'data': record})
class MessageViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
+3
View File
@@ -224,6 +224,7 @@ _LAZY_SYMBOLS = {
'TenantManager': ('auth', 'TenantManager'),
'CompanyManager': ('auth', 'CompanyManager'),
'DepartmentManager': ('auth', 'DepartmentManager'),
'MessageManager': ('auth', 'MessageManager'),
'UserService': ('auth', 'UserService'),
# ── 分佣引擎 ──────────────────────────────────────────
@@ -298,6 +299,8 @@ _LAZY_SYMBOLS = {
'Position': ('models', 'Position'),
'PositionTemplate': ('models', 'PositionTemplate'),
'PositionTemplatePermission': ('models', 'PositionTemplatePermission'),
'Menu': ('models', 'Menu'),
'WorkOrder': ('models', 'WorkOrder'),
'FixedBinaryField': ('models', 'FixedBinaryField'),
'QModel': ('model_base', 'QModel'),
'QModelBase': ('model_base', 'QModelBase'),
+1 -1
View File
@@ -6,7 +6,7 @@ __all__ = [
'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'TenantScopedMixin', 'UserScopedMixin', 'PermissionVerifier',
'IsAdminScopeDisabled', 'IsTenantScopeDisabled',
'IsAdmin', 'IsSuperAdmin', 'IsTenantAdmin',
'RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager', 'TenantManager', 'CompanyManager', 'DepartmentManager', 'UserService',
'RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager', 'TenantManager', 'CompanyManager', 'DepartmentManager', 'MessageManager', 'UserService',
]
+517 -1
View File
@@ -1,7 +1,8 @@
import uuid
from django.db.models import Q
from django.utils import timezone as _timezone
from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, UserRoleDepartment
from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Position, PositionTemplate, PositionTemplatePermission, Shop, RoleShop, User2FA, UserAvatar, Password, UserPending2FA, AccountActivation, Tenant, Company, Department, UserRoleDepartment, Message, Menu, WorkOrder
from gvsdsdk.auth.crypto import HashPassword, VerifyPassword
from gvsdsdk.model_utils import _GetUserModel, UUIDObj
@@ -113,6 +114,21 @@ class RoleManager:
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
return User.objects.filter(UserUUID__in=user_uuids)
@staticmethod
def GetCompanyUserUUIDs(CompanyUUID):
"""获取公司下所有用户 UUID(通过 UserRole 关联,去重)
Args:
CompanyUUID: 公司 UUID(bytes/str/UUIDObj 均可)
Returns:
set[bytes]:公司关联用户 UUID 集合
"""
return set(
UserRole.objects.filter(CompanyUUID=UUIDObj(CompanyUUID).bytes)
.values_list('UserUUID', flat=True)
)
@staticmethod
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
"""为角色分配权限(指定公司)"""
@@ -741,6 +757,15 @@ class AuthManager:
User = _GetUserModel()
return User.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first()
@staticmethod
def CountUsers(UserUUIDs=None):
"""统计用户数量(可按 UUID 集合过滤;空集合返回 0)"""
User = _GetUserModel()
qs = User.objects.all()
if UserUUIDs is not None:
qs = qs.filter(UserUUID__in=UserUUIDs) if UserUUIDs else qs.none()
return qs.count()
@staticmethod
def IsUserNameTaken(UserName):
"""用户名是否已被占用"""
@@ -1255,3 +1280,494 @@ class DepartmentManager:
).values_list('DeptUUID', flat=True)
return Department.objects.filter(DeptUUID__in=list(dept_uuids)).distinct()
class MessageManager:
"""站内信管理器——系统通知与站内信的收发与查询"""
@staticmethod
def _SenderName(SenderUUID):
"""解析发送者用户名(发送者为空视为系统)"""
if not SenderUUID:
return '系统'
User = _GetUserModel()
try:
sender = User.objects.filter(UserUUID=UUIDObj(SenderUUID).bytes).first()
return sender.UserName if sender else '系统'
except Exception:
return '系统'
@staticmethod
def _ToDict(msg, SenderName, PreviewOnly=False):
return {
'MessageUUID': str(msg.MessageUUID) if msg.MessageUUID else '',
'Title': msg.Title or '',
'Content': (msg.Content or '')[:100] if PreviewOnly else (msg.Content or ''),
'MsgType': msg.MsgType or 0,
'IsRead': msg.IsRead or 0,
'SenderName': SenderName,
'RelatedUUID': msg.RelatedUUID or '',
'RelatedType': msg.RelatedType or '',
'CreateTime': msg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if msg.CreateTime else '',
}
@staticmethod
def UnreadCount(UserUUID):
"""获取用户未读消息数"""
return Message.objects.filter(ReceiverUUID=UUIDObj(UserUUID).bytes, IsRead=0).count()
@staticmethod
def RecentMessages(UserUUID, Limit=5):
"""获取最近消息(含发送者名,最多 Limit 条)"""
qs = Message.objects.filter(
ReceiverUUID=UUIDObj(UserUUID).bytes,
).order_by('-CreateTime')[:Limit]
return [MessageManager._ToDict(m, MessageManager._SenderName(m.SenderUUID), PreviewOnly=True) for m in qs]
@staticmethod
def ListMessages(UserUUID, Page=1, PageSize=20, IsRead=None, MsgType=None):
"""分页列出用户消息(可按已读/类型过滤)
Returns:
(total, list[dict])
"""
qs = Message.objects.filter(ReceiverUUID=UUIDObj(UserUUID).bytes)
if IsRead in (0, 1):
qs = qs.filter(IsRead=IsRead)
if MsgType is not None:
qs = qs.filter(MsgType=MsgType)
total = qs.count()
items = qs.order_by('-CreateTime')[(Page - 1) * PageSize:Page * PageSize]
return total, [MessageManager._ToDict(m, MessageManager._SenderName(m.SenderUUID)) for m in items]
@staticmethod
def GetMessageDetail(UserUUID, MessageUUID):
"""获取消息详情并标记已读(不存在返回 None)"""
msg = Message.objects.filter(
MessageUUID=UUIDObj(MessageUUID).bytes,
ReceiverUUID=UUIDObj(UserUUID).bytes,
).first()
if msg is None:
return None
if msg.IsRead != 1:
msg.IsRead = 1
msg.save(update_fields=['IsRead'])
return MessageManager._ToDict(msg, MessageManager._SenderName(msg.SenderUUID))
@staticmethod
def SendMessage(SenderUserUUID, ReceiverUserUUID, Title, Content='',
MsgType=0, RelatedUUID='', RelatedType=''):
"""发送单条站内信;接收者不存在返回 None"""
User = _GetUserModel()
try:
receiver_bytes = UUIDObj(ReceiverUserUUID).bytes
except Exception:
receiver_bytes = ReceiverUserUUID
if not User.objects.filter(UserUUID=receiver_bytes).exists():
return None
sender_bytes = UUIDObj(SenderUserUUID).bytes if SenderUserUUID else None
msg = Message.objects.create(
MessageUUID=uuid.uuid4().bytes,
SenderUUID=sender_bytes,
ReceiverUUID=receiver_bytes,
Title=Title,
Content=Content,
MsgType=MsgType,
IsRead=0,
RelatedUUID=RelatedUUID,
RelatedType=RelatedType,
)
return msg
@staticmethod
def SendRoleMessage(SenderUserUUID, RoleUUID, Title, Content='',
MsgType=0, RelatedUUID='', RelatedType=''):
"""按角色群发站内信(每个持该角色的用户一条);角色不存在返回 -1
Returns:
发送条数
"""
role = RoleManager.GetRoleByUUID(RoleUUID)
if role is None:
return -1
receivers = RoleManager.GetUsersForRole(role.RoleUUID)
count = 0
for receiver in receivers:
msg = MessageManager.SendMessage(
SenderUserUUID, receiver.UserUUID, Title, Content,
MsgType, RelatedUUID, RelatedType,
)
if msg is not None:
count += 1
return count
@staticmethod
def MarkAllRead(UserUUID):
"""全部标记已读;返回更新条数"""
return Message.objects.filter(
ReceiverUUID=UUIDObj(UserUUID).bytes, IsRead=0,
).update(IsRead=1)
@staticmethod
def MarkRead(UserUUID, MessageUUID):
"""单条标记已读;返回是否命中"""
updated = Message.objects.filter(
MessageUUID=UUIDObj(MessageUUID).bytes,
ReceiverUUID=UUIDObj(UserUUID).bytes,
IsRead=0,
).update(IsRead=1)
return updated > 0
@staticmethod
def DeleteMessage(UserUUID, MessageUUID):
"""删除消息;返回是否命中"""
deleted, _ = Message.objects.filter(
MessageUUID=UUIDObj(MessageUUID).bytes,
ReceiverUUID=UUIDObj(UserUUID).bytes,
).delete()
return deleted > 0
@staticmethod
def SearchRecipients(Keyword='', Limit=10):
"""搜索收件人(用户 + 角色)
Returns:
list[dict]:{type: user|role, uuid, name, label}
"""
data = []
User = _GetUserModel()
uqs = User.objects.all()
if Keyword:
uqs = uqs.filter(UserName__icontains=Keyword)
for u in uqs[:Limit]:
uuid_str = UUIDObj(u.UserUUID).str if u.UserUUID else ''
data.append({'type': 'user', 'uuid': uuid_str, 'name': u.UserName, 'label': u.UserName})
rqs = Role.objects.all()
if Keyword:
rqs = rqs.filter(RoleName__icontains=Keyword)
for r in rqs[:Limit]:
uuid_str = UUIDObj(r.RoleUUID).str if r.RoleUUID else ''
data.append({'type': 'role', 'uuid': uuid_str, 'name': r.RoleName, 'label': r.RoleName})
return data
class MenuManager:
"""菜单管理器——菜单查询、保存与递归删除"""
@staticmethod
def _ToFlat(menu):
"""Menu 实例 → 扁平 dict(键兼容 GERP 前端菜单协议)"""
return {
'Id': menu.MenuName,
'Title': menu.MenuTitle,
'Icon': menu.MenuIcon or '',
'Page': menu.MenuLink or '',
'ParentUUID': UUIDObj(menu.ParentMenuUUID).str if menu.ParentMenuUUID else '',
'MenuUUID': UUIDObj(menu.MenuUUID).str if menu.MenuUUID else '',
'CompanyUUID': UUIDObj(menu.CompanyUUID).str if menu.CompanyUUID else '',
'Sort': menu.MenuSort or 0,
'Type': menu.MenuType or 0,
'Children': [],
'PermLevel': menu.PermissionLevel or 0,
'PermAttr': menu.PermissionAttr or '',
}
@staticmethod
def ListVisible(CompanyUUID=None):
"""用户可见菜单(启用状态 + 公司过滤,按 MenuSort/MenuName 排序)
Args:
CompanyUUID: 公司 UUID(bytes/str 均可);传 None 时仅返回公有菜单
Returns:
list[dict]:扁平菜单记录列表
"""
qs = Menu.objects.filter(MenuStatus=1)
if CompanyUUID:
qs = qs.filter(Q(CompanyUUID__isnull=True) | Q(CompanyUUID=CompanyUUID))
else:
qs = qs.filter(CompanyUUID__isnull=True)
qs = qs.order_by('MenuSort', 'MenuName')
return [MenuManager._ToFlat(m) for m in qs]
@staticmethod
def _ToAdminFlat(menu):
"""Menu 实例 → 管理列表 dict(键与 GERP 菜单管理协议一致)"""
return {
'MenuUUID': UUIDObj(menu.MenuUUID).str if menu.MenuUUID else '',
'ParentMenuUUID': UUIDObj(menu.ParentMenuUUID).str if menu.ParentMenuUUID else '',
'CompanyUUID': UUIDObj(menu.CompanyUUID).str if menu.CompanyUUID else '',
'MenuName': menu.MenuName or '',
'MenuTitle': menu.MenuTitle or '',
'MenuIcon': menu.MenuIcon or '',
'MenuLink': menu.MenuLink or '',
'MenuSort': menu.MenuSort if menu.MenuSort is not None else 0,
'MenuType': menu.MenuType if menu.MenuType is not None else 0,
'MenuStatus': menu.MenuStatus if menu.MenuStatus is not None else 0,
'PermissionLevel': menu.PermissionLevel if menu.PermissionLevel is not None else 0,
'PermissionAttr': menu.PermissionAttr or '',
}
@staticmethod
def AdminList(CompanyUUID=None, CompanyProvided=False):
"""菜单管理列表
Args:
CompanyUUID: 公司 UUID(bytes/str 均可)
CompanyProvided: 是否显式提供了 CompanyUUID 参数
(True 且 CompanyUUID 为空 → 仅返回公有菜单;False → 返回全部)
Returns:
list[dict](键为菜单管理协议)
"""
qs = Menu.objects.all()
if CompanyProvided:
if CompanyUUID:
qs = qs.filter(Q(CompanyUUID__isnull=True) | Q(CompanyUUID=CompanyUUID))
else:
qs = qs.filter(CompanyUUID__isnull=True)
qs = qs.order_by('MenuSort', 'MenuName')
return [MenuManager._ToAdminFlat(m) for m in qs]
@staticmethod
def Get(MenuUUID):
"""按 UUID 查询菜单(不存在返回 None)"""
try:
return Menu.objects.get(MenuUUID=UUIDObj(MenuUUID).bytes)
except (Menu.DoesNotExist, ValueError, AttributeError):
return None
@staticmethod
def Save(Data):
"""新增或修改菜单(有 MenuUUID 则修改)
Args:
Data: dict,键与前端协议一致(MenuUUID 可选)
Returns:
(record_dict, err):err 非 None 表示失败(如格式无效/不存在)
"""
menu_uuid_str = (Data.get('MenuUUID') or '').strip()
company_uuid_str = (Data.get('CompanyUUID') or '').strip()
parent_uuid_str = (Data.get('ParentMenuUUID') or '').strip()
# 解析公司(空则不关联公司,即公有菜单)
company_bytes = None
if company_uuid_str:
try:
company_bytes = UUIDObj(company_uuid_str).bytes
except Exception:
return None, 'CompanyUUID 格式无效'
if not Company.objects.filter(CompanyUUID=company_bytes).exists():
return None, '公司不存在'
# 解析父菜单(空则根菜单)
parent_bytes = None
if parent_uuid_str:
try:
parent_bytes = UUIDObj(parent_uuid_str).bytes
except Exception:
return None, 'ParentMenuUUID 格式无效'
if not Menu.objects.filter(MenuUUID=parent_bytes).exists():
return None, '父菜单不存在'
# 通用字段
menu_name = (Data.get('MenuName') or '').strip()
menu_title = (Data.get('MenuTitle') or '').strip()
menu_icon = Data.get('MenuIcon') or ''
menu_link = Data.get('MenuLink') or ''
try:
menu_sort = int(Data.get('MenuSort', '0'))
except (ValueError, TypeError):
menu_sort = 0
try:
menu_type = int(Data.get('MenuType', '0'))
except (ValueError, TypeError):
menu_type = 0
try:
menu_status = int(Data.get('MenuStatus', '1'))
except (ValueError, TypeError):
menu_status = 1
try:
perm_level = float(Data.get('PermissionLevel', '1.0'))
except (ValueError, TypeError):
perm_level = 1.0
perm_attr = Data.get('PermissionAttr') or ''
if menu_uuid_str:
# 修改
try:
menu_uuid = UUIDObj(menu_uuid_str).bytes
except Exception:
return None, 'MenuUUID 格式无效'
menu = Menu.objects.filter(MenuUUID=menu_uuid).first()
if menu is None:
return None, '菜单不存在'
menu.CompanyUUID = company_bytes
menu.ParentMenuUUID = parent_bytes
menu.MenuName = menu_name
menu.MenuTitle = menu_title
menu.MenuIcon = menu_icon
menu.MenuLink = menu_link
menu.MenuSort = menu_sort
menu.MenuType = menu_type
menu.MenuStatus = menu_status
menu.PermissionLevel = perm_level
menu.PermissionAttr = perm_attr
menu.save()
return MenuManager._ToFlat(menu), None
# 新增
menu = Menu.objects.create(
MenuUUID=uuid.uuid4().bytes,
CompanyUUID=company_bytes,
ParentMenuUUID=parent_bytes,
MenuName=menu_name or 'new-menu',
MenuTitle=menu_title or '新菜单',
MenuIcon=menu_icon,
MenuLink=menu_link,
MenuSort=menu_sort,
MenuType=menu_type,
MenuStatus=menu_status,
PermissionLevel=perm_level,
PermissionAttr=perm_attr,
)
return MenuManager._ToFlat(menu), None
@staticmethod
def Delete(MenuUUID):
"""递归删除菜单及其子孙节点
Args:
MenuUUID: 菜单 UUID(bytes/str 均可)
Returns:
(成功?, 错误消息 or None)
"""
try:
menu_uuid = UUIDObj(MenuUUID).bytes
except Exception:
return False, 'MenuUUID 格式无效'
menu = Menu.objects.filter(MenuUUID=menu_uuid).first()
if menu is None:
return False, '菜单不存在'
def _DeleteDescendants(parent_uuid):
for child in Menu.objects.filter(ParentMenuUUID=parent_uuid):
_DeleteDescendants(child.MenuUUID)
child.delete()
_DeleteDescendants(menu.MenuUUID)
menu.delete()
return True, None
class WorkOrderManager:
"""工单管理器——工单查询、统计、创建与更新"""
@staticmethod
def _ToFlat(wo):
"""WorkOrder 实例 → dict(字段与 WorkOrderSerializer 输出一致)"""
return {
'OrderUUID': UUIDObj(wo.OrderUUID).str if wo.OrderUUID else '',
'Title': wo.Title or '',
'Description': wo.Description or '',
'Category': wo.Category or '',
'Priority': wo.Priority or 0,
'Status': wo.Status or 0,
'CreatorUUID': UUIDObj(wo.CreatorUUID).str if wo.CreatorUUID else '',
'AssigneeUUID': UUIDObj(wo.AssigneeUUID).str if wo.AssigneeUUID else '',
'Resolution': wo.Resolution or '',
'CreateTime': wo.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if wo.CreateTime else '',
'UpdateTime': wo.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if wo.UpdateTime else '',
'CloseTime': wo.CloseTime.strftime('%Y-%m-%d %H:%M:%S') if wo.CloseTime else '',
}
@staticmethod
def GetStats():
"""工单统计——按状态/优先级/分类计数
Returns:
dict: {total, byStatus, byPriority, byCategory}
"""
from django.db.models import Count
qs = WorkOrder.objects.all()
def _group(field_name):
return {row[field_name]: row['c'] for row in qs.values(field_name).annotate(c=Count('pk'))}
return {
'total': qs.count(),
'byStatus': _group('Status'),
'byPriority': _group('Priority'),
'byCategory': _group('Category'),
}
@staticmethod
def ListForUser(UserUUID):
"""获取用户创建的工单列表(按创建时间倒序)"""
return [
WorkOrderManager._ToFlat(wo)
for wo in WorkOrder.objects.filter(CreatorUUID=UUIDObj(UserUUID).bytes)
]
@staticmethod
def Create(CreatorUUID, Title, Description='', Category='other',
Priority=0, AssigneeUUID=None):
"""创建工单
Returns:
dict:新工单记录
"""
assignee_bytes = None
if AssigneeUUID:
try:
assignee_bytes = UUIDObj(AssigneeUUID).bytes
except Exception:
assignee_bytes = None
wo = WorkOrder.objects.create(
OrderUUID=uuid.uuid4().bytes,
Title=Title,
Description=Description,
Category=Category,
Priority=Priority,
Status=0,
CreatorUUID=UUIDObj(CreatorUUID).bytes,
AssigneeUUID=assignee_bytes,
)
return WorkOrderManager._ToFlat(wo)
@staticmethod
def Get(OrderUUID):
"""按 UUID 查询工单(不存在返回 None)"""
try:
return WorkOrder.objects.get(OrderUUID=UUIDObj(OrderUUID).bytes)
except (WorkOrder.DoesNotExist, ValueError, AttributeError):
return None
@staticmethod
def Update(OrderUUID, Data):
"""更新工单(Status/AssigneeUUID/Resolution/Description/Priority,自动刷新 UpdateTime)
Args:
Data: 变更字段 dict(键同模型字段名)
Returns:
dict:更新后的工单记录;工单不存在返回 None
"""
wo = WorkOrderManager.Get(OrderUUID)
if wo is None:
return None
field_map = {
'Status': 'Status',
'AssigneeUUID': 'AssigneeUUID',
'Resolution': 'Resolution',
'Description': 'Description',
'Priority': 'Priority',
}
for key in field_map:
if key in Data:
setattr(wo, field_map[key], Data[key])
wo.UpdateTime = _timezone.now()
wo.save()
return WorkOrderManager._ToFlat(wo)
+150
View File
@@ -537,6 +537,97 @@ class UserRoleDepartment(QModel):
verbose_name_plural = '用户-角色部门关联'
# === 消息表 ===
class Message(QModel):
"""消息表——系统通知与站内信(映射 SAAS 主库 Message 表)"""
MessageUUID = UUIDField(primary_key=True, db_column='MessageUUID', verbose_name='消息UUID')
SenderUUID = UUIDField(null=True, blank=True, db_column='SenderUUID', verbose_name='发送者UUID')
ReceiverUUID = UUIDField(db_index=True, db_column='ReceiverUUID', verbose_name='接收者UUID')
Title = models.CharField(max_length=256, db_column='Title', verbose_name='标题')
Content = models.TextField(default='', db_column='Content', verbose_name='内容')
MsgType = models.SmallIntegerField(default=0, db_column='MsgType', verbose_name='消息类型')
IsRead = models.SmallIntegerField(default=0, db_column='IsRead', verbose_name='已读')
RelatedUUID = models.CharField(max_length=64, default='', db_column='RelatedUUID', verbose_name='关联UUID')
RelatedType = models.CharField(max_length=32, default='', db_column='RelatedType', verbose_name='关联类型')
CreateTime = models.DateTimeField(default=_UtcNow, db_column='CreateTime', verbose_name='创建时间')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'Message'
verbose_name = '消息'
verbose_name_plural = '消息'
def __str__(self):
return self.Title
# === 个人中心表 ===
class UserIDCard(QModel):
"""用户身份证表——实名信息(映射 SAAS 主库 UserIDCard 表)"""
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID')
Name = models.CharField(max_length=128, db_column='Name', verbose_name='姓名')
OldName = models.CharField(max_length=128, default='', db_column='OldName', verbose_name='曾用名')
Born = models.DateField(db_column='Born', verbose_name='出生日期')
Sex = models.CharField(max_length=10, db_column='Sex', verbose_name='性别')
Ethnic = models.CharField(max_length=20, db_column='Ethnic', verbose_name='民族')
IdentificationNumber = models.CharField(max_length=20, db_column='IdentificationNumber', verbose_name='身份证号')
StudentRegistrationNumber = models.CharField(max_length=30, default='', db_column='StudentRegistrationNumber', verbose_name='学籍号')
ChangeStudentStatus = models.JSONField(default=list, db_column='ChangeStudentStatus', verbose_name='学籍变动')
CorrespondenceAddress = models.JSONField(default=list, db_column='CorrespondenceAddress', verbose_name='通讯地址')
HeadImage = models.CharField(max_length=4096, default='', db_column='HeadImage', verbose_name='头像')
InformationAboutFamilyMembers = models.JSONField(default=dict, db_column='InformationAboutFamilyMembers', verbose_name='家庭成员信息')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'UserIDCard'
verbose_name = '用户身份证'
verbose_name_plural = '用户身份证'
class PersonalPage(QModel):
"""个人主页表——用户展示页面(映射 SAAS 主库 PersonalPage 表)"""
PersonalPageUserUUID = UUIDField(primary_key=True, db_column='PersonalPageUserUUID', verbose_name='用户UUID')
PersonalPageName = models.CharField(max_length=256, unique=True, db_column='PersonalPageName', verbose_name='主页名称')
Subtitle = models.CharField(max_length=256, default='', db_column='Subtitle', verbose_name='副标题')
BackgroundLink = models.CharField(max_length=256, default='', db_column='BackgroundLink', verbose_name='背景链接')
Classification = models.CharField(max_length=256, default='', db_column='Classification', verbose_name='分类')
Github = models.CharField(max_length=256, default='', db_column='Github', verbose_name='GitHub')
Wechat = models.CharField(max_length=256, default='', db_column='Wechat', verbose_name='微信')
QQ = models.CharField(max_length=256, default='', db_column='QQ', verbose_name='QQ')
SelfIntroduction = models.TextField(default='', db_column='SelfIntroduction', verbose_name='自我介绍')
ProjectExperience = models.CharField(max_length=256, default='', db_column='ProjectExperience', verbose_name='项目经验')
SatisfiedCustomers = models.CharField(max_length=256, default='', db_column='SatisfiedCustomers', verbose_name='满意客户')
ReceiveAward = models.CharField(max_length=256, default='', db_column='ReceiveAward', verbose_name='获奖')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'PersonalPage'
verbose_name = '个人主页'
verbose_name_plural = '个人主页'
class UserSettings(QModel):
"""用户设置表——存储每位用户的个性化系统设置(映射 SAAS 主库 UserSettings 表)"""
UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID')
SettingsData = models.TextField(default='', db_column='SettingsData', verbose_name='设置数据')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'UserSettings'
verbose_name = '用户设置'
verbose_name_plural = '用户设置'
# === 角色表 ===
class Role(QModel):
@@ -900,3 +991,62 @@ class Position(QModel):
return self.PositionName
# (UserDept 表已废弃——员工部门归属已迁移到 UserRoleDepartment 关联表,不再用独立表)
# === 菜单表 ===
class Menu(QModel):
"""菜单表——系统导航与功能入口(映射 SAAS 主库 Menu 表)"""
MenuUUID = UUIDField(primary_key=True, db_column='MenuUUID', verbose_name='菜单UUID')
CompanyUUID = UUIDField(null=True, blank=True, db_column='CompanyUUID', verbose_name='公司UUID')
ParentMenuUUID = UUIDField(null=True, blank=True, db_column='ParentMenuUUID', verbose_name='上级菜单UUID')
MenuName = models.CharField(max_length=128, db_column='MenuName', verbose_name='菜单名称')
MenuTitle = models.CharField(max_length=128, db_column='MenuTitle', verbose_name='菜单标题')
MenuIcon = models.CharField(max_length=128, default='', db_column='MenuIcon', verbose_name='菜单图标')
MenuLink = models.CharField(max_length=256, default='', db_column='MenuLink', verbose_name='菜单链接')
MenuSort = models.SmallIntegerField(default=0, db_column='MenuSort', verbose_name='排序')
PermissionLevel = models.FloatField(default=1.0, db_column='PermissionLevel', verbose_name='权限级别')
PermissionAttr = models.CharField(max_length=256, default='', db_column='PermissionAttr', verbose_name='权限属性')
MenuStatus = models.SmallIntegerField(default=1, db_column='MenuStatus', verbose_name='菜单状态')
MenuType = models.SmallIntegerField(default=0, db_column='MenuType', verbose_name='菜单类型')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'Menu'
verbose_name = '菜单'
verbose_name_plural = '菜单'
def __str__(self):
return self.MenuTitle or self.MenuName
# === 工单表 ===
class WorkOrder(QModel):
"""工单表——任务与问题跟踪(映射 SAAS 主库 WorkOrder 表)"""
OrderUUID = UUIDField(primary_key=True, db_column='OrderUUID', verbose_name='工单UUID')
Title = models.CharField(max_length=256, db_column='Title', verbose_name='标题')
Description = models.CharField(max_length=4096, default='', db_column='Description', verbose_name='描述')
Category = models.CharField(max_length=64, default='other', db_column='Category', verbose_name='分类')
Priority = models.SmallIntegerField(default=0, db_column='Priority', verbose_name='优先级')
Status = models.SmallIntegerField(default=0, db_column='Status', verbose_name='状态')
CreatorUUID = UUIDField(db_column='CreatorUUID', db_index=True, verbose_name='创建人UUID')
AssigneeUUID = UUIDField(null=True, blank=True, db_column='AssigneeUUID', verbose_name='指派人UUID')
Resolution = models.CharField(max_length=4096, default='', db_column='Resolution', verbose_name='解决方案')
CreateTime = models.DateTimeField(default=_UtcNow, db_column='CreateTime', verbose_name='创建时间')
UpdateTime = models.DateTimeField(null=True, blank=True, db_column='UpdateTime', verbose_name='更新时间')
CloseTime = models.DateTimeField(null=True, blank=True, db_column='CloseTime', verbose_name='关闭时间')
class Meta:
managed = False
app_label = 'gvsdsdk'
db_table = 'WorkOrder'
ordering = ['-CreateTime']
verbose_name = '工单'
verbose_name_plural = '工单'
def __str__(self):
return self.Title
+39
View File
@@ -28,6 +28,7 @@ SAAS 级订单编排器——子服务通过此管理器操作订单生命周期
"""
import uuid as _uuid
import logging
import datetime
from decimal import Decimal
from typing import Optional, List
@@ -119,6 +120,44 @@ class OrderManager:
records = list(qs[start:start + page_size])
return {'records': records, 'total': total, 'page': page, 'page_size': page_size}
@staticmethod
def GetStats(tenant_uuid=None, company_uuid=None, buyer_user_uuids=None):
"""仪表盘订单统计——按租户/公司/买家多维度计数
Args:
tenant_uuid: 租户 UUID(None 不限制)
company_uuid: 公司 UUID(None 不限制)
buyer_user_uuids: 买家 UUID 集合(None 不限制;空集合返回全 0)
Returns:
dict: TotalOrders/TodayOrders/WeekOrders/PendingOrders/
PaidOrders/CompletedOrders
"""
now = _timezone.now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - datetime.timedelta(days=today_start.weekday())
qs = Order.objects.all()
if tenant_uuid:
qs = qs.filter(TenantUUID=tenant_uuid)
if company_uuid:
qs = qs.filter(CompanyUUID=company_uuid)
if buyer_user_uuids is not None:
buyer_bytes = [OrderManager._to_bytes(u) for u in buyer_user_uuids]
buyer_bytes = [b for b in buyer_bytes if b]
qs = qs.filter(BuyerUserUUID__in=buyer_bytes) if buyer_bytes else qs.none()
return {
'TotalOrders': qs.count(),
'TodayOrders': qs.filter(CreateTime__gte=today_start).count(),
'WeekOrders': qs.filter(CreateTime__gte=week_start).count(),
'PendingOrders': qs.filter(OrderStatus=Order.ORD_PENDING).count(),
'PaidOrders': qs.filter(
OrderStatus__in=[Order.ORD_PAID, Order.ORD_PROCESSING, Order.ORD_COMPLETED]
).count(),
'CompletedOrders': qs.filter(OrderStatus=Order.ORD_COMPLETED).count(),
}
def GetOrderItems(self, order_uuid):
"""获取订单行项目列表"""
order_uuid_bytes = self._to_bytes(order_uuid)