# 第五章 自媒体文章发布
目标
- 完成自媒体文章列表查询功能
- 完成自媒体文章的发布功能
- 完成自媒体文章的查询
- 完成自媒体文章的删除功能
- 完成自媒体文章的上下架功能功能
1 自媒体文章列表查询
1.1 需求分析
如图所示:
需要展示自媒体发布的文章列表,并实现分页查询展示,而且需要根据关键字(文章的标题)、频道列表 和发布日期范围来进行分页查询
1.2 表结构
wm_news 自媒体文章表
频道表:
1.3 功能实现
1.3.1 自媒体文章分页查询
1.3.1.1 思路分析
页面点击搜索按钮之后 将选中的条件作为请求体传递给后台,后台根据条件进行分页查询即可
请求路径:/search
参数:分页条件封装对象
返回值:分页结果返回对象
1.3.1.2 功能实现
(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());
}
}
1.3.2.3 测试
1.3.2 频道列表查询
1.3.2.1 思路分析
实际上频道列表 需要在页面展示下拉框我们可以直接使用现有的admin微服务中的查询所有的频道列表即可,因为数据量不大,可以直接列出来即可,然后通过自媒体网关路由转发到admin微服务即可。
1.3.2.2 功能实现
目前已经实现了在抽象类中,所以直接关联网关即可。
网关配置:
1.3.2.3 测试
为了测试方便直接在本地访问路径,(当然也可以结合网关进行测试)
1.3.3 网关路由规则配置
结合网关测试:
(1)先启动微服务 和网关 (自媒体微服务 admin微服务 自媒体网关)登录
(2)测试获取频道列表: token从上一节登录之后获取
(3)测试文章分页列表查询:
3 自媒体文章-发布、修改,保存草稿
3.1 需求分析
总体上的需求:
原型:需求可以参考原型,但是会有稍微的变化
项目原型-HTML(使用火狐浏览器打开)/[原型图]_前台_黑马头条_V1.0/index.html#g=1
弹出窗口的图如下:有两种:1 选择现有素材作为 2 重新上传图片
(图3)
流程说明如下:
1.点击发布文章,添加标题 添加内容
2.添加内容有两种 一种是 纯文本 一种是是图片
3.点击文本的时候 弹出窗口直接添加文本 点击确定即可
4.点击图片的时候 弹出窗口 如上图片 图三表示 可以从素材库里面选择一张图,或者自己直接上传一张图 作为内容的一张图片
5.选择标签 频道 和发布定时时间
6.选择封面 选择单图 或者 多图 或者无图或者自动 需要弹出窗口图三那里进行 选择或者上传 多图需要上传3张
7.点击保存草稿或者直接提交
3.2 思路分析
涉及到的表如下:
思路分析:
这个其实也很简单。
发布文章的本质就是像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 提交(待审核)..... 这个字段前端不必传递
3.3 功能实现
(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)测试:
先启动网关和自媒体微服务 并先登录
登录好了之后进行添加保存的操作:
测试数据参考思路分析里面的请求体数据。
3.4 优化
当保存之后需要将生成的主键返回
操作如下:
实现类中 修改如下:
接口修改:
controller修改如下:
4 自媒体文章-根据id查询
4.1 需求分析
点击修改的时候,就是根据文章id查询,跳转至编辑页面进行展示
4.2 思路分析
点击编辑 先根据文章的ID 获取到文章的数据,要注意的是,需要返回的数据不是数据库对应的实体对象,而是刚才我们定义的dto的对象。因为编辑的时候需要用到该数据
4.3 功能实现
(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)测试 通过网关测试(注意:测试也可以不用通过网关)
5 自媒体文章-删除
5.1 需求分析
5.2 思路分析
当文章状态为9 并且已上架的数据 不能删除。 如果是其他的状态可以删除。
删除之后需要同步数据到 APP文章中,该状态待做。
前端发送请求到后台 后台做逻辑判断处理即可
5.3 功能实现
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();
}
6 自媒体文章-上架、下架
6.1 需求分析
6.2 思路分析
当前已经发布(状态为9)的文章可以上架(enable = 1),也可以下架(enable = 0)
在上架和下架操作的同时,需要同步app端的文章配置信息,暂时不做,后期讲到审核文章的时候再优化
6.3 功能实现
修改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();
}
7 优化登录和续约(扩展)
7.1 思路
以自媒体微服务为例
1.先登录。产生两个令牌 一个令牌为访问令牌 一个为刷新令牌
2.访问令牌 访问时 当过期之后,返回状态码403 表示需要刷新令牌
3.用户再发送请求,将之前产生的刷新令牌 传递到后台,后台校验通过之后,返回新的两个令牌
7.2 实现
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();
}
修改 发布文章的代码:
7.3 测试
测试双令牌登录:
(1)为了测试过期 我们修改过期时间为30S
(2)测试登录
测试携带令牌访问:
(3)等过30S的时候再刷新令牌: