ARTICLE DETAIL

资讯详情

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

基于Django的校园线上文印店平台开发实践

基于Django的校园线上文印店平台开发实践 在学校做毕业设计那阵子我几乎每周都要去后门那家打印店报到。店里的老式电脑插着三四个U盘传输速度全看运气遇到PDF要排队等论文改到第8版的时候老板已经能背出我的学号了。排队、拷文件、改格式、反复确认份数这些消耗时间的环节其实完全可以在线上消化掉。后来我在做自己的Python课程设计时决定动手实现一个基于Django的线上文印店平台把校园打印店的整个业务流程搬到浏览器里用户上传文档、设置打印参数、在线支付店家在后台接单、处理、通知取件。项目做完之后不少学弟学妹问我要思路也有打算接毕业设计的人想参考。这篇就把我完整的设计过程、代码结构、部署经验和踩过的坑都整理出来给想做类似平台项目的同学一个可复用的参考。1. 校园打印店的真实痛点这个平台到底在解决什么问题很多人在做Web项目时容易犯一个毛病就是拿到题目就写代码结果功能写了一堆却说不清楚自己到底在解决什么问题。而实际去校园打印店蹲点半天你会发现业务链条上的问题远不止排队这么简单。1.1 线下打印店的三类典型低效场景第一类低效出现在文件传输环节。学生带着手机或U盘到店里接上电脑Windows弹窗提示是否扫描并修复然后等待系统识别如果是Mac用户U盘格式可能是APFSWindows电脑直接不认。就算文件顺利拷进电脑打开Word、PPT、PDF的过程又慢动不动还需要登录账号同步云盘。我观察过高峰期平均每个学生光文件到电脑这一步就耗时3到5分钟而生化的打印执行时间往往不超过1分钟。第二类低效出现在参数确认环节。打印双面还是单面黑白还是彩色份数多少打印店老板靠嘴巴问学生靠手指比划周围还吵。最怕遇到论文打印正反、页码、装订这些反复确认后面排队的人干着急。第三类低效是售后追溯。打印错了谁的责任说不清楚。学生说我明明要双面老板说你只说打印结果只能重打成本自己吞。这个场景在线上系统里就是一张清晰的订单记录。1.2 为什么校园场景适合做线上平台校园是一个相对封闭但密度极高的服务场景。两三千人住在同一个园区打印需求高频、刚需、低客单价单次几块钱但量大。这种场景天然适合做预约制与线上化改造学生人还没到店订单已经提交店家提前把文件处理好到店直接取件把等待时间从客户侧转移到商家侧。同时校园用户对数字化工具的接受度高。不需要教他们怎么用只要做一个上传按钮、一个参数选择、一个支付页面他们自己就能搞清楚。这也大大降低了平台的推广成本。1.3 平台的功能边界哪些必须做哪些先不做做项目最忌讳的就是贪大。我在设计之初就明确了这个MVP最小可行产品的功能边界。必须做的核心功能用户注册、登录区分学生和管理员文件上传支持PDF、Word、PPT、JPG等常见格式打印参数设置黑白/彩色、单双面、份数、纸张尺寸、装订方式在线价格计算与下单订单状态跟踪待处理、打印中、已完成、已取件管理员后台订单管理、文件预览、价格设置取件通知与取件码暂不做的长尾功能在线支付对接课程设计阶段先用模拟支付店铺多门店管理复杂的权限角色体系移动端原生App这样划分之后项目的技术栈和代码量就清晰了也更容易在有限时间内部署上线而不是在过度设计里绕圈。2. 技术选型的底层逻辑为什么骨架选择了Django选框架这件事向来不是哪个火选哪个而是哪个适合自己当前的处境。既然题目已经限定了Python和Django我就在这个前提下讲讲我的落地考虑也补充一下环境搭建时容易忽略的细节。2.1 Django相对于Flask、FastAPI等框架的优势点我在做这个项目前也用过Flask写小工具但一到这种多模块、前后台分离不了的小完整业务系统时Django的优势就体现出来了。自带Admin后台文印店平台天然需要一个商家管理端。如果自己从零写列表、表单、增删改查光这部分至少要一周。Django Admin只要注册一下模型几分钟就能获得一个能用的管理后台。User认证体系开箱即用用户的注册、登录、会话管理、密码加密Django的auth应用全部内置了。在校园打印店这个场景里用户就是学生不需要复杂的RBAC权限模型直接用自带的User表配合分组就能搞定。ORM贴近业务直觉订单、用户、文件的关系用Python类定义迁移到MySQL根本不用手动写SQL。对于以业务为核心关注点的项目来说这种开发效率差异是决定性的。MTV架构职责清晰Model负责数据结构Template负责页面渲染View负责业务逻辑。课程设计答辩时老师一看你的目录结构就觉得你软件工程素养在线。Flask不是不能做但它更像一个组装机——路由、ORM、Admin、表单验证全要你自己选型和拼装适合用来练手底层原理做完整的商业系统偏累。FastAPI适合前后端分离的API服务如果产品形态是小程序后端API那它很合适但传统Django模板渲染的方案对单体应用更直接。2.2 虚拟环境与项目脚手架搭建记录网上关于Python安装的教程很多了我默认读者已经装好了Python 3.10及以上版本。这里重点说一下虚拟环境很多新手在这里栽过跟头。# 创建项目目录并进入 mkdir print_shop_project cd print_shop_project # 创建虚拟环境python3自带venv python3 -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装Django及依赖 pip install django4.2 pillow mysqlclient # 创建Django项目与应用 django-admin startproject printsys . python manage.py startapp printshop我要特别提醒一句pip install django 4.2中间不要加空格不然安装包名解析会出错报No matching distribution found。这个问题我在多个机器上帮人排查过基本全是这个原因。创建完项目后settings.py里有两个地方我建议第一时间改掉# settings.py # 允许所有主机访问开发阶段方便局域网测试 ALLOWED_HOSTS [*] # 配置语言与时区 LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ True2.3 数据库选型从SQLite到MySQL的切换路径课程设计阶段我直接用Django默认的SQLite零配置、开箱即用对单机开发完全够用。等到了部署阶段我切换到MySQL毕竟线上环境并发处理和稳定性更好。切换方式很简单安装mysqlclient后修改settings.py# database配置 DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: printshop, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }迁移时执行python manage.py makemigrations和python manage.py migrate即可。有一个细节MySQL默认字符集在5.7版本下可能是latin1中文标题容易变乱码。所以在创建数据库时要显式指定字符集CREATE DATABASE printshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;我用的是utf8mb4而不是utf8因为utf8mb4支持表情符号和更多特殊字符用户上传文件如果文件名里有emoji也不会出问题。3. 数据模型设计订单、用户、文件怎么组织才不乱数据库设计是整个平台的骨架模型建得合理后面写View和Template的时候会非常省力。我按照用户、文件、订单、店铺配置四个核心维度设计了模型具体如下。3.1 用户模型直接复用Django内置User还是新建单独表我选择复用Django内置的User模型。校园平台用户属性很简单学号、姓名、联系方式、角色。内置User表已经包含username、password、first_name、last_name、email再通过OneToOneField扩展个人资料即可from django.db import models from django.contrib.auth.models import User class Profile(models.Model): # 与内置User一对一关联 user models.OneToOneField(User, on_deletemodels.CASCADE, verbose_name用户) student_id models.CharField(max_length20, verbose_name学号) phone models.CharField(max_length11, blankTrue, verbose_name手机号) id_card models.CharField(max_length18, blankTrue, verbose_name身份证后6位) # 用户角色0为学生1为管理员 role models.IntegerField(choices((0, 学生), (1, 管理员)), default0) def __str__(self): return f{self.user.username} - {self.student_id} class Meta: verbose_name 用户资料 verbose_name_plural verbose_name至于身份验证中的身份证后6位我本来想做取件时的二次身份确认后来发现实际上用取件码就够了这个字段变成了扩展储备字段。这也算是一个经验不要急着把功能都做进系统先做核心路径等核心路径跑通后再考虑增强。3.2 文件模型上传的文件路径、大小、格式如何约束用户上传的打印文件是整个业务的核心对象。这个模型不只需要记录路径还需要记录元信息方便后台预览和价格计算。import os def file_path(instance, filename): 按用户ID分目录存储避免文件名冲突 ext filename.split(.)[-1] return fuser_{instance.user.id}/{filename} class PrintFile(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name上传用户) file models.FileField(upload_tofile_path, verbose_name文件) file_name models.CharField(max_length255, verbose_name原始文件名) file_type models.CharField(max_length10, verbose_name文件类型) file_size models.IntegerField(default0, verbose_name文件大小/KB) page_count models.IntegerField(default1, verbose_name页数) md5 models.CharField(max_length32, blankTrue, verbose_name文件MD5值) upload_time models.DateTimeField(auto_now_addTrue, verbose_name上传时间) def save(self, *args, **kwargs): # 保存前自动获取文件信息 self.file_name self.file.name self.file_size int(self.file.size / 1024) self.file_type os.path.splitext(self.file.name)[1].lower() super().save(*args, **kwargs)页数字段的自动获取是用了pypdf库来读取PDF页数from pypdf import PdfReader def get_pdf_page_count(file_path): try: reader PdfReader(file_path) return len(reader.pages) except Exception: return 1Word和PPT的页数准确读取比较麻烦需要comtypes这类Windows专用库。我当时的妥协方案是PDF文件精确计算页数其他格式统一按1页处理等实际打印时商家在后台手动修正。这也符合先跑通主流程的开发原则。3.3 订单模型状态机设计是整个平台的关键订单表是核心中的核心它贯穿了整个业务流程。我用一个整型字段表示状态而不是用字符串这样在Django模板和前端JS里做判断都更简洁。class Order(models.Model): # 状态枚举定义 STATUS_CHOICES ( (0, 待处理), (1, 打印中), (2, 已完成), (3, 已取件), (4, 已取消), ) order_no models.CharField(max_length32, uniqueTrue, verbose_name订单号) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name下单用户) files models.ManyToManyField(PrintFile, throughOrderFile, verbose_name关联文件) # 打印参数 color_type models.IntegerField(choices((0, 黑白), (1, 彩色)), default0, verbose_name颜色类型) duplex models.BooleanField(defaultTrue, verbose_name是否双面) copies models.IntegerField(default1, verbose_name打印份数) paper_size models.CharField(max_length10, defaultA4, verbose_name纸张尺寸) binding models.BooleanField(defaultFalse, verbose_name是否需要装订) # 价格与状态 total_price models.DecimalField(max_digits8, decimal_places2, verbose_name总价) status models.IntegerField(choicesSTATUS_CHOICES, default0, verbose_name状态) pickup_code models.CharField(max_length6, verbose_name取件码) remark models.CharField(max_length255, blankTrue, verbose_name备注) created_at models.DateTimeField(auto_now_addTrue, verbose_name下单时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) def __str__(self): return self.order_no class OrderFile(models.Model): 中间表关联订单和文件并可记录每个文件的实际打印页数 order models.ForeignKey(Order, on_deletemodels.CASCADE) file models.ForeignKey(PrintFile, on_deletemodels.CASCADE) actual_pages models.IntegerField(default0, verbose_name实印页数)这里我要强调一下为什么要用ManyToManyField而不是单文件外键因为一个订单可以包含多个文件。比如学生一次上传了论文正文和封面它们应该在同一个订单里一起打印方便统一结算和取件。order_no的生成规则我用的是时间戳加随机数import time import random def generate_order_no(): ts time.strftime(%Y%m%d%H%M%S) rand random.randint(1000, 9999) return fP{ts}{rand}这个规则简单够用避免重复并且一眼能看出订单大致创建时间。3.4 价格配置模型别把单价写死在前端价格计算是打印店平台的核心逻辑如果把黑白单面单页0.1元写死在代码里以后搞活动、调价就要改代码重新发布非常不优雅。我把价格体系设计成了数据库模型管理员在后台可以直接修改。class PriceConfig(models.Model): # 用配置键区分不同计费项 item models.CharField(max_length50, uniqueTrue, verbose_name计费项目) price models.DecimalField(max_digits5, decimal_places2, verbose_name单价/元) # 初始化示例数据 PriceConfig.objects.create(itemblack_white_single, price0.10) PriceConfig.objects.create(itemblack_white_double, price0.20) PriceConfig.objects.create(itemcolor_single, price0.50) PriceConfig.objects.create(itemcolor_double, price1.00)这是我在架构设计上的一个关键决策把变化的东西全部配置化。价格、装订费、纸张类型、开放时间凡是可能变化的业务参数都应该存数据库而不是硬编码。这个习惯在后续项目的维护中帮我省了大量的时间。4. 核心业务闭环从上传文档到取件完成的完整代码路径数据模型搭好了接下来就是业务逻辑的拼装。这一部分是平台最核心的代码路径——用户上传文件、选择参数、系统计算出价格、生成订单、用户确认支付、商家后台更新状态、用户取件。我按模块拆开讲。4.1 文件上传接口大小限制、格式校验、中文文件名文件上传是用户体验的第一环也是最容易出问题的一环。中文文件名在上传到服务器后经常变成乱码或者被浏览器的默认行为搞出奇怪后缀。我的处理方案是在views.py做完整拦截import os from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from django.conf import settings login_required def upload_file(request): if request.method POST: uploaded_file request.FILES.get(file) if not uploaded_file: return render(request, printshop/upload.html, {error: 请选择文件}) # 校验文件大小限制50MB if uploaded_file.size 50 * 1024 * 1024: return render(request, printshop/upload.html, {error: 文件不能超过50MB}) # 校验文件格式 ext os.path.splitext(uploaded_file.name)[1].lower() allowed_ext [.pdf, .doc, .docx, .ppt, .pptx, .jpg, .jpeg, .png] if ext not in allowed_ext: return render(request, printshop/upload.html, {error: 不支持的文件格式}) # 保存文件对象 file_obj PrintFile( userrequest.user, fileuploaded_file, file_nameuploaded_file.name ) file_obj.save() return redirect(order_create, file_idfile_obj.id) return render(request, printshop/upload.html)注意我用的是require_POST以外的表单渲染方式直接用request.method POST判断就能在同一个View里兼容GET和POST减少代码量。Django的FileField自动保存文件到MEDIA_ROOT配置的目录我们只需要设置# settings.py MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)有了这个配置上传的文件就会保存到项目根目录的media文件夹下。4.2 下单页面打印参数选择与实时价格计算文件上传成功后跳转到下单页面用户在这里设置打印参数并实时看到价格变化。价格计算逻辑放在后端前端通过AJAX接口获取最新价格。下单页面的核心逻辑login_required def order_create(request, file_id): file_obj PrintFile.objects.get(idfile_id, userrequest.user) if request.method POST: # 获取打印参数 color_type int(request.POST.get(color_type, 0)) duplex request.POST.get(duplex, 1) 1 copies int(request.POST.get(copies, 1)) binding request.POST.get(binding, 0) 1 # 计算价格 price_obj None if color_type 0 and not duplex: price_obj PriceConfig.objects.get(itemblack_white_single) elif color_type 0 and duplex: price_obj PriceConfig.objects.get(itemblack_white_double) elif color_type 1 and not duplex: price_obj PriceConfig.objects.get(itemcolor_single) else: price_obj PriceConfig.objects.get(itemcolor_double) total_price price_obj.price * file_obj.page_count * copies if binding: total_price 2 # 装订费2元可用PriceConfig扩展 # 生成订单 order Order.objects.create( order_nogenerate_order_no(), userrequest.user, color_typecolor_type, duplexduplex, copiescopies, paper_sizeA4, # 暂时只支持A4 bindingbinding, total_pricetotal_price, status0, pickup_codestr(random.randint(100000, 999999)) ) # 通过中间表关联文件 OrderFile.objects.create(orderorder, filefile_obj) return redirect(order_detail, order_idorder.id) return render(request, printshop/order_create.html, {file: file_obj})我在这里遇到了一个新手常犯的坑Django Template里直接用Jinja2语法访问Python三元表达式是会报错的。如果要在模板里根据条件显示不同价格文字我建议把价格计算的完整结果先组织成一个字典传到模板而不是在模板里做逻辑。4.3 模拟支付流程实际效果与课程设计目标匹配真正的在线支付需要对接微信支付/支付宝需要商户资质和回调服务器在课程设计阶段不实际。我的做法是做一个模拟支付页面用户确认订单后点击确认支付系统模拟支付成功并更新订单状态。login_required def order_pay(request, order_id): order Order.objects.get(idorder_id, userrequest.user) if order.status ! 0: return render(request, printshop/order_detail.html, {order: order, error: 订单状态异常}) if request.method POST: # 模拟支付成功 order.status 1 # 直接进入打印中状态 order.save() return redirect(order_detail, order_idorder.id) return render(request, printshop/order_pay.html, {order: order})在学生项目的答辩环节这种模拟支付是可以接受的因为核心业务逻辑在于打印订单的管理闭环而不是支付本身。如果想让项目更有加分项可以在支付页面写一个支付成功后自动回调的模拟函数展示一下对支付回调机制的理解。4.4 商家后台Django Admin定制与自定义管理视图Django自带Admin后台能提供80%的管理功能但做打印店平台时我还想更直观地看到订单状态的流转所以在Admin里做了两个增强显示订单列表的自定义字段以及支持批量操作。from django.contrib import admin from .models import Order, PrintFile, OrderFile, PriceConfig admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display [order_no, user, total_price, status, pickup_code, created_at] search_fields [order_no, user__username] list_filter [status, color_type, duplex] actions [mark_as_printing, mark_as_completed] def mark_as_printing(self, request, queryset): queryset.update(status1) mark_as_printing.short_description 标记为打印中 def mark_as_completed(self, request, queryset): queryset.update(status2) mark_as_completed.short_description 标记为已完成这样商家在后台选中订单一键就能切换状态不用一个个打开再改下拉框了。除了Admin我还写了一个自定义的今日订单看板页面统计今日订单数、总流水、待处理数量等方便商家一打开系统就知道当前状况。用Django的ORM聚合查询很容易实现from django.db.models import Sum, Count from datetime import date def dashboard(request): today date.today() today_orders Order.objects.filter(created_at__datetoday) context { today_count: today_orders.count(), today_amount: today_orders.aggregate(Sum(total_price))[total_price__sum] or 0, pending_count: Order.objects.filter(status0).count(), complete_count: Order.objects.filter(status2).count(), } return render(request, admin_dashboard.html, context)4.5 取件确认流程取件码的生成、展示与核销取件码是一个很实用的细节。我采用6位数字码随机生成在订单详情页展示给用户。学生到店后报取件码商家在后台输入核销订单状态变为已取件。实现如下def pickup_confirm(request, order_id): if not request.user.is_staff: return redirect(login) order Order.objects.get(idorder_id) if request.method POST: input_code request.POST.get(pickup_code, ) if input_code order.pickup_code: order.status 3 order.save() return render(request, printshop/pickup_success.html, {order: order}) else: return render(request, printshop/pickup_fail.html, {order: order})每次生成订单时我用random.randint(100000,999999)生成取件码这个码在后台管理页面上直接展示。一开始也考虑过用二维码但打印店的场景里学生到店报6位数字更直接二维码还得掏手机扫码多一道操作。5. 开发过程中踩过的几个典型坑与排查链路每个Django项目都有那么几个明明照着文档写却怎么都不对的时刻。这里我整理几个印象最深的坑和完整的排查思路帮你省掉几天的排查时间。5.1 图片上传的MEDIA_URL与静态文件404问题第一个坑出现在本地开发调试时。上传文件成功后访问http://127.0.0.1:8000/media/user_1/test.pdf总是404而Media文件确实存在于项目目录下的media文件夹里。排查链路是这样的首先检查settings.py中MEDIA_URL和MEDIA_ROOT是否配置确认无误然后检查URLconf里是否配置了静态文件服务。问题出在最后一步。Django默认开发服务器不会自动服务MEDIA_URL下的文件需要手动在urls.py里加一行from django.conf import settings from django.conf.urls.static import static urlpatterns [ # ... 其他URL配置 ] static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)加上之后访问/media/xxx就正常了。而部署到生产环境后又出现了新的404问题因为Nginx接管了静态文件服务必须在Nginx配置里加location指向media目录这个我在部署章节会详述。5.2 用户认证与CSRF的冲突表单提交的403错误第二个坑是Django表单提交时经常遇到的403 CSRF validation failed。我一开始写登录表单时没有加{% csrf_token %}模板标签一提交就报错。Django的CSRF中间件默认开启对所有POST请求都要校验csrftoken。解决方案就是在Django模板里的每个form内部加上form methodpost {% csrf_token %} !-- 表单内容 -- /form如果你用的是AJAX方式提交那么还需要从cookie中获取CSRF token并放到请求头里// jQuery方式 $.ajaxSetup({ beforeSend: function(xhr, settings) { function getCookie(name) { let cookieValue null; if (document.cookie document.cookie ! ) { const cookies document.cookie.split(;); for (let i 0; i cookies.length; i) { const cookie cookies[i].trim(); if (cookie.substring(0, name.length 1) (name )) { cookieValue decodeURIComponent(cookie.substring(name.length 1)); break; } } } return cookieValue; } xhr.setRequestHeader(X-CSRFToken, getCookie(csrftoken)); } });这个坑对所有Django新手都是必经之坎我当时也是翻了好几个Stack Overflow帖子才彻底搞明白。5.3 中文文件名乱码与Content-Disposition编码问题第三个坑是用户上传毕业论文终版最终版8.0.pdf时文件存到磁盘后变成了一串百分号编码。这是Django自带的get_valid_filename处理逻辑它会将非ASCII字符转成URL编码。我一开始尝试覆写这个函数但在多端测试时发现Windows和Linux的路径处理逻辑有细微差别。索性就换了一个思路保存文件时用uuid4().hex作为存储文件名把原始文件名记录下来展示用。这样既避免中文乱码也防止了路径遍历攻击。import uuid def file_path(instance, filename): ext filename.split(.)[-1] new_name f{uuid.uuid4().hex}.{ext} return fuser_{instance.user.id}/{new_name}然后保存时在file_name字段单独记录原始文件名。这个方法后续使用非常稳定再没出过乱码。5.4 订单状态流转中的并发问题重复支付的边界情况第四个坑是在模拟支付环节。如果用户手快连续点击两次确认支付后端会生成两个订单还是只生成一个最简单的方案是在订单创建时用事务唯一约束from django.db import transaction login_required def order_create(request, file_id): with transaction.atomic(): # 检查同一个文件是否已有未完成订单 existing_order OrderFile.objects.filter( file_idfile_id, order__status__in[0, 1, 2] ).exists() if existing_order: return render(request, printshop/order_create.html, {file: file_obj, error: 该文件已有处理中的订单请勿重复下单}) # ... 创建订单逻辑通过事务包裹配合exists()前置校验把重复下单的问题挡在了入口处。这在实际运行中确实有效至少我没再遇到同份文件生成两个订单的情况。6. 从SV到线上部署到Linux服务器的一次完整记录项目开发完成后面临的下一关是部署。网上关于Django部署的教程很多但大多是零散的步骤我整理了一个能够从头跑通的完整部署流程。6.1 部署方案选型Nginx Gunicorn与宝塔面板的取舍部署Django有两条主流路线一条是传统的手动配置Nginx Gunicorn Supervisor适合想理解底层原理的人另一条是用宝塔面板这类可视化工具适合快速上线。我的选择是Nginx Gunicorn手动部署因为课程设计答辩时老师一定会问你怎么部署的能说出每一层的职责会明显加分。另外手动部署排查问题也更方便不至于宝塔面板UI上一串眼花缭乱的按钮。当然如果你在时间紧张又想快速上线用宝塔面板也完全可行它本质上也是调用Nginx、Gunicorn、Supervisor这些组件只是用图形化界面去配置。6.2 服务器环境准备与项目代码同步我用的是阿里云轻量应用服务器Ubuntu 22.04系统2核2G配置对于一个小型校园平台完全够用。# 更新系统 apt update apt upgrade -y # 安装python3、pip、venv apt install python3-pip python3-venv nginx -y # 安装MySQL apt install mysql-server -y systemctl start mysql systemctl enable mysql # 创建项目目录并拉取代码 mkdir -p /var/www/printshop cd /var/www/printshop # 创建虚拟环境并安装依赖 python3 -m venv venv source venv/bin/activate pip install django4.2 pillow mysqlclient gunicorn pypdf代码上传的方式我推荐用Git仓库管理或者在服务器上用scp命令直接传文件。如果代码文件不大scp -r是最快的scp -r print_shop_project root你的服务器IP:/var/www/printshop/准备好后测试项目是否能启动python manage.py collectstatic --noinput python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000浏览器访问http://服务器IP:8000如果能打开页面说明基础环境正常。6.3 Gunicorn配置与常驻进程管理runserver只能用来开发调试生产环境必须用Gunicorn这类WSGI服务器。Gunicorn使用简单gunicorn --bind 0.0.0.0:8000 printsys.wsgi:application但这样直接运行终端一关服务就停了。需要配合Supervisor来管理进程apt install supervisor -y创建配置文件/etc/supervisor/conf.d/printshop.conf[program:printshop] directory/var/www/printshop command/var/www/printshop/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 printsys.wsgi:application autostarttrue autorestarttrue stderr_logfile/var/log/printshop.err.log stdout_logfile/var/log/printshop.out.log更新配置并启动supervisorctl reread supervisorctl update supervisorctl start printshop supervisorctl status printshop这里我特意用了3个workers而不是默认的1个。对于Django项目公式一般是2 * CPU核心数 12核服务器用3个worker比较稳妥。6.4 Nginx反向代理与静态文件处理Nginx在这里干两件事一是接收外部请求转发给Gunicorn二是托管静态文件和媒体文件。server { listen 80; server_name 你的域名或服务器IP; location /static/ { alias /var/www/printshop/staticfiles/; } location /media/ { alias /var/www/printshop/media/; } location / { 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; } }重新加载Nginxnginx -t systemctl reload nginx这里有一个必须要注意的点不要用proxy_pass http://0.0.0.0:8000一定要用127.0.0.1:8000。如果Gunicorn监听在0.0.0.0:8000它会对公网开放存在安全风险。正确做法是Gunicorn只监听内网回环地址127.0.0.1外网访问全部通过Nginx中转。部署完成后访问http://服务器IP登录后台、上传文件、下单一路测试过去。我在这个环节又踩了一个坑collectstatic没有执行后台样式全是403。重新执行python manage.py collectstatic --noinput后静态文件正常加载后台页面才变好看。6.5 部署后的性能调优与安全加固线上部署后我发现首次请求耗时较高特别是上传文件时每次都要读磁盘。做三个调整后性能提升明显压缩上传图片在校验文件时增加Pillow的Image.verify()检测损坏的图片文件避免恶意上传损坏文件导致后台崩溃。安全配置在settings.py中关闭DEBUG模式并配置ALLOWED_HOSTSDEBUG False ALLOWED_HOSTS [yourdomain.com, 服务器公网IP]如果忘了把DEBUG关掉就部署上线服务器保存的traceback页面会暴露源码结构这是非常低级的错误但也有不少人犯过。数据库连接池Django默认每请求新建数据库连接。对于并发量不大的平台默认连接管理够用但为了稳妥我加了CONN_MAX_AGE60设定数据库连接复用时间。# settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, CONN_MAX_AGE: 60, ... } }7. 从课程设计到真实运营我学到的比代码更重要的东西写完这个项目我把代码部署到一台测试服务器上又拉了几个同学实际体验了一轮发现了很多纯做课程设计时想象不到的问题也积累了一些比代码本身更重要的经验。7.1 真实用户反馈引发的三个需求变更第一打印页数确认机制。我原本让系统根据PDF自动计算页数但学生上传Word文档时页数无法准确获取导致到店后价格对不上。后来把实际打印页数由商家在后台确认后修改作为兜底方案反而比完全自动更可靠。第二订单修改与取消。原本用户下单后就不能改参数了但实际使用中会有人想加印、改成彩色、甚至取消整个订单。后来在状态为待处理时开放了订单取消权限管理员也支持修改订单价格。第三通知提醒。原本只有用户在平台上查看订单状态后来我加了邮件通知Django的send_mail打印完成后自动发邮件提醒取件。这算是低成本高感知的功能很受同学们欢迎。7.2 项目扩展方向哪些地方还能继续做深给打算在这个项目基础上继续完善的同学几个方向接入真实支付申请微信支付商户号配置支付回调验证签名将模拟支付替换为真实支付。多店模式为不同教学楼门店分配独立管理员角色平台层做统一管理。在线预览用PDF.js在网页上直接预览PDF文档减少打错文件的概率。智能推荐纸张根据文档内容自动推荐纸张类型减少用户决策负担。小程序端学生更习惯用微信小程序而不是打开网页接入小程序后体验更顺滑。7.3 关于做项目本身的三句话最后说点心得。做这个项目的过程中我最大的体会是不要上来就写代码。花一个周末的晚上把用户角色、业务状态、异常路径想清楚画一张纸上的数据流图比多写一百行代码都值。我踩的最深的坑就是过早进入编码导致后面改模型改得痛不欲生。第二句话是从核心闭环开始。我第一版只做了上传文件—下单—后台处理—取件这一条最简单的路其余的比如支付、通知、多文件都是后面一步步加的。先跑通再丰富心态完全不一样。第三句话是项目文档和代码一样重要。答辩的时候老师更多问的是你的思路、你踩过的坑、你为什么这么设计而不是你的代码长什么样。把我上面写的这些设计决策和踩坑链路记录下来你的答辩会非常从容。如果你也在做类似的项目希望这篇能给你省点时间。有什么问题欢迎留言交流我尽量回复。
返回列表