ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+SSM旧物回收商城系统:架构、数据库与调试全解析

SpringBoot+SSM旧物回收商城系统:架构、数据库与调试全解析 每年毕业季都有同学倒在做系统上环境配好、代码导入、数据库一还原项目不报错登录页也能显示可一到“注册—登录—发布闲置—下单—生成订单”整套流程走下来就各种卡壳。旧物回收商城系统这类项目表面看起来就是个普通电商实际上订单状态流转、回收单与商品单的类型转换、图片上传路径、分页查询、权限拦截这几个环节才是真正卡人的地方。这篇就围绕用SpringBootSSM搭旧物回收商城系统这件事把架构方案、数据库设计、关键代码套路、调试方法完整捋一遍。无论是做毕业设计、课程设计还是想自己搞一个二手交易平台练手这篇都适用。1. 项目整体设计与技术选型思路1.1 为什么是“SpringBootSSM”这种组合很多同学一看到题目里既有SpringBoot又有SSM就觉得是两套框架并存。实际做过项目的都知道SSM指的是Spring、SpringMVC、MyBatis这三件套而SpringBoot本身是基于Spring生态的一套快速开发脚手架它默认整合了SpringMVC并简化了大量XML配置。所以“SpringBootSSM”这个组合在实际项目里通常理解为SpringBoot作为主框架提供自动配置和内嵌服务持久层用MyBatis操作数据库前端控制器基于SpringMVC规范开发。这种方案的好处是既保留了SSM体系中MyBatis那种“SQL手写可控”的灵活度又享受了SpringBoot“零配置起步”的便利不需要再自己配一堆DispatcherServlet和XML内容。我见过不少同学一开始想用纯SSM框架手写配置结果光spring-mvc.xml、mybatis-config.xml、web.xml三套文件就能折腾两天中间还有各种jar包版本冲突。而SpringBoot把这些公共配置全部收敛到自动配置里你只需要在application.yml里写数据源、MyBatis映射路径、端口这些核心参数框架就能自己把上下文拉起来。对于旧物回收商城这种典型CRUD型管理系统这种组合是最高效的也最贴近现在Java岗位的实际开发习惯。市面上大部分Java开发岗都在用SpringBoot但真正的业务冗杂度又依赖MyBatis这种“半自动”持久层来兜底所以两者一起出现的频率很高。1.2 系统的核心定位这不是普通商城旧物回收商城系统这个名字里有两个关键词旧物、回收。跟普通B2C商城不同它涉足的不只是“用户下单买商品”这条链路还需要处理“用户把闲置物品发布出来、平台审核或回收、然后转成可售卖商品”这条回收链路。简单说一条是前端商城展示和交易一条是后台回收单管理和审核入库。从用户角色上看这个系统一般都分成三类普通用户买家卖家双重身份、回收管理员审核回收单、上架商品、系统管理员用户管理、分类管理、订单管理、统计报表。很多同学容易忽略“用户既是买家又是卖家”这个设定导致数据库设计的时候把买和卖拆得太僵化后面做接口才发现用户的user_id要同时出现在商品表和订单表里而且两张表之间还要互相带出名称。这个定位想清楚后面所有模块都能顺理成章地展开。技术实现方面这个系统的核心模块通常有用户模块注册登录、个人信息、地址管理、商品模块发布闲置、商品列表、详情、搜索、回收模块提交回收单、回收单审核、回收价格评估、订单模块下单、订单列表、订单状态变化、购物车模块加购、结算、后台管理模块用户管理、商品审核、订单管理等。每个模块都能单独拉出来当面试题细问这就是这个项目含金量的来源。2. 数据库设计与核心模块拆解2.1 核心表结构一张表都不能少做这类系统如果直接在代码里写死业务逻辑后期大概率要翻车。核心表至少要包含这几张user用户表user_id、username、password、nickname、phone、avatar、address、role区分普通用户和管理员、create_time。category商品分类表category_id、name、parent_id、sort_order方便做一级和二级分类。product商品表product_id、user_id发布者、category_id、title、description、cover_image、images、price、original_price、status在售/下架/已售/待审核、create_time。recycle回收表recycle_id、user_id、category_id、title、description、estimated_price、status待审核/已通过/已回收/已拒绝、create_time。orders订单表order_id、order_no、user_id、product_id、quantity、total_price、status待付款/待发货/待收货/已完成/已取消、create_time、pay_time。order_item订单明细表order_item_id、order_id、product_id、product_name、price、quantity。cart购物车表cart_id、user_id、product_id、quantity、checked。address收货地址表address_id、user_id、receiver_name、phone、province、city、district、detail、is_default。这里要特别解释一下recycle表的定位。回收单和商品单在旧物回收系统里是两条平行但会交集的业务线用户提交的回收单被审核通过后可以由管理员将其“转成”商品单发布到商城上架。这实际上就是编程里典型的“状态机驱动数据流转”回收单从待审核变成已回收同时生成的商品记录进入“待上架”。如果不单独设计recycle表而是直接在product表里塞一个recycle_flag字段那回收单的审核历史、拒绝原因就全丢了后面排查问题会非常痛苦。2.2 状态字段设计用int小数字代替冗长字符串我见过很多新手设计表喜欢用status字段直接存“待审核”“已发货”这种中文看起来直观实际上坑很多首先存储长度不可控其次每次业务判断都要写字符串比较最后想给状态加个排序条件都无从下手。实战里最稳妥的方式是用int枚举值比如商品状态0待审核、1在售、2已下架、3已售订单状态0待付款、1待发货、2待收货、3已完成、4已取消。在Java代码里包一层枚举类前端用字典映射文本后端只处理数字。这样数据库排序、索引、查询都高效代码里也不会出现魔法值乱飞。另外注意一点凡是涉及钱相关的字段比如estimated_price、total_price在表结构里一定要用decimal别用float或double。二进制浮点数在计算金额时会有精度丢失问题金额除法减法一旦累积误差订单总额对不上、退款金额差几毛钱这种线上事故都是这么来的。Java代码里对应使用BigDecimal这才是电商类项目的基本素养。2.3 关键业务流程回收单到商品单的“变身逻辑”回收模块是这个项目最有区分度的部分。完整流程是这样的用户在小程序或Web端填写回收信息分类、标题、描述、期望价格、图片提交后生成一条recycle记录状态为待审核管理员在后台看到待审核列表审核通过后系统根据回收单内容自动创建product记录并标记卖家user_id为原用户商品状态设为待上架回收单状态改为已通过管理员还可以手动编辑商品标题、价格和图片后再上架。如果审核拒绝则回收单状态变为已拒绝后台记录拒绝理由用户可以在前台看到这条反馈。这个流程拆开来看本质上是两张表之间的一次“数据搬运字段映射”。controller层调用回收审核接口 - service层开启事务 - 先更新recycle状态 - 再insert product记录 - 提交事务。事务性要求极高如果只是update了recycle状态而没有成功创建product数据就不一致。所以业务逻辑必须用Transactional注解包起来运行时异常自动回滚。很多人在这一块忽略事务管理导致项目测试时明明回收单显示已通过商城列表却搜不到商品这就是典型的一致性事故。3. 从0到1搭建环境版本选择与系统初始化3.1 环境准备JDK、Maven、MySQL、IDEA四件套做SpringBootSSM项目环境版本一定不能乱配。我建议用下面这一套实测兼容性最稳JDK 1.8SpringBoot 2.x系列对JDK 8支持最成熟别一上来就装JDK 17除非你用的是SpringBoot 3.xMaven 3.6.3版本太低拉不了新依赖太高有时候和IDEA内置版本冲突MySQL 5.7如果电脑上装了MySQL 8.0注意驱动地址变成了com.mysql.cj.jdbc.Driver且要手动指定时区否则容易报时区错IDEA 2020.3及以上自带Spring Initializr和Maven插件导入项目更方便数据库连接配置在application.yml里这么写server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/recycle_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.recycle.mall.entity configuration: map-underscore-to-camel-case: true注意几个关键点。第一数据库名要在MySQL里先建好create database recycle_mall default character set utf8mb4否则项目一启动直接报Unknown database。第二mybatis.mapper-locations必须指向mapper xml所在包很多人项目能启动但接口报Invalid bound statement原因就是这里没配。第三map-underscore-to-camel-case这个配置能让你数据库字段product_id直接映射到Java属性productId少写一堆resultMap。3.2 Maven依赖哪些jar包是必须的pom.xml里的依赖不需要多但要全。我这里贴一套常见组合覆盖大部分需求parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.12.RELEASE/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.1.4/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.8/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.3.0/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里重点说下为什么需要PageHelper。商品列表、回收单列表、订单列表全部都是分页场景不可能一次把所有数据查出来渲染。PageHelper是一个MyBatis的分页插件它通过拦截器自动在SQL末尾拼接limit你在service层只需要写PageHelper.startPage(pageNum, pageSize)后面紧跟的第一次查询就会被分页同时返回PageInfo对象里带total等分页数据。这比自己手动拼接limit再count一遍方便得多也是实际开发中最常用的方式。3.3 启动类结构SpringBoot无缝接住SSM主启动类很简单核心是SpringBootApplication注解和MyBatis的Mapper扫描SpringBootApplication MapperScan(com.recycle.mall.mapper) public class RecycleMallApplication { public static void main(String[] args) { SpringApplication.run(RecycleMallApplication.class, args); } }这里用MapperScan扫mapper接口包后就不再需要每个Mapper接口写Mapper注解了代码更干净。项目启动后SpringBoot会自动加载application.yml里的数据源配置自动配置一个SqlSessionFactory然后把mapper xml的位置绑定到MyBatis。至此SpringMVC的DispatcherServlet、MyBatis的SqlSession、Druid连接池全部由SpringBoot自动装配完成不需要任何XML配置文件。4. 核心功能代码套路从Controller到Mapper一整套写法4.1 用户登录与拦截器权限控制不能只写在页面里用户模块最核心的除了增删改查就是登录状态的保持。常用方案是Session登录用户登录成功后把user对象放进session同时前端所有页面通过拦截器判断session里有没有user没有就跳转登录页。拦截器的实现分两步。第一步定义拦截器处理类Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(user); if (user null) { response.sendRedirect(/login); return false; } return true; } }第二步在WebMvcConfigurer里注册并设置放行路径Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /product/list, /product/detail, /css/**, /js/**, /images/**); } }这里特别提醒一点静态资源的放行一定要配好。很多同学项目能登录但页面CSS、图片全挂F12一看全是404就是因为静态资源配置在assets目录下但拦截器把它们拦下来跳转到登录页了。静态资源路径要根据你自己的项目结构灵活调整别照搬网上配置。我习惯把前端静态资源统一放在static目录然后css、js、images、fonts、plugins这几个前缀全部放行然后项目根目录下的首页也要放行不然用户访问/就被拦截了。4.2 商品发布与图片上传文件存储的三种常见选择商品发布模块涉及图片上传这是最容易出问题的一关。图片上传有几种方案难度不同方案一把图片Base64编码后存到数据库字段里。实现简单但数据库膨胀严重一张图片几MB编码后更大查询速度也受影响只适合做demo。方案二把图片保存到服务器本地目录数据库里存相对路径。这是当前最推荐的方式开发阶段在本地跑非常方便。方案三接入OSS/MinIO等对象存储生产环境推荐但本地阶段需要另外搭服务对毕设来说属于加分项。方案二的具体做法是在SpringBoot的配置文件里添加自定义上传路径recycle: upload: path: /Users/admin/upload/然后定义一个WebMvcConfigurer中的addResourceHandlers方法把/upload/**这个URL映射到磁盘路径这样才能在前端通过src/upload/xxx.jpg访问到图片Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); }Controller里上传文件的写法PostMapping(/upload) ResponseBody public Result upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(文件不能为空); } String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID().toString().replace(-, ) suffix; File dest new File(uploadPath fileName); try { file.transferTo(dest); return Result.success(/upload/ fileName); } catch (IOException e) { e.printStackTrace(); return Result.error(上传失败); } }生成文件名用了UUID目的是防止重名。如果不做这一步A上传的a.jpg会被B上传的同名文件覆盖非常危险。同时要校验一下文件类型和大小最好只允许jpg、png、jpeg、gif限制在10MB以内。这些限制在配置文件和代码里双保险防止有人绕过前端直接POST大文件导致服务器磁盘被塞满。4.3 订单状态机如何优雅处理“待付款到已收货”订单模块是电商系统的“心脏”。订单状态一共有四五个如果直接用if else嵌套写业务判断代码会越来越乱。我习惯用状态机思路拆解每个状态对应一组允许的操作每个操作只改变一个状态。比如待付款 - 用户点击付款调用pay接口状态改为待发货。待发货 - 管理员点击发货后台操作状态改为待收货。待收货 - 用户点击确认收货状态改为已完成。已完成 - 用户可以评价订单流转结束。待付款状态超过一定时间后可以取消订单状态改为已取消。在订单Controller里核心就是订单状态更新的幂等性。比如用户连续点两次付款如果第一次成功改了状态第二次应该提示“订单状态不允许操作”而不是再执行一次扣款逻辑。实现上很简单写update语句时带上status条件UPDATE orders SET status 1, pay_time NOW() WHERE order_id #{orderId} AND status 0如果返回的受影响行数是0说明订单已经不是待付款状态直接提示异常。这个写法是数据库层面的乐观锁思想在高并发场景下能避免两个请求同时操作同一张订单引起的状态错乱。在实际项目中比先查再改稳得多。4.4 回收单与商品单的关联事务保证数据一致性前面提到回收单审核通过后要生成商品记录这里给出一个简化的service代码Service public class RecycleServiceImpl implements RecycleService { Autowired private RecycleMapper recycleMapper; Autowired private ProductMapper productMapper; Override Transactional(rollbackFor Exception.class) public void approveRecycle(Integer recycleId) { Recycle recycle recycleMapper.selectById(recycleId); if (recycle null || recycle.getStatus() ! 0) { throw new RuntimeException(回收单不存在或已被处理); } // 更新回收单状态为已通过status1 recycleMapper.updateStatus(recycleId, 1); // 根据回收单信息创建商品记录 Product product new Product(); product.setUserId(recycle.getUserId()); product.setCategoryId(recycle.getCategoryId()); product.setTitle(recycle.getTitle()); product.setDescription(recycle.getDescription()); product.setCoverImage(recycle.getImage()); product.setPrice(recycle.getEstimatedPrice()); product.setStatus(0); // 待上架 productMapper.insert(product); } }Transactional(rollbackFor Exception.class)这里注意一定要写rollbackFor因为Spring默认只对RuntimeException回滚而像IOException这类受检异常默认是不会触发回滚的。如果审核过程中商品插入失败而回收单状态已经改成已通过数据就不一致了。加了这个注解后方法内任何异常整个事务都会回滚回收单状态保持原样下次还能再审核。这就是事务控制的经典场景。4.5 分页查询与关键字搜索PageHelper的完整用法商品列表页、后台管理页最核心的查询套路就是PageHelper 动态SQL 关键字搜索。前端接口传参包含pageNum、pageSize、keyword、categoryId、sortTypeService里写PageHelper.startPage(pageNum, pageSize); Example example new Example(Product.class); Example.Criteria criteria example.createCriteria(); criteria.andEqualTo(status, 1); // 只查在售商品 if (StringUtils.hasText(keyword)) { criteria.andLike(title, % keyword %); } if (categoryId ! null) { criteria.andEqualTo(categoryId, categoryId); } example.setOrderByClause(create_time desc); ListProduct productList productMapper.selectByExample(example); PageInfoProduct pageInfo new PageInfo(productList);然后返回给前端的数据结构里包含list、total、pageNum、pageSize、pages这几个字段前端就能正常渲染分页组件了。这里再说个经验Product的image字段在数据库里存的是多张图片路径用逗号分隔比如/upload/a.jpg,/upload/b.jpg取详情的时候要split成数组返回给前端方便轮播图展示。如果设计时就把多图存成单字段查询接口返回时记得做处理别直接把整串字符串渲染到img的src里。5. 调试文档与项目讲解学会让代码“开口说话”5.1 调试文档怎么读、怎么写拿到项目压缩包以后不要一上来就点运行先花半小时梳理项目结构和文档看这几个东西database目录下的sql脚本、application.yml里的配置信息、默认账号密码、项目启动说明。先看这四样能帮你省下大量排查时间。比如数据库脚本没执行或者连错库是最常见启动失败原因而默认账号密码往往就写在README或调试文档里能直接进管理员后台验证项目是否正常。调试文档里有用的东西通常是这些项目用到哪些技术、模块怎么划分、每个模块的核心流程是什么、数据库表字段说明、不同角色有哪些功能权限。这些信息对于后期答辩或者代码重构判断都非常有参考价值。写调试文档时遵循“问题背景 - 问题现象 - 排查过程 - 问题根因 - 解决方案”五步走这样无论是自己复盘还是给别人看都能快速定位。5.2 项目讲解的重点功能演示比技术名词更重要讲解项目的时候很多同学喜欢把技术名词堆在开头说“我这个项目用了SpringBoot加SSM加MyBatis加PageHelper”之类的其实听众最想听的是这个系统能干什么、流程怎么跑、为什么这么设计。一个好的讲解顺序是先讲项目背景旧物回收的痛点、线上线下结合场景再讲系统功能用户前台可以从哪几个模块操作管理员后台能管理哪些内容接着打开界面实操演示一遍完整流程然后讲数据库表设计几张核心表、表之间的关系、状态字段怎么设计最后讲技术难点回收单转商品单的事务控制、拦截器权限控制、PageHelper分页原理。这样一套下来评委或者面试官基本能判断出你真正理解了项目而不是背了一堆名词。5.3 源码阅读顺序从一条用户主流程打通全局如果你拿到一套陌生源码最快的上手方式不是挨个看文件而是找一条“最短主链路”走一遍。比如从登录操作开始登录页提交请求 - controller的login接口 - service层校验用户名密码 - session写入用户状态 - 前端跳转首页。把这一条链路走完你就知道项目里Controller、Service、Mapper分别是如何串联的。然后再走一条业务链路比如用户发布闲置商品打开发布页 - 前端先调图片上传接口 - success后再调商品保存接口 - 商品状态为待审核 - 后台管理员审核上架。两条链路走完整个项目的基本框架八九不离十了。碰到看不懂的代码就先在关键位置打上System.out.println或通过断点调试看变量值比啃代码快得多。6. 常见问题与排查思路实录6.1 启动报错“Invalid bound statement (not found)”这个报错在SpringBootMyBatis项目里出现频率极高。造成的原因有几种按经验排序第一application.yml里mybatis.mapper-locations没配或者路径不对导致mapper xml没被加载第二xml文件里的namespace写错了没有对应当前Mapper接口全限定名第三Mapper接口和xml文件不在同一个包下且没在pom里配置resources扫描xml。解决方案就是逐个排查先看target目录下有没有xml文件没有就是资源扫描问题在pom的build节点里加resources配置把src/main/java目录下的xml也打进去。6.2 页面中文乱码中文乱码是SSM老项目的祖传问题但SpringBoot下简单得多。无非三处检查第一MySQL连接URL里加characterEncodingutf8同时数据库本身创建时指定utf8mb4第二前端页面设置 第三SpringBoot默认的HttpMessageConverter已经配置UTF-8只要你的Java文件编码也是UTF-8基本不会乱码。如果数据库已经建好了但数据全是乱码那只能重导sql或者改数据表的charset。6.3 图片能上传但访问不到这类问题通常是访问路径和磁盘路径没映射。检查思路先看图片保存到了哪个目录确认文件落地了再浏览器访问http://localhost:8080/upload/xx.jpg看返回404还是200404就看WebMvcConfig里的addResourceHandlers映射路径写没写对尤其是末尾的斜杠file:/Users/admin/upload/末尾必须有/。这个斜杠少了很多人排查一下午都找不出原因。6.4 登录拦截导致CSS样式全部丢失现象能访问首页但是页面样式完全错乱F12控制台大量404。原因拦截器把所有请求都拦了静态资源没放行。解决办法就是我前面说的在excludePathPatterns里把css、js、images这些静态目录前缀加进去。注意SpringBoot的静态资源默认映射路径是/static/、/public/、/resources/、/META-INF/resources/这几个如果你的前端文件放在static下访问路径不带static前缀比如src/css/style.css但拦截器排除路径很可能写成/css/**这时候需要你根据自己的实际访问路径调整排除配置。6.5 PageHelper分页数据不对PageHelper分页最需要注意的是startPage之后只能跟一条SQL查询语句如果startPage之后又执行了其他非查询业务逻辑或第二次查询分页就会串台。比如你在startPage和查询之间Debug了一段代码、或者又调了一次select分页效果就会异常。正确用法是startPage紧挨着需要分页的Mapper方法调用。另外PageHelper对多表关联查询本身没问题但如果你用了自定义SQL且SQL里有left join那统计total的count查询在复杂场景下可能数据不对这时可以开启PageHelper的count优化参数。7. 结尾部分在实操里走完整个旧物回收商城项目我个人最大的体会是这类系统真正考察的难点从来不是某个框架的API怎么用而是多个模块之间的数据流转和事务一致性。回收单转商品、订单状态流转、图片存取这三条链路每一条拿出来都能问出不少候选人底细。所以无论是自己做项目还是接手别人源码都别急着表现“我会用SpringBoot”而是把重心放在“我清楚这个系统怎么处理数据和状态”。如果以后想把这个项目扩展加分可以往这几个方向走引入Redis缓存商品热门列表、使用RabbitMQ做回收单审核后的异步通知、接入Spring Security做更细粒度的权限管理、把图片上传换成MinIO对象存储。这些升级点其实都不复杂但每加一个都能让项目在技术深度上提升一个档次。最后分享一个调试心得也是在实战里踩过几次坑后总结的改动任何跟MyBatis有关的配置或SQL后一定要先把项目完整重启确认启动日志里出现“Loaded mapper”或者Mapper xml的报错再继续调前端。很多“接口拿不到数据”的诡异问题其实都是Mapper xml更新后没被重新编译target目录下的还是旧文件。这个细节虽然不起眼但在开发过程中会反复折磨人知道一次就能避开很多无意义的排错时间。
返回列表