Przeglądaj źródła

app用户启动次数,停留时长记录统计

wangxy 1 tydzień temu
rodzic
commit
417646defb

+ 115 - 0
fs-admin/src/main/java/com/fs/his/controller/UserBehaviorController.java

@@ -0,0 +1,115 @@
+package com.fs.his.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.his.dto.UserBehaviorReportDTO;
+import com.fs.his.param.UserBehaviorQueryParam;
+import com.fs.his.service.IUserBehaviorService;
+import com.fs.his.vo.UserBehaviorReportVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * APP用户行为统计Controller
+ * 
+ * 提供以下接口:
+ * 1. POST /his/userBehavior/report - 单条埋点上报
+ * 2. POST /his/userBehavior/reportBatch - 批量埋点上报
+ * 3. GET /his/userBehavior/summary - 获取汇总统计数据
+ * 4. GET /his/userBehavior/daily - 获取按日统计数据
+ * 
+ * 前端埋点说明(UniApp):
+ * 
+ * 1. APP启动时:
+ *    - eventType = 1
+ *    - sessionId = 生成一个唯一会话ID
+ *    - eventTime = 当前时间
+ *    - duration = 0或不传
+ * 
+ * 2. APP切后台时:
+ *    - eventType = 2
+ *    - sessionId = 与启动时相同的会话ID
+ *    - eventTime = 当前时间
+ *    - duration = 本次使用时长(秒)= 当前时间 - 启动时间
+ * 
+ * 注意:APP退出(杀进程)无法捕获,建议在onHide时上报时长
+ */
+@Api(tags = "APP用户行为统计")
+@RestController
+@RequestMapping("/his/userBehavior")
+public class UserBehaviorController extends BaseController {
+
+    @Autowired
+    private IUserBehaviorService userBehaviorService;
+
+    /**
+     * 单条埋点上报
+     * 前端在APP启动或切后台时调用此接口
+     * 
+     * @param dto 埋点数据
+     *            - userId: 用户ID(必填)
+     *            - sessionId: 会话ID,每次启动生成一个唯一ID(必填)
+     *            - eventType: 事件类型,1=启动,2=切后台(必填)
+     *            - eventTime: 事件发生时间(必填)
+     *            - duration: 使用时长,单位秒(切后台时必填)
+     * @return 操作结果
+     */
+    @ApiOperation("单条埋点上报")
+        @PostMapping("/report")
+    public AjaxResult report(@RequestBody UserBehaviorReportDTO dto) {
+        userBehaviorService.reportBehavior(dto);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 批量埋点上报
+     * 用于批量上传多条埋点数据,减少网络请求次数
+     * 
+     * @param dtoList 埋点数据列表
+     * @return 操作结果
+     */
+    @ApiOperation("批量埋点上报")
+    @PostMapping("/reportBatch")
+    public AjaxResult reportBatch(@RequestBody List<UserBehaviorReportDTO> dtoList) {
+        userBehaviorService.reportBehaviorBatch(dtoList);
+        return AjaxResult.success();
+    }
+
+    /**
+     * 获取汇总统计数据
+     * 返回整个时间范围的聚合指标
+     * 
+     * @param param 查询参数
+     *              - startDate: 开始日期,默认近30天
+     *              - endDate: 结束日期
+     *              - userType: 用户类型,1=新用户,2=老用户,不传=全部
+     * @return 汇总统计数据
+     */
+    @ApiOperation("获取汇总统计数据")
+    @GetMapping("/summary")
+    public AjaxResult getSummary(UserBehaviorQueryParam param) {
+        UserBehaviorReportVO vo = userBehaviorService.getBehaviorSummary(param);
+        return AjaxResult.success(vo);
+    }
+
+    /**
+     * 获取按日统计数据
+     * 返回时间范围内每一天的统计数据
+     * 
+     * @param param 查询参数
+     *              - startDate: 开始日期,默认近30天
+     *              - endDate: 结束日期
+     *              - userType: 用户类型,1=新用户,2=老用户,不传=全部
+     * @return 每日统计数据列表
+     */
+    @ApiOperation("获取按日统计数据")
+    @GetMapping("/daily")
+    public AjaxResult getDailyReport(UserBehaviorQueryParam param) {
+        List<UserBehaviorReportVO> list = userBehaviorService.getDailyBehaviorReport(param);
+        return AjaxResult.success(list);
+    }
+}

+ 62 - 0
fs-service/src/main/java/com/fs/his/domain/FsAppUserBehaviorDaily.java

@@ -0,0 +1,62 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * APP用户行为日统计表 fs_app_user_behavior_daily
+ * 
+ * 表说明:
+ * 每个用户每天一条记录,汇总当天的启动次数和使用时长
+ * 用于快速查询统计数据,避免扫描大量原始日志
+ * 
+ * 与fs_app_user_behavior_log的关系:
+ * - log表是原始日志,记录每次事件
+ * - 此表是聚合结果,每个用户每天一条记录
+ * - 通过user_id和日期关联
+ * 
+ * 数据来源:
+ * 前端埋点上报时,实时更新此表
+ */
+@Data
+@TableName("fs_app_user_behavior_daily")
+@ApiModel("APP用户行为日统计")
+public class FsAppUserBehaviorDaily implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @ApiModelProperty("统计日期")
+    private Date statDate;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("当天启动次数")
+    private Integer launchCount;
+
+    @ApiModelProperty("当天使用时长(秒)")
+    private Integer useDuration;
+
+    @ApiModelProperty("是否新用户:1=是(注册当天),0=否")
+    private Integer isNewUser;
+
+    @ApiModelProperty("APP版本号")
+    private String appVersion;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ApiModelProperty("创建时间")
+    private Date createTime;
+}

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

@@ -0,0 +1,67 @@
+package com.fs.his.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * APP用户行为原始日志表 fs_app_user_behavior_log
+ * 
+ * 表说明:
+ * 记录每次APP启动/切后台事件的原始日志
+ * 每次事件产生一条记录
+ * 
+ * 事件类型说明:
+ * - event_type=1(启动):APP启动或从后台恢复时上报
+ * - event_type=2(切后台):APP进入后台时上报,此时记录本次使用时长
+ * 
+ * 与fs_app_user_behavior_daily的关系:
+ * - 此表是原始日志,记录每次事件
+ * - daily表是聚合结果,每个用户每天一条记录
+ * - 通过user_id和日期关联
+ */
+@Data
+@TableName("fs_app_user_behavior_log")
+@ApiModel("APP用户行为原始日志")
+public class FsAppUserBehaviorLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    @ApiModelProperty("主键ID")
+    private Long id;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("会话ID,每次启动生成一个唯一ID")
+    private String sessionId;
+
+    @ApiModelProperty("事件类型:1=APP启动,2=APP切后台(记录使用时长)")
+    private Integer eventType;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ApiModelProperty("事件发生时间")
+    private Date eventTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @ApiModelProperty("事件日期(生成列)")
+    private Date eventDate;
+
+    @ApiModelProperty("使用时长(秒),切后台事件时记录本次使用时长")
+    private Integer duration;
+
+    @ApiModelProperty("APP版本号")
+    private String appVersion;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @ApiModelProperty("创建时间")
+    private Date createTime;
+}

+ 51 - 0
fs-service/src/main/java/com/fs/his/dto/UserBehaviorReportDTO.java

@@ -0,0 +1,51 @@
+package com.fs.his.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户行为埋点上报DTO
+ * 
+ * HTTP接口上报方案:
+ * 
+ * 1. APP启动时:
+ *    - 前端调用 /report 接口,eventType=1
+ *    - 生成唯一sessionId,本次会话期间保持不变
+ * 
+ * 2. APP切后台或退出时:
+ *    - 前端调用 /report 接口,eventType=2
+ *    - 计算并上报使用时长 duration
+ * 
+ * 字段说明:
+ * - userId: 用户ID(必填)
+ * - sessionId: 会话ID,每次启动生成一个唯一ID(必填)
+ * - eventType: 事件类型,1=启动,2=切后台(必填)
+ * - eventTime: 事件发生时间(必填)
+ * - duration: 使用时长,单位秒(切后台时必填)
+ * - appVersion: APP版本号
+ */
+@Data
+@ApiModel("用户行为埋点上报DTO")
+public class UserBehaviorReportDTO {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    private Long userId;
+
+    @ApiModelProperty(value = "会话ID,每次启动生成一个唯一ID", required = true)
+    private String sessionId;
+
+    @ApiModelProperty(value = "事件类型:1=启动,2=切后台", required = true)
+    private Integer eventType;
+
+    @ApiModelProperty(value = "事件发生时间", required = true)
+    private Date eventTime;
+
+    @ApiModelProperty(value = "使用时长(秒),切后台时必填")
+    private Integer duration;
+
+    @ApiModelProperty(value = "APP版本号")
+    private String appVersion;
+}

+ 135 - 0
fs-service/src/main/java/com/fs/his/mapper/FsAppUserBehaviorDailyMapper.java

@@ -0,0 +1,135 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsAppUserBehaviorDaily;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APP用户行为日统计Mapper接口
+ * 
+ * 提供以下查询功能:
+ * 1. 批量插入或更新日统计数据
+ * 2. 启动次数分布统计
+ * 3. 使用时长分布统计
+ * 4. 访问间隔统计
+ * 5. 沉默用户唤醒统计
+ * 6. 按日行为报表查询
+ */
+public interface FsAppUserBehaviorDailyMapper extends BaseMapper<FsAppUserBehaviorDaily> {
+
+    /**
+     * 批量插入或更新日统计数据
+     * 使用ON DUPLICATE KEY UPDATE实现幂等性
+     * 
+     * @param list 日统计数据列表
+     */
+    void batchInsertOrUpdate(@Param("list") List<FsAppUserBehaviorDaily> list);
+
+    /**
+     * 查询启动次数分布
+     * 按用户在时间范围内的日均启动次数分组统计
+     * 返回:启动1次、2-3次、4次+的用户数
+     * 
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param isNewUser 是否新用户(null=全部,1=新用户,0=老用户)
+     * @return 启动次数分布Map列表
+     */
+    List<Map<String, Object>> selectLaunchCountDistribution(@Param("startDate") String startDate,
+                                                             @Param("endDate") String endDate,
+                                                             @Param("isNewUser") Integer isNewUser,
+                                                             @Param("appVersion") String appVersion);
+
+    /**
+     * 查询使用时长分布
+     * 按用户在时间范围内的总使用时长分组统计
+     * 返回:<30分钟、30分钟-1小时、1-2小时、2-4小时、4小时+的用户数
+     * 
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param isNewUser 是否新用户
+     * @param appVersion APP版本号
+     * @return 使用时长分布Map列表
+     */
+    List<Map<String, Object>> selectUseDurationDistribution(@Param("startDate") String startDate,
+                                                             @Param("endDate") String endDate,
+                                                             @Param("isNewUser") Integer isNewUser,
+                                                             @Param("appVersion") String appVersion);
+
+    /**
+     * 查询用户平均访问间隔天数
+     * 计算每个用户相邻两次访问的日期差,再取所有用户的平均值
+     * 
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param isNewUser 是否新用户
+     * @param appVersion APP版本号
+     * @return 平均访问间隔天数
+     */
+    BigDecimal selectAvgVisitInterval(@Param("startDate") String startDate,
+                                      @Param("endDate") String endDate,
+                                      @Param("isNewUser") Integer isNewUser,
+                                      @Param("appVersion") String appVersion);
+
+    /**
+     * 查询沉默用户被唤醒的数量
+     * 沉默用户定义:30天前有活跃记录,之后30天无活跃记录的用户
+     * 被唤醒:沉默用户在查询时间范围内重新活跃
+     * 
+     * @param beforeDate 30天前的日期
+     * @param startDate 查询开始日期
+     * @param endDate 查询结束日期
+     * @param isNewUser 是否新用户
+     * @param appVersion APP版本号
+     * @return 被唤醒的沉默用户数
+     */
+    Long selectSilentUsers(@Param("beforeDate") String beforeDate,
+                           @Param("startDate") String startDate,
+                           @Param("endDate") String endDate,
+                           @Param("isNewUser") Integer isNewUser,
+                           @Param("appVersion") String appVersion);
+
+    /**
+     * 查询沉默用户总数
+     * 用于计算沉默用户唤醒率的分母
+     * 
+     * @param beforeDate 30天前的日期
+     * @param startDate 查询开始日期
+     * @param isNewUser 是否新用户
+     * @param appVersion APP版本号
+     * @return 沉默用户总数
+     */
+    Long selectTotalSilentUsers(@Param("beforeDate") String beforeDate,
+                                @Param("startDate") String startDate,
+                                @Param("isNewUser") Integer isNewUser,
+                                @Param("appVersion") String appVersion);
+
+    /**
+     * 查询按日统计的行为报表
+     * 返回每一天的启动次数分布、使用时长分布等统计数据
+     * 
+     * @param startDate 开始日期
+     * @param endDate 结束日期
+     * @param isNewUser 是否新用户
+     * @param appVersion APP版本号
+     * @return 每日统计Map列表
+     */
+    List<Map<String, Object>> selectDailyBehaviorReport(@Param("startDate") String startDate,
+                                                         @Param("endDate") String endDate,
+                                                         @Param("isNewUser") Integer isNewUser,
+                                                         @Param("appVersion") String appVersion);
+
+    /**
+     * 检查用户是否为新用户
+     * 判断依据:用户的app_create_time日期是否等于指定日期
+     * 
+     * @param userId 用户ID
+     * @param dateStr 日期字符串(yyyy-MM-dd)
+     * @return 1=新用户,0=老用户
+     */
+    Integer checkIsNewUser(@Param("userId") Long userId, @Param("dateStr") String dateStr);
+}

+ 24 - 0
fs-service/src/main/java/com/fs/his/mapper/FsAppUserBehaviorLogMapper.java

@@ -0,0 +1,24 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsAppUserBehaviorLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * APP用户行为原始日志Mapper接口
+ * 
+ * 提供以下功能:
+ * 1. 单条插入日志记录
+ * 2. 批量插入日志记录
+ */
+public interface FsAppUserBehaviorLogMapper extends BaseMapper<FsAppUserBehaviorLog> {
+
+    /**
+     * 批量插入日志记录
+     * 
+     * @param list 日志记录列表
+     */
+    void batchInsert(@Param("list") List<FsAppUserBehaviorLog> list);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/his/param/UserBehaviorQueryParam.java

@@ -0,0 +1,29 @@
+package com.fs.his.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 用户行为统计查询参数
+ * 
+ * 用于查询用户行为统计数据,支持以下筛选条件:
+ * - 时间范围:startDate ~ endDate
+ * - 用户类型:新用户/老用户/全部
+ */
+@Data
+@ApiModel("用户行为统计查询参数")
+public class UserBehaviorQueryParam {
+
+    @ApiModelProperty(value = "开始日期(yyyy-MM-dd),不传默认近30天")
+    private String startDate;
+
+    @ApiModelProperty(value = "结束日期(yyyy-MM-dd),不传默认当天")
+    private String endDate;
+
+    @ApiModelProperty(value = "用户类型:1=新用户,2=老用户,不传=全部")
+    private Integer userType;
+
+    @ApiModelProperty(value = "APP版本号,不传=全部")
+    private String appVersion;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/his/service/IUserBehaviorService.java

@@ -0,0 +1,52 @@
+package com.fs.his.service;
+
+import com.fs.his.dto.UserBehaviorReportDTO;
+import com.fs.his.param.UserBehaviorQueryParam;
+import com.fs.his.vo.UserBehaviorReportVO;
+
+import java.util.List;
+
+/**
+ * APP用户行为统计Service接口
+ * 
+ * 功能说明:
+ * 1. 接收前端埋点上报的用户行为数据(APP启动、APP退出)
+ * 2. 将行为数据汇总到日统计表
+ * 3. 提供按日期范围查询每日行为统计数据
+ */
+public interface IUserBehaviorService {
+
+    /**
+     * 单条埋点上报
+     * 前端在APP启动或退出时调用,记录用户行为日志并更新日统计表
+     * 
+     * @param dto 埋点上报DTO,包含用户ID、会话ID、事件类型、事件时间、使用时长等
+     */
+    void reportBehavior(UserBehaviorReportDTO dto);
+
+    /**
+     * 批量埋点上报
+     * 用于批量上传多条埋点数据,减少网络请求次数
+     * 
+     * @param dtoList 埋点上报DTO列表
+     */
+    void reportBehaviorBatch(List<UserBehaviorReportDTO> dtoList);
+
+    /**
+     * 获取时间范围内的汇总统计数据
+     * 返回整个时间范围的聚合指标,不按日期拆分
+     * 
+     * @param param 查询参数(startDate、endDate、userType)
+     * @return 汇总统计VO
+     */
+    UserBehaviorReportVO getBehaviorSummary(UserBehaviorQueryParam param);
+
+    /**
+     * 获取按日统计的行为数据报表
+     * 返回时间范围内每一天的统计数据,每天一行
+     * 
+     * @param param 查询参数(startDate、endDate、userType)
+     * @return 每日统计VO列表
+     */
+    List<UserBehaviorReportVO> getDailyBehaviorReport(UserBehaviorQueryParam param);
+}

+ 458 - 0
fs-service/src/main/java/com/fs/his/service/impl/UserBehaviorServiceImpl.java

@@ -0,0 +1,458 @@
+package com.fs.his.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.his.domain.FsAppUserBehaviorDaily;
+import com.fs.his.domain.FsAppUserBehaviorLog;
+import com.fs.his.dto.UserBehaviorReportDTO;
+import com.fs.his.mapper.FsAppUserBehaviorDailyMapper;
+import com.fs.his.mapper.FsAppUserBehaviorLogMapper;
+import com.fs.his.param.UserBehaviorQueryParam;
+import com.fs.his.service.IUserBehaviorService;
+import com.fs.his.vo.UserBehaviorReportVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * APP用户行为统计Service实现类
+ * 
+ * 数据流程:
+ * 1. 前端埋点上报 -> fs_app_user_behavior_log(原始日志表)
+ * 2. 实时/定时聚合 -> fs_app_user_behavior_daily(日统计表)
+ * 3. 查询统计 -> 从日统计表聚合计算
+ * 
+ * 两张表关系:
+ * - fs_app_user_behavior_log:原始埋点日志表,记录每次APP启动/切后台事件
+ * - fs_app_user_behavior_daily:日统计汇总表,每个用户每天一条记录
+ * - 关系:log表是原始数据,daily表是聚合结果;通过user_id和日期关联
+ * 
+ * 事件类型说明:
+ * - event_type=1(启动):APP启动或从后台恢复时上报,启动次数+1
+ * - event_type=2(切后台):APP进入后台时上报,累加使用时长
+ */
+@Service
+public class UserBehaviorServiceImpl implements IUserBehaviorService {
+
+    @Autowired
+    private FsAppUserBehaviorLogMapper behaviorLogMapper;
+
+    @Autowired
+    private FsAppUserBehaviorDailyMapper behaviorDailyMapper;
+
+    /**
+     * 单条埋点上报
+     * 处理流程:
+     * 1. 插入原始日志表
+     * 2. 更新日统计表(启动次数+1 或 使用时长累加)
+     */
+    @Override
+    @Transactional
+    public void reportBehavior(UserBehaviorReportDTO dto) {
+        Date eventTime = dto.getEventTime() != null ? dto.getEventTime() : new Date();
+        String sessionId = dto.getSessionId() != null ? dto.getSessionId() : generateSessionId(dto.getUserId());
+
+        FsAppUserBehaviorLog log = new FsAppUserBehaviorLog();
+        log.setUserId(dto.getUserId());
+        log.setSessionId(sessionId);
+        log.setEventType(dto.getEventType());
+        log.setEventTime(eventTime);
+        log.setDuration(dto.getDuration() != null ? dto.getDuration() : 0);
+        log.setAppVersion(parseAppVersion(dto.getAppVersion()));
+        behaviorLogMapper.insert(log);
+
+        dto.setEventTime(eventTime);
+        dto.setSessionId(sessionId);
+        updateDailySummary(dto);
+    }
+
+    /**
+     * 生成会话ID
+     */
+    private String generateSessionId(Long userId) {
+        return "session_" + System.currentTimeMillis() + "_" + userId;
+    }
+
+    /**
+     * 解析APP版本号,提取括号中的数值部分
+     * 输入格式:1.1.2(112) 或 2.0.0(200)
+     * 输出:112 或 200
+     */
+    private String parseAppVersion(String appVersion) {
+        if (appVersion == null || appVersion.isEmpty()) {
+            return null;
+        }
+        // 匹配括号中的数字
+        Pattern pattern = Pattern.compile("\\((\\d+)\\)");
+        Matcher matcher = pattern.matcher(appVersion);
+        if (matcher.find()) {
+            return matcher.group(1);
+        }
+        // 如果没有括号,返回原始版本号
+        return appVersion;
+    }
+
+
+    /**
+     * 批量埋点上报
+     * 用于批量上传多条埋点数据,减少网络请求次数
+     */
+    @Override
+    @Transactional
+    public void reportBehaviorBatch(List<UserBehaviorReportDTO> dtoList) {
+        if (dtoList == null || dtoList.isEmpty()) {
+            return;
+        }
+
+        // 1. 批量插入原始日志表
+        List<FsAppUserBehaviorLog> logList = new ArrayList<>();
+        for (UserBehaviorReportDTO dto : dtoList) {
+            FsAppUserBehaviorLog log = new FsAppUserBehaviorLog();
+            log.setUserId(dto.getUserId());
+            log.setSessionId(dto.getSessionId());
+            log.setEventType(dto.getEventType());
+            log.setEventTime(dto.getEventTime());
+            log.setDuration(dto.getDuration() != null ? dto.getDuration() : 0);
+            log.setAppVersion(parseAppVersion(dto.getAppVersion()));
+            logList.add(log);
+        }
+        behaviorLogMapper.batchInsert(logList);
+
+        // 2. 逐条更新日统计表
+        for (UserBehaviorReportDTO dto : dtoList) {
+            updateDailySummary(dto);
+        }
+    }
+
+    /**
+     * 更新日统计表
+     * 根据事件类型更新对应字段:
+     * - event_type=1(启动):launch_count + 1
+     * - event_type=2(切后台):use_duration 累加传入的duration值
+     */
+    private void updateDailySummary(UserBehaviorReportDTO dto) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        String dateStr = sdf.format(dto.getEventTime());
+
+        // 查询是否已存在该用户当天的记录
+        FsAppUserBehaviorDaily existing = findExistingDaily(dateStr, dto.getUserId());
+
+        if (existing == null) {
+            // 新建记录
+            existing = new FsAppUserBehaviorDaily();
+            try {
+                existing.setStatDate(sdf.parse(dateStr));
+            } catch (Exception e) {
+                return;
+            }
+            existing.setUserId(dto.getUserId());
+            existing.setLaunchCount(0);
+            existing.setUseDuration(0);
+            // 判断是否为新用户(注册当天)
+            existing.setIsNewUser(isNewUser(dto.getUserId(), dto.getEventTime()) ? 1 : 0);
+            existing.setAppVersion(parseAppVersion(dto.getAppVersion()));
+        }
+
+        // 根据事件类型更新对应字段
+        switch (dto.getEventType()) {
+            case 1:
+                // APP启动:启动次数+1
+                existing.setLaunchCount(existing.getLaunchCount() + 1);
+                break;
+            case 2:
+                // APP切后台:累加使用时长(前端传入的duration)
+                if (dto.getDuration() != null && dto.getDuration() > 0) {
+                    existing.setUseDuration(existing.getUseDuration() + dto.getDuration());
+                }
+                break;
+            default:
+                break;
+        }
+
+        // 保存或更新
+        if (existing.getId() == null) {
+            List<FsAppUserBehaviorDaily> list = new ArrayList<>();
+            list.add(existing);
+            behaviorDailyMapper.batchInsertOrUpdate(list);
+        } else {
+            behaviorDailyMapper.updateById(existing);
+        }
+    }
+
+    /**
+     * 查询用户当天的日统计记录
+     */
+    private FsAppUserBehaviorDaily findExistingDaily(String dateStr, Long userId) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Date statDate = sdf.parse(dateStr);
+            LambdaQueryWrapper<FsAppUserBehaviorDaily> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(FsAppUserBehaviorDaily::getUserId, userId)
+                   .eq(FsAppUserBehaviorDaily::getStatDate, statDate);
+            return behaviorDailyMapper.selectOne(wrapper);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 判断用户是否为新用户
+     * 新用户定义:用户的app_create_time日期等于事件发生日期
+     */
+    private boolean isNewUser(Long userId, Date eventTime) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        String dateStr = sdf.format(eventTime);
+        Integer result = behaviorDailyMapper.checkIsNewUser(userId, dateStr);
+        return result != null && result == 1;
+    }
+
+    /**
+     * 获取时间范围内的汇总统计数据
+     * 返回整个时间范围的聚合指标
+     */
+    @Override
+    public UserBehaviorReportVO getBehaviorSummary(UserBehaviorQueryParam param) {
+        String startDate = param.getStartDate();
+        String endDate = param.getEndDate();
+
+        // 默认查询近30天
+        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());
+        }
+
+        // 转换用户类型参数
+        Integer isNewUser = convertUserType(param.getUserType());
+        String appVersion = param.getAppVersion();
+
+        UserBehaviorReportVO vo = new UserBehaviorReportVO();
+
+        // 1. 查询启动次数分布
+        List<Map<String, Object>> launchDist = behaviorDailyMapper.selectLaunchCountDistribution(startDate, endDate, isNewUser, appVersion);
+        if (launchDist != null && !launchDist.isEmpty()) {
+            Map<String, Object> row = launchDist.get(0);
+            vo.setLaunchCount1(toLong(row.get("count_1")));
+            vo.setLaunchCount23(toLong(row.get("count_2_3")));
+            vo.setLaunchCount4Plus(toLong(row.get("count_4_plus")));
+        } else {
+            vo.setLaunchCount1(0L);
+            vo.setLaunchCount23(0L);
+            vo.setLaunchCount4Plus(0L);
+        }
+
+        // 2. 查询使用时长分布
+        List<Map<String, Object>> durationDist = behaviorDailyMapper.selectUseDurationDistribution(startDate, endDate, isNewUser, appVersion);
+        if (durationDist != null && !durationDist.isEmpty()) {
+            Map<String, Object> row = durationDist.get(0);
+            vo.setDurationLt30(toLong(row.get("count_lt_30")));
+            vo.setDuration30To60(toLong(row.get("count_30_60")));
+            vo.setDuration60To120(toLong(row.get("count_60_120")));
+            vo.setDuration120To240(toLong(row.get("count_120_240")));
+            vo.setDurationGt240(toLong(row.get("count_gt_240")));
+        } else {
+            vo.setDurationLt30(0L);
+            vo.setDuration30To60(0L);
+            vo.setDuration60To120(0L);
+            vo.setDuration120To240(0L);
+            vo.setDurationGt240(0L);
+        }
+
+        // 3. 查询平均访问间隔
+        BigDecimal avgInterval = behaviorDailyMapper.selectAvgVisitInterval(startDate, endDate, isNewUser, appVersion);
+        vo.setAvgVisitInterval(avgInterval != null ? avgInterval.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+
+        // 4. 查询沉默用户唤醒率
+        String beforeDate = getBeforeDate(startDate, 30);
+        Long silentWaked = behaviorDailyMapper.selectSilentUsers(beforeDate, startDate, endDate, isNewUser, appVersion);
+        Long totalSilent = behaviorDailyMapper.selectTotalSilentUsers(beforeDate, startDate, isNewUser, appVersion);
+        vo.setSilentWakeUpRate(calculateRate(silentWaked, totalSilent));
+
+        return vo;
+    }
+
+    /**
+     * 获取按日统计的行为数据报表
+     * 返回时间范围内每一天的统计数据
+     */
+    @Override
+    public List<UserBehaviorReportVO> getDailyBehaviorReport(UserBehaviorQueryParam param) {
+        String startDate = param.getStartDate();
+        String endDate = param.getEndDate();
+
+        // 默认查询近30天
+        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());
+        }
+
+        Integer isNewUser = convertUserType(param.getUserType());
+        String appVersion = param.getAppVersion();
+
+        // 查询每日统计数据
+        List<Map<String, Object>> dailyData = behaviorDailyMapper.selectDailyBehaviorReport(startDate, endDate, isNewUser, appVersion);
+
+        // 计算整个时间范围的平均访问间隔(每日报表显示相同的值)
+        BigDecimal avgIntervalTotal = behaviorDailyMapper.selectAvgVisitInterval(startDate, endDate, isNewUser, appVersion);
+        BigDecimal avgIntervalValue = avgIntervalTotal != null ? avgIntervalTotal.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
+
+        // 转换为Map便于查找
+        Map<String, UserBehaviorReportVO> dailyMap = new LinkedHashMap<>();
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (Map<String, Object> row : dailyData) {
+            UserBehaviorReportVO vo = new UserBehaviorReportVO();
+            Object dateObj = row.get("statDate");
+            if (dateObj instanceof Date) {
+                vo.setStatDate((Date) dateObj);
+            }
+            vo.setLaunchCount1(toLong(row.get("launchCount1")));
+            vo.setLaunchCount23(toLong(row.get("launchCount23")));
+            vo.setLaunchCount4Plus(toLong(row.get("launchCount4Plus")));
+            vo.setDurationLt30(toLong(row.get("durationLt30")));
+            vo.setDuration30To60(toLong(row.get("duration30To60")));
+            vo.setDuration60To120(toLong(row.get("duration60To120")));
+            vo.setDuration120To240(toLong(row.get("duration120To240")));
+            vo.setDurationGt240(toLong(row.get("durationGt240")));
+
+            String dateKey = dateObj instanceof Date ? sdf.format((Date) dateObj) : String.valueOf(dateObj);
+            dailyMap.put(dateKey, vo);
+        }
+
+        // 生成完整的日期列表
+        List<String> allDates = new ArrayList<>();
+        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();
+        }
+
+        // 构建结果列表,确保每天都有数据
+        List<UserBehaviorReportVO> result = new ArrayList<>();
+        for (String dateStr : allDates) {
+            UserBehaviorReportVO vo = dailyMap.get(dateStr);
+            if (vo == null) {
+                // 没有数据的日期填充0
+                vo = new UserBehaviorReportVO();
+                try {
+                    vo.setStatDate(sdf.parse(dateStr));
+                } catch (Exception e) {
+                    continue;
+                }
+                vo.setLaunchCount1(0L);
+                vo.setLaunchCount23(0L);
+                vo.setLaunchCount4Plus(0L);
+                vo.setDurationLt30(0L);
+                vo.setDuration30To60(0L);
+                vo.setDuration60To120(0L);
+                vo.setDuration120To240(0L);
+                vo.setDurationGt240(0L);
+            }
+
+            // 计算沉默用户唤醒率(单日数据)
+            String beforeDate = getBeforeDate(dateStr, 30);
+            String nextDate = getNextDate(dateStr);
+            Long silentWaked = behaviorDailyMapper.selectSilentUsers(beforeDate, dateStr, nextDate, isNewUser, appVersion);
+            Long totalSilent = behaviorDailyMapper.selectTotalSilentUsers(beforeDate, dateStr, isNewUser, appVersion);
+            vo.setSilentWakeUpRate(calculateRate(silentWaked, totalSilent));
+
+            // 访问间隔使用整个时间范围的统计值
+            vo.setAvgVisitInterval(avgIntervalValue);
+
+            result.add(vo);
+        }
+
+        return result;
+    }
+
+    /**
+     * 转换用户类型参数
+     * 前端传入:1=新用户,2=老用户
+     * 数据库存储:1=新用户,0=老用户
+     */
+    private Integer convertUserType(Integer userType) {
+        if (userType == null) {
+            return null;
+        }
+        if (userType == 1) {
+            return 1;
+        }
+        if (userType == 2) {
+            return 0;
+        }
+        return null;
+    }
+
+    /**
+     * Object转Long
+     */
+    private Long toLong(Object obj) {
+        if (obj == null) return 0L;
+        if (obj instanceof Number) {
+            return ((Number) obj).longValue();
+        }
+        try {
+            return Long.parseLong(obj.toString());
+        } catch (Exception e) {
+            return 0L;
+        }
+    }
+
+    /**
+     * 计算百分比
+     */
+    private BigDecimal calculateRate(Long part, Long total) {
+        if (total == null || total == 0 || part == null) {
+            return BigDecimal.ZERO;
+        }
+        return new BigDecimal(part).multiply(new BigDecimal(100))
+                .divide(new BigDecimal(total), 2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 获取指定日期前N天的日期
+     */
+    private String getBeforeDate(String dateStr, int days) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(sdf.parse(dateStr));
+            cal.add(Calendar.DAY_OF_MONTH, -days);
+            return sdf.format(cal.getTime());
+        } catch (Exception e) {
+            return dateStr;
+        }
+    }
+
+    /**
+     * 获取指定日期的下一天
+     */
+    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;
+        }
+    }
+}

+ 69 - 0
fs-service/src/main/java/com/fs/his/vo/UserBehaviorReportVO.java

@@ -0,0 +1,69 @@
+package com.fs.his.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * APP用户行为统计报表VO
+ * 
+ * 包含以下统计指标:
+ * 1. 启动次数分布:按用户日均启动次数分组(1次、2-3次、4次+)
+ * 2. 使用时长分布:按用户使用时长分组(<30分钟、30分钟-1小时、1-2小时、2-4小时、4小时+)
+ * 3. 访问间隔:用户两次访问的平均间隔天数
+ * 4. 沉默用户唤醒率:流失30天后重新登录的用户占比
+ */
+@Data
+@ApiModel("用户行为统计报表VO")
+public class UserBehaviorReportVO {
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "统计日期", width = 30, dateFormat = "yyyy-MM-dd")
+    @ApiModelProperty("统计日期")
+    private Date statDate;
+
+    @Excel(name = "启动1次用户数")
+    @ApiModelProperty("日均启动1次的用户数")
+    private Long launchCount1;
+
+    @Excel(name = "启动2-3次用户数")
+    @ApiModelProperty("日均启动2-3次的用户数")
+    private Long launchCount23;
+
+    @Excel(name = "启动4次+用户数")
+    @ApiModelProperty("日均启动4次及以上的用户数")
+    private Long launchCount4Plus;
+
+    @Excel(name = "使用时长<30分钟用户数")
+    @ApiModelProperty("使用时长<30分钟的用户数")
+    private Long durationLt30;
+
+    @Excel(name = "使用时长30分钟-1小时用户数")
+    @ApiModelProperty("使用时长30分钟-1小时的用户数")
+    private Long duration30To60;
+
+    @Excel(name = "使用时长1-2小时用户数")
+    @ApiModelProperty("使用时长1-2小时的用户数")
+    private Long duration60To120;
+
+    @Excel(name = "使用时长2-4小时用户数")
+    @ApiModelProperty("使用时长2-4小时的用户数")
+    private Long duration120To240;
+
+    @Excel(name = "使用时长4小时以上用户数")
+    @ApiModelProperty("使用时长4小时以上的用户数")
+    private Long durationGt240;
+
+    @Excel(name = "平均访问间隔天数")
+    @ApiModelProperty("用户两次访问平均间隔天数")
+    private BigDecimal avgVisitInterval;
+
+    @Excel(name = "沉默用户唤醒率(%)")
+    @ApiModelProperty("流失30天后重新登录的用户占比(%)")
+    private BigDecimal silentWakeUpRate;
+}

+ 195 - 0
fs-service/src/main/resources/mapper/his/FsAppUserBehaviorDailyMapper.xml

@@ -0,0 +1,195 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsAppUserBehaviorDailyMapper">
+
+    <!-- 批量插入或更新日统计数据 -->
+    <insert id="batchInsertOrUpdate" parameterType="java.util.List">
+        INSERT INTO fs_app_user_behavior_daily (stat_date, user_id, launch_count, use_duration, is_new_user, app_version, create_time)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.statDate}, #{item.userId}, #{item.launchCount}, #{item.useDuration}, #{item.isNewUser}, #{item.appVersion}, NOW())
+        </foreach>
+        ON DUPLICATE KEY UPDATE
+            launch_count = VALUES(launch_count),
+            use_duration = VALUES(use_duration),
+            is_new_user = VALUES(is_new_user),
+            app_version = VALUES(app_version)
+    </insert>
+
+    <!-- APP用户条件:筛选有效的APP用户 -->
+    <sql id="appUserCondition">
+        AND d.user_id IN (
+            SELECT DISTINCT ucu.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)
+        )
+    </sql>
+
+    <!-- 查询启动次数分布:按用户日均启动次数分组统计 -->
+    <select id="selectLaunchCountDistribution" resultType="java.util.Map">
+        SELECT
+            SUM(CASE WHEN avg_launch_count = 1 THEN 1 ELSE 0 END) AS count_1,
+            SUM(CASE WHEN avg_launch_count BETWEEN 2 AND 3 THEN 1 ELSE 0 END) AS count_2_3,
+            SUM(CASE WHEN avg_launch_count >= 4 THEN 1 ELSE 0 END) AS count_4_plus
+        FROM (
+            SELECT user_id, AVG(launch_count) AS avg_launch_count
+            FROM fs_app_user_behavior_daily d
+            WHERE d.stat_date >= #{startDate}
+            AND d.stat_date &lt;= #{endDate}
+            <if test="isNewUser != null">
+                AND d.is_new_user = #{isNewUser}
+            </if>
+            <if test="appVersion != null and appVersion != ''">
+                AND d.app_version = #{appVersion}
+            </if>
+            GROUP BY user_id
+        ) t
+    </select>
+
+    <!-- 查询使用时长分布:按用户总使用时长分组统计 -->
+    <select id="selectUseDurationDistribution" resultType="java.util.Map">
+        SELECT
+            SUM(CASE WHEN total_duration &lt; 1800 THEN 1 ELSE 0 END) AS count_lt_30,
+            SUM(CASE WHEN total_duration >= 1800 AND total_duration &lt; 3600 THEN 1 ELSE 0 END) AS count_30_60,
+            SUM(CASE WHEN total_duration >= 3600 AND total_duration &lt; 7200 THEN 1 ELSE 0 END) AS count_60_120,
+            SUM(CASE WHEN total_duration >= 7200 AND total_duration &lt; 14400 THEN 1 ELSE 0 END) AS count_120_240,
+            SUM(CASE WHEN total_duration >= 14400 THEN 1 ELSE 0 END) AS count_gt_240
+        FROM (
+            SELECT user_id, SUM(use_duration) AS total_duration
+            FROM fs_app_user_behavior_daily d
+            WHERE d.stat_date >= #{startDate}
+            AND d.stat_date &lt;= #{endDate}
+            <if test="isNewUser != null">
+                AND d.is_new_user = #{isNewUser}
+            </if>
+            <if test="appVersion != null and appVersion != ''">
+                AND d.app_version = #{appVersion}
+            </if>
+            GROUP BY user_id
+        ) t
+    </select>
+
+    <!-- 查询用户平均访问间隔天数 -->
+    <select id="selectAvgVisitInterval" resultType="java.math.BigDecimal">
+        SELECT IFNULL(AVG(interval_days), 0)
+        FROM (
+            SELECT user_id, AVG(interval_days) AS interval_days
+            FROM (
+                SELECT user_id,
+                       DATEDIFF(LEAD(stat_date) OVER (PARTITION BY user_id ORDER BY stat_date), stat_date) AS interval_days
+                FROM fs_app_user_behavior_daily d
+                WHERE d.stat_date >= #{startDate}
+                AND d.stat_date &lt;= #{endDate}
+                <if test="isNewUser != null">
+                    AND d.is_new_user = #{isNewUser}
+                </if>
+                <if test="appVersion != null and appVersion != ''">
+                    AND d.app_version = #{appVersion}
+                </if>
+            ) t2
+            WHERE interval_days IS NOT NULL AND interval_days > 0
+            GROUP BY user_id
+        ) t3
+    </select>
+
+    <!-- 查询沉默用户被唤醒的数量(流失30天后重新登录的用户) -->
+    <select id="selectSilentUsers" resultType="Long">
+        SELECT COUNT(DISTINCT d.user_id)
+        FROM fs_app_user_behavior_daily d
+        WHERE d.stat_date >= #{startDate}
+        AND d.stat_date &lt;= #{endDate}
+        AND d.user_id IN (
+            SELECT user_id FROM fs_app_user_behavior_daily
+            WHERE stat_date &lt; #{beforeDate}
+        )
+        AND d.user_id NOT IN (
+            SELECT user_id FROM fs_app_user_behavior_daily
+            WHERE stat_date >= #{beforeDate}
+            AND stat_date &lt; #{startDate}
+        )
+        <if test="isNewUser != null">
+            AND d.is_new_user = #{isNewUser}
+        </if>
+        <if test="appVersion != null and appVersion != ''">
+            AND d.app_version = #{appVersion}
+        </if>
+    </select>
+
+    <!-- 查询沉默用户总数(30天前活跃过,但中间30天没活跃的用户) -->
+    <select id="selectTotalSilentUsers" resultType="Long">
+        SELECT COUNT(DISTINCT a.user_id)
+        FROM fs_app_user_behavior_daily a
+        WHERE a.stat_date &lt; #{beforeDate}
+        AND a.user_id NOT IN (
+            SELECT user_id FROM fs_app_user_behavior_daily
+            WHERE stat_date >= #{beforeDate}
+            AND stat_date &lt; #{startDate}
+        )
+        <if test="isNewUser != null">
+            AND a.user_id IN (
+                SELECT d.user_id
+                FROM fs_app_user_behavior_daily d
+                WHERE d.is_new_user = #{isNewUser}
+            )
+        </if>
+        <if test="appVersion != null and appVersion != ''">
+            AND a.user_id IN (
+                SELECT d.user_id
+                FROM fs_app_user_behavior_daily d
+                WHERE d.app_version = #{appVersion}
+            )
+        </if>
+    </select>
+
+    <!-- 查询按日统计的行为报表 -->
+    <select id="selectDailyBehaviorReport" resultType="java.util.Map">
+        SELECT
+            d.stat_date AS statDate,
+            SUM(CASE WHEN user_launch_count = 1 THEN 1 ELSE 0 END) AS launchCount1,
+            SUM(CASE WHEN user_launch_count BETWEEN 2 AND 3 THEN 1 ELSE 0 END) AS launchCount23,
+            SUM(CASE WHEN user_launch_count >= 4 THEN 1 ELSE 0 END) AS launchCount4Plus,
+            SUM(CASE WHEN user_duration &lt; 1800 THEN 1 ELSE 0 END) AS durationLt30,
+            SUM(CASE WHEN user_duration >= 1800 AND user_duration &lt; 3600 THEN 1 ELSE 0 END) AS duration30To60,
+            SUM(CASE WHEN user_duration >= 3600 AND user_duration &lt; 7200 THEN 1 ELSE 0 END) AS duration60To120,
+            SUM(CASE WHEN user_duration >= 7200 AND user_duration &lt; 14400 THEN 1 ELSE 0 END) AS duration120To240,
+            SUM(CASE WHEN user_duration >= 14400 THEN 1 ELSE 0 END) AS durationGt240
+        FROM (
+            SELECT stat_date, user_id,
+                   SUM(launch_count) AS user_launch_count,
+                   SUM(use_duration) AS user_duration
+            FROM fs_app_user_behavior_daily d
+            WHERE d.stat_date >= #{startDate}
+            AND d.stat_date &lt;= #{endDate}
+            <if test="isNewUser != null">
+                AND d.is_new_user = #{isNewUser}
+            </if>
+            <if test="appVersion != null and appVersion != ''">
+                AND d.app_version = #{appVersion}
+            </if>
+            GROUP BY stat_date, user_id
+        ) d
+        GROUP BY d.stat_date
+        ORDER BY d.stat_date
+    </select>
+
+    <!-- 检查用户是否为新用户 -->
+    <select id="checkIsNewUser" resultType="java.lang.Integer">
+        SELECT CASE
+            WHEN DATE(u.app_create_time) = #{dateStr} THEN 1
+            ELSE 0
+        END
+        FROM fs_user u
+        WHERE u.user_id = #{userId}
+        AND u.status = 1
+        AND u.is_del = 0
+        AND (u.source IS NOT NULL OR u.login_device IS NOT NULL OR u.app_create_time IS NOT NULL)
+        LIMIT 1
+    </select>
+
+</mapper>

+ 16 - 0
fs-service/src/main/resources/mapper/his/FsAppUserBehaviorLogMapper.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.his.mapper.FsAppUserBehaviorLogMapper">
+
+    <!-- 批量插入日志记录 -->
+    <insert id="batchInsert" parameterType="java.util.List">
+        INSERT INTO fs_app_user_behavior_log (user_id, session_id, event_type, event_time, duration, app_version, create_time)
+        VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.userId}, #{item.sessionId}, #{item.eventType}, #{item.eventTime}, #{item.duration}, #{item.appVersion}, NOW())
+        </foreach>
+    </insert>
+
+</mapper>