Compare commits

..

69 Commits
lzt ... main

Author SHA1 Message Date
哆哆咯哆哆咯 8973bef0a5 提交项目汇报ppt
2 months ago
lee-zt 965f578567 测试分析报告
2 months ago
哆哆咯哆哆咯 812aa7a798 checklogin实现
2 months ago
lee-zt c1c6f80ebe 优化帖子详情页面
2 months ago
lee-zt 73bcf9e5ee 修复详情页不换行的问题
2 months ago
lee-zt d5e8ae9f7c Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
2 months ago
lee-zt 6096cdac1c 优化主界面和帖子详情页
2 months ago
Hacker-00001 81a0768f0a 发布帖子时如果没上传封面增加提示
2 months ago
Hacker-00001 5eee2f8e14 优化修改个人信息界面和发布帖子界面
2 months ago
lee-zt 1dbe50c50f Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
2 months ago
lee-zt 180a84379e 公告栏显示最新校园活动信息
2 months ago
Hacker-00001 41de0d735f 正常显示帖子封面
2 months ago
lee-zt e0e794ccc9 分类查询显示
2 months ago
forely 5a0117edc0 Merge remote-tracking branch 'origin/main'
2 months ago
forely cb5b133cc6 分类
2 months ago
Hacker-00001 6641070b5c 实现匿名发帖,只修改了postpublish.vue,postdetail.vue两个页面
2 months ago
lee-zt 5280af67db 修复模态框位置问题
2 months ago
lee-zt 944b8f37f8 调整背景和樱花
2 months ago
lee-zt 3eaa20c803 点赞帖子和评论
2 months ago
lee-zt f0ac926e7e 修改删除评论函数(评论数问题)
2 months ago
forely a467856a7c Merge remote-tracking branch 'origin/main'
2 months ago
forely 27f29826bc 删除评论对应的数量问题
2 months ago
lee-zt c467599302 删除评论
2 months ago
Hacker-00001 54afda80e1 实现删除帖子
2 months ago
lee-zt 17531545b2 修改查看和发评论bug
2 months ago
lee-zt c445c1ca5c 实现发送评论
2 months ago
forely b626045de2 优化评论返回模型,注意数据库表变更,需要重新运行sql脚本。完成评论和帖子的自定义热度排序
2 months ago
Hacker-00001 d97a5a0b31 实现首页帖子展示,能加载评论
2 months ago
forely 4aaaa1fc6a 用户端的反馈工单
2 months ago
lee-zt 1934fc460b 修改回复评论
2 months ago
Hacker-00001 60ed9daa67 实现用户主页帖子的展示和帖子页的展示,但首页的帖子未能展示
2 months ago
forely b006059b41 优化评论返回模型,注意数据库表变更,需要重新运行sql脚本。完成评论和帖子的自定义热度排序
2 months ago
Hacker-00001 e683171570 实现发帖但帖子仍无法展示
2 months ago
哆哆咯哆哆咯 538276c248 用户信息显示完成,管理端入口实现,但是登陆后个人中心页面和发布帖子页面刷新会报错,需要解决
2 months ago
哆哆咯哆哆咯 0efc812afb 管理端前后端实现,但还没测试;提供/user/info/getuserinfo接口获取用户信息(靠userid),数据dto对应在user/dto/userinfoDTO里面,有需要自己更改
2 months ago
Hacker-00001 5fc904198b 实现修改个人信息功能
2 months ago
lee-zt 0fc3a5e109 发送评论
2 months ago
lee-zt 2220f3f3e9 1
2 months ago
lee-zt c4eff39ab9 Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
2 months ago
lee-zt 83ebb5436e 继续完善帖子查询
2 months ago
2023302111026黄罗霖 4fb7fc0472 修改查看个人帖子
2 months ago
2023302111026黄罗霖 5befe90555 修改查看个人帖子
2 months ago
lee-zt 61cc9965ff 樱花飘落效果,还需更优素材
2 months ago
lee-zt d856624130 帖子列表和查看帖子详情
2 months ago
lee-zt a637c4eaec 再次修改.gitignore
2 months ago
335942189@qq.com 010088b841 修改个人信息界面和用户见面展示个人信息
2 months ago
335942189@qq.com 8a59195eb6 修改个人信息和主页显示用户信息
2 months ago
2023302111026黄罗霖 791862b593 个人页面查询自己帖子
2 months ago
335942189@qq.com fa3b1557f5 Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
2 months ago
335942189@qq.com 6bafbbeba6 修改个人信息界面和用户信息界面
2 months ago
forely 306b7f5b6c 小改部分后端代码
2 months ago
lee-zt 1ae1607686 重构已有前端代码
2 months ago
哆哆咯哆哆咯 9f79bd04e1 更新API文档
2 months ago
forely 59501feb4a 改了一些bug
2 months ago
forely 11d6d818ae 重构部分接口,改普通分页查询为游标滚动分页查询,优化缓存结构,完善消息模块
2 months ago
forely 70150357e6 重构部分接口,改普通分页查询为游标滚动分页查询,优化缓存结构,完善消息模块
2 months ago
哆哆咯哆哆咯 46ce1ba39f 注册图形验证码实现,用户名注册可成功
2 months ago
2023302111026黄罗霖 46dbe3cd1d 反馈站页面实现,改为登录才能进入
2 months ago
2023302111026黄罗霖 002b258764 个人页面帖子跳转
2 months ago
lee-zt 95a38a1c0e 个人主页跳转,通知移入登录后显示
2 months ago
lee-zt d3158ec4f9 登录,退出登录
3 months ago
lee-zt fa9627a9de Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
3 months ago
lee-zt 5764b011e6 安装pinia,实现登录逻辑
3 months ago
哆哆咯哆哆咯 dd66f95337 API文档
3 months ago
forely 7f2c6fd73e 点赞,评论,关注,私信,在线消息通知,匿名发帖(目前是全局公用的匿名名称与头像url)
3 months ago
forely e562fe9004 重构部分表与接口,新增帖子,评论的相关接口
3 months ago
forely a0e9b5e4c0 Merge branch 'main' of https://bdgit.educoder.net/pmc9py4oq/software_teamwork
3 months ago
forely 4e70fb4ffd 重构部分表与接口,新增帖子,评论的相关接口
3 months ago
forely f07adec658 重构部分表与接口,新增帖子,评论的相关接口
3 months ago

@ -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&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>

@ -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>

@ -0,0 +1,22 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Edge",
"request": "launch",
"type": "msedge",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
},
{
"type": "msedge",
"request": "launch",
"name": "针对 localhost 启动 Edge",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

@ -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

@ -55,13 +55,4 @@
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -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")
);
}
}

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

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

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

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

@ -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; // 数据列表
}

@ -30,6 +30,7 @@ public final class JWTUtil {
private static final long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username";
private static final String USER_AVATAR_KEY = "avatar";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String ISS = "luojiachannel";
public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";
@ -45,6 +46,7 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
@ -64,6 +66,7 @@ public final class JWTUtil {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
@ -148,7 +151,7 @@ public final class JWTUtil {
if(ttl < NEED_REFRESH_TTL)
redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
user.setAccessToken(newAccessToken);
user.setRefreshToken(newRefreshToken);
return user;
}

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

@ -10,16 +10,14 @@ import java.util.Optional;
public final class UserContext {
private static final ThreadLocal<UserDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void setUser(UserDTO user) {
USER_THREAD_LOCAL.set(user);
}
public static Long getUserId() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
if(userInfoDTO == null){
throw new UserException("用户不存在");
}
return userInfoDTO.getUserId();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getUserId).orElse(null);
}
public static String getUsername() {
@ -42,6 +40,10 @@ public final class UserContext {
return Optional.ofNullable(userInfoDTO).map(UserDTO::getRefreshToken).orElse(null);
}
public static UserDTO getUser() {
return USER_THREAD_LOCAL.get();
}
public static void removeUser() {
USER_THREAD_LOCAL.remove();
}

@ -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

@ -106,14 +106,25 @@
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 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>
</project>

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

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

@ -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>

@ -16,10 +16,5 @@
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</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>
</project>

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

@ -56,6 +56,13 @@
<scope>test</scope>
</dependency>
<!-- OpenAPI 3.0 (Swagger) 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>
<build>
@ -63,6 +70,22 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.luojia_channel.LuojiaChannelApplication</mainClass>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

@ -2,12 +2,14 @@ package com.luojia_channel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class LuojiaChannelApplication {
public static void main(String[] args) {
SpringApplication.run(LuojiaChannelApplication.class, args);
}
}
}

@ -0,0 +1,127 @@
package com.luojia_channel.modules.admin.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.admin.dto.*;
import com.luojia_channel.modules.admin.service.AdminService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
@Tag(name = "管理员模块", description = "管理员相关接口")
public class AdminController {
private final AdminService adminService;
@GetMapping("/overview")
@Operation(summary = "获取系统总览数据", description = "获取系统各项统计数据,包括用户总数、帖子总数、评论总数、活跃用户等")
public Result<AdminOverviewDTO> getOverview() {
return Result.success(adminService.getOverview());
}
@GetMapping("/users")
@Operation(summary = "获取用户列表", description = "分页获取用户列表,支持按用户名、手机号、邮箱等搜索筛选")
public Result<PageResponse<AdminUserDTO>> getUsers(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "用户角色") @RequestParam(required = false) Integer role,
@Parameter(description = "用户状态") @RequestParam(required = false) Integer status
) {
return Result.success(adminService.getUserList(page, size, keyword, role, status));
}
@PutMapping("/users/{id}/status")
@Operation(summary = "更改用户状态", description = "冻结或解冻用户")
public Result<Void> changeUserStatus(
@Parameter(description = "用户ID") @PathVariable Long id,
@Parameter(description = "状态1正常 2冻结") @RequestParam Integer status
) {
adminService.changeUserStatus(id, status);
return Result.success();
}
@PutMapping("/users/{id}/role")
@Operation(summary = "更改用户角色", description = "升级或降级用户角色")
public Result<Void> changeUserRole(
@Parameter(description = "用户ID") @PathVariable Long id,
@Parameter(description = "角色1普通用户 2管理员 3超级管理员") @RequestParam Integer role
) {
adminService.changeUserRole(id, role);
return Result.success();
}
@GetMapping("/posts")
@Operation(summary = "获取帖子列表", description = "分页获取帖子列表,支持按标题、内容、分类等搜索筛选")
public Result<PageResponse<AdminPostDTO>> getPosts(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId,
@Parameter(description = "状态") @RequestParam(required = false) Integer status
) {
return Result.success(adminService.getPostList(page, size, keyword, categoryId, status));
}
@DeleteMapping("/posts/{id}")
@Operation(summary = "删除帖子", description = "管理员删除帖子")
public Result<Void> deletePost(@Parameter(description = "帖子ID") @PathVariable Long id) {
adminService.deletePost(id);
return Result.success();
}
@PutMapping("/posts/{id}/status")
@Operation(summary = "更改帖子状态", description = "置顶、取消置顶、隐藏或显示帖子")
public Result<Void> changePostStatus(
@Parameter(description = "帖子ID") @PathVariable Long id,
@Parameter(description = "操作类型(1置顶 2取消置顶 3隐藏 4显示)") @RequestParam Integer action
) {
adminService.changePostStatus(id, action);
return Result.success();
}
@GetMapping("/comments")
@Operation(summary = "获取评论列表", description = "分页获取评论列表支持按内容、帖子ID等搜索筛选")
public Result<PageResponse<AdminCommentDTO>> getComments(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "帖子ID") @RequestParam(required = false) Long postId
) {
return Result.success(adminService.getCommentList(page, size, keyword, postId));
}
@DeleteMapping("/comments/{id}")
@Operation(summary = "删除评论", description = "管理员删除评论")
public Result<Void> deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) {
adminService.deleteComment(id);
return Result.success();
}
@GetMapping("/statistics/user")
@Operation(summary = "获取用户统计数据", description = "获取用户注册增长、活跃度等统计数据")
public Result<Map<String, Object>> getUserStatistics(
@Parameter(description = "统计类型(daily,weekly,monthly)") @RequestParam(defaultValue = "daily") String type,
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate
) {
return Result.success(adminService.getUserStatistics(type, startDate, endDate));
}
@GetMapping("/statistics/post")
@Operation(summary = "获取帖子统计数据", description = "获取帖子发布、点赞、收藏等统计数据")
public Result<Map<String, Object>> getPostStatistics(
@Parameter(description = "统计类型(daily,weekly,monthly)") @RequestParam(defaultValue = "daily") String type,
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate
) {
return Result.success(adminService.getPostStatistics(type, startDate, endDate));
}
}

@ -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,82 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* DTO
*/
@Data
public class AdminOverviewDTO {
/**
*
*/
private Long totalUsers;
/**
*
*/
private Long newUsers;
/**
*
*/
private Long totalPosts;
/**
*
*/
private Long newPosts;
/**
*
*/
private Long totalComments;
/**
*
*/
private Long newComments;
/**
*
*/
private Long totalViews;
/**
*
*/
private Long todayViews;
/**
*
*/
private List<AdminUserDTO> activeUsers;
/**
*
*/
private List<AdminPostDTO> hotPosts;
/**
* 7
*/
private Map<String, Long> userGrowth;
/**
* 7
*/
private Map<String, Long> postGrowth;
/**
*
*/
private Map<String, Long> userRoleDistribution;
/**
*
*/
private Map<String, Long> postCategoryDistribution;
}

@ -0,0 +1,91 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO
*/
@Data
public class AdminPostDTO {
/**
* ID
*/
private Long id;
/**
*
*/
private String title;
/**
*
*/
private String content;
/**
*
*/
private String image;
/**
*
*/
private LocalDateTime createTime;
/**
*
*/
private LocalDateTime updateTime;
/**
* 0 1 2
*/
private Integer status;
/**
* ID
*/
private Long userId;
/**
*
*/
private String username;
/**
*
*/
private String userAvatar;
/**
* ID
*/
private Long categoryId;
/**
*
*/
private String categoryName;
/**
*
*/
private Integer likeCount;
/**
*
*/
private Integer commentCount;
/**
*
*/
private Integer favoriteCount;
/**
*
*/
private Integer viewCount;
}

@ -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,547 @@
package com.luojia_channel.modules.admin.service.impl;
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.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.admin.dto.*;
import com.luojia_channel.modules.admin.service.AdminService;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
*
*/
@Service
@RequiredArgsConstructor
public class AdminServiceImpl implements AdminService {
private final UserMapper userMapper;
private final PostMapper postMapper;
private final CommentMapper commentMapper;
@Override
public AdminOverviewDTO getOverview() {
AdminOverviewDTO overview = new AdminOverviewDTO();
// 获取用户总数
overview.setTotalUsers(userMapper.selectCount(null));
// 获取今日新增用户数
LocalDateTime today = LocalDate.now().atStartOfDay();
overview.setNewUsers(userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, today)
));
// 获取帖子总数
overview.setTotalPosts(postMapper.selectCount(null));
// 获取今日新增帖子数
overview.setNewPosts(postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, today)
));
// 获取评论总数
overview.setTotalComments(commentMapper.selectCount(null));
// 获取今日新增评论数
overview.setNewComments(commentMapper.selectCount(
new LambdaQueryWrapper<Comment>()
.ge(Comment::getCreateTime, today)
));
// 获取总浏览量和今日浏览量(假设有这样的字段或方法)
overview.setTotalViews(0L); // 实际项目中应该从数据库获取
overview.setTodayViews(0L); // 实际项目中应该从数据库获取
// 获取活跃用户排行(假设通过发帖和评论数量判断)
List<AdminUserDTO> activeUsers = new ArrayList<>(); // 实际项目中应该从数据库联表查询
overview.setActiveUsers(activeUsers);
// 获取热门帖子排行
List<AdminPostDTO> hotPosts = new ArrayList<>(); // 实际项目中应该从数据库联表查询
overview.setHotPosts(hotPosts);
// 用户增长趋势最近7天
Map<String, Long> userGrowth = new LinkedHashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(6);
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, dayStart)
.lt(User::getCreateTime, dayEnd)
);
userGrowth.put(dateStr, count);
}
overview.setUserGrowth(userGrowth);
// 帖子增长趋势最近7天
Map<String, Long> postGrowth = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, dayStart)
.lt(Post::getCreateTime, dayEnd)
);
postGrowth.put(dateStr, count);
}
overview.setPostGrowth(postGrowth);
// 用户类型分布(按角色)
Map<String, Long> userRoleDistribution = new HashMap<>();
userRoleDistribution.put("普通用户", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 1)
));
userRoleDistribution.put("管理员", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 2)
));
userRoleDistribution.put("超级管理员", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 3)
));
overview.setUserRoleDistribution(userRoleDistribution);
// 帖子分类分布(实际项目中应该从分类表联查)
Map<String, Long> postCategoryDistribution = new HashMap<>();
overview.setPostCategoryDistribution(postCategoryDistribution);
return overview;
}
@Override
public PageResponse<AdminUserDTO> getUserList(Integer page, Integer size, String keyword, Integer role, Integer status) {
Page<User> pageParam = new Page<>(page, size);
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(User::getUsername, keyword);
}
if (role != null) {
queryWrapper.eq(User::getRole, role);
}
if (status != null) {
queryWrapper.eq(User::getStatus, status);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(User::getCreateTime);
IPage<User> userPage = userMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminUserDTO> userDTOs = userPage.getRecords().stream().map(user -> {
AdminUserDTO dto = new AdminUserDTO();
BeanUtils.copyProperties(user, dto);
// 获取用户的发帖数、评论数、粉丝数、关注数(实际项目中应该通过关联查询获取)
dto.setPostCount(0);
dto.setCommentCount(0);
dto.setFollowerCount(0);
dto.setFollowingCount(0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminUserDTO>builder()
.current((long) page)
.size((long) size)
.total(userPage.getTotal())
.records(userDTOs)
.build();
}
@Override
@Transactional
public void changeUserStatus(Long id, Integer status) {
if (id == null || status == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查状态值是否合法
if (status != 1 && status != 2) {
throw new IllegalArgumentException("用户状态值不合法");
}
// 检查用户是否存在
User user = userMapper.selectById(id);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
// 修改状态
user.setStatus(status);
userMapper.updateById(user);
}
@Override
@Transactional
public void changeUserRole(Long id, Integer role) {
if (id == null || role == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查角色值是否合法
if (role < 1 || role > 3) {
throw new IllegalArgumentException("用户角色值不合法");
}
// 检查用户是否存在
User user = userMapper.selectById(id);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
// 修改角色
user.setRole(role);
userMapper.updateById(user);
}
@Override
public PageResponse<AdminPostDTO> getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status) {
Page<Post> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Post> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Post::getTitle, keyword)
.or().like(Post::getContent, keyword);
}
if (categoryId != null) {
queryWrapper.eq(Post::getCategoryId, categoryId);
}
if (status != null) {
queryWrapper.eq(Post::getStatus, status);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminPostDTO> postDTOs = postPage.getRecords().stream().map(post -> {
AdminPostDTO dto = new AdminPostDTO();
BeanUtils.copyProperties(post, dto);
// 获取发帖用户信息(实际项目中应该通过关联查询获取)
User user = userMapper.selectById(post.getUserId());
if (user != null) {
dto.setUsername(user.getUsername());
dto.setUserAvatar(user.getAvatar());
}
// 获取分类信息(实际项目中应该通过关联查询获取)
dto.setCategoryName("默认分类"); // 示例值,实际应从数据库获取
// 获取帖子的点赞数、评论数、收藏数、浏览量(实际项目中应该通过关联查询获取)
dto.setLikeCount(0);
dto.setCommentCount(0);
dto.setFavoriteCount(0);
dto.setViewCount(0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminPostDTO>builder()
.current((long) page)
.size((long) size)
.total(postPage.getTotal())
.records(postDTOs)
.build();
}
@Override
@Transactional
public void deletePost(Long id) {
if (id == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查帖子是否存在
Post post = postMapper.selectById(id);
if (post == null) {
throw new IllegalArgumentException("帖子不存在或已被删除");
}
// 删除帖子
postMapper.deleteById(id);
// 删除关联的评论(实际项目中应该通过关联删除)
commentMapper.delete(new LambdaQueryWrapper<Comment>()
.eq(Comment::getPostId, id));
}
@Override
@Transactional
public void changePostStatus(Long id, Integer action) {
if (id == null || action == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查帖子是否存在
Post post = postMapper.selectById(id);
if (post == null) {
throw new IllegalArgumentException("帖子不存在或已被删除");
}
// 根据操作类型修改状态
switch (action) {
case 1: // 置顶
post.setStatus(1);
break;
case 2: // 取消置顶
post.setStatus(0);
break;
case 3: // 隐藏
post.setStatus(2);
break;
case 4: // 显示
post.setStatus(0);
break;
default:
throw new IllegalArgumentException("操作类型不合法");
}
postMapper.updateById(post);
}
@Override
public PageResponse<AdminCommentDTO> getCommentList(Integer page, Integer size, String keyword, Long postId) {
Page<Comment> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Comment::getContent, keyword);
}
if (postId != null) {
queryWrapper.eq(Comment::getPostId, postId);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(Comment::getCreateTime);
IPage<Comment> commentPage = commentMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminCommentDTO> commentDTOs = commentPage.getRecords().stream().map(comment -> {
AdminCommentDTO dto = new AdminCommentDTO();
BeanUtils.copyProperties(comment, dto);
// 获取评论用户信息(实际项目中应该通过关联查询获取)
User user = userMapper.selectById(comment.getUserId());
if (user != null) {
dto.setUsername(user.getUsername());
dto.setUserAvatar(user.getAvatar());
}
// 获取回复用户信息(实际项目中应该通过关联查询获取)
if (comment.getParentCommentId() != null) {
User replyUser = userMapper.selectById(comment.getParentCommentId());
if (replyUser != null) {
dto.setReplyUsername(replyUser.getUsername());
}
}
// 获取帖子信息(实际项目中应该通过关联查询获取)
Post post = postMapper.selectById(comment.getPostId());
if (post != null) {
dto.setPostTitle(post.getTitle());
}
// 获取评论的点赞数(实际项目中应该通过关联查询获取)
dto.setLikeCount(comment.getLikeCount() != null ? comment.getLikeCount().intValue() : 0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminCommentDTO>builder()
.current((long) page)
.size((long) size)
.total(commentPage.getTotal())
.records(commentDTOs)
.build();
}
@Override
@Transactional
public void deleteComment(Long id) {
if (id == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查评论是否存在
Comment comment = commentMapper.selectById(id);
if (comment == null) {
throw new IllegalArgumentException("评论不存在或已被删除");
}
// 删除评论
commentMapper.deleteById(id);
// 删除子评论(实际项目中应该根据具体业务逻辑决定是否删除)
commentMapper.delete(new LambdaQueryWrapper<Comment>()
.eq(Comment::getParentCommentId, id));
}
@Override
public Map<String, Object> getUserStatistics(String type, String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 日期格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 默认查询最近7天
LocalDate end = endDate != null ? LocalDate.parse(endDate, formatter) : LocalDate.now();
LocalDate start;
if (startDate != null) {
start = LocalDate.parse(startDate, formatter);
} else {
switch (type) {
case "weekly":
start = end.minusDays(7);
break;
case "monthly":
start = end.minusDays(30);
break;
case "daily":
default:
start = end.minusDays(7);
break;
}
}
// 用户注册统计
Map<String, Long> registrations = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, dayStart)
.lt(User::getCreateTime, dayEnd)
);
registrations.put(dateStr, count);
}
result.put("registrations", registrations);
// 用户活跃度统计(实际项目中应该根据登录记录或操作记录统计)
Map<String, Long> activeUsers = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
activeUsers.put(dateStr, 0L); // 示例值,实际应从数据库获取
}
result.put("activeUsers", activeUsers);
return result;
}
@Override
public Map<String, Object> getPostStatistics(String type, String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 日期格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 默认查询最近7天
LocalDate end = endDate != null ? LocalDate.parse(endDate, formatter) : LocalDate.now();
LocalDate start;
if (startDate != null) {
start = LocalDate.parse(startDate, formatter);
} else {
switch (type) {
case "weekly":
start = end.minusDays(7);
break;
case "monthly":
start = end.minusDays(30);
break;
case "daily":
default:
start = end.minusDays(7);
break;
}
}
// 帖子发布统计
Map<String, Long> publications = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, dayStart)
.lt(Post::getCreateTime, dayEnd)
);
publications.put(dateStr, count);
}
result.put("publications", publications);
// 评论统计
Map<String, Long> comments = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = commentMapper.selectCount(
new LambdaQueryWrapper<Comment>()
.ge(Comment::getCreateTime, dayStart)
.lt(Comment::getCreateTime, dayEnd)
);
comments.put(dateStr, count);
}
result.put("comments", comments);
// 点赞统计(实际项目中应该从点赞表查询)
Map<String, Long> likes = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
likes.put(dateStr, 0L); // 示例值,实际应从数据库获取
}
result.put("likes", likes);
return result;
}
}

@ -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,40 @@
package com.luojia_channel.modules.feedback.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "反馈工单基本信息")
public class FeedbackTicketBasicInfoDTO {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "反馈内容")
private String content;
@Schema(description = "反馈类型,例如:功能建议、问题反馈等")
private String feedbackType;
@Schema(description = "工单状态1:待处理2:处理中3:已处理")
private Integer status;
@Schema(description = "联系方式,如手机号、邮箱等")
private String contactInfo;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "处理时间")
private LocalDateTime handleTime;
@Schema(description = "用户名")
private String userName;
@Schema(description = "用户头像")
private String userAvatar;
}

@ -0,0 +1,14 @@
package com.luojia_channel.modules.feedback.dto;
import com.luojia_channel.common.domain.page.PageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class FeedbackTicketPageQueryDTO extends PageRequest {
@Schema(description = "反馈类型,例如:功能建议、问题反馈等")
private String feedbackType;
@Schema(description = "工单状态1:待处理2:处理中3:已处理")
private Integer status;
}

@ -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,34 @@
package com.luojia_channel.modules.feedback.mq.consumer;
import com.luojia_channel.modules.feedback.config.FeedbackRabbitMQConfig;
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
import com.luojia_channel.modules.feedback.service.FeedbackTicketService;
import com.luojia_channel.modules.message.mq.MessageWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class FeedbackMessageConsumer {
private final FeedbackTicketService feedbackTicketService;
@RabbitListener(queues = FeedbackRabbitMQConfig.FEEDBACK_DLX_QUEUE)
public void handleFeedbackMessage(MessageWrapper<FeedbackTicket> wrapper) {
try {
FeedbackTicket ticket = wrapper.getMessage();
log.info("收到反馈工单延时消息工单ID{}", ticket.getId());
// 处理延时任务,更新工单状态
feedbackTicketService.processDelayedFeedback(ticket.getId());
} catch (Exception e) {
log.error("处理反馈工单延时消息失败消息ID{}", wrapper.getUuid(), e);
throw e;
}
}
}

@ -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 向管理员发送消息
}
}

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

@ -17,6 +17,7 @@ import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -51,21 +52,58 @@ public class FileServiceImpl extends ServiceImpl<LjFileMapper, LjFile> implement
//线程池
private static final ExecutorService SAVE_TO_DB_EXECUTOR = Executors.newFixedThreadPool(10);
@PostConstruct
private void init(){
createBucket("videos");
createBucket("images");
createBucket("chunks");
try {
createBucketWithPolicy("videos");
createBucketWithPolicy("images");
createBucketWithPolicy("chunks");
log.info("MinIO桶初始化完成");
} catch (Exception e) {
log.error("MinIO桶初始化失败: {}", e.getMessage(), e);
}
}
/**
*
*/
private void createBucketWithPolicy(String name) throws Exception {
boolean isExist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
if (!isExist) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
log.info("创建桶: {}", name);
// 设置桶策略为公共读取
String readOnlyPolicy = """
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::%s/*"]
}
]
}
""".formatted(name);
minioClient.setBucketPolicy(
SetBucketPolicyArgs.builder()
.bucket(name)
.config(readOnlyPolicy)
.build()
);
log.info("设置桶{}的公共读取策略成功", name);
}
}
@Override
public Boolean createBucket(String name){
try {
boolean isExist = minioClient.
bucketExists(BucketExistsArgs.builder().bucket(name).build());
if (!isExist) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
}
createBucketWithPolicy(name);
} catch (Exception e) {
log.error("创建桶失败: {}", e.getMessage(), e);
throw new FileException("创建桶失败");
}
return true;

@ -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,81 @@
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));
}
}

@ -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,47 @@
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; // 用户头像
}

@ -0,0 +1,59 @@
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;
}

@ -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,21 @@
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);
}

@ -0,0 +1,46 @@
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());
// 设置消息属性(如延迟时间)
if (baseSendDTO.getDelay() != null) {
m.getMessageProperties().setExpiration(baseSendDTO.getDelay().toString());
}
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;
}
}
}

@ -0,0 +1,18 @@
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安装延迟插件
}

@ -0,0 +1,41 @@
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();
}

@ -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…
Cancel
Save