目录
完整后端代码:https://pan.baidu.com/s/1sEsK4x6uM_o32qdsVTxagQ?pwd=d67f
apifox测试文件:https://pan.baidu.com/s/11liz1MxjYeitrsJbua3VFA?pwd=k8we
项目模块
- 用户功能
- 注册功能
- 登录功能
- jwt实现
- 头条新闻
- 新闻的分页浏览
- 通过标题关键字搜索新闻
- 查看新闻详情
- 新闻的修改和删除
技术栈
前端技术栈:
- ES6作为基础JS语法
- nodejs用于运行环境
- npm用于项目依赖管理工具
- vite用于项目的构建架工具
- Vue3用于项目数据的渲染框架
- Axios用于前后端数据的交互
- Router用于页面的跳转
- Pinia用于存储用户的数据
- LocalStorage作为用户校验token的存储手段
- Element-Plus提供组件
后端技术栈:
- JAVA作为开发语言,版本为JDK17
- Tomcat作为服务容器,版本为10.1.7
- Mysql8用于项目存储数据
- SpringMVC用于控制层实现前后端数据交互
- MyBatis-Plus用于实现数据的CURD
- Druid用于提供数据源的连接池
- SpringBoot作为项目基础架构
- MD5用于用户密码的加密
- Jwt用于token的生成和校验
- Jackson用于转换JSON
功能展示
头条首页信息搜索
登录功能
注册功能
展示功能
发布头条功能
修改头条功能
环境搭建
前端环境搭建:
前端环境下载https://pan.baidu.com/s/16HJROtpdTZ6DrL0GMNJncw?pwd=42wf
下载后解压,使用vscode打开文件夹,在vscode中打开终端依次输入:
npm install
npm run dev
后项目启动:
后端环境搭建:
数据库创建:
(建议使用数据库软件操作)
https://pan.baidu.com/s/19vKtJCn_CwyFHPwinpcmsA?pwd=fkky
springboot项目搭建:
1.创建boot工程:springboot-news
2.导入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.5</version> </parent> <groupId>com.qiu</groupId> <artifactId>springboot-news</artifactId> <version>1.0-SNAPSHOT</version> <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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.编写配置
# server配置 server: port: 8080 servlet: context-path: / # 连接池配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: url: jdbc:mysql:///sm_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # mybatis-plus的配置 mybatis-plus: type-aliases-package: com.qiu.pojo global-config: db-config: logic-delete-field: isDeleted #全局逻辑删除 id-type: auto #主键策略自增长 table-prefix: news_ # 设置表的前缀
4.解决druid问题
druid版本在1.2.20以下的需要:创建文件,添加内容
文件名:org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容:com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure5.编写启动类和mybatis-plus插件
包:com.qiu
@SpringBootApplication @MapperScan("com.qiu.mapper") public class Main { public static void main(String[] args) { System.out.println("Hello world!"); SpringApplication.run(Main.class,args); } @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); //分页 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); //乐观锁 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); //防止全局修改和删除 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } }
6.编写工具类
包:com.qiu.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; } }
结果状态信息枚举类
/** * 统一返回结果状态信息类 * */ 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加密工具类
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); } } }
mybatis-plus逆向工程
1.逆向工程
2.完善pojo类
删除@TableName注解,全局统一设置
添加主键,乐观锁,逻辑删除注解
@Data
public class User implements Serializable {
@TableId
private Integer uid;
private String username;
private String userPwd;
private String nickName;
@Version
private Integer version;
@TableLogic
private Integer isDeleted;
private static final long serialVersionUID = 1L;
}
后端功能开发
用户模块开发
jwt 与 token
令牌(token):用于验证用户身份或授权用户对特定资源的访问。普通令牌可以以多种形式出现:访问令牌,身份令牌,刷新令牌等。
就是在用户登录后生成一段字符或数字给他,用户之后每次访问都携带这个token来证明自己的身份,这段字符或数字就是token
JWT介绍
Token是一项规范和标准(接口)
JWT(JSON Web Token)是具体可以生成,校验,解析等动作Token的技术(实现类)
JWT工作流程
- 用户通过其凭据(通常为账号密码)进行身份认证
- 服务器对凭据进行验证,验证成功后创建一个JWT
- 服务器将JWT发送给客户端,客户端在之后的请求中将JWT添加到请求头或参数中
- 服务器接收请求后,验证JWT的签名和有效性,并根据JWT中的声明进行身份验证和授权操作
JWT的数据组成和包含信息
JWT由三部分组成: header(头部).payload(载荷).signature(签名)
jwt可以携带很多信息:有效时间,签名秘钥,其他用户标识信息等
有效时间为了保证token的时效性,过期可以重新登录获取
签名秘钥为了防止其他人随意解析和校验token数据
用户信息为了我们自己解析的时候,知道Token对应的具体用户
JWT的使用
1.导入依赖
<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>
2.编写配置
application.yaml
jwt:
token:
# tokenExpiration: 120 #有效时间,单位分钟
tokenExpiration: 1 #为了测试token过期,设置一分钟
tokenSignKey: qiu666 #当前程序签名秘钥 自定义
3.导入工具类
@Component
@Data
@ConfigurationProperties(prefix = "jwt.token")
public class JwtHelper {
//有效时间,单位毫秒 1000毫秒 == 1秒
private long tokenExpiration;
//当前程序签名秘钥
private String tokenSignKey;
//生成token
public String createToken(Long userId){
String token = Jwts.builder().setSubject("YYGH-USER")
// 1分钟
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration * 1000 * 60))
//存放数据
.claim("userId", userId)
//签名,当用户请求时携带token,根据签名判断身份是否通过
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP).compact();
return token;
}
//当用户请求中携带token时,从token中获取数据(useId)
public Long getUserId(String token){
//StringUtils的包为com.baomidou.mybatisplus.core.toolkit.StringUtils;
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;
}
}
}
4.使用和测试
@SpringBootTest
public class JwtTest {
@Autowired
private JwtHelper jwtHelper;
@Test
public void test() throws InterruptedException {
//生成数据 用户数据 userId 1L
String token = jwtHelper.createToken(1L);
System.out.println("token = " + token);
//解析用户标识
Long userId = jwtHelper.getUserId(token);
System.out.println("userId = " + userId);
//查看token是否过期 false 未到期 true到期
boolean expiration = jwtHelper.isExpiration(token);;
System.out.println("expiration = " + expiration);
Thread.sleep(1000*60);
System.out.println(" 程序睡眠一分钟后: ");
//查看token是否过期 false 未到期 true到期
boolean expiration1 = jwtHelper.isExpiration(token);;
System.out.println("expiration = " + expiration1);
}
}
登录功能实现
1.需求:用户在客户端输入用户名密码并向后端提交,后端根据用户名和密码判断登录是否成功,用户有误或者密码有误响应不同的提示信息!
2.接口描述:
url:user/login
请求方式:POST
请求参数:
{
"username":"zhangsan",
"userPwd":"123456"
}
响应数据:
成功
{
"code":"200", // 成功状态码
"message":"success" // 成功状态描述
"data":{
"token":"... ..." // 用户id的token
}
}
失败
{
"code":"501",
"message":"用户名有误"
"data":{}
}
或者
{
"code":"503",
"message":"密码有误"
"data":{}
}
3.实现代码:
controller:
@RestController
@RequestMapping("user")
@CrossOrigin //解决跨域问题
public class UserController {
@Autowired
private UserService userService;
@PostMapping("login")
public Result<User> login(@RequestBody User user){
Result<User> result = userService.login(user);
return result;
}
}
service:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
@Autowired
private JwtHelper jwtHelper;
@Autowired
private UserMapper userMapper;
/**
* 登录业务实现
* @param user
* @return result封装
*/
@Override
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()));
Map data = new HashMap();
data.put("token",token);
return Result.ok(data);
}
//密码错误
return Result.build(null,ResultCodeEnum.PASSWORD_ERROR);
}
}
测试结果
根据token获取用户信息
1.需求:客户端发送请求,提交token请求头,后端根据token请求头获取登录用户的详细信息并响应给客户端进行存储
2.接口描述:
url:user/getUserInfo
请求方式:GET
请求头:
token:token内容
响应数据:
成功:
{
"code": 200,
"message": "success",
"data": {
"loginUser": {
"uid": 1,
"username": "zhangsan",
"userPwd": "",
"nickName": "张三"
}
}
}
失败:
{
"code": 504,
"message": "notLogin",
"data": null
}
代码实现:
controller:
/**
* 地址: user/getUserInfo
* 方式: get
* 请求头: token = token内容
* 返回:
* {
* "code": 200,
* "message": "success",
* "data": {
* "loginUser": {
* "uid": 1,
* "username": "zhangsan",
* "userPwd": "",
* "nickName": "张三"
* }
* }
* }
*
* 大概流程:
* 1.获取token,解析token对应的userId
* 2.根据userId,查询用户数据
* 3.将用户数据的密码置空,并且把用户数据封装到结果中key = loginUser
* 4.失败返回504 (本次先写到当前业务,后期提取到拦截器和全局异常处理器)
*/
@GetMapping("getUserInfo")
public Result userInfo(@RequestHeader String token){
Result result = userService.getUserInfo(token);
return result;
}
service:
/**
* 查询用户数据
* @param token
* @return result封装
*/
@Override
public Result getUserInfo(String token) {
//1.判定是否有效期
if (jwtHelper.isExpiration(token)) {
//true过期,直接返回未登录
return Result.build(null,ResultCodeEnum.NOTLOGIN);
}
//2.获取token对应的用户
int userId = jwtHelper.getUserId(token).intValue();
//3.查询数据
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.NOTLOGIN);
}
测试结果:
注册用户名检查
1.需求:用户在注册时输入用户名时,立刻将用户名发送给后端,后端根据用户名查询用户名是否可用并做出响应
2.接口描述:
url:user/checkUserName
请求方式:POST
请求参数:param形式
username=zhangsan
响应数据:
成功:
{
"code":"200",
"message":"success"
"data":{}
}
失败:
{
"code":"505",
"message":"用户名占用"
"data":{}
}
3.代码实现:
controller:
/**
* url地址:user/checkUserName
* 请求方式:POST
* 请求参数:param形式
* username=zhangsan
* 响应数据:
* {
* "code":"200",
* "message":"success"
* "data":{}
* }
*
* 实现步骤:
* 1. 获取账号数据
* 2. 根据账号进行数据库查询
* 3. 结果封装
*/
@PostMapping("checkUserName")
public Result checkUserName(String username){
Result result = userService.checkUserName(username);
return result;
}
service:
/**
* 检查账号是否可以注册
*
* @param username 账号信息
* @return
*/
@Override
public Result checkUserName(String username) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUsername,username);
User user = userMapper.selectOne(queryWrapper);
if (user != null){
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
return Result.ok(null);
}
测试结果:
用户注册功能
1.需求:客户端将新用户信息发送给服务端,服务端将新用户存入数据库,存入之前做用户名是否被占用校验,校验通过响应成功提示,否则响应失败提示
2.接口描述:
url:user/regist
请求方式:POST
请求参数:
{
"username":"zhangsan",
"userPwd":"123456",
"nickName":"张三"
}
响应数据:
成功:
{
"code":"200",
"message":"success"
"data":{}
}
失败:
{
"code":"505",
"message":"用户名占用"
"data":{}
}
3.代码实现:
controller:
/**
* url地址:user/regist
* 请求方式:POST
* 请求参数:
* {
* "username":"zhangsan",
* "userPwd":"123456",
* "nickName":"张三"
* }
* 响应数据:
* {
* "code":"200",
* "message":"success"
* "data":{}
* }
*
* 实现步骤:
* 1. 将密码加密
* 2. 将数据插入
* 3. 判断结果,成 返回200 失败 505
*/
@PostMapping("regist")
public Result regist(@RequestBody User user){
Result result = userService.regist(user);
return result;
}
service:
@Override
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()));
int rows = userMapper.insert(user);
System.out.println("rows = " + rows);
return Result.ok(null);
}
测试结果:
首页模块开发
查询首页分类
1.需求:
进入新闻首页,查询所有分类并动态展示新闻类别栏位
2.接口描述:
url:portal/findAllTypes
请求方式:GET
请求参数:无
响应数据:
成功:
{
"code":"200",
"message":"OK"
"data":{
[
{
"tid":"1",
"tname":"新闻"
},
{
"tid":"2",
"tname":"体育"
},
{
"tid":"3",
"tname":"娱乐"
},
{
"tid":"4",
"tname":"科技"
},
{
"tid":"5",
"tname":"其他"
}
]
}
}
3.代码实现
controller:
@RestController
@RequestMapping("portal")
@CrossOrigin
public class PortalController {
@Autowired
private TypeService typeService;
/**
* 查询全部类别信息
* @return
*/
@GetMapping("findAllTypes")
public Result findAllTypes(){
//直接调用业务层,查询全部数据
List<Type> list = typeService.list();
return Result.ok(list);
}
}
测试结果:
分页查询首页头条信息
1.需求:
客户端向服务端发送查询关键字,新闻类别,页码数,页大小
服务端根据条件搜索分页信息,返回含页码数,页大小,总页数,总记录数,当前页数据等信息,并根据时间降序,浏览量降序排序
2.接口描述:
url:portal/findNewsPage
请求方式:POST
请求参数:
{
"keyWords":"马斯克", // 搜索标题关键字
"type":0, // 新闻类型
"pageNum":1, // 页码数
"pageSize":10 // 页大小
}
响应数据:
成功:
{
"code":"200",
"message":"success"
"data":{
"pageInfo":{
"pageData":[
{
"hid":"1", // 新闻id
"title":"尚硅谷宣布 ... ...", // 新闻标题
"type":"1", // 新闻所属类别编号
"pageViews":"40", // 新闻浏览量
"pastHours":"3" , // 发布时间已过小时数
"publisher":"1" // 发布用户ID
},
{
"hid":"1", // 新闻id
"title":"尚硅谷宣布 ... ...", // 新闻标题
"type":"1", // 新闻所属类别编号
"pageViews":"40", // 新闻浏览量
"pastHours":"3", // 发布时间已过小时数
"publisher":"1" // 发布用户ID
},
{
"hid":"1", // 新闻id
"title":"尚硅谷宣布 ... ...", // 新闻标题
"type":"1", // 新闻所属类别编号
"pageViews":"40", // 新闻浏览量
"pastHours":"3", // 发布时间已过小时数
"publisher":"1" // 发布用户ID
}
],
"pageNum":1, //页码数
"pageSize":10, // 页大小
"totalPage":20, // 总页数
"totalSize":200 // 总记录数
}
}
}
3.代码实现:
准备接收条件类:
@Data
public class PortalVo {
private String keyWords;
private Integer type;
private Integer pageNum = 1;
private Integer pageSize =10;
}
controller:
/**
* 首页分页查询
* @return
*/
@PostMapping("findNewPages")
public Result findNewPage(@RequestBody PortalVo portalVo){
Result result = headlineService.findNewPage(portalVo);
return result;
}
service:
@Service
public class HeadlineServiceImpl extends ServiceImpl<HeadlineMapper, Headline>
implements HeadlineService{
@Autowired
private HeadlineMapper headlineMapper;
/**
* 首页数据查询
* @param portalVo
* @return
*/
@Override
public Result findNewPage(PortalVo portalVo) {
//1.条件拼接 需要非空判断
LambdaQueryWrapper<Headline> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(!StringUtils.isEmpty(portalVo.getKeyWords()),Headline::getTitle,portalVo.getKeyWords())
.eq(portalVo.getType()!= null,Headline::getType,portalVo.getType());
//2.分页参数
IPage<Headline> page = new Page<>(portalVo.getPageNum(),portalVo.getPageSize());
//3.分页查询
//查询的结果 "pastHours":"3" // 发布时间已过小时数 我们查询返回一个map
//自定义方法
headlineMapper.selectPageMap(page, portalVo);
//4.结果封装
//分页数据封装
Map<String,Object> pageInfo =new HashMap<>();
pageInfo.put("pageData",page.getRecords());
pageInfo.put("pageNum",page.getCurrent());
pageInfo.put("pageSize",page.getSize());
pageInfo.put("totalPage",page.getPages());
pageInfo.put("totalSize",page.getTotal());
Map<String,Object> pageInfoMap=new HashMap<>();
pageInfoMap.put("pageInfo",pageInfo);
// 响应JSON
return Result.ok(pageInfoMap);
}
}
mapper:
接口:
public interface HeadlineMapper extends BaseMapper<Headline> {
//自定义分页查询方法
IPage<Map> selectPageMap(IPage<Headline> page,
@Param("portalVo") PortalVo portalVo);
}
mapper.xml:
<select id="selectPageMap" 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>
测试结果:
查询头条详情
1.需求:
用户点击"查看全文"时,向服务端发送新闻id
后端根据新闻id查询完整新闻文章信息并返回
后端要同时让新闻的浏览量+1
2.接口描述:
url:portal/showHeadlineDetial
请求方式:POST
请求参数: param形式
hid=1
响应数据:
成功:
{
"code":"200",
"message":"success",
"data":{
"headline":{
"hid":"1", // 新闻id
"title":"马斯克宣布 ... ...", // 新闻标题
"article":"... ..." // 新闻正文
"type":"1", // 新闻所属类别编号
"typeName":"科技", // 新闻所属类别
"pageViews":"40", // 新闻浏览量
"pastHours":"3" , // 发布时间已过小时数
"publisher":"1" , // 发布用户ID
"author":"张三" // 新闻作者
}
}
}
3.代码实现:
controller:
/**
* 首页详情接口
* @param hid
* @return
*/
@PostMapping("showHeadlineDetail")
public Result showHeadlineDetail(Integer hid){
Result result = headlineService.showHeadlineDetail(hid);
return result;
}
service:
/**
* 详情数据查询
* "headline":{
* "hid":"1", // 新闻id
* "title":"马斯克宣布 ... ...", // 新闻标题
* "article":"... ..." // 新闻正文
* "type":"1", // 新闻所属类别编号
* "typeName":"科技", // 新闻所属类别
* "pageViews":"40", // 新闻浏览量
* "pastHours":"3" , // 发布时间已过小时数
* "publisher":"1" , // 发布用户ID
* "author":"张三" // 新闻作者
* }
* 注意: 是多表查询 , 需要更新浏览量+1
*
* @param hid
* @return
*/
@Override
public Result showHeadlineDetail(Integer hid) {
//1.实现根据id的查询(多表
Map headLineDetail = headlineMapper.selectDetailMap(hid);
//2.拼接头条对象(阅读量和version)进行数据更新
Headline headline = new Headline();
headline.setHid(hid);
headline.setPageViews((Integer) headLineDetail.get("pageViews")+1); //阅读量+1
headline.setVersion((Integer) headLineDetail.get("version")); //设置版本
headlineMapper.updateById(headline);
Map<String,Object> pageInfoMap=new HashMap<>();
pageInfoMap.put("headline",headLineDetail);
return Result.ok(pageInfoMap);
}
mapper:
接口:
/**
* 分页查询头条详情
* @param hid
* @return
*/
Map selectDetailMap(Integer hid);
mapper.xml:
<!-- Map selectDetailMap(Integer hid);-->
<select id="selectDetailMap" 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>
测试结果:
头条模块开发
登录验证和保护
1.需求:
客户端在进入发布页前、发布新闻前、进入修改页前、修改前、删除新闻前先向服务端发送请求携带token请求头
后端接收token请求头后,校验用户登录是否过期并做响应
前端根据响应信息提示用户进入登录页还是进入正常业务页面
2.接口描述:
url:user/checkLogin
请求方式:GET
请求参数:无
请求头:token: 用户token
3.代码实现:
controller:(登录后检查)
@GetMapping("checkLogin")
public Result checkLogin(@RequestHeader String token){
if (StringUtils.isEmpty(token) || jwtHelper.isExpiration(token)){
//没有传或者过期 未登录
return Result.build(null, ResultCodeEnum.NOTLOGIN);
}
return Result.ok(null);
}
创建拦截器:
@Component
public class LoginProtectInterceptor implements HandlerInterceptor {
@Autowired
private JwtHelper jwtHelper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token) || jwtHelper.isExpiration(token)){
Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.getWriter().print(json);
//拦截
return false;
}else{
//放行
return true;
}
}
}
配置拦截器:(使用 /headline 开头都拦截)
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginProtectInterceptor loginProtectInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginProtectInterceptor).addPathPatterns("/headline/**");
}
}
测试结果:
头条发布实现
1.需求:
用户在客户端输入发布的新闻信息完毕后
发布前先请求后端的登录校验接口验证登录
登录通过则提交新闻信息
后端将新闻信息存入数据库
2.接口描述:
url:headline/publish
请求方式:POST
请求头:token:用户token
请求参数:
{
"title":"尚硅谷宣布 ... ...", // 文章标题
"article":"... ...", // 文章内容
"type":"1" // 文章类别
}
响应数据:
未登录:
{
"code":"504",
"message":"loginExpired",
"data":{}
}
成功:
{
"code":"200",
"message":"success",
"data":{}
}
3.代码实现:
controller:
/**
* 实现步骤:
* 1. token获取userId [无需校验,拦截器会校验]
* 2. 封装headline数据
* 3. 插入数据即可
*/
@PostMapping("publish")
public Result publish(@RequestBody Headline headline,@RequestHeader String token){
int userId = jwtHelper.getUserId(token).intValue();
headline.setPublisher(userId);
Result result = headlineService.publish(headline);
return result;
}
service:
/**
* 发布数据
* @param headline
* @return
*/
@Override
public Result publish(Headline headline) {
headline.setCreateTime(new Date());
headline.setUpdateTime(new Date());
headline.setPageViews(0);
headlineMapper.insert(headline);
return Result.ok(null);
}
测试结果:
修改头条回显
1.需求:
前端先调用登录校验接口,校验登录是否过期
登录校验通过后 ,则根据新闻id查询新闻的完整信息并响应给前端
2.接口描述:
url:headline/findHeadlineByHid
请求方式:POST
请求参数:param形式
hid=1
响应数据:
成功:
{
"code":"200",
"message":"success",
"data":{
"headline":{
"hid":"1",
"title":"马斯克宣布",
"article":"... ... ",
"type":"2"
}
}
}
3.代码实现:
controller:
@PostMapping("findHeadlineByHid")
public Result findHeadlineByHid(Integer hid){
Result result = headlineService.findHeadlineByHid(hid);
return result;
}
service:
/**
* 根据id查询详情
* @param hid
* @return
*/
@Override
public Result findHeadlineByHid(Integer hid) {
Headline headline = headlineMapper.selectById(hid);
Map<String,Object> pageInfoMap=new HashMap<>();
pageInfoMap.put("headline",headline);
return Result.ok(pageInfoMap);
}
测试结果:
头条修改实现
1.需求:
客户端将新闻信息修改后,提交前先请求登录校验接口校验登录状态
登录校验通过则提交修改后的新闻信息,后端接收并更新进入数据库
2.接口描述:
url:headline/update
请求方式:POST
请求参数:
{
"hid":"1",
"title":"尚硅谷宣布 ... ...",
"article":"... ...",
"type":"2"
}
响应数据:
成功:
{
"code":"200",
"message":"success",
"data":{}
}
3.代码实现:
controller:
@PostMapping("update")
public Result update(@RequestBody Headline headline){
Result result = headlineService.updateHeadLine(headline);
return result;
}
service:
/**
* 修改业务
* 1.查询version版本
* 2.补全属性,修改时间 , 版本!
*
* @param headline
* @return
*/
@Override
public Result updateHeadLine(Headline headline) {
//读取版本
Integer version = headlineMapper.selectById(headline.getHid()).getVersion();
headline.setVersion(version);
headline.setUpdateTime(new Date());
headlineMapper.updateById(headline);
return Result.ok(null);
}
测试结果:
删除头条功能
1.需求:
将要删除的新闻id发送给服务端
服务端校验登录是否过期,未过期则直接删除,过期则响应登录过期信息
2.接口描述:
url:headline/removeByHid
请求方式:POST
请求参数:param形式
hid=1
响应数据:
成功:
{
"code":"200",
"message":"success",
"data":{}
}
3.代码实现:
controller:
@PostMapping("removeByHid")
public Result removeById(Integer hid){
headlineService.removeById(hid);
return Result.ok(null);
}
测试结果: