diff --git a/backend/constants.py b/backend/constants.py index 22ae6b934055c5e8df049d94a4f72389bd253f25..fbd35ec2c7a9ca245e957229abb74f6cb33c891c 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -1,3 +1,5 @@ +from django_redis import get_redis_connection + # 手机号码正则 REGEX_MOBILE = r'^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$' @@ -7,3 +9,12 @@ REGEX_EMAIL = r'^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$' # 图片后缀名校验正确 REGEX_IMAGE_SUFFIX = r'(\s|\S)+.(bmp|jpg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp' \ r'|jpeg)+$' + +TIER1 = '1' +TIER2 = '2' +TIER3 = '3' +TIER4 = '4' + + +# redis connection +conn = get_redis_connection('default') diff --git a/backend/settings.py b/backend/settings.py index cbf96f6f695ec9d085d2c6643f3e498852a9b10c..328ee4795c8689224a012cb5017932160319234d 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -45,6 +45,8 @@ INSTALLED_APPS = [ 'django_filters', 'crispy_forms', 'notifications', + 'django_apscheduler', + 'django_redis', # --- app --- 'users', @@ -60,6 +62,8 @@ INSTALLED_APPS = [ 'tag', 'notice', 'scratch', + 'history', + # --- DRF --- 'rest_framework', 'rest_framework.authtoken', @@ -117,7 +121,19 @@ DATABASES = { 'PORT': '3306' }, } - +# redisCache +CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://localhost:6379', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + "CONNECTION_POOL_KWARGS": {"max_connections": 100, 'decode_responses': True}, + "PASSWORD": "2002", + }, + "KEY_PREFIX": "qiusuo_" + } +} # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators @@ -221,5 +237,20 @@ APIKEY = "NONE" # 'users.authentication.EmailOrPhoneBackend', # 自定义认证 # ) +# django-apscheduler settings +# Format string for displaying run time timestamps in the Django admin site. The default +# just adds seconds to the standard Django format, which is useful for displaying the timestamps +# for jobs that are scheduled to run on intervals of less than one minute. +# +# See https://docs.djangoproject.com/en/dev/ref/settings/#datetime-format for format string +# syntax details. +APSCHEDULER_DATETIME_FORMAT = "N j, Y, f:s a" +# Maximum run time allowed for jobs that are triggered manually via the Django admin site, which +# prevents admin site HTTP requests from timing out. +# +# Longer running jobs should probably be handed over to a background task processing library +# that supports multiple background worker processes instead (e.g. Dramatiq, Celery, Django-RQ, +# etc. See: https://djangopackages.org/grids/g/workers-queues-tasks/ for popular options). +APSCHEDULER_RUN_NOW_TIMEOUT = 25 # Seconds \ No newline at end of file diff --git a/backend/urls.py b/backend/urls.py index f183a1960e41d74fb9091afa24605c619b3efd15..26db9d1ab418af1c7ab5fc058d1fcbc4238e1036 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -80,6 +80,7 @@ urlpatterns = [ path('tag/', include('tag.urls')), path('notice/', include('notice.urls')), path('scratch/', include('scratch.urls')), + path('user-history/', include('history.urls')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/history/__init__.py b/history/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/history/admin.py b/history/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/history/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/history/apps.py b/history/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..bff7de8b91fd957c2db77a9d48c9196eef8e4a00 --- /dev/null +++ b/history/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HistoryConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'history' diff --git a/history/management/__init__.py b/history/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/history/management/commands/__init__.py b/history/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/history/management/commands/cron.py b/history/management/commands/cron.py new file mode 100644 index 0000000000000000000000000000000000000000..50301d11f3d8057eb4d3aa37cfed0aa47d8e5f2a --- /dev/null +++ b/history/management/commands/cron.py @@ -0,0 +1,84 @@ +from apscheduler.schedulers.blocking import BlockingScheduler +from apscheduler.triggers.cron import CronTrigger +from django.core.management.base import BaseCommand +from django_apscheduler.jobstores import DjangoJobStore +from django_apscheduler.models import DjangoJobExecution +from django_apscheduler import util +import logging +from django.conf import settings + +from backend.constants import conn +from history.models import UserHistory +logger = logging.getLogger(__name__) + + +def persistence_browse_history(): + logging.info("start to persistence browse history") + browsing_user = conn.smembers('browsing_user') + for user in browsing_user: + # 获取当前用户浏览的列表,并取出所有值 + post_list = conn.lrange('history_of_user_' + str(user) + '_post', + start=0, end=-1) + for post in post_list: + post = eval(post) + UserHistory.objects.create(user_id=user, post_id=post['id'], + created_at=post['time']) + conn.delete('history_of_user_' + str(user) + "_post") + passage_list = conn.lrange('history_of_user_' + str(user) + '_passage' + , start=0, end=-1) + for passage in passage_list: + passage = eval(passage) + UserHistory.objects.create(user_id=user, passage_id=passage['id'], created_at=passage['time']) + conn.delete('history_of_user_' + str(user) + "_passage") + logging.info("persistence browse history end") + + +@util.close_old_connections +def delete_old_job_executions(max_age=604_800): + """ + This job deletes APScheduler job execution entries older than `max_age` from the database. + It helps to prevent the database from filling up with old historical records that are no + longer useful. + + :param max_age: The maximum length of time to retain historical job execution records. + Defaults to 7 days. + """ + DjangoJobExecution.objects.delete_old_job_executions(max_age) + + +class Command(BaseCommand): + help = "Runs APScheduler." + + def handle(self, *args, **options): + scheduler = BlockingScheduler(timezone=settings.TIME_ZONE) + scheduler.add_jobstore(DjangoJobStore(), "default") + + scheduler.add_job( + persistence_browse_history, + trigger=CronTrigger(second="*/10"), # Every 10 seconds + id="persistence_browse_history", # The `id` assigned to each job MUST be unique + max_instances=1, + replace_existing=True, + ) + logger.info("Added job 'my_job'.") + + scheduler.add_job( + delete_old_job_executions, + trigger=CronTrigger( + day_of_week="mon", hour="00", minute="00" + ), # Midnight on Monday, before start of the next work week. + id="delete_old_job_executions", + max_instances=1, + replace_existing=True, + ) + logger.info( + "Added weekly job: 'delete_old_job_executions'." + ) + + try: + logger.info("Starting scheduler...") + scheduler.start() + except KeyboardInterrupt: + logger.info("Stopping scheduler...") + scheduler.shutdown() + logger.info("Scheduler shut down successfully!") diff --git a/history/migrations/__init__.py b/history/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/history/models.py b/history/models.py new file mode 100644 index 0000000000000000000000000000000000000000..52d7090223b963513d289e26bb85ec2cf9e5e78a --- /dev/null +++ b/history/models.py @@ -0,0 +1,10 @@ +from django.db import models + + +# Create your models here. +class UserHistory(models.Model): + user_id = models.IntegerField() + post_id = models.IntegerField(null=True) + passage_id = models.IntegerField(null=True) + created_at = models.DateTimeField() + \ No newline at end of file diff --git a/history/serializers.py b/history/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..fca067867edea95813e719a6b61ca05ce1ecb1c7 --- /dev/null +++ b/history/serializers.py @@ -0,0 +1,49 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from history.models import UserHistory +from passages.models import Passage +from posts.models import Post +from users.models import UserInfo + + +class UserPreviewSerializer(serializers.ModelSerializer): + class Meta: + model = UserInfo + fields = ['avatar', 'full_name', 'id'] + + +class PassagePreviewSerializer(serializers.ModelSerializer): + author = UserPreviewSerializer(read_only=True) + + class Meta: + model = Passage + fields = ['id', 'author', 'title'] + + +class PostPreviewSerializer(serializers.ModelSerializer): + author = UserPreviewSerializer(read_only=True) + + class Meta: + model = Passage + fields = ['id', 'author', 'title'] + + +class HistoryDetailSerializer(serializers.ModelSerializer): + + def to_representation(self, data): + data = super().to_representation(data) + try: + data['passage'] = PassagePreviewSerializer( + instance=Passage.objects.get(id=data['passage_id'])).data + del data['passage_id'] + except Exception: + data['post'] = PostPreviewSerializer( + instance=Post.objects.get(id=data['post_id'] + )).data + del data['post_id'] + return data + + class Meta: + model = UserHistory + fields = ['post_id', 'passage_id', 'created_at'] diff --git a/history/tests.py b/history/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/history/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/history/urls.py b/history/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..68110aa3e9c8ce2311351fb391d019d8c6ab3926 --- /dev/null +++ b/history/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from history.views import views +history_api_router = DefaultRouter() + +history_api_router.register( + r'browse_history', + views.UserHistoryViewSet, + basename='browse_history' +) + +urlpatterns = [ + path('', include((history_api_router.urls, 'favorite'), namespace='user_history')), +] \ No newline at end of file diff --git a/history/views.py b/history/views.py new file mode 100644 index 0000000000000000000000000000000000000000..91ea44a218fbd2f408430959283f0419c921093e --- /dev/null +++ b/history/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/history/views/__init__.py b/history/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/history/views/views.py b/history/views/views.py new file mode 100644 index 0000000000000000000000000000000000000000..9502f5dbbae6157aae1478f180809cea222b9829 --- /dev/null +++ b/history/views/views.py @@ -0,0 +1,30 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import viewsets +from rest_framework.decorators import permission_classes +from rest_framework.filters import SearchFilter, OrderingFilter +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from history.models import UserHistory +from history.serializers import HistoryDetailSerializer + + +@permission_classes([IsAuthenticated]) +class UserHistoryViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that not allows users to browse others history. + """ + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()).filter(user_id=request.user.id) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + queryset = UserHistory.objects.all().order_by('-created_at') + serializer_class = HistoryDetailSerializer + filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] diff --git a/index.py b/index.py index 1ad7946558833a6c5c87c9513771f008dc999e8e..875e473df52e092dd4029b1fc8413b20deec01d9 100644 --- a/index.py +++ b/index.py @@ -11,7 +11,7 @@ prefix = ('https://api.qiusuo-mc.cn/' @permission_classes([AllowAny]) def root(request): return Response({ - 'api-version': '1.2', + 'api-version': '1.3', 'swagger-doc': prefix + 'doc/', 'docs': prefix + 'docs/', 'operation-manage': prefix + 'operation-manage/', @@ -26,4 +26,5 @@ def root(request): 'tag': prefix + 'tag/', 'scratch': prefix + 'scratch/', 'favorite-manage/': prefix + 'favorite-manage/', + 'user-history/' : prefix + 'user-history/', }) diff --git a/passages/views/views.py b/passages/views/views.py index 6d828ddf32ed4a605c8352f1a0bb0a141797fc6e..72ff172c1ac96ab54fec37002f9c3930ce07dda8 100644 --- a/passages/views/views.py +++ b/passages/views/views.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from django.utils.datetime_safe import datetime from django_filters.rest_framework import DjangoFilterBackend from rest_framework import serializers, status from rest_framework.decorators import permission_classes, action @@ -7,6 +8,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from backend import helper +from backend.constants import conn from passages.filter import PassageFilter from passages.models import Passage from passages.permissions import PassagePermission @@ -17,6 +19,16 @@ from users import permissions as user_permissions @permission_classes([PassagePermission, user_permissions.IsManualAuthenticatedOrReadOnly]) class PassageViewSet(helper.MyModelViewSet): + def list(self, request, *args, **kwargs): + if hasattr(request, 'GET'): + if 'id' in request.GET: + # 存入redis中缓存 + conn.sadd('browsing_user', str(request.user.id)) + # 将浏览记录存进redis + conn.rpush('history_of_user_' + str(request.user.id) + '_post', + str({'id': request.GET['id'], 'time': str(datetime.now())}), timeout=100) + return super().list(request, *args, **kwargs) + queryset = Passage.objects.all().order_by('created_at').reverse() serializer_class = PassageSerializer filter_class = PassageFilter @@ -46,5 +58,3 @@ class PassageViewSet(helper.MyModelViewSet): "msg": "点赞成功", "likes": instance.likes, }, status=status.HTTP_200_OK) - - diff --git a/posts/views/post_api.py b/posts/views/post_api.py index b922b51b97c028d89d5bb3cf74c8c4696055e77f..93d7946ee9af97eedf9d944d6cdec26505882b6d 100644 --- a/posts/views/post_api.py +++ b/posts/views/post_api.py @@ -1,4 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist +from django.utils.datetime_safe import datetime from django_filters.rest_framework import DjangoFilterBackend from rest_framework import status from rest_framework.decorators import action, permission_classes @@ -7,6 +8,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from backend import helper +from backend.constants import conn from posts.filters import PostFilter from posts.models import Post from posts.permissions import PostPermission @@ -22,6 +24,17 @@ class PostViewSet(helper.MyModelViewSet): 1. 'Basic Auth' 2. JWT认证,请求头Authorization:JWT + 登陆返回的Token """ + + def list(self, request, *args, **kwargs): + if hasattr(request, 'GET'): + if 'id' in request.GET: + # 存入redis中缓存 + conn.sadd('browsing_user', str(request.user.id)) + # 将浏览记录存进redis + conn.rpush('history_of_user_' + str(request.user.id) + '_post', + str({'id': request.GET['id'], 'time': str(datetime.now())})) + return super().list(request, *args, **kwargs) + # 最新的帖子 queryset = Post.objects.all().order_by('-created_at') serializer_class = PostSerializer diff --git a/requirements.txt b/requirements.txt index bd85f854562d6be5b879487a82dffe347b290c13..a006d81d7088f3656799cbc93b60500ebf49a711 100644 Binary files a/requirements.txt and b/requirements.txt differ