1
0

48 Коммиты 92eef70879 ... e43a3e13c4

Автор SHA1 Сообщение Дата
  caoliqin e43a3e13c4 feat:app im发课、标签、欢迎语等 3 недель назад
  caoliqin 5063d784e6 Merge branch 'refs/heads/master' into master-clq-temp 3 недель назад
  wjj 44091548cb 鸿森堂--完善课程优惠券业务(change) 3 недель назад
  wjj 72186b0df1 鸿森堂--完善课程优惠券业务 3 недель назад
  caoliqin 7007b5c59a Merge branch 'refs/heads/master' into master-clq-temp 3 недель назад
  wjj 2a3c85b84c 1.课程优惠券列表 2.核销课程优惠券 4 недель назад
  caoliqin 8278712246 feat:郑多燕配置文件 4 недель назад
  caoliqin 9c97a60029 Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java 4 недель назад
  caoliqin 3bb382debc feat:修复选择会员查询 4 недель назад
  三七 766c5348ba 多小程序管理-新增一个客服电话和备案号 4 недель назад
  yuhongqi 7a8a5174d6 Merge remote-tracking branch 'origin/master' 4 недель назад
  yuhongqi 1527171595 检查限购数据 4 недель назад
  wjj c63c3396ba 手动,自动完课发送课程优惠券 4 недель назад
  yuhongqi 65aca0685b Merge remote-tracking branch 'origin/master' 4 недель назад
  yuhongqi 6c3fc352c3 订单同步功能 4 недель назад
  三七 aec1fa991c avatar不要 4 недель назад
  lk d0ca56acc2 新老AI的url冲突问题处理 4 недель назад
  三七 c92fbe7d73 积分显示名称 4 недель назад
  三七 a89f193854 积分显示名称 4 недель назад
  三七 935fcaa26b 积分显示名称 4 недель назад
  三七 848460c8fc 企业微信 4 недель назад
  yuhongqi 4f8d0cbc4b Merge remote-tracking branch 'origin/master' 4 недель назад
  yuhongqi e645bfea91 查看数据功能 4 недель назад
  三七 98336273ac app/直播Bug修复 1 месяц назад
  wjj c34ad311d7 鸿森堂---课程小节关联课程优惠券 1 месяц назад
  caoliqin 44140c36f0 Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java 1 месяц назад
  caoliqin ddd4dd4015 feat:叮当-企微客户添加重粉记录展示 1 месяц назад
  yuhongqi 751e54a714 评论功能 1 месяц назад
  三七 66aedbb990 获取奖励类型配置 1 месяц назад
  yuhongqi 2dfb3a275f Merge remote-tracking branch 'origin/master' 1 месяц назад
  yuhongqi 3f46fed1b4 提交济南配置文件 1 месяц назад
  三七 adc64a1c9c 红包积分消耗统计/红包显示发送方式转换 1 месяц назад
  caoliqin 0c7332cb05 Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java 1 месяц назад
  caoliqin 27c9250450 feat:配置文件 1 месяц назад
  三七 1aa585754b 三猫网的服务号 1 месяц назад
  cgp 73a6d47903 企业微信数据智能专区-获取企微会话功能 1 месяц назад
  三七 7b72c0c4b4 直播订单 bug修复 1 месяц назад
  三七 47f19c386a cid-主动加微-获客链接-益寿源迁移版 1 месяц назад
  yuhongqi f1fa0bbb69 增加商城订单归属 1 месяц назад
  jzp d95a8dc2c8 1.济世百康直播支付 1 месяц назад
  三七 132b6fcabd cid-主动加微-获客链接-益寿源迁移版 1 месяц назад
  三七 ea96972b78 cid-主动加微-获客链接-益寿源迁移版 1 месяц назад
  三七 095ad68f87 cid-主动加微-获客链接-益寿源迁移版 1 месяц назад
  lk 9edd9367e9 dev.yml微走参数,aiapi参数配置 1 месяц назад
  lk ca3ee64ca3 今正微走订单推送-scrm,积分商城代码补齐 1 месяц назад
  三七 4bc023dcc6 积分统计 1 месяц назад
  yuhongqi 67973fa625 Merge remote-tracking branch 'origin/master' 1 месяц назад
  yuhongqi 4cdcb35f12 增加商城订单归属 1 месяц назад
100 измененных файлов с 5313 добавлено и 56 удалено
  1. 22 0
      fs-admin/src/main/java/com/fs/app/controller/AppArticleController.java
  2. 23 0
      fs-admin/src/main/java/com/fs/app/controller/AppCivilianGoodsController.java
  3. 87 0
      fs-admin/src/main/java/com/fs/app/controller/AppCompanyUserDeptController.java
  4. 45 0
      fs-admin/src/main/java/com/fs/app/controller/AppCourseController.java
  5. 88 0
      fs-admin/src/main/java/com/fs/app/controller/AppCourseFinishTempController.java
  6. 30 0
      fs-admin/src/main/java/com/fs/app/controller/AppCoursePlaySourceConfigController.java
  7. 109 0
      fs-admin/src/main/java/com/fs/app/controller/AppCustomerRoleController.java
  8. 40 0
      fs-admin/src/main/java/com/fs/app/controller/AppGenerateController.java
  9. 12 0
      fs-admin/src/main/java/com/fs/app/controller/AppGroupController.java
  10. 75 0
      fs-admin/src/main/java/com/fs/app/controller/AppInvitationCodeController.java
  11. 33 0
      fs-admin/src/main/java/com/fs/app/controller/AppLiveController.java
  12. 23 0
      fs-admin/src/main/java/com/fs/app/controller/AppMedicinesController.java
  13. 22 0
      fs-admin/src/main/java/com/fs/app/controller/AppOpenClassVideoController.java
  14. 22 0
      fs-admin/src/main/java/com/fs/app/controller/AppShortVideoController.java
  15. 122 0
      fs-admin/src/main/java/com/fs/app/controller/AppSopController.java
  16. 62 0
      fs-admin/src/main/java/com/fs/app/controller/AppSopLogsController.java
  17. 154 0
      fs-admin/src/main/java/com/fs/app/controller/AppSopTempController.java
  18. 62 0
      fs-admin/src/main/java/com/fs/app/controller/AppSopUserLogController.java
  19. 47 0
      fs-admin/src/main/java/com/fs/app/controller/AppSopUserLogInfoController.java
  20. 74 0
      fs-admin/src/main/java/com/fs/app/controller/AppUrgentClassTaskController.java
  21. 103 0
      fs-admin/src/main/java/com/fs/app/controller/AppUserChatLogsController.java
  22. 80 0
      fs-admin/src/main/java/com/fs/app/controller/AppUserController.java
  23. 144 0
      fs-admin/src/main/java/com/fs/app/controller/AppUserPortraitController.java
  24. 93 0
      fs-admin/src/main/java/com/fs/app/controller/AppWelcomeController.java
  25. 186 0
      fs-admin/src/main/java/com/fs/app/controller/CommonV2Controller.java
  26. 36 0
      fs-admin/src/main/java/com/fs/app/controller/FsUserInfoController.java
  27. 34 0
      fs-admin/src/main/java/com/fs/app/controller/VipTagController.java
  28. 98 0
      fs-admin/src/main/java/com/fs/app/controller/VipTagGroupController.java
  29. 79 0
      fs-admin/src/main/java/com/fs/app/controller/VipUserCustomerController.java
  30. 78 0
      fs-admin/src/main/java/com/fs/app/controller/VipUserTagController.java
  31. 116 0
      fs-admin/src/main/java/com/fs/app/job/UserActivityTask.java
  32. 97 0
      fs-admin/src/main/java/com/fs/app/job/UserConsumptionAmountTask.java
  33. 24 0
      fs-admin/src/main/java/com/fs/app/job/UserTagTask.java
  34. 201 0
      fs-admin/src/main/java/com/fs/app/job/VideoTrackTask.java
  35. 37 4
      fs-admin/src/main/java/com/fs/company/controller/CompanyConfigController.java
  36. 6 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java
  37. 514 0
      fs-admin/src/main/java/com/fs/course/controller/FsAppCourseWatchLogController.java
  38. 7 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  39. 38 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCommentController.java
  40. 5 0
      fs-admin/src/main/java/com/fs/his/controller/FsCourseCouponController.java
  41. 24 0
      fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java
  42. 7 0
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java
  43. 5 0
      fs-admin/src/main/java/com/fs/his/controller/FsPackageController.java
  44. 22 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  45. 1 1
      fs-admin/src/main/java/com/fs/his/task/userIntegralTask.java
  46. 1 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  47. 17 5
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  48. 56 0
      fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java
  49. 9 0
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  50. 1 1
      fs-admin/src/main/java/com/fs/third/controller/WeizouController.java
  51. 308 0
      fs-admin/src/main/java/com/fs/utils/AudioUtils.java
  52. 136 0
      fs-admin/src/main/java/com/fs/web/controller/system/AppDeptController.java
  53. 12 0
      fs-admin/src/main/java/com/fs/web/controller/system/SysConfigController.java
  54. 39 0
      fs-common/src/main/java/com/fs/common/constant/VideoRedisKeyConst.java
  55. 198 0
      fs-common/src/main/java/com/fs/common/core/domain/entity/AppDept.java
  56. 3 1
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  57. 1 0
      fs-company/src/main/java/com/fs/company/controller/common/CommonController.java
  58. 37 0
      fs-company/src/main/java/com/fs/company/controller/company/SysConfigController.java
  59. 7 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java
  60. 12 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  61. 8 1
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  62. 3 1
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  63. 1 1
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  64. 17 5
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  65. 4 2
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/job/QwMsgAuditScheduleJob.java
  66. 267 0
      fs-service/src/main/java/com/fs/app/civilgoods/domain/CivilGoods.java
  67. 17 0
      fs-service/src/main/java/com/fs/app/civilgoods/dto/CivilGoodsDTO.java
  68. 11 0
      fs-service/src/main/java/com/fs/app/civilgoods/mapper/CivilGoodsMapper.java
  69. 16 0
      fs-service/src/main/java/com/fs/app/civilgoods/service/ICivilGoodsService.java
  70. 69 0
      fs-service/src/main/java/com/fs/app/civilgoods/service/impl/CivilGoodsServiceImpl.java
  71. 10 0
      fs-service/src/main/java/com/fs/app/civilgoods/vo/CivilGoodsVO.java
  72. 252 0
      fs-service/src/main/java/com/fs/app/medicines/domain/AppFsStoreProduct.java
  73. 17 0
      fs-service/src/main/java/com/fs/app/medicines/dto/AppFsStoreProductDTO.java
  74. 9 0
      fs-service/src/main/java/com/fs/app/medicines/mapper/AppFsStoreProductMapper.java
  75. 16 0
      fs-service/src/main/java/com/fs/app/medicines/service/IAppFsStoreProductService.java
  76. 69 0
      fs-service/src/main/java/com/fs/app/medicines/service/impl/AppFsStoreProductServiceImpl.java
  77. 10 0
      fs-service/src/main/java/com/fs/app/medicines/vo/AppFsStoreProductVO.java
  78. 1 1
      fs-service/src/main/java/com/fs/app/sender/properties/ConfigProperties.java
  79. 4 0
      fs-service/src/main/java/com/fs/app/sop/dto/AppSopUserLogInfoDTO.java
  80. 2 0
      fs-service/src/main/java/com/fs/app/sop/mapper/AppSopUserLogInfoMapper.java
  81. 2 1
      fs-service/src/main/java/com/fs/app/sop/service/IAppSopUserLogInfoService.java
  82. 17 6
      fs-service/src/main/java/com/fs/app/sop/service/impl/AppSopUserLogInfoServiceImpl.java
  83. 11 0
      fs-service/src/main/java/com/fs/common/service/ISmsService.java
  84. 150 5
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  85. 15 9
      fs-service/src/main/java/com/fs/company/domain/CompanySms.java
  86. 19 0
      fs-service/src/main/java/com/fs/company/mapper/CompanySmsMapper.java
  87. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  88. 49 8
      fs-service/src/main/java/com/fs/company/service/ICompanySmsService.java
  89. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  90. 164 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanySmsServiceImpl.java
  91. 16 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  92. 0 0
      fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java
  93. 3 0
      fs-service/src/main/java/com/fs/config/ai/AiHostProper.java
  94. 1 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  95. 1 1
      fs-service/src/main/java/com/fs/course/domain/FsCourseAnswerReward.java
  96. 8 0
      fs-service/src/main/java/com/fs/course/domain/FsCoursePlaySourceConfig.java
  97. 3 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseComment.java
  98. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  99. 2 2
      fs-service/src/main/java/com/fs/course/domain/LuckyBagCollectRecord.java
  100. 15 0
      fs-service/src/main/java/com/fs/course/dto/VideoUpdateDTO.java

+ 22 - 0
fs-admin/src/main/java/com/fs/app/controller/AppArticleController.java

@@ -0,0 +1,22 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.his.service.IFsArticleService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/article")
+@RequiredArgsConstructor
+public class AppArticleController {
+
+    private final IFsArticleService fsArticleService;
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(String keyword, Long metaId, Long limit) {
+        return AjaxResult.success(fsArticleService.findOptions(keyword, metaId, limit));
+    }
+
+}

+ 23 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCivilianGoodsController.java

@@ -0,0 +1,23 @@
+package com.fs.app.controller;
+
+import com.fs.app.civilgoods.dto.CivilGoodsDTO;
+import com.fs.app.civilgoods.service.ICivilGoodsService;
+import com.fs.common.core.domain.AjaxResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/civilianGoods")
+@RequiredArgsConstructor
+public class AppCivilianGoodsController {
+
+    private final ICivilGoodsService appFsStoreProductScrmService;
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(CivilGoodsDTO req) {
+        return AjaxResult.success(appFsStoreProductScrmService.findOptions(req));
+    }
+
+}

+ 87 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCompanyUserDeptController.java

@@ -0,0 +1,87 @@
+package com.fs.app.controller;
+
+import com.fs.app.comuserdept.domain.AppCompanyUserDept;
+import com.fs.app.comuserdept.dto.AppCompanyUserDeptDTO;
+import com.fs.app.comuserdept.service.IAppCompanyUserDeptService;
+import com.fs.app.comuserdept.vo.AppCompanyUserDeptVO;
+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 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("/app/comuserdept")
+public class AppCompanyUserDeptController extends BaseController {
+
+    @Autowired
+    private IAppCompanyUserDeptService appCompanyUserDeptService;
+
+    /**
+     * 查询app-销售/客服绑定部门(与销售原部门无关联)列表
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AppCompanyUserDeptDTO appCompanyUserDept) {
+        startPage();
+        List<AppCompanyUserDeptVO> list = appCompanyUserDeptService.selectAppCompanyUserDeptList(appCompanyUserDept);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出app-销售/客服绑定部门(与销售原部门无关联)列表
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:export')")
+    @Log(title = "app-销售/客服绑定部门(与销售原部门无关联)", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppCompanyUserDeptDTO appCompanyUserDept) {
+        List<AppCompanyUserDeptVO> list = appCompanyUserDeptService.selectAppCompanyUserDeptList(appCompanyUserDept);
+        ExcelUtil<AppCompanyUserDeptVO> util = new ExcelUtil<>(AppCompanyUserDeptVO.class);
+        return util.exportExcel(list, "app-销售/客服绑定部门(与销售原部门无关联)数据");
+    }
+
+    /**
+     * 获取app-销售/客服绑定部门(与销售原部门无关联)详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:list')")
+    @GetMapping(value = "/getById/{id}")
+    public AjaxResult getById(@PathVariable Long id) {
+        return AjaxResult.success(appCompanyUserDeptService.selectAppCompanyUserDeptById(id));
+    }
+
+    /**
+     * 新增app-销售/客服绑定部门(与销售原部门无关联)
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:add')")
+    @Log(title = "app-销售/客服绑定部门(与销售原部门无关联)", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody AppCompanyUserDept appCompanyUserDept) {
+        return toAjax(appCompanyUserDeptService.insertAppCompanyUserDept(appCompanyUserDept));
+    }
+
+    /**
+     * 修改app-销售/客服绑定部门(与销售原部门无关联)
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:edit')")
+    @Log(title = "app-销售/客服绑定部门(与销售原部门无关联)", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody AppCompanyUserDept appCompanyUserDept) {
+        return toAjax(appCompanyUserDeptService.updateAppCompanyUserDept(appCompanyUserDept));
+    }
+
+    /**
+     * 删除app-销售/客服绑定部门(与销售原部门无关联)
+     */
+    @PreAuthorize("@ss.hasPermi('app:comuserdept:delete')")
+    @Log(title = "app-销售/客服绑定部门(与销售原部门无关联)", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByIds/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(appCompanyUserDeptService.deleteAppCompanyUserDeptByIds(ids));
+    }
+}

+ 45 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCourseController.java

@@ -0,0 +1,45 @@
+package com.fs.app.controller;
+
+import com.fs.app.course.service.IAppFsUserCourseService;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.his.vo.OptionsVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/course")
+@RequiredArgsConstructor
+public class AppCourseController extends BaseController {
+
+    private final IAppFsUserCourseService appFsUserCourseService;
+
+    /**
+     * 获取课程选项
+     *
+     * @param keyword
+     * @return
+     */
+    @GetMapping("/courseList")
+    public R courseList(String keyword, String selectedId) {
+        startPage();
+        return R.ok().put("list", appFsUserCourseService.findOptions(keyword, selectedId));
+    }
+
+    /**
+     * 获取指定课程的所有视频列表
+     * @param courseId
+     * @param title
+     * @return
+     */
+    @GetMapping(value = "/videoList")
+    public R videoList(Long courseId, String title) {
+        List<OptionsVO> optionsVOS = appFsUserCourseService.findVideoOptionsByCourse(courseId, title);
+        return R.ok().put("list", optionsVOS);
+    }
+
+}

+ 88 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCourseFinishTempController.java

@@ -0,0 +1,88 @@
+package com.fs.app.controller;
+
+import com.fs.app.course.dto.AppCourseFinishTempDTO;
+import com.fs.app.course.service.IAppCourseFinishTempService;
+import com.fs.app.course.vo.AppCourseFinishTempVO;
+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 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("/app/courseFinishTemp")
+public class AppCourseFinishTempController extends BaseController {
+
+    @Autowired
+    private IAppCourseFinishTempService appCourseFinishTempService;
+
+    /**
+     * 查询app-完课模板列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:list')")
+    @GetMapping("/findList")
+    public TableDataInfo list(AppCourseFinishTempDTO appCourseFinishTemp) {
+        startPage();
+        List<AppCourseFinishTempVO> list = appCourseFinishTempService.selectAppCourseFinishTempList(appCourseFinishTemp);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出app-完课模板列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:export')")
+    @Log(title = "app-完课模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppCourseFinishTempDTO appCourseFinishTemp) {
+        List<AppCourseFinishTempVO> list = appCourseFinishTempService.selectAppCourseFinishTempList(appCourseFinishTemp);
+        ExcelUtil<AppCourseFinishTempVO> util = new ExcelUtil<>(AppCourseFinishTempVO.class);
+        return util.exportExcel(list, "app-完课模板数据");
+    }
+
+    /**
+     * 获取app-完课模板详细信息
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:list')")
+    @Log(title = "app-完课模板", businessType = BusinessType.OTHER)
+    @GetMapping(value = "/getById/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(appCourseFinishTempService.selectAppCourseFinishTempById(id));
+    }
+
+    /**
+     * 新增app-完课模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:add')")
+    @Log(title = "app-完课模板", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody AppCourseFinishTempDTO appCourseFinishTemp) {
+        return toAjax(appCourseFinishTempService.insertAppCourseFinishTemp(appCourseFinishTemp));
+    }
+
+    /**
+     * 修改app-完课模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:edit')")
+    @Log(title = "app-完课模板", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody AppCourseFinishTempDTO appCourseFinishTemp) {
+        return toAjax(appCourseFinishTempService.updateAppCourseFinishTemp(appCourseFinishTemp));
+    }
+
+    /**
+     * 删除app-完课模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:finishTemp:delete')")
+    @Log(title = "app-完课模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByIds/{ids}")
+    public AjaxResult remove(@PathVariable List<Long> ids) {
+        return toAjax(appCourseFinishTempService.deleteAppCourseFinishTempByIds(ids));
+    }
+
+}

+ 30 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCoursePlaySourceConfigController.java

@@ -0,0 +1,30 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.domain.AppFsCoursePlaySourceConfig;
+import com.fs.app.sop.service.IAppCoursePlaySourceConfigService;
+import com.fs.app.sop.vo.AppCoursePlaySourceConfigVO;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.core.domain.R;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/cpsc")
+@RequiredArgsConstructor
+public class AppCoursePlaySourceConfigController {
+
+    private final IAppCoursePlaySourceConfigService appCoursePlaySourceConfigService;
+
+    @GetMapping(value = "/getList")
+    public R getCompanyInfo() {
+        //查询小程序名称
+        List<AppFsCoursePlaySourceConfig> cpsc = appCoursePlaySourceConfigService.lambdaQuery().list();
+        List<AppCoursePlaySourceConfigVO> vos = BeanCopyUtils.copyList(cpsc, AppCoursePlaySourceConfigVO.class);
+        return R.ok().put("data", vos);
+    }
+
+}

+ 109 - 0
fs-admin/src/main/java/com/fs/app/controller/AppCustomerRoleController.java

@@ -0,0 +1,109 @@
+package com.fs.app.controller;
+
+import cn.hutool.http.HttpRequest;
+import com.fs.app.cusrole.dto.AppCustomerRoleDTO;
+import com.fs.app.cusrole.service.IAppCustomerRoleService;
+import com.fs.app.cusrole.vo.AppCustomerRoleVO;
+import com.fs.app.user.vo.AppUserVO;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.StringUtils;
+import com.fs.im.service.OpenIMService;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/app/cusrole")
+@RequiredArgsConstructor
+public class AppCustomerRoleController extends BaseController {
+
+    private final IAppCustomerRoleService appCustomerRoleService;
+
+    @Autowired
+    private OpenIMService openIMService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(AppCustomerRoleDTO dto) {
+        startPage();
+        List<AppCustomerRoleVO> list = appCustomerRoleService.findList(dto);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("校验客服是否注册新的im")
+    @PostMapping("/accountCheck")
+    public R accountCheck(@RequestBody Map<String, String> userIdMap){
+        //获取管理员token
+        String userId = userIdMap.get("userId");
+        String adminToken = openIMService.getAdminToken();
+        JSONObject requestBody = new JSONObject();
+        // 解析响应
+        if (StringUtils.isNotEmpty(adminToken)) {
+            //查询用户是否注册
+            ArrayList<String> userIds = new ArrayList<>();
+            requestBody = new JSONObject();
+            userIds.add(userId);
+            requestBody.put("checkUserIDs", userIds);
+            String body = HttpRequest.post("https://web.im.cdwjyyh.com/api/user/account_check")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(requestBody.toString())
+                    .execute()
+                    .body();
+            JSONObject jsonObject = new JSONObject(body);
+            JSONArray results = jsonObject.getJSONObject("data").getJSONArray("results");
+            if (results != null && results.length() > 0) {
+                JSONObject resultObj = results.getJSONObject(0);
+                int accountStatus = resultObj.getInt("accountStatus");
+                //未注册自动注册
+                if (accountStatus==0){
+                    String s = userId.replaceFirst("^C", "");
+                    AppUserVO appUserVO = appCustomerRoleService.selectById(Long.parseLong(s));
+                    if (null==appUserVO){
+                        return R.error("用户不存在");
+                    }
+                    ArrayList<Object> users = new ArrayList<>();
+                    HashMap<String, String> map = new HashMap<>();
+                    map.put("userID",userId);
+                    map.put("nickname",appUserVO.getUsername());
+                    map.put("faceURL",appUserVO.getAvatar());
+                    users.add(map);
+                    requestBody = new JSONObject();
+                    userIds.add(userId);
+                    requestBody.put("users", users);
+                    HttpRequest.post("https://web.im.cdwjyyh.com/api/user/user_register")
+                            .header("operationID", String.valueOf(System.currentTimeMillis()))
+                            .header("token", adminToken).body(requestBody.toString()).execute().body();
+                }
+            } else {
+                return R.error("返回结果为空");
+            }
+           /* HashMap<String, String> tokenMap = new HashMap<>();
+            tokenMap.put("platformID","1");
+            tokenMap.put("userID",userId);*/
+            requestBody = new JSONObject();
+            requestBody.put("platformID",5);
+            requestBody.put("userID",userId);
+            String body1 = HttpRequest.post("https://web.im.cdwjyyh.com/api/auth/get_user_token")
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .header("token", adminToken)
+                    .body(requestBody.toString()).execute().body();
+            JSONObject userJson = new JSONObject(body1);
+            JSONObject userData = userJson.getJSONObject("data");
+            String userToken = userData.getString("token");
+            return R.ok().put("token", userToken);
+        } else {
+            return R.error("获取管理员token失败");
+        }
+    }
+
+}

+ 40 - 0
fs-admin/src/main/java/com/fs/app/controller/AppGenerateController.java

@@ -0,0 +1,40 @@
+package com.fs.app.controller;
+
+import com.fs.app.cusrole.mapper.AppCustomerRoleMapper;
+import com.fs.app.sop.service.impl.AppSopUserLogsInfoHandle;
+import com.fs.common.core.domain.R;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/generate")
+@RequiredArgsConstructor
+public class AppGenerateController {
+
+    private final AppCustomerRoleMapper appCustomerRoleMapper;
+
+    private final AppSopUserLogsInfoHandle appSopUserLogsInfoHandle;
+
+    /**
+     * 加入或创建营期
+     *
+     * @param userIds
+     * @return
+     */
+    @GetMapping("/joinOrCreateUserLogs/{userIds}")
+    public R joinOrCreateUserLogs(@PathVariable List<Long> userIds) {
+        for (Long userId : userIds) {
+            List<Long> customerIds = appCustomerRoleMapper.getCustomerIdsByUserId(userId);
+            customerIds.forEach(customerId -> {
+                this.appSopUserLogsInfoHandle.joinOrCreateSopUserLogsHandle(customerId, userId);
+            });
+        }
+        return R.ok();
+    }
+
+}

+ 12 - 0
fs-admin/src/main/java/com/fs/app/controller/AppGroupController.java

@@ -0,0 +1,12 @@
+package com.fs.app.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/groupChat")
+@RequiredArgsConstructor
+public class AppGroupController {
+
+}

+ 75 - 0
fs-admin/src/main/java/com/fs/app/controller/AppInvitationCodeController.java

@@ -0,0 +1,75 @@
+package com.fs.app.controller;
+
+import com.fs.app.invitation.dto.AppInvitationCodeDTO;
+import com.fs.app.invitation.service.IAppInvitationCodeService;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * app-邀请码管理
+ */
+@RestController
+@RequestMapping("/app/invCode")
+@RequiredArgsConstructor
+public class AppInvitationCodeController extends BaseController {
+
+    private final IAppInvitationCodeService appInvitationCodeService;
+
+    /**
+     * 列表查询
+     * @param reqData
+     * @return
+     */
+    @GetMapping("/findList")
+    @PreAuthorize("@ss.hasAnyPermi('app:invite:list')")
+    public TableDataInfo findList(AppInvitationCodeDTO reqData) {
+        startPage();
+        return getDataTable(appInvitationCodeService.findList(reqData));
+    }
+
+    /**
+     * 新增编辑保存
+     * @param reqData
+     * @return
+     */
+    @PostMapping("/save")
+    @PreAuthorize("@ss.hasAnyPermi('app:invite:add,app:invite:edit')")
+    @Log(title = "邀请码", businessType = BusinessType.UPDATE)
+    public R saveAddOrEdit(@RequestBody AppInvitationCodeDTO reqData) {
+        return appInvitationCodeService.saveAddOrEdit(reqData);
+    }
+
+    /**
+     * 根据id获取指定记录详情
+     * @param reqData
+     * @return
+     */
+    @GetMapping("/getById")
+    @PreAuthorize("@ss.hasAnyPermi('app:invite:list')")
+    public AjaxResult getById(AppInvitationCodeDTO reqData) {
+        return AjaxResult.success(appInvitationCodeService.getById(reqData));
+    }
+
+    /**
+     * 邀请码删除
+     * @param ids
+     * @return
+     */
+    @DeleteMapping("/deleteByIds/{ids}")
+    @PreAuthorize("@ss.hasAnyPermi('app:invite:delete')")
+    @Log(title = "邀请码", businessType = BusinessType.DELETE)
+    public R deleteByIds(@PathVariable List<Long> ids) {
+        this.appInvitationCodeService.deleteByIds(ids);
+        return R.ok();
+    }
+
+}

+ 33 - 0
fs-admin/src/main/java/com/fs/app/controller/AppLiveController.java

@@ -0,0 +1,33 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.live.domain.Live;
+import com.fs.live.service.ILiveService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/live")
+@RequiredArgsConstructor
+public class AppLiveController extends BaseController {
+
+    private final ILiveService liveService;
+
+    /**
+     * 查询直播列表
+     */
+//    @PreAuthorize("@ss.hasPermi('live:live:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(Live live)
+    {
+        startPage();
+        List<Live> list = liveService.selectLiveList(live);
+        return getDataTable(list);
+    }
+
+}

+ 23 - 0
fs-admin/src/main/java/com/fs/app/controller/AppMedicinesController.java

@@ -0,0 +1,23 @@
+package com.fs.app.controller;
+
+import com.fs.app.medicines.dto.AppFsStoreProductDTO;
+import com.fs.app.medicines.service.IAppFsStoreProductService;
+import com.fs.common.core.domain.AjaxResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/medicines")
+@RequiredArgsConstructor
+public class AppMedicinesController {
+
+    private final IAppFsStoreProductService appFsStoreProductService;
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(AppFsStoreProductDTO req) {
+        return AjaxResult.success(appFsStoreProductService.findOptions(req));
+    }
+
+}

+ 22 - 0
fs-admin/src/main/java/com/fs/app/controller/AppOpenClassVideoController.java

@@ -0,0 +1,22 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.course.service.IFsUserCourseVideoService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/openClassVideo")
+@RequiredArgsConstructor
+public class AppOpenClassVideoController {
+
+    private final IFsUserCourseVideoService fsUserCourseVideoService;
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(String keyword, Long metaId, Long limit) {
+        return AjaxResult.success(fsUserCourseVideoService.findOptions(keyword, metaId, limit));
+    }
+
+}

+ 22 - 0
fs-admin/src/main/java/com/fs/app/controller/AppShortVideoController.java

@@ -0,0 +1,22 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.course.service.IFsUserVideoService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/app/shortVideo")
+@RequiredArgsConstructor
+public class AppShortVideoController {
+
+    private final IFsUserVideoService fsUserVideoService;
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(String keyword, Long metaId, Long limit) {
+        return AjaxResult.success(fsUserVideoService.findOptions(keyword, metaId, limit));
+    }
+
+}

+ 122 - 0
fs-admin/src/main/java/com/fs/app/controller/AppSopController.java

@@ -0,0 +1,122 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.dto.AppSopDTO;
+import com.fs.app.sop.service.IAppSopService;
+import com.fs.app.sop.vo.AppSopVO;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/sop")
+@RequiredArgsConstructor
+public class AppSopController extends BaseController {
+
+
+    private final IAppSopService appSopService;
+
+    /**
+     * 查询 sop 列表
+     */
+    @PreAuthorize("@ss.hasPermi('app:sop:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AppSopDTO sopDTO) {
+        startPage();
+        List<AppSopVO> list = appSopService.findList(sopDTO);
+        return getDataTable(list);
+    }
+
+    /**
+     * 新增 sop
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:add')")
+    @Log(title = "appSop", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody AppSopDTO sop) {
+        int count = appSopService.add(sop);
+//        if (count > 0) {
+//            if (qwSop.getQwUserIds() != null) {
+//                updateTempVoiceInfo(qwSop);
+//            }
+//        }
+        return toAjax(count);
+    }
+
+    /**
+     * 修改 sop
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:edit')")
+    @Log(title = "修改appSop", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    public R edit(@RequestBody AppSopDTO sopDTO) {
+        R sop = appSopService.edit(sopDTO);
+//        String code = sop.get("code").toString();
+//        if(code.equals("200")){
+//            if(qwSop != null && qwSop.getQwUserIds() != null){
+//                updateTempVoiceInfo(qwSop);
+//            }
+//        }
+        return sop;
+    }
+
+    /**
+     * 删除 sop
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:delete')")
+    @Log(title = "删除appSop", businessType = BusinessType.DELETE)
+    @DeleteMapping("/delete/{ids}")
+    public AjaxResult remove(@PathVariable List<String> ids) {
+        return toAjax(appSopService.deleteSopByIds(ids));
+    }
+
+    /**
+     * 根据id获取 sop详情
+     */
+    @PreAuthorize("@ss.hasPermi('app:sop:list')")
+    @GetMapping("/getById")
+    public AjaxResult getById(String id) {
+        return AjaxResult.success(appSopService.getById(id));
+    }
+
+    /**
+     * 修改 sop 自动创建时间
+     */
+    @Log(title = "appSop自动创建时间", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:edit')")
+    @PostMapping("/updateAutoSopTime")
+    public AjaxResult updateAutoSopTime(@RequestBody AppSopDTO appSopReq) {
+        return toAjax(appSopService.updateAutoSopTime(appSopReq));
+    }
+
+    /**
+     * 更新执行状态
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:exec')")
+    @Log(title = "修改执行状态", businessType = BusinessType.UPDATE)
+    @GetMapping(value = "/updateStatus/{ids}")
+    public R updateStatus(@PathVariable List<String> ids) {
+        return appSopService.updateStatusByIds(ids);
+    }
+
+    /**
+     * 导出sop列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:sop:export')")
+    @Log(title = "sop", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppSopDTO sopDTO) {
+        List<AppSopVO> list = appSopService.findList(sopDTO);
+        ExcelUtil<AppSopVO> util = new ExcelUtil<>(AppSopVO.class);
+        return util.exportExcel(list, "自动化数据");
+    }
+
+}

+ 62 - 0
fs-admin/src/main/java/com/fs/app/controller/AppSopLogsController.java

@@ -0,0 +1,62 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.dto.AppSopLogDTO;
+import com.fs.app.sop.service.IAppSopLogsService;
+import com.fs.app.sop.vo.AppSopLogsVO;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/sopLogs")
+@RequiredArgsConstructor
+public class AppSopLogsController extends BaseController {
+
+    private final IAppSopLogsService appSopLogsService;
+
+    @GetMapping("/findList")
+    public TableDataInfo findList(AppSopLogDTO param) {
+        startPage();
+        return getDataTable(this.appSopLogsService.findList(param));
+    }
+
+    /**
+     * 删除SOP  定时任务
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:sopLogs:remove')")
+    @Log(title = "SOP  定时任务", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByIds/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(appSopLogsService.deleteByIds(ids));
+    }
+
+    /**
+     * 修改SOP  定时任务-只修改完课的补发
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:sopLogs:editCourse')")
+    @Log(title = "修改SOP只修改完课", businessType = BusinessType.UPDATE)
+    @PutMapping("/editCourseSopLogs/{ids}")
+    public AjaxResult editCourseQwSopLogs(@PathVariable Long[] ids) {
+        return toAjax(appSopLogsService.editCourseSopLogs(ids));
+    }
+
+    /**
+     * 导出企业微信SOP  定时任务列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:sopLogs:export')")
+    @Log(title = "SOP  定时任务", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppSopLogDTO param) {
+        List<AppSopLogsVO> list = appSopLogsService.findList(param);
+        ExcelUtil<AppSopLogsVO> util = new ExcelUtil<>(AppSopLogsVO.class);
+        return util.exportExcel(list, "SOP 定时任务数据");
+    }
+
+}

+ 154 - 0
fs-admin/src/main/java/com/fs/app/controller/AppSopTempController.java

@@ -0,0 +1,154 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.dto.AppSopTempDTO;
+import com.fs.app.sop.dto.AppSopTempDayDTO;
+import com.fs.app.sop.dto.AppSopTempDaySortDTO;
+import com.fs.app.sop.service.IAppSopTempService;
+import com.fs.app.sop.vo.AppSopTempVO;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ *
+ * @author hulin
+ */
+@RestController
+@RequestMapping("/app/sopTemp")
+@RequiredArgsConstructor
+public class AppSopTempController extends BaseController {
+
+    private final IAppSopTempService appSopTempService;
+
+    /**
+     * 查询 sop 模板列表
+     */
+    @GetMapping("/list")
+    @PreAuthorize("@ss.hasAnyPermi('app:template:list')")
+    public TableDataInfo list(AppSopTempDTO param) {
+        startPage();
+        List<AppSopTempVO> list = appSopTempService.findList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出sop模板列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:template:export')")
+    @Log(title = "sop模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppSopTempDTO param) {
+        List<AppSopTempVO> list = appSopTempService.findList(param);
+        ExcelUtil<AppSopTempVO> util = new ExcelUtil(AppSopTempVO.class);
+        return util.exportExcel(list, "sop模板数据");
+    }
+
+    /**
+     * 获取 sop 模板详细信息
+     */
+    @GetMapping(value = "/{id}")
+    @PreAuthorize("@ss.hasAnyPermi('app:template:list')")
+    public AjaxResult getInfo(@PathVariable String id) {
+        return AjaxResult.success(appSopTempService.getOneById(id));
+    }
+
+    /**
+     * 新增 sop 模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:template:add')")
+    @Log(title = "sop模板", businessType = BusinessType.INSERT)
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody AppSopTempDTO param) {
+        return toAjax(appSopTempService.addSopTemp(param));
+    }
+
+    /**
+     * 修改 sop 模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:template:edit')")
+    @Log(title = "sop模板", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody AppSopTempDTO param) {
+        return toAjax(appSopTempService.editSopTemp(param));
+    }
+
+    /**
+     * 删除 sop 模板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:template:delete,app:template:stop')")
+    @Log(title = "sop模板", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable String[] ids) {
+        return toAjax(appSopTempService.deleteSopTemp(ids));
+    }
+
+
+    /**
+     * 模板复制
+     *
+     * @param param
+     * @return
+     */
+    @Log(title = "sop模板复制", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasAnyPermi('app:template:copy')")
+    @PostMapping("/copy")
+    public AjaxResult copyTemplate(@RequestBody AppSopTempDTO param) {
+        appSopTempService.copyTemplate(param);
+        return toAjax(1);
+    }
+
+    /**
+     * 新增/编辑 保存模板天数
+     *
+     * @param day
+     * @return
+     */
+    @Log(title = "添加修改sop模板天数", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasAnyPermi('app:template:edit')")
+    @PostMapping("/addOrUpdateSetting")
+    public AjaxResult addOrUpdateSetting(@RequestBody AppSopTempDayDTO day) {
+        return AjaxResult.success(appSopTempService.addOrUpdateSetting(day));
+    }
+
+    /**
+     * 获取规则信息
+     *
+     * @param id
+     * @return
+     */
+    @GetMapping("/selectRulesInfo")
+    public AjaxResult selectRulesInfo(Long id) {
+        return AjaxResult.success(appSopTempService.selectRulesInfo(id));
+    }
+
+
+    @GetMapping("/dayList")
+    public AjaxResult dayList(String id) {
+        return AjaxResult.success(appSopTempService.dayList(id));
+    }
+
+    @Log(title = "排序天数", businessType = BusinessType.UPDATE)
+    @PostMapping("/sortDay")
+    public AjaxResult sortDay(@RequestBody List<AppSopTempDaySortDTO> list) {
+        appSopTempService.sortDay(list);
+        return toAjax(1);
+    }
+
+    // 更新模板图片
+    @Log(title = "更新模板图片", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasAnyPermi('app:template:updateImage')")
+    @GetMapping("/updateImage")
+    public AjaxResult updateImage(String id){
+        appSopTempService.updateImage(id);
+        return AjaxResult.success();
+    }
+
+}

+ 62 - 0
fs-admin/src/main/java/com/fs/app/controller/AppSopUserLogController.java

@@ -0,0 +1,62 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.dto.AppGroupSendMessageDTO;
+import com.fs.app.sop.dto.AppSopUserLogDTO;
+import com.fs.app.sop.service.IAppSopUserLogService;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/userLogs")
+@RequiredArgsConstructor
+public class AppSopUserLogController extends BaseController {
+
+    private final IAppSopUserLogService appSopUserLogService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(AppSopUserLogDTO dto) {
+        startPage();
+        return getDataTable(this.appSopUserLogService.findList(dto));
+    }
+
+    /**
+     * 删除sopUserLogs
+     */
+//    @PreAuthorize("@ss.hasPermi('qwSop:sopUserLogs:remove')")
+    @Log(title = "删除sopUserLogs", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable List<String> ids) {
+        return toAjax(appSopUserLogService.deleteByIds(ids));
+    }
+
+
+    /**
+     * 修改营期时间
+     */
+//    @PreAuthorize("@ss.hasPermi('qwSop:sopUserLogs:updateTime')")
+    @Log(title = "批量修改 sopUserLogs", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchUpdateSopUserLogsToTime")
+    public R updateTime(@RequestBody AppSopUserLogDTO dto) {
+        return appSopUserLogService.batchUpdateSopUserLogsToTime(dto);
+    }
+
+    /**
+     * 一键群发
+     * @param appGroupSendMessageDTO
+     * @return
+     */
+    @PostMapping("/groupSendMessage")
+    public R groupSendMessage(@RequestBody AppGroupSendMessageDTO appGroupSendMessageDTO) {
+        this.appSopUserLogService.groupSendMessage(appGroupSendMessageDTO);
+        return R.ok();
+    }
+
+}

+ 47 - 0
fs-admin/src/main/java/com/fs/app/controller/AppSopUserLogInfoController.java

@@ -0,0 +1,47 @@
+package com.fs.app.controller;
+
+import com.fs.app.sop.dto.AppSopUserLogInfoDTO;
+import com.fs.app.sop.service.IAppSopUserLogInfoService;
+import com.fs.common.annotation.Log;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/userLogsInfo")
+@RequiredArgsConstructor
+public class AppSopUserLogInfoController {
+
+    private final IAppSopUserLogInfoService appSopUserLogInfoService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(AppSopUserLogInfoDTO dto) {
+        return this.appSopUserLogInfoService.findList(dto);
+    }
+
+    /**
+     * 删除 sopUserLogsInfo
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:sopUserLogsInfo:remove')")
+    @Log(title = "deleteByIds", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable List<Long> ids) {
+        return appSopUserLogInfoService.deleteByIds(ids) > 0 ? AjaxResult.success() : AjaxResult.error();
+    }
+
+    /**
+     * 营期详情内,修改具体用户所属的营期
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:sopUserLogsInfo:edit')")
+    @Log(title = "updateSopUserLogsInfo", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchUpdateSopUserLogsInfoToTime")
+    public R edit(@RequestBody AppSopUserLogInfoDTO dto) {
+        return appSopUserLogInfoService.batchUpdateSopUserLogsInfoToTime(dto);
+    }
+
+}

+ 74 - 0
fs-admin/src/main/java/com/fs/app/controller/AppUrgentClassTaskController.java

@@ -0,0 +1,74 @@
+package com.fs.app.controller;
+
+import com.fs.app.task.dto.AppWorkTaskDTO;
+import com.fs.app.task.service.IAppWorkTaskService;
+import com.fs.app.task.vo.AppWorkTaskVO;
+import com.fs.app.watchlog.service.IAppCourseWatchLogService;
+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.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * app-任务看板Controller
+ *
+ * @author fs
+ * @date 2026-03-03
+ */
+@RestController
+@RequestMapping("/app/urgentClass")
+public class AppUrgentClassTaskController extends BaseController {
+
+    @Autowired
+    private IAppWorkTaskService appWorkTaskService;
+
+    @Autowired
+    private IAppCourseWatchLogService appCourseWatchLogService;
+
+
+    /**
+     * 查询app-任务看板列表
+     */
+    @GetMapping("/list")
+    @PreAuthorize("@ss.hasAnyPermi('app:urgentClass:list')")
+    public TableDataInfo list(AppWorkTaskDTO appWorkTask) {
+        startPage();
+        List<AppWorkTaskVO> list = appWorkTaskService.selectAppWorkTaskList(appWorkTask);
+        for (AppWorkTaskVO appWorkTaskVO : list) {
+            appWorkTaskVO.setLatestCourseName(appCourseWatchLogService.recentWatchVideoName(appWorkTaskVO.getFsUserId(), appWorkTaskVO.getCustomerRoleId()));
+            appWorkTaskVO.setUserWatchLogs(this.appCourseWatchLogService.getAppUserWatchLogByDaysAndUserId(7, appWorkTaskVO.getFsUserId(), appWorkTaskVO.getCustomerRoleId()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 修改app-任务看板
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:urgentClass:handle')")
+    @Log(title = "催课看板", businessType = BusinessType.UPDATE)
+    @PostMapping("/setStatus")
+    public AjaxResult setStatus(@RequestBody AppWorkTaskDTO appWorkTask) {
+        return toAjax(appWorkTaskService.setStatus(appWorkTask));
+    }
+
+    /**
+     * 查询app-任务看板用户电话
+     */
+    @GetMapping("/getFsUserPhone/{fsUserId}")
+    @PreAuthorize("@ss.hasAnyPermi('app:urgentClass:getFsUserPhone')")
+    public AjaxResult getFsUserPhone(@PathVariable Long fsUserId) {
+        String phone = appWorkTaskService.getFsUserPhone(fsUserId);
+        if(StringUtils.isNotBlank(phone)){
+            return AjaxResult.success("查询成功!",phone);
+        }else{
+            return AjaxResult.error("用户电话不存在");
+        }
+    }
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/app/controller/AppUserChatLogsController.java

@@ -0,0 +1,103 @@
+package com.fs.app.controller;
+
+import com.fs.app.chat.domain.AppUserChatLogs;
+import com.fs.app.chat.dto.AppUserChatLogsDTO;
+import com.fs.app.chat.service.IAppUserChatLogsService;
+import com.fs.app.chat.vo.AppUserChatLogsVO;
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * app-客户与客服对话(问答)Controller
+ *
+ * @author fs
+ * @date 2026-04-07
+ */
+@RestController
+@RequestMapping("/app/userChatLogs")
+public class AppUserChatLogsController extends BaseController {
+
+    @Autowired
+    private IAppUserChatLogsService appUserChatLogsService;
+
+    /**
+     * 查询app-客户与客服对话(问答)列表
+     */
+//    @PreAuthorize("@ss.hasPermi('app:userChatLogs:list')")
+    @GetMapping("/findList")
+    public TableDataInfo findList(AppUserChatLogsDTO appUserChatLogs) {
+        startPage();
+        List<AppUserChatLogsVO> list = appUserChatLogsService.selectAppUserChatLogsList(appUserChatLogs);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出app-客户与客服对话(问答)列表
+     */
+    @PreAuthorize("@ss.hasPermi('app:userChatLogs:export')")
+    @Log(title = "app-客户与客服对话(问答)", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppUserChatLogsDTO appUserChatLogs) {
+        List<AppUserChatLogsVO> list = appUserChatLogsService.selectAppUserChatLogsList(appUserChatLogs);
+        ExcelUtil<AppUserChatLogsVO> util = new ExcelUtil<>(AppUserChatLogsVO.class);
+        return util.exportExcel(list, "app-客户与客服对话(问答)数据");
+    }
+
+    /**
+     * 获取app-客户与客服对话(问答)详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('app:userChatLogs:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(appUserChatLogsService.selectAppUserChatLogsById(id));
+    }
+
+    /**
+     * 新增app-客户与客服对话(问答)
+     */
+    @PreAuthorize("@ss.hasPermi('app:userChatLogs:add')")
+    @Log(title = "app-客户与客服对话(问答)", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AppUserChatLogs appUserChatLogs) {
+        return toAjax(appUserChatLogsService.insertAppUserChatLogs(appUserChatLogs));
+    }
+
+    /**
+     * 修改app-客户与客服对话(问答)
+     */
+//    @PreAuthorize("@ss.hasPermi('app:userChatLogs:edit')")
+    @Log(title = "app-客户与客服对话(问答)", businessType = BusinessType.UPDATE)
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody AppUserChatLogsDTO req) {
+        return AjaxResult.success("ok", appUserChatLogsService.updateAppUserChatLogs(req));
+    }
+
+    /**
+     * 删除app-客户与客服对话(问答)
+     */
+    @PreAuthorize("@ss.hasPermi('app:userChatLogs:remove')")
+    @Log(title = "app-客户与客服对话(问答)", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(appUserChatLogsService.deleteAppUserChatLogsByIds(ids));
+    }
+
+    /**
+     * 查询当前用户和当前客服的最新对话信息
+     *
+     * @return
+     */
+    @GetMapping("/findChatLogsByUserIdAndCustomerId")
+    public AjaxResult findChatLogsByUserIdAndCustomerId(@RequestParam Long userId, @RequestParam Long customerId) {
+        return AjaxResult.success(appUserChatLogsService.findChatLogsByUserIdAndCustomerId(userId, customerId));
+    }
+}

+ 80 - 0
fs-admin/src/main/java/com/fs/app/controller/AppUserController.java

@@ -0,0 +1,80 @@
+package com.fs.app.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.app.user.dto.AppUserDTO;
+import com.fs.app.user.service.IAppUserService;
+import com.fs.app.user.vo.AppUserVO;
+import com.fs.common.annotation.Log;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+import static com.fs.his.utils.PhoneUtil.encryptPhoneOldKey;
+
+@RestController
+@RequestMapping("/app/user")
+@RequiredArgsConstructor
+public class AppUserController {
+
+    private final IAppUserService userService;
+
+
+
+    /**
+     * 筛选列表
+     *
+     * @param userDTO
+     * @return
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(AppUserDTO userDTO) {
+        return userService.list(userDTO);
+    }
+
+    /**
+     * 数据列表
+     *
+     * @param userDTO
+     * @return
+     */
+    @GetMapping("/findList")
+    @PreAuthorize("@ss.hasPermi('app:user:list')")
+    public TableDataInfo findList(AppUserDTO userDTO) {
+        //原本有两种手机号加密方式,为了兼容,故两种都需要计算
+        if (ObjectUtil.isNotEmpty(userDTO.getPhone())) {
+            userDTO.setEncryptPhone(encryptPhone(userDTO.getPhone()));
+            userDTO.setEncryptPhone2(encryptPhoneOldKey(userDTO.getPhone()));
+        }
+        return userService.findList(userDTO);
+    }
+
+    /**
+     * 导出用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('app:user:export')")
+    @Log(title = "用户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppUserDTO userDTO) {
+        //原本有两种手机号加密方式,为了兼容,故两种都需要计算
+        if (ObjectUtil.isNotEmpty(userDTO.getPhone())) {
+            userDTO.setEncryptPhone(encryptPhone(userDTO.getPhone()));
+            userDTO.setEncryptPhone2(encryptPhoneOldKey(userDTO.getPhone()));
+        }
+        List<AppUserVO> list = userService.tableList(userDTO);
+        if (list.size() > 10000) {
+            return AjaxResult.error("导出数据不可超过1w条");
+        }
+        ExcelUtil<AppUserVO> util = new ExcelUtil<>(AppUserVO.class);
+        return util.exportExcel(list, "用户数据");
+    }
+
+}

+ 144 - 0
fs-admin/src/main/java/com/fs/app/controller/AppUserPortraitController.java

@@ -0,0 +1,144 @@
+package com.fs.app.controller;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.app.tag.domain.FsTag;
+import com.fs.app.tag.dto.FsContentTagDTO;
+import com.fs.app.tag.service.IFsTagService;
+import com.fs.app.user.domain.FsUserPortrait;
+import com.fs.app.user.domain.FsUserPortraitTag;
+import com.fs.app.user.service.IFsUserPortraitContentTagService;
+import com.fs.app.user.service.IFsUserPortraitService;
+import com.fs.app.user.service.IFsUserPortraitTagService;
+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.page.TableDataInfo;
+import com.fs.his.utils.PhoneUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@RestController
+@RequestMapping("/app/user/portrait")
+@RequiredArgsConstructor
+public class AppUserPortraitController extends BaseController {
+
+    @Autowired
+    private IFsUserPortraitService userService;
+
+
+    @Autowired
+    private IFsUserPortraitContentTagService contentTagService;
+    @Autowired
+    private IFsUserPortraitTagService userPortraitTagService;
+    @Autowired
+    private IFsTagService fsTagService;
+
+    /**
+     * 数据列表
+     *
+     * @param userDTO
+     * @return
+     */
+    @GetMapping("/findList")
+    //@PreAuthorize("@ss.hasPermi('app:user:list')")
+    public TableDataInfo findList(FsUserPortrait userDTO) {
+        LambdaQueryWrapper<FsUserPortrait> wrapper = new LambdaQueryWrapper<>();
+        //原本有两种手机号加密方式,为了兼容,故两种都需要计算
+        if (ObjectUtil.isNotEmpty(userDTO.getPhone())) {
+            String s = PhoneUtil.encryptPhone(userDTO.getPhone());
+            String s1 = PhoneUtil.encryptPhoneOldKey(userDTO.getPhone());
+            wrapper.in(FsUserPortrait::getPhone, s, s1);
+        }
+        wrapper.eq(ObjectUtil.isNotEmpty(userDTO.getNickName()),FsUserPortrait::getNickName, userDTO.getNickName());
+        wrapper.eq(ObjectUtil.isNotEmpty(userDTO.getUserId()),FsUserPortrait::getUserId, userDTO.getUserId());
+        wrapper.eq(ObjectUtil.isNotEmpty(userDTO.getActivityLevel()),FsUserPortrait::getActivityLevel, userDTO.getActivityLevel());
+        wrapper.eq(ObjectUtil.isNotEmpty(userDTO.getConsumptionLevel()),FsUserPortrait::getConsumptionLevel, userDTO.getConsumptionLevel());
+        int total = userService.count(wrapper);
+        startPage();
+        List<FsUserPortrait> list = userService.list(wrapper);
+        if (CollUtil.isNotEmpty(list)){
+            List<Long> userIds = list.stream()
+                    .map(FsUserPortrait::getUserId)
+                    .collect(Collectors.toList());
+
+            List<FsUserPortraitTag> relationList = userPortraitTagService.list(
+                    new LambdaQueryWrapper<FsUserPortraitTag>()
+                            .in(FsUserPortraitTag::getUserId, userIds)
+            );
+
+            Set<Long> tagIds = relationList.stream()
+                    .map(FsUserPortraitTag::getTagId)
+                    .collect(Collectors.toSet());
+
+            if (CollUtil.isNotEmpty(tagIds)) {
+                Map<Long, String> tagMap = fsTagService.list(
+                        new LambdaQueryWrapper<FsTag>()
+                                .in(FsTag::getId, tagIds)
+                ).stream().collect(Collectors.toMap(FsTag::getId, FsTag::getName));
+
+                Map<Long, List<FsUserPortraitTag>> userTagMap = relationList.stream()
+                        .collect(Collectors.groupingBy(FsUserPortraitTag::getUserId));
+
+                for (FsUserPortrait user : list) {
+                    List<FsUserPortraitTag> tags = userTagMap.get(user.getUserId());
+                    if (tags == null || tags.isEmpty()) {
+                        user.setInterestTags("");
+                        continue;
+                    }
+                    String result = tags.stream()
+                            .map(t -> {
+                                String tagName = tagMap.get(t.getTagId());
+                                Integer weight = t.getWeight(); // 假设字段叫 weight
+                                return tagName + "|" + weight;
+                            })
+                            .collect(Collectors.joining(","));
+                    user.setInterestTags(result);
+                }
+            }
+        }
+        TableDataInfo tableDataInfo = new TableDataInfo();
+        tableDataInfo.setCode(HttpStatus.SUCCESS);
+        tableDataInfo.setMsg("查询成功");
+        tableDataInfo.setTotal(total);
+        tableDataInfo.setRows(list);
+        return tableDataInfo;
+    }
+//
+//    /**
+//     * 导出用户列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('app:user:export')")
+//    @Log(title = "用户", businessType = BusinessType.EXPORT)
+//    @GetMapping("/export")
+//    public AjaxResult export(AppUserDTO userDTO) {
+//        //原本有两种手机号加密方式,为了兼容,故两种都需要计算
+//        if (ObjectUtil.isNotEmpty(userDTO.getPhone())) {
+//            userDTO.setEncryptPhone(encryptPhone(userDTO.getPhone()));
+//            userDTO.setEncryptPhone2(encryptPhoneOldKey(userDTO.getPhone()));
+//        }
+//        List<AppUserVO> list = userService.tableList(userDTO);
+//        if (list.size() > 10000) {
+//            return AjaxResult.error("导出数据不可超过1w条");
+//        }
+//        ExcelUtil<AppUserVO> util = new ExcelUtil<>(AppUserVO.class);
+//        return util.exportExcel(list, "用户数据");
+//    }
+
+    /**
+     * 批量增加内容的标签
+     */
+    @PostMapping("/bind/tag")
+    public AjaxResult bindTag(@RequestBody FsContentTagDTO fsUserTagDTO) {
+        contentTagService.bindTag(fsUserTagDTO);
+        return AjaxResult.success();
+    }
+
+}

+ 93 - 0
fs-admin/src/main/java/com/fs/app/controller/AppWelcomeController.java

@@ -0,0 +1,93 @@
+package com.fs.app.controller;
+
+import com.fs.app.welcome.dto.AppWelcomeDTO;
+import com.fs.app.welcome.service.IAppWelcomeService;
+import com.fs.app.welcome.vo.AppWelcomeVO;
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * app-欢迎语维护Controller
+ *
+ * @author fs
+ * @date 2026-02-27
+ */
+@RestController
+@RequestMapping("/app/welcome")
+public class AppWelcomeController extends BaseController {
+
+    @Autowired
+    private IAppWelcomeService appWelcomeService;
+
+    /**
+     * 查询app-欢迎语维护列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AppWelcomeDTO appWelcome) {
+        startPage();
+        List<AppWelcomeVO> list = appWelcomeService.selectAppWelcomeList(appWelcome);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出app-欢迎语维护列表
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:export')")
+    @Log(title = "app欢迎语", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppWelcomeDTO appWelcome) {
+        List<AppWelcomeVO> list = appWelcomeService.selectAppWelcomeList(appWelcome);
+        ExcelUtil<AppWelcomeVO> util = new ExcelUtil<>(AppWelcomeVO.class);
+        return util.exportExcel(list, "app-欢迎语维护数据");
+    }
+
+    /**
+     * 获取app-欢迎语维护详细信息
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:list')")
+    @GetMapping(value = "/getById/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(appWelcomeService.selectAppWelcomeById(id));
+    }
+
+    /**
+     * 保存 新增/编辑 欢迎语
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:add,app:welcome:edit')")
+    @Log(title = "app欢迎语", businessType = BusinessType.UPDATE)
+    @PostMapping(value = "/save")
+    public AjaxResult save(@RequestBody AppWelcomeDTO appWelcome) {
+        return AjaxResult.success(appWelcomeService.saveWelcome(appWelcome));
+    }
+
+
+    /**
+     * 删除app-欢迎语维护
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:delete')")
+    @Log(title = "app欢迎语", businessType = BusinessType.DELETE)
+    @DeleteMapping("/deleteByIds/{ids}")
+    public AjaxResult remove(@PathVariable List<Long> ids) {
+        return toAjax(appWelcomeService.deleteAppWelcomeByIds(ids));
+    }
+
+    /**
+     * 删除app-设置欢迎语状态
+     */
+    @PreAuthorize("@ss.hasAnyPermi('app:welcome:edit')")
+    @Log(title = "app欢迎语", businessType = BusinessType.UPDATE)
+    @PostMapping("/setSendStatus")
+    public AjaxResult setSendStatus(@RequestBody AppWelcomeDTO appWelcomeDTO) {
+        return toAjax(appWelcomeService.setSendStatus(appWelcomeDTO));
+    }
+}

+ 186 - 0
fs-admin/src/main/java/com/fs/app/controller/CommonV2Controller.java

@@ -0,0 +1,186 @@
+package com.fs.app.controller;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.file.OssException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.file.FileUploadUtils;
+import com.fs.common.utils.file.FileUtils;
+import com.fs.framework.config.ServerConfig;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import com.fs.utils.AudioUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+/**
+ * 通用请求处理
+ *
+ */
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/app/common")
+public class CommonV2Controller {
+
+    private final ServerConfig serverConfig;
+
+
+    /**
+     * 通用下载请求
+     *
+     * @param fileName 文件名称
+     * @param delete   是否删除
+     */
+    @GetMapping("/download")
+    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) {
+        try {
+            if (!FileUtils.checkAllowDownload(fileName)) {
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
+            }
+            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
+            String filePath = FSConfig.getDownloadPath() + fileName;
+
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
+            FileUtils.writeBytes(filePath, response.getOutputStream());
+            if (delete) {
+                FileUtils.deleteFile(filePath);
+            }
+        } catch (Exception e) {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    /**
+     * 通用上传请求
+     */
+    @PostMapping("/upload")
+    public AjaxResult uploadFile(MultipartFile file) throws Exception {
+        try {
+            // 上传文件路径
+            String filePath = FSConfig.getUploadPath();
+            // 上传并返回新文件名称
+            String fileName = FileUploadUtils.upload(filePath, file);
+            String url = serverConfig.getUrl() + fileName;
+            AjaxResult ajax = AjaxResult.success();
+            ajax.put("fileName", fileName);
+            ajax.put("url", url);
+            return ajax;
+        } catch (Exception e) {
+            return AjaxResult.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 本地资源通用下载
+     */
+    @GetMapping("/download/resource")
+    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
+            throws Exception {
+        try {
+            if (!FileUtils.checkAllowDownload(resource)) {
+                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
+            }
+            // 本地资源路径
+            String localPath = FSConfig.getProfile();
+            // 数据库资源地址
+            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
+            // 下载名称
+            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            FileUtils.setAttachmentResponseHeader(response, downloadName);
+            FileUtils.writeBytes(downloadPath, response.getOutputStream());
+        } catch (Exception e) {
+            log.error("下载文件失败", e);
+        }
+    }
+
+    @PostMapping("/uploadOSS")
+    public R uploadOSS(@RequestParam("file") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+        // 上传文件
+        String fileName = file.getOriginalFilename();
+        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(file.getBytes(), suffix);
+        return R.ok().put("url", url);
+    }
+
+    @PostMapping("/uploadOSS2")
+    public R uploadOSS2(@RequestParam("file") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+        // 上传文件
+        String fileName = file.getOriginalFilename();
+        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        String prefix = fileName.substring(0, fileName.lastIndexOf("."));
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.upload(file.getBytes(), prefix + System.currentTimeMillis() + suffix);
+        return R.ok().put("url", url);
+    }
+
+    @PostMapping("/uploadOSSByHOOKImage")
+    public R uploadOSSByHOOK(@RequestParam("file") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+        InputStream inputStream = file.getInputStream();
+        BufferedImage bufferedImage = ImageIO.read(inputStream);
+        if (bufferedImage == null) {
+            throw new OssException("无法解析图片,请检查文件内容是否正确");
+        }
+
+        // 将 BufferedImage 转换为 PNG 格式字节数组
+        ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
+        ImageIO.write(bufferedImage, "png", pngOutputStream);
+
+        // 获取 PNG 格式的字节数组
+        byte[] pngBytes = pngOutputStream.toByteArray();
+
+        // 上传文件
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(pngBytes, ".png");
+
+        return R.ok().put("url", url);
+    }
+
+
+    //MP3上传且转化为Sile,并且返回MP3的地址和sile的地址
+    @PostMapping("/uploadOSSByHOOKVoice")
+    public R uploadOSSByQw(@RequestParam("file") MultipartFile file) throws Exception {
+        if (file.isEmpty()) {
+            throw new OssException("上传文件不能为空");
+        }
+
+        // 上传文件
+        String fileName = file.getOriginalFilename();
+        String suffix = fileName.substring(fileName.lastIndexOf("."));
+        CloudStorageService storage = OSSFactory.build();
+        String mp3Url = storage.uploadSuffix(file.getBytes(), suffix);
+        String silkUrl = "";
+        try {
+            silkUrl = AudioUtils.transferAudioSilkFromUrl(mp3Url, true);
+        } catch (Exception e) {
+            throw new Exception("音频转换失败:" + e);
+        }
+
+        return R.ok().put("mp3Url", mp3Url).put("silkUrl", silkUrl);
+    }
+
+}

+ 36 - 0
fs-admin/src/main/java/com/fs/app/controller/FsUserInfoController.java

@@ -0,0 +1,36 @@
+package com.fs.app.controller;
+
+import com.fs.app.user.dto.FsUserInfoDTO;
+import com.fs.app.user.service.IFsUserInfoService;
+import com.fs.common.core.domain.AjaxResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/app/fsUserInfo")
+public class FsUserInfoController {
+
+    @Autowired
+    private IFsUserInfoService fsUserInfoService;
+
+    @GetMapping("/getByUserId/{userId}")
+    public AjaxResult getByUserId(@PathVariable Long userId) {
+        return AjaxResult.success(fsUserInfoService.selectFsUserInfoByUserId(userId));
+    }
+
+    @PostMapping("/saveUserInfo")
+    public AjaxResult saveUserInfo(@RequestBody FsUserInfoDTO reqParam) {
+        return AjaxResult.success(fsUserInfoService.saveUserInfo(reqParam));
+    }
+
+    /**
+     * 获取用户兴趣标签
+     * @param userId
+     * @return
+     */
+    @GetMapping("/getUserTags/{userId}")
+    public AjaxResult getUserTags(@PathVariable Long userId) {
+        return AjaxResult.success(fsUserInfoService.getUserTags(userId));
+    }
+
+}

+ 34 - 0
fs-admin/src/main/java/com/fs/app/controller/VipTagController.java

@@ -0,0 +1,34 @@
+package com.fs.app.controller;
+
+import com.fs.app.tag.param.FsTagParam;
+import com.fs.app.tag.service.IFsTagService;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 会员标签组
+ */
+@RestController
+@RequestMapping("/vip/tag")
+@RequiredArgsConstructor
+public class VipTagController extends BaseController {
+
+    private final IFsTagService fsTagService;
+
+    /**
+     * 列表
+     * @param param
+     * @return
+     */
+    @GetMapping("/list")
+//    @PreAuthorize("@ss.hasPermi('vip:tagGroup:list')")
+    public TableDataInfo list(FsTagParam param) {
+        startPage();
+        return getDataTable(fsTagService.findList(param));
+    }
+
+}

+ 98 - 0
fs-admin/src/main/java/com/fs/app/controller/VipTagGroupController.java

@@ -0,0 +1,98 @@
+package com.fs.app.controller;
+
+import com.fs.app.tag.dto.FsTagGroupDTO;
+import com.fs.app.tag.param.FsTagGroupParam;
+import com.fs.app.tag.service.IFsTagGroupService;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 会员标签组
+ */
+@RestController
+@RequestMapping("/vip/tagGroup")
+@RequiredArgsConstructor
+public class VipTagGroupController extends BaseController {
+
+    private final IFsTagGroupService fsTagGroupService;
+
+    /**
+     * 列表
+     *
+     * @param param
+     * @return
+     */
+    @GetMapping("/list")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:list')")
+    public TableDataInfo list(FsTagGroupParam param) {
+        startPage();
+        return getDataTable(fsTagGroupService.findList(param));
+    }
+
+    /**
+     * 用户绑定标签,标签(组)列表
+     *
+     * @param param
+     * @return
+     */
+    @GetMapping("/listForUserBindTag")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:list')")
+    public TableDataInfo listForUserBindTag(FsTagGroupParam param) {
+        return fsTagGroupService.listForUserBindTag(param);
+    }
+
+
+    /**
+     * 新增
+     *
+     * @param dto
+     * @return
+     */
+    @PostMapping("/add")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:add')")
+    public AjaxResult add(@RequestBody FsTagGroupDTO dto) {
+        return toAjax(fsTagGroupService.addGroup(dto));
+    }
+
+    /**
+     * 编辑回显
+     *
+     * @param id
+     * @return
+     */
+    @GetMapping("/getById/{id}")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:edit')")
+    public AjaxResult getById(@PathVariable Long id) {
+        return AjaxResult.success(fsTagGroupService.getById(id));
+    }
+
+    /**
+     * 根据 id 列表获取所有信息
+     *
+     * @param ids
+     * @return
+     */
+    @GetMapping("/getTagByIds/{ids}")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:edit')")
+    public AjaxResult getTagByIds(@PathVariable List<Long> ids) {
+        return AjaxResult.success(fsTagGroupService.getTagByIds(ids));
+    }
+
+    /**
+     * 编辑保存
+     *
+     * @param dto
+     * @return
+     */
+    @PostMapping("/edit")
+    @PreAuthorize("@ss.hasPermi('vip:tagGroup:edit')")
+    public AjaxResult edit(@RequestBody FsTagGroupDTO dto) {
+        return toAjax(fsTagGroupService.editGroup(dto));
+    }
+}

+ 79 - 0
fs-admin/src/main/java/com/fs/app/controller/VipUserCustomerController.java

@@ -0,0 +1,79 @@
+package com.fs.app.controller;
+
+import com.fs.app.tag.dto.FsUserCustomerDTO;
+import com.fs.app.tag.service.IFsUserCustomerService;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+@RestController
+@RequestMapping("/vip/userCusRole")
+@RequiredArgsConstructor
+public class VipUserCustomerController {
+
+    private final IFsUserCustomerService fsUserCustomerService;
+
+
+    /**
+     * 用户绑定客服-老版本,后续会弃用
+     *
+     * @param fsUserCustomerDTO
+     * @return
+     */
+    @PostMapping("/bind")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:bindCustomer,app:user:batchBindCustomer')")
+    @Log(title = "APP会员绑定客服", businessType = BusinessType.OTHER)
+    public AjaxResult bind(@RequestBody FsUserCustomerDTO fsUserCustomerDTO) {
+        fsUserCustomerService.bind(fsUserCustomerDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 用户解绑客服-老版本,后续会弃用
+     *
+     * @param fsUserCustomerDTO
+     * @return
+     */
+    @PostMapping("/unbind")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:unbindCustomer,app:user:batchUnbindCustomer')")
+    @Log(title = "APP会员解绑客服", businessType = BusinessType.OTHER)
+    public AjaxResult unbind(@RequestBody FsUserCustomerDTO fsUserCustomerDTO) {
+        fsUserCustomerService.unbind(fsUserCustomerDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 用户绑定客服-新版本,后续需调用这个版本
+     *
+     * @param fsUserCustomerDTO
+     * @return
+     */
+    @PostMapping("/bind/v2")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:bindCustomer,app:user:batchBindCustomer')")
+    @Log(title = "APP会员绑定客服", businessType = BusinessType.OTHER)
+    public AjaxResult bindV2(@RequestBody FsUserCustomerDTO fsUserCustomerDTO) {
+        fsUserCustomerService.bindV2(fsUserCustomerDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 用户解绑客服-新版本,后续需调用这个版本
+     *
+     * @param fsUserCustomerDTO
+     * @return
+     */
+    @PostMapping("/unbind/v2")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:unbindCustomer,app:user:batchUnbindCustomer')")
+    @Log(title = "APP会员解绑客服", businessType = BusinessType.OTHER)
+    public AjaxResult unbindV2(@RequestBody FsUserCustomerDTO fsUserCustomerDTO) {
+        fsUserCustomerService.unbindV2(fsUserCustomerDTO);
+        return AjaxResult.success();
+    }
+
+}

+ 78 - 0
fs-admin/src/main/java/com/fs/app/controller/VipUserTagController.java

@@ -0,0 +1,78 @@
+package com.fs.app.controller;
+
+import com.fs.app.tag.dto.FsUserTagDTO;
+import com.fs.app.tag.service.IFsUserTagService;
+import com.fs.app.user.service.IFsUserPortraitTagService;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+@RestController
+@RequestMapping("/vip/userTag")
+@RequiredArgsConstructor
+public class VipUserTagController extends BaseController {
+
+    private final IFsUserTagService fsUserTagService;
+    private final IFsUserPortraitTagService fsUserPortraitTagService;
+    /**
+     * 用户绑定标签
+     *
+     * @param fsUserTagDTO 用户,标签绑定载体
+     */
+    @PostMapping("/bind")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:bindTag,app:user:batchBindTag')")
+    @Log(title = "APP会员绑定标签", businessType = BusinessType.OTHER)
+    public AjaxResult bind(@RequestBody FsUserTagDTO fsUserTagDTO) {
+        fsUserTagService.bindUserTag(fsUserTagDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 解绑用户标签
+     *
+     * @param fsUserTagDTO 用户,标签绑定载体
+     */
+    @PostMapping("/unbind")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:unbindTag,app:user:batchUnbindTag')")
+    @Log(title = "APP会员解绑标签", businessType = BusinessType.OTHER)
+    public AjaxResult unbind(@RequestBody FsUserTagDTO fsUserTagDTO) {
+        fsUserTagService.unbindUserTag(fsUserTagDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 用户绑定标签-新版本,需后续切换到此版本
+     *
+     * @param fsUserTagDTO 用户,标签绑定载体
+     */
+    @PostMapping("/bind/v2")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:bindTag,app:user:batchBindTag')")
+    @Log(title = "APP会员绑定标签", businessType = BusinessType.OTHER)
+    public AjaxResult bindV2(@RequestBody FsUserTagDTO fsUserTagDTO) {
+        fsUserPortraitTagService.bindTag(fsUserTagDTO.getUserId(),fsUserTagDTO.getTagId());
+        fsUserTagService.bindUserTagV2(fsUserTagDTO);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 解绑用户标签-新版本,需后续切换到此版本
+     *
+     * @param fsUserTagDTO 用户,标签绑定载体
+     */
+    @PostMapping("/unbind/v2")
+    @PreAuthorize("@ss.hasAnyPermi('app:user:unbindTag,app:user:batchUnbindTag')")
+    @Log(title = "APP会员解绑标签", businessType = BusinessType.OTHER)
+    public AjaxResult unbindV2(@RequestBody FsUserTagDTO fsUserTagDTO) {
+        fsUserPortraitTagService.unBindTag(fsUserTagDTO.getUserId(),fsUserTagDTO.getTagId());
+        fsUserTagService.unbindUserTagV2(fsUserTagDTO);
+        return AjaxResult.success();
+    }
+
+}

+ 116 - 0
fs-admin/src/main/java/com/fs/app/job/UserActivityTask.java

@@ -0,0 +1,116 @@
+package com.fs.app.job;
+
+import com.fs.app.user.domain.FsUserPortrait;
+import com.fs.app.user.service.IFsUserPortraitService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Component
+@Slf4j
+public class UserActivityTask {
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+
+    @Autowired
+    private IFsUserPortraitService portraitService;
+
+    private static final String LASTS_EEN_KEY = "userPortrait:last_seen";
+    private static final String TOTAL_7D_KEY = "userPortrait:total:7d";
+    /**
+     * 入口 1:每天凌晨 3 点定时执行
+     */
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void cronTask() {
+        // 昨天
+        String d1 = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+        // 8天前
+        String d8 = LocalDate.now().minusDays(8).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+
+        log.info("开始滑动窗口计算: 增加 {}, 减去 {}", d1, d8);
+
+        String dailyD1Key = "userPortrait:daily:duration:" + d1;
+        String dailyD8Key = "userPortrait:daily:duration:" + d8;
+
+
+
+        Cursor<Map.Entry<Object, Object>> cursor = stringRedisTemplate.opsForHash().scan(LASTS_EEN_KEY,
+                ScanOptions.scanOptions().count(1000).build());
+
+        List<FsUserPortrait> batchList = new ArrayList<>();
+
+        while (cursor.hasNext()) {
+            Map.Entry<Object, Object> entry = cursor.next();
+            String userId = entry.getKey().toString();
+            long lastSeenTs = Long.parseLong(entry.getValue().toString());
+
+            // 1. 获取昨天的增量
+            Object d1Incr = stringRedisTemplate.opsForHash().get(dailyD1Key, userId);
+            long plusSeconds = d1Incr != null ? Long.parseLong(d1Incr.toString()) : 0L;
+
+            // 2. 获取8天前的过期量
+            Object d8Decr = stringRedisTemplate.opsForHash().get(dailyD8Key, userId);
+            long minusSeconds = d8Decr != null ? Long.parseLong(d8Decr.toString()) : 0L;
+
+            // 3. 更新 Redis 中的 7 日滑动窗口总值
+            long current7dTotal = stringRedisTemplate.opsForHash().increment(TOTAL_7D_KEY, userId, plusSeconds - minusSeconds);
+
+            // 防止出现负数
+            if (current7dTotal < 0) {
+                stringRedisTemplate.opsForHash().put(TOTAL_7D_KEY, userId, "0");
+                current7dTotal = 0;
+            }
+
+            // 4. 计算活跃度状态
+            FsUserPortrait activity = calculateStatus(Long.valueOf(userId), lastSeenTs, current7dTotal);
+            batchList.add(activity);
+
+            if (batchList.size() >= 500) {
+                portraitService.updateActivityLevelBatch(batchList);
+                batchList.clear();
+            }
+        }
+        if (!batchList.isEmpty()) {
+            portraitService.updateActivityLevelBatch(batchList);
+            batchList.clear();
+        }
+
+        // 5. 清理 8 天前的数据,释放内存
+        stringRedisTemplate.delete(dailyD8Key);
+    }
+
+    private FsUserPortrait calculateStatus(Long userId, long lastSeenTs, long total7dSeconds) {
+        long now = System.currentTimeMillis() / 1000;
+        long daysInactive = (now - lastSeenTs) / (24 * 3600);
+        double hours7d = total7dSeconds / 3600.0;
+
+        String activityLevel;
+        if (daysInactive >= 30) {
+            activityLevel = "流失";
+            // 删除流失用户数据
+            stringRedisTemplate.opsForHash().delete(LASTS_EEN_KEY, userId);
+            stringRedisTemplate.opsForHash().delete(TOTAL_7D_KEY, userId);
+        } else if (daysInactive >= 7) {
+            activityLevel = "沉默";
+        } else {
+            if (hours7d < 3) activityLevel = "低活跃";
+            else if (hours7d < 7) activityLevel = "活跃";
+            else activityLevel = "高活跃";
+        }
+        FsUserPortrait fsUserPortrait = new FsUserPortrait();
+        fsUserPortrait.setUserId(userId);
+        fsUserPortrait.setActivityLevel(activityLevel);
+        return fsUserPortrait;
+    }
+
+}

+ 97 - 0
fs-admin/src/main/java/com/fs/app/job/UserConsumptionAmountTask.java

@@ -0,0 +1,97 @@
+package com.fs.app.job;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.app.user.domain.FsUserPortrait;
+import com.fs.app.user.service.IFsUserPortraitService;
+import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.vo.UserAmountVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class UserConsumptionAmountTask {
+    @Autowired
+    private IFsUserPortraitService portraitService;
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+
+    @Scheduled(cron = "0 0 2 * * ?") // 确保只执行一次
+    public void cronTask() {
+        LocalDateTime start = LocalDateTime.of(LocalDate.now().minusDays(1), LocalTime.MIN);
+        LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.MIN);
+        long startPlay = System.currentTimeMillis();
+        log.error("开始增量统计消费数据: {} - {}", start, end);
+
+        // 1. 订单量大了之后这里要做分页
+        List<UserAmountVO> dailySums = storeOrderService.getSumAmount(start, end);
+        if (CollUtil.isEmpty(dailySums)) {
+            log.info("昨日无新增消费记录");
+            return;
+        }
+
+        // 2. 分批处理(例如每 1000 个用户一批)
+        List<List<UserAmountVO>> batches = CollUtil.split(dailySums, 1000);
+        for (List<UserAmountVO> batch : batches) {
+            processBatch(batch);
+        }
+        long endPlay = System.currentTimeMillis();
+        log.error("增量统计消费数据完成:{}", endPlay - startPlay);
+    }
+
+    public void processBatch(List<UserAmountVO> batch) {
+        List<Long> userIds = batch.stream().map(UserAmountVO::getUserId).collect(Collectors.toList());
+        Map<Long, FsUserPortrait> portraitMap = portraitService.list(
+                new LambdaQueryWrapper<FsUserPortrait>().in(FsUserPortrait::getUserId, userIds)
+        ).stream().collect(Collectors.toMap(FsUserPortrait::getUserId, p -> p));
+
+        List<FsUserPortrait> updateList = new ArrayList<>();
+
+        for (UserAmountVO dailySum : batch) {
+            FsUserPortrait portrait = portraitMap.get(dailySum.getUserId());
+            if (portrait == null) continue;
+            updatePortraitData(portrait, dailySum);
+            updateList.add(portrait);
+        }
+
+        // 3. 批量更新:一次 IO 更新 1000 行
+        portraitService.updateBatchById(updateList);
+    }
+
+    private void updatePortraitData(FsUserPortrait portrait, UserAmountVO dailySum) {
+        // 累加数据
+        portrait.setConsumptionAmount(portrait.getConsumptionAmount().add(dailySum.getDailySum()));
+        portrait.setSmallOrderCount(portrait.getSmallOrderCount() + dailySum.getSmallCount());
+
+        // 重新计算等级
+        portrait.setConsumptionLevel(calculateLevel(portrait.getConsumptionAmount(), portrait.getSmallOrderCount()));
+    }
+
+    private String calculateLevel(BigDecimal amount, int smallCount) {
+        if (amount.compareTo(new BigDecimal("3000")) > 0) return "S+";
+        if (amount.compareTo(new BigDecimal("500")) > 0) return "S";
+        if (amount.compareTo(new BigDecimal("150")) > 0) {
+            return smallCount > 20 ? "E" : "A";
+        }
+        if (smallCount > 20) return "E";
+        if (smallCount > 8) return "D";
+        if (smallCount > 3) return "C";
+        return "B"; // 补全默认等级
+    }
+
+}
+
+
+

+ 24 - 0
fs-admin/src/main/java/com/fs/app/job/UserTagTask.java

@@ -0,0 +1,24 @@
+package com.fs.app.job;
+
+import com.fs.app.user.service.IFsUserPortraitTagService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class UserTagTask {
+
+    @Autowired
+    private IFsUserPortraitTagService portraitTagService;
+    // 每两天5点执行一次
+    @Scheduled(cron = "0 0 5 1/2 * ?")
+    public void syncDataToDb() {
+        // 权重衰减
+        portraitTagService.weightAttenuation();
+    }
+
+
+
+}

+ 201 - 0
fs-admin/src/main/java/com/fs/app/job/VideoTrackTask.java

@@ -0,0 +1,201 @@
+package com.fs.app.job;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.app.user.domain.FsUserPortraitContentTag;
+import com.fs.app.user.domain.FsUserPortraitTag;
+import com.fs.app.user.service.IFsUserPortraitContentTagService;
+import com.fs.app.user.service.IFsUserPortraitTagService;
+import com.fs.common.constant.VideoRedisKeyConst;
+import com.fs.course.dto.VideoUpdateDTO;
+import com.fs.course.service.IFsUserVideoService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Component
+@Slf4j
+public class VideoTrackTask {
+    @Autowired
+    private StringRedisTemplate stringRedisTemplate;
+    @Autowired
+    @Lazy
+    private IFsUserVideoService userVideoService;
+    @Autowired
+    private IFsUserPortraitContentTagService contentTagService;
+    @Autowired
+    private IFsUserPortraitTagService portraitTagService;
+
+
+    @Scheduled(cron = "0 0/2 * * * ?")
+    public void syncDataToDb() {
+        syncVideoMetrics();
+        syncUserTags();
+    }
+
+    private void syncVideoMetrics() {
+        // 每次最多从待处理池中弹出 1000 个视频ID进行同步 (控制单次处理量)
+
+        List<String> pendingVideoIds = stringRedisTemplate.opsForSet().pop(VideoRedisKeyConst.PENDING_SYNC_VIDEOS, 1000);
+
+        if (pendingVideoIds == null || pendingVideoIds.isEmpty()) {
+            return;
+        }
+
+        // 用于收集批量更新参数的 List
+        List<VideoUpdateDTO> updateBatchList = new ArrayList<>();
+
+        for (String
+                videoIdStr : pendingVideoIds) {
+            String key = VideoRedisKeyConst.VIDEO_METRICS_PREFIX + videoIdStr + VideoRedisKeyConst.KEY_END;
+            String syncKey = key + VideoRedisKeyConst.SYNC_SUFFIX;
+
+            try {
+                if (stringRedisTemplate.hasKey(key) && stringRedisTemplate.renameIfAbsent(key, syncKey)) {
+                    Long videoId = Long.parseLong(videoIdStr);
+                    Map<Object, Object> metrics = stringRedisTemplate.opsForHash().entries(syncKey);
+
+                    long play = parseLong(metrics.get("PLAY"));
+                    long play3s = parseLong(metrics.get("PLAY_3S"));
+                    long complete = parseLong(metrics.get("COMPLETE"));
+
+                    updateBatchList.add(new VideoUpdateDTO(videoId, play, play3s, complete));
+                    stringRedisTemplate.delete(syncKey);
+                } else {
+                    // 重命名失败,说明该Key正在被处理,为了防丢,把 videoId 塞回池子里下次处理
+                    stringRedisTemplate.opsForSet().add(VideoRedisKeyConst.PENDING_SYNC_VIDEOS, videoIdStr );
+                }
+            } catch (Exception e) {
+                stringRedisTemplate.opsForSet().add(VideoRedisKeyConst.PENDING_SYNC_VIDEOS, videoIdStr);
+            }
+        }
+
+
+        if (!updateBatchList.isEmpty()) {
+            userVideoService.batchIncrementMetrics(updateBatchList);
+        }
+    }
+
+    private void syncUserTags() {
+        // 1. 从待处理池中一次性弹出最多 1000 个需要更新的用户 ID
+        List<String> pendingUserIds = stringRedisTemplate.opsForSet().pop(VideoRedisKeyConst.PENDING_SYNC_USERS, 1000);
+
+        if (pendingUserIds == null || pendingUserIds.isEmpty()) {
+            return;
+        }
+
+        // userId -> { videoId -> weight }
+        Map<Long, Map<Long, Integer>> pendingSyncData = new HashMap<>();
+        // videoIds
+        Set<Long> allVideoIdsToQuery = new HashSet<>();
+        List<String> syncKeysToDelete = new ArrayList<>();
+
+        // 2. 遍历处理这批 userId
+        for (String userIdStr : pendingUserIds) {
+            String key = VideoRedisKeyConst.USER_VIDEO_WEIGHT_PREFIX + userIdStr + VideoRedisKeyConst.KEY_END;
+            String syncKey = key + VideoRedisKeyConst.SYNC_SUFFIX;
+
+            try {
+                // 原子重命名
+                if (stringRedisTemplate.hasKey(key) && stringRedisTemplate.renameIfAbsent(key, syncKey)) {
+                    Long userId = Long.parseLong(userIdStr);
+                    Map<Object, Object> videoWeights = stringRedisTemplate.opsForHash().entries(syncKey);
+
+                    Map<Long, Integer> userVideoMap = new HashMap<>();
+                    for (Map.Entry<Object, Object> entry : videoWeights.entrySet()) {
+                        Long videoId = Long.parseLong(String.valueOf(entry.getKey()));
+                        Integer weight = Integer.parseInt(String.valueOf(entry.getValue()));
+
+                        userVideoMap.put(videoId, weight);
+                        allVideoIdsToQuery.add(videoId);
+                    }
+
+                    pendingSyncData.put(userId, userVideoMap);
+                    syncKeysToDelete.add(syncKey);
+                } else {
+                    // 重命名失败说明该 key 正处于其它同步流程中,防丢:塞回待处理池下次再试
+                    stringRedisTemplate.opsForSet().add(VideoRedisKeyConst.PENDING_SYNC_USERS, userIdStr);
+                }
+            } catch (Exception e) {
+                log.error("转移用户视频权重 Redis Key 异常, userId: {}", userIdStr, e);
+                // 发生异常防丢:塞回待处理池下次再试
+                stringRedisTemplate.opsForSet().add(VideoRedisKeyConst.PENDING_SYNC_USERS, userIdStr);
+            }
+        }
+
+        // 如果没有任何有效的视频ID需要查询,直接清理 keys 并返回
+        if (allVideoIdsToQuery.isEmpty()) {
+            if (!syncKeysToDelete.isEmpty()) {
+                stringRedisTemplate.delete(syncKeysToDelete);
+            }
+            return;
+        }
+
+        // 3. 批量查询 DB 获取这些视频的标签 (1次网络IO)
+        List<FsUserPortraitContentTag> videoTags = contentTagService.list(new LambdaQueryWrapper<FsUserPortraitContentTag>()
+                .in(FsUserPortraitContentTag::getContentId, allVideoIdsToQuery));
+
+        // 按照 contentId 分组,收集每个 contentId 对应的 tagId 集合
+        Map<Long, List<Long>> videoTagMap = videoTags.stream()
+                .collect(Collectors.groupingBy(
+                        FsUserPortraitContentTag::getContentId,
+                        Collectors.mapping(FsUserPortraitContentTag::getTagId, Collectors.toList())
+                ));
+
+
+        // 用于收集将要批量写入数据库的对象集合
+        List<FsUserPortraitTag> tagBatchList = new ArrayList<>();
+
+        // 4. 在内存中将 videoId 的权重转化为 tag 的权重
+        for (Map.Entry<Long, Map<Long, Integer>> userEntry : pendingSyncData.entrySet()) {
+            Long userId = userEntry.getKey();
+            Map<Long, Integer> videoWeightsMap = userEntry.getValue();
+
+            // 聚合同一个用户在这批数据里的各个标签总权重: tag -> totalWeight
+            Map<Long, Integer> userTagAggregator = new HashMap<>();
+
+            for (Map.Entry<Long, Integer> vwEntry : videoWeightsMap.entrySet()) {
+                Long videoId = vwEntry.getKey();
+                Integer weight = vwEntry.getValue();
+
+                List<Long> tags = videoTagMap.get(videoId);
+                if (tags != null) {
+                    for (Long tag : tags) {
+                        userTagAggregator.put(tag, userTagAggregator.getOrDefault(tag, 0) + weight);
+                    }
+                }
+            }
+
+            // 组装成实体类放入 List
+            for (Map.Entry<Long, Integer> tagEntry : userTagAggregator.entrySet()) {
+                if (tagEntry.getValue() != 0) { // 只处理权重有净增减的
+                    FsUserPortraitTag utw = new FsUserPortraitTag();
+                    utw.setUserId(userId);
+                    utw.setTagId(tagEntry.getKey());
+                    utw.setWeight(tagEntry.getValue());
+                    tagBatchList.add(utw);
+                }
+            }
+        }
+
+        // 5. 排序防死锁并批量落库
+        if (!tagBatchList.isEmpty()) {
+            tagBatchList.sort(Comparator.comparing(FsUserPortraitTag::getUserId).thenComparing(FsUserPortraitTag::getTagId));
+            portraitTagService.batchAddOrUpdateWeight(tagBatchList);
+        }
+
+        // 落库成功,批量清理 Redis 中的临时 syncKeys
+        if (!syncKeysToDelete.isEmpty()) {
+            stringRedisTemplate.delete(syncKeysToDelete);
+        }
+    }
+
+    private long parseLong(Object obj) {
+        return obj == null ? 0L : Long.parseLong(String.valueOf(obj));
+    }
+}

+ 37 - 4
fs-admin/src/main/java/com/fs/company/controller/CompanyConfigController.java

@@ -3,14 +3,17 @@ package com.fs.company.controller;
 
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.CompanyConfig;
 import com.fs.company.service.ICompanyConfigService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * 系统配置
@@ -44,5 +47,35 @@ public class CompanyConfigController extends BaseController
     }
 
 
+    /**
+    * 修改的是 销售端的配置
+    */
+    @PostMapping(value = "/editCompanyConfig")
+    public R edit(@Validated @RequestBody CompanyConfig companyConfig)
+    {
+        companyConfigService.updateCompanyConfig(companyConfig);
+        return R.ok("操作成功");
+    }
+
+
+    /**
+     * 根据参数键名查询参数值
+     */
+    @GetMapping(value = "/getCompanyConfigKey/{configKey}/{companyId}")
+    public R getConfigKey(@PathVariable String configKey,@PathVariable Long companyId)
+    {
+        CompanyConfig config=companyConfigService.selectCompanyConfigByKey(companyId,configKey);
+        if(config==null){
+            config=new CompanyConfig();
+            config.setCompanyId(companyId);
+            config.setConfigKey(configKey);
+            config.setConfigType("N");
+            companyConfigService.insertCompanyConfig(config);
+
+        }
+        return R.ok().put("data",config);
+
+    }
+
 
 }

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

@@ -214,4 +214,10 @@ public class CompanyUserController extends BaseController
         List<Company> list = companyUserService.getCompanyList(corpId);
         return  R.ok().put("data",list);
     }
+
+    @GetMapping("/findOptions")
+    public AjaxResult findOptions(String keyword, Long selectedId, Integer pageNumber,  Integer pageSize) {
+        return AjaxResult.success(companyUserService.findCompanyUserOptions(keyword, selectedId, pageNumber, pageSize));
+    }
+
 }

+ 514 - 0
fs-admin/src/main/java/com/fs/course/controller/FsAppCourseWatchLogController.java

@@ -0,0 +1,514 @@
+package com.fs.course.controller;
+
+import com.fs.app.cusrole.service.IAppCustomerRoleService;
+import com.fs.app.watchlog.dto.AppCourseWatchLogDto;
+import com.fs.app.watchlog.param.AppCourseWatchLogListParam;
+import com.fs.app.watchlog.service.IAppCourseWatchLogService;
+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.domain.model.LoginUser;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.vo.OptionVO;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.his.domain.FsExportTask;
+import com.fs.his.service.IFsExportTaskService;
+import com.fs.qw.param.QwWatchLogStatisticsListParam;
+import com.fs.qw.service.IQwWatchLogService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 短链课程看课记录Controller
+ *
+ * @author fs
+ * @date 2024-10-24
+ */
+@RestController
+@RequestMapping("/course/appCourseWatchLog")
+public class FsAppCourseWatchLogController extends BaseController {
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+
+    @Autowired
+    private IQwWatchLogService qwWatchLogService;
+    @Autowired
+    private IFsExportTaskService exportTaskService;
+    @Autowired
+    private IAppCourseWatchLogService appCourseWatchLogService;
+    @Autowired
+    private IAppCustomerRoleService appCustomerRoleService;
+
+
+    /**
+     * 查询app课程看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(AppCourseWatchLogListParam param) {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<AppCourseWatchLogDto> list = appCourseWatchLogService.selectAppCourseWatchLogListVO(param);
+        return getDataTable(list);
+    }
+
+//    /**
+//     * 查询短链课程 我的部门 看课记录列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:deptList')")
+//    @GetMapping("/deptList")
+//    public TableDataInfo deptList(FsCourseWatchLogListParam param) {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//
+//
+//        List<Long> combinedList = new ArrayList<>();
+//        //本部门
+//        Long deptId = loginUser.getUser().getDeptId();
+//        if (deptId!=null){
+//            combinedList.add(deptId);
+//        }
+//        //本部门的下级部门
+//        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+//        if (!deptList.isEmpty()){
+//            combinedList.addAll(deptList);
+//        }
+//
+//        param.setCuDeptIdList(combinedList);
+//        param.setUserType(loginUser.getUser().getUserType());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        startPage();
+//        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
+//
+//        TableDataInfo rspData = new TableDataInfo();
+//        rspData.setCode(HttpStatus.SUCCESS);
+//        rspData.setMsg("查询成功");
+//        rspData.setRows(list);
+//        rspData.setTotal(fsCourseWatchLogService.selectFsCourseWatchLogListVOCount(param));
+//        return rspData;
+//    }
+//    /**
+//     * 查询短链课程看课记录列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:myList')")
+//    @GetMapping("/myList")
+//    public TableDataInfo myList(FsCourseWatchLogListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyUserId(loginUser.getUser().getUserId());
+//        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
+//
+//        TableDataInfo rspData = new TableDataInfo();
+//        rspData.setCode(HttpStatus.SUCCESS);
+//        rspData.setMsg("查询成功");
+//        rspData.setRows(list);
+//        rspData.setTotal(fsCourseWatchLogService.selectFsCourseWatchLogListVOCount(param));
+//        return rspData;
+//    }
+
+
+    /**
+     * 判断传入的日期是否是7天之前
+     * @param dateStr
+     * @return
+     */
+    public boolean isDateBefore7Days(String dateStr) {
+        // 定义日期格式
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        // 解析输入日期
+        LocalDate inputDate = LocalDate.parse(dateStr, formatter);
+
+        // 获取当前日期(2025-05-23)
+        LocalDate currentDate = LocalDate.now();
+
+        // 计算日期差
+        long daysBetween = ChronoUnit.DAYS.between(inputDate, currentDate);
+
+        // 判断是否超过7天
+        return daysBetween >= 7;
+    }
+
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:statisticsList')")
+//    @GetMapping("/statisticsList")
+//    public TableDataInfo statisticsList(FsCourseWatchLogStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+//    @GetMapping("/qwWatchLogStatisticsList")
+//    public TableDataInfo qwWatchLogStatisticsList(QwWatchLogStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<QwWatchLogStatisticsListVO> list = qwWatchLogService.selectQwWatchLogStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+//    @GetMapping("/exportWatchLogStatistics")
+//    public AjaxResult exportWatchLogStatistics(QwWatchLogStatisticsListParam param) {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setExcelName("进线客户统计记录导出");
+//
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return AjaxResult.error("请筛选时间后导出");
+//        }
+//
+//        boolean res = qwWatchLogService.dateDifferenceMoreThan7Days(param.getSTime(), param.getETime());
+//
+//        if (res) {
+//            return AjaxResult.error("导出数据时间差不能超过七天");
+//        }
+//        AjaxResult returnRes = new AjaxResult();
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        try {
+//            Class<? extends IQwWatchLogService> clazz = qwWatchLogService.getClass();
+//            returnRes = exportCommon(
+//                    param,
+//                    loginUser.getUser().getUserId(),
+//                    "进线客户统计记录",
+//                    10000L,
+//                    "getQwWatchLogStatisticsCount",
+//                    "exportQwWatchLogStatistics",
+//                    clazz);
+//        } catch (Exception ex) {
+//            ex.printStackTrace();
+//            return AjaxResult.error("导出报错了");
+//        }
+//        return returnRes;
+
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setExcelName("进线客户统计记录导出");
+//        return exportQwWatchLogStatistics(param, loginUser.getUser().getUserId());
+//    }
+
+//    @GetMapping("/myQwWatchLogStatisticsList")
+//    public TableDataInfo myQwWatchLogStatisticsList(QwWatchLogStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setCompanyUserId(loginUser.getUser().getUserId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<QwWatchLogStatisticsListVO> list = qwWatchLogService.selectQwWatchLogStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+    public AjaxResult exportQwWatchLogStatistics(QwWatchLogStatisticsListParam param,Long userId){
+
+        if (param.getSTime() == null || param.getETime() == null) {
+            return AjaxResult.error("请筛选时间后导出");
+        }
+
+        boolean res = qwWatchLogService.dateDifferenceMoreThan7Days(param.getSTime(), param.getETime());
+
+        if (res) {
+            return AjaxResult.error("导出数据时间差不能超过七天");
+        }
+
+        Long count = qwWatchLogService.getQwWatchLogStatisticsCount(param);
+        if (count > 10000) {
+            return AjaxResult.error("导出数据不可超过1w条");
+        }
+//        Long userId = loginUser.getUser().getUserId();
+        FsExportTask task = new FsExportTask();
+        task.setTaskType(2);
+        task.setStatus(0);
+        task.setStartTime(new Date());
+        task.setRemark("客户统计记录");
+        task.setSysType(2);
+        task.setCompanyUserId(userId);
+        exportTaskService.insertFsExportTask(task);
+        param.setTaskId(task.getTaskId());
+        qwWatchLogService.exportQwWatchLogStatistics(param, count);
+
+        return new AjaxResult(200, "后台正在导出,请等待...任务ID:" + task.getTaskId(), task.getTaskId());
+    }
+
+//    @GetMapping("/exportMyQwWatchLogStatistics")
+//    public AjaxResult exportMyQwWatchLogStatistics(QwWatchLogStatisticsListParam param) {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setCompanyUserId(loginUser.getUser().getUserId());
+//        param.setExcelName("我的客户统计记录导出");
+//        return exportQwWatchLogStatistics(param, loginUser.getUser().getUserId());
+//    }
+
+
+
+//    @GetMapping("/qwWatchLogAllStatisticsList")
+//    public TableDataInfo qwWatchLogAllStatisticsList(QwWatchLogStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<QwWatchLogAllStatisticsListVO> list = qwWatchLogService.selectQwWatchLogAllStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+//    @GetMapping("/myQwWatchLogAllStatisticsList")
+//    public TableDataInfo myQwWatchLogAllStatisticsList(QwWatchLogStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setCompanyUserId(loginUser.getUser().getUserId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<QwWatchLogAllStatisticsListVO> list = qwWatchLogService.selectQwWatchLogAllStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+
+    public AjaxResult exportQwWatchLogAllStatistics(QwWatchLogStatisticsListParam param, Long userId) {
+
+        if (param.getSTime() == null || param.getETime() == null) {
+            return AjaxResult.error("请筛选时间后导出");
+        }
+
+        boolean res = qwWatchLogService.dateDifferenceMoreThan7Days(param.getSTime(), param.getETime());
+
+        if (res) {
+            return AjaxResult.error("导出数据时间差不能超过七天");
+        }
+
+        Long count = qwWatchLogService.getQwWatchLogStatisticsCount(param);
+        if (count > 10000) {
+            return AjaxResult.error("导出数据不可超过1w条");
+        }
+//        Long userId = loginUser.getUser().getUserId();
+        FsExportTask task = new FsExportTask();
+        task.setTaskType(2);
+        task.setStatus(0);
+        task.setStartTime(new Date());
+        task.setRemark("我的客户统计记录");
+        task.setSysType(2);
+        task.setCompanyUserId(userId);
+        exportTaskService.insertFsExportTask(task);
+        param.setTaskId(task.getTaskId());
+        qwWatchLogService.exportQwWatchLogAllStatistics(param, count);
+
+        return new AjaxResult(200, "后台正在导出,请等待...任务ID:" + task.getTaskId(), task.getTaskId());
+    }
+
+//    @GetMapping("/exportMyQwWatchLogAllStatistics")
+//    public AjaxResult exportMyQwWatchLogAllStatistics(QwWatchLogStatisticsListParam param) {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setCompanyUserId(loginUser.getUser().getUserId());
+//        param.setExcelName("我的数据汇总记录导出");
+//        return exportQwWatchLogAllStatistics(param, loginUser.getUser().getUserId());
+//    }
+
+//    @GetMapping("/exportQwWatchLogStatisticsAll")
+//    public AjaxResult exportQwWatchLogStatisticsAll(QwWatchLogStatisticsListParam param) {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setExcelName("数据汇总记录导出");
+//        return exportQwWatchLogAllStatistics(param, loginUser.getUser().getUserId());
+//
+//    }
+
+//    @GetMapping("/watchLogStatistics")
+//    public TableDataInfo watchLogStatistics(FsCourseOverParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+//
+//    @GetMapping("/watchLogStatisticsExport")
+//    public AjaxResult watchLogStatisticsExport(FsCourseOverParam param) {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return AjaxResult.error("请选择时间");
+//        }
+//        List<FsCourseOverVO> list = fsCourseWatchLogService.selectFsCourseWatchLogOverStatisticsListVO(param);
+//        ExcelUtil<FsCourseOverVO> util = new ExcelUtil<FsCourseOverVO>(FsCourseOverVO.class);
+//        return util.exportExcel(list, "完课数据");
+//    }
+
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:userStatisticsList')")
+//    @GetMapping("/userStatisticsList")
+//    public TableDataInfo userStatisticsList(FsCourseUserStatisticsListParam param) {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        if (param.getSTime() == null || param.getETime() == null) {
+//            return getDataTable(new ArrayList<>());
+//        }
+//        List<FsCourseUserStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseUserStatisticsListVO(param);
+//        return getDataTable(list);
+//    }
+
+    /**
+     * 导出短链课程看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:export')")
+    @Log(title = "短链课程看课记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(AppCourseWatchLogListParam param)
+    {
+        // 导出
+        List<AppCourseWatchLogDto> list = appCourseWatchLogService.selectAppCourseWatchLogListVO(param);
+        ExcelUtil<AppCourseWatchLogDto> util = new ExcelUtil<AppCourseWatchLogDto>(AppCourseWatchLogDto.class);
+        return util.exportExcel(list, "短链课程看课记录数据");
+//        return exportCourseWatchLog(param);// 放导出任务
+    }
+
+    /**
+     * 导出短链课程看客记录
+     *
+     * @param param 参数
+     * @return AjaxResult
+     */
+    private AjaxResult exportCourseWatchLog(AppCourseWatchLogListParam param) {
+        LoginUser loginUser = getLoginUser();
+        logger.info("导出看课记录");
+        if (appCourseWatchLogService.isEntityNull(param)) {
+            return AjaxResult.error("请筛选数据导出");
+        }
+
+        Long userId = loginUser.getUser().getUserId();
+        Integer exportType = exportTaskService.isExportType1(userId);
+        if (exportType > 0) {
+            return AjaxResult.error("你已经有正在导出的任务");
+        }
+
+        if(param.getSTime() == null || param.getETime() == null || (param.getSTime().getTime() > param.getETime().getTime())) {
+            return AjaxResult.error("导出任务的创建时间范围只能是一天!");
+        }
+
+        Long count = appCourseWatchLogService.selectAppCourseWatchLogListVOCount(param);
+        if (count > 10000) {
+            return AjaxResult.error("导出数据不可超过1w条");
+        }
+
+        FsExportTask task = new FsExportTask();
+        task.setTaskType(2);
+        task.setStatus(0);
+        task.setStartTime(new Date());
+        task.setRemark("app短链课程看课记录");
+        task.setSysType(1);
+        task.setCompanyUserId(userId);
+        exportTaskService.insertFsExportTask(task);
+        param.setTaskId(task.getTaskId());
+
+        appCourseWatchLogService.exportData(param, count);
+        return new AjaxResult(200, "后台正在导出,请等待...任务ID:" + task.getTaskId(), task.getTaskId());
+    }
+
+//    private AjaxResult exportCommon(Object param, Long userId, String remark, Long limitCount,  String getCountMethodName, String exportMethodName, Class<?> clazz) {
+//        AjaxResult res = new AjaxResult();
+//        try {
+//            res = fsCourseWatchLogService.exportCommon(param, userId, remark, limitCount, getCountMethodName, exportMethodName, clazz);
+//        } catch (Exception ex) {
+//            ex.printStackTrace();
+//        }
+//        return res;
+//    }
+
+//    /**
+//     * 导出短链课程看课记录列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:myExport')")
+//    @Log(title = "短链课程看课记录", businessType = BusinessType.EXPORT)
+//    @GetMapping("/myExport")
+//    public AjaxResult myExport(FsCourseWatchLogListParam param)
+//    {
+//        // 导出
+//        return exportCourseWatchLog(param);
+//    }
+//    /**
+//     * 获取短链课程看课记录详细信息
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:query')")
+//    @GetMapping(value = "/{logId}")
+//    public AjaxResult getInfo(@PathVariable("logId") Long logId) {
+//        return AjaxResult.success(fsCourseWatchLogService.selectFsCourseWatchLogByLogId(logId));
+//    }
+//
+//    /**
+//     * 新增短链课程看课记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:add')")
+//    @Log(title = "短链课程看课记录", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody FsCourseWatchLog fsCourseWatchLog) {
+//        return toAjax(fsCourseWatchLogService.insertFsCourseWatchLog(fsCourseWatchLog));
+//    }
+//
+//    /**
+//     * 修改短链课程看课记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:edit')")
+//    @Log(title = "短链课程看课记录", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody FsCourseWatchLog fsCourseWatchLog) {
+//        return toAjax(fsCourseWatchLogService.updateFsCourseWatchLog(fsCourseWatchLog));
+//    }
+//
+//    /**
+//     * 删除短链课程看课记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:remove')")
+//    @Log(title = "短链课程看课记录", businessType = BusinessType.DELETE)
+//    @DeleteMapping("/{logIds}")
+//    public AjaxResult remove(@PathVariable Long[] logIds) {
+//        return toAjax(fsCourseWatchLogService.deleteFsCourseWatchLogByLogIds(logIds));
+//    }
+
+    /**
+     * 昵称模糊查询客服分页
+     */
+    @GetMapping("/getCustomerListLikeName")
+    public R getCustomerListLikeName(@RequestParam(required = false) String name,
+                                     @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                     @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        PageHelper.startPage(pageNum,pageSize);
+        List<OptionVO> appCustomerRoles = appCustomerRoleService.getCustomerListLikeName(name);
+        return R.ok().put("data",new PageInfo<>(appCustomerRoles));
+    }
+
+
+}

+ 7 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -211,6 +211,13 @@ public class FsCourseRedPacketLogController extends BaseController
         return R.ok().put("list", optionsVOS);
     }
 
+    @GetMapping(value = "/videoListByWatch/{id}")
+    public R videoListByWatch(@PathVariable("id") Long id)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllListByWatch(id);
+        return R.ok().put("list", optionsVOS);
+    }
+
 
     /**
     * 红包消耗统计

+ 38 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCommentController.java

@@ -270,6 +270,44 @@ public class FsUserCourseCommentController extends BaseController
         }
     }
 
+    /**
+     * 修改用户留言可见范围(0-自己可见,1-全部人可见)
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseComment:edit')")
+    @Log(title = "用户留言可见范围", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateVisibleAll")
+    public AjaxResult updateVisibleAll(@RequestBody FsUserCourseComment param) {
+        if (param.getCommentId() == null || param.getVisibleAll() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        if (param.getVisibleAll() != 0 && param.getVisibleAll() != 1) {
+            return AjaxResult.error("可见范围参数无效");
+        }
+        FsUserCourseComment update = new FsUserCourseComment();
+        update.setCommentId(param.getCommentId());
+        update.setVisibleAll(param.getVisibleAll());
+        return toAjax(fsUserCourseCommentService.updateFsUserCourseComment(update));
+    }
+
+    /**
+     * 查询指定课程小节下的用户留言(排除精选留言 type=3,支持分页)
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseComment:list')")
+    @GetMapping("/sectionUserComments")
+    public TableDataInfo sectionUserComments(
+            @RequestParam Long courseId,
+            @RequestParam Long videoId,
+            @RequestParam(value = "nickName", required = false) String nickName) {
+        startPage();
+        FsUserCourseCommentParam param = new FsUserCourseCommentParam();
+        param.setCourseId(courseId);
+        param.setVideoId(videoId);
+        param.setNickName(nickName);
+        param.setExcludeFeatured(true);
+        List<FsUserCourseCommentListVO> list = fsUserCourseCommentService.selectFsUserCourseCommentListVO(param);
+        return getDataTable(list);
+    }
+
     /**
      * 查询精选留言历史列表(type=3)
      */

+ 5 - 0
fs-admin/src/main/java/com/fs/his/controller/FsCourseCouponController.java

@@ -108,4 +108,9 @@ public class FsCourseCouponController extends BaseController
     {
         return toAjax(fsCourseCouponService.deleteFsCourseCouponByIds(ids));
     }
+
+    @GetMapping("/options")
+    public AjaxResult selectFsCourseCouponOptions(){
+        return AjaxResult.success(fsCourseCouponService.selectFsCourseCouponOptions());
+    }
 }

+ 24 - 0
fs-admin/src/main/java/com/fs/his/controller/FsDoctorController.java

@@ -63,6 +63,30 @@ public class FsDoctorController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 处方图片列表审核
+     * @param param
+     * @return
+     */
+    @PostMapping("/prescriptionList")
+    public TableDataInfo prescriptionList(@RequestBody DoctorPrescriptionImgLogParam param)
+    {
+        startPage();
+        List<DoctorPrescriptionImgLogParam> doctorList = fsDoctorService.selectPrescriptionLogList(param);
+        return getDataTable(doctorList);
+    }
+
+    /**
+     * 处方图片审核
+     * @param param
+     * @return
+     */
+    @PostMapping("/auditPrescription")
+    public AjaxResult auditPrescription(@RequestBody DoctorPrescriptionParam param)
+    {
+        return toAjax(fsDoctorService.updateFsDoctorPrescription(param));
+    }
+
     /**
      * 导出医生管理列表
      */

+ 7 - 0
fs-admin/src/main/java/com/fs/his/controller/FsIntegralGoodsController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.his.domain.FsIntegralGoods;
+import com.fs.his.param.FsIntegralGoodsParam;
 import com.fs.his.service.IFsIntegralGoodsService;
 import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.FsIntegralGoodsChooseVO;
@@ -148,4 +149,10 @@ public class FsIntegralGoodsController extends BaseController
         List<FsIntegralGoodsChooseVO> list = fsIntegralGoodsService.getChooseIntegralGoodsListByMap(params);
         return R.ok().put("data", new PageInfo<>(list));
     }
+
+    @PostMapping("/batchUpdateIntegralGoods")
+    public R batchUpdateIntegralGoods(@RequestBody FsIntegralGoodsParam integralGoodsParam) {
+        fsIntegralGoodsService.batchUpdateIntegralGoods(integralGoodsParam);
+        return R.ok();
+    }
 }

+ 5 - 0
fs-admin/src/main/java/com/fs/his/controller/FsPackageController.java

@@ -207,4 +207,9 @@ public class FsPackageController extends BaseController
         return R.ok().put("data", new PageInfo<>(list));
     }
 
+    @GetMapping("/getOptions")
+    public AjaxResult getOptions(String keyword, Long metaId, Long limit) {
+        return AjaxResult.success(fsPackageService.getOptions(keyword, metaId, limit));
+    }
+
 }

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

@@ -53,6 +53,7 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.FsSubOrderResultVO;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStorePaymentScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStorePaymentScrmService;
@@ -239,6 +240,8 @@ public class Task {
 
     @Autowired
     private FsUserCompanyUserMapper fsUserCompanyUserMapper;
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
 
     /**
      * 定时任务,处理ai禁止回复之后的消息
@@ -924,7 +927,12 @@ public class Task {
 
     public void CreateWeizouErpPush() {
         List<Long> omsList = fsStoreOrderMapper.selectFsStoreOrderNoCreateOms();
+        List<Long> integrals = fsIntegralOrderMapper.selectFsStoreOrderNoCreateOms();
+        List<Long> scrms = fsStoreOrderScrmMapper.selectFsStoreOrderNoCreateOms();
         logger.info("推送订单id====>{}", omsList);
+        logger.info("推送积分订单id====>{}", integrals);
+        logger.info("推送SCRM订单id====>{}", scrms);
+
         for (Long l : omsList) {
             try {
                 fsStoreOrderService.weizouPush(l);
@@ -932,6 +940,20 @@ public class Task {
                 logger.error("推送订单异常:", e);
             }
         }
+        for (Long l : integrals) {
+            try {
+                fsStoreOrderService.weizouPushIntergral(l);
+            } catch (Exception e) {
+                logger.error("推送积分订单异常:", e);
+            }
+        }
+        for (Long l : scrms) {
+            try {
+                fsStoreOrderService.weizouPushScrm(l);
+            } catch (Exception e) {
+                logger.error("推送SCRM订单异常:", e);
+            }
+        }
     }
     public void CreateOmsAndHis() {
         List<Long> omsList = fsStoreOrderMapper.selectFsStoreOrderNoCreateOms();

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

@@ -22,7 +22,7 @@ public class userIntegralTask {
     @Autowired
     private IFsIntegralCountService integralCountService;
 
-    public void UserIntegralCount(){
+    public void userIntegralCount(){
         log.info("=====用户积分每日消耗统计开始=====");
         try {
             List<FsIntegralCount> list = integralLogsService.selectYesterdayIntegralGroupByLogType();

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

@@ -99,7 +99,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         if(!StringUtils.isEmpty(param.getDeliverySendTimeRange())){
             param.setDeliverySendTimeList(param.getDeliverySendTimeRange().split("--"));
         }
-        param.setIsHealth("1");
+//        param.setIsHealth("1");
         List<FsStoreOrderVO> list = fsStoreOrderService.selectFsStoreOrderListVO(param);
         //金牛需求 区别其他项目 status = 6 (金牛代服管家) ,其他项目请避免使用订单状态status = 6
         TableDataInfo dataTable = getDataTable(list);

+ 17 - 5
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -681,14 +681,19 @@ public class FsStoreOrderScrmController extends BaseController {
             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.getNickName());
-            order.setCompanyName(company.getCompanyName());
+            if (companyUser != null) {
+                Company company = companyService.selectCompanyById(companyUser.getCompanyId());
+                order.setCompanyUserName(companyUser.getNickName());
+                order.setCompanyName(company.getCompanyName());
+            }
         } else if (order.getCompanyId() != null) {
             Company company = companyService.selectCompanyById(order.getCompanyId());
-            order.setCompanyName(company.getCompanyName());
+            if (company != null) {
+                order.setCompanyName(company.getCompanyName());
+            }
         }
 
         if (order.getOrderType() != null && order.getOrderType() == 3) {
@@ -762,10 +767,17 @@ public class FsStoreOrderScrmController extends BaseController {
                 if("恒春来".equals(cloudHostProper.getCompanyName())
                         && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                     if (lastFourNumber.contains("-")) {
-                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                        String beforeDash = lastFourNumber.split("-")[0];
+                        lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
                     }else{
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
+                    expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    if(expressInfoDTO!=null && !expressInfoDTO.isSuccess()){
+                        lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                        expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    }
+                    return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑
                 else if ((lastFourNumber = order.getUserPhone()).length() == 11) {

+ 56 - 0
fs-admin/src/main/java/com/fs/qw/controller/CorporateWeChatSpaceController.java

@@ -0,0 +1,56 @@
+package com.fs.qw.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.CustomException;
+import com.fs.qw.service.ICorporateWeChatSpaceService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 企业微信专区-统一前端 API 接口
+ * */
+@RestController
+@RequestMapping("/weChatSpace")
+@RequiredArgsConstructor
+public class CorporateWeChatSpaceController extends BaseController {
+
+    private final ICorporateWeChatSpaceService weChatSpaceService;
+
+    // 企业微信会话专区中转接口
+    @GetMapping("/conversations")
+    public JSONObject getConversations(
+            @RequestParam(defaultValue = "0") long seq,
+            @RequestParam(defaultValue = "100") long limit,
+            @RequestParam(defaultValue = "0") long proxy,
+            @RequestParam(defaultValue = "30") long timeout,
+            @RequestParam(required = false) String customerId,
+            @RequestParam(required = false) String staffUserId) throws Exception {
+        if (customerId == null|| customerId.isEmpty()) {
+            throw new CustomException("客户id不能为空");
+        }else if (staffUserId == null|| staffUserId.isEmpty()) {
+            throw new CustomException("员工id不能为空");
+        }
+        return weChatSpaceService.fetchConversations(seq, limit, proxy, timeout, customerId,staffUserId);
+    }
+
+
+    // agentConfig 签名
+    @GetMapping("/getAgentConfigSignature")
+    public JSONObject getAgentConfigSignature(@RequestParam("url") String url) {
+        return weChatSpaceService.getAgentConfigSignature(url);
+    }
+
+    // Web 登录
+    @PostMapping("/login")
+    public JSONObject login(@RequestBody JSONObject param) {
+        return weChatSpaceService.login(param.getString("code"));
+    }
+
+    //获取企业微信专区会话配置
+    @GetMapping("/getQwSessionConfig")
+    public AjaxResult getQwSessionConfig() {
+        return AjaxResult.success(weChatSpaceService.getQwSessionConfig());
+    }
+}

+ 9 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -157,6 +157,15 @@ public class QwUserController extends BaseController {
         return getDataTable(list);
     }
 
+    /**
+     * 查询所有企微员工列表
+     * */
+    @PreAuthorize("@ss.hasPermi('qw:user:list')")
+    @GetMapping("/listAllQwUserList")
+    public AjaxResult listAllQwUserList(QwUser qwUser) {
+        List<QwUser> list = qwUserService.selectNotDelQwUserList(qwUser);
+        return AjaxResult.success(list);
+    }
 
     /**
      * 导出企微员工列表

+ 1 - 1
fs-admin/src/main/java/com/fs/third/controller/WeizouController.java

@@ -33,7 +33,7 @@ public class WeizouController extends BaseController {
         if (map.get("orderCode") == null) {
             return error("参数异常请核验参数");
         }
-        fsStoreOrder.setOrderId(Long.valueOf((String) map.get("orderCode")));
+        fsStoreOrder.setOrderCode((String) map.get("orderCode"));
         fsStoreOrder.setOperator("-1");
 // 校验物流单号
         if (map.get("deliverySn") == null) {

+ 308 - 0
fs-admin/src/main/java/com/fs/utils/AudioUtils.java

@@ -0,0 +1,308 @@
+package com.fs.utils;
+
+import com.fs.common.exception.ServiceException;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.file.Files;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class AudioUtils {
+
+    /**
+     * 工具地址
+     **/
+    static String path = "c:\\";
+    static String destinationDir = "c:\\hook\\";
+
+    /**
+     * 从网络 URL 转换 MP3/WAV到SILk格式
+     *
+     * @param audioUrl  音频文件的 URL
+     * @param isSource  是否清空原文件
+     * @return SILK 文件路径
+     */
+    public static String transferAudioSilkFromUrl(String audioUrl, boolean isSource) {
+        try {
+            // 下载文件到本地临时路径
+            File tempFile = downloadFileFromUrl(audioUrl);
+            if (tempFile == null) {
+                throw new ServiceException("下载文件失败");
+            }
+
+            String silkUrl = transferAudioSilk(tempFile.getParent()+"\\", tempFile.getName(), isSource);
+            // 删除临时文件
+            tempFile.delete();
+
+            // 调用原来的转换方法
+            return silkUrl;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+    /**
+     * MP3/WAV转SILk格式
+     *
+     * @param path 文件路径 例:D:\\file\\
+     * @param name 文件名称 例:audio.mp3/audio.wav
+     * @param isSource 是否清空原文件
+     * @return silk文件路径
+     * @throws Exception
+     */
+    public static String transferAudioSilk(String path, String name, boolean isSource) {
+        try {
+            // 判断后缀格式
+            String suffix = name.split("\\.")[1];
+            if (!suffix.toLowerCase().equals("mp3") && !suffix.toLowerCase().equals("wav")) {
+                throw new ServiceException("文件格式必须是mp3/wav");
+            }
+            String filePath = path + name;
+            File file = new File(filePath);
+            if (!file.exists()) {
+                throw new Exception("文件不存在!");
+            }
+            // 文件名时拼接
+            SimpleDateFormat ttime = new SimpleDateFormat("yyyyMMddhhMMSS");
+            String time = ttime.format(new Date());
+            // 导出的pcm格式路径
+            String pcmPath = path + "PCM_" + time + ".pcm";
+            // 先将mp3/wav转换成pcm格式
+            transferAudioPcm(filePath, pcmPath);
+            // 导出的silk格式路径
+            String silkPath = path + "SILK_" + time + ".silk";
+            // 转换成silk格式
+            transferPcmSilk(pcmPath, silkPath);
+            // 删除pcm文件
+            File pcmFile = new File(pcmPath);
+            if (pcmFile.exists()) {
+                pcmFile.delete();
+            }
+            if (isSource) {
+                File audioFile = new File(filePath);
+
+                if (audioFile.exists()) {
+                    audioFile.delete();
+                }
+            }
+
+            File silkFile = new File(silkPath);
+            //上传silk文件
+            String silkPathSuffix = silkPath.split("\\.")[1];
+            CloudStorageService storage = OSSFactory.build();
+            byte[] fileBytes = Files.readAllBytes(silkFile.toPath());
+            String silkUrl = storage.uploadSuffix(fileBytes, "."+silkPathSuffix);
+
+            if (silkFile.exists()) {
+                silkFile.delete();
+            }
+            return silkUrl;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * mp3/wav 通用
+     * @param fpath
+     * @param target
+     */
+    private static void transferAudioPcm(String fpath, String target) {
+        List<String> commend = new ArrayList<String>();
+        commend.add(path + "ffmpeg.exe");
+        commend.add("-y");
+        commend.add("-i");
+        commend.add(fpath);
+        commend.add("-f");
+        commend.add("s16le");
+        commend.add("-ar");
+        commend.add("24000");
+        commend.add("-ac");
+        commend.add("1");
+        commend.add(target);
+        try {
+            ProcessBuilder builder = new ProcessBuilder();
+            builder.command(commend);
+            Process p = builder.start();
+            p.waitFor();
+            p.destroy();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * silk_v3_encoder.exe,转成Silk格式
+     * @param pcmPath pcm 文件地址
+     * @param target  转换后的silk地址
+     * silk_v3_encoder.exe 路径
+     * pcm文件地址
+     * silk输出地址
+     * -Fs_API <Hz>            : API sampling rate in Hz, default: 24000
+     * -Fs_maxInternal <Hz>    : Maximum internal sampling rate in Hz, default: 24000
+     * -packetlength <ms>      : Packet interval in ms, default: 20
+     * -rate <bps>            : Target bitrate;   default: 25000
+     * -loss <perc>          : Uplink loss estimate, in percent (0-100);  default: 0
+     * -complexity <comp>   : Set complexity, 0: low, 1: medium, 2: high; default: 2
+     * -DTX <flag>          : Enable DTX (0/1); default: 0
+     * -quiet               : Print only some basic values
+     * -tencent             : Compatible with QQ/Wechat
+     */
+    public static void transferPcmSilk(String pcmPath, String target) {
+        Process process = null;
+        try {
+            /**
+             // 1、这一节的,语音长度太长会使音频长度丢失
+             List<String> commend = new ArrayList<>();
+             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
+             commend.add(path + "silk_v3_encoder.exe");
+             commend.add(pcmPath);
+             commend.add(target);
+             commend.add("-tencent");
+             ProcessBuilder builder = new ProcessBuilder();
+             builder.command(commend);
+             process = builder.start();
+             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
+             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
+             */
+            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
+            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
+            process .waitFor();
+            Thread.sleep(1000);
+            // 有更好的方法会后续慢慢更新..
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (process != null) {
+                process.destroy();
+            }
+        }
+    }
+    /**
+     * 下载文件从网络 URL 到本地临时文件夹
+     *
+     * @param fileUrl 文件的网络 URL
+     * @return 下载到本地的临时文件
+     */
+    private static File downloadFileFromUrl(String fileUrl) {
+        InputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        try {
+            // 创建 HTTP 连接
+            URL url = new URL(fileUrl);
+            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+            // 设置Referer请求头
+            connection.setRequestProperty("Referer", "cos.his.cdwjyyh.com");
+            connection.setRequestMethod("GET");
+            connection.connect();
+
+            // 检查是否成功连接
+            if (connection.getResponseCode() != 200) {
+                throw new ServiceException("无法下载音频文件,HTTP 响应码:" + connection.getResponseCode());
+            }
+
+            // 获取输入流
+            inputStream = connection.getInputStream();
+
+            // 创建临时文件,并指定存放地址
+            String tempFileName = "temp_" + System.currentTimeMillis() + "_" + getFileExtension(fileUrl);
+            File destinationDirectory = new File(destinationDir);
+
+            // 确保目标目录存在
+            if (!destinationDirectory.exists()) {
+                destinationDirectory.mkdirs();
+            }
+
+            // 将文件保存到指定路径
+            File tempFile = new File(destinationDirectory, tempFileName);
+
+            // 写入文件
+            outputStream = new FileOutputStream(tempFile);
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+
+            return tempFile;
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (inputStream != null) inputStream.close();
+                if (outputStream != null) outputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+//    private static File downloadFileFromUrl(String fileUrl)
+//    {
+//        InputStream inputStream = null;
+//        FileOutputStream outputStream = null;
+//        try {
+//            // 创建 HTTP 连接
+//            URL url = new URL(fileUrl);
+//            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+//            connection.setRequestMethod("GET");
+//            connection.connect();
+//
+//            // 检查是否成功连接
+//            if (connection.getResponseCode() != 200) {
+//                throw new ServiceException("无法下载音频文件,HTTP 响应码:" + connection.getResponseCode());
+//            }
+//
+//            // 获取输入流
+//            inputStream = connection.getInputStream();
+//
+//            // 创建临时文件
+//            String tempFileName = "temp_" + System.currentTimeMillis() + "_" + getFileExtension(fileUrl);
+//            File tempFile = new File(tempFileName);
+//
+//            // 写入文件
+//            outputStream = new FileOutputStream(tempFile);
+//            byte[] buffer = new byte[8192];
+//            int bytesRead;
+//            while ((bytesRead = inputStream.read(buffer)) != -1) {
+//                outputStream.write(buffer, 0, bytesRead);
+//            }
+//
+//            return tempFile;
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//        } finally {
+//            try {
+//                if (inputStream != null) inputStream.close();
+//                if (outputStream != null) outputStream.close();
+//            } catch (IOException e) {
+//                e.printStackTrace();
+//            }
+//        }
+//        return null;
+//    }
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileUrl 文件路径
+     * @return 文件扩展名
+     */
+    private static String getFileExtension(String fileUrl) {
+        return fileUrl.substring(fileUrl.lastIndexOf("."));
+    }
+
+    // 省略其他方法的实现
+
+}

+ 136 - 0
fs-admin/src/main/java/com/fs/web/controller/system/AppDeptController.java

@@ -0,0 +1,136 @@
+package com.fs.web.controller.system;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.constant.UserConstants;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.entity.AppDept;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.system.service.IAppDeptService;
+import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * 部门信息
+ */
+@RestController
+@RequestMapping("/app/dept")
+public class AppDeptController extends BaseController {
+    @Autowired
+    private IAppDeptService deptService;
+
+    /**
+     * 获取部门列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list")
+    public AjaxResult list(AppDept dept) {
+        List<AppDept> depts = deptService.selectDeptList(dept);
+        return AjaxResult.success(depts);
+    }
+
+    /**
+     * 查询部门列表(排除节点)
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
+    @GetMapping("/list/exclude/{deptId}")
+    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) {
+        List<AppDept> depts = deptService.selectDeptList(new AppDept());
+        Iterator<AppDept> it = depts.iterator();
+        while (it.hasNext()) {
+            AppDept d = (AppDept) it.next();
+            if (d.getDeptId().intValue() == deptId
+                    || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + "")) {
+                it.remove();
+            }
+        }
+        return AjaxResult.success(depts);
+    }
+
+    /**
+     * 根据部门编号获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:query')")
+    @GetMapping(value = "/{deptId}")
+    public AjaxResult getInfo(@PathVariable Long deptId) {
+        deptService.checkDeptDataScope(deptId);
+        return AjaxResult.success(deptService.selectDeptById(deptId));
+    }
+
+    /**
+     * 获取部门下拉树列表
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(AppDept dept) {
+        List<AppDept> depts = deptService.selectDeptList(dept);
+        return AjaxResult.success(deptService.buildDeptTreeSelect(depts));
+    }
+
+    /**
+     * 加载对应角色部门列表树
+     */
+    @GetMapping(value = "/roleDeptTreeselect/{roleId}")
+    public AjaxResult roleDeptTreeselect(@PathVariable("roleId") Long roleId) {
+        List<AppDept> depts = deptService.selectDeptList(new AppDept());
+        AjaxResult ajax = AjaxResult.success();
+        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
+        ajax.put("depts", deptService.buildDeptTreeSelect(depts));
+        return ajax;
+    }
+
+    /**
+     * 新增部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:add')")
+    @Log(title = "部门管理", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody AppDept dept) {
+        if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
+            return AjaxResult.error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        }
+        dept.setCreateBy(getUsername());
+        return toAjax(deptService.insertDept(dept));
+    }
+
+    /**
+     * 修改部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
+    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody AppDept dept) {
+        if (UserConstants.NOT_UNIQUE.equals(deptService.checkDeptNameUnique(dept))) {
+            return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
+        } else if (dept.getParentId().equals(dept.getDeptId())) {
+            return AjaxResult.error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
+        } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus())
+                && deptService.selectNormalChildrenDeptById(dept.getDeptId()) > 0) {
+            return AjaxResult.error("该部门包含未停用的子部门!");
+        }
+        dept.setUpdateBy(getUsername());
+        return toAjax(deptService.updateDept(dept));
+    }
+
+    /**
+     * 删除部门
+     */
+    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
+    @Log(title = "部门管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{deptId}")
+    public AjaxResult remove(@PathVariable Long deptId) {
+        if (deptService.hasChildByDeptId(deptId)) {
+            return AjaxResult.error("存在下级部门,不允许删除");
+        }
+        if (deptService.checkDeptExistUser(deptId)) {
+            return AjaxResult.error("部门存在邀请码,不允许删除");
+        }
+        return toAjax(deptService.deleteDeptById(deptId));
+    }
+}

+ 12 - 0
fs-admin/src/main/java/com/fs/web/controller/system/SysConfigController.java

@@ -3,9 +3,11 @@ package com.fs.web.controller.system;
 import java.util.List;
 
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.SecurityUtils;
+import com.fs.course.config.CourseConfig;
 import com.fs.sop.service.impl.QwSopServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -136,6 +138,16 @@ public class SysConfigController extends BaseController {
         return AjaxResult.success(config);
     }
 
+    /**
+    * 获取奖励类型配置
+    */
+    @GetMapping(value = "/getCourseConfigByRewardType")
+    public R getCourseConfigByRewardType() {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        return R.ok().put("rewardType",config.getRewardType());
+    }
+
     @PostMapping(value = "/updateConfigByKey")
     @Log(title = "更改参数", businessType = BusinessType.UPDATE)
     @RepeatSubmit

+ 39 - 0
fs-common/src/main/java/com/fs/common/constant/VideoRedisKeyConst.java

@@ -0,0 +1,39 @@
+package com.fs.common.constant;
+
+public interface VideoRedisKeyConst {
+
+    String ROOT_PREFIX = "video_app:";
+
+    /**
+     * 防刷与幂等校验 Key 前缀
+     * 结构: video_app:event:check:{userId}:{videoId}:{event}
+     */
+    String EVENT_CHECK_PREFIX = ROOT_PREFIX + "event:check:";
+
+    /**
+     * 视频统计指标 Hash Key 前缀
+     * 结构: video_app:metrics:{videoId}
+     */
+    String VIDEO_METRICS_PREFIX = ROOT_PREFIX + "metrics:{";
+
+    /**
+     * 用户对视频的临时权重增量 Hash Key 前缀
+     * 结构: video_app:user_weight:{userId}
+     */
+    String USER_VIDEO_WEIGHT_PREFIX = ROOT_PREFIX + "user_weight:{";
+
+    /**
+     * 定时任务同步专用的后缀
+     */
+    String SYNC_SUFFIX = ":sync";
+    String KEY_END = "}";
+    /**
+     * 待同步到 MySQL 的视频 ID 集合
+     */
+    String PENDING_SYNC_VIDEOS = ROOT_PREFIX + "pending:videos";
+
+    /**
+     * 待同步到 MySQL 的用户 ID 集合
+     */
+    String PENDING_SYNC_USERS = ROOT_PREFIX + "pending:users";
+}

+ 198 - 0
fs-common/src/main/java/com/fs/common/core/domain/entity/AppDept.java

@@ -0,0 +1,198 @@
+package com.fs.common.core.domain.entity;
+
+import com.fs.common.core.domain.BaseEntity;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * app部门表 app_dept
+ */
+public class AppDept extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 父部门ID
+     */
+    private Long parentId;
+
+    /**
+     * 祖级列表
+     */
+    private String ancestors;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 显示顺序
+     */
+    private String orderNum;
+
+    /**
+     * 负责人
+     */
+    private String leader;
+
+    /**
+     * 联系电话
+     */
+    private String phone;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 部门状态:0正常,1停用
+     */
+    private String status;
+
+    /**
+     * 删除标志(0代表存在 2代表删除)
+     */
+    private String delFlag;
+
+    /**
+     * 父部门名称
+     */
+    private String parentName;
+
+    /**
+     * 子部门
+     */
+    private List<AppDept> children = new ArrayList<AppDept>();
+
+    public Long getDeptId() {
+        return deptId;
+    }
+
+    public void setDeptId(Long deptId) {
+        this.deptId = deptId;
+    }
+
+    public Long getParentId() {
+        return parentId;
+    }
+
+    public void setParentId(Long parentId) {
+        this.parentId = parentId;
+    }
+
+    public String getAncestors() {
+        return ancestors;
+    }
+
+    public void setAncestors(String ancestors) {
+        this.ancestors = ancestors;
+    }
+
+    @NotBlank(message = "部门名称不能为空")
+    @Size(min = 0, max = 30, message = "部门名称长度不能超过30个字符")
+    public String getDeptName() {
+        return deptName;
+    }
+
+    public void setDeptName(String deptName) {
+        this.deptName = deptName;
+    }
+
+
+    public String getOrderNum() {
+        return orderNum;
+    }
+
+    public void setOrderNum(String orderNum) {
+        this.orderNum = orderNum;
+    }
+
+    public String getLeader() {
+        return leader;
+    }
+
+    public void setLeader(String leader) {
+        this.leader = leader;
+    }
+
+    @Size(min = 0, max = 11, message = "联系电话长度不能超过11个字符")
+    public String getPhone() {
+        return phone;
+    }
+
+    public void setPhone(String phone) {
+        this.phone = phone;
+    }
+
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public String getDelFlag() {
+        return delFlag;
+    }
+
+    public void setDelFlag(String delFlag) {
+        this.delFlag = delFlag;
+    }
+
+    public String getParentName() {
+        return parentName;
+    }
+
+    public void setParentName(String parentName) {
+        this.parentName = parentName;
+    }
+
+    public List<AppDept> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<AppDept> children) {
+        this.children = children;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+                .append("deptId", getDeptId())
+                .append("parentId", getParentId())
+                .append("ancestors", getAncestors())
+                .append("deptName", getDeptName())
+                .append("orderNum", getOrderNum())
+                .append("leader", getLeader())
+                .append("phone", getPhone())
+                .append("email", getEmail())
+                .append("status", getStatus())
+                .append("delFlag", getDelFlag())
+                .append("createBy", getCreateBy())
+                .append("createTime", getCreateTime())
+                .append("updateBy", getUpdateBy())
+                .append("updateTime", getUpdateTime())
+                .toString();
+    }
+}

+ 3 - 1
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -22,5 +22,7 @@ public enum DataSourceType
      * 从库
      */
     SLAVE,
-    SopREAD
+    SopREAD,
+    ZEROMALL,
+    CIVILGOODS,
 }

+ 1 - 0
fs-company/src/main/java/com/fs/company/controller/common/CommonController.java

@@ -16,6 +16,7 @@ import com.fs.common.utils.file.FileUtils;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.utils.AudioUtils;
 import com.fs.company.vo.WangUploadVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.framework.config.ServerConfig;
 import com.fs.framework.security.LoginUser;

+ 37 - 0
fs-company/src/main/java/com/fs/company/controller/company/SysConfigController.java

@@ -0,0 +1,37 @@
+package com.fs.company.controller.company;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.config.CourseConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 参数配置 信息操作处理
+ */
+@RestController
+@RequestMapping("/system/config")
+public class SysConfigController extends BaseController {
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    public RedisCache redisCache;
+
+
+    /**
+    * 获取奖励类型配置
+    */
+    @GetMapping(value = "/getCourseConfigByRewardType")
+    public R getCourseConfigByRewardType() {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        return R.ok().put("rewardType",config.getRewardType());
+    }
+
+
+}

+ 7 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseRedPacketLogController.java

@@ -267,4 +267,11 @@ public class FsCourseRedPacketLogController extends BaseController
         List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllList(id);
         return R.ok().put("list", optionsVOS);
     }
+
+    @GetMapping(value = "/videoListByWatch/{id}")
+    public R videoListByWatch(@PathVariable("id") Long id)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllListByWatch(id);
+        return R.ok().put("list", optionsVOS);
+    }
 }

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

@@ -35,6 +35,7 @@ import com.fs.qw.param.*;
 import com.fs.qw.service.*;
 import com.fs.qw.vo.QwExternalContactVO;
 import com.fs.qw.vo.QwFsUserVO;
+import com.fs.qw.vo.QwRepeatRecordListVO;
 import com.fs.qw.vo.QwUserDelLossLogVO;
 import com.fs.voice.utils.StringUtil;
 import com.github.pagehelper.PageHelper;
@@ -945,6 +946,7 @@ public class QwExternalContactController extends BaseController
     public R getRepeat(RepeatParam param){
         return  qwExternalContactService.getRepeat(param);
     }
+
     /**
      * 重粉看课记录查询
      */
@@ -957,4 +959,14 @@ public class QwExternalContactController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 同主体下外部联系人全部销售记录(含当前)
+     */
+    @GetMapping("/getRepeatRecordList")
+    public TableDataInfo getRepeatRecordList(RepeatRecordListParam param) {
+        startPage();
+        List<QwRepeatRecordListVO> list = qwExternalContactService.selectRepeatRecordList(param);
+        return getDataTable(list);
+    }
+
 }

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

@@ -435,10 +435,17 @@ public class FsStoreOrderScrmController extends BaseController
                 if("恒春来".equals(cloudHostProper.getCompanyName())
                         && ObjectUtil.isNotEmpty(lastFourNumber = order.getVirtualPhone())){
                     if (lastFourNumber.contains("-")) {
-                        lastFourNumber = lastFourNumber.length() >= 4 ? lastFourNumber.substring(lastFourNumber.length() - 4) : lastFourNumber;
+                        String beforeDash = lastFourNumber.split("-")[0];
+                        lastFourNumber = beforeDash.length() >= 4 ? beforeDash.substring(beforeDash.length() - 4) : beforeDash;
                     }else{
                         lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
                     }
+                    expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    if(expressInfoDTO!=null && !expressInfoDTO.isSuccess()){
+                        lastFourNumber = StrUtil.sub(order.getVirtualPhone(), order.getVirtualPhone().length(), -4);
+                        expressInfoDTO=expressService.getExpressInfo(order.getOrderCode(),order.getDeliverySn(),order.getDeliveryId(),lastFourNumber);
+                    }
+                    return R.ok().put("data",expressInfoDTO);
                 }
                 // 原逻辑
                 else if ((lastFourNumber = order.getUserPhone()).length() == 11) {

+ 3 - 1
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -351,6 +351,8 @@ public class Task {
                     lotteryVo.setPrizeLevel(liveLotteryProductListVo.getPrizeLevel());
                     lotteryVo.setProductName(liveLotteryProductListVo.getProductName());
                     lotteryVo.setProductId(liveLotteryProductListVo.getProductId());
+                    //设置中奖记录id
+                    lotteryVo.setRecordId(record.getId());
                     lotteryVos.add(lotteryVo);
                 }
             }
@@ -431,7 +433,7 @@ public class Task {
                 List<Long> userIds = onlineUser.stream().map(LiveWatchUser::getUserId).collect(Collectors.toList());
                 // 4.保存用户领取记录
                 saveUserRewardRecord(openRewardLive, userIds,config.getScoreAmount());
-                // 5.更新用户积分(芳华币
+                // 5.更新用户积分(积分
                 fsUserService.increaseIntegral(userIds,config.getScoreAmount());
                 // 6.发送websocket事件消息 通知用户自动领取成功
                 userIds.forEach(userId -> webSocketServer.sendIntegralMessage(openRewardLive.getLiveId(),userId,config.getScoreAmount()));

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

@@ -869,7 +869,7 @@ public class WebSocketServer {
         sendMsgVo.setUserId(userId);
         sendMsgVo.setUserType(0L);
         sendMsgVo.setCmd("Integral");
-        sendMsgVo.setMsg("恭喜你成功获得观看奖励:" + scoreAmount + "芳华币");
+        sendMsgVo.setMsg("恭喜你成功获得观看奖励:" + scoreAmount + "积分");
         sendMsgVo.setData(String.valueOf(scoreAmount));
 
         if(Objects.isNull( session)) return;

+ 17 - 5
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -289,7 +289,10 @@ public class QwMsgController {
                 log.info("id:{}, 需要二次验证:"+wxWorkMsgResp.getJson(), id);
 
                 redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),100012,10, TimeUnit.MINUTES);
-
+                QwUser qwUser1 = qwUserMapper.selectQwUserById(id);
+                if(qwUser1.getUid().equals(wxWorkMsgResp.getUuid())){
+                    qwUserService.atMsg(qwUser1, "需要二次验证(在销售后台,企微员工页面点击二次验证)");
+                }
                 break;
             case 100005:
 
@@ -341,14 +344,23 @@ public class QwMsgController {
                         WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
                         ste.setMsgid(wxWorkMessageDTO.getMsg_id());
                         ste.setUuid(wxWorkMsgResp.getUuid());
+
                         WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> dto = wxWorkService.SpeechToTextEntity(ste, serverId);
                         System.out.println(dto);
-                        if(dto.getErrcode() != 0 || Objects.isNull(dto.getData()) || StringUtils.isBlank(dto.getData().getText())){
+
+                        int maxRetries = 3;
+                        int retryCount = 0;
+
+                        while((dto.getErrcode() != 0 || Objects.isNull(dto.getData()) || StringUtils.isBlank(dto.getData().getText()))
+                                && retryCount < maxRetries) {
                             try {
-                                TimeUnit.SECONDS.sleep(2); // 阻塞2秒
+                                TimeUnit.SECONDS.sleep(1);
+                                retryCount++;
+                                log.info("id:{}, 语音转换第{}次重试", id, retryCount);
                             } catch (InterruptedException e) {
-                                Thread.currentThread().interrupt(); // 处理中断异常
-                                log.info("id:{}, 第一次语音转换失败", id);
+                                Thread.currentThread().interrupt();
+                                log.info("id:{}, 语音转换等待被中断", id);
+                                break;
                             }
                             dto = wxWorkService.SpeechToTextEntity(ste, serverId);
                         }

+ 4 - 2
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/job/QwMsgAuditScheduleJob.java

@@ -13,7 +13,9 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -37,7 +39,7 @@ public class QwMsgAuditScheduleJob {
     /**
      * 每1分钟执行一次,拉取会话存档消息并存库
      */
-    @Scheduled(cron = "0 */1 * * * ?")
+//    @Scheduled(cron = "0 */1 * * * ?")
     public void pullQwMsgAndStore() {
         List<String> corpIds = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
         if (corpIds == null || corpIds.isEmpty()) {
@@ -55,7 +57,7 @@ public class QwMsgAuditScheduleJob {
     /**
      * 每2分钟执行一次,扫描 media_oss_url为空的媒体消息(语音/视频/图片),下载并上传至 OSS
      */
-    @Scheduled(cron = "0 */2 * * * ?")
+//    @Scheduled(cron = "0 */2 * * * ?")
     public void uploadMediaToOss() {
         List<String> corpIds = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
         if (corpIds == null || corpIds.isEmpty()) {

+ 267 - 0
fs-service/src/main/java/com/fs/app/civilgoods/domain/CivilGoods.java

@@ -0,0 +1,267 @@
+package com.fs.app.civilgoods.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("fs_store_product")
+public class CivilGoods {
+
+    /**
+     * 商品id
+     */
+    @TableId(type = IdType.AUTO)
+    private Long productId;
+
+    /**
+     * 商品图片
+     */
+    private String image;
+
+    /**
+     * 轮播图
+     */
+    private String sliderImage;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 商品简介
+     */
+    private String productInfo;
+
+    /**
+     * 关键字
+     */
+    private String keyword;
+
+    /**
+     * 产品条码(一维码)
+     */
+    private String barCode;
+
+    /**
+     * 分类id
+     */
+    private Long cateId;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal price;
+
+    /**
+     * 会员价格
+     */
+    private BigDecimal vipPrice;
+
+    /**
+     * 市场价
+     */
+    private BigDecimal otPrice;
+
+    /**
+     * 代理价格
+     */
+    private BigDecimal agentPrice;
+
+    /**
+     * 邮费
+     */
+    private BigDecimal postage;
+
+    /**
+     * 单位名
+     */
+    private String unitName;
+
+    /**
+     * 排序
+     */
+    private Short sort;
+
+    /**
+     * 销量
+     */
+    private Integer sales;
+
+    /**
+     * 库存
+     */
+    private Integer stock;
+
+    /**
+     * 状态(0:未上架,1:上架)
+     */
+    private Integer isShow;
+
+    /**
+     * 是否热卖
+     */
+    private Integer isHot;
+
+    /**
+     * 是否优惠
+     */
+    private Integer isBenefit;
+
+    /**
+     * 是否精品
+     */
+    private Integer isBest;
+
+    /**
+     * 是否新品
+     */
+    private Integer isNew;
+
+    /**
+     * 产品描述
+     */
+    private String description;
+
+    /**
+     * 添加时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 是否包邮
+     */
+    private Integer isPostage;
+
+    /**
+     * 是否删除
+     */
+    private Integer isDel;
+
+    /**
+     * 获得积分
+     */
+    private BigDecimal giveIntegral;
+
+    /**
+     * 成本价
+     */
+    private BigDecimal cost;
+
+    /**
+     * 是否优品推荐
+     */
+    private Integer isGood;
+
+    /**
+     * 浏览量
+     */
+    private Integer browse;
+
+    /**
+     * 产品二维码地址(用户小程序海报)
+     */
+    private String codePath;
+
+    /**
+     * 运费模板ID
+     */
+    private Integer tempId;
+
+    /**
+     * 规格 0单 1多
+     */
+    private Integer specType;
+
+    /**
+     * 是否开启积分兑换
+     */
+    private Integer isIntegral;
+
+    /**
+     * 需要多少积分兑换 只在开启积分兑换时生效
+     */
+    private Integer integral;
+
+    /**
+     * 商品类型:1非处方 2处方
+     */
+    private Integer productType;
+
+    /**
+     * 国药准字
+     */
+    private String prescribeCode;
+
+    /**
+     * 规格
+     */
+    private String prescribeSpec;
+
+    /**
+     * 生产厂家
+     */
+    private String prescribeFactory;
+
+    /**
+     * 处方名
+     */
+    private String prescribeName;
+
+    /**
+     * 是否在商品展示
+     */
+    private Integer isDisplay;
+
+    /**
+     * 商品推广分类
+     */
+    private Integer tuiCateId;
+
+    /**
+     * 仓库id
+     */
+    private Integer warehouseId;
+
+    /**
+     * 仓库code
+     */
+    private String warehouseCode;
+
+    /**
+     * 服用方法
+     */
+    private String usageMethod;
+
+    /**
+     * 一日几次
+     */
+    private String frequency;
+
+    /**
+     * 用药数量
+     */
+    private String dosage;
+
+    /**
+     * 税收分类码
+     */
+    private String taxClassificationCode;
+
+    /**
+     * 商品发票名称
+     */
+    private String invoiceName;
+
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/app/civilgoods/dto/CivilGoodsDTO.java

@@ -0,0 +1,17 @@
+package com.fs.app.civilgoods.dto;
+
+import com.fs.app.civilgoods.domain.CivilGoods;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class CivilGoodsDTO extends CivilGoods {
+
+    private String keyword;
+
+    private Long metaId;
+
+    private Long limit = 50L;
+
+}

+ 11 - 0
fs-service/src/main/java/com/fs/app/civilgoods/mapper/CivilGoodsMapper.java

@@ -0,0 +1,11 @@
+package com.fs.app.civilgoods.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.civilgoods.domain.CivilGoods;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface CivilGoodsMapper extends BaseMapper<CivilGoods> {
+
+
+}

+ 16 - 0
fs-service/src/main/java/com/fs/app/civilgoods/service/ICivilGoodsService.java

@@ -0,0 +1,16 @@
+package com.fs.app.civilgoods.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.civilgoods.domain.CivilGoods;
+import com.fs.app.civilgoods.dto.CivilGoodsDTO;
+import com.fs.app.civilgoods.vo.CivilGoodsVO;
+
+import java.util.List;
+
+public interface ICivilGoodsService extends IService<CivilGoods> {
+
+    CivilGoods getByProductId(Long productId);
+
+    List<CivilGoodsVO> findOptions(CivilGoodsDTO req);
+
+}

+ 69 - 0
fs-service/src/main/java/com/fs/app/civilgoods/service/impl/CivilGoodsServiceImpl.java

@@ -0,0 +1,69 @@
+package com.fs.app.civilgoods.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.civilgoods.domain.CivilGoods;
+import com.fs.app.civilgoods.dto.CivilGoodsDTO;
+import com.fs.app.civilgoods.mapper.CivilGoodsMapper;
+import com.fs.app.civilgoods.service.ICivilGoodsService;
+import com.fs.app.civilgoods.vo.CivilGoodsVO;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@DataSource(DataSourceType.CIVILGOODS)
+public class CivilGoodsServiceImpl extends ServiceImpl<CivilGoodsMapper, CivilGoods> implements ICivilGoodsService {
+
+    @Override
+    public CivilGoods getByProductId(Long productId) {
+        return this.lambdaQuery()
+                .eq(CivilGoods::getProductId, productId)
+                .one();
+    }
+
+    @Override
+    public List<CivilGoodsVO> findOptions(CivilGoodsDTO req) {
+        if (ObjectUtil.isNotEmpty(req.getMetaId())) {
+            CivilGoods topProduct = this.lambdaQuery()
+                    .eq(CivilGoods::getProductId, req.getMetaId())
+                    .eq(CivilGoods::getIsShow, 1)
+                    .eq(CivilGoods::getIsDel, 0)
+                    .one();
+
+            LambdaQueryChainWrapper<CivilGoods> otherQuery = this.lambdaQuery();
+            otherQuery.eq(CivilGoods::getIsShow, 1);
+            otherQuery.eq(CivilGoods::getIsDel, 0);
+
+            if (ObjectUtil.isNotEmpty(req.getKeyword())) {
+                otherQuery.and(q -> q.like(CivilGoods::getProductName, req.getKeyword())
+                        .or()
+                        .eq(CivilGoods::getProductId, req.getKeyword()));
+            }
+            otherQuery.ne(CivilGoods::getProductId, req.getMetaId())
+                    .last(" LIMIT " + (req.getLimit() - 1L));
+            List<CivilGoods> otherList = otherQuery.list();
+            List<CivilGoods> finalList = new ArrayList<>();
+            if (topProduct != null) {
+                finalList.add(topProduct);
+            }
+            finalList.addAll(otherList);
+            return BeanCopyUtils.copyList(finalList, CivilGoodsVO.class);
+        }
+        LambdaQueryChainWrapper<CivilGoods> lambdaQuery = this.lambdaQuery();
+        lambdaQuery.eq(CivilGoods::getIsShow, 1);
+        lambdaQuery.eq(CivilGoods::getIsDel, 0);
+        if (ObjectUtil.isNotEmpty(req.getKeyword())) {
+            lambdaQuery.and(q -> q.like(CivilGoods::getProductName, req.getKeyword())
+                    .or()
+                    .eq(CivilGoods::getProductId, req.getKeyword()));
+        }
+        lambdaQuery.last(" LIMIT " + req.getLimit());
+        return BeanCopyUtils.copyList(lambdaQuery.list(), CivilGoodsVO.class);
+    }
+}

+ 10 - 0
fs-service/src/main/java/com/fs/app/civilgoods/vo/CivilGoodsVO.java

@@ -0,0 +1,10 @@
+package com.fs.app.civilgoods.vo;
+
+import com.fs.app.civilgoods.domain.CivilGoods;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class CivilGoodsVO extends CivilGoods {
+}

+ 252 - 0
fs-service/src/main/java/com/fs/app/medicines/domain/AppFsStoreProduct.java

@@ -0,0 +1,252 @@
+package com.fs.app.medicines.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("fs_store_product")
+public class AppFsStoreProduct {
+
+    /**
+     * 主键ID(根据你的表自行调整)
+     */
+    @TableId(type = IdType.AUTO)
+    private Long productId;
+
+    /**
+     * 商品图片
+     */
+    private String image;
+
+    /**
+     * 轮播图
+     */
+    private String sliderImage;
+
+    /**
+     * 商品名称
+     */
+    private String productName;
+
+    /**
+     * 商品简介
+     */
+    private String productInfo;
+
+    /**
+     * 关键字
+     */
+    private String keyword;
+
+    /**
+     * 产品条码(一维码)
+     */
+    private String barCode;
+
+    /**
+     * 分类id
+     */
+    private Long cateId;
+
+    /**
+     * 商品价格
+     */
+    private BigDecimal price;
+
+    /**
+     * 会员价格
+     */
+    private BigDecimal vipPrice;
+
+    /**
+     * 市场价
+     */
+    private BigDecimal otPrice;
+
+    /**
+     * 代理价格
+     */
+    private BigDecimal agentPrice;
+
+    /**
+     * 邮费
+     */
+    private BigDecimal postage;
+
+    /**
+     * 单位名
+     */
+    private String unitName;
+
+    /**
+     * 排序
+     */
+    private Short sort;
+
+    /**
+     * 销量
+     */
+    private Integer sales;
+
+    /**
+     * 库存
+     */
+    private Integer stock;
+
+    /**
+     * 状态(0:未上架,1:上架)
+     */
+    private Integer isShow;
+
+    /**
+     * 是否热卖
+     */
+    private Integer isHot;
+
+    /**
+     * 是否优惠
+     */
+    private Integer isBenefit;
+
+    /**
+     * 是否精品
+     */
+    private Integer isBest;
+
+    /**
+     * 是否新品
+     */
+    private Integer isNew;
+
+    /**
+     * 产品描述
+     */
+    private String description;
+
+    /**
+     * 添加时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 是否包邮
+     */
+    private Integer isPostage;
+
+    /**
+     * 是否删除
+     */
+    private Integer isDel;
+
+    /**
+     * 获得积分
+     */
+    private BigDecimal giveIntegral;
+
+    /**
+     * 成本价
+     */
+    private BigDecimal cost;
+
+    /**
+     * 是否优品推荐
+     */
+    private Integer isGood;
+
+    /**
+     * 浏览量
+     */
+    private Integer browse;
+
+    /**
+     * 产品二维码地址(用户小程序海报)
+     */
+    private String codePath;
+
+    /**
+     * 运费模板ID
+     */
+    private Integer tempId;
+
+    /**
+     * 规格 0单 1多
+     */
+    private Integer specType;
+
+    /**
+     * 是开启积分兑换
+     */
+    private Integer isIntegral;
+
+    /**
+     * 需要多少积分兑换 只在开启积分兑换时生效
+     */
+    private Integer integral;
+
+    /**
+     * 商品类型:1非处方 2处方
+     */
+    private Integer productType;
+
+    /**
+     * 国药准字
+     */
+    private String prescribeCode;
+
+    /**
+     * 规格
+     */
+    private String prescribeSpec;
+
+    /**
+     * 生产厂家
+     */
+    private String prescribeFactory;
+
+    /**
+     * 处方名
+     */
+    private String prescribeName;
+
+    /**
+     * 仓库id
+     */
+    private Long warehouseId;
+
+    /**
+     * 仓库code
+     */
+    private String warehouseCode;
+
+    /**
+     * 是否在商品展示
+     */
+    private Integer isDisplay;
+
+    /**
+     * 商品推广分类
+     */
+    private Integer tuiCateId;
+
+    /**
+     * 是否免除运费
+     */
+    private Integer isFreePostage;
+
+    /**
+     * 是否免除服务费
+     */
+    private Integer isFreeService;
+
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/app/medicines/dto/AppFsStoreProductDTO.java

@@ -0,0 +1,17 @@
+package com.fs.app.medicines.dto;
+
+import com.fs.app.medicines.domain.AppFsStoreProduct;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AppFsStoreProductDTO extends AppFsStoreProduct {
+
+    private Long metaId;
+
+    private String keyword;
+
+    private Long limit;
+
+}

+ 9 - 0
fs-service/src/main/java/com/fs/app/medicines/mapper/AppFsStoreProductMapper.java

@@ -0,0 +1,9 @@
+package com.fs.app.medicines.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.medicines.domain.AppFsStoreProduct;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AppFsStoreProductMapper extends BaseMapper<AppFsStoreProduct> {
+}

+ 16 - 0
fs-service/src/main/java/com/fs/app/medicines/service/IAppFsStoreProductService.java

@@ -0,0 +1,16 @@
+package com.fs.app.medicines.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.medicines.domain.AppFsStoreProduct;
+import com.fs.app.medicines.dto.AppFsStoreProductDTO;
+import com.fs.app.medicines.vo.AppFsStoreProductVO;
+
+import java.util.List;
+
+public interface IAppFsStoreProductService extends IService<AppFsStoreProduct> {
+
+    AppFsStoreProduct getByProductId(Long productId);
+
+    List<AppFsStoreProductVO> findOptions(AppFsStoreProductDTO req);
+
+}

+ 69 - 0
fs-service/src/main/java/com/fs/app/medicines/service/impl/AppFsStoreProductServiceImpl.java

@@ -0,0 +1,69 @@
+package com.fs.app.medicines.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.medicines.domain.AppFsStoreProduct;
+import com.fs.app.medicines.dto.AppFsStoreProductDTO;
+import com.fs.app.medicines.mapper.AppFsStoreProductMapper;
+import com.fs.app.medicines.service.IAppFsStoreProductService;
+import com.fs.app.medicines.vo.AppFsStoreProductVO;
+import com.fs.common.BeanCopyUtils;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@DataSource(DataSourceType.ZEROMALL)//零利润商城/药品商城
+public class AppFsStoreProductServiceImpl extends ServiceImpl<AppFsStoreProductMapper, AppFsStoreProduct> implements IAppFsStoreProductService {
+
+
+    @Override
+    public AppFsStoreProduct getByProductId(Long productId) {
+        return this.getById(productId);
+    }
+
+    @Override
+    public List<AppFsStoreProductVO> findOptions(AppFsStoreProductDTO req) {
+        if (ObjectUtil.isNotEmpty(req.getMetaId())) {
+            AppFsStoreProduct topProduct = this.lambdaQuery()
+                    .eq(AppFsStoreProduct::getProductId, req.getMetaId())
+                    .eq(AppFsStoreProduct::getIsShow, 1)
+                    .eq(AppFsStoreProduct::getIsDel, 0)
+                    .one();
+
+            LambdaQueryChainWrapper<AppFsStoreProduct> otherQuery = this.lambdaQuery();
+            otherQuery.eq(AppFsStoreProduct::getIsShow, 1);
+            otherQuery.eq(AppFsStoreProduct::getIsDel, 0);
+            if (ObjectUtil.isNotEmpty(req.getKeyword())) {
+                otherQuery.and(q -> q.like(AppFsStoreProduct::getProductName, req.getKeyword())
+                        .or()
+                        .eq(AppFsStoreProduct::getProductId, req.getKeyword()));
+            }
+            otherQuery.ne(AppFsStoreProduct::getProductId, req.getMetaId())
+                    .last(" LIMIT " + (req.getLimit() - 1L));
+
+            List<AppFsStoreProduct> otherList = otherQuery.list();
+            List<AppFsStoreProduct> finalList = new ArrayList<>();
+            if (topProduct != null) {
+                finalList.add(topProduct);
+            }
+            finalList.addAll(otherList);
+            return BeanCopyUtils.copyList(finalList, AppFsStoreProductVO.class);
+        }
+
+        LambdaQueryChainWrapper<AppFsStoreProduct> lambdaQuery = this.lambdaQuery();
+        lambdaQuery.eq(AppFsStoreProduct::getIsShow, 1);
+        lambdaQuery.eq(AppFsStoreProduct::getIsDel, 0);
+        if (ObjectUtil.isNotEmpty(req.getKeyword())) {
+            lambdaQuery.and(q -> q.like(AppFsStoreProduct::getProductName, req.getKeyword())
+                    .or()
+                    .eq(AppFsStoreProduct::getProductId, req.getKeyword()));
+        }
+        lambdaQuery.last(" LIMIT " + req.getLimit());
+        return BeanCopyUtils.copyList(lambdaQuery.list(), AppFsStoreProductVO.class);
+    }
+}

+ 10 - 0
fs-service/src/main/java/com/fs/app/medicines/vo/AppFsStoreProductVO.java

@@ -0,0 +1,10 @@
+package com.fs.app.medicines.vo;
+
+import com.fs.app.medicines.domain.AppFsStoreProduct;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AppFsStoreProductVO extends AppFsStoreProduct {
+}

+ 1 - 1
fs-service/src/main/java/com/fs/app/sender/properties/ConfigProperties.java

@@ -18,7 +18,7 @@ public class ConfigProperties {
      * IM默认接收人,当且仅当isDebug为true时生效
      * 测试环境尽量开启,避免配置错误,导致发送给了真实客户
      */
-    private String receiver = "4052861305";
+    private String receiver = "";
 
     /**
      * IM发送人账号前缀

+ 4 - 0
fs-service/src/main/java/com/fs/app/sop/dto/AppSopUserLogInfoDTO.java

@@ -32,4 +32,8 @@ public class AppSopUserLogInfoDTO {
 
     private Long[] tagIds;
 
+    private Long startIndex;
+
+    private Long pageLimit;
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/app/sop/mapper/AppSopUserLogInfoMapper.java

@@ -26,4 +26,6 @@ public interface AppSopUserLogInfoMapper extends BaseMapper<AppSopUserLogsInfos>
 
     int handleRepeatData();
 
+    Long findListTotal(AppSopUserLogInfoDTO dto);
+
 }

+ 2 - 1
fs-service/src/main/java/com/fs/app/sop/service/IAppSopUserLogInfoService.java

@@ -5,12 +5,13 @@ import com.fs.app.sop.domain.AppSopUserLogsInfos;
 import com.fs.app.sop.dto.AppSopUserLogInfoDTO;
 import com.fs.app.sop.vo.AppSopUserLogInfoVO;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
 
 import java.util.List;
 
 public interface IAppSopUserLogInfoService extends IService<AppSopUserLogsInfos> {
 
-    List<AppSopUserLogInfoVO> findList(AppSopUserLogInfoDTO dto);
+    TableDataInfo findList(AppSopUserLogInfoDTO dto);
 
     int deleteByIds(List<Long> ids);
 

+ 17 - 6
fs-service/src/main/java/com/fs/app/sop/service/impl/AppSopUserLogInfoServiceImpl.java

@@ -11,7 +11,9 @@ import com.fs.app.sop.service.IAppSopService;
 import com.fs.app.sop.service.IAppSopUserLogInfoService;
 import com.fs.app.sop.service.IAppSopUserLogService;
 import com.fs.app.sop.vo.AppSopUserLogInfoVO;
+import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -22,10 +24,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.time.ZoneId;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
 
 @Slf4j
 @Service
@@ -43,8 +42,20 @@ public class AppSopUserLogInfoServiceImpl extends ServiceImpl<AppSopUserLogInfoM
 
 
     @Override
-    public List<AppSopUserLogInfoVO> findList(AppSopUserLogInfoDTO dto) {
-        return baseMapper.findList(dto);
+    public TableDataInfo findList(AppSopUserLogInfoDTO dto) {
+        Long listTotal = this.baseMapper.findListTotal(dto);
+        TableDataInfo tableDataInfo = new TableDataInfo();
+        tableDataInfo.setCode(HttpStatus.SUCCESS);
+        tableDataInfo.setMsg("查询成功");
+        tableDataInfo.setTotal(listTotal);
+        tableDataInfo.setRows(new ArrayList<>());
+        if (listTotal > 0L) {
+            Long startIndex = (dto.getStartIndex() - 1L) * dto.getPageLimit();
+            dto.setStartIndex(startIndex);
+            List<AppSopUserLogInfoVO> userLogInfoVOS = baseMapper.findList(dto);
+            tableDataInfo.setRows(userLogInfoVOS);
+        }
+        return tableDataInfo;
     }
 
     @Override

+ 11 - 0
fs-service/src/main/java/com/fs/common/service/ISmsService.java

@@ -1,9 +1,12 @@
 package com.fs.common.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.company.domain.CompanySmsTemp;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
 import com.fs.crm.param.SmsSendUserParam;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.enums.SmsLogType;
 
 
 public interface ISmsService
@@ -26,4 +29,12 @@ public interface ISmsService
 
     R sendUrl(String phone, String content, String code,Long uuid,Integer smsIndex,String deleteKey,Long companyId,Long companyUserId);
 
+    /**
+     *  根据号码、内容、模板发送短信(简洁版)
+     *  @param phone 号码
+     *  @param content 内容
+     *  @param temp 模板
+     * */
+    R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, SendMsgLogBo sendMsgLogBo);
+
 }

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

@@ -9,6 +9,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 
 import com.fs.common.service.ISmsService;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.vo.SmsNotifyVO;
 import com.fs.common.vo.SmsSendItemVO;
@@ -23,6 +24,7 @@ import com.fs.company.service.ICompanySmsTempService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.impl.CompanyVoiceRoboticCallBlacklistServiceImpl;
 import com.fs.company.vo.CompanyVoiceRoboticCallBlacklistCheckVO;
+import com.fs.course.config.CourseConfig;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.param.SmsSendParam;
@@ -34,15 +36,22 @@ import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.mapper.FsPackageOrderMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.vo.FsPackageOrderVO;
+import com.fs.qw.bo.SendMsgLogBo;
+import com.fs.qw.domain.QwAcquisitionLinkInfo;
 import com.fs.qw.domain.QwSopSmsLogs;
+import com.fs.qw.enums.SmsLogType;
+import com.fs.qw.mapper.QwAcquisitionLinkInfoMapper;
 import com.fs.qw.mapper.QwSopSmsLogsMapper;
 import com.fs.qw.service.IQwSopSmsLogsService;
+import com.fs.qw.strategy.SmsLogStrategyManager;
 import com.fs.sms.domain.SendSmsReturn;
 import com.fs.sms.service.impl.SmsTServiceImpl;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.google.gson.Gson;
 import lombok.Synchronized;
 import lombok.extern.slf4j.Slf4j;
@@ -61,6 +70,8 @@ import com.fs.company.service.ICompanySmsCommonLogsService;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
 @Service
 @Slf4j
 public class SmsServiceImpl implements ISmsService
@@ -109,6 +120,20 @@ public class SmsServiceImpl implements ISmsService
     @Autowired
     private RedissonClient redissonClient;
 
+    //获客链接短信模板code
+    private static final String  SMS_LINK_TEMPLATE_CODE = "获客链接短信模板";
+
+    @Autowired
+    private QwAcquisitionLinkInfoMapper qwAcquisitionLinkInfoMapper;
+
+    @Autowired
+    private SmsLogStrategyManager smsLogStrategyManager;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    private static final String  LINK_SUFFIX = "?customer_channel=up:";
+
     @Override
     public R sendTSms(String mobile, String code) {
 //        try{
@@ -785,7 +810,88 @@ public class SmsServiceImpl implements ISmsService
         return R.ok();
     }
 
+    /**
+     * 发送简单短信
+     * @param phone 接收方手机号
+     * @param content 短信内容
+     * @param temp 短信模板
+     * @param logType 日志记录类型,用于区分调用方
+     * @param sendMsgLogBo 特定业务的上下文对象,如qwAcquisitionId或externalContactId
+     * @return R 响应结果
+     */
+    @Override
+    public R simpleSmsSend(String phone, String content, CompanySmsTemp temp, SmsLogType logType, SendMsgLogBo sendMsgLogBo) {
+        String urls = null;
+        R response; // 存储最终响应
+        Integer number = calculateSmsCount(content);
+        SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("his.sms");
+        FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
+
+        try {
+            urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + phone + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+        } catch (UnsupportedEncodingException e) {
+            log.error("{}发送失败", phone, e);
+            response = R.error("短信发送失败:" + e.getMessage());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            return response;
+        }
+
+        String post = HttpRequest.get(urls).execute().body();
+        SmsSendVO vo = JSONUtil.toBean(post, SmsSendVO.class);
+
+        if (vo.getStatus().equals(0)) {
+            boolean anySuccess = false;
+            for (SmsSendItemVO itemVO : vo.getList()) {
+                if (itemVO.getResult().equals("0")) {
+                    anySuccess = true;
+                    // 记录通用日志
+                    CompanySmsLogs logs = new CompanySmsLogs();
+                    logs.setContent(content);
+                    logs.setTempCode(temp.getTempCode());
+                    logs.setTempId(temp.getTempId());
+                    logs.setPhone(phone);
+                    logs.setSendTime(new Date());
+                    logs.setStatus(0);
+                    logs.setType(sms.getType());
+                    logs.setMid(itemVO.getMid());
+                    logs.setNumber(number);
+                    logs.setCompanyId(sendMsgLogBo.getCompanyId());
+                    logs.setCompanyUserId(sendMsgLogBo.getCompanyUserId());
+                    logs.setCustomerId(sendMsgLogBo.getCustomerId());
+                    smsLogsService.insertCompanySmsLogs(logs);
+                    //子记录表关联主表的id
+                    sendMsgLogBo.setCompanySmsLogsId(logs.getLogsId());
+                }
+            }
+            if(anySuccess) {
+                response = R.ok();
+                // 记录特定业务日志
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            } else {
+                response = R.error("发送短信失败,服务商返回无成功项!");
+                smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+            }
+        } else {
+            response = R.error("发送短信失败!状态码: " + vo.getStatus());
+            // 发送失败也要记录特定业务日志
+            smsLogStrategyManager.executeLogStrategy(logType, response, content, phone, temp.getTempId(), sms.getType(), number, sendMsgLogBo);
+        }
+        return response;
+    }
 
+    // 将计算短信条数的逻辑提取出来,方便复用
+    private int calculateSmsCount(String content) {
+        if (content == null) return 1;
+        int counts = content.length() / 67;
+        if (content.length() % 67 > 0) {
+            counts = counts + 1;
+        }
+        if (counts == 0) {
+            counts = 1;
+        }
+        return counts;
+    }
 
     @Override
     @Synchronized
@@ -965,6 +1071,19 @@ public class SmsServiceImpl implements ISmsService
                 content=content.replace("${sms.senderName}",param.getSenderName());
             }
 
+            if (param.getTempCode()!=null &&SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+
+                String json = configService.selectConfigByKey("course.config");
+                CourseConfig config = JSON.parseObject(json, CourseConfig.class);
+
+                String randomStr = ShortCodeGeneratorUtils.generate8();
+                String replaceText=config.getSmsAcquisitionName()+randomStr;
+                content = content.replace("${sms.friendLink}",replaceText);
+                //添加获客链接记录
+                addAcquisitionLinkInfo(null,crmCustomer.getMobile(),param.getCardUrl(),randomStr, param.getCompanyUserId());
+            }
+
+
             String urls= null;
             // 通知类的不加 退订回T 只有营销类的加
             //最多500个手机号
@@ -972,12 +1091,18 @@ public class SmsServiceImpl implements ISmsService
             FsSmsConfig sms = JSON.parseObject(sysConfig.getConfigValue(), FsSmsConfig.class);
             if (sms.getType().equals("rf")){
                 try {
-                    if(temp.getTempType().equals(1)){
-                        urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
-                    }
-                    else if(temp.getTempType().equals(2)){
-                        urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+
+                    if (temp.getTempType().equals(2) && SMS_LINK_TEMPLATE_CODE.equals(temp.getTempCode())){
+                        urls = sms.getRfUrl2() + "sms?action=send&account=" + sms.getRfAccount2() + "&password=" + sms.getRfPassword2() + "&mobile=" + crmCustomer.getMobile() + "&content=" + URLEncoder.encode(content, "UTF-8") + "&extno=" + sms.getRfCode2() + "&rt=json";
+                    } else {
+                        if(temp.getTempType().equals(1)){
+                            urls = sms.getRfUrl1()+"sms?action=send&account="+sms.getRfAccount1()+"&password="+sms.getRfPassword1()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content, "UTF-8")+"&extno="+sms.getRfCode1()+"&rt=json";
+                        }
+                        else if(temp.getTempType().equals(2)){
+                            urls = sms.getRfUrl2()+"sms?action=send&account="+sms.getRfAccount2()+"&password="+sms.getRfPassword2()+"&mobile="+crmCustomer.getMobile()+"&content="+ URLEncoder.encode(sms.getRfSign()+content+"拒收请回复R", "UTF-8")+"&extno="+sms.getRfCode2()+"&rt=json";
+                        }
                     }
+
                 } catch (UnsupportedEncodingException e) {
                     e.printStackTrace();
                 }
@@ -1074,6 +1199,26 @@ public class SmsServiceImpl implements ISmsService
         }
     }
 
+
+
+    /**
+     * 添加链接生成记录
+     * */
+    public int addAcquisitionLinkInfo(Long qwAcquisitionAssistantId,String originalPhone,String originalLink,String randomStr,Long createBy){
+        QwAcquisitionLinkInfo qwAcquisitionLinkInfo=new QwAcquisitionLinkInfo();
+        qwAcquisitionLinkInfo.setQwAcquisitionAssistantId(qwAcquisitionAssistantId);
+        qwAcquisitionLinkInfo.setCreateBy(createBy);
+        qwAcquisitionLinkInfo.setCreateTime(DateUtils.getTime());
+        qwAcquisitionLinkInfo.setPhone(originalPhone);//这里存储原始手机号
+        //加密手机号
+        String phonePlus = encryptPhone(originalPhone);
+        String linkPlus=originalLink+LINK_SUFFIX+ phonePlus;
+        qwAcquisitionLinkInfo.setLink(linkPlus);
+        qwAcquisitionLinkInfo.setRandomStr(randomStr);
+        int addResult=qwAcquisitionLinkInfoMapper.insertQwAcquisitionLinkInfo(qwAcquisitionLinkInfo);
+        return addResult;
+    }
+
     /**
      * 根据 uuid(QwSopSmsLogs.id)和索引更新子记录,并检查主记录完成状态
      */

+ 15 - 9
fs-service/src/main/java/com/fs/company/domain/CompanySms.java

@@ -7,7 +7,7 @@ import org.apache.commons.lang3.builder.ToStringStyle;
 
 /**
  * 公司短信对象 company_sms
- * 
+ *
  * @author fs
  * @date 2023-01-09
  */
@@ -30,43 +30,49 @@ public class CompanySms extends BaseEntity
     @Excel(name = "总短信数")
     private Long totalSmsCount;
 
-    public void setSmsId(Long smsId) 
+    /** 乐观锁版本号 */
+    private Long version;
+
+    public void setSmsId(Long smsId)
     {
         this.smsId = smsId;
     }
 
-    public Long getSmsId() 
+    public Long getSmsId()
     {
         return smsId;
     }
-    public void setCompanyId(Long companyId) 
+    public void setCompanyId(Long companyId)
     {
         this.companyId = companyId;
     }
 
-    public Long getCompanyId() 
+    public Long getCompanyId()
     {
         return companyId;
     }
-    public void setRemainSmsCount(Long remainSmsCount) 
+    public void setRemainSmsCount(Long remainSmsCount)
     {
         this.remainSmsCount = remainSmsCount;
     }
 
-    public Long getRemainSmsCount() 
+    public Long getRemainSmsCount()
     {
         return remainSmsCount;
     }
-    public void setTotalSmsCount(Long totalSmsCount) 
+    public void setTotalSmsCount(Long totalSmsCount)
     {
         this.totalSmsCount = totalSmsCount;
     }
 
-    public Long getTotalSmsCount() 
+    public Long getTotalSmsCount()
     {
         return totalSmsCount;
     }
 
+    public Long getVersion() {
+        return version;
+    }
     @Override
     public String toString() {
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

+ 19 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanySmsMapper.java

@@ -90,4 +90,23 @@ public interface CompanySmsMapper {
     Long debitSmsCount(Long companyId);
 
 
+    // ========== 扣减/增加方法 ==========
+    /** 直接扣减(配合悲观锁) */
+    int decrementRemainSmsCount(@Param("companyId") Long companyId,
+                                @Param("number") int number);
+
+    /** 直接增加(配合悲观锁) */
+    int incrementRemainSmsCount(@Param("companyId") Long companyId,
+                                @Param("number") int number);
+
+    /** 乐观锁扣减 */
+    int decrementRemainSmsCountWithVersion(@Param("companyId") Long companyId,
+                                           @Param("number") int number,
+                                           @Param("currentVersion") Long currentVersion);
+
+    /** 乐观锁增加 */
+    int incrementRemainSmsCountWithVersion(@Param("companyId") Long companyId,
+                                           @Param("number") int number,
+                                           @Param("currentVersion") Long currentVersion);
+
 }

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

@@ -368,4 +368,7 @@ public interface CompanyUserMapper
             "update company_user set ai_sip_call_user_id=#{aiSipCallId} where user_id=#{companyUserId} " +
             "</script>")
     public int updateCompanyUserByAiSipCall(@Param("companyUserId") Long companyUserId, @Param("aiSipCallId") Long aiSipCallId);
+
+    List<CompanyUser> findCompanyUserOptions(@Param("keyword") String keyword, @Param("selectedId") Long selectedId,
+                                             @Param("pageNumber") Integer pageNumber,  @Param("pageSize") Integer pageSize);
 }

+ 49 - 8
fs-service/src/main/java/com/fs/company/service/ICompanySmsService.java

@@ -6,15 +6,15 @@ import com.fs.company.vo.CompanySmsListVO;
 
 /**
  * 公司短信Service接口
- * 
+ *
  * @author fs
  * @date 2023-01-09
  */
-public interface ICompanySmsService 
+public interface ICompanySmsService
 {
     /**
      * 查询公司短信
-     * 
+     *
      * @param smsId 公司短信ID
      * @return 公司短信
      */
@@ -22,7 +22,7 @@ public interface ICompanySmsService
 
     /**
      * 查询公司短信列表
-     * 
+     *
      * @param companySms 公司短信
      * @return 公司短信集合
      */
@@ -30,7 +30,7 @@ public interface ICompanySmsService
 
     /**
      * 新增公司短信
-     * 
+     *
      * @param companySms 公司短信
      * @return 结果
      */
@@ -38,7 +38,7 @@ public interface ICompanySmsService
 
     /**
      * 修改公司短信
-     * 
+     *
      * @param companySms 公司短信
      * @return 结果
      */
@@ -46,7 +46,7 @@ public interface ICompanySmsService
 
     /**
      * 批量删除公司短信
-     * 
+     *
      * @param smsIds 需要删除的公司短信ID
      * @return 结果
      */
@@ -54,7 +54,7 @@ public interface ICompanySmsService
 
     /**
      * 删除公司短信信息
-     * 
+     *
      * @param smsId 公司短信ID
      * @return 结果
      */
@@ -68,4 +68,45 @@ public interface ICompanySmsService
     int addCompanySms(Long companyId, int number);
 
     CompanySms selectCompanySmsByCompanyIdForUpdate(Long companyId);
+
+
+    // ========== 查询方法 ==========
+    /** 普通查询(不带锁) */
+    CompanySms selectCompanySms(Long companyId);
+
+    /** 悲观锁查询(for update) */
+    CompanySms selectCompanySmsForUpdate(Long companyId);
+
+    // ========== 扣减/增加方法 ==========
+    /** 直接扣减(配合悲观锁使用) */
+    int decrementRemainSmsCount(Long companyId, int number);
+
+    /** 直接增加(配合悲观锁使用) */
+    int incrementRemainSmsCount(Long companyId, int number);
+
+    /** 乐观锁扣减(配合重试机制使用) */
+    int decrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion);
+
+    /** 乐观锁增加(配合重试机制使用) */
+    int incrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion);
+
+    // ========== 缓存方法 ==========
+    /** 获取余额(优先从缓存获取) */
+    Long getBalance(Long companyId);
+
+    /** 删除缓存(数据库变更后调用) */
+    void evictBalance(Long companyId);
+
+    /**
+     * 更新缓存中的余额(数据库更新后调用)
+     */
+    void updateCacheBalance(Long companyId, int delta);
+
+    /**
+     * 充值短信条数(带缓存更新)
+     * @param companyId 公司ID
+     * @param number 充值条数
+     * @return 是否成功
+     */
+    boolean rechargeSms(Long companyId, int number);
 }

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

@@ -311,4 +311,6 @@ public interface ICompanyUserService {
      * 获取销售绑定的fs_user
      */
     int countCompanyUserByUserId(Long userId);
+
+    List<OptionsVO> findCompanyUserOptions(String keyword, Long selectedId, Integer pageNumber,  Integer pageSize);
 }

+ 164 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanySmsServiceImpl.java

@@ -1,13 +1,17 @@
 package com.fs.company.service.impl;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
+import com.fs.common.core.redis.RedisCache;
 import com.fs.company.vo.CompanySmsListVO;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanySmsMapper;
 import com.fs.company.domain.CompanySms;
 import com.fs.company.service.ICompanySmsService;
+import org.springframework.transaction.annotation.Transactional;
 
 /**
  * 公司短信Service业务层处理
@@ -15,12 +19,23 @@ import com.fs.company.service.ICompanySmsService;
  * @author fs
  * @date 2023-01-09
  */
+@Slf4j
 @Service
 public class CompanySmsServiceImpl implements ICompanySmsService
 {
     @Autowired
     private CompanySmsMapper companySmsMapper;
 
+    @Autowired
+    private RedisCache redisCache;
+
+    // 公司短信余额缓存key前缀
+    private static final String BALANCE_KEY_PREFIX = "sms:balance:";
+    // 缓存有效期
+    private static final int CACHE_EXPIRE_SECONDS = 18000; // 300分钟
+    // 最大重试次数
+    private static final int MAX_RETRY_TIMES = 3;
+
     /**
      * 查询公司短信
      *
@@ -119,4 +134,153 @@ public class CompanySmsServiceImpl implements ICompanySmsService
     public CompanySms selectCompanySmsByCompanyIdForUpdate(Long companyId) {
         return companySmsMapper.selectCompanySmsByCompanyIdForUpdate(companyId);
     }
+
+    // ========== 查询方法 ==========
+    @Override
+    public CompanySms selectCompanySms(Long companyId) {
+        return companySmsMapper.selectCompanySmsByCompanyId(companyId);
+    }
+
+    @Override
+    public CompanySms selectCompanySmsForUpdate(Long companyId) {
+        return companySmsMapper.selectCompanySmsByCompanyIdForUpdate(companyId);
+    }
+
+    // ========== 扣减/增加方法 ==========
+    @Override
+    public int decrementRemainSmsCount(Long companyId, int number) {
+        return companySmsMapper.decrementRemainSmsCount(companyId, number);
+    }
+
+    @Override
+    public int incrementRemainSmsCount(Long companyId, int number) {
+        return companySmsMapper.incrementRemainSmsCount(companyId, number);
+    }
+
+    @Override
+    public int decrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion) {
+        return companySmsMapper.decrementRemainSmsCountWithVersion(companyId, number, currentVersion);
+    }
+
+    @Override
+    public int incrementRemainSmsCountWithVersion(Long companyId, int number, Long currentVersion) {
+        return companySmsMapper.incrementRemainSmsCountWithVersion(companyId, number, currentVersion);
+    }
+
+    // ========== 缓存方法 ==========
+    @Override
+    public Long getBalance(Long companyId) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        try {
+            Long balance = redisCache.getCacheObject(cacheKey);
+            if (balance != null) {
+                log.debug("从缓存获取余额成功, companyId={}, balance={}", companyId, balance);
+                return balance;
+            }
+            CompanySms companySms = companySmsMapper.selectCompanySmsByCompanyId(companyId);
+            if (companySms == null) {
+                return null;
+            }
+            Long realBalance = companySms.getRemainSmsCount();
+            redisCache.setCacheObject(cacheKey, realBalance, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
+            log.debug("从数据库加载余额并写入缓存, companyId={}, balance={}", companyId, realBalance);
+            return realBalance;
+        } catch (Exception e) {
+            log.error("获取余额缓存失败, companyId={}", companyId, e);
+            CompanySms companySms = companySmsMapper.selectCompanySmsByCompanyId(companyId);
+            return companySms != null ? companySms.getRemainSmsCount() : null;
+        }
+    }
+
+    @Override
+    public void updateCacheBalance(Long companyId, int delta) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        String lockKey = "sms:balance:lock:" + companyId;
+
+        try {
+            Boolean locked = redisCache.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
+
+            if (Boolean.TRUE.equals(locked)) {
+                try {
+                    Long currentBalance = redisCache.getCacheObject(cacheKey);
+                    if (currentBalance != null) {
+                        Long newBalance = currentBalance + delta;
+                        redisCache.setCacheObject(cacheKey, newBalance, CACHE_EXPIRE_SECONDS, TimeUnit.SECONDS);
+                        log.debug("更新缓存余额成功, companyId={}, old={}, new={}, delta={}",
+                                companyId, currentBalance, newBalance, delta);
+                    } else {
+                        log.debug("缓存不存在,跳过更新, companyId={}", companyId);
+                    }
+                } finally {
+                    redisCache.deleteObject(lockKey);
+                }
+            } else {
+                log.warn("获取缓存锁失败,删除缓存, companyId={}", companyId);
+                redisCache.deleteObject(cacheKey);
+            }
+        } catch (Exception e) {
+            log.error("更新缓存余额失败, companyId={}, delta={}", companyId, delta, e);
+            redisCache.deleteObject(cacheKey);
+        }
+    }
+
+    @Override
+    public void evictBalance(Long companyId) {
+        String cacheKey = BALANCE_KEY_PREFIX + companyId;
+        try {
+            redisCache.deleteObject(cacheKey);
+            log.debug("删除余额缓存成功, companyId={}", companyId);
+        } catch (Exception e) {
+            log.error("删除余额缓存失败, companyId={}", companyId, e);
+        }
+    }
+
+    // ========== 充值方法 ==========
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean rechargeSms(Long companyId, int number) {
+        if (number <= 0) {
+            log.warn("充值条数无效, companyId={}, number={}", companyId, number);
+            return false;
+        }
+
+        int maxRetries = 3;
+
+        for (int retryCount = 0; retryCount < maxRetries; retryCount++) {
+            // 获取最新数据(含version)
+            CompanySms latestSms = selectCompanySms(companyId);
+            if (latestSms == null) {
+                log.error("充值失败,公司短信配置不存在, companyId={}", companyId);
+                return false;
+            }
+
+            Long version = latestSms.getVersion() != null ? latestSms.getVersion() : 0L;
+            int updateCount = companySmsMapper.incrementRemainSmsCountWithVersion(companyId, number, version);
+
+            if (updateCount > 0) {
+                // 充值成功,更新缓存
+                updateCacheBalance(companyId, number);
+                log.info("充值成功, companyId={}, number={}, newBalance={}",
+                        companyId, number, latestSms.getRemainSmsCount() + number);
+                return true;
+            }
+
+            log.warn("乐观锁充值失败,第{}次重试, companyId={}", retryCount + 1, companyId);
+
+            if (retryCount == maxRetries - 1) {
+                log.error("充值失败,重试次数用尽, companyId={}, number={}", companyId, number);
+                return false;
+            }
+
+            try {
+                Thread.sleep(50L * (retryCount + 1));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("充值被中断, companyId={}", companyId);
+                return false;
+            }
+        }
+
+        return false;
+    }
 }

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

@@ -1227,4 +1227,20 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         return 0;
     }
 
+    @Override
+    public List<OptionsVO> findCompanyUserOptions(String keyword, Long selectedId, Integer pageNumber, Integer pageSize) {
+        //计算起始位置
+        pageNumber = (pageNumber - 1) * pageSize;
+        return Optional.ofNullable(companyUserMapper.findCompanyUserOptions(keyword, selectedId, pageNumber, pageSize))
+                .orElse(new ArrayList<>())
+                .stream()
+                .map(cu -> {
+                    OptionsVO vo = new OptionsVO();
+                    vo.setDictLabel(cu.getNickName());
+                    vo.setDictValue(cu.getUserId());
+                    return vo;
+                })
+                .collect(Collectors.toList());
+    }
+
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java


+ 3 - 0
fs-service/src/main/java/com/fs/config/ai/AiHostProper.java

@@ -20,6 +20,9 @@ public class AiHostProper {
     @Value("${ipad.commonApi}")
     private String commonApi;
 
+    @Value("${ipad.aiApiV2:http://1.95.196.10:3000/api}")
+    private String aiApiV2;
+
 
 
 }

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

@@ -28,6 +28,7 @@ public class CourseConfig implements Serializable {
     private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
     private String smsDomainName;//短信推送域名
+    private String smsAcquisitionName;//短信推送域名
     private String smsDomain;//短信推送域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名

+ 1 - 1
fs-service/src/main/java/com/fs/course/domain/FsCourseAnswerReward.java

@@ -24,7 +24,7 @@ public class FsCourseAnswerReward {
     private List<RewardProduct> products;
 
     /**
-     * 本次获得的芳华币数量
+     * 本次获得的积分数量
      */
     private Integer availableCoins;
 

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

@@ -109,4 +109,12 @@ public class FsCoursePlaySourceConfig {
      * 商户支付配置id
      */
     private Long merchantConfigId;
+    /**
+    * 客服电话
+    */
+    private Long customerNum;
+    /**
+    * 备案号
+    */
+    private String recordNumber;
 }

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

@@ -61,4 +61,7 @@ public class FsUserCourseComment extends BaseEntity
     /** 视频ID */
     private Long videoId;
 
+    /** 是否全部人可见:0-自己可见,1-全部人可见 */
+    private Integer visibleAll;
+
 }

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

@@ -142,6 +142,11 @@ public class FsUserCourseVideo extends BaseEntity
     /** 课程介绍图片URL */
     private String courseIntroImg;
 
+    /**
+     * 课程优惠券ID
+     */
+    private Long courseCouponId;
+
     @TableField(exist = false)
     private Integer showProduct; //1不展示疗法,0展示疗法
 

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

@@ -60,8 +60,8 @@ public class LuckyBagCollectRecord extends BaseEntity{
     @Excel(name = "公司名称")
     private String companyName;
 
-    /** 芳华币数量 */
-    @Excel(name = "芳华币数量")
+    /** 积分数量 */
+    @Excel(name = "积分数量")
     private BigDecimal coinAmount;
 
     /** 发放时间 */

+ 15 - 0
fs-service/src/main/java/com/fs/course/dto/VideoUpdateDTO.java

@@ -0,0 +1,15 @@
+package com.fs.course.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@AllArgsConstructor
+public class VideoUpdateDTO implements Serializable {
+    private Long id;
+    private long playCount;
+    private long play3sCount;
+    private long completeCount;
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов