简介:这是一套基于Django与Vue的网上鲜花销售系统毕业设计资源,面向计算机相关专业学生、开发者及小微商家,用于完成课程设计、毕业设计或快速搭建在线花店。系统覆盖鲜花展示、购物车、订单管理、用户管理等核心模块,并包含用户注册登录、鲜花分类浏览、花语信息展示、购物车增删改、订单状态跟踪等典型电商流程。后端采用Django框架,前端使用Vue框架,配套Pycharm+Vscode开发环境,可在Windows、Linux及macOS上运行。资源包共231个文件,约10.91MB,包含93个Python源码文件(后端逻辑与接口)、22个HTML与11个CSS及22个JS文件(前端页面与交互)、32个PNG和12个JPG图片(界面素材与效果图),以及8个PSD设计稿、XML、JSON、TTF字体等辅助文件,目录按功能模块划分,便于对照学习。已有194人浏览学习,适合需要参考完整前后端分离项目实现、理解订单与购物车业务流程的读者。
1. 网上鲜花销售系统,毕设的边界在哪里
毕业设计网上鲜花销售系统,第一反应是“电商项目”,但只做成商品展示加购物车就太单薄了。真正的难点在于把一条完整业务链跑通:用户注册登录、按分类浏览和搜索鲜花、加入购物车、提交订单、模拟支付、后台发货,以及每个环节里库存和状态的联动。很多同学的代码在本地能点通,答辩时被问到“库存怎么防止超卖”“订单状态由谁更新”“后端接口如何鉴权”就露馅。这套方案会从数据库字段设计讲到前后端联调,再落到云服务部署,每步都是可以照着做的代码和命令,适合想做出一个能演示、能扛住追问的毕设项目的人。
2. 网上鲜花销售系统的技术选型与数据库设计
2.1 技术栈对比:为什么 Spring Boot + Vue 更容易过答辩
毕设选题固定后,下一步是选技术组合。常见做法有四种,各有各的代价。JSP + Servlet + JDBC 最传统,代码都堆在 JSP 里,逻辑讲不清;SSH 配置繁琐,容易陷入 XML;Spring Boot + Vue 前后端分离,接口清晰,答辩时可以展示接口设计、事务、部署三条主线;Django + Vue 适合熟 Python 的同学。我一般推荐 Spring Boot 加 Vue,因为 Spring Boot 内置 Tomcat,不需要理解复杂的容器配置,前端由 Vue 做交互,后端只提供 JSON,联调时双方关注点也单一。如果学校不允许前后端分离,可以用 Thymeleaf 直接渲染页面,后端业务代码几乎不用改。
这里给一个对比表,方便答辩时说明你做过选型:
| 方案 | 上手曲线 | 答辩表现 | 维护成本 |
|---|---|---|---|
| JSP + Servlet + JDBC | 平缓 | 代码堆在页面,难点讲不清 | 高 |
| Spring + SpringMVC + MyBatis | 中等 | 配置多,容易被追问细节 | 中 |
| Spring Boot + Vue + MySQL | 中等 | 前后端职责清晰 | 低 |
| Django + Vue | 平缓 | admin 后台演示效果好 | 低 |
选型理由要落在一句话:优先保证在有限时间内做出完整闭环,而不是炫技术。Spring Boot 生态里 MyBatis-Plus、Spring Validation、JWT 都有现成 starter,网上示例和踩坑记录也多,真卡住时更容易搜到答案。
2.2 六张表打通商品到订单的库表设计
网上鲜花销售系统的核心数据落在六张表:分类表、鲜花商品表、用户表、购物车表、订单主表、订单明细表。下方是 MySQL 8 建表脚本,字段命名统一用下划线,便于 MyBatis-Plus 自动驼峰映射。
CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名,如玫瑰、百合、绿植', sort INT DEFAULT 0 COMMENT '排序权重,越小越靠前', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '鲜花分类'; CREATE TABLE flower ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL COMMENT '商品名,如红玫瑰礼盒', price DECIMAL(10,2) NOT NULL COMMENT '售价,单位元', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', image_url VARCHAR(255) COMMENT '图片地址', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', sales INT DEFAULT 0 COMMENT '销量,用于列表排序', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '鲜花商品表'; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT '存 bcrypt 哈希,不存明文', phone VARCHAR(20), address VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '用户表'; CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, flower_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1 COMMENT '加购数量', checked TINYINT DEFAULT 1 COMMENT '勾选结算', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_flower (user_id, flower_id) ) COMMENT '购物车表'; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '业务订单号,要有时序', user_id BIGINT NOT NULL, total_price DECIMAL(10,2) NOT NULL COMMENT '订单总价,快照', status TINYINT DEFAULT 0 COMMENT '0待付款 1已付款 2已发货 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL ) COMMENT '订单主表'; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, flower_id BIGINT NOT NULL, flower_name VARCHAR(100) NOT NULL COMMENT '商品名称快照', price DECIMAL(10,2) NOT NULL COMMENT '下单时价格快照', quantity INT NOT NULL ) COMMENT '订单明细表';订单表和明细表里保存flower_name和price快照是特意为之:鲜花价格会随节日波动,历史订单必须保留下单时的信息。直接在详情页查商品表虽然省事,但商品改价后订单对不上,属于明显的数据设计失误。另一个值得注意的点是cart表的唯一键(user_id, flower_id),它让同一用户对同一鲜花的加购在数据库层面只有一条记录,应用层不需要再写“如果存在就 update,否则 insert”的复杂判断。
2.3 外键、索引和字段取舍的边界
我建议不要建物理外键,只保留逻辑关联。毕设阶段会频繁清表和导入测试数据,物理外键会让DELETE FROM flower报错,影响演示节奏;真正生产环境里分库分表也要求逻辑外键。答辩时可以主动解释:“外键约束放在应用层,数据库用索引和唯一键保证核心约束。”
索引方面,flower表的常用查询条件有status、category_id、price,可以建联合索引(category_id, status, price);orders表的查询入口是user_id + status,建议建普通索引(user_id, status)。订单号order_no需要唯一索引,前端生成重复随机数时数据库会兜底报错,不会产生脏订单。使用DECIMAL(10,2)存价格而不是FLOAT,避免二进制浮点带来的 0.1 精度问题。
3. 网上鲜花销售系统的后端核心:商品、购物车与订单接口
3.1 接口设计与统一返回体
后端接口在设计阶段就把路径定死,前后端都省心。下面这张表是网上鲜花销售系统最核心的四个接口,答辩前一定要能说清每一个的入参和出参:
| 接口 | 方法 | 关键参数 | 作用 |
|---|---|---|---|
| /api/flower | GET | page, size, categoryId, keyword | 商品分页筛选 |
| /api/cart | GET | 无 | 当前用户购物车 |
| /api/cart | POST | flowerId, quantity | 加入购物车 |
| /api/order | POST | cartIds | 提交订单并扣库存 |
后端接口如果每个方法返回不同结构,前端联调会非常痛苦。我一般先定义一个Result<T>:
@Data public class Result<T> { private Integer code; // 0 成功,非 0 业务异常 private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> error(String msg) { Result<T> r = new Result<>(); r.code = 1; r.msg = msg; return r; } }这是所有接口的统一返回体。前端拿到code === 0才取data,HTTP 状态码只表示传输层结果。这样“库存不足”“未登录”这类业务失败可以走同一个弹窗逻辑,不会被打进 axios 的error分支。再配一个@RestControllerAdvice兜住 RuntimeException,代码量能省很多。
3.2 商品分页与条件筛选接口
鲜花首页需要支持分页、分类筛选、关键词搜索、按销量排序。用 MyBatis-Plus 的分页插件,Controller 代码:
@GetMapping("/api/flower") public Result<Page<Flower>> list( @RequestParam(defaultValue = "1") long page, @RequestParam(defaultValue = "12") long size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Flower> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Flower::getStatus, 1); if (categoryId != null) { wrapper.eq(Flower::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(Flower::getName, keyword); } wrapper.orderByDesc(Flower::getSales); return Result.ok(flowerService.page(new Page<>(page, size), wrapper)); }参数说明:page从 1 开始,size控制在 12 到 20 之间,一次返回太多会拖慢移动端渲染。keyword使用LIKE,在几千条测试数据下没有问题,但答辩时要补一句“生产环境会用全文索引或 Elasticsearch”,显得你知道边界。orderByDesc(Flower::getSales)让销量高的鲜花排前面,符合鲜花礼赠场景的挑选习惯。
3.3 购物车接口:累加与商品状态校验
加购接口的常见问题是后端不校验商品状态,导致下架商品残留在购物车。完成校验后再做“已存在则加数量”操作:
public Result<Void> addCart(Long userId, Long flowerId, Integer quantity) { Flower flower = flowerMapper.selectById(flowerId); if (flower == null || flower.getStatus() != 1) { return Result.error("鲜花已下架"); } if (quantity == null || quantity < 1 || quantity > 99) { return Result.error("数量需在1-99之间"); } Cart cart = cartMapper.selectOne( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getFlowerId, flowerId)); if (cart == null) { cart = new Cart(); cart.setUserId(userId); cart.setFlowerId(flowerId); cart.setQuantity(quantity); cartMapper.insert(cart); } else { cart.setQuantity(cart.getQuantity() + quantity); cartMapper.updateById(cart); } return Result.ok(null); }临界参数说明:quantity上限 99 防止手滑,也可以取Math.min(quantity, flower.getStock()),但不要在购物车阶段锁库存。库存扣减的真正时机是下单事务,购物车阶段只做基本校验,这符合电商系统的普遍做法。
3.4 下单事务:乐观锁扣库存与订单快照
下单是毕设里最容易翻车的点,因为它同时触达购物车校验、总价计算、库存扣减、订单生成、购物车清理五件事。必须用@Transactional把它们包进一个事务:
@Transactional(rollbackFor = Exception.class) public Result<OrderVO> submit(Long userId, List<Long> cartIds) { List<Cart> carts = cartMapper.selectBatchIds(cartIds); long total = 0; List<OrderItem> items = new ArrayList<>(); for (Cart cart : carts) { if (!cart.getUserId().equals(userId)) { throw new BizException("购物车数据异常"); } Flower flower = flowerMapper.selectById(cart.getFlowerId()); if (flower.getStock() < cart.getQuantity()) { throw new BizException("库存不足"); } int updated = flowerMapper.deductStock( flower.getId(), cart.getQuantity(), flower.getStock()); if (updated == 0) { throw new BizException("库存已被其他用户修改,请重试"); } total += flower.getPrice() * cart.getQuantity(); OrderItem item = new OrderItem(); item.setFlowerId(flower.getId()); item.setFlowerName(flower.getName()); item.setPrice(flower.getPrice()); item.setQuantity(cart.getQuantity()); items.add(item); } Order order = new Order(); order.setOrderNo(UUID.randomUUID().toString().replace("-", "")); order.setUserId(userId); order.setTotalPrice(total); order.setStatus(0); orderMapper.insert(order); items.forEach(item -> item.setOrderId(order.getId())); orderItemService.saveBatch(items); cartMapper.deleteBatchIds(cartIds); return Result.ok(order); }对应的 SQL 更新方法:
UPDATE flower SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{id} AND stock >= #{quantity} AND stock = #{oldStock}扣库存 SQL 里的stock >= #{quantity}是防超卖的底线,stock = #{oldStock}是乐观锁条件,避免两人同时读到同一个旧库存后叠加覆盖。把它放在事务里后,MySQL 会对该行加锁,第二个请求的更新会等到第一个事务提交,影响行数为 0 时直接抛异常回滚。这一层讲到“行级锁+乐观锁”就已经超过大多数毕设的深度了。
注意:
@Transactional只对 Spring 代理对象生效,在同一个类内部调用submit会绕过事务,必须从 Controller 调用 Service 的 public 方法。
4. 网上鲜花销售系统的前端页面与 API 联调
4.1 初始化 Vue 项目并解决跨域
前端采用 Vue 3 + Vite + Pinia 是较稳妥的组合。创建项目后安装依赖:
npm create vue@latest flower-shop-front npm install npm install axios pinia开发环境里前端跑在 5173 端口,后端跑在 8080 端口,直接请求会出现跨域报错。不要为了省事在 Spring Boot 上加@CrossOrigin("*"),那会把接口开放给所有站点,不符合基本安全习惯。正确做法是 Vite 代理:
// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这里没有写rewrite,意思是后端接口必须带有/api前缀。changeOrigin会把请求头里的Host改成目标地址,防止后端容器基于 Host 做校验;如果你的后端 Controller 没有/api前缀,需要自己用rewrite去掉,但这个动作会让浏览器看到的 URL 和后端实际路径不一致,排错时容易糊涂,我一般选择后端统一加前缀。
4.2 商品列表加载与搜索防抖
商品列表是纯展示页面,数据流简单。在FlowerListView.vue里定义筛选条件并请求接口:
const page = ref(1) const size = ref(12) const categoryId = ref('') const keyword = ref('') const list = ref([]) async function loadFlowers() { const { data } = await axios.get('/api/flower', { params: { page: page.value, size: size.value, categoryId: categoryId.value, keyword: keyword.value } }) list.value = data.data.records }搜索框的v-model每次输入都会触发下拉,如果每次下拉都发一次请求,演示时会看到明显的请求抖动画线。我用lodash的debounce包装一下:
watch(keyword, debounce(() => { page.value = 1 loadFlowers() }, 300))防抖不是毕设硬性要求,但这是前端工程素养的体现。300ms 是常用值:输入停顿超过 300ms 才请求,既不会让人觉得延迟,又能过滤连续打字产生的无效请求。
4.3 购物车状态管理与提交订单后的清理
购物车数据我放在 Pinia 里统一维护,避免“商城页加购后,购物车页刷新才看到”的割裂感。初始化时调一次/api/cart/list,加购成功后更新本地 store,再重新拉取数量。提交订单成功后的处理顺序是:清空 store 中的已购项、刷新购物车角标、跳转订单详情页。如果只调用后端删除而忘记更新本地状态,页面顶部购物车数量会用上一次的缓存,这个小瑕疵很容易被答辩老师看到。
联调频率最高的一个坑是后端主键用雪花 ID 时,JavaScript 解析 Long 丢精度。假设后端返回flowerId是1616837925357731842,前端拿到后末尾变成...1800,再传到后端就匹配不到数据。解决办法是在 Spring Boot 里对所有Long类型的 ID 字段加@JsonSerialize(using = ToStringSerializer.class),或统一配置 Jackson 把Long序列化为字符串。这是一个“不发现在生产,就发现在答辩”的经典问题。
4.4 联调必查的 4 类报错
联调阶段的报错绝大多数可以归类为四种,记住排查顺序能省一半时间。下面这张表按现象分列:
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 控制台报 CORS 错误 | Vite 代理未生效或路径前缀不一致 | 检查 vite.config.js 和后端实际路径 |
| 请求返回 401 | JWT 缺失、过期或格式不对 | 检查 Authorization 请求头 |
| 后端返回日期带 T | LocalDateTime 默认序列化 | 配置 Jackson 或前端用 dayjs 格式化 |
| 参数接收不到 | 请求体格式与 @RequestBody 不匹配 | 统一用 JSON.stringify |
第一类问题看请求 URL 是否为/api,再看 Vite 终端有没有代理日志;第二类检查Authorization是否以Bearer开头,以及登录接口返回的 token 有没有存进 Pinia;第三类比较隐蔽,需要前后端约定日期格式;第四类在post方法里没有设置Content-Type: application/json时最常见。把这四类提前写进测试用例,联调会顺利很多。
5. 用 Docker Compose 部署网上鲜花销售系统到云服务器
5.1 最小可用的 Compose 与 Nginx 配置
答辩时有一个公网可访问的演示地址是明显的加分项。这里用 Docker Compose 编排 MySQL、后端和 Nginx,前端dist静态文件交给 Nginx 托管。目录结构保持backend/、frontend/dist、nginx/flower-shop.conf三个单元即可。
docker-compose.yml核心内容:
services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: change-me MYSQL_DATABASE: flower_shop backend: image: openjdk:17-jdk-slim volumes: - ./backend:/app command: java -jar /app/flower-shop.jar ports: ["8080:8080"] frontend: image: nginx:1.25 volumes: - ./frontend/dist:/usr/share/nginx/html - ./nginx/flower-shop.conf:/etc/nginx/conf.d/default.conf ports: ["80:80"]Nginx 配置:
location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://backend:8080; }参数说明:ports用列表写法减少重复键,backend不暴露给宿主以外的网络。try_files必须保留,否则 Vue Router 的 history 模式刷新子页面会 404。proxy_pass后端地址用服务名backend,Docker 内部 DNS 会自动解析。
5.2 部署后的一个验证技巧
部署成功可以先用docker compose ps确认三个容器都在运行,再curl http://localhost/api/flower看是否返回 JSON;如果 404,先检查/api转发的路径和后端前缀是否一致。最后把 MySQL 初始化脚本放进数据卷,演示完执行docker compose down -v && docker compose up -d恢复初始环境。这个重置动作控制在五分钟内,能应对评委现场要求重新演示的突发情况。
本文还有配套的精品资源,点击获取