feat: add user websocket channel (consumers+routing), drop USE_SQLITE branch
This commit is contained in:
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/qr-status/(?P<token>[^/]+)/$', consumers.QRStatusConsumer.as_asgi()),
|
||||
]
|
||||
Reference in New Issue
Block a user