Explorar el Código

Merge remote-tracking branch 'origin/bjcz_his_scrm' into 北京存在

吴树波 hace 3 días
padre
commit
e2207c6513
Se han modificado 100 ficheros con 4181 adiciones y 158 borrados
  1. 54 1
      fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java
  2. 1 1
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  3. 86 0
      fs-admin/src/main/java/com/fs/his/controller/FsNewcomerQuestionnaireController.java
  4. 2 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  5. 49 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCommentFeatureConfigController.java
  6. 56 0
      fs-admin/src/main/java/com/fs/live/controller/LiveCommentPinAdminController.java
  7. 30 0
      fs-admin/src/main/java/com/fs/live/controller/LiveFloatMsgLogController.java
  8. 5 0
      fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java
  9. 3 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java
  10. 2 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  11. 3 0
      fs-company-app/src/main/java/com/fs/app/param/FsUserUpdateParam.java
  12. 38 9
      fs-company/src/main/java/com/fs/company/controller/company/CompanyProfileController.java
  13. 15 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  14. 69 1
      fs-company/src/main/java/com/fs/company/controller/course/FsCoursePlaySourceConfigController.java
  15. 55 3
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java
  16. 12 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  17. 30 1
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  18. 30 0
      fs-company/src/main/java/com/fs/user/param/FsUserRedStatusParam.java
  19. 104 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  20. 37 0
      fs-live-app/src/main/java/com/fs/live/controller/LiveCommentPushController.java
  21. 44 0
      fs-live-app/src/main/java/com/fs/live/task/LiveCommentPinExpireScheduler.java
  22. 4 0
      fs-live-app/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java
  23. 102 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  24. 14 0
      fs-qw-company-api/src/main/java/com/fs/app/controller/QwController.java
  25. 17 0
      fs-service/src/main/java/com/fs/app/AppPayService.java
  26. 72 0
      fs-service/src/main/java/com/fs/app/impl/AppPayServiceImpl.java
  27. 179 0
      fs-service/src/main/java/com/fs/company/domain/RechargeRecord.java
  28. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyRoleMapper.java
  29. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserRoleMapper.java
  30. 5 0
      fs-service/src/main/java/com/fs/company/service/ICompanyRoleService.java
  31. 7 0
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  32. 21 20
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  33. 4 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyRoleServiceImpl.java
  34. 16 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  35. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCoursePlaySourceConfig.java
  36. 15 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseTrafficLog.java
  37. 7 3
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  38. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseLinkCreateParam.java
  39. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigCreateParam.java
  40. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigEditParam.java
  41. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  42. 3 1
      fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoFinishUParam.java
  43. 3 0
      fs-service/src/main/java/com/fs/course/param/newfs/FsCourseSortLinkParam.java
  44. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseService.java
  45. 3 1
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  46. 155 21
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  47. 533 38
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  48. 8 0
      fs-service/src/main/java/com/fs/course/vo/FsCoursePlaySourceConfigVO.java
  49. 14 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoListUVO.java
  50. 62 0
      fs-service/src/main/java/com/fs/his/config/AppConfig.java
  51. 5 0
      fs-service/src/main/java/com/fs/his/config/CouponConfig.java
  52. 2 0
      fs-service/src/main/java/com/fs/his/config/IntegralConfig.java
  53. 24 0
      fs-service/src/main/java/com/fs/his/domain/FsNewcomerQuestionnaire.java
  54. 33 0
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  55. 2 0
      fs-service/src/main/java/com/fs/his/enums/BusinessTypeEnum.java
  56. 1 0
      fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java
  57. 4 1
      fs-service/src/main/java/com/fs/his/enums/PaymentMethodEnum.java
  58. 21 0
      fs-service/src/main/java/com/fs/his/mapper/FsNewcomerQuestionnaireMapper.java
  59. 7 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java
  60. 10 0
      fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java
  61. 6 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralOrderDoPayParam.java
  62. 3 0
      fs-service/src/main/java/com/fs/his/param/FsUserCouponSendParam.java
  63. 9 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  64. 5 0
      fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java
  65. 10 0
      fs-service/src/main/java/com/fs/his/service/IFsUserNewTaskService.java
  66. 61 0
      fs-service/src/main/java/com/fs/his/service/NewcomerQuestionnaireService.java
  67. 414 0
      fs-service/src/main/java/com/fs/his/service/NewcomerWelfareService.java
  68. 307 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  69. 5 1
      fs-service/src/main/java/com/fs/his/service/impl/FsUserCouponServiceImpl.java
  70. 17 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java
  71. 55 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserNewTaskServiceImpl.java
  72. 20 0
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  73. 37 0
      fs-service/src/main/java/com/fs/his/vo/NewcomerWelfareStateVO.java
  74. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  75. 6 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsUserScrm.java
  76. 1 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  77. 7 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java
  78. 5 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductCategoryScrmMapper.java
  79. 7 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java
  80. 6 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreOrderScrmService.java
  81. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStorePaymentScrmService.java
  82. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  83. 7 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsUserScrmService.java
  84. 97 20
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  85. 210 31
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  86. 532 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  87. 15 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  88. 5 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java
  89. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/h5/FsUserPageListVO.java
  90. 7 1
      fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java
  91. 1 0
      fs-service/src/main/java/com/fs/im/service/OpenIMService.java
  92. 40 0
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  93. 13 0
      fs-service/src/main/java/com/fs/live/constant/LiveCommentPinEndReason.java
  94. 27 0
      fs-service/src/main/java/com/fs/live/domain/LiveCommentFeatureConfig.java
  95. 16 0
      fs-service/src/main/java/com/fs/live/domain/LiveCommentPinActive.java
  96. 26 0
      fs-service/src/main/java/com/fs/live/domain/LiveCommentPinLog.java
  97. 19 0
      fs-service/src/main/java/com/fs/live/domain/LiveFloatMsgLog.java
  98. 13 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCommentFeatureConfigMapper.java
  99. 28 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCommentPinActiveMapper.java
  100. 18 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCommentPinLogMapper.java

+ 54 - 1
fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java

@@ -40,6 +40,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.validation.Valid;
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/course/playSourceConfig")
@@ -82,6 +83,9 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         PageHelper.startPage(pageNum, pageSize);
         List<FsCoursePlaySourceConfigVO> list = fsCoursePlaySourceConfigService.selectCoursePlaySourceConfigVOListByMap(params);
+        for (FsCoursePlaySourceConfigVO item : list) {
+            item.setCompanyIds(parseCompanyIds(item.getCompanyIdsText(), item.getCompanyId()));
+        }
         return getDataTable(list);
     }
 
@@ -95,6 +99,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         FsCoursePlaySourceConfigVO configVO = new FsCoursePlaySourceConfigVO();
         BeanUtils.copyProperties(config, configVO);
+        configVO.setCompanyIds(parseCompanyIds(config.getCompanyIds(), config.getCompanyId()));
         return AjaxResult.success(configVO);
     }
 
@@ -124,6 +129,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         config.setCreateUserId(loginUser.getUserId());
         config.setCreateDeptId(loginUser.getDeptId());
         BeanUtils.copyProperties(param, config);
+        applyCompanyIds(param.getCompanyId(), param.getCompanyIds(), config);
 
         config.setIsDel(0);
         config.setCreateTime(LocalDateTime.now());
@@ -160,6 +166,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
 
         BeanUtils.copyProperties(param, config);
+        applyCompanyIds(param.getCompanyId(), param.getCompanyIds(), config);
 
         fsCoursePlaySourceConfigService.updateById(config);
         return AjaxResult.success();
@@ -278,7 +285,11 @@ public class FsCoursePlaySourceConfigController extends BaseController {
             queryWrapper.eq("create_user_id", loginUser.getUserId()).eq(config.getDept() == null || !config.getDept(), "create_dept_id", loginUser.getDeptId());
         }
         if(companyId != null){
-            queryWrapper.and(e -> e.eq("company_id", companyId).or().isNull("company_id"));
+            queryWrapper.and(e -> e.eq("company_id", companyId)
+                    .or()
+                    .apply("(company_ids is not null and company_ids != '' and find_in_set({0}, company_ids))", companyId)
+                    .or()
+                    .isNull("company_id"));
         }
         queryWrapper.in("status","0","1");//查询正常、半封禁小程序
         return R.ok().put("data", fsCoursePlaySourceConfigService.list(queryWrapper));
@@ -337,4 +348,46 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         return AjaxResult.success(result);
     }
 
+    private static void applyCompanyIds(Long companyId, List<Long> companyIds, FsCoursePlaySourceConfig config) {
+        List<Long> ids = new ArrayList<>();
+        if (companyIds != null) {
+            for (Long id : companyIds) {
+                if (id != null) {
+                    ids.add(id);
+                }
+            }
+        }
+        if (ids.isEmpty() && companyId != null) {
+            ids.add(companyId);
+        }
+
+        if (!ids.isEmpty()) {
+            List<Long> distinct = ids.stream().distinct().collect(Collectors.toList());
+            config.setCompanyId(config.getCompanyId() != null ? config.getCompanyId() : distinct.get(0));
+            config.setCompanyIds(distinct.stream().map(String::valueOf).collect(Collectors.joining(",")));
+        } else {
+            config.setCompanyIds(null);
+        }
+    }
+
+    private static List<Long> parseCompanyIds(String companyIds, Long companyId) {
+        List<Long> ids = new ArrayList<>();
+        if (companyIds != null && !companyIds.trim().isEmpty()) {
+            for (String s : companyIds.split(",")) {
+                String v = s == null ? "" : s.trim();
+                if (!v.isEmpty()) {
+                    try {
+                        ids.add(Long.parseLong(v));
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+        }
+        if (ids.isEmpty() && companyId != null) {
+            ids.add(companyId);
+        }
+        return ids.stream().distinct().collect(Collectors.toList());
+    }
+
 }
+

+ 1 - 1
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -144,7 +144,7 @@ public class FsVideoResourceController extends BaseController {
             return AjaxResult.success();
         }
         fsVideoResourceService.updateById(fsVideoResource);
-        fsUserCourseVideoService.updateVideoByVideoUrl(fsVideoResource.getVideoUrl(),fsVideoResourceResult.getOldVideoUrl(),fsVideoResource.getThumbnail(),fsVideoResource.getResourceName());
+        fsUserCourseVideoService.updateVideoByVideoUrl(fsVideoResource.getVideoUrl(),fsVideoResourceResult.getOldVideoUrl(),fsVideoResource.getThumbnail(),fsVideoResource.getFileName(),fsVideoResource.getDuration());
         return AjaxResult.success();
     }
 

+ 86 - 0
fs-admin/src/main/java/com/fs/his/controller/FsNewcomerQuestionnaireController.java

@@ -0,0 +1,86 @@
+package com.fs.his.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.DateUtils;
+import com.fs.his.domain.FsNewcomerQuestionnaire;
+import com.fs.his.service.NewcomerQuestionnaireService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 新人问卷调查(动态表单 JSON:formSchema)
+ */
+@RestController
+@RequestMapping("/his/newcomerQuestionnaire")
+public class FsNewcomerQuestionnaireController extends BaseController {
+
+    @Autowired
+    private NewcomerQuestionnaireService newcomerQuestionnaireService;
+
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsNewcomerQuestionnaire query) {
+        startPage();
+        List<FsNewcomerQuestionnaire> list = newcomerQuestionnaireService.selectList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(newcomerQuestionnaireService.selectById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:add')")
+    @Log(title = "新人问卷调查", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsNewcomerQuestionnaire row) {
+        row.setCreateTime(DateUtils.getNowDate());
+        row.setUpdateTime(DateUtils.getNowDate());
+        if (row.getSortOrder() == null) {
+            row.setSortOrder(0);
+        }
+        if (row.getStatus() == null) {
+            row.setStatus(1);
+        }
+        return toAjax(newcomerQuestionnaireService.insert(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:edit')")
+    @Log(title = "新人问卷调查", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsNewcomerQuestionnaire row) {
+        row.setUpdateTime(DateUtils.getNowDate());
+        return toAjax(newcomerQuestionnaireService.update(row));
+    }
+
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:remove')")
+    @Log(title = "新人问卷调查", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(newcomerQuestionnaireService.deleteByIds(ids));
+    }
+
+    /**
+     * 当前 App 将使用的问卷(与系统参数 newcomer.welfare.questionnaire_id 及启用状态解析逻辑一致)
+     */
+    @PreAuthorize("@ss.hasPermi('his:newcomerQuestionnaire:query')")
+    @GetMapping("/appActive")
+    public AjaxResult appActive() {
+        return AjaxResult.success(newcomerQuestionnaireService.resolveForApp());
+    }
+}

+ 2 - 2
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -114,7 +114,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
             param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
         }
-        param.setIsHealth("1");
+//        param.setIsHealth("1");
         List<FsStoreOrderVO> list = fsStoreOrderService.selectFsStoreOrderListVO(param);
         //金牛需求 区别其他项目 status = 6 (金牛代服管家) ,其他项目请避免使用订单状态status = 6
         TableDataInfo dataTable = getDataTable(list);
@@ -504,7 +504,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
             param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
         }
-        param.setIsHealth("1");
+//        param.setIsHealth("1");
         List<FsStoreOrderDeliveryNoteExportVO> storeOrderDeliveryNoteExportVOList=fsStoreOrderService.getDeliveryNote(param);
         ExcelUtil<FsStoreOrderDeliveryNoteExportVO> util = new ExcelUtil<>(FsStoreOrderDeliveryNoteExportVO.class);
         //通过商品ID获取关键字,安全处理空列表和null keyword的情况

+ 49 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCommentFeatureConfigController.java

@@ -0,0 +1,49 @@
+package com.fs.live.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.company.service.ICompanyRoleService;
+import com.fs.live.domain.LiveCommentFeatureConfig;
+import com.fs.live.service.ILiveCommentFeatureConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 直播评论飘屏/置顶 — 全局规则(库表 config_id=1)
+ */
+@RestController
+@RequestMapping("/live/commentFeature")
+public class LiveCommentFeatureConfigController extends BaseController {
+
+    @Autowired
+    private ILiveCommentFeatureConfigService liveCommentFeatureConfigService;
+    @Autowired
+    private ICompanyRoleService companyRoleService;
+
+    @GetMapping("/config")
+    public AjaxResult getConfig() {
+        return AjaxResult.success(liveCommentFeatureConfigService.getEffectiveConfig());
+    }
+
+    /**
+     * 可飘屏/可置顶角色多选:下拉选项(去重 role_name)
+     */
+    @GetMapping("/roles")
+    public AjaxResult listDistinctRoleNames() {
+        List<String> names = companyRoleService.selectDistinctRoleNames();
+        return AjaxResult.success(names);
+    }
+
+    @Log(title = "直播评论功能全局配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/config")
+    public AjaxResult updateConfig(@RequestBody LiveCommentFeatureConfig config) {
+        config.setUpdateBy(SecurityUtils.getUsername());
+        liveCommentFeatureConfigService.updateConfig(config);
+        return AjaxResult.success();
+    }
+}

+ 56 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveCommentPinAdminController.java

@@ -0,0 +1,56 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.live.domain.LiveCommentPinLog;
+import com.fs.live.service.ILiveCommentPinService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 评论置顶:记录、实时监控、后台强制取消
+ */
+@RestController
+@RequestMapping("/live/commentPin")
+public class LiveCommentPinAdminController extends BaseController {
+
+    @Autowired
+    private ILiveCommentPinService liveCommentPinService;
+
+    /**
+     * 全站当前生效置顶(含剩余时间、评论摘要)
+     */
+    @GetMapping("/monitor/active")
+    public AjaxResult monitorActive() {
+        return AjaxResult.success(liveCommentPinService.listActiveGlobalMonitor());
+    }
+
+    @GetMapping("/log/list")
+    public TableDataInfo logList(LiveCommentPinLog query) {
+        startPage();
+        List<LiveCommentPinLog> list = liveCommentPinService.listPinLogs(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 某直播间当前置顶列表
+     */
+    @GetMapping("/active/{liveId}")
+    public AjaxResult activeByLive(@PathVariable Long liveId) {
+        return AjaxResult.success(liveCommentPinService.listActiveByLiveId(liveId));
+    }
+
+    @PostMapping("/active/{activeId}/forceCancel")
+    public AjaxResult forceCancel(@PathVariable Long activeId) {
+        R r = liveCommentPinService.forceUnpinByActiveId(activeId, SecurityUtils.getUsername());
+        if (!Integer.valueOf(200).equals(r.get("code"))) {
+            return AjaxResult.error(String.valueOf(r.get("msg")));
+        }
+        return AjaxResult.success();
+    }
+}

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

@@ -0,0 +1,30 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.live.domain.LiveFloatMsgLog;
+import com.fs.live.service.ILiveFloatMsgLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 飘屏发送记录
+ */
+@RestController
+@RequestMapping("/live/floatMsgLog")
+public class LiveFloatMsgLogController extends BaseController {
+
+    @Autowired
+    private ILiveFloatMsgLogService liveFloatMsgLogService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(LiveFloatMsgLog query) {
+        startPage();
+        List<LiveFloatMsgLog> list = liveFloatMsgLogService.selectList(query);
+        return getDataTable(list);
+    }
+}

+ 5 - 0
fs-common/src/main/java/com/fs/common/constant/LiveKeysConstant.java

@@ -40,5 +40,10 @@ public class LiveKeysConstant {
     //记录用户观看直播间信息 直播间id、用户id、外部联系人id、qwUserId
     public static final String LIVE_USER_WATCH_LOG_CACHE = "live:user:watch:log:%s:%s:%s:%s";
 
+    /** 直播评论飘屏/置顶全局配置缓存(单条) */
+    public static final String LIVE_COMMENT_FEATURE_CONFIG_ROW = "live:comment:feature:config:row";
+    public static final int LIVE_COMMENT_FEATURE_CONFIG_EXPIRE_SEC = 300;
+    /** 飘屏冷却 liveId userId */
+    public static final String LIVE_FLOAT_COOLDOWN = "live:float:cooldown:%s:%s";
 
 }

+ 3 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java

@@ -271,6 +271,9 @@ public class FsUserController extends AppBaseController {
 
         fsUser.setNickName(param.getNickName());
         fsUser.setRemark(param.getRemark());
+        if (param.getRedStatus() != null) {
+            fsUser.setRedStatus(param.getRedStatus());
+        }
         fsUserService.updateFsUser(fsUser);
         return ResponseResult.ok();
     }

+ 2 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -235,8 +235,10 @@ public class FsUserCourseVideoController extends AppBaseController {
 
         R courseSortLink = fsUserCourseService.createCourseSortLink(fsCourseLinkCreateParam);
         String url = courseSortLink.get("url").toString();
+        String linkId=courseSortLink.get("linkId").toString();
         Map<String, Object> map = new HashMap<>();
         map.put("url", url);
+        map.put("linkId", linkId);
         return R.ok(map);
     }
 

+ 3 - 0
fs-company-app/src/main/java/com/fs/app/param/FsUserUpdateParam.java

@@ -23,4 +23,7 @@ public class FsUserUpdateParam {
      */
     @ApiModelProperty("用户备注")
     private String remark;
+
+    @ApiModelProperty("红包领取开关:1开启 0关闭,不传则不修改")
+    private Integer redStatus;
 }

+ 38 - 9
fs-company/src/main/java/com/fs/company/controller/company/CompanyProfileController.java

@@ -1,15 +1,19 @@
 package com.fs.company.controller.company;
 
 import com.fs.common.annotation.Log;
-import com.fs.common.config.FSConfig;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.enums.ImTypeEnum;
+import com.fs.common.exception.file.FileNameLengthLimitExceededException;
+import com.fs.common.exception.file.FileSizeLimitExceededException;
+import com.fs.common.exception.file.InvalidExtensionException;
 import com.fs.common.utils.PatternUtils;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.file.FileUploadUtils;
+import com.fs.common.utils.file.MimeTypeUtils;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.param.CompanyUserEditParam;
 import com.fs.company.service.ICompanyUserService;
@@ -18,14 +22,13 @@ import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.im.config.ImTypeConfig;
 import com.fs.im.service.OpenIMService;
-import com.fs.system.service.ISysConfigService;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
-
 /**
  * 个人信息
  */
@@ -135,22 +138,48 @@ public class CompanyProfileController extends BaseController
      */
     @Log(title = "用户头像", businessType = BusinessType.UPDATE)
     @PostMapping("/avatar")
-    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws IOException
+    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file)
     {
-        if (!file.isEmpty())
+        try
         {
+            if (file.isEmpty())
+            {
+                return AjaxResult.error("上传图片异常,请联系管理员");
+            }
+            String originalFilename = file.getOriginalFilename();
+            if (originalFilename != null && originalFilename.length() > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH)
+            {
+                return AjaxResult.error("文件名无效或过长");
+            }
+            FileUploadUtils.assertAllowed(file, MimeTypeUtils.IMAGE_EXTENSION);
+            String ext = FileUploadUtils.getExtension(file);
+            if (StringUtils.isEmpty(ext))
+            {
+                ext = "jpg";
+            }
+            String suffix = "." + ext;
+            CloudStorageService storage = OSSFactory.build();
+            String avatar = storage.uploadSuffix(file.getBytes(), suffix);
+
             LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-            String avatar = FileUploadUtils.upload(FSConfig.getAvatarPath(), file);
-            if (userService.updateUserAvatar(loginUser.getUsername(), avatar)>0)
+            if (userService.updateUserAvatar(loginUser.getUsername(), avatar) > 0)
             {
                 AjaxResult ajax = AjaxResult.success();
                 ajax.put("imgUrl", avatar);
-                // 更新缓存用户头像
                 loginUser.getUser().setAvatar(avatar);
                 tokenService.setLoginUser(loginUser);
                 return ajax;
             }
         }
+        catch (InvalidExtensionException | FileSizeLimitExceededException | FileNameLengthLimitExceededException e)
+        {
+            return AjaxResult.error(e.getMessage());
+        }
+        catch (Exception e)
+        {
+            logger.error("头像上传失败", e);
+            return AjaxResult.error("上传图片异常,请联系管理员");
+        }
         return AjaxResult.error("上传图片异常,请联系管理员");
     }
 }

+ 15 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -855,6 +855,21 @@ public class CompanyUserController extends BaseController {
         return R.ok().put("data", userList);
     }
 
+    /**
+     * 根据用户ID(fs_user.user_id)精确查询fs_user
+     * @param userId 用户ID
+     * @return 用户列表(与按手机号查询结构一致,便于绑定弹窗复用)
+     */
+    @ApiOperation("根据用户ID精确查询fs_user")
+    @GetMapping("/fsUser/queryByUserId")
+    public R queryFsUserByUserId(@RequestParam("userId") Long userId) {
+        if (userId == null || userId <= 0) {
+            return R.error("用户ID不能为空或无效");
+        }
+        List<com.fs.hisStore.domain.FsUserScrm> userList = fsUserScrmService.selectFsUserListByUserIdExact(userId);
+        return R.ok().put("data", userList);
+    }
+
     /**
      * 批量绑定用户到员工
      * @param data 包含 userIds(公司用户ID列表)和 bindCompanyUserId(绑定的销售ID)

+ 69 - 1
fs-company/src/main/java/com/fs/company/controller/course/FsCoursePlaySourceConfigController.java

@@ -26,6 +26,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.validation.Valid;
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.stream.Collectors;
 
 @RestController
 @RequestMapping("/course/playSourceConfig")
@@ -48,6 +49,9 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         PageHelper.startPage(pageNum, pageSize);
         List<FsCoursePlaySourceConfigVO> list = fsCoursePlaySourceConfigService.selectCoursePlaySourceConfigVOListByMap(params);
+        for (FsCoursePlaySourceConfigVO item : list) {
+            item.setCompanyIds(parseCompanyIds(item.getCompanyIdsText(), item.getCompanyId()));
+        }
         return getDataTable(list);
     }
 
@@ -61,6 +65,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         FsCoursePlaySourceConfigVO configVO = new FsCoursePlaySourceConfigVO();
         BeanUtils.copyProperties(config, configVO);
+        configVO.setCompanyIds(parseCompanyIds(config.getCompanyIds(), config.getCompanyId()));
         return AjaxResult.success(configVO);
     }
 
@@ -90,6 +95,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
 
         LoginUser loginUser = SecurityUtils.getLoginUser();
         config.setCompanyId(loginUser.getCompany().getCompanyId());
+        applyCompanyIds(config.getCompanyId(), param.getCompanyIds(), config);
         config.setCompanyUserId(loginUser.getUser().getUserId());
         config.setIsDel(0);
         config.setCreateTime(LocalDateTime.now());
@@ -117,7 +123,8 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
 
         LoginUser loginUser = SecurityUtils.getLoginUser();
-        if (!loginUser.getCompany().getCompanyId().equals(config.getCompanyId())) {
+        Long currentCompanyId = loginUser.getCompany().getCompanyId();
+        if (!belongsToCompany(config, currentCompanyId)) {
             return AjaxResult.error("非法操作");
         }
 
@@ -132,6 +139,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
 
         BeanUtils.copyProperties(param, config);
+        applyCompanyIds(config.getCompanyId(), param.getCompanyIds(), config);
         fsCoursePlaySourceConfigService.updateById(config);
         return AjaxResult.success();
     }
@@ -171,4 +179,64 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
         return R.ok().put("date",start);
     }
+
+    private static void applyCompanyIds(Long companyId, List<Long> companyIds, FsCoursePlaySourceConfig config) {
+        List<Long> ids = new ArrayList<>();
+        if (companyIds != null) {
+            for (Long id : companyIds) {
+                if (id != null) {
+                    ids.add(id);
+                }
+            }
+        }
+        if (ids.isEmpty() && companyId != null) {
+            ids.add(companyId);
+        }
+
+        if (!ids.isEmpty()) {
+            List<Long> distinct = ids.stream().distinct().collect(Collectors.toList());
+            config.setCompanyId(config.getCompanyId() != null ? config.getCompanyId() : distinct.get(0));
+            config.setCompanyIds(distinct.stream().map(String::valueOf).collect(Collectors.joining(",")));
+        } else {
+            config.setCompanyIds(null);
+        }
+    }
+
+    private static List<Long> parseCompanyIds(String companyIds, Long companyId) {
+        List<Long> ids = new ArrayList<>();
+        if (companyIds != null && !companyIds.trim().isEmpty()) {
+            for (String s : companyIds.split(",")) {
+                String v = s == null ? "" : s.trim();
+                if (!v.isEmpty()) {
+                    try {
+                        ids.add(Long.parseLong(v));
+                    } catch (NumberFormatException ignored) {
+                    }
+                }
+            }
+        }
+        if (ids.isEmpty() && companyId != null) {
+            ids.add(companyId);
+        }
+        return ids.stream().distinct().collect(Collectors.toList());
+    }
+
+    private static boolean belongsToCompany(FsCoursePlaySourceConfig config, Long companyId) {
+        if (companyId == null || config == null) {
+            return false;
+        }
+        if (companyId.equals(config.getCompanyId())) {
+            return true;
+        }
+        String companyIds = config.getCompanyIds();
+        if (companyIds == null || companyIds.trim().isEmpty()) {
+            return false;
+        }
+        for (String s : companyIds.split(",")) {
+            if (s != null && s.trim().equals(String.valueOf(companyId))) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

+ 55 - 3
fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java

@@ -10,7 +10,13 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.core.utils.OrderCodeUtils;
+import com.fs.company.domain.CompanyRecharge;
+import com.fs.company.param.CompanyRechargeParam;
+import com.fs.company.service.ICompanyRechargeService;
+import com.fs.company.service.ICompanyService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCourseRedPacketLog;
 import com.fs.course.domain.FsUserCoursePeriod;
@@ -28,8 +34,11 @@ import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
+import java.util.Date;
 import java.util.List;
 
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
@@ -57,6 +66,10 @@ public class FsCourseRedPacketLogController extends BaseController
 
     @Autowired
     private IFsUserCoursePeriodService fsUserCoursePeriodService;
+    @Autowired
+    private ICompanyRechargeService rechargeService;
+    @Autowired
+    private ICompanyService companyService;
     @Value("${cloud_host.company_name}")
     private String signProjectName;
     /**
@@ -305,9 +318,9 @@ public class FsCourseRedPacketLogController extends BaseController
 
 
     /**
-     * 红包消耗统计
+     * 红包消耗统计(含当前企业红包余额 redPackageMoney)
      * @param fsCourseRedPacketLog
-     * @return
+     * @return TableDataInfo,其中 ext.redPackageMoney 为当前企业红包余额
      */
     @GetMapping("/getReadPackageTotal")
     public TableDataInfo getReadPackageTotal(FsCourseRedPacketLog fsCourseRedPacketLog) {
@@ -316,6 +329,45 @@ public class FsCourseRedPacketLogController extends BaseController
         Long companyId = loginUser.getCompany().getCompanyId();
         fsCourseRedPacketLog.setCompanyId(companyId);
         List<FsCourseRedPacketLogListPVO> list = fsCourseRedPacketLogService.getReadPackageTotal(fsCourseRedPacketLog);
-        return getDataTable(list);
+        BigDecimal redPackageMoney = companyService.getRedPackageMoney(companyId);
+        return getDataTable(list).put("redPackageMoney", redPackageMoney);
+    }
+
+
+    /**
+     * 企业转账充值(红包余额,提交即生效,无需审核)
+     */
+    @PreAuthorize("@ss.hasPermi('his:company:recharge')")
+    @Log(title = "企业转账", businessType = BusinessType.INSERT)
+    @PostMapping(value = "/recharge")
+    @Transactional
+    @RepeatSubmit
+    public R recharge(@RequestBody CompanyRechargeParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        String orderSn = OrderCodeUtils.getOrderSn();
+        if (StringUtils.isEmpty(orderSn)) {
+            return R.error("订单生成失败,请重试");
+        }
+        CompanyRecharge recharge = new CompanyRecharge();
+        recharge.setRechargeNo(orderSn);
+        recharge.setCompanyId(companyId);
+        recharge.setMoney(param.getMoney());
+        recharge.setCreateUserId(loginUser.getUser().getUserId());
+        recharge.setRemark(param.getRemark());
+        recharge.setPayType(3);
+        recharge.setBusinessType(1);
+        recharge.setIsAudit(1);
+        recharge.setStatus(0);
+        rechargeService.insertCompanyRecharge(recharge);
+
+        companyService.redPacketTopUpCompany(companyId, param.getMoney(), "1");
+
+        recharge.setPayTime(new Date());
+        recharge.setStatus(1);
+        rechargeService.updateCompanyRecharge(recharge);
+
+        return R.ok("充值成功,红包余额已立即生效!");
     }
 }

+ 12 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -614,6 +614,18 @@ public class QwExternalContactController extends BaseController
         return qwExternalContactService.updateQwExternalContactBindUserId(qwExternalContact);
     }
 
+    /**
+     * 修改绑定会员的「首次登录奖励地址」(fs_user.first_login_reward_address)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit') or @ss.hasPermi('qw:externalContact:deptEdit') or @ss.hasPermi('qw:externalContact:myEdit')")
+    @Log(title = "企微客户-首次登录奖励地址", businessType = BusinessType.UPDATE)
+    @PutMapping("/firstLoginRewardAddress")
+    public R updateFirstLoginRewardAddress(@RequestBody QwExternalContactFirstLoginRewardAddressParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return qwExternalContactService.updateBoundFsUserFirstLoginRewardAddress(param, loginUser.getCompany().getCompanyId());
+    }
+
     /**
      * 解除绑定小程序用户
      */

+ 30 - 1
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -37,6 +37,7 @@ import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.dto.UserProjectDTO;
 import com.fs.qw.service.ICustomerTransferApprovalService;
 import com.fs.store.param.h5.FsUserPageListParam;
+import com.fs.user.param.FsUserRedStatusParam;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
@@ -46,14 +47,15 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.validation.Valid;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 import static com.fs.his.utils.PhoneUtil.encryptPhone;
-import java.util.List;
 
 @Api(tags = "会员管理接口")
 @RestController
@@ -205,6 +207,33 @@ public class FsUserAdminController extends BaseController {
         return toAjax(fsUserCompanyUserService.updateFsUserCompanyUser(fsUser));
     }
 
+    @PreAuthorize("@ss.hasPermi('user:fsUser:edit')")
+    @Log(title = "会员红包领取开关", businessType = BusinessType.UPDATE)
+    @PutMapping("/redStatus")
+    @ApiOperation("修改会员红包领取开关(red_status:1开启 0关闭)")
+    public AjaxResult updateRedStatus(@Valid @RequestBody FsUserRedStatusParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        FsUser fsUser = fsUserService.selectFsUserById(param.getUserId());
+        if (fsUser == null) {
+            return AjaxResult.error("用户不存在");
+        }
+        boolean allowed = Objects.equals(fsUser.getCompanyId(), companyId);
+        if (!allowed) {
+            FsUserCompanyUser query = new FsUserCompanyUser();
+            query.setUserId(param.getUserId());
+            query.setCompanyId(companyId);
+            List<FsUserCompanyUser> rel = fsUserCompanyUserService.selectFsUserCompanyUserList(query);
+            allowed = rel != null && !rel.isEmpty();
+        }
+        if (!allowed) {
+            return AjaxResult.error("无权限操作该用户");
+        }
+        FsUser update = new FsUser();
+        update.setUserId(param.getUserId());
+        update.setRedStatus(param.getRedStatus());
+        return toAjax(fsUserService.updateFsUser(update));
+    }
 
     @ApiOperation("后台会员批量发送课程消息")
     @PostMapping("/batchSendCourse")

+ 30 - 0
fs-company/src/main/java/com/fs/user/param/FsUserRedStatusParam.java

@@ -0,0 +1,30 @@
+package com.fs.user.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 修改会员红包领取开关
+ */
+@Data
+@ApiModel("会员红包领取开关参数")
+public class FsUserRedStatusParam {
+
+    @NotNull(message = "用户ID不能为空")
+    @ApiModelProperty(value = "会员 userId(fs_user.user_id)", required = true)
+    private Long userId;
+
+    /**
+     * 1 可领取红包,0 关闭
+     */
+    @NotNull(message = "redStatus不能为空")
+    @Min(0)
+    @Max(1)
+    @ApiModelProperty(value = "红包领取:1开启 0关闭", required = true)
+    private Integer redStatus;
+}

+ 104 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -1,6 +1,7 @@
 package com.fs.app.service;
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.redis.RedisCache;
@@ -73,10 +74,23 @@ public class IpadSendServer {
 
         // 发送参数原本的appid
         String appid = content.getMiniprogramAppid();
+        List<CompanyMiniapp> listAll = Collections.emptyList();
+        Set<String> companyAppIds = Collections.emptySet();
+        boolean forceCompanyApp = false;
+        if (companyId != null && miniMap != null && !miniMap.isEmpty()) {
+            companyAppIds = miniMap.values().stream()
+                    .filter(Objects::nonNull)
+                    .filter(cfg -> isConfigBoundToCompany(cfg, companyId))
+                    .map(FsCoursePlaySourceConfig::getAppid)
+                    .filter(StringUtils::isNotEmpty)
+                    .collect(Collectors.toSet());
+            forceCompanyApp = !companyAppIds.isEmpty();
+        }
+        final Set<String> allowedCompanyAppIds = companyAppIds;
         // 判断销售工时ID不为空并且有小程序类型
         if (companyId != null && content.getMiniType() != null) {
             // 获取销售公司下面绑定的主备小程序,并且根据当前应该发送的主备类型查询出数据
-            List<CompanyMiniapp> listAll = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().eq("company_id", companyId));
+            listAll = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().eq("company_id", companyId));
             List<CompanyMiniapp> list = listAll.stream().filter(e -> e.getType().equals(content.getMiniType())).collect(Collectors.toList());
             // 判断当前绑定的最新的小程序,并且覆盖以前的值(可以达到实时替换小程序的功能)
             if (!list.isEmpty() && list.get(0) != null && StringUtils.isNotEmpty(list.get(0).getAppId())) {
@@ -106,6 +120,11 @@ public class IpadSendServer {
                             }
                             // 根据小程序ID查询小程序列表
                             List<FsCoursePlaySourceConfig> configList = playSourceConfigService.selectByAppIds(miniAppList);
+                            if (forceCompanyApp) {
+                                configList = configList.stream()
+                                        .filter(e -> allowedCompanyAppIds.contains(e.getAppid()))
+                                        .collect(Collectors.toList());
+                            }
                             configList.sort(Comparator.comparing(e -> miniSortMap.getOrDefault(e.getAppid(), 100)));
                             // 过滤掉完全封禁(status=2)的小程序,只保留正常(status=0)和半封禁(status=1)的小程序
                             List<FsCoursePlaySourceConfig> availableList = configList.stream()
@@ -173,13 +192,41 @@ public class IpadSendServer {
                 }
             }
         }
+        if (forceCompanyApp && StringUtils.isNotEmpty(appid) && !allowedCompanyAppIds.contains(appid)) {
+            Optional<String> preferredAppId = listAll.stream()
+                    .filter(e -> e.getType().equals(content.getMiniType()) && StringUtils.isNotEmpty(e.getAppId()))
+                    .map(CompanyMiniapp::getAppId)
+                    .filter(allowedCompanyAppIds::contains)
+                    .findFirst();
+            if (!preferredAppId.isPresent()) {
+                preferredAppId = allowedCompanyAppIds.stream().findFirst();
+            }
+            if (preferredAppId.isPresent()) {
+                appid = preferredAppId.get();
+                log.info("ID:{}, companyId:{},命中公司隔离规则,改用本公司小程序:{}", vo.getId(), companyId, appid);
+            }
+        }
         FsCoursePlaySourceConfig courseMaConfig = miniMap.get(appid);
+        if (forceCompanyApp && courseMaConfig != null && !isConfigBoundToCompany(courseMaConfig, companyId)) {
+            courseMaConfig = null;
+        }
         if (courseMaConfig == null) {
             List<CompanyMiniapp> list = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().eq("company_id", companyId).eq("type", 1));
             if (!list.isEmpty() && list.get(0) != null && StringUtils.isNotEmpty(list.get(0).getAppId())) {
                 courseMaConfig = miniMap.get(list.get(0).getAppId());
             }
         }
+        if (courseMaConfig == null && forceCompanyApp) {
+            Optional<FsCoursePlaySourceConfig> companyConfig = miniMap.values().stream()
+                    .filter(Objects::nonNull)
+                    .filter(cfg -> StringUtils.isNotEmpty(cfg.getAppid()))
+                    .filter(cfg -> allowedCompanyAppIds.contains(cfg.getAppid()))
+                    .findFirst();
+            if (companyConfig.isPresent()) {
+                courseMaConfig = companyConfig.get();
+                appid = courseMaConfig.getAppid();
+            }
+        }
         if (courseMaConfig == null) {
             throw new BaseException("未找到小程序配置:{}", appid);
         }
@@ -204,6 +251,61 @@ public class IpadSendServer {
         }
     }
 
+    private boolean isConfigBoundToCompany(FsCoursePlaySourceConfig config, Long companyId) {
+        if (config == null || companyId == null || StringUtils.isEmpty(config.getCompanyIds())) {
+            return false;
+        }
+        String targetCompanyId = String.valueOf(companyId);
+        return Arrays.stream(config.getCompanyIds().split(","))
+                .map(String::trim)
+                .anyMatch(targetCompanyId::equals);
+    }
+
+    /**
+     * 业务字段在 {@code content} 嵌套 JSON 里;未合并时 fileUrl/fileName 为空,
+     * 发送文件会退回用 URL 路径名(如 1775377962733.xlsx)作为展示名。
+     */
+    private void applyNestedContentIfPresent(QwSopCourseFinishTempSetting.Setting content) {
+        if (content == null || StringUtils.isEmpty(content.getContent())) {
+            return;
+        }
+        try {
+            JSONObject inner = JSON.parseObject(content.getContent());
+            if (inner == null || inner.isEmpty()) {
+                return;
+            }
+            if (StringUtils.isEmpty(content.getFileUrl()) && StringUtils.isNotEmpty(inner.getString("fileUrl"))) {
+                content.setFileUrl(inner.getString("fileUrl"));
+            }
+            if (StringUtils.isEmpty(content.getFileName())) {
+                String fn = inner.getString("fileName");
+                if (StringUtils.isEmpty(fn)) {
+                    fn = inner.getString("value");
+                }
+                if (StringUtils.isNotEmpty(fn)) {
+                    content.setFileName(fn);
+                }
+            }
+            if (StringUtils.isEmpty(content.getImgUrl()) && StringUtils.isNotEmpty(inner.getString("imgUrl"))) {
+                content.setImgUrl(inner.getString("imgUrl"));
+            }
+            if (StringUtils.isEmpty(content.getVideoUrl()) && StringUtils.isNotEmpty(inner.getString("videoUrl"))) {
+                content.setVideoUrl(inner.getString("videoUrl"));
+            }
+            if (StringUtils.isEmpty(content.getVoiceUrl()) && StringUtils.isNotEmpty(inner.getString("voiceUrl"))) {
+                content.setVoiceUrl(inner.getString("voiceUrl"));
+            }
+            if (StringUtils.isEmpty(content.getValue()) && StringUtils.isNotEmpty(inner.getString("value"))) {
+                content.setValue(inner.getString("value"));
+            }
+            if (StringUtils.isEmpty(content.getContentType()) && inner.get("contentType") != null) {
+                content.setContentType(String.valueOf(inner.get("contentType")));
+            }
+        } catch (Exception e) {
+            log.warn("applyNestedContentIfPresent parse failed: {}", e.getMessage());
+        }
+    }
+
     private void sendFile(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
         FileVo fileVo = FileVo.builder()
                 .url(content.getFileUrl())
@@ -685,6 +787,7 @@ public class IpadSendServer {
         vo.setQwUserId(qwUser.getId());
         try {
             content.setSendStatus(1);
+            applyNestedContentIfPresent(content);
             // 如果是群公告,使用SendNotice接口
             if (qwSopLogs.getSendType() == 21 && "11".equals(content.getContentType())) {
                 sendNotice(vo, content);

+ 37 - 0
fs-live-app/src/main/java/com/fs/live/controller/LiveCommentPushController.java

@@ -0,0 +1,37 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 总后台通过 HTTP 通知 fs-live-app 做 WebSocket 广播(需配置 liveWebSocketUrl)
+ */
+@Slf4j
+@RestController
+@RequestMapping("/app/live/comment")
+public class LiveCommentPushController extends BaseController {
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    /** 全量广播全局评论配置(cmd=liveCommentConfig) */
+    @PostMapping("/broadcastConfig")
+    public R broadcastConfig() {
+        webSocketServer.broadcastLiveCommentConfigToAll();
+        return R.ok();
+    }
+
+    @PostMapping("/broadcastToLive")
+    public R broadcastToLive(@RequestBody Map<String, Object> body) {
+        Long liveId = Long.valueOf(body.get("liveId").toString());
+        String message = body.get("message").toString();
+        webSocketServer.broadcastMessage(liveId, message);
+        return R.ok();
+    }
+}

+ 44 - 0
fs-live-app/src/main/java/com/fs/live/task/LiveCommentPinExpireScheduler.java

@@ -0,0 +1,44 @@
+package com.fs.live.task;
+
+import com.fs.live.service.ILiveCommentPinService;
+import com.fs.live.util.LiveCommentWsMessageBuilder;
+import com.fs.live.vo.LiveCommentPinExpireEvent;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 置顶到期自动结束并广播 commentUnpinned
+ */
+@Slf4j
+@Component
+public class LiveCommentPinExpireScheduler {
+
+    @Autowired
+    private ILiveCommentPinService liveCommentPinService;
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    @Scheduled(fixedDelay = 30000)
+    public void expirePins() {
+        try {
+            List<LiveCommentPinExpireEvent> events = liveCommentPinService.expireDuePins();
+            for (LiveCommentPinExpireEvent e : events) {
+                Map<String, Object> payload = new LinkedHashMap<>();
+                payload.put("msgId", e.getMsgId());
+                payload.put("pinLogId", e.getPinLogId());
+                payload.put("reason", "EXPIRED");
+                String ws = LiveCommentWsMessageBuilder.build(e.getLiveId(), "commentUnpinned", payload);
+                webSocketServer.broadcastMessage(e.getLiveId(), ws);
+            }
+        } catch (Exception ex) {
+            log.error("[置顶到期] 处理失败", ex);
+        }
+    }
+}

+ 4 - 0
fs-live-app/src/main/java/com/fs/live/websocket/bean/SendMsgVo.java

@@ -34,5 +34,9 @@ public class SendMsgVo {
     private boolean on = false;
     private Integer status;
     private Integer duration;
+    /** App 端直播间角色编码,与总后台配置中的角色列表对应,如 USER、ASSISTANT、ANCHOR、ADMIN */
+    private String liveRoleCode;
+    /** 置顶/取消置顶目标评论 msg_id */
+    private Long targetMsgId;
 
 }

+ 102 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -332,6 +332,8 @@ public class WebSocketServer {
             startConsumerThread(liveId);
         }
 
+        pushLiveCommentBootstrap(session, liveId, userType);
+
     }
 
     //关闭连接时调用
@@ -425,6 +427,10 @@ public class WebSocketServer {
 
         long liveId = (long) userProperties.get("liveId");
         long userType = (long) userProperties.get("userType");
+        long companyUserId = -1L;
+        if (!Objects.isNull(userProperties.get("companyUserId"))) {
+            companyUserId = (long) userProperties.get("companyUserId");
+        }
         boolean isAdmin = false;
 
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
@@ -656,6 +662,51 @@ public class WebSocketServer {
                 case "coupon":
                     processCoupon(liveId, msg);
                     break;
+                case "floatScreenMsg":
+                    msg.setMsg(productionWordFilter.filter(msg.getMsg()).getFilteredText());
+                    if (StringUtils.isEmpty(msg.getMsg())) {
+                        return;
+                    }
+                    ILiveCommentFloatScreenService floatScreenService = SpringUtils.getBean(ILiveCommentFloatScreenService.class);
+                    Long checkCompanyUserId = (companyUserId > 0)
+                            ? companyUserId
+                            : (msg.getCompanyUserId() != null && msg.getCompanyUserId() > 0 ? msg.getCompanyUserId() : msg.getUserId());
+                    R floatR = floatScreenService.handleFloatScreen(liveId, msg.getUserId(), msg.getNickName(), msg.getAvatar(),
+                            msg.getMsg(), msg.getLiveRoleCode(), checkCompanyUserId);
+                    if (!Integer.valueOf(200).equals(floatR.get("code"))) {
+                        sendMessage(session, JSONObject.toJSONString(floatR));
+                        break;
+                    }
+                    LiveMsg flm = (LiveMsg) floatR.get("liveMsg");
+                    msg.setCmd("floatScreenDisplay");
+                    msg.setOn(true);
+                    msg.setData(JSONObject.toJSONString(flm));
+                    enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", msg)), true);
+                    break;
+                case "pinComment":
+                    if (msg.getTargetMsgId() == null) {
+                        sendMessage(session, JSONObject.toJSONString(R.error("缺少 targetMsgId")));
+                        break;
+                    }
+                    int pinDur = msg.getDuration() != null ? msg.getDuration() : -1;
+                    ILiveCommentPinService pinService = SpringUtils.getBean(ILiveCommentPinService.class);
+                    R pinR = pinService.pinComment(liveId, msg.getTargetMsgId(), msg.getUserId(), msg.getNickName(),
+                            msg.getLiveRoleCode(), (int) userType, pinDur,
+                            (companyUserId > 0 ? companyUserId :
+                                    (msg.getCompanyUserId() != null && msg.getCompanyUserId() > 0 ? msg.getCompanyUserId() : msg.getUserId())));
+                    sendMessage(session, JSONObject.toJSONString(pinR));
+                    break;
+                case "unpinComment":
+                    if (msg.getTargetMsgId() == null) {
+                        sendMessage(session, JSONObject.toJSONString(R.error("缺少 targetMsgId")));
+                        break;
+                    }
+                    ILiveCommentPinService pinService2 = SpringUtils.getBean(ILiveCommentPinService.class);
+                    R unpinR = pinService2.unpinComment(liveId, msg.getTargetMsgId(), msg.getUserId(), msg.getLiveRoleCode(), (int) userType,
+                            (companyUserId > 0 ? companyUserId :
+                                    (msg.getCompanyUserId() != null && msg.getCompanyUserId() > 0 ? msg.getCompanyUserId() : msg.getUserId())));
+                    sendMessage(session, JSONObject.toJSONString(unpinR));
+                    break;
                 case "delAutoTask":
                     if (userType == 1) {
                         delAutoTask(liveId, DateUtils.parseDate(msg.getData(),"yyyy-MM-dd'T'HH:mm:ss.SSSZ").getTime());
@@ -2208,5 +2259,56 @@ public class WebSocketServer {
         }
     }
 
+    private void pushLiveCommentBootstrap(Session session, long liveId, long userType) {
+        try {
+            ILiveCommentFeatureConfigService cfgSvc = SpringUtils.getBean(ILiveCommentFeatureConfigService.class);
+            String cfgJson = cfgSvc.buildConfigPushJson();
+            SendMsgVo cfgVo = SendMsgVo.builder().liveId(liveId).cmd("liveCommentConfig").data(cfgJson).on(true).build();
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", cfgVo)));
+            if (userType == 0) {
+                ILiveCommentPinService pinSvc = SpringUtils.getBean(ILiveCommentPinService.class);
+                List<LiveCommentPinActive> pins = pinSvc.listActiveByLiveId(liveId);
+                SendMsgVo pvo = SendMsgVo.builder().liveId(liveId).cmd("commentPinList").data(JSON.toJSONString(pins)).on(true).build();
+                sendMessage(session, JSONObject.toJSONString(R.ok().put("data", pvo)));
+            }
+        } catch (IOException e) {
+            log.warn("推送评论扩展配置 IO 异常 liveId={}", liveId, e);
+        } catch (Exception ex) {
+            log.warn("推送评论扩展配置失败 liveId={}", liveId, ex);
+        }
+    }
+
+    /**
+     * 总后台修改全局规则或样式后由 HTTP 触发:向所有在线连接广播(cmd=liveCommentConfig)
+     */
+    public void broadcastLiveCommentConfigToAll() {
+        try {
+            ILiveCommentFeatureConfigService cfgSvc = SpringUtils.getBean(ILiveCommentFeatureConfigService.class);
+            String cfgJson = cfgSvc.buildConfigPushJson();
+            SendMsgVo vo = SendMsgVo.builder().liveId(0L).cmd("liveCommentConfig").data(cfgJson).on(true).build();
+            String message = JSONObject.toJSONString(R.ok().put("data", vo));
+            broadcastToAllLiveConnections(message);
+        } catch (Exception e) {
+            log.error("全量广播评论配置失败", e);
+        }
+    }
+
+    public void broadcastToAllLiveConnections(String message) {
+        for (ConcurrentHashMap<Long, Session> room : rooms.values()) {
+            for (Session s : room.values()) {
+                if (s != null && s.isOpen()) {
+                    sendWithRetry(s, message, 1);
+                }
+            }
+        }
+        for (CopyOnWriteArrayList<Session> adminRoom : adminRooms.values()) {
+            for (Session s : adminRoom) {
+                if (s != null && s.isOpen()) {
+                    sendWithRetry(s, message, 1);
+                }
+            }
+        }
+    }
+
 }
 

+ 14 - 0
fs-qw-company-api/src/main/java/com/fs/app/controller/QwController.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.app.exception.FSException;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.utils.StringUtils;
+import com.fs.course.service.IFsUserCourseService;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qwApi.Params.QwApiParam;
@@ -33,6 +34,7 @@ import java.io.InputStream;
 import java.net.URI;
 import java.net.URL;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -43,6 +45,7 @@ public class QwController {
 
     private final IQwCompanyService qwCompanyService;
     private final RedisCacheT<String> redisCache;
+    private final IFsUserCourseService fsUserCourseService;
 
     @PostMapping("/post")
     public QwApiResult post(@RequestBody QwApiParam param) throws Exception {
@@ -98,6 +101,17 @@ public class QwController {
         return QwApiResult.ok(reJson);
     }
 
+
+    @PostMapping("/timer/processQwSopCourseMaterial")
+    public QwApiResult processQwSopCourseMaterialTimer(@RequestBody List<String> corpIds) {
+        if (corpIds == null || corpIds.isEmpty()) {
+            return QwApiResult.error("corpIds 为空");
+        }
+        log.info("定时任务-企微上传课程图片:本转发节点处理主体数 {}", corpIds.size());
+        fsUserCourseService.processQwSopCourseMaterialForCorpIds(corpIds);
+        return QwApiResult.ok("ok");
+    }
+
     @PostMapping("/uploadImg")
     public QwApiResult uploadImg(@RequestBody QwApiParam param) throws Exception {
         String uuid = UUID.randomUUID().toString();

+ 17 - 0
fs-service/src/main/java/com/fs/app/AppPayService.java

@@ -0,0 +1,17 @@
+package com.fs.app;
+
+import com.fs.common.core.domain.R;
+import com.fs.his.param.FsPackageOrderDoPayParam;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+
+
+import java.util.Map;
+
+public interface AppPayService {
+
+
+    /**
+     * 微信支付回调
+     */
+    String wxNotify(WxPayOrderNotifyResult result);
+}

+ 72 - 0
fs-service/src/main/java/com/fs/app/impl/AppPayServiceImpl.java

@@ -0,0 +1,72 @@
+package com.fs.app.impl;
+
+import com.fs.app.AppPayService;
+import com.fs.his.service.IFsInquiryOrderService;
+import com.fs.his.service.IFsPackageOrderService;
+import com.fs.his.service.IFsStoreOrderService;
+import com.fs.hisStore.service.IFsStoreOrderScrmService;
+import com.fs.live.service.ILiveOrderService;
+import com.fs.system.mapper.SysConfigMapper;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+public class AppPayServiceImpl implements AppPayService {
+
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    private IFsInquiryOrderService inquiryOrderService;
+    @Lazy
+    @Autowired
+    private IFsStoreOrderScrmService storeOrderService;
+
+    @Lazy
+    @Autowired
+    private ILiveOrderService liveOrderService;
+    @Autowired
+    private IFsPackageOrderService packageOrderService;
+
+    @Override
+    public String wxNotify(WxPayOrderNotifyResult result) {
+        log.info("微信回调参数: {}", result);
+        if (!"SUCCESS".equals(result.getReturnCode())){
+            return WxPayNotifyResponse.success("微信回调失败");
+        }
+
+        if (!"SUCCESS".equals(result.getResultCode())){
+            return WxPayNotifyResponse.success("交易失败");
+        }
+
+        String outTradeNo = result.getOutTradeNo();
+        String[] tradeNoArr = outTradeNo.split("-");
+        if (tradeNoArr.length < 2) {
+            log.info("微信回调订单号格式异常, outTradeNo: {}", outTradeNo);
+            return WxPayNotifyResponse.success("OK");
+        }
+        switch (tradeNoArr[0]) {
+            case "inquiry":
+                inquiryOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
+                break;
+            case "store":
+                storeOrderService.payConfirm(1, null, tradeNoArr[1], outTradeNo, result.getTransactionId(), null);
+                break;
+            case "live":
+                liveOrderService.payConfirm(1, null, tradeNoArr[1], outTradeNo, result.getTransactionId(), null);
+                break;
+            case "package":
+                packageOrderService.payConfirm("", tradeNoArr[1],"","",1,result.getTransactionId(),"");
+                break;
+            default:
+                log.warn("微信回调未知订单前缀, outTradeNo: {}", outTradeNo);
+                break;
+        }
+        return WxPayNotifyResponse.success("OK");
+    }
+
+}

+ 179 - 0
fs-service/src/main/java/com/fs/company/domain/RechargeRecord.java

@@ -0,0 +1,179 @@
+package com.fs.company.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 储值支付记录对象 recharge_record
+ *
+ * @author ruoyi
+ * @date 2026-03-17
+ */
+public class RechargeRecord extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 用户ID */
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    /** 用户姓名 */
+    @Excel(name = "用户姓名")
+    private String userName;
+
+    /** 储值金额 */
+    @Excel(name = "储值金额")
+    private BigDecimal totalAmount;
+
+    /** 交易流水号 */
+    @Excel(name = "交易流水号")
+    private String transactionId;
+
+    /** 订单ID */
+    private Long orderId;
+
+    /** 订单编号 */
+    private String orderNo;
+
+    /** 业务类型:0-充值,1-消费 */
+    @Excel(name = "业务类型", readConverterExp = "0=充值,1=消费")
+    private Integer businessType;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "公司名称")
+    private String companyName;
+
+    /** 删除标志:0-未删除,1-已删除 */
+    private String delFlag;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+    public void setUserId(Long userId)
+    {
+        this.userId = userId;
+    }
+
+    public Long getUserId()
+    {
+        return userId;
+    }
+    public void setUserName(String userName)
+    {
+        this.userName = userName;
+    }
+
+    public String getUserName()
+    {
+        return userName;
+    }
+    public void setTotalAmount(BigDecimal totalAmount)
+    {
+        this.totalAmount = totalAmount;
+    }
+
+    public BigDecimal getTotalAmount()
+    {
+        return totalAmount;
+    }
+    public void setTransactionId(String transactionId)
+    {
+        this.transactionId = transactionId;
+    }
+
+    public String getTransactionId()
+    {
+        return transactionId;
+    }
+    public Integer getBusinessType()
+    {
+        return businessType;
+    }
+
+    public void setBusinessType(Integer businessType)
+    {
+        this.businessType = businessType;
+    }
+    public Long getCompanyId()
+    {
+        return companyId;
+    }
+
+    public void setCompanyId(Long companyId)
+    {
+        this.companyId = companyId;
+    }
+    public String getCompanyName()
+    {
+        return companyName;
+    }
+
+    public void setCompanyName(String companyName)
+    {
+        this.companyName = companyName;
+    }
+    public String getDelFlag()
+    {
+        return delFlag;
+    }
+    public void setDelFlag(String delFlag)
+    {
+        this.delFlag = delFlag;
+    }
+
+    public Long getOrderId() {
+        return orderId;
+    }
+
+    public RechargeRecord setOrderId(Long orderId) {
+        this.orderId = orderId;
+        return this;
+    }
+
+    public String getOrderNo() {
+        return orderNo;
+    }
+
+    public RechargeRecord setOrderNo(String orderNo) {
+        this.orderNo = orderNo;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+                .append("id", getId())
+                .append("userId", getUserId())
+                .append("userName", getUserName())
+                .append("totalAmount", getTotalAmount())
+                .append("transactionId", getTransactionId())
+                .append("businessType", getBusinessType())
+                .append("companyId", getCompanyId())
+                .append("companyName", getCompanyName())
+                .append("remark", getRemark())
+                .append("createBy", getCreateBy())
+                .append("createTime", getCreateTime())
+                .append("updateBy", getUpdateBy())
+                .append("updateTime", getUpdateTime())
+                .append("delFlag", getDelFlag())
+                .toString();
+    }
+}

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyRoleMapper.java

@@ -85,4 +85,9 @@ public interface CompanyRoleMapper
      * **/
     CompanyRole selectCompanyRoleByRoleKey(@Param("roleKey") String roleKey);
     Long selectRolesByUserNameAndCompanyId(@Param("roleName") String roleName,@Param("companyId") Long companyId);
+
+    /**
+     * 去重角色名称(直播评论飘屏/置顶配置下拉用)
+     */
+    List<String> selectDistinctRoleNames();
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserRoleMapper.java

@@ -14,6 +14,9 @@ import java.util.List;
  */
 public interface CompanyUserRoleMapper
 {
+    @Select("SELECT COUNT(1) FROM company_user_role WHERE user_id = #{userId}")
+    int countByUserId(@Param("userId") Long userId);
+
     @Select("\n" +
             "SELECT \n" +
             "   cur.user_id\n" +

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyRoleService.java

@@ -109,4 +109,9 @@ public interface ICompanyRoleService
      * @return
      * **/
     int insertDefaultRole(CompanyRole role);
+
+    /**
+     * 去重角色名称(直播评论飘屏/置顶配置下拉用)
+     */
+    List<String> selectDistinctRoleNames();
 }

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -178,6 +178,13 @@ public interface ICompanyService
 
     void redPacketTopUpCompany(Long companyId, BigDecimal money,String type);
 
+    /**
+     * 获取企业红包余额(从 Redis 读取)
+     * @param companyId 企业ID
+     * @return 红包余额,未初始化或不存在时返回 BigDecimal.ZERO
+     */
+    BigDecimal getRedPackageMoney(Long companyId);
+
     void asyncRecordBalanceLog(Long companyId, BigDecimal money, Integer logType, BigDecimal balance, String remark, Long logId);
 
     void recordRedPacketBalance();

+ 21 - 20
fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java

@@ -283,37 +283,38 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
         if (companyId == null) {
             return R.error("公司ID不能为空");
         }
-        CompanyConfig config = companyConfigMapper.selectCompanyConfigByKey(companyId, CONFIG_KEY_MINI_APP);
-        if (config == null) {
-            return R.error("未找到本公司的小程序配置,请先维护小程序配置");
-        }
-        if (!companyId.equals(config.getCompanyId())) {
-            return R.error("无权修改其他公司配置");
-        }
-        String configValue = config.getConfigValue();
-        JSONObject json = StringUtils.isNotEmpty(configValue) ? JSON.parseObject(configValue) : new JSONObject();
-        // 填充 companyId,避免前端看到为 null
-        json.put("companyId", companyId);
-        if (param.getMiniAppMaster() != null) {
-            json.put("miniAppMaster", param.getMiniAppMaster());
+        List<CompanyMiniAppVO> playSourceMiniApps = companyConfigMapper.getCompanyMiniAppListByCompany(companyId);
+        if (playSourceMiniApps == null || playSourceMiniApps.isEmpty()) {
+            return R.error("未找到本公司的小程序配置,请先在点播播放源配置中维护本公司小程序");
         }
-        if (param.getMiniAppServer() != null) {
-            json.put("miniAppServer", param.getMiniAppServer());
+        CompanyConfig config = companyConfigMapper.selectCompanyConfigByKey(companyId, CONFIG_KEY_MINI_APP);
+        if (config != null) {
+            if (!companyId.equals(config.getCompanyId())) {
+                return R.error("无权修改其他公司配置");
+            }
+            String configValue = config.getConfigValue();
+            JSONObject json = StringUtils.isNotEmpty(configValue) ? JSON.parseObject(configValue) : new JSONObject();
+            json.put("companyId", companyId);
+            if (param.getMiniAppMaster() != null) {
+                json.put("miniAppMaster", param.getMiniAppMaster());
+            }
+            if (param.getMiniAppServer() != null) {
+                json.put("miniAppServer", param.getMiniAppServer());
+            }
+            config.setConfigValue(json.toJSONString());
+            companyConfigMapper.updateCompanyConfig(config);
         }
-        config.setConfigValue(json.toJSONString());
-        companyConfigMapper.updateCompanyConfig(config);
 
-        // 同步更新 company_miniapp 表,沿用 saveCompanyMiniApp 的逻辑
         SaveCompanyMiniAppParam saveParam = new SaveCompanyMiniAppParam();
         saveParam.setCompanyId(companyId);
-        // 目前前端是单选下拉,取数组第一个元素作为主/备小程序
+
         if (param.getMiniAppMaster() != null && !param.getMiniAppMaster().isEmpty()) {
             saveParam.setMainMiniAppId(param.getMiniAppMaster().get(0));
         }
         if (param.getMiniAppServer() != null && !param.getMiniAppServer().isEmpty()) {
             saveParam.setBackupMiniAppId(param.getMiniAppServer().get(0));
         }
-        // 这里不强制 updateBy,由调用方在需要时补充;目前公司端只按公司维度更新
+
         saveCompanyMiniApp(saveParam);
 
         return R.ok("操作成功");

+ 4 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyRoleServiceImpl.java

@@ -123,6 +123,10 @@ public class CompanyRoleServiceImpl implements ICompanyRoleService
         return permsSet;
     }
 
+    @Override
+    public List<String> selectDistinctRoleNames() {
+        return companyRoleMapper.selectDistinctRoleNames();
+    }
 
     @Override
     public String checkRoleNameUnique(CompanyRole role) {

+ 16 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -1715,6 +1715,22 @@ public class CompanyServiceImpl implements ICompanyService
                 }));
     }
 
+    @Override
+    public BigDecimal getRedPackageMoney(Long companyId) {
+        if (companyId == null) {
+            return BigDecimal.ZERO;
+        }
+        String moneyStr = redisCache.getCacheObject(FsConstants.COMPANY_MONEY_KEY + companyId);
+        if (StringUtils.isEmpty(moneyStr)) {
+            return BigDecimal.ZERO;
+        }
+        try {
+            return new BigDecimal(moneyStr);
+        } catch (NumberFormatException e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
     /**
      * @Description: 红包充值
      * @Param: type 充值类型 1 充值 2 扣款

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsCoursePlaySourceConfig.java

@@ -82,6 +82,11 @@ public class FsCoursePlaySourceConfig {
      */
     private Long companyId;
 
+    /**
+     * 所属公司ids(逗号分隔),用于一个小程序绑定多个公司
+     */
+    private String companyIds;
+
     /**
      * 销售公司ids 用于判定销售公司可见编辑列表
      */

+ 15 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseTrafficLog.java

@@ -66,6 +66,21 @@ public class FsCourseTrafficLog extends BaseEntity
      */
     private Long periodId;
 
+    /**
+     * appId
+     */
+    private String appId;
+
+    /**
+     * 来源类型标记1.小程序 2.app
+     */
+    private Integer typeFlag;
+
+    /**
+     * 是否公开课 1:是 0:否
+     */
+    private Integer isOpen;
+
 
 
 //    @JsonFormat(pattern = "yyyy-MM-dd")

+ 7 - 3
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -65,7 +65,7 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
      * @return 结果
      */
     public int updateFsUserCourseVideo(FsUserCourseVideo fsUserCourseVideo);
-    int updateVideoByVideoUrl(@Param("videoUrl") String videoUrl,@Param("thumbnail")String thumbnail, @Param("ids") List<Long> ids,@Param("fileName") String fileName);
+    int updateVideoByVideoUrl(@Param("videoUrl") String videoUrl,@Param("thumbnail")String thumbnail, @Param("ids") List<Long> ids,@Param("fileName") String fileName,@Param("duration") Integer duration);
     List<FsUserCourseVideo> selectByVideoUrl(String videoUrl);
 
     @Update("<script> " +
@@ -120,11 +120,13 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
 
 
     @Select({"<script> " +
-            "select v.video_id, v.title, v.video_url,v.package_json, v.thumbnail,v.duration as seconds, SEC_TO_TIME(v.duration) as duration,v.create_time, v.talent_id, v.course_id, " +
+            "select v.video_id, v.title, v.video_url,v.package_json, v.thumbnail,v.duration as seconds, SEC_TO_TIME(v.duration) as duration,v.create_time, v.talent_id, v.course_id, c.project as project, " +
+            " CAST(NULLIF(SUBSTRING_INDEX(REGEXP_REPLACE(c.company_ids, '[\"\\\\[\\\\]]', ''), ',', 1), '') AS UNSIGNED) as company_id, c.company_ids as company_ids, " +
             " v.status, v.course_sort,v.line_one,v.line_two,v.line_three,v.upload_type,1 as is_vip,CASE \n" +
             "        WHEN l.log_id IS NOT NULL THEN 1\n" +
             "        ELSE 0\n" +
             "    END AS is_buy  from fs_user_course_video v  " +
+            " left join fs_user_course c on c.course_id = v.course_id " +
             " left join fs_user_course_study_log l on l.video_id = v.video_id and l.user_id = #{maps.userId} and l.is_buy = 1 " +
             " where v.is_del = 0 and  v.course_id = #{maps.courseId} " +
             "<if test = ' maps.keyword!=null and maps.keyword != \"\" '> " +
@@ -135,8 +137,10 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     List<FsUserCourseVideoListUVO> selectFsUserCourseVideoListUVOByCourseId(@Param("maps") FsUserCourseVideoListUParam param);
 
     @Select({"<script> " +
-            "select v.video_id, v.title,v.package_json, v.video_url, v.thumbnail,v.duration as seconds, SEC_TO_TIME(v.duration) as duration,v.create_time, v.talent_id, v.course_id, " +
+            "select v.video_id, v.title,v.package_json, v.video_url, v.thumbnail,v.duration as seconds, SEC_TO_TIME(v.duration) as duration,v.create_time, v.talent_id, v.course_id, c.project as project, " +
+            " CAST(NULLIF(SUBSTRING_INDEX(REGEXP_REPLACE(c.company_ids, '[\"\\\\[\\\\]]', ''), ',', 1), '') AS UNSIGNED) as company_id, c.company_ids as company_ids, " +
             " v.status, v.course_sort,v.line_one,v.line_two,v.line_three,v.upload_type,1 as is_vip,0 as is_buy  from fs_user_course_video v  " +
+            " left join fs_user_course c on c.course_id = v.course_id " +
             " where v.is_del = 0 and  v.course_id = #{maps.courseId}   " +
             "<if test = ' maps.keyword!=null and maps.keyword != \"\" '> " +
             "and v.title like CONCAT('%',#{maps.keyword},'%') " +

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseLinkCreateParam.java

@@ -40,4 +40,6 @@ public class FsCourseLinkCreateParam {
 
     private Long projectId;//项目ID
 
+    private String type; // 1-app
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigCreateParam.java

@@ -3,6 +3,7 @@ package com.fs.course.param;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.util.List;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
 
@@ -45,6 +46,9 @@ public class FsCoursePlaySourceConfigCreateParam {
 
     @ApiModelProperty("所属公司")
     private Long companyId;
+
+    @ApiModelProperty("所属公司(多选)")
+    private List<Long> companyIds;
     /**
      * 销售公司ids 用于判定销售公司可见编辑列表
      */

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCoursePlaySourceConfigEditParam.java

@@ -3,6 +3,7 @@ package com.fs.course.param;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
+import java.util.List;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
 
@@ -43,6 +44,9 @@ public class FsCoursePlaySourceConfigEditParam {
     @ApiModelProperty("所属公司")
     private Long companyId;
 
+    @ApiModelProperty("所属公司(多选)")
+    private List<Long> companyIds;
+
     /**
      * 销售公司ids 用于判定销售公司可见编辑列表
      */

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java

@@ -39,4 +39,8 @@ public class FsCourseSendRewardUParam implements Serializable
 
     private String code;
 
+    private Integer rewardType;//奖励类型 1红包 2积分
+
+    private Long watchLogId;
+
 }

+ 3 - 1
fs-service/src/main/java/com/fs/course/param/FsUserCourseVideoFinishUParam.java

@@ -21,7 +21,9 @@ public class FsUserCourseVideoFinishUParam implements Serializable {
     private Long qwExternalId;
     private Integer linkType;
     private Integer isRoom;//是否群聊
-    private Integer isOpen;//是否公开课
+    private Integer isOpen = 0;//是否公开课
     private Long periodId;
     private Integer projectId;
+    private String appId;
+    private Integer typeFlag;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/param/newfs/FsCourseSortLinkParam.java

@@ -41,4 +41,7 @@ public class FsCourseSortLinkParam {
     @ApiModelProperty(value = "项目id")
     private Long projectId;
 
+    @ApiModelProperty(value = "app")
+    private String type;
+
 }

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

@@ -110,6 +110,11 @@ public interface IFsUserCourseService {
 
     void processQwSopCourseMaterialTimer();
 
+    /**
+     * 仅处理指定 corpId 列表的课程封面上传企微(供分布式定时任务节点调用)
+     */
+    void processQwSopCourseMaterialForCorpIds(List<String> corpIds);
+
     List<FsCourseListBySidebarVO> getFsCourseListBySidebar(FsCourseListBySidebarParam param);
 
     List<FsCourseListBySidebarVO> getFsCourseListBySidebarToday(FsCourseListBySidebarParam param);

+ 3 - 1
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -207,7 +207,7 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
     R checkUserInfo(long l);
 
     R updateVideo();
-    AjaxResult updateVideoByVideoUrl(String videoUrl,String oldVideoUrl, String thumbnail,String fileName);
+    AjaxResult updateVideoByVideoUrl(String videoUrl,String oldVideoUrl, String thumbnail,String fileName, Integer duration);
 
     R checkUserInfo(Long userId);
 
@@ -281,4 +281,6 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * @return list
      */
     List<OptionsVO> selectVideoOptionsByCourseId(Long courseId);
+
+    R withdrawal(FsCourseSendRewardUParam param);
 }

+ 155 - 21
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -9,6 +9,10 @@ import java.net.URL;
 import java.net.URLConnection;
 import java.util.*;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
@@ -60,7 +64,12 @@ import org.checkerframework.checker.units.qual.A;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
 import com.fs.course.service.IFsUserCourseService;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -76,6 +85,9 @@ import javax.imageio.ImageIO;
 @Slf4j
 public class FsUserCourseServiceImpl implements IFsUserCourseService
 {
+    /** 单节点内按 corp 并行上传时的最大线程数(过大易触发企微频控) */
+    private static final int QW_COURSE_MATERIAL_CORP_PARALLELISM = 8;
+
     @Autowired
     private CompanyTagMapper companyTagMapper;
     @Autowired
@@ -138,7 +150,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
 
     private static final String userRealLink = "/pages/user/users/becomeVIP?";
 
-    private static final String appRealLink = "/#/pages_course/videovip?course=";
+    private static final String appRealLink = "/#/appcourse/pages_course/videovip?course=";
     public static final String appShortLink = "/#/pages_course/videovip?s=";
 
     /**
@@ -495,34 +507,145 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
 
     /**
      * 定时任务 - 处理企业微信SOP课程素材
-     * 每2天执行一次,将课程封面图片上传到企业微信素材库并缓存mediaId
+     * 按 {@link QwCompany#getQwApiUrl()} 分组:无转发地址的在当前机器执行;有转发地址的并行 HTTP
      */
     @Override
     public void processQwSopCourseMaterialTimer() {
-        // 获取所有需要处理的课程列表
-        List<FsUserCourse> fsUserCourses = fsUserCourseMapper.selectFsUserCourseAllCourseByQw();
-        // 获取所有企业微信配置
         List<QwCompany> companies = iQwCompanyService.selectQwCompanyList(new QwCompany());
-
-        // 遍历每个企业微信配置
+        Map<String, LinkedHashSet<String>> bucket = new LinkedHashMap<>();
         for (QwCompany company : companies) {
             String corpId = company.getCorpId();
-            if (corpId == null) {
+            if (corpId == null || StringUtils.isEmpty(corpId)) {
                 continue;
             }
+            String forwardKey = StringUtils.isNotEmpty(company.getQwApiUrl())
+                    ? normalizeQwForwardBaseUrl(company.getQwApiUrl())
+                    : "";
+            bucket.computeIfAbsent(forwardKey, k -> new LinkedHashSet<>()).add(corpId);
+        }
+        if (bucket.isEmpty()) {
+            log.warn("processQwSopCourseMaterialTimer: 无有效企微主体 corpId,跳过");
+            return;
+        }
+        int parallelTasks = (int) bucket.entrySet().stream().filter(e -> !e.getValue().isEmpty()).count();
+        int poolSize = Math.min(Math.max(parallelTasks, 1), 32);
+        ExecutorService executor = Executors.newFixedThreadPool(poolSize);
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        try {
+            for (Map.Entry<String, LinkedHashSet<String>> e : bucket.entrySet()) {
+                List<String> corpIds = new ArrayList<>(e.getValue());
+                if (corpIds.isEmpty()) {
+                    continue;
+                }
+                String forwardBase = e.getKey();
+                if (forwardBase.isEmpty()) {
+                    futures.add(CompletableFuture.runAsync(
+                            () -> processQwSopCourseMaterialForCorpIds(corpIds), executor));
+                } else {
+                    String remoteBase = forwardBase;
+                    futures.add(CompletableFuture.runAsync(
+                            () -> invokeRemoteProcessQwSopCourseMaterial(remoteBase, corpIds), executor));
+                }
+            }
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        } finally {
+            executor.shutdown();
+            try {
+                if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    executor.shutdownNow();
+                }
+            } catch (InterruptedException ie) {
+                executor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
 
-            // 遍历每个课程,上传图片到对应企业的素材库
-            for (FsUserCourse course : fsUserCourses) {
-                try {
-                    uploadCourseImage(course, corpId);
-                } catch (Exception e) {
-                    log.error("处理课程图片失败: courseId={}, corpId={}, error={}",
-                            course.getCourseId(), corpId, e.getMessage());
+    @Override
+    public void processQwSopCourseMaterialForCorpIds(List<String> corpIds) {
+        if (corpIds == null || corpIds.isEmpty()) {
+            return;
+        }
+        List<String> validCorpIds = corpIds.stream()
+                .filter(id -> !StringUtils.isEmpty(id))
+                .collect(Collectors.toList());
+        if (validCorpIds.isEmpty()) {
+            return;
+        }
+        List<FsUserCourse> fsUserCourses = fsUserCourseMapper.selectFsUserCourseAllCourseByQw();
+        if (fsUserCourses == null || fsUserCourses.isEmpty()) {
+            return;
+        }
+        int poolSize = Math.min(validCorpIds.size(), QW_COURSE_MATERIAL_CORP_PARALLELISM);
+        ExecutorService corpExecutor = Executors.newFixedThreadPool(poolSize);
+        List<CompletableFuture<Void>> corpFutures = new ArrayList<>();
+        try {
+            for (String corpId : validCorpIds) {
+                corpFutures.add(CompletableFuture.runAsync(
+                        () -> uploadAllCoursesForOneCorp(fsUserCourses, corpId), corpExecutor));
+            }
+            CompletableFuture.allOf(corpFutures.toArray(new CompletableFuture[0])).join();
+        } finally {
+            corpExecutor.shutdown();
+            try {
+                if (!corpExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    corpExecutor.shutdownNow();
                 }
+            } catch (InterruptedException ie) {
+                corpExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * 单个主体:按课程顺序上传(同一 corp 内保持串行,避免企微侧无序竞态)
+     */
+    private void uploadAllCoursesForOneCorp(List<FsUserCourse> fsUserCourses, String corpId) {
+        for (FsUserCourse course : fsUserCourses) {
+            try {
+                uploadCourseImage(course, corpId);
+            } catch (Exception e) {
+                log.error("处理课程图片失败: courseId={}, corpId={}, error={}",
+                        course.getCourseId(), corpId, e.getMessage());
             }
         }
     }
 
+    /**
+     * 正规化转发地址
+     * @param qwApiUrl
+     * @return
+     */
+    private static String normalizeQwForwardBaseUrl(String qwApiUrl) {
+        String s = qwApiUrl.trim();
+        while (s.endsWith("/")) {
+            s = s.substring(0, s.length() - 1);
+        }
+        return s;
+    }
+
+    /**
+     * 调用部署在转发地址上的 fs-qw-company-api:/timer/processQwSopCourseMaterial
+     */
+    private void invokeRemoteProcessQwSopCourseMaterial(String forwardBaseUrl, List<String> corpIds) {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(60_000);
+        factory.setReadTimeout(6 * 60 * 60 * 1000);
+        RestTemplate restTemplate = new RestTemplate(factory);
+        String url = forwardBaseUrl + "/timer/processQwSopCourseMaterial";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(corpIds), headers);
+        try {
+            log.info("企微课程素材定时任务:HTTP 分发至 {},主体数 {}", url, corpIds.size());
+            restTemplate.postForEntity(url, entity, String.class);
+        } catch (Exception ex) {
+            log.error("企微课程素材定时任务:远程节点调用失败 url={}, corpCount={}, err={}",
+                    url, corpIds.size(), ex.getMessage(), ex);
+        }
+    }
+
     @Override
     public List<FsCourseListBySidebarVO> getFsCourseListBySidebar(FsCourseListBySidebarParam param) {
         return  fsUserCourseMapper.getFsCourseListBySidebar(param);
@@ -597,7 +720,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         BeanUtils.copyProperties(link, courseMap);
         courseMap.setProjectId(param.getProjectId());
         String courseJson = JSON.toJSONString(courseMap);
-        link.setRealLink(realLink + courseJson);
+        link.setRealLink(("1".equals(param.getType()) ?appRealLink:realLink) + courseJson);
 
         link.setCreateTime(new Date());
 
@@ -605,10 +728,17 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
         Calendar calendar = getExpireDay(param, config, link.getCreateTime());
         link.setUpdateTime(calendar.getTime());
         int i = fsCourseLinkMapper.insertFsCourseLink(link);
-        if (i > 0){
-            String domainName = getDomainName(param.getCompanyUserId(), config);
-            String sortLink = domainName + shortLink + link.getLink();
-            return R.ok().put("url", sortLink).put("link", random);
+        if (i > 0) {
+            if ("1".equals(param.getType())) {
+                String domainName = getDomainName(param.getCompanyUserId(), config);
+                String sortLink = domainName + link.getRealLink().replace("/#", "");
+                sortLink = sortLink.replaceAll("\\\\", "");
+                return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());
+            } else {
+                String domainName = getDomainName(param.getCompanyUserId(), config);
+                String sortLink = domainName + shortLink + link.getLink();
+                return R.ok().put("url", sortLink).put("link", random).put("linkId", link.getLinkId());
+            }
         }
         return R.error("生成链接失败!");
     }
@@ -793,8 +923,12 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
                 String sortLink = domainName + link.getRealLink().replace("/#","");
                 return R.ok().put("url", sortLink).put("link", random);
             }
+//            String domainName = getDomainName(param.getCompanyUserId(), config);
+//            String sortLink = domainName + appShortLink + link.getLink();
+//            return R.ok().put("url", sortLink).put("link", random);
             String domainName = getDomainName(param.getCompanyUserId(), config);
-            String sortLink = domainName + appShortLink + link.getLink();
+            String sortLink = domainName+"/courseH5"+ link.getRealLink().replace("/#","");
+            sortLink = sortLink.replaceAll("\\\\", "");
             return R.ok().put("url", sortLink).put("link", random);
         }
         return R.error("生成链接失败!");

+ 533 - 38
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -87,6 +87,7 @@ import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.voice.utils.StringUtil;
 import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
 import com.volcengine.service.vod.IVodService;
 import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
 import com.volcengine.service.vod.model.request.*;
@@ -1643,15 +1644,22 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             switch (config.getRewardType()) {
                 // 红包奖励
                 case 1:
-                    return sendRedPacketReward(param, user, watchLog, video, config);
+                    if (param.getSource() == 3){
+                        param.setWatchLogId(watchLog.getLogId());
+                        return withdrawal(param);
+                    } else {
+                        return sendRedPacketReward(param, user, watchLog, video, config);
+                    }
                 // 积分奖励
                 case 2:
                     return sendIntegralReward(param, user, watchLog, config);
                 // 红包+积分
                 case 3:
-                    R sendRed = sendRedPacketReward(param, user, watchLog, video, config);
-                    if (!Objects.equals(sendRed.get("code"), 200)) {
-                        return sendRed;
+                    if (isUserRedPacketReceiveEnabled(user)) {
+                        R sendRed = sendRedPacketReward(param, user, watchLog, video, config);
+                        if (!Objects.equals(sendRed.get("code"), 200)) {
+                            return sendRed;
+                        }
                     }
                     return sendIntegralReward(param, user, watchLog, config);
                 default:
@@ -1757,7 +1765,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             if (log.getRewardType() == 1) {
                 FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
-                    return R.error("已领取该课程奖励,不可重复领取!");
+                    return R.error("已领取该课程奖励,不可重复领取!").put("data",fsCourseRedPacketLog.getResult());
                 }
                 if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
                     if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
@@ -1779,25 +1787,39 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
-        // 判断来源是否是app,如是app,则发放积分奖励
-        int sourceApp = 3;
-        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
-            return sendIntegralReward(param, user, log, config);
-        }
+//        // 判断来源是否是app,如是app,则发放积分奖励
+//        int sourceApp = 3;
+//        if (sourceApp == param.getSource() && !CloudHostUtils.hasCloudHostName("中康")) {
+//            return sendIntegralReward(param, user, log, config);
+//        }
 
         // 根据奖励类型发放不同奖励
         switch (config.getRewardType()) {
             // 红包奖励
             case 1:
-                return sendRedPacketRewardFsUser(param, user, log, video, config);
+                if (param.getSource()==3){
+                    WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                    String openId = getOpenId(param, user);
+                    if (StringUtils.isBlank(openId)) {
+                        return R.error("请重新使用微信登录");
+                    }
+                    packetParam.setOpenId(openId);
+                    BeanUtils.copyProperties(param, packetParam);
+                    packetParam.setUser(user);
+                    return sendAppRedPacket(packetParam, log,video, config);
+                } else {
+                    return sendRedPacketRewardFsUser(param, user, log, video, config);
+                }
             // 积分奖励
             case 2:
                 return sendIntegralReward(param, user, log, config);
             // 红包+积分
             case 3:
-                R sendRed = sendRedPacketRewardFsUser(param, user, log, video, config);
-                if (!Objects.equals(sendRed.get("code"), 200)) {
-                    return sendRed;
+                if (isUserRedPacketReceiveEnabled(user)) {
+                    R sendRed = sendRedPacketRewardFsUser(param, user, log, video, config);
+                    if (!Objects.equals(sendRed.get("code"), 200)) {
+                        return sendRed;
+                    }
                 }
                 return sendIntegralReward(param, user, log, config);
             default:
@@ -1817,6 +1839,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      */
     private R sendRedPacketReward(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog log, FsUserCourseVideo video, CourseConfig config) {
         logger.info("进入发放红包");
+        if (!isUserRedPacketReceiveEnabled(user)) {
+            return R.ok("恭喜您,答题成功啦 !");
+        }
         // 确定红包金额
         BigDecimal amount = BigDecimal.ZERO;
         FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyId(param.getVideoId(), param.getCompanyId(), param.getPeriodId());
@@ -1911,6 +1936,47 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
     }
 
+    /** 发红包接口返回非成功时,优先透传支付层返回的 msg */
+    private static String resolveSendRedPacketFailMsg(R sendRedPacket) {
+        if (sendRedPacket == null) {
+            return "奖励发送失败,请联系客服";
+        }
+        Object msgObj = sendRedPacket.get("msg");
+        if (msgObj != null) {
+            String m = String.valueOf(msgObj).trim();
+            if (StringUtils.isNotEmpty(m) && !"success".equalsIgnoreCase(m)) {
+                return m;
+            }
+        }
+        return "奖励发送失败,请联系客服";
+    }
+
+    private static boolean isWxAppIdMchIdNotMatch(Throwable t) {
+        while (t != null) {
+            if (t instanceof WxPayException) {
+                WxPayException wx = (WxPayException) t;
+                if ("APPID_MCHID_NOT_MATCH".equals(wx.getErrCode())) {
+                    return true;
+                }
+                String des = wx.getErrCodeDes();
+                if (des != null && des.contains("商户号和appid没有绑定关系")) {
+                    return true;
+                }
+            }
+            // 兜底:有些场景下异常类型/字段未必能完全取到,只要 message 中包含关键字也认为是该错误
+            String msg = t.getMessage();
+            if (msg != null && (msg.contains("APPID_MCHID_NOT_MATCH") || msg.contains("商户号和appid没有绑定关系"))) {
+                return true;
+            }
+            String str = t.toString();
+            if (str != null && (str.contains("APPID_MCHID_NOT_MATCH") || str.contains("商户号和appid没有绑定关系"))) {
+                return true;
+            }
+            t = t.getCause();
+        }
+        return false;
+    }
+
     private R sendRedPacketRewardToUser(FsCourseSendRewardUParam param, FsCourseWatchLog log, CourseConfig config, WxSendRedPacketParam packetParam, BigDecimal amount) {
 
 
@@ -1998,6 +2064,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 // 异常时回滚余额
 
                 rollbackBalance(balanceRollbackError);
+                if (isWxAppIdMchIdNotMatch(e)) {
+                    return R.error("未绑定该小程序,请联系群主");
+                }
                 return R.error("奖励发送失败,请联系客服");
             }
 
@@ -2038,7 +2107,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             } else {
                 // 登记回滚流水表
                 rollbackBalance(balanceRollbackError);
-                return R.error("奖励发送失败,请联系客服");
+                String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
+                if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
+                    return R.error("商户号异常,请联系群主");
+                }
+                return R.error(failMsg);
             }
 
 
@@ -2082,7 +2155,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                 return sendRedPacket;
             } else {
-                return R.error("奖励发送失败,请联系客服");
+                String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
+                if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
+                    return R.error("商户号异常,请联系群主");
+                }
+                return R.error(failMsg);
             }
         }
     }
@@ -2132,6 +2209,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             return R.error(403,"已超过领取红包时间");
         }
 
+        if (!isUserRedPacketReceiveEnabled(user)) {
+            return R.ok("恭喜您,答题成功啦 !");
+        }
 
         // 确定红包金额
         BigDecimal amount = BigDecimal.ZERO;
@@ -2262,6 +2342,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     // 异常时回滚余额
 
                     rollbackBalance(balanceRollbackError);
+                    if (isWxAppIdMchIdNotMatch(e)) {
+                        return R.error("未绑定该小程序,请联系群主");
+                    }
                     return R.error("奖励发送失败,请联系客服");
                 }
 
@@ -2308,7 +2391,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 } else {
                     // 发送失败,回滚余额
                     rollbackBalance(balanceRollbackError);
-                    return R.error("奖励发送失败,请联系客服");
+                    String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
+                    if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定关系"))) {
+                        return R.error("小程序未绑定,请联系群主");
+                    }
+                    return R.error(failMsg);
                 }
 
                 // ===================== 本次修改目的为了实时扣减公司余额=====================
@@ -2356,7 +2443,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
                      return sendRedPacket;
                  } else {
-                     return R.error("奖励发送失败,请联系客服");
+                    String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
+                    if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
+                        return R.error("商户号异常,请联系群主");
+                    }
+                    return R.error(failMsg);
                  }
              }catch (Exception e){
                  return R.error(e.getMessage());
@@ -2454,7 +2545,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         R sendRedPacket = paymentService.sendRedPacket(packetParam);
 
         if (!sendRedPacket.get("code").equals(200)) {
-            return R.error("奖励发送失败,请联系客服");
+            String failMsg = resolveSendRedPacketFailMsg(sendRedPacket);
+            if (failMsg != null && (failMsg.contains("APPID_MCHID_NOT_MATCH") || failMsg.contains("商户号和appid没有绑定"))) {
+                return R.error("商户号异常,请联系群主");
+            }
+            return R.error(failMsg);
         }
 
         createRedPacketLog(sendRedPacket, param, amount, log);
@@ -2507,25 +2602,48 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
      * @param source    来源 1公众号 2小程序
      * @return openId
      */
-    private String getOpenId(Long userId, Long companyId, Integer source) {
-        Company company = companyMapper.selectCompanyById(companyId);
-        String appId = source == 1 ? company.getCourseMaAppId() : company.getCourseMiniAppId();
+    private String getOpenId(FsCourseSendRewardUParam param, FsUser user) {
+        Integer source = param.getSource();
+        Long userId = param.getUserId();
+        switch (source) {
+            case 1:
+                Company company = companyMapper.selectCompanyById(param.getCompanyId());
+                String appId = company.getCourseMaAppId();
 
-        // 公司配置为空时获取默认配置
-        if (StringUtils.isBlank(appId)) {
-            String json = configService.selectConfigByKey("course.config");
-            CourseConfig config = JSON.parseObject(json, CourseConfig.class);
-            appId = source == 1 ? config.getMpAppId() : config.getMiniprogramAppid();
-        }
+                // 公司配置为空时获取默认配置
+                if (StringUtils.isBlank(appId)) {
+                    String json = configService.selectConfigByKey("course.config");
+                    CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+                    appId = config.getMpAppId();
+                }
 
-        // 查询openId
-        Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery().eq(FsUserWx::getFsUserId, userId).eq(FsUserWx::getAppId, appId);
-        FsUserWx fsUserWx = fsUserWxService.getOne(queryWrapper);
-        if (Objects.isNull(fsUserWx)) {
-            throw new CustomException("获取openId失败");
-        }
+                // 查询openId
+                Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery().eq(FsUserWx::getFsUserId, userId).eq(FsUserWx::getAppId, appId);
+                FsUserWx fsUserWx = fsUserWxService.getOne(queryWrapper);
+                if (Objects.isNull(fsUserWx)) {
+                    throw new CustomException("获取openId失败");
+                }
+
+                return fsUserWx.getOpenId();
+            case 2:
+                FsUserWx userWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),userId,1);
+                if (Objects.nonNull(userWx) && StringUtils.isNotBlank(userWx.getOpenId())) {
+                    return userWx.getOpenId();
+                }
 
-        return fsUserWx.getOpenId();
+                if (StringUtils.isNotBlank(user.getCourseMaOpenId())) {
+                    try {
+                        handleFsUserWx(user,param.getAppId());
+                    } catch (Exception e){
+                        log.error("【更新或插入用户与小程序的绑定关系失败】:{}", userId, e);
+                    }
+                    return user.getCourseMaOpenId();
+                }
+                break;
+            case 3:
+                return user.getAppOpenId();
+        }
+        return null;
     }
 
     /**
@@ -3073,7 +3191,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 // 查询视频是否设置了红包,没有就不提示
                 Integer fsUserCourseVideoRedPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyCount(param.getVideoId(), null, param.getPeriodId());
                 if (fsUserCourseVideoRedPackage > 0) {
-                    tipsTime = courseVideoDetails.getDuration() / 3;
+                    // tipsTime = courseVideoDetails.getDuration() / 3;
                     tipsTime2 = (courseVideoDetails.getDuration() * 2) / 3;
                 }
             }
@@ -3862,14 +3980,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
     @Override
-    public AjaxResult updateVideoByVideoUrl(String videoUrl,String oldVideoUrl, String thumbnail,String fileName) {
+    public AjaxResult updateVideoByVideoUrl(String videoUrl,String oldVideoUrl, String thumbnail,String fileName, Integer duration) {
         List<FsUserCourseVideo> videoList = fsUserCourseVideoMapper.selectByVideoUrl(oldVideoUrl);
         if (CollectionUtils.isEmpty(videoList)){
             log.warn("根据videoUrl:{} 未查询到fs_user_course_video表数据",oldVideoUrl);
             return AjaxResult.success();
         }
         List<Long> idList = videoList.stream().map(FsUserCourseVideo::getVideoId).collect(Collectors.toList());
-        fsUserCourseVideoMapper.updateVideoByVideoUrl(videoUrl,thumbnail, idList,fileName);
+        fsUserCourseVideoMapper.updateVideoByVideoUrl(videoUrl,thumbnail, idList,fileName,duration);
         return AjaxResult.success();
     }
     @Override
@@ -4887,5 +5005,382 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     public List<OptionsVO> selectVideoOptionsByCourseId(Long courseId) {
         return fsUserCourseVideoMapper.selectVideoOptionsByCourseId(courseId);
     }
+
+    /**
+     * 用户提现
+     * @param param
+     */
+    @Override
+    @Transactional
+    public R withdrawal(FsCourseSendRewardUParam param) {
+        Long userId = param.getUserId();
+        // 生成锁的key,基于用户ID和视频ID确保同一用户同一视频的请求被锁定
+        String lockKey = "reward_red_lock:user:" + userId;
+        RLock lock = redissonClient.getLock(lockKey);
+
+        try {
+            // 尝试获取锁,等待时间5秒,锁过期时间30秒
+            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
+            if (!isLocked) {
+                logger.warn("获取锁失败,用户ID:{}", userId);
+                return R.error("操作频繁,请稍后再试!");
+            }
+
+            logger.info("成功获取锁,开始处理奖励发放,用户ID:{}", userId);
+            return executeWithdrawal(param);
+
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            logger.error("获取锁被中断,用户ID:{}", userId, e);
+            return R.error("系统繁忙,请重试!");
+        } finally {
+            // 释放锁
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+                logger.info("释放锁成功,用户ID:{}", userId);
+            }
+        }
+    }
+
+    private R executeWithdrawal(FsCourseSendRewardUParam param){
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+
+        FsCourseWatchLog watchLog = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
+        if (watchLog == null) {
+            watchLog = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
+        }
+        if (watchLog == null) {
+            return R.error("无记录");
+        }
+
+        if (watchLog.getLogType() != 2) {
+            return R.error("未完课");
+        }
+
+        FsCourseAnswerLogs rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), param.getQwUserId());
+        if (rightLog == null) {
+            logger.error("未答题:{}", param.getUserId());
+            return R.error("未答题");
+        }
+
+        FsCourseRedPacketLog fsCourseRedPacketLog = redPacketLogMapper.selectUserFsCourseRedPacketLog(param.getVideoId(), param.getUserId(), param.getPeriodId());
+
+        if (watchLog.getRewardType() != null) {
+            if (watchLog.getRewardType() == 1) {
+                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 1) {
+                    return R.error("已领取该课程奖励,不可重复领取!");
+                }
+                if (fsCourseRedPacketLog != null && fsCourseRedPacketLog.getStatus() == 0) {
+                    if (StringUtils.isNotEmpty(fsCourseRedPacketLog.getResult())) {
+                        R r = JSON.parseObject(fsCourseRedPacketLog.getResult(), R.class);
+                        return r;
+                    } else {
+                        return R.error("操作频繁,请稍后再试!");
+                    }
+                }
+            } else if (watchLog.getRewardType() == 2) {
+                return R.error("已领取该课程奖励,不可重复领取!");
+            }
+        }
+
+        // 获取视频信息
+        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+
+        // 获取配置信息
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        // 判断来源是否是app,如是app,则发放积分奖励
+//        int sourceApp = 3;
+//        if (sourceApp == param.getSource() /*&& !CloudHostUtils.hasCloudHostName("中康")*/) {
+//            return sendIntegralReward(param, user, log, config);
+//        }
+        if (ObjectUtils.isEmpty(param.getRewardType())){
+            param.setRewardType(config.getRewardType());
+        }
+        // 根据奖励类型发放不同奖励
+        switch (param.getRewardType()) {
+            // 红包奖励
+            case 1:
+                //来源是小程序切换openId
+                WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                String openId = getOpenId(param, user);
+                if (StringUtils.isBlank(openId)) {
+                    return R.error("请重新使用微信登录");
+                }
+                packetParam.setOpenId(openId);
+                BeanUtils.copyProperties(param, packetParam);
+                packetParam.setUser(user);
+
+                return sendAppRedPacket(packetParam, watchLog,video, config);
+            // 积分奖励
+            case 2:
+                return sendIntegralReward(param, user, watchLog, config);
+//            // 转盘
+//            case 3:
+//                return drawTurntable(param, user, log);
+//            // 保底转盘
+//            case 4:
+//                return drawTurntableGuarantee(param, user, log);
+            default:
+                return R.error("参数错误!");
+        }
+    }
+
+    private R sendAppRedPacket(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
+        // 仅手动发课(有营期)校验红包领取截止时间;自动发课不做该限制
+        if (log.getPeriodId() != null) {
+            FsUserCoursePeriodDays periodDays = new FsUserCoursePeriodDays();
+            periodDays.setVideoId(log.getVideoId());
+            periodDays.setPeriodId(log.getPeriodId());
+            //正常情况是只能查询到一条,之前可能存在重复的脏数据,暂使用查询list的方式
+            List<FsUserCoursePeriodDays> fsUserCoursePeriodDays = fsUserCoursePeriodDaysMapper.selectFsUserCoursePeriodDaysList(periodDays);
+            if (fsUserCoursePeriodDays != null && !fsUserCoursePeriodDays.isEmpty()) {
+                periodDays = fsUserCoursePeriodDays.get(0);
+            }
+            if (periodDays.getLastJoinTime() != null && LocalDateTime.now().isAfter(periodDays.getLastJoinTime())) {
+                return R.error(403, "已超过领取红包时间");
+            }
+        }
+
+        if (packetParam.getUser() != null && !isUserRedPacketReceiveEnabled(packetParam.getUser())) {
+            return R.ok("恭喜您,答题成功啦 !");
+        }
+
+        // 确定红包金额
+        BigDecimal amount = BigDecimal.ZERO;
+        FsUserCourseVideoRedPackage redPackage = fsUserCourseVideoRedPackageMapper.selectRedPacketByCompanyId(log.getVideoId(), log.getCompanyId(), log.getPeriodId());
+
+        if (redPackage != null && redPackage.getRedPacketMoney() != null) {
+            amount = redPackage.getRedPacketMoney();
+        } else if (video != null && video.getRedPacketMoney() != null) {
+            amount = video.getRedPacketMoney();
+        }
+        packetParam.setAmount(amount);
+
+        if (amount.compareTo(BigDecimal.ZERO) > 0) {
+
+            // 打开红包扣减功能
+            if ("1".equals(config.getIsRedPackageBalanceDeduction())) {
+                // 先注释 20251024 redis 余额 充值没有考虑 其余扣减没有考虑
+                // ===================== 20251022 xgb 修改 本次修改目的为了实时扣减公司余额=====================
+                // 1 使用redis缓存加锁 预扣减余额 红包发送失败 恢复redis缓存余额,如果回滚失败登记异常记录表 定时任务重新回滚余额
+                // 2 另起定时任务 同步缓存余额到redis中
+                // 3 注意!!!!! 启动系统时查询公司账户余额(这个时候要保证余额正确)启动会自动保存到redis缓存中
+                // 注意!!!!! 打开这个开关前记得检测redis缓存余额是否正确 若不正确 修改数据库字段red_package_money,删除redis缓存,重启系统,
+
+
+                // 预设值异常对象
+
+                BalanceRollbackError balanceRollbackError = new BalanceRollbackError();
+                balanceRollbackError.setCompanyId(packetParam.getCompanyId());
+                balanceRollbackError.setUserId(log.getUserId());
+                balanceRollbackError.setLogId(log.getLogId());
+                balanceRollbackError.setVideoId(log.getVideoId());
+                balanceRollbackError.setStatus(0);
+                balanceRollbackError.setMoney(amount);
+
+                if (packetParam.getCompanyId() == null) {
+                    logger.error("发送红包参数错误,公司不能为空,异常请求参数{}", packetParam);
+                    return R.error("发送红包失败,请联系管理员");
+                }
+                String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + packetParam.getCompanyId();
+
+                // 第一次加锁:预扣减余额
+                RLock lock1 = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + packetParam.getCompanyId());
+                boolean lockAcquired = false;
+                BigDecimal newMoney;
+                try {
+                    if (lock1.tryLock(3, 10, TimeUnit.SECONDS)) {
+                        lockAcquired = true;
+                        BigDecimal originalMoney;
+                        // 获取当前余额
+                        String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+                        if (StringUtils.isNotEmpty(moneyStr)) {
+                            originalMoney = new BigDecimal(moneyStr);
+                        } else {
+                            // 缓存没有值,重启系统恢复redis数据 保证数据正确性
+                            logger.error("发送红包获取redis余额缓存异常,异常请求参数{}", packetParam);
+                            return R.error("系统异常,请稍后重试");
+                        }
+
+                        if (originalMoney.compareTo(BigDecimal.ZERO) < 0) {
+                            logger.error("服务商余额不足,异常请求参数{}", packetParam);
+                            return R.error("服务商余额不足,请联系群主服务器充值!");
+                        }
+
+                        // 预扣减金额
+                        newMoney = originalMoney.subtract(amount);
+                        redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+                    } else {
+                        logger.error("获取redis锁失败,异常请求参数{}", packetParam);
+                        return R.error("系统繁忙,请稍后重试");
+                    }
+                } catch (Exception e) {
+                    logger.error("预扣减余额失败: 异常请求参数{},异常信息{}", packetParam, e.getMessage(), e);
+                    return R.error("系统异常,请稍后重试");
+                } finally {
+                    // 只有在成功获取锁的情况下才释放锁
+                    if (lockAcquired && lock1.isHeldByCurrentThread()) {
+                        try {
+                            lock1.unlock();
+                        } catch (IllegalMonitorStateException e) {
+                            logger.warn("尝试释放非当前线程持有的锁: companyId={}", packetParam.getCompanyId());
+                        }
+                    }
+                }
+
+
+                // 调用第三方接口(锁外操作)
+                R sendRedPacket;
+                try {
+                    sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                } catch (Exception e) {
+                    logger.error("红包发送异常: 异常请求参数{}", packetParam, e);
+                    // 异常时回滚余额
+
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // 红包发送成功处理
+                if (sendRedPacket.get("code").equals(200)) {
+                    FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+                    TransferBillsResult transferBillsResult;
+                    if (sendRedPacket.get("isNew").equals(1)) {
+                        transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                        redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                        redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                        redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                    } else {
+                        redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                        redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                    }
+                    // 添加红包记录
+                    redPacketLog.setCourseId(log.getCourseId());
+                    redPacketLog.setCompanyId(log.getCompanyId());
+                    redPacketLog.setUserId(log.getUserId());
+                    redPacketLog.setVideoId(log.getVideoId());
+                    redPacketLog.setStatus(0);
+                    redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+                    redPacketLog.setCompanyUserId(log.getCompanyUserId());
+                    redPacketLog.setCreateTime(new Date());
+                    redPacketLog.setAmount(amount);
+                    redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+                    redPacketLog.setPeriodId(log.getPeriodId());
+                    redPacketLog.setAppId(packetParam.getAppId());
+
+                    redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+                    // 更新观看记录的奖励类型
+                    log.setRewardType(config.getRewardType());
+                    courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+                    // 异步登记余额扣减日志
+                    BigDecimal money = amount.multiply(BigDecimal.valueOf(-1));
+                    companyService.asyncRecordBalanceLog(log.getCompanyId(), money, 15, newMoney, "发放红包", redPacketLog.getLogId());
+
+                    return sendRedPacket;
+
+
+                } else {
+                    // 发送失败,回滚余额
+                    rollbackBalance(balanceRollbackError);
+                    return R.error("奖励发送失败,请联系客服");
+                }
+
+                // ===================== 本次修改目的为了实时扣减公司余额=====================
+            } else {
+                Company company = companyMapper.selectCompanyById(log.getCompanyId());
+                BigDecimal money = company.getMoney();
+                if (money.compareTo(BigDecimal.ZERO) <= 0) {
+                    return R.error("服务商余额不足,请联系群主服务器充值!");
+                }
+
+                try{
+                    // 发送红包
+                    R sendRedPacket = paymentService.sendAppRedPacket(packetParam);
+                    if (sendRedPacket.get("code").equals(200)) {
+                        FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+                        TransferBillsResult transferBillsResult;
+                        if (sendRedPacket.get("isNew").equals(1)) {
+                            transferBillsResult = (TransferBillsResult) sendRedPacket.get("data");
+                            redPacketLog.setResult(JSON.toJSONString(sendRedPacket));
+                            redPacketLog.setOutBatchNo(transferBillsResult.getOutBillNo());
+                            redPacketLog.setBatchId(transferBillsResult.getTransferBillNo());
+                        } else {
+                            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+                            redPacketLog.setBatchId(sendRedPacket.get("batchId").toString());
+                        }
+                        // 添加红包记录
+                        redPacketLog.setCourseId(log.getCourseId());
+                        redPacketLog.setCompanyId(log.getCompanyId());
+                        redPacketLog.setUserId(log.getUserId());
+                        redPacketLog.setVideoId(log.getVideoId());
+                        redPacketLog.setStatus(0);
+                        redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+                        redPacketLog.setCompanyUserId(log.getCompanyUserId());
+                        redPacketLog.setCreateTime(new Date());
+                        redPacketLog.setAmount(amount);
+                        redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+                        redPacketLog.setPeriodId(log.getPeriodId());
+                        redPacketLog.setAppId( packetParam.getAppId());
+
+                        redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+                        // 更新观看记录的奖励类型
+                        log.setRewardType(config.getRewardType());
+                        courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+                        return sendRedPacket;
+                    } else {
+                        return R.error("奖励发送失败,请联系客服");
+                    }
+                }catch (Exception e){
+                    return R.error(e.getMessage());
+                }
+
+            }
+        } else {
+            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+            // 添加红包记录
+            redPacketLog.setCourseId(log.getCourseId());
+//            redPacketLog.setOutBatchNo(sendRedPacket.get("orderCode").toString());
+            redPacketLog.setCompanyId(log.getCompanyId());
+            redPacketLog.setUserId(log.getUserId());
+            redPacketLog.setVideoId(log.getVideoId());
+            redPacketLog.setStatus(1);
+            redPacketLog.setQwUserId(log.getQwUserId() != null ? log.getQwUserId().toString() : null);
+            redPacketLog.setCompanyUserId(log.getCompanyUserId());
+            redPacketLog.setCreateTime(new Date());
+            redPacketLog.setAmount(BigDecimal.ZERO);
+            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+            redPacketLog.setPeriodId(log.getPeriodId());
+            redPacketLog.setAppId( packetParam.getAppId());
+            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+
+            // 更新观看记录的奖励类
+            log.setRewardType(config.getRewardType());
+            courseWatchLogMapper.updateFsCourseWatchLog(log);
+            return R.ok("答题成功!");
+        }
+    }
+
+    /** 1 可领取红包;0 关闭;null 视为可领取(兼容历史数据) */
+    private boolean isUserRedPacketReceiveEnabled(FsUser user) {
+        if (user == null) {
+            return false;
+        }
+        if (user.getRedStatus() == null) {
+            return true;
+        }
+        return Objects.equals(user.getRedStatus(), 1);
+    }
+
+
 }
 

+ 8 - 0
fs-service/src/main/java/com/fs/course/vo/FsCoursePlaySourceConfigVO.java

@@ -1,10 +1,12 @@
 package com.fs.course.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 @Data
 public class FsCoursePlaySourceConfigVO {
@@ -42,6 +44,12 @@ public class FsCoursePlaySourceConfigVO {
     @ApiModelProperty("所属公司")
     private Long companyId;
 
+    @ApiModelProperty("所属公司(多选)")
+    private List<Long> companyIds;
+
+    @JsonIgnore
+    private String companyIdsText;
+
     /**
      * 销售公司ids 用于判定销售公司可见编辑列表
      */

+ 14 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoListUVO.java

@@ -65,6 +65,20 @@ public class FsUserCourseVideoListUVO extends BaseEntity
 
     private String packageJson;
 
+    /**
+     * 项目ID(来自课程)
+     */
+    private Long project;
+
+    /**
+     * 公司ID(从company_ids解析首个ID)
+     */
+    private Long companyId;
+
+    /**
+     * 课程关联公司IDs原始值
+     */
+    private String companyIds;
 
 
 

+ 62 - 0
fs-service/src/main/java/com/fs/his/config/AppConfig.java

@@ -4,6 +4,7 @@ import com.fs.course.vo.FsUserCourseVideoVO;
 import com.fs.his.domain.FsPackage;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 @Data
@@ -13,4 +14,65 @@ public class AppConfig {
     private Long courseId;
     private List<FsUserCourseVideoVO> fsCourse;
     private Integer unbindLimit;
+
+    //积分提现商户配置
+    private Integer isNew;//0:老商户 商家转账到零钱 1:新商户 商家转账
+
+    /**
+     * 商户号.
+     */
+    private String mchId;
+    /**
+     * 商户密钥.
+     */
+    private String mchKey;
+
+    /**
+     * p12证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String keyPath;
+
+    /**
+     * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String privateKeyPath;
+
+    /**
+     * apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String privateCertPath;
+
+    /**
+     * apiV3 秘钥值.
+     */
+    private String apiV3Key;
+    /**
+     * 公钥ID
+     */
+    private String publicKeyId;
+
+    /**
+     * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
+     */
+    private String publicKeyPath;
+
+    private String notifyUrl;
+
+    private String withdrawalNotifyUrl;
+
+
+    //一次允许提现最大金额(元)
+    private BigDecimal maxApplicationAmount;
+
+    //一天允提现次数
+    private Integer withdrawNum;
+
+    //连续提现几天封控
+    private Integer limitDayNum;
+
+    //连续提现几天封控
+    private BigDecimal limitAmount;
+
+    private Long tongueFlag;
+    private String appId;
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/config/CouponConfig.java

@@ -8,4 +8,9 @@ import java.io.Serializable;
 public class CouponConfig implements Serializable {
     private Long[] registerCoupon;
     private Long userTaskCoupon;
+
+    /** 新手福利:手机号末位奇数路径(弹窗问卷后发放)对应的优惠券模板 ID */
+    private Long newcomerCouponA;
+    /** 新手福利:手机号末位偶数路径(直接发放)对应的优惠券模板 ID */
+    private Long newcomerCouponB;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/config/IntegralConfig.java

@@ -7,6 +7,8 @@ import java.io.Serializable;
 @Data
 public class IntegralConfig implements Serializable {
     private Integer integralNewTask;
+    /** 首次登录 app 发放积分 */
+    private Integer integralFirstLoginApp;
     private Integer integralRatio; //消费购买比例
     private Integer integralShare;//分享获取积分
     private Integer integralFollow;//随访获取积分

+ 24 - 0
fs-service/src/main/java/com/fs/his/domain/FsNewcomerQuestionnaire.java

@@ -0,0 +1,24 @@
+package com.fs.his.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 新人问卷;form_schema 为 JSON:version + fields[{key,type,label 或 title,required,options}]
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsNewcomerQuestionnaire extends BaseEntity {
+
+    private Long id;
+    @Excel(name = "名称")
+    private String name;
+    @Excel(name = "排序")
+    private Integer sortOrder;
+    @Excel(name = "状态")
+    private Integer status;
+    private String formSchema;
+    private String remark;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/his/domain/FsUser.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
 import org.apache.commons.lang3.StringUtils;
 import com.vdurmont.emoji.EmojiParser;
 import lombok.Data;
@@ -54,10 +55,21 @@ public class FsUser extends BaseEntity
     @Excel(name = "用户积分")
     private Long integral;
 
+    /** 首次登录 app 时间(用于判断是否可发放首次登录积分奖励) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date firstLoginAppTime;
+
+    /** 首次登录可领取奖励的地址(由用户在 app 填写) */
+    private String firstLoginRewardAddress;
+
     /** 1为正常,0为禁止 */
     @Excel(name = "1为正常,0为禁止")
     private Integer status;
 
+    /** 红包领取开关:1 可领取(默认),0 关闭后不允许领取红包 */
+    @ApiModelProperty("红包领取:1开启 0关闭")
+    private Integer redStatus;
+
     /** 推广上级用户ID */
     @Excel(name = "推广上级用户ID")
     private String tuiUserId;
@@ -99,7 +111,16 @@ public class FsUser extends BaseEntity
 
     private Long companyId;
     private Long companyUserId;
+    /** 独立绑定销售ID(落表:fs_user.bind_company_user_id) */
+    private Long bindCompanyUserId;
     private String companyUserName;
+    @TableField(exist = false)
+    private String companyAvatar;
+
+    /** 与直播 WebSocket 参数 userType 一致:0 观众,1 具备飘屏或置顶权限的企业用户(不落库) */
+    @TableField(exist = false)
+    @ApiModelProperty(value = "直播间 userType:0 观众 1 管理(与 WS 连接参数一致)")
+    private Integer liveUserType;
 
     /** 公司用户ID,逗号拼接*/
     @TableField(exist = false)
@@ -194,6 +215,18 @@ public class FsUser extends BaseEntity
 
     private String appleKey; // 苹果key登陆验证
 
+    /** 新手福利券类型:A=注册手机号末位奇数(问卷路径),B=末位偶数(直发路径) */
+    private String newcomerCouponType;
+    /** 新手问卷是否已完成:0否 1是 */
+    private Integer newcomerProfileDone;
+    /** 新手福利券是否已发放:0否 1是 */
+    private Integer newcomerWelfareGranted;
+
+    /** 已提交的新人问卷 id */
+    private Long newcomerQuestionnaireId;
+    /** 问卷答案 JSON(与 form_schema 字段 key 对应) */
+    private String newcomerAnswersJson;
+
     public void setNickName(String nickname)
     {
         if(StringUtils.isNotEmpty(nickname)){

+ 2 - 0
fs-service/src/main/java/com/fs/his/enums/BusinessTypeEnum.java

@@ -7,6 +7,8 @@ import lombok.Getter;
 @AllArgsConstructor
 public enum BusinessTypeEnum {
     INTEGRAL_ORDER("integral", 6, "积分商城订单支付"),
+    ORDER_ORDER("store", 8, "商城订单支付"),
+    LIVE_ORDER("live", 9, "直播订单支付"),
     ;
 
     private final String prefix;

+ 1 - 0
fs-service/src/main/java/com/fs/his/enums/FsUserIntegralLogTypeEnum.java

@@ -35,6 +35,7 @@ public enum FsUserIntegralLogTypeEnum {
     TYPE_25(25, "直播完课积分"),
     TYPE_26(26, "直播红包积分"),
     TYPE_27(27, "积分订单取消退回积分"),
+    TYPE_28(28, "首次登录 app 获得积分"),
     ;
 
 

+ 4 - 1
fs-service/src/main/java/com/fs/his/enums/PaymentMethodEnum.java

@@ -8,8 +8,11 @@ import lombok.Getter;
 public enum PaymentMethodEnum {
     MINIAPP_WECHAT("weixin"), // 小程序微信支付
     H5_WECHAT("微信"),      // H5微信支付
+    WX_APP("wx_app"),      // H5微信支付
+    T_NATIVE("t_native"),      // H5微信支付
     ALIPAY("alipay"),         // 支付宝支付
-    H5_ALIPAY("alipay");       // H5支付宝支付
+    H5_ALIPAY("alipay"),       // H5支付宝支付
+    CZ_PAY("cz_pay");       // H5支付宝支付  // H5微信支付
 
     private final String desc;
 }

+ 21 - 0
fs-service/src/main/java/com/fs/his/mapper/FsNewcomerQuestionnaireMapper.java

@@ -0,0 +1,21 @@
+package com.fs.his.mapper;
+
+import com.fs.his.domain.FsNewcomerQuestionnaire;
+import java.util.List;
+
+public interface FsNewcomerQuestionnaireMapper {
+
+    FsNewcomerQuestionnaire selectFsNewcomerQuestionnaireById(Long id);
+
+    List<FsNewcomerQuestionnaire> selectFsNewcomerQuestionnaireList(FsNewcomerQuestionnaire query);
+
+    FsNewcomerQuestionnaire selectFirstEnabledOrderBySort();
+
+    int insertFsNewcomerQuestionnaire(FsNewcomerQuestionnaire row);
+
+    int updateFsNewcomerQuestionnaire(FsNewcomerQuestionnaire row);
+
+    int deleteFsNewcomerQuestionnaireById(Long id);
+
+    int deleteFsNewcomerQuestionnaireByIds(Long[] ids);
+}

+ 7 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserIntegralLogsMapper.java

@@ -144,4 +144,11 @@ public interface FsUserIntegralLogsMapper
      * 查询用户最新的积分记录
      */
     FsUserIntegralLogs selectLatestIntegralLogByUserId(@Param("userId") Long userId);
+
+    /**
+     * 查询用户当天获得的积分总数(只统计正积分)
+     */
+    @Select("SELECT IFNULL(SUM(integral),0) FROM fs_user_integral_logs " +
+            "WHERE user_id = #{userId} AND integral > 0 AND DATE(create_time) = CURDATE()")
+    Long selectTodayIntegralTotal(@Param("userId") Long userId);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/his/mapper/MerchantAppConfigMapper.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.MerchantAppConfig;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 商户应用配置Mapper接口
@@ -74,4 +75,13 @@ public interface MerchantAppConfigMapper extends BaseMapper<MerchantAppConfig>{
      * @return 结果
      */
     int deleteMerchantAppConfigByIds(Long[] ids);
+
+    /**
+     * 根据appId和支付类型查询商户信息
+     *
+     * @param appId
+     * @param payType
+     * @return
+     */
+    MerchantAppConfig selectMerchantAppConfigByAppId(@Param("appId") String appId, @Param("payType") String payType);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralOrderDoPayParam.java

@@ -10,4 +10,10 @@ public class FsIntegralOrderDoPayParam {
     private Long orderId;
 
     private Long userId;
+    private String appId;
+
+    /**
+     * 商品类型
+     */
+    private String type;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/param/FsUserCouponSendParam.java

@@ -21,4 +21,7 @@ public class FsUserCouponSendParam {
 
     //发送销售公司id
     private Long companyId;
+
+    /** 系统活动发券(如新手福利),为 true 时不写 sendUserId,避免依赖后台登录态 */
+    private Boolean systemSend;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java

@@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
 import com.fs.company.param.FsStoreStatisticsParam;
 import com.fs.company.vo.FsStorePaymentStatisticsVO;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.FsStorePayment;
 import com.fs.his.param.FsStorePaymentParam;
 import com.fs.his.param.PayOrderParam;
@@ -119,6 +120,10 @@ public interface IFsStorePaymentService
 
     String v3TransferNotify(String notifyData, HttpServletRequest request);
 
+    /**
+     * App 商家转账 V3 回调(验签使用 his.AppRedPacket,与小程序 redPacket.config 隔离)
+     */
+    String v3TransferNotifyApp(String notifyData, HttpServletRequest request);
 
     String v3TransferNotifyWithCompanyId(Long companyId,String notifyData, HttpServletRequest request);
 
@@ -146,4 +151,8 @@ public interface IFsStorePaymentService
     void synchronizePayStatus();
 
     List<FsStorePayment> selectAllPayment();
+
+    R sendAppRedPacket(WxSendRedPacketParam packetParam);
+
+    R sendRedPacketV3ByApp(WxSendRedPacketParam param, AppConfig config);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserIntegralLogsService.java

@@ -86,4 +86,9 @@ public interface IFsUserIntegralLogsService
 
     //app获取新人福利完成情况
     R getNewcomerBenefits(Long userId);
+
+    /**
+     * 查询用户当天获得的积分总数(只统计正积分)
+     */
+    Long getTodayIntegralTotal(Long userId);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserNewTaskService.java

@@ -66,4 +66,14 @@ public interface IFsUserNewTaskService
     void performTaskTwo(Long userId);
 
     void performTaskThree(Long userId,Long integral,Long orderId);
+
+    /**
+     * 判断用户是否首次登录 app:
+     * - 若 `fs_user.first_login_app_time` 为空,则写入时间并按配置发放积分
+     * - 若不为空,则不发放
+     *
+     * @param userId 用户ID
+     * @return 1 表示刚完成首次登录发放,0 表示已处理或不满足
+     */
+    int performFirstLoginApp(Long userId);
 }

+ 61 - 0
fs-service/src/main/java/com/fs/his/service/NewcomerQuestionnaireService.java

@@ -0,0 +1,61 @@
+package com.fs.his.service;
+
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsNewcomerQuestionnaire;
+import com.fs.his.mapper.FsNewcomerQuestionnaireMapper;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 新人问卷:后台 CRUD + App 解析当前使用的问卷
+ */
+@Service
+public class NewcomerQuestionnaireService {
+
+    public static final String CONFIG_KEY_QUESTIONNAIRE_ID = "newcomer.welfare.questionnaire_id";
+
+    @Autowired
+    private FsNewcomerQuestionnaireMapper mapper;
+    @Autowired
+    private ISysConfigService configService;
+
+    public FsNewcomerQuestionnaire selectById(Long id) {
+        return mapper.selectFsNewcomerQuestionnaireById(id);
+    }
+
+    public List<FsNewcomerQuestionnaire> selectList(FsNewcomerQuestionnaire query) {
+        return mapper.selectFsNewcomerQuestionnaireList(query);
+    }
+
+    public int insert(FsNewcomerQuestionnaire row) {
+        return mapper.insertFsNewcomerQuestionnaire(row);
+    }
+
+    public int update(FsNewcomerQuestionnaire row) {
+        return mapper.updateFsNewcomerQuestionnaire(row);
+    }
+
+    public int deleteByIds(Long[] ids) {
+        return mapper.deleteFsNewcomerQuestionnaireByIds(ids);
+    }
+
+    /** 优先系统参数 newcomer.welfare.questionnaire_id,否则第一条 status=1 按排序 */
+    public FsNewcomerQuestionnaire resolveForApp() {
+        String idStr = configService.selectConfigByKey(CONFIG_KEY_QUESTIONNAIRE_ID);
+        if (StringUtils.isNotBlank(idStr)) {
+            try {
+                Long id = Long.parseLong(idStr.trim());
+                FsNewcomerQuestionnaire q = mapper.selectFsNewcomerQuestionnaireById(id);
+                if (q != null && q.getStatus() != null && q.getStatus() == 1) {
+                    return q;
+                }
+            } catch (NumberFormatException ignored) {
+                // ignore
+            }
+        }
+        return mapper.selectFirstEnabledOrderBySort();
+    }
+}

+ 414 - 0
fs-service/src/main/java/com/fs/his/service/NewcomerWelfareService.java

@@ -0,0 +1,414 @@
+package com.fs.his.service;
+
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.config.CouponConfig;
+import com.fs.his.domain.FsNewcomerQuestionnaire;
+import com.fs.his.domain.FsUser;
+import com.fs.his.param.FsUserCouponSendParam;
+import com.fs.his.utils.PhoneUtil;
+import com.fs.his.vo.NewcomerWelfareStateVO;
+import com.fs.system.service.ISysConfigService;
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 新手福利:手机号奇偶分流、动态问卷、发券
+ */
+@Service
+public class NewcomerWelfareService {
+
+    @Autowired
+    private IFsUserService fsUserService;
+    @Autowired
+    private IFsUserCouponService fsUserCouponService;
+    @Autowired
+    private NewcomerQuestionnaireService questionnaireService;
+    @Autowired
+    private ISysConfigService configService;
+
+    @Data
+    public static class SubmitBody implements Serializable {
+        @NotNull
+        private Long questionnaireId;
+        @NotNull
+        private Map<String, Object> answers;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public NewcomerWelfareStateVO getState(Long userId) {
+        FsUser user = fsUserService.selectFsUserByUserId(userId);
+        if (user == null) {
+            throw new CustomException("用户不存在");
+        }
+        assignCouponTypeIfNeeded(user);
+
+        NewcomerWelfareStateVO vo = new NewcomerWelfareStateVO();
+        String plain = resolvePlainPhone(user);
+        vo.setHasPhone(StringUtils.isNotBlank(plain));
+        vo.setNewcomerCouponType(user.getNewcomerCouponType());
+
+        boolean eligible = isEligibleByAccountAge(user);
+        boolean granted = isGranted(user);
+        vo.setWelfareGranted(granted);
+
+        FsNewcomerQuestionnaire q = questionnaireService.resolveForApp();
+        fillQuestionnaire(vo, q);
+
+        boolean pathA = "A".equals(user.getNewcomerCouponType());
+        boolean hasForm = q != null && StringUtils.isNotBlank(q.getFormSchema());
+        vo.setShowQuestionnaire(vo.isHasPhone() && eligible && pathA && !granted && !isProfileDone(user) && hasForm);
+
+        vo.setJustGranted(false);
+        if (vo.isHasPhone() && eligible && "B".equals(user.getNewcomerCouponType()) && !granted) {
+            tryGrantAndMark(user, "B");
+            vo.setJustGranted(true);
+            user = fsUserService.selectFsUserByUserId(userId);
+            vo.setWelfareGranted(isGranted(user));
+        }
+        return vo;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void submitQuestionnaire(Long userId, SubmitBody body) {
+        FsUser user = fsUserService.selectFsUserByUserId(userId);
+        if (user == null) {
+            throw new CustomException("用户不存在");
+        }
+        assignCouponTypeIfNeeded(user);
+        if (!"A".equals(user.getNewcomerCouponType())) {
+            throw new CustomException("当前账号无需提交问卷");
+        }
+        if (isGranted(user)) {
+            throw new CustomException("新手福利已领取");
+        }
+        if (isProfileDone(user)) {
+            throw new CustomException("问卷已完成");
+        }
+        if (!isEligibleByAccountAge(user)) {
+            throw new CustomException("当前账号不参与新手福利活动");
+        }
+
+        FsNewcomerQuestionnaire current = questionnaireService.resolveForApp();
+        if (current == null || current.getId() == null) {
+            throw new CustomException("问卷未配置");
+        }
+        if (!current.getId().equals(body.getQuestionnaireId())) {
+            throw new CustomException("问卷已更新,请返回刷新后重新填写");
+        }
+        if (StringUtils.isBlank(current.getFormSchema())) {
+            throw new CustomException("问卷表单为空");
+        }
+
+        validateAnswers(current.getFormSchema(), body.getAnswers());
+
+        FsUser patch = new FsUser();
+        patch.setUserId(userId);
+        patch.setNewcomerQuestionnaireId(current.getId());
+        patch.setNewcomerAnswersJson(JSONUtil.toJsonStr(body.getAnswers()));
+        patch.setNewcomerProfileDone(1);
+        syncSexFromAnswers(patch, body.getAnswers());
+        fsUserService.updateFsUser(patch);
+
+        user.setNewcomerProfileDone(1);
+        tryGrantAndMark(user, "A");
+    }
+
+    private void fillQuestionnaire(NewcomerWelfareStateVO vo, FsNewcomerQuestionnaire q) {
+        if (q == null) {
+            vo.setQuestionnaireId(null);
+            vo.setFormSchema(null);
+            vo.setSchemaVersion(1);
+            return;
+        }
+        vo.setQuestionnaireId(q.getId());
+        vo.setFormSchema(q.getFormSchema());
+        vo.setSchemaVersion(parseSchemaVersion(q.getFormSchema()));
+    }
+
+    private static int parseSchemaVersion(String formSchema) {
+        if (StringUtils.isBlank(formSchema)) {
+            return 1;
+        }
+        try {
+            Integer v = JSONUtil.parseObj(formSchema).getInt("version");
+            return v != null && v > 0 ? v : 1;
+        } catch (Exception e) {
+            return 1;
+        }
+    }
+
+    private void assignCouponTypeIfNeeded(FsUser user) {
+        if (StringUtils.isNotBlank(user.getNewcomerCouponType())) {
+            return;
+        }
+        String plain = resolvePlainPhone(user);
+        if (StringUtils.isBlank(plain)) {
+            return;
+        }
+        char last = plain.charAt(plain.length() - 1);
+        int d = (last >= '0' && last <= '9') ? (last - '0') : 0;
+        String type = (last >= '0' && last <= '9' && d % 2 == 1) ? "A" : "B";
+
+        FsUser patch = new FsUser();
+        patch.setUserId(user.getUserId());
+        patch.setNewcomerCouponType(type);
+        fsUserService.updateFsUser(patch);
+        user.setNewcomerCouponType(type);
+    }
+
+    private String resolvePlainPhone(FsUser user) {
+        if (user == null || StringUtils.isBlank(user.getPhone())) {
+            return null;
+        }
+        String p = user.getPhone().trim();
+        if (p.matches("^1\\d{10}$")) {
+            return p;
+        }
+        String d = PhoneUtil.decryptPhone(p);
+        if (StringUtils.isNotBlank(d) && d.matches("^1\\d{10}$")) {
+            return d;
+        }
+        d = PhoneUtil.decryptPhoneOldKey(p);
+        if (StringUtils.isNotBlank(d) && d.matches("^1\\d{10}$")) {
+            return d;
+        }
+        return null;
+    }
+
+    private boolean isEligibleByAccountAge(FsUser user) {
+        String daysStr = configService.selectConfigByKey("newcomer.welfare.max_account_age_days");
+        if (StringUtils.isBlank(daysStr)) {
+            return true;
+        }
+        int days;
+        try {
+            days = Integer.parseInt(daysStr.trim());
+        } catch (NumberFormatException e) {
+            return true;
+        }
+        if (days <= 0 || user.getCreateTime() == null) {
+            return true;
+        }
+        long cutoff = System.currentTimeMillis() - (long) days * 86_400_000L;
+        return user.getCreateTime().getTime() >= cutoff;
+    }
+
+    private static boolean isGranted(FsUser user) {
+        return user.getNewcomerWelfareGranted() != null && user.getNewcomerWelfareGranted() == 1;
+    }
+
+    private static boolean isProfileDone(FsUser user) {
+        return user.getNewcomerProfileDone() != null && user.getNewcomerProfileDone() == 1;
+    }
+
+    private void tryGrantAndMark(FsUser user, String path) {
+        Long couponId = resolveCouponId(path);
+        if (couponId == null) {
+            throw new CustomException("未配置新手福利券,请在参数 his.coupon 中配置 newcomerCoupon" + path);
+        }
+        FsUserCouponSendParam sendParam = new FsUserCouponSendParam();
+        sendParam.setUserId(user.getUserId());
+        sendParam.setCouponId(couponId);
+        sendParam.setSystemSend(true);
+        fsUserCouponService.sendFsUserCoupon(sendParam);
+
+        FsUser patch = new FsUser();
+        patch.setUserId(user.getUserId());
+        patch.setNewcomerWelfareGranted(1);
+        fsUserService.updateFsUser(patch);
+        user.setNewcomerWelfareGranted(1);
+    }
+
+    private Long resolveCouponId(String path) {
+        String json = configService.selectConfigByKey("his.coupon");
+        if (StringUtils.isBlank(json)) {
+            return null;
+        }
+        CouponConfig cfg = JSONUtil.toBean(json, CouponConfig.class);
+        if (cfg == null) {
+            return null;
+        }
+        return "A".equals(path) ? cfg.getNewcomerCouponA() : cfg.getNewcomerCouponB();
+    }
+
+    private void syncSexFromAnswers(FsUser patch, Map<String, Object> answers) {
+        if (answers == null) {
+            return;
+        }
+        Object s = answers.get("sex");
+        if (s == null) {
+            return;
+        }
+        try {
+            int v = Integer.parseInt(String.valueOf(s).trim());
+            if (v == 1 || v == 2) {
+                patch.setSex(v);
+            }
+        } catch (NumberFormatException ignored) {
+            // ignore
+        }
+    }
+
+    // ---------- 答案与 form_schema 校验(原独立类内聚至此) ----------
+    /** 展示名:优先 label,兼容仅设计器使用的 title */
+    private static String fieldDisplay(JSONObject f, String keyFallback) {
+        if (f == null) {
+            return keyFallback;
+        }
+        if (StringUtils.isNotBlank(f.getStr("label"))) {
+            return f.getStr("label");
+        }
+        if (StringUtils.isNotBlank(f.getStr("title"))) {
+            return f.getStr("title");
+        }
+        return keyFallback;
+    }
+
+    private void validateAnswers(String formSchemaJson, Map<String, Object> answers) {
+        if (StringUtils.isBlank(formSchemaJson)) {
+            throw new CustomException("问卷未配置表单");
+        }
+        JSONArray fields = JSONUtil.parseObj(formSchemaJson).getJSONArray("fields");
+        if (fields == null || fields.isEmpty()) {
+            return;
+        }
+        for (int i = 0; i < fields.size(); i++) {
+            JSONObject f = fields.getJSONObject(i);
+            if (f == null) {
+                continue;
+            }
+            String key = f.getStr("key");
+            if (StringUtils.isBlank(key)) {
+                throw new CustomException("问卷配置异常:题目缺少 key");
+            }
+            String type = StringUtils.defaultString(f.getStr("type"), "text").toLowerCase();
+            boolean required = f.getBool("required", false);
+            Object raw = answers == null ? null : answers.get(key);
+
+            if (required && isAnswerEmpty(raw, type)) {
+                throw new CustomException("请填写:" + fieldDisplay(f, key));
+            }
+            if (isAnswerEmpty(raw, type)) {
+                continue;
+            }
+            switch (type) {
+                case "radio":
+                    assertRadio(f, raw);
+                    break;
+                case "checkbox":
+                    assertCheckbox(f, raw);
+                    break;
+                case "number":
+                    assertNumber(raw, fieldDisplay(f, key));
+                    break;
+                case "text":
+                default:
+                    if (!(raw instanceof String) && raw != null) {
+                        throw new CustomException("题目格式不正确:" + fieldDisplay(f, key));
+                    }
+                    break;
+            }
+        }
+    }
+
+    private static boolean isAnswerEmpty(Object raw, String type) {
+        if (raw == null) {
+            return true;
+        }
+        if ("checkbox".equals(type)) {
+            List<?> list = normalizeList(raw);
+            return list == null || list.isEmpty();
+        }
+        if (raw instanceof String) {
+            return StringUtils.isBlank((String) raw);
+        }
+        return false;
+    }
+
+    private static void assertRadio(JSONObject field, Object raw) {
+        String v = raw == null ? null : String.valueOf(raw).trim();
+        if (!optionValues(field).contains(v)) {
+            throw new CustomException("选项不合法:" + fieldDisplay(field, field.getStr("key")));
+        }
+    }
+
+    private static void assertCheckbox(JSONObject field, Object raw) {
+        List<?> list = normalizeList(raw);
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        Set<String> allowed = optionValues(field);
+        for (Object o : list) {
+            String v = o == null ? "" : String.valueOf(o).trim();
+            if (!allowed.contains(v)) {
+                throw new CustomException("多选项不合法:" + fieldDisplay(field, field.getStr("key")));
+            }
+        }
+    }
+
+    private static void assertNumber(Object raw, String label) {
+        if (raw instanceof Number) {
+            return;
+        }
+        if (raw instanceof String) {
+            try {
+                Double.parseDouble(((String) raw).trim());
+                return;
+            } catch (NumberFormatException ignored) {
+                // fall through
+            }
+        }
+        throw new CustomException("请输入有效数字:" + label);
+    }
+
+    private static Set<String> optionValues(JSONObject field) {
+        Set<String> set = new HashSet<>();
+        JSONArray opts = field.getJSONArray("options");
+        if (opts == null) {
+            return set;
+        }
+        for (int j = 0; j < opts.size(); j++) {
+            JSONObject o = opts.getJSONObject(j);
+            if (o != null && o.get("value") != null) {
+                set.add(String.valueOf(o.get("value")).trim());
+            }
+        }
+        return set;
+    }
+
+    private static List<?> normalizeList(Object raw) {
+        if (raw == null) {
+            return null;
+        }
+        if (raw instanceof List) {
+            return (List<?>) raw;
+        }
+        if (raw instanceof Collection) {
+            return new ArrayList<>((Collection<?>) raw);
+        }
+        if (raw instanceof JSONArray) {
+            JSONArray ja = (JSONArray) raw;
+            List<Object> list = new ArrayList<>(ja.size());
+            for (int i = 0; i < ja.size(); i++) {
+                list.add(ja.get(i));
+            }
+            return list;
+        }
+        return null;
+    }
+}

+ 307 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -49,6 +49,7 @@ import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.service.IFsCourseRedPacketLogService;
 import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.IFsUserVipOrderService;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.*;
 import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.FsExportTaskMapper;
@@ -1168,6 +1169,50 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         }
     }
 
+    @Override
+    public String v3TransferNotifyApp(String notifyData, HttpServletRequest request) {
+        logger.info("【App转账回调V3】收到回调,body={}", notifyData);
+        try {
+            String json = configService.selectConfigByKey("his.AppRedPacket");
+            if (StringUtils.isEmpty(json)) {
+                logger.error("【App转账回调V3】his.AppRedPacket 配置为空");
+                return WxPayNotifyResponse.fail("config empty");
+            }
+            AppConfig appConfig = JSONUtil.toBean(json, AppConfig.class);
+            logger.info("【App转账回调V3】验签配置 mchId={}, notifyUrl={}, requestUri={}",
+                    appConfig.getMchId(), appConfig.getNotifyUrl(), request.getRequestURI());
+            WxPayConfig payConfig = new WxPayConfig();
+            BeanUtils.copyProperties(appConfig, payConfig);
+            WxPayService wxPayService = new WxPayServiceImpl();
+            wxPayService.setConfig(payConfig);
+            SignatureHeader signatureHeader = new SignatureHeader();
+            signatureHeader.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
+            signatureHeader.setNonce(request.getHeader("Wechatpay-Nonce"));
+            signatureHeader.setSerial(request.getHeader("Wechatpay-Serial"));
+            signatureHeader.setSignature(request.getHeader("Wechatpay-Signature"));
+            TransferBillsNotifyResult result = wxPayService.parseTransferBillsNotifyV3Result(notifyData, signatureHeader);
+            logger.info("【App转账回调V3】解析成功,state={}, outBillNo={}, transferBillNo={}",
+                    result.getResult().getState(),
+                    result.getResult().getOutBillNo(),
+                    result.getResult().getTransferBillNo());
+            if (result.getResult().getState().equals("SUCCESS")) {
+                R r = redPacketLogService.syncRedPacket(result.getResult().getOutBillNo(), result.getResult().getTransferBillNo());
+                logger.info("【App转账回调V3】同步红包状态结果,code={}, msg={}", r.get("code"), r.get("msg"));
+                if (r.get("code").equals(200)) {
+                    return WxPayNotifyResponse.success("处理成功");
+                }
+                logger.warn("【App转账回调V3】同步失败,outBillNo={}", result.getResult().getOutBillNo());
+                return WxPayNotifyResponse.fail("");
+            }
+            logger.warn("【App转账回调V3】状态非SUCCESS,state={}, outBillNo={}",
+                    result.getResult().getState(), result.getResult().getOutBillNo());
+            return WxPayNotifyResponse.fail("");
+        } catch (WxPayException e) {
+            logger.error("【App转账回调V3】处理异常,returnMsg={}, message={}", e.getReturnMsg(), e.getMessage(), e);
+            return WxPayNotifyResponse.fail(e.getMessage());
+        }
+    }
+
     @Override
     public String v3TransferNotifyWithCompanyId(Long companyId, String notifyData, HttpServletRequest request) {
         logger.info("分公司回调V3::companyId:{}",companyId);
@@ -1999,4 +2044,266 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
 
     }
 
+
+    @Override
+    @Transactional
+    public R sendAppRedPacket(WxSendRedPacketParam param) {
+        //组合返回参数
+        R result = new R();
+        String json = configService.selectConfigByKey("his.AppRedPacket");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        if (config.getIsNew() != null && config.getIsNew() == 1) {
+            result = sendRedPacketV3ByApp(param, config);
+        } else {
+            result= sendRedPacketLegacy(param, config);
+        }
+
+        result.put("mchId", config.getMchId());
+        result.put("isNew",config.getIsNew());
+        logger.info("App提现返回:{}",result);
+        return result;
+    }
+
+    private R sendRedPacketLegacy(WxSendRedPacketParam param, AppConfig config) {
+        //如果服务号的配置存在,小程序红包接口可以使用服务号来发红包,重新赋值
+        //仅老商户支持
+        if (param.getOpenId()!=null && StringUtils.isNotEmpty(param.getAppId())){
+//            config.setAppId(param.getAppId());
+            param.setOpenId(param.getOpenId());
+        }
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getNotifyUrl());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBatchesRequest request = new TransferBatchesRequest();
+//        request.setAppid(config.getAppId());
+
+
+        // todo 如果未配置负载均衡请还原原本的单号方式
+//        String code = IdUtil.getSnowflake(0, 0).nextIdStr();
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("红包单号生成失败,请重试");
+        }
+        request.setOutBatchNo("fsIntegral" + code);
+        request.setBatchRemark("积分提现");
+        request.setBatchName("积分提现");
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTotalAmount(amount);
+        request.setTotalNum(1);
+        request.setNotifyUrl(config.getNotifyUrl());
+
+        ArrayList<TransferBatchesRequest.TransferDetail> transferDetailList = new ArrayList<>();
+        TransferBatchesRequest.TransferDetail transferDetail = new TransferBatchesRequest.TransferDetail();
+        transferDetail.setOpenid(param.getOpenId());
+        String code1 = IdUtil.getSnowflake(0, 0).nextIdStr();
+        transferDetail.setOutDetailNo("fsCourse" + code1);
+        transferDetail.setTransferAmount(amount);
+        transferDetail.setTransferRemark("积分提现成功!");
+        transferDetailList.add(transferDetail);
+        request.setTransferDetailList(transferDetailList);
+
+        try {
+            TransferBatchesResult transferBatchesResult = transferService.transferBatches(request);
+            return R.ok("积分提现成功").put("orderCode", transferBatchesResult.getOutBatchNo()).put("batchId", transferBatchesResult.getBatchId()).put("mchId", config.getMchId());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            if (e instanceof WxPayException) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+                WxPayException wxPayException = (WxPayException) e;
+                String customErrorMsg = wxPayException.getCustomErrorMsg();
+                if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    return R.error("[积分提现] 账户余额不足,请联系管理员!");
+                }
+            }
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 内部方法:处理新版本的发红包逻辑
+    private R sendRedPacketV3Integral(WxSendRedPacketParam param, AppConfig config) {
+
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+        payConfig.setAppId(param.getAppId());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBillsRequest request = new TransferBillsRequest();
+        request.setAppid(param.getAppId());
+        request.setOpenid(param.getOpenId());
+
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("订单生成失败,请重试");
+        }
+//        String code = String.valueOf(IdUtil.getSnowflake(0, 0).nextId());
+        request.setOutBillNo("fsIntegral" + code);
+        if (param.getAmount() == null) {
+            return R.error();
+        }
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTransferAmount(amount);
+        request.setTransferRemark("积分提现");
+        request.setUserRecvPerception("活动奖励");
+        request.setNotifyUrl(config.getNotifyUrl());
+        request.setTransferSceneId("1000");
+
+        // 设置场景信息
+        List<TransferBillsRequest.TransferSceneReportInfo> transferSceneReportInfos = new ArrayList<>();
+        TransferBillsRequest.TransferSceneReportInfo info1 = new TransferBillsRequest.TransferSceneReportInfo();
+        info1.setInfoType("活动名称");
+        info1.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info1);
+
+        TransferBillsRequest.TransferSceneReportInfo info2 = new TransferBillsRequest.TransferSceneReportInfo();
+        info2.setInfoType("奖励说明");
+        info2.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info2);
+        request.setTransferSceneReportInfos(transferSceneReportInfos);
+
+
+        try {
+            TransferBillsResult transferBillsResult = transferService.transferBills(request);
+            logger.info("商家转账支付完成:[msg:{}]", transferBillsResult);
+            return R.ok("发送红包成功").put("data", transferBillsResult).put("mchId", config.getMchId())
+                    .put("package",transferBillsResult.getPackageInfo())
+                    .put("appId",param.getAppId())
+                    .put("orderCode",request.getOutBillNo());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    private R sendRedPacketLegacyIntegral(WxSendRedPacketParam param, AppConfig config) {
+        //如果服务号的配置存在,小程序红包接口可以使用服务号来发红包,重新赋值
+        //仅老商户支持
+        if (param.getOpenId()!=null && StringUtils.isNotEmpty(param.getAppId())){
+//            config.setAppId(param.getAppId());
+            param.setOpenId(param.getOpenId());
+        }
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        payConfig.setNotifyUrl(config.getWithdrawalNotifyUrl());
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBatchesRequest request = new TransferBatchesRequest();
+//        request.setAppid(config.getAppId());
+
+
+        // todo 如果未配置负载均衡请还原原本的单号方式
+//        String code = IdUtil.getSnowflake(0, 0).nextIdStr();
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("红包单号生成失败,请重试");
+        }
+        request.setOutBatchNo("fsIntegral" + code);
+        request.setBatchRemark("积分提现");
+        request.setBatchName("积分提现");
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTotalAmount(amount);
+        request.setTotalNum(1);
+        request.setNotifyUrl(config.getNotifyUrl());
+
+        ArrayList<TransferBatchesRequest.TransferDetail> transferDetailList = new ArrayList<>();
+        TransferBatchesRequest.TransferDetail transferDetail = new TransferBatchesRequest.TransferDetail();
+        transferDetail.setOpenid(param.getOpenId());
+        String code1 = IdUtil.getSnowflake(0, 0).nextIdStr();
+        transferDetail.setOutDetailNo("fsCourse" + code1);
+        transferDetail.setTransferAmount(amount);
+        transferDetail.setTransferRemark("积分提现成功!");
+        transferDetailList.add(transferDetail);
+        request.setTransferDetailList(transferDetailList);
+
+        try {
+            TransferBatchesResult transferBatchesResult = transferService.transferBatches(request);
+            return R.ok("积分提现成功").put("orderCode", transferBatchesResult.getOutBatchNo()).put("batchId", transferBatchesResult.getBatchId()).put("mchId", config.getMchId());
+        } catch (Exception e) {
+            logger.error("商家转账支付失败:参数: {} :原因: {}", com.alibaba.fastjson.JSON.toJSONString(param), e.getMessage(),e);
+            if (e instanceof WxPayException) {
+//            if (e instanceof WxPayException && "济南联志健康".equals(signProjectName)) {
+                WxPayException wxPayException = (WxPayException) e;
+                String customErrorMsg = wxPayException.getCustomErrorMsg();
+                if (null != customErrorMsg && customErrorMsg.startsWith("商户运营账户资金不足")) {
+                    return R.error("[积分提现] 账户余额不足,请联系管理员!");
+                }
+            }
+            throw new RuntimeException(e);
+        }
+    }
+
+
+    /**
+     *
+     * @param param
+     * @param config
+     * @param
+     * @return
+     */
+    @Override
+    public R sendRedPacketV3ByApp(WxSendRedPacketParam param,AppConfig config) {
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtils.copyProperties(config, payConfig);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        payConfig.setNotifyUrl(config.getNotifyUrl());
+        wxPayService.setConfig(payConfig);
+        TransferService transferService = wxPayService.getTransferService();
+
+        TransferBillsRequest request = new TransferBillsRequest();
+        request.setAppid(param.getAppId());
+        request.setOpenid(param.getOpenId());
+
+        String code =  OrderCodeUtils.getOrderSn();
+        if(StringUtils.isEmpty(code)){
+            return R.error("订单生成失败,请重试");
+        }
+//        String code = String.valueOf(IdUtil.getSnowflake(0, 0).nextId());
+        request.setOutBillNo("fsAppRed" + code);
+        if (param.getAmount() == null) {
+            return R.error();
+        }
+        Integer amount = WxPayUnifiedOrderRequest.yuanToFen(param.getAmount().toString());
+        request.setTransferAmount(amount);
+        request.setTransferRemark("积分提现");
+        request.setUserRecvPerception("活动奖励");
+        request.setNotifyUrl(config.getNotifyUrl());
+        request.setTransferSceneId("1000");
+
+        // 设置场景信息
+        List<TransferBillsRequest.TransferSceneReportInfo> transferSceneReportInfos = new ArrayList<>();
+        TransferBillsRequest.TransferSceneReportInfo info1 = new TransferBillsRequest.TransferSceneReportInfo();
+        info1.setInfoType("活动名称");
+        info1.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info1);
+
+        TransferBillsRequest.TransferSceneReportInfo info2 = new TransferBillsRequest.TransferSceneReportInfo();
+        info2.setInfoType("奖励说明");
+        info2.setInfoContent("积分提现");
+        transferSceneReportInfos.add(info2);
+        request.setTransferSceneReportInfos(transferSceneReportInfos);
+
+
+        try {
+            logger.info("app商家转账开始:[param:{}]", request);
+            TransferBillsResult transferBillsResult = transferService.transferBills(request);
+            logger.info("Method...商家转账支付完成:[msg:{}]", transferBillsResult);
+            return R.ok("发送红包成功").put("data", transferBillsResult).put("mchId", config.getMchId())
+                    .put("package",transferBillsResult.getPackageInfo())
+                    .put("appId",param.getAppId())
+                    .put("orderCode",request.getOutBillNo());
+        } catch (Exception e) {
+            logger.error("app商家转账支付失败:参数: {} :原因: {}", request, e.getMessage(),e);
+            throw new RuntimeException(e);
+        }
+    }
+
 }

+ 5 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsUserCouponServiceImpl.java

@@ -163,7 +163,11 @@ public class FsUserCouponServiceImpl implements IFsUserCouponService
         fsUserCoupon.setCouponCode("C"+System.currentTimeMillis());
         fsUserCoupon.setUserId(param.getUserId());
         fsUserCoupon.setCreateTime(DateUtils.getNowDate());
-        if (param.getCompanyUserId() == null && param.getCompanyId() == null){
+        if (Boolean.TRUE.equals(param.getSystemSend())) {
+            fsUserCoupon.setSendUserId(null);
+            fsUserCoupon.setCompanyId(null);
+            fsUserCoupon.setCompanyUserId(null);
+        } else if (param.getCompanyUserId() == null && param.getCompanyId() == null){
             fsUserCoupon.setSendUserId(SecurityUtils.getUserId());
             fsUserCoupon.setCompanyId(null);
             fsUserCoupon.setCompanyUserId(null);

+ 17 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserIntegralLogsServiceImpl.java

@@ -486,6 +486,7 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         int isNewUser = 1;
         int isFinishConsultation = 0;
         int isFinishFirstOrderPoint = 0;
+        boolean isFinishDownloadAppIntegral = false;
 //        long expireDays = 0;
         Date createTime = null;
 //        FsUser fsUser = fsUserMapper.selectFsUserIsNew(userId);
@@ -533,6 +534,12 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
             isFinishFirstOrderPoint = 1;
         }
 
+        // 完成首次登录 app 获得积分(是否已领取下载/登录 APP 积分)
+        integralLogs = fsUserIntegralLogsMapper.selectFsUserIntegralLogsByUserIdAndLogType(userId, FsUserIntegralLogTypeEnum.TYPE_28.getValue(), null);
+        if (!integralLogs.isEmpty()) {
+            isFinishDownloadAppIntegral = true;
+        }
+
         Map<String,Object> map = new HashMap<>();
         map.put("taskOne",taskOne);
         map.put("taskTwo",taskTwo);
@@ -541,6 +548,16 @@ public class FsUserIntegralLogsServiceImpl implements IFsUserIntegralLogsService
         map.put("taskFive",taskFive);
         map.put("isFinishConsultation", isFinishConsultation);
         map.put("isFinishFirstOrderPoint", isFinishFirstOrderPoint);
+        map.put("isFinishDownloadAppIntegral", isFinishDownloadAppIntegral);
         return R.ok().put("data",map).put("isNewUser",isNewUser).put("createTime",createTime);
     }
+
+    @Override
+    public Long getTodayIntegralTotal(Long userId) {
+        if (userId == null) {
+            return 0L;
+        }
+        Long total = fsUserIntegralLogsMapper.selectTodayIntegralTotal(userId);
+        return total != null ? total : 0L;
+    }
 }

+ 55 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserNewTaskServiceImpl.java

@@ -217,4 +217,59 @@ public class FsUserNewTaskServiceImpl implements IFsUserNewTaskService
             }
         }
     }
+
+    /**
+     * app 首次登录积分奖励发放
+     * 通过 fs_user.first_login_app_time 判断是否首次;并按配置 his.integral.integralFirstLoginApp 发放积分。
+     */
+    @Override
+    @Transactional
+    public int performFirstLoginApp(Long userId) {
+        FsUser fsUser = fsUserMapper.selectFsUserByIdForUpdate(userId);
+        if (fsUser == null) {
+            return 0;
+        }
+        if (fsUser.getFirstLoginAppTime() != null) {
+            return 0;
+        }
+
+        Date now = new Date();
+        Integer awardPoints = null;
+        try {
+            String json = configService.selectConfigByKey("his.integral");
+            if (json != null) {
+                IntegralConfig config = JSONUtil.toBean(json, IntegralConfig.class);
+                awardPoints = config.getIntegralFirstLoginApp();
+            }
+        } catch (Exception ignore) {
+            // 配置解析失败时不阻断登录,只写入首次登录时间
+        }
+
+        long currentIntegral = fsUser.getIntegral() != null ? fsUser.getIntegral() : 0L;
+        long newBalance = currentIntegral;
+        if (awardPoints != null && awardPoints > 0) {
+            newBalance = currentIntegral + awardPoints.longValue();
+        }
+
+        FsUser update = new FsUser();
+        update.setUserId(userId);
+        update.setFirstLoginAppTime(now);
+        if (awardPoints != null && awardPoints > 0) {
+            update.setIntegral(newBalance);
+        }
+        fsUserMapper.updateFsUser(update);
+
+        if (awardPoints != null && awardPoints > 0) {
+            FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+            integralLogs.setUserId(userId);
+            integralLogs.setIntegral(awardPoints.longValue());
+            integralLogs.setBalance(newBalance);
+            integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_28.getValue());
+            integralLogs.setBusinessId(userId.toString());
+            integralLogs.setCreateTime(new Date());
+            fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+        }
+
+        return 1;
+    }
 }

+ 20 - 0
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -97,4 +97,24 @@ public class PhoneUtil {
         }
         return encryptedText;
     }
+
+    /**
+     * 与 {@link #encryptPhoneOldKey(String)} 对应的解密
+     */
+    public static String decryptPhoneOldKey(String encryptedText) {
+        String text = null;
+        if (encryptedText == null || encryptedText.isEmpty()) {
+            return null;
+        }
+        try {
+            SecretKeySpec secretKey = new SecretKeySpec(OLD_KEY.getBytes(), "AES");
+            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            cipher.init(Cipher.DECRYPT_MODE, secretKey);
+            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
+            text = new String(decryptedBytes);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return text;
+    }
 }

+ 37 - 0
fs-service/src/main/java/com/fs/his/vo/NewcomerWelfareStateVO.java

@@ -0,0 +1,37 @@
+package com.fs.his.vo;
+
+import lombok.Data;
+
+/**
+ * App 新手福利弹窗状态
+ */
+@Data
+public class NewcomerWelfareStateVO {
+
+    /** 是否已绑定可解析的 11 位手机号 */
+    private boolean hasPhone;
+
+    /** A / B,无手机号时可能为 null */
+    private String newcomerCouponType;
+
+    /** 是否展示问卷(路径 A 且未完成、未发券,且已配置有效问卷) */
+    private boolean showQuestionnaire;
+
+    /** 是否已发放新手福利券 */
+    private boolean welfareGranted;
+
+    /** 本次请求是否为路径 B 且刚完成自动发券 */
+    private boolean justGranted;
+
+    /** 当前 App 使用的问卷 id(无则 null) */
+    private Long questionnaireId;
+
+    /** schema 版本号,来自 form_schema JSON 根节点 version,默认 1 */
+    private Integer schemaVersion;
+
+    /**
+     * 动态表单 JSON 字符串,App 解析后渲染;
+     * 与总后台「新人问卷调查」中保存的 form_schema 一致
+     */
+    private String formSchema;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -369,5 +369,8 @@ public class FsStoreOrderScrm extends BaseEntity
     // 后台修改商品类型,0-未修改过;1-总后台;2-销售后台
     private Integer backendEditProductType;
 
+    @TableField(exist = false)
+    private Boolean isLive = false;
+
 
 }

+ 6 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsUserScrm.java

@@ -19,6 +19,7 @@ import java.util.Date;
  * @author fs
  * @date 2022-03-15
  */
+@Data
 @TableName("fs_user")
 public class FsUserScrm extends BaseEntity
 {
@@ -187,6 +188,11 @@ public class FsUserScrm extends BaseEntity
      * 小程序appId,多个用逗号分隔
      */
     private String appId;
+    /**
+     * 储值金额
+     */
+    @Excel(name = "储值金额")
+    private BigDecimal rechargeBalance;
 
     public String getAppId() {
         return appId;

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -102,7 +102,7 @@ public interface FsStoreAfterSalesScrmMapper
             "<if test =\"maps.hfOrderCode != null and  maps.hfOrderCode!='' \"> " +
               "and fsps.pay_code = #{maps.hfOrderCode} " +
             "</if>" +
-            "<if test =\"maps.bankTransactionId != null and  maps.bankTransactionId!='' \"> " +
+            "<if test = 'maps.bankTransactionId != null    '> " +
               "and fsps.bank_transaction_id = #{maps.bankTransactionId} " +
             "</if>" +
             "<if test = 'maps.status != null    '> " +

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java

@@ -371,6 +371,13 @@ public interface FsStorePaymentScrmMapper
     @Select("select * from fs_store_payment_scrm where business_type=2 and order_id=#{orderId} and status=1   ")
     List<FsStorePaymentScrm> selectFsStorePaymentByOrderId(Long orderId);
 
+    /** App 发货(business_type=8),不对接小程序/ERP,仅本地订单发货 */
+    @Select("select * from fs_store_payment_scrm where business_type=8 and order_id=#{orderId} and status=1 order by payment_id desc")
+    List<FsStorePaymentScrm> selectFsStorePaymentByOrderIdBusinessType8(Long orderId);
+
+    @Select("select * from fs_store_payment_scrm where (business_type=2 or business_type=8)  and order_id=#{orderId} and status=1   ")
+    List<FsStorePaymentScrm> selectFsStorePaymentByOrderIdApp(Long orderId);
+
 
     Long selectFsStorePaymentCount(@Param("type") int type,@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
 

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductCategoryScrmMapper.java

@@ -113,4 +113,9 @@ public interface FsStoreProductCategoryScrmMapper
     List<OptionsVO> selectFsStoreProductPidList();
 
     List<Long> selectCateIdsByName(FsStoreProductCategoryScrm fsStoreProductCategoryScrm);
+
+    /**
+     * 「秒杀商品」一级分类及其子分类 ID(用于商品 cate_id 过滤)
+     */
+    List<Long> selectSeckillCategoryIdsForProduct(@Param("storeId") Long storeId);
 }

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsUserScrmMapper.java

@@ -146,6 +146,13 @@ public interface FsUserScrmMapper
      */
     List<FsUserScrm> selectFsUserListByPhoneExact(String phone);
 
+    /**
+     * 根据用户ID精确查询用户列表(完全匹配)
+     * @param userId 用户ID(fs_user.user_id)
+     * @return 用户列表
+     */
+    List<FsUserScrm> selectFsUserListByUserIdExact(Long userId);
+
 
     @Select("select  b.total_amount,b.last_buy_time,p.pay_money as number,p.payment_id,p.pay_time," +
             " u.* FROM fs_user u LEFT JOIN  (" +

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

@@ -9,7 +9,10 @@ import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.dto.FsOrderDeliveryNoteDTO;
 import com.fs.erp.domain.ErpOrder;
 import com.fs.his.dto.FsStoreOrderAmountScrmStatsQueryDto;
+import com.fs.his.enums.PaymentMethodEnum;
+import com.fs.his.param.FsIntegralOrderDoPayParam;
 import com.fs.his.param.FsStoreOrderSalesParam;
+import com.fs.his.param.PayOrderParam;
 import com.fs.his.vo.FsStoreOrderAmountScrmStatsVo;
 import com.fs.his.vo.FsPrescribeVO;
 import com.fs.his.vo.FsStoreOrderExcelVO;
@@ -23,6 +26,7 @@ import com.fs.hisStore.dto.FsStoreOrderComputeDTO;
 import com.fs.hisStore.dto.StoreOrderExpressExportDTO;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.vo.*;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
@@ -402,4 +406,6 @@ public interface IFsStoreOrderScrmService
      * @return R
      */
     R batchDeliveryAllPendingOrders(Integer shipmentType);
+
+    R payment(FsIntegralOrderDoPayParam param, PaymentMethodEnum paymentMethodEnum);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStorePaymentScrmService.java

@@ -2,6 +2,7 @@ package com.fs.hisStore.service;
 
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
+import com.fs.his.param.PayOrderParam;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.vo.FsStorePaymentStatisticsVO;
@@ -166,4 +167,8 @@ public interface IFsStorePaymentScrmService
      * 批量导入更新微信订单发货状态
      * **/
     R oneClickShipping();
+
+    R processPaymentScrm(PayOrderParam payOrderParam);
+
+    List<FsStorePaymentScrm> selectFsStorePaymentByOrderIdApp(Long id);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java

@@ -90,6 +90,11 @@ public interface IFsStoreProductScrmService
 
     List<FsStoreProductListQueryVO> selectFsStoreProductListQuery(FsStoreProductQueryParam param);
 
+    /**
+     * 秒杀专区商品:分类为「秒杀商品」或其子分类,最多 10 条
+     */
+    List<FsStoreProductListQueryVO> selectFsStoreSeckillProductListQuery(Long storeId);
+
     FsStoreProductQueryVO selectFsStoreProductByIdQuery(Long productId,String storeId);
 
     void decProductStock(Long productId, Long productAttrValueId, Integer cartNum);

+ 7 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsUserScrmService.java

@@ -119,6 +119,13 @@ public interface IFsUserScrmService
      */
     List<FsUserScrm> selectFsUserListByPhoneExact(String phone);
 
+    /**
+     * 根据用户ID精确查询用户列表(完全匹配)
+     * @param userId 用户ID(fs_user.user_id)
+     * @return 用户列表
+     */
+    List<FsUserScrm> selectFsUserListByUserIdExact(Long userId);
+
     TableDataInfo selectCusListPage(SelectCusListPageParam param);
 
     List<FsCompanyUserListQueryVO> selectFsCompanyUserListQuery(FsUserScrm fsUser);

+ 97 - 20
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -29,6 +29,7 @@ import com.fs.erp.dto.ErpRefundUpdateRequest;
 import com.fs.erp.mapper.FsJstAftersalePushMapper;
 import com.fs.erp.mapper.FsJstAftersalePushScrmMapper;
 import com.fs.erp.service.IErpOrderService;
+import com.fs.his.config.AppConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
@@ -70,6 +71,7 @@ import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.google.gson.Gson;
 import lombok.Synchronized;
 import org.apache.commons.beanutils.BeanUtils;
@@ -84,6 +86,7 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.sql.Timestamp;
 import java.text.DecimalFormat;
 import java.text.ParseException;
@@ -631,7 +634,7 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
     }
 
     @Override
-    @DataScope(deptAlias = "cu", userAlias = "cu")
+//    @DataScope(deptAlias = "cu", userAlias = "cu")
     public List<FsStoreAfterSalesVO> selectFsStoreAfterSalesListVO(FsStoreAfterSalesScrm fsStoreAfterSales) {
         List<FsStoreAfterSalesVO> fsStoreAfterSalesVOS = fsStoreAfterSalesMapper.selectFsStoreAfterSalesListVO(fsStoreAfterSales);
         List<Long> orderIds = new ArrayList<>();
@@ -858,27 +861,29 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
 
         //将钱退还给用户
         if(order.getPayMoney().compareTo(BigDecimal.ZERO)==1){
-            List<FsStorePaymentScrm> payments=paymentService.selectFsStorePaymentByOrderId(order.getId());
+            List<FsStorePaymentScrm> payments=paymentService.selectFsStorePaymentByOrderIdApp(order.getId());
             if(payments!=null){
+                SysConfig sysConfig = configService.selectConfigByConfigKey("app.config");
+                AppConfig config = new Gson().fromJson(sysConfig.getConfigValue(), AppConfig.class);
 
                 for(FsStorePaymentScrm payment:payments){
+                    if (order.getPayType().equals("99")||order.getPayType().equals("5")){
+                        payment.setAppId(config.getAppId());
+                    }
                     if (StringUtils.isBlank(payment.getAppId())) {
                         throw new IllegalArgumentException("appId不能为空");
                     }
-                    FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(payment.getAppId());
-                    if (fsCoursePlaySourceConfig == null) {
-                        throw new CustomException("未找到appId对应的小程序配置: " + payment.getAppId());
+                    String payType = payment.getPayMode();
+                    if ("wxApp".equals(payment.getPayMode())){
+                        payType = "wx";
                     }
-                    Long merchantConfigId = fsCoursePlaySourceConfig.getMerchantConfigId();
-                    if (merchantConfigId == null || merchantConfigId <= 0) {
-                        throw new CustomException("小程序没有配置商户信息");
-                    }
-                    MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
-                    FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
 
                     if (payment.getPayMode()==null||payment.getPayMode().equals("wx")){
+                        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigByAppId(payment.getAppId(),payType);
+                        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
                         WxPayConfig payConfig = new WxPayConfig();
-                        payConfig.setAppId(fsCoursePlaySourceConfig.getAppid());
+                        payConfig.setAppId(payment.getAppId());
                         payConfig.setMchId(fsPayConfig.getWxMchId());
                         payConfig.setMchKey(fsPayConfig.getWxMchKey());
                         payConfig.setKeyPath(fsPayConfig.getKeyPath());
@@ -917,20 +922,20 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
                             return R.error("退款请求失败"+e.getErrCodeDes());
                         }
                     }else if (payment.getPayMode()!=null&&payment.getPayMode().equals("hf")){
+                        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigByAppId(payment.getAppId(),payType);
+                        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
                         String huifuId="";
-                        FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
                         if (payment.getAppId() != null) {
-                            FsHfpayConfig fsHfpayConfig = fsHfpayConfigMapper.selectByAppId(payment.getAppId());
-                            if (fsHfpayConfig == null){
+                            if (merchantAppConfig == null){
                                 huifuId = fsPayConfig.getHuifuId();
                             }else {
-                                huifuId = fsHfpayConfig.getHuifuId();
+                                huifuId = merchantAppConfig.getMerchantId();
                             }
                         } else {
                             if (("益善缘".equals(cloudHostProper.getCompanyName()))) {
 
-                                FsHfpayConfig fsPayConfig2 = fsHfpayConfigMapper.selectByAppId("wx0d1a3dd485268521");
-                                huifuId = fsPayConfig2.getHuifuId();
+                                huifuId = merchantAppConfig.getMerchantId();
                             }else {
                                 huifuId=fsPayConfig.getHuifuId();
                             }
@@ -943,9 +948,17 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
                         request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                         request.setReqSeqId("refund-"+payment.getPayCode());
                         Map<String, Object> extendInfoMap = new HashMap<>();
-                        extendInfoMap.put("org_party_order_id", payment.getBankSerialNo());
+                        if (order.getPayType().equals("99")){
+                            request.setOrdAmt(payment.getPayMoney().setScale(2, RoundingMode.DOWN).toString());
+                            extendInfoMap.put("org_req_seq_id", "store-"+payment.getPayCode());
+                        }else {
+                            request.setOrdAmt(payment.getPayMoney().toString());
+                            extendInfoMap.put("org_party_order_id", payment.getBankSerialNo());
+                            request.setAppId(payment.getAppId());
+                        }
+                        request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                        request.setReqSeqId("refund-"+payment.getPayCode());
                         request.setExtendInfo(extendInfoMap);
-                        request.setAppId(payment.getAppId());
                         HuiFuRefundResult refund = huiFuService.refund(request);
                         logger.info("退款:"+refund);
                         if((refund.getResp_code().equals("00000000")||refund.getResp_code().equals("00000100"))&&(refund.getTrans_stat().equals("S")||refund.getTrans_stat().equals("P"))){
@@ -957,6 +970,42 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
                             TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                             return R.error(refund.getResp_desc());
                         }
+                    }else if ("wxApp".equals(payment.getPayMode()) && "wx_app".equals(payment.getPayTypeCode())) {
+                        // 处理微信退款
+                        WxPayService wxPayService = getWxPayService();
+                        WxPayRefundRequest refundRequest = new WxPayRefundRequest();
+                        refundRequest.setOutTradeNo("store-"+payment.getPayCode());
+                        refundRequest.setOutRefundNo("store-"+payment.getPayCode());
+                        refundRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                        refundRequest.setRefundFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                        try {
+                            WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
+                            WxPayRefundQueryResult refundQueryResult = wxPayService.refundQuery("", refundResult.getOutTradeNo(), refundResult.getOutRefundNo(), refundResult.getRefundId());
+                            if (refundQueryResult != null && refundQueryResult.getResultCode().equals("SUCCESS")) {
+                                FsStorePaymentScrm paymentMap = new FsStorePaymentScrm();
+                                paymentMap.setPaymentId(payment.getPaymentId());
+                                paymentMap.setStatus(-1);
+                                paymentMap.setRefundTime(DateUtils.getNowDate());
+                                paymentMap.setRefundMoney(payment.getPayMoney());
+                                paymentService.updateFsStorePayment(paymentMap);
+                            } else {
+                                throw new CustomException("退款请求失败" + refundQueryResult.getReturnMsg());
+                            }
+
+                        } catch (WxPayException e) {
+                            throw new CustomException("退款请求失败" + e);
+                        }
+
+//                    }else if (payment.getPayMode()!=null&&payment.getPayMode().equals("cz")) {
+//                        FsStorePaymentScrm paymentMap = new FsStorePaymentScrm();
+//                        paymentMap.setPaymentId(payment.getPaymentId());
+//                        paymentMap.setStatus(-1);
+//                        paymentMap.setRefundTime(DateUtils.getNowDate());
+//                        paymentMap.setRefundMoney(payment.getPayMoney());
+//                        paymentService.updateFsStorePayment(paymentMap);
+//                        FsUserScrm fsUserScrm = userService.selectFsUserById(order.getUserId());
+//                        processStoreRechargePayment(payment,fsUserScrm,order);
+
 
                     }else {
                         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
@@ -980,6 +1029,34 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         return R.ok();
     }
 
+    /**
+     * 封装微信参数
+     *
+     * @return
+     */
+    private WxPayService getWxPayService(){
+        SysConfig sysConfig = configService.selectConfigByConfigKey("app.config");
+        AppConfig config = new Gson().fromJson(sysConfig.getConfigValue(), AppConfig.class);
+        FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(config.getAppId());
+        if (fsCoursePlaySourceConfig == null) {
+            throw new CustomException("未找到appId对应的小程序配置: " + config.getAppId());
+        }
+        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
+        FsPayConfig payConfig1 = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(merchantAppConfig.getAppId());
+        payConfig.setMchId(payConfig1.getWxMchId());
+        payConfig.setMchKey(payConfig1.getWxMchKey());
+        payConfig.setSubAppId(StringUtils.trimToNull(null));
+        payConfig.setSubMchId(StringUtils.trimToNull(null));
+        payConfig.setKeyPath(payConfig1.getKeyPath());
+        payConfig.setNotifyUrl(payConfig1.getNotifyUrlScrm());
+        WxPayServiceImpl payService = new WxPayServiceImpl();
+        payService.setConfig(payConfig);
+        return payService;
+    }
+
     @Autowired
     private IFsStoreProductPurchaseLimitScrmService purchaseLimitService;
 

+ 210 - 31
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -17,6 +17,8 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.TypeReference;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.api.param.OrderListParam;
 import com.fs.api.vo.OrderListVO;
 import com.fs.api.vo.ProductListVO;
@@ -61,18 +63,16 @@ import com.fs.erp.dto.ErpRefundUpdateRequest;
 import com.fs.erp.dto.df.*;
 import com.fs.erp.mapper.FsErpFinishPushMapper;
 import com.fs.erp.service.IErpOrderService;
+import com.fs.his.config.AppConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
-import com.fs.his.dto.FsPrescribeUsageDTO;
-import com.fs.his.dto.FsProdItemDTO;
-import com.fs.his.dto.FsStoreOrderAmountScrmStatsQueryDto;
-import com.fs.his.dto.FsStoreOrderItemDTO;
-import com.fs.his.enums.FsStoreOrderLogEnum;
-import com.fs.his.enums.FsStoreOrderStatusEnum;
-import com.fs.his.enums.FsUserIntegralLogTypeEnum;
+import com.fs.his.dto.*;
+import com.fs.his.enums.*;
 import com.fs.his.mapper.*;
+import com.fs.his.param.FsIntegralOrderDoPayParam;
 import com.fs.his.param.FsStoreOrderSalesParam;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
+import com.fs.his.param.PayOrderParam;
 import com.fs.his.service.IFsPrescribeService;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserWatchService;
@@ -88,7 +88,17 @@ import com.fs.hisStore.config.StoreIntegralConfig;
 import com.fs.hisStore.constants.StoreConstants;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.dto.*;
+import com.fs.hisStore.dto.ErpRemarkDTO;
+import com.fs.hisStore.dto.ExpressDataDTO;
+import com.fs.hisStore.dto.ExpressInfoDTO;
+import com.fs.hisStore.dto.ExpressNotifyDTO;
+import com.fs.hisStore.dto.ExpressResultDTO;
+import com.fs.hisStore.dto.FsStoreCartDTO;
+import com.fs.hisStore.dto.StoreOrderExpressExportDTO;
+import com.fs.hisStore.dto.StorePackageProductDTO;
+import com.fs.hisStore.dto.StoreProductGroupDTO;
 import com.fs.hisStore.enums.*;
+import com.fs.hisStore.enums.ShipperCodeEnum;
 import com.fs.hisStore.mapper.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
@@ -109,9 +119,12 @@ import com.fs.huifuPay.domain.HuifuCreateOrderResult;
 import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayRefundRequest;
 import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
 import com.fs.huifuPay.service.HuiFuService;
+import com.fs.live.domain.LiveOrder;
+import com.fs.live.mapper.LiveOrderMapper;
 import com.fs.pay.pay.dto.OrderQueryDTO;
 import com.fs.pay.pay.dto.RefundDTO;
 import com.fs.pay.service.IPayService;
+import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysDictTypeService;
@@ -126,14 +139,18 @@ import com.fs.wx.order.utils.WxShippingErrorHandler;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.ybPay.domain.RefundResult;
 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
 import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
 import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
 import com.github.binarywang.wxpay.bean.result.WxPayRefundQueryResult;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
 import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
+ import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.google.common.base.Joiner;
+    import com.google.gson.Gson;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
@@ -157,6 +174,7 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 import javax.annotation.PostConstruct;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.nio.charset.Charset;
 import java.sql.Timestamp;
 import java.text.ParseException;
@@ -295,6 +313,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     private WxPayService wxPayService;
     @Autowired
     private WxPayProperties wxPayProperties;
+    @Autowired
+    private IFsStorePaymentScrmService storePaymentService;
 
     @Autowired
     private FsErpFinishPushMapper fsErpFinishPushMapper;
@@ -2608,24 +2628,26 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             //货到付款
         } else {
             //将钱退还给用户
-            List<FsStorePaymentScrm> payments = paymentService.selectFsStorePaymentByOrderId(order.getId());
+            List<FsStorePaymentScrm> payments = paymentService.selectFsStorePaymentByOrderIdApp(order.getId());
             if (payments != null) {
+                 SysConfig sysConfig = configService.selectConfigByConfigKey("app.config");
+                 AppConfig config = new Gson().fromJson(sysConfig.getConfigValue(), AppConfig.class);
                 for (FsStorePaymentScrm payment : payments) {
+                    if (order.getPayType().equals("99")){
+                        payment.setAppId(config.getAppId());
+                    }
                     if (StringUtils.isBlank(payment.getAppId())) {
                         throw new IllegalArgumentException("appId不能为空");
                     }
-                    FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(payment.getAppId());
-                    if (fsCoursePlaySourceConfig == null) {
-                        throw new CustomException("未找到appId对应的小程序配置: " + payment.getAppId());
-                    }
-                    Long merchantConfigId = fsCoursePlaySourceConfig.getMerchantConfigId();
-                    if (merchantConfigId == null || merchantConfigId <= 0) {
-                        throw new CustomException("小程序没有配置商户信息");
+                    String payType = payment.getPayMode();
+                    if ("wxApp".equals(payment.getPayMode())){
+                        payType = "wx";
                     }
-                    MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
-                    FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
 
                     if (payment.getPayMode() == null || payment.getPayMode().equals("wx")) {
+
+                        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigByAppId(payment.getAppId(),payType);
+                        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
                         WxPayConfig payConfig = new WxPayConfig();
                         payConfig.setAppId(payment.getAppId());
                         payConfig.setMchId(fsPayConfig.getWxMchId());
@@ -2666,20 +2688,20 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                             return R.error("退款请求失败" + e.getErrCodeDes());
                         }
                     } else if (payment.getPayMode() != null && payment.getPayMode().equals("hf")) {
+                        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigByAppId(payment.getAppId(),payType);
+                        FsPayConfig fsPayConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
                         String huifuId="";
-                        FsHfpayConfigMapper fsHfpayConfigMapper = SpringUtils.getBean(FsHfpayConfigMapper.class);
                         if (payment.getAppId() != null) {
-                            FsHfpayConfig fsHfpayConfig = fsHfpayConfigMapper.selectByAppId(payment.getAppId());
-                            if (fsHfpayConfig == null){
+                            if (merchantAppConfig == null){
                                 huifuId = fsPayConfig.getHuifuId();
                             }else {
-                                huifuId = fsHfpayConfig.getHuifuId();
+                                huifuId = merchantAppConfig.getMerchantId();
                             }
                         } else {
                             if (("益善缘".equals(cloudHostProper.getCompanyName()))) {
 
-                                FsHfpayConfig fsPayConfig2 = fsHfpayConfigMapper.selectByAppId("wx0d1a3dd485268521");
-                                huifuId = fsPayConfig2.getHuifuId();
+                                huifuId = merchantAppConfig.getMerchantId();
                             }else {
                                 huifuId=fsPayConfig.getHuifuId();
                             }
@@ -2689,12 +2711,18 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                         V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
                         request.setOrgHfSeqId(payment.getTradeNo());
                         request.setHuifuId(huifuId);
-                        request.setOrdAmt(payment.getPayMoney().toString());
+                        Map<String, Object> extendInfoMap = new HashMap<>();
+                        if (order.getPayType().equals("99")){
+                            request.setOrdAmt(payment.getPayMoney().setScale(2, RoundingMode.DOWN).toString());
+                            extendInfoMap.put("org_req_seq_id", "store-"+payment.getPayCode());
+                        }else {
+                            request.setOrdAmt(payment.getPayMoney().toString());
+                            extendInfoMap.put("org_party_order_id", payment.getBankSerialNo());
+                            request.setAppId(payment.getAppId());
+                        }
+
                         request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                         request.setReqSeqId("refund-" + payment.getPayCode());
-                        Map<String, Object> extendInfoMap = new HashMap<>();
-                        extendInfoMap.put("org_party_order_id", payment.getBankSerialNo());
-                        request.setExtendInfo(extendInfoMap);
                         request.setAppId(payment.getAppId());
                         HuiFuRefundResult refund = huiFuService.refund(request);
                         logger.info("退款:" + refund);
@@ -2707,7 +2735,41 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                             TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                             return R.error(refund.getResp_desc());
                         }
+                    }else if ("wxApp".equals(payment.getPayMode()) && "wx_app".equals(payment.getPayTypeCode())) {
+                        // 处理微信退款
+                        WxPayService wxPayService = getWxPayService();
+                        WxPayRefundRequest refundRequest = new WxPayRefundRequest();
+                        refundRequest.setOutTradeNo("store-"+payment.getPayCode());
+                        refundRequest.setOutRefundNo("store-"+payment.getPayCode());
+                        refundRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                        refundRequest.setRefundFee(WxPayUnifiedOrderRequest.yuanToFen(payment.getPayMoney().toString()));
+                        try {
+                            WxPayRefundResult refundResult = wxPayService.refund(refundRequest);
+                            WxPayRefundQueryResult refundQueryResult = wxPayService.refundQuery("", refundResult.getOutTradeNo(), refundResult.getOutRefundNo(), refundResult.getRefundId());
+                            if (refundQueryResult != null && refundQueryResult.getResultCode().equals("SUCCESS")) {
+                                FsStorePaymentScrm paymentMap = new FsStorePaymentScrm();
+                                paymentMap.setPaymentId(payment.getPaymentId());
+                                paymentMap.setStatus(-1);
+                                paymentMap.setRefundTime(DateUtils.getNowDate());
+                                paymentMap.setRefundMoney(payment.getPayMoney());
+                                paymentService.updateFsStorePayment(paymentMap);
+                            } else {
+                                throw new CustomException("退款请求失败" + refundQueryResult.getReturnMsg());
+                            }
+
+                        } catch (WxPayException e) {
+                            throw new CustomException("退款请求失败" + e);
+                        }
 
+//                    }else if (payment.getPayMode()!=null&&payment.getPayMode().equals("cz")) {
+//                        FsStorePaymentScrm paymentMap = new FsStorePaymentScrm();
+//                        paymentMap.setPaymentId(payment.getPaymentId());
+//                        paymentMap.setStatus(-1);
+//                        paymentMap.setRefundTime(DateUtils.getNowDate());
+//                        paymentMap.setRefundMoney(payment.getPayMoney());
+//                        paymentService.updateFsStorePayment(paymentMap);
+//                        FsUserScrm fsUserScrm = userService.selectFsUserById(order.getUserId());
+//                        processStoreRechargePayment(payment,fsUserScrm,order);
                     } else {
                         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                         return R.error("支付类型异常");
@@ -2793,6 +2855,34 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
     }
 
+    /**
+     * 封装微信参数
+     *
+     * @return
+     */
+    private WxPayService getWxPayService(){
+        SysConfig sysConfig = configService.selectConfigByConfigKey("app.config");
+        AppConfig config = new Gson().fromJson(sysConfig.getConfigValue(), AppConfig.class);
+        FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(config.getAppId());
+        if (fsCoursePlaySourceConfig == null) {
+            throw new CustomException("未找到appId对应的小程序配置: " + config.getAppId());
+        }
+        MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
+        FsPayConfig payConfig1 = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(merchantAppConfig.getAppId());
+        payConfig.setMchId(payConfig1.getWxMchId());
+        payConfig.setMchKey(payConfig1.getWxMchKey());
+        payConfig.setSubAppId(StringUtils.trimToNull(null));
+        payConfig.setSubMchId(StringUtils.trimToNull(null));
+        payConfig.setKeyPath(payConfig1.getKeyPath());
+        payConfig.setNotifyUrl(payConfig1.getNotifyUrlScrm());
+        WxPayServiceImpl payService = new WxPayServiceImpl();
+        payService.setConfig(payConfig);
+        return payService;
+    }
+
     @Override
     public R updateExpress(FsStoreOrderExpressEditParam param) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(param.getOrderId());
@@ -4644,14 +4734,21 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                         .add(vo);
             }
 
-            // 按小程序appId分组订单(从支付记录中获取)
+            // 按小程序appId分组订单(从支付记录中获取);business_type=8 为 App 发货,跳过小程序仅更新本地已发货
             Map<String, List<FsOrderDeliveryNoteDTO>> ordersByAppId = new HashMap<>();
+            List<FsOrderDeliveryNoteDTO> appDeliveryBusinessType8List = new ArrayList<>();
             for (FsOrderDeliveryNoteDTO dto : successList) {
                 String orderNumber = dto.getOrderNumber();
                 // 通过订单号查询订单
                 FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderByOrderCode(orderNumber);
                 if (order != null) {
-                    // 查询订单的支付记录获取appId
+                    List<FsStorePaymentScrm> appDeliveryPayments = fsStorePaymentMapper.selectFsStorePaymentByOrderIdBusinessType8(order.getId());
+                    if (!appDeliveryPayments.isEmpty()) {
+                        appDeliveryBusinessType8List.add(dto);
+                        log.debug("订单号: {} 为 business_type=8(App发货),跳过小程序发货流程", orderNumber);
+                        continue;
+                    }
+                    // 查询订单的支付记录获取appId(business_type=2)
                     List<FsStorePaymentScrm> paymentList = fsStorePaymentMapper.selectFsStorePaymentByOrderId(order.getId());
                     if (!paymentList.isEmpty() && paymentList.get(0).getAppId() != null) {
                         String orderAppId = paymentList.get(0).getAppId();
@@ -4669,11 +4766,13 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 }
             }
 
-            if (ordersByAppId.isEmpty()) {
+            if (ordersByAppId.isEmpty() && appDeliveryBusinessType8List.isEmpty()) {
                 return R.error("所有订单都无法获取对应的小程序appId,请确保订单已支付");
             }
 
-            log.info("导入订单涉及{}个小程序,开始分组处理", ordersByAppId.size());
+            if (!ordersByAppId.isEmpty()) {
+                log.info("导入订单涉及{}个小程序,开始分组处理", ordersByAppId.size());
+            }
 
             // 按小程序分组批量上传微信发货信息
             for (Map.Entry<String, List<FsOrderDeliveryNoteDTO>> entry : ordersByAppId.entrySet()) {
@@ -4728,6 +4827,25 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                 }
             }
 
+            // business_type=8:仅更新本地订单物流与已发货状态,不上传微信小程序
+            if (!appDeliveryBusinessType8List.isEmpty()) {
+                log.info("business_type=8(App发货) 订单共{}条,跳过小程序,仅更新本地发货信息", appDeliveryBusinessType8List.size());
+            }
+            for (FsOrderDeliveryNoteDTO dto : appDeliveryBusinessType8List) {
+                int rowNum = successList.indexOf(dto) + 2;
+                if (StringUtils.isEmpty(dto.getOrderNumber())) {
+                    continue;
+                }
+                String deliverySn = expressDeliveryMap.get(dto.getDeliveryName());
+                if (deliverySn == null) {
+                    result.addFailure(rowNum, dto.getOrderNumber(), dto.getDeliveryId(), "物流公司名称异常");
+                    continue;
+                }
+                dto.setDeliverySn(deliverySn);
+                updateList.add(dto);
+                result.addSuccess();
+            }
+
             //批量更新数据
             if (!updateList.isEmpty()) {
                 batchUpdateDeliveryNotes(updateList);
@@ -6321,4 +6439,65 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             return WxShippingErrorHandler.handleExceptionErrorWithDetail(e.getMessage(), dto.getOrderNumber());
         }
     }
+
+
+    /**
+     * 预支付
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public R payment(FsIntegralOrderDoPayParam param, PaymentMethodEnum paymentMethod) {
+        FsStoreOrderScrm order = buildPayment(param);
+        if (Objects.isNull(order) || !order.getStatus().equals(0)
+                || order.getPayMoney().compareTo(BigDecimal.ZERO) <= 0){
+            return R.error("非法操作");
+        }
+
+        PayOrderParam payOrderParam = buildPayOrderParam(paymentMethod, order);
+        return storePaymentService.processPaymentScrm(payOrderParam);
+    }
+
+    @Autowired
+    private LiveOrderMapper liveOrderMapper;
+
+    public FsStoreOrderScrm buildPayment(FsIntegralOrderDoPayParam param){
+        if(param.getType().equals("live")){
+            LiveOrder liveOrder = liveOrderMapper.selectLiveOrderByOrderId(String.valueOf(param.getOrderId()));
+            if (ObjectUtil.isNotEmpty(liveOrder)){
+                FsStoreOrderScrm orderScrm = new FsStoreOrderScrm();
+                BeanUtils.copyProperties(liveOrder,orderScrm);
+                orderScrm.setId(liveOrder.getOrderId());
+                orderScrm.setUserId(Long.valueOf(liveOrder.getUserId()));
+                orderScrm.setIsLive(true);
+                return orderScrm;
+            }
+        }
+        if (param.getType().equals("store")){
+            FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(param.getOrderId());
+            if (ObjectUtil.isNotEmpty(order)){
+                return order;
+            }
+        }
+
+        return null;
+    }
+    /**
+     * 构建参数
+     */
+    private static PayOrderParam buildPayOrderParam(PaymentMethodEnum paymentMethod, FsStoreOrderScrm order) {
+        PayOrderParam payOrderParam = new PayOrderParam();
+        payOrderParam.setOrderId(order.getId());
+        payOrderParam.setOrderCode(order.getOrderCode());
+        payOrderParam.setAmount(order.getPayMoney());
+        payOrderParam.setUserId(order.getUserId());
+        payOrderParam.setCompanyId(order.getCompanyId());
+        payOrderParam.setCompanyUserId(order.getCompanyUserId());
+        payOrderParam.setPaymentMethod(paymentMethod);
+        if (order.getIsLive()){
+            payOrderParam.setBusinessType(BusinessTypeEnum.LIVE_ORDER);
+        }else {
+            payOrderParam.setBusinessType(BusinessTypeEnum.ORDER_ORDER);
+        }
+        return payOrderParam;
+    }
 }

+ 532 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.hisStore.service.impl;
 
 import java.math.BigDecimal;
 
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -16,6 +17,7 @@ import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
 import cn.binarywang.wx.miniapp.bean.shop.request.shipping.*;
 import cn.binarywang.wx.miniapp.bean.shop.response.WxMaOrderShippingInfoBaseResponse;
 import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.TypeReference;
@@ -36,6 +38,7 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.RechargeRecord;
 import com.fs.company.mapper.CompanyConfigMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
@@ -47,24 +50,31 @@ import com.fs.course.domain.FsCourseRedPacketLog;
 import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import com.fs.course.service.IFsCourseRedPacketLogService;
+import com.fs.his.config.AppConfig;
 import com.fs.his.domain.*;
 import com.fs.his.domain.FsPayConfig;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
 import com.fs.his.domain.MerchantAppConfig;
+import com.fs.his.dto.PayConfigDTO;
+import com.fs.his.enums.PaymentMethodEnum;
 import com.fs.his.mapper.FsUserWxMapper;
 import com.fs.his.mapper.MerchantAppConfigMapper;
+import com.fs.his.param.PayOrderParam;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.hisStore.config.StoreConfig;
 import com.fs.hisStore.config.StoreConfig;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
 import com.fs.hisStore.domain.FsUserScrm;
+import com.fs.hisStore.enums.OrderInfoEnum;
 import com.fs.hisStore.enums.StatTypeEnum;
 import com.fs.his.utils.HttpUtil;
 import com.fs.hisStore.config.StoreConfig;
 import com.fs.hisStore.enums.SysConfigEnum;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
@@ -73,6 +83,10 @@ import com.fs.huifuPay.domain.HuiFuCreateOrder;
 import com.fs.huifuPay.domain.HuifuCreateOrderResult;
 import com.fs.huifuPay.sdk.opps.core.utils.HuiFuUtils;
 import com.fs.huifuPay.service.HuiFuService;
+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.pay.pay.dto.WxJspayDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.system.oss.CloudStorageService;
@@ -99,6 +113,7 @@ import com.github.binarywang.wxpay.service.WxPayService;
 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
+import com.hc.openapi.tool.fastjson.JSON;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.error.WxErrorException;
@@ -128,6 +143,12 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
     Logger logger = LoggerFactory.getLogger(getClass());
     @Autowired
     private WxPayService wxPayService;
+    @Autowired
+    private FsUserWxMapper userWxMapper;
+    @Autowired
+    private LiveOrderPaymentMapper liveOrderPaymentMapper;
+//    @Autowired
+//    private RechargeRecordMapper rechargeRecordMapper;
 
     @Autowired
     private FsCoursePlaySourceConfigMapper fsCoursePlaySourceConfigMapper;
@@ -160,6 +181,10 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
 
     @Autowired
     private RedisCache redisCache;
+    @Autowired
+    private LiveOrderMapper liveOrderMapper;
+    @Autowired
+    private FsStoreOrderScrmMapper storeOrderScrmMapper;
 
     /**
      * 查询支付明细
@@ -1398,4 +1423,511 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
             return WxShippingErrorHandler.handleExceptionError(e.getMessage(), dto.getBankTransactionId());
         }
     }
+
+
+    /**
+     * 发起支付
+     * @param payOrderParam 入参
+     * @return R
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public R processPaymentScrm( PayOrderParam payOrderParam) {
+        logger.info("发起支付 payOrderParam: {}", JSON.toJSONString(payOrderParam));
+
+        if (payOrderParam.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
+            throw new CustomException("支付金额不正确");
+        }
+
+        FsUserScrm user = userService.selectFsUserById(payOrderParam.getUserId());
+        if (Objects.isNull(user)) {
+            throw new CustomException("用户不存在");
+        }
+        if (PaymentMethodEnum.CZ_PAY == payOrderParam.getPaymentMethod()) {
+            if (user.getRechargeBalance().compareTo(BigDecimal.ZERO) <= 0) {
+                throw new CustomException("余额不足!请先充值。");
+            }
+        }
+        String type = null;
+        FsPayConfig payConfig = new FsPayConfig();
+        if (PaymentMethodEnum.WX_APP == payOrderParam.getPaymentMethod()) {
+            String json = configService.selectConfigByKey("app.config");
+            AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+            payOrderParam.setAppId(config.getAppId());
+            type = "wxApp";
+        }
+        //支付宝可以不需要appid(在没有appid的情况下)【ps:小程序的支付宝没传appid 就G】
+        if ((PaymentMethodEnum.ALIPAY == payOrderParam.getPaymentMethod())
+                && StringUtils.isBlank(payOrderParam.getAppId())) {
+            String json = configService.selectConfigByKey("his.pay");
+            PayConfigDTO payConfigDTO = JSONUtil.toBean(json, PayConfigDTO.class);
+            payConfig.setType(payConfigDTO.getType());
+        }else if(!(PaymentMethodEnum.CZ_PAY == payOrderParam.getPaymentMethod())){
+            if (StringUtils.isBlank(payOrderParam.getAppId())) {
+                throw new IllegalArgumentException("appId不能为空");
+            }
+            FsCoursePlaySourceConfig fsCoursePlaySourceConfig = fsCoursePlaySourceConfigMapper.selectCoursePlaySourceConfigByAppId(payOrderParam.getAppId());
+            if (fsCoursePlaySourceConfig == null) {
+                throw new CustomException("未找到appId对应的小程序配置: " + payOrderParam.getAppId());
+            }
+            Long merchantConfigId = fsCoursePlaySourceConfig.getMerchantConfigId();
+            if (merchantConfigId == null || merchantConfigId <= 0) {
+                throw new CustomException("小程序没有配置商户信息");
+            }
+
+            MerchantAppConfig merchantAppConfig = merchantAppConfigMapper.selectMerchantAppConfigById(fsCoursePlaySourceConfig.getMerchantConfigId());
+            payConfig = JSON.parseObject(merchantAppConfig.getDataJson(), FsPayConfig.class);
+            if (StringUtils.isNotEmpty(type)) {
+                payConfig.setType(type);
+            } else {
+                payConfig.setType(merchantAppConfig.getMerchantType());
+            }
+            payConfig.setAppId(fsCoursePlaySourceConfig.getAppid());
+
+            logger.debug("支付配置 his.pay: {}", payConfig);
+        }
+
+
+//        FsPayConfig payConfig = JSONUtil.toBean(json, FsPayConfig.class);
+
+        if (isWechatPayment(payOrderParam.getPaymentMethod())) {
+            String openId = getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig);
+            if (StringUtils.isBlank(openId)) {
+                throw new CustomException("用户OPENID不存在");
+            }
+        }
+
+        FsStorePaymentScrm storePayment = new FsStorePaymentScrm();
+
+        // 1. 根据业务类型创建对应的支付对象
+        if (payOrderParam.getBusinessType().getPrefix().equals("live")) {
+            // 处理直播业务支付
+            LiveOrderPayment liveOrderPayment = createLiveStorePayment(payConfig, user, payOrderParam);
+            BeanUtils.copyProperties(liveOrderPayment, storePayment);
+
+            // 处理储值支付
+            if (isRechargePayment(payOrderParam)) {
+                return processLiveRechargePayment(liveOrderPayment, user);
+            }
+        } else {
+            // 处理商城业务支付
+            storePayment = createStorePaymentScrm(payConfig, user, payOrderParam);
+
+//            // 处理储值支付
+//            if (isRechargePayment(payOrderParam)) {
+//                return processStoreRechargePayment(storePayment, user);
+//            }
+        }
+        // 根据配置类型创建第三方支付订单
+        return createThirdPartyPaymentScrm(payConfig, storePayment, user, payOrderParam);
+    }
+
+    @Override
+    public List<FsStorePaymentScrm> selectFsStorePaymentByOrderIdApp(Long id) {
+        return fsStorePaymentMapper.selectFsStorePaymentByOrderIdApp(id);
+    }
+
+    /**
+     * 判断是否为储值支付
+     * @param payOrderParam 支付订单参数
+     * @return true:储值支付 false:其他支付方式
+     */
+    private boolean isRechargePayment(PayOrderParam payOrderParam) {
+        return PaymentMethodEnum.CZ_PAY == payOrderParam.getPaymentMethod();
+    }
+
+    /**
+     * 处理直播储值支付
+     * @param liveOrderPayment 直播订单支付信息
+     * @param user 用户信息
+     * @return 支付处理结果
+     */
+    private R processLiveRechargePayment(LiveOrderPayment liveOrderPayment, FsUserScrm user) {
+        // 查询直播订单信息
+        LiveOrder liveOrder = liveOrderMapper.selectLiveOrderByOrderId(liveOrderPayment.getBusinessId());
+        Company company = companyService.selectCompanyById(liveOrder.getCompanyId());
+
+        // 扣减用户储值余额
+        deductUserRechargeBalance(user, liveOrder.getPayMoney());
+
+        // 更新直播订单状态
+        updateLiveOrderStatus(liveOrder);
+
+        // 创建消费记录
+        return R.ok();
+    }
+
+    /**
+     * 处理商城储值支付
+     * @param storePayment 商城订单支付信息
+     * @param user 用户信息
+     * @return 支付处理结果
+     */
+    private R processStoreRechargePayment(FsStorePaymentScrm storePayment, FsUserScrm user) {
+        // 查询商城订单信息
+        FsStoreOrderScrm fsStoreOrderScrm = storeOrderScrmMapper.selectFsStoreOrderById(storePayment.getOrderId());
+        Company company = companyService.selectCompanyById(fsStoreOrderScrm.getCompanyId());
+
+        // 扣减用户储值余额
+        deductUserRechargeBalance(user, storePayment.getPayMoney());
+
+        // 更新商城订单状态
+        updateStoreOrderStatus(fsStoreOrderScrm);
+
+        // 创建消费记录
+        return R.ok();
+    }
+
+    /**
+     * 扣减用户储值余额
+     * @param user 用户信息
+     * @param amount 扣减金额
+     */
+    private void deductUserRechargeBalance(FsUserScrm user, BigDecimal amount) {
+        user.setRechargeBalance(user.getRechargeBalance().subtract(amount));
+        userService.updateFsUser(user);
+    }
+
+    /**
+     * 更新直播订单状态
+     * @param liveOrder 直播订单
+     */
+    private void updateLiveOrderStatus(LiveOrder liveOrder) {
+        liveOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+        liveOrder.setPayTime(LocalDateTime.now());
+        liveOrder.setIsPay("1");
+        liveOrderMapper.updateLiveOrder(liveOrder);
+    }
+
+    /**
+     * 更新商城订单状态
+     * @param storeOrder 商城订单
+     */
+    private void updateStoreOrderStatus(FsStoreOrderScrm storeOrder) {
+        storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
+        storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+        storeOrder.setPayTime(new Date());
+        storeOrderScrmMapper.updateFsStoreOrder(storeOrder);
+    }
+
+
+
+    /**
+     * 订单支付成功后,增加消费记录
+     * @param orderId 订单ID
+     * @param orderNo 订单编号
+     * @param userId 用户ID
+     * @param userName 用户姓名
+     * @param amount 消费金额
+     * @return 影响行数
+     */
+//    public int createConsumptionRecord(Long orderId, String orderNo, Long userId,
+//                                       String userName, BigDecimal amount,Company company
+//    ) {
+//        RechargeRecord record = new RechargeRecord();
+//        record.setUserId(userId);
+//        record.setUserName(userName);
+//        record.setTotalAmount(amount);
+//        record.setTransactionId(generateTransactionId());
+//        record.setOrderId(orderId);
+//        record.setOrderNo(orderNo);
+//        record.setBusinessType(1); // 1-消费
+//        if (ObjectUtil.isEmpty(company)){
+//            record.setRemark("订单消费,自主下单没有归属公司");
+//        }else {
+//            record.setCompanyId(company.getCompanyId());
+//            record.setCompanyName(company.getCompanyName());
+//            record.setRemark("订单消费");
+//        }
+//        record.setCreateTime(new Date());
+//        record.setDelFlag("0");
+//
+//        return rechargeRecordMapper.insertRechargeRecord(record);
+//    }
+
+    /**
+     * 生成交易流水号
+     * @return 交易流水号
+     */
+    private String generateTransactionId() {
+        return "TXF" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
+    }
+    /**
+     * 直播订单支付信息
+     *
+     * @param payConfig
+     * @param user
+     * @param payOrderParam
+     * @return
+     */
+    private LiveOrderPayment createLiveStorePayment(FsPayConfig payConfig, FsUserScrm user, PayOrderParam payOrderParam) {
+        String payCode = OrderCodeUtils.getOrderSn();
+        if (StringUtils.isEmpty(payCode)) {
+            throw new CustomException("订单生成失败,请重试");
+        }
+
+        LiveOrderPayment storePayment = new LiveOrderPayment();
+
+        if (PaymentMethodEnum.CZ_PAY == payOrderParam.getPaymentMethod()){
+            storePayment.setStatus(1);
+            storePayment.setPayMode("cz");
+        }else {
+            storePayment.setStatus(0);
+            storePayment.setPayMode(payConfig.getType());
+        }
+        storePayment.setAppId(payConfig.getAppId());
+        storePayment.setBusinessCode(payOrderParam.getOrderCode());
+        storePayment.setPayCode(payCode);
+        storePayment.setPayMoney(payOrderParam.getAmount());
+        storePayment.setCreateTime(new Date());
+        storePayment.setPayTypeCode(payOrderParam.getPaymentMethod().getDesc());
+        storePayment.setBusinessType(payOrderParam.getBusinessType().getCode());
+        storePayment.setCompanyId(payOrderParam.getCompanyId());
+        storePayment.setCompanyUserId(payOrderParam.getCompanyUserId());
+        storePayment.setRemark(payOrderParam.getBusinessType().getDesc());
+        storePayment.setStoreId(payOrderParam.getStoreId());
+        storePayment.setUserId(user.getUserId());
+        storePayment.setBusinessId(payOrderParam.getOrderId().toString());
+
+        // 设置openId(如果是微信支付)
+        if (isWechatPayment(payOrderParam.getPaymentMethod())) {
+            storePayment.setOpenId(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));
+        }
+
+        if (liveOrderPaymentMapper.insertLiveOrderPayment(storePayment) <= 0) {
+            throw new CustomException("支付订单创建失败");
+        }
+
+        return storePayment;
+    }
+
+    /**
+     * 判断是否微信支付
+     * @param method 支付类型
+     * @return boolean
+     */
+    private boolean isWechatPayment(PaymentMethodEnum method) {
+        return method == PaymentMethodEnum.MINIAPP_WECHAT || method == PaymentMethodEnum.H5_WECHAT;
+    }
+
+    /**
+     * 根据支付方式获取对应的openId
+     */
+    private String getOpenIdForPaymentMethod(FsUserScrm user, PaymentMethodEnum method, FsPayConfig payConfig) {
+        String openId;
+        switch (method) {
+            case MINIAPP_WECHAT:
+                openId = user.getMaOpenId();
+                break;
+            case H5_WECHAT:
+                openId = user.getMpOpenId();
+                break;
+            default:
+                openId = null;
+        }
+
+        if (StringUtils.isBlank(openId)) {
+            Wrapper<FsUserWx> queryWrapper = Wrappers.<FsUserWx>lambdaQuery()
+                    .eq(FsUserWx::getFsUserId, user.getUserId())
+                    .eq(FsUserWx::getAppId, payConfig.getAppId());
+            FsUserWx fsUserWx = userWxMapper.selectOne(queryWrapper);
+            if (Objects.nonNull(fsUserWx)) {
+                openId = fsUserWx.getOpenId();
+            }
+        }
+
+        return openId;
+    }
+
+    /**
+     * 发起预支付
+     */
+    private R createThirdPartyPaymentScrm(FsPayConfig payConfig, FsStorePaymentScrm storePayment, FsUserScrm user, PayOrderParam payOrderParam) {
+        switch (payConfig.getType()) {
+            case "wx":
+                return createWxPayment(storePayment, user, payOrderParam, payConfig);
+            case "wxApp":
+                return createWxAppPayment(storePayment, user, payOrderParam, payConfig);
+            case "hf":
+                return createHfPayment(storePayment, user, payOrderParam, payConfig);
+            default:
+                throw new CustomException("不支持的支付方式");
+        }
+    }
+
+    /**
+     * 汇付
+     */
+    private R createHfPayment(FsStorePaymentScrm storePayment, FsUserScrm user, PayOrderParam payOrderParam, FsPayConfig payConfig) {
+        logger.debug("创建汇付订单");
+
+        HuiFuCreateOrder order = new HuiFuCreateOrder();
+        order.setTradeType(getHfTradeType(payOrderParam.getPaymentMethod()));
+        order.setReqSeqId(payOrderParam.getBusinessType().getPrefix() + "-" + storePayment.getPayCode());
+        order.setTransAmt(storePayment.getPayMoney().toString());
+        order.setGoodsDesc(payOrderParam.getBusinessType().getDesc());
+
+        // 微信支付需要设置openid
+        if (isWechatPayment(payOrderParam.getPaymentMethod())) {
+            order.setOpenid(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));
+        }
+
+        HuifuCreateOrderResult result = huiFuService.createOrder(order);
+        logger.debug("汇付支付创建结果: {}", result);
+
+        updateStorePaymentTradeNo(storePayment.getPaymentId(), result.getHf_seq_id());
+        return R.ok().put("isPay", 0).put("data", result).put("type", "hf");
+    }
+
+    /**
+     * 获取汇付交易类型
+     */
+    private String getHfTradeType(PaymentMethodEnum paymentMethod) {
+        switch (paymentMethod) {
+            case MINIAPP_WECHAT:
+                return "T_MINIAPP";
+            case H5_WECHAT:
+                return "T_JSAPI";
+            case ALIPAY:
+            case H5_ALIPAY:
+                return "A_NATIVE";
+            case T_NATIVE:
+                return "T_NATIVE";
+            default:
+                throw new CustomException("不支持的支付方式");
+        }
+    }
+
+    /**
+     * 更新支付订单交易号
+     */
+    private void updateStorePaymentTradeNo(Long paymentId, String tradeNo) {
+        FsStorePaymentScrm updatePayment = new FsStorePaymentScrm();
+        updatePayment.setPaymentId(paymentId);
+        updatePayment.setTradeNo(tradeNo);
+        fsStorePaymentMapper.updateFsStorePayment(updatePayment);
+    }
+
+
+    /**
+     * 微信支付
+     */
+    private R createWxPayment(FsStorePaymentScrm storePayment, FsUserScrm user, PayOrderParam payOrderParam, FsPayConfig payConfig) {
+        PaymentMethodEnum paymentMethod = payOrderParam.getPaymentMethod();
+        if (paymentMethod != PaymentMethodEnum.MINIAPP_WECHAT) {
+            logger.debug("微信支付 PaymentMethod: {}", paymentMethod.name());
+            throw new CustomException("不支持的支付方式");
+        }
+
+        WxPayConfig wxPayConfig = buildWxPayConfig(payConfig);
+        wxPayService.setConfig(wxPayConfig);
+
+        WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
+        orderRequest.setOpenid(getOpenIdForPaymentMethod(user, paymentMethod, payConfig));
+        orderRequest.setBody(payOrderParam.getBusinessType().getDesc());
+        orderRequest.setOutTradeNo(payOrderParam.getBusinessType().getPrefix() + "-" + storePayment.getPayCode());
+        orderRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(storePayment.getPayMoney().toString()));
+        orderRequest.setTradeType("JSAPI");
+        orderRequest.setSpbillCreateIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
+
+        try {
+            WxPayMpOrderResult orderResult = wxPayService.createOrder(orderRequest);
+            return R.ok().put("data", orderResult).put("type", "wx").put("isPay", 0);
+        } catch (WxPayException e) {
+            logger.error("微信支付发起失败: {}", e.getMessage(), e);
+            throw new CustomException("支付失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 微信App支付
+     */
+    private R createWxAppPayment(FsStorePaymentScrm storePayment, FsUserScrm user, PayOrderParam payOrderParam, FsPayConfig payConfig) {
+        //创建微信订单
+        WxPayConfig wxPayConfig = buildWxPayConfig(payConfig);
+
+        wxPayConfig.setAppId(wxPayConfig.getAppId());
+        wxPayConfig.setMchId(wxPayConfig.getMchId());
+        wxPayConfig.setMchKey(wxPayConfig.getMchKey());
+        wxPayConfig.setSubAppId(StringUtils.trimToNull(null));
+        wxPayConfig.setSubMchId(StringUtils.trimToNull(null));
+        wxPayConfig.setKeyPath(null);
+        wxPayConfig.setNotifyUrl(wxPayConfig.getNotifyUrl());
+        wxPayService.setConfig(wxPayConfig);
+        WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
+        orderRequest.setBody(payOrderParam.getBusinessType().getDesc());
+        orderRequest.setOutTradeNo(payOrderParam.getBusinessType().getPrefix() + "-" + storePayment.getPayCode());
+        orderRequest.setTotalFee(WxPayUnifiedOrderRequest.yuanToFen(storePayment.getPayMoney().toString()));
+        orderRequest.setTradeType("APP");
+        orderRequest.setSpbillCreateIp(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        orderRequest.setNotifyUrl(wxPayConfig.getNotifyUrl());
+        //调用统一下单接口,获取"预支付交易会话标识"
+        try {
+            Object result = wxPayService.createOrder(orderRequest);
+            return R.ok().put("data",result).put("type","wxApp").put("isPay",0);
+        } catch (WxPayException e) {
+            e.printStackTrace();
+            throw new CustomException("支付失败"+e.getMessage());
+        }
+    }
+
+    /**
+     * 构建微信支付配置
+     */
+    private WxPayConfig buildWxPayConfig(FsPayConfig fsPayConfig) {
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(StringUtils.trimToNull(fsPayConfig.getAppId()));
+        payConfig.setMchId(StringUtils.trimToNull(fsPayConfig.getWxMchId()));
+        payConfig.setMchKey(StringUtils.trimToNull(fsPayConfig.getWxMchKey()));
+        payConfig.setSubAppId(StringUtils.trimToNull(null));
+        payConfig.setSubMchId(StringUtils.trimToNull(null));
+        payConfig.setKeyPath(null);
+        payConfig.setNotifyUrl(StringUtils.trimToNull(fsPayConfig.getNotifyUrlScrm()));
+        return payConfig;
+    }
+
+
+    /**
+     * 创建支付订单
+     */
+    private FsStorePaymentScrm createStorePaymentScrm(FsPayConfig payConfig, FsUserScrm user, PayOrderParam payOrderParam) {
+        String payCode = OrderCodeUtils.getOrderSn();
+        if (StringUtils.isEmpty(payCode)) {
+            throw new CustomException("订单生成失败,请重试");
+        }
+
+        FsStorePaymentScrm storePayment = new FsStorePaymentScrm();
+        if (PaymentMethodEnum.CZ_PAY == payOrderParam.getPaymentMethod()){
+            storePayment.setStatus(1);
+            storePayment.setPayMode("cz");
+        }else {
+            storePayment.setStatus(0);
+            storePayment.setPayMode(payConfig.getType());
+        }
+        storePayment.setAppId(payConfig.getAppId());
+        storePayment.setOrderId(payOrderParam.getOrderId());
+        storePayment.setBusinessCode(payOrderParam.getOrderCode());
+        storePayment.setPayCode(payCode);
+        storePayment.setPayMoney(payOrderParam.getAmount());
+        storePayment.setCreateTime(new Date());
+        storePayment.setPayTypeCode(payOrderParam.getPaymentMethod().getDesc());
+        storePayment.setBusinessType(payOrderParam.getBusinessType().getCode());
+        storePayment.setCompanyId(payOrderParam.getCompanyId());
+        storePayment.setCompanyUserId(payOrderParam.getCompanyUserId());
+        storePayment.setRemark(payOrderParam.getBusinessType().getDesc());
+        storePayment.setStoreId(payOrderParam.getStoreId());
+        storePayment.setUserId(user.getUserId());
+        storePayment.setBusinessId(payOrderParam.getOrderId().toString());
+
+        // 设置openId(如果是微信支付)
+        if (isWechatPayment(payOrderParam.getPaymentMethod())) {
+            storePayment.setOpenId(getOpenIdForPaymentMethod(user, payOrderParam.getPaymentMethod(), payConfig));
+        }
+
+        if (fsStorePaymentMapper.insertFsStorePayment(storePayment) <= 0) {
+            throw new CustomException("支付订单创建失败");
+        }
+
+        return storePayment;
+    }
 }

+ 15 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -11,6 +11,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.github.pagehelper.PageHelper;
 import com.fs.common.BeanCopyUtils;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.constant.LiveKeysConstant;
@@ -1119,6 +1120,20 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         return fsStoreProductMapper.selectFsStoreProductListQuery(param);
     }
 
+    @Override
+    public List<FsStoreProductListQueryVO> selectFsStoreSeckillProductListQuery(Long storeId) {
+        List<Long> cateIds = fsStoreProductCategoryScrmMapper.selectSeckillCategoryIdsForProduct(storeId);
+        if (cateIds == null || cateIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        FsStoreProductQueryParam param = new FsStoreProductQueryParam();
+        param.setCateIds(cateIds);
+        param.setIsDisplay(1);
+        param.setStoreId(storeId);
+        PageHelper.startPage(1, 10);
+        return selectFsStoreProductListQuery(param);
+    }
+
     @Override
     public FsStoreProductQueryVO selectFsStoreProductByIdQuery(Long productId,String storeId) {
         return fsStoreProductMapper.selectFsStoreProductByIdQuery(productId,storeId,medicalMallConfig);

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserScrmServiceImpl.java

@@ -254,6 +254,11 @@ public class FsUserScrmServiceImpl implements IFsUserScrmService
         return fsUserMapper.selectFsUserListByPhoneExact(phone);
     }
 
+    @Override
+    public List<FsUserScrm> selectFsUserListByUserIdExact(Long userId) {
+        return fsUserMapper.selectFsUserListByUserIdExact(userId);
+    }
+
     @Override
     public TableDataInfo selectCusListPage(SelectCusListPageParam param) {
         Asserts.check(ObjectUtils.isNotNull(param.getPageNum()), "页数不能为空");

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/h5/FsUserPageListVO.java

@@ -29,6 +29,9 @@ public class FsUserPageListVO {
     @ApiModelProperty(value = "状态:1为正常,0为禁止")
     private Integer status;
 
+    @ApiModelProperty(value = "红包领取:1开启 0关闭")
+    private Integer redStatus;
+
     private String statusText;
 
     @ApiModelProperty(value = "公司id")

+ 7 - 1
fs-service/src/main/java/com/fs/huifuPay/service/impl/HuiFuServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.huifuPay.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
@@ -158,12 +159,17 @@ public class HuiFuServiceImpl implements HuiFuService {
     public HuiFuRefundResult refund(V2TradePaymentScanpayRefundRequest request) {
         HuiFuRefundResult huiFuRefundResult=null;
         try {
-            doInit(getMerConfig(request.getAppId()));
+            if(ObjectUtils.isNotNull(request.getAppId())){
+                doInit(getMerConfig(request.getAppId()));
+            } else {
+                doInit(getMerConfig());
+            }
             request.setReqDate(DateTools.getCurrentDateYYYYMMDD());
             Map<String, Object> response = doExecute(request);
             String jsonString = JSONObject.toJSONString(response);
             huiFuRefundResult = JSON.parseObject(jsonString, HuiFuRefundResult.class);
         } catch (Exception e){
+            e.printStackTrace();
             throw  new CustomException("退款创建失败");
         }
         return huiFuRefundResult;

+ 1 - 0
fs-service/src/main/java/com/fs/im/service/OpenIMService.java

@@ -89,4 +89,5 @@ public interface OpenIMService {
 
     OpenImResponseDTO doctorSendMsgToUser(Long userId,Long doctorId);
 
+    OpenImResponseDTO getFriendList(String userID, int pageNumber, int showNumber,Integer applyType);
 }

+ 40 - 0
fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java

@@ -1628,4 +1628,44 @@ public class OpenIMServiceImpl implements OpenIMService {
         openImMsgDTO.setContent(content);
         return openIMSendMsg(openImMsgDTO);
     }
+
+    @Override
+    public OpenImResponseDTO getFriendList(String userID, int pageNumber, int showNumber,Integer applyType) {
+        String adminToken = getAdminToken();
+        JSONObject jsonObject = new JSONObject();
+        Map<String,Object> map = new HashMap<>();
+        map.put("pageNumber", pageNumber);
+        map.put("showNumber", showNumber);
+        jsonObject.put("userID",userID);
+        jsonObject.put("pagination",map);
+        if (applyType == 1){
+            String body = HttpRequest.post(IMConfig.URL+"/friend/get_friend_list")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(jsonObject.toString())
+                    .execute()
+                    .body();
+            OpenImResponseDTO responseDTO= JSONUtil.toBean(body,OpenImResponseDTO.class);
+            return responseDTO;
+        }else if (applyType == 2){
+            String body = HttpRequest.post(IMConfig.URL+"/friend/get_self_friend_apply_list")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(jsonObject.toString())
+                    .execute()
+                    .body();
+            OpenImResponseDTO responseDTO= JSONUtil.toBean(body,OpenImResponseDTO.class);
+            return responseDTO;
+        }else if (applyType == 3){
+            String body = HttpRequest.post(IMConfig.URL+"/friend/get_friend_apply_list")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(jsonObject.toString())
+                    .execute()
+                    .body();
+            OpenImResponseDTO responseDTO= JSONUtil.toBean(body,OpenImResponseDTO.class);
+            return responseDTO;
+        }
+        return null;
+    }
 }

+ 13 - 0
fs-service/src/main/java/com/fs/live/constant/LiveCommentPinEndReason.java

@@ -0,0 +1,13 @@
+package com.fs.live.constant;
+
+public final class LiveCommentPinEndReason {
+
+    private LiveCommentPinEndReason() {}
+
+    // 置顶到期
+    public static final int EXPIRED = 1;
+    // APP取消
+    public static final int APP_CANCEL = 2;
+    // 管理员强制取消
+    public static final int ADMIN_FORCE = 3;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCommentFeatureConfig.java

@@ -0,0 +1,27 @@
+package com.fs.live.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播评论飘屏/置顶全局配置(库表固定 config_id=1)
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCommentFeatureConfig extends BaseEntity {
+
+    /** 固定为 1 */
+    private Long configId;
+    @Excel(name = "飘屏开关")
+    private Integer floatEnabled;
+    @Excel(name = "飘屏冷却秒")
+    private Integer floatCooldownSec;
+    // 角色
+    private String floatRoleCodes;
+    @Excel(name = "单房间最大置顶")
+    private Integer pinMaxPerRoom;
+    private String pinDurationOptions;
+    private String pinRoleCodes;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCommentPinActive.java

@@ -0,0 +1,16 @@
+package com.fs.live.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class LiveCommentPinActive {
+
+    private Long id;
+    private Long liveId;
+    private Long msgId;
+    private Long pinLogId;
+    private Date expireAt;
+    private Date createTime;
+}

+ 26 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCommentPinLog.java

@@ -0,0 +1,26 @@
+package com.fs.live.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCommentPinLog extends BaseEntity {
+
+    private Long logId;
+    private Long liveId;
+    private Long msgId;
+    private Long operatorUserId;
+    private String operatorNickName;
+    private String operatorRoleCode;
+    private Integer durationMinutes;
+    private Date startTime;
+    /** 置顶结束时间 */
+    private Date pinEndTime;
+    /** 1到期 2App取消 3后台强制 */
+    private Integer endReason;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/live/domain/LiveFloatMsgLog.java

@@ -0,0 +1,19 @@
+package com.fs.live.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveFloatMsgLog extends BaseEntity {
+
+    private Long logId;
+    private Long liveId;
+    private Long userId;
+    private String nickName;
+    private Long msgId;
+    private String liveRoleCode;
+    private String msgContent;
+}

+ 13 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCommentFeatureConfigMapper.java

@@ -0,0 +1,13 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCommentFeatureConfig;
+
+/**
+ * 直播评论飘屏/置顶全局配置
+ */
+public interface LiveCommentFeatureConfigMapper {
+
+    LiveCommentFeatureConfig selectByConfigId(Long configId);
+
+    int updateLiveCommentFeatureConfig(LiveCommentFeatureConfig config);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCommentPinActiveMapper.java

@@ -0,0 +1,28 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCommentPinActive;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+public interface LiveCommentPinActiveMapper {
+
+    int insertLiveCommentPinActive(LiveCommentPinActive row);
+
+    int deleteById(Long id);
+
+    int deleteByLiveIdAndMsgId(@Param("liveId") Long liveId, @Param("msgId") Long msgId);
+
+    int countByLiveId(Long liveId);
+
+    List<LiveCommentPinActive> selectByLiveId(Long liveId);
+
+    LiveCommentPinActive selectByLiveIdAndMsgId(@Param("liveId") Long liveId, @Param("msgId") Long msgId);
+
+    LiveCommentPinActive selectById(Long id);
+
+    List<LiveCommentPinActive> selectExpired(@Param("now") Date now);
+
+    List<LiveCommentPinActive> selectAllActive();
+}

+ 18 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCommentPinLogMapper.java

@@ -0,0 +1,18 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCommentPinLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+public interface LiveCommentPinLogMapper {
+
+    int insertLiveCommentPinLog(LiveCommentPinLog row);
+
+    int updatePinLogEnd(@Param("logId") Long logId,
+                        @Param("endTime") Date endTime,
+                        @Param("endReason") Integer endReason);
+
+    List<LiveCommentPinLog> selectLiveCommentPinLogList(LiveCommentPinLog query);
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio