!68 加入消息中心功能

Merge pull request !68 from dvadmin/v2.x
pull/69/MERGE
dvadmin 2022-08-14 11:37:47 +00:00 committed by Gitee
commit 3b5475aab1
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
11 changed files with 812 additions and 9 deletions

View File

@ -14,4 +14,14 @@ from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
application = get_asgi_application()
from application.websocketConfig import websocket_application
http_application = get_asgi_application()
async def application(scope,receive,send):
if scope['type'] == 'http':
await http_application(scope, receive, send)
elif scope['type'] == 'websocket':
await websocket_application(scope, receive, send)
else:
raise Exception("未知的scope类型,"+ scope['type'])

View File

@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
import django
django.setup()
import json
import urllib
#处理websocket传参
from jwt import InvalidSignatureError
from application import settings
from dvadmin.system.models import MessageCenter
def request_data(scope):
query_string = scope.get('query_string', b'').decode('utf-8')
qs = urllib.parse.parse_qs(query_string)
return qs
# 全部的websocket sender
CONNECTIONS = {}
# 判断用户是否已经连接
def check_connection(key):
return key in CONNECTIONS
# 发送消息结构体
def message(sender, msg_type, msg):
text = json.dumps({
'sender': sender,
'contentType': msg_type,
'content': msg,
})
return {
'type': 'websocket.send',
'text': text
}
async def websocket_application(scope, receive, send):
while True:
event = await receive()
# print('[event] ', event)
qs = request_data(scope)
print(1,qs)
auth = qs.get('auth', [''])[0]
user_id = None
# 收到建立WebSocket连接的消息
if event['type'] == 'websocket.connect':
# 昵称验证
if not auth:
break
else:
try:
import jwt
decoded_result = jwt.decode(auth, settings.SECRET_KEY, algorithms=["HS256"])
if decoded_result:
user_id = decoded_result.get('user_id')
# 记录
CONNECTIONS[user_id] = send
except InvalidSignatureError:
break
if auth in CONNECTIONS:
break
await send({'type': 'websocket.accept'})
await send(message('system', 'INFO', '连接成功'))
# # 发送好友列表
# friends_list = list(CONNECTIONS.keys())
# await send(message('system', 'INFO', friends_list))
#
# # 向其他人群发消息, 有人登录了
# for other in CONNECTIONS.values():
# await other(message('system', 'addFriend', auth))
# 收到中断WebSocket连接的消息
elif event['type'] == 'websocket.disconnect':
# 移除记录
if user_id in CONNECTIONS:
CONNECTIONS.pop(user_id)
# # 向其他人群发消息, 有人离线了
# for other in CONNECTIONS.values():
# await other(message('system', 'removeFriend', user_id))
# 其他情况,正常的WebSocket消息
elif event['type'] == 'websocket.receive':
print(11,event)
if event['text'] == 'ping':
await send(message('system', 'text', 'pong!'))
else:
receive_msg = json.loads(event['text'])
message_id = receive_msg.get('message_id', None)
_MessageCenter = MessageCenter.objects.filter(id=message_id).first()
if _MessageCenter:
user_list = _MessageCenter.target_user.values_list('id',flat=True)
for send_user in user_list:
if send_user in CONNECTIONS:
content_type = receive_msg.get('contentType', 'TEXT')
content = receive_msg.get('content', '')
msg = message(user_id, content_type, content)
await CONNECTIONS[send_user](msg)
else:
msg = message('system', 'text', '对方已下线或不存在')
await send(msg)
else:
print('a1a1a1')
pass
print('[disconnect]')

View File

@ -44,3 +44,6 @@ LOGIN_NO_CAPTCHA_AUTH = True
# ================================================= #
ALLOWED_HOSTS = ["*"]
# daphne启动命令
#daphne application.asgi:application -b 0.0.0.0 -p 8000

View File

@ -272,10 +272,57 @@
}
]
},
{
"name": "消息中心",
"icon": "bullhorn",
"sort": 7,
"is_link": false,
"is_catalog": false,
"web_path": "/messageCenter",
"component": "system/messageCenter/index",
"component_name": "messageCenter",
"status": true,
"cache": false,
"visible": true,
"parent": 277,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "Search",
"api": "/api/system/message_center/",
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/message_center/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/message_center/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/message_center/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/menu/{id}/",
"method": 3
}
]
},
{
"name": "接口白名单",
"icon": "compass",
"sort": 7,
"sort": 8,
"is_link": false,
"is_catalog": false,
"web_path": "/apiWhiteList",

View File

@ -34,8 +34,8 @@ class Users(CoreModel,AbstractUser):
user_type = models.IntegerField(
choices=USER_TYPE, default=0, verbose_name="用户类型", null=True, blank=True, help_text="用户类型"
)
post = models.ManyToManyField(to="Post", verbose_name="关联岗位", db_constraint=False, help_text="关联岗位")
role = models.ManyToManyField(to="Role", verbose_name="关联角色", db_constraint=False, help_text="关联角色")
post = models.ManyToManyField(to="Post",blank=True, verbose_name="关联岗位", db_constraint=False, help_text="关联岗位")
role = models.ManyToManyField(to="Role", blank=True,verbose_name="关联角色", db_constraint=False, help_text="关联角色")
dept = models.ForeignKey(
to="Dept",
verbose_name="所属部门",
@ -416,10 +416,10 @@ class MessageCenter(CoreModel):
title = models.CharField(max_length=100,verbose_name="标题",help_text="标题")
content = models.TextField(verbose_name="内容",help_text="内容")
target_type=models.IntegerField(default=0,verbose_name="目标类型",help_text="目标类型")
target_user = models.ForeignKey(to=Users,related_name="target_user",null=True,blank=True,db_constraint=False,on_delete=models.CASCADE,verbose_name="目标用户",help_text="目标用户")
target_dept = models.ForeignKey(to=Dept, null=True, blank=True, db_constraint=False, on_delete=models.CASCADE,
target_user = models.ManyToManyField(to=Users,related_name="target_user",blank=True,db_constraint=False,verbose_name="目标用户",help_text="目标用户")
target_dept = models.ManyToManyField(to=Dept, null=True, blank=True, db_constraint=False,
verbose_name="目标部门", help_text="目标部门")
target_role = models.ForeignKey(to=Role, null=True, blank=True, db_constraint=False, on_delete=models.CASCADE,
target_role = models.ManyToManyField(to=Role, blank=True, db_constraint=False,
verbose_name="目标角色", help_text="目标角色")
is_read=models.BooleanField(default=False,blank=True,verbose_name="是否已读",help_text="是否已读")
@ -427,5 +427,4 @@ class MessageCenter(CoreModel):
db_table = table_prefix + "message_center"
verbose_name = "消息中心"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)
ordering = ("-create_datetime",)

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
from itertools import chain
from django_restql.fields import DynamicSerializerMethodField
from rest_framework.decorators import action, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from dvadmin.system.models import MessageCenter, Users
from dvadmin.system.views.role import RoleSerializer
from dvadmin.system.views.user import UserSerializer
from dvadmin.utils.json_response import SuccessResponse
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
class MessageCenterSerializer(CustomModelSerializer):
"""
消息中心-序列化器
"""
role_info = DynamicSerializerMethodField()
user_info = DynamicSerializerMethodField()
def get_role_info(self, instance, parsed_query):
roles =instance.target_role.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
serializer = RoleSerializer(
roles,
many=True,
parsed_query=parsed_query
)
return serializer.data
def get_user_info(self, instance, parsed_query):
users = instance.target_user.all()
# You can do what ever you want in here
# `parsed_query` param is passed to BookSerializer to allow further querying
serializer = UserSerializer(
users,
many=True,
parsed_query=parsed_query
)
return serializer.data
class Meta:
model = MessageCenter
fields = "__all__"
read_only_fields = ["id"]
class MessageCenterCreateSerializer(CustomModelSerializer):
"""
消息中心-新增-序列化器
"""
def save(self, **kwargs):
data = super().save(**kwargs)
initial_data = self.initial_data
target_type = initial_data.get('target_type')
# 在保存之前,根据目标类型,把目标用户查询出来并保存
users = initial_data.get('target_user',[])
if target_type in [1]:
target_role = initial_data.get('target_role')
users = Users.objects.exclude(is_deleted=True).filter(role__id__in=target_role).values_list('id',flat=True)
if target_type in [2]:
target_dept = initial_data.get('target_dept')
users = Users.objects.exclude(is_deleted=True).filter(dept__id__in=target_dept).values_list('id',flat=True)
data.save()
data.target_user.set(users)
return data
class Meta:
model = MessageCenter
fields = "__all__"
read_only_fields = ["id"]
class MessageCenterViewSet(CustomModelViewSet):
"""
消息中心接口
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = MessageCenter.objects.all()
serializer_class = MessageCenterSerializer
create_serializer_class = MessageCenterCreateSerializer
extra_filter_backends = []
@action(methods=['GET'],detail=False,permission_classes=[IsAuthenticated])
def get_self_receive(self,request):
"""
获取接收到的消息
"""
self_user_id = self.request.user.id
queryset = MessageCenter.objects.filter(target_user__id=self_user_id)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True, request=request)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True, request=request)
return SuccessResponse(data=serializer.data, msg="获取成功")

69
web/src/api/websocket.js Normal file
View File

@ -0,0 +1,69 @@
import ElementUI from 'element-ui'
import util from '@/libs/util'
function initWebSocket (e) {
const token = util.cookies.get('token')
if (token) {
const wsUri = 'ws://127.0.0.1:8000/?auth=' + token
this.socket = new WebSocket(wsUri)// 这里面的this都指向vue
this.socket.onerror = webSocketOnError
this.socket.onmessage = webSocketOnMessage
this.socket.onclose = closeWebsocket
}
}
function webSocketOnError (e) {
ElementUI.Notification({
title: '',
message: 'WebSocket连接发生错误' + JSON.stringify(e),
type: 'error',
duration: 0
})
}
function webSocketOnMessage (e) {
const data = JSON.parse(e.data)
if (data.contentType === 'INFO') {
ElementUI.Notification({
title: 'websocket',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 3000
})
} else if (data.contentType === 'ERROR') {
ElementUI.Notification({
title: '',
message: data.content,
type: 'error',
position: 'bottom-right',
duration: 0
})
} else if (data.contentType === 'TEXT') {
ElementUI.Notification({
title: '温馨提示',
message: data.content,
type: 'success',
position: 'bottom-right',
duration: 0
})
} else {
console.log(data.content)
}
}
// 关闭websiocket
function closeWebsocket () {
console.log('连接已关闭...')
close()
}
function close () {
this.socket.close() // 关闭 websocket
this.socket.onclose = function (e) {
console.log(e)// 监听关闭事件
console.log('关闭')
}
}
function webSocketSend (message) {
this.socket.send(JSON.stringify(message))
}
export default {
initWebSocket, close, webSocketSend
}

View File

@ -191,6 +191,13 @@ export default {
this.showView = true // DOMv-ifrouter-view
})
}
},
mounted () {
this.$websocket.initWebSocket()
},
destroyed () {
// websocket
this.$websocket.close()
}
}
</script>

View File

@ -0,0 +1,54 @@
import { request } from '@/api/service'
export const urlPrefix = '/api/system/message_center/'
export function GetList (query) {
return request({
url: urlPrefix,
method: 'get',
params: query
})
}
/**
* 获取自己接收的消息
* @param query
* @returns {*}
* @constructor
*/
export function GetSelfReceive (query) {
return request({
url: urlPrefix + 'get_self_receive/',
method: 'get',
params: query
})
}
export function GetObj (obj) {
return request({
url: urlPrefix + obj.id + '/',
method: 'get',
params: {}
})
}
export function AddObj (obj) {
return request({
url: urlPrefix,
method: 'post',
data: obj
})
}
export function UpdateObj (obj) {
return request({
url: urlPrefix + obj.id + '/',
method: 'put',
data: obj
})
}
export function DelObj (id) {
return request({
url: urlPrefix + id + '/',
method: 'delete',
data: { id }
})
}

View File

@ -0,0 +1,301 @@
import { request } from '@/api/service'
export const crudOptions = (vm) => {
return {
indexRow: { // 或者直接传true,不显示title不居中
title: '序号',
align: 'center'
},
options: {
height: '100%' // 表格高度100%, 使用toolbar必须设置
},
viewOptions: {
},
columns: [
{
title: 'id',
key: 'id',
sortable: true,
width: 100,
form: { disabled: true }
},
{
title: '标题',
key: 'title',
search: {
disabled: false
},
width: 400,
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: { span: 24 }
}
},
{
title: '目标类型',
key: 'target_type',
type: 'radio',
dict: { data: [{ value: 0, label: '按用户' }, { value: 1, label: '按角色' }, { value: 2, label: '按部门' }] },
form: {
rules: [
{
required: true,
message: '必选项',
trigger: ['blur', 'change']
}
]
}
},
{
title: '目标用户',
key: 'target_user',
search: {
disabled: true
},
minWidth: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/user/',
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
getData: (url, dict, {
form,
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 10
}
}).then(ret => {
component._elProps.page = ret.data.page
component._elProps.limit = ret.data.limit
component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 24,
show (context) {
return context.form.target_type === 0
},
pagination: true,
props: { multiple: true },
elProps: {
columns: [
{
field: 'name',
title: '用户名称'
},
{
field: 'phone',
title: '用户电话'
}
]
}
}
},
component: {
name: 'manyToMany',
valueBinding: 'user_info',
children: 'name'
}
},
{
title: '目标角色',
key: 'target_role',
search: {
disabled: true
},
minWidth: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/role/',
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
getData: (url, dict, {
form,
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 10
}
}).then(ret => {
component._elProps.page = ret.data.page
component._elProps.limit = ret.data.limit
component._elProps.total = ret.data.total
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 24,
show (context) {
return context.form.target_type === 1
},
pagination: true,
props: { multiple: true },
elProps: {
columns: [
{
field: 'name',
title: '角色名称'
},
{
field: 'key',
title: '权限标识'
}
]
}
}
},
component: {
name: 'manyToMany',
valueBinding: 'role_info',
children: 'name'
}
},
{
title: '目标部门',
key: 'target_dept',
search: {
disabled: true
},
minWidth: 130,
type: 'table-selector',
dict: {
cache: false,
url: '/api/system/dept/',
isTree: true,
value: 'id', // 数据字典中value字段的属性名
label: 'name', // 数据字典中label字段的属性名
children: 'children', // 数据字典中children字段的属性名
getData: (url, dict, {
form,
component
}) => {
return request({
url: url,
params: {
page: 1,
limit: 999
}
}).then(ret => {
return ret.data.data
})
}
},
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
itemProps: {
class: { yxtInput: true }
},
component: {
span: 24,
show (context) {
return context.form.target_type === 2
},
props: {
multiple: true,
elProps: {
treeConfig: {
transform: true,
rowField: 'id',
parentField: 'parent',
expandAll: true
},
columns: [
{
field: 'name',
title: '部门名称',
treeNode: true
},
{
field: 'status_label',
title: '状态'
},
{
field: 'parent_name',
title: '父级部门'
}
]
}
}
}
},
component: {
name: 'manyToMany',
valueBinding: 'dept_info',
children: 'name'
}
},
{
title: '内容',
key: 'content',
width: 300,
type: 'editor-quill', // 富文本图片上传依赖file-uploader请先配置好file-uploader
form: {
rules: [ // 表单校验规则
{
required: true,
message: '必填项'
}
],
component: {
disabled: () => {
return vm.getEditForm().disable
},
props: {
uploader: {
type: 'form' // 上传后端类型cos,aliyun,oss,form
}
},
events: {
'text-change': (event) => {
console.log('text-change:', event)
}
}
}
}
}
]
}
}

View File

@ -0,0 +1,94 @@
<template>
<d2-container :class="{'page-compact':crud.pageOptions.compact}">
<d2-crud-x
ref="d2Crud"
v-bind="_crudProps"
v-on="_crudListeners"
@form-component-ready="handleFormComponentReady"
>
<div slot="header">
<crud-search ref="search" :options="crud.searchOptions" @submit="handleSearch" />
<el-button size="small" type="primary" @click="addRow"><i class="el-icon-plus"/> 新增</el-button>
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
</el-tabs>
<crud-toolbar :search.sync="crud.searchOptions.show"
:compact.sync="crud.pageOptions.compact"
:columns="crud.columns"
@refresh="doRefresh()"
@columns-filter-changed="handleColumnsFilterChanged"/>
</div>
</d2-crud-x>
</d2-container>
</template>
<script>
import { AddObj, GetObj, GetList, UpdateObj, DelObj, GetSelfReceive } from './api'
import { crudOptions } from './crud'
import { d2CrudPlus } from 'd2-crud-plus'
export default {
name: 'messageCenter',
components: {},
mixins: [d2CrudPlus.crud],
data () {
return {
tabActivted: 'send'
}
},
created () {
//
this.crud.options.fetchDetail = this.fetchDetail
},
computed: {
},
methods: {
getCrudOptions () {
return crudOptions(this)
},
pageRequest (query) {
if (this.tabActivted === 'receive') {
return GetSelfReceive({ ...query })
}
return GetList(query)
},
infoRequest (query) {
return GetObj(query)
},
addRequest (row) {
return AddObj(row).then(res => {
const message = {
message_id: res.data.id,
contentType: 'TEXT',
content: '您有新的消息,请到消息中心查看~'
}
this.$websocket.webSocketSend(message)
})
},
updateRequest (row) {
return UpdateObj(row)
},
delRequest (row) {
return DelObj(row.id)
},
//
fetchDetail (index, row) {
if (index == null) {
//
return {}
}
return GetObj(row).then(res => {
return res.data
})
},
handleFormComponentReady (event, key, form) {
// console.log('form component ready:', event, key, form)
},
onTabClick (obj) {
this.doRefresh()
}
}
}
</script>