from adrf.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework import status from utils.response_codes import ResponseCode, create_standardized_response, create_standardized_error_response from tool.models import Tool, ToolFavorite from article.models import Article, ArticleFavorite from learn.models import Course, CourseFavorite from apidirectory.models import ApiItem, ApiFavorite from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response class MyFavoritesView(APIView): permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['收藏'], operation_summary='获取我的收藏', operation_description='获取当前用户的收藏列表,支持按类型筛选(tool/article/course/all)', manual_parameters=[ openapi.Parameter('type', openapi.IN_QUERY, description='收藏类型(all/tool/article/course),默认all', type=openapi.TYPE_STRING), ], responses={200: success_response, 401: unauthorized_response}, ) async def get(self, request): content_type = request.query_params.get('type', 'all') results = [] if content_type == 'all' or content_type == 'tool': tool_favs = ToolFavorite.objects.filter( user=request.user ).select_related('tool', 'tool__category').order_by('-created_at') async for tf in tool_favs: results.append({ 'id': tf.tool.id, 'type': 'tool', 'title': tf.tool.name, 'description': tf.tool.description, 'icon': tf.tool.icon, 'color': getattr(tf.tool, 'color', ''), 'url_path': tf.tool.url_path, 'category': getattr(tf.tool.category, 'name', '') if tf.tool.category else '', 'created_at': tf.created_at.strftime('%Y-%m-%d %H:%M:%S'), }) if content_type == 'all' or content_type == 'article': article_favs = ArticleFavorite.objects.filter( user=request.user ).select_related('article', 'article__author').order_by('-created_at') async for af in article_favs: results.append({ 'id': af.article.id, 'type': 'article', 'title': af.article.title, 'description': af.article.excerpt or '', 'category': af.article.category, 'tags': getattr(af.article, 'tags', []), 'author': getattr(af.article.author, 'nickname', '') if af.article.author else '', 'author_id': af.article.author_id, 'views': af.article.views, 'likes': af.article.likes, 'cover_image': '', 'created_at': af.created_at.strftime('%Y-%m-%d %H:%M:%S'), }) if content_type == 'all' or content_type == 'course': course_favs = CourseFavorite.objects.filter( user=request.user ).select_related('course', 'course__author').order_by('-created_at') async for cf in course_favs: results.append({ 'id': cf.course.id, 'type': 'course', 'title': cf.course.title, 'description': cf.course.description, 'category': cf.course.category, 'level': cf.course.level, 'icon_name': getattr(cf.course, 'icon_name', ''), 'color': getattr(cf.course, 'color', ''), 'chapters_count': await cf.course.chapters.acount() if hasattr(cf.course, 'chapters') else 0, 'author': getattr(cf.course.author, 'nickname', '') if cf.course.author else '', 'author_id': cf.course.author_id, 'created_at': cf.created_at.strftime('%Y-%m-%d %H:%M:%S'), }) if content_type == 'all' or content_type == 'api': api_favs = ApiFavorite.objects.filter( user=request.user ).select_related('api_item').order_by('-created_at') async for af in api_favs: results.append({ 'id': af.api_item.id, 'type': 'api', 'title': af.api_item.name, 'description': af.api_item.description or '', 'icon': 'ApiOutlined', 'color': af.api_item.color or '#faad14', 'url_path': af.api_item.url_path or '', 'category': getattr(af.api_item.category, 'name', '') if af.api_item.category else '', 'created_at': af.created_at.strftime('%Y-%m-%d %H:%M:%S'), }) counts = { 'total': len(results), 'tool': await ToolFavorite.objects.filter(user=request.user).acount(), 'article': await ArticleFavorite.objects.filter(user=request.user).acount(), 'course': await CourseFavorite.objects.filter(user=request.user).acount(), 'api': await ApiFavorite.objects.filter(user=request.user).acount(), } return create_standardized_response( data={'results': results, 'counts': counts}, code=ResponseCode.SUCCESS ) class FavoriteToggleView(APIView): """统一收藏切换端点:按 type + id 切换 tool/article/course/api 收藏状态。 修复:Android 端 FavoritesApi.toggleFavorite 调用的 user/favorites/toggle/ 此前不存在(404)。响应契约与客户端对齐:{is_favorited, favorites_count}。 """ permission_classes = [IsAuthenticated] @swagger_auto_schema( tags=['收藏'], operation_summary='切换收藏状态', operation_description='按类型切换收藏(tool/article/course/api),返回切换后的状态', request_body=openapi.Schema( type=openapi.TYPE_OBJECT, required=['type', 'target_id'], properties={ 'type': openapi.Schema(type=openapi.TYPE_STRING, enum=['tool', 'article', 'course', 'api']), 'target_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='目标对象 ID'), }, ), responses={200: success_response, 400: error_response, 401: unauthorized_response, 404: not_found_response}, ) async def post(self, request): fav_type = request.data.get('type') target_id = request.data.get('target_id') if fav_type not in ('tool', 'article', 'course', 'api'): return create_standardized_error_response( message='type 必须为 tool/article/course/api 之一', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) try: target_id = int(target_id) except (TypeError, ValueError): return create_standardized_error_response( message='target_id 必须为整数', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_400_BAD_REQUEST ) # (模型, FK 字段名, 目标模型) mapping = { 'tool': (ToolFavorite, 'tool', Tool), 'article': (ArticleFavorite, 'article', Article), 'course': (CourseFavorite, 'course', Course), 'api': (ApiFavorite, 'api_item', ApiItem), } model, fk_field, target_model = mapping[fav_type] try: await target_model.objects.aget(id=target_id) except target_model.DoesNotExist: return create_standardized_error_response( message='目标对象不存在', code=ResponseCode.PARAMETER_ERROR, status_code=status.HTTP_404_NOT_FOUND ) existing = await model.objects.filter(user=request.user, **{fk_field: target_id}).afirst() if existing is not None: await model.objects.filter(user=request.user, **{fk_field: target_id}).adelete() favorited = False else: await model.objects.acreate(user=request.user, **{fk_field: target_id}) favorited = True favorites_count = await model.objects.filter(user=request.user).acount() return create_standardized_response( data={'is_favorited': favorited, 'favorites_count': favorites_count, 'type': fav_type, 'target_id': target_id}, code=ResponseCode.SUCCESS )