from django.utils import timezone from django.http import JsonResponse from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.permissions import IsAuthenticated from .models import UserPreference, TravelPlan from .models import City, Attraction import logging logger = logging.getLogger(__name__) from .serializers import ( CitySerializer, AttractionSerializer, UserPreferenceSerializer, TravelPlanSerializer, UserPreferenceCreateSerializer, TravelPlanCreateSerializer ) from .services import TravelPlanService, CacheService, MoonshotAIService from django.contrib.auth import get_user_model User = get_user_model() # views.py from rest_framework.response import Response from .models import City class CityListView(APIView): permission_classes = [] # 无需登录 def get(self, request): try: # Return all cities instead of just hot ones cities = City.objects.all() # Changed from filter(is_hot=True) serializer = CitySerializer(cities, many=True) return Response({ "status": "success", "data": serializer.data }, status=status.HTTP_200_OK) except Exception as e: return Response({ "status": "error", "message": str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class AttractionListView(APIView): def get(self, request, city_id): attractions = CacheService.get_attractions_by_city(city_id) return Response(attractions) class UserPreferenceView(APIView): permission_classes = [IsAuthenticated] def get(self, request): preference = UserPreference.objects.filter(user=request.user).first() if not preference: return Response({'detail': '未找到偏好设置'}, status=status.HTTP_404_NOT_FOUND) serializer = UserPreferenceSerializer(preference) return Response(serializer.data) def post(self, request): serializer = UserPreferenceCreateSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): # 确保每个用户只有一个偏好设置 UserPreference.objects.filter(user=request.user).delete() preference = serializer.save(user=request.user) return Response(UserPreferenceSerializer(preference).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # class TravelPlanView(APIView): # permission_classes = [] # 确保无认证要求 # # def post(self, request): # try: # # 验证必要字段 # required_fields = ['city_ids', 'days', 'interests', 'transport'] # if not all(field in request.data for field in required_fields): # return Response( # {'status': 'error', 'message': '缺少必要参数'}, # status=status.HTTP_400_BAD_REQUEST # ) # # # 准备数据 # plan_data = { # 'city_ids': request.data['city_ids'], # 'days': request.data['days'], # 'interests': request.data['interests'], # 'transport': request.data['transport'], # 'custom_requirements': request.data.get('custom_requirements', '') # } # # # 调用服务生成计划 # travel_plan = TravelPlanService.create_travel_plan(plan_data) # # if travel_plan: # serializer = TravelPlanSerializer(travel_plan) # return Response({ # 'status': 'success', # 'data': serializer.data # }, status=status.HTTP_201_CREATED) # else: # return Response({ # 'status': 'error', # 'message': '生成旅行计划失败' # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # # except Exception as e: # import traceback # traceback.print_exc() # 打印完整错误堆栈 # return Response({ # 'status': 'error', # 'message': str(e) # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class TravelPlanView(APIView): permission_classes = [] # 无需认证 def post(self, request): """ 生成旅行计划(不保存到数据库) 请求参数: - city_ids: 城市ID列表 - days: 旅行天数 - interests: 兴趣标签列表 - transport: 交通方式 - custom_requirements: 自定义需求(可选) """ try: # 1. 验证必要参数 required_fields = ['city_ids', 'days', 'interests', 'transport'] if not all(field in request.data for field in required_fields): return Response( {'status': 'error', 'message': '缺少必要参数'}, status=status.HTTP_400_BAD_REQUEST ) # 2. 准备AI生成所需数据 plan_data = { 'city_ids': request.data['city_ids'], 'days': request.data['days'], 'interests': request.data['interests'], 'transport': request.data['transport'], 'custom_requirements': request.data.get('custom_requirements', '') } # 3. 直接调用Moonshot AI生成计划(不创建数据库记录) generated_plan = MoonshotAIService.generate_travel_plan(plan_data) if not generated_plan or 'error' in generated_plan: error_msg = generated_plan.get('details', '生成旅行计划失败') if generated_plan else 'AI服务无响应' return Response({ 'status': 'error', 'message': error_msg }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 4. 返回生成的计划数据(不包含数据库ID等字段) return Response({ 'status': 'success', 'data': { 'title': generated_plan.get('title', '自定义旅行计划'), 'description': generated_plan.get('description', ''), 'days': generated_plan.get('days', []), 'suitable_for': generated_plan.get('suitable_for', '') } }, status=status.HTTP_200_OK) except Exception as e: import traceback traceback.print_exc() # 打印错误堆栈 return Response({ 'status': 'error', 'message': f'服务器内部错误: {str(e)}' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # views.py class RedTourismPlanView(APIView): """ 红色旅游路线规划API - 稳定版 返回标准化的行程数据结构,确保前端能正确渲染 """ permission_classes = [] def post(self, request): try: # 1. 参数验证 city_ids = request.data.get('city_ids') days = request.data.get('days') if not city_ids or not isinstance(city_ids, list): return Response( {'status': 'error', 'message': '请提供有效的城市ID列表'}, status=status.HTTP_400_BAD_REQUEST ) if not days or not isinstance(days, int) or days < 1 or days > 7: return Response( {'status': 'error', 'message': '请提供1-7天的有效天数'}, status=status.HTTP_400_BAD_REQUEST ) # 2. 准备请求数据 interests = request.data.get('interests', []) if not isinstance(interests, list): interests = [] if 'red-tourism' not in interests: interests.append('red-tourism') plan_data = { 'city_ids': city_ids, 'days': days, 'interests': interests, 'transport': request.data.get('transport', 'public'), 'custom_requirements': request.data.get('custom_requirements', ''), 'is_red_tourism': True } # 3. 调用AI服务 generated_plan = MoonshotAIService.generate_travel_plan(plan_data) logger.info(f"Generated plan: {generated_plan}") if not generated_plan: return Response( {'status': 'error', 'message': 'AI服务无响应'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) if 'error' in generated_plan: return Response( {'status': 'error', 'message': generated_plan.get('message', 'AI生成失败')}, status=status.HTTP_400_BAD_REQUEST ) # 4. 标准化返回数据 standardized_plan = self._standardize_plan(generated_plan, days) return Response({ 'status': 'success', 'data': standardized_plan }) except Exception as e: logger.error(f"API error: {str(e)}", exc_info=True) return Response({ 'status': 'error', 'message': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _standardize_plan(self, plan_data, requested_days): """确保返回数据结构的完整性和一致性""" result = { 'title': plan_data.get('title', f'{requested_days}天红色之旅'), 'description': plan_data.get('description', '通过参观革命历史遗址学习党史'), 'days': [], 'red_tourism_tips': plan_data.get('red_tourism_tips', [ "请着装整洁,保持肃穆", "建议提前学习相关历史知识" ]), 'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等') } # 处理每日行程 for day in plan_data.get('days', []): standardized_day = { 'day': day.get('day', 1), 'theme': day.get('theme', '红色教育'), 'transport': day.get('transport', '公共交通'), 'attractions': [], 'educational_points': day.get('educational_points', [ "学习革命历史", "传承红色精神" ]) } # 处理景点数据 for attr in day.get('attractions', []): standardized_attr = { 'id': attr.get('id', 0), 'name': attr.get('name', '红色教育基地'), 'image': attr.get('image', '/images/default-red.jpg'), 'is_red_tourism': attr.get('is_red_tourism', True), 'educational_value': attr.get('educational_value', '高'), 'visit_time': attr.get('visit_time', '09:00-17:00'), 'ticket_price': attr.get('ticket_price', '免费') } standardized_day['attractions'].append(standardized_attr) result['days'].append(standardized_day) return result class RedAttractionDetailView(APIView): """ 红色景点详情API 权限:无需认证 """ permission_classes = [] def get(self, request, attraction_id): try: attraction = Attraction.objects.get(id=attraction_id) if '红色旅游' not in attraction.tags: return Response({ 'status': 'error', 'message': '该景点不是红色旅游景点' }, status=status.HTTP_400_BAD_REQUEST) # 获取相关红色景点 related_attractions = Attraction.objects.filter( city=attraction.city, tags__contains="红色旅游" ).exclude(id=attraction_id)[:3] serializer = AttractionSerializer(attraction) related_serializer = AttractionSerializer(related_attractions, many=True) # 构建响应数据 response_data = { 'detail': serializer.data, 'historical_background': attraction.history or "暂无详细历史背景", 'educational_significance': self._get_educational_content(attraction), 'related_attractions': related_serializer.data, 'visiting_etiquette': self._get_visiting_etiquette(attraction) } return Response({ 'status': 'success', 'data': response_data }) except Attraction.DoesNotExist: return Response( {'status': 'error', 'message': '景点不存在'}, status=status.HTTP_404_NOT_FOUND ) def _get_educational_content(self, attraction): """生成教育意义内容""" content = [] if 'memorial' in attraction.tags: content.append("此处是重要的革命纪念地,具有深刻的教育意义") content.append("适合开展爱国主义主题教育活动") if 'battle' in attraction.tags: content.append("这里发生过重要历史战役,展现了革命先烈的英勇精神") return content def _get_visiting_etiquette(self, attraction): """生成参观礼仪指南""" etiquette = [ "请保持庄严肃穆", "勿大声喧哗" ] if 'memorial' in attraction.tags: etiquette.append("纪念馆内请勿拍照") return etiquette from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status import json import logging from django.core.exceptions import ValidationError logger = logging.getLogger(__name__) class RedTourismRegenerateView(APIView): """ 红色旅游路线重新生成API - 增强版 功能: 1. 基于用户输入和原始行程生成优化路线 2. 严格验证输入参数 3. 提供标准化的响应格式 """ permission_classes = [] def post(self, request): try: # === 1. 数据预处理 === request_data = self._parse_and_validate_request(request) # === 2. 业务逻辑处理 === generated_plan = self._generate_red_tourism_plan(request_data) # === 3. 响应处理 === return Response({ 'status': 'success', 'data': self._standardize_plan(generated_plan, request_data['days']), 'meta': { 'generated_at': timezone.now().isoformat(), 'version': '1.1' } }) except ValidationError as e: logger.warning(f"参数验证失败: {str(e)}") return Response({ 'status': 'error', 'message': str(e), 'code': 'INVALID_PARAMS' }, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(f"服务器错误: {str(e)}", exc_info=True) return Response({ 'status': 'error', 'message': '服务器处理请求时发生错误', 'code': 'SERVER_ERROR' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def _parse_and_validate_request(self, request): """解析并验证请求数据""" try: # 数据格式检查 if request.content_type != 'application/json': raise ValidationError("只支持JSON格式数据") request_data = request.data.copy() if hasattr(request, 'data') else json.loads(request.body) # 必填字段检查 required_fields = ['city_ids', 'days', 'previous_plan'] for field in required_fields: if field not in request_data: raise ValidationError(f"缺少必要字段: {field}") # 处理city_ids的各种情况 city_ids = request_data['city_ids'] if isinstance(city_ids, int): request_data['city_ids'] = [city_ids] elif isinstance(city_ids, str): request_data['city_ids'] = [int(cid.strip()) for cid in city_ids.split(',')] elif not isinstance(city_ids, (list, tuple)): raise ValidationError("city_ids必须是整数、字符串或数组") # 确保所有ID都是整数 request_data['city_ids'] = [int(cid) for cid in request_data['city_ids']] # 其他验证 request_data['days'] = int(request_data['days']) if not 1 <= request_data['days'] <= 7: raise ValidationError("行程天数需在1-7天范围内") return request_data except Exception as e: raise ValidationError(f"参数验证失败: {str(e)}") def _generate_red_tourism_plan(self, request_data): """增强版生成逻辑,确保返回有效景点""" try: logger.info(f"重新生成请求数据: {json.dumps(request_data, ensure_ascii=False)[:500]}...") # 确保previous_plan有基本结构 if not isinstance(request_data.get('previous_plan'), dict): request_data['previous_plan'] = {'days': []} # 调用AI服务 generated_plan = MoonshotAIService.regenerate_travel_plan(request_data) if not generated_plan: raise Exception("AI服务返回空结果") if 'error' in generated_plan: raise Exception(f"AI服务错误: {generated_plan['error']}") # 验证必要字段 if 'days' not in generated_plan: generated_plan['days'] = [{'day': i + 1} for i in range(request_data['days'])] # 确保每个景点都有必要字段 for day in generated_plan['days']: for attr in day.get('attractions', []): attr.setdefault('is_red_tourism', False) attr.setdefault('educational_value', '中') return generated_plan except Exception as e: logger.error(f"生成失败: {str(e)}", exc_info=True) raise def _standardize_plan(self, plan_data, requested_days): """确保返回数据结构的完整性和一致性""" result = { 'title': plan_data.get('title', f'{requested_days}天红色之旅'), 'description': plan_data.get('description', '通过参观革命历史遗址学习党史'), 'days': [], 'red_tourism_tips': plan_data.get('red_tourism_tips', [ "请着装整洁,保持肃穆", "建议提前学习相关历史知识" ]), 'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等') } # 处理每日行程 for day in plan_data.get('days', []): standardized_day = { 'day': day.get('day', 1), 'theme': day.get('theme', '红色教育'), 'transport': day.get('transport', '公共交通'), 'attractions': [], 'educational_points': day.get('educational_points', [ "学习革命历史", "传承红色精神" ]) } # 处理景点数据 for attr in day.get('attractions', []): standardized_attr = { 'id': attr.get('id', 0), 'name': attr.get('name', '红色教育基地'), 'image': attr.get('image', '/images/default-red.jpg'), 'is_red_tourism': attr.get('is_red_tourism', True), 'educational_value': attr.get('educational_value', '高'), 'visit_time': attr.get('visit_time', '09:00-17:00'), 'ticket_price': attr.get('ticket_price', '免费') } standardized_day['attractions'].append(standardized_attr) result['days'].append(standardized_day) return result class TravelPlanDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, plan_id): try: plan = TravelPlan.objects.get(id=plan_id, user=request.user) except TravelPlan.DoesNotExist: return Response( {'detail': '未找到该旅行计划'}, status=status.HTTP_404_NOT_FOUND ) serializer = TravelPlanSerializer(plan) return Response(serializer.data) def delete(self, request, plan_id): try: plan = TravelPlan.objects.get(id=plan_id, user=request.user) except TravelPlan.DoesNotExist: return Response( {'detail': '未找到该旅行计划'}, status=status.HTTP_404_NOT_FOUND ) plan.delete() return Response(status=status.HTTP_204_NO_CONTENT) import requests from django.conf import settings class RegeneratePlanView(APIView): permission_classes = [IsAuthenticated] def post(self, request, plan_id): try: old_plan = TravelPlan.objects.get(id=plan_id, user=request.user) except TravelPlan.DoesNotExist: return Response( {'detail': '未找到该旅行计划'}, status=status.HTTP_404_NOT_FOUND ) # 使用相同的偏好重新生成计划 new_plan = TravelPlanService.create_travel_plan_from_preference(old_plan.preference) if not new_plan: return Response( {'detail': '重新生成旅行计划失败,请稍后再试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 删除旧计划 old_plan.delete() serializer = TravelPlanSerializer(new_plan) return Response(serializer.data, status=status.HTTP_201_CREATED) def test_api(request): if request.method == "GET": return JsonResponse({"status": "success", "message": "Django 后端已连接完成!"}) return JsonResponse({"status": "error", "message": "仅支持 GET 请求"}, status=400)