基于.NetCore开发评论系统(转)

博客前台以及后端涉及的代码主要在以下文件:

  • StarBlog.Web/Services/CommentService.cs
  • StarBlog.Web/Apis/Comments/CommentController.cs
  • StarBlog.Web/Views/Blog/Widgets/Comment.cshtml
  • StarBlog.Web/wwwroot/js/comment.js

管理后台的代码在以下文件:

  • src/views/Comment/Comments.vue

实现效果#

在开始之前,先来看看实现的效果吧。

博客前台#

讨论区的这部分UI使用 Vue 来驱动,为了开发效率还引入了 ElementUI 的组件,看起来风格跟博客原本的 Bootstrap 不太一样,不过还挺和谐的。

无须登录即可发表或回复评论,但需要输入邮箱地址并接收邮件验证码。

为了构建文明和谐的网络环境,发表评论之后会由小管家自动审核,审核通过才会展示。

如果小管家自动审核没有通过,会进入人工审核流程。

管理后台#

管理后台可以设置评论的审核通过或拒绝。

模型设计#

功能介绍前面都说了,不再赘述,直接从代码开始讲起。

这个功能新增了两个实体类,分别是 Comment 和 AnonymousUser

评论实体类的代码如下,可以看到除了 AnonymousUser 的引用,我还预留了一个 User 属性,目前博客前台是没有做登录功能的,预留这个属性可以方便以后的登录用户进行评论。

public class Comment : ModelBase {
  [Column(IsIdentity = false, IsPrimary = true)]
  public string Id { get; set; }

  public string? ParentId { get; set; }
  public Comment? Parent { get; set; }
  public List<Comment>? Comments { get; set; }

  public string PostId { get; set; }
  public Post Post { get; set; }

  public string? UserId { get; set; }
  public User? User { get; set; }

  public string? AnonymousUserId { get; set; }
  public AnonymousUser? AnonymousUser { get; set; }

  public string? UserAgent { get; set; }
  public string Content { get; set; }
  public bool Visible { get; set; }

  /// <summary>
  /// 是否需要审核
  /// </summary>
  public bool IsNeedAudit { get; set; } = false;

  /// <summary>
  /// 原因
  /// <para>如果验证不通过的话,可能会附上原因</para>
  /// </summary>
  public string? Reason { get; set; }
}

匿名用户实体类,简简单单的,需要访客填写的就三个字段,IP地址自动记录。

public class AnonymousUser : ModelBase {
  public string Id { get; set; }
  public string Name { get; set; }
  public string Email { get; set; }
  public string? Url { get; set; }
  public string? Ip { get; set; }
}

前端接口封装#

前端使用 axios 方便接口调用,当然使用 ES5 原生的 fetch 函数也可以,不过会多一些代码,懒是第一生产力。

使用 Promise 来包装返回值,便于使用 ES5 的 async/await 语法,获得跟C#类似的异步开发体验。

因为篇幅关系,本文无法列举所有接口封装代码,只举两个典型例子。

以下是获取匿名用户的接口,作为 GET 方法的例子。

getAnonymousUser(email, otp) {
  return new Promise((resolve, reject) => {
    axios.get(`/Api/Comment/GetAnonymousUser?email=${email}&otp=${otp}`)
      .then(res => resolve(res.data))
      .catch(res => resolve(res.response.data))
  })
}

以下是提交评论的接口,作为 POST 方法的例子。

submitComment(data) {
  return new Promise((resolve, reject) => {
    axios.post(`/Api/Comment`, {...data})
      .then(res => resolve(res.data))
      .catch(res => resolve(res.response.data))
  })
}

OK,这是俩最简单的例子,没有进行任何数据处理。

生成邮件验证码#

通常使用哈希表类的数据结构来存储这种数据,本项目中,我使用 .NetCore 自带的 MemoryCache 来存储验证码,除此之外,直接使用 Dictionary 或者 Redis 都是可选项。

需要在发送邮件的时候将邮箱地址与对应的验证码存入缓存,然后在验证的时候取出,验证通过后删除这一条记录。

首先在 Program.cs 中注册服务

builder.Services.AddMemoryCache();

检查邮箱地址是否有效#

在 CommentService.cs 中,封装一个方法,使用正则表达式检查邮箱地址。

/// <summary>
/// 检查邮箱地址是否有效
/// </summary>
public static bool IsValidEmail(string email) {
  if (string.IsNullOrEmpty(email) || email.Length < 7) {
    return false;
  }

  var match = Regex.Match(email, @"[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+");
  var isMatch = match.Success;
  return isMatch;
}

发送邮箱验证码#

为了方便发送邮件,我封装了 EmailService,其中的发送验证码的代码如下。

生成四位数的验证码直接使用 Random 生成一个在 1000-9999 之间的随机数即可。

关于发邮件,在友情链接的那篇文章里有介绍: 基于.NetCore开发博客项目 StarBlog - (28) 开发友情链接相关接口

/// <summary>
/// 发送邮箱验证码
/// <returns>生成随机验证码</returns>
/// <param name="mock">只生成验证码,不发邮件</param>
/// </summary>
public async Task<string> SendOtpMail(string email, bool mock = false) {
  var otp = Random.Shared.NextInt64(1000, 9999).ToString();

  var sb = new StringBuilder();
  sb.AppendLine($"<p>欢迎访问StarBlog!验证码:{otp}</p>");
  sb.AppendLine($"<p>如果您没有进行任何操作,请忽略此邮件。</p>");

  if (!mock) {
    await SendEmailAsync(
      "[StarBlog]邮箱验证码",
      sb.ToString(),
      email,
      email
    );
  }

  return otp;
}

检查是否有验证码的缓存,没有的话生成一个并发送邮件,然后存入缓存,这里我设置了过期时间是5分钟。

public async Task<(bool, string?)> GenerateOtp(string email, bool mock = false) {
  var cacheKey = $"comment-otp-{email}";
  var hasCache = _memoryCache.TryGetValue<string>(cacheKey, out var existingValue);
  if (hasCache) return (false, existingValue);

  var otp = await _emailService.SendOtpMail(email, mock);
  _memoryCache.Set<string>(cacheKey, otp, new MemoryCacheEntryOptions {
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
  });

  return (true, otp);
}

接口#

最后在 Controller 里实现这个接口。

这里只考虑了三种情况

  • 邮箱地址错误
  • 发送邮件成功
  • 上一个验证码在有效期,不发送邮件

其实还有一种情况是发送邮件失败,不过我没有写在这个接口里,如果发送失败会抛出错误,然后被全局的错误处理器拦截到并返回500信息。

/// <summary>
/// 获取邮件验证码
/// </summary>
[HttpGet("[action]")]
public async Task<ApiResponse> GetEmailOtp(string email) {
  if (!CommentService.IsValidEmail(email)) {
    return ApiResponse.BadRequest("提供的邮箱地址无效");
  }

  var (result, _) = await _commentService.GenerateOtp(email);
  return result
    ? ApiResponse.Ok("发送邮件验证码成功,五分钟内有效")
    : ApiResponse.BadRequest("上一个验证码还在有效期内,请勿重复请求验证码");
}

检查验证码与获取匿名用户#

前面在「模型设计」部分里有说到,未登录和已登录用户都可以发表评论(当然目前还没有提供其他用户登录的功能),本文只设计了未登录用户(即匿名用户)的评论发表流程。

在用户发送邮件验证码,并且验证码校验通过之后,可以通过接口获取到邮箱地址对应的匿名用户信息,这样不会让访客需要多次重复输入,同时也可以在下一次评论提交时修改这些信息。

核对验证码#

我在 CommentService.cs 中封装了以下方法用于核对验证码,并且增加了 clear 参数,可以控制验证通过后是否清除这个验证码。

/// <summary>
/// 验证一次性密码
/// </summary>
/// <param name="clear">验证通过后是否清除</param>
public bool VerifyOtp(string email, string otp, bool clear = true) {
  var cacheKey = $"comment-otp-{email}";
  _memoryCache.TryGetValue<string>(cacheKey, out var value);

  if (otp != value) return false;

  if (clear) _memoryCache.Remove(cacheKey);
  return true;
}

后端接口#

接口代码如下。

这里把生成新验证码的代码注释掉了,原本我设计的是获取匿名用户信息和发评论都需要验证码,所以匿名用户信息获取之后需要重新生成一个验证码(但不发邮件)给前端,然后前端更新一下暂存的验证码。

但是我发现这样有点过度设计了,而且这种做法会给访客带来一定的困扰(提交的验证码和邮件收到的不是同一个),于是把这一个功能简化了一下,但逻辑还保留着。

/// <summary>
/// 根据邮箱和验证码,获取匿名用户信息
/// </summary>
[HttpGet("[action]")]
public async Task<ApiResponse> GetAnonymousUser(string email, string otp) {
  if (!CommentService.IsValidEmail(email)) return ApiResponse.BadRequest("提供的邮箱地址无效");

  var verified = _commentService.VerifyOtp(email, otp, clear: false);
  if (!verified) return ApiResponse.BadRequest("验证码无效");

  var anonymous = await _commentService.GetAnonymousUser(email);
  // 暂时不使用生成新验证码的功能,避免用户体验割裂
  // var (_, newOtp) = await _commentService.GenerateOtp(email, true);

  return ApiResponse.Ok(new {
    AnonymousUser = anonymous,
    NewOtp = otp
  });
}

前端逻辑#

当访客在讨论区界面填写了验证码之后,会触发 change 事件,执行以下 JavaScript 代码。(篇幅关系做了简化)

当用户输入的验证码长度符合要求之后,会请求后端接口校验这个验证码是否正确,验证码正确的话后端会同时返回这个邮箱地址对应的匿名用户信息。

之后原本锁着的几个输入框也能交互了,或者也可以点击「回复」按钮对其他人的评论进行回复。

async handleEmailOtpChange(value) {
  console.log('handleEmailOtpChange', value)
  if (this.form.email?.length === 0 || value.length < 4) return
  
  // 设置 UI 加载状态
  this.[对应的UI组件] = true
  
  // 校验OTP & 获取匿名用户
  let res = await this.getAnonymousUser(this.form.email, value)

  if (res.successful) {
    if (res.data.anonymousUser) {
      this.form.userName = res.data.anonymousUser.name
      this.form.url = res.data.anonymousUser.url
    }
    this.form.emailOtp = res.data.newOtp
    // 锁住邮箱和验证码,不用编辑了
    this.[对应的UI组件] = true
    // 开启编辑用户名、网址、内容、回复
    this.[对应的UI组件] = false
  } else {
    this.$message.error(res.message)
  }
  this.userNameLoading = false
  this.urlLoading = false
}

提交评论#

这部分是比较复杂的,一步步来介绍

表单验证#

利用 ElementUI 提供的表单验证功能,虽然是比较老的组件库了,但这块的功能还是不错的。

首先定义表单规则。

formRules: {
  userName: [
    {required: true, message: '请输入用户名称', trigger: 'blur'},
    {min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur'}
  ],
  email: [
    {required: true, message: '请输入邮箱', trigger: 'blur'},
    {type: 'email', message: '邮箱格式不正确'}
  ],
  emailOtp: [
    {required: true, message: '请输入邮箱验证码', trigger: 'change'},
    {len: 4, message: '长度 4 个字符', trigger: 'change'}
  ],
  url: [
    {type: 'url', message: `请输入正确的url`, trigger: 'blur'},
  ],
  content: [
    {required: true, message: '请输入评论内容', trigger: 'blur'},
    {min: 1, max: 300, message: '长度 在 1 到 300 个字符', trigger: 'blur'},
    {whitespace: true, message: '评论内容只存在空格', trigger: 'blur'},
  ]
}

然后将这些定好的规则绑定到 form 组件上

<el-form :model="form" status-icon :rules="formRules" ref="form" class="my-3">

在提交的时候调用以下代码进行表单验证。

验证成功可以在其回调里执行接口调用等操作。

this.$refs.form.validate(async (valid) => {
  if (valid) {}
}

发送请求#

表单验证通过之后调用前面封装好的接口提交评论。

如果评论发表失败,则显示错误信息。

如果评论发表成功,显示信息之后,清空整个表单,但保留邮件地址,便于访客提交下一个评论。

最后无论成功与否,都会刷新评论列表。

async handleSubmit() {
  this.$refs.form.validate(async (valid) => {
    if (valid) {
      this.submitLoading = true
      let res = await this.submitComment(this.form)
      if (res.successful) {
        this.$message.success(res.message)
        let email = `${this.form.email}`
        this.handleReset()
        this.form.email = email
      } else this.$message.error(res.message)
      this.submitLoading = false
      await this.getComments()
    }
  })
}

接口设计#

前端的说完了,来到了后端部分,以下代码做了这些事:

  • 核对验证码
  • 获取匿名用户
  • 生成新评论
  • 小管家自动审核(敏感词检测)
  • 保存评论并返回结果
[HttpPost]
public async Task<ApiResponse<Comment>> Add(CommentCreationDto dto) {
  if (!_commentService.VerifyOtp(dto.Email, dto.EmailOtp)) {
    return ApiResponse.BadRequest("验证码无效");
  }

  var anonymousUser = await _commentService.GetOrCreateAnonymousUser(
    dto.UserName, dto.Email, dto.Url,
    HttpContext.GetRemoteIPAddress()?.ToString().Split(":")?.Last()
  );

  var comment = new Comment {
    ParentId = dto.ParentId,
    PostId = dto.PostId,
    AnonymousUserId = anonymousUser.Id,
    UserAgent = Request.Headers.UserAgent,
    Content = dto.Content
  };

  string msg;
  if (_filter.CheckBadWord(dto.Content)) {
    comment.IsNeedAudit = true;
    comment.Visible = false;
    msg = "小管家发现您可能使用了不良用语,该评论将在审核通过后展示~";
  }
  else {
    comment.Visible = true;
    msg = "评论由小管家审核通过,感谢您参与讨论~";
  }

  comment = await _commentService.Add(comment);

  return new ApiResponse<Comment>(comment) {
    Message = msg
  };
}

小管家审核#

说是评论审核,实际上就是敏感词检测,本项目使用 DFA(确定性有限状态自动机)来实现检测。

本来这部分都可以单独写一篇文章介绍了,不过考虑到都写到这了,也简单介绍一下好了。

DFA即确定性有限状态自动机,用于实现状态之间的自动转移。 与DFA对应的还有一个NFA非确定有限状态自动机,二者统称为有限自动状态机FSM。它们的主要区别在于 从一个状态转移的时候是否能唯一确定下一个状态。NFA在转移的时候往往不是转移到某一个确定状态,而是某个状态集合,其中的任一状态都可作为下一个状态,而DFA则是确定的。

DFA的组成#

  • 一个非空有限状态集合 Q
  • 一个输入集合 E
  • 状态转移函数 f
  • 初始状态 q0 为Q的一个元素
  • 终止状态集合 Z 为Q的子集

一个DFA可以写成 M=(Q, E, f, q0, Z)

如何使用DFA实现敏感词过滤算法#

现假设有NND, CNM, MLGB三个敏感词,则:

Q = {N, NN, NND, C, CN, CNM, M, ML, MLG, MLGB}

以所有敏感词的组成作为状态集合,状态机只需在这些状态之间转移即可

E = {B, C, D, G, L, N, M}, 以所有组成敏感词的单个字符作为输入集合,状态机只需识别构成敏感词的字符。

qo = null 初始状态为空,为空的初态可以转移到任意状态

Z = {NND, CNM, MLGB} 识别到任意一个敏感词, 状态转移就可以终止了。

那么f 就可以是一个 读入一个字符后查询是否为Q中的状态进而转移的函数,则转移过程为

f(null, N) = N, f(N, N) = NN, f(NN, D) = NND

f(null, C) = C, f(C, N) = CN, f(CN, M) = CNM

f(null, M) = M , f(M, L) = ML, f(ML, G) = MLG, f(MLG, B) = MLGB

使用方式#

具体的实现代码比较长,我就不贴了,本文的篇幅已经严重超长了…

总之我把这部分代码封装好了,在 CodeLab.Share 这个 nuget 包里,直接调用就完事了。

所以可以看到我在 StarBlog 项目里写了一个 TempFilterService

因为封装好的 StopWordsToolkit 有很多功能,不仅可以检测敏感词,还可以自动替换成星号啥的,当时在做这个功能的时候还想着要不要加点奇奇怪怪的功能,所以叫把这个 service 加了个 temp 的前缀。

public class TempFilterService {
    private readonly StopWordsToolkit _toolkit;

    public TempFilterService() {
        var words = JsonSerializer.Deserialize<IEnumerable<Word>>(File.ReadAllText("words.json"));
        _toolkit = new StopWordsToolkit(words!.Select(a => a.Value));
    }

    public bool CheckBadWord(string word) {
        return _toolkit.CheckBadWord(word);
    }
}

这里初始化的时候需要 words.json 这个敏感词库文件,为了网络环境的文明和谐,本项目的开源代码里不能提供,需要的同学可以自行搜集。

格式是这样的

[
  {
    "Id": 1,
    "Value": "小可爱",
    "Tag": "暴力"
  },
  {
    "Id": 2,
    "Value": "河蟹",
    "Tag": "广告"
  }
]

人工审核#

当评论被小管家判定有敏感词的时候,就会标记 IsNeedAudit=true 进入人工审核流程。

就是 Accept 和 Reject 这俩方法。

public async Task<Comment> Accept(Comment comment, string? reason = null) {
  comment.Visible = true;
  comment.IsNeedAudit = false;
  comment.Reason = reason;
  await _commentRepo.UpdateAsync(comment);
  return comment;
}

对应的接口

[Authorize]
[HttpPost("{id}/[action]")]
public async Task<ApiResponse<Comment>> Accept([FromRoute] string id, [FromBody] CommentAcceptDto dto) {
  var item = await _commentService.GetById(id);
  if (item == null) return ApiResponse.NotFound();
  return new ApiResponse<Comment>(await _commentService.Accept(item, dto.Reason));
}

管理后台#

接下来会有专门的一个系列介绍基于 Vue 的管理后台开发,所以本文不会花太多篇幅介绍,只简单记录一点。

原本我使用了 Dialog 来让用户输入通过或拒绝某个评论审核的原因,后面发现 ElementUI 提供了 prompt 功能,可以弹出一个简单的输入框。

所以拒绝某个评论的代码如下

handleReject(item) {
  this.$prompt('请输入原因', '审核评论 - 补充原因', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
  }).then(({value}) => {
    this.$api.comment.reject(item.id, value)
      .then(res => {
      this.$message.success('操作成功!')
    })
      .catch(res => {
      console.error(res)
      this.$message.warning(`操作失败!${res.message}`)
    })
      .finally(() => this.loadData())
  }).catch(() => {
  })
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习教程(第十三章 Linux数据备份与恢复)

第十三章 Linux数据备份与恢复 不知道大家有没有丢失过重要的数据呢&#xff1f; 丢失数据的理由是多种多样的&#xff0c;有人是因为重装系统时&#xff0c;没有把加密文件的密钥导出&#xff0c;重装系统后密钥丢失&#xff0c;导致所有的加密数据不能解密&#xff1b;也有人…

C语言—小小圣诞树

这个代码会询问用户输入圣诞树的高度&#xff0c;然后根据输入的高度在控制台上显示相应高度的圣诞树。 #include <stdio.h>int main() {int height, spaces, stars;printf("请输入圣诞树的高度: ");scanf("%d", &height);spaces height - 1;st…

使用Halcon实现模板匹配

图片: 代码: read_image (Image, C:/Users/14348/Desktop/mobanpipei.jpg) get_image_size (Image, Width, Height) dev_close_window() dev_open_window (0, 0, Width, Height, black, WindowHandle) dev_display (Image) draw_rectangle1 (WindowHandle, Row1, Column1, Ro…

字符设备驱动框架的编写

一. 简介 我们在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器&#xff0c;在 Linux 驱动开发中&#xff0c;肯定也是要初始化相应的外设寄存器。 只是在 Linux 驱动开发中&#xff0c; 我们需要按照其规定的框架来编写驱动&#xff0c;所以说学 …

Docker介绍,Docker安装

docker镜像仓库官网 一、Docker的基本概念 1.Docker的三大核心组件 docker 镜像 --------docker images docker 仓库---------docker registeries docker 容器---------docker containers 2.Docker 镜像 Docker镜像是运行docker容器时的只读模板&#xff0c;每一个镜像由一…

设计模式——迭代器模式

引言 迭代器模式是一种行为设计模式&#xff0c; 让你能在不暴露集合底层表现形式 &#xff08;列表、 栈和树等&#xff09; 的情况下遍历集合中所有的元素。 问题 集合是编程中最常使用的数据类型之一。 尽管如此&#xff0c; 集合只是一组对象的容器而已。 大部分集合使用…

springboot221酒店管理系统

springboot221酒店管理系统 源码获取&#xff1a; https://docs.qq.com/doc/DUXdsVlhIdVlsemdX

CentOS7安装教程

1.准备工作 安装虚拟机软件 VMware Workstation 17 Player下载CentOS7 镜像 2. 安装VMware Workstation 17 Player 官网 下载链接 下载好了安装包&#xff0c;双击安装包&#xff0c;傻瓜式安装一直下一步&#xff0c;安装即可。 3. 安装CentOS7 官网 推荐下载地址&…

Vue 按键修饰符

常用按键修饰符&#xff1a;enter【回车】、delete【删除】、esc【退出】、space【空格】、tab【缩进】、up【上】、down【下】、left【左】、right【右】 。 系统按键修饰符&#xff1a;ctrl、alt、shift、meta【四个小方块】 。 鼠标修饰符&#xff1a;left【左键】、right…

计算机服务器中了mkp勒索病毒怎么办,mkp勒索病毒解密恢复

在计算机技术飞速发展的今天&#xff0c;越来越多的企业走向了数字化办公模式&#xff0c;极大地方便了企业的生产运营&#xff0c;为企业带来了更高的效率。但网络威胁无处不在&#xff0c;网络威胁手段随着计算机技术的不断发展也在不断增加。近期&#xff0c;云天数据恢复中…

【MySQL】存储过程/参数验证/函数

文章目录 存储过程是什么&#xff1f;创建存储过程&#xff1a;creat procedure…begin…end修改默认分隔符&#xff1a;dilimiter调用存储过程&#xff1a;call删除存储过程&#xff1a;drop procesure if exists练习 存储过程中添加参数&#xff1a;(形参 数据类型)示例练习 …

RocketMQ系统性学习-SpringCloud Alibaba集成RocketMQ以及批量发送消息、消息过滤实战

文章目录 批量发送消息消息过滤 批量发送消息 批量发送消息可以减少网络的 IO 开销&#xff0c;让多个消息通过 1 次网络开销就可以发送&#xff0c;提升数据发送的吞吐量 虽然批量发送消息可以减少网络 IO 开销&#xff0c;但是一次也不能发送太多消息 批量消息直接将多个消…

docker-harbor仓库

Docker 镜像 容器 仓库 仓库&#xff1a;保存镜像 私有&#xff1a;自定义用户的形式登录仓库&#xff0c;拉取或者上传镜像&#xff08;内部管理的用户&#xff09; Harbor&#xff1a;是VMware公司开发的&#xff0c;开源的企业级的docker register项目 帮助用户快速的搭建…

对DataFrame中每列的数值进行限值 指定最小值和最大值:超过最大值的数据,则改为最大值小于最小值的数据,则改为最小值 DataFrame.clip()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 对DataFrame中每列的数值进行限值 指定最小值和最大值&#xff1a; 超过最大值的数据&#xff0c;则改为最大值 小于最小值的数据&#xff0c;则改为最小值 DataFrame.clip() [太阳]选择题 请…

GoLang 学习 (入门)

go run 1.go 执行命令 go build 1.go 打包为exe 快速 并且无依赖 在开始项目 需要 生成 go.mod go mod init mod 终端执行 go: creating new go.mod: module mod go: to add module requirements and sums:go mod tidy go的基本目录结构 src ------gocode ------------项…

整合SpringSecurity

目录 前言 数据库设计 用户表 角色表 用户角色表 权限表 角色权限表 插入数据 表的实体类 用户表实体类 角色表实体类 权限表实体类 mapper层接口 UserMapper RoleMapper AuthorityMapper 封装登录信息 统一响应结果 上下文相关类 jwt令牌工具类 依赖导入…

Win11极速安装Tensorflow-gpu+CUDA+cudnn

文章目录 0.pip/conda换默认源1.Anacondapython虚拟环境2.安装CUDA以及cudnn测试tensorflow的GPU版本安装成功的办法参考文献 不要使用官网版本&#xff0c;直接使用conda版本&#xff0c;有对应的包&#xff0c;安装很方便 0.pip/conda换默认源 为了高效下载&#xff0c;建议…

nodejs微信小程序+python+PHP邮件过滤系统的设计与实现-计算机毕业设计推荐

邮件过滤系统综合网络空间开发设计要求。该系统主要设计并完成了管理过程中的用户登录、个人信息修改、邮件信息、垃圾箱、意见反馈、论坛等功能。该系统操作简便&#xff0c;界面设计简洁&#xff0c;不但可以基本满足本行业的日常管理工作&#xff0c; 目的是将邮件过滤通过网…

Flink系列之:自定义函数

Flink系列之&#xff1a;自定义函数 一、自定义函数二、概述三、开发指南四、函数类五、求值方法六、类型推导七、自动类型推导八、定制类型推导九、确定性十、内置函数的确定性十一、运行时集成十二、标量函数十三、表值函数十四、聚合函数十五、表值聚合函数 一、自定义函数 …

HttpRunner接口自动化测试框架

简介 HttpRunner是一款面向 HTTP(S) 协议的通用测试框架&#xff0c;只需编写维护一份 YAML/JSON 脚本&#xff0c;即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。 项目地址&#xff1a;GitHub - httprunner/httprunner: HttpRunner 是一个开源的 API/UI…
最新文章