项目:博客系统——基于SSM框架Mybatis-plus
基于SSM框架实现的一个博客系统,使用Mybatis-plus进行操作数据库
功能描述
首先进入界面,是一个登录的界面,在输入用户名和密码后登录成功,进入博客列表的页面,在博客列表有查看博客的功能,写博客的功能,注销账户的功能,点击查看博客的功能后,也有编辑博客,删除博客的功能。
功能展示
登录界面
列表页面
写作界面
查看博客界面
编辑博客界面
接下来开始介绍我的项目
项目介绍
数据库的创建
一共有两张表
blog_info表 是博客列表内容的数据库
user_info表 是新增用户的信息
创建的项目
添加Spring MVC和Mybatis-plus的依赖
注:我们这个项目使用的是Mybatis-plus,所以创建好项目记得添加其依赖
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency>
创建项目的结构目录
配置配置文件
由于我们后面需要将项目上传到服务器中,所以配置三个配置文件
包括:
application.yml(主配置文件):用来存放通常的配置
记得在pom文件中进行配置
<profiles><profile><id>dev</id><properties><profile.name>dev</profile.name></properties></profile><profile><id>prog</id><properties><profile.name>prog</profile.name></properties></profile></profiles>
application-dev.yml(开发环境的配置文件):用来存放开放的时候需要的配置文件
application-prod.yml(生产环境的配置文件):用来存放生产的时候需要的配置文件
项目是采用三层架构
分为controller(控制层),service(服务层),mapper(持久层)
首先我们先写common(公共层)的代码
公共层一般是用于存储工具类,配置类
首先定义业务状态的枚举
@AllArgsConstructor:自动生成带参构造器
@Getter
@AllArgsConstructor
public enum ResultCodeEnum {SUCCESS(200,"操作成功"),FAIL(-1,"操作失败");private int code;private String mes;}
统一返回结果的封装类
定义了code(业务状态码),errMes(错误信息),data(返回的数据),其中包含三种方法
success方法,再输入成功返回的数据对象后,返回其数据成功的状态码和"操作成功这个信息"
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {private int code;//业务状态码private String errMes;private Object data;public static Result success(Object data){Result result=new Result();result.setData(data);result.setErrMes("操作成功");result.setCode(ResultCodeEnum.SUCCESS.getCode());return result;}public static Result fail(String errMes,Object data){Result result=new Result();result.setData(data);result.setErrMes(errMes);result.setCode(ResultCodeEnum.FAIL.getCode());return result;}public static Result fail(String errMes){Result result=new Result();result.setErrMes(errMes);result.setCode(ResultCodeEnum.FAIL.getCode());return result;}
}
最后就是统一返回结果
@RestControllerAdvice是全局的数据处理和异常处理,作用在@RestController注解下的方法返回的数据
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;//对所有的controller方法执行,因为已经设置为true@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}//在controller返回数据后,在执行这个方法后返回响应体@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//如果返回了String类型,进行封装if(body instanceof String){return objectMapper.writeValueAsString(Result.success(body));}//如果直接返回了Result类型,直接返回,避免重复的包装if(body instanceof Result){return body;}return Result.success(body);}
}
定义项目异常BlogException
自定义来处理关于博客业务会出现的异常
@Data
public class BlogException extends RuntimeException{private String errMsg;private int code;public BlogException(String errMsg) {this.errMsg=errMsg;}public BlogException(String errMsg,int code) {this.code=code;this.errMsg=errMsg;}
}
统一异常处理
@ExceptionHandler 用于定义处理某种异常的方法,方法签名
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {@ExceptionHandler(Exception.class)public Result exceptionHandler(Exception e){
// log.error("Exception e:"+e);return Result.fail(e.getMessage());}@ExceptionHandler(BlogException.class)public Result exceptionHandler(BlogException e){
// log.error("Exception e:"+e);return Result.fail(e.getErrMsg());}@ExceptionHandler(HandlerMethodValidationException.class)public Result exceptionHandler(HandlerMethodValidationException e){return Result.fail(e.getMessage());}
}
业务代码
实体类
记得对照数据库进行创建
博客列表
@Data
public class BlogInfo {@TableId(value = "id",type = IdType.AUTO) //设置id为主键,并且自增private Integer id;private String title;private String content;private Integer userId;private Integer deleteFlag;private LocalDateTime createTime;private LocalDateTime updateTime;}
用户列表
@Data
public class UserInfo {@TableId(value = "id",type = IdType.AUTO) //设置id为主键,并且自增private Integer id;private String userName;private String password;private String githubUrl;private Byte deleteFlag;private LocalDateTime createTime;private LocalDateTime updateTime;}
持久层 (Mapper)
我们使用的Mybatis-plus完成持久层的开发,创建mapper,记得实现BaseMapper
@Mapper
public interface BlogInfoMapper extends BaseMapper<BlogInfo>{
}
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
实现博客列表
约定前后端的交互接口
客户端给服务端发送了/blog/getList请求,服务端给客户端返回了Json格式的数据
定义接口返回的实体
@JsonFormat注解,用于控制其中日期的格式
@Data
public class BlogInfoResponse {private Integer id;private String title;private String content;private Integer userId;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime updateTime;
}
实现图书列表的Controller类
@Slf4j
@RestController
@RequestMapping("/blog")
public class BlogInfoController {@Resource(name = "blogInfoServiceImpl")private BlogInfoService blogInfoService;@RequestMapping("/getList")public List<BlogInfoResponse> getList(){return blogInfoService.getList();}
}
Service采⽤接⼝对外提供服务,实现类⽤Impl的后缀与接⼝区别,好处是让代码更加灵活,起到了解耦的作用
图书列表的service类
public interface BlogInfoService {List<BlogInfoResponse> getList();
}
@Service
@Slf4j
public class BlogInfoServiceImpl implements BlogInfoService {@Autowiredprivate BlogInfoMapper blogInfoMapper;public List<BlogInfoResponse> getList() {List<BlogInfo> blogInfos=blogInfoMapper.selectList( //mybatis-plus的查询方法,用户查询满足以下条件的所有记录new LambdaQueryWrapper<BlogInfo>() //创建一个条件构造器.eq(BlogInfo::getDeleteFlag, Constants.FLAG_NORMAL).orderByDesc(BlogInfo::getId)); //.eq:条件是//orderByDesc是将得到的id按desc(降序)排序//转化格式将BlogInfo格式转化为BlogInfoResponsereturn blogInfos.stream().map(blogInfo ->{ //.stream()将得到的博客列表变成一个数据流, .map对每一个数据进行转换BlogInfoResponse response=new BlogInfoResponse(); //创建一个新的对象BeanUtils.copyProperties(blogInfo,response); //将blogInfo中的数据复制到response中,return response; // 这里的blogInfo指的是List<BlogInfo> blogInfos中的每一个数据}).collect(Collectors.toList()); //最后再将将 Stream 流结果收集成 List}
}
实现客户端的代码
使⽤ajax给服务器发送HTTP请求
function getBlogList() {$.ajax({type: "get",url: "/blog/getList",success: function (result) {//针对后端的结果, 进行简单校验if (result.code == 200 && result.data != null && result.data.length > 0) {var finalHtml = "";for (var blogInfo of result.data) {finalHtml += '<div class="blog">';finalHtml += '<div class="title">' + blogInfo.title + '</div>';finalHtml += '<div class="date">' + blogInfo.createTime + '</div>';finalHtml += '<div class="desc">' + blogInfo.content + '</div>';finalHtml += '<a class="detail" href="blog_detail.html?blogId=' + blogInfo.id + '">查看全文>></a>';finalHtml += '</div>'}$(".container .right").html(finalHtml);}}});}
验证是否能正确的返回数据
http:127.0.0.1:8080/blog/getList
返回结果正确
实现博客详情
约定好前后端交互的接口
BlogController中写getBlogDetail方法,用于获得博客的详情
@RequestMapping("/getBlogDetail")public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){log.info("blogId:{}",blogId);return blogInfoService.getBlogDetail(blogId);}
记得添加参数校验的依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
BlogInfoservice中添加getBlogInfo和getBlogDetail方法
将博客的详情转换为响应对象的原因:是因为数据库的实体类中很多的属性不适合直接给前端,所以定义了一些需要响应的数据,返回给前端
BlogInfoResponse getBlogDetail(Integer blogId);BlogInfo getBlogInfo(Integer blogId);
@Overridepublic BlogInfoResponse getBlogDetail(Integer blogId) {return BeanTranUtils.tran(getBlogInfo(blogId));}@Overridepublic BlogInfo getBlogInfo(Integer blogId) {QueryWrapper<BlogInfo> queryWrapper=new QueryWrapper<>();queryWrapper.lambda().eq(BlogInfo::getDeleteFlag, Constants.FLAG_NORMAL).eq(BlogInfo::getId, blogId);return blogInfoMapper.selectOne(queryWrapper);}
实现客户端的代码
使用location.search可得到blogId
getBlogDetail();function getBlogDetail() {$.ajax({type: "get",url: "/blog/getBlogDetail" + location.search,success: function (result) {if (result.code == -1) {alert(result.errMsg);return;}if (result.code == 200 && result.data != null) {$(".content .title").text(result.data.title);$(".content .date").text(result.data.createTime);$(".content .detail").text(result.data.content);}}});}
进行验证
返回结果正确
实现登录
使用令牌技术
用户登录进行登录,发送登录请求,经过负载均衡,将请求发给了一台服务器,服务器对其进行账户和密码的验证,验证成功后,生成一个令牌,返回给客户端,客户端收到令牌,将其储存起来,比如说储存在cookie中,用户登录成功后,进行其他功能的方法,此时请求将会发给另一台机器,机器验证其携带的令牌是否有效,如果有效,则说明用户登录成功,如果无效或者为空,则说明用户还未登录。
JWT令牌⽣成和校验
引入JWT的依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><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>
生成令牌
首先生成一个密钥
@Testpublic void genKey(){Key key= Keys.secretKeyFor(SignatureAlgorithm.HS256);String encode= Encoders.BASE64.encode(key.getEncoded());System.out.println(encode);}
生成JWT
我们可以在Https://jwt.io中验证生成的JWT是否正确
约定博客详情的前后端交互的接口
创建JWT⼯具类
public class JwtUtils {private static final Logger log = LoggerFactory.getLogger(JwtUtils.class);private static String SECRET_STRING="2FDVor3CaEMpuG4vQ7T6BACe31F7cqPt09OSXEPQlGc=";private static Key key=Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_STRING));public static String genJwt(Map<String,Object> chaims){String jpt=Jwts.builder().setClaims(chaims).signWith(key).compact();return jpt;}public static Claims parseToken(String token){if(!StringUtils.hasLength(token)){return null;}JwtParser build=Jwts.parserBuilder().setSigningKey(key).build();Claims claims=null;try{claims=build.parseClaimsJws(token).getBody();}catch (Exception e){log.error("token解析失败"+token);}return claims;}
}
创建请求的实体类
@Data
public class UserLoginRequest {@NotNull(message = "用户名不能为空")@Length(max =20)private String userName;@NotNull(message = "密码不能为空")@Length(min = 2,max =20 )private String password;}
创建响应的实体类
@AllArgsConstructor
@Data
public class UserLoginResponse {private Integer UserId;private String token;
}
实现Controller
@RequestBody注解的作用是返回的是Json对象
@Validated是用于触发参数校验的机制
因为在请求的实体类中使用了@NotNull注解和@Length注解
@Slf4j
@RestController
@RequestMapping("/user")
public class UserInfoController {@Resource(name = "userInfoServiceImpl")private UserInfoService userInfoService;@RequestMapping("/login")public UserLoginResponse userLogin(@RequestBody @Validated UserLoginRequest userLoginRequest){log.info("用户登录 用户:"+userLoginRequest.getUserName());return userInfoService.userLogin(userLoginRequest);}
}
实现service
public interface UserInfoService {UserLoginResponse userLogin(UserLoginRequest userLoginRequest);
}
@Service
@Slf4j
public class UserInfoServiceImpl implements UserInfoService {@Autowiredprivate UserInfoMapper userInfoMapper;@Resource(name = "blogInfoServiceImpl")private BlogInfoService blogInfoService;@Overridepublic UserLoginResponse userLogin(UserLoginRequest userLoginRequest) {if(userLoginRequest==null){log.error("请求为空");throw new BlogException("请求为空");}QueryWrapper<UserInfo> queryWrapper=new QueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getUserName,userLoginRequest.getUserName()).eq(UserInfo::getDeleteFlag, Constants.FLAG_NORMAL);UserInfo userInfo=userInfoMapper.selectOne(queryWrapper);if(userInfo==null){throw new BlogException("用户不存在");}if(!SecurityUtils.verify(userLoginRequest.getPassword(),userInfo.getPassword())){throw new BlogException("请输入正确的密码");}Map<String, Object> map=new HashMap<>();map.put("userId",userInfo.getId());map.put("userName",userInfo.getUserName());String token= JwtUtils.genJwt(map);return new UserLoginResponse(userInfo.getId(),token);}
实现客户端的代码
function login() {$.ajax({type:"post",url:"/user/login",contentType: "application/json",data:JSON.stringify({userName:$("#username").val(),password:$("#password").val()}),success:function(result){if(result!=null && result.code == 200){var response=result.data;if(!response.token || !response.userId){alert("登录响应数据不完整,请联系管理员");return;}localStorage.setItem("user_token",response.token);localStorage.setItem("loginUserId",response.userId);location.assign("blog_list.html")}else{alert("用户名或密码错误");return ;}}});}
ajax发⽣异常时,进⾏异常处理,公共处理,可以提取到 common.js
$(document).ajaxError(function(event,xhr,options,exc){if(xhr.status==400){alert("参数校验失败");}});
实现强制要求登录
添加拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String userToken=request.getHeader(Constants.USER_TOKEN_HEADER_KEY);if(userToken==null){response.setStatus(401);return false;}Claims claims= JwtUtils.parseToken(userToken);if(claims==null){response.setStatus(401);return false;}return true;}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/user/**","/blog/**").excludePathPatterns("/user/login");}
}
实现客户端的代码
前端请求时,header中统⼀添加token,后端返回异常时,跳转到登录⻚⾯
$(document).ajaxError(function(event,xhr,options,exc){if(xhr.status==401){location.assign("blog_login.html");}else if(xhr.status==400){alert("参数识别错误");}
});$(document).ajaxSend(function(e,xhr,opt){var user_token=localStorage.getItem("user_token");//从浏览器中的本地存储中得到令牌tokenxhr.setRequestHeader("user_token",user_token);//为即将发送的对象添加一个自定义请求头。
});
实现显示用户信息
我们希望这个信息是根据用户登录的变化而变化
约定前后端交互接⼝
定义返回的实体类
@Data
public class UserInfoResponse {private Integer id;private String userName;private String githubUrl;
}
实现controller
@RequestMapping("/getUserInfo")public UserInfoResponse getUserInfo(@NotNull(message = "userId不能为空") Integer userId){return userInfoService.getUserInfo(userId);}@RequestMapping("/getAuthorInfo")public UserInfoResponse getBlogInfo(@NotNull(message = "blogId不能为空") Integer blogId){return userInfoService.getAuthorId(blogId);}
实现service
UserInfoResponse getUserInfo(Integer userId);UserInfoResponse getAuthorId(Integer blogId);
首先根据博客Id获得博客的信息,然后根据博客信息获得用户Id,最后根据用户的Id获得用户信息
@Overridepublic UserInfoResponse getUserInfo(Integer userId) {QueryWrapper<UserInfo> queryWrapper=new QueryWrapper<>();queryWrapper.lambda().eq(UserInfo::getDeleteFlag,Constants.FLAG_NORMAL).eq(UserInfo::getId,userId);UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);return BeanTranUtils.tran(userInfo);}@Overridepublic UserInfoResponse getAuthorId(Integer blogId) {//根据博客Id获得博客信息BlogInfo blogInfo = blogInfoService.getBlogInfo(blogId);if(blogInfo==null || blogInfo.getUserId()<=0){throw new BlogException("获取博客信息失败");}//根据博客信息获得作者信息return getUserInfo(blogInfo.getUserId());}
实现客户端的代码
由于在列表页面和详情页面都需要得到用户信息
所以我们将用户信息的代码提取common.js,直接在blog_list和blog_detail调用getUserInfo就可以
function getUserInfo(url){$.ajax({type:"get",url:url,success:function(result){if(result!=null && result.data!=null && result.code==200 ){var userInfo=result.data;$(".left .card h3").text(userInfo.userName);$(".left .card a").attr("href",userInfo.githubUrl);}}});
}
引入common.js
<script src="js/common.js"></script>
进行调用
实现用户退出
只需要前端清除token就行,返回登录界面
注:因为也是好几个界面都可以进行用户退出,所以把这个代码也提取到common.js
function logout(){localStorage.removeItem("user_token");localStorage.removeItem("loginUserId");location.assign("blog_login.html");}
实现博客的发布
约定好前后端交互的接口
定义请求的实体类
@Data
public class AddBlogRequest {@NotNull(message = "userId不能为空")private Integer userId;@NotBlank(message = "标题不能为空")private String title;@NotBlank(message = "内容不能为空")private String content;}
实现controller
@RequestMapping("addBlog")public Boolean addBlog(@RequestBody @Validated AddBlogRequest addBlogRequest){return blogInfoService.addBlog(addBlogRequest);}
实现service
Boolean addBlog(AddBlogRequest addBlogRequest);
@Overridepublic Boolean addBlog(AddBlogRequest addBlogRequest) {BlogInfo blogInfo=new BlogInfo();BeanUtils.copyProperties(addBlogRequest,blogInfo);try {int result=blogInfoMapper.insert(blogInfo);if(result==1){return true;}return false;}catch (Exception e){log.error("发布错误,请联系管理员");throw new BlogException("发布错误,请联系管理员");}}
editor.md的介绍
是一个开源的⻚⾯markdown编辑器组件
editor.md的使用
实现客户端的代码
$(function () {var editor = editormd("editor", {width: "100%",height: "550px",path: "blog-editormd/lib/"});}); function submit() {$.ajax({type:"post",url:"/blog/addBlog",contentType:"application/json",data:JSON.stringify({userId:localStorage.getItem("loginUserId"),title:$("#title").val(),content:$("#content").val()}),success: function(result){if(result!=null && result.code==200 && result.data==true){location.assign("blog_list.html");}else{alert("发布失败");}}});}
实现删除/编辑博客
编辑博客
约定前后端交互的代码
实现修改博客的实体类
@Data
public class UpdateBlogRequest {@NotNull(message = "id不能为空")private Integer id;private String title;private String content;
}
实现controller
@RequestMapping("/update")public Boolean updateBlog(@RequestBody @Validated UpdateBlogRequest updateBlogRequest){return blogInfoService.updateBlog(updateBlogRequest);}
实现service
Boolean updateBlog(UpdateBlogRequest updateBlogRequest);
@Overridepublic Boolean updateBlog(UpdateBlogRequest updateBlogRequest) {BlogInfo blogInfo=BeanTranUtils.tran(updateBlogRequest);try {int result = blogInfoMapper.updateById(blogInfo);return result==1;}catch (Exception e){log.error("编辑失败");throw new BlogException("内部错误,请联系管理员");}}
实现客户端代码
editormd.markdownToHTML("detail", {markdown: result.data.content,});
//判断是否显示编辑/删除按钮let loginUserId = localStorage.getItem("loginUserId");if(result.data.userId==loginUserId){//当前作者是登录用户, 显示按钮let blogId = result.data.id;let finalHtml = '<button onclick="window.location.href=\'blog_update.html?blogId='+blogId+'\'">编辑</button>';finalHtml += '<button onclick="deleteBlog('+blogId+')">删除</button>';console.log(finalHtml);$(".content .operating").html(finalHtml);
删除博客
约定前后端交互的代码
实现controller
@RequestMapping("/delete")public Boolean deleteBlog(Integer blogId){return blogInfoService.deleteBlog(blogId);}
实现service
删除一篇博客,并不是真正的从数据库中删除,而是将其delete_flag 修改为1
Boolean deleteBlog(Integer id);
@Overridepublic Boolean deleteBlog(Integer blogId) {BlogInfo blogInfo=new BlogInfo();blogInfo.setId(blogId);blogInfo.setDeleteFlag(Constants.FLAG_DELETE);try {int result = blogInfoMapper.updateById(blogInfo);return result==1;}catch (Exception e){log.error("删除失败");throw new BlogException("内部错误,请联系管理员");}}
实现客户端代码
function deleteBlog(blogId) {$.ajax({type: "post",url: "/blog/delete?blogId=" + blogId,success: function(result){if(result.code ==200 && result.data==true){//删除成功location.href = "blog_list.html";}else{alert("删除失败");}}});alert("删除博客");}
加密/加盐
目前我们的密码还是明文显示的,为了保护我们的密码,所以要对其加密
密码算法主要分为三类:对称密码算法,非对称密码算法,摘要算法
在博客系统中,我们采用MD5进行加密
采用密码拼接一个随机的字符进行加密,随机的字符我们称之为“盐”
加密的原理
检验的原理
加密工具类
public class SecurityUtils {public static String encrypt(String inputPassword){String salt= UUID.randomUUID().toString().replace("-","");String encryptPassword= DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes(StandardCharsets.UTF_8));return salt+encryptPassword;}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);String encryptPassword=DigestUtils.md5DigestAsHex((salt+inputPassword).getBytes(StandardCharsets.UTF_8));String result=salt+encryptPassword;return sqlPassword.equals(result);}}
注:记得将数据库中的密码改为加密后的密码
自此项目结束,大家可以把这个项目部署到云服务器上
上传服务记得勾选prod,然后刷新Maven
希望能对大家有所帮助!!!!