diff --git a/chunyu_project/asgi.py b/chunyu_project/asgi.py index d8dbd4c..eeeb9b6 100644 --- a/chunyu_project/asgi.py +++ b/chunyu_project/asgi.py @@ -7,11 +7,12 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chunyu_project.settings') django_asgi_app = get_asgi_application() -from chat.routing import websocket_urlpatterns +from chat.routing import websocket_urlpatterns as chat_ws +from user.routing import websocket_urlpatterns as user_ws application = ProtocolTypeRouter({ 'http': django_asgi_app, 'websocket': AuthMiddlewareStack( - URLRouter(websocket_urlpatterns) + URLRouter(chat_ws + user_ws) ), }) diff --git a/chunyu_project/settings.py b/chunyu_project/settings.py index c4276cc..ddf15d9 100644 --- a/chunyu_project/settings.py +++ b/chunyu_project/settings.py @@ -168,30 +168,22 @@ INSTALLED_APPS = [ ] # Database -if os.environ.get('USE_SQLITE') == '1': - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } - } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'chunyu_project', - 'USER': 'root', - 'PASSWORD': 'mysql_NpyMCS', - 'HOST': '192.168.5.7', - 'PORT': '3306', - 'OPTIONS': { - 'charset': 'utf8mb4', - 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", - }, - 'CONN_MAX_AGE': 3600, - 'CONN_HEALTH_CHECKS': True, - } +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'chunyu_project', + 'USER': 'root', + 'PASSWORD': 'mysql_NpyMCS', + 'HOST': '192.168.5.7', + 'PORT': '3306', + 'OPTIONS': { + 'charset': 'utf8mb4', + 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", + }, + 'CONN_MAX_AGE': 3600, + 'CONN_HEALTH_CHECKS': True, } +} # Redis REDIS_HOST = '192.168.5.7' diff --git a/user/consumers.py b/user/consumers.py new file mode 100644 index 0000000..4a6c693 --- /dev/null +++ b/user/consumers.py @@ -0,0 +1,72 @@ +import json +import asyncio +from channels.generic.websocket import AsyncWebsocketConsumer +from django.core.cache import cache + + +class QRStatusConsumer(AsyncWebsocketConsumer): + """ + 扫码登录 WebSocket Consumer. + 前端连接 ws://host/ws/qr-status/{token}/, + 后端在扫码/确认/取消/过期时实时推送状态。 + """ + + async def connect(self): + self.qr_token = self.scope['url_route']['kwargs']['token'] + self.group_name = f"qr_{self.qr_token}" + + # 加入该 token 对应的 group + await self.channel_layer.group_add(self.group_name, self.channel_name) + await self.accept() + + # 检查 token 是否已过期/不存在 + data = cache.get(f"qr_token:{self.qr_token}") + if not data: + await self.send(text_data=json.dumps({"status": "expired"})) + await self.close() + return + + # 启动过期倒计时任务 + self._expire_task = asyncio.create_task(self._expire_watcher(data.get("expires_at"))) + + async def disconnect(self, close_code): + if hasattr(self, 'group_name'): + await self.channel_layer.group_discard(self.group_name, self.channel_name) + if hasattr(self, '_expire_task'): + self._expire_task.cancel() + + async def receive(self, text_data): + """客户端不需要发送消息,忽略即可""" + pass + + # ── group 消息处理器:由 views 通过 group_send 触发 ── + + async def qr_status_update(self, event): + """接收扫码/确认/取消状态推送""" + message = {"status": event["status"]} + if event.get("username"): + message["username"] = event["username"] + if event.get("auth"): + message["auth"] = event["auth"] + await self.send(text_data=json.dumps(message)) + + # confirmed 或 cancelled 后关闭连接 + if event["status"] in ("confirmed", "cancelled", "expired"): + await self.close() + + # ── 内部方法 ── + + async def _expire_watcher(self, expires_at): + """Token 过期时主动推送 expired 并关闭""" + if not expires_at: + return + import time + wait = max(expires_at - time.time(), 0) + 1 # 多等 1 秒确保 Redis 已清除 + try: + await asyncio.sleep(wait) + except asyncio.CancelledError: + return + + # token 已过期 + await self.send(text_data=json.dumps({"status": "expired"})) + await self.close() diff --git a/user/routing.py b/user/routing.py new file mode 100644 index 0000000..104ce3b --- /dev/null +++ b/user/routing.py @@ -0,0 +1,6 @@ +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r'ws/qr-status/(?P[^/]+)/$', consumers.QRStatusConsumer.as_asgi()), +]