个人博客系统(SSM版 前端+后端)

前言

        在学习Servlet的时候,也写了一个博客系统,主要的就是使用servelet加Tomcat进行实现的,而这个项目 仅仅适合去学习Web项目开发的思想,并不满足当下企业使用框架的思想,进行学习过Spring,Spring Boot,Spring MVC以及MyBatis之后,我们就可以对之前的项目使用SSM框架的形式进行升级,然后对相应的功能进行升级,帮助我们对企业的开发流程进行了解,文章最后会有本项目的Gitee地址,欢迎访问!!💕💕💕


 

下图对此项目对照之前的版本进行了项目升级的总结.

 那话不多说啦,开始我们的新项目.


 

目录

前言

​编辑

1. 前期项目准备

1.1 创建项目

1.2 准备项目

a. 删除项目中无用的文件和目录

b. 引入前端页面     

c. 初始化数据库

d. 项目分层

2. 添加统一的返回格式

3. 配置登录拦截器

4. 实现用户的注册功能

4.1 编写前端Ajax请求代码 

4.2 编写后端代码

a. 用户信息类UserInfo.class

b. Mapper层(UserMapper)

c. MyBatis的xml文件的构造

d. 服务层Service

e. Controller层

4.3 测试功能

5. 实现用户的登录功能

5.1 编写前端Ajax请求代码 

5.2 编写后端代码

a. Mapper层(UserMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

5.3 测试功能 

6. 个人主页展示功能

6.1 左侧个人信息

6.1.1 编写前端Ajax请求代码 

6.1.2 编写后端代码

a. Mapper层(UserMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.1.3 测试功能 

6.2 右侧个人博客列表信息 

6.2.1 编写前端Ajax请求代码 

6.2.2 编写后端代码

a. Mapper层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.3 删除博客功能 

6.3.1 编写前端Ajax请求代码 

6.3.2 编写后端代码

a. Mapper 层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.3.3 测试功能

 6.4 查看全文功能 

6.4.1 编写前端Ajax请求代码 

6.4.2 编写后端代码

a. Mapper层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.4.3 测试功能

6.5 修改博客功能 

6.5.1 编写前端Ajax请求代码 

6.5.2 编写后端代码

a. Mapper层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.5.3 测试功能

6.6 发布博客功能

6.6.1 编写前端Ajax请求代码 

6.6.2 编写后端代码

a. Mapper层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.6.3 功能测试

6.7 注销用户功能

6.7.1 编写前端Ajax请求代码 

6.7.2 编写后端代码

a. Mapper层(ArticleMapper)

b. MyBatis的xml文件的构造

c. service层

d. Controller层

6.7.3 功能测试

7. 博客主页功能实现

7.1 主页的导航栏

7.1.1 编写前端Ajax请求代码 

7.1.2 编写后端代码

7.2 分页功能  

8. 密码使用加盐的算法进行加密

9. Session持久化到Redis

结语


1. 前期项目准备

1.1 创建项目

第一步 : 创建SSM项目 

 

 第二步: 添加项目依赖

 

1.2 准备项目

a. 删除项目中无用的文件和目录

b. 引入前端页面     

(这里就不打算总结前端页面设计的具体代码了,我们把重点放在后端是实现以及前端与后端交互.)(具体的前端代码,我会将此代码放在项目路径下面,然后大家根据下面的步骤进行下载就可以了).

前端代码在项目中路径,大家将有关于前端的代码放在resources/static下面就可以了

c. 初始化数据库

我使用的MySQL版本是5.7版本,下面是建库建表的sql

-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;-- 使⽤数据数据
use mycnblog;-- 创建表[⽤户表]
drop table if exists userinfo;
create table userinfo(id int primary key auto_increment,username varchar(100) not null,password varchar(32) not null,photo varchar(500) default '',createtime timestamp default current_timestamp,updatetime timestamp default current_timestamp,`state` int default 1
) default charset 'utf8mb4';alter table userinfo modify password varchar(65);-- 创建⽂章表
drop table if exists articleinfo;
create table articleinfo(id int primary key auto_increment,title varchar(100) not null,content text not null,createtime datetime default CURRENT_TIMESTAMP,updatetime datetime default CURRENT_TIMESTAMP,uid int not null,rcount int not null default 1,`state` int default 1
)default charset 'utf8mb4';-- 创建视频表
drop table if exists videoinfo;
create table videoinfo(vid int primary key,`title` varchar(250),`url` varchar(1000),createtime timestamp default current_timestamp,updatetime timestamp default current_timestamp,uid int
)default charset 'utf8mb4';
alter table userinfo add unique(username);
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`,`createtime`, `updatetime`, `state`) 
VALUES(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);-- ⽂章添加测试数据
insert into articleinfo(title,content,createtime,updatetime,uid) values('Java','Java正文','2021-12-06 17:10:48', '2021-12-06 17:10:48',1);
insert into videoinfo(vid,title,url,uid) values(1,'java title','http://www.baidu.com',1);
insert into articleinfo(title,content,uid) values('Java','Java正文',1);

d. 项目分层

我们按照统一的格式,将项目进行分层操作

 

e. 项目的配置文件application.yml

# 配置数据库的连接字符串
spring:
#  jackson:
#    date-format: yyyy-MM-dd HH:mm:ss      # 设置时间的格式
#    time-zone: GMT+8               # 设置地域datasource:url: jdbc:mysql://127.0.0.1/mycnblog?characterEncoding=utf8username: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Driver
server:port: 8800
# 设置 Mybatis 的 xml 保存路径
mybatis:mapper-locations: classpath:mapper/*Mapper.xmlconfiguration: # 配置打印 MyBatis 执行的 SQLlog-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:level:com:example:demo: debug

至此,我们项目的前期准备就完成了,接下来我们将一一进行实现功能.

2. 添加统一的返回格式

        为了与前端更方便更有效率的进行交互,我们可以规定好统一的返回格式给前端,使用了SpringAOP的思想,在项目的common文件夹中创建一个类为AjaxResult.class.我们返回给前端的数据格式为Json格式的数据,主要包括了状态码,状态码描述信息,以及返回的数据.具体代码如下.

具体分为两个方法,一个是访问成功,一个是失败.然后对这两个方法进行重载.

package com.example.demo.common;import lombok.Data;
import org.springframework.stereotype.Component;import java.io.Serializable;/*** Created with IntelliJ IDEA.* Description:普通的对象,统一的返回格式* User: YAO* Date: 2023-07-17* Time: 19:17*/
@Data
public class AjaxResult implements Serializable {// 1.状态码private Integer code;// 2.状态码描述信息private String msg;// 3.返回的数据private Object data;/*** 1. 操作成功返回的结果*/public static AjaxResult success(Object data){AjaxResult result = new AjaxResult();result.setCode(200);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(int code,Object data){AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg("");result.setData(data);return result;}public static AjaxResult success(String msg,int code,Object data){AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}/*** 2. 返回失败结果*/public static AjaxResult fail(int code,String msg){AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(null);return result;}public static AjaxResult fail(String msg,int code,Object data){AjaxResult result = new AjaxResult();result.setCode(code);result.setMsg(msg);result.setData(data);return result;}
}

为了保证最后的返回格式的正确性,我们最后对数据的返回的格式进行验证.在config文件目录下创建返回格式的保底类:ResponseAdvice,对特殊的String类型进行处理. 

package com.example.demo.config;import com.example.demo.common.AjaxResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
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.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** Created with IntelliJ IDEA.* Description:统一返回格式,最保底的返回数据的格式*    说明:在返回数据之前,检测数据的类型是否为统一的对象,如果不是,封装成统一的数据格式* User: YAO* Date: 2023-07-17* Time: 19:28*/
@ControllerAdvice // 统一返回格式
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;/*** 只有返回为true的时候,才会调用supports方法* @param returnType* @param converterType* @return*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}/*** 对数据格式进行校验和封装* @param body* @param returnType* @param selectedContentType* @param selectedConverterType* @param request* @param response* @return*/@SneakyThrows   // 抛出异常@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if (body instanceof AjaxResult) {return body;}if (body instanceof String) {// String 比较特殊需要单独处理// 使用Jackson封装成成功返回的对象的格式return objectMapper.writeValueAsString(AjaxResult.success(body));}return AjaxResult.success(body);}
}

3. 配置登录拦截器

我们对有些系统功能进行捕获用户的登录状态,实现必须登录之后才能进行使用功能.在config的文件目录下创建LoginInterceptor.class.实现HandlerInterceptor接口,重写preHandle方法.获取当前登录的用户的SESSION,判断是否为空,为空就重定向到登录页面,然后将自定义好的登录拦截器进行配置到项目中(添加注解@Configuration,实现接口WebMvcConfigurer,重写addInterceptors方法)具体代码如下:

1.创建全局变量的 USER_SESSION_KEY(SESSION的key值)

package com.example.demo.common;/*** Created with IntelliJ IDEA.* Description:全局变量* User: YAO* Date: 2023-07-18* Time: 10:58*/
public class AppVariable {// 全局变量,session的key值public static final String USER_SESSION_KEY = "USER_SESSION_KEY";// 所有用户的key值是一样的但是内容是不一样的.就相当于大家都用的是钥匙但是钥匙的类型是不一样的.// 这里只是一个统一的格式,或者是同一种规格
}

2. 自定义拦截器 

package com.example.demo.config;import com.example.demo.common.AppVariable;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;/*** Created with IntelliJ IDEA.* Description:自定义登录功能呢的拦截器* User: YAO* Date: 2023-07-18* Time: 11:02*/
public class LoginInterceptor implements HandlerInterceptor {/*** true --> 用户已经登录* false --> 用户未登录,跳转到登录页* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppVariable.USER_SESSION_KEY) != null){// 用户已经登录return true;}// 用户未登录,重定向到登录界面response.sendRedirect("/login.html");return false;}
}

3. 将自定义拦截器加入到项目的配置中.(具体的拦截界面,大家先不要全部添加,可以随着项目的功能的实现,对指定的页面和路由进行拦截.)

package com.example.demo.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** Created with IntelliJ IDEA.* Description:项目的配置文件* User: YAO* Date: 2023-07-18* Time: 11:07*/
@Configuration
public class AppConfig implements WebMvcConfigurer {/*** 1. 配置自定义的拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/css/**").excludePathPatterns("/img/**").excludePathPatterns("/js/**").excludePathPatterns("/editor.md/**").excludePathPatterns("/login.html").excludePathPatterns("/blog_content.html").excludePathPatterns("/blog_list.html").excludePathPatterns("/reg.html").excludePathPatterns("/user/login").excludePathPatterns("/user/reg").excludePathPatterns("/art/detail").excludePathPatterns("/user/getuserbyid").excludePathPatterns("/art/incr-rcount").excludePathPatterns("/art/add").excludePathPatterns("/art/listbypage").excludePathPatterns("/user/loginstate");}
}

4. 实现用户的注册功能

此处我们就用到了Servlet的和前端交互的思想了.以下所有的功能实现就是下面这两步:

  • 前端向用户发送Ajax请求
  • 后端接收请求,进行响应 

4.1 编写前端Ajax请求代码 

reg.html文件中进行补充代码

我们对这个提交的按钮进行构造请求 

<script>// 1.提交注册时间(jQuery进行操作)function mysub(){// 1.1 非空校验var username = jQuery("#username");var password = jQuery("#password");var password2 = jQuery("#password2");if(username.val() == ""){alert("请输入用户名");username.focus(); // 将鼠标光标设置到指定位置return;}if(password.val() == ""){alert("请输入密码");password.focus();return;}if(password2.val() == ""){alert("请输入确认密码");password2.focus();return;}// 1.2 判断两次密码是否一致if(password.val() != password2.val()){alert("两次输入的密码不一致请检查");password.focus();return;}// 1.3 Ajax提交请求jQuery.ajax({url:"/user/reg",type:"post",data:{"username":username.val(),"password":password.val()},// 回调函数,请求成功得到响应.success:function(result){// 判断响应的结果,状态码200 data:修改返回值1if(result != null && result.code == 200 && result.data == 1){// 执行成功 if(confirm("注册成功!是否跳转登录页面?")){location.href="/login.html";}}else{alert("执行失败,请稍后再试");}}})}</script>

 具体的注册页面

4.2 编写后端代码

因为要对数据库中的用户表进行操作,所以后段要对用户表进行创建对象.

a. 用户信息类UserInfo.class

package com.example.demo.entity;import lombok.Data;import java.io.Serializable;
import java.time.LocalDateTime;/*** Created with IntelliJ IDEA.* Description:用户表对象* User: YAO* Date: 2023-07-18* Time: 9:24*/
@Data
public class UserInfo implements Serializable {private Integer id;private String username;private String password;private String photo;private LocalDateTime createtime;private LocalDateTime updatetime;private Integer state;
}

b. Mapper层(UserMapper)

@Mapper
public interface UserMapper {/*** 1.注册功能*/int reg(UserInfo userInfo);
}

c. MyBatis的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="com.example.demo.mapper.ArticleMapper"><select id="getArtCountByUid" resultType="java.lang.Integer">select count(*) from articleinfo where uid=#{uid}</select></mapper>

d. 服务层Service

@Service
public class UserService {@Autowiredprivate UserMapper userMapper;/*** 1. 注册功能*/public int reg(UserInfo userInfo){return userMapper.reg(userInfo);}
}

e. Controller层

@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate ArticleService articleService;@RequestMapping("/reg")public AjaxResult reg(UserInfo userInfo){// 1. 进行非空校验和参数的有效性if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername())||!StringUtils.hasLength(userInfo.getPassword())){return AjaxResult.fail(-1,"非法参数");}// 2.密码加密String olderPassword = userInfo.getPassword();// 加密userInfo.setPassword(PasswordUtils.encrypt(olderPassword));// 2.返回成功的修改值return AjaxResult.success(userService.reg(userInfo));}
}

后端代码完成

4.3 测试功能

注册小白用户 

数据库用户展示(密码是经过加盐加密的,后面会讲到这一点)

我们的注册功能就结束了.

5. 实现用户的登录功能

         用户登录功能与注册功能逻辑差不多,都是先进行前端 提交ajax数据请求给后端,在进行效验,其中后端进行数据库查询判断用户名或者密码是否存在且正确。

5.1 编写前端Ajax请求代码 

 对提交按钮进行方法的实现

<script>function mysub(){// 1. 非空校验var username = jQuery("#username");var password = jQuery("#password");if(username.val() == ""){alert("请输入用户名!");username.focus();return;}if(password.val() == ""){alert("请输入密码!");password.focus();return;}// 2. 构造ajax请求jQuery.ajax({url:"/user/login",type:"post",data:{"username":username.val(),"password":password.val()},success: function(result){if(result != null && result.code == 200 && result.data != null){// 执行成功 location.href="/myblog_list.html";}else{alert("用户名或密码错误,请重新登录!");}}})}</script>

5.2 编写后端代码

a. Mapper 层(UserMapper)

    /*** 2.登录功能*/UserInfo getUserByName(@Param("username") String username);

b. MyBatis的xml文件的构造

<!--    2. 登录功能--><select id="getUserByName" resultType="com.example.demo.entity.UserInfo">select * from userinfo where username=#{username}</select>

c. service层

    /*** 2. 根据名字查询用户信息*/public UserInfo getUserByName(String username){return userMapper.getUserByName(username);}

d. Controller层

(其中代码加入了密码加盐加密的实现,这个具体后面再将)

@RequestMapping("/login")public AjaxResult login(HttpServletRequest request,String username, String password){System.out.println(username + " " + password);// 1. 进行非空校验和参数的有效性if (!StringUtils.hasLength(username)|| !StringUtils.hasLength(password)){return AjaxResult.fail(-1,"非法参数");}// 2.查询数据库UserInfo userInfo = userService.getUserByName(username);if (userInfo != null && userInfo.getId() > 0){// 使用加盐的验证if (PasswordUtils.check(password,userInfo.getPassword())){// 登陆成功// 将用户存储带到Session中HttpSession session = request.getSession();session.setAttribute(AppVariable.USER_SESSION_KEY,userInfo);// 返回之前将密码等信息进行隐藏userInfo.setPassword("");return AjaxResult.success(userInfo);}}return AjaxResult.success(0,null);}

5.3 测试功能 

 跳转到登录页

6. 个人主页展示功能

6.1 左侧个人信息

6.1.1 编写前端Ajax请求代码 

myblog_list.html文件进行补充

这里仅仅对名字和文章数量进行动态展示,其他的标签就没实现,.但是实现的原理都是一样的,后续会做扩展.

<script>function showInfo(){jQuery.ajax({url:"/user/showinfo",type:"get",data:{},success: function(result){if(result != null && result.code==200){jQuery("#username").text(result.data.username);jQuery("#artCount").text(result.data.artCount);}else{{alert("当前用户信息加载失败")}}} })}showInfo();
</script>

6.1.2 编写后端代码

因为要进行获取当前登录用户的信息,所以我们得实现一个得到当前用户的方法

我们在common文件目录下创建一个类UserSessionUtils实现用户登录的相关操作

实现方法getSessionUser()

package com.example.demo.common;import com.example.demo.entity.UserInfo;
import com.example.demo.entity.vo.UserInfoVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;/*** Created with IntelliJ IDEA.* Description:当前登录用户相关的操作* User: YAO* Date: 2023-07-18* Time: 14:28*/
public class UserSessionUtils {/*** 1. 得到当前的登录用户* @param request* @return*/public static UserInfo getSessionUser(HttpServletRequest request){HttpSession session = request.getSession(false);if (session != null && session.getAttribute(AppVariable.USER_SESSION_KEY) != null){// 当前用户已经正常登录return (UserInfo) session.getAttribute(AppVariable.USER_SESSION_KEY);}return null;}
}

a. Mapper 层(UserMapper)

/*** 3.根据id返回用户信息*/UserInfo getUserById(@Param("id") Integer id);

b. MyBatis的xml文件的构造

<select id="getUserById" resultType="com.example.demo.entity.UserInfo">select * from userinfo where id=#{id}</select>

c. service层

/*** 3.根据id返回用户信息*/public UserInfo getUserById(Integer id){return userMapper.getUserById(id);}

d. Controller层

@RequestMapping("/showinfo")public AjaxResult showinfo(HttpServletRequest request){UserInfoVo userInfoVo = new UserInfoVo();// 1. 得到当前登录用户UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null){return AjaxResult.fail(-1,"非法请求");}// 将获得对象赋值给UserinfoVoBeanUtils.copyProperties(userInfo,userInfoVo);// Spring 提供的深拷贝的方法// 2. 得到用户发表文章的总数userInfoVo.setArtCount(articleService.getArtCountByUid(userInfo.getId()));return AjaxResult.success(userInfoVo);}

6.1.3 测试功能 

6.2 右侧个人博客列表信息 

6.2.1 编写前端Ajax请求代码 

myblog_list.html文件进行补充

 <script>function showInfo(){jQuery.ajax({url:"/user/showinfo",type:"get",data:{},success: function(result){if(result != null && result.code==200){jQuery("#username").text(result.data.username);jQuery("#artCount").text(result.data.artCount);}else{{alert("当前用户信息加载失败")}}} })}showInfo();function getMyArtList(){jQuery.ajax({url:"/art/mylist",type:"POST",data:{},success:function(result){if(result!=null && result.code==200){// 有两种情况,一种是发表了文章,一种是没有发表任何文章 if(result.data!=null && result.data.length>0){// 此用户发表文章了var artListDiv ="";for(var i=0;i<result.data.length;i++){var artItem = result.data[i];artListDiv += '<div class="blog">';artListDiv += '<div class="title">'+artItem.title+'</div>';artListDiv += '<div class="date">'+artItem.updatetime+'</div>';artListDiv += '<div class="desc">';artListDiv += artItem.content;artListDiv += '</div>';artListDiv += '<a href="blog_content.html?id='+artItem.id + '" class="detail">查看全文 &gt;&gt;</a>&nbsp;&nbsp;';artListDiv += '<a href="blog_edit.html?id='+artItem.id + '" class="detail">修改 &gt;&gt;</a>&nbsp;&nbsp;';  artListDiv += '<a href="javascript:myDel('+artItem.id+');" class="detail">删除 &gt;&gt;</a>'; artListDiv += '</div>';    }jQuery("#artDiv").html(artListDiv);}}else{alert("查询文章列表出错,请重试!");}}});}getMyArtList();
</script>

6.2.2 编写后端代码

文章信息表实体对象

import java.time.LocalDateTime;
import java.util.Date;/*** Created with IntelliJ IDEA.* Description:文章表* User: YAO* Date: 2023-07-18* Time: 13:52*/
@Data
public class ArticleInfo {private Integer id;private String title;private String content;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")private LocalDateTime createtime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")private LocalDateTime updatetime;private Integer uid;private Integer rcount;private Integer state;
}

a. Mapper 层(ArticleMapper)

/*** 2.根据uid查询对应用户的文章列表*/List<ArticleInfo> getMyList(@Param("uid")Integer uid);

b. MyBatis的xml文件的构造

<select id="getMyList" resultType="com.example.demo.entity.ArticleInfo">select * from articleinfo where uid=#{uid}</select>

c. service层

    public List<ArticleInfo> getMyList(Integer uid){return articleMapper.getMyList(uid);}

d. Controller层

@RestController
@RequestMapping("/art")
public class ArticleController {@Autowiredprivate ArticleService articleService;@RequestMapping("/mylist")public AjaxResult getMyList(HttpServletRequest request){UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null){return AjaxResult.fail(-1,"非法请求");}List<ArticleInfo> list = articleService.getMyList(userInfo.getId());for (ArticleInfo articleInfo : list) {if (articleInfo.getContent().length() > 100){// 当文章的长度大于100字的时候,不完全进行展示articleInfo.setContent(articleInfo.getContent().substring(0,100) + "...");}}return AjaxResult.success(list);}
}

6.3 删除博客功能 

 

6.3.1 编写前端Ajax请求代码 

myblog_list.html文件进行补充

// 删除文章function myDel(id){if(confirm("确实删除?")){// 删除文章jQuery.ajax({url:"art/del",type:"get",data:{"id":id},success:function(result){if(result!=null && result.code==200 && result.data==1){alert("删除成功!");// 刷新当前页面location.href = location.href;}else{alert("抱歉:删除失败,请重试!");}}});}}

6.3.2 编写后端代码

a. Mapper 层(ArticleMapper)

    /*** 3.根据id进行删除文章*/int del(@Param("id") Integer id,@Param("uid") Integer uid);

b. MyBatis的xml文件的构造

<delete id="del">delete from articleinfo where id=#{id} and uid=#{uid}</delete>

c. service层

public Integer del(Integer id,Integer uid){return articleMapper.del(id,uid);}

d. Controller层

@RequestMapping("/del")public AjaxResult del(HttpServletRequest request,Integer id){if (id == null || id <= 0){return AjaxResult.fail(-1,"参数异常");}UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null){return AjaxResult.fail(-2,"用户未登录");}return AjaxResult.success(articleService.del(id,userInfo.getId()));}

6.3.3 测试功能

 6.4 查看全文功能 

6.4.1 编写前端Ajax请求代码 

blog-content.html文件进行补充

        查看全文的功能,要对文章的内容进行展示,以及文章的作者,发布信息,以及文章阅读量进行展示.需要根据url中的id进行获取指定博客的id,我们需要对id的提取进行设置方法,因为后面的修改文章也需要获取文章id,以及主页的列表也需要,所以将此方法写入到公共的js方法中,此处需要注意的操作就是,每次查看的时候就要给文章的阅读量进行加1操作.

common.js 

function getUrlValue(key){// 一开始获取的字符串为?id=2&v=1var params = location.search;if(params.length>1){//?id=2&v=1 去除这个问号params = location.search.substring(1);   // 从第二个字符开始取出var paramArr = params.split("&");   // 差分成key-value的数组for(var i=0;i<paramArr.length;i++){var kv = paramArr[i].split("=");if(kv[0] = key){return kv[1];}} }return "";
}
<script type="text/javascript">var editormd;function initEdit(md){editormd = editormd.markdownToHTML("editorDiv", {markdown : md, // Also, you can dynamic set Markdown text// htmlDecode : true,  // Enable / disable HTML tag encode.// htmlDecode : "style,script,iframe",  // Note: If enabled, you should filter some dangerous HTML tags for website security.});}// 获取当前url查询字符串参数的公共方法function getArtDetail(id){if(id==""){alert("非法参数!");return;}jQuery.ajax({url:"art/detail",type:"POST",data:{"id":id},success:function(result){if(result!=null && result.code==200){jQuery("#title").html(result.data.title);jQuery("#updatetime").html(result.data.updatetime);jQuery("#rcount").html(result.data.rcount);initEdit(result.data.content);// 得到用户 idshowUser(result.data.uid);}else{alert("查询失败,请重试!");  }}});}getArtDetail(getUrlValue("id"));// 查询用户的详情信息function showUser(id){jQuery.ajax({url:"/user/getuserbyid",type:"POST",data:{"id":id},success:function(result){if(result!=null && result.code==200 && result.data.id>0){jQuery("#username").text(result.data.username);jQuery("#artCount").text(result.data.artCount);}else{alert("抱歉:查询用户信息失败,请重试!");}}});}// 阅读量 +1function updataRCount(){// 先得到文章 idvar id = getUrlValue("id");if(id!=""){jQuery.ajax({url:"/art/incr-rcount",type:"POST",data:{"id":id},success:function(result){}});}}updataRCount();</script> 

6.4.2 编写后端代码

a. Mapper 层(ArticleMapper)

    /*** 3.根据id获取文章的具体内容*/ArticleInfo getDetail(@Param("id")Integer id);/*** 4.文章阅读量进行加1*/int incrRCount(@Param("id") Integer id);

b. MyBatis的xml文件的构造

<select id="getDetail" resultType="com.example.demo.entity.ArticleInfo">select * from articleinfo where id=#{id}</select><update id="incrRCount">update articleinfo set rcount=rcount+1 where id=#{id};</update>

c. service层

public int getArtCountByUid(Integer uid){return articleMapper.getArtCountByUid(uid);}public ArticleInfo getDetail(Integer id){return articleMapper.getDetail(id);}

d. Controller层

@RequestMapping("/incr-rcount")public AjaxResult incrRCount(Integer id){if (id == null || id <= 0){return AjaxResult.fail(-1,"参数异常");}return AjaxResult.success(articleService.incrRCount(id));}@RequestMapping("/detail")public AjaxResult getDetail(HttpServletRequest request,Integer id){if (id == null || id <= 0){return AjaxResult.fail(-1,"参数异常");}return AjaxResult.success(articleService.getDetail(id));}

6.4.3 测试功能

6.5 修改博客功能 

6.5.1 编写前端Ajax请求代码 

blog_edit.html文件进行补充

修改功能就是点击跳转到编辑页面,首先发送请求得到当前文章的内容,再进行修改提交发送Ajax请求

function mysub(){// 1.非空效验var title = jQuery("#title");if(title.val()==""){alert("请先输入标题!");title.focus();return;}if(editor.getValue()==""){alert("请先输入正文!");return;}// 2.进行修改操作if(confirm("确定修改文章?")){jQuery.ajax({url:"/art/update",type:"POST",data:{"id":id,"title":title.val(),"content":editor.getValue()},success:function(result){if(result!=null && result.code==200 && result.data==1){alert("修改成功!");location.href = "myblog_list.html";}else{alert("抱歉:操作失败,请重试!");}}});}  }// 文章初始化function initArt(){// 得到当前页面 url 中的参数 id(文章id)id = getUrlValue("id");if(id==""){alert("无效参数");location.href = "myblog_list.html";return;}// 请求后端,查询文章的详情信息jQuery.ajax({url:"art/detail",type:"POST",data:{"id":id},success:function(result){if(result!=null && result.code==200){jQuery("#title").val(result.data.title);initEdit(result.data.content);}else{alert("查询失败,请重试!");  }}});}initArt();

6.5.2 编写后端代码

a. Mapper 层(ArticleMapper)

/*** 3.根据id获取文章的具体内容*/ArticleInfo getDetail(@Param("id")Integer id);/*** 6. 修改文章* @param articleInfo* @return*/int update(ArticleInfo articleInfo);

b. MyBatis的xml文件的构造

<select id="getDetail" resultType="com.example.demo.entity.ArticleInfo">select * from articleinfo where id=#{id}</select><update id="update">update articleinfo set title=#{title},content=#{content},updatetime=#{updatetime}where id=#{id} and uid=#{uid}</update>

c. service层

    public ArticleInfo getDetail(Integer id){return articleMapper.getDetail(id);}public int update(ArticleInfo articleInfo){return articleMapper.update(articleInfo);}

d. Controller层

@RequestMapping("/detail")public AjaxResult getDetail(HttpServletRequest request,Integer id){if (id == null || id <= 0){return AjaxResult.fail(-1,"参数异常");}return AjaxResult.success(articleService.getDetail(id));}@RequestMapping("/update")public AjaxResult update(HttpServletRequest request, ArticleInfo articleInfo){if (articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())|| !StringUtils.hasLength(articleInfo.getContent()) || articleInfo.getId()==null){return AjaxResult.fail(-1,"非法参数");}UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null || userInfo.getId() == null){return AjaxResult.fail(-2,"无效用户");}articleInfo.setUid(userInfo.getId());articleInfo.setUpdatetime(LocalDateTime.now());return AjaxResult.success(articleService.update(articleInfo));}

6.5.3 测试功能

6.6 发布博客功能

6.6.1 编写前端Ajax请求代码 

blog_add.html文件进行补充

<script>var editor;function initEdit(md){// 编辑器设置editor = editormd("editorDiv", {// 这里的尺寸必须在这里设置. 设置样式会被 editormd 自动覆盖掉. width: "100%",// 高度 100% 意思是和父元素一样高. 要在父元素的基础上去掉标题编辑区的高度height: "calc(100% - 50px)",// 编辑器中的初始内容markdown: md,// 指定 editor.md 依赖的插件路径path: "editor.md/lib/",saveHTMLToTextarea: true // });}initEdit("# 在这里写下一篇博客"); // 初始化编译器的值// 提交function mysub(){if(confirm("确认提交?")){// 1.非空效验var title = jQuery("#title");if(title.val()==""){alert("请先输入标题!");title.focus();return;}if(editor.getValue()==""){alert("请先输入文章内容!");return;}// 2.请求后端进行博客添加操作jQuery.ajax({url:"/art/add",type:"POST",data:{"title":title.val(),"content":editor.getValue()},success:function(result){if(result!=null && result.code==200 && result.data==1){if(confirm("文章添加成功!是否继续添加文章?")){// 刷新当前页面location.href = location.href;}else{location.href = "/myblog_list.html";}}else{alert("抱歉,文章添加失败,请重试!");}}});}}</script>

6.6.2 编写后端代码

a. Mapper 层(ArticleMapper)

/*** 5. 添加文章* @param articleInfo* @return*/int add(ArticleInfo articleInfo);

b. MyBatis的xml文件的构造

<insert id="add">insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})</insert>

c. service层

public int add(ArticleInfo articleInfo){return articleMapper.add(articleInfo);}

d. Controller层

@RequestMapping("/add")public AjaxResult add(HttpServletRequest request, ArticleInfo articleInfo){// 1.进行非空校验if (articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())||  !StringUtils.hasLength(articleInfo.getContent())){return AjaxResult.fail(-1,"参数异常");}// 2.获取当前登录用户的id,将当前登录用户的id进行设置到传入sql的文章信息的对象,尽心添加操作的时候就会准确的直到这是当前登录用户记性发布的UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null || userInfo.getId() <= 0){return AjaxResult.fail(-2,"无效的登录用户");}articleInfo.setUid(userInfo.getId());int result = articleService.add(articleInfo);return AjaxResult.success(result);}

6.6.3 功能测试

 

6.7 注销用户功能

        我们要给检测到登录状态的页面加注销功能, 如个人主页,写博客,主页.因为是公共操作,所以直接写成一个公共的js,然后在需要的页面直接引入就可以.

6.7.1 编写前端Ajax请求代码 

common.js

// 公共操作 -- 注销登录
function logout(){if(confirm("确定注销??")){// 注销登录jQuery.ajax({url:"user/logout",type:"get",data:{},success:function(result){if(result!=null && result.code==200){location.href = "/login.html";}}});}
}

6.7.2 编写后端代码

a. Mapper 层(ArticleMapper)

/*** 5. 添加文章* @param articleInfo* @return*/int add(ArticleInfo articleInfo);

b. MyBatis的xml文件的构造

<insert id="add">insert into articleinfo(title,content,uid) values(#{title},#{content},#{uid})</insert>

c. service层

public int add(ArticleInfo articleInfo){return articleMapper.add(articleInfo);}

d. Controller层

@RequestMapping("/add")public AjaxResult add(HttpServletRequest request, ArticleInfo articleInfo){// 1.进行非空校验if (articleInfo == null || !StringUtils.hasLength(articleInfo.getTitle())||  !StringUtils.hasLength(articleInfo.getContent())){return AjaxResult.fail(-1,"参数异常");}// 2.获取当前登录用户的id,将当前登录用户的id进行设置到传入sql的文章信息的对象,尽心添加操作的时候就会准确的直到这是当前登录用户记性发布的UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null || userInfo.getId() <= 0){return AjaxResult.fail(-2,"无效的登录用户");}articleInfo.setUid(userInfo.getId());int result = articleService.add(articleInfo);return AjaxResult.success(result);}

6.7.3 功能测试

 

7. 博客主页功能实现

7.1 主页的导航栏

这里需要注意的是,已经登录的用户的导航栏和未登录的有细微的区别

已经登录的主页是可以进行切换到个人主页的,未登录的是没有这个选项的.

 在进入主页上的时候发送请求的时候进行验证登录的状态,检测为登录,就给导航栏拼接个人主页的跳转按钮就可以了

7.1.1 编写前端Ajax请求代码 

在login.html中进行编写代码

 <script>// 根据登录的状态进行设置列表页的导航栏信息function loginstate(){jQuery.ajax({url:"/user/loginstate",type:"POST",data:{},success:function(result){var navText='<img src="img/logo2.jpg" alt=""><span class="title">我的博客系统</span><span class="spacer"></span>';if(result.code==200 && result!=null){navText += '<a href="myblog_list.html">个人主页</a><a href="blog_add.html">写博客</a><a href="javascript:logout()">注销</a>'jQuery("#nav").html(navText)}else{navText += '<a href="blog_add.html">写博客</a><a href="login.html">登录</a>'jQuery("#nav").html(navText)}}})}loginstate();</script>

7.1.2 编写后端代码

因为不用操作数据库,所以只编写Controller层就可以了

@RequestMapping("loginstate")public AjaxResult loginstate(HttpServletRequest request){UserInfo userInfo = UserSessionUtils.getSessionUser(request);if (userInfo == null || userInfo.getId()<=0){return AjaxResult.fail(-1,"当前用户未登录");}return AjaxResult.success(userInfo);}

7.2 分页功能  

实现分页功能,就要先获取总的文章数量,以及定义好每页显示多少条内容.具体总结为下面.

8. 密码使用加盐的算法进行加密

为了更形象的展示,我使用了别的画图软件

主要总结了三种加密方式\

  • a. 使用传统的MD5进行加密
  • b. 自己实现加盐算法进行加密
  • c. 使用Spring Security进行加密(Spring官方的)

本项目采用自己写的加盐算法:

package com.example.demo.common;import org.springframework.beans.BeanUtils;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.util.UUID;/*** Created with IntelliJ IDEA.* Description:关于密码的工具类* User: YAO* Date: 2023-07-19* Time: 13:49*/
public class PasswordUtils {/*** 1.加盐并生成密码* @param password 明文密码* @return 保存到数据库的密码*/public static String encrypt(String password){// 1. 32位盐值String salt = UUID.randomUUID().toString().replace("-","");// 2. 生成加盐之后的密码String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 3. 生成最终保存在数据库的密码   约定:32位盐值+$+32位加盐之后MD5加密之后的密码  -->65位String finalPassword = salt + "$" + saltPassword;return finalPassword;}/*** 2.生成用于验证的加盐密码* @param password  明文密码* @param salt  盐值* @return 验证的密码*/public static String encrypt(String password,String salt){// 1. 生成一个加盐之后的密码String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 2.生成最终的密码  约定:32位盐值+$+32位加盐之后MD5加密之后的密码  -->65位String finalPassword = salt + "$" + saltPassword;return finalPassword;}/*** 3.验证密码* @param inputPassword  用户输入的明文密码* @param finalPassword  数据库存储的最终的密码* @return*/public static Boolean check(String inputPassword, String finalPassword){if (StringUtils.hasLength(inputPassword) && StringUtils.hasLength(finalPassword)&& finalPassword.length() == 65){// 1.得到盐值String salt = finalPassword.split("\\$")[0];// 2.将用户输入的铭文密码进行加盐加密String finalInputPassword = encrypt(inputPassword,salt);// 3.进行比较return finalInputPassword.equals(finalPassword);}return false;}}

在注册的Controller以及登录的Controller可以看出相应的使用.

9. Session持久化到Redis

Redis的具体使用我会另外进行总结一个系列,这里直接将SSM项目Session持久化到Redis进行总结.

在我们的项目只需要配置好依赖文件配置文件,指定Session存储的类型为redis,指定好Session存储的路径以及仓库.

我们使用redis的可视化工具可以查看我们存储的Session.

结语

        写到这里,这个基础的SSM项目就算实现的差不多了,后续还可以扩展很多的功能,比如评论,草稿,登录验证码登功能,后续时间不紧的话我也会进行响应的更新.好啦,就到这里咯,谢谢!!!可以的话,给个赞,点个关注吧!!!😊😊😊

代码链接👇👇👇👇👇

SSM博客系统(前端+后端)icon-default.png?t=N6B9https://gitee.com/yao-fa/advanced-java-ee/tree/master/MyCnBlog-SSM


 

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

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

相关文章

react+redux异步操作数据

reactredux异步操作数据 redux中操作异步方法&#xff0c;主要是&#xff1a; 1、借助createAsyncThunk()封装异步方法&#xff1b;2、通过extraReducers处理异步方法触发后的具体逻辑&#xff0c;操作派生的state 1、异步操作的slice import { createSlice, createAsyncThunk…

uniapp 之 微信小程序、支付宝小程序 对于自定义导航栏的不同

目录 前言 微信小程序 代码 支付宝小程序 首页配置文件 二级菜单页面 配置 总结 不同 相同 前言 小程序都是 uni-app 写的 不是原生 微信小程序 代码 pages.json文件中配置 重点&#xff1a; "navigationStyle": "custom", // 导航栏样式…

ChatGPT开放自定义系统级别的指令,可设置偏好变成专属助理

OpenAI官方消息https://openai.com/blog/custom-instructions-for-chatgpt OpenAI为其大型语言模型接口ChatGPT引入了自定义指令&#xff0c;旨在为用户提供更加量身定制和个性化的体验&#xff0c;可以设置您的偏好&#xff0c;ChatGPT将在未来的所有对话中记住它们。 该功…

Python—数据结构(一)

先放一张自己学习和整理归纳的思维导图&#xff0c;以便让大家都知道我自己的整体学习路线。 数据结构的学习路上内容枯燥&#xff0c;但坚持下来一定有很大的收获&#xff01;加油&#x1f4aa;&#x1f3fb;&#xff01; 数据结构 数据的概念数据元素&#xff1a; 若干基本…

音视频开发-ffmpeg介绍-系列二

目录 一、FFmpeg核心结构体 二、解码流程 三、FFmpeg解码实现 四、FFmpeg编码实现 五、FFmpeg转码实现 一、FFmpeg核心结构体 AVFormatContext&#xff1a;解封装功能的结构体&#xff0c;包含文件名、音视频流、时长、比特率等信息&#xff1b; AVCodecContext&#xf…

【算法基础:数学知识】4.3 欧拉函数

文章目录 欧拉函数定义性质 例题列表873. 欧拉函数&#xff08;使用质因数分解求一个数的欧拉函数&#xff09;原理讲解&#xff08;公式推导&#xff09;⭐解法代码 874. 筛法求欧拉函数&#xff08;求 1 ~ n 中所有数字的欧拉函数&#xff09;⭐ 欧拉函数 https://oi-wiki.o…

[数据结构 -- 手撕排序算法第六篇] 递归实现快速排序(集霍尔版本,挖坑法,前后指针法为一篇的实现方法,很能打)

目录 1、常见的排序算法 1.1 交换排序基本思想 2、快速排序的实现方法 2.1 基本思想 3 hoare&#xff08;霍尔&#xff09;版本 3.1 实现思路 3.2 思路图解 3.3 为什么实现思路的步骤2、3不能交换 3.4 hoare版本代码实现 3.5 hoare版本代码测试 4、挖坑法 4.1 实现…

【手撕排序算法】---基数排序

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【数据结构初阶&#xff08;C实现&#xff09;】 我们直到一般的排序都是通过关键字的比较和移动这两种操作来进行排序的。 而今天介绍的…

​MySQL高阶语句(三)

目录 1、内连接 2、左连接 3、右连接&#xff1a; 二、存储过程⭐⭐⭐ 4. 调用存储过程 5.查看存储过程 5.1 查看存储过程 5.2查看指定存储过程信息 三. 存储过程的参数 3.1存储过程的参数 3.2修改存储过程 四.删除存储过程 MySQL 的连接查询&#xff0c;通常都是将来…

微服务一 实用篇 - 5.分布式搜索引擎(ElasticSearch基础)

《微服务一 实用篇 - 5.分布式搜索引擎&#xff08;ElasticSearch基础&#xff09;》 提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!! 《微服务一 实用篇 - 5.分布式搜索引擎&#xff08;ElasticSearch基础&#xff09;》 《微服务一 实用篇 - 5.分布式搜索…

【广州华锐互动】列车人员疏散VR虚拟演练系统

随着科技的不断发展&#xff0c;虚拟现实(VR)技术已经逐渐应用于各个领域。在火车站安全方面&#xff0c;为了提高旅客的安全意识和应对突发事件的能力&#xff0c;列车人员疏散VR虚拟演练系统应运而生。 列车人员疏散VR虚拟演练系统是一种基于虚拟现实技术的教育培训系统&…

【Vue】day03-VueCli(脚手架)

day03 一、今日目标 1.生命周期 生命周期介绍 生命周期的四个阶段 生命周期钩子 声明周期案例 2.综合案例-小黑记账清单 列表渲染 添加/删除 饼图渲染 3.工程化开发入门 工程化开发和脚手架 项目运行流程 组件化 组件注册 4.综合案例-小兔仙首页 拆分模块-局部…

SpringBoot 如何使用 MockMvc 进行 Web 集成测试

SpringBoot 如何使用 MockMvc 进行 Web 集成测试 介绍 SpringBoot 是一个流行的 Java Web 开发框架&#xff0c;它提供了一些强大的工具和库&#xff0c;使得开发 Web 应用程序变得更加容易。其中之一是 MockMvc&#xff0c;它提供了一种测试 SpringBoot Web 应用程序的方式&…

什么是神经网络?

我们常常使用深度学习来指训练神经网络的过程。 在这里举一个房屋价格预测的例子&#xff1a;假设有一个数据集&#xff0c;它包含了六栋房子的信息。所以&#xff0c;你知道房屋的面积是多少平方米&#xff0c;并且知道这个房屋的价格。这是&#xff0c;你想要拟合一个根据房屋…

【论文阅读】DQnet: Cross-Model Detail Querying for Camouflaged Object Detection

DQnet: Cross-Model Detail Querying for Camouflaged Object Detection DQnet&#xff1a;伪装目标检测中的跨模型细节查询 论文地址&#xff1a;https://arxiv.org/abs/2212.08296 这篇文章提出了一个交叉模型框架&#xff08;CNN-Transformer并行&#xff09;来检测伪装目…

【自启动配置】Ubuntu 设置开机自启动脚本

Ubuntu 开机运行的脚本和当前操作系统运行的级别有关&#xff0c;OS 的运行级别大概分为七个 目录 1、查看 OS 运行级别 2、创建自启动脚本 3、添加软链接 1、查看 OS 运行级别 输入命令 runlevel 查看当前系统运行级别。当前系统的运行级别为 5 2、创建自启动脚本 在 /et…

实训笔记7.19

实训笔记7.19 7.19一、座右铭二、Hadoop的HDFS分布式文件存储系统的相关原理性内容2.1 HDFS上传数据的流程2.2 HDFS下载数据的流程2.3 HDFS中NameNode和SecondaryNameNode工作机制&#xff08;涉及到HDFS的元数据管理操作&#xff09;2.4 HDFS中NameNode和DataNode的工作机制&a…

小程序自定义步骤条实现

效果展示&#xff1a; 支持背景颜色自定义 <view class"hl_steps"><view class"hl_steps_item" wx:for"{{steps}}" wx:key"id"><view class"hl_steps_item_circle_out" style"background-color: {{col…

【SpringCloud Alibaba】(二)微服务环境搭建

1. 项目流程搭建 整个项目主要分为 用户微服务、商品微服务和订单微服务&#xff0c;整个过程模拟的是用户下单扣减库存的操作。这里&#xff0c;为了简化整个流程&#xff0c;将商品的库存信息保存到了商品数据表&#xff0c;同时&#xff0c;使用商品微服务来扣减库存。小伙…

「苹果安卓」手机搜狗输入法怎么调整字体大小及键盘高度?

手机搜狗输入法怎么调整字体大小及键盘高度&#xff1f; 1、在手机上准备输入文字&#xff0c;调起使用的搜狗输入法手机键盘&#xff1b; 2、点击搜狗输入法键盘左侧的图标&#xff0c;进入更多功能管理&#xff1b; 3、在搜狗输入法更多功能管理内找到定制工具栏&#xff0c…