【方案设计】监控服务-前后端方案综合案例介绍

203 阅读7分钟

在这里插入图片描述

在大多数项目中,结合前端精确采集、可靠的网络传输以及后端严谨处理的方式,是保证功能时长记录准确性和可靠性的常用方案。以下是一个综合方案示例:

前端部分

1. 精确获取时间戳并处理

使用 performance.now() 来精确获取用户操作的时间。在用户进入和离开功能页面时,分别记录开始和结束时间。为了防止页面切换等情况影响计时,利用 Page Visibility API 进行页面可见性监听。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>功能时长记录</title>
</head>
<body>
    <h1>功能页面</h1>
    <script>
        let startTime;
        let pausedTime = 0;
        let isPaused = false;

        function startFunction() {
            startTime = performance.now();
            document.addEventListener('visibilitychange', handleVisibilityChange);
        }

        function endFunction() {
            const endTime = performance.now();
            let totalDuration;
            if (isPaused) {
                totalDuration = endTime - startTime - pausedTime;
            } else {
                totalDuration = endTime - startTime;
            }
            sendDurationData(totalDuration);
            document.removeEventListener('visibilitychange', handleVisibilityChange);
        }

        function handleVisibilityChange() {
            if (document.visibilityState === 'hidden') {
                if (!isPaused) {
                    isPaused = true;
                    pausedTime = performance.now() - startTime;
                }
            } else {
                if (isPaused) {
                    isPaused = false;
                    startTime = performance.now() - pausedTime;
                    pausedTime = 0;
                }
            }
        }

        async function sendDurationData(duration) {
            try {
                const response = await fetch('/api/function-usage', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ duration })
                });
                if (response.ok) {
                    const data = await response.json();
                    console.log('时长数据发送成功', data);
                } else {
                    console.error('发送失败,状态码:', response.status);
                }
            } catch (error) {
                console.error('发送时长数据时出错:', error);
            }
        }

        // 模拟功能开始和结束
        startFunction();
        // 模拟一段时间后结束功能
        setTimeout(() => {
            endFunction();
        }, 5000);
    </script>
</body>
</html>

网络传输部分

在前端,采用 fetch 进行数据传输。为了确保数据传输的可靠性,添加重试机制。同时,在传输过程中对数据进行简单的加密(例如使用 Base64 编码),并在后端进行解码验证。

async function sendDurationDataWithRetry(duration, maxRetries = 3) {
    const encodedDuration = window.btoa(duration.toString());
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await fetch('/api/function-usage', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ encodedDuration })
            });
            if (response.ok) {
                const data = await response.json();
                console.log('时长数据发送成功', data);
                return;
            } else {
                console.error('发送失败,状态码:', response.status);
            }
        } catch (error) {
            console.error('发送时长数据时出错:', error);
        }
        await new Promise(resolve => setTimeout(resolve, 2000)); // 重试间隔 2 秒
    }
    console.error('超过最大重试次数,数据发送失败');
}

后端部分(以 Spring Boot 为例)

1. 接收和验证数据

在后端创建一个控制器来接收前端发送的数据。对接收的数据进行解码和验证,确保数据的合法性和准确性。

package com.example.demo.controller;

import com.example.demo.entity.FunctionUsage;
import com.example.demo.service.FunctionUsageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class FunctionUsageController {

    @Autowired
    private FunctionUsageService functionUsageService;

    @PostMapping("/function-usage")
    public String saveFunctionUsage(@RequestBody FunctionUsageRequest request) {
        try {
            String decodedDuration = new String(java.util.Base64.getDecoder().decode(request.getEncodedDuration()));
            long duration = Long.parseLong(decodedDuration);
            if (duration < 0) {
                return "无效的时长数据";
            }
            FunctionUsage functionUsage = new FunctionUsage();
            functionUsage.setDuration(duration);
            functionUsageService.saveFunctionUsage(functionUsage);
            return "时长数据保存成功";
        } catch (Exception e) {
            e.printStackTrace();
            return "保存时长数据时出错";
        }
    }
}

class FunctionUsageRequest {
    private String encodedDuration;

    public String getEncodedDuration() {
        return encodedDuration;
    }

    public void setEncodedDuration(String encodedDuration) {
        this.encodedDuration = encodedDuration;
    }
}

2. 存储数据

创建一个服务类和对应的实体类来处理数据的存储。使用事务确保数据存储的原子性。

package com.example.demo.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class FunctionUsage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private long duration;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public long getDuration() {
        return duration;
    }

    public void setDuration(long duration) {
        this.duration = duration;
    }
}
package com.example.demo.service;

import com.example.demo.entity.FunctionUsage;
import com.example.demo.repository.FunctionUsageRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class FunctionUsageService {

    @Autowired
    private FunctionUsageRepository functionUsageRepository;

    @Transactional
    public void saveFunctionUsage(FunctionUsage functionUsage) {
        functionUsageRepository.save(functionUsage);
    }
}
package com.example.demo.repository;

import com.example.demo.entity.FunctionUsage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FunctionUsageRepository extends JpaRepository<FunctionUsage, Long> {
}

系统监控和维护

通过日志记录来跟踪功能时长数据的处理过程。使用 Spring Boot 的 @Scheduled 注解来定期检查数据的一致性。

package com.example.demo.service;

import com.example.demo.entity.FunctionUsage;
import com.example.demo.repository.FunctionUsageRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class FunctionUsageService {

    private static final Logger logger = LoggerFactory.getLogger(FunctionUsageService.class);
    @Autowired
    private FunctionUsageRepository functionUsageRepository;

    @Transactional
    public void saveFunctionUsage(FunctionUsage functionUsage) {
        logger.info("开始保存功能时长数据: {}", functionUsage);
        functionUsageRepository.save(functionUsage);
        logger.info("功能时长数据保存成功: {}", functionUsage);
    }

    @Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每天执行一次
    public void checkDataConsistency() {
        logger.info("开始检查功能时长数据一致性...");
        // 数据一致性检查逻辑
        // 例如,统计一定时间内的总时长,与预期值进行比较
        // 如果发现不一致,记录日志并进行相应处理
        logger.info("功能时长数据一致性检查完成");
    }
}

这个方案综合考虑了前端数据采集的精确性、网络传输的可靠性、后端数据处理的严谨性以及系统的监控和维护,能够满足大多数项目中对功能时长记录准确性和可靠性的要求。

常见问题

要避免一个用户同时打开多个浏览器造成的时长统计不准确问题,可以从以下几个方面入手:

基于用户标识的统一管理

  • 使用唯一用户标识:在用户登录系统时,为其分配一个唯一的用户标识(如 JWT 令牌、用户 ID 等)。这个标识在用户使用系统的过程中保持不变,并且在每次与后端交互时都携带该标识。前端在记录功能使用时长时,将用户标识与时长数据一起发送给后端。
// 假设已经获取到用户 ID
const userId = getUserIdFromStorage(); 
function sendDurationData(duration) {
    fetch('/api/function-usage', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ duration, userId })
    })
 .then(response => response.json())
 .then(data => console.log(data))
 .catch(error => console.error('发送时长数据失败', error));
}
  • 后端聚合数据:后端接收到时长数据后,根据用户标识进行聚合处理。可以使用数据库的分组和聚合函数,将同一用户的多个时长记录合并为一个准确的统计结果。例如,在 MySQL 中:
-- 假设存在名为 function_usage 的表,包含 userId 和 duration 字段
SELECT userId, SUM(duration) AS totalDuration
FROM function_usage
GROUP BY userId;

前端状态管理

  • 浏览器间通信:利用浏览器的本地存储(localStorage)或会话存储(sessionStorage)以及 window.postMessage 方法在同一用户打开的多个浏览器窗口之间进行通信。当一个窗口开始或结束功能使用时,向其他窗口发送消息,告知状态变化。
// 发送消息的窗口
function sendFunctionStatus(status) {
    window.localStorage.setItem('functionStatus', JSON.stringify(status));
    window.postMessage({ type: 'functionStatusUpdate', status }, '*');
}

// 接收消息的窗口
window.addEventListener('message', function (event) {
    if (event.data.type === 'functionStatusUpdate') {
        const status = event.data.status;
        // 更新本地状态
    }
});
  • 互斥操作:通过设置一个全局标志来确保同一用户在同一时间只能有一个浏览器窗口进行功能使用计时。当一个窗口开始计时时,将标志设置为已占用,其他窗口检测到该标志后,不再进行计时。可以使用 localStorage 实现这个标志:
// 尝试开始计时
function tryStartFunction() {
    const isFunctionInUse = window.localStorage.getItem('functionInUse');
    if (isFunctionInUse) {
        console.log('功能已在其他窗口使用,无法开始计时');
        return;
    }
    window.localStorage.setItem('functionInUse', 'true');
    // 开始计时逻辑
}

// 结束计时
function endFunction() {
    window.localStorage.removeItem('functionInUse');
    // 结束计时逻辑
}

后端会话管理

  • 会话跟踪:在后端使用会话管理机制(如 Spring Session)来跟踪用户的会话状态。当用户在不同浏览器窗口登录时,后端可以识别这些会话属于同一个用户,并对功能使用时长进行统一管理。例如,在 Spring Boot 中配置 Spring Session:
// 配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@Configuration
@EnableRedisHttpSession
public class SessionConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}
  • 限制并发操作:在后端对同一用户的功能使用请求进行限制,确保同一时间只有一个请求可以进行计时操作。可以使用分布式锁(如 Redis 锁)来实现这一限制。例如,使用 Redisson 实现分布式锁:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class FunctionUsageService {

    @Autowired
    private RedissonClient redissonClient;

    public void saveFunctionUsage(FunctionUsage functionUsage) {
        String lockKey = "function:usage:" + functionUsage.getUserId();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (lock.tryLock()) {
                // 执行计时和保存数据逻辑
            } else {
                // 已有其他请求在处理,本次请求忽略或稍后重试
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

时间戳与状态核对

  • 记录详细时间戳:在前端和后端都记录功能使用的详细时间戳,包括开始时间、结束时间以及每次状态变化的时间。通过对比这些时间戳,可以更准确地判断用户在不同浏览器窗口的实际使用情况。
  • 定期核对状态:后端定期(如每隔一段时间)对同一用户的多个功能使用记录进行状态核对。通过比较时间戳和状态信息,剔除重复或不合理的记录,确保最终统计的时长准确无误。可以使用定时任务实现这一功能:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class FunctionUsageChecker {

    @Scheduled(fixedRate = 60 * 60 * 1000) // 每小时执行一次
    public void checkFunctionUsage() {
        // 从数据库获取同一用户的多个功能使用记录
        // 对比时间戳和状态信息,进行数据清理和合并
    }
}

通过以上多种方法的结合,可以有效避免一个用户同时打开多个浏览器造成的时长统计不准确问题,确保功能时长统计的准确性和可靠性。