ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

美容院会员管理系统源码拆解:从数据模型到扣费对账与流失预警

美容院会员管理系统源码拆解:从数据模型到扣费对账与流失预警

简介:这是一套面向美容院、美妆门店经营者及中小型连锁机构的后台管理系统源码,也可供PHP开发者作为权限控制与业务模块的实战参考。系统把菜单、按钮级权限细分到每个功能点,会员建档、项目套餐、消费记录、员工提成等模块均配备完整的增删改查操作,并内置Excel导出,便于门店做数据汇总与对账,微信端则承接线上预约、会员查询等场景。压缩包共1601个文件,约23.4MB,以662个PHP业务脚本为核心,配合272个HTML模板、148个JS与35个CSS构建前后端界面,另有GIF、PNG、JPG等图片素材与18个配置文件及字体、说明文档,目录结构接近可直接部署的成品。已有3259人学习下载,适合需要快速搭建门店会员体系或研究权限粒度设计的读者。

1. 美容院会员管理系统源码值不值得拆:先看它能不能管住“会员余额”这笔账

某门店用表格记会员,充值送次卡,员工离职后客户档案散在几部手机里,月底对账差出几千块。这套美容院会员管理系统源码把会员档案、储值余额、次卡扣次、预约和店铺流水放进同一套库,微信端让会员自己查余额、看剩余次数、约项目。它适合单店和小型连锁做店务底座,也适合接私活时当会员模块的起点。能不能用,不取决于页面多漂亮,而取决于充值、消费、退款三条流水是否对得上。下面按拆包、跑通、扣费逻辑、避坑、预警五步拆。

2. 拆包先看数据模型:会员、卡项、订单、提成四张主表怎么串

2.1 目录结构与技术栈判断

拿到源码包先别改页面,先看目录。这类美容院会员管理系统源码常见的结构是后端服务、后台前端、微信端、SQL 脚本和说明文件分开,名字可能叫server、admin、miniprogram、sql、docs。目录名不重要,关键是先确认后端是 PHP 还是 Java,后台是 Vue 还是原生模板,微信端是原生小程序还是编译型框架。判断方式不是看 README 口号,而是看依赖文件:composer.json看 PHP 依赖,pom.xml看 Java 依赖,package.json看前端构建。下面几条命令是我拆包时固定跑的,先确认环境,再决定用哪条安装链。

# 在源码根目录执行,先看依赖文件 find . -maxdepth 3 -name "composer.json" -o -name "pom.xml" -o -name "package.json" # 看 PHP 版本约束,不要只看 README 写“环境不限” php -v composer -V # 如果是 Java 包,再看 JDK 和 Maven java -version mvn -v

代码里find只找三层,避免在vendor、node_modules里翻太久。php -v和composer -V用来对照composer.json里的require,低于约束就先换运行环境,不要硬装依赖,后面会出现一堆看不懂的扩展报错。Java 包同理,先看pom.xml的java.version,再决定 JDK。前端目录如果只有package.json没有src,多半是编译后的后台静态文件,改页面要回到源码分支,别在压缩后的 JS 里找接口。

我一般会先做一个最小判断:如果包内有public/index.php、app、config、route,后端大概率是 PHP 框架;如果根目录有src/main/java、application.yml,就是 Java 服务。微信端目录里看app.json、project.config.json、utils/request.js,这几个文件能快速确认请求封装和登录态放在哪。技术栈判断清楚再动手,能少走半天弯路。

2.2 核心表关系与字段设计

会员系统的数据模型不复杂,但很容易在“卡”和“订单”之间做错。我的理解是:member是会员档案,card_type是卡项模板,member_card是会员实际持有的账户,recharge_order记充值,consume_order记消费扣次,commission_log记员工提成。门店隔离靠store_id,连锁跨店靠卡上的scope或store_ids字段。很多源码把“会员卡”直接写成“卡类型”,结果同一张次卡卖了十次只有一个余额字段,后面没法对账。

字段设计上,金额我倾向decimal(12,2),不要用float;次数用int;状态用短枚举;每张流水表都留request_id做幂等。会员手机号在单店可以store_id + mobile唯一,连锁则建议会员全局唯一,再用member_store关联门店。下面这张表是我拆这类源码时重点核对的字段,缺一个后面就可能出现“余额对但次数不对”的血泪经验。

表/字段作用核对要点
member.store_id会员归属门店连锁场景不要只靠这个字段做隔离
member_card.balance实充余额与赠送余额分开记
member_card.gift_balance赠送余额退款时通常不退这部分
member_card.times次卡剩余次数扣次必须带条件更新
card_type.scope单店/跨店跨店要解决总部与门店结算
recharge_order.request_id充值幂等唯一索引,防重复回调
consume_order.request_id消费幂等防前端连点、网络重试
commission_log.order_id提成来源退款要能反向冲回

如果源码里只有balance没有gift_balance,充值送 500 和实充 500 混在一起,退款时就会扯皮。如果member_card没有expire_at,次卡到期只能靠人工看,微信端也查不出“还有几天过期”。这些字段不是锦上添花,是后面做预约、提醒、对账的基础。

2.3 用 SQL 跑通一条“充值-消费-扣次”链路

看表不如跑一条链路。下面这段 SQL 是简化后的建表片段,保留关键字段,方便在本地验证逻辑。真实源码字段可能更多,但member_card作为账户、订单表留幂等键、扣次带条件更新这三件事不能少。

CREATE TABLE `member` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `store_id` int unsigned NOT NULL, `mobile` varchar(20) NOT NULL, `nickname` varchar(64) DEFAULT NULL, `points` int NOT NULL DEFAULT 0, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_store_mobile` (`store_id`,`mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `member_card` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `store_id` int unsigned NOT NULL, `member_id` bigint unsigned NOT NULL, `card_type_id` int unsigned NOT NULL, `balance` decimal(12,2) NOT NULL DEFAULT 0.00, `gift_balance` decimal(12,2) NOT NULL DEFAULT 0.00, `times` int NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 1, `expire_at` date DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_member` (`member_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `consume_order` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `request_id` varchar(64) NOT NULL, `store_id` int unsigned NOT NULL, `member_id` bigint unsigned NOT NULL, `card_id` bigint unsigned NOT NULL, `amount` decimal(12,2) NOT NULL DEFAULT 0.00, `times` int NOT NULL DEFAULT 0, `status` varchar(16) NOT NULL DEFAULT 'paid', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_request` (`request_id`), KEY `idx_member_time` (`member_id`,`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表之后,用一条事务模拟“扣一次次卡”。先插订单拿到流水,再锁卡、判断次数、扣减、提交。真实代码里判断和更新通常写在服务层,这里用 SQL 把顺序摆清楚。

START TRANSACTION; -- 1. 先落消费流水,request_id 唯一,防重复提交 INSERT INTO consume_order (order_no, request_id, store_id, member_id, card_id, amount, times, status) VALUES ('C20250101001', 'req-demo-001', 1, 2001, 1001, 0.00, 1, 'paid'); -- 2. 锁住这张会员卡,避免两个收银台同时扣 SELECT id, balance, gift_balance, times, status FROM member_card WHERE id = 1001 AND member_id = 2001 FOR UPDATE; -- 3. 带条件扣次,影响行数为 0 说明次数不足或卡已停用 UPDATE member_card SET times = times - 1 WHERE id = 1001 AND times >= 1 AND status = 1; COMMIT;

这段 SQL 里request_id是幂等键,前端每次提交带一个唯一值,重复请求会撞唯一索引,后端捕获后直接返回上一笔结果。FOR UPDATE锁的是会员卡行,不是整张表,所以要把事务写短,不要在锁住之后再去调远程接口。UPDATE ... WHERE times >= 1的影响行数必须检查,等于 0 就ROLLBACK,否则会出现负次数。金额字段这里用decimal(12,2),如果团队习惯用整数分,就把所有金额改成int,但不要在同一个项目里混用。

3. 本地跑起来:后端环境、数据库导入与微信端联调

3.1 后端环境检查与依赖安装

本地跑这类源码,第一步不是改代码,是让依赖装干净。PHP 包进根目录跑composer install,Java 包跑mvn clean package,前端后台进admin或web目录跑npm install。如果composer install报扩展缺失,先看composer.json的require里有没有ext-gd、ext-bcmath、ext-pdo_mysql,这些在会员系统里很常见:图形验证码、金额计算、数据库连接都会用到。不要看到报错就删依赖,先补扩展。

# 后端依赖安装,PHP 项目示例 cd /data/app/server composer install --no-dev --optimize-autoloader # 如果包内提供安装脚本,先看脚本内容再执行 ls -la cat README.md | sed -n '1,80p' # 本地启动可以用内置服务器先验通,生产不要这么跑 php -S 127.0.0.1:8000 -t public

--no-dev表示不装开发依赖,本地调试可以去掉;--optimize-autoloader对生产有用,本地不强求。php -S只适合验证路由和接口,静态资源、重写规则、并发都不如 Nginx,所以本地跑通后要换 Nginx 再验一遍。Nginx 的根目录要指向public,不要把根目录指到项目根,否则.env、composer.json可能被直接访问。

server { listen 80; server_name local.member.test; root /data/app/server/public; index index.php index.html; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }

root指向public是这类框架的常见入口,try_files把后台路由交给index.php。fastcgi_pass按本机 PHP-FPM 实际监听地址改,不要照抄。配置改完先nginx -t再 reload,别直接重启。后台如果打不开,先看 Nginx 的error_log和框架的runtime/log,不要一上来就怀疑源码。

3.2 数据库导入与配置项逐项说明

数据库配置集中在.env或config/database.php。我一般先复制一份.env.example,再逐项改,不直接改示例文件。下面这些参数是会员系统最常出问题的:数据库名、用户名、密码、端口、字符集、时区、调试开关、接口签名盐。字符集用utf8mb4,否则会员昵称里的特殊字符会存不进去;时区统一成东八区,不然后台按天统计会和实际营业日差一截。

cd /data/app/server cp .env.example .env # 导入数据库,文件名按包内 sql 目录实际名称改 mysql -u root -p -e "CREATE DATABASE member_demo DEFAULT CHARSET utf8mb4;" mysql -u root -p member_demo < sql/install.sql # 检查表是否导入完整 mysql -u root -p member_demo -e "SHOW TABLES;"
APP_ENV=local APP_DEBUG=false APP_URL=http://127.0.0.1:8000 DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=member_demo DB_USERNAME=member_user DB_PASSWORD=change_me DB_CHARSET=utf8mb4 API_SIGN_SALT=replace_with_random_string

APP_DEBUG本地可以开,上线必须关,否则异常页会暴露路径和 SQL。API_SIGN_SALT不要用示例值,换成随机串,微信端和服务端签名要一致。DB_CHARSET必须是utf8mb4,不要用utf8。导入 SQL 后不要只看“没有报错”,要SHOW TABLES对一遍核心表:会员、会员卡、卡类型、充值订单、消费订单、提成流水、门店、员工。少表就回看 SQL 文件有没有分段执行失败。

提示:导入前先看 SQL 文件里有没有DROP TABLE,本地库无所谓,别在已有数据的库上直接跑。

3.3 微信端登录态和接口域名配置

微信端不要一上来调业务接口,先把登录态跑通。常见流程是:微信端调登录拿code,传给后端,后端换openid和session_key,生成本地token返回,微信端把token存本地,后续请求带在请求头。这里有两个坑:一是把密钥写进微信端代码,二是接口域名用 HTTP 或和后台配置不一致。密钥只能放服务端环境变量,微信端只拿临时code。

// utils/request.js 或 config.js,按包内实际文件改 const BASE_URL = 'https://your-domain.example/api'; const STORE_ID = 1; function request(path, method = 'GET', data = {}) { const token = wx.getStorageSync('token') || ''; return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'content-type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success(res) { if (res.statusCode === 401) { wx.removeStorageSync('token'); reject(new Error('login_expired')); return; } resolve(res.data); }, fail: reject }); }); } module.exports = { request, STORE_ID };

BASE_URL必须是 HTTPS,并且和移动端后台配置的合法域名一致;本地调试可以在开发者工具里关掉域名校验,但真机必须配。STORE_ID是单店场景的兜底,连锁场景不要让前端写死,登录后由后端返回可访问门店列表。Authorization用Bearer只是示例,实际按源码约定改。登录接口返回token后,微信端只存token,不要存openid当身份凭证,openid可以被伪造请求头。

3.4 用 curl 验证三个关键接口

页面能点不代表接口通,我习惯先用 curl 跑三条链路:登录、查会员、创建充值。这样能把跨域、鉴权、参数格式的问题先排掉。下面命令里的域名和 token 都换成自己的,-H里的Authorization按源码实际请求头改。

# 1. 登录,拿 token curl -s -X POST 'http://127.0.0.1:8000/api/member/login' \ -H 'Content-Type: application/json' \ -d '{"code":"demo_code","store_id":1}' # 2. 用 token 查会员信息 TOKEN='replace_with_login_token' curl -s 'http://127.0.0.1:8000/api/member/info' \ -H "Authorization: Bearer ${TOKEN}" # 3. 创建一笔充值订单,验证金额单位和幂等键 curl -s -X POST 'http://127.0.0.1:8000/api/recharge/create' \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer ${TOKEN}" \ -d '{"amount":100,"request_id":"req-demo-1001","store_id":1}'

登录接口重点看返回里有没有token、member_id、store_id;查会员重点看balance、gift_balance、times是否存在且类型正确;创建充值重点看金额单位是元还是分,request_id重复提交会不会返回同一笔。如果接口返回code=0但数据为空,先看后端日志的 SQL,不要只看前端提示。curl 跑通后再回微信端,能少很多“前端说接口挂了”的扯皮。

4. 会员卡与储值扣费逻辑:参数怎么设才不翻车

4.1 储值、次卡、疗程卡的区别与选型

美容院的卡不是一种东西。储值卡看余额,次卡看次数,疗程卡通常绑定项目,积分是营销资产。源码如果只做“余额”一个字段,后面做次卡就要改表。我的建议是:card_type里用type区分stored、times、package、points,member_card同时保留balance、gift_balance、times、expire_at,不同卡类型用不同字段,不互相覆盖。储值卡适合充值送礼,次卡适合洗脸、脱毛这类固定项目,疗程卡适合按疗程核销,积分只做兑换,不要当钱花。

卡类型计价方式适用场景主要风险
储值卡余额扣减充值送金额、通用消费赠送金额退款争议
次卡次数扣减固定项目、固定次数负次数、跨店扣次
疗程卡项目+次数多项目组合疗程项目绑定错、核销乱
积分积分增减签到、消费返积分积分当余额、超发

选型参数里,expire_at要不要设看门店政策,但系统必须支持,否则到期提醒做不了。scope控制单店还是跨店,单店就写store,连锁写all或具体门店 ID 列表。refundable标记是否可退,gift_balance单独记赠送,退款时只退实充部分。很多源码把“充值送 500”直接加进balance,后面退款按balance退,门店就亏了。

4.2 扣费顺序与并发控制

扣费顺序我一般按“项目对应疗程卡 → 次卡 → 储值余额 → 现金补差”走。原因是疗程卡和次卡已经预收了钱,优先核销能减少余额争议;储值余额最后扣,现金补差走线下。这个顺序要写进配置或服务层,不要散落在多个控制器里。并发控制靠三件事:事务、行锁、幂等键。事务保证订单和卡余额一起成功或一起失败,行锁保证两个收银台不会同时扣同一张卡,幂等键保证网络重试不会重复扣。

START TRANSACTION; -- 锁卡:只锁当前会员卡,事务里不要做远程调用 SELECT id, balance, gift_balance, times, status FROM member_card WHERE id = 1001 AND member_id = 2001 FOR UPDATE; -- 先落消费流水,request_id 唯一 INSERT INTO consume_order (order_no, request_id, store_id, member_id, card_id, amount, times, status) VALUES ('C20250101002', 'req-demo-002', 1, 2001, 1001, 0.00, 1, 'paid'); -- 带条件扣次,影响行数为 0 就回滚 UPDATE member_card SET times = times - 1 WHERE id = 1001 AND times >= 1 AND status = 1; COMMIT;

这段事务里,SELECT ... FOR UPDATE必须和UPDATE在同一个连接、同一个事务里,否则锁会提前释放。request_id的唯一索引要建在订单表上,不要只靠前端防连点。如果扣的是余额,UPDATE要写成balance = balance - ? WHERE balance + gift_balance >= ?,并检查影响行数。事务里不要查远程配置、不要发消息、不要调支付回调,锁时间一长就容易死锁。真遇到死锁,先看数据库的innodb status,再看业务代码有没有按相同顺序锁多张卡。

4.3 提成计算与退款回滚

提成有三种常见口径:按充值实收、按消费金额、按项目核销。按充值实收最稳,门店回款当天就能算;按消费金额容易和退款纠缠;按项目核销适合服务老师分成。源码里如果有commission_log,先看它记的是订单 ID 还是卡 ID,退款时能不能找到原提成。我的做法是提成流水只增不改,退款生成负数流水冲回,不直接删原记录,这样月底对账能追。

提成口径计算基数优点注意
充值实收实充金额,不含赠送回款清楚退款要冲回
消费金额实际扣减金额与业绩挂钩次卡核销要折算
项目核销项目单价×次数服务老师易懂项目改价要留快照

退款不要直接改member_card.balance就完事。正确顺序是:生成退款单、按原充值拆分实充和赠送、扣回对应余额、写负数提成流水、更新原订单状态。下面这段 SQL 是退款回滚的简化示例,refund_order和commission_log的字段按源码实际改。

START TRANSACTION; INSERT INTO refund_order (order_no, request_id, store_id, member_id, source_order_id, amount, status) VALUES ('R20250101001', 'req-refund-001', 1, 2001, 9001, 100.00, 'paid'); -- 只退实充部分,赠送余额按门店规则处理 UPDATE member_card SET balance = balance - 100.00 WHERE id = 1001 AND balance >= 100.00; -- 提成冲回,不删原流水 INSERT INTO commission_log (order_id, staff_id, amount, type, created_at) VALUES (9001, 301, -10.00, 'refund', NOW()); COMMIT;

退款金额要加balance >= 100.00条件,影响行数为 0 就说明余额不够,直接回滚。赠送余额是否扣回看门店规则,但系统里要能配置,不要写死。提成冲回用负数流水,月底统计时按SUM(amount)自然抵消。退款后跑一遍对账:充值总额减退款总额,应该等于会员卡余额加已消费金额,差一分都要查。

5. 避坑与排查:源码能跑不等于能营业的四个硬问题

5.1 金额精度和时区对不上,报表每天差一截

现象是后台日报和收银小票差 0.01,或者晚上 11 点后的消费被算到第二天。原因通常有两个:金额字段用了float,多次加减后出现精度误差;PHP 和 MySQL 时区不一致,一个用东八区,一个用系统默认。解决方式是把金额统一成decimal(12,2)或整数分,PHP 端用bcadd、bcsub做金额运算;时区在数据库连接和 PHP 配置里都固定成东八区,统计 SQL 用DATE(created_at)前先确认连接时区。

-- 检查金额字段类型,出现 float/double 就要改 SHOW COLUMNS FROM member_card LIKE 'balance'; SHOW COLUMNS FROM consume_order LIKE 'amount'; -- 检查数据库当前时区 SELECT @@global.time_zone, @@session.time_zone, NOW();

SHOW COLUMNS看Type是不是decimal(12,2),不是就安排迁移。NOW()和 PHP 的date('Y-m-d H:i:s')对比,差几个小时就是时区没对齐。迁移时先加新字段、双写、回填、再切读,不要直接改线上字段类型。

5.2 微信端授权回调失败,登录一直转圈

现象是微信端点登录没反应,后端日志里没有code,或者回调地址报错。原因常见三类:接口域名不是 HTTPS,或和移动端后台配置的合法域名不一致;登录code只能用一次,前端重复用同一个code换 token;redirect_uri带了参数但没做编码。解决方式是先看微信端请求有没有发出去,再看后端有没有收到code,最后看换openid的返回值。域名白名单、HTTPS、回调地址三处要完全一致。

// 登录调用只拿 code,不在前端换 openid wx.login({ success(res) { if (!res.code) return; wx.request({ url: BASE_URL + '/member/login', method: 'POST', data: { code: res.code, store_id: STORE_ID }, success(loginRes) { if (loginRes.data && loginRes.data.token) { wx.setStorageSync('token', loginRes.data.token); } } }); } });

code传给后端就作废,前端不要缓存code。token存本地,过期后重新登录。密钥只放服务端环境变量,不要写进微信端代码。回调地址如果有参数,用encodeURIComponent包一层,别手工拼。

5.3 跨店通用没配好,A 店充值 B 店用不了

现象是会员在 A 店充了钱,到 B 店查不到余额,或者能查到但扣不了。原因通常是查询条件写死了store_id,而卡类型又没有跨店标记;另一种是会员档案按门店重复建,同一个手机号在两家店是两个会员。解决方式是先确认业务:单店就老老实实按store_id隔离,连锁就把会员做成全局,用member_store关联门店,卡上加scope字段,跨店消费走总部结算或门店间分账。

场景会员表会员卡查询条件
单店按门店唯一绑定门店store_id必带
连锁跨店全局唯一scope=all按会员查,不写死门店
连锁部分通用全局唯一scope=store_ids校验门店列表

改跨店之前先跑一遍数据:同一个手机号在member里出现几次,member_card.store_id和订单store_id是否一致。不要只在代码里加or条件,先把数据模型定清楚。

5.4 定时任务和消息推送漏配,到期提醒全没有

现象是会员卡到期没有提醒,积分该冻结没冻结,预约到点没有通知。原因多半是定时任务没配、队列没起、或者任务跑了但日志没地方看。这类源码通常把到期提醒写成命令行脚本,部署时只配了 Web 服务,忘了配crontab。解决方式是把任务命令、执行频率、日志路径写进部署清单,每次上线后手动跑一次,确认能生成提醒记录。

# 每天 09:10 跑会员到期检查,路径和命令按源码实际改 10 9 * * * cd /data/app && php think member:expire >> runtime/cron.log 2>&1 # 手动跑一次,先看日志再等定时 cd /data/app && php think member:expire tail -n 100 runtime/cron.log

crontab里的cd不能省,很多框架按项目根目录找配置。日志要落到固定文件,别只输出到/dev/null。手动跑一次看有没有报错,再看数据库里有没有生成提醒记录。消息推送如果依赖队列,先确认队列进程在跑,不然任务写进队列也没人消费。

6. 会员流失预警怎么落地:一条 SQL 加一个每日定时任务

6.1 用 SQL 找出 30 天未到店会员

会员系统跑通后,最先能加的价值不是花哨营销,而是流失预警。下面这条 SQL 按最近一次消费找 30 天未到店会员,store_id和天数都是参数,连锁场景把store_id换成门店列表或去掉。LEFT JOIN保证从未消费的新会员也能被捞出来,LIMIT防止一次跑太多。

SELECT m.id, m.mobile, MAX(c.created_at) AS last_visit FROM member m LEFT JOIN consume_order c ON c.member_id = m.id AND c.status = 'paid' WHERE m.store_id = 1 GROUP BY m.id, m.mobile HAVING last_visit IS NULL OR last_visit < DATE_SUB(NOW(), INTERVAL 30 DAY) ORDER BY last_visit ASC LIMIT 200;

INTERVAL 30 DAY改成 45 或 60 就是不同流失档位。last_visit IS NULL是新客未消费,运营动作和沉睡客不一样,后台最好分两个列表。跑出来的结果写进member_follow_log,记录跟进人、跟进时间、结果,别只查不记。

6.2 把预警接到后台并做对账验证

定时任务每天跑一次,把结果写跟进表,后台给店长一个“待跟进”列表。上线前跑一遍对账,确认充值、余额、消费、退款能对上。下面这条对账 SQL 我每次交付前都会跑,四个数不平就先别开营销功能。

SELECT (SELECT COALESCE(SUM(amount),0) FROM recharge_order WHERE store_id=1 AND status='paid') AS recharge_total, (SELECT COALESCE(SUM(balance + gift_balance),0) FROM member_card WHERE store_id=1) AS balance_total, (SELECT COALESCE(SUM(amount),0) FROM consume_order WHERE store_id=1 AND status='paid') AS consume_total, (SELECT COALESCE(SUM(amount),0) FROM refund_order WHERE store_id=1 AND status='paid') AS refund_total;

对账口径是:充值总额减退款总额,应该等于会员卡余额加已消费金额。差一分就查流水,别用“四舍五入”糊弄过去。从那以后我每次交付这类会员系统,都先跑一遍充值、消费、退款对账,再开微信端提醒;血泪经验是页面越花,账越要先平。希望帮到你。

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

返回列表