尧图精选

理发店会员管理系统源码:SQLite+Python极简实现,夫妻店两天上手

🕒 发布时间:2026/10/2 15:58:22 📁 来源:尧图网络
简介这是一套面向中小理发店、社区店与夫妻店的极简会员管理系统源码定位为一体化轻量客户管理工具适合希望低成本落地门店数字化、或学习前后端完整业务闭环的开发者。系统内置SQLite单机即可运行覆盖账号注册登录与鉴权、会员新增编辑与状态切换、充值消费、员工与服务类型管理、经营概览与服务分布统计、员工业绩及审计日志等模块并支持会员校验码强校验、交易流水导出与租户隔离。压缩包共74个文件约376KB以37个Java后端源码、5个Vue页面、4个JS与4个CSS前端资源为主另含PNG截图、HTML、JSON配置、SQL脚本及打包脚本结构清晰便于二次开发。目前已有55人学习下载。整体围绕理发店真实日常设计不做过度设计离线友好且可打包为可执行程序适合门店终端直接落地也能作为中小型管理系统全栈实践的参考案例。1. 理发店极简会员管理系统的真实需求为什么夫妻店不需要 SaaS社区理发店的收银台往往只有一部手机和一个二维码立牌。老板王姐的日常是客人剪完头发她翻出微信聊天记录找上次充值余额再手写一张纸条记下本次消费。月底对账时纸条丢了三张两个老客户的余额对不上只能自己贴钱平账。这不是个例而是大量中小理发店、社区店、夫妻店的真实状态。它们不需要连锁品牌那套带营销裂变、员工排班、供应链管理的重型 SaaS年费动辄几千块功能用不到十分之一数据还存在别人服务器上。它们真正需要的是一套能跑在旧笔记本或收银平板上的轻量客户管理工具核心就三件事记会员、扣余额、看流水。理发店会员管理系统源码这个方向之所以值得做是因为需求足够窄、足够痛而且技术栈可以极简到一个人两天就能改出适合自己门店的版本。本文面向想自己动手的店主、帮朋友做小工具的开发者以及想找一个真实练手项目的初学者把从数据表设计到本地跑通的完整路径拆开讲清楚。2. 极简会员管理系统的数据模型与选型为什么用 SQLite 而不是 MySQL2.1 三张表撑起核心业务会员、消费记录、充值记录理发店的业务逻辑比想象中简单。一个会员有姓名、手机号、余额、创建时间一次消费有会员 ID、消费金额、消费项目、扣款后余额、时间一次充值有会员 ID、充值金额、赠送金额、充值后余额、时间。所有复杂功能都是这三张表的衍生。我一般会先画清楚表关系再动手写代码因为一旦字段设计错了后面改起来全是血泪经验。会员表 members 的字段设计字段名类型说明idINTEGER PRIMARY KEY AUTOINCREMENT会员唯一编号nameTEXT NOT NULL会员姓名允许重名phoneTEXT UNIQUE手机号作为主要检索键balanceREAL DEFAULT 0当前余额单位元created_atTEXT建档时间ISO 格式noteTEXT备注比如“只剪不烫”消费记录表 consume_records字段名类型说明idINTEGER PRIMARY KEY AUTOINCREMENT记录编号member_idINTEGER NOT NULL关联会员amountREAL NOT NULL本次消费金额itemTEXT消费项目如“洗剪吹”balance_afterREAL扣款后余额用于对账created_atTEXT消费时间充值记录表 recharge_records字段名类型说明idINTEGER PRIMARY KEY AUTOINCREMENT记录编号member_idINTEGER NOT NULL关联会员amountREAL NOT NULL实收金额bonusREAL DEFAULT 0赠送金额balance_afterREAL充值后余额created_atTEXT充值时间为什么用 SQLite 而不是 MySQL夫妻店的收银电脑可能是一台十年前的 Windows 笔记本装 MySQL 要配服务、设密码、开端口一旦系统重装数据迁移就是灾难。SQLite 单文件存储整个数据库就是一个.db文件复制走就能备份换电脑直接拷过去就能用。并发方面理发店同时只有一个收银员操作SQLite 的写锁完全够用。Python 标准库自带sqlite3不需要额外安装任何数据库驱动这是极简落地的关键。2.2 用 Python 标准库建库建表一段可以反复执行的初始化脚本下面这段代码可以直接保存为init_db.py在门店电脑上运行一次即可完成建库。我习惯把建表语句写成IF NOT EXISTS这样即使误操作重复执行也不会报错相当于给自己留了后悔药。import sqlite3 import os DB_PATH os.path.join(os.path.dirname(__file__), barber_shop.db) def init_database(): conn sqlite3.connect(DB_PATH) cursor conn.cursor() # 会员表手机号唯一余额默认0 cursor.execute( CREATE TABLE IF NOT EXISTS members ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT UNIQUE, balance REAL DEFAULT 0, created_at TEXT DEFAULT (datetime(now, localtime)), note TEXT ) ) # 消费记录表balance_after 用于快速对账 cursor.execute( CREATE TABLE IF NOT EXISTS consume_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id INTEGER NOT NULL, amount REAL NOT NULL, item TEXT, balance_after REAL, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (member_id) REFERENCES members(id) ) ) # 充值记录表bonus 记录赠送金额方便算实际折扣 cursor.execute( CREATE TABLE IF NOT EXISTS recharge_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_id INTEGER NOT NULL, amount REAL NOT NULL, bonus REAL DEFAULT 0, balance_after REAL, created_at TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (member_id) REFERENCES members(id) ) ) # 手机号索引收银时按手机号查会员是最高频操作 cursor.execute(CREATE INDEX IF NOT EXISTS idx_members_phone ON members(phone)) cursor.execute(CREATE INDEX IF NOT EXISTS idx_consume_member ON consume_records(member_id)) cursor.execute(CREATE INDEX IF NOT EXISTS idx_recharge_member ON recharge_records(member_id)) conn.commit() conn.close() print(f数据库初始化完成{DB_PATH}) if __name__ __main__: init_database()这段脚本的逻辑很直白连接数据库文件如果不存在就自动创建依次执行三张表的建表语句最后给高频查询字段加索引。参数方面DB_PATH放在脚本同目录下方便整个文件夹打包迁移。datetime(now, localtime)确保时间戳是本地时间避免 UTC 时差导致对账时日期错位。索引idx_members_phone让手机号查询从全表扫描变成索引查找会员数量到几千条时差距明显。执行方式就是在命令行里运行python init_db.py看到“数据库初始化完成”即可。2.3 充值、消费、查余额三个核心函数的实现与参数说明建完表之后真正每天被调用的就是三个函数充值、消费、查余额。我把它们写在一个core.py里所有操作都包在事务里要么全成功要么全回滚避免出现扣了钱没记流水的情况。import sqlite3 from datetime import datetime DB_PATH barber_shop.db def get_connection(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 让查询结果可以按列名访问 return conn def recharge(phone, amount, bonus0.0): 充值amount 为实收bonus 为赠送两者都计入余额 conn get_connection() try: cursor conn.cursor() cursor.execute(SELECT id, balance FROM members WHERE phone ?, (phone,)) member cursor.fetchone() if not member: return {ok: False, msg: 会员不存在请先建档} new_balance member[balance] amount bonus cursor.execute(UPDATE members SET balance ? WHERE id ?, (new_balance, member[id])) cursor.execute( INSERT INTO recharge_records (member_id, amount, bonus, balance_after) VALUES (?, ?, ?, ?) , (member[id], amount, bonus, new_balance)) conn.commit() return {ok: True, balance: new_balance} except Exception as e: conn.rollback() return {ok: False, msg: str(e)} finally: conn.close() def consume(phone, amount, item洗剪吹): 消费先检查余额是否充足再扣款并记录 conn get_connection() try: cursor conn.cursor() cursor.execute(SELECT id, balance FROM members WHERE phone ?, (phone,)) member cursor.fetchone() if not member: return {ok: False, msg: 会员不存在} if member[balance] amount: return {ok: False, msg: f余额不足当前余额 {member[balance]:.2f} 元} new_balance member[balance] - amount cursor.execute(UPDATE members SET balance ? WHERE id ?, (new_balance, member[id])) cursor.execute( INSERT INTO consume_records (member_id, amount, item, balance_after) VALUES (?, ?, ?, ?) , (member[id], amount, item, new_balance)) conn.commit() return {ok: True, balance: new_balance} except Exception as e: conn.rollback() return {ok: False, msg: str(e)} finally: conn.close() def query_balance(phone): 按手机号查余额和最近三次消费 conn get_connection() cursor conn.cursor() cursor.execute(SELECT name, balance FROM members WHERE phone ?, (phone,)) member cursor.fetchone() if not member: conn.close() return None cursor.execute( SELECT amount, item, created_at FROM consume_records WHERE member_id (SELECT id FROM members WHERE phone ?) ORDER BY created_at DESC LIMIT 3 , (phone,)) records [dict(row) for row in cursor.fetchall()] conn.close() return {name: member[name], balance: member[balance], recent: records}recharge函数里amount是顾客实际付的钱bonus是店里送的金额两者相加才是入账余额。比如“充 200 送 30”调用时传amount200, bonus30余额增加 230。consume函数先查余额再扣款余额不足直接返回提示不会产生负数余额。query_balance返回最近三次消费记录方便收银员和顾客核对。所有函数都返回字典调用方根据ok字段判断成功与否这种设计比抛异常更适合收银场景——收银员不需要看懂 traceback只需要看到“余额不足”四个字。3. 从命令行到收银台把脚本变成店员敢用的工具3.1 用 argparse 做一个收银员能记住的三命令入口有了核心函数下一步是让店员能操作。最简方案不是做界面而是做一个命令行入口只暴露三个命令充值、消费、查余额。店员只需要记住三个词比学一套收银软件快得多。import argparse from core import recharge, consume, query_balance def main(): parser argparse.ArgumentParser(description理发店会员管理) subparsers parser.add_subparsers(destcommand, requiredTrue) # 充值命令 p_recharge subparsers.add_parser(充值, help为会员充值) p_recharge.add_argument(phone, help会员手机号) p_recharge.add_argument(amount, typefloat, help实收金额) p_recharge.add_argument(--bonus, typefloat, default0, help赠送金额) # 消费命令 p_consume subparsers.add_parser(消费, help会员消费扣款) p_consume.add_argument(phone, help会员手机号) p_consume.add_argument(amount, typefloat, help消费金额) p_consume.add_argument(--item, default洗剪吹, help消费项目) # 查询命令 p_query subparsers.add_parser(查询, help查询会员余额) p_query.add_argument(phone, help会员手机号) args parser.parse_args() if args.command 充值: result recharge(args.phone, args.amount, args.bonus) if result[ok]: print(f充值成功当前余额{result[balance]:.2f} 元) else: print(f充值失败{result[msg]}) elif args.command 消费: result consume(args.phone, args.amount, args.item) if result[ok]: print(f消费成功剩余余额{result[balance]:.2f} 元) else: print(f消费失败{result[msg]}) elif args.command 查询: info query_balance(args.phone) if info: print(f会员{info[name]}余额{info[balance]:.2f} 元) for r in info[recent]: print(f {r[created_at]} {r[item]} {r[amount]:.2f} 元) else: print(未找到该会员) if __name__ __main__: main()这个入口的设计原则是命令用中文参数位置固定。店员操作时只需要输入python shop.py 消费 13800001111 35就能完成一次扣款。--bonus和--item是可选参数不传就用默认值。argparse会自动生成帮助信息店员忘了命令格式时输入python shop.py --help就能看到说明。把这段代码保存为shop.py和core.py、barber_shop.db放在同一个文件夹里即可运行。3.2 会员建档与批量导入用 CSV 把老客户一次性搬进来新系统上线时最麻烦的是把微信聊天记录和纸条上的老会员搬进来。我一般会先让店主整理一个 CSV 文件三列姓名、手机号、当前余额。然后用下面这段脚本批量导入避免一个个手动建档。import csv import sqlite3 def import_members(csv_path): conn sqlite3.connect(barber_shop.db) cursor conn.cursor() success, skip 0, 0 with open(csv_path, r, encodingutf-8-sig) as f: reader csv.DictReader(f) for row in reader: name row[姓名].strip() phone row[手机号].strip() balance float(row.get(余额, 0) or 0) if not phone: skip 1 continue try: cursor.execute( INSERT INTO members (name, phone, balance, note) VALUES (?, ?, ?, 批量导入) , (name, phone, balance)) success 1 except sqlite3.IntegrityError: # 手机号已存在跳过而不是报错中断 skip 1 conn.commit() conn.close() print(f导入完成成功 {success} 条跳过 {skip} 条) if __name__ __main__: import_members(members.csv)CSV 文件用utf-8-sig编码读取这样 Excel 保存的中文表头不会出现乱码。sqlite3.IntegrityError捕获的是手机号唯一约束冲突说明该会员已经存在直接跳过继续处理下一条不会因为一条重复数据导致整个导入中断。余额列允许为空空值按 0 处理。导入完成后所有会员的note字段标记为“批量导入”方便后续区分哪些是系统上线前的老数据。3.3 每日对账一条 SQL 核对流水与余额是否一致收银系统最怕的是账对不上。我习惯每天关店前跑一条对账 SQL检查每个会员的当前余额是否等于“所有充值金额加赠送金额减去所有消费金额”。如果不等说明有记录被漏掉或重复。SELECT m.id, m.name, m.phone, m.balance AS 当前余额, COALESCE(r.total_recharge, 0) AS 累计充值, COALESCE(c.total_consume, 0) AS 累计消费, COALESCE(r.total_recharge, 0) - COALESCE(c.total_consume, 0) AS 应有余额, m.balance - (COALESCE(r.total_recharge, 0) - COALESCE(c.total_consume, 0)) AS 差额 FROM members m LEFT JOIN ( SELECT member_id, SUM(amount bonus) AS total_recharge FROM recharge_records GROUP BY member_id ) r ON m.id r.member_id LEFT JOIN ( SELECT member_id, SUM(amount) AS total_consume FROM consume_records GROUP BY member_id ) c ON m.id c.member_id WHERE ABS(m.balance - (COALESCE(r.total_recharge, 0) - COALESCE(c.total_consume, 0))) 0.01 ORDER BY 差额 DESC;这条 SQL 只返回差额大于 0.01 元的会员正常情况下应该返回空结果。COALESCE处理没有充值或没有消费的会员避免 NULL 导致计算错误。0.01的容差是为了兼容浮点数精度问题。如果查出差额优先检查consume_records和recharge_records里是否有balance_after与前后记录不连贯的情况那通常意味着某次操作没有正确写入流水。4. 避坑与排查理发店会员系统落地时最容易翻车的五个地方4.1 手机号重复导致建档失败现象给新会员建档时提示UNIQUE constraint failed: members.phone但店员确认这个手机号没建过档。原因通常是之前用同一个手机号建过档但姓名不同或者批量导入时 CSV 里有重复行。解决方法是先按手机号查询如果已存在就提示“该手机号已建档姓名为 XXX”让店员确认是否同一人。如果确实是不同人共用手机号可以把手机号字段改为允许重复改用姓名加手机号组合检索但这样会牺牲查询便利性。我一般建议夫妻店坚持手机号唯一因为共用手机号的情况极少而唯一性带来的查询效率提升更实在。4.2 浮点数精度导致余额出现 0.01 元误差现象对账时发现某会员差额 0.01 元但流水看起来都对。原因是 SQLite 的 REAL 类型是浮点数多次加减后会产生精度误差。比如 0.1 0.2 在浮点数里不等于 0.3。解决方法是在所有金额计算后用round(value, 2)保留两位小数对账 SQL 里用ABS(差额) 0.01而不是差额 ! 0。更彻底的做法是把金额单位改成“分”用 INTEGER 存储显示时除以 100。对于理发店这种客单价几十元的场景REAL 加四舍五入足够用改成整数分反而增加代码复杂度。4.3 并发写入导致 database is locked现象收银员操作时偶尔弹出sqlite3.OperationalError: database is locked。原因是 SQLite 默认的写锁在多个连接同时写入时会阻塞如果某个连接没有及时提交或关闭其他写入就会超时。解决方法是在get_connection里设置timeout参数让连接等待而不是立刻报错conn sqlite3.connect(DB_PATH, timeout10)timeout10表示最多等 10 秒超时才抛异常。同时确保每个函数在finally里调用conn.close()避免连接泄漏。理发店只有一个收银台并发写入几乎不会发生但如果有第二个终端比如老板手机查账这个设置能避免大部分锁冲突。4.4 备份文件被误删或覆盖现象电脑重装后数据库文件丢失所有会员余额归零。原因是 SQLite 单文件存储文件没了数据就没了。解决方法是在shop.py里加一个自动备份逻辑每次启动时把barber_shop.db复制到backup/目录文件名带日期。保留最近 30 天的备份超过的自动删除。这样即使误删主文件也能从备份恢复。备份目录建议放在 U 盘同步文件夹里每天自动同步一次成本几乎为零。4.5 店员误操作扣错金额现象顾客消费 35 元店员输入成 350 元余额直接扣成负数或扣光。原因是命令行没有二次确认。解决方法是在consume函数里加一个金额上限检查单次消费超过 500 元时要求输入确认码或者直接在命令行入口加--confirm参数不传就打印“请确认消费金额350 元确认请输入 y”。更简单的做法是让店员养成先查余额再扣款的习惯查询命令会显示最近三次消费如果金额明显异常店员自己就能发现。5. 进阶技巧用手机浏览器查余额和导出月度报表5.1 用 Flask 起一个局域网查询页老板手机就能看命令行对店员够用但老板往往想随时看流水。最轻量的做法是用 Flask 起一个局域网服务只暴露一个查询页面手机连上店里 WiFi 就能打开。不需要登录因为只在局域网内可访问。from flask import Flask, request, render_template_string from core import query_balance app Flask(__name__) PAGE !DOCTYPE html html headmeta charsetutf-8title会员查询/title/head body stylefont-family: sans-serif; padding: 20px; h3理发店会员查询/h3 form methodget input namephone placeholder输入手机号 stylefont-size: 18px; padding: 8px; button typesubmit stylefont-size: 18px; padding: 8px 16px;查询/button /form {% if info %} div stylemargin-top: 20px; font-size: 18px; p会员{{ info.name }}/p p余额strong{{ %.2f|format(info.balance) }} 元/strong/p p最近消费/p ul {% for r in info.recent %} li{{ r.created_at }} {{ r.item }} {{ %.2f|format(r.amount) }} 元/li {% endfor %} /ul /div {% elif phone %} p stylecolor: red;未找到该手机号对应的会员/p {% endif %} /body /html app.route(/) def index(): phone request.args.get(phone, ).strip() info query_balance(phone) if phone else None return render_template_string(PAGE, infoinfo, phonephone) if __name__ __main__: app.run(host0.0.0.0, port5000)host0.0.0.0让服务监听所有网卡手机在同一 WiFi 下访问电脑 IP 加端口 5000 即可。页面用内联模板不需要额外模板文件整个脚本一个文件就能跑。查询逻辑复用core.py里的query_balance数据完全一致。老板手机收藏这个地址随时能查不用装任何 App。5.2 导出月度报表一条 SQL 加一个 CSV 写入每月底老板需要知道这个月充了多少、消费了多少、新增多少会员。下面这段脚本导出三个数字和一个明细 CSV。import csv import sqlite3 from datetime import datetime def monthly_report(year, month): conn sqlite3.connect(barber_shop.db) cursor conn.cursor() start f{year}-{month:02d}-01 if month 12: end f{year1}-01-01 else: end f{year}-{month1:02d}-01 cursor.execute( SELECT COALESCE(SUM(amount), 0), COALESCE(SUM(bonus), 0) FROM recharge_records WHERE created_at ? AND created_at ? , (start, end)) recharge_total, bonus_total cursor.fetchone() cursor.execute( SELECT COALESCE(SUM(amount), 0) FROM consume_records WHERE created_at ? AND created_at ? , (start, end)) consume_total cursor.fetchone()[0] cursor.execute( SELECT COUNT(*) FROM members WHERE created_at ? AND created_at ? , (start, end)) new_members cursor.fetchone()[0] print(f{year}年{month}月报表) print(f实收充值{recharge_total:.2f} 元) print(f赠送金额{bonus_total:.2f} 元) print(f消费总额{consume_total:.2f} 元) print(f新增会员{new_members} 人) # 导出消费明细 cursor.execute( SELECT m.name, m.phone, c.amount, c.item, c.created_at FROM consume_records c JOIN members m ON c.member_id m.id WHERE c.created_at ? AND c.created_at ? ORDER BY c.created_at , (start, end)) rows cursor.fetchall() conn.close() filename freport_{year}_{month:02d}.csv with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([姓名, 手机号, 消费金额, 项目, 时间]) writer.writerows(rows) print(f明细已导出{filename}) if __name__ __main__: now datetime.now() monthly_report(now.year, now.month)月份边界处理是关键end取的是下个月 1 号查询用 start AND end这样不会漏掉当月最后一天 23:59 的记录也不会把下月 1 号的算进来。CSV 用utf-8-sig编码Excel 打开不乱码。报表直接打印在终端明细导出成文件老板可以发给会计。这套系统我从给朋友店做第一版到现在前后改了七八次最大的教训是不要一开始就想着做界面、做 App、做云端。先把三张表和三个函数跑通让店员用起来根据实际反馈再加功能。理发店会员管理系统源码这个方向的价值不在于技术多复杂而在于它足够小、足够完整一个人能从头到尾掌控。如果你也想给自己或朋友的店做一套建议从init_db.py开始先跑通充值消费查询再逐步加对账和报表。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →