微服务实战项目 —— 知识分享应用(一)
微服务实战项目 —— 知识分享应用(二)
投稿功能
内容中心 domain/dto 新建 ShareSubmitDTO 类,用来封装投稿数据对象
@Data
public class ShareSubmitDTO {
private Long userId;
private String author;
/**
* 标题
*/
private String title;
/**
* 是否原创 true:是
*/
private Boolean isOriginal;
/**
* 封面
*/
private String cover;
/**
* 概要信息
*/
private String summary;
/**
* 价格(需要的积分)
*/
private Integer price;
/**
* 下载地址
*/
private String downloadUrl;
}
ShareService 中新增投稿方法,入参为 ShareSubmitDTO 对象,返回受影响的记录条数(1 或 0)
/**
* 投稿
*
* @param shareSubmitDTO dto
* @return int
*/
public int contribute(ShareSubmitDTO shareSubmitDTO) {
Share share = Share.builder()
.id(SnowUtil.getSnowflakeNextId())
.userId(shareSubmitDTO.getUserId())
.title(shareSubmitDTO.getTitle())
.createTime(new Date())
.updateTime(new Date())
.isOriginal(shareSubmitDTO.getIsOriginal())
.author(shareSubmitDTO.getAuthor())
.cover(shareSubmitDTO.getCover())
.summary(shareSubmitDTO.getSummary())
.price(shareSubmitDTO.getPrice())
.downloadUrl(shareSubmitDTO.getDownloadUrl())
.buyCount(0)
.showFlag(false)
.auditStatus("NOT_YET")
.reason("暂未审核")
.build();
return shareMapper.insert(share);
}
ShareController 新增投稿接口
@PostMapping("contribute")
public CommonResp<Integer> contribute(@RequestBody ShareSubmitDTO shareSubmitDTO,
@RequestHeader(value = "token") String token) {
shareSubmitDTO.setUserId(getUserIdFromToken(token));
return CommonResp.success(shareService.contribute(shareSubmitDTO));
}
提交:投稿功能
我的投稿列表
ShareService 新增查询“我的投稿”方法(带分页)
/**
* 分页查询我的投稿
*
* @param userId userId
* @param pageSize pageSize
* @param pageNo pageNo
* @return {@link List}<{@link Share}>
*/
public List<Share> myContributeList(Long userId, Integer pageSize, Integer pageNo) {
LambdaQueryWrapper<Share> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Share::getUserId, userId);
Page<Share> page = Page.of(pageNo, pageSize);
return shareMapper.selectList(page, wrapper);
}
ShareController 新增查询我的投稿接口
@GetMapping("contributeList")
public CommonResp<List<Share>> getContributeList(
@RequestParam(required = false, defaultValue = "1") Integer pageNo,
@RequestParam(required = false, defaultValue = "5") Integer pageSize,
@RequestHeader(value = "token") String token
) {
if (pageSize > MAX) {
pageSize = MAX;
}
return CommonResp.success(shareService.myContributeList(getUserIdFromToken(token), pageSize, pageNo));
}
提交:我的投稿功能
管理员待审核分享列表
ShareService 新增查询待审核状态分享列表方法
/**
* 分页查询待审核列表
*
* @param pageSize pageSize
* @param pageNo pageNo
* @return {@link List}<{@link Share}>
*/
public List<Share> notPassShareList(Integer pageSize, Integer pageNo) {
LambdaQueryWrapper<Share> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Share::getAuditStatus, AuditStatusEnum.NOT_YET).eq(Share::getShowFlag, false);
Page<Share> page = Page.of(pageNo, pageSize);
return shareMapper.selectList(page, wrapper);
}
新建 ShareAdminController 类,用来放管理员相关接口,编写 /share/admin/list 接口,用来返回待管理员审核的资源列表:
@RestController
@RequestMapping("share/admin")
public class ShareAdminController {
@Resource
private ShareService shareService;
@GetMapping("list")
public CommonResp<List<Share>> getSharesNotYet(@RequestParam(defaultValue = "1", required = false) Integer pageNo,
@RequestParam(defaultValue = "3", required = false) Integer pageSize,
@RequestHeader(value = "token") String token) {
String role = JwtUtil.getJSONObject(token).getStr("roles");
if (!"admin".equals(role)) {
return CommonResp.error("无权限");
}
return CommonResp.success(shareService.notPassShareList(pageSize, pageNo));
}
}
提交:查询管理员待审核分享列表
管理员审核分享功能
安装 RocktMQ
参考博客:使用docker安装RocketMQ
注意:修改博客中两处主机地址,并且打开四个端口:9999,10909,10911,9876
后端实现
RocketMQ 消息队列依赖,父项目管理版本,然后在公共模块引入
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
用户中心和内容中心都编写一下 RocketMQ 的配置
主要配置了消息队列的 broker 服务器地址和端口,以及生产者的分组
rocketmq:
name-server: IP:PORT
producer:
# 必须指定 group
group: test-group
准备结束,开始编写内容中心业务层的管理员审核分享功能
生产者-内容中心
新建审核状态枚举类 AuditStatusEnum :
@Getter
@AllArgsConstructor
public enum AuditStatusEnum {
PASS,
NOT_YET,
REJECTED
}
然后在 domain/dto 新建一个审核相关的 DTO 类:ShareAuditDTO,封装所示属性:
@Data
public class ShareAuditDTO {
private AuditStatusEnum auditStatusEnum;
private String reason;
private Boolean showFlag;
}
ShareService 中新增审核方法
/**
* 审核
*
* @param shareId id
* @param shareAuditDTO dto
* @return {@link Share}
*/
@Transactional(rollbackFor = Exception.class)
public Share auditById(Long shareId, ShareAuditDTO shareAuditDTO) {
// 查看share是否存在
Share share = shareMapper.selectById(shareId);
if (share == null) {
throw new IllegalArgumentException("非法参数,资源不存在");
}
if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
throw new IllegalArgumentException("非法参数,该分享已被审核");
}
// 审核资源
share.setAuditStatus(shareAuditDTO.getAuditStatusEnum().toString());
share.setReason(shareAuditDTO.getReason());
share.setShowFlag(shareAuditDTO.getShowFlag());
shareMapper.updateById(share);
// 向 关联表 插入数据
midUserShareMapper.insert(MidUserShare.builder()
.id(SnowUtil.getSnowflakeNextId())
.userId(share.getUserId())
.shareId(shareId)
.build());
// 如果是 PASS 那么发送消息给 mq,让用户中心去消费,并添加积分
if (AuditStatusEnum.PASS.equals(shareAuditDTO.getAuditStatusEnum())) {
rocketMQTemplate.convertAndSend("add-bonus", UpdateBonusMqDTO.builder()
.userId(share.getUserId())
.bonus(50)
.build());
}
return share;
}
其中需要一个 DTO 支持,用于和消息队列传输数据:
@Data
@Builder
public class UpdateBonusMqDTO {
private Integer bonus;
private Long userId;
}
启动失败,报错
解决方案:
在内容中心模块下 resources 目录下创建:META_INF / spring / org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,并写入内容:org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
,观察文件图标变化,成功启动。参考博客:Spring boot 3.0整合RocketMQ及不兼容的问题
进行http测试
测试通过,查看RocketMQ控制台,观察到一条消息
生产者的代码实现成功,接下来开始写消费者的代码
消费者-用户中心
用户中心不需要主动调用,写一个监听器,只要消息队列有了某个用户的积分,就会通知他去消费加分
用户中心新建 rocketmq 子包,新建 AddBonusListener 监听类,如下
@Service
@RocketMQMessageListener(consumerGroup = "consumer", topic = "add-bonus")
public class AddBonusListener implements RocketMQListener<UpdateBonusMqDTO> {
@Resource
private UserMapper userMapper;
@Resource
private BonusEventLogMapper bonusEventLogMapper;
@Override
public void onMessage(UpdateBonusMqDTO updateBonusMqDTO) {
// 1. 给用户加积分
Long userId = updateBonusMqDTO.getUserId();
User user = userMapper.selectById(userId);
user.setBonus(user.getBonus() + updateBonusMqDTO.getBonus());
userMapper.updateById(user);
// 2. 写入日志
bonusEventLogMapper.insert(BonusEventLog.builder()
.id(SnowUtil.getSnowflakeNextId())
.userId(updateBonusMqDTO.getUserId())
.value(updateBonusMqDTO.getBonus())
.event("CONTRIBUTE")
.createTime(new Date())
.description("投稿加积分")
.build());
}
}
别忘了在resources目录下添加配置以及配置文件
重启服务,可以看到用户中心已经监听到,并且已经增加了积分,日志表也插入了日志
提交:管理员审核实现
我的兑换
我的兑换列表需要查询到 MidUserShare 中间表的数据后再查询 share 的具体信息,ShareService 创建方法
/**
* 我的兑换
*
* @param userId id
* @param pageSize size
* @param pageNo no
* @return {@link List}<{@link Share}>
*/
public List<Share> getMyExchange(Long userId, Integer pageSize, Integer pageNo) {
LambdaQueryWrapper<MidUserShare> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MidUserShare::getUserId, userId);
// 中间表数据,收集分享id
List<Long> idList = midUserShareMapper.selectList(Page.of(pageNo, pageSize), wrapper).stream().map(MidUserShare::getShareId).toList();
// 根据id批量获取share数据
return shareMapper.selectBatchIds(idList);
}
ShareController 实现我的兑换接口实现
@GetMapping("myExchangeList")
public CommonResp<List<Share>> getExchangeList(
@RequestParam(required = false, defaultValue = "1") Integer pageNo,
@RequestParam(required = false, defaultValue = "5") Integer pageSize,
@RequestHeader(value = "token") String token
) {
if (pageSize > MAX) {
pageSize = MAX;
}
return CommonResp.success(shareService.getMyExchange(getUserIdFromToken(token), pageSize, pageNo));
}
提交:我的兑换接口
积分明细
积分明细列表只需要简单的分页查询积分日志表,UserService 实现 userBonusLog 方法
/**
* 积分明细
*
* @param userId id
* @param pageSize size
* @param pageNo no
* @return {@link List}<{@link BonusEventLog}>
*/
public List<BonusEventLog> userBonusLog(Long userId, Integer pageSize, Integer pageNo) {
Page<BonusEventLog> page = Page.of(pageNo, pageSize);
LambdaQueryWrapper<BonusEventLog> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(BonusEventLog::getUserId, userId);
return bonusEventLogMapper.selectList(page, wrapper);
}
UserController 调用 userBonusLog 方法实现积分明细接口
@GetMapping("bonus")
public CommonResp<List<BonusEventLog>> getUserBonusLog(@RequestParam(defaultValue = "1", required = false) Integer pageNo,
@RequestParam(defaultValue = "10", required = false) Integer pageSize,
@RequestHeader(value = "token") String token) {
return CommonResp.success(userService.userBonusLog(JwtUtil.getJSONObject(token).getLong("id"), pageSize, pageNo));
}
由于使用的分页查询,记得在 config 包中添加 MybatisPlus 的分页器配置
提交:积分明细列表
签到
签到功能的实现关键在于判断当前用户今日是否登录过,UserService 实现 dailyCheck 方法
/**
* 签到
*
* @param userId id
*/
@Transactional(rollbackFor = Exception.class)
public void dailyCheck(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new IllegalArgumentException("用户异常");
}
LambdaQueryWrapper<BonusEventLog> wrapper = new LambdaQueryWrapper<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
// 构造开始时间
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
Date start = calendar.getTime();
// 构造结束时间
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
Date end = calendar.getTime();
// 构造查询条件,用户今日是否有签到的积分日志记录
wrapper.eq(BonusEventLog::getUserId, userId).eq(BonusEventLog::getEvent, "DAILY_CHECK").between(BonusEventLog::getCreateTime, start, end);
// 查询今日是否签到
BonusEventLog bonusEventLog = bonusEventLogMapper.selectOne(wrapper);
if (bonusEventLog != null) {
throw new BusinessException(BusinessExceptionEnum.ALREADY_HAS_CHECK);
}
// 签到成功,插入数据
bonusEventLogMapper.insert(BonusEventLog.builder()
.id(SnowUtil.getSnowflakeNextId())
.userId(userId)
.value(10)
.event("DAILY_CHECK")
.description("签到")
.createTime(new Date())
.build());
// 添加积分
user.setBonus(user.getBonus() + 10);
userMapper.updateById(user);
}
UserController 简单调用实现签到接口
@PostMapping("check")
public CommonResp<Object> dailyCheck(@RequestHeader(value = "token") String token) {
userService.dailyCheck(JwtUtil.getJSONObject(token).getLong("id"));
return CommonResp.success();
}
提交:签到