博客系统完整开发流程
前言
通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使用, 并完成了图书管理系统的常规功能开发, 接下来我们系统的从0到1完成⼀个项⽬的开发.
企业开发的流程
1. 需求评审(产品经理(PM)会和运营(想口号),UI,测试,开发等沟通) ,会涉及到背景/目标/怎么做,可能会有多次评审(对于需求不清晰的部分,文档需要补充) -> 需求文档
2. 开发阶段(前后端并行开发) 1> 任务分工(对于边界不清晰的任务,再进行探讨). 2> 后端进行方案设计 -> 接口涉及,数据库涉及,ER,UML,流程图等,deadLine(前后端把deadline出了,测试人员才会决定测试时间),复杂项目需要出测试列表,上线计划 3> 方案设计评审 4> 开发-> 代码review 5> 自测
3. 测试(联调):各个端一起来(前端调后端,后端调前端..)
4. 上线

项目介绍
使⽤SSM框架实现⼀个简单的博客系统 共5个页面
1. 用户登录
2. 博客发表⻚
3. 博客编辑⻚
4. 博客列表⻚
5. 博客详情⻚
功能描述:
⽤⼾登录成功后, 可以查看所有⼈的博客. 点击 <<查看全⽂>> 可以查看该博客的正⽂内容. 如果该博客作者为当前登录⽤⼾, 可以完成博客的修改和删除操作, 以及发表新博客
页面预览用户登录
博客列表页
博客详情页
博客发表/修改页 
后端需要提供的功能和接口
1. 用户登录: 根据用户名和密码判断用户输入的信息是否能从数据库找到

2. 登录用户的个人页: 博客列表显示当前登录用户的信息,根据用户的ID,返回用户相关信息(后面再详细设计)
3. 博客列表页: 查询你博客的列表(点击查看全文之后)

4. 作者个人主页: 根据用户ID,返回作者信息(俩种实现可能: 1> 根据用户ID返回 (调用方要先拿到作者ID) 2> 根据博客ID返回)
5. 博客详细: 根据博客ID,返回博客信息

点击编辑之后
6. 博客编辑: 1> 根据博客ID,返回博客信息. 2> 根据用户输入的信息,更新博客

如果是点击删除的话
7. 博客删除: 根据ID,删除博客

点击写博客
8. 写博客: 根据用户输入的信息,添加博客

我们把上面的功能再转化为接口
接口
1. 用户相关
根据用户名和密码,判断用户输入的信息是否正确
根据用户的ID,返回用户相关信息
2. 博客相关
查询博客列表
根据博客ID(前端根据博客详情,拿到作者信息,在根据作者信息调用用户信息),返回作者信息
根据博客ID,返回博客详情
根据用户输入的信息,更新博客
根据博客ID,删除博客
根据用户输入信息,添加博客
后面的接口我们会更加详细的写,这里只写了大概(会写参数,返回类型,请求方式...)

1. 准备工作
数据库设计
数据的准备工作: 创建用户表和博客表
架构设计(画图)-> 画实体,实体表和关系表 (此时我们直接拿一个属性user_id进行关联)
-- 建表SQL
create database if not exists java_blog_spring charset utf8mb4;
use java_blog_spring;
-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user;
CREATE TABLE java_blog_spring.user(`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR ( 128 ) NOT NULL,`password` VARCHAR ( 128 ) NOT NULL,`github_url` VARCHAR ( 128 ) NULL,`delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),PRIMARY KEY ( id ),
UNIQUE INDEX user_name_UNIQUE ( user_name ASC )) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';-- 博客表
drop table if exists java_blog_spring.blog;
CREATE TABLE java_blog_spring.blog (`id` INT NOT NULL AUTO_INCREMENT,`title` VARCHAR(200) NULL,`content` TEXT NULL,`user_id` INT(11) NULL,`delete_flag` TINYINT(4) NULL DEFAULT 0,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';-- 新增用户信息
insert into java_blog_spring.user (user_name, password,github_url)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java45");
insert into java_blog_spring.user (user_name, password,github_url)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");insert into java_blog_spring.blog (title,content,user_id) values("第一篇博客","111我是博客正文我是博客正文我是博客正文",1);
insert into java_blog_spring.blog (title,content,user_id) values("第二篇博客","222我是博客正文我是博客正文我是博客正文",2);
创建项目
四个依赖加上
前端代码准备(在资源绑定里面)

数据库yml配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration: # 配置打印 MyBatis⽇志map-underscore-to-camel-case: true #配置驼峰⾃动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句mapper-locations: classpath:mapper/*Mapper.xml
# 设置日志文件
logging:file:name: spring-book.log
2. 项目公共模块
项⽬分为控制层(Controller), 服务层(Service), 持久层(Mapper). 各层之间的调⽤关系如下:
公共层和实体层为上面的所有都提供服务.


2.1 实体类
1> 用户实体
package org.xiaobai.blog_system.model;import lombok.Data;import java.util.Date;
@Data
public class UserInfo {private Integer id;private String userName;private String password;private String githubUrl;//和数据库字段对应,默认数据库字段的下划线后面的第一个单词大写private Integer deleteFlag;private Date createTime;private Date updateTime;
}
2> 博客实体
package org.xiaobai.blog_system.model;import lombok.Data;import java.util.Date;@Data
public class BlogInfo {private Integer id;private String title;private Integer userId;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
3> 结果实体
package org.xiaobai.blog_system.model;import lombok.Data;
import org.xiaobai.blog_system.enums.ResultStatus;//统一结果,通常是业务码
@Data
public class Result<T> {//业务码200-成功 -1失败 -2 未登录private ResultStatus code;//也可以写成枚举类//错误信息private String errMsg;//接口响应的数据private T data;//设置成功的时候的信息public static <T>Result success(T data){Result result = new Result();result.setCode(ResultStatus.SUCCESS);result.setErrMsg("");result.setData(data);return result;}//失败的时候不返回数据public static <T>Result fail(String errMsg){Result result = new Result();result.setCode(ResultStatus.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;}//失败的时候返回数据public static <T>Result fail(String errMsg,T data){Result result = new Result();result.setCode(ResultStatus.FAIL);result.setErrMsg(errMsg);result.setData(data);return result;}
}
4> 枚举类
package org.xiaobai.blog_system.enums;public enum ResultStatus {SUCCESS,FAIL,NOLOGIN;
}
5> 统一结果返回
package org.xiaobai.blog_system.ResponseAdvice;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.xiaobai.blog_system.model.Result;
//TODO 统一结果返回
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return false;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//对结果进行进一步的划分if(body instanceof Result<?>){return body;}if(body instanceof String){//如果是String就进行序列化处理try {return objectMapper.writeValueAsString(Result.success(body));} catch (JsonProcessingException e) {throw new RuntimeException(e);}}return Result.success(body);}
}
5> 统一异常处理
package org.xiaobai.blog_system.ResponseAdvice;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.xiaobai.blog_system.model.Result;//TODO 统一异常处理
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionHandle {@ExceptionHandlerpublic Result handle(Exception e){//打印异常信息log.error("发生异常,e: ",e);return Result.fail("内部错误,请练习管理员");}
}
3. 业务代码
3.1 持久层
接口和数据库操作的关系
根据数据操作写mapper
1> 用户相关的mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}
2> 博客相关的mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.xiaobai.blog_system.model.BlogInfo;
import java.util.List;
@Mapper
public interface BlogMapper {//1. 返回博客列表@Select("select * from blog where delete_flag = 0")List<BlogInfo> selectAll();//2. 根据博客ID,返回博客信息@Select("select * from blog where id = #{id}")BlogInfo selectById(Integer id);//3. 更新博客(涉及更新和删除俩部分)//后面再写,使用xml的形式Integer updateBlog(BlogInfo blogInfo);//4. 发表博客(插入博客)@Insert("insert into blog(title,content,user_id) values (#{title},#{content},#{userId})")Integer insertBlog(BlogInfo blogInfo);
}
更新/删除操作我们使用update,需要进行判断,因此我们需要使用xml的方式
首先我们先进行配置
对应关系

我们在xml里面要写
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xiaobai.blog_system.mapper.BlogMapper">
然后我们进行测试(在mapper里面的接口点击生成测试用例)
测试用户的mapper

测试博客的mapper

我们再单独的对更新语句进行测试

修改一下,就是删除语句测试

接下来正式进入接口功能实现
controler -> service -> mapper
- Controller:接收前端请求,调用Service层。
- Service:处理业务逻辑,调用Mapper层进行数据操作。
- Mapper:与数据库交互,执行数据持久化操作。
总结:
- Controller 处理用户请求和响应。
- Service 处理业务逻辑,调用 Mapper 层进行数据库操作。
- Mapper 执行与数据库的直接交互操作。
3.2 实现博客列表
约定前后端交互接口

客⼾端给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给客⼾端返回了⼀个 JSON
格式的数据.
后端代码
controller
service

mapper

我们也可以限制接口请求类型

前端代码
操作过程

编写结果

结果展示

3.3 实现博客详情
接口定义

后端代码
controller

service

mapper

前端代码
编写过程
我们也可以把时间的格式进行设计一下: dateFormat
package org.xiaobai.blog_system.utils;import java.text.SimpleDateFormat;
import java.util.Date;//对于时间的公共处理
public class DateUtils {public static String dateFormat(Date date){//2025-02-24 21:01SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");return format.format(date);}public static void main(String[] args) {System.out.println(dateFormat(new Date()));}}
结果:

编写结果

结果展示
3.4 实现登陆
之前我们实现图书管理系统的适合采用的思路

传统思路:
• 登陆⻚⾯把⽤⼾名密码提交给服务器.
• 服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端
• 如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器.
问题:
集群环境下⽆法直接使⽤Session.
原因分析:
我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡(相当于调度器). 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.

假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡(负载理解为压力和流量), 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上
2. 查询操作 ⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题, 提⽰⽤⼾登录, 这是⽤⼾不能忍受的.(各个机器都是独立的,因此在第一个机器里面虽然已经set SessionId了,但是和第二个机器没有关系)
` 解决方案

Redis简单介绍: Redis就是一个数据结构,就理解为存数据的

接下来我们介绍第二种⽅案: 令牌技术
令牌技术
令牌的概念
之前我们存储在session里面的信息,现在存储在了token里面
令牌其实就是⼀个⽤⼾⾝份的标识, 名称起的很⾼⼤上, 其实本质就是⼀个字符串.
⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证 ⾝份证不能伪造, 可以辨别真假.
身份证: 公安局发放,公安局来进行鉴别真假
1> 存储信息
2> 不能伪造
3> 可以辨别真假
同理: 我们的令牌token是由服务端生成的,因此我们要在服务端进行鉴别真假.也具备上面身份证的作用
令牌的逻辑
服务器: 要能够生成token和校验token
1> 用户登录,服务器生成token(生成一个字符串)
2> 把token返回给客户端
3> 客户端携带token,再次访问,服务器对token进行校验

服务器具备⽣成令牌和验证令牌的能⼒我们使⽤令牌技术, 继续思考上述场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.
2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如localStorage)
3. 查询操作 ⽤⼾登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明⽤⼾已经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执⾏登录操作.
令牌的优缺点
优点:
• 解决了集群环境下的认证问题
• 减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.
小总结

JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.JWT是令牌的一种实现方式.
介绍
JWT全称: JSON Web Token官⽹:Auth0 | JWT Handbook
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息.其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.
JWT组成
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
• Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
• Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容.
⽐如: {"userId":"123","userName":"zhangsan"} , 也可以存在jwt提供的现场字段, ⽐如exp(过期时间戳)等.此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
• Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性.防⽌被篡改, ⽽不是防⽌被解析.
JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败. 就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任何⼈都可以看到⾝份证的信息, jwt 也是)
JWT不是加密的,而是把信息通过Base64的编码表示的

对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌 Base64是编码⽅式,⽽不是加密⽅式
JWT令牌生成和校验
1. 引⼊JWT令牌的依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is
preferred --><version>0.11.5</version><scope>runtime</scope></dependency>
2. 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

如果设置的签名长度太短了,会报错

我们使用报错信息所提示的方法去创建一个key然后看三个方法的对应关系

令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了.
令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败. 修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改.
但是,我们的登录安全问题,不能只通过JWT令牌来进行保证(当前的JWT只是解决了集群问题(数据不能传递的问题))
代码
package org.xiaobai.blog_system;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JwtUtilsTest {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimkJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//生成 JWT 令牌@Testpublic void genJwt() {Map<String,Object> claim = new HashMap<>();claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION)) //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);}////生成一个随机的 JWT key,用于签名 JWT。@Testpublic void genKey(){SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);String str = Encoders.BASE64.encode(secretKey.getEncoded());System.out.println(str);}//此方法用于 解析 JWT 令牌 并验证其合法性。
@Test//根据令牌校验合法性public void parseToken(){//此时不管你改哪个部分都不会校验成功,过期了也不能String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiZXhwIjoxNzQwMzcwMDk0fQ.KNxDj9RV3IOLq9-YnCBbqFigtxqGtLY2Aswr3UqgQ94";JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims =build.parseClaimsJws(token).getBody(); //解析tokenSystem.out.println(claims);}
}
学习令牌的使用之后, 接下来我们通过令牌来完成用户的登录
1. 登陆⻚⾯把⽤⼾名密码提交给服务器.
2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作
把JWT应用到我们的登录接口上
接口定义

后端代码
controller
package org.xiaobai.blog_system.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.blog_system.model.Result;
import org.xiaobai.blog_system.model.UserInfo;
import org.xiaobai.blog_system.service.UserService;
import org.xiaobai.blog_system.utils.JwtUtils;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(String username, String password) {//1. 后端进行参数的校验(合法性)if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return Result.fail("账号或者密码不能为空");}//2. 校验密码是否正确//从数据库中查找用户UserInfo userInfo = userService.SelectByName(username);//用户不存在if (userInfo == null) {return Result.fail("用户不存在");}//密码错误if (!password.equals(userInfo.getPassword())) {return Result.fail("密码错误");}//3. 密码正确,返回tokenMap<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("userName",userInfo.getUserName());//生成tokenString token = JwtUtils.genJwtToken(claim);//返回tokenreturn Result.success(token);}
}
service
package org.xiaobai.blog_system.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xiaobai.blog_system.mapper.UserInfoMapper;
import org.xiaobai.blog_system.model.UserInfo;@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public UserInfo SelectByName(String username) {return userInfoMapper.selectByName(username);}
}
mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}
utils:
package org.xiaobai.blog_system.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtUtils {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimkJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//1. 生成token//根据载荷信息生成public static String genJwtToken(Map<String,Object> claim){claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION)) //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);return token;}//2. 校验token: claims为null校验失败public Claims parseToken(String token){//此时不管你改哪个部分都不会校验成功,过期了也不能//产生了异常就失败JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = null;try {claims =build.parseClaimsJws(token).getBody(); //解析token}catch (Exception e){log.error("解析token失败:{}",token);return null;}return claims;}
}
这里注意
claim继承了map

验证账号和密码不能为空的时候,我们返回类型的选择

前端代码
编写过程

编写结果

结果展示

3.5 实现强制要求登陆
当⽤⼾访问 博客列表⻚ 和 博客详情⻚ 时, 如果⽤⼾当前尚未登陆, 就⾃动跳转到登陆⻚⾯.
我们可以采⽤拦截器来完成, token通常由前端放在header中, 我们从header中获取token, 并校验token是否合法.
我们客户端保存了token之后,客户端在发送后端请求的时候会带着token,服务端将会接收这个token. 服务器在接收token的时候会有俩个问题:
1. 客户端把token放在哪里比较合适?(header(常常放在这里),参数,url)
2. 服务端强制登录: 通过拦截器实现
1> 从header获取token
2> 校验token,判断是否放行
以后我们再发送请求的时候,就会把token放到http里面的header里面传给服务器

具体流程

如果我们的token没用发送到后端
1. 在我们的common.js中打印日志,判断是否执行了该方法
2. 如果未执行,把这段代码粘到html页面中
3. 确认名称的对应关系
接口定义

后端代码
controller
package org.xiaobai.blog_system.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.blog_system.model.Result;
import org.xiaobai.blog_system.model.UserInfo;
import org.xiaobai.blog_system.service.UserService;
import org.xiaobai.blog_system.utils.JwtUtils;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(String username, String password) {//1. 后端进行参数的校验(合法性)if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return Result.fail("账号或者密码不能为空");}//2. 校验密码是否正确//从数据库中查找用户UserInfo userInfo = userService.SelectByName(username);//用户不存在if (userInfo == null) {return Result.fail("用户不存在");}//密码错误if (!password.equals(userInfo.getPassword())) {return Result.fail("密码错误");}//3. 密码正确,返回tokenMap<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("userName",userInfo.getUserName());//生成tokenString token = JwtUtils.genJwtToken(claim);//返回tokenreturn Result.success(token);}
}
service
package org.xiaobai.blog_system.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xiaobai.blog_system.mapper.UserInfoMapper;
import org.xiaobai.blog_system.model.UserInfo;@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public UserInfo SelectByName(String username) {return userInfoMapper.selectByName(username);}
}
mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}
utils
生成并解析JWT
package org.xiaobai.blog_system.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtUtils {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimk2dJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//1. 生成token//根据载荷信息生成public static String genJwtToken(Map<String,Object> claim){claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION)) //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);return token;}//2. 校验token: claims为null校验失败public static Claims parseToken(String token){//此时不管你改哪个部分都不会校验成功,过期了也不能//产生了异常就失败JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = null;try {claims =build.parseClaimsJws(token).getBody(); //解析token}catch (Exception e){log.error("解析token失败:{}",token);return null;}return claims;}
}
config
把拦截器应用到项目
package org.xiaobai.blog_system.config;
//把拦截器应用到项目
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.xiaobai.blog_system.interceptor.LoginInterceptor;import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;//把之前写的拦截器注入进来private final List excludes = Arrays.asList(//排除路径"/**/*.html","/blog-editormd/**","/css/**","/js/**","/pic/**","/user/login");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor)//添加拦截器.addPathPatterns("/**")//对哪些路径生效.excludePathPatterns(excludes);//排除那些路径}
}
interceptor
拦截器具体内容
package org.xiaobai.blog_system.interceptor;import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.xiaobai.blog_system.utils.JwtUtils;//拦截器的具体内容
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 从header中获取tokenString token = request.getHeader("user_token_header");//2. 校验token,判断是否放行log.info("从header中获取token:{}",token);Claims claims = JwtUtils.parseToken(token);if(claims == null){//校验失败response.setStatus(401);return false;}return true;}
}
前端代码
编写过程

编写结果


注意:
前端请求时, header中统⼀添加token, 可以写在common.js中
$(document).ajaxSend(function (e, xhr, opt) {
var user_token = localStorage.getItem("user_token"); xhr.setRequestHeader("user_token", user_token);});
ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数
• event - 包含 event 对象
• xhr - 包含 XMLHttpRequest 对象
• options - 包含 AJAX 请求中使⽤的选项
结果展示
3.6 实现显示用户信息
我们可以看见,这个信息是写死的

我们期望这个信息可以随着⽤⼾登陆⽽发⽣改变.
• 如果当前⻚⾯是博客列表⻚, 则显⽰当前登陆⽤⼾的信息.
• 如果当前⻚⾯是博客详情⻚, 则显⽰该博客的作者⽤⼾信息.
注意: 当前我们只是实现了显⽰⽤⼾名, 没有实现显⽰⽤⼾的头像以及⽂章数量等信息.
接口定义
在博客列表⻚, 获取当前登陆的⽤⼾的⽤⼾信息.

后端代码
controller
//获取当前登录用户信息的信息,这个从token里面去拿@RequestMapping("/getUserInfo")public UserInfo getLoginUserInfo(HttpServletRequest request){//获取tokenString token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);//从token里面获取登录用户的idInteger userId = JwtUtils.getIdByToken(token);if(userId == null){//用户未登录return null;}UserInfo userInfo = userService.selectById(userId);return userInfo;}
注意

调试的时候可以根据这个方法来看是哪个文件不存在

关于技术选型: 为什么选JWT不选其他?

关于我们接口返回私密信息问题

service

mapper

前端代码
编写过程
根据前端的返回值去写

编写结果

结果展示

接口定义
在博客详情⻚, 获取当前⽂章作者的⽤⼾信息
后端代码
controller
//获取作者信息,根据博客id获取作者信息@RequestMapping("/getAuthorInfo")public UserInfo getAuthorInfo(Integer blogId){//1. 根据博客id获取作者id//2. 根据作者ID,获取作者信息if(blogId <= 0){return null;}UserInfo userInfo = userService.getAuthorInfo(blogId);return userInfo;}
service

mapper

前端代码
编写过程
根据后端返回结果来进行编写

编写结果

结果展示

代码修改只会,前端页面没用变化(查看网页源代码,代码没有更新)解决方案

3.7 实现用户退出
关于用户的登录,我们是通过后端来进行判断的,后端通过拦截器从header里面拿到token
也就是这个值

那如果没有值的话,后端就接收不到了,因此前端直接清除掉token即可.
在js里面使用

3.8 实现发布博客
接口定义
后端代码
controller

service

mapper

前端代码
编写结果

结果展示

代码亮点,学习其他的写博客的网站,可以对字体大小进行编辑,加一些样式.之前的只是一个空的文本框,写不了格式,然后看一下其他的网站博客的编辑格式是怎么实现的,先调研了其他网站用的什么编辑器,然后选择使用哪个编辑器(markdown),如何实现编辑器(1. 自己写 2. 开源的)
editor.md 简单介绍
editor.md 是⼀个开源的⻚⾯ markdown 编辑器组件.
官⽹参见: http://editor.md.ipandao.com/
代码: https://pandao.github.io/editor.md/
我们直接引入进来即可
<link rel="stylesheet" href="editormd/css/editormd.css" />
<div id="test-editor"><textarea style="display:none;">### 关于 Editor.md**Editor.md** 是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。</textarea>
</div>
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="editormd/editormd.min.js"></script>
<script type="text/javascript">$(function() {var editor = editormd("test-editor", {// width : "100%",// height : "100%",path : "editormd/lib/"});});
</script>

因为我的后端返回过来的数据是json,因此我前端取值也要设置接收的为json,前端字符串和json相互转换.

修改详情⻚⻚⾯显示
此时会发现详情⻚会显⽰markdown的格式符号, 我们需要把markdown格式转换为html格式

指定对谁进行渲染

可以拓展的地方

3.9 实现删除/编辑博客
进⼊⽤⼾详情⻚时, 如果当前登陆⽤⼾正是⽂章作者, 则在导航栏中显示 [编辑] [删除] 按钮, ⽤⼾点击时则进⾏相应处理.(作者的userId和登录的userId是一样的那么就可以显示)

需要实现两件事:
• 判定当前博客详情⻚中是否要显示[编辑] [删除] 按钮
• 实现编辑/删除逻辑.
删除采⽤逻辑删除, 所以和编辑其实为同⼀个接⼝.
约定前后端交互接口
1. 判定是否要显⽰[编辑] [删除] 按钮
修改之前的 获取博客 信息的接⼝, 在响应中加上⼀个字段.
• loginUser 为 1 表⽰当前博客就是登陆⽤⼾⾃⼰写的.


后端代码
controller

service

mapper

前端代码
编写结果

结果展示

修改博客接口

后端代码
controller

service

mapper

前端代码
代码
结果演示(此时更新时间没同步,需要改)

3.10 加密/加盐
加密介绍(JWT,和这个都是学习思想)
在MySQL数据库中, 我们常常需要对密码, ⾝份证号, ⼿机号等敏感信息进⾏加密, 以保证数据的安全性. 如果使⽤明⽂存储, 当⿊客⼊侵了数据库时, 就可以轻松获取到⽤⼾的相关信息, 从⽽对⽤⼾或者企业造成信息泄漏或者财产损失.⽬前我们⽤⼾的密码还是明⽂设置的, 为了保护⽤⼾的密码信息, 我们需要对密码进⾏加密.
比如我们现在数据库里面的信息(密码)是明文显示的,因此我们需要对敏感信息进行加密

密码算法分类
密码算法主要分为三类: 对称密码算法, ⾮对称密码算法, 摘要算法.
加密: y = f(x) ,x是明文,y是密文,f(x)是加密算法. 也就是一个算法按照一定的规则把一个字符串(一个信息)->另外一个信息.
对称密码: y = f(x), x=f(y)
非对称密码: y=f(x),x=m(y)

1. 对称密码算法 是指加密秘钥和解密秘钥相同的密码算法. 常⻅的对称密码算法有: AES, DES, 3DES,RC4, RC5, RC6 等.(了解即可)
2. ⾮对称密码算法 是指加密秘钥和解密秘钥不同的密码算法. 该算法使⽤⼀个秘钥进⾏加密, ⽤另外⼀个秘钥进⾏解密.
◦ 加密秘钥可以公开,⼜称为 公钥
◦ 解密秘钥必须保密,⼜称为 私钥
比如我给我的箱子上锁,锁是大家都能看见的,因此叫公钥,但是只能用我自己的钥匙打开,要是就是私钥
常见的⾮对称密码算法有: RSA, DSA, ECDSA, ECC 等
上面俩种都可以通过密文得到明文
3. 摘要算法 是指把任意⻓度的输⼊消息数据转化为固定⻓度的输出数据的⼀种密码算法. 摘要算法是不可逆的(无法解密), 也就是⽆法解密. 通常⽤来检验数据的完整性的重要技术, 即对数据进⾏哈希计算然后⽐较摘要值, 判断是否⼀致(如果一致,数据就是一致的). 常⻅的摘要算法有: MD5, SHA系列(SHA1, SHA2等), CRC(CRC8, CRC16,CRC32)
加密思路
博客系统中, 我们采⽤MD5算法来进⾏加密.
同样的明文,通过MD5进行加密得到的密文都是一样的(不论什么语言,什么网站)
问题: 虽然经过MD5加密后的密⽂⽆法解密, 但由于相同的密码经过MD5哈希之后的密⽂是相同的, 当存储⽤⼾密码的数据库泄露后, 攻击者会很容易便能找到相同密码的⽤⼾, 从⽽降低了破解密码的难度. 因此, 在对用户密码进⾏加密时,需要考虑对密码进⾏包装, 即使是相同的密码, 也保存为不同的密⽂. 即使⽤⼾输⼊的是弱密码, 也考虑进⾏增强, 从⽽增加密码被攻破的难度.
那么如何把简单的密码改成复杂的密码?
1. 加上复杂的字符串, md5(明文+ 复杂字符串)
2. 加上随机的复杂字符串,md5(明文+随机复杂字符串)
解决⽅案: 采⽤为⼀个密码拼接⼀个随机字符来进⾏加密, 这个随机字符我们称之为"盐". 假如有⼀个加盐后的加密串,⿊客通过⼀定⼿段这个加密串, 他拿到的明⽂并不是我们加密前的字符串, ⽽是加密前的字符串和盐组合的字符串, 这样相对来说⼜增加了字符串的安全性.
加密过程图

解密流程: MD5是不可逆的, 通常采⽤"判断哈希值是否⼀致"来判断密码是否正确.
如用户输⼊的密码, 和盐值⼀起拼接后的字符串经过加密算法, 得到的密⽂相同, 我们就认为密码正确(密⽂相同, 盐值相同, 推测明⽂相同)
解密过程图

整体流程:
因为 明文 + 盐值 = 密文 ,当我们的盐值和密文一致的时候=>我们的明文是一样的
当我们输入明文的时候,我们拿一个盐值通过加密算法得到密文,和我们数据库里面的密码加上同样的盐值得到的密文是一样的时候,我们就能够验证我们输入的明文就是一样的.(这个就是验证用户密码是否正确的过程)

结论: 需要存储的信息: 密文+盐值

我们先学习怎么使用

此时提出一个问题,我们该怎么存储盐值?
我们把盐值和密文存储在一起存,盐值和我们的密文一起进行拼接(怎么拼接看我们自己)
我们此时就这么实现: 盐值+密文=>存到数据库里面
数据库存储 盐值 + 密文
盐值 + md5(明文+盐值)

总体流程

具体的实现:
package org.xiaobai.blog_system.utils;import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.util.UUID;public class SecurityUtils {//加密//password用户注册的时候输入的密码//return: 数据库中存储的信息: 盐值 + md5(明文+盐值)public static String encrypt(String password){//生成随机盐值String salt = UUID.randomUUID().toString().replace("-","");//对 明文 + 盐值 进行MD5加密 =>(明文+盐值)String finalPassword = DigestUtils.md5DigestAsHex((password+salt).getBytes());//返回信息return salt + finalPassword;}//验证密码是否正确//inputPassword用户登录的时候需要确认的密码//sqlPassword 数据库中password字段存储的信息 盐值 + md5(明文+盐值)public static boolean verify(String inputPassword,String sqlPassword){if(!StringUtils.hasLength(inputPassword)){return false;}if(sqlPassword == null || sqlPassword.length() != 64){return false;}//获取盐值String salt = sqlPassword.substring(0,32);//根据用户输入的密码和盐值,进行加密 md5(明文+盐值)和上面对应String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());return (salt + finalPassword).equals(sqlPassword);}public static void main(String[] args) {System.out.println(encrypt("123456"));System.out.println(verify("123456", "de32ecf4414243229b11e1b9370d3a530c96c03766fae52a469cf658eb94b9d1"));System.out.println(verify("123457", "de32ecf4414243229b11e1b9370d3a530c96c03766fae52a469cf658eb94b9d1"));}}
测试结果

我们引入到项目中去使用

修改⼀下数据库密码
使⽤测试类给密码123456⽣成密⽂,
然后把数据库的密码改成我们生成的密文.

最后进行部署,把项目搞到云服务器里面(这个后面我们学习了Linux之后再部署)
相关文章:
博客系统完整开发流程
前言 通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使用, 并完成了图书管理系统的常规功能开发, 接下来我们系统的从0到1完成⼀个项⽬的开发. 企业开发的流程 1. 需求评审(产品经理(PM)会和运营(想口号),UI,测试,开发等沟通) ,会涉及到背景/目标/怎么做,可能会有多…...
【C语言】指针笔试题
前言:上期我们介绍了sizeof与strlen的辨析以及sizeof,strlen相关的一些笔试题,这期我们主要来讲指针运算相关的一些笔试题,以此来巩固我们之前所学的指针运算! 文章目录 一,指针笔试题1,题目一…...
大数据开发平台的框架
根据你的需求,以下是从 GitHub 推荐的 10 个可以实现大数据开发平台的项目: 1. Apache Spark Apache Spark 是一个开源的分布式计算框架,适用于大规模数据处理和分析。它提供了强大的数据处理能力,支持实时数据处理、机器学习和…...
【Python爬虫(53)】从入门到精通:Scrapy Spider开发全攻略
【Python爬虫】专栏简介:本专栏是 Python 爬虫领域的集大成之作,共 100 章节。从 Python 基础语法、爬虫入门知识讲起,深入探讨反爬虫、多线程、分布式等进阶技术。以大量实例为支撑,覆盖网页、图片、音频等各类数据爬取ÿ…...
《Keras 3 : 使用迁移学习进行关键点检测》:此文为AI自动翻译
《Keras 3 :使用迁移学习进行关键点检测》 作者:Sayak Paul,由 Muhammad Anas Raza 转换为 Keras 3 创建日期:2021/05/02 最后修改时间:2023/07/19 描述:使用数据增强和迁移学习训练关键点检测器。 (i) 此示例使用 Keras 3 在 Colab 中查看 GitHub 源 关键点检测包…...
CentOS停服后的替代选择:openEuler、Rocky Linux及其他系统的未来展望
CentOS停服后的替代选择:openEuler、Rocky Linux及其他系统的未来展望 引言CentOS停服的背景华为openEuler:面向未来的开源操作系统1. 简介2. 特点3. 发展趋势 Rocky Linux:CentOS的精神继承者1. 简介2. 特点3. 发展趋势 其他可选的替代系统1…...
【Qt】桌面应用开发 ------ 绘图事件和绘图设备 文件操作
文章目录 9、绘图事件和绘图设备9.1 QPainter9.2 手动触发绘图事件9.3 绘图设备9.3.1 QPixmap9.3.2 QImage9.3.3 QImage与QPixmap的区别9.3.4 QPicture 10、文件操作10.1 文件读写10.2 二进制文件读写10.3 文本文件读写10.4 综合案例 9、绘图事件和绘图设备 什么时候画&#x…...
python与C系列语言的差异总结(3)
与其他大部分编程语言不一样,Python使用空白符(whitespace)和缩进来标识代码块。也就是说,循环体、else条件从句之类的构成,都是由空白符加上冒号(:)来确定的。大部分编程语言都是使用某种大括号来标识代码块的。下面的…...
OpenCV(9):视频处理
1 介绍 视频是由一系列连续的图像帧组成的,每一帧都是一幅静态图像。视频处理的核心就是对这些图像帧进行处理。常见的视频处理任务包括视频读取、视频播放、视频保存、视频帧处理等。 视频分析: 通过视频处理技术,可以分析视频中的运动、目标、事件等。…...
【C++设计模式】观察者模式(1/2):从基础到优化实现
1. 引言 在 C++ 软件与设计系列课程中,观察者模式是一个重要的设计模式。本系列课程旨在深入探讨该模式的实现与优化。在之前的课程里,我们已对观察者模式有了初步认识,本次将在前两次课程的基础上,进一步深入研究,着重解决观察者生命周期问题,提升代码的安全性、灵活性…...
2025年华为手机解锁BL的方法
注:本文是我用老机型测试的,新机型可能不适用 背景 华为官方已经在2018年关闭了申请BL解锁码的通道,所以华为手机已经无法通过官方获取解锁码。最近翻出了一部家里的老手机华为畅玩5X,想着能不能刷个系统玩玩,但是卡…...
在 CentOS 7.9上部署 Oracle 11.2.0.4.0 数据库
目录 在 CentOS 7.9上部署 Oracle 11.2.0.4.0 数据库引言安装常见问题vim粘贴问题 环境情况环境信息安装包下载 初始环境准备关闭 SELinux关闭 firewalld 安装前初始化工作配置主机名安装依赖优化内核参数限制 Oracle 用户的 Shell 权限配置 PAM 模块配置swap创建用户组与用户,…...
idea里的插件spring boot helper 如何使用,有哪些强大的功能,该如何去习惯性的运用这些功能
文章精选推荐 1 JetBrains Ai assistant 编程工具让你的工作效率翻倍 2 Extra Icons:JetBrains IDE的图标增强神器 3 IDEA插件推荐-SequenceDiagram,自动生成时序图 4 BashSupport Pro 这个ides插件主要是用来干嘛的 ? 5 IDEA必装的插件&…...
Docker 搭建 Redis 数据库
Docker 搭建 Redis 数据库 前言一、准备工作二、创建 Redis 容器的目录结构三、启动 Redis 容器1. 通过 redis.conf 配置文件设置密码2. 通过 Docker 命令中的 requirepass 参数设置密码 四、Host 网络模式与 Port 映射模式五、检查 Redis 容器状态六、访问 Redis 服务总结 前言…...
JAVAweb之过滤器,监听器
文章目录 过滤器认识生命周期FilterConfigFilterChain过滤器执行顺序应用场景代码 监听器认识ServletContextListenerHttpSessionListenerServletRequestListener代码 过滤器 认识 Java web三大组件之一,与Servlet相似。过滤器是用来拦截请求的,而非处…...
计算机毕业设计SpringBoot+Vue.js足球青训俱乐部管理系统(源码+文档+PPT+讲解)
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
基于 DeepSeek LLM 本地知识库搭建开源方案(AnythingLLM、Cherry、Ragflow、Dify)认知
写在前面 博文内容涉及 基于 Deepseek LLM 的本地知识库搭建使用 ollama 部署 Deepseek-R1 LLM知识库能力通过 Ragflow、Dify 、AnythingLLM、Cherry 提供理解不足小伙伴帮忙指正 😃,生活加油 我站在人潮中央,思考这日日重复的生活。我突然想,…...
QSplashScreen --软件启动前的交互
目录 QSplashScreen 类介绍 使用方式 项目中使用 THPrinterSplashScreen头文件 THPrinterSplashScreen实现代码 使用代码 使用效果 QSplashScreen 类介绍 QSplashScreen 是 Qt 中的一个类,用于显示启动画面。它通常在应用程序启动时显示,以向用户显…...
「软件设计模式」责任链模式(Chain of Responsibility)
深入解析责任链模式:用C打造灵活的请求处理链 引言:当审批流程遇上设计模式 在软件系统中,我们经常会遇到这样的场景:一个请求需要经过多个处理节点的判断,每个节点都有权决定是否处理或传递请求。就像企业的请假审批…...
蓝桥杯嵌入式客观题以及解释
第十一届省赛(大学组) 1.稳压二极管时利用PN节的反向击穿特性制作而成 2.STM32嵌套向量终端控制器NVIC具有可编程的优先等级 16 个 3.一个功能简单但是需要频繁调用的函数,比较适用内联函数 4.模拟/数字转换器的分辨率可以通过输出二进制…...
你对WebAssembly的看法是什么?
WebAssembly(Wasm)是一种新兴的技术,旨在通过提供一种新的低级字节码格式来提高 Web 应用程序的性能和效率。它与 JavaScript 互补,使得开发者可以将其他编程语言(如 C、C、Rust 等)编译为高效的字节码&…...
Qt在Linux嵌入式开发过程中复杂界面滑动时卡顿掉帧问题分析及解决方案
Qt在Linux嵌入式设备开发过程中,由于配置较低,加上没有GPU,我们有时候会遇到有些组件比较多的复杂界面,在滑动时会出现掉帧或卡顿的问题。要讲明白这个问题还得从CPU和GPU的分工说起。 一、硬件层面核心问题根源剖析 CPU&#x…...
vscode 版本
vscode官网 Visual Studio Code - Code Editing. Redefined 但是官网只提供最新 在之前的版本就要去github找了 https://github.com/microsoft/vscode/releases 获取旧版本vscode安装包的方法_vscode 老版本-CSDN博客...
low rank decomposition如何用于矩阵的分解
1. 什么是矩阵分解和低秩分解 矩阵分解是将一个矩阵表示为若干结构更简单或具有特定性质的矩阵的组合或乘积的过程。低秩分解(Low Rank Decomposition)是其中一种方法,旨在将原矩阵近似为两个或多个秩较低的矩阵的乘积,从而降低复…...
C# string转unicode字符
在 C# 中,将字符串转换为 Unicode 字符(即每个字符的 Unicode 码点)可以通过遍历字符串中的每个字符并获取其 Unicode 值来实现。Unicode 值是一个整数,表示字符在 Unicode 标准中的唯一编号。 以下是实现方法: 1. 获…...
51单片机-串口通信编程
串行口工作之前,应对其进行初始化,主要是设置产生波特率的定时器1、串行口控制盒中断控制。具体步骤如下: 确定T1的工作方式(编程TMOD寄存器)计算T1的初值,装载TH1\TL1启动T1(编程TCON中的TR1位…...
Fisher信息矩阵与Hessian矩阵:区别与联系全解析
Fisher信息矩阵与Hessian矩阵:区别与联系全解析 在统计学和机器学习中,Fisher信息矩阵(FIM)和Hessian矩阵是两个经常出现的概念,它们都与“二阶信息”有关,常用来描述函数的曲率或参数的敏感性。你可能听说…...
有哪些开源大数据处理项目使用了大模型
以下是一些使用了大模型的开源大数据处理项目: 1. **RedPajama**:这是一个开源项目,使用了LLM大语言模型数据处理组件,对GitHub代码数据进行清洗和处理。具体流程包括数据清洗、过滤低质量样本、识别和删除重复样本等步骤。 2. …...
ubuntu离线安装Ollama并部署Llama3.1 70B INT4
文章目录 1.下载Ollama2. 下载安装Ollama的安装命令文件install.sh3.安装并验证Ollama4.下载所需要的大模型文件4.1 加载.GGUF文件(推荐、更容易)4.2 加载.Safetensors文件(不建议使用) 5.配置大模型文件 参考: 1、 如…...
机器学习数学通关指南——泰勒公式
前言 本文隶属于专栏《机器学习数学通关指南》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和参考文献请见《机器学习数学通关指南》 正文 一句话总结 泰勒公式是用多…...




