简介:这份源码面向美容院、美业门店的经营者与PHP开发者,提供一套可直接部署的会员管理解决方案,覆盖微信端与后台管理两端,适合需要搭建门店会员体系或进行二次开发的中小团队。系统权限可细分到每个功能点,增删改查模块齐全,并整合了Excel导出能力,便于门店做会员数据统计与报表整理。压缩包共1601个文件,以662个php业务逻辑文件、272个html模板页面为主,辅以148个js脚本、35个css样式及大量gif、png、jpg图片素材,另有config配置、functions函数库、sql建表脚本与install安装文件,整体约23.4MB,目录分层清晰,便于按模块定位代码。目前已有3259人学习下载,说明其在美业会员管理场景中具备一定参考价值。读者可从中获取完整的会员档案、权限控制、数据导出等实现思路,也可作为PHP项目练手或门店系统改造的代码基础。
1. 从一张手写会员卡说起:这套美容院会员管理系统到底解决什么问题
我见过一家开了七八年的美容院,前台抽屉里塞着三本会员登记册,客户做一次护理,店员翻半天找卡、手写划次数、月底再拿计算器对账。老板想搞个“充五千送八百”的活动,结果因为算不清赠送金额怎么摊到每次消费里,活动做了三天就停了。这套美容院会员管理系统源码,本质上就是把这类店铺最头疼的三件事——会员档案、储值卡余额与赠送金、微信端预约和查余额——从纸笔搬到一套能跑起来的系统里。它适合谁?适合想给中小美容、美甲、皮肤管理门店做一套轻量会员管理工具的开发者,也适合店主自己找人二次开发。源码带微信端,意味着客户不用装 App,扫码进公众号或小程序页面就能看卡、约项目,这是这类店铺真正愿意掏钱的地方。
2. 技术栈与目录结构:先看清这套源码的骨架再动手
拿到一份“含微信端”的会员系统源码,最怕的是上来就npm install然后一堆报错。我一般先花二十分钟把目录和依赖读一遍,搞清楚哪部分是后台管理、哪部分是微信端接口、数据表怎么设计的,再决定从哪跑。
2.1 典型技术选型与分层
这类门店管理系统,常见做法是前后端分离:后台管理用 Vue 或 React 搭页面,服务端用 Spring Boot 或 Node.js(Express/Koa)提供 REST 接口,数据库 MySQL,微信端则是一个独立的 H5 页面或小程序,通过微信授权拿到 openid 后调同一套后端接口。源码里如果看到admin、server、wx(或miniprogram)三个并列目录,基本就是这个结构。
| 目录/文件 | 作用 | 常见技术 |
|---|---|---|
admin/ | 后台管理前端,店长/店员用 | Vue2/Vue3 + Element UI |
server/ | 后端服务,统一提供接口 | Spring Boot 或 Node.js |
wx/或miniprogram/ | 微信端页面 | 微信小程序原生或 H5 |
server/src/main/resources/ | 配置与 SQL 脚本 | application.yml、init.sql |
sql/ | 建表与初始数据 | MySQL 8 |
需要说明的是,具体目录名各版本可能不同,但分层逻辑一致:管理端和微信端共用一套后端接口,靠角色和 token 区分权限。这是理解整套源码的第一把钥匙,后面改任何功能都要回到这个分层上想。
2.2 数据库核心表设计
会员系统的命脉在表结构。我拆这类项目时,第一件事是找会员表、储值流水表、预约表这三张。下面是根据这类系统通用做法还原的核心建表片段,字段名以实际源码为准,但结构逻辑基本一致:
-- 会员主表:一个会员一条记录,openid 关联微信端 CREATE TABLE `member` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(32) NOT NULL COMMENT '会员姓名', `phone` VARCHAR(20) NOT NULL COMMENT '手机号,登录用', `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信端唯一标识', `balance` DECIMAL(10,2) DEFAULT 0 COMMENT '本金余额', `gift_balance` DECIMAL(10,2) DEFAULT 0 COMMENT '赠送金余额', `level_id` INT DEFAULT 1 COMMENT '会员等级', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 储值流水表:每一次充值/消费/赠送都记一条,余额靠它算 CREATE TABLE `balance_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `member_id` BIGINT NOT NULL, `type` TINYINT NOT NULL COMMENT '1充值 2消费 3赠送 4退款', `amount` DECIMAL(10,2) NOT NULL COMMENT '变动金额,消费为负', `gift_amount` DECIMAL(10,2) DEFAULT 0 COMMENT '本次涉及赠送金', `remark` VARCHAR(128) DEFAULT NULL, `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 预约表:微信端提交,后台确认 CREATE TABLE `appointment` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `member_id` BIGINT NOT NULL, `service_id` INT NOT NULL COMMENT '项目ID', `appoint_time` DATETIME NOT NULL, `status` TINYINT DEFAULT 0 COMMENT '0待确认 1已确认 2已完成 3已取消' );这里有个关键设计:本金和赠送金分两个字段存。很多新手图省事只存一个balance,结果“充五千送八百”时赠送金和本金混在一起,退款、算提成、算过期全乱套。源码里如果分了balance和gift_balance,说明作者踩过这个坑,二次开发时务必沿用这个拆分,别合并。
2.3 本地跑起来的最小步骤
按常见 Spring Boot + Vue 的组合,跑通顺序是:先建库导 SQL,再起后端,最后起前端。命令大致如下,具体端口和脚本名以源码README为准:
# 1. 建库并导入初始数据(假设源码 sql 目录下有 init.sql) mysql -uroot -p -e "CREATE DATABASE beauty_member DEFAULT CHARSET utf8mb4;" mysql -uroot -p beauty_member < sql/init.sql # 2. 改后端数据库配置后启动(Spring Boot 示例) cd server # 编辑 src/main/resources/application.yml 里的 datasource 账号密码 mvn spring-boot:run # 3. 起后台管理前端 cd ../admin npm install npm run dev启动后先访问后台默认账号(常见是admin/123456,在init.sql里能查到),确认能登录、能看到会员列表,再去看微信端。微信端本地调试通常需要把wx目录下的接口地址改成后端局域网 IP,并在微信开发者工具里关掉域名校验,否则会一直报“不在以下 request 合法域名列表中”。这一步是新手最容易卡住的地方,先记着,第 4 章会细说。
3. 核心模块拆解:充值、消费、预约三条主链路怎么走
骨架看清了,接下来看真正干活的代码。会员系统的价值全在“钱算得对、预约不冲突”上,我把源码里最值得逐行读的三条链路拎出来讲。
3.1 充值接口:本金与赠送金如何入账
充值是最容易写出 bug 的地方,因为涉及“充多少送多少”的规则。常见做法是后端接收充值金额,按会员等级或活动规则算出赠送额,然后在一个事务里同时更新会员余额和写流水。下面这段是这类系统里典型的服务层写法(Java 示例,Node 版逻辑相同):
@Transactional public void recharge(Long memberId, BigDecimal amount, BigDecimal giftAmount) { // 1. 查会员,加行锁防止并发充值导致余额覆盖 Member member = memberMapper.selectForUpdate(memberId); if (member == null) throw new BizException("会员不存在"); // 2. 更新余额:本金加 amount,赠送金加 giftAmount member.setBalance(member.getBalance().add(amount)); member.setGiftBalance(member.getGiftBalance().add(giftAmount)); memberMapper.updateById(member); // 3. 写两条流水,本金和赠送金分开记,方便对账 balanceLogMapper.insert(buildLog(memberId, 1, amount, BigDecimal.ZERO, "充值本金")); if (giftAmount.compareTo(BigDecimal.ZERO) > 0) { balanceLogMapper.insert(buildLog(memberId, 3, BigDecimal.ZERO, giftAmount, "充值赠送")); } }逻辑说明:selectForUpdate是行锁,防止两个店员同时给同一会员充值导致后写覆盖先写,这是并发场景下的血泪经验,源码里如果没加锁,二次开发时建议补上。参数amount是实收本金,giftAmount是赠送额,两者分开传而不是在方法里算,是为了让活动规则可以放在上层配置,改活动不用动这段核心逻辑。流水表里本金和赠送金各记一条,月底对账时能清楚看到“这个月送出去多少”,而不是一笔糊涂账。
3.2 消费扣款:先扣赠送金还是先扣本金
消费扣款有个业务规则问题:客户卡里既有本金又有赠送金,做一次护理该先扣哪个?行业里常见做法是先扣赠送金,再扣本金,因为赠送金通常有有效期,先花掉对客户和门店都合理。源码里如果实现了这个顺序,说明作者懂业务。核心扣款逻辑大致如下:
@Transactional public void consume(Long memberId, BigDecimal totalAmount, String remark) { Member member = memberMapper.selectForUpdate(memberId); BigDecimal gift = member.getGiftBalance(); BigDecimal principal = member.getBalance(); // 1. 先扣赠送金,不够的部分再扣本金 BigDecimal useGift = gift.min(totalAmount); BigDecimal remain = totalAmount.subtract(useGift); BigDecimal usePrincipal = principal.min(remain); if (useGift.add(usePrincipal).compareTo(totalAmount) < 0) { throw new BizException("余额不足,请先充值"); } // 2. 回写余额 member.setGiftBalance(gift.subtract(useGift)); member.setBalance(principal.subtract(usePrincipal)); memberMapper.updateById(member); // 3. 记一条消费流水,金额为负,赠送金消耗单独记 balanceLogMapper.insert(buildLog(memberId, 2, usePrincipal.negate(), useGift.negate(), remark)); }逻辑说明:min用来取“赠送金和消费额中较小的那个”,保证不会扣成负数。remain是赠送金扣完后还差多少,再由本金补。余额不足时直接抛业务异常,事务回滚,不会出现扣了一半的情况。参数totalAmount是本次消费总额,remark记录消费项目,方便后续查账。这里有个细节:流水里本金和赠送金都记成负数,对账时把balance_log按member_id求和,应该等于会员当前余额,这是验证扣款逻辑对不对的最快方法。
3.3 微信端预约:openid 绑定与时间冲突校验
微信端的核心不是页面多漂亮,而是身份识别和预约不撞车。身份识别靠微信授权拿code换openid,再和会员表里的openid关联;预约不撞车靠提交时校验同一时间段该服务是否已被约满。下面是小程序端提交预约的典型请求和对应后端校验:
// 微信小程序端:提交预约 wx.request({ url: baseUrl + '/api/appointment/create', method: 'POST', header: { 'token': wx.getStorageSync('token') }, // 登录后拿到的 token data: { serviceId: 12, appointTime: '2024-06-01 14:00:00' }, success(res) { if (res.data.code === 0) wx.showToast({ title: '预约成功' }); else wx.showToast({ title: res.data.msg, icon: 'none' }); } });后端收到请求后,先根据 token 解析出memberId,再查该服务在该时间段是否已有status in (0,1)的预约,有则返回“该时段已约满”。参数serviceId对应项目表,appointTime是预约时间点。这里常见做法是把时间按半小时或一小时切片,校验同一serviceId同一时间片是否已满,而不是简单判断“有没有人约过”,否则一个项目明明能同时服务三个人,却被第一个预约卡死。源码里如果只做了简单存在性判断,二次开发时建议改成按容量校验,这是门店实际运营会立刻暴露的问题。
4. 避坑与排查:跑不起来、微信端报错、余额对不上
这套源码我拆下来,新手最容易在四个地方翻车。每条都按“现象 → 原因 → 解决”写,照着排查能省不少时间。
现象一:后端启动报Table 'xxx' doesn't exist。原因通常是只建了库没导 SQL,或者init.sql里的库名和application.yml里配的不一致。解决:先确认application.yml的url里库名,再用mysql -uroot -p 库名 < sql/init.sql导入,导入后show tables核对表数量。
现象二:微信端页面空白或一直转圈。原因多半是接口地址还指向作者自己的服务器,或者本地调试没关域名校验。解决:全局搜baseUrl或request域名,改成自己后端局域网 IP(如http://192.168.1.10:8080),微信开发者工具里勾选“不校验合法域名”,真机调试则需在公众号后台配置合法域名。
现象三:充值后余额显示不对,或者对账差钱。原因常见于流水和余额没在同一个事务里,或者赠送金被重复计算。解决:先跑一条 SQL 验证——SELECT member_id, SUM(amount), SUM(gift_amount) FROM balance_log GROUP BY member_id,和member表的balance、gift_balance对比,对不上就说明某次操作漏写或重复写了流水,顺着时间倒查。
现象四:两个店员同时操作同一会员,余额被覆盖。现象是 A 充值成功、B 也充值成功,但最终余额只加了一次。原因是查询会员时没加行锁,两个事务读到同一个旧值再各自回写。解决:在selectById换成selectForUpdate(或对应数据库的FOR UPDATE),把查会员和更新放在同一事务里。
提示:改任何涉及金额的代码前,先在测试库造一个“本金 100、赠送金 50”的会员,跑一遍充值、消费、退款,用上面那条求和 SQL 验证流水和余额一致,再动生产逻辑。
5. 二次开发怎么改:从单店到多店、从手动到自动的三个改造点
源码能跑通只是起点,真正值钱的是按门店实际需求改。我一般从三个方向下手,按改动成本从低到高排。
改造点一:会员等级与折扣规则外置。源码里折扣如果写死在代码里(比如if level == 2 then 0.9),改一次活动就要发一次版。做法是加一张level_rule表,存等级、折扣率、升级所需累计消费,消费时查表算折后价。改动集中在扣款方法开头,加一次查询即可,风险低、收益大。
改造点二:预约从“存在性校验”升级为“容量校验”。给项目表加一个capacity字段(同时可服务人数),预约时统计该时间片已确认数量,小于容量才放行。SQL 大致是SELECT COUNT(*) FROM appointment WHERE service_id=? AND appoint_time=? AND status IN (0,1),和capacity比较。这个改动直接决定门店能不能把预约功能真正用起来。
改造点三:多门店隔离。单店源码通常没有store_id,要做连锁就得在会员、流水、预约、员工表都加store_id,并在所有查询里带上当前登录员工的store_id过滤。这是伤筋动骨的改法,建议用拦截器统一在 SQL 层注入门店条件,而不是逐个接口手改,否则漏一个就是跨店数据泄露。
| 改造点 | 改动范围 | 风险 | 建议顺序 |
|---|---|---|---|
| 等级折扣外置 | 1 张新表 + 扣款方法 | 低 | 先做 |
| 预约容量校验 | 项目表加字段 + 预约接口 | 中 | 次做 |
| 多门店隔离 | 多表加字段 + 全局拦截 | 高 | 最后做 |
验证方法很朴素:每改一处,就用第 4 章那条流水求和 SQL 跑一遍,再手动走一次“充值→预约→消费→退款”全流程,看余额、预约状态、流水三者是否自洽。我吃过一次亏,改折扣逻辑时忘了退款也要按原折扣比例退,结果测试没覆盖退款,上线后对账差了小两千。从那以后我每次动金额相关代码,都强制把退款路径单独跑一遍再提交。希望这套拆解能帮你少走几步弯路,把源码真正改成门店能用的东西。
本文还有配套的精品资源,点击获取