views.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. from django.utils import timezone
  2. from django.http import JsonResponse
  3. from rest_framework.views import APIView
  4. from rest_framework.response import Response
  5. from rest_framework import status
  6. from rest_framework.permissions import IsAuthenticated
  7. from .models import UserPreference, TravelPlan
  8. from .models import City, Attraction
  9. import logging
  10. logger = logging.getLogger(__name__)
  11. from .serializers import (
  12. CitySerializer,
  13. AttractionSerializer,
  14. UserPreferenceSerializer,
  15. TravelPlanSerializer,
  16. UserPreferenceCreateSerializer,
  17. TravelPlanCreateSerializer
  18. )
  19. from .services import TravelPlanService, CacheService, MoonshotAIService
  20. from django.contrib.auth import get_user_model
  21. User = get_user_model()
  22. # views.py
  23. from rest_framework.response import Response
  24. from .models import City
  25. class CityListView(APIView):
  26. permission_classes = [] # 无需登录
  27. def get(self, request):
  28. try:
  29. # Return all cities instead of just hot ones
  30. cities = City.objects.all() # Changed from filter(is_hot=True)
  31. serializer = CitySerializer(cities, many=True)
  32. return Response({
  33. "status": "success",
  34. "data": serializer.data
  35. }, status=status.HTTP_200_OK)
  36. except Exception as e:
  37. return Response({
  38. "status": "error",
  39. "message": str(e)
  40. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  41. class AttractionListView(APIView):
  42. def get(self, request, city_id):
  43. attractions = CacheService.get_attractions_by_city(city_id)
  44. return Response(attractions)
  45. class UserPreferenceView(APIView):
  46. permission_classes = [IsAuthenticated]
  47. def get(self, request):
  48. preference = UserPreference.objects.filter(user=request.user).first()
  49. if not preference:
  50. return Response({'detail': '未找到偏好设置'}, status=status.HTTP_404_NOT_FOUND)
  51. serializer = UserPreferenceSerializer(preference)
  52. return Response(serializer.data)
  53. def post(self, request):
  54. serializer = UserPreferenceCreateSerializer(data=request.data, context={'request': request})
  55. if serializer.is_valid():
  56. # 确保每个用户只有一个偏好设置
  57. UserPreference.objects.filter(user=request.user).delete()
  58. preference = serializer.save(user=request.user)
  59. return Response(UserPreferenceSerializer(preference).data, status=status.HTTP_201_CREATED)
  60. return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  61. # class TravelPlanView(APIView):
  62. # permission_classes = [] # 确保无认证要求
  63. #
  64. # def post(self, request):
  65. # try:
  66. # # 验证必要字段
  67. # required_fields = ['city_ids', 'days', 'interests', 'transport']
  68. # if not all(field in request.data for field in required_fields):
  69. # return Response(
  70. # {'status': 'error', 'message': '缺少必要参数'},
  71. # status=status.HTTP_400_BAD_REQUEST
  72. # )
  73. #
  74. # # 准备数据
  75. # plan_data = {
  76. # 'city_ids': request.data['city_ids'],
  77. # 'days': request.data['days'],
  78. # 'interests': request.data['interests'],
  79. # 'transport': request.data['transport'],
  80. # 'custom_requirements': request.data.get('custom_requirements', '')
  81. # }
  82. #
  83. # # 调用服务生成计划
  84. # travel_plan = TravelPlanService.create_travel_plan(plan_data)
  85. #
  86. # if travel_plan:
  87. # serializer = TravelPlanSerializer(travel_plan)
  88. # return Response({
  89. # 'status': 'success',
  90. # 'data': serializer.data
  91. # }, status=status.HTTP_201_CREATED)
  92. # else:
  93. # return Response({
  94. # 'status': 'error',
  95. # 'message': '生成旅行计划失败'
  96. # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  97. #
  98. # except Exception as e:
  99. # import traceback
  100. # traceback.print_exc() # 打印完整错误堆栈
  101. # return Response({
  102. # 'status': 'error',
  103. # 'message': str(e)
  104. # }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  105. class TravelPlanView(APIView):
  106. permission_classes = [] # 无需认证
  107. def post(self, request):
  108. """
  109. 生成旅行计划(不保存到数据库)
  110. 请求参数:
  111. - city_ids: 城市ID列表
  112. - days: 旅行天数
  113. - interests: 兴趣标签列表
  114. - transport: 交通方式
  115. - custom_requirements: 自定义需求(可选)
  116. """
  117. try:
  118. # 1. 验证必要参数
  119. required_fields = ['city_ids', 'days', 'interests', 'transport']
  120. if not all(field in request.data for field in required_fields):
  121. return Response(
  122. {'status': 'error', 'message': '缺少必要参数'},
  123. status=status.HTTP_400_BAD_REQUEST
  124. )
  125. # 2. 准备AI生成所需数据
  126. plan_data = {
  127. 'city_ids': request.data['city_ids'],
  128. 'days': request.data['days'],
  129. 'interests': request.data['interests'],
  130. 'transport': request.data['transport'],
  131. 'custom_requirements': request.data.get('custom_requirements', '')
  132. }
  133. # 3. 直接调用Moonshot AI生成计划(不创建数据库记录)
  134. generated_plan = MoonshotAIService.generate_travel_plan(plan_data)
  135. if not generated_plan or 'error' in generated_plan:
  136. error_msg = generated_plan.get('details', '生成旅行计划失败') if generated_plan else 'AI服务无响应'
  137. return Response({
  138. 'status': 'error',
  139. 'message': error_msg
  140. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  141. # 4. 返回生成的计划数据(不包含数据库ID等字段)
  142. return Response({
  143. 'status': 'success',
  144. 'data': {
  145. 'title': generated_plan.get('title', '自定义旅行计划'),
  146. 'description': generated_plan.get('description', ''),
  147. 'days': generated_plan.get('days', []),
  148. 'suitable_for': generated_plan.get('suitable_for', '')
  149. }
  150. }, status=status.HTTP_200_OK)
  151. except Exception as e:
  152. import traceback
  153. traceback.print_exc() # 打印错误堆栈
  154. return Response({
  155. 'status': 'error',
  156. 'message': f'服务器内部错误: {str(e)}'
  157. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  158. # views.py
  159. class RedTourismPlanView(APIView):
  160. """
  161. 红色旅游路线规划API - 稳定版
  162. 返回标准化的行程数据结构,确保前端能正确渲染
  163. """
  164. permission_classes = []
  165. def post(self, request):
  166. try:
  167. # 1. 参数验证
  168. city_ids = request.data.get('city_ids')
  169. days = request.data.get('days')
  170. if not city_ids or not isinstance(city_ids, list):
  171. return Response(
  172. {'status': 'error', 'message': '请提供有效的城市ID列表'},
  173. status=status.HTTP_400_BAD_REQUEST
  174. )
  175. if not days or not isinstance(days, int) or days < 1 or days > 7:
  176. return Response(
  177. {'status': 'error', 'message': '请提供1-7天的有效天数'},
  178. status=status.HTTP_400_BAD_REQUEST
  179. )
  180. # 2. 准备请求数据
  181. interests = request.data.get('interests', [])
  182. if not isinstance(interests, list):
  183. interests = []
  184. if 'red-tourism' not in interests:
  185. interests.append('red-tourism')
  186. plan_data = {
  187. 'city_ids': city_ids,
  188. 'days': days,
  189. 'interests': interests,
  190. 'transport': request.data.get('transport', 'public'),
  191. 'custom_requirements': request.data.get('custom_requirements', ''),
  192. 'is_red_tourism': True
  193. }
  194. # 3. 调用AI服务
  195. generated_plan = MoonshotAIService.generate_travel_plan(plan_data)
  196. logger.info(f"Generated plan: {generated_plan}")
  197. if not generated_plan:
  198. return Response(
  199. {'status': 'error', 'message': 'AI服务无响应'},
  200. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  201. )
  202. if 'error' in generated_plan:
  203. return Response(
  204. {'status': 'error', 'message': generated_plan.get('message', 'AI生成失败')},
  205. status=status.HTTP_400_BAD_REQUEST
  206. )
  207. # 4. 标准化返回数据
  208. standardized_plan = self._standardize_plan(generated_plan, days)
  209. return Response({
  210. 'status': 'success',
  211. 'data': standardized_plan
  212. })
  213. except Exception as e:
  214. logger.error(f"API error: {str(e)}", exc_info=True)
  215. return Response({
  216. 'status': 'error',
  217. 'message': '服务器内部错误'
  218. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  219. def _standardize_plan(self, plan_data, requested_days):
  220. """确保返回数据结构的完整性和一致性"""
  221. result = {
  222. 'title': plan_data.get('title', f'{requested_days}天红色之旅'),
  223. 'description': plan_data.get('description', '通过参观革命历史遗址学习党史'),
  224. 'days': [],
  225. 'red_tourism_tips': plan_data.get('red_tourism_tips', [
  226. "请着装整洁,保持肃穆",
  227. "建议提前学习相关历史知识"
  228. ]),
  229. 'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等')
  230. }
  231. # 处理每日行程
  232. for day in plan_data.get('days', []):
  233. standardized_day = {
  234. 'day': day.get('day', 1),
  235. 'theme': day.get('theme', '红色教育'),
  236. 'transport': day.get('transport', '公共交通'),
  237. 'attractions': [],
  238. 'educational_points': day.get('educational_points', [
  239. "学习革命历史",
  240. "传承红色精神"
  241. ])
  242. }
  243. # 处理景点数据
  244. for attr in day.get('attractions', []):
  245. standardized_attr = {
  246. 'id': attr.get('id', 0),
  247. 'name': attr.get('name', '红色教育基地'),
  248. 'image': attr.get('image', '/images/default-red.jpg'),
  249. 'is_red_tourism': attr.get('is_red_tourism', True),
  250. 'educational_value': attr.get('educational_value', '高'),
  251. 'visit_time': attr.get('visit_time', '09:00-17:00'),
  252. 'ticket_price': attr.get('ticket_price', '免费')
  253. }
  254. standardized_day['attractions'].append(standardized_attr)
  255. result['days'].append(standardized_day)
  256. return result
  257. class RedAttractionDetailView(APIView):
  258. """
  259. 红色景点详情API
  260. 权限:无需认证
  261. """
  262. permission_classes = []
  263. def get(self, request, attraction_id):
  264. try:
  265. attraction = Attraction.objects.get(id=attraction_id)
  266. if '红色旅游' not in attraction.tags:
  267. return Response({
  268. 'status': 'error',
  269. 'message': '该景点不是红色旅游景点'
  270. }, status=status.HTTP_400_BAD_REQUEST)
  271. # 获取相关红色景点
  272. related_attractions = Attraction.objects.filter(
  273. city=attraction.city,
  274. tags__contains="红色旅游"
  275. ).exclude(id=attraction_id)[:3]
  276. serializer = AttractionSerializer(attraction)
  277. related_serializer = AttractionSerializer(related_attractions, many=True)
  278. # 构建响应数据
  279. response_data = {
  280. 'detail': serializer.data,
  281. 'historical_background': attraction.history or "暂无详细历史背景",
  282. 'educational_significance': self._get_educational_content(attraction),
  283. 'related_attractions': related_serializer.data,
  284. 'visiting_etiquette': self._get_visiting_etiquette(attraction)
  285. }
  286. return Response({
  287. 'status': 'success',
  288. 'data': response_data
  289. })
  290. except Attraction.DoesNotExist:
  291. return Response(
  292. {'status': 'error', 'message': '景点不存在'},
  293. status=status.HTTP_404_NOT_FOUND
  294. )
  295. def _get_educational_content(self, attraction):
  296. """生成教育意义内容"""
  297. content = []
  298. if 'memorial' in attraction.tags:
  299. content.append("此处是重要的革命纪念地,具有深刻的教育意义")
  300. content.append("适合开展爱国主义主题教育活动")
  301. if 'battle' in attraction.tags:
  302. content.append("这里发生过重要历史战役,展现了革命先烈的英勇精神")
  303. return content
  304. def _get_visiting_etiquette(self, attraction):
  305. """生成参观礼仪指南"""
  306. etiquette = [
  307. "请保持庄严肃穆",
  308. "勿大声喧哗"
  309. ]
  310. if 'memorial' in attraction.tags:
  311. etiquette.append("纪念馆内请勿拍照")
  312. return etiquette
  313. from rest_framework.views import APIView
  314. from rest_framework.response import Response
  315. from rest_framework import status
  316. import json
  317. import logging
  318. from django.core.exceptions import ValidationError
  319. logger = logging.getLogger(__name__)
  320. class RedTourismRegenerateView(APIView):
  321. """
  322. 红色旅游路线重新生成API - 增强版
  323. 功能:
  324. 1. 基于用户输入和原始行程生成优化路线
  325. 2. 严格验证输入参数
  326. 3. 提供标准化的响应格式
  327. """
  328. permission_classes = []
  329. def post(self, request):
  330. try:
  331. # === 1. 数据预处理 ===
  332. request_data = self._parse_and_validate_request(request)
  333. # === 2. 业务逻辑处理 ===
  334. generated_plan = self._generate_red_tourism_plan(request_data)
  335. # === 3. 响应处理 ===
  336. return Response({
  337. 'status': 'success',
  338. 'data': self._standardize_plan(generated_plan, request_data['days']),
  339. 'meta': {
  340. 'generated_at': timezone.now().isoformat(),
  341. 'version': '1.1'
  342. }
  343. })
  344. except ValidationError as e:
  345. logger.warning(f"参数验证失败: {str(e)}")
  346. return Response({
  347. 'status': 'error',
  348. 'message': str(e),
  349. 'code': 'INVALID_PARAMS'
  350. }, status=status.HTTP_400_BAD_REQUEST)
  351. except Exception as e:
  352. logger.error(f"服务器错误: {str(e)}", exc_info=True)
  353. return Response({
  354. 'status': 'error',
  355. 'message': '服务器处理请求时发生错误',
  356. 'code': 'SERVER_ERROR'
  357. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  358. def _parse_and_validate_request(self, request):
  359. """解析并验证请求数据"""
  360. try:
  361. # 数据格式检查
  362. if request.content_type != 'application/json':
  363. raise ValidationError("只支持JSON格式数据")
  364. request_data = request.data.copy() if hasattr(request, 'data') else json.loads(request.body)
  365. # 必填字段检查
  366. required_fields = ['city_ids', 'days', 'previous_plan']
  367. for field in required_fields:
  368. if field not in request_data:
  369. raise ValidationError(f"缺少必要字段: {field}")
  370. # 处理city_ids的各种情况
  371. city_ids = request_data['city_ids']
  372. if isinstance(city_ids, int):
  373. request_data['city_ids'] = [city_ids]
  374. elif isinstance(city_ids, str):
  375. request_data['city_ids'] = [int(cid.strip()) for cid in city_ids.split(',')]
  376. elif not isinstance(city_ids, (list, tuple)):
  377. raise ValidationError("city_ids必须是整数、字符串或数组")
  378. # 确保所有ID都是整数
  379. request_data['city_ids'] = [int(cid) for cid in request_data['city_ids']]
  380. # 其他验证
  381. request_data['days'] = int(request_data['days'])
  382. if not 1 <= request_data['days'] <= 7:
  383. raise ValidationError("行程天数需在1-7天范围内")
  384. return request_data
  385. except Exception as e:
  386. raise ValidationError(f"参数验证失败: {str(e)}")
  387. def _generate_red_tourism_plan(self, request_data):
  388. """增强版生成逻辑,确保返回有效景点"""
  389. try:
  390. logger.info(f"重新生成请求数据: {json.dumps(request_data, ensure_ascii=False)[:500]}...")
  391. # 确保previous_plan有基本结构
  392. if not isinstance(request_data.get('previous_plan'), dict):
  393. request_data['previous_plan'] = {'days': []}
  394. # 调用AI服务
  395. generated_plan = MoonshotAIService.regenerate_travel_plan(request_data)
  396. if not generated_plan:
  397. raise Exception("AI服务返回空结果")
  398. if 'error' in generated_plan:
  399. raise Exception(f"AI服务错误: {generated_plan['error']}")
  400. # 验证必要字段
  401. if 'days' not in generated_plan:
  402. generated_plan['days'] = [{'day': i + 1} for i in range(request_data['days'])]
  403. # 确保每个景点都有必要字段
  404. for day in generated_plan['days']:
  405. for attr in day.get('attractions', []):
  406. attr.setdefault('is_red_tourism', False)
  407. attr.setdefault('educational_value', '中')
  408. return generated_plan
  409. except Exception as e:
  410. logger.error(f"生成失败: {str(e)}", exc_info=True)
  411. raise
  412. def _standardize_plan(self, plan_data, requested_days):
  413. """确保返回数据结构的完整性和一致性"""
  414. result = {
  415. 'title': plan_data.get('title', f'{requested_days}天红色之旅'),
  416. 'description': plan_data.get('description', '通过参观革命历史遗址学习党史'),
  417. 'days': [],
  418. 'red_tourism_tips': plan_data.get('red_tourism_tips', [
  419. "请着装整洁,保持肃穆",
  420. "建议提前学习相关历史知识"
  421. ]),
  422. 'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等')
  423. }
  424. # 处理每日行程
  425. for day in plan_data.get('days', []):
  426. standardized_day = {
  427. 'day': day.get('day', 1),
  428. 'theme': day.get('theme', '红色教育'),
  429. 'transport': day.get('transport', '公共交通'),
  430. 'attractions': [],
  431. 'educational_points': day.get('educational_points', [
  432. "学习革命历史",
  433. "传承红色精神"
  434. ])
  435. }
  436. # 处理景点数据
  437. for attr in day.get('attractions', []):
  438. standardized_attr = {
  439. 'id': attr.get('id', 0),
  440. 'name': attr.get('name', '红色教育基地'),
  441. 'image': attr.get('image', '/images/default-red.jpg'),
  442. 'is_red_tourism': attr.get('is_red_tourism', True),
  443. 'educational_value': attr.get('educational_value', '高'),
  444. 'visit_time': attr.get('visit_time', '09:00-17:00'),
  445. 'ticket_price': attr.get('ticket_price', '免费')
  446. }
  447. standardized_day['attractions'].append(standardized_attr)
  448. result['days'].append(standardized_day)
  449. return result
  450. class TravelPlanDetailView(APIView):
  451. permission_classes = [IsAuthenticated]
  452. def get(self, request, plan_id):
  453. try:
  454. plan = TravelPlan.objects.get(id=plan_id, user=request.user)
  455. except TravelPlan.DoesNotExist:
  456. return Response(
  457. {'detail': '未找到该旅行计划'},
  458. status=status.HTTP_404_NOT_FOUND
  459. )
  460. serializer = TravelPlanSerializer(plan)
  461. return Response(serializer.data)
  462. def delete(self, request, plan_id):
  463. try:
  464. plan = TravelPlan.objects.get(id=plan_id, user=request.user)
  465. except TravelPlan.DoesNotExist:
  466. return Response(
  467. {'detail': '未找到该旅行计划'},
  468. status=status.HTTP_404_NOT_FOUND
  469. )
  470. plan.delete()
  471. return Response(status=status.HTTP_204_NO_CONTENT)
  472. import requests
  473. from django.conf import settings
  474. class RegeneratePlanView(APIView):
  475. permission_classes = [IsAuthenticated]
  476. def post(self, request, plan_id):
  477. try:
  478. old_plan = TravelPlan.objects.get(id=plan_id, user=request.user)
  479. except TravelPlan.DoesNotExist:
  480. return Response(
  481. {'detail': '未找到该旅行计划'},
  482. status=status.HTTP_404_NOT_FOUND
  483. )
  484. # 使用相同的偏好重新生成计划
  485. new_plan = TravelPlanService.create_travel_plan_from_preference(old_plan.preference)
  486. if not new_plan:
  487. return Response(
  488. {'detail': '重新生成旅行计划失败,请稍后再试'},
  489. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  490. )
  491. # 删除旧计划
  492. old_plan.delete()
  493. serializer = TravelPlanSerializer(new_plan)
  494. return Response(serializer.data, status=status.HTTP_201_CREATED)
  495. def test_api(request):
  496. if request.method == "GET":
  497. return JsonResponse({"status": "success", "message": "Django 后端已连接完成!"})
  498. return JsonResponse({"status": "error", "message": "仅支持 GET 请求"}, status=400)