如图所示:
需要展示自媒体发布的文章列表,并实现分页查询展示,而且需要根据关键字(文章的标题)、频道列表 和发布日期范围来进行分页查询
wm_news 自媒体文章表
频道表:
页面点击搜索按钮之后 将选中的条件作为请求体传递给后台,后台根据条件进行分页查询即可 请求路径:/search 参数:分页条件封装对象 返回值:分页结果返回对象
(1)创建dto来接收页面传递的数值
@Data @Getter @Setter public class WmNewsDto extends WmNews { private LocalDateTime startTime; private LocalDateTime endTime; }
(2)修改controller进行分页查询
@PostMapping("/searchPage") public Result<PageInfo<WmNews>> findByPageDto(@RequestBody PageRequestDto<WmNewsDto> pageRequestDto){ PageInfo<WmNews> pageInfo = wmNewsService.findByPageDto(pageRequestDto); return Result.ok(pageInfo); }
service:
@Service public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService { @Override public PageInfo<WmNews> findByPageDto(PageRequestDto<WmNewsDto> pageRequestDto) { //select * from wm_news where status=? and title=? and user_id=当前的用户的ID and channl_id=? and publishedTime between ? and ? limit 0,10 //1.获取请求当前的页码 和每页显示的行 Long page = pageRequestDto.getPage(); Long size = pageRequestDto.getSize(); //2.获取请求体对象 WmNewsDto body = pageRequestDto.getBody(); QueryWrapper<WmNews> queryWrapper = new QueryWrapper<>(); //添加查询当前用户的文章的列表 String userInfo = RequestContextUtil.getUserInfo(); queryWrapper.eq("user_id",Integer.valueOf(userInfo)); //3.判断 是否为空 如果不为空 则拼接查询条件 if(body!=null){ if(!StringUtils.isEmpty(body.getStatus())){ queryWrapper.eq("status",body.getStatus()); } if(!StringUtils.isEmpty(body.getTitle())){ queryWrapper.like("title",body.getTitle()); } if(!StringUtils.isEmpty(body.getChannelId())){ queryWrapper.eq("channel_id",body.getChannelId()); } if(!StringUtils.isEmpty(body.getStartTime()) && !StringUtils.isEmpty(body.getEndTime())){ queryWrapper.between("publish_time", body.getStartTime(),body.getEndTime()); } } //4.执行分页查询 IPage<WmNews> page1 = new Page<WmNews>(page,size); IPage<WmNews> page2 = page(page1, queryWrapper); //5.封装结果 return new PageInfo<WmNews>(page2.getCurrent(),page2.getSize(), page2.getTotal(),page2.getPages(),page2.getRecords()); } }
实际上频道列表 需要在页面展示下拉框我们可以直接使用现有的admin微服务中的查询所有的频道列表即可,因为数据量不大,可以直接列出来即可,然后通过自媒体网关路由转发到admin微服务即可。
目前已经实现了在抽象类中,所以直接关联网关即可。
网关配置:
为了测试方便直接在本地访问路径,(当然也可以结合网关进行测试)
结合网关测试:
(1)先启动微服务 和网关 (自媒体微服务 admin微服务 自媒体网关)登录
(2)测试获取频道列表: token从上一节登录之后获取
(3)测试文章分页列表查询:
总体上的需求:
原型:需求可以参考原型,但是会有稍微的变化
项目原型-HTML(使用火狐浏览器打开)/[原型图]_前台_黑马头条_V1.0/index.html#g=1
弹出窗口的图如下:有两种:1 选择现有素材作为 2 重新上传图片
(图3)
流程说明如下:
1.点击发布文章,添加标题 添加内容 2.添加内容有两种 一种是 纯文本 一种是是图片 3.点击文本的时候 弹出窗口直接添加文本 点击确定即可 4.点击图片的时候 弹出窗口 如上图片 图三表示 可以从素材库里面选择一张图,或者自己直接上传一张图 作为内容的一张图片 5.选择标签 频道 和发布定时时间 6.选择封面 选择单图 或者 多图 或者无图或者自动 需要弹出窗口图三那里进行 选择或者上传 多图需要上传3张 7.点击保存草稿或者直接提交
涉及到的表如下:
思路分析:
这个其实也很简单。 发布文章的本质就是像wm_news文章表进行插入一条记录而已。 这里比较特殊的点在于 可以选择素材 所以需要弹出窗口查询素材 并选中之后将素材对应的图片的路径作为参数请求 传递给后台保存到mw_news文章表中存储起来 还有就是封面的无图 单图 自动 需要进行判断,自动的时候,需要判断当前的内容中是否有图片,如图有图片,则判断有几张,如果小于2 则作为单图 如果小于1 则作为无图,如果大于2 则作为多图即可。
实现步骤:
实现弹窗 获取素材列表--》 【已经实现】 直接调用分页搜索的请求路径即可 实现上传图片---》【已经实现】 直接调用dfs的请求路径即可 封面的单图 多图 选择图片上传--》就是弹窗功能已经实现
实现发表文章功能:
请求:/wmNews/save/{isSubmit} POST 参数:2个 是否为提交 isSubitm 值为 1 或者 0 1标识为提交 0 标识为保存草稿 请求体对象 返回值:result 成功与否即可
前端应当传递的请求体对象数据为如下案例:
{ "title": "黑马头条项目背景", "type": "1", "labels": "黑马头条", "publishTime": "2020-03-14T11:35:49.000Z", "channelId": 1, "images": [ "http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png" ], "status": 1, "content": [ { "type": "text", "value": "随着智能手机的普及,人们更加习惯于通过手机来看新闻。由于生活节奏的加快,很多人只能利用碎片时间来获取信息,因此,对于移动资讯客户端的需求也越来越高。黑马头条项目正是在这样背景下开发出来。黑马头条项目采用当下火热的微服务+大数据技术架构实现。本项目主要着手于获取最新最热新闻资讯,通过大数据分析用户喜好精确推送咨询新闻" }, { "type": "image", "value": "http://192.168.200.130/group1/M00/00/00/wKjIgl5swbGATaSAAAEPfZfx6Iw790.png" } ] }
解释 :
type : 指定为封面类型 0 是无图 1 是单图 3 是多图 -1 是自动 images: 指定为封面图片 以逗号分隔的图片路径 status: 自媒体文章的状态 0 保存草稿 1 提交(待审核)..... 这个字段前端不必传递
(1)创建dto 用来接收页面传递过来的请求体
@Data @Getter @Setter public class ContentNode { //type 指定类型 text 标识文本 image 标识 图片 private String type; //value 指定内容 private String value; }
@Data @Getter @Setter public class WmNewsDtoSave { //主键ID private Integer id; //文章标题 private String title; //图文内容 private List<ContentNode> content; //指定为封面类型 0 是无图 1 是单图 3 是多图 -1 是自动 private Integer type; //指定选中的频道ID private Integer channelId; //指定标签 private String labels; //状态 0 草稿 1 提交 待审核 (该字段可以不用设置,前端不必传递) private Integer status; //定时发布时间 private LocalDateTime publishTime; //封面图片 private List<String> images; }
(2)编写controller
//保存自媒体文章 保存草稿 和 添加 或者修改 @PostMapping("/save/{isSubmit}") public Result save(@PathVariable(name="isSubmit") Integer isSubmit,@RequestBody WmNewsDtoSave wmNewsDtoSave){ if(StringUtils.isEmpty(isSubmit) || wmNewsDtoSave==null){ return Result.errorMessage("数据不能为空"); } if(isSubmit>1 || isSubmit<0){ return Result.errorMessage("isSubmit的值有误"); } wmNewsService.save(wmNewsDtoSave,isSubmit); return Result.ok(); }
(3)编写service 实现类
@Autowired private WmNewsMapper wmNewsMapper; //保存自媒体文章信息 @Override public void save(WmNewsDtoSave wmNewsDtoSave, Integer isSubmit) { WmNews wmNews = new WmNews(); //copy数据 BeanUtils.copyProperties(wmNewsDtoSave, wmNews); //补充设置数据 //设置登录的用户ID wmNews.setUserId(Integer.valueOf(RequestContextUtil.getUserInfo())); //设置成JSON 字符串到数据库中 wmNews.setContent(JSON.toJSONString(wmNewsDtoSave.getContent())); //设置封面图片 将list 转成一个以逗号分隔的字符串 if (wmNewsDtoSave.getImages() != null && wmNewsDtoSave.getImages().size() > 0) { wmNews.setImages(String.join(",", wmNewsDtoSave.getImages())); } //如果是自动图 则判断 图文内容中的图片有多少张,如果是>2 则为多图 如果是1 则为单图 如果是小于1 则为 无图 if (wmNewsDtoSave.getType() == -1) { List<String> imagesFromContent = getImagesFromContent(wmNewsDtoSave); //说明是多图 if (imagesFromContent.size() > 2) { //设置为多图 wmNews.setType(3); //并设置图片 因为页面没有传递了 wmNews.setImages(String.join(",", imagesFromContent)); } else if (imagesFromContent.size() > 0 && imagesFromContent.size() <= 2) { //设置为单图 wmNews.setType(1); //设置图片为一张 wmNews.setImages(imagesFromContent.get(0)); } else { //无图 wmNews.setType(0); //空字符串 wmNews.setImages(""); } } //保存草稿或者提交审核 wmNews.setStatus(isSubmit); if (isSubmit == 1) { wmNews.setSubmitedTime(LocalDateTime.now()); } //修改数据 if (wmNewsDtoSave.getId() != null) { wmNewsMapper.updateById(wmNews); } else { //添加数据 wmNews.setCreatedTime(LocalDateTime.now()); wmNewsMapper.insert(wmNews); } } //获取图片路径列表 private List<String> getImagesFromContent(WmNewsDtoSave wmNewsDtoSave) { List<ContentNode> content = wmNewsDtoSave.getContent(); List<String> images = new ArrayList<String>(); for (ContentNode contentNode : content) { //图片 if (contentNode.getType().equals("image")) { String value = contentNode.getValue(); images.add(value); } } return images; }
(4)测试:
先启动网关和自媒体微服务 并先登录
登录好了之后进行添加保存的操作:
测试数据参考思路分析里面的请求体数据。
当保存之后需要将生成的主键返回
操作如下:
实现类中 修改如下:
接口修改:
controller修改如下:
点击修改的时候,就是根据文章id查询,跳转至编辑页面进行展示
点击编辑 先根据文章的ID 获取到文章的数据,要注意的是,需要返回的数据不是数据库对应的实体对象,而是刚才我们定义的dto的对象。因为编辑的时候需要用到该数据
(1)修改controller
@GetMapping("/one/{id}") public Result<WmNewsDtoSave> getById(@PathVariable(name="id")Integer id){ WmNewsDtoSave wmNewsDtoSave = wmNewsService.getDtoById(id); return Result.ok(wmNewsDtoSave); }
(2)service实现类
@Override public WmNewsDtoSave getDtoById(Integer id) { WmNews wmNews = wmNewsMapper.selectById(id); if(wmNews!=null){ WmNewsDtoSave wmNewsDtoSave = new WmNewsDtoSave(); BeanUtils.copyProperties(wmNews,wmNewsDtoSave); //设置内容 String content = wmNews.getContent(); List<ContentNode> contentNodes = JSON.parseArray(content, ContentNode.class); wmNewsDtoSave.setContent(contentNodes); //设置图片 String images = wmNews.getImages(); if(!StringUtils.isEmpty(images)){ //设置图片列表 wmNewsDtoSave.setImages(Arrays.asList(images.split(","))); } return wmNewsDtoSave; } return null; }
(3)测试 通过网关测试(注意:测试也可以不用通过网关)
当文章状态为9 并且已上架的数据 不能删除。 如果是其他的状态可以删除。 删除之后需要同步数据到 APP文章中,该状态待做。 前端发送请求到后台 后台做逻辑判断处理即可
controller 实现即可:
@Override @DeleteMapping("/{id}") public Result deleteById(@PathVariable(name = "id") Serializable id) { WmNews wmNews = wmNewsService.getById(id); if (wmNews == null) { return Result.errorMessage("不存在的文章"); } Integer enable = wmNews.getEnable(); Integer status = wmNews.getStatus(); //已发布 且上架 if (status == 9 && enable == 1) { return Result.errorMessage("已发布 且上架 不能删除"); } wmNewsService.removeById(id); return Result.ok(); }
当前已经发布(状态为9)的文章可以上架(enable = 1),也可以下架(enable = 0) 在上架和下架操作的同时,需要同步app端的文章配置信息,暂时不做,后期讲到审核文章的时候再优化
修改controller 添加一个方法进行上架和下架。
@PutMapping("/upOrDown/{id}/{enable}") public Result updateUpDown(@PathVariable(name = "id") Serializable id,@PathVariable(name="enable")Integer enable) { WmNews wmNews = wmNewsService.getById(id); if (wmNews == null) { return Result.errorMessage("不存在的文章"); } Integer status = wmNews.getStatus(); //已发布 且上架 if (status != 9) { return Result.errorMessage("文章没发布,不能上下架"); } if(enable>1 || enable<0){ return Result.errorMessage("错误的数字范围 只能是0,1"); } wmNews.setEnable(enable); wmNewsService.updateById(wmNews); return Result.ok(); }
以自媒体微服务为例
1.先登录。产生两个令牌 一个令牌为访问令牌 一个为刷新令牌 2.访问令牌 访问时 当过期之后,返回状态码403 表示需要刷新令牌 3.用户再发送请求,将之前产生的刷新令牌 传递到后台,后台校验通过之后,返回新的两个令牌
1 创建如下类,参考资料中的类直接copy过来
参考类如下:
2 修改登录方法
controller:
@PostMapping("/login") public Result<TokenJsonVo> login(@RequestBody LoginVo loginVo) throws LeadNewsException { TokenJsonVo tokenJsonVo = wmUserService.login(loginVo); return Result.ok(tokenJsonVo); }
VO所在位置:
@Data public class LoginVo { //登录用的用户名 private String username; //登录用的密码 private String password; }
service接口及实现类:
public TokenJsonVo login(LoginVo loginVo) throws LeadNewsException;
@Override public TokenJsonVo login(LoginVo loginVo) throws LeadNewsException { //1.校验数据是否为空 判断 如果为空直接报错 if (StringUtils.isEmpty(loginVo.getUsername()) || StringUtils.isEmpty(loginVo.getPassword())) { throw new LeadNewsException("用户名和密码不能为null"); } //2.要根据 用户名 获取 数据库中的用户的信息 判断 如果没有数据 直接报错 select * from ad_user where name=? QueryWrapper<WmUser> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", loginVo.getUsername()); WmUser wmUserFromDb = wmUserMapper.selectOne(queryWrapper); if (wmUserFromDb == null) { throw new LeadNewsException("用户名或密码错误"); } //3.根据 数据库中的密码(密文) 和 【页面传递过来的密码(明文)+salt--->md5加密之后的密文】 对比 如果不成功 报错 String passwordFromWeb = DigestUtils.md5DigestAsHex((loginVo.getPassword() + wmUserFromDb.getSalt()).getBytes()); String passwordFromDb = wmUserFromDb.getPassword(); if (!passwordFromDb.equals(passwordFromWeb)) { throw new LeadNewsException("用户名或密码错误"); } //4.生成令牌 组装数据返回 UserTokenInfo userTokenInfo = new UserTokenInfo(Long.valueOf(wmUserFromDb.getId()), wmUserFromDb.getImage(), wmUserFromDb.getNickname(), wmUserFromDb.getName(), TokenRole.ROLE_MEDIA); TokenJsonVo token = JwtUtil.createToken(userTokenInfo); return token; }
3 刷新令牌controller:
@PostMapping("/refreshToken") public Result refreshToken(@RequestBody Map<String, String> map) { String refreshToken = map.get("refreshToken"); UserTokenInfoExp userTokenInfoExp = null; try { userTokenInfoExp = JwtUtil.parseJwtUserToken(refreshToken); Long exp = userTokenInfoExp.getExp(); long now = System.currentTimeMillis(); long chazhi = exp - now; //续约的要求是: 必须在 访问令牌的过期时间点 到 刷新令牌的过期时间点 之间 防止 出现过久的令牌来恶意刷新令牌 if(chazhi>(JwtUtil.TOKEN_TIME_OUT*1000)){ return Result.errorMessage("令牌续约时间不在有效范围之内"); } } catch (Exception e) { return Result.errorMessage("令牌错误"); } if (JwtUtil.isExpire(userTokenInfoExp)) { return Result.errorMessage("令牌错误"); } TokenJsonVo token = JwtUtil.createToken(userTokenInfoExp); return Result.ok(token); }
4 网关过滤器实现
@Component public class AuthorizeFilter implements GlobalFilter, Ordered { //获取用户携带的token令牌 解析校验 校验通过放行 不通过 返回错误 @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1.获取请求对象 和 响应对象 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //1.5 如果请求的路径是 自媒体登录【白名单】 放行即可 String path = request.getURI().getPath(); if(path.startsWith("/media/wmUser/login") ||path.startsWith("/media/wmUser/refreshToken") || path.endsWith("v2/api-docs")){ return chain.filter(exchange); } //2.从请求头中获取访问令牌数据 String token = request.getHeaders().getFirst("token"); //3.判断 是否为空 如果为空 返回错误 401 if(StringUtils.isEmpty(token)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //4.校验令牌是否正确了 如果不是 返回错误 401 try { UserTokenInfoExp userTokenInfoExp = JwtUtil.parseJwtUserToken(token); if(!JwtUtil.isValidRole(userTokenInfoExp, TokenRole.ROLE_MEDIA)){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } //直接返回 表示需要续约 if(JwtUtil.isExpire(userTokenInfoExp)){ //403 response.setStatusCode(HttpStatus.FORBIDDEN); return response.setComplete(); } //URL编码 否则有乱码产生 String encode = URLEncoder.encode(JSON.toJSONString(userTokenInfoExp), "UTF-8"); //将信息传递给下游微服务 request.mutate().header(SystemConstants.USER_HEADER_NAME, encode); } catch (Exception e) { e.printStackTrace(); //直接错误 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } return chain.filter(exchange); } //值越低 优先级越高 优先被执行 @Override public int getOrder() { return -10; } }
5 工具类中修改原来的请求头获取用户ID的方法:
public class RequestContextUtil { /** * 获取访问令牌信息解析之后的信息 * * @return */ public static UserTokenInfoExp getRequestUserTokenInfo() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String json = null; try { //解码 json = URLDecoder.decode(request.getHeader(SystemConstants.USER_HEADER_NAME), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } UserTokenInfoExp userTokenInfoExp = JSON.parseObject(json, UserTokenInfoExp.class); return userTokenInfoExp; } //判断是否为匿名用户 public static boolean isAnonymous() { return TokenRole.ROLE_ANONYMOUS == getRequestUserTokenInfo().getRole(); } //获取用户ID值 public static Integer getUserId() { return getRequestUserTokenInfo().getUserId().intValue(); } }
修改原来添加素材的代码:
@Override @PostMapping public Result<WmMaterial> insert(@RequestBody WmMaterial record) {//该方法 叫:处理器handler Integer userId = RequestContextUtil.getUserId(); //设置User_id的值为当前登录的自媒体的用户的ID record.setType(0);//图片 record.setIsCollection(0); record.setCreatedTime(LocalDateTime.now()); //一定是当前登录自媒体的用户的ID record.setUserId(userId); wmMaterialService.save(record); return Result.ok(); }
修改 发布文章的代码:
测试双令牌登录:
(1)为了测试过期 我们修改过期时间为30S
(2)测试登录
测试携带令牌访问:
(3)等过30S的时候再刷新令牌: