Browse Source

“第一次”

zzz 4 months ago
parent
commit
bd7af9c4fa
100 changed files with 3056 additions and 0 deletions
  1. 8 0
      houduan/.idea/.gitignore
  2. 28 0
      houduan/.idea/houduan.iml
  3. 13 0
      houduan/.idea/inspectionProfiles/Project_Default.xml
  4. 6 0
      houduan/.idea/inspectionProfiles/profiles_settings.xml
  5. 7 0
      houduan/.idea/misc.xml
  6. 8 0
      houduan/.idea/modules.xml
  7. 4 0
      houduan/.idea/vcs.xml
  8. 0 0
      houduan/ai_planner/__init__.py
  9. BIN
      houduan/ai_planner/__pycache__/__init__.cpython-311.pyc
  10. BIN
      houduan/ai_planner/__pycache__/__init__.cpython-312.pyc
  11. BIN
      houduan/ai_planner/__pycache__/admin.cpython-311.pyc
  12. BIN
      houduan/ai_planner/__pycache__/admin.cpython-312.pyc
  13. BIN
      houduan/ai_planner/__pycache__/apps.cpython-311.pyc
  14. BIN
      houduan/ai_planner/__pycache__/apps.cpython-312.pyc
  15. BIN
      houduan/ai_planner/__pycache__/models.cpython-311.pyc
  16. BIN
      houduan/ai_planner/__pycache__/models.cpython-312.pyc
  17. BIN
      houduan/ai_planner/__pycache__/serializers.cpython-311.pyc
  18. BIN
      houduan/ai_planner/__pycache__/serializers.cpython-312.pyc
  19. BIN
      houduan/ai_planner/__pycache__/services.cpython-311.pyc
  20. BIN
      houduan/ai_planner/__pycache__/services.cpython-312.pyc
  21. BIN
      houduan/ai_planner/__pycache__/urls.cpython-311.pyc
  22. BIN
      houduan/ai_planner/__pycache__/urls.cpython-312.pyc
  23. BIN
      houduan/ai_planner/__pycache__/views.cpython-311.pyc
  24. BIN
      houduan/ai_planner/__pycache__/views.cpython-312.pyc
  25. 3 0
      houduan/ai_planner/admin.py
  26. 6 0
      houduan/ai_planner/apps.py
  27. 0 0
      houduan/ai_planner/management/__init__.py
  28. BIN
      houduan/ai_planner/management/__pycache__/__init__.cpython-311.pyc
  29. 0 0
      houduan/ai_planner/management/commands/__init__.py
  30. BIN
      houduan/ai_planner/management/commands/__pycache__/__init__.cpython-311.pyc
  31. BIN
      houduan/ai_planner/management/commands/__pycache__/import_red_spots.cpython-311.pyc
  32. BIN
      houduan/ai_planner/management/commands/__pycache__/seed_shandong_cities.cpython-311.pyc
  33. 518 0
      houduan/ai_planner/management/commands/import_red_spots.py
  34. 118 0
      houduan/ai_planner/management/commands/seed_shandong_cities.py
  35. 136 0
      houduan/ai_planner/migrations/0001_initial.py
  36. 18 0
      houduan/ai_planner/migrations/0002_attraction_history.py
  37. 44 0
      houduan/ai_planner/migrations/0003_attraction_latitude_attraction_longitude_and_more.py
  38. 0 0
      houduan/ai_planner/migrations/__init__.py
  39. BIN
      houduan/ai_planner/migrations/__pycache__/0001_initial.cpython-311.pyc
  40. BIN
      houduan/ai_planner/migrations/__pycache__/0001_initial.cpython-312.pyc
  41. BIN
      houduan/ai_planner/migrations/__pycache__/0002_attraction_history.cpython-311.pyc
  42. BIN
      houduan/ai_planner/migrations/__pycache__/0002_attraction_history.cpython-312.pyc
  43. BIN
      houduan/ai_planner/migrations/__pycache__/0003_attraction_latitude_attraction_longitude_and_more.cpython-311.pyc
  44. BIN
      houduan/ai_planner/migrations/__pycache__/0003_attraction_latitude_attraction_longitude_and_more.cpython-312.pyc
  45. BIN
      houduan/ai_planner/migrations/__pycache__/__init__.cpython-311.pyc
  46. BIN
      houduan/ai_planner/migrations/__pycache__/__init__.cpython-312.pyc
  47. 130 0
      houduan/ai_planner/models.py
  48. 128 0
      houduan/ai_planner/serializers.py
  49. 1012 0
      houduan/ai_planner/services.py
  50. 3 0
      houduan/ai_planner/tests.py
  51. 23 0
      houduan/ai_planner/urls.py
  52. 597 0
      houduan/ai_planner/views.py
  53. 0 0
      houduan/api/__init__.py
  54. BIN
      houduan/api/__pycache__/__init__.cpython-311.pyc
  55. BIN
      houduan/api/__pycache__/__init__.cpython-312.pyc
  56. BIN
      houduan/api/__pycache__/admin.cpython-311.pyc
  57. BIN
      houduan/api/__pycache__/admin.cpython-312.pyc
  58. BIN
      houduan/api/__pycache__/apps.cpython-311.pyc
  59. BIN
      houduan/api/__pycache__/apps.cpython-312.pyc
  60. BIN
      houduan/api/__pycache__/models.cpython-311.pyc
  61. BIN
      houduan/api/__pycache__/models.cpython-312.pyc
  62. BIN
      houduan/api/__pycache__/urls.cpython-311.pyc
  63. BIN
      houduan/api/__pycache__/urls.cpython-312.pyc
  64. BIN
      houduan/api/__pycache__/views.cpython-311.pyc
  65. BIN
      houduan/api/__pycache__/views.cpython-312.pyc
  66. 3 0
      houduan/api/admin.py
  67. 6 0
      houduan/api/apps.py
  68. 36 0
      houduan/api/migrations/0001_initial.py
  69. 51 0
      houduan/api/migrations/0002_alter_userinfo_options_userplan.py
  70. 19 0
      houduan/api/migrations/0003_userplan_spot_img.py
  71. 0 0
      houduan/api/migrations/__init__.py
  72. BIN
      houduan/api/migrations/__pycache__/0001_initial.cpython-311.pyc
  73. BIN
      houduan/api/migrations/__pycache__/0001_initial.cpython-312.pyc
  74. BIN
      houduan/api/migrations/__pycache__/0002_alter_userinfo_options_userplan.cpython-311.pyc
  75. BIN
      houduan/api/migrations/__pycache__/0002_alter_userinfo_options_userplan.cpython-312.pyc
  76. BIN
      houduan/api/migrations/__pycache__/0003_userplan_spot_img.cpython-311.pyc
  77. BIN
      houduan/api/migrations/__pycache__/0003_userplan_spot_img.cpython-312.pyc
  78. BIN
      houduan/api/migrations/__pycache__/__init__.cpython-311.pyc
  79. BIN
      houduan/api/migrations/__pycache__/__init__.cpython-312.pyc
  80. 37 0
      houduan/api/models.py
  81. 3 0
      houduan/api/tests.py
  82. 7 0
      houduan/api/urls.py
  83. 75 0
      houduan/api/views.py
  84. 0 0
      houduan/api01/__init__.py
  85. BIN
      houduan/api01/__pycache__/__init__.cpython-311.pyc
  86. BIN
      houduan/api01/__pycache__/__init__.cpython-312.pyc
  87. BIN
      houduan/api01/__pycache__/admin.cpython-311.pyc
  88. BIN
      houduan/api01/__pycache__/admin.cpython-312.pyc
  89. BIN
      houduan/api01/__pycache__/apps.cpython-311.pyc
  90. BIN
      houduan/api01/__pycache__/apps.cpython-312.pyc
  91. BIN
      houduan/api01/__pycache__/models.cpython-311.pyc
  92. BIN
      houduan/api01/__pycache__/models.cpython-312.pyc
  93. BIN
      houduan/api01/__pycache__/urls.cpython-311.pyc
  94. BIN
      houduan/api01/__pycache__/views.cpython-311.pyc
  95. BIN
      houduan/api01/__pycache__/views.cpython-312.pyc
  96. 3 0
      houduan/api01/admin.py
  97. 6 0
      houduan/api01/apps.py
  98. 0 0
      houduan/api01/migrations/__init__.py
  99. BIN
      houduan/api01/migrations/__pycache__/__init__.cpython-311.pyc
  100. BIN
      houduan/api01/migrations/__pycache__/__init__.cpython-312.pyc

+ 8 - 0
houduan/.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 28 - 0
houduan/.idea/houduan.iml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="django" name="Django">
+      <configuration>
+        <option name="rootFolder" value="$MODULE_DIR$" />
+        <option name="settingsModule" value="houduan/settings.py" />
+        <option name="manageScript" value="$MODULE_DIR$/manage.py" />
+        <option name="environment" value="&lt;map/&gt;" />
+        <option name="doNotUseTestRunner" value="false" />
+        <option name="trackFilePattern" value="migrations" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="jdk" jdkName="Python 3.12 (3)" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="TemplatesService">
+    <option name="TEMPLATE_CONFIGURATION" value="Django" />
+    <option name="TEMPLATE_FOLDERS">
+      <list>
+        <option value="$MODULE_DIR$/../houduan\templates" />
+      </list>
+    </option>
+  </component>
+</module>

+ 13 - 0
houduan/.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,13 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" />
+    <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="W292" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
houduan/.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 7 - 0
houduan/.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.12 (2)" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (3)" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
houduan/.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/houduan.iml" filepath="$PROJECT_DIR$/.idea/houduan.iml" />
+    </modules>
+  </component>
+</project>

+ 4 - 0
houduan/.idea/vcs.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings" defaultProject="true" />
+</project>

+ 0 - 0
houduan/ai_planner/__init__.py


BIN
houduan/ai_planner/__pycache__/__init__.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/__init__.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/admin.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/admin.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/apps.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/apps.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/models.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/models.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/serializers.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/serializers.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/services.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/services.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/urls.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/urls.cpython-312.pyc


BIN
houduan/ai_planner/__pycache__/views.cpython-311.pyc


BIN
houduan/ai_planner/__pycache__/views.cpython-312.pyc


+ 3 - 0
houduan/ai_planner/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
houduan/ai_planner/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AiPlannerConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'ai_planner'

+ 0 - 0
houduan/ai_planner/management/__init__.py


BIN
houduan/ai_planner/management/__pycache__/__init__.cpython-311.pyc


+ 0 - 0
houduan/ai_planner/management/commands/__init__.py


BIN
houduan/ai_planner/management/commands/__pycache__/__init__.cpython-311.pyc


BIN
houduan/ai_planner/management/commands/__pycache__/import_red_spots.cpython-311.pyc


BIN
houduan/ai_planner/management/commands/__pycache__/seed_shandong_cities.cpython-311.pyc


+ 518 - 0
houduan/ai_planner/management/commands/import_red_spots.py

@@ -0,0 +1,518 @@
+from django.core.management.base import BaseCommand
+from ai_planner.models import Attraction, City
+import os
+from django.core.files import File
+
+
+class Command(BaseCommand):
+    help = '导入所有红色旅游景点数据到Attraction表(含图片)'
+
+    def handle(self, *args, **options):
+        # 处理门票价格转换
+        def parse_ticket_price(ticket_info):
+            if "免费" in ticket_info:
+                return 0
+            try:
+                # 提取数字部分
+                price_str = ''.join(filter(str.isdigit, ticket_info))
+                return float(price_str) if price_str else 0
+            except:
+                return 0
+
+        # 更健壮的城市名称提取方法
+        def get_city_name(location):
+            shandong_cities = [
+                '济南市', '青岛市', '淄博市', '枣庄市', '东营市',
+                '烟台市', '潍坊市', '济宁市', '泰安市', '威海市',
+                '日照市', '临沂市', '德州市', '聊城市', '滨州市',
+                '菏泽市'
+            ]
+            for city in shandong_cities:
+                if city in location:
+                    return city
+            return '济南市'
+
+        # 图片存储基础路径(根据实际路径修改)
+        IMAGE_BASE_PATH = r"G:\其他\jingdian\image+name"
+
+        # 所有红色景点数据(保持不变)...
+        red_spots_data = [
+            {
+                "name": "台儿庄大战纪念馆",
+                "category": "memorial",
+                "location": "山东省枣庄市台儿庄区沿河南路6号",
+                "description": "台儿庄大战纪念馆是为纪念1938年台儿庄战役而建,展示了大量珍贵的历史照片和文物。",
+                "history": "台儿庄战役是抗日战争初期中国军队在山东省南部台儿庄地区抗击日军的重大胜利,是中国军队在正面战场取得的首次重大胜利。",
+                "open_time": "全年开放 8:30-17:30",
+                "ticket_info": "免费开放",
+                "short_desc": "纪念1938年台儿庄战役的专题纪念馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "孟良崮战役遗址",
+                "category": "revolution",
+                "location": "山东省临沂市蒙阴县垛庄镇",
+                "description": "孟良崮战役遗址是解放战争时期著名的孟良崮战役发生地,现建有纪念馆和烈士陵园。",
+                "history": "1947年5月,华东野战军在陈毅、粟裕指挥下,全歼国民党军整编第74师,击毙师长张灵甫,是解放战争中的重要转折点。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票50元",
+                "short_desc": "解放战争重要战役遗址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "华东革命烈士陵园",
+                "category": "martyrs",
+                "location": "山东省临沂市兰山区陵园前街4号",
+                "description": "华东革命烈士陵园是为纪念在抗日战争和解放战争中牺牲的华东地区革命烈士而建。",
+                "history": "建于1949年,安葬着包括罗炳辉、王麓水等著名烈士在内的6万余名革命烈士。",
+                "open_time": "8:00-17:00",
+                "ticket_info": "免费开放",
+                "short_desc": "安葬6万余名革命烈士的陵园",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "沂蒙革命纪念馆",
+                "category": "museum",
+                "location": "山东省临沂市兰山区银雀山路与沂蒙路交汇处",
+                "description": "沂蒙革命纪念馆全面展示了沂蒙革命老区的光荣历史和沂蒙精神的形成发展过程。",
+                "history": "沂蒙山区是抗日战争和解放战争时期重要的革命根据地之一,涌现出'红嫂'等众多感人事迹。",
+                "open_time": "周二至周日 9:00-16:30",
+                "ticket_info": "免费开放",
+                "short_desc": "展示沂蒙革命老区历史的专题纪念馆",
+                "tags": ["museum", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "铁道游击队纪念园",
+                "category": "memorial",
+                "location": "山东省枣庄市薛城区临山路",
+                "description": "铁道游击队纪念园是为纪念抗日战争时期活跃在鲁南地区的铁道游击队而建。",
+                "history": "铁道游击队是抗日战争时期鲁南地区著名的抗日武装,以扒火车、打游击著称,小说《铁道游击队》即以此为原型。",
+                "open_time": "8:30-17:30",
+                "ticket_info": "门票30元",
+                "short_desc": "纪念抗日战争时期铁道游击队的专题园区",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "胶东革命纪念馆",
+                "category": "museum",
+                "location": "山东省烟台市芝罘区南大街61号",
+                "description": "胶东革命纪念馆展示了胶东地区党组织创建、发展和领导人民进行革命斗争的历史。",
+                "history": "胶东地区是山东党组织活动较早的地区之一,在抗日战争和解放战争中做出了重要贡献。",
+                "open_time": "周二至周日 9:00-16:30",
+                "ticket_info": "免费开放",
+                "short_desc": "展示胶东地区革命历史的专题纪念馆",
+                "tags": ["museum", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "沂蒙山小调诞生地",
+                "category": "revolution",
+                "location": "山东省临沂市费县薛庄镇",
+                "description": "《沂蒙山小调》诞生地是这首著名革命歌曲的创作地,现建有纪念设施。",
+                "history": "1940年,抗大一分校文工团在此创作了《沂蒙山小调》,后成为歌颂沂蒙革命精神的代表性歌曲。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票20元",
+                "short_desc": "著名革命歌曲《沂蒙山小调》的创作地",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "鲁南人民抗日武装起义纪念馆",
+                "category": "memorial",
+                "location": "山东省枣庄市山亭区北庄镇",
+                "description": "纪念馆展示了1938年鲁南人民抗日武装起义的历史和意义。",
+                "history": "1938年5月,中共苏鲁豫皖边区特委领导发动鲁南人民抗日武装起义,创建了鲁南抗日根据地。",
+                "open_time": "周二至周日 9:00-16:30",
+                "ticket_info": "免费开放",
+                "short_desc": "纪念1938年鲁南人民抗日武装起义的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "大青山突围战遗址",
+                "category": "revolution",
+                "location": "山东省临沂市费县薛庄镇",
+                "description": "大青山突围战遗址是抗日战争时期著名的突围战发生地,现建有纪念碑和纪念馆。",
+                "history": "1941年11月,抗大一分校及中共山东分局、省战工会等机关在此成功突围,粉碎了日军的'铁壁合围'",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票30元",
+                "short_desc": "抗日战争时期著名突围战遗址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "胶东抗日烈士陵园",
+                "category": "martyrs",
+                "location": "山东省烟台市牟平区龙泉镇",
+                "description": "胶东抗日烈士陵园是为纪念在胶东抗日斗争中牺牲的烈士而建。",
+                "history": "建于1945年,安葬着包括理琪、林江等著名烈士在内的数千名抗日烈士。",
+                "open_time": "8:00-17:00",
+                "ticket_info": "免费开放",
+                "short_desc": "纪念胶东抗日斗争中牺牲烈士的陵园",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "八路军115师司令部旧址",
+                "category": "revolution",
+                "location": "山东省临沂市莒南县大店镇",
+                "description": "八路军115师司令部旧址是抗日战争时期115师司令部的驻地,现为全国重点文物保护单位。",
+                "history": "1941年3月至1945年9月,八路军115师司令部驻扎于此,罗荣桓等曾在此办公。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票40元",
+                "short_desc": "抗日战争时期八路军115师司令部驻地",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "红嫂纪念馆",
+                "category": "memorial",
+                "location": "山东省临沂市沂南县马牧池乡",
+                "description": "红嫂纪念馆是为纪念沂蒙红嫂明德英等革命妇女而建,展示了红嫂们的感人事迹。",
+                "history": "抗日战争时期,沂蒙山区涌现出许多像明德英这样的'红嫂',她们用乳汁救治伤员,谱写了军民鱼水情的感人篇章。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票30元",
+                "short_desc": "纪念沂蒙红嫂感人事迹的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "山东省政府旧址",
+                "category": "revolution",
+                "location": "山东省临沂市莒南县大店镇",
+                "description": "山东省政府旧址是抗日战争时期山东省政府所在地,现为全国重点文物保护单位。",
+                "history": "1945年8月13日,山东省政府在此成立,是中国共产党领导的第一个省级人民政府。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票30元",
+                "short_desc": "中国共产党领导的第一个省级政府旧址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "徂徕山抗日武装起义旧址",
+                "category": "revolution",
+                "location": "山东省泰安市岱岳区徂徕镇",
+                "description": "徂徕山抗日武装起义旧址是1938年1月徂徕山抗日武装起义的发生地。",
+                "history": "1938年1月1日,中共山东省委在此领导发动徂徕山抗日武装起义,打响了山东省委独立领导山东抗战的第一枪。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票20元",
+                "short_desc": "1938年徂徕山抗日武装起义发生地",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "黑铁山抗日武装起义旧址",
+                "category": "revolution",
+                "location": "山东省淄博市张店区卫固镇",
+                "description": "黑铁山抗日武装起义旧址是1937年12月黑铁山抗日武装起义的发生地。",
+                "history": "1937年12月26日,中共山东省委在此领导发动黑铁山抗日武装起义,是山东最早的抗日武装起义之一。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票20元",
+                "short_desc": "1937年黑铁山抗日武装起义发生地",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "天福山起义纪念馆",
+                "category": "memorial",
+                "location": "山东省威海市文登区文登营镇",
+                "description": "天福山起义纪念馆是为纪念1937年12月天福山起义而建。",
+                "history": "1937年12月24日,中共胶东特委在此领导发动天福山起义,创建了山东人民抗日救国军第三军。",
+                "open_time": "周二至周日 9:00-16:30",
+                "ticket_info": "免费开放",
+                "short_desc": "纪念1937年天福山起义的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "马石山十勇士纪念馆",
+                "category": "memorial",
+                "location": "山东省威海市乳山市马石山",
+                "description": "马石山十勇士纪念馆是为纪念在1942年反'扫荡'中英勇牺牲的十位八路军战士而建。",
+                "history": "1942年冬,十位八路军战士为掩护群众突围,与日军激战后全部壮烈牺牲,谱写了气壮山河的英雄赞歌。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "免费开放",
+                "short_desc": "纪念1942年十位英勇牺牲的八路军战士",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "沂蒙革命根据地旧址群",
+                "category": "revolution",
+                "location": "山东省临沂市沂南县",
+                "description": "沂蒙革命根据地旧址群包括中共山东分局、八路军115师司令部、山东省战工会等多处革命旧址。",
+                "history": "抗日战争和解放战争时期,沂蒙山区是山东党政军领导机关所在地,被誉为'山东的小延安'",
+                "open_time": "8:30-17:00",
+                "ticket_info": "联票60元",
+                "short_desc": "包含多处重要革命旧址的遗址群",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "济南战役纪念馆",
+                "category": "memorial",
+                "location": "济南市市中区英雄山路18号",
+                "description": "全国爱国主义教育示范基地,全景展示济南战役历史进程,含全景画馆、文物陈列厅等。",
+                "history": "1948年9月16-24日,华东野战军经8昼夜激战攻克济南,揭开战略决战序幕。纪念馆2003年正式开放。",
+                "open_time": "周二至周日 9:00-16:30(周一闭馆)",
+                "ticket_info": "凭身份证免费参观",
+                "short_desc": "全景展示济南战役历史的纪念馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "解放阁",
+                "category": "memorial",
+                "location": "济南市历下区黑虎泉北路",
+                "description": "济南战役突破城垣遗址上建立的纪念性建筑,登阁可俯瞰护城河及老城区。",
+                "history": "原为济南城墙东南角,1948年解放军由此突破。1965年建阁,1986年改建为战役纪念建筑。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "免费开放",
+                "short_desc": "济南战役突破城垣遗址纪念建筑",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "英雄山革命烈士陵园",
+                "category": "martyrs",
+                "location": "济南市市中区英雄山路18号",
+                "description": "安葬着1502名革命烈士,包括山东早期中共领导人刘谦初等,建有烈士纪念塔和悼念广场。",
+                "history": "始建于1949年,1968年毛泽东题词'革命烈士纪念塔'。是济南规模最大的烈士陵园。",
+                "open_time": "全天开放",
+                "ticket_info": "免费",
+                "short_desc": "济南规模最大的革命烈士陵园",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "五三惨案纪念园",
+                "category": "memorial",
+                "location": "济南市趵突泉公园内",
+                "description": "含'五三'纪念碑、纪念亭和蔡公时纪念馆,纪念1928年日军制造的'五三惨案'。",
+                "history": "1928年5月3日日军杀害中国外交官蔡公时及军民6000余人,此遗址为惨案见证地。",
+                "open_time": "8:00-18:00",
+                "ticket_info": "含在趵突泉门票内(40元)",
+                "short_desc": "纪念1928年'五三惨案'的遗址",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "大峰山革命遗址",
+                "category": "revolution",
+                "location": "济南市长清区孝里镇",
+                "description": "含中共长清县委旧址、八路军山东纵队第六支队成立旧址等革命遗迹群。",
+                "history": "抗战时期长清县革命活动中心,被誉为'长清的小延安',现存石屋、地道等遗址。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "联票50元",
+                "short_desc": "抗战时期长清县革命活动中心遗址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "莱芜战役纪念馆",
+                "category": "memorial",
+                "location": "济南市莱芜区英雄北路",
+                "description": "全景展示1947年莱芜战役的专题纪念馆,含展览馆、全景画馆和烈士纪念碑。",
+                "history": "1947年2月华东野战军歼灭国民党军5.6万余人,创运动战典范。纪念馆1997年建成。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "免费",
+                "short_desc": "展示1947年莱芜战役的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "辛锐纪念馆",
+                "category": "memorial",
+                "location": "济南市章丘区刁镇",
+                "description": "纪念革命烈士辛锐的专题馆,展示其书画作品、生平事迹及抗战文物。",
+                "history": "辛锐(1918-1941)为著名革命艺术家,在沂蒙山区反扫荡中壮烈牺牲。2014年在其家乡建馆。",
+                "open_time": "9:00-16:30",
+                "ticket_info": "免费",
+                "short_desc": "纪念革命艺术家辛锐的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "历城革命烈士陵园",
+                "category": "martyrs",
+                "location": "济南市历城区英雄山路",
+                "description": "安葬历城战役等各时期烈士1276名,建有22米高革命烈士纪念碑。",
+                "history": "始建于1955年,2014年改造升级,集中展现历城区革命斗争史。",
+                "open_time": "8:00-17:00",
+                "ticket_info": "免费",
+                "short_desc": "安葬1276名革命烈士的陵园",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "济南市党史陈列馆",
+                "category": "museum",
+                "location": "济南市历下区经十路21028号",
+                "description": "系统展示济南党组织1921年成立以来的发展历程,含珍贵党史档案500余件。",
+                "history": "2011年建党90周年时建成开放,2021年完成展陈升级,包括'齐鲁曙光''浴血奋战'等六大展区。",
+                "open_time": "周二至周六 9:00-11:30,13:30-17:00",
+                "ticket_info": "凭身份证免费参观",
+                "short_desc": "展示济南党组织发展历程的专题馆",
+                "tags": ["museum", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "泺口九烈士纪念碑",
+                "category": "martyrs",
+                "location": "济南市天桥区泺口街道黄河大坝南侧",
+                "description": "纪念1933年8月被国民党杀害的9名中共山东地下党领导人,建有12米高纪念碑。",
+                "history": "1933年山东省委遭破坏,李春亭、段亦民等9人在此英勇就义,1989年立碑纪念。",
+                "open_time": "全天开放",
+                "ticket_info": "免费",
+                "short_desc": "纪念1933年牺牲的9名中共地下党领导人",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "平阴县委旧址纪念馆",
+                "category": "revolution",
+                "location": "济南市平阴县孔村镇",
+                "description": "抗日战争时期平阴县委驻地,复原了县委办公室、地道等历史场景。",
+                "history": "1939-1942年间中共平阴县委在此领导抗日斗争,现存石砌民居6间及抗战时期地道遗址。",
+                "open_time": "8:30-17:00(周一闭馆)",
+                "ticket_info": "团体预约制",
+                "short_desc": "抗战时期平阴县委驻地旧址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "历城抗日纪念馆",
+                "category": "memorial",
+                "location": "济南市历城区西营镇",
+                "description": "依托大南营战斗遗址建设,展示历城军民1937-1945年抗日斗争史实。",
+                "history": "1944年八路军在此歼灭日军一个小队,现存战场遗址和22处抗战时期石刻标语。",
+                "open_time": "9:00-16:30(冬季至16:00)",
+                "ticket_info": "免费",
+                "short_desc": "展示历城军民抗日斗争史的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "商河县革命烈士陵园",
+                "category": "martyrs",
+                "location": "济南市商河县青年路东首",
+                "description": "安葬抗日战争、解放战争时期烈士863名,中央矗立19.49米高纪念碑。",
+                "history": "始建于1955年,2015年改建,园内'商河革命历史展馆'详载本县重大战役史料。",
+                "open_time": "8:00-18:00",
+                "ticket_info": "免费",
+                "short_desc": "安葬863名革命烈士的陵园",
+                "tags": ["martyrs", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "钢城区莱芜战役指挥所旧址",
+                "category": "revolution",
+                "location": "济南市钢城区辛庄镇石湾子村",
+                "description": "陈毅、粟裕在此指挥莱芜战役的四合院旧址,保留作战地图等历史场景。",
+                "history": "1947年2月华东野战军前线指挥所设此,现为省级文保单位,院内保留百年古梨树。",
+                "open_time": "8:30-17:00",
+                "ticket_info": "门票15元",
+                "short_desc": "莱芜战役前线指挥所旧址",
+                "tags": ["revolution", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "济南乡师党史陈列室",
+                "category": "education",
+                "location": "济南市天桥区明湖西路",
+                "description": "山东省立乡村师范学校旧址,山东党组织重要活动地,现存'红色乡师'专题展。",
+                "history": "1932-1937年间该校培养200余名党员,赵健民、姚仲明等曾在此开展地下工作。",
+                "open_time": "预约开放",
+                "ticket_info": "免费",
+                "short_desc": "山东党组织重要活动地旧址",
+                "tags": ["education", "红色旅游"],
+                "is_featured": True
+            },
+            {
+                "name": "章丘第一支抗日武装纪念馆",
+                "category": "memorial",
+                "location": "济南市章丘区普集街道三山峪村",
+                "description": "展示1937年成立的章丘人民抗日救国军斗争史,含武器、文件等实物展品。",
+                "history": "1937年12月李曼村等在此建立章丘首支抗日武装,后编入八路军山东纵队。",
+                "open_time": "9:00-16:30",
+                "ticket_info": "免费",
+                "short_desc": "展示章丘首支抗日武装斗争史的专题馆",
+                "tags": ["memorial", "红色旅游"],
+                "is_featured": True
+            }
+        ]
+
+        created_count = 0
+        skipped_count = 0
+        image_added_count = 0
+
+        for spot_data in red_spots_data:
+            try:
+                # 获取或创建城市
+                city_name = get_city_name(spot_data['location'])
+                city, _ = City.objects.get_or_create(
+                    name=city_name,
+                    defaults={
+                        'code': city_name[:2].lower(),
+                        'description': f"{city_name}红色旅游城市",
+                        'is_hot': city_name in ['济南市', '青岛市', '烟台市']
+                    }
+                )
+
+                # 处理门票价格
+                ticket_price = parse_ticket_price(spot_data['ticket_info'])
+
+                # 创建或获取景点
+                attraction, created = Attraction.objects.get_or_create(
+                    name=spot_data['name'],
+                    city=city,
+                    defaults={
+                        'description': spot_data['description'],
+                        'short_desc': spot_data['short_desc'],
+                        'address': spot_data['location'],
+                        'ticket_price': ticket_price,
+                        'open_hours': spot_data['open_time'],
+                        'tags': spot_data['tags'],
+                        'history': spot_data['history'],
+                        'is_featured': spot_data['is_featured']
+                    }
+                )
+
+                # 处理景点图片
+                image_path = os.path.join(IMAGE_BASE_PATH, f"{spot_data['name']}.png")
+                if os.path.exists(image_path):
+                    if not attraction.image:  # 如果景点没有图片则添加
+                        with open(image_path, 'rb') as f:
+                            attraction.image.save(
+                                os.path.basename(image_path),
+                                File(f),
+                                save=True
+                            )
+                        image_added_count += 1
+                        self.stdout.write(f"成功添加图片: {spot_data['name']}")
+                else:
+                    self.stdout.write(f"警告: 未找到图片 {image_path}")
+
+                if created:
+                    created_count += 1
+                    self.stdout.write(f"创建景点: {spot_data['name']}")
+                else:
+                    skipped_count += 1
+                    self.stdout.write(f"已存在景点: {spot_data['name']}")
+
+            except Exception as e:
+                self.stdout.write(self.style.ERROR(
+                    f"处理景点 {spot_data['name']} 时出错: {str(e)}. 位置信息: {spot_data['location']}"
+                ))
+                continue
+
+        self.stdout.write(
+            self.style.SUCCESS(
+                f'导入完成!成功创建 {created_count} 个景点,跳过 {skipped_count} 个已存在景点,添加 {image_added_count} 张图片'
+            )
+        )

+ 118 - 0
houduan/ai_planner/management/commands/seed_shandong_cities.py

@@ -0,0 +1,118 @@
+from django.core.management.base import BaseCommand
+from ai_planner.models import City
+
+class Command(BaseCommand):
+    help = 'Seeds the database with Shandong province city data'
+
+    def handle(self, *args, **options):
+        shandong_cities = [
+            {
+                "name": "济南市",
+                "code": "jn",
+                "description": "山东省省会,泉城",
+                "is_hot": True
+            },
+            {
+                "name": "青岛市",
+                "code": "qd",
+                "description": "计划单列市,海滨城市",
+                "is_hot": True
+            },
+            {
+                "name": "淄博市",
+                "code": "zb",
+                "description": "齐国故都,工业名城",
+                "is_hot": False
+            },
+            {
+                "name": "枣庄市",
+                "code": "zz",
+                "description": "铁道游击队故乡",
+                "is_hot": False
+            },
+            {
+                "name": "东营市",
+                "code": "dy",
+                "description": "黄河入海口,石油之城",
+                "is_hot": False
+            },
+            {
+                "name": "烟台市",
+                "code": "yt",
+                "description": "葡萄酒城,海滨城市",
+                "is_hot": True
+            },
+            {
+                "name": "潍坊市",
+                "code": "wf",
+                "description": "世界风筝之都",
+                "is_hot": False
+            },
+            {
+                "name": "济宁市",
+                "code": "jn2",
+                "description": "孔孟之乡,运河之都",
+                "is_hot": False
+            },
+            {
+                "name": "泰安市",
+                "code": "ta",
+                "description": "泰山所在地",
+                "is_hot": True
+            },
+            {
+                "name": "威海市",
+                "code": "wh",
+                "description": "最适合人类居住城市",
+                "is_hot": True
+            },
+            {
+                "name": "日照市",
+                "code": "rz",
+                "description": "东方太阳城",
+                "is_hot": False
+            },
+            {
+                "name": "临沂市",
+                "code": "ly",
+                "description": "商贸物流之都",
+                "is_hot": False
+            },
+            {
+                "name": "德州市",
+                "code": "dz",
+                "description": "中国太阳城",
+                "is_hot": False
+            },
+            {
+                "name": "聊城市",
+                "code": "lc",
+                "description": "江北水城",
+                "is_hot": False
+            },
+            {
+                "name": "滨州市",
+                "code": "bz",
+                "description": "黄河三角洲中心",
+                "is_hot": False
+            },
+            {
+                "name": "菏泽市",
+                "code": "hz",
+                "description": "中国牡丹之都",
+                "is_hot": False
+            }
+        ]
+
+        created_count = 0
+        for city_data in shandong_cities:
+            _, created = City.objects.get_or_create(
+                code=city_data["code"],
+                defaults=city_data
+            )
+            if created:
+                created_count += 1
+
+        self.stdout.write(
+            self.style.SUCCESS(f'成功添加 {created_count} 个山东省城市数据(共 {len(shandong_cities)} 个)')
+        )

+ 136 - 0
houduan/ai_planner/migrations/0001_initial.py

@@ -0,0 +1,136 @@
+# Generated by Django 5.2.1 on 2025-06-17 04:07
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='City',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=50, verbose_name='城市名称')),
+                ('code', models.CharField(max_length=20, unique=True, verbose_name='城市代码')),
+                ('description', models.TextField(blank=True, verbose_name='城市描述')),
+                ('image', models.ImageField(blank=True, null=True, upload_to='cities/', verbose_name='城市图片')),
+                ('is_hot', models.BooleanField(default=False, verbose_name='是否热门')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+            ],
+            options={
+                'verbose_name': '城市',
+                'verbose_name_plural': '城市',
+            },
+        ),
+        migrations.CreateModel(
+            name='DayPlan',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('day', models.PositiveSmallIntegerField(verbose_name='第几天')),
+                ('theme', models.CharField(max_length=100, verbose_name='当日主题')),
+                ('description', models.TextField(blank=True, verbose_name='当日描述')),
+                ('transport', models.CharField(blank=True, max_length=200, verbose_name='交通方式')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+            ],
+            options={
+                'verbose_name': '每日计划',
+                'verbose_name_plural': '每日计划',
+                'ordering': ['travel_plan', 'day'],
+            },
+        ),
+        migrations.CreateModel(
+            name='Attraction',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, verbose_name='景点名称')),
+                ('description', models.TextField(verbose_name='景点描述')),
+                ('short_desc', models.CharField(max_length=200, verbose_name='简短描述')),
+                ('address', models.CharField(max_length=200, verbose_name='地址')),
+                ('ticket_price', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='门票价格')),
+                ('open_hours', models.CharField(blank=True, max_length=100, verbose_name='开放时间')),
+                ('image', models.ImageField(blank=True, null=True, upload_to='attractions/', verbose_name='景点图片')),
+                ('tags', models.JSONField(default=list, verbose_name='标签')),
+                ('is_featured', models.BooleanField(default=False, verbose_name='是否特色')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+                ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attractions', to='ai_planner.city', verbose_name='所属城市')),
+            ],
+            options={
+                'verbose_name': '景点',
+                'verbose_name_plural': '景点',
+            },
+        ),
+        migrations.CreateModel(
+            name='DayPlanAttraction',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('order', models.PositiveSmallIntegerField(verbose_name='顺序')),
+                ('visit_time', models.CharField(blank=True, max_length=50, verbose_name='参观时间')),
+                ('notes', models.TextField(blank=True, verbose_name='备注')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('attraction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ai_planner.attraction', verbose_name='景点')),
+                ('day_plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attractions', to='ai_planner.dayplan', verbose_name='每日计划')),
+            ],
+            options={
+                'verbose_name': '每日计划景点',
+                'verbose_name_plural': '每日计划景点',
+                'ordering': ['day_plan', 'order'],
+            },
+        ),
+        migrations.CreateModel(
+            name='TravelPlan',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=100, verbose_name='行程标题')),
+                ('description', models.TextField(verbose_name='行程描述')),
+                ('days', models.PositiveSmallIntegerField(verbose_name='天数')),
+                ('suitable_for', models.CharField(blank=True, max_length=100, verbose_name='适合人群')),
+                ('status', models.CharField(choices=[('generating', '生成中'), ('completed', '已完成'), ('failed', '失败')], default='generating', max_length=20, verbose_name='状态')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='travel_plans', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
+            ],
+            options={
+                'verbose_name': '旅行计划',
+                'verbose_name_plural': '旅行计划',
+            },
+        ),
+        migrations.AddField(
+            model_name='dayplan',
+            name='travel_plan',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='day_plans', to='ai_planner.travelplan', verbose_name='旅行计划'),
+        ),
+        migrations.CreateModel(
+            name='UserPreference',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('days', models.PositiveSmallIntegerField(verbose_name='旅行天数')),
+                ('interests', models.JSONField(default=list, verbose_name='兴趣偏好')),
+                ('transport', models.CharField(choices=[('walking', '步行'), ('driving', '自驾'), ('public', '公共交通'), ('mixed', '混合')], max_length=20, verbose_name='出行方式')),
+                ('custom_requirements', models.TextField(blank=True, verbose_name='特殊需求')),
+                ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
+                ('cities', models.ManyToManyField(to='ai_planner.city', verbose_name='选择城市')),
+                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', to=settings.AUTH_USER_MODEL, verbose_name='用户')),
+            ],
+            options={
+                'verbose_name': '用户偏好',
+                'verbose_name_plural': '用户偏好',
+            },
+        ),
+        migrations.AddField(
+            model_name='travelplan',
+            name='preference',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='plans', to='ai_planner.userpreference', verbose_name='偏好设置'),
+        ),
+    ]

+ 18 - 0
houduan/ai_planner/migrations/0002_attraction_history.py

@@ -0,0 +1,18 @@
+# Generated by Django 5.2.1 on 2025-06-18 01:59
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ai_planner', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='attraction',
+            name='history',
+            field=models.TextField(blank=True, default='', verbose_name='历史背景'),
+        ),
+    ]

+ 44 - 0
houduan/ai_planner/migrations/0003_attraction_latitude_attraction_longitude_and_more.py

@@ -0,0 +1,44 @@
+# Generated by Django 5.2.1 on 2025-06-18 03:34
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ai_planner', '0002_attraction_history'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='attraction',
+            name='latitude',
+            field=models.FloatField(blank=True, null=True, verbose_name='纬度'),
+        ),
+        migrations.AddField(
+            model_name='attraction',
+            name='longitude',
+            field=models.FloatField(blank=True, null=True, verbose_name='经度'),
+        ),
+        migrations.AlterField(
+            model_name='attraction',
+            name='city',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attractions', to='ai_planner.city'),
+        ),
+        migrations.AlterField(
+            model_name='attraction',
+            name='created_at',
+            field=models.DateTimeField(auto_now_add=True),
+        ),
+        migrations.AlterField(
+            model_name='attraction',
+            name='history',
+            field=models.TextField(blank=True, verbose_name='历史背景'),
+        ),
+        migrations.AlterField(
+            model_name='attraction',
+            name='updated_at',
+            field=models.DateTimeField(auto_now=True),
+        ),
+    ]

+ 0 - 0
houduan/ai_planner/migrations/__init__.py


BIN
houduan/ai_planner/migrations/__pycache__/0001_initial.cpython-311.pyc


BIN
houduan/ai_planner/migrations/__pycache__/0001_initial.cpython-312.pyc


BIN
houduan/ai_planner/migrations/__pycache__/0002_attraction_history.cpython-311.pyc


BIN
houduan/ai_planner/migrations/__pycache__/0002_attraction_history.cpython-312.pyc


BIN
houduan/ai_planner/migrations/__pycache__/0003_attraction_latitude_attraction_longitude_and_more.cpython-311.pyc


BIN
houduan/ai_planner/migrations/__pycache__/0003_attraction_latitude_attraction_longitude_and_more.cpython-312.pyc


BIN
houduan/ai_planner/migrations/__pycache__/__init__.cpython-311.pyc


BIN
houduan/ai_planner/migrations/__pycache__/__init__.cpython-312.pyc


+ 130 - 0
houduan/ai_planner/models.py

@@ -0,0 +1,130 @@
+from django.db import models
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+
+class City(models.Model):
+    name = models.CharField(max_length=50, verbose_name="城市名称")
+    code = models.CharField(max_length=20, unique=True, verbose_name="城市代码")
+    description = models.TextField(verbose_name="城市描述", blank=True)
+    image = models.ImageField(upload_to='cities/', verbose_name="城市图片", null=True, blank=True)
+    is_hot = models.BooleanField(default=False, verbose_name="是否热门")
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+    class Meta:
+        verbose_name = "城市"
+        verbose_name_plural = "城市"
+
+    def __str__(self):
+        return self.name
+
+class Attraction(models.Model):
+    city = models.ForeignKey(City, on_delete=models.CASCADE, related_name='attractions')
+    name = models.CharField(max_length=100, verbose_name="景点名称")
+    description = models.TextField(verbose_name="景点描述")
+    short_desc = models.CharField(max_length=200, verbose_name="简短描述")
+    address = models.CharField(max_length=200, verbose_name="地址")
+    latitude = models.FloatField(verbose_name="纬度", null=True, blank=True)
+    longitude = models.FloatField(verbose_name="经度", null=True, blank=True)
+    ticket_price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="门票价格", default=0)
+    open_hours = models.CharField(max_length=100, verbose_name="开放时间", blank=True)
+    image = models.ImageField(upload_to='attractions/', verbose_name="景点图片", null=True, blank=True)
+    tags = models.JSONField(default=list, verbose_name="标签")
+    history = models.TextField(verbose_name="历史背景", blank=True)
+    is_featured = models.BooleanField(default=False, verbose_name="是否特色")
+    created_at = models.DateTimeField(auto_now_add=True)
+    updated_at = models.DateTimeField(auto_now=True)
+
+    class Meta:
+        verbose_name = "景点"
+        verbose_name_plural = "景点"
+
+    def __str__(self):
+        return f"{self.name} ({self.city.name})"
+class UserPreference(models.Model):
+    TRANSPORT_CHOICES = [
+        ('walking', '步行'),
+        ('driving', '自驾'),
+        ('public', '公共交通'),
+        ('mixed', '混合'),
+    ]
+
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='preferences', verbose_name="用户")
+    cities = models.ManyToManyField(City, verbose_name="选择城市")
+    days = models.PositiveSmallIntegerField(verbose_name="旅行天数")
+    interests = models.JSONField(default=list, verbose_name="兴趣偏好")
+    transport = models.CharField(max_length=20, choices=TRANSPORT_CHOICES, verbose_name="出行方式")
+    custom_requirements = models.TextField(verbose_name="特殊需求", blank=True)
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+    class Meta:
+        verbose_name = "用户偏好"
+        verbose_name_plural = "用户偏好"
+
+    def __str__(self):
+        return f"{self.user.username}的偏好设置"
+
+
+class TravelPlan(models.Model):
+    STATUS_CHOICES = [
+        ('generating', '生成中'),
+        ('completed', '已完成'),
+        ('failed', '失败'),
+    ]
+
+    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='travel_plans', verbose_name="用户")
+    preference = models.ForeignKey(UserPreference, on_delete=models.CASCADE, related_name='plans',
+                                   verbose_name="偏好设置")
+    title = models.CharField(max_length=100, verbose_name="行程标题")
+    description = models.TextField(verbose_name="行程描述")
+    days = models.PositiveSmallIntegerField(verbose_name="天数")
+    suitable_for = models.CharField(max_length=100, verbose_name="适合人群", blank=True)
+    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='generating', verbose_name="状态")
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+    class Meta:
+        verbose_name = "旅行计划"
+        verbose_name_plural = "旅行计划"
+
+    def __str__(self):
+        return f"{self.user.username}的{self.title}"
+
+
+class DayPlan(models.Model):
+    travel_plan = models.ForeignKey(TravelPlan, on_delete=models.CASCADE, related_name='day_plans',
+                                    verbose_name="旅行计划")
+    day = models.PositiveSmallIntegerField(verbose_name="第几天")
+    theme = models.CharField(max_length=100, verbose_name="当日主题")
+    description = models.TextField(verbose_name="当日描述", blank=True)
+    transport = models.CharField(max_length=200, verbose_name="交通方式", blank=True)
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+    class Meta:
+        verbose_name = "每日计划"
+        verbose_name_plural = "每日计划"
+        ordering = ['travel_plan', 'day']
+
+    def __str__(self):
+        return f"{self.travel_plan.title} - 第{self.day}天"
+
+
+class DayPlanAttraction(models.Model):
+    day_plan = models.ForeignKey(DayPlan, on_delete=models.CASCADE, related_name='attractions', verbose_name="每日计划")
+    attraction = models.ForeignKey(Attraction, on_delete=models.CASCADE, verbose_name="景点")
+    order = models.PositiveSmallIntegerField(verbose_name="顺序")
+    visit_time = models.CharField(max_length=50, verbose_name="参观时间", blank=True)
+    notes = models.TextField(verbose_name="备注", blank=True)
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+
+    class Meta:
+        verbose_name = "每日计划景点"
+        verbose_name_plural = "每日计划景点"
+        ordering = ['day_plan', 'order']
+
+    def __str__(self):
+        return f"{self.day_plan} - {self.attraction.name}"

+ 128 - 0
houduan/ai_planner/serializers.py

@@ -0,0 +1,128 @@
+from rest_framework import serializers
+from .models import City, Attraction, UserPreference, TravelPlan, DayPlan, DayPlanAttraction
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+
+# class CitySerializer(serializers.ModelSerializer):
+#     is_available = serializers.BooleanField(source='is_hot')  # Map is_hot to is_available
+#
+#     class Meta:
+#         model = City
+#         fields = ['id', 'name', 'is_available']  # Keep is_available in response
+#
+#
+# class AttractionSerializer(serializers.ModelSerializer):
+#     city = CitySerializer(read_only=True)
+#
+#     class Meta:
+#         model = Attraction
+#         fields = ['id', 'city', 'name', 'description', 'short_desc', 'address',
+#                   'ticket_price', 'open_hours', 'image', 'tags', 'is_featured']
+class CitySerializer(serializers.ModelSerializer):
+    class Meta:
+        model = City
+        fields = ['id', 'name', 'code', 'image', 'description']
+
+
+class AttractionSerializer(serializers.ModelSerializer):
+    city = CitySerializer(read_only=True)
+    tags = serializers.SerializerMethodField()
+    image_url = serializers.SerializerMethodField()
+
+    class Meta:
+        model = Attraction
+        fields = ['id', 'city', 'name', 'description', 'short_desc', 'address',
+                  'ticket_price', 'open_hours', 'image_url', 'tags', 'is_featured']
+
+    def get_tags(self, obj):
+        # 确保返回的是列表,即使数据库中是字符串
+        if isinstance(obj.tags, str):
+            return [tag.strip() for tag in obj.tags.split(',')]
+        return obj.tags or []
+
+    def get_image_url(self, obj):
+        if obj.image:
+            return obj.image.url
+        return None
+
+
+class RedTourismPlanSerializer(serializers.Serializer):
+    title = serializers.CharField()
+    description = serializers.CharField()
+    days = serializers.ListField(child=serializers.DictField())
+    red_tourism_tips = serializers.ListField(
+        child=serializers.CharField(),
+        required=False
+    )
+    suitable_for = serializers.CharField(
+        required=False,
+        default="适合党员干部、学生团体等"
+    )
+
+    def validate(self, data):
+        """验证至少包含2个红色景点"""
+        red_attractions = 0
+        for day in data.get('days', []):
+            for att in day.get('attractions', []):
+                if att.get('is_red_tourism', False):
+                    red_attractions += 1
+
+        if red_attractions < 2:
+            raise serializers.ValidationError("行程必须包含至少2个红色景点")
+        return data
+
+class UserPreferenceSerializer(serializers.ModelSerializer):
+    cities = CitySerializer(many=True, read_only=True)
+
+    class Meta:
+        model = UserPreference
+        fields = ['id', 'cities', 'days', 'interests', 'transport', 'custom_requirements']
+
+class DayPlanAttractionSerializer(serializers.ModelSerializer):
+    attraction = AttractionSerializer(read_only=True)
+
+    class Meta:
+        model = DayPlanAttraction
+        fields = ['id', 'attraction', 'order', 'visit_time', 'notes']
+
+
+class DayPlanSerializer(serializers.ModelSerializer):
+    attractions = DayPlanAttractionSerializer(many=True, read_only=True)
+
+    class Meta:
+        model = DayPlan
+        fields = ['id', 'day', 'theme', 'description', 'transport', 'attractions']
+
+
+class TravelPlanSerializer(serializers.ModelSerializer):
+    day_plans = DayPlanSerializer(many=True, read_only=True)
+
+    class Meta:
+        model = TravelPlan
+        fields = ['id', 'title', 'description', 'days', 'suitable_for', 'status', 'day_plans', 'created_at']
+
+
+class TravelPlanCreateSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = TravelPlan
+        fields = ['preference', 'title', 'description', 'days', 'suitable_for']
+
+
+class UserPreferenceCreateSerializer(serializers.ModelSerializer):
+    city_ids = serializers.ListField(
+        child=serializers.IntegerField(),
+        write_only=True,
+        required=True
+    )
+
+    class Meta:
+        model = UserPreference
+        fields = ['city_ids', 'days', 'interests', 'transport', 'custom_requirements']
+
+    def create(self, validated_data):
+        city_ids = validated_data.pop('city_ids')
+        preference = UserPreference.objects.create(**validated_data)
+        preference.cities.set(city_ids)
+        return preference

+ 1012 - 0
houduan/ai_planner/services.py

@@ -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

+ 3 - 0
houduan/ai_planner/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 23 - 0
houduan/ai_planner/urls.py

@@ -0,0 +1,23 @@
+from django.urls import path
+from .views import (
+    CityListView,
+    AttractionListView,
+    UserPreferenceView,
+    TravelPlanView,
+    TravelPlanDetailView,
+    RegeneratePlanView, RedTourismPlanView, RedAttractionDetailView, RedTourismRegenerateView
+)
+
+urlpatterns = [
+    path('cities/', CityListView.as_view(), name='city-list'),
+    path('cities/<int:city_id>/attractions/', AttractionListView.as_view(), name='attraction-list'),
+    path('preferences/', UserPreferenceView.as_view(), name='preference'),
+    path('plans/', TravelPlanView.as_view(), name='plan-list'),
+    path('plans/<int:plan_id>/', TravelPlanDetailView.as_view(), name='plan-detail'),
+    path('plans/<int:plan_id>/regenerate/', RegeneratePlanView.as_view(), name='regenerate-plan'),
+    # 新增的红色旅游API
+    path('red-tourism/plan/', RedTourismPlanView.as_view(), name='red-tourism-plan'),
+    path('red-tourism/attractions/<int:attraction_id>/', RedAttractionDetailView.as_view(),
+         name='red-attraction-detail'),
+    path('red-tourism/regenerate/', RedTourismRegenerateView.as_view(), name='red-tourism-regenerate'),
+]

+ 597 - 0
houduan/ai_planner/views.py

@@ -0,0 +1,597 @@
+from django.utils import timezone
+
+from django.http import JsonResponse
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.permissions import IsAuthenticated
+from .models import UserPreference, TravelPlan
+from .models import City, Attraction
+import logging
+logger = logging.getLogger(__name__)
+from .serializers import (
+    CitySerializer,
+    AttractionSerializer,
+    UserPreferenceSerializer,
+    TravelPlanSerializer,
+    UserPreferenceCreateSerializer,
+    TravelPlanCreateSerializer
+)
+from .services import TravelPlanService, CacheService, MoonshotAIService
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+# views.py
+from rest_framework.response import Response
+from .models import City
+
+class CityListView(APIView):
+    permission_classes = []  # 无需登录
+    def get(self, request):
+        try:
+            # Return all cities instead of just hot ones
+            cities = City.objects.all()  # Changed from filter(is_hot=True)
+            serializer = CitySerializer(cities, many=True)
+            return Response({
+                "status": "success",
+                "data": serializer.data
+            }, status=status.HTTP_200_OK)
+        except Exception as e:
+            return Response({
+                "status": "error",
+                "message": str(e)
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class AttractionListView(APIView):
+    def get(self, request, city_id):
+        attractions = CacheService.get_attractions_by_city(city_id)
+        return Response(attractions)
+
+
+class UserPreferenceView(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def get(self, request):
+        preference = UserPreference.objects.filter(user=request.user).first()
+        if not preference:
+            return Response({'detail': '未找到偏好设置'}, status=status.HTTP_404_NOT_FOUND)
+
+        serializer = UserPreferenceSerializer(preference)
+        return Response(serializer.data)
+
+    def post(self, request):
+        serializer = UserPreferenceCreateSerializer(data=request.data, context={'request': request})
+        if serializer.is_valid():
+            # 确保每个用户只有一个偏好设置
+            UserPreference.objects.filter(user=request.user).delete()
+
+            preference = serializer.save(user=request.user)
+            return Response(UserPreferenceSerializer(preference).data, status=status.HTTP_201_CREATED)
+        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+# class TravelPlanView(APIView):
+#     permission_classes = []  # 确保无认证要求
+#
+#     def post(self, request):
+#         try:
+#             # 验证必要字段
+#             required_fields = ['city_ids', 'days', 'interests', 'transport']
+#             if not all(field in request.data for field in required_fields):
+#                 return Response(
+#                     {'status': 'error', 'message': '缺少必要参数'},
+#                     status=status.HTTP_400_BAD_REQUEST
+#                 )
+#
+#             # 准备数据
+#             plan_data = {
+#                 'city_ids': request.data['city_ids'],
+#                 'days': request.data['days'],
+#                 'interests': request.data['interests'],
+#                 'transport': request.data['transport'],
+#                 'custom_requirements': request.data.get('custom_requirements', '')
+#             }
+#
+#             # 调用服务生成计划
+#             travel_plan = TravelPlanService.create_travel_plan(plan_data)
+#
+#             if travel_plan:
+#                 serializer = TravelPlanSerializer(travel_plan)
+#                 return Response({
+#                     'status': 'success',
+#                     'data': serializer.data
+#                 }, status=status.HTTP_201_CREATED)
+#             else:
+#                 return Response({
+#                     'status': 'error',
+#                     'message': '生成旅行计划失败'
+#                 }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+#
+#         except Exception as e:
+#             import traceback
+#             traceback.print_exc()  # 打印完整错误堆栈
+#             return Response({
+#                 'status': 'error',
+#                 'message': str(e)
+#             }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class TravelPlanView(APIView):
+    permission_classes = []  # 无需认证
+
+    def post(self, request):
+        """
+        生成旅行计划(不保存到数据库)
+        请求参数:
+        - city_ids: 城市ID列表
+        - days: 旅行天数
+        - interests: 兴趣标签列表
+        - transport: 交通方式
+        - custom_requirements: 自定义需求(可选)
+        """
+        try:
+            # 1. 验证必要参数
+            required_fields = ['city_ids', 'days', 'interests', 'transport']
+            if not all(field in request.data for field in required_fields):
+                return Response(
+                    {'status': 'error', 'message': '缺少必要参数'},
+                    status=status.HTTP_400_BAD_REQUEST
+                )
+
+            # 2. 准备AI生成所需数据
+            plan_data = {
+                'city_ids': request.data['city_ids'],
+                'days': request.data['days'],
+                'interests': request.data['interests'],
+                'transport': request.data['transport'],
+                'custom_requirements': request.data.get('custom_requirements', '')
+            }
+
+            # 3. 直接调用Moonshot AI生成计划(不创建数据库记录)
+            generated_plan = MoonshotAIService.generate_travel_plan(plan_data)
+
+            if not generated_plan or 'error' in generated_plan:
+                error_msg = generated_plan.get('details', '生成旅行计划失败') if generated_plan else 'AI服务无响应'
+                return Response({
+                    'status': 'error',
+                    'message': error_msg
+                }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+            # 4. 返回生成的计划数据(不包含数据库ID等字段)
+            return Response({
+                'status': 'success',
+                'data': {
+                    'title': generated_plan.get('title', '自定义旅行计划'),
+                    'description': generated_plan.get('description', ''),
+                    'days': generated_plan.get('days', []),
+                    'suitable_for': generated_plan.get('suitable_for', '')
+                }
+            }, status=status.HTTP_200_OK)
+
+        except Exception as e:
+            import traceback
+            traceback.print_exc()  # 打印错误堆栈
+            return Response({
+                'status': 'error',
+                'message': f'服务器内部错误: {str(e)}'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+# views.py
+class RedTourismPlanView(APIView):
+    """
+    红色旅游路线规划API - 稳定版
+    返回标准化的行程数据结构,确保前端能正确渲染
+    """
+    permission_classes = []
+
+    def post(self, request):
+        try:
+            # 1. 参数验证
+            city_ids = request.data.get('city_ids')
+            days = request.data.get('days')
+
+            if not city_ids or not isinstance(city_ids, list):
+                return Response(
+                    {'status': 'error', 'message': '请提供有效的城市ID列表'},
+                    status=status.HTTP_400_BAD_REQUEST
+                )
+
+            if not days or not isinstance(days, int) or days < 1 or days > 7:
+                return Response(
+                    {'status': 'error', 'message': '请提供1-7天的有效天数'},
+                    status=status.HTTP_400_BAD_REQUEST
+                )
+
+            # 2. 准备请求数据
+            interests = request.data.get('interests', [])
+            if not isinstance(interests, list):
+                interests = []
+
+            if 'red-tourism' not in interests:
+                interests.append('red-tourism')
+
+            plan_data = {
+                'city_ids': city_ids,
+                'days': days,
+                'interests': interests,
+                'transport': request.data.get('transport', 'public'),
+                'custom_requirements': request.data.get('custom_requirements', ''),
+                'is_red_tourism': True
+            }
+
+            # 3. 调用AI服务
+            generated_plan = MoonshotAIService.generate_travel_plan(plan_data)
+            logger.info(f"Generated plan: {generated_plan}")
+
+            if not generated_plan:
+                return Response(
+                    {'status': 'error', 'message': 'AI服务无响应'},
+                    status=status.HTTP_500_INTERNAL_SERVER_ERROR
+                )
+
+            if 'error' in generated_plan:
+                return Response(
+                    {'status': 'error', 'message': generated_plan.get('message', 'AI生成失败')},
+                    status=status.HTTP_400_BAD_REQUEST
+                )
+
+            # 4. 标准化返回数据
+            standardized_plan = self._standardize_plan(generated_plan, days)
+            return Response({
+                'status': 'success',
+                'data': standardized_plan
+            })
+
+        except Exception as e:
+            logger.error(f"API error: {str(e)}", exc_info=True)
+            return Response({
+                'status': 'error',
+                'message': '服务器内部错误'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+    def _standardize_plan(self, plan_data, requested_days):
+        """确保返回数据结构的完整性和一致性"""
+        result = {
+            'title': plan_data.get('title', f'{requested_days}天红色之旅'),
+            'description': plan_data.get('description', '通过参观革命历史遗址学习党史'),
+            'days': [],
+            'red_tourism_tips': plan_data.get('red_tourism_tips', [
+                "请着装整洁,保持肃穆",
+                "建议提前学习相关历史知识"
+            ]),
+            'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等')
+        }
+
+        # 处理每日行程
+        for day in plan_data.get('days', []):
+            standardized_day = {
+                'day': day.get('day', 1),
+                'theme': day.get('theme', '红色教育'),
+                'transport': day.get('transport', '公共交通'),
+                'attractions': [],
+                'educational_points': day.get('educational_points', [
+                    "学习革命历史",
+                    "传承红色精神"
+                ])
+            }
+
+            # 处理景点数据
+            for attr in day.get('attractions', []):
+                standardized_attr = {
+                    'id': attr.get('id', 0),
+                    'name': attr.get('name', '红色教育基地'),
+                    'image': attr.get('image', '/images/default-red.jpg'),
+                    'is_red_tourism': attr.get('is_red_tourism', True),
+                    'educational_value': attr.get('educational_value', '高'),
+                    'visit_time': attr.get('visit_time', '09:00-17:00'),
+                    'ticket_price': attr.get('ticket_price', '免费')
+                }
+                standardized_day['attractions'].append(standardized_attr)
+
+            result['days'].append(standardized_day)
+
+        return result
+
+class RedAttractionDetailView(APIView):
+    """
+    红色景点详情API
+    权限:无需认证
+    """
+    permission_classes = []
+
+    def get(self, request, attraction_id):
+        try:
+            attraction = Attraction.objects.get(id=attraction_id)
+
+            if '红色旅游' not in attraction.tags:
+                return Response({
+                    'status': 'error',
+                    'message': '该景点不是红色旅游景点'
+                }, status=status.HTTP_400_BAD_REQUEST)
+
+            # 获取相关红色景点
+            related_attractions = Attraction.objects.filter(
+                city=attraction.city,
+                tags__contains="红色旅游"
+            ).exclude(id=attraction_id)[:3]
+
+            serializer = AttractionSerializer(attraction)
+            related_serializer = AttractionSerializer(related_attractions, many=True)
+
+            # 构建响应数据
+            response_data = {
+                'detail': serializer.data,
+                'historical_background': attraction.history or "暂无详细历史背景",
+                'educational_significance': self._get_educational_content(attraction),
+                'related_attractions': related_serializer.data,
+                'visiting_etiquette': self._get_visiting_etiquette(attraction)
+            }
+
+            return Response({
+                'status': 'success',
+                'data': response_data
+            })
+
+        except Attraction.DoesNotExist:
+            return Response(
+                {'status': 'error', 'message': '景点不存在'},
+                status=status.HTTP_404_NOT_FOUND
+            )
+
+    def _get_educational_content(self, attraction):
+        """生成教育意义内容"""
+        content = []
+
+        if 'memorial' in attraction.tags:
+            content.append("此处是重要的革命纪念地,具有深刻的教育意义")
+            content.append("适合开展爱国主义主题教育活动")
+
+        if 'battle' in attraction.tags:
+            content.append("这里发生过重要历史战役,展现了革命先烈的英勇精神")
+
+        return content
+
+    def _get_visiting_etiquette(self, attraction):
+        """生成参观礼仪指南"""
+        etiquette = [
+            "请保持庄严肃穆",
+            "勿大声喧哗"
+        ]
+
+        if 'memorial' in attraction.tags:
+            etiquette.append("纪念馆内请勿拍照")
+
+        return etiquette
+
+
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+import json
+import logging
+from django.core.exceptions import ValidationError
+
+logger = logging.getLogger(__name__)
+
+
+class RedTourismRegenerateView(APIView):
+    """
+    红色旅游路线重新生成API - 增强版
+    功能:
+    1. 基于用户输入和原始行程生成优化路线
+    2. 严格验证输入参数
+    3. 提供标准化的响应格式
+    """
+    permission_classes = []
+
+    def post(self, request):
+        try:
+            # === 1. 数据预处理 ===
+            request_data = self._parse_and_validate_request(request)
+
+            # === 2. 业务逻辑处理 ===
+            generated_plan = self._generate_red_tourism_plan(request_data)
+
+            # === 3. 响应处理 ===
+            return Response({
+                'status': 'success',
+                'data': self._standardize_plan(generated_plan, request_data['days']),
+                'meta': {
+                    'generated_at': timezone.now().isoformat(),
+                    'version': '1.1'
+                }
+            })
+
+        except ValidationError as e:
+            logger.warning(f"参数验证失败: {str(e)}")
+            return Response({
+                'status': 'error',
+                'message': str(e),
+                'code': 'INVALID_PARAMS'
+            }, status=status.HTTP_400_BAD_REQUEST)
+
+        except Exception as e:
+            logger.error(f"服务器错误: {str(e)}", exc_info=True)
+            return Response({
+                'status': 'error',
+                'message': '服务器处理请求时发生错误',
+                'code': 'SERVER_ERROR'
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+    def _parse_and_validate_request(self, request):
+        """解析并验证请求数据"""
+        try:
+            # 数据格式检查
+            if request.content_type != 'application/json':
+                raise ValidationError("只支持JSON格式数据")
+
+            request_data = request.data.copy() if hasattr(request, 'data') else json.loads(request.body)
+
+            # 必填字段检查
+            required_fields = ['city_ids', 'days', 'previous_plan']
+            for field in required_fields:
+                if field not in request_data:
+                    raise ValidationError(f"缺少必要字段: {field}")
+
+            # 处理city_ids的各种情况
+            city_ids = request_data['city_ids']
+            if isinstance(city_ids, int):
+                request_data['city_ids'] = [city_ids]
+            elif isinstance(city_ids, str):
+                request_data['city_ids'] = [int(cid.strip()) for cid in city_ids.split(',')]
+            elif not isinstance(city_ids, (list, tuple)):
+                raise ValidationError("city_ids必须是整数、字符串或数组")
+
+            # 确保所有ID都是整数
+            request_data['city_ids'] = [int(cid) for cid in request_data['city_ids']]
+
+            # 其他验证
+            request_data['days'] = int(request_data['days'])
+            if not 1 <= request_data['days'] <= 7:
+                raise ValidationError("行程天数需在1-7天范围内")
+
+            return request_data
+
+        except Exception as e:
+            raise ValidationError(f"参数验证失败: {str(e)}")
+
+    def _generate_red_tourism_plan(self, request_data):
+        """增强版生成逻辑,确保返回有效景点"""
+        try:
+            logger.info(f"重新生成请求数据: {json.dumps(request_data, ensure_ascii=False)[:500]}...")
+
+            # 确保previous_plan有基本结构
+            if not isinstance(request_data.get('previous_plan'), dict):
+                request_data['previous_plan'] = {'days': []}
+
+            # 调用AI服务
+            generated_plan = MoonshotAIService.regenerate_travel_plan(request_data)
+
+            if not generated_plan:
+                raise Exception("AI服务返回空结果")
+
+            if 'error' in generated_plan:
+                raise Exception(f"AI服务错误: {generated_plan['error']}")
+
+            # 验证必要字段
+            if 'days' not in generated_plan:
+                generated_plan['days'] = [{'day': i + 1} for i in range(request_data['days'])]
+
+            # 确保每个景点都有必要字段
+            for day in generated_plan['days']:
+                for attr in day.get('attractions', []):
+                    attr.setdefault('is_red_tourism', False)
+                    attr.setdefault('educational_value', '中')
+
+            return generated_plan
+
+        except Exception as e:
+            logger.error(f"生成失败: {str(e)}", exc_info=True)
+            raise
+
+    def _standardize_plan(self, plan_data, requested_days):
+        """确保返回数据结构的完整性和一致性"""
+        result = {
+            'title': plan_data.get('title', f'{requested_days}天红色之旅'),
+            'description': plan_data.get('description', '通过参观革命历史遗址学习党史'),
+            'days': [],
+            'red_tourism_tips': plan_data.get('red_tourism_tips', [
+                "请着装整洁,保持肃穆",
+                "建议提前学习相关历史知识"
+            ]),
+            'suitable_for': plan_data.get('suitable_for', '适合党员干部、学生团体等')
+        }
+
+        # 处理每日行程
+        for day in plan_data.get('days', []):
+            standardized_day = {
+                'day': day.get('day', 1),
+                'theme': day.get('theme', '红色教育'),
+                'transport': day.get('transport', '公共交通'),
+                'attractions': [],
+                'educational_points': day.get('educational_points', [
+                    "学习革命历史",
+                    "传承红色精神"
+                ])
+            }
+
+            # 处理景点数据
+            for attr in day.get('attractions', []):
+                standardized_attr = {
+                    'id': attr.get('id', 0),
+                    'name': attr.get('name', '红色教育基地'),
+                    'image': attr.get('image', '/images/default-red.jpg'),
+                    'is_red_tourism': attr.get('is_red_tourism', True),
+                    'educational_value': attr.get('educational_value', '高'),
+                    'visit_time': attr.get('visit_time', '09:00-17:00'),
+                    'ticket_price': attr.get('ticket_price', '免费')
+                }
+                standardized_day['attractions'].append(standardized_attr)
+
+            result['days'].append(standardized_day)
+
+        return result
+
+
+class TravelPlanDetailView(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def get(self, request, plan_id):
+        try:
+            plan = TravelPlan.objects.get(id=plan_id, user=request.user)
+        except TravelPlan.DoesNotExist:
+            return Response(
+                {'detail': '未找到该旅行计划'},
+                status=status.HTTP_404_NOT_FOUND
+            )
+
+        serializer = TravelPlanSerializer(plan)
+        return Response(serializer.data)
+
+    def delete(self, request, plan_id):
+        try:
+            plan = TravelPlan.objects.get(id=plan_id, user=request.user)
+        except TravelPlan.DoesNotExist:
+            return Response(
+                {'detail': '未找到该旅行计划'},
+                status=status.HTTP_404_NOT_FOUND
+            )
+
+        plan.delete()
+        return Response(status=status.HTTP_204_NO_CONTENT)
+
+import requests
+from django.conf import settings
+class RegeneratePlanView(APIView):
+    permission_classes = [IsAuthenticated]
+
+    def post(self, request, plan_id):
+        try:
+            old_plan = TravelPlan.objects.get(id=plan_id, user=request.user)
+        except TravelPlan.DoesNotExist:
+            return Response(
+                {'detail': '未找到该旅行计划'},
+                status=status.HTTP_404_NOT_FOUND
+            )
+
+        # 使用相同的偏好重新生成计划
+        new_plan = TravelPlanService.create_travel_plan_from_preference(old_plan.preference)
+
+        if not new_plan:
+            return Response(
+                {'detail': '重新生成旅行计划失败,请稍后再试'},
+                status=status.HTTP_500_INTERNAL_SERVER_ERROR
+            )
+
+        # 删除旧计划
+        old_plan.delete()
+
+        serializer = TravelPlanSerializer(new_plan)
+        return Response(serializer.data, status=status.HTTP_201_CREATED)
+
+def test_api(request):
+    if request.method == "GET":
+        return JsonResponse({"status": "success", "message": "Django 后端已连接完成!"})
+    return JsonResponse({"status": "error", "message": "仅支持 GET 请求"}, status=400)

+ 0 - 0
houduan/api/__init__.py


BIN
houduan/api/__pycache__/__init__.cpython-311.pyc


BIN
houduan/api/__pycache__/__init__.cpython-312.pyc


BIN
houduan/api/__pycache__/admin.cpython-311.pyc


BIN
houduan/api/__pycache__/admin.cpython-312.pyc


BIN
houduan/api/__pycache__/apps.cpython-311.pyc


BIN
houduan/api/__pycache__/apps.cpython-312.pyc


BIN
houduan/api/__pycache__/models.cpython-311.pyc


BIN
houduan/api/__pycache__/models.cpython-312.pyc


BIN
houduan/api/__pycache__/urls.cpython-311.pyc


BIN
houduan/api/__pycache__/urls.cpython-312.pyc


BIN
houduan/api/__pycache__/views.cpython-311.pyc


BIN
houduan/api/__pycache__/views.cpython-312.pyc


+ 3 - 0
houduan/api/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
houduan/api/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "api"

+ 36 - 0
houduan/api/migrations/0001_initial.py

@@ -0,0 +1,36 @@
+# Generated by Django 5.1.7 on 2025-05-10 06:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = []
+
+    operations = [
+        migrations.CreateModel(
+            name="UserInfo",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "phone",
+                    models.CharField(max_length=11, unique=True, verbose_name="手机号"),
+                ),
+                (
+                    "token",
+                    models.CharField(
+                        blank=True, max_length=64, null=True, verbose_name="用户TOKEN"
+                    ),
+                ),
+            ],
+        ),
+    ]

+ 51 - 0
houduan/api/migrations/0002_alter_userinfo_options_userplan.py

@@ -0,0 +1,51 @@
+# Generated by Django 5.1.7 on 2025-06-17 02:49
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("api", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="userinfo",
+            options={"verbose_name": "用户信息", "verbose_name_plural": "用户信息"},
+        ),
+        migrations.CreateModel(
+            name="UserPlan",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("spot_id", models.IntegerField(verbose_name="景点ID")),
+                ("spot_name", models.CharField(max_length=100, verbose_name="景点名称")),
+                (
+                    "created_at",
+                    models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
+                ),
+                (
+                    "user",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="plans",
+                        to="api.userinfo",
+                        verbose_name="关联用户",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "用户行程",
+                "verbose_name_plural": "用户行程",
+                "unique_together": {("user", "spot_id")},
+            },
+        ),
+    ]

+ 19 - 0
houduan/api/migrations/0003_userplan_spot_img.py

@@ -0,0 +1,19 @@
+# Generated by Django 5.1.7 on 2025-06-17 06:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("api", "0002_alter_userinfo_options_userplan"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userplan",
+            name="spot_img",
+            field=models.URLField(
+                blank=True, default="", max_length=500, null=True, verbose_name="景点图片"
+            ),
+        ),
+    ]

+ 0 - 0
houduan/api/migrations/__init__.py


BIN
houduan/api/migrations/__pycache__/0001_initial.cpython-311.pyc


BIN
houduan/api/migrations/__pycache__/0001_initial.cpython-312.pyc


BIN
houduan/api/migrations/__pycache__/0002_alter_userinfo_options_userplan.cpython-311.pyc


BIN
houduan/api/migrations/__pycache__/0002_alter_userinfo_options_userplan.cpython-312.pyc


BIN
houduan/api/migrations/__pycache__/0003_userplan_spot_img.cpython-311.pyc


BIN
houduan/api/migrations/__pycache__/0003_userplan_spot_img.cpython-312.pyc


BIN
houduan/api/migrations/__pycache__/__init__.cpython-311.pyc


BIN
houduan/api/migrations/__pycache__/__init__.cpython-312.pyc


+ 37 - 0
houduan/api/models.py

@@ -0,0 +1,37 @@
+from django.db import models
+
+# Create your models here.
+
+
+class UserInfo(models.Model):
+    phone = models.CharField(max_length=11, unique=True,verbose_name='手机号')
+
+    token=models.CharField(verbose_name='用户TOKEN',max_length=64,null=True,blank=True)
+
+    class Meta:
+        verbose_name = '用户信息'
+        verbose_name_plural = verbose_name
+
+
+class UserPlan(models.Model):
+    user = models.ForeignKey(
+        UserInfo,
+        on_delete=models.CASCADE,
+        related_name='plans',
+        verbose_name='关联用户'
+    )
+    spot_img = models.URLField(
+        verbose_name='景点图片',
+        max_length=500,
+        blank=True,
+        null=True,
+        default=''  # 设置默认值为空字符串
+    )
+    spot_id = models.IntegerField(verbose_name='景点ID')
+    spot_name = models.CharField(max_length=100, verbose_name='景点名称')
+    created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+
+    class Meta:
+        verbose_name = '用户行程'
+        verbose_name_plural = verbose_name
+        unique_together = ('user', 'spot_id')  # 确保用户不会重复添加同一景点

+ 3 - 0
houduan/api/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 7 - 0
houduan/api/urls.py

@@ -0,0 +1,7 @@
+from api import views
+from django.urls import re_path
+
+urlpatterns = [
+    re_path(r'^login/', views.LoginView.as_view()),
+    re_path(r'^message/', views.MessageView.as_view()),
+]

+ 75 - 0
houduan/api/views.py

@@ -0,0 +1,75 @@
+from django.shortcuts import render
+from rest_framework.exceptions import ValidationError
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import serializers, status
+from django_redis import get_redis_connection
+import random
+# Create your views here.
+import uuid
+import re
+
+def phone_validator(value):
+    if not re.match(r'^(1[3|4|5|6|7|8|9])\d{9}$', value):
+        raise ValidationError('手机格式错误')
+
+class LoginSerializer(serializers.Serializer):
+    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])
+    code = serializers.CharField(label='短信验证码')
+
+
+    def validate_code(self, value):
+        if len(value) !=4:
+            raise ValidationError('短信格式错误')
+        if not value.isdecimal():
+            raise ValidationError("短信格式错误")
+        phone=self.initial_data.get('phone')
+        conn=get_redis_connection()
+        code=conn.get(phone)
+        if not code:
+            raise ValidationError("验证码过期")
+
+        if value!=code.decode('utf-8'):
+            raise ValidationError("验证码错误")
+        return value
+
+class LoginView(APIView):
+    def post(self, request, *args, **kwargs):
+        ser=LoginSerializer(data=request.data)
+        if not ser.is_valid():
+            return Response({'status': False,'message':'验证码错误'})
+        from api import models
+        phone=ser.validated_data.get('phone')
+        user_object,flag=models.UserInfo.objects.get_or_create(phone=phone)
+        user_object.token=str(uuid.uuid4())
+        user_object.save()
+        from api import models
+
+        return Response({'status': True,"data":{'token':user_object.token,'phone':phone,'id':models.UserInfo.objects.get(phone=phone).id}})
+
+
+
+
+
+class MessageSerializer(serializers.Serializer):
+    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])
+
+
+class MessageView(APIView):
+    def get(self, request, *args, **kwargs):
+        ser=MessageSerializer(data=request.query_params)
+        if not ser.is_valid():
+            return Response({'status':False,'message':'手机号错误'})
+        phone = ser.validated_data.get('phone')
+        print(phone)
+
+        random_code = random.randint(1000, 9999)
+
+        print(random_code)
+        import redis
+
+        conn=get_redis_connection()
+        conn.set(phone,random_code,ex=60)
+        print(conn.keys())
+
+        return Response({'status': True,'message':'发送成功'})

+ 0 - 0
houduan/api01/__init__.py


BIN
houduan/api01/__pycache__/__init__.cpython-311.pyc


BIN
houduan/api01/__pycache__/__init__.cpython-312.pyc


BIN
houduan/api01/__pycache__/admin.cpython-311.pyc


BIN
houduan/api01/__pycache__/admin.cpython-312.pyc


BIN
houduan/api01/__pycache__/apps.cpython-311.pyc


BIN
houduan/api01/__pycache__/apps.cpython-312.pyc


BIN
houduan/api01/__pycache__/models.cpython-311.pyc


BIN
houduan/api01/__pycache__/models.cpython-312.pyc


BIN
houduan/api01/__pycache__/urls.cpython-311.pyc


BIN
houduan/api01/__pycache__/views.cpython-311.pyc


BIN
houduan/api01/__pycache__/views.cpython-312.pyc


+ 3 - 0
houduan/api01/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
houduan/api01/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class Api01Config(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'api01'

+ 0 - 0
houduan/api01/migrations/__init__.py


BIN
houduan/api01/migrations/__pycache__/__init__.cpython-311.pyc


BIN
houduan/api01/migrations/__pycache__/__init__.cpython-312.pyc


Some files were not shown because too many files changed in this diff