12 KiB
12 KiB
任务名称: 发布动态
目标:
- 掌握实体类的设计和实现
- 了解并发控制策略,重点掌握乐观锁的实现
预备知识:
操作步骤
1. 写发布动态接口的API文档
2. 完成实体类设计
根据接口文档,以及前端界面的设计,完成实体类设计.
示例代码只包括重点的部分,其他部分可以参考项目的源码。
Post
@Comment("冒泡/动态/文章")
@Entity
public class Post extends BaseAuditingEntity{
@Version
private Long version;
@Comment("用户ID")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
private List<PostContent> contents;
@Comment("评论数")
@ColumnDefault("0")
@Column(name = "comment_count", nullable = false)
private Long commentCount = 0L;
@Comment("收藏数")
@ColumnDefault("0")
@Column(name = "collection_count", nullable = false)
private Long collectionCount = 0L;
@Comment("点赞数")
@ColumnDefault("0")
@Column(name = "upvote_count", nullable = false)
private Long upvoteCount = 0L;
@Comment("分享数")
@ColumnDefault("0")
@Column(name = "share_count", nullable = false)
private Long shareCount = 0L;
@Comment("可见性: 0私密 10充电可见 20订阅可见 30保留 40保留 50好友可见 60关注可见 70保留 80保留 90公开")
@ColumnDefault("50")
@Column(name = "visibility", nullable = false)
private Byte visibility;
@Comment("是否置顶")
@ColumnDefault("0")
@Column(name = "is_top", nullable = false)
private Boolean isTop = false;
@Comment("是否精华")
@ColumnDefault("0")
@Column(name = "is_essence", nullable = false)
private Boolean isEssence = false;
@Comment("是否锁定")
@ColumnDefault("0")
@Column(name = "is_lock", nullable = false)
private Boolean isLock = false;
@Comment("参与的主题")
@Column(name = "tags")
private String tags;
@Comment("IP地址")
@Column(name = "ip", length = 15)
private String ip;
@Comment("IP城市地址")
@Column(name = "ip_loc", length = 64)
private String ipLoc;
}
PostContent
@Comment("冒泡/动态/文章内容")
@Entity
public class PostContent extends BaseAuditingEntity{
@Comment("冒泡/动态/文章ID")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@Comment("用户ID")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Comment("内容")
@Column(name = "content", nullable = false, length = 4000)
private String content;
@Comment("类型,1标题,2文字段落,3图片地址,4视频地址,5语音地址,6链接地址,7附件资源,8收费资源")
@ColumnDefault("2")
@Column(name = "type", nullable = false)
private Byte type = 2;
@Comment("排序,越小越靠前")
@ColumnDefault("100")
@Column(name = "sort", nullable = false)
private Integer sort =100;
}
Tag
@Comment("标签")
@Entity
public class Tag extends BaseAuditingEntity{
@Version
private Long version;
@Comment("创建者ID")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Comment("标签名")
@Column(name = "tag", nullable = false)
private String tag;
@Comment("引用数")
@ColumnDefault("0")
@Column(name = "quote_num", nullable = false)
private Long quoteNum = 0L;
}
3. 为每个实体类创建其对应的JpaRepository接口
PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
}
PostContentRepository
public interface PostContentRepository extends JpaRepository<PostContent, Long> {
}
TagRepository
public interface TagRepository extends JpaRepository<Tag, Long> {
@Query("SELECT t.tag FROM Tag t WHERE t.tag LIKE CONCAT('%', :key, '%') ORDER BY t.quoteNum DESC limit 20")
List<String> findByTagContainingOrderByQuoteNumDesc(@Param("key") String key);
Optional<Tag> findByTag(String tag);
}
4. 完成ResponseBody的DTO类的设计
PostDto
@Value
public class PostDto implements Serializable {
Long id;
Long userId;
Long commentCount;
Long collectionCount;
Long upvoteCount;
Long shareCount;
Byte visibility;
Boolean isTop;
Boolean isEssence;
Boolean isLock;
Long latestRepliedOn;
String tags;
Long attachmentPrice;
String ip;
String ipLoc;
@JsonSerialize(using = MillisecondToSecondSerializer.class)
Long createdOn;
@JsonSerialize(using = MillisecondToSecondSerializer.class)
Long modifiedOn;
Long deletedOn;
Byte isDel;
UserDto user;
List<PostContentDto> contents;
}
PostContentDto
@Value
public class PostContentDto implements Serializable {
Long id;
Long postId;
String content;
Byte type;
Integer sort;
@JsonSerialize(using = MillisecondToSecondSerializer.class)
Long createdOn;
@JsonSerialize(using = MillisecondToSecondSerializer.class)
Long modifiedOn;
}
TagDto
@Value
public class TagDto implements Serializable {
Long id;
Long userId;
UserDto user;
String tag;
Long quoteNum;
Long createdOn;
Long modifiedOn;
}
5. 完成RequestBody的DTO类的设计
PostRequest
@Setter
@Getter
public class PostRequest {
private Long attachmentPrice;
private Byte visibility;
//帖子内容列表
List<ContentInPostRequest> contents;
// #标签列表
List<String> tags;
// @用户列表
List<String> users;
}
ContentInPostRequest
@Setter
@Getter
public class ContentInPostRequest {
private String content;
private Integer sort;
private Byte type;
}
6. 完成MapStruct的Mapper接口
PostMapper
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface PostMapper {
Post toEntity(PostDto postDto);
PostDto toDto(Post post);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
Post partialUpdate(PostDto postDto, @MappingTarget Post post);
}
PostContentMapper
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface PostContentMapper {
PostContent toEntity(PostContentDto postContentDto);
PostContentDto toDto(PostContent postContent);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
PostContent partialUpdate(PostContentDto postContentDto, @MappingTarget PostContent postContent);
}
TagMapper
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface TagMapper {
Tag toEntity(TagDto tagDto);
@Mapping(source = "user.id", target = "userId")
@Mapping(source = "user", target = "user")
TagDto toDto(Tag tag);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
Tag partialUpdate(TagDto tagDto, @MappingTarget Tag tag);
}
7. 完成PostService中发布动态的实现
创建PostService类,实现createPost方法。
/**
* 处理文章发布请求
* @param postRequest 创建文章的请求参数
* @return 创建成功后的文章DTO
*/
public PostDto handlePostRequest(PostRequest postRequest){
// 获取当前用户
User user = authService.getCurrentUser();
// 保存文章
Post post = savePost(postRequest, user);
// 处理post中的标签
handleTags(postRequest, user);
// 发送消息通知post中@的用户
handleAtUser(postRequest, user);
return postMapper.toDto(post);
}
private Post savePost(PostRequest postRequest, User currentUser){
Post post = new Post();
post.setUser(currentUser);
// 设置附件价格,默认为0
post.setAttachmentPrice(postRequest.getAttachmentPrice()!=null?postRequest.getAttachmentPrice():0);
// 设置文章可见性
post.setVisibility(postRequest.getVisibility());
// 文章内容
List<PostContent> contents = new ArrayList<>();
postRequest.getContents().forEach(contentReq -> {
PostContent content = new PostContent();
content.setContent(contentReq.getContent()); // 设置内容
content.setSort(contentReq.getSort()); // 设置排序
content.setType(contentReq.getType()); // 设置类型
content.setPost(post); // 关联文章
content.setUser(currentUser); // 关联用户
contents.add(content); // 添加到列表
});
post.setContents(contents);
// 保存文章
return postRepository.save(post);
}
private void handleTags(PostRequest postRequest, User currentUser){
// 处理post中的标签
postRequest.getTags().forEach((tag)->{
Tag tagEntity = tagRepository.findByTag(tag).orElse(null);
if(tagEntity==null){
// 如果标签不存在,则创建新标签并关联文章
tagEntity = new Tag();
tagEntity.setTag(tag);
tagEntity.setUser(currentUser);
tagEntity.setQuoteNum(1L);
tagRepository.save(tagEntity);
}else{
// 如果标签已存在,则更新引用次数
try {
tagEntity.setQuoteNum(tagEntity.getQuoteNum() + 1L);
tagRepository.save(tagEntity); // Spring Data JPA 自动处理乐观锁
} catch (OptimisticLockingFailureException e) {
// 处理乐观锁失败的情况,如重新获取实体并重试或记录日志等
log.error("Optimistic locking failed for tag: {}", tagEntity.getTag(), e);
// TODO: 可添加重试逻辑
// 简单处理为继续抛出异常,让此次发帖失败
throw e;
}
}
});
}
private void handleAtUser(PostRequest postRequest, User currentUser){
postRequest.getUsers().forEach((username)->{
User toUser = userRepository.findByUsername(username).orElseThrow(()->new ResourceNotFoundException("User","username="+username));
// TODO: 发送消息通知toUser
});
}
8. 完成PostController中发布动态的实现
创建PostController类,实现createPost方法。
@PostMapping("/post")
public DataResult<PostDto> createPost(@RequestBody PostRequest postRequest) {
return ResultUtil.ok(postService.createPost(postRequest));
}
9. 实现根据id获取动态功能
在PostService类中添加getPost方法。
/**
* 根据文章ID获取文章详情
* @param id 文章ID
* @return 文章DTO
*/
public PostDto getPost(Long id){
Post post = postRepository.findById(id).orElseThrow(()->new ResourceNotFoundException("Post","id="+id));
return postMapper.toDto(post);
}
在PostController类中添加getPost方法。
@GetMapping("/post")
public DataResult<PostDto> getPost(@PathParam("id") Long id) {
return ResultUtil.ok(postService.getPost(id));
}
10. 通过前端完成发布动态
- 运行vue前端项目
- 在浏览器中访问http://localhost:5173
- 登录账号
- 完成发布动态: 上传图片、内容中支持#主题
技术/工具需求:
- ip2region 根据ip地址获取用户所在地
成功标准:
- 通过paopao ce的vue前端完成发布动态功能
扩展学习(可选):
- [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。]
评估与反馈:
- [说明如何提交作业、代码审查的标准、或任何反馈收集机制。]
时间估算:
- 4 Hours