Compare commits

..

No commits in common. 'main' and 'lzt' have entirely different histories.
main ... lzt

File diff suppressed because it is too large Load Diff

@ -1,22 +0,0 @@
package com.luojia_channel.common.config;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("珞珈岛API文档")
.description("珞珈岛社交平台API接口文档")
.version("1.0")
);
}
}

@ -16,14 +16,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login",
"/user/register",
"/user/captcha",
"/user/verify-captcha",
"/post/list",
"/post/detail",
"/comment/list",
"/comment/list/reply",
"/openapi/luojia-channel",
"/swagger-ui.html"
"/post/page",
"/post/detail"
);
}

@ -1,19 +1,12 @@
package com.luojia_channel.common.domain;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
// 统一返回前端的结果
@Data
@Schema(description = "统一返回前端的结果")
public class Result<T> {
@Schema(description = "状态码")
private int code;
@Schema(description = "提示消息")
private String msg;
@Schema(description = "响应数据")
private T data;
public Result(int code, String msg, T data) {
this.code = code;

@ -4,7 +4,6 @@ import lombok.Data;
@Data
public class PageRequest {
// 普通分页参数
private Long current = 1L;
private Long size = 10L;
}

@ -13,9 +13,8 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
// 普通分页参数
private Long current; // 当前页数,适用于普通分页
private Long total;
private Long current;
private Long size = 10L;
private Long total;
private List<T> records = Collections.emptyList();
}

@ -1,14 +0,0 @@
package com.luojia_channel.common.domain.page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ScrollPageRequest {
private Long lastVal; // 上次查询的最小值(用于游标分页)
private Integer offset = 0; // 偏移量(用于分页位置标记)
private Long size = 10L; // 每页数量
}

@ -1,20 +0,0 @@
package com.luojia_channel.common.domain.page;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
// 滚动分页请求
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ScrollPageResponse<T> {
private Long lastVal; // 上次查询的最小值(用于游标分页)
private Integer offset = 0; // 偏移量(用于分页位置标记)
private Long size = 10L; // 每页数量
private List<T> records; // 数据列表
}

@ -30,7 +30,6 @@ public final class JWTUtil {
private static final long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username";
private static final String USER_AVATAR_KEY = "avatar";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String ISS = "luojiachannel";
public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";
@ -46,7 +45,6 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
@ -66,7 +64,6 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())

@ -1,9 +1,5 @@
package com.luojia_channel.common.utils;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@ -13,12 +9,10 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -48,9 +42,7 @@ public class RedisUtil {
return value != null ? type.cast(value) : null;
}
// 安全地从缓存中取值
public <T> T safeGet(String key, Class<T> type, Supplier<T> cacheLoader,
long timeout, TimeUnit timeUnit) {
public <T> T safeGet(String key, Class<T> type, Supplier<T> cacheLoader, long timeout, TimeUnit timeUnit) {
T result = get(key, type);
if(result != null){
return result;
@ -73,42 +65,6 @@ public class RedisUtil {
return get(key, type);
}
// 封装基于redis zset的滚动分页查询
public <T> ScrollPageResponse<T> scrollPageQuery(String key, Class<T> type,
ScrollPageRequest pageRequest,
Function<List<Long>, List<T>> dbFallback) {
long max = pageRequest.getLastVal();
long offset = pageRequest.getOffset();
long size = pageRequest.getSize();
Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, size);
if(typedTuples == null || typedTuples.isEmpty()){
return ScrollPageResponse.<T>builder().build();
}
// 获取返回的offset与minTime
List<Long> ids = new ArrayList<>();
int returnOffset = 1;
long min = 0;
for (ZSetOperations.TypedTuple<Object> tuple : typedTuples) {
Long id = (Long)tuple.getValue();
ids.add(id);
long lastVal = tuple.getScore().longValue();
if(lastVal == min){
returnOffset++;
}else{
returnOffset = 1;
min = lastVal;
}
}
List<T> dbList = dbFallback.apply(ids);
return ScrollPageResponse.<T>builder()
.records(dbList)
.size(pageRequest.getSize())
.offset(returnOffset)
.lastVal(min)
.build();
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT);
}
@ -244,27 +200,4 @@ public class RedisUtil {
}
public <T> List<ZSetItem<T>> zRevRangeWithScores(String key, long count) {
return zRevRangeWithScores(key, 0, count - 1);
}
public <T> List<ZSetItem<T>> zRevRangeWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<Object>> tuples = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
return convertTuples(tuples);
}
public <T> T zRevMaxValue(String key) {
List<ZSetItem<T>> items = zRevRangeWithScores(key, 1);
return items.isEmpty() ? null : items.get(0).getValue();
}
public <T> ZSetItem<T> zRevMaxItem(String key) {
List<ZSetItem<T>> items = zRevRangeWithScores(key, 1);
return items.isEmpty() ? null : items.get(0);
}
}

@ -16,7 +16,10 @@ public final class UserContext {
public static Long getUserId() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getUserId).orElse(null);
if(userInfoDTO == null){
throw new UserException("用户不存在");
}
return userInfoDTO.getUserId();
}
public static String getUsername() {

@ -0,0 +1,11 @@
com\luojia\luojia_channel\advice\GlobalExceptionHandler.class
com\luojia\luojia_channel\domain\UserDTO.class
com\luojia\luojia_channel\utils\UserContext.class
com\luojia\luojia_channel\utils\JWTUtil.class
com\luojia\luojia_channel\config\RedisConfig.class
com\luojia\luojia_channel\utils\RedisUtil$ZSetItem.class
com\luojia\luojia_channel\domain\UserDTO$UserDTOBuilder.class
com\luojia\luojia_channel\exception\BaseException.class
com\luojia\luojia_channel\utils\RedisUtil.class
com\luojia\luojia_channel\domain\Result.class
com\luojia\luojia_channel\exception\UserException.class

@ -1,19 +1,9 @@
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\advice\GlobalExceptionHandler.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\MybatisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\OpenApiConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\RedisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\WebMvcConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\constants\RedisConstant.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageRequest.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageResponse.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\Result.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\UserDTO.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\BaseException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\FileException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\PostException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\UserException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\interceptor\AuthInterceptor.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\JWTUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\PageUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\RedisUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\UserContext.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\advice\GlobalExceptionHandler.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\config\RedisConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\Result.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\UserDTO.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\BaseException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\UserException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\JWTUtil.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\RedisUtil.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\UserContext.java

@ -106,26 +106,6 @@
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- openAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.8</version>
</dependency>
<!-- es -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
<build>

@ -10,4 +10,4 @@ public class LuojiaChannelApplication {
SpringApplication.run(LuojiaChannelApplication.class, args);
}
}
}

@ -1,6 +1,5 @@
package com.luojia_channel.modules.file.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -9,23 +8,10 @@ import org.springframework.web.multipart.MultipartFile;
@Data
@AllArgsConstructor
@Builder
@Schema(description = "上传文件DTO")
public class UploadFileDTO {
@Schema(
description = "文件名",
required = true
)
private MultipartFile file;
// 文件类型image or video
@Schema(
description = "文件类型"
)
private String fileType;
// 文件md5
@Schema(
description = "文件md5"
)
private String fileMd5;
}

@ -1,52 +0,0 @@
package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.interact.service.ChatService;
import com.luojia_channel.modules.message.dto.MessageResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/message")
@RequiredArgsConstructor
@Tag(name = "聊天模块", description = "好友聊天模块相关接口")
public class ChatController {
private final ChatService chatService;
@Operation(
summary = "聊天列表",
description = "传入分页参数,查询私信用户列表(带最新消息)",
tags = {"聊天模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
@GetMapping("/chat-list")
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatList(chatPageQueryDTO));
}
@Operation(
summary = "历史记录",
description = "传入分页参数,获取与特定用户的完整聊天记录",
tags = {"聊天模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
@GetMapping("/history")
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatHistory(chatPageQueryDTO));
}
}

@ -1,81 +0,0 @@
package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/follow")
@RequiredArgsConstructor
@Tag(name = "关注模块", description = "用户之间互相关注模块")
public class FollowController {
private final FollowService followService;
@PutMapping("/{id}/{isFollow}")
@Operation(
summary = "关注用户",
description = "关注用户传入用户id和是否关注的状态",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "操作成功"),
@ApiResponse(responseCode = "500", description = "操作失败,请稍后重试")
})
public Result<Void> follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
followService.follow(followUserId, isFollow);
return Result.success();
}
@GetMapping("/or/not/{id}")
@Operation(
summary = "是否关注用户",
description = "传入用户id返回是否关注该用户",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "已关注"),
@ApiResponse(responseCode = "500", description = "未关注")
})
public Result<Boolean> isFollow(@PathVariable("id") Long followUserId){
return Result.success(followService.isFollow(followUserId));
}
@GetMapping("/common/{id}")
@Operation(
summary = "共同关注",
description = "传入用户id返回该与该用户的共同关注",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<List<UserDTO>> followCommons(@PathVariable("id") Long id){
return Result.success(followService.followCommons(id));
}
@GetMapping("/post")
@Operation(
summary = "关注收件箱",
description = "传入分页参数,查询关注的人的发帖推送",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> queryPostFollow(@RequestBody PostPageQueryDTO postPageQueryDTO){
return Result.success(followService.queryPostFollow(postPageQueryDTO));
}
}

@ -1,51 +0,0 @@
package com.luojia_channel.modules.interact.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "聊天列表项DTO")
public class ChatItemDTO {
@Schema(
description = "聊天对象的用户ID",
required = true,
example = "123456"
)
private Long chatUserId;
@Schema(
description = "聊天对象的头像URL",
example = "https://example.com/avatar.jpg"
)
private String avatar;
@Schema(
description = "聊天对象的用户名",
required = true,
example = "张三"
)
private String username;
@Schema(
description = "最新消息内容",
required = true,
maxLength = 500,
example = "今天下午开会"
)
private String latestMessage;
@Schema(
description = "最新消息时间",
required = true,
example = "2023-10-15T14:30:00"
)
private LocalDateTime latestTime;
}

@ -1,9 +0,0 @@
package com.luojia_channel.modules.interact.dto;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import lombok.Data;
@Data
public class ChatPageQueryDTO extends ScrollPageRequest {
private Long chatUserId;
}

@ -1,49 +0,0 @@
package com.luojia_channel.modules.interact.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("follow")
@Schema(description = "关注表")
public class Follow implements Serializable {
/**
*
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* id
*/
@Schema(
description = "用户id"
)
private Long userId;
/**
* id
*/
@Schema(
description = "关联的用户id"
)
private Long followUserId;
/**
*
*/
@Schema(
description = "创建时间"
)
private LocalDateTime createTime;
}

@ -1,18 +0,0 @@
package com.luojia_channel.modules.interact.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.interact.entity.Follow;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface FollowMapper extends BaseMapper<Follow> {
@Delete("delete from follow where user_id = #{userId} and follow_user_id = #{followUserId}")
boolean delete(Long userId, Long followUserId);
@Select("select count(*) from follow where user_id = #{userId} and follow_user_id = #{followUserId}")
Integer queryCount(Long userId, Long followUserId);
}

@ -1,17 +0,0 @@
package com.luojia_channel.modules.interact.service;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import java.util.List;
public interface ChatService {
ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO);
ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO);
}

@ -1,23 +0,0 @@
package com.luojia_channel.modules.interact.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import java.util.List;
public interface FollowService extends IService<Follow> {
void follow(Long followUserId, Boolean isFollow);
boolean isFollow(Long followUserId);
List<UserDTO> followCommons(Long id);
ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO);
}

@ -1,128 +0,0 @@
package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
import com.luojia_channel.modules.interact.service.ChatService;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import com.luojia_channel.modules.message.mapper.MessageMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ChatServiceImpl implements ChatService {
private final MessageMapper messageMapper;
private final UserMapper userMapper;
private final RedisUtil redisUtil;
@Override
public ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO) {
/*
Long userId = UserContext.getUserId();
IPage<ChatItemDTO> chatPage = messageMapper.selectChatList(PageUtil.convert(chatPageQueryDTO), userId);
return PageResponse.<ChatItemDTO>builder()
.current(chatPage.getCurrent())
.size(chatPage.getSize())
.total(chatPage.getTotal())
.records(chatPage.getRecords())
.build();
*/
Long userId = UserContext.getUserId();
String key = "chat:user_list:" + userId;
return redisUtil.scrollPageQuery(key, ChatItemDTO.class, chatPageQueryDTO,
(chatUserIds) -> {
List<ChatItemDTO> chatItems = new ArrayList<>();
List<Long> latestMessageIds = new ArrayList<>();
List<User> users = userMapper.selectByIdsOrderByField(chatUserIds);
for(Long chatUserId : chatUserIds){
String messageKey = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
// 获取zset中最新的messageId
Long latestMessageId = redisUtil.zRevMaxValue(messageKey);
latestMessageIds.add(latestMessageId);
}
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(latestMessageIds);
int i=0;
for(User user : users){
ChatItemDTO chatItemDTO = ChatItemDTO.builder()
.chatUserId(user.getId())
.avatar(user.getAvatar())
.username(user.getUsername())
.latestMessage(messageDOS.get(i).getContent())
.latestTime(messageDOS.get(i).getCreateTime())
.build();
chatItems.add(chatItemDTO);
i++;
}
return chatItems;
});
}
@Override
public ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO) {
/*
Long userId = UserContext.getUserId();
Long chatUserId = chatPageQueryDTO.getChatUserId();
LambdaQueryWrapper<MessageDO> queryWrapper = Wrappers.lambdaQuery(MessageDO.class)
.eq(MessageDO::getSenderId, userId)
.eq(MessageDO::getReceiverId, chatUserId)
.or()
.eq(MessageDO::getReceiverId, userId)
.eq(MessageDO::getSenderId, chatUserId)
.orderByDesc(MessageDO::getCreateTime);
// 查询的是私信消息
queryWrapper.eq(MessageDO::getMessageType, 1);
IPage<MessageDO> page = messageMapper.selectPage(PageUtil.convert(chatPageQueryDTO), queryWrapper);
User chatUser = userMapper.selectById(chatUserId);
return PageUtil.convert(page, (message) -> {
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
if(messageResponse.getSenderId().equals(userId)) {
messageResponse.setSenderAvatar(UserContext.getAvatar());
messageResponse.setSenderName(UserContext.getUsername());
}else{
messageResponse.setSenderAvatar(chatUser.getAvatar());
messageResponse.setSenderName(chatUser.getUsername());
}
return messageResponse;
});
*/
// 改成滚动分页查询
Long userId = UserContext.getUserId();
Long chatUserId = chatPageQueryDTO.getChatUserId();
String key = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
return redisUtil.scrollPageQuery(key, MessageResponse.class, chatPageQueryDTO,
(messageIds) -> {
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(messageIds);
User chatUser = userMapper.selectById(chatUserId);
List<MessageResponse> messageResponses = new ArrayList<>();
for(MessageDO message : messageDOS){
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
if(messageResponse.getSenderId().equals(userId)) {
messageResponse.setSenderAvatar(UserContext.getAvatar());
messageResponse.setSenderName(UserContext.getUsername());
}else{
messageResponse.setSenderAvatar(chatUser.getAvatar());
messageResponse.setSenderName(chatUser.getUsername());
}
messageResponses.add(messageResponse);
}
return messageResponses;
});
}
}

@ -1,107 +0,0 @@
package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements FollowService {
private final FollowMapper followMapper;
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
@Override
public void follow(Long followUserId, Boolean isFollow) {
Long userId = UserContext.getUserId();
String key = "follows:" + userId;
if(isFollow){
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
follow.setCreateTime(LocalDateTime.now());
boolean isSuccess = save(follow);
if(isSuccess){
redisTemplate.opsForSet().add(key, followUserId);
}
}else{
boolean isSuccess = followMapper.delete(userId, followUserId);
if(isSuccess){
redisTemplate.opsForSet().remove(key, followUserId);
}
}
}
@Override
public boolean isFollow(Long followUserId) {
Long userId = UserContext.getUserId();
Integer count = followMapper.queryCount(userId, followUserId);
return count > 0;
}
@Override
public List<UserDTO> followCommons(Long id) {
Long userId = UserContext.getUserId();
String userKey = "follows:" + userId;
String followKey = "follows:" + id;
Set<Object> intersect = redisTemplate.opsForSet().intersect(userKey, followKey);
if(intersect == null || intersect.isEmpty()){
return Collections.emptyList();
}
List<Long> ids = intersect.stream().map(obj -> (Long)obj).collect(Collectors.toList());
List<UserDTO> userDTOS = userMapper.selectBatchIds(ids).stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.toList();
return userDTOS;
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
String key = "post:follow_of:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Post> posts = postMapper.selectBatchIds(postIds);
List<Long> userIds = posts.stream().map(Post::getUserId).toList();
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
User user = userMap.getOrDefault(post.getUserId(), new User());
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTOS.add(postBasicInfoDTO);
}
// 按照发布时间倒序排序
postBasicInfoDTOS.sort(Comparator.comparing(PostBasicInfoDTO::getCreateTime).reversed());
return postBasicInfoDTOS;
});
}
}

@ -1,27 +0,0 @@
package com.luojia_channel.modules.message.config;
import jakarta.servlet.ServletContext;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.util.WebAppRootListener;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements ServletContextInitializer {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Override
public void onStartup(ServletContext servletContext) {
servletContext.addListener(WebAppRootListener.class);
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize", (1024 * 200) + "");
servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize", (1024 * 200) + "");
}
}

@ -1,47 +0,0 @@
package com.luojia_channel.modules.message.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "消息请求对象")
public class MessageRequest {
@Schema(
description = "消息类型0-私信1-系统通知",
allowableValues = {"0", "1"},
example = "0",
implementation = Integer.class
)
private Integer messageType; // 私信0系统通知1
@Schema(
description = "消息内容",
required = true,
minLength = 1,
maxLength = 500
)
private String content; // 消息内容
@Schema(
description = "接收者ID"
)
private Long receiverId; // 接收者ID
@Schema(
description = "发送者用户名",
required = true
)
private String senderName; // 用户名
@Schema(
description = "发送者头像",
required = true
)
private String senderAvatar; // 用户头像
}

@ -1,59 +0,0 @@
package com.luojia_channel.modules.message.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "消息返回对象")
public class MessageResponse {
@Schema(
description = "消息类型0-私信1-系统通知",
allowableValues = {"0", "1"},
example = "0",
implementation = Integer.class
)
private Integer messageType; // 私信0系统通知1
@Schema(
description = "消息内容",
required = true,
minLength = 1,
maxLength = 500
)
private String content; // 消息内容
@Schema(
description = "发送者ID"
)
private Long senderId;
@Schema(
description = "接收者ID"
)
private Long receiverId; // 接收者ID
@Schema(
description = "发送者用户名",
required = true
)
private String senderName; // 用户名
@Schema(
description = "发送者头像",
required = true
)
private String senderAvatar; // 用户头像
@Schema(
description = "消息创建时间"
)
private LocalDateTime createTime;
}

@ -1,20 +0,0 @@
package com.luojia_channel.modules.message.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
*
*/
@Data
@TableName("message")
public class MessageDO {
@TableId(type = IdType.AUTO)
private Long id;
private Integer messageType; // 0-私聊, 1-系统消息
private String content;
private Long senderId;
private Long receiverId;
private LocalDateTime createTime;
}

@ -1,21 +0,0 @@
package com.luojia_channel.modules.message.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
import com.luojia_channel.modules.message.entity.MessageDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface MessageMapper extends BaseMapper<MessageDO> {
/**
*
*/
IPage<ChatItemDTO> selectChatList(IPage<ChatItemDTO> page, @Param("userId") Long userId);
List<MessageDO> selectByIdsOrderByField(@Param("ids") List<Long> ids);
}

@ -1,46 +0,0 @@
package com.luojia_channel.modules.message.mq;
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
// 模板方法类,提供消息发送的模板方法
public abstract class AbstractSendProduceTemplate<T> {
private final RabbitTemplate rabbitTemplate;
private static final String KEYS = "keys";
protected abstract BaseSendExtendDTO buildBaseSendParam(T messageSendEvent);
public void sendMessage(T messageSendEvent) {
BaseSendExtendDTO baseSendDTO = buildBaseSendParam(messageSendEvent);
try {
// 发送消息
rabbitTemplate.convertAndSend(
baseSendDTO.getExchange(),
baseSendDTO.getRoutingKey(),
new MessageWrapper(baseSendDTO.getKeys(), messageSendEvent),
m -> {
// 设置消息头
m.getMessageProperties().setHeader(KEYS, baseSendDTO.getKeys());
// 设置消息属性(如延迟时间) TODO 若需要延迟消息,需安装延时插件
if (baseSendDTO.getDelay() != null) {
// m.getMessageProperties().setDelay(baseSendDTO.getDelay());
}
return m;
}
);
log.info("[{}] 消息发送成功Exchange{}Routing Key{}",
baseSendDTO.getEventName(), baseSendDTO.getExchange(), baseSendDTO.getRoutingKey());
} catch (Throwable ex) {
log.error("[{}] 消息发送失败,消息体:{}", baseSendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
throw ex;
}
}
}

@ -1,18 +0,0 @@
package com.luojia_channel.modules.message.mq;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public final class BaseSendExtendDTO {
private String eventName; //业务名称
private String exchange; //交换机
private String routingKey; //路由键
private String keys; // 消息唯一标识
private Long delay; // 延迟时间毫秒需RabbitMQ安装延迟插件
}

@ -1,41 +0,0 @@
package com.luojia_channel.modules.message.mq;
import lombok.*;
import java.io.Serializable;
import java.util.UUID;
/**
*
*/
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Keys
*/
@NonNull
private String keys;
/**
*
*/
@NonNull
private T message;
/**
*
*/
private String uuid = UUID.randomUUID().toString();
/**
*
*/
private Long timestamp = System.currentTimeMillis();
}

@ -1,25 +0,0 @@
package com.luojia_channel.modules.message.mq.config;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory, MessageConverter jsonMessageConverter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jsonMessageConverter);
return factory;
}
}

@ -1,47 +0,0 @@
package com.luojia_channel.modules.message.mq.consumer;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.modules.message.dto.MessageRequest;
import com.luojia_channel.modules.message.mq.MessageWrapper;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.message.server.WebSocketServer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationListener {
public static final String EXCHANGE_NAME = "notify.exchange";
public static final String QUEUE_NAME = "notify.queue";
public static final String ROUTING_KEY = "notify.routing.key";
private final WebSocketServer webSocketServer;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = QUEUE_NAME),
exchange = @Exchange(name = EXCHANGE_NAME, type = "direct"),
key = ROUTING_KEY
))
public void handleNotification(MessageWrapper<NotificationMessage> wrapper) {
try {
NotificationMessage message = wrapper.getMessage();
MessageRequest request = BeanUtil.copyProperties(message, MessageRequest.class);
Integer messageType = message.getMessageType();
if (messageType != null && !messageType.equals(0)) {
webSocketServer.sendPrivateMessage(message.getSenderId(), request);
} else {
webSocketServer.sendSystemNotification(request);
}
} catch (Exception e) {
// 记录异常日志
log.error("处理消息时发生异常: {}", e.getMessage(), e);
throw e;
}
}
}

@ -1,21 +0,0 @@
package com.luojia_channel.modules.message.mq.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class NotificationMessage {
private Long senderId;
private Long receiverId;
private String content;
private String senderName; // 用户名
private String senderAvatar; // 用户头像
private Integer messageType; // 0-私信 1-系统
}

@ -1,148 +0,0 @@
package com.luojia_channel.modules.message.server;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.message.dto.MessageRequest;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import com.luojia_channel.modules.message.mapper.MessageMapper;
import com.luojia_channel.modules.message.util.WebSocketContext;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/connect/{userId}")
@Slf4j
@Component
public class WebSocketServer {
// 存储在线用户会话 <userId, Session>
private final static Map<String, Session> CLIENTS = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(@PathParam("userId") String userId,
Session session) {
// 将新连接加入客户端列表
CLIENTS.put(userId, session);
log.info("用户 [{}] 已连接,当前在线人数:{}", userId, CLIENTS.size());
}
@OnClose
public void onClose(@PathParam("userId") String userId,
Session session) {
// 移除断开连接的用户
CLIENTS.remove(userId);
log.info("用户 [{}] 已断开,当前在线人数:{}", userId, CLIENTS.size());
}
@OnError
public void onError(@PathParam("userId") String userId,
Session session, Throwable e) {
log.error("用户 [{}] 发生错误: {}", userId, e.getMessage(), e);
try {
session.close();
} catch (IOException ex) {
log.error("关闭会话失败: {}", ex.getMessage());
}
}
@OnMessage
public void onMessage(@PathParam("userId") String senderId,
String message, Session session) {
try {
// 解析客户端发送的 JSON 消息
MessageRequest request = JSON.parseObject(message, MessageRequest.class);
switch (request.getMessageType()) {
case 0 -> sendSystemNotification(request);
case 1 -> sendPrivateMessage(Long.parseLong(senderId), request);
default -> log.warn("未知消息类型: {}", request.getMessageType());
}
} catch (Exception e) {
log.error("消息处理失败: {}", e.getMessage());
sendErrorResponse(session, "消息处理失败,请稍后重试");
}
}
// 发送一对一私信
public void sendPrivateMessage(Long senderId, MessageRequest request) {
Long receiverId = request.getReceiverId();
Session receiverSession = CLIENTS.get(receiverId.toString());
// 构建私信响应
MessageResponse response = MessageResponse.builder()
.messageType(request.getMessageType())
.content(request.getContent())
.senderId(senderId)
.senderName(request.getSenderName())
.senderAvatar(request.getSenderAvatar())
.receiverId(receiverId)
.createTime(LocalDateTime.now())
.build();
// 发送给接收方
if (receiverSession != null && receiverSession.isOpen()) {
sendMessage(receiverSession, JSON.toJSONString(response));
} else {
log.info("接收方 [{}] 不在线,消息无法即时送达", receiverId);
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
RedisUtil redisUtil = WebSocketContext.getBean(RedisUtil.class);
messageMapper.insert(message);
sendMessage(CLIENTS.get(senderId.toString()), JSON.toJSONString(response));
// 存储消息至redis
if(request.getMessageType().equals(1)){
String key = "chat:history:" + Math.min(senderId, receiverId) + ":" +Math.max(senderId, receiverId);
redisUtil.zAdd(key, message.getId(), System.currentTimeMillis());
String chatListKey = "chat:user_list:" + senderId;
redisUtil.zAdd(chatListKey, receiverId, System.currentTimeMillis());
chatListKey = "chat:user_list:" + receiverId;
redisUtil.zAdd(chatListKey, senderId, System.currentTimeMillis());
}
}
// 发送系统通知
public void sendSystemNotification(MessageRequest request) {
MessageResponse response = MessageResponse.builder()
.senderId(0L)
.receiverId(0L)
.messageType(request.getMessageType())
.content(request.getContent())
.createTime(LocalDateTime.now())
.build();
// 广播给所有在线用户
for (Session session : CLIENTS.values()) {
sendMessage(session, JSON.toJSONString(response));
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
messageMapper.insert(message);
}
// 安全消息
private void sendMessage(Session session, String message) {
try {
if (session != null && session.isOpen()) {
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
log.error("发送消息失败: {}", e.getMessage());
}
}
// 发送错误响应
private void sendErrorResponse(Session session, String errorMessage) {
MessageResponse errorResponse = MessageResponse.builder()
.messageType(-1) // 错误消息类型
.content(errorMessage)
.createTime(LocalDateTime.now())
.build();
sendMessage(session, JSON.toJSONString(errorResponse));
}
}

@ -1,43 +0,0 @@
package com.luojia_channel.modules.message.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class WebSocketContext implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
@Autowired
public void setApplicationContext(ApplicationContext inApplicationContext) throws BeansException {
applicationContext = inApplicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
public static String getActiveProfile() {
String[] activeProfiles = getApplicationContext().getEnvironment().getActiveProfiles();
if (activeProfiles.length == 0) {
return null;
}
return activeProfiles[0];
}
}

@ -2,120 +2,63 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.service.CommentService;
import com.luojia_channel.modules.post.utils.ValidatePostUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/comment")
@Tag(name = "评论模块", description = "评论相关接口")
@RequestMapping("/comments")
public class CommentController {
private final CommentService commentService;
@Autowired
public CommentController(CommentService commentService) {
this.commentService = commentService;
}
// 创建评论
@PostMapping
@Operation(
summary = "创建评论",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Long> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
return Result.success(commentService.saveComment(commentSaveDTO));
public Result<Void> saveComment(@RequestBody Comment comment) {
commentService.saveComment(comment);
return Result.success();
}
// 更新评论
@PutMapping
@Operation(
summary = "更新评论",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "更新成功"),
@ApiResponse(responseCode = "500", description = "更新失败,请稍后重试")
})
public Result<Void> updateComment(@RequestBody CommentSaveDTO commentSaveDTO) {
commentService.updateComment(commentSaveDTO);
@PutMapping("/{id}")
public Result<Void> updateComment(@PathVariable Long id, @RequestBody Comment comment) {
Long currentUserId = UserContext.getUserId();
commentService.updateComment(comment, currentUserId);
return Result.success();
}
// 删除评论
@DeleteMapping
@Operation(
summary = "删除评论",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "删除成功"),
@ApiResponse(responseCode = "500", description = "删除失败,评论不存在或被删除")
})
public Result<Void> deleteComment(@RequestParam("id") Long id) {
commentService.deleteComment(id);
@DeleteMapping("/{id}")
public Result<Void> deleteComment(@PathVariable Long id) {
Long currentUserId = UserContext.getUserId();
commentService.deleteComment(id, currentUserId);
return Result.success();
}
// 根据帖子ID分页获取评论
// 根据帖子ID分页获取评论
@GetMapping("/list")
@Operation(
summary = "根据帖子ID分页获取根评论",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法")
})
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
public Result<PageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
PageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
return Result.success(commentList);
}
// 根据评论ID获取回复
@GetMapping("/list/reply")
@Operation(
summary = "根据评论ID获取回复",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败评论ID不合法")
})
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
return Result.success(commentInfoDTOList);
}
// 点赞评论
@PutMapping("/like/{id}")
@Operation(
summary = "点赞评论",
tags = {"评论模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "点赞成功"),
@ApiResponse(responseCode = "500", description = "点赞失败,评论不存在或被删除")
})
public Result<Void> likeComment(@PathVariable("id") Long id) {
commentService.likeComment(id);
return Result.success();
// 根据帖子ID获取嵌套评论
@GetMapping("/nested/post/{postId}")
public Result<List<CommentInfoDTO>> getNestedCommentsByPostId(@PathVariable Long postId) {
List<CommentInfoDTO> nestedComments = commentService.getNestedCommentsByPostId(postId);
return Result.success(nestedComments);
}
}

@ -3,16 +3,11 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.dto.resp.PostInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -20,49 +15,25 @@ import org.springframework.web.multipart.MultipartFile;
@RequiredArgsConstructor
@RestController
@RequestMapping("/post")
@Tag(name = "帖子模块", description = "帖子相关接口")
public class PostController {
private final PostService postService;
// 创建帖子
@PostMapping
@Operation(
summary = "创建帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Long> savePost(@RequestBody PostSaveDTO postSaveDTO) {
return Result.success(postService.savePost(postSaveDTO));
public Result<Void> createPost(@RequestBody PostSaveDTO postSaveDTO) {
postService.createPost(postSaveDTO);
return Result.success();
}
// 设置帖子封面
@PostMapping("/cover")
@Operation(
summary = "设置帖子封面",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "设置成功"),
@ApiResponse(responseCode = "500", description = "设置失败,请稍后重试")
})
public Result<String> setCover(@RequestParam("file") MultipartFile file){
return Result.success(postService.setCover(file));
}
// 更新帖子
@PutMapping
@Operation(
summary = "更新帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "更新成功"),
@ApiResponse(responseCode = "500", description = "更新失败,请稍后重试")
})
public Result<Void> updatePost(@RequestBody PostSaveDTO postSaveDTO) {
postService.updatePost(postSaveDTO);
return Result.success();
@ -70,14 +41,6 @@ public class PostController {
// 删除帖子
@DeleteMapping
@Operation(
summary = "删除帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "删除成功"),
@ApiResponse(responseCode = "500", description = "删除失败,请稍后重试")
})
public Result<Void> deletePost(@RequestParam("id") Long id) {
postService.deletePost(id);
return Result.success();
@ -85,58 +48,19 @@ public class PostController {
// 根据ID获取帖子详情
@GetMapping("/detail")
@Operation(
summary = "根据ID获取帖子详情",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,帖子不存在或被删除")
})
public Result<PostInfoDTO> getPostDetail(@RequestParam("id") Long id) {
return Result.success(postService.getPostDetail(id));
public Post getPostDetail(@RequestParam("id") Long id) {
return postService.getPostDetail(id);
}
// 分页查询帖子
@GetMapping("/list")
@Operation(
summary = "分页查询帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
public Result<PageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePost(postPageQueryDTO));
}
// 查看用户的帖子
@GetMapping("/user")
@Operation(
summary = "查看用户的帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfUser(postPageQueryDTO));
}
// 点赞帖子
@PutMapping("/like/{id}")
@Operation(
summary = "点赞帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "点赞成功"),
@ApiResponse(responseCode = "500", description = "点赞失败,帖子不存在或被删除")
})
public Result<Void> likePost(@PathVariable("id") Long id) {
postService.likePost(id);
return Result.success();
// 查看自己的帖子
@GetMapping("/of/me")
public Result<PageResponse<PostBasicInfoDTO>> pagePostOfMe(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfMe(postPageQueryDTO));
}
}

@ -1,20 +1,9 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(title = "分页查询评论请求DTO")
public class CommentPageQueryDTO extends ScrollPageRequest {
@Schema(title = "帖子ID")
private Long postId;
@Schema(title = "评论ID")
private Long parentCommentId;
private Boolean orderByTime = true;
private Boolean orderByHot = false;
public class CommentPageQueryDTO extends PageRequest {
private Integer postId;
}

@ -1,41 +0,0 @@
package com.luojia_channel.modules.post.dto.req;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "评论保存DTO")
public class CommentSaveDTO {
@Schema(
description = "评论id"
)
private Long id;
@Schema(
description = "评论内容",
required = true,
minLength = 1,
maxLength = 500,
example = "+3"
)
private String content;
@Schema(
description = "评论的帖子id"
)
private Long postId;
@Schema(
description = "该评论的父评论id若不是回复则传入空值"
)
private Long parentCommentId;
}

@ -1,14 +1,9 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PostPageQueryDTO extends ScrollPageRequest {
@Schema(
description = "想要查看的用户的id输入空时为自己的id"
)
private Long userId;
public class PostPageQueryDTO extends PageRequest {
}

@ -1,49 +1,12 @@
package com.luojia_channel.modules.post.dto.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "帖子保存DTO")
public class PostSaveDTO {
@Schema(
description = "帖子ID"
)
private Long id;
@Schema(
description = "帖子标题",
required = true,
minLength = 3,
maxLength = 16,
example = "帖子标题三个字"
)
private String title;
@Schema(
description = "帖子封面图"
)
private String image;
@Schema(
description = "帖子内容",
required = true,
minLength = 4,
maxLength = 10000,
example = "zsbd"
)
private String content;
@Schema(
description = "帖子类型id"
)
private Long categoryId;
@Schema(
description = "是否匿名发布1-匿名0-不匿名",
allowableValues = {"0", "1"},
example = "0",
implementation = Integer.class
)
private Integer status; // 是否匿名
}

@ -1,6 +1,5 @@
package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -13,69 +12,15 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Schema(description = "评论信息")
public class CommentInfoDTO {
@Schema(
description = "评论id"
)
private Long id;
@Schema(
description = "评论内容",
minLength = 1,
maxLength = 500,
example = "+3"
)
private String content;
@Schema(
description = "评论点赞数"
)
private Long likeCount;
@Schema(
description = "评论回复数"
)
private Long replyCount;
@Schema(
description = "评论用户id"
)
private Long userId;
@Schema(
description = "评论对应的帖子id"
)
private String postType;
private Long postId;
@Schema(
description = "评论对应的父级评论id"
)
private Long parentCommentId;
@Schema(
description = "评论对应的顶级评论id"
)
private Long topId;
@Schema(
description = "评论创建时间"
)
private LocalDateTime createTime;
@Schema(
description = "当前用户是否对评论点过赞,1-已点赞0-未点赞",
allowableValues = {"0", "1"},
example = "1",
implementation = Boolean.class
)
private Boolean isLike;
private String userName;
private String userAvatar;
@Schema(
description = "子评论列表"
)
private List<CommentInfoDTO> commentInfoDTOList;
}

@ -1,81 +1,20 @@
package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "帖子基本信息")
public class PostBasicInfoDTO {
@Schema(
description = "帖子ID"
)
private Long id;
@Schema(
description = "帖子封面图"
)
private String image;
@Schema(
description = "帖子标题",
required = true,
minLength = 3,
maxLength = 16,
example = "帖子标题三个字"
)
private String title;
@Schema(
description = "帖子摘要显示帖子内容的前20个字"
)
private String summary;
@Schema(
description = "点赞数"
)
private Integer likeCount;
@Schema(
description = "评论数"
)
private Integer commentCount;
@Schema(
description = "收藏数"
)
private Integer favoriteCount;
@Schema(
description = "帖子浏览数"
)
private Integer viewCount;
@Schema(
description = "是否点赞,1-已点赞0-未点赞",
allowableValues = {"0", "1"},
example = "1",
implementation = Boolean.class
)
private Boolean isLike;
@Schema(
description = "对应的用户ID"
)
private Long userId;
// 对于匿名的帖子,下述字段进行了处理,如匿名默认名称,头像
@Schema(
description = "匿名情况下用户名"
)
private String userName;
@Schema(
description = "匿名情况下用户头像"
)
private String userAvatar;
@Schema(
description = "帖子创建时间"
)
private LocalDateTime createTime;
}

@ -1,86 +1,22 @@
package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import com.luojia_channel.modules.post.entity.Comment;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Schema(description = "修改帖子信息")
public class PostInfoDTO {
@Schema(
description = "帖子ID"
)
private Long id;
@Schema(
description = "帖子封面图"
)
private String image;
@Schema(
description = "帖子标题",
required = true,
minLength = 3,
maxLength = 16,
example = "帖子标题三个字"
)
private String title;
@Schema(
description = "帖子内容",
required = true,
minLength = 4,
maxLength = 10000,
example = "zsbd"
)
private String content;
@Schema(
description = "点赞数"
)
private Integer likeCount;
@Schema(
description = "评论数"
)
private Integer commentCount;
@Schema(
description = "收藏数"
)
private Integer favoriteCount;
private String content;
@Schema(
description = "帖子浏览数"
)
private Integer viewCount;
@Schema(
description = "是否点赞,1-已点赞0-未点赞",
allowableValues = {"0", "1"},
example = "1",
implementation = Boolean.class
)
private Boolean isLike;
@Schema(
description = "对应用户ID"
)
private Long userId;
// 对于匿名的帖子,下述字段进行了处理,如匿名默认名称,头像
@Schema(
description = "匿名情况下用户名"
)
private String userName;
@Schema(
description = "匿名情况下用户头像"
)
private String userAvatar;
@Schema(
description = "帖子创建时间"
)
private LocalDateTime createTime;
private List<Comment> commentList;
}

@ -16,9 +16,8 @@ import java.time.LocalDateTime;
public class Comment {
private Long id;
private String content;
private Long likeCount;
private Long replyCount;
private Long userId;
private String postType;
private Long postId;
private Long parentCommentId;
private Long topId;

@ -2,11 +2,7 @@ package com.luojia_channel.modules.post.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Comment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
List<Comment> selectByIdsOrderByField(@Param("ids") List<Long> ids);
}

@ -4,11 +4,7 @@ package com.luojia_channel.modules.post.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Post;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface PostMapper extends BaseMapper<Post> {
List<Post> selectByIdsOrderByField(@Param("ids")List<Long> ids);
}

@ -1,27 +0,0 @@
package com.luojia_channel.modules.post.mq.producer;
import com.luojia_channel.modules.message.mq.AbstractSendProduceTemplate;
import com.luojia_channel.modules.message.mq.BaseSendExtendDTO;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class NotificationProducer extends AbstractSendProduceTemplate<NotificationMessage> {
public NotificationProducer(@Autowired RabbitTemplate rabbitTemplate){
super(rabbitTemplate);
}
@Override
protected BaseSendExtendDTO buildBaseSendParam(NotificationMessage messageSendEvent) {
return BaseSendExtendDTO.builder()
.eventName("NotificationMessageEvent")
.exchange("notify.exchange")
.routingKey("notify.routing.key")
.keys(messageSendEvent.getSenderId().toString())
.delay(null)
.build();
}
}

@ -1,9 +1,7 @@
package com.luojia_channel.modules.post.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
import com.luojia_channel.modules.post.entity.Comment;
import org.springframework.stereotype.Service;
@ -13,15 +11,13 @@ import java.util.List;
@Service
public interface CommentService {
Long saveComment(CommentSaveDTO commentSaveDTO);
void saveComment(Comment comment);
void updateComment(CommentSaveDTO commentSaveDTO);
void updateComment(Comment comment, Long userId);
void deleteComment(Long id);
void deleteComment(Long id, Long userId);
ScrollPageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO);
PageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO);
ScrollPageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO);
void likeComment(Long id);
List<CommentInfoDTO> getNestedCommentsByPostId(Long postId);
}

@ -2,16 +2,14 @@ package com.luojia_channel.modules.post.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.dto.resp.PostInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import org.springframework.web.multipart.MultipartFile;
public interface PostService extends IService<Post> {
Long savePost(PostSaveDTO postSaveDTO);
void createPost(PostSaveDTO postSaveDTO);
String setCover(MultipartFile file);
@ -19,11 +17,9 @@ public interface PostService extends IService<Post> {
void deletePost(Long id);
PostInfoDTO getPostDetail(Long id);
Post getPostDetail(Long id);
ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO);
PageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO);
ScrollPageResponse<PostBasicInfoDTO> pagePostOfUser(PostPageQueryDTO postPageQueryDTO);
void likePost(Long id);
PageResponse<PostBasicInfoDTO> pagePostOfMe(PostPageQueryDTO postPageQueryDTO);
}

@ -1,317 +1,91 @@
package com.luojia_channel.modules.post.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.exception.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.post.mq.producer.NotificationProducer;
import com.luojia_channel.modules.post.service.CommentService;
import com.luojia_channel.modules.post.utils.ValidatePostUtil;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
private final ValidatePostUtil validatePostUtil;
private final CommentMapper commentMapper;
private final UserMapper userMapper;
private final NotificationProducer notificationProducer;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
private CommentMapper commentMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long saveComment(CommentSaveDTO commentSaveDTO) {
Long userId = UserContext.getUserId();
if (userId == null) {
throw new UserException("用户未登录");
}
validatePostUtil.validateComment(commentSaveDTO);
Comment comment = BeanUtil.copyProperties(commentSaveDTO, Comment.class);
comment.setUserId(userId);
public void saveComment(Comment comment) {
comment.setCreateTime(LocalDateTime.now());
comment.setUpdateTime(LocalDateTime.now());
if (!save(comment)) {
throw new PostException("创建评论失败");
}
// 保存成功后,若为根级评论,设置 topId 为评论 id
if (comment.getParentCommentId() == null) {
comment.setTopId(comment.getId());
comment.setParentCommentId(0L);
if (!updateById(comment)) {
throw new PostException("更新根级评论 topId 失败");
}
// 同时更新帖子的zset列表
String key = "post:comment_by_time:" + comment.getPostId();
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
}
// 帖子的回复数加一
Long postId = comment.getPostId();
Post post = postMapper.selectById(postId);
if (post == null) {
throw new PostException("回复的帖子不存在");
}
post.setCommentCount(post.getCommentCount() + 1);
if(postMapper.updateById(post) <= 0){
throw new PostException("回复帖子失败");
}
Long receiveUserId = post.getUserId();
Long parentCommentId = comment.getParentCommentId();
// 消息通知,回复帖子
if (!userId.equals(receiveUserId) && parentCommentId == 0) {
String content = String.format("%s 回复了你的帖子: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(receiveUserId)
.content(content)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
}
if (parentCommentId != 0) {
// 是回复的评论
Comment parentComment = commentMapper.selectById(parentCommentId);
if (parentComment == null) {
throw new PostException("父评论不存在");
}
// 设置顶级评论id
comment.setTopId(parentComment.getTopId());
updateById(comment);
// 更新评论的zset回复列表
String buildKey = String.format("%d_%d", comment.getPostId(), comment.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
// 更新顶级评论回复数,当顶级评论与回复评论不同时
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.eq(Comment::getId, comment.getTopId())
.setSql("reply_count = reply_count + 1");
int update = commentMapper.update(null, updateWrapper);
if (update <= 0) {
throw new PostException("回复顶级评论失败");
}
// 消息通知,回复评论
String content = String.format("%s 回复了你的评论: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(parentComment.getUserId())
.content(content)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
}
redisUtil.delete("post:detail:" + comment.getPostId());
return comment.getId();
commentMapper.insert(comment);
}
@Override
public void updateComment(CommentSaveDTO commentSaveDTO) {
validatePostUtil.validateComment(commentSaveDTO);
validatePostUtil.validateCommentOwnership(commentSaveDTO.getId());
Comment comment = BeanUtil.copyProperties(commentSaveDTO, Comment.class);
comment.setUserId(UserContext.getUserId());
public void updateComment(Comment comment, Long userId) {
validatePostOwnership(comment.getId(), userId);
comment.setUpdateTime(LocalDateTime.now());
if(!updateById(comment)){
throw new PostException("更新评论失败");
throw new PostException("更新帖子失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteComment(Long id) {
validatePostUtil.validateCommentOwnership(id);
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getTopId, id);
int delete = commentMapper.delete(queryWrapper);
if(delete <= 0) {
throw new PostException("删除评论失败");
}
Comment comment = commentMapper.selectById(id);
if(comment.getId().equals(comment.getTopId())) {
redisUtil.zRemove("post:comment_by_time:" + comment.getPostId(), comment.getId());
redisUtil.delete("comment:reply_by_time:" + comment.getTopId());
}else{
redisUtil.zRemove("comment:reply_by_time:" + comment.getTopId(), comment.getId());
}
// TODO 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论
public void deleteComment(Long id, Long userId) {
validatePostOwnership(id, userId);
commentMapper.deleteById(id);
}
// 分页查询一系列根评论
@Override
public ScrollPageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getPostId() == null || commentPageQueryDTO.getPostId() < 0){
throw new PostException("帖子id不合法");
}
/*
public PageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getPostId, commentPageQueryDTO.getPostId())
.eq(Comment::getParentCommentId, 0L)
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String key = "post:comment_by_time:" + commentPageQueryDTO.getPostId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<CommentInfoDTO> commentInfoDTOS = new ArrayList<>();
for(Comment comment : comments){
CommentInfoDTO commentInfoDTO = BeanUtil.copyProperties(comment, CommentInfoDTO.class);
User user = userMap.getOrDefault(comment.getUserId(), new User());
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
commentInfoDTOS.add(commentInfoDTO);
}
return commentInfoDTOS;
});
}
@Override
public ScrollPageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getParentCommentId() == null || commentPageQueryDTO.getParentCommentId() < 0){
throw new PostException("评论id不合法");
}
/*
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getTopId, commentPageQueryDTO.getCommentId())
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String buildKey = String.format("%d_%d", commentPageQueryDTO.getPostId(), commentPageQueryDTO.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<CommentInfoDTO> commentInfoDTOS = new ArrayList<>();
for(Comment comment : comments){
CommentInfoDTO commentInfoDTO = BeanUtil.copyProperties(comment, CommentInfoDTO.class);
User user = userMap.getOrDefault(comment.getUserId(), new User());
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
commentInfoDTOS.add(commentInfoDTO);
}
return commentInfoDTOS;
});
IPage<Comment> commentPage = commentMapper.selectPage(PageUtil.convert(commentPageQueryDTO), queryWrapper);
return PageUtil.convert(commentPage, CommentInfoDTO.class);
}
@Override
public void likeComment(Long id) {
Long userId = UserContext.getUserId();
String likeCommentKey = "comment:like:" + id;
RedisTemplate<String, Object> redisTemplate = redisUtil.getInstance();
// 检查是否未点赞
if (!isLikedComment(id)) {
// 数据库点赞记录加一
boolean success = updateCommentLikeCount(id, 1);
if (success) {
redisTemplate.opsForSet().add(likeCommentKey, userId, System.currentTimeMillis());
}
} else {
// 数据库点赞记录减一
boolean success = updateCommentLikeCount(id, -1);
if (success) {
redisTemplate.opsForSet().remove(likeCommentKey, userId);
}
}
}
private boolean isLikedComment(Long commentId) {
Long userId = UserContext.getUserId();
String likeCommentKey = "comment:like:" + commentId;
RedisTemplate<String, Object> redisTemplate = redisUtil.getInstance();
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(likeCommentKey, userId));
}
public List<CommentInfoDTO> getNestedCommentsByPostId(Long postId) {
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getPostId, postId)
.orderByAsc(Comment::getCreateTime);
private boolean updateCommentLikeCount(Long commentId, int increment) {
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.setSql("like_count = like_count + " + increment)
.eq(Comment::getId, commentId);
return commentMapper.update(null, updateWrapper) > 0;
List<Comment> comments = commentMapper.selectList(queryWrapper);
return buildNestedComments(comments);
}
/*
private PageResponse<CommentInfoDTO> getCommentInfoDTOPageResponse(CommentPageQueryDTO commentPageQueryDTO, LambdaQueryWrapper<Comment> queryWrapper) {
IPage<Comment> commentPage = commentMapper.selectPage(PageUtil.convert(commentPageQueryDTO), queryWrapper);
List<Long> userIds = new ArrayList<>();
commentPage.getRecords().forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
return PageUtil.convert(commentPage, (comment) -> {
CommentInfoDTO commentInfoDTO = BeanUtil.copyProperties(comment, CommentInfoDTO.class);
User user = userMap.getOrDefault(comment.getUserId(), new User());
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
return commentInfoDTO;
});
private void validatePostOwnership(Long commentId, Long userId) {
Comment comment = commentMapper.selectById(commentId);
if (comment == null) {
throw new PostException("评论不存在");
}
if (!userId.equals(comment.getUserId())) {
throw new PostException("你无权操作他人的评论");
}
}
*/
private List<CommentInfoDTO> convertToDTO(List<Comment> commentList) {
private List<CommentInfoDTO> convertToDTO(List<Comment> comments) {
List<CommentInfoDTO> dtos = new ArrayList<>();
for (Comment comment : commentList) {
for (Comment comment : comments) {
CommentInfoDTO dto = new CommentInfoDTO();
BeanUtils.copyProperties(comment, dto);
dtos.add(dto);
@ -319,14 +93,16 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return dtos;
}
// 根据评论创建嵌套评论,目前没用到
private List<CommentInfoDTO> buildNestedCommentList(List<Comment> comments) {
private List<CommentInfoDTO> buildNestedComments(List<Comment> comments) {
Map<Long, CommentInfoDTO> map = new HashMap<>();
List<CommentInfoDTO> rootComments = new ArrayList<>();
for (Comment comment : comments) {
CommentInfoDTO dto = new CommentInfoDTO();
BeanUtils.copyProperties(comment, dto);
map.put(comment.getId(), dto);
if (comment.getParentCommentId() == null) {
rootComments.add(dto);
} else {
@ -339,6 +115,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
}
}
}
return rootComments;
}
}

@ -6,39 +6,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.exception.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.service.impl.FileServiceImpl;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.impl.FollowServiceImpl;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.dto.resp.PostInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.post.service.PostService;
import com.luojia_channel.modules.post.utils.ValidatePostUtil;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@ -48,45 +32,16 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
private final FileServiceImpl fileService;
private final ValidatePostUtil validatePostUtil;
private final RedisUtil redisUtil;
private final UserMapper userMapper;
// 匿名用户名与匿名头像
private static final String ANONYMOUS_NAME = "匿名用户";
private static final String ANONYMOUS_AVATAR = "";
private final FollowMapper followMapper;
private final FollowServiceImpl followService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long savePost(PostSaveDTO postSaveDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
public void createPost(PostSaveDTO postSaveDTO) {
validatePostUtil.validatePost(postSaveDTO);
Long userId = UserContext.getUserId();
Post post = BeanUtil.copyProperties(postSaveDTO, Post.class);
post.setUserId(UserContext.getUserId());
post.setCreateTime(LocalDateTime.now());
post.setUpdateTime(LocalDateTime.now());
post.setUserId(userId);
if(!save(post)){
throw new PostException("创建帖子失败");
}
redisUtil.delete("post:detail:" + postSaveDTO.getId());
//redisUtil.delete("post:of:user:" + UserContext.getUserId());
// 非匿名帖子推送给粉丝
if(post.getStatus().equals(0)){
List<Follow> follows = followMapper.selectList(Wrappers.lambdaQuery(Follow.class)
.eq(Follow::getFollowUserId, userId));
for(Follow follow : follows){
Long fansId = follow.getUserId();
String key = "post:follow_of:" + fansId;
redisUtil.zAdd(key, post.getId(), System.currentTimeMillis());
}
// TODO 消息通知?
}
redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis());
return post.getId();
}
@Override
@ -97,227 +52,51 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
@Override
public void updatePost(PostSaveDTO postSaveDTO) {
validatePostUtil.validatePost(postSaveDTO);
validatePostUtil.validatePostOwnership(postSaveDTO.getId());
Post post = BeanUtil.copyProperties(postSaveDTO, Post.class);
post.setUserId(UserContext.getUserId());
post.setUpdateTime(LocalDateTime.now());
if(!updateById(post)){
throw new PostException("更新帖子失败");
}
redisUtil.delete("post:detail:" + postSaveDTO.getId());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
}
@Override
public void deletePost(Long id) {
validatePostUtil.validatePostOwnership(id);
Long userId = UserContext.getUserId();
int delete = postMapper.deleteById(id);
if(delete <= 0){
throw new PostException("删除帖子失败");
}
// redisUtil.delete("post:detail:" + id.toString());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
redisUtil.delete("post:detail:" + id);
redisUtil.zRemove("post:time:", id);
redisUtil.zRemove("post:user:" + userId, id);
}
@Override
public PostInfoDTO getPostDetail(Long id) {
// TODO 定时任务更新浏览数或消息队列解耦,懒得写了
Post oldPost = getById(id);
oldPost.setViewCount(oldPost.getViewCount() + 1);
updateById(oldPost);
return redisUtil.safeGet("post:detail:" + id, PostInfoDTO.class,
public Post getPostDetail(Long id) {
return redisUtil.safeGet("post:detail" + id.toString(), Post.class,
() -> {
Post post = getById(id);
if(post == null){
throw new PostException("帖子不存在或被删除");
}
PostInfoDTO postInfoDTO = BeanUtil.copyProperties(post, PostInfoDTO.class);
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
.eq(User::getId, post.getUserId()));
postInfoDTO.setUserAvatar(user.getAvatar());
postInfoDTO.setUserName(user.getUsername());
if (post.getStatus() == 1) { // 匿名帖子
postInfoDTO.setUserName(ANONYMOUS_NAME);
postInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
postInfoDTO.setIsLike(isLikedPost(post.getId()));
return postInfoDTO;
},
1, TimeUnit.MINUTES);
return post;
}, 10, TimeUnit.MINUTES);
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
public PageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
// TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则
/*
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
List<Long> userIds = new ArrayList<>();
postPage.getRecords().forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
// 组装用户头像与名称,批量查询只要查一次数据库
return PageUtil.convert(postPage, post -> {
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
User user = userMap.getOrDefault(post.getUserId(), new User());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTO.setUserName(user.getUsername());
if (post.getStatus() == 1) { // 匿名帖子
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
return postBasicInfoDTO;
});
*/
String key = "post:time:";
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Long> userIds = new ArrayList<>();
List<Post> posts = postMapper.selectByIdsOrderByField(postIds);
posts.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
// 组装用户头像与名称,批量查询只要查一次数据库
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
posts.forEach(post -> {
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
User user = userMap.getOrDefault(post.getUserId(), new User());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setIsLike(isLikedPost(post.getId()));
String content = post.getContent();
if (content != null) {
content = content.trim();
String summary = content.substring(0, Math.min(content.length(), 15));
postBasicInfoDTO.setSummary(summary);
} else {
postBasicInfoDTO.setSummary("该帖子内容为空哦");
}
if (post.getStatus() == 1) { // 匿名帖子
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
postBasicInfoDTOS.add(postBasicInfoDTO);
});
return postBasicInfoDTOS;
});
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) {
Long myUserId = UserContext.getUserId();
if(myUserId == null){
throw new UserException("用户未登录");
}
Long userId = postPageQueryDTO.getUserId()==null ? myUserId : postPageQueryDTO.getUserId();
/*
// 构建包含分页信息的缓存键
String cacheKey = "post:of:user:" + userId + ":page:" + postPageQueryDTO.getCurrent() + ":size:" + postPageQueryDTO.getSize();
return redisUtil.safeGet(cacheKey, PageResponse.class,
() -> {
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.eq(Post::getUserId, userId);
// 当查询别人时,不显示匿名帖子
if(!myUserId.equals(userId)){
queryWrapper.eq(Post::getStatus, 0);
}
queryWrapper.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
return PageUtil.convert(postPage, post -> {
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserAvatar(UserContext.getAvatar());
postBasicInfoDTO.setUserName(UserContext.getUsername());
if (post.getStatus() == 1) { // 自己的匿名帖子
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
return postBasicInfoDTO;
});
},
60, TimeUnit.MINUTES);
*/
String key = "post:user:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Long> userIds = new ArrayList<>();
List<Post> posts = postMapper.selectByIdsOrderByField(postIds);
posts.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
// 组装用户头像与名称,批量查询只要查一次数据库
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
User user = userMap.getOrDefault(post.getUserId(), new User());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setIsLike(isLikedPost(post.getId()));
String content = post.getContent();
if (content != null) {
content = content.trim();
String summary = content.substring(0, Math.min(content.length(), 15));
postBasicInfoDTO.setSummary(summary);
} else {
postBasicInfoDTO.setSummary("该帖子内容为空哦");
}
if (post.getStatus() == 1) { // 自己的匿名帖子
if(!userId.equals(user.getId())){
continue;
}
postBasicInfoDTO.setUserName(ANONYMOUS_NAME);
postBasicInfoDTO.setUserAvatar(ANONYMOUS_AVATAR);
}
postBasicInfoDTOS.add(postBasicInfoDTO);
}
return postBasicInfoDTOS;
});
return PageUtil.convert(postPage, PostBasicInfoDTO.class);
}
private boolean isLikedPost(Long postId){
Long userId = UserContext.getUserId();
if(userId == null){
return false;
}
String likeBlogKey = "post:like:" + postId;
RedisTemplate<String, Object> redisTemplate = redisUtil.getInstance();
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(likeBlogKey, userId));
}
@Override
public void likePost(Long id) {
public PageResponse<PostBasicInfoDTO> pagePostOfMe(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
String likeBlogKey = "post:like:" + id;
RedisTemplate<String, Object> redisTemplate = redisUtil.getInstance();
//没点赞
if(!isLikedPost(id)){
//数据库点赞记录加一
boolean success = update().setSql("like_count = like_count + 1").eq("id",id).update();
if(success){
redisTemplate.opsForSet().add(likeBlogKey, userId, System.currentTimeMillis());
}
}else{
//数据库点赞记录减一
boolean success = update().setSql("like_count = like_count - 1").eq("id",id).update();
if(success){
redisTemplate.opsForSet().remove(likeBlogKey, userId);
}
}
redisUtil.delete("post:detail:" + id);
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.eq(Post::getUserId, userId)
.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
return PageUtil.convert(postPage, PostBasicInfoDTO.class);
}
}

@ -2,13 +2,9 @@ package com.luojia_channel.modules.post.utils;
import cn.hutool.core.util.StrUtil;
import com.luojia_channel.common.exception.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@ -20,8 +16,6 @@ public class ValidatePostUtil {
private final PostMapper postMapper;
private final CommentMapper commentMapper;
public void validatePost(PostSaveDTO postSaveDTO) {
// 非空字段检验
if (StrUtil.isBlank(postSaveDTO.getTitle())) {
@ -40,12 +34,6 @@ public class ValidatePostUtil {
public void validatePostOwnership(Long id){
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
if(id == null){
throw new PostException("传入id不能为空");
}
Post post = postMapper.selectById(id);
if(post == null){
throw new PostException("帖子不存在");
@ -55,32 +43,7 @@ public class ValidatePostUtil {
}
}
public void validateComment(CommentSaveDTO commentSaveDTO) {
if (StrUtil.isBlank(commentSaveDTO.getContent())) {
throw new PostException("内容不能为空");
}
if (commentSaveDTO.getPostId() == null || commentSaveDTO.getPostId() < 0){
throw new PostException("帖子id不合法");
}
if(commentSaveDTO.getParentCommentId() != null && commentSaveDTO.getParentCommentId() < 0){
throw new PostException("父评论id不合法");
}
}
public void AIValidate(PostSaveDTO postSaveDTO){
public void validateCommentOwnership(Long id) {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
if(id == null){
throw new PostException("传入id不能为空");
}
Comment comment = commentMapper.selectById(id);
if(comment == null){
throw new PostException("评论不存在");
}
if(!userId.equals(comment.getUserId())){
throw new PostException("你无权操作他人的评论");
}
}
}

@ -1,4 +0,0 @@
package com.luojia_channel.modules.search.controller;
public class SearchController {
}

@ -1,36 +0,0 @@
package com.luojia_channel.modules.search.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.time.LocalDateTime;
@Data
@Document(indexName = "post_index")
public class EsPost {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
private String image;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content;
private Integer status;
private Integer likeCount;
private Integer commentCount;
private Integer favoriteCount;
private Integer viewCount;
private Long userId;
private Long categoryId;
private LocalDateTime createTime;
}

@ -1,30 +0,0 @@
package com.luojia_channel.modules.search.entity;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Data
@Document(indexName = "user_index")
public class EsUser {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String username;
@Field(type = FieldType.Keyword)
private String studentId;
private String avatar;
private Integer gender;
private Integer integral;
@Field(type = FieldType.Keyword)
private String college;
}

@ -1,4 +0,0 @@
package com.luojia_channel.modules.search.service;
public class SearchService {
}

@ -1,4 +0,0 @@
package com.luojia_channel.modules.search.task;
public class DataSyncTask {
}

@ -4,10 +4,6 @@ import com.luojia_channel.common.domain.Result;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -15,46 +11,21 @@ import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/user/info")
@RequiredArgsConstructor
@Tag(name = "用户信息管理", description = "用户修改个人信息相关接口")
public class UserInfoController {
private final UserInfoService userInfoService;
@PostMapping("/update")
@Operation(
summary="修改用户信息",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "修改成功"),
@ApiResponse(responseCode = "500", description = "修改失败,请稍后重试")
})
public Result<Void> updateInfo(@RequestBody UserChangeInfoDTO userChangeInfoDTO){
userInfoService.updateInfo(userChangeInfoDTO);
return Result.success();
}
@PostMapping("/password")
@Operation(
summary="用户修改密码",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "修改成功"),
@ApiResponse(responseCode = "500", description = "修改失败,请稍后重试")
})
public Result<Void> updatePassword(@RequestParam String password){
userInfoService.updatePassword(password);
return Result.success();
}
@PostMapping("/avatar")
@Operation(
summary="用户上传头像",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "上传成功"),
@ApiResponse(responseCode = "500", description = "上传失败,请稍后重试")
})
public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
return Result.success(userInfoService.uploadAvatar(file));
}

@ -2,153 +2,37 @@ package com.luojia_channel.modules.user.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.user.utils.CaptchaUtils;
import com.luojia_channel.modules.user.dto.UserLoginDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.service.UserLoginService;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户登陆注册相关接口")
public class UserLoginController {
private final UserLoginService userLoginService;
private final RedisUtil redisUtil;
@PostMapping("/login")
@Operation(
summary = "用户登录",
description = "支持用户名/手机号/邮箱登录",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = UserDTO.class))),
@ApiResponse(responseCode = "500", description = "登录失败,用户名或密码错误")
})
public Result<UserDTO> login(@RequestBody UserLoginDTO userLoginDTO){
return Result.success(userLoginService.login(userLoginDTO));
}
@PostMapping("/register")
@Operation(
summary = "用户注册",
description = "支持用户名/手机号/邮箱注册",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "注册成功"),
@ApiResponse(responseCode = "500", description = "注册失败,请稍后重试")
})
public Result<UserDTO> register(@RequestBody UserRegisterDTO userRegisterDTO){
return Result.success(userLoginService.register(userRegisterDTO));
}
@PostMapping("/logout")
@Operation(
summary = "用户登出",
tags = {"用户管理"}
)
public Result<Void> logout(HttpServletRequest request){
userLoginService.logout(request);
return Result.success();
}
@Hidden
@PostMapping("/hello")
@Operation(summary = "测试接口")
public Result<String> hello(){
return Result.success("hello");
}
/**
*
* @param request
* @param res
* @throws IOException
*/
@GetMapping("/captcha")
@Operation(
summary = "生成验证码图片",
description = "生成图形验证码并设置Cookie存储captchaKey",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "验证码生成成功"),
@ApiResponse(responseCode = "500", description = "验证码生成失败")
})
public void generateCaptcha(HttpServletRequest request,
HttpServletResponse res) throws IOException {
CaptchaUtils captcha = new CaptchaUtils();
BufferedImage image = captcha.getImage();
String text = captcha.getText();
String captchaKey = UUID.randomUUID().toString();
redisUtil.set("captcha:" + captchaKey, text, 60, TimeUnit.SECONDS);
Cookie cookie = new Cookie("captchaKey", captchaKey);
cookie.setPath("/");
res.addCookie(cookie);
CaptchaUtils.output(image,res.getOutputStream());
}
/**
*
* @param session
* @param params
* @return
*/
@PostMapping("/verify-captcha")
@Operation(
summary = "验证图形验证码",
description = "验证用户输入的图形验证码是否正确",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "验证码验证成功"),
@ApiResponse(responseCode = "500", description = "验证码已失效或验证码错误")
})
public Result verifyCaptcha(@RequestBody Map<String, String> params,
@CookieValue(value = "captchaKey", required = false) String captchaKey,
HttpSession session) {
String captcha = params.get("captcha");
if (captchaKey == null) {
return Result.fail(500, "验证码已失效,请重新获取");
}
String redisKey = "captcha:" + captchaKey;
String correctCaptcha = redisUtil.get(redisKey, String.class);
if (correctCaptcha == null) {
return Result.fail(500, "验证码已过期,请重新获取");
}
if (captcha.equalsIgnoreCase(correctCaptcha)) {
redisUtil.delete(redisKey);
return Result.success();
} else {
return Result.fail(500, "图形验证码错误");
}
}
}

@ -1,46 +1,20 @@
package com.luojia_channel.modules.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户修改信息DTO")
public class UserChangeInfoDTO {
@Schema(
description = "用户名"
)
private String username;
@Schema(
description = "手机号,11位数字"
)
private String phone;
@Schema(
description = "邮箱"
)
private String email;
@Schema(
description = "学生学号"
)
private String studentId;
@Schema(
description = "头像url"
)
private String avatar;
@Schema(
description = "性别,0-未知1-男2-女",
allowableValues = {"0", "1", "2"},
example = "1",
implementation = Integer.class
)
private Integer gender;
@Schema(
description = "学院"
)
private String college;
}

@ -1,22 +1,11 @@
package com.luojia_channel.modules.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户登录DTO")
public class UserLoginDTO {
// 用户标志,支持学号,手机号,邮箱
@Schema(
description = "用户登录用标志,支持用户名,手机号,邮箱"
)
private String userFlag;
@Schema(
description = "用户登陆用密码",
required = true,
minLength = 6,
maxLength = 16
)
private String password;
}

@ -1,44 +1,16 @@
package com.luojia_channel.modules.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "用户注册DTO")
public class UserRegisterDTO {
@Schema(
description = "用户名",
example = "ttt",
required = true,
minLength = 1,
maxLength = 15
)
private String username;
@Schema(
description = "密码",
example = "123456",
required = true,
minLength = 6,
maxLength = 16,
format = "password"
)
private String password;
@Schema(
description = "手机号",
example = "12345678901",
pattern = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$"
)
private String phone;
@Schema(
description = "邮箱",
example = "123456@qq.com",
pattern = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"
)
private String email;
@Schema(description = "学生学号,暂不需要")
private String studentId;
}

@ -4,10 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<User> selectByIdsOrderByField(List<Long> ids);
}

@ -1,135 +0,0 @@
package com.luojia_channel.modules.user.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class CaptchaUtils {
/**
*
*/
private int width = 180;
/**
*
*/
private int height = 30;
/**
*
*/
private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" };
/**
*
*/
private Color bgColor = new Color(255, 255, 255);
/**
*
*/
private Random random = new Random();
/**
* code
*/
private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
/**
*
*/
private String text;
/**
*
* @return
*/
private Color randomColor() {
int red = random.nextInt(150);
int green = random.nextInt(150);
int blue = random.nextInt(150);
return new Color(red, green, blue);
}
/**
*
*
* @return
*/
private Font randomFont() {
String name = fontNames[random.nextInt(fontNames.length)];
int style = random.nextInt(4);
int size = random.nextInt(5) + 24;
return new Font(name, style, size);
}
/**
*
*
* @return
*/
private char randomChar() {
return codes.charAt(random.nextInt(codes.length()));
}
/**
* BufferedImage
*
* @return
*/
private BufferedImage createImage() {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
//设置验证码图片的背景颜色
g2.setColor(bgColor);
g2.fillRect(0, 0, width, height);
return image;
}
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g2 = (Graphics2D) image.getGraphics();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 4; i++) {
String s = randomChar() + "";
sb.append(s);
g2.setColor(randomColor());
g2.setFont(randomFont());
float x = i * width * 1.0f / 4;
g2.drawString(s, x, height - 8);
}
this.text = sb.toString();
drawLine(image);
return image;
}
/**
* 线
*
* @param image
*/
private void drawLine(BufferedImage image) {
Graphics2D g2 = (Graphics2D) image.getGraphics();
int num = 5;
for (int i = 0; i < num; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g2.setColor(randomColor());
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(x1, y1, x2, y2);
}
}
public String getText() {
return text;
}
public static void output(BufferedImage image, OutputStream out) throws IOException {
ImageIO.write(image, "PNG", out);
}
}

@ -21,7 +21,7 @@ public class ValidateUserUtil {
private final UserMapper userMapper;
/**
*
*
*
* @param userRegisterDTO
*/
@ -30,6 +30,10 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
if (StrUtil.isBlank(username)){
throw new UserException("用户名不能为空");
}
if (StrUtil.isBlank(password)){
throw new UserException("密码不能为空");
}
@ -38,12 +42,12 @@ public class ValidateUserUtil {
int cnt = 0;
if(StrUtil.isNotBlank(phone)) cnt++;
if(StrUtil.isNotBlank(email)) cnt++;
if(StrUtil.isNotBlank(username)) cnt++;
if(StrUtil.isNotBlank(studentId)) cnt++;
if (cnt == 0) {
throw new UserException("必须填写手机号、邮箱或用户名其中一种注册方式");
throw new UserException("必须填写手机号、邮箱或学号其中一种注册方式");
}
if (cnt > 1) {
throw new UserException("只能选择一种注册方式(手机/邮箱/用户名");
throw new UserException("只能选择一种注册方式(手机/邮箱/学号");
}
// 格式校验
validateFormats(userRegisterDTO);
@ -56,6 +60,7 @@ public class ValidateUserUtil {
String password = userRegisterDTO.getPassword();
String phone = userRegisterDTO.getPhone();
String email = userRegisterDTO.getEmail();
String studentId = userRegisterDTO.getStudentId();
// 仅对非空字段做格式校验
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username))) {
@ -64,13 +69,14 @@ public class ValidateUserUtil {
if(!password.matches(PASSWORD_REGEX)){
throw new UserException("密码格式错误");
}
validateUserFlag(phone, email, null);
validateUserFlag(phone, email, studentId, null);
}
public void validateFormats(UserChangeInfoDTO userChangeInfoDTO, Long currentUserId){
String username = userChangeInfoDTO.getUsername();
String phone = userChangeInfoDTO.getPhone();
String email = userChangeInfoDTO.getEmail();
String studentId = userChangeInfoDTO.getStudentId();
// String college = userChangeInfoDTO.getCollege();
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, userChangeInfoDTO.getUsername())
@ -78,10 +84,10 @@ public class ValidateUserUtil {
throw new UserException("用户名已被使用");
}
validateUserFlag(phone, email, currentUserId);
validateUserFlag(phone, email, studentId, currentUserId);
}
private void validateUserFlag(String phone, String email, Long currentUserId) {
private void validateUserFlag(String phone, String email, String studentId, Long currentUserId) {
if(StrUtil.isNotBlank(phone)){
if(!phone.matches(PHONE_REGEX))
throw new UserException("手机号格式错误");
@ -100,6 +106,15 @@ public class ValidateUserUtil {
throw new UserException("邮箱已存在");
}
}
if(StrUtil.isNotBlank(studentId)){
if(!studentId.matches(STUDENTID_REGEX))
throw new UserException("学号格式错误");
if (userMapper.exists(Wrappers.<User>lambdaQuery()
.eq(User::getStudentId, studentId)
.ne(currentUserId != null, User::getId, currentUserId))) {
throw new UserException("学号已存在");
}
}
}
}

@ -1,36 +1,36 @@
#本地开发环境
lj:
db:
host: localhost
password: 1243969857
redis:
host: localhost
port: 6379
password: 1243969857
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
# 本地开发环境
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
minio:
endpoint: http://192.168.59.129:9000
accessKey: forely
secretKey: Forely123!

@ -1,15 +1,5 @@
server:
port: 8081
springdoc:
api-docs:
path: /openapi/luojia-channel
swagger-ui:
path: /swagger-ui.html
disable-swagger-default-url: true
tags-sorter: alpha
operations-sorter: alpha
packages-to-scan: com.luojia_channel.modules
paths-to-match: /user/**, /post/**, /comment/**, /message/**, /follow/**, /file/**
spring:
application:
name: service
@ -62,8 +52,5 @@ mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.luojia.luojia_channel.modules.*.entity
management:
health:
elasticsearch:
enabled: false

@ -0,0 +1,22 @@
INSERT INTO `post` (`title`, `image`, `content`, `status`, `like_count`, `comment_count`, `favorite_count`, `view_count`, `user_id`, `category_id`)
VALUES
('秋日散步记', 'http://example.com/images/post1.jpg', '今天去公园散步,看到了很多美丽的景色...', 0, 15, 8, 5, 100, 1, 1),
('美食推荐', 'http://example.com/images/post2.jpg', '这家餐厅的披萨非常好吃,强烈推荐给大家...', 1, 20, 12, 7, 150, 2, 2),
('旅行计划', 'http://example.com/images/post3.jpg', '计划下个月去云南旅游,期待已久的旅程...', 0, 10, 5, 3, 80, 3, 3),
('学习心得', 'http://example.com/images/post4.jpg', '最近学到了一个新的编程技巧,感觉很有用...', 0, 25, 18, 10, 200, 4, 4),
('电影分享', 'http://example.com/images/post5.jpg', '昨晚看了《无间道》,剧情非常精彩...', 1, 30, 20, 12, 250, 5, 5),
('健身日常', 'http://example.com/images/post6.jpg', '每天坚持锻炼身体,保持健康的生活方式...', 0, 5, 2, 1, 30, 6, 1),
('宠物趣事', 'http://example.com/images/post7.jpg', '我家的小狗今天做了件搞笑的事情...', 0, 8, 4, 2, 40, 7, 2),
('读书笔记', 'http://example.com/images/post8.jpg', '最近读了一本书,《活着》让人深思...', 1, 12, 6, 4, 60, 8, 3),
('科技前沿', 'http://example.com/images/post9.jpg', '最新的AI技术真的太神奇了改变了我们的生活...', 0, 18, 9, 6, 90, 9, 4),
('摄影技巧', 'http://example.com/images/post10.jpg', '分享几个摄影小技巧,让你的照片更加出色...', 0, 22, 11, 8, 110, 10, 5),
('音乐分享', 'http://example.com/images/post11.jpg', '最近发现了一首好听的歌,一起来听听吧...', 1, 7, 3, 2, 50, 1, 1),
('烹饪食谱', 'http://example.com/images/post12.jpg', '教大家做一道简单的家常菜...', 0, 14, 7, 5, 70, 2, 2),
('游戏体验', 'http://example.com/images/post13.jpg', '玩了一个新出的游戏,超级好玩...', 0, 21, 10, 7, 120, 3, 3),
('时尚搭配', 'http://example.com/images/post14.jpg', '分享我的最新穿搭,希望对你有所帮助...', 1, 9, 4, 3, 45, 4, 4),
('户外运动', 'http://example.com/images/post15.jpg', '周末和朋友们一起去徒步,真的很放松...', 0, 17, 8, 6, 85, 5, 5),
('艺术欣赏', 'http://example.com/images/post16.jpg', '欣赏了一些印象派画作,感受到了不一样的美...', 0, 11, 5, 2, 60, 6, 1),
('创业故事', 'http://example.com/images/post17.jpg', '一位朋友刚刚开始了他的创业之旅,很激动人心...', 1, 13, 6, 4, 75, 7, 2),
('健康饮食', 'http://example.com/images/post18.jpg', '分享一些健康的饮食习惯,让我们一起变得更健康...', 0, 19, 9, 7, 100, 8, 3),
('科技创新', 'http://example.com/images/post19.jpg', '最新的科技成果真是令人惊叹,未来可期...', 0, 24, 12, 8, 130, 9, 4),
('心灵感悟', 'http://example.com/images/post20.jpg', '写下了自己的内心感受,希望能够激励更多的人...', 1, 6, 3, 2, 55, 10, 5);

@ -101,11 +101,10 @@ DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`content` TEXT NOT NULL COMMENT '评论内容',
`like_count` INT DEFAULT 0 COMMENT '点赞数',
`reply_count` INT DEFAULT 0 COMMENT '回复数',
`user_id` BIGINT NOT NULL COMMENT '评论用户ID',
`post_type` VARCHAR(20) NOT NULL COMMENT '帖子类型post/video',
`post_id` BIGINT NOT NULL COMMENT '关联的帖子ID',
`parent_comment_id` BIGINT DEFAULT 0 COMMENT '父评论ID',
`parent_comment_id` BIGINT COMMENT '父评论ID',
`top_id` BIGINT COMMENT '顶层评论ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -152,27 +151,3 @@ CREATE TABLE `view_record` (
INDEX idx_post_type (post_type),
INDEX idx_post_id (post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='浏览记录表';
## 私信消息表
DROP TABLE IF EXISTS `message`;
CREATE TABLE message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
message_type TINYINT NOT NULL COMMENT '0系统消息1私信消息2评论通知',
content TEXT NOT NULL,
sender_id BIGINT NOT NULL,
receiver_id BIGINT NOT NULL,
create_time DATETIME NOT NULL,
INDEX idx_sender_id (sender_id),
INDEX idx_receiver_id (receiver_id),
INDEX idx_create_time (create_time)
);
## 关注表
DROP TABLE IF EXISTS `follow`;
CREATE TABLE `follow` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户id',
`follow_user_id` bigint(20) UNSIGNED NOT NULL COMMENT '关联的用户id',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关注表';

@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.message.mapper.MessageMapper">
<!-- 定义 ChatItemDTO 结果映射 -->
<resultMap id="ChatItemDTOResultMap" type="com.luojia_channel.modules.interact.dto.ChatItemDTO">
<id property="chatUserId" column="chat_user_id"/>
<result property="avatar" column="avatar"/>
<result property="username" column="username"/>
<result property="latestMessage" column="latest_message"/>
<result property="latestTime" column="create_time"/>
</resultMap>
<!-- 查询用户的所有聊天对象及最新消息 -->
<select id="selectChatList" resultMap="ChatItemDTOResultMap">
SELECT
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END AS chat_user_id,
u.avatar,
u.username,
m.content AS latest_message,
m.create_time
FROM message m
JOIN (
-- 子查询获取每个聊天对象的最新消息ID
SELECT
MAX(id) AS max_id
FROM message
WHERE sender_id = #{userId} OR receiver_id = #{userId}
GROUP BY
CASE
WHEN sender_id = #{userId} THEN receiver_id
ELSE sender_id
END
) latest ON m.id = latest.max_id
LEFT JOIN user u ON
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END = u.id
WHERE
m.sender_id = #{userId} OR m.receiver_id = #{userId}
GROUP BY
CASE
WHEN m.sender_id = #{userId} THEN m.receiver_id
ELSE m.sender_id
END
ORDER BY
m.create_time DESC
</select>
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.message.entity.MessageDO">
SELECT * FROM message
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.CommentMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Comment">
SELECT * FROM comment
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.post.mapper.PostMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Post">
SELECT * FROM post
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -2,11 +2,5 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luojia_channel.modules.user.mapper.UserMapper">
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.user.entity.User">
SELECT * FROM user
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
ORDER BY FIELD(id,
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
</select>
</mapper>

@ -1,36 +1,36 @@
#本地开发环境
lj:
db:
host: localhost
password: 1243969857
redis:
host: localhost
port: 6379
password: 1243969857
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
minio:
endpoint: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
# 本地开发环境
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.59.129
password: Forely123!
redis:
host: 192.168.59.129
port: 6379
password: Forely123!
rabbitmq:
host: 192.168.59.129
port: 5672
username: admin
password: Forely123!
minio:
endpoint: http://192.168.59.129:9000
accessKey: forely
secretKey: Forely123!

@ -1,15 +1,5 @@
server:
port: 8081
springdoc:
api-docs:
path: /openapi/luojia-channel
swagger-ui:
path: /swagger-ui.html
disable-swagger-default-url: true
tags-sorter: alpha
operations-sorter: alpha
packages-to-scan: com.luojia_channel.modules
paths-to-match: /user/**, /post/**, /comment/**, /message/**, /follow/**, /file/**
spring:
application:
name: service
@ -62,8 +52,5 @@ mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.luojia.luojia_channel.modules.*.entity
management:
health:
elasticsearch:
enabled: false

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save