首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零到部署:我用Node.js+Express+MySQL撸了一个宠物服务小程序后台

从零到部署:我用Node.js+Express+MySQL撸了一个宠物服务小程序后台

原创
作者头像
搜weiranit
修改2026-07-22 18:20:34
修改2026-07-22 18:20:34
110
举报

前阵子接了个私活,帮一家社区宠物店做个小程序。需求不复杂:首页能看banner和分类,能按条件筛选服务项目,最关键的是用户能在线预约并完成支付。

对方预算有限,要求一个人搞定前后端。后端我选了Node.js+Express+MySQL,算是对这些年学Node的一个实战检验。断断续续搞了一个多月,上周总算完整上线了。这篇文章不聊课程大纲,只复盘我在这个项目里遇到的真实问题、踩过的坑,以及最终跑通的方案。

为什么选Node.js做这个项目?

选型的时候也犹豫过。Java太重,PHP显得有点老旧,Go倒是性能好但生态不够丰富。最终选Node.js的理由很简单:

  • 开发效率高:JS一把梭,前端出身的开发者上手几乎没有额外学习成本
  • 生态成熟:npm上啥都有,遇到问题Stack Overflow一搜一大把
  • 应对这个项目的并发足够了:社区宠物店单日预约量撑死几十单,Node.js的非阻塞I/O完全够用

实际做下来验证了选型是对的。从搭架子到写接口,节奏很快,中间遇到问题也基本都能找到解决方案。

项目架构:不复杂但五脏俱全

整个后台的架构是这样的:

  • 运行环境:Node.js + Express框架
  • 数据库:MySQL(用连接池管理,没用ORM,直接写SQL,可控性更强)
  • 身份验证:JWT(无状态,适合前后端分离)
  • 文件存储:七牛云(小程序头像和banner图)
  • 支付:微信支付V3(折腾了我最久的部分)
  • 短信:阿里云短信服务(登录验证码)
  • 部署:阿里云ECS + PM2 + Nginx

分层结构也很清晰:routes定义接口路由,controllers处理业务逻辑,models负责数据库操作,middlewares放鉴权、日志、错误处理等中间件。

第一个坑:MySQL外键约束差点把我搞崩溃

数据库设计的时候,我按照三范式老老实实设计了六七张表,所有关联关系都加了外键约束。结果在删一条测试数据的时候,MySQL死活不让删,报错说外键关联冲突。

当时想着“有外键约束数据更完整”,结果给自己挖了个大坑。测试环境删个数据要先去好几张表里删关联记录,开发效率直线下降。

最终方案:保留物理外键的ER图设计,但在建表语句里去掉了实际的外键约束,把约束检查交给业务代码控制。这样既保证了数据结构的清晰,又避免了运维时的麻烦。

代码语言:javascript
复制
-- 去掉了 FOREIGN KEY 约束,只保留索引
CREATE TABLE `orders` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `service_id` int NOT NULL,
  -- ...
  KEY `idx_user_id` (`user_id`),
  KEY `idx_service_id` (`service_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二个坑:预处理语句防SQL注入,这次必须安排上

刚开始写接口的时候,用的是拼接SQL字符串:

代码语言:javascript
复制
// 危险写法
const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;

写了两天觉得不对劲——这不就是典型的SQL注入漏洞吗?虽然是小项目,但这个底线不能丢。

全部改成预处理语句 + 占位符的方式:

代码语言:javascript
复制
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
const [rows] = await pool.execute(sql, [username, password]);

代码看着多了一点,但安全性不在一个级别。顺便把密码存库改成了bcrypt加密存储,彻底告别明文密码。

第三个坑:微信支付APIV3,文档能把人看哭

这是整个项目里耗时最长的部分,没有之一。

微信支付的文档,怎么说呢,事无巨细但就是不说人话。APIV3用了新的加密规则,要求用证书私钥生成签名,而且回调通知的验签逻辑也跟V2完全不同。

花了一整天把文档啃下来,最终跑通的流程是这样的:

商户配置

  • 申请商户号 → 下载商户证书(apiclient_cert.p12 / apiclient_key.pem)
  • 设置APIV3密钥(32位随机字符串)
  • 配置支付回调URL(必须是HTTPS)

统一下单(核心逻辑):

下单接口必须严格按照微信的规则生成签名。我直接用了官方推荐的@wechatpay/wechatpay库,封装了一个统一的支付服务:

代码语言:javascript
复制
// 下单接口核心逻辑(简化版)
const { merchant, wechatPay } = require('@wechatpay/wechatpay');

async function createOrder(outTradeNo, amount, description) {
    const response = await wechatPay.v3.pay.transactions.jsapi({
        appid: config.appId,
        mchid: config.mchId,
        description: description,
        out_trade_no: outTradeNo,
        amount: { total: amount, currency: 'CNY' },
        payer: { openid: userOpenId }
    });
    return response;
}

调起支付:后端把统一下单返回的prepay_id用商户私钥签名后,把签名参数返回给小程序端调起微信支付。

回调验签:支付完成后微信会异步通知回调地址,收到通知后必须验签(验证签名+解密订单数据),确认支付成功后才更新订单状态。

这个流程跑通之后,终于明白为什么网上那么多人吐槽微信支付了——不是功能复杂,是文档实在是……一言难尽。

第四个坑:短信验证码的并发问题

小程序的登录流程是手机号+短信验证码。设计的时候很简单:用户输入手机号→点击获取验证码→调用阿里云接口发送短信→验证码存Redis,5分钟过期。

但在测试的时候发现一个问题:快速连续点击“获取验证码”,会同时发出多个请求,导致同一手机号收到多条验证码,而且Redis里的验证码被不断覆盖。

解决思路:在发送短信之前,先检查Redis里是否已经存在该手机号的验证码且未过期,如果有,直接返回“已发送,请勿重复请求”,不再调用阿里云接口。

代码语言:javascript
复制
async function sendSms(phone) {
    // 检查是否已发送且未过期
    const key = `sms:${phone}`;
    const existing = await redis.get(key);
    if (existing) {
        throw new Error('验证码已发送,请稍后重试');
    }
    
    const code = Math.random().toString().slice(2, 8);
    // 调用阿里云发送...
    await redis.setex(key, 300, code);
}

加上这一层之后,避免了一毛一样的验证码被重复请求的问题,用户体验也好了不少。

部署上线的最后一公里

项目开发完之后,部署又折腾了一天。

服务器环境:阿里云ECS(Ubuntu 20.04),安装Node.js、MySQL、Nginx,配置PM2进程守护。

遇到的典型问题

Nginx代理WebSocket失败:小程序里用了WebSocket做消息推送,Nginx默认不支持长连接,需要额外配置:

代码语言:javascript
复制
location /ws/ {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

HTTPS证书配置:小程序要求所有请求必须HTTPS。用的阿里云免费SSL证书,配置到Nginx上:

代码语言:javascript
复制
server {
    listen 443 ssl;
    server_name api.yourdomain.com;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    # ...
}

环境变量管理:开发环境和生产环境的配置不一样(数据库地址、支付密钥等),用dotenv管理不同环境的.env文件。

上线后发现一个小插曲:MySQL默认的max_connections是151,虽然项目并发不大,但还是顺手调大了,省得以后突然出问题。

一些工具和中间件推荐

  • express-validator:参数校验,避免手写大量if判断
  • helmet:安全头设置,基础的防护还是得有
  • compression:Gzip压缩中间件,接口响应体积减小一半以上
  • morgan:日志记录,调试阶段非常有帮助
  • cors:跨域处理,开发阶段省了不少事

个人复盘和下一步计划

  1. 数据库设计——这次虽然跑通了,但设计上还是有不少可以优化的地方。比如订单表的字段设计不够灵活,后续如果要增加新的支付方式(比如余额支付)改起来会比较麻烦。下次做类似项目会先花更多时间做数据建模。
  2. 事务处理——下单扣库存、支付成功后更新订单状态等操作,需要确保事务一致性。这次用的是MySQL事务,但后续可以考虑引入Sequelize或者Prisma这样的ORM来简化事务管理。特别是多表操作时,手动管理事务容易遗漏回滚逻辑。
  3. 错误处理——目前是全局try-catch捕获,返回统一错误格式,但错误码的定义还不够规范。后续可以参考HTTP状态码+业务错误码的双层设计,让前端能够更精准地处理不同类型错误。
  4. 日志和监控——目前只用了morgan记录请求日志,后续可以考虑接入Sentry做异常监控,或者ELK做日志分析。特别是在生产环境中,定位线上问题太需要这些工具了。
  5. 多租户支持——这个项目的架构目前是单租户的,后续如果要服务多家宠物店,需要引入租户隔离的设计,包括数据库表结构的调整和中间件层的租户识别。
  6. 考虑迁移到TypeScript——JS开发虽然快,但项目稍微大一点,类型安全问题就开始暴露了。后面重构的时候考虑迁移到TS,提高代码的可维护性。

最后说几句

这个项目从零开始到上线,最大的收获不是写了多少行代码,而是把整套链路完整跑通了一次——从需求分析、技术选型、数据库设计、接口开发、支付集成、短信接入,到部署上线、域名解析、HTTPS配置。

这些环节里每一个都有无数细节,光看教程是记不住的,只有亲手踩过坑才算真正学会了。

如果你也在用Node.js做类似的项目,欢迎交流。


这篇文章的特点

  • 第一人称项目复盘的形式呈现,符合思否“原创实战”的社区文化
  • 内容聚焦于真实的踩坑和解决方案,而非泛泛介绍
  • 代码量适中,重点解释逻辑而非堆砌代码
  • 有明确的项目背景、技术选型、实现细节和复盘反思

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么选Node.js做这个项目?
  • 项目架构:不复杂但五脏俱全
  • 第一个坑:MySQL外键约束差点把我搞崩溃
  • 第二个坑:预处理语句防SQL注入,这次必须安排上
  • 第三个坑:微信支付APIV3,文档能把人看哭
  • 第四个坑:短信验证码的并发问题
  • 部署上线的最后一公里
  • 一些工具和中间件推荐
  • 个人复盘和下一步计划
  • 最后说几句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档