初始化happy-drf分支
This commit is contained in:
commit
b25dfcac0c
|
|
@ -0,0 +1,22 @@
|
|||
.vscode/
|
||||
.vs/
|
||||
.VSCodeCounter/
|
||||
.idea/
|
||||
venv/
|
||||
__pycache__/
|
||||
h5/*
|
||||
*.pyc
|
||||
media/*
|
||||
dist/*
|
||||
!media/default/
|
||||
celerybeat.pid
|
||||
celerybeat-schedule.bak
|
||||
celerybeat-schedule.dat
|
||||
celerybeat-schedule.dir
|
||||
db.sqlite3
|
||||
server/conf*.py
|
||||
server/conf.ini
|
||||
server/conf.json
|
||||
sh/*
|
||||
temp/*
|
||||
nohup.out
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from django.contrib import admin
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
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(employee__id_number=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,2 @@
|
|||
|
||||
USERNAME_OR_PASSWORD_WRONG = {"code": "username_or_password_wrong", "detail": "账户名或密码错误"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(label="用户名")
|
||||
password = serializers.CharField(label="密码")
|
||||
password_check = serializers.BooleanField(required=False, default=True)
|
||||
|
||||
|
||||
class SendCodeSerializer(serializers.Serializer):
|
||||
phone = serializers.CharField(label="手机号")
|
||||
|
||||
|
||||
class CodeLoginSerializer(serializers.Serializer):
|
||||
phone = serializers.CharField(label="手机号")
|
||||
code = serializers.CharField(label="验证码")
|
||||
|
||||
|
||||
class WxCodeSerializer(serializers.Serializer):
|
||||
code = serializers.CharField(label="code")
|
||||
|
||||
|
||||
class PwResetSerializer(serializers.Serializer):
|
||||
phone = serializers.CharField(label="手机号")
|
||||
code = serializers.CharField(label="验证码")
|
||||
password = serializers.CharField(label="新密码")
|
||||
|
||||
|
||||
class SecretLoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(label="用户名")
|
||||
secret = serializers.CharField(label="密钥")
|
||||
|
||||
|
||||
class FaceLoginSerializer(serializers.Serializer):
|
||||
base64 = serializers.CharField()
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from django.core.cache import cache
|
||||
from rest_framework.exceptions import ParseError
|
||||
import re
|
||||
|
||||
|
||||
def check_phone_code(phone, code, raise_exception=True):
|
||||
code_exist = cache.get(phone, None)
|
||||
if code_exist == code:
|
||||
return True
|
||||
if raise_exception:
|
||||
raise ParseError('验证码错误')
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
# 正则表达式匹配规则
|
||||
pattern = r"^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@#$%^&+=!])(?!.*\s).{8,}$"
|
||||
|
||||
# 使用正则表达式进行匹配
|
||||
if re.match(pattern, password):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
from django.urls import path
|
||||
from rest_framework_simplejwt.views import TokenRefreshView
|
||||
|
||||
from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView,
|
||||
SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView)
|
||||
|
||||
API_BASE_URL = 'api/auth/'
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL + 'token/', TokenLoginView.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 + 'login_secret/', SecretLogin.as_view(), name='secret_login'),
|
||||
path(API_BASE_URL + 'login_wxmp/', WxmpLogin.as_view(), name='login_wxmp'),
|
||||
path(API_BASE_URL + 'login_wx/', WxLogin.as_view(), name='login_wx'),
|
||||
path(API_BASE_URL + 'login_sms_code/', CodeLogin.as_view(), name='login_sms_code'),
|
||||
path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'),
|
||||
path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'),
|
||||
path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'),
|
||||
path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login')
|
||||
]
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
|
||||
from rest_framework.exceptions import ParseError
|
||||
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.errors import USERNAME_OR_PASSWORD_WRONG
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
from django.core.cache import cache
|
||||
from apps.auth1.services import check_phone_code
|
||||
from apps.utils.sms import send_sms
|
||||
from apps.utils.tools import rannum
|
||||
from apps.utils.wxmp import wxmpClient
|
||||
from apps.utils.wx import wxClient
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.db.models import Q
|
||||
from apps.auth1.services import validate_password
|
||||
import base64
|
||||
from apps.utils.tools import tran64
|
||||
from apps.auth1.serializers import FaceLoginSerializer
|
||||
|
||||
|
||||
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
|
||||
PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer)
|
||||
from apps.system.models import User
|
||||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
def get_tokens_for_user(user: User):
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return {
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
}
|
||||
|
||||
class TokenLoginView(CreateAPIView):
|
||||
"""
|
||||
账户名/密码获取token
|
||||
|
||||
账户名/密码获取token
|
||||
"""
|
||||
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
|
||||
password_check = vdata.get('password_check', True)
|
||||
# 校验密码复杂度
|
||||
is_ok = validate_password(vdata.get('password'))
|
||||
if is_ok is False and password_check:
|
||||
raise ParseError('密码校验失败, 请更换登录方式并修改密码')
|
||||
user = authenticate(username=vdata.get('username'),
|
||||
password=vdata.get('password'))
|
||||
if user is not None:
|
||||
token_dict = get_tokens_for_user(user)
|
||||
token_dict['password_ok'] = is_ok
|
||||
return Response(token_dict)
|
||||
raise ParseError(**USERNAME_OR_PASSWORD_WRONG)
|
||||
|
||||
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 Response(status=201)
|
||||
raise ParseError(**USERNAME_OR_PASSWORD_WRONG)
|
||||
|
||||
|
||||
class LogoutView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
退出登录
|
||||
|
||||
|
||||
退出登录
|
||||
"""
|
||||
logout(request)
|
||||
return Response()
|
||||
|
||||
|
||||
class WxmpLogin(CreateAPIView):
|
||||
"""微信小程序自动登录
|
||||
|
||||
微信小程序自动登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = WxCodeSerializer
|
||||
|
||||
def post(self, request):
|
||||
code = request.data['code']
|
||||
info = wxmpClient.get_basic_info(code=code)
|
||||
openid = info['openid']
|
||||
session_key = info['session_key']
|
||||
try:
|
||||
user = User.objects.get(wxmp_openid=openid)
|
||||
ret = get_tokens_for_user(user)
|
||||
ret['wxmp_session_key'] = session_key
|
||||
ret['wxmp_openid'] = openid
|
||||
cache.set(code, ret, 60*5)
|
||||
return Response(ret)
|
||||
except Exception:
|
||||
return Response({'wxmp_openid': openid, 'wxmp_session_key': session_key}, status=400)
|
||||
|
||||
|
||||
class WxLogin(CreateAPIView):
|
||||
"""微信公众号授权登录
|
||||
|
||||
微信公众号授权登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = WxCodeSerializer
|
||||
|
||||
def post(self, request):
|
||||
code = request.data['code']
|
||||
info = wxClient.get_basic_info(code=code)
|
||||
openid = info['openid']
|
||||
access = info['access_token']
|
||||
try:
|
||||
user = User.objects.get(wx_openid=openid)
|
||||
ret = get_tokens_for_user(user)
|
||||
ret['wx_token'] = access
|
||||
ret['wx_openid'] = openid
|
||||
cache.set(code, ret, 60*5)
|
||||
return Response(ret)
|
||||
except Exception:
|
||||
return Response({'wx_openid': openid, 'wx_token': access}, status=400)
|
||||
|
||||
|
||||
class SendCode(CreateAPIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = SendCodeSerializer
|
||||
|
||||
def post(self, request):
|
||||
"""短信验证码发送
|
||||
|
||||
短信验证码发送
|
||||
"""
|
||||
phone = request.data['phone']
|
||||
code = rannum(6)
|
||||
is_ok, _ = send_sms(phone, 505, {'code': code})
|
||||
cache.set(phone, code, 60*5)
|
||||
if is_ok:
|
||||
return Response()
|
||||
raise ParseError('短信发送失败,请确认手机号')
|
||||
|
||||
|
||||
class CodeLogin(CreateAPIView):
|
||||
"""手机验证码登录
|
||||
|
||||
手机验证码登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = CodeLoginSerializer
|
||||
|
||||
def post(self, request):
|
||||
phone = request.data['phone']
|
||||
code = request.data['code']
|
||||
check_phone_code(phone, code)
|
||||
user = User.objects.filter(phone=phone).first()
|
||||
if user:
|
||||
ret = get_tokens_for_user(user)
|
||||
return Response(ret)
|
||||
raise ParseError('账户不存在或已禁用')
|
||||
|
||||
|
||||
class SecretLogin(CreateAPIView):
|
||||
"""App端密钥登录
|
||||
|
||||
App端密钥登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = SecretLoginSerializer
|
||||
|
||||
def post(self, request):
|
||||
sr = SecretLoginSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
username = vdata['username']
|
||||
secret = vdata['secret']
|
||||
user = User.objects.filter(Q(username=username) | Q(phone=username) | Q(
|
||||
employee__id_number=username)).filter(secret=secret).first()
|
||||
if user:
|
||||
ret = get_tokens_for_user(user)
|
||||
return Response(ret)
|
||||
raise ParseError('登录失败')
|
||||
|
||||
|
||||
class PwResetView(CreateAPIView):
|
||||
"""重置密码
|
||||
|
||||
重置密码
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = PwResetSerializer
|
||||
|
||||
def post(self, request):
|
||||
sr = PwResetSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
check_phone_code(vdata['phone'], vdata['code'])
|
||||
user = User.objects.filter(phone=vdata['phone']).first()
|
||||
if user:
|
||||
user.password = make_password(vdata['password'])
|
||||
user.save()
|
||||
return Response()
|
||||
raise ParseError('账户不存在或已禁用')
|
||||
|
||||
|
||||
class FaceLoginView(CreateAPIView):
|
||||
"""人脸识别登录
|
||||
|
||||
人脸识别登录
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
serializer_class = FaceLoginSerializer
|
||||
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
人脸识别登录
|
||||
"""
|
||||
from apps.hrm.services import HrmService
|
||||
base64_data = base64.urlsafe_b64decode(tran64(request.data.get('base64').replace(' ', '+')))
|
||||
ep, msg = HrmService.face_compare_from_base64(base64_data)
|
||||
if ep:
|
||||
if ep.user and ep.user.is_active and ep.user.is_deleted is False:
|
||||
user = ep.user
|
||||
refresh = RefreshToken.for_user(ep.user)
|
||||
# # 可设为在岗
|
||||
# now = timezone.now()
|
||||
# now_local = timezone.localtime()
|
||||
# if 8<=now_local.hour<=17:
|
||||
# ins, created = ClockRecord.objects.get_or_create(
|
||||
# create_by = user, create_time__hour__range = [8,18],
|
||||
# create_time__year=now_local.year, create_time__month=now_local.month,
|
||||
# create_time__day=now_local.day,
|
||||
# defaults={
|
||||
# 'type':ClockRecord.ClOCK_WORK1,
|
||||
# 'create_by':user,
|
||||
# 'create_time':now
|
||||
# })
|
||||
# # 设为在岗
|
||||
# if created:
|
||||
# Employee.objects.filter(user=user).update(is_atwork=True, last_check_time=now)
|
||||
|
||||
return Response({
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'username':user.username,
|
||||
'name':user.name
|
||||
})
|
||||
else:
|
||||
raise ParseError('账户不存在或不可用')
|
||||
raise ParseError(msg)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OpsConfig(AppConfig):
|
||||
name = 'apps.ops'
|
||||
verbose_name = '系统运维'
|
||||
|
|
@ -0,0 +1 @@
|
|||
LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from django_filters import rest_framework as filters
|
||||
|
||||
from apps.ops.models import DrfRequestLog, Tlog
|
||||
|
||||
|
||||
class DrfLogFilterSet(filters.FilterSet):
|
||||
start_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='gte')
|
||||
end_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = DrfRequestLog
|
||||
fields = ['id', 'start_request', 'end_request', 'status_code']
|
||||
|
||||
|
||||
class TlogFilterSet(filters.FilterSet):
|
||||
start_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='gte')
|
||||
end_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = Tlog
|
||||
fields = ['id', 'start_request', 'end_request', 'result']
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Generated by Django 3.2.12 on 2022-11-29 08:47
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tlog',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('target', models.CharField(max_length=20, verbose_name='请求目标')),
|
||||
('result', models.CharField(max_length=20, verbose_name='请求结果')),
|
||||
('path', models.CharField(help_text='请求地址', max_length=400)),
|
||||
('params', models.JSONField(blank=True, null=True)),
|
||||
('body', models.JSONField(blank=True, null=True)),
|
||||
('method', models.CharField(max_length=10)),
|
||||
('requested_at', models.DateTimeField()),
|
||||
('response_ms', models.PositiveIntegerField(default=0)),
|
||||
('headers', models.JSONField(blank=True, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('errors', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DrfRequestLog',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('requested_at', models.DateTimeField(db_index=True)),
|
||||
('response_ms', models.PositiveIntegerField(default=0)),
|
||||
('path', models.CharField(db_index=True, help_text='请求地址', max_length=400)),
|
||||
('view', models.CharField(blank=True, db_index=True, help_text='执行视图', max_length=400, null=True)),
|
||||
('view_method', models.CharField(blank=True, db_index=True, max_length=20, null=True)),
|
||||
('remote_addr', models.GenericIPAddressField()),
|
||||
('host', models.URLField()),
|
||||
('method', models.CharField(max_length=10)),
|
||||
('query_params', models.JSONField(blank=True, null=True)),
|
||||
('data', models.JSONField(blank=True, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('errors', models.TextField(blank=True, null=True)),
|
||||
('agent', models.TextField(blank=True, null=True)),
|
||||
('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
|
||||
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'DRF请求日志',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
|
||||
|
||||
class DrfRequestLog(models.Model):
|
||||
"""Logs Django rest framework API requests"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
user = models.ForeignKey(
|
||||
'system.user',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
requested_at = models.DateTimeField(db_index=True)
|
||||
response_ms = models.PositiveIntegerField(default=0)
|
||||
path = models.CharField(
|
||||
max_length=400,
|
||||
db_index=True,
|
||||
help_text="请求地址",
|
||||
)
|
||||
view = models.CharField(
|
||||
max_length=400,
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
help_text="执行视图",
|
||||
)
|
||||
view_method = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
blank=True,
|
||||
db_index=True,
|
||||
)
|
||||
remote_addr = models.GenericIPAddressField()
|
||||
host = models.URLField()
|
||||
method = models.CharField(max_length=10)
|
||||
query_params = models.JSONField(null=True, blank=True)
|
||||
data = models.JSONField(null=True, blank=True)
|
||||
response = models.JSONField(null=True, blank=True)
|
||||
errors = models.TextField(null=True, blank=True)
|
||||
agent = models.TextField(null=True, blank=True)
|
||||
status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "DRF请求日志"
|
||||
|
||||
def __str__(self):
|
||||
return "{} {}".format(self.method, self.path)
|
||||
|
||||
|
||||
class Tlog(models.Model):
|
||||
"""第三方请求与处理日志
|
||||
"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
target = models.CharField('请求目标', max_length=20)
|
||||
result = models.CharField('请求结果', max_length=20)
|
||||
path = models.CharField(max_length=400, help_text="请求地址")
|
||||
params = models.JSONField(null=True, blank=True)
|
||||
body = models.JSONField(null=True, blank=True)
|
||||
method = models.CharField(max_length=10)
|
||||
requested_at = models.DateTimeField()
|
||||
response_ms = models.PositiveIntegerField(default=0)
|
||||
headers = models.JSONField(null=True, blank=True)
|
||||
response = models.JSONField(null=True, blank=True)
|
||||
errors = models.TextField(null=True, blank=True)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import serializers
|
||||
from apps.ops.models import DrfRequestLog, Tlog
|
||||
|
||||
class DbbackupDeleteSerializer(serializers.Serializer):
|
||||
filepaths = serializers.ListField(child=serializers.CharField(), label="文件地址列表")
|
||||
|
||||
class MemDiskSerializer(serializers.Serializer):
|
||||
total = serializers.FloatField(label="总大小(GB)")
|
||||
used = serializers.FloatField(label="已用(GB)")
|
||||
percent = serializers.FloatField(label="百分比")
|
||||
|
||||
class CpuSerializer(serializers.Serializer):
|
||||
count = serializers.IntegerField(label='物理核心数')
|
||||
lcount = serializers.IntegerField(label="逻辑核心数")
|
||||
percent = serializers.FloatField(label="百分比")
|
||||
|
||||
|
||||
class DrfRequestLogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DrfRequestLog
|
||||
fields = '__all__'
|
||||
|
||||
class TlogSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Tlog
|
||||
fields = '__all__'
|
||||
|
||||
class TextListSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
filepath = serializers.CharField()
|
||||
size = serializers.CharField(label="MB")
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import psutil
|
||||
|
||||
class ServerService:
|
||||
@classmethod
|
||||
def get_memory_dict(cls):
|
||||
ret = {}
|
||||
memory = psutil.virtual_memory()
|
||||
ret['total'] = round(memory.total/1024/1024/1024, 2)
|
||||
ret['used'] = round(memory.used/1024/1024/1024, 2)
|
||||
ret['percent'] = memory.percent
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def get_cpu_dict(cls):
|
||||
ret = {}
|
||||
ret['lcount'] = psutil.cpu_count()
|
||||
ret['count'] = psutil.cpu_count(logical=False)
|
||||
ret['percent'] = psutil.cpu_percent(interval=1)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def get_disk_dict(cls):
|
||||
ret = {}
|
||||
disk = psutil.disk_usage('/')
|
||||
ret['total'] = round(disk.total/1024/1024/1024, 2)
|
||||
ret['used'] = round(disk.used/1024/1024/1024, 2)
|
||||
ret['percent'] = disk.percent
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def get_full(cls):
|
||||
return {'cpu': cls.get_cpu_dict(), 'memory': cls.get_memory_dict(), 'disk': cls.get_disk_dict()}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
# Create your tasks here
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import timedelta
|
||||
from apps.ops.models import DrfRequestLog
|
||||
from apps.utils.tasks import CustomTask
|
||||
from celery import shared_task
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
import subprocess
|
||||
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
|
||||
|
||||
|
||||
@shared_task
|
||||
def backup_database():
|
||||
"""
|
||||
备份数据库
|
||||
"""
|
||||
import datetime
|
||||
name = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
command = 'echo "{}" | sudo -S pg_dump "user={} password={} dbname={}" > {}/bak_{}.sql'.format(
|
||||
SD_PWD,
|
||||
DATABASES['default']['USER'],
|
||||
DATABASES['default']['PASSWORD'],
|
||||
DATABASES['default']['NAME'],
|
||||
BACKUP_PATH + '/database',
|
||||
name)
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
return completed.stderr
|
||||
|
||||
|
||||
@shared_task
|
||||
def reload_server_git():
|
||||
command = 'bash {}/git_server.sh'.format(SH_PATH)
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
return completed.stderr
|
||||
|
||||
|
||||
@shared_task
|
||||
def reload_web_git():
|
||||
command = 'bash {}/git_web.sh'.format(SH_PATH)
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
return completed.stderr
|
||||
|
||||
|
||||
@shared_task
|
||||
def reload_server_only():
|
||||
command = 'echo "{}" | sudo -S supervisorctl reload'.format(SD_PWD)
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
return completed
|
||||
|
||||
|
||||
@shared_task
|
||||
def backup_media():
|
||||
command = 'bash {}/backup_media.sh'.format(SH_PATH)
|
||||
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if completed.returncode != 0:
|
||||
return completed.stderr
|
||||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def clear_drf_log(days: int = 7):
|
||||
"""清除N天前的日志记录,默认七天
|
||||
|
||||
清除N天前的日志记录
|
||||
"""
|
||||
now = timezone.now()
|
||||
days7_ago = now - timedelta(days=days)
|
||||
DrfRequestLog.objects.filter(create_time__lte=days7_ago).delete()
|
||||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def clear_dbbackup(num: int = 7):
|
||||
"""
|
||||
清除N条前的数据库备份记录,默认七条
|
||||
|
||||
清除N条前的数据库备份记录
|
||||
"""
|
||||
from apps.ops.views import get_file_list
|
||||
backpath = settings.BACKUP_PATH + '/database'
|
||||
files = get_file_list(backpath)
|
||||
files_remove_list = files[num:]
|
||||
for f in files_remove_list:
|
||||
filepath = os.path.join(backpath, f)
|
||||
os.remove(filepath)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
from django.urls import path
|
||||
from apps.ops.views import (DrfRequestLogViewSet, CpuView, MemoryView, DiskView, DbBackupDeleteView,
|
||||
LogView, LogDetailView,
|
||||
DbBackupView, ReloadClientGit, ReloadServerGit, ReloadServerOnly,
|
||||
BackupDatabase, BackupMedia, TlogViewSet)
|
||||
|
||||
API_BASE_URL = 'api/ops/'
|
||||
HTML_BASE_URL = 'ops/'
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL + 'reload_server_git/', ReloadServerGit.as_view()),
|
||||
path(API_BASE_URL + 'reload_web_git/', ReloadClientGit.as_view()),
|
||||
path(API_BASE_URL + 'reload_server_only/', ReloadServerOnly.as_view()),
|
||||
path(API_BASE_URL + 'backup_database/', BackupDatabase.as_view()),
|
||||
path(API_BASE_URL + 'backup_media/', BackupMedia.as_view()),
|
||||
path(API_BASE_URL + 'log/', LogView.as_view()),
|
||||
path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()),
|
||||
path(API_BASE_URL + 'dbbackup/', DbBackupView.as_view()),
|
||||
path(API_BASE_URL + 'dbbackup/<str:filepath>/', DbBackupDeleteView.as_view()),
|
||||
path(API_BASE_URL + 'server/cpu/', CpuView.as_view()),
|
||||
path(API_BASE_URL + 'server/memory/', MemoryView.as_view()),
|
||||
path(API_BASE_URL + 'server/disk/', DiskView.as_view()),
|
||||
path(API_BASE_URL + 'request_log/', DrfRequestLogViewSet.as_view({'get': 'list'}), name='requestlog_view'),
|
||||
path(API_BASE_URL + 'tlog/', TlogViewSet.as_view({'get': 'list'}), name='tlog_view'),
|
||||
]
|
||||
|
|
@ -0,0 +1,255 @@
|
|||
|
||||
from django.shortcuts import render
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.conf import settings
|
||||
import os
|
||||
from apps.ops.serializers import DbbackupDeleteSerializer, MemDiskSerializer, CpuSerializer, DrfRequestLogSerializer, TlogSerializer, TextListSerializer
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.mixins import ListModelMixin
|
||||
from apps.ops.filters import DrfLogFilterSet, TlogFilterSet
|
||||
from apps.ops.models import DrfRequestLog, Tlog
|
||||
|
||||
from apps.ops.errors import LOG_NOT_FONED
|
||||
from apps.utils.viewsets import CustomGenericViewSet
|
||||
from rest_framework.exceptions import APIException
|
||||
from apps.ops.tasks import reload_server_git, reload_server_only, reload_web_git, backup_database, backup_media
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from apps.ops.service import ServerService
|
||||
from server.settings import BACKUP_PATH
|
||||
# Create your views here.
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'ops/index.html')
|
||||
|
||||
|
||||
def room(request, room_name):
|
||||
return render(request, 'ops/room.html', {
|
||||
'room_name': room_name
|
||||
})
|
||||
|
||||
|
||||
class ReloadServerGit(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(operation_summary="拉取后端代码并重启服务", responses=None, request_body=None)
|
||||
def post(self, request):
|
||||
reload_server_git.delay()
|
||||
return Response()
|
||||
# if completed.returncode == 0:
|
||||
# return Response()
|
||||
# else:
|
||||
# from server.settings import myLogger
|
||||
# myLogger.error(completed)
|
||||
# raise ParseError(completed.stderr)
|
||||
|
||||
|
||||
class ReloadClientGit(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(operation_summary="拉取前端代码并打包", responses=None, request_body=None)
|
||||
def post(self, request):
|
||||
reload_web_git.delay()
|
||||
return Response()
|
||||
# completed = reload_web_git()
|
||||
# if completed.returncode == 0:
|
||||
# return Response()
|
||||
# else:
|
||||
# raise APIException(completed.stdout)
|
||||
|
||||
|
||||
class ReloadServerOnly(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(operation_summary="仅重启服务", responses=None, request_body=None)
|
||||
def post(self, request):
|
||||
completed = reload_server_only()
|
||||
if completed.returncode == 0:
|
||||
return Response()
|
||||
else:
|
||||
raise APIException(completed.stdout)
|
||||
|
||||
|
||||
class BackupDatabase(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(operation_summary="备份数据库到指定位置", responses=None, request_body=None)
|
||||
def post(self, request):
|
||||
err_str = backup_database()
|
||||
if err_str:
|
||||
raise APIException(err_str)
|
||||
return Response()
|
||||
|
||||
|
||||
class BackupMedia(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
@swagger_auto_schema(operation_summary="备份资源到指定位置", responses=None, request_body=None)
|
||||
def post(self, request):
|
||||
err_str = backup_media()
|
||||
if err_str:
|
||||
raise APIException(err_str)
|
||||
return Response()
|
||||
|
||||
|
||||
class CpuView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(operation_summary="获取服务器cpu当前状态", responses=CpuSerializer, request_body=None)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(ServerService.get_cpu_dict())
|
||||
|
||||
|
||||
class MemoryView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(operation_summary="获取服务器内存当前状态", responses=MemDiskSerializer, request_body=None)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(ServerService.get_memory_dict())
|
||||
|
||||
|
||||
class DiskView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(operation_summary="获取服务器硬盘当前状态", responses=MemDiskSerializer, request_body=None)
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response(ServerService.get_disk_dict())
|
||||
|
||||
|
||||
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):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(operation_summary="查看最近的日志列表", responses=TextListSerializer(many=True), request_body=None)
|
||||
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/1024, 1)
|
||||
})
|
||||
else:
|
||||
fsize = os.path.getsize(filepath)
|
||||
if fsize:
|
||||
logs.append({
|
||||
"name": file,
|
||||
"filepath": filepath,
|
||||
"size": round(fsize/1024, 1)
|
||||
})
|
||||
return Response(logs)
|
||||
|
||||
|
||||
class LogDetailView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(operation_summary="查看日志详情", responses=None)
|
||||
def get(self, request, name):
|
||||
try:
|
||||
with open(os.path.join(settings.LOG_PATH, name)) as f:
|
||||
data = f.read()
|
||||
return Response(data)
|
||||
except Exception:
|
||||
raise NotFound(**LOG_NOT_FONED)
|
||||
|
||||
|
||||
class DbBackupDeleteView(APIView):
|
||||
perms_map = {'delete': 'dbback.delete'}
|
||||
|
||||
@swagger_auto_schema(operation_summary="删除备份", responses={204: None})
|
||||
def delete(self, request, filepath):
|
||||
if BACKUP_PATH in filepath:
|
||||
os.remove(filepath)
|
||||
return Response()
|
||||
|
||||
|
||||
class DbBackupView(APIView):
|
||||
perms_map = {'get': '*', 'post': 'dbback.delete'}
|
||||
|
||||
@swagger_auto_schema(operation_summary="批量删除备份", responses={204: None}, request_body=DbbackupDeleteSerializer)
|
||||
def post(self, request):
|
||||
filepaths = request.data.get('filepaths', [])
|
||||
for i in filepaths:
|
||||
if BACKUP_PATH in i:
|
||||
os.remove(i)
|
||||
return Response()
|
||||
|
||||
@swagger_auto_schema(operation_summary="查看最近的备份列表", responses=TextListSerializer(many=True), request_body=None)
|
||||
def get(self, request, *args, **kwargs):
|
||||
items = []
|
||||
name = request.GET.get('name', None)
|
||||
backpath = settings.BACKUP_PATH + '/database'
|
||||
for file in get_file_list(backpath):
|
||||
if len(items) > 50:
|
||||
break
|
||||
filepath = os.path.join(backpath, file)
|
||||
if name:
|
||||
if name in filepath:
|
||||
fsize = os.path.getsize(filepath)
|
||||
if fsize:
|
||||
items.append({
|
||||
"name": file,
|
||||
"filepath": filepath,
|
||||
"size": round(fsize/1024/1024, 1)
|
||||
})
|
||||
else:
|
||||
fsize = os.path.getsize(filepath)
|
||||
if fsize:
|
||||
items.append({
|
||||
"name": file,
|
||||
"filepath": filepath,
|
||||
"size": round(fsize/1024/1024, 1)
|
||||
})
|
||||
return Response(items)
|
||||
|
||||
|
||||
class DrfRequestLogViewSet(ListModelMixin, CustomGenericViewSet):
|
||||
"""list:请求日志
|
||||
|
||||
请求日志
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
queryset = DrfRequestLog.objects.all()
|
||||
list_serializer_class = DrfRequestLogSerializer
|
||||
ordering = ['-requested_at']
|
||||
filterset_class = DrfLogFilterSet
|
||||
search_fields = ['path', 'view']
|
||||
|
||||
|
||||
class TlogViewSet(ListModelMixin, CustomGenericViewSet):
|
||||
"""list:三方日志查看
|
||||
|
||||
三方日志查看
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
queryset = Tlog.objects.all()
|
||||
list_serializer_class = TlogSerializer
|
||||
ordering = ['-requested_at']
|
||||
filterset_class = TlogFilterSet
|
||||
search_fields = ['path']
|
||||
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'apps.system.apps.SystemConfig'
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from django.contrib import admin
|
||||
from .models import User, Dept, Role, Permission, DictType, Dictionary, 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(Dictionary)
|
||||
admin.site.register(File)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
from django.apps import AppConfig
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class SystemConfig(AppConfig):
|
||||
name = 'apps.system'
|
||||
verbose_name = '系统管理'
|
||||
|
||||
def ready(self) -> None:
|
||||
# 启动时重新加载系统配置json
|
||||
if cache.get('cache_sysconfig_need_task', True):
|
||||
from server.settings import get_sysconfig
|
||||
get_sysconfig(reload=True)
|
||||
cache.set('cache_sysconfig_need_task', False, timeout=30)
|
||||
return super().ready()
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
SCHEDULE_WRONG = {"code": "schedule_wrong", "detail": "时间策略有误"}
|
||||
PASSWORD_NOT_SAME = {"code": "password_not_same", "detail": "新旧密码不一致"}
|
||||
OLD_PASSWORD_WRONG = {"code": "old_password_wrong", "detail": "旧密码错误"}
|
||||
|
||||
FUNC_ERROR = {"code": "func_error", "detail": "执行方法有误"}
|
||||
|
||||
USERNAME_EXIST = {"code": "username_exist", "detail": "账户已存在"}
|
||||
ROLE_NAME_EXIST = {"code": "role_name_exist", "detail": "角色名已存在"}
|
||||
ROLE_CODE_EXIST = {"code": "role_code_exist", "detail": "角色标识已存在"}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from .models import Dept, User
|
||||
|
||||
|
||||
class UserFilterSet(filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
'name': ['exact', 'contains'],
|
||||
'is_deleted': ['exact'],
|
||||
'posts': ['exact'],
|
||||
'post': ['exact'],
|
||||
'belong_dept': ['exact'],
|
||||
'depts': ['exact'],
|
||||
'type': ['exact', 'in']
|
||||
}
|
||||
|
||||
|
||||
class DeptFilterSet(filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = {
|
||||
'type': ['exact', 'in']
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
# Generated by Django 3.2.12 on 2022-08-15 06:02
|
||||
|
||||
import apps.system.models
|
||||
from django.conf import settings
|
||||
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(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='删除标记')),
|
||||
('type', models.CharField(default='employee', max_length=10, verbose_name='账号类型')),
|
||||
('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='姓名')),
|
||||
('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')),
|
||||
('avatar', models.CharField(blank=True, default='/media/default/avatar.png', max_length=100, null=True, verbose_name='头像')),
|
||||
('secret', models.CharField(blank=True, max_length=100, null=True, verbose_name='密钥')),
|
||||
('wx_openid', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信公众号OpenId')),
|
||||
('wx_nickname', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信昵称')),
|
||||
('wx_headimg', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信头像')),
|
||||
('wxmp_openid', models.CharField(blank=True, max_length=100, null=True, verbose_name='微信小程序OpenId')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户信息',
|
||||
'verbose_name_plural': '用户信息',
|
||||
'ordering': ['create_time'],
|
||||
},
|
||||
managers=[
|
||||
('objects', apps.system.models.SoftDeletableUserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Dept',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=60, verbose_name='名称')),
|
||||
('type', models.CharField(default='dept', max_length=20, verbose_name='类型')),
|
||||
('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')),
|
||||
('third_info', models.JSONField(default=dict, 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(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(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, verbose_name='名称')),
|
||||
('code', models.CharField(blank=True, max_length=32, null=True, verbose_name='岗位标识')),
|
||||
('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')),
|
||||
('min_hour', models.PositiveSmallIntegerField(default=0, verbose_name='最小在岗时间')),
|
||||
('max_hour', models.PositiveSmallIntegerField(default=12, 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='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.post', verbose_name='父')),
|
||||
('update_by', 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='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '职位/岗位',
|
||||
'verbose_name_plural': '职位/岗位',
|
||||
'ordering': ['create_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPost',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(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, related_name='up_dept', to='system.dept')),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='up_post', to='system.post')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='up_user', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户岗位关系表',
|
||||
'verbose_name_plural': '用户岗位关系表',
|
||||
'ordering': ['sort', 'create_time'],
|
||||
'unique_together': {('user', 'post', 'dept')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=32, verbose_name='名称')),
|
||||
('code', models.CharField(blank=True, max_length=32, null=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.CreateModel(
|
||||
name='PostRole',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('data_range', models.PositiveSmallIntegerField(choices=[(10, '全部'), (30, '同级及以下'), (40, '本级及以下'), (50, '本级'), (60, '仅本人')], default=40, verbose_name='数据权限范围')),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.post', verbose_name='关联岗位')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.role', verbose_name='关联角色')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='File',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(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(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, 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='post',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.post', verbose_name='主要岗位'),
|
||||
),
|
||||
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='roles',
|
||||
field=models.ManyToManyField(to='system.Role', verbose_name='关联角色'),
|
||||
),
|
||||
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='Dictionary',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=60, verbose_name='名称')),
|
||||
('value', models.CharField(blank=True, max_length=10, null=True, 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='dictionary_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.dictionary', 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='dictionary_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,35 @@
|
|||
# Generated by Django 3.2.12 on 2023-03-09 05:09
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('django_celery_beat', '0016_alter_crontabschedule_timezone'),
|
||||
('system', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MySchedule',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||
('type', models.PositiveSmallIntegerField(default=10, verbose_name='周期类型')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='myschedule_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('crontab', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='django_celery_beat.crontabschedule')),
|
||||
('interval', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='django_celery_beat.intervalschedule')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='myschedule_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
from django.contrib.auth.models import UserManager
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from apps.utils.models import CommonADModel, CommonAModel, CommonBModel, BaseModel, SoftDeletableManagerMixin
|
||||
from django_celery_beat.models import IntervalSchedule, CrontabSchedule
|
||||
|
||||
|
||||
class DataFilter(models.IntegerChoices):
|
||||
ALL = 10, '全部'
|
||||
SAMELEVE_AND_BELOW = 30, '同级及以下'
|
||||
THISLEVEL_AND_BELOW = 40, '本级及以下'
|
||||
THISLEVEL = 50, '本级'
|
||||
MYSELF = 60, '仅本人'
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
部门
|
||||
"""
|
||||
name = models.CharField('名称', max_length=60)
|
||||
type = models.CharField('类型', max_length=20, default='dept')
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
sort = models.PositiveSmallIntegerField('排序标记', default=1)
|
||||
third_info = models.JSONField('三方系统信息', default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '部门'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Role(CommonADModel):
|
||||
"""
|
||||
角色
|
||||
"""
|
||||
name = models.CharField('名称', max_length=32)
|
||||
code = models.CharField('角色标识', max_length=32, 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(CommonADModel):
|
||||
"""
|
||||
职位/岗位
|
||||
"""
|
||||
name = models.CharField('名称', max_length=32)
|
||||
code = models.CharField('岗位标识', max_length=32, null=True, blank=True)
|
||||
description = models.CharField('描述', max_length=50, blank=True, null=True)
|
||||
parent = models.ForeignKey('self', null=True, blank=True,
|
||||
on_delete=models.SET_NULL, verbose_name='父')
|
||||
min_hour = models.PositiveSmallIntegerField('最小在岗时间', default=0)
|
||||
max_hour = models.PositiveSmallIntegerField('最长在岗时间', default=12)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '职位/岗位'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['create_time']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PostRole(BaseModel):
|
||||
"""
|
||||
岗位角色关系
|
||||
"""
|
||||
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices,
|
||||
default=DataFilter.THISLEVEL_AND_BELOW)
|
||||
post = models.ForeignKey(Post, verbose_name='关联岗位', on_delete=models.CASCADE)
|
||||
role = models.ForeignKey(Role, verbose_name='关联角色', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager):
|
||||
pass
|
||||
|
||||
|
||||
class User(AbstractUser, CommonBModel):
|
||||
"""
|
||||
用户
|
||||
"""
|
||||
type = models.CharField('账号类型', max_length=10, default='employee')
|
||||
name = models.CharField('姓名', max_length=20, null=True, blank=True)
|
||||
phone = models.CharField('手机号', max_length=11, null=True, blank=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='上级主管')
|
||||
post = models.ForeignKey(Post, verbose_name='主要岗位', on_delete=models.SET_NULL,
|
||||
null=True, blank=True)
|
||||
posts = models.ManyToManyField(Post, through='system.userpost', related_name='user_posts')
|
||||
depts = models.ManyToManyField(Dept, through='system.userpost')
|
||||
roles = models.ManyToManyField(Role, verbose_name='关联角色')
|
||||
|
||||
# 关联账号
|
||||
secret = models.CharField('密钥', max_length=100, null=True, blank=True)
|
||||
wx_openid = models.CharField('微信公众号OpenId', max_length=100, null=True, blank=True)
|
||||
wx_nickname = models.CharField('微信昵称', max_length=100, null=True, blank=True)
|
||||
wx_headimg = models.CharField('微信头像', max_length=100, null=True, blank=True)
|
||||
wxmp_openid = models.CharField('微信小程序OpenId', max_length=100, null=True, blank=True)
|
||||
|
||||
objects = SoftDeletableUserManager()
|
||||
|
||||
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, related_name='up_user')
|
||||
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='up_post')
|
||||
dept = models.ForeignKey(Dept, on_delete=models.CASCADE, related_name='up_dept')
|
||||
sort = models.PositiveSmallIntegerField('排序', default=1)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '用户岗位关系表'
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort', 'create_time']
|
||||
unique_together = ('user', 'post', 'dept')
|
||||
|
||||
|
||||
class DictType(CommonAModel):
|
||||
"""
|
||||
数据字典类型
|
||||
"""
|
||||
name = models.CharField('名称', max_length=30)
|
||||
code = models.CharField('标识', 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 Dictionary(CommonAModel):
|
||||
"""
|
||||
数据字典
|
||||
"""
|
||||
name = models.CharField('名称', max_length=60)
|
||||
value = models.CharField('值', max_length=10, null=True, blank=True)
|
||||
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
|
||||
|
||||
|
||||
class MySchedule(CommonAModel):
|
||||
"""
|
||||
常用周期
|
||||
"""
|
||||
MS_TYPE = (
|
||||
(10, '间隔'),
|
||||
(20, '定时')
|
||||
)
|
||||
name = models.CharField('名称', max_length=200)
|
||||
type = models.PositiveSmallIntegerField('周期类型', default=10)
|
||||
interval = models.ForeignKey(IntervalSchedule, on_delete=models.PROTECT, null=True, blank=True)
|
||||
crontab = models.ForeignKey(CrontabSchedule, on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
|
||||
from django_celery_beat.models import PeriodicTask, CrontabSchedule, IntervalSchedule
|
||||
from rest_framework import serializers
|
||||
from django_celery_results.models import TaskResult
|
||||
from apps.hrm.errors import PHONE_EXIST
|
||||
from apps.system.errors import USERNAME_EXIST
|
||||
from apps.system.services import sync_dahua_dept
|
||||
from apps.utils.fields import MyFilePathField
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE
|
||||
from apps.utils.tools import check_phone_e
|
||||
from .models import (Dictionary, DictType, File, Dept, MySchedule, Permission, Post, PostRole,
|
||||
Role, User, UserPost)
|
||||
from rest_framework.exceptions import ParseError, ValidationError
|
||||
from django.db import transaction
|
||||
from apps.third.tapis import dhapis
|
||||
from rest_framework.validators import UniqueValidator
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
# from django_q.models import Task as QTask, Schedule as QSchedule
|
||||
|
||||
|
||||
# class QScheduleSerializer(CustomModelSerializer):
|
||||
# success = serializers.SerializerMethodField()
|
||||
|
||||
# class Meta:
|
||||
# model = QSchedule
|
||||
# fields = '__all__'
|
||||
|
||||
# def get_success(self, obj):
|
||||
# return obj.success()
|
||||
|
||||
|
||||
# class QTaskResultSerializer(CustomModelSerializer):
|
||||
# args = serializers.SerializerMethodField()
|
||||
# kwargs = serializers.SerializerMethodField()
|
||||
# result = serializers.SerializerMethodField()
|
||||
|
||||
# class Meta:
|
||||
# model = QTask
|
||||
# fields = '__all__'
|
||||
|
||||
# def get_args(self, obj):
|
||||
# return obj.args
|
||||
|
||||
# def get_kwargs(self, obj):
|
||||
# return obj.kwargs
|
||||
|
||||
# def get_result(self, obj):
|
||||
# return obj.result
|
||||
|
||||
class TaskRunSerializer(serializers.Serializer):
|
||||
sync = serializers.BooleanField(default=True)
|
||||
|
||||
|
||||
class IntervalSerializer(CustomModelSerializer):
|
||||
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__'
|
||||
|
||||
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 PTaskResultSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = TaskResult
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
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 = Dictionary
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DictSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Dictionary
|
||||
fields = ['id', 'name', 'code']
|
||||
|
||||
|
||||
class DictCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
数据字典序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Dictionary
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PostSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class PostCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Post
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
def create(self, validated_data):
|
||||
if Post.objects.filter(name=validated_data['name']).exists():
|
||||
raise ValidationError('该岗位已存在')
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if Post.objects.filter(name=validated_data['name']).exclude(id=instance.id).exists():
|
||||
raise ValidationError('该岗位已存在')
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
角色序列化
|
||||
"""
|
||||
name = serializers.CharField(label="名称", validators=[
|
||||
UniqueValidator(queryset=Role.objects.all(), message='已存在相同名称的角色')])
|
||||
code = serializers.CharField(label="标识", validators=[
|
||||
UniqueValidator(queryset=Role.objects.all(), message='已存在相同标识的角色')])
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
exclude = EXCLUDE_FIELDS
|
||||
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
权限序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class PermissionCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
权限序列化
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Permission
|
||||
exclude = EXCLUDE_FIELDS_BASE
|
||||
|
||||
|
||||
class DeptSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = ['id', 'name', 'type']
|
||||
|
||||
|
||||
class DeptSerializer(CustomModelSerializer):
|
||||
"""
|
||||
组织架构序列化
|
||||
"""
|
||||
class Meta:
|
||||
model = Dept
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class DeptCreateUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
部门序列化
|
||||
"""
|
||||
parent = serializers.PrimaryKeyRelatedField(queryset=Dept.objects.all(), required=True)
|
||||
|
||||
class Meta:
|
||||
model = Dept
|
||||
exclude = EXCLUDE_FIELDS + ['third_info']
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
ins = super().create(validated_data)
|
||||
sync_dahua_dept(ins)
|
||||
return ins
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
ins = super().update(instance, validated_data)
|
||||
sync_dahua_dept(ins)
|
||||
return ins
|
||||
|
||||
|
||||
class UserSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'phone']
|
||||
|
||||
|
||||
class UserSignatureSerializer(CustomModelSerializer):
|
||||
signature = serializers.CharField(source='employee.signature', read_only=True)
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'phone', 'signature']
|
||||
|
||||
|
||||
class UserListSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户列表序列化
|
||||
"""
|
||||
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
|
||||
post_name = serializers.CharField(source='post.name', read_only=True)
|
||||
# posts_ = PostSimpleSerializer(source='posts', many=True)
|
||||
avatar_f = MyFilePathField(source='avatar', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ['password', 'secret']
|
||||
|
||||
|
||||
def phone_exist(phone):
|
||||
if User.objects.filter(phone=phone).exists():
|
||||
raise serializers.ValidationError(**PHONE_EXIST)
|
||||
|
||||
|
||||
def user_exist(username):
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise serializers.ValidationError(**USERNAME_EXIST)
|
||||
return username
|
||||
|
||||
|
||||
class UserUpdateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
用户编辑序列化
|
||||
"""
|
||||
phone = serializers.CharField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'name', 'avatar', 'phone', 'type', 'is_deleted']
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if User.objects.filter(username=validated_data['username']
|
||||
).exclude(id=instance.id).exists():
|
||||
raise ParseError(**USERNAME_EXIST)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class UserCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
创建用户序列化
|
||||
"""
|
||||
username = serializers.CharField(required=True, validators=[user_exist])
|
||||
phone = serializers.CharField(required=False, validators=[phone_exist])
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'name', 'avatar', 'phone', 'type']
|
||||
|
||||
|
||||
class PasswordChangeSerializer(serializers.Serializer):
|
||||
old_password = serializers.CharField(label="原密码")
|
||||
new_password1 = serializers.CharField(label="新密码1")
|
||||
new_password2 = serializers.CharField(label="新密码2")
|
||||
|
||||
|
||||
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 UserPostCreateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = UserPost
|
||||
exclude = EXCLUDE_FIELDS_BASE
|
||||
|
||||
def create(self, validated_data):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class PostRoleSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位-角色序列化
|
||||
"""
|
||||
post_ = PostSimpleSerializer(source='post', read_only=True)
|
||||
role_ = RoleSimpleSerializer(source='role', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PostRole
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class PostRoleCreateSerializer(CustomModelSerializer):
|
||||
"""
|
||||
岗位-角色创建序列化
|
||||
"""
|
||||
class Meta:
|
||||
model = PostRole
|
||||
fields = ['post', 'role', 'data_range']
|
||||
|
||||
|
||||
class UserInfoSerializer(CustomModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'name', 'post', 'avatar', 'belong_dept', 'type']
|
||||
|
||||
|
||||
class ApkSerializer(serializers.Serializer):
|
||||
version = serializers.CharField(label='版本号')
|
||||
file = serializers.CharField(label='文件地址')
|
||||
|
||||
|
||||
class IntervalScheduleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = IntervalSchedule
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CrontabScheduleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CrontabSchedule
|
||||
exclude = ['timezone']
|
||||
|
||||
|
||||
class MyScheduleCreateSerializer(CustomModelSerializer):
|
||||
interval_ = IntervalScheduleSerializer(allow_null=True, required=False)
|
||||
crontab_ = CrontabScheduleSerializer(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = MySchedule
|
||||
fields = ['type', 'interval_', 'crontab_']
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['type'] == 10 and attrs.get('interval_', None):
|
||||
pass
|
||||
elif attrs['type'] == 20 and attrs.get('crontab_', None):
|
||||
pass
|
||||
else:
|
||||
raise ValidationError('信息有误')
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class MyScheduleSerializer(CustomModelSerializer):
|
||||
interval_ = IntervalScheduleSerializer(source='interval', read_only=True)
|
||||
crontab = CrontabScheduleSerializer(source='crontab', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = MySchedule
|
||||
fields = '__all__'
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from apps.system.models import Dept
|
||||
from django.conf import settings
|
||||
from apps.third.tapis import dhapis
|
||||
from apps.third.dahua import dhClient
|
||||
|
||||
|
||||
def sync_dahua_dept(dept: Dept):
|
||||
# 同步大华部门信息
|
||||
third_info = dept.third_info
|
||||
if settings.DAHUA_ENABLED:
|
||||
if third_info.get('dh_id', False):
|
||||
data = {
|
||||
"id": dept.third_info['dh_id'],
|
||||
"parentId": 1,
|
||||
"name": dept.name
|
||||
}
|
||||
dhClient.request(**dhapis['dept_update'], json=data)
|
||||
else:
|
||||
# 如果dh_id 不存在
|
||||
data = {
|
||||
"parentId": 1,
|
||||
"name": dept.name,
|
||||
"service": "ehs"
|
||||
}
|
||||
_, res = dhClient.request(**dhapis['dept_create'], json=data)
|
||||
third_info['dh_id'] = res['id']
|
||||
dept.third_info = third_info
|
||||
dept.save()
|
||||
dhClient.face_bind()
|
||||
|
|
@ -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,18 @@
|
|||
# Create your tasks here
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import timedelta
|
||||
from apps.utils.tasks import CustomTask
|
||||
from celery import shared_task
|
||||
from django_celery_results.models import TaskResult
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def cleanup_dcr():
|
||||
"""清空三十日前的定时任务执行记录
|
||||
|
||||
清空三十日前的定时任务执行记录
|
||||
"""
|
||||
now = timezone.now()
|
||||
days30_ago = now - timedelta(days=30)
|
||||
TaskResult.objects.filter(periodic_task_name__isnull=False, date_done__lte=days30_ago).delete()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
from email.mime import base
|
||||
from django.urls import path, include
|
||||
from .views import ApkViewSet, FileViewSet, PTaskViewSet, PTaskResultViewSet, PostRoleViewSet, TaskList, \
|
||||
UserPostViewSet, UserViewSet, DeptViewSet, \
|
||||
PermissionViewSet, RoleViewSet, PostViewSet, \
|
||||
DictTypeViewSet, DictViewSet, SysConfigView, SysBaseConfigView
|
||||
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('qschedule', QScheduleViewSet, basename="qschedule")
|
||||
# router.register('qtask_result', QTaskResultViewSet, basename="qtask_result")
|
||||
router.register('user_post', UserPostViewSet, basename='user_post')
|
||||
router.register('post_role', PostRoleViewSet, basename='post_role')
|
||||
router.register('apk', ApkViewSet, basename='apk')
|
||||
|
||||
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_BASE_URL + 'base_config/', SysBaseConfigView.as_view()),
|
||||
path(API_BASE_URL + 'config/', SysConfigView.as_view()),
|
||||
path('api/', include(router2.urls)),
|
||||
]
|
||||
|
|
@ -0,0 +1,761 @@
|
|||
import configparser
|
||||
import os
|
||||
import importlib
|
||||
import json
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from django.db import transaction
|
||||
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
|
||||
PeriodicTask)
|
||||
from django_celery_results.models import TaskResult
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
|
||||
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
|
||||
ListModelMixin, RetrieveModelMixin)
|
||||
from rest_framework.parsers import (JSONParser,
|
||||
MultiPartParser)
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from apps.hrm.models import Employee
|
||||
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
|
||||
from apps.system.filters import DeptFilterSet, UserFilterSet
|
||||
# from django_q.models import Task as QTask, Schedule as QSchedule
|
||||
from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin)
|
||||
from django.conf import settings
|
||||
from apps.utils.permission import ALL_PERMS, get_user_perms_map
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from server.celery import app as celery_app
|
||||
from .models import (Dept, Dictionary, DictType, File, Permission, Post, PostRole, Role, User,
|
||||
UserPost, MySchedule)
|
||||
from .serializers import (ApkSerializer, DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer,
|
||||
DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer,
|
||||
FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer,
|
||||
PermissionSerializer, PostCreateUpdateSerializer, PostRoleCreateSerializer,
|
||||
PostRoleSerializer, PostSerializer,
|
||||
PTaskSerializer, PTaskCreateUpdateSerializer, PTaskResultSerializer,
|
||||
RoleCreateUpdateSerializer, RoleSerializer, TaskRunSerializer,
|
||||
UserCreateSerializer, UserListSerializer, UserPostCreateSerializer,
|
||||
UserPostSerializer, UserUpdateSerializer, MyScheduleCreateSerializer, MyScheduleSerializer)
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from cron_descriptor import get_description
|
||||
import locale
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from server.settings import get_sysconfig, update_sysconfig
|
||||
|
||||
# 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, request):
|
||||
"""获取注册任务列表
|
||||
|
||||
获取注册任务列表
|
||||
"""
|
||||
tasks = list(
|
||||
sorted(name for name in celery_app.tasks if not name.startswith('celery.')))
|
||||
return Response(tasks)
|
||||
|
||||
|
||||
# class QScheduleViewSet(CustomModelViewSet):
|
||||
# """
|
||||
# list:定时任务列表
|
||||
|
||||
# 定时任务列表
|
||||
|
||||
# retrieve:定时任务详情
|
||||
|
||||
# 定时任务详情
|
||||
# """
|
||||
# queryset = QSchedule.objects.all()
|
||||
# serializer_class = QScheduleSerializer
|
||||
# search_fields = ['name', 'func']
|
||||
# filterset_fields = ['schedule_type']
|
||||
# ordering = ['-pk']
|
||||
|
||||
# @action(methods=['get'], detail=True, perms_map={'post': 'qschedule:run_once'})
|
||||
# def run_once(self, request, pk=None):
|
||||
# """同步执行一次
|
||||
|
||||
# 同步执行一次
|
||||
# """
|
||||
# obj = self.get_object()
|
||||
# module, func = obj.func.rsplit(".", 1)
|
||||
# m = importlib.import_module(module)
|
||||
# f = getattr(m, func)
|
||||
# f(*obj.args.split(','), **eval(f"dict({obj.kwargs})"))
|
||||
# return Response()
|
||||
|
||||
|
||||
# class QTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||
# """
|
||||
# list:任务执行结果列表
|
||||
|
||||
# 任务执行结果列表
|
||||
|
||||
# retrieve:任务执行结果详情
|
||||
|
||||
# 任务执行结果详情
|
||||
# """
|
||||
# perms_map = {'get': '*'}
|
||||
# filterset_fields = ['func']
|
||||
# queryset = QTask.objects.all()
|
||||
# serializer_class = QTaskResultSerializer
|
||||
# ordering = ['-started']
|
||||
# lookup_field = 'id'
|
||||
|
||||
class PTaskViewSet(CustomModelViewSet):
|
||||
"""
|
||||
list:定时任务列表
|
||||
|
||||
定时任务列表
|
||||
|
||||
retrieve:定时任务详情
|
||||
|
||||
定时任务详情
|
||||
"""
|
||||
queryset = PeriodicTask.objects.exclude(name__contains='celery.')
|
||||
serializer_class = PTaskSerializer
|
||||
create_serializer_class = PTaskCreateUpdateSerializer
|
||||
update_serializer_class = PTaskCreateUpdateSerializer
|
||||
partial_update_serializer_class = PTaskCreateUpdateSerializer
|
||||
search_fields = ['name', 'task']
|
||||
filterset_fields = ['enabled']
|
||||
select_related_fields = ['interval', 'crontab']
|
||||
ordering = ['-id']
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'get': 'qtask.run_once'},
|
||||
serializer_class=TaskRunSerializer)
|
||||
def run_once(self, request, pk=None):
|
||||
"""执行一次
|
||||
|
||||
执行一次
|
||||
"""
|
||||
obj = self.get_object()
|
||||
module, func = obj.task.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
if request.data.get('sync', True):
|
||||
f(*json.loads(obj.args), **json.loads(obj.kwargs))
|
||||
return Response()
|
||||
else:
|
||||
task_obj = f.delay(*json.loads(obj.args), **json.loads(obj.kwargs))
|
||||
return Response({'task_id': task_obj.id})
|
||||
|
||||
@action(methods=['put'], detail=True, perms_map={'put': 'ptask.update'})
|
||||
def toggle(self, request, pk=None):
|
||||
"""修改启用禁用状态
|
||||
|
||||
修改启用禁用状态
|
||||
"""
|
||||
obj = self.get_object()
|
||||
obj.enabled = False if obj.enabled else True
|
||||
obj.save()
|
||||
return Response()
|
||||
|
||||
@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 Exception:
|
||||
raise ParseError(**SCHEDULE_WRONG)
|
||||
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 Exception:
|
||||
raise ParseError(**SCHEDULE_WRONG)
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response()
|
||||
|
||||
@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 Exception:
|
||||
raise ParseError(**SCHEDULE_WRONG)
|
||||
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 Exception:
|
||||
raise ParseError(**SCHEDULE_WRONG)
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
list:任务执行结果列表
|
||||
|
||||
任务执行结果列表
|
||||
|
||||
retrieve:任务执行结果详情
|
||||
|
||||
任务执行结果详情
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
filterset_fields = ['task_name', 'periodic_task_name', 'status']
|
||||
queryset = TaskResult.objects.all()
|
||||
serializer_class = PTaskResultSerializer
|
||||
ordering = ['-date_created']
|
||||
lookup_field = 'task_id'
|
||||
|
||||
|
||||
class DictTypeViewSet(CustomModelViewSet):
|
||||
"""数据字典类型-增删改查
|
||||
|
||||
数据字典类型-增删改查
|
||||
"""
|
||||
queryset = DictType.objects.all()
|
||||
serializer_class = DictTypeSerializer
|
||||
create_serializer_class = DictTypeCreateUpdateSerializer
|
||||
update_serializer_class = DictTypeCreateUpdateSerializer
|
||||
partial_update_serializer_class = DictTypeCreateUpdateSerializer
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
class DictViewSet(CustomModelViewSet):
|
||||
"""数据字典-增删改查
|
||||
|
||||
数据字典-增删改查
|
||||
"""
|
||||
# queryset = Dictionary.objects.get_queryset(all=True) # 获取全部的,包括软删除的
|
||||
queryset = Dictionary.objects.all()
|
||||
filterset_fields = ['type', 'is_used', 'type__code']
|
||||
serializer_class = DictSerializer
|
||||
create_serializer_class = DictCreateUpdateSerializer
|
||||
update_serializer_class = DictCreateUpdateSerializer
|
||||
partial_update_serializer_class = DictCreateUpdateSerializer
|
||||
search_fields = ['name']
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
|
||||
class PostViewSet(CustomModelViewSet):
|
||||
"""岗位-增删改查
|
||||
|
||||
岗位-增删改查
|
||||
"""
|
||||
queryset = Post.objects.all()
|
||||
serializer_class = PostSerializer
|
||||
create_serializer_class = PostCreateUpdateSerializer
|
||||
update_serializer_class = PostCreateUpdateSerializer
|
||||
partial_update_serializer_class = PostCreateUpdateSerializer
|
||||
search_fields = ['name', 'code', 'description']
|
||||
ordering = ['create_time']
|
||||
|
||||
|
||||
class PermissionViewSet(CustomModelViewSet):
|
||||
"""菜单权限-增删改查
|
||||
|
||||
菜单权限-增删改查
|
||||
"""
|
||||
queryset = Permission.objects.all()
|
||||
filterset_fields = ['type']
|
||||
serializer_class = PermissionSerializer
|
||||
create_serializer_class = PermissionCreateUpdateSerializer
|
||||
update_serializer_class = PermissionCreateUpdateSerializer
|
||||
partial_update_serializer_class = PermissionCreateUpdateSerializer
|
||||
search_fields = ['name', 'code']
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def codes(self, request, pk=None):
|
||||
"""获取全部权限标识
|
||||
|
||||
需要先请求一次swagger
|
||||
"""
|
||||
ALL_PERMS.sort()
|
||||
return Response(ALL_PERMS)
|
||||
|
||||
|
||||
class DeptViewSet(CustomModelViewSet):
|
||||
"""部门-增删改查
|
||||
|
||||
部门-增删改查
|
||||
"""
|
||||
queryset = Dept.objects.all()
|
||||
serializer_class = DeptSerializer
|
||||
create_serializer_class = DeptCreateUpdateSerializer
|
||||
update_serializer_class = DeptCreateUpdateSerializer
|
||||
partial_update_serializer_class = DeptCreateUpdateSerializer
|
||||
filterset_class = DeptFilterSet
|
||||
search_fields = ['name']
|
||||
ordering = ['type', 'sort', 'create_time']
|
||||
|
||||
# def filter_queryset(self, queryset):
|
||||
# if not self.detail:
|
||||
# self.request.query_params._mutable = True
|
||||
# self.request.query_params.setdefault('type', 'dept')
|
||||
# return super().filter_queryset(queryset)
|
||||
|
||||
# def get_queryset(self):
|
||||
# type = self.request.query_params.get('type', None)
|
||||
# if type:
|
||||
# queryset = Dept.objects.filter(type='rparty')
|
||||
# else:
|
||||
# queryset = Dept.objects.filter(type__in=['dept', 'company'])
|
||||
# return queryset
|
||||
|
||||
|
||||
class RoleViewSet(CustomModelViewSet):
|
||||
"""角色-增删改查
|
||||
|
||||
角色-增删改查
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
create_serializer_class = RoleCreateUpdateSerializer
|
||||
update_serializer_class = RoleCreateUpdateSerializer
|
||||
partial_update_serializer_class = RoleCreateUpdateSerializer
|
||||
search_fields = ['name', 'code']
|
||||
ordering = ['create_time']
|
||||
|
||||
|
||||
class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
|
||||
"""岗位/角色关系
|
||||
|
||||
岗位/角色关系
|
||||
"""
|
||||
perms_map = {'get': '*', 'post': 'post.update', 'delete': 'post.update'}
|
||||
queryset = PostRole.objects.select_related('post', 'role').all()
|
||||
serializer_class = PostRoleSerializer
|
||||
create_serializer_class = PostRoleCreateSerializer
|
||||
filterset_fields = ['post', 'role']
|
||||
|
||||
|
||||
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
|
||||
create_serializer_class = UserPostCreateSerializer
|
||||
filterset_fields = ['user', 'post', 'dept']
|
||||
ordering = ['sort', 'create_time']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
with transaction.atomic():
|
||||
instance = serializer.save()
|
||||
user = instance.user
|
||||
up = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
|
||||
if up:
|
||||
user.belong_dept = up.dept
|
||||
user.post = up.post
|
||||
user.update_by = self.request.user
|
||||
user.save()
|
||||
# 更新人员表
|
||||
ep = Employee.objects.get_queryset(all=True).filter(user=user).first()
|
||||
if ep:
|
||||
ep.belong_dept = user.belong_dept
|
||||
ep.post = user.post
|
||||
ep.is_deleted = False
|
||||
ep.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
with transaction.atomic():
|
||||
user = instance.user
|
||||
instance.delete()
|
||||
up = UserPost.objects.filter(user=user).order_by('sort', 'create_time').first()
|
||||
if up:
|
||||
user.belong_dept = up.dept
|
||||
user.post = up.post
|
||||
else:
|
||||
user.belong_dept = None
|
||||
user.post = None
|
||||
user.update_by = self.request.user
|
||||
user.save()
|
||||
# 更新人员表
|
||||
ep = Employee.objects.get_queryset(all=True).filter(user=user).first()
|
||||
if ep:
|
||||
ep.belong_dept = user.belong_dept
|
||||
ep.post = user.post
|
||||
ep.is_deleted = False
|
||||
ep.save()
|
||||
|
||||
|
||||
class UserViewSet(CustomModelViewSet):
|
||||
queryset = User.objects.get_queryset(all=True)
|
||||
serializer_class = UserListSerializer
|
||||
create_serializer_class = UserCreateSerializer
|
||||
update_serializer_class = UserUpdateSerializer
|
||||
filterset_class = UserFilterSet
|
||||
search_fields = ['username', 'name', 'phone', 'email', 'id']
|
||||
select_related_fields = ['superior', 'belong_dept', 'post']
|
||||
prefetch_related_fields = ['posts', 'roles', 'depts']
|
||||
ordering = ['create_time', 'type']
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'GET' and (not self.request.query_params.get('is_deleted', None)):
|
||||
self.queryset = User.objects.all()
|
||||
return super().get_queryset()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
instance = serializer.save()
|
||||
ep = Employee.objects.get_queryset(all=True).filter(user=instance).first()
|
||||
ep2 = Employee.objects.get_queryset(all=True).filter(phone=instance.phone).first()
|
||||
if ep:
|
||||
pass
|
||||
elif ep2:
|
||||
ep = ep2
|
||||
else:
|
||||
ep = Employee()
|
||||
ep.user = instance
|
||||
ep.name = instance.name
|
||||
ep.phone = instance.phone
|
||||
ep.type = instance.type
|
||||
ep.is_deleted = False
|
||||
ep.save()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建用户
|
||||
|
||||
创建用户
|
||||
"""
|
||||
password = make_password('abc!0000')
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
instance = serializer.save(password=password, belong_dept=None)
|
||||
ep = Employee.objects.get_queryset(all=True).filter(user=instance).first()
|
||||
ep2 = Employee.objects.get_queryset(all=True).filter(phone=instance.phone).first()
|
||||
if ep:
|
||||
pass
|
||||
elif ep2:
|
||||
ep = ep2
|
||||
else:
|
||||
ep = Employee()
|
||||
ep.user = instance
|
||||
ep.name = instance.name
|
||||
ep.phone = instance.phone
|
||||
ep.type = instance.type
|
||||
ep.is_deleted = False
|
||||
ep.save()
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(methods=['put'], detail=False,
|
||||
permission_classes=[IsAuthenticated],
|
||||
serializer_class=PasswordChangeSerializer)
|
||||
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:
|
||||
if new_password1 == old_password:
|
||||
raise ParseError('新密码不得与旧密码相同')
|
||||
user.set_password(new_password2)
|
||||
user.save()
|
||||
return Response()
|
||||
else:
|
||||
raise ParseError(**PASSWORD_NOT_SAME)
|
||||
else:
|
||||
raise ValidationError(**OLD_PASSWORD_WRONG)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=Serializer)
|
||||
def reset_password(self, request, pk=None):
|
||||
user = self.get_object()
|
||||
if request.user.is_superuser:
|
||||
user.set_password('abc!0000')
|
||||
user.save()
|
||||
else:
|
||||
raise PermissionDenied()
|
||||
return Response()
|
||||
|
||||
@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,
|
||||
'type': user.type,
|
||||
'name': user.name,
|
||||
'roles': user.roles.values_list('name', flat=True),
|
||||
'avatar': user.avatar,
|
||||
'perms': perms,
|
||||
'belong_dept': user.belong_dept.id if user.belong_dept else None,
|
||||
'post': user.post.id if user.post else None,
|
||||
'belong_dept_name': user.belong_dept.name if user.belong_dept else '',
|
||||
'post_name': user.post.name if user.post else '',
|
||||
'is_superuser': user.is_superuser,
|
||||
'wxmp_openid': user.wxmp_openid,
|
||||
'wx_openid': user.wx_openid
|
||||
}
|
||||
return Response(data)
|
||||
|
||||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def bind_wxmp(self, request, pk=None):
|
||||
"""
|
||||
绑定微信小程序
|
||||
|
||||
绑定微信小程序
|
||||
"""
|
||||
openid = request.data['openid']
|
||||
if openid:
|
||||
user = request.user
|
||||
if user.wxmp_openid != openid:
|
||||
User.objects.filter(wxmp_openid=openid).update(wxmp_openid=None)
|
||||
user.wxmp_openid = openid
|
||||
user.save()
|
||||
return Response({'wxmp_openid': openid})
|
||||
|
||||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def unbind_wxmp(self, request, pk=None):
|
||||
"""
|
||||
解绑微信小程序
|
||||
|
||||
解绑微信小程序
|
||||
"""
|
||||
user = request.user
|
||||
user.wxmp_openid = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def bind_wx(self, request, pk=None):
|
||||
"""绑定微信公众号
|
||||
|
||||
绑定微信公众号, 用于发送通知
|
||||
"""
|
||||
openid = request.data['openid']
|
||||
if openid:
|
||||
user = request.user
|
||||
if user.wx_openid != openid:
|
||||
User.objects.filter(wx_openid=openid).update(wx_openid=None)
|
||||
user.wx_openid = openid
|
||||
user.save()
|
||||
return Response({'wx_openid': openid})
|
||||
|
||||
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
||||
def bind_secret(self, request, pk=None):
|
||||
"""创建密钥
|
||||
|
||||
创建密钥
|
||||
"""
|
||||
secret = request.data['secret']
|
||||
if secret:
|
||||
user = request.user
|
||||
user.secret = secret
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
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']
|
||||
cache_seconds = 0
|
||||
|
||||
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 = File.FILE_TYPE_OTHER
|
||||
if 'image' in mime:
|
||||
file_type = File.FILE_TYPE_PIC
|
||||
elif 'video' in mime:
|
||||
file_type = File.FILE_TYPE_VIDEO
|
||||
elif 'audio' in mime:
|
||||
file_type = File.FILE_TYPE_AUDIO
|
||||
elif 'application' or 'text' in mime:
|
||||
file_type = File.FILE_TYPE_DOC
|
||||
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()
|
||||
|
||||
|
||||
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
|
||||
perms_map = {'get': '*', 'post': 'apk.upload'}
|
||||
serializer_class = ApkSerializer
|
||||
|
||||
def get_authenticators(self):
|
||||
if self.request.method == 'GET':
|
||||
return []
|
||||
return super().get_authenticators()
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method == 'GET':
|
||||
return [AllowAny()]
|
||||
return super().get_permissions()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
"""
|
||||
获取apk信息
|
||||
|
||||
获取apk信息
|
||||
"""
|
||||
config = get_sysconfig()
|
||||
return Response({'version': config['apk']['apk_version'], 'file': config['apk']['apk_file']})
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
上传apk
|
||||
|
||||
上传apk
|
||||
"""
|
||||
sr = ApkSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
update_sysconfig({
|
||||
"apk":{
|
||||
"apk_version": vdata['version'],
|
||||
"apk_file": vdata['file']
|
||||
}
|
||||
})
|
||||
return Response()
|
||||
|
||||
|
||||
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
||||
perms_map = {'get': '*', 'post': 'myschedule.create', 'delete': 'myschedule.delete'}
|
||||
serializer_class = MyScheduleSerializer
|
||||
create_serializer_class = MyScheduleCreateSerializer
|
||||
queryset = MySchedule.objects.all()
|
||||
select_related_fields = ['interval', 'crontab']
|
||||
period_dict = {
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"seconds": "秒",
|
||||
"microseconds": "毫秒"
|
||||
}
|
||||
def get_chinese_description(self, type:str = 'interval', data: dict = {}):
|
||||
"""转换为汉语描述
|
||||
"""
|
||||
if type == 'interval':
|
||||
return f"每隔{data['every']}{data['period']}"
|
||||
elif type == 'crontab':
|
||||
locale.setlocale(locale.LC_ALL, 'zh_CN.UTF-8')
|
||||
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
|
||||
return ''
|
||||
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
vdata = serializer.validated_data
|
||||
vdata['create_by'] = self.request.user #不可少
|
||||
interval_data = vdata.pop('interval_', None)
|
||||
crontab_data = vdata.pop('crontab_', None)
|
||||
if vdata['type'] == 10:
|
||||
interval, _ = IntervalSchedule.objects.get_or_create(**interval_data, defaults=interval_data)
|
||||
obj = MySchedule(**vdata)
|
||||
obj.name = self.get_chinese_description('interval', vdata)
|
||||
obj.interval = interval
|
||||
obj.save()
|
||||
elif vdata['type'] == 20:
|
||||
crontab_data['timezone'] = 'Asia/Shanghai'
|
||||
crontab, _ = CrontabSchedule.objects.get_or_create(**crontab_data, defaults=crontab_data)
|
||||
obj = MySchedule(**vdata)
|
||||
obj.name = self.get_chinese_description('crontab', vdata)
|
||||
obj.crontab = crontab
|
||||
obj.save()
|
||||
|
||||
|
||||
class SysBaseConfigView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
read_keys = ['base', 'apk']
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
获取系统基本信息
|
||||
|
||||
获取系统基本信息
|
||||
"""
|
||||
config = get_sysconfig()
|
||||
base_dict = {key: config[key] for key in self.read_keys if key in config}
|
||||
return Response(base_dict)
|
||||
|
||||
class SysConfigView(MyLoggingMixin, APIView):
|
||||
perms_map = {'get': 'sysconfig.view', 'put': 'sysconfig.update'}
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
获取config json
|
||||
|
||||
获取config json
|
||||
"""
|
||||
reload = False
|
||||
if request.query_params.get('reload', None):
|
||||
reload = True
|
||||
return Response(get_sysconfig(reload=reload))
|
||||
|
||||
@swagger_auto_schema(request_body=Serializer)
|
||||
def put(self, request, format=None):
|
||||
"""
|
||||
修改config json
|
||||
|
||||
修改config json
|
||||
"""
|
||||
data = request.data
|
||||
update_sysconfig(data)
|
||||
return Response()
|
||||
|
|
@ -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,5 @@
|
|||
from django.db import models
|
||||
|
||||
EXCLUDE_FIELDS_BASE = ['create_time', 'update_time', 'is_deleted']
|
||||
EXCLUDE_FIELDS = ['create_time', 'update_time', 'is_deleted', 'create_by', 'update_by']
|
||||
EXCLUDE_FIELDS_DEPT = EXCLUDE_FIELDS + ['belong_dept']
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import logging
|
||||
from functools import wraps
|
||||
from apps.utils.tasks import send_mail_task
|
||||
import traceback
|
||||
import json
|
||||
from django.core.cache import cache
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
def auto_log(name='', raise_exception=True, send_mail=False):
|
||||
def decorate(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
real_func = func(*args, **kwargs)
|
||||
return real_func
|
||||
except Exception:
|
||||
myLogger.error(name, exc_info=True)
|
||||
if send_mail:
|
||||
send_mail_task.delay(message=traceback.format_exc())
|
||||
if raise_exception:
|
||||
raise
|
||||
return wrapper
|
||||
return decorate
|
||||
|
||||
|
||||
def idempotent(seconds=4):
|
||||
def decorate(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
rdata = args[1].data
|
||||
rdata['request_userid'] = getattr(args[1], 'user').id
|
||||
rdata['request_path'] = getattr(args[1], 'path')
|
||||
hash_k = hash(json.dumps(rdata))
|
||||
hash_v_e = cache.get(hash_k, None)
|
||||
if hash_v_e is None:
|
||||
cache.set(hash_k, 'o', seconds)
|
||||
real_func = func(*args, **kwargs)
|
||||
# real_func.render()
|
||||
# cache.set(hash_k, real_func, seconds)
|
||||
return real_func
|
||||
elif hash_v_e == 'o': # 说明请求正在处理
|
||||
raise ParseError(f'请求忽略,请{seconds}秒后重试')
|
||||
return wrapper
|
||||
return decorate
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
SIGN_MAKE_FAIL = {"code": "sign_make_fail", "detail": "签名照生成失败,请重新上传"}
|
||||
PKS_ERROR = {"code": "pks_error", "detail": "未获取到主键列表"}
|
||||
WX_REQUEST_ERROR = {"code": "wx_request_error", "detail": "微信接口访问异常"}
|
||||
XX_REQUEST_ERROR = {"code": "xx_request_error", "detail": "寻息接口访问异常"}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import traceback
|
||||
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.http import Http404
|
||||
import logging
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import set_rollback
|
||||
import json
|
||||
from apps.utils.tasks import send_mail_task
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
def custom_exception_hander(exc, context):
|
||||
"""
|
||||
自定义异常处理
|
||||
"""
|
||||
if isinstance(exc, Http404):
|
||||
exc = exceptions.NotFound()
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
exc = exceptions.PermissionDenied()
|
||||
elif isinstance(exc, ValidationError):
|
||||
exc = exceptions.ValidationError(exc.message)
|
||||
|
||||
request_id = getattr(context['request'], 'request_id', None)
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
headers = {}
|
||||
if getattr(exc, 'auth_header', None):
|
||||
headers['WWW-Authenticate'] = exc.auth_header
|
||||
if getattr(exc, 'wait', None):
|
||||
headers['Retry-After'] = '%d' % exc.wait
|
||||
data = {'err_detail': exc.detail}
|
||||
if isinstance(exc.detail, dict):
|
||||
data['err_code'] = exc.default_code
|
||||
data['err_msg'] = json.dumps(exc.detail, ensure_ascii=False) if 'detail' not in exc.detail else exc.detail['detail'] # 取一部分方便前端alert
|
||||
elif isinstance(exc.detail, list):
|
||||
data['err_code'] = exc.default_code
|
||||
data['err_msg'] = json.dumps(exc.detail, ensure_ascii=False)
|
||||
else:
|
||||
data = {'err_msg': exc.detail, 'err_code': exc.get_codes()}
|
||||
|
||||
set_rollback()
|
||||
data['request_id'] = request_id
|
||||
status = exc.status_code
|
||||
if status not in [401, 404]:
|
||||
status = 400
|
||||
return Response(data, status=status, headers=headers)
|
||||
args = (request_id, traceback.format_exc())
|
||||
err_detail = f"{args[0]}-{args[1]}"
|
||||
myLogger.error(err_detail)
|
||||
if settings.DEBUG is False:
|
||||
send_mail_task.delay(message=err_detail) # 500邮件通知到开发人员
|
||||
return Response(data={'err_code': 'server_error', 'err_detail': err_detail if settings.DEBUG else None, 'err_msg': '服务器错误'}, status=500)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import xlwt
|
||||
import time
|
||||
import os
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
from openpyxl import Workbook, styles
|
||||
from openpyxl.drawing.image import Image
|
||||
from openpyxl.utils import get_column_letter, column_index_from_string
|
||||
|
||||
|
||||
def len_byte(value):
|
||||
# 获取字符串长度,一个中文的长度为2
|
||||
length = len(value)
|
||||
utf8_length = len(value.encode('utf-8'))
|
||||
length = (utf8_length - length) / 2 + length
|
||||
return int(length)
|
||||
|
||||
|
||||
def export_excel(field_data: list, data: list, FileName: str):
|
||||
"""
|
||||
Excel导出
|
||||
:param data: 数据源
|
||||
:param field_data: 首行数据源(表头)
|
||||
:param file_path: 文件保存路径(默认保存在media路径)
|
||||
:param FileName: 文件保存名字
|
||||
:return:返回文件的下载url完整路径
|
||||
"""
|
||||
wbk = xlwt.Workbook(encoding='utf-8')
|
||||
sheet = wbk.add_sheet('Sheet1', cell_overwrite_ok=True) # 第二参数用于确认同一个cell单元是否可以重设值。
|
||||
style = xlwt.XFStyle() # 赋值style为XFStyle(),初始化样式
|
||||
# 设置居中
|
||||
wbk.set_colour_RGB(0x23, 0, 60, 139)
|
||||
xlwt.add_palette_colour("custom_colour_35", 0x23)
|
||||
tab_al = xlwt.Alignment()
|
||||
tab_al.horz = 0x02 # 设置水平居中
|
||||
tab_al.vert = 0x01 # 设置垂直居中
|
||||
# 设置表头单元格背景颜色
|
||||
tab_pattern = xlwt.Pattern() # 创建一个模式
|
||||
tab_pattern.pattern = xlwt.Pattern.SOLID_PATTERN # 设置其模式为实型
|
||||
tab_pattern.pattern_fore_colour = 55
|
||||
# 设置单元格内字体样式
|
||||
tab_fnt = xlwt.Font() # 创建一个文本格式,包括字体、字号和颜色样式特性
|
||||
tab_fnt.height = 200
|
||||
default_width = 14
|
||||
tab_fnt.name = u'楷体' # 设置其字体为微软雅黑
|
||||
tab_fnt.colour_index = 1 # 设置其字体颜色
|
||||
# 设置单元格下框线样式
|
||||
tab_borders = xlwt.Borders()
|
||||
tab_borders.left = xlwt.Borders.THIN
|
||||
tab_borders.right = xlwt.Borders.THIN
|
||||
tab_borders.top = xlwt.Borders.THIN
|
||||
tab_borders.bottom = xlwt.Borders.THIN
|
||||
tab_borders.left_colour = 23
|
||||
tab_borders.right_colour = 23
|
||||
tab_borders.bottom_colour = 23
|
||||
tab_borders.top_colour = 23
|
||||
# 把数据写入excel中
|
||||
# 所有表格单元格样式
|
||||
# 先生成表头
|
||||
style.alignment = tab_al # 设置居中
|
||||
style.pattern = tab_pattern # 设置表头单元格背景颜色
|
||||
style.font = tab_fnt # 设置单元格内字体样式
|
||||
style.borders = tab_borders
|
||||
for index, ele in enumerate(field_data):
|
||||
sheet.write_merge(0, 0, index, index, ele, style) # (列开始, 列结束, 行开始, 行结束, '数据内容')
|
||||
|
||||
# 确定栏位宽度
|
||||
col_width = []
|
||||
for index, ele in enumerate(data):
|
||||
for inx, values in enumerate(ele):
|
||||
if index == 0:
|
||||
col_width.append(len_byte(str(values)))
|
||||
else:
|
||||
if col_width[inx] < len_byte(str(values)):
|
||||
col_width[inx] = len_byte(str(values))
|
||||
# 设置栏位宽度,栏位宽度小于10时候采用默认宽度
|
||||
for i in range(len(col_width)):
|
||||
if col_width[i] > 10:
|
||||
width = col_width[i] if col_width[i] < 36 else 36
|
||||
sheet.col(i).width = 256 * (width + 6)
|
||||
else:
|
||||
sheet.col(i).width = 256 * (default_width)
|
||||
|
||||
row = 1
|
||||
# 内容背景颜色
|
||||
left_pattern = xlwt.Pattern() # 创建一个模式
|
||||
left_pattern.pattern = xlwt.Pattern.SOLID_PATTERN # 设置其模式为实型
|
||||
left_pattern.pattern_fore_colour = 1
|
||||
|
||||
# 设置单元格内字体样式
|
||||
left_fnt = xlwt.Font() # 创建一个文本格式,包括字体、字号和颜色样式特性
|
||||
left_fnt.height = 200
|
||||
left_fnt.name = u'楷体' # 设置其字体为微软雅黑
|
||||
left_fnt.colour_index = 0 # 设置其字体颜色
|
||||
|
||||
left_style = style
|
||||
left_style.pattern = left_pattern
|
||||
left_style.font = left_fnt
|
||||
|
||||
for results in data:
|
||||
for index, values in enumerate(results):
|
||||
sheet.write(row, index, label=values, style=left_style)
|
||||
row += 1
|
||||
|
||||
FileNameF = FileName + datetime.now().strftime('%Y%m%d%H%M%S') + '.xls'
|
||||
path = '/media/temp/'
|
||||
pathRoot = settings.BASE_DIR + path
|
||||
if not os.path.exists(pathRoot):
|
||||
os.makedirs(pathRoot)
|
||||
|
||||
path_name = os.path.join(pathRoot, FileNameF)
|
||||
wbk.save(path_name)
|
||||
return path + FileNameF
|
||||
|
||||
|
||||
def export_excel_img(field_data: list, data: list, FileName: str):
|
||||
"""
|
||||
带有image的Excel导出
|
||||
:param data: 数据源
|
||||
:param field_data: 首行数据源(表头){'name':'', 'type':''}
|
||||
:param img_field_indexs: 图片字段名index列表
|
||||
:param file_path: 文件保存路径(默认保存在media路径)
|
||||
:param FileName: 文件保存名字
|
||||
:return:返回文件的下载url完整路径
|
||||
"""
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
imgs = []
|
||||
|
||||
for index, value in enumerate(field_data):
|
||||
cell = ws.cell(column=index+1, row=1)
|
||||
cell.value = value['name']
|
||||
cell.font = styles.Font(bold=True)
|
||||
letter = get_column_letter(index+1)
|
||||
value['letter'] = letter
|
||||
ws.column_dimensions[letter].width = 10 # 修改列宽
|
||||
if value['type'] == 'img':
|
||||
ws.column_dimensions[letter].width = 15 # 修改列宽
|
||||
|
||||
for i1, v1 in enumerate(data):
|
||||
for i2, v2 in enumerate(v1):
|
||||
cell = ws.cell(column=i2+1, row=i1+2)
|
||||
if v2 and field_data[i2]['type'] == 'img':
|
||||
ws.row_dimensions[i1+2].height = 70
|
||||
try:
|
||||
img = Image(settings.BASE_DIR + v2)
|
||||
img.width, img.height = (90, 90)
|
||||
imgs.append((img, field_data[i2]['letter'] + str(i1+2)))
|
||||
except: # 这里先不做处理
|
||||
pass
|
||||
else:
|
||||
cell.value = v2
|
||||
|
||||
for i in imgs:
|
||||
ws.add_image(i[0], i[1])
|
||||
|
||||
FileNameF = FileName + datetime.now().strftime('%Y%m%d%H%M%S') + '.xlsx'
|
||||
path = '/temp/'
|
||||
pathRoot = settings.BASE_DIR + path
|
||||
if not os.path.exists(pathRoot):
|
||||
os.makedirs(pathRoot)
|
||||
|
||||
path_name = os.path.join(pathRoot, FileNameF)
|
||||
wb.save(path_name)
|
||||
return path + FileNameF
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class MyFilePathField(serializers.CharField):
|
||||
|
||||
def to_representation(self, value):
|
||||
if 'http' in value:
|
||||
return str(value)
|
||||
return settings.BASE_URL + str(value)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import os
|
||||
from PIL import Image
|
||||
|
||||
def compress_image(infile, outfile='', kb=40, quality=80):
|
||||
"""不改变图片尺寸压缩到指定大小
|
||||
:param infile: 压缩源文件
|
||||
:param outfile: 压缩文件保存地址
|
||||
:param kb: 压缩目标, KB
|
||||
:param step: 每次调整的压缩比率
|
||||
:param quality: 初始压缩比率
|
||||
:return: 压缩文件地址,压缩文件大小
|
||||
"""
|
||||
o_size = os.path.getsize(infile)/1024
|
||||
if o_size <= kb:
|
||||
return infile
|
||||
if outfile == '':
|
||||
path, end = infile.split('.')
|
||||
outfile = path + '_compressed.' + end
|
||||
im = Image.open(infile)
|
||||
im.save(outfile, quality=quality)
|
||||
while os.path.getsize(outfile) / 1024 > kb:
|
||||
imx = Image.open(outfile)
|
||||
# Resize the image using the same aspect ratio to reduce the file size
|
||||
width, height = imx.size
|
||||
new_width = int(width * 0.9) # You can adjust the scaling factor
|
||||
new_height = int(height * 0.9)
|
||||
imx = imx.resize((new_width, new_height), Image.ANTIALIAS)
|
||||
imx.save(outfile, quality=quality)
|
||||
# quality -= step
|
||||
|
||||
return outfile, os.path.getsize(outfile) / 1024
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
@sync_to_async
|
||||
def _get_user(token: str):
|
||||
jwt = JWTAuthentication()
|
||||
return jwt.get_user(jwt.get_validated_token(token))
|
||||
|
||||
class TokenAuthMiddleware:
|
||||
def __init__(self, app) -> None:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
# Look up user from query string (you should also do things like
|
||||
# checking if it is a valid user ID, or if scope["user"] is already
|
||||
# populated).
|
||||
from urllib.parse import parse_qs
|
||||
token = parse_qs(str(scope["query_string"], 'UTF-8')).get('token', [None])[0]
|
||||
if token:
|
||||
user = await _get_user(token)
|
||||
if user:
|
||||
scope['user'] = user
|
||||
return await self.app(scope, receive, send)
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
import uuid
|
||||
from rest_framework.mixins import CreateModelMixin, UpdateModelMixin, DestroyModelMixin
|
||||
import ast
|
||||
import ipaddress
|
||||
import traceback
|
||||
from apps.ops.models import DrfRequestLog
|
||||
from django.db import connection
|
||||
from django.utils.timezone import now
|
||||
from user_agents import parse
|
||||
import logging
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from rest_framework.exceptions import ParseError, ValidationError
|
||||
from apps.utils.errors import PKS_ERROR
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from apps.utils.serializers import PkSerializer
|
||||
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
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 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 BulkCreateModelMixin(CreateModelMixin):
|
||||
|
||||
def after_bulk_create(self, objs):
|
||||
pass
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""创建(支持批量)
|
||||
|
||||
创建(支持批量)
|
||||
"""
|
||||
rdata = request.data
|
||||
many = False
|
||||
if isinstance(rdata, list):
|
||||
many = True
|
||||
with transaction.atomic():
|
||||
sr = self.get_serializer(data=rdata, many=many)
|
||||
sr.is_valid(raise_exception=True)
|
||||
self.perform_create(sr)
|
||||
if many:
|
||||
self.after_bulk_create(sr.data)
|
||||
return Response(sr.data, status=201)
|
||||
|
||||
|
||||
class BulkUpdateModelMixin(UpdateModelMixin):
|
||||
|
||||
def after_bulk_update(self, objs):
|
||||
pass
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
"""部分更新(支持批量)
|
||||
|
||||
部分更新(支持批量)
|
||||
"""
|
||||
kwargs['partial'] = True
|
||||
return self.update(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""更新(支持批量)
|
||||
|
||||
更新(支持批量)
|
||||
"""
|
||||
partial = kwargs.pop('partial', False)
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
if kwargs[lookup_url_kwarg] == 'bulk': # 如果是批量操作
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
objs = []
|
||||
if isinstance(request.data, list):
|
||||
with transaction.atomic():
|
||||
for ind, item in enumerate(request.data):
|
||||
obj = get_object_or_404(queryset, id=item['id'])
|
||||
sr = self.get_serializer(obj, data=item, partial=partial)
|
||||
if not sr.is_valid():
|
||||
err_dict = { f'第{ind+1}': sr.errors}
|
||||
raise ValidationError(err_dict)
|
||||
self.perform_update(sr) # 用自带的更新,可能需要做其他操作
|
||||
objs.append(sr.data)
|
||||
self.after_bulk_update(objs)
|
||||
else:
|
||||
raise ParseError('提交数据非列表')
|
||||
return Response(objs)
|
||||
else:
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class BulkDestroyModelMixin(DestroyModelMixin):
|
||||
|
||||
@swagger_auto_schema(request_body=PkSerializer)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""删除(支持批量)
|
||||
|
||||
删除(支持批量和硬删除(需管理员权限))
|
||||
"""
|
||||
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
||||
if kwargs[lookup_url_kwarg] == 'bulk': # 如果是批量操作
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
ids = request.data.get('ids', None)
|
||||
soft = request.data.get('soft', True)
|
||||
if not soft and not request.user.is_superuser:
|
||||
raise ParseError('非管理员不支持物理删除')
|
||||
if ids:
|
||||
if soft is True:
|
||||
queryset.filter(id__in=ids).delete()
|
||||
elif soft is False:
|
||||
try:
|
||||
queryset.model.objects.get_queryset(
|
||||
all=True).filter(id__in=ids).delete(soft=False)
|
||||
except Exception:
|
||||
queryset.filter(id__in=ids).delete()
|
||||
return Response(status=204)
|
||||
else:
|
||||
raise ValidationError(**PKS_ERROR)
|
||||
else:
|
||||
instance = self.get_object()
|
||||
self.perform_destroy(instance)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class MyLoggingMixin(object):
|
||||
"""Mixin to log requests"""
|
||||
|
||||
CLEANED_SUBSTITUTE = "********************"
|
||||
|
||||
# logging_methods = "__all__"
|
||||
logging_methods = '__all__'
|
||||
sensitive_fields = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
assert isinstance(
|
||||
self.CLEANED_SUBSTITUTE, str
|
||||
), "CLEANED_SUBSTITUTE must be a string."
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
request_id = uuid.uuid4()
|
||||
self.log = {"requested_at": now(), "id": request_id}
|
||||
setattr(request, 'request_id', request_id)
|
||||
if not getattr(self, "decode_request_body", False):
|
||||
self.log["data"] = ""
|
||||
else:
|
||||
self.log["data"] = self._clean_data(request.body)
|
||||
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
# Accessing request.data *for the first time* parses the request body, which may raise
|
||||
# ParseError and UnsupportedMediaType exceptions. It's important not to swallow these,
|
||||
# as (depending on implementation details) they may only get raised this once, and
|
||||
# DRF logic needs them to be raised by the view for error handling to work correctly.
|
||||
data = self.request.data.dict()
|
||||
except AttributeError:
|
||||
data = self.request.data
|
||||
self.log["data"] = self._clean_data(data)
|
||||
|
||||
def handle_exception(self, exc):
|
||||
response = super().handle_exception(exc)
|
||||
self.log["errors"] = traceback.format_exc()
|
||||
return response
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(
|
||||
request, response, *args, **kwargs
|
||||
)
|
||||
# Ensure backward compatibility for those using _should_log hook
|
||||
should_log = (
|
||||
self._should_log if hasattr(self, "_should_log") else self.should_log
|
||||
)
|
||||
if should_log(request, response):
|
||||
if (connection.settings_dict.get("ATOMIC_REQUESTS") and
|
||||
getattr(response, "exception", None) and connection.in_atomic_block):
|
||||
# response with exception (HTTP status like: 401, 404, etc)
|
||||
# pointwise disable atomic block for handle log (TransactionManagementError)
|
||||
connection.set_rollback(True)
|
||||
connection.set_rollback(False)
|
||||
if response.streaming:
|
||||
rendered_content = None
|
||||
elif hasattr(response, "rendered_content"):
|
||||
rendered_content = response.rendered_content
|
||||
else:
|
||||
rendered_content = response.getvalue()
|
||||
|
||||
self.log.update(
|
||||
{
|
||||
"remote_addr": self._get_ip_address(request),
|
||||
"view": self._get_view_name(request),
|
||||
"view_method": self._get_view_method(request),
|
||||
"path": self._get_path(request),
|
||||
"host": request.get_host(),
|
||||
"method": request.method,
|
||||
"query_params": self._clean_data(request.query_params.dict()),
|
||||
"user": self._get_user(request),
|
||||
"response_ms": self._get_response_ms(),
|
||||
"response": self._clean_data(rendered_content),
|
||||
"status_code": response.status_code,
|
||||
"agent": self._get_agent(request),
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.handle_log()
|
||||
except Exception:
|
||||
# ensure that all exceptions raised by handle_log
|
||||
# doesn't prevent API call to continue as expected
|
||||
myLogger.exception("Logging API call raise exception!")
|
||||
return response
|
||||
|
||||
def handle_log(self):
|
||||
"""
|
||||
Hook to define what happens with the log.
|
||||
|
||||
Defaults on saving the data on the db.
|
||||
"""
|
||||
DrfRequestLog(**self.log).save()
|
||||
|
||||
def _get_path(self, request):
|
||||
"""Get the request path and truncate it"""
|
||||
return request.path
|
||||
|
||||
def _get_ip_address(self, request):
|
||||
"""Get the remote ip address the request was generated from."""
|
||||
ipaddr = request.META.get("HTTP_X_FORWARDED_FOR", None)
|
||||
if ipaddr:
|
||||
ipaddr = ipaddr.split(",")[0]
|
||||
else:
|
||||
ipaddr = request.META.get("REMOTE_ADDR", "")
|
||||
|
||||
# Account for IPv4 and IPv6 addresses, each possibly with port appended. Possibilities are:
|
||||
# <ipv4 address>
|
||||
# <ipv6 address>
|
||||
# <ipv4 address>:port
|
||||
# [<ipv6 address>]:port
|
||||
# Note that ipv6 addresses are colon separated hex numbers
|
||||
possibles = (ipaddr.lstrip("[").split("]")[0], ipaddr.split(":")[0])
|
||||
|
||||
for addr in possibles:
|
||||
try:
|
||||
return str(ipaddress.ip_address(addr))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return ipaddr
|
||||
|
||||
def _get_view_name(self, request):
|
||||
"""Get view name."""
|
||||
method = request.method.lower()
|
||||
try:
|
||||
attributes = getattr(self, method)
|
||||
return (
|
||||
type(attributes.__self__).__module__ + "." + type(attributes.__self__).__name__
|
||||
)
|
||||
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def _get_view_method(self, request):
|
||||
"""Get view method."""
|
||||
if hasattr(self, "action"):
|
||||
return self.action or None
|
||||
return request.method.lower()
|
||||
|
||||
def _get_user(self, request):
|
||||
"""Get user."""
|
||||
user = request.user
|
||||
if user.is_anonymous:
|
||||
return None
|
||||
return user
|
||||
|
||||
def _get_agent(self, request):
|
||||
"""Get os string"""
|
||||
return str(parse(request.META['HTTP_USER_AGENT']))
|
||||
|
||||
def _get_response_ms(self):
|
||||
"""
|
||||
Get the duration of the request response cycle is milliseconds.
|
||||
In case of negative duration 0 is returned.
|
||||
"""
|
||||
response_timedelta = now() - self.log["requested_at"]
|
||||
response_ms = int(response_timedelta.total_seconds() * 1000)
|
||||
return max(response_ms, 0)
|
||||
|
||||
def should_log(self, request, response):
|
||||
"""
|
||||
Method that should return a value that evaluated to True if the request should be logged.
|
||||
By default, check if the request method is in logging_methods.
|
||||
"""
|
||||
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
||||
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
|
||||
|
||||
def _clean_data(self, data):
|
||||
"""
|
||||
Clean a dictionary of data of potentially sensitive info before
|
||||
sending to the database.
|
||||
Function based on the "_clean_credentials" function of django
|
||||
(https://github.com/django/django/blob/stable/1.11.x/django/contrib/auth/__init__.py#L50)
|
||||
|
||||
Fields defined by django are by default cleaned with this function
|
||||
|
||||
You can define your own sensitive fields in your view by defining a set
|
||||
eg: sensitive_fields = {'field1', 'field2'}
|
||||
"""
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode(errors="replace")
|
||||
|
||||
if isinstance(data, list):
|
||||
return [self._clean_data(d) for d in data]
|
||||
|
||||
if isinstance(data, dict):
|
||||
SENSITIVE_FIELDS = {
|
||||
"api",
|
||||
"token",
|
||||
"key",
|
||||
"secret",
|
||||
"password",
|
||||
"signature",
|
||||
}
|
||||
|
||||
data = dict(data)
|
||||
if self.sensitive_fields:
|
||||
SENSITIVE_FIELDS = SENSITIVE_FIELDS | {
|
||||
field.lower() for field in self.sensitive_fields
|
||||
}
|
||||
|
||||
for key, value in data.items():
|
||||
try:
|
||||
value = ast.literal_eval(value)
|
||||
except (ValueError, SyntaxError):
|
||||
pass
|
||||
if isinstance(value, (list, dict)):
|
||||
data[key] = self._clean_data(value)
|
||||
if key.lower() in SENSITIVE_FIELDS:
|
||||
data[key] = self.CLEANED_SUBSTITUTE
|
||||
return data
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
import time
|
||||
import django.utils.timezone as timezone
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from apps.utils.snowflake import idWorker
|
||||
from django.db import IntegrityError
|
||||
|
||||
# 自定义软删除查询基类
|
||||
|
||||
|
||||
class SoftDeletableQuerySetMixin(object):
|
||||
'''
|
||||
QuerySet for SoftDeletableModel. Instead of removing instance sets
|
||||
its ``is_deleted`` field to True.
|
||||
'''
|
||||
|
||||
def delete(self, soft=True):
|
||||
'''
|
||||
Soft delete objects from queryset (set their ``is_deleted``
|
||||
field to True)
|
||||
'''
|
||||
if soft:
|
||||
self.update(is_deleted=True)
|
||||
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,
|
||||
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) -> None:
|
||||
# 出现了雪花ID重复, 先这样异常处理一下;已经修改了snowflake, 以防万一, 这里依然保留
|
||||
gen_id = False
|
||||
if not self.id:
|
||||
gen_id = True
|
||||
self.id = idWorker.get_id()
|
||||
try:
|
||||
return super().save(*args, **kwargs)
|
||||
except IntegrityError as e:
|
||||
if gen_id:
|
||||
time.sleep(0.01)
|
||||
self.id = idWorker.get_id()
|
||||
return super().save(*args, **kwargs)
|
||||
raise e
|
||||
|
||||
|
||||
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')
|
||||
# delete_by = models.ForeignKey(
|
||||
# 'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
# verbose_name='删除人', related_name='%(class)s_delete_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')
|
||||
# delete_by = models.ForeignKey(
|
||||
# 'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
# verbose_name='删除人', related_name='%(class)s_delete_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')
|
||||
# delete_by = models.ForeignKey(
|
||||
# 'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
# verbose_name='删除人', related_name='%(class)s_delete_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')
|
||||
# delete_by = models.ForeignKey(
|
||||
# 'system.user', null=True, blank=True, on_delete=models.SET_NULL,
|
||||
# verbose_name='删除人', related_name='%(class)s_delete_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 Smslog(BaseModel):
|
||||
# """
|
||||
# 短信发送记录表
|
||||
# """
|
||||
# phone = models.CharField('号码')
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import base64
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA
|
||||
from Crypto.Signature import PKCS1_v1_5 as PKCS1_signature
|
||||
from Crypto.Cipher import PKCS1_v1_5 as PKCS1_cipher
|
||||
|
||||
|
||||
def encrypt_data(msg, pub_key):
|
||||
pub_key = '-----BEGIN RSA PUBLIC KEY-----\n'+pub_key+'\n-----END RSA PUBLIC KEY-----'
|
||||
public_key = RSA.importKey(pub_key)
|
||||
cipher = PKCS1_cipher.new(public_key)
|
||||
encrypt_text = base64.b64encode(cipher.encrypt(bytes(msg.encode("utf8"))))
|
||||
return encrypt_text.decode('utf-8')
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
from configparser import ConfigParser
|
||||
import os
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class MyConfig:
|
||||
|
||||
def __init__(self, config_file, encode="utf-8"):
|
||||
if os.path.exists(config_file):
|
||||
self.__cfg_file = config_file
|
||||
else:
|
||||
# 此处做其他异常处理或创建配置文件操作
|
||||
raise OSError("配置文件不存在!")
|
||||
self.__config = ConfigParser()
|
||||
self.__config.read(config_file, encoding=encode)
|
||||
|
||||
def get_sections(self):
|
||||
"""获取配置文件的所有section
|
||||
"""
|
||||
return self.__config.sections()
|
||||
|
||||
def get_options(self, section_name):
|
||||
"""获取指定section的所有option
|
||||
"""
|
||||
if self.__config.has_section(section_name):
|
||||
return self.__config.options(section_name)
|
||||
else:
|
||||
raise ValueError(section_name)
|
||||
|
||||
def get_option_value(self, section_name, option_name):
|
||||
"""获取指定section下option的value值
|
||||
"""
|
||||
if self.__config.has_option(section_name, option_name):
|
||||
return self.__config.get(section_name, option_name)
|
||||
|
||||
def get_all_items(self, section_name, to_dict: bool=True):
|
||||
"""获取指定section下的option的键值对
|
||||
"""
|
||||
if self.__config.has_section(section_name):
|
||||
if to_dict:
|
||||
return dict(self.__config.items(section_name))
|
||||
return self.__config.items(section_name)
|
||||
|
||||
def print_all_items(self):
|
||||
"""打印配置文件所有的值
|
||||
"""
|
||||
for section in self.get_sections():
|
||||
print("[" + section + "]")
|
||||
for K, V in self.__config.items(section):
|
||||
print(K + "=" + V)
|
||||
|
||||
def add_new_section(self, new_section):
|
||||
"""增加section
|
||||
"""
|
||||
if not self.__config.has_section(new_section):
|
||||
self.__config.add_section(new_section)
|
||||
self.__update_cfg_file()
|
||||
|
||||
def add_option(self, section_name, option_key, option_value):
|
||||
"""增加指定section下option
|
||||
"""
|
||||
if self.__config.has_section(section_name):
|
||||
self.__config.set(section_name, option_key, option_value)
|
||||
self.__update_cfg_file()
|
||||
|
||||
def del_section(self, section_name):
|
||||
"""删除指定section
|
||||
"""
|
||||
if self.__config.has_section(section_name):
|
||||
self.__config.remove_section(section_name)
|
||||
self.__update_cfg_file()
|
||||
|
||||
def del_option(self, section_name, option_name):
|
||||
"""删除指定section下的option
|
||||
"""
|
||||
if self.__config.has_option(section_name, option_name):
|
||||
self.__config.remove_option(section_name, option_name)
|
||||
self.__update_cfg_file()
|
||||
|
||||
def update_section(self, section_name, option_dict: dict):
|
||||
"""批量更新指定section下的option的值
|
||||
"""
|
||||
if self.__config.has_section(section_name):
|
||||
for k, v in option_dict:
|
||||
self.__config.set(section_name, k, v)
|
||||
self.__update_cfg_file()
|
||||
|
||||
def update_option_value(self, section_name, option_key, option_value):
|
||||
"""更新指定section下的option的值
|
||||
"""
|
||||
if self.__config.has_option(section_name, option_key):
|
||||
self.add_option(section_name, option_key, option_value)
|
||||
|
||||
# 私有方法:操作配置文件的增删改时,更新配置文件的数据
|
||||
def __update_cfg_file(self):
|
||||
with open(self.__cfg_file, "w") as f:
|
||||
self.__config.write(f)
|
||||
|
||||
|
||||
myConfig = MyConfig(os.path.join(settings.BASE_DIR, 'server/conf.ini'))
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
class MyPagination(PageNumberPagination):
|
||||
"""
|
||||
自定义分页/传入page为0则不分页
|
||||
"""
|
||||
page_size = 10
|
||||
page_size_query_param = 'page_size'
|
||||
|
||||
def paginate_queryset(self, queryset, request, view=None):
|
||||
if request.query_params.get('pageoff', None) or request.query_params.get('page', None) == '0':
|
||||
if queryset.count() < 800:
|
||||
return None
|
||||
raise ParseError('单次请求数据量大,请分页获取')
|
||||
return super().paginate_queryset(queryset, request, view=view)
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
from django.core.cache import cache
|
||||
from rest_framework.permissions import BasePermission
|
||||
from apps.utils.queryset import get_child_queryset2
|
||||
from apps.system.models import DataFilter, Dept, Permission, PostRole, UserPost
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
ALL_PERMS = [
|
||||
|
||||
]
|
||||
|
||||
|
||||
def get_user_perms_map(user):
|
||||
"""
|
||||
获取权限字典,可用redis存取(包括功能和数据权限)
|
||||
"""
|
||||
user_perms_map = {}
|
||||
if user.is_superuser:
|
||||
for perm in Permission.objects.all():
|
||||
if perm.codes:
|
||||
for code in perm.codes:
|
||||
user_perms_map[code] = {}
|
||||
else:
|
||||
objs = UserPost.objects.filter(user=user).exclude(post=None)
|
||||
for i in objs:
|
||||
dept_id = str(i.dept.id)
|
||||
for pr in PostRole.objects.filter(post=i.post):
|
||||
"""
|
||||
岗位角色
|
||||
"""
|
||||
for perm in Permission.objects.filter(role_perms=pr.role):
|
||||
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 pr.data_range < data_range:
|
||||
user_perms_map[code][dept_id] = pr.data_range
|
||||
else:
|
||||
user_perms_map[code] = {dept_id: pr.data_range}
|
||||
cache.set('perms_' + str(user.id), user_perms_map, timeout=300)
|
||||
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 True
|
||||
user_perms_map = cache.get('perms_' + request.user.id, None)
|
||||
if user_perms_map is None:
|
||||
user_perms_map = get_user_perms_map(request.user)
|
||||
if isinstance(user_perms_map, dict):
|
||||
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 == DataFilter.ALL:
|
||||
return queryset
|
||||
elif data_range == DataFilter.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 == DataFilter.THISLEVEL_AND_BELOW:
|
||||
belong_depts = get_child_queryset2(dept)
|
||||
queryset = queryset.filter(
|
||||
belong_dept__in=belong_depts)
|
||||
elif data_range == DataFilter.THISLEVEL:
|
||||
queryset = queryset.filter(belong_dept=dept)
|
||||
elif data_range == DataFilter.MYSELF:
|
||||
queryset = queryset.filter(create_by=user)
|
||||
new_queryset = new_queryset | queryset
|
||||
return new_queryset
|
||||
else:
|
||||
return queryset.none()
|
||||
return queryset
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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,108 @@
|
|||
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:
|
||||
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:
|
||||
pass
|
||||
return model if model else ""
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
from rest_framework import serializers
|
||||
from django_restql.mixins import DynamicFieldsMixin
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.request import Request
|
||||
|
||||
|
||||
class PkSerializer(serializers.Serializer):
|
||||
ids = serializers.ListField(child=serializers.CharField(max_length=20), label="主键ID列表")
|
||||
soft = serializers.BooleanField(label="是否软删除", default=True, required=False)
|
||||
|
||||
|
||||
class GenSignatureSerializer(serializers.Serializer):
|
||||
path = serializers.CharField(label="图片地址")
|
||||
|
||||
|
||||
class CustomModelSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
|
||||
"""
|
||||
自定义serializer/包含创建和新增字段处理
|
||||
"""
|
||||
|
||||
def __init__(self, instance=None, data=empty, request=None, **kwargs):
|
||||
super().__init__(instance, data, **kwargs)
|
||||
self.request: Request = request or self.context.get('request', None)
|
||||
|
||||
def create(self, validated_data):
|
||||
if self.request:
|
||||
if getattr(self.request, 'user', None):
|
||||
if getattr(self.Meta.model, 'create_by', None):
|
||||
validated_data['create_by'] = self.request.user
|
||||
validated_data['update_by'] = self.request.user
|
||||
if 'belong_dept' in validated_data:
|
||||
pass
|
||||
elif getattr(self.request.user, 'belong_dept', None):
|
||||
if hasattr(self.Meta.model, 'belong_dept'):
|
||||
validated_data['belong_dept'] = self.request.user.belong_dept
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if self.request:
|
||||
if hasattr(instance, 'update_by'):
|
||||
validated_data['update_by'] = getattr(self.request, 'user', None)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
class QuerySerializer(serializers.Serializer):
|
||||
field = serializers.CharField(label='字段名')
|
||||
compare = serializers.ChoiceField(label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"])
|
||||
value = serializers.CharField(label='值')
|
||||
|
||||
|
||||
class ComplexSerializer(serializers.Serializer):
|
||||
# page = serializers.IntegerField(min_value=0)
|
||||
# page_size = serializers.IntegerField(min_value=1)
|
||||
# query = serializers.CharField(label='获取字段名')
|
||||
querys = serializers.ListField(child=QuerySerializer(many=True), label="查询列表", required=False)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
from aliyunsdkcore.client import AcsClient
|
||||
from aliyunsdkcore.request import CommonRequest
|
||||
import json
|
||||
import logging
|
||||
from server.settings import get_sysconfig
|
||||
from apps.utils.decorators import auto_log
|
||||
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
@auto_log(name='阿里云短信', raise_exception=True, send_mail=True)
|
||||
def send_sms(phone: str, template_code: int, template_param: dict):
|
||||
config = get_sysconfig()
|
||||
if config['sms'].get('enabled', True) is False:
|
||||
return
|
||||
client = AcsClient(config['sms']['xn_key'], config['sms']['xn_secret'], 'default')
|
||||
request = CommonRequest()
|
||||
# 固定json
|
||||
request.set_accept_format('json')
|
||||
# 固定地址
|
||||
request.set_domain('sms11.hzgxr.com:40081')
|
||||
# 固定POST
|
||||
request.set_method('POST')
|
||||
# 固定HTTP
|
||||
request.set_protocol_type('http') # https | http
|
||||
# 固定版本号
|
||||
request.set_version('2017-05-25')
|
||||
# 固定操作名
|
||||
request.set_action_name('SendSms')
|
||||
# 手机号码
|
||||
request.add_query_param('PhoneNumbers', phone)
|
||||
# 签名名称
|
||||
request.add_query_param('SignName', config['sms']['xn_sign'])
|
||||
# 模板CODE
|
||||
request.add_query_param('TemplateCode', template_code)
|
||||
# 如果有模板参数 填写模板参数 如果无 无须填写
|
||||
request.add_query_param('TemplateParam', json.dumps(template_param))
|
||||
res = client.do_action(request)
|
||||
res_dict = json.loads(str(res, encoding='utf-8'))
|
||||
# print(phone, template_code, template_param, res_dict)
|
||||
if res_dict['result'] == 0:
|
||||
|
||||
return True, res_dict
|
||||
else:
|
||||
myLogger.error("短信发送失败:{}-{}-{}-{}".format(phone, template_code, str(template_param), str(res_dict)))
|
||||
return False, res_dict
|
||||
|
||||
|
||||
def send_sms_huawei():
|
||||
"""华为短信发送/备用
|
||||
"""
|
||||
|
||||
|
||||
def send_sms_tencent():
|
||||
"""腾讯短信发送/备用
|
||||
"""
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# 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
|
||||
|
||||
from random import randint
|
||||
import time
|
||||
from server.settings import SNOW_DATACENTER_ID
|
||||
|
||||
|
||||
class InvalidSystemClock(Exception):
|
||||
"""
|
||||
时钟回拨异常
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Constant(object):
|
||||
# 64位ID的划分
|
||||
WORKER_ID_BITS = 6
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
idWorker = IdWorker(SNOW_DATACENTER_ID, randint(1, 60))
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(idWorker.get_id())
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
from aip import AipSpeech
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
import os
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def generate_voice(msg: str, per: int = 0):
|
||||
"""文本生成语音
|
||||
|
||||
Args:
|
||||
msg (str): 文本
|
||||
per (int): 男/女声
|
||||
|
||||
Returns:
|
||||
bool: 成功
|
||||
str: 地址
|
||||
dict: result
|
||||
"""
|
||||
client = AipSpeech(settings.BD_SP_ID, settings.BD_SP_KEY, settings.BD_SP_SECRET)
|
||||
result = client.synthesis(msg, 'zh', 1, {'vol': 5, 'spd': 5, 'per': per})
|
||||
# 识别正确返回语音二进制 错误则返回dict 参照下面错误码
|
||||
if not isinstance(result, dict):
|
||||
file_name = '{}.mp3'.format(uuid.uuid4())
|
||||
path = '/media/' + timezone.now().strftime('%Y/%m/%d/')
|
||||
full_path = settings.BASE_DIR + path
|
||||
if not os.path.exists(full_path):
|
||||
os.makedirs(full_path)
|
||||
with open(full_path + file_name, 'wb') as f:
|
||||
f.write(result)
|
||||
return True, path + file_name, None
|
||||
return False, None, result
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
from django.db import connection
|
||||
|
||||
def execute_raw_sql(sql: str, params=None):
|
||||
"""执行原始sql并返回rows, columns数据
|
||||
|
||||
Args:
|
||||
sql (str): 查询语句
|
||||
params (_type_, optional): 参数列表. Defaults to None.
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SET statement_timeout TO %s;", [30000])
|
||||
if params:
|
||||
cursor.execute(sql, params=params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
rows = cursor.fetchall()
|
||||
return columns, rows
|
||||
|
||||
def format_sqldata(columns, rows):
|
||||
return [columns] + rows, [dict(zip(columns, row)) for row in rows]
|
||||
|
||||
|
||||
def query_all_dict(sql, params=None):
|
||||
'''
|
||||
查询所有结果返回字典类型数据
|
||||
:param sql:
|
||||
:param params:
|
||||
:return:
|
||||
'''
|
||||
with connection.cursor() as cursor:
|
||||
if params:
|
||||
cursor.execute(sql, params=params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
def query_one_dict(sql, params=None):
|
||||
"""
|
||||
查询一个结果返回字典类型数据
|
||||
:param sql:
|
||||
:param params:
|
||||
:return:
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
if params:
|
||||
cursor.execute(sql, params=params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
row = cursor.fetchone()
|
||||
return dict(zip(columns, row))
|
||||
|
||||
import pymysql
|
||||
|
||||
class DbConnection:
|
||||
def __init__(self, host, user, password, database):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.conn = None
|
||||
self.cursor = None
|
||||
|
||||
def __enter__(self):
|
||||
self.conn = pymysql.connect(
|
||||
host=self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
database=self.database
|
||||
)
|
||||
self.cursor = self.conn.cursor()
|
||||
return self.cursor
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# from __future__ import absolute_import, unicode_literals
|
||||
from celery import Task
|
||||
from celery import shared_task
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from server.settings import get_sysconfig
|
||||
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
@shared_task
|
||||
def send_mail_task(**args):
|
||||
config = get_sysconfig()
|
||||
from django.core.mail import send_mail
|
||||
args['subject'] = '{}:{}_{}_{}'.format(
|
||||
settings.SYS_NAME, settings.SYS_VERSION, config['base']['base_name_short'], args.get('subject', '500'))
|
||||
args['from_email'] = args.get('from_email', settings.EMAIL_HOST_USER)
|
||||
args['recipient_list'] = args.get(
|
||||
'recipient_list', [settings.EMAIL_HOST_USER])
|
||||
send_mail(**args)
|
||||
|
||||
|
||||
class CustomTask(Task):
|
||||
"""
|
||||
自定义的任务回调
|
||||
"""
|
||||
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
detail = '{0!r} failed: {1!r}'.format(task_id, exc)
|
||||
myLogger.error(detail)
|
||||
send_mail_task.delay(subject='task_error', message=detail)
|
||||
return super().on_failure(exc, task_id, args, kwargs, einfo)
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
import re
|
||||
import textwrap
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
import base64
|
||||
import requests
|
||||
from io import BytesIO
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
def tran64(s):
|
||||
missing_padding = len(s) % 4
|
||||
if missing_padding != 0:
|
||||
s = s+'='* (4 - missing_padding)
|
||||
return s
|
||||
|
||||
|
||||
def singleton(cls):
|
||||
_instance = {}
|
||||
|
||||
def inner():
|
||||
if cls not in _instance:
|
||||
_instance[cls] = cls()
|
||||
return _instance[cls]
|
||||
return inner
|
||||
|
||||
|
||||
def print_roundtrip(response, *args, **kwargs):
|
||||
def format_headers(d): return '\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),
|
||||
))
|
||||
|
||||
|
||||
def ranstr(num):
|
||||
salt = ''.join(random.sample(string.ascii_lowercase + string.digits, num))
|
||||
return salt
|
||||
|
||||
|
||||
def rannum(num):
|
||||
salt = ''.join(random.sample(string.digits, num))
|
||||
return salt
|
||||
|
||||
|
||||
def timestamp_to_time(millis):
|
||||
"""10位时间戳转换为日期格式字符串"""
|
||||
return datetime.fromtimestamp(millis)
|
||||
|
||||
|
||||
def convert_to_base64(path: str):
|
||||
"""给定图片转base64
|
||||
|
||||
Args:
|
||||
path (str): 图片地址
|
||||
"""
|
||||
if path.startswith('http'): # 如果是网络图片
|
||||
return str(base64.b64encode(BytesIO(requests.get(url=path).content).read()), 'utf-8')
|
||||
else:
|
||||
with open(settings.BASE_DIR + path, 'rb') as f:
|
||||
return str(base64.b64encode(f.read()), 'utf-8')
|
||||
|
||||
|
||||
def p_in_poly(p, poly):
|
||||
px = p['x']
|
||||
py = p['y']
|
||||
flag = False
|
||||
|
||||
i = 0
|
||||
l = len(poly)
|
||||
j = l - 1
|
||||
# for(i = 0, l = poly.length, j = l - 1; i < l; j = i, i++):
|
||||
while i < l:
|
||||
sx = poly[i]['x']
|
||||
sy = poly[i]['y']
|
||||
tx = poly[j]['x']
|
||||
ty = poly[j]['y']
|
||||
|
||||
# 点与多边形顶点重合
|
||||
if (sx == px and sy == py) or (tx == px and ty == py):
|
||||
return (px, py)
|
||||
|
||||
# 判断线段两端点是否在射线两侧
|
||||
if (sy < py and ty >= py) or (sy >= py and ty < py):
|
||||
# 线段上与射线 Y 坐标相同的点的 X 坐标
|
||||
x = sx + (py - sy) * (tx - sx) / (ty - sy)
|
||||
# 点在多边形的边上
|
||||
if x == px:
|
||||
return (px, py)
|
||||
# 射线穿过多边形的边界
|
||||
if x > px:
|
||||
flag = not flag
|
||||
j = i
|
||||
i += 1
|
||||
|
||||
# 射线穿过多边形边界的次数为奇数时点在多边形内
|
||||
return (px, py) if flag else 'out'
|
||||
|
||||
|
||||
def check_id_number_e(val):
|
||||
re_s = r'^[1-9]\d{5}(18|19|20|(3\d))\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$'
|
||||
if not re.match(re_s, val):
|
||||
raise ValidationError('身份证号校验错误')
|
||||
return val
|
||||
|
||||
|
||||
def get_info_from_id(val):
|
||||
birth = val[6:14]
|
||||
birth_year = birth[0:4]
|
||||
age = datetime.now().year - int(birth_year)
|
||||
sex = int(val[-2])
|
||||
gender = '女'
|
||||
if sex % 2:
|
||||
gender = '男'
|
||||
return dict(age=age, gender=gender)
|
||||
|
||||
|
||||
def check_id_number(idcard):
|
||||
"""校验身份证号
|
||||
|
||||
Args:
|
||||
id_number (_type_): 身份证号
|
||||
"""
|
||||
Errors = ['身份证号码位数不对!', '身份证号码出生日期超出范围或含有非法字符!', '身份证号码校验错误!', '身份证地区非法!']
|
||||
area = {"11": "北京", "12": "天津", "13": "河北", "14": "山西", "15": "内蒙古", "21": "辽宁", "22": "吉林", "23": "黑龙江",
|
||||
"31": "上海", "32": "江苏", "33": "浙江", "34": "安徽", "35": "福建", "36": "江西", "37": "山东", "41": "河南", "42": "湖北",
|
||||
"43": "湖南", "44": "广东", "45": "广西", "46": "海南", "50": "重庆", "51": "四川", "52": "贵州", "53": "云南", "54": "西藏",
|
||||
"61": "陕西", "62": "甘肃", "63": "青海", "64": "宁夏", "65": "新疆", "71": "台湾", "81": "香港", "82": "澳门", "91": "国外"}
|
||||
idcard = str(idcard)
|
||||
idcard = idcard.strip()
|
||||
idcard_list = list(idcard)
|
||||
|
||||
# 地区校验
|
||||
if str(idcard[0:2]) not in area:
|
||||
return False, Errors[3]
|
||||
|
||||
# 15位身份号码检测
|
||||
if len(idcard) == 15:
|
||||
if ((int(idcard[6:8]) + 1900) % 4 == 0 or (
|
||||
(int(idcard[6:8]) + 1900) % 100 == 0 and (int(idcard[6:8]) + 1900) % 4 == 0)):
|
||||
ereg = re.compile(
|
||||
'[1-9][0-9]{5}[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}$') # //测试出生日期的合法性
|
||||
else:
|
||||
ereg = re.compile(
|
||||
'[1-9][0-9]{5}[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|1[0-9]|2[0-8]))[0-9]{3}$') # //测试出生日期的合法性
|
||||
if re.match(ereg, idcard):
|
||||
return True, ''
|
||||
else:
|
||||
return False, Errors[1]
|
||||
# 18位身份号码检测
|
||||
elif len(idcard) == 18:
|
||||
# 出生日期的合法性检查
|
||||
# 闰年月日:((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))
|
||||
# 平年月日:((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|1[0-9]|2[0-8]))
|
||||
if (int(idcard[6:10]) % 4 == 0 or (int(idcard[6:10]) % 100 == 0 and int(idcard[6:10]) % 4 == 0)):
|
||||
# 闰年出生日期的合法性正则表达式
|
||||
ereg = re.compile(
|
||||
'[1-9][0-9]{5}19[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}[0-9Xx]$')
|
||||
else:
|
||||
# 平年出生日期的合法性正则表达式
|
||||
ereg = re.compile(
|
||||
'[1-9][0-9]{5}19[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|3[0-1])|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|1[0-9]|2[0-8]))[0-9]{3}[0-9Xx]$')
|
||||
# 测试出生日期的合法性
|
||||
if re.match(ereg, idcard):
|
||||
# 计算校验位
|
||||
S = (int(idcard_list[0]) + int(idcard_list[10])) * 7 + (int(idcard_list[1]) + int(idcard_list[11])) * 9 + (
|
||||
int(idcard_list[2]) + int(idcard_list[12])) * 10 + (
|
||||
int(idcard_list[3]) + int(idcard_list[13])) * 5 + (
|
||||
int(idcard_list[4]) + int(idcard_list[14])) * 8 + (
|
||||
int(idcard_list[5]) + int(idcard_list[15])) * 4 + (
|
||||
int(idcard_list[6]) + int(idcard_list[16])) * 2 + int(idcard_list[7]) * 1 + int(
|
||||
idcard_list[8]) * 6 + int(idcard_list[9]) * 3
|
||||
Y = S % 11
|
||||
M = "F"
|
||||
JYM = "10X98765432"
|
||||
M = JYM[Y] # 判断校验位
|
||||
if M == idcard_list[17]: # 检测ID的校验位
|
||||
return True, ''
|
||||
else:
|
||||
return False, Errors[2]
|
||||
else:
|
||||
return False, Errors[1]
|
||||
else:
|
||||
return False, Errors[0]
|
||||
|
||||
|
||||
def check_phone_e(phone):
|
||||
re_phone = r'^1\d{10}$'
|
||||
if not re.match(re_phone, phone):
|
||||
raise ValidationError('手机号格式错误')
|
||||
return phone
|
||||
|
|
@ -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,57 @@
|
|||
|
||||
import os
|
||||
import cv2
|
||||
from django.http import HttpResponse
|
||||
from apps.utils.errors import SIGN_MAKE_FAIL
|
||||
from server.settings import BASE_DIR
|
||||
import numpy as np
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import ParseError
|
||||
from apps.utils.viewsets import CustomGenericViewSet
|
||||
from apps.utils.mixins import CustomCreateModelMixin
|
||||
from apps.utils.serializers import GenSignatureSerializer
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.serializers import Serializer
|
||||
from django.core.cache import cache
|
||||
import json
|
||||
import requests
|
||||
|
||||
|
||||
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 Response({'path': new_path.replace(BASE_DIR, '')})
|
||||
except Exception:
|
||||
raise ParseError(**SIGN_MAKE_FAIL)
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
|
||||
from django.core.cache import cache
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError, ParseError
|
||||
from rest_framework.mixins import (CreateModelMixin, ListModelMixin,
|
||||
RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)
|
||||
from rest_framework.permissions import IsAuthenticated, IsAdminUser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from apps.system.models import DataFilter, Dept, User
|
||||
from apps.utils.errors import PKS_ERROR
|
||||
from apps.utils.mixins import MyLoggingMixin, BulkCreateModelMixin, BulkUpdateModelMixin, BulkDestroyModelMixin
|
||||
from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map
|
||||
from apps.utils.queryset import get_child_queryset2
|
||||
from apps.utils.serializers import PkSerializer, ComplexSerializer
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from apps.utils.decorators import idempotent
|
||||
from django.db import transaction
|
||||
import json
|
||||
from rest_framework.generics import get_object_or_404
|
||||
|
||||
|
||||
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
|
||||
"""
|
||||
增强的GenericViewSet
|
||||
"""
|
||||
perms_map = {} # 权限标识
|
||||
throttle_classes = [UserRateThrottle]
|
||||
logging_methods = ['POST', 'PUT', 'PATCH', 'DELETE']
|
||||
ordering_fields = '__all__'
|
||||
ordering = '-create_time'
|
||||
create_serializer_class = None
|
||||
update_serializer_class = None
|
||||
partial_update_serializer_class = None
|
||||
list_serializer_class = None
|
||||
retrieve_serializer_class = None
|
||||
select_related_fields = []
|
||||
prefetch_related_fields = []
|
||||
permission_classes = [IsAuthenticated & RbacPermission]
|
||||
data_filter = False # 数据权限过滤是否开启(需要RbacPermission)
|
||||
data_filter_field = 'belong_dept'
|
||||
hash_k = None
|
||||
cache_seconds = 5 # 接口缓存时间默认5秒
|
||||
filterset_fields = select_related_fields
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
if self.hash_k and self.cache_seconds:
|
||||
cache.set(self.hash_k, response.data,
|
||||
timeout=self.cache_seconds) # 将结果存入缓存,设置超时时间
|
||||
return super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
cache_seconds = getattr(
|
||||
self, f"{self.action}_cache_seconds", getattr(self, 'cache_seconds', 0))
|
||||
if cache_seconds:
|
||||
self.cache_seconds = cache_seconds
|
||||
rdata = {}
|
||||
rdata['request_method'] = request.method
|
||||
rdata['request_path'] = request.path
|
||||
rdata['request_data'] = request.data
|
||||
rdata['request_query'] = request.query_params.dict()
|
||||
rdata['request_userid'] = request.user.id
|
||||
self.hash_k = hash(json.dumps(rdata))
|
||||
hash_v_e = cache.get(self.hash_k, None)
|
||||
if hash_v_e is None:
|
||||
cache.set(self.hash_k, 'o', self.cache_seconds)
|
||||
elif hash_v_e == 'o': # 说明请求正在处理
|
||||
raise ParseError(f'请求忽略,请{self.cache_seconds}秒后重试')
|
||||
elif hash_v_e:
|
||||
return Response(hash_v_e)
|
||||
|
||||
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()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.perms_map:
|
||||
for k, v in self.perms_map.items():
|
||||
if v not in ALL_PERMS and v != '*':
|
||||
ALL_PERMS.append(v)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.select_related_fields:
|
||||
queryset = queryset.select_related(*self.select_related_fields)
|
||||
if self.prefetch_related_fields:
|
||||
queryset = queryset.prefetch_related(*self.prefetch_related_fields)
|
||||
if self.data_filter:
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
return queryset
|
||||
user_perms_map = cache.get('perms_' + str(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, 'perms_map'):
|
||||
perms_map = self.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 == DataFilter.ALL:
|
||||
return queryset
|
||||
elif data_range == DataFilter.SAMELEVE_AND_BELOW:
|
||||
queryset = self.filter_s_a_b(queryset, dept)
|
||||
elif data_range == DataFilter.THISLEVEL_AND_BELOW:
|
||||
queryset = self.filter_t_a_b(queryset, dept)
|
||||
elif data_range == DataFilter.THISLEVEL:
|
||||
queryset = self.filter_t(queryset, dept)
|
||||
elif data_range == DataFilter.MYSELF:
|
||||
queryset = queryset.filter(create_by=user)
|
||||
new_queryset = new_queryset | queryset
|
||||
return new_queryset
|
||||
else:
|
||||
return queryset.none()
|
||||
return queryset
|
||||
|
||||
def filter_s_a_b(self, queryset, dept):
|
||||
"""过滤同级及以下, 可重写
|
||||
"""
|
||||
if hasattr(queryset.model, 'belong_dept'):
|
||||
if dept.parent:
|
||||
belong_depts = get_child_queryset2(dept.parent)
|
||||
else:
|
||||
belong_depts = get_child_queryset2(dept)
|
||||
whereis = {self.data_filter_field + '__in': belong_depts}
|
||||
queryset = queryset.filter(**whereis)
|
||||
return queryset
|
||||
return queryset.filter(create_by=self.request.user)
|
||||
|
||||
def filter_t_a_b(self, queryset, dept):
|
||||
"""过滤本级及以下, 可重写
|
||||
"""
|
||||
if hasattr(queryset.model, 'belong_dept'):
|
||||
belong_depts = get_child_queryset2(dept)
|
||||
whereis = {self.data_filter_field + '__in': belong_depts}
|
||||
queryset = queryset.filter(**whereis)
|
||||
return queryset
|
||||
return queryset.filter(create_by=self.request.user)
|
||||
|
||||
def filter_t(self, queryset, dept):
|
||||
"""过滤本级, 可重写
|
||||
"""
|
||||
if hasattr(queryset.model, 'belong_dept'):
|
||||
whereis = {self.data_filter_field: dept}
|
||||
queryset = queryset.filter(whereis)
|
||||
return queryset
|
||||
return queryset.filter(create_by=self.request.user)
|
||||
|
||||
|
||||
class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, ListModelMixin,
|
||||
RetrieveModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
增强的ModelViewSet
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
# 增加默认权限标识
|
||||
if not self.perms_map or self.perms_map == {'get': '*'}:
|
||||
basename = self.basename
|
||||
self.perms_map = {'get': '*', 'post': '{}.create'.format(basename), 'put': '{}.update'.format(
|
||||
basename), 'patch': '{}.update'.format(basename), 'delete': '{}.delete'.format(basename)}
|
||||
for k, v in self.perms_map.items():
|
||||
if v not in ALL_PERMS and v != '*':
|
||||
ALL_PERMS.append(v)
|
||||
|
||||
@swagger_auto_schema(request_body=ComplexSerializer, responses={200: {}})
|
||||
@action(methods=['post'], detail=False, perms_map={'post': '*'})
|
||||
def cquery(self, request):
|
||||
"""复杂查询
|
||||
|
||||
复杂查询
|
||||
"""
|
||||
sr = ComplexSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
new_qs = queryset.none()
|
||||
try:
|
||||
for m in vdata.get('querys', []):
|
||||
one_qs = queryset
|
||||
for n in m:
|
||||
st = {}
|
||||
if n['compare'] == '!': # 如果是排除比较式
|
||||
st[n['field']] = n['value']
|
||||
one_qs = one_qs.exclude(**st)
|
||||
elif n['compare'] == '':
|
||||
st[n['field']] = n['value']
|
||||
one_qs = one_qs.filter(**st)
|
||||
else:
|
||||
st[n['field'] + '__' + n['compare']] = n['value']
|
||||
one_qs = one_qs.filter(**st)
|
||||
new_qs = new_qs | one_qs
|
||||
except Exception as e:
|
||||
raise ParseError(str(e))
|
||||
page = self.paginate_queryset(new_qs)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
serializer = self.get_serializer(new_qs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
from django.core.cache import cache
|
||||
import time
|
||||
from threading import Thread
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import APIException, ParseError
|
||||
import logging
|
||||
from apps.ops.models import Tlog
|
||||
from apps.utils.errors import WX_REQUEST_ERROR
|
||||
from apps.utils.tools import print_roundtrip
|
||||
from django.utils.timezone import now
|
||||
import traceback
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
class WxClient:
|
||||
"""
|
||||
微信公众号相关
|
||||
"""
|
||||
|
||||
def __init__(self, app_id=settings.WX_APPID,
|
||||
app_secret=settings.WX_APPSECRET) -> None:
|
||||
if settings.WX_ENABLED:
|
||||
self.app_id = app_id
|
||||
self.app_secret = app_secret
|
||||
self.isRuning = True
|
||||
self.token = None # 普通token
|
||||
self.t = None # 线程
|
||||
self.log = {}
|
||||
self.setup()
|
||||
|
||||
def _get_token_loop(self):
|
||||
while self.isRuning:
|
||||
parmas = {
|
||||
'grant_type': 'client_credential',
|
||||
'appid': self.app_id,
|
||||
'secret': self.app_secret
|
||||
}
|
||||
_, ret = self.request(url='/cgi-bin/token', params=parmas, method='get')
|
||||
self.token = ret['access_token']
|
||||
cache.set(self.app_id + '_token', self.token, timeout=3600)
|
||||
time.sleep(3000)
|
||||
|
||||
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=10,
|
||||
file_path_rela=None, raise_exception=True):
|
||||
if not settings.WX_ENABLED:
|
||||
raise ParseError('微信公众号未启用')
|
||||
self.log = {"requested_at": now(), "id": uuid.uuid4(), "path": url, "method": method,
|
||||
"params": params, "body": json, "target": "wx", "result": 10}
|
||||
files = None
|
||||
if file_path_rela: # 相对路径
|
||||
files = {'file': open(settings.BASE_DIR + file_path_rela, 'rb')}
|
||||
try:
|
||||
if params:
|
||||
url = url.format(**params)
|
||||
self.log.update({"path": url})
|
||||
r = getattr(requests, method)('{}{}'.format('https://api.weixin.qq.com', url),
|
||||
params=params, json=json,
|
||||
timeout=timeout, files=files, verify=False)
|
||||
except Exception:
|
||||
errors = traceback.format_exc()
|
||||
myLogger.error('微信错误', exc_info=True)
|
||||
self.handle_log(result='error', errors=errors)
|
||||
if raise_exception:
|
||||
raise APIException(**WX_REQUEST_ERROR)
|
||||
return 'error', WX_REQUEST_ERROR
|
||||
# if settings.DEBUG:
|
||||
# print_roundtrip(r)
|
||||
if r.status_code == 200:
|
||||
ret = r.json()
|
||||
if 'errcode' in ret and ret['errcode'] not in [0, '0']:
|
||||
detail = '微信错误:' + \
|
||||
'{}|{}'.format(str(ret['errcode']), ret.get('errmsg', ''))
|
||||
err_detail = dict(detail=detail, code='wx_'+str(ret['errcode']))
|
||||
self.handle_log(result='fail', response=ret)
|
||||
if raise_exception:
|
||||
raise ParseError(**err_detail)
|
||||
return 'fail', dict(detail=detail, code='wx_'+str(ret['errcode']))
|
||||
# self.handle_log(result='success', response=ret) # 成功的日志就不记录了
|
||||
return 'success', ret
|
||||
|
||||
self.handle_log(result='error', response=None)
|
||||
if raise_exception:
|
||||
raise APIException(**WX_REQUEST_ERROR)
|
||||
return 'error', WX_REQUEST_ERROR
|
||||
|
||||
def _get_response_ms(self):
|
||||
"""
|
||||
Get the duration of the request response cycle is milliseconds.
|
||||
In case of negative duration 0 is returned.
|
||||
"""
|
||||
response_timedelta = now() - self.log["requested_at"]
|
||||
response_ms = int(response_timedelta.total_seconds() * 1000)
|
||||
return max(response_ms, 0)
|
||||
|
||||
def handle_log(self, result, response=None, errors=None):
|
||||
self.log.update({
|
||||
"result": result,
|
||||
"response": response,
|
||||
"response_ms": self._get_response_ms(),
|
||||
"errors": errors
|
||||
})
|
||||
Tlog(**self.log).save()
|
||||
|
||||
def get_basic_info(self, code):
|
||||
params = {
|
||||
'appid': self.app_id,
|
||||
'secret': self.app_secret,
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
_, res = self.request('/sns/oauth2/access_token', params=params, method='get')
|
||||
return res
|
||||
|
||||
def send_tem_msg(self, data: dict):
|
||||
"""发送模板消息
|
||||
"""
|
||||
|
||||
_, res = self.request('/cgi-bin/message/template/send',
|
||||
params={'access_token': self.token}, json=data, method='post')
|
||||
return res
|
||||
|
||||
|
||||
wxClient = WxClient()
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import uuid
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from rest_framework.exceptions import APIException, ParseError
|
||||
import logging
|
||||
from apps.ops.models import Tlog
|
||||
from apps.utils.errors import WX_REQUEST_ERROR
|
||||
from apps.utils.tools import print_roundtrip
|
||||
from django.utils.timezone import now
|
||||
import traceback
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
# 实例化myLogger
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
|
||||
class WxmpClient:
|
||||
"""
|
||||
微信小程序相关
|
||||
"""
|
||||
|
||||
def __init__(self, app_id=settings.WXMP_APPID,
|
||||
app_secret=settings.WXMP_APPSECRET) -> None:
|
||||
self.app_id, self.app_secret = None, None
|
||||
if settings.WXMP_ENABLED:
|
||||
self.app_id = app_id
|
||||
self.app_secret = app_secret
|
||||
self.log = {}
|
||||
|
||||
def request(self, url: str, method: str, params=dict(), json=dict(), timeout=10,
|
||||
file_path_rela=None, raise_exception=True):
|
||||
if not settings.WX_ENABLED:
|
||||
raise ParseError('微信小程序未启用')
|
||||
self.log = {"requested_at": now(), "id": uuid.uuid4(), "path": url, "method": method,
|
||||
"params": params, "body": json, "target": "wx", "result": 10}
|
||||
files = None
|
||||
if file_path_rela: # 相对路径
|
||||
files = {'file': open(settings.BASE_DIR + file_path_rela, 'rb')}
|
||||
try:
|
||||
if params:
|
||||
url = url.format(**params)
|
||||
self.log.update({"path": url})
|
||||
r = getattr(requests, method)('{}{}'.format('https://api.weixin.qq.com', url),
|
||||
params=params, json=json,
|
||||
timeout=timeout, files=files, verify=False)
|
||||
except Exception:
|
||||
errors = traceback.format_exc()
|
||||
myLogger.error('微信小程序错误', exc_info=True)
|
||||
self.handle_log(result='error', errors=errors)
|
||||
if raise_exception:
|
||||
raise APIException(**WX_REQUEST_ERROR)
|
||||
return 'error', WX_REQUEST_ERROR
|
||||
# if settings.DEBUG:
|
||||
# print_roundtrip(r)
|
||||
if r.status_code == 200:
|
||||
ret = r.json()
|
||||
if 'errcode' in ret and ret['errcode'] not in [0, '0']:
|
||||
detail = '微信错误:' + \
|
||||
'{}|{}'.format(str(ret['errcode']), ret.get('errmsg', ''))
|
||||
err_detail = dict(detail=detail, code='wx_'+str(ret['errcode']))
|
||||
self.handle_log(result='fail', response=ret)
|
||||
if raise_exception:
|
||||
raise ParseError(**err_detail)
|
||||
return 'fail', dict(detail=detail, code='wx_'+str(ret['errcode']))
|
||||
# self.handle_log(result='success', response=ret) # 成功的日志就不记录了
|
||||
return 'success', ret
|
||||
|
||||
self.handle_log(result='error', response=None)
|
||||
if raise_exception:
|
||||
raise APIException(**WX_REQUEST_ERROR)
|
||||
return 'error', WX_REQUEST_ERROR
|
||||
|
||||
def _get_response_ms(self):
|
||||
"""
|
||||
Get the duration of the request response cycle is milliseconds.
|
||||
In case of negative duration 0 is returned.
|
||||
"""
|
||||
response_timedelta = now() - self.log["requested_at"]
|
||||
response_ms = int(response_timedelta.total_seconds() * 1000)
|
||||
return max(response_ms, 0)
|
||||
|
||||
def handle_log(self, result, response=None, errors=None):
|
||||
self.log.update({
|
||||
"result": result,
|
||||
"response": response,
|
||||
"response_ms": self._get_response_ms(),
|
||||
"errors": errors
|
||||
})
|
||||
Tlog(**self.log).save()
|
||||
|
||||
def get_basic_info(self, code):
|
||||
params = {
|
||||
'appid': self.app_id,
|
||||
'secret': self.app_secret,
|
||||
'js_code': code,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
_, res = self.request('/sns/jscode2session', params=params, method='get')
|
||||
return res
|
||||
|
||||
|
||||
wxmpClient = WxmpClient()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from django.contrib import admin
|
||||
from apps.wf.models import State, Transition, Workflow
|
||||
# Register your models here.
|
||||
|
||||
|
||||
@admin.register(Workflow)
|
||||
class WorkflowAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = 'create_time'
|
||||
|
||||
|
||||
@admin.register(State)
|
||||
class StateAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = 'create_time'
|
||||
|
||||
|
||||
@admin.register(Transition)
|
||||
class TransitionAdmin(admin.ModelAdmin):
|
||||
date_hierarchy = 'create_time'
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WfConfig(AppConfig):
|
||||
name = 'apps.wf'
|
||||
verbose_name = '工作流管理'
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from .models import Ticket
|
||||
|
||||
|
||||
class TicketFilterSet(filters.FilterSet):
|
||||
start_create = filters.DateTimeFilter(field_name="create_time", lookup_expr='gte')
|
||||
end_create = filters.DateTimeFilter(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', 'script_run_last_result']
|
||||
|
||||
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,185 @@
|
|||
# Generated by Django 3.2.12 on 2022-08-15 06:02
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('system', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='State',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=50, verbose_name='名称')),
|
||||
('key', models.CharField(blank=True, max_length=20, null=True, verbose_name='状态标识')),
|
||||
('is_hidden', models.BooleanField(default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)', verbose_name='是否隐藏')),
|
||||
('sort', models.IntegerField(default=0, help_text='用于工单步骤接口时,step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前', verbose_name='状态顺序')),
|
||||
('type', models.IntegerField(choices=[(0, '普通'), (1, '开始'), (2, '结束')], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)', verbose_name='状态类型')),
|
||||
('enable_retreat', models.BooleanField(default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态', verbose_name='允许撤回')),
|
||||
('enable_deliver', models.BooleanField(default=False, verbose_name='允许转交')),
|
||||
('participant_type', models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型')),
|
||||
('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\\部门id\\角色id\\变量(create_by,create_by_tl)\\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot', verbose_name='参与者')),
|
||||
('state_fields', models.JSONField(blank=True, default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选, 4:隐藏 示例:{"create_time":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='表单字段')),
|
||||
('distribute_type', models.IntegerField(choices=[(1, '主动接单'), (2, '直接处理'), (3, '随机分配'), (4, '全部处理')], default=1, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)', verbose_name='分配方式')),
|
||||
('filter_dept', models.CharField(blank=True, max_length=20, null=True, verbose_name='部门字段过滤')),
|
||||
('participant_cc', models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给')),
|
||||
('on_reach_func', models.CharField(blank=True, max_length=100, null=True, verbose_name='到达时调用方法')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '工作流节点',
|
||||
'verbose_name_plural': '工作流节点',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('title', models.CharField(blank=True, help_text='工单标题', max_length=500, null=True, verbose_name='标题')),
|
||||
('sn', models.CharField(help_text='工单的流水号', max_length=25, verbose_name='流水号')),
|
||||
('ticket_data', models.JSONField(default=dict, help_text='工单自定义字段内容', verbose_name='工单数据')),
|
||||
('in_add_node', models.BooleanField(default=False, help_text='是否处于加签状态下', verbose_name='加签状态中')),
|
||||
('script_run_last_result', models.BooleanField(default=True, verbose_name='脚本最后一次执行结果')),
|
||||
('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型')),
|
||||
('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表', verbose_name='当前处理人')),
|
||||
('act_state', models.IntegerField(choices=[(0, '草稿中'), (1, '进行中'), (2, '被退回'), (3, '被撤回'), (4, '已完成'), (5, '已关闭')], default=1, help_text='当前工单的进行状态', verbose_name='进行状态')),
|
||||
('multi_all_person', models.JSONField(blank=True, default=dict, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式', verbose_name='全部处理的结果')),
|
||||
('add_node_man', models.ForeignKey(blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='加签人')),
|
||||
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_belong_dept', to='system.dept', verbose_name='所属部门')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='wf.ticket', verbose_name='父工单')),
|
||||
('parent_state', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_parent_state', to='wf.state', verbose_name='父工单状态')),
|
||||
('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_state', to='wf.state', verbose_name='当前状态')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Workflow',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=50, verbose_name='名称')),
|
||||
('key', models.CharField(blank=True, max_length=20, null=True, unique=True, verbose_name='工作流标识')),
|
||||
('sn_prefix', models.CharField(default='hb', max_length=50, verbose_name='流水号前缀')),
|
||||
('description', models.CharField(blank=True, max_length=200, null=True, verbose_name='描述')),
|
||||
('view_permission_check', models.BooleanField(default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单', verbose_name='查看权限校验')),
|
||||
('limit_expression', models.JSONField(blank=True, default=dict, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)', verbose_name='限制表达式')),
|
||||
('display_form_str', models.JSONField(blank=True, default=list, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='展现表单字段')),
|
||||
('title_template', models.CharField(blank=True, default='{title}', help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}', max_length=50, null=True, verbose_name='标题模板')),
|
||||
('content_template', models.CharField(blank=True, default='标题:{title}, 创建时间:{create_time}', help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}', max_length=1000, null=True, verbose_name='内容模板')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '工作流',
|
||||
'verbose_name_plural': '工作流',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transition',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('name', models.CharField(max_length=50, verbose_name='操作')),
|
||||
('timer', models.IntegerField(default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效', verbose_name='定时器(单位秒)')),
|
||||
('condition_expression', models.JSONField(blank=True, default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突', verbose_name='条件表达式')),
|
||||
('attribute_type', models.IntegerField(choices=[(1, '同意'), (2, '拒绝'), (3, '其他')], default=1, help_text='属性类型,1.同意,2.拒绝,3.其他', verbose_name='属性类型')),
|
||||
('field_require_check', models.BooleanField(default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容', verbose_name='是否校验必填项')),
|
||||
('on_submit_func', models.CharField(blank=True, max_length=100, null=True, verbose_name='提交操作调用方法')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('destination_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dstate_transition', to='wf.state', verbose_name='目的状态')),
|
||||
('source_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sstate_transition', to='wf.state', verbose_name='源状态')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '工作流流转',
|
||||
'verbose_name_plural': '工作流流转',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketFlow',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('suggestion', models.CharField(blank=True, default='', max_length=10000, verbose_name='处理意见')),
|
||||
('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人等', verbose_name='处理人类型')),
|
||||
('participant_str', models.CharField(blank=True, help_text='非人工处理的处理人相关信息', max_length=200, null=True, verbose_name='处理人')),
|
||||
('ticket_data', models.JSONField(blank=True, default=dict, help_text='可以用于记录当前表单数据,json格式', verbose_name='工单数据')),
|
||||
('intervene_type', models.IntegerField(choices=[(0, '正常处理'), (1, '转交'), (2, '加签'), (3, '加签处理完成'), (4, '接单'), (5, '评论'), (6, '删除'), (7, '强制关闭'), (8, '强制修改状态'), (9, 'hook操作'), (10, '撤回'), (11, '抄送')], default=0, help_text='流转类型', verbose_name='干预类型')),
|
||||
('participant_cc', models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给')),
|
||||
('participant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticketflow_participant', to=settings.AUTH_USER_MODEL, verbose_name='处理人')),
|
||||
('state', models.ForeignKey(blank=True, default=0, on_delete=django.db.models.deletion.CASCADE, to='wf.state', verbose_name='当前状态')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticketflow_ticket', to='wf.ticket', verbose_name='关联工单')),
|
||||
('transition', models.ForeignKey(blank=True, help_text='与worklow.Transition关联, 为空时表示认为干预的操作', null=True, on_delete=django.db.models.deletion.SET_NULL, to='wf.transition', verbose_name='流转id')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='workflow',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='关联工作流'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='state',
|
||||
name='workflow',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomField',
|
||||
fields=[
|
||||
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
|
||||
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||
('field_type', models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('cascader', '单选级联'), ('cascaders', '多选级联'), ('select_dg', '弹框单选'), ('select_dgs', '弹框多选'), ('textarea', '文本域'), ('file', '附件'), ('table', '表格')], help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file', max_length=50, verbose_name='类型')),
|
||||
('field_key', models.CharField(help_text='字段类型请尽量特殊,避免与系统中关键字冲突', max_length=50, verbose_name='字段标识')),
|
||||
('field_name', models.CharField(max_length=50, verbose_name='字段名称')),
|
||||
('sort', models.IntegerField(default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列', verbose_name='排序')),
|
||||
('default_value', models.CharField(blank=True, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值', max_length=100, null=True, verbose_name='默认值')),
|
||||
('description', models.CharField(blank=True, help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述', max_length=100, null=True, verbose_name='描述')),
|
||||
('placeholder', models.CharField(blank=True, help_text='用户工单详情表单中作为字段的占位符显示', max_length=100, null=True, verbose_name='占位符')),
|
||||
('field_template', models.TextField(blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder', null=True, verbose_name='文本域模板')),
|
||||
('boolean_field_display', models.JSONField(blank=True, default=dict, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"是","0":"否"}或{"1":"需要","0":"不需要"},注意数字也需要引号', verbose_name='布尔类型显示名')),
|
||||
('field_choice', models.JSONField(blank=True, default=list, help_text='选项值,格式为list, 例["id":1, "name":"张三"]', verbose_name='选项值')),
|
||||
('label', models.CharField(default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项', max_length=1000, verbose_name='标签')),
|
||||
('is_hidden', models.BooleanField(default=False, help_text='可用于携带不需要用户查看的字段信息', verbose_name='是否隐藏')),
|
||||
('group', models.CharField(blank=True, max_length=100, null=True, verbose_name='字段分组')),
|
||||
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wf.customfield', verbose_name='父字段')),
|
||||
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||
('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.12 on 2023-03-30 02:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wf', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='state',
|
||||
name='filter_dept',
|
||||
field=models.CharField(blank=True, max_length=60, null=True, verbose_name='部门字段过滤'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
from django.db import models
|
||||
from apps.utils.models import CommonAModel, CommonBModel
|
||||
from apps.system.models import User
|
||||
from apps.utils.models import BaseModel
|
||||
|
||||
|
||||
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 Meta:
|
||||
verbose_name = '工作流'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
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
|
||||
PARTICIPANT_TYPE_POST = 10
|
||||
state_participanttype_choices = (
|
||||
(0, '无处理人'),
|
||||
(PARTICIPANT_TYPE_PERSONAL, '个人'),
|
||||
(PARTICIPANT_TYPE_MULTI, '多人'),
|
||||
(PARTICIPANT_TYPE_DEPT, '部门'),
|
||||
(PARTICIPANT_TYPE_ROLE, '角色'),
|
||||
(PARTICIPANT_TYPE_POST, '岗位'),
|
||||
# (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)
|
||||
key = models.CharField('状态标识', max_length=20, null=True, blank=True)
|
||||
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='开启后允许工单创建人在此状态直接撤回工单到初始状态')
|
||||
enable_deliver = models.BooleanField('允许转交', default=False)
|
||||
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')
|
||||
# json格式存储,包括读写属性1:只读,2:必填,3:可选,4:不显示, 字典的字典
|
||||
state_fields = models.JSONField(
|
||||
'表单字段', blank=True, 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:工作流名称')
|
||||
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)
|
||||
filter_dept = models.CharField('部门字段过滤', max_length=60, null=True, blank=True)
|
||||
participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
|
||||
on_reach_func = models.CharField('到达时调用方法', max_length=100, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '工作流节点'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return '{}-{}-{}'.format(self.id, self.workflow.name, self.name)
|
||||
|
||||
|
||||
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('条件表达式', default=list, blank=True,
|
||||
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='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容')
|
||||
on_submit_func = models.CharField('提交操作调用方法', max_length=100, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '工作流流转'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return '{}-{}-{}'.format(self.id, self.workflow.name, self.name)
|
||||
|
||||
|
||||
class CustomField(CommonAModel):
|
||||
"""自定义字段, 设定某个工作流有哪些自定义字段"""
|
||||
field_type_choices = (
|
||||
('string', '字符串'),
|
||||
('int', '整型'),
|
||||
('float', '浮点'),
|
||||
('boolean', '布尔'),
|
||||
('date', '日期'),
|
||||
('datetime', '日期时间'),
|
||||
('radio', '单选'),
|
||||
('checkbox', '多选'),
|
||||
('select', '单选下拉'),
|
||||
('selects', '多选下拉'),
|
||||
('cascader', '单选级联'),
|
||||
('cascaders', '多选级联'),
|
||||
('select_dg', '弹框单选'),
|
||||
('select_dgs', '弹框多选'),
|
||||
('textarea', '文本域'),
|
||||
('file', '附件'),
|
||||
('table', '表格')
|
||||
)
|
||||
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='可用于携带不需要用户查看的字段信息')
|
||||
group = models.CharField('字段分组', max_length=100, null=True, blank=True)
|
||||
parent = models.ForeignKey('self', verbose_name='父字段', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class Ticket(CommonBModel):
|
||||
"""
|
||||
工单
|
||||
"""
|
||||
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.SET_NULL, 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,270 @@
|
|||
from apps.system.models import Dept, User
|
||||
from apps.system.serializers import UserSignatureSerializer, UserSimpleSerializer
|
||||
from rest_framework import serializers
|
||||
from apps.utils.serializers import CustomModelSerializer
|
||||
|
||||
from .models import State, Ticket, TicketFlow, Workflow, Transition, CustomField
|
||||
|
||||
|
||||
class WorkflowSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WorkflowCloneSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ['name', 'key']
|
||||
|
||||
|
||||
class StateSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class WorkflowSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Workflow
|
||||
fields = ['id', 'name', 'key']
|
||||
|
||||
|
||||
class StateSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = State
|
||||
fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat', 'enable_deliver', 'key']
|
||||
|
||||
|
||||
class TransitionSerializer(CustomModelSerializer):
|
||||
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 TransitionSimpleSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = Transition
|
||||
fields = ['id', 'name', 'attribute_type']
|
||||
|
||||
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(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class CustomFieldCreateUpdateSerializer(CustomModelSerializer):
|
||||
|
||||
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(CustomModelSerializer):
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TicketCreateSerializer(CustomModelSerializer):
|
||||
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(CustomModelSerializer):
|
||||
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(CustomModelSerializer):
|
||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
participant_ = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_',
|
||||
'act_state', 'create_time', 'update_time', 'participant_type', 'create_by', 'ticket_data',
|
||||
'participant_', 'script_run_last_result', 'participant']
|
||||
|
||||
def get_participant_(self, obj):
|
||||
if obj.participant_type in [1, 2]:
|
||||
instance = User.objects.filter(id__in=obj.participant) | User.objects.filter(id=obj.participant)
|
||||
return UserSimpleSerializer(instance=instance, many=True).data
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
queryset = queryset.select_related('workflow', 'state')
|
||||
return queryset
|
||||
|
||||
|
||||
class TicketDetailSerializer(CustomModelSerializer):
|
||||
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
ticket_data_ = serializers.SerializerMethodField()
|
||||
participant_ = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = '__all__'
|
||||
|
||||
def get_participant_(self, obj):
|
||||
if obj.participant_type in [1, 2]:
|
||||
instance = User.objects.filter(id__in=obj.participant) | User.objects.filter(id=obj.participant)
|
||||
return UserSimpleSerializer(instance=instance, many=True).data
|
||||
return None
|
||||
|
||||
@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:
|
||||
f_obj = User.objects.filter(id=i['field_value']).first()
|
||||
if f_obj:
|
||||
i['field_display'] = f_obj.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:
|
||||
f_obj = Dept.objects.filter(id=i['field_value']).first()
|
||||
if f_obj:
|
||||
i['field_display'] = f_obj.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(CustomModelSerializer):
|
||||
participant_ = UserSignatureSerializer(source='participant', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
transition_ = TransitionSimpleSerializer(source='transition', read_only=True)
|
||||
transition_attribute = serializers.CharField(source='transition.attribute_type', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TicketFlowSimpleSerializer(CustomModelSerializer):
|
||||
participant_ = UserSignatureSerializer(source='participant', read_only=True)
|
||||
state_ = StateSimpleSerializer(source='state', read_only=True)
|
||||
transition_ = TransitionSimpleSerializer(source='transition', read_only=True)
|
||||
transition_attribute = serializers.CharField(source='transition.attribute_type', 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 TicketDeliverSerializer(serializers.Serializer):
|
||||
target_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), label='转交人')
|
||||
suggestion = serializers.CharField(label="转交原因", required=False)
|
||||
|
||||
|
||||
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列表')
|
||||
|
||||
|
||||
class TicketStateUpateSerializer(serializers.ModelSerializer):
|
||||
suggestion = serializers.CharField(label="变更理由", required=False)
|
||||
need_log = serializers.BooleanField(label="是否记录日志", default=True)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['state', 'suggestion', 'need_log']
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
import importlib
|
||||
from threading import Thread
|
||||
from apps.utils.sms import send_sms
|
||||
from apps.wf.serializers import TicketSimpleSerializer
|
||||
from apps.system.models import Dept, 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, datetime
|
||||
import random
|
||||
from apps.utils.queryset import get_parent_queryset
|
||||
from apps.wf.tasks import run_task
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
|
||||
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 Exception:
|
||||
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 Exception:
|
||||
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_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=None):
|
||||
"""
|
||||
生成工单流水号
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now()
|
||||
today = str(now)[:10]+' 00:00:00'
|
||||
ticket_day_count_new = Ticket.objects.get_queryset(all=True).filter(
|
||||
create_time__gte=today, create_time__lte=now, workflow=workflow).count()
|
||||
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:
|
||||
"""
|
||||
获取下个节点状态
|
||||
"""
|
||||
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
|
||||
import 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: # 代码获取
|
||||
module, func = destination_participant.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
destination_participant = f(state=state, ticket=ticket, new_ticket_data=new_ticket_data, handler=handler)
|
||||
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT: # 部门
|
||||
destination_participant = list(User.objects.filter(
|
||||
depts__in=destination_participant).values_list('id', flat=True))
|
||||
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_POST: # 岗位
|
||||
user_queryset = User.objects.filter(posts__in=destination_participant)
|
||||
# 如果选择了岗位, 可能需要走过滤策略
|
||||
if state.filter_dept not in [0, '0', None]:
|
||||
# if not new_ticket_data.get(state.filter_dept, None):
|
||||
# raise ParseError('部门过滤字段错误')
|
||||
if '.' not in state.filter_dept:
|
||||
dpts = Dept.objects.filter(id=new_ticket_data[state.filter_dept])
|
||||
else:
|
||||
dpt_attrs = state.filter_dept.split('.') # 通过反向查询得到可能有多层
|
||||
expr = ticket
|
||||
for i in dpt_attrs:
|
||||
expr = getattr(expr, i)
|
||||
dpts = Dept.objects.filter(id=expr.id)
|
||||
user_queryset = user_queryset.filter(depts__in=dpts)
|
||||
# if state.filter_policy == 1:
|
||||
# depts = get_parent_queryset(ticket.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
# elif state.filter_policy == 2:
|
||||
# depts = get_parent_queryset(ticket.create_by.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
# elif state.filter_policy == 3:
|
||||
# depts = get_parent_queryset(handler.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
destination_participant = list(user_queryset.values_list('id', flat=True))
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE: # 角色
|
||||
user_queryset = User.objects.filter(roles__in=destination_participant, is_active=True, is_deleted=False)
|
||||
# 如果选择了角色, 需要走过滤策略
|
||||
if state.filter_dept not in [0, '0', None]:
|
||||
# if not new_ticket_data.get(state.filter_dept, None):
|
||||
# raise ParseError('部门过滤字段错误')
|
||||
if '.' not in state.filter_dept:
|
||||
dpts = Dept.objects.filter(id=new_ticket_data[state.filter_dept])
|
||||
else:
|
||||
dpt_attrs = state.filter_dept.split('.')
|
||||
expr = ticket
|
||||
for i in dpt_attrs:
|
||||
expr = getattr(expr, i)
|
||||
dpts = Dept.objects.filter(id=expr.id)
|
||||
user_queryset = user_queryset.filter(depts__in=dpts)
|
||||
# if state.filter_policy == 1:
|
||||
# depts = get_parent_queryset(ticket.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
# elif state.filter_policy == 2:
|
||||
# depts = get_parent_queryset(ticket.create_by.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
# elif state.filter_policy == 3:
|
||||
# depts = get_parent_queryset(handler.belong_dept)
|
||||
# user_queryset = user_queryset.filter(depts__in=depts)
|
||||
destination_participant = list(user_queryset.values_list('id', flat=True))
|
||||
if destination_participant_type != 0 and (not destination_participant):
|
||||
raise ParseError('下一步未找到处理人,工单无法继续')
|
||||
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]
|
||||
elif destination_participant_type == State.PARTICIPANT_TYPE_ROBOT:
|
||||
pass
|
||||
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:
|
||||
if ticket.in_add_node:
|
||||
return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作", need_accept=False)
|
||||
transitions = cls.get_state_transitions(ticket.state)
|
||||
if not transitions:
|
||||
return dict(permission=True, msg="工单当前状态无需操作")
|
||||
participant = ticket.participant
|
||||
state = ticket.state
|
||||
if type(participant) == list:
|
||||
if user.id not in participant:
|
||||
return dict(permission=False, msg="非当前处理人", need_accept=False)
|
||||
if len(participant) > 1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE:
|
||||
return dict(permission=False, msg="需要先接单再处理", need_accept=True)
|
||||
else:
|
||||
if user.id != participant:
|
||||
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 transition.source_state != source_state:
|
||||
raise ParseError('非该工单节点状态下的流转')
|
||||
|
||||
# 提交时可能进行的操作
|
||||
if transition.on_submit_func:
|
||||
module, func = transition.on_submit_func.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
f(ticket=ticket, transition=transition, new_ticket_data=new_ticket_data)
|
||||
|
||||
# 校验处理权限
|
||||
if handler is not None and created is False: # 有处理人意味着系统触发校验处理权限
|
||||
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 and transition.field_require_check:
|
||||
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.type == State.STATE_TYPE_END:
|
||||
TicketFlow.objects.create(ticket=ticket, state=destination_state,
|
||||
participant_type=0, intervene_type=0,
|
||||
participant=None)
|
||||
|
||||
cls.task_ticket(ticket=ticket)
|
||||
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
def update_ticket_state(cls, ticket: Ticket, new_state: State, suggestion: str, handler: User, need_log:bool):
|
||||
participant_info = cls.get_ticket_state_participant_info(
|
||||
new_state, ticket, {})
|
||||
source_state = ticket.state
|
||||
ticket.state = new_state
|
||||
ticket.participant_type = participant_info.get('destination_participant_type', 0)
|
||||
ticket.participant = participant_info.get('destination_participant', 0)
|
||||
ticket.multi_all_person = {}
|
||||
ticket.save()
|
||||
if need_log:
|
||||
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,
|
||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ALTER_STATE,
|
||||
participant=handler)
|
||||
cls.task_ticket(ticket=ticket)
|
||||
|
||||
@classmethod
|
||||
def task_ticket(cls, ticket: Ticket):
|
||||
"""
|
||||
执行任务(自定义任务和通知)
|
||||
"""
|
||||
state = ticket.state
|
||||
# 如果目标状态有func,由func执行额外操作(比如发送通知)
|
||||
if state.on_reach_func:
|
||||
module, func = state.on_reach_func.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
f(ticket=ticket) # 同步执行
|
||||
|
||||
# wf默认只发送通知
|
||||
last_log = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time').first()
|
||||
if (last_log.state != state or
|
||||
last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or
|
||||
ticket.in_add_node):
|
||||
# 如果状态变化或是转交加签的情况再发送通知
|
||||
Thread(target=send_ticket_notice_t, args=(ticket,), daemon=True).start()
|
||||
|
||||
# 如果目标状态是脚本则异步执行
|
||||
if state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
|
||||
run_task.delay(ticket_id=ticket.id)
|
||||
|
||||
@classmethod
|
||||
def close_by_task(cls, ticket: Ticket, suggestion: str):
|
||||
# 定时任务触发的工单关闭
|
||||
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()
|
||||
# 更新流转记录
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_ROBOT,
|
||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CLOSE, transition=None)
|
||||
|
||||
def send_ticket_notice_t(ticket: Ticket):
|
||||
"""
|
||||
发送通知
|
||||
"""
|
||||
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
|
||||
if ticket.participant_type == 1:
|
||||
# 发送短信通知
|
||||
pt = User.objects.filter(id=ticket.participant).first()
|
||||
if pt and pt.phone:
|
||||
send_sms(pt.phone, 1002, params)
|
||||
elif ticket.participant_type == 2:
|
||||
pts = User.objects.filter(id__in=ticket.participant)
|
||||
for i in pts:
|
||||
if i.phone:
|
||||
send_sms(i.phone, 1002, params)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# Create your tasks here
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import importlib
|
||||
import logging
|
||||
import traceback
|
||||
from apps.system.models import User
|
||||
from apps.utils.sms import send_sms
|
||||
from apps.utils.tasks import CustomTask
|
||||
from celery import shared_task
|
||||
from apps.wf.models import State, Ticket, TicketFlow, Transition
|
||||
from apps.wf.serializers import TicketDetailSerializer
|
||||
import time
|
||||
from apps.utils.tasks import send_mail_task
|
||||
from channels.layers import get_channel_layer
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def ticket_push(ticketId, userId):
|
||||
ticket = Ticket.objects.get(id=ticketId)
|
||||
channel_layer = get_channel_layer()
|
||||
data = {
|
||||
'type': 'ticket',
|
||||
'ticket': TicketDetailSerializer(instance=ticket).data,
|
||||
'msg': ''
|
||||
}
|
||||
async_to_sync(channel_layer.group_send)(f"user_{userId}", data)
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def send_ticket_notice(ticket_id):
|
||||
"""
|
||||
发送通知
|
||||
"""
|
||||
ticket = Ticket.objects.filter(id=ticket_id).first()
|
||||
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
|
||||
if ticket:
|
||||
if ticket.participant_type == 1:
|
||||
# ws推送
|
||||
# 发送短信通知
|
||||
pt = User.objects.filter(id=ticket.participant).first()
|
||||
ticket_push.delay(ticket.id, pt.id)
|
||||
if pt and pt.phone:
|
||||
send_sms(pt.phone, 1002, params)
|
||||
elif ticket.participant_type == 2:
|
||||
pts = User.objects.filter(id__in=ticket.participant)
|
||||
for i in pts:
|
||||
ticket_push.delay(ticket.id, i.id)
|
||||
if i.phone:
|
||||
send_sms(i.phone, 1002, params)
|
||||
|
||||
|
||||
@shared_task(base=CustomTask)
|
||||
def run_task(ticket_id: str, retry_num=1):
|
||||
ticket = Ticket.objects.get(id=ticket_id)
|
||||
transition_obj = Transition.objects.filter(
|
||||
source_state=ticket.state, is_deleted=False).first()
|
||||
script_result = True
|
||||
script_result_msg = ''
|
||||
script_str = ticket.participant
|
||||
try:
|
||||
module, func = script_str.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
f(ticket)
|
||||
except Exception:
|
||||
retry_num_new = retry_num - 1
|
||||
err_detail = traceback.format_exc()
|
||||
myLogger.error('工作流脚本执行失败', exc_info=True)
|
||||
script_result = False
|
||||
script_result_msg = err_detail
|
||||
if retry_num_new >= 0:
|
||||
time.sleep(10)
|
||||
run_task.delay(ticket_id, retry_num_new)
|
||||
return
|
||||
send_mail_task.delay(subject='wf_task_error', message=err_detail) # run_task执行失败发送邮件
|
||||
ticket = Ticket.objects.filter(id=ticket_id).first()
|
||||
if not script_result:
|
||||
ticket.script_run_last_result = False
|
||||
ticket.save()
|
||||
# 记录日志
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||
participant_type=State.PARTICIPANT_TYPE_ROBOT,
|
||||
participant_str='func:{}'.format(script_str),
|
||||
transition=transition_obj,
|
||||
suggestion=script_result_msg)
|
||||
# 自动流转
|
||||
if script_result and transition_obj:
|
||||
from apps.wf.services import WfService
|
||||
WfService.handle_ticket(ticket=ticket, transition=transition_obj,
|
||||
new_ticket_data=ticket.ticket_data, by_task=True)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
from apps.wf.views import CustomFieldViewSet, StateViewSet, TicketFlowViewSet, \
|
||||
TicketViewSet, TransitionViewSet, WorkflowKeyInitView, 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='workflow')
|
||||
router.register('state', StateViewSet, basename='state')
|
||||
router.register('transition', TransitionViewSet, basename='transition')
|
||||
router.register('customfield', CustomFieldViewSet, basename='customfield')
|
||||
router.register('ticket', TicketViewSet, basename='ticket')
|
||||
router.register('ticketflow', TicketFlowViewSet, basename='ticketflow')
|
||||
urlpatterns = [
|
||||
path(API_BASE_URL, include(router.urls)),
|
||||
path(API_BASE_URL + 'workflow/<str:key>/init_key/', WorkflowKeyInitView.as_view())
|
||||
]
|
||||
|
|
@ -0,0 +1,546 @@
|
|||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from rest_framework.views import APIView
|
||||
from apps.system.models import User
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
|
||||
from apps.wf.filters import TicketFilterSet
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, \
|
||||
RetrieveModelMixin, UpdateModelMixin
|
||||
from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, \
|
||||
TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, \
|
||||
TicketCreateSerializer, TicketDeliverSerializer, TicketDestorySerializer, TicketFlowSerializer, \
|
||||
TicketHandleSerializer, TicketRetreatSerializer, \
|
||||
TicketSerializer, TransitionSerializer, WorkflowSerializer, \
|
||||
TicketListSerializer, TicketDetailSerializer, WorkflowCloneSerializer, TicketStateUpateSerializer, TicketFlowSimpleSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from rest_framework.decorators import action
|
||||
from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow
|
||||
from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin
|
||||
from apps.wf.services import WfService
|
||||
from rest_framework.exceptions import ParseError, NotFound
|
||||
from rest_framework import status
|
||||
from django.db.models import Count
|
||||
from rest_framework.serializers import Serializer
|
||||
from apps.utils.snowflake import idWorker
|
||||
import importlib
|
||||
from apps.wf.tasks import run_task
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class WorkflowKeyInitView(APIView):
|
||||
perms_map = {'get': '*'}
|
||||
|
||||
def get(self, request, key=None):
|
||||
"""
|
||||
新建工单初始化-通过key
|
||||
|
||||
新建工单初始化
|
||||
"""
|
||||
ret = {}
|
||||
try:
|
||||
wf = Workflow.objects.get(key=key)
|
||||
except Exception:
|
||||
raise NotFound('获取工作流失败')
|
||||
start_state = WfService.get_workflow_start_state(wf)
|
||||
transitions = WfService.get_state_transitions(start_state)
|
||||
ret['workflow'] = wf.id
|
||||
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 WorkflowViewSet(CustomModelViewSet):
|
||||
queryset = Workflow.objects.all()
|
||||
serializer_class = WorkflowSerializer
|
||||
search_fields = ['name', 'description']
|
||||
filterset_fields = []
|
||||
ordering_fields = ['create_time']
|
||||
ordering = ['key', '-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': '*'})
|
||||
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'] = wf.id
|
||||
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)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': 'workflow.clone'},
|
||||
pagination_class=None, serializer_class=WorkflowCloneSerializer)
|
||||
@transaction.atomic
|
||||
def clone(self, request, pk=None):
|
||||
"""工作流复制
|
||||
|
||||
工作流复制
|
||||
"""
|
||||
wf = self.get_object()
|
||||
sr = WorkflowCloneSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
wf_new = Workflow()
|
||||
for f in Workflow._meta.fields:
|
||||
if f.name not in ['id', 'create_by', 'update_by', 'key', 'name', 'create_time', 'update_time']:
|
||||
setattr(wf_new, f.name, getattr(wf, f.name, None))
|
||||
wf_new.id = idWorker.get_id()
|
||||
wf_new.key = vdata['key']
|
||||
wf_new.name = vdata['name']
|
||||
wf_new.create_by = request.user
|
||||
wf_new.save()
|
||||
stas_dict = {}
|
||||
for s in State.objects.filter(workflow=wf):
|
||||
sta = State()
|
||||
sta.id = idWorker.get_id()
|
||||
sta.workflow = wf_new
|
||||
for f in State._meta.fields:
|
||||
if f.name not in ['workflow', 'create_time', 'update_time', 'id']:
|
||||
setattr(sta, f.name, getattr(s, f.name))
|
||||
sta.save()
|
||||
stas_dict[s.id] = sta # 保存一下, 后续备用
|
||||
for c in CustomField.objects.filter(workflow=wf):
|
||||
cf = CustomField()
|
||||
cf.id = idWorker.get_id()
|
||||
cf.workflow = wf_new
|
||||
for f in CustomField._meta.fields:
|
||||
if f.name not in ['workflow', 'create_time', 'update_time', 'id']:
|
||||
setattr(sta, f.name, getattr(s, f.name))
|
||||
cf.save()
|
||||
for t in Transition.objects.filter(workflow=wf):
|
||||
tr = Transition()
|
||||
tr.id = idWorker.get_id()
|
||||
tr.workflow = wf_new
|
||||
for f in Transition._meta.fields:
|
||||
if f.name not in ['workflow', 'create_time', 'update_time', 'id']:
|
||||
setattr(tr, f.name, getattr(t, f.name))
|
||||
tr.source_state = stas_dict[t.source_state.id]
|
||||
tr.destination_state = stas_dict[t.destination_state.id]
|
||||
ce = tr.condition_expression
|
||||
for i in ce:
|
||||
i['target_state'] = stas_dict[i['target_state']].id
|
||||
tr.condition_expression = ce
|
||||
tr.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
||||
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, CustomGenericViewSet):
|
||||
perms_map = {'get': '*', 'post': 'workflow.update',
|
||||
'put': 'workflow.update', 'delete': 'workflow.update'}
|
||||
queryset = Transition.objects.all()
|
||||
serializer_class = TransitionSerializer
|
||||
select_related_fields = ['source_state', 'destination_state']
|
||||
search_fields = ['name']
|
||||
filterset_fields = ['workflow']
|
||||
ordering = ['id']
|
||||
|
||||
|
||||
class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, CustomGenericViewSet):
|
||||
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(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||
perms_map = {'get': '*', 'post': '*'}
|
||||
queryset = Ticket.objects.all()
|
||||
serializer_class = TicketSerializer
|
||||
search_fields = ['title']
|
||||
select_related_fields = ['workflow', 'state']
|
||||
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
|
||||
elif self.action == 'deliver':
|
||||
return TicketDeliverSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not self.detail and not self.request.query_params.get('category', None):
|
||||
raise ParseError('请指定查询分类')
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
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 ParseError('字段{}必填'.format(key))
|
||||
save_ticket_data[key] = ticket_data[key]
|
||||
elif int(value) == State.STATE_FIELD_OPTIONAL:
|
||||
save_ticket_data[key] = ticket_data[key]
|
||||
else:
|
||||
save_ticket_data = ticket_data
|
||||
with transaction.atomic():
|
||||
ticket = serializer.save(state=start_state,
|
||||
create_by=request.user,
|
||||
create_time=timezone.now(),
|
||||
act_state=Ticket.TICKET_ACT_STATE_DRAFT,
|
||||
belong_dept=request.user.belong_dept,
|
||||
ticket_data=save_ticket_data) # 先创建出来
|
||||
# 更新title和sn
|
||||
title = vdata.get('title', '')
|
||||
title_template = ticket.workflow.title_template
|
||||
if title_template:
|
||||
all_ticket_data = {**rdata, **ticket_data}
|
||||
title = title_template.format(**all_ticket_data)
|
||||
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
|
||||
ticket.sn = sn
|
||||
ticket.title = title
|
||||
ticket.save()
|
||||
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
|
||||
handler=request.user, created=True)
|
||||
return Response(TicketSerializer(instance=ticket).data)
|
||||
|
||||
@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': '*'})
|
||||
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'])
|
||||
with transaction.atomic():
|
||||
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
|
||||
new_ticket_data=new_ticket_data, handler=request.user,
|
||||
suggestion=vdata.get('suggestion', ''))
|
||||
return Response(TicketSerializer(instance=ticket).data)
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': '*'})
|
||||
def deliver(self, request, pk=None):
|
||||
"""
|
||||
转交工单
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
rdata = request.data
|
||||
serializer = self.get_serializer(data=rdata)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
vdata = serializer.validated_data # 校验之后的数据
|
||||
if not ticket.state.enable_deliver:
|
||||
raise ParseError('不允许转交')
|
||||
with transaction.atomic():
|
||||
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
|
||||
ticket.participant = vdata['target_user']
|
||||
ticket.save()
|
||||
TicketFlow.objects.create(ticket=ticket, state=ticket.state,
|
||||
ticket_data=WfService.get_ticket_all_field_value(ticket),
|
||||
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
|
||||
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
|
||||
participant=request.user, transition=None)
|
||||
return Response()
|
||||
|
||||
@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)
|
||||
data = StateSerializer(instance=steps, many=True).data
|
||||
for i in data:
|
||||
if i['id'] == ticket.state.id:
|
||||
i['checked'] = True
|
||||
return Response(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 = TicketFlowSimpleSerializer(instance=flowlogs.select_related('participant', 'state', 'transition', 'participant__employee'), 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.select_related('source_state', 'destination_state'), 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 ParseError('无需接单')
|
||||
|
||||
@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 ParseError('非创建人不可撤回')
|
||||
if not ticket.state.enable_retreat:
|
||||
raise ParseError('该状态不可撤回')
|
||||
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):
|
||||
"""
|
||||
加签
|
||||
"""
|
||||
data = request.data
|
||||
sr = TicketAddNodeSerializer(data=data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
ticket = self.get_object()
|
||||
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()
|
||||
if ticket.in_add_node is False:
|
||||
raise ParseError('该工单不在加签状态中')
|
||||
elif ticket.participant != request.user.id:
|
||||
raise ParseError('非当前加签人')
|
||||
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)
|
||||
@transaction.atomic
|
||||
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)
|
||||
if end_state.on_reach_func: # 如果有到达方法还需要进行处理
|
||||
module, func = end_state.on_reach_func.rsplit(".", 1)
|
||||
m = importlib.import_module(module)
|
||||
f = getattr(m, func)
|
||||
f(ticket=ticket) # 同步执行
|
||||
return Response()
|
||||
else:
|
||||
return Response('工单不可关闭', status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post': 'ticket.destorys'},
|
||||
serializer_class=TicketDestorySerializer)
|
||||
def destorys(self, request, pk=None):
|
||||
"""
|
||||
批量物理删除
|
||||
"""
|
||||
Ticket.objects.filter(id__in=request.data.get('ids', [])).delete(soft=False)
|
||||
return Response()
|
||||
|
||||
@action(methods=['post'], detail=True, perms_map={'post': '*'},
|
||||
serializer_class=Serializer)
|
||||
def retry_script(self, request, pk=None):
|
||||
"""重试脚本
|
||||
|
||||
重试脚本
|
||||
"""
|
||||
ticket = self.get_object()
|
||||
if not ticket.script_run_last_result:
|
||||
ticket.script_run_last_result = True
|
||||
ticket.save()
|
||||
run_task.delay(ticket.id)
|
||||
return Response()
|
||||
|
||||
@action(methods=['put'], detail=True, perms_map={'put': 'ticket.state_update'},
|
||||
serializer_class=TicketStateUpateSerializer)
|
||||
def state(self, request, pk=None):
|
||||
"""强制修改工单状态
|
||||
|
||||
强制修改工单状态
|
||||
"""
|
||||
sr = TicketStateUpateSerializer(data=request.data)
|
||||
sr.is_valid(raise_exception=True)
|
||||
vdata = sr.validated_data
|
||||
ticket = self.get_object()
|
||||
WfService.update_ticket_state(ticket, vdata['state'], vdata.get('suggestion', ''), request.user, vdata['need_log'])
|
||||
return Response()
|
||||
|
||||
|
||||
class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
|
||||
"""
|
||||
工单日志
|
||||
"""
|
||||
perms_map = {'get': '*'}
|
||||
queryset = TicketFlow.objects.all()
|
||||
list_serializer_class = TicketFlowSimpleSerializer
|
||||
serializer_class = TicketFlowSerializer
|
||||
search_fields = ['suggestion']
|
||||
select_related_fields = ['participant', 'state', 'transition']
|
||||
filterset_fields = ['ticket']
|
||||
ordering = ['-create_time']
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'ws'
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
import json
|
||||
|
||||
|
||||
class RoomConsumer(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
|
||||
)
|
||||
username = self.scope['user'].username
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chat',
|
||||
'msg': f'你好,{username}, 欢迎进入{self.room_name}房间' ,
|
||||
'from': '系统',
|
||||
'to': username
|
||||
}
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
async def receive(self, text_data=None, bytes_data=None):
|
||||
sender_user = self.scope["user"]
|
||||
if text_data:
|
||||
content = json.loads(text_data)
|
||||
if content['type'] == 'chat':
|
||||
content['from'] = sender_user.username
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
content
|
||||
)
|
||||
|
||||
async def chat(self, content):
|
||||
await self.send(json.dumps(content, ensure_ascii=False))
|
||||
|
||||
|
||||
class MyConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
user_id = self.scope['user'].id
|
||||
self.room_group_name = f'user_{user_id}'
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'remind',
|
||||
'msg': '你好,' + self.scope['user'].username,
|
||||
'from': '系统'
|
||||
}
|
||||
)
|
||||
await self.accept()
|
||||
|
||||
async def receive(self, text_data=None, bytes_data=None):
|
||||
if text_data:
|
||||
content = json.loads(text_data)
|
||||
if content['type'] == 'event':
|
||||
await self.channel_layer.group_add(
|
||||
'event',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
await self.channel_layer.group_discard(
|
||||
'event',
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
async def event(self, content):
|
||||
await self.send(json.dumps(content, ensure_ascii=False))
|
||||
|
||||
async def ticket(self, content):
|
||||
await self.send(json.dumps(content, ensure_ascii=False))
|
||||
|
||||
async def remind(self, content):
|
||||
await self.send(json.dumps(content, ensure_ascii=False))
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
from apps.ws.consumers import MyConsumer, RoomConsumer
|
||||
|
||||
WS_BASE_URL = 'ws/'
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path(f'{WS_BASE_URL}my/', MyConsumer.as_asgi()),
|
||||
path(WS_BASE_URL + '<str:room_name>/', RoomConsumer.as_asgi()),
|
||||
]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
|
@ -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()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue