尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

微信小程序书籍销售系统全栈实战:从源码到跑通

微信小程序书籍销售系统全栈实战:从源码到跑通 简介这是一套面向计算机专业学生与小程序开发初学者的微信小程序书籍销售系统完整项目包适合用作课程设计、毕业设计或自学练手。资源包含可运行源码与配套说明文档文档覆盖需求与可行性分析、系统总体架构、数据库E/R图与数据表设计、小程序端与管理员端功能实现以及调试测试和开发问题总结能帮助读者理清从设计到落地的完整思路。压缩包共1178个文件约18.42MB以png、jpg等图片素材js、vue、json等前后端代码wxss、wxml小程序页面文件为主另含sql建库脚本、md说明、docx文档及bat启动脚本目录结构清晰便于按模块查阅。目前已有811人学习下载适合需要一套可直接参考的书籍电商小程序方案、对照文档理解数据库与功能模块划分的读者。1. 基于微信小程序的书籍销售系统从源码到跑通一个全栈练手项目的真实落地路径如果你正在找一个能同时覆盖前端、后端、数据库和支付闭环的练手项目基于微信小程序的书籍销售系统几乎是绕不开的选项。它不像纯前端 Demo 那样只能看不能跑也不像某些企业级系统那样光环境配置就能劝退一半人。这个项目的核心链路非常清晰用户在小程序里浏览书籍、加入购物车、下单、模拟支付后台管理书籍信息和订单状态。源码加说明文档的组合意味着你拿到的不只是代码还有一套可以照着复现的部署路径。适合谁适合已经学过小程序基础组件、想找一个完整业务闭环来练手的人也适合需要交课程设计但不想随便糊弄的开发者。我见过太多人卡在“源码跑不起来”这一步所以这篇笔记的重点不是讲概念而是把从环境准备到订单闭环的每一步拆开把参数和坑都摆出来。2. 技术选型与项目结构为什么这套组合能跑通2.1 前后端分离的边界在哪里这个项目的典型架构是微信小程序作为前端后端用 Node.js 或 Java 提供 RESTful 接口数据库用 MySQL 或云开发数据库。很多人一上来就纠结选哪个后端语言其实对于书籍销售系统这种业务模型核心不在于语言而在于接口设计是否清晰。我一般会建议新手用 Node.js Express因为 JavaScript 前后端通吃调试时不用来回切换思维。如果你用的是云开发那就更简单数据库和接口都在云端省去服务器部署。但云开发的坑在于权限控制和本地调试后面会细说。项目结构通常分三块小程序端目录、后端服务目录、数据库脚本目录。小程序端又分 pages、components、utils、api 四个子目录。pages 放页面components 放复用组件比如书籍卡片utils 放请求封装和格式化函数api 放各个模块的接口定义。后端目录一般分 routes、controllers、models、middlewares。routes 定义路由controllers 处理业务逻辑models 定义数据模型middlewares 放鉴权和错误处理。数据库脚本里至少要有建表语句和初始数据。提示拿到源码后先别急着跑花十分钟把目录结构看一遍知道每个文件大概干什么后面报错时能快速定位。2.2 数据库表设计的三个关键点书籍销售系统的数据库表不多但有几个字段设计不好会直接影响后续开发。第一张表是书籍表字段包括 id、title、author、price、cover、stock、category、description。这里的关键是 price 用 decimal 而不是 float否则计算总价时会出现 0.1 0.2 0.30000000000000004 的经典问题。stock 字段要设默认值 0并且在后端扣减库存时用事务或原子操作不然并发下单会超卖。第二张表是订单表字段包括 id、user_id、total_price、status、create_time、address。status 用枚举值而不是字符串比如 0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。这样前端判断状态时不用怕拼写错误。第三张表是订单详情表字段包括 id、order_id、book_id、quantity、price。这里 price 要单独存一份因为书籍价格可能后续会改订单里的价格必须是下单那一刻的快照。CREATE TABLE book ( id int NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, author varchar(50) DEFAULT NULL, price decimal(10,2) NOT NULL DEFAULT 0.00, cover varchar(255) DEFAULT NULL, stock int NOT NULL DEFAULT 0, category varchar(50) DEFAULT NULL, description text, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order ( id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL, total_price decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, address varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;上面这段 SQL 可以直接在 MySQL 里执行。注意 decimal(10,2) 表示总共 10 位小数点后 2 位足够存书籍价格。status 用 tinyint 而不是 varchar省空间也省索引。create_time 用 CURRENT_TIMESTAMP 自动填充省得后端每次手动写。2.3 小程序端的请求封装与全局状态小程序端最容易写乱的地方是请求。很多人每个页面都写一遍 wx.request结果接口地址改了要改十几个文件。正确做法是封装一个 request 函数统一处理 baseURL、token、错误提示。下面是一个常见的封装方式。// utils/request.js const BASE_URL http://localhost:3000/api; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 200) { resolve(res.data); } else if (res.statusCode 401) { wx.showToast({ title: 请先登录, icon: none }); wx.navigateTo({ url: /pages/login/login }); reject(res); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };这段代码的关键在于 header 里带了 Authorizationtoken 从本地缓存取。401 状态码统一跳登录页不用每个页面单独判断。BASE_URL 在开发时指向本地上线时改成服务器地址。注意小程序开发工具里要勾选“不校验合法域名”否则本地请求会被拦截。全局状态方面购物车数据建议放在 app.globalData 里同时同步到本地缓存。这样切换页面时购物车数量不会丢。但要注意 globalData 是内存级的小程序被杀掉就没了所以本地缓存是必须的。3. 从零跑通最小闭环环境、接口与页面联调3.1 后端服务启动与数据库连接假设你拿到的是 Node.js 后端第一步是安装依赖。进入后端目录执行 npm install。如果 package.json 里有 nodemon启动命令通常是 npm run dev。没有的话直接 node app.js。启动前要确认数据库配置正确一般配置文件在 config/db.js 或 .env 文件里。# 进入后端目录 cd server # 安装依赖 npm install # 启动服务假设入口是 app.js node app.js启动后如果看到 “Server running on port 3000” 和 “Database connected”说明后端和数据库都通了。如果数据库连接失败先检查 MySQL 服务是否启动再检查用户名密码和数据库名是否对得上。常见错误是 MySQL 8.0 的加密方式导致连接失败需要在 MySQL 里执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; 然后 FLUSH PRIVILEGES;。数据库建表后要导入初始数据否则小程序首页是空的。初始数据至少要有十几本书覆盖不同分类这样前端分类筛选才有东西可看。导入方式可以用 MySQL 命令行 source 命令也可以用 Navicat 之类的工具直接运行 SQL 文件。3.2 小程序端配置与首页渲染小程序端第一步是改 app.js 里的全局配置把 baseURL 指向你的后端地址。如果你在开发工具里跑用 http://localhost:3000 就行。然后检查 app.json 里的 pages 列表确保首页路径存在。首页通常叫 pages/index/index。首页渲染书籍列表的逻辑是onLoad 时调用 request(/books)拿到数据后 setData 到 books 数组。wxml 里用 wx:for 遍历渲染。这里有个细节图片要用 modeaspectFill 防止变形价格要用 toFixed(2) 保留两位小数。// pages/index/index.js const { request } require(../../utils/request); Page({ data: { books: [], loading: true }, onLoad() { this.fetchBooks(); }, async fetchBooks() { try { const res await request({ url: /books }); this.setData({ books: res.data.map(item ({ ...item, price: Number(item.price).toFixed(2) })), loading: false }); } catch (err) { this.setData({ loading: false }); } } });这段代码里 request 返回的是 Promise所以可以用 async/await。map 里把 price 转成数字再 toFixed防止后端返回字符串导致显示异常。loading 状态用来控制骨架屏或加载提示提升体验。3.3 购物车与订单接口的联调顺序联调不要一上来就测下单按这个顺序来先测书籍列表接口再测书籍详情接口然后测加入购物车接口最后测创建订单接口。每测通一个再往下走不然出错时不知道是哪一层的问题。加入购物车的逻辑有两种一种是把购物车存在后端数据库一种存在小程序本地缓存。存后端的好处是换设备也能同步坏处是每次操作都要请求接口。存本地的好处是快坏处是换设备就没了。我一般建议练手项目存本地简单且够用。本地购物车的数据结构是 { bookId, title, price, cover, quantity }用 wx.setStorageSync(cart, cartArray) 存。创建订单时前端把购物车数据和收货地址一起发给后端。后端要做三件事校验库存、扣减库存、生成订单和订单详情。这三步必须在一个事务里否则扣了库存但订单没生成数据就不一致了。// 后端创建订单的核心逻辑Express MySQL router.post(/orders, auth, async (req, res) { const { items, address } req.body; const connection await pool.getConnection(); try { await connection.beginTransaction(); let totalPrice 0; for (const item of items) { const [rows] await connection.query( SELECT stock, price FROM book WHERE id ? FOR UPDATE, [item.bookId] ); if (rows[0].stock item.quantity) { throw new Error(书籍 ${item.bookId} 库存不足); } totalPrice rows[0].price * item.quantity; await connection.query( UPDATE book SET stock stock - ? WHERE id ?, [item.quantity, item.bookId] ); } const [orderResult] await connection.query( INSERT INTO order (user_id, total_price, status, address) VALUES (?, ?, 0, ?), [req.userId, totalPrice, address] ); for (const item of items) { await connection.query( INSERT INTO order_detail (order_id, book_id, quantity, price) VALUES (?, ?, ?, ?), [orderResult.insertId, item.bookId, item.quantity, item.price] ); } await connection.commit(); res.json({ code: 0, data: { orderId: orderResult.insertId } }); } catch (err) { await connection.rollback(); res.status(500).json({ code: 1, message: err.message }); } finally { connection.release(); } });这段代码的关键是 FOR UPDATE 锁行防止并发下单超卖。beginTransaction 和 commit/rollback 保证原子性。order_detail 里存了 price 快照后续书籍改价不影响已生成的订单。注意 auth 中间件要放在路由里确保只有登录用户能下单。4. 避坑与排查源码跑不起来时先看这五条4.1 小程序请求本地接口报“不在以下 request 合法域名列表中”现象是小程序开发工具里请求后端接口控制台报这个错接口根本发不出去。原因是小程序默认只允许 https 且已备案的域名。解决方法是打开开发工具的“详情”面板在“本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。这个选项只在开发阶段用上线前必须换成真实域名并配置服务器。4.2 数据库连接报 ER_NOT_SUPPORTED_AUTH_MODE现象是后端启动时连 MySQL 失败报这个错。原因是 MySQL 8.0 默认用 caching_sha2_password 加密方式而一些 Node.js 的 mysql 驱动版本不支持。解决方法是进入 MySQL 命令行执行 ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; 然后 FLUSH PRIVILEGES;。改完后重启后端服务即可。如果用的是云数据库在控制台里找账户管理把加密方式改成 mysql_native_password。4.3 购物车数量角标不更新现象是加入购物车后底部 tabBar 的角标数字没变。原因是 tabBar 角标需要用 wx.setTabBarBadge 单独设置不会自动跟随数据变化。解决方法是在每次修改购物车后手动调用 wx.setTabBarBadge({ index: 2, text: String(totalCount) })。如果 totalCount 为 0要调用 wx.removeTabBarBadge({ index: 2 }) 清除角标。注意 index 是 tabBar 的序号从 0 开始。4.4 订单列表页下拉刷新后数据重复现象是订单列表下拉刷新新数据追加到旧数据后面列表越来越长。原因是 onPullDownRefresh 里用了 concat 而不是替换。解决方法是下拉刷新时把 page 重置为 1然后直接用新数据替换旧数据而不是拼接。上拉加载更多时才用 concat。另外要在 onPullDownRefresh 最后调用 wx.stopPullDownRefresh()否则刷新动画不会停。4.5 图片上传后显示 404现象是后台上传书籍封面成功但小程序里图片显示不出来控制台报 404。原因是后端把图片存在了本地磁盘但返回的 URL 是相对路径小程序拼接后找不到。解决方法是后端返回完整 URL或者用静态资源中间件把上传目录暴露出去。比如 Express 里加 app.use(/uploads, express.static(uploads))然后数据库里存 /uploads/xxx.jpg前端用 baseURL cover 拼接。注意小程序里图片路径不能有中文和空格上传时最好重命名成时间戳。5. 进阶技巧把练手项目变成能写进简历的作品5.1 用云开发替代自建后端省掉服务器成本如果你不想折腾服务器和域名微信云开发是一个很实际的选择。云开发提供数据库、云函数、存储和 CDN小程序端直接调用不用配 baseURL也不用担心合法域名。迁移思路是把 Express 的路由改成云函数每个云函数处理一个业务动作比如 getBooks、addToCart、createOrder。数据库从 MySQL 换成云开发的文档型数据库集合名对应表名。云开发的权限控制用数据库权限和云函数里的 openid 判断。// 云函数 createOrder 的核心逻辑 const cloud require(wx-server-sdk); cloud.init(); const db cloud.database(); const _ db.command; exports.main async (event, context) { const { OPENID } cloud.getWXContext(); const { items, address } event; // 云开发没有传统事务用 runTransaction 保证一致性 return await db.runTransaction(async transaction { let totalPrice 0; for (const item of items) { const book await transaction.collection(book).doc(item.bookId).get(); if (book.data.stock item.quantity) { await transaction.rollback(); return { code: 1, message: 库存不足 }; } totalPrice book.data.price * item.quantity; await transaction.collection(book).doc(item.bookId).update({ data: { stock: _.inc(-item.quantity) } }); } const order await transaction.collection(order).add({ data: { openid: OPENID, totalPrice, status: 0, address, createTime: db.serverDate() } }); for (const item of items) { await transaction.collection(order_detail).add({ data: { orderId: order._id, bookId: item.bookId, quantity: item.quantity, price: item.price } }); } return { code: 0, data: { orderId: order._id } }; }); };云开发的事务用 runTransaction里面用 transaction.collection 操作。注意云开发的数据库操作是异步的每个 await 都不能漏。库存扣减用 _.inc(-quantity) 原子操作比先查再改更安全。openid 从 getWXContext 拿不用前端传避免伪造。5.2 加一个简单的推荐位让首页不那么单调首页如果只有书籍列表看起来很像半成品。加一个轮播图推荐位成本很低但效果明显。轮播图数据可以存在数据库的 banner 表里也可以直接写死在小程序端。我一般建议存数据库这样后台能改。banner 表字段id、image、bookId、sort。小程序端用 swiper 组件渲染点击跳转到对应书籍详情。!-- pages/index/index.wxml 轮播图部分 -- swiper indicator-dots autoplay circular interval3000 styleheight: 300rpx; swiper-item wx:for{{banners}} wx:keyid bindtapgoToBook style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />
返回列表