12Express简易实战项目(编写api)

12Express简易实战项目

  • 1.初始化
    • 1.1 创建项目
    • 1.2 配置 cors 跨域
    • 1.3配置解析表单数据的中间件
    • 1.4 初始化路由相关的文件夹
    • 1.5 初始化用户路由模块
    • 1.6 抽离用户路由模块中的处理函数
  • 2.登录注册
    • 2.1 新建 ev_users 表
    • 2.2 安装并配置 mysql 模块
    • 2.3 注册
      • (1)实现步骤
      • (2)检测表单数据是否合法
      • (3)检测用户名是否被占用
      • (4)对密码进行加密处理
      • (5)插入新用户
    • 2.4 优化 res.send() 代码
    • 2.5 优化表单数据验证
    • 2.6 登录
      • (1)实现步骤
      • (2)检测登录表单的数据是否合法
      • (2)根据用户名查询用户的数据
      • (3)判断用户输入的密码是否正确
      • (4)生成 JWT 的 Token 字符串
    • 2.7 配置解析 Token 的中间件
  • 3 个人中心
    • 3.1获取用户的基本信息
      • (1) 实现步骤
      • (2) 初始化路由模块
      • (3)初始化路由处理函数模块
      • (4)获取用户的基本信息
    • 3.2更新用户的基本信息
      • (1)实现步骤
      • (2)定义路由和处理函数
      • (3)验证表单数据
      • (4)实现更新用户基本信息的功能
    • 3.3重置密码
      • (1)实现步骤
      • (2)定义路由和处理函数
      • (3)验证表单数据
      • (4)实现重置密码的功能
    • 3.4更新用户头像
      • (1)实现步骤
      • (2)定义路由和处理函数
      • (3)验证表单数据
      • (4)实现更新用户头像的功能
  • 4 文章分类管理
    • 4.1新建 ev_article_cate 表
      • (1) 创建表结构
      • (2) 新增两条初始数据
    • 4.2获取文章分类列表
      • (1) 实现步骤
      • (2) 初始化路由模块
      • (3) 初始化路由处理函数模块
      • (4) 获取文章分类列表数据
    • 4.3新增文章分类
      • (1) 实现步骤
      • (2) 定义路由和处理函数
      • (3) 验证表单数据
      • (4) 查询分类名称与别名是否被占用
      • (5) 实现新增文章分类的功能
    • 4.4根据 Id 删除文章分类
      • (1) 实现步骤
      • (2) 定义路由和处理函数
      • (3) 验证表单数据
      • (4) 实现删除文章分类的功能
    • 4.5根据 Id 获取文章分类数据
      • (1) 实现步骤
      • (2) 定义路由和处理函数
      • (3) 验证表单数据
      • (4) 实现获取文章分类的功能
    • 4.6根据 Id 更新文章分类数据
      • (1) 实现步骤
      • (2) 定义路由和处理函数
      • (3) 定义路由和处理函数
      • (4) 查询分类名称与别名是否被占用
      • (5) 实现更新文章分类的功能
  • 5.文章管理
    • 5.1 新建 ev_articles 表
    • 5.2发布新文章
      • (1) 实现步骤
      • (2) 初始化路由模块
      • (3) 初始化路由处理函数模块
      • (4) 使用 multer 解析表单数据
      • (5) 验证表单数据
      • (6) 实现发布文章的功能

1.初始化

1.1 创建项目

新建 api_server 文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:

npm init -y

运行如下的命令,安装特定版本的 express :

npm 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 跨域

运行如下的命令,安装 cors 中间件:

  npm i cors@2.8.5

在 app.js 中导入并配置 cors 中间件:

// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())

1.3配置解析表单数据的中间件

通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件:

app.use(express.urlencoded({ extended: false }))

1.4 初始化路由相关的文件夹

在项目根目录中,新建 router 文件夹,用来存放所有的 路由模块(路由模块中,只存放客户端的请求与处理函数之间的映射关系)。
在项目根目录中,新建 router_handler 文件夹,用来存放所有的 路由处理函数模块(路由处理函数模块中,专门负责存放每个路由对应的处理函数)。

1.5 初始化用户路由模块

在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码如下:

const express = require('express')
// 创建路由对象
const router = express.Router()
// 注册新用户
router.post('/reguser', (req, res) => {
res.send('reguser OK')
})
// 登录
router.post('/login', (req, res) => {
res.send('login OK')
})
// 将路由对象共享出去
module.exports = router

在 app.js 中,导入并使用 用户路由模块 :

// 导入并注册用户路由模块
const userRouter = require('./router/user')
app.use('/api', userRouter)

1.6 抽离用户路由模块中的处理函数

目的:为了保证 路由模块 的纯粹性,所有的 路由处理函数 ,必须抽离到对应的 路由处理函数
模块中。

在 /router_handler/user.js 中,使用 exports 对象,分别向外共享如下两个 路由处理函数:

/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
res.send('reguser OK')
}
// 登录的处理函数
exports.login = (req, res) => {
res.send('login OK')
}

将 /router/user.js 中的代码修改为如下结构:

const express = require('express')
const router = express.Router()
// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')
// 注册新用户
router.post('/reguser', userHandler.regUser)
// 登录
router.post('/login', userHandler.login)
module.exports = router

2.登录注册

2.1 新建 ev_users 表

可以用MySQL Workbench可视化工具,也可以用navicat。这个看个人喜好,步骤大致上差不多,都是Mysql数据库的可视化工具。平时都是用navicat用的多,这次就用MySQL Workbench工具了(具体下载过程在之前的nodejs系列中有讲述过)。
在 my_db_01 数据库中,新建 ev_users 表如下:
在这里插入图片描述

2.2 安装并配置 mysql 模块

在 API 接口项目中,需要安装并配置 mysql 这个第三方模块,来连接和操作 MySQL 数据库

运行如下命令,安装 mysql 模块:

npm i mysql@2.18.1

在项目根目录中新建 /db/index.js 文件,在此自定义模块中创建数据库的连接对象:

 // 导入 mysql 模块
const mysql = require('mysql')// 创建数据库连接对象
const db = mysql.createPool({host: '127.0.0.1',user: 'root',password: 'admin123',database: 'my_db_01',
})// 向外共享 db 数据库连接对象
module.exports = db

2.3 注册

(1)实现步骤

  • 检测表单数据是否合法
  • 检测用户名是否被占用
  • 对密码进行加密处理
  • 插入新用户

(2)检测表单数据是否合法

判断用户名和密码是否为空

// 接收表单数据
const userinfo = req.body
// 判断数据是否合法
if (!userinfo.username || !userinfo.password) {return res.send({ status: 1, message: '用户名或密码不能为空!' })
}

(3)检测用户名是否被占用

导入数据库操作模块:

const db = require('../db/index')

定义 SQL 语句:

const sql = `select * from ev_users where username=?`

执行 SQL 语句并根据结果判断用户名是否被占用:

db.query(sql, [userinfo.username], function (err, results) {// 执行 SQL 语句失败if (err) {return res.send({ status: 1, message: err.message })}// 用户名被占用if (results.length > 0) {return res.send({ status: 1, message: '用户名被占用,请更换其他用户名!' })}// TODO: 用户名可用,继续后续流程...
})

(4)对密码进行加密处理

为了保证密码的安全性,不建议在数据库以 明文 的形式保存用户密码,推荐对密码进行 加密存储

在当前项目中,使用 bcryptjs 对用户密码进行加密,优点:

  • 加密之后的密码,无法被逆向破解

  • 同一明文密码多次加密,得到的加密结果各不相同,保证了安全性

  1. 运行如下命令,安装指定版本的 bcryptjs
npm i bcryptjs@2.4.3

/router_handler/user.js 中,导入 bcryptjs

const bcrypt = require('bcryptjs')

在注册用户的处理函数中,确认用户名可用之后,调用 bcrypt.hashSync(明文密码, 随机盐的长度) 方法,对用户的密码进行加密处理:

// 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串
userinfo.password = bcrypt.hashSync(userinfo.password, 10)

(5)插入新用户

定义插入用户的 SQL 语句:

const sql = 'insert into ev_users set ?'

调用 db.query() 执行 SQL 语句,插入新用户:

db.query(sql, { username: userinfo.username, password: userinfo.password }, function (err, results) {// 执行 SQL 语句失败if (err) return res.send({ status: 1, message: err.message })// SQL 语句执行成功,但影响行数不为 1if (results.affectedRows !== 1) {return res.send({ status: 1, message: '注册用户失败,请稍后再试!' })}// 注册成功res.send({ status: 0, message: '注册成功!' })
})

2.4 优化 res.send() 代码

在处理函数中,需要多次调用 res.send() 向客户端响应 处理失败 的结果,为了简化代码,可以手动封装一个 res.cc() 函数

app.js 中,所有路由之前,声明一个全局中间件,为 res 对象挂载一个 res.cc() 函数 :

// 响应数据的中间件
app.use(function (req, res, next) {// status = 0 为成功; status = 1 为失败; 默认将 status 的值设置为 1,方便处理失败的情况res.cc = function (err, status = 1) {res.send({// 状态status,// 状态描述,判断 err 是 错误对象 还是 字符串message: err instanceof Error ? err.message : err,})}next()
})

2.5 优化表单数据验证

表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容

在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。

单纯的使用 if...else... 的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后端程序员把更多的精力放在核心业务逻辑的处理上

安装 @hapi/joi 包,为表单中携带的每个数据项,定义验证规则:

npm install @hapi/joi@17.1.0

安装 @escook/express-joi 中间件,来实现自动对表单数据进行验证的功能:

npm i @escook/express-joi

新建 /schema/user.js 用户信息验证规则模块,并初始化代码如下:

const joi = require('@hapi/joi')/*** string() 值必须是字符串* alphanum() 值只能是包含 a-zA-Z0-9 的字符串* min(length) 最小长度* max(length) 最大长度* required() 值是必填项,不能为 undefined* pattern(正则表达式) 值必须符合正则表达式的规则*/// 用户名的验证规则
const username = joi.string().alphanum().min(1).max(10).required()
// 密码的验证规则
const password = joi.string().pattern(/^[\S]{6,12}$/).required()// 注册和登录表单的验证规则对象
exports.reg_login_schema = {// 表示需要对 req.body 中的数据进行验证body: {username,password,},
}

修改 /router/user.js 中的代码如下:

const express = require('express')
const router = express.Router()// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')// 1. 导入验证表单数据的中间件
const expressJoi = require('@escook/express-joi')
// 2. 导入需要的验证规则对象
const { reg_login_schema } = require('../schema/user')// 注册新用户
// 3. 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证
// 3.1 数据验证通过后,会把这次请求流转给后面的路由处理函数
// 3.2 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理
router.post('/reguser', expressJoi(reg_login_schema), userHandler.regUser)
// 登录
router.post('/login', userHandler.login)module.exports = router

app.js 的全局错误级别中间件中,捕获验证失败的错误,并把验证失败的结果响应给客户端:

const joi = require('@hapi/joi')// 错误中间件
app.use(function (err, req, res, next) {// 数据验证失败if (err instanceof joi.ValidationError) return res.cc(err)// 未知错误res.cc(err)
})

2.6 登录

(1)实现步骤

  • 检测表单数据是否合法
  • 根据用户名查询用户的数据
  • 判断用户输入的密码是否正确
  • 生成 JWT 的 Token 字符串

(2)检测登录表单的数据是否合法

/router/user.js登录 的路由代码修改如下:

// 登录的路由
router.post('/login', expressJoi(reg_login_schema), userHandler.login)

(2)根据用户名查询用户的数据

接收表单数据:

const userinfo = req.body

定义 SQL 语句:

const sql = `select * from ev_users where username=?`

执行 SQL 语句,查询用户的数据:

db.query(sql, userinfo.username, function (err, results) {// 执行 SQL 语句失败if (err) return res.cc(err)// 执行 SQL 语句成功,但是查询到数据条数不等于 1if (results.length !== 1) return res.cc('登录失败!')// TODO:判断用户输入的登录密码是否和数据库中的密码一致
})

(3)判断用户输入的密码是否正确

核心实现思路:调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法比较密码是否一致

返回值是布尔值(true 一致、false 不一致)

具体的实现代码如下:

// 拿着用户输入的密码,和数据库中存储的密码进行对比
const compareResult = bcrypt.compareSync(userinfo.password, results[0].password)// 如果对比的结果等于 false, 则证明用户输入的密码错误
if (!compareResult) {return res.cc('登录失败!')
}// TODO:登录成功,生成 Token 字符串

(4)生成 JWT 的 Token 字符串

核心注意点:在生成 Token 字符串的时候,一定要剔除 密码头像 的值

通过 ES6 的高级语法,快速剔除 密码头像 的值:

// 剔除完毕之后,user 中只保留了用户的 id, username, nickname, email 这四个属性的值
const user = { ...results[0], password: '', user_pic: '' }

运行如下的命令,安装生成 Token 字符串的包:

npm i jsonwebtoken@8.5.1
  1. /router_handler/user.js 模块的头部区域,导入 jsonwebtoken 包:
// 用这个包来生成 Token 字符串
const jwt = require('jsonwebtoken')

创建 config.js 文件,并向外共享 加密还原 Token 的 jwtSecretKey 字符串:

module.exports = {jwtSecretKey: 'itheima No1. ^_^',
}

将用户信息对象加密成 Token 字符串:

// 导入配置文件
const config = require('../config')// 生成 Token 字符串
const tokenStr = jwt.sign(user, config.jwtSecretKey, {expiresIn: '10h', // token 有效期为 10 个小时
})

将生成的 Token 字符串响应给客户端:

res.send({status: 0,message: '登录成功!',// 为了方便客户端使用 Token,在服务器端直接拼接上 Bearer 的前缀token: 'Bearer ' + tokenStr,
})

2.7 配置解析 Token 的中间件

运行如下的命令,安装解析 Token 的中间件:

npm i express-jwt@5.3.3

app.js 中注册路由之前,配置解析 Token 的中间件:

// 导入配置文件
const config = require('./config')// 解析 token 的中间件
const expressJWT = require('express-jwt')// 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证
app.use(expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api\//] }))

app.js 中的 错误级别中间件 里面,捕获并处理 Token 认证失败后的错误:

// 错误中间件
app.use(function (err, req, res, next) {// 省略其它代码...// 捕获身份认证失败的错误if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!')// 未知错误...
})

3 个人中心

3.1获取用户的基本信息

(1) 实现步骤

  • 初始化 路由 模块
  • 初始化 路由处理函数 模块
  • 获取用户的基本信息

(2) 初始化路由模块

创建 /router/userinfo.js 路由模块,并初始化如下的代码结构:

// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()// 获取用户的基本信息
router.get('/userinfo', (req, res) => {res.send('ok')
})
// 向外共享路由对象
module.exports = router

app.js 中导入并使用个人中心的路由模块:

// 导入并使用用户信息路由模块
const userinfoRouter = require('./router/userinfo')
// 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证
app.use('/my', userinfoRouter)

(3)初始化路由处理函数模块

创建 /router_handler/userinfo.js 路由处理函数模块,并初始化如下的代码结构:

// 获取用户基本信息的处理函数
exports.getUserInfo = (req, res) => {res.send('ok')
}

修改 /router/userinfo.js 中的代码如下:

const express = require('express')
const router = express.Router()
// 导入用户信息的处理函数模块
const userinfo_handler = require('../router_handler/userinfo')
// 获取用户的基本信息
router.get('/userinfo', userinfo_handler.getUserInfo)
module.exports = router

(4)获取用户的基本信息

/router_handler/userinfo.js 头部导入数据库操作模块:

// 导入数据库操作模块
const db = require('../db/index')

定义 SQL 语句:

// 根据用户的 id,查询用户的基本信息
// 注意:为了防止用户的密码泄露,需要排除 password 字段
const sql = `select id, username, nickname, email, user_pic from ev_users where id=?`

调用 db.query() 执行 SQL 语句:

// 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的
db.query(sql, req.user.id, (err, results) => {// 1. 执行 SQL 语句失败if (err) return res.cc(err)// 2. 执行 SQL 语句成功,但是查询到的数据条数不等于 1if (results.length !== 1) return res.cc('获取用户信息失败!')// 3. 将用户信息响应给客户端res.send({status: 0,message: '获取用户基本信息成功!',data: results[0],})
})

3.2更新用户的基本信息

(1)实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 实现更新用户基本信息的功能

(2)定义路由和处理函数

/router/userinfo.js 模块中,新增 更新用户基本信息 的路由:

// 更新用户的基本信息
router.post('/userinfo', userinfo_handler.updateUserInfo)

/router_handler/userinfo.js 模块中,定义并向外共享 更新用户基本信息 的路由处理函数:

// 更新用户基本信息的处理函数
exports.updateUserInfo = (req, res) => {res.send('ok')
}

(3)验证表单数据

/schema/user.js 验证规则模块中,定义 idnicknameemail 的验证规则如下:

// 定义 id, nickname, emial 的验证规则
const id = joi.number().integer().min(1).required()
const nickname = joi.string().required()
const email = joi.string().email().required()

并使用 exports 向外共享如下的 验证规则对象

// 验证规则对象 - 更新用户基本信息
exports.update_userinfo_schema = {body: {id,nickname,email,},
}

/router/userinfo.js 模块中,导入验证数据合法性的中间件:

// 导入验证数据合法性的中间件
const expressJoi = require('@escook/express-joi')

/router/userinfo.js 模块中,导入需要的验证规则对象:

// 导入需要的验证规则对象
const { update_userinfo_schema } = require('../schema/user')

/router/userinfo.js 模块中,修改 更新用户的基本信息 的路由如下:

// 更新用户的基本信息
router.post('/userinfo', expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo)

(4)实现更新用户基本信息的功能

定义待执行的 SQL 语句:

const sql = `update ev_users set ? where id=?`

调用 db.query() 执行 SQL 语句并传参:

db.query(sql, [req.body, req.body.id], (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 执行 SQL 语句成功,但影响行数不为 1if (results.affectedRows !== 1) return res.cc('修改用户基本信息失败!')// 修改用户信息成功return res.cc('修改用户基本信息成功!', 0)
})

3.3重置密码

(1)实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 实现重置密码的功能

(2)定义路由和处理函数

/router/userinfo.js 模块中,新增 重置密码 的路由:

// 重置密码的路由
router.post('/updatepwd', userinfo_handler.updatePassword)

/router_handler/userinfo.js 模块中,定义并向外共享 重置密码 的路由处理函数:

// 重置密码的处理函数
exports.updatePassword = (req, res) => {res.send('ok')
}

(3)验证表单数据

核心验证思路:旧密码与新密码,必须符合密码的验证规则,并且新密码不能与旧密码一致!

/schema/user.js 模块中,使用 exports 向外共享如下的 验证规则对象

// 验证规则对象 - 重置密码
exports.update_password_schema = {body: {// 使用 password 这个规则,验证 req.body.oldPwd 的值oldPwd: password,// 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值// 解读:// 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致// 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值// 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则newPwd: joi.not(joi.ref('oldPwd')).concat(password),},
}

/router/userinfo.js 模块中,导入需要的验证规则对象:

// 导入需要的验证规则对象
const { update_userinfo_schema, update_password_schema } = require('../schema/user')

并在 重置密码的路由 中,使用 update_password_schema 规则验证表单的数据,示例代码如下:

router.post('/updatepwd', expressJoi(update_password_schema), userinfo_handler.updatePassword)

(4)实现重置密码的功能

根据 id 查询用户是否存在:

// 定义根据 id 查询用户数据的 SQL 语句
const sql = `select * from ev_users where id=?`
// 执行 SQL 语句查询用户是否存在
db.query(sql, req.user.id, (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 检查指定 id 的用户是否存在if (results.length !== 1) return res.cc('用户不存在!')// TODO:判断提交的旧密码是否正确
})

判断提交的 旧密码 是否正确:

// 在头部区域导入 bcryptjs 后,
// 即可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确
// compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误
const bcrypt = require('bcryptjs')
// 判断提交的旧密码是否正确
const compareResult = bcrypt.compareSync(req.body.oldPwd, results[0].password)
if (!compareResult) return res.cc('原密码错误!')

对新密码进行 bcrypt 加密之后,更新到数据库中:

// 定义更新用户密码的 SQL 语句
const sql = `update ev_users set password=? where id=?`
// 对新密码进行 bcrypt 加密处理
const newPwd = bcrypt.hashSync(req.body.newPwd, 10)
// 执行 SQL 语句,根据 id 更新用户的密码
db.query(sql, [newPwd, req.user.id], (err, results) => {// SQL 语句执行失败if (err) return res.cc(err)// SQL 语句执行成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('更新密码失败!')// 更新密码成功res.cc('更新密码成功!', 0)
})

3.4更新用户头像

(1)实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 实现更新用户头像的功能

(2)定义路由和处理函数

/router/userinfo.js 模块中,新增 更新用户头像 的路由:

// 更新用户头像的路由
router.post('/update/avatar', userinfo_handler.updateAvatar)

/router_handler/userinfo.js 模块中,定义并向外共享 更新用户头像 的路由处理函数:

// 更新用户头像的处理函数
exports.updateAvatar = (req, res) => {res.send('ok')
}

(3)验证表单数据

/schema/user.js 验证规则模块中,定义 avatar 的验证规则如下:

// dataUri() 指的是如下格式的字符串数据:
// data:image/png;base64,VE9PTUFOWVNFQ1JFVFM=
const avatar = joi.string().dataUri().required()

并使用 exports 向外共享如下的 验证规则对象

// 验证规则对象 - 更新头像
exports.update_avatar_schema = {body: {avatar,},
}

/router/userinfo.js 模块中,导入需要的验证规则对象:

const { update_avatar_schema } = require('../schema/user')

/router/userinfo.js 模块中,修改 更新用户头像 的路由如下:

router.post('/update/avatar', expressJoi(update_avatar_schema), userinfo_handler.updateAvatar)

(4)实现更新用户头像的功能

定义更新用户头像的 SQL 语句:

const sql = 'update ev_users set user_pic=? where id=?'

调用 db.query() 执行 SQL 语句,更新对应用户的头像:

db.query(sql, [req.body.avatar, req.user.id], (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 执行 SQL 语句成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('更新头像失败!')// 更新用户头像成功return res.cc('更新头像成功!', 0)
})

4 文章分类管理

4.1新建 ev_article_cate 表

(1) 创建表结构

在这里插入图片描述

(2) 新增两条初始数据

在这里插入图片描述

4.2获取文章分类列表

(1) 实现步骤

  • 初始化路由模块
  • 初始化路由处理函数模块
  • 获取文章分类列表数据

(2) 初始化路由模块

创建 /router/artcate.js 路由模块,并初始化如下的代码结构:

// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()// 获取文章分类的列表数据
router.get('/cates', (req, res) => {res.send('ok')
})
// 向外共享路由对象
module.exports = router

app.js 中导入并使用文章分类的路由模块:

// 导入并使用文章分类路由模块
const artCateRouter = require('./router/artcate')
// 为文章分类的路由挂载统一的访问前缀 /my/article
app.use('/my/article', artCateRouter)

(3) 初始化路由处理函数模块

创建 /router_handler/artcate.js 路由处理函数模块,并初始化如下的代码结构:

// 获取文章分类列表数据的处理函数
exports.getArticleCates = (req, res) => {res.send('ok')
}

修改 /router/artcate.js 中的代码如下:

const express = require('express')
const router = express.Router()// 导入文章分类的路由处理函数模块
const artcate_handler = require('../router_handler/artcate')// 获取文章分类的列表数据
router.get('/cates', artcate_handler.getArticleCates)module.exports = router

(4) 获取文章分类列表数据

/router_handler/artcate.js 头部导入数据库操作模块:

// 导入数据库操作模块
const db = require('../db/index')

定义 SQL 语句:

// 根据分类的状态,获取所有未被删除的分类列表数据
// is_delete 为 0 表示没有被 标记为删除 的数据
const sql = 'select * from ev_article_cate where is_delete=0 order by id asc'

调用 db.query() 执行 SQL 语句:

db.query(sql, (err, results) => {// 1. 执行 SQL 语句失败if (err) return res.cc(err)// 2. 执行 SQL 语句成功res.send({status: 0,message: '获取文章分类列表成功!',data: results,})
})

4.3新增文章分类

(1) 实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 查询 分类名称分类别名 是否被占用
  • 实现新增文章分类的功能

(2) 定义路由和处理函数

/router/artcate.js 模块中,添加 新增文章分类 的路由:

// 新增文章分类的路由
router.post('/addcates', artcate_handler.addArticleCates)

/router_handler/artcate.js 模块中,定义并向外共享 新增文章分类 的路由处理函数:

// 新增文章分类的处理函数
exports.addArticleCates = (req, res) => {res.send('ok')
}

(3) 验证表单数据

创建 /schema/artcate.js 文章分类数据验证模块,并定义如下的验证规则:

// 导入定义验证规则的模块
const joi = require('@hapi/joi')// 定义 分类名称 和 分类别名 的校验规则
const name = joi.string().required()
const alias = joi.string().alphanum().required()// 校验规则对象 - 添加分类
exports.add_cate_schema = {body: {name,alias,},
}

/router/artcate.js 模块中,使用 add_cate_schema 对数据进行验证:

// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章分类的验证模块
const { add_cate_schema } = require('../schema/artcate')// 新增文章分类的路由
router.post('/addcates', expressJoi(add_cate_schema), artcate_handler.addArticleCates)

(4) 查询分类名称与别名是否被占用

定义查重的 SQL 语句:

// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
const sql = `select * from ev_article_cate where name=? or alias=?`

调用 db.query() 执行查重的操作:

// 执行查重操作
db.query(sql, [req.body.name, req.body.alias], (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 判断 分类名称 和 分类别名 是否被占用if (results.length === 2) return res.cc('分类名称与别名被占用,请更换后重试!')// 分别判断 分类名称 和 分类别名 是否被占用if (results.length === 1 && results[0].name === req.body.name) return res.cc('分类名称被占用,请更换后重试!')if (results.length === 1 && results[0].alias === req.body.alias) return res.cc('分类别名被占用,请更换后重试!')// TODO:新增文章分类
})

(5) 实现新增文章分类的功能

定义新增文章分类的 SQL 语句:

const sql = `insert into ev_article_cate set ?`

调用 db.query() 执行新增文章分类的 SQL 语句:

db.query(sql, req.body, (err, results) => {// SQL 语句执行失败if (err) return res.cc(err)// SQL 语句执行成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('新增文章分类失败!')// 新增文章分类成功res.cc('新增文章分类成功!', 0)
})

4.4根据 Id 删除文章分类

(1) 实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 实现删除文章分类的功能

(2) 定义路由和处理函数

/router/artcate.js 模块中,添加 删除文章分类 的路由:

// 删除文章分类的路由
router.get('/deletecate/:id', artcate_handler.deleteCateById)

/router_handler/artcate.js 模块中,定义并向外共享 删除文章分类 的路由处理函数:

// 删除文章分类的处理函数
exports.deleteCateById = (req, res) => {res.send('ok')
}

(3) 验证表单数据

/schema/artcate.js 验证规则模块中,定义 id 的验证规则如下:

// 定义 分类Id 的校验规则
const id = joi.number().integer().min(1).required()

并使用 exports 向外共享如下的 验证规则对象

// 校验规则对象 - 删除分类
exports.delete_cate_schema = {params: {id,},
}

/router/artcate.js 模块中,导入需要的验证规则对象,并在路由中使用:

// 导入删除分类的验证规则对象
const { delete_cate_schema } = require('../schema/artcate')// 删除文章分类的路由
router.get('/deletecate/:id', expressJoi(delete_cate_schema), artcate_handler.deleteCateById)

(4) 实现删除文章分类的功能

定义删除文章分类的 SQL 语句:

const sql = `update ev_article_cate set is_delete=1 where id=?`

调用 db.query() 执行删除文章分类的 SQL 语句:

db.query(sql, req.params.id, (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// SQL 语句执行成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('删除文章分类失败!')// 删除文章分类成功res.cc('删除文章分类成功!', 0)
})

4.5根据 Id 获取文章分类数据

(1) 实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 实现获取文章分类的功能

(2) 定义路由和处理函数

/router/artcate.js 模块中,添加 根据 Id 获取文章分类 的路由:

router.get('/cates/:id', artcate_handler.getArticleById)

/router_handler/artcate.js 模块中,定义并向外共享 根据 Id 获取文章分类 的路由处理函数:

// 根据 Id 获取文章分类的处理函数
exports.getArticleById = (req, res) => {res.send('ok')
}

(3) 验证表单数据

/schema/artcate.js 验证规则模块中,使用 exports 向外共享如下的 验证规则对象

// 校验规则对象 - 根据 Id 获取分类
exports.get_cate_schema = {params: {id,},
}

/router/artcate.js 模块中,导入需要的验证规则对象,并在路由中使用:

// 导入根据 Id 获取分类的验证规则对象
const { get_cate_schema } = require('../schema/artcate')// 根据 Id 获取文章分类的路由
router.get('/cates/:id', expressJoi(get_cate_schema), artcate_handler.getArticleById)

(4) 实现获取文章分类的功能

定义根据 Id 获取文章分类的 SQL 语句:

const sql = `select * from ev_article_cate where id=?`

调用 db.query() 执行 SQL 语句:

db.query(sql, req.params.id, (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// SQL 语句执行成功,但是没有查询到任何数据if (results.length !== 1) return res.cc('获取文章分类数据失败!')// 把数据响应给客户端res.send({status: 0,message: '获取文章分类数据成功!',data: results[0],})
})

4.6根据 Id 更新文章分类数据

(1) 实现步骤

  • 定义路由和处理函数
  • 验证表单数据
  • 查询 分类名称分类别名 是否被占用
  • 实现更新文章分类的功能

(2) 定义路由和处理函数

/router/artcate.js 模块中,添加 更新文章分类 的路由:

// 更新文章分类的路由
router.post('/updatecate', artcate_handler.updateCateById)

/router_handler/artcate.js 模块中,定义并向外共享 更新文章分类 的路由处理函数:

// 更新文章分类的处理函数
exports.updateCateById = (req, res) => {res.send('ok')
}

(3) 定义路由和处理函数

/schema/artcate.js 验证规则模块中,使用 exports 向外共享如下的 验证规则对象

// 校验规则对象 - 更新分类
exports.update_cate_schema = {body: {Id: id,name,alias,},
}

/router/artcate.js 模块中,导入需要的验证规则对象,并在路由中使用:

// 导入更新文章分类的验证规则对象
const { update_cate_schema } = require('../schema/artcate')// 更新文章分类的路由
router.post('/updatecate', expressJoi(update_cate_schema), artcate_handler.updateCateById)

(4) 查询分类名称与别名是否被占用

定义查重的 SQL 语句:

// 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句
const sql = `select * from ev_article_cate where Id<>? and (name=? or alias=?)`

调用 db.query() 执行查重的操作:

// 执行查重操作
db.query(sql, [req.body.Id, req.body.name, req.body.alias], (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 判断 分类名称 和 分类别名 是否被占用if (results.length === 2) return res.cc('分类名称与别名被占用,请更换后重试!')if (results.length === 1 && results[0].name === req.body.name) return res.cc('分类名称被占用,请更换后重试!')if (results.length === 1 && results[0].alias === req.body.alias) return res.cc('分类别名被占用,请更换后重试!')// TODO:更新文章分类
})

(5) 实现更新文章分类的功能

定义更新文章分类的 SQL 语句:

const sql = `update ev_article_cate set ? where Id=?`

调用 db.query() 执行 SQL 语句:

db.query(sql, [req.body, req.body.Id], (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// SQL 语句执行成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('更新文章分类失败!')// 更新文章分类成功res.cc('更新文章分类成功!', 0)
})

5.文章管理

5.1 新建 ev_articles 表

在这里插入图片描述

5.2发布新文章

注意:这里是FormData格式,跟之前不一样!!!
在这里插入图片描述

(1) 实现步骤

  • 初始化路由模块
  • 初始化路由处理函数模块
  • 使用 multer 解析表单数据
  • 验证表单数据
  • 实现发布文章的功能

(2) 初始化路由模块

创建 /router/article.js 路由模块,并初始化如下的代码结构:

// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()// 发布新文章
router.post('/add', (req, res) => {res.send('ok')
})// 向外共享路由对象
module.exports = router

app.js 中导入并使用文章的路由模块:

// 导入并使用文章路由模块
const articleRouter = require('./router/article')
// 为文章的路由挂载统一的访问前缀 /my/article
app.use('/my/article', articleRouter)

(3) 初始化路由处理函数模块

创建 /router/article.js 路由模块,并初始化如下的代码结构:

// 导入 express
const express = require('express')
// 创建路由对象
const router = express.Router()// 发布新文章
router.post('/add', (req, res) => {res.send('ok')
})// 向外共享路由对象
module.exports = router

app.js 中导入并使用文章的路由模块:

// 导入并使用文章路由模块
const articleRouter = require('./router/article')
// 为文章的路由挂载统一的访问前缀 /my/article
app.use('/my/article', articleRouter)

(4) 使用 multer 解析表单数据

注意:使用 express.urlencoded() 中间件无法解析 multipart/form-data 格式的请求体数据。

当前项目,推荐使用 multer 来解析 multipart/form-data 格式的表单数据。https://www.npmjs.com/package/multer

运行如下的终端命令,在项目中安装 multer

npm i multer@1.4.2

/router_handler/article.js 模块中导入并配置 multer

// 导入解析 formdata 格式表单数据的包
const multer = require('multer')
// 导入处理路径的核心模块
const path = require('path')
// 创建 multer 的实例对象,通过 dest 属性指定文件的存放路径
const upload = multer({ dest: path.join(__dirname, '../uploads') })

修改 发布新文章 的路由如下:

// 发布新文章的路由
// upload.single() 是一个局部生效的中间件,用来解析 FormData 格式的表单数据
// 将文件类型的数据,解析并挂载到 req.file 属性中
// 将文本类型的数据,解析并挂载到 req.body 属性中
router.post('/add', upload.single('cover_img'), article_handler.addArticle)

/router_handler/article.js 模块中的 addArticle 处理函数中,将 multer 解析出来的数据进行打印:

// 发布新文章的处理函数
exports.addArticle = (req, res) => {console.log(req.body) // 文本类型的数据console.log('--------分割线----------')console.log(req.file) // 文件类型的数据res.send('ok')
})

(5) 验证表单数据

实现思路:通过 express-joi 自动验证 req.body 中的文本数据;通过 if 判断手动验证 req.file 中的文件数据;

创建 /schema/article.js 验证规则模块,并初始化如下的代码结构:

// 导入定义验证规则的模块
const joi = require('@hapi/joi')// 定义 标题、分类Id、内容、发布状态 的验证规则
const title = joi.string().required()
const cate_id = joi.number().integer().min(1).required()
const content = joi.string().required().allow('')
const state = joi.string().valid('已发布', '草稿').required()// 验证规则对象 - 发布文章
exports.add_article_schema = {body: {title,cate_id,content,state,},
}

/router/article.js 模块中,导入需要的验证规则对象,并在路由中使用:

// 导入验证数据的中间件
const expressJoi = require('@escook/express-joi')
// 导入文章的验证模块
const { add_article_schema } = require('../schema/article')// 发布新文章的路由
// 注意:在当前的路由中,先后使用了两个中间件:
//       先使用 multer 解析表单数据
//       再使用 expressJoi 对解析的表单数据进行验证
router.post('/add', upload.single('cover_img'), expressJoi(add_article_schema), article_handler.addArticle)

/router_handler/article.js 模块中的 addArticle 处理函数中,通过 if 判断客户端是否提交了 封面图片

// 发布新文章的处理函数
exports.addArticle = (req, res) => {// 手动判断是否上传了文章封面if (!req.file || req.file.fieldname !== 'cover_img') return res.cc('文章封面是必选参数!')// TODO:表单数据合法,继续后面的处理流程...
})

(6) 实现发布文章的功能

整理要插入数据库的文章信息对象:

// 导入处理路径的 path 核心模块
const path = require('path')const articleInfo = {// 标题、内容、状态、所属的分类Id...req.body,// 文章封面在服务器端的存放路径cover_img: path.join('/uploads', req.file.filename),// 文章发布时间pub_date: new Date(),// 文章作者的Idauthor_id: req.user.id,
}

定义发布文章的 SQL 语句:

const sql = `insert into ev_articles set ?`

调用 db.query() 执行发布文章的 SQL 语句:

// 导入数据库操作模块
const db = require('../db/index')// 执行 SQL 语句
db.query(sql, articleInfo, (err, results) => {// 执行 SQL 语句失败if (err) return res.cc(err)// 执行 SQL 语句成功,但是影响行数不等于 1if (results.affectedRows !== 1) return res.cc('发布文章失败!')// 发布文章成功res.cc('发布文章成功', 0)
})

app.js 中,使用 express.static() 中间件,将 uploads 目录中的图片托管为静态资源:

// 托管静态资源文件
app.use('/uploads', express.static('./uploads'))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/7647.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows系统Tai时长统计工具的使用体验

Windows系统Tai时长统计工具的使用体验 一、Tai介绍1.1 Tai简介1.2 安装环境要求 二、下载及安装Tai2.1 下载Tai2.2 运行Tai工具 三、Tai的使用体验3.1 系统设置3.2 时长统计3.3 分类管理 四、总结 一、Tai介绍 1.1 Tai简介 Tai是一款专为Windows系统设计的开源软件&#xff…

数据结构——二叉树——堆(1)

今天&#xff0c;我们来写一篇关于数据结构的二叉树的知识。 在学习真正的二叉树之前&#xff0c;我们必不可少的先了解一下二叉树的相关概念。 一&#xff1a;树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层…

Vue入门(Vue基本语法、axios、组件、事件分发)

Vue入门 Vue概述 Vue (读音/vju/&#xff0c;类似于view)是一套用于构建用户界面的渐进式框架&#xff0c;发布于2014年2月。与其它大型框架不同的是&#xff0c;Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三…

数据结构:二叉树—面试题(二)

1、二叉树的最近公共祖先 习题链接https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/ 描述&#xff1a; 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点…

使用python-docx包进行多文件word文字、字符批量替换

1、首先下载pycharm。 2、改为中文。 3、安装python-docx包。 搜索包名字&#xff0c;安装。 4、新建py文件&#xff0c;写程序。 from docx import Documentdef replace1(array1):# 替换词典&#xff08;标签值按实际情况修改&#xff09;dic {替换词1: array1[0], 替换…

[操作系统] 进程地址空间管理

虚拟地址空间的初始化 缺页中断 缺页中断的概念 缺页中断&#xff08;Page Fault Interrupt&#xff09; 是指当程序访问的虚拟地址在页表中不存在有效映射&#xff08;即该页未加载到内存中&#xff09;时&#xff0c;CPU 会发出一个中断信号&#xff0c;请求操作系统加载所…

万字长文总结前端开发知识---JavaScriptVue3Axios

JavaScript学习目录 一、JavaScript1. 引入方式1.1 内部脚本 (Inline Script)1.2 外部脚本 (External Script) 2. 基础语法2.1 声明变量2.2 声明常量2.3 输出信息 3. 数据类型3.1 基本数据类型3.2 模板字符串 4. 函数4.1 具名函数 (Named Function)4.2 匿名函数 (Anonymous Fun…

【Linux】21.基础IO(3)

文章目录 3. 动态库和静态库3.1 静态库与动态库3.2 静态库的制作和使用原理3.3 动态库的制作和使用原理3.3.1 动态库是怎么被加载的 3.4 关于地址 3. 动态库和静态库 3.1 静态库与动态库 静态库&#xff08;.a&#xff09;&#xff1a;程序在编译链接的时候把库的代码链接到可…

Linux系统之gzip命令的基本使用

Linux系统之gzip命令的基本使用 一、gzip命令简介二、gzip命令使用帮助2.1 help帮助信息2.2 选项解释 三、gzip命令的基本使用3.1 压缩文件3.2 保留原始文件3.3 解压文件3.4 查看压缩信息3.5 标准输出/输入3.6 批量处理文件3.7 递归解压缩目录3.8测试压缩文件完整性 四、注意事…

【Matlab高端绘图SCI绘图模板】第05期 绘制高阶折线图

1.折线图简介 折线图是一个由点和线组成的统计图表&#xff0c;常用来表示数值随连续时间间隔或有序类别的变化。在折线图中&#xff0c;x 轴通常用作连续时间间隔或有序类别&#xff08;比如阶段1&#xff0c;阶段2&#xff0c;阶段3&#xff09;。y 轴用于量化的数据&#x…

免费SSL证书申请,springboot 部署证书

申请免费域名证书,SSL证书(一共有两个有用&#xff0c;一个是私钥private.key 另一个是certificate.crt) 1、打开网址 申请免费域名证书,SSL证书 2、选择生成CSR 3、生成以后点击下一步&#xff08;private key 有用 ) ​​​​​​​ 4、这里选择 Cname域名解析验证 5、…

Java 大视界 -- Java 大数据中的隐私增强技术全景解析(64)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

深度学习项目--基于LSTM的糖尿病预测探究(pytorch实现)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 LSTM模型一直是一个很经典的模型&#xff0c;一般用于序列数据预测&#xff0c;这个可以很好的挖掘数据上下文信息&#xff0c;本文将使用LSTM进行糖尿病…

js/ts数值计算精度丢失问题及解决方案

文章目录 概念及问题问题分析解决方案方案一方案二方案其它——用成熟的库 概念及问题 js中处理浮点数运算时会出现精度丢失。js中整数和浮点数都属于Number数据类型&#xff0c;所有的数字都是以64位浮点数形式存储&#xff0c;整数也是如此。所以打印x.00这样的浮点数的结果…

vite环境变量处理

环境变量: 会根据当前代码环境产生值的变化的变量就叫做环境变量 代码环境: 开发环境测试环境预发布环境灰度环境生产环境 举例: 百度地图 SDK,小程序的SDK APP_KEY: 测试环境和生产环境还有开发环境是不一样的key 开发环境: 110 生产环境:111 测试环境: 112 我们去请求第三…

Android GLSurfaceView 覆盖其它控件问题 (RK平台)

平台 涉及主控: RK3566 Android: 11/13 问题 在使用GLSurfaceView播放视频的过程中, 增加了一个播放控制面板, 覆盖在视频上方. 默认隐藏setVisibility(View.INVISIBLE);点击屏幕再显示出来. 然而, 在RK3566上这个简单的功能却无法正常工作. 通过缩小视频窗口可以看到, 实际…

Linux 环境变量

目录 一、环境变量的基本概念 1.常见环境变量 2.查看环境变量方法 ​3.几个环境变量 环境变量&#xff1a;PATH 环境变量&#xff1a;HOME 环境变量&#xff1a;SHELL 二、和环境变量相关的命令 三、库函数getenv&#xff0c;setenv 四、环境变量和本地变量 五、命令行…

Redis实战(黑马点评)——涉及session、redis存储验证码,双拦截器处理请求

项目整体介绍 数据库表介绍 基于session的短信验证码登录与注册 controller层 // 获取验证码PostMapping("code")public Result sendCode(RequestParam("phone") String phone, HttpSession session) {return userService.sendCode(phone, session);}// 获…

MYSQL数据库 - 启动与连接

MYSQL数据库的启动&#xff1a; 一 在cmd控制界面以管理员身份运行 执行语句: net start mysql80 net stop mysql80 二 MYSQL数据库客户端建立连接&#xff1a; 1 该种方法是使用windows系统的cmd界面&#xff0c;需要配置相关路径path 2 使用MYSQL自带的

【Salesforce】审批流程,代理登录 tips

审批流程权限 审批流程权限问题解决方案代理登录代理登录后Logout 审批流程权限 前几天&#xff0c;使用审批流程&#xff0c;但是是两个sandbox&#xff0c;同样的配置&#xff0c;我有管理员权限。但是profile不是管理员&#xff0c;只是通过具备管理员权限的permission set…