简介:面向美容院及美业门店的会员管理系统完整源码,包含微信端文件,适合PHP入门开发者、中小型商户和需要快速落地会员体系的团队。系统权限可细分到每个功能模块,增删改查操作一应俱全,并整合Excel导出功能,便于会员数据沉淀、对账和报表处理。压缩包约23.4MB,共1601个文件,其中662个PHP程序文件构成核心业务逻辑,272个HTML页面与148个JS脚本支撑前端交互,35个CSS样式负责界面布局,另有SQL数据库文件、配置文件和图片资源,部署后即可运行、体验并做二次开发。资源中还包含xxtea加解密相关工具文件,可用于研究微信端数据安全或接口加密,二次开发时可参考其权限设计、数据导出思路以及整体目录结构。目前已有3258人学习下载,整套源码结构清晰、文件分类明确,既可作为实际门店运营工具,也能作为管理类项目开发的教学范例。
1. 美容院会员系统源码带微信端:先用三句话讲清你在买什么
打开一套美容院会员管理系统源码,你会发现它和普通进销存系统完全不是一回事:行业里几乎没有标准化的商品,顾客买的是「次卡」「储值卡」「疗程套餐」,消费方式是「到店扣次」「充值送余额」「指定美容师服务」,再加上业绩提成、预约排班、微信端自助查卡和续费。所以这套源码的价值不在用户登录、增删改查,而是在「卡项模型」和「微信端闭环」这两块骨头能不能啃动。
我接触过不少开店老板和接单开发者,最常见的诉求是:店里还在用纸质卡片和 Excel 记账,想上一套带微信端的会员系统,让顾客能在微信里查余额、约时间、看消耗记录,店员在前台只管开单消次。如果你也是这个状态,或者你是接私单的开发者想找一套能快速改造成交付项目的底座,这篇文章会按「源码结构 → 本地跑通 → 微信端打通 → 业务适配」的顺序讲透,连参数设计一起给。
2. 先看懂源码里的骨头:美容院会员的核心业务模型与系统结构
2.1 会员、卡项、流水:这套系统的三张核心表会怎么设计
美容院会员系统和普通零售系统的最大区别,体现在数据表设计上。普通零售只要有 order(订单)、sku(商品)两张表就能转,但美容院必须单独设计会员卡体系,因为「顾客」和「卡」是分离的:一个人可以同时持有次卡、储值卡、疗程卡多张卡,而且每张卡还绑定指定美容师,这就导致消费时要从「会员」找到「卡」,再从「卡」去扣次数或余额。
我在本地把常见的源码建表脚本翻过一遍,绝大多数项目会围绕这三张主表展开:
-- 会员基础信息表:不直接存余额和总次数,余额从流水表汇总 CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, mobile VARCHAR(20) NOT NULL COMMENT '手机号,登录凭证', name VARCHAR(50), openid VARCHAR(64) COMMENT '微信openid,一个会员可绑多个微信号', level TINYINT DEFAULT 1 COMMENT '会员等级,对应折扣率', source_channel VARCHAR(32) COMMENT '来源渠道:前台/微信', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_mobile (mobile) ); -- 会员卡实例表:一张卡一个记录,余额/剩余次数字段是冗余加速字段 CREATE TABLE member_card ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, card_type_id BIGINT NOT NULL COMMENT '卡项模板ID', balance_amount DECIMAL(10,2) DEFAULT 0 COMMENT '储值余额', total_count INT DEFAULT 0 COMMENT '总次数', used_count INT DEFAULT 0 COMMENT '已用次数', expire_at DATETIME COMMENT '到期时间,null表示长期有效', status TINYINT DEFAULT 1 COMMENT '1正常 2冻结 3过期', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 交易流水表:所有加钱、扣次、退卡都在这里留痕 CREATE TABLE member_card_transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_id BIGINT NOT NULL, member_id BIGINT NOT NULL, type TINYINT COMMENT '1充值 2消费 3退款 4赠送 5到期作废', amount DECIMAL(10,2) DEFAULT 0 COMMENT '金额变动,正负表示增减', count_delta INT DEFAULT 0 COMMENT '次数变动', order_no VARCHAR(32) NOT NULL COMMENT '业务单号,用于对账', remark VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这三张表的关键逻辑在两张卡片上。第一,member_card 里冗余存了余额和次数,查询列表时不用聚合,但每次消费必须事务性地同时更新 member_card 和 transaction,否则账会越跑越偏。第二,流水表里的 order_no 承担对账核心职责,充值订单、微信支付回调、消费小票、退款单全用同一个单号,这个设计在微信端接入支付时能省掉大量重复回调的麻烦。
参数上的一个隐形分水岭是 expire_at 的处理方式。有的源码把它设计成「卡项模板自带有效期」,到期统一作废;有的设计成「每张卡单独计算有效期」。实际美容院里「次卡 12 个月用完」是常态,最好把有效期放在卡项模板上,由开卡时写入具体日期,这样后续做提醒脚本时可以直接扫 member_card 表。
2.2 微信端在源码里扮演的角色:不是 H5 壳子,是会员自助服务的第二收银台
很多初学者拿到含微信端的源码,会误以为微信端就是个「会员信息展示页」。实际成熟项目的微信端承担的是真实消费动作:充值、续卡、购买团购券到店核销、预约美容师、查看消耗。也就是说,微信端本质上是一个「由会员自己操作的收银台」,它和前台收银共用同一套卡项和流水逻辑,只是入口不同。
从源码结构上看,微信端通常分两条技术路线:一条是微信网页授权(微信公众号 H5),一条是小程序。绝大多数源码会优先做公众号 H5,因为开发成本低、不需要额外审核类目,打开链接就能用。H5 端的核心配置是三样东西:
- 公众号的 AppID 和 AppSecret,用于网页授权换 openid;
- 微信支付商户号,用于 JSAPI 下单;
- 域名白名单,包括 JS 安全域名和支付授权目录。
这里要提一个决定你要不要改造源码的判断标准:这套源码的「卡项模板」是写进数据库的表驱动,还是写死在代码里的常量?我见过某套老的 PHP 系统,卡项直接在代码里用 switch 分支判断,每次加一个「肩颈调理 10 次卡」都要改代码重新部署,这种源码无论界面多好看都建议放弃。表驱动的项目,卡项、赠送规则、折扣规则都在 admin 后台配置,微信端只是通用渲染,这样的源码才有改造空间。
2.3 一个值得记住的设计经验:扣次数和余额必须走同一套事务
在翻源码的时候,重点看「消费下单」这一个接口的实现,就能判断出代码水平。新手写的系统往往把「扣次」和「记流水」分开两个函数调用,中间没有事务保护,一旦扣次成功但写流水失败,顾客卡上的次数就凭空消失,前台和顾客永远对不上账。
我改造过一次某跨平台系统,原来代码是这样的坏味道:
// 问题版本:两个操作没有事务,中途掉线就数据不一致 public function consume($cardId, $item) { // 1. 扣次数 $card = Card::find($cardId); $card->used_count += $item->count; $card->save(); // 2. 记流水 $transaction = new Transaction(); $transaction->card_id = $cardId; $transaction->count_delta = -$item->count; $transaction->save(); }如果第 2 步失败,顾客被扣了次数却查不到记录,后台统计也会对不上,最后只能靠手工补流水。正确的做法是把两步包进同一个数据库事务,并在更新卡时带上版本号做乐观锁:
public function consume($cardId, $item) { DB::transaction(function () use ($cardId, $item) { // 乐观锁更新:加 where 条件,affected rows 为 0 说明卡已被并发修改 $affected = Card::where('id', $cardId) ->where('used_count', $item->base_used_count) ->update([ 'used_count' => DB::raw('used_count + ' . $item->count), 'updated_at' => now(), ]); if ($affected === 0) { throw new \Exception('操作太频繁,请重试'); } // 同一事务里写流水 Transaction::create([ 'card_id' => $cardId, 'count_delta' => -$item->count, 'order_no' => $item->orderNo, ]); }); }这里有个容易被忽略的参数 is_dimension:乐观锁 where 条件里的 base_used_count,是查询卡信息时带出来的旧值。如果并发下 affected 为 0,直接提示客户重试,比用「加减乘除乱算」靠谱得多。判断一套源码值不值得二次开发,我一般先看它有没有这样的消费接口事务,没有的坚决不碰。
3. 本地把源码跑起来:从环境准备到数据库初始化的最小流程
3.1 部署一个典型 PHP + MySQL 架构的最小步骤
拿到源码第一步是「让它先跑起来」,不要急着改业务。美容院会员系统源码最主流的形态是 PHP + MySQL 的传统架构,部分新项目用 Java Spring 或 Node.js,但入门门槛和后续改造成本都在数据库这张表上,语言反而不重要。下面给一个通用部署流程,你可以对照自己的源码调整:
# 1. 上传源码到站点目录,以 nginx 为例 sudo mkdir -p /var/www/beauty-member cd /var/www/beauty-member # 把源码包解压到当前目录 # 2. 创建数据库和专用账号(不要用 root 跑业务) mysql -u root -p CREATE DATABASE beauty_member DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'beauty_app'@'localhost' IDENTIFIED BY 'ChangeMe_2024'; GRANT ALL PRIVILEGES ON beauty_member.* TO 'beauty_app'@'localhost'; FLUSH PRIVILEGES; # 3. 导入项目自带的初始化 SQL mysql -u beauty_app -p beauty_member < database/init.sql # 4. 配置项目连接参数,PHP 项目一般在 application/config/database.php php artisan 或直接编辑配置文件,填入库名、账号、密码这里给出两个关键参数的选择理由。第一,数据库字符集必须用 utf8mb4,因为微信用户的昵称里可能有 emoji 字符,utf8 存不下会直接报错或者变成问号。第二,数据库账号单独建,不要用 root,源码里的管理后台如果存在 SQL 注入漏洞,独立账号能把损失限制在单个库里——这个观念最好现在就养成,因为后期复现时往往会涉及到线上部署。
3.2 初始数据到底包含什么:为什么你的源码里会有「演示会员」
初始化 SQL 导入后,登录管理后台通常能看到一批「演示数据」:某美容师、某会员「A同学」、几张测试卡项、几条消费记录。这些数据不是垃圾,它们是系统设计的活文档。建议在动手改代码前,先用演示会员走一遍「开卡 → 充值 → 消费 → 查看流水」的完整链路,确认这套源码的流程是否顺畅。
我一般建议在这个阶段做一个「关灯测试」:把管理后台所有菜单点一遍,记录哪些菜单报错、哪些数据是写死的。很多源码会在列表页写死「仅限演示数据」的过滤条件,或者把「本店名称」硬编码在配置文件里,这些坑都要在正式上线前处理掉。具体操作时可以用最小命令来验证核心链路:
# 检查 HTTP 服务是否正常响应 curl -I http://127.0.0.1:8080/admin # 预期返回 302 或 200,如果 500 则查看日志 tail -f /var/www/beauty-member/storage/logs/error.log # 快速确认数据库里有没有灌入演示数据 mysql -u beauty_app -p -e "SELECT COUNT(*) FROM member; USE beauty_member;"这里解释一下为什么要用「关灯测试」而不是直接开改。因为这套源码最终要交到店里用,而美容院的员工和顾客都经不起数据反复修。你提前把异常链路暴露出来,比上线后半夜被电话叫醒要舒服得多。
3.3 本地跑通后的第一件事:先把「卡项模板」按真实店里的规则配好
确认系统能跑之后,立刻进入第一次业务适配。美容院的卡项基本逃不开这几种:次卡(如海藻面膜 10 次)、储值卡(充值 3000 送 500)、疗程卡(分阶段扣次,如背部舒缓 12 次含 2 次精油加项)、限时体验卡(7 天内有效)。配卡项时注意两个参数影响后续所有报表:
- 赠送规则:充值送的部分是进「余额」还是进「赠送金」,两种字段在退卡时的计算逻辑完全不同。进余额,退款按比例退;进赠送金,退款时直接作废。选错了后期极其被动。
- 是否允许过期续费:很多源码卡过期就死了,但真实店面会做「花 300 元激活过期卡」的业务,如果你确认开店需要这个功能,必须在选型阶段就确认源码是否支持,不要等到上线前才发现要改表结构。
建议用表格把你要配的卡项列出来,至少覆盖:卡项名称、售卖价格、是否限时、有效期、允许多次购买还是限购一张、是否绑定指定美容师。这套配置会直接决定微信端展示效果,配错了微信端会显示一堆不可购买的卡,体验非常差。
4. 微信端接入:从网页授权到 JSAPI 支付的关键链路
4.1 公众号 H5 的授权登录:把系统账号和 openid 绑定起来
微信端的起点不是支付,是「让系统认识这个顾客是谁」。微信网页授权换取用户身份的流程是绝大多数源码已经写好的部分,你需要做的是正确配置参数。以 PHP 实现为例,核心代码逻辑如下:
public function wxLogin() { // 第一步:拼接授权链接,引导用户跳转到微信授权页 $redirect = urlencode('https://api.example.com/wx/callback'); $url = "https://open.weixin.qq.com/connect/oauth2/authorize?" . "appid={$this->appId}&redirect_uri={$redirect}" . "&response_type=code&scope=snsapi_base&state=member#wechat_redirect"; // snsapi_base 静默授权,拿 openid,不弹确认框 // 第二步:用户回调回来时带着 code,换 access_token 和 openid $code = $_GET['code']; $tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" . "appid={$this->appId}&secret={$this->appSecret}&code={$code}" . "&grant_type=authorization_code"; $resp = $this->httpGet($tokenUrl); // 响应里会带 openid, access_token, refresh_token // 用 openid 去 member 表查会员,查不到就提示先到前台开卡绑定 }授权参数里有两个容易踩的位置:第一个是 redirect_uri 必须提前在公众号后台配置为「网页授权域名」,而且这个域名必须和实际访问域名完全一致,包括端口。本地调试时微信根本没法回调到 localhost,所以微信端开发首先要有一个可供微信公网访问的测试域名。第二个是 scope=snsapi_base 只能拿到 openid 和头像,拿不到手机号和昵称,美容院系统里会员手机号必须由前台开卡时录入,微信端授权只是完成绑定。
4.2 JSAPI 支付:下单、收银台、异步回调三段的代码边界
微信端充值必然要接微信支付,源码里通常会用 JSAPI 方式。JSAPI 支付的代码难点不在签名算法,而在三段代码的边界划分:统一下单、前端调起收银台、异步回调通知。很多改造翻车的项目,都是把回调逻辑写进了下单接口里,导致支付成功后页面等不到回调结果。
我按常见落地姿势给一套支付流程的关键骨架:
// 1. 后台统一下单 $orderNo = 'C' . date('YmdHis') . rand(1000, 9999); $result = $this->wxPay->unifiedOrder([ 'out_trade_no' => $orderNo, 'body' => '会员充值-' . $rechargePackage->name, 'total_fee' => $rechargePackage->amount * 100, // 金额单位是分 'spbill_create_ip' => $_SERVER['REMOTE_ADDR'], 'notify_url' => 'https://api.example.com/wx/pay/notify', 'openid' => $member->openid, 'trade_type' => 'JSAPI', ]); // 返回 prepay_id,前端用它调起微信收银台 // 2. 前端用 wx.chooseWXPay 调起收银台 // 需要后端用 prepay_id 二次签名生成 paySign $signParams = [ 'appId' => $this->appId, 'timeStamp' => time(), 'nonceStr' => uniqid(), 'package' => 'prepay_id=' . $result['prepay_id'], 'signType' => 'MD5', ]; $paySign = $this->wxPay->makeSign($signParams); // 3. 异步回调:必须校验签名 + 校验金额,然后更新订单 $notifyData = $this->wxPay->verifyAndParseNotify($_POST); // 内部校验签名 if ($notifyData['result_code'] === 'SUCCESS' && $notifyData['total_fee'] == $order->amount * 100) { // 开启事务:把订单置为已支付,给 member_card 加余额 }这段代码里有三个参数请重点核对:total_fee 以「分」为单位,如果传入元,金额会差一百倍,这种错账很难查;notify_url 必须公网可访问,且不能带任何自定义参数,微信回调时会忽略你额外拼接的字符串;回调接口要做好幂等处理,微信网络抖动会重复推送多次回调,代码里必须在订单状态上加判断,已支付订单直接返回成功,不能重复加余额。
4.3 微信端查卡和预约:要留给店员的其实是「解释成本」
微信端的会员查卡、查消耗、预约功能,源码实现难度不高,标题说「含微信端」的源码一般都有,但你上线前一定要想清楚一个运营层面的问题:顾客在微信端看到的数据,店员能不能解释清楚?打个比方,顾客充 3000 送 500,微信端余额显示 3500,但店里规则是「送的 500 不能退」,这个字段在微信端要不要标注?如果界面设计里没有「赠送金」和「本金」的区分展示,退款纠纷一定会在柜台爆发。
我习惯的做法是在微信端的「余额明细」里把每笔流水的来源标注清楚,充值、消费、赠送、退还,一条条列出来,让顾客截个图就知道来龙去脉。这个功能不需要改业务逻辑,只需在交易流水查询接口里把 type 字段翻译成中文名称,但它的实际价值比多做十张营销海报都大——顾客相信系统,就不会来问店员。
5. 部署上线前后最容易翻车的五个细节:现象、原因、解决方案
5.1 本地一切正常,微信端就是打不开:域名白名单和 IP 的玄学
现象:本地后台管理一切正常,用手机访问微信端链接,页面空白或提示「当前网页无法打开」。原因是微信公众平台的 JS 接口安全域名和网页授权域名配置不对,或者绑定的是带端口的内网地址。
我当时遇到的情况是开发者把测试 IP 直接填进白名单,导致每次换网络 IP 都要去后台改。解决方法是准备一个固定的测试域名,本地通过 hosts 解析到开发机指定,把该域名配置到公众号白名单,这样一来无论是内网调试还是上生产,域名路径都不用反复改。注意微信网页授权回调域名不支持带端口,这是硬限制。
5.2 用户充值成功但余额没到账:回调地址被本地代码拦截
现象:微信支付弹窗显示已支付,但会员查不到余额,后台订单状态还是「待支付」。原因多半是测试环境的回调地址写成了 127.0.0.1 或者带端口的局域网地址,微信服务器无法访问。
排查顺序:先看 Web 服务器的 access log,确认微信回调请求打没打到服务器;再看回调接口有没有正确的签名校验——有些源码在回调里先查订单再校验金额,如果金额校验判断出错,即便收到回调也会返回失败。解决思路是把回调地址配置为线上正式环境地址,然后在线上环境调试支付,不要指望本地环境完整模拟。这时候最稳妥的做法是接一个「支付成功后人工确认赠送」的备用按钮,但只给管理员可见,防止线上问题卡住顾客。
5.3 卡项到期时间总差一天:日期边界与时区问题
现象:顾客的次卡明明今天到期,系统却显示昨天已过期,或者反过来,第二天才过期。原因是源码里存到期时间用的是 date 类型,而微信端展示和判断用 time() 函数生成的时间戳,两个时区(数据库时区与 PHP 默认时区)不一致,导致边界日期偏移。
解决方式:统一时间来源。在项目初始化时把数据库连接时区设置为 Asia/Shanghai,PHP 侧设置 date_default_timezone_set('Asia/Shanghai'),同时数据库字段统一用 datetime 类型。如果你拿到的源码用的是 timestamp,迁移数据时要特别小心,timestamp 范围到 2038 年,线上数据量大之后存在溢出风险。
5.4 扣次并发导致卡上次数算错:两个店员同时操作一台电脑的极端场景
现象:一个顾客用次卡做项目,前台甲在收银台刷卡,店长同时在后台修改卡资料,结果卡上次数被扣两次或者显示负数。原因是消费接口没有加锁,两个请求同时读到 used_count=5,各自加 1 后写回,最终变成 6 而不是 7。
这类问题的解法就是前面第 2.3 所述的乐观锁思路,在 update 语句里带上旧值做条件。这里要补充一条血泪经验:不要依赖前端按钮置灰防重复点击,因为小程序和 H5 的请求有重试机制,前端拦住再多,后端不防重也是白搭。更稳的做法是在 member_card 表加一个 version 字段,每次更新版本号加一。
5.5 美容师业绩报表对不上:业绩归属的设计缺陷
现象:钱收进来了,但月底给美容师算提成时,发现系统里的业绩和顾客消费记录对不上。原因是源码的业绩统计是按「开单收银员」归属的,而美容院的实际业务是「顾客指定美容师服务」,服务过程中还可能换人手。
这种问题的根源在数据模型:消费流水表里如果没有「服务美容师」字段,就永远算不对。解决方法是看源码的交易表里有没有绑定员工 ID 的字段,如果没有,就要在改造成本里明确加上;如果有,要重点检查开单页面是否允许店员修改美容师,很多前台开单时默认取当前登录账号,完全忽略了顾客可能指定了其他美容师。
提示:上面五条是线上线下交付时最容易出问题的点。凡是涉及金额、次数、日期的改动,建议每一条都按「现象 → 原因 → 解决」的形式记录到你自己的交付文档里,后面做第二家店时能省很多事。
6. 把源码改造成能交付的「店面版」:三个高性价比的进阶动作
你如果已经跑通并上线了一家店,接下来想让这套源码复制到更多门店,值得优先做三件事:把「本店信息」从配置项提升为数据库字段、把会员导入做成可重复执行的脚本、以及给前台收银加上「快速消次」的快捷键位。
第一件是让系统支持多门店。源码里门店信息常散落在配置文件或单张表里,复制多店时最难匹配的是「会员归属门店」。我的做法是在 member 表加 store_id 字段,所有查询都带上门店条件,微信端则根据 openid 判断顾客归属门店,自动跳转到对应的公众号或小程序。这个改造大约要动十几处查询代码,但能决定你这套系统是「单店工具」还是「连锁平台」。
第二件是数据迁移脚本。老店从 Excel 导入会员,最怕的是手机号格式不统一和重复会员。写脚本时参考下面这个格式:
# 导入脚本示意:只导入会员档案和剩余次数,余额走首充流水 import csv, mysql.connector conn = mysql.connector.connect(user="beauty_app", password="ChangeMe", database="beauty_member") cur = conn.cursor() with open("old_members.csv", encoding="utf-8-sig") as f: for row in csv.DictReader(f): mobile = row["手机号"].strip() if not mobile.isdigit() or len(mobile) != 11: print(f"跳过非法手机号: {row['手机号']}") continue cur.execute( "INSERT INTO member (mobile, name) VALUES (%s, %s) " "ON DUPLICATE KEY UPDATE name=VALUES(name)", (mobile, row["姓名"]), ) # 剩余次数写入卡实例,需先查卡项类型 conn.commit() cur.close() conn.close()导入时注意:编码要读 utf-8-sig,否则 Windows 导出的 CSV 会带 BOM 导致第一列多字符;手机号非 11 位的记录要单独导出标记,不要让脚本静默丢弃;导入动作要支持重复执行,重复执行的幂等效果依靠该数据库表的唯一键实现,不要凭感觉判断。
第三件是前台的「快速消次」。美容院前台忙起来没有时间在几个下拉框里慢慢选卡项,建议把「按手机号快速查询 → 默认展示最近使用的卡 → 一键扣次」做成一个收银页面的固定模块。这也是熟手区分的点:新手觉得功能全就厉害,但店里真正高频的功能只有三五个,做顺了顾客体验和员工效率才同时达标。
最后说一句个人习惯:我是被「上线之后补数据」坑过的人,所以现在每次交付这套系统,都会在开业的第一个星期要求店员把每天的对账截图发到项目群里,连续盯七天的流水和库存。七天不出奇,这系统基本就能稳定跑下去。希望这篇笔记帮你在选型和改造路上少踩几个坑。
本文还有配套的精品资源,点击获取