根据老师上课代码进行手敲与视屏中的一致
1、环境配置
1.1 数据库脚本
https://www.wolai.com/v5Kuct5ZtPeVBk4NBUGBWF
1.2 boot搭建
1、导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 数据库相关配置启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid启动器的依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.18</version>
</dependency>
<!-- 驱动类-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<!-- SpringBoot应用打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2、配置类
application.yml
# server配置
server:
port: 8080
servlet:
context-path: /
# 连接池配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql:///sm_db
username: root
password: pj123456
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis-plus的配置
mybatis-plus:
type-aliases-package: com.melon.pojo
global-config:
db-config:
logic-delete-field: isDeleted #全局逻辑删除
id-type: auto #主键策略自增长
table-prefix: news_ # 设置表的前缀
#jwt配置
jwt:
token:
tokenExpiration: 120 #有效时间,单位分钟
tokenSignKey: headline123456 #当前程序签名秘钥 自定义
3、启动类
- 配置启动类和mybatis-plus配置
package com.melon;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@MapperScan("com.melon.mapper")
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class,args);
}
//配置mybatis-plus插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //分页
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //乐观锁
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); //防全局修改和删除
return interceptor;
}
}
4、工具类
- 结果封装类
package com.melon.utils;
/**
* 全局统一返回结果类
*/
public class Result<T> {
// 返回码
private Integer code;
// 返回消息
private String message;
// 返回数据
private T data;
public Result(){}
// 返回数据
protected static <T> Result<T> build(T data) {
Result<T> result = new Result<T>();
if (data != null)
result.setData(data);
return result;
}
public static <T> Result<T> build(T body, Integer code, String message) {
Result<T> result = build(body);
result.setCode(code);
result.setMessage(message);
return result;
}
public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
Result<T> result = build(body);
result.setCode(resultCodeEnum.getCode());
result.setMessage(resultCodeEnum.getMessage());
return result;
}
/**
* 操作成功
* @param data baseCategory1List
* @param <T>
* @return
*/
public static<T> Result<T> ok(T data){
Result<T> result = build(data);
return build(data, ResultCodeEnum.SUCCESS);
}
public Result<T> message(String msg){
this.setMessage(msg);
return this;
}
public Result<T> code(Integer code){
this.setCode(code);
return this;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
package com.melon.utils;
/**
* 统一返回结果状态信息类
*
*/
public enum ResultCodeEnum {
SUCCESS(200,"success"),
USERNAME_ERROR(501,"usernameError"),
PASSWORD_ERROR(503,"passwordError"),
NOTLOGIN(504,"notLogin"),
USERNAME_USED(505,"userNameUsed");
private Integer code;
private String message;
private ResultCodeEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
- MD5加密工具类
package com.melon.utils;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@Component
public final class MD5Util {
public static String encrypt(String strSrc) {
try {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f' };
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}
- jwt验证类
- 使用jwt需要先引入依赖
- yml进行配置
#jwt配置
jwt:
token:
tokenExpiration: 120 #有效时间,单位分钟
tokenSignKey: headline123456 #当前程序签名秘钥 自定义
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
package com.melon.utils;
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "jwt.token")
public class JwtHelper {
private long tokenExpiration; //有效时间,单位毫秒 1000毫秒 == 1秒
private String tokenSignKey; //当前程序签名秘钥
//生成token字符串
public String createToken(Long userId) {
System.out.println("tokenExpiration = " + tokenExpiration);
System.out.println("tokenSignKey = " + tokenSignKey);
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration*1000*60)) //单位分钟
.claim("userId", userId)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取userid
public Long getUserId(String token) {
if(StringUtils.isEmpty(token)) return null;
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
//判断token是否有效
public boolean isExpiration(String token){
try {
boolean isExpire = Jwts.parser()
.setSigningKey(tokenSignKey)
.parseClaimsJws(token)
.getBody()
.getExpiration().before(new Date());
//没有过期,有效,返回false
return isExpire;
}catch(Exception e) {
//过期出现异常,返回true
return true;
}
}
}
2、后台功能开发
2.1 用户模块开发
1、用户登陆模块
1.controller
@PostMapping("login")
public Result login(@RequestBody User user){
Result result = userService.login(user);
return result;
}
2.service
// 登陆
Result login(User user);
3.serviceimpl
public Result login(User user) {
// 根据账号查询
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername,user.getUsername());
User loginUser = userMapper.selectOne(queryWrapper);
// 账号判断
if(loginUser == null){
// 账号错误
return Result.build(null, ResultCodeEnum.USERNAME_ERROR);
}
// 密码判断
if(!StringUtils.isEmpty(user.getUserPwd())
&& loginUser.getUserPwd().equals(MD5Util.encrypt(user.getUserPwd())))
{
// 账号密码正确
// 根据用户唯一标识生成token
String token = jwtHelper.createToken(Long.valueOf(loginUser.getUid()));
HashMap data = new HashMap();
data.put("token",token);
return Result.ok(data);
}
// 密码错误
return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
}
2、根据token获取用户信息
大致流程:
1.获取token,解析token对应的userId
2.根据userId,查询用户数据
3.将用户数据的密码置空,并且把用户数据封装到结果中key = loginUser
4.失败返回504 (本次先写到当前业务,后期提取到拦截器和全局异常处理器)
1.controller
@GetMapping("getUserInfo")
public Result userInfo(@RequestHeader String token){
Result result = userService.getUserInfo(token);
return result;
}
2.service
// 查询用户信息
Result getUserInfo(String token);
3.serviceimpl
public Result getUserInfo(String token) {
// 判定是否有效期
if(jwtHelper.isExpiration(token)){
//true过期,直接返回登陆
return Result.build(null,ResultCodeEnum.NOTLOGIN);
}
// 根据token获取用户对应的uid
int userId = jwtHelper.getUserId(token).intValue();
// 查询数据
User user = userMapper.selectById(userId);
if(user != null){
user.setUserPwd(null);
Map data = new HashMap();
data.put("loginUser",user);
return Result.ok(data);
}
return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
}
问题:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.melon.mapper.UserMapper.selectById
解决:
没有在对应的实体类主键上加@Tableid
@Data
public class User implements Serializable {
@TableId
private Integer uid;
3、检查账户是否可用
- 获取账户数据
- 根据账号进行数据库查询
- 结果封装
1.controller
@PostMapping("checkUserName")
public Result checkUserName(String username){
Result result = userService.checkUserName(username);
return result;
}
2.service
// 用户注册
Result regist(User user);
3.serviceimple
public Result checkUserName(String username) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername,username);
Long count = userMapper.selectCount(queryWrapper);
if (count==0) {
return Result.ok(null);
}
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
4、用户注册功能
- 将密码加密
- 将数据插入
- 判断结果, 成功 返回200 失败 505
@PostMapping("regist")
public Result regist(@RequestBody User user){
Result result = userService.regist(user);
return result;
}
// 用户注册
Result regist(User user);
public Result regist(User user) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername,user.getUsername());
Long count = userMapper.selectCount(queryWrapper);
if (count>0) {
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
user.setUserPwd(MD5Util.encrypt(user.getUserPwd()));
userMapper.insert(user);
return Result.ok(null);
}
2.2 首页模块开发
1、首页类别查询
@GetMapping("findAllTypes")
public Result findAllTypes(){
Result result = typeService.findAllTypes();
return result;
}
public Result findAllTypes() {
List<Type> types = typeMapper.selectList(null);
return Result.ok(types);
}
2、头条新闻分页查询
- 如果查询的条件并没有对应的实体类,一般创建一个vo类
- 分页查询:
- headlineMapper.selectPage(page,portalVo);传入的参数第一个是page,即要查询的分页对象,第二个是查询的条件或对象,可能包括关键字等信息
1.controller
@PostMapping("findNewsPage")
public Result findNewsPage(@RequestBody PortalVo portalVo){
Result result = headlineService.findNewsPage(portalVo);
return result;
}
2.service
// 首页数据查询
Result findNewsPage(PortalVo portalVo);
3.serviceImpl
public Result findNewsPage(PortalVo portalVo) {
// Page -> 当前页面,页容量
IPage<Map> page = new Page<>(portalVo.getPageNum(),portalVo.getPageSize());
headlineMapper.selectPage(page,portalVo);
List<Map> records = page.getRecords();
Map data = new HashMap();
// Map<Object, Object> data = new HashMap<>();
data.put("pageData",records);
data.put("pageNum",page.getCurrent());
data.put("pageSize",page.getSize());
data.put("totalPage",page.getPages());
data.put("totalSize",page.getTotal());
Map pageInfo = new HashMap();
pageInfo.put("pageInfo",data);
return Result.ok(pageInfo);
}
4.mapper
- IPage
- selectPage:这是方法的名称,它通常用来表示这个方法将被用于执行一个分页查询操作。
- (IPage iPage, @Param(“portalVo”) PortalVo portalVo):这部分定义了方法的参数列表:
- IPage iPage:第一个参数是一个 IPage 类型的对象,它包含了分页的配置信息(如当前页号和每页显示的记录数)。这个对象不仅指导 MyBatis-Plus 如何进行分页查询,还用于存储查询结果。
- @Param(“portalVo”) PortalVo portalVo:第二个参数使用了 @Param 注解,它告诉 MyBatis-Plus 在 SQL 语句中引用这个参数时应该使用注解中提供的名称。这里,“portalVo” 就是传递给 SQL 语句的参数名。PortalVo 类型的 portalVo 参数对象包含了查询所需的其他条件,如筛选关键词、类型等。
IPage<Map> selectPage(IPage iPage, @Param("portalVo") PortalVo portalVo);
5.mapper.xml
<select id="selectPage" resultType="map">
select hid,title,type,page_views pageViews,TIMESTAMPDIFF(HOUR,create_time,NOW()) pastHours,
publisher from news_headline where is_deleted=0
<if test="portalVo.keyWords !=null and portalVo.keyWords.length()>0 ">
and title like concat('%',#{portalVo.keyWords},'%')
</if>
<if test="portalVo.type != null and portalVo.type != 0">
and type = #{portalVo.type}
</if>
</select>
3、头条信息详情
- 连表查询需要自定义查询方法
@PostMapping("showHeadlineDetail")
public Result showHeadlineDetail(Integer hid){
Result result = headlineService.showHeadlineDetail(hid);
return result;
}
// 查询详情信息
Result showHeadlineDetail(Integer hid);
- 步骤
- 获取头条新闻详情,并返回到一个data中
- 将data放入另一个map集合headlineMap中
- 创建新闻头条实体并更新阅读量
- 更新数据库中的新闻头条
public Result showHeadlineDetail(Integer hid) {
Map data = headlineMapper.queryDetail(hid);
Map headlineMap = new HashMap();
headlineMap.put("headline",data);
// 修改阅读量+1
Headline headline = new Headline();
headline.setHid((Integer) data.get("hid"));
headline.setVersion((Integer) data.get("version"));
// 阅读量+1
headline.setPageViews((Integer) data.get("pageViews")+1);
headlineMapper.updateById(headline);
return Result.ok(headlineMap);
}
Map queryDetail(Integer hid);
<!-- Map queryDetail(Integer hid); -->
<select id="queryDetail" resultType="map">
select hid,title,article,type, h.version ,tname typeName ,page_views pageViews
,TIMESTAMPDIFF(HOUR,create_time,NOW()) pastHours,publisher
,nick_name author from news_headline h
left join news_type t on h.type = t.tid
left join news_user u on h.publisher = u.uid
where hid = #{hid}
</select>
2.3 头条模块开发
1、登陆验证和保护
@GetMapping("checkLogin")
public Result checkLogin(@RequestHeader String token){
boolean expiration = jwtHelper.isExpiration(token);
if(expiration){
return Result.build(null, ResultCodeEnum.NOTLOGIN);
}
return Result.ok(null);
}
2、配置拦截器
- 因为下面所有的/headline的操作都需要先检查用户是否登陆,所以需要配置拦截器
- LoginProtectInterceptor
这是一个实现了HandlerInterceptor接口的组件,用于在控制器处理请求之前进行预处理。它主要完成以下任务:
1. **获取Token:** 从HTTP请求的头部**token**字段中获取JWT token。
2. **检查Token有效性:** 使用注入的**JwtHelper**组件调用**isExpiration**方法来检查token是否已经过期。注意,如果**isExpiration**方法的意义是“是否过期”,那么方法名可能会让人困惑,更好的命名可能是**isExpired**。
3. **处理验证结果:**
- **Token有效:** 如果token未过期(**expiration**为**false**),则方法返回**true**,允许请求继续执行。
- **Token无效:** 如果token已过期或无效(**expiration**为**true**),则拦截器拒绝进一步处理请求,并向客户端返回一个状态码为504的JSON响应,表示用户未登录或认证过期。使用**ObjectMapper**将错误信息序列化为JSON格式,并通过**response.getWriter().print(json);**发送到客户端。
@Component
public class LoginProtectInterceptor implements HandlerInterceptor {
@Autowired
private JwtHelper jwtHelper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取token
String token = request.getHeader("token");
// 检查是否有效
boolean expiration = jwtHelper.isExpiration(token);
// 有效放行
if(!expiration){
// 放行
return true;
}
// 状态无效返回504的JSON
Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);
// 序列化为JSON格式
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
// 发送响应给客户端
response.getWriter().print(json);
return false;
}
}
- WebMVCConfig
- 注入拦截器: 通过**@Autowired注解自动注入LoginProtectInterceptor**。
- 配置拦截器: 在addInterceptors方法中,使用InterceptorRegistry添加loginProtectInterceptor,并通过addPathPatterns("/headline/")指定它只对以/headline/开头的URL路径生效。这意味着所有访问这些路径的请求都会先通过LoginProtectInterceptor**的预处理。
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginProtectInterceptor loginProtectInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginProtectInterceptor).addPathPatterns("/headline/**");
}
}
3、头条发布实现
- 需求携带headline,此外还需要将token携带
- 注意:不要忘了注解@RequestHeader
- controller
@PostMapping("publish")
public Result publish(@RequestBody Headline headline,@RequestHeader String token){
Result result = headlineService.publish(headline,token);
return result;
}
2.serviceImpl
- 先根据用户的携带的token查询id
- 数据装配的时候还需要将publisher等信息填入
public Result publish(Headline headline, String token) {
// 根据token查询用户id
int userId = jwtHelper.getUserId(token).intValue();
System.out.println(userId);
// 数据装配
headline.setPublisher(userId);
headline.setPageViews(0);
headline.setCreateTime(new Date());
headline.setUpdateTime(new Date());
headlineMapper.insert(headline);
return Result.ok(null);
}
4、头条修改数据回显
@PostMapping("findHeadlineByHid")
public Result findHeadlineByHid(Integer hid){
Headline headline = headlineService.getById(hid);
Map data = new HashMap();
data.put("headline",headline);
return Result.ok(data);
}
5、修改头条信息
@PostMapping("update")
public Result update(@RequestBody Headline headline){
Result result = headlineService.updateData(headline);
return result;
}
public Result updateData(Headline headline) {
Integer version = headlineMapper.selectById(headline).getVersion();
headline.setVersion(version); // 乐观锁
headline.setUpdateTime(new Date());
headlineMapper.updateById(headline);
return Result.ok(null);
}
6、删除头条信息
@PostMapping("removeByHid")
public Result removeByHid(Integer hid){
headlineService.removeById(hid);
return Result.ok(null);
}