便利店商品管理系统与网上超市:前后端分离实战指南
看到这个项目标题的时候我第一反应是愣了一下Python、Django、Vue3、Java四个词同时出现在一个标题里这明显是需求文档里技术选型还没想清楚或者在同一个项目里同时比较了两种后端方案。但转念一想这不正是很多便利店进销存、网上超市项目开发时的真实常态吗前端基本锁定了Vue3后端要么用Python的Django要么用Java的Spring Boot具体选哪个取决于开发团队熟悉什么。这篇我打算围绕“便利店商品管理 网上超市购物”这个业务核心把技术选型、数据库设计、前后端实现、核心流程和几个容易踩坑的地方挨个梳理一遍给正在做类似系统或者准备拿这个练手的同学一份可以直接参考的实操笔记。先说清楚这套系统到底是干嘛的。便利店的日常经营核心就是三件事管商品、管库存、管订单。商品信息要能录进去价格、分类、图片要能维护库存要能实时扣减卖了多少要有统计。网上超市购物系统则是在此基础上加了线上选购这一个环节顾客通过界面浏览商品、加购物车、提交订单、模拟支付管理员在后台管理商品、处理订单、看销售数据。整套下来就是典型的电商系统骨架麻雀虽小五脏俱全。适合想系统学习前后端分离开发的人也适合接外包项目时需要快速出方案的朋友参考。1. 项目整体设计与技术选型思路1.1 核心业务需求拆解先别急着写代码需求想清楚比什么都重要。便利店商品管理系统和普通的电商系统相比有几个明显不同的业务特点。第一门店库存变化频繁。便利店小、商品多饮料零食日用品每天都有进货和售卖库存量可能只有几件十几件缺货卖完是常见状态所以库存字段必须有而且下单时扣库存的逻辑要做严谨。第二线上线下一体化。顾客通过网上超市下单门店需要配货所以订单状态不能只是简单的“已支付”还要有“待发货、已发货、已完成”这样的履约流程。顾客端要有个人中心能看订单记录和状态。第三促销和价格策略不能太复杂。便利店不像大商超不需要满减、优惠券、秒杀这种大而全的促销体系。不过有个功能值得做商品支持上架/下架。饮料过期了、商品补货中后台直接改状态前台就看不到了这个对便利店很实用。整个系统的角色分两类管理员和普通顾客。管理员负责商品维护、分类管理、库存查看、订单处理和基础的数据统计顾客负责注册登录、浏览商品、加入购物车、下单和查看自己的订单。核心模块拆出来大概有这么几块用户认证模块、商品分类与商品管理模块、购物车模块、订单模块、库存扣减模块以及简单到只剩一张表的销售额统计模块。每个模块之间用标准的RESTful接口关联起来前后端分离Django只负责APIVue3只负责页面渲染。1.2 技术选型解析标题里的“Pythondjangovue3基于Java”到底怎么落地这个标题会让我多解释几句因为不少人在需求文档里就是这么写的写的人还没决定做的人要先学会自洽。实际落地时后端语言只能选一个。如果你选Python系那就是Django Django REST Framework如果你选Java系那就是Spring Boot MyBatis Plus Spring Security。标题中的“基于Java”多数情况是早期的模板描述真正开发的时候可以按团队技术栈来收敛。我这次以Python Django为主线来讲因为Django自带Admin后台和ORM做管理系统的原型效率实在太高了。为了让你在选型时心里有数我把两种方案放在一起做过真实对比感受如下对比项Python Django 方案Java Spring Boot 方案开发效率很高ORM、Admin、迁移工具内置中等配置项多代码量大数据库操作Django ORM 面向对象写起来快MyBatis/XxxPlus 要手写SQL灵活但繁琐并发与性能中小业务量没问题扛不住超大流量高并发分布式场景生态更成熟系统管理后台/admin 直接用省掉基础CRUD页面需要自己搭一套管理后台部署运维gunicorn Nginx 简单直接JVM调优、打jar包、内存要求高适合场景中小项目、快速交付、数据分析延伸大型企业级、复杂业务、海量并发我的建议是如果你是这个项目的第一责任人时间紧、人手少优先选Python Django。如果团队本来Java多或者项目明确要求高并发那就老老实实选Spring Boot不要混着写。像我见过有的项目后端点菜用Python结果团队里没人会维护最后全盘重构那个痛苦是加倍的。1.3 系统架构与项目目录规划前后端分离架构下系统大致的数据流是这样的用户在Vue3页面点击按钮发起HTTP请求到前端代理代理把请求转发到Django服务Django经过路由分发到对应视图再由ORM操作MySQL数据库返回JSON数据给前端渲染。全程无服务端渲染这也是现在主流的小型全栈项目的标准形态。MySQL在本地开发时我建议直接装一个不要用SQLite代替。库存扣减、订单状态变更这类场景下SQLite的锁机制和MySQL差距明显开发时一切正常一上线就出问题的情况我见太多了。如果你不想自己装数据库先连一台云上的MySQL实例也可以关键是让整个开发环境贴近生产环境。目录规划上后端和前端分开两个工程。后端我用一个虚拟环境安装Django、Django REST Framework、djangorestframework-simplejwt、django-cors-headers、Pillow、mysqlclient。前端用Vite脚手架创建Vue3工程引入Vue Router、Pinia、Axios和Element Plus组件库。如果还要做移动端适配可以再引入Vant组件库做一套手机版页面。前后端端口规划Django跑在8000端口Vue3开发服务器跑在5173端口。开发阶段通过Vite的代理解决跨域生产环境则用Nginx统一代理把前端静态文件和后端API放在同一个域名下这样连CORS配置都可以省掉。后面第6章我会专门说这个。2. 数据库设计与核心模块2.1 核心数据表设计与Django模型数据库设计是整套系统的地基。便利店商品管理系统的数据表不需要很多但表之间的关系必须清楚。先说用户。项目里直接使用Django内置的User表它自带用户名、密码、邮箱等字段省去二次开发的成本。如果你要存手机号、昵称、收货地址建议建一张单独的用户资料表用OneToOneField关联User不要往User表里乱塞字段不然以后升级Django版本容易出兼容问题。再说商品分类和商品。分类用一张自关联表实现支持两级分类就够用了别设计成无限级树便利店里的东西再多也就是饮料、零食、日用品、速食这几个大类。商品表是核心字段包括商品名称、所属分类、原价、售价、库存量、商品图片、上下架状态、销量、创建时间。其中销量字段很有价值做销售排行和库存补货预警都要用到。购物车要考虑一个用户对应多个商品而且同一个商品加第二次时应该做数量累加而不是生成新记录所以购物车表用联合唯一约束让同一个用户和同一个商品只能有一条记录。订单表建议分成主表和明细表两张。主表存订单号、用户、总金额、订单状态和创建时间明细表存订单关联的每个商品、下单时的商品价格快照和购买数量。为什么要有“价格快照”这个字段因为商品价格是会变的如果你不下单时刻的价格顾客下单后看到的价格和订单详情里的价格对不上这个在需求里往往被忽略实际做的时候一定要加上。订单号不建议用数据库自增ID最好用时间戳加随机数生成一个唯一订单号一方面避免暴露真实订单数量另一方面后续对接其他系统时也更规范。Django模型代码可以直接参照下面这个结构# goods/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(max_length50, uniqueTrue) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.PROTECT, related_namechildren ) class Meta: verbose_name 商品分类 verbose_name_plural verbose_name def __str__(self): return self.name class Product(models.Model): name models.CharField(max_length200) category models.ForeignKey(Category, on_deletemodels.PROTECT) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) original_price models.DecimalField( max_digits10, decimal_places2, default0, verbose_name原价 ) stock models.IntegerField(default0, verbose_name库存) image models.ImageField(upload_toproduct/, blankTrue, nullTrue) status models.BooleanField(defaultTrue, verbose_name是否上架) sales models.IntegerField(default0, verbose_name销量) created_time models.DateTimeField(auto_now_addTrue) class Meta: verbose_name 商品 verbose_name_plural verbose_name def __str__(self): return self.name2.2 购物车与订单模型设计购物车表的模型长这样class Cart(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) product models.ForeignKey(Product, on_deletemodels.CASCADE) quantity models.IntegerField(default1) class Meta: unique_together (user, product)订单主表和明细表如下class Order(models.Model): ORDER_STATUS ( (0, 待支付), (1, 已支付), (2, 已发货), (3, 已完成), (4, 已取消), ) order_no models.CharField(max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.PROTECT) total_amount models.DecimalField(max_digits10, decimal_places2) status models.IntegerField(choicesORDER_STATUS, default0) created_time models.DateTimeField(auto_now_addTrue) class Meta: verbose_name 订单 verbose_name_plural verbose_name class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) product models.ForeignKey(Product, on_deletemodels.PROTECT) product_name models.CharField(max_length200) price models.DecimalField(max_digits10, decimal_places2) quantity models.IntegerField()关于删除约束我要特别提醒一句商品表和订单明细表之间一定要用PROTECT模式绝对不能用CASCADE。否则一个商品只要被某个历史订单引用过当你想删这个商品时会连带把订单明细删掉订单数据直接损坏。实际操作中商品应该用上下架状态来做“软删除”数据库里的商品记录尽量保留这才是符合业务习惯的做法。2.3 角色权限与数据流转权限控制方面用户只有普通顾客和管理员两种。我的处理方式不复杂但很实用在 Django 的 User 模型上直接利用 is_staff 字段区分管理员和普通顾客。管理员可以访问商品管理、订单处理、统计报表等接口普通用户只能访问与自身相关的购物车、订单和个人信息接口。在 Django REST Framework 里这种权限用 IsAdminUser 就够了。如果后面业务复杂了需要区分超级管理员和普通管理员可以再引入 django-guardian 做对象级权限。现阶段不需要做得太复杂反而增加维护成本这是我从几个项目里总结出的经验权限粒度够用就好。数据流转上的核心关系可以这样理解顾客把商品加入购物车购物车只是“意向单”提交订单后购物车数据会复制到订单明细同时清空对应购物车记录订单状态从待支付开始模拟支付成功后变成已支付管理员在后台发货变成已发货顾客确认收货变成已完成如果超时取消则变成已取消。每一条流转都有清晰的状态边界状态机不复杂但要注意在代码里写状态校验不能让订单从待支付直接跳到已完成。3. 后端核心实现Django API3.1 初始化配置与公共组件新建工程后我习惯先把安装配置一次性搞定避免后面边开发边补漏。项目里主要用到的配置项包括将应用注册进INSTALLED_APPS配置 MySQL 数据库连接设置 REST Framework 的默认认证和分页配置 JWT 认证以及配置跨域。关键配置片段如下# settings.py INSTALLED_APPS [ # django 内置应用 rest_framework, corsheaders, goods, orders, ] DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: shop_db, USER: shop_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } } REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 10, } CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]JWT 认证这里有个点要解释一下。传统 session 认证在前后端分离架构下不是不能用但它天然依赖 Cookie跨域时还要处理 CSRF麻烦。JWT 是把用户标识放进 Token前端每次请求把它放到 Authorization 请求头里后端解析 Token 得到用户信息非常适合纯 API 场景。便利店系统的登录态用 JWT 管理体验明显更接近企业里的真实项目。3.2 用户认证与商品管理接口用户认证我用 djangorestframework-simplejwt。安装完成之后注册它的两个视图就行from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(auth/login/, TokenObtainPairView.as_view()), path(auth/refresh/, TokenRefreshView.as_view()), path(auth/register/, RegisterView.as_view()), ]注册接口需要自己写因为 JWT 默认没有给用户注册的逻辑。注册视图其实就做一件事接收用户名、密码、确认密码校验两次密码是否一致用 Django 的User.objects.create_user创建用户再用TokenObtainPairView的逻辑给新用户签一堆 Token 直接返回。这样用户注册完就能直接进入商城不用跳转登录页体验顺畅。商品接口用视图集写起来效率最高。分类接口和商品接口都可以用一个 ModelViewSet 完成列表用分页创建和更新由 Django REST Framework 的序列化器自动处理。商品序列化器要特别注意图片字段的返回格式我一般会自定义一个get_image_url方法拼上完整请求域名这样前端拿到的是一个可以直接访问的 URL而不是一个相对路径class ProductSerializer(serializers.ModelSerializer): image_url serializers.SerializerMethodField() class Meta: model Product fields [id, name, category, price, stock, image_url, status, sales] def get_image_url(self, obj): request self.context.get(request) if obj.image and request: return request.build_absolute_uri(obj.image.url) return None3.3 购物车与订单接口实现购物车接口是频繁增删改查的一类接口很多初学者会直接把购物车做成一整套视图集其实没必要。购物车操作核心就三个查列表、加商品、改数量/删除。用APIView更直接逻辑一目了然。加购逻辑要注意三个边界情况购物车为空时直接创建已有该商品时累加数量商品库存不足时直接拒绝。这里的前两种逻辑都容易写第三个边界很多人会漏。我之前看到一个项目加购数量已经超过库存前端不拦后端也不校验最后下单阶段才发现库存不够只能把整个订单作废体验很差。所以加购时就校验一次下单时再校验一次两道闸更稳。订单接口建议做成一个独立的视图因为下单不是一个简单的增操作它要串联查询购物车、校验商品库存、计算总金额、生成订单号、扣减库存、清空购物车这一系列操作还要保证要么全部成功要么全部失败。3.4 库存扣减与事务处理库存扣减是整个系统中并发风险最高的一步。便利店网上超市的并发量没有电商平台那么夸张但也不代表你可以忽略。试想一个场景某个爆款商品库存剩最后一件两个顾客同时提交订单如果代码只是先查库存再减库存两个请求都读到库存是1都通过了校验最后都扣减成功库存就变成了 -1这就是经典的超卖问题。解决这个问题最低成本的方案是使用 Django ORM 的 F 表达式或select_for_update()加锁。我在项目里用的是悲观锁方案逻辑很清晰from django.db import transaction from django.db.models import F transaction.atomic def create_order(user, cart_items): order Order.objects.create(...) for item in cart_items: try: product Product.objects.select_for_update().get(iditem[product_id]) except Product.DoesNotExist: raise ValueError(商品不存在) if product.stock item[quantity]: raise ValueError(f商品{product.name}库存不足) product.stock F(stock) - item[quantity] product.sales F(sales) item[quantity] product.save() OrderItem.objects.create(...) Cart.objects.filter(useruser).delete()select_for_update()会把这个商品的记录行锁住直到事务提交才释放。第二个请求只能等第一个事务处理完再读读到的是已经扣减后的库存超卖问题就解决了。代价是并发量高时会有些锁等待便利店系统每天几千单的量级这个代价完全值得。如果你用的是 Java Spring Boot 方案同样的逻辑用Transactional加行锁也是一样套路所以两种技术路线在业务逻辑上的差别真没有想象中大。4. 前端实现Vue3开发实战4.1 项目初始化、API封装与代理配置前端部分我直接用 Vite 初始化项目命令就不啰嗦了。关键是项目里的代码结构建议按功能模块分目录这样后期维护不会乱。我常用的目录结构是这样src/ ├── api/ # 接口请求封装 │ ├── product.js │ ├── cart.js │ └── order.js ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── views/ │ ├── admin/ # 后台管理页面 │ └── shop/ # 商城页面 ├── utils/ │ └── request.js # Axios 实例 └── App.vueAxios 实例封装在 request.js 里主要做两件事请求拦截器统一带上 Token响应拦截器统一处理错误状态码。Token 我放在 localStorage 里虽然存在 XSS 风险但在中小型项目里这是最常见也最方便的方案注意不把敏感信息写进 Token 本身就行。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) 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 requestVite 代理配置必须配不然前端开发时所有请求都会因为跨域被浏览器拦截。我在vite.config.js里这样配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } } })这里有个容易踩坑的细节如果你把 Django 的接口路径配成/api/login那前端 axios 的 baseURL 要写/api请求路径里再写/login拼起来才是完整的/api/login。如果你在 axios 里已经写了/login然后又写了一遍/api就会出现 /api/api/login 这种鬼问题。我见过不止一个同事因为这种拼接顺序问题排查了半天。4.2 管理后台核心页面商品与订单管理管理后台我用 Vue3 Element Plus 来做页面整体布局是左边菜单、右侧内容区。路由根据角色做控制普通用户访问 /admin 时判断权限没有管理员身份就直接重定向首页。商品管理页是后台最核心的页面我拆成两个组件商品列表和一个新增/编辑商品的弹窗。商品列表用 Element Plus 的 table 组件通过分页接口拿数据图片列显示一个小缩略图通过前面序列化器返回的 image_url 直接绑定上下架状态列用 Switch 开关切换时直接调用更新接口这一小块交互特别符合便利店店员的操作习惯点一下就能下架缺货商品。新增商品的表单要注意库存和价格的校验规则价格用数字输入框精确到两位小数库存只允许正整数。图片上传我用 Element Plus 自带的 el-upload 组件配置好 action 指向后端的上传接口。上传接口不是 Django 默认的需要自己写一个视图接收文件流保存到 media 目录然后把文件 URL 返回给前端。这里有一个小坑Django 默认开发环境处理 media 文件需要配置 MEDIA_URL 和 MEDIA_ROOT配置好后直接访问图片 URL 就能预览。订单管理页面主要是订单列表和详情。订单列表支持按状态筛选通过下拉框切换待支付、已支付、已发货等状态。点开订单详情能查看商品明细列表和收货人信息。管理员可以把订单从“已支付”改成“已发货”前端只需要调一个更新状态的接口。这里我提醒一下状态更新接口不要做成通用的表格更新接口而是单独写一个“更改订单状态”的接口这样前端不容易误传其他字段。4.3 商城购物页面商品浏览到结算下单商城页面是顾客视角的系统我用一套独立的路由去承载。首页展示商品列表支持按分类筛选和关键词搜索。商品卡片上显示图片、名称、价格右上角加“加入购物车”按钮点一下就直接加购不用进入详情页这个交互很适合便利店“快速购物”的场景。搜索结果和分类切换保持 URL 同步刷新页面不丢状态。购物车页面用列表形式展示已加购的商品每行显示商品图片、名称、单价和一个数量加减器。数量改变时调用后端修改购物车接口同时前端本地同步最新单价小计。结算按钮点击后进入订单确认页显示商品清单和总金额。我一开始做这个页面的时候踩过一个坑Vue3 中计算属性没有自动更新导致修改购物车数量后页面上的合计金额不变。后来发现是因为没有把购物车列表放到 Pinia 里而是在组件里用 ref 存了一份。改成 Pinia 管理购物车数据之后购物车列表和金额合计就能在所有页面实时联动体验好了不少。所以这里分享一个经验全局数据尽量用 Pinia不要用 localStorage 或者组件内 ref 硬扛不然页面一多数据同步问题会让你改到怀疑人生。订单提交我做成一个独立接口前端拿到成功结果后跳转到“支付成功”页面。支付成功页本质上是模拟把订单状态从待支付改成已支付这步也可以后端在用户点击“模拟支付”时通过接口触发属于业务上的一个小闭环。5. 核心业务流程串联从加购到订单5.1 数据库层面的购物车状态流转购物车在数据库里就是一张用户与商品的关系表但它承载着从“意向”到“订单”的关键流转。用户加购物车时我建议前端先展示一个反馈Toast然后后端返回最新的购物车数量前端把角标数字更新一下。购物车记录什么时候删下单事务里清空这是最合理的时点。有的项目在用户下单后保留购物车记录等支付成功再删结果用户支付前取消订单、再次下单购物车数据就乱套了。我自己的做法是提交订单生成待支付订单后购物车立即清空后面支付失败、订单取消都与购物车无关。逻辑上更干净。5.2 下单与库存校验的双重保障下单接口我总结了一个口诀一要锁行二查库存三减库存四建订单明细五清购物车。这五步在一个事务里完成任何一个环节校验失败整个事务回滚数据库保持原状。写这个接口时有一个细节购物车查询的结果最好在循环里逐条处理不要一次性把购物车列表和商品列表分别查出来后用代码合并那样虽然代码短但多个商品同时下单时商品列表的顺序和购物车数量对不上出过好几次隐藏Bug。老老实实按购物车逐条锁行查询商品性能不会差因为单笔订单的商品数量有限。5.3 支付模拟与订单状态机线上支付对接真实支付渠道需要申请商户号、配置回调接口这在本地开发环境很难完全模拟一般都会做一个模拟支付接口。点击“模拟支付”按钮后前端调用接口后端把订单状态从待支付改成已支付。订单状态机是整个系统的业务脉络我在后端写了一个常量的映射表保证所有状态变更都走同一个小函数避免状态被随意修改。状态变更函数的核心逻辑是只允许合法流转待支付 - 已支付已支付 - 已发货已发货 - 已完成待支付 - 已取消其他任何跳转都直接抛异常。这个状态机写好后前端无论怎么操作后端都不会出现订单状态错乱的问题。6. 常见问题与排查技巧实录6.1 跨域和代理问题的经典场景跨域问题在前后端分离项目里几乎百分之百会遇到但不同场景下解决方法不一样。开发阶段按理说配好 Vite 代理就可以了可我见过很多同学明明配了代理还是访问失败最常见的原因有两个代理配置只在开发服务器里生效所以如果你直接在前端代码里请求http://localhost:8000那等于绕过代理必然跨域另一个原因是代理路径写错了比如后端接口路径是/api/product/list前端 baseURL 是/api请求地址写作/product/list结果代理转发到后端就变成/product/list后端路由里没有这个路径自然报404。如果生产环境也出现跨域建议直接用 Nginx 把前端和后端放在同一个域名下这样前端请求/apiNginx 把/api转发到 Python 服务的8000端口彻底避免跨域标签。这比在 Django 里配置 CORS 要稳妥得多。6.2 JWT Token 过期与前端处理JWT 默认过期时间是小时级便利店店员可能一整天都开着后台页面不做操作很容易遇到 Token 过期返回401。我在项目中配置了一个较长的过期时间同时加了刷新Token的机制让前端在 401 时自动刷新request.interceptors.response.use(null, async error { const originalRequest error.config if (error.response error.response.status 401 !originalRequest._retry) { originalRequest._retry true const refresh localStorage.getItem(refresh) const res await axios.post(/api/auth/refresh/, { refresh }) localStorage.setItem(token, res.data.access) return request(originalRequest) } return Promise.reject(error) })刷新 Token 的逻辑本质上是双 Token 体系一个 access_token 短期访问一个 refresh_token 长期刷新。注意 refresh_token 别存 localStorage 时间太长前端退出登录时要清干净否则会有安全隐患。6.3 库存超卖问题的两种解法对比前面提到超卖问题用 select_for_update 加锁解决这是最直接的做法。但有些场景下比如商品详情页频繁查询库存锁行会影响查询性能。这时候可以用乐观锁方案在商品表加一个版本号字段更新库存时同时判断版本号是否一致不一致就说明被别人改过了直接失败返回。乐观锁的伪代码思路大概是这样UPDATE product SET stock stock - {quantity}, version version 1 WHERE id {product_id} AND version {old_version};如果影响行数是0说明版本不匹配重试或失败。这种方案并发效率高但实现复杂度高一些而且需要在接口层配合重试逻辑。我的建议是业务量小就用悲观锁简单可靠业务量大再考虑乐观锁引入 Redis 缓存库存这属于进阶的优化便利店系统做到悲观锁这步已经够用了。6.4 图片上传与访问路径问题图片上传后访问不到是很多新手都会遇到的问题。Django 开发模式下处理媒体文件的访问需要两步一是在 settings.py 中配置MEDIA_URL /media/和MEDIA_ROOT BASE_DIR / media二是在主项目的 urls.py 中加上静态服务的路由映射。加上之后访问http://localhost:8000/media/product/xxx.jpg就能看到图片。如果你把图片传到了 media 目录但前端返回显示的地址还是相对路径就要检查序列化器里有没有拼写完整域名这个动作。前面代码里的request.build_absolute_uri就是干这个的。生产环境用 Nginx 时需要额外配置/media/这个路径直接映射到服务器的 media 目录同时权限设置为可读。6.5 项目从开发到上线的避坑指南最后分享一个学习路线上容易踩的坑。很多初学者拿到这个项目会把精力全放在前端页面上后台页面做得特别漂亮结果后端接口连最基础的库存事务都没处理好。我的经验是先把 Django Admin 用起来把所有模型注册到详情后你不需要写一行前端代码就能看到所有核心数据的管理界面。先在 Admin 里验证模型关系和库存扣减逻辑再开发 Vue3 页面效率会高很多。另外数据库结构的迁移文件要养成习惯改完模型立刻执行迁移不要在项目后期一次性迁移不然环境一换还能不能正确同步数据库完全看运气。我个人在实际项目中的体会是像便利店商品管理系统这种规模的系统技术难度并不高真正拉开差距的是对业务细节的理解。把库存扣减做严谨、把订单状态流转做清晰、把购物车数据同步做好这个系统就已经超过大部分半成品项目了。如果你正在做类似系统建议先把后端事务和数据库关系吃透再回头优化前端交互顺序对了整个项目会顺畅很多。