
1. 项目缘起与整体设计思路1.1 这个旧物置换网站到底解决什么问题每年毕业季高校宿舍楼下的垃圾桶旁总会堆满还能用的台灯、风扇、书架、自行车。另一边刚入学的新生正打算去电商平台下单同款。信息不对称造成的浪费在校园场景里格外扎眼。我做的这个旧物置换网站核心目标就一个让同一片生活半径里的人把闲置物品的供需信息对上。它不是一个泛化的二手交易平台而是聚焦校园或社区场景的轻量级置换系统。用户发布闲置物品浏览他人发布的物品发起置换请求双方协商完成后线下交接。整个流程不涉及资金流转规避了支付接口的复杂度和合规风险对计算机毕设来说功能边界清晰技术栈可控同时又能覆盖用户管理、内容发布、检索筛选、消息交互等典型Web开发模块。适合谁来参考如果你是计算机相关专业的学生正在找一个功能完整但不臃肿的毕设题目这个方向很合适。如果你是有一定Django基础、想练手一个完整CRUD加权限控制的开发者也能从中拿到可复用的代码结构和设计思路。哪怕你只是好奇一个网站从零到一怎么搭起来跟着走一遍也会有收获。1.2 为什么选Django而不是别的框架做毕设选技术栈核心考量三个维度开发效率、文档丰富度、与题目匹配度。Django在这三项上都占优。先说开发效率。旧物置换网站的本质是“内容管理用户交互”这类需求Django几乎是为它量身定做的。自带ORM省去了手写SQL的繁琐Admin后台让你在半小时内就能管理所有数据Auth模块直接提供用户注册登录的完整方案。如果用Flask这些都要自己拼装用Spring Boot光是配置就够喝一壶。再说文档和社区。Django的中文资料在主流框架里算最全的遇到问题搜索“Django关键词”基本都能找到答案。毕设周期通常只有两三个月把时间花在业务逻辑上比花在踩框架坑上划算得多。最后说匹配度。这个项目需要用户系统、权限控制、表单处理、模板渲染、数据库迁移Django的MTV模式把这些串成了一条顺畅的流水线。特别是Admin后台答辩时演示数据管理非常方便老师一眼就能看懂系统在干什么。注意选Django 3.2 LTS或4.2 LTS版本别追最新版。LTS版本维护周期长第三方库兼容性好出问题网上解决方案多。我见过有人用刚发布的版本结果某个依赖库还没适配卡了两天。1.3 功能模块的取舍逻辑一个毕设项目最怕功能贪多嚼不烂。我把功能分成核心模块和扩展模块两层。核心模块是必须有的用户注册登录与个人中心、物品发布与管理、物品列表与详情展示、置换请求发起与处理。这四个模块构成了一个最小可用闭环缺一个流程就跑不通。扩展模块是锦上添花的站内消息通知、物品分类筛选、搜索功能、收藏夹、物品状态追踪。这些可以根据时间余量选择性实现但建议至少做分类筛选和搜索因为答辩时演示效果会好很多。砍掉的功能在线支付、物流对接、评价体系、即时通讯。支付和物流涉及外部接口和资质问题毕设没必要碰评价体系需要大量数据才有意义即时通讯开发成本高用站内留言板替代即可。这种分层设计的好处是即使时间紧张核心模块做完就是一个完整作品时间充裕扩展模块加上去就是加分项。1.4 数据库设计的核心考量数据库表结构是整个系统的骨架。我设计了五张核心表用户表、物品表、置换请求表、消息表、分类表。用户表直接复用Django的AbstractUser额外加一个avatar字段存头像、一个phone字段存联系方式。复用AbstractUser的好处是Auth模块的所有功能都能直接用密码加密、session管理、权限控制都不用自己写。物品表是核心中的核心。字段包括发布者外键、标题、描述、分类外键、成色、期望置换物品描述、图片、状态可置换/已置换/下架、发布时间。这里有个设计决策物品图片是存文件路径还是存Base64我选文件路径因为Base64会让数据库体积膨胀而且查询时传输量大。图片文件存在media目录下数据库只存相对路径。置换请求表记录谁对哪个物品发起了置换、用什么物品换、留言、状态待处理/已同意/已拒绝/已完成。这里要处理一个并发问题同一个物品可能被多人同时发起请求第一个人同意后其他人的请求应该自动失效。我的做法是在同意某个请求时把该物品状态改为“已置换”同时把其他待处理请求批量更新为“已拒绝”。消息表用于站内通知当有人发起置换请求或处理请求时给对方发一条消息。结构简单发送者、接收者、内容、关联物品、是否已读、时间。分类表就是简单的树形结构支持一级分类即可比如“电子设备”“生活用品”“图书教材”“运动户外”。2. 核心功能模块的细节拆解2.1 用户系统不只是注册登录那么简单Django自带的Auth模块能处理注册、登录、登出、密码修改但毕设里通常需要更多。我的做法是继承AbstractUser创建自定义用户模型这样后续想加字段不用改表结构。注册环节除了用户名密码我加了邮箱和手机号字段。邮箱用于找回密码手机号用于线下联系。这里有个坑Django默认的用户名唯一性校验是大小写敏感的意味着“User”和“user”会被当成两个不同用户。我在表单验证里加了一步lower()处理统一转小写再存。登录环节我用了Django的LoginView但自定义了模板和跳转逻辑。登录成功后跳转到物品列表页而不是默认的首页因为用户来这个网站的主要目的就是看东西。个人中心页面展示用户发布的所有物品、收到的置换请求、发出的置换请求、站内消息。这里用到了Django的related_name反向查询比如user.items.all()拿到该用户发布的所有物品。实操心得用户头像上传后记得用Pillow做压缩。我一开始没做用户传了张5MB的照片列表页加载慢得离谱。后来加了压缩逻辑统一缩到300x300文件大小控制在100KB以内加载速度立刻上来了。2.2 物品发布表单设计与图片处理物品发布表单包含标题、描述、分类、成色、期望置换物、图片。标题限制在50字以内描述限制在500字以内防止有人灌水。图片处理是重点。用户可能上传多张图我限制最多3张。上传后做三件事压缩尺寸、生成缩略图、重命名文件。重命名用UUID加时间戳避免文件名冲突和中文文件名导致的编码问题。import uuid from PIL import Image from io import BytesIO from django.core.files.base import ContentFile def process_image(image_file, max_size(800, 800)): img Image.open(image_file) img.thumbnail(max_size, Image.LANCZOS) buffer BytesIO() img.save(buffer, formatJPEG, quality85) new_name f{uuid.uuid4().hex}.jpg return ContentFile(buffer.getvalue(), namenew_name)这段代码做了三件事用thumbnail等比缩放、转成JPEG格式统一处理、用UUID重命名。quality85是在画质和体积之间取的平衡点再低会有明显压缩痕迹再高体积增长快但肉眼看不出来。物品状态我设计了四个可置换、已预定、已置换、已下架。发布时默认“可置换”。当有人发起置换请求且发布者同意后状态变为“已预定”此时其他人不能再发起请求。双方线下完成交接后发布者手动标记为“已置换”。2.3 置换流程状态机设计与并发处理置换流程是整个系统最复杂的部分涉及多个状态流转和并发控制。我用状态机来管理物品状态可置换 → 已预定 → 已置换 请求状态待处理 → 已同意 → 已完成 → 已拒绝 → 已取消当用户A对用户B的物品发起置换请求时创建一个请求记录状态为“待处理”。用户B看到请求后可以选择同意或拒绝。同意时要做三件事把请求状态改为“已同意”把物品状态改为“已预定”把其他待处理请求批量改为“已拒绝”。这三步必须在一个数据库事务里完成否则可能出现一个物品被两个人同时“同意”的情况。from django.db import transaction transaction.atomic def accept_request(request_id): req ExchangeRequest.objects.select_for_update().get(idrequest_id) if req.status ! pending: raise ValueError(该请求已被处理) req.status accepted req.save() item req.target_item item.status reserved item.save() ExchangeRequest.objects.filter( target_itemitem, statuspending ).exclude(idrequest_id).update(statusrejected)select_for_update()是行级锁防止并发请求同时读到“待处理”状态。transaction.atomic保证要么全成功要么全回滚。这个逻辑不写对答辩时老师一问并发场景就露馅了。2.4 搜索与筛选让用户快速找到想要的东西搜索功能用Django的Q对象做多字段模糊匹配支持按标题和描述搜索。筛选支持按分类、成色、状态过滤。from django.db.models import Q def search_items(keyword, category_idNone, conditionNone): qs Item.objects.filter(statusavailable) if keyword: qs qs.filter( Q(title__icontainskeyword) | Q(description__icontainskeyword) ) if category_id: qs qs.filter(category_idcategory_id) if condition: qs qs.filter(conditioncondition) return qs.order_by(-created_at)icontains是大小写不敏感的包含查询比精确匹配实用得多。order_by(-created_at)让最新的物品排在前面符合用户浏览习惯。分页用Django自带的Paginator每页12个物品三列布局刚好四行。分页参数通过GET请求传递这样用户可以分享和收藏搜索结果页。注意搜索关键词要做长度限制和特殊字符过滤。我遇到过有人输入超长字符串导致数据库查询变慢的情况。限制在50字以内并且用strip()去掉首尾空格。3. 实操过程与关键环节实现3.1 环境搭建与项目初始化先确认Python版本建议3.8以上。然后创建虚拟环境这一步别省不然后面依赖冲突会很头疼。python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install django4.2 pillow创建项目和应用django-admin startproject secondhand_project cd secondhand_project python manage.py startapp exchange在settings.py里注册应用配置数据库。开发阶段用SQLite就够了毕设演示完全够用。如果想让答辩更出彩可以换成MySQL但记得装mysqlclient并配置好字符集为utf8mb4。INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, exchange, ] AUTH_USER_MODEL exchange.User MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / mediaAUTH_USER_MODEL这行必须在第一次migrate之前设置好否则后面改起来很麻烦。我见过有人做到一半想换自定义用户模型结果数据库要重建数据全丢。3.2 模型定义与数据库迁移模型定义是整个开发过程中最需要想清楚的部分。我按前面说的五张表来写。from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): avatar models.ImageField(upload_toavatars/, blankTrue, nullTrue) phone models.CharField(max_length20, blankTrue) class Category(models.Model): name models.CharField(max_length50) def __str__(self): return self.name class Item(models.Model): STATUS_CHOICES [ (available, 可置换), (reserved, 已预定), (exchanged, 已置换), (offline, 已下架), ] CONDITION_CHOICES [ (new, 全新), (like_new, 九成新), (good, 七成新), (fair, 五成新), ] owner models.ForeignKey(User, on_deletemodels.CASCADE, related_nameitems) title models.CharField(max_length50) description models.TextField(max_length500) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue) condition models.CharField(max_length20, choicesCONDITION_CHOICES) expect_item models.CharField(max_length200, blankTrue) image1 models.ImageField(upload_toitems/, blankTrue) image2 models.ImageField(upload_toitems/, blankTrue) image3 models.ImageField(upload_toitems/, blankTrue) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultavailable) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)on_delete的选择有讲究。用户删除时他发布的物品用CASCADE一起删掉因为物品脱离发布者没有意义。分类删除时用SET_NULL物品还在但分类置空避免误删分类导致物品丢失。迁移命令python manage.py makemigrations python manage.py migrate python manage.py createsuperuser3.3 视图层函数视图还是类视图Django支持函数视图和类视图两种写法。我的选择是简单逻辑用函数视图复杂逻辑用类视图。物品列表和详情用DetailView和ListView代码量少分页和上下文自动处理。发布和编辑用CreateView和UpdateView表单验证和保存逻辑不用重复写。置换请求的处理用函数视图因为涉及多步操作和事务控制函数视图写起来更直观。from django.views.generic import ListView, DetailView, CreateView from django.contrib.auth.mixins import LoginRequiredMixin class ItemListView(ListView): model Item template_name exchange/item_list.html context_object_name items paginate_by 12 def get_queryset(self): qs Item.objects.filter(statusavailable) keyword self.request.GET.get(q) if keyword: qs qs.filter( Q(title__icontainskeyword) | Q(description__icontainskeyword) ) return qs.order_by(-created_at)LoginRequiredMixin确保只有登录用户能访问未登录自动跳转到登录页。这个Mixin在需要权限控制的视图上都要加。3.4 模板设计让页面看起来不像毕设很多毕设的页面一看就是学生作品问题出在布局和配色上。我的做法是找一个简洁的CSS框架Bootstrap 5就够用然后统一配色方案。主色调选深蓝加白色辅助色用浅灰和橙色。深蓝用于导航栏和按钮橙色用于强调操作如“发起置换”。字体用系统默认字体栈不引入外部字体文件加快加载速度。物品卡片用Bootstrap的card组件图片固定高度200px用object-fit: cover保证不变形。卡片底部放标题、成色标签、发布时间。鼠标悬停时卡片轻微上浮加个transition动画体验感立刻不一样。.item-card { transition: transform 0.2s, box-shadow 0.2s; } .item-card:hover { transform: translateY(-4px); box-shadow: 0 8px 16px rgba(0,0,0,0.1); }响应式布局用Bootstrap的栅格系统大屏三列、中屏两列、小屏一列。这样手机浏览器打开也不会乱。3.5 置换请求的完整实现发起置换请求的视图需要处理几个逻辑不能对自己的物品发起请求、物品必须是可置换状态、同一用户对同一物品不能重复发起请求。login_required def create_request(request, item_id): item get_object_or_404(Item, iditem_id) if item.owner request.user: messages.error(request, 不能对自己的物品发起置换) return redirect(item_detail, pkitem_id) if item.status ! available: messages.error(request, 该物品当前不可置换) return redirect(item_detail, pkitem_id) if ExchangeRequest.objects.filter( requesterrequest.user, target_itemitem, statuspending ).exists(): messages.error(request, 你已经发起过请求请等待对方处理) return redirect(item_detail, pkitem_id) if request.method POST: form ExchangeRequestForm(request.POST) if form.is_valid(): req form.save(commitFalse) req.requester request.user req.target_item item req.save() Message.objects.create( senderrequest.user, receiveritem.owner, contentf对你的物品「{item.title}」发起了置换请求, related_itemitem ) messages.success(request, 置换请求已发送) return redirect(my_requests) else: form ExchangeRequestForm() return render(request, exchange/create_request.html, {form: form, item: item})这段代码里messages框架用于给用户反馈操作结果比alert弹窗体验好。Message对象在请求创建时自动生成接收者在站内消息里能看到通知。4. 常见问题与排查技巧实录4.1 图片上传后不显示这是最常见的问题通常有三个原因。第一MEDIA_URL和MEDIA_ROOT没配置。第二开发服务器的静态文件路由没加。第三模板里图片src写错了。开发环境下需要在urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 其他路由 ] static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)模板里用{{ item.image1.url }}而不是{{ item.image1 }}。如果图片字段可能为空加个判断{% if item.image1 %} img src{{ item.image1.url }} alt{{ item.title }} {% else %} img src{% static img/placeholder.png %} alt暂无图片 {% endif %}踩坑记录我一开始把static路由加在了urlpatterns里面而不是外面结果一直404。static()返回的是一个列表要用号拼接到urlpatterns后面不能直接放在列表里。4.2 数据库迁移冲突改了模型字段后makemigrations报错说检测到多个迁移文件冲突。这种情况通常是因为在不同分支上改了同一个模型合并后迁移文件对不上。解决办法先python manage.py showmigrations看看哪些迁移没应用然后python manage.py migrate --fake把冲突的迁移标记为已应用再手动调整。如果数据不重要最简单的办法是删掉所有迁移文件重新makemigrations但这样会丢失数据只适合开发阶段。预防措施每次改模型前先pull最新代码改完立刻makemigrations并提交不要攒着。4.3 用户登录后跳转错误Django默认登录成功后跳转到/accounts/profile/但这个页面通常不存在。需要配置LOGIN_REDIRECT_URLLOGIN_REDIRECT_URL /items/ LOGIN_URL /login/ LOGOUT_REDIRECT_URL /LOGIN_URL是未登录用户访问受保护页面时跳转的地址。这三个配置不设好用户体验会很混乱。4.4 常见问题速查表问题现象可能原因排查方法解决方案图片上传后404MEDIA配置缺失检查settings和urls加static路由迁移报错迁移文件冲突showmigrationsfake迁移或重建登录后跳转错误重定向配置缺失检查settings设LOGIN_REDIRECT_URL表单提交403CSRF token缺失检查模板加{% csrf_token %}静态文件不加载DEBUGFalse检查settings开发时设DEBUGTrue中文乱码数据库字符集检查MySQL配置用utf8mb4分页链接错误查询参数丢失检查模板保留GET参数4.5 性能优化的小技巧毕设数据量不大但有些优化做了会让系统更流畅。第一列表页用select_related预加载外键关联的用户和分类减少数据库查询次数。第二图片用缩略图而不是原图列表页加载快很多。第三给常用查询字段加数据库索引比如status和created_at。class Item(models.Model): # ... 字段定义 class Meta: indexes [ models.Index(fields[status, -created_at]), ]这个联合索引让“查询可置换物品按时间倒序”的查询走索引而不是全表扫描。数据量小的时候感觉不出来但这是个好习惯。4.6 答辩演示的注意事项答辩演示最怕现场翻车。我的经验是提前准备好演示数据至少10个物品、3个用户、5条置换请求覆盖各种状态。演示流程提前走三遍确保每一步都顺畅。演示时重点展示核心流程注册登录→发布物品→浏览搜索→发起置换→处理请求→站内消息。每个环节用不同账号切换演示让老师看到完整的交互闭环。如果老师问技术难点重点讲置换请求的并发处理和图片压缩逻辑这两个点既有技术含量又容易讲清楚。如果问扩展性可以说后续可以加即时通讯、评价体系、推荐算法但毕设阶段聚焦核心功能。最后分享一个小技巧演示前把数据库里的测试数据导出为fixture文件万一现场数据乱了一条命令就能恢复。命令是python manage.py dumpdata backup.json恢复用python manage.py loaddata backup.json。这个习惯在开发阶段也很有用改坏了随时回滚。4.7 代码组织与可维护性毕设代码虽然不用像生产项目那么规范但组织好一点自己改起来也方便。我的做法是按功能分文件models.py放所有模型views.py按功能拆成views_item.py、views_user.py、views_request.pyforms.py放所有表单urls.py按应用拆分。模板放在templates/exchange/目录下按功能命名item_list.html、item_detail.html、item_form.html、request_list.html。base.html放公共的导航栏和页脚其他模板继承它。静态文件放在static/目录下css、js、img分开放。不要把所有样式写在模板里抽到独立的css文件改起来方便。这种组织方式在项目变大时优势明显。我见过把所有视图写在一个views.py里超过2000行的毕设改一个功能要翻半天调试起来很痛苦。