feat: 添加短信服务和用户消息通知

This commit is contained in:
xinwen
2021-08-24 14:20:54 +08:00
parent d49d1e1414
commit b1fceca8a6
57 changed files with 1442 additions and 296 deletions

View File

@@ -1,18 +1,18 @@
from django.http import Http404
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveModelMixin
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from common.drf.api import JMSGenericViewSet
from common.permissions import IsObjectOwner, IsSuperUser, OnlySuperUserCanList
from notifications.notifications import system_msgs
from notifications.models import SystemMsgSubscription
from notifications.models import SystemMsgSubscription, UserMsgSubscription
from notifications.backends import BACKEND
from notifications.serializers import (
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer,
UserMsgSubscriptionSerializer,
)
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet')
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet', 'UserMsgSubscriptionViewSet')
class BackendListView(APIView):
@@ -70,3 +70,13 @@ class SystemMsgSubscriptionViewSet(ListModelMixin,
serializer = self.get_serializer(data, many=True)
return Response(data=serializer.data)
class UserMsgSubscriptionViewSet(ListModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
JMSGenericViewSet):
lookup_field = 'user_id'
queryset = UserMsgSubscription.objects.all()
serializer_class = UserMsgSubscriptionSerializer
permission_classes = (IsObjectOwner | IsSuperUser, OnlySuperUserCanList)

View File

@@ -1,11 +1,9 @@
import importlib
from django.utils.translation import gettext_lazy as _
from django.db import models
from .dingtalk import DingTalk
from .email import Email
from .site_msg import SiteMessage
from .wecom import WeCom
from .feishu import FeiShu
client_name_mapper = {}
class BACKEND(models.TextChoices):
@@ -14,17 +12,11 @@ class BACKEND(models.TextChoices):
DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message')
FEISHU = 'feishu', _('FeiShu')
SMS = 'sms', _('SMS')
@property
def client(self):
client = {
self.EMAIL: Email,
self.WECOM: WeCom,
self.DINGTALK: DingTalk,
self.SITE_MSG: SiteMessage,
self.FEISHU: FeiShu,
}[self]
return client
return client_name_mapper[self]
def get_account(self, user):
return self.client.get_account(user)
@@ -37,3 +29,8 @@ class BACKEND(models.TextChoices):
def filter_enable_backends(cls, backends):
enable_backends = [b for b in backends if cls(b).is_enable]
return enable_backends
for b in BACKEND:
m = importlib.import_module(f'.{b}', __package__)
client_name_mapper[b] = m.backend

View File

@@ -14,6 +14,9 @@ class DingTalk(BackendBase):
agentid=settings.DINGTALK_AGENTID
)
def send_msg(self, users, msg):
def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
return self.dingtalk.send_text(accounts, msg)
return self.dingtalk.send_text(accounts, message)
backend = DingTalk

View File

@@ -8,7 +8,10 @@ class Email(BackendBase):
account_field = 'email'
is_enable_field_in_settings = 'EMAIL_HOST_USER'
def send_msg(self, users, subject, message):
def send_msg(self, users, message, subject):
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
accounts, __, __ = self.get_accounts(users)
send_mail(subject, message, from_email, accounts, html_message=message)
backend = Email

View File

@@ -14,6 +14,9 @@ class FeiShu(BackendBase):
app_secret=settings.FEISHU_APP_SECRET
)
def send_msg(self, users, msg):
def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
return self.client.send_text(accounts, msg)
return self.client.send_text(accounts, message)
backend = FeiShu

View File

@@ -5,10 +5,13 @@ from .base import BackendBase
class SiteMessage(BackendBase):
account_field = 'id'
def send_msg(self, users, subject, message):
def send_msg(self, users, message, subject):
accounts, __, __ = self.get_accounts(users)
Client.send_msg(subject, message, user_ids=accounts)
@classmethod
def is_enable(cls):
return True
backend = SiteMessage

View File

@@ -0,0 +1,25 @@
from django.conf import settings
from common.message.backends.sms.alibaba import AlibabaSMS as Client
from .base import BackendBase
class SMS(BackendBase):
account_field = 'phone'
is_enable_field_in_settings = 'AUTH_SMS'
def __init__(self):
"""
暂时只对接阿里,之后再扩展
"""
self.client = Client(
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
)
def send_msg(self, users, sign_name: str, template_code: str, template_param: dict):
accounts, __, __ = self.get_accounts(users)
return self.client.send_sms(accounts, sign_name, template_code, template_param)
backend = SMS

View File

@@ -15,6 +15,9 @@ class WeCom(BackendBase):
agentid=settings.WECOM_AGENTID
)
def send_msg(self, users, msg):
def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users)
return self.wecom.send_text(accounts, msg)
return self.wecom.send_text(accounts, message)
backend = WeCom

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.12 on 2021-08-23 08:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('notifications', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='usermsgsubscription',
name='message_type',
),
migrations.AlterField(
model_name='usermsgsubscription',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL, unique=True),
),
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 3.1.12 on 2021-08-23 07:52
from django.db import migrations
def init_user_msg_subscription(apps, schema_editor):
UserMsgSubscription = apps.get_model('notifications', 'UserMsgSubscription')
User = apps.get_model('users', 'User')
to_create = []
users = User.objects.all()
for user in users:
receive_backends = []
receive_backends.append('site_msg')
if user.email:
receive_backends.append('email')
if user.wecom_id:
receive_backends.append('wecom')
if user.dingtalk_id:
receive_backends.append('dingtalk')
if user.feishu_id:
receive_backends.append('feishu')
to_create.append(UserMsgSubscription(user=user, receive_backends=receive_backends))
UserMsgSubscription.objects.bulk_create(to_create)
print(f'\n Init user message subscription: {len(to_create)}')
class Migration(migrations.Migration):
dependencies = [
('users', '0036_user_feishu_id'),
('notifications', '0002_auto_20210823_1619'),
]
operations = [
migrations.RunPython(init_user_msg_subscription)
]

View File

@@ -6,12 +6,11 @@ __all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
class UserMsgSubscription(JMSModel):
message_type = models.CharField(max_length=128)
user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE)
user = models.ForeignKey('users.User', unique=True, related_name='user_msg_subscriptions', on_delete=models.CASCADE)
receive_backends = models.JSONField(default=list)
def __str__(self):
return f'{self.message_type}'
return f'{self.user} subscription: {self.receive_backends}'
class SystemMsgSubscription(JMSModel):

View File

@@ -1,12 +1,14 @@
from typing import Iterable
import traceback
from itertools import chain
from collections import defaultdict
from django.db.utils import ProgrammingError
from celery import shared_task
from common.utils import lazyproperty
from users.models import User
from notifications.backends import BACKEND
from .models import SystemMsgSubscription
from .models import SystemMsgSubscription, UserMsgSubscription
__all__ = ('SystemMessage', 'UserMessage')
@@ -69,37 +71,49 @@ class Message(metaclass=MessageType):
for backend in backends:
try:
backend = BACKEND(backend)
if not backend.is_enable:
continue
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
msg = get_msg_method()
try:
msg = get_msg_method()
except NotImplementedError:
continue
client = backend.client()
if isinstance(msg, dict):
client.send_msg(users, **msg)
else:
client.send_msg(users, msg)
client.send_msg(users, **msg)
except:
traceback.print_exc()
def get_common_msg(self) -> str:
def get_common_msg(self) -> dict:
raise NotImplementedError
def get_dingtalk_msg(self) -> str:
@lazyproperty
def common_msg(self) -> dict:
return self.get_common_msg()
def get_wecom_msg(self) -> str:
return self.get_common_msg()
# --------------------------------------------------------------
# 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
def get_dingtalk_msg(self) -> dict:
return self.common_msg
def get_wecom_msg(self) -> dict:
return self.common_msg
def get_feishu_msg(self) -> dict:
return self.common_msg
def get_email_msg(self) -> dict:
msg = self.get_common_msg()
subject = f'{msg[:80]} ...' if len(msg) >= 80 else msg
return {
'subject': subject,
'message': msg
}
return self.common_msg
def get_site_msg_msg(self) -> dict:
return self.get_email_msg()
return self.common_msg
def get_sms_msg(self) -> dict:
raise NotImplementedError
# --------------------------------------------------------------
class SystemMessage(Message):
@@ -125,4 +139,16 @@ class SystemMessage(Message):
class UserMessage(Message):
pass
user: User
def __init__(self, user):
self.user = user
def publish(self):
"""
发送消息到每个用户配置的接收方式上
"""
sub = UserMsgSubscription.objects.get(user=self.user)
self.send_msg([self.user], sub.receive_backends)

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from notifications.models import SystemMsgSubscription
from notifications.models import SystemMsgSubscription, UserMsgSubscription
class SystemMsgSubscriptionSerializer(BulkModelSerializer):
@@ -27,3 +27,11 @@ class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
category = serializers.CharField()
category_label = serializers.CharField()
children = SystemMsgSubscriptionSerializer(many=True)
class UserMsgSubscriptionSerializer(BulkModelSerializer):
receive_backends = serializers.ListField(child=serializers.CharField(), read_only=False)
class Meta:
model = UserMsgSubscription
fields = ('user_id', 'receive_backends',)

View File

@@ -6,14 +6,14 @@ from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from django.db.utils import DEFAULT_DB_ALIAS
from django.apps import apps as global_apps
from django.apps import AppConfig
from notifications.backends import BACKEND
from users.models import User
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import SiteMessage, SystemMsgSubscription
from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription
from .notifications import SystemMessage
@@ -82,3 +82,13 @@ def create_system_messages(app_config: AppConfig, **kwargs):
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
except ModuleNotFoundError:
pass
@receiver(post_save, sender=User)
def on_user_post_save(sender, instance, created, **kwargs):
if created:
receive_backends = []
for backend in BACKEND:
if backend.get_account(instance):
receive_backends.append(backend)
UserMsgSubscription.objects.create(user=instance, receive_backends=receive_backends)

View File

@@ -2,9 +2,12 @@ from django.db.models import F
from django.db import transaction
from common.utils.timezone import now
from common.utils import get_logger
from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
logger = get_logger(__file__)
class SiteMessageUtil:
@@ -14,6 +17,11 @@ class SiteMessageUtil:
if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified')
logger.info(f'Site message send: '
f'user_ids={user_ids} '
f'group_ids={group_ids} '
f'subject={subject} '
f'message={message}')
with transaction.atomic():
site_msg = SiteMessageModel.objects.create(
subject=subject, message=message,

View File

@@ -8,6 +8,7 @@ app_name = 'notifications'
router = BulkRouter()
router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription')
router.register('user-msg-subscription', api.UserMsgSubscriptionViewSet, 'user-msg-subscription')
router.register('site-message', api.SiteMessageViewSet, 'site-message')
urlpatterns = [