拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django实战:校园闲置物品换购平台源码与远程调试全解析

Django实战:校园闲置物品换购平台源码与远程调试全解析

1. 项目定位与整体方案设计

先说说我为什么选这个题目。读书的时候宿舍里总有那么几样东西,买的时候花了不少钱,用一两个月就不再动了:考研的资料、桌面风扇、蓝牙音箱、甚至路由器。放在闲鱼上还得跟校外的人约时间见面,放在校园群里又容易被消息刷走。后来我把毕业设计直接锁定了这个方向——做一个基于 Python 的校园个人闲置物品换购平台,关键词很明确:Django、Python、源码、远程调试。它不是一个简单的 CRUD 练习,而是一个从注册登录、发布闲置、站内消息到后台管理的完整闭环项目,拿来当毕设既有技术深度,又有场景价值,答辩的时候很容易把需求讲清楚。

1.1 为什么选择 Django 而不是 Flask 或 Spring Boot

如果只是写接口,Flask 确实更轻,但一个毕设项目要覆盖的需求面比较广:用户系统、表单、后台管理、图片上传、消息提醒。这些东西如果用 Flask 做,一半时间会花在找第三方组件和写重复代码上。Django 的好处是全家桶自带,认证模块、Admin 后台、ORM、表单验证、CSRF 防护都是现成的,学习成本也不算高。对于“校园个人闲置物品换购平台”这个场景,Django 的startapp模式可以把用户、物品、消息这些业务隔离得很干净,后续想拆成微服务也有基础。Spring Boot 当然也能做,但 Python 整个开发链更平滑,尤其是配合数据分析、爬虫等周边课程,后面想扩展推荐算法也比较方便。

选 Django 还有一个很现实的原因:网上资料真多。遇到一个问题,搜“django项目实战新手”基本就能找到对应教程,这对毕设时间紧的同学特别友好。整个项目我用的是 Django 4.2,Python 3.10 起步,数据库先用 SQLite,后期需要展示 MySQL 部署时再切,不用改太多业务代码。

1.2 用户角色与功能模块拆解

这个平台一共有三类角色:

  • 游客:可以浏览物品列表、查看物品详情,但不能发布、不能申请换购。
  • 注册用户:能发布闲置物品、上传多张图片、修改联系方式、浏览和搜索物品、发起换购申请、处理收到的申请、发送站内留言、收藏商品、管理个人资料。
  • 管理员:进入 Django Admin 后台,管理用户、分类、物品、举报记录,还能看到每天的发布量与换购完成数量。

换购和普通的二手买卖有个明显区别:用户发布的物品不见得挂价格,标的可能是“拿两本专业书换”“一杯奶茶换”“免费送”。所以我设计了交换意向字段,让用户自己填写想换什么或者期望条件,这在校园内部特别实用。系统里没有支付流程,核心是“信息撮合 + 站内联系”,换不换得成由双方线下见面决定,和真实闲置微信群的需求完全一致。

1.3 项目目录结构与初始化命令

项目名叫campus_exchange,核心 app 叫exchange。初始化命令就三行:

django-admin startproject campus_exchange cd campus_exchange python manage.py startapp exchange

目录结构大致长这样:

campus_exchange/ ├── manage.py ├── config/ │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── exchange/ │ ├── models.py │ ├── views.py │ ├── forms.py │ ├── urls.py │ ├── admin.py │ └── migrations/ ├── templates/ │ ├── base.html │ ├── item/ │ └── user/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ └── media/ └── items/

templates放公共模板和所有页面模板,static放 CSS、JS、图片资源,media是用户上传文件的存放目录。Django 默认把视图逻辑写在views.py里,一个中小型项目完全够用。如果你的项目想用类视图,可以换成ListView、DetailView,代码还能更短。我当时选择函数视图为主,因为毕设要给别人讲清楚每一步请求是怎么走的,函数式更直观。

2. 数据库模型设计与 ORM 深入解析

数据模型是这个项目最核心的部分。设计得不好,后面每个功能都会别扭。我花了两个晚上把模型理清楚,核心就是六个表:用户扩展信息、物品分类、闲置物品、换购申请、站内消息、收藏记录。

2.1 核心模型字段设计

用户信息没有直接改 Django 自带的User表,而是用OneToOneField扩展出Profile。这样做的原因是,Django 自带的 User 表已经处理好了密码哈希、登录状态、权限标记,直接复用是最稳定的做法,随意改表反而容易在迁移时报错。

class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') student_id = models.CharField('学号', max_length=20, blank=True) campus = models.CharField('校区', max_length=50, blank=True) avatar = models.ImageField('头像', upload_to='avatars/%Y%m/', blank=True) intro = models.TextField('个人简介', blank=True)

物品模型是重头:

class Item(models.Model): STATUS_CHOICES = ( (0, '闲置中'), (1, '已被申请'), (2, '换购完成'), (3, '已下架'), ) title = models.CharField('标题', max_length=100) desc = models.TextField('描述') category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True, related_name='items') owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='items') want_exchange = models.CharField('期望换购', max_length=200, blank=True) contact = models.CharField('联系方式', max_length=100) image = models.ImageField('图片', upload_to='items/%Y%m/', blank=True) status = models.IntegerField(choices=STATUS_CHOICES, default=0) created_time = models.DateTimeField(auto_now_add=True)

我用AutoField当主键,没有搞 UUID,因为校园平台没那么大数据量,自增主键查询效率高,写起来也简单。外键关系上,物品和用户是一对多,用on_delete=models.CASCADE,用户删除时他发布的物品也会被删,避免产生孤立的脏数据。分类字段用SET_NULL,因为允许删分类,但删掉之后物品还能正常显示。

换购申请表:

class ExchangeRequest(models.Model): item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='requests') requester = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_requests') message = models.TextField('留言', blank=True) status = models.IntegerField(choices=( (0, '等待确认'), (1, '已同意'), (2, '已拒绝'), (3, '已完成'), ), default=0) created_time = models.DateTimeField(auto_now_add=True)

这里最需要注意的一点:related_name要取好名字,否则后期查询很难受。比如物品的所有者用owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='items'),那么查询某个用户发布的所有物品就是request.user.items.all(),非常直观。

2.2 ORM 查询与删除对象的正确姿势

搜索功能中用得最多的查询就是多条件过滤。比如首页展示所有“闲置中”的物品,排除当前登录用户自己发布的,只取最新的12条:

items = Item.objects.filter(status=0) \ .exclude(owner=request.user) \ .select_related('category', 'owner') \ .order_by('-created_time')[:12]

select_related能提前把外键对象查出来,避免在模板里循环访问item.owner.username时一条条去查数据库,这一行对性能改善很大。搜索时再加入关键词:

keyword = request.GET.get('q', '') if keyword: items = items.filter(Q(title__icontains=keyword) | Q(desc__icontains=keyword))

Q对象可以做“标题或描述包含关键词”的查询,比写两个 filter 简单得多。

“django执行查询-删除对象”也是个高频问题。在 Django 里删除对象最直接的方式是调delete()方法:

item = get_object_or_404(Item, pk=item_id, owner=request.user) item.delete()

调用delete()后,Django 会先把对象取出来,再根据外键关系删除关联数据。如果模型之间有 CASCADE,那所有关联的申请记录、收藏记录也会一起删掉。有一点要提醒:删除是立即生效的,没有便捷的“放入回收站”功能。如果你给物品设计了“已下架”状态,最佳方案是修改status字段而不是真删,这样还能保留历史记录。

2.3 Cookie、Token 与 Django 会话设置

Django 默认的会话是存在数据库里的,会话 ID 通过 Cookie 发送给浏览器,前端拿到的是sessionid这个 Cookie。在实际项目中,我设置了会话有效期,保证同学借电脑登录之后隔几天会被自动踢下线:

# settings.py SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 # 7天 SESSION_SAVE_EVERY_REQUEST = True

在实现“记住我”和免登录刷新 token 的时候,也可以自己给浏览器下发一个自定义 Cookie:

resp = HttpResponseRedirect('/index/') resp.set_cookie('is_login', 'true', max_age=3600, httponly=True, samesite='Lax')

httponly=True防止脚本读 Cookie,samesite='Lax'提供跨站防护。这些点虽然很小,但都是答辩时容易聊出深度的细节。

3. 换购流程、消息通知与实时推送实现

3.1 用 ModelForm 完成发布与校验

一开始有人问,为什么不直接手写表单?因为在 Django 里用ModelForm是最标准的做法。它自动把 POST 数据映射到模型字段,还能复用clean_字段名方法做自定义校验。

class ItemForm(forms.ModelForm): class Meta: model = Item fields = ['title', 'desc', 'category', 'want_exchange', 'contact', 'image'] widgets = { 'desc': forms.Textarea(attrs={'rows': 5}), }

视图里处理发布逻辑:

@login_required def publish_item(request): if request.method == 'POST': form = ItemForm(request.POST, request.FILES) if form.is_valid(): item = form.save(commit=False) item.owner = request.user item.save() return redirect('item_detail', item.pk) else: form = ItemForm() return render(request, 'item/publish.html', {'form': form})

如果图片是必填的,记得在forms.py里加上required=True。另外图片上传前端还要限制大小,这个我放在后面的“踩坑记录”里说。

3.2 换购申请的状态机设计

换购申请流程不是一个单一的按钮事件,而是多个状态之间的切换。我把它设计成一张状态机:

  • 0 等待确认:发起方发出申请后,物品所有者会看到一条新申请。
  • 1 已同意:所有者同意换购,物品自动变为“已被申请”状态。
  • 2 已拒绝:所有者拒绝,物品恢复“闲置中”。
  • 3 已完成:双方线下完成交换后,由其中一方标记为完成。

换购申请的核心视图:

@login_required def create_request(request, item_id): item = get_object_or_404(Item, pk=item_id, status=0) if item.owner == request.user: messages.error(request, '不能换自己的物品') return redirect('item_detail', item_id) ExchangeRequest.objects.create( item=item, requester=request.user, message=request.POST.get('message', '') ) item.status = 1 item.save() notify_owner.delay(item.owner_id, f'你的物品「{item.title}」收到了新的换购申请') return redirect('item_detail', item_id)

这个流程里有一个坑:当发起申请时,我不能直接判断申请是否有效,而是要看物品状态是不是0。如果物品已经被别人申请过了,状态变成1,那么页面上的“申请换购”按钮就不应该显示。模板里判断:

{% if item.status == 0 and item.owner != request.user %} <a href="{% url 'create_request' item.pk %}" class="btn btn-primary dropdown">申请换购</a> {% endif %}

3.3 站内消息与 WebSocket 实时推送

站内消息我做了两层:一是换购双方在申请记录里互相留言,二是独立的私信模块。最简单的方式是每次请求时直接查询消息表,然后渲染在个人中心列表里。但如果想做“后台有新数据,前端自动弹出提醒”,Django 原生的 Request-Response 是做不到的,需要 WebSocket。

技术选型上,Django Channels 是官方推荐的 WebSocket 方案。它把 HttpRequest 扩展到 Channel,配合 Redis 做消息队列。我实际用 Channels 做了一个小的在线通知提醒:

# consumers.py class NoticeConsumer(AsyncWebsocketConsumer): async def connect(self): self.user = self.scope['user'] if self.user.is_authenticated: await self.channel_layer.group_add(f'user_{self.user.id}', self.channel_name) await self.accept() else: await self.close() async def send_notice(self, event): await self.send(text_data=json.dumps(event['data']))

前端 JS 连接:

const socket = new WebSocket('ws://' + window.location.host + '/ws/notice/'); socket.onmessage = function (e) { const data = JSON.parse(e.data); showNotification(data.message); };

不过要提醒一下:WebSocket 会让项目复杂度上升不少。如果做毕设只是为了应付“实时推荐”这个亮点,用 AJAX 轮询 3 秒刷一次未读消息数也完全够用,功能演示效果一模一样。我最终交付版本里保留了两套?没有,为了稳定性,演示环境我就用轮询,源码里带上了 Channels 版本的注释代码,这样文档里写实时推送,现场演示也能展示。这也是答辩加分项。

4. 前端页面、Admin 后台与部署上线

4.1 基于 Django Templates 的页面渲染

前端我没有用很重的前后端分离方案,直接用 Django Templates 加 Bootstrap 5。理由很简单:毕设要能在答辩电脑上快速跑起来,一个python manage.py runserver就能演示全套功能,不需要额外的 Node 服务和跨域解决。模板继承是必须用的,我建了一个base.html,把导航栏、页脚、消息提示都放进去,其他页面只写内容区:

<!-- base.html --> <html> <head> ... 公共 CSS/JS ... </head> <body> <nav> 校园闲置换购平台 </nav> <main> {% block content %}{% endblock %} </main> <footer> ... </footer> </body> </html>

首页的卡片式展示用了 Bootstrap 的row和col-md-4,每个物品卡片显示缩略图、标题、分类和状态标签。列表页和详情页用同样的模板,可以避免重复写 HTML。前端这块别看它简单,适配手机端很重要,因为校园用户基本都拿手机访问。加上viewport和 Bootstrap 栅格,PC 和移动端的演示效果都能打得过。

4.2 Django Admin 后台定制与权限管理

Django 自带的 Admin 是我选择 Django 的重要原因之一。没有它的话,我还得单独写一个后台管理系统,工作量至少多 20 天。只需要在admin.py注册模型,就能获得分类管理、物品审核、用户管理三件套:

@admin.register(Item) class ItemAdmin(admin.ModelAdmin): list_display = ('title', 'owner', 'status', 'category', 'created_time') search_fields = ('title', 'desc') list_filter = ('status', 'category', 'created_time') list_per_page = 20

这样管理员打开后台,就能直接按状态筛选闲置物品,也能搜索关键标题,比手写 Django 查询接口快多了。权限方面我用 Django 自带权限,只有is_staff=True的用户才能进入/admin/。给普通用户只保留自己的操作能力,视图层用@login_required和get_object_or_404双重校验,避免出现越权修改物品的情况。

4.3 从开发到部署的完整流程

演示环境可以这样跑:

python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000

0.0.0.0表示监听所有网卡地址,这样同一校园网内的手机、同学电脑就能通过你的 IP 访问。生产环境部署时,我不会让 Django 直接跑公网,一般用 Gunicorn 启动服务:

pip install gunicorn gunicorn config.wsgi:application -b 0.0.0.0:8000 --workers=3

然后让 Nginx 做前端静态文件和服务转发。注意settings.py里要把ALLOWED_HOSTS改成你的服务器 IP 或域名,否则访问会直接 400。静态文件用python manage.py collectstatic收集到同一目录,再把media目录单独挂出来,图片才能正常显示。

5. 远程调试与全套源码交付心得

这部分是很多买毕设的同学最关心的点。远程调试并不是什么魔法,本质就是把“运行环境”和“编写环境”分开。这里我推荐两种最实用的方式。

5.1 VS Code Remote-SSH 远程调试

如果你本地是 Windows,代码放在老师提供的 Linux 服务器上跑,最稳的方案是 VS Code 的 Remote-SSH 扩展。安装扩展后,按 F1 输入Remote-SSH: Connect to Host,填写服务器的 IP 和用户名,VS Code 会直接打开远程工作区。这时候你本地修改代码,保存后文件直接同步到远端,然后用终端里跑 Django,打断点也能命中远程代码的行号。本地调试就像在服务器上敲代码一样顺滑。

需要注意的几个细节:

  • 服务器要开放 22 端口,且能用密钥或密码登录。
  • 项目依赖包要在远端安装好,不能只在本地装。
  • 断点调试时,远端 Python 解释器必须指向虚拟环境里的 Python,依赖版本才能对应上。

5.2 PyCharm Professional 远程解释器调试

PyCharm 的远程解释器我也用过,体验也不错。在Settings -> Project -> Python Interpreter里选择SSH Interpreter,配置好远端地址和项目路径,PyCharm 会自动把本地代码同步到服务器。用这种方式,本地写完直接点运行,实际上执行环境是服务器。这个方案适合课堂演示,一台服务器挂代码,自己笔记本遥控运行,给老师展示时只要浏览器打开项目地址就行。

远程调试大忌:不要在服务器上用nohup python manage.py runserver &长期跑开发服务器。开发服务器不做并发优化,�一多就崩。正常演示时开个终端跑一下就行,平时练习用这种轻量方式也没关系。

5.3 源码文档与讲解配合

我拿到手的一整套源码里,除了项目本身,还带一份详细的部署文档,内容包括环境安装(Python 3.10、虚拟环境、Django 安装)、数据库初始化、数据导入、默认账号密码、常见报错对照表。文档写得好的话,远程调试的很多问题根本不会出现。我的习惯是:把 README.md 当成核心入口,里面用流程图直接标清楚urls.py -> views.py -> models -> template的关系,这样接手的人不用一头扎进代码里乱转。

6. 常见问题与踩坑记录

这个项目我从开发到调试,前前后后踩了不少坑,列几个频率最高的出来,给后来者避雷。

6.1 常见报错速查表

报错信息原因解决方法
ModuleNotFoundError: No module named 'PIL'上传图片功能依赖 Pillow,未安装pip install Pillow
TemplateDoesNotExist模板路径不在TEMPLATES的DIRS里在settings.py里设置DIRS: [BASE_DIR/'templates']
Forbidden (403) CSRF verification failedPOST 请求缺少 CSRF Token模板表单里加{% csrf_token %}
/media/图片 404MEDIA_ROOT或 URL 没配置配置MEDIA_URL和MEDIA_ROOT,并在项目 urls 里加static(...)
中文变成问号??数据库字符集问题SQLite 不会出现,MySQL 要设utf8mb4
OperationalError: no such table没执行迁移python manage.py migrate

6.2 图片上传与显示问题

ImageField上传的图片保存在MEDIA_ROOT,但浏览器要能访问,必须在settings.py设置好:

MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'

同时在根路由config/urls.py末尾追加:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

另外,大图片对页面加载速度影响很大。我的做法是在前端上传前用 JavaScript 限制文件大小不超过 5MB,后端再调用 Pillow 把图压缩成宽 800 像素的缩略图再保存。代码片段如下:

from PIL import Image def compress_image(image_field, max_width=800): img = Image.open(image_field) if img.width > max_width: ratio = max_width / img.width img = img.resize((max_width, int(img.height * ratio))) img.save(image_field.path, quality=85)

这套逻辑在答辩现场演示时很能体现工程落地能力。

6.3 密码重置与管理员登录问题

如果忘了管理员密码,不要重装系统,Django 有专门的管理命令:

python manage.py changepassword admin

输入两次新密码就能重置。如果连用户名都不确定,可以进 Django shell 查:

python manage.py shell >>> from django.contrib.auth.models import User >>> User.objects.filter(is_superuser=True).values_list('username', flat=True)

6.4 WebSocket 演示时连不上的排查

用 Channels 做实时通知,最常见的问题就是 Redis 没启动或者channel_layer配置不对。跑起来之前先确认redis-cli ping能返回PONG,然后看settings.py里有没有配:

CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {'hosts': [('127.0.0.1', 6379)]}, } }

如果现场没装 Redis,就直接回退到轮询方案。轮询代码逻辑更简单,也不需要额外进程,演示稳定性最高。

一点后续扩展的想法

做完这个项目之后,我自己迭代了几个小功能,其中反馈最好的一个是“物品置顶”,运营同学可以设置置顶标记,首页优先展示。还有“浏览历史”,基于 Cookie 记录最近看过的物品,用户不用靠记忆找回收藏。如果你也拿这个题目做毕设,我强烈建议不只停留在“能跑”层面,把用户画像、物品推荐、信用评价这几个方向里挑一个做延伸,整个项目深度立刻就不一样了。最后说句实在话,远程调试和源码文档这种事,真正上手体验一次以后,你会发现比看十遍视频教程都管用。踩坑踩明白了,答辩的时候才有东西可聊。

返回列表