ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于SpringBoot与MyBatis的美好乡村农产品交易平台Java毕设实战

基于SpringBoot与MyBatis的美好乡村农产品交易平台Java毕设实战 1. 项目缘起与整体定位做毕设选题那会儿我翻了几十个题目最后落在“美好乡村农产品交易平台”这个方向上。原因很直接农产品交易这个场景足够真实需求链条清晰既能覆盖用户端和管理端又能塞进订单、支付、库存、评价这些Java Web开发里最核心的几个模块。对正在准备Java毕设项目的同学来说选一个业务逻辑自洽、模块边界清晰的题目比追求什么花哨的技术名词要实在得多——毕竟答辩老师真正在意的是你能不能把一条业务链路讲通、把数据表设计合理、把增删改查和状态流转写对。这套系统我采用的是SpringBoot作为后端主框架配合MyBatis做持久层MySQL存数据前端用Thymeleaf或者前后端分离的Vue方案都可以我两个版本都跑过。选择SpringBoot不是因为它流行而是因为毕设的时间窗口通常只有两到三个月SpringBoot的自动配置能把这个时间省下来一大半。你要真拿SSM从零开始配XML光是Spring、SpringMVC、MyBatis三者的整合配置就能耗掉一周而且还容易在各种版本冲突里打转。SpringBoot把这些都收敛到application.yml和几个starter依赖里启动类一跑Tomcat内嵌接口就能访问对毕设这种要快速出成果的场景太合适了。这个平台解决的核心问题是把农产品从产地到消费者之间的信息不对称给补上一块。传统做法是农户摆摊、批发商上门收价格不透明、销路窄。平台化之后农户可以自己上传产品、设置价格和库存买家可以按分类浏览、下单、跟踪物流管理员审核商品和用户资质。听起来就是个电商但对农村场景来说加了很多特有的设计——比如按季节上架、按产地筛选、支持大宗起批量询价这些细节才是毕设里能讲出东西的地方。适合谁参考这篇文章正在为Java毕设发愁的同学尤其是方向定在电商、管理系统、平台类的也适合刚学完springboot框架想找个完整项目练手的人。哪怕你是小白只要装好JDK和IDEA跟着步骤也能把这个项目跑起来。后面我会从整体设计、数据库、核心代码、调试踩坑几个维度把整个项目拆开讲源码结构、配置参数、常见报错都会给到。先说清楚这个项目的边界避免大家期望错位。它不是要做一个能上线的电商平台那涉及的东西太多资质、结算、风控。它的定位是教学级完整项目功能覆盖用户注册登录、商品管理、购物车、订单流转、后台审核技术上覆盖SpringBoot整合MyBatis、拦截器做登录校验、事务控制、分页查询、文件上传。这些点每一个都是面试和答辩的高频考察项。2. 整体架构与技术选型拆解2.1 为什么是这套技术组合很多同学一上来就问用什么框架好我的回答永远是“先看你答辩老师认不认再看你能不能讲明白”。技术选型的核心逻辑是可解释性你选的每一个技术都要能说出为什么用它、不用别的会怎样。后端用SpringBoot 2.7.x这个版本而不是最新的3.x原因很实际。SpringBoot 3.x要求JDK 17起步把javax.*全部换成了jakarta.*很多老教程、老依赖比如某些MyBatis的旧版本插件在3.x下会直接报ClassNotFoundException。毕设阶段时间宝贵用2.7.x对应JDK 8能让你少踩一堆版本兼容的坑网上的中文教程资料也最多。热词里那个“springboot版本太高”的坑我后面会专门讲怎么处理。持久层用MyBatis而不是JPA也是权衡的结果。JPA写起来确实简洁但毕设答辩常被问到SQL怎么写、索引怎么用、多表关联怎么处理MyBatis让你能直接手写SQL把LEFT JOIN、GROUP BY这些展示出来反而更容易讲出深度。而且农产品交易里那些按产地、季节、价格区间的复杂筛选手写SQL比JPA的动态查询可控得多。前端这边我推荐优先用Thymeleaf做服务端渲染的版本先跑通等后端逻辑全对上了再考虑换Vue做前后端分离。为什么因为毕设的评分点在后端逻辑和数据库前端你做得再炫业务逻辑一塌糊涂照样挂。先用Thymeleaf把登录、下单这些链路验证通过心里有底了再去捣鼓前端。2.2 分层结构怎么划分项目采用经典的MVC三层架构但我在实际写的时候做了细化把Service层又拆了接口和实现。目录结构是这样的src/main/java/com/village/trade ├── Application.java // 启动类 ├── config/ // 配置类拦截器、跨域、文件上传路径 ├── controller/ // 控制层接收请求 ├── service/ // 业务接口 │ └── impl/ // 业务实现 ├── mapper/ // MyBatis接口 ├── entity/ // 实体类 ├── dto/ // 数据传输对象 ├── vo/ // 视图返回对象 ├── common/ // 统一返回、常量、异常 └── utils/ // 工具类这个结构不是摆设。dto和vo分离这一步很多同学容易忽略但它能解决一个大问题实体类直接返回给前端会暴露密码字段、内部状态字段。比如用户表里有password字段你用User实体直接返回密码就泄露了。用UserVO只返回需要的字段这才是规范做法答辩老师看到这个细节会加分不少。common包里的统一返回结构ResultT也是必备的。前端拿到{code:200, msg:success, data:...}这样的标准格式处理起来一致出错时也能统一捕获。配合RestControllerAdvice做全局异常处理任何未捕获的异常都会转成标准返回不会给前端抛一堆500堆栈。2.3 核心业务模块划分整个平台的业务模块我梳理成几块每块都有独立的数据表和接口模块核心功能涉及数据表用户模块注册、登录、角色区分、个人信息user商品模块上架、编辑、下架、分类、图片product, category购物车模块加购、修改数量、删除cart订单模块下单、支付模拟、状态流转、查询orders, order_item评价模块下单后评价、评分comment后台模块商品审核、用户管理、订单查看复用上面的表这张表很关键它决定了你要建几张表、写多少个实体类。我见过有同学把订单和订单明细混在一张表里结果一个订单买了三样东西数据直接炸了。订单表存订单头信息订单号、用户、总价、状态订单明细表存每一件商品订单号、商品、数量、单价这是必须分开的一对多的关系。3. 数据库设计与核心表结构落地3.1 建库建表前的思考数据库设计是毕设里最容易被低估、又最影响后续开发效率的部分。表设计得烂后面写SQL天天打补丁。我先把几个原则列出来都是踩过坑总结的第一主键统一用自增bigint。有的同学喜欢用UUID做主键觉得分布式好但毕设根本用不上分布式UUID做主键会导致聚簇索引效率差、调试时不好记。自增ID简单直观调试方便。第二金额字段用decimal(10,2)绝对不能用float或double。浮点数在计算金额时会出现0.10.20.30000000000000004这种经典问题订单总价算错是要出事的。decimal是定点数精确。第三状态字段用tinyint加注释不要用中文字符串。比如订单状态用0,1,2,3,4表示待付款、已付款、已发货、已完成、已取消在Java里用枚举映射既省空间又好维护。第四所有表加create_time、update_time用datetime类型给上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP。这样新插入和更新时数据库自动填充时间省得你在Java代码里到处setCreateTime(new Date())。3.2 关键表结构详解先看用户表这是所有系统的入口CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密后密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL, role tinyint(2) NOT NULL DEFAULT 0 COMMENT 0买家 1卖家 2管理员, status tinyint(2) NOT NULL DEFAULT 1 COMMENT 0禁用 1正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意几点username建了唯一索引uk_username防止重复注册password长度给100因为存的是MD5加盐或BCrypt加密后的串不是明文role用tinyint区分三种角色这是整个权限体系的基石。字符集用utf8mb4而不是utf8因为农产品名字里可能有生僻字或特殊符号utf8存不下4字节的字符会被截断报错。商品表设计要照顾农产品特有的属性CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, seller_id bigint(20) NOT NULL COMMENT 卖家ID, category_id int(11) NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 商品名, price decimal(10,2) NOT NULL COMMENT 单价, unit varchar(10) DEFAULT 斤 COMMENT 计量单位, stock int(11) NOT NULL DEFAULT 0 COMMENT 库存, origin varchar(50) DEFAULT NULL COMMENT 产地, season varchar(20) DEFAULT NULL COMMENT 上市季节, image varchar(255) DEFAULT NULL COMMENT 主图路径, description text COMMENT 详情描述, status tinyint(2) NOT NULL DEFAULT 0 COMMENT 0待审核 1上架 2下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;unit字段存计量单位斤、公斤、箱这个设计很接地气农产品按斤卖是按件卖的差异很大。origin产地和season季节字段是农产品场景的特有需求买家可以按“山东寿光”或者“秋季”筛选这两个字段加索引后查询效率明显提升。status的0/1/2三态设计对应后台审核流程卖家上架的商品要先经过管理员审核才能对买家可见这是电商平台的常规风控。订单相关的两张表是重头戏CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint(2) NOT NULL DEFAULT 0 COMMENT 0待付 1已付 2已发 3完成 4取消, address varchar(200) NOT NULL COMMENT 收货地址, receiver varchar(50) NOT NULL COMMENT 收货人, receiver_phone varchar(20) NOT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单号order_no独立于主键用“年月日时分秒随机数”生成因为主键自增ID不能让用户看到会暴露业务量订单号是给用户看的门面。订单状态用0到4五态流转这个状态机是答辩时老师最爱问的地方你得能说清楚每种状态之间怎么转换、由谁触发。3.3 表之间的关联关系梳理关系梳理清楚了写SQL才不会到处报外键错误。核心关系是这样几条一个卖家user对应多个商品product通过product.seller_id关联一个分类category对应多个商品通过product.category_id关联一个订单orders对应多条订单明细order_item通过订单号关联一条订单明细关联一个商品下单时要把商品的价格、名称快照进明细这里有个细节值得专门说订单明细要存商品快照。什么意思下单那一刻商品的名称和价格要复制到订单明细表里。为什么因为卖家后面可能改价、改名甚至下架商品如果你订单明细只存商品ID去实时关联查询那用户看到的历史订单价格就变了这在逻辑上完全错误。快照是电商订单设计的铁律面试也常考答辩时你要是能主动讲这一点老师印象分会很高。4. 核心功能实现与代码落地4.1 环境搭建与项目启动先把环境这块讲透因为这是新手最容易卡住的地方我按顺序说。JDK安装与配置装JDK 8就行配好JAVA_HOME环境变量把%JAVA_HOME%\bin加到Path里。命令行敲java -version和javac -version都能出来版本号说明配好了。注意两个命令都要测试只测java不够有时候Path配错了javac就用不了。MySQL安装5.7或8.0都可以8.0的话连接URL要加时区参数否则启动时提示时区错误。这个坑下面细讲。装完记得导入建表脚本我习惯把数据库名定为village_trade字符集选utf8mb4。IDEA导入项目用IDEA打开项目根目录等Maven依赖下载完。如果依赖下载慢配置阿里云镜像mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror这段加到Maven的settings.xml的mirrors标签里。下载慢是新手第一个卡点配了镜像能从天级降到分钟级。application.yml配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/village_trade?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.village.trade.entity configuration: map-underscore-to-camel-case: true这里几个参数都是关键。serverTimezoneAsia/Shanghai解决8.0的时区报错map-underscore-to-camel-case: true让数据库的create_time自动映射到Java的createTime省得你用resultMap一个个写multipart配置决定上传商品图片的大小上限不配的话默认1MB稍微大点的图就传不上。4.2 登录认证与权限拦截实现登录这块我不建议用Spring Security对毕设来说太重配置复杂而且会掩盖很多原理。我用的是最朴素的Session 拦截器方案逻辑清晰代码量小答辩也好讲。登录接口的核心逻辑PostMapping(/login) public ResultUserVO login(RequestBody LoginDTO dto, HttpSession session) { User user userService.getByUsername(dto.getUsername()); if (user null) { return Result.error(用户名不存在); } // 比对加密后的密码 String encrypt MD5Util.encrypt(dto.getPassword() user.getSalt()); if (!encrypt.equals(user.getPassword())) { return Result.error(密码错误); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } UserVO vo new UserVO(); BeanUtils.copyProperties(user, vo); session.setAttribute(loginUser, vo); return Result.success(vo); }密码这里用MD5加盐。为什么要加盐如果不加盐直接MD5同样的密码“123456”所有用户加密结果一样黑客用彩虹表一查就出来了。加盐就是给每个用户生成一个随机字符串拼在密码后面再加密这样每个人的密文都不同。盐值存在用户表的salt字段里。生产环境更推荐BCrypt但毕设用MD5加盐足够体现安全意识了。登录成功把用户信息放进Session后面靠拦截器校验public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { HttpSession session req.getSession(); if (session.getAttribute(loginUser) null) { resp.setContentType(application/json;charsetutf-8); resp.getWriter().write({\code\:401,\msg\:\请先登录\}); return false; } return true; } }拦截器要在配置类里注册addPathPatterns指定拦截哪些路径excludePathPatterns放行登录、注册、商品列表这些不需要登录的接口。这个放行清单很关键配错了要么登录页都打不开拦太狠要么谁都能删商品放太松。我的经验是把静态资源、登录注册、商品浏览全部放行只有涉及下单、后台管理的才拦。4.3 商品分页查询与多条件筛选商品列表用分页这块是答辩必问。我用的是MyBatis的PageHelper插件它能自动在SQL后面拼LIMIT不用手写分页SQL。GetMapping(/list) public ResultPageInfoProduct list(ProductQuery query, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageHelper.startPage(pageNum, pageSize); ListProduct list productService.listByCondition(query); PageInfoProduct pageInfo new PageInfo(list); return Result.success(pageInfo); }PageHelper有两点要注意。第一startPage必须紧跟在查询方法前面中间不能插别的查询否则它会作用到错误的那条SQL上导致分页结果莫名其妙。第二返回的应该是PageInfo而不是原始List因为PageInfo里封装了总条数、总页数、当前页这些信息前端分页组件需要。多条件筛选用MyBatis的if动态SQLselect idlistByCondition resultTypecom.village.trade.entity.Product SELECT * FROM product where status 1 if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testorigin ! null and origin ! AND origin #{origin} /if if testminPrice ! null AND price gt; #{minPrice} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理第一个条件前的AND很省心。注意gt;这种写法XML里大于号要转义成gt;这是新手常犯的错直接写会导致XML解析失败。价格区间筛选、产地筛选、分类筛选都通过if按需拼接一条SQL搞定所有筛选组合。4.4 下单流程与事务控制下单是整个项目最核心也最容易出问题的环节因为涉及多表操作必须用事务保证原子性。下单要做几件事校验库存、扣减库存、生成订单、生成订单明细、清空购物车。这几步要么全成功要么全回滚中间任何一步失败都不能留下脏数据。Transactional(rollbackFor Exception.class) public String createOrder(Long userId, OrderCreateDTO dto) { // 1. 查询购物车中选中的商品 ListCart carts cartService.listByUserAndIds(userId, dto.getCartIds()); if (CollectionUtils.isEmpty(carts)) { throw new BusinessException(请选择要购买的商品); } BigDecimal total BigDecimal.ZERO; // 2. 逐个校验库存并计算总价 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(new BigDecimal(cart.getQuantity()))); } // 3. 生成订单号时间戳随机数 String orderNo OrderNoUtil.generate(); // 4. 写入订单主表 Orders order new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); // ... 设置收货信息 orderMapper.insert(order); // 5. 写明细 扣库存 for (Cart cart : carts) { Product product productMapper.selectById(cart.getProductId()); OrderItem item new OrderItem(); item.setOrderNo(orderNo); item.setProductId(product.getId()); item.setProductName(product.getName()); // 快照 item.setPrice(product.getPrice()); // 快照 item.setQuantity(cart.getQuantity()); orderItemMapper.insert(item); // 扣库存用乐观锁思想stock stock - quantity int rows productMapper.reduceStock(product.getId(), cart.getQuantity()); if (rows 0) { throw new BusinessException(库存扣减失败请重试); } } // 6. 清空已下单的购物车项 cartMapper.deleteByUserAndIds(userId, dto.getCartIds()); return orderNo; }这个方法的门道很多逐条说。Transactional(rollbackFor Exception.class)这个注解rollbackFor一定要写因为Spring事务默认只在RuntimeException时回滚你抛的是自定义的BusinessException继承Exception不写这个参数事务不回滚脏数据就进去了。这是超级经典的坑我第一次写就栽在这。再看扣库存那步为什么用reduceStock这种UPDATE ... SET stock stock - ?的写法而不是先查出来再setStock再update因为并发情况下两个请求可能同时查到库存是10都认为够扣然后都去扣结果库存变成-10。用SQL直接做减法数据库层面保证原子性这是最朴素的乐观锁思路。更严谨的做法是加AND stock #{quantity}条件UPDATE product SET stock stock - #{q} WHERE id #{id} AND stock #{q}这样库存不足时影响行数为0直接抛异常。这个写法推荐大家都用上。4.5 文件上传与图片展示农产品交易必须有图不然买家不知道菜长什么样。商品图片上传用SpringBoot的MultipartFilePostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.error(请选择文件); } String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); // 校验后缀 ListString allow Arrays.asList(.jpg, .jpeg, .png, .gif); if (!allow.contains(suffix.toLowerCase())) { return Result.error(只支持图片格式); } // 用UUID重命名避免中文名和重复名问题 String fileName UUID.randomUUID().toString() suffix; // 按日期分目录防止单目录文件过多 String datePath new SimpleDateFormat(yyyyMMdd).format(new Date()); File dir new File(uploadPath datePath); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(dir, fileName)); return Result.success(/upload/ datePath / fileName); }上传这块的坑集中在三点。第一必须校验后缀否则用户能上传.jsp或.exe文件安全漏洞答辩老师问起来你答不上就尴尬了。第二用UUID重命名不能直接用原始文件名因为中文名在服务器上可能乱码而且同名文件会互相覆盖。第三按日期分目录所有图堆在一个目录里文件一多ls都卡而且不好管理。上传路径配置建议用绝对路径不要用相对路径。因为SpringBoot打包成jar运行后相对路径的基准目录会变在jar所在目录容易找不到图。配置文件里定义file: upload-path: D:/village-upload/然后写个配置类把静态资源映射过去让浏览器能通过URL访问到上传的图Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }addResourceLocations后面的file:前缀不能少少了它Spring会当类路径去找找不到图。这个细节我在第三个项目才彻底搞清楚。5. 调试运行与常见问题排查实录5.1 启动阶段的典型报错项目跑不起来八成是环境或配置问题。我把印象最深的几个报错和解决方法整理成表都是实践里反复遇到的报错信息根本原因解决办法Unknown database village_trade数据库没建或名字写错执行建库脚本核对URL里的库名The server time zone value ... is unrecognizedMySQL 8.0时区未指定URL加serverTimezoneAsia/ShanghaiAccess denied for user rootlocalhost数据库密码错改yml里的passwordConsider defining a bean of type xxxMapperMapper未被扫描启动类加MapperScanPort 8080 was already in use端口被占用换端口或杀掉占用进程Failed to configure a DataSource数据源依赖缺失加mysql-connector-java依赖数据库时区那个报错几乎每个用MySQL 8.0的同学都会遇到一次看到那一长串英文别慌就是URL少了时区参数。MapperScan那个也是重灾区MyBatis的Mapper接口不加扫描注解Spring不知道要给它生成代理对象启动就报找不到bean。要么在每个Mapper接口上加Mapper要么在启动类统一加MapperScan(com.village.trade.mapper)后者更省事。5.2 依赖冲突与版本问题热词里“springboot版本太高”这个问题我专门说一下。常见表现是启动报NoSuchMethodError或者ClassNotFoundException明明代码没写错。根源在于SpringBoot的父POM管理了依赖版本但你手动引入的某个依赖版本和它管理的版本不一致。排查方法用Maven的依赖树命令看冲突。mvn dependency:tree输出里如果同一个groupId:artifactId出现两次不同版本就是冲突了。解决办法是在dependency里用exclusions排除掉传递进来的旧版本或者用dependencyManagement统一锁定版本。我的经验是能不手动指定版本就不指定让SpringBoot的父POM来管理除非确实需要覆盖。比如引入mybatis-spring-boot-starter版本号交给父POM就行。还有一个高频问题spring-boot-starter-web和spring-boot-starter-webflux同时存在会冲突因为一个项目不能同时用Servlet和Reactive两种Web模型。这种情况通常是自己引入或者某个依赖传递进来的用mvn dependency:tree能揪出来然后排除掉webflux。5.3 数据库连接与SQL调试技巧MyBatis的SQL看不见出错了很难查。我强烈建议在yml里把SQL日志打开mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl打开后控制台会打印每条执行的SQL和参数以及返回结果。SQL写错了、参数传错了、字段映射不对一眼就能看出来。这个配置在开发阶段必开上线再关掉因为日志量大影响性能。还有个技巧是把动态SQL单独抽出来在数据库客户端直接跑。MyBatis的动态SQL拼错了报错信息往往很含糊。你可以把控制台打印出来的完整SQL参数已替换复制到Navicat或DataGrip里执行看是不是SQL本身的问题。这一步能快速定位是“SQL写错”还是“参数没传到”。字段映射问题也常见。数据库是create_timeJava是createTime如果没配map-underscore-to-camel-case: true查询结果里createTime永远是null。看到某几个字段莫名是null第一个就该怀疑这个配置。5.4 上传和路径相关的坑图片上传后显示不出来是最让新手抓狂的问题。排查顺序我总结成三步第一看文件到底存没存到服务器磁盘去配置的upload-path目录下找文件文件不在说明上传就失败了通常是权限或路径问题第二文件在但访问404检查addResourceHandlers配的URL前缀和实际返回的URL是否一致/upload/**对应的返回路径必须是/upload/xxx开头第三浏览器F12看请求的完整URL对比是否和映射规则匹配。transferTo方法有个坑如果目标文件的父目录不存在它会抛FileNotFoundException但它不会自动创建父目录。所以我在调用前先dir.mkdirs()。还有一个更隐蔽的问题SpringBoot打包成jar后transferTo的相对路径基准会变所以务必用绝对路径。这些都是实际调试才会暴露出来的问题。5.5 事务不生效的排查思路下单明明报错了订单还是插进去了这就是事务没生效。排查四问方法是不是public的Spring AOP只能代理public方法private和protected的事务注解无效。是不是自调用同类里A方法调用本类的B方法B有Transactional事务不生效因为没走代理。要么把B挪到另一个Service要么注入自己。异常类型对不对默认只回滚RuntimeException和Error你的BusinessException如果继承的是Exception必须加rollbackFor Exception.class。类有没有被Spring管理Service实现类上要有Service注解。这四问按顺序排查基本能覆盖95%的事务失效场景。我把这条经验单独拎出来因为它太隐蔽了——代码看起来完全正确就是没效果新手能卡一整天。5.6 答辩高频问题预演调试跑通只是第一步能讲清楚才是毕设的最终目的。我把自己被问过和常见的高频问题整理一下提前准备为什么用SpringBoot而不是SSM——自动配置减少XML起步依赖管理版本内嵌Tomcat方便部署。密码为什么不用明文存——安全用MD5加盐防止彩虹表一人一盐密文不同。下单时库存怎么保证不超卖——SQL层stock stock - ?原子操作加stock ?条件校验。订单明细为什么要存商品快照——防止商品改价后历史订单金额错乱。分页是怎么实现的——PageHelper拦截SQL自动拼LIMIT返回PageInfo。登录状态怎么保持——Session存用户拦截器校验未登录返回401。这些问题你只要能把背后的“为什么”说清楚比功能做得多更能拿高分。老师要的是你的理解不是你复制粘贴了多少代码。我个人在实际操作中的体会是毕设项目真正的难点从来不是某个技术多高深而是把一条完整业务链路从数据库表设计、到后端事务控制、到前端交互全部打通并且能自圆其说。我在做的过程中反复体会到先想清楚表关系再动手写代码比写到一半发现表设计有问题回头改要省太多时间。最后再分享一个小技巧把每次调试遇到的报错和解决方案记到一个单独的md文档里答辩前翻一遍老师问“你遇到过什么难题”你随口就能答出三五个带细节的真实案例这比任何背诵出来的答案都有说服力。
返回列表