services.py 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. import json
  2. import requests
  3. from django.conf import settings
  4. from django.db.models import Q
  5. from openai import OpenAI
  6. from .models import City, Attraction, TravelPlan, DayPlan, DayPlanAttraction
  7. from django.core.cache import cache
  8. import logging
  9. logger = logging.getLogger(__name__)
  10. import json
  11. from django.conf import settings
  12. from openai import OpenAI
  13. from .models import City, Attraction
  14. from django.core.cache import cache
  15. from datetime import datetime, timedelta
  16. import random
  17. # class MoonshotAIService:
  18. # @staticmethod
  19. # def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
  20. # """
  21. # 增强版红色旅游路线规划AI服务
  22. # """
  23. # # 处理输入参数
  24. # if isinstance(preference, dict):
  25. # city_ids = preference.get('city_ids', [])
  26. # cities = City.objects.filter(id__in=city_ids)
  27. # days = preference.get('days', 3)
  28. # interests = preference.get('interests', [])
  29. # transport = preference.get('transport', 'public')
  30. # custom_requirements = preference.get('custom_requirements', '')
  31. # else:
  32. # cities = preference.cities.all()
  33. # days = preference.days
  34. # interests = preference.interests
  35. # transport = preference.get_transport_display()
  36. # custom_requirements = preference.custom_requirements or '无'
  37. #
  38. # # 获取相关景点(优先红色旅游景点)
  39. # attractions = Attraction.objects.filter(
  40. # city__in=cities,
  41. # tags__contains="红色旅游"
  42. # ).select_related('city')
  43. #
  44. # # 如果没有足够红色景点,补充其他景点
  45. # if len(attractions) < days * 3:
  46. # additional_attractions = Attraction.objects.filter(
  47. # city__in=cities
  48. # ).exclude(tags__contains="红色旅游")[:10]
  49. # attractions = list(attractions) + list(additional_attractions)
  50. #
  51. # # 构建提示词
  52. # prompt = MoonshotAIService._build_enhanced_prompt(
  53. # cities, days, interests, transport,
  54. # custom_requirements, attractions,
  55. # is_regeneration, previous_plan
  56. # )
  57. #
  58. # # 调用Moonshot AI API
  59. # client = OpenAI(
  60. # api_key=settings.MOONSHOT_API_KEY,
  61. # base_url="https://api.moonshot.cn/v1"
  62. # )
  63. #
  64. # try:
  65. # response = client.chat.completions.create(
  66. # model="moonshot-v1-8k",
  67. # messages=[
  68. # {
  69. # "role": "system",
  70. # "content": "你是一个红色旅游规划专家,熟悉山东省所有红色景点。请按照指定格式输出,包含详细路线、时间安排和景点介绍。"
  71. # },
  72. # {"role": "user", "content": prompt}
  73. # ],
  74. # response_format={"type": "json_object"},
  75. # temperature=0.7 if is_regeneration else 0.5
  76. # )
  77. #
  78. # # 解析并处理返回数据
  79. # plan_data = json.loads(response.choices[0].message.content.strip())
  80. # return MoonshotAIService._process_ai_response(plan_data, attractions)
  81. #
  82. # except Exception as e:
  83. # print(f"AI服务异常: {str(e)}")
  84. # return {
  85. # "error": "行程生成失败",
  86. # "details": str(e)
  87. # }
  88. #
  89. # @staticmethod
  90. # def _build_enhanced_prompt(cities, days, interests, transport, requirements, attractions, is_regeneration,
  91. # previous_plan):
  92. # """构建增强版红色旅游提示词"""
  93. # city_names = [city.name for city in cities]
  94. # attraction_list = [
  95. # f"{att.id}:{att.name}[标签:{','.join(att.tags)}][城市:{att.city.name}][描述:{att.short_desc}]"
  96. # for att in attractions
  97. # ]
  98. #
  99. # base_prompt = f"""
  100. # 你是一个专业的红色旅游规划AI助手,请根据以下信息为游客规划山东省红色旅游行程:
  101. #
  102. # 基本要求:
  103. # - 城市:{city_names}
  104. # - 天数:{days}天
  105. # - 兴趣偏好:{interests or '无特别偏好'}
  106. # - 交通方式:{transport}
  107. # - 特殊要求:{requirements or '无'}
  108. #
  109. # 可选景点信息(ID:名称[标签][城市][简短描述]):
  110. # {attraction_list}
  111. #
  112. # 请设计一个富有教育意义的红色旅游路线,要求:
  113. # 1. 每天安排3-4个景点,包含至少2个红色景点
  114. # 2. 合理安排景点间的交通时间和午餐时间
  115. # 3. 每个景点提供详细的参观建议和背景介绍
  116. # 4. 路线设计要连贯,避免来回奔波
  117. # 5. 包含早中晚餐的推荐地点(尽量选择红色教育基地附近的餐馆)
  118. # """
  119. #
  120. # if is_regeneration:
  121. # base_prompt += f"""
  122. # 这是之前的行程计划,请重新设计不同的路线:
  123. # {json.dumps(previous_plan, ensure_ascii=False, indent=2)}
  124. # """
  125. #
  126. # base_prompt += f"""
  127. # 请返回JSON格式的行程计划,结构如下:
  128. # {{
  129. # "title": "行程标题(突出红色主题)",
  130. # "description": "行程整体描述(200字左右)",
  131. # "suitable_for": "适合人群(如'亲子家庭'、'学生团体'等)",
  132. # "days": [
  133. # {{
  134. # "day": 1,
  135. # "theme": "当日主题(如'革命精神传承之旅')",
  136. # "description": "当日详细描述",
  137. # "transport": "交通安排建议",
  138. # "morning": {{
  139. # "start_time": "08:00",
  140. # "attraction": 景点ID,
  141. # "visit_time": "建议参观时间(如'1.5小时')",
  142. # "description": "景点详细介绍(300字左右)",
  143. # "recommendation": "参观建议(如'建议先参观纪念馆主展厅')"
  144. # }},
  145. # "lunch": {{
  146. # "time": "12:00",
  147. # "recommendation": "午餐推荐地点和特色菜",
  148. # "description": "餐馆简介(100字左右)"
  149. # }},
  150. # "afternoon": [
  151. # {{
  152. # "start_time": "13:30",
  153. # "attraction": 景点ID,
  154. # "visit_time": "建议参观时间",
  155. # "description": "景点详细介绍",
  156. # "recommendation": "参观建议"
  157. # }},
  158. # ...
  159. # ],
  160. # "dinner": {{
  161. # "time": "18:00",
  162. # "recommendation": "晚餐推荐",
  163. # "description": "餐馆简介"
  164. # }},
  165. # "evening_activity": {{
  166. # "description": "晚间活动建议(如'观看红色主题演出')",
  167. # "recommendation": "活动详情"
  168. # }}
  169. # }},
  170. # ...
  171. # ],
  172. # "travel_tips": [
  173. # "穿着建议:...",
  174. # "注意事项:...",
  175. # "红色教育重点:..."
  176. # ]
  177. # }}
  178. # """
  179. # return base_prompt
  180. #
  181. # @staticmethod
  182. # def _process_ai_response(plan_data, attractions):
  183. # """处理AI返回数据,补充完整景点信息"""
  184. # attraction_map = {att.id: att for att in attractions}
  185. #
  186. # for day in plan_data.get('days', []):
  187. # # 处理上午景点
  188. # if 'morning' in day and day['morning']['attraction'] in attraction_map:
  189. # att = attraction_map[day['morning']['attraction']]
  190. # day['morning'].update({
  191. # 'attraction_data': {
  192. # 'name': att.name,
  193. # 'image': att.image.url if att.image else '',
  194. # 'address': att.address,
  195. # 'open_hours': att.open_hours,
  196. # 'ticket_price': str(att.ticket_price)
  197. # }
  198. # })
  199. #
  200. # # 处理下午景点
  201. # if 'afternoon' in day:
  202. # for item in day['afternoon']:
  203. # if item['attraction'] in attraction_map:
  204. # att = attraction_map[item['attraction']]
  205. # item.update({
  206. # 'attraction_data': {
  207. # 'name': att.name,
  208. # 'image': att.image.url if att.image else '',
  209. # 'address': att.address,
  210. # 'open_hours': att.open_hours,
  211. # 'ticket_price': str(att.ticket_price)
  212. # }
  213. # })
  214. #
  215. # return plan_data
  216. # services.py
  217. class MoonshotAIService:
  218. RED_TOURISM_TAGS = ['红色旅游', '革命', '烈士', '纪念馆', '党史']
  219. @staticmethod
  220. def generate_travel_plan(preference):
  221. """
  222. 红色旅游专用生成方法
  223. """
  224. try:
  225. # 1. 获取城市和景点
  226. cities = City.objects.filter(id__in=preference['city_ids'])
  227. if not cities.exists():
  228. return {'error': '未找到指定城市'}
  229. # 2. 优先获取红色景点
  230. attractions = Attraction.objects.filter(
  231. city__in=cities,
  232. tags__overlap=MoonshotAIService.RED_TOURISM_TAGS
  233. )
  234. # 如果红色景点不足,补充其他景点
  235. if len(attractions) < preference['days'] * 2:
  236. extra_attractions = Attraction.objects.filter(
  237. city__in=cities
  238. ).exclude(tags__overlap=MoonshotAIService.RED_TOURISM_TAGS)[:10]
  239. attractions = list(attractions) + list(extra_attractions)
  240. # 3. 构建AI提示
  241. prompt = MoonshotAIService._build_red_tourism_prompt(
  242. cities,
  243. preference['days'],
  244. preference['interests'],
  245. preference['transport'],
  246. preference.get('custom_requirements', ''),
  247. attractions
  248. )
  249. # 4. 调用AI接口
  250. client = OpenAI(
  251. api_key=settings.MOONSHOT_API_KEY,
  252. base_url="https://api.moonshot.cn/v1"
  253. )
  254. response = client.chat.completions.create(
  255. model="moonshot-v1-8k",
  256. messages=[
  257. {
  258. "role": "system",
  259. "content": "你是红色旅游专家,专门规划革命教育路线。必须包含党史学习内容。"
  260. },
  261. {"role": "user", "content": prompt}
  262. ],
  263. response_format={"type": "json_object"},
  264. temperature=0.5
  265. )
  266. # 5. 解析响应
  267. plan_data = json.loads(response.choices[0].message.content)
  268. return MoonshotAIService._process_red_tourism_response(plan_data, attractions)
  269. except Exception as e:
  270. return {'error': str(e)}
  271. @staticmethod
  272. def _build_red_tourism_prompt(cities, days, interests, transport, requirements, attractions):
  273. """构建红色旅游专用提示词"""
  274. city_names = [city.name for city in cities]
  275. attraction_list = "\n".join([
  276. f"{att.id}:{att.name}[标签:{','.join(att.tags)}][城市:{att.city.name}]"
  277. for att in attractions
  278. ])
  279. return f"""
  280. 请规划一个{days}天的红色旅游路线,要求:
  281. - 城市:{city_names}
  282. - 必须包含至少{max(2, days)}个红色教育基地
  283. - 交通方式:{transport}
  284. - 特殊要求:{requirements or '无'}
  285. 可选景点:
  286. {attraction_list}
  287. 返回JSON格式,包含:
  288. - title: 行程标题(必须含"红色"或"革命")
  289. - description: 行程描述(突出教育意义)
  290. - days: 每日安排(必须包含educational_points教育要点)
  291. - 每个景点标注is_red_tourism是否为红色景点
  292. """
  293. @staticmethod
  294. def _process_red_tourism_response(plan_data, attractions):
  295. """处理AI返回的红色旅游数据"""
  296. attraction_map = {att.id: att for att in attractions}
  297. for day in plan_data.get('days', []):
  298. for attraction in day.get('attractions', []):
  299. att_id = attraction.get('id')
  300. if att_id in attraction_map:
  301. att = attraction_map[att_id]
  302. attraction.update({
  303. 'image': att.image.url if att.image else '',
  304. 'address': att.address,
  305. 'open_hours': att.open_hours,
  306. 'is_red_tourism': any(tag in att.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
  307. })
  308. return plan_data
  309. @staticmethod
  310. def regenerate_travel_plan(params):
  311. """
  312. 增强版重新生成方法,确保返回完整景点数据
  313. """
  314. try:
  315. # 1. 参数验证和转换
  316. city_ids = params.get('city_ids', [])
  317. if isinstance(city_ids, int): # 处理单个城市ID的情况
  318. city_ids = [city_ids]
  319. cities = City.objects.filter(id__in=city_ids)
  320. if not cities.exists():
  321. return {'error': '未找到指定城市'}
  322. days = int(params.get('days', 3))
  323. # 2. 获取所有相关景点(原行程景点 + 新红色景点)
  324. previous_attractions = []
  325. for day in params.get('previous_plan', {}).get('days', []):
  326. for attr in day.get('attractions', []):
  327. if attr.get('id'):
  328. previous_attractions.append(attr['id'])
  329. # 查询数据库获取完整景点对象
  330. attractions = Attraction.objects.filter(
  331. Q(id__in=previous_attractions) |
  332. Q(city__in=cities, tags__overlap=MoonshotAIService.RED_TOURISM_TAGS)
  333. ).distinct()
  334. # 3. 构建更明确的提示词
  335. prompt = f"""
  336. 请基于原行程重新规划{days}天红色旅游路线,要求:
  337. **必须包含以下元素**:
  338. 1. 至少{max(2, days)}个红色教育基地
  339. 2. 保留原行程中评分高的景点
  340. 3. 每日主题明确(如"革命精神传承")
  341. **城市范围**:{[city.name for city in cities]}
  342. **可选景点**:
  343. {[f"{a.id}:{a.name}[红色:{any(tag in a.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)}]" for a in attractions]}
  344. **返回格式示例**:
  345. {{
  346. "title": "新行程标题",
  347. "days": [
  348. {{
  349. "day": 1,
  350. "attractions": [
  351. {{
  352. "id": 景点ID,
  353. "name": "景点名称",
  354. "is_red_tourism": true/false,
  355. "educational_value": "高/中/低"
  356. }}
  357. ]
  358. }}
  359. ]
  360. }}
  361. """
  362. # 4. 调用AI接口(示例代码,实际需要替换为您的AI调用)
  363. client = OpenAI(api_key=settings.MOONSHOT_API_KEY, base_url="https://api.moonshot.cn/v1")
  364. response = client.chat.completions.create(
  365. model="moonshot-v1-8k",
  366. messages=[
  367. {"role": "system", "content": "你是红色旅游专家,严格按照要求生成行程"},
  368. {"role": "user", "content": prompt}
  369. ],
  370. response_format={"type": "json_object"},
  371. temperature=0.6
  372. )
  373. # 5. 处理响应数据
  374. plan_data = json.loads(response.choices[0].message.content)
  375. # 补充景点完整信息
  376. attraction_map = {a.id: a for a in attractions}
  377. for day in plan_data.get('days', []):
  378. for attr in day.get('attractions', []):
  379. if attr['id'] in attraction_map:
  380. a = attraction_map[attr['id']]
  381. attr.update({
  382. 'image': a.image.url if a.image else '',
  383. 'address': a.address,
  384. 'open_hours': a.open_hours,
  385. 'is_red_tourism': any(tag in a.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
  386. })
  387. return plan_data
  388. except Exception as e:
  389. logger.error(f"重新生成失败: {str(e)}", exc_info=True)
  390. return {'error': str(e)}
  391. # class MoonshotAIService:
  392. # @staticmethod
  393. # def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
  394. # """
  395. # 调用Moonshot AI API生成旅行计划
  396. # :param preference: 偏好设置(字典或Django模型对象)
  397. # :param is_regeneration: 是否为重新生成请求
  398. # :param previous_plan: 之前的行程计划(仅重新生成时使用)
  399. # :return: 生成的行程计划字典
  400. # """
  401. # # 处理输入参数(支持字典或Django模型对象)
  402. # if isinstance(preference, dict):
  403. # city_ids = preference.get('city_ids', [])
  404. # cities = City.objects.filter(id__in=city_ids)
  405. # days = preference.get('days', 3)
  406. # interests = preference.get('interests', [])
  407. # transport = preference.get('transport', 'public')
  408. # custom_requirements = preference.get('custom_requirements', '')
  409. # else:
  410. # cities = preference.cities.all()
  411. # days = preference.days
  412. # interests = preference.interests
  413. # transport = preference.get_transport_display()
  414. # custom_requirements = preference.custom_requirements or '无'
  415. #
  416. # # 获取相关景点
  417. # attractions = Attraction.objects.filter(city__in=cities)
  418. #
  419. # # 构建不同的提示词基于是否是重新生成
  420. # if is_regeneration:
  421. # prompt = MoonshotAIService._build_regeneration_prompt(
  422. # cities, days, interests, transport,
  423. # custom_requirements, attractions, previous_plan
  424. # )
  425. # else:
  426. # prompt = MoonshotAIService._build_initial_prompt(
  427. # cities, days, interests, transport,
  428. # custom_requirements, attractions
  429. # )
  430. #
  431. # # 调用Moonshot AI API
  432. # client = OpenAI(
  433. # api_key=settings.MOONSHOT_API_KEY,
  434. # base_url="https://api.moonshot.cn/v1"
  435. # )
  436. #
  437. # try:
  438. # response = client.chat.completions.create(
  439. # model="moonshot-v1-8k",
  440. # messages=[
  441. # {
  442. # "role": "system",
  443. # "content": "你是一个旅行规划专家,严格按照用户指定的JSON格式输出,不添加额外解释。"
  444. # },
  445. # {"role": "user", "content": prompt}
  446. # ],
  447. # response_format={"type": "json_object"},
  448. # temperature=0.7 if is_regeneration else 0.5 # 重新生成时增加一点随机性
  449. # )
  450. #
  451. # # 解析并返回JSON
  452. # return json.loads(response.choices[0].message.content.strip())
  453. #
  454. # except json.JSONDecodeError:
  455. # print("API返回的不是有效JSON,尝试修复...")
  456. # raw_content = response.choices[0].message.content
  457. # start_idx = raw_content.find("{")
  458. # end_idx = raw_content.rfind("}") + 1
  459. # return json.loads(raw_content[start_idx:end_idx])
  460. #
  461. # except Exception as e:
  462. # print(f"Moonshot AI API调用异常: {str(e)}")
  463. # return {
  464. # "error": "行程生成失败",
  465. # "details": str(e)
  466. # }
  467. #
  468. # @staticmethod
  469. # def _build_initial_prompt(cities, days, interests, transport, custom_requirements, attractions):
  470. # """构建初始行程提示词"""
  471. # return f"""
  472. # 你是一个专业的旅行规划AI助手,请根据以下信息规划行程:
  473. # - 城市:{[city.name for city in cities]}
  474. # - 天数:{days}天
  475. # - 兴趣:{', '.join(interests) if interests else '无特别偏好'}
  476. # - 交通方式:{transport}
  477. # - 特殊要求:{custom_requirements if custom_requirements else '无'}
  478. #
  479. # 可选景点(格式:名称[标签]):
  480. # {[f"{att.name}[{', '.join(att.tags)}]" for att in attractions]}
  481. #
  482. # 请返回JSON格式的行程计划,结构如下:
  483. # {{
  484. # "title": "行程标题",
  485. # "description": "行程描述",
  486. # "suitable_for": "适合人群描述",
  487. # "days": [
  488. # {{
  489. # "day": 1,
  490. # "theme": "当日主题",
  491. # "description": "当日描述",
  492. # "transport": "交通方式描述",
  493. # "attractions": [
  494. # {{
  495. # "id": 景点ID,
  496. # "name": "景点名称",
  497. # "visit_time": "建议参观时间",
  498. # "notes": "备注信息"
  499. # }}
  500. # ]
  501. # }}
  502. # ]
  503. # }}
  504. # """
  505. #
  506. # @staticmethod
  507. # def _build_regeneration_prompt(cities, days, interests, transport, custom_requirements, attractions, previous_plan):
  508. # """构建重新生成行程提示词"""
  509. # previous_plan_str = json.dumps(previous_plan, ensure_ascii=False, indent=2) if previous_plan else "无"
  510. #
  511. # return f"""
  512. # 你是一个专业的旅行规划AI助手,请基于相同的偏好但不同的安排重新规划行程:
  513. #
  514. # 原行程计划:
  515. # {previous_plan_str}
  516. #
  517. # 偏好设置:
  518. # - 城市:{[city.name for city in cities]}
  519. # - 天数:{days}天
  520. # - 兴趣:{', '.join(interests) if interests else '无特别偏好'}
  521. # - 交通方式:{transport}
  522. # - 特殊要求:{custom_requirements if custom_requirements else '无'}
  523. #
  524. # 可选景点(格式:名称[标签]):
  525. # {[f"{att.name}[{', '.join(att.tags)}]" for att in attractions]}
  526. #
  527. # 请返回一个不同的JSON格式行程计划,要求:
  528. # 1. 使用不同的景点组合
  529. # 2. 调整每天的行程顺序
  530. # 3. 创建新的每日主题
  531. # 4. 保持相同的天数和城市
  532. #
  533. # 结构如下:
  534. # {{
  535. # "title": "新行程标题(与之前不同)",
  536. # "description": "新行程描述",
  537. # "suitable_for": "适合人群描述",
  538. # "days": [
  539. # {{
  540. # "day": 1,
  541. # "theme": "新当日主题",
  542. # "description": "当日描述",
  543. # "transport": "交通方式描述",
  544. # "attractions": [
  545. # {{
  546. # "id": 景点ID,
  547. # "name": "景点名称",
  548. # "visit_time": "建议参观时间",
  549. # "notes": "备注信息"
  550. # }}
  551. # ]
  552. # }}
  553. # ]
  554. # }}
  555. # """
  556. #
  557. # import json
  558. # from django.db.models import Q
  559. # from django.conf import settings
  560. # from openai import OpenAI
  561. # from .models import City, Attraction
  562. #
  563. #
  564. # class MoonshotAIService:
  565. # """
  566. # 红色文化旅游规划服务类
  567. # 严格限定只选择烈士陵园、革命纪念馆、红色博物馆等红色文化景点
  568. # """
  569. #
  570. # # 定义合法的红色景点类型标签
  571. # RED_ATTRACTION_TAGS = ['martyrs', 'memorial', 'museum', 'revolution', '红色旅游']
  572. #
  573. # @staticmethod
  574. # def generate_travel_plan(preference, is_regeneration=False, previous_plan=None):
  575. # """
  576. # 生成红色文化主题旅行计划
  577. # :param preference: 偏好设置(dict或Django模型)
  578. # :param is_regeneration: 是否重新生成
  579. # :param previous_plan: 原行程(重新生成时用)
  580. # :return: 行程计划(dict)或错误信息
  581. # """
  582. # # 1. 参数解析
  583. # params = MoonshotAIService._parse_preferences(preference)
  584. # if 'error' in params:
  585. # return params
  586. #
  587. # cities, days, interests, transport, custom_reqs = params
  588. #
  589. # # 2. 获取红色文化景点
  590. # attractions = MoonshotAIService._get_red_attractions(cities)
  591. # if isinstance(attractions, dict) and 'error' in attractions:
  592. # return attractions
  593. #
  594. # # 3. 构建AI提示词
  595. # prompt = MoonshotAIService._build_prompt(
  596. # cities, days, interests, transport,
  597. # custom_reqs, attractions, is_regeneration, previous_plan
  598. # )
  599. #
  600. # # 4. 调用AI接口
  601. # plan = MoonshotAIService._call_moonshot_api(prompt, is_regeneration)
  602. # if 'error' in plan:
  603. # return plan
  604. #
  605. # # 5. 验证结果
  606. # if not MoonshotAIService._validate_red_plan(plan):
  607. # return {
  608. # "error": "行程验证失败",
  609. # "details": "生成的行程不符合红色文化主题要求"
  610. # }
  611. #
  612. # return plan
  613. #
  614. # @staticmethod
  615. # def _parse_preferences(preference):
  616. # """解析偏好参数"""
  617. # try:
  618. # if isinstance(preference, dict):
  619. # city_ids = preference.get('city_ids', [])
  620. # cities = City.objects.filter(id__in=city_ids)
  621. # days = preference.get('days', 3)
  622. # interests = preference.get('interests', [])
  623. # transport = preference.get('transport', 'public')
  624. # custom_reqs = preference.get('custom_requirements', '')
  625. # else:
  626. # cities = preference.cities.all()
  627. # days = preference.days
  628. # interests = preference.interests
  629. # transport = preference.get_transport_display()
  630. # custom_reqs = preference.custom_requirements or '无'
  631. #
  632. # if not cities.exists():
  633. # return {"error": "未选择有效城市", "details": "请至少选择一个城市"}
  634. #
  635. # return cities, days, interests, transport, custom_reqs
  636. #
  637. # except Exception as e:
  638. # return {"error": "参数解析失败", "details": str(e)}
  639. #
  640. # @staticmethod
  641. # def _get_red_attractions(cities):
  642. # """获取红色文化景点"""
  643. # try:
  644. # # 精确查询
  645. # attractions = Attraction.objects.filter(
  646. # city__in=cities,
  647. # tags__overlap=MoonshotAIService.RED_ATTRACTION_TAGS
  648. # ).distinct()
  649. #
  650. # # 放宽条件查询
  651. # if not attractions.exists():
  652. # attractions = Attraction.objects.filter(
  653. # city__in=cities,
  654. # name__iregex=r'革命|烈士|党史|红色|纪念馆'
  655. # )
  656. #
  657. # if not attractions.exists():
  658. # red_cities = ["北京", "延安", "井冈山", "遵义", "韶山"]
  659. # return {
  660. # "error": "未找到红色景点",
  661. # "details": "当前城市未找到红色文化景点",
  662. # "suggestion": f"建议选择{','.join(red_cities)}等红色旅游城市"
  663. # }
  664. #
  665. # return attractions
  666. #
  667. # except Exception as e:
  668. # return {"error": "景点查询失败", "details": str(e)}
  669. #
  670. # @staticmethod
  671. # def _build_prompt(cities, days, interests, transport, custom_reqs, attractions, is_regeneration, previous_plan):
  672. # """构建AI提示词"""
  673. # if is_regeneration:
  674. # return MoonshotAIService._build_red_regeneration_prompt(
  675. # cities, days, interests, transport,
  676. # custom_reqs, attractions, previous_plan
  677. # )
  678. # return MoonshotAIService._build_red_initial_prompt(
  679. # cities, days, interests, transport,
  680. # custom_reqs, attractions
  681. # )
  682. #
  683. # @staticmethod
  684. # def _build_red_initial_prompt(cities, days, interests, transport, custom_reqs, attractions):
  685. # """红色文化初始行程提示词"""
  686. # city_names = [city.name for city in cities]
  687. # attraction_list = [
  688. # f"{att.name}[{'烈士纪念' if 'martyrs' in att.tags else '革命遗址' if 'revolution' in att.tags else '红色博物馆'}]"
  689. # for att in attractions
  690. # ]
  691. #
  692. # return f"""
  693. # 你是一个红色文化旅行规划专家,请严格按照以下要求规划行程:
  694. #
  695. # **硬性要求**:
  696. # 1. 所有景点必须为以下类型:
  697. # - 烈士陵园/纪念碑(含martyrs标签)
  698. # - 革命纪念馆/遗址(含memorial/revolution标签)
  699. # - 红色博物馆(含museum标签)
  700. # 2. 每日主题必须包含党史/革命史教育内容
  701. # 3. 景点顺序应符合历史时间线
  702. #
  703. # **行程信息**:
  704. # - 城市:{city_names}
  705. # - 天数:{days}天
  706. # - 兴趣偏好:{interests if interests else '红色文化教育'}
  707. # - 交通方式:{transport}
  708. # - 特殊要求:{custom_reqs if custom_reqs else '无'}
  709. #
  710. # **可选红色景点**:
  711. # {attraction_list}
  712. #
  713. # **输出格式**:
  714. # {{
  715. # "title": "红色主题标题(必须含'红色'或'革命')",
  716. # "description": "突出爱国主义教育的描述",
  717. # "suitable_for": "适合人群(如:党员干部、学生等)",
  718. # "days": [
  719. # {{
  720. # "day": 1,
  721. # "theme": "教育主题(如:'井冈山革命精神')",
  722. # "description": "当日教育重点",
  723. # "transport": "{transport}",
  724. # "attractions": [
  725. # {{
  726. # "id": 景点ID,
  727. # "name": "景点名称",
  728. # "visit_time": "建议时长(如:2小时)",
  729. # "notes": "具体教育意义(如:该景点展示了XX历史)"
  730. # }}
  731. # ]
  732. # }}
  733. # ]
  734. # }}
  735. # """
  736. #
  737. # @staticmethod
  738. # def _build_red_regeneration_prompt(cities, days, interests, transport, custom_reqs, attractions, previous_plan):
  739. # """红色文化重新生成提示词"""
  740. # prev_plan = json.dumps(previous_plan, ensure_ascii=False, indent=2) if previous_plan else "无"
  741. #
  742. # return f"""
  743. # 请基于相同偏好重新规划不同的红色文化行程:
  744. #
  745. # **原行程**:
  746. # {prev_plan}
  747. #
  748. # **新行程要求**:
  749. # 1. 使用不同类型的红色景点(如原行程以纪念馆为主,新行程改为以博物馆为主)
  750. # 2. 按新的历史视角安排(如按时间倒序)
  751. # 3. 创建新的教育主题(如从"革命历程"改为"英雄人物")
  752. #
  753. # **偏好设置**:
  754. # - 城市:{[city.name for city in cities]}
  755. # - 天数:{days}天
  756. # - 交通:{transport}
  757. # - 特殊要求:{custom_reqs or '无'}
  758. #
  759. # **请返回新行程**:
  760. # {{
  761. # "title": "新红色主题(区别于原行程)",
  762. # "days": [
  763. # {{
  764. # "attractions": [
  765. # {{
  766. # "educational_focus": "新的教育侧重点"
  767. # }}
  768. # ]
  769. # }}
  770. # ]
  771. # }}
  772. # """
  773. #
  774. # @staticmethod
  775. # def _call_moonshot_api(prompt, is_regeneration):
  776. # """调用Moonshot AI接口"""
  777. # try:
  778. # client = OpenAI(
  779. # api_key=settings.MOONSHOT_API_KEY,
  780. # base_url="https://api.moonshot.cn/v1"
  781. # )
  782. #
  783. # response = client.chat.completions.create(
  784. # model="moonshot-v1-8k",
  785. # messages=[
  786. # {
  787. # "role": "system",
  788. # "content": "你是一个红色文化专家,严格按用户要求生成行程,不添加无关内容"
  789. # },
  790. # {"role": "user", "content": prompt}
  791. # ],
  792. # response_format={"type": "json_object"},
  793. # temperature=0.7 if is_regeneration else 0.5
  794. # )
  795. #
  796. # content = response.choices[0].message.content
  797. # return json.loads(content.strip())
  798. #
  799. # except json.JSONDecodeError:
  800. # try:
  801. # content = response.choices[0].message.content
  802. # start = content.find('{')
  803. # end = content.rfind('}') + 1
  804. # return json.loads(content[start:end])
  805. # except:
  806. # return {"error": "响应解析失败", "details": "无法解析AI返回的JSON"}
  807. #
  808. # except Exception as e:
  809. # return {"error": "API调用失败", "details": str(e)}
  810. #
  811. # @staticmethod
  812. # def _validate_red_plan(plan):
  813. # """验证行程是否符合红色主题"""
  814. # required_phrases = ['红色', '革命', '烈士', '党史', '爱国主义']
  815. # plan_str = json.dumps(plan, ensure_ascii=False)
  816. #
  817. # # 检查关键词
  818. # if not any(phrase in plan_str for phrase in required_phrases):
  819. # return False
  820. #
  821. # # 检查景点备注
  822. # for day in plan.get('days', []):
  823. # for attr in day.get('attractions', []):
  824. # if not any(phrase in attr.get('notes', '') for phrase in required_phrases):
  825. # return False
  826. #
  827. # return True
  828. class TravelPlanService:
  829. @staticmethod
  830. def create_travel_plan(plan_data):
  831. """
  832. 根据输入数据创建旅行计划(无需登录版)
  833. :param plan_data: 包含 city_ids, days, interests, transport 等
  834. :return: 生成的旅行计划对象
  835. """
  836. try:
  837. # 1. 验证城市是否存在
  838. cities = City.objects.filter(id__in=plan_data['city_ids'])
  839. if not cities.exists():
  840. raise ValueError("选择的城市不存在")
  841. # 2. 调用AI生成计划数据
  842. moonshot_data = {
  843. 'city_ids': plan_data['city_ids'],
  844. 'days': plan_data['days'],
  845. 'interests': plan_data['interests'],
  846. 'transport': plan_data['transport'],
  847. 'custom_requirements': plan_data.get('custom_requirements', '')
  848. }
  849. plan_data = MoonshotAIService.generate_travel_plan(moonshot_data)
  850. if not plan_data or 'error' in plan_data:
  851. return None
  852. # 3. 创建旅行计划(无用户关联)
  853. travel_plan = TravelPlan.objects.create(
  854. title=plan_data['title'],
  855. description=plan_data['description'],
  856. days=len(plan_data['days']),
  857. suitable_for=plan_data.get('suitable_for', ''),
  858. status='completed'
  859. )
  860. # 4. 创建每日计划
  861. for day_info in plan_data['days']:
  862. day_plan = DayPlan.objects.create(
  863. travel_plan=travel_plan,
  864. day=day_info['day'],
  865. theme=day_info['theme'],
  866. description=day_info.get('description', ''),
  867. transport=day_info.get('transport', plan_data.get('transport', ''))
  868. )
  869. # 5. 添加每日景点
  870. for attraction_info in day_info['attractions']:
  871. try:
  872. attraction = Attraction.objects.get(id=attraction_info['id'])
  873. DayPlanAttraction.objects.create(
  874. day_plan=day_plan,
  875. attraction=attraction,
  876. order=attraction_info.get('order', 1),
  877. visit_time=attraction_info.get('visit_time', ''),
  878. notes=attraction_info.get('notes', '')
  879. )
  880. except Attraction.DoesNotExist:
  881. continue
  882. return travel_plan
  883. except Exception as e:
  884. print(f"创建旅行计划失败: {str(e)}")
  885. return None
  886. @staticmethod
  887. def create_travel_plan_from_preference(preference):
  888. """
  889. 根据用户偏好创建旅行计划(需要登录)
  890. """
  891. # 调用AI生成计划
  892. plan_data = MoonshotAIService.generate_travel_plan({
  893. 'city_ids': list(preference.cities.values_list('id', flat=True)),
  894. 'days': preference.days,
  895. 'interests': preference.interests,
  896. 'transport': preference.transport,
  897. 'custom_requirements': preference.custom_requirements
  898. })
  899. if not plan_data or 'error' in plan_data:
  900. return None
  901. # 创建旅行计划
  902. travel_plan = TravelPlan.objects.create(
  903. user=preference.user,
  904. preference=preference,
  905. title=plan_data['title'],
  906. description=plan_data['description'],
  907. days=preference.days,
  908. suitable_for=plan_data.get('suitable_for', ''),
  909. status='completed'
  910. )
  911. # 创建每日计划
  912. for day_info in plan_data['days']:
  913. day_plan = DayPlan.objects.create(
  914. travel_plan=travel_plan,
  915. day=day_info['day'],
  916. theme=day_info['theme'],
  917. description=day_info.get('description', ''),
  918. transport=day_info.get('transport', '')
  919. )
  920. # 添加每日景点
  921. for attraction_info in day_info['attractions']:
  922. try:
  923. attraction = Attraction.objects.get(id=attraction_info['id'])
  924. DayPlanAttraction.objects.create(
  925. day_plan=day_plan,
  926. attraction=attraction,
  927. order=attraction_info.get('order', 1),
  928. visit_time=attraction_info.get('visit_time', ''),
  929. notes=attraction_info.get('notes', '')
  930. )
  931. except Attraction.DoesNotExist:
  932. continue
  933. return travel_plan
  934. class CacheService:
  935. @staticmethod
  936. def get_cities():
  937. """
  938. 获取城市列表(带缓存)
  939. """
  940. cache_key = 'all_cities'
  941. cities = cache.get(cache_key)
  942. if not cities:
  943. cities = list(City.objects.filter(is_hot=True).values('id', 'name', 'code', 'image'))
  944. cache.set(cache_key, cities, timeout=3600) # 缓存1小时
  945. return cities
  946. @staticmethod
  947. def get_attractions_by_city(city_id):
  948. """
  949. 获取城市景点列表(带缓存)
  950. """
  951. cache_key = f'attractions_city_{city_id}'
  952. attractions = cache.get(cache_key)
  953. if not attractions:
  954. attractions = list(Attraction.objects.filter(city_id=city_id).values(
  955. 'id', 'name', 'short_desc', 'image', 'tags'
  956. ))
  957. cache.set(cache_key, attractions, timeout=3600) # 缓存1小时
  958. return attractions