Sfoglia il codice sorgente

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

15376779826 2 settimane fa
parent
commit
419c942478
78 ha cambiato i file con 2931 aggiunte e 502 eliminazioni
  1. 3 8
      fs-admin/src/main/java/com/fs/FSApplication.java
  2. 35 8
      fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java
  3. 61 75
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  4. 23 0
      fs-admin/src/main/java/com/fs/hisStore/task/ExpressTask.java
  5. 2 2
      fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java
  6. 14 0
      fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java
  7. 2 0
      fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java
  8. 2 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveAutoTaskController.java
  9. 13 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveCouponController.java
  10. 14 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
  11. 1 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java
  12. 6 5
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  13. 11 4
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  14. 1 1
      fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java
  15. 17 0
      fs-service/src/main/java/com/fs/course/dto/ImportFailItemDTO.java
  16. 62 0
      fs-service/src/main/java/com/fs/course/dto/ImportResultDTO.java
  17. 2 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java
  18. 2 1
      fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java
  19. 1 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCoursePeriodDaysService.java
  20. 6 9
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java
  21. 19 11
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  22. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  23. 36 15
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  24. 16 39
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  25. 4 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  26. 101 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  27. 2 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  28. 1 1
      fs-service/src/main/java/com/fs/live/domain/LiveAutoTask.java
  29. 9 3
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  30. 11 0
      fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java
  31. 12 11
      fs-service/src/main/java/com/fs/live/param/LiveDataParam.java
  32. 1 1
      fs-service/src/main/java/com/fs/live/service/ILiveAutoTaskService.java
  33. 26 21
      fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java
  34. 22 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  35. 293 150
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  36. 50 33
      fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java
  37. 141 0
      fs-service/src/main/java/com/fs/pay/domain/PaymentMiniProgramConfig.java
  38. 70 0
      fs-service/src/main/java/com/fs/pay/mapper/PaymentMiniProgramConfigMapper.java
  39. 2 1
      fs-service/src/main/java/com/fs/sop/mapper/QwSopTempMapper.java
  40. 6 1
      fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java
  41. 6 0
      fs-service/src/main/java/com/fs/sop/service/IQwSopTempContentService.java
  42. 5 1
      fs-service/src/main/java/com/fs/sop/service/IQwSopTempDayService.java
  43. 6 0
      fs-service/src/main/java/com/fs/sop/service/IQwSopTempRulesService.java
  44. 5 3
      fs-service/src/main/java/com/fs/sop/service/IQwSopTempService.java
  45. 1 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java
  46. 1 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java
  47. 15 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempContentServiceImpl.java
  48. 9 0
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempDayServiceImpl.java
  49. 26 19
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempRulesServiceImpl.java
  50. 140 62
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempServiceImpl.java
  51. 104 0
      fs-service/src/main/java/com/fs/wx/order/domain/FsWxExpressTask.java
  52. 21 0
      fs-service/src/main/java/com/fs/wx/order/dto/Contact.java
  53. 27 0
      fs-service/src/main/java/com/fs/wx/order/dto/OrderKey.java
  54. 77 0
      fs-service/src/main/java/com/fs/wx/order/dto/OrderQueryRequest.java
  55. 236 0
      fs-service/src/main/java/com/fs/wx/order/dto/OrderQueryResponse.java
  56. 17 0
      fs-service/src/main/java/com/fs/wx/order/dto/Payer.java
  57. 26 0
      fs-service/src/main/java/com/fs/wx/order/dto/ShippingItem.java
  58. 42 0
      fs-service/src/main/java/com/fs/wx/order/dto/UploadShippingInfoRequest.java
  59. 21 0
      fs-service/src/main/java/com/fs/wx/order/dto/WeChatApiConfig.java
  60. 20 0
      fs-service/src/main/java/com/fs/wx/order/dto/WeChatApiResponse.java
  61. 117 0
      fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java
  62. 30 0
      fs-service/src/main/java/com/fs/wx/order/service/ExpressToWxHolder.java
  63. 27 0
      fs-service/src/main/java/com/fs/wx/order/service/ExpressToWxService.java
  64. 20 0
      fs-service/src/main/java/com/fs/wx/order/service/OrderQueryService.java
  65. 128 0
      fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java
  66. 46 0
      fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthFactory.java
  67. 22 0
      fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthService.java
  68. 114 0
      fs-service/src/main/java/com/fs/wx/order/service/impl/InMemoryWeChatAuthServiceImpl.java
  69. 107 0
      fs-service/src/main/java/com/fs/wx/order/service/impl/LiveExpressToWxService.java
  70. 107 0
      fs-service/src/main/java/com/fs/wx/order/service/impl/ShopExpressToWxService.java
  71. 5 0
      fs-service/src/main/resources/application-common.yml
  72. 1 1
      fs-service/src/main/resources/application-config-druid-kyt.yml
  73. 1 1
      fs-service/src/main/resources/application-config-druid-qdtst.yml
  74. 5 0
      fs-service/src/main/resources/mapper/course/FsUserCoursePeriodDaysMapper.xml
  75. 1 0
      fs-service/src/main/resources/mapper/live/LiveAutoTaskMapper.xml
  76. 53 11
      fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
  77. 42 1
      fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml
  78. 195 0
      fs-service/src/main/resources/mapper/pay/PaymentMiniProgramConfigMapper.xml

+ 3 - 8
fs-admin/src/main/java/com/fs/FSApplication.java

@@ -1,11 +1,8 @@
 package com.fs;
 
-import com.qiniu.common.Zone;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.Transactional;
@@ -13,14 +10,12 @@ import org.springframework.transaction.annotation.Transactional;
 /**
  * 启动程序
  */
-@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
 @Transactional
 @EnableAsync
 @EnableScheduling
-public class FSApplication
-{
-    public static void main(String[] args)
-    {
+public class FSApplication {
+    public static void main(String[] args) {
         // System.setProperty("spring.devtools.restart.enabled", "false");
         SpringApplication.run(FSApplication.class, args);
         System.out.println("admin启动成功");

+ 35 - 8
fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java

@@ -13,6 +13,7 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
+import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.framework.web.service.TokenService;
 import com.fs.system.service.ISysConfigService;
@@ -21,7 +22,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 题库Controller
@@ -80,6 +83,15 @@ public class FsCourseQuestionBankController extends BaseController
         return util.exportExcel(list, "题库数据");
     }
 
+    @PreAuthorize("@ss.hasPermi('course:courseQuestionBank:exportFail')")
+    @Log(title = "题库", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportFail")
+    public AjaxResult export( @RequestBody List<FsCourseQuestionBankImportDTO> list)
+    {
+        ExcelUtil<FsCourseQuestionBankImportDTO> util = new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
+        return util.exportExcel(list, "题库错误数据");
+    }
+
     /**
      * 获取题库详细信息
      */
@@ -144,19 +156,34 @@ public class FsCourseQuestionBankController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:courseQuestionBank:importData')")
     @PostMapping("/importData")
     public AjaxResult importData(MultipartFile file) throws Exception {
-        ExcelUtil<FsCourseQuestionBankImportDTO> util = new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
-        List<FsCourseQuestionBankImportDTO> list = util.importExcel(file.getInputStream());
+
+        ExcelUtil<FsCourseQuestionBankImportDTO> util =
+                new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
+        List<FsCourseQuestionBankImportDTO> list =
+                util.importExcel(file.getInputStream());
 
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
+
+        // 读取配置
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
-            String message = fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(),userId);
-            return AjaxResult.success(message);
-        }
-        String message = fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(),null);
-        return AjaxResult.success(message);
+
+        // 绑定状态控制 userId
+        Long finalUserId = (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound())
+                ? userId
+                : null;
+
+        // 调用 service
+        ImportResultDTO result =
+                fsCourseQuestionBankService.importData(list, loginUser.getUser().getNickName(), finalUserId);
+
+        // 返回 message + failList
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("message", result.buildResultMessage());
+        resp.put("failList", result.getFailureList());
+
+        return AjaxResult.success(resp);
     }
 
     @GetMapping(value = "/getByIds")

+ 61 - 75
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -1,39 +1,33 @@
 package com.fs.course.controller;
 
-import java.util.List;
-
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
+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.domain.model.LoginUser;
+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.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCourse;
 import com.fs.course.params.FsUserCourseConfigParam;
+import com.fs.course.service.IFsUserCourseService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseListPVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.param.FsUserCourseRedPageParam;
+import com.fs.sop.service.IQwSopTempService;
 import com.fs.system.service.ISysConfigService;
-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.course.domain.FsUserCourse;
-import com.fs.course.service.IFsUserCourseService;
-import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.common.core.page.TableDataInfo;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
 
 /**
  * 课程Controller
@@ -43,8 +37,7 @@ import com.fs.common.core.page.TableDataInfo;
  */
 @RestController
 @RequestMapping("/course/userCourse")
-public class FsUserCourseController extends BaseController
-{
+public class FsUserCourseController extends BaseController {
     @Autowired
     private IFsUserCourseService fsUserCourseService;
 
@@ -60,19 +53,21 @@ public class FsUserCourseController extends BaseController
     @Autowired
     private ISysConfigService configService;
 
+    @Autowired
+    private IQwSopTempService sopTempService;
+
     /**
      * 查询课程列表
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:list')")
     @GetMapping("/list")
-    public TableDataInfo list(FsUserCourse fsUserCourse)
-    {
+    public TableDataInfo list(FsUserCourse fsUserCourse) {
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourseListPVO> list = fsUserCourseService.selectFsUserCourseListPVO(fsUserCourse);
@@ -84,14 +79,13 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicList')")
     @GetMapping("/publicList")
-    public TableDataInfo publicList(FsUserCourse fsUserCourse)
-    {
+    public TableDataInfo publicList(FsUserCourse fsUserCourse) {
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourseListPVO> list = fsUserCourseService.selectFsUserCourseListPVO(fsUserCourse);
@@ -104,13 +98,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:export')")
     @Log(title = "课程", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
-    public AjaxResult export(FsUserCourse fsUserCourse)
-    {
+    public AjaxResult export(FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourse> list = fsUserCourseService.selectFsUserCourseList(fsUserCourse);
@@ -124,13 +117,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicExport')")
     @Log(title = "课程", businessType = BusinessType.EXPORT)
     @GetMapping("/publicExport")
-    public AjaxResult publicExport(FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicExport(FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         List<FsUserCourse> list = fsUserCourseService.selectFsUserCourseList(fsUserCourse);
@@ -143,8 +135,7 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:query')")
     @GetMapping(value = "/{courseId}")
-    public AjaxResult getInfo(@PathVariable("courseId") Long courseId)
-    {
+    public AjaxResult getInfo(@PathVariable("courseId") Long courseId) {
         return AjaxResult.success(fsUserCourseService.selectFsUserCourseByCourseId(courseId));
     }
 
@@ -153,8 +144,7 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicQuery')")
     @GetMapping(value = "/public/{courseId}")
-    public AjaxResult publicGetInfo(@PathVariable("courseId") Long courseId)
-    {
+    public AjaxResult publicGetInfo(@PathVariable("courseId") Long courseId) {
         return AjaxResult.success(fsUserCourseService.selectFsUserCourseByCourseId(courseId));
     }
 
@@ -164,13 +154,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:add')")
     @Log(title = "课程", businessType = BusinessType.INSERT)
     @PostMapping
-    public AjaxResult add(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult add(@RequestBody FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
@@ -185,13 +174,12 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicAdd')")
     @Log(title = "课程", businessType = BusinessType.INSERT)
     @PostMapping("/public")
-    public AjaxResult publicAdd(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicAdd(@RequestBody FsUserCourse fsUserCourse) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long userId = loginUser.getUser().getUserId();
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsUserCourse.setUserId(userId);
         }
         fsUserCourseService.insertFsUserCourse(fsUserCourse);
@@ -206,8 +194,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:edit')")
     @Log(title = "课程", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult edit(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -219,8 +206,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:editRedPage')")
     @Log(title = "修改课程红包", businessType = BusinessType.UPDATE)
     @PostMapping("/editRedPage")
-    public AjaxResult editRedPage(@RequestBody FsUserCourseRedPageParam redPageParam)
-    {
+    public AjaxResult editRedPage(@RequestBody FsUserCourseRedPageParam redPageParam) {
         courseVideoService.updateFsUserCourseRedPage(redPageParam);
         return toAjax(1);
     }
@@ -231,8 +217,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicEdit')")
     @Log(title = "课程", businessType = BusinessType.UPDATE)
     @PutMapping("/public")
-    public AjaxResult publicEdit(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicEdit(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -244,8 +229,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:copy')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
     @GetMapping("/copy/{courseId}")
-    public AjaxResult copy(@PathVariable Long courseId)
-    {
+    public AjaxResult copy(@PathVariable Long courseId) {
         int i = fsUserCourseService.copyFsUserCourse(courseId);
         return toAjax(i);
     }
@@ -255,8 +239,8 @@ public class FsUserCourseController extends BaseController
      */
     @PreAuthorize("@ss.hasPermi('course:userCourse:remove')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
-	@DeleteMapping("/{courseIds}")
-    public AjaxResult remove(@PathVariable Long[] courseIds){
+    @DeleteMapping("/{courseIds}")
+    public AjaxResult remove(@PathVariable Long[] courseIds) {
         fsUserCourseService.deleteFsUserCourseByCourseIds(courseIds);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -268,8 +252,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicRemove')")
     @Log(title = "课程", businessType = BusinessType.DELETE)
     @DeleteMapping("/public/{courseIds}")
-    public AjaxResult publicRemove(@PathVariable Long[] courseIds)
-    {
+    public AjaxResult publicRemove(@PathVariable Long[] courseIds) {
         fsUserCourseService.deleteFsUserCourseByCourseIds(courseIds);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -277,8 +260,7 @@ public class FsUserCourseController extends BaseController
 
 
     @GetMapping("/getAllList")
-    public R getAllList()
-    {
+    public R getAllList() {
         List<OptionsVO> list = fsUserCourseService.selectFsUserCourseAllList();
         return R.ok().put("data", list);
     }
@@ -286,18 +268,16 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:updateIsShow')")
     @Log(title = "课程上架", businessType = BusinessType.UPDATE)
     @PostMapping("/updateIsShow")
-    public AjaxResult updateIsShow(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult updateIsShow(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
-       return toAjax(1);
+        return toAjax(1);
     }
 
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicUpdateIsShow')")
     @Log(title = "课程上架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicUpdateIsShow")
-    public AjaxResult publicUpdateIsShow(@RequestBody FsUserCourse fsUserCourse)
-    {
+    public AjaxResult publicUpdateIsShow(@RequestBody FsUserCourse fsUserCourse) {
         fsUserCourseService.updateFsUserCourse(fsUserCourse);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
@@ -306,9 +286,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:putOn')")
     @Log(title = "课程批量上架", businessType = BusinessType.UPDATE)
     @PostMapping("/putOn/{courseIds}")
-    public AjaxResult putOn(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,1);
+    public AjaxResult putOn(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 1);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -316,9 +295,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicPutOn')")
     @Log(title = "课程批量上架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicPutOn/{courseIds}")
-    public AjaxResult publicPutOn(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,1);
+    public AjaxResult publicPutOn(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 1);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -326,9 +304,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:putOn')")
     @Log(title = "课程批量下架", businessType = BusinessType.UPDATE)
     @PostMapping("/pullOff/{courseIds}")
-    public AjaxResult pullOff(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,0);
+    public AjaxResult pullOff(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 0);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -336,9 +313,8 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:publicPutOff')")
     @Log(title = "课程批量下架", businessType = BusinessType.UPDATE)
     @PostMapping("/publicPutOff/{courseIds}")
-    public AjaxResult publicPutOff(@PathVariable Long[] courseIds)
-    {
-        fsUserCourseService.updateFsUserCourseIsShow(courseIds,0);
+    public AjaxResult publicPutOff(@PathVariable Long[] courseIds) {
+        fsUserCourseService.updateFsUserCourseIsShow(courseIds, 0);
         redisCacheUtil.delRedisKey("getCourseList");
         return toAjax(1);
     }
@@ -349,7 +325,7 @@ public class FsUserCourseController extends BaseController
     @PreAuthorize("@ss.hasPermi('course:userCourse:editConfig')")
     @Log(title = "课程配置", businessType = BusinessType.UPDATE)
     @PostMapping("/editConfig")
-    public R editConfig(@RequestBody FsUserCourseConfigParam params){
+    public R editConfig(@RequestBody FsUserCourseConfigParam params) {
         fsUserCourseService.editConfig(params.getId(), params.getConfigJson());
         redisCacheUtil.delRedisKey("getCourseList");
         redisCacheUtil.delRedisKey("h5user:course:video:list:all");
@@ -357,4 +333,14 @@ public class FsUserCourseController extends BaseController
         redisCacheUtil.delRedisKey("cache:video");
         return R.ok();
     }
+
+    /**
+     * 同步课程模板
+     */
+    @Log(title = "同步课程模板", businessType = BusinessType.UPDATE)
+    @PostMapping("/syncTemplate/{courseId}")
+    public AjaxResult syncTemplate(@PathVariable Long courseId) {
+        sopTempService.syncTemplate(courseId);
+        return toAjax(1);
+    }
 }

+ 23 - 0
fs-admin/src/main/java/com/fs/hisStore/task/ExpressTask.java

@@ -0,0 +1,23 @@
+package com.fs.hisStore.task;
+
+
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 物流信息定时任务
+ */
+@Slf4j
+@Component("expressTask")
+public class ExpressTask {
+
+    @Autowired
+    private IFsStoreOrderScrmService fsStoreOrderScrmService;
+
+    public void syncExpressToWx(){
+        fsStoreOrderScrmService.syncExpressToWx();
+    }
+
+}

+ 2 - 2
fs-admin/src/main/java/com/fs/live/controller/LiveAutoTaskController.java

@@ -116,9 +116,9 @@ public class LiveAutoTaskController extends BaseController
 //    @PreAuthorize("@ss.hasPermi('shop:task:edit')")
     @Log(title = "直播间自动化任务配置", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody LiveAutoTask liveAutoTask)
+    public R edit(@RequestBody LiveAutoTask liveAutoTask)
     {
-        return toAjax(liveAutoTaskService.updateLiveAutoTask(liveAutoTask));
+        return liveAutoTaskService.updateLiveAutoTask(liveAutoTask);
     }
 
     /**

+ 14 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java

@@ -58,6 +58,7 @@ import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Date;
@@ -155,6 +156,11 @@ public class LiveOrderController extends BaseController
         List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
         for (LiveOrderVoZm vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
         }
         return getDataTable(list);
     }
@@ -168,6 +174,14 @@ public class LiveOrderController extends BaseController
     public AjaxResult exportZm(LiveOrder liveOrder)
     {
         List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
+        }
         ExcelUtil<LiveOrderVoZm> util = new ExcelUtil<LiveOrderVoZm>(LiveOrderVoZm.class);
         return util.exportExcel(list, "订单数据");
     }

+ 2 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java

@@ -1,5 +1,6 @@
 package com.fs.qw.controller;
 
+import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
@@ -9,6 +10,7 @@ import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.TimeUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.web.service.TokenService;
 import com.fs.qw.vo.SortDayVo;

+ 2 - 2
fs-company/src/main/java/com/fs/company/controller/live/LiveAutoTaskController.java

@@ -116,9 +116,9 @@ public class LiveAutoTaskController extends BaseController
     @PreAuthorize("@ss.hasPermi('live:task:edit')")
     @Log(title = "直播间自动化任务配置", businessType = BusinessType.UPDATE)
     @PutMapping
-    public AjaxResult edit(@RequestBody LiveAutoTask liveAutoTask)
+    public R edit(@RequestBody LiveAutoTask liveAutoTask)
     {
-        return toAjax(liveAutoTaskService.updateLiveAutoTask(liveAutoTask));
+        return liveAutoTaskService.updateLiveAutoTask(liveAutoTask);
     }
 
     /**

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveCouponController.java

@@ -73,6 +73,19 @@ public class LiveCouponController extends BaseController
         return AjaxResult.success(liveCouponService.selectLiveCouponById(couponId));
     }
 
+    /**
+     * 查询优惠券列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveCoupon:list')")
+    @GetMapping("/listOn")
+    public TableDataInfo listOn(@RequestParam("liveId") Long liveId)
+    {
+        startPage();
+        List<LiveCoupon> list = liveCouponService.listOn(liveId);
+        return getDataTable(list);
+    }
+
+
     /**
      * 新增优惠券
      */

+ 14 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java

@@ -36,6 +36,7 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -122,6 +123,11 @@ public class LiveOrderController extends BaseController
         List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
         for (LiveOrderVoZm vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
         }
         return getDataTable(list);
     }
@@ -136,6 +142,14 @@ public class LiveOrderController extends BaseController
     {
         liveOrder.setCompanyId(SecurityUtils.getLoginUser().getUser().getCompanyId());
         List<LiveOrderVoZm> list = liveOrderService.selectLiveOrderListZm(liveOrder);
+        for (LiveOrderVoZm vo : list){
+            vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setCompanyUserPhone(ParseUtils.parsePhone(vo.getCompanyUserPhone()));
+            vo.setUserBindPhone(ParseUtils.parsePhone(vo.getUserBindPhone()));
+            vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+            vo.setCost(BigDecimal.ZERO);
+            vo.setCostPrice(BigDecimal.ZERO);
+        }
         ExcelUtil<LiveOrderVoZm> util = new ExcelUtil<LiveOrderVoZm>(LiveOrderVoZm.class);
         return util.exportExcel(list, "订单数据");
     }

+ 1 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.qw;
 
+import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;

+ 6 - 5
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -142,6 +142,11 @@ public class Task {
                 }
             }
         });
+        if(!liveList.isEmpty()){
+            for (Live live : liveList) {
+                liveService.updateLiveEntity(live);
+            }
+        }
         String key = "live:auto_task:";
         if (!startLiveList.isEmpty()) {
             for (Live live : startLiveList) {
@@ -182,11 +187,7 @@ public class Task {
             // 重新更新所有在直播的缓存
             liveService.asyncToCache();
         }
-        if(!liveList.isEmpty()){
-            for (Live live : liveList) {
-                liveService.updateLiveEntity(live);
-            }
-        }
+
     }
     @Scheduled(cron = "0/1 * * * * ?")
     @DistributeLock(key = "liveLotteryTask", scene = "task")

+ 11 - 4
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -7,6 +7,8 @@ import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.exception.base.BaseException;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.config.ProductionWordFilter;
 import com.fs.live.mapper.LiveCouponMapper;
 import com.fs.live.websocket.auth.WebSocketConfigurator;
@@ -25,6 +27,7 @@ import org.springframework.stereotype.Component;
 
 import javax.websocket.*;
 import javax.websocket.server.ServerEndpoint;
+import java.io.EOFException;
 import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
@@ -50,7 +53,7 @@ public class WebSocketServer {
     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 IFsUserScrmService fsUserService = SpringUtils.getBean(IFsUserScrmService.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);
@@ -93,7 +96,7 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -192,7 +195,7 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
         if (userType == 0) {
-            FsUser fsUser = fsUserService.selectFsUserByUserId(userId);
+            FsUserScrm fsUser = fsUserService.selectFsUserByUserId(userId);
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -466,7 +469,11 @@ public class WebSocketServer {
     //错误时调用
     @OnError
     public void onError(Session session, Throwable throwable) {
-        log.error("webSocKet连接错误 msg: {}", throwable.getMessage(), throwable);
+        if (throwable instanceof EOFException) {
+            log.info("WebSocket连接被客户端正常关闭(EOF),sessionId: {}", session.getId());
+        } else {
+            log.error("WebSocket连接错误", throwable);
+        }
     }
 
     /**

+ 1 - 1
fs-service/src/main/java/com/fs/course/domain/FsCourseLink.java

@@ -53,7 +53,7 @@ public class FsCourseLink extends BaseEntity
     */
     private Long qwExternalId;
 
-    private Integer linkType; //链接类型 0:正常链接  1:应急链接  2:小程序链接
+    private Integer linkType; //链接类型 0:正常链接  1:应急链接  3:小程序链接 4:APP
 
     private Integer isRoom;//是否发群
     private String chatId;//是否发群

+ 17 - 0
fs-service/src/main/java/com/fs/course/dto/ImportFailItemDTO.java

@@ -0,0 +1,17 @@
+package com.fs.course.dto;
+
+import lombok.Data;
+
+@Data
+public class ImportFailItemDTO {
+
+    private FsCourseQuestionBankImportDTO rowData; // 原始 Excel 行
+    private String reason; // 失败原因
+
+    public ImportFailItemDTO(FsCourseQuestionBankImportDTO rowData, String reason) {
+        this.rowData = rowData;
+        this.reason = reason;
+    }
+
+}
+

+ 62 - 0
fs-service/src/main/java/com/fs/course/dto/ImportResultDTO.java

@@ -0,0 +1,62 @@
+package com.fs.course.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ImportResultDTO {
+
+
+    private int successNum = 0;
+    private int failureNum = 0;
+
+    // 用于 message 拼接
+    private final List<String> failureMsgList = new ArrayList<>();
+
+    // 返回给前端的失败原始数据 list(你要的)
+    private final List<FsCourseQuestionBankImportDTO> failureList = new ArrayList<>();
+
+    public void addSuccess() {
+        successNum++;
+    }
+
+    public void addFailure(FsCourseQuestionBankImportDTO dto, String reason) {
+        failureNum++;
+
+        // message 用
+        failureMsgList.add("题目 " + dto.getTitle() + " 导入失败:" + reason);
+
+        // list 记录原始数据
+        failureList.add(dto);
+    }
+
+    public List<FsCourseQuestionBankImportDTO> getFailureList() {
+        return failureList;
+    }
+
+    /**
+     * 构建你要的 HTML 格式 message
+     */
+    public String buildResultMessage() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append("导入完成!成功")
+                .append(successNum)
+                .append(" 条,失败")
+                .append(failureNum)
+                .append("条。");
+
+        if (!failureMsgList.isEmpty()) {
+            sb.append("<br/>");
+            int index = 1;
+            for (String msg : failureMsgList) {
+                sb.append(index++)
+                        .append("、")
+                        .append(msg)
+                        .append("<br/>");
+            }
+        }
+
+        return sb.toString();
+    }
+    }
+

+ 2 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java

@@ -131,4 +131,6 @@ public interface FsUserCoursePeriodDaysMapper extends BaseMapper<FsUserCoursePer
      * @Date 2025/11/18 11:04
      */
     List<Long> selectFsUserCoursePeriodDaysForLastById(FsUserCoursePeriodDays param);
+
+    List<FsUserCoursePeriodDays> selectFsUserCoursePeriodDaysByCourseId(@Param("courseId") Long courseId);
 }

+ 2 - 1
fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java

@@ -3,6 +3,7 @@ package com.fs.course.service;
 import com.fs.common.core.domain.R;
 import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
+import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
 
 import javax.validation.constraints.Size;
@@ -73,7 +74,7 @@ public interface IFsCourseQuestionBankService
      * @param nickName 昵称
      * @return String
      */
-    String importData(List<FsCourseQuestionBankImportDTO> list, String nickName,Long userId);
+    ImportResultDTO importData(List<FsCourseQuestionBankImportDTO> list, String nickName, Long userId);
 
     /**
      * 根据ID查询题目

+ 1 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCoursePeriodDaysService.java

@@ -122,4 +122,5 @@ public interface IFsUserCoursePeriodDaysService extends IService<FsUserCoursePer
 
     List<Long> selectFsUserCoursePeriodDaysByTime(String periodSTime,String periodETime);
 
+        List<FsUserCoursePeriodDays> selectFsUserCoursePeriodDaysByCourseId(Long courseId);
     }

+ 6 - 9
fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java

@@ -1,10 +1,5 @@
 package com.fs.course.service;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-import java.util.Map;
-
 import com.fs.common.core.domain.R;
 import com.fs.course.domain.FsUserCourse;
 import com.fs.course.param.*;
@@ -13,8 +8,11 @@ import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseListVO;
 import com.fs.his.vo.OptionsVO;
 
-import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
 
 /**
  * 课程Service接口
@@ -22,8 +20,7 @@ import javax.validation.constraints.NotNull;
  * @author fs
  * @date 2024-05-15
  */
-public interface IFsUserCourseService
-{
+public interface IFsUserCourseService {
     /**
      * 查询课程
      *
@@ -110,7 +107,7 @@ public interface IFsUserCourseService
 
     List<FsUserCourseListVO> getFsUserCourseList(FsUserCourseListParam param);
 
-    void  processQwSopCourseMaterialTimer();
+    void processQwSopCourseMaterialTimer();
 
     List<FsCourseListBySidebarVO> getFsCourseListBySidebar(FsCourseListBySidebarParam param);
 

+ 19 - 11
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -11,6 +11,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.*;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
+import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.mapper.*;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
 import com.fs.course.service.IFsCourseQuestionBankService;
@@ -468,43 +469,50 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
      * @return String
      */
     @Override
-    public String importData(List<FsCourseQuestionBankImportDTO> list, String nickName,Long userId) {
+    public ImportResultDTO importData(List<FsCourseQuestionBankImportDTO> list, String nickName, Long userId) {
+
         if (Objects.isNull(list) || list.isEmpty()) {
             throw new ServiceException("导入数据不能为空");
         }
 
-        ImportResult result = new ImportResult();
+        ImportResultDTO result = new ImportResultDTO();
         List<FsCourseQuestionBank> importData = new ArrayList<>();
-        Map<String, FsUserCourseCategory> categoryData = courseCategoryMapper.queryAllCategoryData();
+
+        // 分类数据判空保护
+        Map<String, FsUserCourseCategory> categoryData =
+                Optional.ofNullable(courseCategoryMapper.queryAllCategoryData())
+                        .orElse(Collections.emptyMap());
 
         for (FsCourseQuestionBankImportDTO importDTO : list) {
             try {
-                // 数据验证
                 ValidationResult validation = validateImportData(importDTO);
                 if (!validation.isValid()) {
-                    result.addFailure(importDTO.getTitle(), validation.getErrorMessage());
+                    result.addFailure(importDTO, validation.getErrorMessage());
                     continue;
                 }
 
-                // 构建题目对象
-                FsCourseQuestionBank questionBank = buildQuestionBank(importDTO, categoryData, nickName);
+                FsCourseQuestionBank questionBank =
+                        buildQuestionBank(importDTO, categoryData, nickName);
+
                 questionBank.setUserId(userId);
+
                 importData.add(questionBank);
-                result.addSuccess(importDTO.getTitle());
+                result.addSuccess();
 
             } catch (Exception e) {
-                result.addFailure(importDTO.getTitle(), "导入异常: " + e.getMessage());
+                result.addFailure(importDTO, "导入异常:" + e.getMessage());
             }
         }
 
-        // 批量保存
+        // 4. 批量插入
         if (!importData.isEmpty()) {
             fsCourseQuestionBankMapper.insertFsCourseQuestionBankBatch(importData);
         }
 
-        return result.buildResultMessage();
+        return result;
     }
 
+
     /**
      * 验证导入数据
      */

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -479,6 +479,11 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
         return fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysByTime(periodSTime,periodETime);
     }
 
+    @Override
+    public List<FsUserCoursePeriodDays> selectFsUserCoursePeriodDaysByCourseId(Long courseId) {
+        return fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysByCourseId(courseId);
+    }
+
     private static FsCourseAnalysisCountVO getCourseAnalysisCountVO(FsUserCoursePeriodDays v, Map<Long, FsCourseAnalysisCountVO> courseMap, Map<Long, FsCourseAnalysisCountVO> redPacketMap, Map<Long, FsCourseAnalysisCountVO> answerMap) {
         FsCourseAnalysisCountVO countVO = new FsCourseAnalysisCountVO();
         FsCourseAnalysisCountVO courseVO = courseMap.getOrDefault(v.getVideoId(), countVO);

+ 36 - 15
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -26,6 +26,7 @@ import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
 import com.fs.course.param.*;
 import com.fs.course.param.newfs.FsUserCourseListParam;
+import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseListVO;
@@ -128,6 +129,9 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     @Autowired
     private IFsUserIntegralLogsService userIntegralLogsService;
 
+    @Autowired
+    private IFsUserCoursePeriodDaysService fsUserCoursePeriodDaysService;
+
     private static final String realLink = "/courseH5/pages/course/learning?course=";
     public static final String shortLink = "/courseH5/pages/course/learning?s=";
 
@@ -690,24 +694,41 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     @Transactional(rollbackFor = Exception.class) // 显式声明事务
     public int copyFsUserCourse(Long courseId) {
         FsUserCourse fsUserCourse = fsUserCourseService.selectFsUserCourseByCourseId(courseId);
-        if(fsUserCourse != null){
-            fsUserCourse.setCourseId(null);
-            fsUserCourseService.insertFsUserCourse(fsUserCourse);
-            Long newCourseId = fsUserCourse.getCourseId();
+        try {
+            if(fsUserCourse != null){
+                fsUserCourse.setCourseId(null);
+                fsUserCourseService.insertFsUserCourse(fsUserCourse);
+                Long newCourseId = fsUserCourse.getCourseId();
 
-            if (newCourseId == null) {
-                throw new RuntimeException("课程插入失败,无法获取新课程ID");
-            }
+                if (newCourseId == null) {
+                    throw new RuntimeException("课程插入失败,无法获取新课程ID");
+                }
+
+                FsUserCourseVideo fsUserCourseVideo = new FsUserCourseVideo();
+                fsUserCourseVideo.setCourseId(courseId);
+                List<FsUserCourseVideo> list = fsUserCourseVideoService.selectFsUserCourseVideoListByCourseId(fsUserCourseVideo);
+                if(list != null && !list.isEmpty()){
+                    for (FsUserCourseVideo courseVideo : list) {
+                        courseVideo.setVideoId(null);
+                        courseVideo.setCourseId(newCourseId);
+                        fsUserCourseVideoService.insertFsUserCourseVideo(courseVideo);
+                    }
+                }
+
+                //增加手动发课部分的复制
+                List<FsUserCoursePeriodDays> fsUserCoursePeriodDays =  fsUserCoursePeriodDaysService.selectFsUserCoursePeriodDaysByCourseId(courseId);
+                if(fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()){
+                    for (FsUserCoursePeriodDays periodDays : fsUserCoursePeriodDays) {
+                        periodDays.setId(null);
+                        periodDays.setCourseId(newCourseId);
+                        fsUserCoursePeriodDaysService.insertFsUserCoursePeriodDays(periodDays);
+                    }
+                }
 
-            FsUserCourseVideo fsUserCourseVideo = new FsUserCourseVideo();
-            fsUserCourseVideo.setCourseId(courseId);
-            List<FsUserCourseVideo> list = fsUserCourseVideoService.selectFsUserCourseVideoListByCourseId(fsUserCourseVideo);
-            for (FsUserCourseVideo courseVideo : list) {
-                courseVideo.setVideoId(null);
-                courseVideo.setCourseId(newCourseId);
-                fsUserCourseVideoService.insertFsUserCourseVideo(courseVideo);
+                return 1;
             }
-            return 1;
+        } catch (RuntimeException e) {
+            throw new RuntimeException("复制失败,请联系开发人员");
         }
 
         return 0;

+ 16 - 39
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -546,8 +546,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
 
-        //服务号授权的,缺mpOpenId的重新登录
-        if (config.getMiniAppAuthType()==2 && StringUtil.strIsNullOrEmpty(fsUser.getMpOpenId())){
+        //服务号授权的,缺mpOpenId的重新登录 linkType = 4为app看课 不需要mp
+        if (param.getLinkType() != 4 && config.getMiniAppAuthType()==2 && StringUtil.strIsNullOrEmpty(fsUser.getMpOpenId())){
             return R.error(401,"授权后可继续!");
         }
 
@@ -604,49 +604,26 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         if(qwGroupChatUsers == null || qwGroupChatUsers.isEmpty()){
             return R.error("群参数异常");
         }
-        //群聊寻找用户新逻辑
-        QwExternalContact qwExternalContact = null;
-        if( null != param.getUserId() && null == qwExternalContact){
-            try {
-                qwExternalContact =  qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
+        QwExternalContact qwExternalContact =
+                qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
                         .eq("user_id", qwGroupChat.getOwner())
                         .eq("fs_user_id", param.getUserId())
                         .eq("corp_id", param.getCorpId())
                         .eq("status",0));
-            } catch (Exception e){
-                log.error("群聊用户id匹配异常,参数user_id:{},fs_user_id:{},corp_id:{}",qwGroupChat.getOwner(),param.getUserId(),param.getCorpId(),e);
-            }
-        }
-        //找当前群中的用户匹配
-        if(StringUtils.isNotBlank(param.getChatId()) && null == qwExternalContact){
-            List<QwExternalContact> groupChatUserByChatIdAndUserName = qwExternalContactMapper.getGroupChatUserByChatIdAndUserName(qwGroupChat.getOwner(), user.getNickName(), param.getCorpId(), param.getChatId());
-            //没找到用户 || 找到的用户数量大于1 使用userid查询匹配
-            if(null == groupChatUserByChatIdAndUserName || groupChatUserByChatIdAndUserName.isEmpty() || groupChatUserByChatIdAndUserName.size() > 1){
-                log.error("群聊用户昵称匹配异常,参数user_id:{},name:{},corp_id:{},chatId:{}",qwGroupChat.getOwner(),user.getNickName(),param.getCorpId(),param.getChatId());
-            } else {
-                qwExternalContact =  groupChatUserByChatIdAndUserName.get(0);
+        if(null == qwExternalContact){
+            try{
+                //修改成通过昵称匹配
+                qwExternalContact =
+                        qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
+                                .eq("user_id", qwGroupChat.getOwner())
+                                .eq("name", user.getNickName())
+                                .eq("corp_id", param.getCorpId())
+                                .eq("status",0));
+            } catch(Exception e){
+                log.error("群聊用户昵称匹配异常,参数user_id:{},name:{},corp_id:{}",qwGroupChat.getOwner(),user.getNickName(),param.getCorpId(),e);
             }
+
         }
-//        QwExternalContact qwExternalContact =  qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
-//                            .eq("user_id", qwGroupChat.getOwner())
-//                            .eq("fs_user_id", param.getUserId())
-//                            .eq("corp_id", param.getCorpId())
-//                            .eq("status",0));
-//
-//        if(null == qwExternalContact){
-//            try{
-//                //修改成通过昵称匹配
-//                qwExternalContact =
-//                        qwExternalContactMapper.selectOne(new QueryWrapper<QwExternalContact>()
-//                                .eq("user_id", qwGroupChat.getOwner())
-//                                .eq("name", user.getNickName())
-//                                .eq("corp_id", param.getCorpId())
-//                                .eq("status",0));
-//            } catch(Exception e){
-//                log.error("群聊用户昵称匹配异常,参数user_id:{},name:{},corp_id:{}",qwGroupChat.getOwner(),user.getNickName(),param.getCorpId(),e);
-//            }
-//
-//        }
         if(qwExternalContact==null){
             return addCustomerService(param.getQwUserId(),msg);
         }

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java

@@ -39,6 +39,10 @@ import com.fs.his.vo.FsPrescribeVO;
 public interface IFsStoreOrderScrmService
 {
 
+    /**
+     * 同步物流信息到微信
+     */
+    void syncExpressToWx();
     /**
      * 查询订单
      *

+ 101 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -105,6 +105,12 @@ import com.fs.hisStore.enums.*;
 import com.fs.hisStore.service.*;
 import com.fs.system.service.ISysConfigService;
 import com.fs.wx.miniapp.config.WxMaProperties;
+import com.fs.wx.order.domain.FsWxExpressTask;
+import com.fs.wx.order.dto.*;
+import com.fs.wx.order.mapper.FsWxExpressTaskMapper;
+import com.fs.wx.order.service.ExpressToWxHolder;
+import com.fs.wx.order.service.ExpressToWxService;
+import com.fs.wx.order.service.ShippingService;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.ybPay.domain.RefundResult;
 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
@@ -144,6 +150,7 @@ import java.sql.Timestamp;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -363,6 +370,10 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     @Autowired
     private FsStoreOrderDfMapper fsStoreOrderDfMapper;
+    @Autowired
+    private ShippingService shippingService;
+    @Autowired
+    private FsWxExpressTaskMapper fsWxExpressTaskMapper;
 
     @PostConstruct
     public void initErpServiceMap() {
@@ -374,6 +385,81 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         erpServiceMap.put(5, jSTOrderService);     // 聚水潭
         erpServiceMap.put(6, k9OrderService);      // K9
     }
+    @Override
+    public void syncExpressToWx() {
+        List<FsWxExpressTask> fsWxExpressTasks = fsWxExpressTaskMapper.selectPendingData();
+        if (CollectionUtils.isEmpty(fsWxExpressTasks)) {
+            logger.info("当前没有待同步的数据!已取消");
+            return;
+        }
+
+        for (FsWxExpressTask fsWxExpressTask : fsWxExpressTasks) {
+
+            try{
+                UploadShippingInfoRequest request = new UploadShippingInfoRequest();
+
+                OrderKey orderKey = new OrderKey();
+                orderKey.setOrderNumberType(2);
+
+
+                FsUserScrm fsUser = userService.selectFsUserByUserId(fsWxExpressTask.getUserId());
+
+                ExpressToWxService service = ExpressToWxHolder.findBest(fsWxExpressTask.getType(),fsWxExpressTask.getOrderCode());
+                Asserts.notNull(service,"订单类型不被支持!");
+
+
+                orderKey.setTransactionId(service.getTransactionId());
+
+
+                String userPhone = service.getUserPhone();
+                String orderGoodsInfo = service.getOrderGoodsInfo();
+
+
+                Payer payer = new Payer();
+                if(StringUtils.isNotBlank(fsUser.getMaOpenId())){
+                    payer.setOpenid(fsUser.getMaOpenId());
+                }
+                request.setPayer(payer);
+                request.setOrderKey(orderKey);
+
+                request.setLogisticsType(1);
+                request.setDeliveryMode(1);
+
+                request.setShippingList(Collections.singletonList(ShippingItem.builder()
+                        .itemDesc(orderGoodsInfo)
+                        .expressCompany(service.getExpressCompany())
+                        .trackingNo(service.getExpressNo())
+                        .contact(Contact.builder().consignorContact(userPhone).build())
+                        .build()));
+
+                OffsetDateTime now = OffsetDateTime.now();
+                DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+                String formattedTimestamp = now.format(formatter);
+                request.setUploadTime(formattedTimestamp);
+
+
+                request.setAppid(fsWxExpressTask.getAppid());
+                WeChatApiResponse response = shippingService.uploadShippingInfo(request);
+                if(ObjectUtil.equal(response.getErrcode(),0)){
+                    fsWxExpressTask.setStatus(2);
+                } else {
+                    fsWxExpressTask.setRetryCount(fsWxExpressTask.getRetryCount() +1);
+                    fsWxExpressTask.setStatus(3);
+                    fsWxExpressTask.setData(JSON.toJSONString(request));
+                    fsWxExpressTask.setRequestBody(JSON.toJSONString(request));
+                    fsWxExpressTask.setResponseBody(JSON.toJSONString(response));
+                }
+            }catch (Exception e){
+                logger.info("该单 {} 推送到物流失败!",fsWxExpressTask);
+                fsWxExpressTask.setRetryCount(fsWxExpressTask.getRetryCount() +1);
+                fsWxExpressTask.setStatus(3);
+
+
+            }
+        }
+        fsWxExpressTaskMapper.batchUpdate(fsWxExpressTasks);
+
+    }
     /**
      * 查询订单
      *
@@ -1286,6 +1372,20 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     .templateType(TemplateListenEnum.TYPE_2.getValue())
                     .build();
             publisher.publishEvent(new TemplateEvent(this, templateBean));
+
+            List<FsStorePaymentScrm> fsStorePayments = fsStorePaymentMapper.selectFsStorePaymentByOrderId(order.getId());
+            FsStorePaymentScrm fsStorePayment = fsStorePayments.get(0);
+            FsWxExpressTask fsWxExpressTask = new FsWxExpressTask();
+            fsWxExpressTask.setUserId(order.getUserId());
+            fsWxExpressTask.setStatus(0);
+            fsWxExpressTask.setRetryCount(0);
+            fsWxExpressTask.setCreateTime(LocalDateTime.now());
+            fsWxExpressTask.setUpdateTime(LocalDateTime.now());
+            fsWxExpressTask.setOrderCode(order.getOrderCode());
+            fsWxExpressTask.setExpressCompany(express.getCode());
+            fsWxExpressTask.setExpressNo(deliveryId);
+            fsWxExpressTask.setAppid(fsStorePayment.getAppId());
+            fsWxExpressTaskMapper.insert(fsWxExpressTask);
         }
     }
 
@@ -3971,6 +4071,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 storePayment.setUserId(user.getUserId());
                 storePayment.setBusinessOrderId(order.getId().toString());
                 storePayment.setOrderId(order.getId());
+                storePayment.setAppId(fsPayConfig.getAppId() == null ? "" : fsPayConfig.getAppId());
                 fsStorePaymentMapper.insertFsStorePayment(storePayment);
 
                 if (fsPayConfig.getType().equals("hf")){

+ 2 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -788,7 +788,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
             }
         }
         return R.ok();
-        }
+    }
 
 
     @Override
@@ -890,6 +890,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
         storePayment.setOpenId(user.getMaOpenId());
         storePayment.setUserId(user.getUserId());
         storePayment.setPayMode("hf");//目前微信收款仅支持汇付
+        storePayment.setAppId(param.getAppId());
         fsStorePaymentMapper.insertFsStorePayment(storePayment);
 
         //汇付支付

+ 1 - 1
fs-service/src/main/java/com/fs/live/domain/LiveAutoTask.java

@@ -29,7 +29,7 @@ public class LiveAutoTask extends BaseEntity{
     @Excel(name = "任务名称")
     private String taskName;
 
-    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 */
+    /** 任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动  4-抽奖 5-优惠券 6-自动上下架*/
     @Excel(name = "任务类型:1-定时推送卡片商品 2-定时发送红包 3-定时开启互动 4-抽奖")
     private Long taskType;
 

+ 9 - 3
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -133,10 +133,16 @@ public interface LiveMapper
     @Update("update live set global_visible = #{status} where live_id = #{liveId}")
     void updateGlobalVisible(@Param("liveId")Long liveId,@Param("status") Integer status);
 
-    @Select("select * from live where company_id = #{companyId} and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1")
+    @Select({"<script>" +
+            "select * from live where 1=1 " +
+            " <if test='companyId!=null' > and company_id = #{companyId} </if> and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1" +
+            " </script>"})
     List<Live> listLiveData(@Param("companyId")Long companyId);
 
-    @Select("select count(1) from live where company_id = #{companyId} and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1")
+    @Select({"<script>" +
+            "select count(1) from live where 1=1 " +
+            " <if test='companyId!=null' > and company_id = #{companyId} </if> and live_type IN (1,2, 3) AND status IN (3, 4) AND is_del = 0 and is_audit=1" +
+            " </script>"})
     int listLiveDataCount(@Param("companyId") Long companyId);
 
 
@@ -144,6 +150,6 @@ public interface LiveMapper
 
     List<Live> selectLiveShowReadyStartLiveList(@Param("companyIds") List<Long> companyIds);
 
-    @Select("select * from live where is_audit = 1 and id_del = 0 and status in (1,2,4) and live_type in (2,3) order by create_time desc")
+    @Select("select * from live where is_audit = 1 and is_del = 0 and status in (1,2,4) and live_type in (2,3) order by create_time desc")
     List<Live> liveListAll();
 }

+ 11 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveWatchUserMapper.java

@@ -107,6 +107,17 @@ public interface LiveWatchUserMapper {
     @Select("select * from live_watch_user where live_id = #{liveId} and user_id = #{userId}")
     LiveWatchUser selectUserByLiveIdAndUserId(@Param("liveId") long liveId,@Param("userId")  long userId);
 
+    /**
+     * 根据唯一索引查询:live_id, user_id, live_flag, replay_flag
+     */
+    LiveWatchUser selectByUniqueIndex(@Param("liveId") Long liveId, @Param("userId") Long userId, 
+                                      @Param("liveFlag") Integer liveFlag, @Param("replayFlag") Integer replayFlag);
+
+    /**
+     * 根据唯一索引更新或插入(ON DUPLICATE KEY UPDATE)
+     */
+    int insertOrUpdateByUniqueIndex(LiveWatchUser liveWatchUser);
+
     @Select("SELECT " +
             "    SUM(CASE WHEN lwu.online = 0 and lwu.msg_status = 0 THEN 1 ELSE 0 END) AS online, " +
             "    SUM(CASE WHEN lwu.online = 1 and lwu.msg_status = 0 THEN 1 ELSE 0 END) AS offline, " +

+ 12 - 11
fs-service/src/main/java/com/fs/live/param/LiveDataParam.java

@@ -1,6 +1,7 @@
 package com.fs.live.param;
 
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
 
@@ -19,31 +20,31 @@ public class LiveDataParam {
 
     private Long liveId;
 
-   /* *//** 直播名称 *//*
+
     @Excel(name = "直播名称")
     private String liveName;
 
-    *//** 直播封面 *//*
+
     @Excel(name = "直播封面")
     private String liveImgUrl;
 
-    *//** 1待直播 2直播中 3已结束 *//*
+
     @Excel(name = "1待直播 2直播中 3已结束")
     private Integer status;
 
-    *//** 开始时间 *//*
-    @JsonFormat(pattern = "yyyy-MM-dd")
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date startTime;
 
-    *//** 结束时间 *//*
-    @JsonFormat(pattern = "yyyy-MM-dd")
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date finishTime;
 
-    *//** 直播地址 *//*
+
     @Excel(name = "直播地址")
-    private String rtmpUrl;*/
+    private String rtmpUrl;
 
     /** 浏览量 */
     @Excel(name = "浏览量")
@@ -84,9 +85,9 @@ public class LiveDataParam {
 
     /** 观看时长 */
     private Integer watchDuration;
-    /** 开始时间 */
-    private Date startTime;
     /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date endTime;
 
 

+ 1 - 1
fs-service/src/main/java/com/fs/live/service/ILiveAutoTaskService.java

@@ -53,7 +53,7 @@ public interface ILiveAutoTaskService {
      * @param liveAutoTask 直播间自动化任务配置
      * @return 结果
      */
-    int updateLiveAutoTask(LiveAutoTask liveAutoTask);
+    R updateLiveAutoTask(LiveAutoTask liveAutoTask);
 
     /**
      * 批量删除直播间自动化任务配置

+ 26 - 21
fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java

@@ -22,6 +22,8 @@ import com.fs.live.service.ILiveGoodsService;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.live.vo.LiveLotteryProductListVo;
 import org.checkerframework.checker.units.qual.A;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -34,6 +36,7 @@ import org.springframework.stereotype.Service;
 @Service
 public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
 
+    private static final Logger log = LoggerFactory.getLogger(LiveAutoTaskServiceImpl.class);
     @Autowired
     private LiveMapper liveMapper;
     @Autowired
@@ -249,59 +252,59 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
      * @return 结果
      */
     @Override
-    public int updateLiveAutoTask(LiveAutoTask liveAutoTask)
+    public R updateLiveAutoTask(LiveAutoTask liveAutoTask)
     {
         LiveAutoTask existTask = baseMapper.selectLiveAutoTaskById(liveAutoTask.getId());
         redisCache.redisTemplate.opsForZSet().removeRangeByScore("live:auto_task:" + existTask.getLiveId(), existTask.getAbsValue().getTime(), existTask.getAbsValue().getTime());
         if (liveAutoTask.getTaskType() == 1L) {
             // 商品
             LiveGoodsVo liveGoodsVo = goodsService.selectLiveGoodsVoByGoodsId(Long.valueOf(liveAutoTask.getContent()));
-            if(liveGoodsVo == null) return -1;
+            if(liveGoodsVo == null) return R.error("商品不存在");
             liveAutoTask.setContent(JSON.toJSONString(liveGoodsVo));
-            return baseMapper.updateLiveAutoTask(liveAutoTask);
+             baseMapper.updateLiveAutoTask(liveAutoTask);
         }else if (liveAutoTask.getTaskType() == 2L) {
             // 红包
             LiveRedConf liveRedConf = liveRedConfMapper.selectLiveRedConfByRedId(Long.valueOf(liveAutoTask.getContent()));
-            if(liveRedConf == null) return -1;
-            if(liveRedConf.getRedStatus() != 0L) return -1;
+            if(liveRedConf == null) return R.error("红包不存在!");
+            if(liveRedConf.getRedStatus() != 0L) return R.error("红包已结束!");
             liveAutoTask.setContent(JSON.toJSONString(liveRedConf));
-            return baseMapper.updateLiveAutoTask(liveAutoTask);
+            baseMapper.updateLiveAutoTask(liveAutoTask);
         }else if (liveAutoTask.getTaskType() == 4L) {
             // 开启抽奖
             LiveLotteryConf liveLotteryConf = liveLotteryConfMapper.selectLiveLotteryConfByLotteryId(Long.valueOf(liveAutoTask.getContent()));
-            if(liveLotteryConf == null) return -1;
-            if(!"0".equals(liveLotteryConf.getLotteryStatus())) return -1;
+            if(liveLotteryConf == null) return R.error("抽奖不存在!");
+            if(!"0".equals(liveLotteryConf.getLotteryStatus())) return R.error("抽奖未开始!");
             List<LiveLotteryProduct> prizes = lotteryProductConfMapper.selectLiveLotteryProductConfByLotteryId(liveLotteryConf.getLotteryId());
             if (prizes == null || prizes.isEmpty()) {
-                return -1;
+                return R.error("请先添加奖品");
             }
             liveAutoTask.setContent(JSON.toJSONString(liveLotteryConf));
-            return baseMapper.updateLiveAutoTask(liveAutoTask);
+            baseMapper.updateLiveAutoTask(liveAutoTask);
         } else if(liveAutoTask.getTaskType() == 3L){
-            return baseMapper.updateLiveAutoTask(liveAutoTask);
+            baseMapper.updateLiveAutoTask(liveAutoTask);
         } else if( liveAutoTask.getTaskType() == 5L){
             // 自动优惠券
             LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(Long.valueOf(liveAutoTask.getContent()));
-            if(liveCoupon == null) return -1;
+            if(liveCoupon == null) return R.error("优惠券不存在!");
             LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
-            if(liveCouponIssue == null)return -1;
+            if(liveCouponIssue == null)return R.error("未发布优惠券!");
             LiveCouponIssueRelation liveCouponIssueRelation = liveCouponMapper.selectCouponRelation(liveAutoTask.getLiveId(),liveCouponIssue.getId());
-            if(liveCouponIssueRelation == null) return -1;
-            if(ObjectUtil.isEmpty(liveCouponIssueRelation.getGoodsId())) return -1;
+            if(liveCouponIssueRelation == null) return R.error("未绑定商品,无法发布!");
+            if(ObjectUtil.isEmpty(liveCouponIssueRelation.getGoodsId())) return R.error("未绑定商品,无法发布!");
             liveCoupon.setGoodsId(liveCouponIssueRelation.getGoodsId());
             liveAutoTask.setContent(JSON.toJSONString(liveCoupon));
-            return baseMapper.updateLiveAutoTask(liveAutoTask);
+            baseMapper.updateLiveAutoTask(liveAutoTask);
         } else if (liveAutoTask.getTaskType() == 6L) {
             // 上架/下架商品
             try {
                 com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(liveAutoTask.getContent());
                 Long goodsId = jsonObject.getLong("goodsId");
                 Integer status = jsonObject.getInteger("status");
-                if (goodsId == null || status == null) return -1;
+                if (goodsId == null || status == null) return R.error("商品ID或状态为空");
 
                 // 查询商品信息
                 LiveGoodsVo liveGoodsVo = goodsService.selectLiveGoodsVoByGoodsId(goodsId);
-                if(liveGoodsVo == null) return -1;
+                if(liveGoodsVo == null) return R.error("商品不存在");
 
                 // 保存商品信息和上下架状态
                 com.alibaba.fastjson.JSONObject content = new com.alibaba.fastjson.JSONObject();
@@ -310,13 +313,15 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
                 content.put("productName", liveGoodsVo.getProductName());
                 content.put("status", status);
                 liveAutoTask.setContent(content.toJSONString());
-                return baseMapper.updateLiveAutoTask(liveAutoTask);
+                 baseMapper.updateLiveAutoTask(liveAutoTask);
             } catch (Exception e) {
-                return -1;
+                log.error("上架/下架商品自动化任务,更新异常!" + e.getMessage());
+                return R.error("上架/下架商品自动化任务,更新异常!");
             }
         } else {
-            return -1;
+            return R.error("任务类型错误");
         }
+        return R.ok("更新成功");
     }
 
     /**

+ 22 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -94,6 +94,8 @@ import com.fs.live.service.*;
 import com.fs.live.vo.*;
 import com.fs.store.domain.*;
 import com.fs.system.service.ISysConfigService;
+import com.fs.wx.order.domain.FsWxExpressTask;
+import com.fs.wx.order.mapper.FsWxExpressTaskMapper;
 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
 import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
@@ -270,6 +272,9 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
     @Autowired
     private LiveAfterSalesMapper liveAfterSalesMapper;
 
+    @Autowired
+    private FsWxExpressTaskMapper fsWxExpressTaskMapper;
+
 
     //ERP 类型到服务的映射
     private Map<Integer, IErpOrderService> erpServiceMap;
@@ -1706,6 +1711,20 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                     .templateType(TemplateListenEnum.TYPE_2.getValue())
                     .build();
             publisher.publishEvent(new TemplateEvent(this, templateBean));
+
+            LiveOrderPayment fsStorePayment  = liveOrderPaymentMapper.selectLiveOrderLatestPayByOrderId(order.getOrderId());
+            FsWxExpressTask fsWxExpressTask = new FsWxExpressTask();
+            fsWxExpressTask.setUserId(Long.valueOf(order.getUserId()));
+            fsWxExpressTask.setStatus(0);
+            fsWxExpressTask.setRetryCount(0);
+            fsWxExpressTask.setType(1);
+            fsWxExpressTask.setCreateTime(LocalDateTime.now());
+            fsWxExpressTask.setUpdateTime(LocalDateTime.now());
+            fsWxExpressTask.setOrderCode(order.getOrderCode());
+            fsWxExpressTask.setExpressCompany(express.getCode());
+            fsWxExpressTask.setExpressNo(deliveryId);
+            fsWxExpressTask.setAppid(fsStorePayment.getAppId());
+            fsWxExpressTaskMapper.insert(fsWxExpressTask);
         }
     }
 
@@ -2835,6 +2854,7 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
                 storePayment.setOpenId(user.getRealName());
                 storePayment.setUserId(user.getUserId());
                 storePayment.setBusinessId(String.valueOf(order.getOrderId()));
+                storePayment.setAppId(fsPayConfig.getAppId() == null ? "" : fsPayConfig.getAppId());
                 liveOrderPaymentMapper.insertLiveOrderPayment(storePayment);
 
                 if (fsPayConfig.getType().equals("hf")){
@@ -3194,6 +3214,8 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
         if(goods == null) return R.error("当前商品不存在");
         if(fsStoreProduct == null) return R.error("店铺已下架商品,购买失败");
         if(fsStoreProduct.getIsShow() == 0 || goods.getStatus() == 0) return R.error("商品已下架,购买失败");
+        if(liveOrder.getTotalNum() == null || StringUtils.isEmpty(liveOrder.getTotalNum())) liveOrder.setTotalNum("1");
+        if(goods.getStock() == null) return R.error("直播间商品库存不足");
         if(fsStoreProduct.getStock() < Integer.parseInt(liveOrder.getTotalNum()) || goods.getStock() < Integer.parseInt(liveOrder.getTotalNum())) return R.error("抱歉,这款商品已被抢光,暂时无库存~");
 
         // 更改店铺库存

+ 293 - 150
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -726,7 +726,7 @@ public class LiveServiceImpl implements ILiveService
         if (deleteCount > 0) {
             return R.ok("操作成功");
         }
-        return R.error();
+        return R.error("删除失败");
     }
 
     @Override
@@ -807,24 +807,71 @@ public class LiveServiceImpl implements ILiveService
         if (exist == null) {
             return R.error("直播间不存在");
         }
-        // 复制直播间, 创建 livedata
-        // 身份认证
+        
+        // 1. 复制直播间基础信息
+        Long newLiveId = copyLiveBasicInfo(exist, live, now);
+        
+        // 2. 创建直播数据
+        createLiveData(newLiveId);
+        
+        // 3. 复制直播视频
+        copyLiveVideos(existLiveId, newLiveId, now);
+        
+        // 4. 获取所有自动化任务并按类型分组
+        List<LiveAutoTask> allTasks = liveAutoTaskService.selectLiveAutoTaskByLiveId(existLiveId);
+        Map<Long, List<LiveAutoTask>> tasksByType = allTasks.stream()
+                .collect(Collectors.groupingBy(LiveAutoTask::getTaskType));
+        
+        // 5. 复制弹幕任务(taskType=3)
+        copyBarrageTasks(tasksByType.get(3L), newLiveId, now);
+        
+        // 6. 复制红包配置及任务(taskType=2)
+        Map<Long, Long> redIdMapping = copyRedConfs(existLiveId, newLiveId, tasksByType.get(2L), now);
+        
+        // 7. 复制抽奖配置及任务(taskType=4)
+        Map<Long, Long> lotteryIdMapping = copyLotteryConfs(existLiveId, newLiveId, tasksByType.get(4L), now);
+        
+        // 8. 复制商品、商品任务、上下架任务及优惠券关系
+        Map<Long, Long> goodsIdMapping = copyGoodsAndTasks(existLiveId, newLiveId, live, 
+                tasksByType.get(1L), tasksByType.get(6L), now);
+        
+        // 9. 复制优惠券关系
+        copyCouponRelations(existLiveId, newLiveId, goodsIdMapping);
+        
+        // 10. 复制优惠券自动化任务(taskType=5)
+        copyCouponTasks(tasksByType.get(5L), newLiveId, goodsIdMapping, now);
+
+        return R.ok("复制成功");
+    }
+    
+    /**
+     * 复制直播间基础信息
+     */
+    private Long copyLiveBasicInfo(Live exist, Live live, Date now) {
         Live liveEntity = new Live();
         BeanUtils.copyBeanProp(liveEntity, exist);
         liveEntity.setLiveId(null);
         liveEntity.setCompanyId(live.getCompanyId());
         liveEntity.setCompanyUserId(live.getCompanyUserId());
-        liveEntity.setRtmpUrl( null);
-        liveEntity.setFlvHlsUrl( null);
-        liveEntity.setFinishTime( null);
-        liveEntity.setIsAudit( 0);
-        liveEntity.setStatus( 1);
-        liveEntity.setCreateTime( now);
+        liveEntity.setRtmpUrl(null);
+        liveEntity.setFlvHlsUrl(null);
+        liveEntity.setFinishTime(null);
+        liveEntity.setIsAudit(0);
+        liveEntity.setStatus(1);
+        liveEntity.setCreateTime(now);
         baseMapper.insertLive(liveEntity);
-        if(liveEntity.getLiveId() == null) throw new RuntimeException("插入直播间异常");
-        Long newLiveId = liveEntity.getLiveId();
+        if (liveEntity.getLiveId() == null) {
+            throw new RuntimeException("插入直播间异常");
+        }
+        return liveEntity.getLiveId();
+    }
+    
+    /**
+     * 创建直播数据
+     */
+    private void createLiveData(Long liveId) {
         LiveData liveData = new LiveData();
-        liveData.setLiveId(newLiveId);
+        liveData.setLiveId(liveId);
         liveData.setPageViews(0L);
         liveData.setUniqueVisitors(0L);
         liveData.setTotalViews(0L);
@@ -834,172 +881,268 @@ public class LiveServiceImpl implements ILiveService
         liveData.setFavouriteNum(0L);
         liveData.setFollowNum(0L);
         liveDataService.insertLiveData(liveData);
-        // 优惠券
-        List<LiveCouponIssueRelation> liveCouponIssueRelations = liveCouponIssueMapper.selectRelationByLiveId(existLiveId);
-        Map<Long, LiveCouponIssueRelation> collect = liveCouponIssueRelations.stream().collect(Collectors.toMap(LiveCouponIssueRelation::getGoodsId, Function.identity(), (existing, replacement) -> existing));
-        // 直播视频
+    }
+    
+    /**
+     * 复制直播视频
+     */
+    private void copyLiveVideos(Long existLiveId, Long newLiveId, Date now) {
         List<LiveVideo> liveVideos = liveVideoService.selectLiveVideosByLiveId(existLiveId);
-        if (!liveVideos.isEmpty()) {
-            for (LiveVideo liveVideo : liveVideos) {
-                LiveVideo videoEntity = new LiveVideo();
-                BeanUtils.copyBeanProp(videoEntity, liveVideo);
-                videoEntity.setVideoId(null);
-                videoEntity.setLiveId(newLiveId);
-                videoEntity.setCreateTime(now);
-                liveVideoService.insertLiveVideo(videoEntity);
-            }
+        for (LiveVideo liveVideo : liveVideos) {
+            LiveVideo videoEntity = new LiveVideo();
+            BeanUtils.copyBeanProp(videoEntity, liveVideo);
+            videoEntity.setVideoId(null);
+            videoEntity.setLiveId(newLiveId);
+            videoEntity.setCreateTime(now);
+            liveVideoService.insertLiveVideo(videoEntity);
+        }
+    }
+    
+    /**
+     * 复制弹幕任务(taskType=3)
+     */
+    private void copyBarrageTasks(List<LiveAutoTask> tasks, Long newLiveId, Date now) {
+        if (tasks == null || tasks.isEmpty()) {
+            return;
         }
-
-        // 运营自动化
-        List<LiveAutoTask> liveAutoTasksList = liveAutoTaskService.selectLiveAutoTaskByLiveId(existLiveId);
-        List<LiveAutoTask> barrageTask = liveAutoTasksList.stream().filter(liveAutoTask -> liveAutoTask.getTaskType() == 3L).collect(Collectors.toList());
-        List<LiveAutoTask> goodsTaskList = liveAutoTasksList.stream().filter(liveAutoTask -> liveAutoTask.getTaskType() == 1L).collect(Collectors.toList());
-        List<LiveAutoTask> redTaskList = liveAutoTasksList.stream().filter(liveAutoTask -> liveAutoTask.getTaskType() == 2L).collect(Collectors.toList());
-        List<LiveAutoTask> lotteryTaskList = liveAutoTasksList.stream().filter(liveAutoTask -> liveAutoTask.getTaskType() == 4L).collect(Collectors.toList());
-        List<LiveAutoTask> shelfTaskList = liveAutoTasksList.stream().filter(liveAutoTask -> liveAutoTask.getTaskType() == 6L).collect(Collectors.toList());
         List<LiveAutoTask> addList = new ArrayList<>();
-        if (!barrageTask.isEmpty()) {
-            for (LiveAutoTask liveAutoTask : barrageTask) {
-                LiveAutoTask liveAutoTaskEntity = new LiveAutoTask();
-                BeanUtils.copyBeanProp(liveAutoTaskEntity, liveAutoTask);
-                liveAutoTaskEntity.setId(null);
-                liveAutoTaskEntity.setLiveId(newLiveId);
-                liveAutoTaskEntity.setCreateTime(now);
-                liveAutoTaskEntity.setUpdateTime(now);
-                liveAutoTaskEntity.setFinishStatus(0L);
-                addList.add(liveAutoTaskEntity);
-                if (addList.size() > 100) {
-                    liveAutoTaskService.batchInsertLiveAutoTask(addList);
-                    addList.clear();
-                }
+        for (LiveAutoTask task : tasks) {
+            LiveAutoTask newTask = createAutoTaskEntity(task, newLiveId, now, null);
+            addList.add(newTask);
+            if (addList.size() >= 100) {
+                liveAutoTaskService.batchInsertLiveAutoTask(addList);
+                addList.clear();
             }
         }
         if (!addList.isEmpty()) {
             liveAutoTaskService.batchInsertLiveAutoTask(addList);
-            addList.clear();
         }
-        //直播间红包配置
+    }
+    
+    /**
+     * 复制红包配置及任务
+     */
+    private Map<Long, Long> copyRedConfs(Long existLiveId, Long newLiveId, 
+                                         List<LiveAutoTask> redTasks, Date now) {
+        Map<Long, Long> redIdMapping = new HashMap<>();
+        if (redTasks == null) {
+            redTasks = Collections.emptyList();
+        }
+        Map<Long, LiveAutoTask> redTaskMap = redTasks.stream()
+                .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "redId"), 
+                        Function.identity(), (existing, replacement) -> existing));
+        
         List<LiveRedConf> liveRedConfs = liveRedConfService.selectByLiveId(existLiveId);
-        if (!liveRedConfs.isEmpty()) {
-
-            for (LiveRedConf liveRedConf : liveRedConfs) {
-                LiveRedConf liveRedConfEntity = new LiveRedConf();
-                BeanUtils.copyBeanProp(liveRedConfEntity, liveRedConf);
-                liveRedConfEntity.setRedId(null);
-                liveRedConfEntity.setLiveId(newLiveId);
-                liveRedConfEntity.setRedStatus(0L);
-                liveRedConfEntity.setCreateTime(now);
-                liveRedConfEntity.setTotalSend(0L);
-                liveRedConfService.insertLiveRedConf(liveRedConfEntity);
-                LiveAutoTask liveAutoTask = redTaskList.stream().filter(item -> parseIdFromContent(item.getContent(), "redId").equals(liveRedConf.getRedId())).findFirst().orElse(null);
-                if(liveAutoTask == null) continue;
-                LiveAutoTask liveAutoTaskEntity = new LiveAutoTask();
-                BeanUtils.copyBeanProp(liveAutoTaskEntity, liveAutoTask);
-                liveAutoTaskEntity.setId(null);
-                liveAutoTaskEntity.setLiveId(newLiveId);
-                liveAutoTaskEntity.setCreateTime(now);
-                liveAutoTaskEntity.setUpdateTime(now);
-                liveAutoTaskEntity.setFinishStatus(0L);
-                liveAutoTaskEntity.setContent(JSON.toJSONString(liveRedConfEntity));
-                liveAutoTaskService.directInsertLiveAutoTask(liveAutoTaskEntity);
+        for (LiveRedConf liveRedConf : liveRedConfs) {
+            LiveRedConf newRedConf = new LiveRedConf();
+            BeanUtils.copyBeanProp(newRedConf, liveRedConf);
+            newRedConf.setRedId(null);
+            newRedConf.setLiveId(newLiveId);
+            newRedConf.setRedStatus(0L);
+            newRedConf.setCreateTime(now);
+            newRedConf.setTotalSend(0L);
+            liveRedConfService.insertLiveRedConf(newRedConf);
+            
+            redIdMapping.put(liveRedConf.getRedId(), newRedConf.getRedId());
+            
+            LiveAutoTask task = redTaskMap.get(liveRedConf.getRedId());
+            if (task != null) {
+                LiveAutoTask newTask = createAutoTaskEntity(task, newLiveId, now, 
+                        JSON.toJSONString(newRedConf));
+                liveAutoTaskService.directInsertLiveAutoTask(newTask);
             }
         }
-        // 直播间礼物配置
+        return redIdMapping;
+    }
+    
+    /**
+     * 复制抽奖配置及任务
+     */
+    private Map<Long, Long> copyLotteryConfs(Long existLiveId, Long newLiveId, 
+                                             List<LiveAutoTask> lotteryTasks, Date now) {
+        Map<Long, Long> lotteryIdMapping = new HashMap<>();
+        if (lotteryTasks == null) {
+            lotteryTasks = Collections.emptyList();
+        }
+        Map<Long, LiveAutoTask> lotteryTaskMap = lotteryTasks.stream()
+                .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "lotteryId"), 
+                        Function.identity(), (existing, replacement) -> existing));
+        
         List<LiveLotteryConf> liveLotteryConfs = liveLotteryConfService.selectByLiveId(existLiveId);
         if (!liveLotteryConfs.isEmpty()) {
-
-            List<Long> lotteryIds = liveLotteryConfs.stream().map(LiveLotteryConf::getLotteryId).collect(Collectors.toList());
+            List<Long> lotteryIds = liveLotteryConfs.stream()
+                    .map(LiveLotteryConf::getLotteryId).collect(Collectors.toList());
             List<LiveLotteryProductConf> products = liveLotteryProductConfMapper.selectEntityByIds(lotteryIds);
+            Map<Long, List<LiveLotteryProductConf>> productsByLotteryId = products.stream()
+                    .collect(Collectors.groupingBy(LiveLotteryProductConf::getLotteryId));
+            
             for (LiveLotteryConf liveLotteryConf : liveLotteryConfs) {
-                LiveLotteryConf liveLotteryConfEntity = new LiveLotteryConf();
-                BeanUtils.copyBeanProp(liveLotteryConfEntity, liveLotteryConf);
-                liveLotteryConfEntity.setLotteryId(null);
-                liveLotteryConfEntity.setLiveId(newLiveId);
-                liveLotteryConfEntity.setLotteryStatus(String.valueOf(0));
-                liveLotteryConfEntity.setCreateTime(now);
-                liveLotteryConfEntity.setUpdateTime(now);
-                liveLotteryConfService.insertLiveLotteryConf(liveLotteryConfEntity);
-                products.stream().filter(product -> product.getLotteryId().equals(liveLotteryConf.getLotteryId()))
-                        .forEach(product -> {
-                            product.setId(null);
-                            product.setLotteryId(liveLotteryConfEntity.getLotteryId());
-                            liveLotteryProductConfMapper.insertLiveLotteryProductConf(product);
-                        });
-                LiveAutoTask liveAutoTask = lotteryTaskList.stream().filter(item -> parseIdFromContent(item.getContent(), "lotteryId").equals(liveLotteryConf.getLotteryId())).findFirst().orElse(null);
-                if(liveAutoTask == null) continue;
-                LiveAutoTask liveAutoTaskEntity = new LiveAutoTask();
-                BeanUtils.copyBeanProp(liveAutoTaskEntity, liveAutoTask);
-                liveAutoTaskEntity.setId(null);
-                liveAutoTaskEntity.setLiveId(newLiveId);
-                liveAutoTaskEntity.setCreateTime(now);
-                liveAutoTaskEntity.setUpdateTime(now);
-                liveAutoTaskEntity.setFinishStatus(0L);
-                liveAutoTaskEntity.setContent(JSON.toJSONString(liveLotteryConfEntity));
-                liveAutoTaskService.directInsertLiveAutoTask(liveAutoTaskEntity);
+                LiveLotteryConf newLotteryConf = new LiveLotteryConf();
+                BeanUtils.copyBeanProp(newLotteryConf, liveLotteryConf);
+                newLotteryConf.setLotteryId(null);
+                newLotteryConf.setLiveId(newLiveId);
+                newLotteryConf.setLotteryStatus(String.valueOf(0));
+                newLotteryConf.setCreateTime(now);
+                newLotteryConf.setUpdateTime(now);
+                liveLotteryConfService.insertLiveLotteryConf(newLotteryConf);
+                
+                lotteryIdMapping.put(liveLotteryConf.getLotteryId(), newLotteryConf.getLotteryId());
+                
+                // 复制奖品
+                List<LiveLotteryProductConf> lotteryProducts = productsByLotteryId.get(liveLotteryConf.getLotteryId());
+                if (lotteryProducts != null) {
+                    for (LiveLotteryProductConf product : lotteryProducts) {
+                        LiveLotteryProductConf newProduct = new LiveLotteryProductConf();
+                        BeanUtils.copyBeanProp(newProduct, product);
+                        newProduct.setId(null);
+                        newProduct.setLotteryId(newLotteryConf.getLotteryId());
+                        liveLotteryProductConfMapper.insertLiveLotteryProductConf(newProduct);
+                    }
+                }
+                
+                LiveAutoTask task = lotteryTaskMap.get(liveLotteryConf.getLotteryId());
+                if (task != null) {
+                    LiveAutoTask newTask = createAutoTaskEntity(task, newLiveId, now, 
+                            JSON.toJSONString(newLotteryConf));
+                    liveAutoTaskService.directInsertLiveAutoTask(newTask);
+                }
             }
         }
+        return lotteryIdMapping;
+    }
+    
+    /**
+     * 复制商品、商品任务、上下架任务
+     */
+    private Map<Long, Long> copyGoodsAndTasks(Long existLiveId, Long newLiveId, Live live,
+                                              List<LiveAutoTask> goodsTasks, List<LiveAutoTask> shelfTasks, Date now) {
+        Map<Long, Long> goodsIdMapping = new HashMap<>();
+        if (goodsTasks == null) {
+            goodsTasks = Collections.emptyList();
+        }
+        if (shelfTasks == null) {
+            shelfTasks = Collections.emptyList();
+        }
+        
+        Map<Long, LiveAutoTask> goodsTaskMap = goodsTasks.stream()
+                .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "goodsId"), 
+                        Function.identity(), (existing, replacement) -> existing));
+        Map<Long, LiveAutoTask> shelfTaskMap = shelfTasks.stream()
+                .collect(Collectors.toMap(task -> parseIdFromContent(task.getContent(), "goodsId"), 
+                        Function.identity(), (existing, replacement) -> existing));
+        
         LiveGoods queryParam = new LiveGoods();
         queryParam.setLiveId(existLiveId);
-        // 直播间商品
         List<LiveGoodsVo> goodsList = liveGoodsService.selectProductListByLiveId(queryParam);
+        
         if (!goodsList.isEmpty()) {
-
-            List<Long> goodsProductIds = goodsList.stream().map(item -> item.getProductId()).collect(Collectors.toList());
-            Map<Long, FsStoreProductScrm> goodsMap = fsStoreProductScrmMapper.selectFsStoreProductByProductIds(goodsProductIds).stream().collect(Collectors.toMap(FsStoreProductScrm::getProductId, item -> item));
-
+            List<Long> goodsProductIds = goodsList.stream()
+                    .map(LiveGoodsVo::getProductId).collect(Collectors.toList());
+            Map<Long, FsStoreProductScrm> goodsMap = fsStoreProductScrmMapper
+                    .selectFsStoreProductByProductIds(goodsProductIds).stream()
+                    .collect(Collectors.toMap(FsStoreProductScrm::getProductId, Function.identity()));
+            
             for (LiveGoodsVo liveGoods : goodsList) {
-                LiveGoods liveGoodsEntity = new LiveGoods();
-                BeanUtils.copyBeanProp(liveGoodsEntity, liveGoods);
-                liveGoodsEntity.setGoodsId(null);
-                liveGoodsEntity.setLiveId(newLiveId);
-                liveGoodsEntity.setCreateTime(now);
-                liveGoodsEntity.setIsShow(false);
-                liveGoodsEntity.setCompanyId(live.getCompanyId());
-                liveGoodsEntity.setCompanyUserId(live.getCompanyUserId());
-                liveGoodsService.insertLiveGoods(liveGoodsEntity);
-                // 优惠券
-                if (collect.containsKey(liveGoods.getGoodsId())) {
-                    liveCouponIssueRelations.stream().filter(relation -> relation.getGoodsId().equals(liveGoods.getGoodsId())).findFirst().ifPresent(liveCouponIssueRelation -> liveCouponIssueRelation.setGoodsId(liveGoodsEntity.getGoodsId()));
+                LiveGoods newGoods = new LiveGoods();
+                BeanUtils.copyBeanProp(newGoods, liveGoods);
+                newGoods.setGoodsId(null);
+                newGoods.setLiveId(newLiveId);
+                newGoods.setCreateTime(now);
+                newGoods.setIsShow(false);
+                newGoods.setCompanyId(live.getCompanyId());
+                newGoods.setCompanyUserId(live.getCompanyUserId());
+                newGoods.setStock(goodsMap.containsKey(liveGoods.getProductId()) 
+                        ? goodsMap.get(liveGoods.getProductId()).getStock() : 0);
+                liveGoodsService.insertLiveGoods(newGoods);
+                
+                goodsIdMapping.put(liveGoods.getGoodsId(), newGoods.getGoodsId());
+                
+                // 复制商品推送任务(taskType=1)
+                LiveAutoTask goodsTask = goodsTaskMap.get(liveGoods.getGoodsId());
+                if (goodsTask != null) {
+                    // 使用新的goodsId创建LiveGoodsVo对象
+                    LiveGoodsVo newGoodsVo = new LiveGoodsVo();
+                    BeanUtils.copyBeanProp(newGoodsVo, liveGoods);
+                    newGoodsVo.setGoodsId(newGoods.getGoodsId());
+                    LiveAutoTask newTask = createAutoTaskEntity(goodsTask, newLiveId, now, 
+                            JSON.toJSONString(newGoodsVo));
+                    liveAutoTaskService.directInsertLiveAutoTask(newTask);
                 }
-                LiveAutoTask liveAutoTask = goodsTaskList.stream().filter(item -> parseIdFromContent(item.getContent(), "goodsId").equals(liveGoods.getGoodsId())).findFirst().orElse(null);
-                if(liveAutoTask == null) continue;
-                liveGoods.setGoodsId(liveGoodsEntity.getGoodsId());
-                LiveAutoTask liveAutoTaskEntity = new LiveAutoTask();
-                BeanUtils.copyBeanProp(liveAutoTaskEntity, liveAutoTask);
-                liveAutoTaskEntity.setId(null);
-                liveAutoTaskEntity.setLiveId(newLiveId);
-                liveGoodsEntity.setStock(goodsMap.containsKey(liveGoods.getProductId()) ? goodsMap.get(liveGoods.getProductId()).getStock() : 0);
-                liveAutoTaskEntity.setCreateTime(now);
-                liveAutoTaskEntity.setUpdateTime(now);
-                liveAutoTaskEntity.setFinishStatus(0L);
-                liveAutoTaskEntity.setContent(JSON.toJSONString(liveGoods));
-                liveAutoTaskService.directInsertLiveAutoTask(liveAutoTaskEntity);
-
-                // 处理上下架任务 taskType=6
-                LiveAutoTask shelfTask = shelfTaskList.stream().filter(item -> parseIdFromContent(item.getContent(), "goodsId").equals(liveGoods.getGoodsId())).findFirst().orElse(null);
-                if(shelfTask != null) {
-                    LiveAutoTask shelfAutoTaskEntity = new LiveAutoTask();
-                    BeanUtils.copyBeanProp(shelfAutoTaskEntity, shelfTask);
-                    shelfAutoTaskEntity.setId(null);
-                    shelfAutoTaskEntity.setLiveId(newLiveId);
-                    shelfAutoTaskEntity.setCreateTime(now);
-                    shelfAutoTaskEntity.setUpdateTime(now);
-                    shelfAutoTaskEntity.setFinishStatus(0L);
-                    // 更新content中的goodsId为新的goodsId
+                
+                // 复制上下架任务(taskType=6)
+                LiveAutoTask shelfTask = shelfTaskMap.get(liveGoods.getGoodsId());
+                if (shelfTask != null) {
                     JSONObject contentJson = JSON.parseObject(shelfTask.getContent());
-                    contentJson.put("goodsId", liveGoodsEntity.getGoodsId());
-                    shelfAutoTaskEntity.setContent(contentJson.toJSONString());
-                    liveAutoTaskService.directInsertLiveAutoTask(shelfAutoTaskEntity);
+                    contentJson.put("goodsId", newGoods.getGoodsId());
+                    LiveAutoTask newTask = createAutoTaskEntity(shelfTask, newLiveId, now, 
+                            contentJson.toJSONString());
+                    liveAutoTaskService.directInsertLiveAutoTask(newTask);
+                }
+            }
+        }
+        return goodsIdMapping;
+    }
+    
+    /**
+     * 复制优惠券关系
+     */
+    private void copyCouponRelations(Long existLiveId, Long newLiveId, Map<Long, Long> goodsIdMapping) {
+        List<LiveCouponIssueRelation> relations = liveCouponIssueMapper.selectRelationByLiveId(existLiveId);
+        for (LiveCouponIssueRelation relation : relations) {
+            LiveCouponIssueRelation newRelation = new LiveCouponIssueRelation();
+            BeanUtils.copyBeanProp(newRelation, relation);
+            newRelation.setLiveId(newLiveId);
+            newRelation.setIsShow(0);
+            // 更新goodsId映射
+            if (relation.getGoodsId() != null && goodsIdMapping.containsKey(relation.getGoodsId())) {
+                newRelation.setGoodsId(goodsIdMapping.get(relation.getGoodsId()));
+            }
+            liveCouponIssueMapper.insertLiveCouponIssueRelation(newRelation);
+        }
+    }
+    
+    /**
+     * 复制优惠券自动化任务(taskType=5)
+     */
+    private void copyCouponTasks(List<LiveAutoTask> couponTasks, Long newLiveId, 
+                                 Map<Long, Long> goodsIdMapping, Date now) {
+        if (couponTasks == null || couponTasks.isEmpty()) {
+            return;
+        }
+        for (LiveAutoTask task : couponTasks) {
+            try {
+                LiveCoupon liveCoupon = JSON.parseObject(task.getContent(), LiveCoupon.class);
+                if (liveCoupon != null && liveCoupon.getGoodsId() != null) {
+                    // 更新content中的goodsId为新的goodsId
+                    Long newGoodsId = goodsIdMapping.get(liveCoupon.getGoodsId());
+                    if (newGoodsId != null) {
+                        liveCoupon.setGoodsId(newGoodsId);
+                        LiveAutoTask newTask = createAutoTaskEntity(task, newLiveId, now, 
+                                JSON.toJSONString(liveCoupon));
+                        liveAutoTaskService.directInsertLiveAutoTask(newTask);
+                    }
                 }
+            } catch (Exception e) {
+                log.error("复制优惠券自动化任务失败,taskId: {}", task.getId(), e);
             }
         }
-        for (LiveCouponIssueRelation liveCouponIssueRelation : liveCouponIssueRelations) {
-            liveCouponIssueRelation.setLiveId(newLiveId);
-            liveCouponIssueRelation.setIsShow(0);
-            liveCouponIssueMapper.insertLiveCouponIssueRelation(liveCouponIssueRelation);
+    }
+    
+    /**
+     * 创建自动化任务实体
+     */
+    private LiveAutoTask createAutoTaskEntity(LiveAutoTask source, Long newLiveId, Date now, String content) {
+        LiveAutoTask newTask = new LiveAutoTask();
+        BeanUtils.copyBeanProp(newTask, source);
+        newTask.setId(null);
+        newTask.setLiveId(newLiveId);
+        newTask.setCreateTime(now);
+        newTask.setUpdateTime(now);
+        newTask.setFinishStatus(0L);
+        if (content != null) {
+            newTask.setContent(content);
         }
-
-        return R.ok("复制成功");
+        return newTask;
     }
 
     private Long parseIdFromContent(String content, String key) {

+ 50 - 33
fs-service/src/main/java/com/fs/live/service/impl/LiveWatchUserServiceImpl.java

@@ -12,6 +12,8 @@ import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.live.domain.Live;
 import com.fs.live.domain.LiveVideo;
 import com.fs.live.domain.LiveWatchUser;
@@ -43,7 +45,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     @Autowired
     private RedisCache redisCache;
     @Autowired
-    private IFsUserService fsUserService;
+    private IFsUserScrmService fsUserService;
     @Autowired
     private LiveWatchUserMapper baseMapper;
     @Autowired
@@ -153,38 +155,60 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
     @Override
     public LiveWatchUser join(long liveId, long userId, String location) {
-        LiveWatchUser liveWatchUser = getByLiveIdAndUserId(liveId, userId);
-        FsUser fsUserVO = fsUserService.selectFsUserByUserId(userId);
+        FsUserScrm fsUserVO = fsUserService.selectFsUserByUserId(userId);
         Date now = DateUtils.getNowDate();
 
         // 查询直播间信息
         Live live = liveMapper.selectLiveByLiveId(liveId);
+        if (live == null) {
+            throw new RuntimeException("直播间不存在");
+        }
 
-        // 判断用户进入时间:如果进入时间大于直播结束时间,说明是回放
-        boolean isReplay = false;
-        if (live != null && live.getFinishTime() != null) {
-            Date finishTime = java.sql.Timestamp.valueOf(live.getFinishTime());
-            isReplay = now.after(finishTime);
+        // 查询直播间录播视频时长(video_type IN (1, 2))
+        List<LiveVideo> videos = liveVideoMapper.selectByLiveIdAndType(liveId,1);
+        long totalDuration = 0L;
+        if (videos != null && !videos.isEmpty()) {
+            totalDuration = videos.stream()
+                    .filter(v -> v.getVideoType() != null && (v.getVideoType() == 1 || v.getVideoType() == 2))
+                    .filter(v -> v.getDuration() != null)
+                    .mapToLong(LiveVideo::getDuration)
+                    .sum();
         }
 
-        if(liveWatchUser != null) {
-            liveWatchUser.setUpdateTime(now);
-            liveWatchUser.setOnline(0);
+        // 判断是直播还是回放:开播时间 + 视频时长
+        Integer liveFlag = 0;
+        Integer replayFlag = 0;
 
-            // 更新location
-            if (StringUtils.isNotEmpty(location)) {
-                liveWatchUser.setLocation(location);
-            }
+        if (live.getStartTime() != null) {
+            // 将 LocalDateTime 转换为 Date
+            Date startTime = java.sql.Timestamp.valueOf(live.getStartTime());
+            // 计算结束时间:开播时间 + 视频时长(秒)
+            Date endTime = new Date(startTime.getTime() + totalDuration * 1000);
 
-            // 更新进入标记
-            if (isReplay) {
-                liveWatchUser.setReplayFlag(1);
+            if (now.before(endTime)) {
+                // 当前时间 < 开播时间 + 视频时长,说明是直播
+                liveFlag = 1;
+                replayFlag = 0;
             } else {
-                liveWatchUser.setLiveFlag(1);
+                // 当前时间 >= 开播时间 + 视频时长,说明是回放
+                liveFlag = 0;
+                replayFlag = 1;
             }
+        }
+
+        // 使用唯一索引查询:live_id, user_id, live_flag, replay_flag
+        LiveWatchUser liveWatchUser = baseMapper.selectByUniqueIndex(liveId, userId, liveFlag, replayFlag);
 
+        if (liveWatchUser != null) {
+            // 存在则更新
+            liveWatchUser.setUpdateTime(now);
+            liveWatchUser.setOnline(0);
+            if (StringUtils.isNotEmpty(location)) {
+                liveWatchUser.setLocation(location);
+            }
             baseMapper.updateLiveWatchUser(liveWatchUser);
-        }else{
+        } else {
+            // 不存在则插入
             liveWatchUser = new LiveWatchUser();
             liveWatchUser.setLiveId(liveId);
             liveWatchUser.setUserId(userId);
@@ -192,23 +216,16 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
             liveWatchUser.setMsgStatus(0);
             liveWatchUser.setOnline(0);
             liveWatchUser.setLocation(location);
-
-            // 设置进入标记
-            if (isReplay) {
-                liveWatchUser.setReplayFlag(1);
-                liveWatchUser.setLiveFlag(0);
-            } else {
-                liveWatchUser.setLiveFlag(1);
-                liveWatchUser.setReplayFlag(0);
-            }
-
+            liveWatchUser.setLiveFlag(liveFlag);
+            liveWatchUser.setReplayFlag(replayFlag);
             liveWatchUser.setCreateTime(now);
             liveWatchUser.setUpdateTime(now);
             baseMapper.insertLiveWatchUser(liveWatchUser);
         }
+
         liveWatchUser.setAvatar(fsUserVO.getAvatar());
         liveWatchUser.setNickName(fsUserVO.getNickname());
-        String hashKey  = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
+        String hashKey = String.format(LiveKeysConstant.LIVE_WATCH_USERS, liveId);
         redisCache.hashPut(hashKey, String.valueOf(userId), JSON.toJSONString(liveWatchUser));
         return liveWatchUser;
     }
@@ -216,7 +233,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
     @Override
     public LiveWatchUser joinWithoutLocation(long liveId, long userId) {
         LiveWatchUser liveWatchUser = getByLiveIdAndUserId(liveId, userId);
-        FsUser fsUserVO = fsUserService.selectFsUserByUserId(userId);
+        FsUserScrm fsUserVO = fsUserService.selectFsUserByUserId(userId);
         Date now = DateUtils.getNowDate();
 
         // 查询直播间信息
@@ -337,7 +354,7 @@ public class LiveWatchUserServiceImpl implements ILiveWatchUserService {
 
     @Override
     public int blockUser(Long userId) {
-        FsUser fsUser = new FsUser();
+        FsUserScrm fsUser = new FsUserScrm();
         fsUser.setUserId(userId);
         fsUser.setStatus(0);
         return fsUserService.updateFsUser(fsUser);

+ 141 - 0
fs-service/src/main/java/com/fs/pay/domain/PaymentMiniProgramConfig.java

@@ -0,0 +1,141 @@
+package com.fs.pay.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 多小程序支付配置对象 payment_mini_program_config
+ *
+ * @author fs
+ * @date 2025-11-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class PaymentMiniProgramConfig extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 支付类型:YB-易宝,TZH-台州银行,WX-微信,HF-汇付 */
+    @Excel(name = "支付类型:YB-易宝,TZH-台州银行,WX-微信,HF-汇付")
+    private String payType;
+
+    /**
+     * 小程序名称
+     */
+    @Excel(name = "小程序名称")
+    private String appName;
+    /** 小程序appid */
+    @Excel(name = "小程序appid")
+    private String appid;
+    /**
+     * 小程序密钥
+     */
+    @Excel(name = "小程序密钥")
+    private String appSecret;
+
+    /** 易宝商户号 */
+    @Excel(name = "易宝商户号")
+    private String ybMerchantNo;
+
+    /** 易宝Key */
+    @Excel(name = "易宝Key")
+    private String ybKey;
+
+    /** 易宝回调地址 */
+    @Excel(name = "易宝回调地址")
+    private String ybNotifyUrl;
+
+    /** 台州银行商户号 */
+    @Excel(name = "台州银行商户号")
+    private String tzhMerchantNo;
+
+    /** 台州appSecret */
+    @Excel(name = "台州appSecret")
+    private String tzhAppsecret;
+
+    /** 台州私钥 */
+    @Excel(name = "台州私钥")
+    private String tzhPrivateKey;
+
+    /** 台州平台公钥 */
+    @Excel(name = "台州平台公钥")
+    private String tzhPublicKey;
+
+    /** 台州appKey */
+    @Excel(name = "台州appKey")
+    private String tzhAppkey;
+
+    /** 台州支付回调地址 */
+    @Excel(name = "台州支付回调地址")
+    private String tzhPayNotifyUrl;
+
+    /** 台州退款回调地址 */
+    @Excel(name = "台州退款回调地址")
+    private String tzhRefundNotifyUrl;
+
+    /** 台州分账回调地址 */
+    @Excel(name = "台州分账回调地址")
+    private String tzhSplitNotifyUrl;
+
+    /** 微信商户号 */
+    @Excel(name = "微信商户号")
+    private String wxMerchantNo;
+
+    /** 微信Key */
+    @Excel(name = "微信Key")
+    private String wxKey;
+    /**
+     *
+     */
+    private String wxKeyPath;
+    /**
+     * 微信回调url
+     */
+    @Excel(name = "微信回调url")
+    private String wxNotifyUrl;
+
+    /** 汇付产品号 */
+    @Excel(name = "汇付产品号")
+    private String hfProductNo;
+
+    /** 汇付系统号 */
+    @Excel(name = "汇付系统号")
+    private String hfSystemNo;
+
+    /** 汇付商户号 */
+    @Excel(name = "汇付商户号")
+    private String hfMerchantNo;
+
+    /** 汇付服务商私钥 */
+    @Excel(name = "汇付服务商私钥")
+    private String hfPrivateKey;
+
+    /** 汇付公钥 */
+    @Excel(name = "汇付公钥")
+    private String hfPublicKey;
+
+    /** 汇付支付回调地址 */
+    @Excel(name = "汇付支付回调地址")
+    private String hfPayNotifyUrl;
+
+    /** 汇付大额支付回调地址 */
+    @Excel(name = "汇付大额支付回调地址")
+    private String hfLargePayNotifyUrl;
+
+    /** 汇付退款回调地址 */
+    @Excel(name = "汇付退款回调地址")
+    private String hfRefundNotifyUrl;
+
+    /** 汇付大额退款回调地址 */
+    @Excel(name = "汇付大额退款回调地址")
+    private String hfLargeRefundNotifyUrl;
+
+    /** 状态,1启用,0禁用 */
+    @Excel(name = "状态,1启用,0禁用")
+    private Integer status;
+
+
+}

+ 70 - 0
fs-service/src/main/java/com/fs/pay/mapper/PaymentMiniProgramConfigMapper.java

@@ -0,0 +1,70 @@
+package com.fs.pay.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.pay.domain.PaymentMiniProgramConfig;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 多小程序支付配置Mapper接口
+ *
+ * @author fs
+ * @date 2025-11-05
+ */
+public interface PaymentMiniProgramConfigMapper extends BaseMapper<PaymentMiniProgramConfig>{
+    /**
+     * 查询多小程序支付配置
+     *
+     * @param id 多小程序支付配置主键
+     * @return 多小程序支付配置
+     */
+    PaymentMiniProgramConfig selectPaymentMiniProgramConfigById(String id);
+
+    /**
+     * 查询多小程序支付配置列表
+     *
+     * @param paymentMiniProgramConfig 多小程序支付配置
+     * @return 多小程序支付配置集合
+     */
+    List<PaymentMiniProgramConfig> selectPaymentMiniProgramConfigList(PaymentMiniProgramConfig paymentMiniProgramConfig);
+
+    /**
+     * 新增多小程序支付配置
+     *
+     * @param paymentMiniProgramConfig 多小程序支付配置
+     * @return 结果
+     */
+    int insertPaymentMiniProgramConfig(PaymentMiniProgramConfig paymentMiniProgramConfig);
+
+    /**
+     * 修改多小程序支付配置
+     *
+     * @param paymentMiniProgramConfig 多小程序支付配置
+     * @return 结果
+     */
+    int updatePaymentMiniProgramConfig(PaymentMiniProgramConfig paymentMiniProgramConfig);
+
+    /**
+     * 删除多小程序支付配置
+     *
+     * @param id 多小程序支付配置主键
+     * @return 结果
+     */
+    int deletePaymentMiniProgramConfigById(String id);
+
+    /**
+     * 批量删除多小程序支付配置
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deletePaymentMiniProgramConfigByIds(String[] ids);
+
+    PaymentMiniProgramConfig selectPaymentConfigByAppId(@Param("appid") String appid);
+
+    @Select("select * from payment_mini_program_config where status = 1")
+    List<PaymentMiniProgramConfig> selectAll();
+
+}

+ 2 - 1
fs-service/src/main/java/com/fs/sop/mapper/QwSopTempMapper.java

@@ -6,6 +6,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTemp;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -70,5 +71,5 @@ public interface QwSopTempMapper extends BaseMapper<QwSopTemp> {
 
     List<QwSopTemp> listTemp();
 
-    List<QwSopTemp> selectListByIds(@Param("ids") List<String> ids);
+    List<QwSopTemp> selectListByIds(@Param("ids") Collection<String> ids);
 }

+ 6 - 1
fs-service/src/main/java/com/fs/sop/mapper/QwSopTempRulesMapper.java

@@ -6,6 +6,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTempRules;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
 
 import java.util.List;
 
@@ -16,7 +17,8 @@ import java.util.List;
  * @date 2025-02-06
  */
 @DataSource(DataSourceType.SOP)
-public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules>{
+@Repository
+public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules> {
     /**
      * 查询sop任务模板规则
      *
@@ -78,4 +80,7 @@ public interface QwSopTempRulesMapper extends BaseMapper<QwSopTempRules>{
     List<QwSopTempRules> listByTempIdAndNameAndDayNum(@Param("id") String id, @Param("dayNum") Integer dayNum);
 
     void deleteByIdList(@Param("ids") List<String> ids);
+
+    @Select("select * from qw_sop_temp_rules where course_id = #{courseId} ")
+    List<QwSopTempRules> selectQwSopTempRulesListByCourseId(@Param("courseId")Long courseId);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/sop/service/IQwSopTempContentService.java

@@ -5,7 +5,9 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTempContent;
 
+import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 /**
  * sop任务模板内容Service接口
@@ -84,4 +86,8 @@ public interface IQwSopTempContentService extends IService<QwSopTempContent>{
     void updateDay(QwSopTempContent content);
 
     List<QwSopTempContent> selectQwSopTempContentByTempId(String tempId);
+
+    void removeByTempIds(Collection<String> tempIds);
+
+    List<QwSopTempContent> listByTempIds(Collection<String> tempIds);
 }

+ 5 - 1
fs-service/src/main/java/com/fs/sop/service/IQwSopTempDayService.java

@@ -3,6 +3,7 @@ package com.fs.sop.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.sop.domain.QwSopTempDay;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -11,7 +12,7 @@ import java.util.List;
  * @author fs
  * @date 2025-02-06
  */
-public interface IQwSopTempDayService extends IService<QwSopTempDay>{
+public interface IQwSopTempDayService extends IService<QwSopTempDay> {
 
     void saveList(List<QwSopTempDay> list);
 
@@ -26,9 +27,12 @@ public interface IQwSopTempDayService extends IService<QwSopTempDay>{
     boolean saveOrUpdate(QwSopTempDay day);
 
     void addOrUpdateBatch(List<QwSopTempDay> days);
+
     void removeById(Long id);
 
     List<QwSopTempDay> listById(List<Long> dayIds);
 
     int getDayNumByIdLimitOne(String tempId);
+
+    void removeByTempIds(Collection<String> tempIds);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/sop/service/IQwSopTempRulesService.java

@@ -6,7 +6,9 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTempDay;
 import com.fs.sop.domain.QwSopTempRules;
 
+import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 
 /**
  * sop任务模板规则Service接口
@@ -88,4 +90,8 @@ public interface IQwSopTempRulesService extends IService<QwSopTempRules>{
     List<QwSopTempRules> listById(List<Long> rulesIds);
 
     void updateSiFenTemp();
+
+    List<QwSopTempRules> listByCourseId(Long courseId);
+
+    void removeByTempIds(Collection<String> tempIds);
 }

+ 5 - 3
fs-service/src/main/java/com/fs/sop/service/IQwSopTempService.java

@@ -6,6 +6,7 @@ import com.fs.sop.domain.QwSopTempDay;
 import com.fs.sop.params.QwSopShareTempParam;
 import com.fs.sop.vo.QwSopTempRedPackageVo;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -15,8 +16,7 @@ import java.util.Map;
  * @author fs
  * @date 2024-09-26
  */
-public interface IQwSopTempService
-{
+public interface IQwSopTempService {
     /**
      * 查询sop模板
      *
@@ -58,7 +58,7 @@ public interface IQwSopTempService
     public int deleteQwSopTempByIds(String[] ids);
 
     /**
-     *分享sop模板
+     * 分享sop模板
      */
     public int shareQwSopTemp(QwSopShareTempParam param);
 
@@ -99,4 +99,6 @@ public interface IQwSopTempService
     void updateRedPackage(List<QwSopTempRedPackageVo> list);
 
     List<String> getSelectableRange();
+
+    void syncTemplate(Long courseId);
 }

+ 1 - 0
fs-service/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java

@@ -95,6 +95,7 @@ public class QwSopLogsServiceImpl extends ServiceImpl<QwSopLogsMapper, QwSopLogs
     private QwExternalContactServiceImpl qwExternalContactService;
 
     @Autowired
+    @Lazy
     private IFsCourseWatchLogService watchLogService;
 
     @Autowired

+ 1 - 0
fs-service/src/main/java/com/fs/sop/service/impl/QwSopServiceImpl.java

@@ -137,6 +137,7 @@ public class QwSopServiceImpl implements IQwSopService
     private IFsUserService fsUserService;
 
     @Autowired
+    @Lazy
     private IFsCourseWatchLogService fsCourseWatchLogService;
 
     @Autowired

+ 15 - 0
fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempContentServiceImpl.java

@@ -5,10 +5,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.sop.domain.QwSopTempContent;
+import com.fs.sop.domain.QwSopTempDay;
 import com.fs.sop.mapper.QwSopTempContentMapper;
 import com.fs.sop.service.IQwSopTempContentService;
 import org.springframework.stereotype.Service;
 
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -148,4 +151,16 @@ public class QwSopTempContentServiceImpl extends ServiceImpl<QwSopTempContentMap
     public List<QwSopTempContent> selectQwSopTempContentByTempId(String tempId) {
         return baseMapper.selectQwSopTempContentByTempId(tempId);
     }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public void removeByTempIds(Collection<String> tempIds) {
+        this.remove(new QueryWrapper<QwSopTempContent>().in("temp_id", tempIds));
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTempContent> listByTempIds(Collection<String> tempIds) {
+        return baseMapper.selectList(new QueryWrapper<QwSopTempContent>().in("temp_id", tempIds));
+    }
 }

+ 9 - 0
fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempDayServiceImpl.java

@@ -1,6 +1,8 @@
 package com.fs.sop.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
@@ -11,6 +13,7 @@ import lombok.AllArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -83,4 +86,10 @@ public class QwSopTempDayServiceImpl extends ServiceImpl<QwSopTempDayMapper, QwS
     public int getDayNumByIdLimitOne(String tempId) {
         return  qwSopTempDayMapper.getDayNumByIdLimitOne(tempId);
     }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public void removeByTempIds(Collection<String> tempIds) {
+        this.remove(new QueryWrapper<QwSopTempDay>().in("temp_id", tempIds));
+    }
 }

+ 26 - 19
fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempRulesServiceImpl.java

@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -59,8 +60,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return sop任务模板规则
      */
     @Override
-    public QwSopTempRules selectQwSopTempRulesById(String id)
-    {
+    public QwSopTempRules selectQwSopTempRulesById(String id) {
         return baseMapper.selectQwSopTempRulesById(id);
     }
 
@@ -71,8 +71,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return sop任务模板规则
      */
     @Override
-    public List<QwSopTempRules> selectQwSopTempRulesList(QwSopTempRules qwSopTempRules)
-    {
+    public List<QwSopTempRules> selectQwSopTempRulesList(QwSopTempRules qwSopTempRules) {
         return baseMapper.selectQwSopTempRulesList(qwSopTempRules);
     }
 
@@ -83,8 +82,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return 结果
      */
     @Override
-    public int insertQwSopTempRules(QwSopTempRules qwSopTempRules)
-    {
+    public int insertQwSopTempRules(QwSopTempRules qwSopTempRules) {
         return baseMapper.insertQwSopTempRules(qwSopTempRules);
     }
 
@@ -95,8 +93,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return 结果
      */
     @Override
-    public int updateQwSopTempRules(QwSopTempRules qwSopTempRules)
-    {
+    public int updateQwSopTempRules(QwSopTempRules qwSopTempRules) {
         return baseMapper.updateQwSopTempRules(qwSopTempRules);
     }
 
@@ -107,8 +104,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return 结果
      */
     @Override
-    public int deleteQwSopTempRulesByIds(String[] ids)
-    {
+    public int deleteQwSopTempRulesByIds(String[] ids) {
         return baseMapper.deleteQwSopTempRulesByIds(ids);
     }
 
@@ -119,8 +115,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
      * @return 结果
      */
     @Override
-    public int deleteQwSopTempRulesById(String id)
-    {
+    public int deleteQwSopTempRulesById(String id) {
         return baseMapper.deleteQwSopTempRulesById(id);
     }
 
@@ -140,7 +135,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
     @DataSource(DataSourceType.SOP)
     public List<QwSopTempRules> listByTempId(String id) {
         List<QwSopTempDay> dayList = qwSopTempDayService.list(new QueryWrapper<QwSopTempDay>().eq("temp_id", id).orderByAsc("day_num"));
-        if(dayList.isEmpty()) return Collections.emptyList();
+        if (dayList.isEmpty()) return Collections.emptyList();
         Map<Long, Integer> collect = dayList.stream().collect(Collectors.toMap(QwSopTempDay::getId, QwSopTempDay::getDayNum));
         List<QwSopTempRules> rulesList = list(new QueryWrapper<QwSopTempRules>().in("day_id", PubFun.listToNewList(dayList, QwSopTempDay::getId)));
         rulesList.forEach(r -> {
@@ -148,7 +143,7 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
         });
         List<QwSopTempContent> ts = qwSopTempContentService.list(new QueryWrapper<QwSopTempContent>().in("rules_id", PubFun.listToNewList(rulesList, QwSopTempRules::getId)));
         Map<Long, List<QwSopTempContent>> map = PubFun.listToMapByGroupList(ts, QwSopTempContent::getRulesId);
-        rulesList.stream().filter( e -> map.containsKey(e.getId())).forEach(e -> e.setSettingList(map.get(e.getId())));
+        rulesList.stream().filter(e -> map.containsKey(e.getId())).forEach(e -> e.setSettingList(map.get(e.getId())));
         return rulesList;
     }
 
@@ -202,9 +197,9 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
     public void updateRulesDayNumIsNull() {
         List<QwSopTempRules> rules = qwSopTempRulesMapper.rulesNull();
         log.info("时间为空的有:{}", rules.size());
-        for (QwSopTempRules rule : rules){
+        for (QwSopTempRules rule : rules) {
             QwSopTempDay day = dayMapper.info(rule.getDayId());
-            if (day!=null){
+            if (day != null) {
                 QwSopTempRules rules1 = new QwSopTempRules();
                 rules1.setDayNum(day.getDayNum());
                 rules1.setId(rule.getId());
@@ -226,14 +221,14 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
     @Override
     public void updateSiFenTemp() {
         List<QwSopTempContentVO> contents = contentMapper.updateSiFenTemp();
-        for(QwSopTempContentVO content : contents){
-            if (content.getVideoId()==null){
+        for (QwSopTempContentVO content : contents) {
+            if (content.getVideoId() == null) {
                 continue;
             }
             QwSopTempContent content1 = new QwSopTempContent();
             FsUserCourseVideo video = videoMapper.selectFsUserCourseVideoByVideoId(content.getVideoId());
             QwSopTempSetting.Content.Setting settingMap = new QwSopTempSetting.Content.Setting();
-            QwSopTempSetting.Content.Setting setting = JSON.parseObject(content.getContent(),QwSopTempSetting.Content.Setting.class);
+            QwSopTempSetting.Content.Setting setting = JSON.parseObject(content.getContent(), QwSopTempSetting.Content.Setting.class);
             settingMap.setContentType("4");
             settingMap.setMiniprogramAppid("wx73f85f8d62769119");
             settingMap.setMiniprogramPicUrl(setting.getLinkImageUrl());
@@ -245,4 +240,16 @@ public class QwSopTempRulesServiceImpl extends ServiceImpl<QwSopTempRulesMapper,
             contentMapper.updateQwSopTempContent(content1);
         }
     }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<QwSopTempRules> listByCourseId(Long courseId) {
+        return qwSopTempRulesMapper.selectQwSopTempRulesListByCourseId(courseId);
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public void removeByTempIds(Collection<String> tempIds) {
+        this.remove(new QueryWrapper<QwSopTempRules>().in("temp_id", tempIds));
+    }
 }

+ 140 - 62
fs-service/src/main/java/com/fs/sop/service/impl/QwSopTempServiceImpl.java

@@ -9,7 +9,6 @@ import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.PubFun;
 import com.fs.company.domain.Company;
 import com.fs.company.service.ICompanyService;
-import com.fs.company.service.impl.CompanyServiceImpl;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsUserCourse;
@@ -30,16 +29,16 @@ import com.fs.sop.mapper.QwSopTempMapper;
 import com.fs.sop.params.QwSopShareTempParam;
 import com.fs.sop.service.*;
 import com.fs.sop.vo.QwSopTempRedPackageVo;
-import com.fs.sop.vo.VoiceVo;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
 import io.netty.util.internal.StringUtil;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.beanutils.ConvertUtils;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
@@ -47,6 +46,7 @@ import java.text.SimpleDateFormat;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -59,8 +59,8 @@ import java.util.stream.Collectors;
  */
 @Service
 @AllArgsConstructor
-public class QwSopTempServiceImpl implements IQwSopTempService
-{
+@Slf4j
+public class QwSopTempServiceImpl implements IQwSopTempService {
 
     @Autowired
     private CloudHostProper cloudHostProper;
@@ -79,6 +79,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
     private final IQwSopService qwSopService;
     private final IQwUserService qwUserService;
     private final ICompanyService companyService;
+    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
 
     /**
      * 查询sop模板
@@ -87,7 +88,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return sop模板
      */
     @Override
-    public QwSopTemp selectQwSopTempById(String id){
+    public QwSopTemp selectQwSopTempById(String id) {
 //        QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(id);
         QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(id);
         List<QwSopTempDay> qwSopTempDays = qwSopTempDayService.listByTempId(id);
@@ -117,8 +118,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return sop模板
      */
     @Override
-    public List<QwSopTemp> selectQwSopTempList(QwSopTemp qwSopTemp)
-    {
+    public List<QwSopTemp> selectQwSopTempList(QwSopTemp qwSopTemp) {
         return qwSopTempMapper.selectQwSopTempList(qwSopTemp);
     }
 
@@ -129,7 +129,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return 结果
      */
     @Override
-    public int insertQwSopTemp(QwSopTemp qwSopTemp){
+    public int insertQwSopTemp(QwSopTemp qwSopTemp) {
         qwSopTemp.setId(UUID.randomUUID().toString());
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         qwSopTemp.setCreateTime(sdf.format(new Date()));
@@ -155,7 +155,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return 结果
      */
     @Override
-    public int updateQwSopTemp(QwSopTemp qwSopTemp){
+    public int updateQwSopTemp(QwSopTemp qwSopTemp) {
         return qwSopTempMapper.updateQwSopTemp(qwSopTemp);
     }
 
@@ -166,8 +166,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return 结果
      */
     @Override
-    public int deleteQwSopTempByIds(String[] ids)
-    {
+    public int deleteQwSopTempByIds(String[] ids) {
         return qwSopTempMapper.deleteQwSopTempByIds(ids);
     }
 
@@ -196,8 +195,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
      * @return 结果
      */
     @Override
-    public int deleteQwSopTempById(String id)
-    {
+    public int deleteQwSopTempById(String id) {
         return qwSopTempMapper.deleteQwSopTempById(id);
     }
 
@@ -214,7 +212,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
     public int update(QwSopTemp qwSopTemp) {
         QwSopTemp temp = qwSopTempMapper.selectById(qwSopTemp.getId());
         int i = qwSopTempMapper.updateById(qwSopTemp);
-        if(!Objects.equals(temp.getGap(), qwSopTemp.getGap())){
+        if (!Objects.equals(temp.getGap(), qwSopTemp.getGap())) {
             // 重新排序
             reorder(qwSopTemp.getId());
         }
@@ -233,7 +231,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         qwSopTempRulesService.removeByDayId(day.getId());
         qwSopTempContentService.removeByDayId(day.getId());
 
-        list.forEach(item-> item.setDayId(day.getId()));
+        list.forEach(item -> item.setDayId(day.getId()));
         processAndReplaceContent(list);
         QwSopTempRules rules = list.get(0);
         String tempId = rules.getTempId();
@@ -251,29 +249,29 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         reorder(day.getTempId());
         Map<String, Object> map = new HashMap<>();
         map.put("id", day.getId());
-        if(!voiceList.isEmpty()){
+        if (!voiceList.isEmpty()) {
             for (QwSopTempContent qwSopTempContent : voiceList) {
                 String content = qwSopTempContent.getContent();
                 JSONObject jsonObject = JSONObject.parseObject(content);
                 String text = jsonObject.getString("value");
                 List<QwSop> qwSopList = qwSopService.selectQwSopByTempId(tempId);//通过tempId查询出所有sop
-                if(qwSopList != null && !qwSopList.isEmpty()){
+                if (qwSopList != null && !qwSopList.isEmpty()) {
                     for (QwSop qwSop : qwSopList) {
-                        if(qwSop != null && qwSop.getQwUserIds() != null){
+                        if (qwSop != null && qwSop.getQwUserIds() != null) {
                             //查询出所有的tempContent来筛选文字
                             List<QwSopTempContent> qwSopTempContentList = qwSopTempContentService.selectQwSopTempContentByTempId(tempId);
-                            if(qwSopTempContentList != null && !qwSopTempContentList.isEmpty()){
+                            if (qwSopTempContentList != null && !qwSopTempContentList.isEmpty()) {
                                 for (QwSopTempContent qwSopTemp : qwSopTempContentList) {
-                                    if(qwSopTemp != null && qwSopTemp.getContentType() == 7){
+                                    if (qwSopTemp != null && qwSopTemp.getContentType() == 7) {
                                         String[] split = qwSop.getQwUserIds().split(",");
                                         Long[] qwUserIds = (Long[]) ConvertUtils.convert(split, Long.class);
                                         List<QwUserVO> qwUserVOS = qwUserService.selectQwUserVOByIds(qwUserIds);
-                                        if(qwUserVOS != null){
+                                        if (qwUserVOS != null) {
                                             for (QwUserVO qwUserVO : qwUserVOS) {
                                                 Long companyUserId = qwUserVO.getCompanyUserId();
-                                                QwSopTempVoice qwSopTempVoice = qwSopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(companyUserId,text);
-                                                if(qwSopTempVoice == null){
-                                                    if(companyUserId != null && text != null){
+                                                QwSopTempVoice qwSopTempVoice = qwSopTempVoiceService.selectQwSopTempVoiceByCompanyUserIdAndVoiceTxt(companyUserId, text);
+                                                if (qwSopTempVoice == null) {
+                                                    if (companyUserId != null && text != null) {
                                                         QwSopTempVoice sopTempVoice = new QwSopTempVoice();
                                                         sopTempVoice.setCompanyUserId(companyUserId);
                                                         sopTempVoice.setVoiceTxt(text);
@@ -298,12 +296,12 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         return map;
     }
 
-    private void processAndReplaceContent(List<QwSopTempRules> tempSettings){
+    private void processAndReplaceContent(List<QwSopTempRules> tempSettings) {
         List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
         //循环天
-        tempSettings.forEach(settingList->{
+        tempSettings.forEach(settingList -> {
             //循环单日
-            settingList.getSettingList().forEach(item->{
+            settingList.getSettingList().forEach(item -> {
                 JSONObject obj = JSON.parseObject(item.getContent());
                 List<String> list = Arrays.asList("linkTitle", "linkDescribe", "desc", "nickname", "value");
                 list.stream().filter(obj::containsKey).forEach(key -> {
@@ -316,23 +314,24 @@ public class QwSopTempServiceImpl implements IQwSopTempService
 
     @Override
     public void delRules(Long id) {
-        if(id == null) return;
+        if (id == null) return;
         QwSopTempDay day = qwSopTempDayService.info(id);
-        if(day == null) return;
+        if (day == null) return;
         qwSopTempDayService.removeById(day.getId());
         qwSopTempContentService.removeByDayId(day.getId());
         reorder(day.getTempId());
     }
+
     @Override
     public QwSopTempDay selectRulesInfo(Long id) {
         QwSopTempDay day = qwSopTempDayService.info(id);
         List<QwSopTempRules> rulesList = qwSopTempRulesService.listByDayId(id);
-        if(!rulesList.isEmpty()){
+        if (!rulesList.isEmpty()) {
             List<QwSopTempContent> contentList = qwSopTempContentService.listByRulesIds(PubFun.listToNewList(rulesList, QwSopTempRules::getId));
             Map<Long, List<QwSopTempContent>> contentMap = PubFun.listToMapByGroupList(contentList, QwSopTempContent::getRulesId);
             rulesList.forEach(e -> {
                 List<QwSopTempContent> contents = contentMap.get(e.getId());
-                if (CollectionUtils.isNotEmpty(contents)){
+                if (CollectionUtils.isNotEmpty(contents)) {
                     e.setSetting(contents.stream().map(c -> JSON.parseObject(c.getContent())).collect(Collectors.toList()));
                 }
             });
@@ -353,14 +352,14 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         qwSopTemp.setId(newId);
         qwSopTempMapper.insertQwSopTemp(qwSopTemp);
         List<QwSopTempDay> dayList = qwSopTempRulesService.listByTempIdAll(oldId);
-        if(dayList.isEmpty()) return;
+        if (dayList.isEmpty()) return;
 //        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId(oldId);
         dayList.forEach(day -> {
             day.setTempId(newId);
-            if(day.getList() != null && !day.getList().isEmpty()){
+            if (day.getList() != null && !day.getList().isEmpty()) {
                 day.getList().forEach(e -> {
                     e.setTempId(newId);
-                    if(e.getSettingList() != null && !e.getSettingList().isEmpty()){
+                    if (e.getSettingList() != null && !e.getSettingList().isEmpty()) {
                         e.getSettingList().forEach(item -> {
                             item.setTempId(newId);
                         });
@@ -376,7 +375,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
             item.setRulesId(e.getId());
             item.setDayId(e.getDayId());
         }));
-        qwSopTempContentService.insertBatch(collect.stream().flatMap(e ->e.getSettingList().stream()).collect(Collectors.toList()));
+        qwSopTempContentService.insertBatch(collect.stream().flatMap(e -> e.getSettingList().stream()).collect(Collectors.toList()));
     }
 
     @Override
@@ -390,7 +389,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
     public void sortDay(List<SortDayVo> list) {
         Collection<QwSopTempDay> days = qwSopTempDayService.listByIds(PubFun.listToNewList(list, SortDayVo::getId));
         Map<Long, Integer> dayMap = list.stream().collect(Collectors.toMap(SortDayVo::getId, SortDayVo::getDayNum));
-        if(days.stream().anyMatch(e -> !dayMap.containsKey(e.getId()))){
+        if (days.stream().anyMatch(e -> !dayMap.containsKey(e.getId()))) {
             throw new BaseException("数据错误!");
         }
         days.forEach(day -> day.setDayNum(dayMap.get(day.getId())));
@@ -404,10 +403,10 @@ public class QwSopTempServiceImpl implements IQwSopTempService
 
 
     @DataSource(DataSourceType.SOP)
-    private void reorder(String tempId){
+    private void reorder(String tempId) {
         QwSopTemp qwSopTemp = qwSopTempMapper.selectQwSopTempById(tempId);
         List<QwSopTempDay> days = qwSopTempDayService.listByTempId(tempId);
-        if(days.isEmpty()) return;
+        if (days.isEmpty()) return;
         for (int i = 0; i < days.size(); i++) {
             QwSopTempDay entity = days.get(i);
             entity.setSorts(i);
@@ -418,7 +417,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
 
     @Override
     public void createSopTempRules(QwSopTemp temp) {
-        if(temp.getTime() == null){
+        if (temp.getTime() == null) {
             return;
         }
 
@@ -436,11 +435,11 @@ public class QwSopTempServiceImpl implements IQwSopTempService
             day.setSorts(day.getDayNum());
             day.setList(new ArrayList<>());
             List<String> timeList = new ArrayList<>();
-            if(temp.getTimeList() != null){
+            if (temp.getTimeList() != null) {
                 timeList = JSON.parseArray(JSON.toJSONString(temp.getTimeList()), String.class);
             }
             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
-            if(temp.getTime() != null){
+            if (temp.getTime() != null) {
                 timeList.add(0, temp.getTime().format(formatter));
             }
             AtomicInteger sorts = new AtomicInteger(0);
@@ -448,15 +447,15 @@ public class QwSopTempServiceImpl implements IQwSopTempService
                 QwSopTempRules rules = new QwSopTempRules();
                 rules.setTempId(temp.getId());
                 rules.setName(day.getName());
-                if (temp.getOpenOfficial().equals("1")){
+                if (temp.getOpenOfficial().equals("1")) {
                     rules.setIsOfficial(sorts.get() == 0 ? "1" : "0");
-                }else {
+                } else {
                     rules.setIsOfficial("0");
                 }
 
-                if (day.getDayNum()==1 && sorts.get() == 0 && temp.getOpenOfficial().equals("1")){
+                if (day.getDayNum() == 1 && sorts.get() == 0 && temp.getOpenOfficial().equals("1")) {
                     rules.setTime("01:05");
-                }else {
+                } else {
                     rules.setTime(time);
                 }
 
@@ -475,10 +474,10 @@ public class QwSopTempServiceImpl implements IQwSopTempService
                 setting.setMiniprogramTitle(e.getTitle());
 
                 //用课节图片做封面
-                if("今正科技".equals(cloudHostProper.getCompanyName())){
-                    setting.setMiniprogramPicUrl(!StringUtil.isNullOrEmpty(e.getThumbnail())?e.getThumbnail():fsUserCourse.getImgUrl());
-                    setting.setLinkImageUrl(!StringUtil.isNullOrEmpty(e.getThumbnail())?e.getThumbnail():fsUserCourse.getImgUrl());
-                }else {
+                if ("今正科技".equals(cloudHostProper.getCompanyName())) {
+                    setting.setMiniprogramPicUrl(!StringUtil.isNullOrEmpty(e.getThumbnail()) ? e.getThumbnail() : fsUserCourse.getImgUrl());
+                    setting.setLinkImageUrl(!StringUtil.isNullOrEmpty(e.getThumbnail()) ? e.getThumbnail() : fsUserCourse.getImgUrl());
+                } else {
                     setting.setMiniprogramPicUrl(fsUserCourse.getImgUrl());
                     setting.setLinkImageUrl(fsUserCourse.getImgUrl());
 
@@ -489,7 +488,7 @@ public class QwSopTempServiceImpl implements IQwSopTempService
                 setting.setContentType("4");
                 content.setContent(JSON.toJSONString(setting));
                 content.setIsBindUrl(1);
-                List<QwSopTempContent> qwSopTempContents = new ArrayList<>() ;
+                List<QwSopTempContent> qwSopTempContents = new ArrayList<>();
                 qwSopTempContents.add(content);
                 if (sorts.get() == 0 && !StringUtil.isNullOrEmpty(temp.getModeContent())) {
                     QwSopTempContent content2 = new QwSopTempContent();
@@ -501,13 +500,13 @@ public class QwSopTempServiceImpl implements IQwSopTempService
                     content2.setContent(JSON.toJSONString(setting2));
                     qwSopTempContents.add(content2);
                 }
-                if (sorts.get() > 0){
+                if (sorts.get() > 0) {
 
                     QwSopTempContent content3 = new QwSopTempContent();
                     content3.setTempId(temp.getId());
                     content3.setContentType(3);
                     QwSopTempSetting2.Content.Setting setting3 = new QwSopTempSetting2.Content.Setting();
-                    setting3.setValue(temp.getTimeDesc().get(sorts.get()-1));
+                    setting3.setValue(temp.getTimeDesc().get(sorts.get() - 1));
                     setting3.setContentType("1");
                     content3.setContent(JSON.toJSONString(setting3));
                     qwSopTempContents.add(content3);
@@ -515,11 +514,11 @@ public class QwSopTempServiceImpl implements IQwSopTempService
                 }
 
                 rules.setSorts(sorts.getAndIncrement());
-                if(rules.getSorts() == 0){
+                if (rules.getSorts() == 0) {
                     rules.setCourseType(0);
-                }else if(rules.getSorts() == 1){
+                } else if (rules.getSorts() == 1) {
                     rules.setCourseType(1);
-                }else{
+                } else {
                     rules.setCourseType(4);
                 }
                 rules.setList(qwSopTempContents);
@@ -540,10 +539,10 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         CourseConfig courseConfig = JSON.parseObject(sysConfig.getConfigValue(), CourseConfig.class);
         List<Long> videoIdList = PubFun.listToNewList(ruleList, QwSopTempRules::getVideoId);
         Map<Long, Optional<BigDecimal>> redMap;
-        if(!videoIdList.isEmpty()){
+        if (!videoIdList.isEmpty()) {
             List<FsUserCourseVideoRedPackage> redPackageList = fsUserCourseVideoRedPackageService.listByCompanyIdAndVideoIds(temp.getCompanyId(), videoIdList);
             redMap = redPackageList.stream().collect(Collectors.groupingBy(FsUserCourseVideoRedPackage::getVideoId, Collectors.mapping(FsUserCourseVideoRedPackage::getRedPacketMoney, Collectors.reducing((e1, e2) -> e1))));
-        }else{
+        } else {
             redMap = new HashMap<>();
         }
 
@@ -562,15 +561,15 @@ public class QwSopTempServiceImpl implements IQwSopTempService
     @Override
     public List<QwSopTempRedPackageVo> redList(String id) {
         List<QwSopTempDay> dayList = qwSopTempRulesService.listByTempIdAll(id);
-        if(CollectionUtils.isEmpty(dayList)){
+        if (CollectionUtils.isEmpty(dayList)) {
             return Collections.emptyList();
         }
         List<QwSopTempRules> rules = dayList.stream()
-                .filter(e->e!= null && e.getList()!=null)
+                .filter(e -> e != null && e.getList() != null)
                 .flatMap(e -> e.getList().stream())
                 .filter(Objects::nonNull)
                 .collect(Collectors.toList());
-        if(rules.isEmpty()){
+        if (rules.isEmpty()) {
             return Collections.emptyList();
         }
         List<FsUserCourseVideoRedPackage> redPackageList = fsUserCourseVideoRedPackageService.listByRuleIds(PubFun.listToNewList(rules, QwSopTempRules::getId));
@@ -608,14 +607,93 @@ public class QwSopTempServiceImpl implements IQwSopTempService
         }).collect(Collectors.toList());
         fsUserCourseVideoRedPackageService.batchSaveFsUserCourseVideoRedPackage(redPackage);
     }
+
     @Override
     public List<String> getSelectableRange() {
         SysConfig sysConfig = sysConfigService.selectConfigByConfigKey("course.config");
         CourseConfig courseConfig = JSON.parseObject(sysConfig.getConfigValue(), CourseConfig.class);
         List<CourseConfig.DisabledTimeVo> disabledTimeList = courseConfig.getDisabledTimeList();
-        if(disabledTimeList == null){
+        if (disabledTimeList == null) {
             return Collections.emptyList();
         }
         return TimeCalculator.calculateAvailableTimes(disabledTimeList);
     }
+
+
+    @Override
+    public void syncTemplate(Long courseId) {
+        // 根据courseId查询所有相关的sop模板规则
+        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByCourseId(courseId);
+        if (CollectionUtils.isEmpty(rulesList)) {
+            return;
+        }
+
+        // 获取这些规则关联的模板ID集合
+        Set<String> tempIds = rulesList.stream()
+                .map(QwSopTempRules::getTempId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        // 查询相关联的sop模板
+        if (CollectionUtils.isEmpty(tempIds)) {
+            return;
+        }
+        List<QwSopTemp> tempList = qwSopTempMapper.selectListByIds(tempIds);
+        tempList = tempList.stream().filter(f -> Objects.equals(f.getStatus(), "1")).collect(Collectors.toList());
+        if (CollectionUtils.isEmpty(tempList)) {
+            return;
+        }
+        List<QwSopTempContent> contentList = qwSopTempContentService.listByTempIds(tempIds);
+
+        CountDownLatch latch = new CountDownLatch(tempList.size());
+        // 对每个模板执行同步操作
+        for (QwSopTemp temp : tempList) {
+            // 构造timeList timeDesc time
+            rulesList.stream().filter(e -> e.getTempId().equals(temp.getId())).findFirst()
+                    .ifPresent(first -> temp.setTime(LocalTime.parse(first.getTime() + ":00")));
+
+            temp.setTimeList(rulesList.stream()
+                    .filter(e -> e.getTempId().equals(temp.getId()) && Objects.equals(e.getIsOfficial(), "0")
+                            && Objects.equals(e.getName(), "第1天")).map(QwSopTempRules::getTime)
+                    .collect(Collectors.toList()));
+            // 过滤并找到 dayId 最小的元素
+            Optional<QwSopTempContent> minDayEntity = contentList.stream()
+                    // 1. 过滤条件:tempId匹配 + isBindUrl为null
+                    .filter(e -> e.getTempId().equals(temp.getId())
+                            && Objects.isNull(e.getIsBindUrl()))
+                    // 2. 按 dayId 升序排序,取第一个(最小)
+                    .min(Comparator.comparingLong(QwSopTempContent::getDayId)); // 若dayId是Long,用comparingLong
+
+            if (minDayEntity.isPresent()) {
+                QwSopTempContent qwSopTempContent = minDayEntity.get();
+                temp.setTimeDesc(contentList.stream().filter(e -> e.getTempId().equals(temp.getId())
+                                && Objects.isNull(e.getIsBindUrl())
+                                && Objects.equals(qwSopTempContent.getDayId(), e.getDayId())
+                        )
+                        .map(m -> JSONObject.parseObject(m.getContent()).getString("value")).collect(Collectors.toList()));
+            }
+
+            // 插入课程id
+            temp.setCourseId(courseId);
+            temp.setOpenOfficial("1");
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            temp.setCreateTime(sdf.format(new Date()));
+            temp.setId(UUID.randomUUID().toString());
+            qwSopTempMapper.insertQwSopTemp(temp);
+            // 重新生成该模板的规则和内容
+            threadPoolTaskExecutor.execute(() -> createSopTempRules(temp));
+            latch.countDown();
+        }
+
+        try {
+            latch.await();
+            qwSopTempMapper.deleteQwSopTempByIds(tempIds.toArray(new String[0]));
+            qwSopTempDayService.removeByTempIds(tempIds);
+            qwSopTempRulesService.removeByTempIds(tempIds);
+            qwSopTempContentService.removeByTempIds(tempIds);
+        } catch (InterruptedException e) {
+            log.error("等待线程执行完成时被中断", e);
+        }
+    }
+
 }

+ 104 - 0
fs-service/src/main/java/com/fs/wx/order/domain/FsWxExpressTask.java

@@ -0,0 +1,104 @@
+package com.fs.wx.order.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 微信同步发货信息定时任务表
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class FsWxExpressTask {
+
+    /**
+     * 任务ID,唯一标识
+     */
+    private Long id;
+
+    /**
+     * 订单code
+     */
+    private String orderCode;
+
+    /**
+     * 支付单号
+     */
+    private String payCode;
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 消息内容,JSON格式。
+     */
+    private String data;
+
+    /**
+     * 任务状态:0=待执行, 1=执行中, 2=执行成功, 3=执行失败, 4=已取消
+     */
+    private Integer status;
+
+    /**
+     * 当前重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 最大重试次数
+     */
+    private Integer maxRetries;
+
+    /**
+     * 请求参数(JSON格式,主要记录 access_token 获取方式)
+     */
+    private String requestParams;
+
+    /**
+     * 完整的请求体 (JSON格式)
+     */
+    private String requestBody;
+
+    /**
+     * API 响应结果 (JSON格式)
+     */
+    private String responseBody;
+
+    /**
+     * 错误信息 (如果执行失败)
+     */
+    private String errorMessage;
+
+    /**
+     * 任务创建时间
+     */
+    private LocalDateTime createTime; // 使用LocalDateTime对应timestamp
+
+    /**
+     * 最后更新时间
+     */
+    private LocalDateTime updateTime; // 使用LocalDateTime对应timestamp
+
+
+    /**
+     * 快递公司
+     */
+    private String expressCompany;
+
+    /**
+     * 快递编号
+     */
+    private String expressNo;
+
+    private Integer type;
+
+    /**
+     * 小程序ID
+     */
+    private String appid;
+}

+ 21 - 0
fs-service/src/main/java/com/fs/wx/order/dto/Contact.java

@@ -0,0 +1,21 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Contact {
+    // 根据实际需要添加更多联系人字段
+    @JsonProperty("consignor_contact")
+    private String consignorContact; // 发货人联系方式 (示例字段)
+
+    // 可以添加收货人联系方式等
+    // @JsonProperty("receiver_contact")
+    // private String receiverContact;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wx/order/dto/OrderKey.java

@@ -0,0 +1,27 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderKey {
+
+    @JsonProperty("order_number_type")
+    private Integer orderNumberType;
+
+    @JsonProperty("transaction_id")
+    private String transactionId;
+
+    @JsonProperty("mchid")
+    private String mchId;
+
+    @JsonProperty("out_trade_no")
+    private String outTradeNo;
+}

+ 77 - 0
fs-service/src/main/java/com/fs/wx/order/dto/OrderQueryRequest.java

@@ -0,0 +1,77 @@
+package com.fs.wx.order.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OrderQueryRequest {
+
+    /**
+     * 聚合账户 (Required)
+     */
+    private String account;
+
+    /**
+     * 下游订单号 (Required if upOrderId is null)
+     */
+    private String lowOrderId;
+
+    /**
+     * 通莞订单号 (Required if lowOrderId is null)
+     */
+    private String upOrderId;
+
+    /**
+     * 值为"Y"时,接口返回优惠信息字段 (Optional)
+     */
+    private String extendInfo;
+
+    /**
+     * 值为"1"时,接口返回易宝专业版分账订单详情 (Optional)
+     */
+    private String isExtend;
+
+    /**
+     * 值为"1"时,接口返回SAAS分账订单详情 (Optional)
+     */
+    private String isNeedUpInfo;
+
+    /**
+     * 值为"1"时,接口返回渠道商户订单号字段 (Optional)
+     */
+    private String isNeedChannelMchOrderId;
+
+    /**
+     * 值为"1"时,返回因公付金额信息,仅支付宝服务商渠道支持 (Optional)
+     */
+    private String isNeedEnterprisePayInfo;
+
+    /**
+     * 签名 (Required)
+     * Note: This will be calculated and set by the service.
+     */
+    private String sign;
+
+    /**
+     * Helper method to get parameters for signing.
+     * Excludes the 'sign' field itself.
+     * Returns sorted map to ensure consistent order for signing.
+     *
+     * @return A map of non-null parameters sorted by key.
+     */
+    public Map<String, String> toSignMap() {
+       Map<String,String> sign = new HashMap<>();
+        sign.put("account",account);
+        sign.put("upOrderId",upOrderId);
+        sign.put("isNeedUpInfo", isNeedUpInfo);
+        return sign;
+    }
+}

+ 236 - 0
fs-service/src/main/java/com/fs/wx/order/dto/OrderQueryResponse.java

@@ -0,0 +1,236 @@
+package com.fs.wx.order.dto;
+
+import cn.hutool.core.annotation.Alias;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class OrderQueryResponse {
+
+    /**
+     * 100:成功,101:失败 (Required)
+     */
+    private Integer status;
+
+    /**
+     * 消息描述 (Required)
+     */
+    private String message;
+
+    /**
+     * 上游订单号 (Required)
+     */
+    private String channelOrderId;
+
+    /**
+     * 通莞订单号 (Required)
+     */
+    private String upOrderId;
+
+    /**
+     * 默认传null (Required) - Although API doc says required, it seems informational.
+     */
+    private String payoffType;
+
+    /**
+     * 支付时间 (Required)
+     */
+    private String payTime;
+
+    /**
+     * WX: openid; Alipay: account name (Required)
+     * Mapped from both "openId" and "openid" in JSON
+     */
+    @Alias("openid") // Instruct Hutool to map "openid" to this field as well
+    private String openId;
+
+    /**
+     * 签名 (Required) - For response validation
+     */
+    private String sign;
+
+    /**
+     * 结算渠道编号 (Required)
+     */
+    private String settlementChannel;
+
+    /**
+     * 下游订单号 (Required)
+     */
+    private String lowOrderId;
+
+    /**
+     * 支付金额,单位元 (Required)
+     */
+    private String payMoney; // Keep as String as per API doc, can be converted later
+
+    /**
+     * 支付方式 0:WX, 1:ZFB, 2:UnionPay QR, 6:LongPay, 8:BestPay, H:Digital Currency (Required)
+     */
+    private String payType;
+
+    /**
+     * 0:Success, 1:Fail, 2:Revoked, 4:Pending, 5:Refunded, 6:Partial Refund (Required)
+     */
+    private String state;
+
+    /**
+     * 订单备注 (Optional)
+     */
+    private String attach;
+
+    /**
+     * 聚合账户 (Required)
+     */
+    private String account;
+
+    /**
+     * 支付方式例:WX、ZFB、YZF、LZF、YLZF (Required)
+     */
+    private String channelId;
+
+    /**
+     * 渠道优惠金额 JSON String, e.g., {"discountAmt":"100"} (Unit: Fen) (Optional)
+     */
+    private String discountInfo;
+
+    /**
+     * 扩展信息 JSON String (Optional, if requested)
+     * Need to be parsed into an object if needed.
+     */
+    private String extendInfo; // Raw JSON string
+
+    /**
+     * 易宝/SAAS分账详情 JSON Object (Optional, if requested)
+     */
+    private JSONObject extend; // Parsed JSON Object
+
+    /**
+     * SAAS分账订单详情 JSON Object (Optional, if requested)
+     */
+    private JSONObject upInfo; // Parsed JSON Object
+
+    /**
+     * 渠道商户订单号 (Optional, if requested)
+     */
+    private String channelMchOrderId;
+
+    /**
+     * 订单管控状态 FROZEN/UN_FROZEN (Optional, if requested via isNeedUpInfo=1)
+     */
+    private String fundControlCsStatus;
+
+    /**
+     * 管控订单解冻时间 yyyy-mm-dd hh:mm:ss (Optional, if requested via isNeedUpInfo=1 and status is UN_FROZEN)
+     */
+    private String csUnFrozenCompleteDate;
+
+    /**
+     * 因公付金额信息 JSON String, e.g., {"invoiceAmount":"0.01","isUseEnterprisePay":false} (Optional, if requested)
+     */
+    private String enterprisePayInfo; // Raw JSON String
+
+    // --- Helper methods to access parsed nested JSON data ---
+
+    /**
+     * Gets parsed DiscountInfo object from discountInfo string.
+     * @return DiscountInfo object or null if parsing fails or discountInfo is null/empty.
+     */
+    public DiscountInfo getParsedDiscountInfo() {
+        if (JSONUtil.isJson(this.discountInfo)) {
+            try {
+                return JSONUtil.toBean(this.discountInfo, DiscountInfo.class);
+            } catch (Exception e) {
+                // Log parsing error if needed
+                return null;
+            }
+        }
+        return null;
+    }
+
+     /**
+     * Gets parsed EnterprisePayInfo object from enterprisePayInfo string.
+     * @return EnterprisePayInfo object or null if parsing fails or enterprisePayInfo is null/empty.
+     */
+    public EnterprisePayInfo getParsedEnterprisePayInfo() {
+         if (JSONUtil.isJson(this.enterprisePayInfo)) {
+            try {
+                return JSONUtil.toBean(this.enterprisePayInfo, EnterprisePayInfo.class);
+            } catch (Exception e) {
+                 // Log parsing error if needed
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Gets parsed ExtendInfo object from extendInfo string.
+     * @return JSONObject or null if parsing fails or extendInfo is null/empty.
+     */
+    public JSONObject getParsedExtendInfo() {
+        if (JSONUtil.isJson(this.extendInfo)) {
+            try {
+                return JSONUtil.parseObj(this.extendInfo);
+            } catch (Exception e) {
+                 // Log parsing error if needed
+                return null;
+            }
+        }
+        return null;
+    }
+
+    // --- Nested DTOs for parsed JSON strings ---
+
+    @Data
+    public static class DiscountInfo {
+        /**
+         * Discount amount in Fen (分)
+         */
+        private String discountAmt; // Keep as String as per API, or use Integer/Long
+
+        public BigDecimal getDiscountAmtYuan() {
+            if (discountAmt != null) {
+                try {
+                    // Assuming discountAmt is in Fen (cents)
+                    return new BigDecimal(discountAmt).divide(new BigDecimal("100"));
+                } catch (NumberFormatException e) {
+                    return null; // Or handle error appropriately
+                }
+            }
+            return null;
+        }
+    }
+
+    @Data
+    public static class EnterprisePayInfo {
+        private String invoiceAmount; // Amount in Yuan (元)
+        private Boolean isUseEnterprisePay;
+
+        public BigDecimal getInvoiceAmountValue() {
+             if (invoiceAmount != null) {
+                 try {
+                     return new BigDecimal(invoiceAmount);
+                 } catch (NumberFormatException e) {
+                     return null; // Or handle error appropriately
+                 }
+             }
+             return null;
+         }
+    }
+
+    /**
+     * Validates the response signature.
+     * IMPORTANT: Implement the actual signature validation logic based on Tongguan's specification.
+     * @param secretKey The secret key.
+     * @return true if the signature is valid, false otherwise.
+     */
+    public boolean isResponseSignValid(String secretKey) {
+
+        System.err.println("WARN: Response signature validation is not implemented!");
+        return true;
+    }
+}

+ 17 - 0
fs-service/src/main/java/com/fs/wx/order/dto/Payer.java

@@ -0,0 +1,17 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Payer {
+
+    @JsonProperty("openid")
+    private String openid;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/wx/order/dto/ShippingItem.java

@@ -0,0 +1,26 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShippingItem {
+
+    @JsonProperty("tracking_no")
+    private String trackingNo;
+
+    @JsonProperty("express_company")
+    private String expressCompany;
+
+    @JsonProperty("item_desc")
+    private String itemDesc;
+
+    @JsonProperty("contact")
+    private Contact contact;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/wx/order/dto/UploadShippingInfoRequest.java

@@ -0,0 +1,42 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UploadShippingInfoRequest {
+
+    @JsonProperty("order_key")
+    private OrderKey orderKey;
+
+    @JsonProperty("logistics_type")
+    private Integer logisticsType;
+
+    @JsonProperty("delivery_mode")
+    private Integer deliveryMode;
+
+    @JsonProperty("is_all_delivered")
+    private Boolean isAllDelivered;
+
+    @JsonProperty("shipping_list")
+    private List<ShippingItem> shippingList;
+
+    @JsonProperty("upload_time")
+    private String uploadTime;
+
+    @JsonProperty("payer")
+    private Payer payer;
+
+    @JsonIgnore
+    private String appid;
+
+}

+ 21 - 0
fs-service/src/main/java/com/fs/wx/order/dto/WeChatApiConfig.java

@@ -0,0 +1,21 @@
+package com.fs.wx.order.dto;
+
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@Getter
+public class WeChatApiConfig {
+
+    @Value("${wechat.api.base-url}")
+    private String baseUrl;
+
+    @Value("${wechat.api.upload-shipping-info}")
+    private String uploadShippingInfoPath;
+
+
+    public String getUploadShippingInfoUrl() {
+        return baseUrl + uploadShippingInfoPath;
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wx/order/dto/WeChatApiResponse.java

@@ -0,0 +1,20 @@
+package com.fs.wx.order.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class WeChatApiResponse {
+
+    @JsonProperty("errcode")
+    private Integer errcode;
+
+    @JsonProperty("errmsg")
+    private String errmsg;
+
+    public boolean isSuccess() {
+        return errcode != null && errcode == 0;
+    }
+}

+ 117 - 0
fs-service/src/main/java/com/fs/wx/order/mapper/FsWxExpressTaskMapper.java

@@ -0,0 +1,117 @@
+package com.fs.wx.order.mapper;
+
+import com.fs.wx.order.domain.FsWxExpressTask;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+/**
+ * 微信同步发货信息定时任务表 Mapper 接口
+ */
+@Mapper
+public interface FsWxExpressTaskMapper {
+
+    /**
+     * 根据ID查询任务
+     * @param id 任务ID
+     * @return FsWxExpressTask 任务实体
+     */
+    @Select("SELECT * FROM f s_wx_express_task WHERE id = #{id}")
+    FsWxExpressTask selectById(@Param("id") Long id);
+
+    /**
+     * 插入新任务
+     * @param task 任务实体
+     * @return 影响行数
+     */
+    @Insert("INSERT INTO fs_wx_express_task (order_code, user_id, data, status, retry_count, max_retries, request_params, request_body, response_body, error_message, create_time, update_time,express_company,express_no,type,appid) " +
+            "VALUES (#{orderCode}, #{userId}, #{data}, #{status}, #{retryCount}, #{maxRetries}, #{requestParams}, #{requestBody}, #{responseBody}, #{errorMessage}, #{createTime}, #{updateTime},#{expressCompany},#{expressNo},#{type},#{appid})")
+    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
+    int insert(FsWxExpressTask task);
+
+    /**
+     * 根据ID更新任务信息
+     * @param task 任务实体
+     * @return 影响行数
+     */
+    @Update("<script>" +
+            "UPDATE fs_wx_express_task " +
+            "<set>" +
+            "  <if test='orderCode != null'>order_code = #{orderCode},</if>" +
+            "  <if test='userId != null'>user_id = #{userId},</if>" +
+            "  <if test='data != null'>data = #{data}::json,</if>" +
+            "  <if test='status != null'>status = #{status},</if>" +
+            "  <if test='retryCount != null'>retry_count = #{retryCount},</if>" +
+            "  <if test='maxRetries != null'>max_retries = #{maxRetries},</if>" +
+            "  <if test='requestParams != null'>request_params = #{requestParams},</if>" +
+            "  <if test='requestBody != null'>request_body = #{requestBody},</if>" +
+            "  <if test='responseBody != null'>response_body = #{responseBody},</if>" +
+            "  <if test='errorMessage != null'>error_message = #{errorMessage},</if>" +
+            "  <if test='type != null'>type = #{type},</if>" +
+            "</set>" +
+            "WHERE id = #{id}" +
+            "</script>")
+    int updateById(FsWxExpressTask task);
+
+    /**
+     * 根据ID删除任务
+     * @param id 任务ID
+     * @return 影响行数
+     */
+    @Delete("DELETE FROM fs_wx_express_task WHERE id = #{id}")
+    int deleteById(@Param("id") Long id);
+
+    /**
+     * 根据状态查询任务列表
+     * @param status 任务状态
+     * @return List<FsWxExpressTask> 任务列表
+     */
+    @Select("SELECT * FROM fs_wx_express_task WHERE status = #{status}")
+    List<FsWxExpressTask> selectByStatus(@Param("status") Integer status);
+
+
+    /**
+     * 查询待处理数据
+     * @return
+     */
+    @Select("SELECT * FROM fs_wx_express_task WHERE retry_count < 3 AND status in (0,3)")
+    List<FsWxExpressTask> selectPendingData();
+    @Update("<script>" +
+            "<foreach collection='list' item='task' separator=';'>" +
+            " UPDATE fs_wx_express_task" +
+            " SET" +
+            "  order_code = #{task.orderCode}," +
+            "  user_id = #{task.userId}," +
+            "  data = #{task.data}," +
+            "  status = #{task.status}," +
+            "  retry_count = #{task.retryCount}," +
+            "  max_retries = #{task.maxRetries}," +
+            "  request_params = #{task.requestParams}," +
+            "  request_body = #{task.requestBody}," +
+            "  response_body = #{task.responseBody}," +
+            "  error_message = #{task.errorMessage}," +
+            "  update_time = now()" +
+            " WHERE id = #{task.id}" +
+            "</foreach>" +
+            "</script>")
+    void batchUpdate(List<FsWxExpressTask> fsWxExpressTasks);
+
+    /**
+     * 批量插入新任务
+     * @param tasks 任务实体列表
+     * @return 影响行数 (Optional: change void to int if needed)
+     */
+    @Insert("<script>" +
+            "INSERT INTO fs_wx_express_task " +
+            "(order_code, user_id, data, status, retry_count, max_retries, request_params, request_body, response_body, error_message, create_time, update_time, pay_code,appid) " +
+            "VALUES " +
+            "<foreach collection='tasks' item='task' separator=','>" +
+            "(" +
+            "#{task.orderCode}, #{task.userId}, #{task.data}, #{task.status}, #{task.retryCount}, #{task.maxRetries}, " + // Keep ::json if PostgreSQL
+            "#{task.requestParams}, #{task.requestBody}, #{task.responseBody}, #{task.errorMessage}, " +
+            "#{task.createTime}, #{task.updateTime},#{task.payCode},#{task.appid}" +
+            ")" +
+            "</foreach>" +
+            "</script>")
+    void insertBatch(List<FsWxExpressTask> tasks);
+}

+ 30 - 0
fs-service/src/main/java/com/fs/wx/order/service/ExpressToWxHolder.java

@@ -0,0 +1,30 @@
+package com.fs.wx.order.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class ExpressToWxHolder {
+    private static ApplicationContext applicationContext;
+
+    @Autowired
+    public void setApplicationContext(ApplicationContext applicationContext) {
+        ExpressToWxHolder.applicationContext = applicationContext;
+    }
+
+    public static ExpressToWxService findBest(Integer type,String orderCode) {
+        String[] beanNames = applicationContext.getBeanNamesForType(ExpressToWxService.class);
+
+        for (String beanName : beanNames) {
+            ExpressToWxService handler = (ExpressToWxService) applicationContext.getBean(beanName);
+            if (handler.support(type)) {
+                handler.setOrderCode(orderCode);
+                return handler;
+            }
+        }
+        throw new IllegalArgumentException(String.format(String.format("对应类型 %d 没有被找到!",type)));
+    }
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wx/order/service/ExpressToWxService.java

@@ -0,0 +1,27 @@
+package com.fs.wx.order.service;
+
+import lombok.Getter;
+
+@Getter
+public abstract class ExpressToWxService {
+
+    private String orderCode;
+
+
+    public void setOrderCode(String orderCode) {
+        this.orderCode = orderCode;
+    }
+
+    public abstract String getTransactionId();
+
+
+    public abstract String getUserPhone();
+
+
+    public abstract String getOrderGoodsInfo();
+
+    public abstract String getExpressCompany();
+    public abstract String getExpressNo();
+
+    public abstract boolean support(Integer type);
+}

+ 20 - 0
fs-service/src/main/java/com/fs/wx/order/service/OrderQueryService.java

@@ -0,0 +1,20 @@
+package com.fs.wx.order.service;
+
+
+import com.fs.wx.order.dto.OrderQueryRequest;
+import com.fs.wx.order.dto.OrderQueryResponse;
+
+public interface OrderQueryService {
+
+    /**
+     * Queries an order using either lowOrderId or upOrderId.
+     *
+     * @param request The request object containing query parameters.
+     *                The 'sign' field will be calculated internally.
+     * @return The order query response.
+     * @throws IllegalArgumentException if required parameters are missing.
+     * @throws RuntimeException if the API call fails or returns an error status.
+     */
+    OrderQueryResponse queryOrder(OrderQueryRequest request);
+
+}

+ 128 - 0
fs-service/src/main/java/com/fs/wx/order/service/ShippingService.java

@@ -0,0 +1,128 @@
+package com.fs.wx.order.service;
+
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.http.*;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.wx.order.dto.UploadShippingInfoRequest;
+import com.fs.wx.order.dto.WeChatApiConfig;
+import com.fs.wx.order.dto.WeChatApiResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Slf4j
+@Service
+public class ShippingService {
+
+
+    private final WeChatApiConfig weChatApiConfig;
+
+    public ShippingService(WeChatApiConfig weChatApiConfig) {
+        this.weChatApiConfig = weChatApiConfig;
+    }
+
+    /**
+     * 调用微信 API 上传发货信息 (使用 Hutool HttpUtil)
+     * @param request 发货信息请求体
+     * @return 微信 API 的响应
+     */
+    public WeChatApiResponse uploadShippingInfo(UploadShippingInfoRequest request) {
+        WeChatAuthService weChatAuthService = WeChatAuthFactory.getWeChatAuthService(request.getAppid());
+        String accessToken = weChatAuthService.getAccessToken(false);
+        if (accessToken == null) {
+            log.error("获取微信 Access Token 失败");
+            WeChatApiResponse errorResponse = new WeChatApiResponse();
+            errorResponse.setErrcode(-3); // 自定义错误码,表示获取Token失败
+            errorResponse.setErrmsg("获取微信 Access Token 失败");
+            return errorResponse;
+        }
+
+        String url = UriComponentsBuilder.fromHttpUrl(weChatApiConfig.getUploadShippingInfoUrl())
+                .queryParam("access_token", accessToken)
+                .toUriString();
+
+        log.debug("请求微信上传发货接口 Appid: {} URL: {}", request.getAppid(),url);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        String requestBodyJson = null;
+        try {
+            requestBodyJson = objectMapper.writeValueAsString(request);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+
+        log.debug("请求体 JSON: {}", requestBodyJson);
+
+
+        HttpResponse httpResponse = null;
+        try {
+            HttpRequest httpRequest = HttpUtil.createPost(url)
+                    .header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
+                    .body(requestBodyJson)
+                    .timeout(10000);
+
+            httpResponse = httpRequest.execute();
+
+            int statusCode = httpResponse.getStatus();
+            String responseBodyString = httpResponse.body();
+
+            log.info("微信接口响应状态: {}", statusCode);
+            log.info("微信接口响应体: {}", responseBodyString);
+
+            if (httpResponse.isOk()) {
+                WeChatApiResponse weChatApiResponse = JSONUtil.toBean(responseBodyString, WeChatApiResponse.class);
+
+                if (!weChatApiResponse.isSuccess()) {
+                    log.warn("微信接口返回业务错误: code={}, message={}", weChatApiResponse.getErrcode(), weChatApiResponse.getErrmsg());
+                    if(ObjectUtil.equal(weChatApiResponse.getErrcode(),40001)) {
+                        log.info("token缓存失效,清除token,等待下次执行...");
+                        weChatAuthService.clearToken();
+                    }
+                }
+                return weChatApiResponse;
+
+            } else {
+                log.error("调用微信接口收到非成功状态码: {}", statusCode);
+                try {
+                    WeChatApiResponse errorResponse = JSONUtil.toBean(responseBodyString, WeChatApiResponse.class);
+                    if (errorResponse.getErrcode() == null) {
+                        errorResponse.setErrcode(statusCode);
+                        errorResponse.setErrmsg("微信接口HTTP错误,状态码: " + statusCode + ", 响应体: " + responseBodyString);
+                    }
+                    return errorResponse;
+                } catch (Exception parseEx) {
+                    log.warn("无法将微信错误响应体解析为JSON: {}", responseBodyString, parseEx);
+                    WeChatApiResponse errorResponse = new WeChatApiResponse();
+                    errorResponse.setErrcode(statusCode);
+                    errorResponse.setErrmsg("调用微信接口失败,状态码: " + statusCode + ", 原始响应体: " + responseBodyString);
+                    return errorResponse;
+                }
+            }
+        } catch (HttpException e) {
+            log.error("调用微信接口发生HTTP异常: {}", e.getMessage(), e);
+            WeChatApiResponse errorResponse = new WeChatApiResponse();
+            errorResponse.setErrcode(-1);
+            String detailedMessage = ExceptionUtil.getMessage(e);
+            errorResponse.setErrmsg("调用微信接口时发生HTTP异常: " + detailedMessage);
+            if (httpResponse != null) {
+                errorResponse.setErrmsg(errorResponse.getErrmsg() + ", HTTP状态码: " + httpResponse.getStatus());
+            }
+            return errorResponse;
+        } catch (Exception e) {
+            log.error("调用微信接口时发生意外错误", e);
+            WeChatApiResponse errorResponse = new WeChatApiResponse();
+            errorResponse.setErrcode(-2);
+            errorResponse.setErrmsg("调用微信接口时发生内部服务器错误: " + e.getMessage());
+            return errorResponse;
+        }finally {
+            if(httpResponse != null) {
+                httpResponse.close();
+            }
+        }
+    }
+}

+ 46 - 0
fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthFactory.java

@@ -0,0 +1,46 @@
+package com.fs.wx.order.service;
+
+import com.fs.pay.domain.PaymentMiniProgramConfig;
+import com.fs.pay.mapper.PaymentMiniProgramConfigMapper;
+import com.fs.wx.order.service.impl.InMemoryWeChatAuthServiceImpl;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.SmartInitializingSingleton;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class WeChatAuthFactory implements ApplicationContextAware, SmartInitializingSingleton {
+    private static ApplicationContext applicationContext;
+    private final static Map<String,WeChatAuthService> weChatAuthServices = new ConcurrentHashMap<>();
+
+    @Override
+    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
+        WeChatAuthFactory.applicationContext = applicationContext;
+    }
+
+    @Override
+    public void afterSingletonsInstantiated() {
+        PaymentMiniProgramConfigMapper mapper = applicationContext.getBean(PaymentMiniProgramConfigMapper.class);
+        List<PaymentMiniProgramConfig> configs = mapper.selectAll();
+        for (PaymentMiniProgramConfig config : configs) {
+            WeChatAuthService weChatAuthService = new InMemoryWeChatAuthServiceImpl(config.getAppid(),config.getAppSecret());
+            weChatAuthServices.put(config.getAppid(),weChatAuthService);
+        }
+    }
+
+    /**
+     * 通过appid获取WeChatAuthService
+     * @param appid 小程序ID
+     * @return WeChatAuthService
+     */
+    public static WeChatAuthService getWeChatAuthService(String appid){
+        return weChatAuthServices.get(appid);
+    }
+
+}

+ 22 - 0
fs-service/src/main/java/com/fs/wx/order/service/WeChatAuthService.java

@@ -0,0 +1,22 @@
+package com.fs.wx.order.service;
+
+public interface WeChatAuthService {
+    void clearToken();
+
+    /**
+     * 获取有效的微信小程序 Access Token
+     * @param forceRefresh 是否强制刷新,忽略缓存
+     * @return Access Token
+     * @throws RuntimeException 如果获取失败
+     */
+    String getAccessToken(boolean forceRefresh);
+
+    /**
+     * 获取有效的微信小程序 Access Token (优先使用缓存)
+     * @return Access Token
+     * @throws RuntimeException 如果获取失败
+     */
+    default String getAccessToken() {
+        return getAccessToken(true);
+    }
+}

+ 114 - 0
fs-service/src/main/java/com/fs/wx/order/service/impl/InMemoryWeChatAuthServiceImpl.java

@@ -0,0 +1,114 @@
+package com.fs.wx.order.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fs.wx.order.service.WeChatAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InMemoryWeChatAuthServiceImpl implements WeChatAuthService {
+
+    private static final Logger log = LoggerFactory.getLogger(InMemoryWeChatAuthServiceImpl.class);
+    private String cachedToken = null;
+    private long expiryTime = 0; // Token 过期时间戳 (ms)
+
+    private final String appId;
+    private final String appSecret;
+
+    private final String tokenUrlFormat = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+
+
+    public InMemoryWeChatAuthServiceImpl(String appId, String appSecret) {
+        this.appId = appId;
+        this.appSecret = appSecret;
+    }
+
+    @Override
+    public synchronized void clearToken(){
+        log.info("清除token缓存...");
+        cachedToken = null;
+        expiryTime = 0;
+    }
+
+    @Override
+    public synchronized String getAccessToken(boolean forceRefresh) {
+        long now = System.currentTimeMillis();
+        if (!forceRefresh && cachedToken != null && now < expiryTime) {
+            log.debug("Using cached access token.");
+            return cachedToken;
+        }
+
+        if (StrUtil.hasBlank(appId, appSecret)) {
+            log.error("appId或者appSecret不存在!");
+            throw new RuntimeException("appId或者appSecret不存在!");
+        }
+
+
+        String url = String.format(tokenUrlFormat, appId, appSecret);
+        log.info(url);
+        HttpResponse httpResponse = null;
+        String body = null;
+
+        try {
+            httpResponse = HttpRequest.get(url)
+                    .timeout(5000)
+                    .execute();
+
+            body = httpResponse.body();
+
+            if (!httpResponse.isOk()) {
+                log.error("获取accessToken失败!. Status: {}, Body: {}", httpResponse.getStatus(), body);
+                String errorMsg = parseErrorMsg(body);
+                throw new RuntimeException("获取accessToken失败! Status: " + httpResponse.getStatus() + ", Message: " + errorMsg);
+            }
+
+            if (StrUtil.isBlank(body)) {
+                log.error("获取accessToken失败!.  URL: {}", url);
+                throw new RuntimeException("获取accessToken失败!");
+            }
+
+            JSONObject responseJson = JSONUtil.parseObj(body);
+
+            if (responseJson.containsKey("access_token")) {
+                cachedToken = responseJson.getStr("access_token");
+                if(StrUtil.isBlank(cachedToken)){
+                    log.error("获取accessToken失败!response: {}", body);
+                    throw new RuntimeException("获取accessToken失败!");
+                }
+
+                long expiresInSeconds = responseJson.getLong("expires_in", 7200L);
+                expiryTime = now + (expiresInSeconds - 120) * 1000;
+                log.info("获取accessToken获取成功 {}",cachedToken);
+                return cachedToken;
+            } else {
+                String errorMsg = responseJson.getStr("errmsg", "Unknown error: access_token missing in response");
+                log.error("Failed to fetch access token, 'access_token' key missing. Response: {}", body);
+                throw new RuntimeException("Failed to fetch access token: " + errorMsg);
+            }
+        } catch (Exception e) {
+            cachedToken = null;
+            expiryTime = 0;
+            log.error("Error fetching access token from URL: {}", url, e);
+            throw new RuntimeException("Error fetching access token: " + e.getMessage(), e);
+        }finally {
+            if(httpResponse != null) {
+                httpResponse.close();
+            }
+        }
+    }
+
+    private String parseErrorMsg(String jsonBody) {
+        if (StrUtil.isBlank(jsonBody)) {
+            return "返回为空!";
+        }
+        try {
+            JSONObject json = JSONUtil.parseObj(jsonBody);
+            return json.getStr("errmsg", "Unknown error in response body");
+        } catch (Exception e) {
+            return "返回解析失败!";
+        }
+    }
+}

+ 107 - 0
fs-service/src/main/java/com/fs/wx/order/service/impl/LiveExpressToWxService.java

@@ -0,0 +1,107 @@
+package com.fs.wx.order.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.his.domain.FsStorePayment;
+import com.fs.his.mapper.FsStoreOrderMapper;
+import com.fs.his.mapper.FsStorePaymentMapper;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveOrderMapper;
+import com.fs.live.mapper.LiveOrderPaymentMapper;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.wx.order.service.ExpressToWxService;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE;
+
+@Service
+@Scope(value = SCOPE_PROTOTYPE)
+public class LiveExpressToWxService extends ExpressToWxService {
+    @Autowired
+    private LiveOrderMapper liveOrderMapper;
+    @Autowired
+    private LiveOrderPaymentMapper liveOrderPaymentMapper;
+
+    private LiveOrder liveOrder;
+    private LiveOrderPayment liveOrderPayment ;
+
+    @Override
+    public void setOrderCode(String orderCode) {
+        super.setOrderCode(orderCode);
+        this.liveOrder = liveOrderMapper.selectLiveOrderByOrderCode(getOrderCode());
+        if(ObjectUtil.isNull(liveOrder)) {
+            throw new IllegalArgumentException(String.format("该订单 %s 未找到!",getOrderCode()));
+        }
+        LiveOrderPayment fsStorePayments = liveOrderPaymentMapper.selectLiveOrderLatestPayByOrderId(liveOrder.getOrderId());
+        if(fsStorePayments == null){
+            throw new IllegalArgumentException(String.format("该订单 %s 未找到对应支付记录!", getOrderCode()));
+        }
+    }
+
+
+    @Override
+    public String getTransactionId() {
+        return liveOrderPayment.getBankTransactionId();
+    }
+
+    @Override
+    public String getUserPhone() {
+        return liveOrder.getUserPhone();
+    }
+
+    @Override
+    public String getOrderGoodsInfo() {
+        return getOrderGoodsInfo(liveOrder);
+    }
+
+    @Override
+    public String getExpressCompany() {
+
+        return liveOrder.getDeliveryCode();
+    }
+
+    @Override
+    public String getExpressNo() {
+
+        return liveOrder.getDeliverySn();
+    }
+
+    @Override
+    public boolean support(Integer type) {
+        return 1 == type;
+    }
+
+
+    /**
+     * 获取订单商品信息
+     * @return
+     */
+    private String getOrderGoodsInfo(LiveOrder order){
+        StringBuilder title = new StringBuilder();
+        // 如果是套餐
+//        if(ObjectUtil.equal(order.getIsPackage(),1)){
+//            String packageJson = order.getPackageJson();
+//            JSONObject jsonObject = JSON.parseObject(packageJson);
+//            title = new StringBuilder(jsonObject.getString("title"));
+//        } else {
+            String itemJson = order.getItemJson();
+            com.alibaba.fastjson.JSONArray arrays = JSON.parseArray(itemJson);
+            for(int i=0;i<arrays.size();i++){
+                JSONObject jsonObject = arrays.getJSONObject(i);
+                String jsonInfo = jsonObject.getString("jsonInfo");
+                JSONObject jsonObject1 = JSON.parseObject(jsonInfo);
+                String productName = jsonObject1.getString("productName");
+                title.append(productName).append("\n");
+            }
+//        }
+        return title.toString();
+    }
+}

+ 107 - 0
fs-service/src/main/java/com/fs/wx/order/service/impl/ShopExpressToWxService.java

@@ -0,0 +1,107 @@
+package com.fs.wx.order.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.his.domain.FsStorePayment;
+import com.fs.his.mapper.FsStoreOrderMapper;
+import com.fs.his.mapper.FsStorePaymentMapper;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
+import com.fs.wx.order.service.ExpressToWxService;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Scope;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE;
+
+@Service
+@Scope(value = SCOPE_PROTOTYPE)
+public class ShopExpressToWxService extends ExpressToWxService {
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderMapper;
+    @Autowired
+    private FsStorePaymentScrmMapper fsStorePaymentMapper;
+
+    private FsStoreOrderScrm fsStoreOrder;
+    private FsStorePaymentScrm fsStorePayment;
+
+    @Override
+    public void setOrderCode(String orderCode) {
+        super.setOrderCode(orderCode);
+        this.fsStoreOrder = fsStoreOrderMapper.selectFsStoreOrderByOrderCode(getOrderCode());
+        if(ObjectUtil.isNull(fsStoreOrder)) {
+            throw new IllegalArgumentException(String.format("该订单 %s 未找到!",getOrderCode()));
+        }
+        List<FsStorePaymentScrm> fsStorePayments = fsStorePaymentMapper.selectFsStorePaymentByOrderId(fsStoreOrder.getId());
+        if(CollectionUtils.isEmpty(fsStorePayments)){
+            throw new IllegalArgumentException(String.format("该订单 %s 未找到对应支付记录!", getOrderCode()));
+        }
+        fsStorePayment = fsStorePayments.get(0);
+    }
+
+
+    @Override
+    public String getTransactionId() {
+        return fsStorePayment.getBankTransactionId();
+    }
+
+    @Override
+    public String getUserPhone() {
+        return fsStoreOrder.getUserPhone();
+    }
+
+    @Override
+    public String getOrderGoodsInfo() {
+        return getOrderGoodsInfo(fsStoreOrder);
+    }
+
+    @Override
+    public String getExpressCompany() {
+
+        return fsStoreOrder.getDeliveryCode();
+    }
+
+    @Override
+    public String getExpressNo() {
+
+        return fsStoreOrder.getDeliverySn();
+    }
+
+    @Override
+    public boolean support(Integer type) {
+        return 0 == type;
+    }
+
+
+    /**
+     * 获取订单商品信息
+     * @return
+     */
+    private String getOrderGoodsInfo(FsStoreOrderScrm order){
+        StringBuilder title = new StringBuilder();
+        // 如果是套餐
+//        if(ObjectUtil.equal(order.getIsPackage(),1)){
+//            String packageJson = order.getPackageJson();
+//            JSONObject jsonObject = JSON.parseObject(packageJson);
+//            title = new StringBuilder(jsonObject.getString("title"));
+//        } else {
+            String itemJson = order.getItemJson();
+            com.alibaba.fastjson.JSONArray arrays = JSON.parseArray(itemJson);
+            for(int i=0;i<arrays.size();i++){
+                JSONObject jsonObject = arrays.getJSONObject(i);
+                String jsonInfo = jsonObject.getString("jsonInfo");
+                JSONObject jsonObject1 = JSON.parseObject(jsonInfo);
+                String productName = jsonObject1.getString("productName");
+                title.append(productName).append("\n");
+            }
+//        }
+        return title.toString();
+    }
+}

+ 5 - 0
fs-service/src/main/resources/application-common.yml

@@ -138,3 +138,8 @@ image:
   storage:
     local-path: C:\logoFile\logo.jpg
     server-path: C:\logoFile\logo.jpg
+# application.properties
+wechat:
+  api:
+    base-url: https://api.weixin.qq.com
+    upload-shipping-info: /wxa/sec/order/upload_shipping_info

+ 1 - 1
fs-service/src/main/resources/application-config-druid-kyt.yml

@@ -64,7 +64,7 @@ fs :
   h5CommonApi: http://172.17.0.10:8010
   jwt:
     # 加密秘钥
-    secret: f4e2e52034348f86b67cde581c0f9eb5
+    secret: f4k2y52034348t86b67cde581c0f9eb5
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken

+ 1 - 1
fs-service/src/main/resources/application-config-druid-qdtst.yml

@@ -64,7 +64,7 @@ fs :
   h5CommonApi: http://10.206.0.16:8010
   jwt:
     # 加密秘钥
-    secret: f4q2d52034348t86b678s1581c0f9eb5
+    secret: f4e2e52034348f86b67cde581c0f9eb5
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken

+ 5 - 0
fs-service/src/main/resources/mapper/course/FsUserCoursePeriodDaysMapper.xml

@@ -243,4 +243,9 @@
     <select id="selectFsUserCoursePeriodDaysForLastById" resultType="java.lang.Long">
        select id from fs_user_course_period_days where del_flag ='0' and period_id = #{periodId} and lesson &gt;= #{lesson} order by lesson
     </select>
+    <select id="selectFsUserCoursePeriodDaysByCourseId"
+            resultType="com.fs.course.domain.FsUserCoursePeriodDays">
+        <include refid="selectFsUserCoursePeriodDaysVo"/>
+                 where del_flag ='0' and course_id = #{courseId}
+    </select>
 </mapper>

+ 1 - 0
fs-service/src/main/resources/mapper/live/LiveAutoTaskMapper.xml

@@ -33,6 +33,7 @@
             <if test="triggerType != null "> and trigger_type = #{triggerType}</if>
             <if test="triggerValue != null"> and trigger_value = #{triggerValue}</if>
             <if test="absValue != null"> and abs_value = #{absValue}</if>
+            <if test="taskType != null"> and task_type = #{taskType}</if>
             <if test="content != null  and content != ''"> and content = #{content}</if>
             <if test="status != null "> and status = #{status}</if>
             <if test="createTime != null "> and create_time = #{createTime}</if>

+ 53 - 11
fs-service/src/main/resources/mapper/live/LiveDataMapper.xml

@@ -300,10 +300,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 WHEN l.live_type = 3 AND lwu.online_seconds >= COALESCE(video_duration.total_duration, 0) AND video_duration.total_duration > 0 
                 THEN lwu.user_id 
             END) AS playbackCompletedCourses,
-            COALESCE(SUM(lo.pay_price), 0) AS gmv,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.user_id END) AS paidUsers,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.order_id END) AS paidOrders,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.order_id END) AS salesCount
+            COALESCE((
+                SELECT SUM(pay_price) 
+                FROM live_order 
+                WHERE live_id IN 
+                <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+                    #{liveId}
+                </foreach>
+                AND is_pay = '1'
+            ), 0) AS gmv,
+            COALESCE((
+                SELECT COUNT(DISTINCT user_id) 
+                FROM live_order 
+                WHERE live_id IN 
+                <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+                    #{liveId}
+                </foreach>
+                AND is_pay = '1'
+            ), 0) AS paidUsers,
+            COALESCE((
+                SELECT COUNT(DISTINCT order_id) 
+                FROM live_order 
+                WHERE live_id IN 
+                <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+                    #{liveId}
+                </foreach>
+                AND is_pay = '1'
+            ), 0) AS paidOrders,
+            COALESCE((
+                SELECT COUNT(DISTINCT order_id) 
+                FROM live_order 
+                WHERE live_id IN 
+                <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+                    #{liveId}
+                </foreach>
+                AND is_pay = '1'
+            ), 0) AS salesCount
         FROM live l
         LEFT JOIN live_watch_user lwu ON l.live_id = lwu.live_id
         LEFT JOIN (
@@ -312,7 +344,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHERE video_type IN (1, 2)
             GROUP BY live_id
         ) video_duration ON l.live_id = video_duration.live_id
-        LEFT JOIN live_order lo ON l.live_id = lo.live_id
         WHERE l.live_id IN
         <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
             #{liveId}
@@ -345,10 +376,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 WHEN l.live_type = 3 AND lwu.online_seconds >= COALESCE(video_duration.total_duration, 0) AND video_duration.total_duration > 0 
                 THEN lwu.user_id 
             END) AS playbackCompletedCourses,
-            COALESCE(SUM(lo.pay_price), 0) AS gmv,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.user_id END) AS paidUsers,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.order_id END) AS paidOrders,
-            COUNT(DISTINCT CASE WHEN lo.is_pay = '1' THEN lo.order_id END) AS salesCount
+            COALESCE(order_stats.gmv, 0) AS gmv,
+            COALESCE(order_stats.paidUsers, 0) AS paidUsers,
+            COALESCE(order_stats.paidOrders, 0) AS paidOrders,
+            COALESCE(order_stats.salesCount, 0) AS salesCount
         FROM live l
         LEFT JOIN live_watch_user lwu ON l.live_id = lwu.live_id
         LEFT JOIN (
@@ -357,12 +388,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHERE video_type IN (1, 2)
             GROUP BY live_id
         ) video_duration ON l.live_id = video_duration.live_id
-        LEFT JOIN live_order lo ON l.live_id = lo.live_id
+        LEFT JOIN (
+            SELECT 
+                live_id,
+                SUM(pay_price) AS gmv,
+                COUNT(DISTINCT user_id) AS paidUsers,
+                COUNT(DISTINCT order_id) AS paidOrders,
+                COUNT(DISTINCT order_id) AS salesCount
+            FROM live_order
+            WHERE is_pay = '1'
+            GROUP BY live_id
+        ) order_stats ON l.live_id = order_stats.live_id
         WHERE l.live_id IN
         <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
             #{liveId}
         </foreach>
-        GROUP BY l.live_id, l.live_name, l.live_type, l.status, l.start_time, l.finish_time
+        GROUP BY l.live_id, l.live_name, l.live_type, l.status, l.start_time, l.finish_time, 
+                 order_stats.gmv, order_stats.paidUsers, order_stats.paidOrders, order_stats.salesCount
         ORDER BY l.start_time DESC
     </select>
 </mapper>

+ 42 - 1
fs-service/src/main/resources/mapper/live/LiveWatchUserMapper.xml

@@ -18,10 +18,13 @@
         <result property="onlineSeconds"    column="online_seconds"    />
         <result property="globalVisible"    column="global_visible"    />
         <result property="singleVisible"    column="single_visible"    />
+        <result property="liveFlag"    column="live_flag"    />
+        <result property="replayFlag"    column="replay_flag"    />
+        <result property="location"    column="location"    />
     </resultMap>
 
     <sql id="selectLiveWatchUserVo">
-        select id, live_id,user_id, msg_status, online, create_time, create_by, update_by, update_time, remark,online_seconds,global_visible,single_visible from live_watch_user
+        select id, live_id,user_id, msg_status, online, create_time, create_by, update_by, update_time, remark,online_seconds,global_visible,single_visible,live_flag,replay_flag,location from live_watch_user
     </sql>
 
     <select id="selectLiveWatchUserList" parameterType="LiveWatchUser" resultMap="LiveWatchUserResult">
@@ -125,6 +128,9 @@
             <if test="onlineSeconds != null">online_seconds,</if>
             <if test="globalVisible != null">global_visible,</if>
             <if test="singleVisible != null">single_visible,</if>
+            <if test="liveFlag != null">live_flag,</if>
+            <if test="replayFlag != null">replay_flag,</if>
+            <if test="location != null">location,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="liveId != null">#{liveId},</if>
@@ -139,6 +145,9 @@
             <if test="onlineSeconds != null">#{onlineSeconds},</if>
             <if test="globalVisible != null">#{globalVisible},</if>
             <if test="singleVisible != null">#{singleVisible},</if>
+            <if test="liveFlag != null">#{liveFlag},</if>
+            <if test="replayFlag != null">#{replayFlag},</if>
+            <if test="location != null">#{location},</if>
         </trim>
     </insert>
 
@@ -157,6 +166,9 @@
             <if test="onlineSeconds != null">online_seconds = #{onlineSeconds},</if>
             <if test="globalVisible != null">global_visible = #{globalVisible},</if>
             <if test="singleVisible != null">single_visible = #{singleVisible},</if>
+            <if test="liveFlag != null">live_flag = #{liveFlag},</if>
+            <if test="replayFlag != null">replay_flag = #{replayFlag},</if>
+            <if test="location != null">location = #{location},</if>
         </trim>
         where id = #{id}
     </update>
@@ -186,4 +198,33 @@
               AND DATE(lrr.create_time) = DATE(#{now})
         )
     </select>
+
+    <!-- 根据唯一索引查询:live_id, user_id, live_flag, replay_flag -->
+    <select id="selectByUniqueIndex" resultMap="LiveWatchUserResult">
+        <include refid="selectLiveWatchUserVo"/>
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND live_flag = #{liveFlag}
+          AND replay_flag = #{replayFlag}
+        LIMIT 1
+    </select>
+
+    <!-- 根据唯一索引插入或更新(ON DUPLICATE KEY UPDATE) -->
+    <insert id="insertOrUpdateByUniqueIndex" parameterType="LiveWatchUser">
+        INSERT INTO live_watch_user (
+            live_id, user_id, live_flag, replay_flag,
+             msg_status, online, location,
+            create_time, update_time
+        ) VALUES (
+            #{liveId}, #{userId}, #{liveFlag}, #{replayFlag},
+             #{msgStatus}, #{online}, #{location},
+            #{createTime}, #{updateTime}
+        )
+        ON DUPLICATE KEY UPDATE
+
+            msg_status = VALUES(msg_status),
+            online = VALUES(online),
+            location = VALUES(location),
+            update_time = VALUES(update_time)
+    </insert>
 </mapper>

+ 195 - 0
fs-service/src/main/resources/mapper/pay/PaymentMiniProgramConfigMapper.xml

@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.pay.mapper.PaymentMiniProgramConfigMapper">
+
+    <resultMap type="PaymentMiniProgramConfig" id="PaymentMiniProgramConfigResult">
+        <result property="id"    column="id"    />
+        <result property="payType"    column="pay_type"    />
+        <result property="appName"    column="appname"    />
+        <result property="appid"    column="appid"    />
+        <result property="appSecret"    column="appsecret"    />
+        <result property="ybMerchantNo"    column="yb_merchant_no"    />
+        <result property="ybKey"    column="yb_key"    />
+        <result property="ybNotifyUrl"    column="yb_notify_url"    />
+        <result property="tzhMerchantNo"    column="tzh_merchant_no"    />
+        <result property="tzhAppsecret"    column="tzh_appsecret"    />
+        <result property="tzhPrivateKey"    column="tzh_private_key"    />
+        <result property="tzhPublicKey"    column="tzh_public_key"    />
+        <result property="tzhAppkey"    column="tzh_appkey"    />
+        <result property="tzhPayNotifyUrl"    column="tzh_pay_notify_url"    />
+        <result property="tzhRefundNotifyUrl"    column="tzh_refund_notify_url"    />
+        <result property="tzhSplitNotifyUrl"    column="tzh_split_notify_url"    />
+        <result property="wxMerchantNo"    column="wx_merchant_no"    />
+        <result property="wxKey"    column="wx_key"    />
+        <result property="hfProductNo"    column="hf_product_no"    />
+        <result property="hfSystemNo"    column="hf_system_no"    />
+        <result property="hfMerchantNo"    column="hf_merchant_no"    />
+        <result property="hfPrivateKey"    column="hf_private_key"    />
+        <result property="hfPublicKey"    column="hf_public_key"    />
+        <result property="hfPayNotifyUrl"    column="hf_pay_notify_url"    />
+        <result property="hfLargePayNotifyUrl"    column="hf_large_pay_notify_url"    />
+        <result property="hfRefundNotifyUrl"    column="hf_refund_notify_url"    />
+        <result property="hfLargeRefundNotifyUrl"    column="hf_large_refund_notify_url"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectPaymentMiniProgramConfigVo">
+        select id, pay_type, appid,appname,appsecret, yb_merchant_no, yb_key, yb_notify_url, tzh_merchant_no, tzh_appsecret, tzh_private_key, tzh_public_key, tzh_appkey, tzh_pay_notify_url, tzh_refund_notify_url, tzh_split_notify_url, wx_merchant_no, wx_key, hf_product_no, hf_system_no, hf_merchant_no, hf_private_key, hf_public_key, hf_pay_notify_url, hf_large_pay_notify_url, hf_refund_notify_url, hf_large_refund_notify_url, status, create_time, update_time from payment_mini_program_config
+    </sql>
+
+    <select id="selectPaymentMiniProgramConfigList" parameterType="PaymentMiniProgramConfig" resultMap="PaymentMiniProgramConfigResult">
+        <include refid="selectPaymentMiniProgramConfigVo"/>
+        <where>
+            <if test="payType != null  and payType != ''"> and pay_type = #{payType}</if>
+            <if test="appid != null  and appid != ''"> and appid = #{appid}</if>
+            <if test="ybMerchantNo != null  and ybMerchantNo != ''"> and yb_merchant_no = #{ybMerchantNo}</if>
+            <if test="ybKey != null  and ybKey != ''"> and yb_key = #{ybKey}</if>
+            <if test="ybNotifyUrl != null  and ybNotifyUrl != ''"> and yb_notify_url = #{ybNotifyUrl}</if>
+            <if test="tzhMerchantNo != null  and tzhMerchantNo != ''"> and tzh_merchant_no = #{tzhMerchantNo}</if>
+            <if test="tzhAppsecret != null  and tzhAppsecret != ''"> and tzh_appsecret = #{tzhAppsecret}</if>
+            <if test="tzhPrivateKey != null  and tzhPrivateKey != ''"> and tzh_private_key = #{tzhPrivateKey}</if>
+            <if test="tzhPublicKey != null  and tzhPublicKey != ''"> and tzh_public_key = #{tzhPublicKey}</if>
+            <if test="tzhAppkey != null  and tzhAppkey != ''"> and tzh_appkey = #{tzhAppkey}</if>
+            <if test="tzhPayNotifyUrl != null  and tzhPayNotifyUrl != ''"> and tzh_pay_notify_url = #{tzhPayNotifyUrl}</if>
+            <if test="tzhRefundNotifyUrl != null  and tzhRefundNotifyUrl != ''"> and tzh_refund_notify_url = #{tzhRefundNotifyUrl}</if>
+            <if test="tzhSplitNotifyUrl != null  and tzhSplitNotifyUrl != ''"> and tzh_split_notify_url = #{tzhSplitNotifyUrl}</if>
+            <if test="wxMerchantNo != null  and wxMerchantNo != ''"> and wx_merchant_no = #{wxMerchantNo}</if>
+            <if test="wxKey != null  and wxKey != ''"> and wx_key = #{wxKey}</if>
+            <if test="hfProductNo != null  and hfProductNo != ''"> and hf_product_no = #{hfProductNo}</if>
+            <if test="hfSystemNo != null  and hfSystemNo != ''"> and hf_system_no = #{hfSystemNo}</if>
+            <if test="hfMerchantNo != null  and hfMerchantNo != ''"> and hf_merchant_no = #{hfMerchantNo}</if>
+            <if test="hfPrivateKey != null  and hfPrivateKey != ''"> and hf_private_key = #{hfPrivateKey}</if>
+            <if test="hfPublicKey != null  and hfPublicKey != ''"> and hf_public_key = #{hfPublicKey}</if>
+            <if test="hfPayNotifyUrl != null  and hfPayNotifyUrl != ''"> and hf_pay_notify_url = #{hfPayNotifyUrl}</if>
+            <if test="hfLargePayNotifyUrl != null  and hfLargePayNotifyUrl != ''"> and hf_large_pay_notify_url = #{hfLargePayNotifyUrl}</if>
+            <if test="hfRefundNotifyUrl != null  and hfRefundNotifyUrl != ''"> and hf_refund_notify_url = #{hfRefundNotifyUrl}</if>
+            <if test="hfLargeRefundNotifyUrl != null  and hfLargeRefundNotifyUrl != ''"> and hf_large_refund_notify_url = #{hfLargeRefundNotifyUrl}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+
+    <select id="selectPaymentMiniProgramConfigById" parameterType="String" resultMap="PaymentMiniProgramConfigResult">
+        <include refid="selectPaymentMiniProgramConfigVo"/>
+        where id = #{id}
+    </select>
+    <select id="selectPaymentConfigByAppId" resultType="com.fs.pay.domain.PaymentMiniProgramConfig">
+        select * from payment_mini_program_config where appid=#{appid} limit 1
+    </select>
+
+    <insert id="insertPaymentMiniProgramConfig" parameterType="PaymentMiniProgramConfig" useGeneratedKeys="true" keyProperty="id">
+        insert into payment_mini_program_config
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="payType != null and payType != ''">pay_type,</if>
+            <if test="appName != null and appName != ''">appname,</if>
+            <if test="appid != null and appid != ''">appid,</if>
+            <if test="appSecret != null and appSecret != ''">appsecret,</if>
+            <if test="ybMerchantNo != null">yb_merchant_no,</if>
+            <if test="ybKey != null">yb_key,</if>
+            <if test="ybNotifyUrl != null">yb_notify_url,</if>
+            <if test="tzhMerchantNo != null">tzh_merchant_no,</if>
+            <if test="tzhAppsecret != null">tzh_appsecret,</if>
+            <if test="tzhPrivateKey != null">tzh_private_key,</if>
+            <if test="tzhPublicKey != null">tzh_public_key,</if>
+            <if test="tzhAppkey != null">tzh_appkey,</if>
+            <if test="tzhPayNotifyUrl != null">tzh_pay_notify_url,</if>
+            <if test="tzhRefundNotifyUrl != null">tzh_refund_notify_url,</if>
+            <if test="tzhSplitNotifyUrl != null">tzh_split_notify_url,</if>
+            <if test="wxMerchantNo != null">wx_merchant_no,</if>
+            <if test="wxKey != null">wx_key,</if>
+            <if test="hfProductNo != null">hf_product_no,</if>
+            <if test="hfSystemNo != null">hf_system_no,</if>
+            <if test="hfMerchantNo != null">hf_merchant_no,</if>
+            <if test="hfPrivateKey != null">hf_private_key,</if>
+            <if test="hfPublicKey != null">hf_public_key,</if>
+            <if test="hfPayNotifyUrl != null">hf_pay_notify_url,</if>
+            <if test="hfLargePayNotifyUrl != null">hf_large_pay_notify_url,</if>
+            <if test="hfRefundNotifyUrl != null">hf_refund_notify_url,</if>
+            <if test="hfLargeRefundNotifyUrl != null">hf_large_refund_notify_url,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="payType != null and payType != ''">#{payType},</if>
+            <if test="appName != null and appName != ''">#{appName},</if>
+            <if test="appid != null and appid != ''">#{appid},</if>
+            <if test="appSecret != null and appSecret != ''">#{appSecret},</if>
+            <if test="ybMerchantNo != null">#{ybMerchantNo},</if>
+            <if test="ybKey != null">#{ybKey},</if>
+            <if test="ybNotifyUrl != null">#{ybNotifyUrl},</if>
+            <if test="tzhMerchantNo != null">#{tzhMerchantNo},</if>
+            <if test="tzhAppsecret != null">#{tzhAppsecret},</if>
+            <if test="tzhPrivateKey != null">#{tzhPrivateKey},</if>
+            <if test="tzhPublicKey != null">#{tzhPublicKey},</if>
+            <if test="tzhAppkey != null">#{tzhAppkey},</if>
+            <if test="tzhPayNotifyUrl != null">#{tzhPayNotifyUrl},</if>
+            <if test="tzhRefundNotifyUrl != null">#{tzhRefundNotifyUrl},</if>
+            <if test="tzhSplitNotifyUrl != null">#{tzhSplitNotifyUrl},</if>
+            <if test="wxMerchantNo != null">#{wxMerchantNo},</if>
+            <if test="wxKey != null">#{wxKey},</if>
+            <if test="hfProductNo != null">#{hfProductNo},</if>
+            <if test="hfSystemNo != null">#{hfSystemNo},</if>
+            <if test="hfMerchantNo != null">#{hfMerchantNo},</if>
+            <if test="hfPrivateKey != null">#{hfPrivateKey},</if>
+            <if test="hfPublicKey != null">#{hfPublicKey},</if>
+            <if test="hfPayNotifyUrl != null">#{hfPayNotifyUrl},</if>
+            <if test="hfLargePayNotifyUrl != null">#{hfLargePayNotifyUrl},</if>
+            <if test="hfRefundNotifyUrl != null">#{hfRefundNotifyUrl},</if>
+            <if test="hfLargeRefundNotifyUrl != null">#{hfLargeRefundNotifyUrl},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updatePaymentMiniProgramConfig" parameterType="PaymentMiniProgramConfig">
+        update payment_mini_program_config
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="payType != null and payType != ''">pay_type = #{payType},</if>
+            <if test="appName != null and appName != ''">appname = #{appName},</if>
+            <if test="appid != null and appid != ''">appid = #{appid},</if>
+            <if test="appSecret != null and appSecret != ''">appsecret = #{appSecret},</if>
+            <if test="ybMerchantNo != null">yb_merchant_no = #{ybMerchantNo},</if>
+            <if test="ybKey != null">yb_key = #{ybKey},</if>
+            <if test="ybNotifyUrl != null">yb_notify_url = #{ybNotifyUrl},</if>
+            <if test="tzhMerchantNo != null">tzh_merchant_no = #{tzhMerchantNo},</if>
+            <if test="tzhAppsecret != null">tzh_appsecret = #{tzhAppsecret},</if>
+            <if test="tzhPrivateKey != null">tzh_private_key = #{tzhPrivateKey},</if>
+            <if test="tzhPublicKey != null">tzh_public_key = #{tzhPublicKey},</if>
+            <if test="tzhAppkey != null">tzh_appkey = #{tzhAppkey},</if>
+            <if test="tzhPayNotifyUrl != null">tzh_pay_notify_url = #{tzhPayNotifyUrl},</if>
+            <if test="tzhRefundNotifyUrl != null">tzh_refund_notify_url = #{tzhRefundNotifyUrl},</if>
+            <if test="tzhSplitNotifyUrl != null">tzh_split_notify_url = #{tzhSplitNotifyUrl},</if>
+            <if test="wxMerchantNo != null">wx_merchant_no = #{wxMerchantNo},</if>
+            <if test="wxKey != null">wx_key = #{wxKey},</if>
+            <if test="hfProductNo != null">hf_product_no = #{hfProductNo},</if>
+            <if test="hfSystemNo != null">hf_system_no = #{hfSystemNo},</if>
+            <if test="hfMerchantNo != null">hf_merchant_no = #{hfMerchantNo},</if>
+            <if test="hfPrivateKey != null">hf_private_key = #{hfPrivateKey},</if>
+            <if test="hfPublicKey != null">hf_public_key = #{hfPublicKey},</if>
+            <if test="hfPayNotifyUrl != null">hf_pay_notify_url = #{hfPayNotifyUrl},</if>
+            <if test="hfLargePayNotifyUrl != null">hf_large_pay_notify_url = #{hfLargePayNotifyUrl},</if>
+            <if test="hfRefundNotifyUrl != null">hf_refund_notify_url = #{hfRefundNotifyUrl},</if>
+            <if test="hfLargeRefundNotifyUrl != null">hf_large_refund_notify_url = #{hfLargeRefundNotifyUrl},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deletePaymentMiniProgramConfigById" parameterType="String">
+        delete from payment_mini_program_config where id = #{id}
+    </delete>
+
+    <delete id="deletePaymentMiniProgramConfigByIds" parameterType="String">
+        delete from payment_mini_program_config where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>