瀏覽代碼

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
zyy 6 天之前
父節點
當前提交
36a70b1812
共有 100 個文件被更改,包括 6001 次插入475 次删除
  1. 129 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmBusinessController.java
  2. 13 7
      fs-admin/src/main/java/com/fs/crm/controller/CrmEventController.java
  3. 100 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmExtDetailController.java
  4. 68 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmExtLogController.java
  5. 45 0
      fs-admin/src/main/java/com/fs/crm/task/CrmTask.java
  6. 8 2
      fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java
  7. 66 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsOrderRefundReasonController.java
  8. 82 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsRefundReasonController.java
  9. 52 52
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  10. 15 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  11. 10 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java
  12. 73 44
      fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java
  13. 12 2
      fs-admin/src/main/java/com/fs/live/controller/LiveOrderController.java
  14. 519 0
      fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java
  15. 40 0
      fs-company-app/src/main/java/com/fs/app/controller/app/ImController.java
  16. 128 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserShowController.java
  17. 5 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  18. 24 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerVisitController.java
  19. 14 4
      fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java
  20. 4 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  21. 8 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  22. 16 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
  23. 39 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerAnalyzeController.java
  24. 45 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerPropertyController.java
  25. 25 56
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  26. 198 0
      fs-company/src/main/java/com/fs/crm/CrmBusinessController.java
  27. 100 0
      fs-company/src/main/java/com/fs/crm/CrmExtDetailController.java
  28. 68 0
      fs-company/src/main/java/com/fs/crm/CrmExtLogController.java
  29. 83 0
      fs-company/src/main/java/com/fs/crm/CrmFollowUpController.java
  30. 591 0
      fs-company/src/main/java/com/fs/crm/CrmSjCustomerController.java
  31. 1 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  32. 10 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  33. 118 13
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  34. 7 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  35. 10 0
      fs-qw-api-msg/src/main/resources/application.yml
  36. 20 2
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  37. 110 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUserShow.java
  38. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecLogMapper.java
  39. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  40. 69 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserShowMapper.java
  41. 25 0
      fs-service/src/main/java/com/fs/company/param/CompanyUserShowEditParam.java
  42. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java
  43. 66 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserShowService.java
  44. 6 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  45. 4 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  46. 113 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserShowServiceImpl.java
  47. 22 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  48. 6 2
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  49. 3 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  50. 16 0
      fs-service/src/main/java/com/fs/company/vo/CallContentVO.java
  51. 5 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  52. 2 0
      fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java
  53. 7 0
      fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java
  54. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java
  55. 63 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseReward.java
  56. 79 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardRound.java
  57. 47 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardVideoRelation.java
  58. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriodDays.java
  59. 75 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardRoundMapper.java
  60. 87 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardVideoRelationMapper.java
  61. 10 3
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  62. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  63. 1 0
      fs-service/src/main/java/com/fs/course/param/FsCourseSendRewardUParam.java
  64. 5 6
      fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoRemainTimeParam.java
  65. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  66. 114 26
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  67. 12 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  68. 622 4
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  69. 37 0
      fs-service/src/main/java/com/fs/course/utils/luckyDraw/LotteryUtil.java
  70. 77 0
      fs-service/src/main/java/com/fs/course/utils/luckyDraw/Prize.java
  71. 9 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoH5DVO.java
  72. 5 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoH5VO.java
  73. 339 0
      fs-service/src/main/java/com/fs/crm/domain/CrmBusiness.java
  74. 20 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  75. 8 110
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerContacts.java
  76. 14 125
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerVisit.java
  77. 65 0
      fs-service/src/main/java/com/fs/crm/domain/CrmExtDetail.java
  78. 136 0
      fs-service/src/main/java/com/fs/crm/domain/CrmExtLog.java
  79. 95 0
      fs-service/src/main/java/com/fs/crm/domain/CrmFollowUp.java
  80. 6 1
      fs-service/src/main/java/com/fs/crm/enums/CustomerLogEnum.java
  81. 73 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmBusinessMapper.java
  82. 5 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java
  83. 2 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerContactsMapper.java
  84. 33 4
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  85. 13 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerVisitMapper.java
  86. 88 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmExtDetailMapper.java
  87. 62 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmExtLogMapper.java
  88. 65 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmFollowUpMapper.java
  89. 96 0
      fs-service/src/main/java/com/fs/crm/param/CrmBusinessAddAndUpdateParam.java
  90. 118 0
      fs-service/src/main/java/com/fs/crm/param/CrmBusinessImportParam.java
  91. 109 0
      fs-service/src/main/java/com/fs/crm/param/CrmBusinessQueryParam.java
  92. 2 0
      fs-service/src/main/java/com/fs/crm/param/CrmCompanyLineCustomerImportParam.java
  93. 16 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerBatchReceiveParam.java
  94. 14 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerBatchRecoverParam.java
  95. 4 1
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java
  96. 19 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerRecoverParam.java
  97. 15 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerUpdateOrAddParam.java
  98. 93 0
      fs-service/src/main/java/com/fs/crm/param/CrmExtDetailAddOrUpdateParam.java
  99. 18 0
      fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java
  100. 12 0
      fs-service/src/main/java/com/fs/crm/param/CrmMyCustomerListQueryParam.java

+ 129 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmBusinessController.java

@@ -0,0 +1,129 @@
+package com.fs.crm.controller;
+
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.param.CrmBusinessAddAndUpdateParam;
+import com.fs.crm.param.CrmBusinessQueryParam;
+import com.fs.crm.service.ICrmBusinessService;
+import com.fs.crm.vo.CrmBusinessListVO;
+import com.hc.openapi.tool.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.fs.framework.web.service.TokenService;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 商机Controller
+ *
+ * @author fs
+ * @date 2025-01-16
+ */
+@RestController
+@RequestMapping("/crm/business")
+public class CrmBusinessController extends BaseController
+{
+    @Autowired
+    private ICrmBusinessService crmBusinessService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询商机列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmBusinessQueryParam param)
+    {
+        startPage();
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCreateTimeArr(param.getCreateTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getNextTimeRange())){
+            param.setNextTimeArr(param.getNextTimeRange().split("--"));
+        }
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+
+        return getDataTable(list);
+    }
+//    /**
+//     * 查询商机列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('crm:business:list')")
+//    @GetMapping("/list")
+//    public TableDataInfo list(CrmBusiness crmBusiness)
+//    {
+//        startPage();
+//        List<CrmBusiness> list = crmBusinessService.selectCrmBusinessList(crmBusiness);
+//        return getDataTable(list);
+//    }
+
+    /**
+     * 导出商机列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:export')")
+    @Log(title = "商机", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmBusinessQueryParam param)
+    {
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+        ExcelUtil<CrmBusinessListVO> util = new ExcelUtil<CrmBusinessListVO>(CrmBusinessListVO.class);
+        return util.exportExcel(list, "business");
+    }
+
+    /**
+     * 获取商机详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:query')")
+    @GetMapping(value = "/{businessId}")
+    public AjaxResult getInfo(@PathVariable("businessId") Long businessId)
+    {
+        return AjaxResult.success(crmBusinessService.selectCrmBusinessById(businessId));
+    }
+
+    /**
+     * 新增商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:add')")
+    @Log(title = "商机", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmBusinessAddAndUpdateParam param, HttpServletRequest request)
+    {
+        //创建人
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCreateBy(loginUser.getUsername());
+        return toAjax(crmBusinessService.insertCrmBusiness(param));
+    }
+
+    /**
+     * 修改商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:edit')")
+    @Log(title = "商机", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmBusinessAddAndUpdateParam crmBusiness)
+    {
+        return toAjax(crmBusinessService.updateCrmBusiness(crmBusiness));
+    }
+
+    /**
+     * 删除商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:remove')")
+    @Log(title = "商机", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{businessIds}")
+    public AjaxResult remove(@PathVariable Long[] businessIds)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmBusinessService.deleteCrmBusinessByIds(businessIds,loginUser.getUser().getNickName()));
+    }
+}

+ 13 - 7
fs-admin/src/main/java/com/fs/crm/controller/CrmEventController.java

@@ -1,18 +1,24 @@
 package com.fs.crm.controller;
 
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.crm.domain.CrmEvent;
 import com.fs.crm.service.ICrmEventService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
 
 /**
  * 代办事项Controller

+ 100 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmExtDetailController.java

@@ -0,0 +1,100 @@
+package com.fs.crm.controller;
+
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.param.CrmExtDetailAddOrUpdateParam;
+import com.fs.crm.service.ICrmExtDetailService;
+import com.fs.crm.vo.CrmExtDetailVo;
+import com.fs.watch.param.BaseQueryParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.fs.framework.web.service.TokenService;
+
+import java.util.List;
+
+/**
+ * 字段扩展详情Controller
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+@RestController
+@RequestMapping("/crm/detail")
+public class CrmExtDetailController extends BaseController
+{
+    @Autowired
+    private ICrmExtDetailService crmExtDetailService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询字段扩展列
+     */
+//    @PreAuthorize("@ss.hasPermi('crm:detail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(BaseQueryParam param)
+    {
+        startPage();
+        List<CrmExtDetailVo> list = crmExtDetailService.getTableColumnsMetadata(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出字段扩展详情列表
+     */
+//    @PreAuthorize("@ss.hasPermi('crm:detail:export')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(BaseQueryParam param)
+    {
+        List<CrmExtDetailVo> list = crmExtDetailService.getTableColumnsMetadata(param);
+        ExcelUtil<CrmExtDetailVo> util = new ExcelUtil<CrmExtDetailVo>(CrmExtDetailVo.class);
+        return util.exportExcel(list, "detail");
+    }
+
+
+
+    /**
+     * 新增字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:add')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R add(@RequestBody CrmExtDetailAddOrUpdateParam param)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.insertColumn(param,nickName);
+    }
+
+    /**
+     * 修改字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:edit')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public R edit(@RequestBody CrmExtDetailAddOrUpdateParam param)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.updateColumn(param,nickName);
+    }
+
+    /**
+     * 删除字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:remove')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{columnNames}")
+    public R remove(@PathVariable String[] columnNames)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.deleteColumns(columnNames,nickName);
+    }
+}

+ 68 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmExtLogController.java

@@ -0,0 +1,68 @@
+package com.fs.crm.controller;
+
+import java.util.List;
+
+import com.fs.crm.domain.CrmExtLog;
+import com.fs.crm.service.ICrmExtLogService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 修改字段扩展日志Controller
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+@RestController
+@RequestMapping("/crm/log")
+public class CrmExtLogController extends BaseController
+{
+    @Autowired
+    private ICrmExtLogService crmExtLogService;
+
+    /**
+     * 查询修改字段扩展日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmExtLog crmExtLog)
+    {
+        startPage();
+        List<CrmExtLog> list = crmExtLogService.selectCrmExtLogList(crmExtLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出修改字段扩展日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:export')")
+    @Log(title = "修改字段扩展日志", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmExtLog crmExtLog)
+    {
+        List<CrmExtLog> list = crmExtLogService.selectCrmExtLogList(crmExtLog);
+        ExcelUtil<CrmExtLog> util = new ExcelUtil<CrmExtLog>(CrmExtLog.class);
+        return util.exportExcel(list, "log");
+    }
+
+    /**
+     * 获取修改字段扩展日志详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(crmExtLogService.selectCrmExtLogById(logId));
+    }
+
+}

+ 45 - 0
fs-admin/src/main/java/com/fs/crm/task/CrmTask.java

@@ -0,0 +1,45 @@
+package com.fs.crm.task;
+
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.crm.service.ICrmCustomerVisitService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component("crmTaskSj")
+public class CrmTask {
+
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+    @Autowired
+    private ICrmCustomerVisitService customerVisitService;
+
+    /**
+     * 回收线索
+     * 生产环境 凌晨12点执行
+     * 测试环境 每10秒执行一次 0/10 * * * * ?
+     */
+    public void recoveryClue() {
+        crmCustomerService.recoveryClue();
+    }
+
+    /**
+     * 跟进提醒
+     * 生产环境 每天凌晨查询当天需要跟进的记录
+     * 测试环境 每10秒执行一次 0/10 * * * * ?
+     */
+    public void followupNotice() {
+        customerVisitService.followupNotice();
+    }
+
+    /**
+     * 已存在商机未跟进回收公海
+     * 生产环境 每天凌晨未跟进商机客户
+     * 测试环境 每10秒执行一次 0/10 * * * * ?
+     */
+    public void recoveryBusiness() {
+        customerVisitService.recoveryBusiness();
+    }
+
+
+
+}

+ 8 - 2
fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java

@@ -46,8 +46,14 @@ public class EasyCallController extends BaseController {
     @ApiOperation("获取网关列表")
     @GetMapping("/gateway/list")
     public R getGatewayList() {
-        List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
-        return R.ok().put("data", list);
+        try {
+            List<EasyCallGatewayVO> list = easyCallService.getGatewayList(null);
+            return R.ok().put("data", list);
+        }
+        catch (Exception e){
+            log.error("获取网关列表失败", e);
+            return R.ok().put("msg", e.getMessage());
+        }
     }
 
     /**

+ 66 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsOrderRefundReasonController.java

@@ -0,0 +1,66 @@
+package com.fs.hisStore.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsOrderRefundReason;
+import com.fs.hisStore.service.IFsOrderRefundReasonService;
+
+@RestController
+@RequestMapping("/store/store/orderRefundReason")
+public class FsOrderRefundReasonController extends BaseController {
+
+    @Autowired
+    private IFsOrderRefundReasonService fsOrderRefundReasonService;
+
+    @PreAuthorize("@ss.hasPermi('store:orderRefundReason:list')")
+    @GetMapping("/list")
+    public AjaxResult list(FsOrderRefundReason fsOrderRefundReason) {
+        List<FsOrderRefundReason> list = fsOrderRefundReasonService.selectFsOrderRefundReasonList(fsOrderRefundReason);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:orderRefundReason:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(fsOrderRefundReasonService.selectFsOrderRefundReasonById(id));
+    }
+
+    @GetMapping(value = "/getByOrderCode")
+    public AjaxResult getByOrderCode(String orderCode) {
+        return AjaxResult.success(fsOrderRefundReasonService.selectByOrderCode(orderCode));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:orderRefundReason:add')")
+    @Log(title = "订单退款原因关联", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsOrderRefundReason fsOrderRefundReason) {
+        return toAjax(fsOrderRefundReasonService.insertFsOrderRefundReason(fsOrderRefundReason));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:orderRefundReason:edit')")
+    @Log(title = "订单退款原因关联", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsOrderRefundReason fsOrderRefundReason) {
+        return toAjax(fsOrderRefundReasonService.updateFsOrderRefundReason(fsOrderRefundReason));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:orderRefundReason:remove')")
+    @Log(title = "订单退款原因关联", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(fsOrderRefundReasonService.deleteFsOrderRefundReasonByIds(ids));
+    }
+}

+ 82 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsRefundReasonController.java

@@ -0,0 +1,82 @@
+package com.fs.hisStore.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsRefundReason;
+import com.fs.hisStore.service.IFsRefundReasonService;
+import com.fs.hisStore.vo.FsRefundReasonTreeVO;
+
+@RestController
+@RequestMapping("/store/store/refundReason")
+public class FsRefundReasonController extends BaseController {
+
+    @Autowired
+    private IFsRefundReasonService fsRefundReasonService;
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsRefundReason fsRefundReason) {
+        startPage();
+        List<FsRefundReason> list = fsRefundReasonService.selectFsRefundReasonList(fsRefundReason);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:list')")
+    @GetMapping("/children/{parentId}")
+    public AjaxResult getChildren(@PathVariable("parentId") Long parentId) {
+        List<FsRefundReason> list = fsRefundReasonService.selectChildrenByParentId(parentId);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:list')")
+    @GetMapping("/tree")
+    public AjaxResult tree() {
+        List<FsRefundReasonTreeVO> tree = fsRefundReasonService.selectFsRefundReasonTree();
+        return AjaxResult.success(tree);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(fsRefundReasonService.selectFsRefundReasonById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:add')")
+    @Log(title = "退款原因管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsRefundReason fsRefundReason) {
+        int result = fsRefundReasonService.insertFsRefundReason(fsRefundReason);
+        if (result > 0) {
+            return AjaxResult.success(fsRefundReason.getId());
+        }
+        return AjaxResult.error();
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:edit')")
+    @Log(title = "退款原因管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsRefundReason fsRefundReason) {
+        return toAjax(fsRefundReasonService.updateFsRefundReason(fsRefundReason));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:refundReason:remove')")
+    @Log(title = "退款原因管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        return toAjax(fsRefundReasonService.deleteRefundReasonWithChildren(id));
+    }
+}

+ 52 - 52
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -16,10 +16,7 @@ import com.fs.config.cloud.CloudHostProper;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
-import com.fs.hisStore.domain.FsStoreAfterSalesItemScrm;
-import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
-import com.fs.hisStore.domain.FsStoreAfterSalesStatusScrm;
-import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.domain.*;
 import com.fs.hisStore.param.FsStoreAfterSalesAudit1Param;
 import com.fs.hisStore.param.FsStoreAfterSalesAudit2Param;
 import com.fs.hisStore.param.FsStoreAfterSalesCancelParam;
@@ -88,6 +85,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
                 vo.setUserPhone(vo.getUserPhone());
             }
         }
+
         return getDataTable(list);
     }
 
@@ -109,58 +107,60 @@ public class FsStoreAfterSalesScrmController extends BaseController
         }
 
         List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVOExport(fsStoreAfterSales);
-        if("北京卓美".equals(signProjectName)){
-            List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
-                    .map(vo -> {
-                        FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
-                        try {
-                            zmvo.setPayCode(vo.getPayCode());
-                            zmvo.setOrderCode(vo.getOrderCode());
-                            zmvo.setStatus(vo.getOrderStatus().toString());
-                            zmvo.setUserId(vo.getUserId());
-                            zmvo.setProductName(vo.getProductName());
-                            zmvo.setBarCode(vo.getProductBarCode());
-                            zmvo.setSku(vo.getSku());
-                            zmvo.setNum(vo.getNum());
-                            zmvo.setPrice(vo.getPrice());
-                            zmvo.setCost(vo.getCost());
-//                            zmvo.setFPrice("");
-                            zmvo.setPayMoney(vo.getPayMoney());
-                            zmvo.setPayPostage(vo.getTotalPostage());
-                            zmvo.setCateName(vo.getCateName());
-                            zmvo.setRealName(vo.getUserName());
-                            zmvo.setUserPhone(vo.getUserPhone());
-                            zmvo.setUserAddress(vo.getUserAddress());
-                            zmvo.setCreateTime(vo.getOrderCreateTime());
-                            zmvo.setPayTime(vo.getOrderPayTime());
-                            zmvo.setDeliverySn(vo.getOrderDeliverySn());
-                            zmvo.setDeliveryName(vo.getOrderDeliveryName());
-                            zmvo.setDeliveryId(vo.getOrderDeliveryId());
-                            zmvo.setCompanyName(vo.getCompanyName());
-                            zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
-                            zmvo.setRefundTime(vo.getCreateTime());
-//                            zmvo.setAfterSalesNumber
-                            zmvo.setRefundMoney(vo.getRefundAmount());
-                            zmvo.setBankTransactionId(vo.getBankTransactionId());
-                            zmvo.setReasons(vo.getReasons());
-                            zmvo.setExplains(vo.getExplains());
-
-                        } catch (Exception e) {
-                            // 处理异常
-                            e.printStackTrace();
-                        }
-                        return zmvo;
-                    })
-                    .collect(Collectors.toList());
-            for (FsStoreOrderItemExportRefundZMVO vo : zmvoList){
-                vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+        if("北京卓美".equals(signProjectName)) {
+            if (!list.isEmpty()) {
+                List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
+                        .map(vo -> {
+                            FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
+                            try {
+                                zmvo.setPayCode(vo.getPayCode());
+                                zmvo.setOrderCode(vo.getOrderCode());
+                                zmvo.setStatus(vo.getOrderStatus().toString());
+                                zmvo.setUserId(vo.getUserId());
+                                zmvo.setProductName(vo.getProductName());
+                                zmvo.setBarCode(vo.getProductBarCode());
+                                zmvo.setSku(vo.getSku());
+                                zmvo.setNum(vo.getNum());
+                                zmvo.setPrice(vo.getPrice());
+                                zmvo.setCost(vo.getCost());
+                                zmvo.setPayMoney(vo.getPayMoney());
+                                zmvo.setPayPostage(vo.getTotalPostage());
+                                zmvo.setCateName(vo.getCateName());
+                                zmvo.setRealName(vo.getUserName());
+                                zmvo.setUserPhone(vo.getUserPhone());
+                                zmvo.setUserAddress(vo.getUserAddress());
+                                zmvo.setCreateTime(vo.getOrderCreateTime());
+                                zmvo.setPayTime(vo.getOrderPayTime());
+                                zmvo.setDeliverySn(vo.getOrderDeliverySn());
+                                zmvo.setDeliveryName(vo.getOrderDeliveryName());
+                                zmvo.setDeliveryId(vo.getOrderDeliveryId());
+                                zmvo.setCompanyName(vo.getCompanyName());
+                                zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
+                                zmvo.setRefundTime(vo.getCreateTime());
+                                zmvo.setRefundMoney(vo.getRefundAmount());
+                                zmvo.setBankTransactionId(vo.getBankTransactionId());
+                                zmvo.setReasons(vo.getReasons());
+                                zmvo.setExplains(vo.getExplains());
+                                zmvo.setReasonValue1(vo.getReasonValue1());
+                                zmvo.setReasonValue2(vo.getReasonValue2());
+                                zmvo.setAuditRemark(vo.getAuditRemark());
+                            } catch (Exception e) {
+                                e.printStackTrace();
+                            }
+                            return zmvo;
+                        })
+                        .collect(Collectors.toList());
+                for (FsStoreOrderItemExportRefundZMVO vo : zmvoList) {
+                    vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+                }
+                ExcelUtil<FsStoreOrderItemExportRefundZMVO> util = new ExcelUtil<FsStoreOrderItemExportRefundZMVO>(FsStoreOrderItemExportRefundZMVO.class);
+                return util.exportExcel(zmvoList, "退款订单导出");
             }
-            ExcelUtil<FsStoreOrderItemExportRefundZMVO> util = new ExcelUtil<FsStoreOrderItemExportRefundZMVO>(FsStoreOrderItemExportRefundZMVO.class);
-            return util.exportExcel(zmvoList, "退款订单导出");
         }
         for (FsStoreAfterSalesVO vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
         }
+
         ExcelUtil<FsStoreAfterSalesVO> util = new ExcelUtil<FsStoreAfterSalesVO>(FsStoreAfterSalesVO.class);
         return util.exportExcel(list, "退款订单导出");
     }
@@ -171,7 +171,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
-        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesById(id);
+        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesByIdForDetail(id);
         FsStoreAfterSalesItemScrm map=new FsStoreAfterSalesItemScrm();
         map.setStoreAfterSalesId(id);
         List<FsStoreAfterSalesItemScrm> items=fsStoreAfterSalesItemService.selectFsStoreAfterSalesItemList(map);

+ 15 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -59,6 +59,7 @@ import com.fs.hisStore.domain.FsStoreOrderItemScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStoreOrderStatusScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.hisStore.dto.FsStoreOrderPayDeliveryDTO;
 import com.fs.hisStore.dto.StoreOrderExpressExportDTO;
@@ -173,6 +174,9 @@ public class FsStoreOrderScrmController extends BaseController {
     @Autowired
     private ICompanyService companyService;
 
+    @Autowired
+    private IFsStoreAfterSalesScrmService fsStoreAfterSalesService;
+
     private IErpOrderService getErpService(){
         //判断是否开启erp
         IErpOrderService erpOrderService = null;
@@ -716,8 +720,14 @@ public class FsStoreOrderScrmController extends BaseController {
         }
 
         List<FsStoreOrderAuditLogVO> auditLogs = orderAuditLogService.selectStoreOrderAuditLogVOByOrderId(order.getId());
+
+        FsStoreAfterSalesScrm afterSales = null;
+        if (order.getStatus() != null && (order.getStatus() == -1 || order.getStatus() == -2)) {
+            afterSales = fsStoreAfterSalesService.selectFsStoreAfterSalesByOrderCode(order.getOrderCode());
+        }
+
         return R.ok().put("order", order).put("items", items).put("logs", logs).put("user", user).put("payments",payments).put("tuiMoneyLogs",tuiMoneyLogs)
-                .put("auditLogs",auditLogs);
+                .put("auditLogs",auditLogs).put("afterSales", afterSales);
     }
 
     @GetMapping(value = "/queryAddress/{id}")
@@ -975,6 +985,10 @@ public class FsStoreOrderScrmController extends BaseController {
         if("广州郑多燕".equals(cloudHostProper.getCompanyName())){
             return getDataTable(fsStoreOrderService.selectZDYOrderSaleStatisticsList(param));
         }
+        // 恒春来
+        if("恒春来".equals(cloudHostProper.getCompanyName())){
+            return getDataTable(fsStoreOrderService.selectHCLOrderDimensionStatisticsList(param));
+        }
         return getDataTable(fsStoreOrderService.selectOrderDimensionStatisticsList(param));
     }
 

+ 10 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.dto.FsStoreProductSortItemDTO;
 import com.fs.hisStore.service.IFsStoreUserEndCategoryScrmService;
 import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -55,6 +56,15 @@ public class FsStoreUserEndCategoryScrmController extends BaseController {
         return getDataTable(list);
     }
 
+    /** 保存关联商品在当前页的排序(写入 fs_store_product_user_end_category.sort) */
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:edit')")
+    @Log(title = "用户分端类关联商品排序", businessType = BusinessType.UPDATE)
+    @PutMapping("/products/sort")
+    public AjaxResult saveCategoryProductsSort(@RequestParam Long id,
+                                               @RequestBody List<FsStoreProductSortItemDTO> items) {
+        return toAjax(userEndCategoryService.saveProductsSort(id, items));
+    }
+
     @PreAuthorize("@ss.hasPermi('store:userEndCategory:query')")
     @GetMapping("/{id}")
     public AjaxResult getInfo(@PathVariable Long id) {

+ 73 - 44
fs-admin/src/main/java/com/fs/live/controller/LiveAfterSalesController.java

@@ -16,6 +16,8 @@ import com.fs.his.domain.FsStoreAfterSalesLogs;
 import com.fs.his.domain.FsUser;
 import com.fs.his.enums.FsStoreAfterSalesStatusEnum;
 import com.fs.his.service.IFsUserService;
+import com.fs.hisStore.domain.FsRefundReason;
+import com.fs.hisStore.mapper.FsRefundReasonMapper;
 import com.fs.hisStore.vo.FsStoreOrderItemExportRefundZMVO;
 import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveAfterSalesItem;
@@ -38,7 +40,10 @@ import org.springframework.web.bind.annotation.*;
 
 import java.text.ParseException;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * 售后记录Controller
@@ -66,6 +71,8 @@ public class LiveAfterSalesController extends BaseController
     @Value("${cloud_host.company_name}")
     private String signProjectName;
 
+    @Autowired
+    private FsRefundReasonMapper fsRefundReasonMapper;
 
     /**
      * 获取售后记录详细信息
@@ -117,53 +124,75 @@ public class LiveAfterSalesController extends BaseController
         PageHelper.clearPage();
         PageHelper.startPage(1, 10000, "");
         List<LiveAfterSalesVo> list = liveAfterSalesService.selectLiveAfterSalesVoListExport(liveAfterSales);
-        if("北京卓美".equals(signProjectName)){
-            List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
-                    .map(vo -> {
-                        FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
-                        try {
-                            zmvo.setPayCode(vo.getPayCode());
-                            zmvo.setOrderCode(vo.getOrderCode());
-                            zmvo.setStatus(vo.getOrderStatus().toString());
-                            zmvo.setUserId(vo.getUserId());
-                            zmvo.setProductName(vo.getProductName());
-                            zmvo.setBarCode(vo.getProductBarCode());
-                            zmvo.setSku(vo.getSku());
-                            zmvo.setNum(vo.getNum());
-                            zmvo.setPrice(vo.getPrice());
-                            zmvo.setCost(vo.getCost());
+        if("北京卓美".equals(signProjectName)) {
+            Map<Long, String> map = null;
+            if (!list.isEmpty()) {//不为空获取审核信息
+                List<Long> reasonId = list.stream()
+                        .flatMap(vo -> Stream.of(vo.getReasonId1(), vo.getReasonId2()))
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList());
+                List<FsRefundReason> refundReasons = fsRefundReasonMapper.getFsRefundReasonList(reasonId);
+                if (!refundReasons.isEmpty()) {
+                    map = refundReasons.stream().collect(Collectors.toMap(FsRefundReason::getId, FsRefundReason::getReasonName));
+                }
+
+                Map<Long, String> finalMap = map;
+                List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
+                        .map(vo -> {
+                            FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
+                            try {
+                                zmvo.setPayCode(vo.getPayCode());
+                                zmvo.setOrderCode(vo.getOrderCode());
+                                zmvo.setStatus(vo.getOrderStatus().toString());
+                                zmvo.setUserId(vo.getUserId());
+                                zmvo.setProductName(vo.getProductName());
+                                zmvo.setBarCode(vo.getProductBarCode());
+                                zmvo.setSku(vo.getSku());
+                                zmvo.setNum(vo.getNum());
+                                zmvo.setPrice(vo.getPrice());
+                                zmvo.setCost(vo.getCost());
 //                            zmvo.setFPrice("");
-                            zmvo.setPayMoney(vo.getPayMoney());
-                            zmvo.setPayPostage(vo.getTotalPostage());
-                            zmvo.setCateName(vo.getCateName());
-                            zmvo.setRealName(vo.getUserName());
-                            zmvo.setUserPhone(vo.getUserPhone());
-                            zmvo.setUserAddress(vo.getUserAddress());
-                            zmvo.setCreateTime(vo.getCreateTime());
-                            zmvo.setPayTime(vo.getOrderPayTime());
-                            zmvo.setDeliverySn(vo.getOrderDeliverySn());
-                            zmvo.setDeliveryName(vo.getOrderDeliveryName());
-                            zmvo.setDeliveryId(vo.getOrderDeliveryId());
-                            zmvo.setCompanyName(vo.getCompanyName());
-                            zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
-                            zmvo.setRefundTime(vo.getCreateTime());
+                                zmvo.setPayMoney(vo.getPayMoney());
+                                zmvo.setPayPostage(vo.getTotalPostage());
+                                zmvo.setCateName(vo.getCateName());
+                                zmvo.setRealName(vo.getUserName());
+                                zmvo.setUserPhone(vo.getUserPhone());
+                                zmvo.setUserAddress(vo.getUserAddress());
+                                zmvo.setCreateTime(vo.getCreateTime());
+                                zmvo.setPayTime(vo.getOrderPayTime());
+                                zmvo.setDeliverySn(vo.getOrderDeliverySn());
+                                zmvo.setDeliveryName(vo.getOrderDeliveryName());
+                                zmvo.setDeliveryId(vo.getOrderDeliveryId());
+                                zmvo.setCompanyName(vo.getCompanyName());
+                                zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
+                                zmvo.setRefundTime(vo.getCreateTime());
 //                            zmvo.setAfterSalesNumber
-                            zmvo.setRefundMoney(vo.getRefundAmount());
-                            zmvo.setBankTransactionId(vo.getBankTransactionId());
-                            zmvo.setReasons(vo.getReasons());
-                            zmvo.setExplains(vo.getExplains());
-                        } catch (Exception e) {
-                            // 处理异常
-                            e.printStackTrace();
-                        }
-                        return zmvo;
-                    })
-                    .collect(Collectors.toList());
-            for (FsStoreOrderItemExportRefundZMVO vo : zmvoList){
-                vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+                                zmvo.setRefundMoney(vo.getRefundAmount());
+                                zmvo.setBankTransactionId(vo.getBankTransactionId());
+                                zmvo.setReasons(vo.getReasons());
+                                zmvo.setExplains(vo.getExplains());
+                                if (vo.getReasonId1() != null) {
+                                    zmvo.setReasonValue1(finalMap.get(vo.getReasonId1()));
+                                }
+                                if (vo.getReasonId2() != null) {
+                                    zmvo.setReasonValue2(finalMap.get(vo.getReasonId2()));
+                                }
+                                if (vo.getAuditRemark() != null) {
+                                    zmvo.setAuditRemark(vo.getAuditRemark());
+                                }
+                            } catch (Exception e) {
+                                // 处理异常
+                                e.printStackTrace();
+                            }
+                            return zmvo;
+                        })
+                        .collect(Collectors.toList());
+                for (FsStoreOrderItemExportRefundZMVO vo : zmvoList) {
+                    vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+                }
+                ExcelUtil<FsStoreOrderItemExportRefundZMVO> util = new ExcelUtil<FsStoreOrderItemExportRefundZMVO>(FsStoreOrderItemExportRefundZMVO.class);
+                return util.exportExcel(zmvoList, "退款订单导出");
             }
-            ExcelUtil<FsStoreOrderItemExportRefundZMVO> util = new ExcelUtil<FsStoreOrderItemExportRefundZMVO>(FsStoreOrderItemExportRefundZMVO.class);
-            return util.exportExcel(zmvoList, "退款订单导出");
         }
         for (LiveAfterSalesVo liveAfterSalesVo : list) {
             liveAfterSalesVo.setUserPhone(liveAfterSalesVo.getUserPhone() == null ? "" : liveAfterSalesVo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));

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

@@ -98,6 +98,8 @@ public class LiveOrderController extends BaseController
     @Autowired
     private ILiveOrderPaymentService orderPaymentService;
     @Autowired
+    private ILiveAfterSalesService liveAfterSalesService;
+    @Autowired
     private TokenService tokenService;
     @Autowired
     private IFsExpressScrmService expressService;
@@ -375,7 +377,9 @@ public class LiveOrderController extends BaseController
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
         FsUser user=userService.selectFsUserById(Long.valueOf(order.getUserId()));
-        user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        if(user != null && user.getPhone() != null){
+            user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        }
         List<LiveOrderItem> items=orderItemService.selectCheckedByOrderId(id);
 //        FsStoreOrderStatus statusMap=new FsStoreOrderStatus();
 
@@ -386,7 +390,13 @@ public class LiveOrderController extends BaseController
         if(order.getCustomerId()!=null&&order.getCustomerId()>0){
             customer=crmCustomerService.selectCrmCustomerById(order.getCustomerId());
         }
-        return R.ok().put("order", order).put("items", items).put("logs",logs).put("user",user).put("customer",customer).put("payments",payments) ;
+
+        LiveAfterSales afterSales = null;
+        if (order.getStatus() != null && (order.getStatus() == -1 || order.getStatus() == -2)) {
+            afterSales = liveAfterSalesService.selectLiveAfterSalesByOrderId(id);
+        }
+
+        return R.ok().put("order", order).put("items", items).put("logs",logs).put("user",user).put("customer",customer).put("payments",payments).put("afterSales", afterSales) ;
     }
 
     @PreAuthorize("@ss.hasPermi('live:liveOrder:query')")

+ 519 - 0
fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java

@@ -0,0 +1,519 @@
+package com.fs.task;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.domain.QwExternalAiAnalyzeSession;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.mapper.QwExternalAiAnalyzeSessionMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
+import com.fs.qw.param.audit.QwAuditMessagebackupParam;
+import com.fs.qw.service.IQwCustomerPropertyService;
+import com.fs.qw.shardingConfig.QwMsgAuditMessageSharding;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Component("QwExternalAiAnalyzeTask")
+@RequiredArgsConstructor
+@Slf4j
+public class QwExternalAiAnalyzeTask {
+    private final QwMsgAuditMessageMapper qwMsgAuditMessageMapper;
+    private final QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+    private final QwExternalAiAnalyzeSessionMapper qwExternalAiAnalyzeSessionMapper;
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+    private final IQwCustomerPropertyService qwCustomerPropertyService;
+    private final SysConfigMapper sysConfigMapper;
+    private final static String CHAT_BACKUP_MSG_TYPE = "text";
+
+    //调用时间间隔min
+    @Value("${qw.external.ai.interval:5}")
+    private Integer interval;
+
+//    //表分片数量
+//    @Value("${qw.external.ai.devide:12}")
+//    private Integer divideNum;
+
+    // 自定义线程池
+    private final ExecutorService executorService = new ThreadPoolExecutor(
+            5,  // 核心线程数
+            10, // 最大线程数
+            60, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(200),
+            r -> {
+                Thread thread = new Thread(r);
+                thread.setName("qw-external-ai-processor-" + thread.getId());
+                return thread;
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
+    );
+    //根据会话存档频率拉取
+    public void processQwChatBackup(){
+        log.info("AI开始处理-会话存档");
+        LocalDateTime now = LocalDateTime.now();
+        QwMsgAuditMessage qwMsgAuditMessage = new QwMsgAuditMessage();
+        qwMsgAuditMessage.setMsgType(CHAT_BACKUP_MSG_TYPE);
+        qwMsgAuditMessage.setRoomId("");
+        Long timestamp = now.minusMinutes(interval).atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();//根据配置的分钟间隔获取时间戳
+        qwMsgAuditMessage.setAnalyzeStartTime(timestamp);
+        List<QwMsgAuditMessage> qwMsgAuditMessages = new ArrayList<>();
+        for (int shard = 0; shard < QwMsgAuditMessageSharding.SHARD_COUNT; shard++) {
+            List<QwMsgAuditMessage> shardMessages =
+                    qwMsgAuditMessageMapper.selectQwMsgAuditMessageListByShard(shard, qwMsgAuditMessage);
+            if (shardMessages != null && !shardMessages.isEmpty()) {
+                qwMsgAuditMessages.addAll(shardMessages);
+            }
+        }
+//                .selectList(
+//                new LambdaQueryWrapper<QwMsgAuditMessage>()
+//                        .gt(QwMsgAuditMessage::getMsgTime, now.minusMinutes(interval))
+//                        .eq(QwMsgAuditMessage::getMsgType, CHAT_BACKUP_MSG_TYPE)
+//                        .eq(QwMsgAuditMessage::getRoomId, "").or().isNull(QwMsgAuditMessage::getRoomId)
+//        );
+        if (qwMsgAuditMessages.isEmpty()) {
+            log.info("会话存档qw_msg_audit_message无新数据");
+            return;
+        }
+
+        // 1) 按 msgTime 升序,确保输出组内顺序正确
+        qwMsgAuditMessages.sort(Comparator.comparing(
+                QwMsgAuditMessage::getMsgTime,
+                Comparator.nullsLast(Long::compareTo)
+        ));
+
+        // 2) 解析每条消息的 from_user / to_list(toList 是 JSON 字符串)
+//        List<ParsedMsg> parsedMsgs = new ArrayList<>(qwMsgAuditMessages.size());
+//        for (QwMsgAuditMessage msg : qwMsgAuditMessages) {
+//            Set<String> toUsers = parseToUserSet(msg.getToList());
+//            parsedMsgs.add(new ParsedMsg(msg, msg.getFromUser(), toUsers));
+//        }
+//
+//        // 3) 按会话参与双方分组:
+//        //    只要 A.from_user 出现在 B.to_list,或 B.from_user 出现在 A.to_list,就视为同一会话链,归为同一组;
+//        //    使用并查集把所有满足条件的消息聚成连通分量。
+//        UnionFind uf = new UnionFind(parsedMsgs.size());
+//
+//        // 反向索引:fromUser -> 消息下标列表
+//        Map<String, List<Integer>> fromUserIndex = new HashMap<>();
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            ParsedMsg pm = parsedMsgs.get(i);
+//            if (pm.fromUser == null) {
+//                continue;
+//            }
+//            fromUserIndex.computeIfAbsent(pm.fromUser, k -> new ArrayList<>()).add(i);
+//        }
+//
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            ParsedMsg a = parsedMsgs.get(i);
+//            if (a.fromUser == null || a.toUsers == null || a.toUsers.isEmpty()) {
+//                continue;
+//            }
+//            for (String toUser : a.toUsers) {
+//                List<Integer> candidates = fromUserIndex.get(toUser);
+//                if (candidates == null || candidates.isEmpty()) {
+//                    continue;
+//                }
+//                for (Integer j : candidates) {
+//                    ParsedMsg b = parsedMsgs.get(j);
+//                    if (b == null || b.fromUser == null) {
+//                        continue;
+//                    }
+//                    // 条件1:A.from 在 B.to_list 中
+//                    boolean aInB = b.toUsers != null && b.toUsers.contains(a.fromUser);
+//                    // 条件2:B.from 在 A.to_list 中
+//                    boolean bInA = a.toUsers.contains(b.fromUser);
+//
+//                    if (aInB || bInA) {
+//                        uf.union(i, j);
+//                    }
+//                }
+//            }
+//        }
+
+        // 4) 转成分组结构并按 msgTime 排序
+//        Map<Integer, List<ParsedMsg>> groupedMap = new HashMap<>();
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            groupedMap.computeIfAbsent(uf.find(i), k -> new ArrayList<>()).add(parsedMsgs.get(i));
+//        }
+        Map<String, List<QwMsgAuditMessage>> collect = qwMsgAuditMessages.stream().collect(Collectors.groupingBy(QwMsgAuditMessage::getConversationKey));
+        List<List<QwMsgAuditMessage>> groupedParsed = new ArrayList<>(
+                collect.values()
+//                groupedMap.values()
+        );
+
+//        for (List<ParsedMsg> group : groupedParsed) {
+//            group.sort(Comparator.comparing(ParsedMsg::getMsgTime, Comparator.nullsLast(Long::compareTo)));
+//        }
+
+        log.info("会话存档分组完成: 分组数={}", groupedParsed.size());
+
+        // 5) 生成入参:每组拼 history + 统一更新 param 外部联系人信息
+        ArrayList<QwAuditMessagebackupParam> historys = new ArrayList<>(groupedParsed.size());
+        for (List<QwMsgAuditMessage> group : groupedParsed) {
+            if (group == null || group.isEmpty()) {
+                continue;
+            }
+
+            QwMsgAuditMessage first = group.get(0);
+            QwAuditMessagebackupParam param = new QwAuditMessagebackupParam();
+            param.setCorpId(first.getCorpId());
+
+            // 用“第一条消息”决定 user/external/qwUserId(与旧逻辑一致,但避免重复 parseToUserSet)
+            Integer role = first.getFromUserRole();
+            if (role != null && role == 2) {
+                param.setExternalUserId(first.getFromUser());
+                if (!first.getFromUser().isEmpty()) {
+                    Object o = JSONArray.parseArray(first.getToList()).get(0);
+                    param.setQwUserId(o.toString());
+                }
+            } else {
+                if (!first.getToList().isEmpty()) {
+                    Object o = JSONArray.parseArray(first.getToList()).get(0);
+                    param.setExternalUserId(o.toString());
+                }
+                param.setQwUserId(first.getFromUser());
+            }
+
+            ArrayList<Map<String, String>> maps = new ArrayList<>();
+//            StringBuilder historyArr = new StringBuilder("{");
+            for (QwMsgAuditMessage pm : group) {
+                String roleTag = (pm.getFromUserRole() != null && pm.getFromUserRole() == 2) ? "user" : "ai";
+                String text = pm.getTextContent();
+                if (text == null) {
+                    text = "";
+                }
+                Map<String, String> map = new HashMap<>();
+                map.put(roleTag, text);
+//                if (historyArr.length()>1)historyArr.append(",");
+//                historyArr.append("\"").append(roleTag).append("\":\"").append(text).append("\"");
+                maps.add(map);
+            }
+//            historyArr.append("}");
+            param.setHistory(JSONUtil.toJsonStr(maps));
+            historys.add(param);
+        }
+
+        log.info("会话存档处理完成: 分组数={}", historys.size());
+        //入库
+        List<QwExternalAiAnalyze> qwExternalAiAnalyzes = new ArrayList<>();
+        historys.forEach(o -> {
+            QwExternalAiAnalyze qwExternalAiAnalyze = new QwExternalAiAnalyze();
+            QwExternalAiAnalyzeSession session = new QwExternalAiAnalyzeSession();
+            session.setCorpId(o.getCorpId());
+            session.setQwUserId(o.getQwUserId());
+            session.setExternalUserId(o.getExternalUserId());
+            Long sessionId;
+            //获取唯一sessionId,调用ai时绑定为同一对话
+            try {
+                qwExternalAiAnalyzeSessionMapper.insert(session);
+                sessionId = session.getSessionId();
+            } catch (DuplicateKeyException e) {
+                QwExternalAiAnalyzeSession exist = qwExternalAiAnalyzeSessionMapper.selectByUniqueKey(
+                        o.getExternalUserId(), o.getCorpId(), o.getQwUserId());
+                if (exist == null || exist.getSessionId() == null) {
+                    throw e;
+                }
+                sessionId = exist.getSessionId();
+            }
+
+            qwExternalAiAnalyze.setAiChatRecord(o.getHistory());
+            qwExternalAiAnalyze.setCorpId(o.getCorpId());
+            qwExternalAiAnalyze.setQwUserId(o.getQwUserId());
+            qwExternalAiAnalyze.setExternalUserId(o.getExternalUserId());
+            qwExternalAiAnalyze.setSessionId(sessionId);
+            qwExternalAiAnalyze.setCreateTime(new Date());
+            qwExternalAiAnalyzes.add(qwExternalAiAnalyze);
+        });
+        int affected = qwExternalAiAnalyzeMapper.insertBatch(qwExternalAiAnalyzes);
+        if (qwExternalAiAnalyzes == null || qwExternalAiAnalyzes.isEmpty()) {
+            log.info("会话分析数据为空");
+            return;
+        }
+//        List<Long> insertedIds = qwExternalAiAnalyzes.stream()
+//                .map(QwExternalAiAnalyze::getId)
+//                .filter(id -> id != null)
+//                .collect(Collectors.toList());
+        log.info("会话分析批量入库完成: 影响行数={}", affected);
+        List<List<QwExternalAiAnalyze>> batches = new ArrayList<>();
+        for (int i = 0; i < qwExternalAiAnalyzes.size(); i += 5) {
+            batches.add(qwExternalAiAnalyzes.subList(i, Math.min(i + 5, qwExternalAiAnalyzes.size())));
+        }
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (List<QwExternalAiAnalyze> batch : batches) {
+            futures.add(CompletableFuture.runAsync(() -> processSingleCustomer(batch,successCount,failCount), executorService));
+        }
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    }
+
+    private void processSingleCustomer(List<QwExternalAiAnalyze> qwExternalAiAnalyzes, AtomicInteger successCount,
+                                       AtomicInteger failCount) {
+        String threadName = Thread.currentThread().getName();
+        long batchStartTime = System.currentTimeMillis();
+
+        try {
+            log.info("线程 {} 开始处理批次, 数据量: {}", threadName, qwExternalAiAnalyzes.size());
+
+            for (QwExternalAiAnalyze data : qwExternalAiAnalyzes) {
+                processSingleAiAnalyze(data, successCount, failCount);
+            }
+
+            long costTime = System.currentTimeMillis() - batchStartTime;
+            log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
+                    threadName, qwExternalAiAnalyzes.size(), costTime);
+        } catch (Exception e) {
+            failCount.addAndGet(qwExternalAiAnalyzes.size());
+            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, qwExternalAiAnalyzes.size(), e);
+            throw new RuntimeException("批次处理失败", e);
+        }
+    }
+
+    private void processSingleAiAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze, AtomicInteger successCount,
+                                       AtomicInteger failCount) {
+        log.info("开始处理单条会话分析: {}", qwExternalAiAnalyze.getId());
+        try {
+            //TODO 调用AI分析 分析结果
+
+            String dataJson =
+//                    qwExternalAiAnalyze.getAiChatRecord();
+                    parseAiChat2String(qwExternalAiAnalyze.getAiChatRecord());
+            Long logId = qwExternalAiAnalyze.getSessionId();
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("aiTagTradeType.config");
+
+            long startTime = System.currentTimeMillis();
+
+            Executor asyncPool = ForkJoinPool.commonPool();
+            // 6 个 AI 接口并行;使用 commonPool,避免与批次线程池 executorService 嵌套导致死锁
+// 使用 supplyAsync 获取返回值,定义具体返回类型
+            CompletableFuture<String> portraitFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiGeneratedCustomerPortraitQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> summaryFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationSummaryQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> abstractFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationAbstractQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<Long> attritionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiAttritionLevelQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> focusFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCustomerFocusQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> intentionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiIntentionDegreeQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+            if (sysConfig != null){
+                JSONObject jsonObject = JSONObject.parseObject(sysConfig.getConfigValue());
+                CompletableFuture.runAsync(() ->
+                                qwCustomerPropertyService.analyzeAiTagByTrade(jsonObject.get("tradeType").toString(),qwExternalAiAnalyze)
+                        , asyncPool)
+                ;
+            }else {
+                log.error("ai标签-行业未配置");
+            }
+
+
+// 等待所有异步任务完成
+            CompletableFuture.allOf(portraitFuture, summaryFuture, abstractFuture,
+                    attritionFuture, focusFuture, intentionFuture).join();
+
+            qwExternalAiAnalyze.setCustomerPortraitJson(portraitFuture.get());
+            qwExternalAiAnalyze.setCommunicationSummary(summaryFuture.get());
+            qwExternalAiAnalyze.setCommunicationAbstract(abstractFuture.get());
+            qwExternalAiAnalyze.setAttritionLevel(attritionFuture.get());
+            qwExternalAiAnalyze.setCustomerFocusJson(focusFuture.get());
+            qwExternalAiAnalyze.setIntentionDegree(intentionFuture.get());
+            Integer i = crmCustomerAnalyzeService.updateQwAnalyzeByCustomerId(qwExternalAiAnalyze);
+            long costTime = System.currentTimeMillis() - startTime;
+            successCount.incrementAndGet();
+            log.info("客户 {} 的AI分析完成, 耗时: {}ms,更新{}条", qwExternalAiAnalyze.getExternalUserId(), costTime, i);
+
+
+        }
+         catch (Exception e) {
+            failCount.incrementAndGet();
+            log.error("单条会话分析失败: {}", qwExternalAiAnalyze.getId(), e);
+        }
+    }
+
+
+    private static String parseAiChat2String(String aiChatRecord){
+        JSONArray objects = JSONArray.parseArray(aiChatRecord);
+        StringBuilder result = new StringBuilder("{");
+        if (objects != null) {
+            objects.stream().iterator().forEachRemaining(item -> {
+                if (result.length() > 1)result.append(",");
+                result.append(item.toString());
+            });
+//            for (int i = 0; i < objects.size(); i++) {
+//                JSONObject item = objects.getJSONObject(i);
+//                if (item == null) {
+//                    continue;
+//                }
+//                String role = item.getString("role");
+//                String content = item.getString("content");
+//                String roleTag = "user".equals(role) ? "user" : "ai";
+//
+//                if (result.length() > 1) {
+//                    result.append(",");
+//                }
+//                result.append("\"").append(roleTag).append("\":\"")
+//                        .append(JSON.toJSONString(content == null ? "" : content)).append("\"");
+//            }
+        }
+        result.append("}");
+        return result.toString();
+    }
+
+    private static boolean isMutualContained(ParsedMsg a, ParsedMsg b) {
+        if (a == null || b == null) {
+            return false;
+        }
+        if (a.fromUser == null || b.fromUser == null) {
+            return false;
+        }
+        return a.toUsers.contains(b.fromUser) && b.toUsers.contains(a.fromUser);
+    }
+
+    private static Long groupMinTime(List<ParsedMsg> group) {
+        if (group == null || group.isEmpty()) {
+            return null;
+        }
+        Long min = null;
+        for (ParsedMsg pm : group) {
+            if (pm == null) {
+                continue;
+            }
+            Long t = pm.getMsgTime();
+            if (t == null) {
+                continue;
+            }
+            if (min == null || t < min) {
+                min = t;
+            }
+        }
+        return min;
+    }
+
+    private static class UnionFind {
+        private final int[] parent;
+        private final int[] rank;
+
+        private UnionFind(int n) {
+            this.parent = new int[n];
+            this.rank = new int[n];
+            for (int i = 0; i < n; i++) {
+                parent[i] = i;
+            }
+        }
+
+        private int find(int x) {
+            if (parent[x] != x) {
+                parent[x] = find(parent[x]);
+            }
+            return parent[x];
+        }
+
+        private void union(int a, int b) {
+            int ra = find(a);
+            int rb = find(b);
+            if (ra == rb) {
+                return;
+            }
+            if (rank[ra] < rank[rb]) {
+                parent[ra] = rb;
+            } else if (rank[ra] > rank[rb]) {
+                parent[rb] = ra;
+            } else {
+                parent[rb] = ra;
+                rank[ra]++;
+            }
+        }
+    }
+
+    private static Set<String> parseToUserSet(String toListJson) {
+        if (toListJson == null || toListJson.trim().isEmpty()) {
+            return Collections.emptySet();
+        }
+
+        // 优先按 JSON 数组解析:["1","2"]
+        try {
+            JSONArray arr = JSON.parseArray(toListJson);
+            if (arr != null) {
+                Set<String> set = new HashSet<>();
+                for (int i = 0; i < arr.size(); i++) {
+                    Object v = arr.get(i);
+                    if (v != null) {
+                        set.add(String.valueOf(v));
+                    }
+                }
+                return set;
+            }
+        } catch (Exception ignore) {
+            // fallthrough
+        }
+
+        // 兜底:toListJson 可能是单值或形如 ["1","2"] 的字符串(被转义等情况)
+        String s = toListJson.trim();
+        if (s.startsWith("[") && s.endsWith("]")) {
+            s = s.substring(1, s.length() - 1);
+        }
+        s = s.replace("\"", "");
+
+        Set<String> set = new HashSet<>();
+        if (!s.trim().isEmpty()) {
+            String[] parts = s.split(",");
+            for (String p : parts) {
+                if (p != null && !p.trim().isEmpty()) {
+                    set.add(p.trim());
+                }
+            }
+        }
+        return set;
+    }
+
+    private static class ParsedMsg {
+        private final QwMsgAuditMessage msg;
+        private final String fromUser;
+        private final Set<String> toUsers;
+
+        private ParsedMsg(QwMsgAuditMessage msg, String fromUser, Set<String> toUsers) {
+            this.msg = msg;
+            this.fromUser = fromUser;
+            this.toUsers = toUsers == null ? Collections.<String>emptySet() : toUsers;
+        }
+
+        private Long getMsgTime() {
+            return msg == null ? null : msg.getMsgTime();
+        }
+    }
+}

+ 40 - 0
fs-company-app/src/main/java/com/fs/app/controller/app/ImController.java

@@ -0,0 +1,40 @@
+package com.fs.app.controller.app;
+
+import com.fs.common.core.domain.R;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.service.OpenIMService;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+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;
+
+/**
+ * @description: im 相关接口
+ * @author: Xgb
+ * @createDate: 2026/4/3
+ * @version: 1.0
+ */
+@Api("im 相关接口")
+@RestController
+@RequestMapping(value = "/app/im")
+@Slf4j
+public class ImController {
+
+    @Autowired
+    private OpenIMService openIMService;
+    /**
+     * @Description: 查询用户信息
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2026/4/3 15:45
+     */
+    @GetMapping("/getUserInfo")
+    public R getUserInfo(String id) {
+        OpenImResponseDTO responseDTO = openIMService.getUserInfo(id);
+        return R.ok().put("data",responseDTO);
+    }
+
+}

+ 128 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserShowController.java

@@ -0,0 +1,128 @@
+package com.fs.company.controller.company;
+
+
+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.ServletUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.CompanyUserShow;
+import com.fs.company.param.CompanyUserShowEditParam;
+import com.fs.company.service.ICompanyUserShowService;
+import com.fs.framework.security.LoginUser;
+import com.hc.openapi.tool.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.fs.framework.service.TokenService;
+/**
+ * 用户展示字段Controller
+ *
+ * @author fs
+ * @date 2025-02-11
+ */
+@RestController
+@RequestMapping("/company/show")
+public class CompanyUserShowController extends BaseController
+{
+    @Autowired
+    private ICompanyUserShowService companyUserShowService;
+    @Autowired
+    private TokenService tokenService;
+
+//    /**
+//     * 查询用户展示字段列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:show:list')")
+//    @GetMapping("/list")
+//    public TableDataInfo list(CompanyUserShow companyUserShow)
+//    {
+//        startPage();
+//        List<CompanyUserShow> list = companyUserShowService.selectCompanyUserShowList(companyUserShow);
+//        return getDataTable(list);
+//    }
+//
+//    /**
+//     * 导出用户展示字段列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:show:export')")
+//    @Log(title = "用户展示字段", businessType = BusinessType.EXPORT)
+//    @GetMapping("/export")
+//    public AjaxResult export(CompanyUserShow companyUserShow)
+//    {
+//        List<CompanyUserShow> list = companyUserShowService.selectCompanyUserShowList(companyUserShow);
+//        ExcelUtil<CompanyUserShow> util = new ExcelUtil<CompanyUserShow>(CompanyUserShow.class);
+//        return util.exportExcel(list, "show");
+//    }
+
+//    /**
+//     * 获取用户展示字段详细信息
+//     */
+//    @PreAuthorize("@ss.hasPermi('company:show:query')")
+//    @GetMapping(value = "/{id}")
+//    public AjaxResult getInfo(@PathVariable("id") Long id)
+//    {
+//        return AjaxResult.success(companyUserShowService.selectCompanyUserShowById(id));
+//    }
+
+    /**
+     * 获取用户展示字段详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('company:show:query')")
+    @GetMapping(value = "/{type}")
+    public AjaxResult getInfo(@PathVariable("type") String type)
+    {
+        if (StringUtils.isBlank(type)) {
+            return AjaxResult.error("缺少必要参数");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(companyUserShowService.selectShowByCompanyIdUserId(
+                loginUser.getCompany().getCompanyId(),
+                loginUser.getUser().getUserId(),
+                type));
+    }
+    /**
+     * 新增用户展示字段
+     */
+//    @PreAuthorize("@ss.hasPermi('company:show:add')")
+    @Log(title = "用户展示字段", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyUserShow companyUserShow)
+    {
+        return toAjax(companyUserShowService.insertCompanyUserShow(companyUserShow));
+    }
+
+    /**
+     * 修改用户展示字段
+     */
+//    @PreAuthorize("@ss.hasPermi('company:show:edit')")
+    @Log(title = "用户展示字段", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyUserShowEditParam param)
+    {
+        if (StringUtils.isBlank(param.getType()) || param.getColumns().size()<1) {
+            return AjaxResult.error("缺少必要参数");
+        }
+        CompanyUser user = tokenService.getLoginUser(ServletUtils.getRequest()).getUser();
+        CompanyUserShow show = new CompanyUserShow();
+        show.setCompanyId(user.getCompanyId());
+        show.setUserId(user.getUserId());
+        show.setType(param.getType());
+        show.setColumns(param.getColumns().toString().replace("[", "").replace("]", "").replace(" ", ""));
+        show.setUpdateBy(user.getNickName());
+        return toAjax(companyUserShowService.updateByCompanyIdAndUserIdAndType(show));
+    }
+
+
+    /**
+     * 删除用户展示字段
+     */
+    @PreAuthorize("@ss.hasPermi('company:show:remove')")
+    @Log(title = "用户展示字段", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(companyUserShowService.deleteCompanyUserShowByIds(ids));
+    }
+}

+ 5 - 1
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -15,6 +15,7 @@ import com.fs.company.service.ICompanyUserService;
 import com.fs.company.util.OrderUtils;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.*;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerUserService;
 import com.fs.crm.vo.*;
@@ -51,6 +52,8 @@ public class CrmCustomerController extends BaseController
     private TokenService tokenService;
     @Autowired
     ICrmCustomerUserService crmCustomerUserService;
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
 
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
@@ -199,7 +202,7 @@ public class CrmCustomerController extends BaseController
                     if(vo.getMobile()!=null){
                         vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                     }
-
+                    vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
             }
             return getDataTable(list1);
@@ -210,6 +213,7 @@ public class CrmCustomerController extends BaseController
                     if (vo.getMobile() != null) {
                         vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                     }
+                    vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
                 }
             }

+ 24 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerVisitController.java

@@ -9,6 +9,7 @@ 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.crm.domain.CrmCustomerVisit;
 import com.fs.crm.param.CrmCustomerVisitAddParam;
 import com.fs.crm.param.CrmCustomerVisitListParam;
 import com.fs.crm.service.ICrmCustomerVisitService;
@@ -80,5 +81,28 @@ public class CrmCustomerVisitController extends BaseController
     }
 
 
+    /**
+     * 修改跟进
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customerVisit:edit')")
+    @Log(title = "跟进", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerVisit crmCustomerVisit)
+    {
+        return toAjax(crmCustomerVisitService.updateCrmCustomerVisit(crmCustomerVisit));
+    }
+
+    /**
+     * 删除跟进
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customerVisit:remove')")
+    @Log(title = "跟进", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{visitIds}")
+    public AjaxResult remove(@PathVariable Long[] visitIds)
+    {
+        return toAjax(crmCustomerVisitService.deleteCrmCustomerVisitByIds(visitIds));
+    }
+
+
 
 }

+ 14 - 4
fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java

@@ -13,6 +13,7 @@ import com.fs.crm.service.ICrmCustomerChatMessageService;
 import com.fs.crm.service.ICrmCustomerChatSessionService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.qw.param.audit.QwAiTagGainParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -21,7 +22,7 @@ import java.util.Map;
 
 /**
  * 聊天会话 Controller
- * 
+ *
  * @author ylrz
  * @date 2026-03-30
  */
@@ -91,11 +92,11 @@ public class CrmCustomerChatSessionController extends BaseController {
         try {
             Long sessionId = Long.valueOf(params.get("sessionId").toString());
             String title = (String) params.get("title");
-            
+
             if (title == null) {
                 return error("参数错误");
             }
-            
+
             int result = chatSessionService.updateChatSessionTitle(sessionId, title);
             return result > 0 ? success("更新成功") : error("更新失败");
         } catch (Exception e) {
@@ -111,7 +112,7 @@ public class CrmCustomerChatSessionController extends BaseController {
         try {
             Long sessionId = Long.valueOf(params.get("sessionId").toString());
             Integer isPinned = (Integer) params.get("isPinned");
-            
+
             if (isPinned == null) {
                 return error("置顶参数错误");
             }
@@ -167,4 +168,13 @@ public class CrmCustomerChatSessionController extends BaseController {
     {
         return R.ok().put("data",crmCustomerAnalyzeService.polishingScript(param));
     }
+
+    /**
+     * 企微外部联系人ai打标签
+     */
+    @PostMapping("/qwAiTagGain")
+    public R qwAiTagGain(@RequestBody QwAiTagGainParam param)
+    {
+        return crmCustomerAnalyzeService.qwAiTagGain(param);
+    }
 }

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

@@ -97,12 +97,14 @@ public class LiveAfterSalesController extends BaseController
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
-        LiveAfterSales liveAfterSales = liveAfterSalesService.selectLiveAfterSalesById(id);
+        LiveAfterSales liveAfterSales = liveAfterSalesService.selectLiveAfterSalesByIdForDetail(id);
         if(liveAfterSales==null) return R.error("售后记录不存在");
         List<LiveAfterSalesItem> list = liveAfterSalesItemService.selectLiveAfterSalesItemByAfterId(id);
         List<LiveAfterSalesLogs> logList = liveAfterSalesLogsService.selectLiveAfterSalesLogsByAfterId(id);
         FsUser user=userService.selectFsUserById(liveAfterSales.getUserId());
-        user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        if(user!=null && user.getPhone()!=null){
+            user.setPhone(ParseUtils.parsePhone(user.getPhone()));
+        }
         LiveOrder liveOrder = orderService.selectLiveOrderByOrderId(String.valueOf(liveAfterSales.getOrderId()));
         return R.ok().put("afterSales",liveAfterSales).put("items",list).put("logs",logList).put("user",user).put("order",liveOrder);
 

+ 8 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.http.HttpUtils;
@@ -69,7 +70,13 @@ public class LiveController extends BaseController
     public TableDataInfo listToLiveNoEnd(Live live)
     {
         startPage();
-        List<Live> list = liveService.listToLiveNoEnd(live);
+        List<Live> list = new ArrayList<>();
+        if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+            //直播也发
+            list = liveService.listToLiveNoEndNew(live);
+        }else{
+            list = liveService.listToLiveNoEnd(live);
+        }
         return getDataTable(list);
     }
 

+ 16 - 1
fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java

@@ -27,6 +27,7 @@ import com.fs.hisStore.service.IFsExpressScrmService;
 import com.fs.live.domain.*;
 import com.fs.live.enums.LiveOrderCancleReason;
 import com.fs.live.param.LiveOrderExpressParam;
+import com.fs.live.service.ILiveAfterSalesService;
 import com.fs.live.service.ILiveOrderItemService;
 import com.fs.live.service.ILiveOrderLogsService;
 import com.fs.live.service.ILiveOrderPaymentService;
@@ -61,6 +62,9 @@ public class LiveOrderController extends BaseController
     @Autowired
     private ILiveOrderService liveOrderService;
 
+    @Autowired
+    private ILiveAfterSalesService liveAfterSalesService;
+
     @Autowired
     private ICrmCustomerService crmCustomerService;
     @Autowired
@@ -263,7 +267,18 @@ public class LiveOrderController extends BaseController
     @GetMapping(value = "/info/{orderId}")
     public AjaxResult getInfo(@PathVariable("orderId") String orderId)
     {
-        return AjaxResult.success(liveOrderService.selectLiveOrderByOrderId(orderId));
+        LiveOrder order = liveOrderService.selectLiveOrderByOrderId(orderId);
+        if (order != null && order.getStatus() != null
+                && (order.getStatus() == -1 || order.getStatus() == -2)) {
+            LiveAfterSales las = liveAfterSalesService.selectLiveAfterSalesByOrderIdForDetail(order.getOrderId());
+            if (las != null) {
+                order.setReasonValue1(las.getReasonValue1());
+                order.setReasonValue2(las.getReasonValue2());
+                order.setAuditRemark(las.getAuditRemark());
+                order.setAuditReasonName(las.getAuditReasonName());
+            }
+        }
+        return AjaxResult.success(order);
     }
 
     /**

+ 39 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerAnalyzeController.java

@@ -0,0 +1,39 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.service.IQwExternalAiAnalyzeService;
+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;
+
+/**
+ * 客户聊天记录分析Controller
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@RestController
+@RequestMapping("/qw/analyze")
+public class QwCustomerAnalyzeController extends BaseController
+{
+    @Autowired
+    private IQwExternalAiAnalyzeService qwExternalAiAnalyzeService;
+
+    /**
+     * 查询客户聊天记录分析列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:external:analyze:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwExternalAiAnalyze crmCustomerAnalyze)
+    {
+        startPage();
+        List<QwExternalAiAnalyze> list = qwExternalAiAnalyzeService.selectQwExternalAiAnalyzeList(crmCustomerAnalyze);
+        return getDataTable(list);
+    }
+
+}

+ 45 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerPropertyController.java

@@ -0,0 +1,45 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwCustomerProperty;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.param.QwAnalyzeAiTagParam;
+import com.fs.qw.service.IQwCustomerPropertyService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/qw/customerProperty")
+@RequiredArgsConstructor
+public class QwCustomerPropertyController extends BaseController {
+
+    private final IQwCustomerPropertyService qwCustomerPropertyService;
+    private final QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+
+//    @PreAuthorize("@ss.hasPermi('qw:customerProperty:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwCustomerProperty qwCustomerProperty) {
+        startPage();
+        List<QwCustomerProperty> list = qwCustomerPropertyService.selectQwCustomerPropertyList(qwCustomerProperty);
+        return getDataTable(list);
+    }
+
+    @PostMapping("/analyzeAiTagByTrade")
+    public R analyzeAiTagByTrade(@RequestBody QwAnalyzeAiTagParam param){
+        QwExternalAiAnalyze aiAnalyze = qwExternalAiAnalyzeMapper.selectOne(new LambdaQueryWrapper<QwExternalAiAnalyze>().eq(QwExternalAiAnalyze::getExternalUserId, param.getExternalUserId())
+                .eq(QwExternalAiAnalyze::getQwUserId, param.getQwUserId()).eq(QwExternalAiAnalyze::getCorpId, param.getCorpId())
+                .orderByDesc(QwExternalAiAnalyze::getCreateTime).last("limit 1"));
+        if (ObjectUtil.isNull(aiAnalyze)) {
+            return R.error("无AI分析结果");
+        }
+        qwCustomerPropertyService.analyzeAiTagByTrade(param.getTradeType(),aiAnalyze);
+        return R.ok();
+    }
+}

+ 25 - 56
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -11,7 +11,6 @@ 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.ParseUtils;
-import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.crm.service.ICrmCustomerService;
@@ -98,7 +97,7 @@ public class FsStoreOrderController extends BaseController
      */
     @PostMapping("/healthLiveList")
     public FsStoreOrderListAndStatisticsVo healthLiveList(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        LoginUser loginUser = SecurityUtils.getLoginUser();
         param.setCompanyId(loginUser.getCompany().getCompanyId());
         // 如果前端传了 orderType,使用前端传的值;如果没传(null),设置 orderType = -1(特殊值,SQL 中会转换为查询 orderType IN (2,3))
         if (param.getOrderType() == null) {
@@ -124,7 +123,6 @@ public class FsStoreOrderController extends BaseController
         Map<String, java.math.BigDecimal> statistics = fsStoreOrderScrmService.selectFsStoreOrderStatistics(param);
         String productInfoStr = fsStoreOrderScrmService.selectFsStoreOrderProductStatistics(param);
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
-        param.setIsCompanyOrder(null);
         List<com.fs.hisStore.vo.FsStoreOrderVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVO(param);
         if (list != null) {
             for (com.fs.hisStore.vo.FsStoreOrderVO vo : list) {
@@ -308,24 +306,10 @@ public class FsStoreOrderController extends BaseController
     public AjaxResult healthExportItems(com.fs.hisStore.param.FsStoreOrderParam param) {
         normalizeExportParam(param);
         applyHealthLiveFilter(param);
+        LoginUser loginUser = SecurityUtils.getLoginUser();
         if (fsStoreOrderScrmService.isEntityNull(param)) {
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getPayTimeRange())){
-            param.setPayTimeList(param.getPayTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
-            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
-            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
-        }
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if("北京卓美".equals(signProjectName)){
-            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
-                param.setCompanyUserId(loginUser.getUser().getUserId());
-            }
-        }
         List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
         if ("北京卓美".equals(signProjectName)) {
             List<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> zmvoList = list.stream()
@@ -375,10 +359,9 @@ public class FsStoreOrderController extends BaseController
                         vo.setPayDelivery(java.math.BigDecimal.ZERO);
                         vo.setCost(java.math.BigDecimal.ZERO);
                         vo.setFPrice(java.math.BigDecimal.ZERO);
-                        vo.setBarCode("");
-                        vo.setCateName("");
-                        vo.setBankTransactionId("");
                     }
+                    vo.setCost(java.math.BigDecimal.ZERO);
+                    vo.setFPrice(java.math.BigDecimal.ZERO);
                 }
             }
             ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -388,27 +371,28 @@ public class FsStoreOrderController extends BaseController
         if (list != null) {
             for (FsStoreOrderItemExportVO vo : list) {
                 if (vo.getUserPhone() != null) {
-                    String phone = vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2");
-                    vo.setUserPhone(phone);
+                    vo.setUserPhone(vo.getUserPhone().replaceAll("(\\d{3})\\d*(\\d{1})", "$1****$2"));
                 }
-                if (!StringUtils.isEmpty(vo.getJsonInfo())) {
+                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
                     try {
-                        StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
-                        BeanUtil.copyProperties(orderProductDTO, vo);
+                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(dto, vo);
                     } catch (Exception e) {
+                        // ignore
                     }
                 }
-                //
-                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && !java.util.Objects.isNull(vo.getCost())) {
+                if (vo.getUserAddress() != null) {
+                    vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
+                }
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
                     vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
                     vo.setCost(java.math.BigDecimal.ZERO);
                     vo.setFPrice(java.math.BigDecimal.ZERO);
-                    vo.setBarCode("");
-                    vo.setCateName("");
-                    vo.setBankTransactionId("");
                 }
+                vo.setCost(java.math.BigDecimal.ZERO);
+                vo.setFPrice(java.math.BigDecimal.ZERO);
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
@@ -423,24 +407,10 @@ public class FsStoreOrderController extends BaseController
     public AjaxResult healthExportItemsDetails(com.fs.hisStore.param.FsStoreOrderParam param) {
         normalizeExportParam(param);
         applyHealthLiveFilter(param);
+        LoginUser loginUser = SecurityUtils.getLoginUser();
         if (fsStoreOrderScrmService.isEntityNull(param)) {
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getPayTimeRange())){
-            param.setPayTimeList(param.getPayTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
-            param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
-        }
-        if(!StringUtils.isEmpty(param.getDeliveryImportTimeRange())){
-            param.setDeliveryImportTimeList(param.getDeliveryImportTimeRange().split("--"));
-        }
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if("北京卓美".equals(signProjectName)){
-            if(!"00".equals(loginUser.getUser().getUserType())){//非管理员看见自己数据
-                param.setCompanyUserId(loginUser.getUser().getUserId());
-            }
-        }
         List<FsStoreOrderItemExportVO> list = orderItemScrmService.selectFsStoreOrderItemListExportVO(param);
         // 北京卓美项目特殊处理
         if("北京卓美".equals(signProjectName)){
@@ -480,10 +450,9 @@ public class FsStoreOrderController extends BaseController
                             vo.setPayDelivery(java.math.BigDecimal.ZERO);
                             vo.setCost(java.math.BigDecimal.ZERO);
                             vo.setFPrice(java.math.BigDecimal.ZERO);
-                            vo.setBarCode("");
-                            vo.setCateName("");
-                            vo.setBankTransactionId("");
                         }
+                        vo.setCost(java.math.BigDecimal.ZERO);
+                        vo.setFPrice(java.math.BigDecimal.ZERO);
                     }
                 }
                 ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -492,23 +461,23 @@ public class FsStoreOrderController extends BaseController
         //对手机号脱敏
         if (list != null) {
             for (FsStoreOrderItemExportVO vo : list) {
-                if (!StringUtils.isEmpty(vo.getJsonInfo())) {
+                if (StringUtils.isNotEmpty(vo.getJsonInfo())) {
                     try {
-                        StoreOrderProductDTO orderProductDTO = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
-                        BeanUtil.copyProperties(orderProductDTO, vo);
+                        StoreOrderProductDTO dto = com.alibaba.fastjson.JSONObject.parseObject(vo.getJsonInfo(), StoreOrderProductDTO.class);
+                        BeanUtil.copyProperties(dto, vo);
                     } catch (Exception e) {
+                        // ignore
                     }
                 }
-                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*") ) && vo.getCost() != null) {
+                if ((loginUser.getPermissions().contains("his:storeAfterSales:finance") || loginUser.getPermissions().contains("*:*:*")) && vo.getCost() != null) {
                     vo.setFPrice(vo.getCost().multiply(java.math.BigDecimal.valueOf(vo.getTotalNum())));
                 } else {
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
                     vo.setCost(java.math.BigDecimal.ZERO);
                     vo.setFPrice(java.math.BigDecimal.ZERO);
-                    vo.setBarCode("");
-                    vo.setCateName("");
-                    vo.setBankTransactionId("");
                 }
+                vo.setCost(java.math.BigDecimal.ZERO);
+                vo.setFPrice(java.math.BigDecimal.ZERO);
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);

+ 198 - 0
fs-company/src/main/java/com/fs/crm/CrmBusinessController.java

@@ -0,0 +1,198 @@
+package com.fs.crm;
+
+
+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.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.param.CrmBusinessAddAndUpdateParam;
+import com.fs.crm.param.CrmBusinessImportParam;
+import com.fs.crm.param.CrmBusinessQueryParam;
+import com.fs.crm.service.ICrmBusinessService;
+import com.fs.crm.vo.CrmBusinessListVO;
+import com.fs.framework.security.LoginUser;
+import com.hc.openapi.tool.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import com.fs.framework.service.TokenService;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 商机Controller
+ *
+ * @author fs
+ * @date 2025-01-16
+ */
+@RestController
+@RequestMapping("/crm/business")
+public class CrmBusinessController extends BaseController
+{
+    @Autowired
+    private ICrmBusinessService crmBusinessService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询商机列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CrmBusinessQueryParam param, HttpServletRequest request)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getUser().getCompanyId());
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCreateTimeArr(param.getCreateTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getNextTimeRange())){
+            param.setNextTimeArr(param.getNextTimeRange().split("--"));
+        }
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询我的商机列表
+     */
+    @GetMapping("/myList")
+    public TableDataInfo myList(CrmBusinessQueryParam param, HttpServletRequest request)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getUser().getCompanyId());
+        param.setCreateId(loginUser.getUser().getUserId());
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCreateTimeArr(param.getCreateTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getNextTimeRange())){
+            param.setNextTimeArr(param.getNextTimeRange().split("--"));
+        }
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 导出商机列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:export')")
+    @Log(title = "商机", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmBusinessQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getUser().getCompanyId());
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCreateTimeArr(param.getCreateTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getNextTimeRange())){
+            param.setNextTimeArr(param.getNextTimeRange().split("--"));
+        }
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+        ExcelUtil<CrmBusinessListVO> util = new ExcelUtil<CrmBusinessListVO>(CrmBusinessListVO.class);
+        return util.exportExcel(list, "商机");
+    }
+
+    /**
+     * 导出我的商机列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:export')")
+    @Log(title = "商机", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportMy")
+    public AjaxResult exportMy(CrmBusinessQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getUser().getCompanyId());
+        param.setCreateId(loginUser.getUser().getUserId());
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCreateTimeArr(param.getCreateTimeRange().split("--"));
+        }
+        if(!StringUtils.isEmpty(param.getNextTimeRange())){
+            param.setNextTimeArr(param.getNextTimeRange().split("--"));
+        }
+        List<CrmBusinessListVO> list = crmBusinessService.selectCrmBusinessList(param);
+        ExcelUtil<CrmBusinessListVO> util = new ExcelUtil<CrmBusinessListVO>(CrmBusinessListVO.class);
+        return util.exportExcel(list, "商机");
+    }
+
+    /**
+     * 获取商机详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:query')")
+    @GetMapping(value = "/{businessId}")
+    public AjaxResult getInfo(@PathVariable("businessId") Long businessId)
+    {
+        return AjaxResult.success(crmBusinessService.selectCrmBusinessById(businessId));
+    }
+
+    /**
+     * 新增商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:add')")
+    @Log(title = "商机", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmBusinessAddAndUpdateParam param)
+    {
+        //创建人
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCreateBy(loginUser.getUsername());
+        param.setCreateId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setOpeName(loginUser.getUser().getUserName());
+        return toAjax(crmBusinessService.insertCrmBusiness(param));
+    }
+
+    /**
+     * 修改商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:edit')")
+    @Log(title = "商机", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmBusinessAddAndUpdateParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setUpdateId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(crmBusinessService.updateCrmBusiness(param));
+    }
+
+    /**
+     * 删除商机
+     */
+    @PreAuthorize("@ss.hasPermi('crm:business:remove')")
+    @Log(title = "商机", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{businessIds}")
+    public AjaxResult remove(@PathVariable Long[] businessIds)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmBusinessService.deleteCrmBusinessByIds(businessIds,loginUser.getUser().getNickName()));
+    }
+
+    //下载模板
+    @GetMapping("/importTemplate")
+    public AjaxResult importTemplate()
+    {
+        ExcelUtil<CrmBusinessImportParam> util = new ExcelUtil<CrmBusinessImportParam>(CrmBusinessImportParam.class);
+        return util.importTemplateExcel("商机数据");
+    }
+
+    @Log(title = "导入商机", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('crm:business:import')")
+    @PostMapping("/importBusinessData")
+    public AjaxResult importBusinessData(MultipartFile file) throws Exception
+    {
+        ExcelUtil<CrmBusinessImportParam> util = new ExcelUtil<CrmBusinessImportParam>(CrmBusinessImportParam.class);
+        List<CrmBusinessImportParam> list = util.importExcel(file.getInputStream());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String message = crmBusinessService.importBusinessData(list, loginUser.getUser());
+        return AjaxResult.success(message);
+    }
+}

+ 100 - 0
fs-company/src/main/java/com/fs/crm/CrmExtDetailController.java

@@ -0,0 +1,100 @@
+package com.fs.crm;
+
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.param.CrmExtDetailAddOrUpdateParam;
+import com.fs.crm.service.ICrmExtDetailService;
+import com.fs.crm.vo.CrmExtDetailVo;
+import com.fs.watch.param.BaseQueryParam;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.fs.framework.service.TokenService;
+
+import java.util.List;
+
+/**
+ * 字段扩展详情Controller
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+@RestController
+@RequestMapping("/crm/detail")
+public class CrmExtDetailController extends BaseController
+{
+    @Autowired
+    private ICrmExtDetailService crmExtDetailService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询字段扩展列
+     */
+//    @PreAuthorize("@ss.hasPermi('crm:detail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(BaseQueryParam param)
+    {
+        startPage();
+        List<CrmExtDetailVo> list = crmExtDetailService.getTableColumnsMetadata(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出字段扩展详情列表
+     */
+//    @PreAuthorize("@ss.hasPermi('crm:detail:export')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(BaseQueryParam param)
+    {
+        List<CrmExtDetailVo> list = crmExtDetailService.getTableColumnsMetadata(param);
+        ExcelUtil<CrmExtDetailVo> util = new ExcelUtil<CrmExtDetailVo>(CrmExtDetailVo.class);
+        return util.exportExcel(list, "detail");
+    }
+
+
+
+    /**
+     * 新增字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:add')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.INSERT)
+    @PostMapping
+    public R add(@RequestBody CrmExtDetailAddOrUpdateParam param)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.insertColumn(param,nickName);
+    }
+
+    /**
+     * 修改字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:edit')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public R edit(@RequestBody CrmExtDetailAddOrUpdateParam param)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.updateColumn(param,nickName);
+    }
+
+    /**
+     * 删除字段扩展详情
+     */
+    @PreAuthorize("@ss.hasPermi('crm:detail:remove')")
+    @Log(title = "字段扩展详情", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{columnNames}")
+    public R remove(@PathVariable String[] columnNames)
+    {
+        String nickName = tokenService.getLoginUser(ServletUtils.getRequest()).getUser().getNickName();
+        return crmExtDetailService.deleteColumns(columnNames,nickName);
+    }
+}

+ 68 - 0
fs-company/src/main/java/com/fs/crm/CrmExtLogController.java

@@ -0,0 +1,68 @@
+package com.fs.crm;
+
+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.poi.ExcelUtil;
+import com.fs.crm.domain.CrmExtLog;
+import com.fs.crm.service.ICrmExtLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 修改字段扩展日志Controller
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+@RestController
+@RequestMapping("/crm/log")
+public class CrmExtLogController extends BaseController
+{
+    @Autowired
+    private ICrmExtLogService crmExtLogService;
+
+    /**
+     * 查询修改字段扩展日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmExtLog crmExtLog)
+    {
+        startPage();
+        List<CrmExtLog> list = crmExtLogService.selectCrmExtLogList(crmExtLog);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出修改字段扩展日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:export')")
+    @Log(title = "修改字段扩展日志", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmExtLog crmExtLog)
+    {
+        List<CrmExtLog> list = crmExtLogService.selectCrmExtLogList(crmExtLog);
+        ExcelUtil<CrmExtLog> util = new ExcelUtil<CrmExtLog>(CrmExtLog.class);
+        return util.exportExcel(list, "log");
+    }
+
+    /**
+     * 获取修改字段扩展日志详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('crm:log:query')")
+    @GetMapping(value = "/{logId}")
+    public AjaxResult getInfo(@PathVariable("logId") Long logId)
+    {
+        return AjaxResult.success(crmExtLogService.selectCrmExtLogById(logId));
+    }
+
+}

+ 83 - 0
fs-company/src/main/java/com/fs/crm/CrmFollowUpController.java

@@ -0,0 +1,83 @@
+package com.fs.crm;
+
+
+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.ServletUtils;
+import com.fs.crm.domain.CrmFollowUp;
+import com.fs.crm.service.ICrmFollowUpService;
+import com.fs.crm.vo.CrmFollowUpVo;
+import com.fs.framework.security.LoginUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.fs.framework.service.TokenService;
+
+import java.util.List;
+
+/**
+ * 跟进提醒Controller
+ *
+ * @author fs
+ * @date 2025-04-15
+ */
+@RestController
+@RequestMapping("/crm/followUp")
+public class CrmFollowUpController extends BaseController
+{
+    @Autowired
+    private ICrmFollowUpService crmFollowUpService;
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询跟进提醒列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CrmFollowUp crmFollowUp)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser == null) {
+            return null;
+        }
+        crmFollowUp.setCompanyUserId(loginUser.getUser().getUserId());
+        startPage();
+        List<CrmFollowUpVo> list = crmFollowUpService.selectCrmFollowUpAndCustomerList(crmFollowUp);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取跟进提醒详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(crmFollowUpService.selectCrmFollowUpById(id));
+    }
+
+
+    /**
+     * 修改跟进提醒
+     */
+    @Log(title = "跟进提醒", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmFollowUp crmFollowUp)
+    {
+        return toAjax(crmFollowUpService.updateCrmFollowUp(crmFollowUp));
+    }
+
+
+    /**
+     * 删除跟进提醒
+     */
+    @PreAuthorize("@ss.hasPermi('crm:followUp:remove')")
+    @Log(title = "跟进提醒", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(crmFollowUpService.deleteCrmFollowUpByIds(ids));
+    }
+}

+ 591 - 0
fs-company/src/main/java/com/fs/crm/CrmSjCustomerController.java

@@ -0,0 +1,591 @@
+package com.fs.crm;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiOperation;
+import com.fs.common.OrderUtils;
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.param.*;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.crm.service.ICrmCustomerUserService;
+import com.fs.crm.service.ICrmExtDetailService;
+import com.fs.crm.vo.CrmCustomerExportVO;
+import com.fs.crm.vo.CrmCustomerListQueryVO;
+import com.fs.crm.vo.CrmFullCustomerListQueryVO;
+import com.fs.framework.security.LoginUser;
+import com.github.pagehelper.PageHelper;
+import com.hc.openapi.tool.util.StringUtils;
+import io.swagger.annotations.ApiParam;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import com.fs.framework.service.TokenService;
+import com.fs.common.annotation.Log;
+/**
+ * 客户Controller
+ *
+ * @author fs
+ * @date 2022-12-21
+ */
+@RestController
+@RequestMapping("/crm/sj/customer")
+public class CrmSjCustomerController extends BaseController
+{
+    @Autowired
+    private ICrmCustomerService crmCustomerService;
+    @Autowired
+    ICompanyUserService companyUserService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    ICrmCustomerUserService crmCustomerUserService;
+
+    @Autowired
+    private ICrmExtDetailService crmExtDetailService;
+
+    @ApiOperation("获取线索客户")
+    @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
+    @GetMapping("/getLineCustomerList")
+    public TableDataInfo getLineCustomerList(CrmLineCustomerListQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setIsLine(1);
+        param.setIsPool(0);
+        return crmCustomerService.selectCrmLineCustomerListQueryInfo(param);
+    }
+
+    @ApiOperation("获取可离职继承的线索客户")
+    @PreAuthorize("@ss.hasPermi('crm:customer:transferList')")
+    @GetMapping("/getTransferCustomerList")
+    public TableDataInfo getTransferCustomerList(CrmLineCustomerListQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setIsPool(0);
+        return crmCustomerService.selectTransferCustomerList(param);
+    }
+
+    @ApiOperation("获取我的线索客户")
+    @PreAuthorize("@ss.hasPermi('crm:customer:clueList')")
+    @GetMapping("/getMyLineCustomerList")
+    public TableDataInfo getMyLineCustomerList(CrmLineCustomerListQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setIsReceive(1L);
+        param.setReceiveUserId(loginUser.getUser().getUserId());
+        param.setIsLine(1);
+        param.setIsPool(0);
+//        List<Map<String,Object>> list =
+//        if (list != null) {
+//            for (Map<String,Object> vo : list) {
+//                if(vo.get("mobile")!=null){
+//                    vo.put("mobile",vo.get("mobile").toString().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+//                }
+//            }
+//        }
+        return crmCustomerService.selectCrmLineCustomerListQueryInfo(param);
+    }
+
+    @ApiOperation("获取公海客户")
+    @PreAuthorize("@ss.hasPermi('crm:customer:fullList')")
+    @GetMapping("/getFullCustomerList")
+    @DataScope(deptAlias = "c",userAlias = "c")
+    public TableDataInfo getFullCustomerList(CrmFullCustomerListQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if(loginUser.getCompany().getCompanyId()==116){   // 河北湘银信息咨询服务有限公司(JZ-1)客户假删除不显示
+//            param.setCompanyId(0L);
+//        }
+        List<CrmFullCustomerListQueryVO> list = crmCustomerService.selectCrmFullCustomerListQuery(param);
+//        if (list != null) {
+//            for (CrmFullCustomerListQueryVO vo : list) {
+//                if(vo.getMobile()!=null){
+//                    vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+//                }
+//
+//            }
+//        }
+        return getDataTable(list);
+    }
+
+    @ApiOperation("获取我的客户列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:myList')")
+    @GetMapping("/getMyCustomerList")
+    public TableDataInfo getMyCustomerList(CrmMyCustomerListQueryParam param){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
+            param.setCustomerCreateTime(param.getCreateTimeRange().split("--"));
+        }
+        param.setIsLine(0);
+
+        return crmCustomerService.selectCrmMyCustomerListQueryInfo(param);
+
+    }
+
+    @ApiOperation("获取客户列表")
+    @GetMapping("/getCustomerList")
+    public TableDataInfo getCustomerList(CrmCustomerListQueryParam param){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setIsLine(0);
+        param.setIsPool(0);
+        if (param.getIsReceive() != null && param.getIsReceive() == 0){
+            CrmLineCustomerListQueryParam param1 = new CrmLineCustomerListQueryParam();
+            BeanUtils.copyProperties(param,param1);
+            return crmCustomerService.selectCrmLineCustomerListQueryInfo(param1);
+
+        }else {
+            List<CrmCustomerListQueryVO> list = crmCustomerService.selectCrmCustomerListQuery(param);
+            return getDataTable(list);
+        }
+    }
+
+
+    @ApiOperation("获取未入公海列表")
+    @PostMapping("/getMyUnPoolList")
+    public TableDataInfo getMyUnPoolList(@RequestBody CrmUnPoolListQueryParam param){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setReceiveUserId(loginUser.getUser().getUserId());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return crmCustomerService.selectUnPoolCrmCustomerList(param);
+    }
+
+    @ApiOperation("获取所有未入公海列表")
+    @PostMapping("/getAllUnPoolList")
+    public TableDataInfo getAllUnPoolList(@RequestBody CrmUnPoolListQueryParam param){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return crmCustomerService.selectUnPoolCrmCustomerList(param);
+    }
+
+
+
+    @ApiOperation("获取客户详情")
+    @GetMapping("/getCustomerDetails")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    public R getCustomerDetails(
+            HttpServletRequest request,
+            @ApiParam(required = true, name = "customerId", value = "客户ID") @RequestParam(value = "customerId", required = false) Long customerId
+    ){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
+        Boolean isReceive=false;
+        if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
+            isReceive=true;
+        }
+        //查询扩展字段
+        HashMap<String, Object> map = new HashMap<>();
+        map.put("correlate_id",customerId);
+        map.put("correlate_type","customer_id");
+        Map<String,Object> ext=crmExtDetailService.selectCrmExtDetailByCondition(map);
+        return R.ok().put("customer",customer).put("isReceive",isReceive).put("ext",ext);
+
+    }
+
+    @ApiOperation("获取客户详情")
+    @GetMapping("/getCustomerDetails1")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query1')")
+    public R getCustomerDetails1(
+            HttpServletRequest request,
+            @ApiParam(required = true, name = "customerId", value = "客户ID") @RequestParam(value = "customerId", required = false) Long customerId
+    ){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
+        Boolean isReceive=false;
+        if(customer.getIsReceive()!=null&&customer.getIsReceive()==1&&customer.getReceiveUserId()!=null&&loginUser.getUser().getUserId().equals(customer.getReceiveUserId())){
+            isReceive=true;
+        }
+        return R.ok().put("customer",customer).put("isReceive",isReceive);
+
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:query2')")
+    @GetMapping(value = "/query1/{customerId}")
+    public R getInfo1(@PathVariable("customerId") Long customerId)
+    {
+        CrmCustomer customer=crmCustomerService.selectCrmCustomerById(customerId);
+        String mobile = customer.getMobile();
+        return R.ok().put("mobile",mobile);
+    }
+
+    //分配
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignToUser')")
+    @PostMapping("/assignToUser")
+    public R assignToUser(@RequestBody CrmCustomeAssignParam param)
+    {
+        if(param.getCustomerIds().size()>1000){
+            return R.error("分配数据超出范围,最大1000条");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return crmCustomerService.assignToUser(loginUser.getUsername(),loginUser.getUser().getUserId(),param);
+    }
+
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:add')")
+    @Log(title = "创建客户", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@Validated @RequestBody CrmCustomerUpdateOrAddParam crmCustomer)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        crmCustomer.setCustomerCode(OrderUtils.getOrderNo());
+        crmCustomer.setIsDel(0);
+        crmCustomer.setIsLine(1);
+        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+        crmCustomer.setCreateUserId(loginUser.getUser().getUserId());
+        crmCustomer.setDeptId(loginUser.getUser().getDeptId());
+        //是否是公司负责人新建待分配线索
+//        Integer status = crmCustomer.getStatus();
+//        if (status == 1) {
+//            crmCustomer.set
+//        }
+        crmCustomer.setOpeName(loginUser.getUser().getNickName());
+        return crmCustomerService.insertCrmCustomer(crmCustomer)>0?AjaxResult.success():AjaxResult.error("电话/客户名称已存在");
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:addMyCustomer')")
+    @ApiOperation("添加我的线索")
+    @PostMapping("/addMyClue")
+    public R addMyClue(@RequestBody CrmCustomerUpdateOrAddParam crmCustomer)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        crmCustomer.setCustomerCode(OrderUtils.getOrderNo());
+//        crmCustomer.setIsLine(1);
+//        crmCustomer.setIsPool(0);
+//        crmCustomer.setStatus(1);
+//        crmCustomer.setIsReceive(1);
+        crmCustomer.setDeptId(loginUser.getUser().getDeptId());
+        crmCustomer.setCreateUserId(loginUser.getUser().getUserId());
+        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+        crmCustomer.setOpeName(loginUser.getUser().getNickName());
+        if(crmCustomerService.insertCrmCustomer(crmCustomer)>0){
+            CompanyUser companyUser=loginUser.getUser();
+            CrmCustomeReceiveParam param=new CrmCustomeReceiveParam();
+            String operName = companyUser.getNickName();
+            param.setCompanyId(companyUser.getCompanyId());
+            param.setCompanyUserId(companyUser.getUserId());
+            param.setCustomerId(crmCustomer.getCustomerId());
+            crmCustomerService.receive(param,operName);
+            return R.ok();
+        } else{
+            return R.error("该公司客户已存在,请勿重复添加");
+        }
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:addMyCustomer')")
+    @ApiOperation("添加我的客户")
+    @PostMapping("/addMyCustomer")
+    public R addMyCustomer(@RequestBody CrmCustomerUpdateOrAddParam crmCustomer)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        crmCustomer.setCustomerCode(OrderUtils.getOrderNo());
+        crmCustomer.setIsLine(0);
+        crmCustomer.setIsDel(0);
+        crmCustomer.setIsReceive(0);
+        crmCustomer.setStatus(1);
+        crmCustomer.setDeptId(loginUser.getUser().getDeptId());
+        crmCustomer.setCreateUserId(loginUser.getUser().getUserId());
+        crmCustomer.setCompanyId(loginUser.getCompany().getCompanyId());
+        crmCustomer.setOpeName(loginUser.getUser().getNickName());
+        if(crmCustomerService.insertCrmCustomer(crmCustomer)>0){
+            CompanyUser companyUser=loginUser.getUser();
+            CrmCustomeReceiveParam param=new CrmCustomeReceiveParam();
+            String operName = companyUser.getNickName();
+            param.setCompanyId(companyUser.getCompanyId());
+            param.setCompanyUserId(companyUser.getUserId());
+            param.setCustomerId(crmCustomer.getCustomerId());
+            crmCustomerService.receive(param,operName);
+            return R.ok();
+        }
+        else{
+            return R.error();
+        }
+    }
+
+    /**
+     * 修改客户
+     */
+//    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户", businessType = BusinessType.UPDATE)
+    @PutMapping("/edit")
+    public AjaxResult edit(@RequestBody CrmCustomerUpdateOrAddParam crmCustomer) {
+        return crmCustomerService.updateCrmCustomer(crmCustomer)>0?AjaxResult.success():AjaxResult.error("修改电话或用户名已存在");
+    }
+
+
+    //认领
+    @PreAuthorize("@ss.hasPermi('crm:customer:receive')")
+    @PostMapping("/receive")
+    public R receive(@RequestBody CrmCustomeReceiveParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String operName = loginUser.getUsername();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setDeptId(loginUser.getUser().getDeptId());
+        return crmCustomerService.receive(param,operName);
+    }
+
+    /**
+     * 批量认领
+     * @param param
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:receive')")
+    @PostMapping("/batchReceive")
+    public R receive(@RequestBody CrmCustomerBatchReceiveParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String operName = loginUser.getUsername();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setDeptId(loginUser.getUser().getDeptId());
+        return crmCustomerService.batchReceive(param,operName);
+    }
+
+    //回收
+    @PreAuthorize("@ss.hasPermi('crm:customer:recover')")
+    @PostMapping("/recover")
+    public R recover(@RequestBody CrmCustomerRecoverParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String operName = loginUser.getUsername();
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        return crmCustomerService.recoverR(param,operName);
+    }
+
+    /**
+     * 未分配线索投入公海池
+     * @param param
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:recover')")
+    @PostMapping("/recoverBatch")
+    public R recoverBatch(@RequestBody CrmLineCustomerListQueryParam param)
+    {
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setIsLine(1);
+        param.setIsPool(0);
+        return crmCustomerService.recoverClueBatch(param);
+    }
+
+    /**
+     * 未分配线索投入公海池
+     * @param customerIds
+     * @return  dd
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:recover')")
+    @PostMapping("/recoverBatchByIds")
+    public R recoverBatchByIds(@RequestBody Long[] customerIds)
+    {
+        if (customerIds == null || customerIds.length<1) {
+            return R.error("请选择需要投入公海的待分配线索");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser == null) {
+            return R.error("请先登录再进行操作");
+        }
+        return crmCustomerService.recoverBatchByIds(customerIds,loginUser.getCompany().getCompanyId(),loginUser.getUser().getNickName(),loginUser.getUser().getUserId());
+    }
+
+
+    /**
+     * 客户投入公海池
+     * @param param
+     * @return  dd
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:recover')")
+    @PostMapping("/recoverCustomerBatchByIds")
+    public R recoverCustomerBatchByIds(@RequestBody CrmCustomerBatchRecoverParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(loginUser.getUser().getUserId());
+        return crmCustomerService.recoverCustomerBatchByIds(param,loginUser.getUsername());
+    }
+
+
+
+
+    //分配
+    @PreAuthorize("@ss.hasPermi('crm:customer:assignUser')")
+    @PostMapping("/assignUser")
+    public R assignUser(@RequestBody CrmCustomeAssignUserParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String operName = loginUser.getUsername();
+        param.setDeptId(loginUser.getUser().getDeptId());
+        return crmCustomerService.assignUser(param,operName);
+    }
+
+    /**
+     *
+     * @param file
+     * @param type 导入类型 0:待分配员工 1:我的
+     * @return
+     * @throws Exception
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:importLine')")
+    @PostMapping("/importLineData")
+    public AjaxResult importLineData(MultipartFile file, Integer type) throws Exception
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        ExcelUtil<CrmCompanyLineCustomerImportParam> util = new ExcelUtil<CrmCompanyLineCustomerImportParam>(CrmCompanyLineCustomerImportParam.class);
+        List<CrmCompanyLineCustomerImportParam> list = util.importExcel(file.getInputStream());
+        if(list.size()>12000){
+            return new AjaxResult(500,"导入数据超出范围,最大12000条");
+        }
+        String operName = loginUser.getUsername();
+        String message = crmCustomerService.importCompanyLineCustomerType(list, operName,loginUser.getCompany().getCompanyId(),loginUser.getUser().getUserId(),type);
+        return AjaxResult.success(message);
+    }
+
+    //下载模板
+    @GetMapping("/importLineTemplate")
+    public AjaxResult importLineTemplate()
+    {
+        ExcelUtil<CrmCompanyLineCustomerImportParam> util = new ExcelUtil<CrmCompanyLineCustomerImportParam>(CrmCompanyLineCustomerImportParam.class);
+        return util.importTemplateExcel("线索客户数据");
+    }
+
+    @Log(title = "导入", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('crm:customer:importVisit')")
+    @PostMapping("/importVisitData")
+    public AjaxResult importVisitData(MultipartFile file) throws Exception
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        ExcelUtil<CrmCustomerVisitImportParam> util = new ExcelUtil<CrmCustomerVisitImportParam>(CrmCustomerVisitImportParam.class);
+        List<CrmCustomerVisitImportParam> list = util.importExcel(file.getInputStream());
+        String message = crmCustomerService.importVisitCustomer(list,loginUser.getCompany().getCompanyId());
+        return AjaxResult.success(message);
+    }
+    //下载模板
+    @GetMapping("/importVisitTemplate")
+    public AjaxResult importVisitTemplate()
+    {
+        ExcelUtil<CrmCustomerVisitImportParam> util = new ExcelUtil<CrmCustomerVisitImportParam>(CrmCustomerVisitImportParam.class);
+        return util.importTemplateExcel("客户跟进");
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerListQueryParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (param.getImportType() != null && param.getImportType() == 1){
+            param.setCompanyUserId(loginUser.getCompany().getUserId());
+        }
+        List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListQuery(param);
+        for(CrmCustomerExportVO customer:list){
+            if(StringUtils.isNotEmpty(customer.getMobile())){
+                if(loginUser.getUser().getUserType().equals("00")){
+                }
+                else{
+                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+
+            }
+        }
+        ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
+        return util.exportExcel(list, "客户");
+    }
+
+
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "导出公海", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportFull")
+    public AjaxResult export(CrmFullCustomerListQueryParam param)
+    {
+        List<CrmFullCustomerListQueryVO> list = crmCustomerService.selectCrmFullCustomerListQuery(param);
+        ExcelUtil<CrmFullCustomerListQueryVO> util = new ExcelUtil<CrmFullCustomerListQueryVO>(CrmFullCustomerListQueryVO.class);
+        return util.exportExcel(list, "公海客户");
+    }
+
+    @Log(title = "客户", businessType = BusinessType.EXPORT)
+    @PostMapping("/{customerIds}")
+    public AjaxResult exportByIds(@PathVariable Long[] customerIds)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListByIds(customerIds);
+        for(CrmCustomerExportVO customer:list){
+            if(StringUtils.isNotEmpty(customer.getMobile())){
+                if(loginUser.getUser().getUserType().equals("00")){
+                }
+                else{
+                    customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                }
+
+            }
+        }
+        ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
+        return util.exportExcel(list, "客户");
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:editSource')")
+    @PostMapping("/updateCustomerSource")
+    public AjaxResult updateCustomerSource(@RequestBody CrmCustomerEditSourceParam param)
+    {
+
+        return toAjax(crmCustomerService.updateCrmCustomerSource(param));
+    }
+
+
+    @GetMapping("/getCustomerListByIds")
+    public R getCustomerListByIds(@RequestParam("customerIds")String customerIds)
+    {
+        List<CrmCustomer> customerList=crmCustomerService.selectCrmCustomerListByIds(customerIds);
+        return R.ok().put("data",customerList);
+    }
+
+
+    @ApiOperation("查询客户")
+    @GetMapping("/getCustomerListBySearch")
+    public R getCustomerListBySearch(CrmCustomerSearchParam param){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<CrmCustomer> list = crmCustomerService.selectCrmCustomerListBySearch(param);
+        return R.ok().put("data",list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customer:removeLine')")
+    @Log(title = "客户", businessType = BusinessType.DELETE)
+    @PostMapping("/removeLine")
+    public AjaxResult removeLine(@RequestBody Long[] customerIds)
+    {
+        return toAjax(crmCustomerService.deleteCrmCustomerByIds(customerIds));
+    }
+
+
+}

+ 1 - 1
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -122,7 +122,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
-        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesById(id);
+        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesByIdForDetail(id);
         FsStoreAfterSalesItemScrm map=new FsStoreAfterSalesItemScrm();
         map.setStoreAfterSalesId(id);
         List<FsStoreAfterSalesItemScrm> items=fsStoreAfterSalesItemService.selectFsStoreAfterSalesItemList(map);

+ 10 - 1
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -93,6 +93,8 @@ public class FsStoreOrderScrmController extends BaseController
     @Autowired
     private IFsStoreOrderAuditLogScrmService orderAuditLogService;
     @Autowired
+    private IFsStoreAfterSalesScrmService fsStoreAfterSalesScrmService;
+    @Autowired
     private ISysConfigService configService;
     @Autowired
     private ICompanyUserService companyUserService;
@@ -312,8 +314,15 @@ public class FsStoreOrderScrmController extends BaseController
         }
 
         List<FsStoreOrderAuditLogVO> auditLogs = orderAuditLogService.selectStoreOrderAuditLogVOByOrderId(order.getId());
+
+        FsStoreAfterSalesScrm afterSales = null;
+        if (order.getStatus() != null && (order.getStatus() == -1 || order.getStatus() == -2)
+                && StringUtils.isNotEmpty(order.getOrderCode())) {
+            afterSales = fsStoreAfterSalesScrmService.selectFsStoreAfterSalesByOrderCode(order.getOrderCode());
+        }
+
         return R.ok().put("order", order).put("items", items).put("logs",logs).put("user",user).put("customer",customer).put("payments",payments)
-                .put("auditLogs", auditLogs);
+                .put("auditLogs", auditLogs).put("afterSales", afterSales);
     }
 
     /**

+ 118 - 13
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -43,6 +43,7 @@ import com.fs.qwApi.param.QwExternalContactHParam;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.impl.QwSopLogsServiceImpl;
+import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.wxwork.dto.*;
 import lombok.AllArgsConstructor;
@@ -55,6 +56,8 @@ import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -87,6 +90,8 @@ public class IpadSendServer {
     private static final List<String> PROJECT_NAMES = Arrays.asList("济南联志健康", "北京存在文化","宽益堂");
     private final LiveWatchLogMapper liveWatchLogMapper;
 
+    private static final Pattern LINK_PATTERN = Pattern.compile("link=([^&]+)");
+
     private void sendMiniProgram(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap, Long companyId) {
         // 发送参数原本的appid
         String appid = content.getMiniprogramAppid();
@@ -704,12 +709,28 @@ public class IpadSendServer {
                     }
                     sendMiniProgram(vo, content, miniMap, qwUser.getCompanyId());
                 case "14":
+                    log.error("进入到福袋》》》》》》》");
                     // 记录福袋发送记录
-                    Long businessId = addLuckyBagCollectRecord(qwUser, content,qwSopLogs);
-                    if (ObjectUtil.isEmpty(businessId)) {
+                    LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(content.getLuckyBagId());
+                    if(ObjectUtil.isNotEmpty(luckyBag)&&luckyBag.getDataStatus().equals("0")){
+                        qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "福袋配置被禁用");
+                    }
+                    else if (ObjectUtil.isNotEmpty(qwSopLogs.getFsUserId())){
+                        //获取配置并校验
+                        SysConfig sysConfig = configService.selectConfigByConfigKey("luckyBag.config");
+                        if (ObjectUtil.isEmpty(sysConfig)) {
+                            qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "福袋配置不存在");
+                        }
+                    }
+                    try{
+
+                        Long businessId = addLuckyBagCollectRecord(luckyBag,qwUser, content,qwSopLogs);
+                        content.setMiniprogramPage(quickProcess(content.getMiniprogramPage(),businessId));
+                        // 福袋
+                        sendMiniProgram(vo, content, miniMap);
+                    }catch (Exception e){
                         qwSopLogsService.updateQwSopLogsByWatchLogType(qwSopLogs.getId(), "福袋发放失败,请重新发送!");
                     }
-                    sendMiniProgram(vo, content, miniMap, qwUser.getCompanyId());
                 case "17":
                     // 小程序H5看课
                     sendMiniProgram(vo, content, miniMap);
@@ -763,7 +784,7 @@ public class IpadSendServer {
             sendShortLink = livePrefix + obj.getString("link");
         }
         //解析课程短链信息
-        String coursePrefix = "/pages/courseAnswer/index?link=";
+        String coursePrefix = "/courseH5/pages/course/learning?course=";
         if (miniProgramPage.startsWith(coursePrefix)) {
             JSONObject obj = JSONObject.parseObject(miniProgramPage.substring(coursePrefix.length()));
             sendShortLink = coursePrefix + obj.getString("link");
@@ -865,20 +886,13 @@ public class IpadSendServer {
         ipadSendUtils.loginOut(user.getUid(), user.getServerId());
     }
 
-    /**
-     * @Description: 生成福袋记录
-     * @Param:
-     * @Return:
-     * @Author xgb
-     * @Date 2026/2/4 11:39
-     */
-    private Long addLuckyBagCollectRecord(QwUser qwUser, QwSopCourseFinishTempSetting.Setting content, QwSopLogs qwSopLogs) {
+
+    public Long addLuckyBagCollectRecord(LuckyBag luckyBag, QwUser qwUser, QwSopCourseFinishTempSetting.Setting content, QwSopLogs qwSopLogs) {
 
         try{
             String json = configService.selectConfigByKey("course.config");
             CourseConfig config = JSON.parseObject(json, CourseConfig.class);
             Date updateTime = createUpdateTime(content, new Date(), config);
-            LuckyBag luckyBag = luckyBagMapper.selectLuckyBagById(content.getLuckyBagId());
             String companyUserId = String.valueOf(qwUser.getCompanyUserId()).trim();
             String companyId = String.valueOf(qwUser.getCompanyId()).trim();
             Long businessId = addLuckyBagCollectRecord(qwUser,luckyBag,content,qwSopLogs,updateTime,companyUserId,companyId,content.getChatId());
@@ -889,6 +903,97 @@ public class IpadSendServer {
         }
     }
 
+
+
+    /**
+     * 优化版快速处理方法
+     */
+    public static String quickProcess(String miniprogramPage, Long businessId) {
+        if (miniprogramPage == null || businessId == null) {
+            return miniprogramPage;
+        }
+
+        try {
+            Matcher matcher = LINK_PATTERN.matcher(miniprogramPage);
+            if (matcher.find()) {
+                String encodedLink = matcher.group(1);
+
+                // 第一次URL解码
+                String decodedLink = java.net.URLDecoder.decode(encodedLink, "UTF-8");
+
+                // 处理多次转义的情况:如果解码后仍有转义字符,继续解码
+                String jsonStr = decodedLink;
+                while (jsonStr.contains("\\\"") || jsonStr.contains("\\\\")) {
+                    // 检查是否是合法的JSON格式
+                    if (jsonStr.trim().startsWith("{") && jsonStr.trim().endsWith("}")) {
+                        break; // 已经是JSON对象字符串,不需要再解码
+                    }
+                    // 尝试再次解码
+                    try {
+                        jsonStr = java.net.URLDecoder.decode(jsonStr, "UTF-8");
+                    } catch (Exception e) {
+                        // 如果解码失败,说明不是URL编码,跳出循环
+                        break;
+                    }
+                }
+
+                // 清理JSON字符串中的转义字符
+                jsonStr = cleanJsonString(jsonStr);
+
+                // 解析JSON
+                JSONObject linkJson = JSON.parseObject(jsonStr);
+
+                // 检查并添加businessId
+                if (!linkJson.containsKey("businessId") ||
+                        linkJson.getString("businessId") == null ||
+                        linkJson.getString("businessId").isEmpty()) {
+
+                    linkJson.put("businessId", businessId);
+
+                    // 重新编码(只编码一次)
+                    String updatedJson = linkJson.toJSONString();
+                    String updatedEncoded = java.net.URLEncoder.encode(updatedJson, "UTF-8");
+
+                    // 替换原link参数
+                    return miniprogramPage.replace(
+                            "link=" + encodedLink,
+                            "link=" + updatedEncoded
+                    );
+                }
+            }
+            return miniprogramPage;
+        } catch (Exception e) {
+            // 记录错误日志,这里可以替换为你的日志框架
+            System.err.println("处理小程序页面链接失败: " + e.getMessage());
+            e.printStackTrace();
+            // 根据业务需求,可以选择抛出异常或返回原字符串
+            // throw new RuntimeException("处理小程序页面链接失败", e);
+            return miniprogramPage; // 失败时返回原字符串,避免影响主流程
+        }
+    }
+
+    /**
+     * 清理JSON字符串中的多余转义
+     */
+    private static String cleanJsonString(String jsonStr) {
+        if (jsonStr == null || jsonStr.isEmpty()) {
+            return jsonStr;
+        }
+
+        String result = jsonStr;
+
+        // 移除多余的转义
+        result = result.replace("\\\"", "\"");
+        result = result.replace("\\\\", "\\");
+
+        // 处理开头和结尾的转义引号
+        if (result.startsWith("\"") && result.endsWith("\"")) {
+            result = result.substring(1, result.length() - 1);
+        }
+
+        return result;
+    }
+
     /**
      * 过期时间
      *

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

@@ -6,6 +6,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
@@ -26,6 +27,7 @@ import com.fs.live.domain.*;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.newAdv.service.ILeadService;
+import com.fs.utils.ContentCheckUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -417,6 +419,11 @@ public class WebSocketServer {
         long liveId = (long) userProperties.get("liveId");
         long userType = (long) userProperties.get("userType");
         boolean isAdmin = false;
+        if (CloudHostUtils.hasCloudHostName("济世百康") ) {
+            if(!ContentCheckUtil.checkText(message)){
+                return;
+            }
+        }
 
         SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
         if(msg.isOn()) return;

+ 10 - 0
fs-qw-api-msg/src/main/resources/application.yml

@@ -1,5 +1,15 @@
 server:
   port: 8667
+# 企微会话存档
+qw:
+  msg-audit:
+    sdk-lib-path: C:/finance-sdk/libcrypto-3-x64.dll,libcurl-x64.dll,libssl-3-x64.dll,WeWorkFinanceSdk.dll
+    # 企微语音 amr 转 mp3 时调用,需在服务器安装 ffmpeg 或填写可执行文件绝对路径
+    ffmpeg-path: C:\ffmpeg.exe
+    voice-amr-to-mp3: true
+    # 解密后 raw_json 中文乱码(如「娴嬭瘯」应为「测试」)时改为 true;Linux 上若一直正常请保持 false
+    plain-json-fix-gbk-misread: false
+
 # Spring配置
 spring:
   profiles:

+ 20 - 2
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -1891,8 +1891,26 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
 
                     break;
                 case "14":
-                    String linkBy = createActivityLinkByMiniApp(setting, sopLogs, sopLogs.getCorpId(), new Date(), courseId, videoId,
-                            qwUserId, companyUserId, companyId, cachedCourseConfig, logVo.getChatId());
+                    String linkBy = createActivityLinkByMiniApp(setting,sopLogs,sopLogs.getCorpId(),new Date(), courseId, videoId,
+                            qwUserId, companyUserId, companyId,cachedCourseConfig,logVo.getChatId());
+
+                    if(sopLogs.getSendType()==1){
+                        setting.setMiniprogramAppid(miniAppId);
+                    }
+                    else {
+                        //算主备小程序
+                        String finalAppId = getAppIdFromMiniMap(miniMap, companyId, sendMsgType, grade);
+
+                        if (StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            finalAppId = miniAppId;
+                        }
+
+                        if (!StringUtil.strIsNullOrEmpty(finalAppId)) {
+                            setting.setMiniprogramAppid(finalAppId);
+                        } else {
+                            log.error("公司的小程序id为空:采用了前端传的固定值" + sopLogs.getSopId());
+                        }
+                    }
                     setting.setMiniprogramTitle("福袋发放");
                     setting.setMiniprogramPage(linkBy);
                     setting.setContentType("14");

+ 110 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyUserShow.java

@@ -0,0 +1,110 @@
+package com.fs.company.domain;
+
+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;
+
+/**
+ * 用户展示字段对象 company_user_show
+ *
+ * @author fs
+ * @date 2025-02-11
+ */
+public class CompanyUserShow extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** Id */
+    private Long id;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 列表类型 */
+    @Excel(name = "列表类型")
+    private String type;
+
+    /** 列表字段 */
+    @Excel(name = "列表字段")
+    private String columns;
+
+    /** 是否删除 0:否 1:是 */
+    @Excel(name = "是否删除 0:否 1:是")
+    private Long isDel;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+    public void setCompanyId(Long companyId)
+    {
+        this.companyId = companyId;
+    }
+
+    public Long getCompanyId()
+    {
+        return companyId;
+    }
+    public void setUserId(Long userId)
+    {
+        this.userId = userId;
+    }
+
+    public Long getUserId()
+    {
+        return userId;
+    }
+    public void setType(String type)
+    {
+        this.type = type;
+    }
+
+    public String getType()
+    {
+        return type;
+    }
+    public void setColumns(String columns)
+    {
+        this.columns = columns;
+    }
+
+    public String getColumns()
+    {
+        return columns;
+    }
+    public void setIsDel(Long isDel)
+    {
+        this.isDel = isDel;
+    }
+
+    public Long getIsDel()
+    {
+        return isDel;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("companyId", getCompanyId())
+            .append("userId", getUserId())
+            .append("type", getType())
+            .append("columns", getColumns())
+            .append("createTime", getCreateTime())
+            .append("updateBy", getUpdateBy())
+            .append("updateTime", getUpdateTime())
+            .append("isDel", getIsDel())
+            .toString();
+    }
+}

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

@@ -3,6 +3,7 @@ package com.fs.company.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyAiWorkflowExecLog;
+import com.fs.company.vo.CallContentVO;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -70,4 +71,6 @@ public interface CompanyAiWorkflowExecLogMapper extends BaseMapper<CompanyAiWork
     void batchInsert(@Param("list") List<CompanyAiWorkflowExecLog> logList);
 
     List<CompanyAiWorkflowExecLog> selectByInstanceIds(List<String> instanceIds);
+
+    List<CallContentVO> selectCallContent(@Param("ids") List<Long> ids);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java

@@ -86,4 +86,6 @@ public interface CompanyConfigMapper
             "is_del = 0\n" +
             "and FIND_IN_SET(#{companyId},set_company_ids)")
     List<CompanyMiniAppVO> getCompanyMiniAppList(@Param("companyId") Long companyId);
+
+    List<CompanyConfig> selectListByKey(String configKey);
 }

+ 69 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserShowMapper.java

@@ -0,0 +1,69 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyUserShow;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 用户展示字段Mapper接口
+ *
+ * @author fs
+ * @date 2025-02-11
+ */
+public interface CompanyUserShowMapper
+{
+    /**
+     * 查询用户展示字段
+     *
+     * @param id 用户展示字段ID
+     * @return 用户展示字段
+     */
+    public CompanyUserShow selectCompanyUserShowById(Long id);
+
+    /**
+     * 查询用户展示字段列表
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 用户展示字段集合
+     */
+    public List<CompanyUserShow> selectCompanyUserShowList(CompanyUserShow companyUserShow);
+
+    /**
+     * 新增用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    public int insertCompanyUserShow(CompanyUserShow companyUserShow);
+
+    /**
+     * 修改用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    public int updateCompanyUserShow(CompanyUserShow companyUserShow);
+
+    /**
+     * 删除用户展示字段
+     *
+     * @param id 用户展示字段ID
+     * @return 结果
+     */
+    public int deleteCompanyUserShowById(Long id);
+
+    /**
+     * 批量删除用户展示字段
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteCompanyUserShowByIds(Long[] ids);
+
+    CompanyUserShow selectShowByCompanyIdUserId(@Param("companyId") Long companyId,
+                                                @Param("userId") Long userId,
+                                                @Param("type") String type);
+
+    int updateByCompanyIdAndUserIdAndType(CompanyUserShow show);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/param/CompanyUserShowEditParam.java

@@ -0,0 +1,25 @@
+package com.fs.company.param;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CompanyUserShowEditParam {
+    /** Id */
+    private Long id;
+
+    /** companyId */
+    private Long companyId;
+
+    /** 用户id */
+    private Long userId;
+
+    /** 列表类型 */
+    private String type;
+
+    /** 列表字段 */
+    @Excel(name = "列表字段")
+    private List<String> columns;
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java

@@ -87,4 +87,6 @@ public interface ICompanyConfigService
      * @return
      */
     R saveCompanyMiniApp(SaveCompanyMiniAppParam param);
+
+    List<CompanyConfig> selectListByKey(String configKey);
 }

+ 66 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyUserShowService.java

@@ -0,0 +1,66 @@
+package com.fs.company.service;
+
+import com.fs.company.domain.CompanyUserShow;
+
+import java.util.List;
+
+/**
+ * 用户展示字段Service接口
+ *
+ * @author fs
+ * @date 2025-02-11
+ */
+public interface ICompanyUserShowService
+{
+    /**
+     * 查询用户展示字段
+     *
+     * @param id 用户展示字段ID
+     * @return 用户展示字段
+     */
+    public CompanyUserShow selectCompanyUserShowById(Long id);
+
+    /**
+     * 查询用户展示字段列表
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 用户展示字段集合
+     */
+    public List<CompanyUserShow> selectCompanyUserShowList(CompanyUserShow companyUserShow);
+
+    /**
+     * 新增用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    public int insertCompanyUserShow(CompanyUserShow companyUserShow);
+
+    /**
+     * 修改用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    public int updateCompanyUserShow(CompanyUserShow companyUserShow);
+
+    /**
+     * 批量删除用户展示字段
+     *
+     * @param ids 需要删除的用户展示字段ID
+     * @return 结果
+     */
+    public int deleteCompanyUserShowByIds(Long[] ids);
+
+    /**
+     * 删除用户展示字段信息
+     *
+     * @param id 用户展示字段ID
+     * @return 结果
+     */
+    public int deleteCompanyUserShowById(Long id);
+
+    CompanyUserShow selectShowByCompanyIdUserId(Long companyId, Long userId, String type);
+
+    int updateByCompanyIdAndUserIdAndType(CompanyUserShow show);
+}

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java

@@ -20,6 +20,7 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
@@ -261,4 +262,9 @@ public class CompanyConfigServiceImpl implements ICompanyConfigService
         }
         return R.ok();
     }
+
+    @Override
+    public List<CompanyConfig> selectListByKey(String configKey) {
+        return companyConfigMapper.selectListByKey(configKey);
+    }
 }

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

@@ -22,6 +22,7 @@ import com.fs.company.param.CompanyUserCodeParam;
 import com.fs.company.param.CompanyUserQwParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
@@ -136,6 +137,8 @@ public class CompanyUserServiceImpl implements ICompanyUserService
     @Autowired
     private  CompanyFsUserMapper companyFsUserMapper;
 
+//    @Autowired
+//    private CloudHostProper cloudHostProper;
 
     /**
      * 查询物业公司管理员信息
@@ -1045,6 +1048,7 @@ public class CompanyUserServiceImpl implements ICompanyUserService
     public R getBindInfo(Long companyUserId) {
         //链接
         String url = bindBaseUrl + companyUserId;
+//        String url = cloudHostProper.getBindBaseUrl() + companyUserId;
         if (CloudHostUtils.hasCloudHostName("木易华康")) {
             url = "https://h5api.muyikp.com/bindcompanyuser?companyUserId=" + companyUserId;
         }

+ 113 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyUserShowServiceImpl.java

@@ -0,0 +1,113 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyUserShow;
+import com.fs.company.mapper.CompanyUserShowMapper;
+import com.fs.company.service.ICompanyUserShowService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 用户展示字段Service业务层处理
+ *
+ * @author fs
+ * @date 2025-02-11
+ */
+@Service
+public class CompanyUserShowServiceImpl implements ICompanyUserShowService
+{
+    @Autowired
+    private CompanyUserShowMapper companyUserShowMapper;
+
+    /**
+     * 查询用户展示字段
+     *
+     * @param id 用户展示字段ID
+     * @return 用户展示字段
+     */
+    @Override
+    public CompanyUserShow selectCompanyUserShowById(Long id)
+    {
+        return companyUserShowMapper.selectCompanyUserShowById(id);
+    }
+
+    /**
+     * 查询用户展示字段列表
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 用户展示字段
+     */
+    @Override
+    public List<CompanyUserShow> selectCompanyUserShowList(CompanyUserShow companyUserShow)
+    {
+        return companyUserShowMapper.selectCompanyUserShowList(companyUserShow);
+    }
+
+    /**
+     * 新增用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    @Override
+    public int insertCompanyUserShow(CompanyUserShow companyUserShow)
+    {
+        companyUserShow.setCreateTime(DateUtils.getNowDate());
+        return companyUserShowMapper.insertCompanyUserShow(companyUserShow);
+    }
+
+    /**
+     * 修改用户展示字段
+     *
+     * @param companyUserShow 用户展示字段
+     * @return 结果
+     */
+    @Override
+    public int updateCompanyUserShow(CompanyUserShow companyUserShow)
+    {
+        companyUserShow.setUpdateTime(DateUtils.getNowDate());
+        return companyUserShowMapper.updateCompanyUserShow(companyUserShow);
+    }
+
+    /**
+     * 批量删除用户展示字段
+     *
+     * @param ids 需要删除的用户展示字段ID
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyUserShowByIds(Long[] ids)
+    {
+        return companyUserShowMapper.deleteCompanyUserShowByIds(ids);
+    }
+
+    /**
+     * 删除用户展示字段信息
+     *
+     * @param id 用户展示字段ID
+     * @return 结果
+     */
+    @Override
+    public int deleteCompanyUserShowById(Long id)
+    {
+        return companyUserShowMapper.deleteCompanyUserShowById(id);
+    }
+
+    @Override
+    public CompanyUserShow selectShowByCompanyIdUserId(Long companyId, Long userId, String type) {
+        return companyUserShowMapper.selectShowByCompanyIdUserId(companyId,userId,type);
+    }
+
+    @Override
+    public int updateByCompanyIdAndUserIdAndType(CompanyUserShow show) {
+        show.setUpdateTime(DateUtils.getNowDate());
+        int i = companyUserShowMapper.updateByCompanyIdAndUserIdAndType(show);
+        if (i<1){
+            //如果不存在则新增
+            i = insertCompanyUserShow(show);
+        }
+        return i;
+    }
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -1669,6 +1669,9 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         if (logs == null || logs.isEmpty()) {
             return new ArrayList<>();
         }
+        List<CompanyAiWorkflowExecLog> callLogs = logs.stream().filter(a -> "外呼".equals(a.getNodeName())).collect(Collectors.toList());
+        HashMap<Long,String> callContentMap = selectCallContentByCallLogs(callLogs);
+
         return logs.stream().map(log -> {
             WorkflowExecRecordVo.NodeExecLogVo vo = new WorkflowExecRecordVo.NodeExecLogVo();
             vo.setId(log.getId());
@@ -1683,7 +1686,26 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             vo.setDuration(log.getDuration());
             vo.setErrorMessage(log.getErrorMessage());
             vo.setOutputData(log.getOutputData());
+            vo.setNodeContentList(callContentMap.get(log.getId()));
             return vo;
         }).collect(Collectors.toList());
     }
+
+    /**
+     * 根据外呼记录得到外呼内容
+     * @param callLogs
+     * @return
+     */
+    public  HashMap<Long,String> selectCallContentByCallLogs(List<CompanyAiWorkflowExecLog> callLogs){
+        List<Long> ids = callLogs.stream().map(a -> a.getId()).collect(Collectors.toList());
+        List<CallContentVO> callContentVOS = companyAiWorkflowExecLogMapper.selectCallContent(ids);
+        if(null != callContentVOS && !callContentVOS.isEmpty()){
+            HashMap<Long,String> map = new HashMap<>();
+            callContentVOS.forEach(a -> map.put(a.getLogId(),a.getCallContent()));
+            return map;
+        }
+        else{
+            return new HashMap<>();
+        }
+    }
 }

+ 6 - 2
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -295,8 +295,12 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
         EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
         // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
-        createParam.setBatchName("workflow_" + context.getWorkflowInstanceId() + "_" + calleeId);
-        createParam.setThreadNum(100L);
+        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
+        if (null != callConfigVo.getMaxConcurrency())
+            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+        else {
+            createParam.setThreadNum(3L);
+        }
         // AI 外呼模式
         createParam.setTaskType(1);
         // 外呼线路(网关)

+ 3 - 0
fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java

@@ -64,4 +64,7 @@ public class AiCallConfigVO {
      * 外呼模式
      */
     private Integer callMode;
+
+    /** 最大并发数 */
+    private Integer maxConcurrency;
 }

+ 16 - 0
fs-service/src/main/java/com/fs/company/vo/CallContentVO.java

@@ -0,0 +1,16 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+/**
+ * @author MixLiu
+ * @date 2026/4/16 16:39
+ * @description
+ */
+@Data
+public class CallContentVO {
+
+    private Long logId;
+
+    private String callContent;
+}

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

@@ -201,5 +201,10 @@ public class WorkflowExecRecordVo {
          * 输出数据
          */
         private String outputData;
+
+        /**
+         * 外呼节点的对话记录
+         */
+        private String nodeContentList;
     }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/vo/easycall/EasyCallGatewayVO.java

@@ -25,4 +25,6 @@ public class EasyCallGatewayVO {
     private String gwDesc;
     /** 网关用途:2-AI外呼,3-不限制 */
     private Integer purpose;
+    /** 最大并发数 */
+    private Integer maxConcurrency;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java

@@ -13,6 +13,13 @@ public class CloudHostProper {
     @Value("${headerImg.imgUrl}")
     private String headerImg;
 
+
+    @Value("${headerImg.download_poster_url}")
+    private String downloadPosterUrl;
+
+//    @Value("${headerImg.bindBaseUrl}")
+//    private String bindBaseUrl;
+
     @Value("${cloud_host.projectCode}")
     private String projectCode;
 

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java

@@ -76,4 +76,6 @@ public class FsCourseRedPacketLog extends BaseEntity
     //商户号
     private String mchId;
 
+    private Integer watchType;
+
 }

+ 63 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseReward.java

@@ -0,0 +1,63 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励配置对象 fs_course_reward
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseReward extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 奖励名称 */
+    @Excel(name = "奖励名称")
+    private String name;
+
+    /** 奖励描述 */
+    @Excel(name = "奖励描述")
+    private String description;
+
+    /** 奖励类型 (1:宝箱, 2:红包, 3:积分, 4:转盘, 5:保底转盘) */
+    @Excel(name = "奖励类型 (1:宝箱, 2:红包, 3:积分, 4:转盘, 5:保底转盘)")
+    private Long rewardType;
+
+    /** 状态 (0:禁用, 1:启用) */
+    @Excel(name = "状态 (0:禁用, 1:启用)")
+    private Long status;
+
+    /** 期望值 */
+    @Excel(name = "期望值")
+    private String expectedValue;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 实际获得的奖励内容 */
+    @Excel(name = "实际获得的奖励内容")
+    private String actualRewards;
+
+    /**
+     * 关闭宝箱url
+     */
+    private String closeChestUrl;
+
+    /**
+     * 开启宝箱url
+     */
+    private String openChestUrl;
+
+    /**
+     * 奖励id
+     */
+    private Long [] rewardIds;
+}

+ 79 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRewardRound.java

@@ -0,0 +1,79 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励领取记录对象 reward_round
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseRewardRound extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 奖励ID */
+    @Excel(name = "奖励ID")
+    private Long rewardId;
+
+    /** 领取用户ID */
+    @Excel(name = "领取用户ID")
+    private Long userId;
+    private String userIdByName;
+
+    /** 奖励类型 */
+    @Excel(name = "奖励类型")
+    private Long rewardType;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+    private String companyName;
+
+    /** 实际领取到的奖励 */
+    @Excel(name = "实际领取到的奖励")
+    private String actualRewards;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 奖励状态 (0:已作废, 1:已领取, 2:已过期) */
+    @Excel(name = "奖励状态 (0:已作废, 1:已领取, 2:已过期)")
+    private Long status;
+
+    /** 规则id */
+    @Excel(name = "规则id")
+    private String ruleId;
+
+    /** 看课记录id */
+    @Excel(name = "看课记录id")
+    private Long watchId;
+
+    /** 奖励规则关联id */
+    @Excel(name = "奖励规则关联id")
+    private Long rewardVideoRelationId;
+    /**
+     * 时长
+     */
+    private String second;
+    /**
+     * 小节id
+     */
+    private Long videoId;
+    private String qwUserId;
+    private Long qwExternalId;
+
+    private Integer linkType;
+
+    /**
+     * 奖励商品ID
+     */
+    private Long goodsId;
+}

+ 47 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRewardVideoRelation.java

@@ -0,0 +1,47 @@
+package com.fs.course.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 奖励与视频小节关联关系对象 fs_course_reward_video_relation
+ *
+ * @author fs
+ * @date 2025-09-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsCourseRewardVideoRelation extends BaseEntity{
+
+    /** 关联关系ID */
+    private Long id;
+
+    /** 关联reward_.id */
+    @Excel(name = "关联reward_.id")
+    private Long rewardId;
+
+    /** 关联视频小节ID */
+    @Excel(name = "关联视频小节ID")
+    private Long videoSectionId;
+
+    /** 标志百分比 */
+    @Excel(name = "标志百分比")
+    private String mark;
+
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long createId;
+
+    /** 创建人ID */
+    @Excel(name = "创建人ID")
+    private Long updateId;
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    private Integer type;
+
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriodDays.java

@@ -80,6 +80,9 @@ public class FsUserCoursePeriodDays extends BaseEntityTow {
     @Excel(name = "开课状态;0未开始1一开始2已结束")
     private Integer status;
 
+    /** 删除状态0、正常,1、已删除 */
+    private String delFlag;
+
     @TableField(exist = false)
     private List<Long> videoIds;
     @TableField(exist = false)

+ 75 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardRoundMapper.java

@@ -0,0 +1,75 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.vo.RedPacketMoneyVO;
+import com.fs.course.domain.FsCourseRewardRound;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * 奖励领取记录Mapper接口
+ *
+ * @author 杨衍生
+ * @date 2025-09-02
+ */
+public interface FsCourseRewardRoundMapper extends BaseMapper<FsCourseRewardRound>{
+    /**
+     * 查询奖励领取记录
+     *
+     * @param id 奖励领取记录主键
+     * @return 奖励领取记录
+     */
+    FsCourseRewardRound selectFsCourseRewardRoundById(Long id);
+
+    /**
+     * 查询奖励领取记录列表
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 奖励领取记录集合
+     */
+    List<FsCourseRewardRound> selectFsCourseRewardRoundList(FsCourseRewardRound rewardRound);
+
+    /**
+     * 新增奖励领取记录
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 结果
+     */
+    int insertFsCourseRewardRound(FsCourseRewardRound rewardRound);
+
+    /**
+     * 修改奖励领取记录
+     *
+     * @param rewardRound 奖励领取记录
+     * @return 结果
+     */
+    int updateFsCourseRewardRound(FsCourseRewardRound rewardRound);
+
+    /**
+     * 删除奖励领取记录
+     *
+     * @param id 奖励领取记录主键
+     * @return 结果
+     */
+    int deleteFsCourseRewardRoundById(Long id);
+
+    /**
+     * 批量删除奖励领取记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseRewardRoundByIds(Long[] ids);
+
+    List<RedPacketMoneyVO> selectFsCourseRewardRoundByAmount(@Param("start") String start,@Param("end") String end);
+
+    List<RedPacketMoneyVO> selectFsCourseRewardRoundByAmountForLuckyBag(@Param("start") String start,@Param("end") String end);
+
+    /**
+     * 查询1000条指定状态且小于指定时间的数据
+     */
+    List<FsCourseRewardRound> get1kByStatusAndLtDate(@Param("status") int status, @Param("endTime") LocalDate endTime);
+
+}

+ 87 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardVideoRelationMapper.java

@@ -0,0 +1,87 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsCourseReward;
+import com.fs.course.domain.FsCourseRewardVideoRelation;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+
+/**
+ * 奖励与视频小节关联关系Mapper接口
+ *
+ * @author fs
+ * @date 2025-09-03
+ */
+public interface FsCourseRewardVideoRelationMapper extends BaseMapper<FsCourseRewardVideoRelation>{
+    /**
+     * 查询奖励与视频小节关联关系
+     *
+     * @param id 奖励与视频小节关联关系主键
+     * @return 奖励与视频小节关联关系
+     */
+    FsCourseRewardVideoRelation selectFsCourseRewardVideoRelationById(Long id);
+
+    /**
+     * 查询奖励与视频小节关联关系列表
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 奖励与视频小节关联关系集合
+     */
+    List<FsCourseRewardVideoRelation> selectFsCourseRewardVideoRelationList(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 新增奖励与视频小节关联关系
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 结果
+     */
+    int insertFsCourseRewardVideoRelation(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 修改奖励与视频小节关联关系
+     *
+     * @param fsCourseRewardVideoRelation 奖励与视频小节关联关系
+     * @return 结果
+     */
+    int updateFsCourseRewardVideoRelation(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 删除奖励与视频小节关联关系
+     *
+     * @param id 奖励与视频小节关联关系主键
+     * @return 结果
+     */
+    int deleteFsCourseRewardVideoRelationById(Long id);
+
+    /**
+     * 批量删除奖励与视频小节关联关系
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsCourseRewardVideoRelationByIds(Long[] ids);
+
+    /**
+     * 根据公司ID、小节ID和类型删除关系
+     */
+    void deleteByTypes(@Param("videoId") Long videoId, @Param("companyId") Long companyId, @Param("types") List<Integer> types);
+
+    /**
+     * 根据公司ID、小节ID和类型查询奖励ID
+     */
+    Long getRewardIdByCompanyIdAndVideoIdAndType(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("type") int type);
+
+    /**
+     * 根据公司ID、小节ID和类型查询奖励配置
+     */
+    FsCourseReward getRewardByCompanyIdAndVideoIdAndType(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("type") Integer type);
+
+    List<FsCourseRewardVideoRelation> selectFsCourseRewardVideoRelationListByType(FsCourseRewardVideoRelation fsCourseRewardVideoRelation);
+
+    /**
+     * 根据公司ID、小节ID和奖励ID查询配置关系
+     */
+    FsCourseRewardVideoRelation selectByCompanyIdAndVideoIdAndRewardId(@Param("companyId") Long companyId, @Param("videoId") Long videoId, @Param("rewardId") Long rewardId);
+}

+ 10 - 3
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -113,8 +113,8 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     @Select("select * from fs_course_watch_log " +
             "where user_id = #{userId} " +
             "and video_id = #{videoId} " +
-            "and qw_user_id = #{qwUserId} " +
-            "and qw_external_contact_id = #{externalId}")
+            "and qw_external_contact_id = #{externalId} " +
+            "and qw_user_id = #{qwUserId} " )
     FsCourseWatchLog getWatchCourseVideo(@Param("userId") Long userId, @Param("videoId") Long videoId,@Param("qwUserId") String qwUserId,@Param("externalId") Long externalId);
 
     @Select("select * from fs_course_watch_log " +
@@ -910,6 +910,13 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
     List<AppSalesWatchLogReportVO> selectAppDeptOrderStats(FsCourseWatchLogStatisticsListParam param);
 
+    @Select("select * from fs_course_watch_log " +
+            "where video_id = #{videoId} " +
+            "and company_user_id = #{companyUserId} " +
+            "and user_id = #{userId}  order by create_time desc limit 1")
+    FsCourseWatchLog getWatchCourseVideoByFsUserNew(@Param("userId") Long userId, @Param("videoId") Long videoId, @Param("companyUserId") Long companyUserId);
+
+
     /**
      * 获取过期数据
      * **/
@@ -920,7 +927,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "    SELECT user_id, MAX(create_time) \n" +
             "    FROM fs_course_watch_log \n" +
             "    WHERE send_type = 1 \n" +
-            "      AND create_time <= DATE_SUB(NOW(), INTERVAL 15 DAY)\n" +
+            "      \n" +
             "    GROUP BY user_id\n" +
             "  )")
     List<Long> selectWatchLogOutdatedInfo();

+ 1 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java

@@ -196,7 +196,7 @@ public interface FsUserCourseMapper
     List<FsUserCourseListUVO> selectFsUserCourseCommentListUVO(@Param("maps") FsUserCourseListUParam param);
 
 
-    @Select("select v.video_id,v.title,v.course_id,v.video_url,v.question_bank_id," +
+    @Select("select v.video_id,v.title,v.course_id,v.video_url,v.question_bank_id,v.is_del,v.is_on_put," +
             "SEC_TO_TIME(c.duration) as total_duration,c.views,c.course_name,c.description,c.img_url,c.config_json  " +
             " from fs_user_course_video v " +
             "left join fs_user_course c on v.course_id = c.course_id " +

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

@@ -36,6 +36,7 @@ public class FsCourseSendRewardUParam implements Serializable
     private Long periodId;
     @NotBlank(message = "小程序参数不能为空")
     private String appId; //前端传来的小程序的appid
+    private Integer rewardType; //奖励类型 1红包 2积分 3随机转盘 4保底转盘
 
     private String code;
 

+ 5 - 6
fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoRemainTimeParam.java

@@ -8,28 +8,27 @@ import java.io.Serializable;
 
 @Data
 public class FsUserCourseVideoRemainTimeParam implements Serializable {
-    @NotNull(message = "视频id不能为空")
     @ApiModelProperty(value = "视频id")
     private Integer videoId;
 
-    @NotNull(message = "用户id不能为空")
     @ApiModelProperty(value = "用户id")
     private Long fsUserId;
 
-    @NotNull(message = "课程id不能为空")
     @ApiModelProperty(value = "课程id")
     private Integer courseId;
 
-    @NotNull(message = "销售id不能为空")
     @ApiModelProperty(value = "销售id")
     private Long companyUserId;
 
-    @NotNull(message = "项目id不能为空")
     @ApiModelProperty(value = "项目id")
     private Long projectId;
 
-    @NotNull(message = "营期id不能为空")
     @ApiModelProperty(value = "营期id")
     private Long periodId;
 
+    @ApiModelProperty(value = "外部联系人id")
+    private Long qwExternalId;
+    @ApiModelProperty(value = "企微用户id")
+    private Long qwUserId;
+
 }

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

@@ -283,4 +283,9 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
      * @return
      */
     R createAppFd(LuckyBagCollectRecord param);
+
+    /**
+    * app发红包
+    */
+    R withdrawal(FsCourseSendRewardUParam param);
 }

+ 114 - 26
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -33,10 +33,8 @@ import com.fs.his.vo.AppWatchLogReportVO;
 import com.fs.his.vo.WatchLogReportVO;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.dto.FsStoreCartDTO;
-import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
-import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
@@ -185,9 +183,6 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
 
-    @Autowired
-    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
-
     @Autowired
     private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
@@ -1889,29 +1884,10 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             Long redCount = fsCourseRedPacketLogMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId,companyId,companyUserId);
             vo.setRedPacketUserCount(redCount != null ? redCount : 0L);
 
-            // 单品销量统计:从订单明细汇总
+            // 单品销量统计:使用订单主表 itemJson 明细,按 payPrice 分摊(与 GMV 一致,避免逐单查库)
             Map<Long, CourseProductSalesVO> productSalesMap = new HashMap<>();
             for (FsStoreOrderScrm order : paidOrders) {
-                // todo 数据量大的时候需要优化查询 外面批量查询 里面数据过滤
-                List<FsStoreOrderItemVO> items = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
-                if (items == null || items.isEmpty()) continue;
-                long totalNum = order.getTotalNum() != null && order.getTotalNum() > 0 ? order.getTotalNum() : 1;
-//                BigDecimal orderPayPrice = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
-
-                for (FsStoreOrderItemVO item : items) {
-                    FsStoreCartDTO cartDTO = JSONUtil.toBean(item.getJsonInfo(), FsStoreCartDTO.class);
-                    if (item.getProductId() == null) continue;
-                    long itemNum = item.getNum() != null ? item.getNum() : 0;
-                    BigDecimal itemAmount = totalNum > 0 ? cartDTO.getPrice().multiply(BigDecimal.valueOf(itemNum)) : BigDecimal.ZERO;
-                    CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(item.getProductId(), k -> {
-                        CourseProductSalesVO pvo = new CourseProductSalesVO();
-                        pvo.setProductName(cartDTO.getProductName());
-                        return pvo;
-                    });
-
-                    productSales.setSalesCount(productSales.getSalesCount() + itemNum);
-                    productSales.setSalesAmount(productSales.getSalesAmount().add(itemAmount));
-                }
+                accumulateProductSalesFromPaidOrder(productSalesMap, order);
             }
             List<CourseProductSalesVO> productList = new ArrayList<>(productSalesMap.values());
             productList.sort((a, b) -> b.getSalesAmount().compareTo(a.getSalesAmount()));
@@ -2370,6 +2346,118 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
             vo.setVideoTitle("");
         }
     }
+
+    /**
+     * 将单笔已支付订单的 payPrice 按明细「单价×数量」占比分摊到各商品;无有效标价时按行数均分。
+     * 明细来自订单主表 {@link FsStoreOrderScrm#getItemJson()}(与行表结构一致:productId、num、jsonInfo 等)。
+     * 每笔订单分摊之和严格等于该笔 payPrice,故多笔订单汇总后各商品 salesAmount 之和等于 GMV。
+     */
+    private void accumulateProductSalesFromPaidOrder(Map<Long, CourseProductSalesVO> productSalesMap, FsStoreOrderScrm order) {
+        String itemJson = order.getItemJson();
+        if (itemJson == null || itemJson.trim().isEmpty()) {
+            return;
+        }
+        JSONArray items;
+        try {
+            items = JSON.parseArray(itemJson);
+        } catch (Exception e) {
+            return;
+        }
+        if (items == null || items.isEmpty()) {
+            return;
+        }
+        BigDecimal orderPay = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+        List<Long> pids = new ArrayList<>();
+        List<Long> nums = new ArrayList<>();
+        List<BigDecimal> raws = new ArrayList<>();
+        List<String> names = new ArrayList<>();
+        for (int j = 0; j < items.size(); j++) {
+            JSONObject o = items.getJSONObject(j);
+            if (o == null) {
+                continue;
+            }
+            Long productId = o.getLong("productId");
+            if (productId == null) {
+                continue;
+            }
+            long itemNum = 0L;
+            if (o.get("num") != null) {
+                try {
+                    itemNum = o.getLongValue("num");
+                } catch (Exception e) {
+                    itemNum = 0L;
+                }
+            }
+            String jsonInfo = o.getString("jsonInfo");
+            FsStoreCartDTO cartDTO = null;
+            try {
+                if (jsonInfo != null && !jsonInfo.isEmpty()) {
+                    cartDTO = JSONUtil.toBean(jsonInfo, FsStoreCartDTO.class);
+                }
+            } catch (Exception ignored) {
+            }
+            BigDecimal unit = (cartDTO != null && cartDTO.getPrice() != null) ? cartDTO.getPrice() : BigDecimal.ZERO;
+            BigDecimal raw = unit.multiply(BigDecimal.valueOf(itemNum));
+            pids.add(productId);
+            nums.add(itemNum);
+            raws.add(raw);
+            names.add(cartDTO != null ? cartDTO.getProductName() : null);
+        }
+        int n = pids.size();
+        if (n == 0) {
+            return;
+        }
+        BigDecimal rawSum = BigDecimal.ZERO;
+        for (BigDecimal r : raws) {
+            rawSum = rawSum.add(r);
+        }
+        int lastPos = -1;
+        for (int i = n - 1; i >= 0; i--) {
+            if (raws.get(i).compareTo(BigDecimal.ZERO) > 0) {
+                lastPos = i;
+                break;
+            }
+        }
+        BigDecimal[] allocs = new BigDecimal[n];
+        Arrays.fill(allocs, BigDecimal.ZERO);
+        if (rawSum.compareTo(BigDecimal.ZERO) > 0 && lastPos >= 0) {
+            BigDecimal used = BigDecimal.ZERO;
+            for (int i = 0; i < n; i++) {
+                BigDecimal raw = raws.get(i);
+                if (raw.compareTo(BigDecimal.ZERO) <= 0) {
+                    allocs[i] = BigDecimal.ZERO;
+                } else if (i == lastPos) {
+                    allocs[i] = orderPay.subtract(used);
+                } else {
+                    allocs[i] = orderPay.multiply(raw).divide(rawSum, 8, RoundingMode.HALF_UP);
+                    used = used.add(allocs[i]);
+                }
+            }
+        } else {
+            BigDecimal used = BigDecimal.ZERO;
+            BigDecimal share = orderPay.divide(BigDecimal.valueOf(n), 8, RoundingMode.HALF_UP);
+            for (int i = 0; i < n; i++) {
+                if (i == n - 1) {
+                    allocs[i] = orderPay.subtract(used);
+                } else {
+                    allocs[i] = share;
+                    used = used.add(share);
+                }
+            }
+        }
+        for (int i = 0; i < n; i++) {
+            Long pid = pids.get(i);
+            final int idx = i;
+            CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(pid, k -> {
+                CourseProductSalesVO pvo = new CourseProductSalesVO();
+                pvo.setProductName(names.get(idx));
+                return pvo;
+            });
+            productSales.setSalesCount(productSales.getSalesCount() + nums.get(i));
+            productSales.setSalesAmount(productSales.getSalesAmount().add(allocs[i]));
+        }
+    }
+
     /**
      * 从 Map 中安全获取 Long 值,兼容 MyBatis 返回的驼峰/小写键名
      */

+ 12 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.date.DateUtil;
@@ -154,7 +155,11 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
     @Override
     public R addCourse(FsUserCoursePeriodDays entity) {
         FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(entity.getPeriodId());
-        List<FsUserCoursePeriodDays> dayList = list(new QueryWrapper<FsUserCoursePeriodDays>().eq("period_id", period.getPeriodId()).eq("del_flag","0"));
+        // 查询所有的,包括删除的(后续需要),将原来定义的dayList 筛选出来,避免查询两次
+        List<FsUserCoursePeriodDays> dayAllList = list(new QueryWrapper<FsUserCoursePeriodDays>().eq("period_id", period.getPeriodId()));
+        List<FsUserCoursePeriodDays> dayList = dayAllList.stream()
+                .filter(day -> day.getDelFlag() == null || !"1".equals(day.getDelFlag()))
+                .collect(Collectors.toList());
         long days;
         if(period.getPeriodType() == 2){
             days = 1;
@@ -173,7 +178,12 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
             FsUserCourseVideo video = videoMap.get(e);
             FsUserCoursePeriodDays day = new FsUserCoursePeriodDays();
             day.setPeriodId(entity.getPeriodId());
-            day.setLesson(dayList.size() + i.getAndIncrement());
+            // 叮当国医,需要忽略被删除的
+            if(CloudHostUtils.hasCloudHostName("叮当国医")){
+                day.setLesson(dayAllList.size() + i.getAndIncrement());
+            } else {
+                day.setLesson(dayList.size() + i.getAndIncrement());
+            }
             day.setDayDate(period.getPeriodStartingTime().plusDays(day.getLesson()));
             day.setCourseId(entity.getCourseId());
             if(entity.getStartTime() != null){

+ 622 - 4
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -49,13 +49,15 @@ import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.utils.luckyDraw.LotteryUtil;
+import com.fs.course.utils.luckyDraw.Prize;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.*;
 import com.fs.enums.ExceptionCodeEnum;
 import com.fs.his.config.AppConfig;
-import com.fs.his.domain.FsUser;
-import com.fs.his.domain.FsUserIntegralLogs;
-import com.fs.his.domain.FsUserWx;
+import com.fs.his.domain.*;
+import com.fs.his.mapper.FsCouponMapper;
+import com.fs.his.mapper.FsUserCouponMapper;
 import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.WxSendRedPacketParam;
@@ -177,7 +179,6 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
-
     @Autowired
     private RedissonClient redissonClient;
     @Autowired
@@ -238,6 +239,14 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private IFsUserCompanyUserService userCompanyUserService;
     @Autowired
     private SysDictDataMapper dictDataMapper;
+    @Autowired
+    private FsCourseRewardVideoRelationMapper videoRelationMapper;
+    @Autowired
+    private FsCourseRewardRoundMapper roundMapper;
+    @Autowired
+    private FsUserCouponMapper fsUserCouponMapper;
+    @Autowired
+    private FsCouponMapper fsCouponMapper;
 
     @Autowired
     private FsCourseAnswerLogsMapper courseAnswerLogsMapper;
@@ -2693,6 +2702,15 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 return ResponseResult.fail(ExceptionCodeEnum.WATCH_LATEST_COURSE.getCode(), ExceptionCodeEnum.WATCH_LATEST_COURSE.getDescription());
             }
         }
+
+        if ("北京卓美".equals(signProjectName)) {
+            FsUserCourseVideoH5VO checkVideo = fsUserCourseMapper.selectFsUserCourseVideoH5VOByVideoId(param.getVideoId());
+            if (checkVideo == null
+                    || (checkVideo.getIsDel() != null && checkVideo.getIsDel() == 1)
+                    || (checkVideo.getIsOnPut() != null && checkVideo.getIsOnPut() == 1)) {
+                return ResponseResult.fail(ExceptionCodeEnum.VIDEO_EXPIRED.getCode(), ExceptionCodeEnum.VIDEO_EXPIRED.getDescription());
+            }
+        }
         // 项目看课数限制
         if (!EXCLUDE_PROJECTS.contains(signProjectName) && !CloudHostUtils.hasCloudHostName("弘德堂")) {
             log.error("进入了看课限制:传入参数:={},watchCourseVideo={}",param, watchCourseVideo);
@@ -4805,6 +4823,43 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
     }
 
+    /**
+     * 用户提现
+     * @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 Map<String,Object> addLuckyBagCollectRecord(Long luckyBagId,
                                                         Long userId,
@@ -4911,6 +4966,569 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
 
+    private R executeWithdrawal(FsCourseSendRewardUParam param) {
+        log.info("进入用户判断");
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user == null) {
+            return R.error("未识别到用户信息");
+        }
+
+        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByFsUserNew(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
+        if (log == null) {
+            return R.error("无记录");
+        }
+
+        if (log.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 (log.getRewardType() != null) {
+            if (log.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 (log.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);
+
+                return sendAppRedPacket(packetParam, log,video, config);
+            // 积分奖励
+            case 2:
+                return sendIntegralReward(param, user, log, config);
+            // 转盘
+            case 3:
+                return drawTurntable(param, user, log);
+            // 保底转盘
+            case 4:
+                return drawTurntableGuarantee(param, user, log);
+            default:
+                return R.error("参数错误!");
+        }
+    }
+
+    private R drawTurntable(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog) {
+        log.debug("转盘抽奖 param: {}, user: {}, watchLog: {}",
+                JSON.toJSONString(param),JSON.toJSONString(user),JSON.toJSONString(watchLog));
+        return draw(param, user, watchLog, 4);
+    }
+
+    /**
+     * 保底转盘
+     */
+    private R drawTurntableGuarantee(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog) {
+        log.debug("保底转盘 param: {}, user: {}, watchLog: {}",
+                JSON.toJSONString(param),JSON.toJSONString(user),JSON.toJSONString(watchLog));
+        return draw(param, user, watchLog, 5);
+    }
+
+    /**
+     * 抽奖
+     */
+    private R draw(FsCourseSendRewardUParam param, FsUser user, FsCourseWatchLog watchLog, Integer drawType) {
+        FsCourseReward rewardConfig = videoRelationMapper.getRewardByCompanyIdAndVideoIdAndType(watchLog.getCompanyId(), watchLog.getVideoId(), drawType);
+        String typeName = drawType == 4 ? "转盘" : "保底转盘";
+        if (Objects.isNull(rewardConfig)) {
+            throw new CustomException("销售公司未配置"+ typeName +",请选择其他奖励类型");
+        }
+
+        if (StringUtils.isBlank(rewardConfig.getActualRewards())) {
+            throw new CustomException(typeName + "配置错误1,请联系管理员");
+        }
+
+        // 解析JSON配置
+        JSONArray jsonArray;
+        try {
+            jsonArray = JSON.parseArray(rewardConfig.getActualRewards());
+        } catch (Exception e) {
+            log.error("解析{}配置JSON失败", typeName, e);
+            throw new CustomException(typeName + "配置错误:JSON格式不正确");
+        }
+
+        if (jsonArray == null || jsonArray.isEmpty()) {
+            throw new CustomException(typeName + "配置错误:奖品列表为空");
+        }
+
+        // 配置关系
+        FsCourseRewardVideoRelation relation = videoRelationMapper.selectByCompanyIdAndVideoIdAndRewardId(watchLog.getCompanyId(), watchLog.getVideoId(), rewardConfig.getId());
+
+        List<Prize> prizes = buildPrizes(jsonArray, param, rewardConfig.getId(), drawType);
+        Prize prize = LotteryUtil.draw(prizes);
+        if (Objects.isNull(prize)) {
+            throw new CustomException(typeName + "无可抽取奖励");
+        }
+
+        FsCourseRewardRound rewardRound = new FsCourseRewardRound();
+        rewardRound.setRewardId(rewardConfig.getId());
+        rewardRound.setUserId(user.getUserId());
+        rewardRound.setRewardType(rewardConfig.getRewardType());
+        rewardRound.setCompanyId(watchLog.getCompanyId());
+        rewardRound.setActualRewards(prize.getAmount());
+        rewardRound.setCreateTime(new Date());
+        rewardRound.setStatus(1L);
+        rewardRound.setWatchId(watchLog.getLogId());
+        rewardRound.setRuleId(prize.getCode());
+        rewardRound.setRewardVideoRelationId(relation.getId());
+
+        // 抽中奖励商品
+        if (prize.getType() == 5) {
+            rewardRound.setGoodsId(Long.parseLong(prize.getGoodsId()));
+        }
+
+        roundMapper.insertFsCourseRewardRound(rewardRound);
+
+        // 获取配置信息
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(param.getVideoId());
+        switch (prize.getType()) {
+            case 1:
+                //来源是小程序切换openId
+                WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
+                String openId = getOpenId(param, user);
+                if (StringUtils.isBlank(openId)) {
+                    return R.error("请重新使用微信登录");
+                }
+                packetParam.setOpenId(openId);
+                packetParam.setAmount(new BigDecimal(prize.getAmount()));
+                packetParam.setSource(param.getSource());
+                packetParam.setAppId(param.getAppId());
+                return sendAppRedPacket(packetParam, watchLog,video, config);
+            case 2:
+                return sendIntegralReward(param, user, watchLog, config);
+            case 3:
+                return R.ok().put("data", prize.getCode());
+            case 4:
+                return sendCoupon(param, prize.getCouponId(), Integer.parseInt(prize.getAmount())).put("data", prize.getCode());
+            case 5:
+                Map<String, Object> result = new HashMap<>();
+                result.put("roundId", rewardRound.getId());
+                result.put("goodsId", prize.getGoodsId());
+                result.put("data", prize.getCode());
+                return R.ok(result);
+            default:
+                return R.error(typeName + "配置错误4,请联系管理员");
+        }
+    }
+
+
 
+    /**
+     * 发送优惠券
+     */
+    private R sendCoupon(FsCourseSendRewardUParam param, String couponId, Integer num) {
+        log.debug("发送优惠券 param: {}, couponId: {}, num: {}", JSON.toJSONString(param), couponId, num);
+
+        FsCoupon coupon = fsCouponMapper.selectFsCouponByCouponId(Long.parseLong(couponId));
+        //不存在
+        if (coupon == null) {
+            return R.error("优惠券不存在");
+        }
+        //停用
+        if (coupon.getStatus()==0) {
+            return R.error("优惠券不存在");
+        }
+
+        FsUserCoupon fsUserCoupon = new FsUserCoupon();
+        fsUserCoupon.setCouponId(coupon.getCouponId());
+        fsUserCoupon.setCouponCode("C"+System.currentTimeMillis());
+        fsUserCoupon.setUserId(param.getUserId());
+        fsUserCoupon.setCreateTime(DateUtils.getNowDate());
+        if (coupon.getLimitType() == 2){
+            long limitDay = coupon.getLimitDay().longValue() * 24 * 60 * 60 * 1000;
+            long time = new Date().getTime();
+            fsUserCoupon.setLimitTime(new Date(limitDay+time));
+        }else {
+            fsUserCoupon.setLimitTime(coupon.getLimitTime());
+        }
+        fsUserCoupon.setStatus(0);
+        fsUserCouponMapper.insertFsUserCoupon(fsUserCoupon);
+        return R.ok("奖励发放成功");
+    }
+
+    private List<Prize> buildPrizes(JSONArray jsonArray, FsCourseSendRewardUParam param, Long rewardId, Integer drawType) {
+        List<Prize> prizes = new ArrayList<>();
+
+        // 如果是保底转盘,需要查询已领取记录
+        List<FsCourseRewardRound> rounds = new ArrayList<>();
+        if (drawType == 5) { // 保底转盘
+            FsCourseRewardRound query = new FsCourseRewardRound();
+            query.setUserId(param.getUserId());
+            query.setCompanyId(param.getCompanyId());
+            query.setRewardId(rewardId);
+            rounds = roundMapper.selectFsCourseRewardRoundList(query);
+        }
+
+        Set<String> claimedCodes = rounds.stream()
+                .map(FsCourseRewardRound::getRuleId)
+                .collect(Collectors.toSet());
+
+        for (Object o : jsonArray) {
+            try {
+                JSONObject json = (JSONObject) o;
+
+                Integer type = json.getInteger("type");
+                String amount = json.getString("amount");
+                String code = json.getString("code");
+                String couponId = json.getString("couponId");
+
+                // 安全解析概率
+                double probability = parseProbability(json.getString("probability"));
+                if (probability <= 0) {
+                    log.warn("奖品概率配置错误,跳过: {}", json);
+                    continue;
+                }
+
+                // 保底转盘特殊逻辑
+                if (drawType == 5) {
+                    // 跳过已领取的
+                    if (claimedCodes.contains(code)) {
+                        continue;
+                    }
+
+                    // 保底奖品逻辑
+                    Boolean isGuarantee = json.getBoolean("isGuarantee");
+                    if (Boolean.TRUE.equals(isGuarantee) && jsonArray.size() - rounds.size() > 5) {
+                        continue;
+                    }
+                }
+
+                // APP跳过现金红包
+                if (param.getSource() == 3 && type == 1) {
+                    continue;
+                }
+
+                prizes.add(new Prize(type, amount, code, probability, couponId, null));
+
+            } catch (Exception e) {
+                log.warn("解析奖品配置失败,跳过: {}", JSON.toJSONString(o), e);
+            }
+        }
+
+        return prizes;
+    }
+
+
+    /**
+     * 安全解析概率值
+     */
+    private double parseProbability(String probabilityStr) {
+        try {
+            if (StringUtils.isBlank(probabilityStr)) {
+                return -1;
+            }
+
+            // 移除百分号并转换
+            String cleanStr = probabilityStr.replace("%", "").trim();
+            return Double.parseDouble(cleanStr);
+        } catch (NumberFormatException e) {
+            log.error("概率格式错误: {}", probabilityStr, e);
+            return -1;
+        }
+    }
+
+
+    private R sendAppRedPacket(WxSendRedPacketParam packetParam,FsCourseWatchLog log,FsUserCourseVideo video,CourseConfig config) {
+        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 != null && periodDays.getLastJoinTime() != null && LocalDateTime.now().isAfter(periodDays.getLastJoinTime())) {
+            return R.error(403, "已超过领取红包时间");
+        }
+
+
+        // 确定红包金额
+        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());
+                    redPacketLog.setWatchType(1);
+
+                    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("答题成功!");
+        }
+    }
+
+    /**
+     * 获取用户openId
+     */
+    private String getOpenId(FsCourseSendRewardUParam param, FsUser user) {
+        if (param.getSource()==2){
+            FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),user.getUserId(),1);
+            if (Objects.nonNull(fsUserWx) && StringUtils.isNotBlank(fsUserWx.getOpenId())) {
+                return fsUserWx.getOpenId();
+            }
+
+            if (StringUtils.isNotBlank(user.getCourseMaOpenId())) {
+                try {
+                    handleFsUserWx(user,param.getAppId());
+                } catch (Exception e){
+                    log.error("【更新或插入用户与小程序的绑定关系失败】:{}", user.getUserId(), e);
+                }
+                return user.getCourseMaOpenId();
+            }
+        } else if (param.getSource()==3){
+            return user.getAppOpenId();
+        }
+
+        return user.getMpOpenId();
+    }
 }
 

+ 37 - 0
fs-service/src/main/java/com/fs/course/utils/luckyDraw/LotteryUtil.java

@@ -0,0 +1,37 @@
+package com.fs.course.utils.luckyDraw;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class LotteryUtil {
+
+    /**
+     * 根据奖品配置抽奖
+     *
+     * @param prizes 奖品列表
+     * @return 中奖的奖品
+     */
+    public static Prize draw(List<Prize> prizes) {
+        if (prizes == null || prizes.isEmpty()) {
+            return null;
+        }
+
+        // 计算总概率和
+        double total = prizes.stream().mapToDouble(Prize::getProbability).sum();
+
+        // [0, total) 之间取随机数
+        double random = ThreadLocalRandom.current().nextDouble() * total;
+
+        // 逐步累加找到落点
+        double sum = 0;
+        for (Prize prize : prizes) {
+            sum += prize.getProbability();
+            if (random < sum) {
+                return prize;
+            }
+        }
+
+        // 理论上不会到这里
+        return prizes.get(prizes.size() - 1);
+    }
+}

+ 77 - 0
fs-service/src/main/java/com/fs/course/utils/luckyDraw/Prize.java

@@ -0,0 +1,77 @@
+package com.fs.course.utils.luckyDraw;
+
+
+public class Prize {
+
+    private Integer type;
+    private String amount;
+    private String code;
+    private double probability;
+    private String couponId;
+    private String goodsId;
+
+    public Prize(Integer type, String amount, String code, double probability, String couponId, String goodsId) {
+        this.type = type;
+        this.amount = amount;
+        this.code = code;
+        this.probability = probability;
+        this.couponId = couponId;
+        this.goodsId = goodsId;
+    }
+
+    public Prize(Integer type, String amount, String code, double probability, String couponId) {
+        this.type = type;
+        this.amount = amount;
+        this.code = code;
+        this.probability = probability;
+        this.couponId = couponId;
+    }
+
+    public Integer getType() {
+        return type;
+    }
+
+    public void setType(Integer type) {
+        this.type = type;
+    }
+
+    public String getAmount() {
+        return amount;
+    }
+
+    public void setAmount(String amount) {
+        this.amount = amount;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public double getProbability() {
+        return probability;
+    }
+
+    public void setProbability(double probability) {
+        this.probability = probability;
+    }
+
+    public String getCouponId() {
+        return couponId;
+    }
+
+    public void setCouponId(String couponId) {
+        this.couponId = couponId;
+    }
+
+    public String getGoodsId() {
+        return goodsId;
+    }
+
+    public void setGoodsId(String goodsId) {
+        this.goodsId = goodsId;
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoH5DVO.java

@@ -64,4 +64,13 @@ public class FsUserCourseVideoH5DVO extends BaseEntity
      * 商品列表
      */
     private List<FsStoreProductScrm> fsStoreProductScrms;
+
+
+    /**
+     * 视频展示类型:landscape-横屏,portrait-竖屏
+     */
+    private String displayType;
+
+    private String fileKey;//文件key 对用存储桶
+
 }

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

@@ -38,4 +38,9 @@ public class FsUserCourseVideoH5VO extends BaseEntity
     /** 课堂配置 **/
     private String configJson;
 
+    /** 是否删除 **/
+    private Integer isDel;
+    /** 是否上下架 **/
+    private Integer isOnPut;
+
 }

+ 339 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmBusiness.java

@@ -0,0 +1,339 @@
+package com.fs.crm.domain;
+
+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;
+
+import java.util.Date;
+
+/**
+ * 商机对象 crm_business
+ *
+ * @author fs
+ * @date 2025-01-16
+ */
+public class CrmBusiness extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 商机id */
+    private Long businessId;
+
+    /** 客户id */
+    @Excel(name = "客户id")
+    private Long customerId;
+
+    /** 线索来源
+    0企查询
+    1企查查
+    2领鸟云
+    3其他 */
+    @Excel(name = "线索来源 0企查询 1企查查 2领鸟云 3其他")
+    private Integer source;
+
+    /** 客户经理 */
+    @Excel(name = "客户经理")
+    private String manager;
+
+    /** 公司名称(只填公司全称,个人用户就填名字) */
+    @Excel(name = "公司名称(只填公司全称,个人用户就填名字)")
+    private String companyName;
+
+
+
+    /** 接口人角色 0老板,1采购,2财务,3技术负责人,4一般技术人员,5项目负责人,6其他 */
+    @Excel(name = "接口人角色 0老板,1采购,2财务,3技术负责人,4一般技术人员,5项目负责人,6其他")
+    private Integer contactRole;
+
+    /** 业务场景网站,电商,游戏,小程序,APP,OA,ERP,CRM,物联网,AI人工智能,国产化,数字人,内部办公系统,等保,其他 */
+    @Excel(name = "业务场景")
+    private String businessScenario;
+
+    /** 方案涉及的产品 */
+    @Excel(name = "方案涉及的产品")
+    private String product;
+
+    /** 采购周期(单位:天) */
+    @Excel(name = "采购周期(单位:天)")
+    private Long purchaseCycle;
+
+    /** 跟进状态:0跟进中,1已流失,2已赢单,3待验证 */
+    @Excel(name = "跟进状态:0跟进中,1已流失,2已赢单,3待验证")
+    private Integer businessStatus;
+
+
+    @Excel(name = "备注")
+    private String remark;
+
+    /** 项目阶段
+    A:100%转商,
+    B:90%合同下单&流程,
+    C:80%确认商务阶段,
+    D:70%确认产品技术方案及配置,
+    E:60%POC测试验证,
+    F:50%项目沟通&立项
+    G:40%前期咨询,
+    L:丢单 */
+    @Excel(name = "项目阶段")
+    private String projectPhase;
+
+    /** 意向等级
+    40%~50%:低意向
+    60%~70%:中意向
+    80%~100%:高意向 */
+    @Excel(name = "意向等级")
+    private Integer level;
+
+    /** 是否绑定BP账号:0未注册 1已注册未绑定 2不明确 3已绑定 */
+    @Excel(name = "是否绑定BP账号:0未注册 1已注册未绑定 2不明确 3已绑定")
+    private Integer isBp;
+
+    /** BP账户 */
+    @Excel(name = "BP账户")
+    private String bpAccount;
+
+    /** 预计成单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "预计成单时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date preTime;
+
+    /** 预计付费(元) */
+    @Excel(name = "预计付费(元)")
+    private Float preMoney;
+
+    /** 下次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "下次跟进时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date nextTime;
+
+
+    //创建人Id
+    private Long createId;
+
+    //所属公司id
+    private Long companyId;
+
+
+
+    private Integer isShow;
+
+
+    public void setBusinessId(Long businessId)
+    {
+        this.businessId = businessId;
+    }
+
+    public Long getBusinessId()
+    {
+        return businessId;
+    }
+    public void setCustomerId(Long customerId)
+    {
+        this.customerId = customerId;
+    }
+
+    public Long getCustomerId()
+    {
+        return customerId;
+    }
+    public void setSource(Integer source)
+    {
+        this.source = source;
+    }
+
+    public Integer getSource()
+    {
+        return source;
+    }
+    public void setManager(String manager)
+    {
+        this.manager = manager;
+    }
+
+    public String getManager()
+    {
+        return manager;
+    }
+    public void setCompanyName(String companyName)
+    {
+        this.companyName = companyName;
+    }
+
+    public String getCompanyName()
+    {
+        return companyName;
+    }
+    public void setContactRole(Integer contactRole)
+    {
+        this.contactRole = contactRole;
+    }
+
+    public Integer getContactRole()
+    {
+        return contactRole;
+    }
+    public void setBusinessScenario(String businessScenario)
+    {
+        this.businessScenario = businessScenario;
+    }
+
+    public String getBusinessScenario()
+    {
+        return businessScenario;
+    }
+    public void setProduct(String product)
+    {
+        this.product = product;
+    }
+
+    @Override
+    public String getRemark() {
+        return remark;
+    }
+
+    @Override
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public String getProduct()
+    {
+        return product;
+    }
+    public void setPurchaseCycle(Long purchaseCycle)
+    {
+        this.purchaseCycle = purchaseCycle;
+    }
+
+    public Long getPurchaseCycle()
+    {
+        return purchaseCycle;
+    }
+    public void setBusinessStatus(Integer businessStatus)
+    {
+        this.businessStatus = businessStatus;
+    }
+
+    public Integer getBusinessStatus()
+    {
+        return businessStatus;
+    }
+    public void setProjectPhase(String projectPhase)
+    {
+        this.projectPhase = projectPhase;
+    }
+
+    public String getProjectPhase()
+    {
+        return projectPhase;
+    }
+    public void setLevel(Integer level)
+    {
+        this.level = level;
+    }
+
+    public Integer getLevel()
+    {
+        return level;
+    }
+    public void setIsBp(Integer isBp)
+    {
+        this.isBp = isBp;
+    }
+
+    public Integer getIsBp()
+    {
+        return isBp;
+    }
+    public void setBpAccount(String bpAccount)
+    {
+        this.bpAccount = bpAccount;
+    }
+
+    public String getBpAccount()
+    {
+        return bpAccount;
+    }
+    public void setPreTime(Date preTime)
+    {
+        this.preTime = preTime;
+    }
+
+    public Date getPreTime()
+    {
+        return preTime;
+    }
+    public void setPreMoney(Float preMoney)
+    {
+        this.preMoney = preMoney;
+    }
+
+    public Float getPreMoney()
+    {
+        return preMoney;
+    }
+    public void setNextTime(Date nextTime)
+    {
+        this.nextTime = nextTime;
+    }
+
+    public Date getNextTime()
+    {
+        return nextTime;
+    }
+
+
+    public Long getCreateId() {
+        return createId;
+    }
+
+    public void setCreateId(Long createId) {
+        this.createId = createId;
+    }
+
+    public Long getCompanyId() {
+        return companyId;
+    }
+
+    public void setCompanyId(Long companyId) {
+        this.companyId = companyId;
+    }
+
+    public Integer getIsShow() {
+        return isShow;
+    }
+
+    public void setIsShow(Integer isShow) {
+        this.isShow = isShow;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("businessId", getBusinessId())
+            .append("customerId", getCustomerId())
+            .append("source", getSource())
+            .append("manager", getManager())
+            .append("companyName", getCompanyName())
+            .append("contactRole", getContactRole())
+            .append("businessScenario", getBusinessScenario())
+            .append("product", getProduct())
+            .append("purchaseCycle", getPurchaseCycle())
+            .append("businessStatus", getBusinessStatus())
+            .append("remark", getRemark())
+            .append("projectPhase", getProjectPhase())
+            .append("level", getLevel())
+            .append("isBp", getIsBp())
+            .append("bpAccount", getBpAccount())
+            .append("preTime", getPreTime())
+            .append("preMoney", getPreMoney())
+            .append("nextTime", getNextTime())
+            .append("createTime", getCreateTime())
+            .append("updateTime", getUpdateTime())
+            .append("createBy", getCreateBy())
+            .append("createId", getCreateId())
+            .toString();
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 import javax.validation.constraints.NotBlank;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ import java.util.Date;
  * @author fs
  * @date 2022-12-21
  */
+@EqualsAndHashCode(callSuper = true)
 @Data
 public class CrmCustomer extends BaseEntity
 {
@@ -195,4 +197,22 @@ public class CrmCustomer extends BaseEntity
      */
     private String traceId;
 
+    private String customerCompanyName;
+
+    private String isWx;
+
+    private String businessScenario;
+
+    private String product;
+    private String content;
+
+    private String moneyRemark;
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextTime; //下次跟进时间
+
+    private Integer isPoolRule; //是否参加公海回收规则 1:是
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date sysVisitTime; //系统自建跟进时间
+
 }

+ 8 - 110
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerContacts.java

@@ -2,15 +2,19 @@ package com.fs.crm.domain;
 
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 /**
  * 客户联系人对象 crm_customer_contacts
- * 
+ *
  * @author fs
  * @date 2023-03-14
  */
+@EqualsAndHashCode(callSuper = true)
+@Data
 public class CrmCustomerContacts extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
@@ -22,6 +26,9 @@ public class CrmCustomerContacts extends BaseEntity
     @Excel(name = "客户ID")
     private Long customerId;
 
+    /** 商机ID */
+    private Long businessId;
+
     /** 联系人名称 */
     @Excel(name = "联系人名称")
     private String name;
@@ -54,113 +61,4 @@ public class CrmCustomerContacts extends BaseEntity
     @Excel(name = "是否删除")
     private Long companyId;
 
-    public void setContactsId(Long contactsId) 
-    {
-        this.contactsId = contactsId;
-    }
-
-    public Long getContactsId() 
-    {
-        return contactsId;
-    }
-    public void setCustomerId(Long customerId) 
-    {
-        this.customerId = customerId;
-    }
-
-    public Long getCustomerId() 
-    {
-        return customerId;
-    }
-    public void setName(String name) 
-    {
-        this.name = name;
-    }
-
-    public String getName() 
-    {
-        return name;
-    }
-    public void setMobile(String mobile) 
-    {
-        this.mobile = mobile;
-    }
-
-    public String getMobile() 
-    {
-        return mobile;
-    }
-    public void setEmail(String email) 
-    {
-        this.email = email;
-    }
-
-    public String getEmail() 
-    {
-        return email;
-    }
-    public void setWeixin(String weixin) 
-    {
-        this.weixin = weixin;
-    }
-
-    public String getWeixin() 
-    {
-        return weixin;
-    }
-    public void setAddress(String address) 
-    {
-        this.address = address;
-    }
-
-    public String getAddress() 
-    {
-        return address;
-    }
-    public void setCreateUserId(Long createUserId) 
-    {
-        this.createUserId = createUserId;
-    }
-
-    public Long getCreateUserId() 
-    {
-        return createUserId;
-    }
-    public void setIsDel(Integer isDel) 
-    {
-        this.isDel = isDel;
-    }
-
-    public Integer getIsDel() 
-    {
-        return isDel;
-    }
-    public void setCompanyId(Long companyId) 
-    {
-        this.companyId = companyId;
-    }
-
-    public Long getCompanyId() 
-    {
-        return companyId;
-    }
-
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
-            .append("contactsId", getContactsId())
-            .append("customerId", getCustomerId())
-            .append("name", getName())
-            .append("mobile", getMobile())
-            .append("email", getEmail())
-            .append("weixin", getWeixin())
-            .append("address", getAddress())
-            .append("remark", getRemark())
-            .append("createUserId", getCreateUserId())
-            .append("createTime", getCreateTime())
-            .append("updateTime", getUpdateTime())
-            .append("isDel", getIsDel())
-            .append("companyId", getCompanyId())
-            .toString();
-    }
 }

+ 14 - 125
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerVisit.java

@@ -3,6 +3,8 @@ package com.fs.crm.domain;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
@@ -14,6 +16,8 @@ import java.util.Date;
  * @author fs
  * @date 2022-12-21
  */
+@EqualsAndHashCode(callSuper = true)
+@Data
 public class CrmCustomerVisit extends BaseEntity
 {
     private static final long serialVersionUID = 1L;
@@ -25,6 +29,11 @@ public class CrmCustomerVisit extends BaseEntity
     @Excel(name = "客户ID")
     private Long customerId;
 
+    /** 商机ID */
+    @Excel(name = "商机ID")
+    private Long businessId;
+
+
     /** 类型 1 电话 2 微信 */
     @Excel(name = "类型 1 电话 2 微信")
     private Long visitType;
@@ -65,129 +74,9 @@ public class CrmCustomerVisit extends BaseEntity
     @Excel(name = "客户跟进阶段")
     private Integer customerUserStatus;
 
-    public Integer getCustomerUserStatus() {
-        return customerUserStatus;
-    }
-
-    public void setCustomerUserStatus(Integer customerUserStatus) {
-        this.customerUserStatus = customerUserStatus;
-    }
-
-    public void setVisitId(Long visitId)
-    {
-        this.visitId = visitId;
-    }
-
-    public Long getVisitId()
-    {
-        return visitId;
-    }
-    public void setCustomerId(Long customerId)
-    {
-        this.customerId = customerId;
-    }
-
-    public Long getCustomerId()
-    {
-        return customerId;
-    }
-    public void setVisitType(Long visitType)
-    {
-        this.visitType = visitType;
-    }
-
-    public Long getVisitType()
-    {
-        return visitType;
-    }
-    public void setContent(String content)
-    {
-        this.content = content;
-    }
-
-    public String getContent()
-    {
-        return content;
-    }
-    public void setPhotos(String photos)
-    {
-        this.photos = photos;
-    }
-
-    public String getPhotos()
-    {
-        return photos;
-    }
-    public void setNextTime(Date nextTime)
-    {
-        this.nextTime = nextTime;
-    }
-
-    public Date getNextTime()
-    {
-        return nextTime;
-    }
-    public void setLng(String lng)
-    {
-        this.lng = lng;
-    }
-
-    public String getLng()
-    {
-        return lng;
-    }
-    public void setLat(String lat)
-    {
-        this.lat = lat;
-    }
-
-    public String getLat()
-    {
-        return lat;
-    }
-    public void setAddress(String address)
-    {
-        this.address = address;
-    }
-
-    public String getAddress()
-    {
-        return address;
-    }
-    public void setCompanyUserId(Long companyUserId)
-    {
-        this.companyUserId = companyUserId;
-    }
-
-    public Long getCompanyUserId()
-    {
-        return companyUserId;
-    }
-    public void setCompanyId(Long companyId)
-    {
-        this.companyId = companyId;
-    }
-
-    public Long getCompanyId()
-    {
-        return companyId;
-    }
-
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
-            .append("visitId", getVisitId())
-            .append("customerId", getCustomerId())
-            .append("visitType", getVisitType())
-            .append("content", getContent())
-            .append("photos", getPhotos())
-            .append("nextTime", getNextTime())
-            .append("lng", getLng())
-            .append("lat", getLat())
-            .append("address", getAddress())
-            .append("companyUserId", getCompanyUserId())
-            .append("createTime", getCreateTime())
-            .append("companyId", getCompanyId())
-            .toString();
-    }
+    /** 跟进类型 0:线索跟进 1:客户跟进 */
+    @Excel(name = "跟进类型 0:线索跟进 1:客户跟进 2:商机跟进")
+    private Integer type;
+
+    private Integer isShow;
 }

+ 65 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmExtDetail.java

@@ -0,0 +1,65 @@
+package com.fs.crm.domain;
+
+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;
+
+/**
+ * 字段扩展详情对象 crm_ext_detail
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+public class CrmExtDetail extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    private Long id;
+
+    /** 关联id */
+    @Excel(name = "关联id")
+    private Long correlateId;
+
+    /** 关联类型 */
+    @Excel(name = "关联类型")
+    private String correlateType;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+    public void setCorrelateId(Long correlateId)
+    {
+        this.correlateId = correlateId;
+    }
+
+    public Long getCorrelateId()
+    {
+        return correlateId;
+    }
+    public void setCorrelateType(String correlateType)
+    {
+        this.correlateType = correlateType;
+    }
+
+    public String getCorrelateType()
+    {
+        return correlateType;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("correlateId", getCorrelateId())
+            .append("correlateType", getCorrelateType())
+            .toString();
+    }
+}

+ 136 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmExtLog.java

@@ -0,0 +1,136 @@
+package com.fs.crm.domain;
+
+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;
+
+/**
+ * 修改字段扩展日志对象 crm_ext_log
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+public class CrmExtLog extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long logId;
+
+    /** 执行语句 */
+    @Excel(name = "执行语句")
+    private String executeSql;
+
+    /** 修改字段名 */
+    @Excel(name = "修改字段名")
+    private String columnName;
+//
+    /** 关联表字段Id */
+    @Excel(name = "关联表字段Id")
+    private String correlateTypeId;
+
+
+
+    /** 执行类型 */
+    @Excel(name = "执行类型")
+    private String type;
+
+    /** 备份表地址 */
+    @Excel(name = "备份表地址")
+    private String backupUrl;
+
+
+    @Excel(name = "执行结果")
+    private String result;
+
+    @Excel(name = "报错信息")
+    private String errorMsg;
+
+    public void setLogId(Long logId)
+    {
+        this.logId = logId;
+    }
+
+    public Long getLogId()
+    {
+        return logId;
+    }
+    public void setExecuteSql(String executeSql)
+    {
+        this.executeSql = executeSql;
+    }
+
+    public String getExecuteSql()
+    {
+        return executeSql;
+    }
+    public void setColumnName(String columnName)
+    {
+        this.columnName = columnName;
+    }
+
+    public String getColumnName()
+    {
+        return columnName;
+    }
+    public void setCorrelateTypeId(String correlateTypeId)
+    {
+        this.correlateTypeId = correlateTypeId;
+    }
+
+    public String getCorrelateTypeId()
+    {
+        return correlateTypeId;
+    }
+    public void setBackupUrl(String backupUrl)
+    {
+        this.backupUrl = backupUrl;
+    }
+
+    public String getBackupUrl()
+    {
+        return backupUrl;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getResult() {
+        return result;
+    }
+
+    public void setResult(String result) {
+        this.result = result;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    public void setErrorMsg(String errorMsg) {
+        this.errorMsg = errorMsg;
+    }
+
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("logId", getLogId())
+            .append("executeSql", getExecuteSql())
+            .append("columnName", getColumnName())
+            .append("correlateTypeId", getCorrelateTypeId())
+            .append("type", getType())
+            .append("backupUrl", getBackupUrl())
+            .append("createBy", getCreateBy())
+            .append("createTime", getCreateTime())
+            .append("result", getResult())
+            .append("errorMsg", getErrorMsg())
+            .toString();
+    }
+}

+ 95 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmFollowUp.java

@@ -0,0 +1,95 @@
+package com.fs.crm.domain;
+
+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;
+
+/**
+ * 跟进提醒对象 crm_follow_up
+ *
+ * @author fs
+ * @date 2025-04-15
+ */
+public class CrmFollowUp extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 客户ID */
+    @Excel(name = "客户ID")
+    private Long customerId;
+
+    /** 跟进记录id */
+    @Excel(name = "跟进记录id")
+    private Long visitId;
+
+    /** 销售ID */
+    @Excel(name = "销售ID")
+    private Long companyUserId;
+
+    /** 是否处理 0:未读 1:已处理 */
+    @Excel(name = "是否处理 0:未读 1:已处理")
+    private Integer isHandle;
+
+    public void setId(Long id)
+    {
+        this.id = id;
+    }
+
+    public Long getId()
+    {
+        return id;
+    }
+    public void setCustomerId(Long customerId)
+    {
+        this.customerId = customerId;
+    }
+
+    public Long getCustomerId()
+    {
+        return customerId;
+    }
+    public void setVisitId(Long visitId)
+    {
+        this.visitId = visitId;
+    }
+
+    public Long getVisitId()
+    {
+        return visitId;
+    }
+    public void setCompanyUserId(Long companyUserId)
+    {
+        this.companyUserId = companyUserId;
+    }
+
+    public Long getCompanyUserId()
+    {
+        return companyUserId;
+    }
+    public void setIsHandle(Integer isHandle)
+    {
+        this.isHandle = isHandle;
+    }
+
+    public Integer getIsHandle()
+    {
+        return isHandle;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("id", getId())
+            .append("customerId", getCustomerId())
+            .append("visitId", getVisitId())
+            .append("companyUserId", getCompanyUserId())
+            .append("isHandle", getIsHandle())
+            .append("createTime", getCreateTime())
+            .append("updateTime", getUpdateTime())
+            .toString();
+    }
+}

+ 6 - 1
fs-service/src/main/java/com/fs/crm/enums/CustomerLogEnum.java

@@ -13,7 +13,12 @@ public enum CustomerLogEnum {
     RECEIVE(3,"认领"),
     RECOVER(4,"回收"),
     ASSIGN_USER(5,"分配员工"),
-    CANCEL_ASSIGN(6,"撤销分配");
+    CANCEL_ASSIGN(6,"撤销分配"),
+    GAIN(7,"捞取未分配线索"),
+    CREATE(8,"创建客户"),
+    BUSINESS(9,"创建商机"),
+    DEL_BUSINESS(10,"删除商机"),
+    UPDATE_BUSINESS(11,"修改商机");
 
     private Integer value;
     private String desc;

+ 73 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmBusinessMapper.java

@@ -0,0 +1,73 @@
+package com.fs.crm.mapper;
+
+
+
+import com.fs.crm.domain.CrmBusiness;
+import com.fs.crm.vo.CrmBusinessListVO;
+import com.fs.crm.param.CrmBusinessQueryParam;
+import io.lettuce.core.dynamic.annotation.Param;
+
+import java.util.List;
+
+/**
+ * 商机Mapper接口
+ *
+ * @author fs
+ * @date 2025-01-16
+ */
+public interface CrmBusinessMapper
+{
+    /**
+     * 查询商机
+     *
+     * @param businessId 商机ID
+     * @return 商机
+     */
+    public CrmBusiness selectCrmBusinessById(Long businessId);
+
+    /**
+     * 查询商机列表
+     *
+     * @param param 商机
+     * @return 商机集合
+     */
+    public List<CrmBusinessListVO> selectCrmBusinessList(CrmBusinessQueryParam param);
+
+    /**
+     * 新增商机
+     *
+     * @param crmBusiness 商机
+     * @return 结果
+     */
+    public int insertCrmBusiness(CrmBusiness crmBusiness);
+
+    /**
+     * 修改商机
+     *
+     * @param crmBusiness 商机
+     * @return 结果
+     */
+    public int updateCrmBusiness(CrmBusiness crmBusiness);
+
+    /**
+     * 删除商机
+     *
+     * @param businessId 商机ID
+     * @return 结果
+     */
+    public int deleteCrmBusinessById(Long businessId);
+
+    /**
+     * 批量删除商机
+     *
+     * @param businessIds 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteCrmBusinessByIds(Long[] businessIds);
+
+    int updateCrmBusinessByCustomerId(CrmBusiness crmBusiness);
+
+    void updateBatchCrmBusiness(@Param("businessIds") List<Long> businessIds, @Param("maps") CrmBusiness crmBusiness);
+
+    CrmBusiness getLastByCustomerId(@Param("customerId")Long customerId);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java

@@ -3,6 +3,7 @@ package com.fs.crm.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.qw.domain.QwExternalAiAnalyze;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -65,4 +66,8 @@ public interface CrmCustomerAnalyzeMapper extends BaseMapper<CrmCustomerAnalyze>
     int updateCustomerPortrait(CrmCustomerAnalyze crmCustomerAnalyze);
 
     List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeListAll(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    QwExternalAiAnalyze selectLatestQwAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
+
+    Integer updateAiAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerContactsMapper.java

@@ -75,4 +75,6 @@ public interface CrmCustomerContactsMapper
             " order by contacts_id desc "+
             "</script>"})
     List<CrmCustomerContactsListQueryVO> selectCrmCustomerContactsListQuery(@Param("maps") CrmCustomerContactsListQueryParam param);
+
+    CrmCustomerContacts selectCrmCustomerContactsByMobile(String mobile);
 }

+ 33 - 4
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -12,6 +12,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 
@@ -344,11 +345,12 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             " order by cu.customer_user_id desc "+
             "</script>"})
     List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQuery(@Param("maps") CrmMyCustomerListQueryParam param);
+    List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQueryInfo(@Param("maps") CrmMyCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime,ca.attrition_level,ca.intention_degree,ca.customer_focus_json  from  crm_customer c " +
             "left join company_user u on u.user_id=c.receive_user_id " +
             "left join crm_customer_user ccu on c.customer_user_id = ccu.customer_user_id " +
-            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree,customer_focus_json FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
             "   ORDER BY create_time DESC " +
             "   LIMIT 1 " +
             ") ca ON TRUE " +
@@ -439,9 +441,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "</script>"})
     List<CrmCustomerListQueryVO> selectCrmCustomerListQuery(@Param("maps") CrmCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "select c.*,u.nick_name as company_user_nick_name,ca.attrition_level,ca.intention_degree,ca.customer_focus_json  from  crm_customer c " +
             "left join company_user u on u.user_id=c.receive_user_id " +
-            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree,customer_focus_json FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
             "   ORDER BY create_time DESC " +
             "   LIMIT 1 " +
             ") ca ON TRUE " +
@@ -506,6 +508,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             " order by c.customer_id desc "+
             "</script>"})
     List<CrmLineCustomerListQueryVO> selectCrmLineCustomerListQuery(@Param("maps") CrmLineCustomerListQueryParam param);
+
+    List<CrmLineCustomerListQueryVO> selectCrmLineCustomerListQueryInfo(@Param("maps") CrmLineCustomerListQueryParam param);
+
     @Select({"<script> " +
             "select ifnull(count(1),0) from crm_customer c  " +
             "where c.status=1 and c.is_del=0 " +
@@ -997,4 +1002,28 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
     void insertCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
 
     int updateCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
+
+    Long countReceiveCustomer(@Param("userId")Long userId);
+
+    List<CrmCustomer> selectRecoveryClue(@Param("companyId") Long companyId,@Param("limitDay") Integer limitDay,@Param("businessLimit") Integer businessLimit);
+
+    List<CrmCustomer> selectRecoveryBusiness(@Param("companyId") Long companyId,@Param("limitDay")Integer limitDay);
+
+    List<CrmLineCustomerListQueryVO> selectTransferCustomerList(@Param("maps")CrmLineCustomerListQueryParam param);
+
+    LinkedHashSet<CrmCustomerUnPoolListQueryVO> selectCrmCustomerListByUnPool(@Param("param1")CrmUnPoolListQueryParam param1,
+                                                                              @Param("param2")CrmUnPoolListQueryParam param2,
+                                                                              @Param("param3")CrmUnPoolListQueryParam param3,
+                                                                              @Param("param")CrmUnPoolListQueryParam param
+    );
+
+    Long countCrmCustomerListByUnPool(@Param("param1")CrmUnPoolListQueryParam param1,
+                                      @Param("param2")CrmUnPoolListQueryParam param2,
+                                      @Param("param3")CrmUnPoolListQueryParam param3,
+                                      @Param("param")CrmUnPoolListQueryParam param
+    );
+
+    int recoverCrmLineCustomerList(@Param("maps") CrmLineCustomerListQueryParam param);
+
+    List<CrmCustomer> selectCrmCustomerByCondition(CrmCustomer crmCustomer);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerVisitMapper.java

@@ -1,6 +1,7 @@
 package com.fs.crm.mapper;
 
 import com.alibaba.fastjson.JSONObject;
+import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.domain.CrmCustomerVisit;
 import com.fs.crm.param.CrmCustomerStatisticsParam;
 import com.fs.crm.param.CrmCustomerVisitListParam;
@@ -198,4 +199,16 @@ public interface CrmCustomerVisitMapper
             " order by v.visit_id desc "+
             "</script>"})
     List<CrmCustomerVisitExportVO> selectCrmCustomerVisitExportListVO(@Param("maps") CrmCustomerVisitListParam param);
+
+    CrmCustomerVisit selectNewByBusinessId(@Param("businessId")Long businessId);
+
+    CrmCustomerVisit getLastByCustomerId(@Param("customerId")Long customerId);
+
+    List<CrmCustomer> selectRecoveryClue(@Param("companyId") Long companyId, @Param("limitDay") Integer limitDay);
+
+    List<CrmCustomerVisit> selectNextTimeList();
+
+
+    List<CrmCustomer> selectVisitTimeout();
+
 }

+ 88 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmExtDetailMapper.java

@@ -0,0 +1,88 @@
+package com.fs.crm.mapper;
+
+import com.fs.crm.domain.CrmExtDetail;
+import com.fs.crm.param.CrmExtDetailAddOrUpdateParam;
+import com.fs.crm.vo.CrmExtDetailVo;
+import com.fs.watch.param.BaseQueryParam;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 字段扩展详情Mapper接口
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+public interface CrmExtDetailMapper
+{
+    /**
+     * 查询字段扩展详情
+     *
+     * @param id 字段扩展详情ID
+     * @return 字段扩展详情
+     */
+    public CrmExtDetail selectCrmExtDetailById(Long id);
+
+    /**
+     * 查询字段扩展详情列表
+     *
+     * @param crmExtDetail 字段扩展详情
+     * @return 字段扩展详情集合
+     */
+    public List<CrmExtDetail> selectCrmExtDetailList(CrmExtDetail crmExtDetail);
+
+    /**
+     * 新增字段扩展详情
+     *
+     * @param columns 字段扩展详情
+     * @return 结果
+     */
+    public int insertCrmExtDetail(@Param("columns") List<String> columns,@Param("values")List<Object> values);
+
+    /**
+     * 修改字段扩展详情
+     *
+     * @param crmExtDetail 字段扩展详情
+     * @return 结果
+     */
+    public int updateCrmExtDetail(@Param("crmExtDetail") Map<String,Object> crmExtDetail,@Param("id") int id);
+
+    /**
+     * 删除字段扩展详情
+     *
+     * @param id 字段扩展详情ID
+     * @return 结果
+     */
+    public int deleteCrmExtDetailById(Long id);
+
+    /**
+     * 批量删除字段扩展详情
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteCrmExtDetailByIds(Long[] ids);
+
+    List<CrmExtDetailVo> getTableColumnsMetadata(BaseQueryParam param);
+
+    /**
+     * 查询是否存在该字段
+     * @param fieldName
+     * @return
+     */
+    Integer checkIfFieldExists(String fieldName);
+
+    void insertColumn(CrmExtDetailAddOrUpdateParam param);
+
+    void deleteColumns(@Param("columnNames") String[] columnNames);
+
+    void updateColumn(CrmExtDetailAddOrUpdateParam param);
+
+    List<CrmExtDetailVo> getTableColumnsMetaList(BaseQueryParam param);
+
+    Map<String,Object> selectCrmExtDetailByCondition(Map<String, Object> map);
+
+    void deleteByCorrelateIdAndType(@Param("customerIds") Long[] customerIds, @Param("type")String type);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmExtLogMapper.java

@@ -0,0 +1,62 @@
+package com.fs.crm.mapper;
+
+import com.fs.crm.domain.CrmExtLog;
+
+import java.util.List;
+
+/**
+ * 修改字段扩展日志Mapper接口
+ *
+ * @author fs
+ * @date 2025-02-17
+ */
+public interface CrmExtLogMapper
+{
+    /**
+     * 查询修改字段扩展日志
+     *
+     * @param logId 修改字段扩展日志ID
+     * @return 修改字段扩展日志
+     */
+    public CrmExtLog selectCrmExtLogById(Long logId);
+
+    /**
+     * 查询修改字段扩展日志列表
+     *
+     * @param crmExtLog 修改字段扩展日志
+     * @return 修改字段扩展日志集合
+     */
+    public List<CrmExtLog> selectCrmExtLogList(CrmExtLog crmExtLog);
+
+    /**
+     * 新增修改字段扩展日志
+     *
+     * @param crmExtLog 修改字段扩展日志
+     * @return 结果
+     */
+    public int insertCrmExtLog(CrmExtLog crmExtLog);
+
+    /**
+     * 修改修改字段扩展日志
+     *
+     * @param crmExtLog 修改字段扩展日志
+     * @return 结果
+     */
+    public int updateCrmExtLog(CrmExtLog crmExtLog);
+
+    /**
+     * 删除修改字段扩展日志
+     *
+     * @param logId 修改字段扩展日志ID
+     * @return 结果
+     */
+    public int deleteCrmExtLogById(Long logId);
+
+    /**
+     * 批量删除修改字段扩展日志
+     *
+     * @param logIds 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteCrmExtLogByIds(Long[] logIds);
+}

+ 65 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmFollowUpMapper.java

@@ -0,0 +1,65 @@
+package com.fs.crm.mapper;
+
+import com.fs.crm.domain.CrmFollowUp;
+import com.fs.crm.vo.CrmFollowUpVo;
+
+import java.util.List;
+
+/**
+ * 跟进提醒Mapper接口
+ *
+ * @author fs
+ * @date 2025-04-15
+ */
+public interface CrmFollowUpMapper
+{
+    /**
+     * 查询跟进提醒
+     *
+     * @param id 跟进提醒ID
+     * @return 跟进提醒
+     */
+    public CrmFollowUp selectCrmFollowUpById(Long id);
+
+    /**
+     * 查询跟进提醒列表
+     *
+     * @param crmFollowUp 跟进提醒
+     * @return 跟进提醒集合
+     */
+    public List<CrmFollowUp> selectCrmFollowUpList(CrmFollowUp crmFollowUp);
+
+    /**
+     * 新增跟进提醒
+     *
+     * @param crmFollowUp 跟进提醒
+     * @return 结果
+     */
+    public int insertCrmFollowUp(CrmFollowUp crmFollowUp);
+
+    /**
+     * 修改跟进提醒
+     *
+     * @param crmFollowUp 跟进提醒
+     * @return 结果
+     */
+    public int updateCrmFollowUp(CrmFollowUp crmFollowUp);
+
+    /**
+     * 删除跟进提醒
+     *
+     * @param id 跟进提醒ID
+     * @return 结果
+     */
+    public int deleteCrmFollowUpById(Long id);
+
+    /**
+     * 批量删除跟进提醒
+     *
+     * @param ids 需要删除的数据ID
+     * @return 结果
+     */
+    public int deleteCrmFollowUpByIds(Long[] ids);
+
+    List<CrmFollowUpVo> selectCrmFollowUpAndCustomerList(CrmFollowUp crmFollowUp);
+}

+ 96 - 0
fs-service/src/main/java/com/fs/crm/param/CrmBusinessAddAndUpdateParam.java

@@ -0,0 +1,96 @@
+package com.fs.crm.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.crm.domain.CrmCustomerContacts;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class CrmBusinessAddAndUpdateParam extends BaseEntity {
+    /** 商机id */
+    private Long businessId;
+
+    /** 客户id */
+    private Long customerId;
+
+    /** 线索来源
+     0企查询
+     1企查查
+     2领鸟云
+     3其他 */
+    private Integer source;
+
+    /** 客户经理 */
+    private String manager;
+
+    /** 公司名称(只填公司全称,个人用户就填名字) */
+    private String companyName;
+
+
+    /** 接口人角色 0老板,1采购,2财务,3技术负责人,4一般技术人员,5项目负责人,6其他 */
+    private Integer contactRole;
+
+    /** 业务场景网站,电商,游戏,小程序,APP,OA,ERP,CRM,物联网,AI人工智能,国产化,数字人,内部办公系统,等保,其他 */
+    private String businessScenario;
+
+    /** 方案涉及的产品 */
+    private String product;
+
+    /** 采购周期(单位:天) */
+    private Long purchaseCycle;
+
+    /** 跟进状态:0跟进中,1已流失,2已赢单,3待验证 */
+    private Integer businessStatus;
+
+    /** 项目阶段
+     A:100%转商,
+     B:90%合同下单&流程,
+     C:80%确认商务阶段,
+     D:70%确认产品技术方案及配置,
+     E:60%POC测试验证,
+     F:50%项目沟通&立项
+     G:40%前期咨询,
+     L:丢单 */
+    private String projectPhase;
+
+    /** 意向等级
+     40%~50%:低意向
+     60%~70%:中意向
+     80%~100%:高意向 */
+    private Integer level;
+
+    /** 是否绑定BP账号:0未注册 1已注册未绑定 2不明确 3已绑定 */
+    private Integer isBp;
+
+    /** BP账户 */
+    private String bpAccount;
+
+    /** 预计成单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date preTime;
+
+    /** 预计付费(元) */
+    private Float preMoney;
+
+    /** 下次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextTime;
+
+    private List<CrmCustomerContacts> mobileList;
+
+    private String mobile;
+
+    private Long createId;
+
+    private Long updateId;
+
+    private Long companyId;
+
+    private String opeName;
+
+    //跟进内容
+    private String content;
+}

+ 118 - 0
fs-service/src/main/java/com/fs/crm/param/CrmBusinessImportParam.java

@@ -0,0 +1,118 @@
+package com.fs.crm.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CrmBusinessImportParam {
+    /** 商机id */
+    private Long businessId;
+
+    /** 客户id */
+    @Excel(name = "客户id")
+    private Long customerId;
+
+
+    @Excel(name = "客户电话")
+    private String mobile;
+
+    @Excel(name = "客户来源",dictType = "crm_customer_source")
+    private String source;
+
+    /** 线索来源
+     0企查询
+     1企查查
+     2领鸟云
+     3其他 */
+//    @Excel(name = "线索来源",dictType = "crm_customer_source",required = true)
+//    private String source;
+
+    /** 客户经理 */
+    @Excel(name = "客户经理")
+    private String manager;
+
+    /** 公司名称(只填公司全称,个人用户就填名字) */
+    @Excel(name = "公司名称")
+    private String companyName;
+
+
+    /** 接口人角色 0老板,1采购,2财务,3技术负责人,4一般技术人员,5项目负责人,6其他 */
+    @Excel(name = "接口人角色",dictType = "crm_contact_role")
+    private String contactRole;
+
+    /** 业务场景网站,电商,游戏,小程序,APP,OA,ERP,CRM,物联网,AI人工智能,国产化,数字人,内部办公系统,等保,其他 */
+    @Excel(name = "业务场景",dictType = "business_scenario_type")
+    private String businessScenario;
+
+    /** 方案涉及的产品 */
+    @Excel(name = "意向产品")
+    private String product;
+
+    /** 采购周期(单位:天) */
+    @Excel(name = "采购周期")
+    private Long purchaseCycle;
+
+    /** 跟进状态:0跟进中,1已流失,2已赢单,3待验证 */
+    @Excel(name = "跟进状态",dictType = "crm_business_status")
+    private String businessStatus;
+
+    /** 项目阶段
+     A:100%转商,
+     B:90%合同下单&流程,
+     C:80%确认商务阶段,
+     D:70%确认产品技术方案及配置,
+     E:60%POC测试验证,
+     F:50%项目沟通&立项
+     G:40%前期咨询,
+     L:丢单 */
+    @Excel(name = "项目阶段",dictType = "crm_project_phase")
+    private String projectPhase;
+
+    /** 意向等级
+     40%~50%:低意向
+     60%~70%:中意向
+     80%~100%:高意向 */
+    @Excel(name = "意向等级",dictType = "crm_level")
+    private String level;
+
+//    /** 是否绑定BP账号:0未注册 1已注册未绑定 2不明确 3已绑定 */
+//    @Excel(name = "是否绑定BP账号")
+//    private Integer isBp;
+
+//    /** BP账户 */
+//    @Excel(name = "公司名称")
+//    private String bpAccount;
+
+    /** 预计成单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "预计成单时间",dateFormat = "yyyy-MM-dd")
+    private Date preTime;
+
+    /** 预计付费(元) */
+    @Excel(name = "预计付费(元)")
+    private Float preMoney;
+
+    /** 下次跟进时间 */
+    @Excel(name = "下次跟进时间",dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextTime;
+
+
+//    @Excel(name = "公司名称")
+//    private String mobile;
+
+    private Long createId;
+
+    private Long updateId;
+
+    private Long companyId;
+
+    private String opeName;
+
+    //跟进内容
+    @Excel(name = "跟进内容")
+    private String content;
+}

+ 109 - 0
fs-service/src/main/java/com/fs/crm/param/CrmBusinessQueryParam.java

@@ -0,0 +1,109 @@
+package com.fs.crm.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.param.BaseQueryParam;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CrmBusinessQueryParam extends BaseQueryParam {
+    /** 商机id */
+    private Long businessId;
+
+    /** 客户id */
+    private Long customerId;
+
+    /** 客户名字 */
+    private String customerName;
+
+    /** 线索来源
+     0企查询
+     1企查查
+     2领鸟云
+     3其他 */
+    private Integer source;
+
+    /** 客户经理 */
+    private String manager;
+
+    /** 公司名称(只填公司全称,个人用户就填名字) */
+    private String companyName;
+
+    /** 电话号码 */
+    private String mobile;
+
+    /** 接口人角色 0老板,1采购,2财务,3技术负责人,4一般技术人员,5项目负责人,6其他 */
+    private Integer contactRole;
+
+    /** 业务场景网站,电商,游戏,小程序,APP,OA,ERP,CRM,物联网,AI人工智能,国产化,数字人,内部办公系统,等保,其他 */
+    private String businessScenario;
+
+    /** 方案涉及的产品 */
+    private String product;
+
+    /** 采购周期(单位:天) */
+    private Long purchaseCycle;
+
+    /** 跟进状态:0跟进中,1已流失,2已赢单,3待验证 */
+    private Integer businessStatus;
+
+
+    /** 跟进状态:0跟进中,1已流失,2已赢单,3待验证 */
+    private String remark;
+
+    /** 项目阶段
+     A:100%转商,
+     B:90%合同下单&流程,
+     C:80%确认商务阶段,
+     D:70%确认产品技术方案及配置,
+     E:60%POC测试验证,
+     F:50%项目沟通&立项
+     G:40%前期咨询,
+     L:丢单 */
+    private String projectPhase;
+
+    /** 意向等级
+     40%~50%:低意向
+     60%~70%:中意向
+     80%~100%:高意向 */
+    private Integer level;
+
+    /** 是否绑定BP账号:0未注册 1已注册未绑定 2不明确 3已绑定 */
+    private Integer isBp;
+
+    /** BP账户 */
+    private String bpAccount;
+
+    /** 预计成单时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date preTime;
+
+    /** 预计付费(元) */
+    private Float preMoney;
+
+    /** 最后一次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+//    @Excel(name = "挖掘时间",dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 下次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date nextTime;
+
+
+    private String createTimeRange;
+    private String[] createTimeArr;
+
+    private String nextTimeRange;
+    private String[] nextTimeArr;
+
+    private String orderBy;//下次跟进时间排序规则
+
+    //创建人Id
+    private Long createId;
+
+    //所属公司id
+    private Long companyId;
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCompanyLineCustomerImportParam.java

@@ -130,6 +130,8 @@ public class CrmCompanyLineCustomerImportParam implements Serializable {
     @TableField(exist = false)
     private String msg;
 
+    @Excel(name = "是否是商机" )
+    private String isBusiness;
 
 
 }

+ 16 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerBatchReceiveParam.java

@@ -0,0 +1,16 @@
+package com.fs.crm.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CrmCustomerBatchReceiveParam {
+    private List<Long> customerIds;
+
+    private Long companyUserId;
+
+    private Long companyId;
+
+    private Long deptId;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerBatchRecoverParam.java

@@ -0,0 +1,14 @@
+package com.fs.crm.param;
+
+import lombok.Data;
+
+@Data
+public class CrmCustomerBatchRecoverParam {
+    private Long[] customerUserIds;
+
+    private Long companyUserId;
+
+    private Long companyId;
+
+    private Long customerId;
+}

+ 4 - 1
fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java

@@ -1,7 +1,6 @@
 package com.fs.crm.param;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
 
@@ -108,4 +107,8 @@ public class CrmCustomerListQueryParam extends BaseQueryParam
     /** 意向度 */
     private String intentionDegree;
 
+    private Integer isPool;
+
+    private Integer importType; // 0:所有 //1:我的
+
 }

+ 19 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerRecoverParam.java

@@ -0,0 +1,19 @@
+package com.fs.crm.param;
+
+import com.fs.common.param.BaseQueryParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class CrmCustomerRecoverParam extends BaseQueryParam
+{
+    private Long customerUserId;
+
+    private Long companyUserId;
+
+    private Long companyId;
+
+    private Long customerId;
+
+}

+ 15 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerUpdateOrAddParam.java

@@ -0,0 +1,15 @@
+package com.fs.crm.param;
+
+import com.fs.crm.domain.CrmCustomer;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class CrmCustomerUpdateOrAddParam extends CrmCustomer {
+    //扩展字段
+    private Map<String,Object> ext;
+
+    //创建人名字
+    private String opeName;
+}

+ 93 - 0
fs-service/src/main/java/com/fs/crm/param/CrmExtDetailAddOrUpdateParam.java

@@ -0,0 +1,93 @@
+package com.fs.crm.param;
+
+import lombok.Data;
+
+@Data
+public class CrmExtDetailAddOrUpdateParam {
+
+    /**
+     * 原字段名
+     */
+    private String oldColumnName;
+    /**
+     * 字段名
+     */
+    private String columnName;
+
+    /**
+     * 数据类型及其额外的属性
+     */
+    private String columnType;
+
+    /**
+     * 默认值
+     */
+    private String columnDefault;
+
+
+    /**
+     * 是否可以为 NULL YES:允许为null NO:不允许
+     */
+    private String isNullable;
+
+    /**
+     * 数据类型。例如:VARCHAR, INT, TEXT, DATE 等。
+     */
+    private String dataType;
+
+    /**
+     * 字符类型的列(如 VARCHAR 或 CHAR),该列表示字符的最大长度。如果该列不是字符类型,则该字段为 NULL
+     */
+    private Integer characterMaximumLength;
+
+    /**
+     * 字符类型的列(如 VARCHAR 或 CHAR),该列表示字符的最大长度。如果该列不是字符类型,则该字段为 NULL
+     */
+//    private String characterMaximumLength;
+
+    /**
+     * 数字类型的列,表示小数点后的位数(即精度的小数部分)
+     */
+//    private Integer numericScale;
+
+
+    /**
+     * 对于日期/时间类型的列(如 DATETIME 或 TIMESTAMP),该列表示日期时间值的精度(通常是秒或毫秒)。
+     */
+    private String datetimePrecision;
+
+
+    /**
+     * 是否是索引的一部分
+     * PRI: 主键列
+     * UNI: 唯一索引列
+     * MUL: 非唯一索引列
+     * NULL: 该列没有索引
+     */
+//    private String columnKey;
+
+
+    /**
+     * 列的附加信息
+     * auto_increment: 表示该列为自增列(通常用于主键)
+     * unsigned: 表示该列的无符号属性(对于整数类型)
+     * on update CURRENT_TIMESTAMP: 表示该列的默认值在每次更新时会变为当前时间。
+     */
+//    private String extra;
+
+
+    /**
+     * 该列表示对该列的权限,例如 SELECT, INSERT, UPDATE 等。通常,该字段为 SELECT,INSERT,UPDATE,REFERENCES 等
+     */
+//    private String privileges;
+
+    /**
+     * 字段类型
+     */
+//    private Integer nullable;
+
+    /**
+     * 该列包含列的注释,如果没有注释,则为 NULL
+     */
+    private String columnComment;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/crm/param/CrmLineCustomerListQueryParam.java

@@ -3,9 +3,11 @@ package com.fs.crm.param;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 import java.util.Date;
 
+@EqualsAndHashCode(callSuper = true)
 @Data
 public class CrmLineCustomerListQueryParam extends BaseQueryParam
 {
@@ -88,6 +90,22 @@ public class CrmLineCustomerListQueryParam extends BaseQueryParam
     /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
     private Long attritionLevel;
 
+
+
     /** 意向度 */
     private String intentionDegree;
+
+    private Integer isLine;
+
+    private Integer isPool;
+
+    private Long receiveUserId;
+
+    public Integer getIsLine() {
+        return isLine;
+    }
+
+    public void setIsLine(Integer isLine) {
+        this.isLine = isLine;
+    }
 }

+ 12 - 0
fs-service/src/main/java/com/fs/crm/param/CrmMyCustomerListQueryParam.java

@@ -3,9 +3,11 @@ package com.fs.crm.param;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 
 import java.util.Date;
 
+@EqualsAndHashCode(callSuper = true)
 @Data
 public class CrmMyCustomerListQueryParam extends BaseQueryParam
 {
@@ -95,4 +97,14 @@ public class CrmMyCustomerListQueryParam extends BaseQueryParam
     private String corpId;
     /** 客户级别 */
     private Long customerLevel;
+
+    private Integer isLine;
+
+    public Integer getIsLine() {
+        return isLine;
+    }
+
+    public void setIsLine(Integer isLine) {
+        this.isLine = isLine;
+    }
 }

部分文件因文件數量過多而無法顯示