
简介基于Vue、Node.js和MySQL的网站商城项目源码包内含完整的前端页面与后端接口逻辑及数据库脚本面向具备一定前端基础的开发者可用于课程设计、毕业设计或电商网站开发入门实践。压缩包共258个文件大小约7.85MB包含Vue单文件组件、JavaScript逻辑、CSS/SCSS样式、图片素材以及SQL数据库脚本等其中Vue组件和JS代码实现了页面交互与接口请求样式文件覆盖商品展示、详情、登录等核心页面可用于快速还原商城视觉。项目实现首页轮播图、商品展示、购物车、留言评论、收藏、新闻公告、注册登录和后台管理等电商常见功能前端组件化开发思路清晰后端接口逻辑易于理解并附数据库初始化脚本能显著降低环境配置成本。目前已有2604人在线学习下载适合需要完整可运行商城源码作为参考的开发者也适合作为全栈项目的练手素材。1. 从 zip 里拿到的商城项目到底该怎么跑起来一个打包好的vuenode.jsmysql 网站商城源码数据库.zip压缩包里通常就是三样东西Vue 前端工程、Node.js 后端服务、一个 .sql 数据库脚本。这种组合在小团队实训、毕业设计、企业内部工具站里出现频率极高因为它的技术栈足够主流——Vue 负责页面交互Node.js 负责接口逻辑MySQL 负责持久化三层边界清楚改起来不费劲。你拿到这个包的第一反应不必是急着解压而是先想清楚这个项目是前后端分离的需要分别启动前端和后端再把数据库脚本导入 MySQL三者联通后才能看到完整商品列表和下单流程。这篇文章就按这个链路把每一步要改的文件、要敲的命令、容易掉进去的坑挨个讲透。2. Vue 商城前端源码的结构与核心页面实现2.1 用 vue-router 配置商城的页面路由与懒加载商城项目的前端页面无外乎首页商品列表、商品详情、购物车、结算、个人中心。在 Vue 工程里路由配置是入口。常见做法是在src/router/index.js里按模块拆分路由表并配合import()实现懒加载避免首屏一次性加载全部页面代码。// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(../views/Home.vue) // 懒加载路由命中时才拉取组件 }, { path: /product/:id, name: ProductDetail, component: () import(../views/ProductDetail.vue), props: true // 把路由参数 :id 直接传给组件属性 }, { path: /cart, name: Cart, component: () import(../views/Cart.vue) }, { path: /checkout, name: Checkout, component: () import(../views/Checkout.vue) } ] const router createRouter({ history: createWebHistory(), routes }) export default router这段配置里createWebHistory走的是 HTML5 History 模式URL 里没有#刷新页面时后端需要做 history fallback否则 404。如果你用它部署到 Nginx记得加try_files $uri $uri/ /index.html。props: true是把/product/:id里的 id 作为 prop 传给组件组件里直接用defineProps接收比在组件里再调useRoute()要干净。购物车页面一般需要从后端拉取商品数据同时要维护本地选择状态。我习惯在 Vuex 里存一份购物车列表每次加购时先查本地有没有同 id 商品有就累加数量没有就插入新条目。这样页面切换时购物车数据不会因为组件销毁而丢失。2.2 商品列表页的组件拆分与 v-for 渲染商品列表页是商城最核心的展示页面。通常父组件负责请求数据子组件只负责渲染卡片。下面是商品卡片组件的典型写法!-- ProductCard.vue -- template div classproduct-card clickgoDetail img :srcproduct.image :altproduct.name / div classinfo h3{{ product.name }}/h3 p classprice¥{{ product.price.toFixed(2) }}/p button click.stopaddToCart加入购物车/button /div /div /template script setup import { useRouter } from vue-router import { useStore } from vuex const props defineProps({ product: { type: Object, required: true } }) const router useRouter() const store useStore() function goDetail() { router.push(/product/${props.product.id}) } function addToCart() { store.dispatch(cart/add, props.product) } /script这里用click.stop阻止按钮点击冒泡到外层 div否则会同时触发跳转详情。store.dispatch(cart/add, ...)是 Vuex 的 action 调用方式把商品数据传入action 内部再 commit mutation 修改状态。注意图片地址要从接口返回的绝对路径或相对路径中正确拼接很多前后端分离项目在这里踩坑——后端返回/uploads/a.jpg前端如果直接用:srcproduct.image就能访问但如果后端返回的是a.jpg且图片存在public/uploads下需要自己拼接。2.3 用 Pinia 或 Vuex 管理购物车状态的推荐写法新版 Vue 3 项目用 Pinia 更多它省去了 mutation 的冗余代码。但很多老商城源码还是 Vuex。不管用哪个核心都是把购物车状态集中管理并持久化到 localStorage。以 Pinia 为例// store/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: JSON.parse(localStorage.getItem(cart) || []) }), getters: { totalPrice(state) { return state.items.reduce((sum, item) sum item.price * item.quantity, 0) } }, actions: { add(product) { const index this.items.findIndex(i i.id product.id) if (index -1) { this.items[index].quantity } else { this.items.push({ ...product, quantity: 1 }) } localStorage.setItem(cart, JSON.stringify(this.items)) }, remove(id) { this.items this.items.filter(i i.id ! id) localStorage.setItem(cart, JSON.stringify(this.items)) } } })这段代码把购物车数据直接同步到 localStorage刷新页面后购物车还在。getters里的totalPrice用于结算页展示总价。注意对象展开...product是为了避免直接引用原对象导致后续修改污染商品列表数据。如果项目里用了 TypeScript最好给 product 定义一个接口保证结构稳定。3. Node.js 后端源码的接口层设计与数据交互3.1 Express 服务的初始化与中间件配置后端用 Node.js 的 Express 框架很常见因为生态成熟、中间件多。一个标准的商城后端入口文件通常是这样的// server.js const express require(express) const cors require(cors) const bodyParser require(body-parser) const productRoutes require(./routes/product) const orderRoutes require(./routes/order) const app express() const port process.env.PORT || 3000 app.use(cors()) // 允许跨域前端开发服务器默认端口 5173 或 8080 app.use(bodyParser.json()) // 解析 JSON 请求体 app.use(bodyParser.urlencoded({ extended: true })) // 解析表单请求 // 静态资源映射商品图片放在 public/uploads 下 app.use(/uploads, express.static(public/uploads)) app.use(/api/products, productRoutes) app.use(/api/orders, orderRoutes) app.listen(port, () { console.log(Server running on http://localhost:${port}) })cors()不加参数时允许所有域访问开发环境够用生产环境最好改成具体的域名列表。bodyParser.json()必须放在路由之前否则接口里req.body会是 undefined。静态资源用express.static映射这样图片路径http://localhost:3000/uploads/xxx.jpg就能直接访问。端口用了process.env.PORT方便部署时通过环境变量修改。3.2 使用 mysql2 连接 MySQL 并编写商品查询接口连接 MySQL 数据库推荐使用mysql2这个包它支持 Promise配合async/await写起来很自然。首先要建一个数据库连接池避免每个请求都新建连接浪费资源。// db/index.js const mysql require(mysql2/promise) const pool mysql.createPool({ host: localhost, user: root, password: 123456, database: shop_db, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }) module.exports pool连接池参数里connectionLimit是最大连接数商城并发不大时 10 够用如果后面压测发现连接不够可以调到 20。waitForConnections表示连接池满时请求排队而不是直接报错。接下来写商品路由// routes/product.js const express require(express) const router express.Router() const pool require(../db) // 获取商品列表支持按分类筛选 router.get(/, async (req, res) { const { category } req.query try { let sql SELECT id, name, price, stock, image FROM products if (category) { sql WHERE category ? } const [rows] await pool.query(sql, category ? [category] : []) res.json({ code: 0, data: rows }) } catch (err) { console.error(err) res.status(500).json({ code: 1, message: 服务器繁忙 }) } }) // 获取商品详情 router.get(/:id, async (req, res) { const { id } req.params try { const [rows] await pool.query(SELECT * FROM products WHERE id ?, [id]) if (rows.length 0) { return res.status(404).json({ code: 1, message: 商品不存在 }) } res.json({ code: 0, data: rows[0] }) } catch (err) { console.error(err) res.status(500).json({ code: 1, message: 服务器繁忙 }) } }) module.exports routerpool.query使用占位符?而不是字符串拼接能有效防止 SQL 注入。如果你在调试时看到 SQL 报错先打印一下最终执行的 SQL 语句用pool.query的 debug 参数可以输出。注意SELECT *会返回所有字段包括description这种长文本列表页不需要它查明细时再用完整字段减少网络传输。3.3 订单提交接口的事务处理提交订单涉及两张表订单主表和订单明细表。必须使用数据库事务否则可能出现主表插入成功、明细插入失败导致金额对不上。下面是简化版实现// routes/order.js const express require(express) const router express.Router() const pool require(../db) router.post(/, async (req, res) { const { userId, items, totalPrice } req.body if (!userId || !items || items.length 0) { return res.status(400).json({ code: 1, message: 参数错误 }) } const connection await pool.getConnection() try { await connection.beginTransaction() const [result] await connection.query( INSERT INTO orders (user_id, total_price, status, create_time) VALUES (?, ?, ?, NOW()), [userId, totalPrice, 0] ) const orderId result.insertId const orderItems items.map(item [orderId, item.productId, item.quantity, item.price]) await connection.query( INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ?, [orderItems] ) await connection.commit() res.json({ code: 0, data: { orderId } }) } catch (err) { await connection.rollback() console.error(err) res.status(500).json({ code: 1, message: 下单失败 }) } finally { connection.release() } })这里connection是从连接池中单独获取的事务操作在同一个连接上进行。批量插入用VALUES ?配合一个二维数组这种方式比循环单条插入性能好得多也减少网络往返。注意beginTransaction后必须保证release()被调用否则连接泄漏所以用finally释放。如果事务过程中发生错误回滚rollback()这样订单主表和明细表一致。4. MySQL 数据库脚本的设计与初始化数据导入4.1 商城核心表的字段设计与外键关系打开数据库脚本文件你会看到CREATE TABLE语句。商城最基础的表是用户表、商品表、订单表和订单明细表。设计时要注意字段类型的选择商品价格用DECIMAL(10,2)而不是FLOAT避免浮点误差库存用INT UNSIGNED订单状态用TINYINT即可。下面是精简版建表脚本CREATE DATABASE IF NOT EXISTS shop_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shop_db; CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB; CREATE TABLE products ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT UNSIGNED NOT NULL DEFAULT 0, category VARCHAR(50) NOT NULL, image VARCHAR(255) DEFAULT NULL, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待付款 1-已付款, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user (user_id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users (id) ) ENGINEInnoDB; CREATE TABLE order_items ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id), CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders (id), CONSTRAINT fk_items_product FOREIGN KEY (product_id) REFERENCES products (id) ) ENGINEInnoDB;utf8mb4是必须的它能存表情符号和生僻字。所有表都用InnoDB因为它支持事务和外键。外键在这里用了但要注意如果后续分库分表外键会被去掉。category字段上加索引是因为商品列表经常会按分类筛选。password字段存的是加密后的字符串长度 255 够用。4.2 用 source 命令导入初始化数据数据库脚本里通常还会附一些INSERT INTO语句作为测试数据。你拿到 zip 解压后把.sql文件导入 MySQL 的方式很简单。先用命令行登录 MySQL然后source那个脚本文件mysql -u root -p enter password: ******登录后执行mysql source /path/to/shop_db.sql;这里source后面跟的是脚本文件的绝对路径或相对于当前工作目录的路径。如果脚本里有CREATE DATABASE它会自动建库并选中。如果没有你得先手动CREATE DATABASE并USE。导入完成后可以用SHOW TABLES;验证四张表是否创建成功。再执行SELECT COUNT(*) FROM products;如果返回的条数和脚本里插入的测试数据条数一致说明导入成功。4.3 用 navicat 或 mysql workbench 导入时的注意点如果你用的是图形化工具比如 navicat 或者 mysql workbench 使用教程里最常见的操作右键数据库选择运行 SQL 文件然后选中脚本导入。但要注意脚本文件开头通常有CREATE DATABASE如果你已经在工具里建好了一个同名的shop_db脚本执行时会报错因为库已存在。解决办法是那掉脚本里的CREATE DATABASE和USE语句只保留表结构或者在脚本里用CREATE DATABASE IF NOT EXISTS代替普通写法。同样DROP TABLE IF EXISTS在重复导入时会先删旧表再建新表保护你的测试数据不被旧数据污染。5. 前后端联调、启动顺序与常见报错排查5.1 联调的关键配置代理转发与接口地址前端开发服务器默认端口是 5173Vite或 8080Vue CLI后端 Node.js 在 3000。浏览器访问前端页面时跨域请求会被浏览器拦截虽然后端已经配了cors()但在生产环境里不建议放开所有域。更稳妥的做法是前端配 devServer 代理把/api转发到后端。以 Vite 为例// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } })这样前端代码里请求路径写/api/products实际上由 Vite 代理转发到http://localhost:3000/api/products浏览器里看不到跨域。注意changeOrigin要设成 true否则某些场景下 Host 头不对。联调时先确认后端能单独访问在浏览器里打开http://localhost:3000/api/products返回 JSON 就说明后端正常然后再排查前端请求是否正确。5.2 启动项目的推荐顺序和命令按照依赖从下往上的顺序启动不容易出错。第一步导入数据库脚本第二步启动后端第三步启动前端。命令分别写在两个终端里cd backend npm install node server.jscd frontend npm install npm run dev注意npm install如果报错先看下 node 版本。Vue 3 和 Vite 需要 Node.js 版本至少 16mysql2 包对 Node 版本也很敏感。如果你的环境里 node 版本过低npm install会报engine不兼容的错误这时候用nvm install 18切换版本然后再装依赖。安装完成后后端终端出现Server running前端终端出现Local: http://localhost:5173说明两个进程都起来了。5.3 三个高频报错数据库连接失败、端口占用、图片 404第一个坑ER_ACCESS_DENIED_ERROR。这是密码不对检查db/index.js里的密码能否登录 MySQL 命令行。第二个坑EADDRINUSE: port 3000 already in use。说明之前有进程占用端口用lsof -i:3000查出进程号kill -9 PID杀掉。第三个坑页面能打开但商品图片 404。检查product.image的路径如果后端存的是相对路径浏览器请求的地址是http://localhost:5173/uploads/xxx.jpg而静态资源在 3000 端口上需要把图片 baseURL 改为http://localhost:3000或者在 Vue 里写一个图片地址处理函数拼上后端地址。一个更隐蔽的坑MySQL 8.0 默认用caching_sha2_password认证而数据库脚本里的CREATE DATABASE只建库不建用户。你在后端连接时用的用户可能不存在或者加密规则不兼容。最好在 MySQL 里给项目单独建一个账号并指定mysql_native_password例如CREATE USER shop_userlocalhost IDENTIFIED WITH mysql_native_password BY shop_pass; GRANT ALL PRIVILEGES ON shop_db.* TO shop_userlocalhost; FLUSH PRIVILEGES;然后把后端连接信息里的 user 和 password 改成这个账号。这样做还有一个好处不让后端直接用 root 连接数据库权限最小化即使代码被窃取攻击者也拿不到数据库管理员权限。本文还有配套的精品资源点击获取