做毕业设计或者练手项目的时候,选“网上药房商城”这个方向的人其实不少。但真把“python基于vue的网上药房在线买药商城系统django flask pycharm”这个标题落到代码层面,很多人写着写着就懵了:药品和普通商品不一样,库存、处方药、订单状态、前后端联调,每个环节都有坑。这篇文章就是围绕这个标题做一个完整拆解,把技术选型、数据库建模、后端接口、前端页面、联调排错串成一条线,让准备复现这个项目的朋友能少走弯路。无论你是要用Django写毕业设计,还是想练手一个前后端分离的全栈项目,都可以拿这篇当作战地图。
我先说个总体结论:这类项目的核心难点不在“写代码”,而在于业务约束多、数据关系复杂、前后端坑位密集。药品商城表面上是个电商系统,实际上比普通电商多了几层东西——处方药与OTC的区分、库存批次管理、订单状态流转的严谨性、价格金额的精度控制。把这些想清楚再动手,比急着撸代码重要得多。
1. 拆解标题里的技术选型:为什么这几种技术能凑成一套完整方案
1.1 Django、Flask、Vue、PyCharm各司其职
标题里的每个词都不是随便堆上去的,先看一张分工表,你就明白这套组合为什么是经典搭配:
| 技术栈 | 承担的职责 | 为什么选它 |
|---|---|---|
| Python | 后端语言基础,所有业务逻辑、数据处理都跑在上面 | 语法友好、生态成熟,毕业设计和快速开发的首选 |
| Django | 后端主框架,负责ORM、认证、Admin、路由、模板渲染能力 | 自带Admin后台,药品和订单管理能省大量手写代码 |
| Flask | 后端备选框架,在轻量场景下替代Django | 适合小型原型或想要精细化控制接口时使用 |
| Vue | 前端框架,负责页面交互、购物车状态、下单流程 | 响应式数据驱动,配合组件化开发,用户端体验好做 |
| PyCharm | 开发IDE,贯穿前后端开发、调试、数据库操作的全流程 | 对Django有专门支持,调试器和数据库工具非常省事 |
这个组合的精妙之处在于:Django负责重活重管,Vue负责界面交互,两者通过RESTful API通信,完全的前后端分离;而PyCharm在中间把所有开发环节串起来,让调试和排查效率高一大截。
1.2 Django与Flask的取舍逻辑
标题里同时出现django和flask,很多人会纠结到底用哪个。我的建议是:默认用Django,只有特殊情况才考虑Flask。
Django的优势很实在:自带的ORM能让你用Python类直接映射数据库表,不用手写SQL;自带的Admin后台可以让你在开发阶段就拥有一个能管理药品、订单、用户的可视化界面;自带的认证体系几乎覆盖了用户登录注册的常见需求。这些对一个“商城系统”来说,等于把地基全都给你打好了。
那我为什么还会提到Flask?因为Flask胜在轻量和灵活。如果你只想做一个API后端,每一个接口都由自己完全掌控,不想被Django的“约定大于配置”约束;或者你的核心需求是练习如何从零搭建一套接口服务,那Flask会让你更快看清HTTP请求处理、路由映射、数据库连接这些底层原理。这次的项目里,如果时间充裕,可以先用Django把完整功能跑通,再横向对比一下Flask重写一遍核心接口,学习价值翻倍。
1.3 千万别小看PyCharm的位置
很多教程把PyCharm当成一个高级文本编辑器,这是最大的误区。我在这个项目里最依赖PyCharm的四个能力:
- 虚拟环境管理:创建项目时直接用PyCharm建好venv,Django和所有依赖都隔离在项目内部,不会污染系统Python。
- 断点调试:后端逻辑复杂时,在接口处理函数里打几个断点,看请求走到哪一步、变量值是什么、数据库查询结果长什么样,全程可视化。
- 数据库工具:PyCharm自带Database面板,可以连上SQLite或MySQL直接看表结构、查看数据、执行SQL,不用额外装Navicat。
- HTTP客户端:自带接口调试工具,写完接口直接在IDE里发起POST请求测一下,省得来回切换Postman。
换句话说,PyCharm不只是用来“写”代码的,它让整个开发闭环都留在同一个窗口里,这对查错和验证特别重要。
2. 药房商城和普通商城在业务模型上的本质差异
2.1 药品不是普通商品,先想清楚业务约束
开发之前如果只按“图书商城”“服装商城”的思路设计,后面会吃到苦头。药品有几个普通商品没有的特殊约束:
第一,药品有批准文号。这是药品的“身份证”,一个药品的完整信息包括药品名称、规格、生产厂家、批准文号、有效期、用法用量。数据库里得给药品表专门设计这些字段,否则后台没法录入合规的药品信息。
第二,药品分处方药和非处方药(OTC)。处方药不能像普通商品那样直接下单购买。哪怕你做一个学习项目,业务逻辑上也必须有标记和拦截机制:药品表里存一个“处方类型”字段,前端下单时做一个明显的提示和确认弹窗,后端在订单接口里也要做校验,不然用户随便买个抗生素就有点说不过去了。
第三,库存管理更敏感。药品有批次和有效期,真实业务里还会涉及“先进先出”。项目做到基础版,可以简化成“一个药品一个总库存数”,但设计表结构时最好预留批次相关的字段,方便以后扩展。
第四,药品分类更细。成人用药、儿童用药、感冒用药、肠胃用药、维生素矿物质……分类维度比服装的“男装女装”复杂得多,需要两级甚至三级分类。
这些业务约束看起来是“需求条款”,但它们直接决定了字段设计、接口返回值、前端交互逻辑。先列出来,写代码就不会乱。
2.2 核心数据表怎么拆
结合药房业务特点,我建议数据库至少拆成下面这些表。用Django的ORM来翻译,就是一个个models.Model类。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| User(用户) | username, password, phone, role | 角色字段可区分普通用户和后台管理员 |
| Category(药品分类) | name, parent | 支持二级分类,parent为空表示一级分类 |
| Drug(药品) | name, category, approval_number, prescription_type, price, stock, detail, image | 核心药品表,价格用Decimal,库存用Integer |
| CartItem(购物车) | user, drug, quantity | 一个用户对应多个药品条目 |
| Order(订单) | order_no, user, total_amount, status, address, created_at | 订单主表,status是状态机核心 |
| OrderItem(订单明细) | order, drug, drug_name, price, quantity | 下单时快照药品名和价格,防止日后药品信息改动影响历史订单 |
| UserAddress(收货地址) | user, receiver, phone, address, is_default | 近一年常用配送场景基本少不了它 |
为什么要单独拆OrderItem而不是把药品直接塞进Order?因为一个订单可能包含多种药品,而订单和药品是多对多关系,中间必须有一张明细表。而且明细表里一定要保留“商品名称”和“下单价格”快照,这是订单系统的基本素养——药品以后再改价,历史订单也保持原样。
2.3 价格和库存两个字段设计不能含糊
价格字段在这类项目里翻车率极高。很多新手习惯用FloatField存价格,这是典型的隐患。浮点数是近似值,0.1 + 0.2算出来是0.30000000000000004,一旦涉及金额累加和校验,就会出现“明明价格没错,但订单总额差几分钱”的诡异问题。
正确做法是使用DecimalField:
from django.db import models class Drug(models.Model): name = models.CharField('药品名称', max_length=128) approval_number = models.CharField('批准文号', max_length=64) price = models.DecimalField('单价', max_digits=10, decimal_places=2) stock = models.PositiveIntegerField('库存', default=0) def __str__(self): return self.nameDECIMAL(10, 2)可以存储最大一亿元的价格,两个小数位精准对应货币单位,加减乘除都不会出现精度漂移。
库存字段看起来只是一个整数,但它背后藏着一个并发问题:用户A和用户B同时下单,如果后续扣库存时没有并发控制,库存很容易变成负数。这个问题放后端章节细说,但在建模阶段要有一个意识:库存字段不能任由代码随意赋值。
3. Django后端四条主线实现:认证、药品、购物车、订单
3.1 项目创建和模块结构
用PyCharm新建Django项目之后,我建议按业务模块拆app,而不是把所有代码塞进一个文件里:
# 使用命令行创建项目 django-admin startproject pharmacy_project # 进入项目目录后,创建业务app python manage.py startapp user_app python manage.py startapp drug_app python manage.py startapp order_app python manage.py startapp cart_app这样拆的好处是:每个app只管一条业务线,文件不会越写越乱。比如drug_app里放药品模型、药品视图、药品序列化器;order_app里放订单模型、订单状态逻辑、下单视图。后期无论是自己维护还是给别人讲解项目,都好理解得多。
3.2 用户认证:Django自带还是JWT
这部分是很多人的纠结点。Django自带Session认证确实开箱即用,但它跟“前后端分离”搭配时体验一般。因为前端跑在Vue环境里,没有浏览器的同源Session概念,用Session需要处理跨域Cookie,配置麻烦,而且移动端几乎没法用。
我用的是JWT(JSON Web Token)。用户登录成功,后端返回一个token,前端存到LocalStorage里,之后每次请求在请求头带上Authorization: Bearer <token>,后端验证通过就认为是登录用户。
Python生态里可以用djangorestframework-simplejwt这个库,配置很少。登录视图大概是这样:
from rest_framework_simplejwt.views import TokenObtainPairView # urlpatterns里直接注册 urlpatterns = [ path('api/user/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/user/register/', register_view, name='register'), ]用JWT之后,购物车和订单接口里就非常方便地能拿到当前登录用户:
from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView class CartView(APIView): permission_classes = [IsAuthenticated] def get(self, request): user = request.user # 查询该用户的购物车并返回认证方案的选择影响后面所有接口,这个决策越早定越好。
3.3 药品列表、搜索和详情接口
药品模块的接口不需要太复杂,核心是“列表 + 搜索 + 详情”。
列表接口支持按分类筛选、按药品名模糊搜索、按价格排序,用Django的ORM查询集和DRF(Django REST Framework)很快就能实现:
from rest_framework.generics import ListAPIView from rest_framework.response import Response from .models import Drug from .serializers import DrugSerializer class DrugListView(ListAPIView): queryset = Drug.objects.all() serializer_class = DrugSerializer def get_queryset(self): queryset = super().get_queryset() category_id = self.request.query_params.get('category') keyword = self.request.query_params.get('keyword') ordering = self.request.query_params.get('ordering', 'id') if category_id: queryset = queryset.filter(category_id=category_id) if keyword: queryset = queryset.filter(name__icontains=keyword) if ordering in ['price', '-price', 'stock']: queryset = queryset.order_by(ordering) return queryset这里有个小细节:icontains模糊搜索对中文很友好,比如搜索“感冒”,能把“感冒灵颗粒”“感冒清热胶囊”都捞出来。前端页面上再配合一个分类下拉框和搜索框,体验就很完整了。
3.4 购物车与订单接口设计
购物车和订单是整个系统交互最密集的地方。接口设计上按照Restful风格来,清晰明了:
| 接口 | 方法 | 作用 |
|---|---|---|
| /api/cart/ | GET | 获取购物车列表 |
| /api/cart/ | POST | 添加药品到购物车 |
| /api/cart/{id}/ | PUT | 修改购物车某条记录的数量 |
| /api/cart/{id}/ | DELETE | 删除某条购物车记录 |
| /api/orders/ | POST | 提交订单,生成主表和明细表 |
| /api/orders/ | GET | 获取当前用户的订单列表 |
| /api/orders/{id}/ | GET | 获取订单详情 |
下单接口是最容易写砸的地方,因为它要同时做三件事:读取购物车数据、创建订单主记录、创建订单明细记录。而且这三件事必须“要么全部成功,要么全部失败”,绝不能出现“订单建了,明细丢了”这种半截状态。
用Django的transaction.atomic包起来:
from django.db import transaction class OrderCreateView(APIView): permission_classes = [IsAuthenticated] @transaction.atomic def post(self, request): user = request.user cart_items = CartItem.objects.filter(user=user).select_related('drug') if not cart_items.exists(): return Response({'msg': '购物车为空'}, status=400) total = 0 order = Order.objects.create( order_no=generate_order_no(), user=user, status='pending_payment', address_id=request.data.get('address_id') ) for item in cart_items: drug = item.drug # 检查库存 if drug.stock < item.quantity: raise ValueError(f'药品 {drug.name} 库存不足') # 创建订单明细 OrderItem.objects.create( order=order, drug=drug, drug_name=drug.name, price=drug.price, quantity=item.quantity ) total += drug.price * item.quantity # 扣库存(这一行放到最后统一处理) order.total_amount = total order.save() # 扣库存并清空购物车 for item in cart_items: drug = item.drug drug.stock -= item.quantity drug.save(update_fields=['stock']) cart_items.delete() return Response({'order_no': order.order_no, 'total_amount': str(total)})这里有几个设计点是日常开发中总结出来的经验:
第一,明细表里存了drug_name和price快照,将来药品改名、改价,历史订单依然可追溯。
第二,扣库存动作单独放到最后统一处理,因为在拆解购物车数据阶段不应该提前修改库存。等订单确认要生成了,再一次性扣减,避免中途异常导致库存莫名其妙减少。
第三,total_amount计算过程中全部使用Decimal,最终输出时转成字符串str(total),绝不能直接返回浮点数给前端。
第四,更严谨的并发扣库存需要借助select_for_update()或数据库锁,但在基础版项目里,先做到事务和库存量检查,已经比大多数实现靠谱了。
3.5 订单状态机:别想得太简单
订单状态是整个后端的灵魂。常见的状态至少有这么几个:
| 状态值 | 含义 |
|---|---|
| pending_payment | 待付款 |
| paid | 已付款,待发货 |
| shipped | 已发货 |
| completed | 已完成 |
| canceled | 已取消 |
为什么要把状态做成一个状态机,而不是在视图函数里随便改?因为状态之间不是任意跳跃的。只有待付款订单可以取消;只有待付款订单可以变成已付款;已付款订单才能发货;已发货订单才能完成。如果代码里任何地方都能乱改状态,用不了几天数据就乱了。
我习惯在Order模型里写一个状态转换的方法:
class Order(models.Model): STATUS_CHOICES = [ ('pending_payment', '待付款'), ('paid', '已付款'), ('shipped', '已发货'), ('completed', '已完成'), ('canceled', '已取消'), ] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending_payment') def can_pay(self): return self.status == 'pending_payment' def mark_paid(self): if not self.can_pay(): raise ValueError(f'订单状态不允许支付: {self.status}') self.status = 'paid' self.save(update_fields=['status']) def can_cancel(self): return self.status == 'pending_payment' def cancel(self): if not self.can_cancel(): raise ValueError('当前状态不可取消') self.status = 'canceled' self.save(update_fields=['status'])这套方法把状态流转规则集中到了模型内部,后续在任何视图里调用都不会乱。代码审查的时候也容易看明白。
4. Vue前端怎么组织:药品浏览、购物车、下单页
4.1 页面和路由规划
Vue项目我用Vue CLI或Vite创建,配合Vue Router和Axios。页面不需要多,核心就四个:
- 首页/药品列表页:展示药品、分类筛选、搜索
- 药品详情页:展示药品详细信息、加入购物车
- 购物车页:修改数量、勾选药品、去结算
- 下单确认页:选择收货地址、确认订单、提交
路由配置里,购物车和下单页都要做登录校验,否则未登录用户不能访问:
// router/index.js const routes = [ { path: '/', name: 'DrugList', component: DrugList }, { path: '/drug/:id', name: 'DrugDetail', component: DrugDetail }, { path: '/cart', name: 'Cart', component: Cart, meta: { requiresAuth: true } }, { path: '/checkout', name: 'Checkout', component: Checkout, meta: { requiresAuth: true } }, ] router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !localStorage.getItem('token')) { next('/login') } else { next() } })4.2 药品列表页的请求与渲染
列表页最常用的模式是“搜索框 + 分类侧边栏 + 药品卡片网格”。Axios请求封装成统一方法,把token带上:
// api/drug.js import axios from 'axios' import request from '../utils/request' export function getDrugList(params) { return request({ url: '/api/drugs/', method: 'get', params }) }这里的params就是分类ID、关键词、排序字段。页面交互变化时,重新请求列表数据。为了提升体验,可以用loading状态控制页面加载提示,用computed计算筛选后的展示。
药品卡片上要同时展示药品名、规格、价格、库存状态。这里有一个交互细节:库存为0的药品不能直接禁用整个卡片,而是展示“缺货”标志,点击时提示用户“暂时缺货,关注补货”,这比直接隐藏商品更符合购药场景。
4.3 购物车的状态管理与勾选逻辑
购物车页在前端比较复杂的地方在于勾选结算。用户可能勾选两盒感冒药加一盒维生素,总价要实时计算,未勾选的商品不计入。这个用Vuex或Pinia管理购物车状态很合适。
我用的思路是:
- 购物车数据存在Vuex里,用户进入购物车页时调用
fetchCart获取后端数据。 - 每个购物车条目有一个
selected字段,默认全选。 - 页面底部显示“已选x件商品,合计xxx元”,用
getters计算已选条目和总价。
// store/cart.js 核心getter getters: { selectedItems(state) { return state.items.filter(item => item.selected) }, selectedTotal(state, getters) { return getters.selectedItems.reduce((sum, item) => { return sum + parseFloat(item.drug.price) * item.quantity }, 0) } }这个设计是参考电商平台最常见的模式,前端只负责展示和计算,后端以购物车原始数据为准。提交订单时,把选中的购物车条目的ID列表传给后端,后端根据ID去数据库里关联用户后再创建订单。千万不要把前端计算的总价当真,后端的total_amount才是一锤定音的数据源。
4.4 下单页与订单提交后的跳转逻辑
下单页需要调用“获取默认地址”和“创建订单”两个接口。创建订单成功后,理想情况是跳转到支付页或订单详情页,但因为是学习项目,可以做成“模拟支付成功”按钮,点击后调用支付确认接口,把订单状态从pending_payment推到paid。
这里有一个很重要的交互逻辑:创建订单成功后要清空购物车数据。我在实践中会先调创建订单接口,成功后调“清空已购商品”的后端接口,同时清空Vuex里的购物车items。这样用户返回购物车页时,已经下单的商品不会重现。
订单提交后的跳转要避免用户重复提交。按钮要加submitting状态,提交期间禁用并显示“处理中”,否则用户手快点两次,后端虽然只有第一次会成功,但前端体验会很糟糕。
5. 联调阶段最常见的三个坑:跨域、时区、金额
5.1 跨域问题:为什么请求发不出去
前后端分离项目最常见的第一道坎就是CORS(跨域资源共享)。Django后端跑在8000端口,Vue前端跑在8080端口,浏览器默认不允许不同源的前端页面调用后端接口,于是控制台报错CORS。
解决方式是在Django里用django-cors-headers:
pip install django-cors-headers然后在settings.py里配置:
INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:8080', ] CORS_ALLOW_CREDENTIALS = True配置完重启Django服务,跨域问题就解决了。如果前端用https域名或者部署后的IP地址,记得加到CORS_ALLOWED_ORIGINS列表里。
5.2 Django时区配置:为什么订单时间差了8小时
这是联调时最容易迷惑的问题。Django默认的TIME_ZONE = 'UTC'配合USE_TZ = True,数据库存的时间是UTC,而中国在东八区,前端展示订单创建时间时会出现8个小时的偏差。
最简单的处理方案,在开发阶段把配置改成:
TIME_ZONE = 'Asia/Shanghai' USE_TZ = False注意:USE_TZ = False时,Django按本地时间存储和读取,对学习和演示项目最直观。如果未来上生产环境,建议用USE_TZ = True并统一转成用户时区展示,但那是更复杂的方案,这里不展开。
改完配置后重建数据库或清空旧数据,再重新下单测试时间就对了。
5.3 金额精度:前端展示和后端计算的微妙关系
前后端联调时总金额不一致,十有八九是浮点数精度问题。前端的parseFloat(item.drug.price)在计算合计时可能出现9.999999之类的浮点尾巴,显示出来很难看,也更让用户困惑。
前端展示金额,建议统一用toFixed(2)处理,或者用整数分单位计算。不过最稳妥的原则是:前端价格只做展示,不做后端信任的数据源;后端订单总价必须自己根据购物车明细重新计算,并且用Decimal存储。
开发时验证一下这个原则很简单:前端选两件价格是13.49和8.98的药品,如果后端算出来是11.47之类的乱七八糟数字,说明某个环节浮点精度崩了。正确结果应该永远是22.47整。
6. 踩坑实录:订单支付后状态一直不更新的完整排查过程
6.1 现象与复现条件
这个项目的联调阶段,我遇到过最典型的Bug是:用户在前端点“模拟支付成功后”,页面提示成功了,订单状态也一度变成了已付款。但是刷新页面再看订单列表,订单状态又变回了“待付款”。反复复现,每次都这样。
这种现象特别有欺骗性:接口看起来成功了,数据也变化过,但最终结果却不对。它既不是完全没写,也不是报错,而是“中间出了岔子被回滚了”。
6.2 排查链路:从现象一路倒查
我的排错习惯是沿着“前端 → 接口 → 后端逻辑 → 数据库”逐层倒查。
先看前端的请求。打开浏览器开发者工具,Network面板里查看“模拟支付”请求的响应的确返回了成功。这个请求是POST /api/orders/{id}/pay/,状态码200,返回体里写着status: paid。到这一步,前端没有问题。
再看后端日志。Django控制台没有明显报错,但为了保险,我在pay视图里打了一行print,确认请求进到了支付逻辑里。接着查数据库,发现订单状态确实是“待付款”。问题来了:为什么接口返回paid,数据库还是pending_payment?
到这里我开始怀疑是有代码在支付成功后把状态改回去了。顺着这个思路检查订单详情接口、订单列表接口,最终定位到订单模型的save()方法重载上。
6.3 根因:一行被遗忘的save逻辑
问题不在支付逻辑,而在于我之前为了给订单自动生成订单号,重载了模型的save()方法,并在里面根据订单号是否为空来决定是否补一个order_no。但人不小心把逻辑写成了:
def save(self, *args, **kwargs): if not self.order_no: self.order_no = generate_order_no() super().save(*args, **kwargs)表面看这没问题。问题出在支付视图调用mark_paid()时我写了self.save(update_fields=['status'])。因为update_fields只更新status字段,而模型里的super().save()执行时,如果不传update_fields,Django会保存所有字段的最新值。问题不是这里。
真正触雷的是另一行:在我给订单列表接口返回数据时,我顺手对查询出来的每个订单调用了serializer输出,而在某个工具函数里写了一段逻辑:“如果订单处于待付款状态超过1小时,自动取消订单”。这段逻辑被放进了list接口的序列化流程里。支付成功后,列表接口刷新时拿到订单,检测到pending_payment已超时,于是自动置为canceled。但页面显示的是待付款回退的现象是因为自动取消后又被另一个“恢复待付款”的兜底逻辑改了回来,两个逻辑打架,把状态来回拉扯。
这个坑的教训非常直接:不要把业务副作用塞进序列化过程,更不要在读取数据的接口里偷偷写数据。查询就是查询,状态变更必须走明确的动作方法。清理掉那两段自动逻辑之后,订单状态完全正常。
6.4 排查习惯与预防清单
那次排错之后,我给自己定了一个清单,每次遇到状态不对的问题都先过一遍:
| 排查项 | 检查内容 |
|---|---|
| 前端传参 | 请求体字段名和后端期望是否一致 |
| 接口返回 | 响应状态码和返回数据是否与业务预期一致 |
| 后端日志 | 视图函数是否执行到预期位置 |
| 数据库落库 | 目标字段是否真的写入预期值 |
| 是否存在中途覆盖 | 查询接口、序列化器、模型save是否偷偷改数据 |
| 并发与事务 | 是否有其他进程或事务回滚了本次修改 |
这个清单看起来简单,但大部分“接口成功但数据不对”的问题都能通过它定位。排查时最忌讳直接改代码瞎碰,一层层验证,故障点自然就浮出来了。
7. 项目跑通之后的优化方向与我的个人体会
基础版本跑通之后,如果你的目标是毕业设计拿个好分数,或者想把项目写进简历,我建议再往这几个方向延展一下。这也是我在实际迭代中觉得最有价值的部分。
第一,为购物车和订单增加缓存层。高频读取的药品分类和热门药品列表,可以用Redis或Django缓存框架存起来,减少数据库压力。这个优化点写进项目介绍里特别加分。
第二,补一个简单的后台销售统计接口。用Django的ORM聚合查询,统计每天订单量、销售额、热门药品TOP5。前端用图表库展示,项目从“功能型”升级为“带数据分析型”,整体含金量提高不少。
第三,药品库存预警。后台设置一个库存阈值,低于阈值的药品在Admin后台和列表接口里都显示“库存告急”,这是药房业务非常有实际意义的场景。
第四,把支付流程替换成真实的第三方支付沙箱环境。毕业设计如果只停留在“模拟支付”,答辩时很容易被追问;接一个沙箱环境,代码复杂度和说服力都会上去。
最后说几句个人体会。这类全栈项目,我在实际开发中最受益的是习惯“先设计状态和数据关系,再写视图”。商城系统的坑很少在语法层面,而几乎全部都集中在“状态修改、金额计算、库存并发”这三件事上。如果你刚开始写,建议一步一步来:先把Django后端的订单和购物车逻辑用postman测到稳,再动Vue页面;宁可前端页面丑一点,也不能让订单逻辑飘。
还有一个小技巧:在PyCharm里给Django配置好断点调试之后,遇到任何“奇怪的问题”第一反应不要是加print,而是打断点看数据。亲眼看到某一行数据不对劲,比自己脑补一遍要高效得多。这个习惯保留下来,后面做任何项目都受用。