From c710f6e5ea9d49254aa3409205ddcb40af6b53e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Wed, 31 Jul 2024 10:26:54 +0800 Subject: [PATCH 1/8] feat: remove unused library --- RyoURL/shortURL/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/RyoURL/shortURL/api.py b/RyoURL/shortURL/api.py index c42aa9d..0f66f57 100644 --- a/RyoURL/shortURL/api.py +++ b/RyoURL/shortURL/api.py @@ -1,13 +1,11 @@ import random import string import datetime -import requests from typing import List, Optional from pydantic import HttpUrl, AnyUrl from ninja import NinjaAPI, Schema -from ninja.responses import Response from ninja.renderers import JSONRenderer from django.shortcuts import get_object_or_404 from django.core.serializers.json import DjangoJSONEncoder From 2275186556816e716ea307e0afd695c69e398691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Wed, 31 Jul 2024 15:50:55 +0800 Subject: [PATCH 2/8] feat: using redis cache fix #22 --- .devcontainer/Dockerfile | 5 +++-- RyoURL/RyoURL/settings.py | 24 +++++++++++++++++++++++- RyoURL/shortURL/views.py | 27 +++++++++++++++++++++++---- requirements.txt | 1 + 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3048a2b..d4a7f36 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,8 +9,9 @@ RUN apt-get update && apt-get install -y \ git \ vim \ curl \ - wget + wget \ + redis-server COPY . . -CMD ["bash"] \ No newline at end of file +CMD service redis-server start && python manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/RyoURL/RyoURL/settings.py b/RyoURL/RyoURL/settings.py index 5c537ef..b2482f7 100644 --- a/RyoURL/RyoURL/settings.py +++ b/RyoURL/RyoURL/settings.py @@ -18,7 +18,6 @@ # 建立專案的根目錄路徑 BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # 快速開發設定 - 不適用於生產環境 # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ @@ -97,6 +96,29 @@ } } +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, +} # Password validation # 密碼驗證 diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index e80b8e7..8cfb2ea 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -1,8 +1,13 @@ +import logging +from django.core.cache import cache from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.http import HttpResponse from .models import Url +# logging 的設定 +logger = logging.getLogger(__name__) + # 將短網址導向原網址的函式 def redirectShortUrl(request, short_string): url = get_object_or_404(Url, short_string=short_string) @@ -12,9 +17,23 @@ def redirectShortUrl(request, short_string): url.delete() return HttpResponse("此短網址已過期並已被刪除。", status=404) - # 更新訪問次數 - url.visit_count += 1 - url.save() + # 處理訪問次數 + cache_key = f'visit_count_{url.id}' # 設定快取的鍵 + visit_count = cache.get(cache_key) # 從快取中取得訪問次數 + if visit_count is None: # 如果快取中沒有訪問次數,那就從資料庫拿 + visit_count = url.visit_count + logger.debug(f'在快取中找不到訪問次數,從資料庫拿: {visit_count}') + visit_count += 1 # 更新訪問次數 + + # 更新快取 + cache.set(cache_key, visit_count, timeout=60*60*24) # 每天重置一次 + logger.debug(f'目前快取中的訪問次數: {visit_count}') + + # 每 10 次訪問更新數據庫 + if visit_count % 10 == 0: + url.visit_count = visit_count + url.save() + logger.debug(f'訪問次數儲存進資料庫: {url.visit_count}') - # 進行重定向 + # 將使用者重新導向至原網址 return redirect(url.orign_url) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6a340f9..8b3a0c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Django==4.2 django-ninja django-cors-headers +django-redis # 工具 python-dotenv From 1d2479a017774fe29a9937c5a41365c81ad0c041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Wed, 31 Jul 2024 16:23:30 +0800 Subject: [PATCH 3/8] refactor: make views.py more module --- RyoURL/shortURL/views.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 8cfb2ea..df188e2 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -8,15 +8,14 @@ # logging 的設定 logger = logging.getLogger(__name__) -# 將短網址導向原網址的函式 -def redirectShortUrl(request, short_string): - url = get_object_or_404(Url, short_string=short_string) - - # 檢查短網址是否已過期 +# 檢查短網址是否過期的函式 +def is_url_expired(url): if url.expire_date and url.expire_date < timezone.now(): url.delete() return HttpResponse("此短網址已過期並已被刪除。", status=404) - + +# 處理訪問次數與快取的函式 +def handle_visit_count(url): # 處理訪問次數 cache_key = f'visit_count_{url.id}' # 設定快取的鍵 visit_count = cache.get(cache_key) # 從快取中取得訪問次數 @@ -29,11 +28,17 @@ def redirectShortUrl(request, short_string): cache.set(cache_key, visit_count, timeout=60*60*24) # 每天重置一次 logger.debug(f'目前快取中的訪問次數: {visit_count}') - # 每 10 次訪問更新數據庫 + # 每 10 次訪問更新數資料庫 if visit_count % 10 == 0: url.visit_count = visit_count url.save() logger.debug(f'訪問次數儲存進資料庫: {url.visit_count}') + +# 將短網址導向原網址的函式 +def redirectShortUrl(request, short_string): + url = get_object_or_404(Url, short_string=short_string) + is_url_expired(url) # 檢查短網址是否過期 + handle_visit_count(url) # 處理訪問次數 # 將使用者重新導向至原網址 return redirect(url.orign_url) \ No newline at end of file From 797af41e5cd3cfcf2fcf72c5660ed0c6ad6d519f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Wed, 31 Jul 2024 16:33:06 +0800 Subject: [PATCH 4/8] fix: bug with is_url_expire fn --- RyoURL/shortURL/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index df188e2..886c6cd 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -12,7 +12,8 @@ def is_url_expired(url): if url.expire_date and url.expire_date < timezone.now(): url.delete() - return HttpResponse("此短網址已過期並已被刪除。", status=404) + return True + return False # 處理訪問次數與快取的函式 def handle_visit_count(url): @@ -37,7 +38,8 @@ def handle_visit_count(url): # 將短網址導向原網址的函式 def redirectShortUrl(request, short_string): url = get_object_or_404(Url, short_string=short_string) - is_url_expired(url) # 檢查短網址是否過期 + if is_url_expired(url): # 檢查短網址是否過期 + return HttpResponse("此短網址已過期並已被刪除。", status=410) # 401 Gone handle_visit_count(url) # 處理訪問次數 # 將使用者重新導向至原網址 From caeb7db5e7a4eb782a653618dea288d681cfa071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Wed, 31 Jul 2024 16:35:47 +0800 Subject: [PATCH 5/8] feat: Add exception handle --- RyoURL/shortURL/views.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 886c6cd..59aa7b8 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -37,10 +37,15 @@ def handle_visit_count(url): # 將短網址導向原網址的函式 def redirectShortUrl(request, short_string): - url = get_object_or_404(Url, short_string=short_string) - if is_url_expired(url): # 檢查短網址是否過期 - return HttpResponse("此短網址已過期並已被刪除。", status=410) # 401 Gone - handle_visit_count(url) # 處理訪問次數 + try: + url = get_object_or_404(Url, short_string=short_string) + if is_url_expired(url): # 檢查短網址是否過期 + return HttpResponse("此短網址已過期並已被刪除。", status=410) # 401 Gone + handle_visit_count(url) # 處理訪問次數 + + # 將使用者重新導向至原網址 + return redirect(url.orign_url) - # 將使用者重新導向至原網址 - return redirect(url.orign_url) \ No newline at end of file + except Exception as e: + logger.error(f'發生錯誤: {e}') + return HttpResponse("發生錯誤,請稍後再試。", status=500) \ No newline at end of file From 89793d7a47a42850996524ab95e12d565815c1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Thu, 1 Aug 2024 10:31:49 +0800 Subject: [PATCH 6/8] fix: #22 , let cache save to DB beforce delete --- RyoURL/shortURL/views.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 59aa7b8..277f2ac 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -17,16 +17,16 @@ def is_url_expired(url): # 處理訪問次數與快取的函式 def handle_visit_count(url): - # 處理訪問次數 + # 處理快取 cache_key = f'visit_count_{url.id}' # 設定快取的鍵 visit_count = cache.get(cache_key) # 從快取中取得訪問次數 if visit_count is None: # 如果快取中沒有訪問次數,那就從資料庫拿 visit_count = url.visit_count logger.debug(f'在快取中找不到訪問次數,從資料庫拿: {visit_count}') - visit_count += 1 # 更新訪問次數 - # 更新快取 - cache.set(cache_key, visit_count, timeout=60*60*24) # 每天重置一次 + # 增加訪問次數 + cache.set(cache_key, visit_count, timeout=60*60*24) # 初始化快取,設定快取時間為 24 小時 + visit_count = cache.incr(cache_key) # 訪問次數加 1 logger.debug(f'目前快取中的訪問次數: {visit_count}') # 每 10 次訪問更新數資料庫 @@ -34,6 +34,14 @@ def handle_visit_count(url): url.visit_count = visit_count url.save() logger.debug(f'訪問次數儲存進資料庫: {url.visit_count}') + + # 處理快取過期的處理(每日至少儲存至資料庫一次) + daily_update_key = f'daily_update_{url.id}' + if not cache.get(daily_update_key): + cache.set(daily_update_key, True, timeout=60*60*24) # 快取 24 小時過期 + url.visit_count = visit_count + url.save() # 存進資料庫 + logger.debug(f'每日訪問次數儲存進資料庫: {url.visit_count}') # 將短網址導向原網址的函式 def redirectShortUrl(request, short_string): From db613095b35146ac5df1c04dbd9fc7f0282c7bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Thu, 1 Aug 2024 10:38:13 +0800 Subject: [PATCH 7/8] refactor: using F() save to DB --- RyoURL/shortURL/views.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 277f2ac..5b001cb 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -1,8 +1,9 @@ import logging from django.core.cache import cache -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404 from django.utils import timezone -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponsePermanentRedirect +from django.db.models import F from .models import Url # logging 的設定 @@ -11,8 +12,11 @@ # 檢查短網址是否過期的函式 def is_url_expired(url): if url.expire_date and url.expire_date < timezone.now(): - url.delete() - return True + try: + url.delete() + return True + except Exception as e: + logger.error(f'刪除過期URL時發生錯誤: {e}') return False # 處理訪問次數與快取的函式 @@ -31,29 +35,30 @@ def handle_visit_count(url): # 每 10 次訪問更新數資料庫 if visit_count % 10 == 0: - url.visit_count = visit_count - url.save() - logger.debug(f'訪問次數儲存進資料庫: {url.visit_count}') + Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + 10) + logger.debug(f'訪問次數儲存進資料庫: {visit_count}') # 處理快取過期的處理(每日至少儲存至資料庫一次) daily_update_key = f'daily_update_{url.id}' if not cache.get(daily_update_key): cache.set(daily_update_key, True, timeout=60*60*24) # 快取 24 小時過期 - url.visit_count = visit_count - url.save() # 存進資料庫 - logger.debug(f'每日訪問次數儲存進資料庫: {url.visit_count}') + Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + (visit_count % 10)) + logger.debug(f'每日訪問次數儲存進資料庫: {visit_count}') # 將短網址導向原網址的函式 def redirectShortUrl(request, short_string): try: url = get_object_or_404(Url, short_string=short_string) if is_url_expired(url): # 檢查短網址是否過期 - return HttpResponse("此短網址已過期並已被刪除。", status=410) # 401 Gone + return HttpResponse("此短網址已過期並已被刪除。", status=410) # 410 Gone handle_visit_count(url) # 處理訪問次數 # 將使用者重新導向至原網址 - return redirect(url.orign_url) + return HttpResponsePermanentRedirect(url.orign_url) + except Url.DoesNotExist: + logger.warning(f'短網址不存在: {short_string}') + return HttpResponse("此短網址不存在。", status=404) except Exception as e: - logger.error(f'發生錯誤: {e}') + logger.error(f'發生錯誤: {e}', exc_info=True) return HttpResponse("發生錯誤,請稍後再試。", status=500) \ No newline at end of file From 03b20d3308eb881084171fe38d74313438407c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=B1=E5=87=8C?= Date: Thu, 1 Aug 2024 10:41:27 +0800 Subject: [PATCH 8/8] refactor: #22, update_visit_count() --- RyoURL/shortURL/views.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 5b001cb..8313f54 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -19,6 +19,11 @@ def is_url_expired(url): logger.error(f'刪除過期URL時發生錯誤: {e}') return False +# 更新資料庫中的訪問次數的函式 +def update_visit_count(visit_count, url): + Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + visit_count) + logger.debug(f'訪問次數儲存進資料庫: {visit_count}') + # 處理訪問次數與快取的函式 def handle_visit_count(url): # 處理快取 @@ -33,10 +38,9 @@ def handle_visit_count(url): visit_count = cache.incr(cache_key) # 訪問次數加 1 logger.debug(f'目前快取中的訪問次數: {visit_count}') - # 每 10 次訪問更新數資料庫 + # 每 10 次訪問更新資料庫 if visit_count % 10 == 0: - Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + 10) - logger.debug(f'訪問次數儲存進資料庫: {visit_count}') + update_visit_count(visit_count, url) # 處理快取過期的處理(每日至少儲存至資料庫一次) daily_update_key = f'daily_update_{url.id}'