## 任务名称: 发布动态 ### 目标: - 掌握实体类的设计和实现 - 了解并发控制策略,重点掌握乐观锁的实现 ### 预备知识: - [JPA关联关系](../guides/JPA实体类的关联关系.md) - [并发控制策略](../guides/并发控制策略.md) ### 操作步骤 #### 1. 写发布动态接口的API文档 - [发布动态](../api%20doc/发布动态.md) - [根据id获取动态详情](../api%20doc/根据id获取动态详情.md) #### 2. 完成实体类设计 根据接口文档,以及前端界面的设计,完成实体类设计. **示例代码只包括重点的部分,其他部分可以参考项目的源码。** **Post** ```java @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 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** ```java @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** ```java @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** ```java public interface PostRepository extends JpaRepository { } ``` **PostContentRepository** ```java public interface PostContentRepository extends JpaRepository { } ``` **TagRepository** ```java public interface TagRepository extends JpaRepository { @Query("SELECT t.tag FROM Tag t WHERE t.tag LIKE CONCAT('%', :key, '%') ORDER BY t.quoteNum DESC limit 20") List findByTagContainingOrderByQuoteNumDesc(@Param("key") String key); Optional findByTag(String tag); } ``` #### 4. 完成ResponseBody的DTO类的设计 **PostDto** ```java @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 contents; } ``` **PostContentDto** ```java @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** ```java @Value public class TagDto implements Serializable { Long id; Long userId; UserDto user; String tag; Long quoteNum; Long createdOn; Long modifiedOn; } ``` #### 5. 完成RequestBody的DTO类的设计 **PostRequest** ```java @Setter @Getter public class PostRequest { private Long attachmentPrice; private Byte visibility; //帖子内容列表 List contents; // #标签列表 List tags; // @用户列表 List users; } ``` **ContentInPostRequest** ```java @Setter @Getter public class ContentInPostRequest { private String content; private Integer sort; private Byte type; } ``` #### 6. 完成MapStruct的Mapper接口 **PostMapper** ```java @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** ```java @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** ```java @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方法。 ```java /** * 处理文章发布请求 * @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 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方法。 ```java @PostMapping("/post") public DataResult createPost(@RequestBody PostRequest postRequest) { return ResultUtil.ok(postService.createPost(postRequest)); } ``` #### 9. 实现根据id获取动态功能 在PostService类中添加getPost方法。 ```java /** * 根据文章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方法。 ```java @GetMapping("/post") public DataResult getPost(@PathParam("id") Long id) { return ResultUtil.ok(postService.getPost(id)); } ``` #### 10. 通过前端完成发布动态 - 运行vue前端项目 - 在浏览器中访问http://localhost:5173 - 登录账号 - 完成发布动态: 上传图片、内容中支持#主题 ### 技术/工具需求: - [ip2region](https://github.com/lionsoul2014/ip2region) 根据ip地址获取用户所在地 ### 成功标准: - 通过paopao ce的vue前端完成发布动态功能 ### 扩展学习(可选): - [提供一些额外学习资源或挑战性任务,鼓励学有余力的学生进一步探索。] ### 评估与反馈: - [说明如何提交作业、代码审查的标准、或任何反馈收集机制。] ### 时间估算: - 4 Hours