简介:易客云会员微信小程序多开版会员卡系统1.0.33是一套面向商家与小程序开发者的完整会员运营解决方案,源码基于微信小程序生态开发,支持在同一系统下创建并管理多个会员卡小程序,适用于连锁门店、多品牌经营或跨行业会员互通场景。资源包共1285个文件,压缩后约4.91MB,主要包含564个PNG界面素材、67个JS脚本、59个WXSS样式、58个WXML页面结构、57个JSON配置及29个PHP后端接口文件,另有360个DAT数据文件承载系统运行所需的数据内容;目录结构清晰,并附带readme与资源说明文档,便于快速部署和二次开发。系统内置会员等级与积分、优惠券与满减活动、支付对接及用户行为统计等模块,商家可自定义界面与权益,实现会员拉新、留存和复购的完整闭环。已有507人学习下载,适合需要快速搭建微信小程序会员体系或进行源码研究的技术人员。
1. 易客云会员这类微信小程序多开版会员卡系统,到底在解决什么?
餐饮老板想给门店做会员小程序,一问报价五六千起步,加一个储值功能又要加钱;连锁品牌想给每家店独立发会员卡,又不想重复买系统。易客云会员 1.0.33 这种微信小程序多开版会员卡系统,就是冲着这个场景来的:一套后端服务部署在服务器上,上面挂 N 个微信小程序,每个小程序对应一家门店或一个品牌,会员、储值、积分、卡券在逻辑上完全隔离。它能解决“低成本接几十个小商户会员卡”的需求,也适合做本地生活服务商批量交付。开发者拿到这样的 rar 包,要真正跑起来并交给多个客户用,核心工作不是改 UI,而是读懂多开标识、部署好数据隔离,再处理微信登录那些绕不开的配置。
2. 先拆包:拿到 rar 后如何看清前端、后端和数据初始化文件
2.1 解压后先别急着传服务器,先看目录结构
多开版会员卡系统的发布包通常不是一份单纯的小程序前端,里面至少包含前端工程、后端接口和数据库脚本。拿到 1.0.33 的 rar 包,我不会直接传到 Web 目录,而是先在本地建一个工作目录解压,看一遍顶层目录。这一步能避免把后端接口和小程序代码混在一起,也方便确认包里的路径是否带中文。Linux 服务器上很多 PHP 项目对中文文件名支持不好,解压出来先改名为干净路径能少很多麻烦。
mkdir -p /data/www/yikeyun && cd /data/www/yikeyun # 若拿的是 rar 包,一般用 7z 或 unrar 解压 7z x /path/to/易客云会员_微信小程序多开版会员卡系统_1.0.33.rar # 只看两层目录,快速分辨前端、后端、数据库脚本 tree -L 2 -d这里的7z x会保留包内完整目录结构,不会帮你改名。解压后看到类似app、server、database、docs这样的顶层目录,基本就是一套完整的业务系统:app是指微信小程序前端,server是后端接口,database里是 SQL 初始化脚本,docs是安装说明。如果server目录不存在,那这个包可能只是前端壳子,还得单独找接口端。tree -L 2 -d里的-L控制递归层级,-d表示只显示目录不显示文件,这样一眼能看出工程全貌。解压之后,我习惯用du -sh ./*看看每个目录大小,通常server是最大的,说明业务逻辑都在后端,前端只是展示层。
2.2 从数据库配置文件反推技术栈和运行环境
这类多开版系统常用 ThinkPHP 或 CodeIgniter 写成,数据库配置集中在同一个 PHP 文件里。先确认入口文件和配置格式,再决定装什么版本的 PHP。很多老包在 PHP 8 以上会直接白屏,所以一开始就把运行环境定对,后面能省掉大量排错时间。我会用file命令判断入口文件类型,再用head快速浏览数据库配置,不需要一开始就把整个项目读完。
file server/index.php server/public/index.php 2>/dev/null head -n 30 server/config/database.phpfile命令能识别出index.php是不是 PHP 脚本。有的包入口放在server/public,说明采用了前后端目录分离,Web 根目录要指到public。head -n 30看配置数组,看到return [ 'type' => 'mysql', 'hostname' => '127.0.0.1' ]就是 ThinkPHP 风格;看到define('DB_HOST', '127.0.0.1')则是原生写法或 CI 框架。这两种情况的配置文件路径不同,改法也不同。如果看到database.php里已经写了生产库连接串,说明作者发货前改过,你需要先确认后端有没有暴露源码,再决定是否保留。参数说明:2>/dev/null只是把报错信息丢掉,方便输出更干净。
2.3 在小程序前端里找 appid、serverUrl 和 siteId
多开版前端的核心是全局配置,通常存在app.js、utils/config.js或config/index.js里。里面至少要能找到appid、serverUrl、siteId三类变量。这三个值分别表示“这个小程序是谁、接口去哪、数据归哪个店”。如果这套包用 uniapp 开发,配置位置会在manifest.json和common/config.js里,思路是一样的。改配置时不要把appid和siteId写混,否则登录能过但数据全串。
// app.js 顶部 App({ globalData: { appid: 'wx1234567890abcdef', // 替换成当前小程序的 AppID serverUrl: 'https://member.example.com', // 后端接口域名,必须是 HTTPS siteId: 10001, // 当前门店/品牌的多开ID version: '1.0.33' }, onLaunch() { // 检查登录 token 是否存在 if (!wx.getStorageSync('yikeyun_token')) { wx.reLaunch({ url: '/pages/login/login' }); } } });这里siteId决定了后端读写哪一套会员数据,它最好在打包时写死,而不是从接口动态下发。如果同一个小程序内要切换门店,siteId才会变成动态变量,但多开版更多是每个小程序固定一个值。参数说明:appid必须和微信公众平台注册的小程序一致,serverUrl不能带路径,后面拼接接口时统一在代码里加/api/v1。还需要同步检查project.config.json里的appid字段,和app.js里的保持一致,否则微信开发者工具一直提示appid 不匹配。这个字段我在交付前都会用批处理脚本统一替换,避免漏改。
2.4 用 grep 确认多开标识藏在哪些表里
拿到陌生源码包,最快的方式是全目录搜site_id、store_id、merchant_id这类字段。这一步能直接判断这套系统到底是不是“真多开版”,也能找出核心表清单。有的系统只是把登录态区分了一下,业务表完全没隔离,这种包勉强接单会埋大雷。我会把搜索结果按照出现次数排序,次数最多的表就是需要优先修改的对象。
grep -rE "site_id|store_id|merchant_id|shop_id" server/ --include="*.php" -l 2>/dev/null | head -n 20这条命令递归查找server目录下所有 PHP 文件里出现这些关键字的文件名。-l只打印文件名,不打印匹配行,避免刷屏。如果输出里出现member.php、card.php、recharge.php,说明这三个核心模块都有隔离字段;如果只有Auth.php里有,那多半只是“多应用”而不是多租户。参数说明:head -n 20限制输出行数,防止文件多的时候终端卡住。搜完 PHP 之后,还要去database/*.sql里统计带site_id的表数量,少于 5 张的话,后期接多家门店时就要自己补字段了。
3. 多开原理:一套后端怎么把多个小程序的会员数据隔离干净
3.1 多开版与单商户版在数据表设计上的差异
单商户版会员卡系统只需要考虑一张会员表,卡号是唯一的,手机号也是唯一的。多开版最大的变化是所有业务表都要带上“租户标识”。以member_card为例,单商户版可以没有site_id,多开版必须有这个字段,而且在联合索引里要把site_id放第一位。否则数据库在几十万会员里查WHERE card_no = ?时会先按卡号过滤,再回头筛site_id,卡号跨店重复时,A 店会员能查到 B 店的数据。下面是一张常见的会员卡表设计。
CREATE TABLE `member_card` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `site_id` int unsigned NOT NULL DEFAULT 0 COMMENT '多开站点ID', `member_id` int unsigned NOT NULL COMMENT '会员ID', `card_no` varchar(32) NOT NULL COMMENT '会员卡号', `balance` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '储值余额', `points` int NOT NULL DEFAULT 0 COMMENT '累计积分', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1正常 0冻结 2挂失', `created_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_site_member` (`site_id`, `member_id`), KEY `idx_site_cardno` (`site_id`, `card_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员卡信息表';逻辑说明:这里的site_id不是外键,它只是一个普通整数,用来把同结构数据逻辑隔开。所有where条件必须把它放在第一位,例如WHERE site_id = ? AND card_no = ?。如果索引顺序是(card_no, site_id),同一个卡号最多的商户会挤压其他商户的查询效率。参数说明:balance用decimal(10,2)而不是float,浮点数计算会在储值扣款时产生 0.30000000000000004 这类脏数据。points用int,不需要小数。
3.2 小程序端如何开局:appid、serverUrl、siteId 三件套
多开版小程序通常是一份代码拷贝成多份,每份改appid和siteId后单独上传。微信公众平台的 AppID 不同,但实现逻辑完全一样。用户打开小程序后,要先wx.login拿临时 code,再连siteId一起发给后端,后端拿 code 去微信接口换 openid 和 session_key,最后返回自定义 token。这个流程里最容易错的是把appid也一起传,因为不同小程序对应的微信开放平台密钥不同,后端必须靠appid区分。
const config = require('../../config.js'); // 页面 onLoad 中触发登录 wx.login({ success: (loginRes) => { wx.request({ url: config.serverUrl + '/api/auth/login', method: 'POST', data: { code: loginRes.code, // 临时凭证,5 分钟内有效 site_id: config.siteId, // 每个小程序固定传自己的 appid: config.appid // 后端需要知道是哪个小程序来的 }, header: { 'content-type': 'application/x-www-form-urlencoded' }, success: (res) => { if (res.data.code === 0) { wx.setStorageSync('yikeyun_token', res.data.data.token); wx.setStorageSync('yikeyun_site', config.siteId); } } }); } });这段代码里的site_id和appid都要从全局配置读取,不要硬编码在具体页面里,否则换门店时要全文搜索替换。参数说明:loginRes.code是一次性的,后端换过 session_key 后就失效,所以前端不能重复调用wx.login,也不能在onLaunch和页面onLoad里各调一次。wx.setStorageSync保存的 token 建议带上site_id后缀,例如yikeyun_token_10001,避免同一台手机测试多个门店时 token 互相覆盖。
3.3 后端获取当前站点标识的常用写法
如果前端每个请求都带site_id,后端就不要相信某个默认值。最稳妥的写法是:先读自定义请求头X-Site-Id,读不到再读参数site_id,并强制转成整数。放在公共控制器里,所有接口继承后直接用$this->siteId。这种写法也能兼容小程序端分享卡片打开页面时参数不在 body 里的情况,header 总是更稳定。
class BaseController { protected int $siteId = 0; public function __construct() { $this->siteId = $this->resolveSiteId(); if ($this->siteId <= 0) { throw new \Exception('缺少站点标识', 403); } } private function resolveSiteId(): int { $siteId = $_SERVER['HTTP_X_SITE_ID'] ?? ''; if ($siteId !== '' && ctype_digit($siteId)) { return (int) $siteId; } $siteId = $_REQUEST['site_id'] ?? 0; return ctype_digit((string) $siteId) ? (int) $siteId : 0; } }逻辑说明:HTTP_X_SITE_ID是 PHP 把请求头X-Site-Id转换后的变量名。用ctype_digit判断全是数字,可以挡住1 OR 1=1这类注入前奏。如果没有site_id,直接抛 403,比返回一个空数据列表更安全,因为运营能立刻发现问题,而不是在拿到错误数据后继续使用。参数说明:如果你的后端是 Java 或 Node,只需在中间件里实现同样的逻辑,优先从 header 取值,并校验为整数。
3.4 多开版公共缓存的 key 设计
多个小程序共用一套 Redis 时,缓存 key 不加前缀是灾难。比如登录 token 以token:%s保存,A 店用户拿到一个 token 字符串,B 店后端如果公共 Redis 里有同一个 key,就可能被当成同一个人。正确做法是所有缓存 key 都以site_id开头,并且 token 本身在生成时也绑定site_id。下面是一段 PHP 的缓存读取示例。
$siteId = $this->siteId; $cacheKey = sprintf('ykc:member:%d:%d', $siteId, $memberId); $memberInfo = Redis::get($cacheKey); if (!$memberInfo) { $memberInfo = Db::table('member') ->where('site_id', $siteId) ->where('id', $memberId) ->find(); Redis::setex($cacheKey, 3600, json_encode($memberInfo)); }这里的 key 结构是ykc:member:site_id:member_id,不同站点天然隔离。setex设置 3600 秒过期,会员资料变更后要先删除旧缓存再更新数据库。参数说明:过期时间不要设置太长,会员卡余额更新很频繁,10 分钟到 1 小时比较合适;清缓存可以只Redis::del($cacheKey),不要为了省事执行flushdb,否则全站登录态全部掉线。这个设计同样适用于卡券、核销码、门店配置这些高频读取数据。
4. 部署 1.0.33:从数据库导入到微信后台配置域名
4.1 环境要求和 PHP 配置项
1.0.33 这类多开版系统常见要求是 PHP 7.x 以上、MySQL 5.7 或 8.0、Redis 可选。安装前先检查 PHP 扩展,特别是pdo_mysql、redis、curl、openssl。缺openssl会导致微信登录换 session_key 直接失败,缺curl则无法请求微信接口。很多老包在 PHP 8.2 上会报Deprecated提示,我一般优先用 PHP 7.4 稳定跑这类包。
php -v php -m | grep -E 'pdo_mysql|redis|curl|openssl|fileinfo|mbstring'输出里应该能看到pdo_mysql、curl、openssl、fileinfo。fileinfo影响上传头像时识别文件类型,mbstring影响中文字符串截取。参数说明:grep -E使用扩展正则,|表示多个扩展名之间取“或”。如果某个扩展没装,例如redis,可以先用pecl install redis安装,再重启 PHP-FPM。对于不需要 Redis 的站点,要检查config.php里是否强制启用,如果强制启用但服务没装,后端接口会全部报 500。
4.2 导入数据库并改连接串
解压后数据库脚本可能在database/install.sql或server/data/install.sql。先建库再导入,导入完成后统计表数量,对比系统说明里的数字。随后改后端连接配置。这里以 ThinkPHP 风格的database.php举例。不要复制粘贴整个文件覆盖,只改主机、库名、账号、密码这几处即可。
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS yikeyun DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -u root -p yikeyun < /data/www/yikeyun/database/install.sql mysql -u root -p -e "SHOW TABLES FROM yikeyun;" | wc -l # 用 sed 将配置里的占位值替换成实际值 sed -i "s/'hostname' => '127.0.0.1'/'hostname' => '127.0.0.1'/; s/'database' => 'yikeyun'/'database' => 'yikeyun'/; s/'username' => 'root'/'username' => 'yikeyun_user'/" server/config/database.phpwc -l统计表数量,方便判断 SQL 是否完整导入。sed -i在文件里原地替换,分号分隔多个替换条件。这里把root替换成yikeyun_user,实际生产环境不要用 root 连接数据库,最小权限账号只给这个库的SELECT、INSERT、UPDATE、DELETE。改完后用php -l server/config/database.php检查语法,语法错误会导致整个接口全部 500。参数说明:如果你的 SQL 文件是install.sql.gz,需要先gunzip解压再导入;导入时如果大量报错,看是不是utf8mb4与旧版 MySQL 字符集兼容问题。
4.3 微信小程序后台的域名白名单和业务域名配置
微信小程序请求接口,必须在小程序后台「开发管理-开发设置-服务器域名」里添加接口域名。注意只填域名,不填https://和路径。线上环境必须全站 HTTPS,开发版可以临时勾选“不校验合法域名”。如果小程序内有跳转 H5 页面,还要在业务域名里配置。下面是一段兼容 ThinkPHP 的 Nginx 站点配置,给多开版后端提供 HTTPS 出入口。
server { listen 443 ssl http2; server_name member.example.com; ssl_certificate /etc/letsencrypt/live/member.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/member.example.com/privkey.pem; root /data/www/yikeyun/server/public; index index.php; location / { try_files $uri $uri/ /index.php?s=$uri; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; } }try_files是 ThinkPHP 常用伪静态写法,把不存在的路由交给 index.php 处理。微信小程序的请求是 JSON,不需要为/api单独配置 location。参数说明:fastcgi_pass的 sock 路径与 PHP 版本强相关,如果安装的是 php8.0,写成php8.0-fpm.sock才能对应。执行nginx -t通过后systemctl reload nginx。配好后用curl -I https://member.example.com检查响应头,确认返回 200 而不是 404。
4.4 用一张表维护 appid 和 site_id 的映射
多开版需要一张site_app之类的映射表,把微信小程序 AppID 和site_id绑定,否则后端仅凭前端传的site_id无法判断是谁。常见做法是安装包里自带site表,包含id、name、appid、status。如果原包没有这张表,我在部署时会补一段 SQL,并同步调整后端登录逻辑。
ALTER TABLE `site` ADD COLUMN `appid` varchar(64) NOT NULL DEFAULT '' COMMENT '微信小程序AppID' AFTER `id`; INSERT INTO `site` (`id`, `name`, `appid`, `status`) VALUES (10001, '示例门店A', 'wx1234567890abcdef', 1), (10002, '示例门店B', 'wxabcdef1234567890', 1);后端登录接口收到code和site_id后,先查site表校验appid是否匹配,匹配才继续换 session_key。这样即使有人改了请求体里的site_id,也会因为appid对不上而被拒绝。参数说明:site_id从 10001 开始,避免和自增的 id 混淆;appid一定不要配错,配错了用户登录时会报appid 与 site_id 不匹配,而且这个问题在开发者工具里不容易发现,只有真机才能验出来。
5. 避坑:多开会员卡系统上线前后最容易翻车的 5 处
5.1 多开串号:A 店会员列表出现 B 店会员
现象:门店 A 运营在后台会员列表里能看到门店 B 的会员和一整条储值记录,删掉一笔后 B 店同样少钱。
原因:列表查询用了where('mobile', $mobile)而漏了site_id,或者site_id字段在表里根本不存在。常见于从单商户版改多开版时,只改了新增接口,历史查询没改。这个现象最隐蔽的地方是后台默认只显示第一页,通常要滚动到第二页才发现混入外部数据。
解决:把member、member_card、recharge_log、consume_log四张表的查询全部加上site_id条件。修复后用两个测试账号分别造数,跨店查询验证隔离。代码写法如下。
// 错误写法:全库查询 $list = Db::table('member_card')->where('status', 1)->select(); // 正确写法:强制带 site_id $list = Db::table('member_card') ->where('site_id', $this->siteId) ->where('status', 1) ->select();这类问题靠人工一遍遍翻代码效率太低,我会直接用 grep 搜索所有->where('mobile'、->where('card_no'的地方,逐行检查前面有没有site_id条件。如果是原生查询,则搜索SELECT语句。修复后要测一个边界情况:门店 A 创建一个卡号,门店 B 也能创建同一个卡号,这是多开版的合法行为,因为隔离字段是site_id,卡号允许跨店重复。
5.2 微信登录静默失败:返回 invalid code
现象:用户打开小程序,登录接口返回{"code":40029,"msg":"invalid code"},重试偶尔成功,过一会又失败。
原因:常见场景是前端在app.js的onLaunch里调用了一次wx.login,又在首页onLoad里调用一次,两个 code 都发给后端;后端用第一个 code 换过 session_key 后,第二个 code 就失效了。另外,如果服务器时间与标准时间偏差超过 5 分钟,微信接口校验 code 时也有概率报错。
解决:保证wx.login在会话生命周期内只调用一次,拿到 code 后立刻发起登录。若仍然失败,检查服务器时间并重启 PHP-FPM。
date -u +"%Y-%m-%d %H:%M:%S" # 如果时间误差超过 5 分钟,执行 sudo timedatectl set-ntp true sudo systemctl restart php7.4-fpm时间同步后,原来的invalid code大概率消失。如果多开后端的登录接口写入了缓存,务必把 openid、session_key、site_id 三个值一起缓存,不要只缓存 session_key,否则后续解密手机号时找不到对应的 openid。参数说明:timedatectl set-ntp true开启 NTP 自动同步,适合长期运行的云服务器。另外,微信官方 code2Session 接口有每日调用量限制,不要每进来一个页面就调用一次,正确做法是登录完成后用自定义 token 维持会话。
5.3 储值余额并发扣款:用户开两个页面同时付款,余额变成负数
现象:会员卡余额明明是 10 元,收银台快速扫码扣款,两笔都显示成功,余额变成 -10 元。
原因:PHP 代码先查余额、再判断是否足够、最后执行update,这三步不是原子操作。两个请求同时读到余额 10,各自判断足够,都执行balance = 10 - 10,如果业务允许负数,最终余额就是 -10。这是微信小程序会员卡支付里最典型的翻车场景。
解决:把“判断-扣款”合并成一条条件更新 SQL,利用数据库行锁保证原子性。不要先在代码里查余额再扣款。
UPDATE member_card SET balance = balance - #{amount} WHERE site_id = #{siteId} AND card_no = #{cardNo} AND balance >= #{amount};如果影响行数为 0,说明余额不足,后台要返回“余额不足”并终止后续流程。参数说明:金额统一用decimal类型,业务层不要用浮点计算。注意balance >= #{amount}是扣款成功的必要条件,缺了它就会重蹈负数问题。对于积分扣减,思路相同,只是把balance换成points。真正的支付流程里,还要在流水表里插入一条扣款记录,并和这笔 SQL 放进同一个事务,事务超时时间设置为 5 秒以内,避免锁等待过长。
5.4 小程序线上白屏:request 域名不合法或证书链残缺
现象:开发工具里一切正常,发布体验版后一片空白,console 报https://member.example.com 不在以下 request 合法域名列表中,有时报 404。
原因:小程序后台服务器域名没有添加,或添加的域名与serverUrl不完全一致。比如后台填了member.example.com,但前端配置里写的是https://member.example.com/api,微信要求只填根域名。另外,证书链只有叶子证书时,微信端用系统证书库校验会失败,体验版直接白屏但浏览器访问却正常,这个问题很误导人。
解决:先在小程序后台补齐 request 合法域名,再检查证书链完整度。以下命令用来验证站点证书链是否完整。
openssl s_client -connect member.example.com:443 -servername member.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -enddate命令输出里的issuer如果显示的是你自己的域名而不是中间证书机构,说明证书链不完整。常见修法是在 Nginx 里使用fullchain.pem,而不是只填站点证书。开发过程中可以勾选“不校验合法域名”,但提审前必须取消,否则用户端会白屏。参数说明:-servername是 SNI,多域名证书场景下千万不能省。用完之后用浏览器隐身窗口访问一次 API 地址,如果地址栏出现红色警告,微信端也会同样失败。
5.5 升级 1.0.33 后旧会员卡号查不到
现象:把数据库和代码都替换成 1.0.33 后,会员数没少,但后台按卡号搜不到,储值记录只剩最近几单。
原因:升级包里的 SQL 可能在ALTER TABLE时重建了表,但旧库的字符集或自增字段没有迁移干净;或者新版本改用site_id做隔离,而旧数据只有store_id字段,两列没有映射上。这是把旧的单商户库直接对接到多开版时最常踩的坑。
解决:升级前先备份旧库,不要直接删除旧表。以下是迁移字段的一种做法。
ALTER TABLE `member_card` ADD COLUMN `site_id` int unsigned NOT NULL DEFAULT 10001 AFTER `id`; -- 如果原来有 store_id,可以先建立映射 UPDATE `member_card` SET `site_id` = `store_id` WHERE `store_id` > 0;迁移完成后,对比总数、余额总和、最近一笔交易的卡号,三项都对得上再清掉备份表。参数说明:DEFAULT 10001 只适合“该库原本就全是一家店”的场景;如果原来有多个店,必须分别从store表反查site_id,不能统一塞默认值。升级后还应该重新上传小程序,因为前端配置里可能也新增了字段,老版本前端请求没有带site_id,后端会全部返回 403。
6. 进阶:给多开系统写一个多租户隔离的验证脚本
6.1 用 curl 模拟两个小程序同时调接口
新环境部署完,我会写一个 shell 脚本模拟两个不同site_id的请求,看是否互相串数据。以下用会员查询接口举例。这个脚本不只是验一次,我会在每次改完查询逻辑后都跑一遍,血泪经验告诉我,所谓“只是加一个字段”的改动经常漏条件。
# 模拟门店 A 查询卡号 8888 curl -s -X POST https://member.example.com/api/card/query \ -H "Content-Type: application/json" \ -H "X-Site-Id: 10001" \ -d '{"card_no":"8888"}' | python3 -m json.tool # 模拟门店 B 查询同一个卡号 curl -s -X POST https://member.example.com/api/card/query \ -H "Content-Type: application/json" \ -H "X-Site-Id: 10002" \ -d '{"card_no":"8888"}' | python3 -m json.tool重点看返回结果里的site_id和会员姓名:如果 A 返回的是 B 的会员,说明查询漏了隔离条件。参数说明:-H "X-Site-Id: 10001"对应后端resolveSiteId里的HTTP_X_SITE_ID;如果后端只认参数,可以改成-d '{"site_id":10001,"card_no":"8888"}'。返回结果建议直接用python3 -m json.tool格式化,日志里找关键字段比普通打印清楚得多。
6.2 通过导出报告确认会员总量和储值总额隔离
接口只能验证单个请求,正式验收要看汇总数据。我会执行下面这段 SQL,把每个site_id的会员数和储值总额一次拉出来,确认数据没有互相污染。这也是每次升级后必做的验证项。
SELECT site_id, COUNT(*) AS member_count, SUM(balance) AS total_balance FROM member_card GROUP BY site_id;正常结果应该每个site_id一行,数值与门店登记表一致。如果出现site_id = 0,说明导入时有数据漏传,需要补录。把这份 SQL 存成check_isolation.sql,放到项目根目录的tools下,后面每次发版都跑一遍。参数说明:SUM(balance)在数据量大时注意总数是否溢出;余额是decimal(10,2)时不会溢出,但如果有int类型的积分字段,要用CAST(points AS SIGNED)避免老版本 MySQL 报错。
6.3 一个维护习惯,希望帮到你
我维护多开版会员卡系统时有个固定习惯:每个门店的site_id写在部署文档第一行,统一起始 10001;数据库账号用最小权限,不给整个 MySQL 的 root;后端所有查询函数都强制调用BaseController->siteId,禁止裸查任何业务表。这套东西本身不复杂,难在每次改动都记得带上隔离条件。遇到线上串数据,先停工排查,再总结经验,而不是靠临时补丁把问题压下去。希望帮到你。
本文还有配套的精品资源,点击获取