Browse Source

app用户生命周期总览 实现

wangxy 1 week ago
parent
commit
fa5d08b772

+ 89 - 0
fs-admin/src/main/java/com/fs/his/controller/AppOperationReportController.java

@@ -2,15 +2,22 @@ package com.fs.his.controller;
 
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
 import com.fs.his.param.AppOperationReportParam;
 import com.fs.his.service.IAppOperationReportService;
+import com.fs.his.vo.AppDailyReportVO;
 import com.fs.his.vo.AppOperationReportVO;
+import com.fs.his.vo.AppWeeklyReportVO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.List;
+
 /**
  * App运营报表Controller
  */
@@ -34,4 +41,86 @@ public class AppOperationReportController extends BaseController {
         AppOperationReportVO vo = appOperationReportService.getMonthReport(param);
         return AjaxResult.success(vo);
     }
+
+    /**
+     * 查询日期范围内每日运营报表(支持分页)
+     * @param param 查询参数(startDate、endDate、pageNum、pageSize)
+     * @return 每日运营报表分页数据
+     */
+    @ApiOperation("日报表查询")
+    @PreAuthorize("@ss.hasPermi('his:appOperationReport:list')")
+    @GetMapping("/dailyReport")
+    public TableDataInfo dailyReport(AppOperationReportParam param) {
+        List<AppDailyReportVO> list = appOperationReportService.getDailyReport(param);
+        long total = calculateTotalDays(param);
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(200);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(total);
+        return rspData;
+    }
+
+    /**
+     * 查询日期范围内每周运营报表(支持分页)
+     * @param param 查询参数(startDate、endDate、pageNum、pageSize)
+     * @return 每周运营报表分页数据
+     */
+    @ApiOperation("周报表查询")
+    @PreAuthorize("@ss.hasPermi('his:appOperationReport:list')")
+    @GetMapping("/weeklyReport")
+    public TableDataInfo weeklyReport(AppOperationReportParam param) {
+        List<AppWeeklyReportVO> list = appOperationReportService.getWeeklyReport(param);
+        long total = calculateTotalWeeks(param);
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(200);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(total);
+        return rspData;
+    }
+
+    private long calculateTotalDays(AppOperationReportParam param) {
+        try {
+            String startDate = param.getStartDate();
+            String endDate = param.getEndDate();
+            if (startDate == null || endDate == null) {
+                return 30;
+            }
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(startDate));
+            long startMs = cal.getTimeInMillis();
+            cal.setTime(sdf.parse(endDate));
+            long endMs = cal.getTimeInMillis();
+            return (endMs - startMs) / (1000 * 60 * 60 * 24) + 1;
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
+    private long calculateTotalWeeks(AppOperationReportParam param) {
+        try {
+            String startDate = param.getStartDate();
+            String endDate = param.getEndDate();
+            if (startDate == null || endDate == null) {
+                return 13;
+            }
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(startDate));
+            cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+            Calendar endCal = Calendar.getInstance();
+            endCal.setTime(sdf.parse(endDate));
+            long weeks = 0;
+            Calendar weekStart = (Calendar) cal.clone();
+            while (!weekStart.after(endCal)) {
+                weeks++;
+                weekStart.add(Calendar.DAY_OF_MONTH, 7);
+            }
+            return weeks;
+        } catch (Exception e) {
+            return 0;
+        }
+    }
 }

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

@@ -45,6 +45,15 @@ public class FsAppVersionController extends BaseController
         return getDataTable(list);
     }
 
+
+    /**
+     * app版本下拉列表
+     */
+    @GetMapping("/appVersions")
+    public AjaxResult appVersions(FsAppVersion fsAppVersion){
+        return AjaxResult.success(fsAppVersionService.selectFsAppVersionList(fsAppVersion));
+    }
+
     /**
      * 导出app版本列表
      */

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

@@ -114,6 +114,14 @@ public class TestController extends BaseController {
         return R.ok().put("updateCount", count);
     }
 
+    /**
+     * 测试im会员定时发课
+     */
+    @GetMapping("/sendOpenImCourse")
+    public  void  sendOpenImCourse(){
+        task.sendOpenImCourse();
+    }
+
     /**
      * 测试订单发货权限校验
      */

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

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.HttpRequest;
 import cn.hutool.json.JSONUtil;
 import com.baidu.dev2.thirdparty.jackson.databind.ObjectMapper;
+import com.fs.common.annotation.DataScope;
 import com.fs.common.annotation.Log;
 import com.fs.common.constant.UserConstants;
 import com.fs.common.core.controller.BaseController;
@@ -179,6 +180,7 @@ public class CompanyUserController extends BaseController {
         return getDataTable(list);
     }
     @GetMapping("/qwList")
+    @DataScope(deptAlias = "u", userAlias = "u")
     public TableDataInfo qwList(CompanyUserQwParam user) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         user.setCompanyId(loginUser.getCompany().getCompanyId());

+ 89 - 62
fs-company/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -15,6 +15,8 @@ import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Before;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 
 import java.lang.reflect.Method;
@@ -22,14 +24,16 @@ import java.lang.reflect.Method;
 /**
  * 数据过滤处理
  *
-
+ *
  */
 @Aspect
 @Component
 public class DataScopeAspect
 {
+    private static final Logger log = LoggerFactory.getLogger(DataScopeAspect.class);
+
     /**
-     * 全部数据权限
+     * 全部数据权限(总经理/物流经理)
      */
     public static final String DATA_SCOPE_ALL = "1";
 
@@ -49,17 +53,17 @@ public class DataScopeAspect
     public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
 
     /**
-     * 仅本人数据权限
+     * 仅本人数据权限(组员)
      */
     public static final String DATA_SCOPE_SELF = "5";
 
     /**
-     * 组长级数据权限(查看自己和下属的数据)
+     * 组长级数据权限(组长:查看自己和所有组员的数据)
      */
     public static final String DATA_SCOPE_LEADER = "6";
 
     /**
-     * 销售经理级数据权限(查看自己和所有下属及下属的下属的数据)
+     * 销售经理级数据权限(销售经理:查看自己、所有组长、所有组员的数据)
      */
     public static final String DATA_SCOPE_MANAGER = "7";
 
@@ -68,7 +72,6 @@ public class DataScopeAspect
      */
     public static final String DATA_SCOPE = "dataScope";
 
-    // 配置织入点
     @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
     public void dataScopePointCut()
     {
@@ -82,18 +85,15 @@ public class DataScopeAspect
 
     protected void handleDataScope(final JoinPoint joinPoint)
     {
-        // 获得注解
         DataScope controllerDataScope = getAnnotationLog(joinPoint);
         if (controllerDataScope == null)
         {
             return;
         }
-        // 获取当前的用户
         LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
         if (StringUtils.isNotNull(loginUser))
         {
             CompanyUser currentUser = loginUser.getUser();
-            // 如果是超级管理员,则不过滤数据
             if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
             {
                 dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
@@ -107,88 +107,115 @@ public class DataScopeAspect
      *
      * @param joinPoint 切点
      * @param user 用户
-     * @param userAlias 别名
+     * @param deptAlias 部门表别名
+     * @param userAlias 用户表别名
      */
     public static void dataScopeFilter(JoinPoint joinPoint, CompanyUser user, String deptAlias, String userAlias)
     {
         StringBuilder sqlString = new StringBuilder();
 
-        for (CompanyRole role : user.getRoles())
+        log.info("=== 数据权限过滤开始 ===");
+        log.info("用户ID: {}, 用户名: {}, 公司ID: {}", user.getUserId(), user.getUserName(), user.getCompanyId());
+        log.info("用户角色数量: {}", user.getRoles() != null ? user.getRoles().size() : 0);
+
+        if (user.getRoles() != null)
         {
-            String dataScope = role.getDataScope();
-            if (DATA_SCOPE_ALL.equals(dataScope))
+            for (CompanyRole role : user.getRoles())
             {
-                sqlString = new StringBuilder();
-                break;
-            }
-            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
-            {
-                sqlString.append(StringUtils.format(
-                        " OR {}.dept_id IN ( SELECT dept_id FROM company_role_dept WHERE role_id = {} ) ", deptAlias,
-                        role.getRoleId()));
-            }
-            else if (DATA_SCOPE_DEPT.equals(dataScope))
-            {
-                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
-            }
-            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
-            {
-                sqlString.append(StringUtils.format(
-                        " OR {}.dept_id IN ( SELECT dept_id FROM company_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
-                        deptAlias, user.getDeptId(), user.getDeptId()));
-            }
-            else if (DATA_SCOPE_SELF.equals(dataScope))
-            {
-                if (StringUtils.isNotBlank(userAlias))
+                String dataScope = role.getDataScope();
+                log.info("角色: {}, roleKey: {}, dataScope: {}", role.getRoleName(), role.getRoleKey(), dataScope);
+
+                if (DATA_SCOPE_ALL.equals(dataScope))
                 {
-                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                    log.info("角色 {} 拥有全部数据权限,跳过过滤", role.getRoleName());
+                    sqlString = new StringBuilder();
+                    break;
                 }
-                else
+                else if (DATA_SCOPE_CUSTOM.equals(dataScope))
                 {
-                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
-                    //sqlString.append(" OR 1=0 ");
-                        sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+                    sqlString.append(StringUtils.format(
+                            " OR {}.dept_id IN ( SELECT dept_id FROM company_role_dept WHERE role_id = {} ) ", deptAlias,
+                            role.getRoleId()));
                 }
-            }
-            else if (DATA_SCOPE_LEADER.equals(dataScope))
-            {
-                if (StringUtils.isNotBlank(userAlias))
+                else if (DATA_SCOPE_DEPT.equals(dataScope))
                 {
-                    sqlString.append(StringUtils.format(
-                            " OR {}.user_id = {} OR {}.parent_id = {} ",
-                            userAlias, user.getUserId(), userAlias, user.getUserId()));
+                    sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
                 }
-            }
-            else if (DATA_SCOPE_MANAGER.equals(dataScope))
-            {
-                if (StringUtils.isNotBlank(userAlias))
+                else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
                 {
                     sqlString.append(StringUtils.format(
-                            " OR {}.user_id = {} " +
-                            " OR {}.parent_id = {} " +
-                            " OR {}.user_id IN (SELECT u.user_id FROM company_user u WHERE u.parent_id IN " +
-                            "(SELECT cu.user_id FROM company_user cu WHERE cu.parent_id = {})) ",
-                            userAlias, user.getUserId(),
-                            userAlias, user.getUserId(),
-                            userAlias, user.getUserId()));
+                            " OR {}.dept_id IN ( SELECT dept_id FROM company_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                            deptAlias, user.getDeptId(), user.getDeptId()));
+                }
+                else if (DATA_SCOPE_SELF.equals(dataScope))
+                {
+                    if (StringUtils.isNotBlank(userAlias))
+                    {
+                        sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                    }
+                    else
+                    {
+                        sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+                    }
+                }
+                else if (DATA_SCOPE_LEADER.equals(dataScope))
+                {
+                    if (StringUtils.isNotBlank(userAlias))
+                    {
+                        sqlString.append(StringUtils.format(
+                                " OR {}.user_id = {} " +
+                                " OR {}.user_id IN ( " +
+                                "   SELECT ur.user_id FROM company_user_role ur " +
+                                "   INNER JOIN company_role r ON ur.role_id = r.role_id " +
+                                "   WHERE r.company_id = {} " +
+                                "   AND (r.role_key IN ('member', 'zuyuan') OR r.role_name LIKE '%组员%') " +
+                                ") ",
+                                userAlias, user.getUserId(),
+                                userAlias,
+                                user.getCompanyId()));
+                    }
+                }
+                else if (DATA_SCOPE_MANAGER.equals(dataScope))
+                {
+                    if (StringUtils.isNotBlank(userAlias))
+                    {
+                        sqlString.append(StringUtils.format(
+                                " OR {}.user_id = {} " +
+                                " OR {}.user_id IN ( " +
+                                "   SELECT ur.user_id FROM company_user_role ur " +
+                                "   INNER JOIN company_role r ON ur.role_id = r.role_id " +
+                                "   WHERE r.company_id = {} " +
+                                "   AND (r.role_key IN ('leader', 'zuzhang', 'member', 'zuyuan') " +
+                                "        OR r.role_name LIKE '%组长%' OR r.role_name LIKE '%组员%') " +
+                                ") ",
+                                userAlias, user.getUserId(),
+                                userAlias,
+                                user.getCompanyId()));
+                    }
                 }
             }
         }
 
         if (StringUtils.isNotBlank(sqlString.toString()))
         {
+            String finalSql = " AND (" + sqlString.substring(4) + ")";
+            log.info("生成的权限SQL: {}", finalSql);
+
             Object params = joinPoint.getArgs()[0];
             if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
             {
                 BaseEntity baseEntity = (BaseEntity) params;
-                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+                baseEntity.getParams().put(DATA_SCOPE, finalSql);
             }
         }
+        else
+        {
+            log.info("未生成权限SQL,可能是全部数据权限或无角色");
+        }
+
+        log.info("=== 数据权限过滤结束 ===");
     }
 
-    /**
-     * 是否存在注解,如果存在就获取
-     */
     private DataScope getAnnotationLog(JoinPoint joinPoint)
     {
         Signature signature = joinPoint.getSignature();

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

@@ -1428,7 +1428,7 @@ public class CompanyUserServiceImpl implements ICompanyUserService
                 if ("admin".equals(roleKey) || "general_manager".equals(roleKey) || "logistics_manager".equals(roleKey) ||
                     (roleName != null && (roleName.contains("管理员") || roleName.contains("总经理") || roleName.contains("物流经理")))) {
                     adminRoleId = role.getRoleId();
-                } else if ("leader".equals(roleKey) || "zuzhang".equals(roleKey) || 
+                    } else if ("leader".equals(roleKey) || "zuzhang".equals(roleKey) ||
                     (roleName != null && roleName.contains("组长"))) {
                     leaderRoleId = role.getRoleId();
                 } else if ("sales_manager".equals(roleKey) || "xiaoshoujingli".equals(roleKey) || 

+ 39 - 79
fs-service/src/main/java/com/fs/his/mapper/AppOperationReportMapper.java

@@ -1,101 +1,61 @@
 package com.fs.his.mapper;
 
 import com.fs.his.param.AppOperationReportParam;
+import com.fs.his.vo.AppDailyReportVO;
 import com.fs.his.vo.AppOperationReportVO;
 import org.apache.ibatis.annotations.Param;
 
 import java.math.BigDecimal;
+import java.util.List;
 
 /**
  * App运营报表Mapper接口
  */
 public interface AppOperationReportMapper {
 
-    /**
-     * 查询新增用户数
-     * @param startDate 开始时间
-     * @param endDate 结束时间
-     * @return 新增用户数
-     */
-    Long selectNewUsers(@Param("startDate") String startDate, @Param("endDate") String endDate);
-
-    /**
-     * 查询累计注册用户数
-     * @return 累计注册用户数
-     */
-    Long selectTotalUsers();
-
-    /**
-     * 查询活跃用户数(有看课记录的app用户)
-     * @param startDate 开始时间
-     * @param endDate 结束时间
-     * @return 活跃用户数
-     */
-    Long selectActiveUsers(@Param("startDate") String startDate, @Param("endDate") String endDate);
-
-    /**
-     * 查询留存用户数
-     * @param newUserStartDate 新增用户开始时间
-     * @param newUserEndDate 新增用户结束时间
-     * @param activeStartDate 活跃用户开始时间
-     * @param activeEndDate 活跃用户结束时间
-     * @return 留存用户数
-     */
+    Long selectNewUsers(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    Long selectTotalUsers(@Param("appVersion") String appVersion);
+
+    Long selectActiveUsers(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
     Long selectRetainedUsers(@Param("newUserStartDate") String newUserStartDate,
                               @Param("newUserEndDate") String newUserEndDate,
                               @Param("activeStartDate") String activeStartDate,
-                              @Param("activeEndDate") String activeEndDate);
-
-    /**
-     * 查询总看课时长(秒)
-     * @param startDate 开始时间
-     * @param endDate 结束时间
-     * @return 总看课时长
-     */
-    Long selectTotalWatchDuration(@Param("startDate") String startDate, @Param("endDate") String endDate);
-
-    /**
-     * 查询所有订单总金额(store_order、package_order、inquiry_order、integral_order)
-     * @param endDate 截止时间
-     * @return 订单总金额
-     */
-    BigDecimal selectTotalOrderAmount(@Param("endDate") String endDate);
-
-    /**
-     * 查询红包总金额(app用户)
-     * @param startDate 开始时间
-     * @param endDate 结束时间
-     * @return 红包总金额
-     */
-    BigDecimal selectTotalRedPacketAmount(@Param("startDate") String startDate, @Param("endDate") String endDate);
-
-    /**
-     * 查询上个月活跃用户数
-     * @param lastMonthStart 上个月开始时间
-     * @param lastMonthEnd 上个月结束时间
-     * @return 上个月活跃用户数
-     */
+                              @Param("activeEndDate") String activeEndDate,
+                              @Param("appVersion") String appVersion);
+
+    Long selectTotalWatchDuration(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    BigDecimal selectTotalOrderAmount(@Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    BigDecimal selectTotalRedPacketAmount(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
     Long selectLastMonthActiveUsers(@Param("lastMonthStart") String lastMonthStart,
-                                     @Param("lastMonthEnd") String lastMonthEnd);
-
-    /**
-     * 查询本月未活跃用户数(上月活跃但本月未活跃)
-     * @param lastMonthStart 上个月开始时间
-     * @param lastMonthEnd 上个月结束时间
-     * @param currentMonthStart 本月开始时间
-     * @param currentMonthEnd 本月结束时间
-     * @return 本月未活跃用户数
-     */
+                                     @Param("lastMonthEnd") String lastMonthEnd,
+                                     @Param("appVersion") String appVersion);
+
     Long selectCurrentMonthInactiveUsers(@Param("lastMonthStart") String lastMonthStart,
                                           @Param("lastMonthEnd") String lastMonthEnd,
                                           @Param("currentMonthStart") String currentMonthStart,
-                                          @Param("currentMonthEnd") String currentMonthEnd);
-
-    /**
-     * 查询月度报表基础信息
-     * @param year 年份
-     * @param month 月份
-     * @return 月度报表基础信息
-     */
+                                          @Param("currentMonthEnd") String currentMonthEnd,
+                                          @Param("appVersion") String appVersion);
+
     AppOperationReportVO selectMonthReport(@Param("year") Integer year, @Param("month") Integer month);
+
+    List<AppDailyReportVO> selectDailyNewUsers(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    List<AppDailyReportVO> selectDailyActiveUsers(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    List<AppDailyReportVO> selectDailyWatchDuration(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    List<AppDailyReportVO> selectDailyRedPacketAmount(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("appVersion") String appVersion);
+
+    Long selectNextDayRetainedUsers(@Param("prevDate") String prevDate, @Param("currentDate") String currentDate, @Param("appVersion") String appVersion);
+
+    Long selectDayNewUsers(@Param("date") String date, @Param("appVersion") String appVersion);
+
+    Long selectPrevDayActiveUsers(@Param("prevDate") String prevDate, @Param("appVersion") String appVersion);
+
+    Long selectDailyChurnUsers(@Param("prevDate") String prevDate, @Param("currentDate") String currentDate, @Param("appVersion") String appVersion);
 }

+ 10 - 3
fs-service/src/main/java/com/fs/his/param/AppOperationReportParam.java

@@ -8,12 +8,19 @@ import lombok.Data;
 @Data
 public class AppOperationReportParam {
 
-    /** 年份 */
     private Integer year;
 
-    /** 月份 */
     private Integer month;
 
-    /** 留存天数(默认7天) */
     private Integer retentionDays;
+
+    private String startDate;
+
+    private String endDate;
+
+    private Integer pageNum;
+
+    private Integer pageSize;
+
+    private String appVersion;
 }

+ 18 - 0
fs-service/src/main/java/com/fs/his/service/IAppOperationReportService.java

@@ -1,7 +1,11 @@
 package com.fs.his.service;
 
 import com.fs.his.param.AppOperationReportParam;
+import com.fs.his.vo.AppDailyReportVO;
 import com.fs.his.vo.AppOperationReportVO;
+import com.fs.his.vo.AppWeeklyReportVO;
+
+import java.util.List;
 
 /**
  * App运营报表Service接口
@@ -14,4 +18,18 @@ public interface IAppOperationReportService {
      * @return 月度运营报表数据
      */
     AppOperationReportVO getMonthReport(AppOperationReportParam param);
+
+    /**
+     * 获取日期范围内每日运营报表
+     * @param param 查询参数(startDate、endDate)
+     * @return 每日运营报表数据列表
+     */
+    List<AppDailyReportVO> getDailyReport(AppOperationReportParam param);
+
+    /**
+     * 获取日期范围内每周运营报表
+     * @param param 查询参数(startDate、endDate)
+     * @return 每周运营报表数据列表
+     */
+    List<AppWeeklyReportVO> getWeeklyReport(AppOperationReportParam param);
 }

+ 484 - 23
fs-service/src/main/java/com/fs/his/service/impl/AppOperationReportServiceImpl.java

@@ -4,16 +4,28 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.his.mapper.AppOperationReportMapper;
 import com.fs.his.param.AppOperationReportParam;
 import com.fs.his.service.IAppOperationReportService;
+import com.fs.his.vo.AppDailyReportVO;
 import com.fs.his.vo.AppOperationReportVO;
+import com.fs.his.vo.AppWeeklyReportVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
-import java.util.Calendar;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * APP运营报表Service实现类
+ * 
+ * 提供三种维度的运营数据报表:
+ * 1. 月度报表 - 按月统计各项运营指标
+ * 2. 日报表 - 按日统计各项运营指标,支持分页
+ * 3. 周报表 - 按周统计各项运营指标,支持分页
+ * 
+ * 支持按APP版本筛选数据(动态参数)
+ */
 @Service
 public class AppOperationReportServiceImpl implements IAppOperationReportService {
 
@@ -27,11 +39,23 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
 
     /**
      * 获取月度运营报表
-     * @param param 查询参数(year、month、retentionDays)
+     * 
+     * 统计指标说明:
+     * - 新增用户数:当月新注册的APP用户数
+     * - 累计用户数:截至当月末的APP总用户数
+     * - 活跃用户数:当月有使用行为的用户数
+     * - 留存率:N天后仍活跃的用户占新增用户的比例
+     * - 平均使用时长:活跃用户的平均使用时长(分钟)
+     * - LTV(用户生命周期价值):累计订单金额/累计用户数
+     * - CAC(用户获取成本):红包发放金额/新增用户数
+     * - 月流失率:上月活跃用户中本月未活跃的比例
+     * 
+     * @param param 查询参数(year、month、retentionDays、appVersion)
      * @return 月度运营报表数据
      */
     @Override
     public AppOperationReportVO getMonthReport(AppOperationReportParam param) {
+        // 1. 解析年月参数,默认为当前年月
         Integer year = param.getYear();
         Integer month = param.getMonth();
 
@@ -44,70 +68,503 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
             month = cal.get(Calendar.MONTH) + 1;
         }
 
-        String cacheKey = CACHE_PREFIX + year + ":" + month;
+        // 2. 构建缓存key(包含版本参数,确保不同版本筛选使用不同缓存)
+        String appVersion = param.getAppVersion();
+        String cacheKey = CACHE_PREFIX + year + ":" + month + ":" + (appVersion != null ? appVersion : "all");
         AppOperationReportVO cachedVo = redisCache.getCacheObject(cacheKey);
         if (cachedVo != null) {
             return cachedVo;
         }
 
+        // 3. 计算月份起止日期
         String monthStart = year + "-" + String.format("%02d", month) + "-01";
         String monthEnd = year + "-" + String.format("%02d", month) + "-" + getLastDayOfMonth(year, month);
 
+        // 4. 构建返回对象
         AppOperationReportVO vo = new AppOperationReportVO();
         vo.setYear(year);
         vo.setMonth(month);
         vo.setPeriod("月度统计");
 
-        Long newUsers = appOperationReportMapper.selectNewUsers(monthStart + " 00:00:00", monthEnd + " 23:59:59");
+        // 5. 查询新增用户数
+        Long newUsers = appOperationReportMapper.selectNewUsers(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
         vo.setNewUsers(newUsers);
 
-        Long totalUsers = appOperationReportMapper.selectTotalUsers();
+        // 6. 查询累计用户数
+        Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
         vo.setTotalUsers(totalUsers);
 
-        Long activeUsers = appOperationReportMapper.selectActiveUsers(monthStart, monthEnd);
+        // 7. 查询活跃用户数
+        Long activeUsers = appOperationReportMapper.selectActiveUsers(monthStart, monthEnd, appVersion);
         vo.setActiveUsers(activeUsers);
 
+        // 8. 计算留存率(默认30日留存)
         if (newUsers != null && newUsers > 0) {
             int retentionDays = param.getRetentionDays() != null ? param.getRetentionDays() : 30;
             Long retained = appOperationReportMapper.selectRetainedUsers(
                     monthStart + " 00:00:00", monthEnd + " 23:59:59",
-                    getAfterDate(monthStart, retentionDays) + " 00:00:00", getAfterDate(monthEnd, retentionDays) + " 23:59:59");
+                    getAfterDate(monthStart, retentionDays) + " 00:00:00", getAfterDate(monthEnd, retentionDays) + " 23:59:59", appVersion);
             vo.setRetentionRate(calculateRate(retained, newUsers));
             vo.setRetentionDays(retentionDays);
         }
 
+        // 9. 计算平均使用时长(分钟)
         if (activeUsers != null && activeUsers > 0) {
-            Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(monthStart + " 00:00:00", monthEnd + " 23:59:59");
+            Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
             BigDecimal avgDuration = new BigDecimal(totalDuration).divide(new BigDecimal(activeUsers), 2, RoundingMode.HALF_UP);
             vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
         }
 
+        // 10. 计算LTV(用户生命周期价值)
         if (totalUsers != null && totalUsers > 0) {
-            BigDecimal totalAmount = appOperationReportMapper.selectTotalOrderAmount(monthEnd + " 23:59:59");
+            BigDecimal totalAmount = appOperationReportMapper.selectTotalOrderAmount(monthEnd + " 23:59:59", appVersion);
             vo.setLtv(totalAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
         }
 
+        // 11. 计算CAC(用户获取成本)
         if (newUsers != null && newUsers > 0) {
-            BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(monthStart + " 00:00:00", monthEnd + " 23:59:59");
+            BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(monthStart + " 00:00:00", monthEnd + " 23:59:59", appVersion);
             vo.setCac(redPacketAmount.divide(new BigDecimal(newUsers), 2, RoundingMode.HALF_UP));
         }
 
+        // 12. 计算月流失率
         String[] lastMonthDates = getLastMonthDateRange(year, month);
-        Long lastMonthActive = appOperationReportMapper.selectLastMonthActiveUsers(lastMonthDates[0], lastMonthDates[1]);
+        Long lastMonthActive = appOperationReportMapper.selectLastMonthActiveUsers(lastMonthDates[0], lastMonthDates[1], appVersion);
         if (lastMonthActive != null && lastMonthActive > 0) {
             Long currentMonthInactive = appOperationReportMapper.selectCurrentMonthInactiveUsers(
-                    lastMonthDates[0], lastMonthDates[1], monthStart, monthEnd);
+                    lastMonthDates[0], lastMonthDates[1], monthStart, monthEnd, appVersion);
             vo.setMonthlyChurnRate(calculateRate(currentMonthInactive, lastMonthActive));
         }
 
+        // 13. 写入缓存
         int cacheMinutes = isCurrentMonth(year, month) ? 5 : 60;
         redisCache.setCacheObject(cacheKey, vo, cacheMinutes, TimeUnit.MINUTES);
 
         return vo;
     }
 
+    /**
+     * 获取日运营报表
+     * 
+     * 统计指标说明:
+     * - 新增用户数:当日新注册的APP用户数
+     * - 累计用户数:截至当日的APP总用户数
+     * - 活跃用户数:当日有使用行为的用户数
+     * - 次日留存率:当日新增用户中次日仍活跃的比例
+     * - 平均使用时长:活跃用户的平均使用时长(分钟)
+     * - LTV(用户生命周期价值):累计订单金额/累计用户数
+     * - CAC(用户获取成本):当日红包发放金额/当日新增用户数
+     * - 日流失率:前日活跃用户中当日未活跃的比例
+     * 
+     * @param param 查询参数(startDate、endDate、pageNum、pageSize、appVersion)
+     * @return 日运营报表数据列表
+     */
+    @Override
+    public List<AppDailyReportVO> getDailyReport(AppOperationReportParam param) {
+        // 1. 解析日期参数,默认查询近30天
+        String startDate = param.getStartDate();
+        String endDate = param.getEndDate();
+        String appVersion = param.getAppVersion();
+
+        if (startDate == null || endDate == null) {
+            Calendar cal = Calendar.getInstance();
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            endDate = sdf.format(cal.getTime());
+            cal.add(Calendar.DAY_OF_MONTH, -29);
+            startDate = sdf.format(cal.getTime());
+        }
+
+        // 2. 批量查询各维度数据(减少数据库查询次数)
+        List<AppDailyReportVO> dailyNewUsers = appOperationReportMapper.selectDailyNewUsers(startDate, endDate, appVersion);
+        List<AppDailyReportVO> dailyActiveUsers = appOperationReportMapper.selectDailyActiveUsers(startDate, endDate, appVersion);
+        List<AppDailyReportVO> dailyWatchDuration = appOperationReportMapper.selectDailyWatchDuration(startDate, endDate, appVersion);
+        List<AppDailyReportVO> dailyRedPacketAmount = appOperationReportMapper.selectDailyRedPacketAmount(startDate, endDate, appVersion);
+
+        // 3. 构建日期到数据的映射(便于后续按日期查找)
+        Map<String, AppDailyReportVO> newUsersMap = new LinkedHashMap<>();
+        for (AppDailyReportVO vo : dailyNewUsers) {
+            String key = formatDate(vo.getStatDate());
+            newUsersMap.put(key, vo);
+        }
+
+        Map<String, AppDailyReportVO> activeUsersMap = new LinkedHashMap<>();
+        for (AppDailyReportVO vo : dailyActiveUsers) {
+            String key = formatDate(vo.getStatDate());
+            activeUsersMap.put(key, vo);
+        }
+
+        Map<String, AppDailyReportVO> watchDurationMap = new LinkedHashMap<>();
+        for (AppDailyReportVO vo : dailyWatchDuration) {
+            String key = formatDate(vo.getStatDate());
+            watchDurationMap.put(key, vo);
+        }
+
+        Map<String, AppDailyReportVO> redPacketMap = new LinkedHashMap<>();
+        for (AppDailyReportVO vo : dailyRedPacketAmount) {
+            String key = formatDate(vo.getStatDate());
+            redPacketMap.put(key, vo);
+        }
+
+        // 4. 查询累计数据(截至结束日期)
+        Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
+        BigDecimal totalOrderAmount = appOperationReportMapper.selectTotalOrderAmount(endDate + " 23:59:59", appVersion);
+
+        // 5. 生成日期范围内所有日期列表
+        List<String> allDates = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        try {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(startDate));
+            Calendar endCal = Calendar.getInstance();
+            endCal.setTime(sdf.parse(endDate));
+            while (!cal.after(endCal)) {
+                allDates.add(sdf.format(cal.getTime()));
+                cal.add(Calendar.DAY_OF_MONTH, 1);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        // 6. 分页处理
+        int totalDays = allDates.size();
+        Integer pageNum = param.getPageNum();
+        Integer pageSize = param.getPageSize();
+
+        List<String> pageDates;
+        if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
+            int fromIndex = (pageNum - 1) * pageSize;
+            int toIndex = Math.min(fromIndex + pageSize, totalDays);
+            if (fromIndex >= totalDays) {
+                pageDates = new ArrayList<>();
+            } else {
+                pageDates = allDates.subList(fromIndex, toIndex);
+            }
+        } else {
+            pageDates = allDates;
+        }
+
+        // 7. 遍历每条日期,组装报表数据
+        List<AppDailyReportVO> result = new ArrayList<>();
+        for (String dateStr : pageDates) {
+            AppDailyReportVO vo = new AppDailyReportVO();
+            try {
+                vo.setStatDate(sdf.parse(dateStr));
+            } catch (Exception e) {
+                continue;
+            }
+
+            // 7.1 设置新增用户数
+            AppDailyReportVO newVo = newUsersMap.get(dateStr);
+            vo.setNewUsers(newVo != null ? newVo.getNewUsers() : 0L);
+
+            // 7.2 设置累计用户数
+            vo.setTotalUsers(totalUsers);
+
+            // 7.3 设置活跃用户数
+            AppDailyReportVO activeVo = activeUsersMap.get(dateStr);
+            vo.setActiveUsers(activeVo != null ? activeVo.getActiveUsers() : 0L);
+
+            // 7.4 计算次日留存率
+            String prevDate = getPrevDate(dateStr);
+            Long dayNewUsers = appOperationReportMapper.selectDayNewUsers(dateStr, appVersion);
+            if (dayNewUsers != null && dayNewUsers > 0) {
+                String nextDate = getNextDate(dateStr);
+                Long nextDayRetained = appOperationReportMapper.selectNextDayRetainedUsers(dateStr, nextDate, appVersion);
+                vo.setNextDayRetentionRate(calculateRate(nextDayRetained, dayNewUsers));
+            } else {
+                vo.setNextDayRetentionRate(BigDecimal.ZERO);
+            }
+
+            // 7.5 计算平均使用时长
+            AppDailyReportVO watchVo = watchDurationMap.get(dateStr);
+            Long dayDuration = (watchVo != null && watchVo.getTotalWatchDuration() != null) ? watchVo.getTotalWatchDuration() : 0L;
+            if (vo.getActiveUsers() != null && vo.getActiveUsers() > 0) {
+                BigDecimal avgDuration = new BigDecimal(dayDuration)
+                        .divide(new BigDecimal(vo.getActiveUsers()), 2, RoundingMode.HALF_UP);
+                vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setAvgUseDuration(BigDecimal.ZERO);
+            }
+
+            // 7.6 计算LTV
+            if (totalUsers != null && totalUsers > 0 && totalOrderAmount != null) {
+                vo.setLtv(totalOrderAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setLtv(BigDecimal.ZERO);
+            }
+
+            // 7.7 计算CAC
+            AppDailyReportVO redVo = redPacketMap.get(dateStr);
+            BigDecimal dayRedPacket = (redVo != null && redVo.getTotalRedPacketAmount() != null) ? redVo.getTotalRedPacketAmount() : BigDecimal.ZERO;
+            if (vo.getNewUsers() != null && vo.getNewUsers() > 0) {
+                vo.setCac(dayRedPacket.divide(new BigDecimal(vo.getNewUsers()), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setCac(BigDecimal.ZERO);
+            }
+
+            // 7.8 计算日流失率
+            Long prevDayActive = appOperationReportMapper.selectPrevDayActiveUsers(prevDate, appVersion);
+            if (prevDayActive != null && prevDayActive > 0) {
+                Long churnUsers = appOperationReportMapper.selectDailyChurnUsers(prevDate, dateStr, appVersion);
+                vo.setDailyChurnRate(calculateRate(churnUsers, prevDayActive));
+            } else {
+                vo.setDailyChurnRate(BigDecimal.ZERO);
+            }
+
+            result.add(vo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 获取周运营报表
+     * 
+     * 统计指标说明:
+     * - 新增用户数:当周新注册的APP用户数
+     * - 累计用户数:截至当周末的APP总用户数
+     * - 活跃用户数:当周有使用行为的用户数
+     * - 留存率:当周新增用户中下周仍活跃的比例
+     * - 平均使用时长:活跃用户的平均使用时长(分钟)
+     * - LTV(用户生命周期价值):累计订单金额/累计用户数
+     * - CAC(用户获取成本):当周红包发放金额/当周新增用户数
+     * - 周流失率:上周活跃用户中本周未活跃的比例
+     * 
+     * @param param 查询参数(startDate、endDate、pageNum、pageSize、appVersion)
+     * @return 周运营报表数据列表
+     */
+    @Override
+    public List<AppWeeklyReportVO> getWeeklyReport(AppOperationReportParam param) {
+        // 1. 解析日期参数,默认查询近90天
+        String startDate = param.getStartDate();
+        String endDate = param.getEndDate();
+        String appVersion = param.getAppVersion();
+
+        if (startDate == null || endDate == null) {
+            Calendar cal = Calendar.getInstance();
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            endDate = sdf.format(cal.getTime());
+            cal.add(Calendar.DAY_OF_MONTH, -89);
+            startDate = sdf.format(cal.getTime());
+        }
+
+        // 2. 将日期范围按周拆分
+        List<String[]> weeks = splitIntoWeeks(startDate, endDate);
+
+        // 3. 分页处理
+        Integer pageNum = param.getPageNum();
+        Integer pageSize = param.getPageSize();
+
+        List<String[]> pageWeeks;
+        if (pageNum != null && pageSize != null && pageNum > 0 && pageSize > 0) {
+            int totalWeeks = weeks.size();
+            int fromIndex = (pageNum - 1) * pageSize;
+            int toIndex = Math.min(fromIndex + pageSize, totalWeeks);
+            if (fromIndex >= totalWeeks) {
+                pageWeeks = new ArrayList<>();
+            } else {
+                pageWeeks = weeks.subList(fromIndex, toIndex);
+            }
+        } else {
+            pageWeeks = weeks;
+        }
+
+        // 4. 查询累计数据(截至结束日期)
+        Long totalUsers = appOperationReportMapper.selectTotalUsers(appVersion);
+        BigDecimal totalOrderAmount = appOperationReportMapper.selectTotalOrderAmount(endDate + " 23:59:59", appVersion);
+
+        // 5. 遍历每周,组装报表数据
+        List<AppWeeklyReportVO> result = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+
+        for (int i = 0; i < pageWeeks.size(); i++) {
+            String[] weekRange = pageWeeks.get(i);
+            String weekStart = weekRange[0];
+            String weekEnd = weekRange[1];
+
+            AppWeeklyReportVO vo = new AppWeeklyReportVO();
+            try {
+                vo.setWeekStart(sdf.parse(weekStart));
+                vo.setWeekEnd(sdf.parse(weekEnd));
+            } catch (Exception e) {
+                continue;
+            }
+
+            // 5.1 查询新增用户数
+            Long newUsers = appOperationReportMapper.selectNewUsers(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
+            vo.setNewUsers(newUsers);
+
+            // 5.2 设置累计用户数
+            vo.setTotalUsers(totalUsers);
+
+            // 5.3 查询活跃用户数
+            Long activeUsers = appOperationReportMapper.selectActiveUsers(weekStart, weekEnd, appVersion);
+            vo.setActiveUsers(activeUsers);
+
+            // 5.4 计算留存率(下周留存)
+            if (newUsers != null && newUsers > 0) {
+                String nextWeekStart = getNextDate(weekEnd);
+                String nextWeekEndDate = getAfterDate(nextWeekStart, 6);
+                Long retained = appOperationReportMapper.selectRetainedUsers(
+                        weekStart + " 00:00:00", weekEnd + " 23:59:59",
+                        nextWeekStart, nextWeekEndDate, appVersion);
+                vo.setRetentionRate(calculateRate(retained, newUsers));
+            } else {
+                vo.setRetentionRate(BigDecimal.ZERO);
+            }
+
+            // 5.5 计算平均使用时长
+            if (activeUsers != null && activeUsers > 0) {
+                Long totalDuration = appOperationReportMapper.selectTotalWatchDuration(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
+                BigDecimal avgDuration = new BigDecimal(totalDuration).divide(new BigDecimal(activeUsers), 2, RoundingMode.HALF_UP);
+                vo.setAvgUseDuration(avgDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setAvgUseDuration(BigDecimal.ZERO);
+            }
+
+            // 5.6 计算LTV
+            if (totalUsers != null && totalUsers > 0 && totalOrderAmount != null) {
+                vo.setLtv(totalOrderAmount.divide(new BigDecimal(totalUsers), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setLtv(BigDecimal.ZERO);
+            }
+
+            // 5.7 计算CAC
+            if (newUsers != null && newUsers > 0) {
+                BigDecimal redPacketAmount = appOperationReportMapper.selectTotalRedPacketAmount(weekStart + " 00:00:00", weekEnd + " 23:59:59", appVersion);
+                vo.setCac(redPacketAmount.divide(new BigDecimal(newUsers), 2, RoundingMode.HALF_UP));
+            } else {
+                vo.setCac(BigDecimal.ZERO);
+            }
+
+            // 5.8 计算周流失率
+            if (i > 0) {
+                // 非第一周:使用分页列表中的前一周数据
+                String[] prevWeekRange = pageWeeks.get(i - 1);
+                String prevWeekStart = prevWeekRange[0];
+                String prevWeekEnd = prevWeekRange[1];
+                Long prevWeekActive = appOperationReportMapper.selectLastMonthActiveUsers(prevWeekStart, prevWeekEnd, appVersion);
+                if (prevWeekActive != null && prevWeekActive > 0) {
+                    Long churnUsers = appOperationReportMapper.selectCurrentMonthInactiveUsers(
+                            prevWeekStart, prevWeekEnd, weekStart, weekEnd, appVersion);
+                    vo.setWeeklyChurnRate(calculateRate(churnUsers, prevWeekActive));
+                } else {
+                    vo.setWeeklyChurnRate(BigDecimal.ZERO);
+                }
+            } else {
+                // 第一周:需要查询前一周的数据
+                String prevWeekEnd = getPrevDate(weekStart);
+                String prevWeekStart = getAfterDate(prevWeekEnd, -6);
+                Long prevWeekActive = appOperationReportMapper.selectLastMonthActiveUsers(prevWeekStart, prevWeekEnd, appVersion);
+                if (prevWeekActive != null && prevWeekActive > 0) {
+                    Long churnUsers = appOperationReportMapper.selectCurrentMonthInactiveUsers(
+                            prevWeekStart, prevWeekEnd, weekStart, weekEnd, appVersion);
+                    vo.setWeeklyChurnRate(calculateRate(churnUsers, prevWeekActive));
+                } else {
+                    vo.setWeeklyChurnRate(BigDecimal.ZERO);
+                }
+            }
+
+            result.add(vo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 将日期范围按周拆分
+     * 从起始日期所在周的周一开始,到结束日期所在周的周日结束
+     * 
+     * @param startDate 起始日期
+     * @param endDate 结束日期
+     * @return 周列表,每个元素为[周开始日期, 周结束日期]
+     */
+    private List<String[]> splitIntoWeeks(String startDate, String endDate) {
+        List<String[]> weeks = new ArrayList<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        try {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(startDate));
+            // 调整到所在周的周一
+            cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+            Calendar endCal = Calendar.getInstance();
+            endCal.setTime(sdf.parse(endDate));
+
+            Calendar weekStart = (Calendar) cal.clone();
+            while (!weekStart.after(endCal)) {
+                // 周结束日期为周日(开始日期+6天)
+                Calendar weekEnd = (Calendar) weekStart.clone();
+                weekEnd.add(Calendar.DAY_OF_MONTH, 6);
+                // 如果超过结束日期,则使用结束日期
+                if (weekEnd.after(endCal)) {
+                    weekEnd = (Calendar) endCal.clone();
+                }
+                String ws = sdf.format(weekStart.getTime());
+                String we = sdf.format(weekEnd.getTime());
+                weeks.add(new String[]{ws, we});
+                // 移动到下一周
+                weekStart.add(Calendar.DAY_OF_MONTH, 7);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return weeks;
+    }
+
+    /**
+     * 格式化日期为字符串
+     * 
+     * @param date 日期对象
+     * @return 格式化后的日期字符串(yyyy-MM-dd)
+     */
+    private String formatDate(Date date) {
+        if (date == null) return "";
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        return sdf.format(date);
+    }
+
+    /**
+     * 获取前一天的日期
+     * 
+     * @param dateStr 当前日期字符串
+     * @return 前一天的日期字符串
+     */
+    private String getPrevDate(String dateStr) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(dateStr));
+            cal.add(Calendar.DAY_OF_MONTH, -1);
+            return sdf.format(cal.getTime());
+        } catch (Exception e) {
+            return dateStr;
+        }
+    }
+
+    /**
+     * 获取后一天的日期
+     * 
+     * @param dateStr 当前日期字符串
+     * @return 后一天的日期字符串
+     */
+    private String getNextDate(String dateStr) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(dateStr));
+            cal.add(Calendar.DAY_OF_MONTH, 1);
+            return sdf.format(cal.getTime());
+        } catch (Exception e) {
+            return dateStr;
+        }
+    }
+
     /**
      * 判断是否为当前月份
+     * 
+     * @param year 年份
+     * @param month 月份
+     * @return 是否为当前月份
      */
     private boolean isCurrentMonth(int year, int month) {
         Calendar cal = Calendar.getInstance();
@@ -115,10 +572,11 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
     }
 
     /**
-     * 计算比率
+     * 计算比率(百分比)
+     * 
      * @param part 部分值
      * @param total 总值
-     * @return 比率(百分比)
+     * @return 比率(百分比,保留两位小数
      */
     private BigDecimal calculateRate(Long part, Long total) {
         if (total == null || total == 0 || part == null) {
@@ -129,10 +587,11 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
     }
 
     /**
-     * 获取指定日期后的日期
+     * 获取指定日期后N天的日期
+     * 
      * @param date 基准日期
-     * @param days 天数
-     * @return 计算后的日期
+     * @param days 天数(可为负数,表示往前推)
+     * @return 计算后的日期字符串
      */
     private String getAfterDate(String date, int days) {
         try {
@@ -148,6 +607,7 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
 
     /**
      * 获取指定月份的最后一天
+     * 
      * @param year 年份
      * @param month 月份
      * @return 该月最后一天的日期(1-31)
@@ -162,22 +622,23 @@ public class AppOperationReportServiceImpl implements IAppOperationReportService
 
     /**
      * 获取上个月的日期范围
+     * 
      * @param year 当前年份
      * @param month 当前月份
-     * @return 上个月的开始日期和结束日期数组
+     * @return 数组,[上月开始日期, 上月结束日期]
      */
     private String[] getLastMonthDateRange(int year, int month) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
         Calendar cal = Calendar.getInstance();
         cal.set(Calendar.YEAR, year);
-        cal.set(Calendar.MONTH, month - 2);
+        cal.set(Calendar.MONTH, month - 1);
         cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.add(Calendar.DAY_OF_MONTH, -1);
+        String endDate = sdf.format(cal.getTime());
 
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        cal.set(Calendar.DAY_OF_MONTH, 1);
         String startDate = sdf.format(cal.getTime());
 
-        cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
-        String endDate = sdf.format(cal.getTime());
-
         return new String[]{startDate, endDate};
     }
 }

+ 47 - 0
fs-service/src/main/java/com/fs/his/vo/AppDailyReportVO.java

@@ -0,0 +1,47 @@
+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;
+
+/**
+ * App运营日报统计VO
+ */
+@Data
+public class AppDailyReportVO {
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "统计日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date statDate;
+
+    @Excel(name = "新增用户数")
+    private Long newUsers;
+
+    @Excel(name = "累计注册用户")
+    private Long totalUsers;
+
+    @Excel(name = "活跃用户数")
+    private Long activeUsers;
+
+    @Excel(name = "次日留存率(%)")
+    private BigDecimal nextDayRetentionRate;
+
+    @Excel(name = "用户平均使用时长(分钟)")
+    private BigDecimal avgUseDuration;
+
+    @Excel(name = "用户生命周期价值LTV(元)")
+    private BigDecimal ltv;
+
+    @Excel(name = "用户获取成本CAC(元)")
+    private BigDecimal cac;
+
+    @Excel(name = "日流失率(%)")
+    private BigDecimal dailyChurnRate;
+
+    private Long totalWatchDuration;
+
+    private BigDecimal totalRedPacketAmount;
+}

+ 47 - 0
fs-service/src/main/java/com/fs/his/vo/AppWeeklyReportVO.java

@@ -0,0 +1,47 @@
+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;
+
+/**
+ * App运营周报统计VO
+ */
+@Data
+public class AppWeeklyReportVO {
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "周开始日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date weekStart;
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "周结束日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date weekEnd;
+
+    @Excel(name = "新增用户数")
+    private Long newUsers;
+
+    @Excel(name = "累计注册用户")
+    private Long totalUsers;
+
+    @Excel(name = "活跃用户数")
+    private Long activeUsers;
+
+    @Excel(name = "留存率(%)")
+    private BigDecimal retentionRate;
+
+    @Excel(name = "用户平均使用时长(分钟)")
+    private BigDecimal avgUseDuration;
+
+    @Excel(name = "用户生命周期价值LTV(元)")
+    private BigDecimal ltv;
+
+    @Excel(name = "用户获取成本CAC(元)")
+    private BigDecimal cac;
+
+    @Excel(name = "周流失率(%)")
+    private BigDecimal weeklyChurnRate;
+}

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

@@ -734,7 +734,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             INNER JOIN (
             SELECT
             m.user_id AS member_user_id,
-            l.user_id AS leader_user_id
+            MIN(l.user_id) AS leader_user_id
             FROM company_user m
             INNER JOIN company_user_role mur ON m.user_id = mur.user_id AND mur.role_id = #{memberRoleId}
             LEFT JOIN company_user l ON l.dept_id = m.dept_id
@@ -744,7 +744,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHERE m.company_id = #{companyId}
             AND m.del_flag = '0'
             AND m.parent_id IS NULL
-            AND lur.user_id IS NOT NULL  -- 确保找到了组长
+            AND lur.user_id IS NOT NULL
+            GROUP BY m.user_id
             ) tmp ON cu.user_id = tmp.member_user_id
             SET cu.parent_id = tmp.leader_user_id
         WHERE tmp.leader_user_id IS NOT NULL
@@ -755,7 +756,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             INNER JOIN (
             SELECT
             l.user_id AS leader_user_id,
-            m.user_id AS manager_user_id
+            MIN(m.user_id) AS manager_user_id
             FROM company_user l
             INNER JOIN company_user_role lur ON l.user_id = lur.user_id AND lur.role_id = #{leaderRoleId}
             LEFT JOIN company_user m ON m.del_flag = '0'
@@ -766,12 +767,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHERE l.company_id = #{companyId}
             AND l.del_flag = '0'
             AND l.parent_id IS NULL
-            AND mur.user_id IS NOT NULL  -- 确保找到了销售经理
+            AND mur.user_id IS NOT NULL
             AND (
-            m.dept_id = ld.parent_id
-            OR FIND_IN_SET(md.dept_id, ld.ancestors)
-            OR m.dept_id = l.dept_id
+                m.dept_id = ld.parent_id
+                OR FIND_IN_SET(md.dept_id, ld.ancestors)
+                OR m.dept_id = l.dept_id
             )
+            GROUP BY l.user_id
             ) tmp ON cu.user_id = tmp.leader_user_id
             SET cu.parent_id = tmp.manager_user_id
         WHERE tmp.manager_user_id IS NOT NULL
@@ -782,7 +784,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             INNER JOIN (
             SELECT
             m.user_id AS manager_user_id,
-            a.user_id AS admin_user_id
+            MIN(a.user_id) AS admin_user_id
             FROM company_user m
             INNER JOIN company_user_role mur ON m.user_id = mur.user_id AND mur.role_id = #{managerRoleId}
             LEFT JOIN company_user a ON a.del_flag = '0'
@@ -791,7 +793,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             WHERE m.company_id = #{companyId}
             AND m.del_flag = '0'
             AND m.parent_id IS NULL
-            AND aur.user_id IS NOT NULL  -- 确保找到了管理员
+            AND aur.user_id IS NOT NULL
+            GROUP BY m.user_id
             ) tmp ON cu.user_id = tmp.manager_user_id
             SET cu.parent_id = tmp.admin_user_id
         WHERE tmp.admin_user_id IS NOT NULL

+ 306 - 0
fs-service/src/main/resources/mapper/his/AppOperationReportMapper.xml

@@ -4,6 +4,61 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.his.mapper.AppOperationReportMapper">
 
+    <!-- ==================== 公共SQL片段 ==================== -->
+    
+    <!-- APP版本过滤片段 - 用于用户表(u)的版本筛选 -->
+    <sql id="appVersionFilter">
+        <if test="appVersion != null and appVersion != ''">
+            AND u.user_id IN (
+                SELECT DISTINCT user_id FROM fs_app_user_behavior_daily WHERE app_version = #{appVersion}
+            )
+        </if>
+    </sql>
+
+    <!-- APP版本过滤片段 - 用于活跃用户表(a)的版本筛选 -->
+    <sql id="appVersionFilterActive">
+        <if test="appVersion != null and appVersion != ''">
+            AND a.user_id IN (
+                SELECT DISTINCT user_id FROM fs_app_user_behavior_daily WHERE app_version = #{appVersion}
+            )
+        </if>
+    </sql>
+
+    <!-- APP版本过滤片段 - 用于观看日志表(log)的版本筛选 -->
+    <sql id="appVersionFilterLog">
+        <if test="appVersion != null and appVersion != ''">
+            AND log.user_id IN (
+                SELECT DISTINCT user_id FROM fs_app_user_behavior_daily WHERE app_version = #{appVersion}
+            )
+        </if>
+    </sql>
+
+    <!-- APP版本过滤片段 - 用于红包记录表(r)的版本筛选 -->
+    <sql id="appVersionFilterRedPacket">
+        <if test="appVersion != null and appVersion != ''">
+            AND r.user_id IN (
+                SELECT DISTINCT user_id FROM fs_app_user_behavior_daily WHERE app_version = #{appVersion}
+            )
+        </if>
+    </sql>
+
+    <!-- APP版本过滤片段 - 用于订单表(o)的版本筛选 -->
+    <sql id="appVersionFilterOrder">
+        <if test="appVersion != null and appVersion != ''">
+            AND o.user_id IN (
+                SELECT DISTINCT user_id FROM fs_app_user_behavior_daily WHERE app_version = #{appVersion}
+            )
+        </if>
+    </sql>
+
+    <!-- ==================== 月度报表相关查询 ==================== -->
+
+    <!--
+        查询新增用户数
+        统计指定时间范围内新注册的APP用户数量
+        参数:startDate - 开始时间, endDate - 结束时间, appVersion - APP版本(可选)
+        返回:新增用户数量
+    -->
     <select id="selectNewUsers" resultType="Long">
         SELECT COUNT(DISTINCT u.user_id)
         FROM fs_user_company_user ucu
@@ -18,8 +73,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="endDate != null and endDate != ''">
             AND u.app_create_time &lt;= #{endDate}
         </if>
+        <include refid="appVersionFilter"/>
     </select>
 
+    <!--
+        查询累计用户数
+        统计截至当前的APP总用户数量
+        参数:appVersion - APP版本(可选)
+        返回:累计用户数量
+    -->
     <select id="selectTotalUsers" resultType="Long">
         SELECT COUNT(DISTINCT u.user_id)
         FROM fs_user_company_user ucu
@@ -28,8 +90,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND u.status = 1
         AND u.is_del = 0
         AND ucu.status IN (0, 1)
+        <include refid="appVersionFilter"/>
     </select>
 
+    <!--
+        查询活跃用户数
+        统计指定时间范围内有使用行为的用户数量
+        参数:startDate - 开始日期, endDate - 结束日期, appVersion - APP版本(可选)
+        返回:活跃用户数量
+    -->
     <select id="selectActiveUsers" resultType="Long">
         SELECT COUNT(DISTINCT a.user_id)
         FROM fs_app_active_user_daily a
@@ -39,8 +108,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND u.is_del = 0
         AND a.stat_date >= #{startDate}
         AND a.stat_date &lt;= #{endDate}
+        <include refid="appVersionFilterActive"/>
     </select>
 
+    <!--
+        查询留存用户数
+        统计在指定时间范围内注册,并在后续时间范围内仍活跃的用户数量
+        参数:newUserStartDate - 新用户注册开始时间, newUserEndDate - 新用户注册结束时间
+              activeStartDate - 活跃判定开始时间, activeEndDate - 活跃判定结束时间
+              appVersion - APP版本(可选)
+        返回:留存用户数量
+    -->
     <select id="selectRetainedUsers" resultType="Long">
         SELECT COUNT(DISTINCT u.user_id)
         FROM fs_user_company_user ucu
@@ -61,8 +139,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND a.stat_date >= #{activeStartDate}
             AND a.stat_date &lt;= #{activeEndDate}
         )
+        <include refid="appVersionFilter"/>
     </select>
 
+    <!--
+        查询总观看时长
+        统计指定时间范围内活跃用户的课程观看总时长(秒)
+        参数:startDate - 开始时间, endDate - 结束时间, appVersion - APP版本(可选)
+        返回:总观看时长(秒)
+    -->
     <select id="selectTotalWatchDuration" resultType="Long">
         SELECT IFNULL(SUM(log.duration), 0)
         FROM fs_course_watch_log log
@@ -78,10 +163,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND a.stat_date >= DATE(#{startDate})
             AND a.stat_date &lt;= DATE(#{endDate})
         )
+        <include refid="appVersionFilterLog"/>
     </select>
 
+    <!--
+        查询累计订单金额
+        统计截至指定时间的所有订单总金额(包含商城订单、套餐订单、问诊订单、积分订单)
+        参数:endDate - 截止时间, appVersion - APP版本(可选)
+        返回:累计订单金额
+    -->
     <select id="selectTotalOrderAmount" resultType="java.math.BigDecimal">
         SELECT IFNULL(SUM(amount), 0) FROM (
+            <!-- 商城订单 -->
             SELECT SUM(o.pay_money) AS amount
             FROM fs_store_order o
             WHERE o.status IN (1, 2, 3, 4)
@@ -94,7 +187,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 AND u.status = 1 AND u.is_del = 0
                 AND ucu.status IN (0, 1)
             )
+            <include refid="appVersionFilterOrder"/>
             UNION ALL
+            <!-- 套餐订单 -->
             SELECT SUM(o.pay_money) AS amount
             FROM fs_package_order o
             WHERE o.status IN (1, 2, 3, 4)
@@ -107,7 +202,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 AND u.status = 1 AND u.is_del = 0
                 AND ucu.status IN (0, 1)
             )
+            <include refid="appVersionFilterOrder"/>
             UNION ALL
+            <!-- 问诊订单 -->
             SELECT SUM(o.pay_money) AS amount
             FROM fs_inquiry_order o
             WHERE o.status IN (1, 2, 3, 4)
@@ -120,7 +217,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 AND u.status = 1 AND u.is_del = 0
                 AND ucu.status IN (0, 1)
             )
+            <include refid="appVersionFilterOrder"/>
             UNION ALL
+            <!-- 积分订单 -->
             SELECT SUM(o.pay_money) AS amount
             FROM fs_integral_order o
             WHERE o.status IN (1, 2, 3, 4)
@@ -133,9 +232,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 AND u.status = 1 AND u.is_del = 0
                 AND ucu.status IN (0, 1)
             )
+            <include refid="appVersionFilterOrder"/>
         ) t
     </select>
 
+    <!--
+        查询红包发放总金额
+        统计指定时间范围内发放的红包总金额
+        参数:startDate - 开始时间, endDate - 结束时间, appVersion - APP版本(可选)
+        返回:红包发放总金额
+    -->
     <select id="selectTotalRedPacketAmount" resultType="java.math.BigDecimal">
         SELECT IFNULL(SUM(r.amount), 0)
         FROM fs_course_red_packet_log r
@@ -149,8 +255,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND u.status = 1 AND u.is_del = 0
             AND ucu.status IN (0, 1)
         )
+        <include refid="appVersionFilterRedPacket"/>
     </select>
 
+    <!--
+        查询上月活跃用户数
+        用于计算月流失率的基础数据
+        参数:lastMonthStart - 上月开始日期, lastMonthEnd - 上月结束日期, appVersion - APP版本(可选)
+        返回:上月活跃用户数量
+    -->
     <select id="selectLastMonthActiveUsers" resultType="Long">
         SELECT COUNT(DISTINCT a.user_id)
         FROM fs_app_active_user_daily a
@@ -160,8 +273,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND u.is_del = 0
         AND a.stat_date >= #{lastMonthStart}
         AND a.stat_date &lt;= #{lastMonthEnd}
+        <include refid="appVersionFilterActive"/>
     </select>
 
+    <!--
+        查询当前月未活跃用户数(流失用户)
+        统计上月活跃但在当前月未活跃的用户数量,用于计算月流失率
+        参数:lastMonthStart - 上月开始日期, lastMonthEnd - 上月结束日期
+              currentMonthStart - 当前月开始日期, currentMonthEnd - 当前月结束日期
+              appVersion - APP版本(可选)
+        返回:流失用户数量
+    -->
     <select id="selectCurrentMonthInactiveUsers" resultType="Long">
         SELECT COUNT(DISTINCT a.user_id)
         FROM fs_app_active_user_daily a
@@ -181,8 +303,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND a2.stat_date >= #{currentMonthStart}
             AND a2.stat_date &lt;= #{currentMonthEnd}
         )
+        <include refid="appVersionFilterActive"/>
     </select>
 
+    <!--
+        查询月度报表基础信息(预留)
+        参数:year - 年份, month - 月份
+        返回:月度报表基础信息
+    -->
     <select id="selectMonthReport" resultType="com.fs.his.vo.AppOperationReportVO">
         SELECT 
             #{year} AS year,
@@ -190,4 +318,182 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             '月度统计' AS period
     </select>
 
+    <!-- ==================== 日报表相关查询 ==================== -->
+
+    <!--
+        查询每日新增用户数
+        按日期分组统计新增用户数量
+        参数:startDate - 开始日期, endDate - 结束日期, appVersion - APP版本(可选)
+        返回:每日新增用户数列表(statDate-日期, newUsers-新增用户数)
+    -->
+    <select id="selectDailyNewUsers" resultType="com.fs.his.vo.AppDailyReportVO">
+        SELECT DATE(u.app_create_time) AS statDate,
+               COUNT(DISTINCT u.user_id) AS newUsers
+        FROM fs_user_company_user ucu
+        INNER JOIN fs_user u ON ucu.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND ucu.status IN (0, 1)
+        AND DATE(u.app_create_time) >= #{startDate}
+        AND DATE(u.app_create_time) &lt;= #{endDate}
+        <include refid="appVersionFilter"/>
+        GROUP BY DATE(u.app_create_time)
+    </select>
+
+    <!--
+        查询每日活跃用户数
+        按日期分组统计活跃用户数量
+        参数:startDate - 开始日期, endDate - 结束日期, appVersion - APP版本(可选)
+        返回:每日活跃用户数列表(statDate-日期, activeUsers-活跃用户数)
+    -->
+    <select id="selectDailyActiveUsers" resultType="com.fs.his.vo.AppDailyReportVO">
+        SELECT a.stat_date AS statDate,
+               COUNT(DISTINCT a.user_id) AS activeUsers
+        FROM fs_app_active_user_daily a
+        INNER JOIN fs_user u ON a.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND a.stat_date >= #{startDate}
+        AND a.stat_date &lt;= #{endDate}
+        <include refid="appVersionFilterActive"/>
+        GROUP BY a.stat_date
+    </select>
+
+    <!--
+        查询每日观看时长
+        按日期分组统计课程观看总时长
+        参数:startDate - 开始日期, endDate - 结束日期, appVersion - APP版本(可选)
+        返回:每日观看时长列表(statDate-日期, totalWatchDuration-总时长秒)
+    -->
+    <select id="selectDailyWatchDuration" resultType="com.fs.his.vo.AppDailyReportVO">
+        SELECT DATE(log.create_time) AS statDate,
+               IFNULL(SUM(log.duration), 0) AS totalWatchDuration
+        FROM fs_course_watch_log log
+        WHERE DATE(log.create_time) >= #{startDate}
+        AND DATE(log.create_time) &lt;= #{endDate}
+        AND log.user_id IN (
+            SELECT DISTINCT a.user_id
+            FROM fs_app_active_user_daily a
+            INNER JOIN fs_user u ON a.user_id = u.user_id
+            WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+            AND u.status = 1 AND u.is_del = 0
+            AND a.stat_date = DATE(log.create_time)
+        )
+        <include refid="appVersionFilterLog"/>
+        GROUP BY DATE(log.create_time)
+    </select>
+
+    <!--
+        查询每日红包发放金额
+        按日期分组统计红包发放总金额
+        参数:startDate - 开始日期, endDate - 结束日期, appVersion - APP版本(可选)
+        返回:每日红包金额列表(statDate-日期, totalRedPacketAmount-总金额)
+    -->
+    <select id="selectDailyRedPacketAmount" resultType="com.fs.his.vo.AppDailyReportVO">
+        SELECT DATE(r.create_time) AS statDate,
+               IFNULL(SUM(r.amount), 0) AS totalRedPacketAmount
+        FROM fs_course_red_packet_log r
+        WHERE DATE(r.create_time) >= #{startDate}
+        AND DATE(r.create_time) &lt;= #{endDate}
+        AND EXISTS (
+            SELECT 1 FROM fs_user_company_user ucu
+            INNER JOIN fs_user u ON ucu.user_id = u.user_id
+            WHERE ucu.user_id = r.user_id
+            AND (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+            AND u.status = 1 AND u.is_del = 0
+            AND ucu.status IN (0, 1)
+        )
+        <include refid="appVersionFilterRedPacket"/>
+        GROUP BY DATE(r.create_time)
+    </select>
+
+    <!--
+        查询次日留存用户数
+        统计指定日期注册的用户中,次日仍活跃的用户数量
+        参数:prevDate - 注册日期, currentDate - 次日日期, appVersion - APP版本(可选)
+        返回:次日留存用户数量
+    -->
+    <select id="selectNextDayRetainedUsers" resultType="Long">
+        SELECT COUNT(DISTINCT u.user_id)
+        FROM fs_user_company_user ucu
+        INNER JOIN fs_user u ON ucu.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND ucu.status IN (0, 1)
+        AND DATE(u.app_create_time) = #{prevDate}
+        AND u.user_id IN (
+            SELECT DISTINCT a.user_id
+            FROM fs_app_active_user_daily a
+            INNER JOIN fs_user u2 ON a.user_id = u2.user_id
+            WHERE (u2.source IS NOT NULL OR u2.login_device IS NOT NULL OR u2.app_create_time IS NOT NULL)
+            AND u2.status = 1
+            AND u2.is_del = 0
+            AND a.stat_date = #{currentDate}
+        )
+        <include refid="appVersionFilter"/>
+    </select>
+
+    <!--
+        查询指定日期的新增用户数
+        参数:date - 日期, appVersion - APP版本(可选)
+        返回:新增用户数量
+    -->
+    <select id="selectDayNewUsers" resultType="Long">
+        SELECT COUNT(DISTINCT u.user_id)
+        FROM fs_user_company_user ucu
+        INNER JOIN fs_user u ON ucu.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND ucu.status IN (0, 1)
+        AND DATE(u.app_create_time) = #{date}
+        <include refid="appVersionFilter"/>
+    </select>
+
+    <!--
+        查询前一日活跃用户数
+        用于计算日流失率的基础数据
+        参数:prevDate - 前一日日期, appVersion - APP版本(可选)
+        返回:前一日活跃用户数量
+    -->
+    <select id="selectPrevDayActiveUsers" resultType="Long">
+        SELECT COUNT(DISTINCT a.user_id)
+        FROM fs_app_active_user_daily a
+        INNER JOIN fs_user u ON a.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND a.stat_date = #{prevDate}
+        <include refid="appVersionFilterActive"/>
+    </select>
+
+    <!--
+        查询日流失用户数
+        统计前一日活跃但在当前日未活跃的用户数量,用于计算日流失率
+        参数:prevDate - 前一日日期, currentDate - 当前日期, appVersion - APP版本(可选)
+        返回:日流失用户数量
+    -->
+    <select id="selectDailyChurnUsers" resultType="Long">
+        SELECT COUNT(DISTINCT a1.user_id)
+        FROM fs_app_active_user_daily a1
+        INNER JOIN fs_user u ON a1.user_id = u.user_id
+        WHERE (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        AND u.status = 1
+        AND u.is_del = 0
+        AND a1.stat_date = #{prevDate}
+        AND a1.user_id NOT IN (
+            SELECT DISTINCT a2.user_id
+            FROM fs_app_active_user_daily a2
+            INNER JOIN fs_user u2 ON a2.user_id = u2.user_id
+            WHERE (u2.source IS NOT NULL OR u2.login_device IS NOT NULL OR u2.app_create_time IS NOT NULL)
+            AND u2.status = 1
+            AND u2.is_del = 0
+            AND a2.stat_date = #{currentDate}
+        )
+        <include refid="appVersionFilterActive"/>
+    </select>
+
 </mapper>