news 2026/9/17 2:59:58

uniapp小程序+Vue后台+Node.js+MySQL全栈项目开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uniapp小程序+Vue后台+Node.js+MySQL全栈项目开发实战

简介:一套基于uniapp小程序、Vue后台管理系统与Node.js服务端构成的全栈完整项目,面向需要从零搭建移动端到管理端全流程的小程序开发工程师、前端工程师及全栈学习者。小程序端实现轮播图与招聘车队展示、赛事规则与精彩十佳球查看,用户登录采用JWT+token认证,并支持搜索关注、修改资料、查看战绩、接收通知、意见反馈、绑定手机号、注销等功能;PC端基于vue+webpack+element-ui,包含axios二次封装、动态路由导航、vuex权限管理,覆盖用户权限与数据异步处理。压缩包共488个文件,以Vue页面、JS脚本、JSON配置、WXML页面结构与WXSS样式、SCSS样式和PNG/JPG图片为主,另含MySQL数据库SQL脚本与MP4演示视频,压缩后约91.31MB,目录结构清晰便于按模块学习。目前已有28650人学习/下载,适合希望系统掌握全栈项目开发流程、快速迁移业务代码的开发者参考。

1. uniapp 小程序 + Vue 后台 + Node.js + MySQL,这类全栈完整项目到底该怎么拆

把这四个技术词放在一起,描述的是一套最小可交付的业务闭环:C 端用户在小程序里完成浏览、下单、支付,运营和客服人员通过 Vue 后台管理系统处理订单、管理商品,两个前端共用同一个 Node.js 接口层,最终所有业务状态落到 MySQL。实际的立项场景里,难点从来不是某个框架会不会写,而是四端之间靠什么契约协作:接口返回结构怎么定、登录态怎么传、权限怎么控制、数据不一致了去哪里查。这套组合最吸引人的地方,是把整个团队的技术栈收敛到 JavaScript 一门语言,从建表 SQL 到小程序页面,一个人就能从零走通。适合刚拿到需求不知道从哪里下手的初中级开发者,也适合想用最小成本验证业务模型的小团队。

2. uniapp 小程序端搭建:用 HBuilderX 创建跨端项目并封装统一请求层

2.1 用 HBuilderX 或 CLI 创建 uniapp 项目,跑通微信小程序编译链路

创建 uniapp 项目有两条常见路线。追求零配置就用 HBuilderX,新建项目时选择 uni-app,模板选 Vue3/Vite,编译器已经内置在 IDE 里,打开就能预览。习惯命令行操作的团队,我一般用官方 preset 模板初始化:

npx degit dcloudio/uni-preset-vue#vite my-uniapp-app cd my-uniapp-app npm install npm run dev:mp-weixin

dev:mp-weixin会把源码实时编译到dist/dev/mp-weixin目录,用微信开发者工具导入这个目录,一个能交互的小程序骨架就出来了。注意npx degit是从 GitHub 仓库拉取模板,第一次执行会稍慢。要发测试版或正式包时改用npm run build:mp-weixin,产物在dist/build/mp-weixin,微信开发者工具里点「上传」就能走微信的版本管理流程。HBuilderX 的图形化导出功能做的其实是同一件事,区别在于 CLI 更适合接到 Jenkins 或 GitHub Actions 流水线里。

趁项目还小,把目录结构先定下来。一个能支撑多端开发、又不会让后端同事迷路的划分方式是这样的:

src/ ├── api/ # 按业务模块拆分接口定义 ├── components/ # 全局公共组件 ├── pages/ # 页面,按 tabBar 和分包组织 ├── static/ # 图片等静态资源 ├── store/ # Pinia 状态管理 └── utils/ # 请求封装、格式化工具

目录规范的价值要等后端接口路径变动时才会体现:路径一改,只有api/目录里的文件会动,页面业务代码完全不用碰。同时定一条硬约定:页面内禁止直接写uni.request,所有网络请求必须走utils/request.js。这条规则越早立越好,否则到了联调阶段,全工程散落着几十处带回调的请求代码,想加一个统一的错误上报都无从下手。

2.2 封装 uniapp 的 request 请求库:token 注入、401 统一拦截和超时兜底

uniapp 自带的uni.request是回调式的,直接往页面里塞,会导致每个页面的 onLoad 都被 success/fail 回调撑满。一般做法是包一层 Promise,顺便把登录态和错误提示统一收口:

// utils/request.js export function request(options = {}) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: import.meta.env.VITE_API_BASE + options.url, method: options.method || 'GET', data: options.data || {}, timeout: options.timeout || 10000, header: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, success: (res) => { // 后端统一返回 { code, data, message } 结构 if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { uni.removeStorageSync('token') uni.navigateTo({ url: '/pages/login/login' }) reject(res.data) } else { uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) }

这段封装做了三件关键事:从本地缓存取 token 并注入请求头;以code === 0作为业务成功约定;遇到 401 时清掉本地 token 并跳转登录页。timeout默认设 10 秒,小程序弱网环境比浏览器更需要显式的超时控制,否则请求卡死,用户只能杀进程。import.meta.env.VITE_API_BASE是 uniapp 内置的 Vite 环境变量,变量名必须以VITE_开头才暴露给前端代码。

提示:VITE_前缀的变量会编译进前端产物,里面只放接口地址这类非敏感配置,数据库密码、JWT 密钥这类东西绝不能写在这里。

关于成功和失败的通道,这里用 Promise 的resolvereject分开。页面调用时拿到的是已经剥离过的res.data.data,不会再被多层嵌套结构烦扰。

参数类型必填说明
urlstring拼在VITE_API_BASE后面的接口路径
methodstring默认 GET,传 POST/PUT/DELETE 时 data 会进 body
dataobject请求参数
timeoutnumber超时毫秒数,默认 10000

2.3 用条件编译处理微信小程序和 H5 的定位差异,分享入口也走同一套逻辑

uniapp 的跨端能力不是免费的,最典型的就是定位。公众号 H5 页面里定位要靠浏览器的 Geolocation API,微信小程序里则要用uni.getLocation,两者返回值结构不同,硬写兼容代码会让工具函数越来越臃肿。uniapp 编译期指令在这种场景下很好用:

// utils/location.js export function getLocation() { // #ifdef MP-WEIXIN return new Promise((resolve, reject) => { uni.getLocation({ type: 'gcj02', success: resolve, fail: reject }) }) // #endif // #ifdef H5 return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition( (pos) => resolve({ latitude: pos.coords.latitude, longitude: pos.coords.longitude }), reject ) }) // #endif }

MP-WEIXIN是微信小程序的条件编译标识,H5是网页端标识。编译时 uniapp 只会保留当前平台命中的代码块,另一个平台的分支会被直接剔除,不会进入最终包体积。这个机制同样适用于自定义分享、微信支付、蓝牙扫描这类平台差异很大的能力,各写各的调用,逻辑互不干扰。

小程序端到这里基本骨架齐了:目录分层清晰、请求统一收口、跨端差异被隔离在工具函数里。接下来需要后端按code / message / data的契约把这些接口实现出来。

3. Node.js 后端从零搭建:Express 路由分层、JWT 鉴权与 MySQL 连接池

3.1 初始化 Node.js 项目并装齐四个关键依赖

Node.js 后端框架里 Express 仍然是最稳的选择,生态成熟、中间件机制简单,中小全栈项目的复杂度用 Express 刚刚好,不必一开始就上 NestJS。初始化命令和依赖如下:

mkdir node-backend && cd node-backend npm init -y npm install express mysql2 jsonwebtoken bcryptjs cors npm install -D nodemon

逐个说明为什么是这些包:express提供路由和中间件骨架;mysql2是 MySQL 官方推荐的驱动,原生支持 Promise,比老牌mysql包更适合现代写法;jsonwebtoken负责签发和校验 JWT;bcryptjs是纯 JavaScript 实现的密码哈希库,不需要 node-gyp 编译原生模块,Windows 环境安装零负担;cors解决 Vue 后台开发期的跨域问题;nodemon监听文件变更自动重启服务,开发效率高很多。生产环境部署时,npm install --production会自动跳过devDependencies

后端目录按职责拆开,每一层都有明确的边界:

node-backend/ ├── routes/ # 路由定义,只做路径分发 ├── controllers/ # 业务逻辑 ├── middleware/ # 鉴权、日志等中间件 ├── db/ # 数据库连接池 ├── utils/ # 工具函数 └── app.js # 入口文件

路由文件一旦超过三百行,就按业务模块继续拆成user.jsorder.jsproduct.js。项目越大,这条规则的收益越明显。

3.2 用 mysql2 连接池访问 MySQL:环境变量、连接池参数与查询写法

数据库连接不能被每次请求重复创建,连接池复用连接是基本操作。先配置环境变量:

DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root DB_PASSWORD=yourpassword DB_NAME=fullstack_app JWT_SECRET=replace-with-a-long-random-string JWT_EXPIRES_IN=7d

然后初始化连接池:

// db/index.js const mysql = require('mysql2/promise') const pool = mysql.createPool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT), user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }) module.exports = pool

关键参数里,connectionLimit是池内最大连接数,默认 10 够用,高并发可以调到 20,但不要太大,MySQL 默认最大连接数是 151,池设过高反而把数据库线程占满。waitForConnections设成 true 表示池满时请求排队等待,而不是直接抛错;queueLimit为 0 表示队列不再限制长度。用.env加载环境变量时,记得装dotenv并在app.js第一行require('dotenv').config()

参数默认值含义使用建议
connectionLimit10连接池最大连接数中小项目 10,高并发调到 20-30
queueLimit0排队的最大请求数0 表示不限制
waitForConnectionstrue池满时是否等待必须为 true,否则直接报错
connectTimeout10000建立连接的超时网络抖动时建议调大

写查询时,用?占位符传参是防止 SQL 注入的红线:

// controllers/user.js const pool = require('../db') async function getUserByPhone(phone) { const [rows] = await pool.query( 'SELECT id, nickname, avatar, password FROM users WHERE phone = ?', [phone] ) return rows[0] }

pool.query返回的是二维数组,第一项是行数据,第二项是字段信息。永远不要用字符串拼接的方式组装 SQL,哪怕是内部管理系统的临时脚本,这条规则没有例外。

3.3 用 JWT 实现登录鉴权和接口保护

登录链路是前端传手机号和密码,后端查用户、比对密码哈希、签发 token:

// controllers/auth.js const jwt = require('jsonwebtoken') const bcrypt = require('bcryptjs') async function login(req, res) { const { phone, password } = req.body const user = await getUserByPhone(phone) if (!user) { return res.status(400).json({ code: 1001, message: '用户不存在' }) } const ok = await bcrypt.compare(password, user.password) if (!ok) { return res.status(400).json({ code: 1002, message: '密码错误' }) } const token = jwt.sign( { id: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN } ) res.json({ code: 0, data: { token, user: { id: user.id, nickname: user.nickname } } }) }

jwt.sign的 payload 里只放用户 id 和角色字段,不要放手机号、密码哈希等敏感信息,JWT 是 base64 编码的,任何人拿到都能解码看到内容。bcrypt.compare会从已有的哈希串里自动提取盐值完成比对,不需要手动处理盐。注册用户的密码加密用bcrypt.hash(password, 10),盐轮数为 10 是性能和安全的平衡点。

有了签发逻辑,还需要一个保护接口的中间件:

// middleware/auth.js const jwt = require('jsonwebtoken') function authRequired(req, res, next) { const header = req.headers.authorization || '' const token = header.replace('Bearer ', '') if (!token) { return res.status(401).json({ code: 401, message: '未登录' }) } try { const payload = jwt.verify(token, process.env.JWT_SECRET) req.user = payload next() } catch (e) { return res.status(401).json({ code: 401, message: 'token 已过期' }) } }

authRequired的用法是挂载到需要登录的路由上:router.get('/user/info', authRequired, getUserInfo)。用户在 Vue 后台和 uniapp 小程序端的请求头里带上Authorization: Bearer <token>,这个中间件校验通过后会把 payload 挂到req.user,后面的 controller 直接取用。token 过期时jwt.verify抛异常,统一返回 401,小程序端的 request 封装收到 401 会自动跳转登录页。

3.4 统一错误处理中间件,把异常全部收口

最后在app.js里挂一个四参数错误处理中间件:

app.use((err, req, res, next) => { console.error(err) res.status(500).json({ code: 500, message: '服务器内部错误' }) })

四个参数是 Express 识别错误处理器的标志,一个不能少。controller 里用next(err)把异常抛到这里,后面所有的接口都不需要各自写 try catch 兜底。到这里后端的业务契约和鉴权链路完整了,下一步要把 Vue 后台管理系统接进来。

4. Vue 后台管理系统开发:路由守卫、动态菜单与 Node.js 接口对接

4.1 初始化 Vue3 + Vite 后台项目并接入 Element Plus

后台管理系统面向运营和管理员,组件库优先选 Element Plus,它是 Vue3 生态里资料最全、踩坑记录最多的选择。初始化命令:

npm create vue@latest admin cd admin npm install element-plus @element-plus/icons-vue axios

脚手架会交互式询问是否启用 TypeScript、Router、Pinia,这一步建议全部选是。后面要用vue-router的守卫做登录拦截,用 Pinia 存用户信息和菜单权限,TypeScript 则让接口返回的结构在编码期就暴露问题。axios选它是因为后台管理系统跑在浏览器里,生态比uni.request更顺手。

main.js里全量注册 Element Plus:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' const app = createApp(App) app.use(ElementPlus, { locale: zhCn }) app.mount('#app')

locale: zhCn的作用是把分页器、日期选择器这类组件自带文案切成中文,不配的话默认是英文。全量引入对后台项目足够,首屏体积敏感时再换unplugin-vue-components按需加载,但需要多配一个 Vite 插件,这里不展开。

4.2 实现路由守卫和动态菜单权限控制

后台系统的权限模型通常分成两个层面:路由访问控制和按钮可见性控制。先说路由层面,用 vue-router 的全局前置守卫:

// router/index.js router.beforeEach((to) => { const token = localStorage.getItem('admin_token') if (!token && to.path !== '/login') { return '/login' } if (token && to.path === '/login') { return '/dashboard' } })

vue-router 4 的守卫可以直接用返回值控制跳转。返回路径字符串就是重定向,什么都不返回或返回 true 表示放行。这里用localStorage存 token 是后台管理系统的通行做法,刷新不丢,只有用户主动退出或清浏览器数据才消失。对比一下,uniapp 小程序端用的是同性质的uni.getStorageSync,只是封装不同。

动态菜单在登录后处理:后端/user/info接口返回当前用户的menus数组,前端把菜单渲染和路由实例解耦。管理员和普通用户的菜单权限通常用角色区分,配合 Node.js 的 JWT payload 里的role字段做判断。

角色可访问菜单说明
admindashboard、用户管理、订单管理、商品管理全部菜单
operatordashboard、订单管理只能处理订单
user无后台菜单只使用小程序端

动态路由的注册用router.addRoute,把有权限的页面组件映射到路由表里。这一步一定要小心顺序:先让用户登录拿到菜单,再动态注册路由,最后跳转目标页面,否则刷新后路由表是空的,页面直接白屏。常规解法是在 Pinia 的 action 里保存menusLoaded状态,守卫发现路由已注册就放行。

4.3 用 axios 封装后台请求,并通过 Vite 代理对接 Node.js

axios 封装思路和 uniapp 的 request 保持一致:

// api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' const http = axios.create({ baseURL: import.meta.env.VITE_ADMIN_API_BASE, timeout: 10000 }) http.interceptors.request.use((config) => { const token = localStorage.getItem('admin_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) http.interceptors.response.use( (res) => { if (res.data.code === 0) return res.data.data ElMessage.error(res.data.message) return Promise.reject(res.data) }, (err) => { if (err.response?.status === 401) { localStorage.removeItem('admin_token') location.href = '/login' } ElMessage.error('网络错误') return Promise.reject(err) } )

baseURL在开发阶段可以直接配成 Node.js 服务地址,但更干净的做法是用 Vite 的代理把/api转发到后端,这样前端的请求是同源的,从根上避开跨域:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://127.0.0.1:3000', changeOrigin: true } } } })

target指向 Node.js 服务的地址和端口,changeOrigin让后端收到请求时 Host 头变成127.0.0.1:3000,避免个别环境下的 Host 校验拦截。这样配置的好处是:联调阶段后端没部署时,前端已经可以并行开发,两边的产出最后通过接口文档对齐即可。

5. MySQL 表结构设计与三端联调:建表、数据验证和部署前清单

5.1 三张核心表的建表要点(以 users 表为例)

一个最小可用的全栈项目至少需要用户表、商品表、订单表。用户表是登录入口,字段设计的容忍度最低:

CREATE TABLE users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, phone VARCHAR(20) NOT NULL UNIQUE COMMENT '手机号,登录账号', password VARCHAR(100) NOT NULL COMMENT 'bcrypt 哈希值', nickname VARCHAR(50) NOT NULL DEFAULT '' COMMENT '昵称', avatar VARCHAR(255) DEFAULT NULL COMMENT '头像 URL', role TINYINT NOT NULL DEFAULT 1 COMMENT '1 普通用户 2 管理员', status TINYINT NOT NULL DEFAULT 1 COMMENT '1 正常 0 禁用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

phone加唯一索引保证登录账号不重复;password字段长度设 100 是因为 bcrypt 哈希输出 60 字符,留出余量;created_atupdated_at交给数据库维护,后端不手动塞时间。存储引擎用 InnoDB,因为订单和商品库存之间要事务支持。字符串统一用utf8mb4,否则用户的 emoji 昵称写入会报错。订单表的金额字段用DECIMAL(10,2)而不是 FLOAT,固定位数的浮点运算在商品金额上是不可接受的。

5.2 用 mysql workbench 做三端数据一致性验证

三端联调时最隐蔽的问题,是接口返回的数据和表里实际数据对不上。联调完一个接口,我会打开 mysql workbench 跑一遍对应的聚合查询:

SELECT u.phone, u.nickname, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id;

如果这条 SQL 的结果和后台管理系统页面显示的数字不一致,那 90% 是后端查询语句的 join 条件写错了。用可视化工具还有一层好处:能一眼看出字段类型选得是否合理。全栈项目里业务逻辑集中在 Node.js 层,尽量不写存储过程和触发器,否则排查问题时要在两套语言之间来回跳。

5.3 部署前逐项核对三端配置

上线前检查清单要具体到每端的配置项:小程序端的VITE_API_BASE必须指向正式服务器域名且为 https,微信小程序正式版不认 http;Vue 后台的VITE_ADMIN_API_BASE同样切换到 https 地址;Node.js 端的JWT_SECRET换成生产随机串,DB_PASSWORD改成高强度密码,.env文件确认加入了.gitignore。最后在微信开发者工具上传代码后,用真机把登录、列表、下单这条主链路完整走一遍,再回到 Vue 后台确认这条订单的状态和金额一致,三端数据全部对上,这个全栈项目才算真正交付。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 2:58:07

武汉乡镇面域shp数据处理:WGS84坐标转换与空间关联实战

简介&#xff1a;武汉全市乡镇行政区划面域shp数据&#xff08;WGS-84坐标系&#xff0c;2024最新&#xff09;面向GIS数据处理、城市规划与空间分析人员&#xff0c;用于地图制作、区域查询、面积计算及多源地理数据叠加分析。压缩包内共7个文件&#xff0c;包含shp矢量面域、…

作者头像 李华
网站建设 2026/9/17 2:58:03

OpenClaw工具调用可测试性实践:模拟、注入与回放机制

OpenClaw 开源之后&#xff0c;我注意到圈子里讨论最多的并不是它接了多少个平台、支持多少个模型&#xff0c;而是另一个更现实的问题&#xff1a;大家都在用工具调用&#xff08;Function Calling / Tool Use&#xff09;&#xff0c;但一遇到"模型这次调参数顺序变了&q…

作者头像 李华
网站建设 2026/9/17 2:58:01

基于YOLOv8的无人机射频信号检测:数据集解析与时频图训练实践

简介&#xff1a;无人机频射信号检测数据集是一套面向目标检测与无人机监管的YOLOv8格式标注数据&#xff0c;适用于计算机视觉、无人机反制、低空安全等领域的学习者与算法工程师。压缩包共729个文件&#xff0c;包括364张jpg原始图片、364个txt标注文件及1个yaml配置文件&…

作者头像 李华
网站建设 2026/9/17 2:55:37

2027单片机毕设选题清单:50个落地型题目与答辩硬核细节

1. 这份清单为什么值得你花15分钟认真读完单片机毕设最让人头疼的不是写代码&#xff0c;而是选题——刚打开学校毕设系统&#xff0c;发现“基于STM32的智能小车”已经排在第37位&#xff0c;“温湿度监控系统”同组6人撞了4个。去年我带过3个学院的毕设指导&#xff0c;翻过2…

作者头像 李华
网站建设 2026/9/17 2:55:18

Vue+SpringBoot健身跟踪系统开发与AI技术应用

1. 项目概述这个健身跟踪系统项目结合了Vue前端框架和SpringBoot后端技术&#xff0c;并融入了智能AI技术来实现个性化的健身追踪与管理。作为一名长期从事全栈开发的工程师&#xff0c;我发现这类系统正在成为健康科技领域的热门方向——它不仅能解决传统健身APP数据单一、反馈…

作者头像 李华