☰
SpringBoot+Vue烟酒销售管理系统:从需求到答辩的全栈实战
2026/10/14 4:08:46 网站建设 项目流程

"基于SpringBoot+Vue的烟酒销售管理系统",光看这个题目,估计不少人会觉得平平无奇。但说句公道话,计算机毕设里选这种题,往往是"看着普通、做起来刚刚好"的典型。它没有复杂的分布式架构,也没有让人头皮发麻的高并发场景,但你需要的全栈技能——Java后端、Vue前端、MySQL建模、接口联调、权限控制——一个不落全练到了。我最近刚帮A同学把类似的系统从选题捋到答辩,借着这个机会把整个思路、实现细节和踩过的坑完整写出来,希望能给正在做JavaWeb毕设的你一点参考。

如果你是这类情况的读者——毕设题目已经定好但还不知道怎么做、或者正在犹豫要不要选这个方向、又或者代码已经写到一半发现漏洞百出——这篇文章应该都能帮上忙。我会从需求拆解开始讲,然后聊技术选型为什么是这一套,再给核心模块的实现思路和关键代码片段,最后整理联调和答辩时最常见的坑。啰嗦一句:别急着打开开发工具敲代码,先把整体框架想清楚,后面至少省一半时间。

1. 拿到题目先别急着写代码:需求拆解与边界确认

1.1 从"烟酒销售管理系统"里读出什么

先把这个题目拆开看。烟酒是业务范围,销售是核心动作,管理系统是落地形态。很多人看到最后四个字,脑子里蹦出来的全是"增删改查"——商品增删改查、订单增删改查、用户增删改查,然后就按这个思路一路做下去,结果做出来的东西像个电子表格,完全没有"系统"的样子。

实际上,这类题目真正想考察的,是你能不能把一条完整的业务链路用代码表达出来。烟酒商品因为有品类、品牌、价格、库存、特殊属性(比如年份、度数、容量),比单纯卖书的系统更有建模空间;订单跟踪意味着订单不是查出来躺在那,而是有状态、有流转、有操作节点的。"销售"两个字还隐含了购物车和下单这个动作,而"管理"则要求后台能对商品和订单做干预。

所以你在需求阶段就要先分清:这是一个"面向消费者的在线商城 + 面向运营者的管理后台"的复合系统,不是某个单页面管理系统。这个认知如果一开始没对齐,后面开发时很容易做成"前台一个列表、后台一个列表、中间没有业务逻辑"的空壳。

1.2 功能模块与角色划分

我一般建议把角色分成两类,复杂一点可以分三类:

角色诉求核心功能
普通用户找商品、买商品、看订单注册登录、商品分类浏览、搜索、购物车、下单、订单状态查询、确认收货
系统管理员管商品、管订单、看数据商品上下架、库存管理、订单发货、用户管理、销售统计

毕设系统不建议做回头客积分、会员等级、优惠券、秒杀这类营销功能,因为每多一个功能,数据库表、接口、前端页面、测试用例都会跟着翻倍。你要做的是一条"从货架到用户手里"的完整闭环,而不是一堆功能点的堆砌。

用户端有几个功能是必须有的:

  • 注册/登录:注册时填用户名、密码、手机号,密码做加密存储。
  • 商品展示:按分类展示商品卡片,支持按名称搜索,点进详情页能看大图、描述、库存和价格。
  • 购物车:能加商品、改数量、删条目、批量结算。
  • 下单:结算时填收货人、手机号、地址,生成订单。
  • 订单跟踪:订单列表展示状态,待支付可以模拟支付,已发货可以确认收货。

管理端这边,核心是商品管理、订单管理、分类管理、用户管理、统计看板。商品管理要有"上架/下架"概念,而不是直接删记录,因为历史订单还需要引用商品名称和图片。订单管理要能让管理员看到所有订单并按状态筛选,最关键的一个操作是"发货"——点击发货后,用户端订单状态就变了,这一步是"订单跟踪"闭环里管理者侧的动作。

1.3 给"订单跟踪"设计一条状态流

这是整个项目里最值得花时间设计的地方。很多人的订单表里只有一个status字段,然后用0、1、2、3这种魔法数字表示不同状态,代码里到处写if status == 1,最后自己都记不清1是什么。

正确做法是先定义清楚状态流转:

待付款 → 已付款/待发货 → 已发货 → 已完成

另外,待付款状态允许用户取消订单;已发货后不允许直接取消,但可以简化设计为不支持售后。把这个流转规则固定下来之后,有几个好处:

第一,接口设计清晰。比如"支付订单"就是把状态从待付款改成已付款;"发货"就是把状态从待付款改成已发货;"确认收货"就是把已发货改成已完成。每个状态变更对应一个独立接口,后端逻辑不纠缠。

第二,数据库字段可以加冗余的时间字段。order表里除了create_time,再加pay_time、ship_time、finish_time,对应每个关键节点的完成时间。用户端订单详情页展示状态时间线时,直接用这几个字段,不用每次都去日志表里查。

第三,论文里有图可画。状态流转图配一张,数据流图配一张,表结构放一张,答辩PPT直接有素材。

我在这里还要强调一个细节:状态值不要用阿拉伯数字硬编码在代码里,可以定义常量类,比如ORDER_STATUS_WAIT_PAY = 0,ORDER_STATUS_PAID = 1,这样代码可读性好很多。进阶做法是建一张order_status_log表,记录每一次状态变更的操作人、原状态、新状态、时间,这就是最简版的"审计日志"。有了这张表,老师问"订单跟踪到底怎么实现的",你就可以理直气壮地说:状态字段记录当前状态,日志表记录流转历史,时间字段记录节点时间,三者配合。

2. 技术选型的底层逻辑:SpringBoot+Vue为什么是黄金组合

2.1 SpringBoot靠"约定大于配置"赢在哪里

如果你只写过Servlet/JSP,或者只在课设里用过SSH框架,可能不太理解SpringBoot到底解决了什么问题。简单说,它把"搭一个Java Web项目"的门槛降到了极低。

以前建一个SSM项目,要配置web.xml、spring配置文件、springmvc配置文件,还要处理各种jar包版本冲突,光环境折腾就能耗掉两三天。SpringBoot用"起步依赖"的方式,把常用组件的版本全部管理好,你只需要引用对应的starter,再写一个启动类,内置Tomcat一跑,项目就能启动了。

对毕设来说,这个特性非常关键,因为你的精力应该放在写业务代码上,而不是配置环境。依赖里最核心的几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>

MyBatis Plus这里我要多说一句。如果你还在手写大量的ResultSet和PreparedStatement,或者为每一张表配一个复杂的XML Mapper,真的没必要。MyBatis Plus的BaseMapper把单表CRUD全部封装好了,selectById、selectList、insert、updateById直接能用,你只需要写多表关联或者复杂条件查询的SQL。毕设这种体量的项目,单表操作为主,用它能把开发速度提高一大截。

2.2 Vue组件化把前端化整为零

Vue在前端界的地位,有点像SpringBoot在后端——把复杂的事情变简单。它的核心思想是组件化开发:一个页面由多个组件拼接而成,每个组件有自己的模板、脚本和样式。这样商品卡片、商品列表、订单状态标签这些东西都能复用。

毕设里我建议直接用Vue 3 + Vite + Element Plus。选Vite不选Vue CLI,是因为Vite冷启动和热更新快太多了,改一行代码页面秒刷新,开发体验完全是两个世界。Element Plus是Element UI的Vue 3版本,提供表格、表单、对话框、分页、消息提示这些现成组件,后台管理系统几乎可以"拼"出来。

但是要注意,UI框架只是辅助,你自己的业务代码不能全靠它糊弄。比如"商品列表页",你不能只是拿一个el-table把数据库查出来的数据铺上去,而应该有自己的组件拆分思维:

  • HeaderBar.vue:顶部导航,包含搜索框、购物车入口、登录状态
  • ProductCard.vue:单个商品卡片,接收product对象,暴露"加入购物车"事件
  • CategorySidebar.vue:左侧分类栏,点击分类触发商品刷新

每个组件职责单一,通过props传数据、事件发消息,这种设计才是Vue Component的真正价值。答辩时老师问你"组件化体现在哪里",你能说出这套设计,比说"我用了一个很火的框架"有说服力得多。

2.3 前后端用RESTful接口怎么"说话"

前后端分离部署后,浏览器和服务器之间不再是"跳转页面"的关系,而是"发请求、收数据"的关系。后端只负责返回JSON,前端拿到JSON之后自己决定怎么渲染。为了不各说各话,接口要遵循一套约定。

我习惯的接口设计规则很简单:

  • 用名词表示资源,用HTTP方法表示操作。比如GET /api/product/{id}表示查某个商品,POST /api/order表示创建订单,PUT /api/order/{id}/ship表示发货。
  • 统一返回格式。所有接口都返回Result对象:
public class Result<T> { private Integer code; // 200成功 private String message; // 提示信息 private T data; // 真实数据 }

前端封装axios拦截器,看code是不是200,不是就统一弹提示。这样后端校验失败时,前端页面不会直接白屏或者看着一串英语报错。

这里有个容易被忽视的细节:时间格式。后端Java里用LocalDateTime,默认JSON序列化后是一串数字数组,前端根本没法直接显示。所以必须在配置文件里统一:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

或者在时间字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。这个坑几乎每个前后端分离项目都会踩一次,提前处理能省很多排查时间。

2.4 数据库表结构这样设计,论文才有话写

我把这套系统的核心表设计直接列出来,你照着建库至少不会跑偏。总共6张表,不多不少,足够把业务讲清楚。

表名核心字段说明
userid, username, password, nickname, phone, address, role, create_timerole区分普通用户和管理员
categoryid, name, parent_id, sort支持二级分类,比如"白酒/红酒/啤酒"
productid, category_id, name, brand, price, stock, sales, image, description, status, create_timestatus控制上下架
cartid, user_id, product_id, quantity一个用户对应多个商品条目
orderid, order_no, user_id, total_amount, receiver_name, receiver_phone, receiver_address, status, create_time, pay_time, ship_time, finish_time订单主表
order_itemid, order_id, product_id, product_name, product_image, price, quantity, subtotal订单快照表

有几个设计细节值得你写进论文:

第一,金额一律用decimal,不用double。double在二进制里是近似存储,算钱会出幺蛾子。比如0.1加0.2完全可能是0.30000000000000004,虽然不一定被老师发现,但专业评审看一眼字段类型就会皱眉头。

第二,order_item表里冗余了product_name、product_image。为什么?因为商品可能会改名字、换图片甚至下架,但已经产生的订单必须保持下单那一刻的记录。这叫"快照"思想,是电商系统的基础设计。

第三,status字段用int类型,配合常量类或者枚举类。这样数据库里的是0、1、2、3,代码里读起来却是ORDER_STATUS_WAIT_PAY,可读性高,出问题也好查。

3. 核心功能实现:从后端接口到前端下单页面

3.1 项目初始化与目录结构

后端用Spring Initializr初始化,groupId可以写成com.example,artifactId根据项目起名。前端用Vite脚手架创建。我习惯把工程分成两个目录:backend和frontend,后端归后端,前端归前端,互相不干扰。

后端包结构:

com.example.sales ├── config // 跨域配置、拦截器配置、MyBatis Plus配置 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库映射实体 ├── dto // 接口入参和出参对象 └── common // Result封装、异常处理、常量类

前端src目录:

src ├── api // axios实例和接口方法 ├── router // 路由配置 ├── store // 全局状态(登录信息、购物车数量) ├── components // 通用组件 └── views // 页面级组件 ├── home ├── product ├── cart ├── order └── admin

目录结构不代表什么,但好的分层能让你在写论文时直接照着画系统架构图。Controller只做参数接收和结果返回,Service放业务逻辑,Mapper不写业务。这个分层在答辩时非常经得起追问。

3.2 后端商品模块:只写几行代码就能跑起来的列表接口

先定义一个实体类,用Lombok省掉getter和setter:

@Data @TableName("product") public class Product { private Long id; private Long categoryId; private String name; private String brand; private BigDecimal price; private Integer stock; private Integer sales; private String image; private String description; private Integer status; private LocalDateTime createTime; }

Mapper接口,什么都不用写:

@Mapper public interface ProductMapper extends BaseMapper<Product> { }

Controller:

@RestController @RequestMapping("/api/product") public class ProductController { @Resource private ProductMapper productMapper; @GetMapping("/list") public Result list(@RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "8") Integer size) { LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(categoryId != null, Product::getCategoryId, categoryId) .like(StringUtils.isNotBlank(keyword), Product::getName, keyword) .eq(Product::getStatus, 1) .orderByDesc(Product::getSales); Page<Product> pageInfo = productMapper.selectPage(new Page<>(page, size), wrapper); return Result.success(pageInfo); } @GetMapping("/{id}") public Result detail(@PathVariable Long id) { return Result.success(productMapper.selectById(id)); } }

注意几个细节。查询条件用了LambdaQueryWrapper的eq、like方法,第一个参数是boolean,条件不成立就自动跳过,不用写一堆if判断。推荐这种写法。列表页只查status为1(上架)的商品,下架商品不能出现在前台。排序按销量倒序,销量越高靠前,这是一种最基本的"推荐"逻辑。

分页用MyBatis Plus自带的分页插件,配置一个PaginationInnerInterceptor就可以。前端传page和size两个参数,返回的pageInfo里包含records、total、current这些字段,前端直接能用。

3.3 购物车与下单:服务端金额计算怎么防篡改

购物车表比较简单,但"加购物车"这个动作里有文章。前端传过来的只是productId和quantity,服务端要校验这个商品是否存在、是否上架、库存是否充足。你不能相信前端传过来的价格字段,因为有人可以改请求参数把一个商品价格改成1分钱。

所有涉及金额的计算,都必须在后端做,而且价格取数据库当前的价格,不能取前端传的价格。这是电商系统的铁律。

加入购物车的Service方法大概是这样:

public void addCart(Long userId, Long productId, Integer quantity) { Product product = productMapper.selectById(productId); if (product == null || product.getStatus() == 0) { throw new BusinessException("商品不存在或已下架"); } Cart cart = cartMapper.selectOne( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getProductId, productId)); if (cart == null) { cart = new Cart(); cart.setUserId(userId); cart.setProductId(productId); cart.setQuantity(quantity); cartMapper.insert(cart); } else { cart.setQuantity(cart.getQuantity() + quantity); cartMapper.updateById(cart); } }

前端购物车页面用el-table显示当前用户的购物车条目,每一行有商品图片、名称、单价、数量、小计,下面汇总总价。数量可以用el-input-number组件,修改数量时调后端接口重新拉一次金额。

下单流程的Service方法,我建议把所有数据库操作放在一个事务里,用@Transactional注解。步骤是:查购物车条目,循环校验库存,计算总金额,生成订单号,插入订单主表和订单明细表,扣库存,清购物车。这一步是整个系统业务逻辑最密集的地方,写清楚了好处极大。

@Transactional public Long createOrder(Long userId, Long[] cartIds, OrderRequest req) { // 查询购物车条目 List<Cart> carts = cartMapper.selectBatchIds(Arrays.asList(cartIds)); if (carts.isEmpty()) { throw new BusinessException("购物车为空"); } // 校验库存并计算总金额 BigDecimal total = BigDecimal.ZERO; for (Cart cart : carts) { Product product = productMapper.selectById(cart.getProductId()); if (product.getStock() < cart.getQuantity()) { throw new BusinessException("商品[" + product.getName() + "]库存不足"); } total = total.add(product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); } // 生成订单号,格式如:20250612153012001 LocalDateTime now = LocalDateTime.now(); String orderNo = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(now) + String.format("%03d", new Random().nextInt(1000)); // 插入订单 Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setReceiverName(req.getReceiverName()); order.setReceiverPhone(req.getReceiverPhone()); order.setReceiverAddress(req.getReceiverAddress()); order.setStatus(ORDER_STATUS_WAIT_PAY); order.setCreateTime(now); orderMapper.insert(order); // 批量插入订单明细 + 扣减库存 for (Cart cart : carts) { Product product = productMapper.selectById(cart.getProductId()); OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setProductImage(product.getImage()); item.setPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); item.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity()))); orderItemMapper.insert(item); // 原子扣库存 int rows = productMapper.deductStock(product.getId(), cart.getQuantity()); if (rows == 0) { throw new BusinessException("商品[" + product.getName() + "]库存不足"); } } // 清空购物车 cartMapper.deleteBatchIds(Arrays.asList(cartIds)); return order.getId(); }

这里最值得讲给答辩老师听的是"事务"和"库存扣减"。因为有了@Transactional,任何一步抛异常,前面插入的订单和明细都会被回滚,不会出现订单建了库存没扣的情况。而扣库存用的是原子性SQL更新(后面第4部分会详细说),防止并发超卖。这两点说完,技术深度立刻就上来了。

3.4 订单状态流转与"跟踪"的实现

订单创建后,状态是待付款。毕设里不需要接真实支付接口,但"模拟支付"这个动作建议单独做一个接口,体现业务完整性。支付接口就是把订单状态从待付款改成已付款,同时记录pay_time。

@PutMapping("/{orderId}/pay") public Result pay(@PathVariable Long orderId, @RequestBody LoginUser user) { Order order = orderMapper.selectById(orderId); Order update = new Order(); update.setId(orderId); update.setStatus(ORDER_STATUS_PAID); update.setPayTime(LocalDateTime.now()); orderMapper.updateById(update); return Result.success(); }

管理员后台的"发货"接口同理,登录角色校验为管理员,把状态改成已发货,记录ship_time。用户端的"确认收货"接口,把状态改成已完成,记录finish_time。

用户查订单列表的时候,后端按userId查询,前端用标签展示状态。更高级的做法是给状态用不同颜色:待付款橘色、已付款蓝色、已发货紫色、已完成绿色。这些用Element Plus的el-tag就能实现,type属性切换即可。

订单详情页可以做一条时间线,用el-timeline组件,展示"订单创建""付款成功""商品发货""交易完成"四个节点,时间取order表里的四个时间字段。这样"订单跟踪"的界面反馈就有了:用户不用看冷冰冰的状态文字,一眼扫过去就知道自己的货走到哪一步了。

4. 最容易翻车的三个地方:权限、库存、跨域

4.1 登录状态与权限拦截:别再只用Session

前后端分离之后,最常见的认知误区是"登录之后用Session维护状态就行"。Session机制依赖Cookie,而跨域场景下Cookie的传递需要额外设置,而且后端如果做集群部署,Session同步也是麻烦事。毕设虽然不会做集群,但用JWT(JSON Web Token)会让整个处理流程更加干净。

流程不复杂:用户登录成功后,后端生成一个JWT串返回给前端,前端存在localStorage。之后每次请求,前端在请求头里带上Authorization: Bearer 这个Token。后端写一个拦截器,拦截需要登录的接口,从请求头里解析Token,解析成功就放行,解析失败就返回401。

拦截器的核心逻辑:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { // 解析token,成功则放行 Claims claims = JwtUtil.parseToken(token.substring(7)); request.setAttribute("userId", claims.get("userId")); return true; } response.setStatus(401); return false; } }

管理员角色校验再补一层。一般做法是在后台相关的接口上,额外判断当前用户角色是否为admin。可以在生成Token时把role放进去,拦截器解析出来后在需要管理员的Controller里用注解或者判断。

这个部分我想提醒你:把明文密码存数据库是答辩时最容易暴露的问题。密码一定要加密,至少用MD5加盐,更好一点用BCrypt。Spring Security里的BCryptPasswordEncoder可以直接用,生成的结果是随机盐加哈希,同样密码每次加密结果都不同,安全性高很多。答辩老师问到密码安全,你能说出"BCrypt加盐哈希",印象分会明显不一样。

4.2 库存扣减的并发问题:一个SQL解决超卖

这个问题我在实际项目里见过太多次。假设库存只剩1瓶酒,A和B两个用户几乎同时下单。如果你在Service里先查stock,判断stock >= quantity,再执行update,那么两个请求可能同时查到库存为1,都判定有货,都去执行扣减,最后库存变成-1。这就是典型的"超卖"。

解决办法有很多,但毕设阶段最合适的是把"检查库存+扣减库存"合并成一条SQL:

UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

对应的Mapper方法:

@Update("UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}") int deductStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);

这个SQL返回影响的行数,如果返回0,说明条件不满足,也就是库存不足,下单失败。它的巧妙之处在于数据库的行锁机制:同一时间只有一个事务能成功执行同一行的更新,即使并发请求到达,第二个请求要么等第一个执行完再判断,要么直接因为stock不满足条件而更新0行。

在创建订单的Service方法里,只要发现deductStock返回0,就抛异常回滚整个事务。这样库存永远不会变成负数,也不会出现"两个人都买到同一件商品"的情况。

答辩的时候,只要你能把这个场景和这个SQL讲清楚,基本可以镇住一片评委。如果再补充一句"生产环境还可以用Redis分布式锁或者乐观锁版本号机制",那扩展方向也有了。

4.3 跨域与联调:浏览器拦的不是你的路由

前后端分离开发时,最折磨人的一个坑就是跨域。Vite开发的默认端口是5173,SpringBoot默认是8080,端口不同,浏览器就会认为这是两个源,默认情况下跨域请求会被拦截。前端控制台报"CORS error"或者"Access to XMLHttpRequest at ... has been blocked"。

解决的姿势我推荐两种,选一种就行。

第一种,后端开CORS。写一个配置类:

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

如果前端使用的是JWT放在Authorization头里面,其实可以不开启allowCredentials,直接allowedOriginPatterns("")就好。最多再加个allowedHeaders("")。这里有个细节:allowedOrigins("")和allowCredentials(true)同时使用会被浏览器拒绝,所以要么用allowedOriginPatterns(""),要么不要allowCredentials。

第二种,前端Vite配置代理。这个更推荐在开发阶段用,因为浏览器看到的请求是同源的,不存在跨域问题:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

配置好之后,前端请求/api/product/list,Vite开发服务器会把这个请求转发到http://localhost:8080/api/product/list,后端不用做任何CORS配置。两种方式都可以,我一般建议:开发用Vite代理,部署前如果前后端分开部署,后端再补CORS。提前在项目初期就把CORS配置好,能少踩很多坑。

这里还有一个隐蔽的联调问题:接口联调时前端拿不到数据,先别急着怀疑后端。很多人直接打开http://localhost:5173页面然后点登录,发现一直失败,其实是因为前端请求的接口地址写的是http://localhost:8080,在非代理模式下面临跨域;或者是Tomcat端口不是8080,写成了8081。先看浏览器Network面板里请求有没有发出、返回什么状态码,再定问题方向,效率最高。

5. 常见问题与排障手记

5.1 高频报错速查表

我把这些年带这类毕设项目遇到的问题整理成一张表,遇到报错先对着找。

现象真正原因解决建议
前端请求接口404接口路径或代理没配对后端确认/@RequestingMapping,前端确认/api前缀是否匹配
浏览器报CORS blocked没配CORS或配置有问题用Vite代理,或后端补CORS
页面有数据但日期显示异常LocalDateTime序列化格式问题配置jackson date-format
登录后刷新页面登录状态丢失Token没持久化前端把Token存localStorage,请求拦截器带上
加入购物车后刷新就没了购物车只存在前端状态里购物车数据要存后端cart表,按userId查询
下单时提示库存不足,但库存明明有并发扣库存或事务没生效使用原子SQL更新,确认@Transactional
MyBatis Plus查询列表总是全部数据分页插件没配置注册PaginationInnerInterceptor
上传图片后前端看不到静态资源映射没配置配置WebMvcConfigurer映射本地目录
后台接口可以被普通用户访问只设置了登录拦截,没做角色校验在管理端Controller加管理员权限判断

这些坑里,我想单独讲一下分页插件的问题。MyBatis Plus的PageHelper分页和分页插件是两回事,很多人混着用。如果你用的是PaginationInnerInterceptor,记得在MyBatis Plus配置类里注册它;如果你用的是PageHelper,那要在查询前调用PageHelper.startPage,而且后面必须紧跟着一条MyBatis查询。同一个项目里别两种混着用,会出莫名其妙的SQL分页问题。

再说一个图片上传的细节。商品表里有image字段,开发阶段很多人都用本地磁盘存图片,然后数据库存一个相对路径。这样前端确实能显示,但要注意SpringBoot默认不把本地磁盘目录当成静态资源映射。你需要配置一下:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/project/upload/"); } }

这个配置写清楚后,前端图片地址直接填/upload/xxx.jpg就行。别把图片路径存成D:/project/upload/xxx.jpg,那样前端根本无法访问。

5.2 答辩老师最爱问的几个问题

毕设答辩其实有套路,老师没时间把你代码全部看一遍,但会对几个技术点和业务边界反复问。提前准备好答案,就不会站在台上支支吾吾。

第一个当然是"系统有哪些模块、业务流程是什么"。这个要一句话讲清楚:用户在前台注册登录,浏览商品,加入购物车,下单后生成订单;管理员在后台管理商品上下架、处理订单发货;订单从待付款到已付款到已发货到已完成,状态全程可见。这个回答一定要流畅,背出来都行。

第二个是"表与表之间是什么关系"。用户和订单是一对多,订单和商品是多对多,但多对多通过order_item这个中间表拆成了两个一对多。商品和分类是多对一,用户和购物车是一对多。把这些关系说清楚,数据库设计这关就过了。

第三个是"订单状态怎么流转,为什么这么设计"。照着状态机答:待付款到已付款是支付动作,已付款到已发货是管理员发货动作,已发货到已完成是用户确认收货动作。这样设计是为了让买卖双方都能清楚知道订单当前处于什么阶段,每个动作都有操作人和时间记录。

第四个是"你怎么处理库存量不足,万一两个用户同时买最后一个商品怎么办"。把第4.2节的原子SQL讲一遍就行,最好顺便提一句事务回滚。这一题回答完,基本属于加分项。

第五个是"系统有什么不足,以后可以怎么改进"。千万别说"没有不足"。不说满分的答案可以是:目前支付是模拟的,以后可以对接真实支付;库存扣减用了数据库原子操作,高并发场景下可以沉淀到Redis;目前没有使用消息队列,订单量大的时候可以引入异步处理;管理后台可以加ECharts图表做更详细的数据分析。这些说法既诚实又展示了你的知识面。

6. 实操心得与扩展建议

6.1 时间分配:六周做一个完整闭环

如果你还有一个月到六周的时间,我建议这样分配,最稳。

第一周做需求和设计,把功能模块、表结构、状态流转图都画好。第二周搭后端基础,跑通注册登录和商品模块,数据库表建好,分页接口能返回数据。第三周做购物车和下单,重点把订单创建事务和库存扣减搞定。第四周做前端页面,商品列表、详情、购物车、下单页、订单列表,把核心业务链路先打通。第五周做管理后台和权限,商品管理、订单管理、发货流程跑通。第六周整体联调、修bug、截图、写论文、准备答辩。

我个人的体会是,很多同学时间不够,不是因为代码量大,而是因为前期需求没想清楚,做了一半推翻重来。比如订单状态一开始设计成0和1,后面又要加已经发货,结果字段含义混乱,前端判断也乱,最后重构花的时间比一开始想清楚多得多。所以前面两周看起来慢,其实是在给你的后半程铺路。

6.2 值得做的扩展点

如果项目做到最后还有富余时间,我建议从下面几个方向选一个加进去,立刻能提升项目的整体观感。

加一个ECharts统计看板。管理员登录后台后,能看到最近7天订单量柱状图、销售额趋势折线图、商品分类占比饼图。数据就是查order和order_item表做聚合,技术难度不高,但视觉效果很好,答辩时展示出来非常加分。

加一个订单导出Excel功能。可以直接用阿里EasyExcel,把订单列表导出成一个xlsx文件。这个功能贴近真实办公场景,代码量也不大,还能体现你对第三方工具库的熟悉程度。

加一个基于Redis的缓存。比如把商品列表缓存到Redis,设置缓存过期时间;把用户的购物车数据改用Redis的Hash存储。但是注意,如果你对Redis不熟,别为了加而加,用不好反而给自己增加答辩风险。如果你能讲清Redis为什么快、缓存和数据库怎么保持一致,那这绝对是加分项。

还有一个不算功能但值得做的扩展:给项目写一个README,把技术栈、启动方式、演示账号写清楚。这不仅方便你自己运行,论文的"运行说明"章节也直接有素材了。

6.3 论文与代码一致性:一个容易被忽略的加分项

最后说一个很多人忽视的细节:论文写的系统的框架图、流程图、表结构,必须和代码里实际做的一致。

我见过太多的例子,论文里画了三张表八张图,结果代码里只有五张表;论文里写了"基于Redis缓存",代码里却根本没有Redis依赖。老师翻代码验证的时候,一旦发现对不上,哪怕你论文写得再漂亮,也会被打上"内容存疑"的标签。反过来,你的论文系统架构图、订单状态流转图、数据库ER图完全对应代码实现,会给老师留下"这个学生是真的自己做了项目"的印象,这是一张非常有力的王牌。

还有一个小技巧:在写论文的"系统测试"部分时,不要只写一张功能测试用例表,可以加一张"并发库存扣减测试"的表,描述两个用户同时下单同一件库存为1的商品,一个成功一个失败。这个测试用例能侧面证明你确实处理了并发问题,在本科毕设里属于超预期的内容。

最后分享一个开发习惯:全程用Git管理代码,每完成一个功能就提交一次。这不仅仅是写代码的好习惯,在答辩时如果你能打开Git提交记录,从第一次提交到最后一次提交,清楚展示项目的演进过程,那比你口头描述项目做了多少工作更有说服力。你自己看到那一条条提交记录,也会觉得这个项目是真的沉进去做完的,而不是为了应付交差拼凑出来的。毕设这段路,认真走完,收获比你想象的大得多。

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

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

立即咨询