Compare commits

...

47 Commits
newzwz ... 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
2023302111026黄罗霖 791862b593 个人页面查询自己帖子
2 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,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<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>
</project>

@ -1,20 +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="luojia_channel@192.168.125.128" 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.125.128: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.resource.type" value="Deployment" />
<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,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<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>

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</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>
</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,8 +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/data.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/service/src/main/resources/db/luojia_channel.sql" dialect="MySQL" />
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

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

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

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

@ -18,6 +18,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/user/register",
"/user/captcha",
"/user/verify-captcha",
"/user/check-login",
"/post/list",
"/post/detail",
"/comment/list",

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

@ -85,7 +85,7 @@ public class RedisUtil {
if(typedTuples == null || typedTuples.isEmpty()){
return ScrollPageResponse.<T>builder().build();
}
// 获取返回的offset与minTime
// 获取返回的offset与minval
List<Long> ids = new ArrayList<>();
int returnOffset = 1;
long min = 0;
@ -109,6 +109,8 @@ public class RedisUtil {
.build();
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value, DEFAULT_TIMEOUT, DEFAULT_TIME_UNIT);
}

@ -10,6 +10,7 @@ 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);
}
@ -39,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,19 +0,0 @@
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\advice\GlobalExceptionHandler.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\MybatisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\OpenApiConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\RedisConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\config\WebMvcConfig.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\constants\RedisConstant.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageRequest.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\page\PageResponse.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\Result.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\domain\UserDTO.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\BaseException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\FileException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\PostException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\exception\UserException.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\interceptor\AuthInterceptor.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\JWTUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\PageUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\RedisUtil.java
D:\yys\学习资料?\大二下\软工\大作业\software_teamwork\珞珈岛-项目相关文件\luojia-island\common\src\main\java\com\luojia_channel\common\utils\UserContext.java

@ -127,13 +127,4 @@
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</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,8 +2,10 @@ 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) {

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

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

@ -32,7 +32,7 @@ public class ChatController {
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
@GetMapping("/chat-list")
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatList(chatPageQueryDTO));
}
@ -46,7 +46,7 @@ public class ChatController {
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
@GetMapping("/history")
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(ChatPageQueryDTO chatPageQueryDTO) {
return Result.success(chatService.getChatHistory(chatPageQueryDTO));
}
}

@ -29,9 +29,9 @@ public abstract class AbstractSendProduceTemplate<T> {
m -> {
// 设置消息头
m.getMessageProperties().setHeader(KEYS, baseSendDTO.getKeys());
// 设置消息属性(如延迟时间) TODO 若需要延迟消息,需安装延时插件
// 设置消息属性(如延迟时间)
if (baseSendDTO.getDelay() != null) {
// m.getMessageProperties().setDelay(baseSendDTO.getDelay());
m.getMessageProperties().setExpiration(baseSendDTO.getDelay().toString());
}
return m;
}

@ -0,0 +1,87 @@
package com.luojia_channel.modules.post.algorithm;
import cn.hutool.core.bean.BeanUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.TimeUnit;
@Component
@RequiredArgsConstructor
public class PostSelector {
private final PostMapper postMapper;
private final CommentMapper commentMapper;
private final RedisUtil redisUtil;
public void calculatePostScore(Post post){
long hours = calculateHoursSince(post.getCreateTime());
double likes = post.getLikeCount();
double comments = post.getCommentCount();
double favorites = post.getFavoriteCount();
double newScore = (1 + likes + comments*0.7 + favorites*0.5)
/ Math.pow(1 + hours/24.0, 1.8);
redisUtil.zAdd("post:hot:"+post.getCategoryId(), post.getId(), newScore);
redisUtil.zAdd("post:hot:all", post.getId(), newScore);
}
public void calculateCommentScore(Comment comment){
long hours = calculateHoursSince(comment.getCreateTime());
double likes = comment.getLikeCount();
double replies = comment.getReplyCount();
double newScore = (1 + likes + replies*0.7)
/ Math.pow(1 + hours/24.0, 1.8);
if(comment.getId().equals(comment.getTopId())) {
redisUtil.zAdd("post:comment_by_hot:" + comment.getPostId(), comment.getId(), newScore);
} else {
redisUtil.zAdd("comment:reply_by_hot:" + comment.getTopId(), comment.getId(), newScore);
}
}
// 更新帖子热度分数
public void updatePostScore(Long postId) {
Post post = postMapper.selectById(postId);
if (post == null) return;
calculatePostScore(post);
}
// 更新评论热度分数
public void updateCommentScore(Long commentId) {
Comment comment = commentMapper.selectById(commentId);
if(comment == null) return;
calculateCommentScore(comment);
}
// 计算从指定时间到现在经过的小时数
public long calculateHoursSince(LocalDateTime startTime) {
if (startTime == null) {
return 0;
}
Instant startInstant = startTime.atZone(ZoneId.systemDefault()).toInstant();
Instant nowInstant = Instant.now();
long millisDiff = Duration.between(startInstant, nowInstant).toMillis();
return TimeUnit.HOURS.convert(millisDiff, TimeUnit.MILLISECONDS);
}
// TODO 自定义首页帖子算法
}

@ -82,7 +82,7 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法")
})
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
return Result.success(commentList);
}
@ -98,7 +98,7 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败评论ID不合法")
})
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
return Result.success(commentInfoDTOList);
}

@ -107,7 +107,7 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePost(postPageQueryDTO));
}
@ -121,7 +121,7 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(@RequestBody PostPageQueryDTO postPageQueryDTO) {
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfUser(postPageQueryDTO));
}

@ -14,7 +14,6 @@ public class CommentPageQueryDTO extends ScrollPageRequest {
@Schema(title = "评论ID")
private Long parentCommentId;
private Boolean orderByTime = true;
private Boolean orderByHot = false;
@Schema(title = "排序类型0表示按时间1表示按热度")
private Integer type = 0;
}

@ -11,4 +11,9 @@ public class PostPageQueryDTO extends ScrollPageRequest {
description = "想要查看的用户的id输入空时为自己的id"
)
private Long userId;
@Schema(title = "排序类型0表示按时间1表示按热度2表示自定义的推荐算法(暂未实现)")
private Integer type = 0;
private Long categoryId;
}

@ -74,8 +74,10 @@ public class CommentInfoDTO {
private String userName;
private String userAvatar;
private String replyUserName;
@Schema(
description = "子评论列表"
)
private List<CommentInfoDTO> commentInfoDTOList;
private List<CommentInfoDTO> commentInfoDTOList; //当前返回空
}

@ -77,5 +77,8 @@ public class PostBasicInfoDTO {
@Schema(
description = "帖子创建时间"
)
private Long categoryId;
private LocalDateTime createTime;
}

@ -82,5 +82,8 @@ public class PostInfoDTO {
@Schema(
description = "帖子创建时间"
)
private Long categoryId;
private LocalDateTime createTime;
}

@ -21,6 +21,7 @@ public class Comment {
private Long userId;
private Long postId;
private Long parentCommentId;
private Long replyUserId;
private Long topId;
private LocalDateTime createTime;
private LocalDateTime updateTime;

@ -14,6 +14,7 @@ import com.luojia_channel.common.utils.PageUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
import com.luojia_channel.modules.post.dto.resp.CommentInfoDTO;
@ -35,10 +36,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
@Service
@ -52,6 +50,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
private final NotificationProducer notificationProducer;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
private final PostSelector postSelector;
@Override
@Transactional(rollbackFor = Exception.class)
@ -66,6 +65,9 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
comment.setUserId(userId);
comment.setCreateTime(LocalDateTime.now());
comment.setUpdateTime(LocalDateTime.now());
comment.setReplyCount(0L);
comment.setLikeCount(0L);
if (!save(comment)) {
throw new PostException("创建评论失败");
}
@ -74,6 +76,7 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
if (comment.getParentCommentId() == null) {
comment.setTopId(comment.getId());
comment.setParentCommentId(0L);
comment.setReplyUserId(0L);
if (!updateById(comment)) {
throw new PostException("更新根级评论 topId 失败");
}
@ -81,8 +84,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
String key = "post:comment_by_time:" + comment.getPostId();
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
}
// 帖子的回复数加一
// 帖子的回复数加一
Long postId = comment.getPostId();
Post post = postMapper.selectById(postId);
if (post == null) {
@ -119,15 +122,15 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
}
// 设置顶级评论id
comment.setTopId(parentComment.getTopId());
comment.setReplyUserId(parentComment.getUserId());
updateById(comment);
// 更新评论的zset回复列表
String buildKey = String.format("%d_%d", comment.getPostId(), comment.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
String key = "comment:reply_by_time:" + comment.getTopId();
redisUtil.zAdd(key, comment.getId(), System.currentTimeMillis());
// 更新顶级评论回复数,当顶级评论与回复评论不同时
// 更新顶级评论回复数
LambdaUpdateWrapper<Comment> updateWrapper = Wrappers.lambdaUpdate(Comment.class)
.eq(Comment::getId, comment.getTopId())
.setSql("reply_count = reply_count + 1");
@ -136,20 +139,32 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
throw new PostException("回复顶级评论失败");
}
// 更新父评论回复数
if(!parentComment.getTopId().equals(parentComment.getId())){
parentComment.setReplyCount(parentComment.getReplyCount()+1);
updateById(parentComment);
postSelector.calculateCommentScore(parentComment);
}
postSelector.updateCommentScore(comment.getTopId());
// 消息通知,回复评论
String content = String.format("%s 回复了你的评论: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(parentComment.getUserId())
.content(content)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
if(!userId.equals(comment.getReplyUserId())){
String content = String.format("%s 回复了你的评论: %s",
UserContext.getUsername(),
StringUtils.abbreviate(commentSaveDTO.getContent(), 20));
NotificationMessage notificationMessage = NotificationMessage.builder()
.senderId(userId)
.senderName(UserContext.getUsername())
.senderAvatar(UserContext.getAvatar())
.receiverId(parentComment.getUserId())
.content(content)
.messageType(2)
.build();
notificationProducer.sendMessage(notificationMessage);
}
}
postSelector.calculateCommentScore(comment);
postSelector.calculatePostScore(post);
redisUtil.delete("post:detail:" + comment.getPostId());
return comment.getId();
}
@ -170,19 +185,42 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
@Transactional(rollbackFor = Exception.class)
public void deleteComment(Long id) {
validatePostUtil.validateCommentOwnership(id);
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getTopId, id);
int delete = commentMapper.delete(queryWrapper);
if(delete <= 0) {
throw new PostException("删除评论失败");
}
Comment comment = commentMapper.selectById(id);
if(comment.getId().equals(comment.getTopId())) {
LambdaQueryWrapper<Comment> queryWrapper = Wrappers.lambdaQuery(Comment.class)
.eq(Comment::getTopId, id);
int delete = commentMapper.delete(queryWrapper);
if(delete <= 0) {
throw new PostException("删除根评论失败");
}
Post post = postMapper.selectById(comment.getPostId());
post.setCommentCount(post.getCommentCount() - delete);
postMapper.updateById(post);
redisUtil.zRemove("post:comment_by_time:" + comment.getPostId(), comment.getId());
redisUtil.zRemove("post:comment_by_hot:" + comment.getPostId(), comment.getId());
redisUtil.delete("comment:reply_by_time:" + comment.getTopId());
redisUtil.delete("comment:reply_by_hot:" + comment.getTopId());
}else{
int delete = commentMapper.deleteById(id);
if(delete <= 0){
throw new PostException("删除评论失败");
}
Post post = postMapper.selectById(comment.getPostId());
post.setCommentCount(post.getCommentCount() - delete);
postMapper.updateById(post);
Comment parentComment = commentMapper.selectById(comment.getParentCommentId());
Comment topComment = commentMapper.selectById(comment.getTopId());
parentComment.setReplyCount(parentComment.getReplyCount() - delete);
topComment.setReplyCount(topComment.getReplyCount() - delete);
updateById(parentComment);
updateById(topComment);
redisUtil.zRemove("comment:reply_by_time:" + comment.getTopId(), comment.getId());
redisUtil.zRemove("comment:reply_by_hot:" + comment.getTopId(), comment.getId());
}
// TODO 如果根评论删除,那么其他评论怎么办,目前做法是删除其下所有的子评论
}
@ -199,11 +237,12 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String key = "post:comment_by_time:" + commentPageQueryDTO.getPostId();
String condition = commentPageQueryDTO.getType().equals(0) ? "time:" : "hot:";
String key = "post:comment_by_" + condition + commentPageQueryDTO.getPostId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
Set<Long> userIds = new HashSet<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
@ -232,13 +271,17 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
.orderByDesc(Comment::getCreateTime);
return getCommentInfoDTOPageResponse(commentPageQueryDTO, queryWrapper);
*/
String buildKey = String.format("%d_%d", commentPageQueryDTO.getPostId(), commentPageQueryDTO.getParentCommentId());
String key = "comment:reply_by_time:" + buildKey;
String condition = commentPageQueryDTO.getType().equals(0) ? "time:" : "hot:";
String key = "comment:reply_by_" + condition + commentPageQueryDTO.getParentCommentId();
return redisUtil.scrollPageQuery(key, CommentInfoDTO.class, commentPageQueryDTO,
(commentIds) -> {
List<Comment> comments = commentMapper.selectByIdsOrderByField(commentIds);
List<Long> userIds = new ArrayList<>();
Set<Long> userIds = new HashSet<>();
comments.forEach(comment -> userIds.add(comment.getUserId()));
for(Comment comment : comments){
userIds.add(comment.getUserId());
userIds.add(comment.getReplyUserId());
}
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
@ -249,6 +292,8 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
commentInfoDTO.setUserAvatar(user.getAvatar());
commentInfoDTO.setUserName(user.getUsername());
commentInfoDTO.setIsLike(isLikedComment(comment.getId()));
User replyUser = userMap.getOrDefault(comment.getReplyUserId(), new User());
commentInfoDTO.setReplyUserName(replyUser.getUsername());
commentInfoDTOS.add(commentInfoDTO);
}
return commentInfoDTOS;

@ -17,6 +17,7 @@ import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.impl.FollowServiceImpl;
import com.luojia_channel.modules.message.mq.domain.NotificationMessage;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -49,10 +50,11 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
private final ValidatePostUtil validatePostUtil;
private final RedisUtil redisUtil;
private final UserMapper userMapper;
private final PostSelector postSelector;
// 匿名用户名与匿名头像
private static final String ANONYMOUS_NAME = "匿名用户";
private static final String ANONYMOUS_AVATAR = "";
public static final String ANONYMOUS_NAME = "匿名用户";
public static final String ANONYMOUS_AVATAR = "";
private final FollowMapper followMapper;
private final FollowServiceImpl followService;
@ -68,6 +70,10 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
post.setUserId(UserContext.getUserId());
post.setCreateTime(LocalDateTime.now());
post.setUpdateTime(LocalDateTime.now());
post.setCommentCount(0);
post.setLikeCount(0);
post.setViewCount(0);
post.setFavoriteCount(0);
if(!save(post)){
throw new PostException("创建帖子失败");
}
@ -84,7 +90,9 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
}
// TODO 消息通知?
}
redisUtil.zAdd("post:time:", post.getId(), System.currentTimeMillis());
postSelector.calculatePostScore(post);
redisUtil.zAdd("post:time:"+post.getCategoryId(), post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:time:"+"all", post.getId(), System.currentTimeMillis());
redisUtil.zAdd("post:user:" + userId, post.getId(), System.currentTimeMillis());
return post.getId();
}
@ -112,6 +120,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
public void deletePost(Long id) {
validatePostUtil.validatePostOwnership(id);
Long userId = UserContext.getUserId();
Post post = postMapper.selectById(id);
int delete = postMapper.deleteById(id);
if(delete <= 0){
throw new PostException("删除帖子失败");
@ -119,7 +128,10 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
// redisUtil.delete("post:detail:" + id.toString());
// redisUtil.delete("post:of:user:" + UserContext.getUserId());
redisUtil.delete("post:detail:" + id);
redisUtil.zRemove("post:time:", id);
redisUtil.zRemove("post:time:"+post.getCategoryId(), id);
redisUtil.zRemove("post:hot:"+post.getCategoryId(), id);
redisUtil.zRemove("post:time:"+"all", id);
redisUtil.zRemove("post:hot:"+"all", id);
redisUtil.zRemove("post:user:" + userId, id);
}
@ -135,6 +147,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
if(post == null){
throw new PostException("帖子不存在或被删除");
}
postSelector.calculatePostScore(post);
PostInfoDTO postInfoDTO = BeanUtil.copyProperties(post, PostInfoDTO.class);
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class)
.eq(User::getId, post.getUserId()));
@ -152,7 +165,7 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
@Override
public ScrollPageResponse<PostBasicInfoDTO> pagePost(PostPageQueryDTO postPageQueryDTO) {
// TODO 目前分页查询直接按照创建时间顺序排序了,未来考虑加入多种规则
// TODO 未来考虑加入多种规则
/*
LambdaQueryWrapper<Post> queryWrapper = Wrappers.lambdaQuery(Post.class)
.orderByDesc(Post::getCreateTime);
@ -175,7 +188,12 @@ public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements Po
return postBasicInfoDTO;
});
*/
String key = "post:time:";
String key;
if(postPageQueryDTO.getCategoryId() == null || postPageQueryDTO.getCategoryId().equals(0L)){
key = (postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:") + "all";
}else {
key = (postPageQueryDTO.getType().equals(0) ? "post:time:" : "post:hot:") + postPageQueryDTO.getCategoryId();
}
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Long> userIds = new ArrayList<>();

@ -0,0 +1,91 @@
package com.luojia_channel.modules.post.task;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.post.algorithm.PostSelector;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Component
@RequiredArgsConstructor
public class UpdateScoreTask {
private static final int BATCH_SIZE = 100;
// 每半小时更新热度
private static final long POST_UPDATE_INTERVAL = 30 * 60;
private static final long COMMENT_UPDATE_INTERVAL = 30 * 60;
private final PostSelector postSelector;
private final PostMapper postMapper;
private final CommentMapper commentMapper;
private final RedisUtil redisUtil;
// 定时更新帖子热度分数
@Scheduled(fixedRate = POST_UPDATE_INTERVAL * 1000)
public void updatePostScores() {
// 扩大更新范围到最近30天的帖子
LocalDateTime startTime = LocalDateTime.now().minusDays(30);
LambdaQueryWrapper<Post> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(Post::getCreateTime, startTime);
long totalPosts = postMapper.selectCount(queryWrapper);
long batches = (totalPosts + BATCH_SIZE - 1) / BATCH_SIZE;
for (int i = 0; i < batches; i++) {
queryWrapper.last("LIMIT " + i * BATCH_SIZE + ", " + BATCH_SIZE);
List<Post> posts = postMapper.selectList(queryWrapper);
posts.forEach(post -> postSelector.updatePostScore(post.getId()));
}
}
// 定时更新评论热度分数
@Scheduled(fixedRate = COMMENT_UPDATE_INTERVAL * 1000)
public void updateCommentScores() {
// 扩大更新范围到最近15天的评论
LocalDateTime startTime = LocalDateTime.now().minusDays(15);
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(Comment::getCreateTime, startTime);
long totalComments = commentMapper.selectCount(queryWrapper);
long batches = (totalComments + BATCH_SIZE - 1) / BATCH_SIZE;
for (int i = 0; i < batches; i++) {
queryWrapper.last("LIMIT " + i * BATCH_SIZE + ", " + BATCH_SIZE);
List<Comment> comments = commentMapper.selectList(queryWrapper);
comments.forEach(comment -> postSelector.updateCommentScore(comment.getId()));
}
}
// 定期清理过期的缓存数据
@Scheduled(cron = "0 0 4 * * ?") // 每天凌晨4点执行
public void cleanExpiredData() {
LocalDateTime expiredTime = LocalDateTime.now().minusDays(60);
// 清理过期的帖子缓存
LambdaQueryWrapper<Post> postQueryWrapper = new LambdaQueryWrapper<>();
postQueryWrapper.le(Post::getCreateTime, expiredTime);
List<Post> expiredPosts = postMapper.selectList(postQueryWrapper);
expiredPosts.forEach(post -> {
redisUtil.zRemove("post:hot:", post.getId());
redisUtil.zRemove("post:time:", post.getId());
});
// 清理过期的评论缓存
LambdaQueryWrapper<Comment> commentQueryWrapper = new LambdaQueryWrapper<>();
commentQueryWrapper.le(Comment::getCreateTime, expiredTime);
List<Comment> expiredComments = commentMapper.selectList(commentQueryWrapper);
expiredComments.forEach(comment -> {
// 评论相关缓存清理
});
}
}

@ -1,8 +1,8 @@
package com.luojia_channel.modules.user.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import com.luojia_channel.modules.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -18,6 +18,21 @@ import org.springframework.web.multipart.MultipartFile;
@Tag(name = "用户信息管理", description = "用户修改个人信息相关接口")
public class UserInfoController {
private final UserInfoService userInfoService;
@GetMapping("/getuserinfo")
@Operation(
summary="获取用户信息",
description="获取指定用户的信息如果不指定userId则获取当前登录用户信息",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,用户不存在或未登录")
})
public Result<UserInfoVO> getUserInfo(@RequestParam(required = false) Long userId) {
return Result.success(userInfoService.getUserInfo(userId));
}
@PostMapping("/update")
@Operation(
summary="修改用户信息",

@ -7,6 +7,7 @@ import com.luojia_channel.modules.user.utils.CaptchaUtils;
import com.luojia_channel.modules.user.dto.UserLoginDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.service.UserLoginService;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
@ -19,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.awt.image.BufferedImage;
@ -31,9 +33,11 @@ import java.util.concurrent.TimeUnit;
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户登陆注册相关接口")
@Slf4j
public class UserLoginController {
private final UserLoginService userLoginService;
private final RedisUtil redisUtil;
@PostMapping("/login")
@Operation(
summary = "用户登录",
@ -41,13 +45,49 @@ public class UserLoginController {
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = UserDTO.class))),
@ApiResponse(responseCode = "200", description = "登录成功",content = @Content(schema = @Schema(implementation = UserInfoVO.class))),
@ApiResponse(responseCode = "500", description = "登录失败,用户名或密码错误")
})
public Result<UserDTO> login(@RequestBody UserLoginDTO userLoginDTO){
public Result<UserInfoVO> login(@RequestBody UserLoginDTO userLoginDTO){
return Result.success(userLoginService.login(userLoginDTO));
}
@PostMapping("/check-login")
@Operation(
summary = "检查用户登录状态",
description = "通过accessToken和refreshToken验证用户登录状态",
tags = {"用户管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "验证成功"),
@ApiResponse(responseCode = "500", description = "验证失败token无效或已过期")
})
public Result<UserDTO> checkLogin(@RequestHeader(value = "Authorization", required = false) String accessToken,
@RequestHeader(value = "X-Refresh-Token", required = false) String refreshToken) {
log.info("检查登录状态 - 接收到的Authorization: {}", accessToken);
log.info("检查登录状态 - 接收到的X-Refresh-Token: {}", refreshToken);
// 检查token是否为空
if (accessToken == null || accessToken.isEmpty()) {
log.error("登录验证失败 - accessToken为空");
return Result.fail("未提供访问令牌");
}
if (refreshToken == null || refreshToken.isEmpty()) {
log.error("登录验证失败 - refreshToken为空");
return Result.fail("未提供刷新令牌");
}
try {
UserDTO userDTO = userLoginService.checkLogin(accessToken, refreshToken);
log.info("登录验证成功 - 用户ID: {}, 用户名: {}", userDTO.getUserId(), userDTO.getUsername());
return Result.success(userDTO);
} catch (Exception e) {
log.error("登录验证失败 - 错误信息: {}", e.getMessage(), e);
return Result.fail(e.getMessage());
}
}
@PostMapping("/register")
@Operation(
summary = "用户注册",

@ -1,8 +1,8 @@
package com.luojia_channel.modules.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import com.luojia_channel.modules.user.entity.User;
import org.springframework.web.multipart.MultipartFile;
@ -13,4 +13,11 @@ public interface UserInfoService extends IService<User> {
void updatePassword(String password);
String uploadAvatar(MultipartFile file);
/**
*
* @param userId IDnull
* @return DTO
*/
UserInfoVO getUserInfo(Long userId);
}

@ -5,11 +5,12 @@ import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.modules.user.dto.UserLoginDTO;
import com.luojia_channel.modules.user.dto.UserRegisterDTO;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import jakarta.servlet.http.HttpServletRequest;
public interface UserLoginService extends IService<User> {
UserDTO login(UserLoginDTO userLoginDTO);
UserInfoVO login(UserLoginDTO userLoginDTO);
UserDTO checkLogin(String accessToken, String refreshToken);

@ -5,11 +5,9 @@ import cn.hutool.crypto.digest.BCrypt;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.service.impl.FileServiceImpl;
import com.luojia_channel.modules.file.utils.GeneratePathUtil;
import com.luojia_channel.modules.file.utils.ValidateFileUtil;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import com.luojia_channel.modules.user.service.UserInfoService;
@ -65,4 +63,36 @@ public class UserInfoServiceImpl extends ServiceImpl<UserMapper, User> implement
public String uploadAvatar(MultipartFile file) {
return fileService.uploadFile(file);
}
@Override
public UserInfoVO getUserInfo(Long userId) {
// 如果userId为null则获取当前登录用户的ID
if (userId == null) {
userId = UserContext.getUserId();
if (userId == null) {
throw new UserException("用户未登录");
}
}
// 从数据库获取用户信息
User user = userMapper.selectById(userId);
if (user == null) {
throw new UserException("用户不存在");
}
// 转换为DTO对象
UserInfoVO userInfoVO = UserInfoVO.builder()
.id(user.getId())
.username(user.getUsername())
.phone(user.getPhone())
.email(user.getEmail())
.avatar(user.getAvatar())
.gender(user.getGender())
.college(user.getCollege())
.role(user.getRole())
.status(user.getStatus())
.build();
return userInfoVO;
}
}

@ -17,6 +17,7 @@ import com.luojia_channel.modules.user.service.UserLoginService;
import com.luojia_channel.common.utils.JWTUtil;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.user.utils.ValidateUserUtil;
import com.luojia_channel.modules.user.vo.UserInfoVO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@ -69,16 +70,24 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
/**
* token
* @param userDTO
* @param userId ID
* @param username
* @return accessrefresh token
*/
private void generateTokens(UserDTO userDTO){
private String[] generateTokens(Long userId, String username){
UserDTO userDTO = UserDTO.builder()
.userId(userId)
.username(username)
.build();
String accessToken = jwtUtil.generateAccessToken(userDTO);
String refreshToken = jwtUtil.generateRefreshToken(userDTO);
userDTO.setAccessToken(accessToken);
userDTO.setRefreshToken(refreshToken);
//存储refreshToken到redis
String key = "refresh_token:" + userDTO.getUserId();
String key = REFRESH_TOKEN_PREFIX + userId;
redisUtil.set(key, refreshToken, EXPIRE_TIME, TimeUnit.DAYS);
return new String[]{accessToken, refreshToken};
}
/**
@ -88,19 +97,33 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
* @return
*/
@Override
public UserDTO login(UserLoginDTO userLoginDTO) {
public UserInfoVO login(UserLoginDTO userLoginDTO) {
String userFlag = userLoginDTO.getUserFlag();
String password = userLoginDTO.getPassword();
User user = getUserByFlag(userFlag);
if (!BCrypt.checkpw(password, user.getPassword())) {
throw new UserException("密码错误");
}
UserDTO userDTO = UserDTO.builder()
.userId(user.getId())
// 生成token
String[] tokens = generateTokens(user.getId(), user.getUsername());
// 构建返回的用户信息
UserInfoVO userInfoVO = UserInfoVO.builder()
.id(user.getId())
.username(user.getUsername())
.phone(user.getPhone())
.email(user.getEmail())
.avatar(user.getAvatar())
.gender(user.getGender())
.college(user.getCollege())
.role(user.getRole())
.status(user.getStatus())
.accessToken(tokens[0])
.refreshToken(tokens[1])
.build();
generateTokens(userDTO);
return userDTO;
return userInfoVO;
}
/**
@ -157,7 +180,13 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, User> implemen
.userId(user.getId())
.username(user.getUsername())
.build();
generateTokens(userDTO);
String accessToken = jwtUtil.generateAccessToken(userDTO);
String refreshToken = jwtUtil.generateRefreshToken(userDTO);
userDTO.setAccessToken(accessToken);
userDTO.setRefreshToken(refreshToken);
//存储refreshToken到redis
String key = "refresh_token:" + userDTO.getUserId();
redisUtil.set(key, refreshToken, EXPIRE_TIME, TimeUnit.DAYS);
return userDTO;
}
}

@ -0,0 +1,69 @@
package com.luojia_channel.modules.user.vo;
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 = "用户信息VO")
public class UserInfoVO {
@Schema(
description = "用户ID"
)
private Long id;
@Schema(
description = "用户名"
)
private String username;
@Schema(
description = "手机号"
)
private String phone;
@Schema(
description = "邮箱"
)
private String email;
@Schema(
description = "头像url"
)
private String avatar;
@Schema(
description = "性别(0未知1男2女)"
)
private Integer gender;
@Schema(
description = "学院"
)
private String college;
@Schema(
description = "角色(1普通用户2管理员3超级管理员)"
)
private Integer role;
@Schema(
description = "状态(1正常2冻结)"
)
private Integer status;
@Schema(
description = "访问令牌"
)
private String accessToken;
@Schema(
description = "刷新令牌"
)
private String refreshToken;
}

@ -1,22 +1,3 @@
#本地开发环境
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
@ -35,22 +16,38 @@
# accessKey: forely
# secretKey: Forely123!
#lj:
# db:
# host: 192.168.125.128
# password: MySQL@5678
# redis:
# host: 192.168.125.128
# port: 6379
# password: Redis@9012
# rabbitmq:
# host: 192.168.125.128
# port: 5672
# username: rabbit_admin
# password: Rabbit@3456
# minio:
# endpoint: http://192.168.125.128:9000
# accessKey: minio_admin
# secretKey: Minio@1234
lj:
db:
host: 192.168.125.128
password: MySQL@5678
host: localhost
password: 123456
redis:
host: 192.168.125.128
host: localhost
port: 6379
password: Redis@9012
password: 123456
rabbitmq:
host: 192.168.125.128
host: localhost
port: 5672
username: rabbit_admin
password: Rabbit@3456
username: guest
password: guest
minio:
endpoint: http://192.168.125.128:9000
accessKey: minio_admin
secretKey: Minio@1234
endpoint: http://localhost:9000
accessKey: root
secretKey: 12345678

@ -8,8 +8,11 @@ springdoc:
disable-swagger-default-url: true
tags-sorter: alpha
operations-sorter: alpha
packages-to-scan: com.luojia_channel.modules
paths-to-match: /user/**, /post/**, /comment/**, /message/**, /follow/**, /file/**
enabled: true
default-produces-media-type: application/json
default-consumes-media-type: application/json
packagesToScan: com.luojia_channel.modules
paths-to-match: /api/**,/admin/**
spring:
application:
name: service
@ -46,6 +49,10 @@ spring:
concurrency: 5
max-concurrency: 10
prefetch: 1
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
# minio配置
minio:
endpoint: ${lj.minio.endpoint}
@ -62,5 +69,8 @@ mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.luojia.luojia_channel.modules.*.entity
-management:
- health:
- elasticsearch:
- enabled: false

@ -1,3 +1,4 @@
USE luojia_channel; -- 指定目标数据库
## 用户表
DROP TABLE IF EXISTS `user`;
@ -106,6 +107,7 @@ CREATE TABLE `comment` (
`user_id` BIGINT NOT NULL COMMENT '评论用户ID',
`post_id` BIGINT NOT NULL COMMENT '关联的帖子ID',
`parent_comment_id` BIGINT DEFAULT 0 COMMENT '父评论ID',
`reply_user_id` BIGINT DEFAULT 0 COMMENT '回复用户ID',
`top_id` BIGINT COMMENT '顶层评论ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -176,3 +178,20 @@ CREATE TABLE `follow` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关注表';
## 反馈工单表
DROP TABLE IF EXISTS `feedback_ticket`;
CREATE TABLE `feedback_ticket` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`content` TEXT NOT NULL COMMENT '反馈内容',
`feedback_type` VARCHAR(50) NOT NULL COMMENT '反馈类型,例如:功能建议、问题反馈等',
`status` INT NOT NULL COMMENT '工单状态1:待处理2:处理中3:已处理4超时',
`contact_info` VARCHAR(100) COMMENT '联系方式,如手机号、邮箱等',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`handle_time` DATETIME COMMENT '处理时间',
INDEX idx_user_id (`user_id`),
INDEX idx_feedback_type (`feedback_type`),
INDEX idx_status (`status`),
INDEX idx_create_time (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='反馈工单表';

@ -1,56 +0,0 @@
#本地开发环境
# lj:
# db:
# host: localhost
# password: 123456
# redis:
# host: localhost
# port: 6379
# password: 123456
# rabbitmq:
# host: localhost
# port: 15672
# username: root
# password: 123456
# minio:
# endpoint: http://localhost:9000
# accessKey: minioadmin
# secretKey: minioadmin
#lj:
# db:
# host: 192.168.59.129
# password: Forely123!
# redis:
# host: 192.168.59.129
# port: 6379
# password: Forely123!
# rabbitmq:
# host: 192.168.59.129
# port: 5672
# username: admin
# password: Forely123!
# minio:
# endpoint: http://192.168.59.129:9000
# accessKey: forely
# secretKey: Forely123!
lj:
db:
host: 192.168.125.128
password: MySQL@5678
redis:
host: 192.168.125.128
port: 6379
password: Redis@9012
rabbitmq:
host: 192.168.125.128
port: 5672
username: rabbit_admin
password: Rabbit@3456
minio:
endpoint: http://192.168.125.128:9000
accessKey: minio_admin
secretKey: Minio@1234

@ -1,66 +0,0 @@
server:
port: 8081
springdoc:
api-docs:
path: /openapi/luojia-channel
swagger-ui:
path: /swagger-ui.html
disable-swagger-default-url: true
tags-sorter: alpha
operations-sorter: alpha
packages-to-scan: com.luojia_channel.modules
paths-to-match: /user/**, /post/**, /comment/**, /message/**, /follow/**, /file/**
spring:
application:
name: service
profiles:
active: local
# 数据库
datasource:
url: jdbc:mysql://${lj.db.host}:3306/luojia_channel?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: ${lj.db.password}
# redis配置
data:
redis:
host: ${lj.redis.host}
port: ${lj.redis.port}
password: ${lj.redis.password}
lettuce:
pool:
max-active: 10
max-idle: 10
min-idle: 1
time-between-eviction-runs: 10s
# rabbitmq配置
rabbitmq:
host: ${lj.rabbitmq.host}
port: ${lj.rabbitmq.port}
username: ${lj.rabbitmq.username}
password: ${lj.rabbitmq.password}
virtual-host: /
listener:
simple:
acknowledge-mode: manual
concurrency: 5
max-concurrency: 10
prefetch: 1
# minio配置
minio:
endpoint: ${lj.minio.endpoint}
accessKey: ${lj.minio.accessKey}
secretKey: ${lj.minio.secretKey}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.luojia.luojia_channel.modules.*.entity

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

Loading…
Cancel
Save