华强北这三个字,在老数码玩家心里基本就是“二手手机”的代名词。好多朋友找我聊毕业设计或者课程设计选题的时候,我第一个想到的也是这类系统——业务真实、技术点覆盖全面、做完以后能讲的故事也多。这次拿到的这个题目,基于 Spring Boot 的二手手机商城管理系统,整套东西带源码、数据库脚本和万字文档,直接拿来作为毕设主体或者在此基础上做二次扩展,都很合适。
今天这篇就把这套系统的骨架彻底拆开讲讲:从需求设计、数据库建模、核心模块实现,到部署上线和答辩亮点怎么挖掘,一次性聊透。不管你是刚开始动手的新手,还是已经跑通了一部分功能想再优化优化的进阶选手,这篇都可以直接当一份“避坑长文”来收藏。
1. 项目靠什么立足:二手手机交易的特殊业务逻辑
1.1 泛商城太常见,二手手机商的“非标品”才是差异化
市面上基于 Spring Boot 的商城项目一抓一大把,图书商城、零食商城、服装商城,换套皮肤就又是一份毕设。但二手手机商城不一样,它有一套非常独特的业务规则,这套规则才是系统真正要解决的问题。
- 非标品属性:每一台二手手机的新旧程度、外观瑕疵、维修历史都不一样,没办法像卖新手机那样只有一个 SKU 和一个统一定价。
- 动态定价:一台 iPhone 15 Pro 可能因为发布几个月、供需波动甚至颜色不同,价格每天都有变化,必须有灵活的调价机制。
- 质检与成色评估:懂行的人买二手手机,一上来问的绝不是价格,而是“电池效率多少”“边框有没有磕碰”“屏幕是不是原装”。这些信息必须结构化地录入系统,并且要在商品详情页直观展示。
- 回收与置换:很多用户不仅有买的需求,还有“把旧手机卖掉”的需求。一条完整的业务链路应该能支撑用户提交估价申请,系统给出参考报价,达成意向后再线下寄送或到店交易。
这才是这个系统跟普通“增删改查”商城拉开档次的地方。如果只是把 Spring Boot + MyBatis 跑通,然后做五个基本管理页面,那项目做完之后能讲的东西非常有限。真正拿到高分或者让面试官眼前一亮的,恰恰是在这些业务细节上的处理。
1.2 功能模块怎样划分才能既完整又不过度设计
整套系统我建议拆分为三大端:前台商城端、用户端(个人中心)和后台管理端。后台管理端是重头戏,覆盖面要全;用户端要贴合真实使用习惯,尽量做到流程闭合。
我整理了一张功能模块清单,在实际动手之前建议拿这张表做对照,防止做到一半发现自己漏了业务环节:
| 模块 | 核心功能 | 说明 |
|---|---|---|
| 商品管理 | 上下架、库存锁定、成色参数录入 | 每台手机一条SKU,独立参数 |
| 类目管理 | 品牌、型号、价格区间分类 | 支撑前台筛选和搜索 |
| 用户模块 | 注册登录、收货地址、订单管理 | 不建议只做最简单的登录注册 |
| 交易模块 | 购物车、下单、支付状态、物流状态 | 订单状态必须贯穿全生命周期 |
| 估价回收模块 | 提交回收单、系统估价、线下流转 | 这是二手领域的特色功能 |
| 平台运营 | 公告、横幅、短信通知、操作日志 | 可以让项目层次更丰富 |
| 管理员后台 | 权限划分、数据看板、订单管理 | 至少区分超级管理员和普通管理员 |
这里有一个很关键的取舍思路:模块宁多勿少,但每个模块的深度可以有所侧重。比如权限模块,不一定要引入 Spring Security 全套复杂权限模型,可以直接用拦截器 + 自定义注解来实现“管理员角色校验”,这已经能满足大部分毕设的场景要求,而且实现起来可控性更强。
2. 技术选型到底怎么定:Spring Boot 只是起点
2.1 为什么 Spring Boot 依然是课程设计的最佳选项
很多人在选题的时候会纠结要不要用微服务架构,或者要不要引入 Spring Cloud Alibaba 显得高级一些。我的建议非常明确:课程设计阶段,单体应用 + 模块化设计完完全全够用。理由有两点:
第一,答辩和评审环节,老师更看重的是“你能否把一条完整的请求链路讲清楚”,而不是你用了多少个注册中心。单体架构下,从 Controller 到 Service 到 Mapper 的调用链路非常清晰,你可以随时在白板上画出完整流程,这种能力比堆砌框架更能展示真实水平。
第二,微服务的分布式问题,比如分布式事务、服务间调用、统一配置中心,对于数据量百级别的小型系统来说,很多时候是反模式。如果你强行引入 Nacos + Feign + Sentinel,不仅部署复杂度提升一个量级,还可能因为环境问题在演示当天出丑。
Spring Boot 在这个项目里的定位应该是一个“组装者”和“调度者”:使用 Spring MVC 暴露 RESTful 接口,使用 MyBatis(或 MyBatis-Plus)操作 MySQL,使用 Thymeleaf 或独立前后端分离模式来渲染页面,使用 Spring Task 或 Quartz 处理定时任务。每一个选型都有明确的业务场景对应,这样在文档里写“第X章 技术选型”的时候,你会发现自己能够给出有说服力的理由,而不是在复制框架介绍。
2.2 前端方案:模板渲染还是前后端分离
这是个绕不开的分岔路口。两种方案各有优劣,我的意见取决于你当时的目标场景。
- 服务端模板渲染(Thymeleaf):部署时只需要打一个 jar 包,直接在服务器上跑起来。架构简单,内存占用低,非常适合演示传统“多页面应用”的交互流程。缺点是前端交互体验比较朴素,异步请求要么不写,要么写得非常原始。
- 前后端分离(Vue + Element UI / 微信小程序):更贴近企业级开发流程,接口复用性好,后面前台和后台可以各做一套 UI。如果你打算以后往 Java 后端工程师方向发展,这个方案能让你把“接口文档”“跨域处理”这些企业里真实存在的概念全部接触一遍。
我见过太多人一上来就选前后端分离,结果后端接口写了不到十个,前端页面套了个 Vue Element Admin 模板,几百个文件哐哐报错改不完。如果你的前端基础一般,就老老实实用 Thymeleaf 渲染;如果你前端有一定把握,再上前后端分离。这两者在毕业设计评分里的差异,远没有你想象中那么大,因为老师打分的核心永远是你的业务闭环和数据设计逻辑。
2.3 数据库选型:MySQL 加一套有说服力的初始化数据
数据库不用犹豫,直接选 MySQL 8.x。注意我说的“8.x”,不是“5.7”。两个版本存在细节差异,尤其是窗口函数、JSON 字段特性以及下划线到驼峰自动映射的配置方式。在 MyBatis 配置map-underscore-to-camel-case这个属性时,5.7 和 8.x 的行为基本一致,但你在导入 SQL 脚本的时候,8.x 对默认字符集和排序规则的处理会和 5.7 不同,遇到问题别慌张,先检查连接字符串:
jdbc:mysql://localhost:3306/second_hand_phone?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true这个参数是 8.x 连接时的常见报错点,很多人第一回连的时候都会栽在这里。
另外,初始化数据一定不要偷懒。光有一张空空的用户表、商品表,演示的时候点开页面看到一片空白,非常尴尬。建议至少初始化 30 条商品数据、5 个品类、3 个管理员账号和几个测试用户账号。密码统一使用 BCrypt 加密,可以用在线工具生成或者写一个临时接口来生成。
3. 数据库设计的三个硬骨头
3.1 商品表和商品参数表:一张表还是两张表
这是二手手机系统里最值得讨论的设计点。一台二手手机会有非常多的动态属性,比如:
- 基础属性:品牌、型号、存储容量、颜色、网络制式、发布时间
- 成色属性:屏幕划痕等级、边框磨损等级、电池健康度、维修历史、是否过保
- 交易属性:收购成本价、销售标价、最低可接受价、当前库存状态
我的建议是拆成两张表:核心商品表 + 参数扩展表。
核心商品表存的是所有商品共用且高频查询的字段,比如id、title、price、stock、category_id、status、cover_image。参数扩展表存的是像battery_health、screen_grade、repair_history、accessory这类低频但展示时必需的数据。这样的好处有两个:
- 商品列表页和首页只要查询核心表,性能好、SQL 简单。
- 详情页通过商品 ID 关联参数表,把低频数据用懒加载方式读取,逻辑隔离清晰。
如果你在文档里把这张表结构图画明白,并且解释清楚拆分动机,这就是一个很好的“数据建模能力”展示点。
3.2 订单状态机:从下单到完结的每一个节点
订单表的status字段可以说是整个系统最重要的字段之一,千万不能用一个简单的0/1表示“未支付/已支付”。我见过的典型低分设计就是订单状态只存一个 int,然后逻辑散落在各处判断,最后整个项目自己都讲不清楚订单流到哪一步了。
二手手机商城建议至少包含以下状态:
待支付(WAIT_PAY) -> 待发货(WAIT_DELIVERY) -> 已发货(DELIVERED) -> 已完成(COMPLETED) \-> 已取消(CANCELLED) / 退款中(REFUNDING) / 已退款(REFUNDED)每个状态转换都要有对应的触发动作:
- 用户提交订单,状态为待支付,保存下单时间。
- 用户在有效期内完成模拟支付,状态变为待发货。
- 管理员发货,填写物流单号,状态变为已发货。
- 用户确认收货或者系统超时自动确认,状态变为已完成。
- 支付超时未操作,定时任务自动关单,状态变为已取消。
这里我强烈建议在shop_order表里加两个字段:create_time和payment_time。利用定时任务扫描create_time超过 30 分钟且状态为待支付的订单,将其自动置为已取消。这个功能实现不难,但在文档里能写成“订单超时自动关单机制”,听起来非常加分。
3.3 防止库存超卖:乐观锁和SQL更新的实战姿势
二手手机每一台都是孤品,库存字段的意义和普通电商不一样。普通商城库存为 10 代表可以卖 10 件,二手商城库存为 1 往往代表“这世界上库存就只有这一件”。所以在并发场景下,防止同一个手机被两个人同时下单,成了数据一致性层面必须解决的问题。
最简单的方案是使用乐观锁。在商品表加一个version字段,下单的时候执行:
UPDATE shop_goods SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND version = #{version} AND stock > 0;如果更新行数为 0,说明当前版本已经被别人修改过,或者库存不足,此时立即回滚订单操作,由 Service 层抛出统一业务异常。
这样实现比在 Java 里先select stock再update stock的方式健壮得多——你完全避免了“先查后改”中间那个时间窗口里被别人抢单的可能性。写文档的时候,这一小段逻辑可以展开写成“基于乐观锁的库存扣减方案”,属于标准的实战级写法。
推荐几个关键表名,供你参考:
user_account:用户账号表shop_category:手机类目表shop_goods:商品主表shop_goods_detail:商品参数扩展表shop_cart_item:购物车表shop_order:订单主表shop_order_item:订单明细表recycle_order:回收估价单表admin_user:管理员表operation_log:操作日志表
别把表名起得很随意,表名与字段名风格统一,这在导师初筛论文时是隐性的加分项。
4. 核心功能模块的落地实拆:不只是“能跑”而已
4.1 注册登录与权限控制:从 Session 到拦截器
用户注册登录这块,老生常谈,但我还是建议你至少做三个动作:
- 密码加密:绝对不能明文存储密码。用 Spring Security Crypto 模块里的 BCrypt 进行哈希加密,存储格式是这样的字符串:
$2a$10$7rzTBrg...。以后就算数据库被泄露,攻击者也没办法轻易还原出原始密码。 - 登录状态管理:使用 Redis 存储用户 Session 信息可以展示你对分布式缓存的理解,但如果没有安装 Redis 的条件,直接用 HttpSession 也完全可以接受。建议优先保证功能闭环,而不是为了用 Redis 而强行引入新组件。
- 管理员接口保护:在后台管理 Controller 上加一个自定义注解,比如
@RequireAdmin,通过 HandlerInterceptor 统一拦截,判断当前 Session 中是否包含管理员标识。用很小的代码量解决后台数据暴露风险,同时这也是一个可以写进“系统设计亮点”的小机制。
@Component public class AdminAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object adminId = request.getSession().getAttribute("adminId"); if (adminId == null) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"管理员未登录\"}"); return false; } return true; } }这种实现方式读起来干净清楚,答辩的时候你完全可以把核心代码投影出来,逐行解释“为什么需要这里的判断”,给老师的整体印象会好很多。
4.2 商品列表筛选设计:链式查询让条件组合更优雅
二手手机的筛选请求往往会带多个条件:品牌、价格区间、成色等级、存储容量。后台收到的查询参数是一组可能为空的字段。如果为每一个条件都写一个if拼 SQL,代码会变得非常啰嗦。
建议使用 MyBatis-Plus 的条件构造器,或者最简单的 service 层链式组装:
LambdaQueryWrapper<ShopGoods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(query.getBrand()), ShopGoods::getBrand, query.getBrand()) .between(query.getMinPrice() != null && query.getMaxPrice() != null, ShopGoods::getPrice, query.getMinPrice(), query.getMaxPrice()) .eq(query.getScreenGrade() != null, ShopGoods::getScreenGrade, query.getScreenGrade()) .eq(ShopGoods::getStatus, 1) .orderByDesc(ShopGoods::getCreateTime);这种写法的可读性极高,后期维护成本也低。文档里你还能借此写出“解决了多条件组合查询时 SQL 拼接繁琐的问题”这样有实际内容的技术总结。
4.3 回收估价模块:让系统拥有“行业特色”
我强烈建议你实现一个回收估价模块,这是整个项目里最能够体现“二手手机”行业特色的地方,也是很多人低估了价值的功能。
页面逻辑大致是:
- 用户在回收页面选择品牌、机型、存储容量。
- 系统根据预设的参考价表给出一个基准估价。
- 用户选择手机成色(屏幕划痕、边框磕碰、功能是否正常),每选一项,估价金额对应调整。
- 提交回收单后,后台管理员看到询价信息,可以通过后台修改报价,然后用户端收到“最新报价通知”。
这个功能的代码量不大,但业务逻辑非常完整。你可以设计一个recycle_quote_config表存储不同成色区间对应的折价比例,控制层写一个简单的统计计算逻辑,体验会非常惊艳。更重要的是,当你写到项目文档的“核心业务分析”部分时,这个模块的存在会让整套系统的立意立刻脱离“毕业设计常见电商系统”的俗套。
4.4 定时关单:既能闭环,又是亮点
订单支付超时自动关闭是在真实电商系统里必不可少的环节。Spring Boot 使用自带的@Scheduled注解就可以实现:
@Scheduled(cron = "0 */5 * * * ?") public void closeExpiredOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(30); List<ShopOrder> expiredOrders = orderMapper.selectExpiredPaidOrders(deadline); expiredOrders.forEach(order -> { order.setStatus("CANCELLED"); orderMapper.updateById(order); // 回滚库存 goodsService.restoreStock(order.getId()); }); log.info("定时关单执行完成,本次关闭 {} 单", expiredOrders.size()); }这段逻辑有两个细节需要注意:
- 关单的同时必须把锁定掉的库存回填回去,不然会出现用户看到没有支付成功、但别人又买不了这台手机的情况。
- 定时任务的执行频率不必太频繁,每 5 分钟跑一次已经非常够用。使用
@Scheduled时记得在配置类上开启@EnableScheduling,这是新手最容易漏掉的一行。
5. 从启动到部署:环境问题和打包细节
5.1 本地跑起来的最短路径
如果你想把这套系统在自己电脑上跑通,推荐按以下顺序操作:
- 安装 JDK 8 或 JDK 17(看项目配置的 Java 版本,推荐 8,兼容性最好)。
- 安装 MySQL 8.x,新建数据库并导入随项目附带的
second_hand_phone.sql脚本。 - 配置 IDEA 里的 Maven 仓库,使用阿里云镜像加速依赖下载,避免一直卡在下载 Spring Boot 依赖的状态。
- 修改
application.yml中的数据源配置,把账号密码改成你本地的值。 - 直接运行主启动类,如果遇到端口占用,去配置里改端口。
我用过很多 Spring Boot 开源的毕设项目,最终的拦路虎往往不在代码本身,而是数据库连接、端口占用和依赖下载这三座大山。一旦启动成功,请在后台管理系统先把基础参数配置好,再录入一批商品数据,保证前台页面有内容可看。
5.2 Linux 服务器部署:从“本机能跑”到“线上能访问”
如果你要求自己做得再高级一点,可以把这个项目部署到免费的云服务器或轻量级应用服务器上,展示从本机到线上的完整链路。
打包命令:
mvn clean package -DskipTests打包成功之后,在target目录会生成一个xxx.jar文件。在服务器上只需要:
nohup java -jar second-hand-phone-0.0.1.jar --spring.profiles.active=prod > app.log 2>&1 &这时要特别注意几个环境差异问题:
- 服务器的 MySQL 如果没有开放 3306 端口的外部访问权限,应用是连不上的,需要在防火墙和安全组里放行。
- 数据库初始化脚本要在服务器 MySQL 上再执行一遍,注意 Linux 下 MySQL 对大小写的敏感性比 Windows 严格,表名最好统一小写。
- 图片上传路径建议配置成绝对路径,不要用相对路径。因为 jar 运行时的当前目录和开发时不一致,相对路径很容易出现文件“传成功了但找不到”的诡异问题。
6. 踩坑实录:我在类似项目里遇到的三个隐蔽问题
6.1 图片上传成功却无法访问:绝对路径与映射路径之间的逻辑
这里分享一个我在实操中遇到的经典问题:项目开发时图片上传到本地某个目录,浏览器访问也能看到图。但部署到服务器后,用 jar 方式运行时,上传的图片传给前端再也渲染不出来了。
排查链路是这样的:
- 先确认图片文件真实存在于上传目录,结果文件确实存在。
- 再在浏览器打开图片 URL,结果返回 404。
- 接着检查 Spring Boot 的静态资源映射配置,发现项目里根本没有写“访问路径到磁盘路径”的映射规则。
问题根源在于开发时图片地址恰好落在 IDE 的资源目录里,而打包成 jar 之后路径全变了。解决办法是在配置类里面显式添加资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/upload/"); } }踩过这次坑以后我就养成了习惯:凡是涉及文件上传的项目,一律在文档里标注“图片上传目录为可配置绝对路径”,因为这是一个极易被忽略又极易在演示当天翻车的点。
6.2 MyBatis 的驼峰映射:为什么你查询出来的字段全是 null
这个坑几乎每隔一阵就会有一个同学踩到。数据库字段设计成了下划线风格brand_name,实体类字段写得是驼峰风格brandName,结果运行查询方法后发现所有属性都 null。
原因非常简单:MyBatis 默认不会自动将brand_name映射为brandName。有两个解决方案:
方案一,在application.yml里开启全局配置:
mybatis: configuration: map-underscore-to-camel-case: true方案二,在 SQL 里主动写别名select brand_name as brandName。
我强烈建议选方案一,一次性全局解决,不用在每条 SQL 里折腾。这个细节也是很多新手在文档“项目难点”里提到的第一个坑,完全值得写进正文。
6.3 超卖之外的隐患:订单状态错乱是怎么造成的
订单状态错乱是我在评审别人项目的时候发现的另一类高发问题。很多项目会在updateStatus的操作里直接写:
update shop_order set status = #{targetStatus} where id = #{id}这种做法在单线程环境下没问题,但一旦用户和管理员几乎同时操作同一个订单,就可能出现状态回退。比如管理员发货时把订单从待发货改成已发货,与此同时用户点击取消订单,把同一个订单改成了已取消。数据库最终呈现的状态完全取决于两条 SQL 的执行顺序,这两个操作从业务上来说是矛盾的——已发货订单不应该再被用户取消。
正确的做法是限定条件更新:
update shop_order set status = #{targetStatus} where id = #{id} and status = #{expectedStatus}执行行数为 0 时说明当前状态已经不是你期望的那个状态,此时业务层应该直接报“订单状态已变更,操作失败”的提示。这虽然只是两行 SQL 的差异,却能帮你挡住几乎所有因为并发操作导致的状态错乱问题。
7. 答辩和文档的加分打法:怎么让项目看起来“有深度”
7.1 把技术亮点提炼成一句十五秒的简介
很多同学在答辩时开口就是“我用了 Spring Boot 和 MyBatis 做了一个商城网站”。这句话的信息量太低了。建议按这个句式结构来准备:
“本系统是一个面向二手手机交易的商城平台,核心难点包括:基于乐观锁的库存扣减保证设备唯一性、订单状态机驱动交易全生命周期、回收估价引擎实现动态报价,以及基于定时任务实现的超时关单机制。”
一句话里就包含了四个技术关键词,老师一听就知道你没有停留在增删改查层面。
7.2 文档写作顺序建议:数据库设计优于功能实现
我知道很多同学写文档喜欢按“先环境搭建、再功能实现”的顺序流水账式地写,但这样的文档在导师眼里非常平庸。建议按以下顺序组织:
- 绪论和背景,明确二手手机交易中信息不对称、非标品定价效率低的痛点。
- 需求分析,画清楚用户角色和业务流程,这里重点讲回收估价流程、购买流程和管理流程。
- 数据库设计,放 E-R 图、核心表结构、状态机设计,这部分是重中之重。
- 系统实现,每个功能模块包含“页面截图+核心代码+逻辑解释”。
- 系统测试,功能测试、并发场景模拟、兼容性验证。
- 总结与展望,写清楚你自己从项目里收获了什么。
7.3 并发问题的表现技巧:不要只聊概念,给出数据
如果你在文档里写了“使用乐观锁解决并发超卖问题”,但还是觉得抽象,建议补充一个实测数据:用 JMeter 模拟 100 个用户同时抢购一台手机,最终只有 1 个用户下单成功,其余 99 个请求都返回库存不足或版本冲突。这个测试过程不过二十行字,但能把你从“我学过这个概念”提升到“我亲手验证过这个概念”的层级。
8. 这套系统还能继续进化的方向
整套系统跑通、文档写完、答辩结束之后,其实还有几个非常自然的迭代方向,可以按自己的精力和兴趣选择:
方向一:接入微信小程序端。现在用户习惯在手机上买二手设备,如果能把商城前端做成小程序,复用后端所有接口,工作量集中在 UI 适配和登录鉴权方式改造上,是目前最容易被看懂成果的进化方向之一。
方向二:加入基于规则引擎的报价系统。回收模块目前的估价逻辑是数据库里的折价率配置,如果机器学习的特征工程能跟上,完全可以升级成一个线性回归估价模型,把成交记录作为训练集。
方向三:引入 Redis 缓存热点商品。首页爆款手机的上架信息和价格数据访问频率最高,可以通过缓存降低数据库压力,用本地缓存开除或 Redis 双写机制保证数据一致性。
这几种方向既不会影响现有系统运行的稳定性,又能够在你有余力的情况下保持项目的新鲜感。
这套系统本身的容量其实比我描述的还要大一些,随项目附带的源码和数据库脚本能帮你节省大量从零搭建的耗时,但真正让你在评审中站稳脚跟的东西,永远是你对每一条业务链路背后原因的透彻理解。别让自己停留在“点按钮看页面”的程度——试着把它当成一个真实存在的交易平台去打磨,收获会远超预期。