feat(note): 实现笔记发布功能并优化数据模型

- 新增笔记发布接口,支持图文和视频类型
- 引入分布式ID生成器和KV存储服务
- 修改笔记、频道、话题等实体类使用LocalDateTime
- 添加频道-话题关联表及相应服务实现
- 更新数据库表结构,增加笔记内容UUID字段
- 完善笔记发布时的内容校验和异常处理
- 配置网关路由支持新的笔记服务路径
- 优化MyBatis Mapper扫描和Feign客户端配置
This commit is contained in:
Hanserwei
2025-10-08 19:37:35 +08:00
parent 665ea930fd
commit dd63d30792
32 changed files with 430 additions and 28 deletions

View File

@@ -1,9 +1,13 @@
package com.hanserwei.hannote.note.biz;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@MapperScan("com.hanserwei.hannote.note.biz.domain.mapper")
@EnableFeignClients(basePackages = "com.hanserwei.hannote")
public class HannoteNoteBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteNoteBizApplication.class, args);

View File

@@ -0,0 +1,29 @@
package com.hanserwei.hannote.note.biz.controller;
import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.note.biz.model.vo.PublishNoteReqVO;
import com.hanserwei.hannote.note.biz.service.NoteService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/note")
@Slf4j
public class NoteController {
@Resource
private NoteService noteService;
@PostMapping(value = "/publish")
@ApiOperationLog(description = "笔记发布")
public Response<?> publishNote(@Validated @RequestBody PublishNoteReqVO publishNoteReqVO) {
return noteService.publishNote(publishNoteReqVO);
}
}

View File

@@ -4,15 +4,18 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 频道表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_channel")
@@ -33,13 +36,13 @@ public class ChannelDO {
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
private LocalDateTime updateTime;
/**
* 逻辑删除(0未删除 1已删除)

View File

@@ -4,15 +4,18 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 频道-话题关联表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_channel_topic_rel")
@@ -39,11 +42,11 @@ public class ChannelTopicRelDO {
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
private LocalDateTime updateTime;
}

View File

@@ -4,15 +4,18 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 笔记表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_note")
@@ -63,7 +66,7 @@ public class NoteDO {
* 类型(0图文 1视频)
*/
@TableField(value = "`type`")
private Byte type;
private Integer type;
/**
* 笔记图片链接(逗号隔开)
@@ -81,23 +84,29 @@ public class NoteDO {
* 可见范围(0公开,所有人可见 1仅对自己可见)
*/
@TableField(value = "visible")
private Byte visible;
private Integer visible;
/**
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
private LocalDateTime updateTime;
/**
* 状态(0待审核 1正常展示 2被删除(逻辑删除) 3被下架)
*/
@TableField(value = "`status`")
private Byte status;
private Integer status;
/**
* 笔记内容UUID
*/
@TableField(value = "content_uuid")
private String contentUuid;
}

View File

@@ -4,15 +4,18 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 话题表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_topic")
@@ -33,13 +36,13 @@ public class TopicDO {
* 创建时间
*/
@TableField(value = "create_time")
private Date createTime;
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private Date updateTime;
private LocalDateTime updateTime;
/**
* 逻辑删除(0未删除 1已删除)

View File

@@ -2,6 +2,8 @@ package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChannelDOMapper extends BaseMapper<ChannelDO> {
}

View File

@@ -2,6 +2,8 @@ package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelTopicRelDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChannelTopicRelDOMapper extends BaseMapper<ChannelTopicRelDO> {
}

View File

@@ -2,6 +2,8 @@ package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NoteDOMapper extends BaseMapper<NoteDO> {
}

View File

@@ -2,6 +2,8 @@ package com.hanserwei.hannote.note.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TopicDOMapper extends BaseMapper<TopicDO> {
}

View File

@@ -13,6 +13,8 @@ public enum ResponseCodeEnum implements BaseExceptionInterface {
PARAM_NOT_VALID("NOTE-10001", "参数错误"),
// ----------- 业务异常状态码 -----------
NOTE_TYPE_ERROR("NOTE-20000", "未知的笔记类型"),
NOTE_PUBLISH_FAIL("NOTE-20001", "笔记发布失败"),
;
// 异常码

View File

@@ -0,0 +1,29 @@
package com.hanserwei.hannote.note.biz.model.vo;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PublishNoteReqVO {
@NotNull(message = "笔记类型不能为空")
private Integer type;
private List<String> imgUris;
private String videoUri;
private String title;
private String content;
private Long topicId;
}

View File

@@ -0,0 +1,22 @@
package com.hanserwei.hannote.note.biz.rpc;
import com.hanserwei.hannote.distributed.id.generator.api.DistributedIdGeneratorFeignApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class DistributedIdGeneratorRpcService {
@Resource
private DistributedIdGeneratorFeignApi distributedIdGeneratorFeignApi;
/**
* 生成雪花算法 ID
*
* @return 雪花算法 ID
*/
public String getSnowflakeId() {
return distributedIdGeneratorFeignApi.getSnowflakeId("test");
}
}

View File

@@ -0,0 +1,50 @@
package com.hanserwei.hannote.note.biz.rpc;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.kv.api.KeyValueFeignApi;
import com.hanserwei.hannote.kv.dto.req.AddNoteContentReqDTO;
import com.hanserwei.hannote.kv.dto.req.DeleteNoteContentReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Component
public class KeyValueRpcService {
@Resource
private KeyValueFeignApi keyValueFeignApi;
/**
* 保存笔记内容
*
* @param uuid 笔记UUID
* @param content 笔记内容
* @return 是否成功
*/
public boolean saveNoteContent(String uuid, String content) {
AddNoteContentReqDTO addNoteContentReqDTO = new AddNoteContentReqDTO();
addNoteContentReqDTO.setUuid(uuid);
addNoteContentReqDTO.setContent(content);
Response<?> response = keyValueFeignApi.addNoteContent(addNoteContentReqDTO);
return Objects.nonNull(response) && response.isSuccess();
}
/**
* 删除笔记内容
*
* @param uuid 笔记UUID
* @return 是否成功
*/
public boolean deleteNoteContent(String uuid) {
DeleteNoteContentReqDTO deleteNoteContentReqDTO = new DeleteNoteContentReqDTO();
deleteNoteContentReqDTO.setUuid(uuid);
Response<?> response = keyValueFeignApi.deleteNoteContent(deleteNoteContentReqDTO);
return Objects.nonNull(response) && response.isSuccess();
}
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.note.biz.service;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelDO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ChannelDOService extends IService<ChannelDO>{
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.note.biz.service;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelTopicRelDO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface ChannelTopicRelDOService extends IService<ChannelTopicRelDO>{
}

View File

@@ -0,0 +1,17 @@
package com.hanserwei.hannote.note.biz.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteDO;
import com.hanserwei.hannote.note.biz.model.vo.PublishNoteReqVO;
public interface NoteService extends IService<NoteDO> {
/**
* 笔记发布
* @param publishNoteReqVO 笔记发布请求
* @return 笔记发布结果
*/
Response<?> publishNote(PublishNoteReqVO publishNoteReqVO);
}

View File

@@ -0,0 +1,8 @@
package com.hanserwei.hannote.note.biz.service;
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
import com.baomidou.mybatisplus.extension.service.IService;
public interface TopicDOService extends IService<TopicDO>{
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.note.biz.service.impl;
import com.hanserwei.hannote.note.biz.service.ChannelDOService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelDO;
import com.hanserwei.hannote.note.biz.domain.mapper.ChannelDOMapper;
@Service
public class ChannelDOServiceImpl extends ServiceImpl<ChannelDOMapper, ChannelDO> implements ChannelDOService {
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.note.biz.service.impl;
import com.hanserwei.hannote.note.biz.service.ChannelTopicRelDOService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.note.biz.domain.mapper.ChannelTopicRelDOMapper;
import com.hanserwei.hannote.note.biz.domain.dataobject.ChannelTopicRelDO;
@Service
public class ChannelTopicRelDOServiceImpl extends ServiceImpl<ChannelTopicRelDOMapper, ChannelTopicRelDO> implements ChannelTopicRelDOService {
}

View File

@@ -0,0 +1,135 @@
package com.hanserwei.hannote.note.biz.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.note.biz.domain.dataobject.NoteDO;
import com.hanserwei.hannote.note.biz.domain.mapper.NoteDOMapper;
import com.hanserwei.hannote.note.biz.enums.NoteStatusEnum;
import com.hanserwei.hannote.note.biz.enums.NoteTypeEnum;
import com.hanserwei.hannote.note.biz.enums.NoteVisibleEnum;
import com.hanserwei.hannote.note.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.note.biz.model.vo.PublishNoteReqVO;
import com.hanserwei.hannote.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.hanserwei.hannote.note.biz.rpc.KeyValueRpcService;
import com.hanserwei.hannote.note.biz.service.NoteService;
import com.hanserwei.hannote.note.biz.service.TopicDOService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Slf4j
@Service
public class NoteServiceImpl extends ServiceImpl<NoteDOMapper, NoteDO> implements NoteService {
@Resource
private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;
@Resource
private KeyValueRpcService keyValueRpcService;
@Resource
private TopicDOService topicDOService;
@Override
public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
// 笔记类型
Integer type = publishNoteReqVO.getType();
// 获取对应的枚举类型
NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);
// 若非图文或视频则抛异常
if (Objects.isNull(noteTypeEnum)) {
throw new ApiException(ResponseCodeEnum.NOTE_TYPE_ERROR);
}
String imgUris = null;
// 笔记内容是否为空默认为空即true
boolean isContentEmpty = true;
String videoUri = null;
switch (noteTypeEnum) {
case IMAGE_TEXT -> {
List<String> imgUriList = publishNoteReqVO.getImgUris();
//校验图片是否为空
Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空!");
//校验图片数目
Preconditions.checkArgument(imgUriList.size() <= 8, "图片不能超过8张");
//把图片uri拼接成字符串逗号隔开
imgUris = String.join(",", imgUriList);
}
case VIDEO -> {
videoUri = publishNoteReqVO.getVideoUri();
//校验视频是否为空
Preconditions.checkArgument(StringUtils.isNoneBlank(videoUri), "笔记视频不能为空!");
}
default -> {
}
}
// RPC调用分布式ID生成服务生成笔记ID
String snowflakeId = distributedIdGeneratorRpcService.getSnowflakeId();
// 笔记内容UUID
String contentUuid = null;
// 笔记内容
String content = publishNoteReqVO.getContent();
// 若用户填写了笔记内容则调用KV服务
if (StringUtils.isNotBlank(content)) {
isContentEmpty = false;
// 生成笔记内容UUID
contentUuid = UUID.randomUUID().toString();
// RPC调用KV服务保存笔记内容
boolean isSaveSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
// 若保存笔记内容失败,则抛异常
if (!isSaveSuccess) {
throw new ApiException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
}
}
// 话题
Long topicId = publishNoteReqVO.getTopicId();
String topicName = null;
if (Objects.nonNull(topicId)) {
//获取话题名称
topicName = topicDOService.getById(topicId).getName();
}
// 发布者ID
Long creatorId = LoginUserContextHolder.getUserId();
// 构建笔记对象
NoteDO noteDO = NoteDO.builder()
.id(Long.valueOf(snowflakeId))
.isContentEmpty(isContentEmpty)
.creatorId(creatorId)
.imgUris(imgUris)
.title(publishNoteReqVO.getTitle())
.topicId(publishNoteReqVO.getTopicId())
.topicName(topicName)
.type(type)
.visible(NoteVisibleEnum.PUBLIC.getCode())
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.status(NoteStatusEnum.NORMAL.getCode())
.isTop(Boolean.FALSE)
.videoUri(videoUri)
.contentUuid(contentUuid)
.build();
try {
boolean isSaveSuccess = this.save(noteDO);
} catch (Exception e) {
log.error("保存笔记失败!", e);
// RPC调用KV服务删除笔记内容
if (StringUtils.isNotBlank(contentUuid)) {
keyValueRpcService.deleteNoteContent(contentUuid);
}
}
return Response.success();
}
}

View File

@@ -0,0 +1,12 @@
package com.hanserwei.hannote.note.biz.service.impl;
import com.hanserwei.hannote.note.biz.service.TopicDOService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hanserwei.hannote.note.biz.domain.dataobject.TopicDO;
import com.hanserwei.hannote.note.biz.domain.mapper.TopicDOMapper;
@Service
public class TopicDOServiceImpl extends ServiceImpl<TopicDOMapper, TopicDO> implements TopicDOService {
}

View File

@@ -18,10 +18,11 @@
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="status" jdbcType="TINYINT" property="status" />
<result column="content_uuid" jdbcType="VARCHAR" property="contentUuid" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, title, is_content_empty, creator_id, topic_id, topic_name, is_top, `type`, img_uris,
video_uri, visible, create_time, update_time, `status`
video_uri, visible, create_time, update_time, `status`, content_uuid
</sql>
</mapper>