资讯动态

基于ThinkPHP的水族馆商品销售与经营管理系统实战

发布时间:2026/10/21 9:13:26 来源:尧图企业网站定制
1. 为什么一家水族馆会需要一套专属管理系统大概半年前我常去买鱼的那家水族馆出了个不大不小的事故店员记错了一缸金龙鱼的损耗数盘点时怎么都对不上老板翻了一个多小时纸质单据最后发现是上个月的销售记录漏了两笔。就在那一刻我意识到整个水族馆的进销存、客户关系、活体损耗居然全靠纸笔和一个不太准的Excel表格撑着。后来聊起来老板说他也想上系统市面上的进销存软件他试过好几套要么是通用模板根本不管活体水产要么是价格贵得离谱还要按年续费。我问他要什么样的系统他列了一堆能管商品销售、能管会员打折、能跟踪活体损耗、能看哪个品种赚钱、能控制店员权限。其实拆开来看也就是“商品销售 经营管理”这两个大板块只是行业属性让每个板块都带了些特殊味道。这篇内容就是基于这个需求做的一套“水族馆商品销售与经营管理系统”。整套东西跑在Thinkphp框架上目前已经在一家中小型水族馆里用了三个月线上订单、门店收银、库存盘点、会员积分的日常操作都走了这套系统。如果你正在给水产、花鸟、宠物这类带活体的零售业态做管理系统或者是Thinkphp新手想找一个完整的业务实战案例这篇文章应该对你有参考价值。我会从需求拆解、表结构设计、核心功能实现讲到权限控制和上线之后的坑尽量还原整个决策和开发过程。2. 水族馆业务拆解先搞懂“卖鱼”和“卖杯子”的本质区别2.1 水族商品的两个极端高损耗活体与标准化器材水族馆的店面不大SKU却有几百个。从分类上大致可以切成五块活体水产观赏鱼、虾、蟹、龟、饲料药品、器材设备过滤泵、加热棒、灯具、造景材料底砂、沉木、水草、服务类缸体维护、寄养、上门清洗。这里最麻烦的是活体。一条龙鱼卖8000块顾客看了半小时最终没买鱼缸里的损耗却每天都在发生。活体商品和普通商品在库存模型上完全是两回事普通商品比如一袋饲料进多少、卖多少、退多少库存是可精确扣减的。活体商品进货入缸今天损耗2条明天顾客买走1条后天又病死了1条库存数量始终是流动的。这意味着库存表里必须有一张损耗登记表而且损耗要和库存扣减联动。我在设计时给每一种活体商品增加了一个“可售库存”和“在缸库存”的概念在缸数量是当前鱼缸里的实际存栏数可售数量会扣除已经预定但还没取货的部分。每次盘点时店员登记实际存栏数系统自动计算损耗数并写入损耗台账。2.2 规格和计量单位为什么不能用统一的“件”来管理水族商品的计量单位极其不统一。鱼按“条”、虾按“两”、底砂按“斤”、还有按“袋”和按“套”的。一开始如果图省事统一用“件”后面做报表的时候全是乱的。我的做法是给商品表增加一个单位字段同时在规格层面做了区分活体商品走“规格数量”比如“红龙鱼-25cm/条”和“红龙鱼-35cm/条”价格完全不同这是按尺寸区间分的。器材类商品走标准SKU按型号库存。散装类底砂、饲料走称重计量按“斤”设置浮动换算。价格上也有讲究。水产商品的卖家经常在周末做活动同一品种上午一个价、下午一个价这在会员系统里用优惠券处理起来并不顺手。我是在商品价格表里设置了三档价格基础价、会员价、活动价活动价有起止时间。价格查询组装商品详情时优先取活动价其次会员价最后基础价。2.3 核心业务流程销售、采购、损耗三条线怎么串起来整个系统说到底就是三张业务主线的流转销售线顾客选品 - 加购物车 - 结算 - 支付 - 扣库存 - 出库/预约自提 采购线向供应商下单 - 到货登记 - 入缸/入库 - 损耗登记 会员线注册 - 累积积分 - 升级等级 - 享受折扣 - 积分抵现这三条线互相之间有关联。比如销售订单可以关联会员会员积分会影响成交价采购入库会影响商品库存损耗登记会影响可售库存。关键的地方在于库存表的扣减不能由“库存-1”这样的简单操作完成必须同时写库存流水。我一开始图省事直接在商品表上update库存字段后来对账对不上才补了库存流水表。每一步增删改都有记录才能追溯到底是哪个环节出了问题。这块业务拆解做完数据库表结构就自然出来了。整个系统大概有20多张表其中核心表是这几张表名用途关键字段goods商品主表名称、分类、单位、进价、售价、状态goods_sku商品规格表规格名、条形码、库存、预警值stock_flow库存流水表商品ID、变动类型、变动数量、关联单号loss_record损耗登记表商品ID、损耗原因、损耗数量、登记人order订单主表单号、会员ID、总金额、状态、支付方式order_detail订单明细表订单ID、商品ID、单价、数量、小计member会员表手机号、等级、积分、余额member_level会员等级表等级名、折扣率、升级条件supplier供应商表名称、联系人、电话、结算方式purchase_order采购单表供应商ID、采购总金额、状态admin_user后台用户表用户名、密码、角色IDrole / rule角色权限表角色名、权限规则列表3. 技术选型Thinkphp到底适不适合这类业务系统3.1 从原生PHP转Thinkphp的理由这个项目最开始我考虑过原生PHP因为整个系统其实不算复杂。但写了两天发现不对劲原生代码要做RBAC权限控制从session校验到菜单生成全得自己写数据库操作尤其是事务处理每个SQL都要手动绑定参数效率太低。后来切到Thinkphp确实省了很多事。客观说Thinkphp这类成熟框架对这个体量的业务系统是正合适的。它不像某些重型框架那样上来就要配置一大串东西也不像原生代码那样所有轮子都要自己造。路由、ORM、模板引擎、验证器、中间件都是开箱即用。水族馆这种系统核心是业务逻辑的准确性框架稳定性排第二。Thinkphp的文档在国内社区很多遇到问题搜索成本低这一点对单人开发或者小团队来说很重要。我用的版本是Thinkphp 6.x。相比之前的版本它改变了一些目录结构和底层实现比如采用PHP 8强制更规范的路由定义。数据库操作上我觉得最顺手的还是模型层和查询构造器链式查询写起来非常直观。3.2 工程结构与环境准备整套系统我分成了两个端面向门店员工的PC管理后台和面向顾客的手机端实际上这些是新手易上手的内容结合具体项目落地后可以有不同的部署方式。代码层面直接复用一套核心代码通过模块区分业务逻辑。后台管理功能跑在PC浏览器上手机端做了简单的响应式页面顾客可以在手机上浏览商品、下单、查会员积分。环境方面Thinkphp 6要求PHP版本至少在8.0以上我用的是8.1。数据库用的MySQL 8.0Web服务器选的Nginx部署在云服务器上。本地开发环境用的Windows 小皮面板开发调试比较方便。项目的目录结构基本上是Thinkphp的标准布局我额外添加了几个应用模块来区分业务域app/ ├── index/ # 前台 │ ├── controller/ │ │ ├── Goods.php │ │ ├── Cart.php │ │ ├── Order.php │ │ └── Member.php │ └── view/ ├── admin/ # 后台管理 │ ├── controller/ │ │ ├── Login.php │ │ ├── Goods.php │ │ ├── Stock.php │ │ ├── Order.php │ │ ├── Loss.php │ │ ├── Member.php │ │ ├── Supplier.php │ │ └── Report.php │ └── view/ └── common/ └── model/ # 公共模型层db.php里配置好数据库连接同时开启数据库的字段严格模式避免从查询构造器返回字段类型不一致的问题。伪静态规则在Nginx里配置好把请求都转发到入口文件。这些基础工作大概半天就能跑通。3.3 为什么数据库选MySQL而不是更轻量的方案业务数据量没到需要上重型中间件的程度MySQL足够且稳定。表结构的设计上我坚持一个原则能用外键关联体现的关系就在模型层定义好关联但不一定在数据库里建物理外键。物理外键在并发写入时会带来性能损耗而且后期如果要分表会非常痛苦。Thinkphp的模型关联方法让我们可以用逻辑外键实现同样的效果。比如Order模型加载订单明细只需要这样写public function detail() { return $this-hasMany(OrderDetail::class, order_id); }查询时用with方法预加载避免N1查询$orders Order::with([detail, member])-where(status, 1)-select();这套模型关联用顺手了代码会非常干净每张业务表对应一个模型复杂查询我就在模型里封装方法控制器的代码可以保持很薄。4. 销售端落地从顾客下单到库存扣减的完整链路4.1 商品浏览与购物车设计水族馆的顾客买鱼和买饲料行为差异很大买鱼看实物可能当场拍板也可能在缸前站半小时买器材更像网上购物参数、品牌、价格仔仔细细比。对手机端的购物车我做了轻量化的设计顾客可以添加商品、改数量、删除但不做复杂的促销叠加只算等级折扣和积分抵扣。购物车数据用Redis缓存key设计成cart:{user_id}值存JSON数组商品ID、规格ID、数量、加入时间。这个方案的好处是顾客下次打开手机端还能看到购物车内容不占数据库空间。结算时把购物车数据提交到后端生成订单同时清除缓存。Thinkphp里操作Redis很简单use thinkfacadeCache; $cartKey cart: . $userId; $cartData Cache::get($cartKey);这里有个细节需要考虑活体商品不建议长时间放在购物车里。鱼的存栏数时时在变购物车里的商品可能在结算时已经没货了。所以提交订单后、扣库存之前必须重新校验库存数量。如果不够直接提示顾客联系店员选替代品。4.2 订单状态机与异常处理订单状态我定义了这么几个待支付、已支付待提货、已完成、已取消、售后中。之所以没有物流状态是因为这个店的订单基本都是到店自提或者同城配送不需要对接快递。订单状态流转图上有这么几个关键路线正常路线待支付 - 已支付待提货 - 已完成取消路线待支付 - 已取消超时自动关单售后路线已完成 - 售后中 - 已完成退款/换货完成后回到已完成超时自动关单用的是定时任务每隔一段时间扫描一次超过30分钟还未支付的订单把库存释放掉状态改成已取消。这里注意释放库存不能简单地把库存加回去也要写库存流水不然对账时会多出来无来源的数据。订单号生成我直接用年月日随机数比如20250115-XXXXXX。一开始用的是自增ID做订单号后来发现订单号在客服沟通时需要报给顾客数字太短容易被记混而且同一天多笔订单时容易看错所以改成了后缀随机数的方案。4.3 库存扣减的事务一致性最容易被忽略的一个坑扣库存这个操作是整个系统里我改动次数最多的地方。最初的版本是这样的流程验证库存 - 生成订单 - 扣减库存 - 清购物车。问题是这几步之间没有事务保护万一第二步成功了第三步失败就会出现一个已支付订单却没有库存可提的尴尬局面。改成事务后整个流程变成了Db::transaction(function () { // 获取购物车数据 // 校验库存 // 生成订单主表记录 // 生成订单明细 // 扣减库存写库存流水 // 清除购物车缓存 });不过在思考一些可能的并发场景例如两个顾客同时买同一缸的最后一条鱼事务并不能完整体现这一点。此时还需要配合行锁。我的做法是扣库存的SQL语句加上条件更新只扣减库存大于等于购买数量的记录$result GoodsSku::where(id, $skuId) -where(stock, , $buyNum) -dec(stock, $buyNum) -update();如果更新影响行数为0说明库存不足直接抛异常回滚。这个方案比手动加锁简单可靠在高并发下也不会超卖。库存流水表是独立于商品表的每次库存变动都往流水表里插一条记录类型是初始入库、销售出库、采购入库、损耗扣减、盘点调整、订单取消回补。这样一来库存表只存结果流水表存过程任何库存异常都能通过流水还原。4.4 收银台与支付渠道接入门店的收银场景和线上场景不太一样。门店收银是店员在电脑上直接操作顾客扫码支付。线上是顾客在手机端下单自己完成支付。两套入口走的是同一套订单逻辑只在来源字段上区分。支付渠道我选择了常见的微信支付和支付宝分别通过官方SDK接入。Thinkphp框架下用官方SDK再包一层服务类统一对外提供支付、回调、退款三个方法。回调地址要特别注意微信支付的异步通知是直接POST到配置的回调URL不能用框架的路由默认规则我在后台定义了专门的回调路由。支付回调处理的核心逻辑// 支付成功回调 public function notify() { // 验证签名 // 更新订单状态待支付 - 已支付待提货 // 给会员加积分 }验证签名这步最容易出错。我踩过一次坑微信的回调参数里附带了一些额外字段如果直接用全参数验签会失败。正确做法是只取参与签名的字段按文档要求的规则重新拼接。支付宝那边的验签逻辑类似只是字段名不同。5. 经营管理的深水区会员、采购、权限与报表5.1 会员等级设计打折不是拍脑袋定的水族馆的会员体系要符合零售逻辑。老顾客对活体的购买频次很高按等级打折符合消费心理学。我把等级分成普通会员、银卡会员、金卡会员、钻石会员四档升级条件由累计消费金额决定。等级升级条件折扣率生日礼普通会员注册即享100%无银卡会员累计消费满1000元95%无金卡会员累计消费满5000元90%小礼品钻石会员累计消费满10000元85%免费养护服务每次支付完成后更新会员累计消费金额然后判断等级是否需要提升。注意这里的折扣不要去改订单里的单价而是结算时算出一个折扣优惠金额单独记录在订单表里。这样有利于后续统计实际营收和让利幅度。积分抵现的逻辑采用了简化规则100积分抵1元单笔订单最多抵订单金额的20%。积分的获取同样是在支付成功后自动到账退单时扣回。整个过程在事务里完成避免了积分异常。5.2 供应商与采购单活体损耗最终去哪了水族馆一般有几个固定供应商进鱼走批发市场器材从代理商那里拿。为了对账方便我给每个供应商建了档案记录了联系人、电话、结算方式周结、月结和历史供货记录。采购的核心是采购单。店长下单后供应商发货到货时店员按品种和数量登记入库。活体入缸后数量就进入在缸库存。这时如果发现运输过程中有死鱼直接在采购单上登记损耗数量。这个损耗要单独记录不走销售成本而是算作采购损失。月底的利润报表上活体的采购损耗会和销售成本并列展示。如果不分开水族馆的毛利会被活体损耗严重污染看不出真正赚钱的品种。5.3 RBAC权限控制老板、店长、店员各管一摊水族馆平时店里就两三个人但权限还是要分清楚的。 老板要看全局报表但不想操作日常录单店长负责采购、商品管理和店员排班店员只负责收银、会员登记和损耗录入。基于角色的访问控制RBAC是整个后台的基础能力之一。我设计了角色表和权限节点表角色与权限多对多关联。权限节点按模块划分比如“商品管理-新增”、“商品管理-编辑”、“订单管理-查看”、“报表-查看财务数据”。在后台每个控制器里通过自定义的一个checkRule方法判断当前登录用户的角色是否有该节点权限没有就跳转到无权限提示页。Thinkphp里我通过在控制器基类的初始化方法中统一做登录校验和权限校验这样每个子控制器不需要自己重复写判断逻辑。权限节点维护做成JSON字符串存在角色表里新增节点时不需要改数据库结构只加一个节点标识就行。5.4 经营报表哪些数据值得店长每天盯着看报表模块是这个系统里老板最喜欢的功能。每天营业结束后老板能看到一张汇总表当日营收、订单数、新会员数、损耗金额、库存预警。这些数据放在仪表盘首页不需要打开Excel手工统计。我重点做了三个报表第一是商品销售排行。按销量和销售额两个维度排序周期可以选本周、本月、本季度。统计结果对选品帮助很大哪个品种赚钱一目了然同时也是调整陈列位置、减少滞销品种进货量的依据。第二是损耗报表。按损耗原因分组统计病害死亡、运输损耗、顾客退养、操作不当。起初我发现损耗原因大多集中在“其他”这说明店员点起来嫌麻烦。后来改成默认选“疾病死亡”并且强制填写备注数据质量才上来。第三是毛利报表。这里涉及进价和售价的联动计算公式是毛利 销售额 - 销售成本 - 损耗成本。销售成本按商品进价算损耗成本按损耗商品的加权进价算。这套统计在通用进销存软件里几乎没有但是对水族馆老板来说比什么数据都重要。6. 性能、安全与运维上线前必须做的几件事6.1 缓存策略与查询优化前台商品列表页是访问量最大的页面。首页要把热门商品、新到活体、促销商品都展示出来如果每次刷新都查数据库高峰期MySQL会吃力。我给商品列表和商品详情做了Redis缓存缓存时间设5分钟。数据变更时后台编辑商品、调整库存主动删除对应缓存保证最终一致性。商品列表页的查询用Thinkphp的查询构造器做了预加载避免N1$goodsList Goods::with([category, sku]) -where(status, 1) -order(sort_order, desc) -paginate(12);后台报表统计用的是聚合查询。比如计算某时间段销售总额$total Order::where(status, in, [1, 2]) -where(pay_time, between, [$startTime, $endTime]) -sum(total_amount);这种单表聚合查询在MySQL上加个索引效率很高。我在pay_time、status、order_id这几个字段上建了联合索引报表页面的响应速度从最初的两三秒降到了几百毫秒。6.2 安全加固验证码过滤、权限校验一个都不能少这类业务系统是开放性项目安全不能马虎。主要做了几件事登录页面加了图形验证码。Thinkphp自带验证码扩展包几行配置就能用。重点是用完验证码后需要清理session防止同一个验证码被重放。所有搜索、排序、查询参数都走了框架自带的数据过滤能力。分页参数、搜索关键词、排序字段做了白名单校验。排序字段尤其容易出问题如果直接把前端传来的字段名拼进SQL就可能被拿去探测数据库结构。我的做法是排序字段必须在预设白名单里否则使用默认排序。后台所有接口在基类初始化时就校验session、校验权限节点前台用户和后台用户完全隔离。文件上传比如商品图片做了类型和大小限制图片重命名后再存储路径加了一层随机目录避免路径被猜测。6.3 备份与数据导出水族馆的店里没有专职运维所以我设计了最简单的每日备份方案。利用系统自身的计划任务每天凌晨3点执行数据库导出导出文件保留最近7天同时同步到另一台服务器作日常异地容灾。备份命令是系统命令调用执行的脚本是自动生成SQL格式备份并压缩存储。桌面运维偶尔需要用Excel看数据我也做了订单导出和商品导出功能支持按日期范围筛选。这个模块虽然技术含量不高但它是整个系统稳定运行的兜底。有一次数据库文件损坏就是靠前一天的备份在半小时内恢复了数据损失被控制在最小。7. 复盘踩过的坑和值得扩展的方向7.1 印象最深的三个坑第一个坑是活体商品的价格波动。某个周末老板临时调了红龙鱼的价格活动价忘记设置结束时间导致连续三天顾客都按活动价购买每天少赚几百块。现在的处理方式是活动价必须设置结束时间结束前两小时后台给老板发提醒从机制上消除人为遗忘的风险。第二个坑是库存流水和订单关联。最初设计库存流水表时没有关联单号字段后来发现某个订单被取消后回补库存的数量和原始订单对不上。加了关联单号后所有库存变动都能追溯到具体业务单据对账简单了很多。第三个坑是角色权限变更不下去。原来改角色权限后已登录用户的权限还是旧的。原因是权限放到了session里用户不重新登录就不会更新。后来改成每次请求都从数据库读取当前角色权限虽然多了一次查询但是权限变更马上生效。加上Redis缓存后这个额外查询的消耗几乎可以忽略。7.2 如果还要继续迭代我觉得有三个方向第一是活体鱼缸的实时水位和温度监控通过物联网设备把环境数据接进来出现异常直接给店员发消息。这样能把水族馆的特色服务做成真正的差异化功能。第二是会员消费画像。现在系统里已经积累了会员的购买记录可以做品种偏好分析、购买频次分析然后针对性做一些定向优惠券。比如某个会员常买南美灯科鱼推送相关品种的促销信息转化率大概率比群发广告高得多。第三是小程序的入口。水族馆的顾客大多是周边居民微信小程序比手机端H5更适合高频访问。小程序的分享能力还能帮店铺做老带新的裂变营销。回看这三个月的上线使用这个系统的核心价值其实不在于技术有多复杂而是它把水族馆经营中最容易被忽视的损耗、库存、毛利这些环节用一套逻辑完整地管了起来。老板说最大的变化是月底对账时间从两小时缩短到二十分钟而且终于能确切知道哪种鱼是在赔本赚吆喝。对做这类业务系统的人来说这句话比什么技术指标都让人踏实。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑