ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

萤火商城v2.0.8多端版商业落地:一套代码支撑小程序、H5和App

萤火商城v2.0.8多端版商业落地:一套代码支撑小程序、H5和App

简介:萤火商城 v2.0.8 多端版是一套面向中小型电商创业者、独立开发者与二次开发学习者的轻量级前后端分离电商系统源码,支持微信小程序、H5、公众号与 APP 多端运行,前后端代码 100% 开源,所见即所得,便于快速搭建个性化独立商城。技术栈采用 PHP7.4 + ThinkPHP6.0 + Uni-APP + Ant Design Vue,兼顾轻量、高性能与可持续稳定,可学习亦可商用。资源包共约 2000 个文件,压缩后 17.34MB,以 1175 个 PHP 后端逻辑文件、155 个 JS 脚本、46 个 CSS 样式、48 个 JSON 配置、32 个 HTML 页面及 136 个 PNG、45 个 GIF 等图片资源为主,另含 SQL 建表脚本、Markdown 说明文档与字体文件,目录结构完整清晰。目前已有 267 人学习下载,适合希望研究多端商城架构、进行二次开发或搭建独立商城的开发者参考,可从中掌握前后端分离项目的组织方式与核心业务实现思路。

1. 萤火商城 v2.0.8 多端版商业落地:一套代码如何同时喂饱小程序、H5 和 App

如果你手里正捏着一个「一套后端要同时撑起微信小程序、H5、App 三端」的商城需求,又不想为每个端各养一个前端团队,那萤火商城 v2.0.8 多端版这个方向大概率会被你翻出来。它本质上是把「多端复用」和「商业闭环」这两件事绑在一起做:后端一套 API,前端按端拆包,营销、订单、支付、分销这些商业模块共用同一套领域模型。我最早接触这类多端商城是在一个虚拟项目里,当时团队只有三个人,却要交付小程序加 H5 两个入口,最后就是靠这种「后端统一、前端分端」的结构把工期压下来的。这篇不聊虚的,只讲这套东西怎么跑起来、参数怎么配、哪些地方最容易翻车。

2. 多端版的分层结构:为什么不是「一套代码编译到所有端」

2.1 后端统一 API 与前端分端打包的边界

多端版最容易让人误解的一点,是以为「多端」等于「一份前端代码编译出所有端」。真做过就知道,小程序和 H5 在登录态、支付、路由、甚至图片懒加载上的差异,硬用条件编译抹平,后期维护成本会指数级上升。萤火商城 v2.0.8 这类方案通常采用的分层是:后端提供一套 RESTful 或类 RESTful 的 API,前端按端维护各自的页面壳和交互,但共享请求层、状态管理和业务工具函数。

具体到目录结构,常见做法是后端一个服务,前端一个仓库里按platform分目录,或者用 monorepo 管理多个包。下面是一个典型的后端 API 分层示意,用 Python 伪代码表达,重点看它怎么把「商业逻辑」和「端差异」隔开:

# 后端 API 分层示意:商业逻辑与端适配解耦 class OrderService: """订单领域服务,所有端共用同一套下单、拆单、计价逻辑""" def create_order(self, user_id, cart_items, platform): # 1. 统一校验库存与价格,不区分端 self._check_stock(cart_items) total = self._calc_total(cart_items) # 2. 平台差异只影响「支付方式」和「回调地址」这类外围参数 pay_channel = self._resolve_pay_channel(platform) order = Order(user_id=user_id, total=total, platform=platform) # 3. 落库后返回统一订单号,由前端各自决定跳转 return self.repo.save(order), pay_channel def _resolve_pay_channel(self, platform): # 小程序走微信支付,H5 走聚合支付,App 走原生 SDK 回调 mapping = {"mini": "wxpay", "h5": "h5pay", "app": "apppay"} return mapping.get(platform, "h5pay")

这段代码的关键不在语法,而在platform这个参数只用来决定支付通道和回调,不参与价格计算和库存扣减。很多团队翻车就翻在这里:把「小程序专享价」写进订单服务,结果 H5 端下单时价格对不上,排查半天发现是平台判断写反了。参数上,platform建议用枚举而不是字符串,避免"mini"和"miniprogram"混用导致支付通道取不到值。

2.2 商业模块的领域模型怎么切

「商业」这个词在标题里不是装饰。萤火商城 v2.0.8 多端版要撑起的商业能力,通常包括商品、订单、支付、退款、优惠券、分销、拼团、秒杀这几块。切领域模型时,我一般会按「交易主链路」和「营销旁路」分开:交易主链路(商品→购物车→订单→支付→售后)必须强一致,营销旁路(优惠券、拼团、分销)允许最终一致。

下面这张表是我在虚拟项目里整理的核心表职责划分,可以直接对照建表:

表名职责是否分端关键字段
goods商品 SPU 与 SKU否spu_id, sku_id, price, stock
order订单主表否order_no, user_id, total, status
order_item订单明细否order_no, sku_id, num, price
coupon优惠券模板否coupon_id, type, threshold, amount
user_coupon用户领券记录否user_id, coupon_id, status
distribution分销关系否user_id, parent_id, level

注意所有表都没有platform字段作为业务主键的一部分。平台信息只落在订单的扩展字段或日志里,用于统计和风控,不参与业务唯一性判断。这是多端版能保持数据一致的前提。

2.3 环境准备与最小启动步骤

假设你已经拿到一份可运行的后端代码和前端分端工程,最小启动路径通常是:装依赖、配数据库、导初始数据、起后端、起前端。以常见的 Node 后端加 Vue 前端为例,命令如下:

# 1. 后端依赖与配置 cd server npm install cp .env.example .env # 编辑 .env,填入数据库连接、Redis、支付商户号等 npm run migrate # 执行数据库迁移 npm run seed # 导入初始商品与管理员账号 npm run dev # 启动后端,默认监听 3000 # 2. 小程序端 cd ../client-mini npm install npm run dev:mp-weixin # 生成小程序包,用开发者工具打开 dist 目录 # 3. H5 端 cd ../client-h5 npm install npm run dev # 本地起 H5,默认 8080

参数说明:.env里最不能错的是数据库连接和 Redis 前缀。多端版通常用 Redis 存购物车和登录态,如果多个端共用一个 Redis 实例,务必给每个端加不同的 key 前缀,否则会出现「小程序登录后 H5 也变成已登录」的玄学问题。npm run seed导入的管理员账号一般写在脚本注释里,第一次登录后立刻改密码。

3. 多端登录态与支付回调:最容易出错的三个环节

3.1 小程序、H5、App 的登录态怎么统一

多端版最烦人的不是业务逻辑,是登录态。小程序用code换openid,H5 用手机号加验证码或公众号授权,App 用账号密码或一键登录。三套入口,但后端要发同一种 token。我一般会做一个AuthAdapter,把不同端的凭证统一换成内部user_id,再签发 JWT。

// 登录适配层:不同端凭证统一换 token async function loginByPlatform(platform, payload) { let userId; switch (platform) { case 'mini': // payload.code 来自 wx.login const session = await wxCode2Session(payload.code); userId = await findOrCreateUserByOpenid(session.openid); break; case 'h5': // payload.phone + payload.smsCode await verifySmsCode(payload.phone, payload.smsCode); userId = await findOrCreateUserByPhone(payload.phone); break; case 'app': userId = await verifyPassword(payload.account, payload.password); break; default: throw new Error('unsupported platform'); } // 统一签发 JWT,过期时间按端可配 return signJwt({ userId, platform }, { expiresIn: platform === 'mini' ? '7d' : '2h' }); }

逻辑说明:findOrCreateUserByOpenid和findOrCreateUserByPhone内部要做用户合并,否则同一个人在小程序和 H5 会变成两个账号,订单对不上。参数上,JWT 过期时间建议小程序长一点(7 天),H5 短一点(2 小时),因为 H5 更容易被分享出去。signJwt的密钥必须放在环境变量里,不要硬编码。

3.2 支付回调的幂等与验签

支付回调是多端版另一个血泪重灾区。微信支付、支付宝、聚合支付的回调格式不同,但共同要求是:必须幂等、必须验签、必须返回成功标识。我见过最典型的翻车是回调处理超时,支付平台重试三次,结果订单被重复发货。

# 支付回调处理:幂等 + 验签 + 快速返回 def handle_pay_callback(platform, raw_body, headers): # 1. 验签,失败直接拒绝 if not verify_sign(platform, raw_body, headers): return {"code": "FAIL", "msg": "sign error"} data = parse_callback(platform, raw_body) order_no = data["out_trade_no"] # 2. 幂等:用 Redis 锁 + 订单状态双重判断 lock_key = f"pay_callback:{order_no}" if not redis.set(lock_key, "1", nx=True, ex=30): return {"code": "SUCCESS", "msg": "duplicate"} order = order_repo.find_by_no(order_no) if order.status == "paid": return {"code": "SUCCESS", "msg": "already paid"} # 3. 更新订单并触发后续(发货、分销佣金) order_repo.mark_paid(order_no, data["transaction_id"]) trigger_post_pay(order) return {"code": "SUCCESS", "msg": "ok"}

参数说明:nx=True, ex=30表示锁 30 秒,足够处理一次回调。trigger_post_pay里如果有耗时操作(比如发短信、算分销),建议丢进队列异步做,不要在回调里同步跑,否则支付平台等不到响应会一直重试。验签函数verify_sign每个平台实现不同,但一定要用官方 SDK 或官方文档给的算法,不要自己造。

3.3 多端商品详情与库存扣减的并发处理

商品详情页在三端展示逻辑不同,但库存扣减必须走同一套。常见做法是下单时预扣库存,支付成功后实扣,超时未支付回滚。这里用 Redis 做预扣,数据库做最终一致。

-- 库存预扣:先查再扣,用版本号防并发 UPDATE goods_sku SET stock = stock - #{num}, version = version + 1 WHERE sku_id = #{skuId} AND stock >= #{num} AND version = #{version}; -- 影响行数为 0 说明库存不足或并发冲突,需重试或报错

这条 SQL 的关键是stock >= #{num}和version两个条件同时成立才更新。如果只用stock >= num,高并发下会出现超卖。version从查询时带过来,更新失败就重查重试,一般重试 3 次还失败就提示「手慢了」。注意 Redis 预扣和这条 SQL 要配合使用,Redis 挡掉大部分无效请求,数据库做最终扣减。

4. 避坑与排查:多端版上线前必须过的五道坎

4.1 坑一:小程序包体积超限,主包塞了太多商业模块

现象:微信开发者工具提示主包超过 2MB,上传失败。原因:把商品、订单、分销、拼团所有页面都放主包,没有做分包。解决:按业务域拆分包,商品和订单放主包,分销、拼团、秒杀放子包,用subPackages配置。分包预下载在首页onLoad里触发,用户点进子包时基本已经下载完。

4.2 坑二:H5 端支付回调地址配错,支付成功但订单未更新

现象:用户 H5 支付扣款成功,但订单状态还是「待支付」。原因:.env里H5_PAY_NOTIFY_URL填的是内网地址或带端口的本地地址,支付平台回调不到。解决:回调地址必须是公网可访问的 HTTPS 域名,且不能带端口。排查时先看支付平台后台的回调日志,再查后端访问日志有没有收到请求。

4.3 坑三:多端共用 Redis 导致登录态串端

现象:小程序登录后,H5 刷新也变成已登录,但用户信息不对。原因:JWT 存在 Redis 时 key 没加端前缀,或者前端存 token 的 localStorage key 相同。解决:Redis key 用token:{platform}:{userId},前端存储 key 用token_mini、token_h5区分。这个坑很隐蔽,因为本地开发时往往只跑一个端,发现不了。

4.4 坑四:分销佣金在小程序和 H5 重复计算

现象:同一用户在小程序下单,H5 端也收到佣金通知。原因:佣金计算逻辑写在了前端或回调里,没有做订单维度的幂等。解决:佣金计算必须放在订单支付成功后的异步任务里,用order_no做唯一键,计算前先查commission_log表是否已存在记录。

4.5 坑五:数据库迁移脚本在多个环境执行顺序不一致

现象:测试环境正常,生产环境启动报字段不存在。原因:迁移脚本没有版本号管理,或者多人同时改了迁移文件。解决:用带时间戳的迁移文件名,如20240101120000_add_coupon_table.js,并且迁移记录表migrations要纳入版本控制。每次上线前在预发环境完整跑一遍migrate。

5. 商业数据分析的接入点:从订单表到看板的最小闭环

多端版跑起来之后,下一步一定是看数据。数字商业生态这个词听着大,落到代码里其实就是「订单、用户、商品」三张表加几个聚合查询。我一般会先做一个最小看板:今日 GMV、今日订单数、新增用户数、各端占比。不急着上 BI 工具,先用一个定时任务把数据算好写进统计表。

-- 每日统计表:按端和日期聚合 INSERT INTO daily_stats (stat_date, platform, gmv, order_count, new_user_count) SELECT DATE(created_at) AS stat_date, platform, SUM(total) AS gmv, COUNT(*) AS order_count, 0 AS new_user_count FROM `order` WHERE status = 'paid' AND created_at >= CURDATE() - INTERVAL 1 DAY AND created_at < CURDATE() GROUP BY DATE(created_at), platform ON DUPLICATE KEY UPDATE gmv = VALUES(gmv), order_count = VALUES(order_count);

这段 SQL 每天凌晨跑一次,把前一天各端的 GMV 和订单数写进daily_stats。ON DUPLICATE KEY UPDATE保证重跑不会重复插入。新增用户数可以另起一条 SQL 从user表按created_at聚合,再更新到同一张表。看板前端直接查这张统计表,不要实时查订单表,否则数据量上来后页面会卡。

进阶一点的做法,是把统计任务拆成「实时」和「离线」两层:实时层用 Redis 计数器记录当天的 GMV 和订单数,离线层用上面的 SQL 做 T+1 校准。实时层在支付回调里INCR对应 key,离线层跑完覆盖。这样看板既有实时感,又不会因为 Redis 丢数据而长期偏差。

我自己的习惯是,任何多端项目上线前,先把「支付回调幂等」和「库存扣减并发」这两个测试用例跑通,再谈营销功能。这两个地方翻车,后面所有商业数据都是假的。希望帮到你。

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

RELATED READING

延伸阅读

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