☰
基于Web的酒品商城购物系统设计与实现:Spring Boot+Vue前后端分离实践
2026/10/2 14:59:11 网站建设 项目流程

做计算机毕设选题的时候,很多人一看到“基于web的酒品商城购物系统的设计与实现”这种题目,就开始纠结:是先把Java Web基础啃完,还是先学Vue?其实这类题目是最典型、也最稳的电商类毕设,核心就一句话——把用户、商品、购物车、订单、后台管理这几件事做扎实,论文和答辩内容就都有了。我这两年帮人排查过的毕设项目里,酒品商城和图书商城、办公用品商城在业务逻辑上几乎同源,区别只在于商品字段和几个特别页面。今天这篇就围绕“基于web的酒品商城购物系统”这个题目,把我自己常用的一套设计与实现思路完整过一遍,顺便把那些代码里容易踩的坑一次说清楚。适合正在准备毕设、想找一个能真正跑起来并且能讲清楚的web项目的同学参考。

1. 别急着写代码:先吃透需求与角色划分

1.1 这个系统到底要做什么

先把这个题目拆开看:基于web,说明通过浏览器访问;酒品商城,说明是销售酒类商品的电商平台;购物系统,说明核心流程是“逛商品—加购物车—下单—收货”。如果只是照着网上的源码跑起来就以为完事了,答辩时一问三不知,那才真的麻烦。所以我习惯先把需求翻译成用户能感知的功能。

酒品和其他商品不一样的点在于品类属性更强:浓香、酱香、清香、葡萄酒、啤酒,不同分类下的用户筛选逻辑不同;而且酒品详情页通常要展示酒精度、净含量、产地、年份这些特征。这意味着数据库设计时不能只做简单的“商品表”,还要考虑扩展字段。虽然毕设论文一般不要求做到电商中台的粒度,但字段设计上把容量、酒精度、产地这几个单独列出来,答辩时就能讲出“我考虑了行业特殊性”。

从角色上看,整个系统分为游客、注册用户、管理员。游客只能看商品和搜索;注册用户除了看,还能加购物车、下单、管理地址、查看订单;管理员负责商品上架、分类维护、订单发货和用户状态管理。这个划分清晰了,权限控制才有依据。

1.2 用户端与后台管理端的功能清单

我一般会让学生先把功能清单做成表格,再据此拆页面和接口。这样既方便安排开发进度,也方便写到论文的“需求分析”章节。

端功能模块具体操作
用户端账号注册、登录、退出,密码加密存储
用户端商品浏览首页轮播、商品分类导航、酒品列表、搜索、详情
用户端购物车加入购物车、修改数量、删除、全选结算
用户端订单确认订单、选择收货地址、提交订单、模拟支付、订单列表、取消订单、确认收货
用户端个人中心个人信息修改、收货地址管理
管理端商品管理酒品增删改查、上下架、库存调整、上传图片
管理端分类管理酒品分类新增、改名、删除
管理端订单管理查看订单、按状态筛选、发货操作
管理端用户管理查看用户列表、禁用/启用用户

这里有一个容易被忽略的点:订单状态不能只有“已支付/未支付”两个状态。电商系统天然要求订单有生命周期,我建议至少设计成“待付款、待发货、待收货、已完成、已取消”。一方面是因为业务真实,另一方面是答辩时你可以画一张状态流转图,讲清楚哪个状态下用户能做什么操作、管理员能做什么操作,这属于很好的加分内容。

1.3 边界控制:哪些功能可以砍掉

做毕设最怕的就是需求失控。很多同学一开始想做一个“小淘宝”,优惠券、秒杀、评论、直播带货全都要,最后代码攒了几千行,Bug也多得改不完。我个人的建议是:主流程必须完整,营销功能尽量不做。

可以砍掉的功能包括:优惠券、满减活动、秒杀、购物车跨端同步、物流轨迹查询、在线客服、商品评价。这些功能不是不重要,而是它们会引入大量额外的前端交互和后端状态处理。比如秒杀就要考虑高并发库存,这超出了毕设的范畴。你可以在论文的“系统不足与展望”里写“后期可扩展优惠券模块”,反而显得你明白系统的边界在哪里。

如果学有余力,建议优先做两个“轻量加分项”:一个是后台的简单数据统计,比如商品总数、今日订单数、订单总金额;另一个是订单的模拟支付流程。这两个东西工作量不大,但能让系统看起来完整很多。

2. 技术选型:为什么我说“Spring Boot + Vue + MySQL”是省心组合

2.1 前后端分离与单体JSP的取舍

如果你翻到五年前的毕设源码,大量题目还是Spring MVC + JSP + JSTL那一套。JSP时代不是不能用,而是现在做项目,前后端分离已经是团队协作的主流方式。用JSP最大的问题在于前端页面和后端逻辑耦在一起,改个页面样式就要重新部署项目,而且答辩时也很难说清楚“接口设计”这件事。

前后端分离则完全不同:后端专心提供JSON接口,前端专心渲染页面。两边通过HTTP交互,开发时可以并行,部署时也灵活。对毕设来说,采用前后端分离还有一个实际好处:你很容易用Postman或Apifox来演示接口,答辩时把请求和返回数据一展示,老师立刻明白你的系统架构是清晰的两层结构。

当然,如果你Java基础确实比较薄弱,跟前端分离+部署Nginx也让你头疼,退一步用Spring Boot + Thymeleaf也可以。Thymeleaf比JSP现代,而且能实现模板继承,关键是它仍然是个服务端渲染方案,写起来更简单。只是后续讲接口层面的内容会少一点。选哪种不丢人,但如果你问我,我优先推荐前后端分离。

2.2 后端为什么用Spring Boot

Spring Boot不是靠“约定大于配置”这句话火的,它是真的把项目启动成本降下来了。你创建一个Spring Boot项目,内置Tomcat,写好Controller就能跑,不需要再单独装Tomcat、配web.xml、手动打war包。这对于毕设周期来说非常重要。

版本选择上,我建议用Spring Boot 2.7.x。原因很简单:网上资料最多,跟MyBatis-Plus、Spring Security等组件的兼容性最稳。如果你电脑上装的是JDK 8,那2.7.x非常合适;如果装的是JDK 17,也可以用2.7.x,它支持到JDK 17。Spring Boot 3.x现在也成熟了,但它要求JDK 17起,而且部分老博客里的配置会失效,容易踩坑。毕设求稳,没必要追新。

配套的持久层框架,我建议直接用MyBatis-Plus。它既保留了MyBatis的SQL控制力,又提供了BaseMapper的通用单表CRUD和分页插件。对于酒品商城这种系统,90%的数据库操作都是单表查询和简单联表,MyBatis-Plus能让你少写大量重复的XML映射文件。

2.3 前端为什么用Vue而不是纯HTML

纯HTML + CSS + JavaScript也能写商城前台,而且文件结构简单。但问题在于:商品列表的状态、购物车的数量、订单确认页的数据共享,用原生JS写起来到处都是DOM操作,代码会越来越乱。Vue的核心价值是响应式数据绑定和组件化。你只需要维护一个JavaScript对象,页面上的内容会自动跟着变,这对商城类项目是刚需。

我前端的推荐组合是Vue 3 + Vite + Vue Router + Pinia + Element Plus。Vite比Webpack启动快很多,对新手的体验更友好;Pinia管理购物车和用户登录态非常顺手;Element Plus提供后台管理所需的后台表格、表单、弹窗等组件,可以让你的管理页看起来有模有样。

2.4 开发环境与依赖版本建议

别把环境版本搞得太跳,不然出现莫名其妙的问题时你都分不清是代码问题还是环境问题。我这里列一套经过验证的组合:

软件/框架建议版本说明
JDK1.8 或 11长期支持版本,兼容性好
Maven3.6+管理后端依赖
Spring Boot2.7.18稳定且资料丰富
MyBatis-Plus3.5.x配合Spring Boot 2.x
MySQL5.7 或 8.0使用InnoDB引擎
Node.js16 或 18运行前端工程
Vue3.x配合Vite 4/5
Element Plus2.xUI组件库

注意:MySQL 8.0和5.7的驱动类名不同。8.0的驱动类是com.mysql.cj.jdbc.Driver,5.7是com.mysql.jdbc.Driver。很多同学报错ClassNotFoundException就是因为驱动写成了老版本。

3. 数据库设计:酒品商城最关键的是别把关系弄错

3.1 核心表结构设计

这张数据库ER图是整个项目的地基。我见过太多同学把用户的收货地址直接存在用户表里,把购物车记录直接存在一张大宽表里,后面写代码的时候发现越写越别扭。别着急写SQL,先把表之间的关系梳理清楚。

主表大致有这几张:用户表、分类表、酒品表、购物车表、收货地址表、订单主表、订单明细表。去掉购物车表也能做,可以从购物车直接提交后把它删除,但为了用户登录后能持久化购物车,还是建议建表。

看下单表的设计,这是比较核心的部分。用户下单后,订单里要记录收货人信息、订单总金额、订单状态。很多人会问:为什么收货人和手机号不从用户表里取,而要冗余到订单表?原因很简单,用户后来改了地址信息,历史订单也必须保留当时的下单地址。这就是“下单时生成快照”的思路。同理,订单明细表里也要冗余商品名称和商品快照价格,因为酒品价格以后肯定会调整,但已经创建的订单不应该受商品价格变动影响。

下面是一张酒品表的SQL示例,字段比普通商品多了一些酒类特有的信息:

CREATE TABLE `t_wine` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '酒品ID', `category_id` bigint NOT NULL COMMENT '分类ID', `name` varchar(100) NOT NULL COMMENT '酒品名称', `subtitle` varchar(255) DEFAULT '' COMMENT '卖点副标题', `main_image` varchar(255) DEFAULT '' COMMENT '主图URL', `detail` text COMMENT '商品详情', `price` decimal(10,2) NOT NULL COMMENT '售价', `stock` int NOT NULL DEFAULT 0 COMMENT '库存', `alcohol_content` decimal(4,1) DEFAULT NULL COMMENT '酒精度%vol', `volume` int DEFAULT NULL COMMENT '净含量ml', `origin` varchar(50) DEFAULT NULL COMMENT '产地', `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:1在售,0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='酒品表';

分类表很简单,就是id、分类名称、排序、创建时间。购物车表主要字段是id、用户id、酒品id、数量、加入时间,可以加一个唯一约束,保证同一用户同一酒品只出现一条记录。订单主表和明细表的SQL我就不贴完整了,重点强调:订单主表要有订单号字段,订单号可以用时间戳+用户id生成,也可以用数据库自增主键加日期拼接,但一定要保证唯一,便于前后端追踪。

3.2 字段设计的几个坑

第一,金额永远不要用float或double。0.1加0.2在浮点数里会变成0.30000000000000004,结算金额一旦出现这种误差非常尴尬。Java后端用BigDecimal,数据库用decimal(10,2),两个地方都堵住。

第二,库存字段直接用int就行,但扣库存的SQL必须带条件。后面下单流程我会专门讲,这里提一句:永远不要先查库存再在Java代码里相减,要走UPDATE t_wine SET stock = stock - 1 WHERE id = ? AND stock >= 1这种原子操作。

第三,字符集统一用utf8mb4。饭馆里有些酒品名称会带特殊字符,比如英文商标里的符号,utf8mb4才能完整存储。数据库连接串也要带上characterEncoding=utf8。

第四,关于逻辑删除。用户表、商品表建议加一个deleted字段,默认为0,做删除操作时更新为1,查询时自动过滤。MyBatis-Plus本身有逻辑删除的全局配置,开会话时可以提一下。

3.3 索引与事务考虑

商城业务里,商品列表查询和订单查询是高频操作。商品表除了主键索引,一定要在category_id上建索引;订单表要在user_id上建索引,否则用户查自己的历史订单时只能全表扫描,数据一多就会卡。酒品名称的模糊查询走不走索引要看你的like写法,%关键词%肯定不走,但如果数据量只有几百条,影响不大。

事务方面,最核心的一条规则:生成订单、扣库存、清空购物车这三个操作必须放在同一个事务里。任何一个失败,前面做的操作必须全部回滚。否则就会出现用户没下单成功、库存却扣了的严重问题。在Spring Boot里,只需要在Service方法上加上@Transactional注解,同时注意默认只捕获RuntimeException,如果自己手动catch了异常然后不抛出,事务是不会回滚的。

4. 核心功能实现:从商品列表到下单全流程

4.1 前端页面路由与组件划分

前端页面不需要一开始就铺开,我建议按照用户操作路径去拆分。先做首页和商品列表,再做详情和购物车,再做订单流程,最后做后台管理。这样每完成一段流程,系统就能跑通一个业务闭环,心里不慌。

Vue Router的路由示例:

const routes = [ { path: '/', name: 'Home', component: HomeView }, { path: '/list', name: 'List', component: ListView }, { path: '/detail/:id', name: 'Detail', component: DetailView }, { path: '/cart', name: 'Cart', component: CartView }, { path: '/order/confirm', name: 'OrderConfirm', component: OrderConfirm }, { path: '/order/list', name: 'OrderList', component: OrderList }, { path: '/admin', name: 'AdminLayout', component: AdminLayout, meta: { role: 'admin' } } ]

后台管理路由需要加上守卫,简单做法就是判断本地存储里的用户角色是不是admin。不是管理员就重定向到首页,这样至少能拦住非管理员通过URL直接进入后台页面。真正的接口权限控制的重点还是要放在后端,这个我后面说。

页面组件化的细节也很重要,比如商品卡片可以抽成WineCard.vue,首页和列表页都复用,避免改样式要改两遍。这一步虽然不起眼,但代码目录结构干净,答辩时也能加分。

4.2 商品搜索与分页接口

后端提供商品列表接口时,必须支持分页。分页不是可选项,是商城的基本配置。一个简单的接口设计如下:

@RestController @RequestMapping("/api/wine") public class WineController { @Autowired private WineService wineService; @GetMapping("/list") public Result<PageResult<WineVO>> list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "12") Integer size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { Page<Wine> pageParam = new Page<>(page, size); LambdaQueryWrapper<Wine> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(categoryId != null, Wine::getCategoryId, categoryId) .and(keyword != null && !keyword.isEmpty(), w -> w.like(Wine::getName, keyword).or().like(Wine::getSubtitle, keyword)) .eq(Wine::getStatus, 1) .orderByDesc(Wine::getCreateTime); Page<Wine> result = wineService.page(pageParam, wrapper); return Result.success(PageResult.of(result)); } }

这段代码里有一个小细节:status = 1的过滤条件必须写上去,确保下架商品不在商城展示。很多人会把“上下架”交给前端判断,其实前端根本不可信,后端过滤才是底线。前端传categoryId和keyword时用URL query参数,size默认12,正好对应一页商品展示12个小卡片的常规布局。

4.3 购物车:登录态与本地态怎么处理

购物车有两条常见路线:一种完全存在前端localStorage,另一种完全存在后端数据库。我的建议是别纠结,毕设做后端数据库方案。理由是你后面下单的时候要在后端重新计算金额和校验库存,购物车数据如果只在前端,下单逻辑就得整体从前端传购物车列表给后端,前端传什么后端就信什么,这本身就有安全隐患。

后端购物车表的结构之前已经说过,核心方法就几个:加入购物车、修改数量、删除、查询用户购物车列表。加入购物车时,如果同一个酒品已经在购物车里,那么数量累加,不要插两条记录。这个逻辑可以用唯一约束加ON DUPLICATE KEY UPDATE实现,也可以在Java代码里先查一次再insert。

如果你还想再简单一点,可以把购物车放在Redis里,使用Hash存储,key是用户id,field是酒品id,value是数量。Redis的好处是天然支持存活时间和高性能。但毕设有时候不想额外引入中间件,那数据库表其实完全够用。

4.4 下单与库存扣减:别让超卖毁掉答辩

下单是整个项目技术含量最高、也最能拿来说的部分。简单流程是这样:

第一步,根据当前登录用户获取购物车列表;第二步,从购物车列表取出酒品id列表,查询数据库中的最新商品信息;第三步,校验商品状态、库存是否充足;第四步,计算总金额,记住用数据库里的价格,不能用前端传过来的价格;第五步,插入订单主表和订单明细表;第六步,扣减库存;第七步,清空购物车。

扣库存的SQL是防止超卖的关键:

UPDATE t_wine SET stock = stock - #{quantity}, update_time = NOW() WHERE id = #{wineId} AND stock >= #{quantity}

执行这个update语句后,如果返回的影响行数是0,说明库存不足,直接抛出异常让事务回滚。这就是乐观锁思想的一种简化实现,通过stock >= quantity条件确保不会把库存扣成负数。很多人喜欢先SELECT stock,然后在Java代码里if判断是否大于数量,再UPDATE stock = stock - quantity,这样在多线程同时下单时会超卖。虽然毕设一般没有高并发压测,但答辩老师一定会问“你这个扣库存会不会出问题”,把这条SQL拿出来讲,专业度立刻不一样。

下单的事务实现示例:

@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, Long addressId) { List<CartVO> cartList = cartMapper.selectUserCartList(userId); if (cartList == null || cartList.isEmpty()) { throw new BizException("购物车为空"); } // 计算总金额 BigDecimal totalAmount = BigDecimal.ZERO; for (CartVO item : cartList) { Wine wine = wineMapper.selectById(item.getWineId()); if (wine == null || wine.getStatus() != 1) { throw new BizException("商品已下架:" + item.getWineName()); } if (wine.getStock() < item.getQuantity()) { throw new BizException("库存不足:" + item.getWineName()); } totalAmount = totalAmount.add(wine.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 生成订单 Order order = new Order(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setAddressId(addressId); order.setTotalAmount(totalAmount); order.setStatus(0); orderMapper.insert(order); // 插入明细 + 扣库存 for (CartVO item : cartList) { OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setWineId(item.getWineId()); orderItem.setWineName(item.getWineName()); orderItem.setPrice(item.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); int rows = wineMapper.deductStock(item.getWineId(), item.getQuantity()); if (rows == 0) { throw new BizException("库存扣减失败"); } } // 清空购物车 cartMapper.deleteByUserId(userId); return order.getId(); }

这一步里用@Transactional(rollbackFor = Exception.class)显式声明所有异常都回滚,不光是运行时异常,这是我特别想提醒的。

4.5 订单状态管理

订单状态是整个商城业务的一条主线,建议用整数来存,状态定义做成常量或枚举。我常用的状态定义:0待付款、1待发货、2待收货、3已完成、4已取消。

状态流转的规则要非常明确:用户下单后状态是0;用户点击“模拟支付”后,后端把状态从0改成1,这里一定要校验当前状态必须是0,不能允许把已取消的订单直接支付;管理员在后台把状态从1改成2,表示发货;用户点击“确认收货”后,状态从2改成3;用户可以在待付款状态取消订单,变成4。

这个状态机看起来简单,但很多人会写成“无条件更新状态”。比如直接写update order set status = 1 where id = ?,那就会导致已取消的订单被支付、已经发货的订单被重复发货。正确做法是更新时带条件:

UPDATE t_order SET status = 1 WHERE id = #{orderId} AND status = 0

影响行数为0时提示“订单状态异常”。这个小细节值得在论文或答辩中专门提一句:我通过条件更新保证状态流转的合法性。

5. 这几个“加分项”值得做,但不必贪多

5.1 后台管理端的权限拦截

大多数毕设系统的权限拦截,都是用拦截器检查请求头里有没有Token。具体做法是:用户登录成功后,后端生成一个UUID或JWT返回给前端,前端每次请求都把它放到HTTP Header里。后端写一个HandlerInterceptor,在进入Controller之前检查这个Token是否存在、对应的用户是不是管理员。

这里我给你一个建议:不要为了省时间跳过拦截器,因为“购物车、订单、后台增删改查”的接口一旦裸奔,你无法回答“我这个接口怎么保证安全”这类问题。简单的拦截器代码如下:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null || !TokenStore.isValid(token)) { response.setStatus(401); return false; } return true; } }

注意还需要区分普通用户和管理员。最简单的办法是在Token里存用户ID和角色类型,如果是管理员接口,再查一次用户角色做校验。这样不需要引入Spring Security,也能把权限问题讲明白。

5.2 文件上传(酒品图片)

后台管理里必然要上传酒品图片。比较稳妥的实现是把上传的图片保存到服务器本地某个目录,然后把访问路径存到数据库。步骤如下:获取MultipartFile;校验文件后缀是jpg、png还是jpeg;生成唯一文件名,比如UUID加原后缀;写到指定目录;返回/images/xxx.jpg这样的访问URL。

这里要提醒两个坑:第一,项目部署时上传目录最好不要放在idea工作区的target目录里,否则重新打包后文件会被清掉。更好的做法是配置一个绝对路径目录,然后让Spring MVC的静态资源映射指向它:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/**") .addResourceHandler("file:" + uploadPath + "/"); } }

第二,文件重命名绝对不能直接用用户上传的文件名,否则会有路径穿越风险。虽然毕设不要求像企业一样做安全审计,但不使用原始文件名是一个基本习惯。

5.3 简单的支付模拟:别真接支付网关

很多同学纠结是不是要对接支付宝微信支付。我的建议非常明确:不要接,除非你只是想体验一下。真实支付需要商户号、密钥、回调地址,而且涉及资金,不适合毕设环境。你只需要做一个“模拟支付”页面,用户点击“确认支付”,后端调用一个接口把订单状态从待付款改成待发货。为了看起来严谨,你还可以在订单支付记录表里插入一条支付流水,金额、时间、支付方式都记录下来。这样系统照样能讲成“完整的订单闭环”,并且可以在“展望”里说“目前是模拟支付,后续可对接第三方支付接口”。这是非常合理的取舍。

5.4 日志与全局异常处理

这一个加分项成本很低,收益却不小。统一返回结果对象Result<T>能让所有接口的返回格式保持一致:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } }

再配合@RestControllerAdvice做全局异常处理,把业务异常和系统异常分别返回不同的code。这样做的好处很多:一是前端统一处理错误弹窗;二是数据库报错不会把堆栈直接暴露给浏览器;三是这属于“工程规范”层面的内容,写论文时放在系统实现里能体现你的工程素养。

6. 常见问题与排查技巧实录

6.1 中文乱码问题

中文乱码是Java Web项目的经典问题。排查顺序从外到内:第一步检查数据库表字符集,是否utf8/utf8mb4;第二步检查数据库连接串,是否有characterEncoding=utf8;第三步检查后端响应头,Spring Boot一般默认返回UTF-8,但如果用了老版本的Spring MVC,可以在@RequestMapping的produces里指定;第四步检查前端页面meta标签。我见过最多的情况是连接串没写编码,导致存进去中文变成问号。

6.2 跨域问题

前后端分离开发时,前端跑在localhost:5173,后端跑在localhost:8080,浏览器会拦截后端的异步请求。解决办法有两种:开发环境用Vite的proxy代理,把/api请求转发到8080;或者后端统一配置CORS。我更推荐前端代理方案,因为它更接近生产环境的同源配置,也不会暴露后端端口。

但如果你写的是管理端和前台同时访问同一个后端,那么直接在后端加一个CorsConfig也很常见:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

6.3 刷新页面404

Vue Router使用history模式时,在路由页面刷新会出现404 Not Found,这是因为刷新时浏览器直接访问了/list这个路径,而后端服务器上没有这个物理文件。解决办法有两个:一个是把Vue Router改成hash模式,URL里会带#,刷新不会出问题,但不美观;另一个是部署时在Nginx配置try_files:

location / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; }

我一般建议在写论文反复截图时用history模式,因为URL干净,然后把Nginx配置放到部署文档里,既解决了问题又展示了运维能力。

6.4 结算金额不一致排查

下单时前端展示的金额如果和后端订单金额对不上,首先不要怀疑前端计算,要怀疑后端有没有信任前端传入的金额。排查步骤:查看下单接口入参里有没有totalAmount字段;如果前端传了这个字段,就说明后端又把它存进了订单表,这非常危险。正确做法是后端根据商品当前价格和购物车数量重新计算,前端传的金额只能用于展示。发现不一致时,优先检查后端是否重新查了商品价格。

6.5 数据库连接失败

初学者最常见的是MySQL 8.0的时区报错:The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。解决办法是在JDBC连接串后面加serverTimezone=Asia/Shanghai。此外,还要确认数据库驱动版本和MySQL版本要对应,8.0版本的驱动不能用在5.7的旧写法上,反之亦然。

下面把这几个常见问题汇总成表,方便你排查:

现象常见原因解决办法
存储中文变成问号表和连接串字符集不对统一utf8mb4,URL带characterEncoding
前端请求后端报跨域浏览器同源策略后端CORS或前端proxy
刷新子路由404history模式缺少降级Nginx try_files或改用hash
订单金额不对后端信了前端传的价格后端用数据库价格复算
启动时报数据库时区错MySQL 8.0时区参数加serverTimezone=Asia/Shanghai
登录后后台仍可绕过权限拦截只做了前端后端Interceptor统一校验

7. 部署上线与答辩前最后准备

7.1 本地打包与云服务器部署

系统写完后,总要部署到一台云服务器上演示,或者至少在本地打包给老师看。后端打包非常直接,在IDEA里执行mvn clean package,得到可执行的jar包。服务器上只需要装好JDK和MySQL,然后运行:

java -jar wine-mall-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod

前端打包更简单,执行npm run build后生成dist目录。把dist里的所有文件上传到Nginx的html目录。为了让后端接口和前端页面通过同一个域名访问,在Nginx里加一条反向代理:

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

这样前端请求/api/xxx会被转发到Java后端,而页面静态资源由Nginx直接返回,整体就形成了一个非常标准的前后端分离部署架构。

7.2 答辩时怎么讲项目亮点

答辩不是朗读论文,你要把系统理解和实现思路讲清楚。我的建议是准备一条线:“我用Spring Boot和Vue做了一个前后端分离的酒品商城,核心流程是用户浏览酒品、加购物车、提交订单、管理员发货。数据库设计了七张表,重点解决了库存扣减的一致性问题,并且通过登录拦截器保护后台接口。”然后顺着这条线延伸。

一定要准备两个问题:第一个是“为什么用MyBatis-Plus而不用Spring Data JPA”,你可以回答“因为商城系统有大量自定义查询,MyBatis对SQL控制更直接”;第二个是“如果用户同时下单导致库存超卖怎么办”,你就把那条UPDATE ... WHERE stock >= quantity的SQL和@Transactional讲出来。能把这个讲清楚,项目深度已经超过绝大多数毕设了。

7.3 我的一点建议

我见过很多同学拿到题目后第一反应是到处找源码,这本身没问题,但找到源码后一定要自己动手改一遍。你哪怕只是改了几个字段名、加了两个页面,也会逼着自己去理解表结构和接口调用关系。改完以后再跑一遍完整流程,你会发现很多“源码直接能跑”的说法其实不靠谱,数据库配置、依赖版本、前端API地址都需要调整。耐心把每一步调通,比收藏一百份源码都有用。

酒品商城这种题目,技术上不炫,业务上完整,是那种“做出来就有底气”的项目。如果你能把库存扣减的SQL、订单状态的条件更新、权限拦截器这三件事讲透,答辩老师的注意力基本就集中在你的实现细节上,而不是在问你“你的系统有什么创新点”这种不太好接的问题上。祝你把项目跑通,顺利通过答辩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询