ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Node.js与微信小程序的书籍销售系统开发实战

2026/9/15 6:19:30 拓冰建站 浏览量
基于Node.js与微信小程序的书籍销售系统开发实战 简介基于Node.js的微信小程序书籍销售系统是一份可直接运行和二次开发的全套毕业设计资源适合正在做小程序相关课设、毕设或想学习前后端联调的开发者。系统覆盖用户端完整购书流程包括书籍分类浏览、多条件搜索、购物车结算、多种支付方式、订单状态与历史记录查询、个人信息与收货地址管理、评价收藏以及微信消息通知等模块功能结构清晰。压缩包内共1178个文件约19.66MB核心类型有206个js逻辑文件、136个vue页面组件、80个wxml和82个wxss界面样式、93个json配置以及sql数据库脚本、说明文档和论文文档便于对照前后端实现方式。已有103人学习下载。除源代码外还附带数据库结构、使用说明、论文以及install.bat、run.bat等辅助脚本目录按前端、后端和文档清晰分层可快速本地启动也适合在此基础上做功能扩展。1. 书籍销售系统的技术选型为什么是 nodejs 微信小程序拿到《基于nodejs实现的微信小程序的书籍销售系统》这套交付物时你面对的其实不只是一个能跑的小程序而是一个标准的前端小程序 后端接口 数据库 文档四件套。这类项目在课程设计和毕业设计里非常多见核心诉求有两个代码能跑通、答辩能讲清。技术选型上选 Node.js 而不是 Java/PHP原因很直接——微信小程序端本身是 JavaScript 写的后端再用 Node.js前后端语言栈统一JSON 数据从接口到页面几乎不需要做任何格式转换调试链路最短。而拉取的这套源码大概率由 Express 搭建 REST API、MySQL 或 MongoDB 存数据、原生小程序或 uniapp 做前端组成。本文就顺着这条链路把环境配置、接口设计、数据库表结构、前后端联调和部署排错完整走一遍。2. 后端初始化nodejs 安装与环境配置、Express 项目结构与路由设计2.1 nodejs 安装与 npm 环境的最小可用配置先把运行环境拉齐。Node.js 的安装本身没有难度装 LTS 版本即可但真正的坑在 npm 全局环境上。国内网络环境下npm install 装依赖时经常卡在 socket 超时所以我的习惯是装完 Node.js 第一件事就切镜像源node -v npm -v npm config set registry https://registry.npmmirror.com npm get registry第一条命令确认 Node 版本第二条确认 npm 可用第三条把 registry 切到国内镜像最后一条验证是否生效。这里有一个容易踩的点很多源码包自带的 package.json 里依赖版本都很老npm install 跑出来一堆 deprecated 警告是正常的只要不出现 ERR! 级别的报错就可以继续。如果安装中途报 ERESOLVE 错误通常是依赖树冲突在项目目录下改用npm install --legacy-peer-deps就能绕过去。2.2 Express 项目结构与 REST API 路由设计这类书籍销售系统的后端目录结构业界几乎已经形成了固定范式。我一般会按启动入口、路由、控制器、数据库操作四层拆分源码包里的结构大同小异bookstore-server/ ├── app.js # Express 实例注册中间件和路由 ├── bin/ │ └── www # 启动脚本监听端口默认 3000 ├── routes/ │ ├── users.js # 用户注册、登录、信息查询 │ ├── books.js # 书籍列表、详情、分类查询 │ ├── cart.js # 购物车增删改查 │ └── orders.js # 下单、订单列表、订单状态变更 ├── db/ │ └── index.js # mysql2 连接池 └── package.json入口文件 app.js 的写法这几年的变化不大核心是注册 JSON 解析和跨域中间件:const express require(express); const cors require(cors); const bodyParser require(body-parser); const userRouter require(./routes/users); const bookRouter require(./routes/books); const app express(); app.use(cors()); // 允许小程序端跨域请求 app.use(bodyParser.json()); // 解析 JSON 请求体 app.use(/api/users, userRouter); // 用户模块路由 app.use(/api/books, bookRouter); // 书籍模块路由 module.exports app;这里有个经常被忽略的小细节bodyParser.json()一定要在路由注册之前调用否则 POST 请求里的req.body永远是 undefined。cors()在开发阶段可以无脑全开但部署到线上时应该收窄成具体的域名白名单。路由文件里则统一处理业务逻辑以书籍列表接口为例router.get(/list, async (req, res) { const { page 1, pageSize 10, category } req.query; const offset (page - 1) * pageSize; let sql SELECT id, title, author, price, cover, stock FROM books; const params []; if (category) { sql WHERE category ?; params.push(category); } sql LIMIT ? OFFSET ?; params.push(Number(pageSize), offset); const [rows] await pool.query(sql, params); res.json({ code: 0, data: rows, total: rows.length }); });这里的LIMIT ? OFFSET ?用的是占位符传参而不是字符串拼接目的有两个一是防止 SQL 注入二是 mysql2 驱动对占位符有类型推断pageSize如果以字符串传入部分版本会报语法错误所以用Number()做了一次转换。分页参数page和pageSize是这类接口的通用约定小程序端用wx.request发起请求时query 参数默认全是字符串后端必须做数值转换。2.3 登录鉴权与统一响应格式书籍销售系统绕不开用户体系。常见的做法是微信小程序端调用wx.login()获取临时 code发送到后端后端再用 code 换 openid。但在本地开发调试时这一步往往走不通——因为要配置 AppID 和 AppSecret很多源码包里填的是测试号或者空的。我的建议是开发阶段用账号密码 JWT代替微信登录功能演示时不受网络限制答辩也更稳定。JWT 的用法很固定const jwt require(jsonwebtoken); router.post(/login, async (req, res) { const { username, password } req.body; const [rows] await pool.query( SELECT id, username, nickname FROM users WHERE username ? AND password ?, [username, password] ); if (rows.length 0) { return res.status(401).json({ code: 401, msg: 用户名或密码错误 }); } const token jwt.sign( { uid: rows[0].id, username: rows[0].username }, your_secret_key, { expiresIn: 7d } ); res.json({ code: 0, data: { token, userInfo: rows[0] } }); });密码明文存数据库是这类课设源码的通病如果你打算把它写成简历项目至少换成 bcrypt 哈希。JWT 的expiresIn我习惯设 7 天小程序端用户不会频繁登录太短会导致体验很差。后端拿到 token 后在需要鉴权的接口里做一次解析把uid塞进req对象供后续查询使用。接口路径方法参数鉴权/api/users/registerPOSTusername, password, nickname否/api/users/loginPOSTusername, password否/api/books/listGETpage, pageSize, category否/api/books/detailGETid否/api/cart/addPOSTbookId, count是/api/orders/createPOSTitems, address是/api/orders/listGETuid是路由表要和前端页面一一对应首页对应/api/books/list详情页对应/api/books/detail购物车和订单接口必须带 token。后端调通之后用 Postman 或 Apifox 把注册、登录、书籍列表这三个接口先验证一遍再开始接小程序。3. 小程序端实现从 app.json 到 wx.request 对接后端接口3.1 原生小程序与 uniapp 的差异、tabBar 配置源码包里的小程序端有两种形态原生微信小程序.wxml/.wxss/.js 文件或者 uniapp 工程HBuilderX 创建源码是 .vue 文件。看根目录有没有pages.json就能区分有则是 uniapp没有且直接是app.json则是原生。两者写法差异很大但页面逻辑基本一致——数据请求都是走wx.request这只是换了函数名的问题。这里以原生小程序展开。打开app.json最先要改的是 pages 数组和 tabBar{ pages: [ pages/index/index, pages/category/category, pages/cart/cart, pages/user/user, pages/detail/detail, pages/order/order ], window: { navigationBarTitleText: 校园书籍商城, navigationBarBackgroundColor: #f5f5f5 }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/category/category, text: 分类 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/user/user, text: 我的 } ] } }pages数组的第一项是启动页修改刚进入的加载页面直接调整这一项的顺序即可。tabBar 的list数组最少 2 项最多 5 项每项需要pagePath和text如果后续要加 icon 图标必须准备 PNG 格式且大小限制在 40KB 以内网络图标是不生效的。另外 tabBar 页面必须出现在pages数组中否则编译直接报错。3.2 商品列表页的数据请求与下拉刷新商品列表页是流量入口代码模式也最典型// pages/index/index.js const app getApp(); Page({ data: { books: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.fetchBooks(); }, onPullDownRefresh() { this.setData({ page: 1, books: [], hasMore: true }); this.fetchBooks().finally(() wx.stopPullDownRefresh()); }, onReachBottom() { if (this.data.hasMore !this.data.loading) { this.setData({ page: this.data.page 1 }); this.fetchBooks(); } }, fetchBooks() { if (this.data.loading) return; this.setData({ loading: true }); const { page, pageSize } this.data; return wx.request({ url: http://127.0.0.1:3000/api/books/list, data: { page, pageSize }, success: (res) { const list res.data.data; this.setData({ books: this.data.books.concat(list), hasMore: list.length this.data.pageSize }); }, fail: (err) { wx.showToast({ title: 网络请求失败, icon: none }); }, complete: () { this.setData({ loading: false }); } }); } });这套分页写法有几个必须注意的细节。第一page在onPullDownRefresh时重置为 1同时清空books数组否则下拉刷新后列表会叠加旧数据第二hasMore的判断依据是本次返回条数是否等于 pageSize如果后端返回不足一页说明已到底第三loading标志位防止用户快速滚动时连续触发多次请求这是分页场景最常见的 bug。url里写的http://127.0.0.1:3000是后端本地地址小程序开发者工具默认会拦截需要在详情 - 本地设置里勾选不校验合法域名、web-view、TLS 版本以及 HTTPS 证书才能正常请求。3.3 购物车与订单流程里的 setData 更新策略购物车页面的核心是一个数组里面存放商品信息和数量。每勾选一件商品都要实时计算总价。这里最常见的错误是直接改this.data.books再复制回去导致页面不更新// 错误示范 this.data.books[0].count 2; this.setData({ books: this.data.books }); // 正确做法 const key books[${index}].count; this.setData({ [key]: 2 });用索引字符串作为 key 是微信小程序的更新技巧它的优势是只更新一帧里的局部数据渲染性能好而且避免了对数组整体 setData 可能引发的数据已改变但视图未刷新问题。计算总价时直接从 data 里取勾选状态过滤求和。下单接口则要把购物车里的勾选商品组装成后端需要的结构submitOrder() { const selectedItems this.data.books .filter(item item.checked) .map(item ({ bookId: item.id, count: item.count })); const token wx.getStorageSync(token); wx.request({ url: http://127.0.0.1:3000/api/orders/create, method: POST, data: { items: selectedItems, address: this.data.address }, header: { Authorization: Bearer ${token} }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 下单成功 }); this.setData({ books: [], totalPrice: 0 }); } } }); }注意header里带的Authorization字段必须和后端鉴权中间件解析的字段名保持一致这是前后端联调里最容易出 mismatch 的地方。很多源码里后端读的是req.headers.token前端却发了Bearer xxx结果永远 401。拿到源码第一步先全局搜索一下后端用的字段名。4. 数据库设计与联调MySQL 表结构、订单状态机与常见报错4.1 三张核心表的 DDL 与字段含义这类书籍销售系统的数据库表设计已经非常定型核心是用户表、书籍表、订单表和订单明细表。课程设计论文里写数据库设计这一章时画 ER 图和字段说明表是固定套路。以下是一套开箱即用的建表 SQLCREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50), avatar VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2), cover VARCHAR(255), category VARCHAR(50), stock INT DEFAULT 0, sales INT DEFAULT 0, description TEXT, status TINYINT DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE 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 TINYINT DEFAULT 0, address VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME, ship_time DATETIME, INDEX idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型上有几个经验值价格必须用DECIMAL(10,2)而不是 FLOATFLOAT 计算金额会产生 0.10.2 的精度问题书籍封面cover存的是图片 URL 字符串不要把图片转成 base64 放进数据库那会让表体积爆炸status字段用TINYINT存数字状态而不是字符串性能更好后端维护一张状态映射表即可。订单号order_no我一般用时间戳加随机数生成保证不重复。4.2 订单状态机与后端查询返回结构订单状态是这类系统里唯一有流程感的部分。常见的状态定义是0 - 待付款1 - 已付款2 - 已发货3 - 已收货4 - 已取消。后端在下单接口里做两件事一是插入订单总表二是循环插入订单明细表。注意这里必须用事务否则用户下单时明细写入失败订单总表却已经有了对账会非常痛苦。mysql2 的事务写法如下const conn await pool.getConnection(); try { await conn.beginTransaction(); const [orderResult] await conn.query( INSERT INTO orders (order_no, user_id, total_amount, status) VALUES (?, ?, ?, 0), [orderNo, uid, totalAmount] ); for (const item of items) { await conn.query( INSERT INTO order_items (order_id, book_id, count, price) VALUES (?, ?, ?, ?), [orderResult.insertId, item.bookId, item.count, item.price] ); } await conn.commit(); res.json({ code: 0, data: { orderId: orderResult.insertId } }); } catch (err) { await conn.rollback(); next(err); } finally { conn.release(); }事务的三步——beginTransaction、commit、rollback——顺序不能乱finally里释放连接也是必须的否则连接池会被占满后续请求全部超时。前端拿到orderId后跳转订单详情页再通过查询接口轮询订单状态。这里的联调要点是订单状态变更比如用户点确认收货前端要调用特定接口更新status字段而不是本地改状态否则刷新页面后状态会穿越回去。4.3 联调阶段最常见的三类报错前后端联调阶段的问题高度集中在以下三类排查时按顺序检查即可。第一类是请求 404通常是小程序里的请求路径与后端路由不一致比如后端定义的是/api/books/list前端写成了/api/book/list检查页面里的 url 即可。第二类是跨域 CORS 报错小程序开发者工具里的表现是 request 直接 fail后端需要确认是否安装了cors中间件。第三类是字段名对不上后端返回created_at小程序里写createdAt界面渲染就是 undefined。这类问题推荐用 Charles 抓包或微信开发者工具自带的 Network 面板直接看返回 JSON 的真实字段名比瞎猜快得多。5. 部署与排错把源码跑起来的关键技巧与验证方法5.1 PowerShell 禁止运行脚本的解除方法启动后端服务时如果用的是新版 Node.jsnode app.js不受影响但如果用了 nodemon 或 npm 脚本就会撞上热词里那条经典报错——npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。这是 PowerShell 的 Execution Policy 在拦截 npm.ps1不是 Node.js 坏了。解决方法是在 PowerShell 里执行Set-ExecutionPolicy -Scope CurrentUser RemoteSigned执行后选择 Y 确认即可。RemoteSigned的含义是本地脚本可运行从网络下载的脚本必须有签名。改完之后重新打开终端npm 命令就正常了。注意-Scope CurrentUser只影响当前用户不需要管理员权限比全局修改更安全。5.2 node_modules 损坏的快速修复源码包的解压过程有时会把依赖弄乱表现为 npm install 后启动报Cannot find module express。不要逐个包去装按顺序执行这三条命令rm -rf node_modules package-lock.json npm cache clean --force npm install第一条删除依赖目录和锁文件第二条清空 npm 缓存第三条重新安装。90% 的依赖问题都能用这个组合解决。如果网络差导致安装中断可以再跑一次npm installnpm 会断点续传。项目跑起来后用curl http://127.0.0.1:3000/api/books/list验证接口是否返回 JSON这一步比打开小程序更先确认后端存活。5.3 小程序图片存储与体验版发布书籍封面图这类文件在课设阶段最常见的做法是直接放外链图床或者用后端静态目录托管。小程序端的wx.env.user_data_path是本地用户目录仅适合缓存临时附件不适合存商品图片——因为体验版和正式版在小程序后台的分区不同本地文件不会随代码同步上传换设备就失效。正确做法是书籍表cover字段存完整 URL后端把图片放在public/uploads/目录并通过express.static对外暴露。发布体验版前在小程序后台的开发管理 - 开发设置 - 服务器域名里把 request 合法域名加上并且必须是 HTTPS。本地调试阶段勾选的不校验合法域名在真机预览时不生效这一点亲测是最容易让新手卡住的环节。本文还有配套的精品资源点击获取