123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597 |
- 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)
|