Compare commits

...

61 Commits
main ... main

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

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

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

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

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

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

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

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

@ -1,3 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic"
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive"
}

@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "珞珈岛API文档",
"description": "API文档",
"description": "珞珈岛社交平台API接口文档",
"version": "1.0"
},
"servers": [
@ -100,7 +100,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultVoid"
"$ref": "#/components/schemas/ResultLong"
}
}
}
@ -110,7 +110,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultVoid"
"$ref": "#/components/schemas/ResultLong"
}
}
}
@ -229,7 +229,17 @@
],
"responses": {
"200": {
"description": "OK",
"description": "操作成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultVoid"
}
}
}
},
"500": {
"description": "操作失败,请稍后重试",
"content": {
"*/*": {
"schema": {
@ -303,7 +313,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultVoid"
"$ref": "#/components/schemas/ResultLong"
}
}
}
@ -313,7 +323,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultVoid"
"$ref": "#/components/schemas/ResultLong"
}
}
}
@ -403,6 +413,61 @@
}
}
},
"/user/verify-captcha": {
"post": {
"tags": [
"用户管理"
],
"summary": "验证图形验证码",
"description": "验证用户输入的图形验证码是否正确",
"operationId": "verifyCaptcha",
"parameters": [
{
"name": "captchaKey",
"in": "cookie",
"required": false,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "验证码验证成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Result"
}
}
}
},
"500": {
"description": "验证码已失效或验证码错误",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/Result"
}
}
}
}
}
}
},
"/user/register": {
"post": {
"tags": [
@ -639,49 +704,6 @@
}
}
},
"/sendPrivateMessage": {
"post": {
"tags": [
"聊天模块"
],
"summary": "发送私信",
"description": "发送私信给指定用户",
"operationId": "sendPrivateMessage",
"parameters": [
{
"name": "senderId",
"in": "query",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/post/cover": {
"post": {
"tags": [
@ -731,20 +753,38 @@
}
}
},
"/post/of/me": {
"/user/captcha": {
"get": {
"tags": [
"用户管理"
],
"summary": "生成验证码图片",
"description": "生成图形验证码并设置Cookie存储captchaKey",
"operationId": "generateCaptcha",
"responses": {
"200": {
"description": "验证码生成成功"
},
"500": {
"description": "验证码生成失败"
}
}
}
},
"/post/user": {
"get": {
"tags": [
"帖子模块"
],
"summary": "查看自己的帖子",
"operationId": "pagePostOfMe",
"summary": "查看用户的帖子",
"operationId": "pagePostOfUser",
"responses": {
"200": {
"description": "获取成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponsePostBasicInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
@ -754,7 +794,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponsePostBasicInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
@ -775,7 +815,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponsePostBasicInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
@ -785,7 +825,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponsePostBasicInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
@ -835,6 +875,102 @@
}
}
},
"/message/history": {
"get": {
"tags": [
"聊天模块"
],
"summary": "历史记录",
"description": "传入分页参数,获取与特定用户的完整聊天记录",
"operationId": "getChatHistory",
"responses": {
"200": {
"description": "获取成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponseMessageResponse"
}
}
}
},
"500": {
"description": "获取失败,请稍后重试",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponseMessageResponse"
}
}
}
}
}
}
},
"/message/chat-list": {
"get": {
"tags": [
"聊天模块"
],
"summary": "聊天列表",
"description": "传入分页参数,查询私信用户列表(带最新消息)",
"operationId": "getChatList",
"responses": {
"200": {
"description": "获取成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponseChatItemDTO"
}
}
}
},
"500": {
"description": "获取失败,请稍后重试",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponseChatItemDTO"
}
}
}
}
}
}
},
"/follow/post": {
"get": {
"tags": [
"关注模块"
],
"summary": "关注收件箱",
"description": "传入分页参数,查询关注的人的发帖推送",
"operationId": "queryPostFollow",
"responses": {
"200": {
"description": "获取成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
},
"500": {
"description": "获取失败,请稍后重试",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultScrollPageResponsePostBasicInfoDTO"
}
}
}
}
}
}
},
"/follow/or/not/{id}": {
"get": {
"tags": [
@ -856,7 +992,17 @@
],
"responses": {
"200": {
"description": "OK",
"description": "已关注",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultBoolean"
}
}
}
},
"500": {
"description": "未关注",
"content": {
"*/*": {
"schema": {
@ -873,8 +1019,8 @@
"tags": [
"关注模块"
],
"summary": "关注列表",
"description": "传入用户id返回该用户的关注列表",
"summary": "共同关注",
"description": "传入用户id返回该与该用户的共同关注",
"operationId": "followCommons",
"parameters": [
{
@ -889,7 +1035,17 @@
],
"responses": {
"200": {
"description": "OK",
"description": "获取成功",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultListUserDTO"
}
}
}
},
"500": {
"description": "获取失败,请稍后重试",
"content": {
"*/*": {
"schema": {
@ -914,7 +1070,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponseCommentInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponseCommentInfoDTO"
}
}
}
@ -924,7 +1080,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponseCommentInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponseCommentInfoDTO"
}
}
}
@ -945,7 +1101,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponseCommentInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponseCommentInfoDTO"
}
}
}
@ -955,7 +1111,7 @@
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/ResultPageResponseCommentInfoDTO"
"$ref": "#/components/schemas/ResultScrollPageResponseCommentInfoDTO"
}
}
}
@ -1056,18 +1212,31 @@
"parentCommentId": {
"type": "integer",
"format": "int64",
"description": "该评论的父评论id"
},
"topId": {
"type": "integer",
"format": "int64",
"description": "该评论的顶级评论id"
"description": "该评论的父评论id若不是回复则传入空值"
}
},
"required": [
"content"
]
},
"Result": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
"code": {
"type": "integer",
"format": "int32",
"description": "状态码"
},
"msg": {
"type": "string",
"description": "提示消息"
},
"data": {
"description": "响应数据"
}
}
},
"UserRegisterDTO": {
"type": "object",
"description": "用户注册DTO",
@ -1228,79 +1397,45 @@
}
}
},
"MessageRequest": {
"ResultLong": {
"type": "object",
"description": "消息请求对象",
"description": "统一返回前端的结果",
"properties": {
"messageType": {
"code": {
"type": "integer",
"format": "int32",
"description": "消息类型0-私信1-系统通知",
"enum": [
"0",
"1"
],
"example": 0
"description": "状态码"
},
"content": {
"msg": {
"type": "string",
"description": "消息内容",
"maxLength": 500,
"minLength": 1
"description": "提示消息"
},
"receiverId": {
"data": {
"type": "integer",
"format": "int64",
"description": "接收者ID"
},
"senderName": {
"type": "string",
"description": "发送者用户名"
},
"senderAvatar": {
"type": "string",
"description": "发送者头像"
"description": "响应数据"
}
},
"required": [
"content",
"senderAvatar",
"senderName"
]
}
},
"PostPageQueryDTO": {
"type": "object",
"properties": {
"current": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"size": {
"type": "integer",
"format": "int64"
}
}
},
"PageResponsePostBasicInfoDTO": {
"type": "object",
"properties": {
"current": {
"offset": {
"type": "integer",
"format": "int64"
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
"total": {
"userId": {
"type": "integer",
"format": "int64"
},
"records": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PostBasicInfoDTO"
}
"format": "int64",
"description": "想要查看的用户的id输入空时为自己的id"
}
}
},
@ -1360,13 +1495,18 @@
"userAvatar": {
"type": "string",
"description": "匿名情况下用户头像"
},
"createTime": {
"type": "string",
"format": "date-time",
"description": "帖子创建时间"
}
},
"required": [
"title"
]
},
"ResultPageResponsePostBasicInfoDTO": {
"ResultScrollPageResponsePostBasicInfoDTO": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
@ -1380,11 +1520,34 @@
"description": "提示消息"
},
"data": {
"$ref": "#/components/schemas/PageResponsePostBasicInfoDTO",
"$ref": "#/components/schemas/ScrollPageResponsePostBasicInfoDTO",
"description": "响应数据"
}
}
},
"ScrollPageResponsePostBasicInfoDTO": {
"type": "object",
"properties": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
"records": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PostBasicInfoDTO"
}
}
}
},
"PostInfoDTO": {
"type": "object",
"description": "修改帖子信息",
@ -1448,6 +1611,11 @@
"userAvatar": {
"type": "string",
"description": "匿名情况下用户头像"
},
"createTime": {
"type": "string",
"format": "date-time",
"description": "帖子创建时间"
}
},
"required": [
@ -1455,6 +1623,201 @@
"title"
]
},
"ChatPageQueryDTO": {
"type": "object",
"properties": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
"chatUserId": {
"type": "integer",
"format": "int64"
}
}
},
"MessageResponse": {
"type": "object",
"description": "消息返回对象",
"properties": {
"messageType": {
"type": "integer",
"format": "int32",
"description": "消息类型0-私信1-系统通知",
"enum": [
"0",
"1"
],
"example": 0
},
"content": {
"type": "string",
"description": "消息内容",
"maxLength": 500,
"minLength": 1
},
"senderId": {
"type": "integer",
"format": "int64",
"description": "发送者ID"
},
"receiverId": {
"type": "integer",
"format": "int64",
"description": "接收者ID"
},
"senderName": {
"type": "string",
"description": "发送者用户名"
},
"senderAvatar": {
"type": "string",
"description": "发送者头像"
},
"createTime": {
"type": "string",
"format": "date-time",
"description": "消息创建时间"
}
},
"required": [
"content",
"senderAvatar",
"senderName"
]
},
"ResultScrollPageResponseMessageResponse": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
"code": {
"type": "integer",
"format": "int32",
"description": "状态码"
},
"msg": {
"type": "string",
"description": "提示消息"
},
"data": {
"$ref": "#/components/schemas/ScrollPageResponseMessageResponse",
"description": "响应数据"
}
}
},
"ScrollPageResponseMessageResponse": {
"type": "object",
"properties": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
"records": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"ChatItemDTO": {
"type": "object",
"description": "聊天列表项DTO",
"properties": {
"chatUserId": {
"type": "integer",
"format": "int64",
"description": "聊天对象的用户ID",
"example": 123456
},
"avatar": {
"type": "string",
"description": "聊天对象的头像URL",
"example": "https://example.com/avatar.jpg"
},
"username": {
"type": "string",
"description": "聊天对象的用户名",
"example": "张三"
},
"latestMessage": {
"type": "string",
"description": "最新消息内容",
"example": "今天下午开会",
"maxLength": 500
},
"latestTime": {
"type": "string",
"format": "date-time",
"description": "最新消息时间",
"example": "2023-10-15T14:30:00"
}
},
"required": [
"chatUserId",
"latestMessage",
"latestTime",
"username"
]
},
"ResultScrollPageResponseChatItemDTO": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
"code": {
"type": "integer",
"format": "int32",
"description": "状态码"
},
"msg": {
"type": "string",
"description": "提示消息"
},
"data": {
"$ref": "#/components/schemas/ScrollPageResponseChatItemDTO",
"description": "响应数据"
}
}
},
"ScrollPageResponseChatItemDTO": {
"type": "object",
"properties": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
"records": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ChatItemDTO"
}
}
}
},
"ResultBoolean": {
"type": "object",
"description": "统一返回前端的结果",
@ -1499,10 +1862,14 @@
"CommentPageQueryDTO": {
"type": "object",
"properties": {
"current": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
@ -1512,10 +1879,16 @@
"format": "int64",
"title": "帖子ID"
},
"commentId": {
"parentCommentId": {
"type": "integer",
"format": "int64",
"title": "评论ID"
},
"orderByTime": {
"type": "boolean"
},
"orderByHot": {
"type": "boolean"
}
},
"title": "分页查询评论请求DTO"
@ -1591,18 +1964,37 @@
}
}
},
"PageResponseCommentInfoDTO": {
"ResultScrollPageResponseCommentInfoDTO": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
"current": {
"code": {
"type": "integer",
"format": "int64"
"format": "int32",
"description": "状态码"
},
"size": {
"msg": {
"type": "string",
"description": "提示消息"
},
"data": {
"$ref": "#/components/schemas/ScrollPageResponseCommentInfoDTO",
"description": "响应数据"
}
}
},
"ScrollPageResponseCommentInfoDTO": {
"type": "object",
"properties": {
"lastVal": {
"type": "integer",
"format": "int64"
},
"total": {
"offset": {
"type": "integer",
"format": "int32"
},
"size": {
"type": "integer",
"format": "int64"
},
@ -1613,25 +2005,6 @@
}
}
}
},
"ResultPageResponseCommentInfoDTO": {
"type": "object",
"description": "统一返回前端的结果",
"properties": {
"code": {
"type": "integer",
"format": "int32",
"description": "状态码"
},
"msg": {
"type": "string",
"description": "提示消息"
},
"data": {
"$ref": "#/components/schemas/PageResponseCommentInfoDTO",
"description": "响应数据"
}
}
}
}
}

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

@ -9,13 +9,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("珞珈岛API文档")
.description("珞珈岛社交平台API接口文档")
.version("1.0")
.description("API文档")
);
}
}

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

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

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

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

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

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

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

@ -10,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,11 +0,0 @@
com\luojia\luojia_channel\advice\GlobalExceptionHandler.class
com\luojia\luojia_channel\domain\UserDTO.class
com\luojia\luojia_channel\utils\UserContext.class
com\luojia\luojia_channel\utils\JWTUtil.class
com\luojia\luojia_channel\config\RedisConfig.class
com\luojia\luojia_channel\utils\RedisUtil$ZSetItem.class
com\luojia\luojia_channel\domain\UserDTO$UserDTOBuilder.class
com\luojia\luojia_channel\exception\BaseException.class
com\luojia\luojia_channel\utils\RedisUtil.class
com\luojia\luojia_channel\domain\Result.class
com\luojia\luojia_channel\exception\UserException.class

@ -1,9 +0,0 @@
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\advice\GlobalExceptionHandler.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\config\RedisConfig.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\Result.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\domain\UserDTO.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\BaseException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\exception\UserException.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\JWTUtil.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\RedisUtil.java
D:\javaCode\luojia_channel\common\src\main\java\com\luojia\luojia_channel\utils\UserContext.java

@ -120,14 +120,11 @@
<version>2.8.8</version>
</dependency>
</dependencies>
<!-- es -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</dependencies>
</project>

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

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

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

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

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

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

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

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

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

@ -0,0 +1,81 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO
*/
@Data
public class AdminCommentDTO {
/**
* ID
*/
private Long id;
/**
*
*/
private String content;
/**
*
*/
private LocalDateTime createTime;
/**
*
*/
private LocalDateTime updateTime;
/**
* ID
*/
private Long userId;
/**
*
*/
private String username;
/**
*
*/
private String userAvatar;
/**
* ID
*/
private Long postId;
/**
*
*/
private String postTitle;
/**
* ID
*/
private Long parentCommentId;
/**
* ID
*/
private Long topId;
/**
*
*/
private String replyUsername;
/**
*
*/
private Integer likeCount;
/**
*
*/
private Integer replyCount;
}

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

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

@ -0,0 +1,71 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO -
*/
@Data
public class AdminUserDTO {
/**
* ID
*/
private Long id;
/**
*
*/
private String username;
/**
* URL
*/
private String avatar;
/**
*
*/
private LocalDateTime createTime;
/**
*
*/
private LocalDateTime updateTime;
/**
* (1 2)
*/
private Integer status;
/**
* (1 2 3)
*/
private Integer role;
/**
*
*/
private Integer integral;
/**
*
*/
private Integer postCount;
/**
*
*/
private Integer commentCount;
/**
*
*/
private Integer followerCount;
/**
*
*/
private Integer followingCount;
}

@ -0,0 +1,100 @@
package com.luojia_channel.modules.admin.service;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.admin.dto.*;
import java.util.Map;
/**
*
*/
public interface AdminService {
/**
*
* @return
*/
AdminOverviewDTO getOverview();
/**
*
* @param page
* @param size
* @param keyword
* @param role
* @param status
* @return
*/
PageResponse<AdminUserDTO> getUserList(Integer page, Integer size, String keyword, Integer role, Integer status);
/**
*
* @param id ID
* @param status
*/
void changeUserStatus(Long id, Integer status);
/**
*
* @param id ID
* @param role
*/
void changeUserRole(Long id, Integer role);
/**
*
* @param page
* @param size
* @param keyword
* @param categoryId ID
* @param status
* @return
*/
PageResponse<AdminPostDTO> getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status);
/**
*
* @param id ID
*/
void deletePost(Long id);
/**
*
* @param id ID
* @param action
*/
void changePostStatus(Long id, Integer action);
/**
*
* @param page
* @param size
* @param keyword
* @param postId ID
* @return
*/
PageResponse<AdminCommentDTO> getCommentList(Integer page, Integer size, String keyword, Long postId);
/**
*
* @param id ID
*/
void deleteComment(Long id);
/**
*
* @param type
* @param startDate
* @param endDate
* @return
*/
Map<String, Object> getUserStatistics(String type, String startDate, String endDate);
/**
*
* @param type
* @param startDate
* @param endDate
* @return
*/
Map<String, Object> getPostStatistics(String type, String startDate, String endDate);
}

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

@ -0,0 +1,67 @@
package com.luojia_channel.modules.feedback.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeedbackRabbitMQConfig {
// 反馈工单交换机
public static final String FEEDBACK_EXCHANGE = "feedback.exchange";
// 延迟队列 - 消息先进入此队列,过期后转发到死信队列
public static final String FEEDBACK_DELAY_QUEUE = "feedback.delay.queue";
// 死信交换机 - 延迟队列的死信会转发到此交换机
public static final String FEEDBACK_DLX_EXCHANGE = "feedback.dlx.exchange";
// 死信队列 - 最终消费者监听此队列
public static final String FEEDBACK_DLX_QUEUE = "feedback.dlx.queue";
// 路由键
public static final String FEEDBACK_ROUTING_KEY = "feedback.routing.key";
// 声明反馈交换机
@Bean
DirectExchange feedbackExchange() {
return new DirectExchange(FEEDBACK_EXCHANGE, true, false);
}
// 声明死信交换机
@Bean
DirectExchange feedbackDlxExchange() {
return new DirectExchange(FEEDBACK_DLX_EXCHANGE, true, false);
}
// 声明延迟队列,并绑定到死信交换机
@Bean
Queue feedbackDelayQueue() {
return QueueBuilder.durable(FEEDBACK_DELAY_QUEUE)
.withArgument("x-dead-letter-exchange", FEEDBACK_DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", FEEDBACK_ROUTING_KEY)
.build();
}
// 声明死信队列
@Bean
Queue feedbackDlxQueue() {
return new Queue(FEEDBACK_DLX_QUEUE, true);
}
// 绑定延迟队列到反馈交换机
@Bean
Binding feedbackDelayBinding() {
return BindingBuilder.bind(feedbackDelayQueue())
.to(feedbackExchange())
.with(FEEDBACK_ROUTING_KEY);
}
// 绑定死信队列到死信交换机
@Bean
Binding feedbackDlxBinding() {
return BindingBuilder.bind(feedbackDlxQueue())
.to(feedbackDlxExchange())
.with(FEEDBACK_ROUTING_KEY);
}
}

@ -0,0 +1,65 @@
package com.luojia_channel.modules.feedback.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
import com.luojia_channel.modules.feedback.service.FeedbackTicketService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/feedback")
@Tag(name = "反馈工单模块", description = "反馈工单相关接口")
@RequiredArgsConstructor
public class FeedbackTicketController {
private final FeedbackTicketService feedbackTicketService;
@PostMapping
@Operation(summary = "创建反馈工单", description = "用户创建反馈工单")
public Result<Long> createFeedbackTicket(@RequestBody FeedbackTicket feedbackTicket) {
Long id = feedbackTicketService.createFeedbackTicket(feedbackTicket);
return Result.success(id);
}
@PutMapping
@Operation(summary = "编辑反馈工单", description = "编辑已有的反馈工单")
public Result<Void> updateFeedbackTicket(@RequestBody FeedbackTicket feedbackTicket) {
feedbackTicketService.updateFeedbackTicket(feedbackTicket);
return Result.success();
}
@GetMapping("/of/me")
@Operation(summary = "个人反馈工单查询", description = "查询当前用户的反馈工单")
public Result<List<FeedbackTicket>> getPersonalFeedbackTickets() {
Long userId = UserContext.getUserId();
if(userId == null){
throw new UserException("用户未登录");
}
List<FeedbackTicket> tickets = feedbackTicketService.getPersonalFeedbackTickets(userId);
return Result.success(tickets);
}
@GetMapping("/list")
@Operation(summary = "分页查询反馈工单", description = "分页查询反馈工单列表")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<PageResponse<FeedbackTicketBasicInfoDTO>> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO) {
PageResponse<FeedbackTicketBasicInfoDTO> pageResponse = feedbackTicketService.pageFeedbackTicket(feedbackTicketPageQueryDTO);
return Result.success(pageResponse);
}
}

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

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

@ -0,0 +1,23 @@
package com.luojia_channel.modules.feedback.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("feedback_ticket")
public class FeedbackTicket {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String content;
private String feedbackType;
private Integer status;
private String contactInfo;
private LocalDateTime createTime;
private LocalDateTime handleTime;
}

@ -0,0 +1,10 @@
package com.luojia_channel.modules.feedback.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FeedbackTicketMapper extends BaseMapper<FeedbackTicket> {
}

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

@ -0,0 +1,13 @@
package com.luojia_channel.modules.feedback.mq.domain;
import lombok.Data;
@Data
public class DelayHandleTicketMessage {
private Long id;
private Long userId;
private String content;
private String feedbackType;
private Integer status;
private String contactInfo;
}

@ -0,0 +1,34 @@
package com.luojia_channel.modules.feedback.mq.producer;
import com.luojia_channel.modules.feedback.config.FeedbackRabbitMQConfig;
import com.luojia_channel.modules.feedback.mq.domain.DelayHandleTicketMessage;
import com.luojia_channel.modules.message.mq.AbstractSendProduceTemplate;
import com.luojia_channel.modules.message.mq.BaseSendExtendDTO;
import com.luojia_channel.modules.message.mq.MessageWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class FeedbackMessageProducer extends AbstractSendProduceTemplate<DelayHandleTicketMessage>{
public FeedbackMessageProducer(@Autowired RabbitTemplate rabbitTemplate){
super(rabbitTemplate);
}
@Override
protected BaseSendExtendDTO buildBaseSendParam(DelayHandleTicketMessage messageSendEvent) {
return BaseSendExtendDTO.builder()
.eventName("DelayHandleTicketMessageEvent")
.exchange(FeedbackRabbitMQConfig.FEEDBACK_EXCHANGE)
.routingKey(FeedbackRabbitMQConfig.FEEDBACK_ROUTING_KEY)
.keys(messageSendEvent.getId().toString())
.delay(24*60*60*1000L) //延迟消息是1天
.build();
}
}

@ -0,0 +1,45 @@
package com.luojia_channel.modules.feedback.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import java.util.List;
public interface FeedbackTicketService extends IService<FeedbackTicket> {
/**
*
* @param feedbackTicket
* @return ID
*/
Long createFeedbackTicket(FeedbackTicket feedbackTicket);
/**
*
* @param feedbackTicket
*/
void updateFeedbackTicket(FeedbackTicket feedbackTicket);
/**
*
* @param userId ID
* @return
*/
List<FeedbackTicket> getPersonalFeedbackTickets(Long userId);
/**
*
* @param feedbackTicketPageQueryDTO
* @return
*/
PageResponse<FeedbackTicketBasicInfoDTO> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO);
/**
*
* @param ticketId ID
*/
void processDelayedFeedback(Long ticketId);
}

@ -0,0 +1,111 @@
package com.luojia_channel.modules.feedback.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketBasicInfoDTO;
import com.luojia_channel.modules.feedback.dto.FeedbackTicketPageQueryDTO;
import com.luojia_channel.modules.feedback.entity.FeedbackTicket;
import com.luojia_channel.modules.feedback.mapper.FeedbackTicketMapper;
import com.luojia_channel.modules.feedback.mq.domain.DelayHandleTicketMessage;
import com.luojia_channel.modules.feedback.mq.producer.FeedbackMessageProducer;
import com.luojia_channel.modules.feedback.service.FeedbackTicketService;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class FeedbackTicketServiceImpl extends ServiceImpl<FeedbackTicketMapper, FeedbackTicket> implements FeedbackTicketService {
private final UserMapper userMapper;
private final FeedbackMessageProducer feedbackMessageProducer;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createFeedbackTicket(FeedbackTicket feedbackTicket) {
feedbackTicket.setCreateTime(LocalDateTime.now());
feedbackTicket.setStatus(1);
if(!save(feedbackTicket))
throw new RuntimeException("保存工单失败");
// 发送延时消息
feedbackMessageProducer.sendMessage(BeanUtil.copyProperties(feedbackTicket, DelayHandleTicketMessage.class));
return feedbackTicket.getId();
}
@Override
public void updateFeedbackTicket(FeedbackTicket feedbackTicket) {
updateById(feedbackTicket);
}
@Override
public List<FeedbackTicket> getPersonalFeedbackTickets(Long userId) {
LambdaQueryWrapper<FeedbackTicket> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(FeedbackTicket::getUserId, userId);
return list(queryWrapper);
}
@Override
public PageResponse<FeedbackTicketBasicInfoDTO> pageFeedbackTicket(FeedbackTicketPageQueryDTO feedbackTicketPageQueryDTO) {
LambdaQueryWrapper<FeedbackTicket> queryWrapper = new LambdaQueryWrapper<>();
if (feedbackTicketPageQueryDTO.getFeedbackType() != null) {
queryWrapper.eq(FeedbackTicket::getFeedbackType, feedbackTicketPageQueryDTO.getFeedbackType());
}
if (feedbackTicketPageQueryDTO.getStatus() != null) {
queryWrapper.eq(FeedbackTicket::getStatus, feedbackTicketPageQueryDTO.getStatus());
}
queryWrapper.orderByDesc(FeedbackTicket::getCreateTime);
Page<FeedbackTicket> page = new Page<>(feedbackTicketPageQueryDTO.getCurrent(), feedbackTicketPageQueryDTO.getSize());
IPage<FeedbackTicket> feedbackTicketPage = baseMapper.selectPage(page, queryWrapper);
List<Long> userIds = new ArrayList<>();
feedbackTicketPage.getRecords().forEach(ticket -> userIds.add(ticket.getUserId()));
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<FeedbackTicketBasicInfoDTO> feedbackTicketBasicInfoDTOS = new ArrayList<>();
feedbackTicketPage.getRecords().forEach(ticket -> {
FeedbackTicketBasicInfoDTO dto = new FeedbackTicketBasicInfoDTO();
BeanUtil.copyProperties(ticket, dto);
User user = userMap.getOrDefault(ticket.getUserId(), new User());
dto.setUserName(user.getUsername());
dto.setUserAvatar(user.getAvatar());
feedbackTicketBasicInfoDTOS.add(dto);
});
return PageResponse.<FeedbackTicketBasicInfoDTO>builder()
.current(feedbackTicketPage.getCurrent())
.size(feedbackTicketPage.getSize())
.total(feedbackTicketPage.getTotal())
.records(feedbackTicketBasicInfoDTOS)
.build();
}
@Override
public void processDelayedFeedback(Long ticketId) {
// 查询工单
FeedbackTicket ticket = getById(ticketId);
if (ticket == null || ticket.getStatus().equals(3)) {
return;
}
// 处理延时任务,更新工单状态为超时
ticket.setStatus(4); //
updateById(ticket);
// TODO 向管理员发送消息
}
}

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

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

@ -2,8 +2,14 @@ package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@ -24,6 +30,10 @@ public class FollowController {
description = "关注用户传入用户id和是否关注的状态",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "操作成功"),
@ApiResponse(responseCode = "500", description = "操作失败,请稍后重试")
})
public Result<Void> follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
followService.follow(followUserId, isFollow);
return Result.success();
@ -34,16 +44,38 @@ public class FollowController {
description = "传入用户id返回是否关注该用户",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "已关注"),
@ApiResponse(responseCode = "500", description = "未关注")
})
public Result<Boolean> isFollow(@PathVariable("id") Long followUserId){
return Result.success(followService.isFollow(followUserId));
}
@GetMapping("/common/{id}")
@Operation(
summary = "关注列表",
description = "传入用户id返回该用户的关注列表",
summary = "共同关注",
description = "传入用户id返回该与该用户的共同关注",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<List<UserDTO>> followCommons(@PathVariable("id") Long id){
return Result.success(followService.followCommons(id));
}
@GetMapping("/post")
@Operation(
summary = "关注收件箱",
description = "传入分页参数,查询关注的人的发帖推送",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> queryPostFollow(@RequestBody PostPageQueryDTO postPageQueryDTO){
return Result.success(followService.queryPostFollow(postPageQueryDTO));
}
}

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

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

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

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

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

@ -3,19 +3,25 @@ package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@ -25,6 +31,9 @@ public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> impleme
private final FollowMapper followMapper;
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
@Override
public void follow(Long followUserId, Boolean isFollow) {
Long userId = UserContext.getUserId();
@ -70,4 +79,29 @@ public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> impleme
return userDTOS;
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
String key = "post:follow_of:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Post> posts = postMapper.selectBatchIds(postIds);
List<Long> userIds = posts.stream().map(Post::getUserId).toList();
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
User user = userMap.getOrDefault(post.getUserId(), new User());
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTOS.add(postBasicInfoDTO);
}
// 按照发布时间倒序排序
postBasicInfoDTOS.sort(Comparator.comparing(PostBasicInfoDTO::getCreateTime).reversed());
return postBasicInfoDTOS;
});
}
}

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

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

@ -33,7 +33,7 @@ public class NotificationListener {
NotificationMessage message = wrapper.getMessage();
MessageRequest request = BeanUtil.copyProperties(message, MessageRequest.class);
Integer messageType = message.getMessageType();
if (messageType != null && messageType == 0) {
if (messageType != null && !messageType.equals(0)) {
webSocketServer.sendPrivateMessage(message.getSenderId(), request);
} else {
webSocketServer.sendSystemNotification(request);

@ -2,14 +2,15 @@ package com.luojia_channel.modules.message.server;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.message.dto.MessageRequest;
import com.luojia_channel.modules.message.dto.MessageResponse;
import com.luojia_channel.modules.message.entity.MessageDO;
import com.luojia_channel.modules.message.mapper.MessageMapper;
import com.luojia_channel.modules.message.util.WebSocketContext;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -21,11 +22,9 @@ import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint(value = "/connect/{userId}")
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketServer {
// 存储在线用户会话 <userId, Session>
private final static Map<String, Session> CLIENTS = new ConcurrentHashMap<>();
private final MessageMapper messageMapper;
@OnOpen
public void onOpen(@PathParam("userId") String userId,
@ -60,16 +59,10 @@ public class WebSocketServer {
try {
// 解析客户端发送的 JSON 消息
MessageRequest request = JSON.parseObject(message, MessageRequest.class);
switch (request.getMessageType()) {
case 0:
sendPrivateMessage(Long.parseLong(senderId), request);
break;
case 1:
sendSystemNotification(request);
break;
default:
log.warn("未知消息类型: {}", request.getMessageType());
case 0 -> sendSystemNotification(request);
case 1 -> sendPrivateMessage(Long.parseLong(senderId), request);
default -> log.warn("未知消息类型: {}", request.getMessageType());
}
} catch (Exception e) {
log.error("消息处理失败: {}", e.getMessage());
@ -83,7 +76,7 @@ public class WebSocketServer {
Session receiverSession = CLIENTS.get(receiverId.toString());
// 构建私信响应
MessageResponse response = MessageResponse.builder()
.messageType(0)
.messageType(request.getMessageType())
.content(request.getContent())
.senderId(senderId)
.senderName(request.getSenderName())
@ -99,14 +92,27 @@ public class WebSocketServer {
log.info("接收方 [{}] 不在线,消息无法即时送达", receiverId);
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
RedisUtil redisUtil = WebSocketContext.getBean(RedisUtil.class);
messageMapper.insert(message);
sendMessage(CLIENTS.get(senderId.toString()), JSON.toJSONString(response));
// 存储消息至redis
if(request.getMessageType().equals(1)){
String key = "chat:history:" + Math.min(senderId, receiverId) + ":" +Math.max(senderId, receiverId);
redisUtil.zAdd(key, message.getId(), System.currentTimeMillis());
String chatListKey = "chat:user_list:" + senderId;
redisUtil.zAdd(chatListKey, receiverId, System.currentTimeMillis());
chatListKey = "chat:user_list:" + receiverId;
redisUtil.zAdd(chatListKey, senderId, System.currentTimeMillis());
}
}
// 发送系统通知
public void sendSystemNotification(MessageRequest request) {
MessageResponse response = MessageResponse.builder()
.messageType(1)
.senderId(0L)
.receiverId(0L)
.messageType(request.getMessageType())
.content(request.getContent())
.createTime(LocalDateTime.now())
.build();
@ -115,6 +121,7 @@ public class WebSocketServer {
sendMessage(session, JSON.toJSONString(response));
}
MessageDO message = BeanUtil.copyProperties(response, MessageDO.class);
MessageMapper messageMapper = WebSocketContext.getBean(MessageMapper.class);
messageMapper.insert(message);
}

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

@ -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 自定义首页帖子算法
}

@ -2,6 +2,7 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.post.dto.req.CommentPageQueryDTO;
import com.luojia_channel.modules.post.dto.req.CommentSaveDTO;
@ -37,9 +38,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Void> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
commentService.saveComment(commentSaveDTO);
return Result.success();
public Result<Long> saveComment(@RequestBody CommentSaveDTO commentSaveDTO) {
return Result.success(commentService.saveComment(commentSaveDTO));
}
// 更新评论
@ -82,8 +82,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败帖子ID不合法")
})
public Result<PageResponse<CommentInfoDTO>> getCommentsByPostId(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
PageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
public Result<ScrollPageResponse<CommentInfoDTO>> getCommentsByPostId(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentList = commentService.getCommentsByPostId(commentPageQueryDTO);
return Result.success(commentList);
}
@ -98,8 +98,8 @@ public class CommentController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败评论ID不合法")
})
public Result<PageResponse<CommentInfoDTO>> getReplyById(@RequestBody CommentPageQueryDTO commentPageQueryDTO) {
PageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
public Result<ScrollPageResponse<CommentInfoDTO>> getReplyById(CommentPageQueryDTO commentPageQueryDTO) {
ScrollPageResponse<CommentInfoDTO> commentInfoDTOList = commentService.getReplyById(commentPageQueryDTO);
return Result.success(commentInfoDTOList);
}

@ -3,6 +3,7 @@ package com.luojia_channel.modules.post.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.post.dto.req.PostSaveDTO;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
@ -34,9 +35,8 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "500", description = "创建失败,请稍后重试")
})
public Result<Void> savePost(@RequestBody PostSaveDTO postSaveDTO) {
postService.savePost(postSaveDTO);
return Result.success();
public Result<Long> savePost(@RequestBody PostSaveDTO postSaveDTO) {
return Result.success(postService.savePost(postSaveDTO));
}
// 设置帖子封面
@ -93,8 +93,8 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,帖子不存在或被删除")
})
public PostInfoDTO getPostDetail(@RequestParam("id") Long id) {
return postService.getPostDetail(id);
public Result<PostInfoDTO> getPostDetail(@RequestParam("id") Long id) {
return Result.success(postService.getPostDetail(id));
}
// 分页查询帖子
@ -107,22 +107,22 @@ public class PostController {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<PageResponse<PostBasicInfoDTO>> pagePost(@RequestBody PostPageQueryDTO postPageQueryDTO) {
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePost(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePost(postPageQueryDTO));
}
// 查看自己的帖子
@GetMapping("/of/me")
// 查看用户的帖子
@GetMapping("/user")
@Operation(
summary = "查看自己的帖子",
summary = "查看用户的帖子",
tags = {"帖子模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<PageResponse<PostBasicInfoDTO>> pagePostOfMe(@RequestBody PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfMe(postPageQueryDTO));
public Result<ScrollPageResponse<PostBasicInfoDTO>> pagePostOfUser(PostPageQueryDTO postPageQueryDTO) {
return Result.success(postService.pagePostOfUser(postPageQueryDTO));
}
// 点赞帖子

@ -1,15 +1,19 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(title = "分页查询评论请求DTO")
public class CommentPageQueryDTO extends PageRequest {
public class CommentPageQueryDTO extends ScrollPageRequest {
@Schema(title = "帖子ID")
private Long postId;
@Schema(title = "评论ID")
private Long commentId;
private Long parentCommentId;
@Schema(title = "排序类型0表示按时间1表示按热度")
private Integer type = 0;
}

@ -34,12 +34,8 @@ public class CommentSaveDTO {
private Long postId;
@Schema(
description = "该评论的父评论id"
description = "该评论的父评论id,若不是回复则传入空值"
)
private Long parentCommentId;
@Schema(
description = "该评论的顶级评论id"
)
private Long topId;
}

@ -1,9 +1,19 @@
package com.luojia_channel.modules.post.dto.req;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.ScrollPageRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PostPageQueryDTO extends PageRequest {
public class PostPageQueryDTO extends ScrollPageRequest {
@Schema(
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; //当前返回空
}

@ -3,6 +3,8 @@ package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "帖子基本信息")
public class PostBasicInfoDTO {
@ -25,6 +27,11 @@ public class PostBasicInfoDTO {
)
private String title;
@Schema(
description = "帖子摘要显示帖子内容的前20个字"
)
private String summary;
@Schema(
description = "点赞数"
)
@ -41,7 +48,12 @@ public class PostBasicInfoDTO {
private Integer favoriteCount;
@Schema(
description = "是否点赞,1-已点赞,0-未点赞",
description = "帖子浏览数"
)
private Integer viewCount;
@Schema(
description = "是否点赞,1-已点赞0-未点赞",
allowableValues = {"0", "1"},
example = "1",
implementation = Boolean.class
@ -62,4 +74,11 @@ public class PostBasicInfoDTO {
description = "匿名情况下用户头像"
)
private String userAvatar;
@Schema(
description = "帖子创建时间"
)
private Long categoryId;
private LocalDateTime createTime;
}

@ -3,6 +3,8 @@ package com.luojia_channel.modules.post.dto.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "修改帖子信息")
public class PostInfoDTO {
@ -49,6 +51,11 @@ public class PostInfoDTO {
)
private Integer favoriteCount;
@Schema(
description = "帖子浏览数"
)
private Integer viewCount;
@Schema(
description = "是否点赞,1-已点赞0-未点赞",
allowableValues = {"0", "1"},
@ -72,4 +79,11 @@ public class PostInfoDTO {
description = "匿名情况下用户头像"
)
private String userAvatar;
@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;

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

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

Loading…
Cancel
Save