全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-11-22_《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

您的位置:首页 >> 新闻 >> 行业资讯

《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站? 点击关注公众号,“技术干货”及时达! 一、前言大家好呀,我是summo,最近发生了些事情(被裁员了,在找工作中)导致断更了,非常抱歉。刚被裁的时候还是有些难受,而且我还有房贷要还,有些压力,不过休息了一段时间,心态也平复了一些,打算一边找工作一边写文,如果有和我一样经历的同学,大家共勉! 《花100块做个摸鱼小网站! 》这个系列的前六篇已经大概把整体的流程写完了,从这篇起我会补充一些细节和组件,让我们的小网站更加丰富一些。这一篇呢我会介绍如何将用户的访问记录留下来,看着自己做的网站被别人访问是一件很有意思和很有成就感的事情。 对应的组件也就是我用红框标出来的那个,如下图: 解释下PV和UV的意思,如下: PV:页面访问量,即PageView,用户每次对网站的访问均被记录,用户对同一页面的多次访问,访问量累计。UV:独立访问用户数:即UniqueVisitor,访问网站的一台电脑客户端为一个访客。二、用户身份标识用于表明一个用户身份最好的做法是做登录注册,但是一旦加了这样的逻辑,就会有很多麻烦的问题要处理,比如如何做人机验证啦、接口防刷啦,等等,这些问题不处理的话网站很容易被攻击。像我们这样的小网站,我觉得这个功能没必要,我们只需要知道有多少人访问过我们的网站就可以了。 对于这样的需求,最简单的做法是根据用户的访问IP作为标识,然后根据IP解析一下地域信息,这样就已经很不错了。而目前最常用的IP解析工具就是:「ip2region」,如何使用这个组件我之前写过一篇文章进行介绍了,文章链接:SpringBoot整合ip2region实现使用ip监控用户访问城市。 核心代码是这段: package com.example.springbootip.util; import org.apache.commons.io.FileUtils;import org.lionsoul.ip2region.xdb.Searcher; import java.io.File;import java.text.MessageFormat;import java.util.Objects; public class AddressUtil { /** * 当前记录地址的本地DB */ private static final String TEMP_FILE_DIR = "/home/admin/app/"; /** * 根据IP地址查询登录来源 * * @param ip * @return */ public static String getCityInfo(String ip) { try { // 获取当前记录地址位置的文件 String dbPath = Objects.requireNonNull(AddressUtil.class.getResource("/ip2region/ip2region.xdb")).getPath(); File file = new File(dbPath); //如果当前文件不存在,则从缓存中复制一份 if (!file.exists()) { dbPath = TEMP_FILE_DIR + "ip.db"; System.out.println(MessageFormat.format("当前目录为:[{0}]", dbPath)); file = new File(dbPath); FileUtils.copyInputStreamToFile(Objects.requireNonNull(AddressUtil.class.getClassLoader().getResourceAsStream("classpath:ip2region/ip2region.xdb")), file); } //创建查询对象 Searcher searcher = Searcher.newWithFileOnly(dbPath); //开始查询 return searcher.searchByStr(ip); } catch (Exception e) { e.printStackTrace(); } //默认返回空字符串 return ""; } public static void main(String[] args) { System.out.println(getCityInfo("1.2.3.4")); }}三、功能实现为了解耦逻辑,我使用了一个注解:@VisitLog,只要将该注解放在IndexController的index方法上即可。同时为了统计用户的访问数据,我们需要设计一张访问记录表将数据存下来,并设计一个小组件用来展示这些数据,具体流程如下文。 1. 后端部分(1)访问记录表设计建表语句-- `summo-sbmy`.t_sbmy_visit_log definition CREATE TABLE `t_sbmy_visit_log` ( `id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理主键', `device_type` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设备类型,手机还是电脑', `ip` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '访问', `address` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'IP地址', `time` int DEFAULT NULL COMMENT '耗时', `method` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '调用方法', `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '参数', `gmt_create` datetime DEFAULT NULL COMMENT '创建时间', `gmt_modified` datetime DEFAULT NULL COMMENT '更新时间', `creator_id` bigint DEFAULT NULL COMMENT '创建人', `modifier_id` bigint DEFAULT NULL COMMENT '更新人', PRIMARY KEY (`id`) USING BTREE) ENGINE=InnoDB AUTO_INCREMENT=oDEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;DO、Mapper、Repository等文件还记得我在第三篇介绍的那个DO生成插件吗,在config.properties改下表名和DO名,双击mybatis-generator:generate就可以生成对应的DO、Mapper、xml了。 (2)VisitLog注解VisitLog.javapackage com.summo.sbmy.aspect; /** * 访问标识注解 */public @interface VisitLog {} VisitLogAspect.javapackage com.summo.sbmy.aspect.visit; import java.io.Serializable;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Set; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse; import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.core.JsonProcessingException;import com.summo.sbmy.common.util.AddressUtil;import com.summo.sbmy.common.util.HttpContextUtil;import com.summo.sbmy.common.util.IpUtil;import com.summo.sbmy.dao.entity.SbmyVisitLogDO;import com.summo.sbmy.dao.repository.SbmyVisitLogRepository;import lombok.extern.slf4j.Slf4j;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.aspectj.lang.reflect.MethodSignature;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.LocalVariableTableParameterNameDiscoverer;import org.springframework.stereotype.Component;import org.springframework.web.multipart.MultipartFile; import static com.summo.sbmy.common.util.DeviceUtil.isFromMobile; @Slf4j@Aspect@Componentpublic class VisitLogAspect { @Autowired private SbmyVisitLogRepository sbmyVisitLogRepository; @Pointcut("@annotation(com.summo.sbmy.aspect.visit.Log)") public void pointcut() { // do nothing } @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //获取request HttpServletRequest request = HttpContextUtil.getHttpServletRequest(); // 请求的类名 MethodSignature signature = (MethodSignature)joinPoint.getSignature(); Method method = signature.getMethod(); String className = joinPoint.getTarget().getClass().getName(); // 请求的方法名 String methodName = signature.getName(); String ip = IpUtil.getIpAddr(request); String address = AddressUtil.getAddress(ip); SbmyVisitLogDO sbmyVisitLogDO = SbmyVisitLogDO.builder().deviceType(isFromMobile(request) ? "手机" : "电脑").method( className + "." + methodName + "()").ip(ip).address(AddressUtil.getAddress(address)).build(); // 请求的方法参数值 Object[] args = joinPoint.getArgs(); // 请求的方法参数名称 LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paramNames = u.getParameterNames(method); if (args != null && paramNames != null) { // 创建 key-value 映射用于生成 JSON 字符串 MapString, Object paramMap = new LinkedHashMap(); for (int i = 0; i paramNames.length; i++) { if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse) { continue; } paramMap.put(paramNames[i], args[i]); } // 使用 Fastjson 将参数映射转换为 JSON 字符串 String paramsJson = JSON.toJSONString(paramMap); sbmyVisitLogDO.setParams(paramsJson); } long beginTime = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); long end = System.currentTimeMillis(); sbmyVisitLogDO.setTime((int)(end - beginTime)); sbmyVisitLogRepository.save(sbmyVisitLogDO); return proceed; } /** * 参数构造器? * * @param params * @param args * @param paramNames * @return * @throws JsonProcessingException */ private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames) throws JsonProcessingException { for (int i = 0; i args.length; i++) { if (args[i] instanceof Map) { Set set = ((Map)args[i]).keySet(); ListObject list = new ArrayList(); ListObject paramList = new ArrayList(); for (Object key : set) { list.add(((Map)args[i]).get(key)); paramList.add(key); } return handleParams(params, list.toArray(), paramList); } else { if (args[i] instanceof Serializable) { Class aClass = args[i].getClass(); try { aClass.getDeclaredMethod("toString", new Class[] {null}); // 如果不抛出 NoSuchMethodException 异常则存在 toString 方法 ,安全的 writeValueAsString ,否则 走 Object的 // toString方法 params.append(" ").append(paramNames.get(i)).append(": ").append( JSONObject.toJSONString(args[i])); } catch (NoSuchMethodException e) { params.append(" ").append(paramNames.get(i)).append(": ").append( JSONObject.toJSONString(args[i].toString())); } } else if (args[i] instanceof MultipartFile) { MultipartFile file = (MultipartFile)args[i]; params.append(" ").append(paramNames.get(i)).append(": ").append(file.getName()); } else { params.append(" ").append(paramNames.get(i)).append(": ").append(args[i]); } } } return params; }} ?这里使用到了一些工具类,代码我已经上传到仓库了,大家直接down下来就行。 ?(3)注解使用在IndexController.java中加入该注解即可 package com.summo.sbmy.web.controller; import com.summo.sbmy.aspect.visit.VisitLog;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping; @Controllerpublic class IndexController { @GetMapping("/") @VisitLog public String index(){ return "index"; }} 2. 前端部分(1)新建VisitorLog组件代码如下: template div class="stats-card-container" el-card class="stats-card-main" !-- 突出显示的今日 PV -- div class="stats-section" div class="stats-value-main"{{ statsData.todayPv }}/div div class="stats-label-main"今日 PV/div /div !-- 其他统计数据,以更紧凑的形式显示 -- div class="stats-section stats-others" div class="stats-item" div class="stats-value-small"{{ statsData.todayUv }}/div div class="stats-label-small"今日 UV/div /div div class="stats-item" div class="stats-value-small"{{ statsData.allPv }}/div div class="stats-label-small"总 PV/div /div div class="stats-item" div class="stats-value-small"{{ statsData.allUv }}/div div class="stats-label-small"总 UV/div /div /div /el-card /div/templatescriptimport apiService from "@/config/apiService.js";export default { name: "VisitorLog", data() { return { statsData: { todayPv: 0, todayUv: 0, allPv: 0, allUv: 0, }, }; }, created() { this.fetchVisitorCount(); // 组件创建时立即调用一次 this.startPolling(); // 启动定时器 }, beforeDestroy() { this.stopPolling(); // 在组件销毁前清理定时器 }, methods: { fetchVisitorCount() { apiService .get("/welcome/queryVisitorCount") .then((res) = { // 处理响应数据 this.statsData = res.data.data; }) .catch((error) = { // 处理错误情况 console.error(error); }); }, startPolling() { // 定义一个方法来启动周期性的定时器 this.polling = setInterval(() = { this.fetchVisitorCount(); }, 1000 * 60 * 60); // 每60000毫秒(1分钟)调用一次 }, stopPolling() { // 定义一个方法来停止定时器 clearInterval(this.polling); }, },};/script style scoped .el-card__body{ padding: 10px;}.stats-card-container { max-width: 400px; margin-bottom: 10px;} .stats-card-main { padding: 12px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);} .stats-section { text-align: center;} .stats-value-main { font-size: 24px; font-weight: bold; color: #0A74DA; margin-bottom: 4px;} .stats-label-main { font-size: 14px; color: #333;} .stats-others { display: flex; justify-content: space-between; margin-top: 12px;} .stats-item { text-align: center;} .stats-value-small, .stats-label-small { font-size: 12px; /* 减小字体尺寸以实现更紧凑的布局 */} .stats-value-small { font-weight: bold; color: #333; margin-bottom: 2px;} .stats-label-small { color: #666;} @media (max-width: 400px) { .stats-others { flex-direction: column; align-items: center; } .stats-item { margin-bottom: 8px; }}/style (2)组件使用在App.vue组件中引入VisitorLog组件,顺便将布局重新分一下,代码如下: template div id="app" el-row :gutter="10" el-col :span="20" el-row :gutter="10" el-col :span="6" v-for="(board, index) in hotBoards" :key="index" hot-search-board :title="board.title" :icon="board.icon" :fetch-url="board.fetchUrl" :type="board.type" / /el-col /el-row /el-col el-col :span="4" visitor-log / /el-col /el-row /div/template scriptimport HotSearchBoard from "@/components/HotSearchBoard.vue";import VisitorLog from "@/components/VisitorLog.vue";export default { name: "App", components: { HotSearchBoard, VisitorLog, }, data() { return { hotBoards: [ { title: "百度", icon: require("@/assets/icons/baidu-icon.svg"), type: "baidu", }, { title: "抖音", icon: require("@/assets/icons/douyin-icon.svg"), type: "douyin", }, { title: "知乎", icon: require("@/assets/icons/zhihu-icon.svg"), type: "zhihu", }, { title: "B站", icon: require("@/assets/icons/bilibili-icon.svg"), type: "bilibili", }, { title: "搜狗", icon: require("@/assets/icons/sougou-icon.svg"), type: "sougou", }, ], }; },};/script style#app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; background: #f8f9fa; /* 提供一个柔和的背景色 */ min-height: 100vh; /* 使用视口高度确保填满整个屏幕 */ padding: 0; /* 保持整体布局紧凑,无额外内边距 */}/style 咱们做出来的效果就是这样的,如下: ?这个小组件做起来还是简单的,主要就是监控了别人的访问IP,然后通过IP反解析出所属地域,最后将其存到数据库中。 ?番外:搜狗热搜爬虫1. 爬虫方案评估搜狗的热搜接口返回的一串JSON格式数据,这就很简单了,省的我们去解析dom,访问链接是:https://go.ie.sogou.com/hot_ranks 2. 网页解析代码SougouHotSearchJob.javapackage com.summo.sbmy.job.sougou; import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.google.common.collect.Lists;import com.summo.sbmy.common.model.dto.HotSearchDetailDTO;import com.summo.sbmy.dao.entity.SbmyHotSearchDO;import com.summo.sbmy.service.SbmyHotSearchService;import com.summo.sbmy.service.convert.HotSearchConvert;import com.xxl.job.core.biz.model.ReturnT;import com.xxl.job.core.handler.annotation.XxlJob;import lombok.extern.slf4j.Slf4j;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;import org.apache.commons.collections4.CollectionUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component; import java.io.IOException;import java.util.Calendar;import java.util.List;import java.util.Random;import java.util.UUID;import java.util.stream.Collectors; import static com.summo.sbmy.common.cache.SbmyHotSearchCache.CACHE_MAP;import static com.summo.sbmy.common.enums.HotSearchEnum.DOUYIN;import static com.summo.sbmy.common.enums.HotSearchEnum.SOUGOU; /** * @author summo * @version SougouHotSearchJob.java, 1.0.0 * @description 搜狗热搜Java爬虫代码 * @date 2024年08月09 */@Component@Slf4jpublic class SougouHotSearchJob { @Autowired private SbmyHotSearchService sbmyHotSearchService; @XxlJob("sougouHotSearchJob") public ReturnTString hotSearch(String param) throws IOException { log.info("搜狗热搜爬虫任务开始"); try { //查询搜狗热搜数据 OkHttpClient client = new OkHttpClient().newBuilder().build(); Request request = new Request.Builder().url("https://go.ie.sogou.com/hot_ranks").method("GET", null) .build(); Response response = client.newCall(request).execute(); JSONObject jsonObject = JSONObject.parseObject(response.body().string()); JSONArray array = jsonObject.getJSONArray("data"); ListSbmyHotSearchDO sbmyHotSearchDOList = Lists.newArrayList(); for (int i = 0, len = array.size(); i i++) { //获取知乎热搜信息 JSONObject object = (JSONObject)array.get(i); //构建热搜信息榜 SbmyHotSearchDO sbmyHotSearchDO = SbmyHotSearchDO.builder().hotSearchResource(SOUGOU.getCode()).build(); //设置知乎三方ID sbmyHotSearchDO.setHotSearchId(object.getString("id")); //设置文章标题 sbmyHotSearchDO.setHotSearchTitle(object.getJSONObject("attributes").getString("title")); //设置文章连接 sbmyHotSearchDO.setHotSearchUrl( "https://www.sogou.com/web?ie=utf8&query=" + sbmyHotSearchDO.getHotSearchTitle()); //设置热搜热度 sbmyHotSearchDO.setHotSearchHeat(object.getJSONObject("attributes").getString("num")); //按顺序排名 sbmyHotSearchDO.setHotSearchOrder(i + 1); sbmyHotSearchDOList.add(sbmyHotSearchDO); } if (CollectionUtils.isEmpty(sbmyHotSearchDOList)) { return ReturnT.SUCCESS; } //数据加到缓存中 CACHE_MAP.put(SOUGOU.getCode(), HotSearchDetailDTO.builder() //热搜数据 .hotSearchDTOList( sbmyHotSearchDOList.stream().map(HotSearchConvert::toDTOWhenQuery).collect(Collectors.toList())) //更新时间 .updateTime(Calendar.getInstance().getTime()).build()); //数据持久化 sbmyHotSearchService.saveCache2DB(sbmyHotSearchDOList); log.info("搜狗热搜爬虫任务结束"); } catch (IOException e) { log.error("获取搜狗数据异常", e); } return ReturnT.SUCCESS; }}?代码逻辑还是老一套,不难,嫌麻烦复制粘贴即可。 ?点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2024-01-24_奢侈品龙年营销精选:富贵得千姿百态 下一篇:2022-10-29_还不知道怎么找版权素材?看这里

TAG标签:

15
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价