初始化master
This commit is contained in:
commit
e2a20fe67a
|
|
@ -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,0 +1,2 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthConfig(AppConfig):
|
||||||
|
name = 'apps.auth1'
|
||||||
|
verbose_name = "认证"
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class LoginSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField(label="用户名")
|
||||||
|
password = serializers.CharField(label="密码")
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -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')
|
||||||
|
]
|
||||||
|
|
@ -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,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorConfig(AppConfig):
|
||||||
|
name = 'apps.monitor'
|
||||||
|
verbose_name = '系统监控'
|
||||||
|
|
@ -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
|
||||||
|
}))
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
@ -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())
|
||||||
|
]
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -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()),
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'apps.system.apps.SystemConfig'
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SystemConfig(AppConfig):
|
||||||
|
name = 'apps.system'
|
||||||
|
verbose_name = '系统管理'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
import apps.system.signals
|
||||||
|
|
@ -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'],
|
||||||
|
}
|
||||||
|
|
@ -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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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']
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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')
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -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)),
|
||||||
|
]
|
||||||
|
|
@ -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,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdConfig(AppConfig):
|
||||||
|
name = 'apps.third'
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -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()),
|
||||||
|
]
|
||||||
|
|
@ -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,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UtilsConfig(AppConfig):
|
||||||
|
name = 'apps.utils'
|
||||||
|
|
@ -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,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)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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 ""
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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())
|
||||||
|
|
@ -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),
|
||||||
|
))
|
||||||
|
|
@ -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)),
|
||||||
|
]
|
||||||
|
|
@ -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']
|
||||||
|
|
@ -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='签名照处理失败,请重新上传')
|
||||||
|
|
@ -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,带数据权限过滤
|
||||||
|
"""
|
||||||
|
|
@ -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,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class WfConfig(AppConfig):
|
||||||
|
name = 'apps.wf'
|
||||||
|
verbose_name = '工作流管理'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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列表)')
|
||||||
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
@ -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列表')
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -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)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
@ -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']
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
@ -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()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
|
|
@ -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
|
||||||
|
|
@ -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',)
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -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}')
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue