Compare commits

..

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

@ -1,47 +0,0 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### Maven template
.idea/
**/target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Local
service/**/application-local.yaml

@ -7,12 +7,15 @@
<sourceOutputDir name="target/generated-sources/annotations" /> <sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<module name="common" />
<module name="service" /> <module name="service" />
</profile> </profile>
</annotationProcessing> </annotationProcessing>
</component> </component>
<component name="JavacSettings"> <component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE"> <option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="common" options="-parameters" />
<module name="luojia_channel" options="" />
<module name="service" options="-parameters" /> <module name="service" options="-parameters" />
</option> </option>
</component> </component>

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="0@192.168.59.129" uuid="ad808a5f-004d-4f31-9402-19010c1be1ab">
<driver-ref>redis</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>jdbc.RedisDriver</jdbc-driver>
<jdbc-url>jdbc:redis://192.168.59.129:6379/0</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="luojia_channel@192.168.59.129" uuid="666814a4-5179-4ab7-96f4-c2a810ed102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/service/src/main/resources/application.yaml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://192.168.59.129:3306/luojia_channel?useUnicode=true&amp;characterEncoding=UTF-8&amp;autoReconnect=true&amp;serverTimezone=Asia/Shanghai</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

@ -16,5 +16,10 @@
<option name="name" value="JBoss Community repository" /> <option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" /> <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component> </component>
</project> </project>

@ -8,5 +8,7 @@
</list> </list>
</option> </option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project> </project>

@ -0,0 +1,17 @@
{
"luatools_path": "~/go/bin",
"dto_dir": "./ngx_conf/dto",
"swagger.docs.path": "./docs",
"swagger.excludes": "./client",
"swagger.file.type": "json",
"swagger.main.lua.path": "./main.lua",
"swagger.name": "swagger",
"swagger.search_dirs": "./ngx_conf,./config/cn/online",
"validator_dir": "./ngx_conf/validator",
"yapi.config.file": "docs/swagger-yapi.json",
"yapi.config.mode": "mergin",
"yapi.config.server": "https://api.yapi.net",
"yapi.config.token": "xxxxxxxxx"
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/service/src/main/resources/db/luojia_channel.sql" dialect="MySQL" />
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" /> <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
</component> </component>
</project> </project>

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) registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login", .excludePathPatterns("/user/login",
"/user/register", "/user/register",
"/user/captcha", "/post/page",
"/user/verify-captcha", "/post/detail"
"/post/list",
"/post/detail",
"/comment/list",
"/comment/list/reply",
"/openapi/luojia-channel",
"/swagger-ui.html"
); );
} }

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

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

@ -13,9 +13,8 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class PageResponse<T> { public class PageResponse<T> {
// 普通分页参数 private Long current;
private Long current; // 当前页数,适用于普通分页
private Long total;
private Long size = 10L; private Long size = 10L;
private Long total;
private List<T> records = Collections.emptyList(); 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 long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
private static final String USER_ID_KEY = "userId"; private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username"; 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 TOKEN_PREFIX = "Bearer ";
public static final String ISS = "luojiachannel"; public static final String ISS = "luojiachannel";
public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576"; public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";
@ -46,7 +45,6 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>(); Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId()); customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername()); customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder() String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET) .signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date()) .setIssuedAt(new Date())
@ -66,7 +64,6 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>(); Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId()); customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername()); customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder() String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET) .signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date()) .setIssuedAt(new Date())

@ -1,9 +1,5 @@
package com.luojia_channel.common.utils; 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.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -13,12 +9,10 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations; import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -48,9 +42,7 @@ public class RedisUtil {
return value != null ? type.cast(value) : null; 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); T result = get(key, type);
if(result != null){ if(result != null){
return result; return result;
@ -73,42 +65,6 @@ public class RedisUtil {
return get(key, type); 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) { public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT); 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() { public static Long getUserId() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get(); 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() { 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

@ -0,0 +1,9 @@
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> <artifactId>commons-codec</artifactId>
<version>1.15</version> <version>1.15</version>
</dependency> </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> </dependencies>
<build> <build>

@ -1,8 +0,0 @@
protected-mode no
requirepass "Redis@9012"
bind 0.0.0.0
port 6379

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

@ -1,6 +1,5 @@
package com.luojia_channel.modules.file.dto; package com.luojia_channel.modules.file.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -9,23 +8,10 @@ import org.springframework.web.multipart.MultipartFile;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
@Schema(description = "上传文件DTO")
public class UploadFileDTO { public class UploadFileDTO {
@Schema(
description = "文件名",
required = true
)
private MultipartFile file; private MultipartFile file;
// 文件类型image or video // 文件类型image or video
@Schema(
description = "文件类型"
)
private String fileType; private String fileType;
// 文件md5 // 文件md5
@Schema(
description = "文件md5"
)
private String fileMd5; 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.Result;
import com.luojia_channel.common.domain.page.PageResponse; 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.common.utils.UserContext;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO; 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.CommentInfoDTO;
import com.luojia_channel.modules.post.entity.Comment; import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.service.CommentService; 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.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/comment") @RequestMapping("/comments")
@Tag(name = "评论模块", description = "评论相关接口")
public class CommentController { public class CommentController {
private final CommentService commentService; private final CommentService commentService;
@Autowired
public CommentController(CommentService commentService) {
this.commentService = commentService;
}
// 创建评论 // 创建评论
@PostMapping @PostMapping
@Operation( public Result<Void> saveComment(@RequestBody Comment comment) {
summary = "创建评论", commentService.saveComment(comment);
tags = {"评论模块"} return Result.success();
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Long> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
return Result.success(commentService.saveComment(commentSaveDTO));
} }
// 更新评论 // 更新评论
@PutMapping @PutMapping("/{id}")
@Operation( public Result<Void> updateComment(@PathVariable Long id, @RequestBody Comment comment) {
summary = "更新评论", Long currentUserId = UserContext.getUserId();
tags = {"评论模块"} commentService.updateComment(comment, currentUserId);
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "更新成功"),
@ApiResponse(responseCode = "500", description = "更新失败,请稍后重试")
})
public Result<Void> updateComment(@RequestBody CommentSaveDTO commentSaveDTO) {
commentService.updateComment(commentSaveDTO);
return Result.success(); return Result.success();
} }
// 删除评论 // 删除评论
@DeleteMapping @DeleteMapping("/{id}")
@Operation( public Result<Void> deleteComment(@PathVariable Long id) {
summary = "删除评论", Long currentUserId = UserContext.getUserId();
tags = {"评论模块"} commentService.deleteComment(id, currentUserId);
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "删除成功"),
@ApiResponse(responseCode = "500", description = "删除失败,评论不存在或被删除")
})
public Result<Void> deleteComment(@RequestParam("id") Long id) {
commentService.deleteComment(id);
return Result.success(); return Result.success();
} }
// 根据帖子ID分页获取评论 // 根据帖子ID分页获取评论
@GetMapping("/list") @GetMapping("/list")
@Operation( public Result<PageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
summary = "根据帖子ID分页获取根评论", PageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
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);
return Result.success(commentList); return Result.success(commentList);
} }
// 根据帖子ID获取嵌套评论
// 根据评论ID获取回复 @GetMapping("/nested/post/{postId}")
@GetMapping("/list/reply") public Result<List<CommentInfoDTO>> getNestedCommentsByPostId(@PathVariable Long postId) {
@Operation( List<CommentInfoDTO> nestedComments = commentService.getNestedCommentsByPostId(postId);
summary = "根据评论ID获取回复", return Result.success(nestedComments);
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();
} }
} }

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

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

@ -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; package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest; 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; import lombok.Data;
@Data @Data
public class PostPageQueryDTO extends ScrollPageRequest { public class PostPageQueryDTO extends PageRequest {
@Schema(
description = "想要查看的用户的id输入空时为自己的id"
)
private Long userId;
} }

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

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

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

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

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

@ -2,11 +2,7 @@ package com.luojia_channel.modules.post.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Comment; import com.luojia_channel.modules.post.entity.Comment;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper @Mapper
public interface CommentMapper extends BaseMapper<Comment> { 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.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.post.entity.Post; import com.luojia_channel.modules.post.entity.Post;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper @Mapper
public interface PostMapper extends BaseMapper<Post> { 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; package com.luojia_channel.modules.post.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luojia_channel.common.domain.page.PageResponse; 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.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.CommentInfoDTO;
import com.luojia_channel.modules.post.entity.Comment; import com.luojia_channel.modules.post.entity.Comment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -13,15 +11,13 @@ import java.util.List;
@Service @Service
public interface CommentService { 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); List<CommentInfoDTO> getNestedCommentsByPostId(Long postId);
void likeComment(Long id);
} }

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

@ -1,317 +1,91 @@
package com.luojia_channel.modules.post.service.impl; 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.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; 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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse; 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.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil; 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.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.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.Comment;
import com.luojia_channel.modules.post.entity.Post; import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper; 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.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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
@Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService { public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> implements CommentService {
private final ValidatePostUtil validatePostUtil; private CommentMapper commentMapper;
private final CommentMapper commentMapper;
private final UserMapper userMapper;
private final NotificationProducer notificationProducer;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
@Override @Override
@Transactional(rollbackFor = Exception.class) public void saveComment(Comment comment) {
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);
comment.setCreateTime(LocalDateTime.now()); comment.setCreateTime(LocalDateTime.now());
comment.setUpdateTime(LocalDateTime.now()); comment.setUpdateTime(LocalDateTime.now());
if (!save(comment)) { commentMapper.insert(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();
} }
@Override @Override
public void updateComment(CommentSaveDTO commentSaveDTO) { public void updateComment(Comment comment, Long userId) {
validatePostUtil.validateComment(commentSaveDTO); validatePostOwnership(comment.getId(), userId);
validatePostUtil.validateCommentOwnership(commentSaveDTO.getId());
Comment comment = BeanUtil.copyProperties(commentSaveDTO, Comment.class);
comment.setUserId(UserContext.getUserId());
comment.setUpdateTime(LocalDateTime.now()); comment.setUpdateTime(LocalDateTime.now());
if(!updateById(comment)){ if(!updateById(comment)){
throw new PostException("更新评论失败"); throw new PostException("更新帖子失败");
} }
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) public void deleteComment(Long id, Long userId) {
public void deleteComment(Long id) { validatePostOwnership(id, userId);
validatePostUtil.validateCommentOwnership(id); commentMapper.deleteById(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 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论
} }
// 分页查询一系列根评论
@Override @Override
public ScrollPageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) { public PageResponse<CommentInfoDTO> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
if(commentPageQueryDTO.getPostId() == null || commentPageQueryDTO.getPostId() < 0){
throw new PostException("帖子id不合法");
}
/*
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class) LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getPostId, commentPageQueryDTO.getPostId()) .eq(Comment::getPostId, commentPageQueryDTO.getPostId())
.eq(Comment::getParentCommentId, 0L)
.orderByDesc(Comment::getCreateTime); .orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper); IPage<Comment> commentPage = commentMapper.selectPage(PageUtil.convert(commentPageQueryDTO), queryWrapper);
*/ return PageUtil.convert(commentPage, CommentInfoDTO.class);
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 @Override
public ScrollPageResponse<CommentInfoDTO> getReplyById(CommentPageQueryDTO commentPageQueryDTO) { public List<CommentInfoDTO> getNestedCommentsByPostId(Long postId) {
if(commentPageQueryDTO.getParentCommentId() == null || commentPageQueryDTO.getParentCommentId() < 0){ LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
throw new PostException("评论id不合法"); queryWrapper.eq(Comment::getPostId, postId)
} .orderByAsc(Comment::getCreateTime);
/*
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;
});
}
@Override List<Comment> comments = commentMapper.selectList(queryWrapper);
public void likeComment(Long id) { return buildNestedComments(comments);
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) { private void validatePostOwnership(Long commentId, Long userId) {
Long userId = UserContext.getUserId(); Comment comment = commentMapper.selectById(commentId);
String likeCommentKey = "comment:like:" + commentId; if (comment == null) {
RedisTemplate<String, Object> redisTemplate = redisUtil.getInstance(); throw new PostException("评论不存在");
return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(likeCommentKey, userId));
} }
if (!userId.equals(comment.getUserId())) {
private boolean updateCommentLikeCount(Long commentId, int increment) { throw new PostException("你无权操作他人的评论");
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.setSql("like_count = like_count + " + increment)
.eq(Comment::getId, commentId);
return commentMapper.update(null, updateWrapper) > 0;
} }
/*
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 List<CommentInfoDTO> convertToDTO(List<Comment> commentList) { private List<CommentInfoDTO> convertToDTO(List<Comment> comments) {
List<CommentInfoDTO> dtos = new ArrayList<>(); List<CommentInfoDTO> dtos = new ArrayList<>();
for (Comment comment : commentList) { for (Comment comment : comments) {
CommentInfoDTO dto = new CommentInfoDTO(); CommentInfoDTO dto = new CommentInfoDTO();
BeanUtils.copyProperties(comment, dto); BeanUtils.copyProperties(comment, dto);
dtos.add(dto); dtos.add(dto);
@ -319,14 +93,16 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return dtos; return dtos;
} }
// 根据评论创建嵌套评论,目前没用到 private List<CommentInfoDTO> buildNestedComments(List<Comment> comments) {
private List<CommentInfoDTO> buildNestedCommentList(List<Comment> comments) {
Map<Long, CommentInfoDTO> map = new HashMap<>(); Map<Long, CommentInfoDTO> map = new HashMap<>();
List<CommentInfoDTO> rootComments = new ArrayList<>(); List<CommentInfoDTO> rootComments = new ArrayList<>();
for (Comment comment : comments) { for (Comment comment : comments) {
CommentInfoDTO dto = new CommentInfoDTO(); CommentInfoDTO dto = new CommentInfoDTO();
BeanUtils.copyProperties(comment, dto); BeanUtils.copyProperties(comment, dto);
map.put(comment.getId(), dto); map.put(comment.getId(), dto);
if (comment.getParentCommentId() == null) { if (comment.getParentCommentId() == null) {
rootComments.add(dto); rootComments.add(dto);
} else { } else {
@ -339,6 +115,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
} }
} }
} }
return rootComments; return rootComments;
} }
} }

@ -6,39 +6,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageResponse; 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.PostException;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.PageUtil; import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil; import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext; import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.service.impl.FileServiceImpl; 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.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO; 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.PostBasicInfoDTO;
import com.luojia_channel.modules.post.dto.resp.PostInfoDTO;
import com.luojia_channel.modules.post.entity.Post; import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper; import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.post.service.PostService; import com.luojia_channel.modules.post.service.PostService;
import com.luojia_channel.modules.post.utils.ValidatePostUtil; 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.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -48,45 +32,16 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
private final FileServiceImpl fileService; private final FileServiceImpl fileService;
private final ValidatePostUtil validatePostUtil; private final ValidatePostUtil validatePostUtil;
private final RedisUtil redisUtil; 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 @Override
@Transactional(rollbackFor = Exception.class) public void createPost(PostSaveDTO postSaveDTO) {
public Long savePost(PostSaveDTO postSaveDTO) {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
validatePostUtil.validatePost(postSaveDTO); validatePostUtil.validatePost(postSaveDTO);
Long userId = UserContext.getUserId();
Post post = BeanUtil.copyProperties(postSaveDTO, Post.class); Post post = BeanUtil.copyProperties(postSaveDTO, Post.class);
post.setUserId(UserContext.getUserId()); post.setUserId(userId);
post.setCreateTime(LocalDateTime.now());
post.setUpdateTime(LocalDateTime.now());
if(!save(post)){ if(!save(post)){
throw new PostException("创建帖子失败"); 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 @Override
@ -97,227 +52,51 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
@Override @Override
public void updatePost(PostSaveDTO postSaveDTO) { public void updatePost(PostSaveDTO postSaveDTO) {
validatePostUtil.validatePost(postSaveDTO); validatePostUtil.validatePost(postSaveDTO);
validatePostUtil.validatePostOwnership(postSaveDTO.getId());
Post post = BeanUtil.copyProperties(postSaveDTO, Post.class); Post post = BeanUtil.copyProperties(postSaveDTO, Post.class);
post.setUserId(UserContext.getUserId());
post.setUpdateTime(LocalDateTime.now()); post.setUpdateTime(LocalDateTime.now());
if(!updateById(post)){ if(!updateById(post)){
throw new PostException("更新帖子失败"); throw new PostException("更新帖子失败");
} }
redisUtil.delete("post:detail:" + postSaveDTO.getId());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
} }
@Override @Override
public void deletePost(Long id) { public void deletePost(Long id) {
validatePostUtil.validatePostOwnership(id); validatePostUtil.validatePostOwnership(id);
Long userId = UserContext.getUserId();
int delete = postMapper.deleteById(id); int delete = postMapper.deleteById(id);
if(delete <= 0){ if(delete <= 0){
throw new PostException("删除帖子失败"); 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 @Override
public PostInfoDTO getPostDetail(Long id) { public Post getPostDetail(Long id) {
// TODO 定时任务更新浏览数或消息队列解耦,懒得写了 return redisUtil.safeGet("post:detail" + id.toString(), Post.class,
Post oldPost = getById(id);
oldPost.setViewCount(oldPost.getViewCount() + 1);
updateById(oldPost);
return redisUtil.safeGet("post:detail:" + id, PostInfoDTO.class,
() -> { () -> {
Post post = getById(id); Post post = getById(id);
if(post == null){ if(post == null){
throw new PostException("帖子不存在或被删除"); throw new PostException("帖子不存在或被删除");
} }
PostInfoDTO postInfoDTO = BeanUtil.copyProperties(post, PostInfoDTO.class); return post;
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class) }, 10, TimeUnit.MINUTES);
.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);
} }
@Override @Override
public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) { public PageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
// TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则 // TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则
/*
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class) LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.orderByDesc(Post::getCreateTime); .orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper); IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
List<Long> userIds = new ArrayList<>(); return PageUtil.convert(postPage, PostBasicInfoDTO.class);
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 @Override
public ScrollPageResponse<PostBasicInfoDTO> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) { public PageResponse<PostBasicInfoDTO> pagePostOfMe(PostPageQueryDTO postPageQueryDTO) {
Long myUserId = UserContext.getUserId(); Long userId = 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) LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.eq(Post::getUserId, userId); .eq(Post::getUserId, userId)
// 当查询别人时,不显示匿名帖子 .orderByDesc(Post::getCreateTime);
if(!myUserId.equals(userId)){
queryWrapper.eq(Post::getStatus, 0);
}
queryWrapper.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper); IPage<Post> postPage = postMapper.selectPage(PageUtil.convert(postPageQueryDTO), queryWrapper);
return PageUtil.convert(postPage, post -> { return PageUtil.convert(postPage, PostBasicInfoDTO.class);
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;
});
}
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) {
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);
}
} }

@ -2,13 +2,9 @@ package com.luojia_channel.modules.post.utils;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.luojia_channel.common.exception.PostException; 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.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.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.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper; import com.luojia_channel.modules.post.mapper.PostMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -20,8 +16,6 @@ public class ValidatePostUtil {
private final PostMapper postMapper; private final PostMapper postMapper;
private final CommentMapper commentMapper;
public void validatePost(PostSaveDTO postSaveDTO) { public void validatePost(PostSaveDTO postSaveDTO) {
// 非空字段检验 // 非空字段检验
if (StrUtil.isBlank(postSaveDTO.getTitle())) { if (StrUtil.isBlank(postSaveDTO.getTitle())) {
@ -40,12 +34,6 @@ public class ValidatePostUtil {
public void validatePostOwnership(Long id){ public void validatePostOwnership(Long id){
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
if(id == null){
throw new PostException("传入id不能为空");
}
Post post = postMapper.selectById(id); Post post = postMapper.selectById(id);
if(post == null){ if(post == null){
throw new PostException("帖子不存在"); throw new PostException("帖子不存在");
@ -55,32 +43,7 @@ public class ValidatePostUtil {
} }
} }
public void validateComment(CommentSaveDTO commentSaveDTO) { public void AIValidate(PostSaveDTO postSaveDTO){
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 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.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.service.UserInfoService; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -15,46 +11,21 @@ import org.springframework.web.multipart.MultipartFile;
@RestController @RestController
@RequestMapping("/user/info") @RequestMapping("/user/info")
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "用户信息管理", description = "用户修改个人信息相关接口")
public class UserInfoController { public class UserInfoController {
private final UserInfoService userInfoService; private final UserInfoService userInfoService;
@PostMapping("/update") @PostMapping("/update")
@Operation(
summary="修改用户信息",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "修改成功"),
@ApiResponse(responseCode = "500", description = "修改失败,请稍后重试")
})
public Result<Void> updateInfo(@RequestBody UserChangeInfoDTO userChangeInfoDTO){ public Result<Void> updateInfo(@RequestBody UserChangeInfoDTO userChangeInfoDTO){
userInfoService.updateInfo(userChangeInfoDTO); userInfoService.updateInfo(userChangeInfoDTO);
return Result.success(); return Result.success();
} }
@PostMapping("/password") @PostMapping("/password")
@Operation(
summary="用户修改密码",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "修改成功"),
@ApiResponse(responseCode = "500", description = "修改失败,请稍后重试")
})
public Result<Void> updatePassword(@RequestParam String password){ public Result<Void> updatePassword(@RequestParam String password){
userInfoService.updatePassword(password); userInfoService.updatePassword(password);
return Result.success(); return Result.success();
} }
@PostMapping("/avatar") @PostMapping("/avatar")
@Operation(
summary="用户上传头像",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "上传成功"),
@ApiResponse(responseCode = "500", description = "上传失败,请稍后重试")
})
public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) { public Result<String> uploadAvatar(@RequestParam("file") MultipartFile file) {
return Result.success(userInfoService.uploadAvatar(file)); return Result.success(userInfoService.uploadAvatar(file));
} }

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

Loading…
Cancel
Save