文章目录
一、头条案例介绍
Github项目 : springboot-headline-part,新闻管理CRUD系统登录注册
- 用户功能
- 注册功能
- 登录功能
- 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
三、前端搭建
npm install
npm run dev
四、基于SpringBoot搭建项目基础架构
4.1 数据库脚本执行
CREATE DATABASE sm_db;
USE sm_db;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for news_headline
-- ----------------------------
DROP TABLE IF EXISTS `news_headline`;
CREATE TABLE `news_headline` (
`hid` INT NOT NULL AUTO_INCREMENT COMMENT '头条id',
`title` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头条标题',
`article` VARCHAR(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头条新闻内容',
`type` INT NOT NULL COMMENT '头条类型id',
`publisher` INT NOT NULL COMMENT '头条发布用户id',
`page_views` INT NOT NULL COMMENT '头条浏览量',
`create_time` DATETIME(0) NULL DEFAULT NULL COMMENT '头条发布时间',
`update_time` DATETIME(0) NULL DEFAULT NULL COMMENT '头条最后的修改时间',
`version` INT DEFAULT 1 COMMENT '乐观锁',
`is_deleted` INT DEFAULT 0 COMMENT '头条是否被删除 1 删除 0 未删除',
PRIMARY KEY (`hid`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for news_type
-- ----------------------------
DROP TABLE IF EXISTS `news_type`;
CREATE TABLE `news_type` (
`tid` INT NOT NULL AUTO_INCREMENT COMMENT '新闻类型id',
`tname` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '新闻类型描述',
`version` INT DEFAULT 1 COMMENT '乐观锁',
`is_deleted` INT DEFAULT 0 COMMENT '头条是否被删除 1 删除 0 未删除',
PRIMARY KEY (`tid`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of news_type
-- ----------------------------
INSERT INTO `news_type` (tid,tname) VALUES (1, '新闻');
INSERT INTO `news_type` (tid,tname) VALUES (2, '体育');
INSERT INTO `news_type` (tid,tname) VALUES (3, '娱乐');
INSERT INTO `news_type` (tid,tname) VALUES (4, '科技');
INSERT INTO `news_type` (tid,tname) VALUES (5, '其他');
-- ----------------------------
-- Table structure for news_user
-- ----------------------------
DROP TABLE IF EXISTS `news_user`;
CREATE TABLE `news_user` (
`uid` INT NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户登录名',
`user_pwd` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户登录密码密文',
`nick_name` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户昵称',
`version` INT DEFAULT 1 COMMENT '乐观锁',
`is_deleted` INT DEFAULT 0 COMMENT '头条是否被删除 1 删除 0 未删除',
PRIMARY KEY (`uid`) USING BTREE,
UNIQUE INDEX `username_unique`(`username`) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of news_user
-- ----------------------------
INSERT INTO `news_user` (uid,username,user_pwd,nick_name) VALUES (1, 'zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三');
INSERT INTO `news_user` (uid,username,user_pwd,nick_name) VALUES (2, 'lisi', 'e10adc3949ba59abbe56e057f20f883e', '李四');
INSERT INTO `news_user` (uid,username,user_pwd,nick_name) VALUES (5, 'zhangxiaoming', 'e10adc3949ba59abbe56e057f20f883e', '张小明');
INSERT INTO `news_user` (uid,username,user_pwd,nick_name)VALUES (6, 'xiaohei', 'e10adc3949ba59abbe56e057f20f883e', '李小黑');
SET FOREIGN_KEY_CHECKS = 1;
4.2 搭建SprintBoot工程
- 创建boot工程 : springboot-headline-part
4.2.1 导入依赖:
<?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.wake</groupId>
<artifactId>springboot-headline-part</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<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>
<!-- SpringBoot应用打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.2.2 编写配置
# server配置
server:
port: 8080
servlet:
context-path: /
# 连接池配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql:///sm_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis-plus的配置
mybatis-plus:
type-aliases-package: com.wake.pojo
global-config:
db-config:
logic-delete-field: isDeleted #全局逻辑删除
id-type: auto #主键策略自增长
table-prefix: news_ # 设置表的前缀
- druid兼容springboot3文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure
- 启动类和mybatis-plus配置
@MapperScan("com.wake.mapper")
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class,args);
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
//分页
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//防全局删除与更新
mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
//乐观锁
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}
4.2.3 工具类准备
结果封装类
package com.wake.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.wake.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.wake.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);
}
}
}
4.3 MybatisX逆向工程
完善实体类注解,注意yml已经全局配置的
主键注解、乐观锁、逻辑删除、版本Version
- pojo实体类 属性ID 添加 @TableId
- version版本属性 添加 @Version
五、后台功能开发
5.1 用户模块开发
5.1.1 jwt 和 token 介绍
1. token
令牌(Token):一种代表某种访问权限或身份认证信息的令牌。
可以是一串随机生成的字符或数字,用于验证用户的身份或授权用户对特定资源的访问。
- 每个用户生成的唯一字符串标识,可以进行用户识别和校验
- token验证标识无法直接识别用户的信息,盗取token后也无法`登录`程序! 相对安全!
- Token是一项规范和标准(接口)
2.jwt
JWT(JSON Web Token)是具体可以生成,校验,解析等动作Token的技术(实现类)
jwt 工作流程:
- 用户提供其凭据(通常是用户名和密码)进行身份验证。
- 服务器对这些凭据进行验证,并在验证成功后创建一个JWT。
- 服务器将JWT发送给客户端,并客户端在后续的请求中将JWT附加在请求头或参数中。
- 服务器接收到请求后,验证JWT的签名和有效性,并根据JWT中的声明进行身份验证和授权操作
jwt数据组成和包含信息:
JWT由三部分组成: header(头部).payload(载荷).signature(签名)
jwt可以携带很多信息! 一般情况,需要加入:有效时间,签名秘钥,其他用户标识信息!
-
有效时间为了保证token的时效性,过期可以重新登录获取!
-
签名秘钥为了防止其他人随意解析和校验token数据!
-
用户信息为了我们自己解析的时候,知道Token对应的具体用户!
5.1.2 jwt 使用与测试
- 导入依赖
<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>
- 配置yml
#jwt配置
jwt:
token:
tokenExpiration: 120 #有效时间,单位分钟
tokenSignKey: headline123456 #当前程序签名秘钥 自定义
- 导入工具类
封装jwt技术工具类
package com.wake.utils;
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
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;
}
}
}
- 测试
@SpringBootTest
public class SbTest {
@Autowired
private JwtHelper jwtHelper;
@Test
public void test_jwt() {
//生成 传入用户标识
String token = jwtHelper.createToken(1L);
System.out.println("token = " + token);
//解析用户标识
int userId = jwtHelper.getUserId(token).intValue();
System.out.println("userId = " + userId);
//校验是否到期! false 未到期 true到期
boolean expiration = jwtHelper.isExpiration(token);
System.out.println("expiration = " + expiration);
}
}
5.1.3 用户登录接口实现
-
需求描述
用户客户端输入用户名密码,向后端提交,后端根据用户名密码响应对应提示信息。 -
接口描述
url地址: user/login
请求方式:POST
请求参数:
{
"username":"zhangsan", //用户名
"userPwd":"123456" //明文密码
}
响应数据:
成功:
{
"code":"200", // 成功状态码
"message":"success" // 成功状态描述
"data":{
"token":"... ..." // 用户id的token
}
}
失败:
{
"code":"501",
"message":"用户名有误"
"data":{}
}
{
"code":"503",
"message":"密码有误"
"data":{}
}
- 代码实现
controller:
@CrossOrigin
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("login")
public Result login(@RequestBody User user){
Result result = userService.login(user);
return result;
}
}
service:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private JwtHelper jwtHelper;
@Override
public Result login(User user) {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<User>();
userLambdaQueryWrapper.eq(User::getUsername, user.getUsername());
User loginUser = userMapper.selectOne(userLambdaQueryWrapper);
if (loginUser == null) {
return Result.build(null, ResultCodeEnum.USERNAME_ERROR);
}
//对比密码
if (!StringUtils.isEmpty(loginUser.getUserPwd())
&& MD5Util.encrypt(user.getUserPwd()).equals(loginUser.getUserPwd())) {
//根据用户ID生成token
//并且将token封装进result返回
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);
}
}
- 通过用户名查询 数据库中是否存在该User
- 不存在,即返回501,输入的用户名有误
- 需要直接判断整个User是否为空,因为查询返回的是一个User,而且是用 用户名查询数据库
- 不要单独去判断用户名是否为空
- 存在:即返回的User不为空,进入下一步 密码判断
- 不存在,即返回501,输入的用户名有误
- 密码不为空且加密后密码对比 相等,允许登录且生成token
5.1.4 根据token获取用户数据
- 需求描述
客户端发送请求,提交token 请求头,后端根据token请求头获取登录用户的信息,并响应给用户。
此需求用于登陆之后,在前端页面可以获取使用用户信息,显示登陆信息的。像下列显示头像/用户名之类的…
- 接口描述
url地址:user/getUserInfo
请求方式:GET
请求头:token: token内容
响应数据:
成功
{
"code": 200,
"message": "success",
"data": {
"loginUser": {
"uid": 1,
"username": "zhangsan",
"userPwd": "",
"nickName": "张三"
}
}
}
失败
{
"code": 504,
"message": "notLogin",
"data": null
}
- 代码实现
注意:实体类Id属性 要添加@TableId注解 指定ID,不然查询ID会报错,查不到ID
- controller
@GetMapping("getUserInfo")
public Result getUserInfo(@RequestHeader String token){
Result result = userService.getUserInfo(token);
return result;
}
- service
@Override
public Result getUserInfo(String token) {
//判断token是否过期,TRUE到期,FALSE未到期
if (jwtHelper.isExpiration(token)) {
return Result.build(null,ResultCodeEnum.NOTLOGIN);
}
int userId = jwtHelper.getUserId(token).intValue();
User user = userMapper.selectById(userId);
user.setUserPwd("");
Map map = new HashMap();
map.put("loginUser",user);
return Result.ok(map);
}
先登录得到token,再查相应token。
- 首先 判断token是否过期,过期登录失败
- 在根据token获取uid(需要注解指定ID不然会有bug取不到id)
- 根据ID获取user
- 去掉密码内容
- User封装map
5.1.5 注册用户名占用情况检查
-
需求描述
用户在注册时输入用户名时,立刻将用户名发送给后端,后端根据用户名查询用户名是否可用并做出响应 -
接口描述
url地址:user/checkUserName
请求方式:POST
请求参数:param形式 : username=zhangsan
响应数据:
成功
{
"code":"200",
"message":"success"
"data":{}
}
失败
{
"code":"505",
"message":"用户名占用"
"data":{}
}
- 代码实现
controller
@PostMapping("checkUserName")
public Result checkUserName(@RequestParam String username){
Result result = userService.checkUserName(username);
return result;
}
service
userMapper.selectOne(userLambdaQueryWrapper);
@Override
public Result checkUserName(String username) {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUsername,username);
User user = userMapper.selectOne(userLambdaQueryWrapper);
//不为空,用户已存在
if(user != null){
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
return Result.ok(null);
}
userMapper.selectCount(userLambdaQueryWrapper);
@Override
public Result checkUserName(String username) {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUsername,username);
Long count = userMapper.selectCount(userLambdaQueryWrapper);
//为0,没有数据
if(count == 0){
return Result.ok(null);
}
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
5.1.6 用户注册
-
需求描述
客户端将新用户信息发送给服务端,服务端将新用户存入数据库,存入之前做用户名是否被占用校验,校验通过响应成功提示,否则响应失败提示 -
接口描述
url地址:user/register
请求方式:POST
请求参数:
{
"username":"zhangsan",
"userPwd":"123456",
"nickName":"张三"
}
{
"username":"Doug",
"userPwd":"123456",
"nickName":"道格"
}
响应数据:
成功
{
"code":"200",
"message":"success"
"data":{}
}
失败
{
"code":"505",
"message":"用户名占用"
"data":{}
}
- 代码实现
注意:密码要MD5加密
controller
@PostMapping("register")
public Result register(@RequestBody User user){
Result result = userService.register(user);
return result;
}
service
/**
* 注册
* 1. 检查账号是否已被已被占用
* 2. 密码加密MD5
* 3. 账号数据保存insert
* 4. 返回结果
* @param user
* @return
*/
@Override
public Result register(User user) {
LambdaQueryWrapper<User> userLambdaQueryWrapper = new LambdaQueryWrapper<>();
userLambdaQueryWrapper.eq(User::getUsername,user.getUsername());
Long count = userMapper.selectCount(userLambdaQueryWrapper);
if(count > 0){
return Result.build(null,ResultCodeEnum.USERNAME_USED);
}
user.setUserPwd(MD5Util.encrypt(user.getUserPwd()));
userMapper.insert(user);
return Result.ok(null);
}
5.2 首页模块开发
5.2.1 查询首页分类
-
需求描述
进入新闻首页,查询所有分类并动态展示新闻类别栏位 -
接口描述
url地址:portal/findAllTypes
请求方式:get
请求参数:无
响应数据:
成功
{
"code":"200",
"message":"OK"
"data":{
[
{
"tid":"1",
"tname":"新闻"
},
{
"tid":"2",
"tname":"体育"
},
{
"tid":"3",
"tname":"娱乐"
},
{
"tid":"4",
"tname":"科技"
},
{
"tid":"5",
"tname":"其他"
}
]
}
}
- 代码实现
controller
@RestController
@CrossOrigin
@RequestMapping("portal")
public class PortalController {
@Autowired
private TypeService typeService;
@GetMapping("findAllTypes")
public Result findAllTypes(){
Result result = typeService.findAllTypes();
return result;
}
}
service
@Service
public class TypeServiceImpl extends ServiceImpl<TypeMapper, Type>
implements TypeService{
@Autowired
private TypeMapper typeMapper;
@Override
public Result findAllTypes() {
List<Type> typeList = typeMapper.selectList(null);
return Result.ok(typeList);
}
}
5.2.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 // 总记录数
}
}
}
- 代码实现
请求参数 包装 Vo实体类
@Data
public class PortalVo {
private String keyWords;
private Integer type;
private Integer pageNum = 1;
private Integer pageSize =10;
}
controller
@PostMapping("findNewsPage")
public Result findNewsPage(@RequestBody PortalVo portalVo){
Result result = headlineService.findNewsPage(portalVo);
return result;
}
service
@Service
public class HeadlineServiceImpl extends ServiceImpl<HeadlineMapper, Headline>
implements HeadlineService{
@Autowired
private HeadlineMapper headlineMapper;
/**
* 首页数据查询
* 1. 进行分页数据查询
* 2. 分页数据,拼接到result即可
*
* 注意1 : 查询需要自定义语句,自定义Mapper的方法,携带分页
* 注意2 : 返回结果List<Map>
*
* 第一层pageInfo 由service层 IPage<Headline>整理完包装成pageInfoMap 返回给controller层
* 第二层的pageData 自定义查询语句,包装成List<Headline>
*
* @param portalVo
* @return
*/
@Override
public Result findNewsPage(PortalVo portalVo) {
//配置分页参数,当前页码数和当前页多少条
IPage<Headline> page = new Page<>(portalVo.getPageNum(), portalVo.getPageSize());
//自定义查询语句,portalVo传递keyword和type用于数据库查询
headlineMapper.selectMyPage(page, portalVo);
Map pageInfo = new HashMap();
List<Headline> records = page.getRecords();
pageInfo.put("pageData",records);
pageInfo.put("pageNum",page.getCurrent());
pageInfo.put("pageSize",page.getSize());
pageInfo.put("totalPage",page.getPages());
pageInfo.put("totalSize",page.getTotal());
Map pageInfoMap = new HashMap();
pageInfoMap.put("pageInfo",pageInfo);
return Result.ok(pageInfoMap);
}
}
mapper
/**
* 定义分页查询方法,返回map格式数据
* @param page
* @param portalVo
* @return
*/
IPage<Headline> selectMyPage(IPage page, @Param("portalVo") PortalVo portalVo);
mapper.xml
<select id="selectMyPage" 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>
5.2.3 *查询头条详情 (多表
- 需求
- 用户点击"查看全文"时,向服务端发送新闻id
- 后端根据新闻id查询完整新闻文章信息并返回
- 后端要同时让新闻的浏览量+1
- 接口描述
url地址:portal/showHeadlineDetail
请求方式:post
请求参数:
hid=1 param形成参数
响应数据:
成功
{
"code":"200",
"message":"success",
"data":{
"headline":{
"hid":"1", // 新闻id
"title":"马斯克宣布 ... ...", // 新闻标题
"article":"... ..." // 新闻正文
"type":"1", // 新闻所属类别编号
"typeName":"科技", // 新闻所属类别
"pageViews":"40", // 新闻浏览量
"pastHours":"3" , // 发布时间已过小时数
"publisher":"1" , // 发布用户ID
"author":"张三" // 新闻作者
}
}
}
- 代码实现
controller
@PostMapping("showHeadlineDetail")
public Result showHeadlineDetail(@RequestParam String hid){
Result result = headlineService.showHeadlineDetail(hid);
return result;
}
service
/**
* 根据id查询详情
* 1. 查询对应的数据即可【多表查询,头条和用户表,自定义Mapper方法 返回map】
* 2. 修改阅读量 【 "pageViews":"40", // 新闻浏览量; 涉及到乐观锁需要当前数据对应版本】
*
* @param hid
* @return
*/
@Override
public Result showHeadlineDetail(String hid) {
Map data = headlineMapper.selectDetailByHidMap(hid);
HashMap headlineMap = new HashMap();
headlineMap.put("headline", data);
// 修改阅读量+1,version乐观锁
Headline headline = new Headline();
headline.setHid((Integer) data.get("hid"));
headline.setVersion((Integer) data.get("version"));
headline.setPageViews((Integer) data.get("pageViews") + 1);
headlineMapper.updateById(headline);
return Result.ok(headlineMap);
}
mapper
/**
* 多表查询,新闻详情
* @param hid
* @return
*/
Map selectDetailByHidMap(String hid);
mapper.xml
<select id="selectDetailByHidMap" 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>
5.3 头条模块开发
5.3.1 登陆验证和保护
- 需求
- 客户端在进入发布页前、发布新闻前、进入修改页前、修改前、删除新闻前先向服务端发送请求携带token请求头
- 后端接收token请求头后,校验用户登录是否过期并做响应
- 前端根据响应信息提示用户进入登录页还是进入正常业务页面
- 接口描述
url地址:user/checkLogin
请求方式:get
请求参数: 无
请求头: token: 用户token
响应数据:
未过期:
{
"code":"200",
"message":"success",
"data":{}
}
过期:
{
"code":"504",
"message":"loginExpired",
"data":{}
}
- 代码实现
controller : 【登录检查】
UserController
@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);
}
拦截器 【所有/headline 开头的都需要检查登录状态】
package com.wake.interceptors;
/**
* @Description: 登录包含拦截器,检查请求头是否包含有效token
* 有 有效 放行
* 没有 无效 返回504
*/
@Component
public class LoginProtectedInterceptor implements HandlerInterceptor {
@Autowired
private JwtHelper jwtHelper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 请求头从获取token
String token = request.getHeader("token");
// 检查是否有效
if (StringUtils.isEmpty(token) || jwtHelper.isExpiration(token)) {
// 无效返回504的状态json
Result<Object> result = Result.build(null, ResultCodeEnum.NOTLOGIN);
// 能将result对象转为字符串
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.getWriter().print(json);
return false;
}
// 有效放行
return true;
}
}
拦截器配置
/**
* @Description: 添加拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginProtectedInterceptor loginProtectedInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginProtectedInterceptor).addPathPatterns("/headline/**");
}
}
5.3.2 头条发布实现
- 需求
- 用户在客户端输入发布的新闻信息完毕后
- 发布前先请求后端的登录校验接口验证登录
- 登录通过则提交新闻信息
- 后端将新闻信息存入数据库
- 接口描述
url地址:headline/publish
请求方式:post
请求头 : token: … …
请求参数:
{
"title":"文章标题 ... ...", // 文章标题
"article":"... 文章内容 ...", // 文章内容
"type":"1" // 文章类别
}
响应数据:
未登录
{
"code":"504",
"message":"loginExpired",
"data":{}
}
成功
{
"code":"200",
"message":"success",
"data":{}
}
- 代码实现
controller
@RestController
@CrossOrigin
@RequestMapping("headline")
public class HeadlineController {
@Autowired
private HeadlineService headlineService;
/**
* 登录后才能 发布新闻
* @param headline
* @param token
* @return
*/
@PostMapping("publish")
public Result publish(@RequestBody Headline headline,@RequestHeader String token){
Result result = headlineService.publish(headline,token);
return result;
}
}
service
headline
@Override
public Result publish(Headline headline, String token) {
// token 查询用户id
int userId = jwtHelper.getUserId(token).intValue();
headline.setPublisher(userId);
headline.setPageViews(0);
headline.setCreateTime(new Date());
headline.setUpdateTime(new Date());
headlineMapper.insert(headline);
return Result.ok(null);
}
5.3.3 修改头条回显
- 需求
- 前端先调用登录校验接口,校验登录是否过期
- 登录校验通过后 ,则根据新闻id查询新闻的完整信息并响应给前端
- 接口描述
url地址:headline/findHeadlineByHid
请求方式:post
请求参数:
hid=1 param形成参数
响应数据:
成功
{
"code":"200",
"message":"success",
"data":{
"headline":{
"hid":"1",
"title":"马斯克宣布",
"article":"... ... ",
"type":"2"
}
}
}
- 代码实现
controller
也可以直接调用service的继承的方法
@PostMapping("findHeadlineByHid")
public Result findHeadlineByHid(@RequestParam String hid) {
Headline headline = headlineService.getById(hid);
Map map = new HashMap();
map.put("headline", headline);
return Result.ok(map);
}
先登录,拿到token
5.3.4 头条修改实现
- 需求
- 客户端将新闻信息修改后,提交前先请求登录校验接口校验登录状态
- 登录校验通过则提交修改后的新闻信息,后端接收并更新进入数据库
- 接口描述
url地址:headline/update
请求方式:post
请求参数:
{
"hid":"1",
"title":"尚硅谷宣布 ... ...",
"article":"... ...",
"type":"2"
}
响应数据:
成功
{
"code":"200",
"message":"success",
"data":{}
}
- 代码实现
controller
@PostMapping("update")
public Result update(@RequestBody Headline headline) {
Result result = headlineService.updateData(headline);
return result;
}
service
直接更新前需要设置version乐观锁 以及 最新时间节点
然后再更新
/**
* 修改头条新闻
* 1. hid查询数据的最新version
* 2. 修改数据的更新时间为当前节点
* @param headline
* @return
*/
@Override
public Result updateData(Headline headline) {
Integer version = headlineMapper.selectById(headline.getHid()).getVersion();
//乐观锁
headline.setVersion(version);
headline.setUpdateTime(new Date());
headlineMapper.updateById(headline);
return Result.ok(null);
}
5.3.5 删除头条功能
- 需求
- 将要删除的新闻id发送给服务端
- 服务端校验登录是否过期,未过期则直接删除,过期则响应登录过期信息
- 接口描述
url地址:headline/removeByHid
请求方式:post
请求参数 : hid=1 param形成参数
响应数据:成功
{
"code":"200",
"message":"success",
"data":{}
}
- 代码实现
controller
@PostMapping("removeByHid")
public Result removeByHid(@RequestParam String hid){
headlineService.removeById(hid);
return Result.ok(null);
}
总结
头条新闻纯JavaWeb项目实现 1
JavaWeb_头条新闻项目实现 2