双招项目后端api全过程实现
1.初始化
点击查看详情
1.1创建项目:
新建
api_server
文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:npm init -y
运行如下的命令,安装特定版本的
express
:pnpm i express@4.17.1
在项目根目录中新建
app.js
作为整个项目的入口文件,并初始化如下的代码:// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// write your code here...
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
console.log('api server running at http://127.0.0.1:3007')
})
1.2配置 cors 跨域&解析表单数据的中间件:
# 1.2.1.首先安装 |
1.3 初始化路由文件&文件夹:
在项目根目录中,新建
router
文件夹,用来存放所有的路由
模块路由模块中,只存放客户端的请求与处理函数之间的映射关系
在项目根目录中,新建
router_handler
文件夹,用来存放所有的路由处理函数模块
路由处理函数模块中,专门负责存放每个路由对应的处理函数
1.4初始化路由模块:
在
router
文件夹中,新建user.js
文件,作为用户的路由模块,并初始化代码如下:const express = require('express')
const router = express.Router()
// 导入处理函数
const userHandler = require('../router_handler/user')
// 注册用户
router.post('/login/code', userHandler.regUser)
// 登录接口
router.post('/login',userHandler.login)
module.exports = router
初始化路由处理函数:
/api_server/router_handler/user.js
// 注册用户的处理函数
exports.regUser = (req, res) => {
res.send('reguser OK')
}
// 登录的处理函数
exports.login = (req, res) => {
res.send('login OK')
}
在
app.js
中,导入并使用用户路由模块
// 导入并注册用户路由模块
const userRouter = require('./router/user')
app.use('/api', userRouter)
2.创建 mysql 数据库
点击查看详情
2.1 新建数据库&表
在 MySQL Shell 中,你已经启动了交互式 Shell,所以不需要再次输入 mysqlsh
进行启动。
下面是正确的步骤来连接到数据库:
\sql |
然后,根据项目要求创建数据库表;
-- 创建用户信息表 |
- 以上代码根据原始项目数据格式,由 chatgpt 快速生成,实际开发中需要做少许调整,例如增加 code 验证码,以及过期时间
实际开发中,根据项目需求,需要对表结构进行修改,以下是增加 column 的方法示例
ALTER TABLE tasklist |
3.为项目后端项目安装 mysql2 模块
点击查看详情
执行以下命令,安装 mysql
pnpm i mysql2
在项目根目录中新建
/db/index.js
文件,在此自定义模块中创建数据库的连接对象:// 导入 mysql 模块
const mysql = require('mysql2/promise');
// 创建数据库连接对象
const db = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'admin123',
database: 'shuangzhao_iooio',
})
// 向外共享 db 数据库连接对象
module.exports = db这里需要特别注意,将来在查询数据库时,返回的结果较之前 mysql 有重大改变:
// mysql 响应结果
[
{ id: 1, name: 'Alice', accounts: 'abc123' },
{ id: 2, name: 'Bob', accounts: 'xyz456' }
]
// mysql2 响应结果
[
// 查询结果:
[
{ id: 1, name: 'Alice', accounts: 'abc123' },
{ id: 2, name: 'Bob', accounts: 'xyz456' }
],
// 查询字段元数据:
[
{ name: 'id', type: 'int' },
{ name: 'name', type: 'varchar' },
{ name: 'accounts', type: 'varchar' }
]
]
// 由此可以看出,想像 mysql 那时直接使用查询数据库返回的数据,那是不可能滴
// 在 mysql2 中,需要使用 const [results] = await db.query(sql, [id]) 进行解构
//进而,拿到返回数组中的第一个响应结果,即查询结果
4.登录模块
点击查看详情
1.发送验证码&登录逻辑
路由处理函数:/api_server/router_handler/user.js
/** |
2.登录验证
3.阅读协议
- 首先,创建数据库表
CREATE TABLE protocols ( |
5.定义一个全局中间件 res.cc
定义 res.cc 用于简化错误的响应逻辑 点击查看详情
首先在 app.js 中定义 res.cc 这个中间件
// 响应数据的中间件 |
然后,在 处理函数中直接使用
exports.regUser = async (req, res) => { |
需要注意的是:
代码已经使用了现代的 async/await
风格,避免了传统的回调函数。传统回调函数的用法如下:
db.query(sql, params, (err, results) => { |
而在使用 Promise
和 async/await
时,写法变成这样:
try { |
回调函数的本质已经由 Promise
替代
- 在你的代码中,
db.query()
是一个Promise
,通过await
获取它的结果。 - 数据库操作完成后,返回的结果(或者错误)会自动传递给
await
或catch
,不需要显式地定义回调函数。
操作数据库后的回调函数已经被隐式地替代为 Promise
的 resolve
和 reject
方法:
- 如果查询成功,
Promise
的结果会被赋值给rows
。 - 如果查询失败,错误会被抛出,进入
catch
块。
总结
在现代 JavaScript 中,推荐使用 Promise
和 async/await
进行异步操作,因为它比传统的回调函数写法更简洁、更易读。如果你正在使用的是 mysql2/promise
模块,你的代码中不需要显式定义回调函数。
其实,严格来说,以上代码不是一个错误处理中间件,而是一个响应辅助中间件。它的主要作用是为 res
对象添加一个 res.cc
方法,用于统一处理成功或失败的响应消息。
为什么不是错误处理中间件?
错误处理中间件的定义
错误处理中间件是专门用于捕获和处理请求处理过程中发生的错误的,它的函数签名和普通中间件不同:app.use((err, req, res, next) => {
// 这才是错误处理中间件
});错误处理中间件的第一个参数必须是
err
,这个参数由next(error)
传递过来,Express 会自动将错误传递给这样的中间件。名字不强制一致:
err
是错误处理中间件中常见的约定,但你可以自由命名它。该中间件不接收 err 参数
上面定义的res.cc
是一个工具方法,用于格式化响应内容,它并没有处理通过next(error)
抛出的错误。因此,这不能算作一个错误处理中间件。
这个中间件的作用
- 为 res 对象添加一个统一的响应方法 (res.cc)
- 成功的响应:
res.cc('操作成功', 0)
- 失败的响应:
res.cc('操作失败')
或res.cc(new Error('错误信息'))
- 成功的响应:
- 简化了代码书写
你可以通过res.cc
快速发送 JSON 格式的响应,而不需要每次手动写res.send({ status, message })
。
6.全局错误中间件
点击查看详情
app.js 中,定义全局错误处理中间件
// 响应数据的中间件 |
处理函数末尾,catch(error)
exports.regUser = async (req, res, next) => { |
什么时候用 res.cc?
用于轻量级的错误响应:
如果某些错误逻辑不需要传递到全局错误处理中间件(例如简单的业务校验失败),可以直接使用res.cc
:if (!userInfo.accounts) {
return res.cc('手机号不能为空', 1); // 不传递到错误处理中间件
}用于不可恢复的错误:
如果错误是不可恢复的、需要全局处理的,应该使用next(error)
,让全局错误处理中间件处理。不可恢复的错误(irrecoverable errors)是指那些在发生时无法通过常规的错误处理或重试机制来恢复的错误。这些错误通常意味着应用程序的某些关键部分已经崩溃或无法继续运行,因此程序需要停止执行并返回给用户适当的错误信息。常见的不可恢复错误可能涉及系统级别的问题、程序的逻辑错误或无法处理的外部资源故障。
总结
- res.cc 是响应辅助中间件,而非错误处理中间件。它用于简化成功或失败的响应逻辑,统一返回格式。
- 错误处理中间件需要显式接收 err 参数。如果在业务逻辑中调用
next(error)
,Express 会自动将错误传递给第一个错误处理中间件。 - 两者结合:
res.cc
和全局错误处理中间件搭配使用,可以让代码既简洁又结构清晰,推荐这样做。
7. 为项目配置 eslint
点击查看详情
安装 & 初始化
pnpm npm install eslint
npx eslint --init编辑
eslint.config.js
规则import globals from "globals"
import pluginJs from "@eslint/js"
# rules 外层需要包一层对象
/** @type {import('eslint').Linter.Config[]} */
export default [
{files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
{languageOptions: { globals: globals.browser }},
{rules: {
"semi": ["error", "never"],
}},
pluginJs.configs.recommended,
]
详细说明 es6 和 commonjs 两个标准
module.exports = router
和export default router
的区别主要在于两者所属的模块系统和使用方式。它们分别属于 CommonJS 和 ES6 模块 两种模块规范。1. CommonJS 和 ES6 模块简介
- CommonJS 是 Node.js 默认的模块规范,使用
require
和module.exports
。 - ES6 模块 是 JavaScript 的现代模块系统,使用
import
和export
。
2. 区别分析
语法层面的区别
CommonJS (module.exports)
- 使用
module.exports
导出。 - 使用
require
导入。
示例:
// 导出文件(CommonJS)
const router = {}; // 假设 router 是一个对象
module.exports = router;
// 导入文件
const router = require('./path/to/router');- 使用
- CommonJS 是 Node.js 默认的模块规范,使用
ES6 模块 (export default)
- 使用
export default
导出。 - 使用
import
导入。
示例:
// 导出文件(ES6 模块)
const router = {}; // 假设 router 是一个对象
export default router;
// 导入文件
import router from './path/to/router';- 使用
运行时的区别
- CommonJS
- 模块在运行时加载,是同步的。
module.exports
可以导出任何类型的值(对象、函数、数组、基本类型等)。- 导入时,导出的是一个值的引用。
- ES6 模块
- 模块在编译时解析,是静态的。
export default
是 ES6 中默认导出方式,适用于一个模块导出一个主要功能。- 导入时,支持
import
解构语法。 - ES6 模块是严格模式,语法更现代。
项目中的区别
兼容性
- Node.js 环境:Node.js 默认支持 CommonJS 模块。如果要使用 ES6 模块,需要将
package.json
的type
设置为"module"
,或者使用.mjs
文件扩展名。 - 前端环境:现代前端工具(如 Webpack、Vite)对 ES6 模块有更好的支持。
- Node.js 环境:Node.js 默认支持 CommonJS 模块。如果要使用 ES6 模块,需要将
导出多项功能
CommonJS 使用exports,可以导出多个成员:
exports.func1 = () => {};
exports.func2 = () => {};ES6 模块支持命名导出和默认导出:
export const func1 = () => {};
export const func2 = () => {};
export default router;
使用习惯
- CommonJS 更常用于后端(Node.js 项目)。
- ES6 模块是现代 JavaScript 标准,前端项目和支持现代语法的后端项目更倾向于使用。
混用时的注意事项
如果一个模块使用
module.exports
导出,导入时不能直接用import
,需要借助createRequire
或其他工具。import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const router = require('./path/to/router');
如果一个模块使用
export default
导出,require
导入时会被包装在default
属性中:// 导出文件(ES6 模块)
export default router;
// CommonJS 导入
const router = require('./path/to/router').default;
总结
特性 | module.exports (CommonJS) | export default (ES6 模块) |
---|---|---|
模块规范 | CommonJS | ES6 模块 |
使用的导入方式 | require() | import |
支持环境 | Node.js 默认支持 | 需设置 "type": "module" 或 .mjs 文件 |
导入方式 | 直接导入值 | 默认导出直接导入,支持解构 |
运行时或编译时 | 运行时加载(同步) | 编译时解析(静态) |
灵活性 | 可导出单个或多个对象 | 默认导出一个主要内容,可同时命名导出多个 |
如果你在使用现代工具链或构建前端应用,优先使用 ES6 模块;而在传统 Node.js 项目中,CommonJS 更加直接和兼容性好。
8. 表单验证 @escook/express-joi
点击查看详情
安装
pnpm i @escook/express-joi
// 依赖
pnpm i joi
导入,
app.js
// 导入 express
const express = require('express')
// 创建 express 实例
const app = express()
// 导入 Joi 模块
const Joi = require('joi')
// 配置 cors 跨域中间件
const cors = require('cors')
app.use(cors())
新建 /schema/user.js 用户信息验证规则模块,并初始化以下代码
const Joi = require('joi')
// 定义手机号验证规则
const accounts = Joi.string().required()
const code = Joi.string()
// 定义手机号码的验证规则
const reg_login_schema = {
body: {
accounts,
code
}
}
// 导出 reg_login_schema
module.exports = { reg_login_schema }
然后 ,在路由模块中使用
const { Router } = require('express')
const router = Router()
// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入需要的验证规则对象
const { reg_login_schema } = require('../schema/user')
// 导入处理函数
const { regUser, login } = require('../router_handler/user')
// 注册用户
router.post('/login/code', expressJoi(reg_login_schema), regUser)
// 登录接口
// 需要注意的是,多个路由共用验证规则时,要留意验证对象是否为必填
// 如果验证规则中没有某个数据的验证规则,但发起的请求中携带了该参数,则会导致 [Object: null prototype] 报错
router.post('/login', expressJoi(reg_login_schema), login)
// 导出路由
module.exports = router
9. 使用 fetch 加载本地json数据
点击查看详情
public文件夹下,创建data/*.json文件
{
"list":[
{
"id": 15,
"picture": "/data/img/banner-1.png",
"name": "it求职端2",
"isdelete": 0,
"sort": 50,
"type": 2,
"url": "https://mobile.zcwytd.com/#/pages/login/choice/index",
"create_time": "2022-06-28T13:03:53.000Z",
"update_time": "2022-07-05T08:48:55.000Z"
},
{
"id": 16,
"picture": "/data/img/banner-2.png",
"name": "it求职端1",
"isdelete": 0,
"sort": 50,
"type": 2,
"url": "https://mobile.zcwytd.com/#/pages/login/choice/index",
"create_time": "2022-06-28T13:05:33.000Z",
"update_time": "2022-07-01T06:34:11.000Z"
}
]
}
修改原vue文件中的api请求逻辑
// 这是接口请求函数:
const getBannerList = async () => {
// 这里,原本是发起 api 请求
const res = await bannerList({
type: 2
})
// 这里也很有意思, if (res) {} else{当中, 推断了 res 为 false, 因此不再允许使用已在 api 接口中定义的返回数据类型}
if (res.list) {
console.log(res)
store.setBannerList(res.list)
} else {
showToast(res.msg || '到这里出错啦')
}
}现在,我们已经在前端项目中准备好了json数据,可以直接使用 fetch 从本地获取
const getBannerList = async () => {
try {
// 使用 fetch 从本地 JSON 文件加载数据
// 假设文件在 public/data/banner.json
const response = await fetch('/data/banner.json')
if (!response.ok) {
throw new Error('无法加载本地数据')
}
// 将 JSON 文件解析为对象
const res = await response.json()
// 判断并处理返回的数据
if (res.list) {
console.log(res)
store.setBannerList(res.list) // 假设 store 是一个全局状态管理对象
} else {
showToast(res.msg || '数据格式不正确,缺少 list 字段')
}
} catch (error) {
console.error('加载本地 bannerList 数据失败:', error)
showToast('加载本地数据出错')
}
}
10. 开发阶段,向 mysql 数据库批量添加数据
点击查看详情
需求来了,前端项目demo需要的基础数据,需要批量添加到指定的数据库表当中
try { |
11. jwt-token 验证
点击查看详情
根据后端的 token 验证逻辑
// 一定要在路由之前,应用此中间件
const { expressjwt } = require('express-jwt')
const config = require('./config')
app.use(
expressjwt({
secret: config.jwtSecretKey, // 与生成 Token 时的密钥保持一致
algorithms: ['HS256'], // 确保算法与 Token 的签名方式匹配
getToken: (req) => {
// 从 x-access-token 提取 Token
const token = req.headers['x-access-token']
if (token && token.startsWith('Bearer ')) {
return token.slice(7) // 去掉 "Bearer " 前缀
}
return null
}
}).unless({
path: [/^\/api\/login(\/.*)?$/] // 豁免登录相关接口
})
)前端必须按照后端显示指定的 getToken 方法来携带token
不显示的指定行不行?行!
在使用
express-jwt
时,getToken
方法 不是必须显式指定 的,但它有一个默认的行为。如果不显式提供getToken
,express-jwt
会按照以下默认逻辑来提取 Token:默认行为
express-jwt
默认从以下位置提取 Token:Authorization Header
- 默认从请求头
Authorization
提取。 - 要求格式为:
Bearer <token>
。 - 例如:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
- 默认从请求头
为什么要自定义 getToken?
- Token 来源不同:
- 如果你的 Token 不在 Authorization 头,而是放在其他地方(比如
x-access-token
或者cookie
),就需要通过getToken
方法显式指定如何提取。
- 如果你的 Token 不在 Authorization 头,而是放在其他地方(比如
- 前缀不符合 Bearer 格式:
- 如果你的 Token 没有
Bearer
前缀,默认逻辑无法正确解析 Token,也需要自定义getToken
。
- 如果你的 Token 没有
- 支持更多场景:
- 比如同时支持
Authorization
和x-access-token
两种提取方式,或者允许从请求体中提取 Token。
- 比如同时支持
不显式指定的代码示例
以下是只使用默认行为的代码:
app.use(
expressjwt({
secret: config.jwtSecretKey,
algorithms: ['HS256']
}).unless({
path: [/^\/api\/login(\/.*)?$/] // 豁免登录相关接口
})
);Token 提取方式
默认只从
Authorization: Bearer <token>
中提取。
显式指定的必要性
在如下的代码中,显式定义了
getToken
:getToken: (req) => {
const token = req.headers['x-access-token']; // 自定义从 x-access-token 提取
if (token && token.startsWith('Bearer ')) {
return token.slice(7); // 去掉 "Bearer " 前缀
}
return null;
}- 使用了
x-access-token
来传递 Token,这不是默认行为。 - 如果不指定
getToken
,express-jwt
会尝试从Authorization
头提取,导致找不到 Token 的问题。
结论
- 不显式指定时,
express-jwt
默认从Authorization: Bearer <token>
中提取 Token。 - 如果你的 Token 不在默认位置,或者格式不匹配,必须显式指定 getToken 方法。
ps: 关于错误级别中间件的一些细节试错
// 定义错误级别中间件 |
12.温故知新,前端瀑布流加载数据的后端适配
后端 api 设计 - 点击查看详情
直接上代码
// 导入数据库操作模块
const db = require('../db/index')
const taskAllList = async (req, res) => {
try {
// 从前端请求中提取参数
const {
position_name,
service_mode,
task_cycle,
pageNum = 1,
pageSize = 10,
city
} = req.query
// 基础查询语句
let query = 'SELECT * FROM tasklist WHERE isdelete = 0 AND is_check = 1'
const params = []
// 根据参数动态构建查询条件
if (city) {
query += ' AND city = ?'
params.push(city)
}
if (position_name) {
query += ' AND position_name = ?'
params.push(position_name)
}
if (service_mode) {
query += ' AND service_mode = ?'
params.push(service_mode)
}
if (task_cycle) {
query += ' AND task_cycle <= ?'
params.push(task_cycle)
}
// 添加排序条件(可选,视需求而定)
query += ' ORDER BY created_at DESC'
// 添加分页逻辑
const offset = (pageNum - 1) * pageSize // 计算偏移量
query += ' LIMIT ? OFFSET ?'
params.push(parseInt(pageSize), parseInt(offset))
// 执行 SQL 查询
const [rows] = await db.query(query, params)
// 查询总数(不带分页条件的总记录数)
const totalQuery = 'SELECT COUNT(*) AS total FROM tasklist WHERE isdelete = 0 AND is_check = 1'
const [totalResult] = await db.query(totalQuery,params)
const total = totalResult[0].total
// 响应数据
res.send({
code: 200,
result: {
records: rows, // 当前页的记录
msg: '查询任务列表成功!',
total, // 总记录数
page: parseInt(pageNum), // 当前页码
pageSize: parseInt(pageSize) // 每页条数
}
})
} catch (e) {
res.cc(e) // 捕获并返回错误
}
}
- 代码中的分页逻辑
// 分页相关参数 |
参数定义:
pageNum
: 当前页码(从前端请求中传入,默认值为1
)。pageSize
: 每页显示的记录条数(从前端请求中传入,默认值为10
)。offset
: 偏移量,即查询从哪一条记录开始。
偏移量的计算:
公式:
pageNum - 1
: 计算从第几页开始,第一页pageNum=1
时,偏移量为0
。- 乘以
pageSize
: 确定要跳过的记录条数。
示例:
- 假设每页显示 10 条记录:
- 第 1 页:
offset = (1 - 1) * 10 = 0
(从第 0 条记录开始)。 - 第 2 页:
offset = (2 - 1) * 10 = 10
(从第 10 条记录开始)。 - 第 3 页:
offset = (3 - 1) * 10 = 20
(从第 20 条记录开始)。
- 第 1 页:
- 假设每页显示 10 条记录:
SQL 中的分页:
LIMIT ?: 限制返回的记录条数,
pageSize
决定每页的记录数量。OFFSET ?: 跳过指定数量的记录,
offset
决定从哪一条记录开始。完整示例 SQL:
SELECT *
FROM tasklist
WHERE isdelete = 0 AND is_check = 1
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;- 每页显示
10
条记录,跳过前20
条(即返回第 21~30 条记录)。
- 每页显示
查询总数(Total)逻辑
分页需要知道总记录数,以便前端计算分页按钮或判断是否加载更多数据。
const totalQuery = 'SELECT COUNT(*) AS total FROM tasklist WHERE isdelete = 0 AND is_check = 1'; |
SQL 查询解释:
- SELECT COUNT(*) AS total: 统计
tasklist
表中符合条件的总记录数。 - WHERE 条件: 只统计未被删除且已审核的记录。
- 不受
LIMIT
和OFFSET
限制,返回的是完整数据集的记录总数。
- SELECT COUNT(*) AS total: 统计
结果意义:
total
: 总记录数,供前端分页使用。前端可以用以下公式计算总页数:
分页的工作流程
- 前端传参:
- 用户请求某一页数据,前端发送参数:
pageNum
和pageSize
。
- 用户请求某一页数据,前端发送参数:
- 后端接收:
- 通过
pageNum
和pageSize
计算offset
。 - 使用
LIMIT
和OFFSET
限制返回的记录条数。
- 通过
- 返回数据:
- 返回分页数据(
rows
)和总记录数(total
)。 - 前端可以根据
total
决定是否还有更多数据。
- 返回分页数据(
注意事项
性能优化:
- 当数据量很大时,直接使用
OFFSET
可能导致性能问题,因为数据库需要跳过大量数据。 - 优化方法:通过索引字段(如
id
或created_at
)使用游标进行分页查询。
示例:
SELECT *
FROM tasklist
WHERE isdelete = 0 AND is_check = 1 AND id > ?
ORDER BY id ASC
LIMIT 10;- 当数据量很大时,直接使用
边界情况处理:
pageNum
小于 1:将其修正为1
。pageSize
超过合理范围:设置最大值(如100
)。- 总数为
0
时:返回空数据,并标记分页已完成。
总结
分页逻辑的核心是通过 LIMIT
和 OFFSET
控制查询范围,并通过计算偏移量确定从哪一条记录开始查询。通过统计总记录数,确保前端能够正确显示分页状态或终止加载更多的逻辑。
13. 按照范式标准,创建用户收藏表
点击查看详情
首先,创建数据库表
CREATE TABLE task_favorites (
id INT PRIMARY KEY AUTO_INCREMENT, -- 主键
accounts VARCHAR(20) NOT NULL, -- 用户ID(外键关联用户表)
task_id INT NOT NULL, -- 任务ID(外键关联任务表)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 收藏时间
UNIQUE KEY (accounts, task_id), -- 用户和任务的唯一关系,防止重复收藏
FOREIGN KEY (accounts) REFERENCES users(accounts) ON DELETE CASCADE, -- 外键关联用户表
FOREIGN KEY (task_id) REFERENCES tasklist(task_id) ON DELETE CASCADE -- 外键关联任务表
);说明:
user_id
和task_id
使用外键关联users
表和tasklist
表,保证数据的一致性。UNIQUE KEY (user_id, task_id)
防止同一用户对同一任务重复收藏。ON DELETE CASCADE
确保父表删除对应记录时,自动删除user_favorites
表中相关的关联数据。
外键关联任务表:
在
FOREIGN KEY (task_id) REFERENCES tasklist(id) ON DELETE CASCADE
中:tasklist:表示外键所引用的目标表名称,即这里的
tasklist
是任务表。task_id:表示
tasklist
表中的主键(Primary Key)。它是目标表的列名称,用来唯一标识tasklist
表中的每一行数据。意义是确保
task_id
的值只能是tasklist
表中task_id
列中已经存在的值,从而维持数据的一致性和完整性。如果尝试向
task_favorites
表中插入tasklist
中不存在的task_id
记录,就会报错..ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
创建外键关联任务表之前,需要做一些准备工作
// 为指定列创建索引
CREATE INDEX idx_accounts ON users(accounts);
CREATE INDEX idx_task_id ON tasklist(task_id);封装添加收藏关系的接口
// 任务收藏接口
const taskFavorite = async (req, res) => {
const { task_id } = req.body // 从请求的查询参数中获取任务ID
const { accounts } = req.auth // 从请求的认证信息中获取用户账户(user_id)
try {
// 查询当前用户是否已收藏该任务
const sql = 'SELECT * FROM task_favorites WHERE accounts = ? AND task_id = ?'
const [rows] = await db.query(sql, [accounts, task_id])
if (rows.length > 0) {
// 如果已收藏,则执行取消收藏
const sqlDelete = 'DELETE FROM task_favorites WHERE accounts = ? AND task_id = ?'
await db.query(sqlDelete, [accounts, task_id])
// 返回取消收藏的状态
res.send({
code: 200,
success: 'ok',
result: {
errCode: 200,
msg: '已取消收藏',
data: {
status: 0 // 收藏状态为 0,表示未收藏
}
}
})
} else {
// 如果未收藏,则执行添加收藏
const sqlInsert = 'INSERT INTO task_favorites (accounts, task_id) VALUES (?, ?)'
await db.query(sqlInsert, [accounts, task_id])
// 返回添加收藏的状态
res.send({
code: 200,
success: 'ok',
result: {
errCode: 200,
msg: '已添加收藏',
data: {
status: 1 // 收藏状态为 1,表示已收藏
}
}
})
}
} catch (err) {
// 处理错误
res.cc(err)
}
}这里需要注意一下,前端携带参数发起 post 请求,后端接收时,需要使用req.body进行接收,否则接收不到。
顺便说一下
不同api请求方式的参数接收方式
GET 请求:参数通过 URL 查询字符串(
req.query
)传递。POST 请求:参数通过请求体(
req.body
)传递。PUT 请求:参数通过请求体(
req.body
)传递,路径参数通过req.params
获取。DELETE 请求:参数通常通过路径参数(
req.params
)传递,少量使用请求体。
特殊情况:POST 请求的 JSON 数据
对于 POST
请求,如果发送的是 JSON 数据,后端需要使用中间件来解析请求体中的 JSON 数据。例如在 Express 中,使用 express.json()
来解析 JSON 数据。
14.使用联合查询,合并需要返回的数据
点击查看详情
将从请求头中获取用户信息的逻辑设计为一个中间件
在根目录创建 /utils/handleToken.js
const jwt = require('jsonwebtoken') |
然后,在对应路由中添加该中间件
const { Router } = require('express') |
然后,在处理逻辑中,就可以直接进行接收了 /
const accounts = req.accounts |
其实吧,在 app.js 中,有如下一个中间件了解一下
// 一定要在路由之前,应用此中间件 |
express-jwt
中间件在验证 Token 成功后,自动为当前请求对象 req
添加的一个属性,即req.auth。这个属性存储的是解析后的 JWT Payload 数据(也就是在生成 Token 时嵌入的用户信息),因此,可以对其进行解构以获取目标信息:
const { accounts } = req.auth |
/router_handle/task.js
// 任务详情接口 |
100. 这里必须留一个小尾巴,否则,编辑软件会报错,后面视情况会寻找原因,大概率是编辑器原因!
ttttt 不会错版了kugei