ARTICLE DETAIL

资讯详情

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

ThinkPHP与Laravel双框架下的积分制商城系统设计与实现

ThinkPHP与Laravel双框架下的积分制商城系统设计与实现 开门见山说个场景一个开在写字楼底商的零食自选超市SKU七百多个顾客自己拿篮子挑结账时员工会问一句“有会员卡吗”。老板想把线下的积分余额搬到手机上同时做一个可以线上下单、到店自提的小商城。这个需求听起来不难但实际落地时牵扯的东西远比想象多——完整的商品体系、库存联动、订单状态机、积分账户、抵扣规则、双框架接口协同任何一个环节没设计好后面都是坑。这篇文章就是这套“积分制零食自选超市商城销售平台”的设计与实现复盘。整个系统最终用了 ThinkPHP 和 Laravel 两个框架协作完成属于典型的“老系统 新框架渐进式改造”项目。如果你是做 PHP 商城开发、积分体系设计或者正面临多框架混合架构的整合这篇文章应该能帮你少走不少弯路。1. 先搞清楚“自选超市积分制”到底是一门什么生意很多开发者拿到需求就急着建表、写接口结果做出来的东西跟业务对不上。我习惯先把业务吃透再谈技术实现。这个项目的业态和传统 B2C 电商有明显区别积分制的玩法也远不止“消费攒分”这么简单。1.1 业务场景拆解零食自选超市有几个显著特点客单价低通常二三十块钱、SKU 数量大、复购频率高、冲动消费占比大。这些特点直接影响系统设计。客单价低意味着支付链路的稳定性要求高不能因为结算流程多一步就流失顾客。SKU 数量大意味着商品管理后台必须高效否则上架、改价、调库存都要累死人。复购率高意味着积分、会员体系有发挥空间但也对系统准确性提出更高要求——用户的积分凭空少了一分客服立刻就会收到投诉。冲动消费占比大则意味着自选篮子的体验必须流畅最好在手机端也能随时加入购物车、快速结算。传统电商里用户是先搜后买信息架构围绕商品搜索和推荐展开自选超市则是“逛”的逻辑用户在线下已经看到实物扫码进入商城更多是为了完成支付、查看积分、领优惠券。所以这个系统的核心不是搜索引擎而是“快速结算 积分闭环”。1.2 积分制的核心闭环积分制设计最怕做成“死积分”——用户攒了一堆分但花不出去或者没兴趣花。攒分容易花分难最后积分变成鸡肋。这个项目里我们把积分闭环拆成四个环节获取、消耗、冻结、过期。获取下单消费按比例返积分签到、活动赠送作为辅助。消耗积分抵现、积分换购商品。这是核心出口用户能看到积分实实在在变成钱。冻结用户用积分参与换购时订单未完成前先把积分冻结防止退款退积分造成的重复占用。过期积分设置有效期避免历史积分无限累积造成财务核算上的负债膨胀。技术层面这四个环节最终都要落到一张清晰的积分流水表上。后面我会专门讲表结构这里想强调一个认知积分在账务上近似“预收款项”你发给用户的每一分积分都对应未来的成本支出。所以流水记录必须像对账一样严谨每一笔都要有业务单号关联。1.3 系统模块的目标拆分基于业务拆解系统被划分为四个子模块商城前台商品展示、自选篮子、结算支付、订单追踪、积分查询与抵扣。会员中心登录注册、积分明细、优惠券、收货地址、提货码。运营后台商品管理、库存管理、积分规则配置、订单处理、积分人工调整。系统层双框架数据同步、统一日志、定时任务订单超时关闭、积分过期提醒。这套划分在这类项目里基本是标配。后面的技术方案都是围绕这四个子模块展开的。2. 双框架怎么分工ThinkPHP 与 Laravel 共存的原因与切分逻辑标题里写着“Thinkphp_Laravel框架”很多人会疑惑一个项目为什么要用两个框架统一用一个不更省事吗这个问题的答案恰好就是这个项目里最有价值的架构决策。2.1 为什么一个项目会出现两个 PHP 框架现实原因很常见这家超市原本就有一套用 ThinkPHP 写的进销存系统管商品、仓库、供应商用了两三年稳定运行。现在要新增线上商城把积压的需求一起解决团队评估后得出两个结论商城部分涉及 API 设计、队列、状态机用 Laravel 开发效率更高而老系统两年没动也没必要重写继续在 ThinkPHP 上迭代成本最低。所以架构决策不是“哪个框架好”而是“哪个框架适合哪一块”。老系统承载的是内部管理场景访问量小、逻辑以 CRUD 为主、中文文档多、招人容易新商城承载的是用户端对外请求需要严格的中间件体系、优雅的队列抽象、便捷的 API 资源层。两个框架各干各擅长的活通过共享数据层联通这是渐进式改造里最常见的路径。2.2 按职责切分管理端用 ThinkPHP用户端用 Laravel我在这个项目里的切分方式非常明确ThinkPHP 负责/admin管理后台商品上下架、库存调整、订单发货、积分规则配置、人工积分调整、数据报表。Laravel 负责/api用户端接口商品列表、自选篮子、下单结算、支付回调、积分查询与抵扣、订单状态查询。为什么偏心 Laravel 做用户端因为用户端的关注点是并发下的数据一致性。Laravel 的事务封装、锁机制、队列任务重试、中间件过滤都比 ThinkPHP 顺手尤其是写订单这类高一致性要求的接口Laravel 的 Eloquent 模型事件配合数据库事务代码能写得非常干净。管理端为什么留在 ThinkPHP一是老系统已有商品和供应商数据直接在原表上做管理功能改动最小二是管理端并发低、容错空间大框架的先进特性对这类场景的提升不明显稳定压倒一切。2.3 双框架下的数据层与公共组件如何共用两个框架共存最担心的是数据各写各的最后对不上账。我们的做法是三层隔离第一层共享 MySQL 数据库。两个框架连同一个库表前缀统一字符集统一为 utf8mb4。商品表、订单表、积分表、会员表全部共用不搞分库。小体量项目分库就是给自己找麻烦。第二层共享 Redis。自选篮子、接口频率限制、支付回调幂等标记、并发锁都放 Redis。两个框架都配置同一套 Redis 实例key 命名统一前缀比如cart:user:{id}、lock:stock:{sku_id}避免冲突和误删。第三层公共代码抽成 composer 包。像统一响应格式、统一错误码、文件上传、短信发送这类能力我抽了一个common-service包两个框架通过 composer 引入。公共包里不含框架专属代码不依赖门面模式底层直接基于 PDO 和 Redis 扩展实现保证两边都能调用。这套方案看起来直白却解决了两个框架混用的最大痛点数据结构统一、公共能力统一、入口分开。只要守住这三条线双框架并行并不可怕。3. 积分账户与商城数据模型的设计细节商场类系统表结构就是地基。地基歪了后面全是坑。这块我把设计过程中比较关键的几张表拆开讲每张表都结合实际踩过的坑。3.1 从一张零食小票推导出核心表结构去自选超市买一包薯片小票上会有商品名称、单价、数量、小计、合计、积分余额。简单一张纸背后涉及的商品模型其实是 SPU 和 SKU 两层。零食的特点是同一款商品有口味、克重、包装规格的差异。比如某品牌薯片原味 70g、番茄味 70g、原味 135g它们属于同一个 SPU但对应三个不同的 SKU。商品表存 SPU 层公共信息sku 表存具体规格、价格、库存、条码。库存扣减、购物车、订单明细都以 SKU 为准。商品列表展示时按 SPU 聚合进入详情页再让用户选具体规格。这个模型不做后面的库存和订单一定会乱。3.2 积分流水与账户余额必须分开两张表很多新手把积分余额直接做成会员表里的一个字段积分变动就 update 一下。这在小流量下也能跑但一旦出现并发操作、异常退款、客服调整就说不清楚账了。我的设计是两张表member_account存账户当前状态integral_log存每一笔变动明细。member_account核心字段会员ID、可用积分、冻结积分、累计获得、累计使用。可用积分是真正能抵扣的额度冻结积分是参与换购但订单未完成的部分。integral_log表则更细ID、会员ID、变动类型获得/使用/冻结/解冻/过期/人工调整、业务单号关联订单号或活动编号、变动数量、变动后余额、操作人、创建时间。这里有一条经验必须分享积分流水必须记录“变动后余额”。没有余额快照对账时你只能靠推算一旦中间有误删或漏记数据就永远对不齐了。加了快照任何一笔异常都能快速定位是哪个时间点出了问题。3.3 商品上下架、库存与积分比例配置零食有保质期这个特性很容易被忽略。上架商品时要加批次和有效期字段库存数字不等于能卖的数字有效期不足两个月的商品要么自动沉底要么禁止销售。设计上可以在商品 SKU 表加两个字段near_expiry_days和expiry_status由定时任务每天扫描更新。积分抵扣规则单独建一张integral_rule表配置维度包括适用商品分类、抵扣比例上限、单笔订单最高抵扣金额、积分与现金的换算比例。比如默认规则是“100 积分抵 1 元单笔订单最高抵 10 元零食类目最高抵 30%”。规则独立成表的好处是运营可以用后台改配置不用找开发提需求改代码。最后是索引设计。订单表按order_no建唯一索引所有查询要有连贯的链路积分流水表联合索引(member_id, create_time)支撑用户端积分明细的分页展示购物车 Redis 的 key 设计成cart:{member_id}热点数据天然分散不会出现单 key 过大。4. 用户端核心流程的实现从自选到结算再到积分抵扣用户端是整个系统的门面流程走得顺不顺直接影响转化率。这里我把从自选到结算的完整链路拆开重点说几个容易出现并发问题的环节。4.1 自选篮子的会话与购物车设计零食自选超市的逛购属性强用户可能一边逛一边往篮子里加东西。我们做的是“游客可加购物车结算时强制登录”的方案兼顾体验和转化。购物车数据放 Redis 的 Hash 结构key 是cart:{member_id}field 是 SKU IDvalue 是数量。这种做法配合商品实时价格读取比把价格冗余进购物车更稳妥——价格以商品表为准结算时重新计算避免用户加购后商家改价造成金额不一致。购物车里的商品状态要及时检查。用户结算时系统会筛选出已下架、库存不足、近效期的商品在前端明确提示“xx 商品已不可购买”。零食场景经常出现某个口味断货购物车不清掉这些无效数据用户会觉得系统很蠢。4.2 结算单的生成与积分抵扣计算结算是一个多步骤的复杂操作读取购物车商品 - 校验状态与库存 - 计算商品总额 - 计算积分抵扣 - 计算运费 - 生成订单 - 扣减库存 - 扣减积分。我用的方案是“预结算 订单落库”分两步。预结算阶段生成一个结算快照包含商品明细、金额明细、积分抵扣明细返回给前端确认用户确认后后端再按快照生成正式订单。积分抵扣计算要注意先后顺序先算出商品应付总额再按规则计算最高可抵扣积分取“用户可用积分”和“规则上限”的较小值。如果用户输入的抵扣积分超过可用余额直接拦截并提示不给数据库层面留下脏数据的机会。4.3 库存扣减防超卖在积分商城里的特殊性超卖问题在普通商城和积分商城里的严重程度完全不同。普通商城超卖补个库存就行积分商城超卖意味着用户拿积分换了并不存在的商品处理起来非常被动退款要退积分、还要赔补偿运营成本极高。所以库存扣减我做了两道防线。第一道防线是 Redis 预扣。下单接口先执行DECR stock:{sku_id}返回值小于 0 说明库存不足直接失败回滚。每个 SKU 都有独立的锁 key天然避免并发扣减。第二道防线是数据库层面的条件更新。生成订单时执行UPDATE goods_sku SET stock stock - ? WHERE sku_id ? AND stock ?如果影响行数为 0说明库存已经被扣完了事务回滚。两步都过了才提交订单数据。积分扣减同样要考虑并发。我用的是唯一流水号约束每笔积分扣减生成一个唯一的request_no在integral_log表里建唯一索引重复请求插入必然失败。这比“先查余额再扣减”的写法安全得多天然抵御了并发重复提交。整个链路还有一个兜底支付回调必须做幂等处理。微信/支付宝回调可能重复推送系统里对每笔订单绑定支付单号处理前先查是否已更新避免重复处理订单导致积分、库存双重扣减。5. 双框架协同的实战踩坑记录理论说完了来点实在的。双框架混跑实际遇到的问题是文档里不会写的我把印象最深的几个坑和排查过程记录下来比八股文有价值。5.1 跨框架的会话与登录态无法互通第一个坑发生在用户登录模块。最开始我给用户端也用 Laravel 的 session 管理登录态管理端用 ThinkPHP 的 session两边分开存本来相安无事。问题出在管理后台有个“模拟用户登录”功能——客服帮忙查看用户订单时需要在后台直接以用户身份调接口。两个框架 session 处理机制不一样ThinkPHP 的 session 文件路径和 Laravel 的完全独立后台模拟登录跳转到用户端接口时用户端完全不认这个会话直接返回未登录。排查的时候看日志两边都在写文件但读的路径不同一开始根本没往这个方向想。最终方案是把用户端鉴权全部改成 JWTToken 存 Redis请求头携带。管理端模拟登录时生成一个短期有效的 JWT拼到跳转链接里用户端接口校验通过即可。这样两边完全解耦不再依赖框架自身的 session 机制。这个改动之后登录态问题再没出现过。5.2 路由与中间件的差异处理ThinkPHP 的路由模式是“路径优先路由规则为辅”即使不开路由也能通过控制器/方法访问Laravel 是显式路由不注册的路由一律 404。混用的时候Nginx 要小心分流。我把两个入口文件分开部署ThinkPHP 使用/admin.php入口Laravel 使用/api.php入口Nginx 里配置 location 规则精确匹配。location ^~ /admin/ { try_files $uri /admin.php?$query_string; } location ^~ /api/ { try_files $uri /api.php?$query_string; }这样做的好处是让 ThinkPHP 的控制器方法无法被外部路径猜测恰好也解决了一部分安全问题。要知道 ThinkPHP 老版本出过不少 RCE 漏洞根源就是路由解析太宽松影响了不该影响的入口。5.3 两个框架日志格式不统一导致的排查困难项目上线一个月后运营反馈“用户下单偶发超时”我们排查时发现两边日志格式完全不同Laravel 是结构化 JSONThinkPHP 是纯文本按行拼字符串。同一个错误要同时查两份日志时间格式、字段含义都不一样定位问题的效率低得让人抓狂。痛定思痛我把两个框架的日志格式统一成同样的 JSON 结构timestamp、level、channel、message、context、trace_id。trace_id 是关键在每个请求入口生成贯穿订单、支付、积分扣减全程。用户报障时提供订单号我就能用 trace_id 把两个框架的日志串起来一条链路从头看到尾。日志改造是在项目中期做的虽然费了点时间但每次排查问题至少节省一半时间。这个投资非常值。6. 框架安全问题与升级加固含 CVE 修复实践框架安全在 PHP 项目里是老生常谈但每次出问题都是大事故。这个项目因为同时用了两个框架等于同时背了两套历史包袱安全加固必须优先处理。6.1 ThinkPHP 历史漏洞的教训ThinkPHP 5.0 系列早期的 RCE 漏洞影响面非常大核心原因就是路由解析中方法名可以调用到危险函数攻击者可以绕过正常逻辑直接执行代码。我接手这个项目时先做了一次安全审计确认老系统的 ThinkPHP 版本存在风险直接升级到官方修复后的版本同时做了三层防护。第一层关闭 debug 模式。thinkphp 的 debug 页面会暴露完整报错信息、环境变量、版本号等于把家门钥匙送到攻击者手里。生产环境必须强制关闭。第二层入口文件改名并加访问控制。管理后台入口从admin.php改成一段不可猜测的名字然后绑定 IP 白名单。管理后台本来就该是内网工具开放公网访问等于自己给自己挖坑。第三层PHP 层做函数禁用。在php.ini里通过disable_functions禁掉exec、shell_exec、system、passthru、proc_open等危险函数。即使攻击者拿到执行点也切断了提权路径。6.2 Laravel 侧的 CVE 修复实践Laravel 框架的安全性整体比 ThinkPHP 高但近两年也陆续出现了一些 CVE比如与文件上传、请求内容类型解析相关的问题。2024 年那批 Laravel CVE 公开后我们的处理路径是先升级 composer 依赖到 lts 修复版再检查历史代码里有没有使用受影响的功能模块。这里有一条非常实用的经验不要等着框架官方发安全通告才想起升级把composer.lock里的核心依赖版本钉在“已修复的版本区间”然后每两周做一次composer audit。它会直接列出依赖包已知安全漏洞和对应修复版本比人肉盯公告可靠得多。6.3 部署环境的加固细节除了框架层面的修复运行环境的加固同样重要。一是运行账户权限。PHP-FPM 用独立低权限用户运行目录权限精细化静态资源目录只读上传目录关闭执行权限storage 和 runtime 目录禁止被外部直接访问。这些是基础中的基础。二是 WAF 规则。Nginx 层加了一些基础 WAF 规则对可疑的注入字符串、远程代码执行特征做拦截。PHP 项目的注入点通常在 SQL 拼接和路由边界先把这两层堵住能拦住绝大多数自动化攻击。三是对敏感操作做二次校验。积分人工调整、管理员权限变更这类操作必须双人确认或输入管理员密码二次验证。这种设计看似麻烦实际上是对内部人员风险的一道保险。三个框架版本问题叠加在一起让我明白一个道理框架只是工具安全意识才是底盘。升级要及时、入口要收敛、权限要最小化这些都是老生常谈但每次事故复盘最后都会回到这些最基础的点上。写在最后的一些个人体会这个项目从设计到上线前后经历了三四个月中间踩的坑远不止上面这些。但让我收获最大的不是某一个框架的技巧或某一张表的设计而是理解了“技术选型服务于业务存量”这个原则。ThinkPHP 和 Laravel 共存不是洁癖问题而是老系统迭代过程中的现实选择。与其纠结统一框架的“理想架构”不如想清楚怎么让两套系统在数据层和运维层面稳定协作。积分体系上线三个月后运营数据显示用户使用最多的积分场景不是换购而是结账时的直接抵现。这个反馈说明闭环保真做对了——积分要能花得出去用户才有持续获取的动力。最后分享一个容易被忽视的小设计积分过期提醒。我们提前七天给用户发提醒通知告知即将过期多少积分。第一次做的时候只发了站内提醒打开率极低后来改成了短信通知效果立竿见影当天积分消费量能翻一倍。积分系统不是做出来就完事的它需要持续地运营和微调而技术人要做的就是把这些调整做进系统里让运营可以自己改不用每次都来麻烦研发。
返回列表