多商家商城这个项目,说实话,外面教程一搜一大把,但大多数都是拿个单商家Demo改个字段就出来忽悠人。真把“多商家”这三个字落地,你才会发现商品表加个store_id只是万里长征第一步。权限模型怎么分、订单怎么按店铺拆、佣金怎么结算、商家后台和平台后台怎么隔离,每一个点都能让你加班到怀疑人生。这次我就拿 uniapp + SpringBoot 这套组合,把微信小程序端的多商家购物商城整个架子从头到尾拆一遍,包括我实际开发中踩过的坑、改过的设计、上线时被卡过的审核,全写出来。
这套方案最适合谁?一个是正在准备毕业设计的学生,想做一个看起来完整、答辩能讲清楚的项目;另一个是刚入行的后端开发,想搞明白多商户系统的权限和数据隔离到底怎么设计;还有就是产品经理或者独立开发者,想低成本验证一个平台型电商的想法。不管你属于哪类,看完这篇文章,至少能少走我两个月的弯路。
1. 项目定位:这个多商家商城到底在做什么
1.1 多商家商城与单商家商城的本质差异
很多人对多商城的理解就是“商品表里加个商家ID”,这种思维做出来的东西,顶多算个带店铺名称的单商家商城。真正的多商家平台,你要同时服务三类角色:平台运营方、入驻商家、C端消费者。平台负责审核商家、管理类目、处理纠纷、抽成结算;商家自己管理商品、库存、订单发货、售后;消费者只关心能不能逛得爽、买得方便。
这就意味着你的系统至少要拆成三个端:用户端微信小程序、商家端管理后台、平台端管理后台。小程序端用 uniapp 做,两个管理后台我建议直接上 Vue3 + Element Plus 或者 React + Ant Design 这种传统Web方案,别再拿 uniapp 硬扛后台了,H5在PC上的表单交互体验真不如正经Web框架。
数据隔离是另一个核心问题。商品数据要按 store_id 隔离,订单数据要按 store_id 隔离,甚至商家的登录 token 都不能跟用户端混在一个表里。我见过有人图省事,把商家和用户放同一张 user 表,用 role 字段区分,结果后面加权限、做分账、算佣金的时候,SQL 越写越痛苦,最后重构花了三周。
1.2 技术选型:为什么是 uniapp + SpringBoot
先说前端。uniapp 这套跨端框架,最大的价值不是“一套代码到处跑”这种广告语,而是它帮你把微信小程序的坑提前填平了。比如微信小程序的登录流程、支付跳转、分包加载、底部TabBar,这些在 uniapp 里都有封装好的API,你写一遍,编译到微信小程序能跑,以后想额外出个H5版本或者安卓App,成本也低。
选 SpringBoot 更不需要犹豫。它的生态太成熟了,mybatis-plus 操作数据库、spring-security 或者 sa-token 做鉴权、spring-boot-starter-data-redis 做缓存,全是现成的轮子。遇到任何问题,搜索引擎一搜一大把解决方案,这对个人开发者和学生来说太重要了。你选个冷门框架,出了问题连问的人都没有。
版本这块我提醒一句,SpringBoot 3.x 虽然已经普及,但它要求 JDK 17,而且 javax 包名改成了 jakarta。如果你之前从没接触过 SpringBoot,直接上 2.7.x 配 JDK 8 是最稳的组合,资料最多、坑最少。后面熟练了再考虑升级,别一上来就用最新版给自己添堵。
1.3 三类角色的权限与场景拆分
权限设计是这种平台型系统里最先要定下来的事。我的做法是平台端和商家端彻底分离:平台管理员一套账号体系,用 Spring Security 做RBAC权限控制,管理员分为超级管理员、类目运营、客服等角色;商家走单独的商家账号体系,一张 merchant_user 表,关联 merchant(店铺)表,一个店铺可以绑定多个子账号,比如店长、客服、仓库管理员。
用户端小程序则是另一套逻辑,用微信登录后生成自己的 token,跟商家、管理员的token完全隔离。三个端共用一个后端服务没问题,但 Controller 层要分目录、分路径前缀:/api/user/、/api/merchant/、/api/admin/**,分别走不同的拦截器和权限校验逻辑。这样代码结构清晰,以后就算要把商家端和平台端拆成独立服务,也只需要把目录搬走就行。
2. 前端核心实现:uniapp 的多端适配与微信小程序落地
2.1 项目结构与页面规划
uniapp 的项目结构,pages.json 是灵魂。底部TabBar我配了四个:首页、分类、购物车、我的。第一版的时候我加了五个Tab,把“店铺”也塞进去了,后来发现用户习惯还是京东淘宝那套,店铺入口放在商品详情页和搜索结果页就够了。在这里建议所有做电商的朋友,UI层面尽量贴着主流电商的用户习惯走,别自己发明交互。
首页不要全用静态数据糊弄。我当时是首页放了一个自定义导航栏,下面的轮播图、金刚区图标、秒杀倒计时、商品瀑布流,全部是接口从后端拉的。瀑布流这里有个细节,小程序里图片高度不固定,用 columns 两列布局的时候,左边一列和右边一列单独维护数组,根据图片实际高度决定下一条数据加到左边还是右边,不然会出现一边特别长一边特别空的“锯齿”。
商品详情页是另一个大头。我把 SKU 选择器做成了半屏弹窗,规格数据格式 { "颜色": ["黑", "白"], "内存": ["128G", "256G"] },后端存的SKU是扁平结构,前端用笛卡尔积组合出规格矩阵,再根据用户的选择实时匹配库存。这里有个性能问题,如果你的商品规格超过三组,笛卡尔积可能会生成几十上百个组合,建议后端只返回实际存在的SKU组合,前端拿数组做匹配,而不是用穷举法去猜。
2.2 微信登录:code、openid 与 token 的真实链路
微信小程序登录,网上说法五花八门,但真正的链路就一条:前端 uni.login 拿到临时 code,把 code 发给你的后端,后端拿着 code + appid + secret 去调微信的 jscode2session 接口,换回 openid 和 session_key,然后你自己生成一个业务token返回给前端。
这里要特别注意,很多新手会把 wx.getUserProfile 当成登录,其实那个只是获取用户头像和昵称,已经不能用来做登录凭证了。真正的登录态是 openid 对应的用户身份。头像昵称你可以引导用户手动填,或者用 button 的 open-type="chooseAvatar" 让用户选择头像,用 input 的 nickname 输入昵称,这是现在微信小程序的合规做法,强行调 getUserProfile 弹窗,审核大概率会被拒绝。
后端拿到 openid 后,直接查 user 表,有就更新最近登录时间,没有就插入一条新用户记录。然后我用 hutool 的 JWTUtil 生成了一个 token,有效期设了7天,存到 Redis 里,key 是 token,value 是 userId。后续前端每次请求带上 token,后端从 Redis 查用户信息,查不到就返回 401 让前端重新登录。
2.3 定位、分享、扫码这些高频能力的实现细节
定位这个功能,如果你只是在小程序里用,直接 uni.getLocation 就行。但前提是你得在微信公众平台后台申请地理位置接口权限,类目不同审核要求也不一样,有些类目还需要提交用途说明。这步很多人忽略,结果真机上一直报错 getLocation:fail no permission。
如果你想把同一个 uniapp 项目编译成 H5 嵌进微信公众号里用,定位的逻辑就完全不同了。H5 在微信浏览器里必须要走 JS-SDK 的定位接口,需要后端生成签名,前端引入 jweixin 模块,先 wx.config 再 wx.getLocation,而且还要在公众号后台绑定JS接口安全域名。代码里要做一个环境判断:编译器是 MP-WEIXIN 就走小程序定位,是 H5 就判断是不是在微信浏览器里,再决定走 JS-SDK 还是浏览器的 Geolocation。
分享功能也有讲究。小程序自定义转发,在页面里写 onShareAppMessage 就可以,重点是返回的 path 要带上参数,比如拼上商品 id 和分享人 id,这样别人点开你的分享卡片进到小程序,就能实现锁客和分销溯源。分享朋友圈是 onShareTimeline,但这个功能个人主体小程序用不了,只有企业主体能开。
2.4 uniapp 打包微信小程序的完整流程与配置
uniapp 打包微信小程序,第一步是在 manifest.json 的“微信小程序配置”里填你的 AppID。这个 AppID 要去微信公众平台注册小程序账号拿,个人主体和企业主体都能注册,但电商类目基本都要求企业主体。填完 AppID 后,在 HBuilderX 里点“运行到小程序模拟器”,项目会自动编译生成 dist/dev/mp-weixin 目录。
把这个目录用微信开发者工具打开,就能看到小程序跑起来了。但这里要区分“运行”和“打包”的概念:运行模式是为了本地调试,代码没有压缩,体积也大;真正要上传审核,得点“发行”——“小程序-微信”,生成 dist/build/mp-weixin,这才是生产包。
打包前还有两个配置容易被忽略。一个是 manifest.json 里的“基础库最低版本”,不要设太高,否则老版本微信用户打不开,我一般设 3.0.0 左右;另一个是本地存储和分包,如果你的小程序主包超过 2MB,就必须做分包,把店铺页、商品详情页、订单详情页放到 subPackages 里,首页、TabBar 页面留主包。
小程序后台还要配置服务器域名,request 合法域名必须是 HTTPS 且已备案的。调试的时候可以在开发者工具里勾选“不校验合法域名”,但真实用户设备不勾,上线前一定要配好,不然所有接口全挂。
2.5 vue2 转 vue3 常见迁移问题
现在 HBuilderX 默认创建的项目很多还是 vue2 语法,但 vue3 是趋势。如果你拿到一个 vue2 的 uniapp 项目要转 vue3,最直观的变化就是生命周期函数:onLoad、onShow 这些页面生命周期在 vue3 里要按需导入,比如 import { onLoad } from '@dcloudio/uni-app'。然后是 data 改成了 ref 和 reactive,methods 里的函数要定义在 setup 里返回回去。
有一个迁移时必踩的坑:vue2 里的 this.$emit 触发父组件事件,在 vue3 里要改成 setup 的第二个参数 context.emit,或者直接用 defineEmits。还有全局事件总线,vue2 用 uni.$emit 和 uni.$on,vue3 里这个API还能用,但如果你用的组合式API,官方更推荐用 mitt 之类的库自己维护一个事件总线。
从项目管理角度,我建议新项目直接上 vue3 语法,别在 vue2 上开新坑。虽然资料比 vue2 少一点,但 vue3 的性能和代码组织方式确实更好,而且你再过半年看,vue2 相关的插件和生态会越来越少。
3. 后端核心设计:SpringBoot 里的多商家数据与订单体系
3.1 多商家数据结构:店铺维度与商品维度的关系建模
后端的数据结构是整个项目的基石。店铺表 store 我先设计:store_id、store_name、logo、banner、description、status(0待审核 / 1正常 / 2冻结)、commission_rate(平台抽佣比例)。这里佣金比例设在店铺维度,运营可以针对不同商家单独调整,比写死在代码里灵活得多。
商品表我拆成了 spu 和 sku 两层。goods_spu 表存的是商品公共信息:spu_id、store_id、category_id、goods_name、main_image、detail_images、status。goods_sku 表存的是具体的可售规格:sku_id、spu_id、specs(存JSON,比如 {"颜色":"黑","内存":"128G"})、price(存的是整数分,千万别存小数)、stock、sales_count。为什么要拆两层?因为一个商品多个规格,价格和库存是挂在 SKU 上的,如果不拆,每个规格都要重复一遍商品标题和图片,数据冗余得一塌糊涂,而且后期改商品详情要 update 好几条记录,极容易出bug。
类目表可以设计成两级,parent_id 为0的是顶级类目。类目不建议做成无限极,两级足够覆盖绝大多数电商场景,无限极树在后台管理里维护成本很高,用户选择体验也差。
3.2 下单拆单:一个购物车跨店结算怎么处理
多商家最核心的一个逻辑,就是购物车结算时的拆单。用户购物车里可能同时有 A 店和 B 店的商品,下单时如果生成一个大订单,A 店发货和 B 店发货没法独立操作,退款也没法分开退。所以我的方案是:前端点击“去结算”时,后端收到购物车商品列表,按 store_id 分组,每个店铺生成一个主订单(order),主订单下再挂这个店铺的具体商品明细(order_item)。
订单号生成有个小技巧,我用的格式是时间戳 + 用户ID尾号 + 随机数,比如 20250607153012345 + 001 + 789,这样既有时效性又不至于太长。还有一点,订单表里我冗余了一个 store_name 字段,虽然违反第一范式,但查询订单列表时不用再去 join 店铺表,性能上划得来。这类平台型项目,适当冗余是合理的。
拆单还有一个要注意的地方:订单金额。每个店铺的子订单要单独算商品总额、运费、优惠,最后才是用户实付总额。优惠券如果设计成平台券,可以跨店分摊;如果设计成店铺券,那只能抵扣对应店铺的子订单。第一版建议只做平台券,分摊规则简单,不然优惠计算这块能让你算到崩溃。
3.3 微信支付接入与回调幂等
微信支付在小程序里的链路是:后端先调用微信支付统一下单接口,拿到 prepay_id,返回给前端,前端用 uni.requestPayment 拉起支付面板。用户支付成功后,微信服务器会回调你配置的支付通知地址,这个地址必须是 HTTPS。
支付回调的逻辑要格外小心,因为回调可能因为网络问题重复发送,你的处理函数必须幂等。我的处理方法是:回调进来后先验签,用微信支付平台证书验证签名;验签通过后,根据 out_trade_no(也就是你自己的订单号)查订单,如果订单状态已经是“已支付”,直接返回成功应答,不再重复处理;如果是“待付款”,才更新订单状态为“已支付”,并记录微信的 transaction_id。
代码层面,我用了 wechatpay-java 官方SDK,配置好商户号、APIv3密钥、商户证书序列号,调用起来很省心。这里提醒一个坑:商户平台下载的 apiclient_key.pem 一定要放在服务器安全目录,千万别提交到 Git 仓库,更别写死在代码里,否则拿到你证书的人可以直接拿你的商户号发起退款。
3.4 库存扣减与超卖防护
秒杀场景是电商绕不开的,但秒杀不是你现在最紧迫的问题。你要先解决的是普通购买下的超卖:两个用户同时下单,都读到库存只剩1件,结果都扣减成功了,这就是超卖。
我的解决方案很简单:在 SQL 层面加库存条件。UPDATE goods_sku SET stock = stock - #{num} WHERE sku_id = #{skuId} AND stock >= #{num}。这条 SQL 如果影响行数为0,说明库存不足,直接提示用户。这里的核心逻辑是数据库的行锁机制,同一时刻只有一个事务能更新这一行,别的请求会被阻塞等待,不会出现超卖。但要注意,这条 SQL 所在的 service 方法一定要加 @Transactional,否则更新一半出异常,数据就乱了。
订单创建和库存扣减的顺序也很重要。我的流程是:先预扣库存,再创建订单,再调支付;如果用户超过15分钟未支付,定时任务把订单状态改为已取消,同时把库存回补。这里回补库存的时候也要判断,如果用户取消时商品已经被其他订单锁定了,那还要考虑库存预占的释放逻辑,第一版可以简单处理成直接加回去就行。
3.5 SpringBoot 版本陷阱与配置注意事项
SpringBoot 版本这块,我自己的项目用的是 2.7.18,JDK 8。为什么要守住这个版本?因为 3.x 系列不只是换了 JDK,还有一堆第三方组件的兼容问题。比如 mybatis-plus 对 SpringBoot3 的支持,需要引入 mybatis-plus-spring-boot3-starter 这个独立的包,你如果还是用老的 mybatis-plus-boot-starter,启动直接报错。还有 springfox 的 Swagger 在 SpringBoot3 里也不能直接用,得换 springdoc。
配置文件里还有几个容易踩的坑。数据库连接池我用的 HikariCP(SpringBoot 默认),连接超时和最大连接数一定要根据服务器配置调,一个 2C4G 的服务器我设置了 maximum-pool-size: 10,多了浪费内存,少了高并发会排队。Redis 我主要用来存用户的 token 和秒杀的商品缓存,spring.redis.timeout 记得设置。文件上传我用的本地磁盘路径,配置了一个 upload.dir 自定义属性,转换层通过 @Value 注入,后续要切 OSS 也只是改一个实现类的事。
那个搜索热词“springboot版本太高”,我猜就是很多人新建项目时选了 3.3 或者 3.4,然后发现一堆老教程的代码跑不起来。解决办法我在第4章再展开,这里先给结论:新手做项目,锁定 2.7.x 就是最稳的。
4. 上线部署与高频问题排查
4.1 后端从本地到服务器的部署步骤
部署这块,很多学生一听到“部署服务器”就发怵,其实流程非常固定。第一,在服务器上装好 JDK 8 和 MySQL 8.0、Redis;第二,把项目的 application-prod.yml 里的数据库地址、Redis地址改成服务器的内网IP;第三,本地执行 mvn clean package -DskipTests 打成 jar 包,用 scp 传到服务器;第四,用 nohup java -jar your-project.jar > app.log 2>&1 & 启动,日志输出到文件里,方便排查问题。
前后端联调时还有一个关键点:接口域名。小程序端不能直接请求 IP,必须是 HTTPS 域名。我用的方案是 Nginx 反向代理:买一个域名,解析到服务器IP,配置 SSL 证书,然后在 Nginx 里把 /api/ 路径转发到 localhost:8080。这样小程序端请求 https://api.yourdomain.com/api/user/login,实际上访问的是服务器的 8080 端口。
这里还有一个经验,Nginx 转发的时候要注意请求体大小限制,微信支付回调有时候body会比较大,client_max_body_size 默认1M,建议调到 10M。另外超时时间 proxy_read_timeout 也要设置,不然上传图片这种耗时接口容易被 Nginx 拦腰砍断,返回504。
4.2 微信小程序审核与类目资质
小程序开发完只是第一步,上线审核才是真正的考验。电商类目在微信公众平台属于特殊类目,个人主体基本做不了网上商城,你需要企业主体的营业执照。如果你做的是平台型多商家商城,微信一般会要求提供《增值电信业务经营许可证》(ICP许可证),没有这个证,线上支付、商家入驻这些功能会被打回。
我的建议是:如果你只是想展示项目或者作为毕设演示,可以先用“商家自营”类目提交审核,卖自己的商品,不要在小程序里明显表现多商家入驻、平台抽佣这些功能。等以后真的注册公司、办下资质了,再升级成平台型。这里不是让你违规,而是审核策略上的取舍,项目跑起来、验证商业模式比一开始就碰资质要务实得多。
审核打回最常见的原因有三个:一是没有用户隐私保护指引,这个要在小程序后台配置用户隐私保护协议,把收集的信息项写清楚;二是类目和页面功能不匹配,比如你填的是“商家自营”但页面里出现“入驻开店”入口;三是虚拟支付问题,小程序里不能做虚拟商品的微信支付,知识付费、充值会员这类要绕开。
4.3 高频搜索问题实录:抓包、导航栏、scheme跳转、NFC
把搜索热词里出现频率最高的几个问题集中说一遍。
Charles 抓包微信小程序。Windows 上先配置代理端口 8888,手机要跟电脑同一局域网,设置代理指向电脑 IP。但安卓7.0以上系统默认不信任用户的 CA 证书,所以你会发现 HTTPS 的包全是乱码或者 CONNECT 失败。解决办法是:把 Charles 的证书安装后,用 adb 命令把证书移动到系统证书目录(需要 root),或者用 Android 模拟器(很多模拟器可 root)。iOS 相对简单,下载描述文件后,在设置里“关于本机”——“证书信任设置”里手动开启完全信任。
微信小程序顶部导航栏高度。不要写死 44px,小程序的导航栏在不同机型上高度不一样,尤其是有刘海屏和胶囊按钮的机型。正确的做法是用 uni.getSystemInfoSync() 拿到 statusBarHeight(状态栏高度),再用 uni.getMenuButtonBoundingClientRect() 获取胶囊按钮的位置信息,导航栏高度 = (胶囊按钮的 top - 状态栏高度) * 2 + 胶囊按钮的高度。这个公式我是反复测量多个机型验证过的,把导航栏高度做成动态计算,嵌入H5时也不会错位。
微信小程序跳转 weixin://dl/business 这类 scheme。这个热词很危险,很多人试图在小程序里跳转到外部App或者拉起微信支付业务之外的东西。实际上小程序出于安全限制,你通过 web-view 里的 H5 页面跳转 weixin:// 开头的 scheme,大部分会被系统拦截。就算你用小程序自带的能力,目前支持比较成熟的也只有客服会话、打开半屏小程序、跳转关联公众号这些。想唤起第三方App,必须走微信开放平台的 AppLink/URL Scheme 服务,而且要有企业认证。如果你只是想让用户从H5跳转到小程序,直接用小程序 URL Link 或者 URL Scheme 接口生成链接就行,调的是微信官方接口,需要 appid 和 secret。
uniapp 集成 NFC。想做NFC读卡功能,在小程序里真的非常受限。微信小程序有一个 NFC 相关的API,可以在支持 NFC 的安卓手机上读取 ISO14443 等类型的标签,但 iOS 小程序完全不支持。如果你做的是App端,可以用 uni-app 的 uni.startNFC 之类的原生插件,但需要去插件市场购买或自己写原生代码。我的建议是:如果不是硬需求,这块先放一放,NFC 在小程序生态里的优先级非常低,投入产出比不高。
5. 这套架构还能怎么延伸
项目做完之后,你会发现这套多商城的架子其实就是个“平台底座”,很多业务都能往上长。
秒杀模块。商品表加一个 seckill 活动表,配置开始时间、结束时间、秒杀价、秒杀库存,用 Redis 预扣库存 + 异步写订单的方式扛流量。这个模块做到位,你项目的技术含量直接从“增删改查”跳到“高并发设计”。
分销裂变。用户表加一个 inviter_id 字段,分享进来的用户在下单时,根据分享链接里带的 inviter_id,给邀请人计算佣金。这个功能在营销端很常见,而且改动量很小,对电商项目来说特别加分。
商家结算。订单完成7天后,根据店铺的 commission_rate,算出平台应收佣金和商家应得货款,生成结算单,商家后台可以查看和提现。这个模块一加,你才敢说自己是“平台型电商”。
多端复用。uniapp 编译到 H5 后,可以嵌到微信公众号里;编译到 App,可以上架安卓应用市场。代码大部分是复用同一个业务逻辑的。我会优先做 H5,毕竟公众号生态不需要应用商店审核,上线最快。
这几点我建议你按顺序做,先做结算,再做分销,最后碰秒杀。因为前两个核心是业务逻辑,对你理解平台运营有帮助;秒杀更多是技术挑战,适合作为进阶练习。
最后再分享一个实际开发生涯里的体会。很多人在做这类项目时,容易陷入“把教程跑通就等于会了”的误区。真正让你成长的,往往是那些文档里没有的东西:一个订单状态机的异常流转要怎么兜底,一个商家冻结后他的商品该怎么下架,一个支付回调超时该怎么补偿。这些边界情况,才是你在答辩、面试、实战中真正能拿出来讲的东西。我也是在做完多商家商城,被这些业务细节反复蹂躏过之后,才真正理解了什么叫“系统设计”。