🚀 注重版权,转载请注明原作者和原文链接
🌐 Open开袁 的官方网站已全面升级!探索更多精彩内容,尽在:https://open-yuan.com。
在这里,你将发现丰富的编程资源、深度的技术文章,以及开源项目的地址,一起加入我们的技术交流社区吧!
📱 想要随时随地获取最新动态?微信搜索公众号“全栈小袁”,一手掌握最新项目动态和技术分享。让我们一起,开启技术的无限可能!🌈
CREATE TABLE `access_log` ( `access_log_id` bigint NOT NULL AUTO_INCREMENT, `access_time` datetime NOT NULL COMMENT '访问时间', `access_ip` varchar(30) NOT NULL COMMENT '访问IP', `api_group` varchar(50) NOT NULL DEFAULT '默认' COMMENT '接口分组', `req_url` varchar(100) NOT NULL COMMENT '请求URL', `req_method` varchar(10) NOT NULL COMMENT '请求方式', `os` varchar(100) NULL DEFAULT NULL COMMENT '操作系统', `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器', `lsp` varchar(15) NULL DEFAULT NULL COMMENT '运营商', `country` varchar(15) NULL DEFAULT NULL COMMENT '国家', `province` varchar(15) NULL DEFAULT NULL COMMENT '省', `city` varchar(15) NULL DEFAULT NULL COMMENT '城市', PRIMARY KEY (`access_log_id`) ) COMMENT='访问日志表';
@Data @TableName(value = "access_log") public class AccessLog implements Serializable { private static final long serialVersionUID = 1L; /** * 访问日志ID */ @TableId(type = IdType.AUTO) private Long accessLogId; /** * 访问时间 */ private Date accessTime; /** * 访问IP */ private String accessIp; /** * 接口分组 */ private String apiGroup; /** * 请求URL */ private String reqUrl; /** * 请求方式 */ private String reqMethod; /** * 操作系统 */ private String os; /** * 浏览器 */ private String browser; /** * 运营商 */ private String lsp; /** * 国家 */ private String country; /** * 省 */ private String province; /** * 城市 */ private String city; }
@Repository public interface AccessLogMapper extends BaseMapper{ }
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 给接口分组 */ String apiGroup() default "默认"; }
案例:
@Log(apiGroup = "文章模块") @RestController @RequestMapping("/article") public class ArticleController { ...... }
ps:https://api.vvhan.com/api/getIpInfo?ip=[你的IP],这个网址是一个免费获取国家、省、市、运营商的地址
当然这种对IP地址的解析应该是放在定时任务中,每天晚上定时解析日志IP,如果解析IP的API挂了,接口会受到影响,我这里只是为了方便写在这里
@Slf4j @Component public class AccessLogInterceptor implements HandlerInterceptor { @Autowired private AccessLogMapper accessLogMapper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } try { // 获取客户端真是IP地址,这种网上很多现成代码 String accessIp = NetUtil.getRemoteHost(request); // 获取User-Agent String requestUserAgent = request.getHeader("User-Agent"); // 获取浏览器用户标识 UserAgent userAgent = UserAgentUtil.parse(requestUserAgent); HandlerMethod handlerMethod = (HandlerMethod) handler; Log logAnnotation = handlerMethod.getMethod().getDeclaringClass().getAnnotation(Log.class); AccessLog accessLog = new AccessLog(); accessLog.setAccessIp(accessIp); accessLog.setAccessTime(new Date()); if (logAnnotation != null) { accessLog.setApiGroup(logAnnotation.apiGroup()); } accessLog.setReqUrl(request.getRequestURI()); accessLog.setReqMethod(request.getMethod()); accessLog.setOs(userAgent.getOs().getName()); accessLog.setBrowser(userAgent.getBrowser().getName()); // 解析IP try { String ipParseStr = HttpUtil.get("https://api.vvhan.com/api/getIpInfo?ip=" + accessIp); JSONObject ipParseJson = JSONUtil.parseObj(ipParseStr); if (ipParseJson.getBool("success")) { JSONObject infoJson = ipParseJson.getJSONObject("info"); accessLog.setLsp(infoJson.getStr("lsp")); accessLog.setCountry(infoJson.getStr("country")); accessLog.setProvince(infoJson.getStr("prov")); accessLog.setCity(infoJson.getStr("city")); } } catch (Exception e) { accessLog.setLsp("未知"); accessLog.setCountry("未知"); accessLog.setProvince("未知"); accessLog.setCity("未知"); } accessLogMapper.insert(accessLog); } catch (Exception e) { log.error("", e); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
@Configuration public class WebMVCConfig implements WebMvcConfigurer { @Autowired private AccessLogInterceptor accessLogInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册全局日志拦截器 registry.addInterceptor(accessLogInterceptor) .addPathPatterns("/**") .excludePathPatterns("/static/**") .excludePathPatterns("/error"); // 其他拦截器...... } }
到这里就完成一个简单的全局日志拦截器了,随便发几个请求测试一下,成功记录入库!
@Data public class AccessBrowserGroupVo { private String browser; private Integer count; }
@Data public class AccessLspGroupVo { private String lsp; private Integer count; }
@Data public class AccessProvinceGroupVo { private String province; private Integer count; }
@Data public class AccessTimeGroupVo { private String accessTime; private Integer count; }
/** * FileName: R * Author: 小袁 * Date: 2022/3/12 12:23 * Description: 统一结果返回的类 */ @Data public class R{ private Boolean success; private Integer code; private String message; private T data; // 成功静态方法 public static R success() { R r = new R<>(); r.setSuccess(true); r.setCode(HttpStatusEnum.SUCCESS.getCode()); r.setMessage(HttpStatusEnum.SUCCESS.getName()); return r; } public static R success(String message) { R r = new R<>(); r.setSuccess(true); r.setCode(HttpStatusEnum.SUCCESS.getCode()); r.message(message); return r; } public static R success(T object) { R r = new R<>(); r.setData(object); r.setSuccess(true); r.setCode(HttpStatusEnum.SUCCESS.getCode()); r.setMessage(HttpStatusEnum.SUCCESS.getName()); return r; } public static R success(String msg, T object) { R r = new R<>(); r.setData(object); r.setCode(HttpStatusEnum.SUCCESS.getCode()); r.setMessage(msg); return r; } // 失败静态方法 public static R fail() { R r = new R<>(); r.setSuccess(false); r.setCode(HttpStatusEnum.FAIL.getCode()); r.setMessage(HttpStatusEnum.FAIL.getName()); return r; } public static R fail(String msg) { R r = new R<>(); r.setSuccess(false); r.setCode(HttpStatusEnum.FAIL.getCode()); r.setMessage(msg); return r; } public static R fail(HttpStatusEnum httpStatusEnum) { R r = new R<>(); r.setSuccess(false); r.setCode(httpStatusEnum.getCode()); r.setMessage(httpStatusEnum.getName()); return r; } public R message(String message){ this.setMessage(message); return this; } public R code(Integer code){ this.setCode(code); return this; } public R data(T data){ this.setData(data); return this; } }
/** * FileName: Code * Author: 小袁 * Date: 2022/5/1 23:29 * Description: 客户端响应状态码 */ public enum HttpStatusEnum implements BaseCodeEnum { SUCCESS(200, "成功"), FAIL(20001, "失败"), INTERNAL_SERVER_ERROR(500, "服务器异常"), private final Integer code; private final String name; HttpStatusEnum(int code, String msg) { this.code = code; this.name = msg; } @Override public Integer getCode() { return this.code; } @Override public String getName() { return this.name; } }
@Repository public interface AccessLogMapper extends BaseMapper{ List countLspGroupAccess(); List countBrowserGroupAccess(); List countProvinceGroupAccess(); List countTimeGroupAccess(); }
public interface AccessLogService extends IService{ List countLspGroupAccess(); List countBrowserGroupAccess(); List countProvinceGroupAccess(); List countTimeGroupAccess(); }
@Slf4j @Service public class AccessLogServiceImpl extends ServiceImplimplements AccessLogService { @Override public List countLspGroupAccess() { return this.baseMapper.countLspGroupAccess(); } @Override public List countBrowserGroupAccess() { return this.baseMapper.countBrowserGroupAccess(); } @Override public List countProvinceGroupAccess() { return this.baseMapper.countProvinceGroupAccess(); } @Override public List countTimeGroupAccess() { return this.baseMapper.countTimeGroupAccess(); } }
@RestController @RequestMapping("/stat/access") public class AccessStatController { @Autowired private AccessLogService accessLogService; /** * 查询15天内的访问次数情况-折线图 */ @GetMapping("/query_line_by_day") public R> queryAccessLogByTimeGroup() { return R.success(accessLogService.countTimeGroupAccess()); } /** * 查询省份访问占比-柱形图 */ @GetMapping("/query_col_by_province") public R
> queryAccessLogByProvinceGroup() { return R.success(accessLogService.countProvinceGroupAccess()); } /** * 查询运营商访问占比-饼图 */ @GetMapping("/query_pie_by_lsp") public R
> queryAccessLogByLspGroup() { return R.success(accessLogService.countLspGroupAccess()); } /** * 查询浏览器访问占比-饼图 */ @GetMapping("/query_pie_by_browser") public R
> queryAccessLogByBrowserGroup() { return R.success(accessLogService.countBrowserGroupAccess()); } }
npm install axios npm install echarts
import axios from 'axios' import { Message, MessageBox,} from 'element-ui' import store from '../store' import { getToken } from '@/utils/auth' import router from '@/router' // 创建axios实例 const service = axios.create({ baseURL: process.env.BASE_API, // api 的 base_url // timeout: 5000 // 请求超时时间 }) // request拦截器 service.interceptors.request.use( config => { if (store.getters.token) { config.headers['token'] = getToken() } return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) } ) // response 拦截器 service.interceptors.response.use( response => { /** * code为非200是抛错 可结合自己业务进行修改 */ const res = response.data const url = response.config.url if (res.code !== 200) { if (url.indexOf("/login") < 0 && res.code === 40005) { store.dispatch('FedLogOut').then(() => { router.push(`/login`) }) Message({ message: res.message, type: 'warning', duration: 2 * 1000, }) return Promise.resolve(res) }else if (res.code >= 40000) { Message({ message: res.message, type: 'error', duration: 3 * 1000 }) return Promise.resolve(res) }else { Message({ message: res.message, type: 'error', duration: 5 * 1000 }) return Promise.reject(new Error(res.message || 'Error')) } } else { return res } }, error => { Message({ message: error.message, type: 'error', duration: 5 * 1000 }) return Promise.reject(error) } ) export default service
import request from "../../utils/request"; export default { getAccessStatByTime() { return request({ url: '/stat/access/query_line_by_day', method: 'get' }) }, getAccessStatByProvince() { return request({ url: '/stat/access/query_col_by_province', method: 'get' }) }, getAccessStatByLsp() { return request({ url: '/stat/access/query_pie_by_lsp', method: 'get' }) }, getAccessStatByBrowser() { return request({ url: '/stat/access/query_pie_by_browser', method: 'get' }) }, }
直接引入
到这里整篇文章就结束了,我们重新捋一下整个流程
🚀 注重版权,转载请注明原作者和原文链接
🌐 Open开袁 的官方网站已全面升级!探索更多精彩内容,尽在:https://open-yuan.com。
在这里,你将发现丰富的编程资源、深度的技术文章,以及开源项目的地址,一起加入我们的技术交流社区吧!
📱 想要随时随地获取最新动态?微信搜索公众号“全栈小袁”,一手掌握最新项目动态和技术分享。让我们一起,开启技术的无限可能!🌈