瀏覽代碼

Merge remote-tracking branch 'origin/master'

yjwang 1 天之前
父節點
當前提交
199e11b566
共有 100 個文件被更改,包括 3414 次插入146 次删除
  1. 43 5
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  2. 4 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  3. 59 13
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  4. 25 0
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  5. 60 2
      fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java
  6. 1 0
      fs-ai-call-task/src/main/resources/logback.xml
  7. 1 0
      fs-cid-workflow/src/main/resources/logback.xml
  8. 43 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  9. 51 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java
  10. 25 14
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java
  11. 6 2
      fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java
  12. 62 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  13. 8 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java
  14. 250 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java
  15. 37 33
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  16. 1 0
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  17. 1 1
      fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  18. 1 1
      fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java
  19. 6 15
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java
  20. 1 1
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  21. 26 0
      fs-service/src/main/java/com/fs/company/domain/CompanyFsUser.java
  22. 15 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUser.java
  23. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java
  24. 15 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java
  25. 15 1
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java
  26. 5 1
      fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java
  27. 53 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyFsUserMapper.java
  28. 9 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java
  29. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.java
  30. 2 0
      fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java
  31. 23 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  32. 9 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java
  33. 5 0
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogSendmsgService.java
  34. 3 2
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java
  35. 1 1
      fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java
  36. 72 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  37. 50 9
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java
  38. 11 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java
  39. 107 8
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  40. 3 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java
  41. 15 15
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  42. 79 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneVO.java
  43. 30 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCount.java
  44. 3 0
      fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogSendmsgVO.java
  45. 14 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java
  46. 4 2
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  47. 258 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerInfo.java
  48. 3 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerPropertyTemplate.java
  49. 12 0
      fs-service/src/main/java/com/fs/crm/dto/CrmCustomerAiAutoTagVo.java
  50. 6 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  51. 25 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerAiTagParam.java
  52. 3 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java
  53. 56 3
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  54. 413 0
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  55. 16 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerAiTagVo.java
  56. 7 1
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  57. 2 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java
  58. 15 0
      fs-service/src/main/java/com/fs/his/vo/CompanyUserBindUserVO.java
  59. 5 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java
  60. 7 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java
  61. 1 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  62. 4 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductAttrValueScrmMapper.java
  63. 2 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  64. 1 0
      fs-service/src/main/java/com/fs/hisStore/param/FsCouponIssueParam.java
  65. 24 0
      fs-service/src/main/java/com/fs/hisStore/param/FsIntegralGoodsListUParam.java
  66. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  67. 25 1
      fs-service/src/main/java/com/fs/live/domain/LiveData.java
  68. 99 0
      fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java
  69. 10 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  70. 27 0
      fs-service/src/main/java/com/fs/live/param/InviteCompareParam.java
  71. 34 0
      fs-service/src/main/java/com/fs/live/param/LiveRoomStudentParam.java
  72. 27 0
      fs-service/src/main/java/com/fs/live/param/ProductCompareParam.java
  73. 51 0
      fs-service/src/main/java/com/fs/live/service/ILiveDataService.java
  74. 314 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  75. 36 0
      fs-service/src/main/java/com/fs/live/vo/InviteCompareVO.java
  76. 25 0
      fs-service/src/main/java/com/fs/live/vo/InviteSalesOptionVO.java
  77. 22 0
      fs-service/src/main/java/com/fs/live/vo/LiveEntryTrendSeriesVO.java
  78. 19 0
      fs-service/src/main/java/com/fs/live/vo/LiveEntryTrendVO.java
  79. 35 0
      fs-service/src/main/java/com/fs/live/vo/LiveRoomStudentQueryVO.java
  80. 35 0
      fs-service/src/main/java/com/fs/live/vo/LiveRoomStudentVO.java
  81. 54 0
      fs-service/src/main/java/com/fs/live/vo/LiveStatisticsOverviewVO.java
  82. 30 0
      fs-service/src/main/java/com/fs/live/vo/ProductCompareVO.java
  83. 8 0
      fs-service/src/main/java/com/fs/newAdv/domain/Lead.java
  84. 1 0
      fs-service/src/main/java/com/fs/newAdv/domain/Site.java
  85. 14 0
      fs-service/src/main/java/com/fs/newAdv/dto/req/FormSubmitReq.java
  86. 4 1
      fs-service/src/main/java/com/fs/newAdv/enums/SystemEventTypeEnum.java
  87. 9 0
      fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java
  88. 56 6
      fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java
  89. 1 1
      fs-service/src/main/java/com/fs/newAdv/service/impl/SiteServiceImpl.java
  90. 137 0
      fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java
  91. 26 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionBaseRequest.java
  92. 27 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionCreateResponse.java
  93. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionDeleteResponse.java
  94. 15 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetRequest.java
  95. 63 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetResponse.java
  96. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListRequest.java
  97. 19 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java
  98. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java
  99. 16 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionRange.java
  100. 9 0
      fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionUpdateResponse.java

+ 43 - 5
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -1,16 +1,22 @@
 package com.fs.app.controller;
 
 import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.base.BusinessException;
 import com.fs.common.result.Result;
+import com.fs.common.service.ISmsService;
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.LandingIndexReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.concurrent.TimeUnit;
 
 /**
  * 落地页控制器
@@ -26,6 +32,10 @@ public class LandingPageController {
 
     @Autowired
     private CallbackProcessingFacadeService facadeService;
+    @Autowired
+    private ISmsService smsService;
+    @Autowired
+    private RedisCache redisCache;
 
     /**
      * 落地页访问
@@ -34,7 +44,7 @@ public class LandingPageController {
     public Result<LandingIndexRes> h5Home(
             @RequestBody LandingIndexReq req) {
         // 查询落地页模板
-        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(),req.getAllParams()));
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getViewUrl(), req.getAllParams()));
     }
 
     /**
@@ -45,6 +55,34 @@ public class LandingPageController {
         return Result.success(facadeService.getWxLandingIndexBySiteId(req));
     }
 
+    /**
+     * 获取验证码
+     *
+     * @return
+     */
+    @GetMapping(value = "/sendSmsCode/{phone}")
+    public Result<String> sendSmsCode(@PathVariable String phone) {
+        String captcha = redisCache.getCacheObject("smsCode"+phone);
+        if (StringUtils.isNotEmpty(captcha)) {
+            throw new BusinessException("短信已发送,1分钟以内请勿重新发送验证码:");
+        }
+        int code = (int) (Math.random() * (9999 - 1000 + 1)) + 1000;// 产生1000-9999的随机数
+        redisCache.setCacheObject("smsCode"+phone,code, 2, TimeUnit.MINUTES);
+        R r = smsService.sendCaptcha(phone, code + "","验证码");
+        return Result.success(String.valueOf(r.get("msg")));
+    }
 
+    /**
+     * @return
+     */
+    @PostMapping(value = "/submit")
+    public Result<String> formSubmit(@RequestBody @Valid FormSubmitReq req) {
+        String captcha = redisCache.getCacheObject(req.getPhone());
+        if (StringUtils.isEmpty(captcha) || !captcha.equals(req.getSmsCode())) {
+            throw new BusinessException("短信验证码有误:" + req.getSmsCode());
+        }
+        facadeService.updateFromByTraceId(req);
+        return Result.success();
+    }
 }
 

+ 4 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -1,10 +1,12 @@
 package com.fs.app.facade;
 
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.req.updateNickNameReq;
 import com.fs.newAdv.dto.res.LandingIndexRes;
 
+import javax.validation.Valid;
 import java.util.Map;
 
 public interface CallbackProcessingFacadeService {
@@ -33,4 +35,6 @@ public interface CallbackProcessingFacadeService {
     void qwExternalIdBindTrack(QwExternalIdBindTrackReq req);
 
     void updateNickName(updateNickNameReq req);
+
+    void updateFromByTraceId(@Valid FormSubmitReq req);
 }

+ 59 - 13
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.app.facade;
 
+import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
@@ -9,9 +10,12 @@ import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.fs.common.utils.RedisUtil;
 import com.fs.common.utils.SnowflakeUtil;
+import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.IGeneralCustomerEntryService;
 import com.fs.newAdv.domain.LandingPageTemplate;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.FormSubmitReq;
 import com.fs.newAdv.dto.req.QwExternalIdBindTrackReq;
 import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
 import com.fs.newAdv.dto.req.updateNickNameReq;
@@ -68,6 +72,10 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
     @Autowired
     private RedisUtil redisUtil;
 
+    @Autowired
+    IGeneralCustomerEntryService iGeneralCustomerEntryService;
+
+
     private static final String TEMPLATE_DATA = "new-adv:template-data:";
 
     @Override
@@ -146,16 +154,9 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
             lead.setAdvertiserId(advertiserId);
             lead.setSiteId(siteId);
             lead.setClickId(clickId);
+            lead.setTraceId(SnowflakeUtil.randomUUID());
             // 设置站点和落地页的关联
             setSiteByIdeaId(siteId, lead.getIdeaId());
-        } else {
-            // 检查站点和广告商信息是否异常
-            if (!Objects.equals(lead.getSiteId(), siteId)) {
-                log.info("落地页站点信息异常:{}---{}", lead.getSiteId(), siteId);
-            }
-            if (!Objects.equals(lead.getAdvertiserId(), advertiserId)) {
-                log.info("落地页广告商信息异常:{}---{}", lead.getAdvertiserId(), advertiserId);
-            }
         }
         // 模板缓存
 /*        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
@@ -186,7 +187,6 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         lead.setUpdateTime(now);
         lead.setViewUrl(viewUrl);
         if (isNewLead) {
-            lead.setTraceId(SnowflakeUtil.randomUUID());
             leadService.save(lead);
         } else {
             leadService.updateById(lead);
@@ -221,12 +221,29 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
                 .filter(ObjectUtil::isNotEmpty)
                 .flatMap(Collection::stream)
                 .map(module -> (JSONObject) module)
-                .filter(module -> "h5-qrcode".equals(module.getStr("type")))
                 .forEach(module -> {
-                    if (StrUtil.isEmpty(qrCode.get())) {
-                        qrCode.set(getQrCodeByAllocationRuleId(site.getLaunchType(), site.getAllocationRule(), site.getAllocationRuleId(), lead));
+                    String type = module.getStr("type");
+
+                    switch (type) {
+                        case "h5-qrcode":
+                            if (StrUtil.isEmpty(qrCode.get())) {
+                                qrCode.set(getQrCodeByAllocationRuleId(
+                                        site.getLaunchType(),
+                                        site.getAllocationRule(),
+                                        site.getAllocationRuleId(),
+                                        lead));
+                            }
+                            module.set("workUrl", qrCode.get());
+                            break;
+
+                        case "h5-customer-link-button":
+                            module.set("workUrl", module.getStr("workUrl") + "?customer_channel=" + lead.getTraceId());
+                            log.info("更新获客链接参数: {}", qrCode.get());
+                            break;
+
+                        default:
+                            break;
                     }
-                    module.set("workUrl", qrCode.get());
                 });
     }
 
@@ -316,4 +333,33 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
         update.setWeiChatName(req.getNickName());
         leadService.updateById(update);
     }
+
+    @Override
+    public void updateFromByTraceId(FormSubmitReq req) {
+        leadService.update(new LambdaUpdateWrapper<Lead>()
+                .eq(Lead::getTraceId, req.getTraceId())
+                .set(Lead::getPhone, req.getPhone()));
+        ThreadUtil.execute(() -> {
+            Lead byTraceId = leadService.getByTraceId(req.getTraceId());
+            EntryCustomerParam param = new EntryCustomerParam();
+            param.setMobile(req.getPhone());
+            param.setCustomerName(req.getName());
+            param.setSceneType(getSceneType(byTraceId.getAdvertiserId()));
+            Site byId = siteService.getById(byTraceId.getSiteId());
+            param.setCompanyId(byId.getCompanyId());
+            iGeneralCustomerEntryService.entryCustomer(param);
+            log.info("广告数据发送到ai电话: {}", param);
+        });
+    }
+
+    private Integer getSceneType(Long advertiserId) {
+        switch (AdvertiserTypeEnum.getByCode(advertiserId)) {
+            case BAIDU:
+                return 2;
+            case OCEANENGINE:
+                return 3;
+            default:
+                return 1;
+        }
+    }
 }

+ 25 - 0
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -33,6 +33,7 @@ import com.fs.live.domain.LiveAfterSales;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.domain.LiveOrderItem;
 import com.fs.live.domain.LiveOrderPayment;
+import com.fs.live.mapper.LiveMapper;
 import com.fs.live.mapper.LiveOrderItemMapper;
 import com.fs.live.mapper.LiveOrderMapper;
 import com.fs.live.mapper.LiveOrderPaymentMapper;
@@ -170,6 +171,12 @@ public class LiveTask {
     @Autowired
     public RedisBatchHandler redisBatchHandler;
 
+    @Autowired
+    private LiveMapper liveMapper;
+
+    @Autowired
+    private ILiveDataService liveDataService;
+
     /**
      * 查询被拆分的订单,然后查询拆分订单的物流信息
      */
@@ -677,4 +684,22 @@ public class LiveTask {
         redisBatchHandler.consumeBatchData();
     }
 
+    /**
+     * 直播间数据概览缓存定时任务:查询结束时间在7天之内的直播间,执行数据概览统计并缓存到 live_data
+     */
+    public void cacheLiveStatisticsOverview() {
+        try {
+            List<Long> liveIds = liveMapper.selectLiveIdsByFinishTimeWithinDays(7);
+            if (liveIds == null || liveIds.isEmpty()) {
+                log.debug("直播间数据概览缓存任务:7天内无结束的直播间");
+                return;
+            }
+            log.info("直播间数据概览缓存任务:开始处理 {} 个直播间", liveIds.size());
+            liveDataService.calculateAndSaveOverviewForLive(liveIds);
+            log.info("直播间数据概览缓存任务:完成");
+        } catch (Exception e) {
+            log.error("直播间数据概览缓存任务异常", e);
+        }
+    }
+
 }

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

@@ -10,8 +10,7 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.live.domain.LiveData;
-import com.fs.live.param.LiveDataCompanyParam;
-import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.*;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveUserFirstVo;
@@ -190,6 +189,65 @@ public class LiveDataController extends BaseController {
         return util.exportExcel(list, "直播间用户详情数据");
     }
 
+    /**
+     * 直播数据统计-数据概览(12项指标)
+     * @param liveIds 直播间ID列表,前端传入
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveStatisticsOverview")
+    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
+
+        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播趋势-进入人数折线图
+     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+     * @param liveIds 直播间ID列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveEntryTrend")
+    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播间学员列表(分页,基于 live_user_first_entry)
+     * 筛选:直播名称(liveIds)、首次访问时间范围
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listLiveRoomStudents")
+    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
+        return liveDataService.listLiveRoomStudents(param);
+    }
+
+    /**
+     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listProductCompareStats")
+    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
+        return liveDataService.listProductCompareStats(param);
+    }
+
+    /**
+     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteSalesOptions")
+    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteCompareStats")
+    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
+        return liveDataService.listInviteCompareStats(param);
+    }
+
     /**
      * 查询分公司直播数据统计列表
      */

+ 1 - 0
fs-ai-call-task/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-ai-call-task/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

+ 1 - 0
fs-cid-workflow/src/main/resources/logback.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
+    <springProperty scope="context" name="cidGroupNo" source="cid-group-no"/>
     <!-- 日志存放路径 -->
 	<property name="log.path" value="/home/fs-cid-workflow/${cidGroupNo}/logs" />
     <!-- 日志输出格式 -->

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

@@ -30,10 +30,13 @@ import com.fs.company.vo.CompanyUserQwListVO;
 import com.fs.company.vo.CompanyUserVO;
 import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsUser;
 import com.fs.his.utils.qrcode.QRCodeUtils;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.service.IFsUserScrmService;
 import com.fs.hisStore.vo.FsStoreProductExportVO;
@@ -912,4 +915,44 @@ public class CompanyUserController extends BaseController {
         List<com.fs.hisStore.domain.FsUserScrm> userList = companyUserService.selectBoundFsUsersByCompanyUserId(companyUserId);
         return R.ok().put("data", userList);
     }
+
+    /**
+     * 获取销售绑定的fs_user
+     */
+
+    @GetMapping("/getFsUserBySaleId")
+    public  TableDataInfo getFsUserBySaleId(FsUser fsUser){
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        fsUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<CompanyUserBindUserVO> list = companyUserService.getFsUserByCompanyUserId(fsUser);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 绑定销售和fs_user 的关系(该销售绑定fs_user)
+     */
+    @ApiOperation("绑定销售和fs_user 的关系(该销售绑定fs_user)")
+//    @PreAuthorize("@ss.hasPermi('company:user:bindUser')")
+    @PostMapping("/bindSaleAndFsUser")
+    public R bindSaleAndFsUser(@RequestBody FsUserCompanyUser companyUser){
+        int i = companyUserService.bindCompanyUserAndFsUser(companyUser.getCompanyUserId(), companyUser.getUserId());
+        return i>0?R.ok():R.error("绑定失败");
+    }
+
+    /**
+     * 解绑销售和 fs_user 的关系
+     */
+    @ApiOperation("解绑销售和 fs_user 的关系")
+    @PostMapping("/unbindSaleAndFsUser")
+    public R unbindSaleAndFsUser(@RequestBody FsUserCompanyUser companyUser) {
+        try {
+            boolean result = companyUserService.unbindCompanyUserAndFsUser(companyUser.getCompanyUserId(), companyUser.getUserId());
+            return result ? R.ok("解绑成功") : R.error("解绑失败");
+        } catch (Exception e) {
+            logger.error("解绑销售和用户关系异常", e);
+            return R.error("操作失败:" + e.getMessage());
+        }
+    }
 }

+ 51 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogCallphoneController.java

@@ -2,6 +2,11 @@ package com.fs.company.controller.company;
 
 import java.util.ArrayList;
 import java.util.List;
+
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -60,6 +65,52 @@ public class CompanyVoiceRoboticCallLogCallphoneController extends BaseControlle
         }
     }
 
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/listByCallerIdAndRoboticId")
+    public TableDataInfo listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+
+    }
+
+
+    /**
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/groupList")
+    public TableDataInfo groupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        startPage();
+        List<CompanyVoiceRoboticCallLogCallphone> list = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallPhoneLogCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogCallphoneService.selectCompanyVoiceRoboticCallPhoneLogCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
+    }
+
+    /**
+     * 导出调用日志_ai打电话列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:callphonelog:export')")
+    @Log(title = "调用日志_ai打电话", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone)
+    {
+        List<CompanyVoiceRoboticCallLogCallPhoneVO> list = companyVoiceRoboticCallLogCallphoneService.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+        ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO> util = new ExcelUtil<CompanyVoiceRoboticCallLogCallPhoneVO>(CompanyVoiceRoboticCallLogCallPhoneVO.class);
+        return util.exportExcel(list, "调用日志_ai打电话数据");
+    }
+
 //    /**
 //     * 导出调用日志_ai打电话列表
 //     */

+ 25 - 14
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceRoboticCallLogSendmsgController.java

@@ -4,6 +4,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,14 +41,14 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
     @Autowired
     private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
     /**
-     * 查询调用日志_发送短信列表
+     * 查询调用日志_发送短信列表(按照任务id分组,任务id-任务名称-查询总任务数量-成功数量)
      */
     @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
     @GetMapping("/list")
     public TableDataInfo list(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
     {
         startPage();
-        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
         return getDataTable(list);
     }
 
@@ -75,21 +76,31 @@ public class CompanyVoiceRoboticCallLogSendmsgController extends BaseController
             List<CompanyVoiceRoboticCallLogSendmsgVO> list = companyVoiceRoboticCallLogSendmsgService.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
             return getDataTable(list);
         }
+    }
 
+    /**
+     * 查询调用日志_发送短信列表统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:list')")
+    @GetMapping("/count")
+    public AjaxResult selectCompanyVoiceRoboticCallLogSendMsgCount()
+    {
+        CompanyVoiceRoboticCallLogCount companyVoiceRoboticCallLogCount = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendMsgCount();
+        return AjaxResult.success(companyVoiceRoboticCallLogCount);
     }
 
-//    /**
-//     * 导出调用日志_发送短信列表
-//     */
-//    @PreAuthorize("@ss.hasPermi('company:sendmsglog:export')")
-//    @Log(title = "调用日志_发送短信", businessType = BusinessType.EXPORT)
-//    @GetMapping("/export")
-//    public AjaxResult export(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
-//    {
-//        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
-//        ExcelUtil<CompanyVoiceRoboticCallLogSendmsg> util = new ExcelUtil<CompanyVoiceRoboticCallLogSendmsg>(CompanyVoiceRoboticCallLogSendmsg.class);
-//        return util.exportExcel(list, "调用日志_发送短信数据");
-//    }
+    /**
+     * 导出调用日志_发送短信列表
+     */
+    @PreAuthorize("@ss.hasPermi('company:sendmsglog:export')")
+    @Log(title = "调用日志_发送短信", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg)
+    {
+        List<CompanyVoiceRoboticCallLogSendmsg> list = companyVoiceRoboticCallLogSendmsgService.selectCompanyVoiceRoboticCallLogSendmsgList(companyVoiceRoboticCallLogSendmsg);
+        ExcelUtil<CompanyVoiceRoboticCallLogSendmsg> util = new ExcelUtil<CompanyVoiceRoboticCallLogSendmsg>(CompanyVoiceRoboticCallLogSendmsg.class);
+        return util.exportExcel(list, "调用日志_发送短信数据");
+    }
 //
 //    /**
 //     * 获取调用日志_发送短信详细信息

+ 6 - 2
fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java

@@ -1,8 +1,10 @@
 package com.fs.company.controller.company;
 
 import com.fs.common.core.domain.R;
+import com.fs.company.param.EntryCustomerParam;
 import com.fs.company.service.IGeneralCustomerEntryService;
 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.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -20,8 +22,10 @@ public class GeneralCustomerEntryController {
     IGeneralCustomerEntryService iGeneralCustomerEntryService;
 
     @PostMapping("/entryCustomer")
-    public R entryCustomer(String param){
-       return iGeneralCustomerEntryService.entryCustomer(param);
+    public R entryCustomer(EntryCustomerParam param){
+        iGeneralCustomerEntryService.entryCustomer(param);
+       return R.ok("success");
     }
 
+
 }

+ 62 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -15,6 +15,9 @@ import com.fs.framework.service.TokenService;
 import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.LiveRoomStudentParam;
+import com.fs.live.param.InviteCompareParam;
+import com.fs.live.param.ProductCompareParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
 import com.fs.live.vo.LiveDataCompanyVO;
@@ -46,6 +49,65 @@ public class LiveDataController extends BaseController
     @Autowired
     private TokenService tokenService;
 
+//    /**
+//     * 直播数据统计-数据概览(12项指标)
+//     * @param liveIds 直播间ID列表,前端传入
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/getLiveStatisticsOverview")
+//    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
+//
+//        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 直播趋势-进入人数折线图
+//     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+//     * @param liveIds 直播间ID列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/getLiveEntryTrend")
+//    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
+//        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 直播间学员列表(分页,基于 live_user_first_entry)
+//     * 筛选:直播名称(liveIds)、首次访问时间范围
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listLiveRoomStudents")
+//    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
+//        return liveDataService.listLiveRoomStudents(param);
+//    }
+//
+//    /**
+//     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listProductCompareStats")
+//    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
+//        return liveDataService.listProductCompareStats(param);
+//    }
+//
+//    /**
+//     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listInviteSalesOptions")
+//    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
+//        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
+//    }
+//
+//    /**
+//     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+//     */
+//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+//    @PostMapping("/listInviteCompareStats")
+//    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
+//        return liveDataService.listInviteCompareStats(param);
+//    }
+
     /**
      * 查询直播间详情数据(SQL方式)
      * @param liveId 直播间ID

+ 8 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java

@@ -5,6 +5,9 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.fs.common.result.Result;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
 import com.fs.newAdv.domain.Site;
 import com.fs.newAdv.service.ISiteService;
 import lombok.extern.slf4j.Slf4j;
@@ -27,6 +30,9 @@ public class SiteController {
     @Autowired
     private ISiteService siteService;
 
+    @Autowired
+    private TokenService tokenService;
+
     @GetMapping("/page")
     public Result<IPage<Site>> pageSiteStatistics(
             @RequestParam(defaultValue = "1") Long pageNum,
@@ -66,6 +72,8 @@ public class SiteController {
      */
     @PostMapping
     public Result<Void> create(@RequestBody Site site) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        site.setCompanyId(loginUser.getCompany().getCompanyId());
         siteService.createSite(site);
         return Result.success();
     }

+ 250 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwAcquisitionAssistantController.java

@@ -0,0 +1,250 @@
+package com.fs.company.controller.qw;
+
+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.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwAcquisitionAssistant;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.dto.acquisition.AcquisitionListResponse;
+import com.fs.qw.service.IQwAcquisitionAssistantService;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.AcquisitionAssistantDetailVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理Controller
+ *
+ * @author fs
+ * @date 2026-03-16
+ */
+@Slf4j
+@RestController
+@RequestMapping("/qw/acquisitionAssistant")
+public class QwAcquisitionAssistantController extends BaseController {
+    @Autowired
+    private IQwAcquisitionAssistantService qwAcquisitionAssistantService;
+
+    @Autowired
+    private IQwCompanyService qwCompanyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    /**
+     * 查询企微-获客链接管理列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(QwAcquisitionAssistant qwAcquisitionAssistant) {
+        startPage();
+        List<QwAcquisitionAssistant> list = qwAcquisitionAssistantService.selectQwAcquisitionAssistantList(qwAcquisitionAssistant);
+        return getDataTable(list);
+    }
+
+    /**
+     * 从企微同步获客链接列表(全量)
+     * 手动点击同步按钮时调用
+     */
+    @PostMapping("/syncList")
+    public AjaxResult syncList(@RequestParam String corpId) {
+        try {
+
+            QwCompany qwCompany = getQwCompany(corpId);
+
+            // 调用同步服务
+            String result = qwAcquisitionAssistantService.syncListFromQw(qwCompany.getCorpId(), qwCompany.getOpenSecret());
+
+            return AjaxResult.success(result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 分页获取企微列表(直接调用企微接口)
+     * 用于查看企微原始数据
+     */
+    @GetMapping("/qwList")
+    public AjaxResult getQwList(@RequestParam(required = false) Integer limit,
+                                @RequestParam(required = false) String cursor,
+                                @RequestParam String corpId) {
+        try {
+            QwCompany qwCompany = getQwCompany(corpId);
+            // 调用企微列表接口
+            AcquisitionListResponse response = qwAcquisitionAssistantService.getQwList(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), limit, cursor);
+
+            return AjaxResult.success(response);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据linkId直接获取详情
+     *
+     * @param linkId 企微链接ID
+     */
+    @GetMapping("/getDetailByLinkId/{linkId}")
+    public AjaxResult getDetailByLinkId(@PathVariable String linkId) {
+        try {
+            // 调用获取详情服务
+            AcquisitionAssistantDetailVO detailVo = qwAcquisitionAssistantService.getDetailWithQw(linkId);
+            return AjaxResult.success("获取成功", detailVo);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 新增企微-获客链接管理
+     */
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            qwAcquisitionAssistant.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.createWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+            return AjaxResult.success("创建成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/qwUserList")
+    public AjaxResult getQwUserList(@RequestBody QwUser qwUser) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByAcquisition(qwUser);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 获取企微用户主体列表
+     */
+    @PostMapping("/qwUserCompanyList")
+    public AjaxResult getQwUserCompanyList(@RequestBody QwCompany qwCompany) {
+        qwCompany.setStatus(1L);//启用状态的主体
+        List<QwCompany> qwCompanyList = qwCompanyService.selectQwCompanyList(qwCompany);
+        if (qwCompanyList != null && !qwCompanyList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwCompanyList);
+        } else {
+            return AjaxResult.error("未找到主体");
+        }
+    }
+
+    /**
+     * 获取企微用户列表
+     */
+    @PostMapping("/getQwUserListByIds")
+    public AjaxResult getQwUserListByIds(@RequestBody List<Long> qwUserTableIds) {
+        if (qwUserTableIds == null || qwUserTableIds.isEmpty()) {
+            return AjaxResult.success(Collections.emptyList());
+        }
+        // 限制最多500个,避免性能问题
+        if (qwUserTableIds.size() > 500) {
+            qwUserTableIds = qwUserTableIds.subList(0, 500);
+        }
+        List<QwUser> qwUserList = qwUserService.selectQwUserListByIds(qwUserTableIds);
+        if (qwUserList != null && !qwUserList.isEmpty()) {
+            return AjaxResult.success("获取成功", qwUserList);
+        } else {
+            return AjaxResult.error("未找到用户");
+        }
+    }
+
+    /**
+     * 修改企微-获客链接
+     */
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody QwAcquisitionAssistant qwAcquisitionAssistant) {
+        try {
+            // 参数校验
+            if (qwAcquisitionAssistant.getId() == null) {
+                return AjaxResult.error("ID不能为空");
+            }
+            if (StringUtils.isEmpty(qwAcquisitionAssistant.getLinkId())) {
+                return AjaxResult.error("链接ID不能为空");
+            }
+
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+            qwAcquisitionAssistant.setUpdateBy(String.valueOf(loginUser.getUser().getUserId()));
+            QwCompany qwCompany = getQwCompany(qwAcquisitionAssistant.getCorpId());
+            QwAcquisitionAssistant result = qwAcquisitionAssistantService.updateWithQw(
+                    qwCompany.getCorpId(), qwCompany.getOpenSecret(), qwAcquisitionAssistant);
+
+            return AjaxResult.success("修改成功", result);
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除企微-获客链接
+     *
+     * @param id 本地记录ID
+     */
+    @GetMapping("/delete/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        try {
+            // 先查询本地记录,获取linkId
+            QwAcquisitionAssistant assistant = qwAcquisitionAssistantService.selectQwAcquisitionAssistantById(id);
+            if (assistant == null) {
+                return AjaxResult.error("获客链接不存在");
+            }
+            QwCompany qwCompany = getQwCompany(assistant.getCorpId());
+            // 调用删除服务
+            qwAcquisitionAssistantService.deleteWithQw(qwCompany.getCorpId(), qwCompany.getOpenSecret(), assistant);
+
+            return AjaxResult.success("删除成功");
+        } catch (CustomException e) {
+            return AjaxResult.error(e.getMessage());
+        } catch (Exception e) {
+            return AjaxResult.error("系统异常:" + e.getMessage());
+        }
+    }
+
+    private QwCompany getQwCompany(String corpId) {
+        if (StringUtils.isBlank(corpId)) {
+            log.error("获客链接管理参数异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        QwCompany qwCompany = qwCompanyService.selectQwCompanyByCorpId(corpId);
+        if (qwCompany == null) {
+            log.error("获客链接管理-企微主体获取异常:{}", corpId);
+            throw new CustomException("未找到企业微信主体");
+        }
+        return qwCompany;
+    }
+}

+ 37 - 33
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -25,6 +25,7 @@ import com.fs.common.utils.spring.SpringUtils;
 import com.fs.live.domain.*;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
+import com.fs.newAdv.service.ILeadService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.time.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -71,7 +72,7 @@ public class WebSocketServer {
     private final static long HEARTBEAT_TIMEOUT = 2 * 60 * 1000;
     // admin房间消息发送线程池(单线程,保证串行化)
     private final static ConcurrentHashMap<Long, ExecutorService> adminExecutors = new ConcurrentHashMap<>();
-    
+
     // 消息队列系统
     // 每个直播间的消息队列,使用优先级队列支持管理员消息插队
     private final static ConcurrentHashMap<Long, PriorityBlockingQueue<QueueMessage>> messageQueues = new ConcurrentHashMap<>();
@@ -104,6 +105,7 @@ public class WebSocketServer {
     private final ILiveWatchLogService liveWatchLogService = SpringUtils.getBean(ILiveWatchLogService.class);
     private final ILiveVideoService liveVideoService = SpringUtils.getBean(ILiveVideoService.class);
     private final ILiveCompletionPointsRecordService completionPointsRecordService = SpringUtils.getBean(ILiveCompletionPointsRecordService.class);
+    private final ILeadService leadService = SpringUtils.getBean(ILeadService.class);
     private static Random random = new Random();
 
     // Redis key 前缀:用户进入直播间时间
@@ -292,6 +294,8 @@ public class WebSocketServer {
                     liveUserFirstEntry.setExternalContactId(externalContactId);
                 }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
+                // 第一次进入直播间 发送广告线索
+                leadService.enterLive(userId, liveId);
             }
             redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
 
@@ -309,7 +313,7 @@ public class WebSocketServer {
         sessionLocks.putIfAbsent(session.getId(), new ReentrantLock());
         // 初始化心跳时间
         heartbeatCache.put(session.getId(), System.currentTimeMillis());
-        
+
         // 如果有session,启动消费者线程
         ConcurrentHashMap<Long, Session> tempRoom = getRoom(liveId);
         List<Session> tempAdminRoom = getAdminRoom(liveId);
@@ -400,7 +404,7 @@ public class WebSocketServer {
         // 清理Session相关资源
         heartbeatCache.remove(session.getId());
         sessionLocks.remove(session.getId());
-        
+
         // 检查并清理空的直播间资源
         cleanupEmptyRoom(liveId);
     }
@@ -1621,7 +1625,7 @@ public class WebSocketServer {
     private void startConsumerThread(Long liveId) {
         consumerRunningFlags.computeIfAbsent(liveId, k -> new AtomicBoolean(false));
         AtomicBoolean runningFlag = consumerRunningFlags.get(liveId);
-        
+
         // 如果线程已经在运行,直接返回
         if (runningFlag.get()) {
             return;
@@ -1633,16 +1637,16 @@ public class WebSocketServer {
                 Thread consumerThread = new Thread(() -> {
                     PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
                     log.info("[消息队列] 启动消费者线程, liveId={}", liveId);
-                    
+
                     while (runningFlag.get()) {
                         try {
                             // 检查是否还有session,如果没有则退出
                             ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
                             List<Session> adminRoom = adminRooms.get(liveId);
-                            
-                            boolean hasSession = (room != null && !room.isEmpty()) || 
+
+                            boolean hasSession = (room != null && !room.isEmpty()) ||
                                                 (adminRoom != null && !adminRoom.isEmpty());
-                            
+
                             if (!hasSession) {
                                 log.info("[消息队列] 直播间无session,停止消费者线程, liveId={}", liveId);
                                 break;
@@ -1667,13 +1671,13 @@ public class WebSocketServer {
                             log.error("[消息队列] 消费消息异常, liveId={}", liveId, e);
                         }
                     }
-                    
+
                     // 清理资源
                     runningFlag.set(false);
                     consumerThreads.remove(liveId);
                     log.info("[消息队列] 消费者线程已停止, liveId={}", liveId);
                 }, "MessageConsumer-" + liveId);
-                
+
                 consumerThread.setDaemon(true);
                 consumerThread.start();
                 consumerThreads.put(liveId, consumerThread);
@@ -1705,22 +1709,22 @@ public class WebSocketServer {
     private boolean enqueueMessage(Long liveId, String message, boolean isAdmin) {
         PriorityBlockingQueue<QueueMessage> queue = getMessageQueue(liveId);
         AtomicLong currentSize = queueSizes.computeIfAbsent(liveId, k -> new AtomicLong(0));
-        
+
         // 计算新消息的大小
         long messageSize = message != null ? message.getBytes(StandardCharsets.UTF_8).length : 0;
-        
+
         // 检查队列条数限制
         if (!isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
             log.warn("[消息队列] 队列条数已满,丢弃消息, liveId={}, queueSize={}", liveId, queue.size());
             return false;
         }
-        
+
         // 检查队列大小限制(200MB)
         long newTotalSize = currentSize.get() + messageSize;
         if (newTotalSize > MAX_QUEUE_SIZE_BYTES) {
             if (!isAdmin) {
                 // 普通消息超过大小限制,直接丢弃
-                log.warn("[消息队列] 队列大小超过限制,丢弃普通消息, liveId={}, currentSize={}MB, messageSize={}KB", 
+                log.warn("[消息队列] 队列大小超过限制,丢弃普通消息, liveId={}, currentSize={}MB, messageSize={}KB",
                         liveId, currentSize.get() / (1024.0 * 1024.0), messageSize / 1024.0);
                 return false;
             } else {
@@ -1728,13 +1732,13 @@ public class WebSocketServer {
                 long needToFree = newTotalSize - MAX_QUEUE_SIZE_BYTES;
                 long freedSize = removeMessagesToFreeSpace(queue, currentSize, needToFree, true);
                 if (freedSize < needToFree) {
-                    log.warn("[消息队列] 无法释放足够空间,管理员消息可能无法入队, liveId={}, needToFree={}KB, freed={}KB", 
+                    log.warn("[消息队列] 无法释放足够空间,管理员消息可能无法入队, liveId={}, needToFree={}KB, freed={}KB",
                             liveId, needToFree / 1024.0, freedSize / 1024.0);
                     // 即使空间不足,也尝试入队(可能会超过限制,但管理员消息优先级高)
                 }
             }
         }
-        
+
         // 如果是管理员消息且队列条数已满,移除一个普通消息
         if (isAdmin && queue.size() >= MAX_QUEUE_SIZE) {
             // 由于是优先级队列,普通消息(priority=0)会在队列末尾
@@ -1758,21 +1762,21 @@ public class WebSocketServer {
                 log.warn("[消息队列] 队列条数已满且无普通消息可移除, liveId={}", liveId);
             }
         }
-        
+
         QueueMessage queueMessage = new QueueMessage(message, isAdmin);
         queue.offer(queueMessage);
         currentSize.addAndGet(messageSize);
-        
+
         // 如果有session,确保消费者线程在运行
         ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
         List<Session> adminRoom = adminRooms.get(liveId);
-        boolean hasSession = (room != null && !room.isEmpty()) || 
+        boolean hasSession = (room != null && !room.isEmpty()) ||
                             (adminRoom != null && !adminRoom.isEmpty());
-        
+
         if (hasSession) {
             startConsumerThread(liveId);
         }
-        
+
         return true;
     }
 
@@ -1784,13 +1788,13 @@ public class WebSocketServer {
      * @param onlyRemoveNormal 是否只移除普通消息(true=只移除普通消息,false=可以移除任何消息)
      * @return 实际释放的空间(字节数)
      */
-    private long removeMessagesToFreeSpace(PriorityBlockingQueue<QueueMessage> queue, 
-                                          AtomicLong currentSize, 
-                                          long needToFree, 
+    private long removeMessagesToFreeSpace(PriorityBlockingQueue<QueueMessage> queue,
+                                          AtomicLong currentSize,
+                                          long needToFree,
                                           boolean onlyRemoveNormal) {
         long freedSize = 0;
         List<QueueMessage> toRemove = new ArrayList<>();
-        
+
         // 收集需要移除的消息(优先移除普通消息)
         Iterator<QueueMessage> iterator = queue.iterator();
         while (iterator.hasNext() && freedSize < needToFree) {
@@ -1800,7 +1804,7 @@ public class WebSocketServer {
                 freedSize += msg.getSizeBytes();
             }
         }
-        
+
         // 如果只移除普通消息但空间还不够,可以移除管理员消息
         if (onlyRemoveNormal && freedSize < needToFree) {
             iterator = queue.iterator();
@@ -1812,19 +1816,19 @@ public class WebSocketServer {
                 }
             }
         }
-        
+
         // 移除消息并更新大小
         for (QueueMessage msg : toRemove) {
             if (queue.remove(msg)) {
                 currentSize.addAndGet(-msg.getSizeBytes());
             }
         }
-        
+
         if (freedSize > 0) {
-            log.info("[消息队列] 释放队列空间, removedCount={}, freedSize={}KB", 
+            log.info("[消息队列] 释放队列空间, removedCount={}, freedSize={}KB",
                     toRemove.size(), freedSize / 1024.0);
         }
-        
+
         return freedSize;
     }
 
@@ -1841,10 +1845,10 @@ public class WebSocketServer {
     private void cleanupEmptyRoom(Long liveId) {
         ConcurrentHashMap<Long, Session> room = rooms.get(liveId);
         List<Session> adminRoom = adminRooms.get(liveId);
-        
-        boolean hasSession = (room != null && !room.isEmpty()) || 
+
+        boolean hasSession = (room != null && !room.isEmpty()) ||
                             (adminRoom != null && !adminRoom.isEmpty());
-        
+
         if (!hasSession) {
             // 停止消费者线程
             stopConsumerThread(liveId);

+ 1 - 0
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -206,6 +206,7 @@ public class QwDataCallbackService {
                             if(StateList.getLength() > 0) {
                                 State = StateList.item(0).getTextContent();
                             }
+                            log.info("添加企微State参数: {}",State);
                             String WelcomeCode =null;
                             NodeList WelcomeCodeList = root.getElementsByTagName("WelcomeCode");
                             if(WelcomeCodeList.getLength() > 0) {

+ 1 - 1
fs-qw-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -108,7 +108,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/**/*.js",
                         "/profile/**"
                 ).permitAll()
-
+                .antMatchers("/zentao/**").denyAll() // 拒绝访问
                 .antMatchers("/**").anonymous()
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/msg/**/**").anonymous()

+ 1 - 1
fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java

@@ -11,7 +11,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
-@EnableScheduling
+//@EnableScheduling
 public class FsQwhookSopApplication {
 
 

+ 6 - 15
fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java

@@ -2,8 +2,10 @@ package com.fs.app.controller;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwUserService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -21,25 +23,14 @@ public class testController {
     testService testService;
     @Autowired
     private RedisCache redisCache;
+    @Autowired
+    private  IQwUserService qwUserService;
 
     @GetMapping("/qwHookNotify")
     public R qwHookNotify() {
 
-//        redisCache.setCacheObject("qwUserRd:"+12313+":"+12313 ,JSON.toJSONString(null),1, TimeUnit.HOURS);
-
-//
-//        List<QwUser> qwUserAllKey = qwUserMapper.getQwUserAllKey();
-//
-//
-//        int i=1;
-//
-//        for (QwUser qwUser : qwUserAllKey) {
-//            System.out.println(qwUser);
-//            i++;
-//            System.out.println("执行到第:"+i);
-//            testService.add(qwUser);
-//
-//        }
+        QwUser user = qwUserMapper.selectById(34205);
+        qwUserService.atMsg(user, "掉线提醒(登录异常)");
 
 
         return  R.ok();

+ 1 - 1
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -921,7 +921,7 @@ public class SmsServiceImpl implements ISmsService
      * @param temp
      * @param param
      */
-    @Async
+//    @Async
     public void batchSmsOp4AiSend(CompanySmsTemp temp, SmsSendBatchParam param){
         CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
         for(Long id:param.getCustomerIds()){

+ 26 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyFsUser.java

@@ -0,0 +1,26 @@
+package com.fs.company.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+@Data
+public class CompanyFsUser extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long id;
+
+    /** fs_user 用户id */
+    @Excel(name = "fs_user 用户id")
+    private Long fsUserId;
+
+    /** company_user 销售id */
+    @Excel(name = "company_user 销售id")
+    private Long companyUserId;
+
+    /** 绑定状态 */
+    @Excel(name = "绑定状态")
+    private Integer status;
+
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyUser.java

@@ -195,11 +195,26 @@ public class CompanyUser extends BaseEntity
     @TableField(exist = false)
     private List<Long> deptList;
 
+
+
+    /**
+     * 多个用户ID,逗号分隔
+     */
+    private  String  fsUserId;
+
     /**
      * cid服务id
      */
     private Long cidServerId;
 
+    public String getFsUserId() {
+        return fsUserId;
+    }
+
+    public void setFsUserId(String fsUserId) {
+        this.fsUserId = fsUserId;
+    }
+
     public String getMaOpenId() {
         return maOpenId;
     }

+ 5 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogAddwx.java

@@ -69,7 +69,10 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
 
     @TableField(exist = false)
     private List<Long> wxclientIds;
-    public static CompanyVoiceRoboticCallLogAddwx initCallLog( String runParam, Long keyId, Long taskId,Long wxAccountId,Long companyId) {
+
+    private Integer qwWxAddWayId;
+
+    public static CompanyVoiceRoboticCallLogAddwx initCallLog( String runParam, Long keyId, Long taskId,Long wxAccountId,Long companyId,int qwWxAddWayId) {
         CompanyVoiceRoboticCallLogAddwx log = new CompanyVoiceRoboticCallLogAddwx();
         log.wxClientId = keyId;
         log.runParam = runParam;
@@ -77,6 +80,7 @@ public class CompanyVoiceRoboticCallLogAddwx extends BaseEntity{
         log.runTime = new Date();
         log.wxAccountId = wxAccountId;
         log.companyId = companyId;
+        log.qwWxAddWayId=qwWxAddWayId;
         return log;
     }
 

+ 15 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogCallphone.java

@@ -115,6 +115,21 @@ public class CompanyVoiceRoboticCallLogCallphone extends BaseEntity{
     @TableField(exist = false)
     private List<Long> callerIds;
 
+    @TableField(exist = false)
+    private String roboticName;
+
+    @TableField(exist = false)
+    private Integer totalRecordCount;
+
+    @TableField(exist = false)
+    private Integer successCount;
+
+    @TableField(exist = false)
+    private Integer failCount;
+
+    @TableField(exist = false)
+    private Integer runningCount;
+
     public static CompanyVoiceRoboticCallLogCallphone initCallLog( String runParam, Long keyId, Long taskId,Long companyId) {
         CompanyVoiceRoboticCallLogCallphone log = new CompanyVoiceRoboticCallLogCallphone();
         log.callerId = keyId;

+ 15 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRoboticCallLogSendmsg.java

@@ -49,7 +49,7 @@ public class CompanyVoiceRoboticCallLogSendmsg extends BaseEntity{
     private String result;
 
     /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
-    @Excel(name = "执行状态:1、执行中,2、执行成功,3、执行失败")
+    @Excel(name = "执行状态", readConverterExp = "1=执行中,2=执行成功,3=执行失败")
     private Integer status;
 
     /** 公司id */
@@ -80,6 +80,20 @@ public class CompanyVoiceRoboticCallLogSendmsg extends BaseEntity{
 
     private String callbackUuid;
 
+    private String name;
+
+    private Integer totalRecordCount;
+
+    private Integer successCount;
+
+    private Integer failCount;
+
+    private Integer runningCount;
+
+    private String phone;
+
+
+
     public static CompanyVoiceRoboticCallLogSendmsg initCallLog( String runParam, Long keyId, Long taskId,Long companyId,Long companyUserId,Long tempId) {
         CompanyVoiceRoboticCallLogSendmsg log = new CompanyVoiceRoboticCallLogSendmsg();
         log.callerId = keyId;

+ 5 - 1
fs-service/src/main/java/com/fs/company/domain/CompanyWxClient.java

@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
 
 /**
  * 添加个微信账号对象 company_wx_client
- * 
+ *
  * @author fs
  * @date 2024-12-09
  */
@@ -88,4 +88,8 @@ public class CompanyWxClient extends BaseEntityTow
     /** 是否微信 */
     @Excel(name = "加微类型1个微2企微(防止add_type被占用)")
     private Integer isWeCom;
+    /**
+     * 投流来源id
+     */
+    private String traceId;
 }

+ 53 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyFsUserMapper.java

@@ -0,0 +1,53 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyFsUser;
+import com.fs.his.domain.FsUser;
+import com.fs.his.vo.CompanyUserBindUserVO;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+public interface CompanyFsUserMapper {
+
+    /**
+     * 新增公司用户绑定关系
+     *
+     * @param companyFsUser 公司用户绑定关系
+     * @return 结果
+     */
+    public int insertCompanyFsUser(CompanyFsUser companyFsUser);
+
+    /**
+     * 根据FS用户ID查询绑定关系
+     *
+     * @param fsUserId FS用户ID
+     * @return 公司用户绑定关系
+     */
+    public CompanyFsUser selectByFsUserId(Long fsUserId);
+
+    /**
+     * 更新公司用户绑定关系
+     *
+     * @param companyFsUser 公司用户绑定关系
+     * @return 结果
+     */
+    public int updateCompanyFsUser(CompanyFsUser companyFsUser);
+
+    /**
+     * 销售公司用户列表
+     */
+    List<CompanyUserBindUserVO> selecUserList(FsUser fsUser);
+
+    /**
+     * 根据销售id 查询绑定的用户
+     */
+    String selectUserListBySalesId(Long companyUserId);
+
+    @Select("select * from company_fs_user where company_user_id=#{companyUserId} and fs_user_id=#{userId}")
+    CompanyFsUser selectByCompanyUserIdAndUserId(@Param("companyUserId") Long companyUserId, @Param("userId") Long userId);
+
+    @Delete("delete from company_fs_user WHERE id=#{id}")
+    int deleteById(Long id);
+}

+ 9 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogCallphoneMapper.java

@@ -4,6 +4,8 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import org.apache.ibatis.annotations.Param;
 
 /**
@@ -75,4 +77,11 @@ public interface CompanyVoiceRoboticCallLogCallphoneMapper extends BaseMapper<Co
      * @return 当天通话次数,如果没有记录返回0
      */
     int countTodayCallsByBusinessId(@Param("businessId") Long businessId);
+
+    List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+
+
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticCallLogSendmsgMapper.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.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 
 /**
@@ -28,6 +29,8 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
      */
     List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
+    List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
     /**
      * 新增调用日志_发送短信
      * 
@@ -61,4 +64,6 @@ public interface CompanyVoiceRoboticCallLogSendmsgMapper extends BaseMapper<Comp
     int deleteCompanyVoiceRoboticCallLogSendmsgByLogIds(Long[] logIds);
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
 }

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

@@ -162,5 +162,7 @@ public class EntryCustomerParam {
     private Integer sceneType;
     //对话图
     private String dialogue;
+    //投流id
+    private String traceId;
 
 }

+ 23 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java

@@ -7,7 +7,9 @@ import com.fs.company.param.CompanyUserAreaParam;
 import com.fs.company.param.CompanyUserCodeParam;
 import com.fs.company.param.CompanyUserQwParam;
 import com.fs.company.vo.*;
+import com.fs.his.domain.FsUser;
 import com.fs.his.vo.CitysAreaVO;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.vo.FsStoreProductExportVO;
 import com.fs.qw.dto.UserProjectDTO;
@@ -276,4 +278,25 @@ public interface ICompanyUserService {
     List<CompanyUser> getDataScopeCompanyUser(Long companyUserId);
 
     List<Company> getCompanyList(String corpId);
+
+
+
+    /**
+     * 绑定销售和fs_user 的关系(该销售绑定fs_user)
+     */
+    int  bindCompanyUserAndFsUser(Long companyUserId, Long fsUserId);
+
+    /**
+     * 解绑销售和 fs_user 的关系
+     * @param companyUserId 销售 ID
+     * @param userId 用户 ID
+     * @return 是否删除成功
+     */
+    boolean unbindCompanyUserAndFsUser(Long companyUserId, Long userId);
+
+
+    /**
+     * 获取销售绑定的fs_user
+     */
+    List<CompanyUserBindUserVO> getFsUserByCompanyUserId(FsUser fsUser);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticCallLogCallphoneService.java

@@ -3,6 +3,9 @@ package com.fs.company.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 
 /**
  * 调用日志_ai打电话Service接口
@@ -73,4 +76,10 @@ public interface ICompanyVoiceRoboticCallLogCallphoneService extends IService<Co
      * @return
      */
     List<Long> getCallerIdsByCustomerId(Long customerId);
+
+    List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount();
+
+    List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone);
 }

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

@@ -3,6 +3,7 @@ package com.fs.company.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogSendmsg;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 
 /**
@@ -28,6 +29,8 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
      */
     List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
 
+    List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
     /**
      * 新增调用日志_发送短信
      * 
@@ -61,4 +64,6 @@ public interface ICompanyVoiceRoboticCallLogSendmsgService extends IService<Comp
     int deleteCompanyVoiceRoboticCallLogSendmsgByLogId(Long logId);
 
     List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg);
+
+    CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount();
 }

+ 3 - 2
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -97,9 +97,10 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
     void finishAddWxByCallees(Set<Long> roboticIds);
 
     /**
-     *
+     * 入流程执行
      * @param taskId
      * @param crmCustomerId
+     * @param traceId
      */
-    void addNewExec4Task(Long taskId,Long crmCustomerId);
+    void addNewExec4Task(Long taskId,Long crmCustomerId,String traceId);
 }

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java

@@ -10,7 +10,7 @@ import com.fs.company.param.EntryCustomerParam;
  */
 public interface IGeneralCustomerEntryService {
 
-    R entryCustomer(String param);
+//    R entryCustomer(String param);
 
     String entryCustomer(EntryCustomerParam param);
 }

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

@@ -23,9 +23,12 @@ import com.fs.company.param.CompanyUserQwParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.*;
 import com.fs.course.service.IFsUserCompanyUserService;
+import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsCityService;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.CitysAreaVO;
+import com.fs.his.vo.CompanyUserBindUserVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.domain.FsStoreProductAttrScrm;
 import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
@@ -130,6 +133,9 @@ public class CompanyUserServiceImpl implements ICompanyUserService
     @Autowired
     private CompanyMapper companyMapper;
 
+    @Autowired
+    private  CompanyFsUserMapper companyFsUserMapper;
+
 
     /**
      * 查询物业公司管理员信息
@@ -1128,4 +1134,70 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         return companyMapper.getCompanyList(corpId);
 
     }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int bindCompanyUserAndFsUser(Long companyUserId, Long fsUserId) {
+        // 检查该用户是否已经与其他销售绑定
+        CompanyFsUser existingBinding = companyFsUserMapper.selectByFsUserId(fsUserId);
+
+        if (existingBinding != null) {
+            // 如果已存在绑定关系,检查是否是同一个销售
+            if (existingBinding.getCompanyUserId().equals(companyUserId)) {
+                // 已经是同一个销售,更新状态并返回成功
+                existingBinding.setStatus(1);
+                existingBinding.setCreateTime(DateUtils.getNowDate());
+                return companyFsUserMapper.updateCompanyFsUser(existingBinding);
+            } else {
+                // 已绑定其他销售,需要先解绑
+                throw new CustomException("该用户已绑定销售:" +
+                        companyUserService.selectCompanyUserNameUserById(existingBinding.getCompanyUserId()) +
+                        ",需先解绑后才能重新绑定");
+            }
+        }
+
+        // 没有绑定记录,创建新的绑定关系
+        CompanyFsUser companyFsUser = new CompanyFsUser();
+        companyFsUser.setCompanyUserId(companyUserId);
+        companyFsUser.setFsUserId(fsUserId);
+        companyFsUser.setStatus(1);
+        companyFsUser.setCreateTime(DateUtils.getNowDate());
+        return companyFsUserMapper.insertCompanyFsUser(companyFsUser);
+    }
+
+    @Override
+    public boolean unbindCompanyUserAndFsUser(Long companyUserId, Long userId) {
+        // 校验参数
+        if (companyUserId == null || userId == null) {
+            throw new IllegalArgumentException("销售 ID 或用户 ID 不能为空");
+        }
+
+        // 查询是否存在绑定关系
+        CompanyFsUser relation = companyFsUserMapper.selectByCompanyUserIdAndUserId(companyUserId, userId);
+        if (relation == null) {
+            throw new RuntimeException("未找到销售与用户之间的绑定关系");
+        }
+
+        // 删除绑定关系
+        int rows = companyFsUserMapper.deleteById(relation.getId());
+        return rows > 0;
+    }
+
+
+    @Override
+    public List<CompanyUserBindUserVO> getFsUserByCompanyUserId(FsUser fsUser) {
+        //加密手机号
+        if (StringUtils.isNotBlank(fsUser.getPhone())) {
+            fsUser.setPhone(PhoneUtil.encryptPhone(fsUser.getPhone()));
+        }
+        List<CompanyUserBindUserVO> fsUsers = companyFsUserMapper.selecUserList(fsUser);
+        for (CompanyUserBindUserVO user : fsUsers) {
+            if (StringUtils.isNotBlank(user.getPhone())) {
+                user.setPhone(PhoneUtil.decryptPhone(user.getPhone()));
+            }
+        }
+        return fsUsers;
+    }
+
 }

+ 50 - 9
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogCallphoneServiceImpl.java

@@ -2,12 +2,10 @@ package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -19,28 +17,31 @@ import com.fs.aicall.domain.apiresult.PushIIntentionResult;
 import com.fs.aicall.domain.param.getDialogMapDomain;
 import com.fs.aicall.service.AiCallService;
 import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.*;
-import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
-import com.fs.company.mapper.CompanyVoiceRoboticCalleesMapper;
-import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.company.mapper.*;
 import com.fs.company.service.CompanyWorkflowEngine;
 import com.fs.company.vo.CidConfigVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCallPhoneVO;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
+import com.fs.company.vo.DictVO;
 import com.fs.company.vo.easycall.EasyCallCallPhoneVO;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.store.config.StoreConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.impl.SysDictTypeServiceImpl;
 import com.fs.voice.constant.Constant;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceRoboticCallLogCallphoneMapper;
 import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
 
 import static com.fs.company.service.impl.call.node.AiCallTaskNode.EASYCALL_WORKFLOW_REDIS_KEY;
@@ -72,11 +73,15 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     @Autowired
     CompanyWorkflowEngine companyWorkflowEngine;
     @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
     QwUserMapper qwUserMapper;
     @Autowired
     @Qualifier("cidWorkFlowExecutor")
     private Executor cidWorkFlowExecutor;
 
+    @Autowired
+    SysDictTypeServiceImpl sysDictTypeService;
     /**
      * 查询调用日志_ai打电话
      *
@@ -308,7 +313,17 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 companyVoiceRoboticCallLog.setCallCreateTime(createTime);
                 Long answerTime = result.getCallEndTime();
                 companyVoiceRoboticCallLog.setCallAnswerTime(answerTime);
-                companyVoiceRoboticCallLog.setIntention(result.getIntent());
+                String intention = result.getIntent();
+                String intentf = null;
+                List<SysDictData> customerIntentionLevel = sysDictTypeService.selectDictDataByType("customer_intention_level");
+                if (!isPositiveInteger(intention)) {
+                    Optional<SysDictData> firstDict = customerIntentionLevel.stream().filter(e -> e.getDictLabel().equals(intention)).findFirst();
+                    if (firstDict.isPresent()) {
+                        SysDictData sysDictData = firstDict.get();
+                        intentf = sysDictData.getDictValue();
+                    }
+                }
+                companyVoiceRoboticCallLog.setIntention(intentf);
                 companyVoiceRoboticCallLog.setCallTime(Long.valueOf(result.getTimeLen()/1000));
                 BigDecimal callCharge = cidConfigVO.getCallCharge();
                 //
@@ -320,6 +335,8 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
                 BigDecimal multiply = divide.multiply(callCharge);
                 companyVoiceRoboticCallLog.setCost(multiply);
                 baseMapper.updateCompanyVoiceRoboticCallLogCallphone(companyVoiceRoboticCallLog);
+                // 更新用户标签
+                crmCustomerPropertyService.addPropertyByCallLog(companyVoiceRoboticCallLog);
 
                 if (StringUtils.isNotBlank(result.getBizJson())) {
                     JSONObject bizJson = JSONObject.parseObject(result.getBizJson());
@@ -367,4 +384,28 @@ public class CompanyVoiceRoboticCallLogCallphoneServiceImpl extends ServiceImpl<
     public List<Long> getCallerIdsByCustomerId(Long customerId) {
         return companyVoiceRoboticCalleesMapper.getCallerIdsByCustomerId(customerId);
     }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallphone> selectCompanyVoiceRoboticCallPhoneLogGroupList(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogGroupList(companyVoiceRoboticCallLogCallphone);
+    }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallPhoneLogCount() {
+        return baseMapper.selectCompanyVoiceRoboticCallPhoneLogCount();
+    }
+
+    @Override
+    public List<CompanyVoiceRoboticCallLogCallPhoneVO> listByRoboticId(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        return baseMapper.listByRoboticId(companyVoiceRoboticCallLogCallphone);
+    }
+    /**
+     * 判断整数
+     *
+     * @param str
+     * @return
+     */
+    public boolean isPositiveInteger(String str) {
+        return str != null && str.matches("[1-9]\\d*");
+    }
 }

+ 11 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticCallLogSendmsgServiceImpl.java

@@ -6,6 +6,7 @@ import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.mapper.CompanyVoiceRoboticBusinessMapper;
+import com.fs.company.vo.CompanyVoiceRoboticCallLogCount;
 import com.fs.company.vo.CompanyVoiceRoboticCallLogSendmsgVO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -39,6 +40,11 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
         return baseMapper.selectCompanyVoiceRoboticCallLogSendmsgByLogId(logId);
     }
 
+    @Override
+    public List<CompanyVoiceRoboticCallLogSendmsg> selectCompanyVoiceRoboticCallLogSendmsgGroupList(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg) {
+        return baseMapper.selectCompanyVoiceRoboticCallLogSendmsgGroupList(companyVoiceRoboticCallLogSendmsg);
+    }
+
     /**
      * 查询调用日志_发送短信列表
      * 
@@ -119,4 +125,9 @@ public class CompanyVoiceRoboticCallLogSendmsgServiceImpl extends ServiceImpl<Co
     public List<CompanyVoiceRoboticCallLogSendmsgVO> listByCallerIdAndRoboticId(CompanyVoiceRoboticCallLogSendmsg companyVoiceRoboticCallLogSendmsg){
         return baseMapper.listByCallerIdAndRoboticId(companyVoiceRoboticCallLogSendmsg);
     }
+
+    @Override
+    public CompanyVoiceRoboticCallLogCount selectCompanyVoiceRoboticCallLogSendMsgCount() {
+        return baseMapper.selectCompanyVoiceRoboticCallLogSendMsgCount();
+    }
 }

+ 107 - 8
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -129,6 +129,13 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
 
     final int BATCH_SIZE = 1500;
 
+    /** EasyCall intent 意向度重试队列 Redis key 前缀,value 为已重试次数 */
+    private static final String EASYCALL_INTENT_RETRY_KEY = "easycall:intent:retry:";
+    /** intent 意向度等待重试最大次数(每次间隔约30秒,最多等待 5*30=150秒) */
+    private static final int EASYCALL_INTENT_MAX_RETRY = 5;
+    /** 每次重试等待时长(毫秒) */
+    private static final long EASYCALL_INTENT_RETRY_INTERVAL_MS = 30000L;
+
     /**
      * 查询机器人外呼任务
      *
@@ -466,7 +473,6 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 wxAccount = companyWxAccountService.selectCompanyWxAccountById(wxClient.getAccountId());
             }
 
-
             CompanySmsTemp temp = smsTempService.selectCompanySmsTempById(smsTempId);
 
             if (temp != null && temp.getStatus().equals(1) && temp.getIsAudit().equals(1)) {
@@ -820,18 +826,90 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             return;
         }
         log.info("进入easyCall外呼结果查询结果callPhoneRes:{}", JSON.toJSONString(callPhoneRes));
+        // intent(意向度)由对方异步评估写入,回调时可能尚未赋值,进入延迟重试队列等待
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = 0;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall外呼回调intent意向度暂未评估完成,uuid={},第{}次放入延迟重试队列", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                // 超过最大重试次数,以 intent 为空(意向未知)兜底继续处理
+                log.warn("easyCall外呼回调intent意向度在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已有值,直接正常处理
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 延迟重试处理 EasyCall 外呼回调(等待 intent 意向度异步评估完成)
+     * 每次重试前等待 {@link #EASYCALL_INTENT_RETRY_INTERVAL_MS} 毫秒后重新拉取数据
+     */
+    @Async("cidWorkFlowExecutor")
+    public void doRetryCallerResult4EasyCall(CdrDetailVo result, int currentRetry) {
+        try {
+            Thread.sleep(EASYCALL_INTENT_RETRY_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("easyCall intent重试等待被中断, uuid={}", result.getUuid());
+            return;
+        }
+        log.info("easyCall intent重试第{}次开始, uuid={}", currentRetry, result.getUuid());
+        EasyCallCallPhoneVO callPhoneRes = easyCallMapper.getCallPhoneInfoByUuid(result.getUuid());
+        if (null == callPhoneRes) {
+            log.error("easyCall intent重试时仍未查询到外呼结果, uuid={}", result.getUuid());
+            return;
+        }
+        if (StringUtils.isBlank(callPhoneRes.getIntent())) {
+            // intent 仍为空,继续判断是否还有剩余重试次数
+            String retryKey = EASYCALL_INTENT_RETRY_KEY + result.getUuid();
+            Integer retryCount = redisCache2.getCacheObject(retryKey);
+            if (retryCount == null) {
+                retryCount = currentRetry;
+            }
+            if (retryCount < EASYCALL_INTENT_MAX_RETRY) {
+                redisCache2.setCacheObject(retryKey, retryCount + 1, 10, java.util.concurrent.TimeUnit.MINUTES);
+                log.info("easyCall intent仍未评估完成,uuid={},第{}次继续延迟重试", result.getUuid(), retryCount + 1);
+                doRetryCallerResult4EasyCall(result, retryCount + 1);
+            } else {
+                log.warn("easyCall intent在{}次重试后仍为空,uuid={},以意向未知兜底处理", EASYCALL_INTENT_MAX_RETRY, result.getUuid());
+                redisCache2.deleteObject(retryKey);
+                doHandleEasyCallResult(callPhoneRes);
+            }
+            return;
+        }
+        // intent 已评估完成,正常处理
+        log.info("easyCall intent重试第{}次成功获取到意向度={},uuid={}", currentRetry, callPhoneRes.getIntent(), result.getUuid());
+        redisCache2.deleteObject(EASYCALL_INTENT_RETRY_KEY + result.getUuid());
+        doHandleEasyCallResult(callPhoneRes);
+    }
+
+    /**
+     * 执行 EasyCall 外呼回调核心业务处理(推送对话内容、更新通话日志)
+     * 供 {@link #callerResult4EasyCall} 和重试逻辑统一调用
+     */
+    private void doHandleEasyCallResult(EasyCallCallPhoneVO callPhoneRes) {
         //等待数据信息
         JSONObject bizJson = JSONObject.parseObject(callPhoneRes.getBizJson());
         String cacheString = (String) redisCache2.getCacheObject(EASYCALL_WORKFLOW_REDIS_KEY + bizJson.getString("callBackUuid"));
         if (StringUtils.isBlank(cacheString)) {
-            log.error("easyCall外呼回调缓存信息缺失:{}", JSON.toJSONString(result));
+            log.error("easyCall外呼回调缓存信息缺失, uuid={}", callPhoneRes.getUuid());
             return;
         }
         JSONObject cacheInfo = JSONObject.parseObject(cacheString);
         pushDialogContent4EasyCall(cacheInfo, callPhoneRes);
         CompanyVoiceRoboticCallees callee = companyVoiceRoboticCalleesMapper.selectCompanyVoiceRoboticCalleesById(cacheInfo.getLong("calleeId"));
         companyVoiceRoboticCallLogCallphoneService.asyncHandleCalleeCallBackResult4EasyCall(callPhoneRes, callee);
-        System.out.println(callPhoneRes);
+        log.info("easyCall外呼回调业务处理完成, uuid={}, intent={}", callPhoneRes.getUuid(), callPhoneRes.getIntent());
     }
 
     public void pushDialogContent(PushIIntentionResult result) {
@@ -1074,6 +1152,22 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             initAndExecuteWorkflows(robotic, roboticBusinesseList);
         }
     }
+    /**
+     * 初始化场景任务客户流程
+     * @param robotic
+     * @param business
+     */
+    private void initWorkflows4SceneTask(CompanyVoiceRobotic robotic,CompanyVoiceRoboticBusiness business) {
+        final Long workflowId = robotic.getCompanyAiWorkflowId();
+        final Long roboticId = robotic.getId();
+        Map<String, Object> inputVariables = new HashMap<>();
+        inputVariables.put("roboticId", roboticId);
+        inputVariables.put("businessId", business.getId());
+        inputVariables.put("cidGroupNo", robotic.getCidGroupNo());
+        inputVariables.put("runtimeRangeStart", robotic.getRuntimeRangeStart());
+        inputVariables.put("runtimeRangeEnd", robotic.getRuntimeRangeEnd());
+        companyWorkflowEngine.initialize(workflowId, inputVariables);
+    }
 
     /**
      * 初始化并执行工作流
@@ -1129,7 +1223,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     /**
      * 添加用户到场景任务
      */
-    public void addNewExec4Task(Long taskId, Long crmCustomerId) {
+    public void addNewExec4Task(Long taskId, Long crmCustomerId,String traceId) {
         //保存callees表数据
         CompanyVoiceRobotic companyVoiceRobotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(taskId);
         CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(crmCustomerId);
@@ -1138,6 +1232,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         client.setRoboticId(taskId);
         client.setCustomerId(crmCustomerId);
         client.setIsWeCom(companyVoiceRobotic.getIsWeCom());
+        client.setTraceId(traceId);
         companyWxClientServiceImpl.insertCompanyWxClient(client);
 
         CompanyVoiceRoboticCallees callee = new CompanyVoiceRoboticCallees();
@@ -1186,8 +1281,10 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             companyWxClientServiceImpl.saveOrUpdate(companyWxClient);
         }
         //写入业务表数据
-        buildTaskBussiness4SceneTask(companyVoiceRobotic,callee);
-        //初始化流程表 todo
+        CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = buildTaskBussiness4SceneTask(companyVoiceRobotic, callee);
+        //初始化流程表
+        initWorkflows4SceneTask(companyVoiceRobotic,companyVoiceRoboticBusiness);
+
     }
 
     /**
@@ -1303,7 +1400,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         return resArr;
     }
 
-    public void buildTaskBussiness4SceneTask(CompanyVoiceRobotic robotic,CompanyVoiceRoboticCallees callee){
+    public CompanyVoiceRoboticBusiness buildTaskBussiness4SceneTask(CompanyVoiceRobotic robotic,CompanyVoiceRoboticCallees callee){
         List<CompanyWxClient> companyWxClients = companyWxClientMapper.selectListByRoboticId(robotic.getId());
         Map<String, CompanyWxClient> clientMp = companyWxClients.stream().collect(Collectors.toMap(e -> e.getRoboticId() + "-" + e.getCustomerId(), e -> e));
         CompanyVoiceRoboticBusiness companyVoiceRoboticBusiness = new CompanyVoiceRoboticBusiness();
@@ -1315,6 +1412,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         companyVoiceRoboticBusiness.setSendMsgDone(0);
         companyVoiceRoboticBusiness.setCreateTime(new Date());
         companyVoiceRoboticBusinessMapper.insert(companyVoiceRoboticBusiness);
+        return companyVoiceRoboticBusiness;
     }
     public void buildTaskBussiness(CompanyVoiceRobotic robotic) {
         List<CompanyVoiceRoboticCallees> calleesList = companyVoiceRoboticCalleesMapper.selectByRoboticId(robotic.getId());
@@ -1731,7 +1829,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
                 targetExec.setCurrentNodeKey(nodeInfoVo.getTargetNodeKey());
                 targetExec.setCurrentNodeName(nodeInfoVo.getNodeName());
                 targetExec.setCurrentNodeType(NodeTypeEnum.fromCode(nodeInfoVo.getNodeType()).getValue());
-                targetExec.setStatus(ExecutionStatusEnum.FAILURE.getValue());
+                targetExec.setStatus(ExecutionStatusEnum.INTERRUPT.getValue());
                 targetExec.setStartTime(now);
                 targetExec.setVariables(variables.toJSONString());
                 targetExec.setBusinessKey(startExec.getBusinessKey());
@@ -1763,6 +1861,7 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         int rows = companyAiWorkflowExecMapper.insertBatchInfo(workflowExecs);
         if(rows > 0){
             workflowExecLogBatchInsert(startExecList);//第一节点
+            workflowExecs.stream().forEach(a->a.setStatus(ExecutionStatusEnum.FAILURE.getValue()));
             workflowExecLogBatchInsert(workflowExecs);//第二节点
         }
         workflowExecs.clear();

+ 3 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java

@@ -179,6 +179,9 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
         workflow.setCreateTime(now);
         workflow.setStartNodeKey(source.getStartNodeKey());
         workflow.setEndNodeKey(source.getEndNodeKey());
+        workflow.setCompanyId(source.getCompanyId());
+        workflow.setCompanyUserId(source.getCompanyUserId());
+
         companyWorkflowMapper.insertCompanyWorkflow(workflow);
         Long newWorkflowId = workflow.getWorkflowId();
 

+ 15 - 15
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -52,20 +52,20 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
      * @param param
      * @return
      */
-    @Override
-    public R entryCustomer(String param) {
-        try {
-            String decryptParam = CryptoUtil.decrypt(param);
-            if (StringUtils.isBlank(decryptParam)) {
-                return R.error("参数错误");
-            }
-            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
-            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
-        } catch (Exception ex) {
-            log.error("录入客户异常", ex);
-        }
-        return R.ok().put("result", "录入成功");
-    }
+    //@Override
+//    public R entryCustomer(String param) {
+//        try {
+//            String decryptParam = CryptoUtil.decrypt(param);
+//            if (StringUtils.isBlank(decryptParam)) {
+//                return R.error("参数错误");
+//            }
+//            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
+//            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
+//        } catch (Exception ex) {
+//            log.error("录入客户异常", ex);
+//        }
+//        return R.ok().put("result", "录入成功");
+//    }
 
     @Override
     @Async("crmCustomerExecutor")
@@ -111,7 +111,7 @@ public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntrySer
             CompanyVoiceRobotic companySceneTasks = getCompanySceneTask(data);
             if(null != companySceneTasks){
                 //场景任务存在 加入场景任务队列
-                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId());
+                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId(),data.getTraceId());
             }
         }
     }

+ 79 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCallPhoneVO.java

@@ -0,0 +1,79 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class CompanyVoiceRoboticCallLogCallPhoneVO {
+
+    /** $column.columnComment */
+    private Long logId;
+
+    /** 任务id */
+    @Excel(name = "任务id")
+    private Long roboticId;
+
+    @Excel(name = "任务名称")
+    private String roboticName;
+
+    /** caller_id */
+    @Excel(name = "caller_id")
+    private Long callerId;
+
+    /** 记录调用时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "记录调用时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date runTime;
+
+    /** 调用参数 */
+    @Excel(name = "调用参数")
+    private String runParam;
+
+    /** 执行结果 */
+    @Excel(name = "执行结果")
+    private String result;
+
+    /** 执行状态:1、执行中,2、执行成功,3、执行失败 */
+    @Excel(name = "执行状态:1、执行中,2、执行成功,3、执行失败")
+    private Integer status;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    @Excel(name = "公司名称")
+    private String companyName;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+    @Excel(name = "销售名称")
+    private String companyUserName;
+
+    /** 客户号码 */
+    @Excel(name = "客户号码")
+    private String callerNum;
+
+    /** 话术号码 */
+    @Excel(name = "话术号码")
+    private String calleeNum;
+
+    @Excel(name = "客户类型")
+    private String intention;
+
+    /** 通话时长 */
+    @Excel(name = "通话时长(秒)")
+    private String callTime;
+
+    @Excel(name = "录音地址")
+    private String recordPath;
+
+    /** 花费金额 */
+    @Excel(name = "花费金额")
+    private BigDecimal cost;
+
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyVoiceRoboticCallLogCount.java

@@ -0,0 +1,30 @@
+package com.fs.company.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author ZhuanZ(无密码)
+ */
+@Data
+public class CompanyVoiceRoboticCallLogCount {
+
+    /**
+     * 总记录数
+     */
+    private Integer recordCount;
+    /**
+     * 发送成功记录数
+     */
+    private Integer successRecordCount;
+    /**
+     * 今日发送数
+     */
+    private Integer todayCount;
+    /**
+     * 今日发送成功数
+     */
+    private Integer todaySuccessCount;
+
+}

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

@@ -70,4 +70,7 @@ public class CompanyVoiceRoboticCallLogSendmsgVO {
 
     @Excel(name = "短信模板名称")
     private String smsTempName;
+
+    @Excel(name = "发送手机号")
+    private String phone;
 }

+ 14 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWxClient4WorkFlowVO.java

@@ -80,6 +80,20 @@ public class CompanyWxClient4WorkFlowVO extends BaseEntityTow {
     private String memo;
     private String workflowInstanceId;
     private String currentNodeKey;
+    /**
+    *  加微方式配置
+    */
+    private String nodeConfig;
     private String currentNodeName;
     private Integer currentNodeType;
+
+    /**
+    * 外呼id
+    */
+    private Long calleeId;
+
+    /**
+    * 投流 id
+    */
+    private String traceId;
 }

+ 4 - 2
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -190,7 +190,9 @@ public class CrmCustomer extends BaseEntity
     private String shopName;
     // 平台名称
     private String platformName;
-
-
+    /**
+     * 投流来源id
+     */
+    private String traceId;
 
 }

+ 258 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerInfo.java

@@ -0,0 +1,258 @@
+package com.fs.crm.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * 客户信息表实体类
+ * @author lk
+ * @since 2026-3-19
+ */
+@Data
+@TableName("crm_customer_info")
+@Accessors(chain = true)
+public class CrmCustomerInfo {
+
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * crm_customer_id
+     */
+    @TableField("customer_id")
+    private Long customerId;
+
+    /**
+     * 姓名
+     */
+    @TableField("name")
+    private String name;
+
+    /**
+     * 性别
+     */
+    @TableField("sex")
+    private String sex;
+
+    /**
+     * 年龄
+     */
+    @TableField("age")
+    private String age;
+
+    /**
+     * 地区
+     */
+    @TableField("address")
+    private String address;
+
+    /**
+     * 行为习惯
+     */
+    @TableField("habits")
+    private String habits;
+
+    /**
+     * 患病时间
+     */
+    @TableField("illness_time")
+    private String illnessTime;
+
+    /**
+     * 身体状态
+     */
+    @TableField("body")
+    private String body;
+
+    /**
+     * 学习到的章节
+     */
+    @TableField("study")
+    private String study;
+
+    /**
+     * 今日课程完成情况
+     */
+    @TableField("course_status")
+    private String courseStatus;
+
+    /**
+     * 学习课程
+     */
+    @TableField("course")
+    private String course;
+
+    /**
+     * 提及的家人
+     */
+    @TableField("family")
+    private String family;
+
+    /**
+     * 家人的疾病
+     */
+    @TableField("family_disease")
+    private String familyDisease;
+
+    /**
+     * 疾病
+     */
+    @TableField("disease")
+    private String disease;
+
+    /**
+     * 是否线下就诊
+     */
+    @TableField("is_line")
+    private String isLine;
+
+    /**
+     * 交流状态
+     */
+    @TableField("talk")
+    private String talk;
+
+    /**
+     * 用户分类
+     */
+    @TableField("user_type")
+    private String userType;
+
+    /**
+     * 是否本人会诊
+     */
+    @TableField("is_self")
+    private String isSelf;
+
+    /**
+     * 什么情况加重或缓解
+     */
+    @TableField("intensify")
+    private String intensify;
+
+    /**
+     * 是否怕热或者怕冷
+     */
+    @TableField("is_cold")
+    private String isCold;
+
+    /**
+     * 怕冷或怕热的部位
+     */
+    @TableField("cold_body")
+    private String coldBody;
+
+    /**
+     * 出汗情况
+     */
+    @TableField("sweat")
+    private String sweat;
+
+    /**
+     * 其他情况
+     */
+    @TableField("other")
+    private String other;
+
+    /**
+     * 大小便情况
+     */
+    @TableField("toilet")
+    private String toilet;
+
+    /**
+     * 饮食情况
+     */
+    @TableField("eat")
+    private String eat;
+
+    /**
+     * 经期如何 女 55岁以下
+     */
+    @TableField("menses")
+    private String menses;
+
+    /**
+     * 用药
+     */
+    @TableField("medicine")
+    private String medicine;
+
+    /**
+     * 体质
+     */
+    @TableField("constitution")
+    private String constitution;
+
+    /**
+     * 推荐用药
+     */
+    @TableField("recommend_medicine")
+    private String recommendMedicine;
+
+    /**
+     * 咨询产品
+     */
+    @TableField("consult_product")
+    private String consultProduct;
+
+    /**
+     * 是否已经购买产品
+     */
+    @TableField("is_buy")
+    private String isBuy;
+
+    /**
+     * 已经购买的产品
+     */
+    @TableField("buy_product")
+    private String buyProduct;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /**
+     * 最后回复时间
+     */
+    @TableField("reply_time")
+    private Date replyTime;
+
+    /**
+     * 产品交流
+     */
+    @TableField("product_talk")
+    private String productTalk;
+
+    /**
+     * 疾病交流
+     */
+    @TableField("disease_talk")
+    private String diseaseTalk;
+
+    /**
+     * 渠道类型
+     */
+    @TableField("channel_type")
+    private String channelType;
+
+    /**
+     * ai通话聊天记录
+     */
+    @TableField("ai_chat_record")
+    private String aiChatRecord;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerPropertyTemplate.java

@@ -22,4 +22,7 @@ public class CrmCustomerPropertyTemplate extends BaseEntityTow {
 
     @Excel(name = "行业类型")
     private String tradeType;
+
+    @Excel(name = "是否有意向度0否1是")
+    private Integer intention;
 }

+ 12 - 0
fs-service/src/main/java/com/fs/crm/dto/CrmCustomerAiAutoTagVo.java

@@ -0,0 +1,12 @@
+package com.fs.crm.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class CrmCustomerAiAutoTagVo {
+    private String id;
+    private String name;
+    private String aiHint;
+}

+ 6 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -4,6 +4,7 @@ package com.fs.crm.mapper;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.domain.CrmCustomerInfo;
 import com.fs.crm.param.*;
 import com.fs.crm.vo.*;
 import com.fs.qwApi.param.QwCustomerDetailParam;
@@ -966,4 +967,9 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
      */
     List<Long> selectCustomerIdByCompanyUserId(@Param("companyUserId") Long companyUserId);
 
+    CrmCustomerInfo selectCrmCustomerInfoById(@Param("customerId") Long customerId);
+
+    void insertCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
+
+    int updateCrmCustomerInfo(CrmCustomerInfo crmCustomerInfo);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerAiTagParam.java

@@ -0,0 +1,25 @@
+package com.fs.crm.param;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @Description: 客户ai标签参数
+ */
+@Data
+@Accessors(chain = true)
+public class CrmCustomerAiTagParam {
+
+    //company_voice_robotic_call_log_callphone 的log_id
+    @ApiModelProperty("日志id")
+    private Long logId;
+    @ApiModelProperty("客户id")
+    private Long customerId;
+    //crm_customer_property_template的 trade_type
+    @ApiModelProperty("行业")
+    private String tradeType;
+    @ApiModelProperty("对话内容")
+    private String json;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java

@@ -1,6 +1,7 @@
 package com.fs.crm.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.crm.domain.CrmCustomerProperty;
 
 import java.util.List;
@@ -139,4 +140,6 @@ public interface ICrmCustomerPropertyService extends IService<CrmCustomerPropert
      * @return 添加结果(添加的记录数)
      */
     int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy);
+
+    void addPropertyByCallLog(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLog);
 }

+ 56 - 3
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -1,24 +1,40 @@
 package com.fs.crm.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.domain.CompanyVoiceRoboticCallees;
+import com.fs.company.service.ICompanyVoiceRoboticCalleesService;
 import com.fs.crm.domain.CrmCustomerProperty;
 import com.fs.crm.domain.CrmCustomerPropertyTemplate;
 import com.fs.crm.mapper.CrmCustomerPropertyMapper;
+import com.fs.crm.param.CrmCustomerAiTagParam;
 import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.crm.vo.CrmCustomerAiTagVo;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
+@Slf4j
 @Service
 public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPropertyMapper, CrmCustomerProperty> implements ICrmCustomerPropertyService {
 
     @Autowired
     private ICrmCustomerPropertyTemplateService propertyTemplateService;
+    @Autowired
+    private ICompanyVoiceRoboticCalleesService companyVoiceRoboticCalleesService;
 
     @Override
     public CrmCustomerProperty selectCrmCustomerPropertyById(Long id) {
@@ -89,7 +105,7 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
 
     public int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy) {
         String autoIntention = calculateIntentionByLikeRatio(likeRatio, intention);
-        
+
         CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
         if (existProperty != null) {
             existProperty.setPropertyValue(propertyValue);
@@ -116,12 +132,12 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
             return baseMapper.insertCrmCustomerProperty(property);
         }
     }
-    
+
     private String calculateIntentionByLikeRatio(Integer likeRatio, String intention) {
         if (likeRatio == null) {
             return intention;
         }
-        
+
         if (likeRatio >= 80) {
             return "high";
         } else if (likeRatio >= 50) {
@@ -188,4 +204,41 @@ public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPrope
         }
         return count;
     }
+
+    @Override
+    @Async
+    public void addPropertyByCallLog(CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone) {
+        CompanyVoiceRoboticCallees companyVoiceRoboticCallees = companyVoiceRoboticCalleesService.selectCompanyVoiceRoboticCalleesById(companyVoiceRoboticCallLogCallphone.getCallerId());
+        log.info("获取电话记录:{}", companyVoiceRoboticCallees);
+        CrmCustomerAiTagParam param = new CrmCustomerAiTagParam();
+        param.setLogId(companyVoiceRoboticCallLogCallphone.getLogId());
+        param.setCustomerId(companyVoiceRoboticCallees.getUserId());
+        param.setTradeType("3");
+        param.setJson(companyVoiceRoboticCallLogCallphone.getContentList());
+        log.info("传输数据:{}", param);
+        try {
+            List<CrmCustomerAiTagVo> tag = CrmCustomerAiTagUtil.getCrmCustomerAiTag(param);
+            log.info("解析数据:{}", tag);
+            List<Long> ids = PubFun.listToNewList(tag, e -> Long.parseLong(e.getId()));
+            List<CrmCustomerProperty> propertyList = tag.stream().map(e -> {
+                CrmCustomerProperty property = new CrmCustomerProperty();
+                property.setCustomerId(e.getCustomerId());
+                property.setPropertyId(Long.parseLong(e.getId()));
+                property.setPropertyName(e.getName());
+                property.setPropertyValue(e.getValue());
+                property.setPropertyValueType("String");
+                property.setTradeType("3");
+                property.setIntention("medium");
+                property.setLikeRatio(50);
+                return property;
+            }).collect(Collectors.toList());
+            remove(new LambdaQueryWrapper<CrmCustomerProperty>()
+                    .eq(CrmCustomerProperty::getCustomerId, companyVoiceRoboticCallees.getUserId())
+                    .in(CrmCustomerProperty::getPropertyId, ids)
+            );
+            saveBatch(propertyList);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }

+ 413 - 0
fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java

@@ -0,0 +1,413 @@
+package com.fs.crm.utils;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.DictUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.config.ai.AiHostProper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.domain.CrmCustomerInfo;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.crm.param.CrmCustomerAiTagParam;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.vo.CrmCustomerAiTagVo;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.service.ChatService;
+import com.fs.hisapi.util.MapUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Component
+public class CrmCustomerAiTagUtil {
+
+    //行业字典名称
+    private static final String TRADE_TYPE = "trade_type";
+    @Value("${crm.customer.ai.key:mygpt-tbQfq4ejR162mGJBCTTDUH9ecP1XCVuUfaOGTipnLjb1hP8x5prg}")
+    private String appKey;
+    private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private static String APP_KEY;
+
+    @PostConstruct
+    public void initStatic() {
+        APP_KEY = this.appKey;
+    }
+
+    public static List<CrmCustomerAiTagVo> getCrmCustomerAiTag(CrmCustomerAiTagParam content) throws JsonProcessingException {
+        // 1. 参数校验
+        validateParams(content);
+        Long customerId = content.getCustomerId();
+        Long logId = content.getLogId();
+        String tradeType = content.getTradeType();
+        List<Map<String, Object>> communication = parseCommunicationJson(content.getJson());
+
+        // 2. 构建请求参数
+        Map<String, Object> requestParam = buildRequestParam(customerId, tradeType, communication);
+
+        // 3. 调用AI服务
+        R aiResponse = callAiService(requestParam, logId);
+
+        // 4. 解析响应并保存
+        List<CrmCustomerAiTagVo> results = parseAiResponse(aiResponse, customerId);
+
+        // 5. 异步保存到Redis,后续调用ai分析其他数据
+        saveToRedisAsync(customerId, aiResponse);
+        return results;
+    }
+    private static void saveToRedisAsync(Long customerId, R aiResponse) {
+        // 使用线程池异步保存,避免影响主流程
+        CompletableFuture.runAsync(() -> {
+            try {
+                Map<String, Object> dataMap = new HashMap<>();
+                dataMap.put("customerId", customerId);
+                dataMap.put("data", aiResponse);
+                dataMap.put("timestamp", System.currentTimeMillis());
+
+                RedisTemplate<String, Object> redisTemplate = SpringUtils.getBean(RedisTemplate.class);
+
+                // 存储队列索引
+                redisTemplate.opsForList().rightPush(CRM_AI_REDIS_KEY, dataMap);
+
+            } catch (Exception e) {
+            }
+        });
+    }
+    private static CrmCustomerAiTagVo buildTagVo(Map<String, String> tag, Long customerId) {
+        CrmCustomerAiTagVo vo = new CrmCustomerAiTagVo();
+        vo.setCustomerId(customerId);
+        vo.setId(tag.get("id"));
+        vo.setName(tag.get("name"));
+        vo.setValue(tag.get("value"));
+        return vo;
+    }
+    private static List<CrmCustomerAiTagVo> parseAiResponse(R aiResponse, Long customerId) {
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            throw new RuntimeException("AI响应异常: " +
+                    (aiResponse != null ? aiResponse.get("msg") : "响应为空"));
+        }
+
+        List<Map<String, String>> tagInfos = extractTagInfos(JSONUtil.toJsonStr(aiResponse));
+        if (CollectionUtils.isEmpty(tagInfos)) {
+            return Collections.emptyList();
+        }
+
+        return tagInfos.stream()
+                .map(tag -> buildTagVo(tag, customerId))
+                .collect(Collectors.toList());
+    }
+    private static R callAiService(Map<String, Object> requestParam, Long logId) {
+        try {
+            String requestJson = mapper.writeValueAsString(requestParam);
+
+            ChatParam param = new ChatParam();
+            param.setChatId(logId.toString());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Message message = new ChatParam.Message();
+            List<ChatParam.Message> messageList = new ArrayList<ChatParam.Message>();
+            message.setContent(requestJson);
+            message.setRole("user");
+            messageList.add(message);
+            param.setMessages(messageList);
+            ChatService chatService = SpringUtils.getBean(ChatService.class);
+            AiHostProper aiHost = SpringUtils.getBean(AiHostProper.class);
+
+            return chatService.initiatingTakeChat(param, aiHost.getAiApi(), APP_KEY);
+        } catch (Exception e) {
+            throw new RuntimeException("AI服务调用失败", e);
+        }
+    }
+    private static Map<String, Object> buildRequestParam(Long customerId,
+                                                         String tradeType,
+                                                         List<Map<String, Object>> communication) {
+        Map<String, Object> requestParam = new HashMap<>();
+
+        // 获取各类数据
+        String tradeName = getDictLabel(tradeType);
+        Map<String, Object> tags = getTags(tradeType);
+        Map<String, Object> history = getHistory(communication,customerId.toString());
+        Map<String, Object> userInfo = getUserInfo(customerId);
+        Map<String, Object> aiInfo = getAiInfo(communication.remove(0));
+
+        // 合并数据
+        Stream.of(tags, history, userInfo, aiInfo)
+                .filter(Objects::nonNull)
+                .forEach(requestParam::putAll);
+
+        // 设置其他参数
+        requestParam.put("tradeName", tradeName);
+        requestParam.put("tradeType", tradeType);
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("likeRatio", userInfo != null ? userInfo.remove("likeRatio") : null);
+
+        return requestParam;
+    }
+    private static List<Map<String, Object>> parseCommunicationJson(String jsonStr) {
+        try {
+            return mapper.readValue(jsonStr,
+                    new TypeReference<List<Map<String, Object>>>() {});
+        } catch (Exception e) {
+            throw new RuntimeException("数据格式错误", e);
+        }
+    }
+    private static void validateParams(CrmCustomerAiTagParam content) {
+        if (ObjectUtil.isEmpty(content.getTradeType())
+                || ObjectUtil.isEmpty(content.getCustomerId())
+                || ObjectUtil.isEmpty(content.getLogId())
+                || ObjectUtil.isEmpty(content.getJson())) {
+            throw new IllegalArgumentException("参数不能为空");
+        }
+    }
+    /**
+     * 提取 tagInfos 数据
+     */
+    public static List<Map<String, String>> extractTagInfos(String jsonStr) {
+        try {
+            JsonNode root = mapper.readTree(jsonStr);
+
+            // 获取 responseData 数组
+            JsonNode responseData = root.path("data").path("responseData");
+
+            // 查找 AI 对话节点
+            for (JsonNode node : responseData) {
+                String moduleName = node.path("moduleName").asText();
+                if ("AI 对话".equals(moduleName)) {
+                    // 获取 historyPreview 数组
+                    JsonNode historyPreview = node.path("historyPreview");
+
+                    // 查找 AI 节点的 historyPreview
+                    for (JsonNode preview : historyPreview) {
+                        String objType = preview.path("obj").asText();
+                        if ("AI".equals(objType)) {
+                            JsonNode valueNode = preview.path("value");
+
+                            // 如果 value 是字符串,需要再次解析
+                            if (valueNode.isTextual()) {
+                                String valueStr = valueNode.asText();
+                                JsonNode tagInfosNode = mapper.readTree(valueStr).path("tagInfos");
+
+                                if (tagInfosNode.isArray()) {
+                                    return mapper.convertValue(tagInfosNode,
+                                            new TypeReference<List<Map<String, String>>>() {
+                                            });
+                                }
+                            } else if (valueNode.isObject()) {
+                                JsonNode tagInfosNode = valueNode.path("tagInfos");
+                                if (tagInfosNode.isArray()) {
+                                    return mapper.convertValue(tagInfosNode,
+                                            new TypeReference<List<Map<String, String>>>() {
+                                            });
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * 获取完整的 AI 响应数据
+     */
+    public static Map<String, Object> getAIResponseData(String jsonStr) {
+        try {
+            JsonNode root = mapper.readTree(jsonStr);
+            JsonNode responseData = root.path("data").path("responseData");
+
+            for (JsonNode node : responseData) {
+                if ("AI 对话".equals(node.path("moduleName").asText())) {
+                    JsonNode historyPreview = node.path("historyPreview");
+
+                    for (JsonNode preview : historyPreview) {
+                        if ("AI".equals(preview.path("obj").asText())) {
+                            JsonNode valueNode = preview.path("value");
+
+                            if (valueNode.isTextual()) {
+                                return mapper.readValue(valueNode.asText(),
+                                        new TypeReference<Map<String, Object>>() {
+                                        });
+                            } else if (valueNode.isObject()) {
+                                return mapper.convertValue(valueNode,
+                                        new TypeReference<Map<String, Object>>() {
+                                        });
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new HashMap<>();
+    }
+
+    /**
+     * 提取所有标签(包括 tagInfos 和 tags 定义)
+     */
+    public static Map<String, Object> getAllTagInfo(String jsonStr) {
+        Map<String, Object> result = new HashMap<>();
+
+        try {
+            // 获取 AI 响应数据
+            Map<String, Object> aiResponse = getAIResponseData(jsonStr);
+
+            // tagInfos - 实际提取的标签值
+            List<Map<String, String>> tagInfos = (List<Map<String, String>>) aiResponse.get("tagInfos");
+            result.put("tagInfos", tagInfos != null ? tagInfos : new ArrayList<>());
+
+            // tags - 标签定义(从原始 query 中获取)
+            JsonNode root = mapper.readTree(jsonStr);
+            JsonNode responseData = root.path("data").path("responseData");
+
+            for (JsonNode node : responseData) {
+                if ("AI 对话".equals(node.path("moduleName").asText())) {
+                    JsonNode query = node.path("query");
+                    JsonNode tagsNode = query.path("tags");
+
+                    if (tagsNode.isArray()) {
+                        List<Map<String, String>> tags = mapper.convertValue(tagsNode,
+                                new TypeReference<List<Map<String, String>>>() {
+                                });
+                        result.put("tags", tags);
+                    }
+                    break;
+                }
+            }
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return result;
+    }
+
+    private static Map<String, Object> getAiInfo(Map<String, Object> remove) {
+        HashMap<String, String> aiInfo = new HashMap<>();
+        aiInfo.put("name", "");
+        aiInfo.put("sex", "");
+        aiInfo.put("age", "");
+        aiInfo.put("city", "");
+        aiInfo.put("habits", "");
+        aiInfo.put("describe", "");
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("aiInfo", aiInfo);
+        return result;
+    }
+
+    private static Map<String, Object> getUserInfo(Long customerId) {
+        CrmCustomerMapper customerMapper = SpringUtils.getBean(CrmCustomerMapper.class);
+        CrmCustomer crmCustomer = customerMapper.selectCrmCustomerById(customerId);
+        if (ObjectUtil.isEmpty(crmCustomer)) throw new RuntimeException("客户不存在");
+        CrmCustomerInfo crmCustomerInfo = customerMapper.selectCrmCustomerInfoById(customerId);
+        if (ObjectUtil.isEmpty(crmCustomerInfo)) {
+            crmCustomerInfo = new CrmCustomerInfo();
+            crmCustomerInfo.setCustomerId(crmCustomer.getCustomerId()).setName(crmCustomer.getCustomerName()).setSex(crmCustomer.getSex().toString())
+                    .setTalk("首次交流");
+            customerMapper.insertCrmCustomerInfo(crmCustomerInfo);
+        }
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        userInfo.put("name", crmCustomerInfo.getName());
+        userInfo.put("sex", crmCustomerInfo.getSex());
+        userInfo.put("age", "");
+        userInfo.put("city", "");
+        userInfo.put("habits", "");
+        userInfo.put("describe", "");
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("userInfo", userInfo);
+        result.put("likeRatio", ObjectUtil.isNotEmpty(crmCustomer.getIntention()) ? crmCustomer.getIntention() : "");
+        return result;
+    }
+
+    private static Map<String, Object> getHistory(List<Map<String, Object>> communication,String customerId) {
+        StringBuilder history = new StringBuilder();
+        history.append("{");
+        for (Map<String, Object> o :
+                communication) {
+            String role = (String) o.get("role");
+            String content = (String) o.get("content");
+            String roleTag = "user".equals(role) ? "user" : "ai";
+            history.append(String.format("\"%s\":\"%s\",", roleTag, content));
+        }
+        history.deleteCharAt(history.length() - 1).append("}");
+        Map<String, Object> result = new HashMap<String, Object>();
+        result.put("history", history);
+        ArrayList<Map> maps = new ArrayList<>();
+        communication.forEach(o->{
+            String role = (String) o.get("role");
+            String content = (String) o.get("content");
+            if (content != null && !content.trim().isEmpty()) { // 过滤空内容
+                String roleTag = "user".equals(role) ? "user" : "ai";
+                Map<String, String> message = new HashMap<>();
+                message.put(roleTag, content);
+                maps.add(message);
+            }
+        });
+        if (!maps.isEmpty()){
+            CrmCustomerInfo crmCustomerInfo = new CrmCustomerInfo();
+            crmCustomerInfo.setCustomerId(Long.valueOf(customerId)).setAiChatRecord(JSONUtil.toJsonStr(maps));
+            SpringUtils.getBean(CrmCustomerMapper.class).updateCrmCustomerInfo(crmCustomerInfo);
+        }
+
+        return result;
+    }
+
+    private static Map<String, Object> getTags(String tradeType) {
+        List<CrmCustomerPropertyTemplate> templates = SpringUtils.getBean(ICrmCustomerPropertyTemplateService.class).getBaseMapper().selectList(new LambdaQueryWrapper<CrmCustomerPropertyTemplate>().eq(
+                CrmCustomerPropertyTemplate::getTradeType, tradeType
+        ));
+        if (ObjectUtil.isEmpty(templates)) throw new RuntimeException("该行业无标签模板");
+        ArrayList<Map<String, String>> tags = new ArrayList<>();//标签及提示词
+        templates.forEach(o -> {
+            Map<String, String> tag = MapUtil.convertToMap(new CrmCustomerAiAutoTagVo(String.valueOf(o.getId()), o.getName(), o.getAiHint()));
+            tags.add(tag);
+        });
+        HashMap<String, Object> resultMap = new HashMap<>();
+        resultMap.put("tags", tags);
+        return resultMap;
+    }
+
+    private static String getDictLabel(String tradeType) {
+        List<SysDictData> tradeTypeDict = DictUtils.getDictCache(TRADE_TYPE);
+        String dictLabel;
+        if (ObjectUtil.isEmpty(tradeTypeDict)) {
+            dictLabel = DictUtils.getDictLabel(TRADE_TYPE, tradeType);
+        } else {
+            Map<String, String> collect = tradeTypeDict.stream().collect(Collectors.toMap(SysDictData::getDictValue,
+                    SysDictData::getDictLabel, (v1, v2) -> v1)
+            );
+            dictLabel = collect.get(tradeType);
+        }
+        if (ObjectUtil.isEmpty(dictLabel)) {
+            throw new RuntimeException("字典中不存在该行业");
+        } else return dictLabel;
+    }
+}

+ 16 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerAiTagVo.java

@@ -0,0 +1,16 @@
+package com.fs.crm.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * 调用ai获取标签的响应类
+ */
+@Data
+@Accessors(chain = true)
+public class CrmCustomerAiTagVo {
+    private String id; // 标签id
+    private String name; // 标签名称
+    private String value; // 标签值
+    private Long customerId; // 客户id
+}

+ 7 - 1
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -86,9 +86,15 @@ public interface FsIntegralGoodsMapper
     @Select({"<script> " +
             "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort  from fs_integral_goods g  " +
             "where g.status=1  " +
-            "<if test = 'maps.goodsType != null and maps.goodsType != 0     '> " +
+            "<if test = 'maps.goodsType != null and maps.goodsType != 0 '> " +
             "and g.goods_type = #{maps.goodsType}  " +
             "</if>" +
+            "<if test = 'maps.keyword != null and maps.keyword != \" \" '> " +
+            "and g.goods_name like concat('%', #{maps.keyword}, '%') " +
+            "</if>" +
+            "<if test = 'maps.appId != null and maps.appId != \" \" '> " +
+            " and ((FIND_IN_SET(#{maps.appId}, g.app_ids) > 0)) " +
+            "</if>"+
             " order by g.goods_id desc "+
             "</script>"})
     List<FsIntegralGoodsListUVO> selectFsIntegralGoodsListUVO(@Param("maps")FsIntegralGoodsListUParam param);

+ 2 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java

@@ -28,4 +28,6 @@ public class FsIntegralGoodsListUParam  implements Serializable {
      * 商品状态(1:上架 2:下架)
      */
     private Integer status;
+
+    private String appId;
 }

+ 15 - 0
fs-service/src/main/java/com/fs/his/vo/CompanyUserBindUserVO.java

@@ -0,0 +1,15 @@
+package com.fs.his.vo;
+
+import lombok.Data;
+
+@Data
+public class CompanyUserBindUserVO {
+
+    private Long userId;
+
+    private String phone;
+
+    private  String nickName;
+
+    private  Integer status;
+}

+ 5 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsIntegralGoodsScrmMapper.java

@@ -75,11 +75,14 @@ public interface FsIntegralGoodsScrmMapper
             "</script>"})
     List<FsIntegralGoodsListVO> selectFsIntegralGoodsListVO(FsIntegralGoodsScrm fsIntegralGoods);
     @Select({"<script> " +
-            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.sort  from fs_integral_goods g  " +
+            "select g.goods_id,g.img_url,g.images,g.goods_name,g.ot_price,g.goods_type,g.integral,g.cash,g.sort  from fs_integral_goods g  " +
             "where g.status=1  " +
-            "<if test = 'maps.goodsType != null and maps.goodsType != 0     '> " +
+            "<if test = 'maps.goodsType != null and maps.goodsType != 0 '> " +
             "and g.goods_type = #{maps.goodsType}  " +
             "</if>" +
+            "<if test = 'maps.keyword != null and maps.keyword != \" \"  '> " +
+            "and g.goods_name like concat('%', #{maps.keyword}, '%') " +
+            "</if>" +
             "<if test = 'maps.appId != null and maps.appId != \" \" '> " +
             " and ((FIND_IN_SET(#{maps.appId}, g.app_ids) > 0)) " +
             "</if>"+

+ 7 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCouponIssueScrmMapper.java

@@ -83,9 +83,10 @@ public interface FsStoreCouponIssueScrmMapper
             "update fs_store_coupon_issue_scrm  set remain_count=(select ifnull(count(1),0) from fs_store_coupon_issue_user_scrm where issue_id=#{id}) where id=#{id}  " +
             "</script>"})
     void updateFsStoreCouponIssueCount(Long id);
+
     @Select({"<script> " +
             "select i.*,c.coupon_price,c.use_min_price,c.coupon_time   from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
-            "where i.limit_time &gt; now() and i.is_del=0 and i.status=1   " +
+            "where i.limit_time &gt; now() and i.is_del=0 and i.status=1  and (i.is_permanent = 1 or (i.is_permanent = 0 and i.remain_count &lt; i.total_count)) and c.coupon_id is not null " +
             "<if test = 'maps.cateId != null and maps.cateId!=0     '> " +
             "and find_in_set(#{maps.cateId},c.package_cate_ids) " +
             "</if>" +
@@ -95,9 +96,13 @@ public interface FsStoreCouponIssueScrmMapper
             "<if test = 'maps.couponPrice != null     '> " +
             "and c.coupon_price =  #{maps.couponPrice} " +
             "</if>" +
-            " order by c.coupon_price desc "+
+            "<if test='maps.couponName != null and maps.couponName != \"\"'> " +
+            "and i.coupon_name like concat(#{maps.couponName},'%') " +
+            "</if>" +
+            " order by c.coupon_price desc " +
             "</script>"})
     List<FsStoreCouponIssueVO> getCompanyCouponIssueList(@Param("maps") FsCouponIssueParam map);
+
     @Select({"<script> " +
             "select i.*,c.coupon_price,c.use_min_price,c.coupon_time  from fs_store_coupon_issue_scrm i left join fs_store_coupon_scrm c on c.coupon_id=i.coupon_id  " +
             "where i.id=#{id}  order by c.coupon_price desc " +

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

@@ -69,6 +69,7 @@ public interface FsStoreOrderItemScrmMapper
 
     @Select("select * from fs_store_order_item_scrm where order_id=#{orderId}")
     List<FsStoreOrderItemVO> selectFsStoreOrderItemListByOrderId(Long orderId);
+
     @Select("select * from fs_store_order_item_scrm where order_id=#{orderId}")
     List<FsStoreOrderItemVO> selectMyFsStoreOrderItemListByOrderId(Long id);
 

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

@@ -80,7 +80,7 @@ public interface FsStoreProductAttrValueScrmMapper
     List<FsStoreProductAttrValueScrm> selectFsStoreProductAttrValueByProductId(Long productId);
     @Select("select ifnull(stock,0) from fs_store_product_attr_value_scrm where  id=#{productAttrValueId}")
     int selectFsStoreProductStockById(Long productAttrValueId);
-    
+
     /**
      * 使用行锁查询规格库存
      */
@@ -91,9 +91,11 @@ public interface FsStoreProductAttrValueScrmMapper
     @Update("update fs_store_product_attr_value_scrm set stock=stock-#{num},sales=sales+#{num}" +
             " where id=#{productAttrValueId} and stock >= #{num} ")
     int decProductAttrStock(@Param("productAttrValueId")Long productAttrValueId, @Param("num") Integer cartNum);
+
     @Update("update fs_store_product_attr_value_scrm set stock=stock+#{num}, sales=sales-#{num}" +
             " where product_id=#{productId} and id=#{productAttrValueId}")
     int incProductAttrStock(@Param("num")Long num,@Param("productId") Long productId,@Param("productAttrValueId") Long productAttrValueId);
+
     @Select({"<script> " +
             "select v.*,p.product_name,p.product_type,c.cate_name  from fs_store_product_attr_value_scrm v inner join fs_store_product_scrm p on p.product_id=v.product_id left join fs_store_product_category_scrm c on c.cate_id=p.cate_id   " +
             "where 1=1 and v.bar_code is not null " +
@@ -118,6 +120,7 @@ public interface FsStoreProductAttrValueScrmMapper
             " order by v.id desc "+
             "</script>"})
     List<FsStoreProductAttrValueVO> selectFsStoreProductAttrValueListVO(@Param("maps")FsProductAttrValueParam param);
+
     @Select({"<script> " +
             "select v.*,p.product_name from fs_store_product_attr_value_scrm v " +
             "inner join fs_store_product_scrm p on p.product_id=v.product_id    " +

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -277,6 +277,7 @@ public interface FsStoreProductScrmMapper
             " and p.product_id=#{productId} " +
             "</script>"})
     FsStoreProductQueryVO selectFsStoreProductByIdQuery(@Param("productId") Long productId, @Param("storeId") String storeId, @Param("config") MedicalMallConfig config);
+
     @Update("update fs_store_product_scrm set stock=stock-#{num}, sales=sales+#{num}" +
             " where product_id=#{productId} and stock >= #{num}")
     int decProductAttrStock(@Param("productId")Long productId, @Param("num")Integer cartNum);
@@ -287,6 +288,7 @@ public interface FsStoreProductScrmMapper
     @Select("select stock from fs_store_product_scrm where product_id=#{productId} for update")
     @DataSource(DataSourceType.SLAVE)
     Integer selectProductStockForUpdate(@Param("productId") Long productId);
+
     @Update("update fs_store_product_scrm set stock=stock+#{num}, sales=sales-#{num}" +
             " where product_id=#{productId}")
     int incStockDecSales( @Param("num")Long num, @Param("productId")Long productId);

+ 1 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsCouponIssueParam.java

@@ -13,4 +13,5 @@ public class FsCouponIssueParam extends BaseQueryParam implements Serializable
     private Integer cateId;
     private Integer couponType;
     private BigDecimal couponPrice;
+    private String couponName;
 }

+ 24 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsIntegralGoodsListUParam.java

@@ -1,6 +1,7 @@
 package com.fs.hisStore.param;
 
 import com.fs.common.param.BaseQueryParam;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -8,4 +9,27 @@ import java.io.Serializable;
 @Data
 public class FsIntegralGoodsListUParam extends BaseQueryParam implements Serializable {
     Integer goodsType;
+
+    @ApiModelProperty(value = "页码,默认为1")
+    private Integer pageNum =1;
+    @ApiModelProperty(value = "页大小,默认为10")
+    private Integer pageSize = 10;
+    private String keyword;
+
+    /**
+     * 最小积分
+     */
+    private Integer minPoints;
+
+    /**
+     * 最大积分
+     */
+    private Integer maxPoints;
+
+    /**
+     * 商品状态(1:上架 2:下架)
+     */
+    private Integer status;
+
+    private String appId;
 }

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

@@ -3086,7 +3086,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         if (param.getCouponUserId() != null) {
             FsStoreCouponUserScrm couponUser = couponUserService.selectFsStoreCouponUserById(param.getCouponUserId());
             if (couponUser != null && couponUser.getStatus() == 0) {
-                if (couponUser.getUseMinPrice().compareTo(storeProductPackage.getPayMoney()) == -1) {
+                if (couponUser.getUseMinPrice().compareTo(storeProductPackage.getPayMoney()) <= 0) {
                     totalMoney = totalMoney.subtract(couponUser.getCouponPrice());
                 }
             }

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

@@ -4,6 +4,8 @@ package com.fs.live.domain;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
 
+import java.math.BigDecimal;
+
 /**
  * 直播数据对象 live_data
  *
@@ -14,7 +16,6 @@ import lombok.Data;
 public class LiveData{
 
     /** 直播id */
-
     private Long liveId;
 
    /* *//** 直播名称 *//*
@@ -84,4 +85,27 @@ public class LiveData{
     @Excel(name = "回放点赞数")
     private Long replayLikeNum;
 
+    /** ========== 数据概览缓存字段(每个直播间独立缓存) ========== */
+    private Long overviewBeforeLiveUv;
+    private Long overviewTotalWatchUv;
+    private Long overviewOver10MinCount;
+    private Long overviewTotalWatchMinutes;
+    private Long overviewWatchTotalSeconds;
+    private Long overviewWatchUserCount;
+    private Long overviewReplayWatchUv;
+    private Long overviewReplayVisitPv;
+    private Long overviewReplayOnlyCount;
+    private Long overviewReplayTotalMinutes;
+    private Long overviewReplayTotalSeconds;
+    private Long overviewReplayUserCount;
+    private Long overviewCompleteCount;
+    private Long overviewSubscribeCount;
+    private Long overviewLotteryCount;
+    private Long overviewLotteryJoinCount;
+    private Long overviewLotteryWinCount;
+    private Long overviewPaidUserCount;
+    private Long overviewUnpaidUserCount;
+    private BigDecimal overviewTotalGmv;
+    private Long overviewTotalProductQty;
+
 }

+ 99 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java

@@ -7,8 +7,11 @@ import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveDataDetailVo;
+import com.fs.live.param.LiveRoomStudentParam;
 import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
+import com.fs.live.vo.LiveStatisticsOverviewVO;
+import com.fs.live.vo.LiveRoomStudentQueryVO;
 import com.fs.live.vo.LiveUserDetailVo;
 import com.fs.live.vo.LiveAppSimpleVO;
 import com.fs.live.vo.RecentLiveDataVo;
@@ -17,6 +20,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -36,6 +40,21 @@ public interface LiveDataMapper {
      */
     LiveData selectLiveDataByLiveId(Long liveId);
 
+    /**
+     * 查询已有数据概览缓存的直播间
+     */
+    List<LiveData> selectLiveDataOverviewByLiveIds(@Param("liveIds") List<Long> liveIds);
+
+    /**
+     * 更新数据概览缓存
+     */
+    int updateLiveDataOverview(LiveData liveData);
+
+    /**
+     * 插入数据概览缓存
+     */
+    int insertLiveDataOverview(LiveData liveData);
+
     /**
      * 查询直播数据列表
      *
@@ -246,4 +265,84 @@ public interface LiveDataMapper {
      */
     @DataSource(DataSourceType.SLAVE)
     List<LiveAppSimpleVO> selectLivingLivesForApp();
+
+    /**
+     * 查询直播数据统计-数据概览(12项指标)- 已废弃,使用拆分查询替代
+     * @param liveIds 直播间ID列表
+     * @return 数据概览VO
+     */
+    @Deprecated
+    @DataSource(DataSourceType.SLAVE)
+    LiveStatisticsOverviewVO selectLiveStatisticsOverview(@Param("liveIds") List<Long> liveIds);
+
+    // ========== 数据概览拆分查询(每表独立查询,Java层合并) ==========
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewBeforeLiveUv(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewTotalWatchUv(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewOver10MinCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewWatchTotalSeconds(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewWatchUserCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewReplayWatchUv(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewReplayVisitPv(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewReplayOnlyCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewReplayTotalSeconds(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewReplayUserCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewCompleteCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewSubscribeCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewLotteryCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewLotteryJoinCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewLotteryWinCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewPaidUserCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewUnpaidUserCount(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    java.math.BigDecimal selectOverviewTotalGmv(@Param("liveIds") List<Long> liveIds);
+    @DataSource(DataSourceType.SLAVE)
+    Long selectOverviewTotalProductQty(@Param("liveIds") List<Long> liveIds);
+
+    /**
+     * 直播趋势:查询进入直播间的原始数据(live_id, live_name, start_time, entry_time)
+     * 用于计算相对时间并统计各时段累计进入人数
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<Map<String, Object>> selectLiveEntryTrendRawData(@Param("liveIds") List<Long> liveIds);
+
+    /**
+     * 直播间学员列表(基于 live_user_first_entry)
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveRoomStudentQueryVO> selectLiveRoomStudentList(@Param("param") LiveRoomStudentParam param);
+
+    /**
+     * 商品对比统计:按商品汇总 下单未支付人数、成交人数、成交金额
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<com.fs.live.vo.ProductCompareVO> selectProductCompareList(@Param("param") com.fs.live.param.ProductCompareParam param);
+
+    /**
+     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<com.fs.live.vo.InviteSalesOptionVO> selectInviteSalesOptions(@Param("liveIds") List<Long> liveIds);
+
+    /**
+     * 邀课对比统计:按销售汇总 归属公司、销售名称、邀请人数、已支付订单数、订单总金额
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<com.fs.live.vo.InviteCompareVO> selectInviteCompareList(@Param("param") com.fs.live.param.InviteCompareParam param);
 }

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

@@ -241,4 +241,14 @@ public interface LiveMapper
     List<Live> listToLiveNoEnd(@Param("live") Live live);
 
     List<Live> selectLiveListNew(Live live);
+
+    /**
+     * 查询结束时间在指定天数内的直播间ID列表(用于数据概览缓存定时任务)
+     * @param days 天数,如7表示最近7天内结束的直播间
+     * @return live_id 列表
+     */
+    @DataSource(DataSourceType.SLAVE)
+    @Select("SELECT live_id FROM live WHERE finish_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY) " +
+            "AND finish_time <= NOW() AND is_del = 0 AND is_audit = 1 AND status IN (3, 4)")
+    List<Long> selectLiveIdsByFinishTimeWithinDays(@Param("days") int days);
 }

+ 27 - 0
fs-service/src/main/java/com/fs/live/param/InviteCompareParam.java

@@ -0,0 +1,27 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 邀课对比统计查询参数(基于 live_user_first_entry 的销售信息)
+ *
+ * @author fs
+ * @date 2025-03-19
+ */
+@Data
+public class InviteCompareParam {
+
+    /** 直播间ID列表 */
+    private List<Long> liveIds;
+
+    /** 分享人(公司用户ID列表,company_user_id,可选,不传则查全部) */
+    private List<Long> companyUserIds;
+
+    /** 页码 */
+    private Integer pageNum = 1;
+
+    /** 每页条数 */
+    private Integer pageSize = 10;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/live/param/LiveRoomStudentParam.java

@@ -0,0 +1,34 @@
+package com.fs.live.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播间学员查询参数
+ *
+ * @author fs
+ * @date 2025-03-18
+ */
+@Data
+public class LiveRoomStudentParam {
+
+    /** 直播间ID列表(传ids给后端) */
+    private List<Long> liveIds;
+
+    /** 首次访问时间-开始(live_user_first_entry.create_time 范围) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date firstEntryTimeBegin;
+
+    /** 首次访问时间-结束(live_user_first_entry.create_time 范围) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date firstEntryTimeEnd;
+
+    /** 页码,默认1 */
+    private Integer pageNum = 1;
+
+    /** 每页条数,默认10 */
+    private Integer pageSize = 10;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/live/param/ProductCompareParam.java

@@ -0,0 +1,27 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 商品对比统计查询参数
+ *
+ * @author fs
+ * @date 2025-03-19
+ */
+@Data
+public class ProductCompareParam {
+
+    /** 直播间ID列表 */
+    private List<Long> liveIds;
+
+    /** 商品ID列表(可选,不传则查全部带货商品) */
+    private List<Long> productIds;
+
+    /** 页码 */
+    private Integer pageNum = 1;
+
+    /** 每页条数 */
+    private Integer pageSize = 10;
+}

+ 51 - 0
fs-service/src/main/java/com/fs/live/service/ILiveDataService.java

@@ -5,6 +5,7 @@ import com.fs.common.core.domain.R;
 import com.fs.live.domain.LiveData;
 import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.LiveRoomStudentParam;
 import com.fs.live.vo.*;
 
 import java.util.List;
@@ -182,4 +183,54 @@ public interface ILiveDataService {
      * @return 分公司统计数据
      */
     List<LiveDataCompanyVO> listLiveDataCompany(LiveDataCompanyParam param);
+
+    /**
+     * 直播数据统计-数据概览(12项指标)
+     * @param liveIds 直播间ID列表
+     * @return 数据概览
+     */
+    LiveStatisticsOverviewVO getLiveStatisticsOverview(List<Long> liveIds);
+
+    /**
+     * 直播趋势-进入人数折线图数据
+     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+     * @param liveIds 直播间ID列表
+     * @return 折线图数据(xAxis + series)
+     */
+    LiveEntryTrendVO getLiveEntryTrend(List<Long> liveIds);
+
+    /**
+     * 直播间学员列表(分页,基于 live_user_first_entry)
+     * @param param 查询参数(liveIds、首次访问时间范围、分页)
+     * @return 分页结果
+     */
+    com.fs.common.core.domain.R listLiveRoomStudents(LiveRoomStudentParam param);
+
+    /**
+     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+     * @param param 查询参数(liveIds、productIds、分页)
+     * @return 分页结果
+     */
+    com.fs.common.core.domain.R listProductCompareStats(com.fs.live.param.ProductCompareParam param);
+
+    /**
+     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+     * @param liveIds 直播间ID列表
+     * @return 分享人选项列表
+     */
+    java.util.List<com.fs.live.vo.InviteSalesOptionVO> listInviteSalesOptions(java.util.List<Long> liveIds);
+
+    /**
+     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+     * @param param 查询参数(liveIds、companyUserIds、分页)
+     * @return 分页结果
+     */
+    com.fs.common.core.domain.R listInviteCompareStats(com.fs.live.param.InviteCompareParam param);
+
+    /**
+     * 直播间学员列表(分页,基于 live_user_first_entry)
+     * @LiveRoomStudentParam param 查询参数(liveIds、首次访问时间范围、分页)
+     * @return 分页结果
+     */
+    LiveData calculateAndSaveOverviewForLive(List<Long> liveIds);
 }

+ 314 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -10,14 +10,18 @@ import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.mapper.FsUserScrmMapper;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.*;
+import com.fs.common.utils.ParseUtils;
 import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
+import com.fs.live.param.LiveRoomStudentParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.service.ILiveUserFavoriteService;
 import com.fs.live.service.ILiveUserFollowService;
 import com.fs.live.service.ILiveUserLikeService;
 import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.*;
+import com.fs.live.vo.LiveRoomStudentQueryVO;
+import com.fs.live.vo.LiveRoomStudentVO;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyMapper;
@@ -33,6 +37,7 @@ import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import java.util.stream.Collectors;
 import java.util.function.BiConsumer;
 
+import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -51,6 +56,7 @@ import java.util.*;
 import java.util.concurrent.*;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
+import java.util.concurrent.CompletableFuture;
 
 
 /**
@@ -256,6 +262,314 @@ public class LiveDataServiceImpl implements ILiveDataService {
         return queryLiveDataCompanyByDateRange(param);
     }
 
+    @Override
+    public LiveStatisticsOverviewVO getLiveStatisticsOverview(List<Long> liveIds) {
+        LiveStatisticsOverviewVO vo = new LiveStatisticsOverviewVO();
+        if (liveIds == null || liveIds.isEmpty()) {
+            return vo;
+        }
+        // 1. 先从 live_data 查询已有缓存的直播间
+        List<LiveData> cachedList = liveDataMapper.selectLiveDataOverviewByLiveIds(liveIds);
+        Set<Long> cachedLiveIds = cachedList == null ? Collections.emptySet()
+                : cachedList.stream().map(LiveData::getLiveId).filter(Objects::nonNull).collect(Collectors.toSet());
+        List<Long> needCalcIds = liveIds.stream().filter(id -> !cachedLiveIds.contains(id)).collect(Collectors.toList());
+
+        // 2. 对未缓存的直播间进行统计并保存到 live_data
+        if (!needCalcIds.isEmpty()) {
+            for (Long liveId : needCalcIds) {
+                LiveData overviewData = calculateAndSaveOverviewForLive(Collections.singletonList(liveId));
+                if (overviewData != null) {
+                    cachedList = cachedList == null ? new ArrayList<>() : cachedList;
+                    overviewData.setLiveId(liveId);
+                    cachedList.add(overviewData);
+                }
+            }
+        }
+
+        // 3. 聚合所有直播间数据(来自缓存 + 新计算的)
+        if (cachedList == null || cachedList.isEmpty()) {
+            return vo;
+        }
+        long beforeLiveUv = 0, totalWatchUv = 0, over10MinCount = 0, totalWatchMinutes = 0;
+        long watchTotalSeconds = 0, watchUserCount = 0;
+        long replayWatchUv = 0, replayVisitPv = 0, replayOnlyCount = 0, replayTotalMinutes = 0;
+        long replayTotalSeconds = 0, replayUserCount = 0;
+        long completeCount = 0, subscribeCount = 0, lotteryCount = 0, lotteryJoinCount = 0, lotteryWinCount = 0;
+        long paidUserCount = 0, unpaidUserCount = 0, totalProductQty = 0;
+        BigDecimal totalGmv = BigDecimal.ZERO;
+        for (LiveData ld : cachedList) {
+            beforeLiveUv += nullToZero(ld.getOverviewBeforeLiveUv());
+            totalWatchUv += nullToZero(ld.getOverviewTotalWatchUv());
+            over10MinCount += nullToZero(ld.getOverviewOver10MinCount());
+            totalWatchMinutes += nullToZero(ld.getOverviewTotalWatchMinutes());
+            watchTotalSeconds += nullToZero(ld.getOverviewWatchTotalSeconds());
+            watchUserCount += nullToZero(ld.getOverviewWatchUserCount());
+            replayWatchUv += nullToZero(ld.getOverviewReplayWatchUv());
+            replayVisitPv += nullToZero(ld.getOverviewReplayVisitPv());
+            replayOnlyCount += nullToZero(ld.getOverviewReplayOnlyCount());
+            replayTotalMinutes += nullToZero(ld.getOverviewReplayTotalMinutes());
+            replayTotalSeconds += nullToZero(ld.getOverviewReplayTotalSeconds());
+            replayUserCount += nullToZero(ld.getOverviewReplayUserCount());
+            completeCount += nullToZero(ld.getOverviewCompleteCount());
+            subscribeCount += nullToZero(ld.getOverviewSubscribeCount());
+            lotteryCount += nullToZero(ld.getOverviewLotteryCount());
+            lotteryJoinCount += nullToZero(ld.getOverviewLotteryJoinCount());
+            lotteryWinCount += nullToZero(ld.getOverviewLotteryWinCount());
+            paidUserCount += nullToZero(ld.getOverviewPaidUserCount());
+            unpaidUserCount += nullToZero(ld.getOverviewUnpaidUserCount());
+            totalProductQty += nullToZero(ld.getOverviewTotalProductQty());
+            if (ld.getOverviewTotalGmv() != null) {
+                totalGmv = totalGmv.add(ld.getOverviewTotalGmv());
+            }
+        }
+        vo.setBeforeLiveUv(beforeLiveUv);
+        vo.setTotalWatchUv(totalWatchUv);
+        vo.setOver10MinCount(over10MinCount);
+        vo.setTotalWatchMinutes(totalWatchMinutes);
+        vo.setAvgWatchMinutes(calcAvgMinutes(watchTotalSeconds, watchUserCount));
+        vo.setReplayWatchUv(replayWatchUv);
+        vo.setReplayVisitPv(replayVisitPv);
+        vo.setReplayOnlyCount(replayOnlyCount);
+        vo.setReplayTotalMinutes(replayTotalMinutes);
+        vo.setReplayAvgMinutes(calcAvgMinutes(replayTotalSeconds, replayUserCount));
+        vo.setCompleteCount(completeCount);
+        vo.setSubscribeCount(subscribeCount);
+        vo.setLotteryCount(lotteryCount);
+        vo.setLotteryJoinCount(lotteryJoinCount);
+        vo.setLotteryWinCount(lotteryWinCount);
+        vo.setPaidUserCount(paidUserCount);
+        vo.setUnpaidUserCount(unpaidUserCount);
+        vo.setTotalGmv(totalGmv);
+        vo.setTotalProductQty(totalProductQty);
+        return vo;
+    }
+
+    /**
+     * 对指定直播间进行统计,保存到 live_data,并返回 LiveData
+     */
+    @Override
+    public LiveData calculateAndSaveOverviewForLive(List<Long> liveIds) {
+        if (liveIds == null || liveIds.isEmpty()) return null;
+        CompletableFuture<Long> fBeforeLiveUv = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewBeforeLiveUv(liveIds));
+        CompletableFuture<Long> fTotalWatchUv = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewTotalWatchUv(liveIds));
+        CompletableFuture<Long> fOver10MinCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewOver10MinCount(liveIds));
+        CompletableFuture<Long> fWatchTotalSeconds = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewWatchTotalSeconds(liveIds));
+        CompletableFuture<Long> fWatchUserCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewWatchUserCount(liveIds));
+        CompletableFuture<Long> fReplayWatchUv = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewReplayWatchUv(liveIds));
+        CompletableFuture<Long> fReplayVisitPv = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewReplayVisitPv(liveIds));
+        CompletableFuture<Long> fReplayOnlyCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewReplayOnlyCount(liveIds));
+        CompletableFuture<Long> fReplayTotalSeconds = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewReplayTotalSeconds(liveIds));
+        CompletableFuture<Long> fReplayUserCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewReplayUserCount(liveIds));
+        CompletableFuture<Long> fCompleteCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewCompleteCount(liveIds));
+        CompletableFuture<Long> fSubscribeCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewSubscribeCount(liveIds));
+        CompletableFuture<Long> fLotteryCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewLotteryCount(liveIds));
+        CompletableFuture<Long> fLotteryJoinCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewLotteryJoinCount(liveIds));
+        CompletableFuture<Long> fLotteryWinCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewLotteryWinCount(liveIds));
+        CompletableFuture<Long> fPaidUserCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewPaidUserCount(liveIds));
+        CompletableFuture<Long> fUnpaidUserCount = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewUnpaidUserCount(liveIds));
+        CompletableFuture<BigDecimal> fTotalGmv = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewTotalGmv(liveIds));
+        CompletableFuture<Long> fTotalProductQty = CompletableFuture.supplyAsync(() -> liveDataMapper.selectOverviewTotalProductQty(liveIds));
+
+        CompletableFuture.allOf(fBeforeLiveUv, fTotalWatchUv, fOver10MinCount, fWatchTotalSeconds, fWatchUserCount,
+                fReplayWatchUv, fReplayVisitPv, fReplayOnlyCount, fReplayTotalSeconds, fReplayUserCount,
+                fCompleteCount, fSubscribeCount, fLotteryCount, fLotteryJoinCount, fLotteryWinCount,
+                fPaidUserCount, fUnpaidUserCount, fTotalGmv, fTotalProductQty).join();
+
+        Long watchTotalSeconds = fWatchTotalSeconds.join();
+        Long watchUserCount = fWatchUserCount.join();
+        Long replayTotalSeconds = fReplayTotalSeconds.join();
+        Long replayUserCount = fReplayUserCount.join();
+        long totalWatchMinutes = nullToZero(watchTotalSeconds) / 60;
+        long replayTotalMinutes = nullToZero(replayTotalSeconds) / 60;
+
+        LiveData ld = new LiveData();
+        ld.setLiveId(liveIds.get(0));
+        ld.setOverviewBeforeLiveUv(fBeforeLiveUv.join());
+        ld.setOverviewTotalWatchUv(fTotalWatchUv.join());
+        ld.setOverviewOver10MinCount(fOver10MinCount.join());
+        ld.setOverviewTotalWatchMinutes(totalWatchMinutes);
+        ld.setOverviewWatchTotalSeconds(watchTotalSeconds);
+        ld.setOverviewWatchUserCount(watchUserCount);
+        ld.setOverviewReplayWatchUv(fReplayWatchUv.join());
+        ld.setOverviewReplayVisitPv(fReplayVisitPv.join());
+        ld.setOverviewReplayOnlyCount(fReplayOnlyCount.join());
+        ld.setOverviewReplayTotalMinutes(replayTotalMinutes);
+        ld.setOverviewReplayTotalSeconds(replayTotalSeconds);
+        ld.setOverviewReplayUserCount(replayUserCount);
+        ld.setOverviewCompleteCount(fCompleteCount.join());
+        ld.setOverviewSubscribeCount(fSubscribeCount.join());
+        ld.setOverviewLotteryCount(fLotteryCount.join());
+        ld.setOverviewLotteryJoinCount(fLotteryJoinCount.join());
+        ld.setOverviewLotteryWinCount(fLotteryWinCount.join());
+        ld.setOverviewPaidUserCount(fPaidUserCount.join());
+        ld.setOverviewUnpaidUserCount(fUnpaidUserCount.join());
+        ld.setOverviewTotalGmv(fTotalGmv.join());
+        ld.setOverviewTotalProductQty(fTotalProductQty.join());
+
+        // 保存到 live_data:先尝试 update,无行则 insert
+        int updated = liveDataMapper.updateLiveDataOverview(ld);
+        if (updated == 0) {
+            liveDataMapper.insertLiveDataOverview(ld);
+        }
+        return ld;
+    }
+
+    private long nullToZero(Long v) {
+        return v == null ? 0L : v;
+    }
+
+    private BigDecimal calcAvgMinutes(long totalSeconds, long userCount) {
+        if (userCount <= 0) return BigDecimal.ZERO;
+        return BigDecimal.valueOf(totalSeconds)
+                .divide(BigDecimal.valueOf(60 * userCount), 2, RoundingMode.HALF_UP);
+    }
+
+    @Override
+    public LiveEntryTrendVO getLiveEntryTrend(List<Long> liveIds) {
+        LiveEntryTrendVO vo = new LiveEntryTrendVO();
+        if (liveIds == null || liveIds.isEmpty()) {
+            vo.setXAxis(Collections.singletonList("开播前"));
+            vo.setSeries(Collections.emptyList());
+            return vo;
+        }
+        List<Map<String, Object>> rawList = liveDataMapper.selectLiveEntryTrendRawData(liveIds);
+        if (rawList == null || rawList.isEmpty()) {
+            vo.setXAxis(Collections.singletonList("开播前"));
+            vo.setSeries(Collections.emptyList());
+            return vo;
+        }
+        // 时间桶:开播前, 0min, 5min, 10min, ... (步长5分钟,最多到120分钟)
+        int stepMinutes = 5;
+        int maxMinutes = 120;
+        List<String> xAxis = new ArrayList<>();
+        xAxis.add("开播前");
+        for (int m = 0; m <= maxMinutes; m += stepMinutes) {
+            xAxis.add(m + "min");
+        }
+        // 按 liveId 分组,每个直播间一条折线(累计进入人数)
+        Map<String, Map<Integer, Integer>> liveBucketCount = new LinkedHashMap<>();
+        for (Map<String, Object> row : rawList) {
+            Object liveIdObj = row.get("liveId");
+            Object liveNameObj = row.get("liveName");
+            Object startTimeObj = row.get("startTime");
+            Object entryTimeObj = row.get("entryTime");
+            if (liveIdObj == null || startTimeObj == null || entryTimeObj == null) continue;
+            long liveId = ((Number) liveIdObj).longValue();
+            String liveName = liveNameObj != null ? liveNameObj.toString() : String.valueOf(liveId);
+            String seriesKey = liveId + "|" + liveName;
+            Date startTime = toDate(startTimeObj);
+            Date entryTime = toDate(entryTimeObj);
+            if (startTime == null || entryTime == null) continue;
+            long diffMinutes = (entryTime.getTime() - startTime.getTime()) / (60 * 1000);
+            int bucketIdx;
+            if (diffMinutes < 0) {
+                bucketIdx = 0; // 开播前
+            } else {
+                int m = (int) Math.min(diffMinutes, maxMinutes);
+                bucketIdx = 1 + (m / stepMinutes); // 1-based, 0 is 开播前
+            }
+            liveBucketCount
+                    .computeIfAbsent(seriesKey, k -> new LinkedHashMap<>())
+                    .merge(bucketIdx, 1, Integer::sum);
+        }
+        // 转为累计值并生成 series
+        List<LiveEntryTrendSeriesVO> seriesList = new ArrayList<>();
+        for (Map.Entry<String, Map<Integer, Integer>> e : liveBucketCount.entrySet()) {
+            String[] parts = e.getKey().split("\\|", 2);
+            String name = parts.length > 1 ? parts[1] + "(" + parts[0] + ")" : parts[0];
+            Map<Integer, Integer> countMap = e.getValue();
+            List<Integer> data = new ArrayList<>(xAxis.size());
+            int cumulative = 0;
+            for (int i = 0; i < xAxis.size(); i++) {
+                cumulative += countMap.getOrDefault(i, 0);
+                data.add(cumulative);
+            }
+            seriesList.add(new LiveEntryTrendSeriesVO(name, data));
+        }
+        vo.setXAxis(xAxis);
+        vo.setSeries(seriesList);
+        return vo;
+    }
+
+    @Override
+    public R listLiveRoomStudents(LiveRoomStudentParam param) {
+        if (param == null) {
+            param = new LiveRoomStudentParam();
+        }
+        if (param.getPageNum() == null || param.getPageNum() < 1) {
+            param.setPageNum(1);
+        }
+        if (param.getPageSize() == null || param.getPageSize() < 1) {
+            param.setPageSize(10);
+        }
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<LiveRoomStudentQueryVO> queryList = liveDataMapper.selectLiveRoomStudentList(param);
+        PageInfo<LiveRoomStudentQueryVO> pageInfo = new PageInfo<>(queryList);
+        List<LiveRoomStudentVO> resultList = new ArrayList<>();
+        if (queryList != null) {
+            for (LiveRoomStudentQueryVO q : queryList) {
+                LiveRoomStudentVO vo = new LiveRoomStudentVO();
+                vo.setAvatar(q.getAvatar());
+                vo.setUserName(q.getUserName());
+                vo.setLiveName(q.getLiveName());
+                vo.setSalesName(q.getSalesName());
+                vo.setUserCreateTime(q.getUserCreateTime());
+                vo.setContact(ParseUtils.parsePhone(q.getPhone()));
+                resultList.add(vo);
+            }
+        }
+        return R.ok().put("rows", resultList).put("total", pageInfo.getTotal());
+    }
+
+    @Override
+    public R listProductCompareStats(com.fs.live.param.ProductCompareParam param) {
+        if (param == null || param.getLiveIds() == null || param.getLiveIds().isEmpty()) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0);
+        }
+        if (param.getPageNum() == null || param.getPageNum() < 1) {
+            param.setPageNum(1);
+        }
+        if (param.getPageSize() == null || param.getPageSize() < 1) {
+            param.setPageSize(10);
+        }
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<com.fs.live.vo.ProductCompareVO> list = liveDataMapper.selectProductCompareList(param);
+        PageInfo<com.fs.live.vo.ProductCompareVO> pageInfo = new PageInfo<>(list);
+        return R.ok().put("rows", pageInfo.getList()).put("total", pageInfo.getTotal());
+    }
+
+    @Override
+    public List<com.fs.live.vo.InviteSalesOptionVO> listInviteSalesOptions(List<Long> liveIds) {
+        if (liveIds == null || liveIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return liveDataMapper.selectInviteSalesOptions(liveIds);
+    }
+
+    @Override
+    public R listInviteCompareStats(com.fs.live.param.InviteCompareParam param) {
+        if (param == null || param.getLiveIds() == null || param.getLiveIds().isEmpty()) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0);
+        }
+        if (param.getPageNum() == null || param.getPageNum() < 1) {
+            param.setPageNum(1);
+        }
+        if (param.getPageSize() == null || param.getPageSize() < 1) {
+            param.setPageSize(10);
+        }
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<com.fs.live.vo.InviteCompareVO> list = liveDataMapper.selectInviteCompareList(param);
+        PageInfo<com.fs.live.vo.InviteCompareVO> pageInfo = new PageInfo<>(list);
+        return R.ok().put("rows", pageInfo.getList()).put("total", pageInfo.getTotal());
+    }
+
+    private Date toDate(Object val) {
+        if (val == null) return null;
+        if (val instanceof Date) return (Date) val;
+        if (val instanceof java.sql.Timestamp) return new Date(((java.sql.Timestamp) val).getTime());
+        return null;
+    }
+
     /**
      * 多线程分段查询:步长7天,等待各段数据返回后合并
      */

+ 36 - 0
fs-service/src/main/java/com/fs/live/vo/InviteCompareVO.java

@@ -0,0 +1,36 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 邀课对比统计VO(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+ *
+ * @author fs
+ * @date 2025-03-19
+ */
+@Data
+public class InviteCompareVO {
+
+    /** 归属公司ID */
+    private Long companyId;
+
+    /** 归属公司名称 */
+    private String companyName;
+
+    /** 销售(公司用户)ID */
+    private Long companyUserId;
+
+    /** 销售名称 */
+    private String salesName;
+
+    /** 邀请人数 */
+    private Long inviteCount = 0L;
+
+    /** 已支付订单数 */
+    private Long paidOrderCount = 0L;
+
+    /** 订单总金额 */
+    private BigDecimal totalGmv = BigDecimal.ZERO;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/live/vo/InviteSalesOptionVO.java

@@ -0,0 +1,25 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * 邀课对比-分享人选项VO(用于筛选项下拉)
+ *
+ * @author fs
+ * @date 2025-03-19
+ */
+@Data
+public class InviteSalesOptionVO {
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 公司用户ID(company_user_id) */
+    private Long companyUserId;
+
+    /** 销售名称 */
+    private String salesName;
+
+    /** 归属公司名称 */
+    private String companyName;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/live/vo/LiveEntryTrendSeriesVO.java

@@ -0,0 +1,22 @@
+package com.fs.live.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 直播趋势-单条折线系列
+ *
+ * @author fs
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LiveEntryTrendSeriesVO {
+    /** 系列名称,如 直播间名称(liveId) */
+    private String name;
+    /** 累计进入人数,与 xAxis 一一对应 */
+    private List<Integer> data;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/live/vo/LiveEntryTrendVO.java

@@ -0,0 +1,19 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 直播趋势-进入人数折线图 VO
+ * 基于 live_user_first_entry 与 live.start_time 计算相对时间
+ *
+ * @author fs
+ */
+@Data
+public class LiveEntryTrendVO {
+    /** X轴:相对时间标签,如 开播前、0min、5min、10min */
+    private List<String> xAxis;
+    /** 折线系列:每个直播间一条线 */
+    private List<LiveEntryTrendSeriesVO> series;
+}

+ 35 - 0
fs-service/src/main/java/com/fs/live/vo/LiveRoomStudentQueryVO.java

@@ -0,0 +1,35 @@
+package com.fs.live.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 直播间学员查询结果VO(内部使用,含原始手机号)
+ *
+ * @author fs
+ * @date 2025-03-18
+ */
+@Data
+public class LiveRoomStudentQueryVO {
+
+    /** 用户头像 */
+    private String avatar;
+
+    /** 用户名 */
+    private String userName;
+
+    /** 直播间名称 */
+    private String liveName;
+
+    /** 销售名称 */
+    private String salesName;
+
+    /** 用户创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date userCreateTime;
+
+    /** 手机号(原始,用于加密) */
+    private String phone;
+}

+ 35 - 0
fs-service/src/main/java/com/fs/live/vo/LiveRoomStudentVO.java

@@ -0,0 +1,35 @@
+package com.fs.live.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 直播间学员VO
+ *
+ * @author fs
+ * @date 2025-03-18
+ */
+@Data
+public class LiveRoomStudentVO {
+
+    /** 用户头像 */
+    private String avatar;
+
+    /** 用户名 */
+    private String userName;
+
+    /** 直播间名称 */
+    private String liveName;
+
+    /** 销售名称 */
+    private String salesName;
+
+    /** 用户创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date userCreateTime;
+
+    /** 联系方式(手机号加密) */
+    private String contact;
+}

+ 54 - 0
fs-service/src/main/java/com/fs/live/vo/LiveStatisticsOverviewVO.java

@@ -0,0 +1,54 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 直播数据统计-数据概览VO
+ *
+ * @author fs
+ */
+@Data
+public class LiveStatisticsOverviewVO {
+    /** 1. 开播前访问人数(uv) */
+    private Long beforeLiveUv = 0L;
+    /** 2. 累计观看人数(uv) */
+    private Long totalWatchUv = 0L;
+    /** 3. 停留时长超过10分钟人数 */
+    private Long over10MinCount = 0L;
+    /** 4. 总观看时长(分钟) */
+    private Long totalWatchMinutes = 0L;
+    /** 5. 平均观看时长(分钟) */
+    private BigDecimal avgWatchMinutes = BigDecimal.ZERO;
+    /** 6. 累计回放观看人数(uv) */
+    private Long replayWatchUv = 0L;
+    /** 7. 累计回放访问人数(pv) */
+    private Long replayVisitPv = 0L;
+    /** 8. 仅看过回放的人数 */
+    private Long replayOnlyCount = 0L;
+    /** 9. 回放总观看时长(分钟) */
+    private Long replayTotalMinutes = 0L;
+    /** 10. 回放平均观看时长(分钟) */
+    private BigDecimal replayAvgMinutes = BigDecimal.ZERO;
+    /** 11. 完课人数(看课时长超过20分钟) */
+    private Long completeCount = 0L;
+    /** 12. 预约人数 */
+    private Long subscribeCount = 0L;
+
+    /** ========== 互动带货数据 ========== */
+    /** 13. 抽奖次数 */
+    private Long lotteryCount = 0L;
+    /** 14. 参与抽奖人数 */
+    private Long lotteryJoinCount = 0L;
+    /** 15. 中奖人数 */
+    private Long lotteryWinCount = 0L;
+    /** 16. 支付点击人数(已支付订单的用户数) */
+    private Long paidUserCount = 0L;
+    /** 17. 未支付人数(未支付订单的用户数) */
+    private Long unpaidUserCount = 0L;
+    /** 18. 总成交金额 */
+    private BigDecimal totalGmv = BigDecimal.ZERO;
+    /** 19. 商品总销量 */
+    private Long totalProductQty = 0L;
+}

+ 30 - 0
fs-service/src/main/java/com/fs/live/vo/ProductCompareVO.java

@@ -0,0 +1,30 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 商品对比统计VO
+ *
+ * @author fs
+ * @date 2025-03-19
+ */
+@Data
+public class ProductCompareVO {
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 商品名称 */
+    private String productName;
+
+    /** 下单未支付人数 */
+    private Long unpaidUserCount = 0L;
+
+    /** 成交人数 */
+    private Long paidUserCount = 0L;
+
+    /** 成交金额 */
+    private BigDecimal totalGmv = BigDecimal.ZERO;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/newAdv/domain/Lead.java

@@ -127,6 +127,14 @@ public class Lead implements Serializable {
      * 发起进入小程序 1是 0否
      */
     private Integer miniLaunchIndexCount;
+    /**
+     * 进入直播间1是 0否
+     */
+    private Integer enterLive;
+    /**
+     * 是否看课 1是 0否
+     */
+    private Integer completeCourse;
     /**
      * /**
      * 创建时间

+ 1 - 0
fs-service/src/main/java/com/fs/newAdv/domain/Site.java

@@ -147,5 +147,6 @@ public class Site implements Serializable {
      * 更新人
      */
     private String updater;
+    private Long companyId;
 }
 

+ 14 - 0
fs-service/src/main/java/com/fs/newAdv/dto/req/FormSubmitReq.java

@@ -0,0 +1,14 @@
+package com.fs.newAdv.dto.req;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class FormSubmitReq {
+    private String phone;
+    private String name;
+    @NotBlank
+    private String smsCode;
+    private String traceId;
+}

+ 4 - 1
fs-service/src/main/java/com/fs/newAdv/enums/SystemEventTypeEnum.java

@@ -12,7 +12,10 @@ public enum SystemEventTypeEnum {
     BUY_ORDER("event5", "商品购买订单"),
     AUTH_TODAY_CREATE("event6", "微信授权且当日创建"),
     COMPLETE_CLASS_AND_GROUP_TODAY("event7", "直播完课且当日加群"),
-    COMPLETE_CLASS_AND_WEI_CHAT_TODAY("event8", "直播完课且当日加微");
+    COMPLETE_CLASS_AND_WEI_CHAT_TODAY("event8", "直播完课且当日加微"),
+    ENTER_LIVE("event9", "进入直播间"),
+    COMPLETE_COURSE("event10", "看课记录"),
+    ;
 
     private final String code;
     private final String description;

+ 9 - 0
fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java

@@ -37,6 +37,15 @@ public interface ILeadService extends IService<Lead> {
      */
     void updateAddMemberLead(String externalUserID,String userID,String corpId,String State);
 
+    /**
+     * 第一次进入直播间
+     */
+    void enterLive(Long userId, Long liveId);
+
+    /**
+     * 第一次完成看课
+     */
+    void completeCourse(Long userId, Long courseId);
     /**
      * 用户删除企业微信线索处理
      */

+ 56 - 6
fs-service/src/main/java/com/fs/newAdv/service/impl/LeadServiceImpl.java

@@ -5,6 +5,8 @@ import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
 import com.fs.newAdv.domain.Lead;
 import com.fs.newAdv.enums.SystemEventTypeEnum;
 import com.fs.newAdv.event.ConversionEventPublisher;
@@ -39,6 +41,8 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
 
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private IFsUserService fsUserService;
 
     @Override
     public Lead getByTraceId(String traceId) {
@@ -85,7 +89,7 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
         Lead lead = this.getOne(last);
         if (lead != null) {
             lead.setChatId(chatId);
-            lead.setAddContactQw(1);
+            lead.setAddContactQwGroup(1);
             lead.setCorpId(corpId);
             this.updateById(lead);
             if (ObjectUtil.isNotEmpty(lead.getLandingPageTs()) && lead.getLandingPageTs().toLocalDate().isEqual(LocalDate.now())) {
@@ -108,17 +112,62 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
             return;
         }
         qwExternalContact.setUnionid(unionid);
-        this.updateAddMemberLead(qwExternalContact);
+        this.updateAddMemberLead(qwExternalContact, null);
     }
 
     @Override
+    @Async
     public void updateAddMemberLead(String externalUserID, String userID, String corpId, String State) {
         QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
         if (qwExternalContact == null) {
             log.info("外部联系人信息不存在:{} {} {}", externalUserID, userID, corpId);
             return;
         }
-        this.updateAddMemberLead(qwExternalContact);
+        this.updateAddMemberLead(qwExternalContact, State);
+    }
+
+    @Override
+    @Async
+    public void enterLive(Long userId, Long liveId) {
+        FsUser fsUser = fsUserService.selectFsUserById(userId);
+        if (fsUser == null || ObjectUtil.isEmpty(fsUser.getQwExtId())) {
+            return;
+        }
+        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(fsUser.getQwExtId());
+        if (qwExternalContact == null || ObjectUtil.isEmpty(qwExternalContact.getTraceId())) {
+            return;
+        }
+        String traceId = qwExternalContact.getTraceId();
+        Lead lead = this.getByTraceId(traceId);
+        if (lead == null || lead.getEnterLive() == 1) {
+            log.info("用户进入直播间线索已经完成:{}", lead);
+            return;
+        }
+        lead.setEnterLive(1);
+        this.updateById(lead);
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.ENTER_LIVE);
+    }
+
+    @Override
+    @Async
+    public void completeCourse(Long userId, Long course) {
+        FsUser fsUser = fsUserService.selectFsUserById(userId);
+        if (fsUser == null || ObjectUtil.isEmpty(fsUser.getQwExtId())) {
+            return;
+        }
+        QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(fsUser.getQwExtId());
+        if (qwExternalContact == null || ObjectUtil.isEmpty(qwExternalContact.getTraceId())) {
+            return;
+        }
+        String traceId = qwExternalContact.getTraceId();
+        Lead lead = this.getByTraceId(traceId);
+        if (lead == null || lead.getCompleteCourse() == 1) {
+            log.info("用户看课线索已经完成:{}", lead);
+            return;
+        }
+        lead.setCompleteCourse(1);
+        this.updateById(lead);
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.COMPLETE_COURSE);
     }
 
     @Override
@@ -138,8 +187,8 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
 
     }
 
-    private void updateAddMemberLead(QwExternalContact qwExternalContact) {
-        log.info("用户加微线索信息:{}", qwExternalContact);
+    private void updateAddMemberLead(QwExternalContact qwExternalContact, String state) {
+        log.info("用户加微线索信息:{} {}", qwExternalContact,state);
         LambdaQueryWrapper<Lead> last = new LambdaQueryWrapper<Lead>();
         if (StrUtil.isNotEmpty(qwExternalContact.getUnionid())) {
             last.eq(Lead::getUnionid, qwExternalContact.getUnionid());
@@ -149,9 +198,10 @@ public class LeadServiceImpl extends ServiceImpl<LeadMapper, Lead> implements IL
         last.eq(Lead::getAddContactQwGroup, 0).last("LIMIT 1");
         // 末次归因逻辑
         Lead lead = this.getOne(last);
+        lead = lead == null ? this.getByTraceId(state) : lead;
         if (lead != null) {
             lead.setExternalId(qwExternalContact.getId());
-            lead.setAddContactQwGroup(1);
+            lead.setAddContactQw(1);
             this.updateById(lead);
             // 绑定企微用户线索关系
             QwExternalContact temp = new QwExternalContact();

+ 1 - 1
fs-service/src/main/java/com/fs/newAdv/service/impl/SiteServiceImpl.java

@@ -25,7 +25,7 @@ public class SiteServiceImpl extends ServiceImpl<SiteMapper, Site> implements IS
     @Transactional(rollbackFor = Exception.class)
     public void createSite(Site site) {
         this.save(site);
-        site.setSiteUrl("https://" + site.getLaunchDomain() + "/pages/index/index?siteId=" + site.getId());
+        site.setSiteUrl(site.getLaunchDomain() + "/pages/index/index?siteId=" + site.getId());
         this.updateById(site);
     }
 

+ 137 - 0
fs-service/src/main/java/com/fs/qw/domain/QwAcquisitionAssistant.java

@@ -0,0 +1,137 @@
+package com.fs.qw.domain;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企微-获客链接管理对象 qw_acquisition_assistant
+ * 
+ * @author fs
+ * @date 2026-03-16
+ */
+
+@Data
+public class QwAcquisitionAssistant extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 获客链接ID */
+    private String linkId;
+
+    /** 获客链接名称 */
+    private String linkName;
+
+    /** 获客链接URL */
+    private String url;
+
+    /** 获客链接scheme */
+    private String scheme;
+
+    /** 创建时间(企微返回的时间) */
+    private Date qwCreateTime;
+
+    /** 是否无需验证,默认为true */
+    private String skipVerify;
+
+//    /**
+//     * 是否标记客户添加来源为该应用创建的获客链接, 默认值为true; 仅对「营销获客」应用生效
+//     * */
+//    private Boolean markSource=true;
+
+    /** 优先分配类型(0-不启用 1-全企业范围内优先分配给有好友关系的 2-指定范围内优先分配有好友关系的) */
+    private Integer priorityType;
+
+    /** 关联的成员列表,JSON数组格式 */
+    private String userList;
+
+    /** 关联的部门列表,JSON数组格式 */
+    private String departmentList;
+
+    /** 优先分配成员列表,JSON数组格式 */
+    private String priorityUserList;
+
+    /** qw_user表的主键id列表,JSON数组格式 */
+    private String qwUserTableIdList;
+
+    /** 使用范围描述(用于展示) */
+    private String rangeDesc;
+
+    /** 状态(0-已删除 1-正常 2-已失效) */
+    private Integer status;
+
+    /** 最后同步时间 */
+    private Date syncTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 删除标志(0-正常 1-已删除) */
+    private String delFlag;
+
+    // ==================== 非数据库字段,用于辅助操作 ====================
+
+    /** 成员列表(用于接收前端参数) */
+    private List<String> userListParam;
+
+    /** 部门列表(用于接收前端参数) */
+    private List<Long> departmentListParam;
+
+    /** 优先分配成员列表(用于接收前端参数) */
+    private List<String> priorityUserListParam;
+
+    /** 主体corpId */
+    private String corpId;
+
+    /** 页面参数 */
+    private String pageParam;
+
+    /**
+     * 将参数列表转换为JSON字符串
+     */
+    public void buildJsonFields() 
+    {
+        if (userListParam != null) {
+            this.userList = JSON.toJSONString(userListParam);
+        }
+        if (departmentListParam != null) {
+            this.departmentList = JSON.toJSONString(departmentListParam);
+        }
+        if (priorityUserListParam != null) {
+            this.priorityUserList = JSON.toJSONString(priorityUserListParam);
+        }
+    }
+
+    /**
+     * 解析JSON字段到参数列表
+     */
+    public void parseJsonFields() 
+    {
+        if (StringUtils.isNotBlank(this.userList)) {
+            this.userListParam = JSON.parseArray(this.userList, String.class);
+        }
+        if (StringUtils.isNotBlank(this.departmentList)) {
+            this.departmentListParam = JSON.parseArray(this.departmentList, Long.class);
+        }
+        if (StringUtils.isNotBlank(this.priorityUserList)) {
+            this.priorityUserListParam = JSON.parseArray(this.priorityUserList, String.class);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "QwAcquisitionAssistant{" +
+                "id=" + id +
+                ", linkId='" + linkId + '\'' +
+                ", linkName='" + linkName + '\'' +
+                ", status=" + status +
+                '}';
+    }
+}

+ 26 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionBaseRequest.java

@@ -0,0 +1,26 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class AcquisitionBaseRequest {
+    
+    @JSONField(name = "link_id")      // 编辑时必填,创建时不填
+    private String linkId;
+    
+    @JSONField(name = "link_name")    // 创建时必填,编辑时可选
+    private String linkName;
+    
+    private AcquisitionRange range;    // 范围
+    
+    @JSONField(name = "skip_verify")
+    private Boolean skipVerify;
+    
+    @JSONField(name = "priority_option")
+    private AcquisitionPriority priorityOption;
+
+//    标记来源功能仅对「营销获客」应用生效
+//    @JSONField(name = "mark_source")
+//    private Boolean markSource;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionCreateResponse.java

@@ -0,0 +1,27 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+@Data
+public class AcquisitionCreateResponse {
+    
+    private Integer errcode;
+    private String errmsg;
+    
+    private LinkInfo link;  // 创建成功返回链接信息
+    
+    @Data
+    public static class LinkInfo {
+        @JSONField(name = "link_id")
+        private String linkId;
+        
+        @JSONField(name = "link_name")
+        private String linkName;
+        
+        private String url;
+        
+        @JSONField(name = "create_time")
+        private Long createTime;
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionDeleteResponse.java

@@ -0,0 +1,9 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+@Data
+public class AcquisitionDeleteResponse {
+    private Integer errcode;
+    private String errmsg;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetRequest.java

@@ -0,0 +1,15 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+/**
+ * 获取获客链接详情请求DTO
+ */
+@Data
+public class AcquisitionGetRequest {
+    
+    /** 获客链接id */
+    @JSONField(name = "link_id")
+    private String linkId;
+}

+ 63 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionGetResponse.java

@@ -0,0 +1,63 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AcquisitionGetResponse {
+
+    private Integer errcode;
+    private String errmsg;
+
+    @JSONField(name = "link")
+    private LinkDetail link;
+
+    @JSONField(name = "range")
+    private RangeInfo range;
+
+    @JSONField(name = "priority_option")
+    private PriorityInfo priorityOption;
+
+    @Data
+    public static class LinkDetail {
+        @JSONField(name = "link_name")
+        private String linkName;
+
+        private String url;
+
+        @JSONField(name = "create_time")
+        private Long createTime;
+
+        @JSONField(name = "skip_verify")
+        private String skipVerify;
+    }
+
+    @Data
+    public static class RangeInfo {
+        @JSONField(name = "user_list")
+        private List<String> userList;
+
+        @JSONField(name = "department_list")
+        private List<Integer> departmentList;
+
+        // 可以添加构造方法
+        public RangeInfo() {
+        }
+
+        public RangeInfo(List<String> userList, List<Integer> departmentList) {
+            this.userList = userList;
+            this.departmentList = departmentList;
+        }
+    }
+
+    @Data
+    public static class PriorityInfo {
+        @JSONField(name = "priority_type")
+        private Integer priorityType;
+
+        @JSONField(name = "priority_userid_list")
+        private List<String> priorityUseridList;
+    }
+}

+ 16 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListRequest.java

@@ -0,0 +1,16 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+/**
+ * 获取获客链接列表请求DTO(调用企微接口用)
+ */
+@Data
+public class AcquisitionListRequest {
+    
+    /** 返回的最大记录数,最大值100 */
+    private Integer limit;
+    
+    /** 分页游标 */
+    private String cursor;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionListResponse.java

@@ -0,0 +1,19 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AcquisitionListResponse {
+    
+    private Integer errcode;
+    private String errmsg;
+    
+    @JSONField(name = "link_id_list")
+    private List<String> linkIdList;
+    
+    @JSONField(name = "next_cursor")
+    private String nextCursor;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionPriority.java

@@ -0,0 +1,16 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AcquisitionPriority {
+    
+    @JSONField(name = "priority_type")
+    private Integer priorityType;
+    
+    @JSONField(name = "priority_userid_list")
+    private List<String> priorityUseridList;
+}

+ 16 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionRange.java

@@ -0,0 +1,16 @@
+package com.fs.qw.dto.acquisition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AcquisitionRange {
+    
+    @JSONField(name = "user_list")
+    private List<String> userList;
+    
+    @JSONField(name = "department_list")
+    private List<Integer> departmentList;
+}

+ 9 - 0
fs-service/src/main/java/com/fs/qw/dto/acquisition/AcquisitionUpdateResponse.java

@@ -0,0 +1,9 @@
+package com.fs.qw.dto.acquisition;
+
+import lombok.Data;
+
+@Data
+public class AcquisitionUpdateResponse {
+    private Integer errcode;
+    private String errmsg;
+}

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