ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java商城小程序源码怎么读:从Spring Boot到MyBatis-Plus的落地实践

Java商城小程序源码怎么读:从Spring Boot到MyBatis-Plus的落地实践

简介:面向Java开发者的轻量级商城系统源码,基于Spring Boot、MyBatis、Thymeleaf、Redis、Maven等技术构建,定位为单商户在线销售平台,适合中小企业或个人商家快速搭建电商业务,也适合开发者学习完整的Java Web服务端体系。资源以zip压缩包提供,整体约13.2MB,内含完整工程源码,覆盖商品、订单、用户、支付等多个核心业务的数据表设计与后端逻辑;同时提供对接微信小程序的API,可实践前后端数据交互、小程序SDK集成等场景。项目还体现了微服务拆分思路,涉及OAuth2认证、安全通信、Docker部署、持续集成、日志监控与Git版本控制等工程化实践。目前已有166人学习下载,对于希望从零掌握Spring Boot电商开发流程、或需要参考商城小程序接口设计并二次开发的读者而言,是值得研读的实战材料。

1. 认识 microapp-linjiashop:一个 Java 技术栈的商城小程序源码该怎么读

拿到的包名是microapp-linjiashop-master_java_商城小程序_,拆开看其实很直白:microapp 是前端小程序工程,linjiashop 是商城代号,master 是压缩包里常见的分支标记,而 Java 说明后端不是 Node 也不是 PHP,是 Spring Boot 那一套。这类源码在中小团队里很常见,目标是让一个会 Java 的团队在尽量短的时间内上线一套微信小程序商城,而不是从零设计商品、订单、支付这些领域模型。它适合三类人:想快速交付商城项目的 Java 工程师、正在学 Spring Boot + MyBatis 的全栈新手,以及接外包时需要一套可改源码的小团队。

很多人在拿到这类包之后的第一反应是从 Controller 开始读代码,我的建议正好相反:先把它跑起来,再谈改代码。原因很简单,商城类项目的坑大多数不在业务逻辑,而在环境、配置、数据初始化这些“能跑通才算数”的地方。下面按我的落地顺序拆开讲。

2. 拆解项目骨架:microapp 前端、Spring Boot 后端与 MyBatis 持久层分别管什么

2.1 从目录命名看工程划分:admin-api、mobile-api、microapp 的角色

拿到microapp-linjiashop-master这种包名时,目录结构通常是 Maven 多模块工程。常见做法是分成后台管理端接口、移动端接口、公共核心、数据库脚本和小程序前端几块。命名规律一般是这样的:

linjiashop-master ├── admin-api # 后台管理接口服务,给 PC 管理后台用 ├── mobile-api # 小程序 / 移动端接口服务,给 microapp 用 ├── db # 数据库初始化脚本与说明文档 ├── core # 公共模块:统一返回、异常处理、工具类 ├── microapp # uni-app 写的小程序前端工程 ├── pom.xml # Maven 父工程,统一依赖与插件版本 └── sql # 部分项目会把 SQL 单独放这里

如果你拿到的包结构和上面不完全一致,不要慌,按职责去对应即可:找mobile-api或app-api这类名字确定小程序后端;找microapp、mall-app、miniapp确定小程序前端;找.sql文件确定数据库脚本。这里有一个经验:先分清哪个后端是给小程序用的,哪个是给后台管理用的。很多新手一启动就开两个服务,结果改错了接口,浪费半天。

这个工程最重要的特点是小程序端和后端是分开的。小程序端是microapp工程,里面是uni-app风格的页面;后端提供 REST 接口,数据交互走 JSON。理解了这条线,后面的联调才不会懵。

2.2 后端分层与调用链:Controller → Service → Mapper 的商城写法

商城后端基本都是三层结构,不是这套源码特有,但看懂它决定了你改代码时的下手位置。分层结构通常是:

分层职责对应工程中的典型位置
Controller接收请求、参数校验、返回统一结果mobile-api 下的 controller 包
Service业务逻辑:下单、支付回调、优惠计算mobile-api 下的 service 包
Mapper / DAO数据库操作,MyBatis 或 MyBatis-Plus 接口db 模块或各 api 模块下的 mapper 包
Entity表对应的 Java 实体类通常是 db 模块的 entity 包

一条典型调用链是:小程序端发起POST /api/order/submit,请求进入 OrderController,Controller 把参数组装后调 OrderService,OrderService 里做商品校验、库存扣减、生成订单号,最终通过 OrderMapper 写入数据库。我一般改这类代码时会先确认一件事:业务逻辑是写在 Service 里的,还是写在 Controller 里的。后者在这个行业的开源项目里并不少见,但你在二次开发时最好不要继续那么写。

这里涉及一个底层选择:持久层用的是 MyBatis 还是 MyBatis-Plus。很多商城源码会用 MyBatis-Plus,因为它内置了单表 CRUD、分页插件和 LambdaQueryWrapper,能省掉大量重复的 XML。如果你看到代码里出现了LambdaQueryWrapper、selectPage这类写法,那基本可以确定项目里集成了 MyBatis-Plus。后面我给的代码示例也按这个来。

2.3 核心数据模型:商品、SKU、订单、支付流水怎么组织

商城类项目不管怎么改,核心数据模型都比较固定。先理解模型,再动手改代码,比从页面倒推表结构要快得多。我按常见的拆分方式列一下:

数据对象关键字段说明
用户id、openid、手机号、昵称、积分小程序端通常用微信登录换取 openid
商品 SPUid、分类 id、标题、主图、状态SPU 是抽象商品,比如一个手机型号
商品 SKUid、商品 id、规格、价格、库存SKU 是具体可下单的规格,价格和库存挂在 SKU 上
订单id、订单号、用户 id、总金额、状态状态机:待支付、已支付、已发货、已完成、已取消
订单明细id、订单 id、SKU id、数量、单价下单时把商品快照写进明细,避免商品改了价格影响历史订单
支付流水id、订单号、支付方式、回调参数、状态微信支付或支付宝的回调记录在这张表

这里有一个新手容易踩的设计误区:把库存放在商品表而不是 SKU 表。如果一个商品有多个规格,比如红色 10 件、蓝色 5 件,库存必须拆分到 SKU。商品表只保留 SPU 层面的上下架状态,价格和库存都在 SKU 上。下单时校验和扣减的都是 SKU 库存。

订单号不要用数据库自增 id 直接暴露给用户,常见做法是生成一个业务订单号,格式可以是“前缀 + 年月日 + 随机数”,类似LS20250612153000123456。这样既方便客服查单,也避免用户通过订单号猜到平台单量。支付流水表则要和订单表分开,因为一次订单可能有多笔支付尝试,比如用户支付超时后再次支付。

理解完这些模型,你再看代码时就会有一种“对号入座”的感觉:打开商品表就能找到对应实体,顺着实体找到 Mapper,再找 Service 和 Controller,改起来自然有方向。

3. 本地跑通 microapp-linjiashop:数据库初始化、Spring Boot 配置与小程序的联调指引

3.1 环境准备:JDK 8、Maven、MySQL 5.7、Redis 的版本搭配

跑这套 Java 商城小程序之前,先把环境凑齐。我默认你是在 Windows 或 macOS 本机操作,Linux 服务器后面部署时再说。需要准备的东西如下:

工具推荐版本用途常见问题
JDK1.8 或 11编译和运行 Spring Boot环境变量没配好,java -version报错
Maven3.6 以上拉依赖、打 jar 包镜像源慢,建议配阿里云镜像
MySQL5.7 或 8.0数据存储字符集没设 utf8mb4,中文乱码
Redis5.x 以上缓存、token、验证码未启动导致后端启动失败
HBuilderX最新稳定版运行小程序前端工程需要安装 uni-app 插件
微信开发者工具最新稳定版预览和调试小程序需要注册小程序测试号

环境准备这个环节最磨人的是 Java 环境配置。我见过不少人在一台机器上装了多个 JDK,导致java -version显示的版本和项目要求不一致。建议在命令行里确认一下:java -version和mvn -v分别执行,确保 Maven 使用的 JDK 和你期望的一致。如果 Maven 用的 JDK 版本不对,编译时会出现一些莫名的报错。

MySQL 安装时记得把字符集设为 utf8mb4,排序规则用utf8mb4_general_ci就好。这一步没做好的话,后面导入 SQL 时会有编码警告,小程序端显示商品名称会出现乱码。Redis 安装后先启动服务,用redis-cli ping验证返回PONG再继续,不然等会儿后端启动会卡在连接 Redis 上。

3.2 导入数据库脚本:mysql 命令行初始化与字符集检查

在项目里找到.sql文件后,不要用 Navicat 直接双击打开再运行,虽然能跑,但编码问题不好控制。我习惯用命令行导入,过程可控,报错也直观。

先创建数据库,再指定字符集导入:

mysql -uroot -p --default-character-set=utf8mb4

进入 MySQL 后执行:

CREATE DATABASE IF NOT EXISTS linjiashop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE linjiashop; SOURCE /path/to/your/linjiashop.sql;

这里SOURCE后面是 SQL 文件的绝对路径,不要用相对路径,避免找不到文件。导入完成后,可以用SHOW TABLES;看一下表是否完整,再抽查一张核心表确认数据量:

SELECT COUNT(*) FROM goods;

如果返回结果说明商品表有数据,说明导入成功。字符集这一步很关键:--default-character-set=utf8mb4保证脚本里的中文注释和默认值不乱码。如果导入后中文乱码,大概率是数据库本身的字符集不是 utf8mb4,而不是 SQL 文件的问题。

3.3 修改 Spring Boot 配置:数据源、Redis、端口三处必看

数据库导入完成后,接下来要改后端配置。打开移动端接口服务的配置文件,通常是application.yml或application-dev.yml,核心配置在spring.datasource和spring.redis这两块。先看数据源:

spring: datasource: url: jdbc:mysql://localhost:3306/linjiashop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

需要注意三个参数:serverTimezone=Asia/Shanghai解决时间差 8 小时问题;characterEncoding=utf8保证中文正常读写;driver-class-name如果是com.mysql.jdbc.Driver说明项目用的是 MySQL 5.x 驱动,如果是com.mysql.cj.jdbc.Driver则是 MySQL 8。你本机装的是 MySQL 8,建议显式用后者。

接着看 Redis 配置:

spring: redis: host: 127.0.0.1 port: 6379 password: database: 0

如果你的 Redis 没设密码,password留空即可。注意这里填错会导致后端启动时报连接超时。最后看服务端口,常见的是server.port: 8080或8081。如果本机 8080 被占用,建议直接改成 8081:

server: port: 8081

改配置时有两点要提醒:一是配置文件里经常区分dev、prod多套环境,先确认你改的是 dev 那套;二是密码不要用弱密码,但本地开发无所谓,后面上线前要改成强密码并放到外部配置里。

3.4 启动后端并验证接口:Java 启动失败的排查顺序

配置改完后,进入项目根目录执行 Maven 编译打包。第一次会拉很多依赖,建议提前配好阿里云 Maven 镜像,不然可能等很久:

mvn clean install -DskipTests

编译成功后,找到移动端接口服务对应的 jar 包,通常在mobile-api/target目录下,然后启动:

java -jar mobile-api/target/linjiashop-mobile-api.jar --server.port=8081

这里--server.port=8081是命令行覆盖配置文件的端口,优先级高于配置文件,适合临时测试。看到类似Started Application in xx seconds的日志,说明启动成功。

如果启动失败,大多集中在三类报错。第一类是端口被占用,日志会报Port 8081 was already in use,这时要么改端口,要么用netstat -ano | findstr 8081找到占用进程并处理。第二类是数据库连不上,报错里会出现Access denied或Communications link failure,去核对用户名密码和 URL。第三类是 Redis 没启动,报错一般会带Unable to connect to Redis字样,启动 Redis 再重启后端即可。这一套排查顺序我用了很多次,基本能覆盖八成的启动失败场景。

服务起来后,先用 curl 验证接口是否真的可用。登录接口的路径以你手上源码里的 Controller 为准,常见形式如下:

curl -X POST http://localhost:8081/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}'

如果返回 JSON 里有token字段,说明后端接口正常,可以进入小程序端联调。不要直接打开浏览器看页面,商城接口大多返回 JSON,curl 反而更直观。

3.5 小程序端联调:HBuilderX 运行 microapp 后改 baseURL

后端跑通后,轮到小程序端。用 HBuilderX 打开microapp目录,确认项目类型是 uni-app。在 HBuilderX 的菜单里选择“运行 → 运行到小程序模拟器 → 微信开发者工具”,前提是你已经安装了微信开发者工具并登录。

小程序端跑起来后,第一件事是改接口地址。在 uni-app 工程里,接口基础地址一般集中在一个配置文件或request.js工具类里。找到类似这样的代码:

// config.js 或 request.js 中定义接口基础地址 const BASE_URL = 'http://localhost:8081';

这里的localhost需要注意:如果是微信开发者工具模拟器,可以直接用localhost;但如果用手机真机预览,localhost指向的是手机自己,必须改成你电脑在局域网里的 IP,比如http://192.168.1.100:8081。改完保存,重新编译。

还有一个比较隐蔽的坑:微信开发者工具默认会校验请求域名,本地开发时需要在“详情 → 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。不勾选的话,小程序里发请求会直接报“不在以下 request 合法域名列表中”,页面空白,看起来像后端挂了,其实和后端一点关系都没有。这一步做完,前后端就跑通了,可以在小程序里看到商品列表和首页数据。

4. 动手改商城小程序:商品搜索、下单闭环与 MyBatis-Plus 实践

4.1 商品列表按分类和关键词筛选:MyBatis-Plus 的 LambdaQuery 写法

跑通之后,第一次真正改业务大概率从商品模块开始。商品列表是最常见的需求:按分类筛选、按关键词搜索、分页返回。如果项目集成了 MyBatis-Plus,这个接口写起来很简洁。

先写 Controller,参数用@RequestParam接收:

@RestController @RequestMapping("/api/goods") public class GoodsController { private final GoodsService goodsService; public GoodsController(GoodsService goodsService) { this.goodsService = goodsService; } @GetMapping("/list") public Result<IPage<Goods>> list( @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") Long pageNum, @RequestParam(defaultValue = "10") Long pageSize) { IPage<Goods> page = goodsService.pageQuery(categoryId, keyword, pageNum, pageSize); return Result.ok(page); } }

这里categoryId和keyword都是可选的,pageNum和pageSize带默认值,避免前端漏传参数时报 400。Result是项目里的统一返回体,具体类名以你源码为准,可能是R、AjaxResult,看一眼 Controller 里已有的返回类型就能对上。

Service 的实现是核心,用LambdaQueryWrapper动态拼接条件:

public IPage<Goods> pageQuery(Long categoryId, String keyword, Long pageNum, Long pageSize) { LambdaQueryWrapper<Goods> wrapper = Wrappers.lambdaQuery(); wrapper.eq(categoryId != null, Goods::getCategoryId, categoryId) .and(keyword != null && !keyword.isEmpty(), w -> w.like(Goods::getName, keyword) .or() .like(Goods::getKeywords, keyword)) .eq(Goods::getStatus, 1) .orderByDesc(Goods::getSortOrder); return goodsMapper.selectPage(new Page<>(pageNum, pageSize), wrapper); }

eq方法的第一个参数是 boolean,条件不成立时自动忽略这个条件,这是 MyBatis-Plus 最实用的特性。and里拼接关键词搜索时要注意:如果商品有关键词字段,标题和关键词是或的关系,所以用or()连接。最后加一个status = 1过滤掉下架商品,避免用户搜到不能买的商品。

分页插件需要 MyBatis-Plus 的配置类支持。如果你启动后分页不生效,查一下项目里有没有MybatisPlusInterceptor配置,没有的话加一个:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

不加这个配置时,selectPage会返回全部数据再在内存里“假分页”,数据量一大就出问题。这是很多新手在翻车之后才补上的一课。

4.2 下单状态机与库存扣减:别在 Controller 里写业务

商品看完,下一个绕不开的是下单。商城类项目的业务核心在这里,坑也最多。我会先给你看一个常见的错误写法:在 Controller 里查库存、算价格、更新数量。这种写法在小流量下看着能跑,但代码耦合严重,后续加优惠、加支付回调时根本改不动。

正确做法是把下单逻辑放进 Service,并且用事务包住。下单的核心步骤如下:校验商品状态、校验库存、扣减库存、生成订单、生成订单明细。这些步骤要么全部成功,要么全部回滚,所以需要加事务注解:

@Transactional(rollbackFor = Exception.class) public Order submitOrder(Long userId, Long skuId, Integer count) { GoodsSku sku = goodsSkuMapper.selectById(skuId); if (sku == null || sku.getStatus() != 1) { throw new BizException("商品不存在或已下架"); } // 关键:条件更新扣库存,而不是先查再改 int updated = goodsSkuMapper.deductStock(skuId, count); if (updated == 0) { throw new BizException("库存不足"); } String orderNo = generateOrderNo(); Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(sku.getPrice().multiply(new BigDecimal(count))); order.setStatus(0); // 0 待支付 orderMapper.insert(order); OrderItem item = new OrderItem(); item.setOrderNo(orderNo); item.setSkuId(skuId); item.setGoodsName(sku.getGoodsName()); item.setPrice(sku.getPrice()); item.setCount(count); orderItemMapper.insert(item); return order; }

注意deductStock这个方法对应的 SQL,它是防止超卖的关键。Mapper 接口里定义一个方法:

int deductStock(@Param("skuId") Long skuId, @Param("count") Integer count);

对应的 XML:

<update id="deductStock"> UPDATE goods_sku SET stock = stock - #{count} WHERE id = #{skuId} AND stock >= #{count} </update>

这个写法的精妙之处在于:stock >= #{count}条件放在 WHERE 里,数据库层面保证只有库存充足时才会更新。如果库存不足,影响行数为 0,业务层就能感知并抛异常。这比“先 select 查库存、判断后再 update”的写法安全得多,因为并发请求下,两个请求可能同时读到库存为 1,然后都去扣,结果库存变成负数。条件更新是单体应用里最简单有效的手段。

事务注解@Transactional(rollbackFor = Exception.class)里的rollbackFor要写,因为默认只回滚 RuntimeException,而很多业务异常是自定义 Exception。另外要注意:@Transactional必须写在 public 方法上,并且不能是同类内部调用,否则事务不生效。这个细节我在下一章“避坑”里还会重点提。

订单状态建议用状态机而不是随便改。常见状态对应关系如下:

状态值含义可流转状态
0待支付1 已支付、4 已取消
1已支付2 已发货
2已发货3 已完成
3已完成无
4已取消无

不要在代码里到处写order.setStatus(1)这种魔法数字。我一般会定义一个常量类或枚举维护这些状态,比如OrderStatusEnum.PAID.getValue(),这样后续加支付回调、加退款状态时不会改漏。

4.3 uni-app 请求拦截器:统一加 token、处理登录过期

后端接口里一部分是公开的,比如首页、商品列表;另一部分需要登录,比如下单、查看订单。小程序端需要在每次请求时带上 token,并在 token 失效时统一跳转登录页。这个逻辑如果每个页面写一遍,代码会很散,uni-app 里通常封装成一个request方法。

常见的封装如下:

// utils/request.js const BASE_URL = 'http://localhost:8081'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'token': uni.getStorageSync('token') || '' }, success: (res) => { const resData = res.data; // 登录失效统一处理 if (resData.code === 401) { uni.removeStorageSync('token'); uni.navigateTo({ url: '/pages/login/index' }); return; } if (resData.code === 200) { resolve(resData.data); } else { uni.showToast({ title: resData.msg || '请求失败', icon: 'none' }); reject(resData); } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } export const get = (url, data) => request(url, 'GET', data); export const post = (url, data) => request(url, 'POST', data);

这里的关键点有两个。第一个是 header 里 token 字段名要和后端过滤器对应。有的后端要求token,有的要求Authorization,看一下后端拦截器或过滤器里读取的 header 名是什么,保持一致即可。第二个是 401 的约定:后端在 token 失效时返回的 code 是不是 401,以你源码里的 GlobalExceptionHandler 为准。

我用这个封装时会顺手做一件事:把登录接口单独放一个文件,不经过带 token 的拦截逻辑,不然登录接口本身会循环跳转登录页。常见做法是给 request 方法加一个白名单参数,或者单独封装一个loginRequest。下单、购物车这类页面调用时,直接用post('/api/order/submit', {...})就行,页面里不用关心 token 怎么处理,拦截器已经统一做了。

5. 避坑与常见问题排查:从 SQL 到支付回调的五个翻车点

5.1 现象:SQL 导不进去、启动后表不存在

第一次导入 SQL 时,报了Unknown collation: utf8mb4_0900_ai_ci,或者后端启动后提示某张表不存在。

原因是 SQL 文件可能是从 MySQL 8.0 环境导出的,里面带着 8.0 默认的排序规则,而本机装的是 MySQL 5.7,5.7 不认识这个排序规则。

解决:建库时显式指定排序规则,覆盖脚本里的默认值。上面的建库语句已经写了COLLATE utf8mb4_general_ci,导入前再确认一遍。如果脚本里每条建表语句后面带着utf8mb4_0900_ai_ci,直接全局替换成utf8mb4_general_ci再导入。校验方法很简单:SHOW CREATE TABLE 表名;看表结构里的 COLLATE 是不是正常值。

5.2 现象:小程序真机请求接口失败,开发者工具里却正常

微信开发者工具模拟器上一切都好,一点“真机预览”,全部接口请求失败,报网络错误或者request:fail。

原因是手机和电脑不在同一个网络环境,或者localhost指向了手机自己,而不是电脑。真机上请求http://localhost:8081相当于请求手机自己的 8081 端口,当然会失败。

解决:把BASE_URL改成电脑的局域网 IP,比如http://192.168.1.100:8081。手机和电脑连同一个 Wi-Fi,关掉电脑防火墙对 8081 端口的拦截。再配合微信开发者工具里的“不校验合法域名”选项。一旦用到真实域名上线,必须走 HTTPS,并在微信公众平台的小程序后台配置 request 合法域名,这是后话。

5.3 现象:事务不生效,库存扣成负数

下单接口在高并发测试时,库存从 1 被扣到 -3。在 Service 里明明加了@Transactional,却还是出现数据不一致。

原因大概率有两个。第一个是@Transactional写在了私有方法上,Spring 的 AOP 代理对私有方法不生效,事务注解形同虚设。第二个是同类内部调用,比如this.submitOrder()这种调用方式,绕过了代理。

解决:把事务注解放在 public 方法上,并且从外部注入 Service 调用,不要同类方法内部直接调。另外,库存扣减用条件更新UPDATE goods_sku SET stock = stock - #{count} WHERE id = #{skuId} AND stock >= #{count},让数据库兜底。事务解决的是“多步操作要么都成功要么都失败”,库存的并发正确性要交给条件更新,两者是互补关系。

5.4 现象:时间差 8 小时、金额精度对不上

订单列表里的创建时间比实际时间慢了 8 小时,或者金额从9.90变成了9.9000000004。

原因是多个环节的时区不一致:MySQL 连接的 URL 没带serverTimezone,JVM 默认时区不是东八区,序列化时日期格式没指定。金额问题则是把BigDecimal转成了double进行运算或序列化,丢精度。

解决:JDBC URL 统一加serverTimezone=Asia/Shanghai;JVM 启动参数加-Duser.timezone=Asia/Shanghai;金额运算全程用BigDecimal,并在实体类的金额字段上加格式注解,比如@JsonFormat(pattern = "0.00")或者序列化为字符串。排查时用SELECT NOW();先看 MySQL 时间对不对,再看 Java 进程的时区,一步步缩小范围。

5.5 现象:MyBatis-Plus 自动建表与实体字段对不上

有人图省事,用网上流传的 MyBatis-Plus 基于 Java 实体类生成创建表的 SQL 脚本,结果生成出来的表要么缺少索引,要么Boolean字段变成了tinyint后映射异常,枚举字段也变成了varchar,启动后各种字段类型不匹配。

原因是这种自动脚本只能根据实体字段生成基础列,它不知道哪个字段要做索引、哪个字段是逻辑删除、哪个字段需要精度控制。它适合开发期快速同步字段变更,但绝不能作为生产环境的建表方案。

解决:生产环境的表结构以手工维护的 SQL 脚本为准,索引、默认值、注释都写在里面。实体类的字段变更后,同步用ALTER TABLE修改线上表,而不是重新生成。至于“根据实体类生成建表 SQL”这个工具,可以把它当成开发期的辅助,比如生成字段清单,但不要直接在生产库上执行。

6. 进阶:多商户改造与上线前检查清单

6.1 多商户改造:租户插件接入与权限边界

这套源码如果你要用在多商户或跨境商城方向,常见的改造思路是在核心业务表上加tenant_id字段,然后让 MyBatis-Plus 自动维护这个条件。MyBatis-Plus 的租户插件可以自动在 SQL 上拼接租户条件,避免你每个查询手写一遍。配置如下:

@Configuration public class MybatisPlusTenantConfig { @Bean public MybatisPlusInterceptor tenantInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); TenantLineInnerInterceptor tenantLine = new TenantLineInnerInterceptor(new TenantLineHandler() { @Override public Expression getTenantId() { return new LongValue(UserContext.getTenantId()); } @Override public String getTenantIdColumn() { return "tenant_id"; } }); interceptor.addInnerInterceptor(tenantLine); return interceptor; } }

UserContext.getTenantId()的意思是从当前登录上下文里拿租户 ID。你需要有一个类似UserContext的类,在登录拦截器里把商户 ID 放到 ThreadLocal 中,请求结束时清理,避免线程池复用导致数据串号。这个插件会自动在SELECT、UPDATE、DELETE语句上追加tenant_id条件,但INSERT不会自动补字段,插入时你要自己 set。还要注意:涉及多表 join 的自定义 SQL,插件可能会拼接出歧义条件,需要给表加别名并指定租户字段归属。改造之前先想清楚哪些表是商户隔离的,哪些是全局的,比如系统配置表就不要加租户插件,要在TenantLineHandler里通过ignoreTable方法排除。

6.2 上线前检查清单与启动命令

上线部署时不会再像本地一样用 IDE 启动,而是打 jar 包放到服务器。我常用的启动命令是:

nohup java -jar linjiashop-mobile-api.jar \ --spring.profiles.active=prod \ --server.port=8081 \ > /data/logs/mobile-api.log 2>&1 &

--spring.profiles.active=prod指定生产环境配置,前提是你的配置文件里有application-prod.yml。nohup和&让服务在后台运行,日志重定向到文件方便排查。上线前我一般会过一遍这五件事:关闭 Swagger 或接口文档页面,避免生产环境暴露接口结构;数据库账号用最小权限,不用 root 直连;Redis 设置密码并禁止外网访问;日志级别至少把 root 调到 ERROR,业务包可以保持 WARN;确认小程序后台配置的 request 合法域名和 HTTPS 证书有效。

付个尾账:有一次我在生产环境上线后才发现服务器时区是 UTC,用户下单时间整体慢了 8 小时,排查了半天最后是靠 JVM 参数-Duser.timezone=Asia/Shanghai重启解决。从那以后,我把时区、字符集、域名校验这些都写进了上线检查清单,每次部署前逐项打勾。做 microapp-linjiashop 这类 Java 商城小程序,最值得花的功夫就是把环境参数和事务边界先守住,功能开发其实是水到渠成的事。希望帮到你。

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

RELATED READING

延伸阅读

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