Sfoglia il codice sorgente

直播间独立部署

yuhongqi 2 mesi fa
parent
commit
5f82650bfc
100 ha cambiato i file con 6088 aggiunte e 18 eliminazioni
  1. 1 0
      .gitignore
  2. 4 1
      deploy.sh
  3. 47 1
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  4. 29 10
      fs-admin/src/main/java/com/fs/live/controller/LiveController.java
  5. 158 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCouponController.java
  6. 103 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCouponIssueController.java
  7. 103 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCouponIssueUserController.java
  8. 106 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCouponUserController.java
  9. 4 3
      fs-admin/src/main/java/com/fs/task/LiveTask.java
  10. 3 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  11. 198 0
      fs-live-socket/pom.xml
  12. 27 0
      fs-live-socket/src/main/java/com/fs/LiveSocketAppApplication.java
  13. 79 0
      fs-live-socket/src/main/java/com/fs/core/PermissionService.java
  14. 188 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/DataScopeAspect.java
  15. 91 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java
  16. 58 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLock.java
  17. 113 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockAspect.java
  18. 13 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockConstant.java
  19. 24 0
      fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockException.java
  20. 31 0
      fs-live-socket/src/main/java/com/fs/core/config/ApplicationConfig.java
  21. 123 0
      fs-live-socket/src/main/java/com/fs/core/config/DruidConfig.java
  22. 72 0
      fs-live-socket/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  23. 61 0
      fs-live-socket/src/main/java/com/fs/core/config/FilterConfig.java
  24. 109 0
      fs-live-socket/src/main/java/com/fs/core/config/MyBatisConfig.java
  25. 91 0
      fs-live-socket/src/main/java/com/fs/core/config/RedisConfig.java
  26. 66 0
      fs-live-socket/src/main/java/com/fs/core/config/ResourcesConfig.java
  27. 41 0
      fs-live-socket/src/main/java/com/fs/core/config/SecurityConfig.java
  28. 33 0
      fs-live-socket/src/main/java/com/fs/core/config/ServerConfig.java
  29. 124 0
      fs-live-socket/src/main/java/com/fs/core/config/SwaggerConfig.java
  30. 63 0
      fs-live-socket/src/main/java/com/fs/core/config/ThreadPoolConfig.java
  31. 77 0
      fs-live-socket/src/main/java/com/fs/core/config/properties/DruidProperties.java
  32. 27 0
      fs-live-socket/src/main/java/com/fs/core/datasource/DynamicDataSource.java
  33. 45 0
      fs-live-socket/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java
  34. 56 0
      fs-live-socket/src/main/java/com/fs/core/interceptor/RepeatSubmitInterceptor.java
  35. 126 0
      fs-live-socket/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java
  36. 53 0
      fs-live-socket/src/main/java/com/fs/core/security/SecurityUtils.java
  37. 12 0
      fs-live-socket/src/main/java/com/fs/live/annotation/Login.java
  38. 15 0
      fs-live-socket/src/main/java/com/fs/live/annotation/LoginUser.java
  39. 140 0
      fs-live-socket/src/main/java/com/fs/live/config/ProductionWordFilter.java
  40. 27 0
      fs-live-socket/src/main/java/com/fs/live/config/WebMvcConfig.java
  41. 20 0
      fs-live-socket/src/main/java/com/fs/live/config/WebSocketConfig.java
  42. 13 0
      fs-live-socket/src/main/java/com/fs/live/constant/CommonConstant.java
  43. 122 0
      fs-live-socket/src/main/java/com/fs/live/controller/LiveController.java
  44. 51 0
      fs-live-socket/src/main/java/com/fs/live/exception/FSException.java
  45. 98 0
      fs-live-socket/src/main/java/com/fs/live/exception/FSExceptionHandler.java
  46. 22 0
      fs-live-socket/src/main/java/com/fs/live/param/TestMoneyParam.java
  47. 24 0
      fs-live-socket/src/main/java/com/fs/live/redis/RedisConfiguration.java
  48. 85 0
      fs-live-socket/src/main/java/com/fs/live/utils/CityTreeUtil.java
  49. 26 0
      fs-live-socket/src/main/java/com/fs/live/utils/JsonUtils.java
  50. 95 0
      fs-live-socket/src/main/java/com/fs/live/utils/JwtUtils.java
  51. 46 0
      fs-live-socket/src/main/java/com/fs/live/utils/VerifyUtils.java
  52. 23 0
      fs-live-socket/src/main/java/com/fs/live/vo/CityVO.java
  53. 35 0
      fs-live-socket/src/main/java/com/fs/live/vo/IndexVO.java
  54. 23 0
      fs-live-socket/src/main/java/com/fs/live/vo/LiveInfoVo.java
  55. 50 0
      fs-live-socket/src/main/java/com/fs/live/vo/LiveVo.java
  56. 17 0
      fs-live-socket/src/main/java/com/fs/live/vo/LotteryVo.java
  57. 101 0
      fs-live-socket/src/main/java/com/fs/live/websocket/auth/AuthHandler.java
  58. 88 0
      fs-live-socket/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  59. 13 0
      fs-live-socket/src/main/java/com/fs/live/websocket/bean/MsgBean.java
  60. 37 0
      fs-live-socket/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java
  61. 20 0
      fs-live-socket/src/main/java/com/fs/live/websocket/constant/AttrConstant.java
  62. 53 0
      fs-live-socket/src/main/java/com/fs/live/websocket/constant/NickNameConstant.java
  63. 268 0
      fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java
  64. 97 0
      fs-live-socket/src/main/java/com/fs/live/websocket/service/NettyServerRunner.java
  65. 471 0
      fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  66. 1 0
      fs-live-socket/src/main/resources/META-INF/spring-devtools.properties
  67. 79 0
      fs-live-socket/src/main/resources/application-dev.yml
  68. 79 0
      fs-live-socket/src/main/resources/application-druid-test.yml
  69. 78 0
      fs-live-socket/src/main/resources/application-druid.yml
  70. 122 0
      fs-live-socket/src/main/resources/application.yml
  71. 2 0
      fs-live-socket/src/main/resources/banner.txt
  72. BIN
      fs-live-socket/src/main/resources/fx.jpg
  73. 36 0
      fs-live-socket/src/main/resources/i18n/messages.properties
  74. 15 0
      fs-live-socket/src/main/resources/mybatis/mybatis-config.xml
  75. BIN
      fs-live-socket/src/main/resources/qr.jpg
  76. BIN
      fs-live-socket/src/main/resources/simsunb.ttf
  77. 1 0
      fs-live-socket/src/main/resources/static/S8Zw463cFc.txt
  78. 21 0
      fs-live-socket/src/main/resources/templates/privacyPolicy.html
  79. 21 0
      fs-live-socket/src/main/resources/templates/userAgreement.html
  80. 61 0
      fs-live-socket/src/test/java/com/fs/core/security/BaseSpringBootTest.java
  81. 2 0
      fs-service-system/src/main/java/com/fs/erp/service/IErpOrderService.java
  82. 35 0
      fs-service-system/src/main/java/com/fs/erp/service/impl/ErpOrderServiceImpl.java
  83. 5 0
      fs-service-system/src/main/java/com/fs/erp/service/impl/ErpOrderServiceProxy.java
  84. 132 0
      fs-service-system/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  85. 1 0
      fs-service-system/src/main/java/com/fs/live/domain/Live.java
  86. 70 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveCoupon.java
  87. 67 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveCouponIssue.java
  88. 35 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveCouponIssueUser.java
  89. 73 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveCouponUser.java
  90. 2 0
      fs-service-system/src/main/java/com/fs/live/domain/LiveOrderItem.java
  91. 61 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java
  92. 61 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponIssueUserMapper.java
  93. 66 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponMapper.java
  94. 61 0
      fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java
  95. 6 2
      fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java
  96. 9 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveAfterSalesService.java
  97. 61 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveCouponIssueService.java
  98. 61 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveCouponIssueUserService.java
  99. 64 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveCouponService.java
  100. 61 0
      fs-service-system/src/main/java/com/fs/live/service/ILiveCouponUserService.java

+ 1 - 0
.gitignore

@@ -55,3 +55,4 @@ hs_err_pid*
 #1.配置语法: 以斜杠“/”开头表示目录: 以星号“*”通配多个字符: 以问号“?”通配单个字符 以方括号“[]”包含单个字符的匹配列表: 以叹号“!”表示不忽略(跟踪)匹配到的文件或目录:
 #此外,git 对于 .ignore 配置文件是按行从上到下进行规则匹配的,意味着如果前面的规则匹配的范围更大,则后面的规则将不会生效
 /fs_hospital.sql
+/fs-live-socket/src/test/java/com/fs/core/security/SecurityUtilsTest.java

+ 4 - 1
deploy.sh

@@ -10,6 +10,7 @@ LOCAL_FS_ADMIN_JAR="./fs-admin/target/fs-admin.jar" # 假设target目录和你
 LOCAL_FS_COMPANY_JAR="./fs-company/target/fs-company.jar"
 LOCAL_FS_USER_APP_JAR="./fs-user-app/target/fs-user-app.jar"
 LOCAL_FS_API_APP_JAR="./fs-api/target/fs-api.jar"
+LOCAL_FS_LIVE_SOCKET_JAR="./fs-live-socket/target/fs-live-socket.jar"
 LOCAL_FS_SYNC_APP_JAR="./fs-sync/target/fs-sync-0.0.1-SNAPSHOT.jar"
 
 # 函数:检查本地文件是否存在
@@ -24,6 +25,7 @@ check_local_file() {
 check_local_file "$LOCAL_FS_ADMIN_JAR"
 check_local_file "$LOCAL_FS_COMPANY_JAR"
 check_local_file "$LOCAL_FS_USER_APP_JAR"
+check_local_file "$LOCAL_FS_LIVE_SOCKET_JAR"
 check_local_file "$LOCAL_FS_API_APP_JAR"
 # 停止远程服务器上可能正在运行的旧版本(假设进程名与 JAR 包名相同)
 stop_remote_app() {
@@ -62,7 +64,8 @@ deploy_jar() {
 #deploy_jar "$LOCAL_FS_USER_APP_JAR" "fs-user-app"
 #deploy_jar "$LOCAL_FS_API_APP_JAR" "fs-api"
 
-deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync"
+#deploy_jar "$LOCAL_FS_SYNC_APP_JAR" "fs-sync"
+deploy_jar "$LOCAL_FS_LIVE_SOCKET_JAR" "fs-live-socket"
 
 echo "Deployment completed."
 

+ 47 - 1
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -3,16 +3,22 @@ package com.fs.live.controller;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.core.security.LoginUser;
+import com.fs.core.web.service.TokenService;
 import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.service.ILiveAfterSalesService;
 import com.fs.live.vo.LiveAfterSalesVo;
+import com.fs.store.param.*;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.text.ParseException;
 import java.util.List;
 
 /**
@@ -22,12 +28,15 @@ import java.util.List;
  * @date 2025-07-08
  */
 @RestController
-@RequestMapping("/live/liveAfteraSales")
+@RequestMapping("/live/liveAfterSales")
 public class LiveAfterSalesController extends BaseController
 {
     @Autowired
     private ILiveAfterSalesService liveAfterSalesService;
 
+    @Autowired
+    private TokenService tokenService;
+
     /**
      * 查询售后记录列表
      */
@@ -95,4 +104,41 @@ public class LiveAfterSalesController extends BaseController
     {
         return toAjax(liveAfterSalesService.deleteLiveAfterSalesByIds(ids));
     }
+
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:audit1')")
+    @PostMapping("/audit1")
+    //平台审核
+    public R audit1(@RequestBody LiveAfterSalesAudit1Param param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setOperator(loginUser.getUser().getNickName());
+        return liveAfterSalesService.audit1(param);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:audit2')")
+    @PostMapping("/audit2")
+    //仓库审核
+    public R audit2(@RequestBody LiveAfterSalesAudit2Param param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setOperator(loginUser.getUser().getNickName());
+        return liveAfterSalesService.audit2(param);
+    }
+    //财务审核
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:refund')")
+    @PostMapping("/refund")
+    public R refund(@RequestBody LiveAfterSalesRefundParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setOperator(loginUser.getUser().getNickName());
+        return liveAfterSalesService.refundMoney(param);
+    }
+    //平台撤销
+    @PreAuthorize("@ss.hasPermi('store:storeAfterSales:cancel')")
+    @PostMapping("/cancel")
+    public R cancel(@RequestBody LiveAfterSalesCancelParam param) throws ParseException {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setOperator(loginUser.getUser().getNickName());
+        return liveAfterSalesService.cancel(param);
+    }
 }

+ 29 - 10
fs-admin/src/main/java/com/fs/live/controller/LiveController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
 import com.fs.core.security.SecurityUtils;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveService;
@@ -53,16 +54,6 @@ public class LiveController extends BaseController {
         return util.exportExcel(list, "直播数据");
     }
 
-    /**
-     * 导出直播列表
-     */
-    @PreAuthorize("@ss.hasPermi('live:live:edit')")
-    @GetMapping("/finishLive")
-    public R finishLive(Live live) {
-        Long userId = SecurityUtils.getLoginUser().getUser().getUserId();
-        live.setCompanyUserId(userId);
-        return liveService.finishLive(live);
-    }
 
     /**
      * 获取直播详细信息
@@ -136,5 +127,33 @@ public class LiveController extends BaseController {
         return liveService.verifyIdInfo(payload);
     }
 
+    /**
+     * 结束直播
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @GetMapping("/finishLive")
+    public R finishLive(Live live) {
+        return liveService.finishLive(live);
+    }
+
+    /**
+     * 复制直播
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @GetMapping("/copyLive")
+    public R copyLive(Live live) {
+
+        return liveService.copyLive(live);
+    }
+
+    /**
+     * 开启直播
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @GetMapping("/startLive")
+    public R startLive(Live live) {
+        return liveService.startLive(live);
+    }
+
 
 }

+ 158 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCouponController.java

@@ -0,0 +1,158 @@
+package com.fs.live.controller;
+
+import java.util.Date;
+import java.util.List;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.domain.LiveCouponIssue;
+import com.fs.live.service.ILiveCouponIssueService;
+import com.fs.store.domain.FsStoreCoupon;
+import com.fs.store.domain.FsStoreCouponIssue;
+import com.fs.store.param.FsStoreCouponPublishParam;
+import com.fs.store.param.LiveCouponPublishParam;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveCoupon;
+import com.fs.live.service.ILiveCouponService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 优惠券Controller
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@RestController
+@RequestMapping("/live/coupon")
+public class LiveCouponController extends BaseController
+{
+    @Autowired
+    private ILiveCouponService liveCouponService;
+    @Autowired
+    private ILiveCouponIssueService liveCouponIssueService;
+
+
+    /**
+     * 查询优惠券列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveCoupon liveCoupon)
+    {
+        startPage();
+        liveCoupon.setIsDel(0);
+        List<LiveCoupon> list = liveCouponService.selectLiveCouponList(liveCoupon);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出优惠券列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:export')")
+    @Log(title = "优惠券", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveCoupon liveCoupon)
+    {
+        List<LiveCoupon> list = liveCouponService.selectLiveCouponList(liveCoupon);
+        ExcelUtil<LiveCoupon> util = new ExcelUtil<LiveCoupon>(LiveCoupon.class);
+        return util.exportExcel(list, "coupon");
+    }
+
+    /**
+     * 获取优惠券详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:query')")
+    @GetMapping(value = "/{couponId}")
+    public AjaxResult getInfo(@PathVariable("couponId") Long couponId)
+    {
+        return AjaxResult.success(liveCouponService.selectLiveCouponById(couponId));
+    }
+
+    /**
+     * 新增优惠券
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:add')")
+    @Log(title = "优惠券", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveCoupon liveCoupon)
+    {
+        return toAjax(liveCouponService.insertLiveCoupon(liveCoupon));
+    }
+
+    /**
+     * 修改优惠券
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:edit')")
+    @Log(title = "优惠券", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveCoupon liveCoupon)
+    {
+        return toAjax(liveCouponService.updateLiveCoupon(liveCoupon));
+    }
+
+    /**
+     * 删除优惠券
+     */
+//    @PreAuthorize("@ss.hasPermi('system:coupon:remove')")
+    @Log(title = "优惠券", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{couponIds}")
+    public AjaxResult remove(@PathVariable Long[] couponIds)
+    {
+        return toAjax(liveCouponService.deleteLiveCouponByIds(couponIds));
+    }
+
+//    @PreAuthorize("@ss.hasPermi('store:storeCoupon:publish')")
+    @PostMapping("/publish")
+    public AjaxResult publish(@RequestBody LiveCouponPublishParam publishParam)
+    {
+        LiveCoupon coupon=liveCouponService.selectLiveCouponById(publishParam.getCouponId());
+        LiveCouponIssue issue=new LiveCouponIssue();
+        issue.setCouponId(publishParam.getCouponId());
+        issue.setCouponName(coupon.getTitle());
+        issue.setCouponType(Math.toIntExact(coupon.getType()));
+        issue.setStartTime(publishParam.getStartTime());
+        issue.setLimitTime(publishParam.getLimitTime());
+        issue.setTotalCount(Long.valueOf(publishParam.getTotalCount()));
+        issue.setRemainCount(0L);
+        issue.setIsPermanent(0);
+        issue.setStatus(1);
+        issue.setCreateTime(new Date());
+        return toAjax( liveCouponIssueService.insertLiveCouponIssue(issue));
+    }
+
+//    @PreAuthorize("@ss.hasPermi('store:storeCoupon:batchPublish')")
+    @PostMapping("/batchPublish")
+    public R batchPublish(@RequestBody FsStoreCouponPublishParam publishParam)
+    {
+
+        List<LiveCoupon> list=liveCouponService.selectLiveCouponByIds(publishParam.getIds());
+        for (LiveCoupon coupon :list ){
+            LiveCouponIssue issue=new LiveCouponIssue();
+            issue.setCouponId(coupon.getCouponId());
+            issue.setCouponName(coupon.getTitle());
+            issue.setCouponType(Math.toIntExact(coupon.getType()));
+            issue.setStartTime(publishParam.getStartTime());
+            issue.setLimitTime(publishParam.getLimitTime());
+            issue.setTotalCount(Long.valueOf(publishParam.getTotalCount()));
+            issue.setRemainCount(0L);
+            issue.setIsPermanent(0);
+            issue.setStatus(1);
+            issue.setCreateTime(new Date());
+            liveCouponIssueService.insertLiveCouponIssue(issue);
+        }
+        return R.ok();
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCouponIssueController.java

@@ -0,0 +1,103 @@
+package com.fs.live.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveCouponIssue;
+import com.fs.live.service.ILiveCouponIssueService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 优惠券领取Controller
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@RestController
+@RequestMapping("/live/coupon/issue")
+public class LiveCouponIssueController extends BaseController
+{
+    @Autowired
+    private ILiveCouponIssueService liveCouponIssueService;
+
+    /**
+     * 查询优惠券领取列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveCouponIssue liveCouponIssue)
+    {
+        startPage();
+        List<LiveCouponIssue> list = liveCouponIssueService.selectLiveCouponIssueList(liveCouponIssue);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出优惠券领取列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:export')")
+    @Log(title = "优惠券领取", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveCouponIssue liveCouponIssue)
+    {
+        List<LiveCouponIssue> list = liveCouponIssueService.selectLiveCouponIssueList(liveCouponIssue);
+        ExcelUtil<LiveCouponIssue> util = new ExcelUtil<LiveCouponIssue>(LiveCouponIssue.class);
+        return util.exportExcel(list, "issue");
+    }
+
+    /**
+     * 获取优惠券领取详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveCouponIssueService.selectLiveCouponIssueById(id));
+    }
+
+    /**
+     * 新增优惠券领取
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:add')")
+    @Log(title = "优惠券领取", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveCouponIssue liveCouponIssue)
+    {
+        return toAjax(liveCouponIssueService.insertLiveCouponIssue(liveCouponIssue));
+    }
+
+    /**
+     * 修改优惠券领取
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:edit')")
+    @Log(title = "优惠券领取", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveCouponIssue liveCouponIssue)
+    {
+        return toAjax(liveCouponIssueService.updateLiveCouponIssue(liveCouponIssue));
+    }
+
+    /**
+     * 删除优惠券领取
+     */
+//    @PreAuthorize("@ss.hasPermi('system:issue:remove')")
+    @Log(title = "优惠券领取", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveCouponIssueService.deleteLiveCouponIssueByIds(ids));
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCouponIssueUserController.java

@@ -0,0 +1,103 @@
+package com.fs.live.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveCouponIssueUser;
+import com.fs.live.service.ILiveCouponIssueUserService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 优惠券用户领取记录Controller
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@RestController
+@RequestMapping("/live/coupon/issue/user")
+public class LiveCouponIssueUserController extends BaseController
+{
+    @Autowired
+    private ILiveCouponIssueUserService liveCouponIssueUserService;
+
+    /**
+     * 查询优惠券用户领取记录列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveCouponIssueUser liveCouponIssueUser)
+    {
+        startPage();
+        List<LiveCouponIssueUser> list = liveCouponIssueUserService.selectLiveCouponIssueUserList(liveCouponIssueUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出优惠券用户领取记录列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:export')")
+    @Log(title = "优惠券用户领取记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveCouponIssueUser liveCouponIssueUser)
+    {
+        List<LiveCouponIssueUser> list = liveCouponIssueUserService.selectLiveCouponIssueUserList(liveCouponIssueUser);
+        ExcelUtil<LiveCouponIssueUser> util = new ExcelUtil<LiveCouponIssueUser>(LiveCouponIssueUser.class);
+        return util.exportExcel(list, "user");
+    }
+
+    /**
+     * 获取优惠券用户领取记录详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveCouponIssueUserService.selectLiveCouponIssueUserById(id));
+    }
+
+    /**
+     * 新增优惠券用户领取记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:add')")
+    @Log(title = "优惠券用户领取记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveCouponIssueUser liveCouponIssueUser)
+    {
+        return toAjax(liveCouponIssueUserService.insertLiveCouponIssueUser(liveCouponIssueUser));
+    }
+
+    /**
+     * 修改优惠券用户领取记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "优惠券用户领取记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveCouponIssueUser liveCouponIssueUser)
+    {
+        return toAjax(liveCouponIssueUserService.updateLiveCouponIssueUser(liveCouponIssueUser));
+    }
+
+    /**
+     * 删除优惠券用户领取记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:remove')")
+    @Log(title = "优惠券用户领取记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveCouponIssueUserService.deleteLiveCouponIssueUserByIds(ids));
+    }
+}

+ 106 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCouponUserController.java

@@ -0,0 +1,106 @@
+package com.fs.live.controller;
+
+import java.util.List;
+
+import com.fs.common.utils.ParseUtils;
+import com.fs.store.vo.FsStoreCouponUserVO;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.LiveCouponUser;
+import com.fs.live.service.ILiveCouponUserService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 优惠券发放记录Controller
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@RestController
+@RequestMapping("/live/coupon/user")
+public class LiveCouponUserController extends BaseController
+{
+    @Autowired
+    private ILiveCouponUserService liveCouponUserService;
+
+    /**
+     * 查询优惠券发放记录列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(LiveCouponUser liveCouponUser)
+    {
+        startPage();
+        List<LiveCouponUser> list = liveCouponUserService.selectLiveCouponUserList(liveCouponUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出优惠券发放记录列表
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:export')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(LiveCouponUser liveCouponUser)
+    {
+        List<LiveCouponUser> list = liveCouponUserService.selectLiveCouponUserList(liveCouponUser);
+        ExcelUtil<LiveCouponUser> util = new ExcelUtil<LiveCouponUser>(LiveCouponUser.class);
+        return util.exportExcel(list, "user");
+    }
+
+    /**
+     * 获取优惠券发放记录详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(liveCouponUserService.selectLiveCouponUserById(id));
+    }
+
+    /**
+     * 新增优惠券发放记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:add')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody LiveCouponUser liveCouponUser)
+    {
+        return toAjax(liveCouponUserService.insertLiveCouponUser(liveCouponUser));
+    }
+
+    /**
+     * 修改优惠券发放记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody LiveCouponUser liveCouponUser)
+    {
+        return toAjax(liveCouponUserService.updateLiveCouponUser(liveCouponUser));
+    }
+
+    /**
+     * 删除优惠券发放记录
+     */
+//    @PreAuthorize("@ss.hasPermi('system:user:remove')")
+    @Log(title = "优惠券发放记录", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(liveCouponUserService.deleteLiveCouponUserByIds(ids));
+    }
+}

+ 4 - 3
fs-admin/src/main/java/com/fs/task/LiveTask.java

@@ -31,6 +31,7 @@ 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.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import java.text.ParseException;
@@ -129,7 +130,7 @@ public class LiveTask {
         /**
          * 退款自动处理 24小时未审核自动审核通过 每小时执行一次
          */
-    @QuartzRunnable(name = "退款自动处理")
+    @QuartzRunnable(name = "直播售后自动审核")
     public void refundOp() {
         //获取所有退款申请
         List<LiveAfterSales> list = afterSalesService.selectLiveAfterSalesByDoAudit();
@@ -166,7 +167,7 @@ public class LiveTask {
     /**
      * 同步物流状态
      */
-    @QuartzRunnable(name = "同步物流状态")
+    @QuartzRunnable(name = "直播同步物流状态")
     public void syncExpress() {
         List<Long> ids = liveOrderService.selectSyncExpressIds();
         for (Long id : ids) {
@@ -177,7 +178,7 @@ public class LiveTask {
     /**
      * 更新发货状态
      */
-    @QuartzRunnable(name = "发货任务")
+    @QuartzRunnable(name = "直播发货任务")
     public void updateExpress() {
         List<LiveOrder> list = liveOrderService.selectUpdateExpress();
 

+ 3 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java

@@ -22,7 +22,7 @@ import java.util.List;
  * @date 2025-07-08
  */
 @RestController
-@RequestMapping("/live/liveAfteraSales")
+@RequestMapping("/live/liveAfterSales")
 public class LiveAfterSalesController extends BaseController
 {
     @Autowired
@@ -95,4 +95,6 @@ public class LiveAfterSalesController extends BaseController
     {
         return toAjax(liveAfterSalesService.deleteLiveAfterSalesByIds(ids));
     }
+
+
 }

+ 198 - 0
fs-live-socket/pom.xml

@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>fs-live-socket</artifactId>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </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>
+
+        <!-- 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>com.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- 系统模块-->
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service-system</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.javen205</groupId>
+            <artifactId>IJPay-All</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+            <version>4.8.62.ALL</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>javase</artifactId>
+            <version>3.4.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.7.30</version>
+            <scope>provided</scope>
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Redisson -->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson</artifactId>
+            <version>3.13.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <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>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>1.0.0</version>
+                <configuration>
+                    <imageName>${project.artifactId}:${project.version}</imageName>
+                    <baseImage>kdvolder/jdk8</baseImage>
+                    <maintainer>docker_maven docker_maven@email.com</maintainer>
+                    <workdir>/</workdir>
+                    <cmd>["java", "-version"]</cmd>
+                    <entryPoint>["java", "-jar", "${project.build.finalName}.jar"]</entryPoint>
+                    <!-- 这里是复制 jar 包到 docker 容器指定目录配置 -->
+                    <resources>
+                        <resource>
+                            <targetPath>/</targetPath>
+                            <directory>${project.build.directory}</directory>
+                            <include>${project.build.finalName}.jar</include>
+                        </resource>
+                    </resources>
+                    <dockerHost>http://8.140.143.122:2375</dockerHost>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+</project>

+ 27 - 0
fs-live-socket/src/main/java/com/fs/LiveSocketAppApplication.java

@@ -0,0 +1,27 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+
+/**
+ * 启动程序
+ */
+@EnableCaching
+@EnableAsync
+//@EnableScheduling
+@EnableWebSocket
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+public class LiveSocketAppApplication
+{
+    public static void main(String[] args)
+    {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(LiveSocketAppApplication.class, args);
+        System.out.println("Live-websocket启动成功 \n" );
+    }
+}

+ 79 - 0
fs-live-socket/src/main/java/com/fs/core/PermissionService.java

@@ -0,0 +1,79 @@
+package com.fs.core;
+
+import com.alibaba.druid.support.json.JSONUtils;
+import com.fs.live.utils.JwtUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * 自定义权限实现,ss取自SpringSecurity首字母
+ */
+@Service("ss")
+public class PermissionService
+{
+    @Autowired
+    JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+    /** 所有权限标识 */
+    private static final String ALL_PERMISSION = "*:*:*";
+
+
+
+    /**
+     * 验证用户是否具备某权限
+     *
+     * @param permission 权限字符串
+     * @return 用户是否具备某权限
+     */
+    public boolean hasPermi(String permission)
+    {
+
+        String headValue = ServletUtils.getRequest().getHeader("APPToken");
+        Claims claims=jwtUtils.getClaimByToken(headValue);
+        String userId = claims.getSubject().toString();
+        //获取用户所有的权限
+        String permss=redisCache.getCacheObject("perms:"+userId);
+        if(StringUtils.isNotEmpty(permss)){
+            List<String> perms= (List<String>) JSONUtils.parse(permss);
+            if (StringUtils.isEmpty(permission))
+            {
+                return false;
+            }
+            if (StringUtils.isNull(userId) || CollectionUtils.isEmpty(perms))
+            {
+                return false;
+            }
+            return hasPermissions(perms, permission);
+        }
+        else{
+            return false;
+        }
+
+
+    }
+
+
+
+    /**
+     * 判断是否包含权限
+     *
+     * @param permissions 权限列表
+     * @param permission 权限字符串
+     * @return 用户是否具备某权限
+     */
+    private boolean hasPermissions(List<String> permissions, String permission)
+    {
+        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
+    }
+
+
+}

+ 188 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/DataScopeAspect.java

@@ -0,0 +1,188 @@
+package com.fs.core.aspectj;
+
+import com.fs.live.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
+    {
+        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("APPToken");
+        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 ");
+                    sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+                }
+            }
+        }
+
+        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;
+    }
+}

+ 91 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/LiveWatchUserAspect.java

@@ -0,0 +1,91 @@
+package com.fs.core.aspectj;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.core.redis.RedisUtil;
+import com.fs.live.domain.LiveWatchUser;
+import com.fs.live.service.ILiveWatchUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@Aspect
+@Component
+@Slf4j
+public class LiveWatchUserAspect {
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+
+    @AfterReturning(pointcut = "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.insertLiveWatchUser(..)) || " +
+            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.updateLiveWatchUser(..)) || " +
+            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserById(..)) || " +
+            "execution(* com.fs.live.service.impl.LiveWatchUserServiceImpl.deleteLiveWatchUserByIds(..))",
+            returning = "result")
+    public void afterLiveWatchUserOperation(JoinPoint joinPoint, Object result) {
+        try {
+            String methodName = joinPoint.getSignature().getName();
+            Object[] args = joinPoint.getArgs();
+            log.info("直播观看用户数据发生变化,方法: {}, 参数: {}", methodName, Arrays.toString(args));
+            // 提取liveId并处理缓存更新
+            Set<Long> liveIds = extractLiveIds(methodName, args);
+            for (Long liveId : liveIds) {
+                liveWatchUserService.asyncToCache(liveId);
+            }
+        } catch (Exception e) {
+            log.error("执行直播观看用户变更后逻辑失败", e);
+        }
+    }
+
+    private Set<Long> extractLiveIds(String methodName, Object[] args) {
+        Set<Long> liveIds = new HashSet<>();
+        if (args == null || args.length == 0) {
+            return liveIds;
+        }
+        switch (methodName) {
+            case "insertLiveWatchUser":
+            case "updateLiveWatchUser":
+                // 参数是LiveWatchUser对象
+                if (args[0] instanceof LiveWatchUser) {
+                    LiveWatchUser liveWatchUser = (LiveWatchUser) args[0];
+                    if (liveWatchUser.getLiveId() != null) {
+                        liveIds.add(liveWatchUser.getLiveId());
+                    }
+                }
+                break;
+            case "deleteLiveWatchUserById":
+                // 参数是Long类型的id,需要先查询获取liveId
+                if (args[0] instanceof Long) {
+                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById((Long) args[0]);
+                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+                        liveIds.add(liveWatchUser.getLiveId());
+                    }
+                }
+                break;
+            case "deleteLiveWatchUserByIds":
+                // 参数是Long[]数组
+                if (args[0] instanceof Long[]) {
+                    Long[] ids = (Long[]) args[0];
+                    LiveWatchUser liveWatchUser = liveWatchUserService.selectLiveWatchUserById(ids[0]);
+                    if (ObjectUtil.isNotEmpty(liveWatchUser)) {
+                        liveIds.add(liveWatchUser.getLiveId());
+                    }
+                }
+                break;
+            default:
+                log.warn("未处理的方法: {}", methodName);
+        }
+        return liveIds;
+    }
+
+
+}

+ 58 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLock.java

@@ -0,0 +1,58 @@
+package com.fs.core.aspectj.lock;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 分布式锁注解
+ *
+ * @author Hollis
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DistributeLock {
+
+    /**
+     * 锁的场景
+     *
+     * @return
+     */
+    public String scene();
+
+    /**
+     * 加锁的key,优先取key(),如果没有,则取keyExpression()
+     *
+     * @return
+     */
+    public String key() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * SPEL表达式:
+     * <pre>
+     *     #id
+     *     #insertResult.id
+     * </pre>
+     *
+     * @return
+     */
+    public String keyExpression() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * 超时时间,毫秒
+     * 默认情况下不设置超时时间,会自动续期
+     *
+     * @return
+     */
+    public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;
+
+    public String errorMsg() default DistributeLockConstant.ERROR_MSG;
+
+    /**
+     * 加锁等待时长,毫秒
+     * 默认情况下不设置等待时长,会一直等待直到获取到锁
+     * @return
+     */
+    public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
+}

+ 113 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockAspect.java

@@ -0,0 +1,113 @@
+package com.fs.core.aspectj.lock;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Order(Integer.MIN_VALUE + 1)
+public class DistributeLockAspect {
+
+    private RedissonClient redissonClient;
+
+    public DistributeLockAspect(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(DistributeLockAspect.class);
+
+    @Around("@annotation(com.fs.core.aspectj.lock.DistributeLock)")
+    public Object process(ProceedingJoinPoint pjp) throws Throwable {
+        Object response = null;
+        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+        DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
+
+        String key = distributeLock.key();
+        if (DistributeLockConstant.NONE_KEY.equals(key)) {
+            if (DistributeLockConstant.NONE_KEY.equals(distributeLock.keyExpression())) {
+                throw new DistributeLockException("no lock key found...");
+            }
+            SpelExpressionParser parser = new SpelExpressionParser();
+            Expression expression = parser.parseExpression(distributeLock.keyExpression());
+
+            EvaluationContext context = new StandardEvaluationContext();
+            // 获取参数值
+            Object[] args = pjp.getArgs();
+
+            // 获取运行时参数的名称
+            StandardReflectionParameterNameDiscoverer discoverer
+                    = new StandardReflectionParameterNameDiscoverer();
+            String[] parameterNames = discoverer.getParameterNames(method);
+
+            // 将参数绑定到context中
+            if (parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    context.setVariable(parameterNames[i], args[i]);
+                }
+            }
+
+            // 解析表达式,获取结果
+            key = String.valueOf(expression.getValue(context));
+        }
+
+        String scene = distributeLock.scene();
+
+        String lockKey = scene + "#" + key;
+
+        int expireTime = distributeLock.expireTime();
+        int waitTime = distributeLock.waitTime();
+        RLock rLock= redissonClient.getLock(lockKey);
+        try {
+            boolean lockResult = false;
+            if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("lock for key : %s", lockKey));
+                    rLock.lock();
+                } else {
+                    LOG.info(String.format("lock for key : %s , expire : %s", lockKey, expireTime));
+                    rLock.lock(expireTime, TimeUnit.MILLISECONDS);
+                }
+                lockResult = true;
+            } else {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("try lock for key : %s , wait : %s", lockKey, waitTime));
+                    lockResult = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
+                } else {
+                    LOG.info(String.format("try lock for key : %s , expire : %s , wait : %s", lockKey, expireTime, waitTime));
+                    lockResult = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
+                }
+            }
+
+            if (!lockResult) {
+                LOG.warn(String.format("lock failed for key : %s , expire : %s", lockKey, expireTime));
+                throw new DistributeLockException(distributeLock.errorMsg());
+            }
+
+
+            LOG.info(String.format("lock success for key : %s , expire : %s", lockKey, expireTime));
+            response = pjp.proceed();
+        }  finally {
+            if (rLock.isHeldByCurrentThread()) {
+                rLock.unlock();
+                LOG.info(String.format("unlock for key : %s , expire : %s", lockKey, expireTime));
+            }
+        }
+        return response;
+    }
+}

+ 13 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockConstant.java

@@ -0,0 +1,13 @@
+package com.fs.core.aspectj.lock;
+
+public class DistributeLockConstant {
+
+    public static final String NONE_KEY = "NONE";
+
+    public static final String DEFAULT_OWNER = "DEFAULT";
+
+    public static final int DEFAULT_EXPIRE_TIME = -1;
+
+    public static final int DEFAULT_WAIT_TIME = Integer.MAX_VALUE;
+    public static final String ERROR_MSG  = "请勿重复操作";
+}

+ 24 - 0
fs-live-socket/src/main/java/com/fs/core/aspectj/lock/DistributeLockException.java

@@ -0,0 +1,24 @@
+package com.fs.core.aspectj.lock;
+
+
+public class DistributeLockException extends RuntimeException {
+
+    public DistributeLockException() {
+    }
+
+    public DistributeLockException(String message) {
+        super(message);
+    }
+
+    public DistributeLockException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DistributeLockException(Throwable cause) {
+        super(cause);
+    }
+
+    public DistributeLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}

+ 31 - 0
fs-live-socket/src/main/java/com/fs/core/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.core.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());
+    }
+}

+ 123 - 0
fs-live-socket/src/main/java/com/fs/core/config/DruidConfig.java

@@ -0,0 +1,123 @@
+package com.fs.core.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.core.config.properties.DruidProperties;
+import com.fs.core.datasource.DynamicDataSource;
+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;
+
+/**
+ * druid 配置多数据源
+ * 
+
+ */
+@Configuration
+public class DruidConfig
+{
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.master")
+    public DataSource masterDataSource(DruidProperties druidProperties)
+    {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean
+    @ConfigurationProperties("spring.datasource.druid.slave")
+    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
+    public DataSource slaveDataSource(DruidProperties druidProperties)
+    {
+        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+        return druidProperties.dataSource(dataSource);
+    }
+
+    @Bean(name = "dynamicDataSource")
+    @Primary
+    public DynamicDataSource dataSource(DataSource masterDataSource)
+    {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+    
+    /**
+     * 设置数据源
+     * 
+     * @param targetDataSources 备选数据源集合
+     * @param sourceName 数据源名称
+     * @param beanName bean名称
+     */
+    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
+    {
+        try
+        {
+            DataSource dataSource = SpringUtils.getBean(beanName);
+            targetDataSources.put(sourceName, dataSource);
+        }
+        catch (Exception e)
+        {
+        }
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @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-socket/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.core.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);
+    }
+}

+ 61 - 0
fs-live-socket/src/main/java/com/fs/core/config/FilterConfig.java

@@ -0,0 +1,61 @@
+package com.fs.core.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.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
+public class FilterConfig
+{
+    @Value("${xss.enabled}")
+    private String enabled;
+
+    @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);
+        initParameters.put("enabled", enabled);
+        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-socket/src/main/java/com/fs/core/config/MyBatisConfig.java

@@ -0,0 +1,109 @@
+package com.fs.core.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();
+    }
+}

+ 91 - 0
fs-live-socket/src/main/java/com/fs/core/config/RedisConfig.java

@@ -0,0 +1,91 @@
+package com.fs.core.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.config.Config;
+import org.springframework.beans.factory.annotation.Value;
+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.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+
+    @Value("${spring.redis.host:localhost}")
+    private String host;
+
+    @Value("${spring.redis.port:6379}")
+    private int port;
+
+    @Value("${spring.redis.password:}")
+    private String password;
+
+    @Value("${spring.redis.database:0}")
+    private int database;
+
+    @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;
+    }
+
+    /**
+     * 配置Redisson客户端
+     */
+    @Bean
+    public RedissonClient redissonClient() {
+        Config config = new Config();
+
+        // 构建Redis连接地址
+        String redisUrl = "redis://" + host + ":" + port;
+
+        // 单节点模式配置
+        if (password != null && !password.isEmpty()) {
+            config.useSingleServer()
+                    .setAddress(redisUrl)
+                    .setDatabase(database)
+                    .setPassword(password);
+        } else {
+            config.useSingleServer()
+                    .setAddress(redisUrl)
+                    .setDatabase(database);
+        }
+
+        // 连接池配置
+        config.useSingleServer()
+                .setConnectionMinimumIdleSize(8)
+                .setConnectionPoolSize(32)
+                .setIdleConnectionTimeout(10000);
+
+        return Redisson.create(config);
+    }
+}

+ 66 - 0
fs-live-socket/src/main/java/com/fs/core/config/ResourcesConfig.java

@@ -0,0 +1,66 @@
+package com.fs.core.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.core.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.html").addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @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);
+    }
+}

+ 41 - 0
fs-live-socket/src/main/java/com/fs/core/config/SecurityConfig.java

@@ -0,0 +1,41 @@
+package com.fs.core.config;
+
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+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();
+    }
+
+}

+ 33 - 0
fs-live-socket/src/main/java/com/fs/core/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.core.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-socket/src/main/java/com/fs/core/config/SwaggerConfig.java

@@ -0,0 +1,124 @@
+package com.fs.core.config;
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+
+import com.fs.common.config.FSConfig;
+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-socket/src/main/java/com/fs/core/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.core.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-socket/src/main/java/com/fs/core/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.core.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.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.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-socket/src/main/java/com/fs/core/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.core.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-socket/src/main/java/com/fs/core/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.core.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();
+    }
+}

+ 56 - 0
fs-live-socket/src/main/java/com/fs/core/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,56 @@
+package com.fs.core.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+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))
+                {
+                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
+                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+            return true;
+        }
+        else
+        {
+            return super.preHandle(request, response, handler);
+        }
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request);
+}

+ 126 - 0
fs-live-socket/src/main/java/com/fs/core/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,126 @@
+package com.fs.core.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.core.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";
+
+    // 令牌自定义标识
+    @Value("${fs.jwt.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 间隔时间,单位:秒 默认10秒
+     * 
+     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
+     */
+    private int intervalTime = 10;
+
+    public void setIntervalTime(int intervalTime)
+    {
+        this.intervalTime = intervalTime;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request)
+    {
+        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<String, Object>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放cache的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = request.getHeader(header);
+        if (StringUtils.isEmpty(submitKey))
+        {
+            submitKey = url;
+        }
+
+        // 唯一标识(指定key + 消息头)
+        String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
+        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))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
+        return false;
+    }
+
+    /**
+     * 判断参数是否相同
+     */
+    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)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < (this.intervalTime * 1000))
+        {
+            return true;
+        }
+        return false;
+    }
+}

+ 53 - 0
fs-live-socket/src/main/java/com/fs/core/security/SecurityUtils.java

@@ -0,0 +1,53 @@
+package com.fs.core.security;
+
+import com.tencentcloudapi.yunjing.v20180228.models.MisAlarmNonlocalLoginPlacesRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+
+/**
+ * 安全服务工具类
+ *
+
+ */
+public class SecurityUtils
+{
+
+    /**
+     * 生成BCryptPasswordEncoder密码
+     *
+     * @param password 密码
+     * @return 加密字符串
+     */
+    public static String encryptPassword(String password)
+    {
+
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.encode(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     *
+     * @param rawPassword 真实密码
+     * @param encodedPassword 加密后字符
+     * @return 结果
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword)
+    {
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.matches(rawPassword, encodedPassword);
+    }
+
+    /**
+     * 是否为管理员
+     *
+     * @param userId 用户ID
+     * @return 结果
+     */
+    public static boolean isAdmin(Long userId)
+    {
+        return userId != null && 1L == userId;
+    }
+}

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

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

+ 15 - 0
fs-live-socket/src/main/java/com/fs/live/annotation/LoginUser.java

@@ -0,0 +1,15 @@
+package com.fs.live.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录用户信息
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LoginUser {
+
+}

+ 140 - 0
fs-live-socket/src/main/java/com/fs/live/config/ProductionWordFilter.java

@@ -0,0 +1,140 @@
+package com.fs.live.config;
+
+import com.fs.live.service.ILiveSensitiveWordsService;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class ProductionWordFilter implements InitializingBean {
+
+    @Autowired
+    private ILiveSensitiveWordsService liveSensitiveWordsService;
+
+
+    private static class TrieNode {
+        @Getter
+        private final Map<Character, TrieNode> children = new HashMap<>();
+        private boolean isEndOfWord;
+
+        public boolean isEndOfWord() {
+            return isEndOfWord;
+        }
+
+        public void setEndOfWord(boolean endOfWord) {
+            isEndOfWord = endOfWord;
+        }
+
+    }
+    @Getter
+    private static class MatchResult {
+        private final String word;
+        private final int endIndex;
+
+        public MatchResult(String word, int endIndex) {
+            this.word = word;
+            this.endIndex = endIndex;
+        }
+
+        public int getLength() {
+            return word.length();
+        }
+    }
+
+    @Getter
+    public static class FilterResult {
+        private final String filteredText;
+        private final Set<String> matchedWords;
+        private final int replacedCount;
+
+        public FilterResult(String filteredText, Set<String> matchedWords, int replacedCount) {
+            this.filteredText = filteredText;
+            this.matchedWords = matchedWords;
+            this.replacedCount = replacedCount;
+        }
+
+    }
+
+    private TrieNode root = new TrieNode();
+    private List<String> wordSources;
+    private final ScheduledExecutorService executor;
+
+    public ProductionWordFilter(List<String> wordSources) {
+        this.wordSources = wordSources;
+        this.executor = Executors.newSingleThreadScheduledExecutor();
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+        reload();
+        executor.scheduleWithFixedDelay(this::reload, 1, 1, TimeUnit.HOURS);
+    }
+
+    public synchronized void reload() {
+        wordSources = liveSensitiveWordsService.selectAllWords();
+        TrieNode newRoot = new TrieNode();
+        wordSources.stream()
+                .flatMap(source -> loadWords(source).stream())
+                .forEach(word -> addWord(newRoot, word));
+        this.root = newRoot;
+    }
+
+    public FilterResult filter(String text) {
+        StringBuilder result = new StringBuilder();
+        Set<String> foundWords = new HashSet<>();
+        int replacedCount = 0;
+
+        for (int i = 0; i < text.length(); ) {
+            MatchResult match = findNextMatch(text, i);
+            if (match != null) {
+                foundWords.add(match.getWord());
+                result.append(StringUtils.repeat("", match.getLength()));
+                replacedCount++;
+                i = match.getEndIndex();
+            } else {
+                result.append(text.charAt(i));
+                i++;
+            }
+        }
+
+        return new FilterResult(result.toString(), foundWords, replacedCount);
+    }
+
+
+    private MatchResult findNextMatch(String text, int start) {
+        TrieNode current = root;
+        for (int i = start; i < text.length(); i++) {
+            char c = text.charAt(i);
+            if (!current.getChildren().containsKey(c)) {
+                return null;
+            }
+            current = current.getChildren().get(c);
+            if (current.isEndOfWord()) {
+                return new MatchResult(text.substring(start, i + 1), i + 1);
+            }
+        }
+        return null;
+    }
+
+    private void addWord(TrieNode root, String word) {
+        TrieNode node = root;
+        for (char c : word.toCharArray()) {
+            node = node.getChildren().computeIfAbsent(c, k -> new TrieNode());
+        }
+        node.setEndOfWord(true);
+    }
+
+    private List<String> loadWords(String source) {
+        // 示例:实际应从 source(如 URL 或文件路径)读取
+        return Collections.singletonList(source); // 替换为真实逻辑
+    }
+
+
+}

+ 27 - 0
fs-live-socket/src/main/java/com/fs/live/config/WebMvcConfig.java

@@ -0,0 +1,27 @@
+package com.fs.live.config;
+
+
+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 LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+
+    }
+//
+//    @Override
+//    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
+//        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
+//    }
+}

+ 20 - 0
fs-live-socket/src/main/java/com/fs/live/config/WebSocketConfig.java

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

+ 13 - 0
fs-live-socket/src/main/java/com/fs/live/constant/CommonConstant.java

@@ -0,0 +1,13 @@
+package com.fs.live.constant;
+
+/**
+ * 通用常量
+ */
+public class CommonConstant {
+
+    private CommonConstant(){}
+    /**
+     * 销售登录token
+     */
+    public static final String REDIS_KEY_COMPANY_USER_TOKEN = "company-user-token:";
+}

+ 122 - 0
fs-live-socket/src/main/java/com/fs/live/controller/LiveController.java

@@ -0,0 +1,122 @@
+package com.fs.live.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.PageRequest;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.bean.BeanUtils;
+import com.fs.common.vo.LiveVo;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveMsg;
+import com.fs.live.domain.LiveVideo;
+import com.fs.live.service.ILiveGoodsService;
+import com.fs.live.service.ILiveMsgService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveVideoService;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.service.WebSocketServer;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import lombok.AllArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.time.LocalDateTime;
+import java.util.*;
+
+
+@Api("直播信息接口")
+@RestController
+@AllArgsConstructor
+@RequestMapping(value="/ws/app/live")
+public class LiveController {
+
+	private static final Logger log = LoggerFactory.getLogger(LiveController.class);
+	private final ILiveService liveService;
+	private final ILiveMsgService liveMsgService;
+	@Autowired
+	private ILiveVideoService videoService;
+	@Autowired
+	private WebSocketServer webSocketServer;
+
+	// 直播开播回调
+	@PostMapping("/startLiving")
+	public R checkLiving(HttpServletRequest request, @RequestBody  Map<String, String> params) {
+		log.info("请求参数:{}", params);
+		SendMsgVo sendMsgVo = new SendMsgVo();
+		sendMsgVo.setMsg("开始直播");
+		sendMsgVo.setCmd("live_start");
+		webSocketServer.broadcastMessage(Long.valueOf(params.get("stream_id")), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+		return R.ok();
+//		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=667, errcode=0,
+//		errmsg=, event_time=1753840982, event_type=1,
+//		height=1080, idc_id=38, node=113.249.151.16,
+//		sequence=702115889072680959, set_id=2, stream_id=667,
+//		stream_param=txSecret=6E89AECCA08DBEE82E7F5870BCED7132&txTime=688ACD21, user_ip=125.80.218.101, width=1920}
+	}
+
+	// 直播结束回调
+	@PostMapping("/endLiving")
+	public R endLiving(HttpServletRequest request, @RequestBody  Map<String, String> params) {
+		log.info("请求参数:{}", params);
+		SendMsgVo sendMsgVo = new SendMsgVo();
+		sendMsgVo.setMsg("结束直播");
+		sendMsgVo.setCmd("live_end");
+		webSocketServer.broadcastMessage(Long.valueOf(params.get("stream_id")), JSONObject.toJSONString(R.ok().put("data",sendMsgVo)));
+		Live live = new Live();
+		live.setLiveId(Long.valueOf(params.get("stream_id")));
+		live.setStatus(3);
+		live.setFinishTime(LocalDateTime.now());
+		liveService.updateLive(live);
+		return R.ok();
+//		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live, channel_id=673,
+//				errcode=1, errmsg=The push client actively stopped the push, event_time=1755571239,
+//				event_type=0, height=1080, idc_id=38, node=113.250.23.118, push_duration=1051237,
+//				sequence=721865018844564968, set_id=2, stream_id=673,
+//				stream_param=txSecret=A3EF362C9484D3D091C2E9B08C2C08CB&txTime=68A53145,
+//				user_ip=113.248.98.28, width=1920}
+
+	}
+
+	// 直播录制文件回调 腾讯云 cos桶 fs
+	@PostMapping("/liveReplayFile")
+	public R liveReplayFile(HttpServletRequest request, @RequestBody  Map<String, String> params) {
+		log.info("请求参数:{}", params);
+		Long liveId = Long.valueOf(params.get("stream_id"));
+		LiveVideo liveVideo = new LiveVideo();
+		LiveVideo exist = videoService.selectLiveVideoByLiveId(liveId);
+		if (exist != null) {
+			liveVideo = exist;
+		}
+		Date nowDate = DateUtils.getNowDate();
+		liveVideo.setLiveId(liveId);
+		liveVideo.setVideoUrl(params.get("video_url"));
+		liveVideo.setVideoType(2);
+		liveVideo.setDuration(Long.valueOf(params.get("duration")));
+		liveVideo.setCreateTime(nowDate);
+		liveVideo.setUpdateTime(nowDate);
+		videoService.saveOrUpdate(liveVideo);
+		return R.ok();
+//		{app=200149.push.tlivecloud.com, appid=1319721001, appname=live,
+//				callback_ext={"video_codec":"h264","session_id":"1755326625589574166","resolution":"1920x1080"},
+//			channel_id=774, duration=137, end_time=1757387736, end_time_usec=476103, event_type=100, file_format=mp4,
+//					file_id=1319721001_d69054948de44655b465aaf725cafc1c, file_size=45806289, media_start_time=80, record_bps=0,
+//				record_file_id=1319721001_d69054948de44655b465aaf725cafc1c, record_temp_id=1595756, start_time=1757387600, start_time_usec=942579,
+//				stream_id=774, stream_param=txSecret=CBF1E86FB1AD58B9CC25941F6B9DA854&txTime=68C0E304, task_id=1755326625589574166, video_id=1319721001_05396a7c47914b40aa9ffefcb0ea4626,
+//				video_url=http://fs-1319721001.cos.ap-chongqing.myqcloud.com/origin/200149.push.tlivecloud.com/live/774/1755326625589574166-6a9899ea95be4e89be0eeb30cf458579/2025-09-09-11-13-20.mp4}
+
+	}
+
+
+
+}

+ 51 - 0
fs-live-socket/src/main/java/com/fs/live/exception/FSException.java

@@ -0,0 +1,51 @@
+package com.fs.live.exception;
+
+/**
+ * 自定义异常
+ */
+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;
+	}
+
+	public String getMsg() {
+		return msg;
+	}
+
+	public void setMsg(String msg) {
+		this.msg = msg;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public void setCode(int code) {
+		this.code = code;
+	}
+
+
+}

+ 98 - 0
fs-live-socket/src/main/java/com/fs/live/exception/FSExceptionHandler.java

@@ -0,0 +1,98 @@
+package com.fs.live.exception;
+
+
+
+import com.fs.common.core.domain.R;
+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.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import java.util.List;
+
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class FSExceptionHandler {
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	/**
+	 * 处理自定义异常
+	 */
+	@ExceptionHandler(FSException.class)
+	public R handleFSException(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(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		if (e instanceof BindException){
+			BindException ex = (BindException)e;
+			List<ObjectError> allErrors = ex.getAllErrors();//捕获的所有错误对象
+			ObjectError error = allErrors.get(0);
+			String defaultMessage = error.getDefaultMessage();//异常内容
+
+			return R.error(defaultMessage);
+		}
+
+/*		if(e instanceof IllegalArgumentException){
+			return R.error(e.getMessage());
+		} else {
+			return R.error();
+		}*/
+
+		return R.error(e.getMessage());
+	}
+	@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(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+	@ExceptionHandler(CustomException.class)
+	public R handleException(CustomException e){
+
+		return R.error(e.getMessage());
+	}
+}

+ 22 - 0
fs-live-socket/src/main/java/com/fs/live/param/TestMoneyParam.java

@@ -0,0 +1,22 @@
+package com.fs.live.param;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Getter
+@Setter
+@ToString
+public class TestMoneyParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long companyId;
+    private Long logsId;
+    private BigDecimal money;
+
+
+}

+ 24 - 0
fs-live-socket/src/main/java/com/fs/live/redis/RedisConfiguration.java

@@ -0,0 +1,24 @@
+package com.fs.live.redis;
+
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+@Configuration
+public class RedisConfiguration {
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Bean
+    public RedisMessageListenerContainer redisMessageListenerContainer() {
+        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
+        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
+        return redisMessageListenerContainer;
+    }
+
+
+
+}

+ 85 - 0
fs-live-socket/src/main/java/com/fs/live/utils/CityTreeUtil.java

@@ -0,0 +1,85 @@
+package com.fs.live.utils;
+import com.fs.live.vo.CityVO;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @ClassName 树形工具类
+ **/
+public class CityTreeUtil {
+    /**
+     * 获得指定节点下所有归档
+     *
+     * @param list
+     * @param parentId
+     * @return
+     */
+    public static List<CityVO> list2TreeConverter(List<CityVO> list, int parentId) {
+        List<CityVO> returnList = new ArrayList<>();
+
+        for (CityVO res : list) {
+            //判断对象是否为根节点
+
+            if (res.getPid() == parentId) {
+                //该节点为根节点,开始递归
+
+                //通过递归为节点设置childList
+                recursionFn(list, res);
+
+                returnList.add(res);
+            }
+        }
+
+        return returnList;
+    }
+
+    /**
+     * 递归列表
+     * 通过递归,给指定t节点设置childList
+     *
+     * @param list
+     * @param t
+     */
+    public static void recursionFn(List<CityVO> list, CityVO t) {
+        //只能获取当前t节点的子节点集,并不是所有子节点集
+        List<CityVO> childsList = getChildList(list, t);
+
+        //设置他的子集对象集
+        t.setC(childsList);
+
+        //迭代子集对象集
+
+        //遍历完,则退出递归
+        for (CityVO nextChild : childsList) {
+
+            //判断子集对象是否还有子节点
+            if (!CollectionUtils.isEmpty(childsList)) {
+                //有下一个子节点,继续递归
+                recursionFn(list, nextChild);
+            }
+        }
+    }
+
+    /**
+     * 获得指定节点下的所有子节点
+     *
+     * @param list
+     * @param t
+     * @return
+     */
+    public static List<CityVO> getChildList(List<CityVO> list, CityVO t) {
+        List<CityVO> childsList = new ArrayList<>();
+        //遍历集合元素,如果元素的Parentid==指定元素的id,则说明是该元素的子节点
+        for (CityVO t1 : list) {
+            if (t1.getPid().equals(t.getV())) {
+                childsList.add(t1);
+            }
+        }
+
+        return childsList;
+    }
+
+
+}

+ 26 - 0
fs-live-socket/src/main/java/com/fs/live/utils/JsonUtils.java

@@ -0,0 +1,26 @@
+package com.fs.live.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+
+public class JsonUtils {
+    private static final ObjectMapper JSON = new ObjectMapper();
+
+    static {
+        JSON.setSerializationInclusion(Include.NON_NULL);
+        JSON.configure(SerializationFeature.INDENT_OUTPUT, Boolean.TRUE);
+    }
+
+    public static String toJson(Object obj) {
+        try {
+            return JSON.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}

+ 95 - 0
fs-live-socket/src/main/java/com/fs/live/utils/JwtUtils.java

@@ -0,0 +1,95 @@
+package com.fs.live.utils;
+
+import com.fs.live.exception.FSException;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * jwt工具类
+
+ */
+@ConfigurationProperties(prefix = "fs.jwt")
+@Component
+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);
+
+        return Jwts.builder()
+                .setHeaderParam("typ", "JWT")
+                .setSubject(userId+"")
+                .setIssuedAt(nowDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    public Claims getClaimByToken(String token) {
+        if (StringUtils.isEmpty(token)) {
+            throw new FSException("token不能为空", HttpStatus.UNAUTHORIZED.value());
+        }
+        try {
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        }catch (Exception e){
+            logger.debug("validate is token error ", e);
+            logger.info("token异常,重新登录,token: {}",token);
+
+            throw new FSException("token 失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+        }
+    }
+
+    /**
+     * token是否过期
+     * @return  true:过期
+     */
+    public boolean isTokenExpired(Date expiration) {
+        return expiration.before(new Date());
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public String getHeader() {
+        return header;
+    }
+
+    public void setHeader(String header) {
+        this.header = header;
+    }
+}

+ 46 - 0
fs-live-socket/src/main/java/com/fs/live/utils/VerifyUtils.java

@@ -0,0 +1,46 @@
+package com.fs.live.utils;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+
+public class VerifyUtils {
+
+    /**
+     * 验证签名
+     */
+    public static boolean verifySignature(String liveId, String userId, String userType, String secret, String signature) throws Exception {
+        String expected = generateSignature(liveId, userId, userType, secret);
+        return expected.equals(signature);
+    }
+
+    /**
+     * 生成签名:将 liveId、userId、userType 按顺序拼接后,用 HMAC-SHA256 算法生成签名
+     *
+     * @param liveId   直播间ID
+     * @param userId   用户ID
+     * @param userType 用户类型
+     * @param secret   共享密钥
+     * @return 十六进制格式的签名
+     * @throws Exception e
+     */
+    private static String generateSignature(String liveId, String userId, String userType, String secret) throws Exception {
+        String data = liveId + userId + userType + secret;
+        Mac mac = Mac.getInstance("HmacSHA256");
+        SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+        mac.init(keySpec);
+        byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+        return bytesToHex(rawHmac);
+    }
+
+    /**
+     * 将字节数组转换为十六进制字符串
+     */
+    private static String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+}

+ 23 - 0
fs-live-socket/src/main/java/com/fs/live/vo/CityVO.java

@@ -0,0 +1,23 @@
+package com.fs.live.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@Setter
+@ToString
+public class CityVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long v; //id
+
+    private String n; //名称
+
+    private Long pid;
+
+    private List<CityVO> c; //子集
+}

+ 35 - 0
fs-live-socket/src/main/java/com/fs/live/vo/IndexVO.java

@@ -0,0 +1,35 @@
+package com.fs.live.vo;
+
+import com.fs.store.vo.FsAdvListQueryVO;
+import com.fs.store.vo.FsArticleCateListQueryVO;
+import com.fs.store.vo.FsStoreProductListQueryVO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@ApiModel("首页数据")
+public class IndexVO {
+    @ApiModelProperty("首页BANNER")
+    List<FsAdvListQueryVO> advList;
+// @ApiModelProperty("推荐医生")
+// List<FsDoctorListQueryVO> doctorList;
+    @ApiModelProperty("健康分类")
+    List<FsArticleCateListQueryVO> articleCateList;
+
+    @ApiModelProperty("推荐产品")
+    List<FsStoreProductListQueryVO> tuiProductList;
+    @ApiModelProperty("最新产品")
+    List<FsStoreProductListQueryVO> newProductList;
+    @ApiModelProperty("热门产品")
+    List<FsStoreProductListQueryVO> hotProductList;
+
+}

+ 23 - 0
fs-live-socket/src/main/java/com/fs/live/vo/LiveInfoVo.java

@@ -0,0 +1,23 @@
+package com.fs.live.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class LiveInfoVo {
+    @ApiModelProperty("ID")
+    private Long id;
+    @ApiModelProperty("直播名称")
+    private String liveName;
+    @ApiModelProperty("直播描述")
+    private String liveDesc;
+    @ApiModelProperty("封面图")
+    private String liveImgUrl;
+    @ApiModelProperty("开始时间")
+    private LocalDateTime startTime;
+    @ApiModelProperty("直播状态1待支付 2直播中 3已结束")
+    private Integer status;
+
+}

+ 50 - 0
fs-live-socket/src/main/java/com/fs/live/vo/LiveVo.java

@@ -0,0 +1,50 @@
+//package com.fs.app.vo;
+//
+//
+//
+//
+//import com.fasterxml.jackson.annotation.JsonFormat;
+//import io.swagger.annotations.ApiModelProperty;
+//import lombok.Data;
+//
+//import java.math.BigDecimal;
+//import java.time.LocalDateTime;
+//
+//@Data
+//public class LiveVo {
+//    private Long liveId;
+//    @ApiModelProperty("直播名称")
+//    private String liveName;
+//    @ApiModelProperty("直播描述")
+//    private String liveDesc;
+//    @ApiModelProperty("显示类型 1横屏 2竖屏")
+//    private Integer showType;
+//    @ApiModelProperty("1待支付 2直播中 3已结束")
+//    private Integer status;
+//    @ApiModelProperty("直播ID")
+//    private Long anchorId;
+//    @ApiModelProperty("直播类型 1直播,2录播")
+//    private Integer liveType;
+//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+//    @ApiModelProperty("开始时间")
+//    private LocalDateTime startTime;
+//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+//    @ApiModelProperty("结束时间")
+//    private LocalDateTime finishTime;
+//    @ApiModelProperty("直播封面")
+//    private String liveImgUrl;
+//    @ApiModelProperty("直播配置 JOSN")
+//    private String liveConfig;
+//    @ApiModelProperty("视屏地址")
+//    private String videoUrl;
+//    @ApiModelProperty("直播地址")
+//    private String flvHlsUrl;
+//    @ApiModelProperty("当前跳转秒速")
+//    private Long duration;
+//    @ApiModelProperty("企微二维码")
+//    private String qwQrCode;
+//    private Long nowDuration;
+//    private BigDecimal nowPri;
+//    private Long storeId;
+//    private Integer isShow;
+//}

+ 17 - 0
fs-live-socket/src/main/java/com/fs/live/vo/LotteryVo.java

@@ -0,0 +1,17 @@
+package com.fs.live.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+public class LotteryVo {
+    // 中奖信息
+    private String userName;
+    private Long userId;
+    private String productName;
+    private Long prizeLevel;
+
+}

+ 101 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/auth/AuthHandler.java

@@ -0,0 +1,101 @@
+package com.fs.live.websocket.auth;
+
+import com.fs.live.utils.JwtUtils;
+import com.fs.live.utils.VerifyUtils;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.common.utils.spring.SpringUtils;
+import io.jsonwebtoken.Claims;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.QueryStringDecoder;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Component
+@ChannelHandler.Sharable
+public class AuthHandler extends ChannelInboundHandlerAdapter {
+
+    private final JwtUtils jwtUtils = SpringUtils.getBean(JwtUtils.class);
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        if (msg instanceof FullHttpRequest) {
+            FullHttpRequest req = (FullHttpRequest) msg;
+            String uri = req.uri();
+            QueryStringDecoder decoder = new QueryStringDecoder(uri);
+            Map<String, List<String>> parameterMap = decoder.parameters();
+            if (!parameterMap.containsKey(AttrConstant.LIVE_ID)) {
+                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                return;
+            }
+
+            if (!parameterMap.containsKey(AttrConstant.USER_ID)) {
+                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                return;
+            }
+
+            String tokenKey = jwtUtils.getHeader();
+            if (!parameterMap.containsKey(tokenKey) && !parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+                ctx.writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                return;
+            }
+
+            Long liveId = Long.valueOf(parameterMap.get(AttrConstant.LIVE_ID).get(0));
+            Long userId = Long.valueOf(parameterMap.get(AttrConstant.USER_ID).get(0));
+
+            // 验证 token
+            if (parameterMap.containsKey(tokenKey)) {
+            String token = parameterMap.get(tokenKey).get(0);
+            Claims claims = jwtUtils.getClaimByToken(token);
+            if (claims == null || jwtUtils.isTokenExpired(claims.getExpiration())) {
+                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                return;
+            }
+                // 将 userType 设置为 0(或根据实际业务逻辑设置)
+                ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(0L);
+            }
+
+            // 验证签名
+            if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+                String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
+                String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
+                String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
+
+                try {
+                    if (!VerifyUtils.verifySignature(liveId.toString(), userId.toString(), userTypeStr, timestampStr, signatureStr)) {
+                        ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                        return;
+                    }
+                    ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).set(Long.parseLong(userTypeStr));
+                } catch (Exception e) {
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: invalid parameters")).addListener(ChannelFutureListener.CLOSE);
+                    return;
+                }
+            }
+
+            // 将 liveId 和 userId 保存到 Channel 属性中,供后续处理使用
+            ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).set(liveId);
+            ctx.channel().attr(AttrConstant.ATTR_USER_ID).set(userId);
+
+            // 继续处理 WebSocket 握手
+            ctx.pipeline().remove(this);
+            ctx.fireChannelRead(req.retain());
+        } else {
+            ctx.channel().close();
+        }
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.warn("webSocket 认证异常: {}", cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 88 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -0,0 +1,88 @@
+package com.fs.live.websocket.auth;
+
+import com.fs.live.utils.JwtUtils;
+import com.fs.live.utils.VerifyUtils;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.common.exception.BaseException;
+import com.fs.common.utils.spring.SpringUtils;
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
+
+    private final JwtUtils jwtUtils = SpringUtils.getBean(JwtUtils.class);
+
+    /**
+     * 安全校验
+     * @param sec       配置
+     * @param request   请求
+     * @param response  返回
+     */
+    @Override
+    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+        Map<String, List<String>> parameterMap = request.getParameterMap();
+        if(!parameterMap.containsKey(AttrConstant.LIVE_ID)){
+            throw new BaseException("缺少必要的参数");
+        }
+        if(!parameterMap.containsKey(AttrConstant.USER_ID)){
+            throw new BaseException("缺少必要的参数");
+        }
+
+        String tokenKey = jwtUtils.getHeader();
+        if (!parameterMap.containsKey(tokenKey) && !parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+            throw new BaseException("缺少必要的参数");
+        }
+
+        Long liveId = Long.valueOf(parameterMap.get(AttrConstant.LIVE_ID).get(0));
+        Long userId = Long.valueOf(parameterMap.get(AttrConstant.USER_ID).get(0));
+
+        Map<String, Object> userProperties = sec.getUserProperties();
+        userProperties.put(AttrConstant.LIVE_ID, liveId);
+        userProperties.put(AttrConstant.USER_ID, userId);
+        if (parameterMap.containsKey(AttrConstant.COMPANY_ID)) {
+            userProperties.put(AttrConstant.COMPANY_ID, Long.valueOf(parameterMap.get(AttrConstant.COMPANY_ID).get(0)));
+        }
+        if (parameterMap.containsKey(AttrConstant.COMPANY_USER_ID)) {
+            userProperties.put(AttrConstant.COMPANY_USER_ID, Long.valueOf(parameterMap.get(AttrConstant.COMPANY_USER_ID).get(0)));
+        }
+
+        // 验证token
+        if (parameterMap.containsKey(tokenKey)) {
+            String token = parameterMap.get(tokenKey).get(0);
+            Claims claims = jwtUtils.getClaimByToken(token);
+            if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+                throw new BaseException(jwtUtils.getHeader());
+            }
+
+            userProperties.put(AttrConstant.USER_TYPE, 0L);
+        }
+
+        // 验证签名
+        if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+            String liveIdStr = parameterMap.get(AttrConstant.LIVE_ID).get(0);
+            String userIdStr = parameterMap.get(AttrConstant.USER_ID).get(0);
+            String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
+            String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
+            String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
+
+            try {
+                if (!VerifyUtils.verifySignature(liveIdStr, userIdStr, userTypeStr, timestampStr, signatureStr)) {
+                    throw new BaseException("缺少必要的参数");
+                }
+
+                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
+            } catch (Exception e) {
+                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
+                throw new BaseException("缺少必要的参数");
+            }
+        }
+    }
+
+}

+ 13 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/bean/MsgBean.java

@@ -0,0 +1,13 @@
+package com.fs.live.websocket.bean;
+
+import lombok.Data;
+
+import java.io.Serializable;
+@Data
+public class MsgBean implements Serializable {
+    String cmd; //
+    String userId;
+    Integer userType;//用户类型 1用户2销售3AI
+    String data;
+
+}

+ 37 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java

@@ -0,0 +1,37 @@
+package com.fs.live.websocket.bean;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SendMsgVo {
+    @ApiModelProperty("直播ID")
+    private Long liveId;
+    @ApiModelProperty("用户ID")
+    private Long userId;
+    @ApiModelProperty("企业ID")
+    private Long companyId;
+    @ApiModelProperty("企业用户ID")
+    private Long companyUserId;
+    @ApiModelProperty("用户类型0用户1管理员")
+    private Long userType;
+    @ApiModelProperty("消息代码")
+    private String cmd;
+    @ApiModelProperty("发送消息")
+    private String msg;
+    @ApiModelProperty("消息")
+    private String data;
+    @ApiModelProperty("名称")
+    private String nickName;
+    @ApiModelProperty("头像")
+    private String avatar;
+    private boolean on = false;
+    private Integer status;
+
+}

+ 20 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/constant/AttrConstant.java

@@ -0,0 +1,20 @@
+package com.fs.live.websocket.constant;
+
+import io.netty.util.AttributeKey;
+
+public class AttrConstant {
+
+    // 定义变量名
+    public static final String LIVE_ID = "liveId";
+    public static final String USER_ID = "userId";
+    public static final String USER_TYPE = "userType";
+    public static final String TIMESTAMP = "timestamp";
+    public static final String SIGNATURE = "signature";
+    public static final String COMPANY_ID = "companyId";
+    public static final String COMPANY_USER_ID = "companyUserId";
+
+    // 定义 AttributeKey 保存必要参数
+    public static final AttributeKey<Long> ATTR_LIVE_ID = AttributeKey.valueOf(LIVE_ID);
+    public static final AttributeKey<Long> ATTR_USER_ID = AttributeKey.valueOf(USER_ID);
+    public static final AttributeKey<Long> ATTR_USER_TYPE = AttributeKey.valueOf(USER_TYPE);
+}

+ 53 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/constant/NickNameConstant.java

@@ -0,0 +1,53 @@
+package com.fs.live.websocket.constant;
+
+import java.util.Random;
+
+public class NickNameConstant {
+    private static final String[] PREFIXES = {
+            "酷炫", "梦幻", "闪电", "狂野", "神秘", "银河", "星辰", "烈焰",
+            "冰霜", "雷霆", "疾风", "暗影", "光明", "虚空", "永恒", "混沌",
+            "暴风", "月光", "太阳", "星际", "超能", "量子", "钢铁", "黄金",
+            "钻石", "翡翠", "水晶", "火焰", "寒冰", "时空", "无敌", "战神",
+            "魔法", "精灵", "巨龙", "天使", "恶魔", "幽灵", "忍者", "武士",
+            "海盗", "猎人", "先知", "贤者", "圣光", "黑暗", "荣耀", "王者",
+            "剑圣", "刀锋", "枪神", "箭雨", "符文", "元素", "能量", "脉冲",
+            "辐射", "原子", "流星", "彗星", "宇宙", "维度", "次元", "终极",
+            "幻影", "炽焰", "寒冰", "极光", "天罚", "神罚", "天启", "末日",
+            "曙光", "黄昏", "深渊", "炼狱", "圣洁", "堕落", "不朽", "不灭",
+            "虚空", "湮灭", "创世", "灭世", "天命", "宿命", "轮回", "涅槃",
+            "玄冰", "赤焰", "青岚", "紫电", "金辉", "银翼", "黑曜", "白夜",
+            "血月", "日蚀", "月蚀", "星陨", "天穹", "地核", "海啸", "山崩",
+            "雷鸣", "电闪", "霜冻", "灼烧", "剧毒", "腐蚀", "净化", "重生",
+            "天选", "神谕", "魔咒", "诅咒", "祝福", "厄难", "幸运", "天命",
+            "狂怒", "静谧", "狂暴", "温柔", "疯狂", "理智", "混沌", "秩序",
+            "真理", "谎言", "荣耀", "耻辱", "正义", "邪恶", "自由", "束缚",
+            "新世纪","某科学的"
+    };
+
+    private static final String[] SUFFIXES = {
+            "小王子", "骑士", "王者", "使者", "探险家", "守护者", "征服者", "领主",
+            "皇帝", "霸主", "勇士", "战士", "剑客", "刀客", "枪手", "射手",
+            "法师", "巫师", "术士", "牧师", "萨满", "德鲁伊", "游侠", "盗贼",
+            "刺客", "忍者", "武僧", "格斗家", "拳王", "摔跤手", "冠军", "传奇",
+            "英雄", "大侠", "剑圣", "刀神", "枪王", "箭神", "魔导师", "大法师",
+            "先知", "贤者", "学者", "智者", "预言家", "观察者", "旅行者", "漫游者",
+            "冒险家", "寻宝者", "考古家", "科学家", "工程师", "发明家", "创造者", "毁灭者",
+            "终结者", "幸存者", "流亡者", "放逐者", "守望者", "保卫者", "执法者", "审判者",
+            "救世主", "天选者", "命运之子", "时空旅人", "梦境行者", "虚空漫步者", "星界旅者", "深渊潜行者",
+            "龙骑士", "凤凰使", "麒麟卫", "白虎将", "玄武使", "朱雀使", "青龙将", "饕餮王",
+            "混沌之主", "秩序守护", "光明化身", "黑暗化身", "时间之主", "空间之主", "命运编织者", "因果操纵者",
+            "元素领主", "能量掌控", "符文大师", "咒术宗师", "炼金术士", "机械大师", "数据黑客", "赛博行者",
+            "星际海盗", "银河商人", "黑洞探险家", "白洞观测者", "量子幽灵", "暗物质猎手", "反物质学者", "超弦理论家",
+            "神话缔造者", "传说终结者", "史诗谱写者", "传奇见证者", "不朽者", "永生者", "转世者", "轮回者",
+            "天外来客", "地心访客", "深海来客", "极地来客", "沙漠行者", "丛林猎手", "雪山隐士", "草原游牧",
+            "城市猎人", "废土幸存", "末日使者", "新纪元开创者", "文明守护者", "知识传承者", "智慧化身", "力量象征",
+            "福音战士","超电磁炮"
+    };
+
+    public static String getNickName() {
+        Random random = new Random();
+        int prefixIndex = random.nextInt(PREFIXES.length);
+        int suffixIndex = random.nextInt(SUFFIXES.length);
+        return PREFIXES[prefixIndex] + SUFFIXES[suffixIndex];
+    }
+}

+ 268 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/handle/LiveChatHandler.java

@@ -0,0 +1,268 @@
+package com.fs.live.websocket.handle;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.constant.AttrConstant;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.live.domain.LiveMsg;
+import com.fs.live.domain.LiveWatchUser;
+import com.fs.live.service.ILiveMsgService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveWatchUserService;
+import com.fs.live.vo.LiveWatchUserVO;
+import com.fs.store.domain.FsUser;
+import com.fs.store.service.IFsUserService;
+import io.netty.channel.*;
+import io.netty.channel.group.ChannelGroup;
+import io.netty.channel.group.DefaultChannelGroup;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Component
+@ChannelHandler.Sharable
+@Slf4j
+public class LiveChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
+
+    // 容器
+    private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Channel>> adminRooms = new ConcurrentHashMap<>();
+    private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Channel>> rooms = new ConcurrentHashMap<>();
+    private final static ConcurrentHashMap<Long, ChannelGroup> roomGroups = new ConcurrentHashMap<>();
+    private final static ILiveService liveService = SpringUtils.getBean(ILiveService.class);
+    private final static ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
+    private final static ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
+    private final static IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+
+    /**
+     * 处理握手
+     * @param ctx   连接
+     * @param evt   数据
+     * @throws Exception    异常
+     */
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        log.debug("事件");
+        // 处理 WebSocket 握手完成事件
+        if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
+            Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
+            Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
+            Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+
+            if (Objects.isNull(liveService.selectLiveByLiveId(liveId))) {
+                ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 未找到直播间")).addListener(ChannelFutureListener.CLOSE);
+                return;
+            }
+
+            Map<Long, Channel> room = getRoom(liveId);
+            List<Channel> adminRoom = getAdminRoom(liveId);
+            ChannelGroup roomGroup = getRoomGroup(liveId);
+            roomGroup.add(ctx.channel());
+
+            if (userType == 0) {
+                // 加入房间
+                liveWatchUserService.join(liveId, userId);
+                room.put(userId, ctx.channel());
+
+                FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+                if (Objects.isNull(fsUser)) {
+                    ctx.channel().writeAndFlush(new TextWebSocketFrame("Error: 用户信息错误")).addListener(ChannelFutureListener.CLOSE);
+                    return;
+                }
+
+                LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
+
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setUserType(userType);
+                sendMsgVo.setCmd("entry");
+                sendMsgVo.setMsg("用户进入");
+                sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+                sendMsgVo.setNickName(fsUser.getNickname());
+                sendMsgVo.setAvatar(fsUser.getAvatar());
+
+                // 广播连接消息
+                broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            } else if (userType == 1) {
+                adminRoom.add(ctx.channel());
+            }
+
+            log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
+        }
+    }
+
+    /**
+     * 获取房间
+     * @param liveId 直播间ID
+     * @return 容器
+     */
+    private CopyOnWriteArrayList<Channel> getAdminRoom(Long liveId) {
+        return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
+    }
+
+    /**
+     * 获取房间
+     * @param liveId 直播间ID
+     * @return 容器
+     */
+    private ConcurrentHashMap<Long, Channel> getRoom(Long liveId) {
+        return rooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
+    }
+
+    /**
+     * 获取房间用户组
+     * @param liveId 直播间ID
+     * @return  用户组
+     */
+    private ChannelGroup getRoomGroup(Long liveId) {
+        return roomGroups.computeIfAbsent(liveId, k -> new DefaultChannelGroup(GlobalEventExecutor.INSTANCE));
+    }
+
+    /**
+     * 发送广播
+     * @param liveId    直播间ID
+     * @param msg       消息
+     */
+    private void broadcastMessage(Long liveId, String msg) {
+        getRoomGroup(liveId).writeAndFlush(new TextWebSocketFrame(msg));
+    }
+
+    /**
+     * 发送指定消息
+     * @param channel   连接
+     * @param message   消息
+     */
+    private void sendMessage(Channel channel, String message) {
+        channel.writeAndFlush(new TextWebSocketFrame(message));
+    }
+
+    /**
+     * 接收消息
+     * @param channelHandlerContext 连接
+     * @param textWebSocketFrame    消息
+     * @throws Exception    异常
+     */
+    @Override
+    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
+        log.debug("接收到消息 data: {}", textWebSocketFrame.text());
+        Long liveId = channelHandlerContext.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
+        Long userType = channelHandlerContext.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+
+        SendMsgVo msg = JSONObject.parseObject( textWebSocketFrame.text(), SendMsgVo.class);
+        if(msg.isOn()) return;
+        try {
+            switch (msg.getCmd()) {
+                case "heartbeat":
+                    sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.ok().put("data", msg)));
+                    break;
+                case "sendMsg":
+                    LiveMsg liveMsg = new LiveMsg();
+                    liveMsg.setLiveId(msg.getLiveId());
+                    liveMsg.setUserId(msg.getUserId());
+                    liveMsg.setNickName(msg.getNickName());
+                    liveMsg.setAvatar(msg.getAvatar());
+                    liveMsg.setMsg(msg.getMsg());
+                    liveMsg.setCreateTime(new Date());
+
+                    if (userType == 0) {
+                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
+                        if(liveWatchUser.getMsgStatus() == 1){
+                            sendMessage(channelHandlerContext.channel(), JSONObject.toJSONString(R.error("你以被禁言")));
+                            return;
+                        }
+
+                        liveMsgService.insertLiveMsg(liveMsg);
+                    }
+
+                    msg.setOn(true);
+                    msg.setData(JSONObject.toJSONString(liveMsg));
+
+                    // 广播消息
+                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    break;
+            }
+        } catch (Exception e) {
+            log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 断开连接
+     * @param ctx   连接
+     * @throws Exception    异常
+     */
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+        log.debug("断开连接");
+        Long userId = ctx.channel().attr(AttrConstant.ATTR_USER_ID).get();
+        Long liveId = ctx.channel().attr(AttrConstant.ATTR_LIVE_ID).get();
+        Long userType = ctx.channel().attr(AttrConstant.ATTR_USER_TYPE).get();
+
+        if (Objects.isNull(userId) || Objects.isNull(liveId) || Objects.isNull(userType)) {
+            return;
+        }
+
+        Map<Long, Channel> room = getRoom(liveId);
+        List<Channel> adminRoom = getAdminRoom(liveId);
+        ChannelGroup roomGroup = getRoomGroup(liveId);
+
+        if (userType == 0) {
+            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            liveWatchUserService.close(liveId, userId);
+            room.remove(userId);
+
+            if (room.isEmpty()) {
+                rooms.remove(liveId);
+            }
+
+            LiveWatchUserVO liveWatchUserVO = liveWatchUserService.selectWatchUserByLiveIdAndUserId(liveId, userId);
+
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setUserType(userType);
+            sendMsgVo.setCmd("out");
+            sendMsgVo.setMsg("用户离开");
+            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+            sendMsgVo.setNickName(fsUser.getNickname());
+            sendMsgVo.setAvatar(fsUser.getAvatar());
+
+            // 广播离开消息
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } else {
+            adminRoom.remove(ctx.channel());
+            if (adminRoom.isEmpty()) {
+                adminRooms.remove(liveId);
+            }
+        }
+        roomGroup.remove(ctx.channel());
+        if (roomGroup.isEmpty()) {
+            roomGroups.remove(liveId);
+        }
+
+        log.debug("断开webSocket liveId: {}, userId: {}, 直播间人数: {}", liveId, userId, room.size());
+
+    }
+
+    /**
+     * 连接异常
+     * @param ctx   连接
+     * @param cause 原因
+     * @throws Exception 异常
+     */
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        log.error("连接异常 msg: {}", cause.getMessage(), cause);
+        ctx.close();
+    }
+}

+ 97 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/service/NettyServerRunner.java

@@ -0,0 +1,97 @@
+package com.fs.live.websocket.service;//package com.fs.app.websocket.service;
+//
+//import com.fs.app.websocket.auth.AuthHandler;
+//import com.fs.app.websocket.handle.LiveChatHandler;
+//import io.netty.bootstrap.ServerBootstrap;
+//import io.netty.channel.*;
+//import io.netty.channel.nio.NioEventLoopGroup;
+//import io.netty.channel.socket.SocketChannel;
+//import io.netty.channel.socket.nio.NioServerSocketChannel;
+//import io.netty.handler.codec.http.HttpObjectAggregator;
+//import io.netty.handler.codec.http.HttpServerCodec;
+//import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.boot.CommandLineRunner;
+//import org.springframework.stereotype.Component;
+//
+//import javax.annotation.PreDestroy;
+//import java.util.Objects;
+//
+//@Slf4j
+//@Component
+//public class NettyServerRunner implements CommandLineRunner {
+//
+//    private static final int port = 17114;
+//    private EventLoopGroup bossGroup;
+//    private EventLoopGroup workerGroup;
+//    private Channel serverChannel;
+//    @Autowired
+//    private AuthHandler authHandler;
+//    @Autowired
+//    private LiveChatHandler liveChatHandler;
+//
+//    @Override
+//    public void run(String... args) throws Exception {
+//        new Thread(this::startServer).start();
+//    }
+//
+//    private void startServer() {
+//        bossGroup = new NioEventLoopGroup(); // 处理连接
+//        workerGroup = new NioEventLoopGroup(); // 处理I/O
+//        try {
+//            ServerBootstrap bootstrap = new ServerBootstrap();
+//            bootstrap.group(bossGroup, workerGroup)
+//                    .channel(NioServerSocketChannel.class)
+//                    .childHandler(new ChannelInitializer<SocketChannel>() {
+//
+//                        @Override
+//                        protected void initChannel(SocketChannel socketChannel) throws Exception {
+//                            ChannelPipeline pipeline = socketChannel.pipeline();
+//                            // 编解码
+//                            pipeline.addLast(new HttpServerCodec());
+//                            // 集合消息
+//                            pipeline.addLast(new HttpObjectAggregator(65536));
+//                            // 安全校验
+//                            pipeline.addLast(authHandler);
+//                            // websocket握手
+//                            pipeline.addLast(new WebSocketServerProtocolHandler("/app/webSocket", null, true, 65536, false, true));
+//                            // 自定义聊天
+//                            pipeline.addLast(liveChatHandler);
+//                        }
+//                    })
+//                    .option(ChannelOption.SO_BACKLOG, 1024)
+//                    .childOption(ChannelOption.SO_KEEPALIVE, true);
+//
+//            ChannelFuture future = bootstrap.bind(port).sync();
+//            serverChannel = future.channel();
+//            log.info("netty server started [{}]", port);
+//            serverChannel.closeFuture().sync();
+//        } catch (Exception e) {
+//            log.error("netty server error msg: {}", e.getMessage(), e);
+//        } finally {
+//            shutdown();
+//        }
+//    }
+//
+//    @PreDestroy
+//    public void destroy() {
+//        shutdown();
+//        log.info("netty server destroy");
+//    }
+//
+//    private void shutdown() {
+//        if (Objects.nonNull(bossGroup)) {
+//            bossGroup.shutdownGracefully();
+//        }
+//
+//        if (Objects.nonNull(workerGroup)) {
+//            workerGroup.shutdownGracefully();
+//        }
+//
+//        if (Objects.nonNull(serverChannel)) {
+//            serverChannel.close();
+//        }
+//        log.info("netty server stopped");
+//    }
+//}

+ 471 - 0
fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -0,0 +1,471 @@
+package com.fs.live.websocket.service;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.live.config.ProductionWordFilter;
+import com.fs.live.websocket.auth.WebSocketConfigurator;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.BaseException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.live.domain.*;
+import com.fs.live.service.*;
+import com.fs.live.vo.LiveGoodsVo;
+import com.fs.store.domain.FsUser;
+import com.fs.store.service.IFsUserService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.time.DateUtils;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.*;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+import static com.fs.common.constant.LiveKeysConstant.*;
+
+@ServerEndpoint(value = "/ws/app/webSocket",configurator = WebSocketConfigurator.class)
+@Component
+@Slf4j
+public class WebSocketServer {
+
+    // 直播间用户session
+    private final static ConcurrentHashMap<Long, ConcurrentHashMap<Long, Session>> rooms = new ConcurrentHashMap<>();
+    // 管理端连接
+    private final static ConcurrentHashMap<Long, CopyOnWriteArrayList<Session>> adminRooms = new ConcurrentHashMap<>();
+    private final RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
+    private final ILiveMsgService liveMsgService = SpringUtils.getBean(ILiveMsgService.class);
+    private final ILiveService liveService = SpringUtils.getBean(ILiveService.class);
+    private final ILiveWatchUserService liveWatchUserService = SpringUtils.getBean(ILiveWatchUserService.class);
+    private final IFsUserService fsUserService = SpringUtils.getBean(IFsUserService.class);
+    private final ILiveDataService liveDataService = SpringUtils.getBean(ILiveDataService.class);
+    private final ProductionWordFilter productionWordFilter = SpringUtils.getBean(ProductionWordFilter.class);
+    private final ILiveRedConfService liveRedConfService =  SpringUtils.getBean(ILiveRedConfService.class);
+    private final ILiveLotteryConfService liveLotteryConfService =  SpringUtils.getBean(ILiveLotteryConfService.class);
+    private final ILiveGoodsService liveGoodsService =  SpringUtils.getBean(ILiveGoodsService.class);
+    private final ILiveUserFirstEntryService liveUserFirstEntryService =  SpringUtils.getBean(ILiveUserFirstEntryService.class);
+    // 直播间在线用户缓存
+//    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
+
+
+    //建立连接成功调用
+    @OnOpen
+    public void onOpen(Session session) {
+
+        Map<String, Object> userProperties = session.getUserProperties();
+        long liveId = (long) userProperties.get("liveId");
+        long userId = (long) userProperties.get("userId");
+        long userType = (long) userProperties.get("userType");
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            throw new BaseException("未找到直播间");
+        }
+        long companyId = live.getCompanyId() == null ? -1L : live.getCompanyId();
+        long companyUserId = -1L;
+        if (!Objects.isNull(userProperties.get("companyId"))) {
+            companyId = (long) userProperties.get("companyId");
+        }
+        if (!Objects.isNull(userProperties.get("companyUserId"))) {
+            companyUserId = (long) userProperties.get("companyUserId");
+        }
+
+
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        List<Session> adminRoom = getAdminRoom(liveId);
+
+        // 记录连接信息 管理员不记录
+        if (userType == 0) {
+            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            if (Objects.isNull(fsUser)) {
+                throw new BaseException("用户信息错误");
+            }
+
+            LiveWatchUser liveWatchUserVO = liveWatchUserService.join(liveId, userId);
+            room.put(userId, session);
+            // 直播间浏览量 +1
+            redisCache.increment(PAGE_VIEWS_KEY + liveId, 1);
+
+            // 累计观看人次 +1
+            redisCache.increment(TOTAL_VIEWS_KEY + liveId, 1);
+
+            // 记录在线人数
+            redisCache.increment(ONLINE_USERS_KEY + liveId, 1);
+            Integer currentOnline = redisCache.getCacheObject(ONLINE_USERS_KEY + liveId);
+            //最大同时在线人数
+            Integer maxOnline = redisCache.getCacheObject(MAX_ONLINE_USERS_KEY + liveId);
+            if (maxOnline == null || currentOnline > maxOnline) {
+                redisCache.setCacheObject(MAX_ONLINE_USERS_KEY + liveId, currentOnline);
+            }
+
+            // 判断是否是该直播间的首次访客(独立访客统计)
+            boolean isFirstVisit = redisCache.setIfAbsent(USER_VISIT_KEY + userId, 1, 1, TimeUnit.DAYS);
+            if (isFirstVisit) {
+
+                redisCache.increment(UNIQUE_VISITORS_KEY + liveId, 1);
+            }
+
+            // 判断是否是首次进入直播间的观众
+            boolean isFirstViewer = redisCache.setIfAbsent(UNIQUE_VIEWERS_KEY + liveId + ":" + userId, 1, 1, TimeUnit.DAYS);
+            if (isFirstViewer) {
+                redisCache.increment(UNIQUE_VIEWERS_KEY + liveId, 1);
+            }
+            LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(liveId, userId);
+            liveWatchUserVO.setMsgStatus(liveWatchUser.getMsgStatus());
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setUserType(userType);
+            sendMsgVo.setCmd("entry");
+            sendMsgVo.setMsg("用户进入");
+            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+            sendMsgVo.setNickName(fsUser.getNickname());
+            sendMsgVo.setAvatar(fsUser.getAvatar());
+            // 广播连接消息
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+
+            LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            if (liveUserFirstEntry != null) {
+                // 处理第一次自己进入,第二次扫码销售进入
+                if (liveUserFirstEntry.getCompanyUserId() == -1L && companyUserId != -1L) {
+                    liveUserFirstEntry.setCompanyId(companyId);
+                    liveUserFirstEntry.setCompanyUserId(companyUserId);
+                    liveUserFirstEntryService.updateLiveUserFirstEntry(liveUserFirstEntry);
+                }
+            } else {
+                // 这个用户A邀请用户b,b的业绩算a的销售的
+                if (companyUserId == -2L) {
+                    LiveUserFirstEntry clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
+                    companyId = clientB.getCompanyId();
+                    companyUserId = clientB.getCompanyUserId();
+                }
+                Date date = new Date();
+                liveUserFirstEntry = new LiveUserFirstEntry();
+                liveUserFirstEntry.setUserId(userId);
+                liveUserFirstEntry.setLiveId(liveId);
+                liveUserFirstEntry.setCompanyId(companyId);
+                liveUserFirstEntry.setCompanyUserId(companyUserId);
+                liveUserFirstEntry.setEntryDate(date);
+                liveUserFirstEntry.setFirstEntryTime(date);
+                liveUserFirstEntry.setUpdateTime( date);
+                liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
+            }
+
+
+        } else {
+            adminRoom.add(session);
+        }
+
+        log.debug("加入webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+    }
+
+    //关闭连接时调用
+    @OnClose
+    public void onClose(Session session) {
+        Map<String, Object> userProperties = session.getUserProperties();
+
+        long liveId = (long) userProperties.get("liveId");
+        long userId = (long) userProperties.get("userId");
+        long userType = (long) userProperties.get("userType");
+
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        List<Session> adminRoom = getAdminRoom(liveId);
+        if (userType == 0) {
+            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            if (Objects.isNull(fsUser)) {
+                throw new BaseException("用户信息错误");
+            }
+
+            LiveWatchUser liveWatchUserVO = liveWatchUserService.close(liveId, userId);
+            room.remove(userId);
+
+            if (room.isEmpty()) {
+                rooms.remove(liveId);
+            }
+
+
+            // 直播间在线人数 -1
+            redisCache.increment(ONLINE_USERS_KEY + liveId, -1);
+            SendMsgVo sendMsgVo = new SendMsgVo();
+            sendMsgVo.setLiveId(liveId);
+            sendMsgVo.setUserId(userId);
+            sendMsgVo.setUserType(userType);
+            sendMsgVo.setCmd("out");
+            sendMsgVo.setMsg("用户离开");
+            sendMsgVo.setData(JSONObject.toJSONString(liveWatchUserVO));
+            sendMsgVo.setNickName(fsUser.getNickname());
+            sendMsgVo.setAvatar(fsUser.getAvatar());
+
+            // 广播离开消息
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } else {
+            adminRoom.remove(session);
+        }
+
+        log.debug("离开webSocket liveId: {}, userId: {}, 直播间人数: {}, 管理端人数: {}", liveId, userId, room.size(), adminRoom.size());
+    }
+
+    //收到客户端信息
+    @OnMessage
+    public void onMessage(Session session,String message) throws IOException {
+        Map<String, Object> userProperties = session.getUserProperties();
+
+        long liveId = (long) userProperties.get("liveId");
+        long userType = (long) userProperties.get("userType");
+
+        SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
+        if(msg.isOn()) return;
+        try {
+            switch (msg.getCmd()) {
+                case "heartbeat":
+                    sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    break;
+                case "sendMsg":
+                    msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
+                    if(StringUtils.isEmpty(msg.getMsg())) return;
+                    LiveMsg liveMsg = new LiveMsg();
+                    liveMsg.setLiveId(msg.getLiveId());
+                    liveMsg.setUserId(msg.getUserId());
+                    liveMsg.setNickName(msg.getNickName());
+                    liveMsg.setAvatar(msg.getAvatar());
+                    liveMsg.setMsg(msg.getMsg());
+                    liveMsg.setCreateTime(new Date());
+
+                    if (userType == 0) {
+                        LiveWatchUser liveWatchUser = liveWatchUserService.getByLiveIdAndUserId(msg.getLiveId(), msg.getUserId());
+                        if(liveWatchUser.getMsgStatus() == 1){
+                            sendMessage(session, JSONObject.toJSONString(R.error("你已被禁言")));
+                            return;
+                        }
+
+                        liveMsgService.insertLiveMsg(liveMsg);
+                    }
+
+                    msg.setOn(true);
+                    msg.setData(JSONObject.toJSONString(liveMsg));
+
+                    // 广播消息
+                    broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+                    break;
+                case "sendGift":
+                    break;
+                case "blockUser":
+                    sendBlockMessage(liveId, msg.getUserId());
+                    break;
+                case "goods":
+                    sendGoodsMessage(msg);
+                    break;
+                case "red":
+                    processRed(liveId, msg);
+                    break;
+                case "lottery":
+                    processLottery(liveId, msg);
+                    break;
+                case "delAutoTask":
+                    if (userType == 1) {
+                        delAutoTask(liveId, DateUtils.parseDate(msg.getData(),"yyyy-MM-dd'T'HH:mm:ss.SSSZ").getTime());
+                    }
+                    break;
+            }
+        } catch (Exception e) {
+            log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
+        }
+    }
+
+
+
+    private void sendGoodsMessage(SendMsgVo msg) {
+        JSONObject jsonObject = JSON.parseObject(msg.getData());
+        Long goodsId = jsonObject.getLong("goodsId");
+        Long liveId = jsonObject.getLong("liveId");
+        Integer status = jsonObject.getInteger("status");
+        msg.setStatus(status);
+        LiveGoodsVo liveGoods = liveGoodsService.selectLiveGoodsVoByGoodsId(goodsId);
+        if(liveGoods == null) return;
+        msg.setLiveId(liveId);
+        msg.setData(JSONObject.toJSONString(liveGoods));
+        broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+    }
+
+    /**
+     * 处理红包变动消息
+     */
+    private void processRed(Long liveId, SendMsgVo msg) {
+        log.debug("redData: {}", msg);
+        JSONObject jsonObject = JSON.parseObject(msg.getData());
+        Integer status = jsonObject.getInteger("status");
+        msg.setStatus( status);
+        LiveRedConf liveRedConf = liveRedConfService.selectLiveRedConfByRedId(jsonObject.getLong("redId"));
+        if (Objects.nonNull(liveRedConf)) {
+            msg.setData(JSONObject.toJSONString(liveRedConf));
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+        }
+    }
+
+    /**
+     * 处理抽奖变动消息
+     */
+    private void processLottery(Long liveId, SendMsgVo msg) {
+        log.debug("lotteryData: {}", msg);
+        JSONObject jsonObject = JSON.parseObject(msg.getData());
+        Integer status = jsonObject.getInteger("status");
+        msg.setStatus( status);
+        LiveLotteryConf liveLotteryConf = liveLotteryConfService.selectLiveLotteryConfByLotteryId(jsonObject.getLong("lotteryId"));
+        if (Objects.nonNull(liveLotteryConf)) {
+            msg.setData(JSONObject.toJSONString(liveLotteryConf));
+            broadcastMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)));
+        }
+    }
+
+    //错误时调用
+    @OnError
+    public void onError(Session session, Throwable throwable) {
+        log.error("webSocKet连接错误 msg: {}", throwable.getMessage(), throwable);
+    }
+
+    /**
+     * 获取房间
+     * @param liveId 直播间ID
+     * @return 容器
+     */
+    private ConcurrentHashMap<Long, Session> getRoom(Long liveId) {
+        return rooms.computeIfAbsent(liveId, k -> new ConcurrentHashMap<>());
+    }
+
+    /**
+     * 获取管理端房间
+     * @param liveId  直播间ID
+     * @return  容器
+     */
+    private List<Session> getAdminRoom(Long liveId) {
+        return adminRooms.computeIfAbsent(liveId, k -> new CopyOnWriteArrayList<>());
+    }
+
+    //发送消息
+    public void sendMessage(Session session, String message) throws IOException {
+        session.getAsyncRemote().sendText(message);
+    }
+
+    public void sendIntegralMessage(Long liveId, Long userId,Long scoreAmount) {
+        SendMsgVo sendMsgVo = new SendMsgVo();
+        sendMsgVo.setLiveId(liveId);
+        sendMsgVo.setUserId(userId);
+        sendMsgVo.setUserType(0L);
+        sendMsgVo.setCmd("Integral");
+        sendMsgVo.setMsg("恭喜你成功获得观看奖励:" + scoreAmount + "芳华币");
+        sendMsgVo.setData(String.valueOf(scoreAmount));
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if(Objects.isNull( session)) return;
+        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
+    private void sendBlockMessage(Long liveId, Long userId) {
+        SendMsgVo sendMsgVo = new SendMsgVo();
+        sendMsgVo.setLiveId(liveId);
+        sendMsgVo.setUserId(userId);
+        sendMsgVo.setUserType(0L);
+        sendMsgVo.setCmd("blockUser");
+        sendMsgVo.setMsg("账号已被停用");
+        sendMsgVo.setData(null);
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if(Objects.isNull( session)) return;
+        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
+    /**
+     * 广播消息
+     * @param liveId   直播间ID
+     * @param message  消息内容
+     */
+    public void broadcastMessage(Long liveId, String message) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        List<Session> adminRoom = getAdminRoom(liveId);
+
+        room.forEach((k, v) -> v.getAsyncRemote().sendText(message));
+        adminRoom.forEach(v -> v.getAsyncRemote().sendText(message));
+    }
+
+    /**
+     *定期将缓存的数据写入数据库
+     */
+    @Scheduled(fixedRate = 60000) // 每分钟执行一次
+    public void syncLiveDataToDB() {
+        List<LiveData> liveDatas = liveDataService.getAllLiveDatas(); // 获取所有正在直播的直播间数据
+        if(liveDatas == null)
+            return;
+        liveDatas.forEach(liveData ->{
+            liveData.setLikes(
+                    Optional.ofNullable(redisCache.incrementCacheValue("live:like:" + liveData.getLiveId(),0 )).orElse(0L)
+            );
+
+       /* for (Long liveId : liveIds) {
+            LiveData liveData = liveDataService.selectLiveDataByLiveId(liveId);
+            if (liveData == null) {
+                continue; // 防止空指针异常
+            }*/
+
+
+            // 从 redis 获取数据,并提供默认值,避免 NPE
+            liveData.setPageViews(
+                    Optional.ofNullable(redisCache.incrementCacheValue(PAGE_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L)
+            );
+            liveData.setTotalViews(
+                    Optional.ofNullable(redisCache.incrementCacheValue(TOTAL_VIEWS_KEY + liveData.getLiveId(),0)).orElse(0L)
+            );
+            liveData.setUniqueVisitors(
+                    /*Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VISITORS_KEY + liveId))
+                            .map(Set::size)  // 获取集合大小
+                            .map(Long::valueOf)  // 转换为 Long 类型
+                            .orElse(0L)*/
+                    Optional.ofNullable(redisCache.incrementCacheValue(UNIQUE_VISITORS_KEY + liveData.getLiveId(),0)).orElse(0L)
+            );
+            liveData.setUniqueViewers(
+                    /*Optional.ofNullable(redisCache.getCacheSet(UNIQUE_VIEWERS_KEY + liveId))
+                            .map(Set::size)  // 获取集合大小
+                            .map(Long::valueOf)  // 转换为 Long 类型
+                            .orElse(0L)*/
+                    Optional.ofNullable(redisCache.incrementCacheValue(UNIQUE_VIEWERS_KEY + liveData.getLiveId(),0)).orElse(0L)
+            );
+            liveData.setPeakConcurrentViewers(
+                    Optional.ofNullable(redisCache.incrementCacheValue(MAX_ONLINE_USERS_KEY + liveData.getLiveId(),0)).orElse(0L)
+            );
+        });
+        if(!liveDatas.isEmpty())
+            for (LiveData liveData : liveDatas) {
+                liveDataService.updateLiveData(liveData);
+            }
+
+            /*// 更新数据库
+            liveDataService.updateLiveData(liveData);*/
+    }
+
+
+    public void handleAutoTask(LiveAutoTask task) {
+        if (task.getTaskType() == 1L) {
+            SendMsgVo msg = new SendMsgVo();
+            msg.setLiveId(task.getLiveId());
+            msg.setData(task.getContent());
+            msg.setCmd("goods");
+            try {
+                LiveGoodsVo liveGoodsVo = JSON.parseObject(task.getContent(), LiveGoodsVo.class);
+                liveGoodsService.updateLiveIsShow(liveGoodsVo.getGoodsId(), task.getLiveId());
+            } catch (Exception e) {
+                log.error("定时任务执行异常:{}", e.getMessage());
+            }
+            msg.setStatus(1);
+            broadcastMessage(task.getLiveId(), JSONObject.toJSONString(R.ok().put("data", msg)));
+        }
+    }
+    private void delAutoTask(long liveId, Long data) {
+        String key = "live:auto_task:";
+        redisCache.redisTemplate.opsForZSet().removeRangeByScore(key + liveId, data, data);
+    }
+}

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

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

+ 79 - 0
fs-live-socket/src/main/resources/application-dev.yml

@@ -0,0 +1,79 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: 127.0.0.1
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 0
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://172.16.0.31:3306/fs_ffhx_hospital?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+#                url: jdbc:mysql://127.0.0.1:3307/fs_ffhx_hospital?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: Ylrz_1q2w3e4r5t6y
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            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
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

+ 79 - 0
fs-live-socket/src/main/resources/application-druid-test.yml

@@ -0,0 +1,79 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: localhost
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 1
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://139.186.77.83:3306/his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: Rtroot
+                password: Rtroot
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            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
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true
+

+ 78 - 0
fs-live-socket/src/main/resources/application-druid.yml

@@ -0,0 +1,78 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址 172.30.0.15
+        host: 127.0.0.1
+        # 端口,默认为6379
+        port: 6379
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 100s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 500
+                # 连接池的最大数据库连接数
+                max-active: 500
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+        database: 0
+    datasource:
+        type: com.alibaba.druid.pool.DruidDataSource
+        driverClassName: com.mysql.cj.jdbc.Driver
+        druid:
+            # 主库数据源
+            master:
+                url: jdbc:mysql://127.0.0.1:3306/fs_hospital?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: root
+                password: 123456
+            # 从库数据源
+            slave:
+                # 从数据源开关/默认关闭
+                enabled: false
+                url:
+                username:
+                password:
+            # 初始连接数
+            initialSize: 500
+            # 最小连接池数量
+            minIdle: 2000
+            # 最大连接池数量
+            maxActive: 2000
+            # 配置获取连接等待超时的时间
+            maxWait: 60000
+            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+            timeBetweenEvictionRunsMillis: 60000
+            # 配置一个连接在池中最小生存的时间,单位是毫秒
+            minEvictableIdleTimeMillis: 300000
+            # 配置一个连接在池中最大生存的时间,单位是毫秒
+            maxEvictableIdleTimeMillis: 900000
+            # 配置检测连接是否有效
+            validationQuery: SELECT 1 FROM DUAL
+            testWhileIdle: true
+            testOnBorrow: false
+            testOnReturn: false
+            webStatFilter:
+                enabled: true
+            statViewServlet:
+                enabled: true
+                # 设置白名单,不填则允许所有访问
+                allow:
+                url-pattern: /druid/*
+                # 控制台管理用户名和密码
+                login-username:
+                login-password:
+            filter:
+                stat:
+                    enabled: true
+                    # 慢SQL记录
+                    log-slow-sql: true
+                    slow-sql-millis: 1000
+                    merge-sql: true
+                wall:
+                    config:
+                        multi-statement-allow: true

+ 122 - 0
fs-live-socket/src/main/resources/application.yml

@@ -0,0 +1,122 @@
+# 项目相关配置
+fs:
+  # 名称
+  name: fs
+  # 版本
+  version: 1.1.0
+  # 版权年份
+  copyrightYear: 2020
+  # 实例演示开关
+  demoEnabled: false
+  # 文件路径 示例( Windows配置D:/fs/uploadPath,Linux配置 /home/fs/uploadPath)
+  profile: C:/fs/uploadPath
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数组计算 char 字符验证
+  captchaType: math
+  # APP模块,是通过jwt认证的,如果要使用APP模块,则需要修改【加密秘钥】
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb5
+    # token有效时长,100天,单位秒
+    expire: 8640000
+    header: AppToken
+  url: https://api.yjf.runtzh.com
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为 7014  store 7114
+  port: 7114
+  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
+    # 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。
+    keep-alive-timeout: 20000
+    # 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。
+    max-keep-alive-requests: 100
+
+# 日志配置
+logging:
+  level:
+    com.fs: info
+    org.springframework: warn
+    org.springframework.web: info
+
+# Spring配置
+spring:
+  datasource:
+    druid:
+      stat-view-servlet:
+        enabled: false
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: dev
+    include: config
+  # 文件上传
+  servlet:
+     multipart:
+       # 单个文件大小
+       max-file-size:  10MB
+       # 设置总上传的文件大小
+       max-request-size:  20MB
+  # 服务模块
+  devtools:
+    restart:
+      # 热部署开关
+      enabled: true
+
+# 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: false
+  # 请求前缀
+  pathMapping: /
+
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice/*
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*
+

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

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

BIN
fs-live-socket/src/main/resources/fx.jpg


+ 36 - 0
fs-live-socket/src/main/resources/i18n/messages.properties

@@ -0,0 +1,36 @@
+#错误消息
+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.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-socket/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>

BIN
fs-live-socket/src/main/resources/qr.jpg


BIN
fs-live-socket/src/main/resources/simsunb.ttf


+ 1 - 0
fs-live-socket/src/main/resources/static/S8Zw463cFc.txt

@@ -0,0 +1 @@
+641273d9479c4e0133bfacb9f669d39f

+ 21 - 0
fs-live-socket/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-socket/src/main/resources/templates/userAgreement.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="${userAgreement}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 61 - 0
fs-live-socket/src/test/java/com/fs/core/security/BaseSpringBootTest.java

@@ -0,0 +1,61 @@
+package com.fs.core.security;
+
+import cn.hutool.core.date.DateUtil;
+import com.fs.common.utils.DateUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * 测试基类
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest
+public abstract class BaseSpringBootTest {
+
+    protected Logger logger = LoggerFactory.getLogger(this.getClass());
+
+    private long time;
+
+    public long getTime() {
+        return time;
+    }
+
+    public void setTime(long time) {
+        this.time = time;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        this.setTime(System.currentTimeMillis());
+        logger.info("==> 测试开始执行 <==");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        logger.info("==> 测试执行完成,耗时:{} ms <==",
+                System.currentTimeMillis() - this.getTime());
+    }
+
+    /**
+     * 方法描述:打印list.
+     * 创建时间:2018-10-11 00:23:28
+     */
+    <T> void print(List<T> list) {
+        if (!CollectionUtils.isEmpty(list)) {
+            list.forEach(System.out::println);
+        }
+    }
+
+    void print(Object o) {
+        System.out.println(o.toString());
+    }
+
+}

+ 2 - 0
fs-service-system/src/main/java/com/fs/erp/service/IErpOrderService.java

@@ -14,5 +14,7 @@ public interface IErpOrderService
     ErpDeliverysResponse getDeliver(ErpDeliverysRequest param);
     ErpOrderQueryResponse getOrder(ErpOrderQueryRequert param);
     BaseResponse refundUpdate(ErpRefundUpdateRequest param);
+
+    ErpOrderResponse addLiveOrder(ErpOrder erpOrder);
 }
 

+ 35 - 0
fs-service-system/src/main/java/com/fs/erp/service/impl/ErpOrderServiceImpl.java

@@ -122,4 +122,39 @@ public class ErpOrderServiceImpl implements IErpOrderService
         ErpOrderResponse response=JSONUtil.toBean(result, ErpOrderResponse.class);
         return response;
     }
+
+    @Override
+    public ErpOrderResponse addLiveOrder(ErpOrder order) {
+        JSONObject param = JSONUtil.createObj();
+        param.set("appkey", sysConfig.getErpAppKey());
+        param.set("sessionkey", sysConfig.getErpSessionKey());
+        param.set("method", "gy.erp.trade.add");
+        param.set("shop_code", order.getShop_code());
+        param.set("vip_code", order.getVip_code());
+        param.set("platform_code", order.getPlatform_code());
+        param.set("warehouse_code", order.getWarehouse_code());
+        param.set("express_code", order.getExpress_code());
+        param.set("post_fee", order.getPost_fee());
+        param.set("cod", order.getCod());
+        param.set("seller_memo", order.getSeller_memo());
+        param.set("order_type_code", order.getOrder_type_code());
+        param.set("cod_fee", order.getCod_fee());
+        param.set("buyer_memo", order.getBuyer_memo());
+        param.set("details", order.getDetails());
+        param.set("payments",order.getPayments());
+        param.set("receiver_name", order.getReceiver_name());
+        param.set("receiver_mobile", order.getReceiver_mobile());
+        param.set("receiver_province", order.getReceiver_province());
+        param.set("receiver_city", order.getReceiver_city());
+        param.set("receiver_district", order.getReceiver_district());
+        param.set("receiver_address", order.getReceiver_address());
+        param.set("deal_datetime", order.getDeal_datetime());
+        param.set("sign",CommonUtils.sign(param.toString(),sysConfig.getErpSecret()));
+        String result = HttpUtil.post(sysConfig.getErpUrl(), param.toString());
+        System.out.println("erp-order:"+param.toString());
+        LOGGER.info("erp-order:"+param.toString());
+        LOGGER.error("erp-order-result:"+result);
+        ErpOrderResponse response=JSONUtil.toBean(result, ErpOrderResponse.class);
+        return response;
+    }
 }

+ 5 - 0
fs-service-system/src/main/java/com/fs/erp/service/impl/ErpOrderServiceProxy.java

@@ -113,4 +113,9 @@ public class ErpOrderServiceProxy implements IErpOrderService {
     public BaseResponse refundUpdate(ErpRefundUpdateRequest param) {
         return getCurrentErpService().refundUpdate(param);
     }
+
+    @Override
+    public ErpOrderResponse addLiveOrder(ErpOrder erpOrder) {
+        return getCurrentErpService().addLiveOrder(erpOrder);
+    }
 }

+ 132 - 0
fs-service-system/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -14,6 +14,12 @@ import com.fs.erp.mapper.FsJstCodPushMapper;
 import com.fs.erp.service.IErpOrderService;
 import com.fs.erp.utils.SignUtil;
 import com.fs.express.enums.TaskStatusEnum;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.mapper.LiveOrderItemMapper;
+import com.fs.live.mapper.LiveOrderMapper;
+import com.fs.live.service.ILiveOrderItemService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.live.vo.LiveOrderItemListUVO;
 import com.fs.store.domain.FsStoreAfterSales;
 import com.fs.store.domain.FsStoreDelivers;
 import com.fs.store.domain.FsStoreOrder;
@@ -48,6 +54,12 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
     @Autowired
     private IFsStoreOrderService fsStoreOrderService;
 
+    @Autowired
+    private LiveOrderMapper liveorderMapper;
+
+    @Autowired
+    private LiveOrderItemMapper liveOrderItemMapper;
+
     @Autowired
     private FsJstAftersalePushMapper fsJstAftersalePushMapper;
 
@@ -236,6 +248,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         ErpOrderQuery erpOrder = new ErpOrderQuery();
 
         FsStoreOrder fsStoreOrder = fsStoreOrderService.selectFsStoreOrderByOrderCode(order.getSoId());
+        if(fsStoreOrder == null)
         Asserts.notNull(fsStoreOrder,"该订单号没有找到!");
 
         // 设置基本订单信息
@@ -334,5 +347,124 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
 
         return baseResponse;
     }
+
+    @Override
+    public ErpOrderResponse addLiveOrder(ErpOrder order) {
+        LiveOrder fsStoreOrder = liveorderMapper.selectFsUserVipOrderByOrderCode(order.getPlatform_code());
+
+        ErpOrderPayment erpOrderPayment = order.getPayments().get(0);
+
+        ShopOrderDTO shopOrderDTO = new ShopOrderDTO();
+
+        shopOrderDTO.setShopId(Long.valueOf(order.getShop_code()));
+        shopOrderDTO.setSoId(order.getPlatform_code());
+        shopOrderDTO.setOrderDate(order.getDeal_datetime());
+        // 待发货
+        shopOrderDTO.setShopStatus(OrderStatusEnum.WAIT_SELLER_SEND_GOODS.name());
+        // 买家账号
+        shopOrderDTO.setShopBuyerId(order.getBuyer_account());
+        // 收货人省份
+        shopOrderDTO.setReceiverState(order.getReceiver_province());
+        // 收货人城市
+        shopOrderDTO.setReceiverCity(order.getReceiver_city());
+        // 收货人区域
+        shopOrderDTO.setReceiverDistrict(order.getReceiver_district());
+        // 收货人详细地址
+        shopOrderDTO.setReceiverAddress(order.getReceiver_address());
+        // 收货人详细地址
+        shopOrderDTO.setReceiverName(order.getReceiver_name());
+        // 收货人电话
+        shopOrderDTO.setReceiverPhone(order.getReceiver_mobile());
+        // 支付金额
+        shopOrderDTO.setPayAmount(erpOrderPayment.getPayment());
+        // 运费
+        if(ObjectUtil.isNull(fsStoreOrder.getPayPostage())) {
+            shopOrderDTO.setFreight(Double.valueOf("0.00"));
+        } else {
+            shopOrderDTO.setFreight(fsStoreOrder.getPayPostage().doubleValue());
+        }
+        // 备注
+        shopOrderDTO.setRemark(order.getBuyer_memo());
+        // 买家留言
+        shopOrderDTO.setBuyerMessage(order.getBuyer_memo());
+
+        // 订单商品项列表
+        List<OrderItemDTO> itemDTOList = new ArrayList<>();
+
+        List<LiveOrderItemListUVO> fsStoreOrderItemVOS = liveOrderItemMapper.selectLiveOrderItemListUVOByOrderId(fsStoreOrder.getOrderId());
+        for (LiveOrderItemListUVO item : fsStoreOrderItemVOS) {
+            OrderItemDTO orderItemDTO = new OrderItemDTO();
+            JSONObject jsonObject = JSON.parseObject(item.getJsonInfo());
+
+            String barCode = jsonObject.getString("barCode");
+            String productName = jsonObject.getString("productName");
+
+            orderItemDTO.setSkuId(barCode);
+            orderItemDTO.setShopSkuId(barCode);
+            orderItemDTO.setName(productName);
+
+            FsStoreProduct fsStoreProduct = fsStoreProductService.selectFsStoreProductById(item.getProductId());
+
+            orderItemDTO.setAmount(fsStoreProduct.getPrice());
+
+            orderItemDTO.setQty(item.getNum().intValue());
+            orderItemDTO.setOuterOiId(String.format("%s%s",item.getOrderCode(),item.getItemId()));
+            itemDTOList.add(orderItemDTO);
+        }
+        shopOrderDTO.setItems(itemDTOList);
+
+        // 实际支付金额
+        PaymentDTO paymentDTO = new PaymentDTO();
+        paymentDTO.setAmount(fsStoreOrder.getPayMoney().doubleValue());
+        paymentDTO.setOuterPayId(order.getPlatform_code());
+        paymentDTO.setPayDate(order.getDeal_datetime());
+        paymentDTO.setPayment("微信支付");
+        paymentDTO.setBuyerAccount(order.getBuyer_account());
+        paymentDTO.setSellerAccount("平台销售");
+        shopOrderDTO.setPay(paymentDTO);
+
+
+        // 如果是货到付款
+        if("2".equals(fsStoreOrder.getPayType()) || "3".equals(fsStoreOrder.getPayType())){
+            shopOrderDTO.setIsCod(true);
+            // 货到付款金额 = 物流代收金额-优惠金额
+            shopOrderDTO.setBuyerPaidAmount(fsStoreOrder.getPayDelivery());
+            shopOrderDTO.setPayAmount(fsStoreOrder.getPayPrice().doubleValue());
+
+            // 货到付款要推两次
+            PaymentDTO paymentDTO2 = new PaymentDTO();
+            // 物流代收金额
+            paymentDTO2.setAmount(fsStoreOrder.getPayDelivery().doubleValue());
+            paymentDTO2.setOuterPayId(String.format("%s%d",order.getPlatform_code(),1));
+            paymentDTO2.setPayDate(order.getDeal_datetime());
+            paymentDTO2.setPayment("货到付款");
+            paymentDTO2.setBuyerAccount(order.getBuyer_account());
+            paymentDTO2.setSellerAccount("平台销售");
+            shopOrderDTO.setPay(paymentDTO2);
+
+            FsJstCodPush fsJstCodPush = new FsJstCodPush();
+            fsJstCodPush.setOrderId(fsStoreOrder.getOrderCode());
+            fsJstCodPush.setType("0");
+            fsJstCodPush.setParams(JSON.toJSONString(shopOrderDTO));
+            fsJstCodPush.setRetryCount(0);
+            fsJstCodPush.setTaskStatus(TaskStatusEnum.PENDING.getCode());
+            fsJstCodPushMapper.insert(fsJstCodPush);
+
+            shopOrderDTO.setPay(paymentDTO);
+        }
+
+        ErpOrderResponseDTO upload = jstErpHttpService.upload(shopOrderDTO);
+
+        if(CollectionUtils.isEmpty(upload.getDatas())) {
+            log.info("推送ERP返回结果: {}",upload);
+            throw new IllegalArgumentException("推送ERP返回数不应该为0");
+        }
+        ErpOrderResponseDTO.OrderData orderData = upload.getDatas().get(0);
+
+        ErpOrderResponse erpOrderResponse = new ErpOrderResponse();
+        erpOrderResponse.setSuccess(true);
+        erpOrderResponse.setCode(String.valueOf(orderData.getOId()));
+        return erpOrderResponse;
+    }
 }
 

+ 1 - 0
fs-service-system/src/main/java/com/fs/live/domain/Live.java

@@ -116,4 +116,5 @@ public class   Live extends BaseEntity {
     private Integer isAudit;
     /** 创建时间 */
     private Date createTime;
+    private String companyName;
 }

+ 70 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveCoupon.java

@@ -0,0 +1,70 @@
+package com.fs.live.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 优惠券对象 live_coupon
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCoupon extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 优惠券表ID */
+    private Long couponId;
+
+    /** 优惠券名称 */
+    @Excel(name = "优惠券名称")
+    private String title;
+
+    /** 兑换消耗积分值 */
+    @Excel(name = "兑换消耗积分值")
+    private Integer integral;
+
+    /** 兑换的优惠券面值 */
+    @Excel(name = "兑换的优惠券面值")
+    private BigDecimal couponPrice;
+
+    /** 最低消费多少金额可用优惠券 */
+    @Excel(name = "最低消费多少金额可用优惠券")
+    private BigDecimal useMinPrice;
+
+    /** 优惠券有效期限(单位:天) */
+    @Excel(name = "优惠券有效期限", readConverterExp = "单=位:天")
+    private Long couponTime;
+
+    /** 排序 */
+    @Excel(name = "排序")
+    private Integer sort;
+
+    /** 状态(0:关闭,1:开启) */
+    @Excel(name = "状态", readConverterExp = "0=:关闭,1:开启")
+    private Integer status;
+
+    /** 商品ids */
+    @Excel(name = "商品ids")
+    private String productIds;
+
+    /** 套餐分类ids */
+    @Excel(name = "套餐分类ids")
+    private String packageCateIds;
+
+    /** 优惠券类型 0-通用 1-商品券 */
+    @Excel(name = "优惠券类型 0-通用 1-商品券")
+    private Long type;
+
+    /** 是否删除 */
+    @Excel(name = "是否删除")
+    private Integer isDel;
+
+}

+ 67 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveCouponIssue.java

@@ -0,0 +1,67 @@
+package com.fs.live.domain;
+
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 优惠券领取对象 live_coupon_issue
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCouponIssue extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** $column.columnComment */
+    @Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
+    private String couponName;
+
+    /** 优惠券ID */
+    @Excel(name = "优惠券ID")
+    private Long couponId;
+
+    /** 优惠券类型 0-通用 1-商品券 */
+    @Excel(name = "优惠券类型 0-通用 1-商品券")
+    private Integer couponType;
+
+    /** 优惠券领取开启时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "优惠券领取开启时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date startTime;
+
+    /** 优惠券领取结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "优惠券领取结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date limitTime;
+
+    /** 优惠券领取数量 */
+    @Excel(name = "优惠券领取数量")
+    private Long totalCount;
+
+    /** 优惠券剩余领取数量 */
+    @Excel(name = "优惠券剩余领取数量")
+    private Long remainCount;
+
+    /** 是否无限张数 */
+    @Excel(name = "是否无限张数")
+    private Integer isPermanent;
+
+    /** 1 正常 0 未开启 -1 已无效 */
+    @Excel(name = "1 正常 0 未开启 -1 已无效")
+    private Integer status;
+
+    /** $column.columnComment */
+    @Excel(name = "1 正常 0 未开启 -1 已无效")
+    private Integer isDel;
+
+}

+ 35 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveCouponIssueUser.java

@@ -0,0 +1,35 @@
+package com.fs.live.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 优惠券用户领取记录对象 live_coupon_issue_user
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCouponIssueUser extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 领取优惠券用户ID */
+    @Excel(name = "领取优惠券用户ID")
+    private Long userId;
+
+    /** 优惠券领取ID */
+    @Excel(name = "优惠券领取ID")
+    private Long issueId;
+
+    /** $column.columnComment */
+    @Excel(name = "优惠券领取ID")
+    private Integer isDel;
+
+}

+ 73 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveCouponUser.java

@@ -0,0 +1,73 @@
+package com.fs.live.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 优惠券发放记录对象 live_coupon_user
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCouponUser extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 优惠券发放记录id */
+    private Long id;
+
+    /** 兑换的项目id */
+    @Excel(name = "兑换的项目id")
+    private Long couponId;
+
+    /** 优惠券所属用户 */
+    @Excel(name = "优惠券所属用户")
+    private Integer userId;
+
+    /** 优惠券名称 */
+    @Excel(name = "优惠券名称")
+    private String couponTitle;
+
+    /** 优惠券的面值 */
+    @Excel(name = "优惠券的面值")
+    private BigDecimal couponPrice;
+
+    /** 最低消费多少金额可用优惠券 */
+    @Excel(name = "最低消费多少金额可用优惠券")
+    private BigDecimal useMinPrice;
+
+    /** 优惠券结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "优惠券结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date limitTime;
+
+    /** 使用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "使用时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date useTime;
+
+    /** 获取方式 */
+    @Excel(name = "获取方式")
+    private String type;
+
+    /** 状态(0:未使用,1:已使用, 2:已过期) */
+    @Excel(name = "状态", readConverterExp = "0=:未使用,1:已使用,,2=:已过期")
+    private Integer status;
+
+    /** 是否有效 */
+    @Excel(name = "是否有效")
+    private Integer isFail;
+
+    /** $column.columnComment */
+    @Excel(name = "是否有效")
+    private Integer isDel;
+
+
+}

+ 2 - 0
fs-service-system/src/main/java/com/fs/live/domain/LiveOrderItem.java

@@ -63,5 +63,7 @@ public class LiveOrderItem{
     @Excel(name = "是否赠品")
     private Integer isGift;
 
+    private String barCode;
+
 
 }

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java

@@ -0,0 +1,61 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponIssue;
+
+/**
+ * 优惠券领取Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface LiveCouponIssueMapper
+{
+    /**
+     * 查询优惠券领取
+     *
+     * @param id 优惠券领取ID
+     * @return 优惠券领取
+     */
+    public LiveCouponIssue selectLiveCouponIssueById(Long id);
+
+    /**
+     * 查询优惠券领取列表
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 优惠券领取集合
+     */
+    public List<LiveCouponIssue> selectLiveCouponIssueList(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 新增优惠券领取
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 结果
+     */
+    public int insertLiveCouponIssue(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 修改优惠券领取
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 结果
+     */
+    public int updateLiveCouponIssue(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 删除优惠券领取
+     *
+     * @param id 优惠券领取ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueById(Long id);
+
+    /**
+     * 批量删除优惠券领取
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueByIds(Long[] ids);
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponIssueUserMapper.java

@@ -0,0 +1,61 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponIssueUser;
+
+/**
+ * 优惠券用户领取记录Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface LiveCouponIssueUserMapper
+{
+    /**
+     * 查询优惠券用户领取记录
+     *
+     * @param id 优惠券用户领取记录ID
+     * @return 优惠券用户领取记录
+     */
+    public LiveCouponIssueUser selectLiveCouponIssueUserById(Long id);
+
+    /**
+     * 查询优惠券用户领取记录列表
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 优惠券用户领取记录集合
+     */
+    public List<LiveCouponIssueUser> selectLiveCouponIssueUserList(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 新增优惠券用户领取记录
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 结果
+     */
+    public int insertLiveCouponIssueUser(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 修改优惠券用户领取记录
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 结果
+     */
+    public int updateLiveCouponIssueUser(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 删除优惠券用户领取记录
+     *
+     * @param id 优惠券用户领取记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueUserById(Long id);
+
+    /**
+     * 批量删除优惠券用户领取记录
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueUserByIds(Long[] ids);
+}

+ 66 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponMapper.java

@@ -0,0 +1,66 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.fs.live.domain.LiveCoupon;
+import com.fs.store.domain.FsStoreCoupon;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 优惠券Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface LiveCouponMapper
+{
+    /**
+     * 查询优惠券
+     *
+     * @param couponId 优惠券ID
+     * @return 优惠券
+     */
+    public LiveCoupon selectLiveCouponById(Long couponId);
+
+    /**
+     * 查询优惠券列表
+     *
+     * @param liveCoupon 优惠券
+     * @return 优惠券集合
+     */
+    public List<LiveCoupon> selectLiveCouponList(LiveCoupon liveCoupon);
+
+    /**
+     * 新增优惠券
+     *
+     * @param liveCoupon 优惠券
+     * @return 结果
+     */
+    public int insertLiveCoupon(LiveCoupon liveCoupon);
+
+    /**
+     * 修改优惠券
+     *
+     * @param liveCoupon 优惠券
+     * @return 结果
+     */
+    public int updateLiveCoupon(LiveCoupon liveCoupon);
+
+    /**
+     * 删除优惠券
+     *
+     * @param couponId 优惠券ID
+     * @return 结果
+     */
+    public int deleteLiveCouponById(Long couponId);
+
+    /**
+     * 批量删除优惠券
+     *
+     * @param couponIds 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteLiveCouponByIds(Long[] couponIds);
+
+    @Select("select * from live_coupon where find_in_set(coupon_id,#{ids})")
+    List<LiveCoupon> selectLiveCouponByIds(String ids);
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/mapper/LiveCouponUserMapper.java

@@ -0,0 +1,61 @@
+package com.fs.live.mapper;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponUser;
+
+/**
+ * 优惠券发放记录Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface LiveCouponUserMapper
+{
+    /**
+     * 查询优惠券发放记录
+     *
+     * @param id 优惠券发放记录ID
+     * @return 优惠券发放记录
+     */
+    public LiveCouponUser selectLiveCouponUserById(Long id);
+
+    /**
+     * 查询优惠券发放记录列表
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 优惠券发放记录集合
+     */
+    public List<LiveCouponUser> selectLiveCouponUserList(LiveCouponUser liveCouponUser);
+
+    /**
+     * 新增优惠券发放记录
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 结果
+     */
+    public int insertLiveCouponUser(LiveCouponUser liveCouponUser);
+
+    /**
+     * 修改优惠券发放记录
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 结果
+     */
+    public int updateLiveCouponUser(LiveCouponUser liveCouponUser);
+
+    /**
+     * 删除优惠券发放记录
+     *
+     * @param id 优惠券发放记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponUserById(Long id);
+
+    /**
+     * 批量删除优惠券发放记录
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteLiveCouponUserByIds(Long[] ids);
+}

+ 6 - 2
fs-service-system/src/main/java/com/fs/live/mapper/LiveOrderItemMapper.java

@@ -7,6 +7,7 @@ import com.fs.live.vo.LiveOrderItemListUVO;
 import com.fs.live.vo.LiveOrderItemVo;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 订单详情Mapper接口
@@ -63,7 +64,7 @@ public interface LiveOrderItemMapper {
      */
     int deleteLiveOrderItemByItemIds(String[] itemIds);
 
-    @Select("select * from live_order_item where order_id = #{orderId}")
+    @Select("select loi.*,fsp.bar_code from live_order_item loi left join fs_store_product fsp on loi.product_id =fsp.product_id   where loi.order_id = #{orderId}")
     List<LiveOrderItem> selectLiveOrderItemByOrderId(Long orderId);
 
     List<LiveOrderItemVo> selectLiveOrderItemByOrderIds(@Param("orderIds") List<Long> orderIds);
@@ -73,6 +74,9 @@ public interface LiveOrderItemMapper {
 
     List<LiveOrderItemVo> selectCheckedByUserId(@Param("userId") String userId);
 
-    @Select("select product_id,json_info,num from live_order_item where order_id=#{orderId}")
+    @Select("select product_id,json_info,order_code,item_id,num from live_order_item where order_id=#{orderId}")
     List<LiveOrderItemListUVO> selectLiveOrderItemListUVOByOrderId(Long orderId);
+
+    @Update("update live_order_item set order_code=#{orderCode} where order_id=#{orderId} ")
+    int updateFsStoreOrderCode(@Param("orderId")Long orderId,@Param("orderSn") String orderSn);
 }

+ 9 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveAfterSalesService.java

@@ -10,6 +10,9 @@ import com.fs.live.param.LiveAfterSalesRevokeParam;
 import com.fs.live.vo.LiveAfterSalesListUVO;
 import com.fs.live.vo.LiveAfterSalesVo;
 import com.fs.store.param.LiveAfterSalesAudit1Param;
+import com.fs.store.param.LiveAfterSalesAudit2Param;
+import com.fs.store.param.LiveAfterSalesCancelParam;
+import com.fs.store.param.LiveAfterSalesRefundParam;
 
 import java.util.List;
 
@@ -81,4 +84,10 @@ public interface ILiveAfterSalesService {
     R audit1(LiveAfterSalesAudit1Param audit1Param);
 
     List<LiveAfterSalesVo> selectLiveAfterSalesVoList(LiveAfterSalesVo liveAfterSales);
+
+    R audit2(LiveAfterSalesAudit2Param param);
+
+    R refundMoney(LiveAfterSalesRefundParam param);
+
+    R cancel(LiveAfterSalesCancelParam param);
 }

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveCouponIssueService.java

@@ -0,0 +1,61 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponIssue;
+
+/**
+ * 优惠券领取Service接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface ILiveCouponIssueService
+{
+    /**
+     * 查询优惠券领取
+     *
+     * @param id 优惠券领取ID
+     * @return 优惠券领取
+     */
+    public LiveCouponIssue selectLiveCouponIssueById(Long id);
+
+    /**
+     * 查询优惠券领取列表
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 优惠券领取集合
+     */
+    public List<LiveCouponIssue> selectLiveCouponIssueList(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 新增优惠券领取
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 结果
+     */
+    public int insertLiveCouponIssue(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 修改优惠券领取
+     *
+     * @param liveCouponIssue 优惠券领取
+     * @return 结果
+     */
+    public int updateLiveCouponIssue(LiveCouponIssue liveCouponIssue);
+
+    /**
+     * 批量删除优惠券领取
+     *
+     * @param ids 需要删除的优惠券领取ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueByIds(Long[] ids);
+
+    /**
+     * 删除优惠券领取信息
+     *
+     * @param id 优惠券领取ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueById(Long id);
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveCouponIssueUserService.java

@@ -0,0 +1,61 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponIssueUser;
+
+/**
+ * 优惠券用户领取记录Service接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface ILiveCouponIssueUserService
+{
+    /**
+     * 查询优惠券用户领取记录
+     *
+     * @param id 优惠券用户领取记录ID
+     * @return 优惠券用户领取记录
+     */
+    public LiveCouponIssueUser selectLiveCouponIssueUserById(Long id);
+
+    /**
+     * 查询优惠券用户领取记录列表
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 优惠券用户领取记录集合
+     */
+    public List<LiveCouponIssueUser> selectLiveCouponIssueUserList(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 新增优惠券用户领取记录
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 结果
+     */
+    public int insertLiveCouponIssueUser(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 修改优惠券用户领取记录
+     *
+     * @param liveCouponIssueUser 优惠券用户领取记录
+     * @return 结果
+     */
+    public int updateLiveCouponIssueUser(LiveCouponIssueUser liveCouponIssueUser);
+
+    /**
+     * 批量删除优惠券用户领取记录
+     *
+     * @param ids 需要删除的优惠券用户领取记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueUserByIds(Long[] ids);
+
+    /**
+     * 删除优惠券用户领取记录信息
+     *
+     * @param id 优惠券用户领取记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponIssueUserById(Long id);
+}

+ 64 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveCouponService.java

@@ -0,0 +1,64 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.fs.live.domain.LiveCoupon;
+import com.fs.store.domain.FsStoreCoupon;
+
+/**
+ * 优惠券Service接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface ILiveCouponService
+{
+    /**
+     * 查询优惠券
+     *
+     * @param couponId 优惠券ID
+     * @return 优惠券
+     */
+    public LiveCoupon selectLiveCouponById(Long couponId);
+
+    /**
+     * 查询优惠券列表
+     *
+     * @param liveCoupon 优惠券
+     * @return 优惠券集合
+     */
+    public List<LiveCoupon> selectLiveCouponList(LiveCoupon liveCoupon);
+
+    /**
+     * 新增优惠券
+     *
+     * @param liveCoupon 优惠券
+     * @return 结果
+     */
+    public int insertLiveCoupon(LiveCoupon liveCoupon);
+
+    /**
+     * 修改优惠券
+     *
+     * @param liveCoupon 优惠券
+     * @return 结果
+     */
+    public int updateLiveCoupon(LiveCoupon liveCoupon);
+
+    /**
+     * 批量删除优惠券
+     *
+     * @param couponIds 需要删除的优惠券ID
+     * @return 结果
+     */
+    public int deleteLiveCouponByIds(Long[] couponIds);
+
+    /**
+     * 删除优惠券信息
+     *
+     * @param couponId 优惠券ID
+     * @return 结果
+     */
+    public int deleteLiveCouponById(Long couponId);
+
+    List<LiveCoupon> selectLiveCouponByIds(String ids);
+}

+ 61 - 0
fs-service-system/src/main/java/com/fs/live/service/ILiveCouponUserService.java

@@ -0,0 +1,61 @@
+package com.fs.live.service;
+
+import java.util.List;
+import com.fs.live.domain.LiveCouponUser;
+
+/**
+ * 优惠券发放记录Service接口
+ *
+ * @author fs
+ * @date 2025-09-30
+ */
+public interface ILiveCouponUserService
+{
+    /**
+     * 查询优惠券发放记录
+     *
+     * @param id 优惠券发放记录ID
+     * @return 优惠券发放记录
+     */
+    public LiveCouponUser selectLiveCouponUserById(Long id);
+
+    /**
+     * 查询优惠券发放记录列表
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 优惠券发放记录集合
+     */
+    public List<LiveCouponUser> selectLiveCouponUserList(LiveCouponUser liveCouponUser);
+
+    /**
+     * 新增优惠券发放记录
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 结果
+     */
+    public int insertLiveCouponUser(LiveCouponUser liveCouponUser);
+
+    /**
+     * 修改优惠券发放记录
+     *
+     * @param liveCouponUser 优惠券发放记录
+     * @return 结果
+     */
+    public int updateLiveCouponUser(LiveCouponUser liveCouponUser);
+
+    /**
+     * 批量删除优惠券发放记录
+     *
+     * @param ids 需要删除的优惠券发放记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponUserByIds(Long[] ids);
+
+    /**
+     * 删除优惠券发放记录信息
+     *
+     * @param id 优惠券发放记录ID
+     * @return 结果
+     */
+    public int deleteLiveCouponUserById(Long id);
+}

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