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
|