demo
|
@ -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/
|
|
@ -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.
|
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.9 MiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 228 KiB |
After Width: | Height: | Size: 625 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 415 KiB |
After Width: | Height: | Size: 2.7 MiB |
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 179 KiB |
After Width: | Height: | Size: 102 KiB |
After Width: | Height: | Size: 2.2 MiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 171 KiB |
After Width: | Height: | Size: 395 KiB |
After Width: | Height: | Size: 173 KiB |
After Width: | Height: | Size: 1.1 MiB |
|
@ -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> <!– 使用最新版本 –>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.bouncycastle</groupId>-->
|
||||
<!-- <artifactId>bcpkix-jdk15on</artifactId>-->
|
||||
<!-- <version>1.69</version> <!– 使用最新版本 –>-->
|
||||
<!-- </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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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", "");
|
||||
//deny:Druid 后台拒绝谁访问
|
||||
//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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
// }
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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/");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
|
@ -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("撤回消息时出错了 Σ(゚д゚;)"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; // 最近接收消息的时间或最近打开聊天窗口的时间
|
||||
|
||||
}
|
|
@ -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; // 发送消息的时间
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; // 弹幕发送日期时间
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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已删除
|
||||
}
|
|
@ -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已移出收藏夹
|
||||
}
|
|
@ -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 热
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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; // 动态
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; // 最近投币时间
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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; // 播放数
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
package com.teriteri.backend.service.impl.category;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.teriteri.backend.mapper.CategoryMapper;
|
||||
import com.teriteri.backend.pojo.CustomResponse;
|
||||
import com.teriteri.backend.pojo.dto.CategoryDTO;
|
||||
import com.teriteri.backend.pojo.Category;
|
||||
import com.teriteri.backend.service.category.CategoryService;
|
||||
import com.teriteri.backend.utils.RedisUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CategoryServiceImpl implements CategoryService {
|
||||
|
||||
@Autowired
|
||||
private CategoryMapper categoryMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("taskExecutor")
|
||||
private Executor taskExecutor;
|
||||
|
||||
/**
|
||||
* 获取全部分区数据
|
||||
*
|
||||
* @return CustomResponse对象
|
||||
*/
|
||||
@Override
|
||||
public CustomResponse getAll() {
|
||||
CustomResponse customResponse = new CustomResponse();
|
||||
List<CategoryDTO> sortedCategories = new ArrayList<>();
|
||||
|
||||
// 尝试从redis中获取数据
|
||||
try {
|
||||
sortedCategories = redisUtil.getAllList("categoryList", CategoryDTO.class);
|
||||
if (sortedCategories.size() != 0) {
|
||||
customResponse.setData(sortedCategories);
|
||||
return customResponse;
|
||||
}
|
||||
log.warn("redis中获取不到分区数据");
|
||||
} catch (Exception e) {
|
||||
log.error("获取redis分区数据失败");
|
||||
}
|
||||
|
||||
// 将分区表一次全部查询出来,再在内存执行处理逻辑,可以减少数据库的IO
|
||||
QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
|
||||
List<Category> list = categoryMapper.selectList(queryWrapper);
|
||||
|
||||
// 开一个临时整合map
|
||||
Map<String, CategoryDTO> categoryDTOMap = new HashMap<>();
|
||||
|
||||
for (Category category : list) {
|
||||
String mcId = category.getMcId();
|
||||
String scId = category.getScId();
|
||||
String mcName = category.getMcName();
|
||||
String scName = category.getScName();
|
||||
String descr = category.getDescr();
|
||||
List<String> rcmTag = new ArrayList<>();
|
||||
if (category.getRcmTag() != null) {
|
||||
String[] strings = category.getRcmTag().split("\n"); // 将每个标签切出来组成列表封装
|
||||
rcmTag = Arrays.asList(strings);
|
||||
}
|
||||
|
||||
// 先将主分类和空的子分类列表整合到map中
|
||||
if (!categoryDTOMap.containsKey(mcId)) {
|
||||
CategoryDTO categoryDTO = new CategoryDTO();
|
||||
categoryDTO.setMcId(mcId);
|
||||
categoryDTO.setMcName(mcName);
|
||||
categoryDTO.setScList(new ArrayList<>());
|
||||
categoryDTOMap.put(mcId, categoryDTO);
|
||||
}
|
||||
|
||||
// 把子分类整合到map的子分类列表里
|
||||
Map<String, Object> scMap = new HashMap<>();
|
||||
scMap.put("mcId", mcId);
|
||||
scMap.put("scId", scId);
|
||||
scMap.put("scName", scName);
|
||||
scMap.put("descr", descr);
|
||||
scMap.put("rcmTag", rcmTag);
|
||||
categoryDTOMap.get(mcId).getScList().add(scMap);
|
||||
|
||||
}
|
||||
|
||||
// 按指定序列排序
|
||||
List<String> sortOrder = Arrays.asList("anime", "guochuang", "douga", "game", "kichiku",
|
||||
"music", "dance", "cinephile", "ent", "knowledge",
|
||||
"tech", "information", "food", "life", "car",
|
||||
"fashion", "sports", "animal", "virtual");
|
||||
|
||||
for (String mcId : sortOrder) {
|
||||
if (categoryDTOMap.containsKey(mcId)) {
|
||||
sortedCategories.add(categoryDTOMap.get(mcId));
|
||||
}
|
||||
}
|
||||
// 将分类添加到redis缓存中
|
||||
try {
|
||||
redisUtil.delValue("categoryList");
|
||||
redisUtil.setAllList("categoryList", sortedCategories);
|
||||
} catch (Exception e) {
|
||||
log.error("存储redis分类列表失败");
|
||||
}
|
||||
customResponse.setData(sortedCategories);
|
||||
return customResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询对应分区信息
|
||||
*
|
||||
* @param mcId 主分区ID
|
||||
* @param scId 子分区ID
|
||||
* @return Category类信息
|
||||
*/
|
||||
@Override
|
||||
public Category getCategoryById(String mcId, String scId) {
|
||||
// 从redis中获取最新数据
|
||||
Category category = redisUtil.getObject("category:" + mcId + ":" + scId, Category.class);
|
||||
// 如果redis中没有数据,就从mysql中获取并更新到redis
|
||||
if (category == null) {
|
||||
QueryWrapper<Category> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("mc_id", mcId).eq("sc_id", scId);
|
||||
category = categoryMapper.selectOne(queryWrapper);
|
||||
if (category == null) {
|
||||
return new Category(); // 如果不存在则返回空
|
||||
}
|
||||
|
||||
Category finalCategory = category;
|
||||
CompletableFuture.runAsync(() -> {
|
||||
redisUtil.setExObjectValue("category:" + mcId + ":" + scId, finalCategory); // 默认存活1小时
|
||||
}, taskExecutor);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,332 @@
|
|||
package com.teriteri.backend.service.impl.comment;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.teriteri.backend.im.IMServer;
|
||||
import com.teriteri.backend.mapper.CommentMapper;
|
||||
import com.teriteri.backend.mapper.VideoMapper;
|
||||
import com.teriteri.backend.pojo.*;
|
||||
import com.teriteri.backend.service.comment.CommentService;
|
||||
import com.teriteri.backend.service.message.MsgUnreadService;
|
||||
import com.teriteri.backend.service.user.UserService;
|
||||
import com.teriteri.backend.service.video.VideoStatsService;
|
||||
import com.teriteri.backend.utils.RedisUtil;
|
||||
import io.netty.channel.Channel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CommentServiceImpl implements CommentService {
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Autowired
|
||||
private CommentMapper commentMapper;
|
||||
|
||||
@Autowired
|
||||
private VideoMapper videoMapper;
|
||||
|
||||
@Autowired
|
||||
private VideoStatsService videoStatsService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private MsgUnreadService msgUnreadService;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("taskExecutor")
|
||||
private Executor taskExecutor;
|
||||
|
||||
/**
|
||||
* 获取评论树列表
|
||||
* @param vid 对应视频ID
|
||||
* @param offset 分页偏移量(已经获取到的评论树的数量)
|
||||
* @param type 排序类型 1 按热度排序 2 按时间排序
|
||||
* @return 评论树列表
|
||||
*/
|
||||
@Override
|
||||
public List<CommentTree> getCommentTreeByVid(Integer vid, Long offset, Integer type) {
|
||||
// 查询父级评论
|
||||
List<Comment> rootComments = getRootCommentsByVid(vid, offset, type);
|
||||
|
||||
// 并行执行每个根级评论的子评论查询任务
|
||||
List<CommentTree> commentTreeList = rootComments.stream().parallel()
|
||||
.map(rootComment ->buildCommentTree(rootComment, 0L, 2L))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// System.out.println(commentTreeList);
|
||||
|
||||
return commentTreeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建评论树
|
||||
* @param comment 根评论
|
||||
* @param start 子评论开始偏移量
|
||||
* @param stop 子评论结束偏移量
|
||||
* @return 单棵评论树
|
||||
*/
|
||||
private CommentTree buildCommentTree(Comment comment, Long start, Long stop) {
|
||||
CommentTree tree = new CommentTree();
|
||||
tree.setId(comment.getId());
|
||||
tree.setVid(comment.getVid());
|
||||
tree.setRootId(comment.getRootId());
|
||||
tree.setParentId(comment.getParentId());
|
||||
tree.setContent(comment.getContent());
|
||||
tree.setCreateTime(comment.getCreateTime());
|
||||
tree.setLove(comment.getLove());
|
||||
tree.setBad(comment.getBad());
|
||||
|
||||
tree.setUser(userService.getUserById(comment.getUid()));
|
||||
tree.setToUser(userService.getUserById(comment.getToUserId()));
|
||||
|
||||
// 递归查询构建子评论树
|
||||
// 这里如果是根节点的评论,则查出他的子评论; 如果不是根节点评论,则不查,只填写 User 信息。
|
||||
if (comment.getRootId() == 0) {
|
||||
long count = redisUtil.zCard("comment_reply:" + comment.getId());
|
||||
tree.setCount(count);
|
||||
|
||||
List<Comment> childComments = getChildCommentsByRootId(comment.getId(), comment.getVid(), start, stop);
|
||||
|
||||
List<CommentTree> childTreeList = childComments.stream().parallel()
|
||||
.map(childComment -> buildCommentTree(childComment, start, stop))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
tree.setReplies(childTreeList);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送评论,字数不得大于2000或为空
|
||||
* @param vid 视频id
|
||||
* @param uid 发布者uid
|
||||
* @param rootId 楼层id(根评论id)
|
||||
* @param parentId 被回复的评论id
|
||||
* @param toUserId 被回复用户uid
|
||||
* @param content 评论内容
|
||||
* @return true 发送成功 false 发送失败
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public CommentTree sendComment(Integer vid, Integer uid, Integer rootId, Integer parentId, Integer toUserId, String content) {
|
||||
if (content == null || content.length() == 0 || content.length() > 2000) return null;
|
||||
Comment comment = new Comment(
|
||||
null,
|
||||
vid,
|
||||
uid,
|
||||
rootId,
|
||||
parentId,
|
||||
toUserId,
|
||||
content,
|
||||
0,
|
||||
0,
|
||||
new Date(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
commentMapper.insert(comment);
|
||||
// 更新视频评论 + 1
|
||||
videoStatsService.updateStats(comment.getVid(), "comment", true, 1);
|
||||
|
||||
CommentTree commentTree = buildCommentTree(comment, 0L, -1L);
|
||||
|
||||
try {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// 如果不是根级评论,则加入 redis 对应的 zset 中
|
||||
if (!rootId.equals(0)) {
|
||||
redisUtil.zset("comment_reply:" + rootId, comment.getId());
|
||||
} else {
|
||||
redisUtil.zset("comment_video:"+ vid, comment.getId());
|
||||
}
|
||||
// 表示被回复的用户收到的回复评论的 id 有序集合
|
||||
// 如果不是回复自己
|
||||
if(!Objects.equals(comment.getToUserId(), comment.getUid())) {
|
||||
redisUtil.zset("reply_zset:" + comment.getToUserId(), comment.getId());
|
||||
msgUnreadService.addOneUnread(comment.getToUserId(), "reply");
|
||||
|
||||
// netty 通知未读消息
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("type", "接收");
|
||||
Set<Channel> myChannels = IMServer.userChannel.get(comment.getToUserId());
|
||||
if (myChannels != null) {
|
||||
for (Channel channel: myChannels) {
|
||||
channel.writeAndFlush(IMResponse.message("reply", map));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, taskExecutor);
|
||||
} catch (Exception e) {
|
||||
log.error("发送评论过程中出现一点差错");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return commentTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
* @param id 评论id
|
||||
* @param uid 当前用户id
|
||||
* @param isAdmin 是否是管理员
|
||||
* @return 响应对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public CustomResponse deleteComment(Integer id, Integer uid, boolean isAdmin) {
|
||||
CustomResponse customResponse = new CustomResponse();
|
||||
QueryWrapper<Comment> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("id", id).ne("is_deleted", 1);
|
||||
Comment comment = commentMapper.selectOne(queryWrapper);
|
||||
if (comment == null) {
|
||||
customResponse.setCode(404);
|
||||
customResponse.setMessage("评论不存在");
|
||||
return customResponse;
|
||||
}
|
||||
|
||||
// 判断该用户是否有权限删除这条评论
|
||||
Video video = videoMapper.selectById(comment.getVid());
|
||||
if (Objects.equals(comment.getUid(), uid) || isAdmin || Objects.equals(video.getUid(), uid)) {
|
||||
// 删除评论
|
||||
UpdateWrapper<Comment> commentWrapper = new UpdateWrapper<>();
|
||||
commentWrapper.eq("id", comment.getId()).set("is_deleted", 1);
|
||||
commentMapper.update(null, commentWrapper);
|
||||
|
||||
/*
|
||||
如果该评论是根节点评论,则删掉其所有回复。
|
||||
如果不是根节点评论,则将他所在的 comment_reply(zset) 中的 comment_id 删掉
|
||||
*/
|
||||
if (Objects.equals(comment.getRootId(), 0)) {
|
||||
// 查询总共要减少多少评论数
|
||||
int count = Math.toIntExact(redisUtil.zCard("comment_reply:" + comment.getId()));
|
||||
videoStatsService.updateStats(comment.getVid(), "comment", false, count + 1);
|
||||
redisUtil.zsetDelMember("comment_video:" + comment.getVid(), comment.getId());
|
||||
redisUtil.delValue("comment_reply:" + comment.getId());
|
||||
} else {
|
||||
videoStatsService.updateStats(comment.getVid(), "comment", false, 1);
|
||||
redisUtil.zsetDelMember("comment_reply:" + comment.getRootId(), comment.getId());
|
||||
}
|
||||
|
||||
customResponse.setCode(200);
|
||||
customResponse.setMessage("删除成功!");
|
||||
} else {
|
||||
customResponse.setCode(403);
|
||||
customResponse.setMessage("你无权删除该条评论");
|
||||
}
|
||||
return customResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rootId 根级节点的评论 id, 即楼层 id
|
||||
* @param vid 视频的 vid
|
||||
* @return 1. 根据 redis 查找出回复该评论的子评论 id 列表
|
||||
* 2. 根据 id 多线程查询出所有评论的详细信息
|
||||
*/
|
||||
@Override
|
||||
public List<Comment> getChildCommentsByRootId(Integer rootId, Integer vid, Long start, Long stop) {
|
||||
Set<Object> replyIds = redisUtil.zRange("comment_reply:" + rootId, start, stop);
|
||||
|
||||
if (replyIds == null || replyIds.isEmpty()) return Collections.emptyList();
|
||||
|
||||
QueryWrapper<Comment> wrapper = new QueryWrapper<>();
|
||||
wrapper.in("id", replyIds).ne("is_deleted", 1);
|
||||
|
||||
return commentMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据视频 vid 获取根评论列表,一次查 10 条
|
||||
* @param vid 视频 id
|
||||
* @param offset 偏移量,已经获取到的根评论数量
|
||||
* @param type 1:按热度排序 2:按时间排序
|
||||
* @return List<Comment>
|
||||
*/
|
||||
@Override
|
||||
public List<Comment> getRootCommentsByVid(Integer vid, Long offset, Integer type) {
|
||||
Set<Object> rootIdsSet;
|
||||
if (type == 1) {
|
||||
// 按热度排序就不能用时间分数查偏移量了,要全部查出来,后续在MySQL筛选
|
||||
rootIdsSet = redisUtil.zReverange("comment_video:" + vid, 0L, -1L);
|
||||
} else {
|
||||
rootIdsSet = redisUtil.zReverange("comment_video:" + vid, offset, offset + 9L);
|
||||
}
|
||||
|
||||
if (rootIdsSet == null || rootIdsSet.isEmpty()) return Collections.emptyList();
|
||||
|
||||
QueryWrapper<Comment> wrapper = new QueryWrapper<>();
|
||||
wrapper.in("id", rootIdsSet).ne("is_deleted", 1);
|
||||
if (type == 1) { // 热度
|
||||
wrapper.orderByDesc("(love - bad)").last("LIMIT 10 OFFSET " + offset);
|
||||
} else { // 时间
|
||||
wrapper.orderByDesc("create_time");
|
||||
}
|
||||
return commentMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更多回复评论
|
||||
* @param id 根评论id
|
||||
* @return 包含全部回复评论的评论树
|
||||
*/
|
||||
@Override
|
||||
public CommentTree getMoreCommentsById(Integer id) {
|
||||
Comment comment = commentMapper.selectById(id);
|
||||
return buildCommentTree(comment, 0L, -1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同时相对更新点赞和点踩
|
||||
* @param id 评论id
|
||||
* @param addLike true 点赞 false 点踩
|
||||
*/
|
||||
@Override
|
||||
public void updateLikeAndDisLike(Integer id, boolean addLike) {
|
||||
UpdateWrapper<Comment> updateWrapper = new UpdateWrapper<>();
|
||||
if (addLike) {
|
||||
updateWrapper.setSql("love = love + 1, bad = CASE WHEN " +
|
||||
"bad - 1 < 0 " +
|
||||
"THEN 0 " +
|
||||
"ELSE bad - 1 END");
|
||||
} else {
|
||||
updateWrapper.setSql("bad = bad + 1, love = CASE WHEN " +
|
||||
"love - 1 < 0 " +
|
||||
"THEN 0 " +
|
||||
"ELSE love - 1 END");
|
||||
}
|
||||
|
||||
commentMapper.update(null, updateWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单独更新点赞或点踩
|
||||
* @param id 评论id
|
||||
* @param column "love" 点赞 "bad" 点踩
|
||||
* @param increase true 增加 false 减少
|
||||
* @param count 更改数量
|
||||
*/
|
||||
@Override
|
||||
public void updateComment(Integer id, String column, boolean increase, Integer count) {
|
||||
UpdateWrapper<Comment> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", id);
|
||||
if (increase) {
|
||||
updateWrapper.setSql(column + " = " + column + " + " + count);
|
||||
} else {
|
||||
// 更新后的字段不能小于0
|
||||
updateWrapper.setSql(column + " = CASE WHEN " + column + " - " + count + " < 0 THEN 0 ELSE " + column + " - " + count + " END");
|
||||
}
|
||||
commentMapper.update(null, updateWrapper);
|
||||
}
|
||||
}
|