资讯详情

资讯详情

PHP在线药品销售系统全解析:从LNMP部署到RBAC权限与订单安全

做PHP开发这些年系统类项目我接过不少电商、OA、进销存都碰过但真要论“能写论文、能跑线上、还能拿来当模板”的项目我最常向朋友推荐的还是这套PHP在线药品销售系统。原因很简单它踩中了PHP项目的三个硬需求——业务闭环完整、权限模型清晰、安全问题典型。不管是毕业设计选了《基于PHP的在线药品销售系统的设计与实现》这个题目还是刚入行想搞懂一个药品电商系统的完整业务链路这篇博客应该都能给你不少能直接用的东西。我会把它从需求、数据库、下单支付、安全加固一直讲到论文和答辩该怎么写中间穿插我自己踩过的坑和补救方案。1. 为什么用PHP做药品销售系统选型时的真实考量1.1 系统要解决的核心问题和普通商城有什么不一样很多同学一看到“药品销售系统”第一反应是“这不就是一个商城吗把商品换成药品就行了”。如果你也这么想后面开发必然会返工。药品在线销售和普通电商最大的差别在于药品有特殊的审核流程、库存批次和效期管理以及必须可追溯的操作记录。这三个点直接决定了系统结构不能照搬普通商城。我们的核心业务场景其实是这样的C端用户注册登录后可以浏览药品分类、搜索药品、查看药品详情和库存状态用户可以加入购物车但在提交订单时如果订单里包含处方药必须上传或选择有效的处方单经过后台药师审核后才能继续支付后台管理员负责药品上架、分类维护、库存管理、订单处理和价格配置药师角色负责处方单审核核对用量、数量和合理性审核通过才能放行支付仓库人员根据订单进行出库、发货并维护药品的批次和效期信息。所以这套系统最少要拆出用户端、管理端两个大的子系统管理端还要细分管理员、药师、仓库人员等角色。这也是它在论文里可以写得“有深度”的原因——角色多、状态流长、边界清晰每一块都有东西可画可写。1.2 PHP和框架选择ThinkPHP还是Laravel我一直坚持一个观点这类系统的技术选型不要追求“看着高级”要追求“能写清楚、能维护、能演示”。PHP在这个项目里天然合适原因有三点开发效率高。从框架生成到后台管理界面两三天就能出一个可演示的版本部署成本低。LNMP环境几乎任何一台云服务器都能跑论文里环境配置好写答辩时现场部署也不容易翻车资料多。PHP商城类项目烂大街遇到问题搜得到答案比你用冷门后端框架硬撑要稳妥得多。至于框架ThinkPHP 6 和 Laravel 都是好选择。我个人在这个项目里用了 ThinkPHP 6主要理由是它足够轻、文档全、对新手友好而且很多国内教材和论文模板都默认用TP系列写“系统实现”章节时代码贴上去跟理论部分对得上。如果你更熟悉 Laravel也没问题但你要注意在论文里统一表述不要一会儿控制器一会儿路由闭包写得不伦不类。1.3 最终的架构方案LNMP Redis MySQL我最终定的技术栈是这样一张表写论文的时候可以直接改成架构设计章节的表格层次组件用途前端HTML CSS JavaScript用户端和管理端页面后端PHP 8 ThinkPHP 6业务逻辑、接口、后台管理缓存Redis购物车缓存、验证码、锁库存的原子操作数据库MySQL 8用户、药品、库存、订单等核心数据服务器Nginx静态资源与PHP转发第三方支付接口、短信接口在线支付、验证码通知这个结构不花哨但每个组件都有明确职责。写论文时架构图直接按这个分层画就行不用再加什么微服务网关之类的中看不中用的东西。架构越真实答辩越稳。2. 药品系统比普通商城多出的模块和权限设计2.1 三类核心角色的功能矩阵药品系统不是一个“用户自己买”的单边系统它中间带着审核和后台管理所以功能必须按角色拆开。我把功能清单整理成三个矩阵写需求分析的时候可以整段复制。前端用户端功能功能说明注册登录手机号/用户名密码图形验证码药品浏览分类导航、关键词搜索、药品详情购物车增删改、批量结算、处方药标识处方单管理上传处方图片、查看审核状态下单支付地址管理、订单确认、在线支付订单管理订单列表、取消订单、确认收货个人中心资料修改、收货地址、操作记录后台管理端功能角色功能超级管理员管理员账号管理、角色分配、审计日志、数据统计药品管理员药品分类、药品信息、规格库存、价格上下架药师处方单审核、处方驳回、限购设置仓库管理员批次维护、库存出入库、订单发货客服订单查看、退款处理、用户反馈这里要强调一个点订单和退款不要让普通管理员一删了之而是要走状态流转。我在开发时犯过的错误就是早期后台可以直接“删除订单”结果用户支付成功了但订单记录消失财务对账一片混乱。后来改成只有“取消订单”和“退款”两条动作线删除订单的按钮彻底移除才算消停。2.2 基于RBAC的权限控制实现思路RBAC也就是基于角色的访问控制是这个系统的权限核心。数据库里最少要三张表管理员表、角色表、权限表再配上管理员-角色关联和角色-权限关联。我实际还加了一层“功能菜单”因为后台左侧导航必须跟着角色变化不然药师进后台看到一堆库存按钮体验极差。权限判断我放在中间件里统一处理伪代码如下public function handle($request, \Closure $next) { $admin $request-adminUser; if (!$admin || !$this-hasPermission($admin[role_id], $request-path())) { return json([code 403, msg 没有访问权限]); } return $next($request); }hasPermission 的逻辑就是先查角色对应权限ID列表再判断当前请求路径是否在列表里。这个列表我会放到Redis里缓存半小时更新一次避免每次请求都查数据库。2.3 处方药审核流程系统必须实现的‘人工闸门’处方药和普通药最大的区别在于不能用户点了就能买。我在系统里加了一条处方单链路用户提交订单时若订单包含处方药系统自动锁定订单状态为“待审方”用户上传处方图片填写用药人姓名和用量说明后台生成一条处方审核记录状态为“待审核”药师登录后台查看处方图片核对订单中的药品和数量药师点击“通过”订单状态变为“待支付”点击“驳回”订单状态变为“已取消”并短信通知用户。这条链路并不复杂但数据库表设计时要特别注意一张处方单会关联多个药品明细所以最好的设计是prescription_order处方审核主表加上prescription_item处方明细表而不是把处方信息和订单明细揉在一起。我在第一版就把处方字段直接塞进了订单表后来既要支持一单多药又要支持审核记录追溯只能重构白白浪费两天时间。2.4 限购与操作日志容易被忽略又必须有的功能药品不是普通商品系统里必须有限购逻辑。我的做法是在药品表加一个字段limit_per_order下单时校验不可超过单次限购后台管理员也可以针对某个药品设置“每日限购数量”。这个校验要写两道加入购物车时提醒创建订单时强制拦截因为用户完全可能绕过购物车直接POST一个订单接口。操作日志我单独建了一张operation_log表后台所有关键操作上下架、改价格、改库存、审核处方、发货都要记录操作人、操作时间、IP地址和操作前后数据摘要。这块虽然写起来枯燥但到了论文“系统测试”和“运行维护”章节它就是最好的素材来源。3. 数据库建模从药品SKU到订单状态机3.1 核心表结构一张表说清楚每张表的职责数据库是整个系统最不能将就的部分。我最后落地的表结构不算多但每一张都有明确的业务含义。论文里画E-R图时下面这张表可以直接帮你梳理实体关系表名主要字段说明userid, username, password, phone, status普通用户表adminid, username, password, role_id, status后台管理员表role / permissionid, name, menu_ids / id, name, routeRBAC角色权限drug_categoryid, parent_id, name, sort药品分类支持无限级drugid, category_id, name, generic_name, spec, unit, manufacturer, price, prescription_type, limit_num, status药品主表product_skuid, drug_id, spec_name, barcode, price, image药品多规格表product_stockid, sku_id, batch_no, production_date, expiry_date, quantity, locked批次库存表cartid, user_id, sku_id, quantity, create_time购物车表addressid, user_id, receiver, phone, province, city, district, detail收货地址ordersid, order_no, user_id, total_amount, pay_amount, address_id, status, pay_time, ship_time, finish_time订单主表order_itemsid, order_id, sku_id, drug_id, drug_name, price, quantity, total_price订单明细快照paymentid, pay_no, order_no, platform, amount, status, callback_data支付流水表prescription_orderid, order_no, user_id, image, status, review_by, review_time处方审核主表operation_logid, admin_id, action, target, detail, ip, create_time后台操作日志3.2 药品SKU、批次和效期为什么必须一起设计药品和普通商品的另一个区别是效期。我们卖的药是有生产日期和保质期的入库时不能只填一个总库存数量。我在库存表里加入了batch_no、production_date、expiry_date三个字段一个药品可以有多条批次记录。这样做的好处是订单发货时可以“先进先出”——按效期从近到远发货快过期的先出避免积压到过期。后台还可以做一个效期预警比如距离过期小于30天的批次标红小于7天强制不允许上架销售。这个设计写进论文的“详细设计”章节比单纯写一个商品表有深度得多。面试或者答辩被问到“药品系统和你做过普通商城有什么不同”你就可以拿批次效期、处方审核这两点来回答。3.3 库存扣减方案我为什么选择‘下单锁库存支付后确认’库存超卖是电商系统的经典问题药品系统也一样会遇到。我在开发时对比过三种方案下单只校验不扣库存实现简单但用户支付时可能没货体验很差下单直接扣减库存不会超卖但用户不支付会占着库存导致其他用户买不到下单锁定库存、支付确认、超时释放用户体验好也不会真正扣减。我最终选的是第三种。具体逻辑是下单时把库存从available可用库存移到locked锁定库存支付成功后把locked清零并生成出库记录支付超时或订单取消时把locked加回available。这条逻辑最核心的代码是原子扣减一定要用UPDATE条件更新来避免并发超卖$affected Db::name(product_stock) -where(sku_id, $skuId) -where(quantity, , $quantity) -dec(quantity, $quantity) -inc(locked, $quantity) -update(); if ($affected 0) { throw new \RuntimeException(库存不足); }这里的关键是那条where(quantity, , $quantity)它在数据库层面就保证了库存不够时不会更新成功。只要所有库存操作都走这个带条件的更新语句并发再高也不会把库存扣成负数。3.4 订单状态机把订单流转画清楚订单状态我用了一个整数枚举来管理避免魔法字符串满天飞前端展示时再映射成中文状态值含义前置状态0待支付创建订单1待审方订单含处方药上传处方后2待发货支付成功或审方通过3已发货待发货状态下后台发货4已完成用户确认收货5已取消待支付/待审方状态下取消6退款中已支付后申请售后7已退款退款中审核完成状态迁移不是无条件跳转的比如“已支付”不能直接跳“已完成”必须先发货。我在代码里用一个status_config数组定义了允许的迁移路径每次更新订单状态时校验不合法就抛异常。这个设计在答辩时也是个亮点——你可以直接说“我用状态机约束了订单流转避免出现逻辑漏洞”。4. 下单到支付事务、库存锁定与回调幂等的代码实现4.1 创建订单时的事务边界一个订单的创建绝不只是往orders表插一条记录那么简单。它至少要完成四件事生成订单主表记录、写入订单明细快照、锁定库存、清空购物车。这四步必须在一个数据库事务里完成任何一个失败都要整体回滚否则就会出现“订单创建了但库存没锁住”或者“库存锁了但购物车没清空”的问题。我用 ThinkPHP 的事务方法包了整段逻辑Db::startTrans(); try { $orderNo $this-generateOrderNo(); $orderId $this-createOrderRecord($user, $address, $items, $orderNo); $this-lockStock($items); $this-clearCart($userId, $items); Db::commit(); return $orderNo; } catch (\Throwable $e) { Db::rollback(); Log::error(创建订单失败: . $e-getMessage()); throw new \RuntimeException(创建订单失败请稍后重试); }事务边界要记住一个原则事务里不要调用外部接口。我一开始在事务里发短信通知用户“订单已创建”结果短信接口超时导致整个下单流程回滚用户反复提交订单也成功不了。后来把短信通知挪到事务提交之后才解决这个问题。4.2 防止重复下单业务幂等键用户连续点击“提交订单”按钮前端虽然可以禁用按钮但不能完全信任前端。我的做法是后端生成一个一次性下单token前端在用户点击下单时带上这个token后端收到后先校验token是否存在、是否已被使用用过就直接拒绝重复请求。这个token我在Redis里存一份有效期5分钟使用后立即删除。即使同一个用户疯狂点击也只能成功创建一单。4.3 支付回调的验签和幂等最容易出bug的地方支付这一块我单独建了payment表每次发起支付时先生成一条支付流水记录订单号、金额、平台、支付流水号。回调通知进来时不是直接改订单状态而是要走几步校验验签。用平台公钥验证回调签名防止伪造通知查流水。根据out_trade_no在本地查找对应的支付流水记录幂等判断。如果这个流水已经处理过直接返回成功不再重复更新金额核对。回调金额必须和本地订单金额相等否则拒绝事务更新。把支付流水状态置为成功再更新订单状态、扣减锁定库存。核心的幂等判断代码大概是这样的$payRecord Db::name(payment)-where(pay_no, $payNo)-find(); if (!$payRecord) { return fail; // 本地没有这个流水不能处理 } if ($payRecord[status] 1) { return success; // 已处理过直接返回成功 } Db::startTrans(); try { Db::name(payment)-where(id, $payRecord[id]) -update([status 1, callback_data json_encode($callbackData), notify_time time()]); Db::name(orders)-where(order_no, $payRecord[order_no]) -update([status 2, pay_time time()]); Db::commit(); return success; } catch (\Throwable $e) { Db::rollback(); return fail; }注意回调接口务必在幂等分支上直接返回“成功”不然支付平台会因为你的接口一直报错而反复回调导致日志爆炸。4.4 超时未支付订单的释放策略用户下单锁了库存但一直不支付怎么办我在项目里做了一个定时任务每5分钟扫描一次待支付订单如果创建时间超过30分钟就把订单状态改成“已取消”同时执行库存释放操作。释放库存就是刚才锁库存的逆操作foreach ($items as $item) { Db::name(product_stock) -where(sku_id, $item[sku_id]) -inc(quantity, $item[quantity]) -dec(locked, $item[quantity]) -update(); }定时任务本身不复杂但要注意扫描条件一定包含status 待支付和create_time 半小时前而且每批处理量不要太大我每批次限制100单防止一次扫太多影响数据库性能。5. 安全防护防止越权下单和价格篡改的实践5.1 水平越权用户只能看到自己的订单很多新手做订单查询时直接写$orders Db::name(orders)-where(order_no, $input[order_no])-find();这样写有一个致命漏洞任何一个登录用户只要猜到别人的订单号就能查看别人的订单详情。正确做法是查询条件必须带上当前用户ID$order Db::name(orders) -where(id, $orderId) -where(user_id, $userId) -find();如果查不到就返回“订单不存在”而不是“无权访问”免得被攻击者爆破出敏感信息。这个点在论文的“系统安全”章节非常加分因为它是实际项目中极其常见又极其致命的问题。5.2 价格和金额永远以服务端计算为准用户提交订单时前端会传一个商品列表或者购物车ID列表后端必须根据这些ID去数据库重新查询价格、计算总金额绝不能直接信任前端传过来的total_amount。我的做法是后端拿到SKU ID和数量后统一从数据库读取商品现价、计算优惠、重新生成订单金额。用户如果改前端参数最终生成的订单金额还是服务端算出来的价格篡改自然失效。配合上签名参数更好但至少要做到重新计算这一步。如果系统未来要支持优惠券也要把优惠券发放在服务端校验不要接受前端传来的“已优惠金额”。5.3 SQL注入、XSS和CSRF的常规防护PHP项目最常见的还是SQL注入。框架的查询构造器本身就做了预处理但如果你在写代码时用拼接字符串风险就会回来。我的规矩是所有用户输入都不直接进入SQL条件查询必须走框架的查询构造器或者参数绑定后台搜索药品名、订单号时尤其要注意。XSS主要出现在用户提交的内容上比如收货地址的详细地址、管理员驳回处方的备注信息。这些内容在后台展示时一定要做HTML转义ThinkPHP的模板引擎本身就默认转义但我自己额外加了一道过滤函数凡是用户输入再回显的字段都过一遍htmlspecialchars。CSRF防护我用的是表单token机制。后台每个表单渲染时生成一个随机的CSRF token提交时校验校验不过就拒绝请求。这个token存到Session里可以在框架的全局配置里打开基本零成本。5.4 审计日志改价、改库存必须留下痕迹药品系统里价格和库存都是敏感数据出了问题要能查“是谁在什么时候改的”。我在后台的药品修改、库存调整、订单退款等接口都加了operation_log记录。日志内容包括操作人、操作类型、目标表、目标ID、修改前内容、修改后内容、IP和时间。这个功能看似简单但对运维和论文都极其有用。有一次线上订单金额不对我靠审计日志查到是某个管理员修改药品价格时写错了小数位十分钟就定位了问题。放到论文里“系统运行与维护”章节就有真实案例可以写。6. 论文落地从系统代码到答辩讲稿的转化要点6.1 论文目录结构与项目模块的映射如果你的题目是《基于PHP的在线药品销售系统的设计与实现》那论文目录基本可以跟着系统模块走第一章 绪论研究背景、国内外现状、研究内容第二章 相关技术介绍PHP、ThinkPHP、MySQL、Redis、Nginx第三章 系统需求分析功能性需求、非功能性需求、用例图第四章 系统概要设计架构图、功能模块划分、数据库设计第五章 系统详细设计权限模块、药品模块、购物车、订单、支付、审核的设计第六章 系统实现核心页面和核心代码讲解第七章 系统测试测试用例、测试结果、性能测试第八章 总结与展望这个结构和实际开发顺序是对应的。你在代码里写好模块论文每一个章节都能找到对应的素材。相比凭空编造这种“代码→文档”的映射方式写起来又快又不会前后矛盾。6.2 图表怎么画才加分论文图表是答辩评委最关注的也是最容易出彩的。我建议至少准备这五类图系统架构图分层展示浏览器、Nginx、PHP应用、Redis、MySQL、第三方支付接口功能结构图用树状图把用户端和后台端的功能展开二级功能列清楚E-R图把核心表之间的关系画出来重点画用户、药品、订单、处方单、库存的关系订单状态流程图把订单状态迁移画成流程图评委看到这个会觉得你理解很深时序图下单时用户、订单接口、库存接口、支付接口之间的交互顺序。画图不要追求花哨Visio或者Draw.io用最简单的框线和箭头就行。图表里的中文命名要和论文正文、代码注释保持一致我见过不少论文图表里写英文、正文写中文答辩时直接被问懵。6.3 测试数据和结果分析怎么写系统测试部分不要只写“功能正常性能良好”要给出真实数据和表格。我测试时用的是两套数据一套是手工构造的边界数据比如库存只有1件时并发下两单另一套是用脚本模拟的并发数据比如100个用户同时抢购某个药品。性能测试我用的方式是模拟100用户并发下单记录平均响应时间、数据库连接数、Redis命中率等指标最后整理成表格。不用真的压测到上千并发那对一个小型系统也不现实100并发加上“系统瓶颈分析与优化建议”这一段已经足够体现工作量。6.4 答辩高频问题自查清单答辩前我把自己的项目从头到尾过了一遍总结出评委最爱问的问题你照着自查一遍基本能稳住为什么选PHP不选Java答案框架项目规模、开发效率、部署成本、个人技术栈库存超卖怎么解决答案框架条件更新、锁库存、Redis保证原子性支付回调重复怎么办答案框架验签、查流水、幂等判断、事务更新订单超时未支付怎么处理答案框架定时任务扫描、关闭订单、释放库存处方药为什么不能直接下单答案框架合规审核、药师人工确认、状态锁定系统有哪些安全措施答案框架RBAC权限、水平越权防护、服务端金额计算、XSS注入过滤、操作日志系统的瓶颈在哪怎么优化答案框架数据库连接、慢查询、Redis缓存热点数据、分表分页优化。这些问题每一个都对应本文前面提到的一个具体实现你只要写过代码、看过日志回答起来自然就有底气。我自己做完这个项目最大的感受是药品销售系统听起来只是“商城换了个品类”但真正把处方审核、库存批次、订单状态机、支付幂等这些细节做进去之后它已经是一个完整的业务系统而不是教科书里的练习项目。写完代码再回头看论文你会发现每一章都不是凑字数因为每一段都有真实的设计决策支撑。如果你正在做类似的系统建议不要急着写代码先把业务状态流转画清楚把数据库表关系定下来这一步做好了后面全是水到渠成的事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →