From 02c7252001a0b1fbf35991dc2d4babf6308c1557 Mon Sep 17 00:00:00 2001 From: lhy Date: Sun, 25 Jan 2026 18:31:08 +0800 Subject: [PATCH] .2174177451707596:2f7c395628783c8cd416de09028917f1_6975df77e4f68bfdcb300b54.6975e0ece4f68bfdcb300bb1.6975e0ecb2273292b99ab951:Trae CN.T(2026/1/25 17:22:52) --- blog-api/pom.xml | 12 ++++ .../controller/admin/VisitLogController.java | 22 ++++++ .../top/naccl/service/VisitLogService.java | 3 + .../service/impl/VisitLogServiceImpl.java | 66 +++++++++++++++++ .../main/resources/application-dev.properties | 5 +- blog-cms/src/api/visitLog.js | 11 +++ blog-cms/src/util/request.js | 4 ++ blog-cms/src/views/log/VisitLog.vue | 34 ++++++++- blog-view/vue.config.js | 72 +++++++++---------- 9 files changed, 189 insertions(+), 40 deletions(-) diff --git a/blog-api/pom.xml b/blog-api/pom.xml index aed43a72..8e2d6224 100644 --- a/blog-api/pom.xml +++ b/blog-api/pom.xml @@ -150,6 +150,18 @@ hutool-crypto 5.8.11 + + + cn.hutool + hutool-all + 5.8.11 + + + + org.apache.poi + poi-ooxml + 5.2.3 + diff --git a/blog-api/src/main/java/top/naccl/controller/admin/VisitLogController.java b/blog-api/src/main/java/top/naccl/controller/admin/VisitLogController.java index afe31056..c988f303 100644 --- a/blog-api/src/main/java/top/naccl/controller/admin/VisitLogController.java +++ b/blog-api/src/main/java/top/naccl/controller/admin/VisitLogController.java @@ -13,6 +13,8 @@ import top.naccl.model.vo.Result; import top.naccl.service.VisitLogService; +import javax.servlet.http.HttpServletResponse; + /** * @Description: 访问日志后台管理 * @Author: Naccl @@ -61,4 +63,24 @@ public Result delete(@RequestParam Long id) { visitLogService.deleteVisitLogById(id); return Result.ok("删除成功"); } + + /** + * 导出访问日志到Excel + * + * @param uuid 访客标识码 + * @param date 访问时间范围 + * @param response HTTP响应 + */ + @GetMapping("/visitLog/export") + public void exportVisitLog(@RequestParam(defaultValue = "") String uuid, + @RequestParam(defaultValue = "") String[] date, + HttpServletResponse response) { + String startDate = null; + String endDate = null; + if (date.length == 2) { + startDate = date[0]; + endDate = date[1]; + } + visitLogService.exportVisitLogToExcel(StringUtils.trim(uuid), startDate, endDate, response); + } } diff --git a/blog-api/src/main/java/top/naccl/service/VisitLogService.java b/blog-api/src/main/java/top/naccl/service/VisitLogService.java index fd3b4d41..2b20b7c2 100644 --- a/blog-api/src/main/java/top/naccl/service/VisitLogService.java +++ b/blog-api/src/main/java/top/naccl/service/VisitLogService.java @@ -4,6 +4,7 @@ import top.naccl.entity.VisitLog; import top.naccl.model.dto.VisitLogUuidTime; +import javax.servlet.http.HttpServletResponse; import java.util.List; public interface VisitLogService { @@ -15,4 +16,6 @@ public interface VisitLogService { void saveVisitLog(VisitLog log); void deleteVisitLogById(Long id); + + void exportVisitLogToExcel(String uuid, String startDate, String endDate, HttpServletResponse response); } diff --git a/blog-api/src/main/java/top/naccl/service/impl/VisitLogServiceImpl.java b/blog-api/src/main/java/top/naccl/service/impl/VisitLogServiceImpl.java index bf8a5613..4ab34c24 100644 --- a/blog-api/src/main/java/top/naccl/service/impl/VisitLogServiceImpl.java +++ b/blog-api/src/main/java/top/naccl/service/impl/VisitLogServiceImpl.java @@ -1,5 +1,8 @@ package top.naccl.service.impl; +import cn.hutool.core.date.DateUtil; +import cn.hutool.poi.excel.ExcelUtil; +import cn.hutool.poi.excel.ExcelWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,7 +15,15 @@ import top.naccl.util.IpAddressUtils; import top.naccl.util.UserAgentUtils; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * @Description: 访问日志业务层实现 @@ -56,4 +67,59 @@ public void deleteVisitLogById(Long id) { throw new PersistenceException("删除日志失败"); } } + + @Override + public void exportVisitLogToExcel(String uuid, String startDate, String endDate, HttpServletResponse response) { + // 如果没有设置时间范围,默认导出最近7天的数据 + if ((startDate == null || startDate.trim().isEmpty()) && (endDate == null || endDate.trim().isEmpty())) { + Date endTime = new Date(); + Date startTime = DateUtil.offsetDay(endTime, -7); + startDate = DateUtil.formatDateTime(startTime); + endDate = DateUtil.formatDateTime(endTime); + } + + // 获取符合条件的访问日志列表 + List visitLogList = visitLogMapper.getVisitLogListByUUIDAndDate(uuid, startDate, endDate); + + // 创建Excel数据 + List> rows = new ArrayList<>(); + if (visitLogList != null && !visitLogList.isEmpty()) { + for (int i = 0; i < visitLogList.size(); i++) { + VisitLog log = visitLogList.get(i); + Map row = new LinkedHashMap<>(); + row.put("序号", i + 1); + row.put("访客标识", log.getUuid()); + row.put("访问行为", log.getBehavior()); + row.put("访问内容", log.getContent()); + row.put("IP", log.getIp()); + row.put("IP来源", log.getIpSource()); + row.put("操作系统", log.getOs()); + row.put("浏览器", log.getBrowser()); + row.put("访问时间", DateUtil.format(log.getCreateTime(), "yyyy-MM-dd HH:mm:ss")); + rows.add(row); + } + } + + // 生成文件名 + String fileName = "访问日志_" + DateUtil.format(new Date(), "yyyyMMddHHmmss") + ".xlsx"; + + try { + // 设置响应头 + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setCharacterEncoding("utf-8"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8")); + + // 创建Excel写入器 + ExcelWriter writer = ExcelUtil.getWriter(); + writer.write(rows, true); + + // 写入响应流 + ServletOutputStream outputStream = response.getOutputStream(); + writer.flush(outputStream, true); + writer.close(); + outputStream.close(); + } catch (IOException e) { + throw new RuntimeException("导出Excel文件失败: " + e.getMessage(), e); + } + } } diff --git a/blog-api/src/main/resources/application-dev.properties b/blog-api/src/main/resources/application-dev.properties index 67cee1ac..4ca5ab8a 100644 --- a/blog-api/src/main/resources/application-dev.properties +++ b/blog-api/src/main/resources/application-dev.properties @@ -15,8 +15,9 @@ spring.datasource.url=jdbc:mysql://localhost:3306/nblog?useUnicode=true&characte spring.datasource.username=root spring.datasource.password=root # Redis连接信息 -spring.redis.host=192.168.17.132 -spring.redis.password=123456 +#spring.redis.host=192.168.17.132 +spring.redis.host=127.0.0.1 +#spring.redis.password=123456 spring.redis.port=6379 spring.redis.database=0 spring.redis.timeout=10000ms diff --git a/blog-cms/src/api/visitLog.js b/blog-cms/src/api/visitLog.js index 48300d41..a758cf53 100644 --- a/blog-cms/src/api/visitLog.js +++ b/blog-cms/src/api/visitLog.js @@ -18,4 +18,15 @@ export function deleteVisitLogById(id) { id } }) +} + +export function exportVisitLog(queryInfo) { + return axios({ + url: 'visitLog/export', + method: 'GET', + params: { + ...queryInfo + }, + responseType: 'blob' + }) } \ No newline at end of file diff --git a/blog-cms/src/util/request.js b/blog-cms/src/util/request.js index 4133ac9d..75955780 100644 --- a/blog-cms/src/util/request.js +++ b/blog-cms/src/util/request.js @@ -38,6 +38,10 @@ request.interceptors.request.use(config => { // 响应拦截 request.interceptors.response.use(response => { NProgress.done() + // 如果是blob类型(如下载文件),直接返回response + if (response.config.responseType === 'blob') { + return response + } const res = response.data if (res.code !== 200) { let msg = res.msg || 'Error' diff --git a/blog-cms/src/views/log/VisitLog.vue b/blog-cms/src/views/log/VisitLog.vue index b64e59df..67d6632a 100644 --- a/blog-cms/src/views/log/VisitLog.vue +++ b/blog-cms/src/views/log/VisitLog.vue @@ -13,6 +13,9 @@ 搜索 + + 导出 + @@ -71,7 +74,7 @@ diff --git a/blog-view/vue.config.js b/blog-view/vue.config.js index 37018708..e3dc0329 100644 --- a/blog-view/vue.config.js +++ b/blog-view/vue.config.js @@ -1,38 +1,36 @@ module.exports = { - configureWebpack: { - resolve: { - alias: { - 'assets': '@/assets', - 'common': '@/common', - 'components': '@/components', - 'api': '@/api', - 'views': '@/views', - 'plugins': '@/plugins' - } - } - }, - optimization: { - splitChunks: { - cacheGroups: { - vendor: { - test: /[\\/]node_modules[\\/]/, - name(module) { - // get the name. E.g. node_modules/packageName/not/this/part.js - // or node_modules/packageName - const packageName = module.context.match( - /[\\/]node_modules[\\/](.*?)([\\/]|$)/ - )[1]; - // npm package names are URL-safe, but some servers don't like @ symbols - return `npm.${packageName.replace("@", "")}`; - }, - chunks: "all", - enforce: true, - priority: 10, - minSize: 50000, // 50KB - maxSize: 200000, - reuseExistingChunk: true, - }, - }, - }, - }, -} \ No newline at end of file + configureWebpack: { + resolve: { + alias: { + 'assets': '@/assets', + 'common': '@/common', + 'components': '@/components', + 'api': '@/api', + 'views': '@/views', + 'plugins': '@/plugins' + } + }, + // 把 optimization 放到 configureWebpack 里面 + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name(module) { + const packageName = module.context.match( + /[\\/]node_modules[\\/](.*?)([\\/]|$)/ + )[1]; + return `npm.${packageName.replace("@", "")}`; + }, + chunks: "all", + enforce: true, + priority: 10, + minSize: 50000, + maxSize: 200000, + reuseExistingChunk: true, + }, + }, + } + } + } + } \ No newline at end of file