package org.dromara.crm.service.impl; import cn.hutool.core.util.StrUtil; import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.crm.domain.CrmQuoteShareLog; import org.dromara.crm.domain.vo.CrmQuoteShareLogVo; import org.dromara.crm.mapper.CrmQuoteShareLogMapper; import org.dromara.crm.service.ICrmQuoteShareLogService; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * 报价单分享访问日志Service实现 * * @author 系统管理员 * @date 2025-10-05 */ @Slf4j @RequiredArgsConstructor @Service public class CrmQuoteShareLogServiceImpl implements ICrmQuoteShareLogService { private final CrmQuoteShareLogMapper baseMapper; @Override public Boolean recordVisit(Long quoteId, String shareCode, String ipAddress, String userAgent) { CrmQuoteShareLog log = new CrmQuoteShareLog(); log.setQuoteId(quoteId); log.setShareCode(shareCode); log.setVisitTime(LocalDateTime.now()); log.setIpAddress(ipAddress); log.setPageViews(1); log.setStayDuration(0); // 解析User-Agent if (StrUtil.isNotBlank(userAgent)) { try { UserAgent ua = UserAgentUtil.parse(userAgent); // 设置浏览器信息 String browser = ua.getBrowser().getName() + " " + ua.getVersion(); log.setBrowser(browser); // 判断设备类型 if (ua.isMobile()) { log.setDeviceType("Mobile"); } else { log.setDeviceType("PC"); } } catch (Exception e) { CrmQuoteShareLogServiceImpl.log.warn("解析User-Agent失败: userAgent={}", userAgent, e); log.setBrowser(userAgent.length() > 100 ? userAgent.substring(0, 100) : userAgent); log.setDeviceType("Unknown"); } } else { log.setDeviceType("Unknown"); } // TODO: 根据IP地址解析地理位置(可使用ip2region库) // log.setLocation(getLocationByIp(ipAddress)); CrmQuoteShareLogServiceImpl.log.info("记录报价单访问日志: quoteId={}, shareCode={}, ip={}, device={}", quoteId, shareCode, ipAddress, log.getDeviceType()); return baseMapper.insert(log) > 0; } @Override public List queryLogsByQuoteId(Long quoteId) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmQuoteShareLog::getQuoteId, quoteId); lqw.orderByDesc(CrmQuoteShareLog::getVisitTime); List list = baseMapper.selectList(lqw); return MapstructUtils.convert(list, CrmQuoteShareLogVo.class); } @Override public Map statisticsVisits(Long quoteId) { LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.eq(CrmQuoteShareLog::getQuoteId, quoteId); List logs = baseMapper.selectList(lqw); Map stats = new HashMap<>(); // 总访问次数 stats.put("totalVisits", logs.size()); // 独立IP数 long uniqueIps = logs.stream() .filter(log -> StrUtil.isNotBlank(log.getIpAddress())) .map(CrmQuoteShareLog::getIpAddress) .distinct() .count(); stats.put("uniqueIps", uniqueIps); // 最近访问时间 LocalDateTime lastVisit = logs.stream() .map(CrmQuoteShareLog::getVisitTime) .max(LocalDateTime::compareTo) .orElse(null); stats.put("lastVisitTime", lastVisit); // 首次访问时间 LocalDateTime firstVisit = logs.stream() .map(CrmQuoteShareLog::getVisitTime) .min(LocalDateTime::compareTo) .orElse(null); stats.put("firstVisitTime", firstVisit); // 设备类型分布 Map deviceStats = logs.stream() .collect(Collectors.groupingBy( log -> StrUtil.isNotBlank(log.getDeviceType()) ? log.getDeviceType() : "Unknown", Collectors.counting())); stats.put("deviceStats", deviceStats); // 总停留时长 int totalStayDuration = logs.stream() .mapToInt(log -> log.getStayDuration() != null ? log.getStayDuration() : 0) .sum(); stats.put("totalStayDuration", totalStayDuration); // 平均停留时长 double avgStayDuration = logs.isEmpty() ? 0 : (double) totalStayDuration / logs.size(); stats.put("avgStayDuration", avgStayDuration); log.info("统计报价单访问数据: quoteId={}, totalVisits={}, uniqueIps={}", quoteId, stats.get("totalVisits"), stats.get("uniqueIps")); return stats; } // TODO: 实现IP地址解析地理位置功能 // private String getLocationByIp(String ipAddress) { // // 使用ip2region等库解析IP地址 // return null; // } }