ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot单商户商城小程序源码解析:从部署到二次开发

Spring Boot单商户商城小程序源码解析:从部署到二次开发

简介:这是基于 Java 技术栈构建的轻量级单商户商城系统源代码,面向希望快速搭建在线销售平台的中小型商家,以及想深入理解电商后端开发流程的 Java 学习者。项目整合 Spring Boot、MyBatis、Redis、Maven 等主流工具,并以后端业务为核心,涵盖商品、订单、用户、支付等模块的设计思路,同时预留微信小程序端接口,便于扩展移动购物场景。资源以 ZIP 压缩包形式提供,大小约 13.2MB,内含项目源码与相关配置文件,可结合自身环境导入运行。学习切入点包括:数据库表结构设计、缓存策略运用、模板页面渲染、微服务拆分意识,以及 Docker 部署、日志监控、Git 管理等工程实践。目前已有 166 人学习该资源,适合具备一定 Java 基础、希望获得完整商城项目范例的开发者参考与二次开发。

1. 轻量级单商户商城:一个能同时撑起后台管理和小程序接口的Java商城源码

最近在帮一个做实体店的小商家评估商城方案,对方的需求很直接:商品上架、订单处理、会员管理要能从后台操作,客户在手机里打开小程序就能下单购买。市面上的电商开源项目不是太重就是太玩具,多的做成多商户平台,少的只剩CRUD。这个microapp-linjiashop-master_java_商城小程序源码正好落在中间——基于Spring Boot的单商户商城,管理后台用Thymeleaf渲染开箱即用,接口层同时服务H5和小程序两端。项目体量不大,不依赖微服务和分布式组件,一台小主机就能跑。适合想在Java商城小程序项目上快速起步、又需要完整业务链路做二次开发的开发者。

2. 技术选型与工程结构:从Spring Boot到小程序API的分层设计

2.1 技术栈全景:为什么选单体模块化而不是微服务

看到microapp这个前缀,很多人都先入为主地认为这是一个微服务项目,拆开源码会发现它是模块化的单体应用。业务代码按功能分包,商品模块、订单模块、用户模块各自独立成一个package,但最终打成一个可执行jar。对中小体量的单商户商城来说,这种结构比微服务更合身:没有服务注册,没有配置中心,没有网关,本地启动一个Spring Boot应用就能完成全部接口联调。微服务带来的治理能力在这里用不上,反而徒增部署成本。

核心框架是Spring Boot,它将RESTful接口的提供和管理后台页面的渲染放在同一个工程里。对C端,controller暴露JSON接口给小程序端调用;对运营端,用Thymeleaf模板直接渲染后台页面。数据访问层选MyBatis而非JPA,原因在于商城查询里有大量多表关联,比如订单要带出商品快照、商品列表要按分类和状态组合过滤,这些场景手写SQL更可控,也方便后续针对慢查询做单独的SQL优化。

Redis在这个项目里不是可有可无的装饰品,它承担了两类职责:热点数据和登录会话。商品列表、首页banner这些读多写少的数据放缓存,能明显降低数据库压力;用户登录后签发的Token存在Redis里并设置过期时间,实现会话管理和自动续期。安全性方面,用户密码在入库前经过BCrypt哈希,不会以明文落库,接口层再配一层登录拦截器,未带Token的写操作请求直接拒绝。Maven负责依赖和构建,一条mvn spring-boot:run就能把接口工程拉起来,不需要额外装Tomcat,这也是Spring Boot项目跑起来最省心的点。

2.2 工程目录拆解:管理端、接口端、公共模块的边界

项目结构初次看会有点晕,理顺之后其实是一套很清晰的前后端分离布局。先看整体目录:

microapp-linjiashop-master/ ├── admin-web # 管理后台前端工程(Vue) ├── api-web # C端接口工程(Java + Spring Boot) │ ├── src/main/java │ │ └── com.linjiashop.api │ │ ├── controller # 接口层,小程序端调用的入口 │ │ ├── service # 业务层,商品/订单/用户核心逻辑 │ │ ├── mapper # MyBatis数据访问接口 │ │ ├── model # 实体类与DTO │ │ └── config # Redis、拦截器、跨域等配置 │ └── src/main/resources │ ├── mapper # MyBatis XML文件 │ └── application.yml ├── admin-api # 管理后台接口工程 ├── common # 公共模块:工具类、统一返回结构 ├── docs # 数据库SQL脚本与接口文档 └── pom.xml # Maven父工程

这里容易被忽略的点是admin-api和api-web两个工程是分开的。后台接口给运营人员用,C端接口给普通用户用,两者共用common模块里的工具类和统一返回体,但权限体系独立。我拿到这类项目,第一件事就是确认controller层是否足够薄。如果controller里堆了大量业务代码,改造起来就会很痛苦。规范的做法是业务逻辑沉淀在service层,controller只做参数接收和结果包装。

这样一来,小程序端加一个新接口,或者管理后台加一个新页面,都是只加Controller复用Service。项目里商品、订单、用户的核心逻辑都集中在service层,后面做秒杀、拼团这类营销需求时,扩展点都落在service上,不需要动底层mapper,这是这个项目结构上最值钱的地方。

2.3 数据库设计:商品、订单、用户三张核心表怎么互相引用

单商户商城的库表设计不算复杂,但几个核心表之间的关联方式直接决定业务好不好扩展。最核心的有三张表:商品表、订单表、用户表。商品表负责商品信息与状态,订单表记录交易流水,用户表存账号和会员信息,订单明细表作为关联表把订单和商品串起来。

表名核心字段作用
商品表id、name、price、stock、category_id、status商品基础信息与上下架状态
订单表id、order_sn、user_id、total_price、status、pay_type订单主表,记录交易状态
订单明细表id、order_id、goods_id、goods_name、price、num下单时的商品快照
用户表id、username、password、nickname、level账号与会员等级

订单明细表里冗余了goods_name和price两个字段,这是电商设计里最常见的细节。下单时如果只存goods_id,后续商品改名或调价,历史订单显示就会跟着变,这是很多新手商城项目翻车的根源。做下单快照是这个项目的标准做法,也是我看一个商城源码是否合格最先看的地方。

再往下,商品分类表、购物车表、支付流水表、收货地址表都是围绕这三张核心表展开的附属表。理解了主链路是"用户下单、商品扣库存、订单生成、支付流转",后面读mapper的SQL就不会迷路。订单表里的status字段从待支付、已支付、已发货到已完成是一套状态机,每次状态变更同时更新支付流水表,这条链路理顺后,接支付回调就能直接对号入座。

3. 核心模块代码拆解:一次下单请求从进入到完成的完整链路

3.1 用户登录与鉴权:Token签发与Redis会话缓存

小程序端没有传统Web的Session概念,常见的做法是登录后签发一个Token,后续请求在Header里带上Token,后端通过拦截器校验后放行。这个项目的登录流程也走这条路。先看登录接口:

@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @Autowired private StringRedisTemplate redisTemplate; @PostMapping("/login") public Result login(@RequestBody LoginParam param) { // 1. 校验账号密码 User user = userService.checkLogin(param.getUsername(), param.getPassword()); if (user == null) { return Result.error("账号或密码错误"); } // 2. 签发Token,写入Redis,7天过期 String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set("TOKEN_" + token, String.valueOf(user.getId()), Duration.ofDays(7)); // 3. 返回Token,前端storage存储 return Result.success(new LoginResult(token, user)); } }

这段代码的关键逻辑是三步。第一步调用service层校验账号密码,password字段在service里用BCrypt比对,不会把明文密码传到mapper层,数据库里存的也是BCrypt哈希串。第二步生成一个随机Token作为会话凭证,写入Redis,key格式是TOKEN_前缀加Token值,value是用户ID,过期时间7天。第三步把Token返回给前端,小程序端存到本地storage里,后续请求放到Authorization头。

写拦截器时我一般会加一层白名单:登录、商品列表、商品详情这些无需鉴权的接口放行,下单、购物车、个人中心必须带Token。校验失败统一返回401,前端收到后清理本地Token并跳转登录页。把会话放在Redis还有一个额外好处:运营人员冻结某个用户后,下一请求立即失效,不需要等Token自然过期,这在纯Session方案里做不到。

3.2 商品列表与详情:分页查询与缓存兜底

商品相关接口是商城流量最大的部分,首页推荐、分类页、搜索结果都打在它上面。先看分页查询的实现:

public PageResult<Goods> queryGoodsPage(GoodsQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<Goods> goodsList = goodsMapper.selectGoodsByCondition(query); return new PageResult<>(goodsList); }

这里用PageHelper做物理分页,startPage调用之后紧跟的select查询会自动拼接LIMIT语句,这是这个项目的惯用写法。需要特别注意,PageHelper的startPage只对下一条SQL生效,中间不能插入其他数据库操作,否则分页条件会串到别的查询上,返回结果和总数对不上,这是初学时最容易踩的坑,排查起来又很隐蔽。

商品详情页的访问量更高,直接查数据库扛不住,项目里用了Cache Aside模式的旁路缓存:

public GoodsVO getGoodsDetail(Long goodsId) { // 1. 先查Redis缓存 String cacheKey = "GOODS_DETAIL_" + goodsId; GoodsVO goods = (GoodsVO) redisTemplate.opsForValue().get(cacheKey); if (goods != null) { return goods; } // 2. 缓存未命中,查数据库 goods = goodsMapper.selectGoodsDetail(goodsId); if (goods != null) { // 3. 回填缓存,30分钟过期 redisTemplate.opsForValue().set(cacheKey, goods, Duration.ofMinutes(30)); } return goods; }

这段缓存的逻辑是标准的三步:先读缓存,读不到再查数据库,查到后回填Redis并设置过期时间。30分钟过期时间对商品详情这种数据比较合适,价格调整后能较快生效,又不会频繁穿透到数据库。如果再讲究一点,管理端修改商品时主动删除对应缓存key,下次请求自动回填,可以做到准实时一致。

3.3 下单流程:事务边界与库存扣减

下单是商城最核心的写操作,它同时涉及订单表插入、订单明细插入、商品库存扣减、购物车清理,任何一个步骤失败都要整体回滚。项目里的做法是直接用Spring的事务注解:

@Transactional(rollbackFor = Exception.class) public OrderInfo createOrder(Long userId, List<CartItem> cartItems) { // 1. 创建订单主表记录 Order order = new Order(); order.setOrderSn(generateOrderSn()); order.setUserId(userId); order.setTotalPrice(calculateTotal(cartItems)); order.setStatus(OrderStatus.WAIT_PAY); orderMapper.insert(order); // 2. 写入订单明细,同时扣减库存 for (CartItem item : cartItems) { OrderItem orderItem = buildOrderItem(order.getId(), item); orderItemMapper.insert(orderItem); // 带条件的UPDATE,从数据库层面防止超卖 int rows = goodsMapper.decreaseStock(item.getGoodsId(), item.getNum()); if (rows == 0) { throw new RuntimeException("商品库存不足"); } } // 3. 清空已下单的购物车商品 cartMapper.deleteByUserAndGoods(userId, cartItems); return order; }

@Transactional标注了rollbackFor=Exception.class,保证运行时异常触发回滚,前面的订单插入和明细插入都会撤销。库存扣减用了带条件的UPDATE:decreaseStock在mapper里对应update goods set stock=stock-#{num} where id=#{goodsId} and stock>=#{num},SQL层面保证库存不会扣成负数,返回影响行数为0说明库存不足,直接抛异常让事务回滚。

这段代码还有两个值得留意的点。generateOrderSn生成订单号时,我见过不少实现直接取系统时间戳,并发下容易重复,项目中一般会拼上随机数或用户ID。第二个是幂等,下单接口在前端防重复点击只是第一层,严谨的做法是在创建订单前查一下是否已存在同一用户、同一批商品、待支付状态的订单,有就直接返回,避免连点产生重复订单。

3.4 管理端商品管理:MyBatis动态SQL支撑多条件筛选

管理后台的商品列表要支持按名称模糊查询、按分类筛选、按上下架状态过滤,条件组合不固定,这种场景用MyBatis的动态SQL是最合适的:

<select id="selectGoodsByCondition" resultType="com.linjiashop.api.model.Goods"> select * from lt_goods <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="status != null"> and status = #{status} </if> </where> order by id desc </select>

标签会自动处理第一个条件前面的and,防止拼出where and name=...这种错误SQL。这里全部用的#{}预编译占位符,不会存在SQL注入风险,如果哪天看到项目里用了${}拼接参数,要警惕。另外resultType和实体类的映射需要开启驼峰转换,配置了map-underscore-to-camel-case=true后,category_id能自动映射到categoryId属性,这个开关在数据库字段比较多的时候非常省事。

到这里,一个请求从登录、浏览商品、加购到下单的链路就走通了。接下来把项目真正跑起来,这部分实操细节决定你能不能顺利看到页面。

4. 从源码到跑通:数据库初始化、配置与三端联调

4.1 环境准备:版本选型与安装顺序

跑这个项目需要的基础环境不复杂,JDK、Maven、MySQL、Redis四件套。版本选择上建议JDK用1.8,Maven用3.6.x,MySQL用5.7,Redis 4.0以上都行。这个项目踩的是经典Spring Boot技术栈,用高版本JDK启动偶尔会碰到反射或字节码库的兼容问题,不是不能解,但没必要在环境上给自己加难度。

组件推荐版本说明
JDK1.8兼容性最稳妥
Maven3.6.x配置国内镜像源加速依赖下载
MySQL5.7SQL脚本按5.7语法编写
Redis4.0及以上缓存和登录Token存储

安装顺序建议先装JDK和Maven,因为Maven构建时需要JDK环境变量;MySQL和Redis可以在构建的过程中并行准备。装Maven记得修改settings.xml里的镜像源,不然第一次mvn clean install下载依赖可能要等很久,这个等待的体验会直接影响你对整个项目的第一印象。

4.2 初始化数据库:导入SQL脚本与初始数据

项目的docs目录下一般自带了完整的SQL脚本,包含建库、建表和初始数据三部分。导入时要注意脚本里的库名和字符集设置:

create database if not exists linjiashop default character set utf8mb4; use linjiashop; -- 用户表 create table lt_user ( id bigint primary key auto_increment, username varchar(50) not null, password varchar(100) not null, nickname varchar(50) not null, create_time datetime default current_timestamp ); -- 初始管理员账号:admin / 123456 insert into lt_user(username, password, nickname) values ('admin', '$2a$10$abcdefghijklmnopqrstuv', '管理员');

导入完成之后,我习惯先查三张表:lt_goods有没有商品数据、lt_user里有没有初始账号、lt_banner有没有轮播图。很多项目启动后页面空白,八成是初始数据没导全,而不是代码问题。用mysql命令行执行source导入时,确认文件路径别写错,不同平台的相对路径处理不太一样,直接用绝对路径最省事。还要确认MySQL账号对linjiashop库有增删改查权限,权限不足时启动不报错,但一查数据就抛异常。

4.3 修改配置:application.yml里必须改的几个参数

项目的主配置在api-web模块的resources目录下,默认配置能启动,但有三处地方不改一定会踩坑。第一处是数据库连接信息,按本地环境改url、username、password;第二处是Redis配置,默认连localhost:6379,本机没改密码可以直接用;第三处是文件上传路径,商品图片一般配置一个本地磁盘路径,不配置的话上传功能会失效。

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/linjiashop?useUnicode=true&characterEncoding=utf8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

提示:数据库连接串里的useSSL=false是必须的,本地MySQL一般不启用SSL,不关会持续报警告甚至连接失败。

characterEncoding=utf8是为了防中文乱码,和脚本里的utf8mb4不冲突。MyBatis的驼峰映射开关建议保持开启,否则数据库里的category_id映射不到实体属性,接口返回的字段会是null。如果MySQL账号密码和项目默认不一致,只改password这一行就行,改动范围越小越不容易出问题。

4.4 启动与验证:管理后台、小程序接口一次跑通

依赖装好、配置改完,启动就很直接了,在项目根目录依次执行:

mvn clean install -DskipTests cd api-web mvn spring-boot:run

第一次构建会下载大量依赖,耐心等日志走到Started Application就说明启动成功。如果启动中途失败,优先看Caused by后面的第一行异常,八成是端口被占用、Redis连不上、数据库URL写错这三类问题。接着验证管理后台:浏览器访问http://localhost:8080,用初始账号登录,能进商品列表页面就说明数据库连接和数据导入都正常。再验证C端接口:用接口调试工具直接请求商品列表地址,能返回JSON数组就说明接口层没问题。最后验证鉴权链路:调登录接口拿Token,带上Token请求购物车接口,能返回用户购物车数据就说明Redis会话正常。

我有个习惯,第一次跑通后立刻把启动细节固化成笔记,包括数据库账号、Redis地址、启动命令、验证URL、默认账号密码。后面改代码改坏了,能快速回到基准状态重跑,不至于在环境问题上反复折腾。

5. 避坑指南:跑这个Java商城项目最容易遇到的5个问题

5.1 启动后接口卡死或报错:Redis没起来

现象:项目启动一切正常,但登录或商品接口一调用就卡住,过一会儿报连接超时。原因:Redis服务没启动,或者Redis配置的端口不对。Spring Boot默认连接localhost:6379,本地Redis没开,第一次访问缓存时必然超时。解决:先redis-cli ping验证,返回PONG说明服务正常;如果Redis设置了密码,还要同步改application.yml里的password配置,否则连接直接被拒绝。启动前先确认Redis状态,这是所有带缓存的Spring Boot项目通用排查第一步。

5.2 SQL脚本导入失败:字符集与MySQL版本不兼容

现象:source导入SQL时报语法错误,或者导入成功但页面上中文全变成问号。原因:一是MySQL 8.0对旧脚本里的部分语法兼容性有变化,二是客户端连接字符集没有指定utf8。解决:优先用MySQL 5.7跑;导入前先执行set names utf8mb4,建库语句显式加上default character set utf8mb4。导入后立即查一条中文数据确认不乱码再继续,不要等启动完再返工,页面里零零散散的问号排查起来最费时间。

5.3 访问接口报BindingException:MyBatis的XML没被扫描到

现象:项目启动正常,一访问商品相关接口就抛Invalid bound statement (not found)。原因:mapper接口和XML文件没有匹配上,要么XML文件没放在mapper-locations定义的目录下,要么XML里的namespace写错了。解决:先检查application.yml里mapper-locations是不是classpath:mapper/*.xml,再看XML文件的namespace是否与接口全限定名一致。改完后mvn clean重新编译一次,target目录里的旧文件有时会导致改动不生效,这个问题我遇到过不止一次,后来一律先clean再rebuild。

5.4 小程序真机预览白屏:域名白名单拦住了接口

现象:开发者工具里一切正常,真机扫码后首页空白,接口请求全部失败。原因:小程序真机环境要求接口域名配置在平台的合法域名列表里,且必须走HTTPS。本地联调时用的http://localhost在真机上不可能通。解决:开发阶段在开发者工具里勾选不校验合法域名;正式上线前把接口部署到HTTPS域名,在平台后台配置request合法域名。这问题没有玄学,就是配置项没到位。

5.5 管理后台跨域:分端部署后的CORS配置

现象:Vue管理后台跑在8081端口,接口工程跑在8080端口,浏览器里后台页面请求接口抛跨域异常。原因:前后端分离部署下,浏览器同源策略拦截了不同源的请求。解决:在Spring Boot侧加CORS配置,放行指定来源和请求头。常见做法是写一个WebMvcConfigurer的CorsRegistry,对/api/**路径放行,allowedOrigins里写上管理后台的完整地址包括端口。要注意origin写全,只写域名不带端口一样会被拦,这个细节容易看漏。

6. 二次开发实操:给这个商城加一个限时秒杀模块

6.1 秒杀表设计:营销库存与主库存分离

给商城加营销功能,我的习惯是先把表和库存归属想清楚。秒杀需要一个活动表,字段包括商品ID、秒杀价、开始时间、结束时间、秒杀库存。最容易翻车的地方是把秒杀库存直接加到商品表的主库存里扣,活动结束库存被清空,普通下单也跟着没货卖。正确做法是单独建秒杀库存字段,活动库存扣完只影响秒杀入口,不影响正常销售。

6.2 秒杀接口与Redis预热:库存扣减放在缓存层

秒杀的核心是削峰,项目里已有Redis,方案顺理成章:活动开始前把秒杀库存预热到Redis,请求进来直接扣Redis库存,扣成功才落订单:

public Boolean trySeckill(Long goodsId, Long userId) { String key = "SECKILL_STOCK_" + goodsId; Long stock = redisTemplate.opsForValue().decrement(key); if (stock < 0) { // 库存扣完,回滚并返回失败 redisTemplate.opsForValue().increment(key); return false; } // 扣库存成功,创建秒杀订单 seckillOrderService.createOrder(goodsId, userId); return true; }

Redis的decrement是单线程原子操作,天然避免超卖,比数据库行锁方案简单直接。扣成功之后订单创建可以同步写库,也可以丢到队列里异步处理,取决于秒杀流量。活动结束后把秒杀库存key的过期时间设短,避免脏数据长期占用内存。这个模块只加了一张表、一个接口、一个页面,就复用了项目现有的Service和Redis配置。

6.3 管理端配活动、小程序端展示

管理后台加一个秒杀活动管理页面,配置商品、价格、场次时间、库存;C端首页加一个秒杀入口,列表页展示活动商品,点击跳转商品详情并带上活动ID。下单接口复用现有的createOrder,入参增加活动ID,在创建订单前校验秒杀资格和活动时间即可。整个改造下来核心业务代码基本没动,这就是service层和mapper层分离带来的好处。

我第一次加秒杀功能时翻过一次车:图省事直接扣商品表主库存,活动结束后普通下单一直显示库存不足,排查到半夜才发现是营销库存和主库存混在了一个字段里。从那以后我每次加新营销玩法,都强制先画一遍数据流向和库存归属,确认新库存不会污染核心数据才动代码。这个项目本身结构清晰,按这个思路扩展新功能基本不需要动老代码。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进