Compare commits
69 Commits
@ -0,0 +1,45 @@
|
||||
### 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
|
@ -1,32 +0,0 @@
|
||||
<?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&characterEncoding=UTF-8&autoReconnect=true&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>
|
@ -1,9 +0,0 @@
|
||||
<?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,17 +0,0 @@
|
||||
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
<?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>
|
@ -1,124 +0,0 @@
|
||||
<?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,3 +1,4 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic"
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,22 @@
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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; // 每页数量
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
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; // 数据列表
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +0,0 @@
|
||||
artifactId=common
|
||||
groupId=com.luojia
|
||||
version=1.0.0
|
@ -1,11 +0,0 @@
|
||||
com\luojia\luojia_channel\advice\GlobalExceptionHandler.class
|
||||
com\luojia\luojia_channel\domain\UserDTO.class
|
||||
com\luojia\luojia_channel\utils\UserContext.class
|
||||
com\luojia\luojia_channel\utils\JWTUtil.class
|
||||
com\luojia\luojia_channel\config\RedisConfig.class
|
||||
com\luojia\luojia_channel\utils\RedisUtil$ZSetItem.class
|
||||
com\luojia\luojia_channel\domain\UserDTO$UserDTOBuilder.class
|
||||
com\luojia\luojia_channel\exception\BaseException.class
|
||||
com\luojia\luojia_channel\utils\RedisUtil.class
|
||||
com\luojia\luojia_channel\domain\Result.class
|
||||
com\luojia\luojia_channel\exception\UserException.class
|
@ -1,9 +0,0 @@
|
||||
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
|
@ -0,0 +1,8 @@
|
||||
|
||||
protected-mode no
|
||||
|
||||
requirepass "Redis@9012"
|
||||
|
||||
|
||||
bind 0.0.0.0
|
||||
port 6379
|
@ -0,0 +1 @@
|
||||
LuojiaChannelApplication.java
|
@ -0,0 +1,6 @@
|
||||
<?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,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,81 @@
|
||||
package com.luojia_channel.modules.admin.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理员评论DTO
|
||||
*/
|
||||
@Data
|
||||
public class AdminCommentDTO {
|
||||
/**
|
||||
* 评论ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String userAvatar;
|
||||
|
||||
/**
|
||||
* 帖子ID
|
||||
*/
|
||||
private Long postId;
|
||||
|
||||
/**
|
||||
* 帖子标题
|
||||
*/
|
||||
private String postTitle;
|
||||
|
||||
/**
|
||||
* 父评论ID
|
||||
*/
|
||||
private Long parentCommentId;
|
||||
|
||||
/**
|
||||
* 顶级评论ID
|
||||
*/
|
||||
private Long topId;
|
||||
|
||||
/**
|
||||
* 回复用户名
|
||||
*/
|
||||
private String replyUsername;
|
||||
|
||||
/**
|
||||
* 点赞数
|
||||
*/
|
||||
private Integer likeCount;
|
||||
|
||||
/**
|
||||
* 回复数
|
||||
*/
|
||||
private Integer replyCount;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.luojia_channel.modules.admin.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理员用户DTO - 仅包含管理员可见的非隐私信息
|
||||
*/
|
||||
@Data
|
||||
public class AdminUserDTO {
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 用户状态(1正常 2冻结)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户角色(1普通用户 2管理员 3超级管理员)
|
||||
*/
|
||||
private Integer role;
|
||||
|
||||
/**
|
||||
* 用户积分
|
||||
*/
|
||||
private Integer integral;
|
||||
|
||||
/**
|
||||
* 发帖数量
|
||||
*/
|
||||
private Integer postCount;
|
||||
|
||||
/**
|
||||
* 评论数量
|
||||
*/
|
||||
private Integer commentCount;
|
||||
|
||||
/**
|
||||
* 粉丝数量
|
||||
*/
|
||||
private Integer followerCount;
|
||||
|
||||
/**
|
||||
* 关注数量
|
||||
*/
|
||||
private Integer followingCount;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package com.luojia_channel.modules.admin.service;
|
||||
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
import com.luojia_channel.modules.admin.dto.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理员服务接口
|
||||
*/
|
||||
public interface AdminService {
|
||||
/**
|
||||
* 获取系统总览数据
|
||||
* @return 总览数据
|
||||
*/
|
||||
AdminOverviewDTO getOverview();
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @param keyword 搜索关键词
|
||||
* @param role 角色
|
||||
* @param status 状态
|
||||
* @return 用户列表分页结果
|
||||
*/
|
||||
PageResponse<AdminUserDTO> getUserList(Integer page, Integer size, String keyword, Integer role, Integer status);
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
* @param id 用户ID
|
||||
* @param status 状态
|
||||
*/
|
||||
void changeUserStatus(Long id, Integer status);
|
||||
|
||||
/**
|
||||
* 修改用户角色
|
||||
* @param id 用户ID
|
||||
* @param role 角色
|
||||
*/
|
||||
void changeUserRole(Long id, Integer role);
|
||||
|
||||
/**
|
||||
* 获取帖子列表
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @param keyword 搜索关键词
|
||||
* @param categoryId 分类ID
|
||||
* @param status 状态
|
||||
* @return 帖子列表分页结果
|
||||
*/
|
||||
PageResponse<AdminPostDTO> getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status);
|
||||
|
||||
/**
|
||||
* 删除帖子
|
||||
* @param id 帖子ID
|
||||
*/
|
||||
void deletePost(Long id);
|
||||
|
||||
/**
|
||||
* 修改帖子状态
|
||||
* @param id 帖子ID
|
||||
* @param action 操作类型
|
||||
*/
|
||||
void changePostStatus(Long id, Integer action);
|
||||
|
||||
/**
|
||||
* 获取评论列表
|
||||
* @param page 页码
|
||||
* @param size 每页数量
|
||||
* @param keyword 搜索关键词
|
||||
* @param postId 帖子ID
|
||||
* @return 评论列表分页结果
|
||||
*/
|
||||
PageResponse<AdminCommentDTO> getCommentList(Integer page, Integer size, String keyword, Long postId);
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
* @param id 评论ID
|
||||
*/
|
||||
void deleteComment(Long id);
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
* @param type 统计类型
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 统计数据
|
||||
*/
|
||||
Map<String, Object> getUserStatistics(String type, String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 获取帖子统计数据
|
||||
* @param type 统计类型
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @return 统计数据
|
||||
*/
|
||||
Map<String, Object> getPostStatistics(String type, String startDate, String endDate);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package com.luojia_channel.modules.feedback.config;
|
||||
|
||||
import org.springframework.amqp.core.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FeedbackRabbitMQConfig {
|
||||
|
||||
// 反馈工单交换机
|
||||
public static final String FEEDBACK_EXCHANGE = "feedback.exchange";
|
||||
|
||||
// 延迟队列 - 消息先进入此队列,过期后转发到死信队列
|
||||
public static final String FEEDBACK_DELAY_QUEUE = "feedback.delay.queue";
|
||||
|
||||
// 死信交换机 - 延迟队列的死信会转发到此交换机
|
||||
public static final String FEEDBACK_DLX_EXCHANGE = "feedback.dlx.exchange";
|
||||
|
||||
// 死信队列 - 最终消费者监听此队列
|
||||
public static final String FEEDBACK_DLX_QUEUE = "feedback.dlx.queue";
|
||||
|
||||
// 路由键
|
||||
public static final String FEEDBACK_ROUTING_KEY = "feedback.routing.key";
|
||||
|
||||
// 声明反馈交换机
|
||||
@Bean
|
||||
DirectExchange feedbackExchange() {
|
||||
return new DirectExchange(FEEDBACK_EXCHANGE, true, false);
|
||||
}
|
||||
|
||||
// 声明死信交换机
|
||||
@Bean
|
||||
DirectExchange feedbackDlxExchange() {
|
||||
return new DirectExchange(FEEDBACK_DLX_EXCHANGE, true, false);
|
||||
}
|
||||
|
||||
// 声明延迟队列,并绑定到死信交换机
|
||||
@Bean
|
||||
Queue feedbackDelayQueue() {
|
||||
return QueueBuilder.durable(FEEDBACK_DELAY_QUEUE)
|
||||
.withArgument("x-dead-letter-exchange", FEEDBACK_DLX_EXCHANGE)
|
||||
.withArgument("x-dead-letter-routing-key", FEEDBACK_ROUTING_KEY)
|
||||
.build();
|
||||
}
|
||||
|
||||
// 声明死信队列
|
||||
@Bean
|
||||
Queue feedbackDlxQueue() {
|
||||
return new Queue(FEEDBACK_DLX_QUEUE, true);
|
||||
}
|
||||
|
||||
// 绑定延迟队列到反馈交换机
|
||||
@Bean
|
||||
Binding feedbackDelayBinding() {
|
||||
return BindingBuilder.bind(feedbackDelayQueue())
|
||||
.to(feedbackExchange())
|
||||
.with(FEEDBACK_ROUTING_KEY);
|
||||
}
|
||||
|
||||
// 绑定死信队列到死信交换机
|
||||
@Bean
|
||||
Binding feedbackDlxBinding() {
|
||||
return BindingBuilder.bind(feedbackDlxQueue())
|
||||
.to(feedbackDlxExchange())
|
||||
.with(FEEDBACK_ROUTING_KEY);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package com.luojia_channel.modules.feedback.controller;
|
||||
|
||||
import com.luojia_channel.common.domain.Result;
|
||||
import com.luojia_channel.common.domain.page.PageRequest;
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
import com.luojia_channel.common.exception.UserException;
|
||||
import com.luojia_channel.common.utils.UserContext;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
|
||||
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
|
||||
import com.luojia_channel.modules.feedback.service.FeedbackTicketService;
|
||||
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("/feedback")
|
||||
@Tag(name = "反馈工单模块", description = "反馈工单相关接口")
|
||||
@RequiredArgsConstructor
|
||||
public class FeedbackTicketController {
|
||||
|
||||
private final FeedbackTicketService feedbackTicketService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建反馈工单", description = "用户创建反馈工单")
|
||||
public Result<Long> createFeedbackTicket(@RequestBody FeedbackTicket feedbackTicket) {
|
||||
Long id = feedbackTicketService.createFeedbackTicket(feedbackTicket);
|
||||
return Result.success(id);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "编辑反馈工单", description = "编辑已有的反馈工单")
|
||||
public Result<Void> updateFeedbackTicket(@RequestBody FeedbackTicket feedbackTicket) {
|
||||
feedbackTicketService.updateFeedbackTicket(feedbackTicket);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@GetMapping("/of/me")
|
||||
@Operation(summary = "个人反馈工单查询", description = "查询当前用户的反馈工单")
|
||||
public Result<List<FeedbackTicket>> getPersonalFeedbackTickets() {
|
||||
Long userId = UserContext.getUserId();
|
||||
if(userId == null){
|
||||
throw new UserException("用户未登录");
|
||||
}
|
||||
List<FeedbackTicket> tickets = feedbackTicketService.getPersonalFeedbackTickets(userId);
|
||||
return Result.success(tickets);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "分页查询反馈工单", description = "分页查询反馈工单列表")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "获取成功"),
|
||||
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
|
||||
})
|
||||
public Result<PageResponse<FeedbackTicketBasicInfoDTO>> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO) {
|
||||
PageResponse<FeedbackTicketBasicInfoDTO> pageResponse = feedbackTicketService.pageFeedbackTicket(feedbackTicketPageQueryDTO);
|
||||
return Result.success(pageResponse);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.luojia_channel.modules.feedback.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("feedback_ticket")
|
||||
public class FeedbackTicket {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String content;
|
||||
private String feedbackType;
|
||||
private Integer status;
|
||||
private String contactInfo;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime handleTime;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.luojia_channel.modules.feedback.mapper;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface FeedbackTicketMapper extends BaseMapper<FeedbackTicket> {
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.luojia_channel.modules.feedback.mq.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DelayHandleTicketMessage {
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String content;
|
||||
private String feedbackType;
|
||||
private Integer status;
|
||||
private String contactInfo;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.luojia_channel.modules.feedback.mq.producer;
|
||||
|
||||
|
||||
import com.luojia_channel.modules.feedback.config.FeedbackRabbitMQConfig;
|
||||
import com.luojia_channel.modules.feedback.mq.domain.DelayHandleTicketMessage;
|
||||
import com.luojia_channel.modules.message.mq.AbstractSendProduceTemplate;
|
||||
import com.luojia_channel.modules.message.mq.BaseSendExtendDTO;
|
||||
import com.luojia_channel.modules.message.mq.MessageWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FeedbackMessageProducer extends AbstractSendProduceTemplate<DelayHandleTicketMessage>{
|
||||
|
||||
public FeedbackMessageProducer(@Autowired RabbitTemplate rabbitTemplate){
|
||||
super(rabbitTemplate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BaseSendExtendDTO buildBaseSendParam(DelayHandleTicketMessage messageSendEvent) {
|
||||
return BaseSendExtendDTO.builder()
|
||||
.eventName("DelayHandleTicketMessageEvent")
|
||||
.exchange(FeedbackRabbitMQConfig.FEEDBACK_EXCHANGE)
|
||||
.routingKey(FeedbackRabbitMQConfig.FEEDBACK_ROUTING_KEY)
|
||||
.keys(messageSendEvent.getId().toString())
|
||||
.delay(24*60*60*1000L) //延迟消息是1天
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.luojia_channel.modules.feedback.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
|
||||
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
|
||||
import com.luojia_channel.common.domain.page.PageRequest;
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface FeedbackTicketService extends IService<FeedbackTicket> {
|
||||
/**
|
||||
* 创建反馈工单
|
||||
* @param feedbackTicket 反馈工单实体
|
||||
* @return 工单 ID
|
||||
*/
|
||||
Long createFeedbackTicket(FeedbackTicket feedbackTicket);
|
||||
|
||||
/**
|
||||
* 编辑反馈工单
|
||||
* @param feedbackTicket 反馈工单实体
|
||||
*/
|
||||
void updateFeedbackTicket(FeedbackTicket feedbackTicket);
|
||||
|
||||
/**
|
||||
* 个人反馈工单查询
|
||||
* @param userId 用户 ID
|
||||
* @return 反馈工单列表
|
||||
*/
|
||||
List<FeedbackTicket> getPersonalFeedbackTickets(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询反馈工单
|
||||
* @param feedbackTicketPageQueryDTO 分页查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResponse<FeedbackTicketBasicInfoDTO> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO);
|
||||
|
||||
/**
|
||||
* 处理延时反馈工单
|
||||
* @param ticketId 工单ID
|
||||
*/
|
||||
void processDelayedFeedback(Long ticketId);
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package com.luojia_channel.modules.feedback.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.luojia_channel.common.domain.page.PageRequest;
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
|
||||
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
|
||||
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
|
||||
import com.luojia_channel.modules.feedback.mapper.FeedbackTicketMapper;
|
||||
import com.luojia_channel.modules.feedback.mq.domain.DelayHandleTicketMessage;
|
||||
import com.luojia_channel.modules.feedback.mq.producer.FeedbackMessageProducer;
|
||||
import com.luojia_channel.modules.feedback.service.FeedbackTicketService;
|
||||
import com.luojia_channel.modules.user.entity.User;
|
||||
import com.luojia_channel.modules.user.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FeedbackTicketServiceImpl extends ServiceImpl<FeedbackTicketMapper, FeedbackTicket> implements FeedbackTicketService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final FeedbackMessageProducer feedbackMessageProducer;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createFeedbackTicket(FeedbackTicket feedbackTicket) {
|
||||
feedbackTicket.setCreateTime(LocalDateTime.now());
|
||||
feedbackTicket.setStatus(1);
|
||||
if(!save(feedbackTicket))
|
||||
throw new RuntimeException("保存工单失败");
|
||||
// 发送延时消息
|
||||
feedbackMessageProducer.sendMessage(BeanUtil.copyProperties(feedbackTicket, DelayHandleTicketMessage.class));
|
||||
return feedbackTicket.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFeedbackTicket(FeedbackTicket feedbackTicket) {
|
||||
updateById(feedbackTicket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FeedbackTicket> getPersonalFeedbackTickets(Long userId) {
|
||||
LambdaQueryWrapper<FeedbackTicket> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(FeedbackTicket::getUserId, userId);
|
||||
return list(queryWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResponse<FeedbackTicketBasicInfoDTO> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO) {
|
||||
LambdaQueryWrapper<FeedbackTicket> queryWrapper = new LambdaQueryWrapper<>();
|
||||
if (feedbackTicketPageQueryDTO.getFeedbackType() != null) {
|
||||
queryWrapper.eq(FeedbackTicket::getFeedbackType, feedbackTicketPageQueryDTO.getFeedbackType());
|
||||
}
|
||||
if (feedbackTicketPageQueryDTO.getStatus() != null) {
|
||||
queryWrapper.eq(FeedbackTicket::getStatus, feedbackTicketPageQueryDTO.getStatus());
|
||||
}
|
||||
queryWrapper.orderByDesc(FeedbackTicket::getCreateTime);
|
||||
|
||||
Page<FeedbackTicket> page = new Page<>(feedbackTicketPageQueryDTO.getCurrent(), feedbackTicketPageQueryDTO.getSize());
|
||||
IPage<FeedbackTicket> feedbackTicketPage = baseMapper.selectPage(page, queryWrapper);
|
||||
|
||||
List<Long> userIds = new ArrayList<>();
|
||||
feedbackTicketPage.getRecords().forEach(ticket -> userIds.add(ticket.getUserId()));
|
||||
List<User> users = userMapper.selectBatchIds(userIds);
|
||||
Map<Long, User> userMap = users.stream()
|
||||
.collect(Collectors.toMap(User::getId, user -> user));
|
||||
|
||||
List<FeedbackTicketBasicInfoDTO> feedbackTicketBasicInfoDTOS = new ArrayList<>();
|
||||
feedbackTicketPage.getRecords().forEach(ticket -> {
|
||||
FeedbackTicketBasicInfoDTO dto = new FeedbackTicketBasicInfoDTO();
|
||||
BeanUtil.copyProperties(ticket, dto);
|
||||
User user = userMap.getOrDefault(ticket.getUserId(), new User());
|
||||
dto.setUserName(user.getUsername());
|
||||
dto.setUserAvatar(user.getAvatar());
|
||||
feedbackTicketBasicInfoDTOS.add(dto);
|
||||
});
|
||||
|
||||
return PageResponse.<FeedbackTicketBasicInfoDTO>builder()
|
||||
.current(feedbackTicketPage.getCurrent())
|
||||
.size(feedbackTicketPage.getSize())
|
||||
.total(feedbackTicketPage.getTotal())
|
||||
.records(feedbackTicketBasicInfoDTOS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processDelayedFeedback(Long ticketId) {
|
||||
// 查询工单
|
||||
FeedbackTicket ticket = getById(ticketId);
|
||||
if (ticket == null || ticket.getStatus().equals(3)) {
|
||||
return;
|
||||
}
|
||||
// 处理延时任务,更新工单状态为超时
|
||||
ticket.setStatus(4); //
|
||||
updateById(ticket);
|
||||
// TODO 向管理员发送消息
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
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(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(ChatPageQueryDTO chatPageQueryDTO) {
|
||||
return Result.success(chatService.getChatHistory(chatPageQueryDTO));
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
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) + "");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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-系统
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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];
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue