Bläddra i källkod

Merge remote-tracking branch 'origin/master'

yjwang 3 dagar sedan
förälder
incheckning
3e4e2559ad
100 ändrade filer med 6181 tillägg och 144 borttagningar
  1. 114 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyWithdrawDetailAdminController.java
  2. 129 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmBusinessController.java
  3. 13 7
      fs-admin/src/main/java/com/fs/crm/controller/CrmEventController.java
  4. 100 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmExtDetailController.java
  5. 68 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmExtLogController.java
  6. 45 0
      fs-admin/src/main/java/com/fs/crm/task/CrmTask.java
  7. 8 2
      fs-admin/src/main/java/com/fs/his/controller/EasyCallController.java
  8. 1 1
      fs-admin/src/main/java/com/fs/his/task/Task.java
  9. 5 55
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  10. 4 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  11. 113 0
      fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java
  12. 46 0
      fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java
  13. 25 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java
  14. 47 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java
  15. 181 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java
  16. 62 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java
  17. 55 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java
  18. 192 0
      fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java
  19. 18 0
      fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java
  20. 214 0
      fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java
  21. 114 0
      fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java
  22. 38 0
      fs-admin/src/main/java/com/fs/kdniaoNew/config/KdniaoUniversalConfig.java
  23. 45 0
      fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java
  24. 25 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoAddServiceNew.java
  25. 58 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCarrierConfig.java
  26. 47 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCommodityNew.java
  27. 55 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoPersonNew.java
  28. 240 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoSubmitCommand.java
  29. 21 0
      fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoUniversalResponse.java
  30. 73 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/AbstractKdniaoCarrierRule.java
  31. 22 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/IKdniaoCarrierRule.java
  32. 27 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/DefaultCarrierRule.java
  33. 33 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/EmsCarrierRule.java
  34. 40 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdKyCarrierRule.java
  35. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdsxyyCarrierRule.java
  36. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JosCarrierRule.java
  37. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/SfCarrierRule.java
  38. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoCarrierRule.java
  39. 32 0
      fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoColdCarrierRule.java
  40. 15 0
      fs-admin/src/main/java/com/fs/kdniaoNew/service/IKdniaoUniversalEOrderService.java
  41. 253 0
      fs-admin/src/main/java/com/fs/kdniaoNew/service/impl/KdniaoUniversalEOrderServiceImpl.java
  42. 95 0
      fs-admin/src/main/java/com/fs/kdniaoNew/util/KdniaoRequestUtil.java
  43. 519 0
      fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java
  44. 2 1
      fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  45. 40 0
      fs-company-app/src/main/java/com/fs/app/controller/app/ImController.java
  46. 128 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserShowController.java
  47. 88 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWithdrawDetailController.java
  48. 5 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  49. 44 5
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java
  50. 24 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerVisitController.java
  51. 14 4
      fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java
  52. 4 2
      fs-company/src/main/java/com/fs/company/controller/live/LiveAfterSalesController.java
  53. 8 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  54. 16 1
      fs-company/src/main/java/com/fs/company/controller/live/LiveOrderController.java
  55. 39 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerAnalyzeController.java
  56. 45 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerPropertyController.java
  57. 0 24
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  58. 198 0
      fs-company/src/main/java/com/fs/crm/CrmBusinessController.java
  59. 100 0
      fs-company/src/main/java/com/fs/crm/CrmExtDetailController.java
  60. 68 0
      fs-company/src/main/java/com/fs/crm/CrmExtLogController.java
  61. 83 0
      fs-company/src/main/java/com/fs/crm/CrmFollowUpController.java
  62. 591 0
      fs-company/src/main/java/com/fs/crm/CrmSjCustomerController.java
  63. 1 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  64. 10 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  65. 2 2
      fs-doctor-app/src/main/resources/application.yml
  66. 118 13
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  67. 7 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  68. 23 4
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  69. 20 2
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  70. 110 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUserShow.java
  71. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyAiWorkflowExecLogMapper.java
  72. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyConfigMapper.java
  73. 69 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserShowMapper.java
  74. 17 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWithdrawDetailMapper.java
  75. 25 0
      fs-service/src/main/java/com/fs/company/param/CompanyUserShowEditParam.java
  76. 20 0
      fs-service/src/main/java/com/fs/company/param/CompanyWithdrawDetailAdminParam.java
  77. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyConfigService.java
  78. 1 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  79. 66 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserShowService.java
  80. 24 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWithdrawDetailService.java
  81. 6 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyConfigServiceImpl.java
  82. 58 13
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  83. 9 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  84. 113 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserShowServiceImpl.java
  85. 23 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  86. 64 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWithdrawDetailServiceImpl.java
  87. 7 3
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  88. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java
  89. 90 0
      fs-service/src/main/java/com/fs/company/util/CompanyTuiMoneyCalc.java
  90. 16 0
      fs-service/src/main/java/com/fs/company/vo/CallContentVO.java
  91. 57 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWithdrawDetailVO.java
  92. 11 0
      fs-service/src/main/java/com/fs/company/vo/WorkflowExecRecordVo.java
  93. 7 0
      fs-service/src/main/java/com/fs/config/cloud/CloudHostProper.java
  94. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java
  95. 63 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseReward.java
  96. 79 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardRound.java
  97. 47 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRewardVideoRelation.java
  98. 29 0
      fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java
  99. 75 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardRoundMapper.java
  100. 87 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRewardVideoRelationMapper.java

+ 114 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyWithdrawDetailAdminController.java

@@ -0,0 +1,114 @@
+package com.fs.company.controller;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.param.CompanyParam;
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyNameVO;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.course.config.CourseConfig;
+import com.fs.framework.web.service.TokenService;
+import com.fs.system.service.ISysConfigService;
+import com.github.pagehelper.PageHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 管理后台-提现明细(多公司、类型筛选;与销售端数据源一致)
+ */
+@RestController
+@RequestMapping("/company/withdrawDetailAdmin")
+public class CompanyWithdrawDetailAdminController extends BaseController {
+
+    @Autowired
+    private ICompanyWithdrawDetailService companyWithdrawDetailService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 分公司下拉:关键词模糊匹配公司名称(最多 500 条)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/companyOptions")
+    public AjaxResult companyOptions(@RequestParam(value = "keyword", required = false) String keyword) {
+        CompanyParam param = new CompanyParam();
+        if (StringUtils.isNotEmpty(keyword)) {
+            param.setCompanyName(keyword.trim());
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (!loginUser.isAdmin() && config.getDept() != null && config.getDept()) {
+            param.setDeptId(loginUser.getDeptId());
+        }
+        PageHelper.startPage(1, 500);
+        List<CompanyNameVO> list = companyService.selectCompanyNameVOList(param);
+        return AjaxResult.success(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(
+            @RequestParam(required = false) List<Long> companyIds,
+            @RequestParam(required = false) List<Integer> detailTypes) {
+        PageDomain page = TableSupport.buildPageRequest();
+        int pageNum = page.getPageNum() != null && page.getPageNum() > 0 ? page.getPageNum() : 1;
+        int pageSize = page.getPageSize() != null && page.getPageSize() > 0 ? page.getPageSize() : 10;
+        PageHelper.startPage(pageNum, pageSize);
+
+        CompanyWithdrawDetailAdminParam p = new CompanyWithdrawDetailAdminParam();
+        p.setCompanyIds(companyIds);
+        p.setDetailTypes(detailTypes);
+
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailListAdmin(p);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        long start = (long) (pageNum - 1) * pageSize;
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo((int) (start + i + 1));
+        }
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:export')")
+    @Log(title = "提现明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(
+            @RequestParam(required = false) List<Long> companyIds,
+            @RequestParam(required = false) List<Integer> detailTypes) {
+        CompanyWithdrawDetailAdminParam p = new CompanyWithdrawDetailAdminParam();
+        p.setCompanyIds(companyIds);
+        p.setDetailTypes(detailTypes);
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailListAdmin(p);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo(i + 1);
+        }
+        ExcelUtil<CompanyWithdrawDetailVO> util = new ExcelUtil<>(CompanyWithdrawDetailVO.class);
+        return util.exportExcel(list, "提现明细");
+    }
+}

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 1
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -265,7 +265,7 @@ public class Task {
                     Long sender = jsonObject.getLong("sender");
                     Integer type = jsonObject.getInteger("type");
 
-                    aiHookService.qwHookNotifyAiReply(qwUserId,sender,content,uid,type);
+                    aiHookService.qwHookNotifyAiReply(qwUserId,sender,content,uid,type,false);
                     //删除已经处理的缓存
                     redisCache.hDel(DELAY_MSG,key);
                 }

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

@@ -17,7 +17,6 @@ import com.fs.framework.web.service.TokenService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.service.IFsUserService;
 import com.fs.hisStore.domain.*;
-import com.fs.hisStore.mapper.FsRefundReasonMapper;
 import com.fs.hisStore.param.FsStoreAfterSalesAudit1Param;
 import com.fs.hisStore.param.FsStoreAfterSalesAudit2Param;
 import com.fs.hisStore.param.FsStoreAfterSalesCancelParam;
@@ -37,10 +36,8 @@ import org.springframework.web.bind.annotation.*;
 import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 /**
  * 售后记录Controller
@@ -72,10 +69,6 @@ public class FsStoreAfterSalesScrmController extends BaseController
     @Autowired
     private CloudHostProper cloudHostProper;
 
-
-    @Autowired
-    private FsRefundReasonMapper fsRefundReasonMapper;
-
     /**
      * 查询售后记录列表
      */
@@ -93,28 +86,6 @@ public class FsStoreAfterSalesScrmController extends BaseController
             }
         }
 
-        if(!list.isEmpty()){//不为空获取审核信息
-            List<Long> reasonId = list.stream()
-                    .flatMap(vo -> Stream.of(vo.getReasonId1(), vo.getReasonId2()))
-                    .filter(Objects::nonNull)
-                    .collect(Collectors.toList());
-
-          if(!reasonId.isEmpty()){
-              List<FsRefundReason> refundReasons = fsRefundReasonMapper.getFsRefundReasonList(reasonId);
-              if(!refundReasons.isEmpty()){
-                  Map<Long,String> map = refundReasons.stream().collect(Collectors.toMap(FsRefundReason::getId, FsRefundReason::getReasonName));
-                  list.forEach(vo -> {
-                      if(vo.getReasonId1() != null){
-                          vo.setReasonValue1(map.get(vo.getReasonId1()));
-                      }
-                      if(vo.getReasonId2() != null){
-                          vo.setReasonValue2(map.get(vo.getReasonId2()));
-                      }
-                  });
-              }
-          }
-        }
-
         return getDataTable(list);
     }
 
@@ -137,19 +108,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
 
         List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVOExport(fsStoreAfterSales);
         if("北京卓美".equals(signProjectName)) {
-            Map<Long, String> map = null;
-            if (!list.isEmpty()) {//不为空获取审核信息
-                List<Long> reasonId = list.stream()
-                        .flatMap(vo -> Stream.of(vo.getReasonId1(), vo.getReasonId2()))
-                        .filter(Objects::nonNull)
-                        .collect(Collectors.toList());
-                List<FsRefundReason> refundReasons = fsRefundReasonMapper.getFsRefundReasonList(reasonId);
-                if (!refundReasons.isEmpty()) {
-                    map = refundReasons.stream().collect(Collectors.toMap(FsRefundReason::getId, FsRefundReason::getReasonName));
-                }
-
-                List<Long> finalReasonId = reasonId;
-                Map<Long, String> finalMap = map;
+            if (!list.isEmpty()) {
                 List<FsStoreOrderItemExportRefundZMVO> zmvoList = list.stream()
                         .map(vo -> {
                             FsStoreOrderItemExportRefundZMVO zmvo = new FsStoreOrderItemExportRefundZMVO();
@@ -164,7 +123,6 @@ public class FsStoreAfterSalesScrmController extends BaseController
                                 zmvo.setNum(vo.getNum());
                                 zmvo.setPrice(vo.getPrice());
                                 zmvo.setCost(vo.getCost());
-//                            zmvo.setFPrice("");
                                 zmvo.setPayMoney(vo.getPayMoney());
                                 zmvo.setPayPostage(vo.getTotalPostage());
                                 zmvo.setCateName(vo.getCateName());
@@ -179,22 +137,14 @@ public class FsStoreAfterSalesScrmController extends BaseController
                                 zmvo.setCompanyName(vo.getCompanyName());
                                 zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
                                 zmvo.setRefundTime(vo.getCreateTime());
-//                            zmvo.setAfterSalesNumber
                                 zmvo.setRefundMoney(vo.getRefundAmount());
                                 zmvo.setBankTransactionId(vo.getBankTransactionId());
                                 zmvo.setReasons(vo.getReasons());
                                 zmvo.setExplains(vo.getExplains());
-                                if (vo.getReasonId1() != null) {
-                                    zmvo.setReasonValue1(finalMap.get(vo.getReasonId1()));
-                                }
-                                if (vo.getReasonId2() != null) {
-                                    zmvo.setReasonValue2(finalMap.get(vo.getReasonId2()));
-                                }
-                                if (vo.getAuditRemark() != null) {
-                                    zmvo.setAuditRemark(vo.getAuditRemark());
-                                }
+                                zmvo.setReasonValue1(vo.getReasonValue1());
+                                zmvo.setReasonValue2(vo.getReasonValue2());
+                                zmvo.setAuditRemark(vo.getAuditRemark());
                             } catch (Exception e) {
-                                // 处理异常
                                 e.printStackTrace();
                             }
                             return zmvo;
@@ -221,7 +171,7 @@ public class FsStoreAfterSalesScrmController extends BaseController
     @GetMapping(value = "/{id}")
     public R getInfo(@PathVariable("id") Long id)
     {
-        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesById(id);
+        FsStoreAfterSalesScrm afterSales=fsStoreAfterSalesService.selectFsStoreAfterSalesByIdForDetail(id);
         FsStoreAfterSalesItemScrm map=new FsStoreAfterSalesItemScrm();
         map.setStoreAfterSalesId(id);
         List<FsStoreAfterSalesItemScrm> items=fsStoreAfterSalesItemService.selectFsStoreAfterSalesItemList(map);

+ 4 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -985,6 +985,10 @@ public class FsStoreOrderScrmController extends BaseController {
         if("广州郑多燕".equals(cloudHostProper.getCompanyName())){
             return getDataTable(fsStoreOrderService.selectZDYOrderSaleStatisticsList(param));
         }
+        // 恒春来
+        if("恒春来".equals(cloudHostProper.getCompanyName())){
+            return getDataTable(fsStoreOrderService.selectHCLOrderDimensionStatisticsList(param));
+        }
         return getDataTable(fsStoreOrderService.selectOrderDimensionStatisticsList(param));
     }
 

+ 113 - 0
fs-admin/src/main/java/com/fs/kdniao/config/KdniaoConfig.java

@@ -0,0 +1,113 @@
+package com.fs.kdniao.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 快递鸟配置类
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "kdniao")
+public class KdniaoConfig {
+
+    /**
+     * 快递鸟用户ID
+     */
+    private String eBusinessID;
+
+    /**
+     * 快递鸟API Key
+     */
+    private String apiKey;
+
+    /**
+     * 电子面单接口地址
+     */
+    private String reqURL;
+
+    /**
+     * 默认电子面单账号配置
+     */
+    private Account account;
+
+    /**
+     * 默认发件人配置
+     */
+    private Sender sender;
+
+    /**
+     * 电子面单账号配置
+     */
+    @Data
+    public static class Account {
+
+        /**
+         * 电子面单账号
+         */
+        private String customerName;
+
+        /**
+         * 电子面单密码
+         */
+        private String customerPwd;
+
+        /**
+         * 发件网点编码
+         */
+        private String sendSite;
+
+        /**
+         * 月结号
+         */
+        private String monthCode;
+    }
+
+    /**
+     * 默认发件人配置
+     */
+    @Data
+    public static class Sender {
+
+        /**
+         * 发件公司
+         */
+        private String company;
+
+        /**
+         * 发件人姓名
+         */
+        private String name;
+
+        /**
+         * 发件人手机号
+         */
+        private String mobile;
+
+        /**
+         * 发件省
+         */
+        private String provinceName;
+
+        /**
+         * 发件市
+         */
+        private String cityName;
+
+        /**
+         * 发件区/县
+         */
+        private String expAreaName;
+
+        /**
+         * 发件详细地址
+         */
+        private String address;
+
+        /**
+         * 发件邮编
+         */
+        private String postCode;
+    }
+}

+ 46 - 0
fs-admin/src/main/java/com/fs/kdniao/controller/KdniaoEOrderController.java

@@ -0,0 +1,46 @@
+package com.fs.kdniao.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+import com.fs.kdniao.service.IKdniaoEOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 快递鸟电子面单控制器
+ */
+@RestController
+@RequestMapping("/kdniao/eorder")
+public class KdniaoEOrderController extends BaseController {
+
+    @Autowired
+    private IKdniaoEOrderService kdniaoEOrderService;
+
+    /**
+     * 简化参数下单接口
+     * 前端只需要传常用业务参数,后端自动组装 RequestData
+     */
+    @Log(title = "快递鸟电子面单", businessType = BusinessType.INSERT)
+    @PostMapping("/submit")
+    public AjaxResult submit(@RequestBody KdniaoSimpleOrderRequest request) {
+        try {
+            KdniaoEOrderResponse response = kdniaoEOrderService.submitSimpleOrder(request);
+
+            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
+                return AjaxResult.success("下单成功", response);
+            }
+
+            if ("106".equals(response.getResultCode())) {
+                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
+            }
+
+            return AjaxResult.error("下单失败:" + response.getReason(), response);
+        } catch (Exception e) {
+            return AjaxResult.error("电子面单下单异常:" + e.getMessage());
+        }
+    }
+}

+ 25 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoAddService.java

@@ -0,0 +1,25 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 增值服务(保价、代收货款等)
+ */
+@Data
+public class KdniaoAddService {
+
+    /**
+     * 服务名称(如 COD、INSURE)
+     */
+    private String Name;
+
+    /**
+     * 服务值(金额/参数)
+     */
+    private String Value;
+
+    /**
+     * 客户标识
+     */
+    private String CustomerID;
+}

+ 47 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoCommodity.java

@@ -0,0 +1,47 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 商品信息
+ */
+@Data
+public class KdniaoCommodity {
+
+    /**
+     * 商品名称
+     */
+    private String GoodsName;
+
+    /**
+     * 商品编码
+     */
+    private String GoodsCode;
+
+    /**
+     * 商品数量
+     */
+    private Integer Goodsquantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal GoodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal GoodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String GoodsDesc;
+
+    /**
+     * 商品体积
+     */
+    private BigDecimal GoodsVol;
+}

+ 181 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderRequest.java

@@ -0,0 +1,181 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 快递鸟电子面单下单请求对象(RequestData)
+ */
+@Data
+public class KdniaoEOrderRequest {
+
+    /**
+     * 订单编号(必须唯一)
+     * 不能重复,否则返回106(幂等控制字段)
+     */
+    private String OrderCode;
+
+    /**
+     * 快递公司编码(如 SF、YTO、ZTO、JDKY 等)
+     */
+    private String ShipperCode;
+
+    /**
+     * 电子面单账号(部分快递必填,如顺丰、圆通等)
+     */
+    private String CustomerName;
+
+    /**
+     * 电子面单密码
+     */
+    private String CustomerPwd;
+
+    /**
+     * 发件网点编码
+     */
+    private String SendSite;
+
+    /**
+     * 发件业务员
+     */
+    private String SendStaff;
+
+    /**
+     * 月结账号
+     */
+    private String MonthCode;
+
+    /**
+     * 运费支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     * 4:第三方付(部分公司支持)
+     */
+    private Integer PayType;
+
+    /**
+     * 快递业务类型(不同快递公司不同)
+     */
+    private String ExpType;
+
+    /**
+     * 发件人信息(必填)
+     */
+    private KdniaoPerson Sender;
+
+    /**
+     * 收件人信息(必填)
+     */
+    private KdniaoPerson Receiver;
+
+    /**
+     * 包裹数量(>=1)
+     */
+    private Integer Quantity;
+
+    /**
+     * 总重量(kg)
+     * 京东/快运类必填
+     */
+    private BigDecimal Weight;
+
+    /**
+     * 总体积(m³)
+     * 京东/快运类必填
+     */
+    private BigDecimal Volume;
+
+    /**
+     * 运费(部分到付场景必填)
+     */
+    private BigDecimal Cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal OtherCost;
+
+    /**
+     * 增值服务(保价、代收货款等)
+     */
+    private List<KdniaoAddService> AddService;
+
+    /**
+     * 备注(会打印在面单上)
+     */
+    private String Remark;
+
+    /**
+     * 商品信息(至少一个 GoodsName)
+     */
+    private List<KdniaoCommodity> Commodity;
+
+    /**
+     * 是否返回电子面单模板
+     * 0:否
+     * 1:是
+     */
+    private String IsReturnPrintTemplate;
+
+    /**
+     * 面单模板尺寸(如 130)
+     */
+    private String TemplateSize;
+
+    /**
+     * 自定义打印内容
+     */
+    private String CustomArea;
+
+    /**
+     * 是否订阅轨迹推送
+     * 1:订阅(默认)
+     * 0:不订阅(避免消耗余额)
+     */
+    private String IsSubscribe;
+
+    /**
+     * 自定义回传字段
+     */
+    private String Callback;
+
+    /**
+     * 是否通知快递员上门揽件
+     * 0:通知
+     * 1:不通知
+     */
+    private Integer IsNotice;
+
+    /**
+     * 上门揽件开始时间(格式:yyyy-MM-dd HH:mm:ss)
+     */
+    private String StartDate;
+
+    /**
+     * 上门揽件结束时间
+     */
+    private String EndDate;
+
+    /**
+     * 是否要求签回单
+     * 0:否
+     * 1:是
+     */
+    private Integer IsReturnSignBill;
+
+    /**
+     * 是否发送短信通知
+     * 0:否
+     * 1:是
+     */
+    private Integer IsSendMessage;
+
+    /**
+     * 币种(顺丰港澳台必填)
+     * CNY / HKD / NTD
+     */
+    private String CurrencyCode;
+}

+ 62 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoEOrderResponse.java

@@ -0,0 +1,62 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 快递鸟电子面单返回对象
+ */
+@Data
+public class KdniaoEOrderResponse {
+
+    /**
+     * 用户ID
+     */
+    private String EBusinessID;
+
+    /**
+     * 是否成功
+     */
+    private Boolean Success;
+
+    /**
+     * 返回编码
+     */
+    private String ResultCode;
+
+    /**
+     * 返回原因
+     */
+    private String Reason;
+
+    /**
+     * 订单信息
+     */
+    private Order Order;
+
+    /**
+     * 面单模板
+     */
+    private String PrintTemplate;
+
+    /**
+     * 运单信息
+     */
+    @Data
+    public static class Order {
+
+        /**
+         * 订单编号
+         */
+        private String OrderCode;
+
+        /**
+         * 快递公司编码
+         */
+        private String ShipperCode;
+
+        /**
+         * 运单号
+         */
+        private String LogisticCode;
+    }
+}

+ 55 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoPerson.java

@@ -0,0 +1,55 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+/**
+ * 发件人 / 收件人信息
+ */
+@Data
+public class KdniaoPerson {
+
+    /**
+     * 公司名称
+     */
+    private String Company;
+
+    /**
+     * 姓名
+     */
+    private String Name;
+
+    /**
+     * 电话
+     */
+    private String Tel;
+
+    /**
+     * 手机号
+     */
+    private String Mobile;
+
+    /**
+     * 省
+     */
+    private String ProvinceName;
+
+    /**
+     * 市
+     */
+    private String CityName;
+
+    /**
+     * 区/县
+     */
+    private String ExpAreaName;
+
+    /**
+     * 详细地址
+     */
+    private String Address;
+
+    /**
+     * 邮编
+     */
+    private String PostCode;
+}

+ 192 - 0
fs-admin/src/main/java/com/fs/kdniao/domain/KdniaoSimpleOrderRequest.java

@@ -0,0 +1,192 @@
+package com.fs.kdniao.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 前端简化下单请求对象
+ */
+@Data
+public class KdniaoSimpleOrderRequest {
+
+    /**
+     * 业务订单号
+     * 传系统自己的订单号
+     */
+    private String bizOrderNo;
+
+    /**
+     * 快递公司编码,如 SF、YTO、ZTO、JDKY、EMS
+     */
+    private String shipperCode;
+
+    /**
+     * 运费支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     */
+    private Integer payType;
+
+    /**
+     * 快递业务类型
+     * 常见值:1
+     */
+    private String expType;
+
+    /**
+     * 收件人姓名
+     */
+    private String receiverName;
+
+    /**
+     * 收件人手机号
+     */
+    private String receiverMobile;
+
+    /**
+     * 收件人电话
+     */
+    private String receiverTel;
+
+    /**
+     * 收件省
+     */
+    private String receiverProvinceName;
+
+    /**
+     * 收件市
+     */
+    private String receiverCityName;
+
+    /**
+     * 收件区/县
+     */
+    private String receiverExpAreaName;
+
+    /**
+     * 收件详细地址
+     * 不要包含省市区
+     */
+    private String receiverAddress;
+
+    /**
+     * 收件邮编
+     * EMS 场景建议传
+     */
+    private String receiverPostCode;
+
+    /**
+     * 商品名称
+     * 建议传类别,如:文件、衣服、电子产品
+     */
+    private String goodsName;
+
+    /**
+     * 商品数量
+     */
+    private Integer goodsQuantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal goodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal goodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String goodsDesc;
+
+    /**
+     * 包裹数量
+     */
+    private Integer quantity;
+
+    /**
+     * 总重量(kg)
+     */
+    private BigDecimal weight;
+
+    /**
+     * 总体积(m³)
+     */
+    private BigDecimal volume;
+
+    /**
+     * 运费
+     */
+    private BigDecimal cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal otherCost;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否返回面单模板
+     * 0:否
+     * 1:是
+     */
+    private String isReturnPrintTemplate;
+
+    /**
+     * 面单模板尺寸
+     */
+    private String templateSize;
+
+    /**
+     * 是否订阅轨迹推送
+     * 1:订阅
+     * 0:不订阅
+     */
+    private String isSubscribe;
+
+    /**
+     * 是否通知上门取件
+     * 0:通知
+     * 1:不通知
+     */
+    private Integer isNotice;
+
+    /**
+     * 上门取件开始时间
+     * 格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startDate;
+
+    /**
+     * 上门取件结束时间
+     */
+    private String endDate;
+
+    /**
+     * 是否要求签回单
+     * 0:否
+     * 1:是
+     */
+    private Integer isReturnSignBill;
+
+    /**
+     * 是否发送短信
+     * 0:否
+     * 1:是
+     */
+    private Integer isSendMessage;
+
+    /**
+     * 币种
+     * 特殊场景需要
+     */
+    private String currencyCode;
+}

+ 18 - 0
fs-admin/src/main/java/com/fs/kdniao/service/IKdniaoEOrderService.java

@@ -0,0 +1,18 @@
+package com.fs.kdniao.service;
+
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+
+/**
+ * 快递鸟电子面单业务接口
+ */
+public interface IKdniaoEOrderService {
+
+    /**
+     * 前端简化参数下单
+     *
+     * @param request 简化请求参数
+     * @return 下单结果
+     */
+    KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request);
+}

+ 214 - 0
fs-admin/src/main/java/com/fs/kdniao/service/impl/KdniaoEOrderServiceImpl.java

@@ -0,0 +1,214 @@
+package com.fs.kdniao.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniao.config.KdniaoConfig;
+import com.fs.kdniao.domain.KdniaoCommodity;
+import com.fs.kdniao.domain.KdniaoEOrderRequest;
+import com.fs.kdniao.domain.KdniaoEOrderResponse;
+import com.fs.kdniao.domain.KdniaoPerson;
+import com.fs.kdniao.domain.KdniaoSimpleOrderRequest;
+import com.fs.kdniao.service.IKdniaoEOrderService;
+import com.fs.kdniao.util.KdniaoUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.net.URLEncoder;
+import java.util.Collections;
+
+/**
+ * 快递鸟电子面单业务实现类
+ */
+@Service
+public class KdniaoEOrderServiceImpl implements IKdniaoEOrderService {
+
+    @Autowired
+    private KdniaoConfig kdniaoConfig;
+
+    /**
+     * 前端简化参数下单
+     */
+    @Override
+    public KdniaoEOrderResponse submitSimpleOrder(KdniaoSimpleOrderRequest request) {
+        validateRequest(request);
+
+        KdniaoEOrderRequest eOrderRequest = buildEOrderRequest(request);
+
+        String requestData = KdniaoUtil.toRequestDataJson(eOrderRequest);
+        String dataSign = KdniaoUtil.getDataSign(requestData, kdniaoConfig.getApiKey());
+
+        try {
+            String formData = buildFormData(requestData, dataSign);
+            String result = KdniaoUtil.doPost(kdniaoConfig.getReqURL(), formData);
+            return JSON.parseObject(result, KdniaoEOrderResponse.class);
+        } catch (Exception e) {
+            throw new RuntimeException("电子面单下单失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 组装快递鸟标准请求对象
+     */
+    private KdniaoEOrderRequest buildEOrderRequest(KdniaoSimpleOrderRequest request) {
+        KdniaoEOrderRequest eOrderRequest = new KdniaoEOrderRequest();
+
+        // 基础字段
+        eOrderRequest.setOrderCode(buildOrderCode(request.getBizOrderNo()));
+        eOrderRequest.setShipperCode(request.getShipperCode());
+        eOrderRequest.setCustomerName(kdniaoConfig.getAccount().getCustomerName());
+        eOrderRequest.setCustomerPwd(kdniaoConfig.getAccount().getCustomerPwd());
+        eOrderRequest.setSendSite(kdniaoConfig.getAccount().getSendSite());
+        eOrderRequest.setMonthCode(kdniaoConfig.getAccount().getMonthCode());
+        eOrderRequest.setPayType(request.getPayType());
+        eOrderRequest.setExpType(request.getExpType());
+
+        // 发件人
+        eOrderRequest.setSender(buildSender());
+
+        // 收件人
+        eOrderRequest.setReceiver(buildReceiver(request));
+
+        // 商品
+        eOrderRequest.setCommodity(Collections.singletonList(buildCommodity(request)));
+
+        // 包裹信息
+        eOrderRequest.setQuantity(request.getQuantity() == null ? 1 : request.getQuantity());
+        eOrderRequest.setWeight(request.getWeight());
+        eOrderRequest.setVolume(request.getVolume());
+        eOrderRequest.setCost(request.getCost());
+        eOrderRequest.setOtherCost(request.getOtherCost());
+
+        // 打印和备注
+        eOrderRequest.setRemark(trimToNull(request.getRemark()));
+        eOrderRequest.setIsReturnPrintTemplate(StringUtils.hasText(request.getIsReturnPrintTemplate()) ? request.getIsReturnPrintTemplate() : "1");
+        eOrderRequest.setTemplateSize(StringUtils.hasText(request.getTemplateSize()) ? request.getTemplateSize() : "130");
+        eOrderRequest.setIsSubscribe(StringUtils.hasText(request.getIsSubscribe()) ? request.getIsSubscribe() : "0");
+
+        // 上门取件
+        eOrderRequest.setIsNotice(request.getIsNotice());
+        eOrderRequest.setStartDate(trimToNull(request.getStartDate()));
+        eOrderRequest.setEndDate(trimToNull(request.getEndDate()));
+
+        // 其他
+        eOrderRequest.setIsReturnSignBill(request.getIsReturnSignBill());
+        eOrderRequest.setIsSendMessage(request.getIsSendMessage());
+        eOrderRequest.setCurrencyCode(trimToNull(request.getCurrencyCode()));
+
+        return eOrderRequest;
+    }
+
+    /**
+     * 构建订单号
+     * 规则:业务订单号 + 时间戳,保证唯一
+     */
+    private String buildOrderCode(String bizOrderNo) {
+        if (StringUtils.hasText(bizOrderNo)) {
+            return bizOrderNo.trim() + "-" + System.currentTimeMillis();
+        }
+        return "KD" + System.currentTimeMillis();
+    }
+
+    /**
+     * 组装默认发件人
+     */
+    private KdniaoPerson buildSender() {
+        KdniaoPerson sender = new KdniaoPerson();
+        sender.setCompany(trimToNull(kdniaoConfig.getSender().getCompany()));
+        sender.setName(kdniaoConfig.getSender().getName());
+        sender.setMobile(kdniaoConfig.getSender().getMobile());
+        sender.setProvinceName(kdniaoConfig.getSender().getProvinceName());
+        sender.setCityName(kdniaoConfig.getSender().getCityName());
+        sender.setExpAreaName(kdniaoConfig.getSender().getExpAreaName());
+        sender.setAddress(kdniaoConfig.getSender().getAddress());
+        sender.setPostCode(trimToNull(kdniaoConfig.getSender().getPostCode()));
+        return sender;
+    }
+
+    /**
+     * 组装收件人
+     */
+    private KdniaoPerson buildReceiver(KdniaoSimpleOrderRequest request) {
+        KdniaoPerson receiver = new KdniaoPerson();
+        receiver.setName(request.getReceiverName());
+        receiver.setMobile(trimToNull(request.getReceiverMobile()));
+        receiver.setTel(trimToNull(request.getReceiverTel()));
+        receiver.setProvinceName(request.getReceiverProvinceName());
+        receiver.setCityName(request.getReceiverCityName());
+        receiver.setExpAreaName(request.getReceiverExpAreaName());
+        receiver.setAddress(request.getReceiverAddress());
+        receiver.setPostCode(trimToNull(request.getReceiverPostCode()));
+        return receiver;
+    }
+
+    /**
+     * 组装商品信息
+     */
+    private KdniaoCommodity buildCommodity(KdniaoSimpleOrderRequest request) {
+        KdniaoCommodity commodity = new KdniaoCommodity();
+        commodity.setGoodsName(request.getGoodsName());
+        commodity.setGoodsquantity(request.getGoodsQuantity() == null ? 1 : request.getGoodsQuantity());
+        commodity.setGoodsPrice(request.getGoodsPrice());
+        commodity.setGoodsWeight(request.getGoodsWeight());
+        commodity.setGoodsDesc(trimToNull(request.getGoodsDesc()));
+        return commodity;
+    }
+
+    /**
+     * 构建表单请求参数
+     */
+    private String buildFormData(String requestData, String dataSign) throws Exception {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RequestData=").append(URLEncoder.encode(requestData, "UTF-8"));
+        sb.append("&EBusinessID=").append(URLEncoder.encode(kdniaoConfig.getEBusinessID(), "UTF-8"));
+        sb.append("&RequestType=").append(URLEncoder.encode("1007", "UTF-8"));
+        sb.append("&DataSign=").append(dataSign);
+        sb.append("&DataType=").append(URLEncoder.encode("2", "UTF-8"));
+        return sb.toString();
+    }
+
+    /**
+     * 前端简化参数校验
+     */
+    private void validateRequest(KdniaoSimpleOrderRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (!StringUtils.hasText(request.getShipperCode())) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+        if (request.getPayType() == null) {
+            throw new IllegalArgumentException("payType不能为空");
+        }
+        if (!StringUtils.hasText(request.getExpType())) {
+            throw new IllegalArgumentException("expType不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverName())) {
+            throw new IllegalArgumentException("receiverName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverMobile()) && !StringUtils.hasText(request.getReceiverTel())) {
+            throw new IllegalArgumentException("receiverMobile和receiverTel至少填写一个");
+        }
+        if (!StringUtils.hasText(request.getReceiverProvinceName())) {
+            throw new IllegalArgumentException("receiverProvinceName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverCityName())) {
+            throw new IllegalArgumentException("receiverCityName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverExpAreaName())) {
+            throw new IllegalArgumentException("receiverExpAreaName不能为空");
+        }
+        if (!StringUtils.hasText(request.getReceiverAddress())) {
+            throw new IllegalArgumentException("receiverAddress不能为空");
+        }
+        if (!StringUtils.hasText(request.getGoodsName())) {
+            throw new IllegalArgumentException("goodsName不能为空");
+        }
+    }
+
+    /**
+     * 去除空白,空字符串转 null
+     */
+    private String trimToNull(String value) {
+        return StringUtils.hasText(value) ? value.trim() : null;
+    }
+}

+ 114 - 0
fs-admin/src/main/java/com/fs/kdniao/util/KdniaoUtil.java

@@ -0,0 +1,114 @@
+package com.fs.kdniao.util;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniao.domain.KdniaoEOrderRequest;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * 快递鸟工具类
+ */
+public class KdniaoUtil {
+
+    private KdniaoUtil() {
+    }
+
+    /**
+     * 将标准请求对象转成 RequestData JSON 字符串
+     */
+    public static String toRequestDataJson(KdniaoEOrderRequest request) {
+        return JSON.toJSONString(request);
+    }
+
+    /**
+     * 生成 DataSign
+     * 规则:Base64(MD5(RequestData + ApiKey))
+     */
+    public static String getDataSign(String requestData, String apiKey) {
+        try {
+            String md5Result = md5(requestData + apiKey);
+            String base64 = Base64.getEncoder().encodeToString(md5Result.getBytes("UTF-8"));
+            return URLEncoder.encode(base64, "UTF-8");
+        } catch (Exception e) {
+            throw new RuntimeException("生成DataSign失败", e);
+        }
+    }
+
+    /**
+     * POST 表单请求
+     */
+    public static String doPost(String reqURL, String formData) {
+        HttpURLConnection connection = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        try {
+            URL url = new URL(reqURL);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(20000);
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            connection.setUseCaches(false);
+            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+
+            os = connection.getOutputStream();
+            os.write(formData.getBytes("UTF-8"));
+            os.flush();
+
+            br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟接口失败", e);
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+            } catch (Exception ignored) {
+            }
+            try {
+                if (br != null) {
+                    br.close();
+                }
+            } catch (Exception ignored) {
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * MD5
+     */
+    private static String md5(String text) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] digest = md.digest(text.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                String hex = Integer.toHexString(b & 0xff);
+                if (hex.length() == 1) {
+                    sb.append("0");
+                }
+                sb.append(hex);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("MD5计算失败", e);
+        }
+    }
+}

+ 38 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/config/KdniaoUniversalConfig.java

@@ -0,0 +1,38 @@
+package com.fs.kdniaoNew.config;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 快递鸟统一配置
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "kdniao")
+public class KdniaoUniversalConfig {
+
+    /**
+     * 快递鸟用户ID
+     */
+    private String eBusinessId;
+
+    /**
+     * 快递鸟API Key
+     */
+    private String apiKey;
+
+    /**
+     * 请求地址
+     */
+    private String reqUrl;
+
+    /**
+     * 各快递公司独立配置
+     */
+    private Map<String, KdniaoCarrierConfig> carriers = new HashMap<>();
+}

+ 45 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/controller/KdniaoUniversalEOrderController.java

@@ -0,0 +1,45 @@
+package com.fs.kdniaoNew.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.domain.KdniaoUniversalResponse;
+import com.fs.kdniaoNew.service.IKdniaoUniversalEOrderService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 快递鸟统一电子面单控制器
+ */
+@RestController
+@RequestMapping("/kdniao/universal/eorder")
+public class KdniaoUniversalEOrderController extends BaseController {
+
+    @Autowired
+    private IKdniaoUniversalEOrderService kdniaoUniversalEOrderService;
+
+    /**
+     * 统一下单
+     */
+    @Log(title = "快递鸟统一电子面单", businessType = BusinessType.INSERT)
+    @PostMapping("/submit")
+    public AjaxResult submit(@RequestBody KdniaoSubmitCommand command) {
+        try {
+            KdniaoUniversalResponse response = kdniaoUniversalEOrderService.submit(command);
+
+            if (Boolean.TRUE.equals(response.getSuccess()) && "100".equals(response.getResultCode())) {
+                return AjaxResult.success("下单成功", response);
+            }
+
+            if ("106".equals(response.getResultCode())) {
+                return AjaxResult.error("订单号重复,快递鸟返回:该订单号已下单成功");
+            }
+
+            return AjaxResult.error("下单失败:" + response.getReason(), response);
+        } catch (Exception e) {
+            return AjaxResult.error("下单异常:" + e.getMessage());
+        }
+    }
+}

+ 25 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoAddServiceNew.java

@@ -0,0 +1,25 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+/**
+ * 增值服务
+ */
+@Data
+public class KdniaoAddServiceNew {
+
+    /**
+     * 服务名称
+     */
+    private String name;
+
+    /**
+     * 服务值
+     */
+    private Object value;
+
+    /**
+     * 客户标识
+     */
+    private String customerId;
+}

+ 58 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCarrierConfig.java

@@ -0,0 +1,58 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 单个快递公司的账号配置
+ */
+@Data
+public class KdniaoCarrierConfig {
+
+    /**
+     * 电子面单账号
+     */
+    private String customerName;
+
+    /**
+     * 电子面单密码
+     */
+    private String customerPwd;
+
+    /**
+     * 发件网点编码
+     */
+    private String sendSite;
+
+    /**
+     * 发件业务员
+     */
+    private String sendStaff;
+
+    /**
+     * 月结号
+     */
+    private String monthCode;
+
+    /**
+     * 仓库编码 / 业务员编码
+     */
+    private String wareHouseId;
+
+    /**
+     * 会员ID
+     */
+    private String memberId;
+
+    /**
+     * 当前快递专属发件人
+     */
+    private KdniaoPersonNew sender;
+
+    /**
+     * 当前快递扩展字段
+     */
+    private Map<String, Object> extras = new HashMap<>();
+}

+ 47 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoCommodityNew.java

@@ -0,0 +1,47 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 商品信息
+ */
+@Data
+public class KdniaoCommodityNew {
+
+    /**
+     * 商品名称
+     */
+    private String goodsName;
+
+    /**
+     * 商品编码
+     */
+    private String goodsCode;
+
+    /**
+     * 商品数量
+     */
+    private Integer goodsQuantity;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal goodsPrice;
+
+    /**
+     * 商品重量
+     */
+    private BigDecimal goodsWeight;
+
+    /**
+     * 商品描述
+     */
+    private String goodsDesc;
+
+    /**
+     * 商品体积
+     */
+    private BigDecimal goodsVol;
+}

+ 55 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoPersonNew.java

@@ -0,0 +1,55 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+/**
+ * 发件人/收件人
+ */
+@Data
+public class KdniaoPersonNew {
+
+    /**
+     * 公司名称
+     */
+    private String company;
+
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 电话
+     */
+    private String tel;
+
+    /**
+     * 手机号
+     */
+    private String mobile;
+
+    /**
+     * 省
+     */
+    private String provinceName;
+
+    /**
+     * 市
+     */
+    private String cityName;
+
+    /**
+     * 区/县
+     */
+    private String expAreaName;
+
+    /**
+     * 详细地址
+     */
+    private String address;
+
+    /**
+     * 邮编
+     */
+    private String postCode;
+}

+ 240 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoSubmitCommand.java

@@ -0,0 +1,240 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 快递鸟统一下单入参
+ * 设计说明:
+ * 1. 公共字段:所有快递共用
+ * 2. 专属字段:每个快递有·一个扩展对象
+ */
+@Data
+public class KdniaoSubmitCommand {
+
+    /**
+     * 业务订单号
+     */
+    private String bizOrderNo;
+
+    /**
+     * 快递公司编码
+     * 例如:SF、EMS、JDKY、JOS、JDSXYY、ZTO、ZTOCOLD
+     */
+    private String shipperCode;
+
+    /**
+     * 支付方式
+     * 1:现付
+     * 2:到付
+     * 3:月结
+     */
+    private Integer payType;
+
+    /**
+     * 业务类型
+     */
+    private String expType;
+
+    /**
+     * 收件人
+     */
+    private KdniaoPersonNew receiver;
+
+    /**
+     * 商品列表
+     */
+    private List<KdniaoCommodityNew> commodity;
+
+    /**
+     * 包裹数量
+     */
+    private Integer quantity;
+
+    /**
+     * 总重量
+     */
+    private BigDecimal weight;
+
+    /**
+     * 总体积
+     */
+    private BigDecimal volume;
+
+    /**
+     * 运费
+     */
+    private BigDecimal cost;
+
+    /**
+     * 其他费用
+     */
+    private BigDecimal otherCost;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否返回电子面单模板
+     * 0:否
+     * 1:是
+     */
+    private String isReturnPrintTemplate;
+
+    /**
+     * 模板尺寸
+     */
+    private String templateSize;
+
+    /**
+     * 是否订阅轨迹
+     * 0:否
+     * 1:是
+     */
+    private String isSubscribe;
+
+    /**
+     * 是否通知上门取件
+     * 某些快递需要
+     */
+    private Integer isNotice;
+
+    /**
+     * 上门取件开始时间
+     */
+    private String startDate;
+
+    /**
+     * 上门取件结束时间
+     */
+    private String endDate;
+
+    /**
+     * 增值服务
+     */
+    private List<KdniaoAddServiceNew> addService;
+
+    /**
+     * 顺丰专属参数
+     */
+    private SfExt sf;
+
+    /**
+     * EMS专属参数
+     * 当前先预留,后续扩展
+     */
+    private EmsExt ems;
+
+    /**
+     * 京东快运专属参数
+     */
+    private JdKyExt jdky;
+
+    /**
+     * 京东快递专属参数
+     */
+    private JosExt jos;
+
+    /**
+     * 京东生鲜医药专属参数
+     */
+    private JdsxyyExt jdsxyy;
+
+    /**
+     * 中通专属参数
+     */
+    private ZtoExt zto;
+
+    /**
+     * 中通冷链专属参数
+     */
+    private ZtoColdExt ztoCold;
+
+    // ================================== 不同快递专属扩展对象 ==================================
+
+    /**
+     * 顺丰专属参数
+     */
+    @Data
+    public static class SfExt {
+        /**
+         * 币种
+         * 例如:CNY、HKD、NTD、MOP
+         */
+        private String currencyCode;
+    }
+
+    /**
+     * EMS专属参数
+     */
+    @Data
+    public static class EmsExt {
+        /**
+         * 当前先预留,后续 EMS 有新增前端字段时可放这里
+         */
+        private String reserved;
+    }
+
+
+    /**
+     * 京东快运专属参数
+     */
+    @Data
+    public static class JdKyExt {
+        /**
+         * 配送方式
+         */
+        private Integer deliveryMethod;
+    }
+
+
+    /**
+     * 京东快递专属参数
+     */
+    @Data
+    public static class JosExt {
+        /**
+         * 运输类型
+         */
+        private String transType;
+    }
+
+
+    /**
+     * 京东生鲜医药专属参数
+     */
+    @Data
+    public static class JdsxyyExt {
+        /**
+         * 运输类型
+         */
+        private String transType;
+    }
+
+    /**
+     * 中通专属参数(expType 为 21,22,23 时必填)
+     */
+    @Data
+    public static class ZtoExt {
+        /**
+         * 操作指令
+         */
+        private String sendSite  ;
+    }
+
+    /**
+     * 中通冷链专属参数
+     */
+    @Data
+    public static class ZtoColdExt {
+
+        /**
+         * 运输类型
+         */
+        private String transportType;
+    }
+}

+ 21 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/domain/KdniaoUniversalResponse.java

@@ -0,0 +1,21 @@
+package com.fs.kdniaoNew.domain;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 快递鸟统一返回对象
+ */
+@Data
+public class KdniaoUniversalResponse {
+
+    private String EBusinessID;
+    private Boolean Success;
+    private String ResultCode;
+    private String Reason;
+    private Map<String, Object> Order;
+    private String PrintTemplate;
+    private String UniquerRequestNumber;
+    private Integer SubCount;
+}

+ 73 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/AbstractKdniaoCarrierRule.java

@@ -0,0 +1,73 @@
+package com.fs.kdniaoNew.rule;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * 快递规则抽象基类
+ */
+public abstract class AbstractKdniaoCarrierRule implements IKdniaoCarrierRule {
+
+    /**
+     * 有值才放入Map
+     */
+    protected void putIfHasText(Map<String, Object> map, String key, String value) {
+        if (StringUtils.hasText(value)) {
+            map.put(key, value.trim());
+        }
+    }
+
+    /**
+     * 不为空才放入Map
+     */
+    protected void putIfNotNull(Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+
+    /**
+     * 条件校验
+     */
+    protected void require(boolean condition, String message) {
+        if (!condition) {
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    /**
+     * 要求文本非空
+     */
+    protected void requireText(String value, String message) {
+        require(StringUtils.hasText(value), message);
+    }
+
+    /**
+     * 要求数字大于0
+     */
+    protected void requirePositive(BigDecimal value, String message) {
+        require(value != null && value.compareTo(BigDecimal.ZERO) > 0, message);
+    }
+
+    /**
+     * 填充账号字段
+     */
+    protected void fillAccountFields(KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        putIfHasText(requestData, "CustomerName", config.getCustomerName());
+        putIfHasText(requestData, "CustomerPwd", config.getCustomerPwd());
+        putIfHasText(requestData, "SendSite", config.getSendSite());
+        putIfHasText(requestData, "SendStaff", config.getSendStaff());
+        putIfHasText(requestData, "MonthCode", config.getMonthCode());
+        putIfHasText(requestData, "WareHouseID", config.getWareHouseId());
+        putIfHasText(requestData, "MemberID", config.getMemberId());
+
+        if (config.getExtras() != null && !config.getExtras().isEmpty()) {
+            requestData.putAll(config.getExtras());
+        }
+    }
+
+}

+ 22 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/IKdniaoCarrierRule.java

@@ -0,0 +1,22 @@
+package com.fs.kdniaoNew.rule;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+
+import java.util.Map;
+
+/**
+ * 快递规则接口
+ */
+public interface IKdniaoCarrierRule {
+
+    /**
+     * 当前规则是否支持该快递
+     */
+    boolean supports(String shipperCode);
+
+    /**
+     * 应用当前快递规则
+     */
+    void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData);
+}

+ 27 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/DefaultCarrierRule.java

@@ -0,0 +1,27 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 默认规则
+ */
+@Component
+public class DefaultCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return true;
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "当前快递未配置账号信息");
+        requireText(config.getCustomerName(), "当前快递要求customerName不能为空");
+        fillAccountFields(config, requestData);
+    }
+}

+ 33 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/EmsCarrierRule.java

@@ -0,0 +1,33 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+
+/**
+ * EMS / 邮政规则
+ */
+@Component
+public class EmsCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "EMS".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "EMS未配置账号信息");
+        requireText(config.getCustomerName(), "EMS要求customerName不能为空");
+        require(config.getSender() != null, "EMS要求发件人配置不能为空");
+        requireText(config.getSender().getPostCode(), "EMS要求发件人postCode不能为空");
+        require(command.getReceiver() != null && StringUtils.hasText(command.getReceiver().getPostCode()),
+                "EMS要求收件人postCode不能为空");
+
+        fillAccountFields(config, requestData);
+    }
+}

+ 40 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdKyCarrierRule.java

@@ -0,0 +1,40 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东快运规则
+ */
+@Component
+public class JdKyCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JDKY".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东快运未配置账号信息");
+        requireText(config.getCustomerName(), "京东快运要求customerName不能为空");
+        requirePositive(command.getWeight(), "京东快运要求weight必填且大于0");
+        requirePositive(command.getVolume(), "京东快运要求volume必填且大于0");
+        require(command.getIsNotice() != null, "京东快运要求isNotice必填");
+
+        if (command.getIsNotice() == 0) {
+            requireText(command.getStartDate(), "京东快运在isNotice=0时要求startDate必填");
+            requireText(command.getEndDate(), "京东快运在isNotice=0时要求endDate必填");
+        }
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJdky() != null) {
+            putIfNotNull(requestData, "DeliveryMethod", command.getJdky().getDeliveryMethod());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JdsxyyCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东生鲜医药规则
+ */
+@Component
+public class JdsxyyCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JDSXYY".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东生鲜医药未配置账号信息");
+        requireText(config.getCustomerName(), "京东生鲜医药要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJdsxyy() != null) {
+            putIfHasText(requestData, "TransType", command.getJdsxyy().getTransType());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/JosCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 京东快递规则
+ */
+@Component
+public class JosCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "JOS".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "京东快递未配置账号信息");
+        requireText(config.getCustomerName(), "京东快递要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getJos() != null) {
+            putIfHasText(requestData, "TransType", command.getJos().getTransType());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/SfCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 顺丰规则
+ */
+@Component
+public class SfCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "SF".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "顺丰未配置账号信息");
+        requireText(config.getMonthCode(), "顺丰要求monthCode不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getSf() != null) {
+            putIfHasText(requestData, "CurrencyCode", command.getSf().getCurrencyCode());
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 中通快递规则
+ */
+@Component
+public class ZtoCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "ZTO".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "中通未配置账号信息");
+        requireText(config.getCustomerName(), "中通要求customerName不能为空");
+        requireText(config.getCustomerPwd(), "中通要求customerPwd不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getZto() != null) {
+        }
+    }
+}

+ 32 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/rule/impl/ZtoColdCarrierRule.java

@@ -0,0 +1,32 @@
+package com.fs.kdniaoNew.rule.impl;
+
+import com.fs.kdniaoNew.domain.KdniaoCarrierConfig;
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.rule.AbstractKdniaoCarrierRule;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 中通冷链规则
+ */
+@Component
+public class ZtoColdCarrierRule extends AbstractKdniaoCarrierRule {
+
+    @Override
+    public boolean supports(String shipperCode) {
+        return "ZTOCOLD".equalsIgnoreCase(shipperCode);
+    }
+
+    @Override
+    public void apply(KdniaoSubmitCommand command, KdniaoCarrierConfig config, Map<String, Object> requestData) {
+        require(config != null, "中通冷链未配置账号信息");
+        requireText(config.getCustomerName(), "中通冷链要求customerName不能为空");
+
+        fillAccountFields(config, requestData);
+
+        if (command.getZtoCold() != null) {
+            putIfHasText(requestData, "TransportType", command.getZtoCold().getTransportType());
+        }
+    }
+}

+ 15 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/service/IKdniaoUniversalEOrderService.java

@@ -0,0 +1,15 @@
+package com.fs.kdniaoNew.service;
+
+import com.fs.kdniaoNew.domain.KdniaoSubmitCommand;
+import com.fs.kdniaoNew.domain.KdniaoUniversalResponse;
+
+/**
+ * 快递鸟统一电子面单服务
+ */
+public interface IKdniaoUniversalEOrderService {
+
+    /**
+     * 统一下单
+     */
+    KdniaoUniversalResponse submit(KdniaoSubmitCommand command);
+}

+ 253 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/service/impl/KdniaoUniversalEOrderServiceImpl.java

@@ -0,0 +1,253 @@
+package com.fs.kdniaoNew.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.kdniaoNew.config.KdniaoUniversalConfig;
+import com.fs.kdniaoNew.domain.*;
+import com.fs.kdniaoNew.rule.IKdniaoCarrierRule;
+import com.fs.kdniaoNew.rule.impl.DefaultCarrierRule;
+import com.fs.kdniaoNew.service.IKdniaoUniversalEOrderService;
+import com.fs.kdniaoNew.util.KdniaoRequestUtil;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 快递鸟统一电子面单服务实现
+ *
+ * 核心思想:
+ * 1. 公共字段统一组装
+ * 2. 每个快递规则单独类实现
+ * 3. 每个快递配置单独放在 yml
+ */
+@Service
+public class KdniaoUniversalEOrderServiceImpl implements IKdniaoUniversalEOrderService {
+
+    private final KdniaoUniversalConfig kdniaoConfig;
+    private final List<IKdniaoCarrierRule> carrierRules;
+
+    public KdniaoUniversalEOrderServiceImpl(KdniaoUniversalConfig kdniaoConfig,
+                                            List<IKdniaoCarrierRule> carrierRules) {
+        this.kdniaoConfig = kdniaoConfig;
+        this.carrierRules = carrierRules;
+    }
+
+    @Override
+    public KdniaoUniversalResponse submit(KdniaoSubmitCommand command) {
+        //校验公共参数
+        validateCommon(command);
+
+        String shipperCode = command.getShipperCode() == null ? null : command.getShipperCode().trim().toUpperCase();
+
+        if (!StringUtils.hasText(shipperCode)) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+
+        if (kdniaoConfig.getCarriers() == null || kdniaoConfig.getCarriers().isEmpty()) {
+            throw new IllegalArgumentException("kdniao.carriers配置为空,请检查application.yml");
+        }
+
+        KdniaoCarrierConfig config = kdniaoConfig.getCarriers().get(shipperCode);
+        if (config == null) {
+            throw new IllegalArgumentException("未配置快递公司账号信息:" + shipperCode);
+        }
+        if (config.getSender() == null) {
+            throw new IllegalArgumentException("未配置快递公司发件人信息:" + shipperCode);
+        }
+
+        Map<String, Object> requestData = buildCommonRequestData(command, config, shipperCode);
+
+        IKdniaoCarrierRule rule = resolveRule(shipperCode);
+        rule.apply(command, config, requestData);
+
+        try {
+            String requestDataJson = JSON.toJSONString(requestData);
+            String dataSign = KdniaoRequestUtil.getDataSign(requestDataJson, kdniaoConfig.getApiKey());
+
+            StringBuilder form = new StringBuilder();
+            form.append("RequestData=").append(URLEncoder.encode(requestDataJson, "UTF-8"));
+            form.append("&EBusinessID=").append(URLEncoder.encode(kdniaoConfig.getEBusinessId(), "UTF-8"));
+            form.append("&RequestType=").append(URLEncoder.encode("1007", "UTF-8"));
+            form.append("&DataSign=").append(dataSign);
+            form.append("&DataType=").append(URLEncoder.encode("2", "UTF-8"));
+
+            String resp = KdniaoRequestUtil.doPost(kdniaoConfig.getReqUrl(), form.toString());
+            return JSON.parseObject(resp, KdniaoUniversalResponse.class);
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 构建公共请求体
+     */
+    private Map<String, Object> buildCommonRequestData(KdniaoSubmitCommand command,
+                                                       KdniaoCarrierConfig config,
+                                                       String shipperCode) {
+        Map<String, Object> data = new LinkedHashMap<>();
+
+        data.put("ShipperCode", shipperCode);
+        data.put("OrderCode", buildOrderCode(command.getBizOrderNo()));
+        data.put("PayType", command.getPayType());
+        data.put("ExpType", command.getExpType());
+
+        data.put("Sender", toPersonMap(config.getSender()));
+        data.put("Receiver", toPersonMap(command.getReceiver()));
+        data.put("Commodity", toCommodityList(command.getCommodity()));
+        data.put("Quantity", command.getQuantity() == null ? 1 : command.getQuantity());
+
+        putIfNotNull(data, "Weight", command.getWeight());
+        putIfNotNull(data, "Volume", command.getVolume());
+        putIfNotNull(data, "Cost", command.getCost());
+        putIfNotNull(data, "OtherCost", command.getOtherCost());
+
+        putIfHasText(data, "Remark", command.getRemark());
+        putIfHasText(data, "IsReturnPrintTemplate",
+                StringUtils.hasText(command.getIsReturnPrintTemplate()) ? command.getIsReturnPrintTemplate() : "1");
+        putIfHasText(data, "TemplateSize",
+                StringUtils.hasText(command.getTemplateSize()) ? command.getTemplateSize() : "130");
+        putIfHasText(data, "IsSubscribe",
+                StringUtils.hasText(command.getIsSubscribe()) ? command.getIsSubscribe() : "0");
+
+        putIfNotNull(data, "IsNotice", command.getIsNotice());
+        putIfHasText(data, "StartDate", command.getStartDate());
+        putIfHasText(data, "EndDate", command.getEndDate());
+
+        if (command.getAddService() != null && !command.getAddService().isEmpty()) {
+            List<Map<String, Object>> addServices = new ArrayList<>();
+            for (KdniaoAddServiceNew item : command.getAddService()) {
+                Map<String, Object> map = new LinkedHashMap<>();
+                putIfHasText(map, "Name", item.getName());
+                if (item.getValue() != null) {
+                    map.put("Value", item.getValue());
+                }
+                putIfHasText(map, "CustomerID", item.getCustomerId());
+                addServices.add(map);
+            }
+            data.put("AddService", addServices);
+        }
+
+        return data;
+    }
+
+    /**
+     * 解析规则
+     */
+    private IKdniaoCarrierRule resolveRule(String shipperCode) {
+        for (IKdniaoCarrierRule rule : carrierRules) {
+            if (!(rule instanceof DefaultCarrierRule) && rule.supports(shipperCode)) {
+                return rule;
+            }
+        }
+        for (IKdniaoCarrierRule rule : carrierRules) {
+            if (rule instanceof DefaultCarrierRule) {
+                return rule;
+            }
+        }
+        throw new IllegalStateException("未找到默认规则实现");
+    }
+
+    /**
+     * 公共参数校验
+     */
+    private void validateCommon(KdniaoSubmitCommand command) {
+        if (command == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (!StringUtils.hasText(command.getShipperCode())) {
+            throw new IllegalArgumentException("shipperCode不能为空");
+        }
+        if (command.getPayType() == null) {
+            throw new IllegalArgumentException("payType不能为空");
+        }
+        if (!StringUtils.hasText(command.getExpType())) {
+            throw new IllegalArgumentException("expType不能为空");
+        }
+        if (command.getReceiver() == null) {
+            throw new IllegalArgumentException("receiver不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getName())) {
+            throw new IllegalArgumentException("receiver.name不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getMobile())
+                && !StringUtils.hasText(command.getReceiver().getTel())) {
+            throw new IllegalArgumentException("receiver.mobile和receiver.tel至少填写一个");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getProvinceName())) {
+            throw new IllegalArgumentException("receiver.provinceName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getCityName())) {
+            throw new IllegalArgumentException("receiver.cityName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getExpAreaName())) {
+            throw new IllegalArgumentException("receiver.expAreaName不能为空");
+        }
+        if (!StringUtils.hasText(command.getReceiver().getAddress())) {
+            throw new IllegalArgumentException("receiver.address不能为空");
+        }
+        if (command.getCommodity() == null || command.getCommodity().isEmpty()) {
+            throw new IllegalArgumentException("commodity不能为空");
+        }
+        if (!StringUtils.hasText(command.getCommodity().get(0).getGoodsName())) {
+            throw new IllegalArgumentException("commodity.goodsName不能为空");
+        }
+    }
+
+    /**
+     * 构建订单号
+     */
+    private String buildOrderCode(String bizOrderNo) {
+        if (StringUtils.hasText(bizOrderNo)) {
+            return bizOrderNo.trim() + "-" + System.currentTimeMillis();
+        }
+        return "KD" + System.currentTimeMillis();
+    }
+
+    /**
+     * 发件人/收件人转Map
+     */
+    private Map<String, Object> toPersonMap(KdniaoPersonNew p) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        putIfHasText(map, "Company", p.getCompany());
+        putIfHasText(map, "Name", p.getName());
+        putIfHasText(map, "Tel", p.getTel());
+        putIfHasText(map, "Mobile", p.getMobile());
+        putIfHasText(map, "ProvinceName", p.getProvinceName());
+        putIfHasText(map, "CityName", p.getCityName());
+        putIfHasText(map, "ExpAreaName", p.getExpAreaName());
+        putIfHasText(map, "Address", p.getAddress());
+        putIfHasText(map, "PostCode", p.getPostCode());
+        return map;
+    }
+
+    /**
+     * 商品列表转Map列表
+     */
+    private List<Map<String, Object>> toCommodityList(List<KdniaoCommodityNew> commodityList) {
+        return commodityList.stream().map(c -> {
+            Map<String, Object> map = new LinkedHashMap<>();
+            putIfHasText(map, "GoodsName", c.getGoodsName());
+            putIfHasText(map, "GoodsCode", c.getGoodsCode());
+            putIfNotNull(map, "Goodsquantity", c.getGoodsQuantity());
+            putIfNotNull(map, "GoodsPrice", c.getGoodsPrice());
+            putIfNotNull(map, "GoodsWeight", c.getGoodsWeight());
+            putIfHasText(map, "GoodsDesc", c.getGoodsDesc());
+            putIfNotNull(map, "GoodsVol", c.getGoodsVol());
+            return map;
+        }).collect(Collectors.toList());
+    }
+
+    private void putIfHasText(Map<String, Object> map, String key, String value) {
+        if (StringUtils.hasText(value)) {
+            map.put(key, value.trim());
+        }
+    }
+
+    private void putIfNotNull(Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+}

+ 95 - 0
fs-admin/src/main/java/com/fs/kdniaoNew/util/KdniaoRequestUtil.java

@@ -0,0 +1,95 @@
+package com.fs.kdniaoNew.util;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.util.Base64;
+
+/**
+ * 快递鸟请求工具类
+ */
+public class KdniaoRequestUtil {
+
+    private KdniaoRequestUtil() {
+    }
+
+    /**
+     * 生成 DataSign
+     */
+    public static String getDataSign(String requestData, String apiKey) {
+        try {
+            String md5 = md5Hex(requestData + apiKey);
+            String base64 = Base64.getEncoder().encodeToString(md5.getBytes("UTF-8"));
+            return URLEncoder.encode(base64, "UTF-8");
+        } catch (Exception e) {
+            throw new RuntimeException("生成DataSign失败", e);
+        }
+    }
+
+    /**
+     * POST表单请求
+     */
+    public static String doPost(String reqUrl, String formData) {
+        HttpURLConnection conn = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        try {
+            URL url = new URL(reqUrl);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setConnectTimeout(10000);
+            conn.setReadTimeout(20000);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setUseCaches(false);
+            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+
+            os = conn.getOutputStream();
+            os.write(formData.getBytes("UTF-8"));
+            os.flush();
+
+            br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("调用快递鸟失败", e);
+        } finally {
+            try {
+                if (os != null) os.close();
+            } catch (Exception ignored) {
+            }
+            try {
+                if (br != null) br.close();
+            } catch (Exception ignored) {
+            }
+            if (conn != null) conn.disconnect();
+        }
+    }
+
+    /**
+     * MD5
+     */
+    private static String md5Hex(String str) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] bytes = md.digest(str.getBytes("UTF-8"));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : bytes) {
+                String hex = Integer.toHexString(b & 0xff);
+                if (hex.length() == 1) sb.append('0');
+                sb.append(hex);
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("MD5失败", e);
+        }
+    }
+}

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

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

+ 2 - 1
fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -255,7 +255,8 @@ public class CompanyUserController extends AppBaseController {
         // 查询公司下销售
         CompanyUser companyUser = new CompanyUser();
         companyUser.setCompanyId(getCompanyId());
-        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserList(companyUser);
+//        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserList(companyUser);
+        List<CompanyUser> companyUsers = companyUserService.selectCompanyUserListByCompanyId(companyUser);
         return R.ok().put("data",companyUsers);
     }
 

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

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

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

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

+ 88 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWithdrawDetailController.java

@@ -0,0 +1,88 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
+import com.github.pagehelper.PageHelper;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 销售端-提现明细(新接口,数据自 2026-03-27 起)
+ */
+@RestController
+@RequestMapping("/company/withdrawDetail")
+public class CompanyWithdrawDetailController extends BaseController {
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private ICompanyWithdrawDetailService companyWithdrawDetailService;
+
+    /**
+     * 分公司名称 + 可提现金额(实时,取 company.money)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/summary")
+    public R summary() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        return R.ok(companyWithdrawDetailService.summary(companyId));
+    }
+
+    /**
+     * 分页列表:订单记录时间倒序,默认每页 10 条(由前端传 pageSize)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:list')")
+    @GetMapping("/list")
+    public TableDataInfo list() {
+        PageDomain page = TableSupport.buildPageRequest();
+        int pageNum = page.getPageNum() != null && page.getPageNum() > 0 ? page.getPageNum() : 1;
+        int pageSize = page.getPageSize() != null && page.getPageSize() > 0 ? page.getPageSize() : 10;
+        PageHelper.startPage(pageNum, pageSize);
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailList(companyId);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        long start = (long) (pageNum - 1) * pageSize;
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo((int) (start + i + 1));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出全部字段(同列表,不受分页限制)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyMoneyLogsDetail:export')")
+    @Log(title = "提现明细", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        List<CompanyWithdrawDetailVO> list = companyWithdrawDetailService.selectWithdrawDetailList(companyId);
+        companyWithdrawDetailService.normalizeDisplayAmounts(list);
+        for (int i = 0; i < list.size(); i++) {
+            list.get(i).setSerialNo(i + 1);
+        }
+        ExcelUtil<CompanyWithdrawDetailVO> util = new ExcelUtil<>(CompanyWithdrawDetailVO.class);
+        return util.exportExcel(list, "提现明细");
+    }
+}

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

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

+ 44 - 5
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java

@@ -18,6 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -136,25 +137,63 @@ public class CrmCustomerPropertyController extends BaseController {
         return toAjax(crmCustomerPropertyService.updateCrmCustomerProperty(property));
     }
 
+//    @ApiOperation("删除客户属性标签")
+//    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+//    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+//    @DeleteMapping("/{ids}")
+//    public AjaxResult remove(@PathVariable Long[] ids) {
+//        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+//    }
+
+//    @ApiOperation("删除客户单个属性标签")
+//    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+//    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+//    @DeleteMapping("/deleteByPropertyId")
+//    public AjaxResult deleteByPropertyId(
+//            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+//            @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
+//        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+//                .eq(CrmCustomerProperty::getCustomerId, customerId)
+//                .eq(CrmCustomerProperty::getPropertyId, propertyId)
+//                .remove());
+//    }
+
     @ApiOperation("删除客户属性标签")
-    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:delete')")
     @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
     @DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids) {
-        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Date now = new Date();
+        boolean success = crmCustomerPropertyService.lambdaUpdate()
+                .in(CrmCustomerProperty::getId, ids)
+                .eq(CrmCustomerProperty::getDeleted, 0)
+                .set(CrmCustomerProperty::getDeleted, 1)
+                .set(CrmCustomerProperty::getDeleteBy, loginUser.getUsername())
+                .set(CrmCustomerProperty::getDeleteTime, now)
+                .update();
+
+        return toAjax(success);
     }
 
     @ApiOperation("删除客户单个属性标签")
-    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:delete')")
     @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
     @DeleteMapping("/deleteByPropertyId")
     public AjaxResult deleteByPropertyId(
             @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
             @ApiParam(required = true, name = "propertyId", value = "属性模板 ID") @RequestParam Long propertyId) {
-        return toAjax(crmCustomerPropertyService.lambdaUpdate()
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        boolean success = crmCustomerPropertyService.lambdaUpdate()
                 .eq(CrmCustomerProperty::getCustomerId, customerId)
                 .eq(CrmCustomerProperty::getPropertyId, propertyId)
-                .remove());
+                .eq(CrmCustomerProperty::getDeleted, 0)
+                .set(CrmCustomerProperty::getDeleted, 1)
+                .set(CrmCustomerProperty::getDeleteBy, loginUser.getUsername())
+                .set(CrmCustomerProperty::getDeleteTime, new Date())
+                .update();
+
+        return toAjax(success);
     }
 
     @ApiOperation("导出客户属性标签")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 0 - 24
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -359,15 +359,9 @@ public class FsStoreOrderController extends BaseController
                         vo.setPayDelivery(java.math.BigDecimal.ZERO);
                         vo.setCost(java.math.BigDecimal.ZERO);
                         vo.setFPrice(java.math.BigDecimal.ZERO);
-                        vo.setBarCode("");
-                        vo.setCateName("");
-                        vo.setBankTransactionId("");
                     }
                     vo.setCost(java.math.BigDecimal.ZERO);
                     vo.setFPrice(java.math.BigDecimal.ZERO);
-                    vo.setBarCode("");
-                    vo.setCateName("");
-                    vo.setBankTransactionId("");
                 }
             }
             ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -396,15 +390,9 @@ public class FsStoreOrderController extends BaseController
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
                     vo.setCost(java.math.BigDecimal.ZERO);
                     vo.setFPrice(java.math.BigDecimal.ZERO);
-                    vo.setBarCode("");
-                    vo.setCateName("");
-                    vo.setBankTransactionId("");
                 }
                 vo.setCost(java.math.BigDecimal.ZERO);
                 vo.setFPrice(java.math.BigDecimal.ZERO);
-                vo.setBarCode("");
-                vo.setCateName("");
-                vo.setBankTransactionId("");
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
@@ -462,15 +450,9 @@ public class FsStoreOrderController extends BaseController
                             vo.setPayDelivery(java.math.BigDecimal.ZERO);
                             vo.setCost(java.math.BigDecimal.ZERO);
                             vo.setFPrice(java.math.BigDecimal.ZERO);
-                            vo.setBarCode("");
-                            vo.setCateName("");
-                            vo.setBankTransactionId("");
                         }
                         vo.setCost(java.math.BigDecimal.ZERO);
                         vo.setFPrice(java.math.BigDecimal.ZERO);
-                        vo.setBarCode("");
-                        vo.setCateName("");
-                        vo.setBankTransactionId("");
                     }
                 }
                 ExcelUtil<com.fs.hisStore.vo.FsStoreOrderItemExportZMVO> util = new ExcelUtil<>(com.fs.hisStore.vo.FsStoreOrderItemExportZMVO.class);
@@ -493,15 +475,9 @@ public class FsStoreOrderController extends BaseController
                     vo.setPayPostage(java.math.BigDecimal.ZERO);
                     vo.setCost(java.math.BigDecimal.ZERO);
                     vo.setFPrice(java.math.BigDecimal.ZERO);
-                    vo.setBarCode("");
-                    vo.setCateName("");
-                    vo.setBankTransactionId("");
                 }
                 vo.setCost(java.math.BigDecimal.ZERO);
                 vo.setFPrice(java.math.BigDecimal.ZERO);
-                vo.setBarCode("");
-                vo.setCateName("");
-                vo.setBankTransactionId("");
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 2 - 2
fs-doctor-app/src/main/resources/application.yml

@@ -5,5 +5,5 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: dev
-    active: dev-yjb
+    active: dev
+#    active: dev-yjb

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

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

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

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

+ 23 - 4
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -2,10 +2,14 @@ package com.fs.app.controller;
 
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
+import com.fs.aiSoundReplication.service.AsrService;
+import com.fs.app.msgarchives.QwMsgAuditIngestService;
+import com.fs.app.msgarchives.QwMsgMediaFileService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.uuid.IdUtils;
+import com.fs.enums.MediaMsgTypeEnum;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.AiHookService;
 import com.fs.fastGpt.service.IFastGptRoleService;
@@ -15,10 +19,14 @@ import com.fs.his.dto.TracesDTO;
 import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreOrderService;
+import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.domain.QwUserVideo;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwUserService;
@@ -79,6 +87,17 @@ public class QwMsgController {
     private IFsExpressService expressService;
     @Autowired
     private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private QwMsgAuditIngestService ingestService;
+    @Autowired
+    private QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    private QwMsgAuditMessageMapper messageMapper;
+    @Autowired
+    private QwMsgMediaFileService mediaFileService;
+    @Autowired
+    private AsrService asrService;
 
     @GetMapping("/sendExpressInfo/{orderId}")
     public R sendExpressInfo(@PathVariable Long orderId){
@@ -196,9 +215,10 @@ public class QwMsgController {
     }
 
 
-    @PostMapping("/callback/{serverId}")
+    @PostMapping("/callback/{serverId}/{isNewVersion}")
     @ResponseBody
-    public Map<String,String> callback(@RequestBody String json,@PathVariable Long serverId ){
+    public Map<String,String> callback(@RequestBody String json, @PathVariable Long serverId, @PathVariable(required = false) Boolean isNewVersion){
+
       //  System.out.println(json);
         WxWorkMsgResp wxWorkMsgResp = JSON.parseObject(json, WxWorkMsgResp.class);
         Integer type = wxWorkMsgResp.getType();
@@ -351,7 +371,7 @@ public class QwMsgController {
 
                     if (2000000000000000L-receiver>0){
                         log.info("id:{}, 客户发送", id);
-                        aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype());
+                        aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype(), isNewVersion);
                     }else {
                         log.info("销售发送");
                         aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);
@@ -562,5 +582,4 @@ public class QwMsgController {
             qwUserService.atMsg(qwUser1, "掉线提醒("+msg+")");
         }
     }
-
 }

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

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

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

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

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

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

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

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

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

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

+ 17 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWithdrawDetailMapper.java

@@ -0,0 +1,17 @@
+package com.fs.company.mapper;
+
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 提现明细(基于 company_money_logs 聚合展示)
+ */
+public interface CompanyWithdrawDetailMapper {
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailList(@Param("companyId") Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(@Param("p") CompanyWithdrawDetailAdminParam p);
+}

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

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

+ 20 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWithdrawDetailAdminParam.java

@@ -0,0 +1,20 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 管理端-提现明细查询(多公司、多明细类型)
+ */
+@Data
+public class CompanyWithdrawDetailAdminParam {
+
+    /** 分公司 ID,空表示不限 */
+    private List<Long> companyIds;
+
+    /**
+     * 明细类型:1订单金额入账 2订单金额扣减 3总公司充值 4总公司扣款 5分公司提现 6总公司驳回;空表示全部
+     */
+    private List<Integer> detailTypes;
+}

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

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

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

@@ -48,6 +48,7 @@ public interface ICompanyUserService {
      * @return 物业公司管理员信息集合
      */
     public List<CompanyUser> selectCompanyUserList(CompanyUser companyUser);
+    public List<CompanyUser> selectCompanyUserListByCompanyId(CompanyUser companyUser);
 
     /**
      * 新增物业公司管理员信息

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

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

+ 24 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyWithdrawDetailService.java

@@ -0,0 +1,24 @@
+package com.fs.company.service;
+
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 销售端提现明细
+ */
+public interface ICompanyWithdrawDetailService {
+
+    Map<String, Object> summary(Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailList(Long companyId);
+
+    List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(CompanyWithdrawDetailAdminParam param);
+
+    /**
+     * PRD:订单金额扣减类中 logs_type=6 在库中为成本返还入账,展示为与「扣减」一致的符号口径
+     */
+    void normalizeDisplayAmounts(List<CompanyWithdrawDetailVO> list);
+}

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

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

+ 58 - 13
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -1,6 +1,7 @@
 package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -20,6 +21,7 @@ import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyLiveShowParam;
 import com.fs.company.param.CompanyParam;
 import com.fs.company.service.*;
+import com.fs.company.util.CompanyTuiMoneyCalc;
 import com.fs.company.vo.*;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.config.RedPacketConfig;
@@ -33,8 +35,10 @@ import com.fs.his.domain.FsStorePayment;
 import com.fs.his.dto.InquiryConfigDTO;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.OptionsVO;
+import com.fs.hisStore.domain.FsStoreAfterSalesScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.mapper.FsStoreAfterSalesScrmMapper;
 import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.live.domain.LiveOrder;
 import com.fs.live.mapper.LiveOrderMapper;
@@ -60,6 +64,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -143,6 +148,15 @@ public class CompanyServiceImpl implements ICompanyService
     @Autowired
     CompanyBindGatewayMapper companyBindGatewayMapper;
 
+    @Value("${cloud_host.company_name}")
+    private String companyName;
+
+
+    private static final BigDecimal BJZM_PARTIAL_REFUND_COMMISSION_RATE = new BigDecimal("0.20");
+
+    @Autowired
+    private FsStoreAfterSalesScrmMapper fsStoreAfterSalesScrmMapper;
+
     @Override
     public List<CompanyVO> liveShowList(CompanyParam param) {
         return companyMapper.liveShowList(param);
@@ -162,9 +176,12 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 Double rate=config.getTuiMoneyRate()/100d;
-                BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                        CompanyTuiMoneyCalc.paidAmount(order),
+                        CompanyTuiMoneyCalc.freight(order));
+                BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 logger.info("写入公司推广佣金:"+tuiMoney);
                 company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
@@ -626,9 +643,12 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("his.store");
                 StoreConfig config= JSONUtil.toBean(json,StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 Double rate=config.getTuiMoneyRate()/100d;
-                BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                        CompanyTuiMoneyCalc.paidAmount(order),
+                        CompanyTuiMoneyCalc.freight(order));
+                BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 logger.info("写入公司推广佣金:"+tuiMoney);
                 company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                 companyMapper.updateCompany(company);
@@ -648,10 +668,13 @@ public class CompanyServiceImpl implements ICompanyService
             if(company!=null){
                 String json =configService.selectConfigByKey("his.store");
                 StoreConfig config= JSONUtil.toBean(json,StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率
                 if (config.getTuiMoneyRate()!=null){
                     Double rate=config.getTuiMoneyRate()/100d;
-                    BigDecimal tuiMoney=order.getPayPrice().subtract(order.getTotalPrice().multiply(new BigDecimal(rate)));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    BigDecimal tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                     logger.info("写入公司推广佣金:"+tuiMoney);
                     company.setTuiMoney(company.getTuiMoney().add(tuiMoney));
                     companyMapper.updateCompany(company);
@@ -708,11 +731,14 @@ public class CompanyServiceImpl implements ICompanyService
                 // 卓美,按照润天进行百分比进行分佣
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率;未配置费率时仍为应付 payPrice
                 BigDecimal tuiMoney = BigDecimal.ZERO;
                 if (config != null && config.getTuiMoneyRate() != null) {
                     Double rate = config.getTuiMoneyRate() / 100d;
-                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 } else {
                     tuiMoney = order.getPayPrice();
                 }
@@ -731,6 +757,7 @@ public class CompanyServiceImpl implements ICompanyService
                 FsStoreOrderScrm fsStoreOrder = new FsStoreOrderScrm();
                 fsStoreOrder.setId(order.getId());
                 fsStoreOrder.setTuiMoneyStatus(1);
+                fsStoreOrder.setTuiMoney(tuiMoney);
                 storeOrderScrmMapper.updateFsStoreOrder(fsStoreOrder);
             }
         }
@@ -744,11 +771,14 @@ public class CompanyServiceImpl implements ICompanyService
                 // 卓美,按照润天进行百分比进行分佣
                 String json =configService.selectConfigByKey("store.config");
                 com.fs.store.config.StoreConfig config= JSONUtil.toBean(json, com.fs.store.config.StoreConfig.class);
-                //支付金额-(订单金额*rate%)
+                //(实付金额 - 运费)× 费率;未配置费率时仍为 payPrice
                 BigDecimal tuiMoney = BigDecimal.ZERO;
                 if (config != null && config.getTuiMoneyRate() != null) {
                     double rate = config.getTuiMoneyRate() / 100d;
-                    tuiMoney = order.getTotalPrice().multiply(new BigDecimal(rate));
+                    BigDecimal base = CompanyTuiMoneyCalc.commissionBase(
+                            CompanyTuiMoneyCalc.paidAmount(order),
+                            CompanyTuiMoneyCalc.freight(order));
+                    tuiMoney = CompanyTuiMoneyCalc.multiplyRate(base, rate);
                 } else {
                     tuiMoney = order.getPayPrice();
                 }
@@ -806,14 +836,29 @@ public class CompanyServiceImpl implements ICompanyService
         if(order.getCompanyId()>0){
             Company company=companyMapper.selectCompanyByIdForUpdate(order.getCompanyId());
             if(company!=null){
-                company.setMoney(company.getMoney().subtract(order.getTuiMoney()));
-                company.setTuiMoney(company.getTuiMoney().subtract(order.getTuiMoney()));
+                BigDecimal clawback = order.getTuiMoney();
+                BigDecimal logMoney = order.getTuiMoney().multiply(new BigDecimal(-1));
+
+                // 260415 卓美财务需求,希望部分退款:部分退款金额 * 20%
+                if ("北京卓美".equals(companyName)) {
+                    FsStoreAfterSalesScrm after = fsStoreAfterSalesScrmMapper.selectFsStoreAfterSalesByOrderCode(order.getOrderCode());
+                    BigDecimal payMoney = order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO;
+                    BigDecimal refundAmount = after != null && after.getRefundAmount() != null ? after.getRefundAmount() : null;
+                    if (refundAmount != null && payMoney.compareTo(refundAmount) != 0) {
+                        // 部分退款:公司扣回金额 / 日志金额 = -(退款金额 × 20%)
+                        clawback = refundAmount.multiply(BJZM_PARTIAL_REFUND_COMMISSION_RATE).setScale(2, RoundingMode.HALF_UP);
+                        logMoney = clawback.negate();
+                    }
+                }
+
+                company.setMoney(company.getMoney().subtract(clawback));
+                company.setTuiMoney(company.getTuiMoney().subtract(clawback));
                 companyMapper.updateCompany(company);
                 //写入日志
                 CompanyMoneyLogs log=new CompanyMoneyLogs();
                 log.setCompanyId(order.getCompanyId());
                 log.setRemark("订单佣金退款");
-                log.setMoney(order.getTuiMoney().multiply(new BigDecimal(-1)));
+                log.setMoney(logMoney);
                 log.setLogsType(4);
                 log.setBalance(company.getMoney());
                 log.setCreateTime(new Date());

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

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

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

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

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

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

+ 64 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWithdrawDetailServiceImpl.java

@@ -0,0 +1,64 @@
+package com.fs.company.service.impl;
+
+import com.fs.company.mapper.CompanyWithdrawDetailMapper;
+import com.fs.company.param.CompanyWithdrawDetailAdminParam;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyWithdrawDetailService;
+import com.fs.company.vo.CompanyWithdrawDetailVO;
+import com.fs.company.domain.Company;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class CompanyWithdrawDetailServiceImpl implements ICompanyWithdrawDetailService {
+
+    @Autowired
+    private CompanyWithdrawDetailMapper companyWithdrawDetailMapper;
+
+    @Autowired
+    private ICompanyService companyService;
+
+    @Override
+    public Map<String, Object> summary(Long companyId) {
+        Company company = companyService.selectCompanyById(companyId);
+        Map<String, Object> m = new HashMap<>(4);
+        if (company != null) {
+            m.put("companyName", company.getCompanyName());
+            m.put("withdrawableMoney", company.getMoney() != null ? company.getMoney() : BigDecimal.ZERO);
+        } else {
+            m.put("companyName", "");
+            m.put("withdrawableMoney", BigDecimal.ZERO);
+        }
+        return m;
+    }
+
+    @Override
+    public List<CompanyWithdrawDetailVO> selectWithdrawDetailList(Long companyId) {
+        return companyWithdrawDetailMapper.selectWithdrawDetailList(companyId);
+    }
+
+    @Override
+    public List<CompanyWithdrawDetailVO> selectWithdrawDetailListAdmin(CompanyWithdrawDetailAdminParam param) {
+        if (param == null) {
+            param = new CompanyWithdrawDetailAdminParam();
+        }
+        return companyWithdrawDetailMapper.selectWithdrawDetailListAdmin(param);
+    }
+
+    @Override
+    public void normalizeDisplayAmounts(List<CompanyWithdrawDetailVO> list) {
+        if (list == null) {
+            return;
+        }
+        for (CompanyWithdrawDetailVO vo : list) {
+            if (vo.getLogsType() != null && vo.getLogsType() == 6 && vo.getAmount() != null) {
+                vo.setAmount(vo.getAmount().negate());
+            }
+        }
+    }
+}

+ 7 - 3
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java

@@ -295,8 +295,12 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 3. 构建创建任务参数(AI 外呼模式:taskType=1)
         EasyCallCreateTaskParam createParam = new EasyCallCreateTaskParam();
         // 任务名称:使用工作流实例 ID + 被叫人 ID 组合,保证唯一性
-        createParam.setBatchName("workflow_" + context.getWorkflowInstanceId() + "_" + calleeId);
-        createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+        createParam.setBatchName(robotic.getName() + "_" + context.getWorkflowInstanceId() + "_" + calleeId);
+        if (null != callConfigVo.getMaxConcurrency())
+            createParam.setThreadNum(Long.valueOf(callConfigVo.getMaxConcurrency()));
+        else {
+            createParam.setThreadNum(3L);
+        }
         // AI 外呼模式
         createParam.setTaskType(1);
         // 外呼线路(网关)
@@ -308,7 +312,7 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
         // 音色来源(如未配置默认留空,由 EasyCallCenter365 使用默认值)
         createParam.setVoiceSource(callConfigVo.getVoiceSource());
         // 技能组(转人工客服分组,可选)
-//        createParam.setGroupId(callConfigVo.getBusiGroupId());
+        createParam.setGroupId(callConfigVo.getBusiGroupId());
 
         JSONObject runParam = (JSONObject) JSON.toJSON(createParam);
         runParam.put("companyId", robotic.getCompanyId());

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiQwAddWxTaskNode.java

@@ -31,7 +31,7 @@ public class AiQwAddWxTaskNode extends AbstractWorkflowNode {
     private static final CompanyWxClientMapper companyWxClientMapper = SpringUtils.getBean(CompanyWxClientMapper.class);
     @SuppressWarnings("unchecked")
     private static final RedisCacheT<String> redisCache = SpringUtils.getBean(RedisCacheT.class);
-    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:";
+    public static final String DELAY_QW_ADD_WX_KEY = "qwAddWxTask:delay:%s:%s:%s:";
     /**
      * 默认加微超时时间(分钟)
      */

+ 90 - 0
fs-service/src/main/java/com/fs/company/util/CompanyTuiMoneyCalc.java

@@ -0,0 +1,90 @@
+package com.fs.company.util;
+
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.live.domain.LiveOrder;
+
+import java.math.BigDecimal;
+
+/**
+ * 分公司佣金/入账基数:(实付金额 - 运费)× 费率
+ */
+public final class CompanyTuiMoneyCalc {
+
+    private CompanyTuiMoneyCalc() {
+    }
+
+    /** 实付金额:优先取 payMoney,否则 payPrice */
+    public static BigDecimal paidAmount(FsStoreOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayMoney() != null) {
+            return order.getPayMoney();
+        }
+        return order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+    }
+
+    /** 运费:his 订单取 freightPrice */
+    public static BigDecimal freight(FsStoreOrder order) {
+        if (order == null || order.getFreightPrice() == null) {
+            return BigDecimal.ZERO;
+        }
+        return order.getFreightPrice();
+    }
+
+    public static BigDecimal paidAmount(FsStoreOrderScrm order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayPrice() != null) {
+            return order.getPayPrice();
+        }
+        return order.getPayMoney() != null ? order.getPayMoney() : BigDecimal.ZERO;
+    }
+
+    /** 运费:商城 SCRM 订单取用户实付邮费 payPostage */
+    public static BigDecimal freight(FsStoreOrderScrm order) {
+        if (order == null || order.getPayPostage() == null) {
+            return BigDecimal.ZERO;
+        }
+
+        return order.getPayPostage();
+    }
+
+    public static BigDecimal paidAmount(LiveOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayMoney() != null) {
+            return order.getPayMoney();
+        }
+        return order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+    }
+
+    /** 运费:直播单优先 payPostage,否则 totalPostage */
+    public static BigDecimal freight(LiveOrder order) {
+        if (order == null) {
+            return BigDecimal.ZERO;
+        }
+        if (order.getPayPostage() != null) {
+            return order.getPayPostage();
+        }
+        if (order.getTotalPostage() != null) {
+            return order.getTotalPostage();
+        }
+        return BigDecimal.ZERO;
+    }
+
+    /** 计佣基数:max(0, 实付 - 运费) */
+    public static BigDecimal commissionBase(BigDecimal paid, BigDecimal freightAmt) {
+        BigDecimal f = freightAmt != null ? freightAmt : BigDecimal.ZERO;
+        BigDecimal p = paid != null ? paid : BigDecimal.ZERO;
+        BigDecimal b = p.subtract(f);
+        return b.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : b;
+    }
+
+    public static BigDecimal multiplyRate(BigDecimal base, double rateFraction) {
+        return base.multiply(BigDecimal.valueOf(rateFraction));
+    }
+}

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

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

+ 57 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWithdrawDetailVO.java

@@ -0,0 +1,57 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 销售端-提现明细分页/导出
+ */
+@Data
+public class CompanyWithdrawDetailVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 内部用:原始 logs_type */
+    private Integer logsType;
+
+    private Long logsId;
+
+    @Excel(name = "序号", sort = 1)
+    private Integer serialNo;
+
+    @Excel(name = "公司名称", sort = 2)
+    private String companyName;
+
+    @Excel(name = "所属销售", sort = 3)
+    private String salesName;
+
+    @Excel(name = "订单号", sort = 4)
+    private String orderCode;
+
+    @Excel(name = "交易单号", sort = 5)
+    private String tradeNo;
+
+    @Excel(name = "订单状态", sort = 6)
+    private String orderStatusText;
+
+    /** 订单原始状态码,与商城订单列表字典 store_order_status 一致;非订单行无此值 */
+    private Integer orderStatus;
+
+    @Excel(name = "售后状态", sort = 7)
+    private String afterSalesStatusText;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "订单记录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 8)
+    private Date recordTime;
+
+    @Excel(name = "明细类型", sort = 9)
+    private String detailTypeText;
+
+    @Excel(name = "金额", sort = 10)
+    private BigDecimal amount;
+}

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

@@ -129,6 +129,12 @@ public class WorkflowExecRecordVo {
      */
     private String contentList;
 
+    /**
+     * 意向度
+     */
+    private String intention;
+
+
     /**
      * 节点执行日志VO
      */
@@ -195,5 +201,10 @@ public class WorkflowExecRecordVo {
          * 输出数据
          */
         private String outputData;
+
+        /**
+         * 外呼节点的对话记录
+         */
+        private String nodeContentList;
     }
 }

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

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

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

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

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

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

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

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

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

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

+ 29 - 0
fs-service/src/main/java/com/fs/course/domain/FsPublicCourseTrafficLog.java

@@ -0,0 +1,29 @@
+package com.fs.course.domain;
+
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 公开课流量记录对象 fs_public_course_traffic_log
+ */
+@Data
+public class FsPublicCourseTrafficLog extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    private Long logId;
+
+    private String uuId;
+
+    private Long userId;
+
+    private Long courseId;
+
+    private Long videoId;
+
+    private Long internetTraffic;
+
+    private Integer status;
+
+    private  Long projectId;
+}

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

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

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

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

Vissa filer visades inte eftersom för många filer har ändrats