Node+Express+MySQL电影平台后端实战指南

发布时间:2026/9/16 5:17:37
Node+Express+MySQL电影平台后端实战指南 简介这是一套基于Node.js全栈技术实现的猫眼电影平台源码面向Web开发初学者与全栈进阶学习者帮助其掌握Express框架路由设计、MySQL数据建模、前后端交互及静态资源组织等核心能力。资源共37个文件含28张PNG涵盖电影海报、界面图标、UI组件等、5个HTML构成首页、列表页、详情页等核心视图、1个CSS定义全局样式、1个JS处理前端交互逻辑、1个ICO网站favicon及1份readme.txt说明文档压缩包仅266KB轻量易读。已有246人学习下载适合快速部署调试与代码拆解。读者可完整复现一个具备电影展示、搜索筛选、用户界面渲染功能的轻量级购票平台深入理解static资源目录划分、HTML模板嵌套结构、MySQL表关系设计如电影/用户/订单并获得可直接运行的server.js服务入口与清晰的项目启动指引。1. 这不是静态页面而是一个可运行的电影平台最小可行后端——NodeExpressMySQL 三件套跑通即用你下载的这个upload.zip表面看是37个零散文件一堆PNG图标、5个HTML、1个CSS、1个JS、1个README甚至还有favicon.ico和V3.png这类细节图。但真正让它“活起来”的是那个不起眼的server.js——它才是整个猫眼电影平台的中枢神经。这不是一个纯前端演示站而是一个完整闭环的轻量级服务端应用用户在cinema.html点击电影请求发到server.jsExpress 路由解析后查 MySQL 表比如movies或tickets再把 JSON 或渲染后的 HTML 返回浏览器。整个链路不依赖任何云服务或第三方 API本地装好 Node、启动 MySQL、执行node server.js端口3000就能打开真实交互界面。适合刚学完 Express 基础路由、想立刻验证“数据库连上了没”“POST 请求能不能写入”的开发者也适合需要快速搭建内部影讯管理后台的运维或产品同学——删掉购票逻辑、保留电影列表和搜索20 分钟就能改成部门团建排片系统。2. 从server.js入手解析 Express 路由与 MySQL 查询的耦合逻辑2.1server.js的核心结构四层职责清晰分离打开server.js你会发现它没有用require(mysql2)而是require(mysql)这是关键线索。当前主流推荐mysql2支持 Promise 和预编译语句但本项目用原生mysql模块意味着所有数据库操作都基于回调函数。典型结构如下const express require(express); const mysql require(mysql); const app express(); // 1. 数据库连接池配置注意密码为空host 为 localhost const db mysql.createPool({ host: localhost, user: root, password: , database: maoyan_db, port: 3306, waitForConnections: true, connectionLimit: 10 }); // 2. 静态资源托管对应 static/ 目录 app.use(express.static(static)); // 3. 解析 POST 请求体关键否则表单提交拿不到数据 app.use(express.urlencoded({ extended: true })); app.use(express.json()); // 4. 核心路由/api/movies 返回全部电影/api/movie/:id 查单部 app.get(/api/movies, (req, res) { db.query(SELECT * FROM movies ORDER BY score DESC, (err, results) { if (err) return res.status(500).json({ error: err.message }); res.json(results); }); });提示express.urlencoded({ extended: true })必须放在路由定义之前否则req.body为空对象。这是新手最常踩的坑——页面表单提交后console.log(req.body)显示{}实际是中间件加载顺序错了。2.2 MySQL 表结构逆向还原从 SQL 查询语句反推建表语句项目没提供.sql导出文件但server.js里的查询语句暴露了表名和字段。例如SELECT * FROM movies ORDER BY score DESC→ 表名movies含score字段INSERT INTO tickets (user_id, movie_id, seat, time) VALUES (?, ?, ?, ?)→ 表名tickets字段user_id,movie_id,seat,timeSELECT * FROM users WHERE phone ?→ 表名users含phone字段据此可还原基础建表语句直接复制执行-- 创建数据库若不存在 CREATE DATABASE IF NOT EXISTS maoyan_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库 USE maoyan_db; -- 电影表 CREATE TABLE movies ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, director VARCHAR(50), actors TEXT, score DECIMAL(2,1) DEFAULT 0.0, duration INT COMMENT 单位分钟, release_date DATE, poster_url VARCHAR(200) COMMENT 对应 static/images/ 下的 PNG 文件名 ); -- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL, phone VARCHAR(11) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL COMMENT 应为 SHA256 加密存储 ); -- 订票记录表 CREATE TABLE tickets ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, movie_id INT NOT NULL, seat VARCHAR(10) NOT NULL COMMENT 如 A12, B05, time DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (movie_id) REFERENCES movies(id) );注意poster_url字段存的是相对路径如images/yan.png前端cinema.html中img srcstatic/{{poster_url}}才能正确加载。若你把图片放在static/外路径会 404。2.3 Express 中间件链为什么/list.html能直接访问而/api/movies却返回 JSON项目中list.html是纯静态页通过app.use(express.static(static))自动映射到http://localhost:3000/list.html。但server.js里还定义了显式路由app.get(/list.html, (req, res) { res.sendFile(__dirname /list.html); });这看似冗余实则关键express.static默认只服务static/目录下的文件而list.html在根目录必须显式sendFile。同理cinema.html、movie.html都需此处理。而/api/movies这类路径未被static拦截自然落到 Express 路由处理器返回 JSON 数据供前端 AJAX 调用。路径处理方式返回内容前端调用场景/list.htmlres.sendFile()HTML 文本页面首次加载/static/images/yan.pngexpress.static(static)PNG 二进制img标签自动请求/api/movies自定义路由 db.query()JSON 数组fetch(/api/movies).then(data renderList(data))3. 前端交互实战用cinema.htmlmaoyan.cssindex.js实现动态电影列表3.1cinema.html的 DOM 结构与数据绑定模式cinema.html是主入口页其核心是div idmovie-list/div容器。它不包含任何硬编码电影数据而是依赖 JavaScript 动态渲染。查看index.js注意项目正文提到只有1个 JS 文件即此文件// index.js document.addEventListener(DOMContentLoaded, () { loadMovies(); // 页面加载完立即拉取数据 }); function loadMovies() { fetch(/api/movies) .then(res res.json()) .then(movies { const container document.getElementById(movie-list); container.innerHTML movies.map(movie div classmovie-card img srcstatic/${movie.poster_url} alt${movie.title} h3${movie.title}/h3 p导演${movie.director || 未知}/p p评分span classscore${movie.score}/span/p button onclickbookTicket(${movie.id})购票/button /div ).join(); }) .catch(err console.error(加载电影失败:, err)); } function bookTicket(movieId) { // 弹出购票表单简化版 const seat prompt(请输入座位号如 A12); if (!seat) return; fetch(/api/book, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ movie_id: movieId, seat: seat }) }) .then(res res.json()) .then(data alert(data.success ? 购票成功 : data.error)); }逻辑说明fetch(/api/movies)对应后端app.get(/api/movies)movie.poster_url直接拼接成static/images/yan.png与static/目录结构严格匹配。若你把yan.png放错位置这里就会 404。3.2maoyan.css的响应式布局技巧适配移动端电影海报栅格maoyan.css采用传统 float 布局而非 Flex/Grid因项目较老但对电影卡片做了精准控制.movie-card { width: 200px; margin: 10px; float: left; text-align: center; border: 1px solid #eee; border-radius: 4px; overflow: hidden; } .movie-card img { width: 100%; height: 260px; /* 固定高度避免海报比例不一导致错位 */ object-fit: cover; /* 关键裁剪居中显示 */ } /* 移动端适配小屏下每行只显示2张 */ media (max-width: 768px) { .movie-card { width: 140px; margin: 5px; } }参数说明object-fit: cover确保不同尺寸的 PNG 海报如yan.png400×600mengban.png300×400都能填满卡片且不拉伸变形。若去掉此属性矮胖海报会被强行拉高人物脸变宽。3.3readme.txt的隐藏配置项启动前必须修改的三个参数readme.txt内容虽短但包含三个必须手动修改的配置点原文未明说但从代码可推断MySQL 密码server.js中password: 生产环境必须改为强密码数据库名database: maoyan_db需确保该库已按 2.2 节创建端口号app.listen(3000)若 3000 被占用需改为app.listen(3001)并同步更新前端 AJAX 地址验证是否生效启动后访问http://localhost:3000/api/movies应返回 JSON 数组若返回Cannot GET /api/movies检查server.js是否在根目录、Node 版本是否 ≥ 14本项目兼容 Node 12。4. 排查常见启动失败从端口冲突到 MySQL 连接拒绝的逐层诊断4.1 启动报错Error: connect ECONNREFUSED 127.0.0.1:3306的根因与修复这是最典型的错误表示 Express 找不到 MySQL 服务。不要先怀疑代码按以下顺序排查确认 MySQL 服务已运行Windows任务管理器 → 服务 → 找到MySQL80或mysqld状态应为“正在运行”macOS/Linux终端执行brew services list | grep mysqlHomebrew或sudo systemctl status mysql验证 MySQL 登录凭据mysql -u root -p # 输入密码若为空则直接回车 # 成功进入 MySQL 命令行后执行 SHOW DATABASES; # 应看到 maoyan_db检查server.js中的 host 是否为127.0.0.1某些 MySQL 安装默认只监听::1IPv6需在 MySQL 配置文件my.cnf中添加[mysqld] bind-address 0.0.0.0然后重启 MySQL 服务。提示若你用 Docker 运行 MySQLhost应改为容器名如mysql-container而非localhost——因为 Node 进程在宿主机localhost指向宿主机而非容器。4.2 页面空白或图片 404静态资源路径的双重陷阱cinema.html中图片显示为叉号检查两个层面第一层文件物理路径server.js的app.use(express.static(static))要求所有静态资源CSS、JS、图片必须放在static/目录下。但项目文件列表显示images/是static的子目录所以yan.png的真实路径是static/images/yan.png。若你解压时把images放到项目根目录img srcstatic/images/yan.png就会 404。第二层HTML 中的引用路径cinema.html里写的是img srcstatic/images/yan.png这没问题但list.html若写成img srcimages/yan.png漏掉static/就会失败。统一原则所有src和href必须以static/开头。4.3 表单提交后无反应req.body为空的中间件缺失诊断当点击“购票”按钮bookTicket()发送 POST 请求但后端req.body为空movie_id取不到。原因必然是// ❌ 错误中间件放在路由之后 app.get(/api/movies, ...); app.use(express.urlencoded({ extended: true })); // 位置错了 // ✅ 正确必须在所有路由之前 app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.get(/api/movies, ...);验证方法在路由处理器中加一行console.log(Body:, req.body)启动服务后提交表单观察终端输出。若输出{}就是中间件顺序问题。5. 进阶技巧用mysql2替换原生mysql模块并启用 Promise 风格查询5.1 为什么必须升级原生mysql的回调地狱与安全风险原项目用mysql模块每个查询都要嵌套回调db.query(SELECT * FROM movies WHERE id ?, [id], (err, movie) { if (err) throw err; db.query(SELECT * FROM tickets WHERE movie_id ?, [movie.id], (err, tickets) { // 第二层嵌套... }); });这导致三层以上关联查询时代码难以维护且易出现callback called multiple times错误。更严重的是原生mysql对 SQL 注入防护较弱?占位符虽基础防护但复杂条件拼接仍可能漏掉。5.2 三步完成mysql2迁移安装、连接池重构、Promise 化查询第一步安装新模块npm uninstall mysql npm install mysql2第二步重写数据库连接server.js开头const mysql require(mysql2/promise); // 注意导入 promise 版本 const db mysql.createPool({ host: localhost, user: root, password: your_secure_password, database: maoyan_db, waitForConnections: true, connectionLimit: 10, queueLimit: 0 });第三步将回调查询改为 await/async// 原回调写法删除 // app.get(/api/movie/:id, (req, res) { // db.query(SELECT * FROM movies WHERE id ?, [req.params.id], (err, results) { // res.json(results[0]); // }); // }); // 新 Promise 写法替换 app.get(/api/movie/:id, async (req, res) { try { const [rows] await db.execute(SELECT * FROM movies WHERE id ?, [req.params.id]); res.json(rows[0]); } catch (err) { res.status(500).json({ error: err.message }); } });参数说明mysql2/promise的execute()返回[rows, fields]元组[rows]解构直接获取结果数组await让异步代码线性书写避免回调嵌套。同时mysql2默认启用typeCast类型转换数字字段不再返回字符串省去parseInt()步骤。5.3 安全加固为用户登录增加 bcrypt 密码哈希当前users表password字段明文存储从readme.txt提示可推断必须加密。在server.js中加入npm install bcryptjsconst bcrypt require(bcryptjs); // 用户注册时假设 POST /api/register app.post(/api/register, async (req, res) { const { username, phone, password } req.body; const hashedPassword await bcrypt.hash(password, 10); // 10 轮 salt db.execute( INSERT INTO users (username, phone, password) VALUES (?, ?, ?), [username, phone, hashedPassword] ); }); // 登录验证时 app.post(/api/login, async (req, res) { const { phone, password } req.body; const [users] await db.execute(SELECT * FROM users WHERE phone ?, [phone]); if (users.length await bcrypt.compare(password, users[0].password)) { res.json({ success: true, user: { id: users[0].id, username: users[0].username } }); } else { res.status(401).json({ error: 手机号或密码错误 }); } });至此这个猫眼电影平台源码就从“能跑”升级为“可交付”。你不仅掌握了 Express 路由与 MySQL 的基础集成更获得了排查真实线上问题的能力——端口、路径、中间件顺序这些细节决定项目生死。下一步把tickets表加上唯一索引防止重复购票ALTER TABLE tickets ADD UNIQUE KEY unique_seat_time (movie_id, seat, time);本文还有配套的精品资源点击获取