ai-要約を取得 文章摘要

双招项目后端api全过程实现系列

  1. 双招项目后端api全过程实现(一)- 登录&任务模块 ⇦当前位置🪂
  2. 双招项目后端api全过程实现(二)- 合约模块
  3. 双招项目后端api全过程实现(三)- 消息模块
  4. 双招项目后端api全过程实现(四)- 我的个人中心模块

1.初始化

点击查看详情

1.1创建项目:

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

    npm init -y
  2. 运行如下的命令,安装特定版本的 express

    pnpm i express@4.17.1
  3. 在项目根目录中新建 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.首先安装
pnpm i cors@2.8.5

然后在 app.js 中配置

// 导入 express
const express = require('express')
// 创建 express 实例
const app = express()

# 1.2.2.配置 cors 跨域中间件
const cors = require('cors')
app.use(cors())

# 1.2.3.配置解析表单数据的中间件,仅能解析 application/x-www-form-urlencoded 类型的表单数据
app.use(express.urlencoded({ extended: false }))

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

// 启动服务器
app.listen(3007, () => {
console.log('服务器启动成功: http://127.0.0.1:3007')
})

1.3 初始化路由文件&文件夹:

  1. 在项目根目录中,新建 router 文件夹,用来存放所有的路由模块

    路由模块中,只存放客户端的请求与处理函数之间的映射关系

  2. 在项目根目录中,新建 router_handler 文件夹,用来存放所有的 路由处理函数模块

    路由处理函数模块中,专门负责存放每个路由对应的处理函数

1.4初始化路由模块:

  1. 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
  1. 初始化路由处理函数:/api_server/router_handler/user.js

    // 注册用户的处理函数
    exports.regUser = (req, res) => {
    res.send('reguser OK')
    }

    // 登录的处理函数
    exports.login = (req, res) => {
    res.send('login OK')
    }
  1. app.js 中,导入并使用 用户路由模块

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

2.创建 mysql 数据库

点击查看详情

2.1 新建数据库&表

在 MySQL Shell 中,你已经启动了交互式 Shell,所以不需要再次输入 mysqlsh进行启动。

下面是正确的步骤来连接到数据库:

\sql

// 端口号默认即为:3006
\connect root@localhost

// 输入密码后,即可以连接到数据库

// 然后执行下面一行代码,创建数据库表
CREATE DATABASE shuangzhao_iooio CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci

其中:CHARACTER SET utf8mb4:指定数据库的字符集为 utf8mb4,支持完整的 Unicode,包括表情符号。
COLLATE utf8mb4_general_ci:设置排序规则为不区分大小写的通用规则。

// 然后设置默认数据库
USE shuangzhao_iooio

然后,根据项目要求创建数据库表;

-- 创建用户信息表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
accounts VARCHAR(20) NOT NULL COMMENT '用户账号(手机号)',
user_name VARCHAR(50) NOT NULL COMMENT '用户名',
sex TINYINT NOT NULL COMMENT '性别(1: 男, 2: 女, 0: 未知)',
birthday DATE COMMENT '出生日期',
pay_password VARCHAR(255) DEFAULT NULL COMMENT '支付密码(加密存储)',
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
work_time DATE COMMENT '参加工作时间',
head_img VARCHAR(255) DEFAULT NULL COMMENT '头像URL',
idcard VARCHAR(20) DEFAULT NULL COMMENT '身份证号码',
account_price DECIMAL(10, 2) DEFAULT 0 COMMENT '账户余额',
is_check TINYINT DEFAULT 0 COMMENT '审核状态(0: 未审核, 1: 已审核)',
it_enterprise TINYINT DEFAULT 0 COMMENT '是否为IT企业(1: 是, 0: 否)',
enterprise TINYINT DEFAULT 0 COMMENT '是否为企业用户(1: 是, 0: 否)',
manage TINYINT DEFAULT 0 COMMENT '是否为管理员(1: 是, 0: 否)',
token VARCHAR(512) DEFAULT NULL COMMENT '登录Token',
expire_time BIGINT DEFAULT NULL COMMENT 'Token过期时间(时间戳)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表';
  • 以上代码根据原始项目数据格式,由 chatgpt 快速生成,实际开发中需要做少许调整,例如增加 code 验证码,以及过期时间

实际开发中,根据项目需求,需要对表结构进行修改,以下是增加 column 的方法示例

ALTER TABLE tasklist 
ADD COLUMN head_img VARCHAR(255) DEFAULT NULL COMMENT '用户头像链接地址';

3.为项目后端项目安装 mysql2 模块

点击查看详情
  1. 执行以下命令,安装 mysql

    pnpm i mysql2
  2. 在项目根目录中新建 /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
  3. 这里需要特别注意,将来在查询数据库时,返回的结果较之前 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

/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/

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

// 导入 bcryptjs 加密模块
const bcrypt = require('bcryptjs')

// 导入生成 Token 的方法
const jwt = require('jsonwebtoken')


// 导入配置文件
const config = require('../config')

// 生成随机验证码
function generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString()
}

const regUser = async (req, res) => {
try {
// 获取客户端提交的数据
const userInfo = req.body

// if (!userInfo.accounts) {
// // return res.send({ status: 1, message: '手机号码不能为空!' });
// return res.cc('手机号不能为空',1)
// }

const code = generateCode() // 生成 6 位随机验证码
const codeExpiresAt = new Date(Date.now() + 5 * 60 * 1000) // 验证码有效期 5 分钟

const codeBcrypt = bcrypt.hashSync(code, 10) // 对验证码进行加密处理

// 查询是否存在相同手机号码的用户
const sqlStr = 'SELECT * FROM users WHERE accounts = ?'
const rows = await db.query(sqlStr, userInfo.accounts) // 参数数组必须使用 [], 只有一个参数时可以省略

// 查询语句得到的是一个数组,如果数组长度大于 0,说明用户已存在
if (rows.length > 0) {
// 如果用户已存在,更新验证码和过期时间
const updateSql = 'UPDATE users SET code = ?, code_expires_at = ? WHERE accounts = ?'
await db.query(updateSql, [codeBcrypt, codeExpiresAt, userInfo.accounts])
} else {
// 如果用户不存在,插入新用户记录
const insertSql = 'INSERT INTO users (accounts, code, code_expires_at) VALUES (?, ?, ?)'
await db.query(insertSql, [userInfo.accounts, codeBcrypt, codeExpiresAt])
}

// 返回验证码(开发阶段用,生产环境中不要返回验证码)
res.send({
code: 200,
success: '验证码已生成',
result: {
code: code
}
})
} catch (error) {
// res.status(500).send({ status: 1, message: '服务器内部错误' });
res.cc(error,500)
}
}

// 登录的处理函数
const login = async (req, res) => {
try {
const userInfo = req.body // 获取用户提交的数据
console.log('userInfo:', userInfo)


// 1. 验证输入
if (!userInfo.code) return res.cc("验证码不能为空!")

// 2. 查询用户信息
const sqlStr = "SELECT * FROM users WHERE accounts = ?"
const [results] = await db.query(sqlStr, [userInfo.accounts]) // 确保 `db.query` 返回一个 Promise
if (results.length !== 1) return res.cc("没有查到用户信息!")

let user = results[0]

// 3. 检查验证码是否正确
const compareResult = bcrypt.compareSync(userInfo.code, user.code)
if (!compareResult) return res.cc("验证码错误!")

// 4. 检查验证码是否过期
const now = Date.now()
if (now > user.code_expires_at) return res.cc("验证码已过期,请重新获取!")

// 生成 Token 字符串
user = {...results[0], code: '你猜', code_expires_at: '你猜', head_img: '你猜'}
// 对信息加密
const tokenStr = jwt.sign(user, config.jwtSecretKey, { expiresIn: '30d' })

// 5. 登录成功
res.send({
code: 200,
success: "ok",
result: {
errCode: 200,
msg: "登录成功",
data: {
user_info: {
accounts: user.accounts,
id: user.id,
user_name: user.user_name,
sex: user.sex,
birthday: user.birthday,
pay_password: user.pay_password,
email: user.email,
work_time: user.work_time,
head_img: user.head_img,
idcard: user.idcard,
account_price: user.account_price,
is_check: user.is_check,
it_enterprise: user.it_enterprise,
enterprise: user.enterprise,
manage: user.manage
},
token: "Bearer " + tokenStr,
expireTime: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7 天后的时间戳
}
}
})
} catch (error) {
// 捕获异常
console.error(error)
res.cc("服务器内部错误!")
}
}


module.exports = { regUser, login }

2.登录验证

3.阅读协议

  1. 首先,创建数据库表
CREATE TABLE protocols (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT,
isdelete TINYINT DEFAULT 0,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
sort INT DEFAULT 0,
type INT DEFAULT 1
);

-- 插入测试数据
INSERT INTO protocols (title, content, isdelete, sort, type)
VALUES
('《用户协议》', '尊敬的用户:


在您成为IT企业服务注册用户,使用IT企业服务提供的服务之前,请您认真阅读IT企业服务《用户协议》(以下简称“协议”),更好地了解我们所提供的服务以及您享有的权利和承担的义务。您一旦开始使用IT企业服务服务,即表示您已经确认并接受了本文件中的全部条款。


本协议系由您(以下简称“用户”或“您”)与北京北京驻场无忧科技有限公司(以下简称“IT企业服务”或“我们”)就IT企业服务公司提供的IT企业服务软件(以下简称“本平台”或“IT企业服务”)所订立的相关权利义务规范。请您在注册、使用IT企业服务之前,认真阅读以下条款。


(温馨提示:为了您的合法权益不受损害,平台建议您在使用IT企业服务找工作前,仔细阅读《安全求职指南》,谨防各类求职陷阱。)


一、注册条款的接受


一旦您在注册页面点击或勾选“阅读并同意接受用户协议及隐私政策”相关内容后,即表示您已经阅读并且同意与IT企业服务公司达成协议,成为IT企业服务的用户,并接受本协议中的全部注册条款以及IT企业服务《隐私政策》和本平台内公布的其他专项协议或规则,包括但不限于IT企业服务《职位信息发布规则》《IT企业服务增值服务协议》(以下简称“本平台规则”)的所有条款的约束。


二、用户注册条件


1. 申请注册成为IT企业服务的用户应同时满足下列全部条件:在注册之日以及此后使用IT企业服务服务期间必须以招聘和/或求职为目的;在注册之日必须年满16周岁以上。


2. 为了更好地享有IT企业服务网络信息发布平台服务,用户应遵守IT企业服务注册机制的要求,向IT企业服务提供本人真实、准确、最新及完整的资料;如注册并认证成为招聘用户,应保证及时更新本人提供的“单位名称、职务或岗位信息、企业邮箱”等相关授权信息及材料,并确保前述授权的真实性;用户应保证其职务行为,包括但不限于发布招聘信息、与求职者沟通等均在使用本平台期间持续有效;通过认证的用户应保持其招聘账号与对应的授权单位具有唯一性。


3. 若用户提供任何错误、不实或不完整的资料,或IT企业服务有理由怀疑资料为错误、不实或不完整及违反用户注册条款的,或IT企业服务有理由怀疑其用户资料、言行等有悖于“严肃纯净的招聘APP”主题或违反《IT企业服务职位信息发布规则》的,IT企业服务有权修改用户的注册昵称、个人说明、发布的信息等,或暂停或终止该用户使用其账号,或暂停或终止提供IT企业服务提供的全部或部分服务。


4. 若用户故意提供虚假的身份信息、公司信息进行注册,发布虚假招聘信息或者求职信息的,视为严重违反本协议,IT企业服务有权暂停或终止该用户账号并停止提供服务。虚假注册、发布虚假信息给IT企业服务造成经济、名誉等任何损失的,IT企业服务将追究该用户的法律责任。


三、用户账号安全


1. 用户的账号遭到未获授权的使用,或者发生其他任何安全问题时,用户应立即通知IT企业服务。由于用户使用不当或者其他非因IT企业服务导致的账号、密码泄漏,进而导致其资料、信息泄漏的,由用户承担其不利后果。


2. IT企业服务账号的所有权归IT企业服务公司所有,用户完成账号注册程序后,获得IT企业服务账号的使用权,且该使用权仅属于账号初始注册人。同时,用户不得赠与、借用、租用、转让或售卖IT企业服务账号或者以其他方式许可他人使用IT企业服务账号。其他人不得通过受赠、继承、承租、受让或者其他任何方式使用IT企业服务账号。如果我们发现或者有合理理由认为账号使用者并非账号初始注册人,为保障账号安全,我们有权立即暂停或终止向该注册账号提供服务,并有权永久禁用该账号。


3. 用户不得将账号主动告知第三方或提供给第三方进行使用,例如提供给第三方进行代为购买IT企业服务服务等。如因此造成其他用户隐私泄露或经济损失以及本平台损失的,用户应当承担全部责任。


四、服务说明


1. 基于风控策略/安全风险/产品政策等的需要,IT企业服务可能要求部分用户补充提供材料(包括但不限于企业资质证明、承诺书、业务协议等),具体要求会在相关页面上做明确展示。如用户拒绝提供前述材料,IT企业服务有权视情况暂停或终止向该用户提供部分或全部服务。


2. 为落实《网络招聘服务管理规定》的核验更新义务,IT企业服务可能会不定期对部分用户的企业地址、招聘授权等相关信息进行真实性审查以及更新核验(目前,该审核机制包括“环境认证”和“线下审核”)。审查过程中,用户应配合 IT企业服务工作人员补充相关资料(包括但不限于营业执照、资质证书/相关业务协议、授权书、被授权人身份证信息、租赁协议/水电费记录等),并允许 IT企业服务工作人员对其企业 LOGO、办公环境进行审查及拍照备案(具体要求见网页说明或“小秘书”通知)。若用户拒绝,IT企业服务将视情况决定是否恢复该用户使用账号,或者暂停或终止为该用户提供部分或全部的招聘、求职服务。


3. 对于利用IT企业服务进行非法活动,或其言行(无论线上或者线下的)背离IT企业服务严肃招聘目的的,IT企业服务将严肃处理,包括将其列入黑名单、将其被投诉的情形公之于众、删除用户账号等处罚措施,给IT企业服务造成经济或者名誉等任何损失的,IT企业服务将追究其法律责任。


4. IT企业服务有权通过拨打电话、发送短信或电子邮件等方式,告知用户IT企业服务服务相关的广告信息、促销等营销信息,以及邀请用户参与版本测试、用户体验反馈、回访等活动。除系统通知或重要信息外,用户可以通过IT企业服务提供的方式选择不接收上述信息。


5. 为提高IT企业服务用户求职招聘的成功率和效率,IT企业服务公司可能会将IT企业服务用户的信息公开展示范围扩大至我们的第三方合作平台,因此,您在使用IT企业服务时,可能还会收到来自IT企业服务合作的其他平台上的注册用户向您开聊、联系电话、视频面试邀请等相关信息。在此期间,您可通过IT企业服务《隐私政策》了解我们如何保障您的个人信息的安全。


6. 用户应通过本平台使用相关服务,未经许可,不得通过其他第三方工具或运营平台获取IT企业服务服务,包括但不限于通过第三方软件登录IT企业服务账号、发布职位、浏览职位、收发简历等。如因用户使用第三方软件导致相关信息泄漏的,IT企业服务不承担任何责任,且用户还应承担由此给IT企业服务造成的损失。


五、有限责任条款


1. IT企业服务公司将尽力为用户提供提供安全、及时、准确、高质量的服务,但不保证一定能满足用户的要求和期望,也不保证服务不会中断,对服务的及时性、安全性、准确性都不作保证。除非另有约定,否则用户因无法使用IT企业服务服务,或使用服务未达到心理预期的,IT企业服务不承担责任。


2. 对于用户通过IT企业服务公司提供的服务传送的内容,IT企业服务会尽合理努力按照国家有关规定严格审查,但无法完全控制经由软件/网站服务传送的内容,不保证内容的正确性、完整性或品质。因此用户在使用IT企业服务服务时,可能会接触到令人不快、不适当或令人厌恶的内容。在任何情况下,IT企业服务均不为用户经由软件/网站服务以张贴、发送电子邮件或其它方式传送的任何内容负责。但IT企业服务有权依法停止传输任何前述内容并采取相应行动,包括但不限于暂停用户使用软件/网站服务的全部或部分,保存有关记录,并根据国家法律法规、相关政策在必要时向有关机关报告并配合有关机关的行动。


3. 对于IT企业服务提供的各种第三方广告信息、链接、资讯等(如有),IT企业服务不保证其内容的正确性、合法性或可靠性,相关责任由广告主承担;并且,对于用户经由IT企业服务服务与广告主进行联系或商业往来,完全属于用户和广告主之间的行为,与IT企业服务无关。对于前述商业往来所产生的任何损害或损失,IT企业服务不承担任何责任。


4. 对于用户上传的照片、资料、证件、视频、内容及图片等,IT企业服务已采用相关措施并已尽合理努力进行审核,但不保证其内容的正确性、合法性或可靠性,相关责任由上传上述内容的用户承担。


5. 用户应对IT企业服务上的其他用户发布的内容自行加以判断,并承担因使用内容而引起的所有风险,包括但不限于因对内容的正确性、完整性或实用性的依赖而产生的风险。IT企业服务公司无法且不会对因前述风险而导致的任何损失或损害承担责任。


6. 是否使用软件/网站服务下载或取得任何资料应由用户自行考虑并自负风险,因任何资料的下载而导致的用户电脑系统的任何损坏或数据丢失等后果,IT企业服务不承担任何责任。


7. 对于IT企业服务公司在线上或线下策划、发起、组织或是承办的任何招聘相关的活动(包括但不限于收取费用以及完全公益的活动),IT企业服务不对上述招聘效果向用户作出任何保证或承诺,也不担保活动期间用户自身行为的合法性、合理性。由此产生的任何对于用户个人或者他人的人身或者是名誉以及其他损害,应由行为实施主体承担责任。


8. 对于用户的投诉,IT企业服务将尽合理努力进行核实和处理,但不保证一定能满足投诉者的要求。IT企业服务有权决定是否向公众或向被投诉者公开投诉内容。对于投诉内容侵犯用户隐私权、名誉权等合法权益的,所有法律责任由投诉者承担,与IT企业服务无关。


六、用户权利


用户对于自己的个人信息享有以下权利:

1. 随时查询及请求阅览,但因极少数特殊情况(如被网站加入黑名单等)无法查询及提供阅览的除外;
1. 随时查询及请求阅览,但因极少数特殊情况(如被网站加入黑名单等)无法查询及提供阅览的除外;


2. 随时请求补充或更正,但因极少数特殊情况(如网站或有关机关为司法程序保全证据等)无法补充或更正的除外;


七、用户应承诺其平台使用行为遵守以下规定


1. 本协议所称“平台使用”是指用户使用本平台服务所进行的任何行为,包括但不限于注册、登录、认证、查看开聊、账号管理、发布招聘信息、邀约面试以及其他通过IT企业服务账号在本平台所进行的一切行为。


2. IT企业服务公司提醒用户在使用IT企业服务服务时,应遵守《中华人民共和国民法典》《中华人民共和国著作权法》《全国人民代表大会常务委员会关于维护互联网安全的决定》《中华人民共和国保守国家秘密法》《中华人民共和国电信条例》《中华人民共和国计算机信息系统安全保护条例》《中华人民共和国计算机信息网络国际联网管理暂行规定》《计算机信息系统国际联网保密管理规定》《互联网信息服务管理办法》《计算机信息网络国际联网安全保护管理办法》《互联网电子公告服务管理规定》《网络安全法》《中华人民共和国劳动法》《中华人民共和国劳动合同法》《网络信息内容生态治理规定》等相关中国法律法规的规定。


3. 在任何情况下,如果IT企业服务公司有理由认为用户使用IT企业服务服务过程中的任何行为,包括但不限于用户的任何言论和其它行为违反或可能违反上述法律和法规的任何规定,IT企业服务公司可在任何时候不经任何事先通知终止向该用户提供服务。


4. 用户承诺在使用IT企业服务期间,遵守法律法规、社会主义制度、国家利益、公民合法权益、公共秩序、社会道德风尚和信息真实性等七条底线。


5. 您理解并同意,本平台仅为用户提供招聘信息分享、传播及获取招聘、求职机会的平台,您必须为自己的注册、认证账号下的一切行为负责,包括您所发表的任何内容以及由此产生的任何后果。


6. 用户使用本平台服务进行招聘或求职的,还应遵守IT企业服务《职位信息发布规则》。


八、禁止用户利用IT企业服务从事下列行为


禁止用户在IT企业服务平台或利用IT企业服务提供的服务,制作、发送、复制、发布、传播违反国家相关法律法规、七条底线、九不准管理规定、本平台规则的信息、从事违反前述规定/规则的活动,主要表现为:


1. 反对宪法所确定的基本原则的。


2. 危害国家安全,泄露国家秘密,颠覆国家政权,破坏国家统一的。


3. 损害国家荣誉和利益的;煽动民族仇恨、民族歧视、破坏民族团结的。


4. 破坏国家宗教政策,宣扬邪教和封建迷信的。


5. 散布谣言,扰乱社会秩序,破坏社会稳定的。


6. 散布淫秽、色情、赌博、暴力、凶杀、恐怖或者教唆犯罪的。


7. 侮辱或者诽谤他人,侵害他人合法权益的。


8. 含有虚假、有害、胁迫、侵害他人隐私、骚扰、侵害、中伤、粗俗、猥亵、或有悖道德、令人反感的内容的。


9. 含有中国法律、法规、规章、条例以及任何具有法律效力的规范所限制或禁止的其他内容的。


10. 使用IT企业服务服务的过程中,以任何方式危害求职者合法权益的。


11. 冒充任何人或机构,包含但不限于冒充IT企业服务工作人员或以虚伪不实的方式陈述或谎称与任何人或机构有关的。


12. 发布、传播侵犯任何人的肖像权、名誉权、隐私权、专利权、商标权、著作权、商业秘密的信息或言论的。


13. 将病毒或其它计算机代码、档案和程序,加以上载、张贴、发送电子邮件或以其它方式传送的。


14. 跟踪或以其它方式骚扰其他用户的。


15. 未经合法授权而截获、篡改、收集、储存或删除他人个人信息、电子邮件或其它数据资料,或将获知的此类资料用于任何非法或不正当目的。


16. 以任何方式干扰或企图干扰IT企业服务的任何产品、任何部分或功能的正常运行,或者制作、发布、传播上述工具、方法等。


17. 未能按照本平台的流程、规则进行注册、认证或使用本服务的,违反本服务功能限制或运营策略,或采取任何措施规避前述流程、规则、限制或策略的。


18. 未经IT企业服务公司的许可使用插件、外挂或通过其他第三方工具、运营平台或任何服务接入本服务和相关系统的。


19. 利用IT企业服务账号或本平台服务从事,包括但不限于欺诈、传销、刷流量、好评、违法物品营销等任何违法兼职或犯罪活动的。


20. 仿冒、混淆他人账号昵称、头像、功能介绍或发布招聘内容等,或冒充、利用他人名义对外招聘的。


21. 未经IT企业服务公司的许可,以任何目的自行或授权、允许、协助任何第三人对平台内的任何信息内容进行非法获取,用于商业用途或其他任何目的。“非法获取”是指采用包括但不限于“”(spider)程序、爬虫程序、拟人程序等非真实用户或避开、破坏技术措施等非正常浏览的手段、方式,读取、复制、转存、获得数据和信息内容的行为。


22. 为任何注册用户或非注册用户提供自动登录到本平台、代办或协助他人代办身份认证服务的或代售身份认证所需的相关材料或凭据等。


23. 任何导致或可能导致IT企业服务公司与第三方产生纠纷、争议或诉讼的行为。


九、特别规定


1. 用户如违反本协议第八条,IT企业服务有权在任何时候不经任何事先通知暂停或终止向该用户提供服务。


2. 用户有下列行为或发布/散布/传播如下相关信息的,IT企业服务在发现或接到投诉后,有权采取冻结账号、升级认证或以其他方式暂停向该用户提供服务,并要求用户承担相应的损害赔偿责任:


(1)涉及广告(寻求合作)、传销或直销等内容


(2)涉及色情、淫秽内容


(3)涉及违法/政治敏感内容


(4)虚假信息,包括但不限于不真实的公司信息、薪资、身份、个人简历、职位信息等


(5)利用IT企业服务提供的服务索取他人隐私


(6)涉及人身攻击或其他侵害他人权益的内容


(7)未成年人工作信息


(8)招聘他人从事违法活动


(9)以培训费、服装费等名义骗取求职者财物


(10)骚扰其他用户


(11)不符合IT企业服务相关服务性质的信息,如鸡汤、段子、水贴等


(12)利用本平台可能存在的漏洞恶意充值直豆、获取道具等虚拟产品或服务


(13)在本平台以外的任何第三方平台(包括但不限于淘宝、闲鱼等)售卖道具等虚拟产品或服务的行为


(14)通过第三方平台或渠道(如淘宝店铺等)购买道具等虚拟产品或服务


(15)涉嫌拖欠/未按法律规定支付薪资/劳务报酬的,或涉嫌具有其他可能损害劳动者或劳务人员合法权益的。(涉及农民工或涉众的均属于“情节严重”)本平台有权对前述情形进行处置,相关判断方式包括但不限于因上述行为被列入相关政府部门“黑名单”、被多名用户举报投诉或被新闻媒体曝光等情形


(16)两位用户名下认证的账号,经系统判定为关联账号的,如其中一个账号因违法违规被冻结的,其他关联账号均将被同时冻结。


(17)其他违反法律法规或国家政策以及损害IT企业服务及其合法用户之合法权益的行为

3. 根据我国现行的法律法规等相关规定,如用户实施前述第(4)项“发布虚假信息”的,包括但不限于用户发布的职位信息与其实际招聘的职位不符的,如用户实际招聘的职位为“保险销售、信用卡销售、理财产品销售、地产中介或销售或劳务派遣”,与其发布的职位信息在内容、类型或其他方面并非一致或对应的甚至不存在,IT企业服务公司随时有权拒绝向该用户提供服务,并可采取其他处理措施,包括但不限于“永久性封禁账号”、“永久性将其设备号、手机号等相关信息冻结”或“永久性加入名单’”等。
3. 根据我国现行的法律法规等相关规定,如用户实施前述第(4)项“发布虚假信息”的,包括但不限于用户发布的职位信息与其实际招聘的职位不符的,如用户实际招聘的职位为“保险销售、信用卡销售、理财产品销售、地产中介或销售或劳务派遣”,与其发布的职位信息在内容、类型或其他方面并非一致或对应的甚至不存在,IT企业服务公司随时有权拒绝向该用户提供服务,并可采取其他处理措施,包括但不限于“永久性封禁账号”、“永久性将其设备号、手机号等相关信息冻结”或“永久性加入名单’”等。


十、隐私政策


IT企业服务依法保护用户个人信息和隐私信息。有关隐私政策的内容,详见IT企业服务《隐私政策》。


十一、关于用户在IT企业服务的上传或张贴的内容


1. 用户在IT企业服务上传或张贴的内容(包括但不限于照片、文字、面试经历及心得评价等),视为用户授予IT企业服务公司及其关联公司免费、非独家的使用权,IT企业服务有权为展示、传播及推广前述张贴内容的目的,对上述内容进行复制、修改、出版等。该使用权持续至用户书面通知IT企业服务不得继续使用,且IT企业服务实际收到该等书面通知时止。


2. 因用户上传或张贴的内容侵犯他人权利,而导致任何第三方向IT企业服务公司提出侵权或索赔要求的,用户应承担全部责任。


3. 任何第三方对于用户在IT企业服务的公开使用区域张贴的内容进行复制、修改、编辑、传播等行为的,该行为产生的法律后果和责任均由行为人承担,与IT企业服务无关。


十二、关于面试聊天等即时通讯服务


1. 用户在接受IT企业服务提供与IT企业服务注册用户或IT企业服务关联方用户进行提在线开聊、邀约面试等即时通讯服务时,应当遵守法律法规、社会主义制度、国家利益、公民合法权益、公共秩序、社会道德风尚,并保证所传输的信息真实性等七条底线。


2. 用户通过本平台与他人在线开聊、拨打电话以及视频面试等商务场景下产生的文字、语音及视频等形式的沟通信息,IT企业服务将会根据法律规定暂时存储,且仅用于投诉举报的处理、安全风控及离线暂存功能的实现。


3. IT企业服务对该信息的采集、传输及存储均会采取加密、防泄露等相关措施。


4. 为保护其他用户隐私,您不得下载、传播或公开发布本条规定的其他用户通讯信息,如面试聊天记录等。如因此造成IT企业服务损失,或者侵害其他用户权益的,您应当承担违约责任或赔偿责任。


十三、信息储存和限制


IT企业服务有权制定一般措施及限制,包含但不限于软件服务将保留的电子邮件、聊天信息、所张贴内容或其他上载内容的最长期间、每个账号可收发沟通讯息的最大数量及可收发的单个消息的大小。通过服务存储或传送之任何信息、通讯资料和其他内容,如被删除或未予储存,IT企业服务不承担任何责任。


十四、结束服务


用户若反对任何注册条款的内容或对之后注册条款修改有异议,或对IT企业服务服务不满,用户有以下权利:不再使用IT企业服务服务;结束用户使用IT企业服务服务的资格;通知IT企业服务停止该用户的服务。结束用户服务的同时,用户使用IT企业服务服务的权利立即终止,IT企业服务不再对用户承担任何义务。


十五、禁止商业行为


1. 用户同意不对IT企业服务提供的服务或服务的任何部分,进行复制、拷贝、出售、转售或用于任何其他商业目的。


2. 禁止通过职位向应聘者收费,如有不实,我们将结束用户使用IT企业服务服务的资格。


十六、违约责任


1. 用户使用虚假身份信息、公司信息进行注册,发布虚假招聘、求职信息,发布含有传销、色情、反动等严重违法内容,对外传播面试聊天等通讯记录等行为,视为严重违反本协议,应当承担给IT企业服务公司造成的经济损失和名誉损失。


2. 因用户通过IT企业服务提供的服务提供、张贴或传送内容、违反本服务条款、或侵害他人任何合法权益而导致任何第三人对IT企业服务提出任何索赔或请求,用户应当赔偿IT企业服务或其他合作伙伴的损失,包括但不限于赔偿金额、律师费和合理的调查费用等。


3. 用户在投诉其他用户有违法行为或违反本注册条款情形时,投诉者应承担不实投诉所产生的全部法律责任。如侵犯他人的合法权益,投诉人应独立承担全部法律责任。如给IT企业服务造成损失的,投诉人应对IT企业服务承担相应的赔偿责任。


十七、本协议条款的变更和修改


IT企业服务有权依法随时对本协议的任何条款进行变更和修改。一旦发生条款变动,我们将在IT企业服务软件内进行更新及提示,或将最新版本的《用户协议》以系统消息、弹框或邮件的形式发送给用户阅读及确认接收。用户如果不同意条款的修改,应主动停止使用IT企业服务或申请注销IT企业服务账号,如未使用的付费权益将在注销后清空。否则,如果用户继续使用用户账号,则视为用户已经接受本协议全部条款的修改。


十八、不可抗力


1. “不可抗力”是指IT企业服务不能合理控制、不可预见或即使预见亦无法避免的事件,该事件妨碍、影响或延误IT企业服务根据本注册条款履行其全部或部分义务。该事件包括但不限于政府行为、自然灾害、、黑客袭击、电脑病毒、网络故障等。不可抗力可能导致IT企业服务无法访问、访问速度缓慢、存储数据丢失、用户个人信息泄漏等不利后果。


2. 遭受不可抗力事件时,IT企业服务可中止履行本协议项下的义务直至不可抗力的影响消除为止,并且不因此承担违约责任;但应尽最大努力克服该事件,减轻其负面影响。


十九、通知


IT企业服务向其用户发出的通知,将采用系统消息、弹窗、电子邮件或页面公告等形式。本《用户协议》的条款修改或其他事项变更时,IT企业服务可以以上述形式进行通知。
', 0, 50, 2);

5.定义一个全局中间件 res.cc

定义 res.cc 用于简化错误的响应逻辑 点击查看详情

首先在 app.js 中定义 res.cc 这个中间件

// 响应数据的中间件
app.use((req, res, next) => {
res.cc = (err, status = 1) => {
// status 默认值为 1,表示失败的情况
// err 可能是一个 Error 对象,也可能时一个描述错误信息的字符串
res.send({
status,
message: err instanceof Error ? err.message : err
})
}
next()
})

然后,在 处理函数中直接使用

exports.regUser = async (req, res) => {
try {
// 获取客户端提交的数据
const userInfo = req.body;

if (!userInfo.accounts) {
// return res.send({ status: 1, message: '手机号码不能为空!' });
return res.cc('手机号不能为空')
}

const code = generateCode(); // 生成 6 位随机验证码
const codeExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 验证码有效期 5 分钟

codeBcrypt = bcrypt.hashSync(code, 10); // 对验证码进行加密处理

// 查询是否存在相同手机号码的用户
const sqlStr = 'SELECT * FROM users WHERE accounts = ?';
const rows = await db.query(sqlStr, userInfo.accounts); // 参数数组必须使用 [], 只有一个参数时可以省略

// 查询语句得到的是一个数组,如果数组长度大于 0,说明用户已存在
if (rows.length > 0) {
// 如果用户已存在,更新验证码和过期时间
const updateSql = 'UPDATE users SET code = ?, code_expires_at = ? WHERE accounts = ?';
await db.query(updateSql, [codeBcrypt, codeExpiresAt, userInfo.accounts]);
} else {
// 如果用户不存在,插入新用户记录
const insertSql = 'INSERT INTO users (accounts, code, code_expires_at) VALUES (?, ?, ?)';
await db.query(insertSql, [userInfo.accounts, codeBcrypt, codeExpiresAt]);
}

// 返回验证码(开发阶段用,生产环境中不要返回验证码)
res.send({
code: 200,
success: '验证码已生成',
result: {
code: code,
}
});
} catch (error) {
res.cc(error,500)
}
};

需要注意的是:

代码已经使用了现代的 async/await 风格,避免了传统的回调函数。传统回调函数的用法如下:

db.query(sql, params, (err, results) => {
if (err) {
// 处理错误
console.error(err);
return;
}
// 处理查询结果
console.log(results);
});

而在使用 Promiseasync/await 时,写法变成这样:

try {
const [rows] = await db.query(sql, params); // `rows` 是查询结果
console.log(rows);
} catch (error) {
res.cc(error,500) // 处理错误
}

回调函数的本质已经由 Promise 替代

  • 在你的代码中,db.query() 是一个 Promise,通过 await 获取它的结果。
  • 数据库操作完成后,返回的结果(或者错误)会自动传递给 awaitcatch,不需要显式地定义回调函数。

操作数据库后的回调函数已经被隐式地替代为 Promiseresolvereject 方法:

  • 如果查询成功,Promise 的结果会被赋值给 rows
  • 如果查询失败,错误会被抛出,进入 catch 块。

总结

在现代 JavaScript 中,推荐使用 Promiseasync/await 进行异步操作,因为它比传统的回调函数写法更简洁、更易读。如果你正在使用的是 mysql2/promise 模块,你的代码中不需要显式定义回调函数。

其实,严格来说,以上代码不是一个错误处理中间件,而是一个响应辅助中间件。它的主要作用是为 res 对象添加一个 res.cc 方法,用于统一处理成功或失败的响应消息。

为什么不是错误处理中间件?

  1. 错误处理中间件的定义
    错误处理中间件是专门用于捕获和处理请求处理过程中发生的错误的,它的函数签名和普通中间件不同:

    app.use((err, req, res, next) => {
    // 这才是错误处理中间件
    });

    错误处理中间件的第一个参数必须是 err,这个参数由 next(error) 传递过来,Express 会自动将错误传递给这样的中间件。

    名字不强制一致
    err 是错误处理中间件中常见的约定,但你可以自由命名它。

  2. 该中间件不接收 err 参数
    上面定义的 res.cc 是一个工具方法,用于格式化响应内容,它并没有处理通过 next(error) 抛出的错误。因此,这不能算作一个错误处理中间件


这个中间件的作用

  1. 为 res 对象添加一个统一的响应方法 (res.cc)
    • 成功的响应:res.cc('操作成功', 0)
    • 失败的响应:res.cc('操作失败')res.cc(new Error('错误信息'))
  2. 简化了代码书写
    你可以通过 res.cc 快速发送 JSON 格式的响应,而不需要每次手动写 res.send({ status, message })

6.全局错误中间件

点击查看详情

app.js 中,定义全局错误处理中间件

// 响应数据的中间件
app.use((req, res, next) => {
res.cc = (err, status = 1) => {
res.send({
status,
message: err instanceof Error ? err.message : err,
});
};
next();
});

// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error('捕获到错误:', err); // 打印错误日志
res.cc(err, 500); // 使用 res.cc 统一返回错误信息
});

处理函数末尾,catch(error)

exports.regUser = async (req, res, next) => {
try {
// 获取客户端提交的数据
const userInfo = req.body;

if (!userInfo.accounts) {
return res.cc('手机号不能为空', 1); // 使用 res.cc 返回错误
}

// 模拟生成验证码的逻辑
const code = '123456';
const codeExpiresAt = new Date(Date.now() + 5 * 60 * 1000); // 验证码 5 分钟有效期

// 模拟数据库操作
const sqlStr = 'SELECT * FROM users WHERE accounts = ?';
const rows = await db.query(sqlStr, [userInfo.accounts]);

if (rows.length > 0) {
const updateSql = 'UPDATE users SET code = ?, code_expires_at = ? WHERE accounts = ?';
await db.query(updateSql, [code, codeExpiresAt, userInfo.accounts]);
} else {
const insertSql = 'INSERT INTO users (accounts, code, code_expires_at) VALUES (?, ?, ?)';
await db.query(insertSql, [userInfo.accounts, code, codeExpiresAt]);
}

// 成功返回响应
res.cc('验证码已发送', 0); // 0 表示成功状态
} catch (error) {
next(error); // 把错误交给全局错误处理中间件处理
}
};

什么时候用 res.cc?

  • 用于轻量级的错误响应:
    如果某些错误逻辑不需要传递到全局错误处理中间件(例如简单的业务校验失败),可以直接使用 res.cc

    if (!userInfo.accounts) {
    return res.cc('手机号不能为空', 1); // 不传递到错误处理中间件
    }
  • 用于不可恢复的错误:
    如果错误是不可恢复的、需要全局处理的,应该使用 next(error),让全局错误处理中间件处理。

  • 不可恢复的错误(irrecoverable errors)是指那些在发生时无法通过常规的错误处理或重试机制来恢复的错误。这些错误通常意味着应用程序的某些关键部分已经崩溃或无法继续运行,因此程序需要停止执行并返回给用户适当的错误信息。常见的不可恢复错误可能涉及系统级别的问题、程序的逻辑错误或无法处理的外部资源故障。

总结

  1. res.cc 是响应辅助中间件,而非错误处理中间件。它用于简化成功或失败的响应逻辑,统一返回格式。
  2. 错误处理中间件需要显式接收 err 参数。如果在业务逻辑中调用 next(error),Express 会自动将错误传递给第一个错误处理中间件。
  3. 两者结合res.cc 和全局错误处理中间件搭配使用,可以让代码既简洁又结构清晰,推荐这样做。

7. 为项目配置 eslint

点击查看详情
  1. 安装 & 初始化

    pnpm npm install eslint

    npx eslint --init
  2. 编辑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,

    ]
  1. 详细说明 es6 和 commonjs 两个标准

    module.exports = routerexport default router 的区别主要在于两者所属的模块系统和使用方式。它们分别属于 CommonJSES6 模块 两种模块规范。


    1. CommonJS 和 ES6 模块简介

    • CommonJS 是 Node.js 默认的模块规范,使用 requiremodule.exports
    • ES6 模块 是 JavaScript 的现代模块系统,使用 importexport

    2. 区别分析

    语法层面的区别

    1. CommonJS (module.exports)

      • 使用 module.exports 导出。
      • 使用 require 导入。

      示例:

      // 导出文件(CommonJS)
      const router = {}; // 假设 router 是一个对象
      module.exports = router;

      // 导入文件
      const router = require('./path/to/router');
  1. ES6 模块 (export default)

    • 使用 export default 导出。
    • 使用 import 导入。

    示例:

    // 导出文件(ES6 模块)
    const router = {}; // 假设 router 是一个对象
    export default router;

    // 导入文件
    import router from './path/to/router';

运行时的区别

  1. CommonJS
    • 模块在运行时加载,是同步的。
    • module.exports 可以导出任何类型的值(对象、函数、数组、基本类型等)。
    • 导入时,导出的是一个值的引用。
  2. ES6 模块
    • 模块在编译时解析,是静态的。
    • export default 是 ES6 中默认导出方式,适用于一个模块导出一个主要功能。
    • 导入时,支持 import 解构语法。
    • ES6 模块是严格模式,语法更现代。

项目中的区别

  1. 兼容性

    • Node.js 环境:Node.js 默认支持 CommonJS 模块。如果要使用 ES6 模块,需要将 package.jsontype 设置为 "module",或者使用 .mjs 文件扩展名。
    • 前端环境:现代前端工具(如 Webpack、Vite)对 ES6 模块有更好的支持。
  2. 导出多项功能

    • CommonJS 使用exports,可以导出多个成员:

      exports.func1 = () => {};
      exports.func2 = () => {};
    • ES6 模块支持命名导出和默认导出:

      export const func1 = () => {};
      export const func2 = () => {};
      export default router;
  1. 使用习惯

    • CommonJS 更常用于后端(Node.js 项目)。
    • ES6 模块是现代 JavaScript 标准,前端项目和支持现代语法的后端项目更倾向于使用。

混用时的注意事项

  1. 如果一个模块使用 module.exports 导出,导入时不能直接用 import,需要借助 createRequire 或其他工具。

    import { createRequire } from 'module';
    const require = createRequire(import.meta.url);
    const router = require('./path/to/router');
  1. 如果一个模块使用 export default 导出,require 导入时会被包装在 default 属性中:

    // 导出文件(ES6 模块)
    export default router;

    // CommonJS 导入
    const router = require('./path/to/router').default;

总结

特性module.exports (CommonJS)export default (ES6 模块)
模块规范CommonJSES6 模块
使用的导入方式require()import
支持环境Node.js 默认支持需设置 "type": "module".mjs 文件
导入方式直接导入值默认导出直接导入,支持解构
运行时或编译时运行时加载(同步)编译时解析(静态)
灵活性可导出单个或多个对象默认导出一个主要内容,可同时命名导出多个

如果你在使用现代工具链或构建前端应用,优先使用 ES6 模块;而在传统 Node.js 项目中,CommonJS 更加直接和兼容性好

8. 表单验证 @escook/express-joi

点击查看详情
  1. 安装

    pnpm i @escook/express-joi

    // 依赖
    pnpm i joi
  1. 导入,app.js

    // 导入 express
    const express = require('express')
    // 创建 express 实例
    const app = express()

    // 导入 Joi 模块
    const Joi = require('joi')

    // 配置 cors 跨域中间件
    const cors = require('cors')
    app.use(cors())
  1. 新建 /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 }
  1. 然后 ,在路由模块中使用

    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数据

点击查看详情
  1. 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"

    }
    ]
    }
  1. 修改原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 {
// 首先,准备数据
const taskList = [
{
"task_id": 33,
"task_name": "拯救喵星人",
"position_name": "前端开发",
"task_budget": 1,
"task_cycle": 60,
"service_mode": "驻场",
"task_ask": "救救孩子吧",
"task_grade": "初级",
"isdelete": 0,
"is_check": 1,
"company_name": "北京驻场无忧科技有限公司",
"city": "北京",
"area": "朝阳区",
"user_name": null,
"it_head": null,
"is_emergency": 1
},
{
"task_id": 38,
"task_name": "金融行业app的一个小程序设计",
"position_name": "UI设计师",
"task_budget": 15000,
"task_cycle": 30,
"service_mode": "全职",
"task_ask": "金融行业的小程序设计,对ui界面要求很高,有完整的小程序开发者优先,能独立完成一整套小程序设计的流程,跟踪开发落地。",
"task_grade": null,
"isdelete": 0,
"is_check": 1,
"company_name": "北京驻场无忧科技有限公司",
"city": "北京",
"area": "朝阳区",
"user_name": null,
"it_head": null,
"is_emergency": 0
},
]

// 定义查询字符串和数据数组
const sql = `
INSERT INTO taskList (
task_name,
position_name,
task_budget,
task_cycle,
service_mode,
task_ask,
task_grade,
isdelete,
is_check,
company_name,
city,
area,
user_name,
it_head,
is_emergency,
created_at,
updated_at
) VALUES ?
`
// 转换数据为批量插入格式
const values = taskList.map(task => [
task.task_name,
task.position_name,
task.task_budget,
task.task_cycle,
task.service_mode,
task.task_ask,
task.task_grade,
task.isdelete,
task.is_check,
task.company_name,
task.city,
task.area,
task.user_name,
task.it_head,
task.is_emergency,
task.created_at,
task.updated_at
])

// 执行插入
db.query(sql, [values], (err, result) => {
if (err) {
console.error('批量插入数据失败:', err)
return
}
// 这里插入成功的提示貌似没有打印出来,但是已经验证,数据是批量注入了
// 后面类似的操作会特别留意原因
console.log('批量插入数据成功:', result)
db.end() // 关闭连接
})
} catch(error) {
res.cc(error,500)
}

11. jwt-token 验证

点击查看详情
  1. 根据后端的 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

  2. 不显示的指定行不行?行!

    在使用 express-jwt 时,getToken 方法 不是必须显式指定 的,但它有一个默认的行为。如果不显式提供 getTokenexpress-jwt 会按照以下默认逻辑来提取 Token:


    默认行为

    express-jwt 默认从以下位置提取 Token:

    1. Authorization Header

      • 默认从请求头 Authorization 提取。
      • 要求格式为:Bearer <token>
      • 例如:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

    为什么要自定义 getToken?

    1. Token 来源不同:
      • 如果你的 Token 不在 Authorization 头,而是放在其他地方(比如 x-access-token 或者 cookie),就需要通过 getToken 方法显式指定如何提取。
    2. 前缀不符合 Bearer 格式:
      • 如果你的 Token 没有 Bearer前缀,默认逻辑无法正确解析 Token,也需要自定义 getToken
    3. 支持更多场景:
      • 比如同时支持 Authorizationx-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,这不是默认行为。
    • 如果不指定 getTokenexpress-jwt 会尝试从 Authorization 头提取,导致找不到 Token 的问题。

    结论

    1. 不显式指定时express-jwt 默认从 Authorization: Bearer <token> 中提取 Token。
    2. 如果你的 Token 不在默认位置,或者格式不匹配,必须显式指定 getToken 方法

​ ps: 关于错误级别中间件的一些细节试错

// 定义错误级别中间件
// 这里必须要有 next(), 否则会导致中间件无法正常工作
// 例如,使用 apifox 工具时,未能正确执行 res.cc('身份认证失败!')
app.use((err, req, res, next) => {
// 数据验证失败
if (err instanceof Joi.ValidationError) return res.cc(err)
// 身份认证失败
if (err.name === 'UnauthorizedError') return res.cc('身份认证失败!')
// 未知错误
res.cc(err)
console.error(err)
next()
})

12.温故知新,前端瀑布流加载数据的后端适配

后端 api 设计 - 点击查看详情
  1. 直接上代码

    // 导入数据库操作模块
    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) // 捕获并返回错误
    }
    }
  1. 代码中的分页逻辑
// 分页相关参数
const offset = (pageNum - 1) * pageSize; // 计算偏移量
query += ' LIMIT ? OFFSET ?';
params.push(parseInt(pageSize), parseInt(offset));
  1. 参数定义:

    • pageNum: 当前页码(从前端请求中传入,默认值为 1)。
    • pageSize: 每页显示的记录条数(从前端请求中传入,默认值为 10)。
    • offset: 偏移量,即查询从哪一条记录开始。
  2. 偏移量的计算:

    • 公式:

      计算偏移量

      • 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 条记录开始)。
  3. 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';
const [totalResult] = await db.query(totalQuery);
const total = totalResult[0].total;
  1. SQL 查询解释:

    • SELECT COUNT(*) AS total: 统计 tasklist 表中符合条件的总记录数。
    • WHERE 条件: 只统计未被删除且已审核的记录。
    • 不受 LIMITOFFSET 限制,返回的是完整数据集的记录总数。
  2. 结果意义:

    • total: 总记录数,供前端分页使用。

    • 前端可以用以下公式计算总页数:

      计算总数


分页的工作流程

  1. 前端传参:
    • 用户请求某一页数据,前端发送参数:pageNumpageSize
  2. 后端接收:
    • 通过 pageNumpageSize 计算 offset
    • 使用 LIMITOFFSET 限制返回的记录条数。
  3. 返回数据:
    • 返回分页数据(rows)和总记录数(total)。
    • 前端可以根据 total 决定是否还有更多数据。

注意事项

  1. 性能优化:

    • 当数据量很大时,直接使用 OFFSET 可能导致性能问题,因为数据库需要跳过大量数据。
    • 优化方法:通过索引字段(如 idcreated_at)使用游标进行分页查询。

    示例:

    SELECT * 
    FROM tasklist
    WHERE isdelete = 0 AND is_check = 1 AND id > ?
    ORDER BY id ASC
    LIMIT 10;
  2. 边界情况处理:

    • pageNum 小于 1:将其修正为 1
    • pageSize 超过合理范围:设置最大值(如 100)。
    • 总数为 0 时:返回空数据,并标记分页已完成。

总结

分页逻辑的核心是通过 LIMITOFFSET 控制查询范围,并通过计算偏移量确定从哪一条记录开始查询。通过统计总记录数,确保前端能够正确显示分页状态或终止加载更多的逻辑。

13. 按照范式标准,创建用户收藏表

点击查看详情
  1. 首先,创建数据库表

    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_idtask_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);

  2. 封装添加收藏关系的接口

    // 任务收藏接口
    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进行接收,否则接收不到。

顺便说一下
  1. 不同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 config = require('../config')

const handleToken = (req, res, next) => {
// 从请求头的 'x-access-token' 中获取 token
let token = req.headers['x-access-token'] || req.headers['authorization'] // 支持两种方式获取 Token

// 如果存在 Token 且包含 'Bearer ' 前缀
if (token && token.startsWith('Bearer ')) {
token = token.slice(7, token.length) // 移除 'Bearer ' 前缀
}

if (!token) {
return res.status(401).send({ code: 401, msg: '缺少 token,认证失败' })
}

try {
// 替换为你的 JWT 密钥
const decoded = jwt.verify(token, config.jwtSecretKey)
req.accounts = decoded.accounts // 将解析出的用户信息存入 req 对象
next() // 继续处理请求
} catch (err) {
console.error('Token 验证失败:', err)
return res.status(401).send({ code: 401, msg: '无效的 token' })
}
}

module.exports = { handleToken }

然后,在对应路由中添加该中间件

const { Router } = require('express')
const router = Router()

const { handleToken } = require('../utils/handleToken')

// 导入处理函数
const { taskAllList,taskDetail,hotSearch } = require('../router_handler/task')

// 挂载路由
router.get('/task/taskAllList',taskAllList)

// 在请求任务详情之前验证 token
router.get('/task/getTaskDetails',handleToken,taskDetail)

router.get('/position/public/getHotSeach',hotSearch)

// 导出路由
module.exports = router

然后,在处理逻辑中,就可以直接进行接收了 /

const accounts = req.accounts

其实吧,在 app.js 中,有如下一个中间件了解一下

// 一定要在路由之前,应用此中间件
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(\/.*)?$/] // 豁免登录相关接口
})
)

express-jwt 中间件在验证 Token 成功后,自动为当前请求对象 req 添加的一个属性,即req.auth。这个属性存储的是解析后的 JWT Payload 数据(也就是在生成 Token 时嵌入的用户信息),因此,可以对其进行解构以获取目标信息:

const { accounts } = req.auth

/router_handle/task.js

// 任务详情接口
const taskDetail = async (req, res) => {
try {
// 从中间件中接受解析出的 accounts
// const accounts = req.accounts
// 从请求参数中解构出来要查询的任务id
const { task_id } = req.query
const { accounts } = req.auth

if (!accounts) {
return res.status(401).send({ code: 401, msg: '用户未登录或无效 token' })
}

// 查询任务详情的SQL语句
const sql = 'SELECT * FROM tasklist WHERE task_id = ?'

// 执行查询任务详情的SQL语句
const [result] = await db.query(sql, task_id)

const taskDetail = result[0]

// 查询收藏关系的 sql 语句
const sqlFavorite = 'SELECT * FROM task_favorites WHERE accounts = ? AND task_id = ?'
// 执行查询收藏关系
const [favoriteResult] = await db.query(sqlFavorite, [accounts, task_id])
// 根据查询结果,设置 status 值
const status = favoriteResult.length > 0 ? 1 : 0 // 1 表示已收藏,0 表示未收藏
// 响应查询结果
res.send({
code: 200,
result: {
records: [{
...taskDetail,
status
}]
},
msg: '查询任务详情成功!'
})
} catch (e) {
res.cc(e) // 捕获并返回错误
}
}

100. 这里必须留一个小尾巴,否则,编辑软件会报错,后面视情况会寻找原因,大概率是编辑器原因!

ttttt 不会错版了kugei