基于Django的景点印象服务管理系统设计与实现 一、 技术栈本系统采用前后端分离的架构模式主要技术栈如下1.1 后端技术栈核心框架Django 4.x数据库MySQL 8.0 / PostgreSQL 14ORMDjango ORMAPI接口Django REST Framework (DRF)用户认证Django内置认证系统 JWT (djangorestframework-simplejwt)缓存Redis任务队列Celery Redis文件存储本地存储 / 阿里云OSS / 七牛云1.2 前端技术栈前端框架Vue.js 3.x / React 18UI组件库Element Plus (Vue) / Ant Design (React)状态管理Vuex / Pinia (Vue) 或 Redux Toolkit (React)HTTP客户端Axios路由管理Vue Router / React Router1.3 开发与部署版本控制Git容器化Docker, Docker ComposeCI/CDGitHub Actions / GitLab CI部署Nginx Gunicorn (Django) / Node.js (前端静态资源)二、 背景与意义2.1 项目背景随着旅游业的蓬勃发展和互联网技术的深度渗透游客获取景点信息、分享旅行体验的方式发生了根本性变革。传统的旅游信息平台多以官方介绍和商业推广为主缺乏真实、多元、个性化的用户声音。游客在规划行程时往往难以获得关于景点实时状况、真实体验、小众玩法的有效信息。“景点印象”旨在构建一个以用户生成内容为核心的服务管理系统让游客成为信息的创造者和分享者通过真实的图文、视频、评分和评论形成一个动态、鲜活、可信的景点知识库。2.2 现实意义对游客提供决策依据提升旅行体验发现隐藏玩法建立旅行社交圈。对景点管理者获取真实用户反馈用于服务改进和营销决策建立与游客的直接沟通渠道。对旅游生态沉淀非结构化的旅行数据为行业分析、智能推荐、精准营销提供数据基础。技术实践意义本项目涵盖了Web全栈开发的典型技术栈是学习Django、前后端分离、RESTful API设计、数据库优化、缓存、异步任务等技术的优秀实践案例。三、 核心代码实现3.1 数据模型设计 (models.py)from django.db import models from django.contrib.auth.models import User class ScenicSpot(models.Model): 景点模型 name models.CharField(max_length200, verbose_name景点名称) description models.TextField(verbose_name景点描述) location models.CharField(max_length500, verbose_name地理位置) latitude models.FloatField(nullTrue, blankTrue, verbose_name纬度) longitude models.FloatField(nullTrue, blankTrue, verbose_name经度) cover_image models.ImageField(upload_tospots/covers/, verbose_name封面图) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: verbose_name 景点 verbose_name_plural 景点 def __str__(self): return self.name class Impression(models.Model): 印象用户评价模型 RATING_CHOICES [ (1, ⭐), (2, ⭐⭐), (3, ⭐⭐⭐), (4, ⭐⭐⭐⭐), (5, ⭐⭐⭐⭐⭐), ] spot models.ForeignKey(ScenicSpot, on_deletemodels.CASCADE, related_nameimpressions) author models.ForeignKey(User, on_deletemodels.CASCADE, related_nameimpressions) title models.CharField(max_length200, verbose_name印象标题) content models.TextField(verbose_name印象内容) rating models.IntegerField(choicesRATING_CHOICES, verbose_name评分) visit_date models.DateField(verbose_name到访日期) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: verbose_name 景点印象 verbose_name_plural 景点印象 ordering [-created_at] def __str__(self): return f{self.spot.name} - {self.title} class ImpressionImage(models.Model): 印象图片模型 impression models.ForeignKey(Impression, on_deletemodels.CASCADE, related_nameimages) image models.ImageField(upload_toimpressions/images/) uploaded_at models.DateTimeField(auto_now_addTrue) class Comment(models.Model): 评论模型 impression models.ForeignKey(Impression, on_deletemodels.CASCADE, related_namecomments) author models.ForeignKey(User, on_deletemodels.CASCADE, related_namecomments) content models.TextField(verbose_name评论内容) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [created_at]3.2 序列化器 (serializers.py)from rest_framework import serializers from .models import ScenicSpot, Impression, ImpressionImage, Comment from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, email] class ImpressionImageSerializer(serializers.ModelSerializer): class Meta: model ImpressionImage fields [id, image, uploaded_at] class CommentSerializer(serializers.ModelSerializer): author UserSerializer(read_onlyTrue) class Meta: model Comment fields [id, author, content, created_at] read_only_fields [author, created_at] class ImpressionSerializer(serializers.ModelSerializer): author UserSerializer(read_onlyTrue) images ImpressionImageSerializer(manyTrue, read_onlyTrue) comments CommentSerializer(manyTrue, read_onlyTrue) spot_name serializers.CharField(sourcespot.name, read_onlyTrue) class Meta: model Impression fields [id, spot, spot_name, author, title, content, rating, visit_date, images, comments, created_at, updated_at] read_only_fields [author, created_at, updated_at] class ScenicSpotSerializer(serializers.ModelSerializer): # 计算平均评分 average_rating serializers.SerializerMethodField() # 关联的印象列表可控制数量 recent_impressions serializers.SerializerMethodField() class Meta: model ScenicSpot fields [id, name, description, location, latitude, longitude, cover_image, average_rating, recent_impressions, created_at] def get_average_rating(self, obj): # 计算该景点的平均评分 impressions obj.impressions.all() if impressions: total sum(imp.rating for imp in impressions) return round(total / len(impressions), 1) return 0 def get_recent_impressions(self, obj): # 获取最近3条印象 impressions obj.impressions.all().order_by(-created_at)[:3] return ImpressionSerializer(impressions, manyTrue).data3.3 视图集 (views.py)from rest_framework import viewsets, permissions, filters, status from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from .models import ScenicSpot, Impression, Comment from .serializers import ScenicSpotSerializer, ImpressionSerializer, CommentSerializer from .permissions import IsOwnerOrReadOnly class ScenicSpotViewSet(viewsets.ModelViewSet): 景点视图集支持列表、详情、创建、更新、删除。 支持按名称、位置搜索按评分排序。 queryset ScenicSpot.objects.all() serializer_class ScenicSpotSerializer filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields [] search_fields [name, description, location] ordering_fields [average_rating, created_at] ordering [-created_at] # 默认按创建时间倒序 action(detailTrue, methods[get]) def impressions(self, request, pkNone): 获取某个景点的所有印象 spot self.get_object() impressions spot.impressions.all() page self.paginate_queryset(impressions) if page is not None: serializer ImpressionSerializer(page, manyTrue) return self.get_paginated_response(serializer.data) serializer ImpressionSerializer(impressions, manyTrue) return Response(serializer.data) class ImpressionViewSet(viewsets.ModelViewSet): 印象视图集用户可创建、查看、更新、删除自己的印象。 支持按景点、评分、日期过滤。 queryset Impression.objects.all() serializer_class ImpressionSerializer permission_classes [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] filter_backends [DjangoFilterBackend, filters.OrderingFilter] filterset_fields [spot, rating, visit_date] ordering_fields [created_at, rating, visit_date] ordering [-created_at] def perform_create(self, serializer): # 自动将当前用户设置为作者 serializer.save(authorself.request.user) action(detailTrue, methods[post]) def add_comment(self, request, pkNone): 为某条印象添加评论 impression self.get_object() serializer CommentSerializer(datarequest.data) if serializer.is_valid(): serializer.save(impressionimpression, authorrequest.user) return Response(serializer.data, statusstatus.HTTP_201_CREATED) return Response(serializer.errors, statusstatus.HTTP_400_BAD_REQUEST) class CommentViewSet(viewsets.ModelViewSet): 评论视图集用户可对印象发表评论或删除自己的评论。 queryset Comment.objects.all() serializer_class CommentSerializer permission_classes [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] def perform_create(self, serializer): serializer.save(authorself.request.user)3.4 权限控制 (permissions.py)from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): 自定义权限只允许对象的所有者对其进行编辑其他用户只有读取权限。 def has_object_permission(self, request, view, obj): # 读取权限允许任何请求 (GET, HEAD, OPTIONS) if request.method in permissions.SAFE_METHODS: return True # 写入权限仅允许对象的所有者 # 假设对象有 author 字段 return obj.author request.user四、 系统测试4.1 测试策略与工具单元测试使用 Django 内置的TestCase和APITestCase。API 接口测试使用 Django REST Framework 的APIClient。前端组件测试使用 Jest Vue Test Utils (Vue) 或 Jest React Testing Library (React)。端到端测试使用 Cypress 或 Playwright。性能测试使用 Locust 或 Apache JMeter。4.2 后端单元测试示例 (tests.py)from django.test import TestCase from django.contrib.auth.models import User from rest_framework.test import APITestCase, APIClient from rest_framework import status from .models import ScenicSpot, Impression class ScenicSpotModelTest(TestCase): 景点模型测试 def setUp(self): self.spot ScenicSpot.objects.create( name测试景点, description这是一个测试景点描述, location测试地点, cover_imagespots/covers/test.jpg ) def test_spot_creation(self): self.assertEqual(self.spot.name, 测试景点) self.assertTrue(isinstance(self.spot, ScenicSpot)) self.assertEqual(str(self.spot), 测试景点) class ImpressionAPITest(APITestCase): 印象API接口测试 def setUp(self): self.user User.objects.create_user(usernametestuser, passwordtestpass123) self.client APIClient() self.client.force_authenticate(userself.user) self.spot ScenicSpot.objects.create( nameAPI测试景点, description描述, location地点, cover_imagespots/covers/test.jpg ) def test_create_impression(self): 测试创建印象 url /api/impressions/ data { spot: self.spot.id, title: 一次美好的体验, content: 这里风景很美服务也很好。, rating: 5, visit_date: 2024-10-01 } response self.client.post(url, data, formatjson) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Impression.objects.count(), 1) self.assertEqual(Impression.objects.get().title, 一次美好的体验) self.assertEqual(Impression.objects.get().author, self.user) def test_get_impression_list(self): 测试获取印象列表 # 先创建一条印象 Impression.objects.create( spotself.spot, authorself.user, title测试印象, content内容, rating4, visit_date2024-10-01 ) url /api/impressions/ response self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data[results]), 1) # 假设使用了分页 def test_unauthenticated_user_cannot_create(self): 测试未认证用户无法创建印象 self.client.logout() # 退出登录 url /api/impressions/ data { spot: self.spot.id, title: 匿名尝试, content: 应该失败, rating: 3, visit_date: 2024-10-01 } response self.client.post(url, data, formatjson) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)4.3 前端组件测试示例 (Vue Jest)// ImpressionList.spec.js import { mount } from vue/test-utils import ImpressionList from /components/ImpressionList.vue import { createRouter, createWebHistory } from vue-router describe(ImpressionList.vue, () { it(渲染印象列表, async () { const mockImpressions [ { id: 1, title: 测试印象1, content: 内容1, rating: 5 }, { id: 2, title: 测试印象2, content: 内容2, rating: 4 } ] const wrapper mount(ImpressionList, { props: { impressions: mockImpressions }, global: { plugins: [createRouter({ history: createWebHistory(), routes: [] })] } }) // 检查是否渲染了正确数量的印象项 const items wrapper.findAll(.impression-item) expect(items).toHaveLength(2) // 检查标题是否正确显示 expect(wrapper.text()).toContain(测试印象1) expect(wrapper.text()).toContain(测试印象2) }) it(点击“查看更多”按钮触发事件, async () { const mockImpressions [{ id: 1, title: 测试, content: 内容, rating: 5 }] const wrapper mount(ImpressionList, { props: { impressions: mockImpressions }, global: { plugins: [createRouter({ history: createWebHistory(), routes: [] })] } }) // 模拟按钮点击 await wrapper.find(.view-more-btn).trigger(click) // 检查是否触发了对应的事件 expect(wrapper.emitted(view-more)).toBeTruthy() })