services.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import re
  2. import uuid
  3. from django.db.models import Q
  4. from .models import City, Attraction
  5. import logging
  6. logger = logging.getLogger(__name__)
  7. import json
  8. from django.conf import settings
  9. from openai import OpenAI
  10. from .models import City, Attraction
  11. from django.core.cache import cache
  12. import traceback
  13. # services.py
  14. class MoonshotAIService:
  15. RED_TOURISM_TAGS = ['红色旅游', '革命', '烈士', '纪念馆', '党史']
  16. BATCH_SIZE = 3 # 分批处理的天数
  17. @staticmethod
  18. def generate_travel_plan(preference):
  19. """
  20. 红色旅游专用生成方法 - 增强版,支持长行程分批次处理
  21. """
  22. try:
  23. # 1. 获取城市和景点
  24. cities = City.objects.filter(id__in=preference['city_ids'])
  25. if not cities.exists():
  26. return {'error': '未找到指定城市'}
  27. days = preference['days']
  28. # 2. 获取所有景点(一次性查询提高效率)
  29. all_attractions = list(Attraction.objects.filter(city__in=cities))
  30. # 3. 根据天数决定生成方式
  31. return MoonshotAIService._generate_long_plan_in_batches(cities, preference, all_attractions)
  32. except Exception as e:
  33. logger.error(f"生成行程失败: {str(e)}", exc_info=True)
  34. return {'error': str(e)}
  35. @staticmethod
  36. def _generate_long_plan_in_batches(cities, preference, all_attractions):
  37. """处理2天及以上的长行程(分批生成),解决景点重复问题"""
  38. total_days = preference['days']
  39. batches = (total_days + MoonshotAIService.BATCH_SIZE - 1) // MoonshotAIService.BATCH_SIZE
  40. full_plan = {
  41. "title": f"{total_days}天红色文化之旅",
  42. "description": "通过参观革命历史遗址学习党史",
  43. "days": [],
  44. "education_goals": []
  45. }
  46. # 维护已使用的景点ID集合和名称集合(使用字典提高查找效率)
  47. used_attractions = {
  48. 'ids': set(),
  49. 'names': set()
  50. }
  51. for batch_num in range(batches):
  52. start_day = batch_num * MoonshotAIService.BATCH_SIZE + 1
  53. end_day = min((batch_num + 1) * MoonshotAIService.BATCH_SIZE, total_days)
  54. current_batch_days = end_day - start_day + 1
  55. try:
  56. # 为当前批次筛选景点(严格排除已使用的景点)
  57. batch_attractions = [
  58. att for att in all_attractions
  59. if (att.id not in used_attractions['ids'] and
  60. att.name not in used_attractions['names'])
  61. ]
  62. # 确保每批有足够的红色景点
  63. red_attractions = [
  64. att for att in batch_attractions
  65. if any(tag in att.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
  66. ]
  67. # 计算需要的红色景点数(每天至少2个)
  68. needed_red_attractions = current_batch_days * 2
  69. if len(red_attractions) < needed_red_attractions:
  70. # 如果红色景点不足,从所有景点中补充
  71. extra_attractions = [
  72. att for att in batch_attractions
  73. if att not in red_attractions
  74. ]
  75. batch_attractions = red_attractions + extra_attractions[
  76. :needed_red_attractions - len(red_attractions)]
  77. else:
  78. batch_attractions = red_attractions[:needed_red_attractions] + [
  79. att for att in batch_attractions
  80. if att not in red_attractions
  81. ]
  82. # 构建当前批次的提示词 - 添加景点去重提示
  83. batch_prompt = f"""
  84. 你是一个专业的红色旅游路线规划AI助手。请为第{start_day}天到第{end_day}天规划红色旅游路线,共{current_batch_days}天。
  85. 重要要求:
  86. 1. 必须严格避免选择重复景点(已使用景点:{list(used_attractions['names'])[:10]}...)
  87. 2. 必须返回严格符合JSON格式的数据
  88. 3. 所有字符串值必须用双引号括起来
  89. 4. 确保所有JSON字段都有正确的闭合
  90. 5. 不要包含任何注释或额外文本
  91. 6. 每天必须安排1-5个景点
  92. 7. 每个景点必须与红色主题有关
  93. 8. 景点之间要考虑地理位置和交通时间
  94. 9. 确保每个JSON对象属性后面都有逗号分隔符,除了最后一个属性
  95. 10. 所有字符串值必须在一行内完成,不能换行
  96. 11. 字符串值中不能包含未转义的引号
  97. 特别注意:
  98. - 严格避免景点重复(已使用景点:{list(used_attractions['names'])[:5]}...)
  99. - "description"、"history_significance"字段必须详细描述(15字左右)
  100. - "history_significance"应详细描述该地点在党史中的具体作用和事件
  101. - 所有字符串值必须用双引号正确闭合
  102. - 确保没有缺失的逗号或引号
  103. 城市范围:{[city.name for city in cities]}
  104. 交通方式:{preference['transport']}
  105. 特殊要求:{preference.get('custom_requirements', '无')}
  106. 返回格式示例:
  107. {{
  108. "days": [
  109. {{
  110. "day": 1,
  111. "theme": "主题",
  112. "attractions": [
  113. {{
  114. "id": "景点ID",
  115. "name": "景点名称",
  116. "address": "详细地址",
  117. "description": "这里应该提供详细的景点描述,包括建筑特色、主要展区、重要展品等。例如:井冈山革命博物馆始建于1958年,馆藏文物3万余件,珍贵文献资料和历史图片2万余份,保存毛泽东、朱德等领导人重上井冈山时的影视资料数百件。",
  118. "history_significance": "这里应该详细描述景点的历史意义。例如:井冈山是中国革命的摇篮,1927年10月,毛泽东率领秋收起义部队到达井冈山,创建了中国第一个农村革命根据地,开辟了农村包围城市、武装夺取政权的中国革命道路。",
  119. "open_hours": "08:00-17:00",
  120. "ticket_info": "门票信息",
  121. "is_red_tourism": true,
  122. "latitude": 35.1234,
  123. "longitude": 116.5678,
  124. "visit_time": "09:00-11:00",
  125. "duration": 120
  126. }}
  127. ]
  128. }}
  129. ]
  130. }}
  131. """
  132. # 调用AI接口
  133. client = OpenAI(
  134. api_key=settings.MOONSHOT_API_KEY,
  135. base_url="https://api.moonshot.cn/v1"
  136. )
  137. response = client.chat.completions.create(
  138. model="moonshot-v1-8k",
  139. messages=[
  140. {
  141. "role": "system",
  142. "content": "你是专业的红色旅游规划专家,必须严格按要求返回有效的JSON格式数据。特别注意:\n"
  143. "1. 严格避免景点重复(已使用景点:" + ", ".join(
  144. list(used_attractions['names'])[:5]) + "...)\n"
  145. "2. 提供详细的景点描述、历史意义和学习要点\n"
  146. "3. 所有字符串用双引号\n"
  147. "4. 确保所有引号正确闭合\n"
  148. "5. 不要有注释\n"
  149. "6. 确保JSON格式完整正确\n"
  150. "7. 对于红色教育基地,必须详细描述其历史背景和教育意义"
  151. },
  152. {"role": "user", "content": batch_prompt}
  153. ],
  154. response_format={"type": "json_object"},
  155. temperature=0.3,
  156. max_tokens=6000
  157. )
  158. # 获取并预处理响应内容
  159. response_content = response.choices[0].message.content.strip()
  160. logger.debug(f"原始AI响应: {response_content}")
  161. # 增强的JSON修复处理
  162. def fix_json(json_str):
  163. json_str = json_str.strip()
  164. start_idx = json_str.find('{')
  165. end_idx = json_str.rfind('}')
  166. if start_idx == -1 or end_idx == -1:
  167. raise ValueError("无效的JSON结构")
  168. json_str = json_str[start_idx:end_idx + 1]
  169. # 替换中文标点
  170. json_str = json_str.replace('"', '"').replace('"', '"')
  171. json_str = json_str.replace(''', "'").replace(''', "'")
  172. json_str = json_str.replace(',', ',').replace(':', ':')
  173. # 修复未闭合的字符串
  174. json_str = re.sub(r'(?<!\\)"(.*?)(?<!\\)"(?=\s*[:,\]}])',
  175. lambda m: '"' + m.group(1).replace('\n', ' ').replace('\r', '').replace('"',
  176. '\\"') + '"',
  177. json_str)
  178. # 修复缺失的逗号
  179. json_str = re.sub(r'("[^"]+")\s*(?=[}\]])', r'\1', json_str)
  180. # 平衡大括号
  181. open_braces = json_str.count('{')
  182. close_braces = json_str.count('}')
  183. if open_braces > close_braces:
  184. json_str += '}' * (open_braces - close_braces)
  185. return json_str
  186. # 尝试解析JSON
  187. try:
  188. batch_data = json.loads(response_content)
  189. except json.JSONDecodeError as e:
  190. logger.warning(f"首次JSON解析失败,尝试修复... 错误: {str(e)}")
  191. try:
  192. fixed_content = fix_json(response_content)
  193. batch_data = json.loads(fixed_content)
  194. logger.info("JSON修复成功")
  195. except Exception as fix_error:
  196. logger.error(f"JSON修复失败: {str(fix_error)}")
  197. logger.error(f"问题内容: {response_content[max(0, e.pos - 50):e.pos + 50]}")
  198. raise ValueError(f"无法解析AI响应,请检查响应格式。错误:{str(e)}")
  199. # 验证数据结构
  200. if not isinstance(batch_data, dict) or 'days' not in batch_data:
  201. raise ValueError("AI返回的数据结构无效,缺少'days'字段")
  202. # 处理AI响应并检查景点重复
  203. processed_data = MoonshotAIService._process_red_tourism_response(batch_data, batch_attractions)
  204. # 记录已使用的景点
  205. for day in processed_data.get('days', []):
  206. for attraction in day.get('attractions', []):
  207. if 'id' in attraction:
  208. used_attractions['ids'].add(attraction['id'])
  209. if 'name' in attraction:
  210. used_attractions['names'].add(attraction['name'])
  211. # 合并到完整行程中
  212. full_plan['days'].extend(processed_data['days'])
  213. if processed_data.get('education_goals'):
  214. full_plan['education_goals'].extend(processed_data['education_goals'])
  215. except Exception as e:
  216. logger.error(f"处理第{batch_num + 1}批行程时出错: {str(e)}", exc_info=True)
  217. if len(full_plan['days']) == 0:
  218. raise Exception(f"生成行程失败: {str(e)}")
  219. else:
  220. logger.warning(f"部分行程生成失败,已生成{len(full_plan['days'])}天")
  221. # 最终验证和去重检查
  222. if len(full_plan['days']) < total_days:
  223. logger.warning(f"生成的行程不完整,只有{len(full_plan['days'])}天/{total_days}天")
  224. full_plan['warning'] = f"行程不完整,只生成{len(full_plan['days'])}天"
  225. # 最终检查所有景点是否重复
  226. all_attractions_in_plan = []
  227. for day in full_plan['days']:
  228. for attraction in day['attractions']:
  229. all_attractions_in_plan.append((attraction.get('id'), attraction.get('name')))
  230. unique_attractions = set(all_attractions_in_plan)
  231. if len(all_attractions_in_plan) != len(unique_attractions):
  232. duplicate_count = len(all_attractions_in_plan) - len(unique_attractions)
  233. logger.warning(f"最终检查发现{duplicate_count}个重复景点")
  234. # 进行最终去重处理
  235. seen = set()
  236. for day in full_plan['days']:
  237. day['attractions'] = [
  238. att for att in day['attractions']
  239. if (att.get('id'), att.get('name')) not in seen and not seen.add((att.get('id'), att.get('name')))
  240. ]
  241. return full_plan
  242. @staticmethod
  243. def regenerate_red_tourism_plan(preference):
  244. """
  245. 重新规划红色旅游行程 - 增强版,支持长行程分批次处理
  246. """
  247. try:
  248. # 1. 获取城市和景点
  249. cities = City.objects.filter(id__in=preference['city_ids'])
  250. if not cities.exists():
  251. return {'error': '未找到指定城市'}
  252. days = preference['days']
  253. previous_plan = preference.get('previous_plan', {})
  254. # 2. 获取所有景点(一次性查询提高效率)
  255. all_attractions = list(Attraction.objects.filter(city__in=cities))
  256. # 3. 获取已使用的景点ID和名称(从原行程中)
  257. used_attractions = {
  258. 'ids': set(),
  259. 'names': set()
  260. }
  261. if previous_plan and 'days' in previous_plan:
  262. for day in previous_plan['days']:
  263. for attraction in day.get('attractions', []):
  264. if 'id' in attraction:
  265. used_attractions['ids'].add(str(attraction['id']))
  266. if 'name' in attraction:
  267. used_attractions['names'].add(attraction['name'])
  268. # 4. 根据天数决定生成方式
  269. return MoonshotAIService._regenerate_long_plan_in_batches(
  270. cities, preference, all_attractions, used_attractions
  271. )
  272. except Exception as e:
  273. logger.error(f"重新规划行程失败: {str(e)}", exc_info=True)
  274. return {'error': str(e)}
  275. @staticmethod
  276. def _regenerate_long_plan_in_batches(cities, preference, all_attractions, used_attractions):
  277. """处理2天及以上的长行程(分批重新生成),解决景点重复问题"""
  278. total_days = preference['days']
  279. batches = (total_days + MoonshotAIService.BATCH_SIZE - 1) // MoonshotAIService.BATCH_SIZE
  280. full_plan = {
  281. "title": f"{total_days}天红色文化之旅(重新规划)",
  282. "description": "通过参观革命历史遗址学习党史 - 重新规划版本",
  283. "days": [],
  284. "education_goals": []
  285. }
  286. for batch_num in range(batches):
  287. start_day = batch_num * MoonshotAIService.BATCH_SIZE + 1
  288. end_day = min((batch_num + 1) * MoonshotAIService.BATCH_SIZE, total_days)
  289. current_batch_days = end_day - start_day + 1
  290. try:
  291. # 为当前批次筛选景点(严格排除已使用的景点)
  292. batch_attractions = [
  293. att for att in all_attractions
  294. if (str(att.id) not in used_attractions['ids'] and
  295. att.name not in used_attractions['names'])
  296. ]
  297. # 确保每批有足够的红色景点
  298. red_attractions = [
  299. att for att in batch_attractions
  300. if any(tag in att.tags for tag in MoonshotAIService.RED_TOURISM_TAGS)
  301. ]
  302. # 计算需要的红色景点数(每天至少2个)
  303. needed_red_attractions = current_batch_days * 2
  304. if len(red_attractions) < needed_red_attractions:
  305. # 如果红色景点不足,从所有景点中补充
  306. extra_attractions = [
  307. att for att in batch_attractions
  308. if att not in red_attractions
  309. ]
  310. batch_attractions = red_attractions + extra_attractions[
  311. :needed_red_attractions - len(red_attractions)]
  312. else:
  313. batch_attractions = red_attractions[:needed_red_attractions] + [
  314. att for att in batch_attractions
  315. if att not in red_attractions
  316. ]
  317. # 构建当前批次的提示词 - 添加景点去重提示
  318. batch_prompt = f"""
  319. 你是一个专业的红色旅游路线规划AI助手。现在需要基于用户偏好重新规划第{start_day}天到第{end_day}天的红色旅游路线,共{current_batch_days}天。
  320. 重要要求:
  321. 1. 必须严格避免选择重复景点(已使用景点:{list(used_attractions['names'])[:10]}...)
  322. 2. 必须返回严格符合JSON格式的数据
  323. 3. 所有字符串值必须用双引号括起来
  324. 4. 确保所有JSON字段都有正确的闭合
  325. 5. 不要包含任何注释或额外文本
  326. 6. 每天必须安排1-5个景点
  327. 7. 每个景点必须与红色主题有关
  328. 8. 景点之间要考虑地理位置和交通时间
  329. 9. 确保每个JSON对象属性后面都有逗号分隔符,除了最后一个属性
  330. 10. 所有字符串值必须在一行内完成,不能换行
  331. 11. 字符串值中不能包含未转义的引号
  332. 特别注意:
  333. - 严格避免景点重复(已使用景点:{list(used_attractions['names'])[:5]}...)
  334. - 必须生成全新的景点安排,避免与原有行程重复
  335. - "description"、"history_significance"字段必须详细描述(15字左右)
  336. - "history_significance"应详细描述该地点在党史中的具体作用和事件
  337. - 所有字符串值必须用双引号正确闭合
  338. - 确保没有缺失的逗号或引号
  339. 城市范围:{[city.name for city in cities]}
  340. 交通方式:{preference['transport']}
  341. 特殊要求:{preference.get('custom_requirements', '无')}
  342. 返回格式示例:
  343. {{
  344. "days": [
  345. {{
  346. "day": {start_day},
  347. "theme": "新主题",
  348. "attractions": [
  349. {{
  350. "id": "新景点ID",
  351. "name": "新景点名称",
  352. "address": "详细地址",
  353. "description": "这里应该提供详细的景点描述,包括建筑特色、主要展区、重要展品等。例如:井冈山革命博物馆始建于1958年,馆藏文物3万余件,珍贵文献资料和历史图片2万余份,保存毛泽东、朱德等领导人重上井冈山时的影视资料数百件。",
  354. "history_significance": "这里应该详细描述景点的历史意义。例如:井冈山是中国革命的摇篮,1927年10月,毛泽东率领秋收起义部队到达井冈山,创建了中国第一个农村革命根据地,开辟了农村包围城市、武装夺取政权的中国革命道路。",
  355. "open_hours": "08:00-17:00",
  356. "ticket_info": "门票信息",
  357. "is_red_tourism": true,
  358. "latitude": 35.1234,
  359. "longitude": 116.5678,
  360. "visit_time": "09:00-11:00",
  361. "duration": 120
  362. }}
  363. ]
  364. }}
  365. ]
  366. }}
  367. """
  368. # 调用AI接口
  369. client = OpenAI(
  370. api_key=settings.MOONSHOT_API_KEY,
  371. base_url="https://api.moonshot.cn/v1"
  372. )
  373. response = client.chat.completions.create(
  374. model="moonshot-v1-8k",
  375. messages=[
  376. {
  377. "role": "system",
  378. "content": "你是专业的红色旅游规划专家,正在重新规划行程。必须严格按要求返回有效的JSON格式数据。特别注意:\n"
  379. "1. 严格避免景点重复(已使用景点:" + ", ".join(
  380. list(used_attractions['names'])[:5]) + "...)\n"
  381. "2. 必须生成全新的景点安排\n"
  382. "3. 提供详细的景点描述、历史意义和学习要点\n"
  383. "4. 所有字符串用双引号\n"
  384. "5. 确保所有引号正确闭合\n"
  385. "6. 不要有注释\n"
  386. "7. 确保JSON格式完整正确\n"
  387. "8. 对于红色教育基地,必须详细描述其历史背景和教育意义"
  388. },
  389. {"role": "user", "content": batch_prompt}
  390. ],
  391. response_format={"type": "json_object"},
  392. temperature=0.4, # 稍高的temperature以获得更多变化
  393. max_tokens=6000
  394. )
  395. # 获取并预处理响应内容
  396. response_content = response.choices[0].message.content.strip()
  397. logger.debug(f"重新规划原始AI响应: {response_content}")
  398. # 尝试解析JSON
  399. try:
  400. batch_data = json.loads(response_content)
  401. except json.JSONDecodeError as e:
  402. logger.warning(f"重新规划JSON解析失败,尝试修复... 错误: {str(e)}")
  403. try:
  404. fixed_content = MoonshotAIService._fix_json(response_content)
  405. batch_data = json.loads(fixed_content)
  406. logger.info("JSON修复成功")
  407. except Exception as fix_error:
  408. logger.error(f"JSON修复失败: {str(fix_error)}")
  409. raise ValueError(f"无法解析AI响应,请检查响应格式。错误:{str(e)}")
  410. # 验证数据结构
  411. if not isinstance(batch_data, dict) or 'days' not in batch_data:
  412. raise ValueError("AI返回的数据结构无效,缺少'days'字段")
  413. # 处理AI响应并检查景点重复
  414. processed_data = MoonshotAIService._process_red_tourism_response(batch_data, batch_attractions)
  415. # 检查并处理重复景点
  416. duplicate_attractions = []
  417. for day in processed_data.get('days', []):
  418. for attraction in day.get('attractions', []):
  419. if ('id' in attraction and attraction['id'] in used_attractions['ids']) or \
  420. ('name' in attraction and attraction['name'] in used_attractions['names']):
  421. duplicate_attractions.append(attraction.get('name', '未知景点'))
  422. # 记录已使用的景点
  423. for day in processed_data.get('days', []):
  424. for attraction in day.get('attractions', []):
  425. if 'id' in attraction:
  426. used_attractions['ids'].add(attraction['id'])
  427. if 'name' in attraction:
  428. used_attractions['names'].add(attraction['name'])
  429. # 合并到完整行程中
  430. full_plan['days'].extend(processed_data['days'])
  431. if processed_data.get('education_goals'):
  432. full_plan['education_goals'].extend(processed_data['education_goals'])
  433. except Exception as e:
  434. logger.error(f"处理第{batch_num + 1}批重新规划行程时出错: {str(e)}", exc_info=True)
  435. if len(full_plan['days']) == 0:
  436. raise Exception(f"重新生成行程失败: {str(e)}")
  437. else:
  438. logger.warning(f"部分行程重新生成失败,已生成{len(full_plan['days'])}天")
  439. # 最终验证和去重检查
  440. if len(full_plan['days']) < total_days:
  441. logger.warning(f"重新生成的行程不完整,只有{len(full_plan['days'])}天/{total_days}天")
  442. full_plan['warning'] = f"行程不完整,只生成{len(full_plan['days'])}天"
  443. # 最终检查所有景点是否重复
  444. all_attractions_in_plan = []
  445. for day in full_plan['days']:
  446. for attraction in day['attractions']:
  447. all_attractions_in_plan.append((attraction.get('id'), attraction.get('name')))
  448. unique_attractions = set(all_attractions_in_plan)
  449. if len(all_attractions_in_plan) != len(unique_attractions):
  450. duplicate_count = len(all_attractions_in_plan) - len(unique_attractions)
  451. logger.warning(f"最终检查发现{duplicate_count}个重复景点")
  452. # 进行最终去重处理
  453. seen = set()
  454. for day in full_plan['days']:
  455. day['attractions'] = [
  456. att for att in day['attractions']
  457. if (att.get('id'), att.get('name')) not in seen and not seen.add((att.get('id'), att.get('name')))
  458. ]
  459. return full_plan
  460. @staticmethod
  461. def _process_red_tourism_response(plan_data, attractions):
  462. """处理AI响应数据,补充完整景点信息"""
  463. attraction_map = {str(att.id): att for att in attractions}
  464. for day in plan_data.get('days', []):
  465. for attraction in day.get('attractions', []):
  466. # 从数据库获取完整景点信息
  467. db_attraction = attraction_map.get(str(attraction.get('id')))
  468. if db_attraction:
  469. # 补充详细信息
  470. attraction.update({
  471. 'address': getattr(db_attraction, 'address', '地址信息待补充'),
  472. 'description': getattr(db_attraction, 'description', '红色教育基地,具有重要的历史教育意义'),
  473. 'history_significance': getattr(db_attraction, 'history_significance', '革命历史重要遗址'),
  474. 'open_hours': getattr(db_attraction, 'open_hours', '09:00-17:00'),
  475. 'ticket_info': getattr(db_attraction, 'ticket_info', '凭身份证免费参观'),
  476. 'image': db_attraction.image.url if hasattr(db_attraction,
  477. 'image') and db_attraction.image else '/static/images/default-red.jpg'
  478. })
  479. else:
  480. # 为AI生成的景点提供默认值
  481. attraction.update({
  482. 'address': attraction.get('address', '地址信息待补充'),
  483. 'description': attraction.get('description', '红色教育基地,具有重要的历史教育意义'),
  484. 'history_significance': attraction.get('history_significance', '革命历史重要遗址'),
  485. 'open_hours': attraction.get('open_hours', '09:00-17:00'),
  486. 'ticket_info': attraction.get('ticket_info', '凭身份证免费参观'),
  487. 'image': attraction.get('image', '/static/images/default-red.jpg')
  488. })
  489. return plan_data