尧图精选

从零开发农业种子商城:Node.js+Vue全栈实践与踩坑复盘

🕒 发布时间:2026/10/1 18:25:34 📁 来源:尧图网络
做农业类电商系统尤其是种子商城和普通卖衣服、卖数码的站点完全是两码事。我前阵子用 Node.js 和 Vue 框架从零开发了一套农业种子商城系统过程中边写边改从数据库表设计到订单扣库存、再到部署上线踩了不少坑。这篇博客就把这套系统的设计与开发过程完整复盘一遍包括技术选型、环境配置、核心模块实现和排障实录希望能给正在做类似商城项目的同学一点参考。1. 项目雏形先想清楚再做技术选型1.1 种子商城与普通电商的差异很多人觉得做个商城系统无非就是商品列表、购物车、订单套一个通用模板就行。等到真正动手做种子商城才发现里面的商品维度完全不一样。普通电商卖的是袜子、手机壳属性简单标题加价格就能卖。但农业种子是典型的非标品水稻、小麦、蔬菜、花卉每种作物都有自己的品种属性同一种作物还有生长周期、适宜季节、亩用量、发芽率、产地、保质期等一堆信息。用户在选购时不是只看价格和图片他需要知道这个种子适不适合当前季节种下去多久能收一亩地要用多少克。这些信息不展示清楚下单后就容易产生纠纷。所以在设计阶段我做了一个重要决定商品表不把所有属性都堆在字段里而是采用“分类表 商品主表 冗余属性字段”的组合。把季节、生长周期、亩用量这些高频筛选字段直接冗余在主表里查询效率高同时保留一个扩展 JSON 字段用来存品种特性、种植说明这类非结构化的信息。这样既照顾了普通电商的通用逻辑又满足了种子商品的特殊需求。另外种子商品还有一个特点是库存和批次强相关。同一款种子可能存在多个批次发芽率、入库时间不同。考虑到项目体量我没有单独做批次表而是用stock字段表示当前可售库存在商品详情里展示一个“本批种植建议”的说明文本。对于中小型种子商城这个粒度已经够用了。1.2 技术选型的取舍Node.js Vue 为什么合适技术选型没有绝对最优只有最合适。我当时选择 Node.js Vue主要基于三个层面的考虑。第一团队技术栈统一。前后端都用 JavaScript一个人也能全栈开发。不需要在 Java、Python 和前端之间切换思维上下文成本低很多。尤其是做个人项目、课程设计、中小团队内部系统这种统一性价值很高。第二Node.js 的生态足够成熟。Express 做 REST API 非常轻量配合mysql2、jsonwebtoken、multer、cors这些库几行代码就能把后端的核心链路搭起来。npm 上几乎能找到所有需要的功能包而且社区资料极其丰富遇到问题搜索一下基本都有现成答案。第三Vue 在国内的普及率和使用体验都很不错。组件化开发让页面逻辑清晰Vue Router 处理路由、Pinia 管理全局状态再配合 Element Plus 这样的组件库管理后台的表格、表单、弹窗都能快速搭建。我选择的是 Vue 3 而不是 Vue 2因为新项目没必要再走老路Vite 的冷启动速度和开发体验也确实比 Vue CLI 好很多。为什么没有用 SSR服务端渲染因为商城的前台页面主要是商品展示和下单流程对 SEO 要求不高用纯 SPA REST API 完全能胜任。引入 Nuxt 或者 Next.js 会显著增加项目复杂度对“快速上线、低成本维护”的目标来说不划算。1.3 前后端分离的目录结构怎么摆目录结构直接影响后续维护效率。项目一开始我就采用了前后端分离的 monorepo 风格把前端和后端放在同一个仓库的两个目录里方便统一管理。seed-shop/ ├── client/ # Vue 前端项目 │ ├── src/ │ │ ├── api/ # axios 接口封装 │ │ ├── components/ # 通用组件 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # Pinia 状态管理 │ │ ├── views/ # 页面 │ │ ├── utils/ # 工具函数 │ │ ├── App.vue │ │ └── main.js │ ├── vite.config.js │ └── package.json ├── server/ # Node.js 后端项目 │ ├── routes/ # 路由定义 │ ├── controllers/ # 控制器处理业务逻辑 │ ├── services/ # 服务层封装数据库操作 │ ├── models/ # 数据模型/SQL 语句 │ ├── middleware/ # 中间件鉴权、错误处理 │ ├── uploads/ # 本地上传目录 │ ├── app.js │ ├── package.json │ └── .env └── docs/ # 接口文档、SQL 脚本这种分层的好处是路由层只负责分发请求控制器负责参数校验和调用服务服务层专注 SQL 和事务。一旦出了 bug很快能定位是前端问题、接口问题还是数据库问题。需要说明的是我在开发初期没有引入过于复杂的 ORM 框架而是直接使用mysql2/promise写 SQL。原因很简单商城系统的表关系相对清晰直接写 SQL 更容易控制 index、事务和联表查询也方便排查性能问题。等需求复杂了再考虑引入 Sequelize 也不迟。2. 环境准备Node.js 安装和项目初始化2.1 Node.js 安装与 npm 环境变量配置很多新手一上来就栽在环境配置上。当时我下载的是 Node.js 18 LTS 版本Windows 安装包默认会勾选 “Add to PATH”但如果图省事一直点下一步还是有可能漏掉。装完打开终端执行node -v如果提示“不是内部或外部命令”说明 Node.js 没有写入系统的 PATH 环境变量。解决办法是手动配置环境变量右键“此电脑” → “属性” → “高级系统设置” → “环境变量”在系统变量的Path中新增 Node.js 的安装目录比如D:\Program Files\nodejs\。配置完成后重新打开终端再执行node -v和npm -v就能看到版本号了。这里有一个实际踩坑的地方Node.js 的默认安装路径如果带了空格比如D:\Program Files\nodejs在一些脚本工具里会解析异常。虽然现代 npm 大多数情况下能处理但我在使用某些 npm 包时依然遇到过因为路径空格导致的编译错误。所以个人经验是装 Node.js 时手动把安装目录改成D:\nodejs\或C:\software\nodejs\避免空格能省去很多莫名其妙的麻烦。2.2 解决 npm.ps1 无法加载文件的经典报错热词里那么多人在搜 “npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本”这个问题我在 Windows 环境下一开始也遇到了。完整报错信息类似这样npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。有关详细信息请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 的 about_Execution_Policies。这个问题的根源不是 npm 本身坏了而是 Windows PowerShell 的默认执行策略是Restricted禁止运行.ps1脚本文件npm 的命令行工具正好是通过npm.ps1这个脚本启动的。最简单的解决办法是以管理员身份打开 PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入Y确认。执行后可以用Get-ExecutionPolicy验证返回RemoteSigned表示已经放行。这样之后在 PowerShell 里就能正常敲npm命令了。如果不想修改系统的执行策略还有一个临时办法直接用cmd命令提示符窗口运行npmcmd 执行的是npm.cmd不涉及 PowerShell 脚本限制。或者手动调用npm.cmd install也能绕过这个报错。其实这个坑并不复杂但很有代表性很多 Windows 下的 Node.js 新手遇到报错第一反应是卸载重装实际上改一个策略就解决。后面我会再提一些 npm 相关的坑。2.3 用 Vite 快速创建 Vue3 项目前端项目我用了 Vite 而不是早期的 Vue CLI。Vite 基于 esbuild冷启动速度快热更新也快开发体验好太多。创建命令npm create vuelatest clientnpm create vue实际上是调用 create-vite 这个脚手架按提示选择需要的特性。我当时选择了 Vue Router 和 Pinia没有选 TypeScript。原因是我对这个项目的定位是快速上线、便于维护JS 动态类型在这种体量下足以应付而且团队成员对 TS 的熟练度也不够高强行上 TS 反而拖慢节奏。如果只是想要一个最基础的模板也可以直接npm create vitelatest client -- --template vue创建完成后cd client npm install npm run dev默认会在http://localhost:5173启动开发服务器。如果端口被占用可以在vite.config.js里修改server.portexport default defineConfig({ plugins: [vue()], server: { port: 5173, host: true } })host: true可以让局域网内其他设备访问方便手机测试。2.4 Express 后端骨架搭建后端我选的是 Express因为它足够简单、稳定完全符合一个商城后端的需求。初始化过程很直接mkdir server cd server npm init -y npm install express cors mysql2 jsonwebtoken multer dotenv其中cors用来解决跨域问题mysql2用来操作 MySQL 数据库jsonwebtoken负责签发和校验 JWTmulter处理文件上传dotenv读取.env配置文件。创建一个最基础的服务入口app.jsconst express require(express); const cors require(cors); require(dotenv).config(); const app express(); app.use(cors()); app.use(express.json()); app.get(/api/health, (req, res) { res.json({ ok: true, message: server is running }); }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(server running at http://localhost:${PORT}); });启动后访问http://localhost:3000/api/health能返回{ok:true}就说明后端跑通了。这里有个小建议express.json()一定要加不然req.body永远是undefined。另外开发时可以使用nodemon监听文件变化自动重启npm install -D nodemon然后在 package.json 的 scripts 里加上dev: nodemon app.js这样每次修改后端代码保存后服务会自动重启省去手动停服务的繁琐操作。3. 数据库设计与核心模块实现3.1 用户表、商品表、订单表怎样设计才不返工数据库设计是商城系统的地基表结构设计不好后面写业务逻辑会越写越痛苦。当时我花了一个晚上把核心表结构理清楚后面基本没有返工。用户表usersCREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role ENUM(user, admin) DEFAULT user, nickname VARCHAR(50), phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );分类表categoriesCREATE TABLE categories ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, parent_id INT DEFAULT 0, sort INT DEFAULT 0 );商品表productsCREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, unit VARCHAR(20) DEFAULT 袋, image_url VARCHAR(255), season VARCHAR(20) COMMENT 适宜季节, growth_cycle VARCHAR(50) COMMENT 生长周期, sowing_amount VARCHAR(50) COMMENT 亩用量, is_on_sale TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_season (season), INDEX idx_sale (is_on_sale) );购物车表cart_itemsCREATE TABLE cart_items ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) );订单表ordersCREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status VARCHAR(20) DEFAULT pending, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );订单项表order_itemsCREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100), price DECIMAL(10,2), quantity INT );订单的商品信息为什么要冗余product_name和price因为商品价格和名称可能会被后台修改但订单保存的应当是下单时的快照避免历史订单数据被商品变更影响。这是电商系统的一个基本设计原则。3.2 JWT 登录鉴权与后台权限区分用户认证这块我选择了 JWT因为它是无状态的适合前后端分离。注册时密码不能明文存到数据库使用bcryptjs做哈希处理const bcrypt require(bcryptjs); const hash bcrypt.hashSync(password, 10);登录时校验密码成功后用 jsonwebtoken 签发 tokenconst jwt require(jsonwebtoken); const token jwt.sign( { id: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: 7d } );后端需要一个统一鉴权中间件auth在每个受保护的路由前使用function auth(req, res, next) { const header req.headers.authorization; if (!header) return res.status(401).json({ message: 未登录 }); const token header.split( )[1]; try { req.user jwt.verify(token, process.env.JWT_SECRET); next(); } catch (e) { res.status(401).json({ message: token 无效或过期 }); } }针对管理后台接口还需要判断角色function adminAuth(req, res, next) { if (req.user.role ! admin) { return res.status(403).json({ message: 没有权限 }); } next(); }使用方式app.post(/api/admin/products, auth, adminAuth, productController.create);管理员的初始账号可以通过 SQL 脚本插入或者提供一个内置初始化接口。注意JWT_SECRET永远不要写死在代码里放在.env中并且使用足够长的随机字符串。3.3 种子商品 CRUD 与图片上传管理后台需要支持商品新增、编辑、删除、上下架。前端用 Element Plus 的表格和表单搞定后端就是对products表做 CRUD。商品图片上传是商城的一个必需功能。我用 multer 实现本地存储const multer require(multer); const path require(path); const storage multer.diskStorage({ destination: uploads/, filename: (req, file, cb) { const ext path.extname(file.originalname); cb(null, Date.now() - Math.random().toString(36).slice(2, 8) ext); } }); const upload multer({ storage }); app.post(/api/upload, auth, upload.single(file), (req, res) { if (!req.file) return res.status(400).json({ message: 请选择文件 }); res.json({ url: /uploads/ req.file.filename }); });文件名里加时间戳和随机数可以避免重名冲突。同时后端需要对上传文件做类型和大小限制防止用户上传超大文件或恶意脚本const upload multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter(req, file, cb) { if (!/\.(jpg|jpeg|png|webp)$/.test(file.originalname)) { return cb(new Error(仅支持图片文件)); } cb(null, true); } });还要在app.js中把 uploads 目录做成静态资源app.use(/uploads, express.static(uploads));前端用el-upload组件把action指到/api/upload拿到返回的 url 后保存到表单的image_url字段即可。3.4 购物车与订单事务扣库存的硬骨头购物车逻辑相对简单加购时先查一下该用户购物车中是否已有这个商品有则数量加一没有则插入新记录。结算时由前端把勾选的购物车项cartItemIds传给后端。订单模块是整个系统最核心也最容易出问题的地方。需要考虑两个问题一是订单数据一致性二是库存不能超卖。先说事务。一个订单会同时往orders表和order_items表插入数据并扣减商品库存。任何一个步骤失败数据都不能处于“半完成”状态。所以必须使用数据库事务。我用mysql2/promise的连接池执行事务const connection await pool.getConnection(); try { await connection.beginTransaction(); // 生成订单号 const orderNo SO Date.now() Math.floor(Math.random() * 1000); const [orderResult] await connection.execute( INSERT INTO orders (order_no, user_id, total_amount, status, receiver_name, receiver_phone, receiver_address) VALUES (?, ?, ?, ?, ?, ?, ?), [orderNo, userId, totalAmount, pending, name, phone, address] ); const orderId orderResult.insertId; for (const item of items) { await connection.execute( INSERT INTO order_items (order_id, product_id, product_name, price, quantity) VALUES (?, ?, ?, ?, ?), [orderId, item.productId, item.name, item.price, item.quantity] ); } await connection.commit(); res.json({ orderNo }); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); }再解决超卖问题。我见过不少初学者写先SELECT stock FROM products WHERE id ?在 Node 里判断stock quantity再执行UPDATE products SET stock stock - quantity。这在并发场景下是错的。两个请求同时读到stock 1都判断“够卖”随后都去更新最终库存会变成负数出现超卖。正确的做法是使用条件更新让数据库自己保证原子性const [result] await connection.execute( UPDATE products SET stock stock - ? WHERE id ? AND stock ?, [quantity, productId, quantity] ); if (result.affectedRows 0) { throw new Error(库存不足); }这条 SQL 的意思是只有当前库存大于等于要扣减的数量时才允许更新。如果影响行数为 0说明库存不够直接抛出异常让事务回滚。这种方式简单高效非常适合中小型商城的并发量。4. 前后端联调与部署上线4.1 联调前的 CORS 与代理配置开发环境最常见的问题是跨域。前端跑在5173端口后端跑在3000端口浏览器会拦截前端的 HTTP 请求除非后端返回了正确的 CORS 响应头。最省事的方式是在后端启用cors中间件const cors require(cors); app.use(cors());默认允许所有跨域请求开发阶段足够了。如果以后要限制域名可以改成app.use(cors({ origin: [http://localhost:5173, https://seed.example.com], credentials: true }));另一种更规范的方案是让前端在开发环境通过 Vite 代理转发请求。在vite.config.js中配置server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }这样前端代码里所有/api开头的请求都会代理到后端服务浏览器看到的请求是同源的根本不会触发跨域。我在项目中同时用了这两种方案主要是为了照顾“部分接口不走 Vite 代理”的调试场景。4.2 接口封装与 axios 拦截器一个商城系统有几十个接口如果每个页面都手动写axios.get再手动拼 token代码会非常冗余。我选择封装一个统一的请求模块。新建src/api/request.jsimport axios from axios; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; }); request.interceptors.response.use( response response.data, error { if (error.response) { const status error.response.status; if (status 401) { localStorage.removeItem(token); if (router.currentRoute.value.path ! /login) { router.push(/login); } } else if (status 403) { ElMessage.error(没有操作权限); } else { ElMessage.error(error.response.data?.message || 请求失败); } } else { ElMessage.error(网络异常请稍后重试); } return Promise.reject(error); } ); export default request;这样业务代码里只需写import request from /api/request; export function getProductList(params) { return request.get(/products, { params }); } export function addToCart(data) { return request.post(/cart, data); }统一拦截器的好处是后期要改 token 逻辑、加错误埋点都只需要改一处。需要注意 401 跳转时排除登录页否则登录接口自己返回 401 也会触发跳转造成死循环。4.3 用 PM2 Nginx 把项目跑起来开发完成后的部署环节我用了一台 Linux 服务器以 Ubuntu 为例。部署前先把项目上传到服务器然后安装生产依赖。后端使用 PM2 守护进程npm install -g pm2 pm2 start app.js --name seed-server pm2 save pm2 startuppm2 startup会生成一条开机自启命令保证服务器重启后服务自动拉起。查看日志使用pm2 logs seed-server前端先本地构建再把dist目录上传到服务器cd client npm run buildNginx 配置一个站点需要同时处理静态资源和 API 反向代理server { listen 80; server_name seed.example.com; root /var/www/seed-shop/client/dist; index index.html; location / { try_files $uri $uri/ /index.html; } 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 { proxy_pass http://127.0.0.1:3000; } }这里最容易被忽略的是try_files $uri $uri/ /index.html。Vue Router 默认使用 history 模式如果直接访问https://seed.example.com/products/3Nginx 会去磁盘找products/3这个文件找不到就返回 404。加上try_files后匹配不到文件就会回退到index.html由前端路由接管刷新页面就不会白屏。5. 复盘高频故障与避坑经验5.1 Node 版本与 npm 生态的坑Node 的版本管理是个容易被忽略的坑。我一开始图新鲜装了 Node 20结果某些老牌 npm 包编译不过去报各种node-gyp、node-sass错误。后来统一切到 Node 18 LTS此类问题少了很多。经验是新项目优先使用 LTS 版本主线版本虽然特性新但第三方包兼容性不一定跟上。另外Windows 用户强烈建议安装 nvm-windows 来管理多个 Node 版本。切换版本很简单nvm install 18.20.0 nvm use 18.20.0这样即使某个项目必须要用老版本也不需要反复卸载重装。npm 安装依赖慢是普遍痛点。国内直接把 registry 切到镜像源npm config set registry https://registry.npmmirror.com安装完可以查看npm config get registry如果遇到node_modules损坏表现为各种奇怪的模块找不到最稳妥的办法就是删掉重装rm -rf node_modules package-lock.json npm install不要试图修补依赖树一乱修修补补反而浪费时间。5.2 MySQL 8 连不上的常见原因我用的是 MySQL 8在后端连接数据库时遇到过两类问题。一类是密码认证插件不兼容。MySQL 8 的默认认证方式是caching_sha2_password一些旧客户端不认这个插件启动时报ER_NOT_SUPPORTED_AUTH_MODE。解决办法有两种要么给用户换成mysql_native_passwordALTER USER seed_userlocalhost IDENTIFIED WITH mysql_native_password BY yourpassword; FLUSH PRIVILEGES;要么在连接配置里加allowPublicKeyRetrieval: true。个人建议优先使用 mysql2 这个库它本身支持caching_sha2_password但如果你用的是老版本驱动遇到报错时可以直接改成mysql_native_password来快速解决。另一类是连接池配置不当。频繁创建数据库连接会让系统在高峰期直接卡死。我采用连接池const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0 });业务代码中每次从pool拿连接用完释放不要手工创建mysql.createConnection。5.3 并发订单导致的库存超卖超卖问题我在前面订单模块已经写了核心解法条件更新加事务。这里再补充几个实际排障时容易忽略的细节。第一一定要确认事务里的所有 SQL 使用同一个连接。很多人犯了低级错误在事务里调用自己封装的方法方法内部又另外从连接池获取连接导致事务里的commit根本控制不住另一条连接的 SQL。正确做法是统一把连接对象传下去或者把数据访问方法设计成“接收连接对象”的调用方式。第二要注意 MySQL 默认的事务隔离级别是REPEATABLE READ普通商城的扣库存场景其实不需要降低隔离级别直接依赖条件更新即可。如果使用了“先查后改”的代码即便开启事务也可能因为快照读导致判断不准。第三考虑幂等性。用户提交订单时可能因为网络原因重复点击“提交订单”后端会创建两个订单。建议在orders表用order_no唯一约束同时前端在按钮提交后立刻禁用按钮后端也可以用SELECT ... FOR UPDATE锁住用户尚未支付的订单记录但最简单的是在服务端生成一个短时唯一的clientToken来防止重复提交。5.4 Vue 打包部署后的 404 与刷新问题SPA 部署后刷新 404是所有 Vue 项目上生产前必踩的坑。原因是浏览器请求/products/3Nginx 去服务器磁盘找这个路径对应的文件找不到就返回 404。解决办法就是前面说的 Nginxtry_files。还有一个相关联的坑如果后端接口是以/api开头但前端恰好也有/api相关路由又配了 Nginx 反向代理请求会被错误转发到后端。让前后端路由彻底分开比如前端页面路由用/product后端统一用/api前缀就不会打架。另外构建后如果发现接口 404先检查 Nginx 的反向代理路径是否写错。比如location /api { proxy_pass http://127.0.0.1:3000; }当请求为/api/products时后端收到的路径仍然是/api/products所以后端路由必须以/api开头保持和代理前缀一致。如果想把/api前缀剥掉就要在proxy_pass末尾加斜杠proxy_pass http://127.0.0.1:3000/;这样/api/products会被转发成/products。项目里我保持了前后端都带/api前缀逻辑最简单不容易出错。部署问题还有一个容易被忽视的地方服务器防火墙。如果只开放了 80 端口没开放 443HTTPS 会失效如果安全组没放行 3306后端连不上数据库。上线之前用curl http://127.0.0.1:3000/api/health先测本机后端再通过公网地址测 Nginx能快速缩小排查范围。最后说一个个人的习惯项目上线后我会在 PM2 日志里保留console.log的请求关键信息比如用户 ID、订单号、错误堆栈。很多人觉得 console.log 影响性能而去掉但对中小型项目来说日志是排查问题最快的救命稻草。遇到用户反馈“下单未成功”看一眼pm2 logs seed-server就能定位到具体环节比用户自己截图描述靠谱得多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →