You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Angular前端与Spring Boot+Hibernate后端的服务器端用户活动追踪及数据库存储实现示例咨询

嘿,我刚好在Spring Boot+Hibernate的项目里实现过类似的用户活动追踪功能,给你几个实用的实现方案和代码示例思路,完全适配你的技术栈:

1. 用Hibernate拦截器监听数据库变更日志

既然你用了Hibernate,它的拦截器可以直接捕获实体的增、删、改操作,非常适合记录数据库层面的变更细节。

首先定义一个自定义拦截器,继承EmptyInterceptor:

public class AuditInterceptor extends EmptyInterceptor {

    @Autowired
    private ActivityLogRepository logRepository;

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (entity instanceof BaseEntity) { // 假设你的实体都继承BaseEntity,包含创建人等基础信息
            saveActivityLog(id.toString(), "CREATE", entity.getClass().getSimpleName(), getCurrentUserId());
        }
        return super.onSave(entity, id, state, propertyNames, types);
    }

    @Override
    public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (entity instanceof BaseEntity) {
            saveActivityLog(id.toString(), "DELETE", entity.getClass().getSimpleName(), getCurrentUserId());
        }
    }

    @Override
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
        if (entity instanceof BaseEntity) {
            // 对比前后状态,生成具体变更内容
            String changes = compareChanges(currentState, previousState, propertyNames);
            saveActivityLog(id.toString(), "UPDATE", entity.getClass().getSimpleName(), getCurrentUserId(), changes);
        }
        return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
    }

    private void saveActivityLog(String entityId, String operationType, String entityType, String userId, String... changes) {
        ActivityLog log = new ActivityLog();
        log.setEntityId(entityId);
        log.setOperationType(operationType);
        log.setEntityType(entityType);
        log.setUserId(userId);
        log.setChangeDetails(changes.length > 0 ? changes[0] : null);
        log.setOperationTime(LocalDateTime.now());
        // 用异步线程保存,避免影响主业务流程
        CompletableFuture.runAsync(() -> logRepository.save(log));
    }

    // 辅助方法:对比实体字段前后变化
    private String compareChanges(Object[] currentState, Object[] previousState, String[] propertyNames) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < propertyNames.length; i++) {
            if (!Objects.equals(currentState[i], previousState[i])) {
                sb.append(propertyNames[i]).append(": ").append(previousState[i]).append(" -> ").append(currentState[i]).append("; ");
            }
        }
        return sb.length() > 0 ? sb.toString() : null;
    }

    // 辅助方法:获取当前登录用户ID(结合Spring Security)
    private String getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null ? authentication.getName() : "anonymous";
    }
}

然后在Spring配置里注册这个拦截器:

@Configuration
public class HibernateConfig {

    @Autowired
    private AuditInterceptor auditInterceptor;

    @Bean
    public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setPackagesToScan("com.yourpackage.entity");
        sessionFactory.setHibernateProperties(hibernateProperties());
        sessionFactory.setEntityInterceptor(auditInterceptor); // 绑定拦截器
        return sessionFactory;
    }

    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQL8Dialect");
        // 其他Hibernate配置项...
        return properties;
    }
}
2. Spring AOP实现通用操作日志(含登录登出)

对于登录、登出这类非数据库直接操作的行为,或者业务层的自定义操作,用Spring AOP来做更灵活。

首先定义一个自定义注解,标记需要记录日志的方法:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String operationType();
    String description() default "";
}

然后写一个AOP切面:

@Aspect
@Component
public class ActivityLogAspect {

    @Autowired
    private ActivityLogRepository logRepository;

    @Around("@annotation(loggable)")
    public Object logActivity(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
        // 提取日志基础信息
        String userId = getCurrentUserId();
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();

        Object result = joinPoint.proceed(); // 执行原业务方法

        // 异步保存日志
        CompletableFuture.runAsync(() -> {
            ActivityLog log = new ActivityLog();
            log.setOperationType(loggable.operationType());
            log.setDescription(loggable.description());
            log.setUserId(userId);
            log.setOperationTime(LocalDateTime.now());
            log.setSourceMethod(className + "." + methodName);
            logRepository.save(log);
        });

        return result;
    }

    private String getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication != null ? authentication.getName() : "anonymous";
    }
}

接下来在登录登出的方法上加上注解:

@Service
public class AuthService {

    @Loggable(operationType = "LOGIN", description = "用户登录系统")
    public Authentication login(String username, String password) {
        // 你的登录逻辑...
    }

    @Loggable(operationType = "LOGOUT", description = "用户登出系统")
    public void logout() {
        // 你的登出逻辑...
    }
}
3. 结合Spring Security的登录登出日志

如果你的登录登出是基于Spring Security实现的,还可以直接通过自定义成功处理器来记录:

@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private ActivityLogRepository logRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 记录登录日志,包含用户IP
        ActivityLog log = new ActivityLog();
        log.setOperationType("LOGIN");
        log.setUserId(authentication.getName());
        log.setOperationTime(LocalDateTime.now());
        log.setDescription("登录IP:" + request.getRemoteAddr());
        CompletableFuture.runAsync(() -> logRepository.save(log));

        // 执行原登录成功逻辑,比如跳转首页
        response.sendRedirect("/dashboard");
    }
}

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Autowired
    private ActivityLogRepository logRepository;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication != null) {
            ActivityLog log = new ActivityLog();
            log.setOperationType("LOGOUT");
            log.setUserId(authentication.getName());
            log.setOperationTime(LocalDateTime.now());
            CompletableFuture.runAsync(() -> logRepository.save(log));
        }

        // 执行原登出成功逻辑,比如跳转登录页
        response.sendRedirect("/login");
    }
}

然后在Spring Security配置里指定这两个处理器:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomAuthenticationSuccessHandler authSuccessHandler;

    @Autowired
    private CustomLogoutSuccessHandler logoutSuccessHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .successHandler(authSuccessHandler) // 绑定自定义登录成功处理器
            )
            .logout(logout -> logout
                .logoutSuccessHandler(logoutSuccessHandler) // 绑定自定义登出成功处理器
            );
        return http.build();
    }
}
最后补充几个最佳实践
  • 异步保存日志:用CompletableFuture或者Spring的@Async注解,避免日志操作拖慢主业务流程
  • 日志实体设计:建议包含userId、operationType(枚举:LOGIN/LOGOUT/CREATE/UPDATE/DELETE)、entityType、entityId、changeDetails、operationTime、sourceIp这些核心字段
  • 敏感信息脱敏:如果变更内容里有密码、手机号等敏感信息,一定要在保存前做脱敏处理
  • 日志查询优化:如果日志量很大,可以考虑分表或者用Elasticsearch存储,方便后续查询分析

内容的提问来源于stack exchange,提问作者Kerk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 16:38:15