雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

往期回顾

前端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(一)

后端框架搭建——从零开始搭建一个高颜值后台管理系统全栈框架(二)

实现登录功能jwt or token+redis?——从零开始搭建一个高颜值后台管理系统全栈框架(三)

封装axios,让请求变得丝滑——从零开始搭建一个高颜值后台管理系统全栈框架(四)

实现前后端全自动化部署,解放你的双手。——从零开始搭建一个高颜值后台管理系统全栈框架(五)

前言

这一期文章的内容有点杂,实现了通过雪花算法生成id、通用的附件方案,邮箱验证、修改密码功能。有很多兄弟对这个项目会开发哪些功能感到好奇,在后面我列了一下后续项目的功能清单。

雪花算法

数据库主键id生成方案

数据库主键id有几种常用的方案:

自增方案

数据库可以设置主键id自增,但是后续数据量大的情况下,如果使用水平分表方式优化,可能会生成重复ID。并且id是连续的,别人可以轻松猜到下一条数据id。

uuid

UUID 生成的是一个无序且唯一的字符串,这种数据正常来说很适合做数据库id,但是它也有自己的缺点。

  1. 存储空间占用:UUID通常以128位的形式表示,相比于较短的整型或字符串标识符,需要更多的存储空间。这可能在大规模数据集合和索引中导致存储成本和性能方面的负担。

  2. 可读性差:UUID是由数字和字母组成的字符串,对人类来说不易读写和记忆。当需要手动查询或分析数据库时,可读性差可能造成不便。

  3. 索引效率下降:UUID具有随机性,生成的值没有明显的顺序性或局部性。这导致在使用UUID作为主键或索引时,插入新记录的效率会下降,因为新记录通常被插入到已有索引的各个位置,而不是一个连续的位置。

  4. 查询性能影响:在某些情况下,由于UUID的无序性,查询效率可能受到影响。特别是基于范围的查询、排序和连接操作可能不如使用递增整数类型的标识符高效。

  5. 数据库碎片化:由于UUID的随机性,插入新记录时可能导致数据库表的碎片化。碎片化会增加数据库的存储空间占用和查询性能下降。

利用redis原子性生成自增id

这个和上面自增id方案差不多,虽然解决了水平分表可能带来的问题,但是它也有自己的缺陷。

  1. 生成的id也是连续的,和自增id一样,下一条数据的id很容易被别人猜到。
  2. 当并发请求生成自增ID较高时,单个Redis实例可能成为性能瓶颈。

雪花算法

目前在分布式系统中常用的生成数据库主键算法,它没有上面方案的缺点,性能也比较高。

雪花算法介绍

雪花算法(Snowflake Algorithm)是一种用于生成全局唯一标识符(Unique Identifier)的算法,最初由Twitter开发并开源。它主要用于分布式系统中,以解决在分布式环境下生成唯一ID的需求。

雪花算法原理就是生成一个的64位比特位的 long 类型的唯一 id。

  • 最高1位固定值0,没有意义。
  • 接下来41位存储毫秒级时间戳,2^41/(1000606024365)=69,大概可以使用69年。
  • 再接下10位存储机器码,包括5位 datacenterId 和5位 workerId。最多可以部署2^10=1024台机器。
  • 最后12位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成2^12=4096个不重复 id。

node版本算法实现

网上有很多java版本的实现,我找了一篇,仿造用node实现了一下,具体实现看代码中的注释。

export class SnowFlake {
// 系统上线的时间戳,我这里设置为 2023-06-22 00:00:00 的时间戳
epoch = BigInt(1687392000000);
// 数据中心的位数
dataCenterIdBits = 5;
// 机器id的位数
workerIdBits = 5;
// 自增序列号的位数
sequenceBits = 12;
// 最大的数据中心id 这段位运算可以理解为2^5-1 = 31
maxDataCenterId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1);
// 最大的机器id 这段位运算可以理解为2^5-1 = 31
maxWorkerId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1);
// 时间戳偏移位数
timestampShift = BigInt(
this.dataCenterIdBits + this.workerIdBits + this.sequenceBits
);
// 数据中心偏移位数
dataCenterIdShift = BigInt(this.workerIdBits + this.sequenceBits);
// 机器id偏移位数
workerIdShift = BigInt(this.sequenceBits);
// 自增序列号的掩码
sequenceMask = (BigInt(1) << BigInt(this.sequenceBits)) - BigInt(1);
// 记录上次生成id的时间戳
lastTimestamp = BigInt(-1);
// 数据中心id
dataCenterId = BigInt(0);
// 机器id
workerId = BigInt(0);
// 自增序列号
sequence = BigInt(0);
constructor(dataCenterId: number, workerId: number) {
// 校验数据中心 ID 和工作节点 ID 的范围
if (dataCenterId > this.maxDataCenterId || dataCenterId < 0) {
throw new Error(
`Data center ID must be between 0 and ${this.maxDataCenterId}`
);
}
if (workerId > this.maxWorkerId || workerId < 0) {
throw new Error(`Worker ID must be between 0 and ${this.maxWorkerId}`);
}
this.dataCenterId = BigInt(dataCenterId);
this.workerId = BigInt(workerId);
}
nextId() {
let timestamp = BigInt(Date.now());
// 如果上一次生成id的时间戳比下一次生成的还大,说明服务器时间有问题,出现了回退,这时候再生成id,可能会生成重复的id,所以直接抛出异常。
if (timestamp < this.lastTimestamp) {
// 时钟回拨,抛出异常并拒绝生成 ID
throw new Error('Clock moved backwards. Refusing to generate ID.');
}
// 如果当前时间戳和上一次的时间戳相等,序列号加一
if (timestamp === this.lastTimestamp) {
// 同一毫秒内生成多个 ID,递增序列号,防止冲突
this.sequence = (this.sequence + BigInt(1)) & this.sequenceMask;
if (this.sequence === BigInt(0)) {
// 序列号溢出,等待下一毫秒
timestamp = this.waitNextMillis(this.lastTimestamp);
}
} else {
// 不同毫秒,重置序列号
this.sequence = BigInt(0);
}
this.lastTimestamp = timestamp;
// 组合各部分生成最终的 ID,可以理解为把64位二进制转换位十进制数字
const id =
((timestamp - this.epoch) << this.timestampShift) |
(this.dataCenterId << this.dataCenterIdShift) |
(this.workerId << this.workerIdShift) |
this.sequence;
return id.toString();
}
waitNextMillis(lastTimestamp) {
let timestamp = BigInt(Date.now());
while (timestamp <= lastTimestamp) {
// 主动等待,直到当前时间超过上次记录的时间戳
timestamp = BigInt(Date.now());
}
return timestamp;
}
}

代码里使用了BitInt,这个node10.4.0才支持。

测试算法

雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
可以看到生成的id都是递增的,使用这个类有个需要注意的地方,不要在使用的地方每次都去new,应该给他做成单例,所有地方都用同一个实例,不然可能会生成出重复id。

pm2中使用雪花算法

这个其实才是我这篇文章中关于雪花算法的重点,因为上面那些东西网上都能搜索到,而在pm2中使用雪花算法生成id,我没看到类似的文章。
pm2中使用雪花算法的问题是啥,因为pm2启动服务是多进程的,也就是有多个SnowFlake实例,如果并发高的情况下,很可能会生成重复的id。怎么解决这个问题呢,上文中机器id派上用场了,我们只要保证每个实例的机器id不一样就行了。从网上了找了一些资料,没有找到答案。突然想到我以前用pm2 list查看每个进程前面有个id。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
只要在服务里获取这个id就行了,最后发现pm2启动服务的时候,会把当前实例id注入到当前环境变量pm_id中,我们只要在new SnowFlake的时候把当前pm_id当成机器id传进去就行了。

项目中引用SnowFlake

新建src/utils/snow.flake.ts文件,把上面代码复制进去,为了让每一个地方使用同一个SnowFlake实例,我们在这个文件中提前new好,然后再导出new好的实例。(js中实现单例模式真是超级简单)

import { env } from 'process';
export class SnowFlake {
// 系统上线的时间戳,我这里设置为 2023-06-22 00:00:00 的时间戳
epoch = BigInt(1687392000000);
// 数据中心的位数
dataCenterIdBits = 5;
// 机器id的位数
workerIdBits = 5;
// 自增序列号的位数
sequenceBits = 12;
// 最大的数据中心id 这段位运算可以理解为2^5-1 = 31
maxDataCenterId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1);
// 最大的机器id 这段位运算可以理解为2^5-1 = 31
maxWorkerId = (BigInt(1) << BigInt(this.workerIdBits)) - BigInt(1);
// 时间戳偏移位数
timestampShift = BigInt(
this.dataCenterIdBits + this.workerIdBits + this.sequenceBits
);
// 数据中心偏移位数
dataCenterIdShift = BigInt(this.workerIdBits + this.sequenceBits);
// 机器id偏移位数
workerIdShift = BigInt(this.sequenceBits);
// 自增序列号的掩码
sequenceMask = (BigInt(1) << BigInt(this.sequenceBits)) - BigInt(1);
// 记录上次生成id的时间戳
lastTimestamp = BigInt(-1);
// 数据中心id
dataCenterId = BigInt(0);
// 机器id
workerId = BigInt(0);
// 自增序列号
sequence = BigInt(0);
constructor(dataCenterId: number, workerId: number) {
// 校验数据中心 ID 和工作节点 ID 的范围
if (dataCenterId > this.maxDataCenterId || dataCenterId < 0) {
throw new Error(
`Data center ID must be between 0 and ${this.maxDataCenterId}`
);
}
if (workerId > this.maxWorkerId || workerId < 0) {
throw new Error(`Worker ID must be between 0 and ${this.maxWorkerId}`);
}
this.dataCenterId = BigInt(dataCenterId);
this.workerId = BigInt(workerId);
}
nextId() {
let timestamp = BigInt(Date.now());
// 如果上一次生成id的时间戳比下一次生成的还大,说明服务器时间有问题,出现了回退,这时候再生成id,可能会生成重复的id,所以直接抛出异常。
if (timestamp < this.lastTimestamp) {
// 时钟回拨,抛出异常并拒绝生成 ID
throw new Error('Clock moved backwards. Refusing to generate ID.');
}
// 如果当前时间戳和上一次的时间戳相等,序列号加一
if (timestamp === this.lastTimestamp) {
// 同一毫秒内生成多个 ID,递增序列号,防止冲突
this.sequence = (this.sequence + BigInt(1)) & this.sequenceMask;
if (this.sequence === BigInt(0)) {
// 序列号溢出,等待下一毫秒
timestamp = this.waitNextMillis(this.lastTimestamp);
}
} else {
// 不同毫秒,重置序列号
this.sequence = BigInt(0);
}
this.lastTimestamp = timestamp;
// 组合各部分生成最终的 ID,可以理解为把64位二进制转换位十进制数字
const id =
((timestamp - this.epoch) << this.timestampShift) |
(this.dataCenterId << this.dataCenterIdShift) |
(this.workerId << this.workerIdShift) |
this.sequence;
return id.toString();
}
waitNextMillis(lastTimestamp) {
let timestamp = BigInt(Date.now());
while (timestamp <= lastTimestamp) {
// 主动等待,直到当前时间超过上次记录的时间戳
timestamp = BigInt(Date.now());
}
return timestamp;
}
}
// 如果有pm_id,把pm_id当机器id传进去
export const snowFlake = new SnowFlake(0, +env.pm_id || 0);

改造base.entity

把base.entity里面id自增给删除,同时把数据库类型设置为bigint,然后字段类型设置为string,typeorm会自动把数据库中的bigint转换为string。

这里相信大多数前端都遇到过一个问题,如果你的后端使用雪花算法生成id,然后以long类型返回给前端,前端会因为精度问题,会把最后几位变成0。这种情况有两个解决方案,第一个是前端解决,在请求拦截器中判断然后转成字符串。第二种方案是后端转。第一种方案性能很差,相信大部分兄弟都是使用第二种方案。

使用typeorm插入数据拦截器注入id

上面我们把id自增给删除了,所以需要我们自己手动传id,我们不可能每次new实体的时候,掉snowFlake.nextId()方法生成一个id然后赋值给当前实体id,这样做其实也可以,但是有点麻烦。

好在typeorm支持插入实体拦截器,所有的插入都会执行这个拦截器。我们只需要在这个拦截器中,把id注入进去就行了。

创建src/typeorm-event-subscriber.ts文件

import { EventSubscriberModel } from '@midwayjs/typeorm';
import { EntitySubscriberInterface, InsertEvent } from 'typeorm';
import { snowFlake } from './utils/snow.flake';
@EventSubscriberModel()
export class EverythingSubscriber implements EntitySubscriberInterface {
beforeInsert(event: InsertEvent<any>) {
if (!event.entity.id) {
event.entity.id = snowFlake.nextId();
}
}
}

把这个拦截器配置到typeorm中
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
这样每次插入数据的时候,都会自动注入雪花算法生成的id。

附件方案

介绍

后台管理系统肯定少不了附件上传功能,下面给大家分享一个使用起来比较简单的方案。

文件服务器

要做附件,肯定要先有一个文件服务,常用的有腾讯的cos,阿里的oss,七牛云也可以。不过这些都是收费的,大家如果自己有服务器,可以自己使用minio搭建一个。

使用镜像启动minio

先拉minio镜像

docker pull minio/minio

启动minio镜像,9000对应的是控制台前端服务,9001是接口调用的上传下载文件服务。账号是minio,密码是minio@123。

docker run  -p 9000:9000 -p 9001:9001 --name minio \
-d --restart=always \
-e MINIO_ACCESS_KEY=minio \
-e MINIO_SECRET_KEY=minio@123 \
-v /usr/local/minio/data:/data \
-v /usr/local/minio/config:/root/.minio \
minio/minio server /data  --console-address ":9000" --address ":9001"

启动完成后,访问http://localhost:9000,出现下面的界面
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
输入上面设置的帐号密码登录进去,创建一个桶。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
改变桶的权限,不然可以上传文件,但是别人无法访问,所以给桶设置public权限。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
我们上传一个文件测试一下
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
上传成功后,可以通过桶名称和文件名访问,注意这里的端口号是上面设置的文件服务端口号。
http://localhost:7101/fluxy-admin/242091682079921_.pic_hd.jpg

后端服务对接minio

实现思路

整个流程很简单,前端上传文件,后端接受到文件,然后调用minio的接口上传到minio服务器,把文件访问地址返回给前端。

文件服务可能会在后面很多地方使用,比如用户头像,这种一对一的方案还好(一个用户只有一个头像),上传成功后,把图片地址存到头像字段中。一对多的情况下就不能这样用了,比如一个单据有多个附件,这种就需要加关联关系表了,下面给大家分享一个不要加关联关系吧。

我们建一个文件表,把单据id存到文件表里,这样就不用加关联关系表了,虽然用雪花算法可以保证不同业务单据id是唯一的,但是如果后面按业务模块拆分微服务单独部署了,机器id可能是一样的,就有可能生成重复的单据id,所以我们再加一个业务字段来保证当前业务下id是不会重复的。

因为是先上传文件再保存单据,然后把单据id存到文件表里。假设文件上传了,但是用户又取消了创建单据,这样文件表和文件服务器就会有很多脏数据,这时候我们可以写一个定时任务定期去清理没有单据id的文件。

引入upload组件

后端服务引入upload组件,这里miday官方文档写的很清楚,这里我就不说了。

封装minio服务

安装minio依赖

pnpm i minio

新建src/autoload/minio.ts文件

import { Config, IMidwayContainer, Singleton } from '@midwayjs/core';
import { ApplicationContext, Autoload, Init } from '@midwayjs/decorator';
import * as Minio from 'minio';
import { MinioConfig } from '../interface';
export type MinioClient = Minio.Client;
@Autoload()
@Singleton()
export class MinioAutoLoad {
@ApplicationContext()
applicationContext: IMidwayContainer;
@Config('minio')
minioConfig: MinioConfig;
@Init()
async init() {
const minioClient = new Minio.Client(this.minioConfig);
this.applicationContext.registerObject('minioClient', minioClient);
}
}

这里用到了@Singleton单例装饰器和@Autoload自动执行装饰器,只要在方法上使用@Init()装饰器,下面的init方法就会在项目启动后自动执行。init方法中,先根据配置new了一个Minio.Client实例,然后注入到上下文中,这样就可以在代码中直接使用这个服务了。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
代码中使用上面注入的minioClient实例
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
src/config/config.default.ts添加minio配置
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

新建file服务

使用下面命令创建file服务

node ./script/create-module file

file实体文件。pkName就是业务单据类型,pkValue就是单据id。

// src/module/file/entity/file.ts
import { Entity, Column } from 'typeorm';
import { BaseEntity } from '../../../common/base.entity';
@Entity('sys_file')
export class FileEntity extends BaseEntity {
@Column({ comment: '文件名' })
fileName?: string;
@Column({ comment: '文件路径' })
filePath?: string;
@Column({ comment: '外健名称', nullable: true })
pkName: string;
@Column({ comment: '外健值', nullable: true })
pkValue?: string;
}

file service文件。封装了三个方法,一个功能的上传方法、设置pkName和pkValue方法、清理脏数据。

import { Config, Inject, Provide } from '@midwayjs/decorator';
import { InjectDataSource, InjectEntityModel } from '@midwayjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { BaseService } from '../../../common/base.service';
import { FileEntity } from '../entity/file';
import { UploadFileInfo } from '@midwayjs/upload';
import { MinioClient } from '../../../autoload/minio';
import { MinioConfig } from '../../../interface';
@Provide()
export class FileService extends BaseService<FileEntity> {
@InjectEntityModel(FileEntity)
fileModel: Repository<FileEntity>;
@Inject()
minioClient: MinioClient;
@Config('minio')
minioConfig: MinioConfig;
@InjectDataSource()
defaultDataSource: DataSource;
getModel(): Repository<FileEntity> {
return this.fileModel;
}
// 上传方法
async upload(file: UploadFileInfo<string>) {
// 生成文件名。因为文件名可能重复,这里手动拼了时间戳。
const fileName = `${new Date().getTime()}_${file.filename}`;
// 这里使用了typeorm的事务,如果文件信息存表失败的情况下,就不用上传到minio服务器了,如果后面上传文件失败了,前面插入的数据,也会自动会滚。保证了不会有脏数据。
const data = await this.defaultDataSource.transaction(async manager => {
const fileEntity = new FileEntity();
fileEntity.fileName = fileName;
fileEntity.filePath = `/file/${this.minioConfig.bucketName}/${fileName}`;
await manager.save(FileEntity, fileEntity);
await this.minioClient.fPutObject(
this.minioConfig.bucketName,
fileName,
file.data
);
return fileEntity;
});
return data;
}
// 上传单据时,把单据id注入进去
async setPKValue(id: string, pkValue: string, pkName: string) {
const entity = await this.getById(id);
if (!entity) return;
entity.pkValue = pkValue;
entity.pkName = pkName;
await this.fileModel.save(entity);
return entity;
}
// 清理脏数据,清理前一天的数据
async clearEmptyPKValueFiles() {
const curDate = new Date();
curDate.setDate(curDate.getDate() - 1);
const records = await this.fileModel
.createQueryBuilder()
.where('createDate < :date', { date: curDate })
.andWhere('pkValue is null')
.getMany();
this.defaultDataSource.transaction(async manager => {
await manager.remove(FileEntity, records);
await Promise.all(
records.map(record =>
this.minioClient.removeObject(
this.minioConfig.bucketName,
record.fileName
)
)
);
});
}
}

file controller。使用upload组件上传文件后,upload会把文件暂时存在服务器临时目录下,files里面存的有临时文件地址。

import { Controller, Inject, Post, Provide, Files } from '@midwayjs/core';
import { FileService } from '../service/file';
import { NotLogin } from '../../../decorator/not.login';
import { ApiBody } from '@midwayjs/swagger';
@Provide()
@Controller('/file')
export class FileController {
@Inject()
fileService: FileService;
@Inject()
minioClient;
@Post('/upload')
@ApiBody({ description: 'file' })
@NotLogin()
async upload(@Files() files) {
if (files.length) {
return await this.fileService.upload(files[0]);
}
return {};
}
}

实战,实现上传头像功能

前端头像上传

封装前端头像上传组件,这里使用了antd-img-crop头像剪裁组件。因为这个组件要在FormItem组件下使用,所以参数里有value,onChange参数。

// src/pages/user/avatar.tsx
import React from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Upload } from 'antd';
import type { UploadChangeParam } from 'antd/es/upload';
import type { RcFile, UploadFile, UploadProps } from 'antd/es/upload/interface';
import ImgCrop from 'antd-img-crop';
import { antdUtils } from '@/utils/antd';
const beforeUpload = (file: RcFile) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
antdUtils.message?.error('文件类型错误');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
antdUtils.message?.error('文件大小不能超过2M');
}
if (!(isJpgOrPng && isLt2M)) {
return Upload.LIST_IGNORE;
}
return true;
};
interface PropsType {
value?: UploadFile[];
onChange?: (value: UploadFile[]) => void;
}
const Avatar: React.FC<PropsType> = ({
value,
onChange,
}) => {
const handleChange: UploadProps['onChange'] = (info: UploadChangeParam<UploadFile>) => {
if (onChange) {
onChange(info.fileList);
}
};
const onPreview = async (file: UploadFile) => {
const src = file.url || file?.response?.filePath;
if (src) {
const imgWindow = window.open(src);
if (imgWindow) {
const image = new Image();
image.src = src;
imgWindow.document.write(image.outerHTML);
} else {
window.location.href = src;
}
}
};
return (
<ImgCrop showGrid rotationSlider showReset>
<Upload
name="avatar"
listType="picture-card"
className="avatar-uploader"
action="/api/file/upload"
onChange={handleChange}
fileList={value}
beforeUpload={beforeUpload}
onPreview={onPreview}
>
{(value?.length || 0) < 1 && <PlusOutlined />}
</Upload>
</ImgCrop>
);
};
export default Avatar;

在表单中使用这个组件
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
表单提交后,把附件id传到后台
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
编辑时给默认值
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
这里的代码平平无奇,没啥可说的。

后端头像上传功能实现

改造创建用户的方法,如果添加用户时上传了头像,根据当前文件id更新pkName和pkValue字段数据。pkValue时当前用户id,pkName是类型,建议用表名_字段名。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
编辑用户时稍微复杂一点
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
改造分页查询方法,把当前用户表和file表关联查询,把查询出来的file信息映射到user的avatarEntity字段上。这里可以使用typeorm的关联关系,查询用户的时候会自动把头像信息查出来,这种对于新手很友好,上手简单,基本不用学习sql。但是这种方式会自动给创建外键,现在很多公司都不推荐使用外键了,所以我这里都使用join自己去查。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

使用定时任务清除脏数据

midway内置了任务队列组件。

安装组件

pnpm i @midwayjs/bull@3

src/configuration.ts文件中导入组件
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
因为任务队列依赖redis,所以需要在配置中配置redis信息
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
创建src/queue/clear.file.ts文件

// src/queue/clear.file.ts
import { Processor, IProcessor } from '@midwayjs/bull';
import { Inject } from '@midwayjs/core';
import { FileService } from '../module/file/service/file';
// 每天凌晨00:00:00定时执行下面清理文件的方法
@Processor('clear_file', {
repeat: {
cron: '0 0 0 * * *',
},
})
export class ClearFileProcessor implements IProcessor {
@Inject()
fileService: FileService;
async execute() {
// 调用文件服务里清理文件方法
this.fileService.clearEmptyPKValueFiles();
}
}

邮箱验证

前言

一般后台管理系统不会开放注册功能,很多都是管理员给员工开通帐号,开通完帐号后,随机生成一个密码,然后通过邮箱发送给当前用户,员工收到邮件就可以登录系统了。这里面有个问题,万一管理员手滑了,邮箱写错了发送给了别人,别人知道了帐号密码就能登录系统了。所以我们在添加用户的时候,先给员工发一个验证码,然后员工把验证码发给管理员,管理员填写验证码才能添加用户,这样就防止手滑写错邮箱的问题了。

开通个人邮箱服务

想发送邮件,需要先开启邮箱服务。下面我以qq邮箱为例开通邮件服务。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
这里开启后会给密钥记得要存起来,后面会用到。

封装发邮件的公共服务

创建src/common/mail.service.ts文件,使用nodemailer这个库去发送邮件。

// src/common/mail.service.ts
import { Config, Provide, Singleton } from '@midwayjs/core';
import * as nodemailer from 'nodemailer';
import { MailConfig } from '../interface';
interface MailInfo {
// 目标邮箱
to: string;
// 标题
subject: string;
// 文本
text?: string;
// 富文本,如果文本和富文本同时设置,富文本生效。
html?: string;
}
@Provide()
@Singleton()
export class MailService {
@Config('mail')
mailConfig: MailConfig;
async sendMail(mailInfo: MailInfo) {
const transporter = nodemailer.createTransport(this.mailConfig);
// 定义transport对象并发送邮件
const info = await transporter.sendMail({
from: this.mailConfig.auth.user, // 发送方邮箱的账号
...mailInfo,
});
return info;
}
}

在配置文件中添加邮箱服务器配置
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

  • host:邮箱服务器地址,qq邮箱是smtp.qq.com
  • port:邮箱服务器端口号,qq邮箱是465
  • secure:表示使用安全连接
  • auth.user:服务器的邮箱账号
  • auth.pass:上面生成的密钥

实战,实现添加用户时邮箱验证

前端实现

封装一个带有邮箱输入框和计时器的表单组件,代码很简单,我就不详细说了。

import { useRequest } from '@/hooks/use-request';
import { Button, Form, Input } from 'antd';
import React, { ChangeEventHandler, useEffect, useRef, useState } from "react";
import userService from './service';
interface PropsType {
value?: string;
onChange?: ChangeEventHandler;
disabled?: boolean;
}
const EmailInput: React.FC<PropsType> = ({
value,
onChange,
disabled,
}) => {
const [timer, setTimer] = useState<number>(0);
const form = Form.useFormInstance();
const intervalTimerRef = useRef<number>();
const { runAsync } = useRequest(userService.sendEmailCaptcha, { manual: true });
const sendEmailCaptcha = async () => {
const values = await form.validateFields(['email']);
setTimer(180);
await runAsync(values.email);
intervalTimerRef.current = window.setInterval(() => {
setTimer(prev => {
if (prev - 1 === 0) {
window.clearInterval(intervalTimerRef.current);
}
return prev - 1;
});
}, 1000);
}
useEffect(() => {
return () => {
if (intervalTimerRef.current) {
window.clearInterval(intervalTimerRef.current);
}
}
}, []);
return (
<div className='flex items-center gap-[12px]'>
<Input disabled={disabled} onChange={onChange} value={value} className='flex-1' />
{!disabled && (
<Button
disabled={timer > 0}
onClick={sendEmailCaptcha}>
{timer > 0 ? `重新发送(${timer}秒)` : '发送邮箱验证码'}
</Button>
)}
</div>
)
}
export default EmailInput;

在表单中添加刚加的组件和添加一个邮箱验证码输入框
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
效果展示
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

后端实现

在user controler中添加一个发送邮箱验证码的接口
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
效果
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
改造添加用户方法,先校验邮箱验证码对不对,然后随机生成一个密码加盐保存到数据库中,最后把帐号和密码发送给对应的用户。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

修改密码

前言

可以看到上面生成密码很难记,不可能用户每次登录都看一下邮箱,所以增加了修改密码的功能。
正好我前两天在掘金重置密码,看到掘金重置密码发送邮件的弹框有点意思,这里就拿来用用。(如果不能用,可以联系我删除。)

掘金的效果图,注意看上面的图片

邮箱输入框未获到焦点
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
邮箱输入框获取到焦点时
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
这种交互挺有意思的,为掘金的设计人员点个赞。

前端代码实现

雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
代码实现很简单,搞两张图片,加一个标记输入框是否获取到焦点的变量,获取到焦点显示一张图片,失去焦点显示另外一张图片。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
在做一个修改密码的页面,用户在邮件中点击链接重定向到这个页面修改密码,url上会带两个参数一个是邮箱,另外一个是邮箱验证码,这两个参数是后端发送邮件时注入到url上的。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

后端实现

在auth controller中添加一个发送重置邮箱验证码的接口,因为这个接口不需要登录也能调用,所以加了NotLogin装饰器。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
邮件内容展示
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
添加修改密码接口。实现比较简单,先校验邮箱和邮箱验证码,然后再把前端传过来的密码加盐更新到当前用户。这里有个小细节,用户修改完密码,需要把当前用户以前颁发过的token和refreshToken全部删除掉,然后让用户重新登录。这样子的话,需要在登录的时候把当前颁发的token和refreshToken存起来。redis的smembers方法获取某个key的数组值,redis的sadd方法往某个key里面添加一项。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
登录的时候,把当前token和refreshToken存起来
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

docker-compose中增加minio服务和邮箱服务配置

上篇写部署的时候,我其实已经把minio文件服务部署上去了。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
后端服务增加邮箱服务器配置
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

把路由方式从hash模式改成history模式

很简单把createHashRouter方法改成createBrowserRouter就行了
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)
还需要改一下nginx配置,不然进入一个功能后,然后手动刷新一下页面就会404了。因为如果当前路由为https://fluxyadmin.cn/dashboard这个,刷新一下,相当于向nginx请求这个dashboard这个资源,这个资源当然不存在,所以我们需要改一下nginx配置,在请求不到资源的时候尝试加载根目录下的index.html文件。
雪花算法,附件方案,邮箱验证,修改密码。——从零开始搭建一个高颜值后台管理系统全栈框架(六)

后续项目功能清单

有不少兄弟对这个项目会实现哪些功能很好奇,这里和大家说一下。

  • 前端动态菜单、动态路由、按钮权限。
  • 后端接口权限
  • 项目实战、封装常用组件、可能会集成工作流。实战项目暂定人事管理系统。
  • 前端低代码平台
  • 使用前端低代码平台重构前面项目的前端部分
  • 后端低代码平台
  • 使用后端低代码平台重构前面做的实战项目后端接口部分
  • 低代码平台对接chatgpt,实现用户说个需求,就能实现一个功能或系统
  • 搭建框架文档平台
  • 编写框架脚手架

这些功能大部分我在公司里都已经实现过,所以大家不用担心做不出来。

总结

这篇文章我写的很糟心,这种实现业务功能的文章,其实没啥可写的,写了也没啥亮点。但是我这个专栏,主打的是从零开始,不可能绕过一些功能点不写的,因为有些刚入门的同学在跟着项目做,如果我漏了一些功能没写,然后后面突然冒出来一个功能,他们可能会感到疑惑。为了照顾这些人,我会把我这个框架做的功能都给写出来,无论大小,简单的功能我就写思路,复杂一点的会把核心代码贴出来。

写作不易,如果文章对你有帮助,就帮忙给个赞吧,你们赞和收藏就是我写文章的动力。

项目体验地址:fluxyadmin.cn/user/login

前端仓库地址:github.com/dbfu/fluxy-…

后端仓库地址:github.com/dbfu/fluxy-…

原文链接:https://juejin.cn/post/7248219648054378554 作者:前端小付

(0)
上一篇 2023年6月25日 上午11:01
下一篇 2023年6月25日 上午11:14

相关推荐

发表回复

登录后才能评论