Compare commits

...

61 Commits
main ... main

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