简介:JAVA微信小程序商城源码及完整后台是一套面向Java开发者的电商小程序前后端解决方案,基于SpringMVC、MyBatis、Spring、Maven和MySQL构建,前端采用H5与CSS3,后台管理界面基于Bootstrap-ACE框架。资源覆盖商品发布、物流管理、评价系统、优惠券、运费规则、在线客服、微信支付、在线退款及微信管理等核心电商业务模块,流程完整,可直接运行部署,也可作为二次开发或课程设计、毕业设计的参考起点。源码按照前端页面、后端业务与后台管理进行分层组织,目录结构清晰,便于按需定位和修改代码。压缩包采用7z格式,大小约20.5MB,体量适中,适合个人开发者快速下载学习。目前已有5047人浏览学习,适合具备一定Java Web基础、希望快速上手微信小程序商城开发的学习者。
1. 从一套「JAVA微信小程序商城源码+完整后台」说起:这套东西到底能给你什么
这套 JAVA微信小程序商城源码+完整后台在电商外包和私活市场里一直是最热的关键词,背后是个真实需求:用最低成本做完“小程序端选品下单 + 后台管商品订单”的完整闭环。常见做法是,后端用 Spring Boot 搭好的接口,前端是微信原生小程序,再加上一份 MySQL 建表脚本和一个能录入商品、处理订单的 Web 后台。你拿着它,能省下从零搭建的时间,但也别指望直接双击就能上线——我见过太多人卡在数据库导入、微信登录配置和图片上传这三件事上。适合谁呢?一是想快速上线自营微信小店的团队,二是刚把 Java 基础学完、想拿完整项目练手的同学,三是接外包需要一套可交付底盘的工程师。
2. 先看后台骨架再下手:Spring Boot + MyBatis的商城系统是怎么组织的
2.1 为什么主流源码都压Spring Boot + MyBatis而不是SSH/SSM
现在的 JAVA 微信小程序商城源码,后台十有八九是 Spring Boot + MyBatis。很多人刚学 Java 时还在背 SSH(Spring MVC + Spring + Hibernate)的配置项,到了开源商城源码里,你会发现那套已经属于考古范围。Spring Boot 用 starter 和自动配置把粘合逻辑收走,内嵌 Tomcat 让部署变成一条java -jar命令;MyBatis 又保留了 SQL 的手写自由,电商后台那些多表 join、分组统计、动态条件查询,用 Hibernate 的 HQL 写起来反而不直观。要是去翻 java 面试题,Spring Boot + MyBatis 的出现频率也远高于 SSH,团队招人不用重新培训。
从再造一个后台的成本看,这套组合有四个实际好处:第一,微信支付、阿里云 OSS 这些常用 SDK 都有对应的 spring-boot-starter,能省掉大量样板代码;第二,MyBatis 的热门框架 PageHelper 做分页方便,详情页、列表页的查询条件再复杂也能用动态 SQL 扛住;第三,Spring Boot 的 actuator 能直接暴露健康检查接口,部署在服务器上方便监控进程是死是活;第四,市面上你能搜到的大多数 Spring Boot + mybatis 的 java 开源多商户跨境商城源码下载,都是同一套骨架,二次开发的资料好找。这套源码也不例外,我一般会先花十分钟把包结构扫描一遍,确认它是不是标准的 controller-service-mapper 三层,再决定从哪开始改。
为了让你判断手里源码的底子,这里有一个简单的选型对比:如果你是第一次接触这类项目,直接用 Spring Boot 2.x + MyBatis 的骨架就够了。有些老源码还标着 SSM 甚至更古老的 Spring + Struts,看到 Struts 可以直接放弃,因为微信支付对接、文件上传那些依赖,在老旧框架里处理起来全是坑,不值得为省一点迁移时间搭上排错精力。
2.2 小程序端和后台的鉴权链路:wx.login到底换了什么
小程序和传统 Web 登录最大的区别是:没有 cookie,也没有 session 容器,所有请求都靠一个自定义 token 来认人。所以源码里有一整套隐藏链路,理解它,你后面改任何需要登录的接口才不会懵。链路是这样的:小程序调用wx.login()拿到一个一次性 code,然后用wx.request把这个 code 发给后台的登录接口;后台拿到 code 后,拿 appid、secret、code 去调微信的jscode2session接口,换回 openid 和 session_key;接着后台用 openid 查用户表,查不到就自动注册一条新用户,最后生成一个随机的 token 存进 Redis,设置 7 天过期,返回给小程序。从此刻起,小程序每个请求都把这个 token 放在 header 里,后台通过拦截器校验。
在一套完整后台里,这段代码一般长这样:
// AuthService.java 里最核心的三个动作 public String wxLogin(String code) { // 1. 用 code 换 openid,这一步叫 jscode2session Map<String, String> params = new HashMap<>(); params.put("appid", wxProperties.getAppid()); params.put("secret", wxProperties.getSecret()); params.put("js_code", code); params.put("grant_type", "authorization_code"); String result = restTemplate.getForObject( "https://api.weixin.qq.com/sns/jscode2session?appid={appid}&secret={secret}&js_code={js_code}&grant_type={grant_type}", String.class, params); // 2. 解析 openid,查表或建新用户 JSONObject json = JSON.parseObject(result); String openid = json.getString("openid"); if (openid == null) { throw new BusinessException("微信登录失败: " + json.getString("errmsg")); } User user = userMapper.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setAvatar("default.jpg"); userMapper.insert(user); } // 3. 生成 token 放 Redis,key 里带前缀方便诊断 String token = UUID.randomUUID().toString().replaceAll("-", ""); redisTemplate.opsForValue().set("wxmall:token:" + token, user.getId(), 7, TimeUnit.DAYS); return token; }这段代码里,最容易出问题的是第一步。微信接口返回的 openid 只在 code 有效期内有效,code 一次性的,用第二次就会报 10002;还有,像appid和secret这两个参数不要写死在小程序前端,因为 secret 一旦暴露,别人就能冒充你的用户登录。正确的做法是让小程序只传 code,所有换 openid 的逻辑都留在后台。第二步的findByOpenid必须走索引,openid 字段长度一般是 28 位左右,建表时要给唯一索引,否则流量上来后按 openid 查用户的 SQL 全表扫描,接口撑不住。第三步把 token 存 Redis 而不是放 JWT 里,是为了之后想在后台踢人下线时,直接删掉这条 key 就能生效;如果你拿到的源码用的是 JWT,那就要额外维护黑名单,这是另一个话题。
检查你手上的小程序端代码,如果看到它把 appid 和 secret 直接写在某个 js 文件里,那说明这份源码的安全意识不太行。正常源码应该在小程序端只配置 appid,secret 永远只出现在后台的配置文件中。改法也简单:把小程序端写死 secret 的代码删掉,改成只传 code 给后台,后台自己拼参数调微信接口即可。
2.3 数据库设计:商品、订单、用户三张主表的字段边界
源码的完整后台一般会给你一张数据库设计文档,没有文档的话就自己看 SQL 建表脚本。商城业务再复杂,核心逃不开用户、商品、订单这三大块。我习惯把表分成三类:用户相关(user、user_address、member_point_log)、商品相关(product、sku、product_category、brand)、订单相关(cart、order、order_item、payment_log)。如果这套源码还要做营销,还会有 coupon、seckill 这类表,但先不管那些,基础核心是先分清楚这几张主表之间的关联。
看一套开源商城源码值不值,重点看它的 sku 表设计。土办法是把 product 和 sku 混在一张表里,用一行记录一个颜色、一个尺码的库存价格;做得好的是 product 表只放商品基础信息,具体到“红色、XL、价格 199、库存 50”这种粒度,全放 sku 表。后者在面对多规格商品时,改价格、补库存、锁定库存都方便,而且以后对接仓储系统,每个 sku 就是最小的库存单位。给你一个简化结构参考:
-- 商品主表 CREATE TABLE product ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT '商品标题', category_id INT UNSIGNED NOT NULL, default_image VARCHAR(500) NOT NULL, detail_html MEDIUMTEXT COMMENT '富文本详情', sales INT UNSIGNED DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品主表'; -- SKU 表 CREATE TABLE sku ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, product_id INT UNSIGNED NOT NULL, specs_json JSON NOT NULL COMMENT '规格组合,如 [{"颜色":"红"},{"尺码":"L"}]', price DECIMAL(10,2) NOT NULL, stock INT UNSIGNED NOT NULL, sku_code VARCHAR(64) DEFAULT NULL COMMENT '用于对接物流/ERP' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='最小库存单位表';这里有一个关键参数:specs_json用 JSON 类型而不是拼接字符串。很多老源码是 “红,L” 这种逗号拼接,一旦规格值里出现中文逗号或用户乱填,匹配就崩。MySQL 5.7 以上原生支持 JSON,查起来能用JSON_CONTAINS,不过商城场景里更推荐把 specs_json 取出来在 Java 层做精确匹配,性能不会差,代码还好读。库存字段用INT UNSIGNED,注意做好乐观锁,扣库存时 SQL 带上WHERE stock >= #{count},避免超卖。order 表同理,下单时要冗余商品标题、规格快照和成交单价,防止商品之后改价或删除,订单历史仍然能还原当时的交易现场。
3. 本地跑通这套源码:数据库初始化、配置档位、开发者工具联调三连
3.1 导入MySQL数据库:字符集和时区是第一步
拿到源码压缩包,先别急着解压运行,第一步是建库导表。源码包里的 SQL 文件一般是shop.sql或者db/shop.sql,而且常常只包含表结构和一部分测试数据,没有建库语句。你需要在 MySQL 里手动建库,再导入。我习惯在命令行里操作,比 Navicat 导入大 SQL 更不容易卡死。
# 创建一个专门给商城的库,字符集直接定成 utf8mb4 mysql -uroot -p -e "CREATE DATABASE shopmall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入表结构和基础数据 mysql -uroot -p shopmall < shop.sql这里有几个参数必须确认:字符集别用默认的 utf8,因为商品名、收货人姓名里一旦有 emoji,utf8 会报错或者存成问号,utf8mb4才能完整存下四个字节的字符。COLLATE用utf8mb4_general_ci就够了,排序规则对商城业务影响不大。如果导入时报 1273 错误,说明 SQL 脚本里指定了更高版本的字符集排序,你当前的 MySQL 不支持,可以把脚本里的COLLATE utf8mb4_0900_ai_ci全局替换成utf8mb4_general_ci再导入。导入完成后,用SHOW TABLES;至少能看到 user、product、sku、order 这几张核心表,如果连 order 表都没有,那这份源码连半成品都算不上,建议换一套。
3.2 修改application.yml:端口、数据源、Redis三个必调项
数据库就位后,就要让后台代码连上去。打开源码里的application.yml(也有可能是application.properties),前 30 行就会被三个配置块堵住:server、datasource、redis。常见做法是把这三项一次性改完,再启动,不然你会反复看到连接超时和 Redis 连接拒绝的报错。下面是典型配置和必改项:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/shopmall?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 password: database: 0 # 自定义微信配置,源码里一般叫 wx 或 wechat wx: appid: 你的小程序appid secret: 你的小程序secret先看 datasource 的 url。serverTimezone=Asia/Shanghai必须加,否则控制台时间字段会差 8 小时;characterEncoding=utf8要和数据库里建库时的utf8mb4区分——这里的 utf8 是 JDBC 编码参数,一般写成 utf8 没问题,但如果你遇到乱码,改成characterEncoding=utf8mb4反而会让某些版本的驱动报错,推荐保持utf8,依靠库表级别的utf8mb4来兜底。useSSL=false也是必须的,本地 MySQL 没配证书,true 会启动告警甚至连不上。再看端口号,8080是 Spring Boot 默认,但如果你的电脑上有别的 Java 程序占了这个端口,可以改成 8081,对应后面小程序端 baseUrl 里的端口也要同步改。
Redis 配置里,password留空表示本地 Redis 没设密码。如果你本地 Redis 设过密码,这里填错的话,后台启动会直接报Unable to connect to Redis; no password,而且控制台会不断重连刷日志。这时候先去 Redis 配置文件里把requirepass注释掉,或者把正确的密码填进来。wx.appid 和 wx.secret 现在可以不急着换,本地先用源码自带的测试值跑通登录,但要心里有数:不换的话,微信登录接口拿不到真实 openid,后面小程序登录一定失败。把这三块改完后,在项目根目录执行:
mvn spring-boot:run # 或者先打包再启动,二选一 mvn clean package -DskipTests java -jar target/shop-admin.jar看到控制台出现Started Application in xx seconds并且没有初始化异常,就说明后台已经起来了。此时你可以用浏览器或 curl 试一下http://localhost:8080/api/health或随便一个公开接口,能返回 JSON 就继续做小程序端联动。
3.3 微信开发者工具里改request地址:让小程序找到后台
后台接口活着,小程序却不一定能打通,问题基本出在小程序端配置的 baseUrl 上。打开小程序前端源码(原生小程序一般直接是miniapp/目录,如果你拿到的是 uniapp 工程,目录结构会多一层src),找到config.js或utils/request.js,里面有一段类似这样的配置:
// 小程序全局请求地址,本地开发时写电脑的局域网IP module.exports = { baseUrl: 'http://192.168.31.115:8080', timeout: 10000 }这里重点说一句:不要在小程序里写http://localhost:8080。开发者工具里,localhost 指向的是你的开发电脑,看起来能通,但一旦换成真机预览,手机上的 localhost 指向手机自己,请求必挂。真机调试的常见做法是让手机和电脑连同一个 Wi-Fi,把 baseUrl 改成电脑的局域网 IP。怎么查 IP?命令行里ipconfig(Windows)或ifconfig(macOS/Linux)都能看到。改完之后,在微信开发者工具右上角点“详情-本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这个开发选项,因为后台本地是 http 协议,域名校验会拦下所有请求。
还有一个和页面显示强相关的坑:微信小程序顶部导航栏高度。很多源码把页面标题写死在app.json里,你改了后发现 iPhone 刘海屏和安卓状态栏高度不一样,导航栏把页面顶上去一块。源码里如果有自定义导航栏组件,记得去读它的getSystemInfoSync()相关代码,确认是否用了statusBarHeight做偏移。如果源码连这个都没处理,那大概率是套很早期的模板,要么接受它的样式,要么自己补一个navigationStyle: custom的页面重写头部。这几个配置都搞定后,你在开发者工具里登录、点进商品列表,能看到真实数据从后台返回,这套源码就真正算跑通了。
4. 完整后台怎么扩展:商品SKU、订单状态机、会员积分的改法
4.1 商品模块:规格与SKU是一对多的树,别用逗号拼接
很多后台的“商品管理”页面只给你填一个“商品名称”和一个“商品价格”,这对卖标品还行,卖服饰水果就废了。完整的商城后台必须要支持多规格:颜色、尺寸、套餐,任意组合生成一个独立 SKU,有自己的价格、库存、编码。这套源码如果连规格功能都没有,扩展点就要从这里补起。我在第 2 章给过 sku 表的结构,这里说具体实现时最容易错的点——后台管理端编辑商品时,规格怎么保存。
常见做法是前端把规格选项收集成一个数组,后端基于这些选项做笛卡尔积生成 SKU。比如颜色有“黑、白”,尺码有“L、XL”,笛卡尔积就是“黑+L、黑+XL、白+L、白+XL”四条记录。后端代码大致是这个套路:
// 生成 SKU 组合的后端方法,省略了异常和事务 public List<SkuVO> generateSkus(Long productId, List<SpecOption> specs) { // specs 例如 [{name:"颜色", values:["黑","白"]}, {name:"尺码", values:["L","XL"]}] List<List<String>> matrix = new ArrayList<>(); List<Map<String, String>> skuSpecsList = cartesianProduct(specs); // 递归排列组合 List<SkuVO> result = new ArrayList<>(); for (Map<String, String> skuSpecs : skuSpecsList) { Sku sku = new Sku(); sku.setProductId(productId); sku.setSpecsJson(JSON.toJSONString(skuSpecs)); // 价格和库存,由运营在弹窗里逐个填写,也可以按默认值初始化 sku.setPrice(0.00); sku.setStock(0); skuMapper.insert(sku); result.add(convertToVO(sku)); } return result; }这里有两个关键参数:specsJson一定要按固定 key 的 Map 存储,不要直接存前端传来的数组,否则前后端字段顺序一变,同一条 SKU 的规格匹配就乱套。另外,后台一般还要支持“保存时全删再插入”的简化逻辑:编辑商品规格后,把旧 sku 记录逻辑删除,再按新组合重新生成,虽然粗暴,但对中小型商城来说不容易留下脏数据。别用逗号拼接的原因再强调一遍:规格值里只要出现一个中文逗号,你所谓的 “黑,L” 和 “黑,L” 就会变成两个 SKU,库存和价格完全对不上。至于商品主图还是直接用默认图片,可以给每个 SKU 单独设置一个skuImage字段,但大多数源码只在 product 层管图,先不做复杂化,能跑通列表加载更多就够第二步再优化。
4.2 订单模块:状态机与支付回调处理
订单模块是整个商城的命门,也是后台管理里最容易改出 bug 的地方。“完整后台”如果订单管理只有一张表格和一个删除按钮,那交付出去客户肯定会崩。最核心的是把订单状态机理清楚。一个最基础的订单状态流是这样:下单时创建订单,状态为 0(待支付);用户支付成功后回调通知,状态变为 1(待发货);商家后台点击发货,状态变为 2(待收货);用户确认收货,状态变为 3(已完成);如果超过支付时限或用户主动取消,状态变为 4(已取消)。此外还有退款中的状态,但先看主流程。
在 Java 代码里,我建议把状态定义成枚举而不是魔法数字,至少让别人读代码时不用猜:
public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "待发货"), SHIPPED(2, "待收货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); private final int code; private final String desc; // 构造函数和 getter 省略 }状态流转不要写在小程序端,更不能让小程序端直接猜后台状态。后台一定要做两件事:一是每个状态变更接口都要校验前置状态,比如已取消的订单不能发货;二是支付回调这种被动通知,必须做幂等。微信支付回调会发多次,第一次回调处理成功后,第二次回调再进来,如果代码没判断状态就直接改单,会导致发货时间被覆盖,甚至重复发放积分。处理回调的伪代码大致是:
@Transactional(rollbackFor = Exception.class) public void handlePaidNotify(String orderNo, String transactionId) { Order order = orderMapper.selectByOrderNoForUpdate(orderNo); // 行锁 if (order == null || order.getStatus() != OrderStatus.UNPAID.getCode()) { // 关键:状态不是待支付,直接返回成功,让微信停止回调 return; } order.setStatus(OrderStatus.PAID.getCode()); order.setPayTransactionId(transactionId); order.setPayTime(new Date()); orderMapper.updateById(order); }这里的selectByOrderNoForUpdate是专门用来控制并发的,行锁会锁住这一条订单,避免两个线程同时读到 UNPAID 状态导致重复处理,这就是 java 怎么保证数据一致性的最直接例子。很多人以为只要加了@Transactional就安全,实际上事务不解决并发读同一行的覆盖问题,必须配合锁或乐观锁字段。支付回调里还有一个同样明显的坑:回调路径必须公网可访问,本地环境测不了。建议先在后台日志里把回调查询打出来,看到异常就能快速定位,而不是直接去改支付接口。
4.3 会员模块:积分流水与等级阈值
商城后台的会员功能,基础要求是会员列表、会员等级、积分余额。如果源码只有一个user表加一个points字段,那积分变动就没有追溯,用户一投诉送积分不存在的纠纷,你什么证据都拿不出来。正确做法是单独建一张积分流水表,任何一次积分变动都记录一条流水,余额用累计求和或者单独字段缓存,但流水是硬账。表结构可以参考:
CREATE TABLE member_point_log ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, change_amount INT NOT NULL COMMENT '正数增加,负数扣减', balance_after INT NOT NULL COMMENT '本次变动后的积分余额', type TINYINT NOT NULL COMMENT '1下单赠送 2退款扣回 3签到 4后台调整', order_id INT UNSIGNED DEFAULT NULL, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME NOT NULL, INDEX idx_user_id (user_id), INDEX idx_order_id (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分流水表';关键参数是balance_after,一定不能省。有的源码只记录 change_amount,用户查到积分总数对不上时,要拿着流水一行行加减,运维成本极高。有了 balance_after,每一行都是当时账本快照,对账直接查最后一条流水就能对齐。会员等级则建议用一张 threshold 表,存“累计积分达到多少就是几级会员,享受几折优惠”,后台运营能自己调,不用发版本。如果源码把等级写死在 switch 里,改成读取配置表即可,成本很低。
等级打折要在下单时计算,这里注意别在订单表里存“折扣率”而要把“实际成交价”写进 order_item。因为你后台等级调整后,历史订单的折扣率可能变,但已成交订单的金额不能变。积分赠送最好也做成异步或者放在支付回调之后,别在下单校验库存时就送,否则用户取消订单积分还没扣回,账目就乱了。基于微信小程序的社区团购系统这类旁支需求,其实就是在订单里加一个“团长 id”和“自提点 id”,核心表不动也可以加字段硬扩,但积分账模型不能乱。
5. 避坑与排查:本地能跑不代表线上稳,五个常见翻车点
5.1 小程序请求404:request合法域名和端口匹配不上
现象:开发者工具里点登录、点商品列表,网络面板一堆 404,后台控制台没有任何请求日志进入。
原因:要么是小程序端 baseUrl 还是localhost,要么端口和后台实际端口不一致,要么上线后 request 域名没加到微信公众平台的“request 合法域名”白名单里。微信开发者工具里勾了“不校验合法域名”只是开发期的豁免,真机预览时该拦还是拦。
解决:先在开发者工具的 Network 面板看请求 URL 到底是什么。如果是https://localhost:8080,改成电脑的局域网 IP;如果后台端口是 8081,baseUrl 也要跟着改。上线前,把你购买或备案的域名加到mp.weixin.qq.com后台的“开发-开发管理-服务器域名”里,而且接口必须走 HTTPS,不能带端口(除非 443)。这套源码支持多个接入环境的话,还要检查小程序端是否按wx.getSystemInfoSync().platform区分了开发版和正式版请求地址,没有就自己加一份环境判断。
5.2 微信支付回调验签失败:notify_url要外网可访问
现象:用户微信支付成功,钱已经扣了,但后台订单状态一直不变,后台日志里出现“验签失败”或“回调处理异常”。
原因:支付回调notify_url配置成了内网地址,或者写的是localhost。微信服务器从公网发起回调,连不上你的内网机器,就会一直重试,重试几次失败后就不再通知,订单卡在待支付。
解决:开发阶段可以使用支持公网访问的临时调试域名(这个就不展开工具了),生产环境一定要把notify_url配成公网 HTTPS 域名。然后在商户平台检查 API 证书序列号和 APIv3 密钥是否都填对了,源码里如果用的是旧版本 SDK,还要注意回调地址路径是否和 Controller 里的 mapping 完全一致,少一个斜杠都验签失败。最后,在回调接口里第一行就打印收到的报文和签名,用真实报文对一遍,能省下半天猜测时间。
5.3 MySQL时间差了8小时:时区参数没配
现象:后台管理订单列表里创建时间是上午 10 点,实际用户是下午 6 点下单;小程序端商品上架时间显示也不对。
原因:JDBC 连接串没加serverTimezone=Asia/Shanghai,MySQL 默认时区是 UTC,而服务器时间又是东八区,两边一减正好 8 小时。
解决:在application.yml的 datasource url 末尾加上serverTimezone=Asia/Shanghai,同时重启确认。如果还不对,进 MySQL 执行show variables like '%time_zone%';,看 global time_zone 是不是SYSTEM,系统时区如果不是东八区,就改成SET GLOBAL time_zone = '+08:00';。注意改完数据库后连接池要重连,改配置后重启 Java 进程是最稳定的。还有一个冷门情况:如果用了LocalDateTime,JDBC 驱动版本低也可能导致时区转换出问题,把 MySQL 驱动升到 8.x 基本能解决。
5.4 图片上传后回显裂图:静态资源映射被拦截
现象:后台管理上传商品图片提示成功,但前端小程序的图片地址直接裂开,浏览器单独打开图片 URL 返回 404。
原因:后台把图片保存到本地磁盘的/upload/目录,但没有告诉 Spring Boot 如何映射这个目录到 URL。Spring Boot 默认只映射classpath:/static/,你写http://localhost:8080/upload/xxx.jpg自然是 404。如果上线后有 Nginx,也可能是 Nginx 没有把/upload/转发到后台服务。
解决:在 Java 端补一个资源映射。注意路径最后面的斜杠,Linux 下/home/shop/upload/少了结尾斜杠会映射失败。代码块我放在避坑章节里:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/home/shop/upload/"); } }配置完成后重启后台,访问一次图片 URL 确认能打开。如果放在 Nginx 后面,还应在 location 里加一条location /upload/ { proxy_pass http://127.0.0.1:8080; },不然请求到不了 Java 进程。再有就是本地 Windows 环境,file:D:/shop/upload/的写法里盘符后的冒号和反斜杠也很容易写错,看到一个 D 盘路径映射一直 404,基本是路径格式问题。
5.5 后台启动报端口占用:别急着杀进程,先看是哪个服务
现象:java -jar启动后台,控制台抛出Web server failed to start. Port 8080 was already in use.,进程直接退出。这是 java 启动失败怎么解决里最频繁的报错。
原因:之前一次启动没有彻底关闭,或者电脑上另一个程序占用了 8080。有些源码还会默认用 8080 再带一个 Redis listener,两个端口,都是一个道理。
解决:先看端口到底被谁占用。Linux 用netstat -anp | grep 8080,Windows 用netstat -ano | findstr 8080;拿到 PID 后,Windows 用taskkill /PID <pid> /F,Linux 用kill -9 <pid>。如果是你自己之前跑的后台,这条命令很安全;但如果 PID 对应的是别的业务服务,就换个端口更省事。在application.yml里把server.port改成 8081,然后小程序端 baseUrl 同步改端口,完美绕开。顺带一提,别为了找端口占用直接重启机器,那才是运维上的翻车。
6. 别把源码当黑匣子:两个能放进简历的进阶改造点
这套源码跑通、能下单、后台能发货之后,你和它之间就过了蜜月期。接下来最值得做的事,不是继续堆功能,而是验证它到底能在多大数据量、多高并发下站住,同时把后台审计能力补上。我做的第一件事是加上操作日志——管理员改了商品价格、手动取消订单、调整用户积分,这些都是高危动作,没有日志等于穿着溜冰鞋走钢丝。要做得干净,可以用注解 + Spring AOP 统一埋点:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface OpLog { String module(); // 例如 "商品" "订单" String action(); // 例如 "改价" "发货" }在需要记录的 Controller 或 Service 方法上标一下,然后用 AOP 拦截写日志,包含操作人、IP、请求参数、耗时,落库或打到日志文件都行。这个小功能在简历上可以写“基于 AOP 实现后台操作审计和用户行为追踪”,面试官看到这种描述,比看到“会用 CRUD”值钱得多。第二个值得做的是接口压测:用 JMeter 对商品列表接口和一个下单接口分别跑 50 并发,看看吞吐量和错误率。如果你发现线程调优后内存占用暴增,就回头检查是不是某些查询没有分页、有没有索引缺失,而不是对着源码抱怨。老实说,我早期拿到源码就急着改页面,没有先做压测和日志,后来上线被瞬时流量打挂,花了整晚才定位到一条没有索引的订单查询。先跑通、再补日志、最后压测,这三步是这套源码最稳的升级路径。希望帮到你。
本文还有配套的精品资源,点击获取