"refactor(gerp): 菜单与工单 API 委托 gvsdsdk
menu.py 四视图与 WorkOrderViewSet 自定义 action、WorkOrderStatsView 改经 MenuManager/WorkOrderManager,移除视图层裸 ORM 查询。"
This commit is contained in:
1 parent
f6b057bc19
commit
e2015164ea
12 files changed
+856
-447
No files matched your search
@@ -6,18 +6,18 @@ GET /API/Dashboard/Stats
|
|||||||
- dept_manager: 本部门统计
|
- dept_manager: 本部门统计
|
||||||
- workgroup_lead: 本工作组统计
|
- workgroup_lead: 本工作组统计
|
||||||
- member: 个人统计
|
- member: 个人统计
|
||||||
|
|
||||||
|
统计逻辑委托 gvsdsdk:用户数经 RoleManager/AuthManager,订单数经 OrderManager。
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
|
||||||
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from . import ELTAuthenticatedAPIView
|
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.role_scope import GetUserScope, _get_scope_members
|
||||||
from gvsds.apps.gerp.models import UserRole
|
from gvsdsdk.auth.managers import AuthManager, RoleManager
|
||||||
from gvsds.apps.core.models import User
|
from gvsdsdk.order.manager import OrderManager
|
||||||
from gvsdsdk.order.models import Order
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -29,11 +29,6 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
|
|||||||
_, current_company_uuid, current_tenant_uuid = _get_current_context(request)
|
_, current_company_uuid, current_tenant_uuid = _get_current_context(request)
|
||||||
scope = GetUserScope(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 = {
|
stats = {
|
||||||
'Scope': scope.Type,
|
'Scope': scope.Type,
|
||||||
'TotalUsers': 0,
|
'TotalUsers': 0,
|
||||||
@@ -48,17 +43,11 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
|
|||||||
try:
|
try:
|
||||||
# 用户统计
|
# 用户统计
|
||||||
if scope.Type == 'admin':
|
if scope.Type == 'admin':
|
||||||
# 管理员:全公司用户
|
# 管理员:全公司用户(无公司上下文时统计全量)
|
||||||
user_qs = User.objects.all()
|
company_user_uuids = None
|
||||||
if current_company_uuid:
|
if current_company_uuid:
|
||||||
# 通过 UserRole 关联查找本公司用户
|
company_user_uuids = RoleManager.GetCompanyUserUUIDs(current_company_uuid)
|
||||||
company_user_uuids = set(
|
stats['TotalUsers'] = AuthManager.CountUsers(company_user_uuids)
|
||||||
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()
|
|
||||||
elif scope.Type in ('dept_manager', 'workgroup_lead'):
|
elif scope.Type in ('dept_manager', 'workgroup_lead'):
|
||||||
member_uuids = _get_scope_members(scope)
|
member_uuids = _get_scope_members(scope)
|
||||||
if member_uuids:
|
if member_uuids:
|
||||||
@@ -67,28 +56,19 @@ class DashboardStatsView(ELTAuthenticatedAPIView):
|
|||||||
stats['TotalUsers'] = 1
|
stats['TotalUsers'] = 1
|
||||||
|
|
||||||
# 订单统计
|
# 订单统计
|
||||||
order_qs = Order.objects.all()
|
buyer_uuids = None
|
||||||
if current_tenant_uuid:
|
if scope.Type == 'member':
|
||||||
order_qs = order_qs.filter(TenantUUID=current_tenant_uuid)
|
buyer_uuids = [scope.UserUUID]
|
||||||
|
|
||||||
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)
|
|
||||||
elif scope.Type in ('dept_manager', 'workgroup_lead'):
|
elif scope.Type in ('dept_manager', 'workgroup_lead'):
|
||||||
|
# 无成员上下文视为空集合(统计全 0),与 None 区分
|
||||||
member_uuids = _get_scope_members(scope)
|
member_uuids = _get_scope_members(scope)
|
||||||
if member_uuids:
|
buyer_uuids = member_uuids if member_uuids is not None else []
|
||||||
order_qs = order_qs.filter(BuyerUserUUID__in=member_uuids)
|
|
||||||
else:
|
|
||||||
order_qs = order_qs.none()
|
|
||||||
|
|
||||||
stats['TotalOrders'] = order_qs.count()
|
stats.update(OrderManager.GetStats(
|
||||||
stats['TodayOrders'] = order_qs.filter(CreateTime__gte=today_start).count()
|
tenant_uuid=current_tenant_uuid,
|
||||||
stats['WeekOrders'] = order_qs.filter(CreateTime__gte=week_start).count()
|
company_uuid=current_company_uuid if scope.Type == 'admin' else None,
|
||||||
stats['PendingOrders'] = order_qs.filter(OrderStatus=Order.ORD_PENDING).count()
|
buyer_user_uuids=buyer_uuids,
|
||||||
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()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning('[DashboardStats] 统计计算失败', exc_info=True)
|
logger.warning('[DashboardStats] 统计计算失败', exc_info=True)
|
||||||
|
|
||||||
|
|||||||
@@ -464,22 +464,17 @@ class WorkOrderStatsView(ELTAuthenticatedAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
from django.db.models import Count
|
from gvsdsdk.auth.managers import WorkOrderManager
|
||||||
from gvsds.apps.gerp.models import WorkOrder
|
|
||||||
from gvsds.apps.gerp.form_service import FormService
|
from gvsds.apps.gerp.form_service import FormService
|
||||||
|
|
||||||
qs = WorkOrder.objects.all()
|
stats = WorkOrderManager.GetStats()
|
||||||
|
|
||||||
def _group(field_name):
|
|
||||||
return {row[field_name]: row['c'] for row in qs.values(field_name).annotate(c=Count('pk'))}
|
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'data': {
|
'data': {
|
||||||
'total': qs.count(),
|
'total': stats['total'],
|
||||||
'byStatus': _group('Status'),
|
'byStatus': stats['byStatus'],
|
||||||
'byPriority': _group('Priority'),
|
'byPriority': stats['byPriority'],
|
||||||
'byCategory': _group('Category'),
|
'byCategory': stats['byCategory'],
|
||||||
# 附状态/优先级中文标签,前端可直接渲染
|
# 附状态/优先级中文标签,前端可直接渲染
|
||||||
'statusOptions': FormService.WORKORDER_STATUS_OPTIONS['Status'],
|
'statusOptions': FormService.WORKORDER_STATUS_OPTIONS['Status'],
|
||||||
'priorityOptions': FormService.WORKORDER_PRIORITY_OPTIONS['Priority'],
|
'priorityOptions': FormService.WORKORDER_PRIORITY_OPTIONS['Priority'],
|
||||||
|
|||||||
+16
-173
@@ -15,9 +15,8 @@
|
|||||||
|
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
import logging
|
import logging
|
||||||
from django.db.models import Q
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from gvsds.apps.gerp.models import Menu, Company
|
from gvsdsdk.auth.managers import MenuManager
|
||||||
from . import ELTAuthenticatedAPIView
|
from . import ELTAuthenticatedAPIView
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -79,43 +78,13 @@ class MenuListCompatView(ELTAuthenticatedAPIView):
|
|||||||
# 管理员特权:仅跳过权限过滤,公司过滤对所有用户生效(防止跨公司私有菜单泄漏)
|
# 管理员特权:仅跳过权限过滤,公司过滤对所有用户生效(防止跨公司私有菜单泄漏)
|
||||||
is_admin = _IsAdminUser(user, role_uuid_bytes)
|
is_admin = _IsAdminUser(user, role_uuid_bytes)
|
||||||
|
|
||||||
# 公司过滤(对所有用户生效,包括管理员)
|
# 公司过滤(对所有用户生效,包括管理员)——经 MenuManager 委托查询
|
||||||
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')
|
|
||||||
|
|
||||||
# 构建扁平映射
|
|
||||||
menu_map = {}
|
menu_map = {}
|
||||||
for menu in menus:
|
for item in MenuManager.ListVisible(current_company):
|
||||||
menu_uuid_str = str(menu.MenuUUID) if menu.MenuUUID else ''
|
menu_uuid_str = item['MenuUUID']
|
||||||
if not menu_uuid_str:
|
if not menu_uuid_str:
|
||||||
continue
|
continue
|
||||||
parent_uuid_str = ''
|
menu_map[menu_uuid_str] = item
|
||||||
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 '',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 祖先链校验:任何祖先不属于当前公司 → 整个分支移除
|
# 祖先链校验:任何祖先不属于当前公司 → 整个分支移除
|
||||||
# 对所有用户生效(包括管理员),防止公有子节点挂在私有父节点下导致结构泄漏
|
# 对所有用户生效(包括管理员),防止公有子节点挂在私有父节点下导致结构泄漏
|
||||||
@@ -248,31 +217,9 @@ class MenuAdminListView(ELTAuthenticatedAPIView):
|
|||||||
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
||||||
|
|
||||||
company_uuid_str = (request.query_params.get('CompanyUUID') or '').strip()
|
company_uuid_str = (request.query_params.get('CompanyUUID') or '').strip()
|
||||||
qs = Menu.objects.all()
|
company_provided = bool(company_uuid_str)
|
||||||
if company_uuid_str:
|
company_uuid_bytes = _ParseUUID(company_uuid_str) if company_provided else None
|
||||||
company_uuid_bytes = _ParseUUID(company_uuid_str)
|
records = MenuManager.AdminList(company_uuid_bytes, company_provided)
|
||||||
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 '',
|
|
||||||
})
|
|
||||||
return Response({'code': 0, 'data': records, 'total': len(records)})
|
return Response({'code': 0, 'data': records, 'total': len(records)})
|
||||||
|
|
||||||
|
|
||||||
@@ -292,100 +239,11 @@ class MenuSaveView(ELTAuthenticatedAPIView):
|
|||||||
if user is None:
|
if user is None:
|
||||||
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
||||||
|
|
||||||
def _get(key, default=''):
|
record, err = MenuManager.Save(dict(request.data))
|
||||||
v = request.data.get(key, default)
|
if err:
|
||||||
return '' if v is None else v
|
return Response({'code': 1, 'msg': err, 'data': None})
|
||||||
|
menu_uuid_str = record['MenuUUID']
|
||||||
menu_uuid_str = _get('MenuUUID', '').strip()
|
return Response({'code': 0, 'id': menu_uuid_str, 'data': {'MenuUUID': menu_uuid_str}})
|
||||||
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}})
|
|
||||||
|
|
||||||
|
|
||||||
class MenuDeleteView(ELTAuthenticatedAPIView):
|
class MenuDeleteView(ELTAuthenticatedAPIView):
|
||||||
@@ -404,22 +262,7 @@ class MenuDeleteView(ELTAuthenticatedAPIView):
|
|||||||
if not menu_uuid_str:
|
if not menu_uuid_str:
|
||||||
return Response({'code': 1, 'msg': '缺少 MenuUUID', 'data': None})
|
return Response({'code': 1, 'msg': '缺少 MenuUUID', 'data': None})
|
||||||
|
|
||||||
menu_uuid_bytes = _ParseUUID(menu_uuid_str)
|
ok, err = MenuManager.Delete(menu_uuid_str)
|
||||||
if not menu_uuid_bytes:
|
if not ok:
|
||||||
return Response({'code': 1, 'msg': 'MenuUUID 格式无效', 'data': None})
|
return Response({'code': 1, 'msg': err, '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()
|
|
||||||
return Response({'code': 0, 'data': None})
|
return Response({'code': 0, 'data': None})
|
||||||
@@ -1,127 +1,74 @@
|
|||||||
import uuid
|
import logging
|
||||||
import datetime
|
|
||||||
from rest_framework.response import Response
|
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
|
from . import ELTAuthenticatedAPIView
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MessageCompatView(ELTAuthenticatedAPIView):
|
class MessageCompatView(ELTAuthenticatedAPIView):
|
||||||
"""消息兼容视图——站内信收发与查询"""
|
"""消息兼容视图——站内信收发与查询(委托 gvsdsdk.MessageManager)"""
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
action = request.query_params.get('action', 'unread_count')
|
action = request.query_params.get('action', 'unread_count')
|
||||||
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
||||||
if not user:
|
if not user:
|
||||||
return Response({'code': 0, 'UnreadCount': 0, 'RecentMessages': []})
|
return Response({'code': 0, 'UnreadCount': 0, 'RecentMessages': []})
|
||||||
|
from gvsdsdk.auth.managers import MessageManager
|
||||||
# request.user 是 gvsdsdk.User,但 Message.ReceiverUUID/SenderUUID 是 FK 指向 core.User
|
|
||||||
# 使用 _id 后缀直接传列值避免类型不匹配
|
|
||||||
user_uuid = user.UserUUID
|
user_uuid = user.UserUUID
|
||||||
|
|
||||||
if action == 'unread_count':
|
if action == 'unread_count':
|
||||||
count = Message.objects.filter(ReceiverUUID_id=user_uuid, IsRead=0).count()
|
count = MessageManager.UnreadCount(user_uuid)
|
||||||
recent = Message.objects.filter(ReceiverUUID_id=user_uuid).select_related('SenderUUID').order_by('-CreateTime')[:5]
|
recent = MessageManager.RecentMessages(user_uuid, Limit=5)
|
||||||
recent_data = []
|
return Response({'code': 0, 'UnreadCount': count, 'RecentMessages': recent})
|
||||||
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})
|
|
||||||
|
|
||||||
elif action == 'list':
|
if action == 'list':
|
||||||
# 分页参数:兼容前端协议 PageIndex/PageSize(原生 page/pageSize 兜底)
|
# 分页参数:兼容前端协议 PageIndex/PageSize(原生 page/pageSize 兜底)
|
||||||
page = int(request.query_params.get('PageIndex', request.query_params.get('page', 1)))
|
try:
|
||||||
page_size = int(request.query_params.get('PageSize', request.query_params.get('pageSize', 20)))
|
page = int(request.query_params.get('PageIndex', request.query_params.get('page', 1)))
|
||||||
qs = Message.objects.filter(ReceiverUUID_id=user_uuid).select_related('SenderUUID').order_by('-CreateTime')
|
except (TypeError, ValueError):
|
||||||
# 筛选:IsRead(0 未读 / 1 已读)、MsgType(0 系统 / 1 审核 / 2 工单 / 3 个人)
|
page = 1
|
||||||
is_read = request.query_params.get('IsRead')
|
try:
|
||||||
if is_read in ('0', '1'):
|
page_size = int(request.query_params.get('PageSize', request.query_params.get('pageSize', 20)))
|
||||||
qs = qs.filter(IsRead=int(is_read))
|
except (TypeError, ValueError):
|
||||||
msg_type = request.query_params.get('MsgType')
|
page_size = 20
|
||||||
if msg_type and msg_type.isdigit():
|
is_read_raw = request.query_params.get('IsRead')
|
||||||
qs = qs.filter(MsgType=int(msg_type))
|
is_read = int(is_read_raw) if is_read_raw in ('0', '1') else None
|
||||||
total = qs.count()
|
msg_type_raw = request.query_params.get('MsgType')
|
||||||
items = qs[(page - 1) * page_size:page * page_size]
|
msg_type = int(msg_type_raw) if msg_type_raw and msg_type_raw.isdigit() else None
|
||||||
data = []
|
total, data = MessageManager.ListMessages(
|
||||||
for msg in items:
|
user_uuid, Page=page, PageSize=page_size,
|
||||||
sender_name = "\u7cfb\u7edf"
|
IsRead=is_read, MsgType=msg_type,
|
||||||
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 '',
|
|
||||||
})
|
|
||||||
return Response({'code': 0, 'total': total, 'data': data})
|
return Response({'code': 0, 'total': total, 'data': data})
|
||||||
|
|
||||||
elif action == 'detail':
|
if action == 'detail':
|
||||||
# 消息UUID:兼容前端协议 MessageUUID(原生 uuid 兜底)
|
# 消息UUID:兼容前端协议 MessageUUID(原生 uuid 兜底)
|
||||||
message_uuid_str = request.query_params.get('MessageUUID', '') or request.query_params.get('uuid', '')
|
message_uuid_str = request.query_params.get('MessageUUID', '') or request.query_params.get('uuid', '')
|
||||||
if not message_uuid_str:
|
if not message_uuid_str:
|
||||||
return Response({'code': 1, 'msg': '\u7f3a\u5c11MessageUUID\u53c2\u6570'}, status=400)
|
return Response({'code': 1, 'msg': '缺少MessageUUID参数'}, status=400)
|
||||||
msg = Message.objects.filter(MessageUUID=message_uuid_str, ReceiverUUID_id=user_uuid).select_related('SenderUUID').first()
|
data = MessageManager.GetMessageDetail(user_uuid, message_uuid_str)
|
||||||
if not msg:
|
if data is None:
|
||||||
return Response({'code': 1, 'msg': '\u6d88\u606f\u4e0d\u5b58\u5728'}, status=404)
|
return Response({'code': 1, 'msg': '消息不存在'}, 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': 0, 'data': data})
|
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):
|
def post(self, request):
|
||||||
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
||||||
if not user:
|
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
|
user_uuid = user.UserUUID
|
||||||
title = request.data.get('Title', '')
|
title = request.data.get('Title', '')
|
||||||
content = request.data.get('Content', '')
|
content = request.data.get('Content', '')
|
||||||
@@ -131,67 +78,52 @@ class MessageCompatView(ELTAuthenticatedAPIView):
|
|||||||
related_uuid_str = request.data.get('RelatedUUID', '')
|
related_uuid_str = request.data.get('RelatedUUID', '')
|
||||||
related_type = request.data.get('RelatedType', '')
|
related_type = request.data.get('RelatedType', '')
|
||||||
if not title or not receiver_uuid_str:
|
if not title or not receiver_uuid_str:
|
||||||
return Response({'code': 1, 'msg': '\u7f3a\u5c11\u5fc5\u8981\u53c2\u6570'}, status=400)
|
return Response({'code': 1, 'msg': '缺少必要参数'}, 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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if receiver_type == 'role':
|
if receiver_type == 'role':
|
||||||
# 角色群发:给持有该角色的所有用户各发一条(复用 UserRole 关联,去重)
|
# 角色群发:给持有该角色的所有用户各发一条
|
||||||
role_uuid_str = receiver_uuid_str
|
sent = MessageManager.SendRoleMessage(
|
||||||
from gvsdsdk.auth.managers import RoleManager
|
user_uuid, receiver_uuid_str, title, content,
|
||||||
role = RoleManager.GetRoleByUUID(role_uuid_str)
|
msg_type, related_uuid_str, related_type,
|
||||||
if role is None:
|
)
|
||||||
|
if sent < 0:
|
||||||
return Response({'code': 1, 'msg': '角色不存在'}, status=400)
|
return Response({'code': 1, 'msg': '角色不存在'}, status=400)
|
||||||
receivers = RoleManager.GetUsersForRole(role.RoleUUID)
|
return Response({'code': 0, 'msg': '发送成功'})
|
||||||
for receiver in receivers:
|
|
||||||
_make_message(receiver).save()
|
msg = MessageManager.SendMessage(
|
||||||
return Response({'code': 0, 'msg': '\u53d1\u9001\u6210\u529f'})
|
user_uuid, receiver_uuid_str, title, content,
|
||||||
try:
|
msg_type, related_uuid_str, related_type,
|
||||||
receiver = User.objects.get(UserUUID=receiver_uuid_str)
|
)
|
||||||
except User.DoesNotExist:
|
if msg is None:
|
||||||
return Response({'code': 1, 'msg': '\u63a5\u6536\u8005\u4e0d\u5b58\u5728'}, status=400)
|
return Response({'code': 1, 'msg': '接收者不存在'}, status=400)
|
||||||
_make_message(receiver).save()
|
return Response({'code': 0, 'msg': '发送成功'})
|
||||||
return Response({'code': 0, 'msg': '\u53d1\u9001\u6210\u529f'})
|
|
||||||
|
|
||||||
def patch(self, request):
|
def patch(self, request):
|
||||||
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
||||||
if not user:
|
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
|
# 全部已读:body 携带 action=read_all
|
||||||
if request.data.get('action') == 'read_all':
|
if request.data.get('action') == 'read_all':
|
||||||
Message.objects.filter(ReceiverUUID_id=user.UserUUID, IsRead=0).update(IsRead=1)
|
MessageManager.MarkAllRead(user.UserUUID)
|
||||||
return Response({'code': 0, 'msg': '\u5df2\u5168\u90e8\u6807\u8bb0\u4e3a\u5df2\u8bfb'})
|
return Response({'code': 0, 'msg': '已全部标记为已读'})
|
||||||
uuid_str = request.data.get('UUID', '')
|
uuid_str = request.data.get('UUID', '')
|
||||||
if not uuid_str:
|
if not uuid_str:
|
||||||
return Response({'code': 1, 'msg': '\u7f3a\u5c11uuid\u53c2\u6570'}, status=400)
|
return Response({'code': 1, 'msg': '缺少uuid参数'}, status=400)
|
||||||
msg = Message.objects.filter(MessageUUID=uuid_str, ReceiverUUID_id=user.UserUUID).first()
|
if not MessageManager.MarkRead(user.UserUUID, uuid_str):
|
||||||
if not msg:
|
return Response({'code': 1, 'msg': '消息不存在'}, status=404)
|
||||||
return Response({'code': 1, 'msg': '\u6d88\u606f\u4e0d\u5b58\u5728'}, status=404)
|
return Response({'code': 0, 'msg': '已标记为已读'})
|
||||||
msg.IsRead = 1
|
|
||||||
msg.save(update_fields=['IsRead'])
|
|
||||||
return Response({'code': 0, 'msg': '\u5df2\u6807\u8bb0\u4e3a\u5df2\u8bfb'})
|
|
||||||
|
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
user = request.user if hasattr(request, 'user') and request.user.IsAuthenticated else None
|
||||||
if not user:
|
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:兼容前端协议(DELETE body MessageUUID),原生 uuid(query)/UUID(body)兜底
|
||||||
uuid_str = (request.data.get('MessageUUID', '')
|
uuid_str = (request.data.get('MessageUUID', '')
|
||||||
or request.query_params.get('uuid', '')
|
or request.query_params.get('uuid', '')
|
||||||
or request.data.get('UUID', ''))
|
or request.data.get('UUID', ''))
|
||||||
if not uuid_str:
|
if not uuid_str:
|
||||||
return Response({'code': 1, 'msg': '\u7f3a\u5c11uuid\u53c2\u6570'}, status=400)
|
return Response({'code': 1, 'msg': '缺少uuid参数'}, status=400)
|
||||||
Message.objects.filter(MessageUUID=uuid_str, ReceiverUUID_id=user.UserUUID).delete()
|
MessageManager.DeleteMessage(user.UserUUID, uuid_str)
|
||||||
return Response({'code': 0, 'msg': '\u5220\u9664\u6210\u529f'})
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"""用户设置 API——基于 UserSettings 模型提供存取"""
|
"""用户设置 API——基于 gvsdsdk UserService 提供存取"""
|
||||||
import json
|
import json
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from gvsds.apps.gerp.models import UserSettings
|
|
||||||
from . import ELTAuthenticatedAPIView
|
from . import ELTAuthenticatedAPIView
|
||||||
|
|
||||||
|
|
||||||
@@ -13,14 +12,10 @@ class SettingsAPIView(ELTAuthenticatedAPIView):
|
|||||||
if not user:
|
if not user:
|
||||||
return Response({'code': 1, 'msg': '未登录'}, status=401)
|
return Response({'code': 1, 'msg': '未登录'}, status=401)
|
||||||
|
|
||||||
|
from gvsdsdk.auth.user_service import UserService
|
||||||
try:
|
try:
|
||||||
# request.user 是 gvsdsdk.User,UserSettings.UserUUID 是 FK 指向 core.User
|
settings_text = UserService.GetUserSettings(user.UserUUID)
|
||||||
# 使用 _id 后缀直接传列值避免类型不匹配
|
settings = json.loads(settings_text) if settings_text else {}
|
||||||
record = UserSettings.objects.filter(UserUUID_id=user.UserUUID).first()
|
|
||||||
if record and record.SettingsData:
|
|
||||||
settings = json.loads(record.SettingsData)
|
|
||||||
else:
|
|
||||||
settings = {}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
settings = {}
|
settings = {}
|
||||||
|
|
||||||
@@ -39,12 +34,10 @@ class SettingsAPIView(ELTAuthenticatedAPIView):
|
|||||||
elif not isinstance(settings_data, str):
|
elif not isinstance(settings_data, str):
|
||||||
return Response({'code': 1, 'msg': '无效的设置数据'}, status=400)
|
return Response({'code': 1, 'msg': '无效的设置数据'}, status=400)
|
||||||
|
|
||||||
|
from gvsdsdk.auth.user_service import UserService
|
||||||
try:
|
try:
|
||||||
record, created = UserSettings.objects.update_or_create(
|
UserService.SaveUserSettings(user.UserUUID, settings_data)
|
||||||
UserUUID_id=user.UserUUID,
|
|
||||||
defaults={'SettingsData': settings_data}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response({'code': 1, 'msg': f'保存失败: {str(e)}'}, status=500)
|
return Response({'code': 1, 'msg': f'保存失败: {str(e)}'}, status=500)
|
||||||
|
|
||||||
return Response({'code': 0, 'msg': '保存成功'})
|
return Response({'code': 0, 'msg': '保存成功'})
|
||||||
@@ -5,7 +5,7 @@ import datetime
|
|||||||
import bcrypt
|
import bcrypt
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from gvsds.apps.core.models import User
|
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 gvsdsdk.model_utils import UUIDObj
|
||||||
from . import ELTAuthenticatedAPIView
|
from . import ELTAuthenticatedAPIView
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
@@ -121,9 +121,9 @@ class UserIdCardView(ELTAuthenticatedAPIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
user = request.user
|
user = request.user
|
||||||
try:
|
from gvsdsdk.auth.user_service import UserService
|
||||||
idcard = UserIDCard.objects.get(UserUUID=user.UserUUID)
|
idcard = UserService.GetUserIDCard(user.UserUUID)
|
||||||
except UserIDCard.DoesNotExist:
|
if idcard is None:
|
||||||
return Response({'code': 0, 'count': 1000, 'data': {}})
|
return Response({'code': 0, 'count': 1000, 'data': {}})
|
||||||
born = idcard.Born
|
born = idcard.Born
|
||||||
return Response({
|
return Response({
|
||||||
@@ -166,9 +166,9 @@ class PersonalPageConfigView(ELTAuthenticatedAPIView):
|
|||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
user = request.user
|
user = request.user
|
||||||
try:
|
from gvsdsdk.auth.user_service import UserService
|
||||||
page = PersonalPage.objects.get(PersonalPageUserUUID=user.UserUUID)
|
page = UserService.GetPersonalPage(user.UserUUID)
|
||||||
except PersonalPage.DoesNotExist:
|
if page is None:
|
||||||
return Response({'code': 1, 'msg': '未注册个人主页'})
|
return Response({'code': 1, 'msg': '未注册个人主页'})
|
||||||
return Response({
|
return Response({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
@@ -214,52 +214,14 @@ class PersonalPageConfigView(ELTAuthenticatedAPIView):
|
|||||||
return Response({'code': 1, 'msg': 'PersonalPageName 不能为空'})
|
return Response({'code': 1, 'msg': 'PersonalPageName 不能为空'})
|
||||||
if not re.fullmatch(r'^[a-z0-9_]+$', personal_page_name):
|
if not re.fullmatch(r'^[a-z0-9_]+$', personal_page_name):
|
||||||
return Response({'code': 1, 'msg': 'PersonalPageName 仅允许小写字母、数字和下划线'})
|
return Response({'code': 1, 'msg': 'PersonalPageName 仅允许小写字母、数字和下划线'})
|
||||||
field_map = {
|
from gvsdsdk.auth.user_service import UserService
|
||||||
'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
|
|
||||||
try:
|
try:
|
||||||
page, created = PersonalPage.objects.update_or_create(
|
page, err = UserService.SavePersonalPage(user.UserUUID, request.data)
|
||||||
PersonalPageUserUUID=user.UserUUID,
|
if page is None:
|
||||||
defaults=defaults,
|
return Response({'code': 1, 'msg': err})
|
||||||
)
|
|
||||||
return Response({'code': 0, 'msg': '修改成功'})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response({'code': 1, 'msg': str(e)}, status=400)
|
return Response({'code': 1, 'msg': str(e)}, status=400)
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
|
||||||
class MemberCardView(ELTAuthenticatedAPIView):
|
class MemberCardView(ELTAuthenticatedAPIView):
|
||||||
|
|||||||
@@ -655,9 +655,9 @@ class WorkOrderViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
|
|||||||
user_uuid = request.user.UserUUID if request.user.IsAuthenticated else None
|
user_uuid = request.user.UserUUID if request.user.IsAuthenticated else None
|
||||||
if not user_uuid:
|
if not user_uuid:
|
||||||
return Response({'code': 401, 'msg': '未登录'}, status=401)
|
return Response({'code': 401, 'msg': '未登录'}, status=401)
|
||||||
qs = WorkOrder.objects.filter(CreatorUUID_id=user_uuid)
|
from gvsdsdk.auth.managers import WorkOrderManager
|
||||||
serializer = self.get_serializer(qs, many=True)
|
data = WorkOrderManager.ListForUser(user_uuid)
|
||||||
return Response({'code': 0, 'data': serializer.data})
|
return Response({'code': 0, 'data': data})
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'])
|
||||||
def create_order(self, request):
|
def create_order(self, request):
|
||||||
@@ -665,31 +665,27 @@ class WorkOrderViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
|
|||||||
ser.IsValid(raise_exception=True)
|
ser.IsValid(raise_exception=True)
|
||||||
data = ser.ValidatedData
|
data = ser.ValidatedData
|
||||||
user_uuid = request.user.UserUUID
|
user_uuid = request.user.UserUUID
|
||||||
assignee = data.get('AssigneeUUID')
|
from gvsdsdk.auth.managers import WorkOrderManager
|
||||||
order = WorkOrder.objects.create(
|
record = WorkOrderManager.Create(
|
||||||
OrderUUID=uuid.uuid4().bytes,
|
CreatorUUID=user_uuid,
|
||||||
Title=data['Title'],
|
Title=data['Title'],
|
||||||
Description=data.get('Description', ''),
|
Description=data.get('Description', ''),
|
||||||
Category=data.get('Category', 'other'),
|
Category=data.get('Category', 'other'),
|
||||||
Priority=data.get('Priority', 0),
|
Priority=data.get('Priority', 0),
|
||||||
CreatorUUID_id=user_uuid,
|
AssigneeUUID=data.get('AssigneeUUID'),
|
||||||
AssigneeUUID_id=assignee,
|
|
||||||
)
|
)
|
||||||
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'])
|
@action(detail=True, methods=['post'])
|
||||||
def update_order(self, request, pk=None):
|
def update_order(self, request, pk=None):
|
||||||
order = self.get_object()
|
|
||||||
ser = WorkOrderUpdateSerializer(data=request.data)
|
ser = WorkOrderUpdateSerializer(data=request.data)
|
||||||
ser.IsValid(raise_exception=True)
|
ser.IsValid(raise_exception=True)
|
||||||
data = ser.ValidatedData
|
data = ser.ValidatedData
|
||||||
field_map = {'Status': 'Status', 'AssigneeUUID': 'AssigneeUUID', 'Resolution': 'Resolution', 'Description': 'Description', 'Priority': 'Priority'}
|
from gvsdsdk.auth.managers import WorkOrderManager
|
||||||
for key in field_map:
|
record = WorkOrderManager.Update(pk, data)
|
||||||
if key in data:
|
if record is None:
|
||||||
setattr(order, field_map[key], data[key])
|
return Response({'code': 404, 'msg': '工单不存在'}, status=404)
|
||||||
order.UpdateTime = datetime.datetime.utcnow()
|
return Response({'code': 0, 'msg': '更新成功', 'data': record})
|
||||||
order.save()
|
|
||||||
return Response({'code': 0, 'msg': '更新成功', 'data': WorkOrderSerializer(order).data})
|
|
||||||
|
|
||||||
|
|
||||||
class MessageViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
|
class MessageViewSet(CompanyScopedMixin, viewsets.ModelViewSet):
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ _LAZY_SYMBOLS = {
|
|||||||
'TenantManager': ('auth', 'TenantManager'),
|
'TenantManager': ('auth', 'TenantManager'),
|
||||||
'CompanyManager': ('auth', 'CompanyManager'),
|
'CompanyManager': ('auth', 'CompanyManager'),
|
||||||
'DepartmentManager': ('auth', 'DepartmentManager'),
|
'DepartmentManager': ('auth', 'DepartmentManager'),
|
||||||
|
'MessageManager': ('auth', 'MessageManager'),
|
||||||
'UserService': ('auth', 'UserService'),
|
'UserService': ('auth', 'UserService'),
|
||||||
|
|
||||||
# ── 分佣引擎 ──────────────────────────────────────────
|
# ── 分佣引擎 ──────────────────────────────────────────
|
||||||
@@ -298,6 +299,8 @@ _LAZY_SYMBOLS = {
|
|||||||
'Position': ('models', 'Position'),
|
'Position': ('models', 'Position'),
|
||||||
'PositionTemplate': ('models', 'PositionTemplate'),
|
'PositionTemplate': ('models', 'PositionTemplate'),
|
||||||
'PositionTemplatePermission': ('models', 'PositionTemplatePermission'),
|
'PositionTemplatePermission': ('models', 'PositionTemplatePermission'),
|
||||||
|
'Menu': ('models', 'Menu'),
|
||||||
|
'WorkOrder': ('models', 'WorkOrder'),
|
||||||
'FixedBinaryField': ('models', 'FixedBinaryField'),
|
'FixedBinaryField': ('models', 'FixedBinaryField'),
|
||||||
'QModel': ('model_base', 'QModel'),
|
'QModel': ('model_base', 'QModel'),
|
||||||
'QModelBase': ('model_base', 'QModelBase'),
|
'QModelBase': ('model_base', 'QModelBase'),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ __all__ = [
|
|||||||
'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'TenantScopedMixin', 'UserScopedMixin', 'PermissionVerifier',
|
'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'TenantScopedMixin', 'UserScopedMixin', 'PermissionVerifier',
|
||||||
'IsAdminScopeDisabled', 'IsTenantScopeDisabled',
|
'IsAdminScopeDisabled', 'IsTenantScopeDisabled',
|
||||||
'IsAdmin', 'IsSuperAdmin', 'IsTenantAdmin',
|
'IsAdmin', 'IsSuperAdmin', 'IsTenantAdmin',
|
||||||
'RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager', 'TenantManager', 'CompanyManager', 'DepartmentManager', 'UserService',
|
'RoleManager', 'ShopManager', 'PositionManager', 'AuthManager', 'AdminUserManager', 'TenantManager', 'CompanyManager', 'DepartmentManager', 'MessageManager', 'UserService',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+517
-1
@@ -1,7 +1,8 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils import timezone as _timezone
|
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.auth.crypto import HashPassword, VerifyPassword
|
||||||
from gvsdsdk.model_utils import _GetUserModel, UUIDObj
|
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)
|
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
|
||||||
return User.objects.filter(UserUUID__in=user_uuids)
|
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
|
@staticmethod
|
||||||
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
|
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
|
||||||
"""为角色分配权限(指定公司)"""
|
"""为角色分配权限(指定公司)"""
|
||||||
@@ -741,6 +757,15 @@ class AuthManager:
|
|||||||
User = _GetUserModel()
|
User = _GetUserModel()
|
||||||
return User.objects.filter(UserUUID=UUIDObj(UserUUID).bytes).first()
|
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
|
@staticmethod
|
||||||
def IsUserNameTaken(UserName):
|
def IsUserNameTaken(UserName):
|
||||||
"""用户名是否已被占用"""
|
"""用户名是否已被占用"""
|
||||||
@@ -1255,3 +1280,494 @@ class DepartmentManager:
|
|||||||
).values_list('DeptUUID', flat=True)
|
).values_list('DeptUUID', flat=True)
|
||||||
return Department.objects.filter(DeptUUID__in=list(dept_uuids)).distinct()
|
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)
|
||||||
|
|
||||||
@@ -537,6 +537,97 @@ class UserRoleDepartment(QModel):
|
|||||||
verbose_name_plural = '用户-角色部门关联'
|
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):
|
class Role(QModel):
|
||||||
@@ -900,3 +991,62 @@ class Position(QModel):
|
|||||||
return self.PositionName
|
return self.PositionName
|
||||||
|
|
||||||
# (UserDept 表已废弃——员工部门归属已迁移到 UserRoleDepartment 关联表,不再用独立表)
|
# (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
|
||||||
@@ -28,6 +28,7 @@ SAAS 级订单编排器——子服务通过此管理器操作订单生命周期
|
|||||||
"""
|
"""
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
@@ -119,6 +120,44 @@ class OrderManager:
|
|||||||
records = list(qs[start:start + page_size])
|
records = list(qs[start:start + page_size])
|
||||||
return {'records': records, 'total': total, 'page': page, 'page_size': 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):
|
def GetOrderItems(self, order_uuid):
|
||||||
"""获取订单行项目列表"""
|
"""获取订单行项目列表"""
|
||||||
order_uuid_bytes = self._to_bytes(order_uuid)
|
order_uuid_bytes = self._to_bytes(order_uuid)
|
||||||
|
|||||||
Reference in new issue
Block a user