ソースを参照

app登录,bug修复,app统计

wangxy 3 週間 前
コミット
7e93566644

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

@@ -854,6 +854,18 @@ public class CompanyStatisticsController extends BaseController {
         return getDataTable(list);
     }
 
+    /**
+     * 导出app看课统计报表
+     * @param param
+     * @return
+     */
+        @GetMapping("/exportAppCourseReport")
+    public AjaxResult exportAppCourseReport(FsCourseWatchLogStatisticsListParam param) {
+        List<AppCourseReportVO> list = fsCourseWatchLogService.selectAppCourseReportVO(param);
+        ExcelUtil<AppCourseReportVO> util = new ExcelUtil<AppCourseReportVO>(AppCourseReportVO.class);
+        return util.exportExcel(list, "APP看课统计报表");
+    }
+
     @GetMapping("/exportFsCourseReportVO")
     public AjaxResult exportFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
         List<FsCourseReportVO> list = fsCourseWatchLogService.selectFsCourseReportVO(param);

+ 58 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java

@@ -18,6 +18,7 @@ import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseUserStatisticsListVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
+import com.fs.his.vo.AppSalesWatchLogReportVO;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
@@ -30,6 +31,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -376,6 +378,62 @@ public class FsCourseWatchLogController extends BaseController
         ExcelUtil<FsCourseWatchLogListVO> util = new ExcelUtil<FsCourseWatchLogListVO>(FsCourseWatchLogListVO.class);
         return util.exportExcel(list, "短链课程看课记录数据");
     }
+
+    /**
+     * APP端销售维度看课统计报表
+     * 注意:必须放在 /{logId} 之前,避免路径冲突
+     */
+//    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:appSalesReport')")
+    @GetMapping("/appSalesWatchLogReport")
+    public TableDataInfo appSalesWatchLogReport(FsCourseWatchLogStatisticsListParam param)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesWatchLogReportVO> list = fsCourseWatchLogService.selectAppSalesWatchLogReportVO(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出APP端销售维度看课统计报表
+     */
+    @Log(title = "APP销售维度看课统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/appSalesWatchLogReportExport")
+    public AjaxResult appSalesWatchLogReportExport(FsCourseWatchLogStatisticsListParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppSalesWatchLogReportVO> list = fsCourseWatchLogService.selectAppSalesWatchLogReportVO(param);
+        // 根据维度获取需要导出的字段
+        List<String> selectedFields = getAppSalesWatchLogReportFields(param.getDimension());
+        ExcelUtil<AppSalesWatchLogReportVO> util = new ExcelUtil<AppSalesWatchLogReportVO>(AppSalesWatchLogReportVO.class);
+        return util.exportExcelSelectedColumns(list, "APP销售维度看课统计报表", selectedFields);
+    }
+
+    /**
+     * 获取AppSalesWatchLogReportVO需要导出的字段
+     * @param dimension 维度:sales-销售维度, dept-销售部门维度
+     */
+    private List<String> getAppSalesWatchLogReportFields(String dimension) {
+        // 销售维度字段
+        List<String> salesFields = Arrays.asList(
+                "salesName", "appUserCount", "newAppUserCount", "salesDept",
+                "salesCompany", "trainingCampName", "periodName", "videoTitle",
+                "finishedCount", "unfinishedCount", "completionRate",
+                "notWatchedCount", "notAnsweredCount", "redPacketAmount", "historyOrderCount"
+        );
+
+        // 销售部门维度字段(去掉销售列,销售数放在销售部门后面)
+        List<String> deptFields = Arrays.asList(
+                "salesDept", "salesCount", "appUserCount", "newAppUserCount",
+                "salesCompany", "trainingCampName", "periodName", "videoTitle",
+                "finishedCount", "unfinishedCount", "completionRate",
+                "notWatchedCount", "notAnsweredCount", "redPacketAmount", "historyOrderCount"
+        );
+
+        return "dept".equals(dimension) ? deptFields : salesFields;
+    }
+
     /**
      * 获取短链课程看课记录详细信息
      */

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

@@ -16,6 +16,7 @@ import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.his.vo.AppWatchLogReportVO;
 import com.fs.his.vo.FsUserReportVO;
 import com.fs.his.vo.WatchLogReportVO;
 import com.fs.hisStore.param.FsStoreStatisticsParam;
@@ -219,6 +220,69 @@ public class FsStoreStatisticsScrmController extends BaseController {
         return getDataTable(courseWatchLogService.selectWatchLogReportVO(param));
     }
 
+    /**
+     * 销售后台app看课统计 会员维度
+     * @param param
+     * @return
+     */
+    @GetMapping("/appWatchLogReportSales")
+    public TableDataInfo aPPWatchLogReportSales(FsCourseWatchLogStatisticsListParam param) {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        return getDataTable(courseWatchLogService.selectUserAppWatchLogReportVO(param));
+    }
+
+    /**
+     * 销售后台app看课统计 会员维度导出
+     * @param param
+     * @return
+     */
+    @GetMapping("/appWatchLogReportSalesExport")
+    public AjaxResult appWatchLogReportSalesExport(FsCourseWatchLogStatisticsListParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<AppWatchLogReportVO> list = courseWatchLogService.selectUserAppWatchLogReportVO(param);
+        // 转换登录渠道和答题状态
+        list.forEach(this::convertAppWatchLogReportVO);
+        // 获取所有字段,排除会员维度的三个字段
+        List<String> selectedFields = getAppWatchLogReportFields();
+        ExcelUtil<AppWatchLogReportVO> util = new ExcelUtil<AppWatchLogReportVO>(AppWatchLogReportVO.class);
+        return util.exportExcelSelectedColumns(list, "APP看课统计报表", selectedFields);
+    }
+
+    /**
+     * 转换AppWatchLogReportVO字段
+     * - 登录渠道:有值显示"app",无值显示"小程序"
+     * - 答题状态:无值显示"未答题"
+     */
+    private void convertAppWatchLogReportVO(AppWatchLogReportVO vo) {
+        // 登录渠道转换
+        if (StringUtils.isNotEmpty(vo.getLoginChannel())) {
+            vo.setLoginChannel("app");
+        } else {
+            vo.setLoginChannel("小程序");
+        }
+        // 答题状态转换
+        if (StringUtils.isEmpty(vo.getAnswerStatus())) {
+            vo.setAnswerStatus("未答题");
+        }
+    }
+
+    /**
+     * 获取AppWatchLogReportVO需要导出的字段(排除特定字段)
+     */
+    private List<String> getAppWatchLogReportFields() {
+        // 需要排除的字段:app会员数、销售数、新注册app会员数
+        List<String> excludeFields = Arrays.asList("AppUserCount", "salesCount", "AppNewUser");
+        
+        return Arrays.stream(AppWatchLogReportVO.class.getDeclaredFields())
+                .filter(field -> field.isAnnotationPresent(Excel.class))
+                .map(Field::getName)
+                .filter(fieldName -> !excludeFields.contains(fieldName))
+                .collect(Collectors.toList());
+    }
+
     @GetMapping("/watchLogReportExport")
     public AjaxResult watchLogReportExport(FsCourseWatchLogStatisticsListParam param) {
         startPage();
@@ -315,7 +379,8 @@ public class FsStoreStatisticsScrmController extends BaseController {
     private boolean isEmpty(Object value) {
         if (value == null) return true;
         if (value instanceof String) return ((String) value).trim().isEmpty();
-        if (value instanceof Number) return ((Number) value).doubleValue() == 0;
+        // 数字类型不为空(包括0),确保历史订单数等字段能正常导出
+        if (value instanceof Number) return false;
         return false;
     }
 

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

@@ -273,7 +273,8 @@ public interface CompanyMapper
             "<if test=\"companyId != null\"> " +
             "  AND company_id = #{companyId} " +
             "</if> " +
-            "GROUP BY company_id "+
+            "GROUP BY company_id " +
+            "ORDER BY company_id DESC " +
             "</script>"})
     List<AppCourseReportVO> selectAllCompanies(@Param("companyId") Long companyId);
 }

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

@@ -865,4 +865,39 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * @return 活跃用户 ID 列表
      */
     List<Long> selectActiveUserIds(@Param("userIds") List<Long> userIds);
+
+    /**
+     * 销售维度APP会员数统计
+     */
+    List<AppSalesWatchLogReportVO> selectAppSalesUserStats(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 销售维度基础数据+看课统计(合并查询)
+     */
+    List<AppSalesWatchLogReportVO> selectAppSalesWatchStats(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 销售维度订单统计
+     */
+    List<AppSalesWatchLogReportVO> selectAppSalesOrderStats(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 销售维度营期信息
+     */
+    List<AppSalesWatchLogReportVO> selectAppSalesCampPeriod(@Param("periodIds") List<Long> periodIds);
+
+    /**
+     * 销售部门维度APP会员数统计
+     */
+    List<AppSalesWatchLogReportVO> selectAppDeptUserStats(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 销售部门维度基础数据+看课统计(合并查询)
+     */
+    List<AppSalesWatchLogReportVO> selectAppDeptWatchStats(FsCourseWatchLogStatisticsListParam param);
+
+    /**
+     * 销售部门维度订单统计
+     */
+    List<AppSalesWatchLogReportVO> selectAppDeptOrderStats(FsCourseWatchLogStatisticsListParam param);
 }

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

@@ -177,6 +177,13 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      */
     List<AppWatchLogReportVO> selectUserAppWatchLogReportVO(FsCourseWatchLogStatisticsListParam param);
 
+    /**
+     * app端销售维度看课统计报表
+     * @param param
+     * @return
+     */
+    List<AppSalesWatchLogReportVO> selectAppSalesWatchLogReportVO(FsCourseWatchLogStatisticsListParam param);
+
     /**
      * 根据看课记录id获取所有的外部联系人ids
      * @param watchLogIds

+ 272 - 90
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -1358,112 +1358,97 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
 
     @Override
     public List<AppCourseReportVO> selectAppCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
-        if(param.getSTime()!=null && param.getETime()!=null){
+        // 时间转字符串
+        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());
 
+        // 1. 查询所有公司列表
+        List<AppCourseReportVO> allCompanies = companyMapper.selectAllCompanies(param.getCompanyId());
         if (CollectionUtils.isEmpty(allCompanies)) {
             return Collections.emptyList();
         }
 
-        // 2. 查询指定公司和时间范围内的 APP 会员(包含公司信息)
+        // 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) {
+        
+        // 3. 如果有APP会员数据,则统计活跃用户数
+        if (!CollectionUtils.isEmpty(appUserList)) {
+            // 提取所有唯一的 userId 并查询活跃用户
+            Set<Long> allUserIds = appUserList.stream()
+                    .map(AppUserCompanyDTO::getUserId)
+                    .collect(Collectors.toSet());
+
+            Set<Long> activeUserSet = new HashSet<>(
+                    fsCourseWatchLogMapper.selectActiveUserIds(new ArrayList<>(allUserIds))
+            );
+
+            // 按公司分组统计 APP 会员数据 [总数, 活跃数]
+            Map<Long, int[]> companyStatsMap = new HashMap<>();
             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 算一个)
+                int[] stats = companyStatsMap.computeIfAbsent(cid, k -> new int[]{0, 0});
                 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) {
+            // 组装 APP 会员统计数据
+            for (AppCourseReportVO vo : allCompanies) {
+                int[] stats = companyStatsMap.getOrDefault(vo.getCompanyId(), new int[]{0, 0});
                 vo.setAppUserCount(stats[0]);
                 vo.setActiveAppUserCount(stats[1]);
-            } else {
-                vo.setAppUserCount(0);
-                vo.setActiveAppUserCount(0);
             }
         }
-        //获取公域课 统计信息
+
+        // 6. 批量查询看课、答题、红包统计数据
         List<Long> companyIds = allCompanies.stream()
-                .map(company -> (Long) company.getCompanyId())
+                .map(AppCourseReportVO::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());
+        param.setCompanyIds(companyIds);
+
+        // 并行查询三个统计数据源
+        List<AppCourseReportVO> watchStatsList = fsCourseWatchLogMapper.selectAppWatchStatistics(param);
+        List<AppCourseReportVO> answerList = fsCourseWatchLogMapper.selectAppAnswerStatistics(param);
+        List<AppCourseReportVO> redpackList = fsCourseWatchLogMapper.selectAppRedPacketStatistics(param);
+
+        // 7. 转换为 Map 便于查找
+        Map<Long, AppCourseReportVO> watchStatsMap = watchStatsList.stream()
+                .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity()));
+        Map<Long, AppCourseReportVO> answerStatsMap = answerList.stream()
+                .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity(), (e, r) -> e));
+        Map<Long, AppCourseReportVO> redPacketStatsMap = redpackList.stream()
+                .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity(), (e, r) -> e));
+
+        // 8. 组装最终数据
+        for (AppCourseReportVO vo : allCompanies) {
+            Long companyId = vo.getCompanyId();
+
+            // 看课统计
+            AppCourseReportVO watchStats = watchStatsMap.get(companyId);
+            if (watchStats != null) {
+                vo.setPendingCount(watchStats.getPendingCount());
+                vo.setWatchingCount(watchStats.getWatchingCount());
+                vo.setFinishedCount(watchStats.getFinishedCount());
+                vo.setWatchRate(calculateWatchingRate(
+                        watchStats.getWatchingCount(),
+                        watchStats.getFinishedCount(),
+                        watchStats.getAccessCount()));
             }
+
+            // 答题统计
+            AppCourseReportVO answerStats = answerStatsMap.getOrDefault(companyId, new AppCourseReportVO());
+            vo.setAnswerUserCount(answerStats.getAnswerUserCount());
+
+            // 红包统计
+            AppCourseReportVO redPacketStats = redPacketStatsMap.getOrDefault(companyId, new AppCourseReportVO());
+            vo.setPacketUserCount(redPacketStats.getPacketUserCount());
+            vo.setPacketAmount(redPacketStats.getPacketAmount());
         }
+
         return allCompanies;
     }
 
@@ -1527,25 +1512,25 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
 
 // ... existing code ...
     /**
-     * app 看课率(私域看课中人次/私域课待看课人次)
+     * app 看课率((看课中人次+完课人次)/ 私域课总人次)
      * @param watchingCount 私域看课中人次
-     * @param pendingCount 私域课待看课人次
+     * @param finishedCount 私域完课人次
+     * @param totalCount 私域课总人次
      * @return 看课率(百分比,保留 2 位小数)
      */
-    private BigDecimal calculateWatchingRate(Integer watchingCount, Integer pendingCount) {
+    private BigDecimal calculateWatchingRate(Integer watchingCount, Integer finishedCount, Integer totalCount) {
         // 防止除以 0
-        if (pendingCount == null || pendingCount == 0) {
+        if (totalCount == null || totalCount == 0) {
             return BigDecimal.ZERO;
         }
 
         // 防止空指针
-        if (watchingCount == null || watchingCount == 0) {
-            return BigDecimal.ZERO;
-        }
+        int watching = watchingCount != null ? watchingCount : 0;
+        int finished = finishedCount != null ? finishedCount : 0;
 
-        // 看课率 = 看课中人次 / 待看课人次 * 100%
-        return BigDecimal.valueOf(watchingCount)
-                .divide(BigDecimal.valueOf(pendingCount), 4, RoundingMode.HALF_UP)
+        // 看课率 = (看课中人次 + 完课人次) / 总人次 * 100%
+        return BigDecimal.valueOf(watching + finished)
+                .divide(BigDecimal.valueOf(totalCount), 4, RoundingMode.HALF_UP)
                 .multiply(BigDecimal.valueOf(100))
                 .setScale(2, RoundingMode.HALF_UP);
     }
@@ -1833,6 +1818,203 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     }
 
 
+    @Override
+    public List<AppSalesWatchLogReportVO> selectAppSalesWatchLogReportVO(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()));
+        }
+
+        // 根据维度选择查询方式
+        String dimension = param.getDimension();
+        if ("dept".equals(dimension)) {
+            return selectAppDeptWatchLogReportVO(param);
+        } else {
+            return selectAppSalesWatchLogReportVOBySales(param);
+        }
+    }
+
+    /**
+     * 销售维度APP看课统计报表
+     */
+    private List<AppSalesWatchLogReportVO> selectAppSalesWatchLogReportVOBySales(FsCourseWatchLogStatisticsListParam param) {
+        // 1. 批量查询统计数据
+        // APP会员数统计(直接查fs_user表,按销售ID分组)
+        List<AppSalesWatchLogReportVO> userStatsList = fsCourseWatchLogMapper.selectAppSalesUserStats(param);
+        // 基础数据+看课统计+答题统计+红包统计(合并查询,按销售ID+营期ID+视频ID分组)
+        List<AppSalesWatchLogReportVO> watchStatsList = fsCourseWatchLogMapper.selectAppSalesWatchStats(param);
+        if (CollectionUtils.isEmpty(watchStatsList)) {
+            return Collections.emptyList();
+        }
+        // 订单统计
+        List<AppSalesWatchLogReportVO> orderStatsList = fsCourseWatchLogMapper.selectAppSalesOrderStats(param);
+
+        // 2. 查询营期信息
+        List<Long> periodIds = watchStatsList.stream()
+                .map(AppSalesWatchLogReportVO::getPeriodId)
+                .distinct()
+                .collect(Collectors.toList());
+        List<AppSalesWatchLogReportVO> campPeriodList = fsCourseWatchLogMapper.selectAppSalesCampPeriod(periodIds);
+
+        // 3. 转换为Map便于查找
+        // APP会员数统计按销售ID分组
+        Map<Long, AppSalesWatchLogReportVO> userStatsMap = userStatsList.stream()
+                .collect(Collectors.toMap(
+                        AppSalesWatchLogReportVO::getSalesId,
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        // 看课统计(已包含基础数据、答题、红包)按销售ID+营期ID+视频ID分组
+        Map<String, AppSalesWatchLogReportVO> watchStatsMap = watchStatsList.stream()
+                .collect(Collectors.toMap(
+                        item -> item.getSalesId() + "_" + item.getPeriodId() + "_" + item.getVideoId(),
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        // 订单统计按销售ID+营期ID+视频ID分组
+        Map<String, AppSalesWatchLogReportVO> orderStatsMap = orderStatsList.stream()
+                .collect(Collectors.toMap(
+                        item -> item.getSalesId() + "_" + item.getPeriodId() + "_" + item.getVideoId(),
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        Map<Long, AppSalesWatchLogReportVO> campPeriodMap = campPeriodList.stream()
+                .collect(Collectors.toMap(
+                        AppSalesWatchLogReportVO::getPeriodId,
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+
+        // 4. 组装数据
+        for (AppSalesWatchLogReportVO vo : watchStatsList) {
+            String key = vo.getSalesId() + "_" + vo.getPeriodId() + "_" + vo.getVideoId();
+
+            // APP会员数统计(按销售ID)
+            AppSalesWatchLogReportVO userStats = userStatsMap.get(vo.getSalesId());
+            if (userStats != null) {
+                vo.setAppUserCount(userStats.getAppUserCount());
+                vo.setNewAppUserCount(userStats.getNewAppUserCount());
+            }
+
+            // 计算完课率 = 完课数 / (完课数 + 未完课数 + 未看数) * 100%
+            int total = vo.getFinishedCount() + vo.getUnfinishedCount() + vo.getNotWatchedCount();
+            if (total > 0) {
+                vo.setCompletionRate(BigDecimal.valueOf(vo.getFinishedCount())
+                        .multiply(BigDecimal.valueOf(100))
+                        .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setCompletionRate(BigDecimal.ZERO);
+            }
+
+            // 订单统计
+            AppSalesWatchLogReportVO orderStats = orderStatsMap.get(key);
+            if (orderStats != null) {
+                vo.setHistoryOrderCount(orderStats.getHistoryOrderCount());
+            }
+
+            // 营期信息
+            AppSalesWatchLogReportVO campPeriod = campPeriodMap.get(vo.getPeriodId());
+            if (campPeriod != null) {
+                vo.setPeriodName(campPeriod.getPeriodName());
+                vo.setTrainingCampName(campPeriod.getTrainingCampName());
+            }
+        }
+
+        return watchStatsList;
+    }
+
+    /**
+     * 销售部门维度APP看课统计报表
+     */
+    private List<AppSalesWatchLogReportVO> selectAppDeptWatchLogReportVO(FsCourseWatchLogStatisticsListParam param) {
+        // 1. 批量查询统计数据
+        // APP会员数统计(直接查fs_user表,按部门ID分组)
+        List<AppSalesWatchLogReportVO> userStatsList = fsCourseWatchLogMapper.selectAppDeptUserStats(param);
+        // 基础数据+看课统计+答题统计+红包统计(合并查询,按部门ID+营期ID+视频ID分组)
+        List<AppSalesWatchLogReportVO> watchStatsList = fsCourseWatchLogMapper.selectAppDeptWatchStats(param);
+        if (CollectionUtils.isEmpty(watchStatsList)) {
+            return Collections.emptyList();
+        }
+        // 订单统计
+        List<AppSalesWatchLogReportVO> orderStatsList = fsCourseWatchLogMapper.selectAppDeptOrderStats(param);
+
+        // 2. 查询营期信息
+        List<Long> periodIds = watchStatsList.stream()
+                .map(AppSalesWatchLogReportVO::getPeriodId)
+                .distinct()
+                .collect(Collectors.toList());
+        List<AppSalesWatchLogReportVO> campPeriodList = fsCourseWatchLogMapper.selectAppSalesCampPeriod(periodIds);
+
+        // 3. 转换为Map便于查找
+        // APP会员数统计按部门ID分组
+        Map<Long, AppSalesWatchLogReportVO> userStatsMap = userStatsList.stream()
+                .collect(Collectors.toMap(
+                        AppSalesWatchLogReportVO::getDeptId,
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        // 看课统计(已包含基础数据、答题、红包)按部门ID+营期ID+视频ID分组
+        Map<String, AppSalesWatchLogReportVO> watchStatsMap = watchStatsList.stream()
+                .collect(Collectors.toMap(
+                        item -> item.getDeptId() + "_" + item.getPeriodId() + "_" + item.getVideoId(),
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        // 订单统计按部门ID+营期ID+视频ID分组
+        Map<String, AppSalesWatchLogReportVO> orderStatsMap = orderStatsList.stream()
+                .collect(Collectors.toMap(
+                        item -> item.getDeptId() + "_" + item.getPeriodId() + "_" + item.getVideoId(),
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+        Map<Long, AppSalesWatchLogReportVO> campPeriodMap = campPeriodList.stream()
+                .collect(Collectors.toMap(
+                        AppSalesWatchLogReportVO::getPeriodId,
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+
+        // 4. 组装数据
+        for (AppSalesWatchLogReportVO vo : watchStatsList) {
+            String key = vo.getDeptId() + "_" + vo.getPeriodId() + "_" + vo.getVideoId();
+
+            // APP会员数统计(按部门ID)
+            AppSalesWatchLogReportVO userStats = userStatsMap.get(vo.getDeptId());
+            if (userStats != null) {
+                vo.setAppUserCount(userStats.getAppUserCount());
+                vo.setNewAppUserCount(userStats.getNewAppUserCount());
+                vo.setSalesCount(userStats.getSalesCount()); // 销售数(部门维度特有)
+            }
+
+            // 计算完课率 = 完课数 / (完课数 + 未完课数 + 未看数) * 100%
+            int total = vo.getFinishedCount() + vo.getUnfinishedCount() + vo.getNotWatchedCount();
+            if (total > 0) {
+                vo.setCompletionRate(BigDecimal.valueOf(vo.getFinishedCount())
+                        .multiply(BigDecimal.valueOf(100))
+                        .divide(BigDecimal.valueOf(total), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setCompletionRate(BigDecimal.ZERO);
+            }
+
+            // 订单统计
+            AppSalesWatchLogReportVO orderStats = orderStatsMap.get(key);
+            if (orderStats != null) {
+                vo.setHistoryOrderCount(orderStats.getHistoryOrderCount());
+            }
+
+            // 营期信息
+            AppSalesWatchLogReportVO campPeriod = campPeriodMap.get(vo.getPeriodId());
+            if (campPeriod != null) {
+                vo.setPeriodName(campPeriod.getPeriodName());
+                vo.setTrainingCampName(campPeriod.getTrainingCampName());
+            }
+        }
+
+        return watchStatsList;
+    }
+
     /**
      * 根据维度获取基础数据
      */

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

@@ -68,6 +68,11 @@ public class AppCourseReportVO {
     @Excel(name = "红包金额")
     private  BigDecimal packetAmount;
 
+    /**
+     * 总人数
+     */
+    private  Integer  accessCount;
+
     /**
      * 日志id
      */

+ 89 - 0
fs-service/src/main/java/com/fs/his/vo/AppSalesWatchLogReportVO.java

@@ -0,0 +1,89 @@
+package com.fs.his.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class AppSalesWatchLogReportVO {
+
+    /** 销售ID */
+    private Long salesId;
+
+    /** 销售名称 */
+    @Excel(name = "销售")
+    private String salesName;
+
+    /** APP会员数 */
+    @Excel(name = "APP会员数")
+    private Integer appUserCount;
+
+    /** 新注册APP会员数 */
+    @Excel(name = "新注册APP会员数")
+    private Integer newAppUserCount;
+
+    /** 所属销售部门 */
+    @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 Integer finishedCount;
+
+    /** 未完课数 */
+    @Excel(name = "未完课数")
+    private Integer unfinishedCount;
+
+    /** 完课率 */
+    @Excel(name = "完课率")
+    private BigDecimal completionRate;
+
+    /** 未看数 */
+    @Excel(name = "未看数")
+    private Integer notWatchedCount;
+
+    /** 未答题数 */
+    @Excel(name = "未答题数")
+    private Integer notAnsweredCount;
+
+    /** 红包金额 */
+    @Excel(name = "红包金额")
+    private BigDecimal redPacketAmount;
+
+    /** 历史疗法订单数 */
+    @Excel(name = "历史疗法订单数")
+    private Integer historyOrderCount;
+
+    /** 销售数(部门维度特有) */
+    @Excel(name = "销售数")
+    private Integer salesCount;
+
+    /** 营期ID */
+    private Long periodId;
+
+    /** 视频ID */
+    private Long videoId;
+
+    /** 部门ID */
+    private Long deptId;
+
+    /** 公司ID */
+    private Long companyId;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/store/vo/h5/FsUserPageListExportVO.java

@@ -128,4 +128,12 @@ public class FsUserPageListExportVO {
     @ApiModelProperty(value = "用户头像")
     private String avatar;
 
+    @ApiModelProperty(value = "APP来源")
+    @Excel(name = "APP来源")
+    private String source;
+
+    @ApiModelProperty(value = "登录设备")
+    @Excel(name = "登录设备")
+    private String loginDevice;
+
 }

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

@@ -1996,11 +1996,13 @@ FROM
     </select>
     <select id="selectActiveUserIds" resultType="java.lang.Long">
         SELECT DISTINCT user_id
-        FROM fs_user_course_study_log
+        FROM fs_course_watch_log
         WHERE user_id IN
         <foreach item="userId" collection="userIds" open="(" separator="," close=")">
             #{userId}
         </foreach>
+        and send_type = 1
+        and watch_type = 1
     </select>
     <select id="selectAppWatchStatistics" resultType="com.fs.his.vo.AppCourseReportVO">
         SELECT
@@ -2008,7 +2010,8 @@ FROM
             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
+                COUNT(CASE WHEN log_type = 2 THEN log_id END) AS finishedCount,
+                count(log_id) as accessCount
         FROM fs_course_watch_log
         <where>
             watch_type =1 and send_type = 1
@@ -2135,4 +2138,184 @@ FROM
             AND u.nick_name LIKE CONCAT('%', #{nickName}, '%')
         </if>
     </sql>
+
+    <!-- 销售维度APP会员数统计(直接查fs_user表) -->
+    <select id="selectAppSalesUserStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cu.user_id AS salesId,
+            COUNT(DISTINCT CASE WHEN u.source IS NOT NULL THEN u.user_id END) AS appUserCount,
+            <choose>
+                <when test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                    COUNT(DISTINCT CASE WHEN u.source IS NOT NULL AND u.register_date &gt;= #{startDate} AND u.register_date &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY) THEN u.user_id END)
+                </when>
+                <otherwise>
+                    COUNT(DISTINCT CASE WHEN u.source IS NOT NULL THEN u.user_id END)
+                </otherwise>
+            </choose> AS newAppUserCount
+        FROM fs_user u
+        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 cuu.company_id = c.company_id
+        LEFT JOIN company_dept cd ON cu.dept_id = cd.dept_id
+        WHERE u.source IS NOT NULL
+        <if test="companyId != null and companyId != ''">
+            AND cuu.company_id = #{companyId}
+        </if>
+        <if test="deptId != null and deptId != ''">
+            AND cu.dept_id = #{deptId}
+        </if>
+        <if test="salesId != null and salesId != ''">
+            AND cu.user_id = #{salesId}
+        </if>
+        <if test="project != null and project != ''">
+            AND cuu.project_id = #{project}
+        </if>
+        GROUP BY cu.user_id
+    </select>
+
+    <!-- 销售维度基础数据+看课统计(合并查询) -->
+    <select id="selectAppSalesWatchStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cu.user_id AS salesId,
+            cu.nick_name AS salesName,
+            cd.dept_name AS salesDept,
+            c.company_name AS salesCompany,
+            log.period_id AS periodId,
+            log.video_id AS videoId,
+            cv.title AS videoTitle,
+            cd.dept_id AS deptId,
+            c.company_id AS companyId,
+            COUNT(DISTINCT CASE WHEN log.log_type = '2' THEN log.log_id END) AS finishedCount,
+            COUNT(DISTINCT CASE WHEN log.log_type = '1' THEN log.log_id END) AS unfinishedCount,
+            COUNT(DISTINCT CASE WHEN log.log_type = '3' THEN log.log_id END) AS notWatchedCount,
+            COUNT(DISTINCT CASE WHEN a.log_id IS NULL THEN log.log_id END) AS notAnsweredCount,
+            COALESCE(SUM(rpl.amount), 0) AS redPacketAmount
+        FROM fs_course_watch_log log
+        LEFT JOIN company_user cu ON log.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
+        LEFT JOIN fs_course_answer_logs a ON a.watch_log_id = log.log_id
+        LEFT JOIN fs_course_red_packet_log rpl ON rpl.watch_log_id = log.log_id
+        WHERE log.send_type = 1
+        AND log.watch_type = 1
+        <include refid="commonConditions"/>
+        GROUP BY cu.user_id, log.period_id, log.video_id
+        ORDER BY cu.user_id
+    </select>
+
+    <!-- 销售维度订单统计 -->
+    <select id="selectAppSalesOrderStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cu.user_id AS salesId,
+            log.period_id AS periodId,
+            log.video_id AS videoId,
+            COUNT(DISTINCT CASE WHEN po.status = 3 THEN po.order_id END) AS historyOrderCount
+        FROM fs_course_watch_log log
+        LEFT JOIN company_user cu ON log.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_package_order po ON po.user_id = log.user_id
+        WHERE log.send_type = 1
+        AND log.watch_type = 1
+        <include refid="commonConditions"/>
+        GROUP BY cu.user_id, log.period_id, log.video_id
+    </select>
+
+    <!-- 销售维度营期信息 -->
+    <select id="selectAppSalesCampPeriod" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+        cp.period_id periodId,
+        cp.period_name periodName,
+        camp.training_camp_name trainingCampName
+        FROM
+        fs_user_course_period cp
+        LEFT JOIN fs_user_course_training_camp camp ON camp.training_camp_id = cp.training_camp_id
+        WHERE cp.period_id in
+        <foreach collection="periodIds" item="periodId" open="(" separator="," close=")">
+            #{periodId}
+        </foreach>
+    </select>
+
+    <!-- 销售部门维度APP会员数统计 -->
+    <select id="selectAppDeptUserStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cd.dept_id AS deptId,
+            COUNT(DISTINCT CASE WHEN u.source IS NOT NULL THEN u.user_id END) AS appUserCount,
+            <choose>
+                <when test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                    COUNT(DISTINCT CASE WHEN u.source IS NOT NULL AND u.register_date &gt;= #{startDate} AND u.register_date &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY) THEN u.user_id END)
+                </when>
+                <otherwise>
+                    COUNT(DISTINCT CASE WHEN u.source IS NOT NULL THEN u.user_id END)
+                </otherwise>
+            </choose> AS newAppUserCount,
+            COUNT(DISTINCT cu.user_id) AS salesCount
+        FROM fs_user u
+        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 cuu.company_id = c.company_id
+        LEFT JOIN company_dept cd ON cu.dept_id = cd.dept_id
+        WHERE u.source IS NOT NULL
+        <if test="companyId != null and companyId != ''">
+            AND cuu.company_id = #{companyId}
+        </if>
+        <if test="deptId != null and deptId != ''">
+            AND cu.dept_id = #{deptId}
+        </if>
+        <if test="salesId != null and salesId != ''">
+            AND cu.user_id = #{salesId}
+        </if>
+        <if test="project != null and project != ''">
+            AND cuu.project_id = #{project}
+        </if>
+        GROUP BY cd.dept_id
+    </select>
+
+    <!-- 销售部门维度基础数据+看课统计(合并查询) -->
+    <select id="selectAppDeptWatchStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cd.dept_id AS deptId,
+            cd.dept_name AS salesDept,
+            c.company_name AS salesCompany,
+            log.period_id AS periodId,
+            log.video_id AS videoId,
+            cv.title AS videoTitle,
+            c.company_id AS companyId,
+            COUNT(DISTINCT CASE WHEN log.log_type = '2' THEN log.log_id END) AS finishedCount,
+            COUNT(DISTINCT CASE WHEN log.log_type = '1' THEN log.log_id END) AS unfinishedCount,
+            COUNT(DISTINCT CASE WHEN log.log_type = '3' THEN log.log_id END) AS notWatchedCount,
+            COUNT(DISTINCT CASE WHEN a.log_id IS NULL THEN log.log_id END) AS notAnsweredCount,
+            COALESCE(SUM(rpl.amount), 0) AS redPacketAmount
+        FROM fs_course_watch_log log
+        LEFT JOIN company_user cu ON log.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
+        LEFT JOIN fs_course_answer_logs a ON a.watch_log_id = log.log_id
+        LEFT JOIN fs_course_red_packet_log rpl ON rpl.watch_log_id = log.log_id
+        WHERE log.send_type = 1
+        AND log.watch_type = 1
+        <include refid="commonConditions"/>
+        GROUP BY cd.dept_id, log.period_id, log.video_id
+        ORDER BY cd.dept_id
+    </select>
+
+    <!-- 销售部门维度订单统计 -->
+    <select id="selectAppDeptOrderStats" resultType="com.fs.his.vo.AppSalesWatchLogReportVO">
+        SELECT
+            cd.dept_id AS deptId,
+            log.period_id AS periodId,
+            log.video_id AS videoId,
+            COUNT(DISTINCT CASE WHEN po.status = 3 THEN po.order_id END) AS historyOrderCount
+        FROM fs_course_watch_log log
+        LEFT JOIN company_user cu ON log.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_package_order po ON po.user_id = log.user_id
+        WHERE log.send_type = 1
+        AND log.watch_type = 1
+        <include refid="commonConditions"/>
+        GROUP BY cd.dept_id, log.period_id, log.video_id
+    </select>
 </mapper>

+ 3 - 1
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -2498,7 +2498,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             ucu.company_id,
             ucu.project_id,
             fs_user.integral,
-            if(fs_user.pay_count>0,1,0) as isBuy
+            if(fs_user.pay_count>0,1,0) as isBuy,
+            fs_user.source,
+            fs_user.login_device as loginDevice
         FROM fs_user
         left join fs_user_company_user ucu on ucu.user_id = fs_user.user_id
         <where>

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

@@ -487,7 +487,7 @@ public class AppLoginController extends AppBaseController{
         }
         if (usersByPhone.size()==1){
             user = usersByPhone.get(0);
-            if (user.getUnionId()==null){
+            if (StringUtils.isEmpty(user.getUnionId())){
                 if (user.getPhone().length()<=11){
                     FsUser fsUser = new FsUser();
                     fsUser.setUserId(user.getUserId());

+ 784 - 0
fs-user-app/src/main/java/com/fs/app/controller/AppLoginController2.java

@@ -0,0 +1,784 @@
+package com.fs.app.controller;
+
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.date.DateTime;
+import com.fs.app.annotation.Login;
+import com.fs.app.param.*;
+import com.fs.app.utils.WxUtil;
+import com.fs.common.VerifyCodeUtil;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.service.ISmsService;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.sign.Md5Utils;
+import com.fs.core.config.WxOpenProperties;
+import com.fs.his.domain.FsUser;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.service.IFsUserNewTaskService;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.vo.FsUserRegisterParam;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.annotations.Param;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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 java.util.*;
+import java.util.concurrent.TimeUnit;
+
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
+
+@Api("app登录接口2")
+@RestController
+@RequestMapping(value="/app2/app")
+@Slf4j
+public class AppLoginController2 extends AppBaseController{
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    @Autowired
+    private IFsUserService userService;
+    @Autowired
+    private FsUserMapper userMapper;
+    @Autowired
+    private IFsUserNewTaskService userNewTaskService;
+    @Autowired
+    private WxOpenProperties openProperties;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ISmsService smsService;
+    @ApiOperation("注册app用户")
+    @PostMapping("/register")
+    @RepeatSubmit
+    public R registerDoctor(@Validated @RequestBody FsUserRegisterParam param){
+        FsUser fsUser = findUserByPhone(param.getPhone());
+
+//        if (fsUser == null) {
+//            // 尝试使用加密后的手机号查询
+//            fsUser = userService.selectFsUserByPhone(encryptPhone(param.getPhone()));
+//        }
+
+        if (fsUser != null && StringUtils.isNotEmpty(fsUser.getPassword())) {
+            return R.error("此账号已经注册");
+        }
+
+        FsUser user = new FsUser();
+        if (fsUser != null) {
+            // 更新已有用户的密码
+            user.setUserId(fsUser.getUserId());
+            user.setPassword(Md5Utils.hash(param.getPassword()));
+            user.setUpdateTime(new DateTime());
+            userService.updateFsUser(user);
+            return R.ok("注册成功");
+        } else {
+            // 创建新用户
+            user.setPhone(param.getPhone());
+            user.setNickName("app用户" + param.getPhone().substring(param.getPhone().length() - 4));
+            user.setStatus(1);
+            user.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+            user.setPassword(Md5Utils.hash(param.getPassword()));
+            user.setCreateTime(new Date());
+
+            if (userService.insertFsUser(user) > 0) {
+                return R.ok("注册成功");
+            } else {
+                return R.error("注册失败");
+            }
+        }
+
+    }
+
+    @ApiOperation("登录")
+    @PostMapping("/login")
+    @Transactional
+    public R login(@Validated @RequestBody FsUserLoginParam param) {
+        int loginType = param.getLoginType();
+        switch (loginType) {
+            case 1:
+                return handleLoginType1(param);
+            case 3:
+                return handleLoginType3(param);
+            default:
+                return R.error("请选择正确的登陆类型!");
+        }
+    }
+
+
+
+
+
+    @ApiOperation("找回密码")
+    @PostMapping("/editPwd")
+    public R login(@Validated @RequestBody FsUserEditPwdParam param) {
+        FsUser user = userService.selectFsUserByPhone(encryptPhone(param.getPhone()));
+        if (user==null){
+            return R.error("用户不存在");
+        }
+        String code = redisCache.getCacheObject(user.getPhone());
+        if(StringUtils.isEmpty(code)){
+            return R.error("验证码已过期");
+        }
+        if(!code.equals(param.getCode())){
+            return R.error("验证码不正确");
+        }
+        FsUser userMap=new FsUser();
+        userMap.setUserId(user.getUserId());
+        userMap.setPassword(Md5Utils.hash(param.getPassword()));
+        if (userService.updateFsUser(userMap)>0){
+            return R.ok("新密码设置成功");
+        }
+        return R.error("新密码设置失败,请稍后再试!");
+    }
+
+
+    @ApiOperation("微信登录")
+    @PostMapping("/loginByWeChat")
+    @Transactional
+    public R loginByWeChat(@Validated @RequestBody FsUserLoginByWeChatParam param) {
+        try {
+            if (StringUtils.isBlank(param.getCode())) {
+                return R.error("code不存在");
+            }
+            logger.info("zyp app微信登录,param:{},输出appid,{},secret:{}", param, openProperties.getAppId(), openProperties.getSecret());
+            Map result = WxUtil.getAccessToken(param.getCode(), openProperties.getAppId(), openProperties.getSecret());
+            String accessToken = result.get("access_token").toString();
+            String unionid = result.get("unionid").toString();
+
+            String openid = result.get("openid").toString();
+
+            Map userInfo = WxUtil.getUserInfo(accessToken, openid);
+
+            String nickname = userInfo.get("nickname").toString();
+            Integer sex = (Integer) userInfo.get("sex");
+            String avatar = userInfo.get("headimgurl").toString();
+            FsUser user = userService.selectFsUserByUnionid(unionid);
+
+            Map<String, Object> map = new HashMap<>();
+            if (user == null) {
+                user = new FsUser();
+                user.setLoginDevice(param.getLoginDevice()!=null ? param.getLoginDevice() : null);
+                user.setSource(param.getSource()!=null ? param.getSource() : null);
+                user.setNickName(nickname);
+                user.setAvatar(avatar);
+                if (sex!=0){
+                    user.setSex(sex);
+                }
+                user.setUnionId(unionid);
+                user.setCreateTime(new Date());
+                user.setAppId(openProperties.getAppId());
+                user.setStatus(1);
+                if (StringUtils.isNotEmpty(param.getJpushId())) {
+                    user.setJpushId(param.getJpushId());
+                }
+                String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                user.setLastIp(ipAddr);
+                userService.insertFsUser(user);
+                map.put("isNew", true);
+                map.put("unionid",unionid);
+                return R.ok(map);
+            } else {
+                if (StringUtils.isNotEmpty(param.getJpushId())) {
+                    updateExistingUserJpushId(user, param.getJpushId());
+                }
+                if (StringUtils.isEmpty(user.getPhone())) {
+                    map.put("isNew", true);
+                    map.put("unionid",user.getUnionId());
+                    return R.ok(map);
+                }
+            }
+            int isFirstLogin = userNewTaskService.performTaskOne(user.getUserId());
+            String token = jwtUtils.generateToken(user.getUserId());
+            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+            map.put("token", token);
+            map.put("user", user);
+            map.put("isFirst",isFirstLogin);
+            return R.ok(map);
+        }catch (Exception e){
+            logger.error("zyp 登录失败:{}", e.getMessage());
+            return R.error("登录失败");
+        }
+
+    }
+
+    @ApiOperation("绑定手机号")
+    @PostMapping("/setPhone")
+    @Transactional(rollbackFor = Exception.class)
+    public R setPhone(@Validated @RequestBody FsUserEditPhoneParam param) {
+        logger.info("fcj 绑定手机号入参:{}", param);
+        FsUser user = userService.selectFsUserByUnionid(param.getUnionId());
+        if (user==null){
+            return R.error("用户数据不存在");
+        }
+//        //验证码绑定,需要校验验证码
+        if (param.getBindType()!=null){
+            String redisCode = redisCache.getCacheObject("sms:code:" + param.getPhone());
+            if (StringUtils.isEmpty(redisCode)){
+                return R.error("验证码已过期,请重新发送");
+            }
+            if (!redisCode.equals(param.getCode())) {
+                return R.error("验证码错误");
+            }
+            redisCache.deleteObject("sms:code:" + param.getPhone());
+        }
+        FsUser userMap = findUserByPhone(param.getPhone());
+        //绑定的手机号已存在用户的情况,将微信登录的时候创建的新号的UnionId移动到老号中,删除新号(将两个号合并)
+        if (userMap!=null){
+            if (userMap.getUserId().equals(user.getUserId())) {
+                user.setPhone(param.getPhone());
+                user.setLoginDevice(param.getLoginDevice());
+                user.setSource(param.getSource());
+                String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                user.setLastIp(ipAddr);
+                userService.updateFsUser(user);
+                return generateTokenAndReturn(user);
+            }
+            if (StringUtils.isNotEmpty(userMap.getUnionId())&&!userMap.getUnionId().equals(user.getUnionId())){
+                return R.error("该手机号已绑定其他微信");
+            }
+            //合并规则修改 保留手机号存在的用户 合并掉unionid存在但手机号不存在的用户
+            FsUser keepUser;
+            FsUser deleteUser;
+            // 判断哪个用户有手机号,优先保留有手机号的用户
+            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());
+            keepUser.setUnionId(user.getUnionId());
+            keepUser.setPhone(param.getPhone());
+            String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+            keepUser.setLastIp(ipAddr);
+            if (userService.updateFsUser(keepUser)>0){
+                userService.realDeleteFsUserByUserId(deleteUser.getUserId());
+                return generateTokenAndReturn(keepUser);
+            }
+            return R.error("绑定手机号失败");
+        }
+        user.setPhone(param.getPhone());
+        user.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null );
+        user.setSource(param.getSource()!= null ? param.getSource() : null);
+        userMap = new FsUser();
+        userMap.setLoginDevice(param.getLoginDevice());
+        userMap.setSource(param.getSource());
+        userMap.setUserId(user.getUserId());
+        userMap.setPhone(param.getPhone());
+        String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+        userMap.setLastIp(ipAddr);
+        if (userService.updateFsUser(userMap)>0){
+            return generateTokenAndReturn(user);
+        }
+        return R.error("绑定手机号失败");
+    }
+
+    @ApiOperation("绑定微信")
+    @PostMapping("/bindWeChat")
+    @Transactional(rollbackFor = Exception.class)
+    public R bindWeChat(@Validated @RequestBody FsUserEditUnionidParam param) {
+        try {
+            logger.info("zyp app绑定微信,param:{}", param);
+            Map result = WxUtil.getAccessToken(param.getCode(), openProperties.getAppId(), openProperties.getSecret());
+            if (!result.containsKey("access_token")) {
+                return R.error("微信授权失败");
+            }
+            String accessToken = result.get("access_token").toString();
+            String unionid = result.get("unionid").toString();
+            String openid = result.get("openid").toString();
+            Map userInfo = WxUtil.getUserInfo(accessToken, openid);
+            String nickname = userInfo.get("nickname").toString();
+            Integer sex = (Integer) userInfo.get("sex");
+            String avatar = userInfo.get("headimgurl").toString();
+            FsUser user = findUserByPhone(param.getPhone());
+            if (user!=null && StringUtils.isEmpty(user.getUnionId())){
+                FsUser userByUnionId = userMapper.selectFsUserByUnionid(unionid);
+                //绑定的微信已存在用户的情况,将手机号一键登录的时候创建的新号的手机号移动到老号中,删除新号(将两个号合并)
+                if (userByUnionId!=null){
+                    if (StringUtils.isNotEmpty(userByUnionId.getPhone())&&!user.getPhone().equals(userByUnionId.getPhone())){
+                        return R.error("该微信已绑定其他手机号");
+                    }
+                    if (userByUnionId.getUserId().equals(user.getUserId())) {
+                        user.setPhone(param.getPhone());
+                        user.setLoginDevice(param.getLoginDevice());
+                        user.setSource(param.getSource());
+                        user.setUnionId(unionid);
+                        String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                        user.setLastIp(ipAddr);
+                        userService.updateFsUser(user);
+                        return generateTokenAndReturn(user);
+                    }
+                    //改一下合并规则 将unionid存在的用户保留 合并掉手机号存在但unionid不存在的用户,而且如果被合并的用户如果有source 就转移过去用
+                    // 合并用户逻辑:优先保留有 union_id 的用户
+                    FsUser keepUser;
+                    FsUser deleteUser;
+
+                    // 判断哪个用户有 union_id,优先保留有 union_id 的用户
+                    if (StringUtils.isNotEmpty(user.getUnionId()) && StringUtils.isEmpty(userByUnionId.getUnionId())) {
+                        // 当前用户有 union_id,保留当前用户
+                        keepUser = user;
+                        deleteUser = userByUnionId;
+                    } 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());
+                    // 如果保留用户没有 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);
+                    String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                    keepUser.setLastIp(ipAddr);
+                    if (userService.updateFsUser(keepUser)>0){
+                        userService.realDeleteFsUserByUserId(deleteUser.getUserId());
+                        return generateTokenAndReturn(keepUser);
+                    }
+                    else {
+                        return R.error("绑定微信失败");
+                    }
+                }else {
+                    user.setSource(param.getSource() != null ? param.getSource() : null );
+                    user.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
+                    user.setNickName(nickname);
+                    user.setAvatar(avatar);
+                    user.setSex(sex);
+                    user.setUnionId(unionid);
+                    user.setAppOpenId(openid);
+                    String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                    user.setLastIp(ipAddr);
+                    if (userService.updateFsUser(user)>0){
+                        return generateTokenAndReturn(user);
+                    }
+                    else {
+                        return R.error("绑定微信失败");
+                    }
+                }
+            }
+            return R.error("数据参数异常!");
+        }catch (Exception e){
+            logger.error("zyp 登录失败:{}", e.getMessage());
+            return R.error("登录失败");
+        }
+    }
+
+
+    private void updateExistingUserJpushId(FsUser user, String jpushId) {
+        FsUser userMap = new FsUser();
+        userMap.setUserId(user.getUserId());
+        userMap.setJpushId(jpushId);
+        if (StringUtils.isNotEmpty(user.getAppOpenId())) {
+            userMap.setAppOpenId(user.getAppOpenId());
+        }
+        String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+        userMap.setLastIp(ipAddr);
+        userService.updateFsUser(userMap);
+    }
+
+    private R generateTokenAndReturn(FsUser user) {
+        String token = jwtUtils.generateToken(user.getUserId());
+        redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+        int isFirstLogin = userNewTaskService.performTaskOne(user.getUserId());
+        Map<String, Object> map = new HashMap<>();
+        map.put("token", token);
+        map.put("user", user);
+        map.put("isFirst",isFirstLogin);
+        return R.ok(map);
+    }
+
+    private R handleLoginType1(FsUserLoginParam param) {
+        if (StringUtils.isEmpty(param.getPhone()) || StringUtils.isEmpty(param.getPassword())) {
+            return R.error("账号或密码不能为空");
+        }
+        FsUser user = findUserByPhone(param.getPhone());
+        // 校验用户是否存在及账号状态
+        if (user == null) {
+            return R.error("该手机账户不存在");
+        } else if (user.getStatus() == 0) {
+            return R.error("账号已停用");
+        }
+        if (StringUtils.isNotEmpty(param.getJpushId())) {
+            updateExistingUserJpushId(user, param.getJpushId());
+        }
+
+        if (!Md5Utils.hash(param.getPassword()).equals(user.getPassword())) {
+            return R.error("密码不正确");
+        }
+
+        return generateTokenAndReturn(user);
+
+    }
+
+    private R handleLoginType3(FsUserLoginParam param) {
+        // 根据手机号查询用户
+        FsUser user = null;
+        List<FsUser> usersByPhone = findUsersByPhone(param.getPhone());
+        if (CollectionUtil.isEmpty(usersByPhone)){
+            FsUser newUser = createNewUser(param);
+            if (StringUtils.isNotEmpty(newUser.getJpushId())) {
+//                try {
+//                    //发送注册优惠券
+//                    fsUserCouponService.sendRegisterCoupon(newUser);
+//                } catch (Exception e) {
+//                    logger.error("发送注册优惠券失败:{}",e.getMessage());
+//                }
+            }
+            return R.ok().put("isNew",true).put("phone",encryptPhone(param.getPhone()));
+        }
+        if (usersByPhone.size()==1){
+            user = usersByPhone.get(0);
+            if (StringUtils.isEmpty(user.getUnionId())){
+                if (user.getPhone().length()<=11){
+                    FsUser fsUser = new FsUser();
+                    fsUser.setUserId(user.getUserId());
+                    fsUser.setPhone(encryptPhone(param.getPhone()));
+                    String ipAddr = IpUtils.getIpAddr(ServletUtils.getRequest());
+                    fsUser.setLastIp(ipAddr);
+                    userMapper.updateFsUser(fsUser);
+                    logger.info("zyp \n【手机加密】:{}",encryptPhone(param.getPhone()));
+                }
+                return R.ok().put("isNew",true).put("phone",encryptPhone(param.getPhone()));
+            }
+            if (StringUtils.isNotEmpty(param.getJpushId())) {
+                updateExistingUserJpushId(user, param.getJpushId());
+//                try {
+//                    //发送注册优惠券
+//                    fsUserCouponService.sendRegisterCoupon(user);
+//                } catch (Exception e) {
+//                    logger.error("发送注册优惠券失败:{}",e.getMessage());
+//                }
+            }
+        }else {
+            return R.ok().put("users",usersByPhone);
+        }
+        return generateTokenAndReturn(user);
+    }
+
+    private List<FsUser> findUsersByPhone(String phone) {
+        // 先根据加密手机号查询用户
+        String jiami = (encryptPhone(phone));
+        List<FsUser> fsUsers = userMapper.selectFsUsersByPhoneLimitOne(jiami);
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = userMapper.selectFsUsersByPhoneLimitOne(encryptPhoneOldKey(phone));
+        }
+        // 如果没有找到用户,再根据手机号查询
+        if (CollectionUtil.isEmpty(fsUsers)) {
+            fsUsers = userMapper.selectFsUsersByPhoneLimitOne(phone);
+
+        }
+        return fsUsers;
+    }
+    private FsUser createNewUser(FsUserLoginParam param) {
+        FsUser newUser = new FsUser();
+        newUser.setLoginDevice(param.getLoginDevice() != null ? param.getLoginDevice() : null);
+        newUser.setSource(param.getSource() != null ? param.getSource() : null );
+        newUser.setNickName("匿名用户**");
+        newUser.setPhone(param.getPhone());
+        newUser.setCreateTime(new Date());
+        newUser.setStatus(1);
+        newUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+        if (StringUtils.isNotEmpty(param.getJpushId())) {
+            newUser.setJpushId(param.getJpushId());
+        }
+        userService.insertFsUser(newUser);
+        return newUser;
+    }
+
+    private FsUser findUserByPhone(String phone) {
+        // 先根据加密手机号查询用户
+        String jiami = (encryptPhone(phone));
+        FsUser user = userMapper.selectFsUserByPhoneLimitOne(jiami);
+
+        // 如果没有找到用户,再根据手机号查询
+        if (user == null) {
+            user = userMapper.selectFsUserByPhoneLimitOne(phone);
+
+        }
+        return user;
+    }
+
+    @PostMapping("/loginTest")
+    public R loginTest(@RequestBody String userId) {
+        FsUser user = userMapper.selectFsUserByUserId(Long.parseLong(userId));
+        if (user!=null){
+            String token = jwtUtils.generateToken(user.getUserId());
+            redisCache.setCacheObject("userToken:" + user.getUserId(), token, 604800, TimeUnit.SECONDS);
+            Map<String, Object> map = new HashMap<>();
+            map.put("token", token);
+            map.put("user", user);
+            return R.ok(map);
+        }
+        return R.error("用户不存在!");
+    }
+
+    /**
+     * 退出清除jpushId
+     */
+    @Login
+    @GetMapping("/logout")
+    public R logout() {
+        String userId = getUserId();
+        FsUser fsUser = new FsUser();
+        fsUser.setUserId(Long.parseLong(userId));
+        fsUser.setJpushId("");
+        if (userMapper.updateFsUser(fsUser)>0) {
+            return R.ok();
+        } else {
+            return R.error("用户不存在!");
+        }
+    }
+
+    /**
+     * 更新jpushId
+     */
+    @Login
+    @GetMapping("/updatePushId")
+    public R updatePushId(@Param("pushId") String pushId) {
+        String userId = getUserId();
+        FsUser fsUser = new FsUser();
+        fsUser.setUserId(Long.parseLong(userId));
+        fsUser.setJpushId(pushId);
+        if (userMapper.updateFsUser(fsUser)>0) {
+            return R.ok();
+        } else {
+            return R.error("用户不存在!");
+        }
+    }
+
+    @PostMapping("/sendCode")
+    public R sendCode(@RequestBody Map<String, String> body){
+        String phone = body.get("phone");
+        String encryptPhone = encryptPhone(phone);
+//        List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
+//        if(CollectionUtil.isEmpty(user)){
+//            user = userService.selectFsUserListByPhone(encryptPhoneOldKey(phone));
+//        }
+//        if (CollectionUtil.isEmpty(user)){
+//            return R.error("此电话号码未绑定用户");
+//        }
+
+        // 验证码 key(3分钟有效)
+        String smsCodeKey = "sms:code:" + phone;
+        // 冷却 key(60秒内不能重复发送)
+        String smsCooldownKey = "sms:cooldown:" + phone;
+
+        // 判断是否在 60 秒冷却期
+        if (redisCache.getCacheObject(smsCooldownKey) != null) {
+            return R.error("验证码已发送,请稍后再试");
+        }
+
+        // 生成新验证码
+        String smsCode = VerifyCodeUtil.generateCode();
+
+        // 发送短信
+        smsService.sendCaptcha(phone, smsCode, "验证码");
+
+        // 缓存验证码(3分钟有效)
+        redisCache.setCacheObject(smsCodeKey, smsCode, 180, TimeUnit.SECONDS);
+        // 设置冷却时间(60秒内不能再发)
+        redisCache.setCacheObject(smsCooldownKey, "1", 60, TimeUnit.SECONDS);
+
+        return R.ok("验证码已发送");
+    }
+
+    /**
+     * 用于查询 使用老的数据加密
+     * @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;
+    }
+
+    @PostMapping("/loginByPhone")
+    public R loginByPhone(@RequestBody Map<String,String> map){
+        String phone = map.get("phone");
+        String code = map.get("code");
+        String encryptPhone = encryptPhone(phone);
+        List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
+        if (CollectionUtil.isEmpty(user)){
+            user = userService.selectFsUserListByPhone(encryptPhoneOldKey(phone));
+        }
+        if (CollectionUtil.isEmpty(user)){
+            return R.error("此电话号码未绑定用户");
+        }
+        if (user.size()>1){
+            //如果出现了一个手机号多个用户的情况,找出登陆过app的那个用户
+            user.removeIf(fsUser -> StringUtils.isEmpty(fsUser.getHistoryApp()));
+        }
+        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        if (StringUtils.isEmpty(redisCode)){
+            return R.error("验证码已过期,请重新发送");
+        }
+        if (!redisCode.equals(code)) {
+            return R.error("验证码错误");
+        }
+        updateExistingUserJpushId(user.get(0), map.get("jpushId"));
+        return generateTokenAndReturn(user.get(0));
+    }
+
+    @PostMapping("/registerSendCode")
+    public R registerSendCode(@RequestBody Map<String, String> body){
+        String phone = body.get("phone");
+        String encryptPhone = encryptPhone(phone);
+        List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
+        if(CollectionUtil.isEmpty(user)){
+            user = userService.selectFsUserListByPhone(encryptPhoneOldKey(phone));
+        }
+        if (!CollectionUtil.isEmpty(user)){
+            return R.error("此电话号码已注册");
+        }
+
+        // 验证码 key(存验证码,3分钟有效)
+        String smsCodeKey = "sms:code:" + phone;
+        // 发送冷却 key(限制60秒内不能再次发送)
+        String smsCooldownKey = "sms:cooldown:" + phone;
+
+        // 判断是否在60秒冷却时间内
+        if (redisCache.getCacheObject(smsCooldownKey) != null) {
+            return R.error("验证码已发送,请稍后再试");
+        }
+
+        // 生成新的验证码
+        String smsCode = VerifyCodeUtil.generateCode();
+
+        // 发送短信
+        smsService.sendCaptcha(phone, smsCode, "验证码");
+
+        // 缓存验证码(3分钟有效)
+        redisCache.setCacheObject(smsCodeKey, smsCode, 180, TimeUnit.SECONDS);
+        // 设置冷却时间(60秒内不能再发)
+        redisCache.setCacheObject(smsCooldownKey, "1", 60, TimeUnit.SECONDS);
+
+        return R.ok("验证码已发送");
+    }
+
+    @PostMapping("/registerByPhone")
+    public R registerByPhone(@RequestBody Map<String,String> map){
+        String phone = map.get("phone");
+        String code = map.get("code");
+        String password = map.get("password");
+        String encryptPhone = encryptPhone(phone);
+        List<FsUser> users = userService.selectFsUserListByPhone(encryptPhone);
+        if (users == null || CollectionUtil.isEmpty(users)){
+            String s = encryptPhoneOldKey(phone);
+            users = userService.selectFsUserListByPhone(s);
+        }
+        if (!CollectionUtil.isEmpty(users)){
+            return R.error("此账号已经注册");
+        }
+        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        if (StringUtils.isEmpty(redisCode)){
+            return R.error("验证码已过期,请重新发送");
+        }
+        if (!redisCode.equals(code)) {
+            return R.error("验证码错误");
+        }
+        FsUser user = new FsUser();
+        // 创建新用户
+        user.setPhone(phone);
+        user.setJpushId(map.get("jpushId"));
+        user.setSource(map.get("source"));
+        user.setNickName("app用户" + phone.substring(phone.length() - 4));
+        user.setStatus(1);
+        user.setAvatar("https://cos.his.cdwjyyh.com/fs/20240926/420728ee06e54575ba82665dedb4756b.png");
+        user.setPassword(Md5Utils.hash(password));
+        user.setCreateTime(new Date());
+        if (userService.insertFsUser(user) > 0) {
+            return R.ok("注册成功");
+        } else {
+            return R.error("注册失败");
+        }
+    }
+
+    @PostMapping("/resetPassword")
+    public R resetPassword(@RequestBody Map<String, String> body){
+        String phone = body.get("phone");
+        String code = body.get("code");
+        String newPassword = body.get("newPassword");
+        String confirmPassword = body.get("confirmPassword");
+        if (!newPassword.equals(confirmPassword)){
+            throw new ServiceException("两次输入密码不一致,请检查");
+        }
+        String encryptPhone = encryptPhone(phone);
+        List<FsUser> user = userService.selectFsUserListByPhone(encryptPhone);
+        if (CollectionUtil.isEmpty(user)){
+            user = userService.selectFsUserListByPhone(encryptPhoneOldKey(phone));
+        }
+        if (CollectionUtil.isEmpty(user)){
+            return R.error("此电话号码未绑定用户");
+        }
+        String redisCode = redisCache.getCacheObject("sms:code:" + phone);
+        if (StringUtils.isEmpty(redisCode)){
+            return R.error("验证码已过期,请重新发送");
+        }
+        if (!redisCode.equals(code)) {
+            return R.error("验证码错误");
+        }
+        String password = Md5Utils.hash(newPassword);
+        return userService.updatePasswordByPhone(password,encryptPhone);
+    }
+
+}

+ 60 - 3
fs-user-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -146,13 +146,14 @@ public class CompanyUserController extends AppBaseController {
     @PostMapping("/bindCompanyUser")
     @Transactional
     public R bindCompanyUser(@Validated @RequestBody FsBindCompanyUserParam param, HttpServletRequest request) {
+        Long currentUserId = Long.parseLong(getUserId());
         CompanyUserUser map = new CompanyUserUser();
         map.setCompanyUserId(param.getCompanyUserId());
-        map.setUserId(Long.parseLong(getUserId()));
-        FsUser user = fsUserService.selectFsUserByUserId(Long.parseLong(getUserId()));
-        //设置用户手机号
+        map.setUserId(currentUserId);
+        FsUser user = fsUserService.selectFsUserByUserId(currentUserId);
         if(user != null && StringUtils.isNotEmpty(param.getPhone())){
             List<FsUser> usersByPhone = findUsersByPhone(param.getPhone());
+            //如果有多个相同手机号的用户 就合并用户保留unionid 存在的  只有一个用户就正常绑定手机号就可以了
             if(CollectionUtil.isNotEmpty(usersByPhone)){
                 //和绑定的用户id比较 如果相同给出限制
                 for (FsUser fsUser : usersByPhone){
@@ -160,6 +161,62 @@ public class CompanyUserController extends AppBaseController {
                         return R.error("您已注册康享银龄APP,如需修改手机号,请联系您的专属客服");
                     }
                 }
+                // 有多个相同手机号的用户,需要合并用户
+//                if (usersByPhone.size() > 1) {
+//                    // 合并规则:优先保留有 union_id 的用户
+//                    FsUser keepUser = null;
+//                    FsUser deleteUser = null;
+//                    FsUser userByUnionId = usersByPhone.stream()
+//                            .filter(u -> StringUtils.isNotEmpty(u.getUnionId()))
+//                            .findFirst()
+//                            .orElse(null);
+//
+//                    if (userByUnionId != null) {
+//                        // 有 union_id 的用户保留
+//                        keepUser = userByUnionId;
+//                        // 找出需要删除的其他用户(第一个非保留用户)
+//                        FsUser finalKeepUser = keepUser;
+//                        deleteUser = usersByPhone.stream()
+//                                .filter(u -> !u.getUserId().equals(finalKeepUser.getUserId()))
+//                                .findFirst()
+//                                .orElse(null);
+//                    } else {
+//                        // 都没有 union_id,按创建时间保留较早的用户
+//                        keepUser = usersByPhone.get(0);
+//                        for (FsUser u : usersByPhone) {
+//                            if (u.getCreateTime().before(keepUser.getCreateTime())) {
+//                                keepUser = u;
+//                            }
+//                        }
+//                        FsUser finalKeepUser1 = keepUser;
+//                        deleteUser = usersByPhone.stream()
+//                                .filter(u -> !u.getUserId().equals(finalKeepUser1.getUserId()))
+//                                .findFirst()
+//                                .orElse(null);
+//                    }
+//
+//                    if (deleteUser != null && keepUser != null) {
+//                        // 更新保留用户的信息
+//                        keepUser.setPhone(param.getPhone());
+//                        // 如果保留用户没有 source,而被删除用户有 source,则转移 source
+//                        if (StringUtils.isEmpty(keepUser.getSource()) && StringUtils.isNotEmpty(deleteUser.getSource())) {
+//                            keepUser.setSource(deleteUser.getSource());
+//                        }
+//                        // 如果保留用户没有 loginDevice,而被删除用户有 loginDevice,则转移 loginDevice
+//                        if (StringUtils.isEmpty(keepUser.getLoginDevice()) && StringUtils.isNotEmpty(deleteUser.getLoginDevice())) {
+//                            keepUser.setLoginDevice(deleteUser.getLoginDevice());
+//                        }
+//                        // 删除被合并的用户
+//                        fsUserService.realDeleteFsUserByUserId(deleteUser.getUserId());
+//
+//                        // 如果当前用户不是保留用户,需要更新当前用户
+//                        if (!user.getUserId().equals(keepUser.getUserId())) {
+//                            currentUserId = keepUser.getUserId();
+//                            map.setUserId(currentUserId);
+//                            user = keepUser;
+//                        }
+//                    }
+//                }
             }
             user.setPhone(param.getPhone());
             fsUserService.updateFsUser(user);