paopao/docs/tasks/任务10-Sprint-2- 发布动态.md
2024-05-26 15:44:22 +08:00

12 KiB
Raw Blame History

任务名称: 发布动态

目标:

  • 掌握实体类的设计和实现
  • 了解并发控制策略,重点掌握乐观锁的实现

预备知识:

操作步骤

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 createPost(PostRequest postRequest){
        // 获取当前用户
        User user = authService.getCurrentUser();
        Post post = new Post();
        post.setUser(user);
        // 设置附件价格默认为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(user);  // 关联用户
            contents.add(content);  // 添加到列表
        });
        post.setContents(contents);
        // 保存文章
        Post savedPost = postRepository.save(post);

        // 处理post中的标签
        handleTags(postRequest, user);
        // 发送消息通知post中@的用户
        handleAtUser(postRequest, user);

        return postMapper.toDto(savedPost);
    }

    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