简介:这是一套基于 Java 构建的轻量级单商户商城系统源码项目,面向中小型商家、个人开发者以及希望深入理解电商后台实现的 Java 学习者,可直接用于搭建在线销售平台,也适合作为课程设计或毕业设计的参考范本。压缩包为 zip 格式、整体约 13.2MB,上游未提供文件总数与类型明细,故不虚构清单;目前已有 166 人浏览学习。项目围绕商城业务展示了完整的 Java 技术栈:Spring Boot 负责核心服务编排,MyBatis 处理数据库访问,Thymeleaf 渲染管理端页面,Redis 承担缓存加速,Maven 管理项目构建;同时涉及微服务拆分、微信小程序 API 对接、OAuth2 安全认证、MySQL 数据表设计,以及 Docker 部署、CI/CD 发布流程和测试日志等工程化实践。通过阅读源码,可掌握从商品、订单、用户、支付等模块的数据建模,到前端交互、服务监控与日志处理的电商系统闭环;对想从单体迈向微服务、或需要小程序商城参考实现的人来说,是一份值得对照学习的实战素材。
1. 认识 linjiashop:一个 Java 商城小程序项目值得你花一个下午跑通
microapp-linjiashop-master 这串包名说白了就是:一套 Java 后端 + 一个商城小程序前端。名字里的 microapp 指向小程序端,linjiashop 是商城业务主体,master 是主干分支。你解压后拿到的是一套完整的电商主流程:商品、购物车、订单、支付回调、管理后台都齐全,不需要从零搭。适合两类人:一类是想快速给客户交付商城 Demo 的开发者,另一类是想拿真实 Java 项目做经验背书、顺便应付面试里 Spring Boot 和 MyBatis 问题的求职者。但我见过太多人卡在第一步:后端起来了小程序白屏,数据库脚本导入顺序错了,Redis 密码对不上——问题几乎都不在 Java 本身,而在工程怎么被正确喂饱。下面从解压到联调,把这条路完整走一遍。
2. 本地跑通 linjiashop 后端:环境准备、数据库初始化与启动验证
跑通后端是整个项目的地基,小程序前端只是把地基上的接口翻译成页面。下面按我上手一套陌生 Spring Boot 商城工程的习惯来:先解压看结构,再配环境,再导数据,最后启动验证。每一步都有对应命令,照着改完,后端就能起来。
2.1 先看压缩包结构:microapp 不是后端工程
先解开压缩包,用命令看一下根目录长什么样:
unzip microapp-linjiashop-master.zip cd microapp-linjiashop-master ls -l # 期望看到:后端 Maven 工程目录、小程序前端目录、文档与 SQL 目录解压出来的一般是两类东西:后端是一个 Maven 多模块工程,根目录有 pom.xml,聚合了 core、admin、app 等模块;microapp 才是小程序前端,里面没有 XML 和 Mapper,只有 js、wxml、json 这些前端文件。区分不清楚的人会把小程序目录当成 Java 工程丢进 IDEA,导入后全是红色报错,直接劝退。
另外有个常见问题:压缩包解压后出现双重目录,比如 microapp-linjiashop-master/microapp-linjiashop-master,这在 Mac 解压时经常发生。记得进入最里层再用 IDE 打开,否则 Maven 找不到根 pom,启动任务都创建不出来。
2.2 环境清单:JDK 用 8,MySQL 5.7/8.0,Redis 别省
这套技术栈是典型的 Spring Boot + MyBatis 组合,依赖的外部组件不多,但一个都不能少。我列一下推荐环境:
| 组件 | 推荐 | 说明 |
|---|---|---|
| JDK | 1.8 | 老牌商城工程大多跑在 Spring Boot 2.x,用 JDK 8 最稳。JDK 11/17 也能跑,但要额外检查插件版本,容易在反射和动态代理上报错。 |
| Maven | 3.6+ | 多模块构建必需。第一次下载依赖很慢,建议先配阿里云镜像。 |
| MySQL | 5.7 或 8.0 | 5.7 兼容性最好;8.0 需要在 JDBC URL 加时区参数。 |
| Redis | 5.x 以上 | 登录态、验证码、部分缓存走 Redis,只装 MySQL 不够。 |
很多教程默认你已经有了 Redis,新手经常漏掉。Redis 没起时后端可以正常启动,但登录接口一调就 500,排查起来特别玄学。启动前先确认一条命令:redis-cli ping 返回 PONG 再往下走。还有 java 环境配置这里多说一句:不要系统里装了多个 JDK 就放着不管,Spring Boot 的 Maven 插件对 JDK 版本很敏感,最好在 IDEA 的 Project Structure 里把 SDK 和 language level 都固定到 8。
提示:本机 Redis 如果没有密码,yaml 里的 password 就留空;如果设了密码,必须填一致,后面登录验证码接口才会走通。
2.3 初始化数据库:先建库再导脚本,字符集选 utf8mb4
压缩包内部一般会有 doc 或 sql 目录,里面放着建表脚本和初始数据。常见做法是脚本拆成多个文件,比如基础表、商品、订单、权限分开。如果直接按文件名一个一个 source,可能会因为外键依赖顺序而失败。我习惯手动建库,再按依赖顺序导入:
mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS linjiashop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p linjiashop < doc/sql/init.sqlutf8mb4 对应小程序里用户昵称可能带 emoji 的场景;用 utf8 遇到 emoji 会直接报 Incorrect string value 错误。数据库名要和后面 JDBC URL 保持一致。导入时如果报外键错误,把基础表放最前面重新导入。
数据导完,改后端 resources 下的 application-dev.yml。我一般至少改三段:数据源连接、Redis 地址、文件上传目录。数据源这块是启动失败的高发区:
spring: datasource: url: jdbc:mysql://localhost:3306/linjiashop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false username: root password: 123456 redis: host: localhost port: 6379 password: database: 0url 里的 serverTimezone=Asia/Shanghai 解决 MySQL 8 下查询时间比本地少 8 小时的问题;allowPublicKeyRetrieval=true 解决 MySQL 8 caching_sha2_password 用户在某些驱动版本下报 Public Key Retrieval is not allowed。Redis password 如果没设就留空,设了就填对。这一步最怕启动时报错后把整个 yaml 来回改,实际上只要确认这三种配置正确,后端启动就成功了一大半。
2.4 启动后端:一条 Maven 命令,两个验证点
启动命令不复杂,问题是首次启动要下载大量依赖。先用 Maven 跑 dev 环境:
mvn spring-boot:run -Dspring-boot.run.profiles=dev如果你用 IDEA,也可以在 Maven 面板里先 root 工程再选择 spring-boot:run。第一次启动慢是正常的,看到 Tomcat started on port(s): 8080 才算真正起来。这时候别急着开小程序,先用 curl 验证 HTTP 层:
curl -i http://localhost:8080/ curl -i http://localhost:8080/goods/page?pageNum=1&pageSize=1第一个命令看服务在不在,第二个看典型接口能否返回 JSON。如果 curl 返回 404,多半是工程配置了 context-path 或项目根路径,这个坑第 5 章会专门讲。还有一个容易翻车的地方:IDEA 编译时如果你手动改过进程堆大小,比如调到 8000m,启动时可能直接 OOM。这类报错和 Java 代码无关,把 VM options 里的 -Xmx 调回 512m 或 1g 再试。
3. Spring Boot + MyBatis 模块拆解:从一次商品查询看完整调用链
跑通只是开始。要做二次开发,你得先懂 linjiashop 这类 Spring Boot 商城项目的代码怎么组织。这个工程和我见过的大多数 Java 商城一样:Controller 接参数、Service 管业务、Mapper 管 SQL,三层各干各的。下面拿最常见的商品列表请求走一遍,你就知道改需求时应该动哪个文件。
3.1 Controller 只做参数翻译,真正的逻辑在 Service
一个典型的商品分页查询 Controller 长这样:
@RestController @RequestMapping("/goods") public class GoodsController { private final GoodsService goodsService; public GoodsController(GoodsService goodsService) { this.goodsService = goodsService; } @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer status) { return Result.ok(goodsService.page(pageNum, pageSize, status)); } }注意几个点。构造器注入比 @Autowired 字段注入更推荐,测试时好 mock,Spring 官方也认可这种写法。defaultValue 让分页参数可以省略,前端不传就默认第一页十条;status 设为 required = false,前端不传就查全部。Result.ok 是这类商城项目里常见的统一返回体,包一层 code/message/data,前端根据 code 判断业务成败,而不是看 HTTP 状态码。
有些旧工程里你会看到 controller 里直接 new ServiceImpl(),甚至把 SQL 写在 controller 里,这是最糟糕的写法,改了前端还要跟着改。接手这种代码的正确姿势是:先从 controller 找到入口,再进 service 看实现,最后到 mapper 看 SQL。完整调用链是:小程序请求 -> Nginx/开发者工具 -> Controller -> Service -> Mapper -> MySQL -> JSON 返回。联调时接口报错,先看日志打到哪一层,就能快速定位是前端拼错路径、后端逻辑挂了,还是 SQL 问题。
3.2 核心表与订单状态机:改商城先改哪儿
电商商城的核心关系,用一张表就能说清:
| 数据域 | 表 | 关键字段 |
|---|---|---|
| 用户 | sys_user | id, nickname, openid, phone |
| 商品 | goods | id, name, price, stock, status |
| 规格 | goods_sku | goods_id, spec_name, price, stock |
| 购物车 | cart | user_id, goods_id, sku_id, number |
| 订单 | orders | order_no, user_id, status, total_amount |
| 订单明细 | order_items | order_id, goods_id, sku_id, price |
这些表之间的关键不是外键,而是 sku_id 和 order_no 这类业务键。订单状态一般用 int 表示:0 待支付、1 待发货、2 待收货、3 已完成、4 已取消。二次开发里最常踩的坑是直接在数据库改状态绕过状态机,导致支付回调对不上账。正确做法是找到订单状态流转的 service 方法,比如 cancel、pay、confirm,统一走方法改状态。
库存扣减也是个隐性雷。下单时先查库存再 update,并发一高就超卖。要改就去 Service 层把扣库存 SQL 改成条件更新,类似update goods_sku set stock = stock - #{num} where id = #{id} and stock >= #{num},返回影响行数为 0 说明库存不足,直接回滚订单。购物车和库存为什么要分开看?购物车是用户维度的临时数据,库存是商品维度的强一致数据,两者更新频率完全不同,混在一个表里锁竞争会很严重。
3.3 Mapper 层两种风格:单表用通用 Mapper,复杂统计用 XML
linjiashop 这类工程的 Mapper 一般分两种:单表 CRUD 靠 MyBatis 或 MyBatis-Plus 内置方法,多表 JOIN 和复杂统计写在 XML 里。先看 XML 典型写法,这种写法决定了你改查询条件时改哪里:
<select id="selectGoodsPage" resultType="com.linjiashop.entity.Goods"> SELECT id, name, price, stock, status FROM goods <where> <if test="status != null"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY id DESC </select><where>标签会自动去掉多余的 AND,<if>让参数可缺省,#{}是预编译占位符而不是字符串拼接,这是网上那些用${}拼 SQL 导致注入漏洞的根本区别。如果工程集成了 MyBatis-Plus,单表查询不用写 XML,直接用 LambdaQueryWrapper 或 BaseMapper.selectPage,开发效率高很多。网上常说的根据 Java 实体类生成建表 SQL,也是基于 MyBatis-Plus 的表注解,比如 @TableName 指定表名、@TableField 指定列名,配合自动填充可以做 create_time/update_time。
改表时最容易忽略的是实体类、Mapper 接口、XML resultMap、数据库表四层同步。实体类加了字段,XML 里 SELECT 没带这个列,查出来就是 null;数据库改了字段名,resultType 映射对不上,报错很隐晦。所以我的习惯是:改表先改实体,再改 Mapper 接口,最后改 XML 和数据库,四层一起提交,不要分两次。写 SQL 也记住三条:所有 select 都带列名而不是 select *;统计类 SQL 先 explain 看索引;mapper 方法名和 XML 的 id 必须一致,否则启动时直接报 BindingException。
4. 小程序商城前端编译:把 microapp 跑进微信开发者工具
后端跑通后,前端 microapp 就是把这些接口变成可点可滑的小程序页面。导入、改地址、登录对接,三步走。这一章以微信开发者工具为基准,因为这类商城小程序基本都跑在微信生态里,你需要做的是让本地开发环境和后端接口正常对话。
4.1 导入项目:选 microapp 目录,先解决 appid
开发者工具里选导入项目,目录指向解压后的 microapp 文件夹,不要选整个 linjiashop 根目录。导入成功后看 project.config.json,这是小程序的工程配置:
{ "appid": "wx your appid", "projectname": "linjiashop", "compileType": "miniprogram", "setting": { "urlCheck": false } }appid 可以用自己注册的小程序,也可以临时用测试号。urlCheck 如果为 true,开发环境请求 http 接口会被域名校验拦住;本地调试把它设为 false,或者直接在开发者工具右上角详情-本地设置里勾选不校验合法域名。注意:测试号 appid 不能调微信支付,只能测购物流程里的非支付部分。
如果导入后编译报 app.json 未找到,说明目录选错;如果一堆文件编译不过,先看调试基础库版本,切到 2.x 以上一般能解决。小程序商城这种项目,前端目录里页面文件很多,但和接口相关的就是几个核心文件,不用每个页面都看。
4.2 接口地址:localhost 只属于电脑,真机预览要填局域网 IP
小程序跑在开发者工具里时,localhost 指向电脑,接口能通;一旦扫码真机预览,手机访问 localhost 就是手机自己。所以真机联调前必须把接口地址改成电脑的局域网 IP。先查 IP,再改前端配置:
# mac/linux ifconfig | grep inet # windows ipconfig// utils/config.js 或 api.js,以工程实际为准 module.exports = { baseUrl: 'http://192.168.1.100:8080' }改完记得电脑和手机连同一个 Wi-Fi。如果后端配了 context-path,比如 /api,baseUrl 要写成 http://192.168.1.100:8080/api,漏掉前缀就是第 5 章第一个坑。开发阶段不要用 https,微信开发者工具在 urlCheck=false 时是允许 http 的。这里还有一个容易被忽略的点:如果你用了跨域请求,后端还要允许来自小程序的 Origin,或者在小程序端用 wx.request 的 header 里加 Content-Type 让请求变成简单请求,否则预检请求会挡在前面。
注意:真机预览必须用局域网 IP,不能用 localhost;改完配置要重新编译,微信开发者工具不会热更新配置文件。
4.3 登录链路:wx.login 换 code,code 换 token,token 带走
商城小程序的登录链路几乎是标准答案:wx.login 拿到临时 code -> 请求后端登录接口 -> 后端拿 code 找微信换 openid -> 返回 token -> 小程序把 token 存本地,后续请求自动带上。代码看起来不多,但坑不少:
wx.login({ success(res) { wx.request({ url: BASE_URL + '/wx/login', method: 'POST', data: { code: res.code }, success(r) { if (r.data.code === 200) { wx.setStorageSync('token', r.data.data.token) } else { console.error('登录失败', r.data) } } }) } })r.data 的结构取决于后端 Result 包装:一般外层是 code/message/data,data 里才是 token。如果后端返回的字段名不是 token,而是 accessToken,这个判断就拿不到值。取不到 token 时先把完整响应打印出来看字段名,再改这里,不要盲目加逻辑。
后续每个请求都要带 token,一般在请求封装里统一处理:
header: { 'token': wx.getStorageSync('token') }header 的字段名必须和后端拦截器一致,常见有 token、Authorization、X-Token。对不上时后端可能报 401,也可能拦截器直接跳过,表现是接口能通但拿不到当前用户信息。另外微信官方规定 wx.login 只在需要登录时调用,不要在页面 onLoad 和 onShow 各刷一次 code,code 是一次性的,后端用过一次再传就会报 invalid code。
4.4 通常只需要动三个文件:config、request、app.js
小程序端二次开发,大多数时候不用碰页面 wxml。先分清几个文件的分工:project.config.json 管项目配置;utils/config.js 或 api.js 管接口地址;utils/request.js 管统一请求封装;app.js 管全局登录初始化。改接口地址只动 config;改登录逻辑只动 app.js 和 request.js;页面 UI 才去动 pages 下的 wxml、wxss。
一个通用经验:不要在页面里直接 wx.request,统一走 request.js 封装。原因很简单,登录 token 注入、错误弹窗、code 过期重新登录,这些都要集中处理。手头这个工程如果已经把 wx.request 封装好了,就顺着封装改,不要自己另起一套,否则第 5 章的坑会成倍增加。前端这块最容易失控的就是每个页面各写各的请求逻辑,最后后端改个字段名,要翻几十个文件。
5. 避坑清单:前后端联调与二次开发里最常翻车的 5 个问题
跑通 demo 和稳定运行之间,隔着大量让人上火的细节。下面五条是我在 Spring Boot 商城项目里见过最多、也亲自踩过的坑,每条按现象、原因、解决三步写。这五条查一遍,能省下大半天排查时间。
5.1 后端起来了,小程序却全部 404:先看 context-path
现象:curl 后端接口正常,小程序请求全 404,网络面板里的路径少了前缀。
原因:后端 application.yml 配了 server.servlet.context-path,所有接口都挂在前缀下;小程序 baseUrl 没带这个前缀,请求发到根路径,自然找不到。
解决:先看后端配置里有没有 context-path,再改前端 baseUrl。以常见的 /api 前缀为例:
curl -i http://127.0.0.1:8080/api/goods/page?pageNum=1&pageSize=1// 前端 config.js baseUrl: 'http://192.168.1.100:8080/api'改完前端后重新编译小程序,清掉缓存再试。404 的排查有个笨办法:把 network 面板里的完整 URL 复制到浏览器地址栏访问,浏览器能打开说明后端没问题,问题只在前端拼接。这一步能把 404 的定位时间压缩一半。如果两边都带前缀了还是 404,把后端日志调到 DEBUG,看 Spring MVC 的 Mapping 信息。
5.2 小程序登录失败:invalid code 是一场三方对账
现象:wx.login 返回的 code 提交后端,后端日志报 invalid code 或 code been used,前端一直停在未登录状态。
原因:code 是微信下发的临时凭证,5 分钟有效且只能用一次。最常见的原因是测试号 appid/secret 没配对,小程序端和后端各用了不同的 appid。也有后端拿 code 换 openid 时请求微信接口超时,code 被消耗了又没拿到 openid。
解决:先确认后端的微信配置:
wx: appid: 你的小程序appid secret: 对应secretappid 和 secret 必须和 project.config.json 里的一致,测试号有自己的 appid/secret。另外把代码检查一遍,是不是同一个 code 被连续调了两次后端,页面 onLoad 和 onShow 各触发一次登录就会出现这个情况。看日志时重点关注微信接口返回的 errcode,只有 0 才是成功;非 0 就直接按错误码查含义,别猜。
5.3 MySQL 导入 SQL 报错:MySQL 8 的 sql_mode 和时区
现象:导入 SQL 报 ERROR 1067 或 ERROR 1292,提示 Invalid default value for create_time;启动后查询时间少 8 小时。
原因:MySQL 8 默认 sql_mode 包含 NO_ZERO_DATE 和 ONLY_FULL_GROUP_BY,老商城脚本里 timestamp 默认值写成 0000-00-00 00:00:00 会被直接拒绝。连接串没带 serverTimezone 时,驱动用本地时区,和数据库时区错位。
解决:导入前临时放宽 sql_mode:
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; SET NAMES utf8mb4; SOURCE /path/to/init.sql;JDBC URL 里加 serverTimezone=Asia/Shanghai;MySQL 8 用户再用 caching_sha2_password 的话,补 allowPublicKeyRetrieval=true。注意 SET GLOBAL 只影响新连接,执行完 sql_mode 后要重连数据库再 source。
5.4 图片上传成功但前端裂图:本地上传目录是纸糊的
现象:管理后台提示上传成功,商品图片在小程序里全部裂开;或者开发环境正常、部署后失效。
原因:上传接口把文件写到了相对路径,目录不存在时组件静默失败;返回的图片地址是相对路径,前端拼 baseUrl 拼错;部署后 Nginx 没有把上传目录映射到静态资源。
解决:先在浏览器直接访问图片 URL,能快速分清是路径错还是文件没落盘。本地开发把上传目录改成绝对路径:
file: upload-path: /data/linjiashop/upload同时确认目录存在且有写权限:mkdir -p /data/linjiashop/upload && chmod 755。前端显示图片时,用 BASE_URL 加相对路径拼接,不要在数据库里存完整 URL,否则换域名所有图片都失效。部署后裂图,还要看 Nginx 配置里有没有把 upload 目录映射出来,常见的 alias 配置写错根目录就会出现这个症状。
5.5 登录闪退 500:Redis 没对上是隐性炸弹
现象:后端能启动,登录或获取验证码接口一调就 500,日志里有 RedisConnectionFailureException 或 SerializationException。
原因:本机 Redis 没启动;application-dev.yml 里 Redis password 和实际不一致;Redis 序列化器用的是 JDK 默认,而工程里缓存对象没有实现 Serializable,存取时直接抛异常。
解决:先确认 Redis 活着:
redis-cli ping # 应该返回 PONG再核对配置。没设密码就把 yaml 里 password 留空;设了密码必须填对,不能赌本机不需要密码。序列化异常时,把 RedisTemplate 的 key/value 序列化器改成 StringRedisSerializer 或 Jackson 序列化器,或者在实体上实现 Serializable。遇到无法解释的偶发 500,先翻堆栈看是不是和 Redis 相关,很多时候缓存没配对就是这种玄学问题。Redis 一挂,最直接的体验就是用户刚登录又掉线,因为 token 查不到了。
6. 从跑通到上线:三个验证技巧与配置习惯
6.1 先 curl 后小程序:接口验证别靠肉眼
联调时我习惯先 curl 后端,再碰小程序。绕开前端,直接看后端返回的 JSON 结构有没有变:
curl -i "http://127.0.0.1:8080/api/goods/page?pageNum=1&pageSize=1"看状态码、响应头、返回体。字段名变化、类型变化、分页结构对不对,命令行里一眼就能看出来。小程序端报错时,先用同一个地址 curl,如果正常,问题就出在小程序侧,不用反复改后端。这个方法对排查 404、500、跨域都有效,能省下大量来回切换工具的时间。
6.2 上线前把配置拆成 prod,密码别进仓库
本地跑通用的是 dev 配置。上线前我一般会加一个 application-prod.yml,把数据源密码改从环境变量读,SQL 日志关掉,上传路径改成服务器绝对路径,Redis 指向生产实例。开发配置可以提交,但密码和密钥用占位符,不写真实值。打包时用 mvn clean package -DskipTests,启动时用 java -jar 加 --spring.profiles.active=prod 切换。我还会在发布前打一个 git tag,出问题快速回退,相当于给自己留后悔药。
6.3 二次开发最小改动:协议不变,逻辑加码
接手这类商城项目,最大的纠结点是前端接口协议。我的习惯是:小程序端的接口路径和返回结构能不动就不动,加需求优先在 Service 层加方法,加字段优先扩展表或加字段,不要在 Controller 里顺手改返回结构。前端和后端各改一套对齐逻辑时,维护成本会翻倍。真要做大调整,先和后端约定好接口版本,再一次性同步改小程序端。
这套一遍跑通、验证、再上线的流程,是我做过几个商城项目后慢慢定下来的。第一次跑这种项目,不翻几次车反而不正常;翻一次,记下原因,下次就稳了。希望帮到你。
本文还有配套的精品资源,点击获取