paopao/docs/tasks/任务10-Sprint-2- 发布动态.md
2024-05-30 11:45:54 +08:00

444 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 任务名称: 发布动态
### 目标:
- 掌握实体类的设计和实现
- 了解并发控制策略,重点掌握乐观锁的实现
### 预备知识:
- [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<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**
```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<Post, Long> {
}
```
**PostContentRepository**
```java
public interface PostContentRepository extends JpaRepository<PostContent, Long> {
}
```
**TagRepository**
```java
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**
```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<PostContentDto> 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<ContentInPostRequest> contents;
// #标签列表
List<String> tags;
// @用户列表
List<String> 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<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方法。
```java
@PostMapping("/post")
public DataResult<PostDto> 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<PostDto> 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