Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions blog-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@
<artifactId>hutool-crypto</artifactId>
<version>5.8.11</version>
</dependency>
<!-- hutool-all 包含Excel导出功能 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<!-- poi 用于Excel操作 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>

<!-- devtools -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import top.naccl.model.vo.Result;
import top.naccl.service.VisitLogService;

import javax.servlet.http.HttpServletResponse;

/**
* @Description: 访问日志后台管理
* @Author: Naccl
Expand Down Expand Up @@ -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);
}
}
3 changes: 3 additions & 0 deletions blog-api/src/main/java/top/naccl/service/VisitLogService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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: 访问日志业务层实现
Expand Down Expand Up @@ -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<VisitLog> visitLogList = visitLogMapper.getVisitLogListByUUIDAndDate(uuid, startDate, endDate);

// 创建Excel数据
List<Map<String, Object>> rows = new ArrayList<>();
if (visitLogList != null && !visitLogList.isEmpty()) {
for (int i = 0; i < visitLogList.size(); i++) {
VisitLog log = visitLogList.get(i);
Map<String, Object> 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);
}
}
}
5 changes: 3 additions & 2 deletions blog-api/src/main/resources/application-dev.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions blog-cms/src/api/visitLog.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ export function deleteVisitLogById(id) {
id
}
})
}

export function exportVisitLog(queryInfo) {
return axios({
url: 'visitLog/export',
method: 'GET',
params: {
...queryInfo
},
responseType: 'blob'
})
}
4 changes: 4 additions & 0 deletions blog-cms/src/util/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
34 changes: 33 additions & 1 deletion blog-cms/src/views/log/VisitLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
<el-form-item>
<el-button type="primary" size="small" icon="el-icon-search" @click="search">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button type="success" size="small" icon="el-icon-download" @click="exportData">导出</el-button>
</el-form-item>
</el-form>

<el-table :data="logList">
Expand Down Expand Up @@ -71,7 +74,7 @@

<script>
import Breadcrumb from "@/components/Breadcrumb";
import {getVisitLogList, deleteVisitLogById} from "@/api/visitLog";
import {getVisitLogList, deleteVisitLogById, exportVisitLog} from "@/api/visitLog";
import DateTimeRangePicker from "@/components/DateTimeRangePicker";

export default {
Expand Down Expand Up @@ -132,6 +135,35 @@
setDate(value) {
this.queryInfo.date = value
},
exportData() {
let query = {...this.queryInfo}
if (query.date && query.date.length === 2) {
query.date = query.date[0] + ',' + query.date[1]
}
exportVisitLog(query).then(res => {
// 创建下载链接
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const fileName = '访问日志_' + this.$moment(new Date()).format('YYYYMMDDHHmmss') + '.xlsx'
if ('download' in document.createElement('a')) {
// 非IE下载
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
} else {
// IE10+下载
navigator.msSaveBlob(blob, fileName)
}
this.msgSuccess('导出成功')
}).catch(error => {
console.error('导出失败:', error)
this.msgError('导出失败: ' + (error.message || '未知错误'))
})
},
}
}
</script>
Expand Down
72 changes: 35 additions & 37 deletions blog-view/vue.config.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
},
}
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,
},
},
}
}
}
}