Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
yjwang 1 päivä sitten
vanhempi
commit
e99681fa43
82 muutettua tiedostoa jossa 2902 lisäystä ja 133 poistoa
  1. 67 3
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  2. 67 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerPropertyController.java
  3. 67 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerPropertyTemplateController.java
  4. 4 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  5. 48 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  6. 2 2
      fs-admin/src/main/resources/application.yml
  7. 4 4
      fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java
  8. 12 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseLinkController.java
  9. 170 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java
  10. 78 0
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java
  11. 4 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  12. 60 5
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  13. 41 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  14. 4 0
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java
  15. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyWxClientMapper.java
  16. 1 1
      fs-service/src/main/java/com/fs/company/service/ICompanyWxClientService.java
  17. 17 19
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  18. 2 2
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java
  19. 7 0
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiCallTaskNode.java
  20. 6 3
      fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java
  21. 4 0
      fs-service/src/main/java/com/fs/company/vo/AiCallConfigVO.java
  22. 2 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  23. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  24. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java
  25. 64 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  26. 9 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java
  27. 24 0
      fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java
  28. 27 0
      fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoRemainTimeParam.java
  29. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java
  30. 24 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  31. 50 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  32. 200 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  33. 43 16
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  34. 13 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  35. 23 0
      fs-service/src/main/java/com/fs/course/vo/CourseProductSalesVO.java
  36. 72 0
      fs-service/src/main/java/com/fs/course/vo/CourseStatisticsDetailVO.java
  37. 34 0
      fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java
  38. 8 0
      fs-service/src/main/java/com/fs/course/vo/UpdateCourseTimeVo.java
  39. 38 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java
  40. 25 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomerPropertyTemplate.java
  41. 28 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java
  42. 22 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyTemplateMapper.java
  43. 38 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyService.java
  44. 21 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyTemplateService.java
  45. 193 0
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java
  46. 46 0
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyTemplateServiceImpl.java
  47. 6 1
      fs-service/src/main/java/com/fs/enums/ExecutionStatusEnum.java
  48. 64 0
      fs-service/src/main/java/com/fs/enums/TaskTypeEnum.java
  49. 39 0
      fs-service/src/main/java/com/fs/his/domain/FsUserPlayer.java
  50. 67 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserPlayerMapper.java
  51. 67 0
      fs-service/src/main/java/com/fs/his/service/IFsUserPlayerService.java
  52. 131 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserPlayerServiceImpl.java
  53. 12 0
      fs-service/src/main/java/com/fs/his/vo/FsUserPlayerVo.java
  54. 13 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  55. 12 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  56. 8 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  57. 4 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreOrderCreateParam.java
  58. 2 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  59. 10 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  60. 35 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java
  61. 3 1
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java
  62. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductListQueryVO.java
  63. 7 7
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java
  64. 5 1
      fs-service/src/main/resources/application-config-druid-jsbk.yml
  65. 1 1
      fs-service/src/main/resources/application-config-myhk.yml
  66. 3 4
      fs-service/src/main/resources/application-druid-jzzx.yml
  67. 32 0
      fs-service/src/main/resources/db/20260317-客户属性模板菜单.sql
  68. 4 0
      fs-service/src/main/resources/db/20260318-客户属性标签添加意向和占比.sql
  69. 3 1
      fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml
  70. 1 1
      fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml
  71. 133 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  72. 130 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml
  73. 91 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyTemplateMapper.xml
  74. 96 0
      fs-service/src/main/resources/mapper/his/FsUserPlayerMapper.xml
  75. 1 0
      fs-service/src/main/resources/mapper/hisStore/FsShippingTemplatesScrmMapper.xml
  76. 46 20
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  77. 8 16
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  78. 4 4
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml
  79. 6 3
      fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
  80. 86 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java
  81. 83 12
      fs-user-app/src/main/java/com/fs/app/controller/game/PlayerController.java
  82. 1 1
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

+ 67 - 3
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -9,9 +9,7 @@ import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
-import com.fs.course.vo.FsCourseOverVO;
-import com.fs.course.vo.FsCourseWatchLogListVO;
-import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
+import com.fs.course.vo.*;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
 import com.github.pagehelper.PageHelper;
@@ -25,6 +23,7 @@ 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.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
@@ -236,4 +235,69 @@ public class FsCourseWatchLogController extends BaseController
         List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
         return getDataTable(list);
     }
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsDetail")
+    public R getCourseStatisticsDetail(@RequestParam("videoId") Long videoId, @RequestParam("periodId") Long periodId)
+    {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        return R.ok().put("data", fsCourseWatchLogService.getCourseStatisticsDetail(videoId, periodId));
+    }
+
+    /**
+     * 课程小结-用户详情列表(分页)
+     * 根据videoId、periodId查询观看记录,区分首次/第2-n次观看时长,关联订单及销售公司
+     *
+     * @param videoId  视频ID
+     * @param periodId 营期ID
+     * @param pageNum  页码
+     * @param pageSize 每页条数
+     * @return 分页用户详情
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @GetMapping("/courseStatisticsUserDetail")
+    public R getCourseStatisticsUserDetail(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId,
+            @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+        if (videoId == null || periodId == null) {
+            return R.error("视频ID和营期ID不能为空");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        param.setPageNum(pageNum);
+        param.setPageSize(pageSize);
+        PageHelper.startPage(pageNum, pageSize);
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
+    }
+
+    /**
+     * 课程小结-用户详情导出(按创建时间倒序,最多50000条)
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+    @Log(title = "课程小结用户详情导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/courseStatisticsUserDetailExport")
+    public AjaxResult courseStatisticsUserDetailExport(
+            @RequestParam("videoId") Long videoId,
+            @RequestParam("periodId") Long periodId) {
+        if (videoId == null || periodId == null) {
+            return AjaxResult.error("视频ID和营期ID不能为空");
+        }
+        com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
+        param.setVideoId(videoId);
+        param.setPeriodId(periodId);
+        List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
+        ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
+        return util.exportExcel(list, "用户看课数据");
+    }
 }

+ 67 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerPropertyController.java

@@ -0,0 +1,67 @@
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/crm/customerProperty")
+public class CrmCustomerPropertyController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerProperty crmCustomerProperty) {
+        startPage();
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyList(crmCustomerProperty);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:export')")
+    @Log(title = "客户属性标签", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerProperty crmCustomerProperty) {
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyList(crmCustomerProperty);
+        ExcelUtil<CrmCustomerProperty> util = new ExcelUtil<CrmCustomerProperty>(CrmCustomerProperty.class);
+        return util.exportExcel(list, "客户属性标签数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(crmCustomerPropertyService.selectCrmCustomerPropertyById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:add')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerProperty crmCustomerProperty) {
+        return toAjax(crmCustomerPropertyService.insertCrmCustomerProperty(crmCustomerProperty));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerProperty crmCustomerProperty) {
+        return toAjax(crmCustomerPropertyService.updateCrmCustomerProperty(crmCustomerProperty));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerProperty:remove')")
+    @Log(title = "客户属性标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyService.deleteCrmCustomerPropertyByIds(ids));
+    }
+}

+ 67 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerPropertyTemplateController.java

@@ -0,0 +1,67 @@
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/crm/customerPropertyTemplate")
+public class CrmCustomerPropertyTemplateController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyTemplateService crmCustomerPropertyTemplateService;
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        startPage();
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:export')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        ExcelUtil<CrmCustomerPropertyTemplate> util = new ExcelUtil<CrmCustomerPropertyTemplate>(CrmCustomerPropertyTemplate.class);
+        return util.exportExcel(list, "客户属性标签模板数据");
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:add')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.insertCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.updateCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @PreAuthorize("@ss.hasPermi('crm:customerPropertyTemplate:remove')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyTemplateService.deleteCrmCustomerPropertyTemplateByIds(ids));
+    }
+}

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

@@ -309,6 +309,8 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                 for (FsStoreOrderItemExportZMVO vo : zmvoList) {
                     if("2".equals(vo.getOrderType())){
                         vo.setOrderTypeStr("直播订单" );
+                    }else if ("3".equals(vo.getOrderType())){
+                        vo.setOrderTypeStr("点播订单" );
                     }else{
                         vo.setOrderTypeStr("商城订单" );
                     }
@@ -418,6 +420,8 @@ public class FsStoreHealthOrderScrmController extends BaseController {
                     for (FsStoreOrderItemExportZMVO vo : zmvoList) {
                         if ("2".equals(vo.getOrderType())) {
                             vo.setOrderTypeStr("直播订单");
+                        }else if ("3".equals(vo.getOrderType())){
+                            vo.setOrderTypeStr("点播订单" );
                         }else {
                             vo.setOrderTypeStr("商城订单");
                         }

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

@@ -19,10 +19,21 @@ import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.param.CompanyStoreOrderMoneyLogsListParam;
 import com.fs.company.service.ICompanyMoneyLogsService;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyUserService;
 import com.fs.company.vo.CompanyStoreOrderMoneyLogsVO;
 import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.param.FsCourseWatchLogParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrderQuery;
 import com.fs.erp.dto.ErpOrderQueryRequert;
@@ -147,9 +158,21 @@ public class FsStoreOrderScrmController extends BaseController {
     @Autowired
     private IFsStoreOrderLogsScrmService fsStoreOrderLogsService;
 
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+    @Autowired
+    private IFsUserCoursePeriodService fsUserCoursePeriodService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
+
     @Value("${cloud_host.company_name}")
     private String signProjectName;
 
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private ICompanyService companyService;
+
     private IErpOrderService getErpService(){
         //判断是否开启erp
         IErpOrderService erpOrderService = null;
@@ -652,6 +675,31 @@ public class FsStoreOrderScrmController extends BaseController {
         if (user != null) {
             user.setPhone(ParseUtils.parsePhone(user.getPhone()));
         }
+
+        if (order.getCompanyUserId() != null) {
+            CompanyUser companyUser = companyUserService.selectCompanyUserByUserId(order.getCompanyUserId());
+            Company company = companyService.selectCompanyById(companyUser.getCompanyId());
+            order.setCompanyUserName(companyUser.getUserName());
+            order.setCompanyName(company.getCompanyName());
+        } else if (order.getCompanyId() != null) {
+            Company company = companyService.selectCompanyById(order.getCompanyId());
+            order.setCompanyName(company.getCompanyName());
+        }
+
+        if (order.getOrderType() != null && order.getOrderType() == 3) {
+//            FsCourseWatchLogParam param = new FsCourseWatchLogParam();
+//            param.setVideoId(Long.valueOf(order.getVideoId()));
+//            FsCourseWatchLog log = fsCourseWatchLogService.selectFsCourseWatchLogWithUCCV(order.getUserId(), order.getCompanyUserId(), order.getCourseId(), order.getVideoId());
+            if (order.getPeriodId() != null) {
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(Long.valueOf(order.getPeriodId()));
+                order.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
+            if (order.getVideoId() != null) {
+                FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoService.selectFsUserCourseVideoByVideoId(Long.valueOf(order.getVideoId()));
+                order.setVideoName(fsUserCourseVideo.getTitle());
+            }
+        }
+
         FsStoreOrderItemScrm itemMap = new FsStoreOrderItemScrm();
         itemMap.setOrderId(order.getId());
         List<FsStoreOrderItemScrm> items = orderItemService.selectFsStoreOrderItemList(itemMap);

+ 2 - 2
fs-admin/src/main/resources/application.yml

@@ -4,11 +4,11 @@ server:
 # Spring配置
 spring:
   profiles:
-#    active: druid-ylrz
+    active: druid-bjzm-test
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz-test
 #    active: druid-sft
 #    active: druid-fby
-    active: dev
+#    active: dev
 

+ 4 - 4
fs-cid-workflow/src/main/java/com/fs/app/task/CidTask.java

@@ -43,10 +43,10 @@ public class CidTask {
     /**
      * 扫描服务定时任务执行
      */
-    @Scheduled(cron = "0 0/1 * * * ?")
-    public void runContinueTask() {
-        cidWorkflowTaskService.runContinueTask();
-    }
+//    @Scheduled(cron = "0 0/1 * * * ?")
+//    public void runContinueTask() {
+//        cidWorkflowTaskService.runContinueTask();
+//    }
 
 
 

+ 12 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseLinkController.java

@@ -182,4 +182,16 @@ public class FsCourseLinkController extends BaseController
         return fsUserCourseVideoService.createRoomMiniLinkByCourse(param);
     }
 
+    /**
+     * 创建销售易看课链接
+     */
+@PostMapping("/createSaleLink")
+@ApiOperation("创建销售易链接")
+public R createSaleLink(@RequestBody FsCourseLinkCreateParam param) {
+    LoginUser loginUser = SecurityUtils.getLoginUser();
+    param.setCompanyId(loginUser.getCompany().getCompanyId());
+    param.setCompanyUserId(loginUser.getUser().getUserId());
+   return  null;
+}
+
 }

+ 170 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyController.java

@@ -0,0 +1,170 @@
+package com.fs.company.controller.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.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+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 java.util.Map;
+
+@Api(tags = "客户属性标签管理")
+@RestController
+@RequestMapping("/crm/customerProperty")
+public class CrmCustomerPropertyController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @ApiOperation("根据客户 ID 查询属性标签列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list/{customerId}")
+    public R listByCustomerId(@PathVariable("customerId") Long customerId) {
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(customerId);
+        return R.ok().put("data", list);
+    }
+
+    @ApiOperation("查询单个属性标签详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerProperty property = crmCustomerPropertyService.selectCrmCustomerPropertyById(id);
+        return R.ok().put("data", property);
+    }
+
+    @ApiOperation("为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setCreateBy(loginUser.getUsername());
+        return toAjax(crmCustomerPropertyService.insertCrmCustomerProperty(property));
+    }
+
+    @ApiOperation("为客户添加或更新属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdate")
+    public AjaxResult addOrUpdate(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdateCustomerPropertyWithExtra(
+                property.getCustomerId(),
+                property.getPropertyId(),
+                property.getPropertyName(),
+                property.getPropertyValue(),
+                property.getPropertyValueType(),
+                property.getTradeType(),
+                property.getIntention(),
+                property.getLikeRatio(),
+                loginUser.getUsername()
+        ));
+    }
+
+    @ApiOperation("批量为客户添加属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAdd/{customerId}")
+    public AjaxResult batchAdd(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody List<CrmCustomerProperty> properties) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        for (CrmCustomerProperty property : properties) {
+            property.setCreateBy(loginUser.getUsername());
+        }
+        return toAjax(crmCustomerPropertyService.batchAddCustomerProperties(customerId, properties));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/addByTemplateId")
+    public AjaxResult addByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addPropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("通过属性模板 ID 为客户添加或更新标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addOrUpdateByTemplateId")
+    public AjaxResult addOrUpdateByTemplateId(
+            @ApiParam(required = true, name = "customerId", value = "客户 ID") @RequestParam Long customerId,
+            @ApiParam(required = true, name = "templateId", value = "属性模板 ID") @RequestParam Long templateId,
+            @ApiParam(required = true, name = "propertyValue", value = "属性值") @RequestParam String propertyValue) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.addOrUpdatePropertyByTemplateId(customerId, templateId, propertyValue, loginUser.getUsername()));
+    }
+
+    @ApiOperation("批量通过属性模板 ID 为客户添加标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.INSERT)
+    @PostMapping("/batchAddByTemplateIds/{customerId}")
+    public AjaxResult batchAddByTemplateIds(
+            @PathVariable("customerId") Long customerId,
+            @RequestBody Map<Long, String> propertyMap) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return toAjax(crmCustomerPropertyService.batchAddPropertiesByTemplateIds(customerId, propertyMap, loginUser.getUsername()));
+    }
+
+    @ApiOperation("修改客户属性标签")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerProperty property) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        property.setUpdateBy(loginUser.getUsername());
+        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:export')")
+    @Log(title = "客户属性标签", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerProperty crmCustomerProperty) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CrmCustomerProperty> list = crmCustomerPropertyService.selectCrmCustomerPropertyList(crmCustomerProperty);
+        ExcelUtil<CrmCustomerProperty> util = new ExcelUtil<CrmCustomerProperty>(CrmCustomerProperty.class);
+        return util.exportExcel(list, "客户属性标签数据");
+    }
+}

+ 78 - 0
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerPropertyTemplateController.java

@@ -0,0 +1,78 @@
+package com.fs.company.controller.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.poi.ExcelUtil;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "客户属性标签模板管理")
+@RestController
+@RequestMapping("/crm/customerPropertyTemplate")
+public class CrmCustomerPropertyTemplateController extends BaseController {
+
+    @Autowired
+    private ICrmCustomerPropertyTemplateService crmCustomerPropertyTemplateService;
+
+    @ApiOperation("查询客户属性标签模板列表")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        startPage();
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("查询客户属性标签模板详情")
+    @PreAuthorize("@ss.hasPermi('crm:customer:query')")
+    @GetMapping("/{id}")
+    public R getInfo(@PathVariable("id") Long id) {
+        CrmCustomerPropertyTemplate template = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateById(id);
+        return R.ok().put("data", template);
+    }
+
+    @ApiOperation("添加客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.insertCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("修改客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return toAjax(crmCustomerPropertyTemplateService.updateCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate));
+    }
+
+    @ApiOperation("删除客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:edit')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(crmCustomerPropertyTemplateService.deleteCrmCustomerPropertyTemplateByIds(ids));
+    }
+
+    @ApiOperation("导出客户属性标签模板")
+    @PreAuthorize("@ss.hasPermi('crm:customer:export')")
+    @Log(title = "客户属性标签模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        List<CrmCustomerPropertyTemplate> list = crmCustomerPropertyTemplateService.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+        ExcelUtil<CrmCustomerPropertyTemplate> util = new ExcelUtil<CrmCustomerPropertyTemplate>(CrmCustomerPropertyTemplate.class);
+        return util.exportExcel(list, "客户属性标签模板数据");
+    }
+}

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

@@ -1,6 +1,7 @@
 package com.fs.company.controller.live;
 
 import com.fs.common.annotation.Log;
+import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
@@ -12,9 +13,11 @@ import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.framework.service.TokenService;
 import com.fs.live.domain.LiveData;
+import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.ColumnsConfigVo;
+import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveUserDetailExportVO;
 import com.github.pagehelper.PageHelper;
@@ -24,6 +27,7 @@ import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 

+ 60 - 5
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -83,14 +83,18 @@ public class FsStoreOrderController extends BaseController
     private IFsStoreOrderItemScrmService orderItemScrmService;
 
     /**
-     * 查询直播订单列表(仅 fs_store_order_scrm 中 order_type=2)
-     * 分公司负责人(userType=00)可查公司下所有直播订单,否则仅能查自己的直播订单
+     * 查询直播/点播订单列表(fs_store_order_scrm 中 order_type=2 直播订单,order_type=3 点播订单)
+     * 如果前端传了 orderType,则按指定类型查询;如果没传(null),则查询所有直播和点播订单(orderType IN (2,3))
+     * 分公司负责人(userType=00)可查公司下所有订单,否则仅能查自己的订单
      */
     @PostMapping("/healthLiveList")
     public FsStoreOrderListAndStatisticsVo healthLiveList(@RequestBody com.fs.hisStore.param.FsStoreOrderParam param) {
         LoginUser loginUser = SecurityUtils.getLoginUser();
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-        param.setOrderType(2);
+        // 如果前端传了 orderType,使用前端传的值;如果没传(null),设置 orderType = -1(特殊值,SQL 中会转换为查询 orderType IN (2,3))
+        if (param.getOrderType() == null) {
+            param.setOrderType(-1); // 特殊值,表示查询所有直播和点播订单
+        }
         if (!"00".equals(loginUser.getUser().getUserType())) {
             param.setCompanyUserId(loginUser.getUser().getUserId());
         } else {
@@ -147,11 +151,14 @@ public class FsStoreOrderController extends BaseController
         return vo;
     }
 
-    /** 直播订单导出:筛选条件与 healthLiveList 一致(orderType=2 + 公司/负责人权限) */
+    /** 直播/点播订单导出:筛选条件与 healthLiveList 一致(支持按 orderType 筛选,不传则查询所有 + 公司/负责人权限) */
     private void applyHealthLiveFilter(com.fs.hisStore.param.FsStoreOrderParam param) {
         LoginUser loginUser = SecurityUtils.getLoginUser();
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-        param.setOrderType(2);
+        // 如果前端传了 orderType,使用前端传的值;如果没传(null),设置 orderType = -1(特殊值,SQL 中会转换为查询 orderType IN (2,3))
+        if (param.getOrderType() == null) {
+            param.setOrderType(-1); // 特殊值,表示查询所有直播和点播订单
+        }
         if (!"00".equals(loginUser.getUser().getUserType())) {
             param.setCompanyUserId(loginUser.getUser().getUserId());
         } else {
@@ -212,6 +219,17 @@ public class FsStoreOrderController extends BaseController
                 if (vo.getUserAddress() != null) {
                     vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
                 }
+                // 设置订单类型中文显示
+                if (vo.getOrderType() != null) {
+                    String orderTypeStr = vo.getOrderType().toString();
+                    if ("2".equals(orderTypeStr)) {
+                        vo.setOrderType("直播订单");
+                    } else if ("3".equals(orderTypeStr)) {
+                        vo.setOrderType("点播订单");
+                    } else {
+                        vo.setOrderType("商城订单");
+                    }
+                }
             }
         }
         String filter = param.getFilter();
@@ -243,6 +261,21 @@ public class FsStoreOrderController extends BaseController
             return AjaxResult.error("请筛选数据导出");
         }
         List<FsStoreOrderErpExportVO> list = fsStoreOrderScrmService.selectFsStoreOrderListVOByExport(param);
+        if (list != null) {
+            for (FsStoreOrderErpExportVO vo : list) {
+                // 设置订单类型中文显示
+                if (vo.getOrderType() != null) {
+                    String orderTypeStr = vo.getOrderType().toString();
+                    if ("2".equals(orderTypeStr)) {
+                        vo.setOrderType("直播订单");
+                    } else if ("3".equals(orderTypeStr)) {
+                        vo.setOrderType("点播订单");
+                    } else {
+                        vo.setOrderType("商城订单");
+                    }
+                }
+            }
+        }
         String filter = param.getFilter();
         ArrayList<String> filterList = new ArrayList<>();
         if (StringUtils.isNotBlank(filter)) {
@@ -296,6 +329,17 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
+                // 设置订单类型中文显示
+                if (vo.getOrderType() != null) {
+                    String orderTypeStr = vo.getOrderType().toString();
+                    if ("2".equals(orderTypeStr)) {
+                        vo.setOrderType("直播订单");
+                    } else if ("3".equals(orderTypeStr)) {
+                        vo.setOrderType("点播订单");
+                    } else {
+                        vo.setOrderType("商城订单");
+                    }
+                }
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);
@@ -335,6 +379,17 @@ public class FsStoreOrderController extends BaseController
                     vo.setCateName("");
                     vo.setBankTransactionId("");
                 }
+                // 设置订单类型中文显示
+                if (vo.getOrderType() != null) {
+                    String orderTypeStr = vo.getOrderType().toString();
+                    if ("2".equals(orderTypeStr)) {
+                        vo.setOrderType("直播订单");
+                    } else if ("3".equals(orderTypeStr)) {
+                        vo.setOrderType("点播订单");
+                    } else {
+                        vo.setOrderType("商城订单");
+                    }
+                }
             }
         }
         ExcelUtil<FsStoreOrderItemExportVO> util = new ExcelUtil<>(FsStoreOrderItemExportVO.class);

+ 41 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -18,9 +18,18 @@ import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.config.cloud.CloudHostProper;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.param.FsCourseWatchLogParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.framework.security.LoginUser;
@@ -88,6 +97,14 @@ public class FsStoreOrderScrmController extends BaseController
     private ICompanyUserService companyUserService;
     @Autowired
     private CloudHostProper cloudHostProper;
+    @Autowired
+    private ICompanyService companyService;
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+    @Autowired
+    private IFsUserCoursePeriodService fsUserCoursePeriodService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
 
     /**
      * 查询订单列表
@@ -237,6 +254,30 @@ public class FsStoreOrderScrmController extends BaseController
         FsStoreOrderScrm order=fsStoreOrderService.selectFsStoreOrderById(id);
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
+
+        if (order.getCompanyUserId() != null) {
+            CompanyUser companyUser = companyUserService.selectCompanyUserByUserId(order.getCompanyUserId());
+            Company company = companyService.selectCompanyById(companyUser.getCompanyId());
+            order.setCompanyUserName(companyUser.getUserName());
+            order.setCompanyName(company.getCompanyName());
+        } else if (order.getCompanyId() != null) {
+            Company company = companyService.selectCompanyById(order.getCompanyId());
+            order.setCompanyName(company.getCompanyName());
+        }
+        if (order.getOrderType() != null && order.getOrderType() == 3) {
+//            FsCourseWatchLogParam param = new FsCourseWatchLogParam();
+//            param.setVideoId(Long.valueOf(order.getVideoId()));
+//            FsCourseWatchLog log = fsCourseWatchLogService.selectFsCourseWatchLogWithUCCV(order.getUserId(), order.getCompanyUserId(), order.getCourseId(), order.getVideoId());
+            if (order.getPeriodId() != null) {
+                FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodService.selectFsUserCoursePeriodById(Long.valueOf(order.getPeriodId()));
+                order.setPeriodName(fsUserCoursePeriod.getPeriodName());
+            }
+            if (order.getVideoId() != null) {
+                FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoService.selectFsUserCourseVideoByVideoId(Long.valueOf(order.getVideoId()));
+                order.setVideoName(fsUserCourseVideo.getTitle());
+            }
+        }
+
         FsUser user=userService.selectFsUserById(order.getUserId());
         user.setPhone(ParseUtils.parsePhone(user.getPhone()));
         FsStoreOrderItemScrm itemMap=new FsStoreOrderItemScrm();

+ 4 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceRobotic.java

@@ -129,4 +129,8 @@ public class CompanyVoiceRobotic {
     //企微用户id
     @TableField(exist = false)
     private String qwUserId;
+    private Integer taskType;
+    private Integer sceneType;
+    private LocalTime availableStartTime;
+    private LocalTime availableEndTime;
 }

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

@@ -73,7 +73,7 @@ public interface CompanyWxClientMapper extends BaseMapper<CompanyWxClient> {
 
     List<CompanyWxClient> getAddWxList(@Param("accountIdList") List<Long> accountIdList, @Param("isWeCom") Integer isWeCom);
 
-    List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType);
+    List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(@Param("accountIdList") List<Long> accountIdList, @Param("execStatus") Integer execStatus, @Param("execNodeType") Integer execNodeType, @Param("cidGroupId") Integer cidGroupId);
 
     CompanyWxClient selectWx(@Param("accountId") Long accountId, @Param("v3") String v3);
 

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

@@ -71,7 +71,7 @@ public interface ICompanyWxClientService extends IService<CompanyWxClient> {
 
     List<CompanyWxClient> getAddWxList(List<Long> accountIdList,Integer isWeCom);
 
-    List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList);
+    List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList,Integer cidGroupId);
 
     List<CompanyWxClient> getQwAddWxList(List<Long> accountIdList,Integer isWeCom);
 

+ 17 - 19
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -31,6 +31,7 @@ import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 import com.fs.his.config.CidPhoneConfig;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
@@ -1046,29 +1047,24 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         if (robotic.getCompanyAiWorkflowId() == null) {
             throw new RuntimeException("任务未配置工作流: " + id);
         }
-
         robotic.setTaskStatus(1);
         updateById(robotic);
-
-        // 根据任务加微方式决定是否直接分配微信 平均时 直接分配用户
-        if (Integer.valueOf(0).equals(robotic.getAddType())) {
+        // 根据任务加微方式决定是否直接分配微信 平均时 直接分配用户 场景任务不做分配
+        if (Integer.valueOf(0).equals(robotic.getAddType()) && robotic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())) {
             allocateWx(robotic);
         }
-
         // 新增启动写入任务业务表数据
-        buildTaskBussiness(robotic);
-
-        // 查询业务列表
-        List<CompanyVoiceRoboticBusiness> roboticBusinesseList = companyVoiceRoboticBusinessMapper
-                .selectList(new QueryWrapper<CompanyVoiceRoboticBusiness>().eq("robotic_id", id).eq("is_generate",0));
-
-        if (roboticBusinesseList.isEmpty()) {
-            log.warn("任务没有业务数据: {}", id);
-            return;
+        if(robotic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+            buildTaskBussiness(robotic);
+            // 查询业务列表
+            List<CompanyVoiceRoboticBusiness> roboticBusinesseList = companyVoiceRoboticBusinessMapper
+                    .selectList(new QueryWrapper<CompanyVoiceRoboticBusiness>().eq("robotic_id", id).eq("is_generate",0));
+            if (roboticBusinesseList.isEmpty()) {
+                log.warn("任务没有业务数据: {}", id);
+                return;
+            }
+            initAndExecuteWorkflows(robotic, roboticBusinesseList);
         }
-
-        // 初始化并执行工作流
-        initAndExecuteWorkflows(robotic, roboticBusinesseList);
     }
 
     /**
@@ -1293,11 +1289,13 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
         for (WorkflowExecRecordVo record : records) {
             // 设置工作流状态名称
             if (record.getWorkflowStatus() != null) {
-                record.setWorkflowStatusName(getStatusName(record.getWorkflowStatus()));
+                ExecutionStatusEnum executionStatusEnum = ExecutionStatusEnum.fromValue(record.getWorkflowStatus());
+                record.setWorkflowStatusName( executionStatusEnum.getDescription());
             }
             // 设置节点类型名称
             if (record.getCurrentNodeType() != null) {
-                record.setCurrentNodeTypeName(getNodeTypeName(record.getCurrentNodeType()));
+                NodeTypeEnum nodeTypeEnum = NodeTypeEnum.fromValue(record.getCurrentNodeType());
+                record.setCurrentNodeTypeName(nodeTypeEnum.getDescription());
             }
             // 查询节点执行日志
             if (record.getWorkflowInstanceId() != null) {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/impl/CompanyWxClientServiceImpl.java

@@ -238,8 +238,8 @@ public class CompanyWxClientServiceImpl extends ServiceImpl<CompanyWxClientMappe
      * @return
      */
     @Override
-    public  List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList){
-        return baseMapper.getAddWxList4Workflow(accountIdList, ExecutionStatusEnum.PAUSED.getValue(), NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+    public  List<CompanyWxClient4WorkFlowVO> getAddWxList4Workflow(List<Long> accountIdList,Integer cidGroupId){
+        return baseMapper.getAddWxList4Workflow(accountIdList, ExecutionStatusEnum.PAUSED.getValue(), NodeTypeEnum.AI_ADD_WX_TASK.getValue(),cidGroupId);
     }
 
     @Override

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

@@ -171,6 +171,13 @@ public class AiCallTaskNode extends AbstractWorkflowNode {
                 if (bus == null) {
                     return ExecutionResult.failure().errorMessage("未找到业务数据").build();
                 }
+                //手动外呼配置 1、人工 2、ai外呼
+                if(Integer.valueOf(1).equals(callConfigVo.getCallMode())){
+                    super.asyncWorkflowForBlockingNode(context.getWorkflowInstanceId(), context.getCurrentNodeKey(), context, ExecutionStatusEnum.WAITING_DO_CALL);
+                    return ExecutionResult.paused()
+                            .outputData(context.getVariables())
+                            .nextNodeKey("").build();
+                }
 //                companyVoiceRoboticService.workflowCallPhoneOne(bus.getRoboticId(), bus.getCalleeId(), context, callConfigVo);
                 // EasyCallCenter365 外呼
                  workflowCallPhoneOne4EasyCall(bus.getRoboticId(),bus.getCalleeId(), context, callConfigVo);

+ 6 - 3
fs-service/src/main/java/com/fs/company/service/impl/call/node/EndNode.java

@@ -9,6 +9,7 @@ import com.fs.company.param.ExecutionContext;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
+import com.fs.enums.TaskTypeEnum;
 
 import java.util.Map;
 
@@ -51,9 +52,11 @@ public class EndNode extends AbstractWorkflowNode {
             Integer i = companyVoiceRoboticBusinessMapper.selectUnfinishedTaskCountByRoboticId(roboticBusiness.getRoboticId(), nodeKey);
             if(Integer.valueOf(0).equals(i)){
                 CompanyVoiceRobotic robotic = new CompanyVoiceRobotic();
-                robotic.setId(roboticBusiness.getRoboticId());
-                robotic.setTaskStatus(3);
-                companyVoiceRoboticMapper.updateById(robotic);
+                if(robotic.getTaskType().equals(TaskTypeEnum.ORDINARY.getValue())){
+                    robotic.setId(roboticBusiness.getRoboticId());
+                    robotic.setTaskStatus(3);
+                    companyVoiceRoboticMapper.updateById(robotic);
+                }
             }
         }
     }

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

@@ -60,4 +60,8 @@ public class AiCallConfigVO {
      */
     private String busiGroupId;
 
+    /**
+     * 外呼模式
+     */
+    private Integer callMode;
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/config/CourseConfig.java

@@ -19,6 +19,7 @@ public class CourseConfig implements Serializable {
     private Integer appAnswerIntegral; //app答题积分
     private Integer defaultLine;//默认看课线路
     private String realLinkDomainName;//真链域名
+    private String salesEaseCourseDomain; //销售易看课域名
     private String realLinkH5DomainName;//H5通用看课域名
     private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
@@ -47,6 +48,7 @@ public class CourseConfig implements Serializable {
     private Integer isNegative;//是否为负数 0、不允许,1、允许
 
     private Integer isOpen;
+    private Boolean completionCountdown;
 
     /**
      * 侧边栏是否仅展示当天课程

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java

@@ -138,4 +138,8 @@ public interface FsCourseAnswerLogsMapper
 
     @Select("select * from fs_course_answer_logs where video_id = #{videoId} and user_id = #{userId} and is_right = 1 limit 1")
     FsCourseAnswerLogs selectRightLogByCourseVideoIsOpen(@Param("videoId") Long videoId,@Param("userId") Long userId);
+
+    /** 统计指定视频+营期下去重答题人数 */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_answer_logs WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseRedPacketLogMapper.java

@@ -188,6 +188,10 @@ public interface FsCourseRedPacketLogMapper
     @Select("select * from fs_course_red_packet_log where video_id = #{videoId} and user_id = #{userId} and period_id = #{periodId} limit 1")
     FsCourseRedPacketLog selectUserFsCourseRedPacketLog(@Param("videoId") Long videoId, @Param("userId")Long userId, @Param("periodId")Long periodId);
 
+    /** 统计指定视频+营期下去重领红包人数 */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_red_packet_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctUsersByVideoAndPeriod(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
     @Select("SELECT * FROM fs_course_red_packet_log \n" +
             "WHERE create_time <= DATE_SUB(NOW(), INTERVAL 10 MINUTE)  -- 10 分钟前或更早\n" +
             "AND create_time >= DATE(NOW())  -- 但必须是今天\n" +

+ 64 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -755,4 +755,68 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
 
     List<FsSopMyCourseH5LinkVO> getSopCourseH5StudyListByQwExId(@Param("qwExternalId") Long qwExternalId);
 
+    @Select("select * from fs_course_watch_log where user_id=#{userId} and company_user_id=#{companyUserId} and course_id=#{courseId} and video_id=#{videoId} limit 1")
+    FsCourseWatchLog selectFsCourseWatchLogWithUCCV(@Param("userId") Long userId,@Param("companyUserId") Long companyUserId,@Param("courseId") Integer courseId,@Param("videoId") Integer videoId);
+
+    /**
+     * 查询视频时长(只返回duration字段)
+     * @param videoId 视频ID
+     * @return 视频时长(秒)
+     */
+    @Select("SELECT duration FROM fs_user_course_video WHERE video_id = #{videoId} AND is_del = 0 LIMIT 1")
+    Long selectVideoDurationByVideoId(@Param("videoId") Long videoId);
+
+    /**
+     * 统计累计观看人数(对userId去重)
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 累计观看人数
+     */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId}")
+    Long countDistinctWatchUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
+    /**
+     * 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 累计完课人数
+     */
+    @Select("SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log WHERE video_id = #{videoId} AND period_id = #{periodId} AND duration >= 1200")
+    Long countDistinctCompleteUsers(@Param("videoId") Long videoId, @Param("periodId") Long periodId);
+
+    /**
+     * 首次点播数据统计:观看人数、>=20分钟人数、>=30分钟人数
+     * 首次点播窗口 = [营期课程开始时间, 营期课程开始时间+视频时长](由XML内联计算)
+     * 用户观看开始时间 = COALESCE(finish_time - duration, update_time - duration, create_time)
+     *
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return Map: firstWatchCount, firstWatch20MinCount, firstWatch30MinCount
+     */
+    Map<String, Object> selectFirstPlaybackStats(@Param("videoId") Long videoId,
+                                                 @Param("periodId") Long periodId);
+
+    /**
+     * 第2-n次观看数据统计:view_start不在首次点播窗口内的观看记录
+     * 首次点播窗口 = [营期课程开始时间, 营期课程开始时间+视频时长]
+     * 第2-n次:view_start < 窗口开始 或 view_start >= 窗口结束
+     *
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return Map: repeatWatchCount, repeatWatch20MinCount, repeatWatch30MinCount
+     */
+    Map<String, Object> selectRepeatPlaybackStats(@Param("videoId") Long videoId,
+                                                 @Param("periodId") Long periodId);
+
+    /**
+     * 课程小结-用户详情列表(分页):按videoId+periodId查观看记录,区分首次/2-n次时长,关联订单及公司/销售
+     */
+    List<com.fs.course.vo.CourseStatisticsUserDetailVO> selectCourseStatisticsUserDetailList(
+            @Param("param") com.fs.course.param.CourseStatisticsUserDetailParam param);
+
+    /**
+     * 课程小结-用户详情导出:按创建时间倒序,最多50000条
+     */
+    List<com.fs.course.vo.CourseStatisticsUserDetailVO> selectCourseStatisticsUserDetailExportList(
+            @Param("param") com.fs.course.param.CourseStatisticsUserDetailParam param);
 }

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

@@ -133,4 +133,13 @@ public interface FsUserCoursePeriodDaysMapper extends BaseMapper<FsUserCoursePer
     List<Long> selectFsUserCoursePeriodDaysForLastById(FsUserCoursePeriodDays param);
 
     List<FsUserCoursePeriodDays> selectFsUserCoursePeriodDaysByCourseId(@Param("courseId") Long courseId);
+
+    /**
+     * 根据营期ID和视频ID查询营期课程开始时间(首次点播窗口起始)
+     * @param periodId 营期ID
+     * @param videoId 视频ID
+     * @return 开始时间,无则null
+     */
+    @Select("SELECT start_date_time FROM fs_user_course_period_days WHERE period_id = #{periodId} AND video_id = #{videoId} AND del_flag = '0' ORDER BY lesson ASC LIMIT 1")
+    LocalDateTime selectStartDateTimeByPeriodAndVideo(@Param("periodId") Long periodId, @Param("videoId") Long videoId);
 }

+ 24 - 0
fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java

@@ -0,0 +1,24 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 课程小结-用户详情查询参数
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsUserDetailParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 视频ID */
+    private Long videoId;
+    /** 营期ID */
+    private Long periodId;
+    /** 页码 */
+    private Integer pageNum = 1;
+    /** 每页条数 */
+    private Integer pageSize = 10;
+}

+ 27 - 0
fs-service/src/main/java/com/fs/course/param/newfs/FsUserCourseVideoRemainTimeParam.java

@@ -0,0 +1,27 @@
+package com.fs.course.param.newfs;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Data
+public class FsUserCourseVideoRemainTimeParam implements Serializable {
+    @NotNull(message = "视频id不能为空")
+    @ApiModelProperty(value = "视频id")
+    private Integer videoId;
+
+    @NotNull(message = "用户id不能为空")
+    @ApiModelProperty(value = "用户id")
+    private Long fsUserId;
+
+    @NotNull(message = "课程id不能为空")
+    @ApiModelProperty(value = "课程id")
+    private Integer courseId;
+
+    @NotNull(message = "销售id不能为空")
+    @ApiModelProperty(value = "销售id")
+    private Long companyUserId;
+
+}

+ 7 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseLinkService.java

@@ -70,6 +70,13 @@ public interface IFsCourseLinkService
 
     R createLinkUrl(FsCourseLinkCreateParam param);
 
+    /**
+     * 创建销售易链接
+     * @param param
+     * @return
+     */
+    R createSaleLink(FsCourseLinkCreateParam param);
+
     R createLinkUrlWc(FsCourseLinkCreateParam param);
 
     String createLinkUrlWcmini(FsCourseLinkCreateParam param);

+ 24 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -169,4 +169,28 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
     R decryptLink(String url);
 
     List<FsCourseWatchLog> selectFsUserWatchLogByExtId(QwExternalContact qwExternalContact);
+
+    FsCourseWatchLog selectFsCourseWatchLogWithUCCV(Long userId, Long companyUserId, Integer courseId, Integer videoId);
+
+    /**
+     * 查询课程小结详情总体数据
+     * @param videoId 视频ID
+     * @param periodId 营期ID
+     * @return 总体统计数据
+     */
+    CourseStatisticsDetailVO getCourseStatisticsDetail(Long videoId, Long periodId);
+
+    /**
+     * 查询课程小结-用户详情列表(分页)
+     * @param param 查询参数
+     * @return 用户详情列表
+     */
+    List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailList(CourseStatisticsUserDetailParam param);
+
+    /**
+     * 课程小结-用户详情导出:按创建时间倒序,最多50000条
+     * @param param 查询参数
+     * @return 用户详情列表
+     */
+    List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailExportList(CourseStatisticsUserDetailParam param);
 }

+ 50 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -214,6 +214,7 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
     private static final String realLink = "/courseH5/pages/course/learning?course=";
     private static final String shortLink = "/courseH5/pages/course/learning?s=";
     private static final String miniappRealLink = "/pages_course/video?course=";
+    private static final String saleLink = "/pages_course/video?course=";
 
 
     /**
@@ -269,6 +270,48 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         return R.error("短链生成失败!");
     }
 
+    @Override
+    public R createSaleLink(FsCourseLinkCreateParam param) {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        QwUser qwUser = qwUserMapper.selectQwUserById(param.getQwUserId());
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(param.getCompanyId());
+        link.setCompanyUserId(param.getCompanyUserId());
+        link.setVideoId(param.getVideoId());
+        link.setCourseId(param.getCourseId());
+        link.setLinkType(5);//销售易看课域名
+        link.setLink(generateRandomString());
+        link.setQwUserId(param.getQwUserId());
+        if(qwUser!=null){
+            link.setCorpId(qwUser.getCorpId());
+        }
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link,courseMap);
+        // 将实体类对象转换为 JSON 字符串
+        String course = JSON.toJSONString(courseMap);
+        link.setRealLink(saleLink+course);
+        link.setCreateTime(new Date());
+        Integer expireDays = 0;
+        if (param.getDays() == null || param.getDays() == 0){
+            expireDays = config.getVideoLinkExpireDate();
+        }else {
+            expireDays = param.getDays();
+        }
+        // 设置过期时间
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(link.getCreateTime());
+        calendar.add(Calendar.DAY_OF_MONTH, expireDays);
+        link.setUpdateTime(calendar.getTime());
+        if (fsCourseLinkMapper.insertFsCourseLink(link)>0){
+            String domainName=getSaleDomainName(param.getCompanyUserId(), config);
+            String sortLink = domainName + shortLink + link.getLink();
+
+            return R.ok("短链生成").put("url",sortLink);
+        }
+        return R.error("短链生成失败!");
+    }
+
     @Override
     public R  createRoomLinkUrl(FsCourseLinkCreateParam param) {
 //        QwUser qwUser;
@@ -415,6 +458,13 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         }
         return domainName;
     }
+    private  String getSaleDomainName(Long companyUserId,CourseConfig config){
+        String domainName = companyUserMapper.selectDomainByUserId(companyUserId);
+        if (StringUtils.isEmpty(domainName)){
+            domainName = config.getSalesEaseCourseDomain();
+        }
+        return domainName;
+    }
 
     private Date createUpdateTime(Integer expiresDays, Date sendTime, CourseConfig config){
 

+ 200 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -25,6 +25,12 @@ import com.fs.course.config.RedisKeyScanner;
 import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
 import com.fs.course.param.*;
+import com.fs.hisStore.domain.FsStoreOrderScrm;
+import com.fs.hisStore.dto.FsStoreCartDTO;
+import com.fs.hisStore.mapper.FsStoreOrderItemScrmMapper;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
@@ -162,6 +168,24 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
 
+    @Autowired
+    private FsUserCoursePeriodMapper fsUserCoursePeriodMapper;
+
+    @Autowired
+    private FsUserCoursePeriodDaysMapper fsUserCoursePeriodDaysMapper;
+
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+
+    @Autowired
+    private FsStoreOrderItemScrmMapper fsStoreOrderItemScrmMapper;
+
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
+    @Autowired
+    private FsCourseAnswerLogsMapper fsCourseAnswerLogsMapper;
+
     /**
      * 查询短链课程看课记录
      *
@@ -1724,4 +1748,180 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return fsCourseWatchLogMapper.selectFsUserWatchLogByExtId(qwExternalContact);
     }
 
+    @Override
+    public FsCourseWatchLog selectFsCourseWatchLogWithUCCV(Long userId, Long companyUserId, Integer courseId, Integer videoId) {
+        return fsCourseWatchLogMapper.selectFsCourseWatchLogWithUCCV(userId, companyUserId, courseId, videoId);
+    }
+
+    @Override
+    public CourseStatisticsDetailVO getCourseStatisticsDetail(Long videoId, Long periodId) {
+        CourseStatisticsDetailVO vo = new CourseStatisticsDetailVO();
+
+        // 总体数据
+        
+        // 1. 查询视频时长(只返回duration字段)
+        FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
+        vo.setVideoDuration(fsUserCourseVideo != null ? fsUserCourseVideo.getDuration() : 0L);
+
+        FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(periodId);
+
+
+        // 2. 统计累计观看人数(对userId去重)
+        Long totalWatchCount = fsCourseWatchLogMapper.countDistinctWatchUsers(videoId, periodId);
+        vo.setTotalWatchCount(totalWatchCount != null ? totalWatchCount : 0L);
+        
+        // 3. 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
+        Long totalCompleteCount = fsCourseWatchLogMapper.countDistinctCompleteUsers(videoId, periodId);
+        vo.setTotalCompleteCount(totalCompleteCount != null ? totalCompleteCount : 0L);
+        
+        // 4. 计算到课完课率 = 累计完课人数 / 累计观看人数
+        BigDecimal completeRate = BigDecimal.ZERO;
+        if (vo.getTotalWatchCount() != null && vo.getTotalWatchCount() > 0) {
+            completeRate = BigDecimal.valueOf(vo.getTotalCompleteCount())
+                    .divide(BigDecimal.valueOf(vo.getTotalWatchCount()), 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100));
+        }
+        vo.setCompleteRate(completeRate);
+
+        // 首次点播数据:营期开始时间+视频时长内的观看记录,view_start=update_time-duration 或 finish_time-duration(SQL内联计算窗口)
+        if (periodId != null && videoId != null) {
+            Map<String, Object> firstStats = fsCourseWatchLogMapper.selectFirstPlaybackStats(videoId, periodId);
+            if (firstStats != null && !firstStats.isEmpty()) {
+                Long firstWatch = getLongFromMap(firstStats, "firstWatchCount");
+                Long first20 = getLongFromMap(firstStats, "firstWatch20MinCount");
+                Long first30 = getLongFromMap(firstStats, "firstWatch30MinCount");
+                vo.setFirstWatchCount(firstWatch != null ? firstWatch : 0L);
+                vo.setFirstWatch20MinCount(first20 != null ? first20 : 0L);
+                vo.setFirstWatch30MinCount(first30 != null ? first30 : 0L);
+                if (firstWatch != null && firstWatch > 0) {
+                    vo.setFirstCompleteRate20Min(BigDecimal.valueOf(first20 != null ? first20 : 0)
+                            .divide(BigDecimal.valueOf(firstWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                    vo.setFirstCompleteRate30Min(BigDecimal.valueOf(first30 != null ? first30 : 0)
+                            .divide(BigDecimal.valueOf(firstWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                }
+            }
+        }
+
+        // 第2-n次观看数据:view_start不在首次点播窗口内的记录(窗口外=首次窗口前或窗口后)
+        if (periodId != null && videoId != null) {
+            Map<String, Object> repeatStats = fsCourseWatchLogMapper.selectRepeatPlaybackStats(videoId, periodId);
+            if (repeatStats != null && !repeatStats.isEmpty()) {
+                Long repeatWatch = getLongFromMap(repeatStats, "repeatWatchCount");
+                Long repeat20 = getLongFromMap(repeatStats, "repeatWatch20MinCount");
+                Long repeat30 = getLongFromMap(repeatStats, "repeatWatch30MinCount");
+                vo.setRepeatWatchCount(repeatWatch != null ? repeatWatch : 0L);
+                vo.setRepeatWatch20MinCount(repeat20 != null ? repeat20 : 0L);
+                vo.setRepeatWatch30MinCount(repeat30 != null ? repeat30 : 0L);
+                if (repeatWatch != null && repeatWatch > 0) {
+                    vo.setRepeatCompleteRate20Min(BigDecimal.valueOf(repeat20 != null ? repeat20 : 0)
+                            .divide(BigDecimal.valueOf(repeatWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                    vo.setRepeatCompleteRate30Min(BigDecimal.valueOf(repeat30 != null ? repeat30 : 0)
+                            .divide(BigDecimal.valueOf(repeatWatch), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100)));
+                }
+            }
+        }
+
+        // 订单数据:fs_store_order_scrm order_type=3,videoId+periodId 匹配,paid=1
+        if (periodId != null && videoId != null) {
+            FsStoreOrderScrm orderQuery = new FsStoreOrderScrm();
+            orderQuery.setOrderType(3);
+            orderQuery.setVideoId(videoId.intValue());
+            orderQuery.setPeriodId(periodId.intValue());
+            orderQuery.setPaid(1);
+            List<FsStoreOrderScrm> orders = fsStoreOrderScrmMapper.selectFsStoreOrderList(orderQuery);
+            List<FsStoreOrderScrm> paidOrders = orders != null ? orders.stream()
+                    .filter(o -> o.getPaid() != null && o.getPaid() == 1)
+                    .collect(Collectors.toList()) : Collections.emptyList();
+
+            BigDecimal gmv = paidOrders.stream()
+                    .map(FsStoreOrderScrm::getPayPrice)
+                    .filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            vo.setGmv(gmv);
+
+            long paidUserCount = paidOrders.stream()
+                    .filter(o -> o.getUserId() != null)
+                    .map(FsStoreOrderScrm::getUserId)
+                    .distinct()
+                    .count();
+            vo.setPaidUserCount(paidUserCount);
+            vo.setPaidOrderCount((long) paidOrders.size());
+
+            if (vo.getTotalWatchCount() != null && vo.getTotalWatchCount() > 0 && paidUserCount > 0) {
+                vo.setTotalPaidConversionRate(BigDecimal.valueOf(paidUserCount * 100.0 / vo.getTotalWatchCount()).setScale(2, RoundingMode.HALF_UP));
+            }
+            if (vo.getTotalCompleteCount() != null && vo.getTotalCompleteCount() > 0 && paidUserCount > 0) {
+                vo.setPaidConversionRate20Min(BigDecimal.valueOf(paidUserCount * 100.0 / vo.getTotalCompleteCount()).setScale(2, RoundingMode.HALF_UP));
+            }
+            if (vo.getTotalCompleteCount() != null && vo.getTotalCompleteCount() > 0 && gmv != null && gmv.compareTo(BigDecimal.ZERO) > 0) {
+                vo.setCompleteRValue(gmv.divide(BigDecimal.valueOf(vo.getTotalCompleteCount()), 2, RoundingMode.HALF_UP));
+            }
+
+            Long answerCount = fsCourseAnswerLogsMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId);
+            vo.setAnswerUserCount(answerCount != null ? answerCount : 0L);
+
+            Long redCount = fsCourseRedPacketLogMapper.countDistinctUsersByVideoAndPeriod(videoId, periodId);
+            vo.setRedPacketUserCount(redCount != null ? redCount : 0L);
+
+            // 单品销量统计:从订单明细汇总
+            Map<Long, CourseProductSalesVO> productSalesMap = new HashMap<>();
+            for (FsStoreOrderScrm order : paidOrders) {
+                // todo 数据量大的时候需要优化查询 外面批量查询 里面数据过滤
+                List<FsStoreOrderItemVO> items = fsStoreOrderItemScrmMapper.selectFsStoreOrderItemListByOrderId(order.getId());
+                if (items == null || items.isEmpty()) continue;
+                long totalNum = order.getTotalNum() != null && order.getTotalNum() > 0 ? order.getTotalNum() : 1;
+                BigDecimal orderPayPrice = order.getPayPrice() != null ? order.getPayPrice() : BigDecimal.ZERO;
+
+                for (FsStoreOrderItemVO item : items) {
+                    FsStoreCartDTO cartDTO = JSONUtil.toBean(item.getJsonInfo(), FsStoreCartDTO.class);
+                    if (item.getProductId() == null) continue;
+                    long itemNum = item.getNum() != null ? item.getNum() : 0;
+                    BigDecimal itemAmount = totalNum > 0 ? orderPayPrice.multiply(BigDecimal.valueOf(itemNum)).divide(BigDecimal.valueOf(totalNum), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+                    CourseProductSalesVO productSales = productSalesMap.computeIfAbsent(item.getProductId(), k -> {
+                        CourseProductSalesVO pvo = new CourseProductSalesVO();
+                        pvo.setProductName(cartDTO.getProductName());
+                        return pvo;
+                    });
+
+                    productSales.setSalesCount(productSales.getSalesCount() + itemNum);
+                    productSales.setSalesAmount(productSales.getSalesAmount().add(itemAmount));
+                }
+            }
+            List<CourseProductSalesVO> productList = new ArrayList<>(productSalesMap.values());
+            productList.sort((a, b) -> b.getSalesAmount().compareTo(a.getSalesAmount()));
+            vo.setProductList(productList);
+        }
+
+        
+        return vo;
+    }
+
+    @Override
+    public List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailList(CourseStatisticsUserDetailParam param) {
+        if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
+            return Collections.emptyList();
+        }
+        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailList(param);
+    }
+
+    @Override
+    public List<CourseStatisticsUserDetailVO> getCourseStatisticsUserDetailExportList(CourseStatisticsUserDetailParam param) {
+        if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
+            return Collections.emptyList();
+        }
+        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailExportList(param);
+    }
+
+    /**
+     * 从 Map 中安全获取 Long 值,兼容 MyBatis 返回的驼峰/小写键名
+     */
+    private Long getLongFromMap(Map<String, Object> map, String key) {
+        if (map == null || key == null) return null;
+        Object v = map.get(key);
+        if (v == null) v = map.get(key.toLowerCase());
+        if (v == null) return null;
+        if (v instanceof Number) return ((Number) v).longValue();
+        try { return Long.parseLong(String.valueOf(v)); } catch (NumberFormatException e) { return null; }
+    }
+
 }

+ 43 - 16
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -437,23 +437,50 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
 
     @Override
     public R updateCourseDate(UpdateCourseTimeVo vo) {
-        FsUserCoursePeriodDays day = getById(vo.getId());
-        FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(day.getPeriodId());
-        if(!DateUtil.isWithinRangeSafe(vo.getDayDate(), period.getPeriodStartingTime(), period.getPeriodEndTime())) return R.error("时间不在营期范围内");
-        day.setDayDate(vo.getDayDate());
-        day.setStartDateTime(LocalDateTime.of(day.getDayDate(), day.getStartDateTime().toLocalTime()));
-        day.setEndDateTime(LocalDateTime.of(day.getDayDate(), day.getEndDateTime().toLocalTime()));
-        day.setLastJoinTime(LocalDateTime.of(day.getDayDate(), day.getLastJoinTime().toLocalTime()));
-        // 默认开启今天及以后的两天,为进行中
-        LocalDateTime compareDayTime = LocalDateTime.now();
-        if(compareDayTime.isAfter(day.getStartDateTime()) && compareDayTime.isBefore(day.getEndDateTime())){
-            day.setStatus(1);
-        } else if(compareDayTime.isBefore(day.getStartDateTime())){
-            day.setStatus(0);
-        } else {
-            day.setStatus(2);
+        // 确定要更新的id列表:优先使用ids(批量),否则使用id(单个)
+        List<Long> idList = vo.getIds();
+        if (idList == null || idList.isEmpty()) {
+            if (vo.getId() == null) {
+                return R.error("请选择要修改的课程");
+            }
+            idList = java.util.Collections.singletonList(vo.getId());
+        }
+        // 必须提供开始和结束时间,且开始时间不能晚于结束时间
+        LocalDateTime startDateTime = vo.getStartDateTime();
+        LocalDateTime endDateTime = vo.getEndDateTime();
+        if (startDateTime == null || endDateTime == null) {
+            return R.error("请选择营期开始时间和结束时间");
+        }
+        if (!startDateTime.isBefore(endDateTime)) {
+            return R.error("开始时间必须早于结束时间");
+        }
+        LocalDate dayDate = startDateTime.toLocalDate();
+        for (Long dayId : idList) {
+            FsUserCoursePeriodDays day = getById(dayId);
+            if (day == null) {
+                continue;
+            }
+            FsUserCoursePeriod period = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(day.getPeriodId());
+            if (period == null) {
+                continue;
+            }
+            if (!DateUtil.isWithinRangeSafe(dayDate, period.getPeriodStartingTime(), period.getPeriodEndTime())) {
+                return R.error("营期时间不在营期范围内");
+            }
+            day.setDayDate(dayDate);
+            day.setStartDateTime(startDateTime);
+            day.setEndDateTime(endDateTime);
+            day.setLastJoinTime(endDateTime);
+            LocalDateTime compareDayTime = LocalDateTime.now();
+            if (compareDayTime.isAfter(day.getStartDateTime()) && compareDayTime.isBefore(day.getEndDateTime())) {
+                day.setStatus(1);
+            } else if (compareDayTime.isBefore(day.getStartDateTime())) {
+                day.setStatus(0);
+            } else {
+                day.setStatus(2);
+            }
+            updateById(day);
         }
-        updateById(day);
         return R.ok();
     }
 

+ 13 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -4596,6 +4596,19 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     fsStoreProductScrm.setBarCode(validProduct.getBarCode());
                     fsStoreProductScrm.setPrice(validProduct.getPrice());
                     fsStoreProductScrm.setProductName(validProduct.getProductName());
+
+
+
+                    String onShelfTime = originalNode != null ? originalNode.path("onShelfTime").asText("00:00:00") : "00:00:00";
+                    String offShelfTime = originalNode != null ? originalNode.path("offShelfTime").asText("00:00:00") : "00:00:00";
+                    String cardPopupTime = originalNode != null ? originalNode.path("cardPopupTime").asText("00:00:00") : "00:00:00";
+                    String cardCloseTime = originalNode != null ? originalNode.path("cardCloseTime").asText("00:00:00") : "00:00:00";
+                    fsStoreProductScrm.setOnShelfTime(onShelfTime);
+                    fsStoreProductScrm.setOffShelfTime(offShelfTime);
+                    fsStoreProductScrm.setCardPopupTime(cardPopupTime);
+                    fsStoreProductScrm.setCardCloseTime(cardCloseTime);
+
+
                     fsPackageListVOS.add(fsStoreProductScrm);
                 }
                 vo.setFsStoreProductScrms(fsPackageListVOS);

+ 23 - 0
fs-service/src/main/java/com/fs/course/vo/CourseProductSalesVO.java

@@ -0,0 +1,23 @@
+package com.fs.course.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程单品销量统计VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseProductSalesVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 商品名称 */
+    private String productName;
+    /** 商品销量 */
+    private Long salesCount = 0L;
+    /** 商品销售额 */
+    private BigDecimal salesAmount = BigDecimal.ZERO;
+}

+ 72 - 0
fs-service/src/main/java/com/fs/course/vo/CourseStatisticsDetailVO.java

@@ -0,0 +1,72 @@
+package com.fs.course.vo;
+
+import lombok.Data;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程小结详情统计VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsDetailVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 视频时长(秒) */
+    private Long videoDuration;
+
+    /** 累计观看人数 */
+    private Long totalWatchCount;
+
+    /** 累计完课人数 */
+    private Long totalCompleteCount;
+
+    /** 到课完课率(百分比,如:80.50 表示 80.50%) */
+    private BigDecimal completeRate;
+
+    // ========== 首次点播数据 ==========
+    /** 首次点播-观看人数(去重) */
+    private Long firstWatchCount;
+    /** 首次点播->=20分钟人数 */
+    private Long firstWatch20MinCount;
+    /** 首次点播->=30分钟人数 */
+    private Long firstWatch30MinCount;
+    /** 首次点播-到课完课率(>=20分钟) */
+    private BigDecimal firstCompleteRate20Min;
+    /** 首次点播-到课完课率(>=30分钟) */
+    private BigDecimal firstCompleteRate30Min;
+
+    // ========== 第2-n次观看数据 ==========
+    /** 第2-n次-观看人数(去重) */
+    private Long repeatWatchCount;
+    /** 第2-n次->=20分钟人数 */
+    private Long repeatWatch20MinCount;
+    /** 第2-n次->=30分钟人数 */
+    private Long repeatWatch30MinCount;
+    /** 第2-n次-到课完课率(>=20分钟) */
+    private BigDecimal repeatCompleteRate20Min;
+    /** 第2-n次-到课完课率(>=30分钟) */
+    private BigDecimal repeatCompleteRate30Min;
+
+    // ========== 订单数据(fs_store_order_scrm order_type=3,videoId+periodId 匹配) ==========
+    /** GMV=付款订单总金额 */
+    private BigDecimal gmv;
+    /** 付费人数=实际支付下单商品的人数(去重) */
+    private Long paidUserCount;
+    /** 付费单数=付费订单总数 */
+    private Long paidOrderCount;
+    /** 总付费转化率=付费人数/累计观看人数 */
+    private BigDecimal totalPaidConversionRate;
+    /** 20min付费转化率=付费人数/观看时长>=20分钟人数 */
+    private BigDecimal paidConversionRate20Min;
+    /** 完课R值=GMV/完课人数 */
+    private BigDecimal completeRValue;
+    /** 累计答题人数(去重) */
+    private Long answerUserCount;
+    /** 领红包人数(去重) */
+    private Long redPacketUserCount;
+    /** 单品销量统计 */
+    private java.util.List<CourseProductSalesVO> productList;
+}

+ 34 - 0
fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java

@@ -0,0 +1,34 @@
+package com.fs.course.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 课程小结-用户详情VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class CourseStatisticsUserDetailVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+    /** 用户ID */
+    private Long userId;
+    @Excel(name = "用户名称")
+    private String userName;
+    @Excel(name = "观看时长(秒)")
+    private Long watchDuration;
+    @Excel(name = "第2-n次观看时长(秒)")
+    private Long repeatWatchDuration;
+    @Excel(name = "订单数")
+    private Long orderCount;
+    @Excel(name = "订单金额")
+    private BigDecimal orderAmount;
+    @Excel(name = "分公司名称")
+    private String companyName;
+    @Excel(name = "销售名称")
+    private String salesName;
+}

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

@@ -12,6 +12,7 @@ import java.util.List;
 @Data
 public class UpdateCourseTimeVo {
 
+    /** 批量修改营期时间的id列表,与id二选一 */
     private List<Long> ids;
     private Long id;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -22,6 +23,13 @@ public class UpdateCourseTimeVo {
     private LocalDateTime joinTime;
     private LocalDate dayDate;
 
+    /** 修改营期时间:开始时间(与endDateTime成对使用,修改后与结束时间保持一致) */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startDateTime;
+    /** 修改营期时间:结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime endDateTime;
+
     // 批量修改开关 0 关闭 1 开启 默认关闭 打开的话修改时间,后续的时间会一起改变
     private Integer batchUpdateSwitch;
 }

+ 38 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomerProperty.java

@@ -0,0 +1,38 @@
+package com.fs.crm.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CrmCustomerProperty extends BaseEntityTow {
+
+    @Excel(name = "客户ID")
+    private Long customerId;
+
+    @Excel(name = "字段ID")
+    private Long propertyId;
+
+    @Excel(name = "字段名称")
+    private String propertyName;
+
+    @Excel(name = "字段内容")
+    private String propertyValue;
+
+    @Excel(name = "字段类型")
+    private String propertyValueType;
+
+    @Excel(name = "行业类型")
+    private String tradeType;
+
+    @Excel(name = "内容解析")
+    private String aiAnalysis;
+
+    @Excel(name = "意向登记")
+    private String intention;
+
+    @Excel(name = "喜欢占比")
+    private Integer likeRatio;
+}

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

@@ -0,0 +1,25 @@
+package com.fs.crm.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntityTow;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CrmCustomerPropertyTemplate extends BaseEntityTow {
+
+    @Excel(name = "属性名称")
+    private String name;
+
+    @Excel(name = "属性类型")
+    private String valueType;
+
+    @Excel(name = "AI属性提取提示")
+    private String aiHint;
+
+    @Excel(name = "行业类型")
+    private String tradeType;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyMapper.java

@@ -0,0 +1,28 @@
+package com.fs.crm.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.crm.domain.CrmCustomerProperty;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CrmCustomerPropertyMapper extends BaseMapper<CrmCustomerProperty> {
+
+    CrmCustomerProperty selectCrmCustomerPropertyById(Long id);
+
+    List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty);
+
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+
+    CrmCustomerProperty selectByCustomerIdAndPropertyId(@Param("customerId") Long customerId, @Param("propertyId") Long propertyId);
+
+    int insertCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
+
+    int updateCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
+
+    int deleteCrmCustomerPropertyById(Long id);
+
+    int deleteCrmCustomerPropertyByIds(Long[] ids);
+
+    int deleteByCustomerIdAndPropertyId(@Param("customerId") Long customerId, @Param("propertyId") Long propertyId);
+}

+ 22 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerPropertyTemplateMapper.java

@@ -0,0 +1,22 @@
+package com.fs.crm.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CrmCustomerPropertyTemplateMapper extends BaseMapper<CrmCustomerPropertyTemplate> {
+
+    CrmCustomerPropertyTemplate selectCrmCustomerPropertyTemplateById(Long id);
+
+    List<CrmCustomerPropertyTemplate> selectCrmCustomerPropertyTemplateList(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int insertCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int updateCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int deleteCrmCustomerPropertyTemplateById(Long id);
+
+    int deleteCrmCustomerPropertyTemplateByIds(Long[] ids);
+}

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

@@ -0,0 +1,38 @@
+package com.fs.crm.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.crm.domain.CrmCustomerProperty;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ICrmCustomerPropertyService extends IService<CrmCustomerProperty> {
+
+    CrmCustomerProperty selectCrmCustomerPropertyById(Long id);
+
+    List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty);
+
+    int insertCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
+
+    int updateCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty);
+
+    int deleteCrmCustomerPropertyByIds(Long[] ids);
+
+    int deleteCrmCustomerPropertyById(Long id);
+
+    List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId);
+
+    int addCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy);
+
+    int addOrUpdateCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy);
+
+    int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy);
+
+    int batchAddCustomerProperties(Long customerId, List<CrmCustomerProperty> properties);
+
+    int addPropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy);
+
+    int addOrUpdatePropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy);
+
+    int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy);
+}

+ 21 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerPropertyTemplateService.java

@@ -0,0 +1,21 @@
+package com.fs.crm.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+
+import java.util.List;
+
+public interface ICrmCustomerPropertyTemplateService extends IService<CrmCustomerPropertyTemplate> {
+
+    CrmCustomerPropertyTemplate selectCrmCustomerPropertyTemplateById(Long id);
+
+    List<CrmCustomerPropertyTemplate> selectCrmCustomerPropertyTemplateList(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int insertCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int updateCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate);
+
+    int deleteCrmCustomerPropertyTemplateByIds(Long[] ids);
+
+    int deleteCrmCustomerPropertyTemplateById(Long id);
+}

+ 193 - 0
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyServiceImpl.java

@@ -0,0 +1,193 @@
+package com.fs.crm.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.crm.domain.CrmCustomerProperty;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.mapper.CrmCustomerPropertyMapper;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class CrmCustomerPropertyServiceImpl extends ServiceImpl<CrmCustomerPropertyMapper, CrmCustomerProperty> implements ICrmCustomerPropertyService {
+
+    @Autowired
+    private ICrmCustomerPropertyTemplateService propertyTemplateService;
+
+    @Override
+    public CrmCustomerProperty selectCrmCustomerPropertyById(Long id) {
+        return baseMapper.selectCrmCustomerPropertyById(id);
+    }
+
+    @Override
+    public List<CrmCustomerProperty> selectCrmCustomerPropertyList(CrmCustomerProperty crmCustomerProperty) {
+        return baseMapper.selectCrmCustomerPropertyList(crmCustomerProperty);
+    }
+
+    @Override
+    public int insertCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty) {
+        crmCustomerProperty.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCrmCustomerProperty(crmCustomerProperty);
+    }
+
+    @Override
+    public int updateCrmCustomerProperty(CrmCustomerProperty crmCustomerProperty) {
+        crmCustomerProperty.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateCrmCustomerProperty(crmCustomerProperty);
+    }
+
+    @Override
+    public int deleteCrmCustomerPropertyByIds(Long[] ids) {
+        return baseMapper.deleteCrmCustomerPropertyByIds(ids);
+    }
+
+    @Override
+    public int deleteCrmCustomerPropertyById(Long id) {
+        return baseMapper.deleteCrmCustomerPropertyById(id);
+    }
+
+    @Override
+    public List<CrmCustomerProperty> selectCrmCustomerPropertyByCustomerId(Long customerId) {
+        return baseMapper.selectCrmCustomerPropertyByCustomerId(customerId);
+    }
+
+    @Override
+    public int addCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy) {
+        CrmCustomerProperty property = new CrmCustomerProperty();
+        property.setCustomerId(customerId);
+        property.setPropertyId(propertyId);
+        property.setPropertyName(propertyName);
+        property.setPropertyValue(propertyValue);
+        property.setPropertyValueType(propertyValueType);
+        property.setTradeType(tradeType);
+        property.setCreateBy(createBy);
+        property.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCrmCustomerProperty(property);
+    }
+
+    @Override
+    public int addOrUpdateCustomerProperty(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String createBy) {
+        CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
+        if (existProperty != null) {
+            existProperty.setPropertyValue(propertyValue);
+            existProperty.setPropertyName(propertyName);
+            existProperty.setPropertyValueType(propertyValueType);
+            existProperty.setTradeType(tradeType);
+            existProperty.setUpdateBy(createBy);
+            existProperty.setUpdateTime(DateUtils.getNowDate());
+            return baseMapper.updateCrmCustomerProperty(existProperty);
+        } else {
+            return addCustomerProperty(customerId, propertyId, propertyName, propertyValue, propertyValueType, tradeType, createBy);
+        }
+    }
+
+    public int addOrUpdateCustomerPropertyWithExtra(Long customerId, Long propertyId, String propertyName, String propertyValue, String propertyValueType, String tradeType, String intention, Integer likeRatio, String createBy) {
+        String autoIntention = calculateIntentionByLikeRatio(likeRatio, intention);
+        
+        CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, propertyId);
+        if (existProperty != null) {
+            existProperty.setPropertyValue(propertyValue);
+            existProperty.setPropertyName(propertyName);
+            existProperty.setPropertyValueType(propertyValueType);
+            existProperty.setTradeType(tradeType);
+            existProperty.setIntention(autoIntention);
+            existProperty.setLikeRatio(likeRatio);
+            existProperty.setUpdateBy(createBy);
+            existProperty.setUpdateTime(DateUtils.getNowDate());
+            return baseMapper.updateCrmCustomerProperty(existProperty);
+        } else {
+            CrmCustomerProperty property = new CrmCustomerProperty();
+            property.setCustomerId(customerId);
+            property.setPropertyId(propertyId);
+            property.setPropertyName(propertyName);
+            property.setPropertyValue(propertyValue);
+            property.setPropertyValueType(propertyValueType);
+            property.setTradeType(tradeType);
+            property.setIntention(autoIntention);
+            property.setLikeRatio(likeRatio);
+            property.setCreateBy(createBy);
+            property.setCreateTime(DateUtils.getNowDate());
+            return baseMapper.insertCrmCustomerProperty(property);
+        }
+    }
+    
+    private String calculateIntentionByLikeRatio(Integer likeRatio, String intention) {
+        if (likeRatio == null) {
+            return intention;
+        }
+        
+        if (likeRatio >= 80) {
+            return "high";
+        } else if (likeRatio >= 50) {
+            return "medium";
+        } else if (likeRatio >= 20) {
+            return "low";
+        } else {
+            return "none";
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchAddCustomerProperties(Long customerId, List<CrmCustomerProperty> properties) {
+        int count = 0;
+        for (CrmCustomerProperty property : properties) {
+            property.setCustomerId(customerId);
+            property.setCreateTime(DateUtils.getNowDate());
+            if (property.getPropertyId() != null) {
+                CrmCustomerProperty existProperty = baseMapper.selectByCustomerIdAndPropertyId(customerId, property.getPropertyId());
+                if (existProperty != null) {
+                    existProperty.setPropertyValue(property.getPropertyValue());
+                    existProperty.setPropertyName(property.getPropertyName());
+                    existProperty.setPropertyValueType(property.getPropertyValueType());
+                    existProperty.setTradeType(property.getTradeType());
+                    existProperty.setUpdateBy(property.getCreateBy());
+                    existProperty.setUpdateTime(DateUtils.getNowDate());
+                    count += baseMapper.updateCrmCustomerProperty(existProperty);
+                    continue;
+                }
+            }
+            count += baseMapper.insertCrmCustomerProperty(property);
+        }
+        return count;
+    }
+
+    @Override
+    public int addPropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy) {
+        CrmCustomerPropertyTemplate template = propertyTemplateService.selectCrmCustomerPropertyTemplateById(propertyTemplateId);
+        if (template == null) {
+            return 0;
+        }
+        return addCustomerProperty(customerId, propertyTemplateId, template.getName(), propertyValue, template.getValueType(), template.getTradeType(), createBy);
+    }
+
+    @Override
+    public int addOrUpdatePropertyByTemplateId(Long customerId, Long propertyTemplateId, String propertyValue, String createBy) {
+        CrmCustomerPropertyTemplate template = propertyTemplateService.selectCrmCustomerPropertyTemplateById(propertyTemplateId);
+        if (template == null) {
+            return 0;
+        }
+        return addOrUpdateCustomerProperty(customerId, propertyTemplateId, template.getName(), propertyValue, template.getValueType(), template.getTradeType(), createBy);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchAddPropertiesByTemplateIds(Long customerId, Map<Long, String> propertyMap, String createBy) {
+        int count = 0;
+        for (Map.Entry<Long, String> entry : propertyMap.entrySet()) {
+            Long templateId = entry.getKey();
+            String propertyValue = entry.getValue();
+            int result = addOrUpdatePropertyByTemplateId(customerId, templateId, propertyValue, createBy);
+            if (result > 0) {
+                count++;
+            }
+        }
+        return count;
+    }
+}

+ 46 - 0
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerPropertyTemplateServiceImpl.java

@@ -0,0 +1,46 @@
+package com.fs.crm.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.mapper.CrmCustomerPropertyTemplateMapper;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class CrmCustomerPropertyTemplateServiceImpl extends ServiceImpl<CrmCustomerPropertyTemplateMapper, CrmCustomerPropertyTemplate> implements ICrmCustomerPropertyTemplateService {
+
+    @Override
+    public CrmCustomerPropertyTemplate selectCrmCustomerPropertyTemplateById(Long id) {
+        return baseMapper.selectCrmCustomerPropertyTemplateById(id);
+    }
+
+    @Override
+    public List<CrmCustomerPropertyTemplate> selectCrmCustomerPropertyTemplateList(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        return baseMapper.selectCrmCustomerPropertyTemplateList(crmCustomerPropertyTemplate);
+    }
+
+    @Override
+    public int insertCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        crmCustomerPropertyTemplate.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate);
+    }
+
+    @Override
+    public int updateCrmCustomerPropertyTemplate(CrmCustomerPropertyTemplate crmCustomerPropertyTemplate) {
+        crmCustomerPropertyTemplate.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateCrmCustomerPropertyTemplate(crmCustomerPropertyTemplate);
+    }
+
+    @Override
+    public int deleteCrmCustomerPropertyTemplateByIds(Long[] ids) {
+        return baseMapper.deleteCrmCustomerPropertyTemplateByIds(ids);
+    }
+
+    @Override
+    public int deleteCrmCustomerPropertyTemplateById(Long id) {
+        return baseMapper.deleteCrmCustomerPropertyTemplateById(id);
+    }
+}

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

@@ -53,7 +53,12 @@ public enum ExecutionStatusEnum {
     /**
      * 待处理
      */
-    PENDING("PENDING", "待处理", 10);
+    PENDING("PENDING", "待处理", 10),
+
+    /**
+     * 等待人工外呼
+     */
+    WAITING_DO_CALL("WAITINGDOCALL", "等待人工外呼", 11);
 
     private final String code;
     private final String description;

+ 64 - 0
fs-service/src/main/java/com/fs/enums/TaskTypeEnum.java

@@ -0,0 +1,64 @@
+package com.fs.enums;
+
+/**
+ * @author MixLiu
+ * @date 2026/1/28 10:35
+ * @description
+ */
+public enum TaskTypeEnum {
+    /**
+     * 成功
+     */
+    ORDINARY("ORDINARY", "普通任务", 1),
+
+    /**
+     * 失败
+     */
+    SCENE("SCENE", "场景任务", 2);
+
+    private final String code;
+    private final String description;
+    private final int value;
+
+    TaskTypeEnum(String code, String description, int value) {
+        this.code = code;
+        this.description = description;
+        this.value = value;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * 根据状态码获取枚举值
+     */
+    public static TaskTypeEnum fromCode(String code) {
+        for (TaskTypeEnum status : values()) {
+            if (status.code.equals(code)) {
+                return status;
+            }
+        }
+        throw new IllegalArgumentException("Unknown ExecutionStatus code: " + code);
+    }
+
+    /**
+     * 根据int值获取枚举值
+     */
+    public static TaskTypeEnum fromValue(int value) {
+        for (TaskTypeEnum status : values()) {
+            if (status.value == value) {
+                return status;
+            }
+        }
+        throw new IllegalArgumentException("Unknown ExecutionStatus value: " + value);
+    }
+}

+ 39 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserPlayer.java

@@ -0,0 +1,39 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 用户游戏记录对象 fs_user_player
+ *
+ * @author fs
+ * @date 2026-02-25
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserPlayer extends BaseEntity{
+
+    /** 主键id */
+    @TableId
+    private Long id;
+
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 游戏类型 */
+    @Excel(name = "游戏类型")
+    private Integer gameId;
+
+    /** 等级 */
+    @Excel(name = "等级")
+    private Integer level;
+
+    /** 星星数 */
+    @Excel(name = "星星数")
+    private String star;
+
+
+}

+ 67 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserPlayerMapper.java

@@ -0,0 +1,67 @@
+package com.fs.his.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsUserPlayer;
+import com.fs.his.vo.FsUserPlayerVo;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 用户游戏记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-02-25
+ */
+public interface FsUserPlayerMapper extends BaseMapper<FsUserPlayer>{
+    /**
+     * 查询用户游戏记录
+     *
+     * @param id 用户游戏记录主键
+     * @return 用户游戏记录
+     */
+    FsUserPlayer selectFsUserPlayerById(Long id);
+
+    /**
+     * 查询用户游戏记录列表
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 用户游戏记录集合
+     */
+    List<FsUserPlayer> selectFsUserPlayerList(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 新增用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    int insertFsUserPlayer(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 修改用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    int updateFsUserPlayer(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 删除用户游戏记录
+     *
+     * @param id 用户游戏记录主键
+     * @return 结果
+     */
+    int deleteFsUserPlayerById(Long id);
+
+    /**
+     * 批量删除用户游戏记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserPlayerByIds(Long[] ids);
+
+    FsUserPlayerVo selectFsUserByUserIdAndGameId(@Param("userId") Long userId,@Param("gameId") Integer gameId);
+
+    int updateFsUserPlayerByUserIdAndGameId(FsUserPlayer fsUserPlayer);
+}

+ 67 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserPlayerService.java

@@ -0,0 +1,67 @@
+package com.fs.his.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserPlayer;
+import com.fs.his.vo.FsUserPlayerVo;
+
+/**
+ * 用户游戏记录Service接口
+ *
+ * @author fs
+ * @date 2026-02-25
+ */
+public interface IFsUserPlayerService extends IService<FsUserPlayer>{
+    /**
+     * 查询用户游戏记录
+     *
+     * @param id 用户游戏记录主键
+     * @return 用户游戏记录
+     */
+    FsUserPlayer selectFsUserPlayerById(Long id);
+
+    /**
+     * 查询用户游戏记录列表
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 用户游戏记录集合
+     */
+    List<FsUserPlayer> selectFsUserPlayerList(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 新增用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    int insertFsUserPlayer(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 修改用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    int updateFsUserPlayer(FsUserPlayer fsUserPlayer);
+
+    /**
+     * 批量删除用户游戏记录
+     *
+     * @param ids 需要删除的用户游戏记录主键集合
+     * @return 结果
+     */
+    int deleteFsUserPlayerByIds(Long[] ids);
+
+    /**
+     * 删除用户游戏记录信息
+     *
+     * @param id 用户游戏记录主键
+     * @return 结果
+     */
+    int deleteFsUserPlayerById(Long id);
+
+    FsUserPlayerVo selectFsUserByUserIdAndGameId(Long userId,Integer gameId);
+
+    int updateFsUserPlayerByUserIdAndGameId(FsUserPlayer fsUserPlayer);
+}

+ 131 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserPlayerServiceImpl.java

@@ -0,0 +1,131 @@
+package com.fs.his.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserPlayer;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.mapper.FsUserPlayerMapper;
+import com.fs.his.service.IFsUserPlayerService;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.vo.FsUserPlayerVo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 用户游戏记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-02-25
+ */
+@Service
+public class FsUserPlayerServiceImpl extends ServiceImpl<FsUserPlayerMapper, FsUserPlayer> implements IFsUserPlayerService {
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+
+    /**
+     * 查询用户游戏记录
+     *
+     * @param id 用户游戏记录主键
+     * @return 用户游戏记录
+     */
+    @Override
+    public FsUserPlayer selectFsUserPlayerById(Long id)
+    {
+        return baseMapper.selectFsUserPlayerById(id);
+    }
+
+    /**
+     * 查询用户游戏记录列表
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 用户游戏记录
+     */
+    @Override
+    public List<FsUserPlayer> selectFsUserPlayerList(FsUserPlayer fsUserPlayer)
+    {
+        return baseMapper.selectFsUserPlayerList(fsUserPlayer);
+    }
+
+    /**
+     * 新增用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserPlayer(FsUserPlayer fsUserPlayer)
+    {
+        fsUserPlayer.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsUserPlayer(fsUserPlayer);
+    }
+
+    /**
+     * 修改用户游戏记录
+     *
+     * @param fsUserPlayer 用户游戏记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserPlayer(FsUserPlayer fsUserPlayer)
+    {
+        fsUserPlayer.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsUserPlayer(fsUserPlayer);
+    }
+
+    /**
+     * 批量删除用户游戏记录
+     *
+     * @param ids 需要删除的用户游戏记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserPlayerByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsUserPlayerByIds(ids);
+    }
+
+    /**
+     * 删除用户游戏记录信息
+     *
+     * @param id 用户游戏记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserPlayerById(Long id)
+    {
+        return baseMapper.deleteFsUserPlayerById(id);
+    }
+
+    @Override
+    public FsUserPlayerVo selectFsUserByUserIdAndGameId(Long userId,Integer gameId) {
+        if (userId == null || gameId == null) {
+            return null;
+        }
+        FsUserPlayerVo fsUserPlayerVo = baseMapper.selectFsUserByUserIdAndGameId(userId, gameId);
+        if(fsUserPlayerVo == null){
+            //查询用户是否存在
+            FsUser fsUser = fsUserMapper.selectFsUserByUserId(userId);
+            if(fsUser == null){
+                return null;
+            }
+            FsUserPlayer fsUserPlayer = new FsUserPlayer();
+            fsUserPlayer.setGameId(gameId);
+            fsUserPlayer.setUserId(userId);
+            fsUserPlayer.setLevel(1); //等级
+            fsUserPlayer.setStar("{}"); //星星数
+            insertFsUserPlayer(fsUserPlayer);
+            fsUserPlayerVo = baseMapper.selectFsUserByUserIdAndGameId(userId, gameId);
+        }
+        return fsUserPlayerVo;
+    }
+
+    @Override
+    public int updateFsUserPlayerByUserIdAndGameId(FsUserPlayer fsUserPlayer) {
+        fsUserPlayer.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsUserPlayerByUserIdAndGameId(fsUserPlayer);
+    }
+}

+ 12 - 0
fs-service/src/main/java/com/fs/his/vo/FsUserPlayerVo.java

@@ -0,0 +1,12 @@
+package com.fs.his.vo;
+
+import com.fs.his.domain.FsUserPlayer;
+import lombok.Data;
+
+@Data
+public class FsUserPlayerVo extends FsUserPlayer {
+    private String nickName;
+    private String avatar;
+    private Long integral;
+
+}

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

@@ -358,6 +358,15 @@ public class FsStoreOrderScrm extends BaseEntity
     @TableField(exist = false)
     private Boolean isLive = false;
 
+    @TableField(exist = false)
+    private String companyUserName;
+    @TableField(exist = false)
+    private String companyName;
+    @TableField(exist = false)
+    private String periodName;
+    @TableField(exist = false)
+    private String videoName;
+
      // 是否审核,1-是,0-否
     private Integer isAudit;
 
@@ -373,4 +382,8 @@ public class FsStoreOrderScrm extends BaseEntity
     private Integer videoId;
     //课程ID
     private Integer courseId;
+    // 项目ID
+    private Integer projectId;
+    // 营期ID
+    private Integer periodId;
 }

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -350,6 +350,18 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "限购数量")
     private Integer purchaseLimit;
 
+    @TableField(exist = false)
+    private String onShelfTime;
+
+    @TableField(exist = false)
+    private String cardPopupTime;
+
+    @TableField(exist = false)
+    private String cardCloseTime;
+
+    @TableField(exist = false)
+    private String offShelfTime;
+
     /** 过滤商品id */
     private Long[] excludeProductIds;
 }

+ 8 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -140,9 +140,12 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
             "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
             "</if>" +
-            "<if test = 'maps.orderType != null    '> " +
+            "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
+            "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
+            "and o.order_type in (2, 3) " +
+            "</if>" +
             "<if test = 'maps.createTimeList != null    '> " +
             " AND date_format(o.create_time,'%y%m%d') &gt;= date_format(#{maps.createTimeList[0]},'%y%m%d') " +
             " AND date_format(o.create_time,'%y%m%d') &lt;= date_format(#{maps.createTimeList[1]},'%y%m%d') " +
@@ -251,9 +254,12 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
             "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
             "</if>" +
-            "<if test = 'maps.orderType != null    '> " +
+            "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
+            "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
+            "and o.order_type in (2, 3) " +
+            "</if>" +
             "<if test = 'maps.createTimeList != null    '> " +
             " AND date_format(o.create_time,'%y%m%d') &gt;= date_format(#{maps.createTimeList[0]},'%y%m%d') " +
             " AND date_format(o.create_time,'%y%m%d') &lt;= date_format(#{maps.createTimeList[1]},'%y%m%d') " +

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

@@ -61,4 +61,8 @@ public class FsStoreOrderCreateParam implements Serializable
     private Integer videoId;
     //课程ID
     private Integer courseId;
+    //项目ID
+    private Integer projectId;
+    //营期ID
+    private Integer periodId;
 }

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

@@ -912,6 +912,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if ("北京卓美".equals(companyName) && param.getVideoId()!=null){
                 storeOrder.setVideoId(param.getVideoId());
                 storeOrder.setCourseId(param.getCourseId());
+                storeOrder.setPeriodId(param.getPeriodId());
+                storeOrder.setProjectId(param.getProjectId());
             }
             String json = configService.selectConfigByKey("store.config");
             StoreConfig config= JSONUtil.toBean(json, StoreConfig.class);

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

@@ -681,6 +681,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     @Transactional
     public R addOrEdit(FsStoreProductAddEditParam param) {
         ProductAttrCountDto countDto=computedProductCount(param.getValues());
+        Date nowDate = DateUtils.getNowDate();
         FsStoreProductScrm product=new FsStoreProductScrm();
         BeanUtils.copyProperties(param,product);
         product.setPrice(countDto.getMinPrice());
@@ -744,6 +745,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
                 //默认已审核
                 product.setIsAudit("1");
             }
+            product.setUpdateTime(nowDate);
             fsStoreProductMapper.updateFsStoreProduct(product);
             // 清除缓存
             clearProductDetailCache(product.getProductId());
@@ -761,6 +763,8 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
             }
         }
         else{
+            product.setCreateTime(nowDate);
+            product.setUpdateTime(nowDate);
             fsStoreProductMapper.insertFsStoreProduct(product);
         }
         storeAuditLogUtil.addOperLog(product.getProductId());
@@ -1248,6 +1252,7 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         int failureNum = 0;
         StringBuilder successMsg = new StringBuilder();
         StringBuilder failureMsg = new StringBuilder();
+        Date nowDate = DateUtils.getNowDate();
         for (FsStoreProductExportVO productVO : list){
             try
             {
@@ -1255,6 +1260,8 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
                 if (product.getBarCode()==null || product.getBarCode()==""){
                     throw new CustomException("商品编号为空");
                 }
+                product.setCreateTime(nowDate);
+                product.setUpdateTime(nowDate);
                 this.insertFsStoreProduct(product);
                 ProductArrtDTO formatDetailDto = ProductArrtDTO.builder()
                         .value("规格")
@@ -1647,11 +1654,13 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         // 查询原商品的规格属性与属性值
         List<FsStoreProductAttrScrm> attrList = fsStoreProductAttrMapper.selectFsStoreProductAttrByProductId(productId);
         List<FsStoreProductAttrValueScrm> attrValueList = fsStoreProductAttrValueMapper.selectFsStoreProductAttrValueByProductId(productId);
-
+        Date nowDate = DateUtils.getNowDate();
         FsStoreProductScrm copy = new FsStoreProductScrm();
         BeanUtils.copyProperties(fsStoreProductScrm, copy);
         copy.setProductId(null);
         copy.setIsAudit("0");
+        copy.setCreateTime(nowDate);
+        copy.setUpdateTime(nowDate);
         fsStoreProductMapper.insertFsStoreProduct(copy);
 
         // 复制规格属性

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

@@ -1,5 +1,6 @@
 package com.fs.hisStore.service.impl;
 
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
@@ -15,11 +16,14 @@ import com.github.pagehelper.PageHelper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 /**
@@ -40,6 +44,14 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
     @Autowired
     private FsStoreProductTagRelationScrmMapper productTagRelationMapper;
 
+    @Autowired
+    private RedisCache redisCache;
+
+    /** Redis key 用于存储固定的好评率随机值 */
+    private static final String POSITIVE_RATING_REDIS_KEY = "product:positiveRating:fixed:";
+    private static final double MIN_RATING = 95.0;
+    private static final double MAX_RATING = 99.9;
+
     @Override
     public List<FsStoreUserEndCategoryProductVO> listProductsByCategoryId(Long categoryId, String keyword) {
         if (categoryId == null) return new ArrayList<>();
@@ -121,7 +133,7 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
             vo.setOtPrice(p.getOtPrice());
             vo.setSales(p.getSales());
             vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
-            vo.setPositiveRating();
+            vo.setPositiveRating(getFixedPositiveRating(p.getProductId()));
             result.add(vo);
         }
         out.put("list", result);
@@ -179,4 +191,26 @@ public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCat
         productUserEndCategoryMapper.deleteByCategoryIds(ids);
         return mapper.deleteByIds(ids);
     }
+
+    /**
+     * 获取固定的好评率随机值
+     * 先从 Redis 获取,如果不存在则生成随机值并保存到 Redis(永久保存)
+     * @return 固定的好评率值
+     */
+    private BigDecimal getFixedPositiveRating(Long productId) {
+        // 先从 Redis 获取
+        BigDecimal cachedRating = redisCache.getCacheObject(POSITIVE_RATING_REDIS_KEY + productId);
+        if (cachedRating != null) {
+            return cachedRating;
+        }
+        
+        // Redis 不存在,生成随机值
+        double rating = ThreadLocalRandom.current().nextDouble(MIN_RATING, MAX_RATING);
+        BigDecimal fixedRating = new BigDecimal(rating).setScale(1, RoundingMode.HALF_UP);
+        
+        // 保存到 Redis(永久保存,不设置过期时间)
+        redisCache.setCacheObject(POSITIVE_RATING_REDIS_KEY + productId, fixedRating);
+        
+        return fixedRating;
+    }
 }

+ 3 - 1
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportVO.java

@@ -14,6 +14,9 @@ public class FsStoreOrderItemExportVO implements Serializable
 
     private Long itemId;
 
+    @Excel(name = "订单类型")
+    private String orderType; // 订单类型 2.直播订单 3.点播订单
+
     /** 订单号 */
     @Excel(name = "订单号")
     private String orderCode;
@@ -121,7 +124,6 @@ public class FsStoreOrderItemExportVO implements Serializable
     private String isAudit;
 
 
-    private String orderType; // 订单类型 2.直播
     private BigDecimal payPrice;// 应付金额
     private BigDecimal deductionPrice;// 应付金额
     private BigDecimal payDelivery;// 应付邮费

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

@@ -21,6 +21,9 @@ public class FsStoreProductListQueryVO implements Serializable
     /** 商品图片 */
     private String image;
 
+    /** 轮播图 */
+    private String sliderImage;
+
     /** 商品名称 */
     private String productName;
 

+ 7 - 7
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java

@@ -4,9 +4,7 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
-import java.math.RoundingMode;
 import java.util.List;
-import java.util.concurrent.ThreadLocalRandom;
 
 /**
  * 用户端分类下的商品项:商品ID、名称、售价、原价、销量、产品标签列表
@@ -15,8 +13,6 @@ import java.util.concurrent.ThreadLocalRandom;
 public class FsStoreUserEndCategoryProductVO implements Serializable {
 
     private static final long serialVersionUID = 1L;
-    private static final double min = 95.0;
-    private static final double max = 99.9;
 
     private Long productId;
     private String productName;
@@ -32,8 +28,12 @@ public class FsStoreUserEndCategoryProductVO implements Serializable {
     private Long sales;
     /** 产品标签名称列表 */
     private List<String> tagList;
-    public void setPositiveRating() {
-        double rating = ThreadLocalRandom.current().nextDouble(min, max);
-        this.positiveRating = new BigDecimal(rating).setScale(1, RoundingMode.HALF_UP);
+    
+    /**
+     * 设置好评率(从外部传入固定值)
+     * @param rating 好评率值
+     */
+    public void setPositiveRating(BigDecimal rating) {
+        this.positiveRating = rating;
     }
 }

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

@@ -46,6 +46,10 @@ wx:
         secret: 99fc38771e111e640c654626cbf7c5e9 # 公众号的appsecret
         token: PPKOdAlCoMO # 接口配置里的Token值
         aesKey: Eswa6VjwtVcw03qZy6Wllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+  # 开放平台app微信授权配置
+  open:
+    app-id: wx0472e2edacf036da
+    secret: 1edaa1b2c153a87bedce8dfb78adffce
 aifabu:  #爱链接
   appKey: 7b471be905ab17ef358c610dd117601d008
 watch:
@@ -89,7 +93,7 @@ headerImg:
   imgUrl: https://jsbk-1323137866.cos.ap-chongqing.myqcloud.com/app/jsbk.jpg
 ipad:
   ipadUrl: http://jsbkipad.sywktxd.cn
-  aiApi:
+  aiApi: http://49.232.181.28:3000/api
   wxIpadUrl:
   voiceApi:
   commonApi:

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

@@ -95,8 +95,8 @@ headerImg:
 ipad:
   ipadUrl: http://qwipad.muyi88.com
 #  aiApi: http://152.136.202.157:3000/api
+  wxIpadUrl: http://ipad.cdwjyyh.com
   aiApi: http://49.232.181.28:3000/api
-  wxIpadUrl:
   voiceApi: http://106.52.21.84:8009
   commonApi: http://106.52.21.84:7771
 wx_miniapp_temp:

+ 3 - 4
fs-service/src/main/resources/application-druid-jzzx.yml

@@ -47,10 +47,9 @@ spring:
                 # 从库数据源
                 slave:
                     # 从数据源开关/默认关闭
-                    enabled: false
-                    url:
-                    username:
-                    password:
+                    url: jdbc:mysql://192.168.0.137:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrztek250218!3@.
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 32 - 0
fs-service/src/main/resources/db/20260317-客户属性模板菜单.sql

@@ -0,0 +1,32 @@
+-- 客户属性模板管理菜单
+-- 注意:parent_id 需要根据实际CRM模块的父菜单ID进行调整
+-- 可以通过 SELECT * FROM sys_menu WHERE menu_name LIKE '%CRM%' OR menu_name LIKE '%客户%'; 查询
+
+-- 先查询CRM模块的父菜单ID,假设CRM模块的父菜单ID需要确认
+-- 如果CRM模块不存在,需要先创建CRM模块菜单
+
+-- 方案一:如果已有CRM父菜单,请替换下面的parent_id值
+-- 方案二:如果需要新建CRM模块,先执行下面的创建CRM模块语句
+
+-- 创建CRM模块(如果不存在)
+-- INSERT INTO `sys_menu`
+--     (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+-- VALUES
+--     ('CRM管理', 0, 10, 'crm', NULL, NULL, 1, 0, 'M', '0', '0', '', 'peoples', 'admin', NOW(), '', NULL, 'CRM管理目录');
+
+-- 客户属性模板菜单(请根据实际情况修改parent_id)
+INSERT INTO `sys_menu`
+    (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+    ('客户属性模板', 0, 100, 'propertyTemplate', 'crm/propertyTemplate/index', NULL, 1, 0, 'C', '0', '0', 'crm:customerPropertyTemplate:list', 'edit', 'admin', NOW(), '', NULL, '客户属性模板菜单');
+
+SET @parent_id = LAST_INSERT_ID();
+
+INSERT INTO `sys_menu`
+    (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+VALUES
+    ('客户属性模板查询', @parent_id, 1, '', NULL, NULL, 1, 0, 'F', '0', '0', 'crm:customerPropertyTemplate:query', '#', 'admin', NOW(), '', NULL, ''),
+    ('客户属性模板新增', @parent_id, 2, '', NULL, NULL, 1, 0, 'F', '0', '0', 'crm:customerPropertyTemplate:add', '#', 'admin', NOW(), '', NULL, ''),
+    ('客户属性模板修改', @parent_id, 3, '', NULL, NULL, 1, 0, 'F', '0', '0', 'crm:customerPropertyTemplate:edit', '#', 'admin', NOW(), '', NULL, ''),
+    ('客户属性模板删除', @parent_id, 4, '', NULL, NULL, 1, 0, 'F', '0', '0', 'crm:customerPropertyTemplate:remove', '#', 'admin', NOW(), '', NULL, ''),
+    ('客户属性模板导出', @parent_id, 5, '', NULL, NULL, 1, 0, 'F', '0', '0', 'crm:customerPropertyTemplate:export', '#', 'admin', NOW(), '', NULL, '');

+ 4 - 0
fs-service/src/main/resources/db/20260318-客户属性标签添加意向和占比.sql

@@ -0,0 +1,4 @@
+-- 为客户属性标签表添加意向登记和喜欢占比字段
+ALTER TABLE `crm_customer_property` 
+ADD COLUMN `intention` varchar(20) DEFAULT NULL COMMENT '意向登记:high-高意向,medium-中意向,low-低意向,none-无意向' AFTER `ai_analysis`,
+ADD COLUMN `like_ratio` int(3) DEFAULT NULL COMMENT '喜欢占比:0-100' AFTER `intention`;

+ 3 - 1
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

@@ -61,7 +61,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         inner join company_user u on a.create_user = u.user_id
         inner join company_dept d on u.dept_id = d.dept_id
         <where>
-            <if test="companyId != null"> and a.company_id = #{companyId}</if>
+            <if test="taskType != null "> and a.task_type = #{taskType} </if>
+            <if test="companyId != null"> and a.company_id = #{companyId} </if>
             <if test="name != null  and name != ''"> and a.name like concat('%', #{name}, '%')</if>
             <if test="taskName != null  and taskName != ''"> and a.task_name like concat('%', #{taskName}, '%')</if>
             <if test="taskId != null "> and a.task_id = #{taskId}</if>
@@ -85,6 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 ${params.dataScope}
             </if>
         </where>
+                   order by a.id desc
     </select>
     
     <select id="selectCompanyVoiceRoboticById" parameterType="Long" resultMap="CompanyVoiceRoboticResult">

+ 1 - 1
fs-service/src/main/resources/mapper/company/CompanyWxClientMapper.xml

@@ -182,7 +182,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                              inner join company_voice_robotic_business t2 on t1.id = t2.wx_client_id and t1.robotic_id = t2.robotic_id
                              inner join company_ai_workflow_exec t3 on t3.business_key = t2.id
         where t1.is_add = 0 and t1.account_id is not null and t1.is_we_com = 1
-        and t3.current_node_type = #{execNodeType} And t3.status = #{execStatus}
+        and t3.current_node_type = #{execNodeType} And t3.status = #{execStatus} and t3.cid_group_no = #{cidGroupId}
         <if test="accountIdList != null and !accountIdList.isEmpty()">
             and t1.account_id in <foreach collection="accountIdList" open="(" separator="," close=")" item="item">#{item}</foreach>
         </if>

+ 133 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1361,4 +1361,137 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
           AND l.create_time &lt; CURDATE() + INTERVAL 1 DAY
         order by l.create_time desc
     </select>
+
+    <!-- 首次点播数据统计:看课时间在[营期开始时间, 营期开始时间+视频时长]范围内,view_start=COALESCE(finish_time-duration, update_time-duration, create_time) -->
+    <select id="selectFirstPlaybackStats" resultType="map">
+        SELECT
+            COUNT(DISTINCT l.user_id) AS firstWatchCount,
+            COUNT(DISTINCT CASE WHEN l.duration &gt;= 1200 THEN l.user_id END) AS firstWatch20MinCount,
+            COUNT(DISTINCT CASE WHEN l.duration &gt;= 1800 THEN l.user_id END) AS firstWatch30MinCount
+        FROM fs_course_watch_log l
+        INNER JOIN fs_user_course_period_days fcpd ON fcpd.period_id = l.period_id AND fcpd.video_id = l.video_id AND fcpd.del_flag = '0'
+        INNER JOIN fs_user_course_video v ON v.video_id = l.video_id AND v.is_del = 0
+        WHERE l.video_id = #{videoId}
+          AND l.period_id = #{periodId}
+          AND l.user_id IS NOT NULL
+          AND (
+              (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration, 0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration, 0) SECOND), l.create_time) &gt;= fcpd.start_date_time)
+              AND
+              (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration, 0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration, 0) SECOND), l.create_time) &lt; DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration, 0) SECOND))
+          )
+    </select>
+
+    <!-- 第2-n次观看数据统计:view_start不在[营期开始时间, 营期开始时间+视频时长]内 -->
+    <select id="selectRepeatPlaybackStats" resultType="map">
+        SELECT
+            COUNT(DISTINCT l.user_id) AS repeatWatchCount,
+            COUNT(DISTINCT CASE WHEN l.duration &gt;= 1200 THEN l.user_id END) AS repeatWatch20MinCount,
+            COUNT(DISTINCT CASE WHEN l.duration &gt;= 1800 THEN l.user_id END) AS repeatWatch30MinCount
+        FROM fs_course_watch_log l
+        INNER JOIN fs_user_course_period_days fcpd ON fcpd.period_id = l.period_id AND fcpd.video_id = l.video_id AND fcpd.del_flag = '0'
+        INNER JOIN fs_user_course_video v ON v.video_id = l.video_id AND v.is_del = 0
+        WHERE l.video_id = #{videoId}
+          AND l.period_id = #{periodId}
+          AND l.user_id IS NOT NULL
+          AND (
+              (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration, 0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration, 0) SECOND), l.create_time) &lt; fcpd.start_date_time)
+              OR
+              (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration, 0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration, 0) SECOND), l.create_time) &gt;= DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration, 0) SECOND))
+          )
+    </select>
+
+    <!-- 课程小结-用户详情列表:按user_id分组,区分首次/2-n次观看时长,关联订单,按创建时间倒序 -->
+    <select id="selectCourseStatisticsUserDetailList" resultType="com.fs.course.vo.CourseStatisticsUserDetailVO">
+        SELECT
+            ua.user_id AS userId,
+            COALESCE(u.nick_name, u.nickname, '未知用户') AS userName,
+            COALESCE(ua.first_dur, 0) AS watchDuration,
+            COALESCE(ua.repeat_dur, 0) AS repeatWatchDuration,
+            COALESCE(ord.order_count, 0) AS orderCount,
+            COALESCE(ord.order_amount, 0) AS orderAmount,
+            c.company_name AS companyName,
+            cu.nick_name AS salesName
+        FROM (
+            SELECT
+                l.user_id,
+                MAX(l.create_time) AS max_create_time,
+                SUM(CASE WHEN
+                    (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &gt;= fcpd.start_date_time)
+                    AND (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &lt; DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration,0) SECOND))
+                THEN COALESCE(l.duration,0) ELSE 0 END) AS first_dur,
+                SUM(CASE WHEN
+                    (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &lt; fcpd.start_date_time)
+                    OR (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &gt;= DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration,0) SECOND))
+                THEN COALESCE(l.duration,0) ELSE 0 END) AS repeat_dur,
+                MAX(l.company_id) AS company_id,
+                MAX(l.company_user_id) AS company_user_id
+            FROM fs_course_watch_log l
+            INNER JOIN fs_user_course_period_days fcpd ON fcpd.period_id = l.period_id AND fcpd.video_id = l.video_id AND fcpd.del_flag = '0'
+            INNER JOIN fs_user_course_video v ON v.video_id = l.video_id AND v.is_del = 0
+            WHERE l.video_id = #{param.videoId} AND l.period_id = #{param.periodId} AND l.user_id IS NOT NULL
+            GROUP BY l.user_id
+        ) ua
+        LEFT JOIN fs_user u ON u.user_id = ua.user_id
+        LEFT JOIN (
+            SELECT
+                o.user_id,
+                COUNT(o.id) AS order_count,
+                SUM(IFNULL(o.pay_price, 0)) AS order_amount,
+                MIN(o.id) AS min_order_id
+            FROM fs_store_order_scrm o
+            WHERE o.order_type = 3 AND o.video_id = #{param.videoId} AND o.period_id = #{param.periodId} AND o.paid = 1
+            GROUP BY o.user_id
+        ) ord ON ord.user_id = ua.user_id
+        LEFT JOIN company c ON c.company_id = ua.company_id
+        LEFT JOIN company_user cu ON cu.user_id = ua.company_user_id
+        ORDER BY ua.max_create_time DESC
+    </select>
+
+    <!-- 课程小结-用户详情导出:按创建时间倒序,最多50000条 -->
+    <select id="selectCourseStatisticsUserDetailExportList" resultType="com.fs.course.vo.CourseStatisticsUserDetailVO">
+        SELECT
+            ua.user_id AS userId,
+            COALESCE(u.nick_name, u.nickname, '未知用户') AS userName,
+            COALESCE(ua.first_dur, 0) AS watchDuration,
+            COALESCE(ua.repeat_dur, 0) AS repeatWatchDuration,
+            COALESCE(ord.order_count, 0) AS orderCount,
+            COALESCE(ord.order_amount, 0) AS orderAmount,
+            c.company_name AS companyName,
+            cu.nick_name AS salesName
+        FROM (
+            SELECT
+                l.user_id,
+                MAX(l.create_time) AS max_create_time,
+                SUM(CASE WHEN
+                    (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &gt;= fcpd.start_date_time)
+                    AND (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &lt; DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration,0) SECOND))
+                THEN COALESCE(l.duration,0) ELSE 0 END) AS first_dur,
+                SUM(CASE WHEN
+                    (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &lt; fcpd.start_date_time)
+                    OR (COALESCE(DATE_SUB(l.finish_time, INTERVAL COALESCE(l.duration,0) SECOND), DATE_SUB(l.update_time, INTERVAL COALESCE(l.duration,0) SECOND), l.create_time) &gt;= DATE_ADD(fcpd.start_date_time, INTERVAL COALESCE(v.duration,0) SECOND))
+                THEN COALESCE(l.duration,0) ELSE 0 END) AS repeat_dur,
+                MAX(l.company_id) AS company_id,
+                MAX(l.company_user_id) AS company_user_id
+            FROM fs_course_watch_log l
+            INNER JOIN fs_user_course_period_days fcpd ON fcpd.period_id = l.period_id AND fcpd.video_id = l.video_id AND fcpd.del_flag = '0'
+            INNER JOIN fs_user_course_video v ON v.video_id = l.video_id AND v.is_del = 0
+            WHERE l.video_id = #{param.videoId} AND l.period_id = #{param.periodId} AND l.user_id IS NOT NULL
+            GROUP BY l.user_id
+        ) ua
+        LEFT JOIN fs_user u ON u.user_id = ua.user_id
+        LEFT JOIN (
+            SELECT
+                o.user_id,
+                COUNT(o.id) AS order_count,
+                SUM(IFNULL(o.pay_price, 0)) AS order_amount,
+                MIN(o.id) AS min_order_id
+            FROM fs_store_order_scrm o
+            WHERE o.order_type = 3 AND o.video_id = #{param.videoId} AND o.period_id = #{param.periodId} AND o.paid = 1
+            GROUP BY o.user_id
+        ) ord ON ord.user_id = ua.user_id
+        LEFT JOIN company c ON c.company_id = ua.company_id
+        LEFT JOIN company_user cu ON cu.user_id = ua.company_user_id
+        ORDER BY ua.max_create_time DESC
+        LIMIT 50000
+    </select>
 </mapper>

+ 130 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyMapper.xml

@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.crm.mapper.CrmCustomerPropertyMapper">
+
+    <resultMap type="CrmCustomerProperty" id="CrmCustomerPropertyResult">
+        <result property="id"    column="id"    />
+        <result property="customerId"    column="customer_id"    />
+        <result property="propertyId"    column="property_id"    />
+        <result property="propertyName"    column="property_name"    />
+        <result property="propertyValue"    column="property_value"    />
+        <result property="propertyValueType"    column="property_value_type"    />
+        <result property="tradeType"    column="trade_type"    />
+        <result property="aiAnalysis"    column="ai_analysis"    />
+        <result property="intention"    column="intention"    />
+        <result property="likeRatio"    column="like_ratio"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectCrmCustomerPropertyVo">
+        select id, customer_id, property_id, property_name, property_value, property_value_type, trade_type, ai_analysis, intention, like_ratio, create_time, create_by, update_time, update_by, remark from crm_customer_property
+    </sql>
+
+    <select id="selectCrmCustomerPropertyList" parameterType="CrmCustomerProperty" resultMap="CrmCustomerPropertyResult">
+        <include refid="selectCrmCustomerPropertyVo"/>
+        <where>
+            <if test="customerId != null"> and customer_id = #{customerId}</if>
+            <if test="propertyId != null"> and property_id = #{propertyId}</if>
+            <if test="propertyName != null and propertyName != ''"> and property_name like concat('%', #{propertyName}, '%')</if>
+            <if test="propertyValue != null and propertyValue != ''"> and property_value like concat('%', #{propertyValue}, '%')</if>
+            <if test="propertyValueType != null and propertyValueType != ''"> and property_value_type = #{propertyValueType}</if>
+            <if test="tradeType != null and tradeType != ''"> and trade_type = #{tradeType}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectCrmCustomerPropertyById" parameterType="Long" resultMap="CrmCustomerPropertyResult">
+        <include refid="selectCrmCustomerPropertyVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertCrmCustomerProperty" parameterType="CrmCustomerProperty" useGeneratedKeys="true" keyProperty="id">
+        insert into crm_customer_property
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="customerId != null">customer_id,</if>
+            <if test="propertyId != null">property_id,</if>
+            <if test="propertyName != null">property_name,</if>
+            <if test="propertyValue != null">property_value,</if>
+            <if test="propertyValueType != null">property_value_type,</if>
+            <if test="tradeType != null">trade_type,</if>
+            <if test="aiAnalysis != null">ai_analysis,</if>
+            <if test="intention != null">intention,</if>
+            <if test="likeRatio != null">like_ratio,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="customerId != null">#{customerId},</if>
+            <if test="propertyId != null">#{propertyId},</if>
+            <if test="propertyName != null">#{propertyName},</if>
+            <if test="propertyValue != null">#{propertyValue},</if>
+            <if test="propertyValueType != null">#{propertyValueType},</if>
+            <if test="tradeType != null">#{tradeType},</if>
+            <if test="aiAnalysis != null">#{aiAnalysis},</if>
+            <if test="intention != null">#{intention},</if>
+            <if test="likeRatio != null">#{likeRatio},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateCrmCustomerProperty" parameterType="CrmCustomerProperty">
+        update crm_customer_property
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="customerId != null">customer_id = #{customerId},</if>
+            <if test="propertyId != null">property_id = #{propertyId},</if>
+            <if test="propertyName != null">property_name = #{propertyName},</if>
+            <if test="propertyValue != null">property_value = #{propertyValue},</if>
+            <if test="propertyValueType != null">property_value_type = #{propertyValueType},</if>
+            <if test="tradeType != null">trade_type = #{tradeType},</if>
+            <if test="aiAnalysis != null">ai_analysis = #{aiAnalysis},</if>
+            <if test="intention != null">intention = #{intention},</if>
+            <if test="likeRatio != null">like_ratio = #{likeRatio},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCrmCustomerPropertyById" parameterType="Long">
+        delete from crm_customer_property where id = #{id}
+    </delete>
+
+    <delete id="deleteCrmCustomerPropertyByIds" parameterType="String">
+        delete from crm_customer_property where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectCrmCustomerPropertyByCustomerId" parameterType="Long" resultMap="CrmCustomerPropertyResult">
+        <include refid="selectCrmCustomerPropertyVo"/>
+        where customer_id = #{customerId}
+        order by id desc
+    </select>
+
+    <select id="selectByCustomerIdAndPropertyId" resultMap="CrmCustomerPropertyResult">
+        <include refid="selectCrmCustomerPropertyVo"/>
+        where customer_id = #{customerId} and property_id = #{propertyId}
+        limit 1
+    </select>
+
+    <delete id="deleteByCustomerIdAndPropertyId">
+        delete from crm_customer_property where customer_id = #{customerId} and property_id = #{propertyId}
+    </delete>
+</mapper>

+ 91 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerPropertyTemplateMapper.xml

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.crm.mapper.CrmCustomerPropertyTemplateMapper">
+
+    <resultMap type="CrmCustomerPropertyTemplate" id="CrmCustomerPropertyTemplateResult">
+        <result property="id"    column="id"    />
+        <result property="name"    column="name"    />
+        <result property="valueType"    column="value_type"    />
+        <result property="aiHint"    column="ai_hint"    />
+        <result property="tradeType"    column="trade_type"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectCrmCustomerPropertyTemplateVo">
+        select id, name, value_type, ai_hint, trade_type, create_time, create_by, update_time, update_by, remark from crm_customer_property_template
+    </sql>
+
+    <select id="selectCrmCustomerPropertyTemplateList" parameterType="CrmCustomerPropertyTemplate" resultMap="CrmCustomerPropertyTemplateResult">
+        <include refid="selectCrmCustomerPropertyTemplateVo"/>
+        <where>
+            <if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="valueType != null and valueType != ''"> and value_type = #{valueType}</if>
+            <if test="tradeType != null and tradeType != ''"> and trade_type = #{tradeType}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectCrmCustomerPropertyTemplateById" parameterType="Long" resultMap="CrmCustomerPropertyTemplateResult">
+        <include refid="selectCrmCustomerPropertyTemplateVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertCrmCustomerPropertyTemplate" parameterType="CrmCustomerPropertyTemplate" useGeneratedKeys="true" keyProperty="id">
+        insert into crm_customer_property_template
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="name != null">name,</if>
+            <if test="valueType != null">value_type,</if>
+            <if test="aiHint != null">ai_hint,</if>
+            <if test="tradeType != null">trade_type,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="name != null">#{name},</if>
+            <if test="valueType != null">#{valueType},</if>
+            <if test="aiHint != null">#{aiHint},</if>
+            <if test="tradeType != null">#{tradeType},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateCrmCustomerPropertyTemplate" parameterType="CrmCustomerPropertyTemplate">
+        update crm_customer_property_template
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="name != null">name = #{name},</if>
+            <if test="valueType != null">value_type = #{valueType},</if>
+            <if test="aiHint != null">ai_hint = #{aiHint},</if>
+            <if test="tradeType != null">trade_type = #{tradeType},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteCrmCustomerPropertyTemplateById" parameterType="Long">
+        delete from crm_customer_property_template where id = #{id}
+    </delete>
+
+    <delete id="deleteCrmCustomerPropertyTemplateByIds" parameterType="String">
+        delete from crm_customer_property_template where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 96 - 0
fs-service/src/main/resources/mapper/his/FsUserPlayerMapper.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsUserPlayerMapper">
+
+    <resultMap type="FsUserPlayer" id="FsUserPlayerResult">
+        <result property="id"    column="id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="gameId"    column="game_id"    />
+        <result property="level"    column="level"    />
+        <result property="star"    column="star"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsUserPlayerVo">
+        select id, user_id, game_id, level, star, create_time, update_time from fs_user_player
+    </sql>
+
+    <select id="selectFsUserPlayerList" parameterType="FsUserPlayer" resultMap="FsUserPlayerResult">
+        <include refid="selectFsUserPlayerVo"/>
+        <where>
+            <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="gameId != null "> and game_id = #{gameId}</if>
+            <if test="level != null "> and `level` = #{level}</if>
+            <if test="star != null "> and star = #{star}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserPlayerById" parameterType="Long" resultMap="FsUserPlayerResult">
+        <include refid="selectFsUserPlayerVo"/>
+        where id = #{id}
+    </select>
+    <select id="selectFsUserByUserIdAndGameId" resultType="com.fs.his.vo.FsUserPlayerVo">
+        select fup.id, fup.user_id, fup.game_id, fup.level, fup.star, fup.create_time, fup.update_time,fu.nick_name,
+               fu.avatar,fu.integral
+        from fs_user_player fup
+        inner join fs_user fu on fu.user_id = fup.user_id
+        WHERE
+            fup.user_id = #{userId} and fup.game_id = #{gameId}
+    </select>
+
+    <insert id="insertFsUserPlayer" parameterType="FsUserPlayer" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_player
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="gameId != null">game_id,</if>
+            <if test="level != null">`level`,</if>
+            <if test="star != null">star,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="gameId != null">#{gameId},</if>
+            <if test="level != null">#{level},</if>
+            <if test="star != null">#{star},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserPlayer" parameterType="FsUserPlayer">
+        update fs_user_player
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="gameId != null">game_id = #{gameId},</if>
+            <if test="level != null">`level` = #{level},</if>
+            <if test="star != null">star = #{star},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+    <update id="updateFsUserPlayerByUserIdAndGameId">
+        update fs_user_player
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="level != null">`level` = #{level},</if>
+            <if test="star != null">star = #{star},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where user_Id = #{userId} and game_id = #{gameId}
+    </update>
+
+    <delete id="deleteFsUserPlayerById" parameterType="Long">
+        delete from fs_user_player where id = #{id}
+    </delete>
+
+    <delete id="deleteFsUserPlayerByIds" parameterType="String">
+        delete from fs_user_player where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 1 - 0
fs-service/src/main/resources/mapper/hisStore/FsShippingTemplatesScrmMapper.xml

@@ -32,6 +32,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isDel != null "> and is_del = #{isDel}</if>
             <if test="sort != null "> and sort = #{sort}</if>
         </where>
+        order by sort , create_time desc
     </select>
     
     <select id="selectFsShippingTemplatesById" parameterType="Long" resultMap="FsShippingTemplatesResult">

+ 46 - 20
fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -93,7 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectFsStoreOrderVo">
-        select id, order_code,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type from fs_store_order_scrm
+        select id, order_code,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type,video_id,course_id,project_id,period_id from fs_store_order_scrm
     </sql>
 
     <select id="selectFsStoreOrderList" parameterType="FsStoreOrderScrm" resultMap="FsStoreOrderResult">
@@ -157,6 +157,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="remark != null and remark != ''"> and remark = #{remark}</if>
             <if test="videoId != null and videoId != ''"> and video_id = #{videoId}</if>
             <if test="courseId != null and courseId != ''"> and course_id = #{courseId}</if>
+            <if test="periodId != null"> and period_id = #{periodId}</if>
         </where>
     </select>
 
@@ -272,6 +273,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="backendEditProductType != null">backend_edit_product_type,</if>
             <if test="videoId != null">video_id,</if>
             <if test="courseId != null" >course_id,</if>
+            <if test="projectId != null" >project_id,</if>
+            <if test="periodId != null" >period_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="orderCode != null and orderCode != ''">#{orderCode},</if>
@@ -361,6 +364,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="backendEditProductType != null">#{backendEditProductType},</if>
             <if test="videoId != null">#{videoId},</if>
             <if test="courseId != null" >#{courseId},</if>
+            <if test="projectId != null" >#{projectId},</if>
+            <if test="periodId != null" >#{periodId},</if>
          </trim>
     </insert>
 
@@ -1154,10 +1159,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -1174,9 +1179,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -1330,10 +1338,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -1350,9 +1358,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -1483,10 +1494,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -1503,9 +1514,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -1640,10 +1654,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -1660,9 +1674,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -1793,10 +1810,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -1813,9 +1830,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -1989,10 +2009,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
             <if test="maps.isHealth != null and maps.isHealth !=  ''   ">
                 and (o.company_id is null
-                or o.order_type = 2)
+                or o.order_type = 2 or o.order_type = 3)
             </if>
             <if test="maps.notHealth != null  ">
-                and o.company_id is not null
+                and o.company_id is not null and o.order_type = 0
             </if>
             <if test="maps.companyUserId != null  ">
                 and o.company_user_id =#{maps.companyUserId}
@@ -2009,9 +2029,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="maps.productName != null and  maps.productName !=  '' ">
                 and fsp.product_name like concat('%', #{maps.productName}, '%')
             </if>
-            <if test="maps.orderType != null    ">
+            <if test="maps.orderType != null and maps.orderType != -1">
                 and o.order_type =#{maps.orderType}
             </if>
+            <if test="maps.orderType != null and maps.orderType == -1">
+                and o.order_type in (2, 3)
+            </if>
             <if test="maps.payType != null    ">
                 and o.pay_type =#{maps.payType}
             </if>
@@ -2183,9 +2206,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="maps.companyUserNickName != null and maps.companyUserNickName != ''">
             AND cu.nick_name LIKE CONCAT('%', #{maps.companyUserNickName}, '%')
         </if>
-        <if test="maps.orderType != null">
+        <if test="maps.orderType != null and maps.orderType != -1">
             AND o.order_type = #{maps.orderType}
         </if>
+        <if test="maps.orderType != null and maps.orderType == -1">
+            AND o.order_type in (2, 3)
+        </if>
         <if test="maps.payType != null">
             AND o.pay_type = #{maps.payType}
         </if>

+ 8 - 16
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -514,13 +514,11 @@
             inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
-        <if test='config.isAudit == "1"'>
-            and p.is_audit = '1'
-        </if>
+            and p.is_audit = 1
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and p.is_new=1 and p.is_display=1 order by p.sort desc limit #{count}
+        and p.is_new=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc limit #{count}
     </select>
     <select id="selectFsStoreProductNewQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -528,16 +526,14 @@
             inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
-        <if test='config.isAudit == "1"'>
-            and p.is_audit = '1'
-        </if>
+            and p.is_audit = 1
         <if test='appId != null and appId != ""'>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and p.is_new=1 and p.is_display=1 order by p.sort desc
+        and p.is_new=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc
     </select>
     <select id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -545,13 +541,11 @@
             inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
-        <if test='config.isAudit == "1" '>
-            and p.is_audit = '1'
-        </if>
+            and p.is_audit = 1
         <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by p.sort desc limit #{count}
+        and  p.is_hot=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc limit #{count}
     </select>
     <select id="selectFsStoreProductHotQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
@@ -559,16 +553,14 @@
             inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
         </if>
         where p.is_del=0 and p.is_show=1
-        <if test='config.isAudit == "1" '>
-            and p.is_audit = '1'
-        </if>
+            and p.is_audit = 1
         <if test='appId != null and appId != ""'>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         <if test='keyword != null and keyword != ""'>
             and p.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        and  p.is_hot=1 and p.is_display=1 order by p.sort desc
+        and  p.is_hot=1 and p.is_display=1 order by COALESCE(p.sort, 999999) asc, p.create_time desc
     </select>
     <select id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p

+ 4 - 4
fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml

@@ -22,19 +22,19 @@
 
     <select id="selectDistinctProductIdsByCategoryId" resultType="java.lang.Long">
         select distinct a.product_id from fs_store_product_user_end_category a left join fs_store_product_scrm c on a.product_id = c.product_id
-        where a.user_end_category_id = #{categoryId} and c.is_del = 0 and c.is_show = 1
+        where a.user_end_category_id = #{categoryId} and c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1
         <if test="keyword != null and keyword != ''">
             and c.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        order by c.sort desc, c.create_time desc, a.product_id
+        order by c.sort asc, c.create_time desc, a.product_id
     </select>
 
     <select id="selectDistinctProductIds" resultType="java.lang.Long">
         select distinct a.product_id from fs_store_product_user_end_category  a left join fs_store_product_scrm c on a.product_id = c.product_id
-        where c.is_del = 0 and c.is_show = 1
+        where c.is_del = 0 and c.is_show = 1 and c.is_display = 1 and c.is_audit = 1
         <if test="keyword != null and keyword != ''">
             and c.product_name like CONCAT('%', #{keyword}, '%')
         </if>
-        order by c.sort desc, c.create_time desc, a.product_id
+        order by c.sort asc, c.create_time desc, a.product_id
     </select>
 </mapper>

+ 6 - 3
fs-service/src/main/resources/mapper/live/LiveDataMapper.xml

@@ -629,7 +629,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             lufe.company_id AS companyId,
             COALESCE(c.company_name, '总台') AS companyName,
             COALESCE(COUNT(DISTINCT CASE
-                WHEN lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                WHEN lwu.online_seconds >= 1200
+<!--                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)-->
                      AND COALESCE(vd.total_duration, 0) > 0
                 THEN lwu.user_id END), 0) AS totalCompleteCount
         FROM live_watch_user lwu
@@ -684,7 +685,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             COALESCE(COUNT(DISTINCT CASE
                 WHEN lwu.live_flag = 1
                      AND lwu.replay_flag = 0
-                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                     AND lwu.online_seconds >= 1200
+<!--                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)-->
                      AND COALESCE(vd.total_duration, 0) > 0
                 THEN lwu.user_id END), 0) AS liveCompleteCount
         FROM live_watch_user lwu
@@ -739,7 +741,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             COALESCE(COUNT(DISTINCT CASE
                 WHEN lwu.live_flag = 0
                      AND lwu.replay_flag = 1
-                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                     AND lwu.online_seconds >= 1200
+<!--                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)-->
                      AND COALESCE(vd.total_duration, 0) > 0
                 THEN lwu.user_id END), 0) AS replayCompleteCount
         FROM live_watch_user lwu

+ 86 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -3,6 +3,7 @@ package com.fs.app.controller.course;
 
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.controller.AppBaseController;
@@ -16,10 +17,17 @@ import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.SecurityUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.dto.BatchSendCourseDTO;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.*;
 import com.fs.course.param.newfs.FsUserCourseAddCompanyUserParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
+import com.fs.course.param.newfs.FsUserCourseVideoRemainTimeParam;
 import com.fs.course.param.newfs.FsUserCourseVideoUParam;
 import com.fs.course.service.*;
 import com.fs.course.vo.FsUserCourseVideoH5VO;
@@ -28,6 +36,7 @@ import com.fs.his.domain.FsUser;
 import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.im.dto.OpenImResponseDTO;
 import com.fs.im.service.OpenIMService;
+import com.fs.system.service.ISysConfigService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.slf4j.Logger;
@@ -64,6 +73,14 @@ public class CourseFsUserController extends AppBaseController {
 
     @Autowired
     private RedisTemplate redisTemplate;
+    @Autowired
+    private ISysConfigService sysConfigService;
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    private RedisCache redisCache;
 
 
 
@@ -103,6 +120,75 @@ public class CourseFsUserController extends AppBaseController {
         return R.ok().put("data",course);
     }
 
+    @ApiOperation("h5课程完课倒计时")
+    @PostMapping("/getRemainTime")
+    public R getRemainTime(@RequestBody FsUserCourseVideoRemainTimeParam param)
+    {
+        String json = sysConfigService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        Integer remainTime = 0;
+        if (config.getCompletionCountdown() != null && config.getCompletionCountdown()) {
+            FsCourseWatchLog fsCourseWatchLog = fsCourseWatchLogMapper.selectFsCourseWatchLogWithUCCV(param.getFsUserId(), param.getCompanyUserId(), param.getCourseId(), param.getVideoId());
+            if (fsCourseWatchLog == null) {
+                return R.error("未查询到用户的看课记录!");
+            }
+            
+            // 如果已经完课,剩余时间为0
+            if (fsCourseWatchLog != null && fsCourseWatchLog.getLogType() != null && fsCourseWatchLog.getLogType() == 2) {
+                remainTime = 0;
+            } else {
+                // 获取已观看时长(优先从Redis获取,因为可能还未同步到数据库)
+                Long watchedDuration = 0L;
+                String redisKey = "h5wxuser:watch:duration:" + param.getFsUserId() + ":" + param.getVideoId() + ":" + param.getCompanyUserId();
+                String durationStr = redisCache.getCacheObject(redisKey);
+                if (durationStr != null) {
+                    watchedDuration = Long.valueOf(durationStr);
+                } else if (fsCourseWatchLog != null && fsCourseWatchLog.getDuration() != null) {
+                    watchedDuration = fsCourseWatchLog.getDuration();
+                }
+                
+                // 获取视频总时长(参照scheduleUpdateDurationToDatabase中的getFsUserVideoDuration方法)
+                Long videoDuration = getFsUserVideoDuration(param.getVideoId().longValue());
+                
+                if (videoDuration != null && videoDuration > 0 && config.getAnswerRate() != null) {
+                    // 参照scheduleUpdateDurationToDatabase中的完课逻辑:percentage >= config.getAnswerRate()
+                    // 计算需要观看的时长(秒)
+                    long requiredDuration = videoDuration * config.getAnswerRate() / 100;
+                    
+                    // 计算剩余时间(秒)
+                    long remainTimeSeconds = requiredDuration - watchedDuration;
+                    remainTime = (int) Math.max(0, remainTimeSeconds);
+                }
+            }
+        }
+
+        return R.ok().put("remainTime",remainTime);
+    }
+
+    private Long getFsUserVideoDuration(Long videoId) {
+        //将视频时长也存到redis
+        String videoRedisKey = "h5wxuser:video:duration:" + videoId;
+        Long videoDuration = 0L;
+        try {
+            videoDuration = redisCache.getCacheObject(videoRedisKey);
+        } catch (Exception e) {
+            String string = redisCache.getCacheObject(videoRedisKey);
+            if (string != null) {
+                videoDuration = Long.parseLong(string);
+            }
+            logger.error("key中id为S:{}", videoDuration);
+        }
+
+        if (videoDuration == null || videoDuration == 0) {
+            FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
+            if (video != null && video.getDuration() != null) {
+                videoDuration = video.getDuration();
+                redisCache.setCacheObject(videoRedisKey, video.getDuration());
+            }
+        }
+        return videoDuration;
+    }
+
     @Login
     @ApiOperation("H5课程详情")
     @GetMapping("/videoDetails")

+ 83 - 12
fs-user-app/src/main/java/com/fs/app/controller/game/PlayerController.java

@@ -1,30 +1,101 @@
 package com.fs.app.controller.game;
 
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import cn.hutool.json.JSONUtil;
 import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
 import com.fs.common.core.domain.R;
+import com.fs.his.config.AppConfig;
+import com.fs.his.config.IntegralConfig;
 import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserPlayer;
+import com.fs.his.param.FsUserAddIntegralParam;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.service.IFsUserPlayerService;
 import com.fs.his.service.IFsUserService;
+import com.fs.his.vo.FsUserPlayerVo;
+import com.fs.system.service.ISysConfigService;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.fs.his.utils.PhoneUtil.decryptPhoneMk;
+
 
 @RestController
-@RequestMapping("/player")
-public class PlayerController {
+@RequestMapping("/app/player")
+public class PlayerController extends AppBaseController {
 
     @Autowired
-    private IFsUserService fsUserService;
+    private IFsUserIntegralLogsService userIntegralLogsService;
+    @Autowired
+    private IFsUserPlayerService userPlayerService;
+    @Autowired
+    private ISysConfigService configService;
 
-    @Login
     @PostMapping("/updateCurrency")
     @ApiOperation("玩家货币更新")
-    public R updateCurrency(@RequestBody FsUser player){
-        Boolean  flag=fsUserService.updateFsUser(player)>0;
-        return R.ok().put("flag",flag);
+    public R updateCurrency(@RequestBody FsUserAddIntegralParam param){
+        param.setUserId(param.getUserId());
+        param.setType(3);//游戏添加积分
+        return userIntegralLogsService.addIntegral(param);
+    }
+
+    /**
+     * 获取用户信息
+     * @param userId
+     * @return     */
+    @ApiOperation("获取用户信息")
+    @GetMapping("/getUserInfo")
+    public R getUserInfo(@RequestParam Long userId,@RequestParam Integer gameId){
+        try {
+            FsUserPlayerVo user=userPlayerService.selectFsUserByUserIdAndGameId(userId,gameId);
+            Map<String,Object> map=new HashMap<>();
+            map.put("user",user);
+            return R.ok(map);
+        } catch (Exception e){
+            return R.error("操作异常");
+        }
+    }
+
+    /**
+     * 获取游戏配置
+     * @param gameId
+     * @return
+     * */
+    @ApiOperation("获取游戏配置")
+    @GetMapping("/config")
+    public R config(@RequestParam Integer gameId){
+        String json = configService.selectConfigByKey("app.config");
+        AppConfig config = JSONUtil.toBean(json, AppConfig.class);
+        if (config == null) {
+            return R.error("游戏配置错误");
+        }else {
+            return R.ok().put("data",config);
+        }
+    }
+
+    /**
+     * 更新用户等级
+     * @param param
+     * @return
+    */
+    @ApiOperation("更新用户等级")
+    @PostMapping("/updateUserPlayer")
+    public R updateUserPlayer(@RequestBody FsUserPlayer param){
+        if (param.getUserId() == null) {
+            return R.error();
+        }
+        FsUserPlayer fsUserPlayer = new FsUserPlayer();
+        fsUserPlayer.setUserId(param.getUserId());
+        fsUserPlayer.setGameId(param.getGameId());
+        fsUserPlayer.setStar(param.getStar());
+        fsUserPlayer.setLevel(param.getLevel());
+        userPlayerService.updateFsUserPlayerByUserIdAndGameId(fsUserPlayer);
+        return R.ok();
     }
 
 }

+ 1 - 1
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -252,7 +252,7 @@ public class WxTaskService {
         String json = sysConfigService.selectConfigByKey("wx.config");
         WxConfig config = JSONUtil.toBean(json, WxConfig.class);
         // 需要添加微信的列表
-        List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getAddWxList4Workflow(accountIdList);
+        List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getAddWxList4Workflow(accountIdList,cidGroupNo);
         log.info("需要添加微信的数量:{}", list.size());
         if (list.isEmpty()) return;
         List<CompanyWxClient> addList = new ArrayList<>();