Compare commits
No commits in common. "c8a6ced7a09d1d1394099fea844515c22e032705" and "3d2c703cac3c839ea10ed43e4fa023e2abebc27a" have entirely different histories.
c8a6ced7a0
...
3d2c703cac
|
|
@ -20,8 +20,6 @@ apps.zip
|
||||||
server/conf.py
|
server/conf.py
|
||||||
server/conf.ini
|
server/conf.ini
|
||||||
server/conf*.json
|
server/conf*.json
|
||||||
config/conf*.py
|
|
||||||
config/conf*.json
|
|
||||||
sh/*
|
sh/*
|
||||||
temp/*
|
temp/*
|
||||||
nohup.out
|
nohup.out
|
||||||
|
|
@ -10,7 +10,7 @@ from apps.auth1.errors import USERNAME_OR_PASSWORD_WRONG
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from apps.auth1.services import check_phone_code
|
from apps.auth1.services import check_phone_code
|
||||||
|
from apps.utils.sms import send_sms
|
||||||
from apps.utils.tools import rannum
|
from apps.utils.tools import rannum
|
||||||
from apps.utils.wxmp import wxmpClient
|
from apps.utils.wxmp import wxmpClient
|
||||||
from apps.utils.wx import wxClient
|
from apps.utils.wx import wxClient
|
||||||
|
|
@ -182,7 +182,6 @@ class SendCode(CreateAPIView):
|
||||||
|
|
||||||
短信验证码发送
|
短信验证码发送
|
||||||
"""
|
"""
|
||||||
from apps.utils.sms import send_sms
|
|
||||||
phone = request.data['phone']
|
phone = request.data['phone']
|
||||||
code = rannum(6)
|
code = rannum(6)
|
||||||
is_ok, _ = send_sms(phone, 505, {'code': code})
|
is_ok, _ = send_sms(phone, 505, {'code': code})
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
from django_filters import rest_framework as filters
|
from django_filters import rest_framework as filters
|
||||||
from .models import Dept, User
|
from .models import Dept, User
|
||||||
from apps.utils.queryset import get_child_queryset2
|
|
||||||
from rest_framework.exceptions import ParseError
|
|
||||||
|
|
||||||
|
|
||||||
class UserFilterSet(filters.FilterSet):
|
class UserFilterSet(filters.FilterSet):
|
||||||
ubelong_dept__name = filters.CharFilter(label='归属于该部门及以下(按名称)', method='filter_ubelong_dept__name')
|
|
||||||
ubelong_dept = filters.CharFilter(label='归属于该部门及以下', method='filter_ubelong_dept')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
|
@ -24,20 +20,6 @@ class UserFilterSet(filters.FilterSet):
|
||||||
'posts__code': ["exact", "contains"],
|
'posts__code': ["exact", "contains"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def filter_ubelong_dept__name(self, queryset, name, value):
|
|
||||||
try:
|
|
||||||
depts = get_child_queryset2(Dept.objects.get(name=value))
|
|
||||||
except Exception as e:
|
|
||||||
raise ParseError(f"部门名称错误: {value} {str(e)}")
|
|
||||||
return queryset.filter(belong_dept__in=depts)
|
|
||||||
|
|
||||||
def filter_ubelong_dept(self, queryset, name, value):
|
|
||||||
try:
|
|
||||||
depts = get_child_queryset2(Dept.objects.get(id=value))
|
|
||||||
except Exception as e:
|
|
||||||
raise ParseError(f"部门ID错误: {value} {str(e)}")
|
|
||||||
return queryset.filter(belong_dept__in=depts)
|
|
||||||
|
|
||||||
|
|
||||||
class DeptFilterSet(filters.FilterSet):
|
class DeptFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
|
|
@ -45,6 +27,5 @@ class DeptFilterSet(filters.FilterSet):
|
||||||
model = Dept
|
model = Dept
|
||||||
fields = {
|
fields = {
|
||||||
'type': ['exact', 'in'],
|
'type': ['exact', 'in'],
|
||||||
'name': ['exact', 'in', 'contains'],
|
'name': ['exact', 'in', 'contains']
|
||||||
"parent": ['exact', 'isnull'],
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -272,10 +272,12 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||||
model = Dept
|
model = Dept
|
||||||
exclude = EXCLUDE_FIELDS + ['third_info']
|
exclude = EXCLUDE_FIELDS + ['third_info']
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
ins = super().create(validated_data)
|
ins = super().create(validated_data)
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
ins = super().update(instance, validated_data)
|
ins = super().update(instance, validated_data)
|
||||||
return ins
|
return ins
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
||||||
from django_celery_results.models import TaskResult
|
from django_celery_results.models import TaskResult
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
|
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
|
||||||
from rest_framework.mixins import RetrieveModelMixin
|
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
|
||||||
|
ListModelMixin, RetrieveModelMixin)
|
||||||
from rest_framework.parsers import (JSONParser,
|
from rest_framework.parsers import (JSONParser,
|
||||||
MultiPartParser)
|
MultiPartParser)
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
@ -18,7 +19,7 @@ from rest_framework.views import APIView
|
||||||
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
|
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
|
||||||
from apps.system.filters import DeptFilterSet, UserFilterSet
|
from apps.system.filters import DeptFilterSet, UserFilterSet
|
||||||
# from django_q.models import Task as QTask, Schedule as QSchedule
|
# from django_q.models import Task as QTask, Schedule as QSchedule
|
||||||
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin)
|
from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin)
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from apps.utils.permission import ALL_PERMS
|
from apps.utils.permission import ALL_PERMS
|
||||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||||
|
|
@ -227,7 +228,7 @@ class PTaskViewSet(CustomModelViewSet):
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
list:任务执行结果列表
|
list:任务执行结果列表
|
||||||
|
|
||||||
|
|
@ -371,7 +372,7 @@ class RoleViewSet(CustomModelViewSet):
|
||||||
ordering = ['create_time']
|
ordering = ['create_time']
|
||||||
|
|
||||||
|
|
||||||
class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||||
"""岗位/角色关系
|
"""岗位/角色关系
|
||||||
|
|
||||||
岗位/角色关系
|
岗位/角色关系
|
||||||
|
|
@ -383,7 +384,7 @@ class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
|
||||||
filterset_fields = ['post', 'role']
|
filterset_fields = ['post', 'role']
|
||||||
|
|
||||||
|
|
||||||
class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||||
"""用户/岗位关系
|
"""用户/岗位关系
|
||||||
|
|
||||||
用户/岗位关系
|
用户/岗位关系
|
||||||
|
|
@ -396,29 +397,31 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
|
||||||
ordering = ['sort', 'create_time']
|
ordering = ['sort', 'create_time']
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
with transaction.atomic():
|
||||||
user = instance.user
|
instance = serializer.save()
|
||||||
up = UserPost.objects.filter(user=user).order_by(
|
user = instance.user
|
||||||
'sort', 'create_time').first()
|
up = UserPost.objects.filter(user=user).order_by(
|
||||||
if up:
|
'sort', 'create_time').first()
|
||||||
user.belong_dept = up.dept
|
if up:
|
||||||
user.post = up.post
|
user.belong_dept = up.dept
|
||||||
user.update_by = self.request.user
|
user.post = up.post
|
||||||
user.save()
|
user.update_by = self.request.user
|
||||||
|
user.save()
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
user = instance.user
|
with transaction.atomic():
|
||||||
instance.delete()
|
user = instance.user
|
||||||
up = UserPost.objects.filter(user=user).order_by(
|
instance.delete()
|
||||||
'sort', 'create_time').first()
|
up = UserPost.objects.filter(user=user).order_by(
|
||||||
if up:
|
'sort', 'create_time').first()
|
||||||
user.belong_dept = up.dept
|
if up:
|
||||||
user.post = up.post
|
user.belong_dept = up.dept
|
||||||
else:
|
user.post = up.post
|
||||||
user.belong_dept = None
|
else:
|
||||||
user.post = None
|
user.belong_dept = None
|
||||||
user.update_by = self.request.user
|
user.post = None
|
||||||
user.save()
|
user.update_by = self.request.user
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(CustomModelViewSet):
|
class UserViewSet(CustomModelViewSet):
|
||||||
|
|
@ -557,7 +560,7 @@ class UserViewSet(CustomModelViewSet):
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet):
|
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||||
"""文件上传
|
"""文件上传
|
||||||
|
|
||||||
list:
|
list:
|
||||||
|
|
@ -598,7 +601,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
|
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': '*', 'post': 'apk.upload'}
|
perms_map = {'get': '*', 'post': 'apk.upload'}
|
||||||
serializer_class = ApkSerializer
|
serializer_class = ApkSerializer
|
||||||
|
|
||||||
|
|
@ -639,7 +642,7 @@ class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, Gen
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
||||||
perms_map = {'get': '*', 'post': '*',
|
perms_map = {'get': '*', 'post': '*',
|
||||||
'delete': 'myschedule.delete'}
|
'delete': 'myschedule.delete'}
|
||||||
serializer_class = MyScheduleSerializer
|
serializer_class = MyScheduleSerializer
|
||||||
|
|
@ -665,6 +668,7 @@ class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyM
|
||||||
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
|
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
vdata = serializer.validated_data
|
vdata = serializer.validated_data
|
||||||
vdata['create_by'] = self.request.user # 不可少
|
vdata['create_by'] = self.request.user # 不可少
|
||||||
|
|
@ -714,8 +718,6 @@ class SysBaseConfigView(APIView):
|
||||||
config = get_sysconfig()
|
config = get_sysconfig()
|
||||||
base_dict = {key: config[key]
|
base_dict = {key: config[key]
|
||||||
for key in self.read_keys if key in config}
|
for key in self.read_keys if key in config}
|
||||||
base_dict.get("base", {})["sys_version"] = settings.SYS_VERSION
|
|
||||||
base_dict.get("base", {})["sys_name"] = settings.SYS_NAME
|
|
||||||
return Response(base_dict)
|
return Response(base_dict)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.db.models import DecimalField
|
|
||||||
from django.core.validators import MinValueValidator
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
|
|
||||||
class MyFilePathField(serializers.CharField):
|
class MyFilePathField(serializers.CharField):
|
||||||
|
|
@ -12,9 +8,3 @@ class MyFilePathField(serializers.CharField):
|
||||||
if 'http' in value:
|
if 'http' in value:
|
||||||
return str(value)
|
return str(value)
|
||||||
return settings.BASE_URL + str(value)
|
return settings.BASE_URL + str(value)
|
||||||
|
|
||||||
class PositiveDecimalField(DecimalField):
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def validators(self):
|
|
||||||
return [MinValueValidator(Decimal('0.0'))] + super().validators
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
from contextlib import contextmanager
|
|
||||||
from rest_framework.exceptions import ParseError
|
|
||||||
from functools import wraps
|
|
||||||
from django.db import transaction
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def lock_model_record(model_class, pk):
|
|
||||||
"""
|
|
||||||
Locks a model instance and returns it.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
instance = model_class.objects.select_for_update().get(pk=pk)
|
|
||||||
yield instance
|
|
||||||
except model_class.DoesNotExist:
|
|
||||||
raise ParseError("该记录不存在或已被删除")
|
|
||||||
|
|
||||||
def lock_model_record_d_func(model_class, pk_attr='id'):
|
|
||||||
"""
|
|
||||||
通用模型锁装饰器(内置事务),用于装饰函数
|
|
||||||
"""
|
|
||||||
def decorator(func):
|
|
||||||
@wraps(func)
|
|
||||||
@transaction.atomic
|
|
||||||
def wrapper(old_instance, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
# 获取新鲜记录
|
|
||||||
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
|
|
||||||
# 调用原函数,但传入新鲜记录
|
|
||||||
return func(fresh_record, *args, **kwargs)
|
|
||||||
except model_class.DoesNotExist:
|
|
||||||
raise ParseError('记录不存在或已被删除')
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def lock_model_record_d_method(model_class, pk_attr='id'):
|
|
||||||
"""
|
|
||||||
通用模型锁装饰器(内置事务), 用于装饰类方法
|
|
||||||
"""
|
|
||||||
def decorator(func):
|
|
||||||
@wraps(func)
|
|
||||||
@transaction.atomic
|
|
||||||
def wrapper(self, old_instance, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
# 获取新鲜记录
|
|
||||||
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
|
|
||||||
# 调用原函数,但传入新鲜记录
|
|
||||||
return func(self, fresh_record, *args, **kwargs)
|
|
||||||
except model_class.DoesNotExist:
|
|
||||||
raise ParseError('记录不存在或已被删除')
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
|
@ -9,16 +9,13 @@ from django.utils.timezone import now
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
import logging
|
import logging
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
from django.db import transaction
|
||||||
from rest_framework.exceptions import ParseError, ValidationError
|
from rest_framework.exceptions import ParseError, ValidationError
|
||||||
from apps.utils.errors import PKS_ERROR
|
from apps.utils.errors import PKS_ERROR
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from apps.utils.serializers import PkSerializer
|
from apps.utils.serializers import PkSerializer
|
||||||
from rest_framework.decorators import action
|
|
||||||
from apps.utils.serializers import ComplexSerializer
|
|
||||||
from django.db.models import F
|
|
||||||
from django.db import transaction
|
|
||||||
|
|
||||||
# 实例化myLogger
|
# 实例化myLogger
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
@ -82,7 +79,6 @@ class BulkCreateModelMixin(CreateModelMixin):
|
||||||
def after_bulk_create(self, objs):
|
def after_bulk_create(self, objs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""创建(支持批量)
|
"""创建(支持批量)
|
||||||
|
|
||||||
|
|
@ -91,16 +87,11 @@ class BulkCreateModelMixin(CreateModelMixin):
|
||||||
rdata = request.data
|
rdata = request.data
|
||||||
many = False
|
many = False
|
||||||
if isinstance(rdata, list):
|
if isinstance(rdata, list):
|
||||||
for item in rdata:
|
|
||||||
if "id" in item and item["id"]:
|
|
||||||
raise ParseError('创建数据中不能包含id字段')
|
|
||||||
many = True
|
many = True
|
||||||
else:
|
with transaction.atomic():
|
||||||
if "id" in rdata and rdata["id"]:
|
sr = self.get_serializer(data=rdata, many=many)
|
||||||
raise ParseError('创建数据中不能包含id字段')
|
sr.is_valid(raise_exception=True)
|
||||||
sr = self.get_serializer(data=rdata, many=many)
|
self.perform_create(sr)
|
||||||
sr.is_valid(raise_exception=True)
|
|
||||||
self.perform_create(sr)
|
|
||||||
if many:
|
if many:
|
||||||
self.after_bulk_create(sr.data)
|
self.after_bulk_create(sr.data)
|
||||||
return Response(sr.data, status=201)
|
return Response(sr.data, status=201)
|
||||||
|
|
@ -111,7 +102,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
def after_bulk_update(self, objs):
|
def after_bulk_update(self, objs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
"""部分更新(支持批量)
|
"""部分更新(支持批量)
|
||||||
|
|
||||||
|
|
@ -120,7 +110,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
kwargs['partial'] = True
|
kwargs['partial'] = True
|
||||||
return self.update(request, *args, **kwargs)
|
return self.update(request, *args, **kwargs)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
"""更新(支持批量)
|
"""更新(支持批量)
|
||||||
|
|
||||||
|
|
@ -132,15 +121,16 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
objs = []
|
objs = []
|
||||||
if isinstance(request.data, list):
|
if isinstance(request.data, list):
|
||||||
for ind, item in enumerate(request.data):
|
with transaction.atomic():
|
||||||
obj = get_object_or_404(queryset, id=item['id'])
|
for ind, item in enumerate(request.data):
|
||||||
sr = self.get_serializer(obj, data=item, partial=partial)
|
obj = get_object_or_404(queryset, id=item['id'])
|
||||||
if not sr.is_valid():
|
sr = self.get_serializer(obj, data=item, partial=partial)
|
||||||
err_dict = { f'第{ind+1}': sr.errors}
|
if not sr.is_valid():
|
||||||
raise ValidationError(err_dict)
|
err_dict = { f'第{ind+1}': sr.errors}
|
||||||
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
|
raise ValidationError(err_dict)
|
||||||
objs.append(sr.data)
|
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
|
||||||
self.after_bulk_update(objs)
|
objs.append(sr.data)
|
||||||
|
self.after_bulk_update(objs)
|
||||||
else:
|
else:
|
||||||
raise ParseError('提交数据非列表')
|
raise ParseError('提交数据非列表')
|
||||||
return Response(objs)
|
return Response(objs)
|
||||||
|
|
@ -155,7 +145,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
|
||||||
class BulkDestroyModelMixin(DestroyModelMixin):
|
class BulkDestroyModelMixin(DestroyModelMixin):
|
||||||
|
|
||||||
@swagger_auto_schema(request_body=PkSerializer)
|
@swagger_auto_schema(request_body=PkSerializer)
|
||||||
@transaction.atomic
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
"""删除(支持批量)
|
"""删除(支持批量)
|
||||||
|
|
||||||
|
|
@ -199,8 +188,6 @@ class CustomRetrieveModelMixin(RetrieveModelMixin):
|
||||||
|
|
||||||
给dict返回数据添加额外信息
|
给dict返回数据添加额外信息
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'add_info_for_list'):
|
|
||||||
return self.add_info_for_list([data])[0]
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
class CustomListModelMixin(ListModelMixin):
|
class CustomListModelMixin(ListModelMixin):
|
||||||
|
|
@ -232,79 +219,6 @@ class CustomListModelMixin(ListModelMixin):
|
||||||
"""
|
"""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ComplexQueryMixin:
|
|
||||||
"""复杂查询
|
|
||||||
"""
|
|
||||||
|
|
||||||
@swagger_auto_schema(request_body=ComplexSerializer, responses={200: {}})
|
|
||||||
@action(methods=['post'], detail=False, perms_map={'post': '*'})
|
|
||||||
def cquery(self, request):
|
|
||||||
"""复杂查询
|
|
||||||
|
|
||||||
复杂查询
|
|
||||||
"""
|
|
||||||
sr = ComplexSerializer(data=request.data)
|
|
||||||
sr.is_valid(raise_exception=True)
|
|
||||||
vdata = sr.validated_data
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
querys = vdata.get('querys', [])
|
|
||||||
annotate_field_list = vdata.get('annotate_field_list', [])
|
|
||||||
|
|
||||||
if not querys:
|
|
||||||
new_qs = queryset
|
|
||||||
else:
|
|
||||||
new_qs = queryset.none()
|
|
||||||
try:
|
|
||||||
for m in querys:
|
|
||||||
one_qs = queryset
|
|
||||||
for n in m:
|
|
||||||
st = {}
|
|
||||||
if n['compare'] == '!': # 如果是排除比较式
|
|
||||||
st[n['field']] = n['value']
|
|
||||||
one_qs = one_qs.exclude(**st)
|
|
||||||
elif n['compare'] == '':
|
|
||||||
st[n['field']] = n['value']
|
|
||||||
one_qs = one_qs.filter(**st)
|
|
||||||
else:
|
|
||||||
st[n['field'] + '__' + n['compare']] = n['value']
|
|
||||||
one_qs = one_qs.filter(**st)
|
|
||||||
new_qs = new_qs | one_qs
|
|
||||||
except Exception as e:
|
|
||||||
raise ParseError(str(e))
|
|
||||||
|
|
||||||
if annotate_field_list:
|
|
||||||
annotate_dict = getattr(self, "annotate_dict", {})
|
|
||||||
if annotate_dict:
|
|
||||||
filtered_annotate_dict = { key: annotate_dict[key] for key in annotate_field_list if key in annotate_dict}
|
|
||||||
new_qs = new_qs.annotate(**filtered_annotate_dict)
|
|
||||||
|
|
||||||
ordering = vdata.get('ordering', None)
|
|
||||||
if not ordering:
|
|
||||||
ordering = getattr(self, 'ordering', None)
|
|
||||||
if isinstance(ordering, str):
|
|
||||||
ordering = ordering.replace('\n', '').replace(' ', '')
|
|
||||||
ordering = ordering.split(',')
|
|
||||||
order_fields = []
|
|
||||||
if ordering:
|
|
||||||
for item in ordering:
|
|
||||||
if item.startswith('-'):
|
|
||||||
# JSONField 排序只能用字符串,不要 F
|
|
||||||
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
|
|
||||||
else:
|
|
||||||
order_fields.append(F(item).asc(nulls_last=True) if '__' not in item else item)
|
|
||||||
new_qs = new_qs.order_by(*order_fields)
|
|
||||||
|
|
||||||
page = self.paginate_queryset(new_qs)
|
|
||||||
if page is not None:
|
|
||||||
serializer = self.get_serializer(page, many=True)
|
|
||||||
return self.get_paginated_response(serializer.data)
|
|
||||||
serializer = self.get_serializer(new_qs, many=True)
|
|
||||||
rdata = serializer.data
|
|
||||||
if hasattr(self, 'add_info_for_list'):
|
|
||||||
rdata = self.add_info_for_list(rdata)
|
|
||||||
return Response(rdata)
|
|
||||||
|
|
||||||
class MyLoggingMixin(object):
|
class MyLoggingMixin(object):
|
||||||
"""Mixin to log requests"""
|
"""Mixin to log requests"""
|
||||||
|
|
||||||
|
|
@ -350,7 +264,6 @@ class MyLoggingMixin(object):
|
||||||
response = super().finalize_response(
|
response = super().finalize_response(
|
||||||
request, response, *args, **kwargs
|
request, response, *args, **kwargs
|
||||||
)
|
)
|
||||||
self.log["response_ms"] = self._get_response_ms()
|
|
||||||
# Ensure backward compatibility for those using _should_log hook
|
# Ensure backward compatibility for those using _should_log hook
|
||||||
should_log = (
|
should_log = (
|
||||||
self._should_log if hasattr(self, "_should_log") else self.should_log
|
self._should_log if hasattr(self, "_should_log") else self.should_log
|
||||||
|
|
@ -379,7 +292,7 @@ class MyLoggingMixin(object):
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"query_params": self._clean_data(request.query_params.dict()),
|
"query_params": self._clean_data(request.query_params.dict()),
|
||||||
"user": self._get_user(request),
|
"user": self._get_user(request),
|
||||||
# "response_ms": self._get_response_ms(),
|
"response_ms": self._get_response_ms(),
|
||||||
"response": self._clean_data(rendered_content),
|
"response": self._clean_data(rendered_content),
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
"agent": self._get_agent(request),
|
"agent": self._get_agent(request),
|
||||||
|
|
@ -473,8 +386,7 @@ class MyLoggingMixin(object):
|
||||||
By default, check if the request method is in logging_methods.
|
By default, check if the request method is in logging_methods.
|
||||||
"""
|
"""
|
||||||
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
||||||
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])\
|
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
|
||||||
or (self.log.get("response_ms", 0) > 2000)
|
|
||||||
|
|
||||||
def _clean_data(self, data):
|
def _clean_data(self, data):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from django.db import IntegrityError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import transaction, connection
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
# 自定义软删除查询基类
|
# 自定义软删除查询基类
|
||||||
|
|
@ -116,66 +115,13 @@ class BaseModel(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def safe_get_or_create(cls, defaults=None, **kwargs):
|
def safe_get_or_create(cls, defaults=None, **kwargs):
|
||||||
"""
|
|
||||||
多进程/多服务器安全的 get_or_create
|
|
||||||
- 数据库唯一约束不够时,用 Redis 锁防止重复创建
|
|
||||||
- 在事务中使用 select_for_update
|
|
||||||
"""
|
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
create_kwargs = {**kwargs, **defaults}
|
lock_data = {**kwargs, **defaults}
|
||||||
|
lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest()
|
||||||
|
lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
|
||||||
|
with cache.lock(lock_key, timeout=10):
|
||||||
|
return cls.objects.get_or_create(**kwargs, defaults=defaults)
|
||||||
|
|
||||||
for attempt in range(3):
|
|
||||||
try:
|
|
||||||
if connection.in_atomic_block:
|
|
||||||
# 在事务中,先锁定再获取
|
|
||||||
try:
|
|
||||||
obj = cls.objects.select_for_update().get(**kwargs)
|
|
||||||
return obj, False
|
|
||||||
except cls.DoesNotExist:
|
|
||||||
obj = cls(**create_kwargs)
|
|
||||||
obj.save()
|
|
||||||
return obj, True
|
|
||||||
else:
|
|
||||||
# 非事务,使用分布式锁
|
|
||||||
sorted_kwargs = dict(sorted(create_kwargs.items()))
|
|
||||||
lock_hash = hashlib.md5(str(sorted_kwargs).encode()).hexdigest()
|
|
||||||
lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
|
|
||||||
|
|
||||||
with cache.lock(lock_key, timeout=10):
|
|
||||||
return cls.objects.get_or_create(**kwargs, defaults=defaults)
|
|
||||||
|
|
||||||
except IntegrityError:
|
|
||||||
# 唯一约束冲突,重试
|
|
||||||
if attempt == 2:
|
|
||||||
raise
|
|
||||||
time.sleep(0.1 * (attempt + 1))
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def locked_get_or_create(cls, defaults: dict, **kwargs):
|
|
||||||
"""
|
|
||||||
仅用于事务内
|
|
||||||
并发安全的 get_or_create
|
|
||||||
"""
|
|
||||||
if not connection.in_atomic_block:
|
|
||||||
raise RuntimeError("locked_get_or_create 必须在事务中调用")
|
|
||||||
|
|
||||||
defaults = defaults or {}
|
|
||||||
|
|
||||||
qs = cls.objects.select_for_update().filter(**kwargs)
|
|
||||||
|
|
||||||
cnt = qs.count()
|
|
||||||
if cnt > 1:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"{cls.__name__} 数据异常:定位条件 {kwargs} 命中 {cnt} 条"
|
|
||||||
)
|
|
||||||
|
|
||||||
if cnt == 1:
|
|
||||||
return qs.get(), False
|
|
||||||
|
|
||||||
params = {**kwargs, **defaults}
|
|
||||||
obj = cls.objects.create(**params)
|
|
||||||
return obj, True
|
|
||||||
|
|
||||||
def handle_parent(self):
|
def handle_parent(self):
|
||||||
pass
|
pass
|
||||||
|
|
@ -186,32 +132,32 @@ class BaseModel(models.Model):
|
||||||
if not self.id:
|
if not self.id:
|
||||||
is_create = True
|
is_create = True
|
||||||
self.id = idWorker.get_id()
|
self.id = idWorker.get_id()
|
||||||
|
with transaction.atomic():
|
||||||
old_parent = None
|
old_parent = None
|
||||||
need_handle_parent = False
|
need_handle_parent = False
|
||||||
if hasattr(self, "parent"):
|
if hasattr(self, "parent"):
|
||||||
if is_create:
|
if is_create:
|
||||||
need_handle_parent = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
old_parent = self.__class__.objects.get(id=self.id).parent
|
|
||||||
except Exception:
|
|
||||||
self.parent = None
|
|
||||||
need_handle_parent = True
|
need_handle_parent = True
|
||||||
if self.parent != old_parent:
|
else:
|
||||||
need_handle_parent = True
|
try:
|
||||||
try:
|
old_parent = self.__class__.objects.get(id=self.id).parent
|
||||||
ins = super().save(*args, **kwargs)
|
except Exception:
|
||||||
except IntegrityError as e:
|
self.parent = None
|
||||||
if is_create:
|
need_handle_parent = True
|
||||||
time.sleep(0.01)
|
if self.parent != old_parent:
|
||||||
self.id = idWorker.get_id()
|
need_handle_parent = True
|
||||||
|
try:
|
||||||
ins = super().save(*args, **kwargs)
|
ins = super().save(*args, **kwargs)
|
||||||
raise e
|
except IntegrityError as e:
|
||||||
# 处理父级
|
if is_create:
|
||||||
if need_handle_parent:
|
time.sleep(0.01)
|
||||||
self.handle_parent()
|
self.id = idWorker.get_id()
|
||||||
return ins
|
ins = super().save(*args, **kwargs)
|
||||||
|
raise e
|
||||||
|
# 处理父级
|
||||||
|
if need_handle_parent:
|
||||||
|
self.handle_parent()
|
||||||
|
return ins
|
||||||
|
|
||||||
|
|
||||||
class SoftModel(BaseModel):
|
class SoftModel(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ def get_user_route(user: User) -> List[str]:
|
||||||
else:
|
else:
|
||||||
user_routes_qs = perm_qs.filter(role_perms__in=PostRole.objects.filter(
|
user_routes_qs = perm_qs.filter(role_perms__in=PostRole.objects.filter(
|
||||||
post__in=UserPost.objects.filter(user=user).values_list("post", flat=True)).values_list("role", flat=True)).distinct()
|
post__in=UserPost.objects.filter(user=user).values_list("post", flat=True)).values_list("role", flat=True)).distinct()
|
||||||
user_routes_qs = user_routes_qs.order_by('sort', 'create_time')
|
user_routes_qs = user_routes_qs.order_by('sort')
|
||||||
user_routes_list = list(user_routes_qs.values("id", "name", "type", "route_name", "icon", "path", "component", "is_hidden", "is_fullpage", "parent"))
|
user_routes_list = list(user_routes_qs.values("id", "name", "type", "route_name", "icon", "path", "component", "is_hidden", "is_fullpage", "parent"))
|
||||||
for item in user_routes_list:
|
for item in user_routes_list:
|
||||||
item["meta"] = {}
|
item["meta"] = {}
|
||||||
|
|
@ -54,8 +54,6 @@ def get_user_route(user: User) -> List[str]:
|
||||||
item.pop("is_fullpage")
|
item.pop("is_fullpage")
|
||||||
item["name"] = item["route_name"]
|
item["name"] = item["route_name"]
|
||||||
item.pop("route_name")
|
item.pop("route_name")
|
||||||
if item["path"].startswith("http"):
|
|
||||||
item["meta"]["type"] = "iframe"
|
|
||||||
return build_tree_from_list(user_routes_list)
|
return build_tree_from_list(user_routes_list)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,12 @@ class CustomModelSerializer(DynamicFieldsMixin, TreeSerializerMixin, serializers
|
||||||
class QuerySerializer(serializers.Serializer):
|
class QuerySerializer(serializers.Serializer):
|
||||||
field = serializers.CharField(label='字段名')
|
field = serializers.CharField(label='字段名')
|
||||||
compare = serializers.ChoiceField(
|
compare = serializers.ChoiceField(
|
||||||
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains", "isnull"])
|
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"])
|
||||||
value = serializers.JSONField(label='值', allow_null=True)
|
value = serializers.CharField(label='值')
|
||||||
|
|
||||||
|
|
||||||
class ComplexSerializer(serializers.Serializer):
|
class ComplexSerializer(serializers.Serializer):
|
||||||
page = serializers.IntegerField(min_value=0, required=False)
|
page = serializers.IntegerField(min_value=0, required=False)
|
||||||
page_size = serializers.IntegerField(min_value=1, required=False)
|
page_size = serializers.IntegerField(min_value=1, required=False)
|
||||||
ordering = serializers.CharField(required=False)
|
|
||||||
querys = serializers.ListField(child=QuerySerializer(
|
querys = serializers.ListField(child=QuerySerializer(
|
||||||
many=True), label="查询列表", required=False)
|
many=True), label="查询列表", required=False)
|
||||||
annotate_field_list = serializers.ListField(child=serializers.CharField(), label="RawSQL字段列表", required=False)
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
|
from aliyunsdkcore.client import AcsClient
|
||||||
|
from aliyunsdkcore.request import CommonRequest
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from server.settings import get_sysconfig
|
from server.settings import get_sysconfig
|
||||||
|
|
@ -7,11 +8,8 @@ from apps.utils.decorators import auto_log
|
||||||
# 实例化myLogger
|
# 实例化myLogger
|
||||||
myLogger = logging.getLogger('log')
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
|
@auto_log(name='阿里云短信', raise_exception=True, send_mail=True)
|
||||||
@auto_log(name='阿里云短信', raise_exception=True, send_mail=False)
|
|
||||||
def send_sms(phone: str, template_code: int, template_param: dict):
|
def send_sms(phone: str, template_code: int, template_param: dict):
|
||||||
from aliyunsdkcore.client import AcsClient
|
|
||||||
from aliyunsdkcore.request import CommonRequest
|
|
||||||
config = get_sysconfig()
|
config = get_sysconfig()
|
||||||
if config.get("sms", {}).get('enabled', True) is False:
|
if config.get("sms", {}).get('enabled', True) is False:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.utils import timezone
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def execute_raw_sql(sql: str, params=None, timeout=30):
|
def execute_raw_sql(sql: str, params=None):
|
||||||
"""执行原始sql并返回rows, columns数据
|
"""执行原始sql并返回rows, columns数据
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -10,8 +8,7 @@ def execute_raw_sql(sql: str, params=None, timeout=30):
|
||||||
params (_type_, optional): 参数列表. Defaults to None.
|
params (_type_, optional): 参数列表. Defaults to None.
|
||||||
"""
|
"""
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
if timeout:
|
cursor.execute("SET statement_timeout TO %s;", [30000])
|
||||||
cursor.execute(f"SET statement_timeout TO '{int(timeout*1000)}ms';")
|
|
||||||
if params:
|
if params:
|
||||||
cursor.execute(sql, params=params)
|
cursor.execute(sql, params=params)
|
||||||
else:
|
else:
|
||||||
|
|
@ -26,7 +23,7 @@ def format_sqldata(columns, rows):
|
||||||
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
|
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def query_all_dict(sql, params=None, with_time_format=False):
|
def query_all_dict(sql, params=None):
|
||||||
'''
|
'''
|
||||||
查询所有结果返回字典类型数据
|
查询所有结果返回字典类型数据
|
||||||
:param sql:
|
:param sql:
|
||||||
|
|
@ -39,19 +36,9 @@ def query_all_dict(sql, params=None, with_time_format=False):
|
||||||
else:
|
else:
|
||||||
cursor.execute(sql)
|
cursor.execute(sql)
|
||||||
columns = [desc[0] for desc in cursor.description]
|
columns = [desc[0] for desc in cursor.description]
|
||||||
if with_time_format:
|
|
||||||
results = []
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
row_dict = {}
|
|
||||||
for col, val in zip(columns, row):
|
|
||||||
if isinstance(val, datetime):
|
|
||||||
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
row_dict[col] = val
|
|
||||||
results.append(row_dict)
|
|
||||||
return results
|
|
||||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
|
|
||||||
def query_one_dict(sql, params=None, with_time_format=False):
|
def query_one_dict(sql, params=None):
|
||||||
"""
|
"""
|
||||||
查询一个结果返回字典类型数据
|
查询一个结果返回字典类型数据
|
||||||
:param sql:
|
:param sql:
|
||||||
|
|
@ -59,17 +46,13 @@ def query_one_dict(sql, params=None, with_time_format=False):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute(sql, params or ()) # 更简洁的参数处理
|
if params:
|
||||||
|
cursor.execute(sql, params=params)
|
||||||
|
else:
|
||||||
|
cursor.execute(sql)
|
||||||
columns = [desc[0] for desc in cursor.description]
|
columns = [desc[0] for desc in cursor.description]
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if with_time_format:
|
return dict(zip(columns, row))
|
||||||
row_dict = {}
|
|
||||||
for col, val in zip(columns, row):
|
|
||||||
if isinstance(val, datetime):
|
|
||||||
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
row_dict[col] = val
|
|
||||||
return row_dict
|
|
||||||
return dict(zip(columns, row)) if row else None # 安全处理None情况
|
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,6 @@ from io import BytesIO
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
import ast
|
import ast
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
class MyJSONEncoder(DjangoJSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, Decimal):
|
|
||||||
return float(obj)
|
|
||||||
return super().default(obj)
|
|
||||||
|
|
||||||
class CodeAnalyzer(ast.NodeVisitor):
|
class CodeAnalyzer(ast.NodeVisitor):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import StreamingHttpResponse, Http404
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework.mixins import RetrieveModelMixin
|
from rest_framework.mixins import RetrieveModelMixin
|
||||||
|
|
@ -10,17 +9,14 @@ from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from apps.system.models import DataFilter, Dept
|
from apps.system.models import DataFilter, Dept
|
||||||
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkUpdateModelMixin,
|
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkUpdateModelMixin,
|
||||||
BulkDestroyModelMixin, CustomListModelMixin,
|
BulkDestroyModelMixin, CustomListModelMixin, CustomRetrieveModelMixin)
|
||||||
CustomRetrieveModelMixin, ComplexQueryMixin)
|
|
||||||
from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map
|
from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map
|
||||||
from apps.utils.queryset import get_child_queryset2, get_child_queryset_u
|
from apps.utils.queryset import get_child_queryset2, get_child_queryset_u
|
||||||
from apps.utils.serializers import ComplexSerializer
|
from apps.utils.serializers import ComplexSerializer
|
||||||
from rest_framework.throttling import UserRateThrottle
|
from rest_framework.throttling import UserRateThrottle
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
import json
|
import json
|
||||||
from django.db import connection
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.db.utils import NotSupportedError
|
|
||||||
|
|
||||||
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
"""
|
"""
|
||||||
|
|
@ -88,36 +84,6 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
elif hash_v_e:
|
elif hash_v_e:
|
||||||
return Response(hash_v_e)
|
return Response(hash_v_e)
|
||||||
|
|
||||||
def get_object(self, force_lock=False):
|
|
||||||
"""
|
|
||||||
智能加锁的get_object
|
|
||||||
- 只读请求:普通查询
|
|
||||||
- 非只读请求且在事务中:加锁查询
|
|
||||||
- 非只读请求但不在事务中:普通查询(带警告)
|
|
||||||
"""
|
|
||||||
# 只读方法列表
|
|
||||||
read_only_methods = ['GET', 'HEAD', 'OPTIONS']
|
|
||||||
|
|
||||||
if self.request.method not in read_only_methods and connection.in_atomic_block:
|
|
||||||
if force_lock:
|
|
||||||
raise ParseError("当前操作需要在事务中进行,请使用事务装饰器")
|
|
||||||
# 非只读请求且在事务中:加锁查询
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
||||||
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
|
||||||
|
|
||||||
try:
|
|
||||||
obj = queryset.get(**filter_kwargs)
|
|
||||||
l_obj = queryset.model._base_manager.select_for_update().get(pk=obj.pk)
|
|
||||||
self.check_object_permissions(self.request, l_obj)
|
|
||||||
return l_obj
|
|
||||||
|
|
||||||
except queryset.model.DoesNotExist:
|
|
||||||
raise Http404
|
|
||||||
else:
|
|
||||||
# 其他情况:普通查询
|
|
||||||
return super().get_object()
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
action_serializer_name = f"{self.action}_serializer_class"
|
action_serializer_name = f"{self.action}_serializer_class"
|
||||||
action_serializer_class = getattr(self, action_serializer_name, None)
|
action_serializer_class = getattr(self, action_serializer_name, None)
|
||||||
|
|
@ -136,16 +102,16 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
queryset = super().filter_queryset(queryset)
|
||||||
|
# 如果带有with_children查询, 出于优化需要应自动过滤掉一些内容
|
||||||
|
if (self.request.query_params.get("with_children", "no") in ["yes", "count"]
|
||||||
|
and self.request.query_params.get("parent", None) is None):
|
||||||
|
queryset = queryset.filter(parent=None)
|
||||||
# 用于性能优化
|
# 用于性能优化
|
||||||
if self.select_related_fields:
|
if self.select_related_fields:
|
||||||
queryset = queryset.select_related(*self.select_related_fields)
|
queryset = queryset.select_related(*self.select_related_fields)
|
||||||
if self.prefetch_related_fields:
|
if self.prefetch_related_fields:
|
||||||
queryset = queryset.prefetch_related(*self.prefetch_related_fields)
|
queryset = queryset.prefetch_related(*self.prefetch_related_fields)
|
||||||
queryset = super().filter_queryset(queryset)
|
|
||||||
# 如果带有with_children查询, 出于优化需要应自动过滤掉一些内容
|
|
||||||
# if (self.request.query_params.get("with_children", "no") in ["yes", "count"]
|
|
||||||
# and self.request.query_params.get("parent", None) is None):
|
|
||||||
# queryset = queryset.filter(parent=None)
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
@ -217,16 +183,44 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(create_by=self.request.user)
|
return queryset.filter(create_by=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListModelMixin,
|
class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListModelMixin,
|
||||||
CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
CustomRetrieveModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||||
"""
|
"""
|
||||||
增强的ModelViewSet
|
增强的ModelViewSet
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@swagger_auto_schema(request_body=ComplexSerializer, responses={200: {}})
|
||||||
|
@action(methods=['post'], detail=False, perms_map={'post': '*'})
|
||||||
|
def cquery(self, request):
|
||||||
|
"""复杂查询
|
||||||
|
|
||||||
class EuModelViewSet(BulkCreateModelMixin, CustomListModelMixin,
|
复杂查询
|
||||||
CustomRetrieveModelMixin, BulkDestroyModelMixin, ComplexQueryMixin, CustomGenericViewSet):
|
"""
|
||||||
"""
|
sr = ComplexSerializer(data=request.data)
|
||||||
不支持更新的增强ModelViewSet
|
sr.is_valid(raise_exception=True)
|
||||||
"""
|
vdata = sr.validated_data
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
new_qs = queryset.none()
|
||||||
|
try:
|
||||||
|
for m in vdata.get('querys', []):
|
||||||
|
one_qs = queryset
|
||||||
|
for n in m:
|
||||||
|
st = {}
|
||||||
|
if n['compare'] == '!': # 如果是排除比较式
|
||||||
|
st[n['field']] = n['value']
|
||||||
|
one_qs = one_qs.exclude(**st)
|
||||||
|
elif n['compare'] == '':
|
||||||
|
st[n['field']] = n['value']
|
||||||
|
one_qs = one_qs.filter(**st)
|
||||||
|
else:
|
||||||
|
st[n['field'] + '__' + n['compare']] = n['value']
|
||||||
|
one_qs = one_qs.filter(**st)
|
||||||
|
new_qs = new_qs | one_qs
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError(str(e))
|
||||||
|
page = self.paginate_queryset(new_qs)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
serializer = self.get_serializer(new_qs, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2025-09-19 01:08
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wf', '0002_alter_state_filter_dept'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='workflow',
|
|
||||||
name='view_path',
|
|
||||||
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2025-11-18 01:44
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wf', '0004_workflow_view_path2'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='workflow',
|
|
||||||
name='cate',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='分类'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 3.2.12 on 2025-12-15 08:45
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wf', '0005_workflow_cate'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='workflow',
|
|
||||||
name='content_template',
|
|
||||||
field=models.TextField(blank=True, default='标题:{title}, 创建时间:{create_time}', help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}', null=True, verbose_name='内容模板'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='workflow',
|
|
||||||
name='title_template',
|
|
||||||
field=models.TextField(blank=True, default='{title}', help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}', null=True, verbose_name='标题模板'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
from apps.wf.models import Workflow, Ticket, State
|
|
||||||
from rest_framework.exceptions import ParseError
|
|
||||||
from apps.wf.services import WfService
|
|
||||||
from apps.system.models import User
|
|
||||||
|
|
||||||
class TicketMixin:
|
|
||||||
"""
|
|
||||||
可挂载到正常model,使其支持工作流
|
|
||||||
model添加ticket字段
|
|
||||||
serializer添加ticket_
|
|
||||||
该处会修改perform_create和perform_update方法,注意!
|
|
||||||
"""
|
|
||||||
workflow_key = None
|
|
||||||
ticket_auto_submit_on_update = True
|
|
||||||
ticket_auto_submit_on_create = True
|
|
||||||
|
|
||||||
def get_workflow_key(self, instance):
|
|
||||||
return self.workflow_key
|
|
||||||
|
|
||||||
def should_create_ticket(self, instance):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def gen_other_ticket_data(self, instance):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def gen_ticket_data(self, instance):
|
|
||||||
ticket_data = {"t_model": instance.__class__.__name__, "t_id": str(instance.id)}
|
|
||||||
other_data = self.gen_other_ticket_data(instance)
|
|
||||||
if other_data:
|
|
||||||
ticket_data.update(other_data)
|
|
||||||
return ticket_data
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
|
||||||
ins = serializer.save()
|
|
||||||
ruser = self.request.user
|
|
||||||
if ins.ticket and self.ticket_auto_submit_on_update:
|
|
||||||
source_state:State = ins.ticket.state
|
|
||||||
if source_state.type != State.STATE_TYPE_START:
|
|
||||||
raise ParseError('该工单已开始流转,不可修改')
|
|
||||||
if ruser != ins.ticket.create_by:
|
|
||||||
raise ParseError('非工单创建人不可修改')
|
|
||||||
transitions = WfService.get_state_transitions(source_state)
|
|
||||||
if transitions.count() == 1:
|
|
||||||
transition = transitions.first()
|
|
||||||
ticket_data = self.gen_ticket_data(ins)
|
|
||||||
WfService.handle_ticket(ticket=ins.ticket, transition=transition, new_ticket_data=ticket_data,
|
|
||||||
handler=self.request.user, oinfo=self.request.data)
|
|
||||||
else:
|
|
||||||
raise ParseError('有多个或无后续状态;不可处理')
|
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
|
||||||
ins = serializer.save()
|
|
||||||
handler:User = self.request.user
|
|
||||||
if self.should_create_ticket(ins):
|
|
||||||
workflow_key = self.get_workflow_key(ins)
|
|
||||||
if not workflow_key:
|
|
||||||
raise ParseError('工作流异常:必须赋值workflow_key')
|
|
||||||
try:
|
|
||||||
wf = Workflow.objects.get(key=workflow_key)
|
|
||||||
except Exception as e:
|
|
||||||
raise ParseError(f'工作流{workflow_key}异常:{e}')
|
|
||||||
|
|
||||||
# 开始创建工单
|
|
||||||
ticket_data = self.gen_ticket_data(ins)
|
|
||||||
ticket = WfService.handle_ticket(ticket=None, transition=None, workflow=wf, new_ticket_data=ticket_data,
|
|
||||||
handler=handler, oinfo=self.request.data)
|
|
||||||
ins.ticket = ticket
|
|
||||||
ins.save(update_fields=['ticket'])
|
|
||||||
|
|
||||||
if self.ticket_auto_submit_on_create:
|
|
||||||
source_state: State = WfService.get_workflow_start_state(wf)
|
|
||||||
transitions = WfService.get_state_transitions(source_state)
|
|
||||||
if transitions.count() == 1:
|
|
||||||
transition = transitions.first()
|
|
||||||
WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
|
|
||||||
handler=handler, oinfo=self.request.data)
|
|
||||||
else:
|
|
||||||
raise ParseError(f'工作流{workflow_key}异常:有多个或无后续状态;不可处理')
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
|
||||||
ticket = instance.ticket
|
|
||||||
if ticket and ticket.state.type != State.STATE_TYPE_START:
|
|
||||||
raise ParseError('该工单已开始流转,不可删除')
|
|
||||||
instance.delete()
|
|
||||||
ticket.delete()
|
|
||||||
|
|
||||||
|
|
@ -9,7 +9,6 @@ class Workflow(CommonAModel):
|
||||||
工作流
|
工作流
|
||||||
"""
|
"""
|
||||||
name = models.CharField('名称', max_length=50)
|
name = models.CharField('名称', max_length=50)
|
||||||
cate = models.CharField('分类', max_length=50, null=True, blank=True)
|
|
||||||
key = models.CharField('工作流标识', unique=True, max_length=20, null=True, blank=True)
|
key = models.CharField('工作流标识', unique=True, max_length=20, null=True, blank=True)
|
||||||
sn_prefix = models.CharField('流水号前缀', max_length=50, default='hb')
|
sn_prefix = models.CharField('流水号前缀', max_length=50, default='hb')
|
||||||
description = models.CharField('描述', max_length=200, null=True, blank=True)
|
description = models.CharField('描述', max_length=200, null=True, blank=True)
|
||||||
|
|
@ -18,11 +17,10 @@ class Workflow(CommonAModel):
|
||||||
'限制表达式', default=dict, blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)')
|
'限制表达式', default=dict, blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)')
|
||||||
display_form_str = models.JSONField('展现表单字段', default=list, blank=True,
|
display_form_str = models.JSONField('展现表单字段', default=list, blank=True,
|
||||||
help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称')
|
help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称')
|
||||||
title_template = models.TextField(
|
title_template = models.CharField(
|
||||||
'标题模板', default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
|
'标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
|
||||||
content_template = models.TextField(
|
content_template = models.CharField(
|
||||||
'内容模板', default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
|
'内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
|
||||||
view_path = models.TextField('前端自定义页面路径', null=True, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '工作流'
|
verbose_name = '工作流'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from apps.system.models import Dept, User, Post, Role
|
from apps.system.models import Dept, User
|
||||||
from apps.system.serializers import UserSignatureSerializer, UserSimpleSerializer, DeptSimpleSerializer, PostSimpleSerializer, RoleSimpleSerializer
|
from apps.system.serializers import UserSignatureSerializer, UserSimpleSerializer
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from apps.utils.serializers import CustomModelSerializer
|
from apps.utils.serializers import CustomModelSerializer
|
||||||
|
|
||||||
|
|
@ -23,25 +23,11 @@ class StateSerializer(CustomModelSerializer):
|
||||||
model = State
|
model = State
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
class StateDetailSerializer(StateSerializer):
|
|
||||||
participant_ = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
def get_participant_(self, obj:State):
|
|
||||||
if obj.participant_type == State.PARTICIPANT_TYPE_PERSONAL:
|
|
||||||
return UserSimpleSerializer(instance=User.objects.get(id=obj.participant)).data
|
|
||||||
elif obj.participant_type == State.PARTICIPANT_TYPE_MULTI:
|
|
||||||
return UserSimpleSerializer(instance=User.objects.filter(id__in=obj.participant), many=True).data
|
|
||||||
elif obj.participant_type == State.PARTICIPANT_TYPE_DEPT:
|
|
||||||
return DeptSimpleSerializer(instance=Dept.objects.filter(id__in=obj.participant), many=True).data
|
|
||||||
elif obj.participant_type == State.PARTICIPANT_TYPE_POST:
|
|
||||||
return PostSimpleSerializer(instance=Post.objects.filter(id__in=obj.participant), many=True).data
|
|
||||||
elif obj.participant_type == State.PARTICIPANT_TYPE_ROLE:
|
|
||||||
return RoleSimpleSerializer(instance=Role.objects.filter(id__in=obj.participant), many=True).data
|
|
||||||
|
|
||||||
class WorkflowSimpleSerializer(CustomModelSerializer):
|
class WorkflowSimpleSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Workflow
|
model = Workflow
|
||||||
fields = ['id', 'name', 'key', 'view_path']
|
fields = ['id', 'name', 'key']
|
||||||
|
|
||||||
|
|
||||||
class StateSimpleSerializer(CustomModelSerializer):
|
class StateSimpleSerializer(CustomModelSerializer):
|
||||||
|
|
@ -108,7 +94,7 @@ class TicketSimpleSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class TicketCreateSerializer(CustomModelSerializer):
|
class TicketCreateSerializer(CustomModelSerializer):
|
||||||
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), write_only=True, allow_null=True, required=False)
|
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), write_only=True)
|
||||||
title = serializers.CharField(allow_blank=True, required=False)
|
title = serializers.CharField(allow_blank=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -137,13 +123,11 @@ class TicketListSerializer(CustomModelSerializer):
|
||||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||||
participant_ = serializers.SerializerMethodField()
|
participant_ = serializers.SerializerMethodField()
|
||||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_',
|
fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_',
|
||||||
'act_state', 'create_time', 'update_time', 'participant_type',
|
'act_state', 'create_time', 'update_time', 'participant_type', 'create_by', 'ticket_data',
|
||||||
'create_by', 'create_by_name', 'ticket_data',
|
|
||||||
'participant_', 'script_run_last_result', 'participant']
|
'participant_', 'script_run_last_result', 'participant']
|
||||||
|
|
||||||
def get_participant_(self, obj):
|
def get_participant_(self, obj):
|
||||||
|
|
@ -154,7 +138,7 @@ class TicketListSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_eager_loading(queryset):
|
def setup_eager_loading(queryset):
|
||||||
queryset = queryset.select_related('workflow', 'state', 'create_by')
|
queryset = queryset.select_related('workflow', 'state')
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -163,7 +147,6 @@ class TicketDetailSerializer(CustomModelSerializer):
|
||||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||||
ticket_data_ = serializers.SerializerMethodField()
|
ticket_data_ = serializers.SerializerMethodField()
|
||||||
participant_ = serializers.SerializerMethodField()
|
participant_ = serializers.SerializerMethodField()
|
||||||
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Ticket
|
model = Ticket
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import random
|
||||||
from apps.utils.queryset import get_parent_queryset
|
from apps.utils.queryset import get_parent_queryset
|
||||||
from apps.wf.tasks import run_task
|
from apps.wf.tasks import run_task
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
import time
|
|
||||||
|
|
||||||
class WfService(object):
|
class WfService(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -77,7 +77,7 @@ class WfService(object):
|
||||||
"""
|
"""
|
||||||
获取状态可执行的操作
|
获取状态可执行的操作
|
||||||
"""
|
"""
|
||||||
return Transition.objects.filter(is_deleted=False, source_state=state).all().order_by("-attribute_type", "-id")
|
return Transition.objects.filter(is_deleted=False, source_state=state).all()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_ticket_steps(cls, ticket: Ticket):
|
def get_ticket_steps(cls, ticket: Ticket):
|
||||||
|
|
@ -184,15 +184,7 @@ class WfService(object):
|
||||||
dpt_attrs = state.filter_dept.split('.') # 通过反向查询得到可能有多层
|
dpt_attrs = state.filter_dept.split('.') # 通过反向查询得到可能有多层
|
||||||
expr = ticket
|
expr = ticket
|
||||||
for i in dpt_attrs:
|
for i in dpt_attrs:
|
||||||
try:
|
expr = getattr(expr, i)
|
||||||
expr = getattr(expr, i)
|
|
||||||
except AttributeError as e:
|
|
||||||
if "'RelatedManager' object has no attribute" in str(e):
|
|
||||||
expr = getattr(expr.first(), i)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
if expr is None:
|
|
||||||
raise ParseError('未找到对应部门')
|
|
||||||
dpts = Dept.objects.filter(id=expr.id)
|
dpts = Dept.objects.filter(id=expr.id)
|
||||||
user_queryset = user_queryset.filter(depts__in=dpts)
|
user_queryset = user_queryset.filter(depts__in=dpts)
|
||||||
# if state.filter_policy == 1:
|
# if state.filter_policy == 1:
|
||||||
|
|
@ -305,56 +297,9 @@ class WfService(object):
|
||||||
return field_info_dict
|
return field_info_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle_ticket(cls, ticket: Ticket=None, transition: Transition=None, workflow: Workflow=None, new_ticket_data: dict = {}, oinfo: dict = {}, handler: User = None,
|
def handle_ticket(cls, ticket: Ticket, transition: Transition, new_ticket_data: dict = {}, handler: User = None,
|
||||||
suggestion: str = '', by_timer: bool = False,
|
suggestion: str = '', created: bool = False, by_timer: bool = False,
|
||||||
by_task: bool = False, by_hook: bool = False):
|
by_task: bool = False, by_hook: bool = False):
|
||||||
just_created = False
|
|
||||||
if ticket is None:
|
|
||||||
# 创建工单逻辑
|
|
||||||
if transition:
|
|
||||||
if workflow and transition.workflow.id != workflow.id:
|
|
||||||
raise ParseError("当前流转不属于该工作流")
|
|
||||||
workflow = transition.workflow
|
|
||||||
|
|
||||||
start_state = WfService.get_workflow_start_state(workflow)
|
|
||||||
save_ticket_data = {}
|
|
||||||
if transition and transition.field_require_check:
|
|
||||||
for key, value in start_state.state_fields.items():
|
|
||||||
if int(value) == State.STATE_FIELD_REQUIRED:
|
|
||||||
if key not in new_ticket_data and not new_ticket_data[key]:
|
|
||||||
raise ParseError('字段{}必填'.format(key))
|
|
||||||
save_ticket_data[key] = new_ticket_data[key]
|
|
||||||
elif int(value) == State.STATE_FIELD_OPTIONAL:
|
|
||||||
save_ticket_data[key] = new_ticket_data[key]
|
|
||||||
else:
|
|
||||||
save_ticket_data = new_ticket_data
|
|
||||||
ticket = Ticket.objects.create(workflow=workflow,
|
|
||||||
state=start_state,
|
|
||||||
create_by=handler,
|
|
||||||
create_time=timezone.now(),
|
|
||||||
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
|
|
||||||
belong_dept=handler.belong_dept,
|
|
||||||
ticket_data=save_ticket_data, participant_type=1, participant=handler.id) # 先创建出来
|
|
||||||
|
|
||||||
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
|
|
||||||
ticket.sn = sn
|
|
||||||
ticket.save()
|
|
||||||
if not transition:
|
|
||||||
return ticket
|
|
||||||
just_created = True # 刚创建的工单不需要校验权限
|
|
||||||
|
|
||||||
if transition and transition.source_state.type == State.STATE_TYPE_START:
|
|
||||||
# 更新title和sn
|
|
||||||
ticket_title = oinfo.get("title", "")
|
|
||||||
title_template = ticket.workflow.title_template
|
|
||||||
if title_template:
|
|
||||||
all_ticket_data = {**oinfo, **new_ticket_data}
|
|
||||||
try:
|
|
||||||
ticket_title = title_template.format(**all_ticket_data)
|
|
||||||
except KeyError as e:
|
|
||||||
raise ParseError(f"工单标题模板中存在未定义的变量:{e}")
|
|
||||||
ticket.title = ticket_title
|
|
||||||
ticket.save(update_fields=["title"])
|
|
||||||
|
|
||||||
source_state = ticket.state
|
source_state = ticket.state
|
||||||
source_ticket_data = ticket.ticket_data
|
source_ticket_data = ticket.ticket_data
|
||||||
|
|
@ -370,13 +315,13 @@ class WfService(object):
|
||||||
f(ticket=ticket, transition=transition, new_ticket_data=new_ticket_data)
|
f(ticket=ticket, transition=transition, new_ticket_data=new_ticket_data)
|
||||||
|
|
||||||
# 校验处理权限
|
# 校验处理权限
|
||||||
if handler is not None and just_created is False: # 有处理人意味着系统触发校验处理权限
|
if handler is not None and created is False: # 有处理人意味着系统触发校验处理权限
|
||||||
result = WfService.ticket_handle_permission_check(ticket, handler)
|
result = WfService.ticket_handle_permission_check(ticket, handler)
|
||||||
if result.get('permission') is False:
|
if result.get('permission') is False:
|
||||||
raise PermissionDenied(result.get('msg'))
|
raise PermissionDenied(result.get('msg'))
|
||||||
|
|
||||||
# 校验表单必填项目
|
# 校验表单必填项目
|
||||||
if transition.field_require_check or not just_created:
|
if transition.field_require_check or not created:
|
||||||
for key, value in ticket.state.state_fields.items():
|
for key, value in ticket.state.state_fields.items():
|
||||||
if int(value) == State.STATE_FIELD_REQUIRED:
|
if int(value) == State.STATE_FIELD_REQUIRED:
|
||||||
if key not in new_ticket_data or not new_ticket_data[key]:
|
if key not in new_ticket_data or not new_ticket_data[key]:
|
||||||
|
|
@ -424,7 +369,7 @@ class WfService(object):
|
||||||
ticket.act_state = Ticket.TICKET_ACT_STATE_BACK
|
ticket.act_state = Ticket.TICKET_ACT_STATE_BACK
|
||||||
|
|
||||||
# 只更新必填和可选的字段
|
# 只更新必填和可选的字段
|
||||||
if not just_created and transition.field_require_check:
|
if not created and transition.field_require_check:
|
||||||
for key, value in source_state.state_fields.items():
|
for key, value in source_state.state_fields.items():
|
||||||
if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL):
|
if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL):
|
||||||
if key in new_ticket_data:
|
if key in new_ticket_data:
|
||||||
|
|
@ -439,7 +384,7 @@ class WfService(object):
|
||||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
participant=handler, transition=transition)
|
participant=handler, transition=transition)
|
||||||
|
|
||||||
if just_created:
|
if created:
|
||||||
if source_state.participant_cc:
|
if source_state.participant_cc:
|
||||||
TicketFlow.objects.create(ticket=ticket, state=source_state,
|
TicketFlow.objects.create(ticket=ticket, state=source_state,
|
||||||
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
|
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
|
||||||
|
|
@ -497,18 +442,12 @@ class WfService(object):
|
||||||
last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or
|
last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or
|
||||||
ticket.in_add_node):
|
ticket.in_add_node):
|
||||||
# 如果状态变化或是转交加签的情况再发送通知
|
# 如果状态变化或是转交加签的情况再发送通知
|
||||||
cls.send_ticket_notice(ticketflow=last_log)
|
Thread(target=send_ticket_notice_t, args=(ticket,), daemon=True).start()
|
||||||
|
|
||||||
# 如果目标状态是脚本则异步执行
|
# 如果目标状态是脚本则异步执行
|
||||||
if state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
|
if state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
|
||||||
run_task.delay(ticket_id=ticket.id)
|
run_task.delay(ticket_id=ticket.id)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def send_ticket_notice(cls, ticketflow:TicketFlow):
|
|
||||||
# 根据ticketflow发送通知
|
|
||||||
Thread(target=send_ticket_notice_t, args=(ticketflow.id,), daemon=True).start()
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def close_by_task(cls, ticket: Ticket, suggestion: str):
|
def close_by_task(cls, ticket: Ticket, suggestion: str):
|
||||||
# 定时任务触发的工单关闭
|
# 定时任务触发的工单关闭
|
||||||
|
|
@ -523,31 +462,11 @@ class WfService(object):
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_ROBOT,
|
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_ROBOT,
|
||||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CLOSE, transition=None)
|
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CLOSE, transition=None)
|
||||||
@classmethod
|
|
||||||
def retreat(cls, ticket: Ticket, suggestion: str, handler: User, next_handler: User):
|
|
||||||
"""
|
|
||||||
回退
|
|
||||||
"""
|
|
||||||
start_state = WfService.get_workflow_start_state(ticket.workflow)
|
|
||||||
ticket.state = start_state
|
|
||||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
|
||||||
ticket.participant = next_handler.id
|
|
||||||
ticket.act_state = Ticket.TICKET_ACT_STATE_RETREAT
|
|
||||||
ticket.save()
|
|
||||||
# 更新流转记录
|
|
||||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
|
||||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
|
||||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT,
|
|
||||||
participant=handler, transition=None)
|
|
||||||
cls.task_ticket(ticket=ticket)
|
|
||||||
|
|
||||||
def send_ticket_notice_t(ticketflowId: str):
|
def send_ticket_notice_t(ticket: Ticket):
|
||||||
"""
|
"""
|
||||||
发送通知
|
发送通知
|
||||||
"""
|
"""
|
||||||
time.sleep(3)
|
|
||||||
ticket = TicketFlow.objects.get(id=ticketflowId).ticket
|
|
||||||
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
|
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
|
||||||
if ticket.participant_type == 1:
|
if ticket.participant_type == 1:
|
||||||
# 发送短信通知
|
# 发送短信通知
|
||||||
|
|
|
||||||
124
apps/wf/views.py
124
apps/wf/views.py
|
|
@ -8,7 +8,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, \
|
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, \
|
||||||
RetrieveModelMixin, UpdateModelMixin
|
RetrieveModelMixin, UpdateModelMixin
|
||||||
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, \
|
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, \
|
||||||
StateDetailSerializer, TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, \
|
TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, \
|
||||||
TicketCreateSerializer, TicketDeliverSerializer, TicketDestorySerializer, TicketFlowSerializer, \
|
TicketCreateSerializer, TicketDeliverSerializer, TicketDestorySerializer, TicketFlowSerializer, \
|
||||||
TicketHandleSerializer, TicketRetreatSerializer, \
|
TicketHandleSerializer, TicketRetreatSerializer, \
|
||||||
TicketSerializer, TransitionSerializer, WorkflowSerializer, \
|
TicketSerializer, TransitionSerializer, WorkflowSerializer, \
|
||||||
|
|
@ -20,7 +20,7 @@ from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin
|
||||||
from apps.wf.services import WfService
|
from apps.wf.services import WfService
|
||||||
from rest_framework.exceptions import ParseError, NotFound
|
from rest_framework.exceptions import ParseError, NotFound
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from django.db.models import Count, Case, When, IntegerField, F
|
from django.db.models import Count
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
from apps.utils.snowflake import idWorker
|
from apps.utils.snowflake import idWorker
|
||||||
import importlib
|
import importlib
|
||||||
|
|
@ -60,16 +60,10 @@ class WorkflowKeyInitView(APIView):
|
||||||
class WorkflowViewSet(CustomModelViewSet):
|
class WorkflowViewSet(CustomModelViewSet):
|
||||||
queryset = Workflow.objects.all()
|
queryset = Workflow.objects.all()
|
||||||
serializer_class = WorkflowSerializer
|
serializer_class = WorkflowSerializer
|
||||||
search_fields = ['name', 'description', 'key']
|
search_fields = ['name', 'description']
|
||||||
filterset_fields = ['key', 'cate']
|
filterset_fields = []
|
||||||
ordering_fields = ['create_time', 'key', 'cate']
|
ordering_fields = ['create_time']
|
||||||
|
ordering = ['key', '-create_time']
|
||||||
@action(methods=['get'], detail=False, perms_map={'get': '*'})
|
|
||||||
def cates(self, request, pk=None):
|
|
||||||
"""
|
|
||||||
工作流分类
|
|
||||||
"""
|
|
||||||
return Response(Workflow.objects.filter(cate__isnull=False).values_list('cate', flat=True).distinct())
|
|
||||||
|
|
||||||
@action(methods=['get'], detail=True, perms_map={'get': 'workflow.update'},
|
@action(methods=['get'], detail=True, perms_map={'get': 'workflow.update'},
|
||||||
pagination_class=None, serializer_class=StateSerializer)
|
pagination_class=None, serializer_class=StateSerializer)
|
||||||
|
|
@ -179,28 +173,12 @@ class WorkflowViewSet(CustomModelViewSet):
|
||||||
tr.save()
|
tr.save()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map={'get': '*'})
|
|
||||||
def ticket_count(self, request, pk=None):
|
|
||||||
"""工作流下的工单数量统计
|
|
||||||
|
|
||||||
工作流下的工单数量统计
|
|
||||||
"""
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
|
||||||
result = Ticket.objects.filter(workflow__in=queryset).annotate(
|
|
||||||
workflow_name=F('workflow__name'), workflow_cate=F('workflow__cate')).values(
|
|
||||||
'workflow', 'workflow_name', 'workflow_cate').annotate(
|
|
||||||
count_done=Count(Case(When(state__type=2, then=1), output_field=IntegerField())),
|
|
||||||
count_processing=Count(Case(When(state__type=1, then=1), output_field=IntegerField())),
|
|
||||||
)
|
|
||||||
return Response(list(result))
|
|
||||||
|
|
||||||
|
|
||||||
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
||||||
perms_map = {'get': '*', 'post': 'workflow.update',
|
perms_map = {'get': '*', 'post': 'workflow.update',
|
||||||
'put': 'workflow.update', 'delete': 'workflow.update'}
|
'put': 'workflow.update', 'delete': 'workflow.update'}
|
||||||
queryset = State.objects.all()
|
queryset = State.objects.all()
|
||||||
serializer_class = StateSerializer
|
serializer_class = StateSerializer
|
||||||
retrieve_serializer_class = StateDetailSerializer
|
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
filterset_fields = ['workflow']
|
filterset_fields = ['workflow']
|
||||||
ordering = ['sort']
|
ordering = ['sort']
|
||||||
|
|
@ -261,7 +239,6 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
raise ParseError('请指定查询分类')
|
raise ParseError('请指定查询分类')
|
||||||
return super().filter_queryset(queryset)
|
return super().filter_queryset(queryset)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
新建工单
|
新建工单
|
||||||
|
|
@ -270,12 +247,41 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
serializer = self.get_serializer(data=rdata)
|
serializer = self.get_serializer(data=rdata)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
vdata = serializer.validated_data # 校验之后的数据
|
vdata = serializer.validated_data # 校验之后的数据
|
||||||
transition = vdata.get("transition", None)
|
start_state = WfService.get_workflow_start_state(vdata['workflow'])
|
||||||
workflow = vdata['workflow']
|
transition = vdata.pop('transition')
|
||||||
ticket_data = vdata['ticket_data']
|
ticket_data = vdata['ticket_data']
|
||||||
ticket = WfService.handle_ticket(ticket=None, transition=transition,
|
|
||||||
workflow=workflow, new_ticket_data=ticket_data,
|
save_ticket_data = {}
|
||||||
oinfo=rdata, handler=request.user)
|
# 校验必填项
|
||||||
|
if transition.field_require_check:
|
||||||
|
for key, value in start_state.state_fields.items():
|
||||||
|
if int(value) == State.STATE_FIELD_REQUIRED:
|
||||||
|
if key not in ticket_data and not ticket_data[key]:
|
||||||
|
raise ParseError('字段{}必填'.format(key))
|
||||||
|
save_ticket_data[key] = ticket_data[key]
|
||||||
|
elif int(value) == State.STATE_FIELD_OPTIONAL:
|
||||||
|
save_ticket_data[key] = ticket_data[key]
|
||||||
|
else:
|
||||||
|
save_ticket_data = ticket_data
|
||||||
|
with transaction.atomic():
|
||||||
|
ticket = serializer.save(state=start_state,
|
||||||
|
create_by=request.user,
|
||||||
|
create_time=timezone.now(),
|
||||||
|
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
|
||||||
|
belong_dept=request.user.belong_dept,
|
||||||
|
ticket_data=save_ticket_data) # 先创建出来
|
||||||
|
# 更新title和sn
|
||||||
|
title = vdata.get('title', '')
|
||||||
|
title_template = ticket.workflow.title_template
|
||||||
|
if title_template:
|
||||||
|
all_ticket_data = {**rdata, **ticket_data}
|
||||||
|
title = title_template.format(**all_ticket_data)
|
||||||
|
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
|
||||||
|
ticket.sn = sn
|
||||||
|
ticket.title = title
|
||||||
|
ticket.save()
|
||||||
|
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
|
||||||
|
handler=request.user, created=True)
|
||||||
return Response(TicketSerializer(instance=ticket).data)
|
return Response(TicketSerializer(instance=ticket).data)
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map={'get': '*'})
|
@action(methods=['get'], detail=False, perms_map={'get': '*'})
|
||||||
|
|
@ -291,7 +297,6 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
return Response(ret)
|
return Response(ret)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': '*'})
|
@action(methods=['post'], detail=True, perms_map={'post': '*'})
|
||||||
@transaction.atomic
|
|
||||||
def handle(self, request, pk=None):
|
def handle(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
处理工单
|
处理工单
|
||||||
|
|
@ -302,13 +307,13 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
vdata = serializer.validated_data
|
vdata = serializer.validated_data
|
||||||
new_ticket_data = ticket.ticket_data
|
new_ticket_data = ticket.ticket_data
|
||||||
new_ticket_data.update(**vdata['ticket_data'])
|
new_ticket_data.update(**vdata['ticket_data'])
|
||||||
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
|
with transaction.atomic():
|
||||||
new_ticket_data=new_ticket_data, handler=request.user,
|
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
|
||||||
suggestion=vdata.get('suggestion', ''))
|
new_ticket_data=new_ticket_data, handler=request.user,
|
||||||
|
suggestion=vdata.get('suggestion', ''))
|
||||||
return Response(TicketSerializer(instance=ticket).data)
|
return Response(TicketSerializer(instance=ticket).data)
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': '*'})
|
@action(methods=['post'], detail=True, perms_map={'post': '*'})
|
||||||
@transaction.atomic
|
|
||||||
def deliver(self, request, pk=None):
|
def deliver(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
转交工单
|
转交工单
|
||||||
|
|
@ -320,15 +325,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
vdata = serializer.validated_data # 校验之后的数据
|
vdata = serializer.validated_data # 校验之后的数据
|
||||||
if not ticket.state.enable_deliver:
|
if not ticket.state.enable_deliver:
|
||||||
raise ParseError('不允许转交')
|
raise ParseError('不允许转交')
|
||||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
with transaction.atomic():
|
||||||
ticket.participant = vdata['target_user']
|
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||||
ticket.save()
|
ticket.participant = vdata['target_user']
|
||||||
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
ticket.save()
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||||
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
|
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
participant=request.user, transition=None)
|
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
|
||||||
WfService.send_ticket_notice(ticketflow=tf)
|
participant=request.user, transition=None)
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
@action(methods=['get'], detail=True, perms_map={'get': '*'})
|
||||||
|
|
@ -376,12 +381,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
ticket.save()
|
ticket.save()
|
||||||
# 接单日志
|
# 接单日志
|
||||||
# 更新工单流转记录
|
# 更新工单流转记录
|
||||||
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
|
intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
|
||||||
participant=request.user, transition=None)
|
participant=request.user, transition=None)
|
||||||
WfService.send_ticket_notice(ticketflow=tf)
|
|
||||||
return Response()
|
return Response()
|
||||||
else:
|
else:
|
||||||
raise ParseError('无需接单')
|
raise ParseError('无需接单')
|
||||||
|
|
@ -396,7 +400,19 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
raise ParseError('非创建人不可撤回')
|
raise ParseError('非创建人不可撤回')
|
||||||
if not ticket.state.enable_retreat:
|
if not ticket.state.enable_retreat:
|
||||||
raise ParseError('该状态不可撤回')
|
raise ParseError('该状态不可撤回')
|
||||||
WfService.retreat(ticket, request.data.get('suggestion', ''), request.user, request.user)
|
start_state = WfService.get_workflow_start_state(ticket.workflow)
|
||||||
|
ticket.state = start_state
|
||||||
|
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||||
|
ticket.participant = request.user.id
|
||||||
|
ticket.act_state = Ticket.TICKET_ACT_STATE_RETREAT
|
||||||
|
ticket.save()
|
||||||
|
# 更新流转记录
|
||||||
|
suggestion = request.data.get('suggestion', '') # 撤回原因
|
||||||
|
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||||
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
|
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
|
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT,
|
||||||
|
participant=request.user, transition=None)
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeSerializer)
|
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeSerializer)
|
||||||
|
|
@ -416,12 +432,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
ticket.save()
|
ticket.save()
|
||||||
# 更新流转记录
|
# 更新流转记录
|
||||||
suggestion = request.data.get('suggestion', '') # 加签说明
|
suggestion = request.data.get('suggestion', '') # 加签说明
|
||||||
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE,
|
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE,
|
||||||
participant=request.user, transition=None)
|
participant=request.user, transition=None)
|
||||||
WfService.send_ticket_notice(ticketflow=tf)
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer)
|
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer)
|
||||||
|
|
@ -441,12 +456,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
|
||||||
ticket.save()
|
ticket.save()
|
||||||
# 更新流转记录
|
# 更新流转记录
|
||||||
suggestion = request.data.get('suggestion', '') # 加签意见
|
suggestion = request.data.get('suggestion', '') # 加签意见
|
||||||
tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END,
|
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END,
|
||||||
participant=request.user, transition=None)
|
participant=request.user, transition=None)
|
||||||
WfService.send_ticket_notice(ticketflow=tf)
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=True, perms_map={'post': '*'},
|
@action(methods=['post'], detail=True, perms_map={'post': '*'},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
SECRET_KEY = 'xx'
|
|
||||||
DEBUG = False
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
|
||||||
'NAME': 'xx',
|
|
||||||
'USER': 'postgres',
|
|
||||||
'PASSWORD': 'xx',
|
|
||||||
'HOST': 'xx',
|
|
||||||
'PORT': '5432',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
celery==5.6.2
|
celery==5.2.3
|
||||||
Django==4.2.27
|
Django==3.2.12
|
||||||
django-celery-beat==2.8.1
|
django-celery-beat==2.3.0
|
||||||
django-celery-results==2.6.0
|
django-celery-results==2.4.0
|
||||||
django-cors-headers==4.9.0
|
django-cors-headers==3.11.0
|
||||||
django-filter==23.5
|
django-filter==21.1
|
||||||
djangorestframework==3.16.1
|
djangorestframework==3.13.1
|
||||||
djangorestframework-simplejwt==5.5.1
|
djangorestframework-simplejwt==5.1.0
|
||||||
drf-yasg==1.21.7
|
drf-yasg==1.21.3
|
||||||
psutil==5.9.0
|
psutil==5.9.0
|
||||||
redis==7.1.0
|
redis==4.4.0
|
||||||
django-redis==6.0.0
|
django-redis==5.2.0
|
||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
daphne==4.0.0
|
daphne==4.0.0
|
||||||
channels-redis==4.3.0
|
channels-redis==4.0.0
|
||||||
django-restql==0.15.2
|
django-restql==0.15.2
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
xlwt==1.3.0
|
xlwt==1.3.0
|
||||||
openpyxl==3.1.5
|
openpyxl==3.1.0
|
||||||
cron-descriptor==1.2.35
|
cron-descriptor==1.2.35
|
||||||
docxtpl==0.16.7
|
docxtpl==0.16.7
|
||||||
|
# deepface==0.0.79
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import os
|
import os
|
||||||
from config import conf
|
from . import conf
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.app.control import Control, Inspect
|
from celery.app.control import Control, Inspect
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,8 @@ from datetime import datetime, timedelta
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from config.conf import *
|
from .conf import *
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
import logging
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
|
||||||
|
|
@ -36,7 +34,7 @@ ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
SYS_NAME = 'XT_ADMIN'
|
SYS_NAME = 'XT_ADMIN'
|
||||||
SYS_VERSION = '2.3.0'
|
SYS_VERSION = '2.3.0'
|
||||||
X_FRAME_OPTIONS = 'SAMEORIGIN'
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
|
@ -247,18 +245,6 @@ LOG_PATH = os.path.join(BASE_DIR, 'log')
|
||||||
if not os.path.exists(LOG_PATH):
|
if not os.path.exists(LOG_PATH):
|
||||||
os.makedirs(LOG_PATH)
|
os.makedirs(LOG_PATH)
|
||||||
|
|
||||||
class TimedSizeRotatingHandler(logging.handlers.TimedRotatingFileHandler):
|
|
||||||
def __init__(self, filename, when='midnight', interval=1, backupCount=0,
|
|
||||||
maxBytes=0, encoding=None, delay=False, utc=False, atTime=None):
|
|
||||||
super().__init__(filename, when, interval, backupCount, encoding, delay, utc, atTime)
|
|
||||||
self.maxBytes = maxBytes
|
|
||||||
|
|
||||||
def shouldRollover(self, record):
|
|
||||||
if self.maxBytes > 0 and os.path.exists(self.baseFilename):
|
|
||||||
if os.stat(self.baseFilename).st_size >= self.maxBytes:
|
|
||||||
return True
|
|
||||||
return super().shouldRollover(record)
|
|
||||||
|
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
'version': 1,
|
'version': 1,
|
||||||
'disable_existing_loggers': False,
|
'disable_existing_loggers': False,
|
||||||
|
|
@ -282,28 +268,22 @@ LOGGING = {
|
||||||
# 默认记录所有日志
|
# 默认记录所有日志
|
||||||
'default': {
|
'default': {
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
||||||
'class': 'server.settings.TimedSizeRotatingHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': os.path.join(LOG_PATH, 'all.log'),
|
'filename': os.path.join(LOG_PATH, 'all-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||||
'when': 'midnight', # 每天午夜滚动
|
|
||||||
'interval': 1,
|
|
||||||
'maxBytes': 1024 * 1024 * 2, # 文件大小
|
'maxBytes': 1024 * 1024 * 2, # 文件大小
|
||||||
'backupCount': 30, # 备份数
|
'backupCount': 10, # 备份数
|
||||||
'formatter': 'standard', # 输出格式
|
'formatter': 'standard', # 输出格式
|
||||||
'encoding': 'utf-8', # 设置默认编码,否则打印出来汉字乱码
|
'encoding': 'utf-8', # 设置默认编码,否则打印出来汉字乱码
|
||||||
'delay': True, # 延迟打开文件,减少锁定冲突
|
|
||||||
},
|
},
|
||||||
# 输出错误日志
|
# 输出错误日志
|
||||||
'error': {
|
'error': {
|
||||||
'level': 'ERROR',
|
'level': 'ERROR',
|
||||||
'class': 'server.settings.TimedSizeRotatingHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': os.path.join(LOG_PATH, 'error.log'),
|
'filename': os.path.join(LOG_PATH, 'error-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||||
'when': 'midnight',
|
|
||||||
'interval': 1,
|
|
||||||
'maxBytes': 1024 * 1024 * 2, # 文件大小
|
'maxBytes': 1024 * 1024 * 2, # 文件大小
|
||||||
'backupCount': 30, # 备份数
|
'backupCount': 10, # 备份数
|
||||||
'formatter': 'standard', # 输出格式
|
'formatter': 'standard', # 输出格式
|
||||||
'encoding': 'utf-8', # 设置默认编码
|
'encoding': 'utf-8', # 设置默认编码
|
||||||
'delay': True, # 延迟打开文件,减少锁定冲突
|
|
||||||
},
|
},
|
||||||
# 控制台输出
|
# 控制台输出
|
||||||
'console': {
|
'console': {
|
||||||
|
|
@ -315,15 +295,12 @@ LOGGING = {
|
||||||
# 输出info日志
|
# 输出info日志
|
||||||
'info': {
|
'info': {
|
||||||
'level': 'INFO',
|
'level': 'INFO',
|
||||||
'class': 'server.settings.TimedSizeRotatingHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': os.path.join(LOG_PATH, 'info.log'),
|
'filename': os.path.join(LOG_PATH, 'info-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
|
||||||
'when': 'midnight',
|
|
||||||
'interval': 1,
|
|
||||||
'maxBytes': 1024 * 1024 * 2,
|
'maxBytes': 1024 * 1024 * 2,
|
||||||
'backupCount': 30,
|
'backupCount': 10,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
'encoding': 'utf-8', # 设置默认编码
|
'encoding': 'utf-8', # 设置默认编码
|
||||||
'delay': True, # 延迟打开文件,减少锁定冲突
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# 配置用哪几种 handlers 来处理日志
|
# 配置用哪几种 handlers 来处理日志
|
||||||
|
|
@ -344,7 +321,7 @@ LOGGING = {
|
||||||
}
|
}
|
||||||
|
|
||||||
##### 加载客户可自定义配置并提供操作方法 #####
|
##### 加载客户可自定义配置并提供操作方法 #####
|
||||||
SYS_JSON_PATH = os.path.join(BASE_DIR, 'config/conf.json')
|
SYS_JSON_PATH = os.path.join(BASE_DIR, 'server/conf.json')
|
||||||
|
|
||||||
def get_sysconfig(key='', default='raise_error', reload=False):
|
def get_sysconfig(key='', default='raise_error', reload=False):
|
||||||
"""获取系统配置可指定key字符串
|
"""获取系统配置可指定key字符串
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue