Compare commits

...

No commits in common. 'main' and 'zwz' have entirely different histories.
main ... zwz

@ -1,2 +0,0 @@
# software_teamwork

@ -0,0 +1 @@
Subproject commit fa9627a9dee077d927bb59a23291fb718ab8a807

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="common" />
<module name="service" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="common" options="-parameters" />
<module name="luojia_channel" options="" />
<module name="service" options="-parameters" />
</option>
</component>
</project>

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

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

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

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

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

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

@ -1,3 +0,0 @@
{
"java.compile.nullAnalysis.mode": "automatic"
}

File diff suppressed because it is too large Load Diff

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.luojia</groupId>
<artifactId>luojia_channel</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>common</artifactId>
<dependencies>
<!-- Redisson分布式锁 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!-- 工具类库 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.13.0</version>
</dependency>
<!-- JWT 依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -1,22 +0,0 @@
package com.luojia_channel.common.advice;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
// 全局异常处理
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BaseException.class)
public Object handleBadRequestException(BaseException e) {
log.error("自定义异常 -> {} , 异常原因:{} ",e.getClass().getName(), e.getMessage());
log.debug("", e);
return Result.fail(e.getErrorCode(), e.getErrorMessage());
}
}

@ -1,17 +0,0 @@
package com.luojia_channel.common.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

@ -1,22 +0,0 @@
package com.luojia_channel.common.config;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("珞珈岛API文档")
.description("珞珈岛社交平台API接口文档")
.version("1.0")
);
}
}

@ -1,45 +0,0 @@
package com.luojia_channel.common.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericFastJsonRedisSerializer());
return redisTemplate;
}
// Redisson分布式锁
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private String port;
@Value("${spring.data.redis.password}")
private String password;
@Bean
public RedissonClient redissonClient(){
//配置
Config config=new Config();
config.useSingleServer().setAddress("redis://"+host+":"+port).setPassword(password);
//创建对并且返回
return Redisson.create(config);
}
}

@ -1,30 +0,0 @@
package com.luojia_channel.common.config;
import com.luojia_channel.common.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截器
registry.addInterceptor(authInterceptor)
.excludePathPatterns("/user/login",
"/user/register",
"/user/captcha",
"/user/verify-captcha",
"/post/list",
"/post/detail",
"/comment/list",
"/comment/list/reply",
"/openapi/luojia-channel",
"/swagger-ui.html"
);
}
}

@ -1,10 +0,0 @@
package com.luojia_channel.common.constants;
public class RedisConstant {
// redis存储的refreshToken前缀
public static final String REFRESH_TOKEN_PREFIX = "refresh_token:";
// redis存储的黑名单前缀
public static final String BLACKLIST_PREFIX = "blacklist:";
// 重建缓存分布式锁前缀
public static final String SAFE_GET_LOCK_KEY_PREFIX = "safe_get_lock_key_prefix:";
}

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

@ -1,18 +0,0 @@
package com.luojia_channel.common.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDTO {
private Long userId;
private String username;
private String avatar;
private String accessToken;
private String refreshToken;
}

@ -1,10 +0,0 @@
package com.luojia_channel.common.domain.page;
import lombok.Data;
@Data
public class PageRequest {
// 普通分页参数
private Long current = 1L;
private Long size = 10L;
}

@ -1,21 +0,0 @@
package com.luojia_channel.common.domain.page;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Collections;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResponse<T> {
// 普通分页参数
private Long current; // 当前页数,适用于普通分页
private Long total;
private Long size = 10L;
private List<T> records = Collections.emptyList();
}

@ -1,14 +0,0 @@
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; // 每页数量
}

@ -1,20 +0,0 @@
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; // 数据列表
}

@ -1,15 +0,0 @@
package com.luojia_channel.common.exception;
import lombok.Getter;
@Getter
public class BaseException extends RuntimeException {
private final int errorCode;
private final String errorMessage;
public BaseException(int errorCode, String errorMessage) {
super(errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}

@ -1,7 +0,0 @@
package com.luojia_channel.common.exception;
public class FileException extends BaseException{
public FileException(String msg){
super(500, msg);
}
}

@ -1,7 +0,0 @@
package com.luojia_channel.common.exception;
public class PostException extends BaseException{
public PostException(String msg){
super(500, msg);
}
}

@ -1,7 +0,0 @@
package com.luojia_channel.common.exception;
public class UserException extends BaseException{
public UserException(String msg){
super(500, msg);
}
}

@ -1,50 +0,0 @@
package com.luojia_channel.common.interceptor;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import com.luojia_channel.common.utils.JWTUtil;
import com.luojia_channel.common.utils.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JWTUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取请求中的双Token
String accessToken = request.getHeader("Authorization");
String refreshToken = request.getHeader("X-Refresh-Token");
try {
// 验证Token并处理自动刷新
UserDTO user = jwtUtil.checkLogin(accessToken, refreshToken);
// 将新Token写入响应头
if (user.getAccessToken() != null) {
response.setHeader("New-Access-Token", JWTUtil.TOKEN_PREFIX + user.getAccessToken());
response.setHeader("New-Refresh-Token", user.getRefreshToken());
}
UserContext.setUser(user);
return true;
} catch (UserException ex) {
// Token验证失败处理
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(
JSON.toJSONString(Result.fail(ex.getMessage()))
);
return false;
}
}
}

@ -1,182 +0,0 @@
package com.luojia_channel.common.utils;
import com.alibaba.fastjson.JSON;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.common.constants.RedisConstant.BLACKLIST_PREFIX;
import static com.luojia_channel.common.constants.RedisConstant.REFRESH_TOKEN_PREFIX;
@Slf4j
@Component
@RequiredArgsConstructor
public final class JWTUtil {
private static final long ACCESS_EXPIRATION = 60 * 60 * 1000; //一小时
private static final long REFRESH_EXPIRATION = 60 * 60 * 24 * 15 * 1000; //15天
private static final long NEED_REFRESH_TTL = 60 * 60 * 24 * 7 * 1000; //7天
private static final String USER_ID_KEY = "userId";
private static final String USER_NAME_KEY = "username";
private static final String USER_AVATAR_KEY = "avatar";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String ISS = "luojiachannel";
public static final String SECRET = "SecretKey5464Created2435By54377Forely02345239354893543157956476525685754352976546564766315468763584576";
private final RedisUtil redisUtil;
/**
* accessToken
* @param userInfo
* @return
*/
public String generateAccessToken(UserDTO userInfo) {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
.setIssuer(ISS)
.setSubject(JSON.toJSONString(customerUserMap))
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION))
.compact();
return jwtToken;
}
/**
* refreshToken
* @param userInfo
* @return
*/
public String generateRefreshToken(UserDTO userInfo) {
Map<String, Object> customerUserMap = new HashMap<>();
customerUserMap.put(USER_ID_KEY, userInfo.getUserId());
customerUserMap.put(USER_NAME_KEY, userInfo.getUsername());
customerUserMap.put(USER_AVATAR_KEY, userInfo.getAvatar());
String jwtToken = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setIssuedAt(new Date())
.setIssuer(ISS)
.setSubject(JSON.toJSONString(customerUserMap))
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION))
.compact();
return jwtToken;
}
/**
* token
* @param accessToken
* @return
*/
public UserDTO parseJwtToken(String accessToken) {
if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
throw new UserException("Token格式错误");
}
String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
try {
Claims claims = parseToken(rawAccessToken);
Date expiration = claims.getExpiration();
if (expiration.after(new Date())) {
String subject = claims.getSubject();
return JSON.parseObject(subject, UserDTO.class);
}
} catch (ExpiredJwtException ignored) {
} catch (Exception ex) {
log.error("JWT Token解析失败请检查", ex);
}
return null;
}
/**
*
* @param accessToken
* @param refreshToken
* @return
*/
public UserDTO checkLogin(String accessToken, String refreshToken) {
if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(TOKEN_PREFIX)) {
throw new UserException("Token格式错误");
}
String rawAccessToken = accessToken.replace(TOKEN_PREFIX, "");
// 在黑名单中
if (redisUtil.hasKey(BLACKLIST_PREFIX + rawAccessToken)) {
throw new UserException("Token已失效");
}
try {
Claims claims = parseToken(rawAccessToken);
UserDTO user = extractUserFromClaims(claims);
if (needRefresh(claims)) {
return refreshTokens(user, refreshToken);
}
return user;
} catch (ExpiredJwtException ex) {
// accessToken过期尝试用refreshToken刷新
// 注意,这里不能直接用过期时间与现在时间做差判断,因为过期会抛异常
return handleExpiredToken(ex, refreshToken);
} catch (Exception ex) {
throw new UserException("Token无效");
}
}
private boolean needRefresh(Claims claims) {
long remaining = claims.getExpiration().getTime() - System.currentTimeMillis();
return remaining < 30 * 60 * 1000; // 剩余30分钟刷新
}
private UserDTO refreshTokens(UserDTO user, String refreshToken) {
String redisKey = REFRESH_TOKEN_PREFIX + user.getUserId();
String storedRefreshToken = redisUtil.get(redisKey, String.class);
if (!refreshToken.equals(storedRefreshToken)) {
throw new UserException("refreshToken无效");
}
// 生成新token并更新redis
String newAccessToken = generateAccessToken(user);
String newRefreshToken = generateRefreshToken(user);
// 惰性刷新refreshToken
Long ttl = redisUtil.getExpire(redisKey, TimeUnit.MILLISECONDS);
if(ttl < NEED_REFRESH_TTL)
redisUtil.set(redisKey, newRefreshToken, REFRESH_EXPIRATION, TimeUnit.MILLISECONDS);
user.setAccessToken(newAccessToken);
return user;
}
private UserDTO handleExpiredToken(ExpiredJwtException ex, String refreshToken) {
// 从过期token中提取用户信息
UserDTO user = extractUserFromClaims(ex.getClaims());
// 刷新双 Token
return refreshTokens(user, refreshToken);
}
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
private UserDTO extractUserFromClaims(Claims claims) {
String subject = claims.getSubject();
return JSON.parseObject(subject, UserDTO.class);
}
public long getRemainingTime(String rawToken){
Claims claims = parseToken(rawToken);
return claims.getExpiration().getTime()-System.currentTimeMillis();
}
}

@ -1,41 +0,0 @@
package com.luojia_channel.common.utils;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luojia_channel.common.domain.page.PageRequest;
import com.luojia_channel.common.domain.page.PageResponse;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class PageUtil {
public static <R,T> PageResponse<R> convert(IPage<T> page, Function<? super T,? extends R> mapper) {
List<R> targetList = page.getRecords().stream()
.map(mapper)
.collect(Collectors.toList());
return PageResponse.<R>builder()
.current(page.getCurrent())
.size(page.getSize())
.records(targetList)
.total(page.getTotal())
.build();
}
public static <R,T> PageResponse<R> convert(IPage<T> page, Class<R> clazz) {
List<R> targetList = page.getRecords().stream()
.map(each -> BeanUtil.copyProperties(each, clazz))
.collect(Collectors.toList());
return PageResponse.<R>builder()
.current(page.getCurrent())
.size(page.getSize())
.records(targetList)
.total(page.getTotal())
.build();
}
public static Page convert(PageRequest request){
return new Page(request.getCurrent(), request.getSize());
}
}

@ -1,270 +0,0 @@
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;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
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;
import static com.luojia_channel.common.constants.RedisConstant.SAFE_GET_LOCK_KEY_PREFIX;
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
private static final Long DEFAULT_TIMEOUT = 30000L;
private static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.MILLISECONDS;
// 获取redisTemplate,因为复杂的redis操作不便于封装
public RedisTemplate<String, Object> getInstance(){
return redisTemplate;
}
/**
* redis
*/
public <T> T get(String key, Class<T> type) {
Object value = redisTemplate.opsForValue().get(key);
return value != null ? type.cast(value) : null;
}
// 安全地从缓存中取值
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;
}
// 重建缓存
RLock lock = redissonClient.getLock(SAFE_GET_LOCK_KEY_PREFIX + key);
lock.lock();
try{
if(get(key, type) == null){
T dbResult = cacheLoader.get();
if(dbResult == null){
return null;
}
set(key, dbResult, timeout, timeUnit);
return dbResult;
}
}finally {
lock.unlock();
}
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与minTime
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);
}
public void set(String key, Object value, long timeout, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
public void expire(String key, long time, TimeUnit timeUnit) {
redisTemplate.expire(key, time, timeUnit);
}
public void delete(String key) {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
Boolean result = redisTemplate.hasKey(key);
return result != null && result;
}
public Long getExpire(String key, TimeUnit timeUnit){
return redisTemplate.getExpire(key, timeUnit);
}
/**
* set
*/
private <T> List<T> convertSetToList(Set<Object> set) {
if(set == null || set.isEmpty()){
return Collections.emptyList();
}
return set.stream().map(obj -> (T) obj).collect(Collectors.toList());
}
public void sAdd(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public void sRemove(String key, Object... values) {
redisTemplate.opsForSet().remove(key, values);
}
public <T> List<T> sGet(String key) {
Set<Object> members = redisTemplate.opsForSet().members(key);
return convertSetToList(members);
}
public Boolean sIsMember(String key, Object value){
return redisTemplate.opsForSet().isMember(key, value);
}
public <T> List<T> sCommon(String key, String otherKey){
Set<Object> intersect = redisTemplate.opsForSet().intersect(key, otherKey);
return convertSetToList(intersect);
}
/**
* zSet
*/
// 带分数的结果包装类
@Data
@AllArgsConstructor
public static class ZSetItem<T> {
private T value;
private Double score;
}
private <T> List<ZSetItem<T>> convertTuples(Set<ZSetOperations.TypedTuple<Object>> tuples) {
if(tuples == null || tuples.isEmpty()){
return Collections.emptyList();
}
return tuples.stream()
.map(tuple -> new ZSetItem<T>(
(T) tuple.getValue(),
tuple.getScore() != null ? tuple.getScore() : 0.0
))
.collect(Collectors.toList());
}
// 添加元素
public Boolean zAdd(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public Boolean zAdd(String key, Object value, double score, long timeout, TimeUnit unit) {
Boolean result = redisTemplate.opsForZSet().add(key, value, score);
if (Boolean.TRUE.equals(result)) {
redisTemplate.expire(key, timeout, unit);
}
return result;
}
// 获取范围
public <T> List<T> zRange(String key, long start, long end) {
Set<Object> values = redisTemplate.opsForZSet().range(key, start, end);
return convertSetToList(values);
}
public <T> List<T> zRevRange(String key, long start, long end) {
Set<Object> values = redisTemplate.opsForZSet().reverseRange(key, start, end);
return convertSetToList(values);
}
// 带分数查询(用于热度计算)
public <T> List<ZSetItem<T>> zRangeWithScores(String key, long start, long end) {
Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet().rangeWithScores(key, start, end);
return convertTuples(typedTuples);
}
// 增减分数(用于点赞数统计)
public Double zIncrScore(String key, Object value, double delta) {
return redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
// 删除元素
public Long zRemove(String key, Object... values) {
return redisTemplate.opsForZSet().remove(key, values);
}
// 获取排名(用于热榜)
public Long zRank(String key, Object value) {
return redisTemplate.opsForZSet().rank(key, value);
}
public Long zRevRank(String key, Object value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
// 获取集合大小
public Long zCard(String key) {
return redisTemplate.opsForZSet().zCard(key);
}
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);
}
}

@ -1,45 +0,0 @@
package com.luojia_channel.common.utils;
import com.alibaba.ttl.TransmittableThreadLocal;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.exception.UserException;
import java.util.Optional;
// 用户上下文
public final class UserContext {
private static final ThreadLocal<UserDTO> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();
public static void setUser(UserDTO user) {
USER_THREAD_LOCAL.set(user);
}
public static Long getUserId() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getUserId).orElse(null);
}
public static String getUsername() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getUsername).orElse(null);
}
public static String getAvatar() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getAvatar).orElse(null);
}
public static String getAccessToken() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getAccessToken).orElse(null);
}
public static String getRefreshToken() {
UserDTO userInfoDTO = USER_THREAD_LOCAL.get();
return Optional.ofNullable(userInfoDTO).map(UserDTO::getRefreshToken).orElse(null);
}
public static void removeUser() {
USER_THREAD_LOCAL.remove();
}
}

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

@ -1,6 +0,0 @@
{
"name": "luojia-island",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/>
</parent>
<groupId>com.luojia</groupId>
<artifactId>luojia_channel</artifactId>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>common</module>
<module>service</module>
</modules>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2024.0.0</spring-cloud.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<redisson.version>3.29.0</redisson.version>
<mysql.version>8.0.33</mysql.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- springboot3配套的MybatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok (全局管理) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
<!-- minio -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.12</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- openAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.8</version>
</dependency>
<!-- es -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.luojia</groupId>
<artifactId>luojia_channel</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>service</artifactId>
<dependencies>
<!-- 公共模块依赖 -->
<dependency>
<groupId>com.luojia</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 分布式系统支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -1,13 +0,0 @@
package com.luojia_channel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LuojiaChannelApplication {
public static void main(String[] args) {
SpringApplication.run(LuojiaChannelApplication.class, args);
}
}

@ -1,31 +0,0 @@
package com.luojia_channel.modules.file.config;
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.accessKey}")
private String accessKey;
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
try {
MinioClient minioClient = MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
return minioClient;
} catch (Exception e){
e.printStackTrace();
}
return null;
}
}

@ -1,7 +0,0 @@
package com.luojia_channel.modules.file.constants;
public class FileConstant {
public static final String CHUNK_BUCKET = "chunks";
public static final String CHUNK_PREFIX = "file:chunks:";
public static final long MAX_UPLOAD_SIZE = 10*1024*1024;
}

@ -1,17 +0,0 @@
package com.luojia_channel.modules.file.dto;
import lombok.Builder;
import lombok.Data;
// 合并分片DTO
@Data
@Builder
public class CompleteUploadDTO {
private String fileMd5;
private Integer totalChunks;
private String fileType;
private String fileName;
}

@ -1,22 +0,0 @@
package com.luojia_channel.modules.file.dto;
import lombok.Builder;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Builder
public class UploadChunkDTO {
// 分片文件
private MultipartFile file;
// 整个文件的md5
private String fileMd5;
// 分片序号从0开始
private Integer chunkNumber;
// 分片总数
private Integer totalChunks;
private String fileType;
private String fileName;
}

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

@ -1,32 +0,0 @@
package com.luojia_channel.modules.file.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("lj_file")
@Builder
public class LjFile {
private Long id;
private String fileName;
private String fileUrl;
private Long fileSize;
private String fileMd5;
private String fileType;
private Integer fileStatus; // 0正在上传1上传成功2失败3待审核
private Long userId; // 上传用户id
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

@ -1,9 +0,0 @@
package com.luojia_channel.modules.file.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.file.entity.LjFile;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LjFileMapper extends BaseMapper<LjFile> {
}

@ -1,17 +0,0 @@
package com.luojia_channel.modules.file.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import org.springframework.web.multipart.MultipartFile;
public interface FileService extends IService<LjFile> {
Boolean createBucket(String name);
Boolean deleteBucket(String name);
String uploadFile(MultipartFile file);
Long uploadFileAndGetFileId(UploadFileDTO uploadFileDTO);
Boolean uploadChunk(UploadChunkDTO chunkDTO);
Long completeUpload(CompleteUploadDTO completeDTO);
}

@ -1,342 +0,0 @@
package com.luojia_channel.modules.file.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.exception.FileException;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.dto.CompleteUploadDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import com.luojia_channel.modules.file.mapper.LjFileMapper;
import com.luojia_channel.modules.file.service.FileService;
import com.luojia_channel.modules.file.utils.GeneratePathUtil;
import com.luojia_channel.modules.file.utils.ValidateFileUtil;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_BUCKET;
import static com.luojia_channel.modules.file.constants.FileConstant.CHUNK_PREFIX;
@Service
@Slf4j
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<LjFileMapper, LjFile> implements FileService {
private final MinioClient minioClient;
private final RedisUtil redisUtil;
private final ValidateFileUtil validateFileUtil;
private final GeneratePathUtil generatePathUtil;
@Value("${minio.endpoint}")
private String endpoint;
//线程池
private static final ExecutorService SAVE_TO_DB_EXECUTOR = Executors.newFixedThreadPool(10);
private void init(){
createBucket("videos");
createBucket("images");
createBucket("chunks");
}
@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());
}
} catch (Exception e) {
throw new FileException("创建桶失败");
}
return true;
}
@Override
public Boolean deleteBucket(String name){
try {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(name).build());
} catch (Exception e) {
throw new FileException("删除桶失败");
}
return true;
}
// 普通上传MultipartFile
@Override
public String uploadFile(MultipartFile file){
String bucket;
String objectName;
String fileUrl;
try {
InputStream inputStream = file.getInputStream();
String fileName = file.getOriginalFilename();
String fileMd5 = DigestUtils.md5DigestAsHex(inputStream);
String fileType = detectFileType(file);
fileUrl = validateFileUtil.getExistedFileUrl(fileMd5);
if(fileUrl != null){
return fileUrl;
}
validateFileUtil.validateFile(new UploadFileDTO(file, fileType, fileMd5));
bucket = generatePathUtil.getBucketName(fileType);
objectName = generatePathUtil.getObjectName(fileName, fileMd5);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(file.getInputStream(),
file.getSize(), -1)
.build()
);
fileUrl = endpoint + "/" + bucket + "/" + objectName;
/* 线
SAVE_TO_DB_EXECUTOR.submit(() -> saveFileToDB(fileName, fileUrl, file.getSize(),
fileMd5, fileType, 1));
*/
saveFileToDB(fileName, fileUrl, file.getSize(),
fileMd5, fileType, 1);
} catch (Exception e){
log.error("文件上传失败: {}", e.getMessage());
throw new FileException("文件上传失败");
}
return fileUrl;
}
// 普通上传
@Override
@Transactional(rollbackFor = Exception.class)
public Long uploadFileAndGetFileId(UploadFileDTO fileDTO) {
validateFileUtil.validateFile(fileDTO);
Long fileId = validateFileUtil.getExistedFileId(fileDTO.getFileMd5());
if(fileId != null){
return fileId;
}
// 普通上传
try {
String fileName = fileDTO.getFile().getOriginalFilename();
String bucket = generatePathUtil.getBucketName(fileDTO.getFileType());
String objectName = generatePathUtil.getObjectName(fileName,
fileDTO.getFileMd5());
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(fileDTO.getFile().getInputStream(),
fileDTO.getFile().getSize(), -1)
.build()
);
return saveFileToDB(fileName, objectName, fileDTO.getFile().getSize(),
fileDTO.getFileMd5(), fileDTO.getFileType(), 1);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage());
throw new FileException("文件上传失败");
}
}
private Long saveFileToDB(String fileName, String fileUrl, Long fileSize,
String fileMd5, String fileType, Integer fileStatus){
Long userId = UserContext.getUserId();
LjFile file= LjFile.builder()
.fileName(fileName)
.fileUrl(fileUrl)
.fileSize(fileSize)
.fileMd5(fileMd5)
.fileType(fileType)
.fileStatus(fileStatus)
.userId(userId)
.createTime(LocalDateTime.now())
.updateTime(LocalDateTime.now())
.build();
if (!save(file)) {
throw new FileException("文件记录保存失败");
}
return file.getId();
}
// 分片上传
@Override
public Boolean uploadChunk(UploadChunkDTO chunkDTO) {
// 验证分片信息
validateFileUtil.validateChunk(chunkDTO);
// 判断整个文件是否存在
if (validateFileUtil.getExistedFileId(chunkDTO.getFileMd5()) != null) {
return true;
}
// 检查分片是否已上传
if (validateFileUtil.isChunkUploaded(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber())) {
return true;
}
// 保存分片到MinIO临时Bucket
String objectName = generatePathUtil
.getChunkObjectName(chunkDTO.getFileMd5(), chunkDTO.getChunkNumber());
// 一小时内保存进度
String key = CHUNK_PREFIX + chunkDTO.getFileMd5();
try {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.stream(chunkDTO.getFile().getInputStream(),
chunkDTO.getFile().getSize(), -1)
.build()
);
redisUtil.sAdd(key, chunkDTO.getChunkNumber());
redisUtil.expire(key, 1, TimeUnit.HOURS);
return true;
} catch (Exception e) {
// 遇到异常删除已上传的分片
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.build());
redisUtil.sRemove(key, chunkDTO.getChunkNumber());
} catch (Exception ex) {
log.error("删除分片失败: {}", ex.getMessage(), ex);
}
log.error("分片上传失败: {}", e.getMessage());
throw new FileException("分片上传失败");
}
}
// 合并分片
@Override
@Transactional(rollbackFor = Exception.class)
public Long completeUpload(CompleteUploadDTO completeDTO) {
// 验证分片完整性
if (!validateFileUtil.
areAllChunksUploaded(completeDTO.getFileMd5(), completeDTO.getTotalChunks())) {
throw new FileException("存在未上传的分片");
}
// 合并分片到目标Bucket
String bucket = generatePathUtil.getBucketName(completeDTO.getFileType());
String objectName = generatePathUtil
.getObjectName(completeDTO.getFileName(), completeDTO.getFileMd5());
try {
List<ComposeSource> sources = new ArrayList<>();
for (int i = 0; i < completeDTO.getTotalChunks(); i++) {
String chunkObjectName = generatePathUtil
.getChunkObjectName(completeDTO.getFileMd5(), i);
sources.add(
ComposeSource.builder()
.bucket(CHUNK_BUCKET)
.object(chunkObjectName)
.build());
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.sources(sources)
.bucket(bucket)
.object(objectName)
.build());
// 获得文件大小
long fileSize = minioClient.statObject(
StatObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build()
).size();
// 保存文件到数据库
Long fileId = saveFileToDB(completeDTO.getFileName(), objectName, fileSize,
completeDTO.getFileMd5(), completeDTO.getFileType(), 3);
// 删除临时分片
deleteChunks(completeDTO.getFileMd5());
return fileId;
} catch (Exception e) {
log.error("合并分片失败: {}", e.getMessage());
throw new FileException("合并分片失败");
}
}
// 删除临时分片,与遍历删除相比该操作只需要进行一次io
public void deleteChunks(String fileMd5) {
try {
// 获取需要删除的对象列表
Iterable<Result<Item>> items = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(CHUNK_BUCKET)
.prefix(fileMd5 + "/")
.build()
);
// 转换为 DeleteObject 列表
List<DeleteObject> deleteObjects = new ArrayList<>();
for (Result<Item> result : items) {
Item item = result.get();
log.info("要删除的分片文件:{}", item.objectName());
deleteObjects.add(new DeleteObject(item.objectName()));
}
// 执行删除操作
if (!deleteObjects.isEmpty()) {
log.info("正在删除 {} 个分片文件", deleteObjects.size());
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(CHUNK_BUCKET)
.objects(deleteObjects)
.build()
);
// 遍历结果以触发删除
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
if (error != null) {
log.error("删除文件 {} 失败: {}", error.objectName(), error.message());
}
}
redisUtil.delete(CHUNK_PREFIX + fileMd5);
log.info("分片删除完成");
} else {
log.warn("未找到需要删除的分片文件fileMd5={}", fileMd5);
}
} catch (Exception e) {
log.error("删除分片失败: {}", e.getMessage(), e);
throw new FileException("删除分片失败");
}
}
private String detectFileType(MultipartFile file) throws Exception {
// 获取 MIME 类型
String mimeType = file.getContentType();
if (mimeType == null || mimeType.isEmpty()) {
throw new IllegalArgumentException("Could not determine file type");
}
// 根据 MIME 类型分类
if (mimeType.startsWith("image/")) {
return "image";
} else if (mimeType.startsWith("video/")) {
return "video";
} else {
throw new FileException("Unsupported file type: " + mimeType);
}
}
private String getObjectUrl(MinioClient minioClient, String bucket, String objectName) throws Exception {
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucket)
.object(objectName)
.build();
return minioClient.getPresignedObjectUrl(args);
}
}

@ -1,35 +0,0 @@
package com.luojia_channel.modules.file.utils;
import com.luojia_channel.common.exception.FileException;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
@Component
public class GeneratePathUtil {
public String getBucketName(String fileType) {
return switch (fileType) {
case "image" -> "images";
case "video" -> "videos";
default -> throw new FileException("无效的文件类型");
};
}
// 分片对象路径格式fileMd5/chunk_{number}
public String getChunkObjectName(String fileMd5, int chunkNumber) {
return String.format("%s/chunk_%d", fileMd5, chunkNumber);
}
// year/month/day/fileMd5/suffix
public String getObjectName(String fileName, String fileMd5) {
String suffix = fileName.contains(".") ?
fileName.substring(fileName.lastIndexOf(".")) : "";
LocalDate now = LocalDate.now();
return String.format("%d/%02d/%02d/%s%s",
now.getYear(),
now.getMonthValue(),
now.getDayOfMonth(),
fileMd5,
suffix);
}
}

@ -1,111 +0,0 @@
package com.luojia_channel.modules.file.utils;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.luojia_channel.common.exception.FileException;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.modules.file.dto.UploadChunkDTO;
import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.file.entity.LjFile;
import com.luojia_channel.modules.file.mapper.LjFileMapper;
import io.minio.MinioClient;
import io.minio.StatObjectArgs;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import static com.luojia_channel.modules.file.constants.FileConstant.*;
@Component
@RequiredArgsConstructor
public class ValidateFileUtil {
private final LjFileMapper ljFileMapper;
private final RedisUtil redisUtil;
private final GeneratePathUtil generatePathUtil;
private final MinioClient minioClient;
// 验证分片参数
public void validateChunk(UploadChunkDTO chunkDTO) {
if (chunkDTO.getFile().isEmpty()) {
throw new FileException("分片数据不能为空");
}
if (StrUtil.isBlank(chunkDTO.getFileMd5())) {
throw new FileException("文件MD5缺失");
}
if (chunkDTO.getChunkNumber() < 0) {
throw new FileException("分片编号不能为负数");
}
if (chunkDTO.getTotalChunks() <= 0) {
throw new FileException("总分片数必须大于0");
}
if(chunkDTO.getChunkNumber() >= chunkDTO.getTotalChunks()){
throw new FileException("分片序号不能大于分片总数");
}
}
// 校验普通文件上传
public void validateFile(UploadFileDTO dto) {
if (dto.getFile().isEmpty()) {
throw new FileException("文件不能为空");
}
// MD5校验
if (StrUtil.isBlank(dto.getFileMd5())) {
throw new FileException("文件MD5缺失");
}
// 类型校验
if (!Arrays.asList("image", "video").contains(dto.getFileType())) {
throw new FileException("不支持的文件类型");
}
// 大小校验
if(dto.getFile().getSize() > MAX_UPLOAD_SIZE){
throw new FileException("上传文件大小过大");
}
}
// 秒传检查
public Long getExistedFileId(String fileMd5) {
LjFile file = ljFileMapper.selectOne(Wrappers.<LjFile>lambdaQuery()
.eq(LjFile::getFileMd5, fileMd5));
if(file == null){
return null;
}
return file.getId();
}
// 检查分片是否已上传
public boolean isChunkUploaded(String fileMd5, int chunkNumber) {
String objectName = generatePathUtil.getChunkObjectName(fileMd5, chunkNumber);
String redisKey = CHUNK_PREFIX + fileMd5;
// 检查redis中是否已记录该分片
if (redisUtil.sIsMember(redisKey, chunkNumber)) {
return true;
}
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(CHUNK_BUCKET)
.object(objectName)
.build()
);
return true;
} catch (Exception e) {
return false;
}
}
// 检查所有分片是否已上传
public boolean areAllChunksUploaded(String fileMd5, int totalChunks) {
String key = CHUNK_PREFIX + fileMd5;
List<Integer> uploadedChunks = redisUtil.sGet(key);
return uploadedChunks.size() == totalChunks;
}
public String getExistedFileUrl(String fileMd5) {
LjFile file = ljFileMapper.selectOne(Wrappers.<LjFile>lambdaQuery()
.eq(LjFile::getFileMd5, fileMd5));
if(file == null){
return null;
}
return file.getFileUrl();
}
}

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

@ -1,81 +0,0 @@
package com.luojia_channel.modules.interact.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/follow")
@RequiredArgsConstructor
@Tag(name = "关注模块", description = "用户之间互相关注模块")
public class FollowController {
private final FollowService followService;
@PutMapping("/{id}/{isFollow}")
@Operation(
summary = "关注用户",
description = "关注用户传入用户id和是否关注的状态",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "操作成功"),
@ApiResponse(responseCode = "500", description = "操作失败,请稍后重试")
})
public Result<Void> follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow){
followService.follow(followUserId, isFollow);
return Result.success();
}
@GetMapping("/or/not/{id}")
@Operation(
summary = "是否关注用户",
description = "传入用户id返回是否关注该用户",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "已关注"),
@ApiResponse(responseCode = "500", description = "未关注")
})
public Result<Boolean> isFollow(@PathVariable("id") Long followUserId){
return Result.success(followService.isFollow(followUserId));
}
@GetMapping("/common/{id}")
@Operation(
summary = "共同关注",
description = "传入用户id返回该与该用户的共同关注",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<List<UserDTO>> followCommons(@PathVariable("id") Long id){
return Result.success(followService.followCommons(id));
}
@GetMapping("/post")
@Operation(
summary = "关注收件箱",
description = "传入分页参数,查询关注的人的发帖推送",
tags = {"关注模块"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,请稍后重试")
})
public Result<ScrollPageResponse<PostBasicInfoDTO>> queryPostFollow(@RequestBody PostPageQueryDTO postPageQueryDTO){
return Result.success(followService.queryPostFollow(postPageQueryDTO));
}
}

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

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

@ -1,49 +0,0 @@
package com.luojia_channel.modules.interact.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("follow")
@Schema(description = "关注表")
public class Follow implements Serializable {
/**
*
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* id
*/
@Schema(
description = "用户id"
)
private Long userId;
/**
* id
*/
@Schema(
description = "关联的用户id"
)
private Long followUserId;
/**
*
*/
@Schema(
description = "创建时间"
)
private LocalDateTime createTime;
}

@ -1,18 +0,0 @@
package com.luojia_channel.modules.interact.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luojia_channel.modules.interact.entity.Follow;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface FollowMapper extends BaseMapper<Follow> {
@Delete("delete from follow where user_id = #{userId} and follow_user_id = #{followUserId}")
boolean delete(Long userId, Long followUserId);
@Select("select count(*) from follow where user_id = #{userId} and follow_user_id = #{followUserId}")
Integer queryCount(Long userId, Long followUserId);
}

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

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

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

@ -1,107 +0,0 @@
package com.luojia_channel.modules.interact.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luojia_channel.common.domain.UserDTO;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.common.domain.page.ScrollPageResponse;
import com.luojia_channel.common.utils.RedisUtil;
import com.luojia_channel.common.utils.UserContext;
import com.luojia_channel.modules.interact.entity.Follow;
import com.luojia_channel.modules.interact.mapper.FollowMapper;
import com.luojia_channel.modules.interact.service.FollowService;
import com.luojia_channel.modules.post.dto.req.PostPageQueryDTO;
import com.luojia_channel.modules.post.dto.resp.PostBasicInfoDTO;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements FollowService {
private final FollowMapper followMapper;
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private final RedisUtil redisUtil;
private final PostMapper postMapper;
@Override
public void follow(Long followUserId, Boolean isFollow) {
Long userId = UserContext.getUserId();
String key = "follows:" + userId;
if(isFollow){
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
follow.setCreateTime(LocalDateTime.now());
boolean isSuccess = save(follow);
if(isSuccess){
redisTemplate.opsForSet().add(key, followUserId);
}
}else{
boolean isSuccess = followMapper.delete(userId, followUserId);
if(isSuccess){
redisTemplate.opsForSet().remove(key, followUserId);
}
}
}
@Override
public boolean isFollow(Long followUserId) {
Long userId = UserContext.getUserId();
Integer count = followMapper.queryCount(userId, followUserId);
return count > 0;
}
@Override
public List<UserDTO> followCommons(Long id) {
Long userId = UserContext.getUserId();
String userKey = "follows:" + userId;
String followKey = "follows:" + id;
Set<Object> intersect = redisTemplate.opsForSet().intersect(userKey, followKey);
if(intersect == null || intersect.isEmpty()){
return Collections.emptyList();
}
List<Long> ids = intersect.stream().map(obj -> (Long)obj).collect(Collectors.toList());
List<UserDTO> userDTOS = userMapper.selectBatchIds(ids).stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.toList();
return userDTOS;
}
@Override
public ScrollPageResponse<PostBasicInfoDTO> queryPostFollow(PostPageQueryDTO postPageQueryDTO) {
Long userId = UserContext.getUserId();
String key = "post:follow_of:" + userId;
return redisUtil.scrollPageQuery(key, PostBasicInfoDTO.class, postPageQueryDTO,
(postIds) -> {
List<Post> posts = postMapper.selectBatchIds(postIds);
List<Long> userIds = posts.stream().map(Post::getUserId).toList();
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
List<PostBasicInfoDTO> postBasicInfoDTOS = new ArrayList<>();
for(Post post : posts){
User user = userMap.getOrDefault(post.getUserId(), new User());
PostBasicInfoDTO postBasicInfoDTO = BeanUtil.copyProperties(post, PostBasicInfoDTO.class);
postBasicInfoDTO.setUserName(user.getUsername());
postBasicInfoDTO.setUserAvatar(user.getAvatar());
postBasicInfoDTOS.add(postBasicInfoDTO);
}
// 按照发布时间倒序排序
postBasicInfoDTOS.sort(Comparator.comparing(PostBasicInfoDTO::getCreateTime).reversed());
return postBasicInfoDTOS;
});
}
}

@ -1,27 +0,0 @@
package com.luojia_channel.modules.message.config;
import jakarta.servlet.ServletContext;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import org.springframework.web.util.WebAppRootListener;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements ServletContextInitializer {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Override
public void onStartup(ServletContext servletContext) {
servletContext.addListener(WebAppRootListener.class);
servletContext.setInitParameter("org.apache.tomcat.websocket.textBufferSize", (1024 * 200) + "");
servletContext.setInitParameter("org.apache.tomcat.websocket.binaryBufferSize", (1024 * 200) + "");
}
}

@ -1,47 +0,0 @@
package com.luojia_channel.modules.message.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "消息请求对象")
public class MessageRequest {
@Schema(
description = "消息类型0-私信1-系统通知",
allowableValues = {"0", "1"},
example = "0",
implementation = Integer.class
)
private Integer messageType; // 私信0系统通知1
@Schema(
description = "消息内容",
required = true,
minLength = 1,
maxLength = 500
)
private String content; // 消息内容
@Schema(
description = "接收者ID"
)
private Long receiverId; // 接收者ID
@Schema(
description = "发送者用户名",
required = true
)
private String senderName; // 用户名
@Schema(
description = "发送者头像",
required = true
)
private String senderAvatar; // 用户头像
}

@ -1,59 +0,0 @@
package com.luojia_channel.modules.message.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "消息返回对象")
public class MessageResponse {
@Schema(
description = "消息类型0-私信1-系统通知",
allowableValues = {"0", "1"},
example = "0",
implementation = Integer.class
)
private Integer messageType; // 私信0系统通知1
@Schema(
description = "消息内容",
required = true,
minLength = 1,
maxLength = 500
)
private String content; // 消息内容
@Schema(
description = "发送者ID"
)
private Long senderId;
@Schema(
description = "接收者ID"
)
private Long receiverId; // 接收者ID
@Schema(
description = "发送者用户名",
required = true
)
private String senderName; // 用户名
@Schema(
description = "发送者头像",
required = true
)
private String senderAvatar; // 用户头像
@Schema(
description = "消息创建时间"
)
private LocalDateTime createTime;
}

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

Loading…
Cancel
Save