ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

洗浴中心管理系统开发:手牌计费与日结账务设计要点

洗浴中心管理系统开发:手牌计费与日结账务设计要点 简介在门店管理系统中计费规则与账务处理往往是比界面更核心的工程难点。无论是餐饮、零售还是服务行业按次、按时长、按过夜等多变的计费口径以及跨天结算、并发抢占等场景都对数据库设计和事务一致性提出较高要求。洗浴中心管理系统正是这类问题的典型缩影顾客手牌作为业务主键所有消费挂账、离店统一结算浴资超时计算、凌晨封账、技师提成归集均需在数据层面精确定义。本文从通用计费模型与事务处理切入结合洗浴业态的手牌状态管理、计费规则表、结算事务、日结脚本等实践拆解一套可落地的数据模型与账务流程帮助后端开发与实施人员快速理解此类系统的核心设计要点避免在并发、对账和报表口径上反复踩坑。1. 洗浴中心管理系统.doc 背后要做的是哪一套账在门店系统这个序列里洗浴中心管理系统.doc 往往不是源码而是一份需求说明书或者立项方案。真正接触过这类项目的开发都知道洗浴中心和餐饮、零售差别很大顾客进门先领手牌所有消费都记在手牌上离店才统一结账。因此这套系统的难点不在界面而在手牌账户、计费规则和凌晨封账三条主线。解读文档时重点看浴资怎么算、中途加单怎么记、跨天账单归到哪一天三条线理顺了会员卡和库存都是常规模块。适合正在接门店系统、又没做过洗浴业态的开发和实施人员。2. 先把数据模型画对洗浴中心管理系统的会员、手牌与计费表2.1 手牌号是业务主键不是自增 ID洗浴中心里每个顾客进门领一个手牌对应一把更衣柜钥匙。后续的浴资、搓澡、饮料、过夜全部挂在这个手牌上。设计时手牌要单独建表手牌号建议直接用柜号或连续号段例如 001 到 500打印成手环上的条码既能扫码也能人工输入。常见做法是建一张locker_wristband表记录手牌状态字段定义如下字段类型说明wristband_novarchar(10)手牌号主键对应更衣柜statustinyint0 空闲 / 1 占用 / 2 挂失 / 3 停用checkin_idbigint当前占用记录的 ID空闲时可空member_idbigint绑定的会员 ID可空broken_atdatetime挂失或停用时间这里的关键是status和checkin_id要分开存。很多第一版设计只存状态结果顾客换柜、手牌损坏重新发牌时历史账单就找不回来了。锁柜和锁账单是两件事前台看到的这个柜子有人和后台的这张账单有效必须解耦。2.2 计费项目和卡项分开建表改价不动表结构浴资、助浴、足疗的价格会随季节调整会员折扣又和项目组合绑定。把价格直接写死在订单表里是初期最顺手、后期最痛苦的方案。我一般拆成service_item和member_card两张表中间再挂套餐明细关系。service_item里除了价格还必须带billing_type因为浴资按次、足疗按时长、过夜按封顶价结算口径完全不同。CREATE TABLE service_item ( item_id INT PRIMARY KEY, item_name VARCHAR(50) NOT NULL, billing_type TINYINT NOT NULL COMMENT 1按次 2按时长 3按过夜, price DECIMAL(10,2) NOT NULL, overtime_rate DECIMAL(10,2) DEFAULT 0 COMMENT 按时长项目超时单价, unit_minutes INT DEFAULT 60 COMMENT 一个计费周期的分钟数, status TINYINT DEFAULT 1 );unit_minutes和overtime_rate是给按时长项目用的。比如足疗 90 分钟 128 元超时每 30 分钟加 30 元那么unit_minutes30、overtime_rate30计费时按分钟差计算而不是让代码里写死 128 和 30。价格变动只改表记录不用重新发版连锁门店还能用一套代码支撑不同分店的差异化定价。2.3 技师分成和库存消耗要预留扩展位洗浴行业和餐饮不一样服务由技师完成提成按项目金额的区间比例走。如果提成比例存进订单表后面调整一次就要改一遍历史数据对账时还会和当月的实际发放口径冲突。正确做法是消费明细里只存technician_id和service_item_id提成比例在日结时由计提规则统一计算。同理毛巾和一次性消耗品按项目展开项目表里留consume_stock_items字段日结时一并扣库存避免月底盘点才发现毛巾少了三百条却不知道哪个环节透的。注意手牌、项目、技师三张表是洗浴中心管理系统的骨架。骨架不歪后续的计费和报表才能接得住。订单表里永远不要直接存浴资 38 元这种写死文本。3. 浴资计费是洗浴中心管理系统最容易翻车的环节规则与实现3.1 先定计费口径按分钟算展示层再决定要不要按小时浴资是洗浴中心最主要的收入也是最容易产生客诉的部分。常见口径有三种按次、按小时超时补差、按过夜。按次最简单进门 38 元不限时按小时需要记录started_at和settled_at超过基础时长后按周期补收。我建议系统内统一按分钟计算展示层再决定显示成小时或半小时一组。原因是跨天结算时分钟口径最好对账小时口径经常出现 59 分钟和 60 分钟差一位小数的情况客诉起来说不清。计费规则我习惯单独存一张参数表比写在代码里灵活规则项典型值说明base_minutes180基础时长超过后开始计超时base_price38基础时长内的费用cycle_minutes30超时计费周期cycle_price15每个超时周期的费用night_start00:00过夜计费开始时间night_price58过夜一口价3.2 用一段最小函数把超时和过夜算出来下面的函数按分钟口径计算浴资适合放在结算接口里复用def calc_bath_fee(started_at, settled_at, rule): total_min int((settled_at - started_at).total_seconds() // 60) if total_min rule.base_minutes: return rule.base_price # 过夜跨天且结算时间在凌晨5点前按过夜一口价封顶 if started_at.date() ! settled_at.date() and settled_at.hour 5: return rule.night_price extra_min total_min - rule.base_minutes cycles (extra_min rule.cycle_minutes - 1) // rule.cycle_minutes # 向上取整 return rule.base_price cycles * rule.cycle_price参数含义base_minutes是免超时的时间窗口cycle_minutes是超时计费周期向上取整表示超时 31 分钟按 2 个周期收而不是 1.03 个周期。过夜判断放在超时判断之后因为过夜是封顶价一旦命中就不走超时累加。注意settled_at.hour 5这个边界值凌晨 4 点结账和早上 6 点结账在规则里是两个收法门店不特殊但系统要支持参数配置。3.3 凌晨封账日结和夜审不能用同一个脚本洗浴中心很少零点关门营业往往会持续到凌晨所以日结时间点不能是自然日 24 点通常是早上 5 点到 7 点之间。封账脚本要做两件事把未结账手牌的当前时间写入账单快照再按技师、项目、会员卡汇总生成当日营业表。这里有个高频坑跨天过夜的账单消费时间跨了两个自然日但收款只发生在离店那一天。如果按created_at汇总这笔钱会归到离店日和当天的营业日报对不上财务会天天来问。提示日报的归属日建议用手牌的结账时间而不是消费时间。夜审跑批时先把前一天 5 点到当天 5 点的账单标记为已封账再去重算技师提成。顺序错了报表必错。4. 业务链路落库洗浴中心管理系统里的开牌、加单与离店结算4.1 开牌接口手牌状态和账单必须同时落库顾客进店取手牌系统要做两个原子操作把手牌改为占用创建一条待结算的主账单。两步必须在一个事务里完成否则会出现手牌发出去了、账单却没建顾客消费完无法结账的尴尬局面。def checkin(wristband_no, member_idNone): with db.transaction(): band db.execute( SELECT status FROM locker_wristband WHERE wristband_no%s FOR UPDATE, wristband_no) if band.status ! 0: raise LockerOccupied(wristband_no) db.execute( UPDATE locker_wristband SET status1 WHERE wristband_no%s, wristband_no) bill_id db.insert( INSERT INTO bill(main_flag, wristband_no, member_id, started_at) VALUES (1,%s,%s,NOW()), wristband_no, member_id) return bill_idFOR UPDATE的作用是锁住手牌行防止两个收银员同时把同一个手牌发给两位顾客。洗浴中心高峰期的开牌是典型的并发写操作这一行不加后续所有对账问题都会从这里冒出来。main_flag标记主账单后面所有加单都挂在主账单下离店时一次结算避免拆成多笔导致收银员漏单。4.2 加单采用明细记账不维护主单实时总额顾客中途点一瓶水、加一个足疗这些都属于bill_item明细。主账单上不要维护实时总额总额由明细汇总算出。理由很直接任何一笔加单都要可追溯如果直接改主单金额日结时无法区分是手动调价还是正常消费。INSERT INTO bill_item(bill_id, item_id, technician_id, qty, amount, created_at) SELECT 1024, item_id, %s, 1, price, NOW() FROM service_item WHERE item_id%s;注意amount是从service_item现查出来的不是前端传过来的数字。前端传价格的收银系统是常见漏洞价格篡改成本太低。服务端按item_id重新取价同时记下当时生效的会员折扣规则编号。技师 ID 也在这里落库日结算提成时直接按bill_item分组不用回去翻操作日志。4.3 离店结算确认、支付、冲正必须是一个事务洗浴中心的结算比餐饮复杂在三个地方顾客可能同时结多人账单会员余额不足时要拆成部分划卡加部分现金支付平台回调还可能超时。完整流程是汇总主账单下所有明细、扣除折扣、生成应收、写支付流水、把手牌置回空闲。这几步不拆开任何一步失败都会出现钱收了柜子没还或者柜子还了账没消。场景处理方式会员余额不足余额全扣差额生成现金支付单多人合并结账只结算主账单各明细归并到同一支付单支付平台回调超时支付单标记为待确认收银台二次查询而不是直接入账payment_record里必须有status待支付、已支付、已冲正和channel现金、会员卡、微信、支付宝。冲正不是删流水而是写一条负数流水保证流水总和始终等于收银现金盘点数。多支付方式混合时把每笔拆分记录都写成独立流水而不是一个总金额这样日结对账时定位到具体某一笔就快得多。注意支付回调超时最忌讳的是先改状态再对账。生产环境的标准做法是保持待支付由收银端主动向支付平台查询结果查询成功后才落已支付。被动等回调的单据丢单率比想象中高。5. 洗浴中心管理系统上线后重点盯的并发冲突、对账口径与日结脚本5.1 手牌并发不要靠应用锁要靠数据库行锁很多第一版用全局锁或 Redis 分布式锁防止手牌重复发放单机门店够用一旦做到多门店连锁就失效。数据库行锁永远比应用锁可靠事务提交后锁自动释放。注意FOR UPDATE必须放在事务里且查询和更新要使用同一连接否则锁不会生效。用 ORM 时尤其要检查连接复用配置连接池换连接会导致锁在另一个会话里形同虚设。5.2 对不上账时先查三个位置日结对账不平九成是三类原因跨天账单归属日不对、冲正流水直接改了原记录、退柜时没有清空checkin_id导致重复关联。排查顺序建议是先按手牌找当天未闭合的账单再按支付渠道加总对现金最后检查bill_item里有没有technician_id为空的项目。这里给一个能直接跑的核对脚本日结后看输出是否为空mysql -uops -p洗浴系统 -e \ SELECT wristband_no,bill_id FROM locker_wristband WHERE status1 AND checkin_id IS NULL;输出为空说明手牌状态与账单关联一致有记录说明存在开了牌但没建账单的异常要补账单而不是直接改手牌状态。这类检查脚本建议写进 cron每天封账后自动跑一遍异常单独发到运维群。5.3 一个可以直接抄的日结脚本骨架#!/bin/bash BILL_DATE$(date -d yesterday 05:00 %F) mysql -uops -p$DB_PASS -e UPDATE bill SET settled_flag1 WHERE settled_at $BILL_DATE 05:00 AND settled_flag0; INSERT INTO daily_report(biz_date, item_id, amount) SELECT $BILL_DATE, item_id, SUM(amount) FROM bill_item WHERE settled_flag1 GROUP BY item_id;这段脚本的核心是把账单封存和报表生成分成两条语句执行先标记settled_flag再根据标记生成报表。这样同一天重跑脚本不会产生重复汇总封账和生成报表之间断电也不至于把报表跑成半截。生产环境建议把两步放进存储过程并加事务cron只负责触发判断逻辑留在数据库里。日结脚本要保留每次执行的时间戳和影响行数门店隔周来查一笔账时靠的就是这份执行痕迹定位数据是哪个批次产生的。本文还有配套的精品资源点击获取
返回列表