尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot宠物用品交易网站实战:从项目结构到部署上线全解析

Spring Boot宠物用品交易网站实战:从项目结构到部署上线全解析 我见过太多同学拿到一套“源码文档”的项目第一反应是赶紧跑起来结果要么卡在环境上要么跑起来之后不知道怎么改最后对着后台一堆功能发呆。宠物用品交易网站是Java开发里非常经典的实战选题它不追求高并发也不涉及分布式那些复杂概念但只要把Spring Boot的常用模块串起来你就掌握了企业级Web开发的完整骨架。这篇文章就围绕基于Spring Boot的在线宠物用品交易网站源码文档这套项目来拆从项目结构、数据库设计、核心业务实现到部署上线和文档编写我会把每个环节的原理讲透也把我实际开发中踩过的坑一并交代清楚。不管你是做课程设计还是想拿一个完整的Java后端项目去找实习这篇都能给你一个清晰的参照系。1. 项目整体设计与技术选型1.1 为什么选Spring Boot做交易网站先聊个基本问题网站类的项目那么多为什么Spring Boot在高校、培训班和求职项目里几乎成了标配道理其实很朴素它解决了Java后端开发里最头疼的配置问题。早些年用SSMSpring Spring MVC MyBatis你得写一堆XML配置文件什么DataSource、SqlSessionFactory、事务管理器每个都要手动装配而且版本稍微差一点就能报出让人摸不着头脑的错。Spring Boot把“约定大于配置”这个理念做到了极致内置Tomcat默认配置就能跑你只要引入对应的starter场景启动器框架会自动帮你把该配的都配上。这带来的直接好处是你可以把精力放在写业务代码、梳理业务逻辑上而不是跟繁琐的配置文件搏斗。这套宠物用品交易网站从技术栈上属于典型的“前后端不分离 模板引擎渲染 关系型数据库存储”模式。它适合用来练手但你别小看它里面涉及的东西量其实很大用户注册登录、商品分类浏览、购物车管理、订单生成、库存扣减、后台管理覆盖了一个电商系统最小的闭环。换句话说把这一套吃透以后哪怕让你去做一个更复杂的项目你也能很快上手。1.2 技术栈选型背后的理由做选型前先明确这个项目的定位。它不是一个给百万用户用的高并发商城而是一个教学演示和毕设级别的系统所以技术选型的唯一标准就是恰到好处不过度设计。我实际推荐的基础组合是这样的模块选型选择理由开发语言Java 8生态成熟Spring Boot官方支持稳定大部分学校和企业还在用核心框架Spring Boot 2.7.x稳定可靠资料多问题好搜比3.x更适合新手持久层MyBatis-Plus单表操作几乎不用写SQL复杂查询手写XML也不难数据库MySQL 5.7 / 8.0免费的社区版够用事务性能稳定前端模板Thymeleaf和后端集成方便能直接在HTML里写表达式拿后端数据前端UIBootstrap jQuery不折腾框架直接可用表格、表单、弹窗都有现成组件文件存储本地存储 / MinIO本地存储简单MinIO适合后面扩展分布式部署权限校验JWT 或 Session二选一看项目要求毕设用Session更简单企业项目用JWT更普遍MyBatis-Plus这里我多说一句它本质上是在MyBatis之上做了一层增强内置了通用的Mapper接口和Service方法。增删改查、分页查询这些基础操作你不用自己写SQL直接调用它的API就行。这对提高开发效率特别明显——我曾经算过一笔账同样的商品管理功能纯MyBatis写一套CRUD增删改查要自己处理的结果映射、动态SQL拼接大约需要两三天用MyBatis-Plus大半天就能搞定而且代码更干净、不容易出错。至于为什么不用Redis、MQ消息队列这类中间件是因为这个项目场景用不ago。购物车数据直接放Session或者数据库表里就行库存扣减就是一次事务性的UPDATE操作。如果你硬要加Redis缓存热点商品可以加但属于锦上添花不建议一开始就上因为部署复杂度会显著提升。1.3 数据库设计才是灵魂很多人拿到项目源码后只盯着代码看这是方向性错误。一个交易类网站的灵魂在数据库表结构表字段设计得合理后面写代码会非常顺设计得不好各种JOIN、各种补字段甚至推倒重来都是常有的事。这套宠物用品交易网站核心表至少要有以下这几张用户表user用户ID、用户名、密码加密存储、昵称、手机号、邮箱、头像、地址列表可以冗余一个“默认收货地址ID”商品分类表category分类ID、分类名称、父分类ID、排序字段设计成支持两级分类比如“猫粮”下面还能分“幼猫粮”“成猫粮”商品表product商品ID、分类ID、商品名称、主图地址、商品详情、价格、库存、销量、上下架状态购物车表cart_item用户ID、商品ID、数量、加入时间加一个唯一索引避免同一个用户反复插入同一件商品订单表order订单编号、用户ID、订单总金额、收货人姓名、联系电话、收货地址、订单状态、下单时间、支付时间订单项表order_item订单ID、商品ID、商品名称快照、购买时单价、数量、小计金额——这里必须做快照因为商品信息后续可能改价或者删除但订单历史不能被影响收货地址表address用户ID、收货人、手机号、省市区、详细地址、默认标记这些表之间的关联关系都是典型的“一对多”一个用户有多张订单一个订单包含多个订单项一个分类下面有多个商品。新手最常见的错误是把订单项直接塞进订单表或者把商品名称直接存到订单表里而不考虑快照问题这两种习惯都要改掉。数据库初始化脚本建议写清楚包括建库语句、建表语句和测试数据。源码配套文档里会带完整的SQL文件这点很重要因为数据库建不好项目是无论如何跑不起来的。2. 核心功能模块拆解与实现2.1 用户注册登录安全是第一位的用户模块看起来简单但有一个绝对不可妥协的原则密码不能明文存储。我见过太多课程设计的源码数据库里password字段直接裸存“123456”这要是在真实的生产环境一旦数据库泄露所有用户账号全部沦陷。Spring Boot里做密码加密最推荐的是BCrypt算法。它和普通的MD5、SHA不同是一种自适应哈希算法会自动加盐并且同一个密码每次计算出来的哈希值都不一样。使用起来很简单项目引入spring-security-crypto依赖后直接调用BCryptPasswordEncoder的encode和matches方法就行业务代码里不需要关心salt的存储和传递框架全帮你处理了。登录逻辑这里要注意一个细节登录成功后用户信息的存放位置。用Session存放优点是实现简单、线程安全Tomcat自己管理缺点是水平扩展时要做Session共享用JWT存放优点是服务端无状态、适合前后端分离缺点是token过期控制、续期这些逻辑要自己写。这个项目既然是用Thymeleaf渲染页面说明是“前后端不分离”那用Session方案就够了也省心。再补充一个常见问题用户状态。有的项目会加“禁用状态”字段后台管理员可以封禁某个违规买家。这个功能不算复杂但在后台管理模块里是一个加分项建议写上。2.2 商品浏览与搜索页面渲染别走弯路商品列表页是用户访问最多的页面这里要兼顾两个事情查询效率和数据展示的友好程度。分页查询是必须的商品可能几百条不可能一次全部load出来。MyBatis-Plus内置的分页插件非常方便你只需要创建一个MybatisPlusInterceptor的Bean注册PaginationInnerInterceptor然后调用selectPage方法即可。数据库端的分页SQL它会自动帮你写好不用担心MySQL的LIMIT语法问题。商品搜索则可以用一个动态SQL根据商品名称模糊查询根据分类ID精确筛选根据价格区间过滤再按上架时间或销量排序。这里就是手写XML的用武之地了。有一个性能优化点不要用SELECT *最好只查列表页需要的字段因为商品详情文本detail往往是大字段如果列表页包含它每次查询的数据量会大很多。另外列表页的商品主图建议用懒加载方式对用户体验也更友好。搜索出来的商品列表点进去之后是商品详情页。详情页至少包括商品大图、名称、价格、库存、销量、详细介绍富文本、用户评价列表。这里要做的核心操作是把“加入购物车”按钮和库存量联动起来——库存为0时按钮置灰不允许购买。这个逻辑既在前端做也在后端接口层做不能只靠前端理由下面讲。2.3 购物车与订单事务一致性是重头戏购物车功能在J2EE课程里经常就是一张表存用户的收藏但真实的购物车业务要复杂一些用户可能加购多次同一商品需要数量累加而不是重复插入用户可能修改数量、删除某个条目、清空购物车结算时还要对购物车里所有商品做一次总额计算。订单模块是整个项目的重头戏因为这里涉及“事务”。什么叫“提交订单”的正确流程我总结下来核心操作是这样的接收用户的购物车条目ID列表、收货地址ID、备注。根据购物车条目查出对应的商品信息在代码里累加算出订单总额注意价格不能直接用前端传过来的值。前端传的价格可能被篡改后端必须重新查询数据库里的商品单价。校验库存是否充足不足的比如想买5件库存只剩3件要么直接报错中断要么提示用户修改数量。扣减库存执行UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}。这里带stock #{count}条件的写法可以防止超卖是一种乐观锁的思想。生成订单主表和订单明细表状态置为“待支付”。清除已购买的购物车条目。把上述操作全部包裹在同一个数据库事务中任何一个步骤失败整体回滚。很多人实现的“提交订单”是往订单表插一条记录就完事了库存不扣、购物车不清这对学习项目来说还能糊弄但离真实的业务逻辑差太远。既然要做一套完整的交易网站入库、扣库存、清购物车这三件事一个都不能少。2.4 订单状态流转别用魔法数字订单模块除了正常的下单还有一个核心问题是“状态”。常见的状态有待支付、已支付/待发货、已发货、已完成、已取消超时或主动取消稍微复杂一点的还会有“售后处理中”。我强烈建议在项目里为订单状态定义一个枚举类或者在常量类里定义一组常量而不是在代码里到处写status 1这样的魔法数字。为什么因为我见过太多项目写着写着就分不清1到底是“待支付”还是“已支付”了。而且这个东西在后端接口、后台列表筛选、前端显示等地方都要用到统一管理会省掉很多沟通和时间成本。支付环节在演示项目里通常用一个“模拟支付”按钮替代点击后直接把状态从“待支付”改成“已支付”并记录支付时间。文档里要明确说明这是模拟流程真实环境需要对接第三方支付接口代码结构上也留好扩展点就行。2.5 后台管理给管理员一套趁手的工具后台管理功能是这类毕设项目的标配也是拉开工作量差距的地方。最基本的后台模块如下管理员登录鉴权后进入后台商品管理新增、编辑、上下架、修改库存、删除分类管理维护两级商品分类订单管理查看订单列表、订单详情发货操作修改订单状态为已发货用户管理查看注册用户列表禁用/启用用户这里要特别注意的是后台和前台的权限一定要做区分。最简单的方案是用户表再加一个role字段0是普通用户1是管理员。后台的所有Controller请求加一个拦截器检查当前登录用户的角色是否为管理员不是就直接拒绝。这个拦截器千万不要忘记——我见过不少项目后台路由没有任何保护谁都能打开/admin页面这属于安全漏洞级别的问题了。3. 项目工程结构与配置文件解析3.1 包结构与分层思想拿到源码后我建议你第一件事不是急着运行而是先看包结构。标准的三层架构在你的源代码里应当是清晰可见的com.pet.shop ├── controller // 控制层接收请求、返回响应 ├── service // 业务层核心业务逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层数据库操作接口 ├── entity // 实体类对应数据库表结构的POJO ├── dto // 数据传输对象接收前端参数 ├── vo // 视图对象向前端展示的数据 ├── config // 配置类拦截器、跨域、MyBatis-Plus分页插件等 ├── common // 通用类统一返回结果、异常处理、常量、工具类 └── interceptor // 登录拦截器、管理员拦截器Controller层只做参数接收和响应返回不写业务逻辑Service层是业务逻辑的核心事务边界也是在这里用Transactional标注Mapper层只管和数据库打交道。有些新手喜欢把业务逻辑全都堆在Controller里虽然能跑但后面一旦要加需求改动会非常痛苦。实体类和数据库表的对应关系也要注意命名的规范实体类用驼峰命名数据库表字段用下划线命名比如字段product_name对应实体属性productName。MyBatis-Plus默认开启了驼峰转换这个映射是自动的不需要额外配置。3.2 application.yml配置文件里那些注意点Spring Boot的配置文件一般放在src/main/resources/application.yml里。这个文件是整个项目运行的钥匙配置错了项目连启动都困难。很多同学会纠结“为什么项目在自己电脑上跑不起来”十个里面有两三个是代码问题剩下七八个都在配置文件上。关键配置项我来逐个说一下server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/pet_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl几个容易踩坑的点我这里强调一下serverTimezone必填。MySQL 8.0默认使用了CST时区会导致连接报错或时间偏差建议统一写成Asia/Shanghai。driver-class-nameMySQL 5.x和8.x的驱动类不一样。5.x是com.mysql.jdbc.Driver8.x是com.mysql.cj.jdbc.Driver复制粘贴时容易混。mapper-locations如果你的Mapper接口和XML文件不在同一个包下这个路径一定要配置对。有个常见做法是在resources下建一个mapper目录专门放XML文件编译后XML文件会和mapper接口分离开。开发阶段把log-impl配成StdOutImpl这样SQL语句会直接打印到控制台调试时你会感谢这个配置的。3.3 Mapper XML与接口不在同一个目录时的处理我在实际项目里经常遇到Mapper接口在com.pet.shop.mapper包下而XML文件在resources/mapper目录下。这种结构很常见但也有不少人在这里翻车因为默认的Spring Boot配置不会自动扫描到resources/mapper下的XML文件。解决方案有两个方案一在application.yml里配置mybatis-plus.mapper-locations指向classpath:mapper/**/*.xml然后在启动类上加MapperScan(com.pet.shop.mapper)注解扫描Mapper接口。这也就是我上面配置文件的写法。方案二把XML文件和Mapper接口放同一个目录下然后在pom.xml里配置resources包含xml文件。这个方案在Maven项目里需要额外配置resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources我个人更推荐方案一。因为把XML放在resources目录下是Maven项目的一个标准约定所有资源文件统一管理而且你不会被“为什么编译的时候XML没被复制到classes目录”这种问题坑到。另外还要检查IDEA里面有没有一个很容易被忽略的坑编译后如果target/classes里面没有XML文件启动时就会报Invalid bound statement (not found)。遇到这个错误优先去检查target目录看XML到底编译进来没有没进来的话按方案一或方案二调整。4. 完整实现流程与关键代码示例4.1 环境准备和项目初始化先列一下环境需求这是跑通项目的前提软件版本要求备注JDK1.8及以上推荐1.8稳定Maven3.6项目依赖管理IDEA2020.1及以上社区版也行MySQL5.7或8.0本地安装或DockerNavicat / DBeaver不限可视化操作数据库初始化步骤很简单我这里按顺序说用IDEA打开项目源码等待Maven自动下载依赖。如果下载慢在settings.xml里配置阿里云镜像。新建数据库执行项目里自带的sql/init.sql脚本初始化表结构和测试数据。修改application.yml的数据库用户名、密码为本地环境的值。启动主类PetShopApplication看到控制台输出Started PetShopApplication说明启动成功。浏览器访问http://localhost:8080进入前台首页。访问http://localhost:8080/admin/login用文档里给的管理员账号密码登录后台。提示IDEA默认情况下不会把resources目录的配置同步到编译输出目录如果你在运行测试类时发现数据库配置没生效右键resources目录点击“Mark Directory as Resources Root”就能解决。4.2 用户登录状态管理与拦截器用户登录状态管理我用Session方案来演示。登录成功后把用户信息放进Session并在后续请求中用拦截器校验。先定义一个WebConfig配置类Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Autowired private AdminInterceptor adminInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { // 拦截所有 /user/** 和 /cart/** 和 /order/** 下的请求 registry.addInterceptor(loginInterceptor) .addPathPatterns(/user/**, /cart/**, /order/**); // 拦截所有 /admin/** 请求 registry.addInterceptor(adminInterceptor) .addPathPatterns(/admin/**); } }然后写登录拦截器Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } return true; } }这个拦截器的逻辑看起来很短但它能挡住很多问题未登录用户无法访问购物车、无法提交订单访问后台会被重定向到登录页。如果你不配拦截器那用户的购物车和订单接口就等于裸奔只要知道URL就能操作别人的数据这在项目答辩或面试时是一个很严重的减分项。管理员拦截器略微不同它在判断登录之外还要判断角色Component public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/admin/login); return false; } User user (User) loginUser; if (user.getRole() ! 1) { response.setStatus(403); return false; } return true; } }这里的核心思路就是拦截器只做鉴权不替代业务逻辑。什么时候该用拦截器什么时候该在Service里判断角色原则很简单和业务状态无关的通用行为放在拦截器比如“没有登录不能访问”和具体业务规则相关的放在Service层比如“只有管理员才能把商品下架”。4.3 商品分页、搜索与购物车核心代码商品分页用MyBatis-Plus非常方便Controller里的代码大概是这样GetMapping(/product/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 12) Integer pageSize, RequestParam(required false) Integer categoryId, RequestParam(required false) String keyword, Model model) { PageProduct page productService.pageProducts(pageNum, pageSize, categoryId, keyword); model.addAttribute(page, page); model.addAttribute(categoryId, categoryId); model.addAttribute(keyword, keyword); return product/list; }Service层封装了分页查询逻辑public PageProduct pageProducts(int pageNum, int pageSize, Integer categoryId, String keyword) { LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (categoryId ! null) { wrapper.eq(Product::getCategoryId, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(Product::getName, keyword); } wrapper.orderByDesc(Product::getCreateTime); return productMapper.selectPage(new Page(pageNum, pageSize), wrapper); }LambdaQueryWrapper是MyBatis-Plus的亮点它用方法引用的方式指定数据库字段编译期就能发现字段名错误比写字符串product_name安全得多。再加购物车的核心逻辑。加入购物车时要注意“重复商品数量累加”这个细节PostMapping(/cart/add) ResponseBody public Result addCart(RequestParam Long productId, RequestParam(defaultValue 1) Integer quantity) { User loginUser getLoginUser(); CartItem existing cartItemService.findByUserIdAndProductId(loginUser.getId(), productId); if (existing ! null) { existing.setQuantity(existing.getQuantity() quantity); cartItemService.updateById(existing); } else { CartItem item new CartItem(); item.setUserId(loginUser.getId()); item.setProductId(productId); item.setQuantity(quantity); cartItemService.save(item); } return Result.success(加入购物车成功); }这里有一个我特别想强调的细节购物车条目里不要把商品名称、价格都冗余进去。原因很简单如果后台修改了商品价格购物车里旧的冗余价格就过期了用户下单时要么用旧价亏钱要么用新价产生“购物车价格和结算价不一致”的纠纷。正确的做法是购物车只存userId和productId算价格的时候再去查商品表。4.4 提交订单与事务管理提交订单是整个项目最有技术含量的部分也是面试官最喜欢追问的地方。核心实现思路我前面说了这里放一个关键代码骨架Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long addressId, ListLong cartItemIds, String remark) { // 1. 查出购物车条目 ListCartItem cartItems cartItemMapper.selectBatchIds(cartItemIds); if (CollectionUtils.isEmpty(cartItems)) { throw new BusinessException(购物车为空); } // 2. 遍历购物车查询商品并计算总价 BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem item : cartItems) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品不存在或已下架); } if (product.getStock() item.getQuantity()) { throw new BusinessException(商品【 product.getName() 】库存不足); } // 3. 乐观锁扣减库存 int rows productMapper.deductStock(product.getId(), item.getQuantity()); if (rows 0) { throw new BusinessException(商品【 product.getName() 】库存扣减失败); } OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductImage(product.getMainImage()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItems.add(orderItem); totalAmount totalAmount.add(orderItem.getTotalPrice()); } // 4. 生成订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setAddressId(addressId); order.setTotalAmount(totalAmount); order.setStatus(0); // 待支付 order.setRemark(remark); orderMapper.insert(order); // 5. 批量插入订单明细 for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 6. 清空购物车 cartItemMapper.deleteBatchIds(cartItemIds); // 7. 返回订单信息 OrderVO vo new OrderVO(); vo.setOrderNo(order.getOrderNo()); vo.setTotalAmount(totalAmount); return vo; }Transactional(rollbackFor Exception.class)是这道工序的保险锁一旦方法内的任何一个操作抛异常前面所有已执行的数据库变更全部回滚。这里有几个容易忽视的细节rollbackFor必须设置成Exception.class而不是默认的RuntimeException。因为有些异常是受检异常比如IOExceptionSpring默认不会对受检异常回滚。deductStock这个SQL本身带条件判断写的是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个写法避免了两个用户同时下单造成库存超卖。这是一个很实际的问题我在布置这个项目时经常让同学们想一想“两个用户同时购买最后一个商品会发生什么”能答上来的人不多。订单明细表的product_name和price字段存的是当时的快照。也就是说订单生成后再修改商品名称和价格历史订单不会受影响。这在电商里属于常识但很多初学的项目完全没有这个概念。5. 文件上传与MinIO集成方案5.1 为什么用MinIO而不直接用本地路径宠物用品网站必然要上传商品图片这就涉及文件存储方案。最偷懒的写法是建一个upload目录把图片保持到本地磁盘数据库里存一个相对路径。这个方法在开发调试阶段没问题但有一个隐性坑IDEA部署时使用的临时目录和打包后的目录经常不一致图片明明上传成功了页面却找不到图片。比本地存储靠谱一些的方案是用MinIO。MinIO是一个开源的对象存储服务API完全兼容Amazon S3部署简单单机版一条命令就能跑起来非常适合中小型项目。它把文件以对象二进制数据的形式放进桶Bucket里每个对象有一个唯一的访问路径通过HTTP就能访问。有了MinIO商品的图片管理就变得很舒服图片上传到MinIO返回一个访问URL数据库存储这个URL后台删除图片时同时删除MinIO里的对象将来部署到Linux服务器时图片存储逻辑不用改只要把MinIO地址改一下就行5.2 Spring Boot集成MinIO步骤第一步在pom.xml里引入依赖dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency第二步在application.yml里配置MinIO连接参数minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: pet-images第三步写一个MinIO配置类Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }第四步封装文件上传服务Service public class FileService { Autowired private MinioClient minioClient; Value(${minio.bucket-name}) private String bucketName; public String upload(MultipartFile file) throws Exception { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String objectName UUID.randomUUID().toString().replace(-, ) suffix; minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return http://localhost:9000/ bucketName / objectName; } }这里有一个关键点文件名一定要用UUID或者时间戳重命名. 为什么不直接用原始文件名因为用户上传的图片很可能叫未命名.jpg或者cat (1).png这些名字里可能包含括号、空格甚至中文字符放进URL里会引发编码问题另外如果两个用户上传了同名文件后面的会覆盖前面的造成图片丢失。商品管理的后台表单图片上传按钮调用这个Service把返回的URL存入商品表前端img标签直接引用这个URL显示图片整个闭环就打通了。6. 部署上线与文档编写要点6.1 本地打包和部署方式当我们把功能都调通以后最后一个环节是部署。这一步对就业导向的人来说尤为重要因为在企业里开发完成之后必然要提交测试、部署到测试环境或生产环境只会在IDEA里点“Run”是不够的。Spring Boot项目打包部署有几种常见方式方式一使用IDEA右侧Maven面板执行package命令在target目录下生成一个xxx.jar文件。方式二使用IDEA终端执行mvn clean package -DskipTests跳过测试打包。方式三部署到服务器将jar包上传到服务器使用java -jar pet-shop.jar启动。官方推荐用nohup java -jar pet-shop.jar app.log 21 来后台运行这样关闭终端后项目不会停。打包之前有一个问题需要确认application.yml里数据库的IP是不是指向了部署环境对应的地址。很多人打包到服务器后发现连不上数据库就是因为配置里还写着localhost。如果项目里的前端资源也打成jar包了那么整个站点其实就是“一个jar包 一个MySQL 一个MinIO”部署起来非常轻量。这也是Spring Boot与传统war包部署方式最大的区别——不需要独立安装Tomcat内置的Tomcat会随着jar包一起运行。6.2 项目文档应该怎么写才“加分”“源码文档”这个组合里文档往往被低估了。实际上文档写得好不好直接决定这个项目拿出去答辩或者面试时的说服力。一套完整的项目文档在我眼里至少应该包含下面这些内容项目概述这个系统解决什么问题面向什么用户技术选型说明用了哪些技术为什么这样选数据库设计文档ER图、表结构说明、核心字段解释功能模块说明每个模块的作用、界面截图、操作流程核心业务流程提交订单、库存扣减这类重点环节的时序或文字描述部署文档环境要求、部署步骤、初始账号测试报告核心功能的测试用例与结果很多人的文档写到最后变成了“代码注释大杂烩”把每个方法的源码都贴一遍但毫无解释这种文档价值很低。文档的核心价值在于“辅助理解”不在于“代码复现”。数据库设计文档应该解释为什么订单项要做商品信息快照业务流程序章应该讲清楚库存扣减和事务的关系。写这类内容对你自己的知识梳理也很有好处半年后再回看你会感谢当初认真写过文档的自己。6.3 源码阅读的正确姿势最后简单聊一下怎么读一套开源或购买的Spring Boot项目源码。我的习惯是先跑起来顺着界面点一遍知道网站有哪些功能。看数据库表结构通过表和表之间的关系反推业务模型。从Controller层入手顺着一个完整业务比如“用户下单”走一遍代码调用链。再看配置文件和通用类统一返回结果、异常处理、全局常量。最后精读核心业务的ServiceImpl尤其是涉及事务的地方。这个顺序其实是从“整体”到“局部”的过程。不要一开始就钻进某个复杂的方法里那样很容易迷路。7. 常见问题与排查技巧7.1 项目启动失败控制台报数据库连接错误这个问题出现频率极高。报错信息一般包含Access denied for user或Communications link failure。排查步骤很简单检查application.yml里的username和password是否和本地MySQL一致检查MySQL服务是否启动可以在命令行执行mysql -u root -p验证检查数据库是否已经创建执行show databases;看看里面有没有项目需要的库名如果密码本身包含特殊字符比如或#注意Spring Boot读取时是否需要转义7.2 启动后浏览器显示Whitelabel Error Page这个错误最常见的原因是后台代码执行时抛了异常但页面没捕获到具体信息。解决方法是打开控制台查看日志看堆栈信息或者临时把application.yml里的server.error.include-message设为always。我在开发阶段一般直接把异常打印到控制台定位起来最直观。还有一个隐蔽原因Controller里返回的视图名称和templates目录下的HTML文件不对应。Thymeleaf会拼上prefix和suffix查找模板如果文件不存在或路径写错同样会报这个错。7.3 Mapper接口和XML绑定的坑报错Invalid bound statement (not found)十有八九是XML文件没有被Spring Boot扫描到。排查方法如下表检查项说明MapperScan是否配置了Mapper接口所在包启动类注解里的包路径必须和接口实际路径完全一致mapper-locations是否指向XML所在位置默认是classpath:/mapper/**/*.xml如果XML放别处要改target目录下是否存在XML用IDEA的Build - Rebuild Project看编译输出Mapper接口方法名是否和XML里的id一致动态代理是通过方法名匹配XML的这一块我也踩过坑之前有一次在resources下建了mappers目录但配置里写的是classpath:mapper/**/*.xml少了个s结果所有查询都报错。这种错误纯粹是细心问题但特别消磨耐心。7.4 图片上传成功但页面无法显示如果用的是本地存储方案先检查保存的路径是否在可访问的静态资源目录下。Spring Boot默认的静态资源路径是classpath:/static/如果你上传到了项目根目录的upload文件夹那就需要自己写一个映射registry.addResourceHandler(/upload/**).addResourceHandler(file: uploadPath)。如果用的是MinIO方案则检查MinIO的Bucket策略是否允许公共读或者直接访问MinIO生成的临时预签名URL。7.5 IDEA启动时控制台不打印端口号这个现象很多人遇到项目明明启动成功了但控制台没有输出“Tomcat started on port 8080”那一行浏览器也访问不了。大概率是启动类扫描不到Controller或者启动类本身没有SpringBootApplication注解。解决办法确认SpringBootApplication所在包是com.pet.shop而所有Controller都放在它的子包下面不然组件扫描默认只会扫描启动类当前包和子包。如果还不行执行一次mvn clean再重新启动有时候是target目录里残留了旧的编译文件导致新的控制器没有被加载进去。8. 项目扩展与性能优化建议项目做完了、跑通了如果能再往前多走一步它的含金量会明显不同。我这里根据自己的经验给出几个值得优化的方向。第一个方向是接口层面的参数校验。用Spring Boot自带的Validated和NotBlank、Min这类注解对前端传参做规范和限制。比如商品库存不能小于0、订单数量不能大于999这些校验规则写在Controller层比散落在Service层要清晰得多。第二个方向是统一异常处理。很多项目在Controller里写大量的try-catch重复又难看。用RestControllerAdvice定义一个全局异常处理器把业务异常、系统异常分开处理返回统一的Result结构代码会清爽很多。第三个方向是缓存热点数据。比如商品分类是很少变的数据可以用Spring Cache配合Caffeine本地缓存做一层缓存减少对数据库的查询压力。商品详情页也可以加Redis缓存但考虑到一个课设或毕设的量级本地缓存已经足够了。第四个方向是日志规范。在关键业务节点创建订单、支付回调、库存不足打印日志带上关键参数后续排查线上问题会非常高效。很多新手不写日志出了问题只能靠“猜”这是很糟糕的工程习惯。不过反过来我也要说一句优化的前提是“能用、好用、稳定”。不要一开始就抱着“我要把项目做成高并发亿级流量”的念头去设计那必定过度设计。先把一个完整的交易闭环做的扎实、没有低级Bug比什么都重要。这个项目能让你把Spring MVC、MyBatis-Plus、事务、拦截器、文件上传、部署这些都练熟学到的东西已经足够你应付工作中八成的CRUD场景了。按照我个人的经验拿到这样一套项目最忌讳的就是“眼高手低”——代码跑起来就觉得万事大吉一问订单快照是什么答不上来。真正把这套项目的每个模块都自己敲一遍、改一遍、部署一遍你的Spring Boot水平肯定会上一个台阶。
返回列表