Sfoglia il codice sorgente

主播直播相关代码

Long 5 giorni fa
parent
commit
b0ce4bbc41
98 ha cambiato i file con 4717 aggiunte e 7 eliminazioni
  1. 30 0
      fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackLogController.java
  2. 58 0
      fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackTypeController.java
  3. 6 6
      fs-admin/src/main/resources/application-dev.yml
  4. 4 0
      fs-common/src/main/java/com/fs/common/core/domain/R.java
  5. 108 0
      fs-live-streamer/pom.xml
  6. 14 0
      fs-live-streamer/src/main/java/com/fs/FSServletInitializer.java
  7. 17 0
      fs-live-streamer/src/main/java/com/fs/FsLiveStreamApplication.java
  8. 12 0
      fs-live-streamer/src/main/java/com/fs/app/annotation/Login.java
  9. 37 0
      fs-live-streamer/src/main/java/com/fs/app/annotation/RateLimiter.java
  10. 23 0
      fs-live-streamer/src/main/java/com/fs/app/config/WebMvcConfig.java
  11. 13 0
      fs-live-streamer/src/main/java/com/fs/app/constant/LiveStreamConstant.java
  12. 52 0
      fs-live-streamer/src/main/java/com/fs/app/controller/AgreementController.java
  13. 34 0
      fs-live-streamer/src/main/java/com/fs/app/controller/AppBaseController.java
  14. 45 0
      fs-live-streamer/src/main/java/com/fs/app/controller/CommonController.java
  15. 52 0
      fs-live-streamer/src/main/java/com/fs/app/controller/FeedbackController.java
  16. 146 0
      fs-live-streamer/src/main/java/com/fs/app/controller/LiveController.java
  17. 169 0
      fs-live-streamer/src/main/java/com/fs/app/controller/LoginController.java
  18. 158 0
      fs-live-streamer/src/main/java/com/fs/app/controller/StreamerController.java
  19. 20 0
      fs-live-streamer/src/main/java/com/fs/app/enums/LimitType.java
  20. 39 0
      fs-live-streamer/src/main/java/com/fs/app/exception/FSException.java
  21. 90 0
      fs-live-streamer/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  22. 75 0
      fs-live-streamer/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  23. 12 0
      fs-live-streamer/src/main/java/com/fs/app/params/ChangeAvatarParam.java
  24. 18 0
      fs-live-streamer/src/main/java/com/fs/app/params/ChangePasswordParm.java
  25. 15 0
      fs-live-streamer/src/main/java/com/fs/app/params/LoginByPasswordParam.java
  26. 15 0
      fs-live-streamer/src/main/java/com/fs/app/params/LoginBySmsCodeParam.java
  27. 15 0
      fs-live-streamer/src/main/java/com/fs/app/params/LoginByWeChatParam.java
  28. 20 0
      fs-live-streamer/src/main/java/com/fs/app/params/SubmitFeedbackParam.java
  29. 68 0
      fs-live-streamer/src/main/java/com/fs/app/utils/JwtUtils.java
  30. 26 0
      fs-live-streamer/src/main/java/com/fs/app/utils/PasswordUtils.java
  31. 19 0
      fs-live-streamer/src/main/java/com/fs/app/utils/PatternUtils.java
  32. 18 0
      fs-live-streamer/src/main/java/com/fs/app/utils/VerifyCodeUtil.java
  33. 81 0
      fs-live-streamer/src/main/java/com/fs/app/utils/WxUtil.java
  34. 25 0
      fs-live-streamer/src/main/java/com/fs/framework/annotation/RepeatSubmit.java
  35. 197 0
      fs-live-streamer/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  36. 73 0
      fs-live-streamer/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  37. 117 0
      fs-live-streamer/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  38. 31 0
      fs-live-streamer/src/main/java/com/fs/framework/config/ApplicationConfig.java
  39. 71 0
      fs-live-streamer/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java
  40. 93 0
      fs-live-streamer/src/main/java/com/fs/framework/config/DataSourceConfig.java
  41. 72 0
      fs-live-streamer/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  42. 59 0
      fs-live-streamer/src/main/java/com/fs/framework/config/FilterConfig.java
  43. 109 0
      fs-live-streamer/src/main/java/com/fs/framework/config/MyBatisConfig.java
  44. 113 0
      fs-live-streamer/src/main/java/com/fs/framework/config/RedisConfig.java
  45. 65 0
      fs-live-streamer/src/main/java/com/fs/framework/config/ResourcesConfig.java
  46. 50 0
      fs-live-streamer/src/main/java/com/fs/framework/config/SecurityConfig.java
  47. 33 0
      fs-live-streamer/src/main/java/com/fs/framework/config/ServerConfig.java
  48. 124 0
      fs-live-streamer/src/main/java/com/fs/framework/config/SwaggerConfig.java
  49. 63 0
      fs-live-streamer/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  50. 77 0
      fs-live-streamer/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  51. 27 0
      fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  52. 45 0
      fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  53. 49 0
      fs-live-streamer/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  54. 116 0
      fs-live-streamer/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  55. 1 0
      fs-live-streamer/src/main/resources/META-INF/spring-devtools.properties
  56. 94 0
      fs-live-streamer/src/main/resources/application-dev.yml
  57. 110 0
      fs-live-streamer/src/main/resources/application.yml
  58. 2 0
      fs-live-streamer/src/main/resources/banner.txt
  59. 37 0
      fs-live-streamer/src/main/resources/i18n/messages.properties
  60. 15 0
      fs-live-streamer/src/main/resources/mybatis/mybatis-config.xml
  61. 21 0
      fs-live-streamer/src/main/resources/templates/privacyPolicy.html
  62. 21 0
      fs-live-streamer/src/main/resources/templates/streamAgreement.html
  63. 19 0
      fs-service-system/src/main/java/com/fs/common/config/SmsConfig.java
  64. 18 0
      fs-service-system/src/main/java/com/fs/common/domain/SendSmsByMY.java
  65. 14 0
      fs-service-system/src/main/java/com/fs/common/domain/SendSmsReturn.java
  66. 2 0
      fs-service-system/src/main/java/com/fs/common/service/ISmsService.java
  67. 144 0
      fs-service-system/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  68. 10 0
      fs-service-system/src/main/java/com/fs/company/domain/CompanySmsLogs.java
  69. 16 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveMapper.java
  70. 15 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveService.java
  71. 26 0
      fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  72. 30 0
      fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackLog.java
  73. 44 0
      fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackType.java
  74. 22 0
      fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackTypeLog.java
  75. 75 0
      fs-service-system/src/main/java/com/fs/liveStream/domain/LiveStreamer.java
  76. 20 0
      fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackLogMapper.java
  77. 14 0
      fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackTypeLogMapper.java
  78. 55 0
      fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackTypeMapper.java
  79. 34 0
      fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveStreamerMapper.java
  80. 17 0
      fs-service-system/src/main/java/com/fs/liveStream/param/LiveFeedbackLogParam.java
  81. 19 0
      fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackLogService.java
  82. 4 0
      fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackTypeLogService.java
  83. 42 0
      fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackTypeService.java
  84. 31 0
      fs-service-system/src/main/java/com/fs/liveStream/service/ILiveStreamerService.java
  85. 70 0
      fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackLogServiceImpl.java
  86. 10 0
      fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackTypeLogServiceImpl.java
  87. 116 0
      fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackTypeServiceImpl.java
  88. 57 0
      fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveStreamerServiceImpl.java
  89. 40 0
      fs-service-system/src/main/java/com/fs/liveStream/vo/LiveFeedbackLogVO.java
  90. 24 0
      fs-service-system/src/main/java/com/fs/liveStream/vo/LiveFeedbackTypeVO.java
  91. 24 0
      fs-service-system/src/main/java/com/fs/system/config/FsSmsConfig.java
  92. 5 1
      fs-service-system/src/main/resources/mapper/company/CompanySmsLogsMapper.xml
  93. 44 0
      fs-service-system/src/main/resources/mapper/live/LiveMapper.xml
  94. 54 0
      fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackLogMapper.xml
  95. 14 0
      fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackTypeLogMapper.xml
  96. 75 0
      fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackTypeMapper.xml
  97. 93 0
      fs-service-system/src/main/resources/mapper/liveStream/LiveStreamerMapper.xml
  98. 1 0
      pom.xml

+ 30 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackLogController.java

@@ -0,0 +1,30 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.liveStream.param.LiveFeedbackLogParam;
+import com.fs.liveStream.service.ILiveFeedbackLogService;
+import com.fs.liveStream.vo.LiveFeedbackLogVO;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/live/liveFeedbackLog")
+@AllArgsConstructor
+public class LiveFeedbackLogController extends BaseController {
+
+    private final ILiveFeedbackLogService liveFeedbackLogService;
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveFeedbackLogParam liveFeedbackLog) {
+        startPage();
+        List<LiveFeedbackLogVO> list = liveFeedbackLogService.selectLiveFeedbackLogList(liveFeedbackLog);
+        return getDataTable(list);
+    }
+}

+ 58 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveFeedbackTypeController.java

@@ -0,0 +1,58 @@
+package com.fs.live.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.liveStream.domain.LiveFeedbackType;
+import com.fs.liveStream.service.ILiveFeedbackTypeService;
+import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/live/liveFeedbackType")
+@AllArgsConstructor
+public class LiveFeedbackTypeController extends BaseController {
+
+    private final ILiveFeedbackTypeService liveFeedbackTypeService;
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveFeedbackType liveFeedbackType) {
+        List<LiveFeedbackType> list = liveFeedbackTypeService.selectLiveFeedbackTypeList(liveFeedbackType);
+        return getDataTable(list);
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(liveFeedbackTypeService.selectLiveFeedbackTypeById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:add')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.INSERT)
+    @PostMapping
+    @RepeatSubmit(intervalTime = 500)
+    public AjaxResult add(@RequestBody LiveFeedbackType liveFeedbackType) {
+        return toAjax(liveFeedbackTypeService.insertLiveFeedbackType(liveFeedbackType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:edit')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveFeedbackType liveFeedbackType) {
+        return toAjax(liveFeedbackTypeService.updateLiveFeedbackType(liveFeedbackType));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:liveFeedbackType:remove')")
+    @Log(title = "直播反馈类型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{typeIds}")
+    public AjaxResult remove(@PathVariable Long[] typeIds) {
+        return toAjax(liveFeedbackTypeService.deleteLiveFeedbackTypeByIds(typeIds));
+    }
+
+}

+ 6 - 6
fs-admin/src/main/resources/application-dev.yml

@@ -5,11 +5,11 @@ spring:
     # redis 配置
     redis:
         # 地址
-        host: 10.0.0.8
+        host: localhost
         # 端口,默认为6379
         port: 6379
         # 密码
-        password: Ylrz_1q2w3e4r5t6y
+        password:
         # 连接超时时间
         timeout: 10s
         lettuce:
@@ -29,15 +29,15 @@ spring:
         druid:
             # 主库数据源
             master:
-                url: jdbc:mysql://10.0.0.6:3306/fs_ffhx_shop?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                url: jdbc:mysql://42.194.245.189:3306/live_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
-                password: Ylrz_1q2w3e4r5t6y
+                password: YJF_2024
             # 从库数据源
             slave:
                 # 从数据源开关/默认关闭
-                url: jdbc:mysql://10.0.0.3:3306/fs_ffhx_shop?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                url: jdbc:mysql://42.194.245.189:3306/live_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                 username: root
-                password: Ylrz_1q2w3e4r5t6y
+                password: YJF_2024
             # 初始连接数
             initialSize: 5
             # 最小连接池数量

+ 4 - 0
fs-common/src/main/java/com/fs/common/core/domain/R.java

@@ -53,4 +53,8 @@ public class R extends HashMap<String, Object> {
 		super.put(key, value);
 		return this;
 	}
+
+	public boolean isSuccess() {
+		return (Integer) get("code") == HttpStatus.SUCCESS;
+	}
 }

+ 108 - 0
fs-live-streamer/pom.xml

@@ -0,0 +1,108 @@
+<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.fs</groupId>
+        <artifactId>fs</artifactId>
+        <version>1.1.0</version>
+    </parent>
+
+    <artifactId>fs-live-streamer</artifactId>
+    <description>
+        芳华直播app
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service-system</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 14 - 0
fs-live-streamer/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsLiveStreamApplication.class);
+    }
+}

+ 17 - 0
fs-live-streamer/src/main/java/com/fs/FsLiveStreamApplication.java

@@ -0,0 +1,17 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableTransactionManagement
+public class FsLiveStreamApplication {
+
+    public static void main(String[] args)
+    {
+        SpringApplication.run(FsLiveStreamApplication.class, args);
+        System.out.println("芳华直播app启动成功");
+    }
+}

+ 12 - 0
fs-live-streamer/src/main/java/com/fs/app/annotation/Login.java

@@ -0,0 +1,12 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * app登录效验
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Login {
+}

+ 37 - 0
fs-live-streamer/src/main/java/com/fs/app/annotation/RateLimiter.java

@@ -0,0 +1,37 @@
+package com.fs.app.annotation;
+
+import com.fs.app.constant.LiveStreamConstant;
+import com.fs.app.enums.LimitType;
+
+import java.lang.annotation.*;
+
+/**
+ * 限流注解
+ * 
+
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RateLimiter
+{
+    /**
+     * 限流key
+     */
+    public String key() default LiveStreamConstant.RATE_LIMIT_KEY;
+
+    /**
+     * 限流时间,单位秒
+     */
+    public int time() default 60;
+
+    /**
+     * 限流次数
+     */
+    public int count() default 100;
+
+    /**
+     * 限流类型
+     */
+    public LimitType limitType() default LimitType.DEFAULT;
+}

+ 23 - 0
fs-live-streamer/src/main/java/com/fs/app/config/WebMvcConfig.java

@@ -0,0 +1,23 @@
+package com.fs.app.config;
+
+
+import com.fs.app.interceptor.AuthorizationInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * MVC配置
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+    @Autowired
+    private AuthorizationInterceptor authorizationInterceptor;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authorizationInterceptor)
+                .addPathPatterns("/app/**");
+    }
+}

+ 13 - 0
fs-live-streamer/src/main/java/com/fs/app/constant/LiveStreamConstant.java

@@ -0,0 +1,13 @@
+package com.fs.app.constant;
+
+public class LiveStreamConstant {
+
+    public final static String LOGIN_SMS_CODE ="liveStream:loginSmsCode:";
+    public final static String CHANGE_PWD_SMS_CODE ="liveStream:changePwdSmsCode:";
+
+    public final static String WE_CHAT_APP_ID = "wx703c4bd07bbd1695";
+    public final static String WE_CHAT_APP_SECRET = "034f5cc8d9b5151f9d25da9628541e35";
+
+
+    public static final String RATE_LIMIT_KEY = "rate_limit:";
+}

+ 52 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/AgreementController.java

@@ -0,0 +1,52 @@
+package com.fs.app.controller;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.store.config.AgreementConfig;
+import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Api("协议接口")
+@Slf4j
+@Controller
+@RequestMapping(value="/app/agreement")
+@AllArgsConstructor
+public class AgreementController {
+
+    private final ISysConfigService configService;
+
+    @ApiOperation("直播协议")
+    @GetMapping("/streamAgreement")
+    public String userAgreement(Model model) {
+        AgreementConfig config = getAgreementConfig();
+        model.addAttribute("streamAgreement", config.getUserAgreement());
+        return "streamAgreement";
+    }
+
+    @ApiOperation("隐私协议")
+    @GetMapping("/privacyPolicy")
+    public String privacyPolicy(Model model) {
+        AgreementConfig config = getAgreementConfig();
+        model.addAttribute("privacyPolicy", config.getPrivacyPolicy());
+        return "privacyPolicy";
+    }
+
+    /**
+     * 获取协议配置
+     */
+    private AgreementConfig getAgreementConfig() {
+        String json = configService.selectConfigByKey("liveStreamer.agreement");
+        try {
+            return JSONUtil.toBean(json, AgreementConfig.class);
+        } catch (Exception e) {
+            log.error("解析协议配置失败: {}", json, e);
+        }
+        return new AgreementConfig();
+    }
+}

+ 34 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/AppBaseController.java

@@ -0,0 +1,34 @@
+package com.fs.app.controller;
+
+
+import com.fs.app.utils.JwtUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+public class AppBaseController {
+
+	@Autowired
+	JwtUtils jwtUtils;
+
+	public Long getUserId() {
+		HttpServletRequest request = ServletUtils.getRequest();
+		String headValue =  request.getHeader(jwtUtils.getHeader());
+		if(StringUtils.isBlank(headValue)){
+			headValue = request.getParameter(jwtUtils.getHeader());
+		}
+		if (StringUtils.isNotEmpty(headValue)){
+			Claims claims = jwtUtils.getClaimByToken(headValue);
+			if(claims != null){
+                return Long.parseLong(claims.getSubject());
+			}
+		}
+		return null;
+	}
+
+
+}

+ 45 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,45 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.StringUtils;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@Api("通用接口")
+@Slf4j
+@RestController
+@RequestMapping(value="/app/common")
+@AllArgsConstructor
+public class CommonController {
+
+    @ApiOperation("通用上传请求")
+    @PostMapping("uploadOSS")
+    public R uploadOSS(@RequestParam("file") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            return R.error("上传文件不能为空");
+        }
+        // 上传文件
+        String fileName = file.getOriginalFilename();
+        if (StringUtils.isBlank(fileName)) {
+            return R.error("名称不能为空");
+        }
+        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        CloudStorageService storage = OSSFactory.build();
+        if (storage == null) {
+            return R.error("服务器配置错误,请联系管理员!");
+        }
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
+        return R.ok().put("url",url);
+    }
+}

+ 52 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/FeedbackController.java

@@ -0,0 +1,52 @@
+package com.fs.app.controller;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.params.SubmitFeedbackParam;
+import com.fs.common.core.domain.R;
+import com.fs.framework.annotation.RepeatSubmit;
+import com.fs.liveStream.service.ILiveFeedbackLogService;
+import com.fs.liveStream.service.ILiveFeedbackTypeService;
+import com.fs.liveStream.vo.LiveFeedbackTypeVO;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Api("问题反馈接口")
+@RestController
+@RequestMapping(value="/app/feedback")
+@AllArgsConstructor
+public class FeedbackController extends AppBaseController {
+
+    private final ILiveFeedbackLogService liveFeedbackLogService;
+    private final ILiveFeedbackTypeService liveFeedbackTypeService;
+
+    @Login
+    @ApiOperation("获取反馈类型列表")
+    @GetMapping("/types")
+    public R types() {
+        List<LiveFeedbackTypeVO> types = liveFeedbackTypeService.selectLiveFeedbackTypeList();
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("data", new PageInfo<>(types));
+        return R.ok(map);
+    }
+
+    @RepeatSubmit(interval = 500)
+    @Login
+    @ApiOperation("反馈问题")
+    @PostMapping("/submitFeedback")
+    public R submitFeedback(@Validated @RequestBody SubmitFeedbackParam param) {
+        liveFeedbackLogService.submitFeedback(getUserId(), param.getTypeIds(), param.getContent());
+        return R.ok();
+    }
+
+}

+ 146 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/LiveController.java

@@ -0,0 +1,146 @@
+package com.fs.app.controller;
+
+import com.fs.app.annotation.Login;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.annotation.RepeatSubmit;
+import com.fs.live.domain.Live;
+import com.fs.live.service.ILiveService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Slf4j
+@Api("登录接口")
+@RestController
+@RequestMapping(value="/app/live")
+@AllArgsConstructor
+public class LiveController extends AppBaseController {
+
+    private final ILiveService liveService;
+
+    @Login
+    @ApiOperation("新增直播间")
+    @PostMapping("/addLive")
+    @RepeatSubmit(interval = 500)
+    public R addLive(@RequestBody Live live) {
+        live.setAnchorId(getUserId());
+        liveService.insertLive(live);
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("修改直播间")
+    @PostMapping("/editLive")
+    public R editLive(@RequestBody Live live) {
+        Live oldLive = liveService.selectLiveByLiveId(live.getLiveId());
+        if (oldLive == null) {
+            return R.error("直播间不存在");
+        }
+        if (!Objects.equals(oldLive.getAnchorId(), getUserId())) {
+            return R.error("无权限修改");
+        }
+
+        liveService.updateLive(live);
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("直播间详情")
+    @GetMapping("/{liveId}")
+    public R liveDetail(@PathVariable Long liveId) {
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null || !Objects.equals(live.getAnchorId(), getUserId())) {
+            return R.error("直播间不存在");
+        }
+        Map<String, Object> map = new HashMap<>();
+        map.put("data", live);
+        return R.ok(map);
+    }
+
+    @Login
+    @ApiOperation("删除直播间")
+    @DeleteMapping("/{liveIds}")
+    public R deleteLive(@PathVariable Long[] liveIds) {
+        liveService.deleteLiveByLiveIds(liveIds, getUserId());
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("直播间列表")
+    @GetMapping("/myLive")
+    public R myLive(@RequestParam(required = false) String liveName,
+                    @RequestParam(required = false) Integer status,
+                    @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                    @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String, Object> params = new HashMap<>();
+        params.put("liveName", liveName);
+        params.put("status", status);
+        params.put("anchorId", getUserId());
+        params.put("nowTime", LocalDateTime.now());
+
+        PageHelper.startPage(pageNum, pageSize);
+        List<Live> lives = liveService.selectLiveListByMap(params);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("data", new PageInfo<>(lives));
+        map.put("todayLiveCount", liveService.getLiveCountByMap(params));
+        return R.ok(map);
+    }
+
+    @Login
+    @ApiOperation("开始直播")
+    @PostMapping("/startLive/{liveId}")
+    @RepeatSubmit(interval = 500)
+    public R startLive(@PathVariable Long liveId) {
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null || !Objects.equals(live.getAnchorId(), getUserId())) {
+            return R.error("直播间不存在");
+        }
+
+        if (StringUtils.isBlank(live.getRtmpUrl())) {
+            R result = liveService.startLive(live);
+            Object codeObj = result.get("code");
+            if (!(codeObj instanceof Number) || ((Number) codeObj).intValue() != 200) {
+                return result;
+            }
+        }
+
+        live = liveService.selectLiveByLiveId(liveId);
+        String liveRtmpUrl = live.getRtmpUrl();
+        int lastSlashIndex = liveRtmpUrl.lastIndexOf('/');
+        String url = liveRtmpUrl.substring(0, lastSlashIndex);
+        String code = liveRtmpUrl.substring(lastSlashIndex + 1);
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("pushUrl", url);
+        map.put("pushCode", code);
+        map.put("flvUrl", live.getFlvHlsUrl());
+        map.put("wsUrl", "wss://im.fhhx.runtzh.com/ws");
+        return R.ok(map);
+    }
+
+    @Login
+    @ApiOperation("结束直播")
+    @PostMapping("/finishLive/{liveId}")
+    @RepeatSubmit(interval = 500)
+    public R finishLive(@PathVariable Long liveId) {
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null || !Objects.equals(live.getAnchorId(), getUserId())) {
+            return R.error("直播间不存在");
+        }
+        liveService.finishLive(live);
+        return R.ok();
+    }
+
+}

+ 169 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/LoginController.java

@@ -0,0 +1,169 @@
+package com.fs.app.controller;
+
+import com.fs.app.constant.LiveStreamConstant;
+import com.fs.app.params.LoginByPasswordParam;
+import com.fs.app.params.LoginBySmsCodeParam;
+import com.fs.app.params.LoginByWeChatParam;
+import com.fs.app.utils.*;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.annotation.RepeatSubmit;
+import com.fs.liveStream.domain.LiveStreamer;
+import com.fs.liveStream.service.ILiveStreamerService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Api("登录接口")
+@RestController
+@RequestMapping(value="/app/streamer")
+@AllArgsConstructor
+public class LoginController extends AppBaseController {
+
+    private final ILiveStreamerService streamerService;
+    private final JwtUtils jwtUtils;
+    private final RedisCache redisCache;
+    private final ISmsService smsService;
+
+    @ApiOperation("手机号密码登录")
+    @PostMapping("/loginByPassword")
+    public R loginByPassword(@Validated @RequestBody LoginByPasswordParam loginParam) {
+        String phoneNumber = loginParam.getPhone();
+        if (!PatternUtils.checkPhone(phoneNumber)) {
+            return R.error("手机号格式不正确!");
+        }
+
+        LiveStreamer streamer = streamerService.selectStreamerByPhone(phoneNumber);
+        if (streamer == null) {
+            return R.error("账号不存在或密码错误!");
+        }
+
+        if (!PasswordUtils.matchesPassword(loginParam.getPassword(), streamer.getPassword())) {
+            return R.error("账号不存在或密码错误!");
+        }
+
+        return login(streamer);
+    }
+
+    @ApiOperation("发送短信验证码")
+    @PostMapping("/sendSmsCode/{phone}")
+    @RepeatSubmit(interval = 60000, message = "验证码已发送,请稍后再试")
+    public R sendSmsCode(@PathVariable String phone) {
+        if (!PatternUtils.checkPhone(phone)) {
+            return R.error("手机号格式不正确!");
+        }
+
+        String codeKey = LiveStreamConstant.LOGIN_SMS_CODE + phone;
+
+        // 生成新验证码
+        String smsCode = redisCache.getCacheObject(codeKey);
+        if (StringUtils.isBlank(smsCode)) {
+            smsCode = VerifyCodeUtil.generateCode();
+        }
+
+        // 发送短信
+        R result = smsService.sendCaptcha(phone, smsCode, "验证码");
+        if (!result.isSuccess()) {
+            return result;
+        }
+
+        // 缓存验证码(3分钟有效)
+        redisCache.setCacheObject(codeKey, smsCode, 180, TimeUnit.SECONDS);
+        return R.ok("验证码已发送");
+    }
+
+    @ApiOperation("短信验证码登录")
+    @PostMapping("/loginBySmsCode")
+    public R loginBySmsCode(@Validated @RequestBody LoginBySmsCodeParam loginParam) {
+        String codeKey = LiveStreamConstant.LOGIN_SMS_CODE + loginParam.getPhone();
+        String code = redisCache.getCacheObject(codeKey);
+        if (StringUtils.isBlank(code) || !code.equals(loginParam.getCode())) {
+            return R.error("验证码错误");
+        }
+
+        redisCache.deleteObject(codeKey);
+
+        String phoneNumber = loginParam.getPhone();
+        LiveStreamer streamer = streamerService.selectStreamerByPhone(phoneNumber);
+        if (streamer == null) {
+            streamer = new LiveStreamer();
+            streamer.setPhone(phoneNumber);
+            streamer.setStatus(1);
+            streamer.setIsDel(0);
+            streamer.setIsRealNameVerify(0);
+            streamer.setCreateTime(LocalDateTime.now());
+            streamerService.saveStreamer(streamer);
+        }
+
+        return login(streamer);
+    }
+
+    @ApiOperation("手机号一键登录/微信登录")
+    @PostMapping("/loginByWeChat")
+    public R loginByWeChat(@Validated @RequestBody LoginByWeChatParam loginParam) {
+        Map<?, ?> wxResult = WxUtil.getAccessToken(loginParam.getCode(), LiveStreamConstant.WE_CHAT_APP_ID, LiveStreamConstant.WE_CHAT_APP_SECRET);
+        String accessToken = wxResult.get("access_token").toString();
+        String unionid = wxResult.get("unionid").toString();
+        String openid = wxResult.get("openid").toString();
+
+        Map userInfo = WxUtil.getUserInfo(accessToken, openid);
+        String nickname = userInfo.get("nickname").toString();
+        String avatar = userInfo.get("headimgurl").toString();
+
+        String phoneNumber = loginParam.getPhone();
+        LiveStreamer streamer = streamerService.selectStreamerByPhone(phoneNumber);
+        if (streamer == null) {
+            streamer = streamerService.selectStreamerByUnionId(unionid);
+        }
+
+        if (streamer == null) {
+            streamer = new LiveStreamer();
+            streamer.setStatus(1);
+            streamer.setIsDel(0);
+            streamer.setIsRealNameVerify(0);
+            streamer.setCreateTime(LocalDateTime.now());
+            streamerService.saveStreamer(streamer);
+        }
+
+        streamer.setPhone(phoneNumber);
+        streamer.setUnionId(unionid);
+        streamer.setOpenId(openid);
+        streamer.setAvatar(avatar);
+        streamer.setNickName(nickname);
+
+        return login(streamer);
+    }
+
+    /**
+     * 登录
+     */
+    private R login(LiveStreamer streamer) {
+        if (streamer.getStatus() == 0) {
+            return R.error("账号已被禁用!");
+        }
+
+        Map<String, Object> resultMap = new HashMap<>();
+        resultMap.put("token", jwtUtils.generateToken(streamer.getId()));
+        resultMap.put("header", jwtUtils.getHeader());
+
+        streamer.setUpdateTime(LocalDateTime.now());
+        streamer.setLastIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        streamerService.updateStreamer(streamer);
+
+        return R.ok(resultMap);
+    }
+
+}

+ 158 - 0
fs-live-streamer/src/main/java/com/fs/app/controller/StreamerController.java

@@ -0,0 +1,158 @@
+package com.fs.app.controller;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.constant.LiveStreamConstant;
+import com.fs.app.params.ChangeAvatarParam;
+import com.fs.app.params.ChangePasswordParm;
+import com.fs.app.utils.PasswordUtils;
+import com.fs.app.utils.PatternUtils;
+import com.fs.app.utils.VerifyCodeUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.annotation.RepeatSubmit;
+import com.fs.liveStream.domain.LiveStreamer;
+import com.fs.liveStream.service.ILiveStreamerService;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Api("主播管理接口")
+@RestController
+@RequestMapping(value="/app/profile")
+@AllArgsConstructor
+public class StreamerController extends AppBaseController {
+
+    private final ILiveStreamerService streamerService;
+    private final RedisCache redisCache;
+    private final ISmsService smsService;
+
+    @Login
+    @ApiOperation("获取个人信息")
+    @PostMapping("/index")
+    public R index() {
+        Map<String, Object> map = new HashMap<>();
+        map.put("data", streamerService.selectStreamerById(getUserId()));
+        return R.ok(map);
+    }
+
+    @Login
+    @ApiOperation("修改头像")
+    @PostMapping("/changeAvatar")
+    public R changeAvatar(@RequestParam("file") MultipartFile file) throws Exception {
+        LiveStreamer liveStreamer = streamerService.selectStreamerById(getUserId());
+        if (liveStreamer == null) {
+            return R.error("主播不存在");
+        }
+        if (file == null || file.isEmpty()) {
+            return R.error("头像不能为空");
+        }
+
+        // 限制图片最大为10M
+        if (file.getSize() > 10 * 1024 * 1024) {
+            return R.error("头像大小不能超过10M");
+        }
+
+        // 只能是图片类型
+        if (StringUtils.isBlank(file.getContentType()) && !file.getContentType().startsWith("image")) {
+            return R.error("只能上传图片类型");
+        }
+
+        // 上传文件
+        String fileName = file.getOriginalFilename();
+        if (StringUtils.isBlank(fileName)) {
+            return R.error("头像不能为空");
+        }
+
+        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        if (!suffix.matches("\\.(jpg|jpeg|png)$")) {
+            return R.error("不支持的图片格式");
+        }
+
+        CloudStorageService storage = OSSFactory.build();
+        if (storage == null) {
+            return R.error("服务器配置错误,请联系管理员!");
+        }
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
+
+        liveStreamer.setAvatar(url);
+        streamerService.updateStreamer(liveStreamer);
+
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("修改昵称")
+    @PostMapping("/changeNickName")
+    public R changeNickName(@Validated @RequestBody ChangeAvatarParam param) {
+        LiveStreamer liveStreamer = streamerService.selectStreamerById(getUserId());
+        if (liveStreamer != null) {
+            liveStreamer.setNickName(param.getNickName());
+            streamerService.updateStreamer(liveStreamer);
+        }
+        return R.ok();
+    }
+
+    @Login
+    @ApiOperation("发送短信验证码")
+    @PostMapping("/sendSmsCode/{phone}")
+    @RepeatSubmit(interval = 60000, message = "验证码已发送,请稍后再试")
+    public R sendSmsCode(@PathVariable String phone) {
+        if (!PatternUtils.checkPhone(phone)) {
+            return R.error("手机号格式不正确!");
+        }
+
+        String codeKey = LiveStreamConstant.CHANGE_PWD_SMS_CODE + phone;
+
+        // 生成新验证码
+        String smsCode = redisCache.getCacheObject(codeKey);
+        if (StringUtils.isBlank(smsCode)) {
+            smsCode = VerifyCodeUtil.generateCode();
+        }
+
+        // 发送短信
+        R result = smsService.sendCaptcha(phone, smsCode, "验证码");
+        if (!result.isSuccess()) {
+            return result;
+        }
+
+        // 缓存验证码(3分钟有效)
+        redisCache.setCacheObject(codeKey, smsCode, 180, TimeUnit.SECONDS);
+        return R.ok("验证码已发送");
+    }
+
+    @Login
+    @ApiOperation("修改密码")
+    @PostMapping("/changePassword")
+    public R changePassword(@Validated @RequestBody ChangePasswordParm param) {
+        LiveStreamer liveStreamer = streamerService.selectStreamerById(getUserId());
+        String codeKey = LiveStreamConstant.CHANGE_PWD_SMS_CODE + liveStreamer.getPhone();
+        String code = redisCache.getCacheObject(codeKey);
+        if (StringUtils.isBlank(code) || !code.equals(param.getCode())) {
+            return R.error("验证码错误");
+        }
+
+        redisCache.deleteObject(codeKey);
+
+        if (!param.getPassword().equals(param.getConfirmPassword())) {
+            return R.error("两次密码不一致!");
+        }
+
+        liveStreamer.setPassword(PasswordUtils.encryptPassword(param.getPassword()));
+        streamerService.updateStreamer(liveStreamer);
+
+        return R.ok();
+    }
+}

+ 20 - 0
fs-live-streamer/src/main/java/com/fs/app/enums/LimitType.java

@@ -0,0 +1,20 @@
+package com.fs.app.enums;
+
+/**
+ * 限流类型
+ *
+
+ */
+
+public enum LimitType
+{
+    /**
+     * 默认策略全局限流
+     */
+    DEFAULT,
+
+    /**
+     * 根据请求者IP进行限流
+     */
+    IP
+}

+ 39 - 0
fs-live-streamer/src/main/java/com/fs/app/exception/FSException.java

@@ -0,0 +1,39 @@
+package com.fs.app.exception;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 自定义异常
+ */
+@Getter
+@Setter
+public class FSException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
+	
+    private String msg;
+    private int code = 500;
+    
+    public FSException(String msg) {
+		super(msg);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, int code) {
+		super(msg);
+		this.msg = msg;
+		this.code = code;
+	}
+	
+	public FSException(String msg, int code, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+		this.code = code;
+	}
+
+}

+ 90 - 0
fs-live-streamer/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,90 @@
+package com.fs.app.exception;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.BaseException;
+import com.fs.common.exception.CustomException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class FSExceptionHandler {
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	/**
+	 * 处理自定义异常
+	 */
+	@ExceptionHandler(FSException.class)
+	public R handleRRException(FSException e){
+		R r = new R();
+		r.put("code", e.getCode());
+		r.put("msg", e.getMessage());
+
+		return r;
+	}
+
+	@ExceptionHandler(NoHandlerFoundException.class)
+	public R handlerNoFoundException(Exception e) {
+		logger.error(e.getMessage(), e);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+	@ExceptionHandler(MethodArgumentTypeMismatchException.class)
+	public R handleNumberFormatException(MethodArgumentTypeMismatchException e){
+		return R.error("数值类型错误");
+	}
+
+
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		FieldError error = e.getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public R exceptionHandler(MethodArgumentNotValidException e) {
+		FieldError error = e.getBindingResult().getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+	@ExceptionHandler(CustomException.class)
+	public R handleException(CustomException e){
+		return R.error(e.getMessage());
+	}
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+
+	@ExceptionHandler(BaseException.class)
+	public R handleException(BaseException e){
+		logger.error(e.getMessage(), e);
+		return R.error(e.getMessage());
+	}
+
+}

+ 75 - 0
fs-live-streamer/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -0,0 +1,75 @@
+package com.fs.app.interceptor;
+
+
+import com.fs.app.annotation.Login;
+import com.fs.app.exception.FSException;
+import com.fs.app.utils.JwtUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 权限(Token)验证
+ */
+@Component
+public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
+    @Autowired
+    private JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+    public static final String USER_KEY = "userId";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        Login annotation;
+        if(handler instanceof HandlerMethod) {
+            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
+        }else{
+            return true;
+        }
+
+        if(annotation == null){
+            return true;
+        }
+
+        //获取用户凭证
+        String token = request.getHeader(jwtUtils.getHeader());
+        if(StringUtils.isBlank(token)){
+            token = request.getParameter(jwtUtils.getHeader());
+        }
+
+        //凭证为空
+        if(StringUtils.isBlank(token)){
+            throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        Claims claims = jwtUtils.getClaimByToken(token);
+        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+        }
+        //查询用户的TOKEN是否和REDIS中的一样
+//        String redisToken=redisCache.getCacheObject("token:"+ Long.parseLong(claims.getSubject()));
+//        String redisUserToken=redisCache.getCacheObject("userToken:"+ Long.parseLong(claims.getSubject()));
+//        //token
+//        boolean a = StringUtils.isBlank(redisToken) || !redisToken.equals(token);
+//        //useToken
+//        boolean b = StringUtils.isBlank(redisUserToken) || !token.equals(redisUserToken);
+//        if(a&&b){
+//            redisCache.deleteObject("token:"+ Long.parseLong(claims.getSubject()));
+//            redisCache.deleteObject("userToken:"+ Long.parseLong(claims.getSubject()));
+//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+//        }
+        //设置userId到request里,后续根据userId,获取用户信息
+        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
+
+        return true;
+    }
+}

+ 12 - 0
fs-live-streamer/src/main/java/com/fs/app/params/ChangeAvatarParam.java

@@ -0,0 +1,12 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class ChangeAvatarParam {
+
+    @NotBlank(message = "昵称不能为空")
+    private String nickName;
+}

+ 18 - 0
fs-live-streamer/src/main/java/com/fs/app/params/ChangePasswordParm.java

@@ -0,0 +1,18 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class ChangePasswordParm {
+
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+
+    @NotBlank(message = "密码不能为空")
+    private String password;
+
+    @NotBlank(message = "确认密码不能为空")
+    private String confirmPassword;
+}

+ 15 - 0
fs-live-streamer/src/main/java/com/fs/app/params/LoginByPasswordParam.java

@@ -0,0 +1,15 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class LoginByPasswordParam {
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    @NotBlank(message = "密码不能为空")
+    private String password;
+}

+ 15 - 0
fs-live-streamer/src/main/java/com/fs/app/params/LoginBySmsCodeParam.java

@@ -0,0 +1,15 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class LoginBySmsCodeParam {
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    @NotBlank(message = "验证码不能为空")
+    private String code;
+}

+ 15 - 0
fs-live-streamer/src/main/java/com/fs/app/params/LoginByWeChatParam.java

@@ -0,0 +1,15 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class LoginByWeChatParam {
+
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+
+    @NotBlank(message = "code不能为空")
+    private String code;
+}

+ 20 - 0
fs-live-streamer/src/main/java/com/fs/app/params/SubmitFeedbackParam.java

@@ -0,0 +1,20 @@
+package com.fs.app.params;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Data
+public class SubmitFeedbackParam {
+
+    @NotEmpty(message = "反馈类型不能为空")
+    @ApiModelProperty("反馈类型")
+    private List<Long> typeIds;
+
+    @NotBlank(message = "反馈内容不能为空")
+    @ApiModelProperty("反馈内容")
+    private String content;
+}

+ 68 - 0
fs-live-streamer/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -0,0 +1,68 @@
+package com.fs.app.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.Getter;
+import lombok.Setter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * jwt工具类
+
+ */
+@ConfigurationProperties(prefix = "fs.jwt")
+@Component
+@Getter
+@Setter
+public class JwtUtils {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    private String secret;
+    private long expire;
+    private String header;
+
+    /**
+     * 生成jwt token
+     */
+    public String generateToken(long userId) {
+        Date nowDate = new Date();
+        //过期时间
+        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
+//        log.info("==============================="+secret);
+        logger.info("secret:{}",secret);
+        return Jwts.builder()
+                .setHeaderParam("typ", "JWT")
+                .setSubject(userId+"")
+                .setIssuedAt(nowDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    public Claims getClaimByToken(String token) {
+        try {
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        }catch (Exception e){
+            logger.debug("validate is token error ", e);
+            return null;
+        }
+    }
+
+    /**
+     * token是否过期
+     * @return  true:过期
+     */
+    public boolean isTokenExpired(Date expiration) {
+        return expiration.before(new Date());
+    }
+
+}

+ 26 - 0
fs-live-streamer/src/main/java/com/fs/app/utils/PasswordUtils.java

@@ -0,0 +1,26 @@
+package com.fs.app.utils;
+
+import com.fs.common.utils.StringUtils;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+public class PasswordUtils {
+
+    private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
+
+    private PasswordUtils() {}
+
+    /**
+     * 生成BCryptPasswordEncoder密码
+     */
+    public static String encryptPassword(String password) {
+        return PASSWORD_ENCODER.encode(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword) {
+
+        return PASSWORD_ENCODER.matches(rawPassword, encodedPassword);
+    }
+}

+ 19 - 0
fs-live-streamer/src/main/java/com/fs/app/utils/PatternUtils.java

@@ -0,0 +1,19 @@
+package com.fs.app.utils;
+
+import com.fs.common.utils.StringUtils;
+
+import java.util.regex.Pattern;
+
+public class PatternUtils {
+
+    /**
+     * 验证手机号
+     */
+    public static boolean checkPhone(String phone) {
+        if (phone == null || StringUtils.isBlank(phone)) {
+            return false;
+        }
+
+        return Pattern.matches("1[3-9]\\d{9}", phone);
+    }
+}

+ 18 - 0
fs-live-streamer/src/main/java/com/fs/app/utils/VerifyCodeUtil.java

@@ -0,0 +1,18 @@
+package com.fs.app.utils;
+
+import java.security.SecureRandom;
+
+public class VerifyCodeUtil {
+
+    // 使用 SecureRandom 保证安全性
+    private static final SecureRandom random = new SecureRandom();
+
+    /**
+     * 生成随机 6 位数字验证码
+     * @return 六位验证码字符串
+     */
+    public static String generateCode() {
+        int code = 100000 + random.nextInt(900000); // 保证是六位数,范围 100000-999999
+        return String.valueOf(code);
+    }
+}

+ 81 - 0
fs-live-streamer/src/main/java/com/fs/app/utils/WxUtil.java

@@ -0,0 +1,81 @@
+package com.fs.app.utils;
+
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+public class WxUtil {
+    private static final Logger logger = LoggerFactory.getLogger(WxUtil.class);
+    private static final String getAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token";
+    private static final String refreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token";
+    private static final String getUserInfo = "https://api.weixin.qq.com/sns/userinfo";
+
+
+    /**
+     * 获取微信AccessToken
+     * @param code 用户code
+     * @return
+     */
+    public static Map<?, ?> getAccessToken(String code, String appid, String secret) {
+        StringBuffer url = new StringBuffer();
+        url.append(getAccessTokenUrl)
+                .append("?appid=")
+                .append(appid)
+                .append("&secret=")
+                .append(secret)
+                .append("&code=")
+                .append(code)
+                .append("&grant_type=authorization_code");
+        String rs = HttpUtil.get(url.toString());
+        Map map = JSONObject.parseObject(rs, Map.class);
+        if (null == map.get("errcode")) {
+            return map;
+        } else {
+            logger.info("获取access_token出错,错误信息:" + map);
+            throw new RuntimeException("获取access_token出错");
+        }
+    }
+
+    /**
+     * 刷新AccessToken
+     * @param refreshToken
+     * @return
+     */
+    public static Map refreshToken(String refreshToken,String appid) {
+        StringBuffer url = new StringBuffer();
+        url.append(refreshTokenUrl)
+                .append("?appid=")
+                .append(appid)
+                .append("&grant_type=refresh_token&refresh_token=")
+                .append(refreshToken);
+        String rs = HttpUtil.get(url.toString());
+        Map map = JSONObject.parseObject(rs, Map.class);
+        if (null == map.get("errcode")) {
+            return map;
+        } else {
+            throw new RuntimeException("刷新access_token出错");
+        }
+    }
+
+    /**
+     * 获取用户信息
+     * @param accessToken
+     * @param openid
+     * @return
+     */
+    public static Map getUserInfo(String accessToken, String openid) {
+        StringBuffer url = new StringBuffer();
+        url.append(getUserInfo)
+                .append("?access_token=")
+                .append(accessToken)
+                .append("&openid=")
+                .append(openid)
+                .append("&lang=zh_CN");
+        String rs = HttpUtil.get(url.toString());
+        Map map = JSONObject.parseObject(rs, Map.class);
+        return map;
+    }
+}

+ 25 - 0
fs-live-streamer/src/main/java/com/fs/framework/annotation/RepeatSubmit.java

@@ -0,0 +1,25 @@
+package com.fs.framework.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义注解防止表单重复提交
+ *
+ * @author ruoyi
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit {
+
+    /**
+     * 间隔时间(ms),小于此时间视为重复提交
+     */
+    int interval() default 5000;
+
+    /**
+     * 提示消息
+     */
+    String message() default "操作频繁,请稍后再试";
+}

+ 197 - 0
fs-live-streamer/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,197 @@
+package com.fs.framework.aspectj;
+
+import com.fs.app.utils.JwtUtils;
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.service.ISysUserService;
+import io.jsonwebtoken.Claims;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    @Autowired
+    JwtUtils jwtUtils;
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
+        ServletRequestAttributes sra = (ServletRequestAttributes)ra;
+        HttpServletRequest request = sra.getRequest();
+
+        String headValue = request.getHeader(jwtUtils.getHeader());
+        Claims claims = jwtUtils.getClaimByToken(headValue);
+        Long userId =Long.parseLong( claims.getSubject().toString());
+        // 获取当前的用户
+        SysUser sysUser = SpringUtils.getBean(ISysUserService.class).selectUserById(userId);
+        if (StringUtils.isNotNull(sysUser))
+        {
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(sysUser) && !sysUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, sysUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    sqlString.append(" OR 1=0 ");
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-live-streamer/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ * 
+
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 117 - 0
fs-live-streamer/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.app.annotation.RateLimiter;
+import com.fs.app.enums.LimitType;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 限流处理
+ *
+
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    private RedisScript<Long> limitScript;
+
+    @Autowired
+    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+    {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Autowired
+    public void setLimitScript(RedisScript<Long> limitScript)
+    {
+        this.limitScript = limitScript;
+    }
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.app.annotation.RateLimiter)")
+    public void rateLimiterPointCut()
+    {
+    }
+
+    @Before("rateLimiterPointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        RateLimiter rateLimiter = getAnnotationRateLimiter(point);
+        String key = rateLimiter.key();
+        int time = rateLimiter.time();
+        int count = rateLimiter.count();
+
+        String combineKey = getCombineKey(rateLimiter, point);
+        List<Object> keys = Collections.singletonList(combineKey);
+        try
+        {
+            Long number = redisTemplate.execute(limitScript, keys, count, time);
+            if (StringUtils.isNull(number) || number.intValue() > count)
+            {
+                throw new CustomException("访问过于频繁,请稍后再试");
+            }
+            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
+        }
+        catch (CustomException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("服务器限流异常,请稍后再试");
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private RateLimiter getAnnotationRateLimiter(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(RateLimiter.class);
+        }
+        return null;
+    }
+
+    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+    {
+        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+        if (rateLimiter.limitType() == LimitType.IP)
+        {
+            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        }
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
+        return stringBuffer.toString();
+    }
+}

+ 31 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.framework.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * 程序注解配置
+ *
+
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.fs.**.mapper")
+public class ApplicationConfig
+{
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+    {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 71 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java

@@ -0,0 +1,71 @@
+package com.fs.framework.config;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.springframework.context.annotation.Configuration;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@Configuration
+public class ArrayStringTypeHandler extends BaseTypeHandler<List<String>> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
+        // 将 List<String> 转换为字符串,ClickHouse 支持的格式为 "['item1', 'item2']"
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        for (int j = 0; j < parameter.size(); j++) {
+            sb.append("'").append(parameter.get(j)).append("'");
+            if (j < parameter.size() - 1) {
+                sb.append(",");
+            }
+        }
+        sb.append("]");
+        ps.setString(i, sb.toString());
+    }
+
+    @Override
+    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        // 处理查询结果,将其转换为 List<String>
+        String result = rs.getString(columnName);
+        return parseArray(result);
+    }
+
+    @Override
+    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        String result = rs.getString(columnIndex);
+        return parseArray(result);
+    }
+
+    @Override
+    public List<String> getNullableResult(java.sql.CallableStatement cs, int columnIndex) throws SQLException {
+        String result = cs.getString(columnIndex);
+        return parseArray(result);
+    }
+
+    private List<String> parseArray(String arrayStr) {
+        if (arrayStr == null || arrayStr.trim().isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        arrayStr = arrayStr.trim();
+
+        // 添加长度检查
+        if (arrayStr.length() < 2 || !arrayStr.startsWith("[") || !arrayStr.endsWith("]")) {
+            return Collections.emptyList();
+        }
+
+        String content = arrayStr.substring(1, arrayStr.length() - 1);
+        if (content.trim().isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        String[] elements = content.split(",");
+        return Arrays.asList(elements);
+    }
+}

+ 93 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,93 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource
+                                        ) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.framework.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ * 
+
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 59 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/FilterConfig.java

@@ -0,0 +1,59 @@
+package com.fs.framework.config;
+
+import com.fs.common.filter.RepeatableFilter;
+import com.fs.common.filter.XssFilter;
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filter配置
+ *
+
+ */
+@Configuration
+@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+public class FilterConfig
+{
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

+ 109 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,109 @@
+package com.fs.framework.config;
+
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ * 
+
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+        String configLocation = env.getProperty("mybatis.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 113 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,113 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        serializer.setObjectMapper(mapper);
+
+        template.setValueSerializer(serializer);
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        serializer.setObjectMapper(mapper);
+
+        template.setValueSerializer(serializer);
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+
+}

+ 65 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -0,0 +1,65 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 通用配置
+ * 
+
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+    @Autowired
+    private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry)
+    {
+        /** 本地文件上传路径 */
+        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + FSConfig.getProfile() + "/");
+
+        /** swagger配置 */
+        registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry)
+    {
+        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+    }
+
+    /**
+     * 跨域配置
+     */
+    @Bean
+    public CorsFilter corsFilter()
+    {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置访问源地址
+        config.addAllowedOrigin("*");
+        // 设置访问源请求头
+        config.addAllowedHeader("*");
+        // 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 对接口配置跨域设置
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+}

+ 50 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * spring security配置
+ * 
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

+ 33 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ * 
+
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     * 
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 124 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,124 @@
+package com.fs.framework.config;//package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+import io.swagger.annotations.ApiOperation;
+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;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ *
+
+ */
+@Configuration
+@EnableSwagger2
+@EnableSwaggerBootstrapUI
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private FSConfig fsConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.fs.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<ApiKey> securitySchemes()
+    {
+        List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
+        apiKeyList.add(new ApiKey("Authorization", "AppToken", "header"));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:FS管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact(fsConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + fsConfig.getVersion())
+                .build();
+    }
+}

+ 63 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService()
+    {
+        return new ScheduledThreadPoolExecutor(corePoolSize,
+                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
+        {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 77 - 0
fs-live-streamer/src/main/java/com/fs/framework/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.framework.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * druid 配置属性
+ *
+
+ */
+@Configuration
+public class DruidProperties
+{
+    @Value("${spring.datasource.mysql.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.mysql.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.mysql.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.mysql.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.mysql.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.mysql.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.mysql.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.mysql.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.mysql.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource)
+    {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 27 - 0
fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ * 
+
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 45 - 0
fs-live-streamer/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ * 
+
+ */
+public class DynamicDataSourceContextHolder
+{
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+//        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 49 - 0
fs-live-streamer/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,49 @@
+package com.fs.framework.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.annotation.RepeatSubmit;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+
+/**
+ * 防止重复提交拦截器
+ *
+
+ */
+@Component
+public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
+{
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+    {
+        if (handler instanceof HandlerMethod)
+        {
+            HandlerMethod handlerMethod = (HandlerMethod) handler;
+            Method method = handlerMethod.getMethod();
+            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+            if (annotation != null)
+            {
+                if (this.isRepeatSubmit(request, annotation))
+                {
+                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
+                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
+}

+ 116 - 0
fs-live-streamer/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,116 @@
+package com.fs.framework.interceptor.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.filter.RepeatedlyRequestWrapper;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.http.HttpHelper;
+import com.fs.framework.annotation.RepeatSubmit;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ * 
+
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
+
+    public final String SESSION_REPEAT_KEY = "repeatData";
+
+    // 令牌自定义标识
+    @Value("${fs.jwt.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
+    {
+        String nowParams = "";
+        if (request instanceof RepeatedlyRequestWrapper)
+        {
+            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+        }
+
+        // body参数为空,获取Parameter的数据
+        if (StringUtils.isEmpty(nowParams))
+        {
+            nowParams = JSONObject.toJSONString(request.getParameterMap());
+        }
+        Map<String, Object> nowDataMap = new HashMap<>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放session的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = request.getHeader(header);
+        if (StringUtils.isEmpty(submitKey))
+        {
+            submitKey = url;
+        }
+
+        // 唯一标识(指定key + 消息头)
+        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+        if (sessionObj != null)
+        {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url))
+            {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        return !redisCache.setIfAbsent(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
+
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < interval)
+        {
+            return true;
+        }
+        return false;
+    }
+}

+ 1 - 0
fs-live-streamer/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 94 - 0
fs-live-streamer/src/main/resources/application-dev.yml

@@ -0,0 +1,94 @@
+# 数据源配置
+spring:
+  # redis 配置
+  redis:
+    # 地址  localhost
+    host: localhost
+    # 端口,默认为6379
+    port: 6379
+    # 数据库索引
+    database: 1
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 10s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 100
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms
+
+  datasource:
+    mysql:
+      type: com.alibaba.druid.pool.DruidDataSource
+      driverClassName: com.mysql.cj.jdbc.Driver
+      druid:
+        # 主库数据源
+        master:
+          url: jdbc:mysql://42.194.245.189:3306/live_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+          username: root
+          password: YJF_2024
+        # 从库数据源
+        slave:
+          # 从数据源开关/默认关闭
+          enabled: true
+          url: jdbc:mysql://42.194.245.189:3306/live_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+          username: root
+          password: YJF_2024
+        # 初始连接数
+        initialSize: 5
+        # 最小连接池数量
+        minIdle: 10
+        # 最大连接池数量
+        maxActive: 20
+        # 配置获取连接等待超时的时间
+        maxWait: 60000
+        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+        timeBetweenEvictionRunsMillis: 60000
+        # 配置一个连接在池中最小生存的时间,单位是毫秒
+        minEvictableIdleTimeMillis: 300000
+        # 配置一个连接在池中最大生存的时间,单位是毫秒
+        maxEvictableIdleTimeMillis: 900000
+        # 配置检测连接是否有效
+        validationQuery: SELECT 1 FROM DUAL
+        testWhileIdle: true
+        testOnBorrow: false
+        testOnReturn: false
+        webStatFilter:
+          enabled: true
+        stat-view-servlet:
+          enabled: false
+          # 设置白名单,不填则允许所有访问
+          allow:
+          url-pattern: /druid/*
+          # 控制台管理用户名和密码
+          login-username: fs
+          login-password: 123456
+        filter:
+          stat:
+            enabled: true
+            # 慢SQL记录
+            log-slow-sql: true
+            slow-sql-millis: 1000
+            merge-sql: true
+          wall:
+            config:
+              multi-statement-allow: true
+
+
+rocketmq:
+  name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+  producer:
+    group: my-producer-group
+    access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+    secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+  consumer:
+    group: test-group
+    access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+    secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+

+ 110 - 0
fs-live-streamer/src/main/resources/application.yml

@@ -0,0 +1,110 @@
+# 项目相关配置
+fs:
+  # 名称
+  name: fs
+  # 版本
+  version: 1.1.0
+  # 版权年份
+  copyrightYear: 2025
+  # 实例演示开关
+  demoEnabled: true
+  # 文件路径 示例( Windows配置D:/fs/uploadPath,Linux配置 /home/fs/uploadPath)
+  profile: c:/fs/uploadPath
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数组计算 char 字符验证
+  captchaType: math
+  # APP模块,是通过jwt认证的,如果要使用APP模块,则需要修改【加密秘钥】
+  jwt:
+    # 新加密秘钥
+    secret: f4e2e52034348f86b68cde581c0f9999
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
+
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8113
+  port: 9113
+  servlet:
+    # 应用的访问路径
+    context-path: /
+    # 指定静态资源的路径
+    resources:
+      static-locations: classpath:/static/
+    #设定thymeleaf
+    thymeleaf:
+      #thymeleaf对html的检查过于严格,设置spring.thymeleaf.mode=LEGACYHTML5
+      mode: LEGACYHTML5
+      cache: false
+  tomcat:
+    # tomcat的URI编码
+    uri-encoding: UTF-8
+    # tomcat最大线程数,默认为200
+    max-threads: 5000
+    # Tomcat启动初始化的线程数,默认值25
+    min-spare-threads: 100
+    # 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。
+    max-connections: 30000
+    # 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度
+    accept-count: 1000
+    # 连接器在接受连接后等待显示请求 URI 行的时间。
+    connection-timeout: 20000
+
+# 日志配置
+logging:
+  level:
+    com.fs: info
+    org.springframework: warn
+
+# Spring配置
+spring:
+  cache:
+    type: redis
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    #    active: druid
+    active: dev
+    include: config
+  # 文件上传
+  servlet:
+    multipart:
+      # 单个文件大小
+      max-file-size:  100MB
+      # 设置总上传的文件大小
+      max-request-size:  200MB
+
+# MyBatis配置
+mybatis:
+  # 搜索指定包别名
+  typeAliasesPackage: com.fs.**.domain,com.fs.**.bo,com.fs.**.vo
+  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  mapperLocations: classpath*:mapper/**/*Mapper.xml
+  # 加载全局的配置文件
+  configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper分页插件
+pagehelper:
+  helperDialect: mysql
+  reasonable: false
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Swagger配置
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*

+ 2 - 0
fs-live-streamer/src/main/resources/banner.txt

@@ -0,0 +1,2 @@
+Application Version: ${fs.version}
+Spring Boot Version: ${spring-boot.version}

+ 37 - 0
fs-live-streamer/src/main/resources/i18n/messages.properties

@@ -0,0 +1,37 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 15 - 0
fs-live-streamer/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+
+	<settings>
+		<setting name="cacheEnabled"             value="false" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+
+</configuration>

+ 21 - 0
fs-live-streamer/src/main/resources/templates/privacyPolicy.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >隐私政策</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${privacyPolicy}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 21 - 0
fs-live-streamer/src/main/resources/templates/streamAgreement.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >直播协议</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${streamAgreement}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 19 - 0
fs-service-system/src/main/java/com/fs/common/config/SmsConfig.java

@@ -0,0 +1,19 @@
+package com.fs.common.config;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public interface SmsConfig {
+    //String smsSubmit="http://dh3t.com/json/sms/Submit"; //发送短信
+    String smsSubmit="http://219.151.150.81:7862/sms"; //发送短信
+
+    String querySms="http://dh3t.com/http/tjreport/querySms";//短信统计
+
+    String uploadTemplate="http://www.dh3t.com/json/sms/upload/template";
+
+    String showTemplate="http://www.dh3t.com/json/sms/show/template";
+
+    String updateTemplate="http://www.dh3t.com/json/sms/update/template";
+
+    String deleteTemplate="http://www.dh3t.com/json/sms/delete/template";
+}

+ 18 - 0
fs-service-system/src/main/java/com/fs/common/domain/SendSmsByMY.java

@@ -0,0 +1,18 @@
+package com.fs.common.domain;
+
+import lombok.Data;
+
+@Data
+public class SendSmsByMY {
+    private String account; // 账号
+    private String password; // 密码
+    private String msgid; // 可不填
+    private String mobile; // 手机号列表
+    private String content; // 短信内容
+    private String sign; // 签名 可不填
+    private String subcode; // 子码  可不填
+    private String extno; // 接入号
+    private String rt; // 响应数据类型
+    private String action; // 请求动作
+    private String sendtime; // 发送时间 可不填
+}

+ 14 - 0
fs-service-system/src/main/java/com/fs/common/domain/SendSmsReturn.java

@@ -0,0 +1,14 @@
+package com.fs.common.domain;
+
+import lombok.Data;
+
+@Data
+public class SendSmsReturn {
+    private String msgid;
+    // 结果代码
+    private String result;
+    // 描述信息
+    private String desc;
+    // 失败电话号码
+    private String failPhones;
+}

+ 2 - 0
fs-service-system/src/main/java/com/fs/common/service/ISmsService.java

@@ -18,4 +18,6 @@ public interface ISmsService
 
     R sendStoreOrderSms(SmsSendStoreOrderParam param);
 
+    R sendCaptcha(String phone, String captcha, String code);
+
 }

+ 144 - 0
fs-service-system/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -3,8 +3,12 @@ package com.fs.common.service.impl;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.config.SmsConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.domain.SendSmsByMY;
+import com.fs.common.domain.SendSmsReturn;
 import com.fs.common.param.SmsSendBatchParam;
 import com.fs.common.param.SmsSendOrderBatchParam;
 import com.fs.common.param.SmsSendParam;
@@ -20,6 +24,9 @@ import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.store.domain.FsStoreOrder;
 import com.fs.store.service.IFsStoreOrderService;
+import com.fs.system.config.FsSmsConfig;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
 import com.tencentcloudapi.common.Credential;
 import com.tencentcloudapi.common.exception.TencentCloudSDKException;
 import com.tencentcloudapi.common.profile.ClientProfile;
@@ -28,18 +35,29 @@ import com.tencentcloudapi.sms.v20190711.SmsClient;
 import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
 import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
 import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+@Slf4j
 @Service
 public class SmsServiceImpl implements ISmsService
 {
@@ -69,6 +87,8 @@ public class SmsServiceImpl implements ISmsService
 
     @Autowired
     private ICompanyUserService companyUserService;
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
 
     @Override
     public R sendTSms(String mobile, String code) {
@@ -396,6 +416,130 @@ public class SmsServiceImpl implements ISmsService
         }
     }
 
+    @Override
+    public R sendCaptcha(String phone, String captcha, String code) {
+        log.info(captcha);
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempByCode(code);
+        if (temp == null) {
+            return R.error("没有模板");
+        }
+        String content = temp.getContent();
+        if (StringUtils.isNotEmpty(captcha)) {
+            content = content.replace("${sms.captcha}", captcha);
+        }
+        String urls = null;
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+        if (sms.getType().equals("rf")) {
+            try {
+                content = content.replace("${sms.sign}",sms.getRfSign());
+                urls = sms.getRfUrl1() + "sms?action=send&account=" + sms.getRfAccount1() + "&password=" + sms.getRfPassword1() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode1() + "&rt=json";
+            } catch (UnsupportedEncodingException e) {
+                e.printStackTrace();
+            }
+            String post = HttpRequest.get(urls)
+                    .execute().body();
+            SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+            if (vo.getStatus().equals(0)) {
+                for (SmsSendItemVO itemVO : vo.getList()) {
+                    if (itemVO.getResult().equals("0")) {
+                        CompanySmsLogs logs = new CompanySmsLogs();
+                        logs.setContent(content);
+                        logs.setTempCode(temp.getTempCode());
+                        logs.setTempId(temp.getTempId());
+                        logs.setPhone(phone);
+                        logs.setSendTime(new Date());
+                        logs.setStatus(0);
+                        logs.setType(sms.getType());
+                        logs.setMid(itemVO.getMid());
+                        Integer counts = logs.getContent().length() / 67;
+                        if (logs.getContent().length() % 67 > 0) {
+                            counts = counts + 1;
+                        }
+                        if (counts == 0) {
+                            counts = 1;
+                        }
+                        logs.setNumber(counts);
+                        smsLogsService.insertCompanySmsLogs(logs);
+                    }
+                }
+            }
+        } else if (sms.getType().equals("dh")) {
+            SendSmsReturn sendSmsReturn = null;
+            content = content.replace("${sms.sign}",sms.getDhSign());
+            sendSmsReturn = this.sendSms(sms.getDhAccount1(), sms.getDhPassword1(), content, phone);
+            if (sendSmsReturn != null) {
+                if (sendSmsReturn.getResult() != null && sendSmsReturn.getResult().equals("0")) {
+                    CompanySmsLogs logs = new CompanySmsLogs();
+                    logs.setContent(content);
+                    logs.setTempCode(temp.getTempCode());
+                    logs.setTempId(temp.getTempId());
+                    logs.setPhone(phone);
+                    logs.setSendTime(new Date());
+                    logs.setStatus(0);
+                    logs.setType(sms.getType());
+                    logs.setMid(sendSmsReturn.getMsgid());
+                    Integer counts = logs.getContent().length() / 67;
+                    if (logs.getContent().length() % 67 > 0) {
+                        counts = counts + 1;
+                    }
+                    if (counts == 0) {
+                        counts = 1;
+                    }
+                    logs.setNumber(counts);
+                    smsLogsService.insertCompanySmsLogs(logs);
+                }
+            }
+        }
+        return R.ok();
+    }
+
+    private SendSmsReturn sendSms(String account, String password, String content, String phone) {
+        SendSmsByMY sendSmsByMY = new SendSmsByMY();
+
+        sendSmsByMY.setAccount(account);
+        sendSmsByMY.setPassword(password);
+        sendSmsByMY.setContent(content);
+        sendSmsByMY.setMobile(phone);
+        sendSmsByMY.setRt("json");
+        sendSmsByMY.setAction("send");
+        sendSmsByMY.setExtno("10690");
+        String sendReturn = sendHttp(SmsConfig.smsSubmit, sendSmsByMY);
+        SendSmsReturn sendSmsReturn = JSON.parseObject(sendReturn, SendSmsReturn.class);
+        log.info("数据:{}", sendSmsReturn);
+        return sendSmsReturn;
+    }
+
+    private String sendHttp(String url,Object o){
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+        String responseText="";
+        try {
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.addHeader("Content-Type", "application/json");
+
+            String jsonString = JSON.toJSONString(o);
+            StringEntity stringEntity = new StringEntity(jsonString, StandardCharsets.UTF_8);
+            httpPost.setEntity(stringEntity);
+            CloseableHttpResponse response = httpClient.execute(httpPost);
+            HttpEntity entity = response.getEntity();
+
+            if (entity != null) {
+                responseText = EntityUtils.toString(entity, "UTF-8");
+            }
+            response.close();
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                httpClient.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return responseText;
+    }
+
     @Autowired
     private IFsStoreOrderService orderService;
 

+ 10 - 0
fs-service-system/src/main/java/com/fs/company/domain/CompanySmsLogs.java

@@ -65,6 +65,16 @@ public class CompanySmsLogs extends BaseEntity
 
     private Integer number;
 
+    private String type;
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
     public Integer getNumber() {
         return number;
     }

+ 16 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Update;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 直播Mapper接口
@@ -79,6 +80,11 @@ public interface LiveMapper
      */
     public int deleteLiveByLiveIds(Long[] liveIds);
 
+    /**
+     * 批量删除直播
+     */
+    void deleteLiveByAnchorAndLiveIds(@Param("userId") Long userId, @Param("liveIds") Long[] liveIds);
+
     List<Live> liveList();
 
     @Select("select * from live where talent_id=#{talentId}")
@@ -175,4 +181,14 @@ public interface LiveMapper
 
     @Update("update live set global_visible = #{status} where live_id = #{liveId}")
     void updateGlobalVisible(@Param("liveId")Long liveId,@Param("status") Integer status);
+
+    /**
+     * 查询直播列表
+     */
+    List<Live> selectLiveListByMap(@Param("params") Map<String, Object> params);
+
+    /**
+     * 获取直播间数量
+     */
+    Integer getLiveCountByMap(@Param("params") Map<String, Object> params);
 }

+ 15 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveService.java

@@ -76,6 +76,11 @@ public interface ILiveService
      */
     public int deleteLiveByLiveIds(Long[] liveIds);
 
+    /**
+     * 批量删除直播
+     */
+    void deleteLiveByLiveIds(Long[] liveIds, Long userId);
+
     /**
      * 删除直播信息
      *
@@ -189,4 +194,14 @@ public interface ILiveService
     void updateGlobalVisible(long liveId, Integer status);
 
     Live selectLiveDbByLiveId(Long liveId);
+
+    /**
+     * 查询直播列表
+     */
+    List<Live> selectLiveListByMap(Map<String, Object> params);
+
+    /**
+     * 获取直播间数量
+     */
+    Integer getLiveCountByMap(Map<String, Object> params);
 }

+ 26 - 0
fs-service-system/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -483,6 +483,16 @@ public class LiveServiceImpl implements ILiveService
         return result;
     }
 
+    /**
+     * 批量删除直播
+     */
+    @Override
+    public void deleteLiveByLiveIds(Long[] liveIds, Long userId) {
+        baseMapper.deleteLiveByAnchorAndLiveIds(userId, liveIds);
+        // 清除缓存
+        clearLiveCache(liveIds);
+    }
+
     /**
      * 删除直播信息
      *
@@ -1304,4 +1314,20 @@ public class LiveServiceImpl implements ILiveService
             }
         }
     }
+
+    /**
+     * 查询直播列表
+     */
+    @Override
+    public List<Live> selectLiveListByMap(Map<String, Object> params) {
+        return baseMapper.selectLiveListByMap(params);
+    }
+
+    /**
+     * 获取直播间数量
+     */
+    @Override
+    public Integer getLiveCountByMap(Map<String, Object> params) {
+        return baseMapper.getLiveCountByMap(params);
+    }
 }

+ 30 - 0
fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackLog.java

@@ -0,0 +1,30 @@
+package com.fs.liveStream.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class LiveFeedbackLog {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 主播ID
+     */
+    private Long streamerId;
+
+    /**
+     * 反馈内容
+     */
+    private String content;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+}

+ 44 - 0
fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackType.java

@@ -0,0 +1,44 @@
+package com.fs.liveStream.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class LiveFeedbackType {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 反馈问题类型名称
+     */
+    private String typeName;
+
+    /**
+     * 上级类型名称
+     */
+    private Long parentId;
+
+    /**
+     * 状态  1正常 0禁用
+     */
+    private Integer status;
+
+    /**
+     * 是否删除 1删除 0未删除
+     */
+    private Integer isDel;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+}

+ 22 - 0
fs-service-system/src/main/java/com/fs/liveStream/domain/LiveFeedbackTypeLog.java

@@ -0,0 +1,22 @@
+package com.fs.liveStream.domain;
+
+import lombok.Data;
+
+@Data
+public class LiveFeedbackTypeLog {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 反馈类型ID
+     */
+    private Long typeId;
+
+    /**
+     * 反馈记录ID
+     */
+    private Long logId;
+}

+ 75 - 0
fs-service-system/src/main/java/com/fs/liveStream/domain/LiveStreamer.java

@@ -0,0 +1,75 @@
+package com.fs.liveStream.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class LiveStreamer {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+    /**
+     * 用户昵称
+     */
+    private String nickName;
+    /**
+     * 用户头像
+     */
+    private String avatar;
+    /**
+     * 手机号码
+     */
+    private String phone;
+    /**
+     * 密码
+     */
+    private String password;
+    /**
+     * 微信OPENID
+     */
+    private String openId;
+    /**
+     * 关联ID
+     */
+    private String unionId;
+    /**
+     * 1为正常,0为禁止
+     */
+    private Integer status;
+    /**
+     * 0正常 1删除
+     */
+    private Integer isDel;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 添加时间
+     */
+    private LocalDateTime createTime;
+    /**
+     * 最后一次登录时间
+     */
+    private LocalDateTime updateTime;
+    /**
+     * 最后一次登录ip
+     */
+    private String lastIp;
+    /**
+     * 是否实名认证 0否 1是
+     */
+    private Integer isRealNameVerify;
+    /**
+     * 身份证正面
+     */
+    private String idCardFrontUrl;
+    /**
+     * 身份证反面
+     */
+    private String idCardBackUrl;
+
+}

+ 20 - 0
fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackLogMapper.java

@@ -0,0 +1,20 @@
+package com.fs.liveStream.mapper;
+
+import com.fs.liveStream.domain.LiveFeedbackLog;
+import com.fs.liveStream.param.LiveFeedbackLogParam;
+import com.fs.liveStream.vo.LiveFeedbackLogVO;
+
+import java.util.List;
+
+public interface LiveFeedbackLogMapper {
+
+    /**
+     * 插入反馈记录
+     */
+    void insertFeedbackLog(LiveFeedbackLog feedbackLog);
+
+    /**
+     * 查询直播反馈日志列表
+     */
+    List<LiveFeedbackLogVO> selectFeedbackLogList(LiveFeedbackLogParam liveFeedbackLog);
+}

+ 14 - 0
fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackTypeLogMapper.java

@@ -0,0 +1,14 @@
+package com.fs.liveStream.mapper;
+
+import com.fs.liveStream.domain.LiveFeedbackTypeLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface LiveFeedbackTypeLogMapper {
+
+    /**
+     * 批量插入
+     */
+    void insertBatch(@Param("typeLogList") List<LiveFeedbackTypeLog> typeLogList);
+}

+ 55 - 0
fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveFeedbackTypeMapper.java

@@ -0,0 +1,55 @@
+package com.fs.liveStream.mapper;
+
+import com.fs.liveStream.domain.LiveFeedbackType;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 直播反馈类型Mapper接口
+ */
+public interface LiveFeedbackTypeMapper {
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    List<LiveFeedbackType> selectFeedbackTypes();
+
+    /**
+     * 根据类型ID查询直播反馈类型列表
+     */
+    List<LiveFeedbackType> selectFeedbackTypeListByTypeIds(@Param("typeIds") List<Long> typeIds);
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    List<LiveFeedbackType> selectFeedbackTypeList(LiveFeedbackType liveFeedbackType);
+
+    /**
+     * 查询直播反馈类型
+     */
+    @Select("select * from live_feedback_type where id = #{id}")
+    LiveFeedbackType selectFeedbackTypeListById(@Param("id") Long id);
+
+    /**
+     * 新增直播反馈类型
+     */
+    int insertLiveFeedbackType(LiveFeedbackType liveFeedbackType);
+
+    /**
+     * 根据类型名称查询直播反馈类型
+     */
+    @Select("select * from live_feedback_type where type_name = #{typeName} and is_del = 0")
+    LiveFeedbackType selectLiveFeedbackTypeByTypeName(@Param("typeName") String typeName);
+
+    /**
+     * 修改直播反馈类型
+     */
+    int updateLiveFeedbackType(LiveFeedbackType type);
+
+    /**
+     * 删除直播反馈类型
+     */
+    int deleteLiveFeedbackTypeByIds(@Param("typeIds") Long[] typeIds);
+}

+ 34 - 0
fs-service-system/src/main/java/com/fs/liveStream/mapper/LiveStreamerMapper.java

@@ -0,0 +1,34 @@
+package com.fs.liveStream.mapper;
+
+import com.fs.liveStream.domain.LiveStreamer;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+public interface LiveStreamerMapper {
+
+    /**
+     * 根据手机号查询主播记录
+     */
+    LiveStreamer selectStreamerByPhone(@Param("phone") String phone);
+
+    /**
+     * 根据unionid查询主播记录
+     */
+    LiveStreamer selectStreamerByUnionId(@Param("unionId") String unionId);
+
+    /**
+     * 保存主播记录
+     */
+    int insertStreamer(LiveStreamer streamer);
+
+    /**
+     * 更新主播信息
+     */
+    int updateStreamer(LiveStreamer streamer);
+
+    /**
+     * 查询主播信息
+     */
+    @Select("select * from live_streamer where id = #{id}")
+    LiveStreamer selectStreamerById(@Param("id") Long id);
+}

+ 17 - 0
fs-service-system/src/main/java/com/fs/liveStream/param/LiveFeedbackLogParam.java

@@ -0,0 +1,17 @@
+package com.fs.liveStream.param;
+
+import lombok.Data;
+
+@Data
+public class LiveFeedbackLogParam {
+
+    /**
+     * 主播名称
+     */
+    private String streamerName;
+
+    /**
+     * 投诉类型
+     */
+    private Long typeId;
+}

+ 19 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackLogService.java

@@ -0,0 +1,19 @@
+package com.fs.liveStream.service;
+
+import com.fs.liveStream.param.LiveFeedbackLogParam;
+import com.fs.liveStream.vo.LiveFeedbackLogVO;
+
+import java.util.List;
+
+public interface ILiveFeedbackLogService {
+
+    /**
+     * 提交反馈
+     */
+    void submitFeedback(Long userId, List<Long> typeIds, String content);
+
+    /**
+     * 查询直播反馈日志列表
+     */
+    List<LiveFeedbackLogVO> selectLiveFeedbackLogList(LiveFeedbackLogParam liveFeedbackLog);
+}

+ 4 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackTypeLogService.java

@@ -0,0 +1,4 @@
+package com.fs.liveStream.service;
+
+public interface ILiveFeedbackTypeLogService {
+}

+ 42 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/ILiveFeedbackTypeService.java

@@ -0,0 +1,42 @@
+package com.fs.liveStream.service;
+
+import com.fs.liveStream.domain.LiveFeedbackType;
+import com.fs.liveStream.vo.LiveFeedbackTypeVO;
+
+import java.util.List;
+
+/**
+ * 直播反馈类型Service接口
+ */
+public interface ILiveFeedbackTypeService {
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    List<LiveFeedbackTypeVO> selectLiveFeedbackTypeList();
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    List<LiveFeedbackType> selectLiveFeedbackTypeList(LiveFeedbackType liveFeedbackType);
+
+    /**
+     * 查询直播反馈类型
+     */
+    LiveFeedbackType selectLiveFeedbackTypeById(Long id);
+
+    /**
+     * 新增直播反馈类型
+     */
+    int insertLiveFeedbackType(LiveFeedbackType liveFeedbackType);
+
+    /**
+     * 修改直播反馈类型
+     */
+    int updateLiveFeedbackType(LiveFeedbackType liveFeedbackType);
+
+    /**
+     * 删除直播反馈类型
+     */
+    int deleteLiveFeedbackTypeByIds(Long[] typeIds);
+}

+ 31 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/ILiveStreamerService.java

@@ -0,0 +1,31 @@
+package com.fs.liveStream.service;
+
+import com.fs.liveStream.domain.LiveStreamer;
+
+public interface ILiveStreamerService {
+
+    /**
+     * 根据手机号查询主播记录
+     */
+    LiveStreamer selectStreamerByPhone(String phone);
+
+    /**
+     * 根据unionid查询主播记录
+     */
+    LiveStreamer selectStreamerByUnionId(String unionid);
+
+    /**
+     * 保存主播记录
+     */
+    int saveStreamer(LiveStreamer streamer);
+    
+    /**
+     * 更新主播信息
+     */
+    int updateStreamer(LiveStreamer streamer);
+
+    /**
+     * 查询主播信息
+     */
+    LiveStreamer selectStreamerById(Long id);
+}

+ 70 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackLogServiceImpl.java

@@ -0,0 +1,70 @@
+package com.fs.liveStream.service.impl;
+
+import com.fs.common.exception.CustomException;
+import com.fs.liveStream.domain.LiveFeedbackLog;
+import com.fs.liveStream.domain.LiveFeedbackType;
+import com.fs.liveStream.domain.LiveFeedbackTypeLog;
+import com.fs.liveStream.mapper.LiveFeedbackLogMapper;
+import com.fs.liveStream.mapper.LiveFeedbackTypeLogMapper;
+import com.fs.liveStream.mapper.LiveFeedbackTypeMapper;
+import com.fs.liveStream.param.LiveFeedbackLogParam;
+import com.fs.liveStream.service.ILiveFeedbackLogService;
+import com.fs.liveStream.vo.LiveFeedbackLogVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class LiveFeedbackLogServiceImpl implements ILiveFeedbackLogService {
+
+    @Resource
+    private LiveFeedbackLogMapper liveFeedbackLogMapper;
+    @Resource
+    private LiveFeedbackTypeMapper liveFeedbackTypeMapper;
+    @Resource
+    private LiveFeedbackTypeLogMapper liveFeedbackTypeLogMapper;
+
+    /**
+     * 提交反馈
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void submitFeedback(Long userId, List<Long> typeIds, String content) {
+        List<LiveFeedbackType> types = liveFeedbackTypeMapper.selectFeedbackTypeListByTypeIds(typeIds);
+        if (types == null || types.isEmpty()) {
+            throw new CustomException("反馈类型不存在");
+        }
+
+        LiveFeedbackLog feedbackLog = new LiveFeedbackLog();
+        feedbackLog.setStreamerId(userId);
+        feedbackLog.setContent(content);
+        feedbackLog.setCreateTime(LocalDateTime.now());
+        liveFeedbackLogMapper.insertFeedbackLog(feedbackLog);
+
+        List<LiveFeedbackTypeLog> typeLogList = types.stream().map(type -> {
+            LiveFeedbackTypeLog typeLog = new LiveFeedbackTypeLog();
+            typeLog.setTypeId(type.getId());
+            typeLog.setLogId(feedbackLog.getId());
+            return typeLog;
+        }).collect(Collectors.toList());
+
+        if (!typeLogList.isEmpty()) {
+            liveFeedbackTypeLogMapper.insertBatch(typeLogList);
+        }
+    }
+
+    /**
+     * 查询直播反馈日志列表
+     */
+    @Override
+    public List<LiveFeedbackLogVO> selectLiveFeedbackLogList(LiveFeedbackLogParam liveFeedbackLog) {
+        return liveFeedbackLogMapper.selectFeedbackLogList(liveFeedbackLog);
+    }
+}

+ 10 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackTypeLogServiceImpl.java

@@ -0,0 +1,10 @@
+package com.fs.liveStream.service.impl;
+
+import com.fs.liveStream.service.ILiveFeedbackTypeLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class LiveFeedbackTypeLogServiceImpl implements ILiveFeedbackTypeLogService {
+}

+ 116 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveFeedbackTypeServiceImpl.java

@@ -0,0 +1,116 @@
+package com.fs.liveStream.service.impl;
+
+import com.fs.common.exception.CustomException;
+import com.fs.liveStream.domain.LiveFeedbackType;
+import com.fs.liveStream.mapper.LiveFeedbackTypeMapper;
+import com.fs.liveStream.service.ILiveFeedbackTypeService;
+import com.fs.liveStream.vo.LiveFeedbackTypeVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * 直播反馈类型Service业务层处理
+ */
+@Slf4j
+@Service
+public class LiveFeedbackTypeServiceImpl implements ILiveFeedbackTypeService {
+
+    @Resource
+    private LiveFeedbackTypeMapper liveFeedbackTypeMapper;
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    @Override
+    public List<LiveFeedbackTypeVO> selectLiveFeedbackTypeList() {
+        List<LiveFeedbackType> types = liveFeedbackTypeMapper.selectFeedbackTypes();
+
+        Map<Long, List<LiveFeedbackType>> groupMap = types.stream()
+                .collect(Collectors.groupingBy(
+                        t -> Optional.ofNullable(t.getParentId()).orElse(0L)
+                ));
+
+        return buildTree(groupMap, 0L);
+    }
+
+    /**
+     * 组装返回
+     */
+    private List<LiveFeedbackTypeVO> buildTree(Map<Long, List<LiveFeedbackType>> groupMap, Long parentId){
+        List<LiveFeedbackType> children = groupMap.get(parentId);
+        if (children == null) {
+            return Collections.emptyList();
+        }
+
+        List<LiveFeedbackTypeVO> result = new ArrayList<>(children.size());
+        for (LiveFeedbackType type : children) {
+            LiveFeedbackTypeVO vo = new LiveFeedbackTypeVO();
+            vo.setId(type.getId());
+            vo.setTypeName(type.getTypeName());
+            vo.setChildren(buildTree(groupMap, type.getId()));
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 查询直播反馈类型列表
+     */
+    @Override
+    public List<LiveFeedbackType> selectLiveFeedbackTypeList(LiveFeedbackType liveFeedbackType) {
+        return liveFeedbackTypeMapper.selectFeedbackTypeList(liveFeedbackType);
+    }
+
+    /**
+     * 查询直播反馈类型
+     */
+    @Override
+    public LiveFeedbackType selectLiveFeedbackTypeById(Long id) {
+        return liveFeedbackTypeMapper.selectFeedbackTypeListById(id);
+    }
+
+    /**
+     * 新增直播反馈类型
+     */
+    @Override
+    public int insertLiveFeedbackType(LiveFeedbackType liveFeedbackType) {
+        LiveFeedbackType type = liveFeedbackTypeMapper.selectLiveFeedbackTypeByTypeName(liveFeedbackType.getTypeName());
+        if (Objects.nonNull(type)) {
+            throw new CustomException("类型名称已存在");
+        }
+
+        liveFeedbackType.setIsDel(0);
+        liveFeedbackType.setCreateTime(LocalDateTime.now());
+        return liveFeedbackTypeMapper.insertLiveFeedbackType(liveFeedbackType);
+    }
+
+    /**
+     * 修改直播反馈类型
+     */
+    @Override
+    public int updateLiveFeedbackType(LiveFeedbackType liveFeedbackType) {
+        LiveFeedbackType type = liveFeedbackTypeMapper.selectLiveFeedbackTypeByTypeName(liveFeedbackType.getTypeName());
+        if (Objects.nonNull(type) && !type.getId().equals(liveFeedbackType.getId())) {
+            throw new CustomException("类型名称已存在");
+        }
+
+        type.setTypeName(liveFeedbackType.getTypeName());
+        type.setStatus(liveFeedbackType.getStatus());
+        type.setUpdateTime(LocalDateTime.now());
+        return liveFeedbackTypeMapper.updateLiveFeedbackType(type);
+    }
+
+    /**
+     * 删除直播反馈类型
+     */
+    @Override
+    public int deleteLiveFeedbackTypeByIds(Long[] typeIds) {
+        return liveFeedbackTypeMapper.deleteLiveFeedbackTypeByIds(typeIds);
+    }
+}

+ 57 - 0
fs-service-system/src/main/java/com/fs/liveStream/service/impl/LiveStreamerServiceImpl.java

@@ -0,0 +1,57 @@
+package com.fs.liveStream.service.impl;
+
+import com.fs.liveStream.domain.LiveStreamer;
+import com.fs.liveStream.mapper.LiveStreamerMapper;
+import com.fs.liveStream.service.ILiveStreamerService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+@Slf4j
+@Service
+public class LiveStreamerServiceImpl implements ILiveStreamerService {
+
+    @Resource
+    private LiveStreamerMapper liveStreamerMapper;
+
+    /**
+     * 根据手机号查询主播记录
+     */
+    @Override
+    public LiveStreamer selectStreamerByPhone(String phone) {
+        return liveStreamerMapper.selectStreamerByPhone(phone);
+    }
+
+    /**
+     * 根据unionid查询主播记录
+     */
+    @Override
+    public LiveStreamer selectStreamerByUnionId(String unionid) {
+        return liveStreamerMapper.selectStreamerByUnionId(unionid);
+    }
+
+    /**
+     * 保存主播记录
+     */
+    @Override
+    public int saveStreamer(LiveStreamer streamer) {
+        return liveStreamerMapper.insertStreamer(streamer);
+    }
+
+    /**
+     * 更新主播信息
+     */
+    @Override
+    public int updateStreamer(LiveStreamer streamer) {
+        return liveStreamerMapper.updateStreamer(streamer);
+    }
+
+    /**
+     * 查询主播信息
+     */
+    @Override
+    public LiveStreamer selectStreamerById(Long id) {
+        return liveStreamerMapper.selectStreamerById(id);
+    }
+}

+ 40 - 0
fs-service-system/src/main/java/com/fs/liveStream/vo/LiveFeedbackLogVO.java

@@ -0,0 +1,40 @@
+package com.fs.liveStream.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class LiveFeedbackLogVO {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 主播ID
+     */
+    private Long streamerId;
+
+    /**
+     * 主播名称
+     */
+    private String streamerName;
+
+    /**
+     * 反馈内容
+     */
+    private String content;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 反馈类型列表
+     */
+    private List<String> typeList;
+}

+ 24 - 0
fs-service-system/src/main/java/com/fs/liveStream/vo/LiveFeedbackTypeVO.java

@@ -0,0 +1,24 @@
+package com.fs.liveStream.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class LiveFeedbackTypeVO {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 反馈问题类型名称
+     */
+    private String typeName;
+
+    /**
+     * 下级问题类型
+     */
+    List<LiveFeedbackTypeVO> children;
+}

+ 24 - 0
fs-service-system/src/main/java/com/fs/system/config/FsSmsConfig.java

@@ -0,0 +1,24 @@
+package com.fs.system.config;
+
+import lombok.Data;
+
+@Data
+public class FsSmsConfig {
+    private String type;
+    private String rfAccount1;
+    private String rfCode1;
+    private String rfPassword1;
+    private String rfUrl1;
+    private String rfAccount2;
+    private String rfCode2;
+    private String rfPassword2;
+    private String rfUrl2;
+    private String rfSign;
+    private String rfextno;
+    private String dhAccount1;
+    private String dhPassword1;
+    private String dhAccount2;
+    private String dhPassword2;
+    private String dhSign;
+
+}

+ 5 - 1
fs-service-system/src/main/resources/mapper/company/CompanySmsLogsMapper.xml

@@ -20,11 +20,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="stat"    column="stat"    />
         <result property="replyContent"    column="reply_content"    />
         <result property="number"    column="number"    />
+        <result property="type"    column="type"    />
 
     </resultMap>
 
     <sql id="selectCompanySmsLogsVo">
-        select logs_id, company_id, company_user_id, customer_id, temp_id, temp_code, phone, content, create_time, send_time, status,mid,stat,reply_content,number from company_sms_logs
+        select logs_id, company_id, company_user_id, customer_id, temp_id, temp_code, phone, content, create_time, send_time, status,mid,stat,reply_content,number,type from company_sms_logs
     </sql>
 
 
@@ -51,6 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="stat != null">stat,</if>
             <if test="replyContent != null">reply_content,</if>
             <if test="number != null">number,</if>
+            <if test="type != null">type,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
@@ -67,6 +69,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="stat != null">#{stat},</if>
             <if test="replyContent != null">#{replyContent},</if>
             <if test="number != null">#{number},</if>
+            <if test="type != null">#{type},</if>
          </trim>
     </insert>
 
@@ -87,6 +90,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="stat != null">stat = #{stat},</if>
             <if test="replyContent != null">reply_content = #{replyContent},</if>
             <if test="number != null">number = #{number},</if>
+            <if test="type != null">type = #{type},</if>
         </trim>
         where logs_id = #{logsId}
     </update>

+ 44 - 0
fs-service-system/src/main/resources/mapper/live/LiveMapper.xml

@@ -236,6 +236,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </if>
     </delete>
 
+    <delete id="deleteLiveByAnchorAndLiveIds">
+        delete from live where live.anchor_id = #{userId} and live_id in
+        <foreach item="liveId" collection="liveIds" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+    </delete>
+
     <update id="updateBatchLiveList" parameterType="com.fs.live.vo.LiveListVo">
         update live set is_show = #{liveVo.isShow},status = 3,finish_time = NOW() where live_id in
         <foreach item="liveId" collection="liveVo.liveIds" open="(" separator="," close=")">
@@ -358,5 +365,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by create_time desc
     </select>
 
+    <select id="selectLiveListByMap" resultType="com.fs.live.domain.Live">
+        select * from live where is_del = 0
+        <if test="params.liveName != null and params.liveName != ''">
+            and live_name like concat('%', #{params.liveName}, '%')
+        </if>
+        <if test="params.status != null">
+            <choose>
+                <when test="params.status == 3">
+                    and status = 3
+                    <![CDATA[
+                    and finish_time < #{params.nowTime}
+                    ]]>
+                </when>
+                <otherwise>
+                    and status in (1,2)
+                    and finish_time >= #{params.nowTime}
+                    and is_show = 1
+                </otherwise>
+            </choose>
+        </if>
+        <if test="params.anchorId != null">
+            and anchor_id = #{params.anchorId}
+        </if>
+        order by create_time desc
+    </select>
+
+    <select id="getLiveCountByMap" resultType="java.lang.Integer">
+        select count(1) from live
+        where is_del = 0
+          and status in (1,2)
+          and is_show = 1
+          and anchor_id = #{params.anchorId}
+          <![CDATA[
+          and start_time <= #{params.nowTime}
+          ]]>
+          and (finish_time >= #{params.nowTime} or finish_time is null)
+    </select>
 
 </mapper>

+ 54 - 0
fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackLogMapper.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.liveStream.mapper.LiveFeedbackLogMapper">
+
+    <insert id="insertFeedbackLog" parameterType="LiveFeedbackLog" useGeneratedKeys="true" keyProperty="id">
+        insert into live_feedback_log
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="streamerId != null">streamer_id,</if>
+            <if test="content != null">content,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="streamerId != null">#{streamerId},</if>
+            <if test="content != null">#{content},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+    </insert>
+
+    <resultMap id="LiveFeedbackLogVOResultMap" type="LiveFeedbackLogVO">
+        <id property="id" column="id"/>
+        <result property="streamerId" column="streamer_id"/>
+        <result property="streamerName" column="streamer_name"/>
+        <result property="content" column="content"/>
+        <result property="createTime" column="create_time"/>
+        <collection property="typeList" ofType="string" javaType="java.util.ArrayList">
+            <result column="type_name"/>
+        </collection>
+    </resultMap>
+
+    <select id="selectFeedbackLogList" parameterType="com.fs.liveStream.param.LiveFeedbackLogParam" resultMap="LiveFeedbackLogVOResultMap">
+        SELECT
+        l.id,
+        l.streamer_id,
+        s.nick_name as streamer_name,
+        l.content,
+        l.create_time,
+        t.type_name
+        FROM live_feedback_log l
+        LEFT JOIN live_streamer s ON l.streamer_id = s.id
+        LEFT JOIN live_feedback_type_log tl ON l.id = tl.log_id
+        LEFT JOIN live_feedback_type t ON tl.type_id = t.id
+        <where>
+            <if test="streamerName != null and streamerName != ''">
+                AND s.nick_name like concat('%', #{streamerName}, '%')
+            </if>
+            <if test="typeId != null">
+                AND t.id = #{typeId}
+            </if>
+        </where>
+        ORDER BY l.create_time DESC
+    </select>
+</mapper>

+ 14 - 0
fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackTypeLogMapper.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.liveStream.mapper.LiveFeedbackTypeLogMapper">
+
+    <insert id="insertBatch">
+        insert into live_feedback_type_log (type_id, log_id)
+        values
+        <foreach collection="typeLogList" item="item" separator=",">
+            (#{item.typeId}, #{item.logId})
+        </foreach>
+    </insert>
+</mapper>

+ 75 - 0
fs-service-system/src/main/resources/mapper/liveStream/LiveFeedbackTypeMapper.xml

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.liveStream.mapper.LiveFeedbackTypeMapper">
+
+    <insert id="insertLiveFeedbackType" parameterType="LiveFeedbackType" useGeneratedKeys="true" keyProperty="id">
+        insert into live_feedback_type
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="typeName != null and typeName != ''">type_name,</if>
+            <if test="parentId != null">parent_id,</if>
+            <if test="status != null">`status`,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        values
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="typeName != null and typeName != ''">#{typeName},</if>
+            <if test="parentId != null">#{parentId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLiveFeedbackType" parameterType="LiveFeedbackType">
+        update live_feedback_type
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="typeName != null and typeName != ''">type_name = #{typeName},</if>
+            <if test="parentId != null">parent_id = #{parentId},</if>
+            <if test="status != null">`status` = #{status},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="deleteLiveFeedbackTypeByIds">
+        update live_feedback_type set is_del = 1 where id in
+        <foreach item="id" index="index" collection="typeIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <select id="selectFeedbackTypes" resultType="com.fs.liveStream.domain.LiveFeedbackType">
+        select * from live_feedback_type where is_del = 0 and status = 1
+    </select>
+
+    <select id="selectFeedbackTypeListByTypeIds" resultType="com.fs.liveStream.domain.LiveFeedbackType">
+        select * from live_feedback_type where id in
+        <foreach item="id" index="index" collection="typeIds" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <select id="selectFeedbackTypeList" parameterType="LiveFeedbackType" resultType="LiveFeedbackType">
+        select * from live_feedback_type
+        <where>
+            is_del = 0
+            <if test="id != null">
+                and id = #{id}
+             </if>
+            <if test="typeName != null and typeName != ''">
+                 and type_name like concat('%', #{typeName}, '%')
+             </if>
+            <if test="status != null">
+                 and `status` = #{status}
+            </if>
+        </where>
+    </select>
+
+</mapper>

+ 93 - 0
fs-service-system/src/main/resources/mapper/liveStream/LiveStreamerMapper.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.liveStream.mapper.LiveStreamerMapper">
+
+    <resultMap id="LiveStreamerResultMap" type="com.fs.liveStream.domain.LiveStreamer">
+        <result property="id" column="id"/>
+        <result property="nickName" column="nick_name"/>
+        <result property="avatar" column="avatar"/>
+        <result property="phone" column="phone"/>
+        <result property="password" column="password"/>
+        <result property="openId" column="open_id"/>
+        <result property="unionId" column="union_id"/>
+        <result property="status" column="status"/>
+        <result property="isDel" column="is_del"/>
+        <result property="remark" column="remark"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="lastIp" column="last_ip"/>
+        <result property="isRealNameVerify" column="is_real_name_verify"/>
+        <result property="idCardFrontUrl" column="id_card_front_url"/>
+        <result property="idCardBackUrl" column="id_card_back_url"/>
+    </resultMap>
+
+    <insert id="insertStreamer" parameterType="LiveStreamer" useGeneratedKeys="true" keyProperty="id">
+        insert into live_streamer
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="nickName != null">nick_name,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="phone != null">phone,</if>
+            <if test="password != null">`password`,</if>
+            <if test="openId != null">open_id,</if>
+            <if test="unionId != null">union_id,</if>
+            <if test="status != null">`status`,</if>
+            <if test="isDel != null">is_del,</if>
+            <if test="remark != null">remark,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="lastIp != null">last_ip,</if>
+            <if test="isRealNameVerify != null">is_real_name_verify,</if>
+            <if test="idCardFrontUrl != null">id_card_front_url,</if>
+            <if test="idCardBackUrl != null">id_card_back_url,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="nickName != null">#{nickName},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="phone != null">#{phone},</if>
+            <if test="password != null">#{password},</if>
+            <if test="openId != null">#{openId},</if>
+            <if test="unionId != null">#{unionId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="isDel != null">#{isDel},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="lastIp != null">#{lastIp},</if>
+            <if test="isRealNameVerify != null">#{isRealNameVerify},</if>
+            <if test="idCardFrontUrl != null">#{idCardFrontUrl},</if>
+            <if test="idCardBackUrl != null">#{idCardBackUrl},</if>
+        </trim>
+    </insert>
+
+    <update id="updateStreamer" parameterType="LiveStreamer">
+        update live_streamer
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="password != null">`password` = #{password},</if>
+            <if test="openId != null">open_id = #{openId},</if>
+            <if test="unionId != null">union_id = #{unionId},</if>
+            <if test="status != null">`status` = #{status},</if>
+            <if test="isDel != null">is_del = #{isDel},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="lastIp != null">last_ip = #{lastIp},</if>
+            <if test="isRealNameVerify != null">is_real_name_verify = #{isRealNameVerify},</if>
+            <if test="idCardFrontUrl != null">id_card_front_url = #{idCardFrontUrl},</if>
+            <if test="idCardBackUrl != null">id_card_back_url = #{idCardBackUrl},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <select id="selectStreamerByPhone" resultType="com.fs.liveStream.domain.LiveStreamer">
+        select * from live_streamer where phone = #{phone} and is_del = 0
+    </select>
+
+    <select id="selectStreamerByUnionId" resultType="com.fs.liveStream.domain.LiveStreamer">
+        select * from live_streamer where union_id = #{unionId} and is_del = 0
+    </select>
+</mapper>

+ 1 - 0
pom.xml

@@ -220,6 +220,7 @@
         <module>fs-socket</module>
         <module>fs-sync</module>
         <module>fs-live-socket</module>
+        <module>fs-live-streamer</module>
     </modules>
     <packaging>pom</packaging>