初始化master

This commit is contained in:
caoqianming 2022-04-02 10:08:26 +08:00
commit e2a20fe67a
89 changed files with 4692 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
.vscode/
.vs/
.idea/
venv/
__pycache__/
*.pyc
media/*
dist/*
!media/default/
celerybeat.pid
celerybeat-schedule.bak
celerybeat-schedule.dat
celerybeat-schedule.dir
db.sqlite3
server/conf.py
sh/*

0
apps/auth1/__init__.py Normal file
View File

2
apps/auth1/admin.py Normal file
View File

@ -0,0 +1,2 @@
from django.contrib import admin
# Register your models here.

7
apps/auth1/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class AuthConfig(AppConfig):
name = 'apps.auth1'
verbose_name = "认证"

View File

@ -0,0 +1,23 @@
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from django.contrib.auth import get_user_model
UserModel = get_user_model()
class CustomBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
if username is None or password is None:
return
try:
user = UserModel._default_manager.get(
Q(username=username) | Q(phone=username) | Q(email=username))
except UserModel.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
UserModel().set_password(password)
else:
if user.check_password(password) and self.user_can_authenticate(user):
return user

View File

3
apps/auth1/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,5 @@
from rest_framework import serializers
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(label="用户名")
password = serializers.CharField(label="密码")

3
apps/auth1/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
apps/auth1/urls.py Normal file
View File

@ -0,0 +1,14 @@
from django.urls import path
from rest_framework_simplejwt.views import (TokenObtainPairView,
TokenRefreshView)
from apps.auth1.views import LoginView, LogoutView, TokenBlackView
API_BASE_URL = 'api/auth/'
urlpatterns = [
path(API_BASE_URL + 'token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path(API_BASE_URL + 'token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path(API_BASE_URL + 'token/black/', TokenBlackView.as_view(), name='token_black'),
path(API_BASE_URL + 'login/', LoginView.as_view(), name='session_login'),
path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout')
]

60
apps/auth1/views.py Normal file
View File

@ -0,0 +1,60 @@
from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate, login, logout
from rest_framework.generics import CreateAPIView
from rest_framework.permissions import IsAuthenticated
from apps.auth1.serializers import LoginSerializer
from apps.utils.response import FailResponse, SuccessResponse
# Create your views here.
class TokenBlackView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request, *args, **kwargs):
"""
Token拉黑
Token拉黑
"""
return Response(status=status.HTTP_200_OK)
class LoginView(CreateAPIView):
"""
Session登录
账户密码Session登录
"""
authentication_classes = []
permission_classes = []
serializer_class = LoginSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data
user = authenticate(username = vdata.get('username'),
password = vdata.get('password'))
if user is not None:
login(request, user)
return SuccessResponse()
return FailResponse(msg='账户或密码错误')
class LogoutView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
"""
退出登录
退出登录
"""
logout(request)
return SuccessResponse()

0
apps/monitor/__init__.py Normal file
View File

3
apps/monitor/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/monitor/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MonitorConfig(AppConfig):
name = 'apps.monitor'
verbose_name = '系统监控'

46
apps/monitor/consumers.py Normal file
View File

@ -0,0 +1,46 @@
import imp
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class MonitorConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))

View File

@ -0,0 +1 @@
from django.utils.deprecation import MiddlewareMixin

View File

3
apps/monitor/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

8
apps/monitor/routing.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from apps.monitor import consumers
WS_BASE_URL = 'ws/monitor/'
websocket_urlpatterns = [
path(WS_BASE_URL + '<str:room_name>/', consumers.MonitorConsumer.as_asgi())
]

View File

@ -0,0 +1,27 @@
<!-- chat/templates/chat/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Rooms</title>
</head>
<body>
What chat room would you like to enter?<br>
<input id="room-name-input" type="text" size="100"><br>
<input id="room-name-submit" type="button" value="Enter">
<script>
document.querySelector('#room-name-input').focus();
document.querySelector('#room-name-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#room-name-submit').click();
}
};
document.querySelector('#room-name-submit').onclick = function(e) {
var roomName = document.querySelector('#room-name-input').value;
window.location.pathname = '/monitor/' + roomName + '/';
};
</script>
</body>
</html>

View File

@ -0,0 +1,50 @@
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/monitor/'
+ roomName
+ '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
};
chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>videojs-contrib-hls embed</title>
<link href="https://unpkg.com/video.js/dist/video-js.css" rel="stylesheet">
<script src="https://unpkg.com/video.js/dist/video.js"></script>
<script src="https://unpkg.com/videojs-contrib-hls/dist/videojs-contrib-hls.js"></script>
</head>
<body>
<h1>Video.js Example Embed</h1>
<video id="my_video_1" class="video-js vjs-default-skin" controls preload="auto" width="640" height="268"
data-setup='{}'>
<source src="http://60.191.94.122:20046/live/cameraid/1001339%240/substream/1.m3u8" type="application/x-mpegURL">
</video>
<script>
</script>
</body>
</html>

3
apps/monitor/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
apps/monitor/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework import routers
from .views import ServerInfoView, LogView, LogDetailView, index, room, video
API_BASE_URL = 'api/monitor/'
HTML_BASE_URL = 'monitor/'
urlpatterns = [
path(HTML_BASE_URL, index),
path(HTML_BASE_URL + 'index/', index),
path(HTML_BASE_URL + 'video/', video),
path(HTML_BASE_URL + '<str:room_name>/', room, name='room'),
path(API_BASE_URL + 'log/', LogView.as_view()),
path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()),
path(API_BASE_URL + 'server/', ServerInfoView.as_view()),
]

120
apps/monitor/views.py Normal file
View File

@ -0,0 +1,120 @@
from django.shortcuts import render
import psutil
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ViewSet
from django.conf import settings
import os
from rest_framework import serializers, status
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from apps.utils.response import SuccessResponse
# Create your views here.
def index(request):
return render(request, 'monitor/index.html')
def room(request, room_name):
return render(request, 'monitor/room.html', {
'room_name': room_name
})
def video(request):
return render(request, 'monitor/video.html')
class ServerInfoView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
"""
获取服务器当前状态
cpu/内存/硬盘
"""
ret = {'cpu': {}, 'memory': {}, 'disk': {}}
ret['cpu']['count'] = psutil.cpu_count()
ret['cpu']['lcount'] = psutil.cpu_count(logical=False)
ret['cpu']['percent'] = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
ret['memory']['total'] = round(memory.total/1024/1024/1024, 2)
ret['memory']['used'] = round(memory.used/1024/1024/1024, 2)
ret['memory']['percent'] = memory.percent
disk = psutil.disk_usage('/')
ret['disk']['total'] = round(disk.total/1024/1024/1024, 2)
ret['disk']['used'] = round(disk.used/1024/1024/1024, 2)
ret['disk']['percent'] = disk.percent
return SuccessResponse(ret)
def get_file_list(file_path):
dir_list = os.listdir(file_path)
if not dir_list:
return
else:
# 注意这里使用lambda表达式将文件按照最后修改时间顺序升序排列
# os.path.getmtime() 函数是获取文件最后修改时间
# os.path.getctime() 函数是获取文件最后创建时间
dir_list = sorted(dir_list, key=lambda x: os.path.getmtime(
os.path.join(file_path, x)), reverse=True)
# print(dir_list)
return dir_list
class LogView(APIView):
@swagger_auto_schema(manual_parameters=[
openapi.Parameter('name', openapi.IN_QUERY,
description='日志文件名', type=openapi.TYPE_STRING)
])
def get(self, request, *args, **kwargs):
"""
查看最近的日志列表
查看最近的日志列表
"""
logs = []
name = request.GET.get('name', None)
# for root, dirs, files in os.walk(settings.LOG_PATH):
# files.reverse()
for file in get_file_list(settings.LOG_PATH):
if len(logs) > 50:
break
filepath = os.path.join(settings.LOG_PATH, file)
if name:
if name in filepath:
fsize = os.path.getsize(filepath)
if fsize:
logs.append({
"name": file,
"filepath": filepath,
"size": round(fsize/1000, 1)
})
else:
fsize = os.path.getsize(filepath)
if fsize:
logs.append({
"name": file,
"filepath": filepath,
"size": round(fsize/1000, 1)
})
return SuccessResponse(data=logs)
class LogDetailView(APIView):
def get(self, request, name):
"""
查看日志详情
查看日志详情
"""
try:
with open(os.path.join(settings.LOG_PATH, name)) as f:
data = f.read()
return Response(data)
except:
return Response('未找到', status=status.HTTP_404_NOT_FOUND)

1
apps/system/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'apps.system.apps.SystemConfig'

11
apps/system/admin.py Normal file
View File

@ -0,0 +1,11 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import User, Dept, Role, Permission, DictType, Dict, File
# Register your models here.
admin.site.register(User)
admin.site.register(Dept)
admin.site.register(Role)
admin.site.register(Permission)
admin.site.register(DictType)
admin.site.register(Dict, SimpleHistoryAdmin)
admin.site.register(File)

9
apps/system/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class SystemConfig(AppConfig):
name = 'apps.system'
verbose_name = '系统管理'
def ready(self):
import apps.system.signals

11
apps/system/filters.py Normal file
View File

@ -0,0 +1,11 @@
from django_filters import rest_framework as filters
from .models import User
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = {
'name': ['exact', 'contains'],
'is_active': ['exact'],
}

View File

@ -0,0 +1,263 @@
# Generated by Django 3.2.12 on 2022-04-02 01:21
import apps.utils.snowflake
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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(blank=True, max_length=20, null=True, verbose_name='姓名')),
('phone', models.CharField(blank=True, max_length=11, null=True, unique=True, verbose_name='手机号')),
('avatar', models.CharField(blank=True, default='/media/default/avatar.png', max_length=100, null=True, verbose_name='头像')),
],
options={
'verbose_name': '用户信息',
'verbose_name_plural': '用户信息',
'ordering': ['create_time'],
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Dept',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=60, verbose_name='名称')),
('type', models.PositiveSmallIntegerField(choices=[(10, '公司'), (20, '部门')], default=20, verbose_name='类型')),
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dept', verbose_name='')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '部门',
'verbose_name_plural': '部门',
'ordering': ['sort'],
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=30, verbose_name='名称')),
('type', models.PositiveSmallIntegerField(choices=[(10, '目录'), (20, '菜单'), (30, '按钮')], default=30, verbose_name='类型')),
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')),
('codes', models.JSONField(blank=True, default=list, null=True, verbose_name='权限标识')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.permission', verbose_name='')),
],
options={
'verbose_name': '功能权限表',
'verbose_name_plural': '功能权限表',
'ordering': ['sort'],
},
),
migrations.CreateModel(
name='Post',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=32, unique=True, verbose_name='名称')),
('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='岗位标识')),
('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')),
('data_range', models.PositiveSmallIntegerField(choices=[(10, '全部'), (30, '同级及以下'), (40, '本级及以下'), (50, '本级'), (60, '仅本人')], default=40, verbose_name='数据权限范围')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
],
options={
'verbose_name': '职位/岗位',
'verbose_name_plural': '职位/岗位',
'ordering': ['create_time'],
},
),
migrations.CreateModel(
name='UserPost',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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(blank=True, max_length=20, null=True, verbose_name='名称')),
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')),
('dept', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dept')),
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.post')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': '用户岗位关系表',
'verbose_name_plural': '用户岗位关系表',
'ordering': ['sort', 'create_time'],
},
),
migrations.CreateModel(
name='Role',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=32, unique=True, verbose_name='角色')),
('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='角色标识')),
('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('perms', models.ManyToManyField(blank=True, related_name='role_perms', to='system.Permission', verbose_name='功能权限')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '角色',
'verbose_name_plural': '角色',
'ordering': ['code'],
},
),
migrations.AddField(
model_name='post',
name='roles',
field=models.ManyToManyField(related_name='post_roles', to='system.Role', verbose_name='关联角色'),
),
migrations.AddField(
model_name='post',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.CreateModel(
name='File',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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(blank=True, max_length=100, null=True, verbose_name='名称')),
('size', models.IntegerField(blank=True, default=1, null=True, verbose_name='文件大小')),
('file', models.FileField(upload_to='%Y/%m/%d/', verbose_name='文件')),
('mime', models.CharField(blank=True, max_length=120, null=True, verbose_name='文件格式')),
('type', models.CharField(choices=[(10, '文档'), (20, '视频'), (30, '音频'), (40, '图片'), (50, '其它')], default='文档', max_length=50, verbose_name='文件类型')),
('path', models.CharField(blank=True, max_length=200, null=True, verbose_name='地址')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='file_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='file_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '文件库',
'verbose_name_plural': '文件库',
},
),
migrations.CreateModel(
name='DictType',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=30, verbose_name='名称')),
('code', models.CharField(max_length=30, unique=True, verbose_name='标识')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dicttype', verbose_name='')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '字典类型',
'verbose_name_plural': '字典类型',
'ordering': ['-create_time'],
},
),
migrations.AddField(
model_name='user',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AddField(
model_name='user',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AddField(
model_name='user',
name='depts',
field=models.ManyToManyField(through='system.UserPost', to='system.Dept'),
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
),
migrations.AddField(
model_name='user',
name='posts',
field=models.ManyToManyField(related_name='user_posts', through='system.UserPost', to='system.Post'),
),
migrations.AddField(
model_name='user',
name='superior',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='上级主管'),
),
migrations.AddField(
model_name='user',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.CreateModel(
name='Dict',
fields=[
('id', models.CharField(default=apps.utils.snowflake.IdWorker.get_id, 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=60, verbose_name='名称')),
('value', models.CharField(max_length=10, verbose_name='')),
('code', models.CharField(blank=True, max_length=30, null=True, verbose_name='标识')),
('description', models.TextField(blank=True, null=True, verbose_name='描述')),
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')),
('is_used', models.BooleanField(default=True, verbose_name='是否有效')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dict', verbose_name='')),
('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dicttype', verbose_name='类型')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '字典',
'verbose_name_plural': '字典',
'ordering': ['sort'],
'unique_together': {('name', 'is_used', 'type')},
},
),
]

View File

220
apps/system/models.py Normal file
View File

@ -0,0 +1,220 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from apps.utils.models import CommonAModel, CommonBModel, BaseModel
class Permission(BaseModel):
"""
功能权限:目录,菜单,按钮
"""
PERM_TYPE_LIST = 10
PERM_TYPE_MENU = 20
PERM_TYPE_BUTTON = 30
menu_type_choices = (
(PERM_TYPE_LIST, '目录'),
(PERM_TYPE_MENU, '菜单'),
(PERM_TYPE_BUTTON, '按钮')
)
name = models.CharField('名称', max_length=30)
type = models.PositiveSmallIntegerField('类型', choices=menu_type_choices, default=30)
sort = models.PositiveSmallIntegerField('排序标记', default=1)
parent = models.ForeignKey('self', null=True, blank=True,
on_delete=models.SET_NULL, verbose_name='')
codes = models.JSONField('权限标识', default=list, null=True, blank=True)
def __str__(self):
return self.name
class Meta:
verbose_name = '功能权限表'
verbose_name_plural = verbose_name
ordering = ['sort']
class Dept(CommonAModel):
"""
部门
"""
DEPT_TYPE_COMPANY = 10
DEPT_TYPE_DEPT = 20
dept_type_choices = (
(DEPT_TYPE_COMPANY, '公司'),
(DEPT_TYPE_DEPT, '部门')
)
name = models.CharField('名称', max_length=60)
type = models.PositiveSmallIntegerField('类型', choices=dept_type_choices, default=20)
parent = models.ForeignKey('self', null=True, blank=True,
on_delete=models.SET_NULL, verbose_name='')
sort = models.PositiveSmallIntegerField('排序标记', default=1)
class Meta:
verbose_name = '部门'
verbose_name_plural = verbose_name
ordering = ['sort']
def __str__(self):
return self.name
class Role(CommonAModel):
"""
角色
"""
name = models.CharField('角色', max_length=32, unique=True)
code = models.CharField('角色标识', max_length=32, unique=True, null=True, blank=True)
perms = models.ManyToManyField(Permission, blank=True, verbose_name='功能权限', related_name='role_perms')
description = models.CharField('描述', max_length=50, blank=True, null=True)
class Meta:
verbose_name = '角色'
verbose_name_plural = verbose_name
ordering = ['code']
def __str__(self):
return self.name
class Post(CommonAModel):
"""
职位/岗位
"""
name = models.CharField('名称', max_length=32, unique=True)
code = models.CharField('岗位标识', max_length=32, unique=True, null=True, blank=True)
description = models.CharField('描述', max_length=50, blank=True, null=True)
roles = models.ManyToManyField(Role, verbose_name='关联角色', related_name='post_roles')
POST_DATA_ALL = 10
POST_DATA_CUSTOM = 20
POST_DATA_SAMELEVE_AND_BELOW = 30
POST_DATA_THISLEVEL_AND_BELOW = 40
POST_DATA_THISLEVEL = 50
POST_DATA_MYSELF = 60
data_type_choices = (
(POST_DATA_ALL, '全部'),
(POST_DATA_SAMELEVE_AND_BELOW, '同级及以下'),
(POST_DATA_THISLEVEL_AND_BELOW, '本级及以下'),
(POST_DATA_THISLEVEL, '本级'),
(POST_DATA_MYSELF, '仅本人')
)
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=data_type_choices,
default=POST_DATA_THISLEVEL_AND_BELOW)
class Meta:
verbose_name = '职位/岗位'
verbose_name_plural = verbose_name
ordering = ['create_time']
def __str__(self):
return self.name
class User(AbstractUser, CommonBModel):
"""
用户
"""
name = models.CharField('姓名', max_length=20, null=True, blank=True)
phone = models.CharField('手机号', max_length=11,
null=True, blank=True, unique=True)
avatar = models.CharField(
'头像', default='/media/default/avatar.png', max_length=100, null=True, blank=True)
superior = models.ForeignKey(
'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管')
posts = models.ManyToManyField(Post, through='system.userpost', related_name='user_posts')
depts = models.ManyToManyField(Dept, through='system.userpost')
class Meta:
verbose_name = '用户信息'
verbose_name_plural = verbose_name
ordering = ['create_time']
def __str__(self):
return self.username
class UserPost(BaseModel):
"""
用户岗位关系表
"""
name = models.CharField('名称', max_length=20, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
post = models.ForeignKey(Post, on_delete=models.CASCADE)
dept = models.ForeignKey(Dept, on_delete=models.CASCADE)
sort = models.PositiveSmallIntegerField('排序', default=1)
class Meta:
verbose_name = '用户岗位关系表'
verbose_name_plural = verbose_name
ordering = ['sort', 'create_time']
class DictType(CommonAModel):
"""
数据字典类型
"""
name = models.CharField('名称', max_length=30)
code = models.CharField('标识', unique=True, max_length=30)
parent = models.ForeignKey('self', null=True, blank=True,
on_delete=models.SET_NULL, verbose_name='')
class Meta:
verbose_name = '字典类型'
verbose_name_plural = verbose_name
ordering = ['-create_time']
def __str__(self):
return self.name
class Dict(CommonAModel):
"""
数据字典
"""
name = models.CharField('名称', max_length=60)
value = models.CharField('', max_length=10)
code = models.CharField('标识', max_length=30, null=True, blank=True)
description = models.TextField('描述', blank=True, null=True)
type = models.ForeignKey(
DictType, on_delete=models.CASCADE, verbose_name='类型')
sort = models.PositiveSmallIntegerField('排序', default=1)
parent = models.ForeignKey('self', null=True, blank=True,
on_delete=models.SET_NULL, verbose_name='')
is_used = models.BooleanField('是否有效', default=True)
class Meta:
verbose_name = '字典'
verbose_name_plural = verbose_name
unique_together = ('name', 'is_used', 'type')
ordering = ['sort']
def __str__(self):
return self.name
class File(CommonAModel):
"""
文件存储表,业务表根据具体情况选择是否外键关联
"""
FILE_TYPE_DOC = 10
FILE_TYPE_VIDEO = 20
FILE_TYPE_AUDIO = 30
FILE_TYPE_PIC = 40
FILE_TYPE_OTHER = 50
name = models.CharField('名称', max_length=100, null=True, blank=True)
size = models.IntegerField('文件大小', default=1, null=True, blank=True)
file = models.FileField('文件', upload_to='%Y/%m/%d/')
type_choices = (
(FILE_TYPE_DOC, '文档'),
(FILE_TYPE_VIDEO, '视频'),
(FILE_TYPE_AUDIO, '音频'),
(FILE_TYPE_PIC, '图片'),
(FILE_TYPE_OTHER, '其它')
)
mime = models.CharField('文件格式', max_length=120, null=True, blank=True)
type = models.CharField('文件类型', max_length=50, choices=type_choices, default='文档')
path = models.CharField('地址', max_length=200, null=True, blank=True)
class Meta:
verbose_name = '文件库'
verbose_name_plural = verbose_name
def __str__(self):
return self.name

286
apps/system/serializers.py Normal file
View File

@ -0,0 +1,286 @@
import re
from django_celery_beat.models import PeriodicTask, CrontabSchedule, IntervalSchedule
from rest_framework import serializers
from django_celery_results.models import TaskResult
from apps.utils.serializers import CustomModelSerializer
from apps.utils.vars import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
from .models import (Dict, DictType, File, Dept, Permission, Post,
Role, User, UserPost)
class IntervalSerializer(CustomModelSerializer):
class Meta:
model = IntervalSchedule
fields = '__all__'
class CrontabSerializer(CustomModelSerializer):
class Meta:
model = CrontabSchedule
exclude = ['timezone']
class PTaskCreateUpdateSerializer(CustomModelSerializer):
class Meta:
model = PeriodicTask
fields = ['name', 'task', 'interval', 'crontab', 'args', 'kwargs']
class PTaskSerializer(CustomModelSerializer):
interval_ = IntervalSerializer(source='interval', read_only=True)
crontab_ = CrontabSerializer(source='crontab', read_only=True)
schedule = serializers.SerializerMethodField()
timetype = serializers.SerializerMethodField()
class Meta:
model = PeriodicTask
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.select_related('interval', 'crontab')
return queryset
def get_schedule(self, obj):
if obj.interval:
return obj.interval.__str__()
elif obj.crontab:
return obj.crontab.__str__()
return ''
def get_timetype(self, obj):
if obj.interval:
return 'interval'
elif obj.crontab:
return 'crontab'
return 'interval'
class FileSerializer(CustomModelSerializer):
class Meta:
model = File
fields = "__all__"
class DictTypeSerializer(CustomModelSerializer):
"""
数据字典类型序列化
"""
class Meta:
model = DictType
fields = '__all__'
class DictTypeCreateUpdateSerializer(CustomModelSerializer):
class Meta:
model = DictType
fields = ['name', 'code', 'parent']
class DictSerializer(CustomModelSerializer):
"""
数据字典序列化
"""
class Meta:
model = Dict
fields = '__all__'
class DictCreateUpdateSerializer(CustomModelSerializer):
"""
数据字典序列化
"""
class Meta:
model = Dict
exclude = EXCLUDE_FIELDS
class PostSerializer(CustomModelSerializer):
"""
岗位序列化
"""
class Meta:
model = Post
fields = '__all__'
class PostCreateUpdateSerializer(CustomModelSerializer):
"""
岗位序列化
"""
class Meta:
model = Post
exclude = EXCLUDE_FIELDS
class PostSimpleSerializer(CustomModelSerializer):
class Meta:
model = Post
fields = ['id', 'name', 'code']
class RoleSerializer(CustomModelSerializer):
"""
角色序列化
"""
class Meta:
model = Role
fields = '__all__'
class RoleSimpleSerializer(CustomModelSerializer):
class Meta:
model = Role
fields = ['id', 'name', 'code']
class RoleCreateUpdateSerializer(CustomModelSerializer):
"""
角色序列化
"""
class Meta:
model = Role
exclude = EXCLUDE_FIELDS
class PermissionSerializer(CustomModelSerializer):
"""
权限序列化
"""
class Meta:
model = Permission
fields = '__all__'
class PermissionCreateUpdateSerializer(CustomModelSerializer):
"""
权限序列化
"""
class Meta:
model = Permission
exclude = EXCLUDE_FIELDS_BASE
class DeptSimpleSerializer(CustomModelSerializer):
class Meta:
model = Dept
fields = ['id', 'name', 'type']
class DeptSerializer(CustomModelSerializer):
"""
组织架构序列化
"""
type = serializers.ChoiceField(
choices=Dept.dept_type_choices, default='部门')
class Meta:
model = Dept
fields = '__all__'
class DeptCreateUpdateSerializer(CustomModelSerializer):
"""
部门序列化
"""
class Meta:
model = Dept
exclude = EXCLUDE_FIELDS
class UserSimpleSerializer(CustomModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'name']
class UserListSerializer(CustomModelSerializer):
"""
用户列表序列化
"""
belong_dept_ = DeptSimpleSerializer(source='belong_dept', read_only=True)
posts_ = PostSimpleSerializer(source='posts', many=True)
class Meta:
model = User
exclude = ['password']
@staticmethod
def setup_eager_loading(queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.select_related('superior', 'belong_dept')
queryset = queryset.prefetch_related('posts')
return queryset
class UserUpdateSerializer(CustomModelSerializer):
"""
用户编辑序列化
"""
phone = serializers.CharField(max_length=11, required=False)
class Meta:
model = User
fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept',
'avatar', 'is_active', 'is_superuser']
def validate_phone(self, phone):
re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
if not re.match(re_phone, phone):
raise serializers.ValidationError('手机号码不合法')
return phone
class UserCreateSerializer(CustomModelSerializer):
"""
创建用户序列化
"""
username = serializers.CharField(required=True)
phone = serializers.CharField(max_length=11, required=False)
class Meta:
model = User
fields = ['id', 'username', 'name', 'phone', 'email', 'belong_dept',
'avatar', 'is_active']
def validate_username(self, username):
if User.objects.filter(username=username):
raise serializers.ValidationError(username + ' 账号已存在')
return username
def validate_phone(self, phone):
re_phone = '^1[358]\d{9}$|^147\d{8}$|^176\d{8}$'
if not re.match(re_phone, phone):
raise serializers.ValidationError('手机号码不合法')
if User.objects.filter(phone=phone):
raise serializers.ValidationError('手机号已经被注册')
return phone
class PTaskResultSerializer(CustomModelSerializer):
class Meta:
model = TaskResult
fields = '__all__'
class UserPostSerializer(CustomModelSerializer):
"""
用户-岗位序列化
"""
user_ = UserSimpleSerializer(source='user', read_only=True)
post_ = PostSimpleSerializer(source='post', read_only=True)
dept_ = DeptSimpleSerializer(source='dept', read_only=True)
class Meta:
model = UserPost
fields = '__all__'
class UserInfoSerializer(CustomModelSerializer):
posts_ = UserPostSerializer(source='post', read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'name', 'posts_', 'avatar']

12
apps/system/signals.py Normal file
View File

@ -0,0 +1,12 @@
from django.db.models.signals import m2m_changed
from .models import Role, Permission, User
from django.dispatch import receiver
from django.core.cache import cache
from apps.utils.permission import get_user_perms_map
# 变更用户角色时动态更新权限或者前端刷新
# @receiver(m2m_changed, sender=User.roles.through)
# def update_perms_cache_user(sender, instance, action, **kwargs):
# if action in ['post_remove', 'post_add']:
# if cache.get('perms_' + instance.id, None):
# get_user_perms_map(instance)

9
apps/system/tasks.py Normal file
View File

@ -0,0 +1,9 @@
# Create your tasks here
from __future__ import absolute_import, unicode_literals
from celery import shared_task
@shared_task
def show():
print('ok')

3
apps/system/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

27
apps/system/urls.py Normal file
View File

@ -0,0 +1,27 @@
from django.urls import path, include
from .views import FileViewSet, PTaskResultViewSet, TaskList, UserPostViewSet, UserViewSet, DeptViewSet, PermissionViewSet, RoleViewSet, PostViewSet, DictTypeViewSet, DictViewSet, PTaskViewSet
from rest_framework import routers
API_BASE_URL = 'api/system/'
HTML_BASE_URL = 'system/'
router = routers.DefaultRouter()
router.register('user', UserViewSet, basename="user")
router.register('dept', DeptViewSet, basename="dept")
router.register('permission', PermissionViewSet, basename="permission")
router.register('role', RoleViewSet, basename="role")
router.register('post', PostViewSet, basename="post")
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('user_post', UserPostViewSet, basename='user_post')
router2 = routers.DefaultRouter()
router2.register('file', FileViewSet, basename='file')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
path(API_BASE_URL + 'task/', TaskList.as_view()),
path('api/', include(router2.urls)),
]

390
apps/system/views.py Normal file
View File

@ -0,0 +1,390 @@
import logging
from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from django.db import transaction
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
PeriodicTask)
from django_celery_results.models import TaskResult
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
ListModelMixin, RetrieveModelMixin)
from rest_framework.parsers import (JSONParser,
MultiPartParser)
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.utils.mixins import (CustomCreateModelMixin)
from apps.utils.permission import get_user_perms_map
from apps.utils.queryset import get_child_queryset2
from apps.utils.response import FailResponse, SuccessResponse
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from server.celery import app as celery_app
from .filters import UserFilter
from .models import (Dept, Dict, DictType, File, Permission, Post, Role, User,
UserPost)
from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer, DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer,
FileSerializer, PermissionCreateUpdateSerializer, PermissionSerializer, PostCreateUpdateSerializer, PostSerializer,
PTaskCreateUpdateSerializer, PTaskResultSerializer,
PTaskSerializer, RoleCreateUpdateSerializer, RoleSerializer,
UserCreateSerializer, UserListSerializer,
UserPostSerializer, UserUpdateSerializer)
logger = logging.getLogger('log')
# logger.info('请求成功! response_code:{}response_headers:{}
# response_body:{}'.format(response_code, response_headers, response_body[:251]))
# logger.error('请求出错-{}'.format(error))
class TaskList(APIView):
permission_classes = [IsAuthenticated]
def get(self, requests):
"""获取注册任务列表
获取注册任务列表
"""
tasks = list(
sorted(name for name in celery_app.tasks if not name.startswith('celery.')))
return Response(tasks)
class PTaskViewSet(CustomModelViewSet):
"""
list:定时任务列表
定时任务列表
retrieve:定时任务详情
定时任务详情
"""
queryset = PeriodicTask.objects.exclude(name__contains='celery.')
serializer_class = PTaskSerializer
create_serializer_class = PTaskCreateUpdateSerializer
update_serializer_class = PTaskCreateUpdateSerializer
search_fields = ['name', 'task']
filterset_fields = ['enabled']
ordering = ['-create_time']
@action(methods=['put'], detail=True, perms_map={'put': 'ptask_update'})
def toggle(self, request, pk=None):
"""修改启用禁用状态
修改启用禁用状态
"""
obj = self.get_object()
obj.enabled = False if obj.enabled else True
obj.save()
return SuccessResponse()
@transaction.atomic
def create(self, request, *args, **kwargs):
"""创建定时任务
创建定时任务
"""
data = request.data
timetype = data.get('timetype', None)
interval_ = data.get('interval_', None)
crontab_ = data.get('crontab_', None)
if timetype == 'interval' and interval_:
data['crontab'] = None
try:
interval, _ = IntervalSchedule.objects.get_or_create(
**interval_, defaults=interval_)
data['interval'] = interval.id
except:
raise ValidationError('时间策略有误')
if timetype == 'crontab' and crontab_:
data['interval'] = None
try:
crontab_['timezone'] = 'Asia/Shanghai'
crontab, _ = CrontabSchedule.objects.get_or_create(
**crontab_, defaults=crontab_)
data['crontab'] = crontab.id
except:
raise ValidationError('时间策略有误')
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return SuccessResponse()
@transaction.atomic
def update(self, request, *args, **kwargs):
"""更新定时任务
更新定时任务
"""
data = request.data
timetype = data.get('timetype', None)
interval_ = data.get('interval_', None)
crontab_ = data.get('crontab_', None)
if timetype == 'interval' and interval_:
data['crontab'] = None
try:
if 'id' in interval_:
del interval_['id']
interval, _ = IntervalSchedule.objects.get_or_create(
**interval_, defaults=interval_)
data['interval'] = interval.id
except:
raise ValidationError('时间策略有误')
if timetype == 'crontab' and crontab_:
data['interval'] = None
try:
crontab_['timezone'] = 'Asia/Shanghai'
if 'id' in crontab_:
del crontab_['id']
crontab, _ = CrontabSchedule.objects.get_or_create(
**crontab_, defaults=crontab_)
data['crontab'] = crontab.id
except:
raise ValidationError('时间策略有误')
instance = self.get_object()
serializer = self.get_serializer(instance, data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return SuccessResponse()
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
"""
list:任务执行结果列表
任务执行结果列表
retrieve:任务执行结果详情
任务执行结果详情
"""
perms_map = {'get': '*'}
filterset_fields = ['task_name']
queryset = TaskResult.objects.all()
serializer_class = PTaskResultSerializer
ordering = ['-date_created']
class DictTypeViewSet(CustomModelViewSet):
"""数据字典类型-增删改查
数据字典类型-增删改查
"""
queryset = DictType.objects.all()
serializer_class = DictTypeSerializer
create_serializer_class = DictTypeCreateUpdateSerializer
update_serializer_class = DictTypeCreateUpdateSerializer
search_fields = ['name']
class DictViewSet(CustomModelViewSet):
"""数据字典-增删改查
数据字典-增删改查
"""
# queryset = Dict.objects.get_queryset(all=True) # 获取全部的,包括软删除的
queryset = Dict.objects.all()
filterset_fields = ['type', 'is_used', 'type__code']
serializer_class = DictSerializer
create_serializer_class = DictCreateUpdateSerializer
update_serializer_class = DictCreateUpdateSerializer
search_fields = ['name']
class PostViewSet(CustomModelViewSet):
"""岗位-增删改查
岗位-增删改查
"""
queryset = Post.objects.all()
serializer_class = PostSerializer
create_serializer_class = PostCreateUpdateSerializer
update_serializer_class = PostCreateUpdateSerializer
search_fields = ['name', 'description']
class PermissionViewSet(CustomModelViewSet):
"""菜单权限-增删改查
菜单权限-增删改查
"""
queryset = Permission.objects.all()
filterset_fields = ['type']
serializer_class = PermissionSerializer
create_serializer_class = PermissionCreateUpdateSerializer
update_serializer_class = PermissionCreateUpdateSerializer
search_fields = ['name', 'code']
class DeptViewSet(CustomModelViewSet):
"""部门-增删改查
部门-增删改查
"""
queryset = Dept.objects.all()
serializer_class = DeptSerializer
create_serializer_class = DeptCreateUpdateSerializer
update_serializer_class = DeptCreateUpdateSerializer
filterset_fields = ['type']
search_fields = ['name']
class RoleViewSet(CustomModelViewSet):
"""角色-增删改查
角色-增删改查
"""
queryset = Role.objects.all()
serializer_class = RoleSerializer
create_serializer_class = RoleCreateUpdateSerializer
update_serializer_class = RoleCreateUpdateSerializer
search_fields = ['name', 'code']
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
"""用户/岗位关系
用户/岗位关系
"""
perms_map = {'get': '*', 'post': 'user_update', 'delete': 'user_update'}
queryset = UserPost.objects.select_related('user', 'post', 'dept').all()
serializer_class = UserPostSerializer
filterset_fields = ['user', 'post', 'dept']
def perform_create(self, serializer):
instance = serializer.save()
user = instance.user
adept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
if adept:
user.belong_dept = adept
user.update_by = self.request.user
user.save()
def perform_destroy(self, instance):
user = instance.user
instance.delete()
fdept = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
if fdept:
user.belong_dept = fdept
else:
user.belong_dept = None
user.update_by = self.request.user
user.save()
class UserViewSet(CustomModelViewSet):
queryset = User.objects.all()
serializer_class = UserListSerializer
create_serializer_class = UserCreateSerializer
update_serializer_class = UserUpdateSerializer
filterset_class = UserFilter
search_fields = ['username', 'name', 'phone', 'email']
def get_queryset(self):
queryset = self.queryset
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
dept = self.request.query_params.get(
'belong_dept', None) # 该部门及其子部门所有员工
if dept:
dept_queryset = get_child_queryset2(
Dept.objects.get(pk=dept))
queryset = queryset.filter(dept__in=dept_queryset)
return queryset
def create(self, request, *args, **kwargs):
"""创建用户
创建用户
"""
password = request.data.get('password', None)
if password:
password = make_password(password)
else:
password = make_password('0000')
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(password=password)
return SuccessResponse(data=serializer.data)
@action(methods=['put'], detail=False, permission_classes=[IsAuthenticated])
def password(self, request, pk=None):
"""修改密码
修改密码
"""
user = request.user
old_password = request.data['old_password']
if check_password(old_password, user.password):
new_password1 = request.data['new_password1']
new_password2 = request.data['new_password2']
if new_password1 == new_password2:
user.set_password(new_password2)
user.save()
return SuccessResponse()
else:
return FailResponse(msg='新密码两次输入不一致!')
else:
return FailResponse(msg='旧密码错误!')
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
def info(self, request, pk=None):
"""登录用户信息
获取登录用户信息
"""
user = request.user
perms = get_user_perms_map(user)
data = {
'id': user.id,
'username': user.username,
'name': user.name,
'posts': user.posts.values_list('name', flat=True),
'avatar': user.avatar,
'perms': perms,
}
return SuccessResponse(data)
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
"""文件上传
list:
文件列表
文件列表
create:
文件上传
文件上传
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, JSONParser]
queryset = File.objects.all()
serializer_class = FileSerializer
filterset_fields = ['type']
search_fields = ['name']
def perform_create(self, serializer):
file_obj = self.request.data.get('file')
name = file_obj._name
size = file_obj.size
mime = file_obj.content_type
file_type = '其它'
if 'image' in mime:
file_type = '图片'
elif 'video' in mime:
file_type = '视频'
elif 'audio' in mime:
file_type = '音频'
elif 'application' or 'text' in mime:
file_type = '文档'
instance = serializer.save(
create_by=self.request.user, name=name, size=size, type=file_type, mime=mime)
instance.path = settings.MEDIA_URL + instance.file.name
instance.save()

0
apps/third/__init__.py Normal file
View File

3
apps/third/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
apps/third/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ThirdConfig(AppConfig):
name = 'apps.third'

View File

3
apps/third/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

14
apps/third/serializers.py Normal file
View File

@ -0,0 +1,14 @@
from rest_framework import serializers
class RequestCommonSerializer(serializers.Serializer):
method_choice = (
('post','post'),
('get','get'),
('put','put'),
('delete','delete')
)
url = serializers.CharField(label='请求地址')
method = serializers.ChoiceField(label='请求方法', choices=method_choice)
params = serializers.JSONField(label='请求参数', required=False, allow_null=True)
json = serializers.JSONField(label='请求body', required=False, allow_null=True)

3
apps/third/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
apps/third/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework import routers
from apps.third.views import DahuaTestView, DhCommonViewSet, XxCommonViewSet, XxTestView
API_BASE_URL = 'api/third/'
HTML_BASE_URL = 'third/'
router = routers.DefaultRouter()
router.register('xunxi', XxCommonViewSet, basename='api_xunxi')
router.register('dahua', DhCommonViewSet, basename='api_dahua')
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()),
]

95
apps/third/views.py Normal file
View File

@ -0,0 +1,95 @@
from django.shortcuts import render
from apps.utils.dahua import dhClient
from apps.utils.xunxi import xxClient
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from apps.utils.viewsets import CustomGenericViewSet
from rest_framework.mixins import CreateModelMixin
from apps.third.serializers import RequestCommonSerializer
# Create your views here.
class DahuaTestView(APIView):
"""
大华测试接口
"""
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
data = {
"data":{
"channelId": "1001339$1$0$0",
"streamType": "1",
"type": "hls"
}
}
res = dhClient.request(
url='/evo-apigw/admin/API/video/stream/realtime', method='post', json=data)
# data = {
# "pageNum":1,
# "pageSize":100,
# "isOnline":1,
# "showChildNodeData":1,
# "categorys":[8]
# }
# res = dhClient.request(
# url='/evo-apigw/evo-brm/1.0.0/device/subsystem/page', method='post', json=data)
# data = {
# "channelCodeList": ["1001382$7$0$0"]
# }
# res = dhClient.request(
# url='/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/channelControl/closeDoor', method='post', json=data)
return Response(res)
class XxTestView(APIView):
"""
寻息测试接口
"""
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
res = xxClient.request(
url='/api/application/build/buildListV2', json={})
return Response(res)
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
res = xxClient.request(
url=vdata['url'],
method=vdata.get('method', 'post'),
params=vdata.get('params', {}),
json=vdata.get('data', {}))
return Response(res)
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
res = dhClient.request(
url=vdata['url'],
method=vdata.get('method', 'post'),
params=vdata.get('params', {}),
json=vdata.get('data', {}))
return Response(res)

0
apps/utils/__init__.py Normal file
View File

3
apps/utils/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
apps/utils/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class UtilsConfig(AppConfig):
name = 'apps.utils'

99
apps/utils/dahua.py Normal file
View File

@ -0,0 +1,99 @@
from threading import Thread
import traceback
import requests
from apps.utils.tools import print_roundtrip
from server import settings
import json
import time
requests.packages.urllib3.disable_warnings()
class DhClient:
"""
大华
"""
def __init__(self, client_id= settings.DAHUA_CLIENTID
, client_secret = settings.DAHUA_SECRET) -> None:
self.client_id = client_id
self.client_secret = client_secret
self.headers = {}
self.isGetingToken = False
self.isRuning = True
self.t = None # 线程
self.setup()
def _get_token_loop(self):
while self.isRuning:
params = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret
}
r = requests.post(params=params, url=settings.DAHUA_BASE_URL + '/evo-apigw/evo-oauth/oauth/token', verify=False)
ret = r.json()
if ret['success']:
self.headers['Authorization'] = 'bearer ' + ret['data']['access_token']
self.headers['User-Id'] = '1'
time.sleep(3600)
def get_token(self):
self.isGetingToken = True
params = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret
}
r = requests.post(params=params, url=settings.DAHUA_BASE_URL + '/evo-apigw/evo-oauth/oauth/token', verify=False)
ret = r.json()
if ret['success']:
self.headers['Authorization'] = 'bearer ' + ret['data']['access_token']
self.headers['User-Id'] = '1'
self.isGetingToken = False
def setup(self):
t = Thread(target=self._get_token_loop, args=(), daemon=True)
t.start()
def __del__(self):
"""
自定义销毁
"""
self.isRuning = False
self.t.join()
def request(self, url:str, method:str, params=dict(), json=dict(), timeout=20):
if self.isGetingToken:
req_num = 0
while True:
time.sleep(0.5)
if not self.isGetingToken:
self.request(url, method, params, json, timeout)
req_num = req_num + 1
if req_num > 4:
break
else:
r = getattr(requests, method)('{}{}'.format(settings.DAHUA_BASE_URL, url)
, headers = self.headers, params=params, json=json, verify=False)
if settings.DEBUG:
print_roundtrip(r)
if r.status_code == 200:
"""
请求成功
"""
ret = r.json()
if ret.get('code') == '27001007':
self.get_token() # 重新获取token
self.request(url, method, params, json, timeout) # 重新请求
else:
msg = '{}|{}{}'.format(str(ret['code']), ret.get('errMsg',''), ret.get('desc', ''))
res = dict(success=True, code=200000, msg= msg, data=ret.get('data', None))
if ret['code'] not in ['0', '100', '00000']:
res['success'] = False
res['code'] = 400000
return res
return dict(success=False, code=400901, msg='大华接口访问异常', data=None)
if settings.DAHUA_ENABLED:
dhClient = DhClient()

0
apps/utils/filters.py Normal file
View File

65
apps/utils/mixins.py Normal file
View File

@ -0,0 +1,65 @@
from django.db.models.query import QuerySet
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, ListModelMixin, RetrieveModelMixin, DestroyModelMixin
class CreateUpdateModelAMixin:
"""
业务用基本表A用
"""
def perform_create(self, serializer):
serializer.save(create_by = self.request.user)
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
class CreateUpdateModelBMixin:
"""
业务用基本表B用
"""
def perform_create(self, serializer):
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
class CreateUpdateCustomMixin:
"""
整合
"""
def perform_create(self, serializer):
if hasattr(self.queryset.model, 'belong_dept'):
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
else:
serializer.save(create_by = self.request.user)
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
class OptimizationMixin:
"""
性能优化,需要在序列化器里定义setup_eager_loading,可在必要的View下继承
"""
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
return queryset
class CustomCreateModelMixin(CreateModelMixin):
def perform_create(self, serializer):
if hasattr(self.queryset.model, 'belong_dept'):
serializer.save(create_by = self.request.user, belong_dept=self.request.user.dept)
else:
serializer.save(create_by = self.request.user)
class CustomUpdateModelMixin(UpdateModelMixin):
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
class CustomDestoryModelMixin(DestroyModelMixin):
def perform_destroy(self, instance):
instance.delete(update_by = self.request.user)

162
apps/utils/models.py Normal file
View File

@ -0,0 +1,162 @@
from django.db import models
import django.utils.timezone as timezone
from django.db.models.query import QuerySet
from apps.utils.snowflake import worker
# 自定义软删除查询基类
class SoftDeletableQuerySetMixin(object):
'''
QuerySet for SoftDeletableModel. Instead of removing instance sets
its ``is_deleted`` field to True.
'''
def delete(self, soft=True, update_by=None):
'''
Soft delete objects from queryset (set their ``is_deleted``
field to True)
'''
if soft:
self.update(is_deleted=True, update_by=update_by)
else:
return super(SoftDeletableQuerySetMixin, self).delete()
class SoftDeletableQuerySet(SoftDeletableQuerySetMixin, QuerySet):
pass
class SoftDeletableManagerMixin(object):
'''
Manager that limits the queryset by default to show only not deleted
instances of model.
'''
_queryset_class = SoftDeletableQuerySet
def get_queryset(self, all=False):
'''
Return queryset limited to not deleted entries.
'''
kwargs = {'model': self.model, 'using': self._db}
if hasattr(self, '_hints'):
kwargs['hints'] = self._hints
if all:
return self._queryset_class(**kwargs)
return self._queryset_class(**kwargs).filter(is_deleted=False)
class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager):
pass
class BaseModel(models.Model):
"""
基本表
"""
id = models.CharField(max_length=20, primary_key=True, default=worker.get_id, editable=False, verbose_name='主键ID', help_text='主键ID')
create_time = models.DateTimeField(
default=timezone.now, verbose_name='创建时间', help_text='创建时间')
update_time = models.DateTimeField(
auto_now=True, verbose_name='修改时间', help_text='修改时间')
is_deleted = models.BooleanField(
default=False, verbose_name='删除标记', help_text='删除标记')
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.pk:
# If self.pk is not None then it's an update.
cls = self.__class__
old = cls.objects.filter(pk=self.pk).first()
if old:
# This will get the current model state since super().save() isn't called yet.
new = self # This gets the newly instantiated Mode object with the new values.
changed_fields = []
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
changed_fields.append(field_name)
except Exception as ex: # Catch field does not exist exception
pass
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
class SoftModel(BaseModel):
"""
软删除基本表
"""
class Meta:
abstract = True
objects = SoftDeletableManager()
def delete(self, using=None, soft=True, update_by=None, *args, **kwargs):
'''
这里需要真删除的话soft=False即可
'''
if soft:
self.is_deleted = True
self.update_by = update_by
self.save(using=using)
else:
return super(SoftModel, self).delete(using=using, *args, **kwargs)
class CommonAModel(SoftModel):
"""
业务用基本表A,包含create_by, update_by字段
"""
create_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by')
update_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by')
class Meta:
abstract = True
class CommonBModel(SoftModel):
"""
业务用基本表B,包含create_by, update_by, belong_dept字段
"""
create_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by')
update_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by')
belong_dept = models.ForeignKey(
'system.dept', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept')
class Meta:
abstract = True
class CommonADModel(BaseModel):
"""
业务用基本表A, 物理删除, 包含create_by, update_by字段
"""
create_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name= '%(class)s_create_by')
update_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name= '%(class)s_update_by')
class Meta:
abstract = True
class CommonBDModel(BaseModel):
"""
业务用基本表B, 物理删除, 包含create_by, update_by, belong_dept字段
"""
create_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='创建人', related_name = '%(class)s_create_by')
update_by = models.ForeignKey(
'system.user', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='最后编辑人', related_name = '%(class)s_update_by')
belong_dept = models.ForeignKey(
'system.organzation', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='所属部门', related_name= '%(class)s_belong_dept')
class Meta:
abstract = True

14
apps/utils/pagination.py Normal file
View File

@ -0,0 +1,14 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import ParseError
class MyPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
def paginate_queryset(self, queryset, request, view):
if request.query_params.get('pageoff', None) or request.query_params.get('page', None)=='0':
if queryset.count()<500:
return None
elif queryset.count()>=500:
raise ParseError('单次请求数据量大,请分页获取')
return super().paginate_queryset(queryset, request, view=view)

128
apps/utils/permission.py Normal file
View File

@ -0,0 +1,128 @@
from django.core.cache import cache
from rest_framework.permissions import BasePermission, DjangoModelPermissions
from apps.utils.queryset import get_child_queryset2
from apps.system.models import Dept, Permission, Post, Role, UserPost
from django.db.models import Q
from django.db.models.query import QuerySet
def get_user_perms_map(user):
"""
获取权限列表,可用redis存取
"""
user_perms_map = {}
if user.is_superuser:
user_perms_map = {'superuser':None}
else:
objs = UserPost.objects.filter(user=user)
for i in objs:
dept_id = str(i.dept.id)
if i.post.roles: # 岗位下有角色
for perm in Permission.objects.filter(role__perms__in=i.post.roles):
if perm.codes:
for code in perm.codes:
if code in user_perms_map:
data_range = user_perms_map[code].get(dept_id, -1)
if i.post.data_range < data_range:
user_perms_map[code][dept_id] = data_range
else:
user_perms_map[code] = {dept_id:i.post.data_range}
cache.set('perms_' + user.id, user_perms_map, 60*60)
return user_perms_map
class RbacPermission(BasePermission):
"""
基于角色的权限校验类
"""
def has_permission(self, request, view):
"""
权限校验逻辑
:param request:
:param view:
:return:
"""
if not hasattr(view, 'perms_map'):
return False
user_perms_map = cache.get('perms_' + request.user.id, None)
if user_perms_map is None:
user_perms_map = get_user_perms_map(self.request.user)
if isinstance(user_perms_map, dict):
if 'superuser' in user_perms_map:
return True
else:
perms_map = view.perms_map
_method = request._request.method.lower()
if perms_map:
for key in perms_map:
if key == _method or key == '*':
if perms_map[key] in user_perms_map or perms_map[key] == '*':
return True
return False
return False
class RbacDataMixin:
"""
数据权限控权返回的queryset
在必须的View下继承
需要控数据权限的表需有belong_dept, create_by, update_by字段(部门, 创建人, 编辑人)
带性能优化
此处对性能有较大影响,根据业务需求进行修改或取舍
"""
def get_queryset(self):
assert self.queryset is not None, (
"'%s' should either include a `queryset` attribute, "
"or override the `get_queryset()` method."
% self.__class__.__name__
)
queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
queryset = self.get_serializer_class().setup_eager_loading(queryset) # 性能优化
if self.request.user.is_superuser:
return queryset
if hasattr(queryset.model, 'belong_dept'):
user = self.request.user
user_perms_map = cache.get('perms_' + user.id, None)
if user_perms_map is None:
user_perms_map = get_user_perms_map(self.request.user)
if isinstance(user_perms_map, dict):
if hasattr(self.view, 'perms_map'):
perms_map = self.view.perms_map
action_str = perms_map.get(self.request._request.method.lower(), None)
if '*' in perms_map:
return queryset
elif action_str == '*':
return queryset
elif action_str in user_perms_map:
new_queryset = queryset.none()
for dept_id, data_range in user_perms_map[action_str].items:
dept = Dept.objects.get(id=dept_id)
if data_range == Post.POST_DATA_ALL:
return queryset
elif data_range == Post.POST_DATA_SAMELEVE_AND_BELOW:
if dept.parent:
belong_depts = get_child_queryset2(dept.parent)
else:
belong_depts = get_child_queryset2(dept)
queryset = queryset.filter(belong_dept__in = belong_depts)
elif data_range == Post.POST_DATA_THISLEVEL_AND_BELOW:
belong_depts = get_child_queryset2(dept)
queryset = queryset.filter(belong_dept__in = belong_depts)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(belong_dept = dept)
elif data_range == Post.POST_DATA_THISLEVEL:
queryset = queryset.filter(create_by = user)
new_queryset = new_queryset | queryset
return new_queryset
else:
return queryset.none()
return queryset

70
apps/utils/queryset.py Normal file
View File

@ -0,0 +1,70 @@
from django.db import models
from django.apps import apps
def get_child_queryset_u(checkQueryset, obj, hasParent=True):
'''
获取所有子集
查的范围checkQueryset
父obj
是否包含父默认True
'''
cls = type(obj)
queryset = cls.objects.none()
fatherQueryset = cls.objects.filter(pk=obj.id)
if hasParent:
queryset = queryset | fatherQueryset
child_queryset = checkQueryset.filter(parent=obj)
while child_queryset:
queryset = queryset | child_queryset
child_queryset = checkQueryset.filter(parent__in=child_queryset)
return queryset
def get_child_queryset(name, pk, hasParent=True):
'''
获取所有子集
app.model名称
Id
是否包含父默认True
'''
app, model = name.split('.')
cls = apps.get_model(app, model)
queryset = cls.objects.none()
fatherQueryset = cls.objects.filter(pk=pk)
if fatherQueryset.exists():
if hasParent:
queryset = queryset | fatherQueryset
child_queryset = cls.objects.filter(parent=fatherQueryset.first())
while child_queryset:
queryset = queryset | child_queryset
child_queryset = cls.objects.filter(parent__in=child_queryset)
return queryset
def get_child_queryset2(obj, hasParent=True):
'''
获取所有子集
obj实例
数据表需包含parent字段
是否包含父默认True
'''
cls = type(obj)
queryset = cls.objects.none()
fatherQueryset = cls.objects.filter(pk=obj.id)
if hasParent:
queryset = queryset | fatherQueryset
child_queryset = cls.objects.filter(parent=obj)
while child_queryset:
queryset = queryset | child_queryset
child_queryset = cls.objects.filter(parent__in=child_queryset)
return queryset
def get_parent_queryset(obj, hasSelf=True):
cls = type(obj)
ids = []
if hasSelf:
ids.append(obj.id)
while obj.parent:
obj = obj.parent
ids.append(obj.id)
return cls.objects.filter(id__in=ids)

104
apps/utils/request.py Normal file
View File

@ -0,0 +1,104 @@
import json
from user_agents import parse
def get_request_ip(request):
"""
获取请求IP
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[-1].strip()
return ip
ip = request.META.get('REMOTE_ADDR', '') or getattr(request, 'request_ip', None)
return ip or 'unknown'
def get_request_data(request):
"""
获取请求参数
"""
request_data = getattr(request, 'request_data', None)
if request_data:
return request_data
data: dict = {**request.GET.dict(), **request.POST.dict()}
if not data:
try:
body = request.body
if body:
data = json.loads(body)
except Exception as e:
pass
if not isinstance(data, dict):
data = {'data': data}
return data
def get_request_path(request, *args, **kwargs):
"""
获取请求路径
"""
request_path = getattr(request, 'request_path', None)
if request_path:
return request_path
values = []
for arg in args:
if len(arg) == 0:
continue
if isinstance(arg, str):
values.append(arg)
elif isinstance(arg, (tuple, set, list)):
values.extend(arg)
elif isinstance(arg, dict):
values.extend(arg.values())
if len(values) == 0:
return request.path
path: str = request.path
for value in values:
path = path.replace('/' + value, '/' + '{id}')
return path
def get_browser(request, ):
"""
获取浏览器名
:param request:
:param args:
:param kwargs:
:return:
"""
ua_string = request.META['HTTP_USER_AGENT']
user_agent = parse(ua_string)
return user_agent.get_browser()
def get_os(request, ):
"""
获取操作系统
:param request:
:param args:
:param kwargs:
:return:
"""
ua_string = request.META['HTTP_USER_AGENT']
user_agent = parse(ua_string)
return user_agent.get_os()
def get_verbose_name(queryset=None, view=None, model=None):
"""
获取 verbose_name
:param request:
:param view:
:return:
"""
try:
if queryset and hasattr(queryset, 'model'):
model = queryset.model
elif view and hasattr(view.get_queryset(), 'model'):
model = view.get_queryset().model
elif view and hasattr(view.get_serializer(), 'Meta') and hasattr(view.get_serializer().Meta, 'model'):
model = view.get_serializer().Meta.model
if model:
return getattr(model, '_meta').verbose_name
else:
model = queryset.model._meta.verbose_name
except Exception as e:
pass
return model if model else ""

83
apps/utils/response.py Normal file
View File

@ -0,0 +1,83 @@
import traceback
from rest_framework.renderers import JSONRenderer
from rest_framework.views import exception_handler
from rest_framework.response import Response
from rest_framework import status as drf_status
import logging
logger = logging.getLogger('log')
def custome_exception_hander(exc, context):
"""
自定义异常处理
"""
res = exception_handler(exc, context)
if res:
return res
else:
"""
日志记录
"""
logger.error(traceback.format_exc())
return None
class FitJSONRenderer(JSONRenderer):
"""
自行封装的渲染器
"""
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
如果使用这个render
普通的response将会被包装成
{"code":200000,"data":"X","msg":"X"}
这样的结果
使用方法
- 全局
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': ('utils.response.FitJSONRenderer', ),
}
- 局部
class UserCountView(APIView):
renderer_classes = [FitJSONRenderer]
:param data:
:param accepted_media_type:
:param renderer_context:
:return: {"code":200,"data":"X","msg":"X"}
"""
response_body = dict(success=True, code=200000, msg='请求成功', data=None)
response = renderer_context.get("response")
status_code = response.status_code
if isinstance(data, dict) and data.keys() == response_body.keys():
response_body = data
else:
# 处理drf格式的返回数据
if status_code >= 400: # 如果http响应异常
response_body = dict(success=False, code=400000, msg='请求失败', data=None)
response_body['data'] = data # data里是详细异常信息
response_body['code'] = status_code*1000
prefix = ""
if isinstance(data, dict):
prefix = list(data.keys())[0]
data = data[prefix]
if isinstance(data, list):
data = data[0]
if prefix != 'detail':
response_body['msg'] = prefix + str(data) # 取一部分放入msg,方便前端alert
else:
response_body['data'] = data
renderer_context.get("response").status_code = 200 # 统一成200响应, 可用body里code区分业务异常
return super(FitJSONRenderer, self).render(response_body, accepted_media_type, renderer_context)
class SuccessResponse(Response):
def __init__(self, data=None, msg='请求成功', code=200000, status=None, template_name=None, headers=None, exception=False, content_type=None):
std_data = dict(success=True, code=code, msg=msg, data=data)
super().__init__(std_data, status, template_name, headers, exception, content_type)
class FailResponse(Response):
def __init__(self, msg='请求失败', data=None, code=400000, status=400, template_name=None, headers=None, exception=False, content_type=None):
std_data = dict(success=False, code=code, msg=msg, data=data)
super().__init__(std_data, status, template_name, headers, exception, content_type)

13
apps/utils/serializers.py Normal file
View File

@ -0,0 +1,13 @@
from rest_framework import serializers
from django_restql.mixins import DynamicFieldsMixin
class PkSerializer(serializers.Serializer):
pks = serializers.ListField(child=serializers.IntegerField(min_value=1), label="主键ID列表")
class GenSignatureSerializer(serializers.Serializer):
path = serializers.CharField(label="图片地址")
class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
pass

107
apps/utils/snowflake.py Normal file
View File

@ -0,0 +1,107 @@
# Twitter's Snowflake algorithm implementation which is used to generate distributed IDs.
# https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala
import time
import logging
from django.conf import settings
# logger = logging.getLogger('log')
class InvalidSystemClock(Exception):
"""
时钟回拨异常
"""
pass
class Constant(object):
# 64位ID的划分
WORKER_ID_BITS = 5
DATACENTER_ID_BITS = 5
SEQUENCE_BITS = 12
# 最大取值计算
MAX_WORKER_ID = -1 ^ (-1 << WORKER_ID_BITS) # 2**5-1 0b11111
MAX_DATACENTER_ID = -1 ^ (-1 << DATACENTER_ID_BITS)
# 移位偏移计算
WOKER_ID_SHIFT = SEQUENCE_BITS
DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS
TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS
# 序号循环掩码
SEQUENCE_MASK = -1 ^ (-1 << SEQUENCE_BITS)
# Twitter元年时间戳
TWEPOCH = 1288834974657
class IdWorker(object):
"""
用于生成IDs
"""
def __init__(self, datacenter_id, worker_id, sequence=0):
"""
初始化
:param datacenter_id: 数据中心机器区域ID
:param worker_id: 机器ID
:param sequence: 起始序号
"""
# sanity check
if worker_id > Constant.MAX_WORKER_ID or worker_id < 0:
raise ValueError('worker_id值越界')
if datacenter_id > Constant.MAX_DATACENTER_ID or datacenter_id < 0:
raise ValueError('datacenter_id值越界')
self.worker_id = worker_id
self.datacenter_id = datacenter_id
self.sequence = sequence
self.last_timestamp = -1 # 上次计算的时间戳
def _gen_timestamp(self):
"""
生成整数时间戳
:return:int timestamp
"""
return int(time.time() * 1000)
def get_id(self):
"""
获取新ID
:return:
"""
timestamp = self._gen_timestamp()
# 时钟回拨
if timestamp < self.last_timestamp:
# logger.error('clock is moving backwards. Rejecting requests until {}'.format(self.last_timestamp))
raise InvalidSystemClock
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & Constant.SEQUENCE_MASK
if self.sequence == 0:
timestamp = self._til_next_millis(self.last_timestamp)
else:
self.sequence = 0
self.last_timestamp = timestamp
new_id = ((timestamp - Constant.TWEPOCH) << Constant.TIMESTAMP_LEFT_SHIFT) | (self.datacenter_id << Constant.DATACENTER_ID_SHIFT) | \
(self.worker_id << Constant.WOKER_ID_SHIFT) | self.sequence
return new_id
def _til_next_millis(self, last_timestamp):
"""
等到下一毫秒
"""
timestamp = self._gen_timestamp()
while timestamp <= last_timestamp:
timestamp = self._gen_timestamp()
return timestamp
worker = IdWorker(settings.SNOW_DATACENTER_ID, settings.SNOW_WORKER_ID)
if __name__ == '__main__':
print(worker.get_id())

22
apps/utils/tools.py Normal file
View File

@ -0,0 +1,22 @@
import textwrap
import requests
def print_roundtrip(response, *args, **kwargs):
format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
print(textwrap.dedent('''
---------------- request ----------------
{req.method} {req.url}
{reqhdrs}
{req.body}
---------------- response ----------------
{res.status_code} {res.reason} {res.url}
{reshdrs}
{res.text}
''').format(
req=response.request,
res=response,
reqhdrs=format_headers(response.request.headers),
reshdrs=format_headers(response.headers),
))

11
apps/utils/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework import routers
from apps.utils.views import SignatureViewSet
API_BASE_URL = 'api/utils/'
router = routers.DefaultRouter()
router.register('signature', SignatureViewSet, basename='signature')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

4
apps/utils/vars.py Normal file
View File

@ -0,0 +1,4 @@
EXCLUDE_FIELDS_BASE = ['create_time', 'update_time', 'is_deleted']
EXCLUDE_FIELDS = ['create_time', 'update_time', 'is_deleted', 'create_by', 'update_by']

47
apps/utils/views.py Normal file
View File

@ -0,0 +1,47 @@
from rest_framework.views import APIView
import os
import cv2
from server.settings import BASE_DIR
import numpy as np
from .response import FailResponse, SuccessResponse
from apps.utils.viewsets import CustomGenericViewSet
from apps.utils.mixins import CustomCreateModelMixin
from apps.utils.serializers import GenSignatureSerializer
class SignatureViewSet(CustomCreateModelMixin, CustomGenericViewSet):
authentication_classes = ()
permission_classes = ()
create_serializer_class = GenSignatureSerializer
def create(self, request, *args, **kwargs):
"""
照片生成透明签名图片
照片生成透明签名图片
"""
path = (BASE_DIR + request.data['path']).replace('\\', '/')
try:
image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
size = image.shape
width = size[0] # 宽度
height = size[1] # 高度
if size[2] != 4: # 判断
background = np.zeros((size[0], size[1], 4))
for yh in range(height):
for xw in range(width):
background[xw, yh, :3] = image[xw, yh]
background[xw, yh, 3] = 255
image = background
size = image.shape
for i in range(size[0]):
for j in range(size[1]):
if image[i][j][0] > 100 and image[i][j][1] > 100 and image[i][j][2] > 100:
image[i][j][3] = 0
else:
image[i][j][0], image[i][j][1], image[i][j][2] = 0, 0, 0
ext = os.path.splitext(path)
new_path = ext[0] + '.png'
cv2.imwrite(new_path, image)
return SuccessResponse({'path': new_path.replace(BASE_DIR, '')})
except:
return FailResponse(msg='签名照处理失败,请重新上传')

71
apps/utils/viewsets.py Normal file
View File

@ -0,0 +1,71 @@
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.decorators import action
from apps.utils.mixins import CustomCreateModelMixin, CustomDestoryModelMixin, CustomUpdateModelMixin, OptimizationMixin
from apps.utils.permission import RbacDataMixin, RbacPermission
from apps.utils.serializers import PkSerializer
from apps.utils.response import FailResponse, SuccessResponse
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, ListModelMixin
from rest_framework.permissions import IsAuthenticated
class CustomGenericViewSet(GenericViewSet):
"""
增强的GenericViewSet
"""
perms_map = {}
ordering_fields = '__all__'
filter_fields = '__all__'
ordering = '-create_time'
filterset_fields = '__all__'
create_serializer_class = None
update_serializer_class = None
list_serializer_class = None
retrieve_serializer_class = None
permission_classes = [IsAuthenticated & RbacPermission]
def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None)
if action_serializer_class:
return action_serializer_class
return super().get_serializer_class()
class CustomDataGenericViewSet(RbacDataMixin, CustomGenericViewSet):
"""
增强的GenericViewSet, 带数据权限过滤
"""
class CustomModelViewSet(OptimizationMixin, CustomCreateModelMixin
, CustomUpdateModelMixin, ListModelMixin, RetrieveModelMixin
, CustomDestoryModelMixin, CustomGenericViewSet):
"""
增强的ModelViewSet
"""
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
# 增加默认权限标识
if not self.perms_map:
basename = self.basename
self.perms_map = {'get':'*', 'post':'{}_create'.format(basename)
,'put':'{}_update'.format(basename)
,'patch':'{}_update'.format(basename)
,'delete':'{}_delete'.format(basename)
,'deletes':'{}_delete'.format(basename)}
@action(methods=['post'], detail=False, serializer_class=PkSerializer)
def deletes(self,request,*args,**kwargs):
request_data = request.data
pks = request_data.get('pks',None)
if pks:
self.get_queryset().filter(id__in=pks).delete(update_by=request.user)
return SuccessResponse()
else:
return FailResponse(msg="未获取到pks字段")
class CustomDataModelViewSet(RbacDataMixin, CustomModelViewSet):
"""
增强的ModelViewSet,带数据权限过滤
"""

88
apps/utils/xunxi.py Normal file
View File

@ -0,0 +1,88 @@
from threading import Thread
import requests
import json
from apps.utils.tools import print_roundtrip
from server import settings
import time
requests.packages.urllib3.disable_warnings()
class XxClient:
"""
寻息
"""
def __init__(self, licence=settings.XX_LICENCE, username=settings.XX_USERNAME) -> None:
self.licence = licence
self.username = username
self.isGetingToken = False
self.isRuning = True
self.token = ''
self.t = None
self.setup()
def _get_token_loop(self):
while self.isRuning:
json = {
'licence':self.licence
}
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
ret = r.json()
if ret.get('errorCode', 1) == 0:
self.token = ret['data']['token']
time.sleep(3600)
def get_token(self):
self.isGetingToken = True
json = {
'licence':self.licence
}
r = requests.post(json=json, url=settings.XX_BASE_URL + '/getAccessTokenV2', verify=False, timeout=20)
ret = r.json()
if ret.get('errorCode', 1) == 0:
self.isGetingToken = False
self.token = ret['data']['token']
def setup(self):
self.t = Thread(target= self._get_token_loop, args=(), daemon=True)
self.t.start()
def __del__(self):
"""
自定义销毁
"""
self.isRuning = False
self.t.join()
def request(self, url:str, method:str='post', params=dict(), json=dict(), timeout=20):
params['accessToken'] = self.token
json['username'] = self.username
if self.isGetingToken:
req_num = 0
while True:
time.sleep(0.5)
if not self.isGetingToken:
self.request(url, method, params, json, timeout)
req_num = req_num + 1
if req_num > 4:
break
else:
r = getattr(requests, method)('{}{}'.format(settings.XX_BASE_URL, url)
, params=params, json=json, verify=False)
if settings.DEBUG:
print_roundtrip(r)
ret = r.json()
if ret.get('errorCode') == '1060000':
self.get_token() # 重新获取token
self.request(url, method, params, json, timeout) # 重新请求
else:
msg = '{}|{}'.format(str(ret['errorCode']), '|'.join(ret['errorMsg']))
res = dict(success=True, code=200000, msg= msg, data=ret['data'])
if ret['errorCode'] != 0:
res['success'] = False
res['code'] = 400000
return res
return dict(success=False, code=400900, msg='寻息接口访问异常', data=None)
if settings.XX_ENABLED:
xxClient = XxClient()

0
apps/wf/__init__.py Normal file
View File

3
apps/wf/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

7
apps/wf/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class WfConfig(AppConfig):
name = 'apps.wf'
verbose_name = '工作流管理'

26
apps/wf/filters.py Normal file
View File

@ -0,0 +1,26 @@
from django_filters import rest_framework as filters
from .models import Ticket
class TicketFilterSet(filters.FilterSet):
start_create = filters.DateFilter(field_name="create_time", lookup_expr='gte')
end_create = filters.DateFilter(field_name="create_time", lookup_expr='lte')
category = filters.ChoiceFilter(choices = Ticket.category_choices, method='filter_category')
class Meta:
model = Ticket
fields = ['workflow', 'state', 'act_state', 'start_create', 'end_create', 'category']
def filter_category(self, queryset, name, value):
user=self.request.user
if value == 'owner': # 我的
queryset = queryset.filter(create_by=user)
elif value == 'duty': # 待办
queryset = queryset.filter(participant__contains=user.id).exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED])
elif value == 'worked': # 处理过的
queryset = queryset.filter(ticketflow_ticket__participant=user).exclude(create_by=user).order_by('-update_time').distinct()
elif value == 'cc': # 抄送我的
queryset = queryset.filter(ticketflow_ticket__participant_cc__contains=user.id).exclude(create_by=user).order_by('-update_time').distinct()
elif value == 'all':
pass
else:
queryset = queryset.none()
return queryset

237
apps/wf/models.py Normal file
View File

@ -0,0 +1,237 @@
from random import choice
from django.db import models
from django.db.models.base import Model
import django.utils.timezone as timezone
from django.db.models.query import QuerySet
from apps.utils.models import CommonAModel
from apps.system.models import Dept, User, Dict, File
from apps.utils.models import SoftModel, BaseModel
from simple_history.models import HistoricalRecords
class Workflow(CommonAModel):
"""
工作流
"""
name = models.CharField('名称', max_length=50)
key = models.CharField('工作流标识', unique=True, max_length=20, null=True, blank=True)
sn_prefix = models.CharField('流水号前缀', max_length=50, default='hb')
description = models.CharField('描述', max_length=200, null=True, blank=True)
view_permission_check = models.BooleanField('查看权限校验', default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单')
limit_expression = models.JSONField('限制表达式', 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, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称)state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称')
title_template = models.CharField('标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
content_template = models.CharField('内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
class State(CommonAModel):
"""
状态记录
"""
STATE_TYPE_START = 1
STATE_TYPE_END = 2
type_choices = (
(0, '普通'),
(STATE_TYPE_START, '开始'),
(STATE_TYPE_END, '结束')
)
PARTICIPANT_TYPE_PERSONAL = 1
PARTICIPANT_TYPE_MULTI = 2
PARTICIPANT_TYPE_DEPT = 3
PARTICIPANT_TYPE_ROLE = 4
PARTICIPANT_TYPE_VARIABLE = 5
PARTICIPANT_TYPE_ROBOT = 6
PARTICIPANT_TYPE_FIELD = 7
PARTICIPANT_TYPE_PARENT_FIELD = 8
PARTICIPANT_TYPE_FORMCODE = 9
state_participanttype_choices = (
(0, '无处理人'),
(PARTICIPANT_TYPE_PERSONAL, '个人'),
(PARTICIPANT_TYPE_MULTI, '多人'),
# (PARTICIPANT_TYPE_DEPT, '部门'),
(PARTICIPANT_TYPE_ROLE, '角色'),
# (PARTICIPANT_TYPE_VARIABLE, '变量'),
(PARTICIPANT_TYPE_ROBOT, '脚本'),
(PARTICIPANT_TYPE_FIELD, '工单的字段'),
# (PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段'),
(PARTICIPANT_TYPE_FORMCODE, '代码获取')
)
STATE_DISTRIBUTE_TYPE_ACTIVE = 1 # 主动接单
STATE_DISTRIBUTE_TYPE_DIRECT = 2 # 直接处理(当前为多人的情况,都可以处理,而不需要先接单)
STATE_DISTRIBUTE_TYPE_RANDOM = 3 # 随机分配
STATE_DISTRIBUTE_TYPE_ALL = 4 # 全部处理
state_distribute_choices=(
(STATE_DISTRIBUTE_TYPE_ACTIVE, '主动接单'),
(STATE_DISTRIBUTE_TYPE_DIRECT, '直接处理'),
(STATE_DISTRIBUTE_TYPE_RANDOM, '随机分配'),
(STATE_DISTRIBUTE_TYPE_ALL, '全部处理'),
)
STATE_FIELD_READONLY= 1 # 字段只读
STATE_FIELD_REQUIRED = 2 # 字段必填
STATE_FIELD_OPTIONAL = 3 # 字段可选
STATE_FIELD_HIDDEN = 4 # 字段隐藏
state_filter_choices=(
(0, ''),
(1, '和工单同属一及上级部门'),
(2, '和创建人同属一及上级部门'),
(3, '和上步处理人同属一及上级部门'),
)
name = models.CharField('名称', max_length=50)
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)')
sort = models.IntegerField('状态顺序', default=0, help_text='用于工单步骤接口时step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前')
type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理即没有对应的transition)')
enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态')
participant_type = models.IntegerField('参与者类型', choices=state_participanttype_choices, default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5参与人填create_by')
participant = models.JSONField('参与者', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\部门id\角色id\变量(create_by,create_by_tl)\脚本记录的id等包含子工作流的需要设置处理人为loonrobot')
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:工作流名称') # json格式存储,包括读写属性1只读2必填3可选4不显示, 字典的字典
distribute_type = models.IntegerField('分配方式', default=1, choices=state_distribute_choices, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
filter_policy = models.IntegerField('参与人过滤策略', default=0, choices=state_filter_choices)
participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
class Transition(CommonAModel):
"""
工作流流转定时器条件(允许跳过) 条件流转与定时器不可同时存在
"""
TRANSITION_ATTRIBUTE_TYPE_ACCEPT = 1 # 同意
TRANSITION_ATTRIBUTE_TYPE_REFUSE = 2 # 拒绝
TRANSITION_ATTRIBUTE_TYPE_OTHER = 3 # 其他
attribute_type_choices = (
(1, '同意'),
(2, '拒绝'),
(3, '其他')
)
TRANSITION_INTERVENE_TYPE_DELIVER = 1 # 转交操作
TRANSITION_INTERVENE_TYPE_ADD_NODE = 2 # 加签操作
TRANSITION_INTERVENE_TYPE_ADD_NODE_END = 3 # 加签处理完成
TRANSITION_INTERVENE_TYPE_ACCEPT = 4 # 接单操作
TRANSITION_INTERVENE_TYPE_COMMENT = 5 # 评论操作
TRANSITION_INTERVENE_TYPE_DELETE = 6 # 删除操作
TRANSITION_INTERVENE_TYPE_CLOSE = 7 # 强制关闭操作
TRANSITION_INTERVENE_TYPE_ALTER_STATE = 8 # 强制修改状态操作
TRANSITION_INTERVENE_TYPE_HOOK = 9 # hook操作
TRANSITION_INTERVENE_TYPE_RETREAT = 10 # 撤回
TRANSITION_INTERVENE_TYPE_CC = 11 # 抄送
intervene_type_choices = (
(0, '正常处理'),
(TRANSITION_INTERVENE_TYPE_DELIVER, '转交'),
(TRANSITION_INTERVENE_TYPE_ADD_NODE, '加签'),
(TRANSITION_INTERVENE_TYPE_ADD_NODE_END, '加签处理完成'),
(TRANSITION_INTERVENE_TYPE_ACCEPT, '接单'),
(TRANSITION_INTERVENE_TYPE_COMMENT, '评论'),
(TRANSITION_INTERVENE_TYPE_DELETE, '删除'),
(TRANSITION_INTERVENE_TYPE_CLOSE, '强制关闭'),
(TRANSITION_INTERVENE_TYPE_ALTER_STATE, '强制修改状态'),
(TRANSITION_INTERVENE_TYPE_HOOK, 'hook操作'),
(TRANSITION_INTERVENE_TYPE_RETREAT, '撤回'),
(TRANSITION_INTERVENE_TYPE_CC, '抄送')
)
name = models.CharField('操作', max_length=50)
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效')
source_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='源状态', related_name='sstate_transition')
destination_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='目的状态', related_name='dstate_transition')
condition_expression = models.JSONField('条件表达式', max_length=1000, default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准所以多个条件不要有冲突' )
attribute_type = models.IntegerField('属性类型', default=1, choices=attribute_type_choices, help_text='属性类型1.同意2.拒绝3.其他')
field_require_check = models.BooleanField('是否校验必填项', default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容')
class CustomField(CommonAModel):
"""自定义字段, 设定某个工作流有哪些自定义字段"""
field_type_choices = (
('string', '字符串'),
('int', '整型'),
('float', '浮点'),
('boolean', '布尔'),
('date', '日期'),
('datetime', '日期时间'),
('radio', '单选'),
('checkbox', '多选'),
('select', '单选下拉'),
('selects', '多选下拉'),
('cascader', '单选级联'),
('cascaders', '多选级联'),
('select_dg', '弹框单选'),
('select_dgs', '弹框多选'),
('textarea', '文本域'),
('file', '附件')
)
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
field_type = models.CharField('类型', max_length=50, choices=field_type_choices,
help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file')
field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突')
field_name = models.CharField('字段名称', max_length=50)
sort = models.IntegerField('排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列')
default_value = models.CharField('默认值', null=True, blank=True, max_length=100, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值')
description = models.CharField('描述', max_length=100, blank=True, null=True, help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述')
placeholder = models.CharField('占位符', max_length=100, blank=True, null=True, help_text='用户工单详情表单中作为字段的占位符显示')
field_template = models.TextField('文本域模板', null=True, blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder')
boolean_field_display = models.JSONField('布尔类型显示名', default=dict, blank=True,
help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号')
field_choice = models.JSONField('选项值', default=list, blank=True,
help_text='选项值格式为list, 例["id":1, "name":"张三"]')
label = models.CharField('标签', max_length=1000, default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项')
# hook = models.CharField('hook', max_length=1000, default='', help_text='获取下拉选项用于动态选项值')
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='可用于携带不需要用户查看的字段信息')
class Ticket(CommonAModel):
"""
工单
"""
TICKET_ACT_STATE_DRAFT = 0 # 草稿中
TICKET_ACT_STATE_ONGOING = 1 # 进行中
TICKET_ACT_STATE_BACK = 2 # 被退回
TICKET_ACT_STATE_RETREAT = 3 # 被撤回
TICKET_ACT_STATE_FINISH = 4 # 已完成
TICKET_ACT_STATE_CLOSED = 5 # 已关闭
act_state_choices =(
(TICKET_ACT_STATE_DRAFT, '草稿中'),
(TICKET_ACT_STATE_ONGOING, '进行中'),
(TICKET_ACT_STATE_BACK, '被退回'),
(TICKET_ACT_STATE_RETREAT, '被撤回'),
(TICKET_ACT_STATE_FINISH, '已完成'),
(TICKET_ACT_STATE_CLOSED, '已关闭')
)
category_choices =(
('all', '全部'),
('owner', '我创建的'),
('duty', '待办'),
('worked', '我处理的'),
('cc', '抄送我的')
)
title = models.CharField('标题', max_length=500, null=True, blank=True, help_text="工单标题")
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流')
sn = models.CharField('流水号', max_length=25, help_text="工单的流水号")
state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='当前状态', related_name='ticket_state')
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单')
parent_state = models.ForeignKey(State, null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单状态', related_name='ticket_parent_state')
ticket_data = models.JSONField('工单数据', default=dict, help_text='工单自定义字段内容')
in_add_node = models.BooleanField('加签状态中', default=False, help_text='是否处于加签状态下')
add_node_man = models.ForeignKey(User, verbose_name='加签人', on_delete=models.SET_NULL, null=True, blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效')
script_run_last_result = models.BooleanField('脚本最后一次执行结果', default=True)
participant_type = models.IntegerField('当前处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.state_participanttype_choices)
participant = models.JSONField('当前处理人', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表')
act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices)
multi_all_person = models.JSONField('全部处理的结果', default=dict, blank=True, help_text='需要当前状态处理人全部处理时实际的处理结果json格式')
class TicketFlow(BaseModel):
"""
工单流转日志
"""
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='关联工单', related_name='ticketflow_ticket')
transition = models.ForeignKey(Transition, verbose_name='流转id', help_text='与worklow.Transition关联 为空时表示认为干预的操作', on_delete=models.CASCADE, null=True, blank=True)
suggestion = models.CharField('处理意见', max_length=10000, default='', blank=True)
participant_type = models.IntegerField('处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人等', choices=State.state_participanttype_choices)
participant = models.ForeignKey(User, verbose_name='处理人', on_delete=models.SET_NULL, null=True, blank=True, related_name='ticketflow_participant')
participant_str = models.CharField('处理人', max_length=200, null=True, blank=True, help_text='非人工处理的处理人相关信息')
state = models.ForeignKey(State, verbose_name='当前状态', default=0, blank=True, on_delete=models.CASCADE)
ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据json格式')
intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices)
participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')

56
apps/wf/scripts.py Normal file
View File

@ -0,0 +1,56 @@
from apps.system.models import User
from apps.wf.models import State, Ticket, TicketFlow, Transition
class GetParticipants:
"""
获取处理人脚本
"""
all_funcs = [
{'func':'get_create_by', 'name':'获取工单创建人'}
]
# def all_funcs(self):
# # return list(filter(lambda x: x.startswith('get_') and callable(getattr(self, x)), dir(self)))
# return [(func, getattr(self, func).__doc__) for func in dir(self) if callable(getattr(self, func)) and func.startswith('get_')]
@classmethod
def get_create_by(cls, state:dict={}, ticket:dict={}, new_ticket_data:dict={}, handler:User={}):
"""工单创建人"""
participant = ticket.create_by.id
return participant
class HandleScripts:
"""
任务处理脚本
"""
all_funcs = [
{'func': 'handle_something', 'name':'处理一些工作'}
]
@classmethod
def to_next(cls, ticket:Ticket, by_timer:bool=False, by_task:bool=False, by_hook:bool=False, script_str:str=''):
# 获取信息
transition_obj = Transition.objects.filter(source_state=ticket.state, is_deleted=False).first()
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
participant_type=State.PARTICIPANT_TYPE_ROBOT,
participant_str='func:{}'.format(script_str),
transition=transition_obj)
from .services import WfService
# 自动执行流转
WfService.handle_ticket(ticket=ticket, transition=transition_obj, new_ticket_data=ticket.ticket_data, by_task=True)
return ticket
@classmethod
def handle_something(cls, ticket:Ticket):
"""处理一些工作"""
# 任务处理代码区
# 调用自动流转
ticket = cls.to_next(ticket=ticket, by_task=True, script_str= 'handle_something')

193
apps/wf/serializers.py Normal file
View File

@ -0,0 +1,193 @@
from apps.system.models import Dept, User
from apps.system.serializers import UserSimpleSerializer
import rest_framework
from rest_framework import serializers
from .models import State, Ticket, TicketFlow, Workflow, Transition, CustomField
class WorkflowSerializer(serializers.ModelSerializer):
class Meta:
model = Workflow
fields = '__all__'
class StateSerializer(serializers.ModelSerializer):
class Meta:
model = State
fields = '__all__'
class WorkflowSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Workflow
fields = ['id', 'name']
class StateSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = State
fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat']
class TransitionSerializer(serializers.ModelSerializer):
source_state_ = StateSimpleSerializer(source='source_state', read_only=True)
destination_state_ = StateSimpleSerializer(source='destination_state', read_only=True)
class Meta:
model = Transition
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.select_related('source_state','destination_state')
return queryset
class AllField(serializers.Field):
def to_representation(self, value):
return value
def to_internal_value(self, data):
return data
class FieldChoiceSerializer(serializers.Serializer):
id = AllField(label='ID')
name = serializers.CharField(label='名称')
class CustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = CustomField
fields = '__all__'
class CustomFieldCreateUpdateSerializer(serializers.ModelSerializer):
field_choice = FieldChoiceSerializer(label='选项列表', many=True, required=False)
class Meta:
model = CustomField
fields = ['workflow', 'field_type', 'field_key', 'field_name',
'sort', 'default_value', 'description', 'placeholder', 'field_template',
'boolean_field_display', 'field_choice', 'label', 'is_hidden']
class TicketSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = '__all__'
class TicketCreateSerializer(serializers.ModelSerializer):
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), write_only=True)
title = serializers.CharField(allow_blank=True, required=False)
class Meta:
model=Ticket
fields=['title','workflow', 'ticket_data', 'transition']
def create(self, validated_data):
return super().create(validated_data)
class TicketSerializer(serializers.ModelSerializer):
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
class Meta:
model = Ticket
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('workflow','state')
return queryset
class TicketListSerializer(serializers.ModelSerializer):
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
class Meta:
model = Ticket
fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_', 'act_state', 'create_time', 'update_time', 'participant_type', 'create_by']
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('workflow','state')
return queryset
class TicketDetailSerializer(serializers.ModelSerializer):
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
ticket_data_ = serializers.SerializerMethodField()
class Meta:
model = Ticket
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('workflow','state')
return queryset
def get_ticket_data_(self, obj):
ticket_data = obj.ticket_data
state_fields = obj.state.state_fields
all_fields = CustomField.objects.filter(workflow=obj.workflow).order_by('sort')
all_fields_l = CustomFieldSerializer(instance=all_fields, many=True).data
for i in all_fields_l:
key = i['field_key']
i['field_state'] = state_fields.get(key, 1)
i['field_value'] = ticket_data.get(key, None)
i['field_display'] = i['field_value'] # 该字段是用于查看详情直接展示
if i['field_value']:
if 'sys_user' in i['label']:
if isinstance(i['field_value'], list):
i['field_display'] = ','.join(list(User.objects.filter(id__in=i['field_value']).values_list('name', flat=True)))
else:
i['field_display'] = User.objects.get(id=i['field_value']).name
elif 'deptSelect' in i['label']:
if isinstance(i['field_value'], list):
i['field_display'] = ','.join(list(Dept.objects.filter(id__in=i['field_value']).values_list('name', flat=True)))
else:
i['field_display'] = Dept.objects.get(id=i['field_value']).name
elif i['field_type'] in ['radio', 'select']:
for m in i['field_choice']:
if m['id'] == i['field_value']:
i['field_display'] = m['name']
elif i['field_type'] in ['checkbox', 'selects']:
d_list = []
for m in i['field_choice']:
if m['id'] in i['field_value']:
d_list.append(m['name'])
i['field_display'] = ','.join(d_list)
return all_fields_l
def filter_display(self, item, field_value):
if item['id'] == field_value:
return
class TicketFlowSerializer(serializers.ModelSerializer):
participant_ = UserSimpleSerializer(source='participant', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
class Meta:
model = TicketFlow
fields = '__all__'
class TicketFlowSimpleSerializer(serializers.ModelSerializer):
participant_ = UserSimpleSerializer(source='participant', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
class Meta:
model = TicketFlow
exclude = ['ticket_data']
class TicketHandleSerializer(serializers.Serializer):
transition = serializers.PrimaryKeyRelatedField(queryset=Transition.objects.all(), label="流转id")
ticket_data = serializers.JSONField(label="表单数据json")
suggestion = serializers.CharField(label="处理意见", required = False, allow_blank=True)
class TicketRetreatSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="撤回原因", required = False)
class TicketCloseSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="关闭原因", required = False)
class TicketAddNodeSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="加签说明", required = False)
toadd_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), label='发送给谁去加签')
class TicketAddNodeEndSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="加签意见", required = False)
class TicketDestorySerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.PrimaryKeyRelatedField(queryset=Ticket.objects.all()), label='工单ID列表')

353
apps/wf/services.py Normal file
View File

@ -0,0 +1,353 @@
from apps.wf.serializers import CustomFieldSerializer
from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer
from typing import Tuple
from apps.system.models import User
from apps.wf.models import CustomField, State, Ticket, TicketFlow, Transition, Workflow
from rest_framework.exceptions import APIException, PermissionDenied, ValidationError
from django.utils import timezone
from datetime import timedelta
import random
from .scripts import GetParticipants, HandleScripts
from apps.utils.queryset import get_parent_queryset
class WfService(object):
@staticmethod
def get_worlflow_states(workflow:Workflow):
"""
获取工作流状态列表
"""
return State.objects.filter(workflow=workflow, is_deleted=False).order_by('sort')
@staticmethod
def get_workflow_transitions(workflow:Workflow):
"""
获取工作流流转列表
"""
return Transition.objects.filter(workflow=workflow, is_deleted=False)
@staticmethod
def get_workflow_start_state(workflow:Workflow):
"""
获取工作流初始状态
"""
try:
wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_START, is_deleted=False)
return wf_state_obj
except:
raise APIException('工作流状态配置错误')
@staticmethod
def get_workflow_end_state(workflow:Workflow):
"""
获取工作流结束状态
"""
try:
wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_END, is_deleted=False)
return wf_state_obj
except:
raise APIException('工作流状态配置错误')
@staticmethod
def get_workflow_custom_fields(workflow:Workflow):
"""
获取工单字段
"""
return CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort')
@staticmethod
def get_workflow_custom_fields_list(workflow:Workflow):
"""
获取工单字段key List
"""
return list(CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort').values_list('field_key', flat=True))
@classmethod
def get_ticket_transitions(cls, ticket:Ticket):
"""
获取工单当前状态下可用的流转条件
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_state_transitions(cls, state:State):
"""
获取状态可执行的操作
"""
return Transition.objects.filter(is_deleted=False, source_state=state).all()
@classmethod
def get_ticket_steps(cls, ticket:Ticket):
steps = cls.get_worlflow_states(ticket.workflow)
nsteps_list = []
for i in steps:
if ticket.state == i or (not i.is_hidden):
nsteps_list.append(i)
return nsteps_list
@classmethod
def get_ticket_transitions(cls, ticket:Ticket):
"""
获取工单可执行的操作
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_transition_by_args(cls, kwargs:dict):
"""
查询并获取流转
"""
kwargs['is_deleted'] = False
return Transition.objects.filter(**kwargs).all()
@classmethod
def get_ticket_sn(cls, workflow:Workflow):
"""
生成工单流水号
"""
now = timezone.now()
today = str(now)[:10]+' 00:00:00'
next_day = str(now+timedelta(days=1))[:10]+' 00:00:00'
ticket_day_count_new = Ticket.objects.filter(create_time__gte=today, create_time__lte=next_day, workflow=workflow).count()+1
return '%s_%04d%02d%02d%04d' % (workflow.sn_prefix, now.year, now.month, now.day, ticket_day_count_new)
@classmethod
def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, new_ticket_data:dict={})->object:
"""
获取下个节点状态
"""
source_state = ticket.state
destination_state = transition.destination_state
ticket_all_value = cls.get_ticket_all_field_value(ticket)
ticket_all_value.update(**new_ticket_data)
for key, value in ticket_all_value.items():
if isinstance(ticket_all_value[key], str):
ticket_all_value[key] = "'" + ticket_all_value[key] + "'"
if transition.condition_expression:
for i in transition.condition_expression:
expression = i['expression'].format(**ticket_all_value)
import datetime, time # 用于支持条件表达式中对时间的操作
if eval(expression, {'__builtins__':None}, {'datetime':datetime, 'time':time}):
destination_state = State.objects.get(pk=i['target_state'])
return destination_state
return destination_state
@classmethod
def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, new_ticket_data:dict={}, handler:User=None):
"""
获取工单目标状态实际的处理人, 处理人类型
"""
if state.type == State.STATE_TYPE_START:
"""
回到初始状态
"""
return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL,
destination_participant=ticket.create_by.id,
multi_all_person={})
elif state.type == State.STATE_TYPE_END:
"""
到达结束状态
"""
return dict(destination_participant_type=0,
destination_participant=0,
multi_all_person={})
multi_all_person_dict = {}
destination_participant_type, destination_participant = state.participant_type, state.participant
if destination_participant_type == State.PARTICIPANT_TYPE_FIELD:
destination_participant = new_ticket_data.get(destination_participant, 0) if destination_participant in new_ticket_data \
else Ticket.ticket_data.get(destination_participant, 0)
elif destination_participant_type == State.PARTICIPANT_TYPE_FORMCODE:#代码获取
destination_participant = getattr(GetParticipants, destination_participant)(
state=state, ticket=ticket, new_ticket_data=new_ticket_data, hander=handler)
elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#部门
destination_participant = list(User.objects.filter(dept__in=destination_participant).values_list('id', flat=True))
elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#角色
user_queryset = User.objects.filter(roles__in=destination_participant)
# 如果选择了角色, 需要走过滤策略
if state.filter_policy == 1:
depts = get_parent_queryset(ticket.belong_dept)
user_queryset = user_queryset.filter(dept__in=depts)
elif state.filter_policy == 2:
depts = get_parent_queryset(ticket.create_by.dept)
user_queryset = user_queryset.filter(dept__in=depts)
elif state.filter_policy == 3:
depts = get_parent_queryset(handler.dept)
user_queryset = user_queryset.filter(dept__in=depts)
destination_participant = list(user_queryset.values_list('id', flat=True))
if type(destination_participant) == list:
destination_participant_type = State.PARTICIPANT_TYPE_MULTI
destination_participant = list(set(destination_participant))
if len(destination_participant) == 1: # 如果只有一个人
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
destination_participant = destination_participant[0]
else:
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
if destination_participant_type == State.PARTICIPANT_TYPE_MULTI:
if state.distribute_type == State.STATE_DISTRIBUTE_TYPE_RANDOM:
destination_participant = random.choice(destination_participant)
elif state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ALL:
for i in destination_participant:
multi_all_person_dict[i]={}
return dict(destination_participant_type=destination_participant_type,
destination_participant=destination_participant,
multi_all_person=multi_all_person_dict)
@classmethod
def ticket_handle_permission_check(cls, ticket:Ticket, user:User)-> dict:
transitions = cls.get_state_transitions(ticket.state)
if not transitions:
return dict(permission=True, msg="工单当前状态无需操作")
current_participant_count = 0
participant_type = ticket.participant_type
participant = ticket.participant
state = ticket.stateF
if participant_type == State.PARTICIPANT_TYPE_PERSONAL:
if user.id != participant:
return dict(permission=False, msg="非当前处理人", need_accept=False)
elif participant_type in [State.PARTICIPANT_TYPE_MULTI, State.PARTICIPANT_TYPE_DEPT, State.PARTICIPANT_TYPE_ROLE]:
if user.id not in participant:
return dict(permission=False, msg="非当前处理人", need_accept=False)
current_participant_count = len(participant)
if current_participant_count == 1:
if [user.id] == participant or user.id == participant:
pass
else:
return dict(permission=False, msg="非当前处理人", need_accept=False)
elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE:
if user.id not in participant:
return dict(permission=False, msg="非当前处理人", need_accept=False)
return dict(permission=False, msg="需要先接单再处理", need_accept=True)
if ticket.in_add_node:
return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作", need_accept=False)
return dict(permission=True, msg="", need_accept=False)
@classmethod
def check_dict_has_all_same_value(cls, dict_obj: object)->tuple:
"""
check whether all key are equal in a dict
:param dict_obj:
:return:
"""
value_list = []
for key, value in dict_obj.items():
value_list.append(value)
value_0 = value_list[0]
for value in value_list:
if value_0 != value:
return False
return True
@classmethod
def get_ticket_all_field_value(cls, ticket: Ticket)->dict:
"""
工单所有字段的值
get ticket's all field value
:param ticket:
:return:
"""
# 获取工单基础表中的字段中的字段信息
field_info_dict = TicketSimpleSerializer(instance=ticket).data
# 获取自定义字段的值
custom_fields_queryset = cls.get_workflow_custom_fields(ticket.workflow)
for i in custom_fields_queryset:
field_info_dict[i.field_key] = ticket.ticket_data.get(i.field_key, None)
return field_info_dict
@classmethod
def handle_ticket(cls, ticket:Ticket, transition: Transition, new_ticket_data:dict={}, handler:User=None,
suggestion:str='', created:bool=False, by_timer:bool=False, by_task:bool=False, by_hook:bool=False):
source_state = ticket.state
source_ticket_data = ticket.ticket_data
# 校验处理权限
if not handler or not created: # 没有处理人意味着系统触发不校验处理权限
result = WfService.ticket_handle_permission_check(ticket, handler)
if result.get('permission') is False:
raise PermissionDenied(result.get('msg'))
# 校验表单必填项目
if transition.field_require_check or not created:
for key, value in ticket.state.state_fields.items():
if int(value) == State.STATE_FIELD_REQUIRED:
if key not in new_ticket_data or not new_ticket_data[key]:
raise ValidationError('字段{}必填'.format(key))
destination_state = cls.get_next_state_by_transition_and_ticket_info(ticket, transition, new_ticket_data)
multi_all_person = ticket.multi_all_person
if multi_all_person:
multi_all_person[handler.id] =dict(transition=transition.id)
# 判断所有人处理结果是否一致
if WfService.check_dict_has_all_same_value(multi_all_person):
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, new_ticket_data)
destination_participant_type = participant_info.get('destination_participant_type', 0)
destination_participant = participant_info.get('destination_participant', 0)
multi_all_person = {}
else:
# 处理人没有没有全部处理完成或者处理动作不一致
destination_participant_type = ticket.participant_type
destination_state = ticket.state # 保持原状态
destination_participant = []
for key, value in multi_all_person.items():
if not value:
destination_participant.append(key)
else:
# 当前处理人类型非全部处理
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, new_ticket_data)
destination_participant_type = participant_info.get('destination_participant_type', 0)
destination_participant = participant_info.get('destination_participant', 0)
multi_all_person = participant_info.get('multi_all_person', {})
# 更新工单信息:基础字段及自定义字段, add_relation字段 需要下个处理人是部门、角色等的情况
ticket.state = destination_state
ticket.participant_type = destination_participant_type
ticket.participant = destination_participant
ticket.multi_all_person = multi_all_person
if destination_state.type == State.STATE_TYPE_END:
ticket.act_state = Ticket.TICKET_ACT_STATE_FINISH
elif destination_state.type == State.STATE_TYPE_START:
ticket.act_state = Ticket.TICKET_ACT_STATE_DRAFT
else:
ticket.act_state = Ticket.TICKET_ACT_STATE_ONGOING
if transition.attribute_type == Transition.TRANSITION_ATTRIBUTE_TYPE_REFUSE:
ticket.act_state = Ticket.TICKET_ACT_STATE_BACK
# 只更新必填和可选的字段
if not created:
for key, value in source_state.state_fields.items():
if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL):
if key in new_ticket_data:
source_ticket_data[key] = new_ticket_data[key]
ticket.ticket_data = source_ticket_data
ticket.save()
# 更新工单流转记录
if not by_task:
TicketFlow.objects.create(ticket=ticket, state=source_state, ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
participant=handler, transition=transition)
if created:
if source_state.participant_cc:
TicketFlow.objects.create(ticket=ticket, state=source_state,
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
participant=None, participant_cc=source_state.participant_cc)
# 目标状态需要抄送
if destination_state.participant_cc:
TicketFlow.objects.create(ticket=ticket, state=destination_state,
participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
participant=None, participant_cc=destination_state.participant_cc)
# 如果目标状态是脚本则执行
if destination_state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
getattr(HandleScripts, destination_state.participant)(ticket)
return ticket

3
apps/wf/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

21
apps/wf/urls.py Normal file
View File

@ -0,0 +1,21 @@
from django.db.models import base
from rest_framework import urlpatterns
from apps.wf.views import CustomFieldViewSet, FromCodeListView, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
API_BASE_URL = 'api/wf/'
HTML_BASE_URL = 'wf/'
router = DefaultRouter()
router.register('workflow', WorkflowViewSet, basename='wf')
router.register('state', StateViewSet, basename='wf_state')
router.register('transition', TransitionViewSet, basename='wf_transitions')
router.register('customfield', CustomFieldViewSet, basename='wf_customfield')
router.register('ticket', TicketViewSet, basename='wf_ticket')
router.register('ticketflow', TicketFlowViewSet, basename='wf_ticketflow')
urlpatterns = [
path(API_BASE_URL + 'participant_from_code', FromCodeListView.as_view()),
path(API_BASE_URL, include(router.urls)),
]

373
apps/wf/views.py Normal file
View File

@ -0,0 +1,373 @@
from django.utils import timezone
from django.db import transaction
from django.db.models import query
from rest_framework.utils import serializer_helpers
from rest_framework.views import APIView
from apps.system.models import User
from apps.wf.filters import TicketFilterSet
from django.core.exceptions import AppRegistryNotReady
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, TicketCreateSerializer, TicketDestorySerializer, TicketFlowSerializer, TicketFlowSimpleSerializer, TicketHandleSerializer, TicketRetreatSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer, TicketListSerializer, TicketDetailSerializer
from django.shortcuts import get_object_or_404, render
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action, api_view
from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow
from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin
from apps.wf.services import WfService
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework import status
from django.db.models import Count
from .scripts import GetParticipants, HandleScripts
# Create your views here.
class FromCodeListView(APIView):
def get(self, request, format=None):
"""
获取处理人代码列表
"""
return Response(GetParticipants.all_funcs)
class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
perms_map = {'get': '*', 'post': 'workflow_create',
'put': 'workflow_update', 'delete': 'workflow_delete'}
queryset = Workflow.objects.all()
serializer_class = WorkflowSerializer
search_fields = ['name', 'description']
filterset_fields = []
ordering_fields = ['create_time']
ordering = ['-create_time']
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=StateSerializer)
def states(self, request, pk=None):
"""
工作流下的状态节点
"""
wf = self.get_object()
serializer = self.serializer_class(instance=WfService.get_worlflow_states(wf), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=TransitionSerializer)
def transitions(self, request, pk=None):
"""
工作流下的流转规则
"""
wf = self.get_object()
serializer = self.serializer_class(instance=WfService.get_workflow_transitions(wf), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=CustomFieldSerializer)
def customfields(self, request, pk=None):
"""
工作流下的自定义字段
"""
wf = self.get_object()
serializer = self.serializer_class(instance=CustomField.objects.filter(workflow=wf, is_deleted=False).order_by('sort'), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_init'})
def init(self, request, pk=None):
"""
新建工单初始化
"""
ret={}
wf = self.get_object()
start_state = WfService.get_workflow_start_state(wf)
transitions = WfService.get_state_transitions(start_state)
ret['workflow'] = pk
ret['transitions'] = TransitionSerializer(instance=transitions, many=True).data
field_list = CustomFieldSerializer(instance=WfService.get_workflow_custom_fields(wf), many=True).data
for i in field_list:
if i['field_key'] in start_state.state_fields:
i['field_attribute'] = start_state.state_fields[i['field_key']]
else:
i['field_attribute'] = State.STATE_FIELD_READONLY
ret['field_list'] = field_list
return Response(ret)
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'get':'*', 'post':'workflow_update',
'put':'workflow_update', 'delete':'workflow_update'}
queryset = State.objects.all()
serializer_class = StateSerializer
search_fields = ['name']
filterset_fields = ['workflow']
ordering = ['sort']
class TransitionViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'get':'*', 'post':'workflow_update',
'put':'workflow_update', 'delete':'workflow_update'}
queryset = Transition.objects.all()
serializer_class = TransitionSerializer
search_fields = ['name']
filterset_fields = ['workflow']
ordering = ['id']
class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'get':'*', 'post':'workflow_update',
'put':'workflow_update', 'delete':'workflow_update'}
queryset = CustomField.objects.all()
serializer_class = CustomFieldSerializer
search_fields = ['field_name']
filterset_fields = ['workflow', 'field_type']
ordering = ['sort']
def get_serializer_class(self):
if self.action in ['create', 'update']:
return CustomFieldCreateUpdateSerializer
return super().get_serializer_class()
class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet):
perms_map = {'get':'*', 'post':'ticket_create'}
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
search_fields = ['title']
filterset_class = TicketFilterSet
ordering = ['-create_time']
def get_serializer_class(self):
if self.action == 'create':
return TicketCreateSerializer
elif self.action == 'handle':
return TicketHandleSerializer
elif self.action == 'retreat':
return TicketRetreatSerializer
elif self.action == 'list':
return TicketListSerializer
elif self.action == 'retrieve':
return TicketDetailSerializer
return super().get_serializer_class()
def filter_queryset(self, queryset):
if not self.detail and not self.request.query_params.get('category', None):
raise APIException('请指定查询分类')
return super().filter_queryset(queryset)
@transaction.atomic
def create(self, request, *args, **kwargs):
"""
新建工单
"""
rdata = request.data
serializer = self.get_serializer(data=rdata)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data #校验之后的数据
start_state = WfService.get_workflow_start_state(vdata['workflow'])
transition = vdata.pop('transition')
ticket_data = vdata['ticket_data']
save_ticket_data = {}
# 校验必填项
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 APIException('字段{}必填'.format(key))
save_ticket_data[key] = ticket_data[key]
elif int(value) == State.STATE_FIELD_OPTIONAL:
save_ticket_data[key] = ticket_data[key]
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.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)
@action(methods=['get'], detail=False, perms_map={'get':'*'})
def duty_agg(self, request, pk=None):
"""
工单待办聚合
"""
ret = {}
queryset = Ticket.objects.filter(participant__contains=request.user.id, is_deleted=False)\
.exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED])
ret['total_count'] = queryset.count()
ret['details'] = list(queryset.values('workflow', 'workflow__name').annotate(count = Count('workflow')))
return Response(ret)
@action(methods=['post'], detail=True, perms_map={'post':'*'})
@transaction.atomic
def handle(self, request, pk=None):
"""
处理工单
"""
ticket = self.get_object()
serializer = TicketHandleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
vdata = serializer.validated_data
new_ticket_data = ticket.ticket_data
new_ticket_data.update(**vdata['ticket_data'])
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
new_ticket_data=new_ticket_data, handler=request.user, suggestion=vdata['suggestion'])
return Response(TicketSerializer(instance=ticket).data)
@action(methods=['get'], detail=True, perms_map={'get':'*'})
def flowsteps(self, request, pk=None):
"""
工单流转step, 用于显示当前状态的step图(线性结构)
"""
ticket = self.get_object()
steps = WfService.get_ticket_steps(ticket)
return Response(StateSerializer(instance=steps, many=True).data)
@action(methods=['get'], detail=True, perms_map={'get':'*'})
def flowlogs(self, request, pk=None):
"""
工单流转记录
"""
ticket = self.get_object()
flowlogs = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time')
serializer = TicketFlowSerializer(instance=flowlogs, many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'*'})
def transitions(self, request, pk=None):
"""
获取工单可执行的操作
"""
ticket = self.get_object()
transitions = WfService.get_ticket_transitions(ticket)
return Response(TransitionSerializer(instance=transitions, many=True).data)
@action(methods=['post'], detail=True, perms_map={'post':'*'})
def accpet(self, request, pk=None):
"""
接单,当工单当前处理人实际为多个人时(角色部门多人都有可能 注意角色和部门有可能实际只有一人)
"""
ticket = self.get_object()
result = WfService.ticket_handle_permission_check(ticket, request.user)
if result.get('need_accept', False):
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant = request.user.id
ticket.save()
# 接单日志
# 更新工单流转记录
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
participant=request.user, transition=None)
return Response()
else:
raise APIException('无需接单')
@action(methods=['post'], detail=True, perms_map={'post':'*'})
def retreat(self, request, pk=None):
"""
撤回工单允许创建人在指定状态撤回工单至初始状态状态设置中开启允许撤回
"""
ticket = self.get_object()
if ticket.create_by != request.user:
raise APIException('非创建人不可撤回')
if not ticket.state.enable_retreat:
raise APIException('该状态不可撤回')
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()
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketAddNodeSerializer)
def add_node(self, request, pk=None):
"""
加签
"""
ticket = self.get_object()
data = request.data
add_user = User.objects.get(pk=data['toadd_user'])
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant = add_user.id
ticket.in_add_node = True
ticket.add_node_man = request.user
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_ADD_NODE,
participant=request.user, transition=None)
return Response()
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketAddNodeEndSerializer)
def add_node_end(self, request, pk=None):
"""
加签完成
"""
ticket = self.get_object()
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.in_add_node = False
ticket.participant = ticket.add_node_man.id
ticket.add_node_man = None
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_ADD_NODE_END,
participant=request.user, transition=None)
return Response()
@action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=TicketCloseSerializer)
def close(self, request, pk=None):
"""
关闭工单(创建人在初始状态)
"""
ticket = self.get_object()
if ticket.state.type == State.STATE_TYPE_START and ticket.create_by==request.user:
end_state = WfService.get_workflow_end_state(ticket.workflow)
ticket.state = end_state
ticket.participant_type = 0
ticket.participant = 0
ticket.act_state = Ticket.TICKET_ACT_STATE_CLOSED
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_CLOSE,
participant=request.user, transition=None)
return Response()
else:
return Response('工单不可关闭', status=status.HTTP_400_BAD_REQUEST)
@action(methods=['post'], detail=False, perms_map={'post':'ticket_deletes'}, serializer_class=TicketDestorySerializer)
def destory(self, request, pk=None):
"""
批量物理删除
"""
Ticket.objects.filter(id__in=request.data.get('ids', [])).delete(soft=False)
return Response()
class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
"""
工单日志
"""
perms_map = {'get':'*'}
queryset = TicketFlow.objects.all()
serializer_class = TicketFlowSerializer
search_fields = ['suggestion']
filterset_fields = ['ticket']
ordering = ['-create_time']

2
log/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

21
manage.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

BIN
media/default/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

20
requirements.txt Normal file
View File

@ -0,0 +1,20 @@
celery==5.2.3
Django==3.2.12
django-celery-beat==2.2.1
django-celery-results==2.3.0
django-cors-headers==3.11.0
django-filter==21.1
django-simple-history==3.0.0
djangorestframework==3.13.1
djangorestframework-simplejwt==5.1.0
drf-yasg==1.20.0
psutil==5.9.0
pillow==9.0.1
opencv-python==4.5.5.62
gunicorn==20.1.0
redis==4.1.4
user-agents==2.2.0
daphne==3.0.2
channels==3.0.4
channels-redis==3.4.0
django-restql==0.15.2

5
server/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

25
server/asgi.py Normal file
View File

@ -0,0 +1,25 @@
"""
ASGI config for server project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
"""
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from channels.auth import AuthMiddlewareStack
import apps.monitor.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
apps.monitor.routing.websocket_urlpatterns
)
)
})

22
server/celery.py Normal file
View File

@ -0,0 +1,22 @@
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
app = Celery('server')
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@app.task(bind=True)
def debug_task(self):
print(f'Request: {self.request!r}')

12
server/conf.example.py Normal file
View File

@ -0,0 +1,12 @@
SECRET_KEY = '48j$m*^$m0hf0c7&=+!i9&3$8k8^6^a18t9(c*e8^sp(1&3^z0'
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'ehs',
'USER': 'postgres',
'PASSWORD': 'zcDsj2021',
'HOST': '49.232.14.174',
'PORT': '5432',
}
}

318
server/settings.py Normal file
View File

@ -0,0 +1,318 @@
"""
Django settings for server project.
Generated by 'django-admin startproject' using Django 3.0.3.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
from datetime import datetime, timedelta
import os
from . import conf
from server.conf import DATABASES, XX_ENABLED, XX_LICENCE
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = conf.SECRET_KEY
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = conf.DEBUG
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'channels',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'django_celery_beat',
'django_celery_results',
'drf_yasg',
'rest_framework',
"django_filters",
'simple_history',
'apps.utils',
'apps.third',
'apps.system',
'apps.auth1',
'apps.monitor',
'apps.wf'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]
ROOT_URLCONF = 'server.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['dist'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# WSGI
WSGI_APPLICATION = 'server.wsgi.application'
# ASGI
ASGI_APPLICATION = 'server.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = conf.DATABASES
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static_collect')
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'dist/static'),
)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# 默认主键
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 雪花ID生成配置
SNOW_DATACENTER_ID = conf.SNOW_DATACENTER_ID
SNOW_WORKER_ID = conf.SNOW_WORKER_ID
# restframework配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'apps.utils.permission.RbacPermission'
],
'DEFAULT_RENDERER_CLASSES': [
'apps.utils.response.FitJSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer'
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter'
],
'DEFAULT_PAGINATION_CLASS': 'apps.utils.pagination.MyPagination',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
'DATE_FORMAT': '%Y-%m-%d',
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'UNAUTHENTICATED_USER': None,
'UNAUTHENTICATED_TOKEN': None,
'EXCEPTION_HANDLER': 'apps.utils.response.custome_exception_hander',
}
# simplejwt配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
}
# 跨域配置/可用nginx处理,无需引入corsheaders
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
# Auth配置
AUTH_USER_MODEL = 'system.User'
AUTHENTICATION_BACKENDS = (
'apps.auth1.authentication.CustomBackend',
)
# 缓存配置,有需要可更改为redis
# CACHES = {
# "default": {
# "BACKEND": "django_redis.cache.RedisCache",
# "LOCATION": "redis://redis:6379/1",
# "OPTIONS": {
# "CLIENT_CLASS": "django_redis.client.DefaultClient",
# "PICKLE_VERSION": -1
# }
# }
# }
# celery配置,celery正常运行必须安装redis
CELERY_BROKER_URL = "redis://redis:6379/0" # 任务存储
CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁可防止内存泄露
CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区
CELERY_ENABLE_UTC = True # 启动时区设置
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# swagger配置
SWAGGER_SETTINGS = {
'LOGIN_URL':'/django/login/',
'LOGOUT_URL':'/django/logout/',
}
# 日志配置
# 创建日志的路径
LOG_PATH = os.path.join(BASE_DIR, 'log')
# 如果地址不存在则自动创建log文件夹
if not os.path.exists(os.path.join(LOG_PATH)):
os.mkdir(LOG_PATH)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
# 日志格式
'standard': {
'format': '[%(asctime)s] [%(filename)s:%(lineno)d] [%(module)s:%(funcName)s] '
'[%(levelname)s]- %(message)s'},
'simple': { # 简单格式
'format': '%(levelname)s %(message)s'
},
},
# 过滤
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# 定义具体处理日志的方式
'handlers': {
# 默认记录所有日志
'default': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'all-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
'maxBytes': 1024 * 1024 * 5, # 文件大小
'backupCount': 5, # 备份数
'formatter': 'standard', # 输出格式
'encoding': 'utf-8', # 设置默认编码,否则打印出来汉字乱码
},
# 输出错误日志
'error': {
'level': 'ERROR',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'error-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
'maxBytes': 1024 * 1024 * 5, # 文件大小
'backupCount': 5, # 备份数
'formatter': 'standard', # 输出格式
'encoding': 'utf-8', # 设置默认编码
},
# 控制台输出
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'filters': ['require_debug_true'],
'formatter': 'standard'
},
# 输出info日志
'info': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'info-{}.log'.format(datetime.now().strftime('%Y-%m-%d'))),
'maxBytes': 1024 * 1024 * 5,
'backupCount': 5,
'formatter': 'standard',
'encoding': 'utf-8', # 设置默认编码
},
},
# 配置用哪几种 handlers 来处理日志
'loggers': {
# 类型 为 django 处理所有类型的日志, 默认调用
'django': {
'handlers': ['default', 'console', 'error'],
'level': 'INFO',
'propagate': False
},
# log 调用时需要当作参数传入
'log': {
'handlers': ['error', 'info', 'console', 'default'],
'level': 'INFO',
'propagate': True
},
}
}
# 大华ICC平台
DAHUA_ENABLED = conf.DAHUA_ENABLED
DAHUA_BASE_URL = conf.DAHUA_BASE_URL
DAHUA_USERNAME = conf.DAHUA_USERNAME
DAHUA_PASSWORD = conf.DAHUA_PASSWORD
DAHUA_CLIENTID = conf.DAHUA_CLIENTID
DAHUA_SECRET = conf.DAHUA_SECRET
# 寻息定位
XX_ENABLED = conf.XX_ENABLED
XX_BASE_URL = conf.XX_BASE_URL
XX_LICENCE = conf.XX_LICENCE
XX_USERNAME = conf.XX_USERNAME

60
server/urls.py Normal file
View File

@ -0,0 +1,60 @@
"""server URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework.documentation import include_docs_urls
from django.views.generic import TemplateView
schema_view = get_schema_view(
openapi.Info(
title="EHS API",
default_version='v1',
contact=openapi.Contact(email="caoqianming@foxmail.com"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=[],
)
urlpatterns = [
# django后台
path('django/doc/', include('django.contrib.admindocs.urls')),
path('django/', admin.site.urls),
# api
path('', include('apps.auth1.urls')),
path('', include('apps.system.urls')),
path('', include('apps.monitor.urls')),
path('', include('apps.wf.urls')),
path('', include('apps.third.urls')),
path('', include('apps.utils.urls')),
# api文档
path('api/docs/', include_docs_urls(title="接口文档", authentication_classes=[], permission_classes=[])),
path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# 前端页面入口
path('',TemplateView.as_view(template_name="index.html"))
] + \
static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + \
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
server/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for server project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
application = get_wsgi_application()