首先讲述一下该项目能实现的功能,通过这些功能开始编写后端代码。
所运用到的前后端技术栈:
本次项目开发依旧使用到了mvc的设计模式结构,具体的解释可以见我上一篇博客。
如图该项目依旧是和上一篇日程管理系统的结构大相径庭。common层是规定后端给前端响应的状态码,dao层用来和后端数据库发生交互,filters层放入了校验相关的过滤器,pojo层是创建相关实体类,service层是处理除了和数据库交互之外的其他业务需求(例如将明文密码转换为密文密码,以及后面要讲到的处理新闻分页查询功能),util层放入相关的工具类。
controller层我们这次也称为业务处理接口,作用就是连接前后端,前端发送需求给后端,controller层进行接收然后发送给service层继续后端的操作。这次项目会采用接口文档的方式来对后端代码进行开发,对照接口文档对controller层代码进行开发。
同时在截图中我也是给出了相关的jar包,在maven中都可以找到资源并下载。
Vo层解析
这次在开发pojo层的时候会发现多了一层叫Vo层
这层Vo用于解决多表查询时,好几个表要用到一个实体类来装,这时pojo层单独的实体类就装不了这些数据了。通俗来说就是一个实体类的属性少于SQL 查询结果的字段,就装不下所有的数据,所以要再定义一个类。后面会使用到多表查询就是要使用Vo层中的类,将多张表中的属性封装到一个实体类中。
Pojo层代码
NewsUser :
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class NewsUser implements Serializable {
private Integer uid;
private String username;
private String userPwd;
private String nickName;
}
NewsType:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class NewsType implements Serializable {
private Integer tid;
private String tname;
}
NewsHeadline:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class NewsHeadline implements Serializable {
private Integer hid;
private String title;
private String article;
private Integer type;
private Integer publisher;
private Integer pageViews;
private Date createTime;
private Date updateTime;
private Integer isDeleted;
}
HeadlineDetailVo:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class HeadlineDetailVo implements Serializable {
private Integer hid;
private String title;
private String article;
private Integer type;
private String typeName;
private Integer pageViews;
private Long pastHours;
private Integer publisher;
private String author;
}
HeadlinePageVo:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class HeadlinePageVo implements Serializable {
private Integer hid;
private String title;
private Integer type;
private Integer pageViews;
private Long pastHours;
private Integer publisher;
}
HeadlineQueryVo:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class HeadlineQueryVo implements Serializable {
private String keyWords;
private Integer type ;
private Integer pageNum;
private Integer pageSize;
}
Postman工具
PostMan是一个接口测试工具,在做接口测试的时候,Postman相当于一个客户端,它可以模仿用户发起的各类http请求,将请求数据发送至服务端,获取对应的响应结果,从而验证响应中的结果数据是否和预期值相匹配。
右边的POST那一行是输入要往后端的哪一个方法发送的url,即发送到Controller层。旁边的POST也可以修改成GET,即指定发送方式。下面的Key-Value就是指定要发送的键值对格式。
以第一个需求(将新闻的5种类型显示在页面最上方)举例
首先我们需要在controller层中多创建一个PortalController层作为门户控制器,那些不需要登录,不需要做增删改的门户页的请求都放在这里。然后根据前端需要的信息:
可知所需要的实体类是pojo中的NewsType类。
1,创建方法用于调用service层:
/**
* 查询所有头条类型的业务接口实现
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void findAllTypes(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//查询所有的新闻类型,装入Result响应给客户端
List<NewsType> newsTypeList = typeService.findAll();
WebUtil.writeJson(resp, Result.ok(newsTypeList));
}
2,在NewsTypeService接口中实现:
import com.ryy.headline.pojo.NewsType;
import java.util.List;
public interface NewsTypeService {
/**
* 查询所有头条类型的方法
* @return 多个头条类型以List<NewsType>集合形式返回
*/
List<NewsType> findAll();
}
3,调用dao层代码:
import com.ryy.headline.dao.NewsTypeDao;
import com.ryy.headline.dao.Impl.NewsTypeDaoImpl;
import com.ryy.headline.pojo.NewsType;
import com.ryy.headline.service.NewsTypeService;
import java.util.List;
public class NewsTypeServiceImpl implements NewsTypeService {
private NewsTypeDao typeDao =new NewsTypeDaoImpl();
@Override
public List<NewsType> findAll() {
return typeDao.findAll();
}
}
4,发送sql语句:
import com.ryy.headline.dao.BaseDao;
import com.ryy.headline.dao.NewsTypeDao;
import com.ryy.headline.pojo.NewsType;
import java.util.List;
public class NewsTypeDaoImpl extends BaseDao implements NewsTypeDao {
@Override
public List<NewsType> findAll() {
String sql = "select tid,tname from news_type";
return baseQuery(NewsType.class,sql);
}
}
当这些代码编写好之后我们就能使用postman进行测试:
我们输入相应的url进行查询,postman就会显示后端发送过来的数据,而返回的数据与刚才前端需要的样子相同,即代表代码无问题。
登录需求的实现
token
在实现登录功能之前需要讲述一个知识点叫token,其作用其实和我们之前使用的session域的作用是一样的,都是将数据暂时存储起来然后下次登录时候就去这些域中寻找查看是否已经登陆过。
而使用token的优势就在于后端不用产生大量session对象,前端也不用存储一大堆cookie,token会存储在客户端的localStorage中,每次发送请求时,都将token以请求头的形式发送给服务器。不用session域的原因是当session特别多的时候就牵扯到高并发问题,对于Io流的读取就会造成巨大的性能损失。如图:
我们可以发现token是存储在客户端中的,而这样做可以大大减小后端服务器的压力。
以登录功能举例:(第一步)
这里要注意登录成功之后还会要将token口令给到客户端,而客户端下次发送任何请求时只需要判断本地token是否为空,而无需再次触发登录验证。能够实现的业务逻辑就是用户能够在规定时间内免登录直接访问新闻首页。
(第二步)
在登录成功之后页面右上角的“注册”“登录”将会变为“欢迎...登录”。后端通过解析token将id取出然后到数据库中找到对应数据,前端就会将返回的nickName存储到pinia中,然后通过前端页面绑定pinia即可将nickName显示到页面上。
token的相关工具类:
由于token是在后端生成的:(一共有三个方法,第一是生成token密文字符串,第二是解析token密文,第三是判断token的时效性是否到期),该工具类放在utils包下
import com.alibaba.druid.util.StringUtils;
import io.jsonwebtoken.*;
import java.util.Date;
public class JwtHelper {
private static long tokenExpiration = 1000*60*60; //定义token的过期时间
private static String tokenSignKey = "123456"; //需要知道这一段数据才能解析出token密文
//生成token字符串
public static String createToken(Long userId ) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId) //返回的密文信息有userId
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
//从token字符串获取userid,即从密文形式解析出userId
public static 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 static 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;
}
}
}
token的相关前端代码:
1,token-utils.ts中先得到token:
const TokenKey = 'vue_admin_template_token'
export function getToken() {
return localStorage.getItem(TokenKey)
}
export function setToken(token: string) {
localStorage.setItem(TokenKey, token)
}
export function removeToken() {
localStorage.removeItem(TokenKey)
}
2,userInfo.js中的pinia共享数据来得到token:
import { getToken, removeToken, setToken } from '../utils/token-utils';
export const useUserInfoStore = defineStore('userInfo', {
state: () => ({
token: getToken(),
nickName: '',
uid: '',
}),
3,request.js来判断token是否为空,即添加一个请求拦截器:
// 添加请求拦截器
service.interceptors.request.use((config) => {
NProgress.start()//开启进度条
// 如果有token, 通过请求头携带给后台
const userInfoStore = useUserInfoStore(pinia) // 如果不是在组件中调用,必须传入pinia
const token = userInfoStore.token
if (token) {
// config.headers['token'] = token // 报错: headers对象并没有声明有token, 不能随便添加
(config.headers)['token'] = token
}
return config;
});
4,请求成功后, 取出token保存 pinia和local中
import { getLogin,getUserInfo } from '../api/index';
actions: {
// 登陆的异步action
async login (loginForm) {
// 发送登陆的请求
const result = await getLogin(loginForm)
// 请求成功后, 取出token保存 pinia和local中
const token = result.token
this.token = token
setToken(token)
},
5,以post形式向后端发送请求(api/index)
//登录的接口
export const getLogin = (info) => {
return request.post("user/login",info);
};
正式实现登录需求
登录功能看图可知后端接口需要有接收两个请求,一个是校验密码和用户名是否正确,一个是解析客户端发来的token。
处理登陆表单提交的业务接口实现
根据文档提供的路径可知在NewsUserController类中需要有一个login的方法用来接收POST请求形式发来的数据并且将数据发送给service层再到数据库中进行一系列操作。同时还需要验证如果数据库中确实存在该用户,那就需要给客户端创建一个token口令并返回表示登录成功。
同时还需要注意格式问题,在文档中可以看到token形式是:"token":"... ...",说明返回的token需要是键值对的形式,因此需要创建一个Map来封装。
代码实现:
/**
* 处理登陆表单提交的业务接口实现
*
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收用户名和密码
/*{
"username":"zahngsan", //用户名
"userPwd":"123456", //明文密码
*/
NewsUser paramUser = WebUtil.readJson(req, NewsUser.class);
//调用服务层方法实现登录
NewsUser loginUser = userService.findByUsername(paramUser.getUsername());
Result result = null;
if (null != loginUser) {
if (MD5Util.encrypt(paramUser.getUserPwd()).equalsIgnoreCase(loginUser.getUserPwd())) {
//返回的信息还需要有一个token,因此在这里创建一个token密文
Integer uid = loginUser.getUid();
String token = JwtHelper.createToken(uid.longValue());
//由于需要返回键值对形式的token,因此需要创建一个Map进行发送
//"token":"... ..."
Map data = new HashMap();
data.put("token", token);
result = Result.ok(data);
} else {
result = Result.build(null, ResultCodeEnum.PASSWORD_ERROR);
}
} else {
result = Result.build(null, ResultCodeEnum.USERNAME_ERROR);
}
//向客户端响应登录验证信息
WebUtil.writeJson(resp, result);
}
然后就是按照MVC设计模式到Service层,再从Service层到DAO层书写相关sql语句对数据库进行CRUD操作。
NewsUserServiceImpl:
@Override
public NewsUser findByUsername(String username) {
return userDao.findByUsername(username);
}
NewsUserDaoImpl:
@Override
public NewsUser findByUsername(String username) {
String sql = """
select
uid,
username,
user_pwd userPwd,
nick_name nickName
from
news_user
where
username=?
""";
//由于baseQueryObject是返回单个值的,而此处的需求是返回具体的对象,即使只有一个对象
List<NewsUser> newsUserList = baseQuery(NewsUser.class, sql, username);
return newsUserList != null && newsUserList.size() > 0 ? newsUserList.get(0) : null;
}
根据token口令获得用户信息的接口实现
这里主要是写对于请求头中的token进行的解析操作,判断完token不为空并且时效性还在之后会根据token中解析出来的id到数据库中寻找对应id并且把nickName返回显示在页面右上角原本是登录和注册按钮的位置。
返回形式以键值对形式返回。
注意这边有一个细节就是根据文档要将userPwd设置为空串,不然所有人都知道密码了,不符合业务逻辑。
NewsUserController:
/**
* 根据token口令获得用户信息的接口实现
*
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void getUserInfo(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求中的token
String token = req.getHeader("token");
Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);
if (null != token && (!"".equals(token))) { //判断是否为空串
if (!JwtHelper.isExpiration(token)) { //判断token是否过期
Integer userId = JwtHelper.getUserId(token).intValue();
NewsUser newsUser = userService.findByUid(userId);
if (null != newsUser) {
//通过校验 查询用户信息放入Result
Map data = new HashMap(); //依旧需要键值对形式
newsUser.setUserPwd(""); //需要将密码设置为空串进行返回
data.put("loginUser", newsUser);
result = Result.ok(data);
}
}
}
WebUtil.writeJson(resp, result);
}
然后依旧使用MVC模式到DAO层,我这边就直接演示DAO层代码了:(根据文档提供的格式将用户数据以List形式返回)
@Override
public NewsUser findByUid(Integer userId) {
String sql = """
select
uid,
username,
user_pwd userPwd,
nick_name nickName
from
news_user
where
uid=?
""";
//由于baseQueryObject是返回单个值的,而此处的需求是返回具体的对象,即使只有一个对象
List<NewsUser> newsUserList = baseQuery(NewsUser.class, sql, userId);
return newsUserList != null && newsUserList.size() > 0 ? newsUserList.get(0) : null;
}
注册需求的实现
注册用户的操作和校验用户是否已经被抢注的两个接口,前端先向后端发送校验请求(见api-index.js)然后然后再向后端发送注册的请求
根据文档相关信息可知,首先需要去数据库校验该注册的用户是否己经在数据库中存在了,执行checkUsername方法,如果返回的结果为空则说明数据库中没有该用户,代表可以注册,既然可以注册那就执行login方法进行注册。
NewsUserController:
/**
* 完成注册的业务接口
*
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void regist(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收JSON信息
NewsUser registUser = WebUtil.readJson(req, NewsUser.class);
//调用服务层方法
Integer rows = userService.registUser(registUser);
//根据存入是否成功处理响应值
Result result = Result.ok(null);
if (rows == 0) {
result = Result.build(null, ResultCodeEnum.USERNAME_USED);
}
WebUtil.writeJson(resp, result);
}
/**
* 校验用户名是否被占用的业务接口实现
*
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void checkUserName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取账号(以键值对形式接收)
String username = req.getParameter("username");
//根据用户名查询用户信息 找到了返回505 找不到就返回200(代表能够注册)
NewsUser newsUser = userService.findByUsername(username);
Result result = Result.ok(null); //先给result一个初始的默认值
if (null != newsUser) {
//如果返回的结果不是为空代表已经被抢注了
result = Result.build(null, ResultCodeEnum.USERNAME_USED);
}
WebUtil.writeJson(resp, result);
}
DAO层:
@Override
public NewsUser findByUsername(String username) {
String sql = """
select
uid,
username,
user_pwd userPwd,
nick_name nickName
from
news_user
where
username=?
""";
//由于baseQueryObject是返回单个值的,而此处的需求是返回具体的对象,即使只有一个对象
List<NewsUser> newsUserList = baseQuery(NewsUser.class, sql, username);
return newsUserList != null && newsUserList.size() > 0 ? newsUserList.get(0) : null;
}
registUser的service层需要执行将明文密码转为密文密码的操作,不能写在dao层中,
NewsUserServiceImpl:
@Override
public Integer registUser(NewsUser registUser) {
//处理增加数据的业务
//用户传来的信息需要将明文密码转换成密文密码
registUser.setUserPwd(MD5Util.encrypt(registUser.getUserPwd()));
return userDao.insertUser(registUser);
}
DAO层实现向数据库中新增数据的操作:
@Override
public Integer insertUser(NewsUser registUser) {
String sql = """
insert into news_user values (DEFAULT,?,?,?)
""";
return baseUpdate(sql,
registUser.getUsername(),
registUser.getUserPwd(),
registUser.getNickName()
);
}
分页查询
首先需要解释一下分页查询:即用户点击不同的新闻类型,在页面上会显示出对应新闻类型的新闻。并且用户如果使用关键词搜索也能在页面上显示出对应关键字的新闻。
细节:一页上可以显示多少条新闻可以修改。当点击下一页时,页面数会+1。若查询出来6条新闻记录,一页上最多显示5条记录,最后一条新闻就必须再单开一页到下一页上显示。
如上图使用F12之后显示出的数据:
keyWords:关键字查询,type:查询新闻的类型(体育?娱乐?... ...),
pageNum:查询出来的新闻数一共占多少页,pageSize:一页上最多显示多少条新闻。
如图文档所示为服务器响应回来的数据:
首先需要对文档中的各个参数进行分类:
根据请求参数,此时就要使用到vo中自己创建的实体类了
此处一共进行了两次Map的嵌套:
第一层就是外层的pageInfo为key,然后pageInfo中存储的各对象为value
第二层是内部的pageInfo,此时key为pageData, pageNum, pageSize... ...,pageData数组中存放的各页面属性为value(如新闻id,新闻标题等),pageNum后面的页码数... ...
//将参数传递给服务层进行分页查询
Map pageInfo = headlineService.findPage(headlineQueryVo);
Map data = new HashMap();
data.put("pageInfo", pageInfo);
PortalController:
protected void findNewsPage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收请求中的参数
HeadlineQueryVo headlineQueryVo = WebUtil.readJson(req, HeadlineQueryVo.class);
//将参数传递给服务层进行分页查询
/*
pageData:[
{
"hid":"1", // 新闻id
"title":"尚硅谷宣布 ... ...", // 新闻标题
"type":"1", // 新闻所属类别编号
"pageViews":"40", // 新闻浏览量
"pastHours":"3", // 发布时间已过小时数
"publisher":"1"
}
]
pageNum:1,
pageSize:1,
totalPage:1,
totalSize:1
*/
Map pageInfo = headlineService.findPage(headlineQueryVo);
Map data = new HashMap();
data.put("pageInfo", pageInfo);
//将分页查询的结果转换成json相应给客户端
WebUtil.writeJson(resp, Result.ok(data));
}
}
内层的map文档给我们提供了5个需要的参数:pageNum,pageSize,totalSize,totalSize,pageInfo。
NewsHeadlineServiceImpl:(注:解决了页数加一问题)
@Override
public Map findPage(HeadlineQueryVo headlineQueryVo) {
//这两条数据都是从客户端传来的信息,可以原封不动进行返回
int pageNum = headlineQueryVo.getPageNum();
int pageSize = headlineQueryVo.getPageSize();
//要将pageData数组中的对象做成一个集合,里面需要的实体类对象在Vo中已经创建完毕
List<HeadlinePageVo> pageData = headlineDao.findPageList(headlineQueryVo);
int totalSize = headlineDao.findPageCount(headlineQueryVo);
//totalPage的总页码数是根据totalSize和pageNum共同决定的,总页数如果可以被整除那就直接返回页数
//如果不能被整除那就把余下的新闻加一页显示到下一页上去
int totalPage = totalSize % pageSize == 0 ? totalSize/pageSize : totalSize/pageSize + 1;
//把上面五条数据放在一个大的Map中
Map pageInfo = new HashMap();
pageInfo.put("pageNum", pageNum);
pageInfo.put("pageSize", pageSize);
pageInfo.put("totalSize", totalSize);
pageInfo.put("totalPage", totalPage);
pageInfo.put("pageData", pageData);
return pageInfo;
}
findPageList的DAO层代码:
/* HeadlinePageVo中的各实体类(pageData)
private Integer hid;
private String title;
private Integer type;
private Integer pageViews;
private Long pastHours;
private Integer publisher;
剩余的四条数据
private String keyWords;
private Integer type ;
private Integer pageNum;
private Integer pageSize;
*/
@Override
public List<HeadlinePageVo> findPageList(HeadlineQueryVo headlineQueryVo) {
//需要准备一个集合用来装入拼接到sql语句最后的?中的参数,此处传来的是一个类对象而不是一个属性,因此需要集合进行封装
List params = new ArrayList();
//TIMESTAMPDIFF(HOUR,create_time,now())时间计算函数,返回当前时间减去发布时间的时间差,并且以小时为单位
String sql = """
select
hid,
title,
type,
page_views pageViews,
TIMESTAMPDIFF(HOUR,create_time,now()) pastHours,
publisher
from
news_headline
where
is_deleted = 0
""";
if (headlineQueryVo.getType() != 0) { //查询新闻类型,只要点击的是界面上微头条标题以外的其他类型新闻都能执行查询和页面跳转
sql = sql.concat(" and type = ? ");
params.add(headlineQueryVo.getType());
}
//搜索框中输入的不是空串,也会到数据库中去进行查询并返回关键字对应的新闻
if (headlineQueryVo.getKeyWords() != null && !headlineQueryVo.getKeyWords().equals("")) {
sql = sql.concat(" and title like ? "); //使用了模糊查询
params.add("%" + headlineQueryVo.getKeyWords() + "%");
}
//根据发布的时间进行升序排列,如果发布时间相同再根据浏览量进行降序排列
sql = sql.concat(" order by pastHours ASC, page_views DESC ");
//由于是分页查询,因此还涉及到目标新闻的前面已经过去了多少条新闻,以及距离目标新闻页面还要取多少条新闻才能到目标页
sql = sql.concat(" limit ? , ? ");
params.add((headlineQueryVo.getPageNum() - 1) * headlineQueryVo.getPageSize());
params.add(headlineQueryVo.getPageSize());
return baseQuery(HeadlinePageVo.class, sql, params.toArray());
}
需要进行新闻类型与关键词搜索进行if语句的判断对sql语句进行适当的拼接然后到数据库中进行对应查找。需要注意的细节是新闻是讲究一个发布时间,浏览量的排序的,因此也需要将发布的时间进行升序排列,如果发布时间相同再根据浏览量进行降序排列。
代码解析:
分页查询问题://由于是分页查询,因此还涉及到目标新闻的前面已经过去了多少条新闻,以及距离目标新闻页面还要取多少条新闻才能到目标页
sql = sql.concat(" limit ? , ? ");
params.add((headlineQueryVo.getPageNum() - 1) * headlineQueryVo.getPageSize());
params.add(headlineQueryVo.getPageSize());
这段代码在我自己理解的时候也有一些模糊不清,我会尽量以举例的方式解释清楚:
这段代码的作用是根据用户请求的页码和每页显示的记录数,对查询结果进行分页,返回指定页码的数据。
举例:假设 headlineQueryVo.getPageNum() = 2 (新闻到达第二页)且 headlineQueryVo.getPageSize() = 10(每页只能显示10条新闻),那么分页查询的 limit 子句将被解释为 LIMIT 10, 10,表示从第11条记录开始,返回接下来的10条记录(即需要显示下一页上的新闻),如果剩余记录数不足10条那就将剩余记录全部返回。
还需要的一个是totalSize,即一共通过搜索返回了多少条新闻,只有获得了新闻的总数才能对新闻进行分页操作。
findPageCount:(代码与上面的分页查询返回对应新闻的代码相似)
@Override
public int findPageCount(HeadlineQueryVo headlineQueryVo) {
//需要准备一个集合用来装入拼接到sql语句最后的?中的参数,此处传来的是一个类对象而不是一个属性,因此需要集合进行封装
List params = new ArrayList();
//TIMESTAMPDIFF(HOUR,create_time,now())时间计算函数,返回当前时间减去发布时间的时间差,并且以小时为单位
String sql = """
select
count(1)
from
news_headline
where
is_deleted = 0
""";
if (headlineQueryVo.getType() != 0) { //查询新闻类型,只要点击的是界面上微头条标题以外的其他类型新闻都能执行查询和页面跳转
sql = sql.concat(" and type = ? ");
params.add(headlineQueryVo.getType());
}
//搜索框中输入的不是空串,也会到数据库中去进行查询并返回关键字对应的新闻
if (headlineQueryVo.getKeyWords() != null && !headlineQueryVo.getKeyWords().equals("")) {
sql = sql.concat(" and title like ? "); //使用了模糊查询
params.add("%" + headlineQueryVo.getKeyWords() + "%");
}
Long count = baseQueryObject(Long.class, sql, params.toArray());
return count.intValue();
}
查看头条详情
不仅要做到查看全文,还要做到当前浏览量+1,并且还涉及到sql的多表查询问题。
根据文档注释的参数可知要调用的是HeadlineDetailVo实体类,并且需要获得用户的hid
PortalController层:
/**
* 查看头条全文的接口实现
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void showHeadlineDetail(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收要查询的头条的hid
int hid = Integer.parseInt(req.getParameter("hid"));
//调用服务层完成查询处理
HeadlineDetailVo headlineDetailVo = headlineService.findHeadlineDetail(hid);
//将查询到的信息响应给客户端
Map data = new HashMap();
data.put("headline", headlineDetailVo);
WebUtil.writeJson(resp, Result.ok(data));
}
要做到当前浏览量+1和查询头条详情需要分别到dao层中去实现
NewsHeadlineServiceImpl:
@Override
public HeadlineDetailVo findHeadlineDetail(int hid) {
//修改该头条的浏览量 +1
headlineDao.incrPageViews(hid);
//查询头条的详情
return headlineDao.findHeadlineDetail(hid);
}
dao层中浏览量+1:
@Override
public int incrPageViews(int hid) {
String sql = "update news_headline set page_views = page_views+1 where hid = ?";
return baseUpdate(sql, hid);
}
查询头条详情需要使用sql语句的多表查询:(即将多个表格中一样的属性作为条件拼接多张表格)设计语法有left,其含义为左连接。
@Override
public HeadlineDetailVo findHeadlineDetail(int hid) {
/* 需要返回的实体类
private Integer hid;
private String title;
private String article;
private Integer type;
private String typeName;
private Integer pageViews;
private Long pastHours;
private Integer publisher;
private String author;
*/
//此处涉及多表查询问题
String sql = """
select
h.hid hid ,
h.title title ,
h.article article ,
h.type type ,
t.tname typeName ,
h.page_views pageViews ,
TIMESTAMPDIFF(HOUR,create_time,now()) pastHours ,
h.publisher publisher ,
u.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
h.hid = ?
""";
List<HeadlineDetailVo> list = baseQuery(HeadlineDetailVo.class, sql, hid);
return null != list && list.size() > 0 ? list.get(0) : null;
}
用户对自己发布的新闻进行增删改业务
登录校验:
在修改自己发布的新闻之前需要先向服务端发送一个是否还处在登陆状态的请求(即:token有没有过期),如果token已经过期则无法完成修改。
NewsUserController:
/**
* 前端自己校验是否失去登录状态的接口
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void checkLogin(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String token = req.getHeader("token");
Result result = Result.build(null, ResultCodeEnum.NOTLOGIN);
if(null != token){
if(!JwtHelper.isExpiration(token)){
result = Result.ok(null);
}
}
WebUtil.writeJson(resp, result);
}
注意:但是会有一个问题就是后端留好了这个校验是否处在登录状态的接口,但是前端可能不向这个接口发请求,因此必须做一个过滤器对请求进行拦截。登录过滤器是不可以把所有的请求都拦截的,只需要拦截NewsHeadlineController下的请求,因此需要注解配置。如果拦在PortalController和NewsUserController那就会变成必须要登录才能查看新闻,不符合业务需求。因此拦在NewsHeadlineController。
LoginFilter:
import com.ryy.headline.common.Result;
import com.ryy.headline.common.ResultCodeEnum;
import com.ryy.headline.util.JwtHelper;
import com.ryy.headline.util.WebUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter("/headline/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = request.getHeader("token");
boolean flag = null != token && (!JwtHelper.isExpiration(token));
if(flag){ //如果还在登陆状态就放行到Controller层继续执行接下去的代码
filterChain.doFilter(servletRequest,servletResponse);
}else{
WebUtil.writeJson(response, Result.build(null, ResultCodeEnum.NOTLOGIN));
}
}
}
修改头条并回显:
即用户点击修改之后需要向后端发送一个请求返回对应的头条详情并进行修改。
/**
* 修改头条回显业务接口
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void findHeadlineByHid(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Integer hid = Integer.parseInt(req.getParameter("hid"));
NewsHeadline headline = headlineService.findByHid(hid);
//注意这里前端要求的返回格式是键值对形式
Map data = new HashMap();
data.put("headline", headline);
WebUtil.writeJson(resp, Result.ok(data));
}
DAO层(NewsHeadlineDaoImpl)中得sql:
public NewsHeadline findByHid(Integer hid) {
String sql = """
select
hid,
title,
article,
type,
publisher,
page_views pageViews,
create_time createTime,
update_time updateTime,
is_deleted isDeleted
from
news_headline
where
hid = ?
""";
List<NewsHeadline> list = baseQuery(NewsHeadline.class, sql, hid);
return null != list && list.size() > 0 ? list.get(0) : null;
}
保存修改业务:
/**
* 修改头条并保存进入数据库的接口实现
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
NewsHeadline newsHeadline = WebUtil.readJson(req, NewsHeadline.class);
headlineService.update(newsHeadline);
WebUtil.writeJson(resp, Result.ok(null));
}
DAO层的sql:
@Override
public int update(NewsHeadline newsHeadline) {
String sql = """
update
news_headline
set
title = ?,
article = ?,
type = ?,
update_time = now()
where
hid = ?
""";
return baseUpdate(sql,
newsHeadline.getTitle(),
newsHeadline.getArticle(),
newsHeadline.getType(),
newsHeadline.getHid()
);
}
删除业务:
最后的删除业务实际上是一个修改状态的操作,即把is_deleted属性设置为1,因为前面查询新闻的这些业务逻辑都是查is_deleted=0的新闻。
这里提供dao层写法:
@Override
public int removeByHid(int hid) {
String sql = "update news_headline set is_deleted = 1 where hid = ?";
return baseUpdate(sql, hid);
}