
简介这是一套基于 Java 的轻量级单商户商城系统源码主项目 microapp-linjiashop-master 面向中小型商家、个人卖家和 Java 学习者可用来搭建微信小程序商城也能作为毕业设计或电商实战项目的参考。尤其适合有 Java 基础、想转入电商方向的开发者。代码采用 Spring Boot、MyBatis、Redis、Thymeleaf 等主流技术覆盖商品管理、订单处理、用户中心、支付模块等核心业务同时体现微服务拆分、小程序接口对接、OAuth2/HTTPS 安全认证、前端 Vue.js 交互以及 Docker 部署与自动化测试思路适合想了解电商全栈架构的开发者细读。资源压缩包为 ZIP 格式约 13.2MB上游暂未解析出文件总数与类型明细预计包含 Java 后端工程、配置文件和页面相关源码。截至目前该资源已有 166 人学习浏览读者可以借此梳理从数据库表设计、接口开发到前后端联调、部署上线的完整体验。1. 一套能跑通商品到成交的 Java 商城源码linjiashop 的定位与打开方式如果你是第一次打开 microapp-linjiashop-master 这套 Java 商城源码大概率会被目录里的 admin、api、weixin 几个模块绕晕甚至因为名字里的 microapp 前缀就以为它是微服务架构——这恰恰是很多人卡在第一步的原因。实际上这是一套单商户商城系统后端用 Spring Boot MyBatis 处理商品、订单、用户与支付回调管理后台用 Thymeleaf 渲染页面缓存依赖 Redis接口层同时服务管理端和小程序端。它能解决的是从零搭建电商后端的样板问题你不需要从空项目开始搭框架直接把这套代码跑起来就能看到商品上架、下单扣库存、订单状态机、微信登录的完整实现路径。适合刚接触 Java 电商开发、想找一个能运行的项目来改的人也适合面试前快速回顾订单状态和库存扣减这些高频考点。2. 技术栈与模块边界从 pom.xml 看懂这套 Java 商城2.1 从 pom.xml 确认版本搭配拿到源码第一步打开根目录的 pom.xml确认 Spring Boot 版本和 MyBatis starter 版本是否兼容。这套项目最核心的依赖组合是 Spring Boot MyBatis Redis Thymeleaf。常见的坑是 Spring Boot 2.3.x 配了最新版的 mybatis-spring-boot-starter启动时报 Mapper 代理创建失败查半天发现是版本不匹配。我一般先看 parent 里锁定的 spring-boot-dependencies再决定要不要手动覆盖 starter 版本。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.4.RELEASE/version relativePath/ /parent dependencies dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.1.4/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency /dependenciesmybatis-spring-boot-starter 负责数据访问层的自动装配starter-data-redis 提供 RedisTemplate 和 Spring Cache 抽象实现thymeleaf 渲染管理后台页面。这个组合决定了 linjiashop 的运行方式一个 Spring Boot 进程同时承载 HTML 后台和 JSON 接口而不是多个独立服务。版本上有两个容易踩的点。第一mybatis starter 2.1.x 默认扫描 Mapper 注解的包路径如果你的 mapper 被放在 common 这类子模块里需要额外配置 MapperScan 指定基础包否则启动时会出现 mapper bean 找不到的报错。第二Spring Boot 2.3 之后 Redis 默认客户端是 Lettuce如果你原来的代码里用的是 Jedis 连接池配置会直接不生效。判断办法是看启动日志里有没有 “Using Lettuce” 字样。这类问题属于“配置看着没问题但服务起不来”的典型报错往往藏在嵌套堆栈里需要耐心点开。另外如果依赖冲突导致 Lettuce 和 Jedis 同时出现在 classpath 里Redis 连接池初始化的行为会变得很怪。我一般用 mvn dependency:tree -Dincludesredis.clients:jedis 检查是否有意外的 Jedis 传递依赖有就直接在 pom 里排除。这个检查花不了两分钟能省掉后面反复重启试错的时间。2.2 模块目录不是微服务而是单体内部分层microapp 前缀容易让人误判。这套代码并不是把订单、用户、商品拆成独立部署的微服务而是同一个 Spring Boot 工程里的多模块分层各模块只是职责边界不同。我一般把这类项目叫“单体分层商城”对学习来说它比微服务更好入手因为没有服务发现和分布式事务的额外负担一次只用看一条调用链。模块职责通常会这样划分core 或 common 放实体、Mapper 和公共工具类admin 是后台管理模块提供商家管理页面和接口api 是面向小程序和 App 的接口模块商品列表、购物车、下单、支付都走这里weixin 单独处理微信相关能力比如登录 code2session、支付回调的报文解析。这样一个请求从 controller 到 service 到 mapper 再到 MySQL 的链路在每个模块里是完整的调试时只需要打断点不需要跨服务看链路。模块主要职责常见路径前缀core/common实体、Mapper、工具类被其他模块依赖admin后台商品管理、订单管理/admin/**api小程序端接口/api/**weixin微信登录与支付回调/wx/**这种结构对 Java 基础阶段的人来说非常友好。你不需要理解注册中心、网关、配置中心那一套直接把 Maven 依赖关系理清楚就知道代码该往哪里放。比如我要加一个优惠券模块实体和 mapper 放 core后台管理页面放 admin小程序领券接口放 api这个边界在动手之前就明确了。2.3 配置文件的读取顺序与 Redis 依赖Spring Boot 的配置文件优先级里application.yml 是底真正部署时会用环境变量或外部 config 目录覆盖。源码里一般有两个必改项数据源连接串和 Redis 连接地址。下面这段是典型的 application.yml 局部内容。server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/linjiashop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: 127.0.0.1 port: 6379 database: 0 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueurl 里的 characterEncoding 和 serverTimezone 一定要保留。MySQL 8 要求 serverTimezone不然驱动初始化直接报时区错误MyBatis 的 map-underscore-to-camel-case 打开之后数据库的 create_time 才能自动转成实体里的 createTime 字段否则所有带下划线的字段映射出来全是 null这种问题很容易被误判成 SQL 写错。Redis 的 database 默认 0如果本机 Redis 被其他项目占用可以改成 1 或 2避免 key 冲突。需要注意的是这套代码里的验证码、Token、热点数据几乎都依赖 Redis所以数据库和 Redis 任何一个没准备好启动阶段就会失败。建议先把 Redis 服务跑起来再动 Maven 启动命令否则看到的报错里一半以上都是从 Redis 连接引发的连锁异常。3. 本地跑通全流程建库、导数据、启动与登录3.1 找到 SQL 脚本并导入源码里一般会有 sql 或 doc 目录放数据库脚本常见是一个 schema.sql 负责建库建表另一个 data.sql 插入初始商家、分类和商品数据。拿到后先确认本机 MySQL 版本5.7 和 8.0 对 utf8mb4 的默认配置不一样但这类商城脚本通常都兼容。我一般用命令行导入而不是在图形化工具里手动跑因为命令行能保留脚本执行顺序报错位置也明确。mysql -uroot -p123456 -e CREATE DATABASE IF NOT EXISTS linjiashop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p123456 linjiashop sql/schema.sql mysql -uroot -p123456 linjiashop sql/data.sql第一句创建库指定 utf8mb4 字符集避免后续插入中文乱码。第二句和第三句按顺序导入表结构和初始数据。如果 data.sql 里有外键依赖导入顺序不能反先表结构后数据是底线。执行完后可以用 mysql linjiashop -e show tables; 验证一下商品表、订单表、用户表、支付记录表都应该在里面。这里有一个“后悔药”性质的建议建表语句里如果没写 DEFAULT CHARSETutf8mb4建议手工补上改库字符集比改代码容易得多。乱码问题一旦发生清理数据重新导入的成本不低。3.2 修改配置并启动后端导入完数据后改 application.yml 里的数据库密码和 Redis 地址确认本机 6379 端口能连上然后开始启动。第一次跑优先用 Maven 直接启动 admin 模块下的启动类因为热加载和日志输出都在前台报错一目了然。命令行方式则用 mvn spring-boot:run 指定模块。mvn spring-boot:run -pl admin -am如果工程聚合了多个模块-pl admin -am 的意思是只构建 admin 模块及其依赖模块避免把无关子模块全部构建一遍。admin 是管理后台入口启动端口以配置为准通常是 8080。看到 Started AdminApplication 这样的日志后说明 Web 容器起来了但这不代表所有初始化都完成。启动成功的标志不只是 “Started”还要看有没有跟 Redis 建立起实际连接。如果 Redis 里没有出现预期的缓存 key说明登录模块的初始化可能没走完最直接的表现是验证码接口第一次调用超时或报错。先不要急着看页面先用 curl 调一个不需要登录的接口验证链路具体方法看下一节。3.3 验证管理后台登录浏览器访问 http://127.0.0.1:8080/admin看到登录页说明 Thymeleaf 模板渲染成功。初始账号密码一般写在 data.sql 或 README 里常见是 admin 加一串简单密码。登录时如果验证码不显示优先检查 Redis 连接因为验证码是先存进 Redis 再返回图片的。不用浏览器也可以验证接口层。api 模块通常会有不需要登录就能访问的商品列表接口比如 GET /api/goods用 curl 加 JSON 请求头调用curl -H Content-Type: application/json http://127.0.0.1:8080/api/goods如果返回 200 和商品 JSON说明 Spring 容器、MyBatis、MySQL 链路是通的。如果这一层都不通问题大概率在数据源配置而不是页面模板。先确定接口通再进后台登录排查范围会小很多。登录成功后进入后台首页左侧菜单一般包含商品管理、分类管理、订单管理和会员管理。此时整个系统的调用链已经跑通浏览器请求、Spring Boot 控制器、Service 业务层、MyBatis Mapper、MySQL 数据库、Redis 缓存。到这一步本地“能跑”的目标就完成了接下来才有资格谈改造。3.4 小程序接口的启动差异管理后台跑起来后小程序端的启动其实复用同一个进程没有单独的端口。api 模块的接口统一挂在 /api 前缀下微信开发者工具里配置的合法域名要先填 http://127.0.0.1:8080。开发期有个常用做法在微信开发者工具里勾选“不校验合法域名”这样可以直接用本地 IP不用等域名备案。但要注意小程序端和后台管理的会话机制不一样。后台走登录态加 Cookie小程序端走 token 请求头。如果你改动代码新增了一个 controller记得在鉴权拦截器里加上对应的放行规则否则请求会直接返回 401。很多人第一次联调时花半小时排查跨域其实拦住的不是跨域是拦截器白名单没配好。4. 核心业务拆解商品表、下单流程与支付回调4.1 商品与 SKU 的表结构电商代码里最核心的表结构组合是 goods 主表和 goods_sku 子表。单商户商城场景下商品是 SPU规格如颜色、尺码、版本是 SKU价格和库存挂在 SKU 上而不是商品上。下面是一个简化后的建表参考。CREATE TABLE goods ( id bigint NOT NULL AUTO_INCREMENT, name varchar(128) NOT NULL, main_image varchar(255) DEFAULT NULL, category_id bigint DEFAULT NULL, status tinyint DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods_sku ( id bigint NOT NULL AUTO_INCREMENT, goods_id bigint NOT NULL, spec varchar(64) DEFAULT NULL, price decimal(10,2) NOT NULL, stock int NOT NULL DEFAULT 0, image varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_goods_id (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;数据访问层用 MyBatis 实现时一次商品详情查询通常要分两步先查 goods 主表再查 goods_sku 列表。常见做法是在 mapper 里写一个关联查询用 resultMap 的 collection 标签把 SKU 列表嵌套进去这样前端只需要调用一次接口就能拿到完整商品信息不用前端自己拼多个请求。status 字段控制上下架列表查询时记得过滤 status1否则会出现“商品列表里看到了已下架商品”这种低级 bug。price 字段用 decimal(10,2) 而不是 float商城金额用浮点数保存会导致结算出现累积误差这是一条 Java 电商开发里的老规矩。4.2 创建订单先锁库存还是先写订单下单接口是最能体现业务功底的部分。常见做法是开启事务先查 SKU 并加行锁再校验库存扣减成功后才插入订单记录。下面是一段贴近这类商城写法的代码参考。Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, Long skuId, Integer count) { GoodsSku sku skuMapper.selectByIdForUpdate(skuId); if (sku null || sku.getStock() count) { throw new BizException(库存不足); } skuMapper.decreaseStock(skuId, count); Order order new Order(); order.setUserId(userId); order.setSkuId(skuId); order.setCount(count); order.setAmount(sku.getPrice().multiply(new BigDecimal(count))); order.setState(0); // 0待支付 1已支付 2已发货 3已完成 4已关闭 order.setCreateTime(new Date()); orderMapper.insert(order); return order; }逻辑说明selectByIdForUpdate 使用数据库的行级排他锁保证同一时刻只有一个线程能读到同一 SKU 的库存decreaseStock 是 update 语句把 stock 减掉 count并在 SQL 里加 stock count 的条件双保险防超卖。订单金额计算用 BigDecimal避免 double 的浮点误差。对应的 MyBatis XML 里锁查询的写法如下select idselectByIdForUpdate resultTypecom.linjiashop.entity.GoodsSku select id, goods_id, spec, price, stock from goods_sku where id #{id} for update /select这条 SQL 里的 for update 在事务提交前会一直持有行锁。如果一个请求已经把某个 SKU 锁住了另一个请求的 selectByIdForUpdate 会等锁这在并发下单时是正常的压测时看到等待时间变长不要误判成死锁。还有一层要注意事务注解加在 public 方法上且必须在这个类内部被调用才生效如果从其他类绕过 service 直接调 mapper锁和扣库存就散落了。有一种典型的翻车场景是新建订单后用户没支付库存已经被扣了这时候需要另外一个定时关单任务把库存加回来常见做法是自己加一个定时任务去超时关单再配合支付结果做补偿。4.3 支付回调与状态机更新支付回调是商城系统里最容易踩坑的接口。微信支付成功后服务器会回调我们配置的 notify_url回调报文是 XML里面包含 out_trade_no、transaction_id、total_fee 和验签信息。常见做法是在回调接口里做三件事验签、核对订单金额、幂等更新订单状态。PostMapping(/wx/pay/notify) public String payNotify(RequestBody String xml) { MapString, String result WxPayUtil.parseXml(xml); String orderNo result.get(out_trade_no); String fee result.get(total_fee); Order order orderMapper.selectByOrderNo(orderNo); if (order null || order.getState() ! 0) { return WxPayUtil.responseXml(SUCCESS); } if (!String.valueOf(order.getAmount().multiply(new BigDecimal(100)).intValue()).equals(fee)) { return WxPayUtil.responseXml(FAIL); } order.setState(1); order.setPayTime(new Date()); orderMapper.updateState(order); return WxPayUtil.responseXml(SUCCESS); }逻辑说明第一步判断订单为空或状态已经不是待支付直接返回 SUCCESS因为重复回调不能报错第二步把订单金额换算成分与微信回调金额比对防止“支付了 1 元但订单是 100 元”的篡改场景第三步更新订单状态并返回 SUCCESS 给微信服务器。这里有个新手常犯的错回调成功时返回的报文必须严格是字符串 SUCCESS否则微信会按失败继续重试造成回调日志刷屏。另一个细节是回调更新订单状态最好在数据库层面也加条件比如 update order set state1 where state0 and order_noxxx避免两个线程同时处理同一个订单。这类源码读下来基本就能理解整个商城的状态机是怎么转的待支付、已支付、已发货、已完成、已关闭每个状态变更都有一个明确的触发入口。4.4 用户登录与小程序的 token 鉴权后台管理端登录用 Session 配合 Thymeleaf 模板Session 存 Redis实现跨实例共享。小程序端登录流程则是 wx.login 拿到 code后端拿 code 去微信服务器换 openid 和 session_key再为本端生成一个自定义 token 返回给小程序之后所有请求头带 token。这段流程在 Java 面试题里也是常客。用参考代码表示PostMapping(/wx/login) public MapString, Object wxLogin(RequestBody WxLoginReq req) { String openid wxService.code2Session(req.getCode()); // 调用微信接口换取 openid String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(wx:token: token, openid, 7, TimeUnit.DAYS); MapString, Object result new HashMap(); result.put(token, token); return result; }逻辑说明code2Session 是后端用 code 换 openid 的微信接口token 是本地自己生成的存 Redis 并设置过期时间。后续接口拦截器用 token 从 Redis 查 openid就知道当前用户是谁。注意如果项目没配 appid 和 secret这个接口会调不通联调时优先检查这两项。5. 常见问题排查启动失败、验证码不显示、接口 4045.1 Redis 连不上服务一直重启或登录接口报错现象Spring Boot 启动过程中抛 RedisConnectionFailureException或者管理后台登录时验证码接口直接 500日志里出现 Connection refused: 127.0.0.1:6379。原因本机没有安装 Redis或者 Redis 配置了密码而 application.yml 里没填 password也可能是 Redis 端口被改过。解决先跑 redis-cli ping确认是否返回 PONG。返回异常就检查 Redis 进程和端口有密码就在配置里加 spring.redis.password。启动失败之后不要急着重试先把 Error creating bean with name redisTemplate 整段日志看完确认连不上再动手。这也是一种“黑匣子”式报错不点开堆栈很容易觉得是代码写错其实是环境问题。5.2 数据库中文乱码商品名称全是问号现象导入 data.sql 后数据库里中文正常但后台页面显示乱码或者反过来页面正常但数据库查询出来是 ?集中在第一次建库时出现。原因建库语句没有指定 utf8mb4数据库默认字符集是 latin1连接串也没带 characterEncodingutf8。解决在 MySQL 里执行 ALTER DATABASE linjiashop CHARACTER SET utf8mb4;并把 application.yml 的 jdbc 连接串补上 useUnicodetruecharacterEncodingutf8。已经乱码的表数据需要删掉重新导入修改字符集不会自动修复存量数据。5.3 验证码不显示图片裂了或永远校验失败现象后台登录页能打开但验证码区域显示红叉或者验证码明明输对了登录还是提示验证码错误。原因验证码问题通常有两类。一类是生成验证码的字体缺失Linux 服务器上没有对应字体图片渲染失败另一类是 Redis 里验证码 key 过期时间太短输入验证码时已经失效。解决先看后台日志有没有 AWTError 字样有就是字体库问题安装 fontconfig 依赖没有的话把验证码过期时间从 60 秒调大到 180 秒同时确认 Redis 的 key 前缀和校验逻辑一致。本地 Windows 上第一类问题很少见部署到服务器时才高发。5.4 接口 404 或 401模块扫描和拦截器放行现象管理后台正常但小程序端请求 /api/goods 返回 404或者接口明明存在却返回 401。原因404 大概率是 controller 的 RequestMapping 路径写错或者包扫描起点没覆盖 api 模块401 则是鉴权拦截器拦住了未放行的路径常见于新增接口时忘了加白名单。解决404 先翻源码确认 mapping 路径看控制器类上的 RequestMapping(/api/goods) 是否真的存在401 找拦截器注册配置把新接口添加到排除路径里。这里有一条经验新增 controller 时先加白名单再写业务避免联调时反复被拦。5.5 登录态失效token 明明在 Redis 里却鉴权失败现象小程序端登录成功后过一段时间再请求接口返回 401但查 Redis 发现 token 还存在。原因多数情况是 Redis key 的前缀不一致。登录时用的前缀是 wx:token:拦截器校验时取的前缀是 wx:login:两处字符差一点就匹配不上。这种问题日志里可能没有任何报错属于最磨人的一种。解决全局搜索 token 前缀字符串把设置和读取统一到一个常量类里。另一个原因是 Jackson 反序列化 Redis 里的对象时类型不兼容通常发生在把用户对象序列化成 JSON、反序列化时又要求转成指定对象解决方法是给 RedisTemplate 显式配置 GenericJackson2JsonRedisSerializer。6. 进阶用法缓存预热与上线前的最小改造6.1 缓存预热让第一批请求不再慢这套代码跑通之后第一个可以动手的改造是商品详情的缓存预热。第一次访问某个商品详情时Redis 里没有缓存请求会穿透到 MySQL虽然单次查询不慢但首页同时加载几十个商品会造成明显的响应毛刺。常见做法是在应用启动完成后用 ApplicationRunner 把热门前 N 个商品的详情写入 Rediskey 设计为 goods:detail:{id}过期时间设 30 分钟接口读取时先查缓存。Component public class GoodsCacheWarmer implements ApplicationRunner { Autowired private GoodsService goodsService; Autowired private StringRedisTemplate redisTemplate; Override public void run(ApplicationArguments args) { ListLong hotIds goodsService.getHotGoodsIds(20); for (Long id : hotIds) { String detail goodsService.getDetailJson(id); redisTemplate.opsForValue().set(goods:detail: id, detail, 30, TimeUnit.MINUTES); } } }这个类的执行时机是 Spring 容器初始化完成之后不会影响启动流程即使预热失败也只是缓存为空接口照样能走数据库兜底。预热逻辑放在代码里而不是手动执行 SQL 或脚本好处是部署新版本时不需要人工介入服务起来自动完成。6.2 用 JMeter 验证下单接口的并发表现改造完缓存用 JMeter 做一次基础压测能直观看到这套代码的边界。我一般会建一个线程组模拟 50 个并发用户同时调用商品列表接口和下单接口。商品列表接口关注的是 TPS 和响应时间下单接口更关键的是有没有超卖或锁等待超时。压测前把日志级别调成 WARN否则并发请求产生的 INFO 日志会把磁盘写满压测结果也会被日志写入拖慢。下单接口压测时观察数据库连接池和行锁等待时间如果错误率集中在 deadlock 或 Lock wait timeout exceeded就要检查是否正确使用了 for update以及是否有慢 SQL 把事务拉长。6.3 上线前改掉这些默认值本地能跑和真正上线之间还有一段距离。数据库密码不能留在 application.yml 里常见做法是用环境变量注入比如 spring.datasource.password${MYSQL_PASSWORD}Redis 要开密码验证不能裸奔在局域网管理后台的初始账号密码必须强制第一次登录修改。另外如果项目里有 Swagger 或调试接口生产环境要关掉避免接口文档暴露给公网。从那以后我每拆一套商城源码都会先确认 Redis 依赖、数据库脚本、模块扫描范围这三件事再动手顺序一旦颠倒排查成本会翻倍。希望帮到你。本文还有配套的精品资源点击获取