From bea318dc5309c014ebf5c64840a0e4eec8a26ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=B9=E5=89=8D=E6=98=8E?= <909355014@qq.com> Date: Tue, 10 May 2022 21:10:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E8=B0=83=E5=BA=A6celery?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/system/errors.py | 2 + apps/system/serializers.py | 46 +++++++- apps/system/tasks.py | 9 +- apps/system/urls.py | 6 +- apps/system/views.py | 79 ++++++++++++- apps/third/urls.py | 2 +- apps/third/views.py | 16 ++- apps/third/views_d.py | 33 +++++- apps/utils/speaker.py | 4 +- apps/utils/task.py | 11 ++ apps/wf/migrations/0001_initial.py | 174 +++++++++++++++++++++++++++++ apps/wf/migrations/__init__.py | 0 requirements.txt | 2 +- server/celery.py | 2 +- server/settings.py | 39 ++++++- 15 files changed, 392 insertions(+), 33 deletions(-) create mode 100644 apps/utils/task.py create mode 100644 apps/wf/migrations/0001_initial.py create mode 100644 apps/wf/migrations/__init__.py diff --git a/apps/system/errors.py b/apps/system/errors.py index afdbbd7a..ef2d1eb2 100755 --- a/apps/system/errors.py +++ b/apps/system/errors.py @@ -2,6 +2,8 @@ SCHEDULE_WRONG = {"code": "schedule_wrong", "detail": "时间策略有误"} PASSWORD_NOT_SAME = {"code": "password_not_same", "detail": "新旧密码不一致"} OLD_PASSWORD_WRONG = {"code": "old_password_wrong", "detail": "旧密码错误"} +FUNC_ERROR = {"code": "func_error", "detail": "执行方法有误"} + USERNAME_EXIST = {"code": "username_exist", "detail": "账户已存在"} ROLE_NAME_EXIST = {"code": "role_name_exist", "detail": "角色名已存在"} ROLE_CODE_EXIST = {"code": "role_code_exist", "detail": "角色标识已存在"} diff --git a/apps/system/serializers.py b/apps/system/serializers.py index 3414a6d9..3fb8b457 100755 --- a/apps/system/serializers.py +++ b/apps/system/serializers.py @@ -11,6 +11,40 @@ from rest_framework.exceptions import ParseError from django.db import transaction from apps.third.tapis import dhapis from rest_framework.validators import UniqueValidator +# from django_q.models import Task as QTask, Schedule as QSchedule + + +# class QScheduleSerializer(CustomModelSerializer): +# success = serializers.SerializerMethodField() + +# class Meta: +# model = QSchedule +# fields = '__all__' + +# def get_success(self, obj): +# return obj.success() + + +# class QTaskResultSerializer(CustomModelSerializer): +# args = serializers.SerializerMethodField() +# kwargs = serializers.SerializerMethodField() +# result = serializers.SerializerMethodField() + +# class Meta: +# model = QTask +# fields = '__all__' + +# def get_args(self, obj): +# return obj.args + +# def get_kwargs(self, obj): +# return obj.kwargs + +# def get_result(self, obj): +# return obj.result + +class TaskRunSerializer(serializers.Serializer): + sync = serializers.BooleanField(default=True) class IntervalSerializer(CustomModelSerializer): @@ -56,6 +90,12 @@ class PTaskSerializer(CustomModelSerializer): return 'interval' +class PTaskResultSerializer(CustomModelSerializer): + class Meta: + model = TaskResult + fields = '__all__' + + class FileSerializer(CustomModelSerializer): class Meta: model = File @@ -308,12 +348,6 @@ class PasswordChangeSerializer(serializers.Serializer): new_password2 = serializers.CharField(label="新密码2") -class PTaskResultSerializer(CustomModelSerializer): - class Meta: - model = TaskResult - fields = '__all__' - - class UserPostSerializer(CustomModelSerializer): """ 用户-岗位序列化 diff --git a/apps/system/tasks.py b/apps/system/tasks.py index 3b23e547..69bd0984 100755 --- a/apps/system/tasks.py +++ b/apps/system/tasks.py @@ -1,9 +1,12 @@ # Create your tasks here from __future__ import absolute_import, unicode_literals - +from random import random, randint +from apps.utils.task import CustomTask from celery import shared_task +from django.core.mail import send_mail -@shared_task +@shared_task(base=CustomTask) def show(): - print('ok') + x = randint(0, 5) + print(40/x) diff --git a/apps/system/urls.py b/apps/system/urls.py index 6d0bf054..927eede4 100755 --- a/apps/system/urls.py +++ b/apps/system/urls.py @@ -1,8 +1,8 @@ from django.urls import path, include -from .views import FileViewSet, PTaskResultViewSet, TaskList, \ +from .views import FileViewSet, PTaskViewSet, PTaskResultViewSet, TaskList, \ UserPostViewSet, UserViewSet, DeptViewSet, \ PermissionViewSet, RoleViewSet, PostViewSet, \ - DictTypeViewSet, DictViewSet, PTaskViewSet + DictTypeViewSet, DictViewSet from rest_framework import routers API_BASE_URL = 'api/system/' @@ -18,6 +18,8 @@ router.register('dicttype', DictTypeViewSet, basename="dicttype") router.register('dict', DictViewSet, basename="dict") router.register('ptask', PTaskViewSet, basename="ptask") router.register('ptask_result', PTaskResultViewSet, basename="ptask_result") +# router.register('qschedule', QScheduleViewSet, basename="qschedule") +# router.register('qtask_result', QTaskResultViewSet, basename="qtask_result") router.register('user_post', UserPostViewSet, basename='user_post') router2 = routers.DefaultRouter() diff --git a/apps/system/views.py b/apps/system/views.py index 4748229f..f0bd786d 100755 --- a/apps/system/views.py +++ b/apps/system/views.py @@ -1,3 +1,5 @@ +import importlib +import json from django.contrib.auth.hashers import check_password, make_password from django.db import transaction from django_celery_beat.models import (CrontabSchedule, IntervalSchedule, @@ -12,8 +14,8 @@ from rest_framework.parsers import (JSONParser, from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG - +from apps.system.errors import FUNC_ERROR, OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG +# from django_q.models import Task as QTask, Schedule as QSchedule from apps.utils.mixins import (CustomCreateModelMixin) from django.conf import settings from apps.utils.permission import ALL_PERMS, get_user_perms_map @@ -26,8 +28,8 @@ from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreate DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer, FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer, - PTaskCreateUpdateSerializer, PTaskResultSerializer, - PTaskSerializer, RoleCreateUpdateSerializer, RoleSerializer, + PTaskSerializer, PTaskCreateUpdateSerializer, PTaskResultSerializer, + RoleCreateUpdateSerializer, RoleSerializer, TaskRunSerializer, UserCreateSerializer, UserListSerializer, UserPostCreateSerializer, UserPostSerializer, UserUpdateSerializer) @@ -40,7 +42,7 @@ from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreate class TaskList(APIView): permission_classes = [IsAuthenticated] - def get(self, requests): + def get(self, request): """获取注册任务列表 获取注册任务列表 @@ -50,6 +52,53 @@ class TaskList(APIView): return Response(tasks) +# class QScheduleViewSet(CustomModelViewSet): +# """ +# list:定时任务列表 + +# 定时任务列表 + +# retrieve:定时任务详情 + +# 定时任务详情 +# """ +# queryset = QSchedule.objects.all() +# serializer_class = QScheduleSerializer +# search_fields = ['name', 'func'] +# filterset_fields = ['schedule_type'] +# ordering = ['-pk'] + +# @action(methods=['get'], detail=True, perms_map={'post': 'qschedule:run_once'}) +# def run_once(self, request, pk=None): +# """同步执行一次 + +# 同步执行一次 +# """ +# obj = self.get_object() +# module, func = obj.func.rsplit(".", 1) +# m = importlib.import_module(module) +# f = getattr(m, func) +# f(*obj.args.split(','), **eval(f"dict({obj.kwargs})")) +# return Response() + + +# class QTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet): +# """ +# list:任务执行结果列表 + +# 任务执行结果列表 + +# retrieve:任务执行结果详情 + +# 任务执行结果详情 +# """ +# perms_map = {'get': '*'} +# filterset_fields = ['func'] +# queryset = QTask.objects.all() +# serializer_class = QTaskResultSerializer +# ordering = ['-started'] +# lookup_field = 'id' + class PTaskViewSet(CustomModelViewSet): """ list:定时任务列表 @@ -68,7 +117,25 @@ class PTaskViewSet(CustomModelViewSet): search_fields = ['name', 'task'] filterset_fields = ['enabled'] select_related_fields = ['interval', 'crontab'] - ordering = ['-create_time'] + ordering = ['-id'] + + @action(methods=['post'], detail=True, perms_map={'get': 'qtask:run_once'}, + serializer_class=TaskRunSerializer) + def run_once(self, request, pk=None): + """执行一次 + + 执行一次 + """ + obj = self.get_object() + module, func = obj.task.rsplit(".", 1) + m = importlib.import_module(module) + f = getattr(m, func) + if request.data.get('sync', True): + f(*json.loads(obj.args), **json.loads(obj.kwargs)) + return Response() + else: + task_obj = f.delay(*json.loads(obj.args), **json.loads(obj.kwargs)) + return Response({'task_id': task_obj.id}) @action(methods=['put'], detail=True, perms_map={'put': 'ptask:update'}) def toggle(self, request, pk=None): diff --git a/apps/third/urls.py b/apps/third/urls.py index a940aa11..62e7fc19 100755 --- a/apps/third/urls.py +++ b/apps/third/urls.py @@ -14,5 +14,5 @@ urlpatterns = [ path(API_BASE_URL, include(router.urls)), path(API_BASE_URL + 'dahua/test/', DahuaTestView.as_view()), path(API_BASE_URL + 'xunxi/test/', XxTestView.as_view()), - path(API_BASE_URL + 'speaker/test/', SpTestView.as_view()), + path(API_BASE_URL + 'speaker/test/', SpTestView.as_view()) ] diff --git a/apps/third/views.py b/apps/third/views.py index 53614837..47bf351b 100755 --- a/apps/third/views.py +++ b/apps/third/views.py @@ -82,13 +82,15 @@ class XxListener(stomp.ConnectionListener): class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet): - """ - 寻息通用调用接口 - """ perms_map = {'post': '*'} serializer_class = RequestCommonSerializer def create(self, request, *args, **kwargs): + """ + 寻息通用调用接口 + + 寻息通用调用接口 + """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) vdata = serializer.validated_data @@ -116,13 +118,15 @@ class XxCommonViewSet(CreateModelMixin, CustomGenericViewSet): class DhCommonViewSet(CreateModelMixin, CustomGenericViewSet): - """ - 大华通用调用接口 - """ perms_map = {'post': '*'} serializer_class = RequestCommonSerializer def create(self, request, *args, **kwargs): + """ + 大华通用调用接口 + + 大华通用调用接口 + """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) vdata = serializer.validated_data diff --git a/apps/third/views_d.py b/apps/third/views_d.py index b4ca55d9..f7ae1f20 100644 --- a/apps/third/views_d.py +++ b/apps/third/views_d.py @@ -3,24 +3,53 @@ from apps.third.serializers import BindAreaSerializer, LabelLocationSerializer from apps.utils.viewsets import CustomGenericViewSet from rest_framework.mixins import ListModelMixin from apps.third.clients import xxClient, dhClient, spClient -from apps.third.tapis import xxapis, dhapis +from apps.third.tapis import xxapis, dhapis, spapis from rest_framework.response import Response from rest_framework.serializers import Serializer from rest_framework.decorators import action from apps.am.models import Area +from rest_framework.views import APIView class TDeviceViewSet(CustomGenericViewSet): """ - 视频接口 + 三方设备接口 """ @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=Serializer) def vchannel(self, request): + """ + 视频通道列表 + + 视频通道列表 + """ request.data.update({'channelTypeList': ["1"]}) _, res = dhClient.request(**dhapis['channel_list'], json=request.data) return Response(res) + @action(methods=['post'], detail=False, perms_map={'post': '*'}, + serializer_class=Serializer) + def speaker(self, request): + """ + 喇叭列表 + + 喇叭列表 + """ + _, res = spClient.request(**spapis['device_list'], params=request.data) + return Response(res) + + @action(methods=['post'], detail=False, perms_map={'post': '*'}, + serializer_class=Serializer) + def dchannel(self, request): + """ + 闸机通道列表 + + 闸机通道列表 + """ + request.data.update({'channelTypeList': ["7"]}) + _, res = dhClient.request(**dhapis['channel_list'], json=request.data) + return Response(res) + @action(methods=['post'], detail=False, perms_map={'post': 'tdevice:label_location'}, serializer_class=LabelLocationSerializer) def label_location(self, request): diff --git a/apps/utils/speaker.py b/apps/utils/speaker.py index fde4d552..3703b559 100644 --- a/apps/utils/speaker.py +++ b/apps/utils/speaker.py @@ -92,8 +92,8 @@ class SpClient: if raise_exception: raise APIException(**SP_REQUEST_ERROR) return 'error', SP_REQUEST_ERROR - if settings.DEBUG: - print_roundtrip(r) + # if settings.DEBUG: + # print_roundtrip(r) if r.status_code == 200: ret = r.json() if 'code' in ret and ret['code'] not in ['0', '100', '00000', '1000', 0, 100, 1000]: diff --git a/apps/utils/task.py b/apps/utils/task.py new file mode 100644 index 00000000..0ba3fb80 --- /dev/null +++ b/apps/utils/task.py @@ -0,0 +1,11 @@ +from celery import Task +from server.settings import myLogger + + +class CustomTask(Task): + """ + 自定义的任务回调 + """ + def on_failure(self, exc, task_id, args, kwargs, einfo): + myLogger.error('{0!r} failed: {1!r}'.format(task_id, exc)) + return super().on_failure(exc, task_id, args, kwargs, einfo) diff --git a/apps/wf/migrations/0001_initial.py b/apps/wf/migrations/0001_initial.py new file mode 100644 index 00000000..62bab698 --- /dev/null +++ b/apps/wf/migrations/0001_initial.py @@ -0,0 +1,174 @@ +# Generated by Django 3.2.12 on 2022-05-10 02:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='State', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(max_length=50, verbose_name='名称')), + ('is_hidden', models.BooleanField(default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)', verbose_name='是否隐藏')), + ('sort', models.IntegerField(default=0, help_text='用于工单步骤接口时,step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前', verbose_name='状态顺序')), + ('type', models.IntegerField(choices=[(0, '普通'), (1, '开始'), (2, '结束')], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)', verbose_name='状态类型')), + ('enable_retreat', models.BooleanField(default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态', verbose_name='允许撤回')), + ('participant_type', models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型')), + ('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\\部门id\\角色id\\变量(create_by,create_by_tl)\\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot', verbose_name='参与者')), + ('state_fields', models.JSONField(default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选, 4:隐藏 示例:{"create_time":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='表单字段')), + ('distribute_type', models.IntegerField(choices=[(1, '主动接单'), (2, '直接处理'), (3, '随机分配'), (4, '全部处理')], default=1, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)', verbose_name='分配方式')), + ('filter_policy', models.IntegerField(choices=[(0, '无'), (1, '和工单同属一及上级部门'), (2, '和创建人同属一及上级部门'), (3, '和上步处理人同属一及上级部门')], default=0, verbose_name='参与人过滤策略')), + ('participant_cc', models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Ticket', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('title', models.CharField(blank=True, help_text='工单标题', max_length=500, null=True, verbose_name='标题')), + ('sn', models.CharField(help_text='工单的流水号', max_length=25, verbose_name='流水号')), + ('ticket_data', models.JSONField(default=dict, help_text='工单自定义字段内容', verbose_name='工单数据')), + ('in_add_node', models.BooleanField(default=False, help_text='是否处于加签状态下', verbose_name='加签状态中')), + ('script_run_last_result', models.BooleanField(default=True, verbose_name='脚本最后一次执行结果')), + ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型')), + ('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表', verbose_name='当前处理人')), + ('act_state', models.IntegerField(choices=[(0, '草稿中'), (1, '进行中'), (2, '被退回'), (3, '被撤回'), (4, '已完成'), (5, '已关闭')], default=1, help_text='当前工单的进行状态', verbose_name='进行状态')), + ('multi_all_person', models.JSONField(blank=True, default=dict, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式', verbose_name='全部处理的结果')), + ('add_node_man', models.ForeignKey(blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='加签人')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='wf.ticket', verbose_name='父工单')), + ('parent_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_parent_state', to='wf.state', verbose_name='父工单状态')), + ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_state', to='wf.state', verbose_name='当前状态')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Workflow', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(max_length=50, verbose_name='名称')), + ('key', models.CharField(blank=True, max_length=20, null=True, unique=True, verbose_name='工作流标识')), + ('sn_prefix', models.CharField(default='hb', max_length=50, verbose_name='流水号前缀')), + ('description', models.CharField(blank=True, max_length=200, null=True, verbose_name='描述')), + ('view_permission_check', models.BooleanField(default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单', verbose_name='查看权限校验')), + ('limit_expression', models.JSONField(blank=True, default=dict, 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的用户提交工单)', verbose_name='限制表达式')), + ('display_form_str', models.JSONField(blank=True, default=list, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='展现表单字段')), + ('title_template', models.CharField(blank=True, default='{title}', help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}', max_length=50, null=True, verbose_name='标题模板')), + ('content_template', models.CharField(blank=True, default='标题:{title}, 创建时间:{create_time}', help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}', max_length=1000, null=True, verbose_name='内容模板')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Transition', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('name', models.CharField(max_length=50, verbose_name='操作')), + ('timer', models.IntegerField(default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效', verbose_name='定时器(单位秒)')), + ('condition_expression', models.JSONField(default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突', max_length=1000, verbose_name='条件表达式')), + ('attribute_type', models.IntegerField(choices=[(1, '同意'), (2, '拒绝'), (3, '其他')], default=1, help_text='属性类型,1.同意,2.拒绝,3.其他', verbose_name='属性类型')), + ('field_require_check', models.BooleanField(default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容', verbose_name='是否校验必填项')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('destination_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dstate_transition', to='wf.state', verbose_name='目的状态')), + ('source_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sstate_transition', to='wf.state', verbose_name='源状态')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='TicketFlow', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('suggestion', models.CharField(blank=True, default='', max_length=10000, verbose_name='处理意见')), + ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人等', verbose_name='处理人类型')), + ('participant_str', models.CharField(blank=True, help_text='非人工处理的处理人相关信息', max_length=200, null=True, verbose_name='处理人')), + ('ticket_data', models.JSONField(blank=True, default=dict, help_text='可以用于记录当前表单数据,json格式', verbose_name='工单数据')), + ('intervene_type', models.IntegerField(choices=[(0, '正常处理'), (1, '转交'), (2, '加签'), (3, '加签处理完成'), (4, '接单'), (5, '评论'), (6, '删除'), (7, '强制关闭'), (8, '强制修改状态'), (9, 'hook操作'), (10, '撤回'), (11, '抄送')], default=0, help_text='流转类型', verbose_name='干预类型')), + ('participant_cc', models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给')), + ('participant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticketflow_participant', to=settings.AUTH_USER_MODEL, verbose_name='处理人')), + ('state', models.ForeignKey(blank=True, default=0, on_delete=django.db.models.deletion.CASCADE, to='wf.state', verbose_name='当前状态')), + ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticketflow_ticket', to='wf.ticket', verbose_name='关联工单')), + ('transition', models.ForeignKey(blank=True, help_text='与worklow.Transition关联, 为空时表示认为干预的操作', null=True, on_delete=django.db.models.deletion.CASCADE, to='wf.transition', verbose_name='流转id')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='ticket', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='关联工作流'), + ), + migrations.AddField( + model_name='state', + name='workflow', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流'), + ), + migrations.CreateModel( + name='CustomField', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('field_type', models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('cascader', '单选级联'), ('cascaders', '多选级联'), ('select_dg', '弹框单选'), ('select_dgs', '弹框多选'), ('textarea', '文本域'), ('file', '附件')], help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file', max_length=50, verbose_name='类型')), + ('field_key', models.CharField(help_text='字段类型请尽量特殊,避免与系统中关键字冲突', max_length=50, verbose_name='字段标识')), + ('field_name', models.CharField(max_length=50, verbose_name='字段名称')), + ('sort', models.IntegerField(default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列', verbose_name='排序')), + ('default_value', models.CharField(blank=True, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值', max_length=100, null=True, verbose_name='默认值')), + ('description', models.CharField(blank=True, help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述', max_length=100, null=True, verbose_name='描述')), + ('placeholder', models.CharField(blank=True, help_text='用户工单详情表单中作为字段的占位符显示', max_length=100, null=True, verbose_name='占位符')), + ('field_template', models.TextField(blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder', null=True, verbose_name='文本域模板')), + ('boolean_field_display', models.JSONField(blank=True, default=dict, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"是","0":"否"}或{"1":"需要","0":"不需要"},注意数字也需要引号', verbose_name='布尔类型显示名')), + ('field_choice', models.JSONField(blank=True, default=list, help_text='选项值,格式为list, 例["id":1, "name":"张三"]', verbose_name='选项值')), + ('label', models.CharField(default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项', max_length=1000, verbose_name='标签')), + ('is_hidden', models.BooleanField(default=False, help_text='可用于携带不需要用户查看的字段信息', verbose_name='是否隐藏')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/wf/migrations/__init__.py b/apps/wf/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/requirements.txt b/requirements.txt index 7b6cbc78..8f72ead3 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ celery==5.2.3 Django==3.2.12 django-celery-beat==2.2.1 -django-celery-results==2.3.0 +django-celery-results==2.3.1 django-cors-headers==3.11.0 django-filter==21.1 djangorestframework==3.13.1 diff --git a/server/celery.py b/server/celery.py index dbe6eb8b..16eaf672 100755 --- a/server/celery.py +++ b/server/celery.py @@ -19,4 +19,4 @@ app.autodiscover_tasks() @app.task(bind=True) def debug_task(self): - print(f'Request: {self.request!r}') \ No newline at end of file + print(f'Request: {self.request!r}') diff --git a/server/settings.py b/server/settings.py index 1838bf61..a5ef0b4f 100755 --- a/server/settings.py +++ b/server/settings.py @@ -45,7 +45,8 @@ INSTALLED_APPS = [ 'django_celery_results', 'drf_yasg', 'rest_framework', - "django_filters", + 'django_filters', + # 'django_q', 'apps.develop', 'apps.utils', 'apps.third', @@ -148,6 +149,15 @@ STATICFILES_DIRS = ( MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# 邮箱配置 +EMAIL_HOST = conf.EMAIL_HOST +EMAIL_PORT = conf.EMAIL_PORT +EMAIL_HOST_USER = conf.EMAIL_HOST_USER +EMAIL_HOST_PASSWORD = conf.EMAIL_HOST_PASSWORD +EMAIL_SUBJECT_PREFIX = conf.EMAIL_SUBJECT_PREFIX +EMAIL_USE_TLS = conf.EMAIL_USE_TLS + # 默认主键 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 雪花ID生成配置 @@ -183,7 +193,7 @@ REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'apps.utils.exceptions.custom_exception_hander', 'DEFAULT_THROTTLE_RATES': { 'anon': '1/second', - 'user': '1/second' + 'user': '2/second' } } # simplejwt配置 @@ -214,11 +224,34 @@ AUTHENTICATION_BACKENDS = ( # } # celery配置,celery正常运行必须安装redis -CELERY_BROKER_URL = "redis://redis:6379/0" # 任务存储 +CELERY_BROKER_URL = "redis://127.0.0.1:6379/1" # 任务存储 CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁,可防止内存泄露 CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区 CELERY_ENABLE_UTC = True # 启动时区设置 +CELERY_RESULT_BACKEND = 'django-db' +CELERY_CACHE_BACKEND = 'django-cache' CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' +CELERY_ACCEPT_CONTENT = ['application/json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TASK_TRACK_STARTED = True +CELERY_CACHE_BACKEND = 'default' +CELERYD_SOFT_TIME_LIMIT = 60*10 +CELERY_TASK_TRACK_STARTED = True + +# django_q配置 +# Q_CLUSTER = { +# 'name': 'ehs', +# # 'workers': 8, +# 'recycle': 500, +# 'timeout': 60, +# 'compress': True, +# 'save_limit': 250, +# 'queue_limit': 500, +# 'cpu_affinity': 1, +# 'label': 'Django Q', +# 'orm': 'default' +# } # swagger配置 SWAGGER_SETTINGS = {