简介:这份源码面向美容院、美业门店的经营者与PHP开发者,提供一套可直接部署的会员管理解决方案,重点解决门店会员档案、消费记录与权限管控等日常运营问题。系统权限可细分到每个功能点,增删改查模块齐全,并整合了Excel导出能力,便于门店整理会员数据与经营报表;同时附带微信端,方便顾客在移动端查询与互动。压缩包共1601个文件,以662个php业务逻辑文件、272个html页面模板、148个js脚本与35个css样式为主,另含gif、png、jpg等界面素材及config、functions等配置与公共函数文件,整体约23.4MB,目录分层清晰,便于按模块定位二次开发。已有3259人学习下载,适合需要快速搭建美业会员系统或研究权限细分与数据导出实现的中级开发者参考借鉴。
1. 一套美容院会员系统源码,为什么值得拆开看
上周有个做美业 SaaS 的朋友甩给我一份源码包,标题写着「美容院会员管理系统源码(含微信端)」,让我帮忙看看能不能直接改改给客户用。我第一反应是:这类系统市面上模板一大把,能有什么好看的?结果解压之后翻了半小时,发现它把美业门店最核心的几个业务闭环——会员建档、储值卡、次卡核销、微信端预约——都串起来了,而且微信端不是那种贴个二维码糊弄人的假入口,是真的有独立的前端页面和接口层。这就有点意思了。
美容会员管理这个场景,看着简单,实际坑特别多。普通零售的会员系统只管积分和折扣,但美容院要管的是「谁办了卡、卡里还剩几次、这次做的项目扣哪张卡、技师提成怎么算」。店铺老板最怕的就是卡项对不上账,客户来了说「我明明还有三次补水」,系统一查显示零次,当场尴尬。所以这类源码的价值不在于界面多漂亮,而在于它的数据模型有没有把「卡项-项目-核销记录」这三层关系理清楚。这份源码我拆下来,适合两类人:一是想快速搭个门店管理原型的小团队,二是接私活需要一套能改的业务底座。下面我按「它是什么 → 怎么跑起来 → 哪里会翻车」的顺序,把拆解过程摊开讲。
2. 会员卡项的数据模型:储值卡和次卡怎么在表结构里共存
2.1 为什么不能只用一张会员表打天下
很多新手拿到这类源码,第一眼会去找「会员表」,然后发现里面只有姓名、手机号、余额几个字段,就觉得这系统太简陋。其实真正的业务重心不在会员表,而在卡项表。美容院的卡分两种:储值卡是充钱进去按金额扣,次卡是买次数按项目核销。这两种卡的扣减逻辑完全不同——储值卡要处理折扣率、赠送金额、退款折算;次卡要处理项目绑定、有效期、跨店通用。如果硬塞进一张表,字段会膨胀到没法维护。
这份源码的做法是拆成三张核心表:member(会员主档)、member_card(会员持有的卡实例)、card_type(卡类型模板)。card_type定义这张卡是储值还是次卡、面额多少、绑定哪些服务项目;member_card记录某个会员买了哪张卡、剩余余额或剩余次数、生效和过期时间。这样设计的好处是,同一个会员可以同时持有一张储值卡和两张次卡,核销时按项目去匹配对应的卡类型。
我一般会先看card_type里有没有card_category这个字段,它的取值决定了后续扣减走哪条分支。源码里用的是枚举值1表示储值、2表示次卡,简单粗暴但够用。如果你要二次开发,建议在这里加一个deduct_priority字段,控制同一项目有多张卡可用时先扣哪张,不然客户投诉「为什么先扣我贵的卡」的时候你没法解释。
2.2 核销记录表是整个系统的账本
真正让这套系统能对账的,是consume_record这张核销记录表。每次客户做项目,不管扣的是余额还是次数,都会往这张表插一条记录,字段包括会员 ID、卡 ID、项目 ID、扣减数量、操作技师、操作时间。这张表只增不改,是典型的流水账设计。店铺月底对账,就是拿这张表按卡 ID 分组求和,跟member_card里的剩余值做比对,对不上就说明有异常操作。
这里有个细节值得说:源码在核销时用了事务包裹,先查卡状态、再扣减、再写流水,三步在一个事务里完成。我见过太多小系统省掉事务,结果并发核销时同一张次卡被扣了两次,客户次数凭空少一次,这种血泪经验在美业门店里特别常见——前台两个人同时操作一台电脑,或者微信端和收银端同时提交,没有事务就是灾难。
-- 核销次卡的核心事务逻辑(简化自源码) START TRANSACTION; -- 1. 锁定该会员卡记录,防止并发扣减 SELECT remaining_times FROM member_card WHERE id = ? AND status = 1 FOR UPDATE; -- 2. 校验剩余次数是否足够 -- 若 remaining_times < deduct_count 则回滚 -- 3. 扣减次数 UPDATE member_card SET remaining_times = remaining_times - ? WHERE id = ? AND remaining_times >= ?; -- 4. 写入核销流水 INSERT INTO consume_record (member_id, card_id, project_id, deduct_count, operator_id, create_time) VALUES (?, ?, ?, ?, ?, NOW()); COMMIT;上面这段 SQL 的关键在FOR UPDATE那行行锁。没有它,两个请求同时读到剩余 3 次,各自扣 2 次,最后剩 -1 次,账就烂了。参数上要注意UPDATE语句里的remaining_times >= ?这个条件,它是第二道保险,即使锁没生效,扣减也不会把次数扣成负数。源码里这个写法是对的,但我在别的项目里见过漏掉这个条件的版本,结果就是负数次数在报表里飘着,查起来特别费劲。
2.3 微信端和后台共用一套接口层
这份源码的微信端不是独立后端,而是复用同一套 API,通过source参数区分请求来源(1是后台收银端,2是微信端)。这样做的好处是核销逻辑只有一份,不会出现「后台扣了微信端没同步」的问题。微信端主要做三件事:会员绑定手机号、查看自己的卡项余额、在线预约项目。预约这块源码做得比较轻,只是往appointment表插一条记录,没有做技师排班冲突检测,这是后面要说的坑之一。
接口层的鉴权用的是 token,微信端登录后拿 openid 换 token,后台用账号密码换 token,两套入口最后都落到同一个中间件校验。我拆的时候特意看了 token 的过期时间,源码写的是 7 天,对门店场景来说偏长,建议改成 2 小时加刷新机制,不然前台电脑忘了锁屏,别人拿手机就能操作。
3. 把源码跑起来:环境配置与微信端联调的实操步骤
3.1 后端环境搭建与数据库初始化
源码包里的后端是典型的 PHP 加 MySQL 组合,目录结构分application(业务逻辑)、public(入口)、config(配置)。我本地用的是 PHP 7.4 加 MySQL 5.7,跑起来没报兼容问题。如果你用 PHP 8,要注意源码里有些地方用了each()函数,这个在 8.0 已经移除,得手动改成foreach。
第一步先把数据库建好。源码的sql目录下有一个初始化脚本,直接导入就行:
# 创建数据库并导入初始化脚本 mysql -u root -p -e "CREATE DATABASE beauty_member DEFAULT CHARSET utf8mb4;" mysql -u root -p beauty_member < ./sql/init.sql # 检查核心表是否建成功 mysql -u root -p beauty_member -e "SHOW TABLES;"导入之后应该能看到member、member_card、card_type、consume_record、appointment这几张核心表,外加一些系统配置表。如果SHOW TABLES出来的数量明显偏少,多半是init.sql里有外键依赖顺序问题,把SET FOREIGN_KEY_CHECKS=0;加到脚本开头再导一次。
接着改config/database.php里的连接信息,把 host、用户名、密码、库名换成你本地的。这里有个小坑:源码默认用的是localhost,如果你用 Docker 跑 MySQL,得改成容器 IP 或者host.docker.internal,不然会报连接拒绝,我第一次跑就卡在这。
// config/database.php 关键配置项 return [ 'hostname' => '127.0.0.1', // Docker 环境改成 host.docker.internal 'database' => 'beauty_member', 'username' => 'root', 'password' => 'your_password', 'hostport' => '3306', 'charset' => 'utf8mb4', ];配置改完,用 PHP 内置服务器起一下:php -S 0.0.0.0:8080 -t public,浏览器访问http://localhost:8080能看到登录页就说明后端通了。默认管理员账号一般在init.sql的注释里写着,通常是admin加一个初始密码,登录后第一件事就是改掉。
3.2 微信端配置与本地调试
微信端在wechat目录下,是一个独立的前端工程,用的是原生 HTML 加 JS,没有上框架,好处是改起来直观,坏处是没有构建流程,改完直接刷新。它需要配置appid和接口域名,源码里在wechat/config.js里留了两个占位符:
// wechat/config.js const config = { appId: 'YOUR_APPID', // 替换为公众号或小程序的 appid apiBase: 'http://localhost:8080/api', // 本地调试指向后端地址 debug: true // 开启后会在控制台打印请求日志 };本地调试微信端有个绕不开的问题:微信的网页授权必须在真实域名下走,localhost不行。常见做法是用内网穿透工具把本地 8080 端口映射出去,拿到一个临时域名填到公众号后台的「网页授权域名」里。这一步我没法在源码里替你完成,但流程是固定的:映射 → 填域名 → 微信端访问映射地址 → 授权回调。如果你只是想看页面长什么样,把debug打开,源码里有个 mock 模式,会在没有真实 openid 时用测试账号兜底,能跳过授权直接看界面。
微信端联调时重点看三个接口:/api/member/info(会员信息)、/api/card/list(卡项列表)、/api/appointment/create(预约提交)。前两个是读,第三个是写。预约接口的参数里project_id和technician_id都是必填,源码没做非空校验,传空会插一条脏数据进去,这个后面避坑章节会细说。
3.3 后台收银端的核销流程走查
后台登录后,主界面是会员搜索加卡项展示。我拿测试会员走了一遍完整流程:建档 → 办卡 → 核销 → 查流水。办卡时选卡类型,储值卡填充值金额,次卡填购买次数,提交后member_card里会多一条记录。核销时选项目,系统会自动匹配可用卡,这里源码的匹配逻辑是按card_type绑定的项目 ID 来的,如果会员有多张卡都绑了同一个项目,默认取最早办的那张。
走查时我特意试了一个边界:给会员办一张次卡,然后手动把member_card的status改成过期,再去核销,系统正确拦截了并提示「卡已过期」。说明状态校验是生效的。但反过来,如果卡没过期但项目不匹配,源码的提示语是「无可用卡项」,没有告诉用户具体哪张卡不匹配,体验上差一点,二次开发时可以把这个提示细化。
4. 避坑排查:这套源码落地时最容易翻车的五个地方
4.1 并发核销导致次数扣成负数
现象:两个前台同时给同一个会员核销次卡,事后查流水发现扣了两次但剩余次数只减了一次,或者直接变成负数。原因:核销接口虽然用了事务,但查询剩余次数那步没加行锁,两个请求读到的都是扣减前的值。解决:在SELECT语句后加FOR UPDATE,并且UPDATE的WHERE条件里带上remaining_times >= deduct_count,双保险。改完之后我用两个并发请求压了一下,次数扣减正确,第二个请求会等第一个事务提交后才执行。
4.2 微信端预约不校验技师时间冲突
现象:同一个技师同一时间段被约了两次,到店后客户和技师面面相觑。原因:appointment表插入前只校验了会员是否存在,没查该技师在目标时段有没有已有预约。解决:在插入前加一条查询,按technician_id和预约时间段做重叠判断,有冲突就返回提示。源码里没有现成的冲突检测函数,得自己补,逻辑不复杂,就是时间区间求交集。
4.3 储值卡退款时赠送金额没折算
现象:客户充 1000 送 200,消费了 500 后要退款,系统按剩余 700 全退,店铺亏了赠送部分。原因:源码的退款逻辑只读了balance字段,没区分本金和赠送金。解决:在member_card里加principal_balance和gift_balance两个字段,退款时按比例折算赠送部分。这个改动涉及表结构,建议在初始化阶段就加上,后期补数据很麻烦。
4.4 微信端 token 过期后页面白屏
现象:会员打开微信端,页面一直转圈不加载。原因:token 过期后接口返回 401,但前端没有统一拦截,请求失败后页面停在 loading 状态。解决:在wechat的请求封装里加一个响应拦截,遇到 401 就跳转重新授权,同时把本地缓存的 token 清掉。源码的请求封装在wechat/utils/request.js,改一个地方就能全局生效。
4.5 数据库字符集不一致导致中文乱码
现象:会员姓名在后台显示正常,微信端显示成问号。原因:init.sql建库时用了utf8,但微信端接口返回的 header 里写的是charset=gbk,两边对不上。解决:统一改成utf8mb4,建库、建表、连接配置、接口 header 四处都要改。这个坑我在别的项目里也踩过,美业系统里会员姓名带生僻字的不少,utf8存不下,必须utf8mb4。
提示:改完字符集后,已有的乱码数据不会自动恢复,需要重新录入或者从备份里按正确编码导一次。
5. 二次开发进阶:把卡项核销做成可配置的规则引擎
源码跑通之后,真正决定这套东西能不能商用的,是核销规则够不够灵活。我拆的时候注意到,扣减逻辑是硬编码在ConsumeService里的,储值走一个分支、次卡走另一个分支,加一种新卡类型就得改代码。对单店够用,但你要是想给多个门店用,每家店的扣减规则不一样,硬编码就顶不住了。
我的做法是抽一层规则配置出来。在card_type表里加一个deduct_rule字段,存 JSON,描述这张卡在什么条件下扣多少。比如「工作日扣 1 次、周末扣 1.5 次」或者「指定项目打八折」。核销时先读规则 JSON,再走一个简单的解析器算出实际扣减量。这样加新规则不用动代码,改配置就行。
# 核销规则解析的伪代码(Python 示意,实际项目用 PHP 实现) import json from datetime import datetime def calc_deduct(card_type_rule, project_id, consume_time): rule = json.loads(card_type_rule) base = rule.get("base_deduct", 1) # 周末系数 if consume_time.weekday() >= 5: base *= rule.get("weekend_factor", 1.0) # 指定项目折扣 project_rules = rule.get("project_rules", {}) if str(project_id) in project_rules: base *= project_rules[str(project_id)] return round(base, 2)这段逻辑的关键是把「扣多少」从代码里挪到数据里。参数上base_deduct是基础扣减量,weekend_factor是周末系数,project_rules是按项目覆盖的系数。实际落地时要注意浮点精度,次卡扣 1.5 次这种场景,剩余次数得用decimal存,用float会出现 0.30000000000000004 这种鬼东西,对账时能把人逼疯。
验证这套规则是否生效,我一般会建一张测试卡,配三条规则,然后分别在工作日、周末、指定项目三个场景下各核销一次,比对consume_record里的deduct_count和预期值。三个都对上,规则引擎就算通了。从那以后我每次改核销逻辑,都强制先跑一遍这个三场景验证,不然改一处崩一处,后悔药都没地方买。希望这套拆解能帮到你,少走几个我踩过的坑。
本文还有配套的精品资源,点击获取