简介:这是一套面向Java后端与uniapp前端开发者的新零售电商解决方案,专为快速搭建微信小程序商城而设计,适用于中小型电商项目开发、毕业设计及全栈技术学习者。资源包含前后端完整源码,后端基于Spring Boot+MyBatis实现用户管理、商品运营、订单支付等核心业务,前端采用uniapp框架统一构建多端界面,并深度适配微信小程序生态。压缩包共2000个文件,涵盖497个Java类(业务逻辑与数据访问)、659个JS脚本(交互与API调用)、55个Vue组件(可复用UI模块)、260个CSS样式文件(含iview、element、bootstrap等多套UI支持)以及32个wxml/wxss小程序原生文件,整体16.06MB,结构清晰、模块解耦。已有1170人学习下载,读者可直接部署运行,获取从数据库设计、RESTful接口开发、小程序页面渲染到微信支付对接的全流程实践能力,并参考配套JSON配置、SQL建表语句与MD文档快速上手。 如果你跟我说要做一个小程序商城,第一反应往往是:前端一套、后端一套、微信生态又一堆限制,光是把“登录、下单、支付”这三件事跑通,就够折腾好几个周末。更别提商品管理、购物车、订单状态这类看似简单、实际到处是细节的模块。所以当我看到这套 Java + uni-app 的前后端全部开源商城项目时,第一感受是:这东西能帮你省掉大量重复造轮子的时间,让你把精力放到真正重要的业务上。
这个项目最大的特点就是“全”——后端用 Java 写,前端用 uni-app 写,小程序端可以直接跑,源码全部公开。对正在学前后端分离开发的同学来说,它是一份很完整的学习资料;对想快速上线一个小程序商城的中小团队来说,它又是一个可以拿来改改就能用的基础工程。写这篇文章,我就围绕这套项目的整体设计、数据库、后端实现、前端页面、部署上线和常见坑位,做一次比较完整的拆解,希望对正在研究同类项目的朋友有帮助。
1. 项目整体设计与技术选型——为什么看中这套 Java + uni-app 组合
1.1 技术栈全景与选型逻辑
先列出这套项目里最核心的技术组件:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 后端语言 | Java 8+ | 稳定,生态成熟,招聘市场上人多 |
| 后端框架 | Spring Boot 2.x | 快速构建接口,内置容器,适合前后端分离 |
| ORM | MyBatis-Plus | 单表 CRUD 很方便,复杂 SQL 也能手写 |
| 数据库 | MySQL 5.7+ | 最常见的关系型数据库,成本低 |
| 缓存 | Redis | 登录态、验证码、热点数据缓存 |
| 前端框架 | uni-app(Vue 2/3) | 一套代码生成小程序、H5、App |
| 状态管理 | Vuex/Pinia | 管理用户信息、购物车、全局状态 |
| 构建工具 | Maven、HBuilderX | 后端依赖构建、前端打包发行 |
选这套组合,我自己的判断有三点。
第一,Java 后端做电商类系统是有历史沉淀的。订单、支付、库存这些场景,Java 生态里有大量现成解决方案和开源组件,遇到问题很容易搜到答案。对比 Node.js 或 Go,Java 在中小团队里可能显得“重”,但稳定性和可维护性确实有优势。
第二,uni-app 对微信小程序的支持很成熟。它本质上是用 Vue 语法写一套代码,编译到不同平台。尤其微信小程序的登录、支付、分享这类能力,uni-app 都封装好了,虽然偶尔还是要写条件编译,但总体比原生小程序开发更高效。
第三,前后端分离已经是当前 Web 开发的主流形态。后端只出接口,前端负责页面渲染和交互。这套项目本身就是一个很好的前后端分离实战样例,接口怎么设计、请求怎么封装、鉴权怎么做,都能直接看到。
1.2 项目目录结构与模块规划
我拿到源码后,首先看的就是目录结构。一个结构清晰的项目,能省去很多理解成本。
后端部分大致长这样:
mall-server ├── src/main/java/com/mall │ ├── config // 全局配置:跨域、拦截器、微信参数 │ ├── controller // 接口层,接收前端请求 │ ├── service // 业务逻辑层,核心逻辑都在这里 │ ├── mapper // MyBatis 数据访问层 │ ├── entity // 数据库实体类 │ ├── dto // 请求参数对象 │ └── vo // 返回给前端的视图对象 ├── src/main/resources │ ├── mapper // MyBatis XML 文件 │ └── application.yml // 数据源、Redis、微信配置 └── sql └── init.sql // 数据库初始化脚本前端部分大致长这样:
mall-app ├── pages │ ├── index // 首页 │ ├── category // 分类页 │ ├── cart // 购物车 │ ├── user // 个人中心 │ ├── goods // 商品详情 │ ├── order // 订单确认、订单列表 │ └── address // 收货地址 ├── api // 接口请求封装 ├── utils // 工具函数 ├── static // 静态资源 ├── App.vue ├── main.js ├── manifest.json // 小程序 appid、权限配置 └── pages.json // 页面路由和 tabBar 配置这种分层比较常规,但它符合大多数团队的习惯:controller 只做参数接收和返回,业务逻辑全部放 service,mapper 只负责数据库交互。我看过不少开源项目把业务逻辑写在 controller 里,一个接口几百行,改起来非常痛苦。这套项目至少在分层上没有走偏,对新手来说也更容易读懂。
2. 商城核心业务模块拆解与数据库设计
2.1 用户端与管理端功能清单
商城的核心业务,无论大小,基本都绕不开这些模块:
- 用户端:微信登录、首页轮播、商品分类、商品列表、商品详情、购物车、下单、微信支付、订单查询、售后申请、收货地址管理、优惠券领取
- 管理端:商品上架/下架、分类管理、库存管理、订单发货、退款处理、基础数据统计
这套开源项目一般会包含用户端完整功能和简单管理后台。如果管理后台没做得很完整,也不用太担心,因为商城最核心的链路是“浏览商品→加购物车→下单→支付→发货→收货”,把这套链路跑通,剩下的都是锦上添花。
2.2 数据库表结构与关键字段设计
我把这套项目里比较核心的几张表列一下,字段不必完全照抄,重点是理解设计思路:
用户表(user)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| openid | varchar(64) | 微信用户唯一标识 |
| unionid | varchar(64) | 用户跨应用统一标识,可空 |
| nickname | varchar(64) | 昵称 |
| avatar | varchar(255) | 头像地址 |
| phone | varchar(20) | 手机号,可空 |
| session_key | varchar(64) | 微信会话密钥,敏感,注意加密 |
| status | tinyint | 状态:1 正常,0 禁用 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
商品表(goods)
商品表是电商系统里最核心的表之一。字段一般包括:商品名称、副标题、分类 ID、主图、详情富文本、价格、库存、销量、上下架状态、逻辑删除标记。价格必须用 decimal,不能用 float/double,否则会有精度问题。
SKU 表(sku)
如果商品有规格,比如颜色、尺码,那必须有 SKU 表。一个商品对应多个 SKU,每个 SKU 有自己的价格和库存。有些简单的商城项目没有 SKU 表,商品直接一个价格一个库存,但真正要商用,SKU 是绕不开的。
订单表(order)
| 字段 | 类型 | 说明 |
|---|---|---|
| order_no | varchar(32) | 订单号,唯一索引 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 订单总金额 |
| pay_amount | decimal(10,2) | 实付金额 |
| pay_status | tinyint | 支付状态:0 未支付,1 已支付 |
| order_status | tinyint | 订单状态:待付款、待发货、待收货、已完成、已关闭 |
| address_snapshot | varchar(500) | 收货地址快照 JSON |
| pay_time | datetime | 支付时间 |
| create_time | datetime | 下单时间 |
订单明细表(order_item)
订单明细记录每个订单里的具体商品。关键点是:商品名称、价格、图片、数量都要冗余存储。为什么?因为商品信息后续可能会改,甚至可能被删除,但订单里的快照必须保留,否则用户查历史订单时看到的商品信息会变。
购物车表(cart)
购物车表相对简单:用户 ID、商品 ID、SKU ID、数量、选中状态。但要注意一点:购物车数据可以存数据库,也可以存 Redis。存数据库实现简单,但频繁读写压力大;存 Redis 性能好,但需要考虑持久化和数据一致性问题。这套项目如果用了 Redis,把购物车放 Redis 也是一个合理方案。
2.3 数据库设计里容易被忽略的几个关键点
第一,订单状态和支付状态要分开。很多新手会把这两个字段合成一个,比如“已支付”就算“已发货”,结果后续退款、售后的时候状态根本不够用。分开之后,订单状态管履约流程,支付状态管资金流,逻辑清晰很多。
第二,金额运算全部用分为单位或 decimal。与钱相关的计算,前端展示用元,后端计算用分,避免浮点误差。
第三,库存扣减要用条件更新。比如下单时执行:
update goods set stock = stock - #{num}, sales = sales + #{num} where id = #{id} and stock >= #{num}这样利用数据库行锁保证不会超卖。如果先查库存、再判断、再更新,并发场景下肯定会出问题。
3. 后端 Spring Boot 实现要点与代码还原
3.1 微信小程序登录与 token 体系
商城第一个要解决的问题就是“用户是谁”。微信小程序提供了 wx.login 拿到临时 code,后端拿 code 去微信接口换 openid 和 session_key,然后确定用户身份。
整个流程是:
- 前端调用
uni.login拿到 code - 前端把 code 传给后端
/api/auth/login - 后端用 code 请求微信
jscode2session接口 - 微信返回 openid 和 session_key
- 后端查数据库,用户不存在则自动注册
- 后端生成 token,存 Redis,返回给前端
- 前端把 token 缓存起来,后续请求都带上
我见过不少项目把 openid 直接返回给前端,这是不合适的。openid 是用户在小程序里的唯一标识,相当于用户的身份证号,前端拿到它没有意义,反而增加了泄露风险。正确做法是后端生成一个随机的 token 作为登录凭证,openid 只保存在服务端。
核心代码大致是这个意思:
@PostMapping("/auth/login") public Result login(@RequestBody LoginDTO dto) { // 1. 调用微信接口换取 openid String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; WechatSession session = restTemplate.getForObject(url, WechatSession.class); // 2. 根据 openid 查找用户,不存在则注册 User user = userMapper.selectByOpenid(session.getOpenid()); if (user == null) { user = new User(); user.setOpenid(session.getOpenid()); userMapper.insert(user); } // 3. 生成 token 存入 Redis,设置过期时间 String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set("token:" + token, user.getId(), 7, TimeUnit.DAYS); return Result.ok(token); }拦截器里要校验 token:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); if (StringUtils.isBlank(token)) { throw new BusinessException("未登录"); } Object userId = redisTemplate.opsForValue().get("token:" + token); if (userId == null) { throw new BusinessException("登录已过期"); } request.setAttribute("userId", userId); return true; }这里有个很容易踩的坑:Redis 里存的 key 如果没用前缀区分业务,不同模块之间可能互相覆盖。所以 token 的 key 一定要加上业务前缀,比如token:xxxx。
3.2 商品列表与购物车接口设计
商品模块的接口一般包括:
| 接口 | 方法 | 说明 |
|---|---|---|
| /api/goods | GET | 分页查询商品,支持分类筛选、关键字搜索 |
| /api/goods/{id} | GET | 商品详情 |
| /api/cart/list | GET | 购物车列表 |
| /api/cart/add | POST | 加入购物车 |
| /api/cart/update | PUT | 修改购物车商品数量 |
| /api/cart/checked | PUT | 修改选中状态 |
| /api/cart/delete | DELETE | 删除购物车商品 |
接口设计上要注意:列表接口不要一把梭把所有字段查出来,要分页。详情接口要包含富文本、轮播图、SKU 列表等完整信息,但用不着把浏览量、点击量这些非核心字段全塞进去。
购物车里“选中状态”这个字段容易被人忽略。很多商城购物车是支持勾选一部分商品去结算的,所以购物车表里要有 checked 字段。后端在下单时,只处理选中状态的商品。如果把这个逻辑放到前端处理,用户切换设备后数据就不一致了。
3.3 订单生成与微信支付对接
订单是整个商城里最复杂的链路,也最容易出问题。我把流程拆开写:
创建订单
- 接收请求,从购物车中读取选中的商品,或者直接传商品 ID 和数量
- 校验商品是否存在、是否上架、库存是否充足
- 生成唯一订单号
- 计算订单总金额
- 插入订单表和订单明细表
- 扣减库存
- 清空对应购物车数据
订单号生成有一些讲究。最简单的方案是用时间和随机数拼接,但要注意并发重复。更稳妥的方式是:日期 + 用户 ID + 随机数,或者直接用 Redis 自增序列。订单号要有唯一索引,防止重复。
微信支付
微信支付流程对第一次接触的同学来说有点绕。我来理清一下。
第一步,后端调用微信支付“统一下单”接口,传入业务参数:订单号、金额(单位是分)、用户 openid、支付回调地址。微信返回一个 prepay_id。
第二步,后端拿着 prepay_id 和时间戳、随机字符串、签名一起返回给前端。
第三步,前端用uni.requestPayment调起微信支付面板。
第四步,用户输入密码完成支付,微信服务器异步通知后端的回调地址。
第五步,后端收到回调,先验签,再校验订单号和金额,最后更新订单状态为已支付。
回调处理是重中之重。微信支付回调可能因为网络原因重复发送,所以服务端处理必须幂等。也就是说,同一个订单收到多次回调,最终结果必须一样,不能因为回调两次就把订单状态覆盖成“已发货”或者重复加积分。
我在项目里见过一个很低级的错误:回调里直接把订单状态改成“已支付”,但没判断订单当前状态。结果用户在未支付状态下点了取消,然后微信回调到了,订单状态又变成了已支付,最后用户的订单要人工介入才能处理。正确做法是:只有在订单原状态是“待付款”时才更新为“已支付”,其他情况直接忽略。
回调接口代码大致是这样:
@PostMapping("/api/pay/notify") public String payNotify(HttpServletRequest request) { // 1. 读取回调数据 String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); // 2. 解密验签,确认是微信发的 Map<String, String> params = WxPayUtil.decrypt(body); if (!WxPayUtil.verifySign(params)) { return "fail"; } // 3. 取出订单号和金额 String orderNo = params.get("out_trade_no"); String totalFee = params.get("total_fee"); // 4. 查订单,校验金额 Order order = orderMapper.selectByOrderNo(orderNo); if (order == null || !order.getPayAmount().equals(new BigDecimal(totalFee).divide(new BigDecimal(100)))) { return "fail"; } // 5. 幂等更新 int rows = orderMapper.updatePayStatus(orderNo, 1, 0); if (rows == 0) { return "success"; } // 6. 其他业务:通知发货、加积分等 return "success"; }这里要注意,回调接口返回给微信的必须是success或fail字符串,不能返回 JSON。微信只有收到success才认为通知成功,否则会持续重试。
3.4 并发场景下的库存扣减
如果你只做演示,库存扣减随便写写没关系。但真实商城,超卖是必须解决的问题。
最常见的做法有两个:
第一种是数据库乐观锁。商品表加一个 version 字段,更新时带上 version,version 不匹配就更新失败。但这种方案在库存扣减这种场景下显得绕,而且重试逻辑要自己写。
第二种是条件更新,也就是我前面写的where stock >= #{num}。利用数据库行锁,同一时刻只有一个事务能更新成功,天然防超卖。大多数中小商城用这种方式足够了。
如果你的并发量真的很高,再考虑 Redis 预扣库存,但这套方案的复杂度会大幅提升,普通项目没必要。
4. 前端 uniapp 页面实现与微信小程序适配
4.1 页面结构、tabBar 与分包加载
前端页面结构里,最常见的四个 tab 是:首页、分类、购物车、我的。在pages.json里配置 tabBar,对应这四套页面。
商城应用不是只有这几个页面。商品详情、订单确认、订单列表、支付结果页、地址管理、售后页,这些页面都属于低频访问页面。在微信小程序里,主包体积限制是 2M,超了就得用分包。所以页面规划时,可以把高频页面放在主包,低频页面放分包。
分包有两点要注意:
第一,分包不能嵌套,只能是主包和一级分包这种结构。
第二,页面跳转时,如果目标页面在分包里,用uni.navigateTo传入分包路径即可,不需要额外配置。但如果要访问分包里的组件,那就涉及“分包异步化”的问题,需要配置。
对于商城来说,如果商品图片不多、代码也不复杂,第一阶段不分包也能过,但建议从一开始就做好目录规划,避免后期迁移成本。
4.2 请求封装与登录态处理
前后端分离的核心就是接口调用。uni-app 里用uni.request发起请求,但它是个回调函数,如果不做封装,页面里会写一堆重复代码。
我自己习惯把请求封装成一个 Promise 方法,统一处理 baseURL、token、错误码、加载动画:
// utils/request.js const BASE_URL = 'https://api.example.com' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { token: uni.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { // token 过期,重新登录 uni.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }关于登录态,有一个很常见的坑:小程序里wx.login生成的 code 是一次性的,并且有效期很短,前端不能把 code 存起来备用。每次登录都必须在需要时重新调用。
另外要区分两个“登录”概念:
一个叫静默登录。用户进入小程序,后端自动用 code 换 openid 创建用户,这个过程用户无感知。用户进来就已经是登录状态了。
另一个叫主动授权登录。用户点击“微信登录”按钮,前端调用授权弹窗,拿到用户头像昵称,再调后端接口更新用户资料。
几乎所有商城都建议先做静默登录,不然用户一进来就看到一堆需要登录的提示,体验非常差。
4.3 首页、商品详情、购物车与订单页实现
首页:通常由搜索框、轮播图、金刚区图标、商品推荐列表组成。数据来源是后端接口:轮播图接口返回 banner 列表,商品接口返回推荐商品。首页不需要太复杂,重点是首屏加载速度。图片要压缩,列表要分页,不能一次查几千条数据。
商品详情页:核心数据包括商品信息、轮播图、价格、销量、库存、SKU 规格选择、图文详情。SKU 选择的逻辑稍微麻烦一点:用户选了某个规格,要计算哪些 SKU 是可选的,哪些是缺货下架的。简单做法是遍历所有 SKU,匹配用户当前已选规格,能匹配的显示可点击,否则置灰。
购物车页:除了基本列表,还要处理全选、单选、数量加减、左滑删除。购物车底部的“合计金额”要在前端实时计算,但真正下单时以后端计算为准。前端可以先给用户一个预估,后端做最终校验。
订单确认页:用户从购物车或商品详情页跳过来,展示收货地址、商品清单、运费、总金额,点击“提交订单”后调后端创建订单接口,成功后调起支付。
支付:代码比较简单:
uni.requestPayment({ provider: 'wxpay', timeStamp: res.timeStamp, nonceStr: res.nonceStr, package: res.package, signType: 'MD5', paySign: res.paySign, success: () => { uni.redirectTo({ url: '/pages/order/paySuccess' }) }, fail: (err) => { // 用户取消支付,跳到订单列表让用户重新支付 } })有个细节要注意:package参数的值一定是prepay_id=xxx这个完整格式,很多新手只传了prepay_id,导致调起支付失败。
4.4 微信小程序特有的适配坑
微信小程序虽然上手简单,但适配问题不少。
rpx 和 px 的换算:小程序里 750rpx 等于屏幕宽度,开发时不用担心不同机型。但如果页面里嵌入了第三方 H5 或者原生组件,尺寸单位就还得自己处理。
安全区适配:iPhone X 以后的机型底部有 home 条,页面底部按钮会被遮挡。处理方式是在底部容器加 padding:
padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);图片域名必须配白名单:小程序里<image>标签加载的图片,域名必须在小程序后台配置为“downloadFile 合法域名”,而且必须 HTTPS。开发模式下可以在“详情→本地设置”里勾选“不校验合法域名”,但上线前一定要配置好。
头像昵称填写规则:现在微信不再支持wx.getUserInfo直接弹窗拿头像昵称,而是推荐用button的open-type="chooseAvatar"和input的type="nickname"让用户主动填写。如果你还在用老方法,审核大概率会挂。
隐私协议弹窗:微信对用户隐私的管控越来越严,小程序中用到的隐私接口要先声明。到时间节点后,强制要求隐私弹窗授权,商城这种收集用户昵称头像的手机号的项目,一定要提前把隐私政策页面做好。
5. 本地搭建、部署上线与二次开发实操指南
5.1 从拉取源码到本地跑通前后端
我按照实际流程走一遍,告诉你怎么在本地把项目跑起来。
第一步,准备环境:安装 JDK 1.8+、Maven、MySQL 5.7+、Redis、HBuilderX、微信开发者工具。
第二步,导入数据库。项目里一般会有sql/init.sql或者doc/init.sql,在 MySQL 里执行,创建数据库和表。如果提供了初始化数据,那就更好,首页不至于一片空白。
第三步,修改后端配置。打开application.yml,把数据源地址、账号、密码改成你自己的,Redis 地址也改一下。如果有微信相关配置,先填上测试用的 appid 和 secret(没有的话,登录功能暂时不可用,但其他接口能测)。
第四步,启动后端。在项目根目录执行:
mvn spring-boot:run后端起来之后,先访问一下接口文档,比如http://localhost:8080/api/goods,能返回 JSON 就说明环境没问题。
第五步,用 HBuilderX 导入前端项目。在manifest.json里配置小程序 appid,然后运行到微信开发者工具。
第六步,微信开发者工具打开后,点击“详情→本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这是因为本地调试用的通常是 http 地址,微信默认禁止。
到此,前后端就打通了。
5.2 前后端分离联调与接口自测
前端开发中用的接口地址和后端本地地址不一样,怎么解决?两种方式。
第一种,在request.js里把 baseURL 拆成多个环境变量:
const ENV = 'dev' const BASE_URL = { dev: 'http://localhost:8080', prod: 'https://api.example.com' }[ENV]第二种,用 HBuilderX 里常见的方式:开发环境请求走本地局域网 IP,比如http://192.168.1.100:8080,真机调试时手机和电脑在同一 Wi-Fi 下就能访问。
接口自测推荐用 Apifox 或 Postman。先把后端接口导出来,所有接口测通过之后再联调前端,能省非常多时间。
5.3 生产环境部署
本地跑通只是第一步,上线才是真正的考验。上线流程大概是:
后端打包:
mvn clean package -DskipTests生成 jar 包后,上传到服务器,用 Java 命令启动。更推荐的方式是写一个 Dockerfile 或直接用 docker-compose,把 MySQL、Redis、后端服务一起编排起来。一个小型商城用 docker-compose 管理三个容器就够了:
version: '3' services: mysql: image: mysql:5.7 restart: always environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: mall volumes: - ./sql:/docker-entrypoint-initdb.d redis: image: redis:6 restart: always server: build: . restart: always ports: - "8080:8080" depends_on: - mysql - redisNginx 负责反向代理和 HTTPS 配置。小程序要求所有请求域名必须是 HTTPS,所以你需要一个已备案的域名,并配置 SSL 证书。Nginx 配置里把/api的请求转发到内网的 8080 端口。
前端打包:在 HBuilderX 里选择“发行→小程序-微信”,生成微信小程序代码包,在微信开发者工具里打开,点击“上传”,然后到微信公众平台提交审核。
还有图片存储的问题。本地存储图片在联调时没问题,但生产环境建议用对象存储服务,比如阿里云 OSS、腾讯云 COS,或者七牛云。商品图片、用户头像都放到对象存储,后端只存 URL。否则服务器磁盘会很快被打满,而且图片加载速度也会拖慢用户体验。
5.4 基于开源项目的二次开发建议
拿到开源项目后,怎么改才能不踩坑?我的经验是:先读懂,再动手。
第一步,把“用户登录→浏览商品→加购物车→下单→支付”这条主链路完整跑通,边跑边打断点,搞清楚每次请求经过哪些类、哪些方法。
第二步,不要上来就重构。开源项目的代码风格可能不符合你的习惯,但它能跑。等你完全理解每一行代码为什么这么写之后,再决定要不要改。
第三步,从一个小需求开始实践。比如给商品列表加一个“新品”标签,或者给订单列表加一个“取消订单”按钮。通过改一个小功能,你能快速熟悉项目的代码习惯和扩展方式。
常见的扩展方向:
- 优惠券模块:后台发券、用户领券、下单抵扣
- 秒杀模块:Redis 预扣库存 + 限流 + 异步下单
- 分销模块:用户推广关系绑定、分销佣金结算
- 多商户支持:把商品表加一个商户 ID,订单结算时拆单
这些都是很成熟的场景,网上也有大量参考实现。
6. 常见问题与排坑记录
6.1 真机预览时请求失败
表现:开发者工具里页面正常,手机预览时首页空白、数据加载不出来。
原因基本是这么几个:
- 前端请求的 baseURL 是
http://localhost,手机访问的是电脑的 localhost,当然是通的。要改成局域网 IP。 - 后端没监听
0.0.0.0,只监听了本地回环地址,局域网访问不到。Spring Boot 默认是0.0.0.0,一般不会出这个问题,但如果你用了自定义配置,确认一下。 - 手机和电脑不在同一个网段,防火墙拦截了端口。
排查方法:先用手机浏览器访问一下http://电脑IP:8080/api/goods,能通说明后端没问题,问题在前端配置。
6.2 支付回调不触发或重复触发
回调不触发,去查这么几项:
- 回调地址是不是公网可访问?本地开发没有公网地址,微信服务器根本调不到。可以先用内网穿透工具暴露本地端口。
- 回调地址的路径和后端代码是否完全一致?
- 回调地址是不是 HTTPS?微信支付回调要求 HTTPS(本地调试时可以临时用 HTTP 但生产必须 HTTPS)。
回调重复触发是正常现象,微信会重试,直到收到success。所以回调逻辑必须幂等。
签名验证失败,先检查密钥是否配置正确,再检查参数拼接顺序是否和微信文档一致。不同语言的签名实现细节不同,Java 里尤其要注意空值和大小写。
6.3 登录态突然失效
用户操作到一半被弹回登录页,体验非常糟糕。常见原因:
- token 过期时间太短。如果只是个小商城,token 有效期设 7 天比较合适。
- Redis 数据丢失。Redis 如果没有持久化,重启后所有 token 都没了,用户全部掉线。生产环境要开启 AOF 或 RDB 持久化。
- 前后端时间不一致。如果用了 JWT,签名验证时需要对比时间,服务器和客户端时间偏差大会导致 token 提前失效。
6.4 小程序提审被拒
小程序审核被拒,最常见的几条原因:
- 类目选择不对。商城类小程序需要选择“电商平台”或“商家自营”类目,并上传相应资质。
- 涉及虚拟商品支付。微信对虚拟支付管控很严,如果商城卖的是会员、课程、虚拟币这类虚拟商品,个人主体基本过不了。解决方案是走微信的小程序虚拟支付能力,或者做成 H5 支付。
- 没有隐私政策。小程序里收集用户信息,必须有明确的隐私政策弹窗,说明收集了哪些信息、用来做什么。
- 需要测试账号。如果管理后台登录需要账号密码,提审时要在后台配置体验账号,否则审核员进不去。
6.5 常见问题速查表
| 问题 | 原因 | 解决思路 |
|---|---|---|
| 真机加载不出图片 | 图片域名没有加入 downloadFile 白名单,或不是 HTTPS | 后台配置白名单,图片改成 HTTPS |
| requestPayment 提示参数错误 | package 参数少了prepay_id=前缀 | 检查参数拼装格式 |
| 下单提示库存不足 | 库存扣减逻辑写成了先查后改 | 改成条件更新 SQL |
| 回调后订单还是待付款 | 回调里没做幂等处理,订单状态被后续请求覆盖 | 加状态判断,只有待付款才更新 |
| 购物车数据丢失 | 购物车只存在 localStorage,用户换设备就没了 | 购物车数据同步到后端 |
| 支付金额少了 0.01 | 前后端金额单位不统一 | 统一用分传给微信支付,展示时转元 |
| 页面跳转后数据没刷新 | 页面 onLoad 只执行一次,再次进入时走了缓存 | 在 onShow 里重新拉取数据 |
6.6 我的几点避坑经验
最后说几个我在实际开发中悟出来的经验,不算高深,但很实用。
第一,商城项目里,订单状态机的设计一定要想清楚再写代码。把“待付款→待发货→待收货→已完成”这条主链路和“已取消、售后中、已退款”这些分支画清楚,后面加功能、改 bug 都会轻松很多。我见过太多项目因为状态机混乱,改一个状态影响一片逻辑。
第二,日志必须打足。尤其是支付回调、订单状态变更这种关键节点,每一步都要有日志。出问题的时候,没有日志你会非常痛苦。
第三,不要把开源项目的代码当成完美的。开源项目解决的是通用问题,很多细节是参差不齐的。你在二次开发时,一定要自己把“登录→下单→支付”这条链路的关键逻辑完整读一遍,确认没有明显漏洞再上线。尤其是涉及钱的逻辑,怎么小心都不为过。
这套 Java + uni-app 的开源商城项目,像一套可以正常运转的样板间。你可以直接入住,也可以照着它的格局重新装修。最怕的是拿到源码之后一上来就这里改改那里删删,结果改了三天发现项目跑不起来了。从读懂到改好,中间没有捷径,但沿着“主链路走通→读关键代码→小改动试水→完整功能开发”的路线,收益会是最稳的。
本文还有配套的精品资源,点击获取