用户行为日志与动态查询功能旨在记录系统中所有用户的关键行为操作,包括所有后端接口的调用(如患者上传数据、医生确认复诊、管理员管理操作等),并基于日志数据为医生提供患者动态查询服务。该功能允许记录用户活动,并根据用户角色提供差异化的查询权限:
该功能遵循项目数据库规范(参见 docs/OLD/DevRule/07-数据库规范.md)和API设计规范(参见 docs/OLD/DevRule/03-API设计规范.md),确保日志记录的完整性和查询的安全性。
根据项目数据库表设计规范,创建用户行为日志表,用于记录所有用户的关键操作,包括所有接口调用:
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT(20) | 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID) |
| user_id | BIGINT(20) | 操作用户ID,外键(关联 t_user_info) |
| activity_type | VARCHAR(50) | 活动类型(枚举值,如 BLOOD_GLUCOSE_UPLOAD, FOLLOW_UP_CONFIRM 等) |
| activity_description | TEXT | 活动描述(简要说明操作内容) |
| related_entity_type | VARCHAR(50) | 相关实体类型(可选,如 BLOOD_GLUCOSE, HEALTH_RECORD, FOLLOW_UP) |
| related_entity_id | BIGINT(20) | 相关实体ID(可选,用于关联具体记录) |
| metadata | JSON | 元数据(存储额外信息,如数值、状态变更等,JSON格式) |
| ip_address | VARCHAR(45) | 操作IP地址 |
| user_agent | TEXT | 用户代理字符串 |
| version | INT(11) | 版本号(乐观锁) |
| create_user | BIGINT(20) | 创建者ID(同 user_id) |
| create_time | DATETIME | 创建时间,默认值CURRENT_TIMESTAMP |
| update_user | BIGINT(20) | 更新者ID |
| update_time | DATETIME | 更新时间,默认值CURRENT_TIMESTAMP,更新时自动设置为当前时间 |
| remark | VARCHAR(255) | 备注 |
遵循的数据库规范要点:
BIGINT 主键并通过雪花算法生成(MyBatis-Plus ASSIGN_ID)。version、create_user、create_time、update_user、update_time)由 BaseEntity 与 CustomMetaObjectHandler 自动填充。user_id、activity_type、create_time、related_entity_type。V{version}__{description}.sql)。示例建表:
CREATE TABLE `t_user_activity_log` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '操作用户ID',
`activity_type` varchar(50) NOT NULL COMMENT '活动类型',
`activity_description` text NOT NULL COMMENT '活动描述',
`related_entity_type` varchar(50) DEFAULT NULL COMMENT '相关实体类型',
`related_entity_id` bigint(20) DEFAULT NULL COMMENT '相关实体ID',
`metadata` json DEFAULT NULL COMMENT '元数据(JSON格式)',
`ip_address` varchar(45) DEFAULT NULL COMMENT '操作IP地址',
`user_agent` text COMMENT '用户代理字符串',
`version` int(11) DEFAULT '0' COMMENT '版本号(乐观锁)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建者ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '更新者ID',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_activity_type` (`activity_type`),
KEY `idx_create_time` (`create_time`),
KEY `idx_related_entity_type` (`related_entity_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户行为日志表';
备注:对外响应的 id 建议以字符串形式返回以避免前端 JS 精度丢失(参见 docs/OLD/DevRule/03-API设计规范.md 的长整型 ID 传输策略)。
package work.baiyun.chronicdiseaseapp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
/**
* 用户活动类型枚举(涵盖所有接口调用)
*/
public enum ActivityType {
// 患者健康数据相关
BLOOD_GLUCOSE_UPLOAD("BLOOD_GLUCOSE_UPLOAD", "血糖数据上传"),
BLOOD_PRESSURE_UPLOAD("BLOOD_PRESSURE_UPLOAD", "血压数据上传"),
HEART_RATE_UPLOAD("HEART_RATE_UPLOAD", "心率数据上传"),
PHYSICAL_DATA_UPLOAD("PHYSICAL_DATA_UPLOAD", "体格数据上传"),
// 患者档案相关
HEALTH_RECORD_UPDATE("HEALTH_RECORD_UPDATE", "健康档案更新"),
// 用药相关
MEDICATION_CREATE("MEDICATION_CREATE", "用药记录创建"),
MEDICATION_UPDATE("MEDICATION_UPDATE", "用药记录更新"),
// 提醒相关
REMINDER_SAVE("REMINDER_SAVE", "提醒设置保存"),
REMINDER_DELETE("REMINDER_DELETE", "提醒设置删除"),
// 复诊相关(患者)
FOLLOW_UP_CREATE("FOLLOW_UP_CREATE", "复诊请求创建"),
FOLLOW_UP_UPDATE("FOLLOW_UP_UPDATE", "复诊记录更新"),
// 医生复诊操作
FOLLOW_UP_CONFIRM("FOLLOW_UP_CONFIRM", "医生确认复诊"),
FOLLOW_UP_CANCEL("FOLLOW_UP_CANCEL", "医生取消复诊"),
FOLLOW_UP_COMPLETE("FOLLOW_UP_COMPLETE", "医生完成复诊"),
// 用户绑定相关
USER_BINDING_CREATE("USER_BINDING_CREATE", "用户绑定创建"),
USER_BINDING_DELETE("USER_BINDING_DELETE", "用户绑定删除"),
// 头像相关
AVATAR_UPLOAD("AVATAR_UPLOAD", "头像上传"),
// 地理位置相关
GEO_QUERY("GEO_QUERY", "地理位置查询"),
// 药品管理相关(管理员/医生)
MEDICINE_CREATE("MEDICINE_CREATE", "药品信息创建"),
MEDICINE_UPDATE("MEDICINE_UPDATE", "药品信息更新"),
MEDICINE_LIST("MEDICINE_LIST", "药品信息查询"),
// 微信相关
WECHAT_GET_OPENID("WECHAT_GET_OPENID", "获取微信openid"),
// 其他通用操作(可扩展)
DATA_QUERY("DATA_QUERY", "数据查询"),
DATA_UPDATE("DATA_UPDATE", "数据更新"),
DATA_DELETE("DATA_DELETE", "数据删除");
@EnumValue
private final String code;
private final String description;
ActivityType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return description;
}
public static ActivityType fromCode(String code) {
if (code == null) return null;
for (ActivityType type : ActivityType.values()) {
if (type.code.equals(code)) return type;
}
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"Unknown ActivityType code: " + code);
}
}
package work.baiyun.chronicdiseaseapp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
/**
* 相关实体类型枚举
*/
public enum RelatedEntityType {
BLOOD_GLUCOSE("BLOOD_GLUCOSE", "血糖数据"),
BLOOD_PRESSURE("BLOOD_PRESSURE", "血压数据"),
HEART_RATE("HEART_RATE", "心率数据"),
PHYSICAL_DATA("PHYSICAL_DATA", "体格数据"),
HEALTH_RECORD("HEALTH_RECORD", "健康档案"),
MEDICATION("MEDICATION", "用药记录"),
REMINDER("REMINDER", "提醒设置"),
FOLLOW_UP("FOLLOW_UP", "复诊记录"),
USER_BINDING("USER_BINDING", "用户绑定"),
AVATAR("AVATAR", "头像"),
MEDICINE("MEDICINE", "药品信息"),
USER_INFO("USER_INFO", "用户信息");
@EnumValue
private final String code;
private final String description;
RelatedEntityType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return description;
}
public static RelatedEntityType fromCode(String code) {
if (code == null) return null;
for (RelatedEntityType type : RelatedEntityType.values()) {
if (type.code.equals(code)) return type;
}
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"Unknown RelatedEntityType code: " + code);
}
}
参考复诊文档,为 ActivityType 和 RelatedEntityType 创建相应的 TypeHandler(ActivityTypeTypeHandler.java 和 RelatedEntityTypeTypeHandler.java),实现与数据库的映射。
根据项目数据库表设计规范,创建用户行为日志表,用于记录所有用户的关键操作:
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | BIGINT(20) | 主键ID,使用雪花算法(MyBatis-Plus ASSIGN_ID) |
| user_id | BIGINT(20) | 操作用户ID,外键(关联 t_user_info) |
| activity_type | VARCHAR(50) | 活动类型(枚举值,如 DATA_UPLOAD, PROFILE_UPDATE, FOLLOW_UP_UPDATE 等) |
| activity_description | TEXT | 活动描述(简要说明操作内容) |
| related_entity_type | VARCHAR(50) | 相关实体类型(可选,如 BLOOD_GLUCOSE, HEALTH_RECORD, FOLLOW_UP) |
| related_entity_id | BIGINT(20) | 相关实体ID(可选,用于关联具体记录) |
| metadata | JSON | 元数据(存储额外信息,如数值、状态变更等,JSON格式) |
| ip_address | VARCHAR(45) | 操作IP地址 |
| user_agent | TEXT | 用户代理字符串 |
| version | INT(11) | 版本号(乐观锁) |
| create_user | BIGINT(20) | 创建者ID(同 user_id) |
| create_time | DATETIME | 创建时间,默认值CURRENT_TIMESTAMP |
| update_user | BIGINT(20) | 更新者ID |
| update_time | DATETIME | 更新时间,默认值CURRENT_TIMESTAMP,更新时自动设置为当前时间 |
| remark | VARCHAR(255) | 备注 |
遵循的数据库规范要点:
BIGINT 主键并通过雪花算法生成(MyBatis-Plus ASSIGN_ID)。version、create_user、create_time、update_user、update_time)由 BaseEntity 与 CustomMetaObjectHandler 自动填充。user_id、activity_type、create_time、related_entity_type。V{version}__{description}.sql)。示例建表:
CREATE TABLE `t_user_activity_log` (
`id` bigint(20) NOT NULL COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '操作用户ID',
`activity_type` varchar(50) NOT NULL COMMENT '活动类型',
`activity_description` text NOT NULL COMMENT '活动描述',
`related_entity_type` varchar(50) DEFAULT NULL COMMENT '相关实体类型',
`related_entity_id` bigint(20) DEFAULT NULL COMMENT '相关实体ID',
`metadata` json DEFAULT NULL COMMENT '元数据(JSON格式)',
`ip_address` varchar(45) DEFAULT NULL COMMENT '操作IP地址',
`user_agent` text COMMENT '用户代理字符串',
`version` int(11) DEFAULT '0' COMMENT '版本号(乐观锁)',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建者ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '更新者ID',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_activity_type` (`activity_type`),
KEY `idx_create_time` (`create_time`),
KEY `idx_related_entity_type` (`related_entity_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户行为日志表';
备注:对外响应的 id 建议以字符串形式返回以避免前端 JS 精度丢失(参见 docs/OLD/DevRule/03-API设计规范.md 的长整型 ID 传输策略)。
package work.baiyun.chronicdiseaseapp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
/**
* 用户活动类型枚举
*/
public enum ActivityType {
// 患者健康数据相关
BLOOD_GLUCOSE_UPLOAD("BLOOD_GLUCOSE_UPLOAD", "血糖数据上传"),
BLOOD_PRESSURE_UPLOAD("BLOOD_PRESSURE_UPLOAD", "血压数据上传"),
HEART_RATE_UPLOAD("HEART_RATE_UPLOAD", "心率数据上传"),
PHYSICAL_DATA_UPLOAD("PHYSICAL_DATA_UPLOAD", "体格数据上传"),
// 患者档案相关
HEALTH_RECORD_UPDATE("HEALTH_RECORD_UPDATE", "健康档案更新"),
// 用药相关
MEDICATION_CREATE("MEDICATION_CREATE", "用药记录创建"),
MEDICATION_UPDATE("MEDICATION_UPDATE", "用药记录更新"),
// 提醒相关
REMINDER_SAVE("REMINDER_SAVE", "提醒设置保存"),
REMINDER_DELETE("REMINDER_DELETE", "提醒设置删除"),
// 复诊相关
FOLLOW_UP_CREATE("FOLLOW_UP_CREATE", "复诊请求创建"),
FOLLOW_UP_UPDATE("FOLLOW_UP_UPDATE", "复诊记录更新"),
// 其他活动(管理员可见)
USER_BINDING_CREATE("USER_BINDING_CREATE", "用户绑定创建"),
USER_BINDING_DELETE("USER_BINDING_DELETE", "用户绑定删除"),
AVATAR_UPLOAD("AVATAR_UPLOAD", "头像上传"),
GEO_QUERY("GEO_QUERY", "地理位置查询");
@EnumValue
private final String code;
private final String description;
ActivityType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return description;
}
public static ActivityType fromCode(String code) {
if (code == null) return null;
for (ActivityType type : ActivityType.values()) {
if (type.code.equals(code)) return type;
}
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"Unknown ActivityType code: " + code);
}
}
package work.baiyun.chronicdiseaseapp.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
/**
* 相关实体类型枚举
*/
public enum RelatedEntityType {
BLOOD_GLUCOSE("BLOOD_GLUCOSE", "血糖数据"),
BLOOD_PRESSURE("BLOOD_PRESSURE", "血压数据"),
HEART_RATE("HEART_RATE", "心率数据"),
PHYSICAL_DATA("PHYSICAL_DATA", "体格数据"),
HEALTH_RECORD("HEALTH_RECORD", "健康档案"),
MEDICATION("MEDICATION", "用药记录"),
REMINDER("REMINDER", "提醒设置"),
FOLLOW_UP("FOLLOW_UP", "复诊记录"),
USER_BINDING("USER_BINDING", "用户绑定"),
AVATAR("AVATAR", "头像");
@EnumValue
private final String code;
private final String description;
RelatedEntityType(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return description;
}
public static RelatedEntityType fromCode(String code) {
if (code == null) return null;
for (RelatedEntityType type : RelatedEntityType.values()) {
if (type.code.equals(code)) return type;
}
throw new work.baiyun.chronicdiseaseapp.exception.CustomException(
work.baiyun.chronicdiseaseapp.enums.ErrorCode.PARAMETER_ERROR.getCode(),
"Unknown RelatedEntityType code: " + code);
}
}
参考复诊文档,为 ActivityType 和 RelatedEntityType 创建相应的 TypeHandler(ActivityTypeTypeHandler.java 和 RelatedEntityTypeTypeHandler.java),实现与数据库的映射。
package work.baiyun.chronicdiseaseapp.model.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import work.baiyun.chronicdiseaseapp.enums.ActivityType;
import work.baiyun.chronicdiseaseapp.enums.RelatedEntityType;
import java.time.LocalDateTime;
@Schema(description = "用户行为日志表")
@TableName("t_user_activity_log")
@Data
@EqualsAndHashCode(callSuper = false)
public class UserActivityLog extends BaseEntity {
@Schema(description = "操作用户ID")
@TableField("user_id")
private Long userId;
@Schema(description = "活动类型")
@TableField(value = "activity_type", typeHandler = work.baiyun.chronicdiseaseapp.handler.ActivityTypeTypeHandler.class)
private ActivityType activityType;
@Schema(description = "活动描述")
@TableField("activity_description")
private String activityDescription;
@Schema(description = "相关实体类型")
@TableField(value = "related_entity_type", typeHandler = work.baiyun.chronicdiseaseapp.handler.RelatedEntityTypeTypeHandler.class)
private RelatedEntityType relatedEntityType;
@Schema(description = "相关实体ID")
@TableField("related_entity_id")
private Long relatedEntityId;
@Schema(description = "元数据(JSON格式)")
@TableField("metadata")
private String metadata; // 使用 String 存储 JSON
@Schema(description = "操作IP地址")
@TableField("ip_address")
private String ipAddress;
@Schema(description = "用户代理字符串")
@TableField("user_agent")
private String userAgent;
}
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import work.baiyun.chronicdiseaseapp.enums.ActivityType;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "患者动态查询请求")
@Data
public class PatientActivityQueryRequest extends BaseQueryRequest {
@Schema(description = "患者用户ID(医生查询时必填)")
private Long patientUserId;
@Schema(description = "活动类型列表(可选,用于过滤特定动态)")
private List<ActivityType> activityTypes;
@Schema(description = "开始时间")
private LocalDateTime startTime;
@Schema(description = "结束时间")
private LocalDateTime endTime;
}
package work.baiyun.chronicdiseaseapp.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import work.baiyun.chronicdiseaseapp.enums.ActivityType;
import work.baiyun.chronicdiseaseapp.enums.RelatedEntityType;
import java.time.LocalDateTime;
@Schema(description = "用户活动响应")
@Data
public class UserActivityResponse {
@Schema(description = "活动ID")
private String id;
@Schema(description = "操作用户ID")
private Long userId;
@Schema(description = "活动类型")
private ActivityType activityType;
@Schema(description = "活动描述")
private String activityDescription;
@Schema(description = "相关实体类型")
private RelatedEntityType relatedEntityType;
@Schema(description = "相关实体ID")
private Long relatedEntityId;
@Schema(description = "元数据")
private Object metadata; // 反序列化为对象
@Schema(description = "操作时间")
private LocalDateTime createTime;
}
继承分页响应结构,包含 records 列表。
package work.baiyun.chronicdiseaseapp.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import work.baiyun.chronicdiseaseapp.model.vo.PatientActivityQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UserActivityResponse;
/**
* 用户活动日志服务接口
*/
public interface UserActivityLogService {
/**
* 记录用户活动
*/
void logActivity(Long userId, ActivityType activityType, String description,
RelatedEntityType relatedEntityType, Long relatedEntityId,
Object metadata, String ipAddress, String userAgent);
/**
* 医生查询绑定患者的动态(仅限特定类型)
*/
Page<UserActivityResponse> queryPatientActivitiesForDoctor(PatientActivityQueryRequest request);
/**
* 管理员查询所有用户动态
*/
Page<UserActivityResponse> queryAllActivities(PatientActivityQueryRequest request);
}
实现日志记录和查询逻辑,包括权限校验(医生只能查绑定患者,管理员查所有)。使用 JsonUtils 工具类处理 metadata 字段的JSON序列化/反序列化。
package work.baiyun.chronicdiseaseapp.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import work.baiyun.chronicdiseaseapp.mapper.UserActivityLogMapper;
import work.baiyun.chronicdiseaseapp.model.po.UserActivityLog;
import work.baiyun.chronicdiseaseapp.model.vo.PatientActivityQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UserActivityResponse;
import work.baiyun.chronicdiseaseapp.service.UserActivityLogService;
import work.baiyun.chronicdiseaseapp.service.UserBindingService;
import work.baiyun.chronicdiseaseapp.enums.ActivityType;
import work.baiyun.chronicdiseaseapp.enums.RelatedEntityType;
import work.baiyun.chronicdiseaseapp.enums.PermissionGroup;
import work.baiyun.chronicdiseaseapp.util.JsonUtils;
import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
import java.util.List;
@Service
public class UserActivityLogServiceImpl implements UserActivityLogService {
@Autowired
private UserActivityLogMapper userActivityLogMapper;
@Autowired
private UserBindingService userBindingService;
@Override
public void logActivity(Long userId, ActivityType activityType, String description,
RelatedEntityType relatedEntityType, Long relatedEntityId,
Object metadata, String ipAddress, String userAgent) {
UserActivityLog log = new UserActivityLog();
log.setUserId(userId);
log.setActivityType(activityType);
log.setActivityDescription(description);
log.setRelatedEntityType(relatedEntityType);
log.setRelatedEntityId(relatedEntityId);
// 使用 JsonUtils 序列化 metadata 为 JSON 字符串
log.setMetadata(JsonUtils.toJson(metadata));
log.setIpAddress(ipAddress);
log.setUserAgent(userAgent);
userActivityLogMapper.insert(log);
}
@Override
public Page<UserActivityResponse> queryPatientActivitiesForDoctor(PatientActivityQueryRequest request) {
Long currentUserId = SecurityUtils.getCurrentUserId();
PermissionGroup role = SecurityUtils.getCurrentUserRole();
// 权限校验:医生需绑定患者
if (!PermissionGroup.DOCTOR.equals(role)) {
throw new RuntimeException("无权限");
}
// 校验绑定关系
boolean isBound = userBindingService.checkBinding(currentUserId, request.getPatientUserId());
if (!isBound) {
throw new RuntimeException("未绑定该患者");
}
// 过滤特定活动类型(患者健康数据、健康档案、复诊)
List<ActivityType> allowedTypes = List.of(
ActivityType.BLOOD_GLUCOSE_UPLOAD, ActivityType.HEALTH_RECORD_UPDATE,
ActivityType.FOLLOW_UP_CREATE, ActivityType.FOLLOW_UP_UPDATE
);
QueryWrapper<UserActivityLog> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", request.getPatientUserId())
.in("activity_type", allowedTypes)
.orderByDesc("create_time");
Page<UserActivityLog> page = userActivityLogMapper.selectPage(
new Page<>(request.getCurrent(), request.getSize()), wrapper);
// 转换为响应VO,使用 JsonUtils 反序列化 metadata
List<UserActivityResponse> responses = page.getRecords().stream().map(log -> {
UserActivityResponse resp = new UserActivityResponse();
resp.setId(String.valueOf(log.getId()));
resp.setUserId(log.getUserId());
resp.setActivityType(log.getActivityType());
resp.setActivityDescription(log.getActivityDescription());
resp.setRelatedEntityType(log.getRelatedEntityType());
resp.setRelatedEntityId(log.getRelatedEntityId());
// 使用 JsonUtils 反序列化 metadata
resp.setMetadata(JsonUtils.fromJson(log.getMetadata(), Object.class));
resp.setCreateTime(log.getCreateTime());
return resp;
}).collect(Collectors.toList());
Page<UserActivityResponse> resultPage = new Page<>();
resultPage.setRecords(responses);
resultPage.setTotal(page.getTotal());
return resultPage;
}
@Override
public Page<UserActivityResponse> queryAllActivities(PatientActivityQueryRequest request) {
PermissionGroup role = SecurityUtils.getCurrentUserRole();
// 权限校验:仅管理员
if (!PermissionGroup.ADMIN.equals(role)) {
throw new RuntimeException("无权限");
}
QueryWrapper<UserActivityLog> wrapper = new QueryWrapper<>();
if (request.getUserId() != null) {
wrapper.eq("user_id", request.getUserId());
}
wrapper.orderByDesc("create_time");
Page<UserActivityLog> page = userActivityLogMapper.selectPage(
new Page<>(request.getCurrent(), request.getSize()), wrapper);
// 转换为响应VO
List<UserActivityResponse> responses = page.getRecords().stream().map(log -> {
UserActivityResponse resp = new UserActivityResponse();
// ... 类似上述代码
resp.setMetadata(JsonUtils.fromJson(log.getMetadata(), Object.class));
return resp;
}).collect(Collectors.toList());
Page<UserActivityResponse> resultPage = new Page<>();
resultPage.setRecords(responses);
resultPage.setTotal(page.getTotal());
return resultPage;
}
}
package work.baiyun.chronicdiseaseapp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import work.baiyun.chronicdiseaseapp.common.R;
import work.baiyun.chronicdiseaseapp.model.vo.PatientActivityQueryRequest;
import work.baiyun.chronicdiseaseapp.model.vo.UserActivityResponse;
import work.baiyun.chronicdiseaseapp.service.UserActivityLogService;
import work.baiyun.chronicdiseaseapp.enums.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/user-activity")
@Tag(name = "用户活动日志", description = "用户行为日志与动态查询相关接口")
public class UserActivityLogController {
private static final Logger logger = LoggerFactory.getLogger(UserActivityLogController.class);
@Autowired
private UserActivityLogService userActivityLogService;
@Operation(summary = "医生查询患者动态", description = "医生查询绑定患者的健康相关动态")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功查询患者动态",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse.class))),
@ApiResponse(responseCode = "403", description = "无权限访问",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/query-patient-activities", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> queryPatientActivities(@RequestBody PatientActivityQueryRequest req) {
try {
Page<UserActivityResponse> page = userActivityLogService.queryPatientActivitiesForDoctor(req);
work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse();
vo.setRecords(page.getRecords());
vo.setTotal(page.getTotal());
vo.setSize(page.getSize());
vo.setCurrent(page.getCurrent());
vo.setPages(page.getPages());
return R.success(200, "ok", vo);
} catch (Exception e) {
logger.error("query patient activities failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
@Operation(summary = "管理员查询所有用户动态", description = "管理员查询所有用户的活动日志")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "成功查询所有动态",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse.class))),
@ApiResponse(responseCode = "403", description = "无权限访问",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/query-all-activities", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> queryAllActivities(@RequestBody PatientActivityQueryRequest req) {
try {
Page<UserActivityResponse> page = userActivityLogService.queryAllActivities(req);
work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse vo = new work.baiyun.chronicdiseaseapp.model.vo.UserActivityPageResponse();
vo.setRecords(page.getRecords());
vo.setTotal(page.getTotal());
vo.setSize(page.getSize());
vo.setCurrent(page.getCurrent());
vo.setPages(page.getPages());
return R.success(200, "ok", vo);
} catch (Exception e) {
logger.error("query all activities failed", e);
return R.fail(ErrorCode.SYSTEM_ERROR.getCode(), ErrorCode.SYSTEM_ERROR.getMessage());
}
}
}
日志记录应在所有Controller的方法中集成,以记录所有用户的接口调用。使用AOP(面向切面编程)或直接在方法中调用Service来实现。
创建Aspect类(UserActivityLogAspect.java)来自动记录所有接口调用:
package work.baiyun.chronicdiseaseapp.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import work.baiyun.chronicdiseaseapp.service.UserActivityLogService;
import work.baiyun.chronicdiseaseapp.enums.ActivityType;
import work.baiyun.chronicdiseaseapp.enums.RelatedEntityType;
import work.baiyun.chronicdiseaseapp.util.SecurityUtils;
@Aspect
@Component
public class UserActivityLogAspect {
@Autowired
private UserActivityLogService userActivityLogService;
@Pointcut("execution(* work.baiyun.chronicdiseaseapp.controller.*.*(..)) && !execution(* work.baiyun.chronicdiseaseapp.controller.UserActivityLogController.*(..))")
public void controllerMethods() {}
@Around("controllerMethods()")
public Object logActivity(ProceedingJoinPoint joinPoint) throws Throwable {
Long userId = SecurityUtils.getCurrentUserId();
if (userId == null) {
return joinPoint.proceed();
}
// 获取方法信息
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
// 映射活动类型(可根据方法名或注解动态确定)
ActivityType activityType = mapMethodToActivityType(className, methodName);
if (activityType == null) {
return joinPoint.proceed();
}
Object result = joinPoint.proceed();
// 记录日志
String description = generateDescription(activityType, joinPoint.getArgs());
userActivityLogService.logActivity(userId, activityType, description,
null, null, joinPoint.getArgs(), null, null);
return result;
}
private ActivityType mapMethodToActivityType(String className, String methodName) {
// 根据Controller类名和方法名映射活动类型
switch (className) {
case "BloodGlucoseDataController":
if ("add".equals(methodName)) return ActivityType.BLOOD_GLUCOSE_UPLOAD;
break;
case "FollowUpController":
if ("update".equals(methodName)) return ActivityType.FOLLOW_UP_UPDATE;
break;
// 添加更多映射
}
return null; // 不记录或使用通用类型
}
private String generateDescription(ActivityType type, Object[] args) {
// 根据活动类型生成描述
return type.getDescription();
}
}
在现有Controller方法中手动添加日志记录调用,例如在FollowUpController的update方法中:
// 在 FollowUpController.update() 方法中添加
userActivityLogService.logActivity(currentUserId, ActivityType.FOLLOW_UP_UPDATE,
"医生更新复诊记录: " + req.getStatus(), RelatedEntityType.FOLLOW_UP, id,
req, ipAddress, userAgent);
类似地,在所有Controller方法中添加相应的日志记录调用,包括医生确认复诊、管理员操作等。
在AOP方式中,也可集成JsonUtils处理metadata。
功能中使用 JsonUtils.java 工具类处理 metadata 字段的JSON序列化/反序列化:
metadata 对象转换为JSON字符串存储到数据库。示例:
log.setMetadata(JsonUtils.toJson(metadata));resp.setMetadata(JsonUtils.fromJson(log.getMetadata(), Object.class));这确保了复杂对象的灵活存储和检索。
d:\慢病APP\DevAll\apiserver-springboot\docs\New\用户行为日志与动态查询功能设计文档.md