☰
SpringBoot+Vue+MyBatis实战:企业级二手交易管理系统设计与实现
2026/10/14 4:02:21 网站建设 项目流程

前段时间一直在整理一套企业级二手物品交易管理系统的完整源码,技术栈是SpringBoot+Vue+MyBatis+MySQL。这套系统不是简单的“增删改查Demo”,而是真正按企业项目标准来设计的:前后端分离、角色权限划分、商品发布审核、订单状态流转、数据统计分析都有完整实现。正好趁这次整理,把整个项目的设计思路和实操过程做一个完整复盘,从需求拆解、表结构设计到启动部署、问题排查,一步一步说清楚,想拿这套源码做毕业设计、入职项目经验或者二次开发的朋友,可以直接照着走。

有人可能会问,市面上的商城系统源码那么多,为什么偏偏要拿“二手物品交易”来做?我的看法是:二手交易系统和普通电商系统最大的区别在于它的业务状态特别复杂——商品要经过发布、审核、上架、下架、售出等多个阶段,用户有买家和卖家的双重视角,订单也有线上下单、线下交易、平台担保等多种形态。这种复杂度恰恰是练手和展示能力的好地方,也是这套源码最大的价值所在。

1. 项目到底做了什么:需求拆解与系统边界

1.1 从“二手交易”到“管理系统”:这个系统的真实使用场景

先搞清楚一个关键问题:标题里说的是“二手物品交易管理系统”,不是“二手交易平台”。这两者的差别很大。如果是面向C端用户的交易平台,重点在于用户注册、商品浏览、加购下单、支付结算这些功能;但“管理系统”意味着这是一个偏后台、偏运营视角的系统,它的核心用户是平台管理员和运营人员,主要工作是审核商品、管理用户、跟踪订单、查看经营数据。

所以这套系统的定位就很清晰了:它是一个B端管理后台,管理员登录之后可以看到平台的整体运营情况,对二手商品进行审核和上下架管理,对注册用户进行禁用和权限管理,对交易订单进行全流程跟踪。当然,为了让后台有真实数据可管,系统也保留了前台用户发布商品、下单购买的基础接口,前后台共用一套数据库,只是入口不同。

这样设计有一个很实际的好处:你在简历上写“开发过二手交易系统”和写“开发过二手交易管理系统”,别人一眼就能看出前者可能只是复制了一个开源商城,后者说明你理解业务后台和前台的区别,知道权限管理、审核流、数据看板这些东西。在实际开发中,管理系统类项目的需求量其实非常大,企业内部的运营后台、管理后台、数据后台比比皆是,掌握这套系统的设计模式,比堆一堆花哨的前端交互值钱得多。

1.2 核心功能模块梳理与优先级划分

回到这套源码本身,打开工程之后,你会发现功能模块划分得非常清楚。我按业务优先级把它整理成三个层级:

第一层是基础必备功能,没有这些系统跑不起来:管理员登录认证(支持验证码)、用户管理(列表查询、状态禁用/启用)、商品分类管理(树形结构、增删改查)、商品信息管理(多条件搜索、审核、上架下架)。

第二层是交易核心功能,这是二手交易系统的灵魂:订单管理(订单列表、订单详情、发货/收货状态更新)、交易统计(成交量、成交额、商品发布量)、站内消息通知(买家卖家之间的沟通记录)。这些功能共同构成了“交易闭环”。

第三层是展示与运营功能,让系统看起来更完整、更像企业级项目:轮播图管理(首页Banner配置)、公告管理(发布平台公告)、数据报表(按时间段统计走势)、操作日志记录(记录管理员的关键操作)。这些功能对于丰富简历上的项目描述非常有帮助。

我当时设计模块的时候刻意控制了一个度——没有盲目堆功能。很多人做项目喜欢什么都加,今天加一个秒杀,明天加一个优惠券,最后代码烂成一锅粥。二手交易系统的核心是“商品流转”和“交易状态”,只要把这两条链路做扎实,系统的主体就立住了。其他功能是围绕这两条主线的辅助模块,做到够用就好。

2. 技术选型与整体架构设计

2.1 SpringBoot+Vue+MyBatis+MySQL为什么这么搭

先把技术栈挨个拆开说。后端用SpringBoot,不需要多解释,这是目前Java后端开发的事实标准。之所以不用SpringCloud,是因为单体应用完全能满足这个项目的规模,微服务反而会把问题复杂化——你部署一个注册中心、一个网关、几个服务,光环境搭建就够折腾半天,对于一个管理系统来说投入产出比太低。

持久层选MyBatis有两个原因。第一是因为二手商品查询场景特别适合写SQL:按分类查、按价格区间查、按关键词模糊查、按状态查,这几种条件经常要组合使用,MyBatis的动态SQL写起来非常灵活,比JPA那种自动生成SQL的方式可控得多。第二是面试和实际工作中MyBatis的使用频率很高,用这个框架能更好地展示你的SQL功底。

前端用Vue全家桶(Vue2+Vue Router+Element UI管理后台组件库+Axios)。Vue的响应式机制做后台管理系统非常顺手,Element UI提供现成的表格、表单、弹窗、树形控件,不需要从零去写样式交互,一两周就能把整个后台界面搭出来。

数据库当然就是MySQL,这个没什么悬念,稳定、好部署、关于它的优化资料到处都是。整个项目的架构可以这样理解:

前端Vue页面发起HTTP请求,通过Axios发送到后台接口;SpringBoot的Controller层接收请求,调用Service层处理业务逻辑;Service层通过MyBatis的Mapper接口操作MySQL数据库;处理结果统一封装为JSON格式返回给前端渲染。中间还穿插了登录拦截器做接口权限校验、全局异常处理器做错误信息统一返回、跨域配置解决前后端分离的联调问题。

2.2 后端工程结构与分层设计

整套后端源码包结构我建议你重点看一下com.xxx.secondhand这个根包下面的划分逻辑。标准的MVC分层,每一层的职责非常单一:

  • controller包:放所有接口入口,只做参数接收和结果返回,不写业务逻辑。
  • service包:接口加impl实现类的方式,业务逻辑都在这层。为什么要拆分接口和实现?一是为了规范,二是为后续做分布式场景或者Mock测试留余地,习惯性做法。
  • mapper包:MyBatis的Mapper接口,对应resources目录下的XML文件。复杂SQL写在XML里,简单SQL用注解。
  • entity包:数据库表的映射实体类,字段和表字段一一对应。
  • dto包:接口传输对象,比如分页查询的请求参数、登录返回的Token信息,不直接暴露实体类给前端。
  • common包:统一返回结果Result、错误码枚举、全局异常处理器、常量类。
  • config包:各种配置类,包括WebMvcConfig拦截器注册、CorsConfig跨域配置、文件上传静态资源映射配置。
  • utils包:JWT工具类、MD5加密工具类、文件上传工具类。

这种分包方式看着常规,但实际写代码的时候很舒服。我见过一些项目把所有类都塞在两三个包里,后面前端联调的时候找接口都要翻半天。分层清晰不只是给别人看的,自己维护起来也会省很多事。

2.3 前端工程结构与页面规划

前端工程使用Vue CLI创建,src下面的目录也做了统一划分。views目录按业务模块分子目录,比如system、goods、order、statistics。components目录放公共组件,比如UploadImage图片上传组件、Editor富文本组件、Pagination分页组件。router目录集中配置路由和路由守卫。utils目录放request.js(Axios实例封装)、auth.js(Token存取)。api目录按模块封装接口请求方法,每个页面文件对应一个api文件,前端代码里不直接写请求地址。

页面设计按后台管理系统常见的布局:左侧菜单栏加上方顶栏,顶栏放管理员信息和退出登录按钮。左侧菜单包括仪表盘、商品管理、订单管理、用户管理、分类管理、内容管理、系统管理。仪表盘放几个统计卡片展示商品总数、订单总数、用户总数、成交总额,下面放一个最近七天的交易趋势图。

前端这块有一点要注意:Vue版本不要盲目求新。这套源码用的是Vue2,因为Element UI的精髓在Vue2时代,如果你硬把它改成Vue3+Element Plus,工作量会翻倍。本质是管理后台,UI框架稳定好用排第一位。

3. 数据库设计:二手交易系统的表结构与关键字段

3.1 核心表与关联关系

数据库是这套系统的地基。我设计表结构的时候画了一张ER图,核心思想是:用户、商品、订单、分类这四张主表相互关联,其他表都是辅助运营的附属表。下面把这套源码里的核心表列出来:

sys_user(用户表):主键id、用户名username、密码password(MD5加密存储)、昵称nickname、手机号phone、头像avatar、角色标识role(1管理员,2普通用户)、状态status(1正常,0禁用)、创建时间create_time。用户名加唯一索引。

tb_category(商品分类表):主键id、分类名称name、父分类id parent_id、排序sort、状态status。这张表采用自关联方式实现树形结构,一级分类和二级分类都存在同一张表里,分类管理页面用树形表格来展示。

tb_goods(二手商品表):主键id、商品标题title、分类id category_id、发布用户id user_id、商品描述description、售价price、原价original_price、封面图cover、轮播图集images、成色等级degree(99新、95新、9成新等)、交易方式trade_type(面交、快递)、商品状态status、浏览量view_count、创建时间create_time、更新时间update_time。

tb_order(订单表):主键id、订单编号order_no、商品id goods_id、买家id buyer_id、卖家id seller_id、订单金额amount、订单状态status、买家留言buyer_note、创建时间create_time、支付时间pay_time、发货时间deliver_time、完成时间finish_time。

tb_message(消息表):主键id、发送方send_id、接收方receive_id、关联商品goods_id、消息内容content、是否已读is_read、创建时间create_time。这张表用来实现买卖双方的站内沟通记录。

辅助表包括TB_BANNER(轮播图表:id、图片地址、跳转链接、排序、启用状态)、TB_NOTICE(公告表:id、标题、内容、发布时间)、TB_OPERATION_LOG(操作日志表:id、操作人、操作类型、操作方法、操作详情、操作时间)。

设计的时候我特意没有给表加物理外键约束,只用逻辑外键关联,全部依靠索引来保证查询性能。很多新手喜欢把所有外键都建上constraint,觉得这样数据更安全。实际在企业项目里,物理外键会严重影响删除和更新性能,而且一旦有历史数据需要清洗,外键约束会变成噩梦。表之间的关联关系由Service层代码来维护,逻辑更清晰,出问题也更好排查。这也是很多实际项目的常见做法。

3.2 状态字段与逻辑删除的设计细节

状态字段是整个系统设计里最容易踩坑的地方。我举商品表和订单表的例子:

tb_goods的status字段,我用int类型,0代表待审核,1代表已上架,2代表已下架,3代表审核拒绝,4代表已售出。tb_order的status字段,0代表待付款,1代表待发货,2代表待收货,3代表已完成,4代表已取消。这两个状态机是整个系统的核心逻辑。

为什么用int而不是varchar存中文?因为状态值在程序代码里要做判断,int直接比较效率高,而且状态码比中文更稳定——如果你用varchar存“待审核”,后面想改成“审核中”,还得先更新数据库。用int加常量类的方式,前端拿到数字之后通过字典翻译显示中文,改文案只改前端不做后端迁移。

另外一个容易忽略的点是逻辑删除。很多企业级系统不会真的物理删除数据库记录,而是在表里加一个is_delete字段(0未删除,1已删除)。查询列表时统一加过滤条件is_delete=0。这样做的好处是:数据误删可以恢复、运营审计可以追溯、关联数据不会出现孤儿记录。这套源码里商品表、用户表都采用了逻辑删除。

磁盘上的浮点数比较也是一样的道理。价格字段用decimal(10,2)类型,Java对应BigDecimal,绝不能用float或double来存金额,否则0.1+0.2不等于0.3的问题会让你查账查到怀疑人生。

4. 核心模块实操:从零跑通完整系统

4.1 初始化数据库与基础配置

拿到源码之后的第一步不是急着看代码,而是先把数据库跑起来。这套系统根目录下有一个secondhand.sql文件,这是完整的建库建表脚本和初始数据脚本。我建议你在本机MySQL里创建一个全新的数据库:

CREATE DATABASE IF NOT EXISTS second_hand_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE second_hand_db; SOURCE /你的源码路径/secondhand.sql;

导入完成后,你可以先看看sys_user表,里面预置了一个管理员账号:用户名admin,密码是MD5加密后的123456(对应密文e10adc3949ba59abbe56e057f20f883e)。表里还带了几十条测试商品数据和几个测试用户,方便前端联调时直接展示效果。

接下来修改后端配置文件。源码中application.yml文件里最关键的三块配置:数据源、MyBatis映射、文件上传路径。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/second_hand_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

这里有两个细节值得说。第一是连接串后面的serverTimezone=Asia/Shanghai,如果你不指定时区,新版MySQL驱动会报时区错误或者时间比本地时间差8小时。第二是map-underscore-to-camel-case这个配置,它的作用是把数据库的create_time字段自动映射成Java实体里的createTime属性,省去每一处都写resultMap的麻烦。很多新手不配这个,导致实体属性查出来全是null,其实不是代码问题,就是这一行配置的问题。

4.2 后端启动与接口验证

配置没问题之后,在IDEA里直接运行SpringBootApplication主类。启动日志出现“Started SecondHandApplication in x.xxx seconds”就代表后端成功跑起来了。

后端启动后要做的第一件事,不是急着开前端,而是先验证接口通不通。可以在浏览器访问接口文档地址(源码集成了在线接口文档工具),页面上会列出所有Controller的接口列表。把admin接口的登录接口找到,输入用户名admin、密码123456,会返回一个Token字符串。拿到Token之后,设置到接口文档的全局参数里,再去调用商品列表接口,就能看到数据了。

这个操作的意义在于:先确认后端和数据库链路正常,再去联调前端。否则前后端同时出了问题,你根本不知道bug是后端接口返回错了还是前端渲染错了。

接口验证通过后,我来看看几个核心接口的实际代码逻辑。以商品分页查询为例,Controller层的接收参数是一个PageRequest对象,包含pageNum、pageSize、keyword、categoryId、status几个字段。Service层调用Mapper的selectPage方法,动态拼接SQL条件。这里MyBatis的动态SQL是核心:

<select id="selectPage" resultType="com.xxx.secondhand.entity.Goods"> SELECT * FROM tb_goods <where> is_delete = 0 <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>

前端传什么条件就拼什么条件,不传就查全部,这就是动态SQL比注解SQL舒服的地方。商品审核的操作更简单,管理员点击“通过”按钮,前端调用审核接口,后端把商品的status从0改成1;点击“拒绝”则改成3,同时可以填写拒绝原因。

4.3 前端启动与联调

后端自测没问题后,开始启前端。用命令行或者WebStorm打开前端工程目录,执行:

npm install npm run serve

启动成功后访问http://localhost:8081,默认会自动跳转到登录页。输入预置的管理员账号密码,进入仪表盘。如果仪表盘上有数据展示,说明整个链路已经全部打通。

前后端联调过程中,最容易被卡住的就是跨域问题。浏览器会在8081端口的页面上发起对8080端口的请求,端口不一致就触发了跨域限制。源码里解决跨域的方式是在后端加了全局CORS配置类,允许所有来源和所有请求方式。我建议理解一下这个原理,因为面试时“前后端分离如何解决跨域”几乎是必考题。

同时前端也做了代理配置,开发环境vue.config.js里设置了一个/dev-api的代理:

module.exports = { devServer: { port: 8081, proxy: { '/dev-api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/dev-api': '' } } } } }

这样前端请求/dev-api/goods/list会被转发到http://localhost:8080/goods/list,接口请求地址不会硬编码到业务代码里,后续部署到生产环境只要把target改成真正的后端地址即可。

这里插一个我踩过的坑:开发环境明明配置了代理,但请求还是404。排查了半天发现是Axios请求地址的开头写的是/api而不是/dev-api,代理匹配的规则是字符串前缀匹配,前缀对不上就不会代理。所以前端工具类封装request.js的时候,baseURL一定要跟代理的前缀保持一致,这是新手最容易忽略的细节之一。

5. 真实运行中的问题排查与避坑实录

5.1 跨域问题排查手册

跨域报错说是最常见的问题,但表现形式有很多种。最典型的一种是No 'Access-Control-Allow-Origin' header is present on the requested resource。如果你用的是后端CorsFilter方式解决跨域,要注意这个过滤器一定要放在SpringMVC处理请求之前拦截到OPTIONS预检请求。我见过把CorsFilter写成一个@Bean的普通类,但没注册到FilterRegistrationBean,结果发现配置完全不生效,浏览器还是报跨域,最后排查出来是因为过滤器顺序不对。建议直接用源码里的WebMvcConfigurer方式,重写addCorsMappings方法,这是Spring官方推荐的做法,基本不会踩坑。

还有一种情况是前端无法获取响应数据,网络请求状态码是200,但页面里拿不到数据。这种多半是前端跨域代理配置没刷新,改了vue.config.js必须重启npm run serve,热更新不会自动加载代理配置。

5.2 登录鉴权失效的N种可能

这套系统的鉴权逻辑是JWT加拦截器。前端在登录接口成功之后,把Token存到localStorage,每次请求在Axios请求拦截器里统一加上Authorization头。后端写一个LoginInterceptor拦截器,放行登录接口和获取验证码的接口,其他路径全部校验Token。

有几个地方特别容易出问题。第一是Token过期时间配置,JWT工具类里设置了过期时间,如果你改过系统时间,会导致JWT验签失败。第二是拦截器放行路径写错,比如你把登录接口的路径写成了/goods/login,实际接口是/user/login,那登录请求也被拦截了,就会一直跳转登录页。第三是前端Axios响应拦截器里对401的处理,一定要在收到401之后清空本地Token并跳转登录页,否则会出现登录过期但页面还停留着、再点接口直接卡死的现象。

鉴权这块我建议你在本地测试的时候重点看拦截器实现:HandlerInterceptor的preHandle方法里,从请求头拿Token,调用JWT工具类解析。解析成功就把用户ID放到request attribute里,后续Service层直接用,解析失败就返回401状态码统一结果。这个模式本身很经典,吃透了对理解SpringMVC的拦截器机制也很有帮助。

5.3 图片上传偶发404,问题不在上传在访问

商品发布功能里有一个图片上传组件,使用Element UI的Upload组件,后端接收MultipartFile然后保存到本地磁盘。上传逻辑本身很简单,麻烦的是前端预览上传后的图片经常404。

我用图片路径排查了很久,发现上传文件已经完整保存到本地了,但浏览器访问不了。原因是后端没有对本地磁盘的物理路径做静态资源映射。前端返回的图片URL是/images/uploads/xxx.jpg,SpringBoot默认只能访问classpath下的static目录,磁盘路径不属于classpath,所以Tomcat找不到文件。

解决办法是在WebMvcConfigurer中重写addResourceHandlers方法,把外部的uploads目录映射到访问路径:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/images/uploads/**") .addResourceLocations("file:" + uploadDir + "/"); }

这里有一点要特别注意:addResourceLocations的路径结尾必须带斜杠,否则路径拼接会出问题。Windows和Linux的盘符写法也有差异,如果你的系统部署到服务器上,最好用配置文件动态指定上传根目录,不要硬编码成D:/这种本地绝对路径。

5.4 数据统计不出来,先检查时间范围设计

仪表盘里的交易趋势图、分类占比图这些数据统计模块,我一开始写的时候掉进过一个坑:统计最近30天的数据,但SQL的条件写得非常死板,导致边界时间对不上。

比如统计7月1日到7月30日的交易额,如果直接用DATE(create_time) BETWEEN '2025-07-01' AND '2025-07-30',7月30日的订单其实包含了那天发起的但还没支付的订单,而7月1日的又漏掉了凌晨的数据。正确做法是让前端传startDate和endDate,后端对endDate做加一天处理,条件写成create_time >= #{startDate} AND create_time < DATE_ADD(#{endDate}, INTERVAL 1 DAY)。日期边界问题在数据统计里是高频bug,面试和实际工作中都很常见。

另外一个建议:所有统计SQL都从tb_order表查,不要从商品表或者其他辅助表“推算”交易数据。一方面订单表记录的是真实成交行为,另一方面统计口径要保持唯一性。如果商品表也维护了一个“是否已售”标志,订单表也有订单状态,两边数对不上,那就是给自己挖坑。

5.5 常见问题速查汇总

我把日常使用和部署这套源码过程中遇到的高频问题整理成了一张速查表,方便你快速定位问题:

问题现象可能原因解决方案
后端启动报数据库连接失败数据库没启动、密码错误、主机地址不对检查MySQL服务状态,核对application.yml账号密码
接口查询返回字段全是nullMyBatis驼峰映射没开启配置map-underscore-to-camel-case: true
登录后页面跳回登录页Token校验失败或过期检查系统时间、Token过期时间配置,重新登录
前端请求404代理前缀不匹配、后端路径错误核对vue.config.js中代理前缀与request.js的baseURL
上传图片正常但访问404静态资源映射未配置添加ResourceHandlers磁盘路径映射
启动后端口被占用8080或8081被其他程序占用修改server.port或关闭占用进程
中文乱码数据库连接串编码问题url加characterEncoding=utf8,表结构用utf8mb4
条件查询数据不对动态SQL的if条件判断错误在XML中检查if test的字段名是否正确

6. 写在最后:一些个人经验与扩展建议

整套源码从设计到落地,我最想强调的一点是:做管理系统最怕的不是功能多,而是表结构乱、状态不清晰、权限没边界。这套二手交易系统之所以结构经得起推敲,是因为先把商品状态、订单状态、用户角色这三种核心数据模型想清楚了,再往上堆模块,后面做起来就顺了。如果你只想照着代码抄一遍,那没问题,但建议你先把tb_goods表的status字段和tb_order表的status字段吃透,理解了这两个状态机,整个系统的骨架就装进了脑子里。

如果你打算在这个基础上做二次开发,我的建议是从三个方向切入。第一是引入Redis做热数据的缓存,比如首页轮播图、商品分类树这些变化频率很低但访问量很高的数据,直接从缓存读,能明显提升接口响应速度。第二是文件存储改造,当前上传的图片保存在本地磁盘,如果部署到多台服务器,图片就会分散在不同机器上,需要将上传逻辑改造成对接对象存储服务(比如阿里云OSS或MinIO),一个是本地存储,一个是分布式存储,很典型的改造方向。第三是接入WebSocket做消息实时通知,目前买卖双方的站内信需要刷新页面才能看到新消息,接入了WebSocket之后,A给B发消息,B的页面能立刻弹出来,交互体验会有质的提升。

我自己在实测这套源码时还有一个小体会:源码里所有Controller接口都统一返回了Result对象(code、message、data三段式结构),我在前后端联调阶段,因为返回格式统一,前端Axios响应拦截器只需要判断code是否等于200,异常情况全部统一弹出后端返回的message。这一点非常实用,在你自己独立做项目时也建议从一开始就确定一套统一返回规范,千万不要一个接口返回一个样子,不然前端写response处理会非常崩溃。

如果你拿这套源码做参考项目,记得把数据库初始化脚本、源码目录结构、接口文档这三样东西作为一个整体来学习和复用。源码不是拿来交差就行,真正理解了它的分层思想和状态设计,你在下一家公司做核心业务系统时才会比别人更快进入状态。后面如果遇到启动不了或者联调有问题的情况,按照前面第5章的问题排查表一步步来,90%的问题都能自己定位到原因。

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

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

立即咨询