|
|
@@ -0,0 +1,1012 @@
|
|
|
+import json
|
|
|
+import requests
|
|
|
+from django.conf import settings
|
|
|
+from django.db.models import Q
|
|
|
+from openai import OpenAI
|
|
|
+
|
|
|
+from .models import City, Attraction, TravelPlan, DayPlan, DayPlanAttraction
|
|
|
+from django.core.cache import cache
|
|
|
+import logging
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+import json
|
|
|
+from django.conf import settings
|
|
|
+from openai import OpenAI
|
|
|
+from .models import City, Attraction
|
|
|
+from django.core.cache import cache
|
|
|
+from datetime import datetime, timedelta
|
|
|
+import random
|
|
|
+
|
|
|
+
|
|
|
+# class MoonshotAIService:
|
|
|
+# @staticmethod
|
|
|
+# def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
|
|
|
+# """
|
|
|
+# 增强版红色旅游路线规划AI服务
|
|
|
+# """
|
|
|
+# # 处理输入参数
|
|
|
+# if isinstance(preference, dict):
|
|
|
+# city_ids = preference.get('city_ids', [])
|
|
|
+# cities = City.objects.filter(id__in=city_ids)
|
|
|
+# days = preference.get('days', 3)
|
|
|
+# interests = preference.get('interests', [])
|
|
|
+# transport = preference.get('transport', 'public')
|
|
|
+# custom_requirements = preference.get('custom_requirements', '')
|
|
|
+# else:
|
|
|
+# cities = preference.cities.all()
|
|
|
+# days = preference.days
|
|
|
+# interests = preference.interests
|
|
|
+# transport = preference.get_transport_display()
|
|
|
+# custom_requirements = preference.custom_requirements or '无'
|
|
|
+#
|
|
|
+# # 获取相关景点(优先红色旅游景点)
|
|
|
+# attractions = Attraction.objects.filter(
|
|
|
+# city__in=cities,
|
|
|
+# tags__contains="红色旅游"
|
|
|
+# ).select_related('city')
|
|
|
+#
|
|
|
+# # 如果没有足够红色景点,补充其他景点
|
|
|
+# if len(attractions) < days * 3:
|
|
|
+# additional_attractions = Attraction.objects.filter(
|
|
|
+# city__in=cities
|
|
|
+# ).exclude(tags__contains="红色旅游")[:10]
|
|
|
+# attractions = list(attractions) + list(additional_attractions)
|
|
|
+#
|
|
|
+# # 构建提示词
|
|
|
+# prompt = MoonshotAIService._build_enhanced_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_requirements, attractions,
|
|
|
+# is_regeneration, previous_plan
|
|
|
+# )
|
|
|
+#
|
|
|
+# # 调用Moonshot AI API
|
|
|
+# client = OpenAI(
|
|
|
+# api_key=settings.MOONSHOT_API_KEY,
|
|
|
+# base_url="https://api.moonshot.cn/v1"
|
|
|
+# )
|
|
|
+#
|
|
|
+# try:
|
|
|
+# response = client.chat.completions.create(
|
|
|
+# model="moonshot-v1-8k",
|
|
|
+# messages=[
|
|
|
+# {
|
|
|
+# "role": "system",
|
|
|
+# "content": "你是一个红色旅游规划专家,熟悉山东省所有红色景点。请按照指定格式输出,包含详细路线、时间安排和景点介绍。"
|
|
|
+# },
|
|
|
+# {"role": "user", "content": prompt}
|
|
|
+# ],
|
|
|
+# response_format={"type": "json_object"},
|
|
|
+# temperature=0.7 if is_regeneration else 0.5
|
|
|
+# )
|
|
|
+#
|
|
|
+# # 解析并处理返回数据
|
|
|
+# plan_data = json.loads(response.choices[0].message.content.strip())
|
|
|
+# return MoonshotAIService._process_ai_response(plan_data, attractions)
|
|
|
+#
|
|
|
+# except Exception as e:
|
|
|
+# print(f"AI服务异常: {str(e)}")
|
|
|
+# return {
|
|
|
+# "error": "行程生成失败",
|
|
|
+# "details": str(e)
|
|
|
+# }
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_enhanced_prompt(cities, days, interests, transport, requirements, attractions, is_regeneration,
|
|
|
+# previous_plan):
|
|
|
+# """构建增强版红色旅游提示词"""
|
|
|
+# city_names = [city.name for city in cities]
|
|
|
+# attraction_list = [
|
|
|
+# f"{att.id}:{att.name}[标签:{','.join(att.tags)}][城市:{att.city.name}][描述:{att.short_desc}]"
|
|
|
+# for att in attractions
|
|
|
+# ]
|
|
|
+#
|
|
|
+# base_prompt = f"""
|
|
|
+# 你是一个专业的红色旅游规划AI助手,请根据以下信息为游客规划山东省红色旅游行程:
|
|
|
+#
|
|
|
+# 基本要求:
|
|
|
+# - 城市:{city_names}
|
|
|
+# - 天数:{days}天
|
|
|
+# - 兴趣偏好:{interests or '无特别偏好'}
|
|
|
+# - 交通方式:{transport}
|
|
|
+# - 特殊要求:{requirements or '无'}
|
|
|
+#
|
|
|
+# 可选景点信息(ID:名称[标签][城市][简短描述]):
|
|
|
+# {attraction_list}
|
|
|
+#
|
|
|
+# 请设计一个富有教育意义的红色旅游路线,要求:
|
|
|
+# 1. 每天安排3-4个景点,包含至少2个红色景点
|
|
|
+# 2. 合理安排景点间的交通时间和午餐时间
|
|
|
+# 3. 每个景点提供详细的参观建议和背景介绍
|
|
|
+# 4. 路线设计要连贯,避免来回奔波
|
|
|
+# 5. 包含早中晚餐的推荐地点(尽量选择红色教育基地附近的餐馆)
|
|
|
+# """
|
|
|
+#
|
|
|
+# if is_regeneration:
|
|
|
+# base_prompt += f"""
|
|
|
+# 这是之前的行程计划,请重新设计不同的路线:
|
|
|
+# {json.dumps(previous_plan, ensure_ascii=False, indent=2)}
|
|
|
+# """
|
|
|
+#
|
|
|
+# base_prompt += f"""
|
|
|
+# 请返回JSON格式的行程计划,结构如下:
|
|
|
+# {{
|
|
|
+# "title": "行程标题(突出红色主题)",
|
|
|
+# "description": "行程整体描述(200字左右)",
|
|
|
+# "suitable_for": "适合人群(如'亲子家庭'、'学生团体'等)",
|
|
|
+# "days": [
|
|
|
+# {{
|
|
|
+# "day": 1,
|
|
|
+# "theme": "当日主题(如'革命精神传承之旅')",
|
|
|
+# "description": "当日详细描述",
|
|
|
+# "transport": "交通安排建议",
|
|
|
+# "morning": {{
|
|
|
+# "start_time": "08:00",
|
|
|
+# "attraction": 景点ID,
|
|
|
+# "visit_time": "建议参观时间(如'1.5小时')",
|
|
|
+# "description": "景点详细介绍(300字左右)",
|
|
|
+# "recommendation": "参观建议(如'建议先参观纪念馆主展厅')"
|
|
|
+# }},
|
|
|
+# "lunch": {{
|
|
|
+# "time": "12:00",
|
|
|
+# "recommendation": "午餐推荐地点和特色菜",
|
|
|
+# "description": "餐馆简介(100字左右)"
|
|
|
+# }},
|
|
|
+# "afternoon": [
|
|
|
+# {{
|
|
|
+# "start_time": "13:30",
|
|
|
+# "attraction": 景点ID,
|
|
|
+# "visit_time": "建议参观时间",
|
|
|
+# "description": "景点详细介绍",
|
|
|
+# "recommendation": "参观建议"
|
|
|
+# }},
|
|
|
+# ...
|
|
|
+# ],
|
|
|
+# "dinner": {{
|
|
|
+# "time": "18:00",
|
|
|
+# "recommendation": "晚餐推荐",
|
|
|
+# "description": "餐馆简介"
|
|
|
+# }},
|
|
|
+# "evening_activity": {{
|
|
|
+# "description": "晚间活动建议(如'观看红色主题演出')",
|
|
|
+# "recommendation": "活动详情"
|
|
|
+# }}
|
|
|
+# }},
|
|
|
+# ...
|
|
|
+# ],
|
|
|
+# "travel_tips": [
|
|
|
+# "穿着建议:...",
|
|
|
+# "注意事项:...",
|
|
|
+# "红色教育重点:..."
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# """
|
|
|
+# return base_prompt
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _process_ai_response(plan_data, attractions):
|
|
|
+# """处理AI返回数据,补充完整景点信息"""
|
|
|
+# attraction_map = {att.id: att for att in attractions}
|
|
|
+#
|
|
|
+# for day in plan_data.get('days', []):
|
|
|
+# # 处理上午景点
|
|
|
+# if 'morning' in day and day['morning']['attraction'] in attraction_map:
|
|
|
+# att = attraction_map[day['morning']['attraction']]
|
|
|
+# day['morning'].update({
|
|
|
+# 'attraction_data': {
|
|
|
+# 'name': att.name,
|
|
|
+# 'image': att.image.url if att.image else '',
|
|
|
+# 'address': att.address,
|
|
|
+# 'open_hours': att.open_hours,
|
|
|
+# 'ticket_price': str(att.ticket_price)
|
|
|
+# }
|
|
|
+# })
|
|
|
+#
|
|
|
+# # 处理下午景点
|
|
|
+# if 'afternoon' in day:
|
|
|
+# for item in day['afternoon']:
|
|
|
+# if item['attraction'] in attraction_map:
|
|
|
+# att = attraction_map[item['attraction']]
|
|
|
+# item.update({
|
|
|
+# 'attraction_data': {
|
|
|
+# 'name': att.name,
|
|
|
+# 'image': att.image.url if att.image else '',
|
|
|
+# 'address': att.address,
|
|
|
+# 'open_hours': att.open_hours,
|
|
|
+# 'ticket_price': str(att.ticket_price)
|
|
|
+# }
|
|
|
+# })
|
|
|
+#
|
|
|
+# return plan_data
|
|
|
+
|
|
|
+# services.py
|
|
|
+class MoonshotAIService:
|
|
|
+ RED_TOURISM_TAGS = ['红色旅游', '革命', '烈士', '纪念馆', '党史']
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def generate_travel_plan(preference):
|
|
|
+ """
|
|
|
+ 红色旅游专用生成方法
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 1. 获取城市和景点
|
|
|
+ cities = City.objects.filter(id__in=preference['city_ids'])
|
|
|
+ if not cities.exists():
|
|
|
+ return {'error': '未找到指定城市'}
|
|
|
+
|
|
|
+ # 2. 优先获取红色景点
|
|
|
+ attractions = Attraction.objects.filter(
|
|
|
+ city__in=cities,
|
|
|
+ tags__overlap=MoonshotAIService.RED_TOURISM_TAGS
|
|
|
+ )
|
|
|
+
|
|
|
+ # 如果红色景点不足,补充其他景点
|
|
|
+ if len(attractions) < preference['days'] * 2:
|
|
|
+ extra_attractions = Attraction.objects.filter(
|
|
|
+ city__in=cities
|
|
|
+ ).exclude(tags__overlap=MoonshotAIService.RED_TOURISM_TAGS)[:10]
|
|
|
+ attractions = list(attractions) + list(extra_attractions)
|
|
|
+
|
|
|
+ # 3. 构建AI提示
|
|
|
+ prompt = MoonshotAIService._build_red_tourism_prompt(
|
|
|
+ cities,
|
|
|
+ preference['days'],
|
|
|
+ preference['interests'],
|
|
|
+ preference['transport'],
|
|
|
+ preference.get('custom_requirements', ''),
|
|
|
+ attractions
|
|
|
+ )
|
|
|
+
|
|
|
+ # 4. 调用AI接口
|
|
|
+ client = OpenAI(
|
|
|
+ api_key=settings.MOONSHOT_API_KEY,
|
|
|
+ base_url="https://api.moonshot.cn/v1"
|
|
|
+ )
|
|
|
+
|
|
|
+ response = client.chat.completions.create(
|
|
|
+ model="moonshot-v1-8k",
|
|
|
+ messages=[
|
|
|
+ {
|
|
|
+ "role": "system",
|
|
|
+ "content": "你是红色旅游专家,专门规划革命教育路线。必须包含党史学习内容。"
|
|
|
+ },
|
|
|
+ {"role": "user", "content": prompt}
|
|
|
+ ],
|
|
|
+ response_format={"type": "json_object"},
|
|
|
+ temperature=0.5
|
|
|
+ )
|
|
|
+
|
|
|
+ # 5. 解析响应
|
|
|
+ plan_data = json.loads(response.choices[0].message.content)
|
|
|
+ return MoonshotAIService._process_red_tourism_response(plan_data, attractions)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ return {'error': str(e)}
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _build_red_tourism_prompt(cities, days, interests, transport, requirements, attractions):
|
|
|
+ """构建红色旅游专用提示词"""
|
|
|
+ city_names = [city.name for city in cities]
|
|
|
+ attraction_list = "\n".join([
|
|
|
+ f"{att.id}:{att.name}[标签:{','.join(att.tags)}][城市:{att.city.name}]"
|
|
|
+ for att in attractions
|
|
|
+ ])
|
|
|
+
|
|
|
+ return f"""
|
|
|
+ 请规划一个{days}天的红色旅游路线,要求:
|
|
|
+ - 城市:{city_names}
|
|
|
+ - 必须包含至少{max(2, days)}个红色教育基地
|
|
|
+ - 交通方式:{transport}
|
|
|
+ - 特殊要求:{requirements or '无'}
|
|
|
+
|
|
|
+ 可选景点:
|
|
|
+ {attraction_list}
|
|
|
+
|
|
|
+ 返回JSON格式,包含:
|
|
|
+ - title: 行程标题(必须含"红色"或"革命")
|
|
|
+ - description: 行程描述(突出教育意义)
|
|
|
+ - days: 每日安排(必须包含educational_points教育要点)
|
|
|
+ - 每个景点标注is_red_tourism是否为红色景点
|
|
|
+ """
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _process_red_tourism_response(plan_data, attractions):
|
|
|
+ """处理AI返回的红色旅游数据"""
|
|
|
+ attraction_map = {att.id: att for att in attractions}
|
|
|
+
|
|
|
+ for day in plan_data.get('days', []):
|
|
|
+ for attraction in day.get('attractions', []):
|
|
|
+ att_id = attraction.get('id')
|
|
|
+ if att_id in attraction_map:
|
|
|
+ att = attraction_map[att_id]
|
|
|
+ attraction.update({
|
|
|
+ 'image': att.image.url if att.image else '',
|
|
|
+ 'address': att.address,
|
|
|
+ 'open_hours': att.open_hours,
|
|
|
+ 'is_red_tourism': any(tag in att.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
|
|
|
+ })
|
|
|
+
|
|
|
+ return plan_data
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def regenerate_travel_plan(params):
|
|
|
+ """
|
|
|
+ 增强版重新生成方法,确保返回完整景点数据
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 1. 参数验证和转换
|
|
|
+ city_ids = params.get('city_ids', [])
|
|
|
+ if isinstance(city_ids, int): # 处理单个城市ID的情况
|
|
|
+ city_ids = [city_ids]
|
|
|
+
|
|
|
+ cities = City.objects.filter(id__in=city_ids)
|
|
|
+ if not cities.exists():
|
|
|
+ return {'error': '未找到指定城市'}
|
|
|
+
|
|
|
+ days = int(params.get('days', 3))
|
|
|
+
|
|
|
+ # 2. 获取所有相关景点(原行程景点 + 新红色景点)
|
|
|
+ previous_attractions = []
|
|
|
+ for day in params.get('previous_plan', {}).get('days', []):
|
|
|
+ for attr in day.get('attractions', []):
|
|
|
+ if attr.get('id'):
|
|
|
+ previous_attractions.append(attr['id'])
|
|
|
+
|
|
|
+ # 查询数据库获取完整景点对象
|
|
|
+ attractions = Attraction.objects.filter(
|
|
|
+ Q(id__in=previous_attractions) |
|
|
|
+ Q(city__in=cities, tags__overlap=MoonshotAIService.RED_TOURISM_TAGS)
|
|
|
+ ).distinct()
|
|
|
+
|
|
|
+ # 3. 构建更明确的提示词
|
|
|
+ prompt = f"""
|
|
|
+ 请基于原行程重新规划{days}天红色旅游路线,要求:
|
|
|
+
|
|
|
+ **必须包含以下元素**:
|
|
|
+ 1. 至少{max(2, days)}个红色教育基地
|
|
|
+ 2. 保留原行程中评分高的景点
|
|
|
+ 3. 每日主题明确(如"革命精神传承")
|
|
|
+
|
|
|
+ **城市范围**:{[city.name for city in cities]}
|
|
|
+
|
|
|
+ **可选景点**:
|
|
|
+ {[f"{a.id}:{a.name}[红色:{any(tag in a.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)}]" for a in attractions]}
|
|
|
+
|
|
|
+ **返回格式示例**:
|
|
|
+ {{
|
|
|
+ "title": "新行程标题",
|
|
|
+ "days": [
|
|
|
+ {{
|
|
|
+ "day": 1,
|
|
|
+ "attractions": [
|
|
|
+ {{
|
|
|
+ "id": 景点ID,
|
|
|
+ "name": "景点名称",
|
|
|
+ "is_red_tourism": true/false,
|
|
|
+ "educational_value": "高/中/低"
|
|
|
+ }}
|
|
|
+ ]
|
|
|
+ }}
|
|
|
+ ]
|
|
|
+ }}
|
|
|
+ """
|
|
|
+
|
|
|
+ # 4. 调用AI接口(示例代码,实际需要替换为您的AI调用)
|
|
|
+ client = OpenAI(api_key=settings.MOONSHOT_API_KEY, base_url="https://api.moonshot.cn/v1")
|
|
|
+ response = client.chat.completions.create(
|
|
|
+ model="moonshot-v1-8k",
|
|
|
+ messages=[
|
|
|
+ {"role": "system", "content": "你是红色旅游专家,严格按照要求生成行程"},
|
|
|
+ {"role": "user", "content": prompt}
|
|
|
+ ],
|
|
|
+ response_format={"type": "json_object"},
|
|
|
+ temperature=0.6
|
|
|
+ )
|
|
|
+
|
|
|
+ # 5. 处理响应数据
|
|
|
+ plan_data = json.loads(response.choices[0].message.content)
|
|
|
+
|
|
|
+ # 补充景点完整信息
|
|
|
+ attraction_map = {a.id: a for a in attractions}
|
|
|
+ for day in plan_data.get('days', []):
|
|
|
+ for attr in day.get('attractions', []):
|
|
|
+ if attr['id'] in attraction_map:
|
|
|
+ a = attraction_map[attr['id']]
|
|
|
+ attr.update({
|
|
|
+ 'image': a.image.url if a.image else '',
|
|
|
+ 'address': a.address,
|
|
|
+ 'open_hours': a.open_hours,
|
|
|
+ 'is_red_tourism': any(tag in a.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
|
|
|
+ })
|
|
|
+
|
|
|
+ return plan_data
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"重新生成失败: {str(e)}", exc_info=True)
|
|
|
+ return {'error': str(e)}
|
|
|
+# class MoonshotAIService:
|
|
|
+# @staticmethod
|
|
|
+# def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
|
|
|
+# """
|
|
|
+# 调用Moonshot AI API生成旅行计划
|
|
|
+# :param preference: 偏好设置(字典或Django模型对象)
|
|
|
+# :param is_regeneration: 是否为重新生成请求
|
|
|
+# :param previous_plan: 之前的行程计划(仅重新生成时使用)
|
|
|
+# :return: 生成的行程计划字典
|
|
|
+# """
|
|
|
+# # 处理输入参数(支持字典或Django模型对象)
|
|
|
+# if isinstance(preference, dict):
|
|
|
+# city_ids = preference.get('city_ids', [])
|
|
|
+# cities = City.objects.filter(id__in=city_ids)
|
|
|
+# days = preference.get('days', 3)
|
|
|
+# interests = preference.get('interests', [])
|
|
|
+# transport = preference.get('transport', 'public')
|
|
|
+# custom_requirements = preference.get('custom_requirements', '')
|
|
|
+# else:
|
|
|
+# cities = preference.cities.all()
|
|
|
+# days = preference.days
|
|
|
+# interests = preference.interests
|
|
|
+# transport = preference.get_transport_display()
|
|
|
+# custom_requirements = preference.custom_requirements or '无'
|
|
|
+#
|
|
|
+# # 获取相关景点
|
|
|
+# attractions = Attraction.objects.filter(city__in=cities)
|
|
|
+#
|
|
|
+# # 构建不同的提示词基于是否是重新生成
|
|
|
+# if is_regeneration:
|
|
|
+# prompt = MoonshotAIService._build_regeneration_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_requirements, attractions, previous_plan
|
|
|
+# )
|
|
|
+# else:
|
|
|
+# prompt = MoonshotAIService._build_initial_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_requirements, attractions
|
|
|
+# )
|
|
|
+#
|
|
|
+# # 调用Moonshot AI API
|
|
|
+# client = OpenAI(
|
|
|
+# api_key=settings.MOONSHOT_API_KEY,
|
|
|
+# base_url="https://api.moonshot.cn/v1"
|
|
|
+# )
|
|
|
+#
|
|
|
+# try:
|
|
|
+# response = client.chat.completions.create(
|
|
|
+# model="moonshot-v1-8k",
|
|
|
+# messages=[
|
|
|
+# {
|
|
|
+# "role": "system",
|
|
|
+# "content": "你是一个旅行规划专家,严格按照用户指定的JSON格式输出,不添加额外解释。"
|
|
|
+# },
|
|
|
+# {"role": "user", "content": prompt}
|
|
|
+# ],
|
|
|
+# response_format={"type": "json_object"},
|
|
|
+# temperature=0.7 if is_regeneration else 0.5 # 重新生成时增加一点随机性
|
|
|
+# )
|
|
|
+#
|
|
|
+# # 解析并返回JSON
|
|
|
+# return json.loads(response.choices[0].message.content.strip())
|
|
|
+#
|
|
|
+# except json.JSONDecodeError:
|
|
|
+# print("API返回的不是有效JSON,尝试修复...")
|
|
|
+# raw_content = response.choices[0].message.content
|
|
|
+# start_idx = raw_content.find("{")
|
|
|
+# end_idx = raw_content.rfind("}") + 1
|
|
|
+# return json.loads(raw_content[start_idx:end_idx])
|
|
|
+#
|
|
|
+# except Exception as e:
|
|
|
+# print(f"Moonshot AI API调用异常: {str(e)}")
|
|
|
+# return {
|
|
|
+# "error": "行程生成失败",
|
|
|
+# "details": str(e)
|
|
|
+# }
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_initial_prompt(cities, days, interests, transport, custom_requirements, attractions):
|
|
|
+# """构建初始行程提示词"""
|
|
|
+# return f"""
|
|
|
+# 你是一个专业的旅行规划AI助手,请根据以下信息规划行程:
|
|
|
+# - 城市:{[city.name for city in cities]}
|
|
|
+# - 天数:{days}天
|
|
|
+# - 兴趣:{', '.join(interests) if interests else '无特别偏好'}
|
|
|
+# - 交通方式:{transport}
|
|
|
+# - 特殊要求:{custom_requirements if custom_requirements else '无'}
|
|
|
+#
|
|
|
+# 可选景点(格式:名称[标签]):
|
|
|
+# {[f"{att.name}[{', '.join(att.tags)}]" for att in attractions]}
|
|
|
+#
|
|
|
+# 请返回JSON格式的行程计划,结构如下:
|
|
|
+# {{
|
|
|
+# "title": "行程标题",
|
|
|
+# "description": "行程描述",
|
|
|
+# "suitable_for": "适合人群描述",
|
|
|
+# "days": [
|
|
|
+# {{
|
|
|
+# "day": 1,
|
|
|
+# "theme": "当日主题",
|
|
|
+# "description": "当日描述",
|
|
|
+# "transport": "交通方式描述",
|
|
|
+# "attractions": [
|
|
|
+# {{
|
|
|
+# "id": 景点ID,
|
|
|
+# "name": "景点名称",
|
|
|
+# "visit_time": "建议参观时间",
|
|
|
+# "notes": "备注信息"
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# """
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_regeneration_prompt(cities, days, interests, transport, custom_requirements, attractions, previous_plan):
|
|
|
+# """构建重新生成行程提示词"""
|
|
|
+# previous_plan_str = json.dumps(previous_plan, ensure_ascii=False, indent=2) if previous_plan else "无"
|
|
|
+#
|
|
|
+# return f"""
|
|
|
+# 你是一个专业的旅行规划AI助手,请基于相同的偏好但不同的安排重新规划行程:
|
|
|
+#
|
|
|
+# 原行程计划:
|
|
|
+# {previous_plan_str}
|
|
|
+#
|
|
|
+# 偏好设置:
|
|
|
+# - 城市:{[city.name for city in cities]}
|
|
|
+# - 天数:{days}天
|
|
|
+# - 兴趣:{', '.join(interests) if interests else '无特别偏好'}
|
|
|
+# - 交通方式:{transport}
|
|
|
+# - 特殊要求:{custom_requirements if custom_requirements else '无'}
|
|
|
+#
|
|
|
+# 可选景点(格式:名称[标签]):
|
|
|
+# {[f"{att.name}[{', '.join(att.tags)}]" for att in attractions]}
|
|
|
+#
|
|
|
+# 请返回一个不同的JSON格式行程计划,要求:
|
|
|
+# 1. 使用不同的景点组合
|
|
|
+# 2. 调整每天的行程顺序
|
|
|
+# 3. 创建新的每日主题
|
|
|
+# 4. 保持相同的天数和城市
|
|
|
+#
|
|
|
+# 结构如下:
|
|
|
+# {{
|
|
|
+# "title": "新行程标题(与之前不同)",
|
|
|
+# "description": "新行程描述",
|
|
|
+# "suitable_for": "适合人群描述",
|
|
|
+# "days": [
|
|
|
+# {{
|
|
|
+# "day": 1,
|
|
|
+# "theme": "新当日主题",
|
|
|
+# "description": "当日描述",
|
|
|
+# "transport": "交通方式描述",
|
|
|
+# "attractions": [
|
|
|
+# {{
|
|
|
+# "id": 景点ID,
|
|
|
+# "name": "景点名称",
|
|
|
+# "visit_time": "建议参观时间",
|
|
|
+# "notes": "备注信息"
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# """
|
|
|
+#
|
|
|
+# import json
|
|
|
+# from django.db.models import Q
|
|
|
+# from django.conf import settings
|
|
|
+# from openai import OpenAI
|
|
|
+# from .models import City, Attraction
|
|
|
+#
|
|
|
+#
|
|
|
+# class MoonshotAIService:
|
|
|
+# """
|
|
|
+# 红色文化旅游规划服务类
|
|
|
+# 严格限定只选择烈士陵园、革命纪念馆、红色博物馆等红色文化景点
|
|
|
+# """
|
|
|
+#
|
|
|
+# # 定义合法的红色景点类型标签
|
|
|
+# RED_ATTRACTION_TAGS = ['martyrs', 'memorial', 'museum', 'revolution', '红色旅游']
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
|
|
|
+# """
|
|
|
+# 生成红色文化主题旅行计划
|
|
|
+# :param preference: 偏好设置(dict或Django模型)
|
|
|
+# :param is_regeneration: 是否重新生成
|
|
|
+# :param previous_plan: 原行程(重新生成时用)
|
|
|
+# :return: 行程计划(dict)或错误信息
|
|
|
+# """
|
|
|
+# # 1. 参数解析
|
|
|
+# params = MoonshotAIService._parse_preferences(preference)
|
|
|
+# if 'error' in params:
|
|
|
+# return params
|
|
|
+#
|
|
|
+# cities, days, interests, transport, custom_reqs = params
|
|
|
+#
|
|
|
+# # 2. 获取红色文化景点
|
|
|
+# attractions = MoonshotAIService._get_red_attractions(cities)
|
|
|
+# if isinstance(attractions, dict) and 'error' in attractions:
|
|
|
+# return attractions
|
|
|
+#
|
|
|
+# # 3. 构建AI提示词
|
|
|
+# prompt = MoonshotAIService._build_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_reqs, attractions, is_regeneration, previous_plan
|
|
|
+# )
|
|
|
+#
|
|
|
+# # 4. 调用AI接口
|
|
|
+# plan = MoonshotAIService._call_moonshot_api(prompt, is_regeneration)
|
|
|
+# if 'error' in plan:
|
|
|
+# return plan
|
|
|
+#
|
|
|
+# # 5. 验证结果
|
|
|
+# if not MoonshotAIService._validate_red_plan(plan):
|
|
|
+# return {
|
|
|
+# "error": "行程验证失败",
|
|
|
+# "details": "生成的行程不符合红色文化主题要求"
|
|
|
+# }
|
|
|
+#
|
|
|
+# return plan
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _parse_preferences(preference):
|
|
|
+# """解析偏好参数"""
|
|
|
+# try:
|
|
|
+# if isinstance(preference, dict):
|
|
|
+# city_ids = preference.get('city_ids', [])
|
|
|
+# cities = City.objects.filter(id__in=city_ids)
|
|
|
+# days = preference.get('days', 3)
|
|
|
+# interests = preference.get('interests', [])
|
|
|
+# transport = preference.get('transport', 'public')
|
|
|
+# custom_reqs = preference.get('custom_requirements', '')
|
|
|
+# else:
|
|
|
+# cities = preference.cities.all()
|
|
|
+# days = preference.days
|
|
|
+# interests = preference.interests
|
|
|
+# transport = preference.get_transport_display()
|
|
|
+# custom_reqs = preference.custom_requirements or '无'
|
|
|
+#
|
|
|
+# if not cities.exists():
|
|
|
+# return {"error": "未选择有效城市", "details": "请至少选择一个城市"}
|
|
|
+#
|
|
|
+# return cities, days, interests, transport, custom_reqs
|
|
|
+#
|
|
|
+# except Exception as e:
|
|
|
+# return {"error": "参数解析失败", "details": str(e)}
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _get_red_attractions(cities):
|
|
|
+# """获取红色文化景点"""
|
|
|
+# try:
|
|
|
+# # 精确查询
|
|
|
+# attractions = Attraction.objects.filter(
|
|
|
+# city__in=cities,
|
|
|
+# tags__overlap=MoonshotAIService.RED_ATTRACTION_TAGS
|
|
|
+# ).distinct()
|
|
|
+#
|
|
|
+# # 放宽条件查询
|
|
|
+# if not attractions.exists():
|
|
|
+# attractions = Attraction.objects.filter(
|
|
|
+# city__in=cities,
|
|
|
+# name__iregex=r'革命|烈士|党史|红色|纪念馆'
|
|
|
+# )
|
|
|
+#
|
|
|
+# if not attractions.exists():
|
|
|
+# red_cities = ["北京", "延安", "井冈山", "遵义", "韶山"]
|
|
|
+# return {
|
|
|
+# "error": "未找到红色景点",
|
|
|
+# "details": "当前城市未找到红色文化景点",
|
|
|
+# "suggestion": f"建议选择{','.join(red_cities)}等红色旅游城市"
|
|
|
+# }
|
|
|
+#
|
|
|
+# return attractions
|
|
|
+#
|
|
|
+# except Exception as e:
|
|
|
+# return {"error": "景点查询失败", "details": str(e)}
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_prompt(cities, days, interests, transport, custom_reqs, attractions, is_regeneration, previous_plan):
|
|
|
+# """构建AI提示词"""
|
|
|
+# if is_regeneration:
|
|
|
+# return MoonshotAIService._build_red_regeneration_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_reqs, attractions, previous_plan
|
|
|
+# )
|
|
|
+# return MoonshotAIService._build_red_initial_prompt(
|
|
|
+# cities, days, interests, transport,
|
|
|
+# custom_reqs, attractions
|
|
|
+# )
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_red_initial_prompt(cities, days, interests, transport, custom_reqs, attractions):
|
|
|
+# """红色文化初始行程提示词"""
|
|
|
+# city_names = [city.name for city in cities]
|
|
|
+# attraction_list = [
|
|
|
+# f"{att.name}[{'烈士纪念' if 'martyrs' in att.tags else '革命遗址' if 'revolution' in att.tags else '红色博物馆'}]"
|
|
|
+# for att in attractions
|
|
|
+# ]
|
|
|
+#
|
|
|
+# return f"""
|
|
|
+# 你是一个红色文化旅行规划专家,请严格按照以下要求规划行程:
|
|
|
+#
|
|
|
+# **硬性要求**:
|
|
|
+# 1. 所有景点必须为以下类型:
|
|
|
+# - 烈士陵园/纪念碑(含martyrs标签)
|
|
|
+# - 革命纪念馆/遗址(含memorial/revolution标签)
|
|
|
+# - 红色博物馆(含museum标签)
|
|
|
+# 2. 每日主题必须包含党史/革命史教育内容
|
|
|
+# 3. 景点顺序应符合历史时间线
|
|
|
+#
|
|
|
+# **行程信息**:
|
|
|
+# - 城市:{city_names}
|
|
|
+# - 天数:{days}天
|
|
|
+# - 兴趣偏好:{interests if interests else '红色文化教育'}
|
|
|
+# - 交通方式:{transport}
|
|
|
+# - 特殊要求:{custom_reqs if custom_reqs else '无'}
|
|
|
+#
|
|
|
+# **可选红色景点**:
|
|
|
+# {attraction_list}
|
|
|
+#
|
|
|
+# **输出格式**:
|
|
|
+# {{
|
|
|
+# "title": "红色主题标题(必须含'红色'或'革命')",
|
|
|
+# "description": "突出爱国主义教育的描述",
|
|
|
+# "suitable_for": "适合人群(如:党员干部、学生等)",
|
|
|
+# "days": [
|
|
|
+# {{
|
|
|
+# "day": 1,
|
|
|
+# "theme": "教育主题(如:'井冈山革命精神')",
|
|
|
+# "description": "当日教育重点",
|
|
|
+# "transport": "{transport}",
|
|
|
+# "attractions": [
|
|
|
+# {{
|
|
|
+# "id": 景点ID,
|
|
|
+# "name": "景点名称",
|
|
|
+# "visit_time": "建议时长(如:2小时)",
|
|
|
+# "notes": "具体教育意义(如:该景点展示了XX历史)"
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# """
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _build_red_regeneration_prompt(cities, days, interests, transport, custom_reqs, attractions, previous_plan):
|
|
|
+# """红色文化重新生成提示词"""
|
|
|
+# prev_plan = json.dumps(previous_plan, ensure_ascii=False, indent=2) if previous_plan else "无"
|
|
|
+#
|
|
|
+# return f"""
|
|
|
+# 请基于相同偏好重新规划不同的红色文化行程:
|
|
|
+#
|
|
|
+# **原行程**:
|
|
|
+# {prev_plan}
|
|
|
+#
|
|
|
+# **新行程要求**:
|
|
|
+# 1. 使用不同类型的红色景点(如原行程以纪念馆为主,新行程改为以博物馆为主)
|
|
|
+# 2. 按新的历史视角安排(如按时间倒序)
|
|
|
+# 3. 创建新的教育主题(如从"革命历程"改为"英雄人物")
|
|
|
+#
|
|
|
+# **偏好设置**:
|
|
|
+# - 城市:{[city.name for city in cities]}
|
|
|
+# - 天数:{days}天
|
|
|
+# - 交通:{transport}
|
|
|
+# - 特殊要求:{custom_reqs or '无'}
|
|
|
+#
|
|
|
+# **请返回新行程**:
|
|
|
+# {{
|
|
|
+# "title": "新红色主题(区别于原行程)",
|
|
|
+# "days": [
|
|
|
+# {{
|
|
|
+# "attractions": [
|
|
|
+# {{
|
|
|
+# "educational_focus": "新的教育侧重点"
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# ]
|
|
|
+# }}
|
|
|
+# """
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _call_moonshot_api(prompt, is_regeneration):
|
|
|
+# """调用Moonshot AI接口"""
|
|
|
+# try:
|
|
|
+# client = OpenAI(
|
|
|
+# api_key=settings.MOONSHOT_API_KEY,
|
|
|
+# base_url="https://api.moonshot.cn/v1"
|
|
|
+# )
|
|
|
+#
|
|
|
+# response = client.chat.completions.create(
|
|
|
+# model="moonshot-v1-8k",
|
|
|
+# messages=[
|
|
|
+# {
|
|
|
+# "role": "system",
|
|
|
+# "content": "你是一个红色文化专家,严格按用户要求生成行程,不添加无关内容"
|
|
|
+# },
|
|
|
+# {"role": "user", "content": prompt}
|
|
|
+# ],
|
|
|
+# response_format={"type": "json_object"},
|
|
|
+# temperature=0.7 if is_regeneration else 0.5
|
|
|
+# )
|
|
|
+#
|
|
|
+# content = response.choices[0].message.content
|
|
|
+# return json.loads(content.strip())
|
|
|
+#
|
|
|
+# except json.JSONDecodeError:
|
|
|
+# try:
|
|
|
+# content = response.choices[0].message.content
|
|
|
+# start = content.find('{')
|
|
|
+# end = content.rfind('}') + 1
|
|
|
+# return json.loads(content[start:end])
|
|
|
+# except:
|
|
|
+# return {"error": "响应解析失败", "details": "无法解析AI返回的JSON"}
|
|
|
+#
|
|
|
+# except Exception as e:
|
|
|
+# return {"error": "API调用失败", "details": str(e)}
|
|
|
+#
|
|
|
+# @staticmethod
|
|
|
+# def _validate_red_plan(plan):
|
|
|
+# """验证行程是否符合红色主题"""
|
|
|
+# required_phrases = ['红色', '革命', '烈士', '党史', '爱国主义']
|
|
|
+# plan_str = json.dumps(plan, ensure_ascii=False)
|
|
|
+#
|
|
|
+# # 检查关键词
|
|
|
+# if not any(phrase in plan_str for phrase in required_phrases):
|
|
|
+# return False
|
|
|
+#
|
|
|
+# # 检查景点备注
|
|
|
+# for day in plan.get('days', []):
|
|
|
+# for attr in day.get('attractions', []):
|
|
|
+# if not any(phrase in attr.get('notes', '') for phrase in required_phrases):
|
|
|
+# return False
|
|
|
+#
|
|
|
+# return True
|
|
|
+class TravelPlanService:
|
|
|
+ @staticmethod
|
|
|
+ def create_travel_plan(plan_data):
|
|
|
+ """
|
|
|
+ 根据输入数据创建旅行计划(无需登录版)
|
|
|
+ :param plan_data: 包含 city_ids, days, interests, transport 等
|
|
|
+ :return: 生成的旅行计划对象
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ # 1. 验证城市是否存在
|
|
|
+ cities = City.objects.filter(id__in=plan_data['city_ids'])
|
|
|
+ if not cities.exists():
|
|
|
+ raise ValueError("选择的城市不存在")
|
|
|
+
|
|
|
+ # 2. 调用AI生成计划数据
|
|
|
+ moonshot_data = {
|
|
|
+ 'city_ids': plan_data['city_ids'],
|
|
|
+ 'days': plan_data['days'],
|
|
|
+ 'interests': plan_data['interests'],
|
|
|
+ 'transport': plan_data['transport'],
|
|
|
+ 'custom_requirements': plan_data.get('custom_requirements', '')
|
|
|
+ }
|
|
|
+ plan_data = MoonshotAIService.generate_travel_plan(moonshot_data)
|
|
|
+
|
|
|
+ if not plan_data or 'error' in plan_data:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # 3. 创建旅行计划(无用户关联)
|
|
|
+ travel_plan = TravelPlan.objects.create(
|
|
|
+ title=plan_data['title'],
|
|
|
+ description=plan_data['description'],
|
|
|
+ days=len(plan_data['days']),
|
|
|
+ suitable_for=plan_data.get('suitable_for', ''),
|
|
|
+ status='completed'
|
|
|
+ )
|
|
|
+
|
|
|
+ # 4. 创建每日计划
|
|
|
+ for day_info in plan_data['days']:
|
|
|
+ day_plan = DayPlan.objects.create(
|
|
|
+ travel_plan=travel_plan,
|
|
|
+ day=day_info['day'],
|
|
|
+ theme=day_info['theme'],
|
|
|
+ description=day_info.get('description', ''),
|
|
|
+ transport=day_info.get('transport', plan_data.get('transport', ''))
|
|
|
+ )
|
|
|
+
|
|
|
+ # 5. 添加每日景点
|
|
|
+ for attraction_info in day_info['attractions']:
|
|
|
+ try:
|
|
|
+ attraction = Attraction.objects.get(id=attraction_info['id'])
|
|
|
+ DayPlanAttraction.objects.create(
|
|
|
+ day_plan=day_plan,
|
|
|
+ attraction=attraction,
|
|
|
+ order=attraction_info.get('order', 1),
|
|
|
+ visit_time=attraction_info.get('visit_time', ''),
|
|
|
+ notes=attraction_info.get('notes', '')
|
|
|
+ )
|
|
|
+ except Attraction.DoesNotExist:
|
|
|
+ continue
|
|
|
+
|
|
|
+ return travel_plan
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ print(f"创建旅行计划失败: {str(e)}")
|
|
|
+ return None
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def create_travel_plan_from_preference(preference):
|
|
|
+ """
|
|
|
+ 根据用户偏好创建旅行计划(需要登录)
|
|
|
+ """
|
|
|
+ # 调用AI生成计划
|
|
|
+ plan_data = MoonshotAIService.generate_travel_plan({
|
|
|
+ 'city_ids': list(preference.cities.values_list('id', flat=True)),
|
|
|
+ 'days': preference.days,
|
|
|
+ 'interests': preference.interests,
|
|
|
+ 'transport': preference.transport,
|
|
|
+ 'custom_requirements': preference.custom_requirements
|
|
|
+ })
|
|
|
+
|
|
|
+ if not plan_data or 'error' in plan_data:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # 创建旅行计划
|
|
|
+ travel_plan = TravelPlan.objects.create(
|
|
|
+ user=preference.user,
|
|
|
+ preference=preference,
|
|
|
+ title=plan_data['title'],
|
|
|
+ description=plan_data['description'],
|
|
|
+ days=preference.days,
|
|
|
+ suitable_for=plan_data.get('suitable_for', ''),
|
|
|
+ status='completed'
|
|
|
+ )
|
|
|
+
|
|
|
+ # 创建每日计划
|
|
|
+ for day_info in plan_data['days']:
|
|
|
+ day_plan = DayPlan.objects.create(
|
|
|
+ travel_plan=travel_plan,
|
|
|
+ day=day_info['day'],
|
|
|
+ theme=day_info['theme'],
|
|
|
+ description=day_info.get('description', ''),
|
|
|
+ transport=day_info.get('transport', '')
|
|
|
+ )
|
|
|
+
|
|
|
+ # 添加每日景点
|
|
|
+ for attraction_info in day_info['attractions']:
|
|
|
+ try:
|
|
|
+ attraction = Attraction.objects.get(id=attraction_info['id'])
|
|
|
+ DayPlanAttraction.objects.create(
|
|
|
+ day_plan=day_plan,
|
|
|
+ attraction=attraction,
|
|
|
+ order=attraction_info.get('order', 1),
|
|
|
+ visit_time=attraction_info.get('visit_time', ''),
|
|
|
+ notes=attraction_info.get('notes', '')
|
|
|
+ )
|
|
|
+ except Attraction.DoesNotExist:
|
|
|
+ continue
|
|
|
+
|
|
|
+ return travel_plan
|
|
|
+
|
|
|
+
|
|
|
+class CacheService:
|
|
|
+ @staticmethod
|
|
|
+ def get_cities():
|
|
|
+ """
|
|
|
+ 获取城市列表(带缓存)
|
|
|
+ """
|
|
|
+ cache_key = 'all_cities'
|
|
|
+ cities = cache.get(cache_key)
|
|
|
+
|
|
|
+ if not cities:
|
|
|
+ cities = list(City.objects.filter(is_hot=True).values('id', 'name', 'code', 'image'))
|
|
|
+ cache.set(cache_key, cities, timeout=3600) # 缓存1小时
|
|
|
+
|
|
|
+ return cities
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def get_attractions_by_city(city_id):
|
|
|
+ """
|
|
|
+ 获取城市景点列表(带缓存)
|
|
|
+ """
|
|
|
+ cache_key = f'attractions_city_{city_id}'
|
|
|
+ attractions = cache.get(cache_key)
|
|
|
+
|
|
|
+ if not attractions:
|
|
|
+ attractions = list(Attraction.objects.filter(city_id=city_id).values(
|
|
|
+ 'id', 'name', 'short_desc', 'image', 'tags'
|
|
|
+ ))
|
|
|
+ cache.set(cache_key, attractions, timeout=3600) # 缓存1小时
|
|
|
+
|
|
|
+ return attractions
|