程序员做毕设或者课设,最怕的不是写代码,而是拿到一个题目之后无从下手。尤其是像“花店销售系统”这种看起来简单、但实际牵扯到用户、商品、订单、库存、支付状态的完整业务闭环,如果一上来就埋头写Controller,写着写着就会发现表结构对不上、接口逻辑拧巴、前端页面不知道怎么绑数据。这篇就结合我实际做完的一个基于SpringBoot+Vue的花店销售系统,把从需求拆解、技术选型、数据库设计、后端接口、前端联调到部署运行的完整链路捋一遍,给正在做同类JavaWeb项目的同学一个可以直接照着做的参考。
这个系统本身不复杂,但胜在业务线完整:前台要能展示花卉、搜索商品、加入购物车、下单结算、查看订单;后台要能管理花卉分类、维护商品上下架、处理订单状态。SpringBoot负责提供RESTful接口和业务逻辑,Vue负责渲染页面和交互,两者通过JSON交换数据,前后端彻底分离。这套架构在现在的毕业设计和中小型项目中非常主流,学会了不只是应付一个花店,换任何电商类业务都能套用。
我下面会按照实际开发顺序来讲:先分析这个系统到底要哪些功能,再讲为什么用SpringBoot+Vue而不是老一套的JSP,然后是数据库怎么建模,核心的订单流程怎么设计,最后是常见到让人头疼的报错和排查方法。
1. 项目整体设计与思路拆解
1.1 花店系统到底需要哪些功能
很多人在拿到“花店销售系统”这个题目时,容易犯的一个毛病是上来就列几十个功能点,恨不得做个京东出来。实际上这类课设/毕设性质的项目,核心只需要把“商品→购物车→订单→支付状态→订单管理”这条主链路走通,再配上用户的登录注册和后台的管理入口,就完全称得上完整。
我当时把功能分成了两个端口,用户端和管理端,每个端又拆成若干模块。
用户端主要做这几件事:注册登录、浏览花卉商品、按分类筛选或者按关键词搜索、查看商品详情、加入购物车、修改购物车数量、结算下单、查看自己的订单列表和订单状态。这些功能不需要全部堆在首页上,但要保证能顺畅串联起来。
管理端则围绕商品和订单展开:管理员登录后台,可以维护花卉分类(增删改)、发布新花卉商品、编辑商品价格和库存、下架商品、看到所有用户的订单列表、修改订单状态(已支付、已发货、已完成、已取消)。
这样梳理下来,整个系统的角色只有两个,普通用户和管理员,权限模型非常清晰。功能不多不少,但对于体现一个完整的Web系统设计能力来看,已经绰绰有余。更重要的是,这套东西写进论文或者答辩PPT里,“业务分析+功能建模+数据库设计+系统实现”每一个章节都有实打实的内容可以写。
1.2 为什么选择SpringBoot+Vue而不是传统的JSP
如果你去翻以前老一代的课程设计,多半是SSH(Struts2+Spring+Hibernate)或者SSM(Spring+SpringMVC+MyBatis)+JSP,前端写一大堆JSTL标签,页面和后端代码耦合成一团。那种模式不是说不能做,而是在2024年的今天,无论从开发效率还是从答辩展示层面看,都已经不太合适了。
前端展示效果,Vue配合Element UI做出来的后台管理界面,比JSP那个年代的样式起码好两个档次。用户端花店的首页、商品卡片、购物车列表,用Vue的双向绑定来做交互,体验顺畅很多。
后端开发效率,SpringBoot把SpringMVC的复杂XML配置几乎全部自动搞定,内嵌Tomcat意味着不需要单独装一个Tomcat容器来跑WAR包,直接打JAR包就能运行。这一点在演示答辩的时候特别稳妥,省去了一堆环境配置带来的不确定性。
前后端分离带来的明确分工,这在写论文的时候好处会特别明显——系统设计可以明确讲出“前后端通过JSON交互,后端不关心页面渲染,前端不关心业务逻辑”,这本身就是当前企业开发的主流模式,答辩老师听了会认可。
从学习成本上看,SpringBoot的核心用法(Controller写接口、Service写业务、Mapper操作数据库)配合Vue的组件化开发,上手速度其实比当年学SSH的XML配置文件要快得多。
1.3 角色权限与模块划分
系统只有两种角色,所以权限控制不需要引入Spring Security那套复杂的权限模型,只要做一个简单的登录拦截器,配合Redis或者直接查库校验登录状态就够了。
用户方面的功能模块有:注册登录、花卉浏览、购物车、订单管理、个人中心。管理端的功能模块有:后台登录、商品管理、分类管理、订单管理、数据概览。这些模块在后端可以对应成 package(包),我建议直接在项目中按模块分包,而不是按技术类型分包。
这句话是什么意思呢?不推荐那种controller/user、controller/admin、service/user、service/admin的分法,把同一个业务的东西拆散到各种包里去。推荐的是一刀切的分层法,每个业务模块(比如flower、cart、order)各自拥有自己的Controller、Service、Mapper,这样当你要改订单相关代码时,只需要在一个包下面找,改起来清爽得多。
2. 数据库表结构设计与关键字段解析
2.1 核心表划分:需要几张表,分别干什么
花店销售系统的核心表,我当时设计的是六张:用户表(user)、花卉分类表(flower_category)、花卉商品表(flower)、购物车表(cart)、订单表(orders)、订单明细表(order_item)。
除了这六张核心表之外,还有一张评论表(flower_comment)可以作为扩展,如果你要写论文或者给设计加分,可以加上;如果赶时间,砍掉也不影响主流程。
有一点要特别提醒,order在MySQL里是关键字,如果你直接把订单表命名为order,写SQL语句的时候必须加反引号包起来,非常麻烦。最好的做法是表名用orders,千万别踩这个坑。
2.2 每张表的字段设计与设计理由
用户表 user_fields 不需要太多,核心要记录的是用户名username、加密后的密码password、昵称nickname、手机号phone、头像avatar和注册时间。注意密码一定不要存明文,当时我用的是SpringBoot自带的BCrypt加密,注册时把明文密码转成一长串加密字符串存进去,登录时用BCrypt自带的校验方法比对。这样做最简单的理由是:如果答辩现场把数据库表展示出来,看到一堆明文密码,印象分会大打折扣。
花卉分类表 flower_category 非常简单,就是分类id和分类名称name,最多加一个排序字段sort。但不要小看这张表,分类的增删查是管理端一个很明确的CRUD题目,代码量不大却很能体现基础功。
花卉商品表 flower 需要仔细设计。核心字段包括:商品名称name、商品描述description、商品主图image(存图片的URL地址而不是图片本身)、价格price(这里强烈建议用DECIMAL(10,2)而不是FLOAT,用FLOAT会有浮点精度问题,算总价的时候可能出现 19.199999 这种恶心的情况)、库存stock(整数)、所属分类ID category_id、上下架状态status(0下架1上架)、创建时间。
还有一个字段容易被忽略,就是销量sales。这个字段可以在每次下单成功后把订单明细中的数量累加上去,用户端首页就可以直接按销量倒序做“热销花卉”推荐,效果很好,代码也就一行的功夫。
购物车表 cart 不用做成复杂的设计,核心字段是:用户ID user_id、商品ID flower_id、购买数量quantity。每个用户对同一件商品只保留一条购物车记录,如果再次加入同一件商品,就执行数量的累加而不是插入新记录。
2.3 订单表设计是重中之重
订单这块的设计,最需要想清楚两件事:一是订单表与订单明细表为什么要分开,二是订单状态如何用数字表达。
为什么需要订单明细表?因为一个订单里可以同时包含多件商品(比如一束玫瑰+一束百合+一份配草),如果把这些商品信息全塞进订单表的一行里,设计上就不规范了。所以拆成两张表:orders表记录“这一单是谁下的、总共多少钱、当前状态是什么、什么时候下的单”;order_item表记录“这个订单里包含哪些商品、每件商品购买了多少、下单时单价是多少”。
关键点来了,order_item表里必须冗余一个商品单价快照字段。这句话值得重点强调:商品表里的价格是可以被后台修改的,但订单明细里的价格,必须是用户下单那一刻的成交价。如果不做这个冗余,三个月后要查历史订单里的商品价格,你会发现价格已经对不上了。
订单状态我推荐用一组数字表示,比如1代表待支付,2代表已支付,3代表已发货,4代表已完成,0代表已取消。这样的设计在前后端联调的时候特别方便,后端只存数字,前端用一个statusMap做映射把数字转成对应的中文标签和Tag颜色。
2.4 建表SQL与字段说明参考
建表SQL模板我用字段省略形式来写,方便你直接复制后根据自己情况调整。
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `nickname` varchar(50) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `flower_category` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `sort` int DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `flower` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, `description` text, `image` varchar(255) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `stock` int NOT NULL DEFAULT 0, `category_id` int NOT NULL, `status` tinyint NOT NULL DEFAULT 1, `sales` int NOT NULL DEFAULT 0, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `cart` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `flower_id` int NOT NULL, `quantity` int NOT NULL DEFAULT 1, PRIMARY KEY (`id`), UNIQUE KEY `user_flower` (`user_id`,`flower_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `orders` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `order_no` varchar(50) NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT 1, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `pay_time` datetime DEFAULT NULL, `consignee` varchar(50) NOT NULL, `phone` varchar(20) NOT NULL, `address` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` int NOT NULL AUTO_INCREMENT, `order_id` int NOT NULL, `flower_id` int NOT NULL, `flower_name` varchar(100) NOT NULL, `flower_image` varchar(255) DEFAULT NULL, `price` decimal(10,2) NOT NULL, `quantity` int NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表里的order_no是业务订单号,不能跟主键id混用。主键id是数据库自增的,但对外展示订单号时不应该暴露你的数据量;业务单号的生成规则,我推荐用yyyyMMddHHmmss + 用户ID + 三位随机数,既保证同一秒内也不会撞号,而且从订单号本身就能看出下单时间,排查问题的时候非常方便。
3. 后端SpringBoot核心接口与业务实现
3.1 项目结构和启动类设计
后端的包结构,我采用的是一个比较标准的SpringBoot单体分层结构:
com.example.flowershop ├── FlowerShopApplication.java ├── common │ ├── Result.java │ ├── exception │ └── interceptor ├── config │ ├── WebConfig.java │ └── CorsConfig.java ├── module │ ├── user │ ├── flower │ ├── cart │ └── order └── utilsResult是一个统一的返回体,所有接口不管成功失败,都返回固定的JSON结构:{code: 200, msg: "成功", data: xxx}。这个习惯看上去是小事,但能让前端拦截器统一处理状态码、统一弹出错误消息,写起来省很多事。
3.2 统一返回体设计——一个关键决定
统一返回体的代码大概长这样:
public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } }前后端联调时,Axios的响应拦截器里判断如果code不是200,就统一提示后端返回的msg。这样做的好处非常直观:后端某个接口报错了,前端不需要每个请求都写catch单独处理,一个全局拦截器就能兜住所有异常提示。
为什么强调这个设计?因为我见过太多毕设项目,每个接口返回的格式都不一样,有的直接返回实体类,有的返回Map,有的返回字符串,前端写一个接口就要写一种解析方法,三天联调改成三天半崩溃。统一返回体是性价比最高的代码约束。
3.3 登录鉴权的实现方案:拦截器+Token
用户登录在系统中属于基础模块,但对于整个系统的安全性来说又是最重要的一环。不建议用传统的Session做法,而是用Token方案:
用户登录成功后,后端生成一个Token字符串返回给前端,前端把它存在localStorage里,每次发请求时在请求头加上Authorization: Token xxx,后端写一个拦截器统一从Header里取Token并解析出用户ID。
Token的生成可以用UUID加用户ID拼一下,也可以直接引入JWT依赖生成带过期时间的Token。毕设层面用UUID拼接用户ID的方式已经完全够用,关键是理解这个思路:无状态、不占服务器内存、前端好处理。
登录拦截器的核心逻辑就是:写一个HandlerInterceptor,在preHandle方法中取出Header里的Token,如果为空或者解析失败就返回401,让前端跳回登录页;如果解析成功,把用户ID存进ThreadLocal或者request的attribute里,后面的业务接口直接从上下文里取当前用户即可。
要特别提醒的是,登录接口本身、注册接口、商品列表接口、商品详情接口必须放行,不然用户根本没登录就访问不了购物页面,这就成死循环了。放行路径用addPathPatterns和excludePathPatterns配置即可。
3.4 下单流程的实现——事务和库存扣减
下单是花店系统最核心的一个业务方法,它牵扯到多张表的写操作,必须具备事务性。
下单的流程是这样的:前端传入收货人信息和一个商品明细列表,后端拿到当前登录用户的ID;根据商品ID批量查出商品当前价格和库存;计算总金额;生成订单号;插入订单表orders,拿到订单主键ID;遍历商品列表,逐条插入order_item表;扣减每件商品的库存;清空对应用户的购物车记录。
这五个步骤必须保证“要么全成功、要么全失败”,比如商品A扣了库存,但插入订单明细时失败了,库存就白白扣掉了。解决这个问题只需要在Service方法上加上@Transactional注解,Spring会帮助把整个方法包在一个事务里,任何一个RuntimeException跑出来,前面执行的数据库操作都会回滚。
库存扣减这里有一句SQL细节值得说一下:扣库存绝对不能先SELECT出来再在代码里减一,再UPDATE回去,因为这样会出并发问题。正确做法是直接在UPDATE语句里扣减,并且加一个库存大于0的条件:
UPDATE flower SET stock = stock - #{quantity} WHERE id = #{flowerId} AND stock >= #{quantity}这个语句返回的影响行数为0,说明库存不足,直接抛业务异常让整个事务回滚。这也是企业开发里典型的乐观锁思路,特别适合在答辩的时候讲出来——这一句话就能体现你的工程意识,而绝大多数课设代码是写不出这个细节的。
3.5 商品接口和订单模块的代码实现要点
商品模块大部分都是简单的CRUD,但有一个接口需要特别说明,就是商品列表的分页查询。如果你用了MyBatis-Plus,分页非常简单,只要配置一个分页插件,然后调用Page对象查询即可;如果用的是通用Mapper或者手写SQL,就需要自己写LIMIT语句并计算总页数。建议有条件的话早用MyBatis-Plus的IPage分页,会让你省掉很多代码量。
订单模块有一个接口也值得单独讲,就是“订单收货”这个操作。用户在前端看到订单状态为已发货时,可以点击确认收货,此时订单状态从3(已发货)变成4(已完成)。这个接口的SQL很简单,但需要注意限定条件:只有这个订单属于当前登录用户,且当前状态是3,才能更新成4,否则直接给提示错误,防止用户把别人的订单点了。
3.6 application.yml配置注意事项
我当时配置的application.yml重点有几个字段:数据源的地址和账号密码、Tomcat端口、MyBatis的MapUnderscoreToCamelCase配置(开启驼峰映射,数据库的create_time自动对应Java的createTime)、文件上传大小限制。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/flowershop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: trueserverTimezone=Asia/Shanghai这个参数建议加上,不加的话MySQL驱动可能会报时区错误。如果你用的MySQL是8.x版本,记得驱动类要用com.mysql.cj.jdbc.Driver,而不是老版的com.mysql.jdbc.Driver,否则也是启动即报错,这两点是运行时最常见的问题。
4. 前端Vue页面设计与接口联调
4.1 用户端页面结构和路由设计
前端我使用的是Vue全家桶:Vue2+Vue Router+Axios+Element UI。这个组合非常成熟,网上资料也多,遇到问题一搜到处都是解决方案。
页面结构上,我拆成了两个入口:
- 前台用户端(面向普通用户):首页、商品列表页、商品详情页、购物车页、结算页、个人中心(包含我的订单)。
- 后台管理端(面向管理员):登录页、后台布局页,内含商品管理、分类管理、订单管理、数据概览等子页面。
路由层面,使用Vue Router的懒加载按需导入组件,后台加上路由守卫,判断localStorage里是否有管理员Token,没有就跳回登录页。
4.2 接口请求的封装与代理配置
前端每个页面直接axios请求后端地址会有两个问题:一是每个组件都要重复写baseURL和token,二是跨区问题绕不开。所以我把axios封装成了一个request工具模块,在utils目录里统一创建axios实例,设置baseURL为/api,在请求拦截器里自动从localStorage取出Token并加到Header里,在响应拦截器里统一处理错误码并弹出提示。
Vue开发服务器的端口是8081(或9528),后端SpringBoot是8080,两者不同端口必然存在跨域问题,但好用的解决办法是在前端项目的vue.config.js里做代理,把所有/api开头的请求转发到http://localhost:8080:
module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } };这样前端代码里写请求路径只需要写/api/flower/list,开发时不会跨域,后期部署到服务器时把/api反向代理到后端端口即可。很多新手不懂这个原理,直接在axios里写http://localhost:8080然后被跨域折磨一整晚,看到这里你直接复制配置就行。
4.3 商品展示和购物车页面的核心实现
商品列表页用Vue的卡片组件循环渲染,数据来自页面的flowerList变量,通过this.$axios.get('/api/flower/list', {params})拿到。有一个功能是分类筛选,我用了Tabs标签页或者下拉选择,切换时重新带条件请求后端接口,逻辑不复杂但页面效果很加分。
商品详情页展示图片、名称、价格、库存、描述,用户点击加入购物车时组装好参数去请求后端。一个细节是按钮要在库存为0时置灰,并且显示“缺货”状态,这个小交互会显得页面很专业。
购物车页面的核心操作是:每次修改数量,调用后端接口更新数据库中的记录,同时前端重新计算小计和总计。记住,一定不要只在页面上改数字而不调后端接口。因为结算的时候后端会重新算总价并用事务保证一致性,但如果购物车本身不同步,用户每次进页面都要重新刷新,体验很不好。
4.4 后台管理页面的实现技巧
后台页面我直接用了Element UI的Layout布局组件,左侧是菜单栏,右侧是内容区。菜单项就是商品管理、分类管理、订单管理、数据概览。这样一套后台模板搭好,后面所有管理页面都是套模板往里填数据,效率非常高。
商品管理页面核心是一个表格加一个弹窗:表格展示商品所有字段,行内放“编辑”“上架/下架”“删除”操作按钮;弹窗用来新增或者编辑商品。
编辑商品时有一个图片上传的问题:推荐上传到服务器本地目录(用SpringBoot的静态资源映射),或者如果你搭建了MinIO,也可以上传到MinIO图床。上传接口返回图片URL后,把这个URL回填到表单的image字段里。同时用Element UI的Upload组件配合action属性指定后台上传接口地址,在on-success回调里把返回的URL存到表单数据里。
4.5 路由守卫和子路由的配置参考
后台部分的Vue Router配置我用了嵌套路由,整个后台作为一个父路由组件(布局组件),再把商品管理、分类管理、订单管理等页面配成它的children。这样搭配el-menu的router模式,点击菜单就能自动切换子页面,代码量非常少但效果很优雅。
{ path: '/admin', component: AdminLayout, children: [ { path: 'flowers', component: () => import('@/views/admin/FlowerManage.vue') }, { path: 'categories', component: () => import('@/views/admin/CategoryManage.vue') }, { path: 'orders', component: () => import('@/views/admin/OrderManage.vue') } ] }同时路由守卫中检查当前路由是否以/admin开头,如果是就校验管理员身份,只要用户了登录且为管理员才放行,否则跳转登录页。前端守卫只是体验优化,真正保证安全的还是后端接口的Token校验,这个你心里要有数。
5. 全栈联调、部署运行与常见问题排查
5.1 从零到一运行整个项目的完整流程
先说本地开发环境的准备:JDK 8或者11(推荐8,兼容性最稳)、IDEA、MySQL 5.7或8.0、Vue的Node环境、Navicat或命令行工具建库。
运行步骤很固定,我列出实际操作顺序:
第一步,MySQL中创建数据库,比如create database flowershop,然后导入建表SQL。推荐在运行项目之前就先把表建好,不要等启动后让框架自动建表,免受不必要的麻烦。
第二步,打开IDEA,File -> Open选择后端项目文件夹,等Maven把依赖下载下来。如果没有本地Maven仓库,首次下载会比较久,建议配一下阿里云镜像,速度会快很多。
第三步,修改application.yml里的数据库账号密码,改成你自己本机的。
第四步,直接运行FlowerShopApplication主类的main方法,看到SpringBoot启动成功的日志,说明后端已经跑起来了。
第五步,打开命令行工具进入前端项目目录,执行npm install安装依赖。这一步如果慢,同样可以配置淘宝镜像npm config set registry https://registry.npmmirror.com。
第六步,依赖装完之后运行npm run serve,等Vue编译完成,浏览器访问http://localhost:8081就能看到系统页面。
5.2 部署到服务器上的简化方案
课程设计的部署,我没有推荐去买昂贵的服务器,最简单的方案是用腾讯云或阿里云的轻量应用服务器跑CentOS。
部署步骤概括起来是三步:后端打JAR包上传服务器之后用java -jar flowershop.jar直接跑起来;前端执行npm run build打出一个dist目录,然后在服务器上装一个Nginx,将dist目录作为Nginx的根路径;在Nginx配置中加一个反向代理,把/api路径的请求转发到本机8080端口。
核心的Nginx配置片段:
server { listen 80; server_name localhost; location / { root /usr/local/flowershop/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行必须有,因为Vue是单页应用,刷新路由时如果直接刷新 /admin/flowers 路径,Nginx找不到物理文件就会404,try_files会把它重新引到 index.html 入口。
5.3 常见问题排查速查表
做这个项目时最常踩的坑,我整理成了一张排查表,按照出现的频率从高到低排序,你遇到问题直接按这个查就行。
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 后端启动时报数据库连接失败 | 数据库名写错、密码不对、驱动不对 | 检查url里的库名和密码,MySQL8必须用cj驱动 |
| 访问后端接口提示CORS跨域 | 前后端端口不一致且未做代理 | 使用vue.config.js配置代理,不要在前端代码里写死8080 |
| axios请求返回404 | 前端代理路径和后端mapping路径不一致 | 检查代理里的pathRewrite是否把/api去掉了 |
| 商品列表是空的 | 数据库里还没插数据 | 在flower表里插入几条测试数据,记得status设为1 |
| 注册后登录提示用户名不存在 | 用户表设计时没建唯一索引 | 检查username的unique key,建索引再测 |
| 下单报库存不足 | 扣减逻辑用了先查再改 | 改成UPDATE ... WHERE stock >= quantity的写法 |
| 图片上传成功但显示不出来 | 静态资源路径没有映射 | 在配置类里addResourceHandlers映射本地存储路径 |
| npm run serve很慢或者报错 | 依赖版本冲突或镜像速度慢 | 删除node_modules重新install,配置淘宝镜像 |
5.4 几个我踩过的实际教训
图片上传这个功能,我一开始直接存在数据库字段里,结果数据库体积涨得飞快,而且每次查询商品列表都要传一大坨数据,页面卡得很。后来改成上传到服务器本地一个upload目录,然后数据库只存相对URL路径。这里有个配置关键,SpringBoot默认访问不到项目外的静态资源,所以需要一个WebConfig配置类,把/upload/**路径映射到存储目录。
另一个教训是订单状态字段的问题。我最早用字符串状态,比如“pending”“paid”“shipped”,前后端联调时倒没出什么问题,但是数据库统计时写SQL很别扭。后来改成数字枚举状态,配合前端一个状态映射对象,代码量反而更干净。
还有一点是前端部署时刷新404的问题,我第一次部署的时候完全没意识到Vue Router需要try_files配置,结果用户从管理后台点浏览器的刷新按钮,直接白屏。这个问题查了很多资料才定位到Nginx配置,这也印证了前面说的那行try_files配置基本是刚需,只要用了Vue Router的history模式就绕不开它。
5.5 项目完成后的扩展方向
花店系统做完主流程之后,如果要再往上提一个档次,有几个方向可以针对性地展开:新增一个促销或者优惠券表、订单对接第三方支付模拟实现、引入Redis做热点商品缓存、用ECharts给管理后台加一个销售统计看板。
这些扩展点每一个都能对应到技术亮点上,比如Redis缓存热销花卉、ECharts做销量趋势折线图、支付回调的异步处理。写到论文的创新点部分时,能切实增加技术含金量。
我在实际做这个项目时还有一个体会:花店销售系统的代码量在所有JavaWeb毕设里属于中等偏下,但正因为业务模型完整、技术栈主流、扩展方向多,反而特别适合用来当作一个完整项目来练手和完善。把这个系统从头到尾跑通一遍,你对SpringBoot接口开发、Vue组件通信、前后端联调、Linux部署这套流程的熟悉程度,基本上可以吊打绝大多数同届毕设。有条件的同学建议不要只在本地跑通就算结束,找一台云服务器把它部署上去,拿手机浏览器通过公网IP访问一次,这个成就感和对整体架构的理解,是没有部署经验的同学很难体会的。