Compare commits

..

No commits in common. "main" and "master" have entirely different histories.
main ... master

146 changed files with 10563 additions and 2 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### config ###
*.yml
### upload-dir ###
/public/chunk/
/public/img/cover/
/public/video/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 寻鹿
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
README.assets/0EF500CA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

312
database/teriteri.sql Normal file

File diff suppressed because one or more lines are too long

243
pom.xml Normal file
View File

@ -0,0 +1,243 @@
<?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>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- Generated by https://start.springboot.io -->
<!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn -->
<groupId>com.teriteri</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>backend</description>
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.17.16</elasticsearch.version>
</properties>
<dependencies>
<!--高性能数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.19</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.19</version>
</dependency>
<!--druid依赖log4j包-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--简化代码帮写什么get、setter函数-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
<!--省很多手写语句-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.5</version>
</dependency>
<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>
<!--JSON解析器-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.40</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--一些常用工具类的包装-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.13.0</version>
</dependency>
<!--文件上传相关-->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!--阿里云OSS对象存储-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.1</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
<!--配置注解处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.66.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.1.66.Final</version>
</dependency>
<!-- 加入ssl证书认证-->
<!-- <dependency>-->
<!-- <groupId>org.bouncycastle</groupId>-->
<!-- <artifactId>bcprov-jdk15on</artifactId>-->
<!-- <version>1.69</version> &lt;!&ndash; 使用最新版本 &ndash;&gt;-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.bouncycastle</groupId>-->
<!-- <artifactId>bcpkix-jdk15on</artifactId>-->
<!-- <version>1.69</version> &lt;!&ndash; 使用最新版本 &ndash;&gt;-->
<!-- </dependency>-->
<!--elasticsearch-->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>7.17.16</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.json</artifactId>
<version>2.0.1</version>
</dependency>
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-elasticsearch</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

1
readme.md Normal file
View File

@ -0,0 +1 @@
# 哈哈哈

View File

@ -0,0 +1,28 @@
package com.teriteri.backend;
import com.teriteri.backend.im.IMServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
// Generated by https://start.springboot.io
// 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn
@SpringBootApplication
@EnableScheduling // 启用定时任务
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
new Thread(() -> {
try {
new IMServer().start();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}

View File

@ -0,0 +1,168 @@
package com.teriteri.backend.component.danmu;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.teriteri.backend.mapper.DanmuMapper;
import com.teriteri.backend.mapper.VideoStatsMapper;
import com.teriteri.backend.pojo.Danmu;
import com.teriteri.backend.pojo.User;
import com.teriteri.backend.pojo.VideoStats;
import com.teriteri.backend.service.video.VideoStatsService;
import com.teriteri.backend.utils.JwtUtil;
import com.teriteri.backend.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Slf4j
@Component
@ServerEndpoint(value = "/ws/danmu/{vid}")
public class DanmuWebSocketServer {
// 由于每个连接都不是共享一个WebSocketServer所以要静态注入
private static JwtUtil jwtUtil;
private static RedisUtil redisUtil;
private static DanmuMapper danmuMapper;
private static VideoStatsService videoStatsService;
@Autowired
public void setDependencies(JwtUtil jwtUtil, RedisUtil redisUtil, DanmuMapper danmuMapper, VideoStatsService videoStatsService) {
DanmuWebSocketServer.jwtUtil = jwtUtil;
DanmuWebSocketServer.redisUtil = redisUtil;
DanmuWebSocketServer.danmuMapper = danmuMapper;
DanmuWebSocketServer.videoStatsService = videoStatsService;
}
// 对每个视频存储该视频下的session集合
private static final Map<String, Set<Session>> videoConnectionMap = new ConcurrentHashMap<>();
/**
* 连接建立时触发记录session到map
* @param session 会话
* @param vid 视频的ID
*/
@OnOpen
public void onOpen(Session session, @PathParam("vid") String vid) {
if (videoConnectionMap.get(vid) == null) {
Set<Session> set = new HashSet<>();
set.add(session);
videoConnectionMap.put(vid, set);
} else {
videoConnectionMap.get(vid).add(session);
}
sendMessage(vid, "当前观看人数" + videoConnectionMap.get(vid).size());
// System.out.println("建立连接,当前观看人数: " + videoConnectionMap.get(vid).size());
}
/**
* 收到消息时触发记录到数据库并转发到对应的全部连接
* @param session 当前会话
* @param message 信息体包含"token""vid""data"字段
* @param vid 视频ID
*/
@OnMessage
public void onMessage(Session session, String message, @PathParam("vid") String vid) {
try {
JSONObject msg = JSON.parseObject(message);
// token鉴权
String token = msg.getString("token");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
session.getBasicRemote().sendText("登录已过期");
return;
}
token = token.substring(7);
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
session.getBasicRemote().sendText("登录已过期");
return;
}
String userId = JwtUtil.getSubjectFromToken(token);
String role = JwtUtil.getClaimFromToken(token, "role");
User user = redisUtil.getObject("security:" + role + ":" + userId, User.class);
if (user == null) {
session.getBasicRemote().sendText("登录已过期");
return;
}
// 写库
JSONObject data = msg.getJSONObject("data");
// System.out.println(data);
Danmu danmu = new Danmu(
null,
Integer.parseInt(vid),
user.getUid(),
data.getString("content"),
data.getInteger("fontsize"),
data.getInteger("mode"),
data.getString("color"),
data.getDouble("timePoint"),
1,
new Date()
);
danmuMapper.insert(danmu);
videoStatsService.updateStats(Integer.parseInt(vid), "danmu", true, 1);
redisUtil.addMember("danmu_idset:" + vid, danmu.getId()); // 加入对应视频的ID集合以便查询
// 广播弹幕
String dmJson = JSON.toJSONString(danmu);
sendMessage(vid, dmJson);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接关闭时执行
* @param session 当前会话
* @param vid 视频ID
*/
@OnClose
public void onClose(Session session, @PathParam("vid") String vid) {
// 从缓存中移除连接记录
videoConnectionMap.get(vid).remove(session);
if (videoConnectionMap.get(vid).size() == 0) {
// 如果没人了就直接移除这个视频
videoConnectionMap.remove(vid);
} else {
// 否则更新在线人数
sendMessage(vid, "当前观看人数" + videoConnectionMap.get(vid).size());
}
// System.out.println("关闭连接,当前观看人数: " + videoConnectionMap.get(vid).size());
}
@OnError
public void onError(Throwable error) {
log.error("websocket发生错误");
error.printStackTrace();
}
/**
* 往对应的全部连接发送消息
* @param vid 视频ID
* @param text 消息内容对象需转成JSON字符串
*/
public void sendMessage(String vid, String text) {
Set<Session> set = videoConnectionMap.get(vid);
// 使用并行流往各客户端发送数据
set.parallelStream().forEach(session -> {
try {
session.getBasicRemote().sendText(text);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}

View File

@ -0,0 +1,43 @@
package com.teriteri.backend.config;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class CorsConfig implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
String origin = request.getHeader("Origin");
if(origin!=null) {
response.setHeader("Access-Control-Allow-Origin", origin);
}
String headers = request.getHeader("Access-Control-Request-Headers");
if(headers!=null) {
response.setHeader("Access-Control-Allow-Headers", headers);
response.setHeader("Access-Control-Expose-Headers", headers);
}
response.setHeader("Access-Control-Allow-Methods", "*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}

View File

@ -0,0 +1,85 @@
package com.teriteri.backend.config;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.ResourceServlet;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
@Configuration
public class DruidConfig {
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druid(){
return new DruidDataSource();
}
/*
配置一个druid的监控
1配置一个druid的后台 管理servlet
2配置一个druid的filter
*/
// 配置 Druid 监控管理后台的Servlet
// 内置 Servlet 容器时没有web.xml文件所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean servletRegistrationBean(){
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
/*
这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
*/
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", username); //后台管理界面的登录账号
initParams.put("loginPassword", password); //后台管理界面的登录密码
//后台允许谁可以访问
//initParams.put("allow", "localhost")表示只有本机可以访问
//initParams.put("allow", "")为空或者为null时表示允许所有访问
initParams.put("allow", "");
//denyDruid 后台拒绝谁访问
//initParams.put("listen", "192.168.1.20");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
// 配置 Druid 监控 web 监控的 filter
// WebStatFilter用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions设置哪些请求进行过滤排除掉从而不进行统计
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
bean.setInitParameters(initParams);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}

View File

@ -0,0 +1,61 @@
package com.teriteri.backend.config;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ElasticSearchConfig {
@Value("${elasticsearch.host}")
private String host;
@Value("${elasticsearch.port}")
private Integer port;
@Value("${elasticsearch.username}")
private String username;
@Value("${elasticsearch.password}")
private String password;
@Bean(destroyMethod = "close")
public RestClient restClient() {
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
return RestClient
.builder(new HttpHost(host, port, "http"))
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider) // 身份验证
.setMaxConnTotal(100) // 最大总连接数
.setMaxConnPerRoute(50) // 每个路由的最大连接数
.setKeepAliveStrategy(((response, context) -> Duration.ofMinutes(5).toMillis())) // httpclient保活策略 5分钟不连接就关闭
.setDefaultIOReactorConfig(IOReactorConfig.custom().setSoKeepAlive(true).build()) // 开启tcp keepalive
)
.build();
}
@Bean(destroyMethod = "close")
public ElasticsearchTransport transport() {
return new RestClientTransport(restClient(), new JacksonJsonpMapper());
}
@Bean
public ElasticsearchClient elasticsearchClient() {
return new ElasticsearchClient(transport());
}
}

View File

@ -0,0 +1,24 @@
package com.teriteri.backend.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import java.io.IOException;
@Configuration
@ConfigurationProperties(prefix = "file")
public class FileUploadConfig {
@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
multipartResolver.setDefaultEncoding("UTF-8");
multipartResolver.setMaxUploadSizePerFile(30 * 1024 * 1024); // 设置文件上传大小限制
multipartResolver.setResolveLazily(true);
return multipartResolver;
}
}

View File

@ -0,0 +1,32 @@
package com.teriteri.backend.config;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OSSConfig {
@Value("${oss.endpoint}")
private String OSS_ENDPOINT;
@Value("${oss.keyId}")
private String ACCESS_KEY_ID;
@Value("${oss.keySecret}")
private String ACCESS_KEY_SECRET;
@Value("${oss.idleTimeout}")
private long IDLE_TIMEOUT;
@Bean(destroyMethod = "shutdown")
public OSS ossClient() {
ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
//连接空闲超时时间超时则关闭
conf.setIdleConnectionTime(IDLE_TIMEOUT);
// 创建OSSClient实例
return new OSSClientBuilder().build(OSS_ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET, conf);
}
}

View File

@ -0,0 +1,40 @@
package com.teriteri.backend.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 暂时用不到先注了
*/
@Configuration
public class RabbitMQConfig {
// // 1.声明 direct 模式的交换机
// /**
// *投稿相关的交换机
// */
// @Bean
// public DirectExchange directUploadExchange() {
// return new DirectExchange("direct_upload_exchange", true, false);
// }
//
// // 2.声明队列
// /**
// * 视频投稿信息队列
// */
// @Bean
// public Queue videoUploadQueue() {
// return new Queue("videoUpload_direct_queue", true);
// }
//
// // 3.队列和交换机完成绑定关系
// @Bean
// public Binding videoUploadBinding() {
// return BindingBuilder.bind(videoUploadQueue()).to(directUploadExchange()).with("videoUpload");
// }
}

View File

@ -0,0 +1,137 @@
package com.teriteri.backend.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.lang.reflect.Method;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
/**
* 创建并配置一个 Jedis 连接池
* @return
*/
@Bean
@SuppressWarnings("all")
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
return new JedisPool(jedisPoolConfig, host, port, timeout);
}
/**
* 缓存键生成器在这个项目中用不到我更倾向自己手动命名 user:1:age
* @return
*/
@Bean
@SuppressWarnings("all")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* Redis 缓存管理器
* @param connectionFactory
* @return
*/
@Bean
@SuppressWarnings("all")
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory).build();
return redisCacheManager;
}
/**
* 编写自己的 redisTemplate用于与 Redis 进行交互配置了json格式存储序列化与反序列化
* @param redisConnectionFactory
* @return
*/
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 为了方便自己开发一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
// 序列化配置
// json的序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // 用json序列化任意对象类
ObjectMapper om = new ObjectMapper(); // 对象类序列化过程中用 ObjectMapper 进行转义
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用了String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
/**
* 一个专门用于操作 Redis 字符串类型的模板它是 RedisTemplate 的子类只支持字符串数据的存储和检索
* @param factory
* @return
*/
@Bean
@SuppressWarnings("all")
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(factory);
return stringRedisTemplate;
}
}

View File

@ -0,0 +1,128 @@
package com.teriteri.backend.config;
import com.teriteri.backend.config.filter.JwtAuthenticationTokenFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
import java.util.Objects;
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Resource
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 密码BCrypt加密
* @return BCrypt加密后的密码
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 用户名和密码验证
* @return Authentication对象
*/
@Bean
public AuthenticationProvider authenticationProvider() {
return new AuthenticationProvider() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 从Authentication对象中获取用户名和身份凭证信息
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails loginUser = userDetailsService.loadUserByUsername(username);
if (Objects.isNull(loginUser) || !passwordEncoder().matches(password, loginUser.getPassword())) {
// 密码匹配失败抛出异常
throw new BadCredentialsException("访问拒绝:用户名或密码错误!");
}
// log.info("访问成功:" + loginUser);
return new UsernamePasswordAuthenticationToken(loginUser, password, loginUser.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
};
}
/**
* 请求接口过滤器验证是否开放接口如果不是开放接口请求头又没带 Authorization 属性会被直接拦截
* @param http
* @return
* @throws Exception
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 基于 token不需要 csrf
.csrf().disable()
// 基于 token不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 下面开始设置权限
.authorizeRequests(authorize -> authorize
// 放开Swagger路径
.antMatchers("/doc.html", "/webjars/**").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs/**").permitAll()
// 请求放开接口
.antMatchers("/druid/**","/favicon.ico",
"/user/account/register",
"/user/account/login",
"/admin/account/login",
"/category/getall",
"/video/random/visitor",
"/video/cumulative/visitor",
"/video/getone",
"/ws/danmu/**",
"/danmu-list/**",
"/msg/chat/outline",
"/video/play/visitor",
"/favorite/get-all/visitor",
"/search/**",
"/comment/get",
"/comment/reply/get-more",
"/comment/get-up-like",
"/user/info/get-one",
"/video/user-works-count",
"/video/user-works",
"/video/user-love",
"/video/user-collect").permitAll()
// 允许HTTP OPTIONS请求
.antMatchers(HttpMethod.OPTIONS).permitAll()
// 其他地址的访问均需验证权限
.anyRequest().authenticated()
)
// 添加 JWT 过滤器JWT 过滤器在用户名密码认证过滤器之前
.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}

View File

@ -0,0 +1,34 @@
package com.teriteri.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync // 开启异步
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//设置核心线程数
executor.setCorePoolSize(20);
//设置最大线程数
executor.setMaxPoolSize(100);
//配置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
//设置线程活跃时间
executor.setKeepAliveSeconds(60);
//设置默认线程名称
executor.setThreadNamePrefix("teriteri");
//等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,54 @@
package com.teriteri.backend.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
/**
* @author: 顾挽
* @description:
* @create: 2024-04-28 16:26
**/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("仿BiliBili后端接口文档")
.version("1.0")
.description("BiliBili后端接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.teriteri.backend.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}

View File

@ -0,0 +1,19 @@
package com.teriteri.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
// 注入一个ServerEndpointExporter该Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -0,0 +1,85 @@
package com.teriteri.backend.config.filter;
import com.teriteri.backend.pojo.User;
import com.teriteri.backend.service.impl.user.UserDetailsImpl;
import com.teriteri.backend.utils.JwtUtil;
import com.teriteri.backend.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.security.sasl.AuthenticationException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisUtil redisUtil;
/**
* token 认证过滤器任何请求访问服务器都会先被这里拦截验证token合法性
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
// 通过开放接口过滤器后如果没有可解析的token就放行
filterChain.doFilter(request, response);
return;
}
token = token.substring(7);
// 解析token
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
// log.error("当前token已过期");
response.addHeader("message", "not login"); // 设置响应头信息给前端判断用
response.setStatus(403);
// throw new AuthenticationException("当前token已过期");
return;
}
String userId = JwtUtil.getSubjectFromToken(token);
String role = JwtUtil.getClaimFromToken(token, "role");
// 从redis中获取用户信息
User user = redisUtil.getObject("security:" + role + ":" + userId, User.class);
if (user == null) {
// log.error("用户未登录");
response.addHeader("message", "not login"); // 设置响应头信息给前端判断用
response.setStatus(403);
// throw new AuthenticationException("用户未登录");
return;
}
// 存入SecurityContextHolder这里建议只供读取uid用其中的状态等非静态数据可能不准所以建议redis另外存值
UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 放行
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,22 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.category.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 获取全部分区接口
* @return CustomResponse对象
*/
@GetMapping("/category/getall")
public CustomResponse getAll() {
return categoryService.getAll();
}
}

View File

@ -0,0 +1,97 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.message.ChatService;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@RestController
public class ChatController {
@Autowired
private ChatService chatService;
@Autowired
private CurrentUser currentUser;
@Autowired
private RedisUtil redisUtil;
/**
* 新建一个聊天与其他用户首次聊天时调用
* @param uid 对方用户ID
* @return CustomResponse对象 message可能值"新创建"/"已存在"/"未知用户"
*/
@GetMapping("/msg/chat/create/{uid}")
public CustomResponse createChat(@PathVariable("uid") Integer uid) {
CustomResponse customResponse = new CustomResponse();
Map<String, Object> result = chatService.createChat(uid, currentUser.getUserId());
if (Objects.equals(result.get("msg").toString(), "新创建")) {
customResponse.setData(result); // 返回新创建的聊天
} else if (Objects.equals(result.get("msg").toString(), "未知用户")) {
customResponse.setCode(404);
}
customResponse.setMessage(result.get("msg").toString());
return customResponse;
}
/**
* 获取用户最近的聊天列表
* @param offset 分页偏移量前端查询了多少个聊天
* @return CustomResponse对象 包含带用户信息和最近一条消息的聊天列表以及是否还有更多数据
*/
@GetMapping("/msg/chat/recent-list")
public CustomResponse getRecentList(@RequestParam("offset") Long offset) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
Map<String, Object> map = new HashMap<>();
map.put("list", chatService.getChatListWithData(uid, offset));
// 检查是否还有更多
if (offset + 10 < redisUtil.zCard("chat_zset:" + uid)) {
map.put("more", true);
} else {
map.put("more", false);
}
customResponse.setData(map);
return customResponse;
}
/**
* 移除聊天
* @param uid 对方用户ID
* @return CustomResponse对象
*/
@GetMapping("/msg/chat/delete/{uid}")
public CustomResponse deleteChat(@PathVariable("uid") Integer uid) {
CustomResponse customResponse = new CustomResponse();
chatService.delChat(uid, currentUser.getUserId());
return customResponse;
}
/**
* 切换窗口时 更新在线状态以及清除未读
* @param from 对方UID
*/
@GetMapping("/msg/chat/online")
public void updateWhisperOnline(@RequestParam("from") Integer from) {
Integer uid = currentUser.getUserId();
chatService.updateWhisperOnline(from, uid);
}
/**
* 切换窗口时 更新为离开状态 该接口要放开无需验证token防止token过期导致用户一直在线
* @param from 对方UID
*/
@GetMapping("/msg/chat/outline")
public void updateWhisperOutline(@RequestParam("from") Integer from, @RequestParam("to") Integer to) {
chatService.updateWhisperOutline(from, to);
}
}

View File

@ -0,0 +1,50 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.message.ChatDetailedService;
import com.teriteri.backend.service.utils.CurrentUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatDetailedController {
@Autowired
private ChatDetailedService chatDetailedService;
@Autowired
private CurrentUser currentUser;
/**
* 获取更多历史消息记录
* @param uid 聊天对象的UID
* @param offset 偏移量即已经获取过的消息数量从哪条开始获取更多
* @return CustomResponse对象包含更多消息记录的map
*/
@GetMapping("/msg/chat-detailed/get-more")
public CustomResponse getMoreChatDetails(@RequestParam("uid") Integer uid,
@RequestParam("offset") Long offset) {
Integer loginUid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
customResponse.setData(chatDetailedService.getDetails(uid, loginUid, offset));
return customResponse;
}
/**
* 删除消息
* @param id 消息ID
* @return CustomResponse对象
*/
@PostMapping("/msg/chat-detailed/delete")
public CustomResponse delDetail(@RequestParam("id") Integer id) {
Integer loginUid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
if (!chatDetailedService.deleteDetail(id, loginUid)) {
customResponse.setCode(500);
customResponse.setMessage("删除消息失败");
}
return customResponse;
}
}

View File

@ -0,0 +1,101 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CommentTree;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.comment.CommentService;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private CurrentUser currentUser;
@Autowired
private RedisUtil redisUtil;
/**
* 获取评论树列表每次查十条
* @param vid 对应视频ID
* @param offset 分页偏移量已经获取到的评论树的数量
* @param type 排序类型 1 按热度排序 2 按时间排序
* @return 评论树列表
*/
@GetMapping("/comment/get")
public CustomResponse getCommentTreeByVid(@RequestParam("vid") Integer vid,
@RequestParam("offset") Long offset,
@RequestParam("type") Integer type) {
CustomResponse customResponse = new CustomResponse();
long count = redisUtil.zCard("comment_video:" + vid);
Map<String, Object> map = new HashMap<>();
if (offset >= count) {
// 表示前端已经获取到全部根评论了没必要继续
map.put("more", false);
map.put("comments", Collections.emptyList());
} else if (offset + 10 >= count){
// 表示这次查询会查完全部根评论
map.put("more", false);
map.put("comments", commentService.getCommentTreeByVid(vid, offset, type));
} else {
// 表示这次查的只是冰山一角还有很多评论没查到
map.put("more", true);
map.put("comments", commentService.getCommentTreeByVid(vid, offset, type));
}
customResponse.setData(map);
return customResponse;
}
/**
* 展开更多回复评论
* @param id 根评论id
* @return 完整的一棵包含全部评论的评论树
*/
@GetMapping("/comment/reply/get-more")
public CommentTree getMoreCommentById(@RequestParam("id") Integer id) {
return commentService.getMoreCommentsById(id);
}
/**
* 发表评论
* @param vid 视频id
* @param rootId 根评论id
* @param parentId 被回复评论id
* @param toUserId 被回复者uid
* @param content 评论内容
* @return 响应对象
*/
@PostMapping("/comment/add")
public CustomResponse addComment(
@RequestParam("vid") Integer vid,
@RequestParam("root_id") Integer rootId,
@RequestParam("parent_id") Integer parentId,
@RequestParam("to_user_id") Integer toUserId,
@RequestParam("content") String content ) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
CommentTree commentTree = commentService.sendComment(vid, uid, rootId, parentId, toUserId, content);
if (commentTree == null) {
customResponse.setCode(500);
customResponse.setMessage("发送失败!");
}
customResponse.setData(commentTree);
return customResponse;
}
/**
* 删除评论
* @param id 评论id
* @return 响应对象
*/
@PostMapping("/comment/delete")
public CustomResponse delComment(@RequestParam("id") Integer id) {
Integer loginUid = currentUser.getUserId();
return commentService.deleteComment(id, loginUid, currentUser.isAdmin());
}
}

View File

@ -0,0 +1,49 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.pojo.Danmu;
import com.teriteri.backend.service.danmu.DanmuService;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
@RestController
public class DanmuController {
@Autowired
private DanmuService danmuService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private CurrentUser currentUser;
/**
* 获取弹幕列表
* @param vid 视频ID
* @return CustomResponse对象
*/
@GetMapping("/danmu-list/{vid}")
public CustomResponse getDanmuList(@PathVariable("vid") String vid) {
Set<Object> idset = redisUtil.getMembers("danmu_idset:" + vid);
List<Danmu> danmuList = danmuService.getDanmuListByIdset(idset);
CustomResponse customResponse = new CustomResponse();
customResponse.setData(danmuList);
return customResponse;
}
/**
* 删除弹幕
* @param id 弹幕id
* @return 响应对象
*/
@PostMapping("/danmu/delete")
public CustomResponse deleteDanmu(@RequestParam("id") Integer id) {
Integer loginUid = currentUser.getUserId();
return danmuService.deleteDanmu(id, loginUid, currentUser.isAdmin());
}
}

View File

@ -0,0 +1,67 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.service.video.FavoriteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class FavoriteController {
@Autowired
private FavoriteService favoriteService;
@Autowired
private CurrentUser currentUser;
/**
* 站内用户请求某个用户的收藏夹列表需要jwt鉴权
* @param uid 被查看的用户ID
* @return 包含收藏夹列表的响应对象
*/
@GetMapping("/favorite/get-all/user")
public CustomResponse getAllFavoritiesForUser(@RequestParam("uid") Integer uid) {
Integer loginUid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
if (Objects.equals(loginUid, uid)) {
customResponse.setData(favoriteService.getFavorites(uid, true));
} else {
customResponse.setData(favoriteService.getFavorites(uid, false));
}
return customResponse;
}
/**
* 游客请求某个用户的收藏夹列表不需要jwt鉴权
* @param uid 被查看的用户ID
* @return 包含收藏夹列表的响应对象
*/
@GetMapping("/favorite/get-all/visitor")
public CustomResponse getAllFavoritiesForVisitor(@RequestParam("uid") Integer uid) {
CustomResponse customResponse = new CustomResponse();
customResponse.setData(favoriteService.getFavorites(uid, false));
return customResponse;
}
/**
* 创建一个新的收藏夹
* @param title 标题 限80字需前端做合法判断
* @param desc 简介 限200字需前端做合法判断
* @param visible 是否公开 0否 1是
* @return 包含新创建的收藏夹信息的响应对象
*/
@PostMapping("/favorite/create")
public CustomResponse createFavorite(@RequestParam("title") String title,
@RequestParam("desc") String desc,
@RequestParam("visible") Integer visible) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
customResponse.setData(favoriteService.addFavorite(uid, title, desc, visible));
return customResponse;
}
}

View File

@ -0,0 +1,129 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.pojo.Favorite;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.service.video.FavoriteService;
import com.teriteri.backend.service.video.FavoriteVideoService;
import com.teriteri.backend.service.video.UserVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
public class FavoriteVideoController {
@Autowired
private CurrentUser currentUser;
@Autowired
private FavoriteService favoriteService;
@Autowired
private FavoriteVideoService favoriteVideoService;
@Autowired
private UserVideoService userVideoService;
/**
* 获取用户收藏了该视频的收藏夹列表
* @param vid 视频id
* @return 收藏了该视频的收藏夹列表
*/
@GetMapping("/video/collected-fids")
public CustomResponse getCollectedFids(@RequestParam("vid") Integer vid) {
Integer uid = currentUser.getUserId();
Set<Integer> fids = findFidsOfUserFavorites(uid);
Set<Integer> collectedFids = favoriteVideoService.findFidsOfCollected(vid, fids);
CustomResponse customResponse = new CustomResponse();
customResponse.setData(collectedFids);
return customResponse;
}
/**
* 收藏或取消收藏某视频
* @param vid 视频ID
* @param addArray 包含需要添加收藏的多个收藏夹ID组成的字符串形式如 1,12,13,20 不能含有字符"[""]"
* @param removeArray 包含需要移出收藏的多个收藏夹ID组成的字符串形式如 1,12,13,20 不能含有字符"[""]"
* @return 无数据返回
*/
@PostMapping("/video/collect")
public CustomResponse collectVideo(@RequestParam("vid") Integer vid,
@RequestParam("adds") String[] addArray,
@RequestParam("removes") String[] removeArray) {
CustomResponse customResponse = new CustomResponse();
Integer uid = currentUser.getUserId();
Set<Integer> fids = findFidsOfUserFavorites(uid);
Set<Integer> addSet = Arrays.stream(addArray).map(Integer::parseInt).collect(Collectors.toSet());
Set<Integer> removeSet = Arrays.stream(removeArray).map(Integer::parseInt).collect(Collectors.toSet());
boolean allElementsInFids = fids.containsAll(addSet) && fids.containsAll(removeSet); // 判断添加或移出的收藏夹是否都属于该用户
if (!allElementsInFids) {
customResponse.setCode(403);
customResponse.setMessage("无权操作该收藏夹");
return customResponse;
}
Set<Integer> collectedFids = favoriteVideoService.findFidsOfCollected(vid, fids); // 原本该用户已收藏该视频的收藏夹ID集合
if (addSet.size() > 0) {
favoriteVideoService.addToFav(uid, vid, addSet);
}
if (removeSet.size() > 0) {
favoriteVideoService.removeFromFav(uid, vid, removeSet);
}
boolean isCollect = addSet.size() > 0 && collectedFids.size() == 0;
boolean isCancel = addSet.size() == 0 && collectedFids.size() > 0 && collectedFids.size() == removeSet.size() && collectedFids.containsAll(removeSet);
if (isCollect) {
userVideoService.collectOrCancel(uid, vid, true);
} else if (isCancel) {
userVideoService.collectOrCancel(uid, vid, false);
}
return customResponse;
}
/**
* 取消单个视频在单个收藏夹的收藏
* @param vid 视频vid
* @param fid 收藏夹id
* @return 响应对象
*/
@PostMapping("/video/cancel-collect")
public CustomResponse cancelCollect(@RequestParam("vid") Integer vid, @RequestParam("fid") Integer fid) {
CustomResponse customResponse = new CustomResponse();
Integer uid = currentUser.getUserId();
Set<Integer> fids = findFidsOfUserFavorites(uid);
Set<Integer> removeSet = new HashSet<>();
removeSet.add(fid);
if (!fids.containsAll(removeSet)) {
customResponse.setCode(403);
customResponse.setMessage("无权操作该收藏夹");
return customResponse;
}
Set<Integer> collectedFids = favoriteVideoService.findFidsOfCollected(vid, fids); // 原本该用户已收藏该视频的收藏夹ID集合
favoriteVideoService.removeFromFav(uid, vid, removeSet);
// 判断是否是最后一个取消收藏的收藏夹是就要标记视频为未收藏
boolean isCancel = collectedFids.size() > 0 && collectedFids.size() == removeSet.size() && collectedFids.containsAll(removeSet);
if (isCancel) {
userVideoService.collectOrCancel(uid, vid, false);
}
return customResponse;
}
/**
* 提取某用户的全部收藏夹信息的FID整合成集合
* @param uid 用户ID
* @return fid集合
*/
private Set<Integer> findFidsOfUserFavorites(Integer uid) {
List<Favorite> list = favoriteService.getFavorites(uid, true);
if (list == null) return new HashSet<>();
return list.stream()
.map(Favorite::getFid)
.collect(Collectors.toSet());
}
}

View File

@ -0,0 +1,41 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.message.MsgUnreadService;
import com.teriteri.backend.service.utils.CurrentUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MsgUnreadController {
@Autowired
private MsgUnreadService msgUnreadService;
@Autowired
private CurrentUser currentUser;
/**
* 获取当前用户全部消息未读数
* @return
*/
@GetMapping("/msg-unread/all")
public CustomResponse getMsgUnread() {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
customResponse.setData(msgUnreadService.getUnread(uid));
return customResponse;
}
/**
* 清除某一列的未读消息提示
* @param column msg_unread表列名 "reply"/"at"/"love"/"system"/"whisper"/"dynamic"
*/
@PostMapping("/msg-unread/clear")
public void clearUnread(@RequestParam("column") String column) {
Integer uid = currentUser.getUserId();
msgUnreadService.clearUnread(uid, column);
}
}

View File

@ -0,0 +1,110 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.search.SearchService;
import com.teriteri.backend.service.user.UserService;
import com.teriteri.backend.service.video.VideoService;
import com.teriteri.backend.utils.ESUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.List;
@RestController
public class SearchController {
@Autowired
private SearchService searchService;
@Autowired
private ESUtil esUtil;
@Autowired
private VideoService videoService;
@Autowired
private UserService userService;
/**
* 获取热搜词条
* @return 热搜列表
*/
@GetMapping("/search/hot/get")
public CustomResponse getHotSearch() {
CustomResponse customResponse = new CustomResponse();
customResponse.setData(searchService.getHotSearch());
return customResponse;
}
/**
* 添加搜索词或者给该搜索词热度加一
* @param keyword 搜索词
* @return 返回格式化后的搜索词有可能为null
*/
@PostMapping("/search/word/add")
public CustomResponse addSearchWord(@RequestParam("keyword") String keyword) {
CustomResponse customResponse = new CustomResponse();
customResponse.setData(searchService.addSearchWord(keyword));
return customResponse;
}
/**
* 根据输入内容获取相关搜索推荐词
* @param keyword 关键词
* @return 包含推荐搜索词的列表
*/
@GetMapping("/search/word/get")
public CustomResponse getSearchWord(@RequestParam("keyword") String keyword) throws UnsupportedEncodingException {
keyword = URLDecoder.decode(keyword, "UTF-8"); // 解码经过url传输的字符串
CustomResponse customResponse = new CustomResponse();
if (keyword.trim().length() == 0) {
customResponse.setData(Collections.emptyList());
} else {
customResponse.setData(searchService.getMatchingWord(keyword));
}
return customResponse;
}
/**
* 获取各种类型相关数据数量 视频&用户
* @param keyword 关键词
* @return 包含视频数量和用户数量的顺序列表
*/
@GetMapping("/search/count")
public CustomResponse getCount(@RequestParam("keyword") String keyword) throws UnsupportedEncodingException {
keyword = URLDecoder.decode(keyword, "UTF-8"); // 解码经过url传输的字符串
CustomResponse customResponse = new CustomResponse();
customResponse.setData(searchService.getCount(keyword));
return customResponse;
}
/**
* 搜索相关已过审视频
* @param keyword 关键词
* @param page 第几页
* @return 视频列表
* @throws UnsupportedEncodingException
*/
@GetMapping("/search/video/only-pass")
public CustomResponse getMatchingVideo(@RequestParam("keyword") String keyword, @RequestParam("page") Integer page) throws UnsupportedEncodingException {
keyword = URLDecoder.decode(keyword, "UTF-8"); // 解码经过url传输的字符串
CustomResponse customResponse = new CustomResponse();
List<Integer> vids = esUtil.searchVideosByKeyword(keyword, page, 30, true);
customResponse.setData(videoService.getVideosWithDataByIdList(vids));
return customResponse;
}
@GetMapping("/search/user")
public CustomResponse getMatchingUser(@RequestParam("keyword") String keyword, @RequestParam("page") Integer page) throws UnsupportedEncodingException {
keyword = URLDecoder.decode(keyword, "UTF-8"); // 解码经过url传输的字符串
CustomResponse customResponse = new CustomResponse();
List<Integer> uids = esUtil.searchUsersByKeyword(keyword, page, 30);
customResponse.setData(userService.getUserByIdList(uids));
return customResponse;
}
}

View File

@ -0,0 +1,106 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.user.UserAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class UserAccountController {
@Autowired
private UserAccountService userAccountService;
/**
* 注册接口
* @param map 包含 username password confirmedPassword map
* @return CustomResponse对象
*/
// 前端使用axios传递的data是Content-Type: application.yml/json需要用@RequestBody获取参数
@PostMapping("/user/account/register")
public CustomResponse register(@RequestBody Map<String, String> map) {
String username = map.get("username");
String password = map.get("password");
String confirmedPassword = map.get("confirmedPassword");
try {
return userAccountService.register(username, password, confirmedPassword);
} catch (Exception e) {
e.printStackTrace();
CustomResponse customResponse = new CustomResponse();
customResponse.setCode(500);
customResponse.setMessage("特丽丽被玩坏了");
return customResponse;
}
}
/**
* 登录接口
* @param map 包含 username password map
* @return CustomResponse对象
*/
@PostMapping("/user/account/login")
public CustomResponse login(@RequestBody Map<String, String> map) {
String username = map.get("username");
String password = map.get("password");
return userAccountService.login(username, password);
}
/**
* 管理员登录接口
* @param map 包含 username password map
* @return CustomResponse对象
*/
@PostMapping("/admin/account/login")
public CustomResponse adminLogin(@RequestBody Map<String, String> map) {
String username = map.get("username");
String password = map.get("password");
return userAccountService.adminLogin(username, password);
}
/**
* 获取当前登录用户信息接口
* @return CustomResponse对象
*/
@GetMapping("/user/personal/info")
public CustomResponse personalInfo() {
return userAccountService.personalInfo();
}
/**
* 获取当前登录管理员信息接口
* @return CustomResponse对象
*/
@GetMapping("/admin/personal/info")
public CustomResponse adminPersonalInfo() {
return userAccountService.adminPersonalInfo();
}
/**
* 退出登录接口
*/
@GetMapping("/user/account/logout")
public void logout() {
userAccountService.logout();
}
/**
* 管理员退出登录接口
*/
@GetMapping("/admin/account/logout")
public void adminLogout() {
userAccountService.adminLogout();
}
/**
* 修改当前用户密码
* @param pw 就密码
* @param npw 新密码
* @return 响应对象
*/
@PostMapping("/user/password/update")
public CustomResponse updatePassword(@RequestParam("pw") String pw, @RequestParam("npw") String npw) {
return userAccountService.updatePassword(pw, npw);
}
}

View File

@ -0,0 +1,65 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.comment.UserCommentService;
import com.teriteri.backend.service.utils.CurrentUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Slf4j
@RestController
public class UserCommentController {
@Autowired
private CurrentUser currentUser;
@Autowired
private UserCommentService userCommentService;
/**
* 获取用户点赞点踩评论集合
*/
@GetMapping("/comment/get-like-and-dislike")
public CustomResponse getLikeAndDislike() {
Integer uid = currentUser.getUserId();
CustomResponse response = new CustomResponse();
response.setCode(200);
response.setData(userCommentService.getUserLikeAndDislike(uid));
return response;
}
/**
* 点赞或点踩某条评论
* @param id 评论id
* @param isLike true false
* @param isSet true false 取消
*/
@PostMapping("/comment/love-or-not")
public CustomResponse loveOrNot(@RequestParam("id") Integer id,
@RequestParam("isLike") boolean isLike,
@RequestParam("isSet") boolean isSet) {
Integer uid = currentUser.getUserId();
userCommentService.userSetLikeOrUnlike(uid, id, isLike, isSet);
return new CustomResponse();
}
/**
* 获取UP主觉得很淦的评论
* @param uid UP主uid
* @return 点赞的评论id列表
*/
@GetMapping("/comment/get-up-like")
public CustomResponse getUpLike(@RequestParam("uid") Integer uid) {
CustomResponse customResponse = new CustomResponse();
Map<String, Object> map = userCommentService.getUserLikeAndDislike(uid);
customResponse.setData(map.get("userLike"));
return customResponse;
}
}

View File

@ -0,0 +1,66 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.user.UserService;
import com.teriteri.backend.service.utils.CurrentUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private CurrentUser currentUser;
/**
* 更新用户部分个人信息
* @param nickname 昵称
* @param desc 个性签名
* @param gender 性别0 1 2 保密
* @return
*/
@PostMapping("/user/info/update")
public CustomResponse updateUserInfo(@RequestParam("nickname") String nickname,
@RequestParam("description") String desc,
@RequestParam("gender") Integer gender) {
Integer uid = currentUser.getUserId();
try {
return userService.updateUserInfo(uid, nickname, desc, gender);
} catch (Exception e) {
e.printStackTrace();
CustomResponse customResponse = new CustomResponse();
customResponse.setCode(500);
customResponse.setMessage("特丽丽被玩坏了");
return customResponse;
}
}
/**
* 更新用户头像
* @param file 头像文件
* @return 成功则返回新头像url
*/
@PostMapping("/user/avatar/update")
public CustomResponse updateUserAvatar(@RequestParam("file") MultipartFile file) {
Integer uid = currentUser.getUserId();
try {
return userService.updateUserAvatar(uid, file);
} catch (Exception e) {
e.printStackTrace();
return new CustomResponse(500, "头像更新失败", null);
}
}
@GetMapping("/user/info/get-one")
public CustomResponse getOneUserInfo(@RequestParam("uid") Integer uid) {
CustomResponse customResponse = new CustomResponse();
customResponse.setData(userService.getUserById(uid));
return customResponse;
}
}

View File

@ -0,0 +1,49 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.service.video.UserVideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserVideoController {
@Autowired
private UserVideoService userVideoService;
@Autowired
private CurrentUser currentUser;
/**
* 登录用户播放视频时更新播放次数有30秒更新间隔防止用户刷播放量
* @param vid 视频ID
* @return 返回用户与该视频的交互数据
*/
@PostMapping("/video/play/user")
public CustomResponse newPlayWithLoginUser(@RequestParam("vid") Integer vid) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
customResponse.setData(userVideoService.updatePlay(uid, vid));
return customResponse;
}
/**
* 点赞或点踩
* @param vid 视频ID
* @param isLove 赞还是踩 true赞 false踩
* @param isSet 点还是取消 true点 false取消
* @return 返回用户与该视频更新后的交互数据
*/
@PostMapping("/video/love-or-not")
public CustomResponse loveOrNot(@RequestParam("vid") Integer vid,
@RequestParam("isLove") boolean isLove,
@RequestParam("isSet") boolean isSet) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
customResponse.setData(userVideoService.setLoveOrUnlove(uid, vid, isLove, isSet));
return customResponse;
}
}

View File

@ -0,0 +1,303 @@
package com.teriteri.backend.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.teriteri.backend.mapper.FavoriteVideoMapper;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.pojo.FavoriteVideo;
import com.teriteri.backend.pojo.Video;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.service.video.VideoService;
import com.teriteri.backend.utils.RedisUtil;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
@RestController
public class VideoController {
@Autowired
private VideoService videoService;
@Autowired
private FavoriteVideoMapper favoriteVideoMapper;
@Autowired
private RedisUtil redisUtil;
@Autowired
private CurrentUser currentUser;
@Autowired
private SqlSessionFactory sqlSessionFactory;
/**
* 更新视频状态包括过审不通过删除其中审核相关需要管理员权限删除可以是管理员或者投稿用户
* @param vid 视频ID
* @param status 要修改的状态1通过 2不通过 3删除
* @return 无data返回 仅返回响应
*/
@PostMapping("/video/change/status")
public CustomResponse updateStatus(@RequestParam("vid") Integer vid,
@RequestParam("status") Integer status) {
try {
return videoService.updateVideoStatus(vid, status);
} catch (Exception e) {
e.printStackTrace();
return new CustomResponse(500, "操作失败", null);
}
}
/**
* 游客访问时的feed流随机推荐
* @return 返回11条随机推荐视频
*/
@GetMapping("/video/random/visitor")
public CustomResponse randomVideosForVisitor() {
CustomResponse customResponse = new CustomResponse();
int count = 11;
Set<Object> idSet = redisUtil.srandmember("video_status:1", count);
List<Map<String, Object>> videoList = videoService.getVideosWithDataByIds(idSet, 1, count);
// 随机打乱列表顺序
Collections.shuffle(videoList);
customResponse.setData(videoList);
return customResponse;
}
/**
* 累加获取更多视频
* @param vids 曾经查询过的视频id列表用于去重
* @return 每次返回新的10条视频以及其id列表并标注是否还有更多视频可以获取
*/
@GetMapping("/video/cumulative/visitor")
public CustomResponse cumulativeVideosForVisitor(@RequestParam("vids") String vids) {
CustomResponse customResponse = new CustomResponse();
Map<String, Object> map = new HashMap<>();
List<Integer> vidsList = new ArrayList<>();
if (vids.trim().length() > 0) {
vidsList = Arrays.stream(vids.split(","))
.map(Integer::parseInt)
.collect(Collectors.toList()); // 从字符串切分出id列表
}
Set<Object> set = redisUtil.getMembers("video_status:1");
if (set == null) {
map.put("videos", new ArrayList<>());
map.put("vids", new ArrayList<>());
map.put("more", false);
customResponse.setData(map);
return customResponse;
}
vidsList.forEach(set::remove); // 去除已获取的元素
Set<Object> idSet = new HashSet<>(); // 存放将要返回的id集合
Random random = new Random();
// 随机获取10个vid
for (int i = 0; i < 10 && set.size() > 0; i++) {
Object[] arr = set.toArray();
int randomIndex = random.nextInt(set.size());
idSet.add(arr[randomIndex]);
set.remove(arr[randomIndex]); // 查过的元素移除
}
List<Map<String, Object>> videoList = videoService.getVideosWithDataByIds(idSet, 1, 10);
Collections.shuffle(videoList); // 随机打乱列表顺序
map.put("videos", videoList);
map.put("vids", idSet);
if (set.size() > 0) {
map.put("more", true);
} else {
map.put("more", false);
}
customResponse.setData(map);
return customResponse;
}
/**
* 获取单条视频的信息
* @param vid 视频vid
* @return 视频信息
*/
@GetMapping("/video/getone")
public CustomResponse getOneVideo(@RequestParam("vid") Integer vid) {
CustomResponse customResponse = new CustomResponse();
Map<String, Object> map = videoService.getVideoWithDataById(vid);
if (map == null) {
customResponse.setCode(404);
customResponse.setMessage("特丽丽没找到个视频QAQ");
return customResponse;
}
Video video = (Video) map.get("video");
if (video.getStatus() != 1) {
customResponse.setCode(404);
customResponse.setMessage("特丽丽没找到个视频QAQ");
return customResponse;
}
customResponse.setData(map);
return customResponse;
}
@GetMapping("/video/user-works-count")
public CustomResponse getUserWorksCount(@RequestParam("uid") Integer uid) {
return new CustomResponse(200, "OK", redisUtil.zCard("user_video_upload:" + uid));
}
/**
* 获取用户视频投稿
* @param uid 用户id
* @param rule 排序方式 1 投稿日期 2 播放量 3 点赞数
* @param page 分页 从1开始
* @param quantity 每页查询数量
* @return 视频信息列表
*/
@GetMapping("/video/user-works")
public CustomResponse getUserWorks(@RequestParam("uid") Integer uid,
@RequestParam("rule") Integer rule,
@RequestParam("page") Integer page,
@RequestParam("quantity") Integer quantity) {
CustomResponse customResponse = new CustomResponse();
Map<String, Object> map = new HashMap<>();
Set<Object> set = redisUtil.zReverange("user_video_upload:" + uid, 0, -1);
if (set == null || set.isEmpty()) {
map.put("count", 0);
map.put("list", Collections.emptyList());
customResponse.setData(map);
return customResponse;
}
List<Integer> list = new ArrayList<>();
set.forEach(vid -> {
list.add((Integer) vid);
});
map.put("count", set.size());
switch (rule) {
case 1:
map.put("list", videoService.getVideosWithDataByIdsOrderByDesc(list, "upload_date", page, quantity));
break;
case 2:
map.put("list", videoService.getVideosWithDataByIdsOrderByDesc(list, "play", page, quantity));
break;
case 3:
map.put("list", videoService.getVideosWithDataByIdsOrderByDesc(list, "good", page, quantity));
break;
default:
map.put("list", videoService.getVideosWithDataByIdsOrderByDesc(list, "upload_date", page, quantity));
}
customResponse.setData(map);
return customResponse;
}
/**
* 获取用户最近点赞视频列表
* @param uid 用户uid
* @param offset 偏移量即当前已查询到多少条视频
* @param quantity 查询数量
* @return 视频信息列表
*/
@GetMapping("/video/user-love")
public CustomResponse getUserLoveMovies(@RequestParam("uid") Integer uid,
@RequestParam("offset") Integer offset,
@RequestParam("quantity") Integer quantity) {
CustomResponse customResponse = new CustomResponse();
Set<Object> set = redisUtil.zReverange("love_video:" + uid, (long) offset, (long) offset + quantity - 1);
if (set == null || set.isEmpty()) {
customResponse.setData(Collections.emptyList());
return customResponse;
}
List<Integer> list = new ArrayList<>();
set.forEach(vid -> {
list.add((Integer) vid);
});
customResponse.setData(videoService.getVideosWithDataByIdsOrderByDesc(list, null, 1, list.size()));
return customResponse;
}
/**
* 获取当前登录用户最近播放视频列表
* @param offset 偏移量即当前已查询到多少条视频
* @param quantity 查询数量
* @return 视频信息列表
*/
@GetMapping("/video/user-play")
public CustomResponse getUserPlayMovies(@RequestParam("offset") Integer offset,
@RequestParam("quantity") Integer quantity) {
Integer uid = currentUser.getUserId();
CustomResponse customResponse = new CustomResponse();
Set<Object> set = redisUtil.zReverange("user_video_history:" + uid, (long) offset, (long) offset + quantity - 1);
if (set == null || set.isEmpty()) {
customResponse.setData(Collections.emptyList());
return customResponse;
}
List<Integer> list = new ArrayList<>();
set.forEach(vid -> {
list.add((Integer) vid);
});
customResponse.setData(videoService.getVideosWithDataByIdsOrderByDesc(list, null, 1, list.size()));
return customResponse;
}
/**
* 获取某个收藏夹的视频
* @param fid 收藏夹ID
* @param rule 排序规则 1 最近收藏 2 最多播放 3 最新投稿
* @param page 分页 从1开始
* @param quantity 每页查询数量
* @return 视频信息列表
*/
@GetMapping("/video/user-collect")
public CustomResponse getUserCollectVideos(@RequestParam("fid") Integer fid,
@RequestParam("rule") Integer rule,
@RequestParam("page") Integer page,
@RequestParam("quantity") Integer quantity) {
CustomResponse customResponse = new CustomResponse();
Set<Object> set;
if (rule == 1) {
set = redisUtil.zReverange("favorite_video:" + fid, (long) (page - 1) * quantity, (long) page * quantity);
} else {
set = redisUtil.zReverange("favorite_video:" + fid, 0, -1);
}
if (set == null || set.isEmpty()) {
customResponse.setData(Collections.emptyList());
return customResponse;
}
List<Integer> list = new ArrayList<>();
set.forEach(vid -> {
list.add((Integer) vid);
});
List<Map<String, Object>> result;
switch (rule) {
case 1:
result = videoService.getVideosWithDataByIdsOrderByDesc(list, null, page, quantity);
break;
case 2:
result = videoService.getVideosWithDataByIdsOrderByDesc(list, "play", page, quantity);
break;
case 3:
result = videoService.getVideosWithDataByIdsOrderByDesc(list, "upload_date", page, quantity);
break;
default:
result = videoService.getVideosWithDataByIdsOrderByDesc(list, null, page, quantity);
}
if (result.size() == 0) {
customResponse.setData(result);
return customResponse;
}
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
result.stream().parallel().forEach(map -> {
Video video = (Video) map.get("video");
QueryWrapper<FavoriteVideo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("vid", video.getVid()).eq("fid", fid);
map.put("info", favoriteVideoMapper.selectOne(queryWrapper));
});
sqlSession.commit();
}
customResponse.setData(result);
return customResponse;
}
}

View File

@ -0,0 +1,67 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.utils.CurrentUser;
import com.teriteri.backend.service.video.VideoReviewService;
import com.teriteri.backend.service.video.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class VideoReviewController {
@Autowired
private VideoReviewService videoReviewService;
@Autowired
private VideoService videoService;
@Autowired
private CurrentUser currentUser;
/**
* 审核 查询对应状态的视频数量
* @param status 状态 0待审核 1通过 2未通过
* @return
*/
@GetMapping("/review/video/total")
public CustomResponse getTotal(@RequestParam("vstatus") Integer status) {
return videoReviewService.getTotalByStatus(status);
}
/**
* 审核 分页查询对应状态视频
* @param status 状态 0待审核 1通过 2未通过
* @param page 当前页
* @param quantity 每页的数量
* @return
*/
@GetMapping("/review/video/getpage")
public CustomResponse getVideos(@RequestParam("vstatus") Integer status,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "quantity", defaultValue = "10") Integer quantity) {
return videoReviewService.getVideosByPage(status, page, quantity);
}
/**
* 审核 查询单个视频详情
* @param vid 视频id
* @return
*/
@GetMapping("/review/video/getone")
public CustomResponse getOneVideo(@RequestParam("vid") Integer vid) {
CustomResponse customResponse = new CustomResponse();
if (!currentUser.isAdmin()) {
customResponse.setCode(403);
customResponse.setMessage("您不是管理员,无权访问");
return customResponse;
}
Map<String, Object> map = videoService.getVideoWithDataById(vid);
customResponse.setData(map); // 如果是是空照样返回前端自动处理
return customResponse;
}
}

View File

@ -0,0 +1,38 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.service.video.VideoStatsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class VideoStatsController {
@Autowired
private VideoStatsService videoStatsService;
/**
* 游客观看视频时更新视频播放量数据这个做不到时间间隔就是说每次刷新都会播放数加一有一个思路是使用浏览器指纹但是我不会
* @param vid 视频ID
* @return
*/
//使用浏览器指纹
// 目前 新建一个指纹表
// 游客的话
// 指纹信息 写入
// 用户id 为空
// 绑定视频
// 如果 该视频下存在用户id为空指纹信息 不增加播放量
// 如果 该视频下存在用户id为空指纹信息
@PostMapping("/video/play/visitor")
public CustomResponse newPlayWithVisitor(@RequestParam("vid") Integer vid) {
videoStatsService.updateStats(vid, "play", true, 1);
return new CustomResponse();
}
}

View File

@ -0,0 +1,94 @@
package com.teriteri.backend.controller;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.pojo.dto.VideoUploadInfoDTO;
import com.teriteri.backend.service.video.VideoUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
public class VideoUploadController {
@Autowired
private VideoUploadService videoUploadService;
/**
* 查询当前视频准备要上传的分片序号
* @param hash 视频的hash值
* @return
*/
@GetMapping("/video/ask-chunk")
public CustomResponse askChunk(@RequestParam("hash") String hash) {
return videoUploadService.askCurrentChunk(hash);
}
/**
* 上传分片
* @param chunk 分片的blob文件
* @param hash 视频的hash值
* @param index 当前分片的序号
* @return
* @throws IOException
*/
@PostMapping("/video/upload-chunk")
public CustomResponse uploadChunk(@RequestParam("chunk") MultipartFile chunk,
@RequestParam("hash") String hash,
@RequestParam("index") Integer index) throws IOException {
try {
return videoUploadService.uploadChunk(chunk, hash, index);
} catch (Exception e) {
e.printStackTrace();
return new CustomResponse(500, "分片上传失败", null);
}
}
/**
* 取消上传
* @param hash 视频的hash值
* @return
*/
@GetMapping("/video/cancel-upload")
public CustomResponse cancelUpload(@RequestParam("hash") String hash) {
return videoUploadService.cancelUpload(hash);
}
/**
* 添加视频投稿
* @param cover 封面文件
* @param hash 视频的hash值
* @param title 投稿标题
* @param type 视频类型 1自制 2转载
* @param auth 作者声明 0不声明 1未经允许禁止转载
* @param duration 视频总时长
* @param mcid 主分区ID
* @param scid 子分区ID
* @param tags 标签
* @param descr 简介
* @return 响应对象
*/
@PostMapping("/video/add")
public CustomResponse addVideo(@RequestParam("cover") MultipartFile cover,
@RequestParam("hash") String hash,
@RequestParam("title") String title,
@RequestParam("type") Integer type,
@RequestParam("auth") Integer auth,
@RequestParam("duration") Double duration,
@RequestParam("mcid") String mcid,
@RequestParam("scid") String scid,
@RequestParam("tags") String tags,
@RequestParam("descr") String descr) {
VideoUploadInfoDTO videoUploadInfoDTO = new VideoUploadInfoDTO(null, hash, title, type, auth, duration, mcid, scid, tags, descr, null);
try {
return videoUploadService.addVideo(cover, videoUploadInfoDTO);
} catch (Exception e) {
e.printStackTrace();
return new CustomResponse(500, "封面上传失败", null);
}
}
}

View File

@ -0,0 +1,59 @@
package com.teriteri.backend.im;
import com.teriteri.backend.im.handler.TokenValidationHandler;
import com.teriteri.backend.im.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class IMServer {
// 存储每个用户的全部连接
public static final Map<Integer, Set<Channel>> userChannel = new ConcurrentHashMap<>();
public void start() throws InterruptedException {
// 主从结构
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 绑定监听端口
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline(); // 处理流
// 添加 Http 编码解码器
pipeline.addLast(new HttpServerCodec())
// 添加处理大数据的组件
.addLast(new ChunkedWriteHandler())
// Http 消息做聚合操作方便处理产生 FullHttpRequest FullHttpResponse
// 1024 * 64 是单条信息最长字节数
.addLast(new HttpObjectAggregator(1024 * 64))
.addLast(new TokenValidationHandler())
// 添加 WebSocket 支持
.addLast(new WebSocketServerProtocolHandler("/im"))
.addLast(new WebSocketHandler());
}
});
ChannelFuture future = bootstrap.bind(7071).sync();
}
}

View File

@ -0,0 +1,164 @@
package com.teriteri.backend.im.handler;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.teriteri.backend.im.IMServer;
import com.teriteri.backend.mapper.ChatDetailedMapper;
import com.teriteri.backend.pojo.ChatDetailed;
import com.teriteri.backend.pojo.IMResponse;
import com.teriteri.backend.service.message.ChatService;
import com.teriteri.backend.service.user.UserService;
import com.teriteri.backend.utils.RedisUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Slf4j
@Component
public class ChatHandler {
private static ChatService chatService;
private static ChatDetailedMapper chatDetailedMapper;
private static UserService userService;
private static RedisUtil redisUtil;
private static Executor taskExecutor;
@Autowired
private void setDependencies(ChatService chatService,
ChatDetailedMapper chatDetailedMapper,
UserService userService,
RedisUtil redisUtil,
@Qualifier("taskExecutor") Executor taskExecutor) {
ChatHandler.chatService = chatService;
ChatHandler.chatDetailedMapper = chatDetailedMapper;
ChatHandler.userService = userService;
ChatHandler.redisUtil = redisUtil;
ChatHandler.taskExecutor = taskExecutor;
}
/**
* 发送消息
* @param ctx
* @param tx
*/
public static void send(ChannelHandlerContext ctx, TextWebSocketFrame tx) {
try {
ChatDetailed chatDetailed = JSONObject.parseObject(tx.text(), ChatDetailed.class);
// System.out.println("接收到聊天消息:" + chatDetailed);
// 从channel中获取当前用户id 封装写库
Integer user_id = (Integer) ctx.channel().attr(AttributeKey.valueOf("userId")).get();
chatDetailed.setUserId(user_id);
chatDetailed.setUserDel(0);
chatDetailed.setAnotherDel(0);
chatDetailed.setWithdraw(0);
chatDetailed.setTime(new Date());
chatDetailedMapper.insert(chatDetailed);
// "chat_detailed_zset:对方:自己"
redisUtil.zset("chat_detailed_zset:" + user_id + ":" + chatDetailed.getAnotherId(), chatDetailed.getId());
redisUtil.zset("chat_detailed_zset:" + chatDetailed.getAnotherId() + ":" + user_id, chatDetailed.getId());
boolean online = chatService.updateChat(user_id, chatDetailed.getAnotherId());
// 转发到发送者和接收者的全部channel
Map<String, Object> map = new HashMap<>();
map.put("type", "接收");
map.put("online", online); // 对方是否在窗口
map.put("detail", chatDetailed);
CompletableFuture<Void> chatFuture = CompletableFuture.runAsync(() -> {
map.put("chat", chatService.getChat(user_id, chatDetailed.getAnotherId()));
}, taskExecutor);
CompletableFuture<Void> userFuture = CompletableFuture.runAsync(() -> {
map.put("user", userService.getUserById(user_id));
}, taskExecutor);
chatFuture.join();
userFuture.join();
// 发给自己的全部channel
Set<Channel> from = IMServer.userChannel.get(user_id);
if (from != null) {
for (Channel channel : from) {
channel.writeAndFlush(IMResponse.message("whisper", map));
}
}
// 发给对方的全部channel
Set<Channel> to = IMServer.userChannel.get(chatDetailed.getAnotherId());
if (to != null) {
for (Channel channel : to) {
channel.writeAndFlush(IMResponse.message("whisper", map));
}
}
} catch (Exception e) {
log.error("发送聊天信息时出错了:" + e);
ctx.channel().writeAndFlush(IMResponse.error("发送消息时出错了 Σ(゚д゚;)"));
}
}
/**
* 撤回消息
* @param ctx
* @param tx
*/
public static void withdraw(ChannelHandlerContext ctx, TextWebSocketFrame tx) {
try {
JSONObject jsonObject = JSONObject.parseObject(tx.text());
Integer id = jsonObject.getInteger("id");
Integer user_id = (Integer) ctx.channel().attr(AttributeKey.valueOf("userId")).get();
// 查询数据库
ChatDetailed chatDetailed = chatDetailedMapper.selectById(id);
if (chatDetailed == null) {
ctx.channel().writeAndFlush(IMResponse.error("消息不存在"));
return;
}
if (!Objects.equals(chatDetailed.getUserId(), user_id)) {
ctx.channel().writeAndFlush(IMResponse.error("无权撤回此消息"));
return;
}
long diff = System.currentTimeMillis() - chatDetailed.getTime().getTime();
if (diff > 120000) {
ctx.channel().writeAndFlush(IMResponse.error("发送时间超过两分钟不能撤回"));
return;
}
// 更新 withdraw 字段
UpdateWrapper<ChatDetailed> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", id).setSql("withdraw = 1");
chatDetailedMapper.update(null, updateWrapper);
// 转发到发送者和接收者的全部channel
Map<String, Object> map = new HashMap<>();
map.put("type", "撤回");
map.put("sendId", chatDetailed.getUserId());
map.put("acceptId", chatDetailed.getAnotherId());
map.put("id", id);
// 发给自己的全部channel
Set<Channel> from = IMServer.userChannel.get(user_id);
if (from != null) {
for (Channel channel : from) {
channel.writeAndFlush(IMResponse.message("whisper", map));
}
}
// 发给对方的全部channel
Set<Channel> to = IMServer.userChannel.get(chatDetailed.getAnotherId());
if (to != null) {
for (Channel channel : to) {
channel.writeAndFlush(IMResponse.message("whisper", map));
}
}
} catch (Exception e) {
log.error("撤回聊天信息时出错了:" + e);
ctx.channel().writeAndFlush(IMResponse.error("撤回消息时出错了 Σ(゚д゚;)"));
}
}
}

View File

@ -0,0 +1,103 @@
package com.teriteri.backend.im.handler;
import com.alibaba.fastjson2.JSON;
import com.teriteri.backend.im.IMServer;
import com.teriteri.backend.pojo.Command;
import com.teriteri.backend.pojo.IMResponse;
import com.teriteri.backend.pojo.User;
import com.teriteri.backend.utils.JwtUtil;
import com.teriteri.backend.utils.RedisUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Set;
@Slf4j
@Component
public class TokenValidationHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static JwtUtil jwtUtil;
private static RedisUtil redisUtil;
@Autowired
public void setDependencies(JwtUtil jwtUtil, RedisUtil redisUtil) {
TokenValidationHandler.jwtUtil = jwtUtil;
TokenValidationHandler.redisUtil = redisUtil;
}
public TokenValidationHandler() {
}
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame tx) {
Command command = JSON.parseObject(tx.text(), Command.class);
String token = command.getContent();
Integer uid = isValidToken(token);
if (uid != null) {
// 将uid绑到ctx上
ctx.channel().attr(AttributeKey.valueOf("userId")).set(uid);
// 将channel存起来
if (IMServer.userChannel.get(uid) == null) {
Set<Channel> set = new HashSet<>();
set.add(ctx.channel());
IMServer.userChannel.put(uid, set);
} else {
IMServer.userChannel.get(uid).add(ctx.channel());
}
redisUtil.addMember("login_member", uid); // 将用户添加到在线用户集合
// System.out.println("该用户的全部连接状态:" + IMServer.userChannel.get(uid));
// System.out.println("当前在线人数:" + IMServer.userChannel.size());
// 移除token验证处理器以便以后使用无需判断
ctx.pipeline().remove(TokenValidationHandler.class);
// 保持消息的引用计数以确保消息不会被释放
tx.retain();
// 将消息传递给下一个处理器
ctx.fireChannelRead(tx);
} else {
ctx.channel().writeAndFlush(IMResponse.error("登录已过期"));
ctx.close();
}
}
/**
* 进行JWT验证
* @param token Bearer JWT
* @return 返回用户ID 验证不通过则返回null
*/
private Integer isValidToken(String token) {
if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
return null;
}
token = token.substring(7);
// 解析token
boolean verifyToken = jwtUtil.verifyToken(token);
if (!verifyToken) {
log.error("当前token已过期");
return null;
}
String userId = JwtUtil.getSubjectFromToken(token);
String role = JwtUtil.getClaimFromToken(token, "role");
User user = redisUtil.getObject("security:" + role + ":" + userId, User.class);
if (user == null) {
log.error("用户未登录");
return null;
}
// log.info("成功通过验证!");
return user.getUid();
}
}

View File

@ -0,0 +1,80 @@
package com.teriteri.backend.im.handler;
import com.alibaba.fastjson2.JSON;
import com.teriteri.backend.im.IMServer;
import com.teriteri.backend.pojo.Command;
import com.teriteri.backend.pojo.CommandType;
import com.teriteri.backend.pojo.IMResponse;
import com.teriteri.backend.utils.RedisUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Set;
@Slf4j
@Component
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private static RedisUtil redisUtil;
@Autowired
public void setDependencies(RedisUtil redisUtil) {
WebSocketHandler.redisUtil = redisUtil;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame tx) {
try {
Command command = JSON.parseObject(tx.text(), Command.class);
// System.out.println("command: " + command);
// 根据code分发不同处理程序
switch (CommandType.match(command.getCode())) {
case CONNETION: // 如果是连接消息就不需要做任何操作了因为连接上的话在token鉴权那就做了
break;
case CHAT_SEND:
ChatHandler.send(ctx, tx);
break;
case CHAT_WITHDRAW:
ChatHandler.withdraw(ctx, tx);
break;
default: ctx.channel().writeAndFlush(IMResponse.error("不支持的CODE " + command.getCode()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 连接断开时执行 将channel从集合中移除 如果集合为空则从Map中移除该用户 即离线状态
* @param ctx
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// 当连接断开时 userChannel 中移除对应的 Channel
Integer uid = (Integer) ctx.channel().attr(AttributeKey.valueOf("userId")).get();
Set<Channel> userChannels = IMServer.userChannel.get(uid);
// System.out.println("移除channel前的集合状态" + userChannels);
if (userChannels != null) {
userChannels.remove(ctx.channel());
// System.out.println("移除channel后的集合状态" + IMServer.userChannel.get(uid));
// 用户离线操作
if (IMServer.userChannel.get(uid).size() == 0) {
IMServer.userChannel.remove(uid);
// System.out.println("当前在线人数:" + IMServer.userChannel.size());
redisUtil.deleteKeysWithPrefix("whisper:" + uid + ":"); // 清除全部在聊天窗口的状态
redisUtil.delMember("login_member", uid); // 从在线用户集合中移除
}
}
// 继续处理后续逻辑
ctx.fireChannelInactive();
}
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Category;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.ChatDetailed;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChatDetailedMapper extends BaseMapper<ChatDetailed> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Chat;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ChatMapper extends BaseMapper<Chat> {
}

View File

@ -0,0 +1,19 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Comment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface CommentMapper extends BaseMapper<Comment> {
// @Select("SELECT * FROM comment WHERE root_id = #{rootId} AND vid = #{vid}")
// List<Comment> getChildCommentsByRootId(@Param("rootId") Integer rootId, @Param("vid") Integer vid);
@Select("SELECT * FROM comment WHERE vid = #{vid} AND root_id = 0")
List<Comment> getRootCommentsByVid(@Param("vid") Integer vid);
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Danmu;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DanmuMapper extends BaseMapper<Danmu> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Favorite;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FavoriteMapper extends BaseMapper<Favorite> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.FavoriteVideo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FavoriteVideoMapper extends BaseMapper<FavoriteVideo> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.MsgUnread;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MsgUnreadMapper extends BaseMapper<MsgUnread> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.UserVideo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserVideoMapper extends BaseMapper<UserVideo> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.Video;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VideoMapper extends BaseMapper<Video> {
}

View File

@ -0,0 +1,9 @@
package com.teriteri.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.teriteri.backend.pojo.VideoStats;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface VideoStatsMapper extends BaseMapper<VideoStats> {
}

View File

@ -0,0 +1,17 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Category {
private String mcId;
private String scId;
private String mcName;
private String scName;
private String descr;
private String rcmTag;
}

View File

@ -0,0 +1,23 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Chat {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer userId; // 发送者UID
private Integer anotherId; // 接收者UID
private Integer isDeleted; // 是否移除聊天 0否 1是
private Integer unread; // 消息未读数
private Date latestTime; // 最近接收消息的时间或最近打开聊天窗口的时间
}

View File

@ -0,0 +1,24 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatDetailed {
@TableId(type = IdType.AUTO)
private Integer id; // 消息id
private Integer userId; // 发送者uid
private Integer anotherId; // 接受者uid
private String content; // 消息内容
private Integer userDel; // 发送者是否删除
private Integer anotherDel; // 接受者者是否删除
private Integer withdraw; // 消息是否被撤回
private Date time; // 发送消息的时间
}

View File

@ -0,0 +1,13 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Command {
private Integer code;
private String content;
}

View File

@ -0,0 +1,37 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum CommandType {
/**
* 建立连接
*/
CONNETION(100),
/**
* 聊天功能 发送
*/
CHAT_SEND(101),
/**
* 聊天功能 撤回
*/
CHAT_WITHDRAW(102),
ERROR(-1),
;
private final Integer code;
public static CommandType match(Integer code) {
for (CommandType value: CommandType.values()) {
if (value.getCode().equals(code)) {
return value;
}
}
return ERROR;
}
}

View File

@ -0,0 +1,29 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer vid;
private Integer uid;
private Integer rootId;
private Integer parentId;
private Integer toUserId;
private String content;
private Integer love;
private Integer bad;
private Date createTime;
private Integer isTop;
private Integer isDeleted;
}

View File

@ -0,0 +1,27 @@
package com.teriteri.backend.pojo;
import com.teriteri.backend.pojo.dto.UserDTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommentTree {
private Integer id;
private Integer vid;
private Integer rootId;
private Integer parentId;
private String content;
private UserDTO user;
private UserDTO toUser;
private Integer love;
private Integer bad;
private List<CommentTree> replies;
private Date createTime;
private Long count;
}

View File

@ -0,0 +1,18 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 响应包装类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
// 自定义响应对象
public class CustomResponse {
private int code = 200;
private String message = "OK";
private Object data;
}

View File

@ -0,0 +1,27 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Danmu {
@TableId(type = IdType.AUTO)
private Integer id; // 弹幕ID
private Integer vid; // 视频ID
private Integer uid; // 用户ID
private String content; // 弹幕内容
private Integer fontsize; // 字体大小 默认25 小18
private Integer mode; // 模式 1滚动 2顶部 3底部
private String color; // 字体颜色 6位十六进制标准格式 #FFFFFF
private Double timePoint; // 弹幕在视频中的时间点位置
private Integer state; // 弹幕状态 1默认过审 2被举报审核中 3删除
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Shanghai")
private Date createDate; // 弹幕发送日期时间
}

View File

@ -0,0 +1,12 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ESSearchWord {
private String content;
}

View File

@ -0,0 +1,13 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ESUser {
private Integer uid;
private String nickname;
}

View File

@ -0,0 +1,18 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ESVideo {
private Integer vid;
private Integer uid;
private String title;
private String mc_id;
private String sc_id;
private String tags;
private Integer status;
}

View File

@ -0,0 +1,23 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Favorite {
@TableId(type = IdType.AUTO)
private Integer fid; // 收藏夹ID
private Integer uid; // 所属用户ID
private Integer type; // 收藏夹类型 1默认收藏夹 2用户创建
private Integer visible; // 对外开放 0隐藏 1公开
private String cover; // 收藏夹封面url
private String title; // 收藏夹名称
private String description; // 简介
private Integer count; // 收藏夹中视频数量
private Integer isDelete; // 是否删除 1已删除
}

View File

@ -0,0 +1,21 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FavoriteVideo {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer vid; // 视频ID
private Integer fid; // 收藏夹ID
private Date time; // 收藏时间
private Integer isRemove; // 是否移除 1已移出收藏夹
}

View File

@ -0,0 +1,14 @@
package com.teriteri.backend.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HotSearch {
private String content; // 内容
private Double score; // 热度
private Integer type = 0; // 类型 0 普通 1 2
}

View File

@ -0,0 +1,37 @@
package com.teriteri.backend.pojo;
import com.alibaba.fastjson2.JSON;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class IMResponse {
private String type; // 消息类型 "reply","at","love","system","whisper","dynamic" / "error"
private LocalDateTime time;
private Object data; // 返回的相关数据
/**
* 返回系统失败消息
* @param message 自定义系统消息提示
* @return 返回系统失败消息
*/
public static TextWebSocketFrame error(String message) {
return new TextWebSocketFrame(JSON.toJSONString(new IMResponse("error", LocalDateTime.now(), message)));
}
/**
* 非系统类消息
* @param type 消息类型 "reply","at","love","system","whisper","dynamic" 对应 msg_unread 表的每一列
* @param data 返回的相关数据
* @return 返回非系统消息以及携带数据
*/
public static TextWebSocketFrame message(String type, Object data) {
return new TextWebSocketFrame(JSON.toJSONString(new IMResponse(type, LocalDateTime.now(), data)));
}
}

View File

@ -0,0 +1,20 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MsgUnread {
@TableId
private Integer uid; // 用户uid 不自动增长 跟随注册时的uid
private Integer reply; // 回复我的
private Integer at; // @ 我的
private Integer love; // 收到的赞
private Integer system; // 系统通知
private Integer whisper; // 我的消息私聊总数
private Integer dynamic; // 动态
}

View File

@ -0,0 +1,36 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@TableId(type = IdType.AUTO)
private Integer uid;
private String username;
private String password;
private String nickname;
private String avatar;
private String background;
private Integer gender; // 性别0女性 1男性 2无性别默认2
private String description;
private Integer exp; // 经验值 50/200/1500/4500/10800/28800 分别是0~6级的区间
private Double coin; // 硬币数 保留一位小数
private Integer vip; // 0 普通用户1 月度大会员2 季度大会员3 年度大会员
private Integer state; // 0 正常1 封禁中2 已注销
private Integer role; // 0 普通用户1 普通管理员2 超级管理员
private Integer auth; // 0 普通用户1 个人认证2 机构认证
private String authMsg; // 认证信息 teriteri官方账号
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Shanghai")
private Date createDate;
// @JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Shanghai")
private Date deleteDate;
}

View File

@ -0,0 +1,27 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserVideo {
@TableId(type = IdType.AUTO)
private Integer id; // 唯一标识
private Integer uid; // 观看视频的用户ID
private Integer vid; // 视频ID
private Integer play; // 观看次数
private Integer love; // 点赞 0没赞 1已点赞
private Integer unlove; // 不喜欢 0没点 1已不喜欢
private Integer coin; // 投币数 0-2 一个视频一个用户上限投2个币
private Integer collect; // 收藏 0未收藏 1已收藏
private Date playTime; // 最近观看时间
private Date loveTime; // 最近点赞时间
private Date coinTime; // 最近投币时间
}

View File

@ -0,0 +1,34 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Video {
@TableId(type = IdType.AUTO)
private Integer vid;
private Integer uid;
private String title;
private Integer type;
private Integer auth;
private Double duration;
private String mcId;
private String scId;
private String tags;
private String descr;
private String coverUrl;
private String videoUrl;
private Integer status; // 0审核中 1通过审核 2打回整改指投稿信息不符 3视频违规删除视频内容违规
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Shanghai")
private Date uploadDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Shanghai")
private Date deleteDate;
}

View File

@ -0,0 +1,23 @@
package com.teriteri.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VideoStats {
@TableId
private Integer vid;
private Integer play;
private Integer danmu;
private Integer good;
private Integer bad;
private Integer coin;
private Integer collect;
private Integer share;
private Integer comment;
}

View File

@ -0,0 +1,17 @@
package com.teriteri.backend.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryDTO {
private String mcId;
private String mcName;
private List<Map<String, Object>> scList;
}

View File

@ -0,0 +1,28 @@
package com.teriteri.backend.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
private Integer uid;
private String nickname;
private String avatar_url;
private String bg_url;
private Integer gender; // 性别0女性 1男性 2无性别默认2
private String description;
private Integer exp; // 经验值 50/200/1500/4500/10800/28800 分别是0~6级的区间
private Double coin; // 硬币数 保留一位小数
private Integer vip; // 0 普通用户1 月度大会员2 季度大会员3 年度大会员
private Integer state; // 0 正常1 封禁中
private Integer auth; // 0 普通用户1 个人认证2 机构认证
private String authMsg; // 认证信息 teriteri官方账号
private Integer videoCount; // 视频投稿数
private Integer followsCount; // 关注数
private Integer fansCount; // 粉丝数
private Integer loveCount; // 获赞数
private Integer playCount; // 播放数
}

View File

@ -0,0 +1,22 @@
package com.teriteri.backend.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class VideoUploadInfoDTO {
private Integer uid;
private String hash;
private String title;
private Integer type;
private Integer auth;
private Double duration;
private String mcId;
private String scId;
private String tags;
private String descr;
private String coverUrl;
}

View File

@ -0,0 +1,20 @@
package com.teriteri.backend.service.category;
import com.teriteri.backend.pojo.Category;
import com.teriteri.backend.pojo.CustomResponse;
public interface CategoryService {
/**
* 获取全部分区数据
* @return CustomResponse对象
*/
CustomResponse getAll();
/**
* 根据id查询对应分区信息
* @param mcId 主分区ID
* @param scId 子分区ID
* @return Category类信息
*/
Category getCategoryById(String mcId, String scId);
}

View File

@ -0,0 +1,28 @@
package com.teriteri.backend.service.comment;
import com.teriteri.backend.pojo.Comment;
import com.teriteri.backend.pojo.CommentTree;
import com.teriteri.backend.pojo.CustomResponse;
import java.util.List;
public interface CommentService {
List<CommentTree> getCommentTreeByVid(Integer vid, Long offset, Integer type);
CommentTree sendComment(Integer vid, Integer uid, Integer rootId, Integer parentId, Integer toUserId, String content);
CustomResponse deleteComment(Integer id, Integer uid, boolean isAdmin);
List<Comment> getChildCommentsByRootId(Integer rootId, Integer vid, Long start, Long stop);
List<Comment> getRootCommentsByVid(Integer vid, Long offset, Integer type);
CommentTree getMoreCommentsById(Integer id);
/*
评论点赞点踩相关
*/
void updateLikeAndDisLike(Integer id, boolean addLike);
void updateComment(Integer id, String column, boolean incr, Integer count);
}

View File

@ -0,0 +1,14 @@
package com.teriteri.backend.service.comment;
import com.teriteri.backend.pojo.CustomResponse;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface UserCommentService {
Map<String, Object> getUserLikeAndDislike(Integer uid);
void userSetLikeOrUnlike(Integer uid, Integer id, boolean isLike, boolean isSet);
}

View File

@ -0,0 +1,25 @@
package com.teriteri.backend.service.danmu;
import com.teriteri.backend.pojo.CustomResponse;
import com.teriteri.backend.pojo.Danmu;
import java.util.List;
import java.util.Set;
public interface DanmuService {
/**
* 根据弹幕ID集合查询弹幕列表
* @param idset 弹幕ID集合
* @return 弹幕列表
*/
List<Danmu> getDanmuListByIdset(Set<Object> idset);
/**
* 删除弹幕
* @param id 弹幕id
* @param uid 操作用户
* @param isAdmin 是否管理员
* @return 响应对象
*/
CustomResponse deleteDanmu(Integer id, Integer uid, boolean isAdmin);
}

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