441 lines
12 KiB
Markdown
441 lines
12 KiB
Markdown
## 任务名称: 发布动态
|
||
### 目标:
|
||
- 掌握实体类的设计和实现
|
||
- 了解并发控制策略,重点掌握乐观锁的实现
|
||
### 预备知识:
|
||
- [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 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方法。
|
||
```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 |