commit f27d8cdb7d2dbe993cf27b5f95ceaad9b878dd2e Author: tangwenkang <2720983602@qq.com> Date: Mon Nov 6 19:03:35 2023 +0800 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9369b72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +logs +.idea +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7edcc5b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +#起始镜像 +FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:17-8.6 +#暴露端口号 +EXPOSE 10010 +#挂载目录的位置 +VOLUME /home/logs/health-video +#构建复制外部文件到docker +COPY health-video-server/target/health-video-server.jar /home/app.jar +#工作目录 exec -it 进入容器内部后的默认的起始目录 +WORKDIR /home +ENV TIME_ZONE Asia/Shanghai +#指定东八区 +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +#启动java 程序 +ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-jar","/home/app.jar"] + + diff --git a/health-video-common/pom.xml b/health-video-common/pom.xml new file mode 100644 index 0000000..999f5fa --- /dev/null +++ b/health-video-common/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.health + health-video + 3.6.3 + + 3.6.3 + health-video-common + + + 17 + 17 + UTF-8 + + + + + com.health + health-common-core + + + com.health + health-common-security + + + org.projectlombok + lombok + + + + + + dragon-public + dragon-maven + http://10.100.1.7:8081/repository/maven-public/ + + + + + dragon-release + dragon-releases + http://10.100.1.7:8081/repository/maven-releases/ + + + diff --git a/health-video-common/src/main/java/com/health/video/common/domain/Comments.java b/health-video-common/src/main/java/com/health/video/common/domain/Comments.java new file mode 100644 index 0000000..1beeff5 --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/Comments.java @@ -0,0 +1,37 @@ +package com.health.video.common.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/20 21:22 + */ +@Data +public class Comments { + /** + * 评论表主键 + */ + private Integer commentsId; + /** + * 评论内容 + */ + private String commentContent; + /** + * 发布评论用户ID + */ + private Integer commentsUserId; + /** + * 评论时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date createTime; + /** + * 视频表主键 + */ + private Integer videoId; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/Video.java b/health-video-common/src/main/java/com/health/video/common/domain/Video.java new file mode 100644 index 0000000..36349b5 --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/Video.java @@ -0,0 +1,54 @@ +package com.health.video.common.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/20 21:17 + */ +@Data +public class Video { + /** + * 视频表主键 + */ + private Integer videoId; + /** + * 标题 + */ + private String title; + /** + * 详情内容 + */ + private String details; + /** + * 完整视频 + */ + private String fullVideo; + /** + * 被购买数 + */ + private Integer purchaseNumber; + /** + * 视频分类表主键 + */ + private Integer videoTypeId; + /** + * 视频价格 + */ + private Integer price; + /** + * 发布视频的作者 + */ + private Integer founder; + /** + * 发布时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date releaseTime; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/VideoBuy.java b/health-video-common/src/main/java/com/health/video/common/domain/VideoBuy.java new file mode 100644 index 0000000..2fe288d --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/VideoBuy.java @@ -0,0 +1,41 @@ +package com.health.video.common.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/24 20:03 + */ +@Data +public class VideoBuy { + /** + * 视频购买表 + */ + private Integer videoBuyId; + /** + * 视频表主键 + */ + private Integer videoId; + /** + * 用户表主键 + */ + private Integer buyUserId; + /** + * 是否购买 1-是 + */ + private Integer isBuy; + /** + * 购买时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date buyTime; + /** + * 是否删除购买视频 1-是 2-否 + */ + private Integer isDeleteBuy; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/VideoCollection.java b/health-video-common/src/main/java/com/health/video/common/domain/VideoCollection.java new file mode 100644 index 0000000..adb300f --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/VideoCollection.java @@ -0,0 +1,41 @@ +package com.health.video.common.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/24 20:05 + */ +@Data +public class VideoCollection { + /** + * 视频收藏表主键 + */ + private Integer videoCollectionId; + /** + * 视频表主键 + */ + private Integer videoId; + /** + * 用户表主键 + */ + private Integer collectionUserId; + /** + * 是否收藏视频 1-是 + */ + private Integer isCollection; + /** + * 收藏时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date collectTime; + /** + * 是否删除收藏视频 1-是 2-否 + */ + private Integer isDeleteCollection; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/VideoType.java b/health-video-common/src/main/java/com/health/video/common/domain/VideoType.java new file mode 100644 index 0000000..b7d952e --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/VideoType.java @@ -0,0 +1,19 @@ +package com.health.video.common.domain; + +import lombok.Data; + +/** + * @author Wenkang Tang + * @date 2023/10/20 21:18 + */ +@Data +public class VideoType { + /** + * 视频分类表主键 + */ + private Integer videoTypeId; + /** + * 分类名称 + */ + private String videoTypeName; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/request/RequestInsertComments.java b/health-video-common/src/main/java/com/health/video/common/domain/request/RequestInsertComments.java new file mode 100644 index 0000000..4b00386 --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/request/RequestInsertComments.java @@ -0,0 +1,20 @@ +package com.health.video.common.domain.request; + +import lombok.Data; + +/** + * @author Wenkang Tang + * @date 2023/10/21 11:25 + * 发送评论请求参数 + */ +@Data +public class RequestInsertComments { + /** + * 视频表主键 + */ + private Integer videoId; + /** + * 评论内容 + */ + private String commentContent; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/request/RequestPostVideos.java b/health-video-common/src/main/java/com/health/video/common/domain/request/RequestPostVideos.java new file mode 100644 index 0000000..9a8702c --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/request/RequestPostVideos.java @@ -0,0 +1,51 @@ +package com.health.video.common.domain.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/31 13:27 + * 发布视频请求参数 + */ +@Data +public class RequestPostVideos { + /** + * 标题 + */ + private String title; + /** + * 详情内容 + */ + private String details; + /** + * 完整视频 + */ + private String fullVideo; + /** + * 被购买数 + */ + private Integer purchaseNumber; + /** + * 视频分类表主键 + */ + private Integer videoTypeId; + /** + * 视频价格 + */ + private BigDecimal price; + /** + * 发布视频的作者 + */ + private Integer founder; + /** + * 发布时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date releaseTime; +} diff --git a/health-video-common/src/main/java/com/health/video/common/domain/response/ResponseVideo.java b/health-video-common/src/main/java/com/health/video/common/domain/response/ResponseVideo.java new file mode 100644 index 0000000..2201d3e --- /dev/null +++ b/health-video-common/src/main/java/com/health/video/common/domain/response/ResponseVideo.java @@ -0,0 +1,103 @@ +package com.health.video.common.domain.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * @author Wenkang Tang + * @date 2023/10/20 21:45 + * 视频列表响应参数 + */ +@Data +public class ResponseVideo { + /** + * 视频表主键 + */ + private Integer videoId; + /** + * 标题 + */ + private String title; + /** + * 详情内容 + */ + private String details; + /** + * 完整视频 + */ + private String fullVideo; + /** + * 被购买数 + */ + private Integer purchaseNumber; + /** + * 视频分类表主键 + */ + private Integer videoTypeId; + /** + * 分类名称 + */ + private String videoTypeName; + /** + * 视频价格 + */ + private BigDecimal price; + /** + * 视频购买表 + */ + private Integer videoBuyId; + /** + * 是否购买 1-是 + */ + private Integer isBuy; + /** + * 购买时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date buyTime; + /** + * 是否删除购买视频 1-是 2-否 + */ + private Integer isDeleteBuy; + /** + * 视频收藏表主键 + */ + private Integer videoCollectionId; + /** + * 是否收藏视频 1-是 + */ + private Integer isCollection; + /** + * 收藏时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date collectTime; + /** + * 是否删除收藏视频 1-是 2-否 + */ + private Integer isDeleteCollection; + /** + * 发布视频的作者 + */ + private Integer founder; + /** + * 发布时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") + private Date releaseTime; + /** + * 收藏视频用户表主键 + */ + private Integer collectionUserId; + /** + * 购买视频用户表主键 + */ + private Integer buyUserId; +} diff --git a/health-video-remote/pom.xml b/health-video-remote/pom.xml new file mode 100644 index 0000000..95980a7 --- /dev/null +++ b/health-video-remote/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.health + health-video + 3.6.3 + + 3.6.3 + health-video-remote + + + 17 + 17 + UTF-8 + + + diff --git a/health-video-server/pom.xml b/health-video-server/pom.xml new file mode 100644 index 0000000..a28e433 --- /dev/null +++ b/health-video-server/pom.xml @@ -0,0 +1,197 @@ + + + 4.0.0 + + com.health + health-video + 3.6.3 + + 3.6.3 + health-video-server + + + 17 + 17 + UTF-8 + + + + + + com.health + health-video-common + 3.6.3 + + + com.health + health-wallet-common + 3.6.5 + + + com.health + health-wallet-remote + 3.6.5 + + + + com.health + health-common-security + 3.6.3 + + + + + cn.hutool + hutool-all + 4.5.16 + + + + com.health + base-file-remote + + + + com.health + base-system-common + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.springfox + springfox-swagger-ui + ${swagger.fox.version} + + + + + com.mysql + mysql-connector-j + + + + + com.health + health-common-datasource + + + + + com.health + health-common-datascope + + + + com.health + health-common-security + + + + com.amazonaws + aws-java-sdk-core + 1.11.813 + + + + software.amazon.awssdk + s3 + 2.18.30 + + + + com.amazonaws + aws-java-sdk-s3 + 1.11.813 + + + + org.springframework.boot + spring-boot-starter-amqp + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.redisson + redisson + 3.16.0 + + + + + + + + + dragon-public + dragon-maven + http://10.100.1.7:8081/repository/maven-public/ + + + + + dragon-release + dragon-releases + http://10.100.1.7:8081/repository/maven-releases/ + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + diff --git a/health-video-server/src/main/java/com/health/video/server/VideoApplication.java b/health-video-server/src/main/java/com/health/video/server/VideoApplication.java new file mode 100644 index 0000000..e54338d --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/VideoApplication.java @@ -0,0 +1,18 @@ +package com.health.video.server; + +import com.health.common.security.annotation.EnableRyFeignClients; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Wenkang Tang + * @date 2023/10/20 20:01 + */ +@SpringBootApplication +@EnableRyFeignClients +public class VideoApplication { + public static void main(String[] args) { + SpringApplication.run(VideoApplication.class); + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/config/ConfirmCallbackConfig.java b/health-video-server/src/main/java/com/health/video/server/config/ConfirmCallbackConfig.java new file mode 100644 index 0000000..f2d0b4e --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/config/ConfirmCallbackConfig.java @@ -0,0 +1,50 @@ +package com.health.video.server.config; + +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +/** + * 消息发送确认配置 消息发送到交换机的确认 + * + * @author 唐文康 + */ +@Component +public class ConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback { + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * @PostContruct是spring框架的注解,在⽅法上加该注解会在项⽬启动的时候执⾏该⽅法,也可以理解为在spring容器初始化的时候执 + * @PostConstruct bean 被初始化的时候执行的方法的注解 + * @PreDestory bean 被销毁的时候执行的方法的注解 + */ + @PostConstruct + public void init() { + rabbitTemplate.setConfirmCallback(this); + } + + /** + * 交换机不管是否收到消息的一个回调方法(生产者消息不丢失) + * + * @param correlationData 消息相关数据 + * @param ack 交换机是否收到消息 + * @param cause 失败原因 + */ + @Override + public void confirm(CorrelationData correlationData, boolean ack, String cause) { + if (ack) { + // 消息投递到 broker 的状态,true表示成功 + System.out.println("消息发送成功!"); + } else { + // 发送异常 + System.out.println("发送异常原因 = " + cause); + // TODO 可以将消息 内容 以及 失败的原因 记录到 日志表中 + } + } + +} diff --git a/health-video-server/src/main/java/com/health/video/server/config/RabbitAdminConfig.java b/health-video-server/src/main/java/com/health/video/server/config/RabbitAdminConfig.java new file mode 100644 index 0000000..f19002c --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/config/RabbitAdminConfig.java @@ -0,0 +1,60 @@ +package com.health.video.server.config; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * RabbitAdmin是RabbitMQ的一个Java客户端库,它提供了管理RabbitMQ资源的功能。它是通过与RabbitMQ服务器进行交互来执行管理操作的。 + * + * @author 唐文康 + */ +@Configuration +public class RabbitAdminConfig { + + @Value("${spring.rabbitmq.host}") + private String host; + @Value("${spring.rabbitmq.username}") + private String username; + @Value("${spring.rabbitmq.password}") + private String password; + @Value("${spring.rabbitmq.virtualhost}") + private String virtualhost; + + /** + * 构建 RabbitMQ的连接工厂 + * + * @return + */ + @Bean + public ConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + connectionFactory.setAddresses(host); + connectionFactory.setUsername(username); + connectionFactory.setPassword(password); + connectionFactory.setVirtualHost(virtualhost);//虚拟主机 + // 配置发送确认回调时,次配置必须配置,否则即使在RabbitTemplate配置了ConfirmCallback也不会生效 + connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);//开启confirm机制 消息到达MQ的确认机制 + connectionFactory.setPublisherReturns(true);//开启消息返回机制 消息无法送到Exchange时返回到生产者 + return connectionFactory; + } + + /** + * 自己初始化 RabbitAdmin + * + * @param connectionFactory + * @return + */ + @Bean + public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { + RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory); + rabbitAdmin.setAutoStartup(true); + return rabbitAdmin; + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/config/RabbitmqConfig.java b/health-video-server/src/main/java/com/health/video/server/config/RabbitmqConfig.java new file mode 100644 index 0000000..4ffe420 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/config/RabbitmqConfig.java @@ -0,0 +1,18 @@ +package com.health.video.server.config; + +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author 唐文康 + */ +@Configuration +public class RabbitmqConfig { + // 消息转换配置 + @Bean + public MessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/config/RedissonConfig.java b/health-video-server/src/main/java/com/health/video/server/config/RedissonConfig.java new file mode 100644 index 0000000..a0d1a08 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/config/RedissonConfig.java @@ -0,0 +1,29 @@ +package com.health.video.server.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + * @author 唐文康 + */ +@Configuration +@Component +public class RedissonConfig { + + @Bean(destroyMethod = "shutdown") // 服务停止后调用 shutdown 方法。 + public RedissonClient redisson() { + System.out.println("Redisson配置类初始加载......"); + // 1.创建配置 + Config config = new Config(); + // 集群模式 + // config.useClusterServers().addNodeAddress("127.0.0.1:6379", "127.0.0.1:6378"); + // 2.根据 Config 创建出 RedissonClient 实例。 + config.useSingleServer().setAddress("redis://10.100.1.2:6379"); + return Redisson.create(config); + } + +} diff --git a/health-video-server/src/main/java/com/health/video/server/config/ReturnCallbackConfig.java b/health-video-server/src/main/java/com/health/video/server/config/ReturnCallbackConfig.java new file mode 100644 index 0000000..df15e8f --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/config/ReturnCallbackConfig.java @@ -0,0 +1,41 @@ +package com.health.video.server.config; + +import org.springframework.amqp.core.ReturnedMessage; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; + +/** + * 消息发送到 队列的 确认 【 只有消息发送失败的时候执行 】 + * + * @author 唐文康 + */ +@Component +public class ReturnCallbackConfig implements RabbitTemplate.ReturnsCallback { + + @Autowired + private RabbitTemplate rabbitTemplate; + + /** + * @PostContruct是spring框架的注解,在⽅法上加该注解会在项⽬启动的时候执⾏该⽅法,也可以理解为在spring容器初始化的时候执 + */ + @PostConstruct + public void init() { + rabbitTemplate.setReturnsCallback(this); + } + + /** + * 只要消息发送失败 就会执行 + * + * @param returnedMessage the returned message and metadata. + */ + @Override + public void returnedMessage(ReturnedMessage returnedMessage) { + System.out.println("消息" + returnedMessage.getMessage().toString() + "被交换机" + returnedMessage.getExchange() + "回退!" + + "退回原因为:" + returnedMessage.getReplyText()); + // TODO 回退了所有的信息,可做补偿机制 记录日志 + } + +} diff --git a/health-video-server/src/main/java/com/health/video/server/controller/VideoController.java b/health-video-server/src/main/java/com/health/video/server/controller/VideoController.java new file mode 100644 index 0000000..05e212e --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/controller/VideoController.java @@ -0,0 +1,151 @@ +package com.health.video.server.controller; + +import com.health.common.core.domain.Result; +import com.health.video.common.domain.Comments; +import com.health.video.common.domain.VideoType; +import com.health.video.common.domain.request.RequestInsertComments; +import com.health.video.common.domain.request.RequestPostVideos; +import com.health.video.common.domain.response.ResponseVideo; +import com.health.video.server.service.VideoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + + +/** + * @author Wenkang Tang + * @date 2023/10/20 20:02 + */ +@RestController +@RequestMapping("/video") +public class VideoController { + @Autowired + private VideoService videoService; + + /** + * 查询视频类型列表 + * + * @return + */ + @GetMapping("/listVideoType") + public Result> listVideoType() { + List videoTypes = videoService.listVideoType(); + return Result.success(videoTypes, "操作成功!"); + } + + /** + * 视频列表 + * + * @param videoTypeId + * @return + */ + @PostMapping("/listVideo/{videoTypeId}") + public Result> listVideo(@PathVariable Integer videoTypeId) { + List responseVideos = videoService.listVideo(videoTypeId); + return Result.success(responseVideos, "操作成功!"); + } + + /** + * 发送评论 + * + * @param requestInsertComments + */ + @PostMapping("/insertComments") + public Result insertComments(@RequestBody RequestInsertComments requestInsertComments) { + videoService.insertComments(requestInsertComments); + return Result.success(null, "操作成功!"); + } + + /** + * 收藏视频 + * + * @param videoId + */ + @PostMapping("/insertVideoCollection/{videoId}") + public Result insertVideoCollection(@PathVariable Integer videoId) { + videoService.insertVideoCollection(videoId); + return Result.success(null, "操作成功!"); + } + + /** + * 购买视频 + * + * @param videoId + */ + @PostMapping("/insertVideoBuy/{videoId}") + public void insertVideoBuy(@PathVariable Integer videoId) { + videoService.insertVideoBuy(videoId); + } + + /** + * 查询我收藏的视频 + * + * @param collectionUserId + * @return + */ + @PostMapping("/listVideoCollection/{collectionUserId}") + public Result> listVideoCollection(@PathVariable Integer collectionUserId) { + List responseVideos = videoService.listVideoCollection(collectionUserId); + return Result.success(responseVideos, "操作成功!"); + } + + /** + * 删除我收藏的视频 + * + * @param videoCollectionId + */ + @PostMapping("/deleteVideoCollection/{videoCollectionId}") + public Result deleteVideoCollection(@PathVariable Integer videoCollectionId) { + videoService.deleteVideoCollection(videoCollectionId); + return Result.success(null, "操作成功!"); + } + + /** + * 查询我购买的视频 + * + * @param buyUserId + * @return + */ + @PostMapping("/listVideoBuy/{buyUserId}") + public Result> listVideoBuy(@PathVariable Integer buyUserId) { + List responseVideos = videoService.listVideoBuy(buyUserId); + return Result.success(responseVideos, "操作成功!"); + } + + /** + * 上传视频 + * + * @param file + * @return + */ + @PostMapping("/uploadVideo") + public Result uploadVideo(@RequestParam MultipartFile file) throws IOException, InterruptedException { + return videoService.uploadVideo(file); + } + + /** + * 发布视频 + * + * @param requestPostVideos + */ + @PostMapping("/PostVideos") + public Result postVideos(@RequestBody RequestPostVideos requestPostVideos) { + return videoService.postVideos(requestPostVideos); + } + + /** + * 查询评论列表 + * + * @param videoId + * @return + */ + @PostMapping("/listComments/{videoId}") + public Result> listComments(@PathVariable Integer videoId) { + List commentsList = videoService.listComments(videoId); + return Result.success(commentsList, "操作成功!"); + } + +} diff --git a/health-video-server/src/main/java/com/health/video/server/mapper/VideoMapper.java b/health-video-server/src/main/java/com/health/video/server/mapper/VideoMapper.java new file mode 100644 index 0000000..91776b0 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/mapper/VideoMapper.java @@ -0,0 +1,111 @@ +package com.health.video.server.mapper; + +import com.health.video.common.domain.*; +import com.health.video.common.domain.request.RequestPostVideos; +import com.health.video.common.domain.response.ResponseVideo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * @author Wenkang Tang + * @date 2023/10/20 20:03 + */ +@Mapper +public interface VideoMapper { + /** + * 查询视频类型列表 + * + * @return + */ + List listVideoType(); + + /** + * 视频列表 + * + * @return + */ + List listVideo(@Param("videoTypeId") Integer videoTypeId); + + /** + * 发送评论 + * + * @param comments + */ + void insertComments(Comments comments); + + /** + * 根据videoId查询视频表 + * + * @param videoId + */ + Video getVideoByVideoId(@Param("videoId") Integer videoId); + + + /** + * 收藏视频 + * + * @param videoCollection + */ + void insertVideoCollection(VideoCollection videoCollection); + + /** + * 视频添加视频购买列表中 + * + * @param videoBuy + */ + void insertVideoBuy(VideoBuy videoBuy); + + /** + * 查询我收藏的视频 + * + * @param collectionUserId + * @return + */ + List listVideoCollection(@Param("collectionUserId") Integer collectionUserId); + + /** + * 删除我收藏的视频 + * + * @param videoCollection + */ + void deleteVideoCollection(VideoCollection videoCollection); + + /** + * 查询我购买的视频 + * + * @param buyUserId + * @return + */ + List listVideoBuy(@Param("buyUserId") Integer buyUserId); + + /** + * 发布视频 + * + * @param requestPostVideos + */ + void postVideos(RequestPostVideos requestPostVideos); + + /** + * 查询评论列表 + * + * @param videoId + * @return + */ + List listComments(@Param("videoId") Integer videoId); + + /** + * 修改购买后的视频购买总人数 + * + * @param videoId + */ + void updateVideo(@Param("videoId") Integer videoId); + + /** + * 查询视频价格 + * @param videoId + * @return + */ + Video getVideoPrice(@Param("videoId") Integer videoId); +} diff --git a/health-video-server/src/main/java/com/health/video/server/service/VideoService.java b/health-video-server/src/main/java/com/health/video/server/service/VideoService.java new file mode 100644 index 0000000..df79a24 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/service/VideoService.java @@ -0,0 +1,102 @@ +package com.health.video.server.service; + +import com.health.common.core.domain.Result; +import com.health.video.common.domain.Comments; +import com.health.video.common.domain.VideoType; +import com.health.video.common.domain.request.RequestInsertComments; +import com.health.video.common.domain.request.RequestPostVideos; +import com.health.video.common.domain.response.ResponseVideo; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; + +/** + * @author Wenkang Tang + * @date 2023/10/20 20:03 + */ +public interface VideoService { + /** + * 查询视频类型列表 + * + * @return + */ + List listVideoType(); + + /** + * 视频列表 + * + * @param videoTypeId + * @return + */ + List listVideo(Integer videoTypeId); + + /** + * 发送评论 + * + * @param requestInsertComments + */ + void insertComments(RequestInsertComments requestInsertComments); + + /** + * 收藏视频 + * + * @param videoId + */ + void insertVideoCollection(Integer videoId); + + /** + * 购买视频 + * + * @param videoId + */ + void insertVideoBuy(Integer videoId); + + /** + * 查询我收藏的视频 + * + * @param collectionUserId + * @return + */ + List listVideoCollection(Integer collectionUserId); + + /** + * 删除我收藏的视频 + * + * @param videoCollectionId + */ + void deleteVideoCollection(Integer videoCollectionId); + + /** + * 查询我购买的视频 + * + * @param buyUserId + * @return + */ + List listVideoBuy(Integer buyUserId); + + /** + * 上传视频 + * + * @param file + * @return + */ + Result uploadVideo(MultipartFile file) throws InterruptedException, IOException; + + /** + * 发布视频 + * + * @param requestPostVideos + * @return + */ + Result postVideos(RequestPostVideos requestPostVideos); + + /** + * 查询评论列表 + * + * @param videoId + * @return + */ + List listComments(Integer videoId); + +} diff --git a/health-video-server/src/main/java/com/health/video/server/service/impl/InsertUserVideoQueue.java b/health-video-server/src/main/java/com/health/video/server/service/impl/InsertUserVideoQueue.java new file mode 100644 index 0000000..ecd4add --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/service/impl/InsertUserVideoQueue.java @@ -0,0 +1,48 @@ +package com.health.video.server.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.health.video.common.domain.VideoBuy; +import com.health.video.server.mapper.VideoMapper; +import com.rabbitmq.client.Channel; +import lombok.extern.log4j.Log4j2; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +/** + * @author Wenkang Tang + * @date 2023/10/24 10:51 + */ +@Log4j2 +@Service +public class InsertUserVideoQueue { + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private VideoMapper videoMapper; + + @RabbitListener(queuesToDeclare = {@Queue("insert_video_buy")}) + public void insertUserVideo(String msg, Message message, Channel channel) { + try { + log.info("消费者接收到消息,消息是:{}", msg); + String messageId = message.getMessageProperties().getMessageId(); + Long insertUserVideo = redisTemplate.opsForSet().add("insert_video_buy", messageId);//消息不重复 + if (null != insertUserVideo && 0 < insertUserVideo) { + VideoBuy videoBuy = JSONObject.parseObject(msg, VideoBuy.class); + videoMapper.insertVideoBuy(videoBuy); + channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//消息成功消费后发送消息给生产者 + log.info("消费者成功消费消息,消息是:{}", msg); + } + } catch (Exception e) { + log.error("消费者消费消息异常,消息内容是:{},异常信息是:{}", msg, e); + try { + channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);//消费失败 消息回退到队列 ,其他消费者继续消费 + } catch (Exception ex) { + log.error("消费者回退消息异常,消息内容是:{},异常信息是:{}", msg, ex); + } + } + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/service/impl/VideoServiceImpl.java b/health-video-server/src/main/java/com/health/video/server/service/impl/VideoServiceImpl.java new file mode 100644 index 0000000..97b996a --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/service/impl/VideoServiceImpl.java @@ -0,0 +1,334 @@ +package com.health.video.server.service.impl; + +import com.health.common.core.constant.SecurityConstants; +import com.health.wallet.common.domain.req.MoneyChangeRecordReq; +import com.alibaba.fastjson.JSONObject; +import com.amazonaws.AmazonClientException; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicSessionCredentials; +import com.amazonaws.client.builder.AwsClientBuilder; +import com.amazonaws.event.ProgressEventType; +import com.amazonaws.event.ProgressListener; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.TransferManagerBuilder; +import com.amazonaws.services.s3.transfer.Upload; +import com.health.common.core.domain.Result; +import com.health.common.security.utils.SecurityUtils; +import com.health.video.common.domain.*; +import com.health.video.common.domain.request.RequestInsertComments; +import com.health.video.common.domain.request.RequestPostVideos; +import com.health.video.common.domain.response.ResponseVideo; +import com.health.video.server.mapper.VideoMapper; +import com.health.video.server.service.VideoService; +import com.health.wallet.remote.RemoteWalletService; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import com.health.video.server.utils.DogeUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * @author Wenkang Tang + * @date 2023/10/20 20:03 + */ +@Service +@SuppressWarnings("ALL") +public class VideoServiceImpl implements VideoService { + @Autowired + private VideoMapper videoMapper; + @Autowired + private RabbitTemplate rabbitTemplate; + @Autowired + private RedissonClient redissonClient; + @Autowired + private RemoteWalletService remoteWalletService; + + /** + * 查询视频类型列表 + * + * @return + */ + @Override + public List listVideoType() { + return videoMapper.listVideoType(); + } + + /** + * 视频列表 + * + * @return + */ + @Override + public List listVideo(Integer videoTypeId) { + return videoMapper.listVideo(videoTypeId); + } + + /** + * 发送评论 + * + * @param requestInsertComments + */ + @Override + public void insertComments(RequestInsertComments requestInsertComments) { + Comments comments = new Comments();//创建Comments对象 + //赋值 + comments.setCommentsUserId(Math.toIntExact(SecurityUtils.getUserId())); + comments.setCommentContent(requestInsertComments.getCommentContent()); + comments.setVideoId(requestInsertComments.getVideoId()); + videoMapper.insertComments(comments); + + } + + /** + * 收藏视频 + * + * @param videoId + */ + @Override + public void insertVideoCollection(Integer videoId) { + VideoCollection videoCollection = new VideoCollection();//创建VideoCollection对象 + videoCollection.setVideoId(videoId); + videoCollection.setCollectionUserId(Math.toIntExact(SecurityUtils.getUserId()));//获取当前登录人ID给用户ID赋值 + videoCollection.setIsCollection(1); + videoCollection.setIsDeleteCollection(2); + videoMapper.insertVideoCollection(videoCollection); + } + + /** + * 购买视频 + * + * @param videoId + */ + @Override + @Transactional + public void insertVideoBuy(Integer videoId) { + Video video = videoMapper.getVideoPrice(videoId);//查询视频价格 + //创建分布式锁对象 + RLock myLock = redissonClient.getLock("myLock"); + try { + //加锁 最多等待5秒 10秒后自动释放 + boolean tryLock = myLock.tryLock(5, 10, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(tryLock)) { //获取锁成功 + //创建VideoBuy对象 + VideoBuy videoBuy = new VideoBuy(); + videoBuy.setVideoId(videoId); + videoBuy.setIsBuy(1); + videoBuy.setIsDeleteBuy(2); + videoBuy.setBuyUserId(Math.toIntExact(SecurityUtils.getUserId())); + + //rabbitMq发送消息 + rabbitTemplate.convertAndSend("insert_video_buy", JSONObject.toJSONString(videoBuy), msg -> { + msg.getMessageProperties().setMessageId(UUID.randomUUID().toString().replaceAll("-", "")); + msg.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);//生产者开启消息持久化 + return msg; + }); + + //修改用户余额 + MoneyChangeRecordReq moneyChangeRecordReq = new MoneyChangeRecordReq(); + moneyChangeRecordReq.setChangeAmount(video.getPrice()); + moneyChangeRecordReq.setChangeOrigin("视频购买"); + moneyChangeRecordReq.setChangeType(0); + moneyChangeRecordReq.setCreateUser(SecurityUtils.getUserId()); + remoteWalletService.moneyChangeByOtherOperate(moneyChangeRecordReq, SecurityConstants.INNER); + + //异步修改购买后的视频购买总人数 + CompletableFuture.runAsync(()->{ + videoMapper.updateVideo(videoId); + }); + } else {//获取锁失败 + throw new RuntimeException("等待时间过长,请重试!"); + } + } catch (Exception e) { + throw new RuntimeException("异常内容:"+e); + } finally { + //释放锁 + myLock.unlock(); + } + } + + /** + * 查询我收藏的视频 + * + * @param collectionUserId + * @return + */ + @Override + public List listVideoCollection(Integer collectionUserId) { + return videoMapper.listVideoCollection(collectionUserId); + } + + /** + * 删除我收藏的视频 + * + * @param videoCollectionId + */ + @Override + public void deleteVideoCollection(Integer videoCollectionId) { + VideoCollection videoCollection = new VideoCollection();//创建VideoCollection对象 + videoCollection.setVideoCollectionId(videoCollectionId); + videoCollection.setIsCollection(2); + videoCollection.setIsDeleteCollection(1); + videoMapper.deleteVideoCollection(videoCollection); + } + + /** + * 查询我购买的视频 + * + * @param buyUserId + * @return + */ + @Override + public List listVideoBuy(Integer buyUserId) { + return videoMapper.listVideoBuy(buyUserId); + } + + /** + * 上传视频 + * + * @param file + * @return + */ + @Override + public Result uploadVideo(MultipartFile file) { + // 初始化JSONObject对象 + JSONObject vodConfig = new JSONObject(); + // 获取文件名称 + String originalFilename = file.getOriginalFilename(); + int dotIndex = originalFilename.lastIndexOf("."); + String result = originalFilename.substring(0, dotIndex); + // 获取文件后缀,因此此后端代码可接收一切文件,上传格式前端限定 + String fileExt = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".") + 1).toLowerCase(); + // 重构文件名称 + String newVideoName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileExt;// 新的文件名 + + vodConfig.put("filename", originalFilename + fileExt); // 此参数不重要,只要能保留文件后缀名如 a.mp4 即可 + vodConfig.put("vn", newVideoName); // .. . 分组配置、回调参数 + JSONObject body = new JSONObject();// 构建body对象 + body.put("channel", "VOD_UPLOAD"); + body.put("vodConfig", vodConfig); + + JSONObject api = DogeUtil.dogeAPIGet("/auth/tmp_token.json", body);//获取临时密码和上传信息 + JSONObject credentials = api.getJSONObject("Credentials"); // 包含本次上传视频使用的临时密钥,下面初始化 S3 实例时会用到 + JSONObject uploadInfo = api.getJSONObject("VodUploadInfo"); // 包含本次上传视频的目标信息(s3Bucket、s3Endpoint、key,系统提供,无需修改)以及本次上传的唯一 ID:did ,下面上传时会用到 + System.out.println(uploadInfo); + + // 使用临时密钥信息初始化 BasicSessionCredentials 对象 + BasicSessionCredentials awsCredentials = new BasicSessionCredentials( + credentials.getString("accessKeyId"), + credentials.getString("secretAccessKey"), + credentials.getString("sessionToken")); + + // 使用上传信息中的S3配置信息和认证信息初始化 AmazonS3 对象 + AmazonS3 s3 = AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials)). + withEndpointConfiguration(new AwsClientBuilder. + EndpointConfiguration(uploadInfo.getString("s3Endpoint"), "automatic")).build(); + try { + // 利用 s3 初始化,并配置上传进度回调 + TransferManager tm = TransferManagerBuilder + .standard() + .withS3Client(s3) + .build(); + + // 获取上传的文件名 + String filename = file.getOriginalFilename(); + // 设置本地存储路径 + String localPath = "/tmp/tomcat.10010.12973406643068623510/work/Tomcat/localhost/ROOT"; + // 拼接本地文件路径,使用了文件分隔符来保证路径的正确性 + String filePath = localPath + File.separator + "D:\\uploadVideo" + File.separator + filename; + // 创建本地文件对象 + File localFile = new File(filePath); + // 检查目录是否存在,如果不存在则创建 + File localDirectory = localFile.getParentFile(); + if (!localDirectory.exists()) { + localDirectory.mkdirs(); + } + // 将上传的文件保存到本地文件 + file.transferTo(localFile); + // 获取本地文件的大小 + long fileSize = localFile.length(); + + System.out.println("开始分片上传"); + + // 指定上传的目标空间路径、目标文件名和本地文件 + PutObjectRequest req = new PutObjectRequest( + uploadInfo.getString("s3Bucket"), + uploadInfo.getString("key"), + localFile); + // 目标空间路径,由上面获取到的 VodUploadInfo 指定,不可更改 + + // 设置各过程进度回调 + req.setGeneralProgressListener(new ProgressListener() { + Long totalByteRead = 0L; + + @Override + public void progressChanged(com.amazonaws.event.ProgressEvent progressEvent) { + if (progressEvent.getEventType() == ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT && totalByteRead < fileSize) { + totalByteRead += progressEvent.getBytesTransferred(); + System.out.println("传输中:" + totalByteRead + "/" + fileSize + " (" + ((totalByteRead * 100.0F / fileSize)) + "%)"); + } else if (progressEvent.getEventType() == ProgressEventType.TRANSFER_COMPLETED_EVENT) { + System.out.println("传输完成"); + } else if (progressEvent.getEventType() == ProgressEventType.TRANSFER_FAILED_EVENT) { + System.out.println("传输失败"); + } + } + }); + // TransferManager 的处理默认是异步的,下面的 upload 方法调用完成后不会等待 + // 调用 tm.upload(req) 方法开始上传文件 + Upload upload = tm.upload(req); + // 不过,你也可以使用 waitForCompletion 来等待上传完成 + upload.waitForCompletion(); + System.out.println("分片上传完成"); + // 向多吉云汇报此次上传已完成并获得本次上传的视频 ID,did 参数来自上面获取临时密钥时获取到的 VodUploadInfo 。 + JSONObject cbapi = DogeUtil.dogeAPIGet("/callback/upload.json?did=" + uploadInfo.getString("did")); + System.out.println(cbapi); + Integer vid = cbapi.getInteger("vid"); + System.out.println("上传完成,视频 ID:" + vid); + // 获取视频的详细信息 + JSONObject msg = DogeUtil.dogeAPIGet("/video/info.json?vid=" + vid); + System.out.println("视频详细信息:" + msg); + return Result.success(msg.get("vcode"), "操作成功!"); + } catch (AmazonClientException | InterruptedException | IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 发布视频 + * + * @param requestPostVideos + * @return + */ + @Override + public Result postVideos(RequestPostVideos requestPostVideos) { + Video video = new Video();// 创建Video对象 + video.setFounder(Math.toIntExact(SecurityUtils.getUserId())); + //发布视频 + videoMapper.postVideos(requestPostVideos); + return Result.success(null, "成功发布视频!"); + } + + /** + * 查询评论列表 + * + * @param videoId + * @return + */ + @Override + public List listComments(Integer videoId) { + return videoMapper.listComments(videoId); + } + +} diff --git a/health-video-server/src/main/java/com/health/video/server/utils/DLXQueue.java b/health-video-server/src/main/java/com/health/video/server/utils/DLXQueue.java new file mode 100644 index 0000000..65cf13a --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/utils/DLXQueue.java @@ -0,0 +1,77 @@ +package com.health.video.server.utils; + +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.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +@Component +public class DLXQueue { + // routingKey + private static final String DEAD_ROUTING_KEY = "dead.routingkey"; + private static final String ROUTING_KEY = "routingkey"; + private static final String DEAD_EXCHANGE = "dead.exchange"; + private static final String EXCHANGE = "common.exchange"; + @Autowired + RabbitTemplate rabbitTemplate; + @Resource + RabbitAdmin rabbitAdmin; + + /** + * 发送死信队列,过期后进入死信交换机,进入死信队列 + * + * @param queueName 队列名称 + * @param deadQueueName 死信队列名称 + * @param params 消息内容 + * @param expiration 过期时间 毫秒 + */ + public void sendDLXQueue(String queueName, String deadQueueName, Object params, Integer expiration) { + /** + * ----------------------------------先创建一个ttl队列和死信队列-------------------------------------------- + */ + Map map = new HashMap<>(); + // 队列设置存活时间,单位ms, 必须是整形数据。 + map.put("x-message-ttl", expiration); + // 设置死信交换机 + map.put("x-dead-letter-exchange", DEAD_EXCHANGE); + // 设置死信交换器路由 + map.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY); + /*参数1:队列名称 参数2:持久化 参数3:是否排他 参数4:自动删除队列 参数5:队列参数*/ + Queue queue = new Queue(queueName, true, false, false, map); + rabbitAdmin.declareQueue(queue); + /** + * ---------------------------------创建交换机--------------------------------------------- + */ + DirectExchange directExchange = new DirectExchange(EXCHANGE, true, false); + rabbitAdmin.declareExchange(directExchange); + /** + * ---------------------------------队列绑定交换机--------------------------------------------- + */ + Binding binding = BindingBuilder.bind(queue).to(directExchange).with(ROUTING_KEY); + rabbitAdmin.declareBinding(binding); + /** + * ---------------------------------在创建一个死信交换机和队列,接收死信队列--------------------------------------------- + */ + DirectExchange deadExchange = new DirectExchange(DEAD_EXCHANGE, true, false); + rabbitAdmin.declareExchange(deadExchange); + + Queue deadQueue = new Queue(deadQueueName, true, false, false); + rabbitAdmin.declareQueue(deadQueue); + /** + * ---------------------------------队列绑定死信交换机--------------------------------------------- + */ + // 将队列和交换机绑定 + Binding deadbinding = BindingBuilder.bind(deadQueue).to(deadExchange).with(DEAD_ROUTING_KEY); + rabbitAdmin.declareBinding(deadbinding); + // 发送消息 + rabbitTemplate.convertAndSend(EXCHANGE, ROUTING_KEY, params); + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/utils/DelayedQueue.java b/health-video-server/src/main/java/com/health/video/server/utils/DelayedQueue.java new file mode 100644 index 0000000..96512fb --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/utils/DelayedQueue.java @@ -0,0 +1,80 @@ +package com.health.video.server.utils; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.CustomExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 发送延迟队列的工具类 + * @author 唐文康 + */ +@Component +public class DelayedQueue { + + // routingKey + private static final String DELAYED_ROUTING_KEY = "delayed.routingkey"; + + // 延迟队列交换机 + private static final String DELAYED_EXCHANGE = "delayed.exchange"; + + @Autowired + RabbitTemplate rabbitTemplate; + + @Resource + RabbitAdmin rabbitAdmin; + + /** + * 发送延迟队列 + * + * @param queueName 队列名称 + * @param params 消息内容 + * @param expiration 延迟时间 毫秒 + */ + public void sendDelayedQueue(String queueName, Object params, Integer expiration) { + // 先创建一个队列 + Queue queue = new Queue(queueName); + rabbitAdmin.declareQueue(queue); + + // 创建延迟队列交换机 + CustomExchange customExchange = createCustomExchange(); + rabbitAdmin.declareExchange(customExchange); + + // 将队列和交换机绑定 + Binding binding = BindingBuilder.bind(queue).to(customExchange).with(DELAYED_ROUTING_KEY).noargs(); + rabbitAdmin.declareBinding(binding); + + // 发送延迟消息 + rabbitTemplate.convertAndSend(DELAYED_EXCHANGE, DELAYED_ROUTING_KEY, params, msg -> { + // 发送消息的时候 延迟时长 + msg.getMessageProperties().setMessageId(UUID.randomUUID().toString().replaceAll("-", "")); + msg.getMessageProperties().setDelay(expiration); + return msg; + }); + } + + private CustomExchange createCustomExchange() { + Map arguments = new HashMap<>(); + /** + * 参数说明: + * 1.交换机的名称 + * 2.交换机的类型 + * 3.是否需要持久化 + * 4.是否自动删除 + * 5.其它参数 + */ + arguments.put("x-delayed-type", "direct"); + return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, arguments); + } + +} + diff --git a/health-video-server/src/main/java/com/health/video/server/utils/DogeUtil.java b/health-video-server/src/main/java/com/health/video/server/utils/DogeUtil.java new file mode 100644 index 0000000..7dd0f20 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/utils/DogeUtil.java @@ -0,0 +1,114 @@ +package com.health.video.server.utils; + +import com.alibaba.fastjson.JSONObject; +import org.apache.commons.codec.binary.Hex; +import org.apache.http.impl.io.ChunkedOutputStream; +import org.apache.http.io.SessionOutputBuffer; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +/** + * @author Wenkang Tang + * @date 2023/10/27 20:33 + */ +@Configuration +@Component +public class DogeUtil { + // 普通 API 请使用这个方法 + public static JSONObject dogeAPIGet(String apiPath, Map params) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry hm : params.entrySet()) { + try { + sb.append(URLEncoder.encode(hm.getKey(), String.valueOf(StandardCharsets.UTF_8))).append('=').append(URLEncoder.encode(hm.getValue(), String.valueOf(StandardCharsets.UTF_8))).append("&"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + String bodyText = sb.toString().replace("&$", ""); + try { + return dogeAPIGet(apiPath, bodyText, false); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + // 要求请求内容 Body 是一个 JSON 的 API,请使用这个方法 + public static JSONObject dogeAPIGet(String apiPath, JSONObject params) { + String bodyText = params.toString(); + try { + return dogeAPIGet(apiPath, bodyText, true); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + // 无参数 API + public static JSONObject dogeAPIGet(String apiPath) { + try { + return dogeAPIGet(apiPath, "", true); + } catch (IOException e) { + throw new RuntimeException(e.getMessage()); + } + } + + public static JSONObject dogeAPIGet(String apiPath, String paramsText, Boolean jsonMode) throws IOException { + // 这里返回值类型是 JSONObject,你也可以根据你的具体情况使用不同的 JSON 库并修改最下方 JSON 处理代码 + + // 这里替换为你的多吉云永久 AccessKey 和 SecretKey,可在用户中心 - 密钥管理中查看 + // 请勿在客户端暴露 AccessKey 和 SecretKey,那样恶意用户将获得账号完全控制权 + String accessKey = "02a4810cb73129c4"; + String secretKey = "4d9a1342fcee91d96698ce9425d5a23a"; + + String signStr = apiPath + "\n" + paramsText; + String sign = ""; + try { + Mac mac = Mac.getInstance("HmacSHA1"); + mac.init(new SecretKeySpec(secretKey.getBytes(), "HmacSHA1")); + sign = new String(new Hex().encode(mac.doFinal(signStr.getBytes())), StandardCharsets.UTF_8); // 这里 Hex 来自 org.apache.commons.codec.binary.Hex + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e.getMessage()); + } + String authorization = "TOKEN " + accessKey + ':' + sign; + + URL u = new URL("https://api.dogecloud.com" + apiPath); + HttpURLConnection conn = (HttpURLConnection) u.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", jsonMode ? "application/json" : "application/x-www-form-urlencoded"); + conn.setRequestProperty("Authorization", authorization); + conn.setRequestProperty("Content-Length", String.valueOf(paramsText.length())); + OutputStream os = conn.getOutputStream(); + os.write(paramsText.getBytes()); + os.flush(); + os.close(); + StringBuilder retJSON = new StringBuilder(); + if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { + String readLine = ""; + try (BufferedReader responseReader = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + while ((readLine = responseReader.readLine()) != null) { + retJSON.append(readLine).append("\n"); + } + } + JSONObject ret = JSONObject.parseObject(retJSON.toString()); + if (ret.getInteger("code") != 200) { + System.err.println("{\"error\":\"API 返回错误:" + ret.getString("msg") + "\"}"); + } else { + return ret.getJSONObject("data"); + } + } else { + System.err.println("{\"error\":\"网络错误:" + conn.getResponseCode() + "\"}"); + } + return null; + } +} diff --git a/health-video-server/src/main/java/com/health/video/server/utils/TtlQueue.java b/health-video-server/src/main/java/com/health/video/server/utils/TtlQueue.java new file mode 100644 index 0000000..9f2ce92 --- /dev/null +++ b/health-video-server/src/main/java/com/health/video/server/utils/TtlQueue.java @@ -0,0 +1,66 @@ +package com.health.video.server.utils; + +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.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * 发送TTL队列 设置 消息的存活时间 如果超过了存活时间 + * 该条消息还没有被消费 则自动从队列中消息 ,如果配置了死信队列则消息会进入死信队列 + */ +@Component +public class TtlQueue { + // routingKey + private static final String TTL_KEY = "ttl.routingkey"; + private static final String TTL_EXCHANGE = "ttl.exchange"; + + @Autowired + RabbitTemplate rabbitTemplate; + + @Resource + RabbitAdmin rabbitAdmin; + + /** + * 发送TTL队列 + * + * @param queueName 队列名称 + * @param params 消息内容 + * @param expiration 过期时间 毫秒 + */ + public void sendTtlQueue(String queueName, Object params, Integer expiration) { + /** + * ----------------------------------先创建一个ttl队列-------------------------------------------- + */ + Map map = new HashMap<>(); + // 队列设置存活时间,单位ms,必须是整形数据。 + map.put("x-message-ttl", expiration); + /*参数1:队列名称 参数2:持久化 参数3:是否排他 参数4:自动删除队列 参数5:队列参数*/ + Queue queue = new Queue(queueName, true, false, false, map); + rabbitAdmin.declareQueue(queue); + + /** + * ---------------------------------创建交换机--------------------------------------------- + */ + DirectExchange directExchange = new DirectExchange(TTL_EXCHANGE, true, false); + rabbitAdmin.declareExchange(directExchange); + /** + * ---------------------------------队列绑定交换机--------------------------------------------- + */ + // 将队列和交换机绑定 + Binding binding = BindingBuilder.bind(queue).to(directExchange).with(TTL_KEY); + rabbitAdmin.declareBinding(binding); + + // 发送消息 + rabbitTemplate.convertAndSend(TTL_EXCHANGE, TTL_KEY, params); + } +} + diff --git a/health-video-server/src/main/resources/bootstrap.yml b/health-video-server/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..243c3b9 --- /dev/null +++ b/health-video-server/src/main/resources/bootstrap.yml @@ -0,0 +1,57 @@ +# Tomcat +server: + port: 10010 + +# Spring +spring: + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + servlet: + multipart: + max-file-size: 50MB + max-request-size: 300MB + application: + # 应用名称 + name: health-video + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 10.100.1.5:8848 + config: + # 配置中心地址 + server-addr: 10.100.1.5:8848 + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} + rabbitmq: + host: 10.100.1.3 + port: 5072 + username: guest + password: guest + virtual-host: / + publisher-confirm-type: correlated #确认消息已发送到交换机(Exchange) + publisher-returns: true #确认消息已发送到队列(Queue) + listener: + simple: + prefetch: 1 # 每次只能获取一条,处理完成才能获取下一条 + acknowledge-mode: manual # 设置消费端手动ack确认 + retry: + enabled: true # 是否支持重试 + template: + # 只要消息抵达Queue,就会异步发送优先回调return firm + mandatory: true + redis: + host: 10.100.1.2 + # 关闭健康检查 +management: + health: + rabbit: + enabled: false + diff --git a/health-video-server/src/main/resources/logback.xml b/health-video-server/src/main/resources/logback.xml new file mode 100644 index 0000000..bff4d3f --- /dev/null +++ b/health-video-server/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + diff --git a/health-video-server/src/main/resources/mapper/VideoMapper.xml b/health-video-server/src/main/resources/mapper/VideoMapper.xml new file mode 100644 index 0000000..da58ca5 --- /dev/null +++ b/health-video-server/src/main/resources/mapper/VideoMapper.xml @@ -0,0 +1,138 @@ + + + + + + + INSERT INTO `health-video`.`t_comments` (`comments_id`, `comment_content`, `comments_user_id`, `create_time`, + `video_id`) + VALUES (0, #{commentContent}, #{commentsUserId}, NOW(), #{videoId}); + + + + + INSERT INTO `health-video`.`t_video_collection` (`video_collection_id`, `video_id`, `collection_user_id`, + `is_collection`, `collect_time`, `is_delete_collection`) + VALUES (0, #{videoId}, #{collectionUserId}, #{isCollection}, NOW(), #{isDeleteCollection}); + + + + + INSERT INTO `health-video`.`t_video` (`video_id`, `title`, `details`, `full_video`, `purchase_number`, + `video_type_id`, `price`, `founder`, `release_time`) + VALUES (0, #{title}, #{details}, #{fullVideo}, 0, #{videoTypeId}, #{price}, #{founder}, NOW()); + + + + + INSERT INTO `health-video`.`t_video_buy` (`video_buy_id`, `video_id`, `buy_user_id`, `is_buy`, `buy_time`, + `is_delete_buy`) + VALUES (0, #{videoId}, #{buyUserId}, #{isBuy}, now(), #{isDeleteBuy}); + + + + + UPDATE `health-video`.`t_video_collection` + SET `is_collection` = 2, + `is_delete_collection` = 1 + WHERE `video_collection_id` = #{videoCollectionId}; + + + + + UPDATE `health-video`.`t_video` + SET `purchase_number` = purchase_number + 1 + WHERE `video_id` = #{videoId}; + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..07153c4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + com.health + health-modules + 3.6.3 + + + health-video + 3.6.3 + pom + + + health-video-common + health-video-remote + health-video-server + + + + 17 + 17 + UTF-8 + + + + + dragon-public + dragon-maven + http://10.100.1.7:8081/repository/maven-public/ + + + + + dragon-release + dragon-releases + http://10.100.1.7:8081/repository/maven-releases/ + + + +