diff --git a/RyoURL/shortURL/api.py b/RyoURL/shortURL/api.py index 0f66f57..65352a8 100644 --- a/RyoURL/shortURL/api.py +++ b/RyoURL/shortURL/api.py @@ -8,6 +8,7 @@ from ninja import NinjaAPI, Schema from ninja.renderers import JSONRenderer from django.shortcuts import get_object_or_404 +from django.utils.crypto import get_random_string from django.core.serializers.json import DjangoJSONEncoder from .models import Url @@ -40,13 +41,26 @@ class UrlSchema(Schema): class ErrorSchema(Schema): message: str +# BASE62 編碼的函式 +def base62_encode(num): + base62 = string.digits + string.ascii_letters + if num == 0: + return base62[0] + array = [] + while num: + num, rem = divmod(num, 62) + array.append(base62[rem]) + array.reverse() + return ''.join(array) + # 產生隨機短網址的函式 -def generator_short_url(length = 6): - char = string.ascii_letters + string.digits - while True: - short_url = ''.join(random.choices(char, k=length)) - if not Url.objects.filter(short_url=short_url).exists(): - return short_url # 如果短網址不存在 DB 中,則回傳此短網址 +def generator_short_url(orign_url: str, length = 6): + hash_value = abs(hash(orign_url)) # 取得原網址的 hash 值 + encode = base62_encode(hash_value) # 將 hash 值轉換為 BASE62 編碼 + if len(encode) < length: + encode += get_random_string(length - len(encode), string.ascii_letters + string.digits) + return encode + return encode[:length] # 處理短網址域名的函式 def handle_domain(request, short_string): @@ -71,7 +85,7 @@ def index(request): # POST : 新增短網址 API /short_url @api.post("short-url", response={200: UrlSchema, 404: ErrorSchema}) def create_short_url(request, orign_url: HttpUrl, expire_date: Optional[datetime.datetime] = None): - short_string = generator_short_url() + short_string = generator_short_url(orign_url) short_url = HttpUrl(handle_domain(request, short_string)) url = create_url_entry(orign_url, short_string, short_url, expire_date) return 200, url diff --git a/RyoURL/shortURL/views.py b/RyoURL/shortURL/views.py index 4a139f6..8f6c70a 100644 --- a/RyoURL/shortURL/views.py +++ b/RyoURL/shortURL/views.py @@ -1,16 +1,24 @@ import logging + +from redis import RedisError from django.core.cache import cache from django.shortcuts import get_object_or_404 from django.utils import timezone from django.http import HttpResponse, HttpResponseRedirect from django.db.models import F +from typing import Optional + from .models import Url # logging 的設定 logger = logging.getLogger(__name__) +# 常數設定 +CACHE_TIMEOUT = 60 * 60 * 24 # 快取過期時間為 24 小時 +VISIT_COUNT_UPDATE_THRESHOLD = 10 # 訪問次數更新的閾值 + # 檢查短網址是否過期的函式 -def is_url_expired(url): +def is_url_expired(url: Url) -> bool: if url.expire_date and url.expire_date < timezone.now(): try: url.delete() @@ -20,37 +28,46 @@ def is_url_expired(url): return False # 更新資料庫中的訪問次數的函式 -def update_visit_count(visit_count, url): +def update_visit_count(visit_count: int, url: Url) -> None: Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + visit_count) logger.debug(f'訪問次數儲存進資料庫: {visit_count}') # 處理訪問次數與快取的函式 -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}') - - # 增加訪問次數 - cache.set(cache_key, visit_count, timeout=60*60*24) # 初始化快取,設定快取時間為 24 小時 - visit_count = cache.incr(cache_key) # 訪問次數加 1 - logger.debug(f'目前快取中的訪問次數: {visit_count}') +def handle_visit_count(url: Url) -> None: + try: + # 處理快取 + 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}') + + # 增加訪問次數 + cache.set(cache_key, visit_count, timeout=CACHE_TIMEOUT) # 初始化快取,設定快取時間為 24 小時 + visit_count = cache.incr(cache_key) # 訪問次數加 1 + logger.debug(f'目前快取中的訪問次數: {visit_count}') + + # 每 10 次訪問更新資料庫 + if visit_count % VISIT_COUNT_UPDATE_THRESHOLD == 0: + update_visit_count(visit_count, url) + + # 處理快取過期的處理(每日至少儲存至資料庫一次) + daily_update_key = f'daily_update_{url.id}' + if not cache.get(daily_update_key): + cache.set(daily_update_key, True, timeout=CACHE_TIMEOUT) # 快取 24 小時過期 + Url.objects.filter(id=url.id).update(visit_count=F('visit_count') + (visit_count % VISIT_COUNT_UPDATE_THRESHOLD)) + logger.debug(f'每日訪問次數儲存進資料庫: {visit_count}') - # 每 10 次訪問更新資料庫 - if visit_count % 10 == 0: - update_visit_count(visit_count, url) + # 如果 Redis 連線失敗,直接更新資料庫 + except RedisError as e: + logger.error(f'與 Redis 操作失敗,直接更新資料庫: {e}', exc_info=True) + update_visit_count(1, url) + # 其他錯誤 + except Exception as e: + logger.error(f'處理訪問次數時發生錯誤: {e}', exc_info=True) - # 處理快取過期的處理(每日至少儲存至資料庫一次) - 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.objects.filter(id=url.id).update(visit_count=F('visit_count') + (visit_count % 10)) - logger.debug(f'每日訪問次數儲存進資料庫: {visit_count}') - # 將短網址導向原網址的函式 -def redirectShortUrl(request, short_string): +def redirectShortUrl(request, short_string: str) -> HttpResponse: try: url = get_object_or_404(Url, short_string=short_string) if is_url_expired(url): # 檢查短網址是否過期