前阵子接了个私活,帮一家社区宠物店做个小程序。需求不复杂:首页能看banner和分类,能按条件筛选服务项目,最关键的是用户能在线预约并完成支付。
对方预算有限,要求一个人搞定前后端。后端我选了Node.js+Express+MySQL,算是对这些年学Node的一个实战检验。断断续续搞了一个多月,上周总算完整上线了。这篇文章不聊课程大纲,只复盘我在这个项目里遇到的真实问题、踩过的坑,以及最终跑通的方案。
选型的时候也犹豫过。Java太重,PHP显得有点老旧,Go倒是性能好但生态不够丰富。最终选Node.js的理由很简单:
实际做下来验证了选型是对的。从搭架子到写接口,节奏很快,中间遇到问题也基本都能找到解决方案。
整个后台的架构是这样的:
分层结构也很清晰:routes定义接口路由,controllers处理业务逻辑,models负责数据库操作,middlewares放鉴权、日志、错误处理等中间件。
数据库设计的时候,我按照三范式老老实实设计了六七张表,所有关联关系都加了外键约束。结果在删一条测试数据的时候,MySQL死活不让删,报错说外键关联冲突。
当时想着“有外键约束数据更完整”,结果给自己挖了个大坑。测试环境删个数据要先去好几张表里删关联记录,开发效率直线下降。
最终方案:保留物理外键的ER图设计,但在建表语句里去掉了实际的外键约束,把约束检查交给业务代码控制。这样既保证了数据结构的清晰,又避免了运维时的麻烦。
-- 去掉了 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字符串:
// 危险写法
const sql = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;写了两天觉得不对劲——这不就是典型的SQL注入漏洞吗?虽然是小项目,但这个底线不能丢。
全部改成预处理语句 + 占位符的方式:
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
const [rows] = await pool.execute(sql, [username, password]);代码看着多了一点,但安全性不在一个级别。顺便把密码存库改成了bcrypt加密存储,彻底告别明文密码。
这是整个项目里耗时最长的部分,没有之一。
微信支付的文档,怎么说呢,事无巨细但就是不说人话。APIV3用了新的加密规则,要求用证书私钥生成签名,而且回调通知的验签逻辑也跟V2完全不同。
花了一整天把文档啃下来,最终跑通的流程是这样的:
商户配置:
统一下单(核心逻辑):
下单接口必须严格按照微信的规则生成签名。我直接用了官方推荐的@wechatpay/wechatpay库,封装了一个统一的支付服务:
// 下单接口核心逻辑(简化版)
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里是否已经存在该手机号的验证码且未过期,如果有,直接返回“已发送,请勿重复请求”,不再调用阿里云接口。
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默认不支持长连接,需要额外配置:
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上:
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,虽然项目并发不大,但还是顺手调大了,省得以后突然出问题。
这个项目从零开始到上线,最大的收获不是写了多少行代码,而是把整套链路完整跑通了一次——从需求分析、技术选型、数据库设计、接口开发、支付集成、短信接入,到部署上线、域名解析、HTTPS配置。
这些环节里每一个都有无数细节,光看教程是记不住的,只有亲手踩过坑才算真正学会了。
如果你也在用Node.js做类似的项目,欢迎交流。
这篇文章的特点:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。