fix: S-01反模式清扫收尾(opt_id归一化+music分页钳制+visitors外键归一化)
This commit is contained in:
@@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.videos.models import Video
|
||||
from core.api import render_data
|
||||
from core.api import opt_id, render_data
|
||||
|
||||
from .models import VideoPlayStat
|
||||
|
||||
@@ -27,7 +27,7 @@ class PlayStatReportView(APIView):
|
||||
|
||||
async def post(self, request):
|
||||
try:
|
||||
video_id = int(request.data.get("video_id") or 0)
|
||||
video_id = opt_id(request.data.get("video_id"))
|
||||
watch_seconds = float(request.data.get("watch_seconds") or 0.0)
|
||||
duration = float(request.data.get("duration") or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
|
||||
@@ -1,247 +1,244 @@
|
||||
"""Messages views: system, conversations, messages REST."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db.models import F
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from adrf.views import APIView
|
||||
|
||||
from .models import Conversation, Message, SystemMessage, SystemMessageRead
|
||||
from .serializers import (
|
||||
ConversationSerializer,
|
||||
MessageCreateSerializer,
|
||||
MessageSerializer,
|
||||
SystemMessageSerializer,
|
||||
)
|
||||
from core.api import paginate_cursor, parse_page_size, render_data
|
||||
|
||||
|
||||
def _channel_layer():
|
||||
return get_channel_layer()
|
||||
|
||||
|
||||
def _user_key(uid: int) -> str:
|
||||
return f"user_{uid}"
|
||||
|
||||
|
||||
async def _broadcast(conversation_id: int, sender_id: int, receiver_id: int, payload: dict):
|
||||
layer = _channel_layer()
|
||||
if layer is None:
|
||||
return
|
||||
await layer.group_send(
|
||||
f"conv_{conversation_id}",
|
||||
{"type": "chat.message", "payload": payload},
|
||||
)
|
||||
await layer.group_send(
|
||||
f"user_{receiver_id}",
|
||||
{"type": "user.notify", "payload": {"event": "new_message", **payload}},
|
||||
)
|
||||
|
||||
|
||||
class SystemMessageListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
qs = SystemMessage.objects.filter(
|
||||
Q(user__isnull=True) | Q(user=request.user)
|
||||
).order_by("-id")
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(SystemMessageSerializer(rows, many=True)),
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request):
|
||||
"""Mark messages as read.
|
||||
|
||||
Only the caller's own targeted messages may be touched; a broadcast row
|
||||
(user=null) is shared by every account, so marking it read here would
|
||||
clear it for everyone. Those are marked per-user in the read table.
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
ids = request.data.get("ids") or []
|
||||
if not isinstance(ids, list):
|
||||
return Response({"detail": "ids must be list"}, status=400)
|
||||
if not ids:
|
||||
return Response({"status": "ok", "updated_count": 0})
|
||||
|
||||
own = SystemMessage.objects.filter(pk__in=ids, user=request.user)
|
||||
updated = await own.aupdate(is_read=True)
|
||||
|
||||
broadcast_ids = [
|
||||
sid
|
||||
async for sid in SystemMessage.objects.filter(
|
||||
Q(pk__in=ids) & Q(user__isnull=True)
|
||||
).values_list("id", flat=True)
|
||||
]
|
||||
for sid in broadcast_ids:
|
||||
await SystemMessageRead.objects.aget_or_create(
|
||||
user=request.user, message_id=sid
|
||||
)
|
||||
return Response({"status": "ok", "updated_count": updated})
|
||||
|
||||
|
||||
class UnreadCountView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
read_ids = [
|
||||
mid
|
||||
async for mid in SystemMessageRead.objects.filter(user=request.user).values_list(
|
||||
"message_id", flat=True
|
||||
)
|
||||
]
|
||||
sys_unread = (
|
||||
await SystemMessage.objects.filter(Q(user__isnull=True) | Q(user=request.user), is_read=False)
|
||||
.exclude(pk__in=read_ids)
|
||||
.acount()
|
||||
)
|
||||
dm_unread = await Message.objects.filter(receiver=request.user, is_read=False).acount()
|
||||
return Response({"system_unread": sys_unread, "dm_unread": dm_unread})
|
||||
|
||||
|
||||
class ConversationListCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
qs = (
|
||||
Conversation.objects.filter(Q(user_a=request.user) | Q(user_b=request.user))
|
||||
.select_related("user_a", "user_b")
|
||||
.order_by(F("last_message_at").desc(nulls_last=True), "-id")
|
||||
)
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(
|
||||
ConversationSerializer(rows, many=True, context={"request": request})
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request):
|
||||
try:
|
||||
peer_id = int(request.data.get("peer_id") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return Response({"detail": "invalid peer"}, status=400)
|
||||
if not peer_id or peer_id == request.user.id:
|
||||
return Response({"detail": "invalid peer"}, status=400)
|
||||
a, b = sorted([request.user.id, peer_id])
|
||||
from apps.accounts.models import User
|
||||
|
||||
me = (
|
||||
await Conversation.objects.filter(user_a_id=a, user_b_id=b)
|
||||
.select_related("user_a", "user_b")
|
||||
.afirst()
|
||||
)
|
||||
if me:
|
||||
return Response(
|
||||
await render_data(ConversationSerializer(me, context={"request": request}))
|
||||
)
|
||||
try:
|
||||
peer = await User.objects.aget(pk=peer_id)
|
||||
except User.DoesNotExist:
|
||||
return Response({"detail": "peer not found"}, status=404)
|
||||
|
||||
@sync_to_async(thread_sensitive=True)
|
||||
def _open():
|
||||
# 并发下同一对用户可能同时创建;唯一约束冲突时取已存在那条。
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
conv = Conversation()
|
||||
if a == request.user.id:
|
||||
conv.user_a = request.user
|
||||
conv.user_b = peer
|
||||
else:
|
||||
conv.user_a = peer
|
||||
conv.user_b = request.user
|
||||
try:
|
||||
with transaction.atomic():
|
||||
conv.save()
|
||||
return conv, True
|
||||
except IntegrityError:
|
||||
return Conversation.objects.get(user_a_id=a, user_b_id=b), False
|
||||
|
||||
conv, created = await _open()
|
||||
return Response(
|
||||
await render_data(ConversationSerializer(conv, context={"request": request})),
|
||||
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class ConversationMessagesView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request, conv_id: int):
|
||||
from django.db.models import Q
|
||||
|
||||
try:
|
||||
conv = await Conversation.objects.aget(pk=conv_id)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
if conv.user_a_id != request.user.id and conv.user_b_id != request.user.id:
|
||||
return Response({"detail": "forbidden"}, status=403)
|
||||
|
||||
cursor = request.query_params.get("cursor")
|
||||
page_size = parse_page_size(request.query_params.get("page_size"), default=30)
|
||||
qs = Message.objects.filter(conversation_id=conv_id).order_by("-id")
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
# mark received as read
|
||||
await Message.objects.filter(
|
||||
conversation_id=conv_id, receiver=request.user, is_read=False
|
||||
).aupdate(is_read=True)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": MessageSerializer(list(reversed(rows)), many=True).data,
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request, conv_id: int):
|
||||
s = MessageCreateSerializer(data=request.data)
|
||||
s.is_valid(raise_exception=True)
|
||||
d = s.validated_data
|
||||
try:
|
||||
conv = await Conversation.objects.aget(pk=conv_id)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
if conv.user_a_id != request.user.id and conv.user_b_id != request.user.id:
|
||||
return Response({"detail": "forbidden"}, status=403)
|
||||
receiver_id = conv.user_b_id if conv.user_a_id == request.user.id else conv.user_a_id
|
||||
|
||||
msg = await Message.objects.acreate(
|
||||
conversation=conv,
|
||||
sender=request.user,
|
||||
receiver_id=receiver_id,
|
||||
content_type=d["content_type"],
|
||||
content=d["content"],
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
await Conversation.objects.filter(pk=conv.id).aupdate(
|
||||
last_message=d["content"][:200],
|
||||
last_message_at=timezone.now(),
|
||||
)
|
||||
payload = MessageSerializer(msg).data
|
||||
await _broadcast(conv.id, request.user.id, receiver_id, payload)
|
||||
return Response(payload, status=status.HTTP_201_CREATED)
|
||||
"""Messages views: system, conversations, messages REST."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from channels.layers import get_channel_layer
|
||||
from django.db.models import F
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from adrf.views import APIView
|
||||
|
||||
from .models import Conversation, Message, SystemMessage, SystemMessageRead
|
||||
from .serializers import (
|
||||
ConversationSerializer,
|
||||
MessageCreateSerializer,
|
||||
MessageSerializer,
|
||||
SystemMessageSerializer,
|
||||
)
|
||||
from core.api import opt_id, paginate_cursor, parse_page_size, render_data
|
||||
|
||||
|
||||
def _channel_layer():
|
||||
return get_channel_layer()
|
||||
|
||||
|
||||
def _user_key(uid: int) -> str:
|
||||
return f"user_{uid}"
|
||||
|
||||
|
||||
async def _broadcast(conversation_id: int, sender_id: int, receiver_id: int, payload: dict):
|
||||
layer = _channel_layer()
|
||||
if layer is None:
|
||||
return
|
||||
await layer.group_send(
|
||||
f"conv_{conversation_id}",
|
||||
{"type": "chat.message", "payload": payload},
|
||||
)
|
||||
await layer.group_send(
|
||||
f"user_{receiver_id}",
|
||||
{"type": "user.notify", "payload": {"event": "new_message", **payload}},
|
||||
)
|
||||
|
||||
|
||||
class SystemMessageListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
qs = SystemMessage.objects.filter(
|
||||
Q(user__isnull=True) | Q(user=request.user)
|
||||
).order_by("-id")
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(SystemMessageSerializer(rows, many=True)),
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request):
|
||||
"""Mark messages as read.
|
||||
|
||||
Only the caller's own targeted messages may be touched; a broadcast row
|
||||
(user=null) is shared by every account, so marking it read here would
|
||||
clear it for everyone. Those are marked per-user in the read table.
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
ids = request.data.get("ids") or []
|
||||
if not isinstance(ids, list):
|
||||
return Response({"detail": "ids must be list"}, status=400)
|
||||
if not ids:
|
||||
return Response({"status": "ok", "updated_count": 0})
|
||||
|
||||
own = SystemMessage.objects.filter(pk__in=ids, user=request.user)
|
||||
updated = await own.aupdate(is_read=True)
|
||||
|
||||
broadcast_ids = [
|
||||
sid
|
||||
async for sid in SystemMessage.objects.filter(
|
||||
Q(pk__in=ids) & Q(user__isnull=True)
|
||||
).values_list("id", flat=True)
|
||||
]
|
||||
for sid in broadcast_ids:
|
||||
await SystemMessageRead.objects.aget_or_create(
|
||||
user=request.user, message_id=sid
|
||||
)
|
||||
return Response({"status": "ok", "updated_count": updated})
|
||||
|
||||
|
||||
class UnreadCountView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
read_ids = [
|
||||
mid
|
||||
async for mid in SystemMessageRead.objects.filter(user=request.user).values_list(
|
||||
"message_id", flat=True
|
||||
)
|
||||
]
|
||||
sys_unread = (
|
||||
await SystemMessage.objects.filter(Q(user__isnull=True) | Q(user=request.user), is_read=False)
|
||||
.exclude(pk__in=read_ids)
|
||||
.acount()
|
||||
)
|
||||
dm_unread = await Message.objects.filter(receiver=request.user, is_read=False).acount()
|
||||
return Response({"system_unread": sys_unread, "dm_unread": dm_unread})
|
||||
|
||||
|
||||
class ConversationListCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
from django.db.models import Q
|
||||
|
||||
qs = (
|
||||
Conversation.objects.filter(Q(user_a=request.user) | Q(user_b=request.user))
|
||||
.select_related("user_a", "user_b")
|
||||
.order_by(F("last_message_at").desc(nulls_last=True), "-id")
|
||||
)
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(
|
||||
ConversationSerializer(rows, many=True, context={"request": request})
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request):
|
||||
peer_id = opt_id(request.data.get("peer_id"))
|
||||
if not peer_id or peer_id == request.user.id:
|
||||
return Response({"detail": "invalid peer"}, status=400)
|
||||
a, b = sorted([request.user.id, peer_id])
|
||||
from apps.accounts.models import User
|
||||
|
||||
me = (
|
||||
await Conversation.objects.filter(user_a_id=a, user_b_id=b)
|
||||
.select_related("user_a", "user_b")
|
||||
.afirst()
|
||||
)
|
||||
if me:
|
||||
return Response(
|
||||
await render_data(ConversationSerializer(me, context={"request": request}))
|
||||
)
|
||||
try:
|
||||
peer = await User.objects.aget(pk=peer_id)
|
||||
except User.DoesNotExist:
|
||||
return Response({"detail": "peer not found"}, status=404)
|
||||
|
||||
@sync_to_async(thread_sensitive=True)
|
||||
def _open():
|
||||
# 并发下同一对用户可能同时创建;唯一约束冲突时取已存在那条。
|
||||
from django.db import IntegrityError, transaction
|
||||
|
||||
conv = Conversation()
|
||||
if a == request.user.id:
|
||||
conv.user_a = request.user
|
||||
conv.user_b = peer
|
||||
else:
|
||||
conv.user_a = peer
|
||||
conv.user_b = request.user
|
||||
try:
|
||||
with transaction.atomic():
|
||||
conv.save()
|
||||
return conv, True
|
||||
except IntegrityError:
|
||||
return Conversation.objects.get(user_a_id=a, user_b_id=b), False
|
||||
|
||||
conv, created = await _open()
|
||||
return Response(
|
||||
await render_data(ConversationSerializer(conv, context={"request": request})),
|
||||
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class ConversationMessagesView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request, conv_id: int):
|
||||
from django.db.models import Q
|
||||
|
||||
try:
|
||||
conv = await Conversation.objects.aget(pk=conv_id)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
if conv.user_a_id != request.user.id and conv.user_b_id != request.user.id:
|
||||
return Response({"detail": "forbidden"}, status=403)
|
||||
|
||||
cursor = request.query_params.get("cursor")
|
||||
page_size = parse_page_size(request.query_params.get("page_size"), default=30)
|
||||
qs = Message.objects.filter(conversation_id=conv_id).order_by("-id")
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
# mark received as read
|
||||
await Message.objects.filter(
|
||||
conversation_id=conv_id, receiver=request.user, is_read=False
|
||||
).aupdate(is_read=True)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": MessageSerializer(list(reversed(rows)), many=True).data,
|
||||
}
|
||||
)
|
||||
|
||||
async def post(self, request, conv_id: int):
|
||||
s = MessageCreateSerializer(data=request.data)
|
||||
s.is_valid(raise_exception=True)
|
||||
d = s.validated_data
|
||||
try:
|
||||
conv = await Conversation.objects.aget(pk=conv_id)
|
||||
except Conversation.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
if conv.user_a_id != request.user.id and conv.user_b_id != request.user.id:
|
||||
return Response({"detail": "forbidden"}, status=403)
|
||||
receiver_id = conv.user_b_id if conv.user_a_id == request.user.id else conv.user_a_id
|
||||
|
||||
msg = await Message.objects.acreate(
|
||||
conversation=conv,
|
||||
sender=request.user,
|
||||
receiver_id=receiver_id,
|
||||
content_type=d["content_type"],
|
||||
content=d["content"],
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
await Conversation.objects.filter(pk=conv.id).aupdate(
|
||||
last_message=d["content"][:200],
|
||||
last_message_at=timezone.now(),
|
||||
)
|
||||
payload = MessageSerializer(msg).data
|
||||
await _broadcast(conv.id, request.user.id, receiver_id, payload)
|
||||
return Response(payload, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -8,7 +8,7 @@ from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.api import render_data
|
||||
from core.api import opt_id, render_data
|
||||
|
||||
from .models import Report, SensitiveWord
|
||||
from .serializers import ReportCreateSerializer, ReportSerializer, SensitiveWordSerializer
|
||||
@@ -122,7 +122,7 @@ class AdminAuditView(APIView):
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
async def post(self, request):
|
||||
report_id = int(request.data.get("report_id") or 0)
|
||||
report_id = opt_id(request.data.get("report_id"))
|
||||
action = request.data.get("action") # "hide", "delete", "reject"
|
||||
note = request.data.get("note", "")
|
||||
try:
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
"""Music views (async)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from adrf.views import APIView
|
||||
from django.db.models import Q
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.videos.models import Video
|
||||
from apps.videos.serializers import VideoListSerializer
|
||||
from core.api import paginate_cursor, parse_page_size, render_data
|
||||
|
||||
from .models import Music
|
||||
from .serializers import MusicSerializer
|
||||
|
||||
|
||||
class MusicListView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request):
|
||||
q = (request.query_params.get("q") or "").strip()
|
||||
page_size = min(max(int(request.query_params.get("page_size") or 20), 1), 50)
|
||||
qs = Music.objects.all().order_by("-use_count", "-id")
|
||||
if q:
|
||||
qs = qs.filter(Q(title__icontains=q) | Q(artist__icontains=q))
|
||||
rows = [m async for m in qs[:page_size]]
|
||||
data = await render_data(MusicSerializer(rows, many=True, context={"request": request}))
|
||||
return Response({"results": data})
|
||||
|
||||
|
||||
class MusicDetailView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request, music_id: int):
|
||||
try:
|
||||
m = await Music.objects.select_related("original_author").aget(pk=music_id)
|
||||
except Music.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
data = await render_data(MusicSerializer(m, context={"request": request}))
|
||||
return Response(data)
|
||||
|
||||
|
||||
class MusicVideosListView(APIView):
|
||||
"""Aggregate all videos using this same music / soundtrack."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request, music_id: int):
|
||||
try:
|
||||
m = await Music.objects.aget(pk=music_id)
|
||||
except Music.DoesNotExist:
|
||||
return Response({"detail": "music not found"}, status=404)
|
||||
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
qs = (
|
||||
Video.objects.filter(music=m, status="published")
|
||||
.select_related("author", "music")
|
||||
.prefetch_related("tags")
|
||||
# 排序键与游标键保持同序(hot_score 恒为 0,真正生效的是 id)
|
||||
.order_by("-id")
|
||||
)
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
data = await render_data(VideoListSerializer(rows, many=True, context={"request": request}))
|
||||
return Response({
|
||||
"music": await render_data(MusicSerializer(m, context={"request": request})),
|
||||
"next_cursor": next_cursor,
|
||||
"results": data,
|
||||
})
|
||||
"""Music views (async)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from adrf.views import APIView
|
||||
from django.db.models import Q
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.videos.models import Video
|
||||
from apps.videos.serializers import VideoListSerializer
|
||||
from core.api import paginate_cursor, parse_page_size, render_data
|
||||
|
||||
from .models import Music
|
||||
from .serializers import MusicSerializer
|
||||
|
||||
|
||||
class MusicListView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request):
|
||||
q = (request.query_params.get("q") or "").strip()
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
qs = Music.objects.all().order_by("-use_count", "-id")
|
||||
if q:
|
||||
qs = qs.filter(Q(title__icontains=q) | Q(artist__icontains=q))
|
||||
rows = [m async for m in qs[:page_size]]
|
||||
data = await render_data(MusicSerializer(rows, many=True, context={"request": request}))
|
||||
return Response({"results": data})
|
||||
|
||||
|
||||
class MusicDetailView(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request, music_id: int):
|
||||
try:
|
||||
m = await Music.objects.select_related("original_author").aget(pk=music_id)
|
||||
except Music.DoesNotExist:
|
||||
return Response({"detail": "not found"}, status=404)
|
||||
data = await render_data(MusicSerializer(m, context={"request": request}))
|
||||
return Response(data)
|
||||
|
||||
|
||||
class MusicVideosListView(APIView):
|
||||
"""Aggregate all videos using this same music / soundtrack."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
async def get(self, request, music_id: int):
|
||||
try:
|
||||
m = await Music.objects.aget(pk=music_id)
|
||||
except Music.DoesNotExist:
|
||||
return Response({"detail": "music not found"}, status=404)
|
||||
|
||||
page_size = parse_page_size(request.query_params.get("page_size"))
|
||||
qs = (
|
||||
Video.objects.filter(music=m, status="published")
|
||||
.select_related("author", "music")
|
||||
.prefetch_related("tags")
|
||||
# 排序键与游标键保持同序(hot_score 恒为 0,真正生效的是 id)
|
||||
.order_by("-id")
|
||||
)
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
data = await render_data(VideoListSerializer(rows, many=True, context={"request": request}))
|
||||
return Response({
|
||||
"music": await render_data(MusicSerializer(m, context={"request": request})),
|
||||
"next_cursor": next_cursor,
|
||||
"results": data,
|
||||
})
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
"""Visitor views."""
|
||||
from __future__ import annotations
|
||||
|
||||
from adrf.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.api import paginate_cursor, parse_page_size, render_data
|
||||
|
||||
from .models import Visit
|
||||
from .serializers import VisitSerializer
|
||||
|
||||
|
||||
class VisitView(APIView):
|
||||
"""Record a visit and list recent visits received by the current user."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def post(self, request):
|
||||
owner_id = int(request.data.get("owner_id") or 0)
|
||||
if not owner_id or owner_id == request.user.id:
|
||||
return Response({"detail": "invalid owner"}, status=400)
|
||||
video_id = request.data.get("video_id")
|
||||
source = request.data.get("source", "profile")[:32]
|
||||
await Visit.objects.acreate(
|
||||
visitor=request.user,
|
||||
owner_id=owner_id,
|
||||
video_id=video_id,
|
||||
source=source,
|
||||
)
|
||||
return Response({"status": "recorded"})
|
||||
|
||||
async def get(self, request):
|
||||
cursor = request.query_params.get("cursor")
|
||||
page_size = parse_page_size(request.query_params.get("page_size"), default=20)
|
||||
qs = (
|
||||
Visit.objects.filter(owner=request.user)
|
||||
.select_related("visitor", "video")
|
||||
.order_by("-visited_at", "-id")
|
||||
)
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(VisitSerializer(rows, many=True)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MyVisitsView(APIView):
|
||||
"""Visits made BY the current user."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
rows = [v async for v in Visit.objects.filter(visitor=request.user).order_by("-visited_at")[:50]]
|
||||
return Response(await render_data(VisitSerializer(rows, many=True)))
|
||||
"""Visitor views."""
|
||||
from __future__ import annotations
|
||||
|
||||
from adrf.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from core.api import opt_id, paginate_cursor, parse_page_size, render_data
|
||||
|
||||
from .models import Visit
|
||||
from .serializers import VisitSerializer
|
||||
|
||||
|
||||
class VisitView(APIView):
|
||||
"""Record a visit and list recent visits received by the current user."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def post(self, request):
|
||||
owner_id = opt_id(request.data.get("owner_id"))
|
||||
if not owner_id or owner_id == request.user.id:
|
||||
return Response({"detail": "invalid owner"}, status=400)
|
||||
video_id = opt_id(request.data.get("video_id"))
|
||||
source = request.data.get("source", "profile")[:32]
|
||||
await Visit.objects.acreate(
|
||||
visitor=request.user,
|
||||
owner_id=owner_id,
|
||||
video_id=video_id or None,
|
||||
source=source,
|
||||
)
|
||||
return Response({"status": "recorded"})
|
||||
|
||||
async def get(self, request):
|
||||
cursor = request.query_params.get("cursor")
|
||||
page_size = parse_page_size(request.query_params.get("page_size"), default=20)
|
||||
qs = (
|
||||
Visit.objects.filter(owner=request.user)
|
||||
.select_related("visitor", "video")
|
||||
.order_by("-visited_at", "-id")
|
||||
)
|
||||
rows, next_cursor = await paginate_cursor(
|
||||
qs, request.query_params.get("cursor"), page_size
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"next_cursor": next_cursor,
|
||||
"results": await render_data(VisitSerializer(rows, many=True)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MyVisitsView(APIView):
|
||||
"""Visits made BY the current user."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
async def get(self, request):
|
||||
rows = [v async for v in Visit.objects.filter(visitor=request.user).order_by("-visited_at")[:50]]
|
||||
return Response(await render_data(VisitSerializer(rows, many=True)))
|
||||
|
||||
@@ -31,6 +31,18 @@ def parse_page_size(raw: Any, default: int = DEFAULT_PAGE_SIZE, maximum: int = M
|
||||
return default
|
||||
|
||||
|
||||
def opt_id(raw: Any, default: int = 0) -> int:
|
||||
"""归一化外部 id:非正整数一律回落 default,绝不抛 ValueError 进 500。
|
||||
|
||||
S-01 反模式清扫:外部 id 必须经此函数再进 ORM,禁止 int() 裸调用。
|
||||
"""
|
||||
try:
|
||||
v = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return v if v > 0 else default
|
||||
|
||||
|
||||
async def paginate_cursor(
|
||||
qs,
|
||||
cursor: Any,
|
||||
|
||||
Reference in New Issue
Block a user