1. 网上药店商城到底在做什么先想清楚再动手老实说刚拿到nodevue网上药店购物药品商城管理系统这个需求时我一度以为是又一套普通的电商Demo——商品列表、购物车、下单、支付换层皮而已。但真正梳理完业务需求后发现药品电商和普通商品电商有本质区别药品牵扯到处方药与非处方药的区分、库存批次管理、效期预警、药师审核、购买数量限制、物流冷链要求等一系列特殊规则。也就是说这不该简单套用仿天猫那套模板而是要在电商通用能力之上叠加医药行业的专属逻辑。这套系统的核心组成从技术栈上讲是三条线Node.js 后端服务负责接口鉴权、药品数据维护、购物车与订单状态机、库存批次与效期计算、药师审核流程。Vue 前端管理端给药店运营人员用的后台包含商品上下架、订单处理、用户管理、库存预警看板。Vue 前台商城端给普通用户浏览选购的界面核心是药品分类检索、药品详情含处方药购买流程、购物车、订单确认与支付对接。我第一次做这个项目时踩的最大的坑就是一开始没有划分前后台两套界面结果所有功能揉在一个项目里路由混乱、权限控制形同虚设、处方药和非处方药逻辑混在一起改。后来重构时我把项目拆成admin-web管理端和client-web商城端两个 Vue 工程再配上 Node 后端统一提供服务瞬间清爽很多。如果你也要做这套系统我建议你第一周先把下面这张架构图文字版刻在脑子里client-web(Vue)───┐ ├── Node.js API Server ── MySQL admin-web(Vue)────┘ │ ├── Redis(缓存/会话) └── 文件存储(药品图片/电子处方)下面我按环境准备 → 后端设计 → 用户端实现 → 管理端实现 → 部署运维的顺序把我实际整理过的完整开发流程和踩坑记录摊开讲。这份内容偏向实操每一步都能直接照着敲。2. 环境准备Node、Vue、数据库这“三大件”怎么搭配不折腾2.1 Node 环境安装版本选对后面少哭十次Node 的安装本身不复杂但版本问题能把你折磨到怀疑人生。我先说结论做这个项目不要一上来就用最新的 Node 大版本建议用长期维护版LTS比如 18.x 或 20.x。为什么这么强调因为后面你要安装的依赖比如node-sass、node-gyp编译的原生模块或者某些老牌 UI 组件库对 Node 版本敏感。我见过太多人装了 Node 22结果跑 Vue 2 项目时一编译就报Error: Node Sass does not yet support your current environment只能灰溜溜卸掉重来。安装步骤我用最简单的方式整理在下面Windows/macOS 通用思路去 Node 官网下载 LTS 版本安装包或者用包管理器安装。Windows 推荐直接下载.msimacOS 推荐brew install node20。安装完成后打开终端验证node -v、npm -v能打印版本号就说明装好了。设置 npm 国内镜像源如果你访问官方源慢npm config set registry https://registry.npmmirror.com注意如果你需要同一台机器上多个 Node 版本切换建议装一个 nvmNode Version Manager。但有个坑Windows 的 nvm 和 macOS/Linux 的 nvm 不是同一个工具Windows 下叫nvm-windows安装后要在管理员权限的终端里执行nvm install 20、nvm use 20。我实际经验是如果只是为了这个项目直接固定用一个 LTS 版本最省心。nvm 适合你同时维护多个老项目的时候用新手阶段没必要给自己加复杂度。2.2 Vue 环境搭建用 Vite 还是 Vue CLI现在 2025 年了新项目我建议直接用 Vite 脚手架。Vue CLI基于 webpack虽然成熟但对于药店商城这种中后台 商城双端项目Vite 的启动速度和热更新体验会好非常多。创建项目命令如下npm create vitelatest client-web -- --template vue npm create vitelatest admin-web -- --template vue这里有个细节Vite 默认生成的是 Vue 3 项目。如果你比较熟悉 Vue 2 的 Options API想用 Vue 2那需要额外配置插件我个人不建议在这个新项目里刻意用旧版。Vue 3 的组合式 APIComposition API写业务逻辑更清晰特别是管理端那种大量列表筛选、表单联动的场景代码组织起来比 Vue 2 舒服得多。装完后把 Vue Router 和 Pinia状态管理也一并装上npm install vue-router4 pinia这两个库分别是路由管理区分管理员页面和用户页面和全局状态管理管理登录态、购物车数据。在我实际开发中前端工程基本就是这三件套Vue 3 Vue Router 4 Pinia。2.3 MySQL 与 Redis数据库选型背后的理由商品、订单、用户这类结构化数据我的选择是 MySQL 8.x。原因很直接项目生态成熟、事务支持可靠而且药品库存批次和效期这种需要一致性约束的数据用关系型数据库的事务才能保证不出错比如并发下单时扣库存不能超卖。Redis 在这个项目里的角色是缓存和会话。药品分类、首页推荐位这类不经常变动的数据可以缓存到 Redis减轻 MySQL 的压力登录态也可以放 Redis配合 JWT 做双 token 机制后面细说。本地搭建时如果你不想装完整 MySQL可以偷懒用 Dockerdocker run --name pharmacy-mysql -e MYSQL_ROOT_PASSWORDroot123 -p 3306:3306 -d mysql:8 docker run --name pharmacy-redis -p 6379:6379 -d redis:7不过要提醒你这只是本地开发用的方案。生产环境一定要给 MySQL 配置持久化卷、定期备份、主从分离Redis 也要设置密码和持久化策略这些后面在部署章节我会展开。3. 数据库设计药品商品表、订单表、库存批次表怎么关联数据库设计是整个系统最见功底的一步。很多新手直接照抄普通电商的表结构把药品当成普通商品来设计结果做到处方药流程时发现字段不够用改表改到崩溃。我的建议是一开始就按下面的表结构来设计。3.1 药品表核心中的核心普通电商商品表一般就是商品名称、价格、库存、图片、描述。但药品不同我实际用到的关键字段如下CREATE TABLE drug_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_name VARCHAR(100) NOT NULL COMMENT 药品通用名, trade_name VARCHAR(100) COMMENT 商品名/品牌名, category_id BIGINT COMMENT 分类ID, specification VARCHAR(50) COMMENT 规格如0.25g*24片, dosage_form VARCHAR(20) COMMENT 剂型片剂/胶囊/口服液等, manufacturer VARCHAR(100) COMMENT 生产厂家, approval_number VARCHAR(50) COMMENT 药品批准文号国药准字, prescription_type TINYINT COMMENT 0-OTC非处方 1-处方药, price DECIMAL(10,2) COMMENT 销售价格, stock_total INT COMMENT 总库存冗余字段实际以批次为准, description TEXT COMMENT 药品说明书/适应症等, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME, updated_at DATETIME ) COMMENT 药品商品表;这里面的prescription_type字段极其关键前台商城端要根据它来决定走普通购买流程还是上传处方/药师审核流程。approval_number批准文号也是药品区别于普通商品的重要标识建议做个唯一索引防止重复录入。3.2 库存批次表与效期管理药品库存不能像普通商品那样只记一个总数因为同一种药可能分属不同进货批次每个批次的效期不同。电商系统里常见的需求是下单时优先扣最早过期的批次先进先出FIFO并且在效期前多少天自动预警。CREATE TABLE drug_stock_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drug_id BIGINT NOT NULL, batch_no VARCHAR(50) COMMENT 批次号, expire_date DATE COMMENT 效期, quantity INT COMMENT 当前批次剩余数量, created_at DATETIME ) COMMENT 库存批次表;有了批次表扣库存的逻辑就清晰了。下单时不是直接UPDATE drug_product SET stock_total stock_total - 1而是查出该药品所有未过期批次按expire_date升序排序。依次扣减批次数量直到满足购买数量。扣减成功后再更新药品总库存。这个逻辑用事务包起来既能防止超卖又能自动实现效期优先出库。3.3 订单表与订单明细表订单拆成主表和明细表这是标准设计。但药品订单有一个独特需求处方药订单要记录审核状态。我通常会在订单主表上加这些字段CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE COMMENT 订单号, user_id BIGINT, total_amount DECIMAL(10,2), status TINYINT COMMENT 状态机待付款/待审核/待发货/已发货/已完成/已取消, audit_status TINYINT DEFAULT 0 COMMENT 处方审核状态0无需审核 1待审核 2通过 3驳回, prescription_image VARCHAR(255) COMMENT 处方图片URL处方药必填, receive_name VARCHAR(50), receive_phone VARCHAR(20), receive_address VARCHAR(255), created_at DATETIME, updated_at DATETIME ) COMMENT 订单主表; CREATE TABLE order_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT, drug_id BIGINT, drug_name VARCHAR(100), specification VARCHAR(50), price DECIMAL(10,2), quantity INT, batch_no VARCHAR(50) COMMENT 记录实际扣减的批次 ) COMMENT 订单明细表;这里我多说一句order_items里记录batch_no看起来是冗余但非常有用。后续如果要做药品召回、效期追溯可以直接通过订单明细追踪到具体批次这种细节在医药场景里是加分项。3.4 购物车表记住一条原则购物车不要只存药品 ID 和数量要把价格、规格、是否处方药、上下架状态快照都存一份。因为用户加入购物车后过几天再下单商品可能已涨价或者下架如果没有快照下单时不光要重新算价还容易因为上下架状态的变动导致各种逻辑冲突。我在实际开发里购物车表字段大致是CREATE TABLE cart_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, drug_id BIGINT, drug_name VARCHAR(100), specification VARCHAR(50), price DECIMAL(10,2) COMMENT 加入时的价格快照, quantity INT, is_prescription TINYINT COMMENT 是否处方药, checked TINYINT DEFAULT 1 COMMENT 是否勾选, created_at DATETIME ) COMMENT 购物车表;这样做的好处是下单时直接基于购物车快照生成订单前端展示永远和结算价格一致用户不会在支付页发现价格不对而困惑。4. Node后端接口设计从登录鉴权到下单减库存的完整链路后端我用的是 Express 框架也可以用 Koa 或 NestJS但 Express 上手快生态最全。整个后端的模块划分大致如下server/ ├── app.js # 入口 ├── config/ # 数据库、Redis、JWT等配置 ├── routes/ # 路由定义 ├── controllers/ # 控制器处理请求参数和响应 ├── services/ # 业务逻辑层事务、库存计算等 ├── models/ # 数据模型/数据库查询 ├── middlewares/ # 鉴权、错误处理、参数校验 └── utils/ # 响应格式化、日期工具等4.1 用户登录与双 Token 机制药品商城涉及处方药下单用户身份验证比普通商城更敏感。我使用的方案是 JWT Redis 双 tokenAccess Token短期有效比如 2 小时放在前端内存里每次请求带上。Refresh Token长期有效比如 7 天存到 Redis用于自动续期。登录接口流程// routes/auth.js router.post(/login, async (req, res) { const { username, password } req.body; // 1. 校验用户名密码 const user await User.findOne({ username }); if (!user || !bcrypt.compareSync(password, user.password)) { return res.fail(用户名或密码错误); } // 2. 生成accessToken和refreshToken const accessToken jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: 2h }); const refreshToken uuid.v4(); await redis.set(refresh:${refreshToken}, user.id, EX, 7 * 24 * 3600); // 3. 返回给前端 res.success({ accessToken, refreshToken, userInfo: { id: user.id, username: user.username, role: user.role } }); });刷新 token 的接口就简单多了前端发现 access token 过期后axios 拦截器判断 401把 refreshToken 发过来后端从 Redis 查到对应用户重新签发 accessToken。提示管理端和用户端共用一个登录接口通过role字段区分。管理员账号的 role 为admin普通用户为user。鉴权中间件里判断 role不满足的直接返回 403。4.2 药品列表与搜索Vue前端最依赖的接口前台商城的核心接口大概有这几个GET /api/drugs?categoryIdkeywordpagepageSizesort列表 搜索 分页。GET /api/drugs/:id药品详情包含说明书、库存状态、是否处方药。GET /api/categories药品分类树比如感冒用药肠胃用药维生素/矿物质等。我在写这个接口时的思考点是不要把 SQL 查询逻辑散落在 controller 里。搜索条件较多时我习惯用一个查询条件对象来组装// services/drugService.js async function getDrugList({ categoryId, keyword, page 1, pageSize 10, sortBy default }) { const where { status: 1 }; // 只查上架的 if (categoryId) where.categoryId categoryId; if (keyword) { where[Op.or] [ { drugName: { [Op.like]: %${keyword}% } }, { tradeName: { [Op.like]: %${keyword}% } }, { approvalNumber: { [Op.like]: %${keyword}% } } ]; } // 排序逻辑 let order [[createdAt, DESC]]; if (sortBy price_asc) order [[price, ASC]]; if (sortBy price_desc) order [[price, DESC]]; // 分页 const { count, rows } await Drug.findAndCountAll({ where, order, offset: (page - 1) * pageSize, limit: pageSize }); return { total: count, list: rows }; }这个接口设计对前端非常友好前端只管传keyword、categoryId、page这些参数不需要关心 SQL 细节。4.3 下单与库存扣减事务里最不能出错的一段下单接口是整个后端最核心、最容易出 bug 的部分。一个完整的下单流程我拆成以下步骤校验用户是否登录。解析购物车勾选列表或直接从前端接收购买清单。遍历药品校验上下架状态、是否处方药处方药必须携带审核通过的标识才能下单。计算总金额。开启数据库事务。扣减库存批次按效期升序扣更新总库存。如果扣减失败库存不足回滚事务提示库存不足。生成订单主表和订单明细表记录。清空购物车中已下单的项。提交事务返回订单号。我在 Express 里的代码结构大概是这样的// services/orderService.js const sequelize require(../config/db); async function createOrder(userId, items, receiveInfo) { return sequelize.transaction(async (t) { let totalAmount 0; const orderItems []; for (const item of items) { const drug await Drug.findByPk(item.drugId, { transaction: t, lock: t.LOCK.UPDATE }); if (!drug || drug.status ! 1) throw new ApiError(药品已下架, 400); if (drug.prescriptionType 1 !item.auditPassed) { throw new ApiError(处方药需先完成处方审核, 400); } // 扣减库存批次逻辑省略这里核心是锁行防止超卖 await deductStock(drug.id, item.quantity, t); totalAmount drug.price * item.quantity; orderItems.push({ ... }); } const order await Order.create({ ... }, { transaction: t }); await OrderItem.bulkCreate(orderItems, { transaction: t }); return order; }); }注意我用了lock: t.LOCK.UPDATE也就是行级锁。这是为了防止两个用户同时抢最后一件药品时出现超卖。没有这行锁高并发下就可能出现库存变负数的情况。药品行业对库存准确性要求很高这一步不能省。4.4 处方药审核流程这个行业的独有环节如果是 OTC非处方药用户下单后直接付款发货。但处方药必须由药师审核处方。我在项目里实现的流程是用户在提交订单时上传处方图片或填写处方信息。订单状态变为待审核status 待审核audit_status 1。管理端订单列表里出现处方审核入口药师/管理员点击查看处方图片。审核通过audit_status 2订单自动进入待发货驳回audit_status 3用户可以修改处方重新提交或者直接取消订单退款。这个流程并不复杂但要注意一点监管要求的合规性。在实际商用场景里可能还需要对接电子处方平台、留存审核日志。我在这个项目中至少做了审核日志表记录审核人、审核时间、审核结果、驳回原因避免事后扯皮。5. Vue前端实现商城端与管理端的核心页面开发细节5.1 商城端的药品展示与分类检索用户端首页我一般放这几个区块搜索栏、分类导航、轮播推荐位、热销药品列表。技术实现上没什么高难度的关键要处理好的点是分类树加载从后端拿一次性拉取分类树缓存起来。因为分类变动频率低放 Pinia 里全局共享就行。药品卡片组件展示药品名、规格、价格、是否处方药标识、库存紧张标识。药品卡片点击进入详情页。搜索防抖实时搜索框需要做 300ms 防抖避免每次输入都发请求。这个坑我踩过不防抖时数据库毫无压力但前端会卡顿。药品详情页有几个特殊点要讲。第一处方药要明显提示本品为处方药需凭处方购买。第二显示生产厂家、批准文号、效期如果是展示最近批次情况这能让用户产生信任感。第三购买数量选择器要限制最大可购数量不能超过当前库存也不能超过平台对单次购买数量的限制比如有些含麻黄碱类药品限购 2 盒。5.2 购物车与结算从勾选到提交订单的前端交互购物车页面我采用的是表格 复选框布局每一行展示药品信息、单价、数量调节器和小计。这里最容易出问题的联动逻辑是全选/取消全选。修改数量后重新计算总价。删除某个条目。失效药品已下架置灰但不能删除用户记录。我把购物车状态放在 Pinia 里统一管理因为结算页、购物车角标、订单确认页都需要共享这些数据。结算流程前端的关键代码如下简化版// stores/cart.js import { defineStore } from pinia; export const useCartStore defineStore(cart, { state: () ({ items: [], }), getters: { checkedItems: (state) state.items.filter(item item.checked item.status 1), totalPrice: (state) { return state.items .filter(item item.checked item.status 1) .reduce((sum, item) sum item.price * item.quantity, 0); } }, actions: { async fetchCart() { const res await api.getCart(); this.items res.data; }, async addToCart(drug, quantity) { await api.addToCart({ drugId: drug.id, quantity }); await this.fetchCart(); } } });结算页有个细节提交前要重新计算金额并让用户确认而不是直接拿本地购物车的数字去下单。因为本地数据可能已经过期比如后端价格调整了。所以提交订单时前端把items发给后端后端重新计算金额返回给前端展示前端确认后再提交。5.3 管理端药品管理、订单处理、库存预警可视化管理端我使用了现在比较流行的中后台布局左侧菜单 顶部面包屑 右侧内容区。菜单项大概是仪表盘、药品管理、分类管理、订单管理、用户管理、库存预警、审核日志。药品管理是管端最核心的页面功能包括药品列表分页 筛选新增药品表单包含前文说的所有字段编辑药品价格、上下架状态批量上下架。这里我建议把药品表单抽成独立组件新增和编辑复用同一个组件减少代码重复。订单管理页面主要做三件事查看所有订单支持状态筛选待付款/待审核/待发货/已发货/已完成/已取消。针对处方药订单点击审核处方打开弹窗查看订单详情和处方图片通过/驳回。对已审核订单点击发货填写物流单号。库存预警页是药品管理系统最有价值的一块。我在后端写了定时任务node-cron每天扫描drug_stock_batch表把满足以下条件的批次找出来expire_date NOW() 90天三个月内到期或quantity low_stock_threshold低库存阈值。结果写到一张stock_warning表管理端直接查这张表展示。这个功能看似简单但在真实药房场景里非常救命很多门店就是靠它避免出现过期药。5.4 路由权限控制admin和user页面分离由于商城端和管理端是两个独立的 Vue 工程路由权限管理比较简单商城端不做复杂的动态权限只需要在支付、购物车、订单页面加是否登录的前置守卫。管理端则需要根据登录用户的role判断是否能访问。如果用户不是管理员强制跳回用户端首页。路由守卫的代码我放在 Vue Router 的beforeEach里router.beforeEach((to, from, next) { const authStore useAuthStore(); if (to.meta.requiresAuth !authStore.isLoggedIn) { next({ path: /login, query: { redirect: to.fullPath } }); return; } if (to.meta.requiresAdmin authStore.userInfo?.role ! admin) { next({ path: /, message: 无权访问 }); return; } next(); });两个独立工程的好处是管理端路由不用和用户端路由混在一起打包体积也小。缺点是要维护两套登录逻辑但实际做了以后发现利大于弊。6. 联调与问题排查我实际踩过的7个高频坑做这类全栈项目前后端联调阶段总是最耗时的。下面这几个坑是我在实际开发中反复遇到、网上资料又不容易找到明确答案的问题统一记录在这里。6.1 跨域问题前端端口与后端端口不一致前端开发服务器跑在5173Vite 默认后端跑在3000直接请求必然跨域。解决方案我推荐在后端加 CORS 中间件npm install corsconst cors require(cors); app.use(cors({ origin: [http://localhost:5173, http://localhost:5174], credentials: true }));同时前端 Vite 的server.proxy也可以配一层代理把/api代理到http://localhost:3000这样开发环境可以少处理 CORS 的很多奇奇怪怪的问题比如携带 cookie 的跨域请求。6.2 时间格式时区问题订单时间显示差8小时这是一个非常隐蔽的坑。MySQL 默认使用系统时区Node 层如果没有配置timezone查询出来的日期会比北京时间少 8 小时。解决办法是在建立 Sequelize 连接时指定new Sequelize(database, username, password, { host: localhost, dialect: mysql, timezone: 08:00 });前端展示时建议再用 dayjs 格式化一下避免用户在订单列表里看到下单时间 1970-01-01这种灵异事件。6.3 npm 依赖版本冲突Vue 3 项目安装 Vue 2 组件库这是新手最容易犯的错。比如搜到某个 UI 组件库的安装命令没注意它只支持 Vue 2装完后页面空白、控制台报错 Cannot read properties of undefined。我的建议是Vue 3 项目优先用 Element Plus而不是 Element UI、或者 Ant Design Vue 3.x、或者 Naive UI。装之前看一眼文档首页的大版本标识比啥都强。6.4 药品图片上传路径问题开发环境能显示部署后失效图片上传接口返回的路径如果是绝对路径如http://localhost:3000/uploads/xxx.jpg部署到服务器后就歇菜。正确做法是后端返回相对路径/uploads/xxx.jpg前端在请求时动态拼接当前 API 域名。我在前端封装了一个getFileUrl方法export function getFileUrl(path) { if (!path) return ; if (/^https?:\/\//.test(path)) return path; return ${import.meta.env.VITE_API_BASE_URL}${path}; }6.5 处方图片回显失败静态资源被 Express 路由拦截这个坑真的很典型。你配置了app.use(/uploads, express.static(uploads))但放在app.use(/, router)之后结果所有/uploads/xxx.jpg请求都被路由拦截了返回 404。解决方法是把静态资源配置放在所有路由之前。6.6 购物车数量与库存不同步前端限制失效有些用户会开两个标签页一个页面把库存加到购物车另一个页面已经下单把库存买完了。这时候你提交订单时后端必须再次校验库存而不能信任前端传来的数量。我为此在后端订单接口里又做了一次完整的库存校验宁可多写几行代码也不让用户的购物车显示有货但下单提示库存不足。6.7 管理端打包后接口地址失效前端打包成静态文件后接口地址默认是相对路径/api如果 API 和前端不在同一个域名就会 404。我习惯在.env.production里配VITE_API_BASE_URLhttps://你的域名/api并且所有 axios 请求基准地址都从环境变量读取。这个配置记得在部署前就写好别等上线了才改。7. 项目部署上线从本地开发到 Linux 服务器的完整流程这个项目我在本地 Mac 上开发完最终部署到了腾讯云轻量服务器Ubuntu 22.042核4G 配置跑这套系统完全够用。部署思路主要分三块前端静态资源用 Nginx 承载后端 Node 服务用 PM2 守护数据库和 Redis 可以直接装系统包。7.1 后端部署PM2 守护 Node 服务先在服务器上安装 Node 20方法跟本地一样然后把后端代码传到服务器执行npm install --production启动服务我强烈建议用 PM2因为直接node app.js一旦终端断开服务就停了。这在热搜词里也频繁出现通过ssh连接服务器断开以后node服务会停其实是没用进程守护工具。正确操作npm install -g pm2 pm2 start app.js --name pharmacy-server pm2 save pm2 startuppm2 startup可以让服务器重启后自动拉起 Node 服务。做完这三步之后你再也不怕 SSH 断开导致服务挂掉。7.2 前端部署Nginx 托管静态文件前端两个工程分别执行npm run build得到dist目录上传到服务器然后配置 Nginx 站点server { listen 80; server_name your-domain.com; # 用户端商城 location / { alias /var/www/client-web/; try_files $uri $uri/ /index.html; } # 管理端 location /admin { alias /var/www/admin-web/; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传文件访问 location /uploads { alias /var/www/pharmacy-server/uploads/; } }这里有一个要注意的点try_files $uri $uri/ /index.html是 Vue Router 的 history 模式必需配置不配的话你访问https://域名/order/detail/123这种二级路由会 404。如果你用的是 hash 模式URL 带#倒是不需要这行但我不推荐 hash丑且不利于 SEO。7.3 环境变量与 HTTPS 配置部署后一定要做的一步是改环境变量。开发时数据库密码、JWT 密钥都是明文写在配置文件里上生产必须改成强密钥并放到.env文件里统一管理。JWT 密钥如果泄露别人就能伪造管理员 token后果很严重。域名解析做好后我建议顺手把 HTTPS 配上。现在用 Let‘s Encrypt 很方便一条命令就能申请证书apt install certbot python3-certbot-nginx certbot --nginx -d your-domain.com配好 HTTPS 之后用户在前台提交处方图片、在管理端审核订单的过程就都是加密传输了。药品电商涉及用户健康隐私这一步别省。8. 功能上线后的运营维护与优化方向系统能跑起来只是第一步。上线后我最关心的三个指标是接口响应速度、库存准确率、用户复购率。针对前两点我做了下面这些优化8.1 接口性能优化Node 后端默认情况下每个接口都是直连 MySQL请求量上来后响应会明显变慢。我的优化顺序是先加 Redis 缓存热点数据再优化 SQL 索引最后才考虑加机器。具体到本项目药品分类树、首页推荐位、热门药品列表这类读多写少的数据缓存到 RedisTTL 设置为 1 小时。药品列表接口的categoryId、keyword查询字段都加了合适的索引。MySQL 的索引不是越多越好但这两个高频查询字段必须建。订单列表接口做了分页并强制按created_at倒序避免深分页时全表扫描。8.2 库存准确率保障我在前面反复提到库存扣减用事务 行锁确保高并发下不出错。但还有一个容易被忽略的运行期问题退款和取消订单时要回补库存。我在取消订单的接口里实现了把订单明细中的批次数量加回到对应drug_stock_batch表的逻辑。如果忘记回补用户取消订单后库存就白白少了长期跑下来会对账不平。8.3 可扩展方向这个项目做到底我觉得是有足够扩展空间的。比如后续可以加药品浏览历史与智能推荐根据用户历史购买记录推荐关联药品。电子处方流转对接对接医院系统实现在线问诊后直接开方购药这会大幅提升处方药购买体验。定时效期提醒推送不仅管理端预警还可以在用户端提醒您购买的药品将于X月过期这种细节对用户信任度提升非常明显。WebSocket 实时通知药师审核结果出来时前端商城可以实时弹窗通知用户避免用户反复刷新订单列表。9. 我最后想分享的几点真实体会如果你准备照着这篇文章去开发一套 nodevue 的网上药店系统有几个来自实践、而非教科书的心得我想单独留到最后说。第一不要被商城系统这三个字骗了它比外卖系统、图书商城复杂在业务规则上。真正难的永远不是技术而是对医药场景的理解处方药不能随便卖、效期库存要先进先出、购买数量要受限、用户隐私要保护。把这些业务规则吃透了代码反而顺理成章。第二开发顺序建议是先数据库、再后端接口、再前端页面。不要一上手就开前端因为前端页面的每一个交互按钮背后都对应着接口接口没定好前端白做。我一般是先画好接口文档哪怕用 Markdown 手写前端再照着联调效率最高。第三Node 后端项目尽量保持模块清晰。很多朋友喜欢把所有 controller 都写到 app.js 里项目一跑起来乱成一锅粥。我见过一个项目路由文件 3 万行最后没人敢改。宁可前期多建几个目录也不要让自己三个月后读不懂自己的代码。第四如果在部署环节遇到奇怪问题比如 PM2 里运行正常但服务器重启后就失效第一反应应该去查 PM2 的启动项有没有配置成功第二查 Nginx 的错误日志/var/log/nginx/error.log不要急着重装系统。日志会告诉你 99% 的真相。网上药店管理系统说白了是一个垂直领域电商系统通用技术栈 行业规则 独特价值。把 Node 和 Vue 吃透把药品的库存、审核、效期玩明白这套系统就能真正落地而不是停留在课程设计的 Demo 层面。希望这篇记录能给你节省一些从零起步的摸索时间。