
去年接了一个医药客户的定制项目需求方一开始就给了七个字网上药店商城进销存管理系统。我当时第一反应是这不就是个带库存的电商系统嘛拿个现成的电商框架改一改就行了。等真正开始做业务梳理我才发现自己想得太简单了药品不是普通商品从采购入库那一天起批号、效期、GSP合规记录就天天跟在流程后面处方药还得考虑在线问诊和审方这些都决定了这个系统的数据模型跟普通商城完全不是一个设计思路。这套项目最终采用Vue3做前端、Python做后端前台商城和后台进销存共用一套核心库存数据前后端加起来跑了大半年日常维护基本稳定我把整个设计和落地过程中的经验整理出来给后面打算做同类系统的人一个参考。这套系统做成什么样了呢往大了说它是一个B2C网上药店用户可以浏览药品、加购物车、下单支付往实际业务看它又是一套完整的进销存后台采购、入库、出库、盘点、效期预警、销售统计全部串联。前台每成交一单后台库存实时变化后台每进一批药前台可售数量同步更新。适合想从零搭建商城进销存一体化系统的开发者参考也适合医药行业信息化人员拿来做业务蓝图。1. 药品生意为什么要把商城和进销存绑在一起1.1 网上药店比普通电商多出来的三座大山普通电商做一套商城核心就三件事商品、订单、库存。库存直接用SKU数量来管卖一个减一个非常简单。但网上药店不一样给业务建模时你马上会撞上三座大山。第一座是批号管理。药品和衣服、手机最大的区别是它有批号同一款药、同一个SKU可能同时存在五个不同批号的货每一批又对应不同的供货商、不同的生产日期、不同的质检报告。消费者看到的只是一个商品链接但后台发货时必须知道出的是哪一批否则一旦出现质量问题召回你根本查不到流向。第二座是效期管理。药品有严格的效期临近效期的药不能正常卖甚至有些药过了效期必须报废。这个效期不是一个备注字段而是一条硬性的业务规则。销售时原则上要效期先到先出FEFO仓库里的药不能像普通商品那样随便发。系统必须能算出哪些药临近效期、哪些已经过期并在前台商城里对临期药品做拦截或提示。我对客户的一句原话印象特别深普通商品卖不掉是压资金药品卖不掉是压资金加压合规风险。这句话后来直接决定了我给这个系统加了多少跟效期、批号相关的功能模块。第三座是GSP合规。药品经营质量管理规范要求企业记录每一批药品的来源、验收、储存、销售去向形成完整的追溯链条。普通商城的订单明细表在这里远远不够你得有采购记录、验收记录、上架记录、销售记录、退换货记录并且每条记录都要能关联到批号。这一块做好了系统是加分项做不好以后合规检查就是灾难。1.2 我对项目模块的理解与边界划分搞清楚了业务约束模块划分其实就顺理成章了。这套系统我拆成两大端四个域商城端面向消费者包含用户认证、药品展示、购物车、下单支付、订单查询几个模块。这里面的难点不是页面多炫而是所有展示给用户的库存和价格都必须来源于后台进销存的真实数据不能像普通商城那样在商品表里存一个假库存随便改。后台端面向药房员工和管理者包含商品管理、采购管理、入库管理、出库/销售管理、库存管理、效期预警、盘点管理、报表统计、会员管理。一件药品从供应商报价开始到采购单审核、到货验收、入库、上架商城、下单出库、最后形成销售统计整条链路在两个端之间来回流转。我特别强调一个边界原则商城端的任何操作都不允许直接修改库存主数据。商城端通过接口读库存、锁定库存、请求扣减库存但真正的数量变更只发生在后端服务里并且每一步变更都要求写一条流水。这样做的好处是就算商城前端出了Bug导致多下单后台数据仍是可信的排查问题时有据可查。1.3 技术选型Vue3 Python这套组合的取舍为什么前端选Vue3原因很朴素这个项目需要大量中后台页面Vue3的Composition API更适合把复杂业务逻辑拆成一个个可复用的组合式函数TypeScript支持也成熟商品、订单、库存这些有明确结构的数据类型定义清晰后能少一大半低级错误。加上Element Plus在后台管理界面的组件覆盖度高省去了大量造轮子的时间。后端选Python我在这套系统里用了FastAPI而不是Django或者Flask这个选择后面单独讲。但整体来说Python在药店这种业务规则复杂、报表需求频繁变化的项目里开发效率是真高。组合方案适合场景我的判断Vue3 FastAPI前后端分离、异步接口多、追求开发效率本次选用Vue3 Django需要现成的Admin后台、用户权限体系也可以但Django自带Admin基本没法直接用在这个领域Vue3 Flask接口极少、只想快速跑通页面业务深入后会很累Flask得自己装太多东西这套组合唯一的代价是前后端两种语言并存团队需要同时具备前端生态和Python生态的知识。但就网上药店商城进销存管理系统这类业务而言开发速度和小步迭代的价值远大于这个代价。2. Vue3端在落地中的几个关键决策2.1 用Composition API组织业务逻辑而不是把代码堆在setup里很多Vue3教程会把所有逻辑都塞进一个setup函数里我刚开始也是这么干的等到商品列表页写了十几个reactive数组、五六个函数的时候页面已经乱到没法维护了。后来我改成按业务领域拆HookuseProductList管商品列表和搜索useCart管购物车useStockAlert管库存预警useOrderSubmit管下单流程。拆完之后商品管理页的setup变得非常薄const { list, total, params, loading, fetchList, handleDelete } useProductList() const { updateSkuStatus } useProductSku()每个Hook内部自己管状态、自己调接口、自己暴露方法页面只做组合和模板绑定。这样做的好处是采购入库单和销售出库单都涉及选择药品这个交互但我只需要在不同的页面里复用useProductSelector这个Hook药品搜索、分页、选中逻辑完全一致不用改第二遍。这里我想强调Composition API的价值不是让你写更少的代码而是让你把相同业务的代码聚在一起。选项式API时代data和methods隔着十万八千里想改一个业务逻辑要来回滚动屏幕组合式函数直接把这段业务相关的状态、计算属性、方法放一起这才是它真正的意义。2.2 computed计算属性在库存预警和订单金额计算中的实际用法Vue3里computed是出镜率最高的API之一但很多人只拿它做简单的加法。在这个项目里我用计算属性处理了两类特别典型的场景。第一类是订单金额的实时计算。购物车里的商品可能调整数量、可能勾选不同药房的配送费规则、还有满减活动这些叠加起来如果每次都在模板里写表达式模板会膨胀到没法看。我把它收敛成一个computedconst cartTotal computed(() { const goodsAmount cartItems.value.reduce( (sum, item) sum item.price * item.qty, 0 ) const discount calcPromotion(goodsAmount) const deliveryFee goodsAmount freeShippingThreshold ? 0 : 8 return { goodsAmount, discount, deliveryFee, payable: goodsAmount - discount deliveryFee } })第二类是库存预警。后台首页的待办卡片上要显示临期药品和低库存药品两个列表如果每次进入页面都重新请求接口既慢又浪费后端资源。我选择在前端把当前页面的库存数据拉回来之后用computed做客户端实时过滤配合5分钟一次的刷新体验非常顺滑。不过要提醒一件我在项目里踩过的小事computed默认是只读的如果商品数量增减逻辑里想用cartTotal反推每个商品的最新行小计那是方向错了。行小计应该由每个商品自己的计算属性负责购物车合计永远只做加法汇总。这个边界想清楚计算层就不会越搅越乱。2.3 动态路由和按钮级权限后台菜单不是写死的网上药店进销存后台的账号分好几类角色店长、采购、仓管、收银、财务。不同角色登录后看到的菜单和按钮完全不一样。最开始我图省事在前端维护了一个静态路由表每个路由的meta里写死roles登录后再用v-if控制整个菜单栏。结果第一个客户现场就出了幺蛾子采购经理居然能看到销售出库单的作废按钮。原因是前端只能控制显示不能控制路由是否存在——技术较强的员工直接在地址栏敲路径还是能进入权限之外的页面。后面我改成了真正的前端动态路由方案登录后拿到当前用户的权限标识用router.addRoute()按角色动态挂载路由同时注册全局前置守卫router.beforeEach((to, from, next) { if (!userStore.token) return next(/login) if (userStore.routesLoaded) return next() // 根据用户权限生成动态路由并addRoute buildDynamicRoutes(userStore.permissions) userStore.routesLoaded true next({ ...to, replace: true }) })按钮级权限我在项目里用了一个自定义指令v-permission没有权限的元素直接被移除。实测下来这套组合能挡掉八成以上的越权操作剩下的两层靠后端接口鉴权兜底。2.4 Element Plus表单联动校验和tabs样式修改的小笔记药品管理的表单校验比普通商品复杂。比如采购入库单录入时选择了一个SKU后批号是必填的但普通商品SKU不需要批号选择了处方药分类后必须勾选需要审方选项否则不能上架。Element Plus的规则是声明式的遇到这种根据其他字段决定当前字段是否必填的场景我在validator里拿到整个表单的值再动态判断const rules { batchNo: [{ validator: (rule, value, callback) { if (form.skuType drug !value) { callback(new Error(药品必填批号)) } else { callback() } }, trigger: blur }] }另外提一个样式上的小坑。后台首页用了若干tabs切换当日订单、待补货清单、临期药品客户觉得默认的Tabs样式太普通要求把激活标签改成圆角胶囊。Element Plus的样式是带scoped的直接写在组件里不生效必须用:deep()改动子组件内部类名:deep(.el-tabs__item.is-active) { background: var(--el-color-primary); color: #fff; border-radius: 16px; }热搜词里有vue3修改tabs标签页样式估计大家也遇到这个问题这里把答案写清楚记住一条规则在scoped样式范围内改第三方组件内部结构一律用:deep()把选择器渗透进去配合!important处理优先级冲突。3. Python后端的接口设计与库存数据模型3.1 为什么选择FastAPI而不是Django或Flask网上药店这个业务对接口的诉求有几个特点接口多、字段校验严格、并发集中在热销药品的扣库存操作、后续肯定要接小程序和App。基于这些我在Python后端里选择了FastAPI。FastAPI的几个特性在这个项目中是实打实节省了时间的。首先Pydantic的模型校验让我不用在每个接口里手写参数判断——比如SKU编码格式、批号的日期格式、采购价的非负校验都只要在Schema上声明约束就行前端传错数据直接返回422。其次FastAPI原生支持异步热销药品的浏览接口从同步改成async之后并发能力提升明显同时使用协程处理那些需要同时查库存、查价格、查促销的聚合接口非常自然。最后是自动生成的OpenAPI文档。跟客户对接联调的时候我把FastAPI自动生成的/docs页面直接发给对方他们自己就能看接口契约省了大量口头沟通成本。3.2 库存表的数据结构批号是药品进销存的命根子先看商品侧spu表存药品通用信息sku表存具体的规格包装比如阿莫西林胶囊 0.25g*24粒/盒是一个SKU。跟普通商城一样这两张表负责前台展示。关键在库存侧。我没有像普通电商那样在sku表里放一个stock_qty当总数而是设计了四级sku绑定多个stock_batch库存批次表每个批次记录批号、生产日期、效期、入库数量、剩余数量stock_batch的每次数量变化都写进stock_flow库存流水表可售库存总数由批次汇总计算得出不落冗余字段。表名核心字段作用spu药品名称、批准文号、OTC类型、通用名商品主数据skuspu_id、规格、单位、条码、零售价具体销售单元stock_batchsku_id、batch_no、生产日期、效期、qty_remaining批次化库存stock_flowbatch_id、change_type、change_qty、关联单号库存变动流水purchase_order供应商、采购总金额、状态采购单主表sale_order用户、订单金额、状态销售订单主表批号一旦绑定到库存批次后续所有采购、销售、盘点、调拨都围绕batch_id展开。前台用户选中一个SKU后系统查询该SKU下所有剩余数量大于0且未过效期的批次把剩余数量加总成可售库存。这个设计虽然查询时多一步汇总但换来了批号维度的完全可控我认为非常值得。3.3 一个入库接口里事务和行锁是怎么配合的库存不是把数字加一减一那么简单。拿采购入库接口举例前端提交的是一张入库单明细里面包含SKU、批号、数量、生产日期、有效期。后端要做的事包括校验采购单状态、校验药品信息、写入库存批次、写库存流水、更新采购单状态每一步都不能出错。我强烈建议库存变动接口统一走事务行锁流水三件套。事务保证要么全部成功要么全部回滚行锁防止两个并发入库请求同时修改同一个库存批次流水保证解锁后可以追溯。router.post(/purchase/inbound) async def inbound(payload: InboundPayload, session: AsyncSession Depends(get_db)): async with session.begin(): # 锁定采购单防止重复入库 po (await session.execute( select(PurchaseOrder).where(PurchaseOrder.id payload.po_id) .with_for_update() )).scalar_one() if po.status ! APPROVED: raise HTTPException(400, 采购单不在可入库状态) for item in payload.items: batch (await session.execute( select(StockBatch) .where(StockBatch.sku_id item.sku_id, StockBatch.batch_no item.batch_no) .with_for_update() )).scalar_one_or_none() if batch is None: batch StockBatch(sku_iditem.sku_id, batch_noitem.batch_no, production_dateitem.production_date, expiry_dateitem.expiry_date, qty_remainingitem.qty) session.add(batch) else: batch.qty_remaining item.qty session.add(StockFlow(batch_idbatch.id, change_typeINBOUND, change_qtyitem.qty, ref_nopo.order_no)) po.status INBOUNDED这里最容易被忽略的是with_for_update()。刚开始我以为事务能解决并发问题结果测试时开两个进程同时提交同一张采购单的入库两张单都成功了库存加了两次。加了行锁之后第二个事务只能等第一个事务提交再读到采购单状态已变成INBOUNDED直接拒绝入库。这个锁是库存系统的命门不管入库还是出库凡涉及数量变更查询涉及的对象都要考虑加锁。4. 进销存核心流程从采购到销售钱货移动怎么管住4.1 采购入库状态流比想象中复杂采购流程我一开始做了三步建单、审核、入库。后来客户说不行药品采购还有到货验收环节——到货了仓库要清点数量、核对批号、检查外包装是否完好验收不合格的拒收部分还得单独记录。所以采购单状态最终增加到了六个草稿、待审核、已审核、待验收、已完成、已作废。每个状态之间的流转不是自由的。已作废的单子不能重新激活待验收状态下可以录入实际到货数量和拒收原因验收完成才能触发入库。在系统里我通过状态机和接口粒度把规则控制得很死只有已审核的采购单允许填写验收记录只有待验收的采购单允许调用入库接口校验状态的同时后端会比对采购明细和实际入库数量超收数量超过5%直接拒绝入库。4.2 销售出库预占、扣减、取消释放商城下单跟后台出库是同一个业务流程的两端。用户在前台提交订单后我不会立刻扣减库存总数而是先预占库存——把当前SKU下可用的批次数量锁定。预占期间其他用户看到的可售库存已经减掉了这部分避免超卖。用户支付成功后预占变成正式扣减如果订单超时未支付或用户取消预占释放。订单状态库存操作说明待支付预占库存锁定批次可售数减已支付扣减预占生成出库单库存从锁定变已出已取消释放预占可售数恢复售后完成原路退回对应批次校验效期后重新入可售这块业务里最容易翻车的是一个订单包含多个SKU每个SKU涉及多个批次预占时要按效期先到先出原则优先锁定早过期的那批。我用一个简单的循环处理remaining item.qty for batch in available_batches: # 按过期时间升序 if remaining 0: break lock_qty min(remaining, batch.qty_remaining) create_stock_lock(order_item_id, batch.id, lock_qty) remaining - lock_qty if remaining 0: raise HTTPException(400, 库存不足)这个逻辑保证了同一批次药品不会在整个订单中间被拆没也让售后回退时能确定是回退到哪个批号。4.3 效期预警和滞销分析药店老板每天睁开眼想看的数据系统上线后药房老板每天先看两个报表效期预警和滞销排行。效期预警的SQL思路很直接——找出所有剩余数量大于0、且计算出的效期剩余天数小于预警阈值的批次SELECT sku.name, stock_batch.batch_no, stock_batch.qty_remaining, DATEDIFF(stock_batch.expiry_date, CURDATE()) AS remain_days FROM stock_batch JOIN sku ON sku.id stock_batch.sku_id WHERE stock_batch.qty_remaining 0 AND stock_batch.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 90 DAY) ORDER BY remain_days ASC;为什么这个报表重要因为药品不像衣服衣服过季还能清仓药过了效期就是纯损失还得承担销毁成本。系统里我还加了一个开关低于30天效期的药品前台商品详情页直接显示临期药品标签低于15天的禁止正常销售。滞销分析则用库存流转流水来统计——一张SKU如果连续三个月没有出库流水或者库存周转天数为负数后台首页会出现滞销预警卡片提醒运营尽快做买赠活动或者退回供应商。5. 部署到服务器时遇到的那些实际问题5.1 nginx配置Vue3项目的正确姿势前端项目打包后是纯静态文件我用nginx做静态服务器和反向代理。第一个坑是路由模式。Vue3项目如果用了createWebHistory()刷新某个子路径时nginx会按路径去找真实文件找不到就404必须配置try_files把请求都转发到index.htmlserver { listen 80; server_name your-domain.com; root /var/www/pharmacy-admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location /assets/ { expires 30d; add_header Cache-Control public, immutable; } }gzip压缩也别忘了开Vue3打包出来的vendor文件经常超过1MB不压缩首屏加载能慢3秒以上。另外客户用Edge浏览器时偶尔反馈系统里按钮点了没反应之类玄学问题多数是浏览器缓存顽固assets目录启用上面这段永久缓存后配合打包文件名hash基本能解决。5.2 后端进程、Python环境与数据库的日常维护Python后端的部署我用了这么一套服务器上建独立的虚拟环境用pip install -r requirements.txt安装依赖用uvicorn作为ASGI服务器跑FastAPI最后用systemd托管进程保证服务器重启后服务自动拉起[Unit] Descriptionpharmacy-fastapi Afternetwork.target [Service] Userwww-data WorkingDirectory/opt/pharmacy-backend ExecStart/opt/pharmacy-backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 4 Restartalways [Install] WantedBymulti-user.target这里有一个项目初期踩过的坑venv没建直接把依赖装进系统Python里后来系统升级Python版本把依赖搞坏了后台全挂。现在我的原则是一个项目一个虚拟环境不管什么语言隔离是第一原则。数据库每天凌晨自动备份备份文件保留最近30天药品业务的数据千万别省这个钱。5.3 一次线上重复出库事故让我补了一个联合唯一索引项目上线第二周客服反馈说一张订单被仓库发了两遍货。我排查后发现是出库接口高并发下出了漏洞两个仓库管理员同时处理同一订单的出库操作后端没有对订单明细做锁校验结果同一条销售明细生成了两条出库记录库存也扣了两次。修复方案分两层。第一层在业务代码里出库前先查询订单状态只有已支付且未出库的订单才能出库查出后立即with_for_update()锁住订单行。第二层在数据库层面给stock_flow表加了联合唯一索引(ref_no, ref_item_id, change_type)确保同一条业务单据的同一个明细同一种变动类型只能有一条流水。加完这个索引后就算代码层万一漏了判断数据库也会在第二次写入时抛唯一约束错误不至于让数据静默错下去。这套代码防业务错、数据库防数据错的双保险我现在做任何进销存项目都会沿用。6. 系统跑起来之后还可以往这几个方向扩展6.1 GSP合规留痕和处方药流程对接目前的系统已经记录了出入库流水但严格来说GSP合规还要求更多留痕内容比如供应商资质证照、药品检验报告、冷链运输温度记录。这些数据建议扩展成资质档案模块跟采购单和库存批次做关联效期快到期时同步提醒资质续期。处方药这块如果要做B2C必须对接有资质的互联网医院进行在线问诊和电子处方开方处方笺本身也要电子留档。这些是政策层面的硬要求技术实现上主要是跟第三方平台对接接口建议尽早预留回调接口的数据表。6.2 扫码、批号标签和PDA手边作业仓库操作员如果对着电脑录出入库单效率低还容易录错。我在迭代计划里加入了扫码方案采购验收时用扫码枪扫描包装上的药品条码系统自动填充SKU和批号出库分拣时扫订单条码自动跳过无库存批次。有条件的仓库直接上PDAVue3的移动端适配加上WebSocket实时同步仓库员工不用坐在电脑前也能收发货出入库效率至少提升一倍。6.3 自动补货建议与多门店调拨进销存做到后面最有价值的不是记账功能而是数据洞察。把每个SKU的历史销售数据、当前库存、采购在途、效期天数全部跑一遍就能生成建议采购量——效期紧的先打折清销量高的自动提醒补货。多门店场景下还需要设计库存调拨单把总仓的货调给缺货门店同时记录收货方、发货方两个维度的库存流水这个跟采购入库是一个套路数据模型已经支持。最后再分享一点我个人的体会。这套系统前后端加起来上万行代码真正难的地方从来不是哪个表格或者接口而是数据模型跟业务规则的映射。药品的批号、效期、GSP记录开发时觉得多此一举等真遇到临期药压仓、批号追溯查不到的时候你才会发现当初多花的那两天设计时间有多值钱。做进销存系统把进、销、存、流水的底子打好商城界面做得朴素一点都不丢人。后面不管是接处方平台、上PDA、做自动补货底层数据不变扩展起来都很顺。