核心目标
- 掌握 Node.js 后端必备核心语法;
- 掌握 Node.js 与 MySQL 的连接、CRUD 全流程;
1.NodeJS核心知识回顾
1.1 认识NodeJS
在现有的知识储备中(JavaScript、Html、Css、MySQL、Node)中,我们能够知道MySQL是存储数据的
而JS+H5+C3是负责做页面设计的
那么我们不禁思考一个问题:前端页面展示的数据,是如何从后端数据库中拿到的?
这就涉及到了我们经常听过,但目前实操经验还欠缺的:前后端分离/不分离开发,其核心流程如下
所以我们接下来的思路,就是选择其中的Node来连接后端数据库,实现数据获取与修改,完整前后端开发
1.2 NodeJS安装
安装包下载
我们有两种安装方式,第一种从官网下载:
第二种,根据我们提前提供好的资料安装包:
本地安装
- 参考提供的安装手册即可,注意:安装路径选择英文、没有空格的目录
环境验证
- win + R,输入cmd
- 在命令控制台,输入命令:
node -v,有输出即可。这里版本不一定跟我本地保持一致,可以高/低于我的
1.3 Node核心语法回顾
1. VsCode安装
在完成上述的Node环境安装之后,我们接下来通过几个核心语法,回顾一下js、node知识。工欲善其事必先利其器,所以我需要确保你本地安装了Ide软件,即VsCode。你依然有两种安装方式
- 官方下载、安装
https://code.visualstudio.com/
- 根据我所提供的安装包,解压安装
2. 新建本地工程
我们可以在任意非中文目录下,新建一个文件夹,用于后续存放代码,如下我就创建在桌面
然后,我们用VsCode,打开当前文件夹即可
3. 运行Node.js代码
如果我们需要编写并运行一段js代码,实际非常简单,只需要创建一个后缀为js的文件,然后输出对应的文本
console.log('Hello World')
4. 掌握代码拆分复用
在复杂的工作场景中,往往我们需要调用别人写好的代码,比如我现在需要
- 将2025-12-12 17:00:00这种年月日时分秒的格式,转换成只要年月日
- 计算两个数值的求和、求差、开方、求根、求导、微分等等函数计算场景
如果多个地方都需要,我不可能每个地方都编写一份重复代码,因此我开始考虑做一下代码的复用
这也是整个编程领域非常关注的一个点:高内聚、低耦合
为了完成代码的复用,我们就需要做一下代码的:封装处理,这里我们举一个简单的例子来完成当前思想的实践
- 现在A、B两个调用方都需要完成一个求和的计算,因此就需要封装一个求和的函数,这里我们叫
util.js
function sum(a, b) { return a + b; } module.exports = { sum };
然后我们就可以在需要使用的地方完成代码的调用,如A.js
const { sum } = require('./util'); console.log(sum(2, 3));
代码结构如下:
4.1. 案例练习
- 封装一个函数,用于计算商品的价格,用户会输入:单价、数量、优惠金额
- 定义一个调用文件,调用上面的函数,完成价格计算
这个案例对于大家不难,但我们着重关注这个编程思想:封装、代码复用
5. 掌握异步函数实现
在上面的案例中,我们的调用思路都是阻塞式的执行,即执行完之后,必须等对方返回结果。这种方式的调用我们也称之为:同步调用。同步的优势在于实时性强、能立马得到结果,缺点就是性能较差,阻塞等待结果。
而异步调用,就很好的解决了这个问题,这里我们模拟一个:查询用户的等待异步执行的效果。
function sum(a, b) { return a + b; } function getUser() { return new Promise(resolve => setTimeout(() => resolve('张三'), 1000)) } module.exports = { sum, getUser };
然后定义一个新的调用函数,以便做接口测试
const { getUser } = require('./util'); // 使用 async/await 方式调用异步函数 async function fetchUser() { try { const user = await getUser(); console.log('获取到用户:', user); } catch (error) { console.error('获取用户失败:', error); } } fetchUser();
解释一下这里的三个核心关键词的作用
- Promise:给一步函数打包,统一返回结果。类似于点单之后拿到取餐号
- async:声明一下我这个函数式有一步操作,给函数打个标签,内部有一个等待的异步操作
- await:等取餐号叫到你拿到结果。只能在async修饰的函数里,等
Promise完成,拿到resolve的结果
异步操作(如数据库查询) → 用 Promise 封装 → 在 async 函数里用 await 等待 → 优雅拿到结果/捕获错误
5.1. 案例练习
- 封装一个函数,查询购物车里面的商品,返回任意商品即可
- 封装一个异步调用函数,完成购物车商品的查询
2.MySQL核心知识回顾
2.1 MySQL安装
如果你本地已经有了MySQL,可以直接忽略本小节,如果没有,这里我也提供好了对应的安装包
安装好了之后,我们需要用一个可视化界面工具来使用,可以用任意你熟悉的,如Navicat、SqlYog、DataGrip
2.2 SQL
SQL是一门操作关系型数据库的编程语言,定义操作所有关系型数据库的统一标准。
分类 |
全称 |
说明 |
DDL |
Data Definition Language |
数据定义语言,用来定义数据库对象(数据库,表,字段) |
DML |
Data Manipulation Language |
数据操作语言,用来对数据库表中的数据进行增删改 |
DQL |
Data Query Language |
数据查询语言,用来查询数据库中表的记录 |
DCL |
Data Control Language |
数据控制语言,用来创建数据库用户、控制数据库的访问权限 |
1.DDL-数据库
-- 查询所有数据库 show databases; -- 查询当前数据库 select database(); -- 使用/切换数据库 use 数据库名; -- 创建数据库 create database [if not exists] 数据库名 [default charset utf8mb4]; -- 删除数据库 drop database [if exists] 数据库名;
上述语法的database也可换成schema。如:create schema db01; MySQL8版本中,默认字符集为utf8mb4
utf8mb4是一种能支持emoji表情包(😌)的存储方式
2.DDL-表结构
create table tablename( 字段1 字段类型 [约束] [comment 字段1注释], ...... 字段2 字段类型 [约束] [comment 字段2注释] )[comment 表注释];
这里,我们结合一个实际页面做一个需求分析,尝试解读他的表结构是什么样(给大家5min时间)
现在我来带领大家一起分析一下
字段 |
字段类型 |
字段长度 |
字段描述 |
id |
int |
20 |
数据唯一ID |
name |
varchar |
255 |
武器名称(AK-47、M4A1等) |
baseWeapon |
varchar |
255 |
武器型号(消音型) |
price |
decimal |
10,2 |
武器价格 |
appearance |
varchar |
50 |
外观:崭新出厂、久经沙场、略有磨损 |
category |
varchar |
20 |
类别(武器皮肤、印花) |
quality |
varchar |
20 |
品质(全息、闪耀、冠军等) |
isCollectible |
tinyint |
1 |
收藏品:是(1)、否(0) |
imgUrl |
varchar |
500 |
商品图片(存储图片 URL) |
stock |
int |
10 |
在售数量 |
有了这个之后,我们就需要结合上面的语法,在DataGrip里完成表结构的创建
- 在自己的数据库连接下,右键-创建表
- 声明表名,这里我用:
weapon,你可以随意
- 声明表字段,如上分析的,依次填写,这里我不再一一截图
- 声明一个表的唯一主键,用于标识数据
- 所有字段填写完毕后,点击ok,此时我们的表就创建好了(这里我只填写了2个)
案例练习
- 请你完成我们本次实训需要开发的游戏饰品前后端开发项目的表结构分析设计,页面原型图如下
案例讲解
- 课堂实施,最终分析下来的SQL语句如下:
-- 3. 创建武器皮肤表(完全匹配字段表) CREATE TABLE IF NOT EXISTS weapon_skins ( -- 数据唯一ID id INT(20) NOT NULL AUTO_INCREMENT COMMENT '数据唯一ID', -- 武器名称(如AK-47、M4A1) name VARCHAR(255) NOT NULL COMMENT '武器名称(AK-47、M4A1等)', -- 武器型号(如消音型) baseWeapon VARCHAR(255) NOT NULL COMMENT '武器型号(消音型)', -- 武器价格(10位长度、2位小数) price DECIMAL(10,2) NOT NULL COMMENT '武器价格', -- 外观状态 appearance VARCHAR(50) NOT NULL COMMENT '外观:崭新出厂、久经沙场、略有磨损', -- 商品类别 category VARCHAR(20) NOT NULL COMMENT '类别(武器皮肤、印花)', -- 品质等级 quality VARCHAR(20) NOT NULL COMMENT '品质(全息、闪耀、冠军等)', -- 收藏品标识(1=是、0=否) isCollectible TINYINT(1) NOT NULL DEFAULT 0 COMMENT '收藏品:是(1)、否(0)', -- 商品图片URL imgUrl VARCHAR(500) DEFAULT '' COMMENT '商品图片(存储图片URL)', -- 在售数量 stock INT(10) NOT NULL DEFAULT 0 COMMENT '在售数量', -- 主键约束 PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='武器皮肤信息表(匹配字段表设计)';
3.DML-新增数据
语法如下,我们依次按照下面语法完成上述表结构数据的新增
-- 指定字段添加数据 insert into 表名(字段名1, 字段名2) values (值1, 值2); -- 全部字段添加数据 insert into 表名 values (值1, 值2, ...); -- 批量添加数据(指定字段) insert into 表名 (字段名1, 字段名2) values (值1, 值2), (值1, 值2); -- 批量添加数据(全部字段) insert into 表名 values (值1, 值2, ...), (值1, 值2, ...);
⏰注意:
- 插入数据时,指定的字段顺序需要与值的顺序是一一对应的 。
- 字符串和日期型数据应该包含在引号中(单引号、双引号都可以)。
- 插入的数据大小/长度,应该在字段的规定范围内 。
4.DML-删除数据
语法如下,我们按照下面语法完成上述表结构数据的删除
-- 删除数据 delete from 表名 [where 条件];
⏰注意:
- DELETE 语句的条件可以有,也可以没有,如果没有条件,则会删除整张表的所有数据。
- DELETE 语句不能删除某一个字段的值(如果要操作,可以使用UPDATE,将该字段的值置为NULL)。
🤓思考一个问题:项目正式上线之后,你是否会删除这条数据?
- 会:数据就真的没了
- 这种,我们称之为:物理删除
- 不会:那怎么实现删除的效果呢
- 这种,我们称之为:逻辑删除
5.DML-修改数据
语法如下,我们按照下面语法完成上述表结构数据的更新
-- 修改数据 update 表名 set 字段名1 = 值1 , 字段名2 = 值2 , .... [ where 条件 ] ;
⏰注意:
- 修改语句的条件可以有,也可以没有,如果没有条件,则会修改整张表的所有数据。
6.DQL-查找数据
|
一个完整的DQL查询包含以下几个部分
|
基本查询
-- 查询多个字段 select 字段1,字段2,字段3 from 表名; -- 查询所有字段(通配符) select * from 表名; -- 为查询字段设置别名,as关键字可以省略 select 字段1 [as 别名1], 字段2 [as 别名2] from 表名; -- 去除重复记录 select distinct 字段列表 from 表名;
* 号代表查询所有字段,在实际开发中尽量少用(不直观、影响效率)。
案例练习:
- 请你查找出当前武器表里面的:图片、价格、数量
条件查询
-- 条件查询 select 字段列表 from 表名 where 条件列表 ;
比较运算符 |
功能 |
> |
大于 |
>= |
大于等于 |
< |
小于 |
<= |
小于等于 |
= |
等于 |
<> 或 != |
不等于 |
between ... and ... |
在某个范围之内(含最小、最大值) |
in(...) |
在in之后的列表中的值,多选一 |
like 占位符 |
模糊匹配(_匹配单个字符, %匹配任意个字符) |
is null |
是null |
逻辑运算符 |
功能 |
and 或 && |
并且 (多个条件同时成立) |
or 或 || |
或者 (多个条件任意一个成立) |
not 或 ! |
非 , 不是 |
案例练习:
- 基础比较与逻辑组合查出当前武器表中:
- 价格大于 200 元,且外观为 “崭新出厂”,且品质是 “全息” 或 “冠军” 的武器
- 范围与否定逻辑查出当前武器表中:
- 库存数量在 10 到 50 之间(含 10 和 50),且类别不是 “印花”,且标签不包含 “略有磨损” 的武器
- 模糊匹配与空值判断查出当前武器表中:
- 名称以 “AK-” 开头(如 AK-47、AK-74),且评价数量不为空的武器
- 多条件组合与枚举匹配查出当前武器表中:
- 武器型号在“M4A1”“AWP”“P2000”列表中,且价格小于 100 元,且库存数量不等于 0 的武器
- 复杂逻辑嵌套查出当前武器表中:
- 价格在 500 元以上且品质为闪耀 或者价格在 100-300 元之间且外观为久经沙场,且标签不为空的武器
分组查询
函数 |
功能 |
count |
统计数量 |
max |
最大值 |
min |
最小值 |
avg |
平均值 |
sum |
求和 |
案例练习:
- 查找出价格最大的武器
- 查找所有武器的平均价格
- null值不参与所有聚合函数的运算 。
- 统计数量可以使用:count(*) count(字段) count(常量),推荐使用count(*) 。
除了上述的分组函数,还有两个关键字也是经常使用的
-- 分组查询 select 字段列表 from 表名 [where 条件列表] group by 分组字段名 [having 分组后过滤条件];
where与having的区别:
执行时机不同:where是分组之前进行过滤,不满足where条件,不参与分组;
而having是分组之后对结果进行过滤。判断条件不同:where不能对聚合函数进行判断,而having可以。
比如我要统计每个基础武器型号(baseWeapon)的平均价格,但只关注:
- 外观为 “崭新出厂” 的商品(分组前过滤)
- 平均价格超过 200 元的武器型号(分组后过滤)
假设表结构核心字段如下
字段名 |
说明 |
baseWeapon |
基础武器型号(如 AK-47、M4A1) |
appearance |
外观状态(如 “崭新出厂”“久经沙场”) |
price |
单价(元) |
则对应的sql如下,表示:外观为崭新出厂的 AWP 和 M4A1,平均价格均超过 200 元
-- 统计外观为“崭新出厂”、且平均价格超200元的武器型号及其平均价格 SELECT baseWeapon, AVG(price) AS avg_price -- 计算每个武器型号的平均价格 FROM weapon_skins WHERE appearance = '崭新出厂' -- 分组前过滤:只保留外观为崭新出厂的记录 GROUP BY baseWeapon -- 按武器型号分组 HAVING AVG(price) > 200; -- 分组后过滤:只保留平均价格超200元的组
排序查询
-- 排序查询 select 字段列表 from 表名 [where 条件列表] order by 排序字段 排序方式;
- 排序方式:升序(asc),降序(desc);默认为升序asc,是可以不写的
- 如果是多字段排序,当第一个字段值相同时,才会根据第二个字段进行排序
案例练习:
查询 类别为 “武器皮肤” 且品质为 “闪耀” 的商品,要求:
- 只显示商品名称(name)、基础武器型号(baseWeapon)、价格(price)、库存(stock)这 4 个字段
- 按价格(price)从高到低排序(价格相同的情况下,按库存从低到高排序)
select name, baseWeapon, price, stock from weapon_skins where category = '武器皮肤' and quality = '闪耀' order by price desc, -- 第一排序字段:价格降序 stock asc; -- 第二排序字段:库存升序(价格相同时生效)
分页查询
select 字段 from 表名 [where 条件] [order by 排序字段] limit 起始索引,查询记录数;
😏说明:
- 起始索引从0开始,起始索引 = (查询页码 - 1)* 每页显示记录数。
- 分页查询是数据库的方言,不同的数据库有不同的实现,MySQL中是LIMIT。
- 如果查询的是第一页数据,起始索引可以省略,直接简写为 limit 10。
案例练习:
查询 类别为 “印花” 且外观为 “略有磨损” 的商品,要求:
- 只显示商品名称(name)、价格(price)、库存(stock)这 3 个字段;
- 按价格从低到高排序;
- 分页参数:每页显示 15 条记录,查询第 3 页的数据。
select name, price, stock from weapon_skins where category = '印花' and appearance = '略有磨损' order by price asc limit 30, 15; -- 起始索引=(3-1)*15=30,查询15条记录
3.NodeJS连接数据库实现增删改查
3.1 工程搭建
- 新建一个文件夹,即工程名称:weapon
- vscode打开当前文件夹,此时如下
- 执行命令创建本地
package.json文件,后续用于描述项目信息、管理依赖、指定工程入口等
npm init -y
- 执行命令,用于后续连接mysql
npm install mysql2 dotenv
3.2 nodejs连接mysql
- 新建文件夹
config,后续存放所有配置文件 - 在工程目录下用于存储数据库配置信息:
.env。单独定义一个文件的目的也是为了解耦合。
# .env 文件 DB_HOST=localhost DB_PORT=3306 DB_USER=root # 你的 MySQL 用户名 DB_PASSWORD=123456 # 你的 MySQL 密码 DB_NAME=weapon_skins_db # 目标数据库名
此时,目录结构如下
- config文件夹下新建连接数据库的配置文件:
db.js
// config/db.js const mysql = require('mysql2/promise'); require('dotenv').config(); // 加载环境变量,固定写法,会读取项目根目录下的.env文件 // 从环境变量读取配置 const dbConfig = { host: process.env.DB_HOST, port: process.env.DB_PORT, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, charset: 'utf8mb4', }; // 创建连接池 const pool = mysql.createPool(dbConfig); // 测试连接池是否可用 async function testPoolConnection() { try { const connection = await pool.getConnection(); console.log('✅ 数据库连接池创建成功!'); connection.release(); // 释放连接(归还到连接池) } catch (error) { console.error('❌ 数据库连接池创建失败:', error.message); process.exit(1); // 连接失败则退出程序 } } // 暴露连接池和测试函数 module.exports = { pool, testPoolConnection };
3.3 测试数据库查询
- 新建一个文件夹
model,在这个文件夹下新建weaponSkinModel.js文件
// model/weaponSkinModel.js const { pool } = require('../config/db'); // 👉 第1个功能:查询所有武器 async function getAllWeaponSkins() { try { const [rows] = await pool.execute('SELECT * FROM weapon_skins ORDER BY id DESC'); return rows; } catch (error) { console.error('❌ 查询所有数据失败:', error.message); throw error; } } // 只暴露当前开发的功能 module.exports = { getAllWeaponSkins };
- 创建外层
index.js文件,开始对上面代码做测试,此时目录结构如下。注意层级
|
weapon ├─ config/ # 配置文件夹 │ └─ db.js # 数据库连接池配置 ├─ model/ # 数据模型/CRUD 文件夹 │ └─ weaponSkinModel.js # 武器皮肤表 CRUD 逻辑 ├─ .env # 环境变量 ├─ index.js # 入口文件(逐个功能测试) └─ package.json |
- 在
index.js文件中,创建测试代码
// index.js const { testPoolConnection } = require('./config/db'); const weaponSkinModel = require('./model/weaponSkinModel'); // 👉 测试 1:查询所有数据(对应 getAllWeaponSkins 功能) async function testGetAllWeaponSkins() { console.log('===== 开始测试:查询所有数据 ====='); try { // 先测试连接池 await testPoolConnection(); // 调用查询函数 const result = await weaponSkinModel.getAllWeaponSkins(); console.log('查询成功!结果:'); console.log(result.length > 0 ? result : '暂无数据'); } catch (error) { console.log('查询失败!原因:', error.message); } console.log('===== 测试结束:查询所有数据 =====\n'); } // 👉 执行当前测试(只运行这一个) testGetAllWeaponSkins();
- 运行
index.js文件,可以看到控制台输出正常
3.4 测试数据库新增
在上面的案例中,我们完成了查找,但是因为没有数据,所以无法知道查询功能是否真的正常。因此接下来我们就要陆续完成:增删改查的操作了。首先,我们来看一下新增
- 在
weaponSkinModel.js文件中,增加新增数据的js代码
// model/weaponSkinModel.js(新增) // 👉 第2个功能:新增单条数据 async function addWeaponSkin(weaponSkin) { const { name, baseWeapon, skinName, price, appearance, category, stock } = weaponSkin; try { const [result] = await pool.execute( `INSERT INTO weapon_skins (name, baseWeapon, skinName, price, appearance, category, stock) VALUES (?, ?, ?, ?, ?, ?, ?)`, [name, baseWeapon, skinName || '', price, appearance || '未知', category || '武器皮肤', stock || 0] ); return { success: true, insertId: result.insertId, message: '新增成功' }; } catch (error) { console.error('❌ 新增数据失败:', error.message); throw error; } } // 更新暴露(添加新功能) module.exports = { getAllWeaponSkins, addWeaponSkin // 新增 };
- 接下来我们就可以在
index.js文件中测试一下了
// index.js(新增测试函数) // 👉 测试 2:新增单条数据(对应 addWeaponSkin 功能) async function testAddWeaponSkin() { console.log('===== 开始测试:新增单条数据 ====='); // 测试用数据 const testData = { name: 'AK-47 | 火神 (崭新出厂)', baseWeapon: 'AK-47', skinName: '火神', price: 1599.99, appearance: '崭新出厂', category: '武器皮肤', stock: 8 }; try { await testPoolConnection(); const result = await weaponSkinModel.addWeaponSkin(testData); console.log('新增成功!结果:', result); // 额外验证:查询刚新增的数据 const newData = await weaponSkinModel.getWeaponSkinById(result.insertId); console.log('验证新增数据:', newData); } catch (error) { console.log('新增失败!原因:', error.message); } console.log('===== 测试结束:新增单条数据 =====\n'); } // 👉 执行当前测试(注释掉之前的,只运行这一个) // testGetAllWeaponSkins(); testAddWeaponSkin();
- 控制台打印输出正常
- 此时,我们放开
index.js里面的testGetAllWeaponSkins单元测试,做一下查询,可以看到查到数据了
案例练习
- 在上述新增的功能中,尝试不传入name,或者价格为负数,观察是否有错误提示,如果没有请完善提示信息
// 👉 第2个功能:新增单条数据 async function addWeaponSkin(weaponSkin) { const { name, baseWeapon, skinName, price, appearance, category, stock } = weaponSkin; // 基础校验 if (!name || !baseWeapon || !price) { throw new Error('必填项:商品名称、基础武器型号、价格'); } if (price < 0) { throw new Error('价格不能为负数'); } try { const [result] = await pool.execute( `INSERT INTO weapon_skins (name, baseWeapon, skinName, price, appearance, category, stock) VALUES (?, ?, ?, ?, ?, ?, ?)`, [name, baseWeapon, skinName || '', price, appearance || '未知', category || '武器皮肤', stock || 0] ); return { success: true, insertId: result.insertId, message: '新增成功' }; } catch (error) { console.error('❌ 新增数据失败:', error.message); throw error; } }
3.5 测试数据库删除
- 新增删除功能代码
// model/weaponSkinModel.js(新增) // 👉 第3个功能:按 ID 删除数据 async function deleteWeaponSkin(id) { if (!id) throw new Error('ID 不能为空'); try { const [result] = await pool.execute( 'DELETE FROM weapon_skins WHERE id = ?', [id] ); if (result.affectedRows === 0) throw new Error(`未找到 ID=${id} 的数据`); return { success: true, message: `ID=${id} 数据已删除` }; } catch (error) { console.error(`❌ 删除 ID=${id} 失败:`, error.message); throw error; } } // 最终暴露所有功能 module.exports = { getAllWeaponSkins, addWeaponSkin, deleteWeaponSkin // 新增 };
- 在
index.js中增加测试代码
// index.js(新增测试函数) // 👉 测试 3:删除数据(对应 deleteWeaponSkin 功能) async function testDeleteWeaponSkin() { console.log('===== 开始测试:删除数据 ====='); try { await testPoolConnection(); // 执行删除:这里的id需要跟数据库保持一致 const result = await weaponSkinModel.deleteWeaponSkin(2); console.log('删除结果:', result); } catch (error) { console.log('删除失败!原因:', error.message); } console.log('===== 测试结束:删除数据 =====\n'); } // 👉 执行当前测试 testDeleteWeaponSkin();
- 重复上述测试步骤
3.6 测试数据库修改
- 新增修改功能代码
// model/weaponSkinModel.js(新增) // 👉 第4个功能:按 ID 修改价格 async function updateWeaponSkinPrice(id, newPrice) { if (!id) throw new Error('ID 不能为空'); if (typeof newPrice !== 'number' || newPrice < 0) { throw new Error('价格必须为非负数值'); } try { const [result] = await pool.execute( 'UPDATE weapon_skins SET price = ? WHERE id = ?', [newPrice, id] ); if (result.affectedRows === 0) throw new Error(`未找到 ID=${id} 的数据`); return { success: true, message: `价格更新为 ${newPrice} 元` }; } catch (error) { console.error(`❌ 修改 ID=${id} 价格失败:`, error.message); throw error; } } // 更新暴露 module.exports = { getAllWeaponSkins, addWeaponSkin, deleteWeaponSkin, updateWeaponSkinPrice // 新增 };
- 在
index.js中增加测试代码
// 👉 测试 4:修改价格(对应 updateWeaponSkinPrice 功能) async function testUpdateWeaponSkinPrice() { console.log('===== 开始测试:修改价格 ====='); const testId = 3; // 实际存在的 ID const newPrice = 1799.99; try { await testPoolConnection(); const result = await weaponSkinModel.updateWeaponSkinPrice(testId, newPrice); console.log('修改结果:', result); } catch (error) { console.log('修改失败!原因:', error.message); } console.log('===== 测试结束:修改价格 =====\n'); } // 👉 执行当前测试(只运行这一个) // testGetAllWeaponSkins(); // testAddWeaponSkin(); // testDeleteWeaponSkin(1); testUpdateWeaponSkinPrice();
- 控制台和数据库双向验证,发现符合逻辑
案例练习
- 增加一个根据id,扣减库存的功能。函数入参为:id,stock两个参数
// model/weaponSkinModel.js(新增) async function updateWeaponSkinStock(id, newStock) { // 校验 if (!id) throw new Error('ID 不能为空'); if (typeof newStock !== 'number' || newStock < 0) { throw new Error('库存必须为非负整数'); } try { const [result] = await pool.execute( 'UPDATE weapon_skins SET stock = ? WHERE id = ?', [newStock, id] ); if (result.affectedRows === 0) { throw new Error(`未找到 ID=${id} 的数据`); } return { success: true, message: `库存更新为 ${newStock}`, affectedRows: result.affectedRows }; } catch (error) { console.error(`❌ 修改 ID=${id} 库存失败:`, error.message); throw error; } } // 更新暴露 module.exports = { // ***** updateWeaponSkinStock // 新增 };
// index.js(新增测试函数) // 👉 测试:修改库存(对应 updateWeaponSkinStock 功能) async function testUpdateWeaponSkinStock() { console.log('===== 开始测试:修改库存 ====='); const testId = 2; // 用实际存在的 ID const newStock = 15; try { await testPoolConnection(); // 执行修改 const result = await weaponSkinModel.updateWeaponSkinStock(testId, newStock); console.log('修改结果:', result); } catch (error) { console.log('修改失败!原因:', error.message); } console.log('===== 测试结束:修改库存 =====\n'); } // 👉 执行当前测试 testUpdateWeaponSkinStock();
4.今日作业
对 updateWeaponSkinPrice 优化,新增「价格变动日志」:修改价格时,在控制台打印日志如下:
// 👉 第4个功能:按 ID 修改价格(新增价格变动日志) async function updateWeaponSkinPrice(id, newPrice) { if (!id) throw new Error('ID 不能为空'); if (typeof newPrice !== 'number' || newPrice < 0) { throw new Error('价格必须为非负数值'); } // 新增:查询商品旧价格(修复类型问题) let oldPrice; try { const [oldData] = await pool.execute( 'SELECT price FROM weapon_skins WHERE id = ?', [id] ); if (oldData.length === 0) { throw new Error(`未找到 ID=${id} 的数据`); } // 关键修复:将数据库返回的价格转为 Number 类型(兼容 DECIMAL/BigInt) oldPrice = Number(oldData[0].price); } catch (error) { console.error(`❌ 修改 ID=${id} 价格失败:`, error.message); throw error; } try { const [result] = await pool.execute( 'UPDATE weapon_skins SET price = ? WHERE id = ?', [newPrice, id] ); if (result.affectedRows === 0) throw new Error(`未找到 ID=${id} 的数据`); // 核心新增:打印价格变动日志(已修复类型问题) const logTime = formatTime(); // 确保 newPrice 也转为 Number(避免传入字符串类型的数字) const finalNewPrice = Number(newPrice); console.log( `[${logTime}] 管理员修改ID=${id}的商品价格:旧价格${oldPrice.toFixed(2)} → 新价格${finalNewPrice.toFixed(2)}` ); return { success: true, message: `价格更新为 ${finalNewPrice.toFixed(2)} 元`, oldPrice: oldPrice.toFixed(2), newPrice: finalNewPrice.toFixed(2) }; } catch (error) { console.error(`❌ 修改 ID=${id} 价格失败:`, error.message); throw error; } } // 新增:辅助函数 - 格式化时间为「YYYY-MM-DD HH:mm:ss」 function formatTime() { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hour = String(date.getHours()).padStart(2, '0'); const minute = String(date.getMinutes()).padStart(2, '0'); const second = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}:${second}`; }