简介:基于 Java 打造的家电商城完整版源码包含商品浏览、搜索、购物车、订单管理、支付处理等电商核心功能的代码实现,适合课程设计、毕业设计或作为 Java Web 开发进阶的实操参考。压缩包共 29 个文件、53KB,其中 14 个 class 为编译后的业务类,7 个 java 覆盖用户、商品、Service 接口与实现以及入口类,5 个 xml 用于 IDE 模块与运行配置,另有 readme.txt 部署说明、.iml 项目文件和 .gitignore 版本控制文件。项目通过 JDBC、Servlet 等 Java 标准技术串联数据访问与请求处理,可导入 IntelliJ IDEA 直接运行学习,便于观察电商系统从持久层到界面层的完整链路。已有 264 人学习下载,适合中初级开发者对照源码理解电商平台的分层设计、集中式配置管理和 Git 忽略规则的工程实践。
1. 家电商城的完整版到底“完整”在哪
商城源码很好找,能照着改源码的 Java 开发也很多,但真正把家电交易闭环写全的完整版不常见。常见的电商源码里,能注册、能搜索、能加购,一到下单就露馅:库存扣减先查询再更新,优惠券叠加结算账是负的,支付回调只改订单状态、不校验金额。家电的复杂度又比卖书卖日用品高出一截,一台电视拆成屏幕尺寸、能效等级、安装方式好几个可选组合,价格从几千到上万,大件物流费用要单独算,所以“完整版”意味着这些规则都存在,标题要表达的是那份把库存、下单、支付、售后串成闭环的 Java 工程。它适合两类人:拿完整工程当毕设和面试项目的开发,以及想把交易链路嵌进现有供应链系统的后端工程师。
2. 技术选型:Java 生态里最常见的 Spring Boot + MyBatis 组合
2.1 为什么选择 Spring Boot + MyBatis,而不是 JPA 或微服务
看到“完整版”这三个字,最容易踩的坑是先去拆微服务:商品一个服务、订单一个服务、库存一个服务。如果是在企业供应链之上叠加交易链路,这么拆是合理的;但如果目标是让接手的人能在几天内把整条链路改明白,绝大多数源码会选择单应用多模块。Java 生态里,Spring Boot 3.x 加 MyBatis 是这几年一致性最强的组合。
选 MyBatis 而不是 Spring Data JPA,核心原因是商城查询几乎不存在纯粹的实体操作:一个商品列表要把库存、价格、促销三层数据拼起来,用 JPQL 和 Specification 反而要写一堆关联对象。反过来,MyBatis 把 SQL 交回给代码,哪些查询要防重、哪些更新要带where stock >= ?条件,一眼就能看明白。MyBatis-Plus 把单表 CRUD 用通用 Mapper 兜底,业务 SQL 自己写,这一层折中是这类源码里最常见的取舍。
有个版本问题容易被忽略。现在新源码大多要求 JDK 17 或 21 跑 Spring Boot 3.x,如果本机还在 JDK 8,启动时大概率会抛UnsupportedClassVersionError,这时就得退回 Spring Boot 2.7.x。下载代码后先检查pom.xml里的 parent 版本和本机java -version,比把报错贴回搜索引擎更快。
| 组合 | JDK 要求 | 复杂查询 | 字段变更适配 | 适合场景 |
|---|---|---|---|---|
| Spring Boot 3.x + MyBatis-Plus | 17+ | XML 手写,可控 | 中 | 大多数商城源码 |
| Spring Boot 2.7 + MyBatis-Plus | 8+ | XML 手写,可控 | 中 | 旧环境团队 |
| Spring Data JPA | 17+ | Specification 学习成本高 | 高 | 字段稳定的小系统 |
2.2 Maven 依赖:一个能直接启动的最小集合
完整版的 pom.xml 通常会堆上十几个依赖,含验证码、支付 SDK、文档组件等等。去掉这些外围,一个家电商城后端至少要有下面四组依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <!-- mybatis-plus 3.5.x 对 Spring Boot 3 的兼容最稳 --> <version>3.5.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>spring-boot-starter-web提供 Web 容器和 MVC,MyBatis-Plus 负责数据库访问,Redis 用于购物车、验证码和支付锁,MySQL 驱动负责底层连接。注意 MyBatis-Plus 在 Spring Boot 3 和 2.7 下不是同一个 starter,前者要带spring-boot3标记,选错会在启动时出现 Mapper 注册失败。这个差异是 Java 面试里常被追问的版本兼容问题,也是背八股背不到、真正排查一遍就能记住的知识点。
2.3 启动前要先改的三个配置项
拿到源码后第一件事不是启动,而是把配置文件里这三组参数核一遍,它们分布在src/main/resources/application.yml:
spring: datasource: url: jdbc:mysql://localhost:3306/appliance_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: changeme hikari: maximum-pool-size: 20 minimum-idle: 5 data: redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/**/*.xml configuration: map-underscore-to-camel-case: truecharacterEncoding=utf8直接决定商品名会不会乱码,中文乱码多数不是数据库字符集不对,而是 JDBC URL 没带编码参数。maximum-pool-size不要拍脑袋填一个大值,连接池占满时新请求会排队超时,家用场景 20 足够,压测时再调大并配合数据库的max_connections。mapper-locations指定 XML 位置,运行时报Invalid bound statement,第一嫌疑人就是路径和 XML 里的 namespace 不一致。
提示:MyBatis-Plus 的逻辑删除会拦截
updateById、selectById这类内置方法,但手写在 XML 里的update要自己补上deleted = 0条件,否则可能更新到一条已经标记删除的历史 SKU。
2.4 包结构就是这页源码的骨架
完整版的“设计”体现在包结构上。一个合理的家电商城后端,会按模块而不是按 controller/service/dao 这种技术层来分包,因为后者在代码量大了以后,改一个订单功能要跨五个目录。
com.example.mall ├── common # 统一返回、异常、工具类 ├── config # 拦截器、Redis 配置、线程池 ├── module │ ├── user # 用户注册/登录/收货地址 │ ├── product # 商品:SPU/SKU/分类/品牌 │ ├── cart # 购物车 │ ├── order # 订单、订单明细、售后 │ ├── stock # 库存锁定、回补、入库单 │ └── pay # 支付单、回调、对账 └── job # 超时未支付关单、自动确认收货按模块分包后,商品模块内部才有自己的 controller、service、mapper 子包,改动边界清晰。排查问题时打开 module 目录,按名字就能定位,不需要满项目搜索。这也是“完整版”和“脚手架”的区别:脚手架给的是技术骨架,完整源码给的是能支撑买卖双方的业务模块划分。
3. 商品模型:把家电规格属性落进 SPU/SKU 与库存表
3.1 家电为什么必须拆 SPU 和 SKU
卖书只需要一本书对应一个 ISBN,卖家电不行。同一款电视可以选择屏幕尺寸、颜色、能效等级、安装方案,用户下单时买的是这些维度的具体组合。此类结构被称为 SPU(Standard Product Unit)和 SKU(Stock Keeping Unit):SPU 是“货”,SKU 是“能卖的那一件”。
这个拆分对库存的意味很明显:库存记在 SKU 上,不记在 SPU 上。一款电视有 55 英寸和 65 英寸两个 SKU,55 缺货不应拦截 65 的加购。很多初学源码把库存做成 SPU 级整数字段,表面看简化了,实际下单时不校验规格,结算价格也取不到对应型号。在完整版家电商城设计里,“库存一定在 SKU 上”是第一原则。
3.2 建表:SPU、SKU 与规格 JSON
完整版会把 SPU 和 SKU 拆成两张表,规格属性用 JSON 存放在 SKU 上,不做几十个定死字段。这样加属性不用改表结构,查询时对高频属性做精确匹配即可:
CREATE TABLE spu ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT '商品标题,如 某某品牌 65英寸液晶电视', category_id BIGINT NOT NULL COMMENT '三级类目ID', brand_id BIGINT DEFAULT NULL COMMENT '品牌ID', main_image VARCHAR(500) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT '商品标准单元'; CREATE TABLE sku ( id BIGINT NOT NULL AUTO_INCREMENT, spu_id BIGINT NOT NULL, spec_json JSON NOT NULL COMMENT '规格值,如 {"颜色":"银色","能效":"一级","尺寸":"65英寸"}', price_cent INT NOT NULL COMMENT '价格,单位分', stock INT NOT NULL DEFAULT 0 COMMENT '可售库存', sold INT NOT NULL DEFAULT 0 COMMENT '累计销量,列表排序用', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除,1已删 0未删', PRIMARY KEY (id), KEY idx_spu (spu_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT '库存单元';价格用INT存“分”,是交易系统里很常见的做法,浮点运算在优惠分摊时有精度问题,整数分在 Java 里也方便调试。逻辑删除deleted配合 MyBatis-Plus 的logic-delete-field配置,删 SKU 走更新而不是物理删除,避免订单明细关联不到历史商品。建表脚本显式写utf8mb4,否则特殊符号在部分 MySQL 8 的默认配置下会报字符集错误。
3.3 商品列表查询:从缓存兜底到条件过滤
商品详情页是典型的读多写少,完整版会在 Mapper 之上加一层 Redis 缓存。缓存粒度有讲究:SPU 标题和图片这类静态信息可以长期缓存,价格和库存必须读实时值,否则会出现下单时才发现价格对不上的尴尬。下面这段 XML 是规格条件下的 SKU 列表查询,也是家电筛选页的核心 SQL:
<select id="listSkuByCondition" resultType="com.example.mall.module.product.vo.SkuVO"> SELECT s.id, s.spu_id, s.spec_json, s.price_cent, s.stock FROM sku s INNER JOIN spu p ON s.spu_id = p.id WHERE p.category_id = #{categoryId} AND p.brand_id = #{brandId} AND s.stock > 0 AND s.deleted = 0 ORDER BY s.sold DESC, s.id DESC LIMIT #{offset}, #{pageSize} </select>条件里用s.deleted = 0过滤逻辑删除商品,用s.stock > 0把无货 SKU 直接排除,避免用户选到不可售的规格。排序用s.sold DESC而不是价格,家电的价格敏感度比快消品低,销量更容易吸引点击。LIMIT #{offset}, #{pageSize}是简单分页写法,SKU 规模到百万级后再换成WHERE s.id > #{lastId} LIMIT #{pageSize}的键集分页,SQL 主体仍是这一套。
提示:用 JSON 存规格后,如果硬要按“能效等级”筛选,MySQL 8 支持
JSON_CONTAINS(spec_json, '"一级"'),但会失去索引优化空间。SKU 在几十万以内,更稳的做法是 Java 层过滤或加一层内存缓存。
4. 订单闭环:家电下单、锁定库存与支付回调
4.1 从购物车提交到订单主表的链路
加购场景可以只操作 Redis,但提交订单必须落库。浏览器传来购物车里的一组 SKU ID 后,后端先按用户维度查回购物车,校验商品是否下架;第二步读 SKU 当前价格,注意不能用购物车的快照价格,避免用户停留较久后价格已变;第三步调用库存服务锁定库存;全部通过后生成订单主表和明细表,订单状态设为UNPAID,最后把已下单的购物车条目删掉。
4.2 库存锁定:用一条 UPDATE 解决并发问题
库存扣减最忌讳的做法是“先SELECT stock,在 Java 里判断大于 0,再UPDATE”。两个并发请求同时读到 stock=5,各自判断都能通过,最后把库存扣成负数。正确处理是让数据库保证原子性,在 UPDATE 语句里把“剩余库存足够”作为条件:
<update id="deductStock"> UPDATE sku SET stock = stock - #{quantity}, sold = sold + #{quantity} WHERE id = #{skuId} AND stock >= #{quantity} AND deleted = 0 </update>这条语句的成功与否看影响行数,update 返回 0 说明库存不足或 SKU 已删,业务层直接抛异常。stock = stock - #{quantity}是原子自减,InnoDB 在 UPDATE 时会自动锁行,并发正确性交给数据库。如果业务要求“下单预留、支付后才真正扣减”,可以再加一个lock_stock字段,同样的思路:SET stock = stock - #{quantity}, lock_stock = lock_stock + #{quantity},订单超时取消时把两列加回去。
4.3 支付回调:幂等、金额校验、状态推进
“完整版”和“能演示版”在支付回调这里的差别最大。能演示的代码收到回调就改订单状态为已支付,完整版必须处理:重复通知、伪造回调、回调金额与订单应付不一致。
@Transactional(rollbackFor = Exception.class) public void handlePayNotify(String orderNo, Integer paidAmount) { // 1. 用订单号做 Redis 锁,防止同一笔回调并发进入 Boolean locked = redisTemplate.opsForValue() .setIfAbsent("pay:notify:" + orderNo, "1", 10, TimeUnit.SECONDS); if (!Boolean.TRUE.equals(locked)) { return; } try { OrderDO order = orderMapper.selectByOrderNo(orderNo); // 2. 幂等:订单已经越过待支付状态,不再重复处理 if (!"UNPAID".equals(order.getStatus())) { return; } // 3. 金额校验:单位统一为分,不一致直接驳回 if (!order.getPayAmount().equals(paidAmount)) { log.warn("支付金额不匹配 orderNo={}, expect={}, actual={}", orderNo, order.getPayAmount(), paidAmount); return; } order.setStatus("PAID"); order.setPaidAt(LocalDateTime.now()); orderMapper.updateById(order); } finally { redisTemplate.delete("pay:notify:" + orderNo); } }三个关键点能记很久:幂等不靠支付平台保证,靠状态机自己挡;金额校验不信任回调参数,信任订单里存的应付值;Redis 锁这里防的是两个相同回调同时打到服务里,第二次进入看到状态已经不是UNPAID就直接返回。这段逻辑如果不写完整,支付成功率看着高,偶发的重复回调会把一个订单处理两遍。
4.4 订单状态机:用一张表把闭环串起来
家电订单状态不能只靠一个字符串,完整版会定义好状态机和允许的跳转,否则售后阶段会出现从“待支付”直接到“已完成”的脏数据。最小状态集如下:
| 当前状态 | 事件 | 跳转状态 | 涉及操作 |
|---|---|---|---|
| UNPAID | 支付回调成功 | PAID | 记录支付时间,通知仓库 |
| PAID | 发货 | SHIPPED | 写物流单号,可选短信通知 |
| SHIPPED | 用户确认收货 | COMPLETED | 释放预占积分 |
| PAID / SHIPPED | 申请退货 | REFUNDING | 冻结退款单,等仓储验货 |
| UNPAID | 定时任务超时 | CLOSED | 回补锁定库存 |
状态机里最容易漏的是REFUNDING -> 回补库存这一步。售后退货不是把订单删掉,而是把卖出的数量退回 SKU 库存池,同时把销量扣回去,否则首页销量只增不减。完整版源码通常把这类过渡写进 service 方法而不是散落在 controller,后接手的人看一眼状态表就能知道当前卡在哪个环节。
5. 家电商城源码的启动顺序与上线前自检清单
5.1 按这个顺序把环境搭起来
这套 Java 技术栈落地到新环境,常见做法是五步走:先装 MySQL 8 和 Redis 7,再导入 SQL 初始化脚本,接着改 application.yml 里的数据源,然后 Maven 打包启动,最后用 curl 验证链路。下面这组命令可以直接用:
# 打包,跳过测试避免因缺测试数据失败 mvn clean package -DskipTests # 启动,--spring.profiles.active 指定环境,默认读 application-dev.yml java -jar target/mall-1.0.0.jar --spring.profiles.active=dev # 验证商品查询接口 curl "http://localhost:8080/api/product/sku?spuId=1001&page=1" # 加购前先确认登录态,购物车接口依赖用户上下文 curl -H "Content-Type: application/json" \ -d '{"skuId":1001,"quantity":1}' \ http://localhost:8080/api/cart/add启动参数--spring.profiles.active=dev会覆盖配置文件里的默认值,环境隔离是完整版常见的做法,生产用 prod profile,不把生产库地址写死在源码里。curl 验证先走商品接口,确认数据库、Redis、MyBatis 全部就绪,再走加购;不要在还没登录时就调用下单接口,否则会被鉴权拦截,得出“代码有问题”的错误结论。
5.2 上线前把这张表过一遍
完整版源码一般不会在应用启动时自动建库建表,而是提供独立的初始化 SQL 脚本,手动导入这一步是默认动作。正式联调前,按下面这张表逐项核对:
| 检查项 | 验证方式 | 常见失败原因 |
|---|---|---|
| 数据库版本 | SELECT VERSION() | SQL 用了 MySQL 8 新语法,在 5.7 上报错 |
| 数据源密码 | 看启动日志是否出现连接成功 | 密码含特殊字符未转义或漏改 |
| Redis 连通 | redis-cli ping | 密码配置漏在 yml 里 |
| 定时关单 | 造一个 UNPAID 订单等 30 分钟 | 主类漏加 @EnableScheduling |
| 支付回调模拟 | 手动 POST 一笔金额错误的回调 | 未做金额校验,订单状态被改 |
定时关单和支付回调这两项,最好在联调时各造一次真实数据:把订单created_at改成 30 分钟前,重启服务或者等定时任务跑完,看锁定库存有没有回补;再给支付回调接口发一笔金额对不上的请求,确认订单状态不会被推进。完整版源码能跑通页面不算数,能跑通这几个边界才有底气自称“完整版”。
本文还有配套的精品资源,点击获取