简介:面向小程序开发者和运营人员的营销系统源码,整合排队免单、买单返现与连动2+1玩法,适配抖音短视频小程序等常见平台,既适合商家和服务商快速搭建会员营销活动,也适合有前端基础的开发者学习活动类小程序的工程实现。压缩包共包含983个文件,整体约2.36MB,其中659个vue页面组件与143个png图片构成界面主体,67个js处理交互逻辑,31个json管理配置,23个scss与9个css负责样式,另有字体、图标及40个md文档辅助说明,整体为较标准的uni-app工程结构,可导入开发者工具运行预览。当前已有174人学习下载。源码提供完整页面目录、公共样式与基础配置,覆盖排队免单、买单返现的前端交互和规则展示,可帮助使用者从用户参与、排队状态到免单返现快速理解业务流程,并在此基础上扩展分销、会员或界面定制等运营能力。
1. 排队免单、买单返现、连动2+1:这套小程序营销系统解决的是什么问题
一家饮品店开业,门店排队排到马路对面,老板既怕顾客等不及走掉,又怕活动做完没有回头客。“排队免单,买单返现系统,连动2+1新小程序营销系统”就是把这个场景里的三个营销动作做成一套微信小程序:用户到店扫码排队,满足活动规则的第N位或幸运用户可以免单;买单完成后按实付金额返现到账户余额;用户邀请朋友完成首购,两级联动结算会继续给返现。它解决的不是“做一个商城”,而是把排队、成交、复购和裂变串成一条完整的私域链路。适合手里有一家或多家实体门店的运营者,也适合接这类营销小程序定制的开发者——前者关心规则设置和最终增收,后者关心数据表怎么设计、并发不翻车和结算不重复。
2. 先立业务模型:排队免单与买单返现的规则引擎和表结构怎么定
2.1 排队免单的两种常见玩法与选型
排队免单不是让用户傻等。常见玩法有两种:第N人免单和成团抽免单。第N人免单是“排到第10个下单的用户,这一单免单”,规则简单,顾客可预期,倒计时感强,适合客单价不高、翻台快的茶饮店和小吃店。成团抽免单是“满20人支付后,系统随机抽1人退免单”,不确定性强,但单日两三百单以上的门店用起来更稳妥——因为流量不够时,第100个免单一天都凑不满,活动就凉了。
选型时我先问门店日单量:日单量低于100单,不要设超过20的免单门槛;日单量在300单以上,才适合“第N人免单”和大额免单。还要确认免单资格能不能转移:顾客排到免单,自己不想用,能不能送给同行朋友。可以转移的版本要多一个二维码核销环节,把免单资格变成票券;不转移的版本就在支付回调后直接退款,开发量小一半。
2.2 买单返现的结算口径:按实付返还是按应收返
买单返现的“返现”必须有明确口径。系统里常见三个口径:按实付金额返、按应收金额返、按商品利润返。我一般推荐按实付金额返——优惠券、满减已经让利一次,再按应收返等于双重让利;按实付返回,账单上清清楚楚,活动成本也容易算。
返现去向决定用户体验:可提现余额是“真金白银”,用户感知最强,但提现手续费和风控压力也大;待解锁余额(下次消费可抵)对门店更安全,能把返现变成二次复购的凭证;返到储值卡则是把资金留在店里。落地时我倾向默认“可提现余额 + 消费满额才可提现”的组合,用户在活动页能看到数字,钱又不会一次性被取走。
返现速率还要加三个风控参数:单用户每日返现单数上限、单笔返现封顶、返现池日预算。这三个参数不设置,做活动第一天就会被羊毛客用多账号刷穿。退款场景也要提前约定:订单退款时,已发放的返现必须从余额扣回,扣不回的记为负余额,限制再次提现。
2.3 数据表设计:排队单和返现流水两张核心表
排队和返现两张表是整个系统的地基,先看建表语句。
CREATE TABLE queue_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, queue_seq INT NOT NULL, order_amount DECIMAL(10,2) NOT NULL, is_free TINYINT DEFAULT 0, rule_id INT NOT NULL, created_at DATETIME NOT NULL, KEY idx_user_id (user_id), KEY idx_queue_seq (queue_seq), UNIQUE KEY uk_order_seq (order_no, queue_seq) ); CREATE TABLE wallet_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, source_order_no VARCHAR(32) NOT NULL, flow_type VARCHAR(16) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, created_at DATETIME NOT NULL, UNIQUE KEY uk_source_type (source_order_no, flow_type), KEY idx_user_id (user_id) );queue_seq 就是用户看到的“您是第X位”。order_no 必须唯一,为的是整条支付回调链路能对齐;rule_id 记录当期活动规则,活动结束调整规则后,历史单据仍按历史规则结算。wallet_flow 负责返现流水,不单独在流水表里存余额字段,余额由 SUM(amount) 派生——没有余额字段,就不会出现“流水的钱和账户余额对不上”的账目黑洞。
但查询性能上不能所有余额都实时聚合。我一般会再建一张 user_wallet 表冗余一个 balance 字段,更新余额和写流水放在同一个事务里,查询直接取字段,对账时用流水反推校验。
2.4 规则引擎参数化:活动规则写成 JSON 而不是写死在代码里
活动规则如果用常量写死在服务里,每次改活动都要发版;活动运营一周改三次,开发就要陪跑一周。营销系统做久了都会同意一个结论:规则必须参数化。
{ "ruleId": 20250312, "scene": "queue_free", "freeType": "nth_user", "nth": 10, "maxFreePerUserPerDay": 1, "odds": null, "enabled": true }freeType 是 nth_user 时,nth 就是第几个免单;freeType 是 lucky_draw 时,odds 是命中率,例如 0.05 表示 5%。后端每次订单支付完成,先按 scene 找到当前启用规则,再按 freeType 分支判断。参数校验顺序也很重要:先校验 enabled,再校验用户今日免单次数,最后再做免单命中判断,顺序反了会把用户今日额度提前消耗掉。
2.5 不建议直接在通用商城插件上加排队免单的三个理由
市面上带营销插件的小程序商城不少,但直接在通用商城插件上加排队免单,通常会在三个地方吃亏。第一,通用商城核心是商品、订单、库存,排队免单本质是营销活动引擎,要控制“第几个人免单”“今天免单预算用了多少”,商城插件没有活动预算概念。第二,并发性能隔离差:排队入口是瞬时流量聚集点,和下单抢库存叠在一起,容易互相拖垮。第三,回调逻辑不一样:免单要等支付结果再退钱,而不是下单时直接减价,很多商城插件的优惠框架不支持这种“先收后返”模式。
所以更常见的做法是独立建一个营销服务,专门处理排队、返现、关系链结算,订单和支付走基础商城能力,两边用消息队列对接。系统复杂一点,但活动规则怎么改都不会碰核心交易链路,夜里发版也只影响营销模块。
3. 连动2+1的关系链与分佣结算:一张表加一段事务代码把裂变跑起来
3.1 连动2+1到底算的是什么业务账
连动2+1是这套营销系统里最容易让开发理解偏的部分。落到代码层面,它的含义是:用户A带来两个新用户B和C,B、C各自完成首单后,A完成一轮任务,获得一次免单或返现权益,称为“出局”;出局后A自动进入上级的团队,继续为上级贡献联动计数。
这里的“2”是两个有效首购用户,“1”是完成一轮后给本人的奖励。奖励可以设计成返现金额,也可以设计成免单资格,由活动规则决定。关系链只记录两层联动:直接推荐、间接推荐,超过两层的收益我不建议设计——营销返现层级越多,风控和合规压力越大,也越容易被羊毛党批量注册刷号。
做这块前要先把业务参数和开发对齐,否则后端的“等级”概念会被运营不断加码,最后变成一棵无限深的递归树。
3.2 关系链存储:为什么不递归查表
用户关系链最简单朴素的做法是用一张自关联表记录 parent_id,每次结算从当前用户一路向上查。这个方案在小规模用户下没问题,但当用户量过万、层级一深,MySQL 递归查询的延迟就会拖慢支付回调。更通用的做法是加一个 path 字段冗余祖先路径。
CREATE TABLE user_relation ( user_id BIGINT PRIMARY KEY, parent_id BIGINT NOT NULL DEFAULT 0, root_id BIGINT NOT NULL DEFAULT 0, path VARCHAR(1000) NOT NULL DEFAULT '', team_count INT DEFAULT 0, joined_order_no VARCHAR(32), created_at DATETIME NOT NULL, KEY idx_parent (parent_id) );parent_id 是直接上级,path 存从根到当前用户的完整链路,比如“0/10001/10002”。结算时用 path 就能直接定位任意级祖先,不需要递归查询。team_count 是团队计数,B、C 完成首购后,A 的 team_count 加 1,达 2 时触发一轮联动奖励。joined_order_no 存用户首购订单号,用来保证“只能绑定一次关系”,避免用户反复换上级。
这个表的写入顺序也容易踩坑:先建关系,再更新 team_count。如果先记团队数再写关系,扫码进来的新用户挂到一半时支付回调到了,结算会找不到祖先路径,导致这一单漏佣。
3.3 结算代码:支付回调后的事务处理与幂等
连动结算放在支付成功回调里,核心代码如下。
@Transactional public void settleByOrder(OrderPaidEvent event) { // 1. 找到当前用户的关系节点 RelationNode node = relationMapper.findByUserId(event.getUserId()); if (node == null) { return; } // 2. 按层数规则结算直接和间接两级返现 List<LevelRule> rules = ruleMapper.findEnabledLevelRules(); for (LevelRule rule : rules) { Long targetUserId = node.getAncestorByLevel(rule.getLevel()); if (targetUserId == null) { break; } String bizId = event.getOrderNo() + "_LEVEL_" + rule.getLevel(); if (settleLogMapper.existsByBizId(bizId)) { continue; } BigDecimal amount = event.getPaidAmount() .multiply(rule.getRate()) .setScale(2, RoundingMode.HALF_UP); walletMapper.addBalance(targetUserId, amount); settleLogMapper.insert(bizId, targetUserId, amount); } // 3. 上级团队计数 +1 relationMapper.incrTeamCountForAncestors(node.getPath()); }这块有四个要点。第一,事务必须包住“加余额 + 写流水 + 更新计数”,任何一步失败整体回滚,否则会出现余额加了但流水缺失。第二,幂等检查不能只靠代码里的 existsByBizId,并发重试时两个请求同时查到不存在,就会重复入账,所以表里必须对 biz_id 建唯一索引,靠数据库兜底。第三,金额计算用 BigDecimal,乘法后要 setScale 保留两位并指定舍入方式,直接用 double 会出现 11.999999 这种流水。第四,settleLogMapper.insert 要捕获唯一键冲突,冲突时说明这条已经结算过,直接返回成功而不是抛异常。
3.4 连动奖励的四个必调参数
连动模块上线前,运营需要在后台配置四类参数。返现比例 rate,按订单实付金额的百分比结算,我见过把直接推荐设 20%、间接推荐设 10% 的门店,也见过只能承受 5% 的连锁品牌,这个值取决于毛利空间。层级规则 level,只开启 level=1 就是单级,level=2 就是两级联动,层级越多分账越复杂,不建议超过两级。
单人单日结算上限,防止某一个用户当天被大量订单涌入刷出巨额返现;最低消费门槛,用户低于某个客单价的首购不计入联动,可以把一分钱下单的刷单挡在外面。四个参数凑齐,连动模块才敢在真实门店放着跑,不然上线第二天就能看到异常账单。
4. 用 uniapp + Spring Boot 把营销小程序骨架跑通
4.1 为什么前端选 uniapp、后端选 Spring Boot
做这类营销系统,uniapp 加 Spring Boot 是社区里最常见的组合,理由也很实际。前端用 uniapp 开发微信小程序,一套代码能同时编小程序和 H5,门店如果想在 PC 端或公众号里做活动入口,不用重写一套页面;后端用 Spring Boot,生态成熟,微信支付、消息队列、定时任务都有现成集成。
对比原生微信小程序,uniapp 的代价是偏重的框架抽象,遇到平台差异还是要写条件编译;但营销系统页面多、活动入口散,跨端收益大于学习成本。后端也可以换成别的语言,但团队如果以 Java 为主,Spring Boot 是最少踩坑的选择。
4.2 微信小程序端的请求封装和缓存设置
小程序端第一步是把请求封装好,不要把uni.request散落在每个页面里。下面是一份最小封装。
// config.js const ENV = 'dev'; const BASE_URL = ENV === 'dev' ? 'https://dev-api.example.com' : 'https://api.example.com'; const APP_ID = 'wx1234567890abcdef'; // request.js const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { uni.request({ url: `${BASE_URL}${url}`, method, data, header: { 'Authorization': uni.getStorageSync('token'), 'Content-Type': 'application/json' }, timeout: 10000, success: (res) => { if (res.statusCode === 200) { resolve(res.data); } else { reject(res); } }, fail: reject }); }); }; export const api = { joinQueue: (data) => request('/api/queue/join', 'POST', data), getWallet: () => request('/api/wallet/info', 'GET'), getQueueStatus: (data) => request('/api/queue/status', 'GET', data) };这里要说明三个点。请求头里的 Authorization 从本地缓存读取,登录成功后就uni.setStorageSync('token', token),后续所有接口自动带上,避免每个页面重复写。timeout 设 10 秒,排队接口是长轮询时单独设置更长超时,不要用全局同一个值。开发调试时切换 ENV 为 dev,体验版和正式版要分别配置合法域名,否则开发者工具里会报url not in domain list。
缓存时间也是营销系统容易被忽略的点。用户排队状态、返现金额这类数据要实时,不要本地缓存;门店活动规则、营销文案这类不频繁变化的数据,可以缓存 5 分钟,减小接口压力。
4.3 后端核心接口设计与参数约定
前端骨架搭好后,后端接口需要和前端约定清楚。核心接口就四个,其余都是后台管理接口。
| 接口 | 方法 | 核心入参 | 返回说明 |
|---|---|---|---|
| /api/queue/join | POST | userId, storeId | 返回 queueSeq,即排队序号 |
| /api/order/paid/callback | POST | orderNo, paidAmount | 支付回调,触发返现和联动结算 |
| /api/wallet/info | GET | userId | 返回余额、今日返现、可提现金额 |
| /api/relation/bind | POST | userId, referrerId | 绑定上级关系,幂等 |
支付回调接口不建议让前端直接调用,而是由后端接收微信支付结果通知,验签通过后再走业务逻辑。前端能调用的只有一个“确认订单去支付”的接口,支付结果一律以回调为准。这样设计是为了防止用户伪造“支付成功”请求,把返现刷走。
4.4 动态标题和顶部导航栏:排队状态实时反馈
营销小程序特别依赖页面标题的实时反馈。用户在排队页时,标题一直显示“排队中”没有任何信息增量;更好的做法是在标题上动态展示“您是第X位,前面还有3位”。
uni.setNavigationBarTitle({ title: `排队中 · 前面还有${waitingCount}位` });顶部导航栏高度处理要注意胶囊按钮的位置。自定义导航栏时,用uni.getMenuButtonBoundingClientRect()拿到胶囊按钮的坐标,再计算状态栏高度,避免自定义按钮被胶囊遮住。微信小程序顶部导航栏高度不是固定值,不同机型状态栏高度不同,硬编码 68px 会在带刘海的机型上错位。另外,正式版出现“开发版小程序已过期,请在开发者工具重新扫码”时,多数情况是开发者工具的体验版二维码过期,重新扫码或换正式版即可,和代码没有关系,排查时先别改代码。
4.5 项目目录与联调三个注意点
前端建议按页面和 API 分层,目录如下。
src/ pages/ queue/ // 排队页 order/ // 买单页 wallet/ // 返现账户页 invite/ // 邀请页 utils/ request.js // 请求封装 config.js // 环境配置 api/ queue.js // 排队相关接口 order.js // 订单相关接口 wallet.js // 钱包相关接口联调时要盯住三件事:第一,后端返回的数据结构要统一,{ code, message, data }是常见约定,前端 request.js 里解析 data,不要把 list 和 object 混着返。第二,时间字段统一返回时间戳或统一格式的字符串,前端不要自己拼接,否则 iOS 和 Android 上解析会有差异。第三,金额字段后端用“分”为单位返回整数,前端展示时再除以 100,避免浮点误差,这是小程序商城项目里最容易统一也最容易忽略的约定。
5. 排队免单和返现结算的避坑排查:五条实战踩坑记录
5.1 排队序号重复,两个用户同时拿到第10位
现象:活动开始一分钟,两个用户都收到“您是第10位”的提示,第10位免单资格不知道给谁。
原因:排队序号生成用了“先查最大序号再加1”的方式。两个请求同时读到当前最大序号是 9,各自返回 10,数据库里出现两条 queue_seq=10 的记录。
解决:序号生成改用 Redis INCR,同一时刻只能有一个请求拿到 10。数据库再加一个唯一索引兜底,(order_no, queue_seq)或(activity_id, queue_seq),插入失败的应用层捕获后重新取号。我排查这个问题时还发现,仅靠 Redis 计数不够,Redis 重启会丢号,所以数据库唯一索引必须留着,两者配合才能同时解决并发和持久化两个问题。
5.2 支付回调重试导致返现重复到账
现象:用户下单后收到两次返现到账通知,金额翻倍。
原因:微信支付回调在超时后会重试,后端第一次处理成功了,但响应超时,微信又发了一次回调,代码里没有幂等控制,就重复入账。
解决:wallet_flow 表建UNIQUE KEY uk_source_type (source_order_no, flow_type),同一订单同一返现类型只能有一条流水;插入时捕获唯一键冲突,异常直接忽略并返回成功。这里有一个血泪经验:仅靠代码里“先查后插”不可靠,并发下两个请求都查到不存在,还是会重复插入,必须靠数据库唯一索引兜底。
5.3 邀请参数丢失,下级关系挂到了别人头上
现象:用户A转发邀请海报,朋友B点开后没有绑定到A,而是挂到了平台默认用户下面,A的联动计数一直不涨。
原因:小程序码生成时,把推荐人ID放在 scene 参数里,但 B 进入页面时 scene 还没解析完成,前端就已经调了绑定接口,拿到的 referrerId 是空。
解决:前端必须在onLoad里先解析options.scene,拿到推荐人 ID 后再渲染页面,绑定接口只有在 referrerId 非空时才允许调用。后端也要做校验:referrerId 不能等于当前用户 ID,referrerId 必须真实存在,同一个用户只能绑定一次上级。排查时看网络请求的调用顺序,就会发现是前端在解析完成前抢先发了请求。
5.4 排队人数上千后,排队页面卡死
现象:活动上了公众号推文,门店排队人数一小时涨到一千多,用户滑动排队进度页卡顿,接口响应也越来越慢。
原因:排队进度接口一次返回全部排队数据,上千条记录反复渲染;前端开了轮询,每 5 秒请求一次全量数据,把服务器和手机都拖垮了。
解决:前端只展示最近 100 条排队记录,接口支持分页,用户上拉加载更多。实时进度改造为后端只返回“当前我的序号”和“当前总排队数”两个字段,不做列表轮询;如果需要展示队列变化动画,用 WebSocket 做增量推送,没有 WebSocket 条件时就缩短轮询间隔但减小返回体。后来的排查结论是,营销活动的高峰流量全部打在了一个全量列表接口上,这是设计问题,不是服务器不行。
5.5 苹果端虚拟支付冲突,返现功能被审核拒绝
现象:小程序提审时被拒,理由是涉及虚拟支付。排查后发现是“购买返现特权”这个页面触发了苹果的虚拟支付条款。
原因:营销系统里设计了“花9.9元购买返现会员资格”的入口,这属于虚拟商品,在微信小程序 iOS 端必须走苹果内购(IAP),普通微信支付不被允许,和营销返现逻辑冲突。
解决:把“购买返现会员”改成“实付满59元自动获得返现权益”,返现变成消费后的附属激励,不单独售卖。虚拟支付相关的功能全部移除,活动只围绕实体商品买单做文章,审核就不再触碰虚拟支付条款。这个小程序营销系统的边界值得每个开发者记住:返现和免单都可以围绕实物消费设计,但不要设计成单独购买的虚拟权益。
6. 上线前做一轮并发验证与资金对账,再放门店灰度
6.1 用一段脚本模拟多人同时排队
上线前最值得做的测试就是并发排队。用协程模拟 100 个用户同时请求排队接口,观察是否出现重复序号、接口是否超时。
import asyncio import aiohttp async def join_queue(session, user_id): async with session.post( 'https://api.example.com/api/queue/join', json={'userId': user_id, 'storeId': 1001} ) as resp: return await resp.json() async def main(): async with aiohttp.ClientSession() as session: tasks = [join_queue(session, i) for i in range(100)] results = await asyncio.gather(*tasks) seqs = [r['data']['queueSeq'] for r in results] assert len(seqs) == len(set(seqs)), '存在重复序号' asyncio.run(main())跑完只看两个指标:有没有重复序号,以及 100 个请求的平均响应时间。响应时间超过 500 毫秒就要检查 RT 是否在数据库查询上,序号生成是否走了 Redis。
6.2 返现流水对账 SQL 与灰度节奏
资金类的功能必须能对账。每条返现流水都应该能追溯到来源订单,所以对账 SQL 很直接:按来源订单分组查流水条数和金额,异常自然暴露。
SELECT source_order_no, COUNT(*) AS cnt, SUM(amount) AS total_amount FROM wallet_flow WHERE flow_type = 'RETURN_CASH' GROUP BY source_order_no HAVING cnt > 1;这条 SQL 查出同一订单多笔返现的记录,就是重复入账的嫌疑单。对账脚本建议放在定时任务里,每天凌晨跑一次,有异常就给运营发消息。
灰度节奏我习惯分三步:内部员工白名单试跑一天,确认返现到账和关系链绑定都正常;单店试点跑三天,观察返现对账无异常、退款流程能冲正;最后才全门店放开。我每次上线这种营销小程序,第一件事永远不是看用户增长,而是把所有“多付钱、多返钱、重复返钱”的路径堵死,再谈活动效果。这套系统里排队免单是拉新噱头,买单返现是锁客钩子,连动2+1是裂变杠杆,三条链路互相嵌套,规则可以热闹,资金账目必须清爽。希望帮到你。
本文还有配套的精品资源,点击获取