Prechádzať zdrojové kódy

app登录,bug修复

wangxy 3 týždňov pred
rodič
commit
8f8a503974
26 zmenil súbory, kde vykonal 1035 pridanie a 33 odobranie
  1. 13 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java
  2. 9 0
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  3. 1 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  4. 13 0
      fs-admin/src/main/java/com/fs/web/controller/tool/TestController.java
  5. 12 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  6. 36 3
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  7. 10 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseStudyLogMapper.java
  8. 2 0
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java
  9. 15 3
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  10. 267 3
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  11. 23 0
      fs-service/src/main/java/com/fs/his/dto/AppUserCompanyDTO.java
  12. 50 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  13. 6 0
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  14. 111 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  15. 75 0
      fs-service/src/main/java/com/fs/his/vo/AppCourseReportVO.java
  16. 12 0
      fs-service/src/main/java/com/fs/his/vo/AppUserCountVO.java
  17. 146 0
      fs-service/src/main/java/com/fs/his/vo/AppWatchLogReportVO.java
  18. 4 0
      fs-service/src/main/java/com/fs/store/vo/h5/FsUserPageListVO.java
  19. 2 1
      fs-service/src/main/resources/mapper/MerchantAppConfigMapper.xml
  20. 1 1
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  21. 95 2
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  22. 17 0
      fs-service/src/main/resources/mapper/course/FsUserCourseStudyLogMapper.xml
  23. 2 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  24. 58 16
      fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java
  25. 53 1
      fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java
  26. 2 3
      fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

+ 13 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java

@@ -841,6 +841,19 @@ public class CompanyStatisticsController extends BaseController {
         return getDataTable(fsCourseReportVOS);
     }
 
+
+    /**
+     * app看课统计报表
+      * @param param
+     * @return
+     */
+    @GetMapping("/appCourseReport")
+    public TableDataInfo selectFsAppCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        startPage();
+        List<AppCourseReportVO> list = fsCourseWatchLogService.selectAppCourseReportVO(param);
+        return getDataTable(list);
+    }
+
     @GetMapping("/exportFsCourseReportVO")
     public AjaxResult exportFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
         List<FsCourseReportVO> list = fsCourseWatchLogService.selectFsCourseReportVO(param);

+ 9 - 0
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -513,4 +513,13 @@ public class FsUserController extends BaseController
         return fsCourseWatchLogService.clearUserWatchLog(param.getUserId(), param.getProjectId());
     }
 
+    /**
+     * 查询 APP 用户统计数据
+     */
+    @GetMapping("/appUserCount")
+    public AjaxResult getAppUserCount()
+    {
+        return AjaxResult.success(fsUserService.getAppUserCount());
+    }
+
 }

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

@@ -1749,6 +1749,7 @@ public class Task {
         // 等待所有任务完成
         waitForAllTasksToComplete(orderFutures);
         waitForAllTasksToComplete(packageOrderFutures);
+        waitForAllTasksToComplete(integralOrderFutures);
     }
 
     /**

+ 13 - 0
fs-admin/src/main/java/com/fs/web/controller/tool/TestController.java

@@ -6,6 +6,7 @@ import java.util.*;
 import com.fs.common.core.domain.R;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.service.IFsUserService;
 import com.fs.his.task.Task;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.ResponseEntity;
@@ -48,6 +49,9 @@ public class TestController extends BaseController {
     @Autowired
     FsStoreOrderMapper fsStoreOrderMapper;
 
+    @Autowired
+    IFsUserService  iFsUserService;
+
     @Autowired
     private Task task;
 
@@ -119,6 +123,15 @@ public class TestController extends BaseController {
         return AjaxResult.success(users.put(user.getUserId(), user));
     }
 
+    /**
+     * 合并用户
+     */
+    @ApiOperation("合并用户")
+    @PostMapping("/mergeUser")
+    public void mergeUser() {
+        iFsUserService.mergeUser();
+    }
+
     @ApiOperation("删除用户信息")
     @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path")
     @DeleteMapping("/{userId}")

+ 12 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -11,6 +11,7 @@ import com.fs.company.param.CompanyParam;
 import com.fs.company.vo.CompanyCrmVO;
 import com.fs.company.vo.CompanyNameVO;
 import com.fs.company.vo.CompanyVO;
+import com.fs.his.vo.AppCourseReportVO;
 import com.fs.his.vo.OptionsVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
@@ -264,4 +265,15 @@ public interface CompanyMapper
             " `status` != 0   " +
             " and is_del != 1 ")
     List<CompanyVO> getCompanyDropList();
+
+    @Select({"<script> " +
+            "SELECT company_id as companyId, company_name as companyName " +
+            "FROM company " +
+            "WHERE status = 1 " +
+            "<if test=\"companyId != null\"> " +
+            "  AND company_id = #{companyId} " +
+            "</if> " +
+            "GROUP BY company_id "+
+            "</script>"})
+    List<AppCourseReportVO> selectAllCompanies(@Param("companyId") Long companyId);
 }

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

@@ -7,9 +7,7 @@ import com.fs.course.dto.WatchLogDTO;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
 import com.fs.his.dto.UserConditionDTO;
-import com.fs.his.vo.FsCourseReportVO;
-import com.fs.his.vo.FsUserReportVO;
-import com.fs.his.vo.WatchLogReportVO;
+import com.fs.his.vo.*;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.sop.vo.QwRatingVO;
@@ -658,8 +656,29 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
     List<FsCourseReportVO> selectWatchStatistics(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     * app看课统计信息
+     * @param param
+     * @return
+     */
+    List<AppCourseReportVO> selectAppWatchStatistics(FsCourseWatchLogStatisticsListParam param);
+
     List<FsCourseReportVO> selectAnswerStatistics(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     * app 答题统计信息
+     * @param param
+     * @return
+     */
+    List<AppCourseReportVO> selectAppAnswerStatistics(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * app红包统计信息
+     * @param param
+     * @return
+     */
+    List<AppCourseReportVO> selectAppRedPacketStatistics(FsCourseWatchLogStatisticsListParam param);
+
     List<FsCourseReportVO> selectRedPacketStatistics(FsCourseWatchLogStatisticsListParam param);
 
     List<FsCourseReportVO> selectCampPeriodInfo(@Param("periodIds") List<Long> periodIds);
@@ -722,6 +741,13 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
     List<WatchLogReportVO> selectUserBaseData(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     *app用户维度看课报表
+     *
+     */
+    List<AppWatchLogReportVO>  selectAppUserBaseData(FsCourseWatchLogStatisticsListParam param);
+
+
     /**
      * 销售端看课报表 销售维度基础数据
      * @param param
@@ -832,4 +858,11 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      */
      @Select("select * from fs_course_watch_log where user_id = #{userId} and project = #{projectId} and create_time >= #{startTime}")
      FsCourseWatchLog selectByUserIdAndProjectId(@Param("userId") Long userId, @Param("projectId") Long projectId, @Param("startTime") Date startTime);
+
+    /**
+     * 查询有看课记录的活跃用户 ID 列表
+     * @param userIds 用户 ID 列表
+     * @return 活跃用户 ID 列表
+     */
+    List<Long> selectActiveUserIds(@Param("userIds") List<Long> userIds);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseStudyLogMapper.java

@@ -2,6 +2,7 @@ package com.fs.course.mapper;
 
 import java.util.List;
 import com.fs.course.domain.FsUserCourseStudyLog;
+import com.fs.his.vo.AppWatchLogReportVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
@@ -76,4 +77,13 @@ public interface FsUserCourseStudyLogMapper
 
     @Select("select count(0) from fs_user_course_study_log where user_id = #{userId} and course_id = #{courseId} and status = 1 ")
     Long selectStudyLogFinishCount(@Param("courseId") Long courseId,@Param("userId") Long userId);
+
+    /**
+     * 批量查询用户学习时长
+     * @param userIds 用户ID列表
+     * @param sTime 开始时间
+     * @param eTime 结束时间
+     * @return 学习时长统计列表
+     */
+    List<AppWatchLogReportVO> selectStudyDurationByUserIds(@Param("userIds") List<Long> userIds, @Param("sTime") String sTime, @Param("eTime") String eTime);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogStatisticsListParam.java

@@ -101,6 +101,8 @@ public class FsCourseWatchLogStatisticsListParam extends BaseEntity {
      */
     private  Integer watchType;
 
+    private  List<Long> logIds;
+
 
 
 }

+ 15 - 3
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -5,9 +5,7 @@ import com.fs.common.core.domain.R;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
-import com.fs.his.vo.FsCourseReportVO;
-import com.fs.his.vo.FsUserReportVO;
-import com.fs.his.vo.WatchLogReportVO;
+import com.fs.his.vo.*;
 import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 
@@ -151,6 +149,13 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      */
     List<FsCourseReportVO> selectFsCourseReportVO(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     * app端看课统计报表
+     * @param param
+     * @return
+     */
+    List<AppCourseReportVO> selectAppCourseReportVO(FsCourseWatchLogStatisticsListParam param);
+
     /**
      * 会员积分统计报表
      * @param param
@@ -165,6 +170,13 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      */
     List<WatchLogReportVO> selectWatchLogReportVO(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     * app端销售端用户看课统计报表
+     * @param param
+     * @return
+     */
+    List<AppWatchLogReportVO> selectUserAppWatchLogReportVO(FsCourseWatchLogStatisticsListParam param);
+
     /**
      * 根据看课记录id获取所有的外部联系人ids
      * @param watchLogIds

+ 267 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -19,6 +19,7 @@ import com.fs.company.cache.ICompanyCacheService;
 import com.fs.company.cache.ICompanyUserCacheService;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyMapper;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.constant.CourseConstant;
 import com.fs.course.domain.*;
@@ -31,13 +32,13 @@ import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
 import com.fs.course.vo.*;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
+import com.fs.his.dto.AppUserCompanyDTO;
 import com.fs.his.dto.UserConditionDTO;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.utils.PhoneUtil;
-import com.fs.his.vo.FsCourseReportVO;
-import com.fs.his.vo.FsUserReportVO;
-import com.fs.his.vo.WatchLogReportVO;
+import com.fs.his.vo.*;
 import com.fs.qw.Bean.MsgBean;
 import com.fs.qw.cache.IQwExternalContactCacheService;
 import com.fs.qw.cache.IQwUserCacheService;
@@ -155,6 +156,15 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private SysDictDataMapper dictDataMapper;
 
+    @Autowired
+    private FsUserMapper userMapper;
+
+    @Autowired
+    private CompanyMapper companyMapper;
+
+    @Autowired
+    private FsUserCourseStudyLogMapper fsUserCourseStudyLogMapper;
+
     /**
      * 查询短链课程看课记录
      *
@@ -1346,6 +1356,117 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redPacketStatistics, param);
     }
 
+    @Override
+    public List<AppCourseReportVO> selectAppCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        if(param.getSTime()!=null && param.getETime()!=null){
+            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+            param.setStartDate(simpleDateFormat.format(param.getSTime()));
+            param.setEndDate(simpleDateFormat.format(param.getETime()));
+        }
+        // 1. 先查询所有公司列表
+        List<AppCourseReportVO> allCompanies = companyMapper.selectAllCompanies(param.getCompanyId());
+
+        if (CollectionUtils.isEmpty(allCompanies)) {
+            return Collections.emptyList();
+        }
+
+        // 2. 查询指定公司和时间范围内的 APP 会员(包含公司信息)
+        List<AppUserCompanyDTO> appUserList = userMapper.selectAppUserListForActiveCount(param);
+
+        // 3. 提取所有唯一的 userId
+        Set<Long> allUserIds = appUserList != null ?
+                appUserList.stream().map(AppUserCompanyDTO::getUserId).collect(Collectors.toSet()) :
+                new HashSet<>();
+
+        // 4. 查询有看课记录的活跃用户 ID(只查一次,避免 N+1 问题)
+        List<Long> activeUserIds = allUserIds.isEmpty() ?
+                new ArrayList<>() :
+                fsCourseWatchLogMapper.selectActiveUserIds(new ArrayList<>(allUserIds));
+        Set<Long> activeUserSet = new HashSet<>(activeUserIds);
+
+        // 5. 按公司分组统计 APP 会员数据
+        Map<Long, int[]> companyStatsMap = new HashMap<>();
+
+        if (appUserList != null) {
+            for (AppUserCompanyDTO dto : appUserList) {
+                Long cid = dto.getCompanyId();
+
+                if (!companyStatsMap.containsKey(cid)) {
+                    companyStatsMap.put(cid, new int[]{0, 0}); // [总数,活跃数]
+                }
+
+                int[] stats = companyStatsMap.get(cid);
+
+                // 统计 APP 会员总数(每个 user_id + company_user_id 算一个)
+                stats[0]++;
+
+                // 如果是活跃用户,活跃数 +1
+                if (activeUserSet.contains(dto.getUserId())) {
+                    stats[1]++;
+                }
+            }
+        }
+
+        // 6. 组装结果:遍历所有公司,填充 APP 会员统计数据
+        for (AppCourseReportVO vo : allCompanies) {
+            int[] stats = companyStatsMap.get(vo.getCompanyId());
+            if (stats != null) {
+                vo.setAppUserCount(stats[0]);
+                vo.setActiveAppUserCount(stats[1]);
+            } else {
+                vo.setAppUserCount(0);
+                vo.setActiveAppUserCount(0);
+            }
+        }
+        //获取公域课 统计信息
+        List<Long> companyIds = allCompanies.stream()
+                .map(company -> (Long) company.getCompanyId())
+                .collect(Collectors.toList());
+        if (CollectionUtils.isNotEmpty(companyIds)) {
+            param.setCompanyIds(companyIds);
+            List<AppCourseReportVO> appCourseReportVOList = fsCourseWatchLogMapper.selectAppWatchStatistics(param);
+            Map<String, AppCourseReportVO> answerStatsMap=new HashMap<>();
+            Map<String, AppCourseReportVO> redPacketStatsMap=new HashMap<>();
+            //统计答题数据
+
+            List<AppCourseReportVO> answerList = fsCourseWatchLogMapper.selectAppAnswerStatistics(param);
+            //根据公司id 分组
+            answerStatsMap= answerList.stream()
+                    .collect(Collectors.toMap(
+                            stats -> String.valueOf(stats.getCompanyId()),
+                            Function.identity(),
+                            (existing, replacement) -> existing // 当出现重复键时,保留第一个值
+                    ));
+            //统计红包数据
+            List<AppCourseReportVO> redpackList = fsCourseWatchLogMapper.selectAppRedPacketStatistics(param);
+            redPacketStatsMap= redpackList.stream()
+                    .collect(Collectors.toMap(
+                            stats -> String.valueOf(stats.getCompanyId()),
+                            Function.identity(),
+                            (existing, replacement) -> existing // 当出现重复键时,保留第一个值
+                    ));
+            for (AppCourseReportVO vo : allCompanies) {
+                Long companyId = vo.getCompanyId();
+                AppCourseReportVO watchStats = appCourseReportVOList.stream()
+                        .filter(w -> w.getCompanyId().equals(companyId))
+                        .findFirst()
+                        .orElse(null);
+                if (watchStats != null) {
+                    vo.setPendingCount(watchStats.getPendingCount());
+                    vo.setWatchingCount(watchStats.getWatchingCount());
+                    vo.setFinishedCount(watchStats.getFinishedCount());
+                    vo.setWatchRate(calculateWatchingRate(watchStats.getWatchingCount(),watchStats.getPendingCount()));
+                }
+                AppCourseReportVO anserStats = answerStatsMap.getOrDefault(companyId.toString(), new AppCourseReportVO());
+                vo.setAnswerUserCount(anserStats.getAnswerUserCount());
+                AppCourseReportVO redPacketStats = redPacketStatsMap.getOrDefault(companyId.toString(), new AppCourseReportVO());
+                vo.setPacketUserCount(redPacketStats.getPacketUserCount());
+                vo.setPacketAmount(redPacketStats.getPacketAmount());
+            }
+        }
+        return allCompanies;
+    }
+
     private List<FsCourseReportVO> assembleStatisticsResult(List<FsCourseReportVO> companyList, Map<String, FsCourseReportVO> watchStatistics, Map<String, FsCourseReportVO> answerStatistics, Map<String, FsCourseReportVO> redPacketStatistics, FsCourseWatchLogStatisticsListParam param) {
         for (FsCourseReportVO company : companyList) {
             FsCourseReportVO watchStats;
@@ -1399,6 +1520,36 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 .setScale(2, RoundingMode.HALF_UP);
     }
 
+    /**
+     * app 看课率(私域看课中人次/私域课待看课人次)
+     */
+
+
+// ... existing code ...
+    /**
+     * app 看课率(私域看课中人次/私域课待看课人次)
+     * @param watchingCount 私域看课中人次
+     * @param pendingCount 私域课待看课人次
+     * @return 看课率(百分比,保留 2 位小数)
+     */
+    private BigDecimal calculateWatchingRate(Integer watchingCount, Integer pendingCount) {
+        // 防止除以 0
+        if (pendingCount == null || pendingCount == 0) {
+            return BigDecimal.ZERO;
+        }
+
+        // 防止空指针
+        if (watchingCount == null || watchingCount == 0) {
+            return BigDecimal.ZERO;
+        }
+
+        // 看课率 = 看课中人次 / 待看课人次 * 100%
+        return BigDecimal.valueOf(watchingCount)
+                .divide(BigDecimal.valueOf(pendingCount), 4, RoundingMode.HALF_UP)
+                .multiply(BigDecimal.valueOf(100))
+                .setScale(2, RoundingMode.HALF_UP);
+    }
+// ... existing code ...
     /**
      * 计算完成率
      */
@@ -1568,6 +1719,119 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return assembleStatisticsData(baseData, param);
     }
 
+    @Override
+    public List<AppWatchLogReportVO> selectUserAppWatchLogReportVO(FsCourseWatchLogStatisticsListParam param) {
+        if (StringUtils.isNotEmpty(param.getUserPhone())) {
+            //加密手机号
+            param.setUserPhone(PhoneUtil.encryptPhone(param.getUserPhone()));
+        }
+        // 时间转字符串
+        if (param.getSTime() != null && param.getETime() != null) {
+            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+            param.setStartDate(simpleDateFormat.format(param.getSTime()));
+            param.setEndDate(simpleDateFormat.format(param.getETime()));
+        }
+        // 获取基础数据
+        List<AppWatchLogReportVO> baseData = fsCourseWatchLogMapper.selectAppUserBaseData(param);
+        if (CollectionUtils.isEmpty(baseData)) {
+            return Collections.emptyList();
+        }
+        // 获取统计数据和组装结果
+        return assembleAppStatisticsData(baseData, param);
+    }
+
+    /**
+     * 组装APP统计数据
+     */
+    private List<AppWatchLogReportVO> assembleAppStatisticsData(List<AppWatchLogReportVO> baseData, FsCourseWatchLogStatisticsListParam param) {
+        // 准备查询条件
+        List<Long> periods = baseData.stream().map(AppWatchLogReportVO::getPeriodId).collect(Collectors.toList());
+        List<Long> logIds = baseData.stream().map(AppWatchLogReportVO::getLogId).collect(Collectors.toList());
+        List<Long> userIds = baseData.stream().map(AppWatchLogReportVO::getUserId).collect(Collectors.toList());
+
+        // 批量查询统计数据
+        // 营期数据
+        Map<Long, WatchLogReportVO> perMap = convertCampPeriodToMap(fsCourseWatchLogMapper.selectCampPeriodByPeriod(periods));
+
+        // 红包数据
+        Map<Long, WatchLogReportVO> redPacketMap = convertRedPacketToMap(
+                fsCourseWatchLogMapper.selectRedPacketStats(logIds)
+        );
+
+        // 订单数据
+        Map<Long, WatchLogReportVO> orderMap = convertOrderToMap(
+                fsCourseWatchLogMapper.selectOrderStats(userIds, param)
+        );
+
+        // 答题数据
+        Map<Long, WatchLogReportVO> answerMap = convertAnswerToMap(
+                fsCourseWatchLogMapper.selectAnswerStats(logIds)
+        );
+
+        // 学习时长数据(来自fs_user_course_study_log表)- 使用字符串时间
+        Map<String, AppWatchLogReportVO> studyDurationMap = fsUserCourseStudyLogMapper.selectStudyDurationByUserIds(userIds, param.getStartDate(), param.getEndDate())
+                .stream()
+                .collect(Collectors.toMap(
+                        item -> item.getUserId() + "_" + item.getVideoId(),
+                        Function.identity()
+                ));
+
+        // 组装数据
+        for (AppWatchLogReportVO item : baseData) {
+            // 营期数据
+            WatchLogReportVO watchStats = perMap.getOrDefault(item.getPeriodId(), null);
+            if (watchStats != null) {
+                item.setPeriodName(watchStats.getPeriodName());
+                item.setTrainingCampName(watchStats.getTrainingCampName());
+            }
+
+            // 红包数据
+            WatchLogReportVO redPacketStats = redPacketMap.getOrDefault(item.getLogId(), null);
+            if (redPacketStats != null) {
+                item.setRedPacketAmount(redPacketStats.getRedPacketAmount());
+            }
+
+            // 订单数据
+            WatchLogReportVO order = orderMap.getOrDefault(item.getUserId(), null);
+            if (order != null) {
+                item.setHistoryOrderCount(order.getHistoryOrderCount());
+            }
+
+            // 答题数据
+            WatchLogReportVO answer = answerMap.getOrDefault(item.getLogId(), null);
+            if (answer != null) {
+                item.setAnswerStatus(answer.getAnswerStatus());
+            }
+
+            // 学习时长数据
+            AppWatchLogReportVO studyDuration = studyDurationMap.get(item.getUserId() + "_" + item.getVideoId());
+            if (studyDuration != null && studyDuration.getPublicCourseDuration() != null) {
+                // 将秒转换为时分秒格式
+                item.setPublicCourseDuration(formatDuration(Long.valueOf(studyDuration.getPublicCourseDuration())));
+            }
+        }
+        return baseData;
+    }
+
+    /**
+     * 将秒数转换为时分秒格式
+     */
+    private String formatDuration(Long totalSeconds) {
+        if (totalSeconds == null || totalSeconds <= 0) {
+            return "0秒";
+        }
+        long hours = totalSeconds / 3600;
+        long minutes = (totalSeconds % 3600) / 60;
+        long seconds = totalSeconds % 60;
+        if (hours > 0) {
+            return String.format("%d小时%d分%d秒", hours, minutes, seconds);
+        } else if (minutes > 0) {
+            return String.format("%d分%d秒", minutes, seconds);
+        } else {
+            return String.format("%d秒", seconds);
+        }
+    }
+
 
     /**
      * 根据维度获取基础数据

+ 23 - 0
fs-service/src/main/java/com/fs/his/dto/AppUserCompanyDTO.java

@@ -0,0 +1,23 @@
+package com.fs.his.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class AppUserCompanyDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 公司 ID */
+    private Long companyId;
+
+    /** 公司名称 */
+    private String companyName;
+
+    /** 用户 ID */
+    private Long userId;
+
+    /** 销售 ID */
+    private Long companyUserId;
+}

+ 50 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -7,9 +7,11 @@ import java.util.Map;
 import com.fs.course.domain.FsUserWatchCourseStatistics;
 import com.fs.course.domain.FsUserWatchStatistics;
 import com.fs.course.param.CourseAnalysisParam;
+import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.vo.FsPeriodCountVO;
 import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.his.domain.FsUser;
+import com.fs.his.dto.AppUserCompanyDTO;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsUserParam;
@@ -82,6 +84,27 @@ public interface FsUserMapper
      */
     public int deleteFsUserByUserId(Long userId);
 
+    @Select("SELECT * \n" +
+            "FROM fs_user\n" +
+            "WHERE phone IS NOT NULL AND phone != ''\n" +
+            "GROUP BY phone \n" +
+            "HAVING COUNT(*) > 1")
+    List<FsUser> findDuplicatePhonesWithCount();
+
+    /**
+     * 查询 APP 总用户数(source 不为空的用户)
+     * @return APP 总用户数
+     */
+    @Select("SELECT COUNT(user_id) FROM fs_user WHERE source IS NOT NULL AND is_del = 0")
+    Long selectAppUserCount();
+
+    @Select("SELECT COUNT(user_id) \n" +
+            "FROM fs_user \n" +
+            "WHERE source IS NOT NULL \n" +
+            "  AND is_del = 0 \n" +
+            "  AND DATE(create_time) = CURDATE()")
+    Long selectAppNewUser();
+
     /**
      * 批量删除用户
      *
@@ -507,4 +530,31 @@ public interface FsUserMapper
     List<FsUserPageListExportVO> FsUserPageListExportVO(FsUserPageListExportParam param);
 
     void updatePasswordByPhone(@Param("password")String password, @Param("encryptPhone")String encryptPhone);
+
+    /**
+     * 查询公司维度的 APP 会员列表(按 user_id + company_user_id 去重)
+     * 用于后续计算活跃人数
+     * @return APP 会员列表
+     */
+    @Select({"<script> " +
+            "SELECT DISTINCT " +
+            "    c.company_id, " +
+            "    c.company_name, " +
+            "    ucu.user_id, " +
+            "    ucu.company_user_id " +
+            "FROM fs_user_company_user ucu " +
+            "INNER JOIN fs_user u ON ucu.user_id = u.user_id " +
+            "INNER JOIN company c ON ucu.company_id = c.company_id " +
+            "WHERE u.source IS NOT NULL " +
+            "  AND u.source != '' " +
+            "  AND u.status = 1 " +
+            "  AND ucu.status IN (0, 1) " +
+            "<if test=\"param.companyId != null\"> " +
+            "  AND c.company_id = #{param.companyId} " +
+            "</if> " +
+            "<if test=\"param.startDate != null and param.startDate != '' and param.endDate != null and param.endDate != ''\"> " +
+            "  AND DATE(u.create_time) &gt;= #{param.startDate} AND DATE(u.create_time) &lt;= #{param.endDate} " +
+            "</if> " +
+            "</script>"})
+    List<AppUserCompanyDTO> selectAppUserListForActiveCount(@Param("param")FsCourseWatchLogStatisticsListParam param);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -271,4 +271,10 @@ public interface IFsUserService
     R updatePasswordByPhone(String password, String encryptPhone);
 
     int realDeleteFsUserByUserId(Long userId);
+
+    //合并用户
+    void mergeUser();
+
+    //首页app用户统计
+    AppUserCountVO getAppUserCount();
 }

+ 111 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -1942,4 +1942,115 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.deleteFsUserByUserId(userId);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void mergeUser() {
+        List<FsUser> phonesWithCount = fsUserMapper.findDuplicatePhonesWithCount();
+        for (FsUser phoneWithCount : phonesWithCount) {
+            List<FsUser> usersWithSamePhone = fsUserMapper.selectFsUsersByPhoneLimitOne(phoneWithCount.getPhone());
+            if (usersWithSamePhone.size() > 1) {
+                this.mergeUser(usersWithSamePhone);
+            }
+        }
+    }
+
+    @Override
+    public AppUserCountVO getAppUserCount() {
+        AppUserCountVO result = new AppUserCountVO();
+        // 查询 APP 总用户数(source 不为空的用户)
+        Long appTotalUser = fsUserMapper.selectAppUserCount();
+        result.setAppTotalUser(appTotalUser != null ? appTotalUser : 0L);
+        //app新注册用户(当天的app用户)
+        Long appNewUser = fsUserMapper.selectAppNewUser();
+        result.setAppNewUser(appNewUser != null ? appNewUser : 0L);
+        return result;
+    }
+
+
+    /**
+     * 合并用户
+     * 1.合并逻辑是需要保留其中有union_id的用户 如果都有union_id 则去根据fs_course_watch_log 是否有看课记录去判断 有看课记录的就是需要保留的 如果都有看课记录则保留
+     * 2.积分总和合并
+     * 3.要是有union_id的用户 没有source字段 就要给他合并上去
+     * 4.合并后删除被合并的
+     */
+    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
+    public  void  mergeUser(List<FsUser> userList){
+        if (userList == null || userList.isEmpty()) {
+            return;
+        }
+
+        // 按是否有union_id分组
+        FsUser userWithUnionId = null;  // 有union_id的用户(保留的用户)
+        List<FsUser> usersWithoutUnionId = new ArrayList<>();  // 没有union_id的用户(将被合并的用户)
+
+        for (FsUser user : userList) {
+            if (user.getUnionId() != null && !user.getUnionId().isEmpty()) {
+                userWithUnionId = user;
+            } else {
+                usersWithoutUnionId.add(user);
+            }
+        }
+
+        // 如果没有有union_id的用户,则选择第一个用户作为保留用户
+        if (userWithUnionId == null && !userList.isEmpty()) {
+            userWithUnionId = userList.get(0);
+            if (userList.size() > 1) {
+                usersWithoutUnionId = userList.subList(1, userList.size());
+            }
+        }
+
+        // 如果没有需要合并的用户,直接返回
+        if (usersWithoutUnionId.isEmpty()) {
+            logger.info("没有需要合并的用户");
+            return;
+        }
+
+        try {
+            // 计算积分总和
+            Long totalIntegral = userWithUnionId.getIntegral() != null ? userWithUnionId.getIntegral() : 0L;
+            String sourceToMerge = userWithUnionId.getSource();
+
+            // 累加需要合并用户的积分和其他需要合并的字段
+            for (FsUser user : usersWithoutUnionId) {
+                // 累加积分
+                if (user.getIntegral() != null) {
+                    totalIntegral += user.getIntegral();
+                }
+
+                // 如果保留用户没有source,而被合并用户有source,则使用被合并用户的source
+                if ((sourceToMerge == null || sourceToMerge.isEmpty()) &&
+                        user.getSource() != null && !user.getSource().isEmpty()) {
+                    sourceToMerge = user.getSource();
+                }
+            }
+
+            // 更新保留用户的积分和source
+            FsUser updateUser = new FsUser();
+            updateUser.setUserId(userWithUnionId.getUserId());
+            updateUser.setIntegral(totalIntegral);
+
+            // 只有当原用户没有source且找到有效的source时才更新
+            if ((userWithUnionId.getSource() == null || userWithUnionId.getSource().isEmpty())
+                    && sourceToMerge != null && !sourceToMerge.isEmpty()) {
+                updateUser.setSource(sourceToMerge);
+            }
+
+            // 执行更新
+            updateFsUser(updateUser);
+
+            // 删除被合并的用户
+            for (FsUser user : usersWithoutUnionId) {
+                fsUserMapper.deleteFsUserByUserId(user.getUserId());
+            }
+
+            logger.info("用户合并完成:保留用户ID={}, 合并用户数={}, 总积分={}",
+                    userWithUnionId.getUserId(), usersWithoutUnionId.size(), totalIntegral);
+
+        } catch (Exception e) {
+            logger.error("用户合并失败:{}", e.getMessage(), e);
+            throw new RuntimeException("用户合并过程中发生错误:" + e.getMessage());
+        }
+    }
+
 }

+ 75 - 0
fs-service/src/main/java/com/fs/his/vo/AppCourseReportVO.java

@@ -0,0 +1,75 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AppCourseReportVO {
+    /** 公司id */
+    private  Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "销售公司")
+    private String companyName;
+
+    /**
+     * app新增注册人数
+     */
+    @Excel(name = "新增注册人数")
+    private  Integer appUserCount;
+
+    /** 活跃 APP 会员数 */
+    @Excel(name = "APP活跃人数")
+    private Integer activeAppUserCount;
+
+
+    /**
+     * 待看课人数
+     */
+    @Excel(name = "私域课待看课人次")
+    private  Integer pendingCount;
+
+    /**
+     * 看课中人数
+     */
+    @Excel(name = "私域课看课中人次")
+    private  Integer watchingCount;
+
+    /**
+     * 完课人数
+     */
+    @Excel(name = "私域课完课人次")
+    private  Integer finishedCount;
+
+
+    /**
+     * 看课率
+     */
+    @Excel(name = "看课率")
+    private BigDecimal watchRate;
+
+    /**
+     * 答题人数
+     */
+    @Excel(name = "答题人次")
+    private  Integer answerUserCount;
+
+    /**
+     * 红包领取数
+     */
+    @Excel(name = "红包领取人次")
+    private  Integer packetUserCount;
+
+    /**
+     * 红包金额
+     */
+    @Excel(name = "红包金额")
+    private  BigDecimal packetAmount;
+
+    /**
+     * 日志id
+     */
+    private  Long logId;
+}

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

@@ -0,0 +1,12 @@
+package com.fs.his.vo;
+
+import lombok.Data;
+
+@Data
+public class AppUserCountVO {
+
+    private Long appTotalUser;
+
+    private  Long appNewUser;
+
+}

+ 146 - 0
fs-service/src/main/java/com/fs/his/vo/AppWatchLogReportVO.java

@@ -0,0 +1,146 @@
+package com.fs.his.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class AppWatchLogReportVO {
+
+    @Excel(name = "会员id")
+    private Long userId;
+    /**
+     * 昵称
+     */
+    @Excel(name = "会员昵称")
+    private String nickName;
+
+
+    /**
+     * app会员数
+     */
+    @Excel(name = "app会员数")
+    private  Integer AppUserCount;
+
+    /**
+     * 新注册app会员数
+     */
+    @Excel(name = "新注册app会员数")
+    private  Integer AppNewUser;
+
+    /**
+     * 登录渠道
+     */
+    @Excel(name = "登录渠道")
+    private  String loginChannel;
+
+    /**
+     * 销售数
+     */
+    @Excel(name = "销售数")
+    private  Integer salesCount;
+
+
+    /**
+     * 所属销售数
+     */
+    @Excel(name = "所属销售")
+    private  String salesName;
+
+    /**
+     * 所属销售部门
+     */
+    @Excel(name = "销售部门")
+    private  String salesDept;
+
+    /**
+     * 所属销售公司
+     */
+    @Excel(name = "所属销售公司")
+    private  String salesCompany;
+
+    /**
+     * 培训营名称
+     */
+    @Excel(name = "训练营")
+    private String trainingCampName;
+
+    /**
+     * 营期
+     */
+    @Excel(name = "营期")
+    private  String periodName;
+
+    /**
+     * 视频名称
+     */
+    @Excel(name = "小节名称")
+    private  String videoTitle;
+
+
+    /**
+     * 公开课播放时长
+     */
+    @Excel(name = "公开课播放时长")
+    private  String publicCourseDuration;
+
+    /**
+     * 私欲看课状态
+     */
+    @Excel(name = "私域课看课状态")
+    private  String privateWatchStatus;
+
+    /**
+     * 私欲课播放时长
+      */
+    @Excel(name = "私域课播放时长")
+    private  String privateWatchDuration;
+
+    /**
+     * 观看完成时间
+     */
+    @Excel(name = "完课时间",dateFormat = "yyyy-MM-dd")
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date finishTime;
+
+    /**
+     * 回答状态
+     */
+    @Excel(name = "答题状态")
+    private  String answerStatus;
+
+    /**
+     * 领取红包金额
+     */
+    @Excel(name = "红包金额")
+    private BigDecimal redPacketAmount;
+
+    /**
+     * 历史订单数
+     */
+    @Excel(name = "历史疗法订单数")
+    private  Integer historyOrderCount;
+
+
+    /**
+     * 营期id
+     */
+    private  Long periodId;
+
+    /**
+     * 视频id
+     */
+    private  Long videoId;
+
+    /**
+     * 观看记录id
+     */
+    private  Long logId;
+
+    private  Long deptId;
+
+
+}

+ 4 - 0
fs-service/src/main/java/com/fs/store/vo/h5/FsUserPageListVO.java

@@ -109,4 +109,8 @@ public class FsUserPageListVO {
     // 项目会员 主键
     private Long id;
 
+    private String loginDevice;//当前登录设备
+
+    private String source;//app来源
+
 }

+ 2 - 1
fs-service/src/main/resources/mapper/MerchantAppConfigMapper.xml

@@ -56,9 +56,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <where>
             <if test="merchantId != null and merchantId != ''"> and merchant_id = #{merchantId}</if>
             <if test="merchantType != null  and merchantType != ''"> and merchant_type = #{merchantType}</if>
-            <if test="appId != null  and appId != ''"> and app_id = #{appId}</if>
+            <if test="appId != null  and appId != ''"> and  FIND_IN_SET(#{appId},app_id)  </if>
             <if test="params.beginCreatedTime != null and params.beginCreatedTime != '' and params.endCreatedTime != null and params.endCreatedTime != ''"> and created_time between #{params.beginCreatedTime} and #{params.endCreatedTime}</if>
             <if test="isDeleted != null "> and is_deleted = #{isDeleted}</if>
+             <if test="id != null"> and id = #{id}</if>
         </where>
     </select>
 

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

@@ -147,9 +147,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="deptId != null and deptId != 0">
             AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{deptId}, ancestors) ))
         </if>
-        GROUP BY u.user_id
         <!-- 数据范围过滤 -->
         ${params.dataScope}
+        GROUP BY u.user_id
     </select>
 
     <!-- 角色子查询 -->

+ 95 - 2
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1994,6 +1994,99 @@ FROM
         </where>
         ORDER BY period.create_time DESC
     </select>
+    <select id="selectActiveUserIds" resultType="java.lang.Long">
+        SELECT DISTINCT user_id
+        FROM fs_user_course_study_log
+        WHERE user_id IN
+        <foreach item="userId" collection="userIds" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </select>
+    <select id="selectAppWatchStatistics" resultType="com.fs.his.vo.AppCourseReportVO">
+        SELECT
+            log_id logId,
+            company_id AS companyId,
+                COUNT(CASE WHEN log_type = 3 THEN log_id END) AS pendingCount,
+                COUNT(CASE WHEN log_type = 1 THEN log_id END) AS watchingCount,
+                COUNT(CASE WHEN log_type = 2 THEN log_id END) AS finishedCount
+        FROM fs_course_watch_log
+        <where>
+            watch_type =1 and send_type = 1
+            <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                AND create_time &gt;= #{startDate} AND create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+            </if>
+            <if test="companyIds != null and companyIds.size() > 0">
+                AND company_id IN
+                <foreach collection="companyIds" item="companyId" open="(" separator="," close=")">
+                    #{companyId}
+                </foreach>
+            </if>
+        </where>
+        GROUP BY company_id
+    </select>
+    <select id="selectAppAnswerStatistics" resultType="com.fs.his.vo.AppCourseReportVO">
+        SELECT
+            l.company_id AS companyId,
+            COUNT( l.log_id) AS answerUserCount
+        FROM fs_course_answer_logs l
+        LEFT JOIN fs_course_watch_log a on l.watch_log_id=a.log_id
+        WHERE l.company_id IN
+        <foreach collection="companyIds" item="companyId" open="(" separator="," close=")">
+            #{companyId}
+        </foreach>
+        and a.watch_type=1 and a.send_type=1
+        <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+            AND l.create_time &gt;= #{startDate} AND l.create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+        </if>
+        GROUP BY l.company_id
+    </select>
+    <select id="selectAppRedPacketStatistics" resultType="com.fs.his.vo.AppCourseReportVO">
+        SELECT
+        rpl.company_id AS companyId,
+        COUNT( rpl.log_id) AS packetUserCount,
+        COALESCE(SUM(rpl.amount), 0) AS packetAmount
+        FROM fs_course_red_packet_log rpl
+        LEFT JOIN fs_course_watch_log a on rpl.watch_log_id=a.log_id
+        WHERE rpl.company_id IN
+        <foreach collection="companyIds" item="companyId" open="(" separator="," close=")">
+            #{companyId}
+        </foreach>
+        and a.watch_type=1 and a.send_type=1
+        <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+            AND rpl.create_time &gt;= #{startDate} AND rpl.create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+        </if>
+        GROUP BY rpl.company_id
+    </select>
+    <select id="selectAppUserBaseData" resultType="com.fs.his.vo.AppWatchLogReportVO">
+        SELECT
+        log.user_id userId,
+        u.nick_name AS nickName,
+        u.source loginChannel,
+        cu.nick_name AS salesName,
+        c.company_name AS salesCompany,
+        cd.dept_name AS salesDept,
+        log.period_id periodId,
+        log.video_id videoId,
+        log.log_id logId,
+        log.create_time courseTime,
+        log.finish_time finishTime,
+        log.duration privateWatchDuration,
+        log.log_type privateWatchStatus,
+        cv.title AS videoTitle
+        FROM
+        fs_course_watch_log log
+        LEFT JOIN fs_user u ON u.user_id = log.user_id
+        LEFT JOIN fs_user_company_user cuu ON cuu.user_id = u.user_id
+        LEFT JOIN company_user cu ON cuu.company_user_id = cu.user_id
+        LEFT JOIN company c ON log.company_id = c.company_id
+        LEFT JOIN company_dept cd ON cu.dept_id = cd.dept_id
+        LEFT JOIN fs_user_course_video cv ON log.video_id = cv.video_id
+        WHERE log.send_type = 1
+        AND log.watch_type = 1
+        <include refid="commonConditions"/>
+        group by log.user_id
+        ORDER BY u.register_date DESC
+    </select>
     <sql id="commonConditions">
         <!-- 销售公司 -->
         <if test="companyId != null and companyId != ''">
@@ -2015,8 +2108,8 @@ FROM
             AND cuu.project_id = #{project}
         </if>
         <!-- 时间范围 -->
-        <if test="sTime != null and eTime != null">
-            AND DATE(log.create_time)  BETWEEN #{sTime} AND #{eTime}
+        <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+            AND log.create_time &gt;= #{startDate} AND log.create_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
         </if>
         <!-- 训练营 -->
         <if test="trainingCampId != null and trainingCampId != ''">

+ 17 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseStudyLogMapper.xml

@@ -109,4 +109,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{logId}
         </foreach>
     </delete>
+
+    <!-- 批量查询用户学习时长 -->
+    <select id="selectStudyDurationByUserIds" resultType="com.fs.his.vo.AppWatchLogReportVO">
+        SELECT
+            user_id AS userId,
+            video_id AS videoId,
+            CAST(SUM(total_times) AS SIGNED) AS publicCourseDuration
+        FROM fs_user_course_study_log
+        WHERE user_id IN
+        <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+        <if test="sTime != null and eTime != null">
+            AND create_time &gt;= #{sTime} AND create_time &lt; DATE_ADD(#{eTime}, INTERVAL 1 DAY)
+        </if>
+        GROUP BY user_id, video_id
+    </select>
 </mapper>

+ 2 - 0
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -370,6 +370,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         fs_user.nick_name as nickname,
         fs_user.avatar,
         fs_user.phone,
+        fs_user.source,
+        fs_user.login_device,
         ucu.update_time createTime,
         fs_user.remark,
         ucu.id,

+ 58 - 16
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController.java

@@ -231,7 +231,7 @@ public class AppLoginController extends AppBaseController{
         if (user==null){
             return R.error("用户数据不存在");
         }
-        //验证码绑定,需要校验验证码
+//        //验证码绑定,需要校验验证码
         if (param.getBindType()!=null){
             String redisCode = redisCache.getCacheObject("sms:code:" + param.getPhone());
             if (StringUtils.isEmpty(redisCode)){
@@ -257,15 +257,27 @@ public class AppLoginController extends AppBaseController{
             if (StringUtils.isNotEmpty(userMap.getUnionId())&&!userMap.getUnionId().equals(user.getUnionId())){
                 return R.error("该手机号已绑定其他微信");
             }
-            //如果存在手机号也有用户,微信也有用户,保留创建时间比较久的用户
+            //合并规则修改 保留手机号存在的用户 合并掉unionid存在但手机号不存在的用户
             FsUser keepUser;
             FsUser deleteUser;
-            if (userMap.getCreateTime().before(user.getCreateTime())){
-                keepUser = userMap;
-                deleteUser = user;
-            }else {
+            // 判断哪个用户有手机号,优先保留有手机号的用户
+            if (StringUtils.isNotEmpty(user.getPhone()) && StringUtils.isEmpty(userMap.getPhone())) {
+                // 当前用户有手机号,保留当前用户
                 keepUser = user;
                 deleteUser = userMap;
+            } else if (StringUtils.isNotEmpty(userMap.getPhone()) && StringUtils.isEmpty(user.getPhone())) {
+                // userMap 用户有手机号,保留 userMap 用户
+                keepUser = userMap;
+                deleteUser = user;
+            } else {
+                // 如果两个用户都有或都没有手机号,则按创建时间判断,保留较早创建的用户
+                if (userMap.getCreateTime().before(user.getCreateTime())) {
+                    keepUser = userMap;
+                    deleteUser = user;
+                } else {
+                    keepUser = user;
+                    deleteUser = userMap;
+                }
             }
             keepUser.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
             keepUser.setSource(param.getSource());
@@ -330,19 +342,47 @@ public class AppLoginController extends AppBaseController{
                         userService.updateFsUser(user);
                         return generateTokenAndReturn(user);
                     }
+                    //改一下合并规则 将unionid存在的用户保留 合并掉手机号存在但unionid不存在的用户,而且如果被合并的用户如果有source 就转移过去用
+                    // 合并用户逻辑:优先保留有 union_id 的用户
                     FsUser keepUser;
                     FsUser deleteUser;
-                    if (user.getCreateTime().before(userByUnionId.getCreateTime())){
+
+                    // 判断哪个用户有 union_id,优先保留有 union_id 的用户
+                    if (StringUtils.isNotEmpty(user.getUnionId()) && StringUtils.isEmpty(userByUnionId.getUnionId())) {
+                        // 当前用户有 union_id,保留当前用户
                         keepUser = user;
                         deleteUser = userByUnionId;
-                    } else {
+                    } else if (StringUtils.isNotEmpty(userByUnionId.getUnionId()) && StringUtils.isEmpty(user.getUnionId())) {
+                        // union_id 用户有 union_id,保留 union_id 用户
                         keepUser = userByUnionId;
                         deleteUser = user;
+                    } else {
+                        // 如果两个用户都有或都没有 union_id,则按创建时间判断,保留较早创建的用户
+                        if (user.getCreateTime().before(userByUnionId.getCreateTime())) {
+                            keepUser = user;
+                            deleteUser = userByUnionId;
+                        } else {
+                            keepUser = userByUnionId;
+                            deleteUser = user;
+                        }
                     }
+
+                    // 更新保留用户的信息
                     keepUser.setUnionId(unionid);
                     keepUser.setPhone(param.getPhone());
-                    keepUser.setSource(param.getSource() != null ? param.getSource() : null );
-                    keepUser.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
+                    // 如果保留用户没有 source,而被删除用户有 source,则转移 source
+                    if (StringUtils.isEmpty(keepUser.getSource()) && StringUtils.isNotEmpty(deleteUser.getSource())) {
+                        keepUser.setSource(deleteUser.getSource());
+                    } else if (param.getSource() != null) {
+                        keepUser.setSource(param.getSource());
+                    }
+                    // 如果保留用户没有 loginDevice,而被删除用户有 loginDevice,则转移 loginDevice
+                    if (StringUtils.isEmpty(keepUser.getLoginDevice()) && StringUtils.isNotEmpty(deleteUser.getLoginDevice())) {
+                        keepUser.setLoginDevice(deleteUser.getLoginDevice());
+                    } else if (param.getLoginDevice() != null) {
+                        keepUser.setLoginDevice(param.getLoginDevice());
+                    }
+
                     keepUser.setNickName(nickname);
                     keepUser.setAvatar(avatar);
                     keepUser.setSex(sex);
@@ -408,18 +448,15 @@ public class AppLoginController extends AppBaseController{
         if (StringUtils.isEmpty(param.getPhone()) || StringUtils.isEmpty(param.getPassword())) {
             return R.error("账号或密码不能为空");
         }
-
         FsUser user = findUserByPhone(param.getPhone());
-
         // 校验用户是否存在及账号状态
         if (user == null) {
-            return R.error("账号不存在,请先注册账号");
+            return R.error("该手机账户不存在");
         } else if (user.getStatus() == 0) {
             return R.error("账号已停用");
-        } else if (StringUtils.isEmpty(user.getPassword())) {
-            return R.error("账号不存在,请先注册账号");
+        } else if (StringUtils.isEmpty(user.getUnionId())) {
+            return R.ok().put("isNew",true).put("phone",encryptPhone(param.getPhone()));
         }
-
         if (StringUtils.isNotEmpty(param.getJpushId())) {
             updateExistingUserJpushId(user, param.getJpushId());
         }
@@ -641,6 +678,11 @@ public class AppLoginController extends AppBaseController{
             //如果出现了一个手机号多个用户的情况,找出登陆过app的那个用户
             user.removeIf(fsUser -> StringUtils.isEmpty(fsUser.getHistoryApp()));
         }
+        // 检查用户是否存在 unionId,如果不存在就去绑定微信
+        FsUser currentUser = user.get(0);
+        if (StringUtils.isEmpty(currentUser.getUnionId())){
+            return R.ok().put("isNew",true).put("phone",encryptPhone(phone));
+        }
         String redisCode = redisCache.getCacheObject("sms:code:" + phone);
         if (StringUtils.isEmpty(redisCode)){
             return R.error("验证码已过期,请重新发送");

+ 53 - 1
fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -1,6 +1,7 @@
 package com.fs.app.controller;
 
 
+import cn.hutool.core.collection.CollectionUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.app.annotation.Login;
@@ -28,6 +29,7 @@ import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.*;
 import com.fs.his.service.IFsPrescribeService;
 import com.fs.his.service.IFsUserService;
@@ -53,9 +55,12 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
 import java.io.*;
+import java.util.Base64;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -89,6 +94,9 @@ public class CompanyUserController extends AppBaseController {
     @Autowired
     private IFsUserService fsUserService;
 
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
 
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
 
@@ -144,7 +152,15 @@ public class CompanyUserController extends AppBaseController {
         FsUser user = fsUserService.selectFsUserByUserId(Long.parseLong(getUserId()));
         //设置用户手机号
         if(user != null && StringUtils.isNotEmpty(param.getPhone())){
-            //加密手机号
+            List<FsUser> usersByPhone = findUsersByPhone(param.getPhone());
+            if(CollectionUtil.isNotEmpty(usersByPhone)){
+                //和绑定的用户id比较 如果相同给出限制
+                for (FsUser fsUser : usersByPhone){
+                    if(fsUser.getUserId().equals(user.getUserId())){
+                        return R.error("您已注册康享银龄APP,如需修改手机号,请联系您的专属客服");
+                    }
+                }
+            }
             user.setPhone(param.getPhone());
             fsUserService.updateFsUser(user);
         }
@@ -159,6 +175,42 @@ public class CompanyUserController extends AppBaseController {
         return R.ok();
     }
 
+    private List<FsUser> findUsersByPhone(String phone) {
+        // 先根据加密手机号查询用户
+        String jiami = (encryptPhone(phone));
+        List<FsUser> fsUsers = fsUserMapper.selectFsUsersByPhoneLimitOne(jiami);
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = fsUserMapper.selectFsUsersByPhoneLimitOne(encryptPhoneOldKey(phone));
+        }
+        // 如果没有找到用户,再根据手机号查询
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = fsUserMapper.selectFsUsersByPhoneLimitOne(phone);
+
+        }
+        return fsUsers;
+    }
+
+    /**
+     * 用于查询 使用老的数据加密
+     * @param text
+     * @return
+     */
+    private static String OLD_KEY = "2c8d1a7f4e9b3c6ae6d5c4b3a291f8c9";
+    public static String encryptPhoneOldKey(String text) {
+        String encryptedText=null;
+        try {
+            SecretKeySpec secretKey = new SecretKeySpec(OLD_KEY.getBytes(), "AES");
+            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+            // Encryption
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+            byte[] encryptedBytes = cipher.doFinal(text.getBytes());
+            encryptedText = Base64.getEncoder().encodeToString(encryptedBytes);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return encryptedText;
+    }
+
 
     @Login
     @ApiOperation("上传声纹")

+ 2 - 3
fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

@@ -12,7 +12,6 @@ import java.io.Serializable;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class FsUserEditParam implements Serializable {
-    @NotNull(message = "用户昵称不能为空!")
     @JsonAlias("nickname")
     private String nickname;
 
@@ -25,8 +24,8 @@ public class FsUserEditParam implements Serializable {
     private Long userId;
     private Integer isWeixinAuth;
 
-
-    public @NotNull(message = "用户昵称不能为空!") String getNickname() {
+//    @NotNull(message = "用户昵称不能为空!")
+    public String getNickname() {
         return nickname;
     }