不知道你有没有这样的感觉:大部分Django教程做出来的项目,要么是“图书管理系统”,要么是“博客系统”,功能简单到让人怀疑自己到底学会了什么。我在带学生做课设、自己接外包项目时,最常被问的一句话就是:“老师,我想做个二手交易平台,和普通商城有什么区别?”我一开始也以为没啥区别——书本加上架管理、订单、用户认证,完事。但真正动手做完一个面向学生的二手书籍交易平台之后才明白:这类项目的难点根本不在“增删改查”,而在商品状态流转、多用户并发下单的竞争条件、还有权限边界这些教科书里一笔带过的地方。
这篇文章我以自己近期主导的一个真实项目为例,完整复盘一个“基于Django的学生二手书籍交易平台”是怎样从需求理清到数据建模、从请求链路到权限设计、再从上线的全过程。内容会尽量扎实,也会穿插不少只有踩过坑才写得出来的细节,适合准备做课程设计、毕业设计,或者想在校园场景里做一个小而美的独立项目的开发者。
1. 为什么选Django做二手书平台,而不是Flask或Node?
很多人一上来就纠结框架。说实话,做这种规模的校园项目,Flask、FastAPI、Express都能做出来,区别在于你愿意花多少时间处理那些“不性感但必须存在”的模块。二手书交易平台的本质是一个C2C电商场景的缩微版,它天生需要这些能力:
- 用户注册、登录、退出、密码找回
- 商品(书籍)的发布、编辑、上下架
- 订单的产生、状态流转、交易确认
- 后台管理(书籍审核、用户管理、订单处理)
- 图片上传和静态资源处理
这些模块如果用Flask做,通常得自己拼Flask-Login、Flask-SQLAlchemy、Flask-WTF、Flask-Admin,麻烦不说,版本兼容问题就够喝一壶。用Node做也行,但生态里没有官方认可的ORM,很多同学最后写出来的SQL像是回到了十年前。
Django最大的优势不是”强大”,而是约定优于配置。它把上面这些模块都内置好了,你只需要在项目里创建一个app、把模型写出来,Django自动给你生成后台管理界面、自动处理表单校验、自动带好CSRF防护。对学生二手书这种运营大于开发的场景来说,简直不要太合适——因为运营同学需要快速录入书籍、上架下架,而不是让开发人员每次手动改数据库。
1.1 项目定位:我们要服务哪些人
我不建议上来就做一个覆盖全校的全功能平台,那会拖垮开发周期。我给这个项目定的边界是:
- 卖家:在校大学生,手头有闲置教材、考研资料、文学作品等,需要拍照、定价、填写八成新,然后发布上架。
- 买家:同样是在校学生,可以按书名、关键词、分类搜索书籍,看到感兴趣的书私信卖家或直接下单。
- 管理员:辅导员或学生团队负责人,负责审核违规书籍(比如内容敏感或明显恶搞)、处理纠纷订单。
听起来平平无奇,但这三个角色引出了一个关键设计问题:二手书的交易流程到底是“先付款后发货”,还是“线下当面交易”?我在需求评审时挖到了这个,它直接影响订单表怎么建。大多数校园场景下,二手书都是同校当面交易,线上平台承担的是“信息撮合”职责,而不是支付网关。所以订单表不需要对接支付宝、微信支付,但需要有一个明确的“交易状态机”——从“已拍下”到“已确认交货”,每一步都需要卖家或买家主动触发。
把目标用户厘清后,技术选型就顺理成章了:Django 4.2 + SQLite起步(后期切PostgreSQL)+ Bootstrap 5做界面。Django 自带的Admin站点作为运营后台,前端页面用服务端渲染模板,不需要额外搞一整套前后端分离方案。究其原因:这种平台的页面量就几十个,用不着把复杂度硬堆上去。
2. 需求拆解里的两个关键分歧:要不要购物车,要不要支付
我在做需求分析时,发现网上很多二手书平台的教程特别喜欢设计购物车模块,购物车加结算加支付,一套流程做下来页面多得像淘宝。但我反对这种设计,原因也很现实:二手书每本书只有一件库存,购物车对“单件商品、多卖家、面对面交易”的场景意义不大。如果硬加购物车,你还得处理“买家加购后书被其他人买走”、“购物车长期不结算导致卖家无法下架”这类怪问题,白白增加开发量。
于是我做了个取舍:不设计购物车,改为“立即联系/立即下单”两种模式。
- 立即下单:买家点击下单,订单状态变为“待卖家确认”,卖家可以在后台同意或拒绝。
- 立即联系:页面展示卖家的联系方式(学生可以填写微信号/QQ),交易完全转移到线下。
这里很多人会犯一个错:把“下单”做成直接改商品状态为“已售出”。这在小项目里看起来没问题,学生演示不会真有人同时买同一本书,但实际多人测试时就会暴露问题。我在需求文档里专门写了这么一句:“商品状态的变更必须和订单状态联动,不允许通过直接改商品字段绕过订单流程。”这句话成了后面数据库设计的重要约束。
2.1 功能清单
我最终敲定的功能点如下(全部围绕核心需求收敛):
| 模块 | 功能点 | 说明 |
|---|---|---|
| 用户 | 注册、登录、退出、个人信息编辑、头像上传 | 使用Django自带认证,扩展一个Profile |
| 书籍 | 发布、编辑、下架、软删除、按分类浏览、搜索 | 核心模块,重点做状态机 |
| 交易 | 下单、确认订单、取消订单、交易完成 | 非支付场景,线下成交 |
| 个人中心 | 我发布的、我买到的、我卖出的、收藏 | 收藏利用多对多关系 |
| 后台 | 书籍审核、用户管理、订单管理、数据看板 | 复用Django Admin |
你看,这个清单没有一项是多余的。收藏可以后面再加,也可以不做,但我保留它的原因是它能帮助学生建立信任——买家的收藏列表本身就是一种社交证明。
3. 数据模型设计:一张书表怎么演化成一套完整模型体系
二手书交易平台的核心就是Book这张表,但它绝不能设计成一张大宽表,和一个普通博客的“Article”完全不是一个量级。我一开始写的模型是这样的:
class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=50) publisher = models.CharField(max_length=100) price = models.DecimalField(max_digits=6, decimal_places=2) description = models.TextField() cover = models.ImageField(upload_to='covers/') created_at = models.DateTimeField(auto_now_add=True)这个模型能跑,但存在几个问题。首先,book和user发生了关联,竟然没有记录这是谁发布的;其次,没有分类字段,搜索只能全表扫;最后,没有状态字段,你根本不知道这本书是“在售”“被预定”还是“已售出”。
经过重构,我把模型拆成了三块:书籍主体、用户扩展、订单与交易。
3.1 用户模型设计:用Profile扩展而非删改auth_user
学生平台的用户除了登录名和密码,还需要学院、宿舍楼、昵称、头像等额外信息。官方推荐的做法是新建一个Profile模型,和Django内置User模型建立OneToOne关系,而不是直接修改内置表。我这样写:
from django.contrib.auth.models import User from django.db import models class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') college = models.CharField('学院', max_length=50, blank=True) dormitory = models.CharField('宿舍楼', max_length=50, blank=True) wechat = models.CharField('微信号', max_length=50, blank=True) avatar = models.ImageField('头像', upload_to='avatars/', blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f'{self.user.username}的档案'这里给一个新手容易踩坑的点:注册表单里要一次提交用户名、密码、学院、微信号,很多同学会在视图里手动写User.objects.create_user之后,又取user.profile来dump数据。问题是Profile对象还没创建,你直接get会报错。正确做法是注册成功后立即创建Profile,或者用Django信号在创建User时自动创建Profile。我推荐后者,稳:
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance)3.2 书籍模型的状态机设计
Book的状态是整个平台最关键的字段,我给它设计了四个互斥状态:
- draft:草稿,卖家编辑中,前台不可见
- on_sale:在售,前台可见可下单
- reserved:已被预订,订单生成但未完成
- sold:已售出,不可再下单
为什么需要reserved状态,而不是“一被下单就直接sold”?我遇到的真实场景是:买家下单后,卖家可能希望再和买家沟通一下价格或取书地点,如果直接标为sold,卖家还想修改信息就麻烦。所以订单未完成前,书处于“预占”状态,这样既不会被人重复下单,也给卖家保留撤销余地。
核心模型的完整设计:
from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField('分类名', max_length=30) sort_order = models.IntegerField('排序', default=0) class Meta: ordering = ['sort_order'] verbose_name_plural = '分类' def __str__(self): return self.name class Book(models.Model): class Status(models.TextChoices): DRAFT = 'draft', '草稿' ON_SALE = 'on_sale', '在售' RESERVED = 'reserved', '已被预订' SOLD = 'sold', '已售出' title = models.CharField('书名', max_length=100) author = models.CharField('作者', max_length=50) publisher = models.CharField('出版社', max_length=100, blank=True) original_price = models.DecimalField('原价', max_digits=6, decimal_places=2, default=0) price = models.DecimalField('售价', max_digits=6, decimal_places=2) category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='books') uploader = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books') description = models.TextField('描述', blank=True) cover = models.ImageField('封面图', upload_to='covers/', blank=True, null=True) status = models.CharField('状态', max_length=10, choices=Status.choices, default=Status.DRAFT) view_count = models.PositiveIntegerField('浏览量', default=0) created_at = models.DateTimeField('发布时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['status', 'created_at']), ] def __str__(self): return self.title def is_visible(self): return self.status in (self.Status.ON_SALE, self.Status.RESERVED)几个初看不理解、细想很有用的细节:
- 原价用DecimalField而不是FloatField,是为了避免浮点精度问题,比如28.8存成了28.79999999。卖书定价虽然不涉及大批量计算,但这种习惯应该从一开始就养成。
- 分类外键用models.PROTECT,不是CASCADE。因为分类被删掉时,书还在,直接级联删除会把在售商品全部带走,后台运营容易铸成大错。
- 索引加在status和created_at上,是因为首页和列表页最常用的查询就是“查所有在售书并按时间排序”,这个联合索引能避免全表扫。
3.3 订单表:谁、买了什么、状态怎么变
订单模型同样需要精细设计。没有支付的校园二手交易,订单更多像“意向单”或“预约单”。
class Order(models.Model): class Status(models.TextChoices): PENDING = 'pending', '待卖家确认' CONFIRMED = 'confirmed', '卖家已确认' COMPLETED = 'completed', '交易完成' CANCELED = 'canceled', '已取消' book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='orders') buyer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='buy_orders') seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sell_orders') price = models.DecimalField('成交价', max_digits=6, decimal_places=2) status = models.CharField('订单状态', max_length=10, choices=Status.choices, default=Status.PENDING) message = models.TextField('买家留言', blank=True) created_at = models.DateTimeField('下单时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: ordering = ['-created_at']这里注意,我把seller单独存了一个字段,而不是通过book.uploader去取。为什么不直接关联书再关联卖家?因为多数情况下,订单生成后,即使卖家把书下架了,订单依然要保留当时的价格和卖方信息。快照思想——把下单那一刻的关键信息固化下来,不要依赖实时关联。走得远一点,你甚至可以存一份快照JSON,但那对小项目就过分了,单独存price已经够用。
4. 从发布书籍到下单完成:一次完整请求链路的防坑实录
模型建好只是开始,真正的复杂度都在视图和业务流程里。
4.1 发布书籍的视图:表单校验与缩略图处理
发布书籍的表单,我用ModelForm直接映射Book模型,方便省事。不过有个bug我记忆犹新:图片上传后,我在模板里显示{{ book.cover.url }},本地环境好好的,放到服务器上就404。原因是我没配置MEDIA_URL和MEDIA_ROOT,本地开发时Django顺手把媒体文件也serve了,部署后Nginx不认。
这是Django新手几乎必踩的坑,建议项目开始就做好:
# settings.py MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'# urls.py(仅开发环境使用) from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)4.2 下单操作的并发保护:同一本书不能被两个人同时买走
前面几次测试我都没考虑并发。后来让两个学生同时点同一本书的“立即下单”,结果订单表里出现了两条待确认订单,书也同时进入“reserved”状态,卖家和两个买家聊起来才发现撞车。这就是典型的并发竞争条件。
解决办法并不复杂,就是在下单视图里用select_for_update()锁住那行记录:
from django.db import transaction from django.http import JsonResponse from django.views.decorators.http import require_POST @require_POST @transaction.atomic def create_order(request, book_id): book = Book.objects.select_for_update().filter(id=book_id).first() if not book: return JsonResponse({'ok': False, 'msg': '书不存在'}, status=404) if book.status != Book.Status.ON_SALE: return JsonResponse({'ok': False, 'msg': '这本书已被预订或已售出'}, status=400) order = Order.objects.create( book=book, buyer=request.user, seller=book.uploader, price=book.price, status=Order.Status.PENDING, ) book.status = Book.Status.RESERVED book.save(update_fields=['status', 'updated_at']) return JsonResponse({'ok': True, 'order_id': order.id})select_for_update()的作用是事务期间锁定数据库中的对应行,第二个请求必须等第一个事务结束才能读到这条记录,这样两次并发请求就不会同时通过状态检查。
4.3 卖家确认、取消订单的权限控制
确认订单和取消订单的视图,需要校验“当前登录用户必须是这本书的卖家”。很多人会把权限判断写在模板里,只让卖家看到“确认”按钮,但在视图层完全没有校验。于是出现经典漏洞:用户猜到订单ID,直接POST请求就把别人的订单确认了。
我建议所有操作都统一加这个逻辑:
from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic import View from django.shortcuts import get_object_or_404, redirect, render class ConfirmOrderView(LoginRequiredMixin, View): def post(self, request, order_id): order = get_object_or_404(Order, id=order_id) if order.seller != request.user: # 这里不能只返回403,还要留一条后台日志,便于追查 logger.warning('用户%s尝试操作不属于自己的订单#%s', request.user, order_id) return render(request, 'error.html', {'msg': '这不是你的订单'}, status=403) if order.status != Order.Status.PENDING: return render(request, 'error.html', {'msg': '订单状态不允许该操作'}, status=400) order.status = Order.Status.CONFIRMED order.save(update_fields=['status', 'updated_at']) return redirect('order_detail', order_id=order.id)核心原则:模板中的按钮只是体验升级,视图层的权限校验才是安全底线。
5. Django ORM实操:查询与删除,多少人在阴沟里翻船
这一节对应很多搜索热词——查询、删除、导入项目,因为这些看起来太基础,反而最容易掉进陷阱。
5.1 列表页的N+1查询问题
首页书籍列表如果用最简单的写法:
books = Book.objects.filter(status=Book.Status.ON_SALE)模板里再显示{{ book.uploader.profile.wechat }}或者{{ book.category.name }},你会有意无意制造出N+1查询问题:每显示一本书,Django都会额外查一次上传者和分类。后端只查了1次,实际数据库却被访问了几十次,页面慢是理所当然的。
正确做法是用select_related一次性把外键连表查出来:
books = ( Book.objects .select_related('uploader__profile', 'category') .filter(status=Book.Status.ON_SALE) .order_by('-created_at') )select_related就是告诉数据库做JOIN,把uploader、profile、category一次性加载到内存,列表页从N+1次查询降为1次。记住:凡是模板里用到点号关系,在视图里就要考虑select_related。
5.2 搜索功能:不要一上来就搞全文检索
学生平台需要有搜索框,但直接上全文检索引擎(Elasticsearch、Whoosh)对小项目太重了。我的建议是先做最简单的模糊匹配:
from django.db.models import Q def search_books(request): keyword = request.GET.get('q', '').strip() books = Book.objects.filter(status=Book.Status.ON_SALE) if keyword: books = books.filter( Q(title__icontains=keyword) | Q(author__icontains=keyword) | Q(publisher__icontains=keyword) ) return books搜索关键词时用Q对象组合多个字段的模糊匹配,这是最通用、最容易被新手接受的做法。当数据量大了再考虑PostgreSQL的全文检索字段,或者接入搜索引擎,前期不要过度设计。
5.3 删除对象的三种方式,别用错
“删除”这个操作在Django里至少有三种层面,很容易搞混:
- 物理删除:
book.delete(),从数据库里彻底清除,不可恢复。 - 级联删除:通过外键的
on_delete=CASCADE触发,比如删除一个用户时,他发布的所有书一起没了。 - 软删除:就是更新状态字段,例如把Book的status改成draft,或者加一个
is_active=False,让记录还在、数据可追踪。
在二手书平台这种有交易往来、有纠纷可能性的场景里,我强烈建议:订单永远不要物理删除,书籍尽量不要物理删除。即使卖家要下架一本书,也只是把状态改成draft,而不是delete。因为如果有人问“这本书怎么就没了”,管理员还能在后台看到历史记录。
如果你确实要做软删除,建议单独建一个is_active布尔字段,而不是用status兼任,否则业务代码里到处是“状态加is_active”的混合判断,后期维护会想哭。
5.4 在PyCharm里接手别人Django项目的第一件事
这个热搜词(pycharm中怎样导入已建立好的django信息系统)说明很多人在团队协作或课程设计时会拿到现成的Django项目。记住,拿到项目后不要急着点Run,先做三件事:
- 第一,确认解释器:虚拟环境是项目自带的还是系统Python?创建虚拟环境后运行
pip install -r requirements.txt,没有requirements.txt就按项目引入的模块一个个装。 - 第二,检查数据库:Django项目往往自带SQLite文件,但如果是MySQL/PostgreSQL配置,需要先建库改配置。
- 第三,跑迁移:
python manage.py migrate,看有没有未执行的迁移文件。
只要执行顺序正确,绝大部分导入问题都能解决。
6. 权限与安全:学生平台容易被忽视的三个漏洞点
6.1 用户认证:内置系统的二次封装
Django直接提供了User模型,登录、认证、session都现成。但干这个项目时,我发现一个体验痛点:学生注册时的用户名,很多人起得千奇百怪,比如带下划线、数字或者中文,给同学之间的联系带来不便。我们重新把注册逻辑封装了一下,保留用户名但增加昵称和手机号必填。
这里要特别提醒:如果修改了User模型相关的认证行为(比如支持手机号登录),建议在项目一开始就做AUTH_USER_MODEL自定义,而不是在中途改。中途切换User模型会面临数据库迁移的巨坑,业内人士都懂,代价大到能让人重开项目。
6.2 越权操作与URL试探
我在6.2提过的权限校验就不重复了,再补一个敏感点:不要用自增ID作为订单ID直接暴露给用户。如果订单ID是1、2、3这种连续数字,用户完全可以遍历,看看你的订单系统有多少交易量,这是业务信息泄露。
我建议订单号使用比较随机但好看的格式,比如时间戳加随机4位数字:
import random import time def generate_order_no(): return f"{time.strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"6.3 XSS防御:富文本描述是攻击入口
书籍描述字段我是用TextField存的,默认情况下Django模板会自动转义HTML,这个机制本身是安全的。但如果你在模板里这么写:
{{ book.description|safe }}那等于亲手把XSS漏洞大门打开。学生如果真的在描述里塞了一段<script>恶意脚本,所有访问这本书页面的人都会执行这段代码。记住:用户输入的内容,默认永远当成不可信数据,不该用的safe过滤器坚决不用。
Django的模板自动转义,足以为这个项目挡住绝大部分XSS攻击,只要你不手贱加safe。
7. 部署上线阶段容易踩的连环坑:SecretKey、静态文件与图片目录
项目开发完成后要展示或上线,很多人会卡在这个阶段。这里我记录了部署时亲身经历的几个连环问题。
7.1 该保密的必须保密
直接把settings.py推到公开仓库,里面包含了SECRET_KEY、数据库密码、邮箱密码。如果只是课程设计还好,真要上线,这就是实打实的安全事故。
建议把敏感配置放入环境变量或.env文件,用os.environ.get()读取:
import os SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True' ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', '').split(',')7.2 静态文件404的经典场景
开发环境一切正常,部署后打开页面发现CSS全丢了。原因很简单:开发时Django自己提供静态文件服务,部署时你需要先执行python manage.py collectstatic把所有静态文件收集到STATIC_ROOT,然后交给Nginx托管。
我遇到的最麻烦的情况是,Windows开发环境路径用反斜杠,部署到Linux服务器后路径分隔符错乱。解决办法是尽量用pathlib.Path和BASE_DIR / 'static'这样的写法,不要手写斜杠。
7.3 图片上传目录与数据备份
封面图、头像会上传到MEDIA_ROOT,这个目录绝不能在项目目录里直接固定写死,因为发布代码时会把本地图片也带上去,不干净也不安全。生产环境我用外部存储桶或专门的存储目录,然后把media目录彻底从Git里忽略。
对于数据库备份,SQLite阶段最简单的方式就是定期拷贝数据库文件,但如果你有外键、事务、并发需求,还是建议切PostgreSQL。我在这个项目里前期用SQLite开发,后期迁移到PostgreSQL,原因就是生产环境的并发一下来,SQLite容易报“database is locked”错误。如果你确定项目只是演示用,SQLite也够,但心中要有这条迁移路径。
8. 给同样在做Django课设/毕设的同学几句大实话
项目做完了,表格梳理一下不算什么,但有几个倾向性问题我特别想说一下。
8.1 功能不在多,而在闭环
我见过太多同学的项目页面有几十个,但点来点去全是死链,要么点个按钮没反应,要么数据之间对不上。面试官或评委老师最看重的,是一个核心流程能不能彻底走通。对于这个二手书平台,核心流程就是:注册登录 → 发布书籍 → 买家搜索 → 下单 → 卖家确认 → 交易完成 → 双方评价。你把这个闭环打磨顺了,比多十个花哨功能都值钱。
8.2 不要害怕用AI辅助开发,但别让它替你思考
现在的开发流程和几年前已经很不一样了,我自己也用AI辅助生成模板和表单代码。比如写一个ModelForm,或者写一个Bootstrap模态框,让AI代劳完全OK,省时间。但涉及业务状态流转和权限判断的代码,我强烈建议你每一行都自己理解清楚,因为这些才是项目的灵魂。你可以让AI帮你写,但要在关键逻辑上做code review,否则出问题都不知道从哪查。
8.3 数据填充是演示成败的关键
很多同学的演示现场翻车,不是因为程序有bug,而是因为数据库空荡荡——没有书、没有分类、没有订单,评委看不到任何页面效果。我的习惯是用Django的manage.py shell写一个小脚本,自动生成几十本带真实封面图链接的假数据,并保证分类均匀。有时候还会故意造几个多订单、多用户的数据,演示时直接展示“三个买家同时下单”这种场景,说服力强很多。
具体可以这样写:
from django.contrib.auth.models import User from book.models import Category, Book cate, _ = Category.objects.get_or_create(name='计算机教材') u, _ = User.objects.get_or_create(username='demo_seller', defaults={'password': '123456'}) for i in range(20): Book.objects.create( title=f'Python编程入门第{i+1}版', author='张三老师', publisher='人民邮电出版社', original_price=69.00, price=25.00 + i, category=cate, uploader=u, status=Book.Status.ON_SALE, description='这是一本适合初学者的教材,内页有少量笔记。', )8.4 一次真实需求变更给我的教训
这个项目做了一半,需求方提出要增加“教材版本”筛选(第几版),因为学生买教材特别在意版本。我一听觉得简单,就往Book模型加了一个edition字段。但后来发现,真正麻烦的不是加字段,而是已经有十几条测试数据要回填version信息,而以前的下单订单里也没记录当时是哪个版本。这件事让我明白:早期建模时,凡是可能会被搜索筛选项使用的字段,都要提前预留,哪怕先置空。搜索需求永远比你以为的出现得早。
类似地,我还撞过一个问题:发布书籍的封面图,学生随手拍的手机照片动辄3MB,还带EXIF信息。我一开始没做压缩,结果平台图片加载越来越慢。后来我在封面字段上加了upload_to的分目录处理,配合一个简单的Pillow压缩工具,把超过1MB的图片等比压缩到800px宽再存入。这些小优化,直到用户量真上来的时候才体现价值。
做这样一个平台,说到底不是炫技,而是训练自己如何在真实约束下决策。约束来自时间、来自用户习惯、来自运营场景,不来自框架本身,这也是Django值得被选作这个项目基础的原因。你把它用熟了,后面的Flask也好、Go也罢,都不是问题,因为工程思维是通的。
最后分享一个小经验:这个项目做完后,我又花了两个晚上把后台数据整理好,导出了一份操作手册,直接给对方管理员照着用。所有可持续运行的项目,背后都少不了一份傻瓜式文档,花这个时间远比多写20个接口更值得。