简介:群果果多商户小程序是一套面向中小商家与平台运营者的微信小程序电商解决方案,帮助多个商家在同一平台入驻开店,无需独立开发即可拥有线上店铺,降低进入移动电商的门槛。资源以zip压缩包形式提供,整体约59.56MB,包内文件类型明细与文件总数上游暂未提供,可结合描述了解其覆盖多商户管理、商品展示与库存维护、订单处理、微信支付集成、优惠券与拼团等营销工具、用户互动、数据分析、个性化店铺设计、社交分享及在线售后等模块。已有269人学习下载,适合希望快速搭建多商户商城、研究小程序电商架构与后台运营逻辑的开发者与运营人员参考,可据此梳理平台端与商家端的职责划分、交易流程闭环及营销玩法设计思路,为二次开发或方案选型提供借鉴。
1. 群果果多商户小程序:一个后台撑起 N 个店铺的落地拆解
如果你手里有一个社区团购、本地生活或批发市场类的小程序需求,大概率会遇到同一个问题:不是做一个店,而是要让几十上百个商户在同一个入口里各自卖货、各自结算、各自看数据。群果果多商户小程序这类方案,解决的就是「一套代码、一个后台、多商户独立运营」这件事。它适合中小型平台方、区域运营商,以及需要快速验证多商户模式的开发者。核心难点不在前端页面,而在商户隔离、订单归属、分账逻辑和权限边界这四件事上。我见过太多团队前端两天撸完,后端数据串了三个月,最后返工重写。这篇笔记按「先想清楚数据怎么隔离,再动手搭最小闭环,最后处理分账和踩坑」的顺序讲,新手能照着跑通,熟手能对照检查自己的边界设计。
2. 多商户的数据隔离:先定模型再写代码
多商户系统翻车,十有八九是数据模型没定好就开始写接口。群果果这类小程序,商户之间既要共享商品池(平台自营),又要各自上架独立商品,还要保证 A 商户看不到 B 商户的订单和客户。这一章把隔离方案讲透,后面写代码才不会反复改表。
2.1 三种隔离方案怎么选
常见做法有三种:独立数据库、独立 schema、共享表加商户 ID 字段。独立数据库隔离最彻底,但运维成本和跨商户统计难度陡增,适合商户数量少、数据敏感度极高的场景。独立 schema 是折中方案,一个商户一个 schema,连接池切换,适合几十到一两百商户。共享表加 merchant_id 字段是绝大多数小程序平台的选择,开发快、统计方便,代价是每条查询都必须带商户条件,一旦漏写就是数据泄露。
我一般会推荐共享表方案,但加两道保险:一是所有商户相关表强制带 merchant_id 且建联合索引;二是在数据访问层做统一拦截,不让业务代码直接拼 SQL。下面是一个简化的表结构设计,用 SQL 表达:
-- 商户表:平台侧管理,记录每个商户的基本信息和状态 CREATE TABLE merchant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '商户名称', contact VARCHAR(32) COMMENT '联系人', status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 商品表:merchant_id 为 0 表示平台自营,大于 0 表示归属某商户 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, merchant_id BIGINT NOT NULL DEFAULT 0 COMMENT '0=平台自营', title VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, status TINYINT DEFAULT 1, INDEX idx_merchant_status (merchant_id, status) ); -- 订单表:每笔订单必须归属一个商户,平台订单 merchant_id 为 0 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, merchant_id BIGINT NOT NULL DEFAULT 0, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, pay_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待付 1已付 2已发 3完成 4取消', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_merchant_created (merchant_id, created_at), INDEX idx_user (user_id) );逻辑说明:merchant_id 作为隔离核心字段贯穿商品和订单,平台自营统一用 0 表示,避免 NULL 带来的索引和判断麻烦。参数上,联合索引的顺序很关键,merchant_id 放前面是因为几乎所有查询都会先过滤商户,再按时间或状态筛选。如果你们的商户数量超过 500 且数据量级到千万,建议把订单表按 merchant_id 做哈希分表,否则单表查询会明显变慢。
2.2 数据访问层的统一拦截
光有表结构不够,业务代码里手写where merchant_id = ?迟早会漏。我一般会在 ORM 层做一次封装,让所有商户相关查询自动带上条件。以 Python 的 SQLAlchemy 为例:
from sqlalchemy import event from sqlalchemy.orm import Session # 当前请求上下文中的商户ID,由中间件从登录态解析后注入 CURRENT_MERCHANT_ID = None def set_merchant_context(merchant_id): global CURRENT_MERCHANT_ID CURRENT_MERCHANT_ID = merchant_id @event.listens_for(Session, "do_orm_execute") def _add_merchant_filter(execute_state): # 只对查询类操作自动加条件,增删改由业务显式处理 if execute_state.is_select and CURRENT_MERCHANT_ID is not None: execute_state.statement = execute_state.statement.filter( # 假设模型有 merchant_id 属性,平台侧查询时传 0 execute_state.statement.column_descriptions[0]['entity'].merchant_id == CURRENT_MERCHANT_ID )逻辑说明:这段代码利用 SQLAlchemy 的do_orm_execute事件,在所有 SELECT 语句执行前自动追加 merchant_id 过滤。参数上,CURRENT_MERCHANT_ID由请求中间件设置,平台管理员查询时设为 0 或走单独的免过滤通道。注意,这种全局拦截对复杂 join 查询可能误伤,实际项目里我会加一个skip_merchant_filter标记,让平台统计类接口显式跳过。这个方案的好处是业务开发不用记着加条件,坏处是调试时容易懵——明明代码没写条件,SQL 里却多了个 where,这就是典型的「黑匣子」时刻,团队里要有人清楚这套机制。
2.3 商户入驻与审核的最小闭环
数据隔离定好后,商户入驻流程要能跑通。最小闭环包括:提交资料、平台审核、开通账号、分配权限。这里不贴完整前端,只讲后端接口设计要点。商户表加一个audit_status字段,审核通过后才把status置为 1。审核接口要记录操作人和时间,方便追溯。权限方面,商户账号只能操作自己的商品和订单,平台账号可以看全部但不能替商户发货——这个边界要在接口层用角色判断卡死,不能只靠前端隐藏按钮。
3. 订单归属与分账:钱的事不能含糊
多商户系统里,订单和钱是最容易出纠纷的地方。用户在小程序里下一单,可能包含平台自营商品和商户商品,支付是一笔,但钱要分给不同主体。这一章讲清楚订单拆分、分账记录和结算逻辑。
3.1 订单按商户拆单的时机
常见做法有两种:下单时拆、支付后拆。下单时拆的好处是每笔子订单独立走状态机,商户各自发货互不影响;坏处是用户要合并支付,支付回调要处理多笔子订单的关联。支付后拆的好处是支付逻辑简单,坏处是支付成功到拆单之间有一小段状态不一致窗口,需要补偿机制。
我一般选下单时拆,因为多商户场景下商户发货节奏差异大,合并订单状态机太复杂。具体做法是:用户提交购物车后,后端按 merchant_id 分组,生成一个主订单和 N 个子订单,主订单只记录支付总额和支付状态,子订单记录各自商品和金额。支付回调只更新主订单,然后触发子订单状态流转。
def create_order(user_id, cart_items): # 按商户分组,平台自营 merchant_id=0 groups = {} for item in cart_items: groups.setdefault(item['merchant_id'], []).append(item) main_order_no = gen_order_no() sub_orders = [] for merchant_id, items in groups.items(): sub_no = gen_order_no() total = sum(i['price'] * i['qty'] for i in items) sub_orders.append({ 'order_no': sub_no, 'main_order_no': main_order_no, 'merchant_id': merchant_id, 'total_amount': total, 'status': 0 # 待支付 }) # 主订单金额为子订单之和,实际支付以主订单为准 main_amount = sum(o['total_amount'] for o in sub_orders) save_main_order(main_order_no, user_id, main_amount) save_sub_orders(sub_orders) return main_order_no逻辑说明:拆单核心是按 merchant_id 分组,主订单只做支付聚合,子订单各自独立。参数上,main_order_no是支付和退款的入口,子订单号用于商户侧操作。注意金额计算要用 Decimal 避免浮点误差,这是血泪经验,用 float 迟早对不上账。
3.2 分账记录的落库设计
分账不是支付平台自动完成的,尤其在小程序生态里,资金先到平台账户,再结算给商户。所以系统里必须有一张分账流水表,记录每笔子订单应结算给谁、扣多少佣金、实际结算多少。
CREATE TABLE settlement ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sub_order_no VARCHAR(32) NOT NULL, merchant_id BIGINT NOT NULL, order_amount DECIMAL(10,2) NOT NULL COMMENT '子订单金额', commission_rate DECIMAL(5,4) NOT NULL COMMENT '平台佣金比例', commission_amount DECIMAL(10,2) NOT NULL COMMENT '平台佣金', settle_amount DECIMAL(10,2) NOT NULL COMMENT '应结算给商户', settle_status TINYINT DEFAULT 0 COMMENT '0待结算 1已结算', settle_time DATETIME, INDEX idx_merchant_status (merchant_id, settle_status) );逻辑说明:分账表在子订单支付成功后写入,佣金比例从商户配置读取。参数上,commission_rate用小数存储,比如 0.05 表示 5%。结算状态由定时任务或人工触发更新,每次结算要记录结算批次号,方便对账。这里的关键是分账记录只增不改,退款时生成反向记录,不要直接改原记录,否则对账时根本说不清。
3.3 退款与分账冲正
退款是多商户系统里最绕的环节。用户退一个子订单,平台要退用户钱,同时冲正分账记录。如果商户已经结算,还要从下次结算里扣回。我的做法是:退款时生成一条负向 settlement 记录,settle_amount为负,settle_status直接置为已结算(表示这笔冲正已生效),然后在商户余额里扣减。如果商户余额不足,挂账到下次结算。这套逻辑要用事务包住,否则退款成功但冲正失败,账就乱了。
4. 权限与角色:别让商户看到不该看的
多商户系统里,权限设计比普通后台复杂,因为存在平台、商户、用户三层角色,且商户之间完全隔离。这一章讲角色模型和接口鉴权。
4.1 角色与权限的映射表
我一般用 RBAC 模型,但简化到三层:平台管理员、商户管理员、商户操作员。权限点按模块划分,比如商品管理、订单管理、结算查看。下面是一个权限映射的示例表:
| 角色 | 商品管理 | 订单管理 | 结算查看 | 商户管理 |
|---|---|---|---|---|
| 平台管理员 | 全部 | 全部 | 全部 | 全部 |
| 商户管理员 | 本商户 | 本商户 | 本商户 | 无 |
| 商户操作员 | 本商户只读 | 本商户发货 | 无 | 无 |
逻辑说明:权限判断分两步,先判断角色能访问哪些模块,再判断数据范围是否属于本商户。数据范围判断依赖第 2 章的 merchant_id 拦截,两者配合才能既控制功能又控制数据。
4.2 接口鉴权的实现要点
鉴权中间件从 token 解析出 user_id 和 role,再查出对应的 merchant_id(平台角色为 0)。每个接口声明所需权限点,中间件比对角色权限表。这里要注意,商户操作员的 merchant_id 必须从数据库查,不能从 token 里直接取,否则 token 被篡改就能越权。另外,平台管理员访问商户数据时,要在日志里记录操作,方便审计。
def auth_required(permission): def decorator(func): def wrapper(request, *args, **kwargs): user = parse_token(request.headers.get('Authorization')) if not user: return jsonify({'code': 401, 'msg': '未登录'}) # 从数据库查角色和商户绑定关系,不信任 token 中的 merchant_id role = get_user_role(user['user_id']) if permission not in role['permissions']: return jsonify({'code': 403, 'msg': '无权限'}) request.merchant_id = role['merchant_id'] return func(request, *args, **kwargs) return wrapper return decorator逻辑说明:装饰器模式让权限声明和业务代码分离,permission参数是权限点字符串。关键点是 merchant_id 从数据库查,token 只存 user_id。参数上,权限点建议用「模块:操作」格式,比如product:edit,方便扩展。
5. 避坑与排查:多商户系统最常见的五个翻车点
这一章是我踩过的坑和帮别人排查过的典型问题,每条按现象、原因、解决写。多商户系统的坑大多集中在数据泄露、金额错乱和状态不一致上,下面五条覆盖了八成场景。
5.1 商户 A 看到了商户 B 的订单
现象:商户登录后台,订单列表里混入了其他商户的订单。原因:某个查询接口漏了 merchant_id 条件,或者用了平台侧的免过滤通道但没加权限判断。解决:先查该接口的 SQL 日志,确认 where 条件;然后在数据访问层加自动化测试,每个商户相关查询必须验证隔离。我一般会写一个测试用例,用两个商户账号交叉请求,断言返回数据不重叠。
5.2 支付成功但子订单状态没变
现象:用户付了钱,主订单显示已支付,但商户侧子订单还是待支付,商户不敢发货。原因:支付回调只更新了主订单,忘记触发子订单状态流转,或者子订单更新失败但没回滚。解决:支付回调里用事务包住主订单和所有子订单的更新,任何一笔失败整体回滚并记录补偿任务。另外加一个定时对账任务,扫描主订单已支付但子订单未支付的记录,自动修复。
5.3 分账金额对不上,差几分钱
现象:结算时商户收到的钱和订单金额减佣金对不上,差几分。原因:浮点数计算误差,或者佣金比例四舍五入规则不统一。解决:所有金额用 Decimal 或整数分存储,佣金计算统一用round且明确舍入规则,分账表记录计算过程。对账时用整数分比对,不要用浮点。
5.4 商户禁用后还能登录
现象:平台把商户 status 置为 0,但商户账号还能登录操作。原因:登录接口只校验了账号密码,没校验商户状态;或者 token 未过期,旧 token 仍有效。解决:登录时校验商户 status,同时在鉴权中间件里每次请求都查一次商户状态,禁用则拒绝。token 设置合理过期时间,关键操作要求重新验证。
5.5 退款后分账没冲正,商户多拿了钱
现象:用户退款成功,但商户结算里没扣回,商户实际多收了钱。原因:退款逻辑只处理了支付侧,没联动分账表。解决:退款接口里同步生成负向分账记录,并在商户余额里扣减,余额不足挂账。所有退款操作要能追溯到分账冲正记录,对账时双向核对。
6. 用对账任务兜底:多商户系统的后悔药
前面讲了隔离、拆单、分账和避坑,但线上系统再小心也会有状态不一致的时候。这一章讲一个我每套多商户系统都会加的对账任务,它不能防止问题发生,但能在问题发生后几小时内发现并修复,算是「后悔药」。
对账任务的核心思路是:每天凌晨扫描前一天的订单和分账记录,比对三个一致性——主订单金额等于子订单之和、子订单支付状态与分账记录匹配、分账结算金额与商户余额变动匹配。任何一条不一致就生成告警和修复工单。
def daily_reconcile(date): # 1. 主订单与子订单金额一致性 main_orders = query_main_orders(date) for mo in main_orders: subs = query_sub_orders(mo['order_no']) if sum(s['total_amount'] for s in subs) != mo['total_amount']: alert('金额不一致', mo['order_no']) # 2. 子订单支付状态与分账记录匹配 paid_subs = query_paid_sub_orders(date) for sub in paid_subs: settle = query_settlement(sub['order_no']) if not settle: # 支付成功但没有分账记录,补写 create_settlement(sub) alert('补写分账', sub['order_no']) # 3. 分账结算金额与商户余额变动匹配 settlements = query_settled(date) for st in settlements: if not verify_balance_change(st['merchant_id'], st['settle_amount']): alert('余额不匹配', st['sub_order_no'])逻辑说明:对账任务分三步,每步独立可重跑。参数上,date是账期日期,建议按自然日跑,避免跨天订单遗漏。补写分账要幂等,用子订单号做唯一键,重复执行不会产生多条记录。告警要落到具体订单号,方便人工介入。
这个任务我一般用定时调度跑,凌晨低峰期执行,结果写一张对账日志表。如果连续三天有同类告警,说明业务逻辑有系统性漏洞,要回头改代码而不是靠对账兜底。对账任务本身也要监控,它挂了没人知道,那就真成摆设了。
最后说个习惯:每接一个多商户需求,我会先画一张数据归属图,标清楚每张表属于平台还是商户,哪些字段是隔离键。这张图贴在工位上,写代码时瞄一眼,能省掉后面很多返工。多商户系统的复杂度不在技术难度,而在边界清晰——边界清了,代码自然稳。希望帮到你。
本文还有配套的精品资源,点击获取