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

资讯详情

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

校园闲置物品交易系统开发实战:Django与Vue前后端联调全记录

校园闲置物品交易系统开发实战:Django与Vue前后端联调全记录 校园闲置物品交易系统开发实录从Django与Flask的二选一到Vue前后端联调把Django Flask Python PyCharm Vue串在同一行里当项目题目的要么是毕设选题表上的固定格式要么就是还没想清楚到底要用哪个框架。这不是嘲玩笑——经常有学弟把这类标题发给我问学长django-flask到底是什么意思是不是要两个框架一起用。答案其实不复杂这是把调研过的、计划使用的技术栈一股脑写进了标题里真正动手前必须做一次选型决策。这篇就围绕一次完整的校园闲置物品交易系统开发过程聊聊我为什么选Django、环境怎么搭、后端核心模块怎么写、Vue前端怎么联调、最后怎么部署给老师和同学演示。无论你是为课设、毕设还是自己练手项目来参考这条链路都应该能帮你少走不少弯路。1. 标题里的django-flask不是组合框架是让你先做减法1.1 拆解项目题目的真实意图很多人拿到这个标题的第一反应是是不是要用Django做后端、Flask做接口两个框架配合起来用我可以明确说对于校园闲置物品交易这种典型的管理系统不要把两个Web框架同时引进来。框架解决的是同一个问题——处理HTTP请求、路由分发、与数据库交互、渲染/返回响应。同时引入Django和Flask相当于请了两个厨师做同一道菜除了让项目依赖膨胀、目录结构变复杂、部署时多一份配置之外没有任何收益。这个标题的本质是一个技术栈罗列式命名。写题目的人把所有预研过的关键词都堆了上去真正要做的项目是一个基于Python后端、Vue前端的校园二手交易平台。所以动手第一步就是确定后端到底选Django还是Flask。我在需求阶段先列了一张功能清单用户注册登录、个人信息维护、商品发布与图片上传、商品列表与分类筛选、商品详情展示、站内留言/联系卖家、交易状态管理在售、已预订、已售出、后台管理用户管理、商品审核、举报处理。这套功能里80%是标准的增删改查剩下20%是权限控制和状态流转。用这个需求清单去比对两个框架答案会非常清晰。1.2 Django与Flask的选型对比我把自己的评估过程简化成一张表方便做决策时直接对照维度DjangoFlask项目结构固定分层项目/应用MTV模式目录清晰灵活自由可以只是一个单文件入口ORM内置ORM自动建表支持迁移不内置需要自行集成SQLAlchemyAdmin后台自带admin注册模型即可管理数据需装第三方插件如Flask-Admin用户认证内置User模型、会话、权限框架需自行设计或集成Flask-Login表单处理内置Form/ModelForm带CSRF防护需集成Flask-WTF适合人群希望开箱即用偏工程化喜欢小而美、自己拼装技术栈学习曲线概念多但一套学完通吃上手快但各种功能要自己找轮子就校园闲置交易系统而言用户系统、后台管理、数据库建模这三块刚需Django都是自带解决的。特别是Django自带的一个成熟后台商品审核、用户封禁、数据统计这些管理功能只要注册模型就能在几分钟内跑起来。如果换Flask前期可以很快写出一个能显示Hello World的项目可一旦进入用户登录、重置密码、权限角色、分页搜索这些环节你会发现所有东西都要一点点搭时间成本全部堆到中期。1.3 MTV模式理解透了Django就算入门了百度热搜词里有个高频问题Django之MTV模式的MTV有什么作用。Django的M是Model模型负责定义数据结构、与数据库交互T是Template模板负责页面展示V是View视图负责业务逻辑——接收请求、调用模型、返回响应。可以这样去理解把Django想成一个餐厅Model是后厨的食材与菜谱数据是什么、怎么存Template是摆盘和菜单样式的设计页面长什么样View是服务员客人点了什么菜服务员去后厨取菜端到客人面前。客人永远直接和View打交道View才知道该拿哪些数据、走哪套流程。拿商品详情页举例用户浏览器请求/item/3/Django先通过URL路由找到对应的View函数View用Item.objects.get(id3)去数据库把商品数据取出来这个操作就是Model在工作。取出后View把数据传递给TemplateTemplate负责把标题、价格、图片渲染成HTML返回给用户。这个链路理解了后面所有接口的开发都是在往这条主线上加细节。2. 开发环境搭建PyCharm、虚拟环境与Vue CLI这些坑一次说清2.1 PyCharm中建项目时容易被忽略的虚拟环境很多新手卡在第一步为什么自己用pip装的Django运行manage.py时提示ModuleNotFoundError。问题几乎都出在用PyCharm新建项目时右下角解释器选到了系统自带Python之后命令行里pip又装了一份包两边各自为战互相看不见。规范做法是用虚拟环境。PyCharm创建项目时New environment using选项选择VirtualenvPyCharm会为当前项目单独生成一个venv目录。之后所有依赖都只装在这个项目内部和环境隔离多项目之间互不干扰。这样操作之后import django到底装没装用项目目录下的python -m django --version来验证而不是在任意终端敲django-admin。关于PyCharm激活的问题顺带说一句社区版完全够用完整体开发流学校信息中心一般也提供正版教育授权没必要在这件事上折腾。一个正常运行的开发环境应该把时间花在写代码和调功能上。2.2 后端依赖管理的规范化项目依赖需要用文件锁住。虚拟环境创建好之后装依赖pip install django pip install django-cors-headers pip install pillow pip install djangorestframework提示pillow是Django处理ImageField图片字段的必备库不装它上传图片功能会直接报错。装完执行pip freeze requirements.txt把依赖清单保存下来。换电脑、换环境时一条pip install -r requirements.txt就能恢复全部环境。这是我建议所有人都养成的基本习惯多少人的项目最后代码发过去了对方跑不起来八成是依赖环境没同步。2.3 Vue前端环境Node版本与Vue CLI前端部分的技术栈是Vue。需要先装Node.js环境建议用LTS版本。Node安装完毕后在命令行里检查node -v npm -v然后全局安装Vue CLI脚手架老牌稳定适合课设/毕设场景npm install -g vue/cli vue --version用脚手架创建前端项目vue create campus-trade-web创建时选Manually select features勾选Router和BabelVue版本选2.x或3.x都行关键是在开发过程中保持一致。项目创建好后npm run serve就能在本地跑起来默认端口是8080。到这里后端Django跑在8000端口、前端Vue跑在8080端口会分别启动。前后端分离架构下跨域问题从这一刻开始找上门来。2.4 跨域问题前后端分离的第一次交锋前端在http://localhost:8080后端接口在http://localhost:8000浏览器会认为这是两个源前端发Ajax请求时默认被拦。解决办法在后端加CORS头。最省事的方式是使用django-cors-headers在settings.py里注册并配置INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOW_ALL_ORIGINS True # 开发阶段用部署时收敛注意CorsMiddleware要放在其他中间件前面尤其是放在CommonMiddleware之前否则某些情况下头信息带不完整。开发阶段可以先全部放开等部署后改成只允许自己的域名访问。3. Django后端核心模块设计从模型建表到对象删除的完整细节3.1 创建App并注册项目内部的职责划分Django的项目下业务模块拆分成一个个App。校园闲置交易系统我分成三个核心Apppython manage.py startapp users # 用户模块 python manage.py startapp goods # 商品模块 python manage.py startapp orders # 订单/交易模块每建一个App都要手动去settings.py的INSTALLED_APPS里注册。漏注册的典型表现为改完模型执行makemigrations提示No changes detected。INSTALLED_APPS [ django.contrib.admin, # ... users.apps.UsersConfig, goods.apps.GoodsConfig, orders.apps.OrdersConfig, ]3.2 用户模型Django内置User加扩展Profile用户功能不需要从零造轮子Django内置的django.contrib.auth.models.User已经涵盖了用户名、密码、邮箱、is_staff、is_superuser等字段。直接复用内置模型登录验证的安全性比自己手写强得多。但校园闲置交易需要一个学号或昵称/头像之类的扩展信息。做法是创建一个Profile模型与User形成一对一关系from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) student_id models.CharField(学号, max_length20, blankTrue) nickname models.CharField(昵称, max_length50, blankTrue) avatar models.ImageField(头像, upload_toavatar/, blankTrue, nullTrue) phone models.CharField(联系电话, max_length20, blankTrue) credit_score models.IntegerField(信用分, default100) def __str__(self): return self.user.usernameOneToOneField保证了一个用户只有一份扩展资料。on_deletemodels.CASCADE的意思是用户被删除时他的Profile也跟随删除不会留下孤儿数据。这块设计想要更省事也可以直接用Django的信号signal在User创建时自动创建Profile但课设项目里用手动保障也够了。3.3 商品模型设计字段类型与状态选择商品是整个系统的核心设计时考虑了几类信息商品本身的属性标题、描述、图片、原价、售价、归属信息发布者、软删除标记、交易状态。参考代码如下from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(分类名称, max_length50) class Meta: verbose_name 商品分类 verbose_name_plural verbose_name def __str__(self): return self.name class Item(models.Model): STATUS_ON_SALE 0 STATUS_BOOKED 1 STATUS_SOLD 2 STATUS_OFF_SHELF 3 STATUS_CHOICES [ (STATUS_ON_SALE, 在售), (STATUS_BOOKED, 已预订), (STATUS_SOLD, 已售出), (STATUS_OFF_SHELF, 已下架), ] title models.CharField(标题, max_length100) desc models.TextField(描述) price models.DecimalField(售价, max_digits8, decimal_places2) original_price models.DecimalField(原价, max_digits8, decimal_places2, blankTrue, nullTrue) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, related_nameitems) seller models.ForeignKey(User, on_deletemodels.CASCADE, related_nameitems) image models.ImageField(商品图片, upload_toitem_images/, blankTrue, nullTrue) status models.IntegerField(状态, choicesSTATUS_CHOICES, defaultSTATUS_ON_SALE) pub_time models.DateTimeField(发布时间, auto_now_addTrue) update_time models.DateTimeField(更新时间, auto_nowTrue) is_deleted models.BooleanField(逻辑删除, defaultFalse) class Meta: ordering [-pub_time] def __str__(self): return self.title这里特意解释几个设计决策price用DecimalField而不是FloatField因为浮点数在计算时会有精度损失涉及钱一定要用定点数。max_digits8, decimal_places2表示最多8位数、小数点后保留两位。status用IntegerField配choices而不是直接用字符串是为了在代码里用常量去判断状态。比如筛选在售商品写Item.STATUS_ON_SALE比写死字符串在售更安全。中文名称在Django Admin和表单里能自动展示模型里只存数字。is_deleted字段是逻辑删除标记。二手物品交易讲究留痕用户删除自己商品时实际上只是打一个标记后台管理员还能看到完整记录。物理删数据这种操作在生产项目里要相当谨慎。3.4 模型迁移与常见报错定义好模型后执行python manage.py makemigrations python manage.py migratemakemigrations会根据模型变化生成迁移文件migrate是把迁移真正应用进数据库。常见报错是图像字段报错ModuleNotFoundError: No module named PIL原因就是没有装pillow。3.5 视图层的CRUD查询与删除对象的正确姿势模型定义好之后最核心的代码落在View上。Django里查询数据库最常用的API有get、filter、exclude、all它们返回的数据类型不同操作上差别很大。先看查询场景。商品列表页需要展示所有在售、未删除的商品from django.http import JsonResponse from django.core.paginator import Paginator from django.views.decorators.csrf import csrf_exempt from .models import Item from django.contrib.auth.models import User def item_list(request): 商品列表接口支持分类和关键词过滤 items Item.objects.filter( statusItem.STATUS_ON_SALE, is_deletedFalse ).select_related(seller) # 分类过滤 category_id request.GET.get(category) if category_id: items items.filter(category_idcategory_id) # 关键词搜索 keyword request.GET.get(q) if keyword: items items.filter(title__icontainskeyword) # 分页 paginator Paginator(items, 12) page request.GET.get(page, 1) page_obj paginator.get_page(page) data { list: [ { id: item.id, title: item.title, price: str(item.price), image: item.image.url if item.image else , seller: item.seller.username, pub_time: item.pub_time.strftime(%Y-%m-%d %H:%M), } for item in page_obj.object_list ], total: paginator.count, page: page_obj.number, pages: paginator.num_pages, } return JsonResponse(data)几个细节值得多说几句filter返回的是QuerySet它具备惰性求值特性。真正执行SQL发生在对结果进行迭代、切片或调用list()时。配合select_related可以提前把外键关联的表join进来避免循环取seller时触发N1次查询。get返回单个模型实例如果查不到会抛DoesNotExist如果查到多条会抛MultipleObjectsReturned所以一般配合try/except使用。title__icontains是Django的字段查询语法icontains表示不区分大小写的包含匹配在SQL中等价于LIKE %kw%。再看删除对象。用户删除自己发布的商品这个操作有权限边界只能删自己的from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.views.decorators.http import require_POST login_required require_POST def item_delete(request, item_id): try: item Item.objects.get(iditem_id) except Item.DoesNotExist: return JsonResponse({code: 404, msg: 商品不存在}, status404) if item.seller_id ! request.user.id: return JsonResponse({code: 403, msg: 无权删除他人商品}, status403) # 逻辑删除 item.is_deleted True item.status Item.STATUS_OFF_SHELF item.save() return JsonResponse({code: 0, msg: 删除成功})这里做的是逻辑删除好处是用户误删可以恢复、管理员留痕可查。如果你想物理删除Item.objects.filter(iditem_id).delete()也支持。但注意delete()在QuerySet上是批量删除在单个实例上执行时效果一样但一定要想清楚是否真的要删数据因为物理删除后数据不可逆而且涉及外键关联的记录也会按CASCADE规则被连带删除。3.6 交易状态流转状态机才是系统的灵魂商品模型里的status字段如果不做约束用户可以随意改来改去交易流程就没法保障。我为这个状态流转画过逻辑这里不用图形用表格说明流转关系当前状态可执行操作操作后状态在售买家下单生成订单已预订在售卖家下架已下架已预订卖家确认交易完成已售出已预订卖家/买家取消交易在售已下架卖家重新上架在售这些判断放在视图层实现if item.status ! Item.STATUS_ON_SALE: return JsonResponse({code: 400, msg: 当前商品不可订购}, status400) # 生成订单、扣减库存前把状态置为已预订 with transaction.atomic(): Order.objects.create(itemitem, buyerrequest.user, priceitem.price) item.status Item.STATUS_BOOKED item.save()用transaction.atomic()事务包裹确保建订单和改商品状态这两步要么同时成功、要么同时回滚不会出现订单建好了商品却还是在售的情况。4. Vue前端页面搭建与前后端联调路由、Interceptor、文件上传与流媒体播放4.1 页面结构与路由设计前端Vue项目的标准目录结构src/ ├── router/index.js # 路由配置 ├── views/ # 页面组件 │ ├── Home.vue # 首页商品列表/瀑布流 │ ├── ItemDetail.vue # 商品详情 │ ├── Publish.vue # 发布商品 │ ├── Login.vue # 登录 │ ├── Register.vue # 注册 │ └── Profile.vue # 个人中心 ├── components/ # 复用组件商品卡片、分页条等 ├── api/request.js # axios封装 └── App.vue路由配置用Vue Router注意懒加载方式import Vue from vue import Router from vue-router Vue.use(Router) export default new Router({ mode: history, routes: [ { path: /, name: Home, component: () import(../views/Home.vue) }, { path: /item/:id, name: ItemDetail, component: () import(../views/ItemDetail.vue) }, { path: /publish, name: Publish, component: () import(../views/Publish.vue), meta: { requiresAuth: true } }, { path: /login, name: Login, component: () import(../views/Login.vue) }, { path: /profile, name: Profile, component: () import(../views/Profile.vue), meta: { requiresAuth: true } } ] })路由守卫里检查登录状态未登录访问个人中心或发布页面自动跳转到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.matched.some(record record.meta.requiresAuth) !token) { next({ path: /login }) } else { next() } })4.2 axios封装与Token处理前端请求后端接口直接每个页面里import axios from axios再写一遍baseURL太原始了。我习惯封装一个request实例import axios from axios const request axios.create({ baseURL: http://127.0.0.1:8000/api, timeout: 10000 }) // 请求拦截器自动带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request这里统一把token放到HTTP请求头里后端通过解析Token来识别当前登录用户。这种设计是前后端分离项目识别你是谁的标准做法比在本地session里存用户ID靠谱得多。4.3 商品发布页与图片上传发布商品时图片上传是容易出问题的环节。前端用FormData对象把文本字段和一个文件一起提交handlePublish() { const formData new FormData() formData.append(title, this.form.title) formData.append(desc, this.form.desc) formData.append(price, this.form.price) formData.append(original_price, this.form.original_price) formData.append(category, this.form.categoryId) if (this.form.image) { formData.append(image, this.form.image) } request.post(/items/, formData, { headers: { Content-Type: multipart/form-data } }).then(res { this.$message.success(发布成功) this.$router.push(/) }) }后端Django接收文件时需要从request.FILES里取csrf_exempt login_required def item_publish(request): if request.method ! POST: return JsonResponse({code: 405, msg: 仅支持POST}, status405) title request.POST.get(title) desc request.POST.get(desc) price request.POST.get(price) category_id request.POST.get(category) image request.FILES.get(image) # 基础校验 if not title or not price: return JsonResponse({code: 400, msg: 标题和价格必填}, status400) try: category Category.objects.get(idcategory_id) except Category.DoesNotExist: category None item Item.objects.create( titletitle, descdesc, priceprice, categorycategory, sellerrequest.user, imageimage, # Django会自动处理上传和存储 ) return JsonResponse({code: 0, data: {id: item.id}})Django端的图片文件存储需要先在settings.py里配置MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)同时在主urls.py里把media目录挂出去from django.conf import settings from django.conf.urls.static import static urlpatterns [...] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这样前端通过http://127.0.0.1:8000/media/item_images/xxx.jpg就能访问到上传的商品图片。4.4 商品图片导出与视频演示StreamingHttpResponse的content_type和content_disposition热搜词里有一个相关问题DjangoStreamingHttpResponse的参数content_type和content-disposition。这个在校园闲置系统里的实际场景是老师要求提交一个系统数据导出功能或者卖家想给买家发送高清大图/演示视频。当后端需要把文件以附件下载方式返回给前端时用StreamingHttpResponse可以边读边发避免大文件占满内存。核心代码from django.http import StreamingHttpResponse import os def download_file(request, file_path): file_path os.path.join(settings.MEDIA_ROOT, file_path) def file_iterator(temp_file_path, chunk_size8192): with open(temp_file_path, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break yield chunk filename os.path.basename(file_path) response StreamingHttpResponse( file_iterator(file_path), content_typeapplication/octet-stream ) # content_disposition要指定attachment同时处理文件名中的空格和中文 response[Content-Disposition] fattachment; filename{filename} return response这里content_typeapplication/octet-stream告诉浏览器这是二进制文件流而不是普通文本必须用下载方式打开。Content-Disposition里的attachment参数是关键没有它浏览器可能会直接尝试在标签页里打开文件。顺便讲一下m3u8相关的流媒体延伸场景。有同学问过Vue播放m3u8该怎么处理这个在二手交易系统里也不是硬需求但如果卖家想上传一个二手电子产品的演示视频选HLSm3u8协议是个不错的选择。做法可以是后端把上传的MP4切片成m3u8用ffmpeg前端用video.js的videojs-contrib-hls插件播放import videojs from video.js import video.js/dist/video-js.css mounted() { videojs(this.$refs.videoPlayer, { sources: [{ src: this.videoUrl, type: application/x-mpegURL }] }) }后端给前端返回m3u8文件路径时同样可以设置content_typeapplication/vnd.apple.mpegurl和Content-Disposition: inline。不过说句实在话课设/毕设项目里做完整视频流媒体是给自己挖坑一个上传成功的图片字段再加一个外部链接字段就够应付大多数场景了。这里点到为止给需要的人一个方向。4.5 联调阶段的三个经典问题前端和后端联调时最常踩的坑我在实际项目里一一体会过第一个是跨域预检请求。前端提交JSON数据时会先发一个OPTIONS请求探路如果后端没配置好CORS前端看到的报错是Access-Control-Allow-Origin缺失。用django-cors-headers后这个基本解决。第二个是字段命名冲突。Python后端习惯用snake_case比如pub_time前端JavaScript习惯用camelCase比如pubTime如果前后端没有约定好一个字段拼错就会让某个页面数据一直为undefined。我的建议是前后端接口文档或者POSTMAN里面统一维护一份字段对照表至少在后端返回时专门构造一个前端友好的数据结构。第三个是Token过期和401刷新。登录功能做好后token过期是必然的。前端响应拦截器收到401时不要只是弹错误要跳转登录页并且携带来源路径登录完成后跳回来这个交互细节能让系统看起来专业不少。5. 从开发到部署waitress加nginx把项目从本机搬到演示环境5.1 为什么不能用runserver应付生产开发阶段跑Django用的是python manage.py runserver这个服务器自带自动重载方便改代码立即生效。但它是一个单线程开发服务器性能上限很低也不够稳定。如果直接把runserver开给老师或同学访问并发请求稍微多一点就卡死甚至有些浏览器会直接连接重置。Django官方文档也明确说了runserver only for development。生产部署需要真正的WSGI服务器。Linux上常用gunicorn或uWSGIWindows上更推荐waitress。这次项目目标环境是Windows服务器/老师演示机用waitress最省心。它不需要编译、不需要额外依赖直接用pip装pip install waitress启动方式waitress-serve --port8000 config.wsgi:application如果配置了settings环境变量也可以写一个启动脚本# server.py或者直接用waitress-serve命令 from waitress import serve from config.wsgi import application serve(application, host0.0.0.0, port8000, threads8)这里threads8指waitress处理请求的线程数。Django耗时的操作主要是数据库查询、文件IO多线程能显著提高并发吞吐。注意waitress默认监听0.0.0.0表示所有网卡本机外也能访问。如果只给本机访问可以改绑127.0.0.1。部署在学校局域网环境里直接绑定0.0.0.0方便其他同学直接通过IP访问。5.2 nginx承担反向代理与静态文件服务waitress把Django应用跑起来了但还需要nginx来做一层反向代理和静态文件处理。校园场景下nginx负责三件事接收外部的HTTP请求默认80端口转发给内部8000端口上的waitress。处理前端Vue打包后的静态文件dist目录。处理Django的media用户上传的图片和staticAdmin后台的CSS/JS。一个基础但完整的nginx配置长这样server { listen 80; server_name your_server_ip_or_domain; # 前端Vue打包文件 root /path/to/vue-project/dist; index index.html; # 前端history路由的兼容配置 location / { try_files $uri $uri/ /index.html; } # 后端API接口 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 用户上传的媒体文件 location /media/ { alias /path/to/django-project/media/; } # Django后台静态文件 location /static/ { alias /path/to/django-project/staticfiles/; } }try_files $uri $uri/ /index.html;这一行是Vue history路由模式部署必须加的。没有它用户直接访问/item/3这种二级路由时nginx找不到对应的物理文件会返回404而加了这行配置它会回退到index.html由Vue Router接管前端路由。5.3 部署前的安全设置清单把项目从开发模式切到部署模式有几项设置必须改# settings.py DEBUG False ALLOWED_HOSTS [your_server_ip, localhost, 127.0.0.1] # 收集静态文件到staticfiles目录 # 执行python manage.py collectstatic STATIC_ROOT os.path.join(BASE_DIR, staticfiles)DEBUG必须关闭否则Django会暴露详细的报错堆栈包含服务器目录结构、数据库连接信息等重要敏感信息。ALLOWED_HOSTS限制了哪些域名/IP可以访问这个服务。CORS配置这时也不能再全放开了改为CORS_ALLOWED_ORIGINS [ http://your_server_ip, ]如果是跨项目之间共享数据库还要检查数据库的DATABASES配置。这些做完基本可以在局域网里流畅演示整个系统了。6. 项目复盘与核心经验模型先行、细节控、信任机制6.1 顺序问题先画模型再写接口最后调页面回看这次开发过程我认为最值得分享的经验是开发顺序千万不要颠倒。最好的顺序是模型先行、接口其次、页面最后。很多人上来就写前端页面等页面写完了才开始建后端表会发现前端要的字段后端根本没有后端建的字段前端又用不上两边来回改时间都耗在返工上。先花半天时间把数据模型画清楚用户表有哪些字段、商品表有哪些字段、订单表关联谁、图片存哪个目录。这一步想清楚后面所有接口和页面开发都会变得按部就班。模型设计的完成度直接决定项目上限。尤其是状态字段status和逻辑删除字段is_deleted最好在模型设计阶段就想好半路加字段虽然在Django里不难加字段后makemigrations即可但涉及到已有数据默认值、历史数据的兼容问题越到后期成本越高。6.2 二手交易系统的核心不是功能是信任技术功能实现完之后我才意识到校园闲置物品交易系统真正要解决的是信任问题。功能可以无限堆商品浏览、发布、订单这些两天就能写完。但系统能否真正被用起来取决于三个隐形的设计第一实名认证。学生之间交易学号、学院这种信息比昵称可信得多。虽然普通用户看不到完整学号但可以展示XX学院、注册时长、完成交易笔数。第二历史交易记录。卖家的历史成交记录和评价能显著降低买家决策门槛。给每个用户一个信用分模型里那个credit_score字段完成一单交易信用分增加被投诉核实后扣分这比任何广告语都有说服力。第三举报与下架机制。商品描述与实际不符、发布违禁品这些情况必须有人处理。Django自带的Admin后台在这里发挥巨大价值商品分类审核、用户封禁几分钟配置就能启用。很多课设/毕设做完就没有然后了就是因为只做了能交易的功能没做敢交易的机制。这两者的差距恰恰是系统价值的分水岭。6.3 扩展方向给后续开发留的卷子项目做出一个可演示的版本后如果你还想继续深入有几个方向值得投入一个是搜索优化。现在title__icontains是模糊匹配数据量大了之后性能会下降。可以引入全文搜索引擎或者用数据库的全文索引再做搜索词联想。一个是消息通知。买家下单、卖家发货、交易完成这些关键节点通过站内信、邮件或者小程序模板消息推送给用户能极大提升活跃度。还有一个是推荐系统。校园交易有很强的社区属性新手教程、书籍、电子产品都是高频品类可以按用户浏览历史和分类偏好在首页做个性化商品推荐。延伸阅读里的热搜词有很多相关内容比如vue和react的区别决定你下次前端选型js深入浅出vue能帮你写出更地道的组件flask后台管理插件如果将来换个更轻量的项目可以参考。从Django延续下去可以学DRFDjango REST Framework序列化器、认证、权限体系会让接口开发效率再次翻倍。最后说一句个人体会。把Django、Flask、Python、PyCharm、Vue都堆在标题里的项目看起来技术栈很全其实每一项都只需要用最核心的那部分。Django用来做成熟稳定的ORM、内置Admin和事务支持Vue用来把页面组件化、管理前端路由PyCharm让调试和虚拟环境管理省心。技术选型上做减法功能设计上做加法你的校园闲置交易系统就不只是一份交差的作业而是一个能真正拿得出手的作品。如果你在开发过程中卡在某个具体的Django查询、Vue联调报错或者部署配置上随时可以把报错信息发出来一起盘一盘。
返回列表