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
6 changes: 6 additions & 0 deletions blog-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@
<artifactId>commonmark-ext-task-list-items</artifactId>
<version>0.18.1</version>
</dependency>
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package top.naccl.controller.admin;

import com.alibaba.excel.EasyExcel;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -11,8 +13,16 @@
import org.springframework.web.bind.annotation.RestController;
import top.naccl.entity.VisitLog;
import top.naccl.model.vo.Result;
import top.naccl.model.vo.VisitLogExcel;
import top.naccl.service.VisitLogService;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
* @Description: 访问日志后台管理
* @Author: Naccl
Expand Down Expand Up @@ -61,4 +71,60 @@ public Result delete(@RequestParam Long id) {
visitLogService.deleteVisitLogById(id);
return Result.ok("删除成功");
}

/**
* 导出访问日志
*
* @param uuid 按访客标识码模糊查询
* @param date 按访问时间查询
* @param response HTTP响应
* @throws IOException IO异常
*/
@GetMapping("/visitLog/export")
public void exportVisitLog(@RequestParam(defaultValue = "") String uuid,
@RequestParam(defaultValue = "") String[] date,
HttpServletResponse response) throws IOException {
String startDate = null;
String endDate = null;
if (date.length == 2) {
startDate = date[0];
endDate = date[1];
} else {
// 默认导出最近7天
Date now = new Date();
endDate = DateFormatUtils.format(now, "yyyy-MM-dd HH:mm:ss");
startDate = DateFormatUtils.format(new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000), "yyyy-MM-dd HH:mm:ss");
}

List<VisitLog> visitLogList = visitLogService.getVisitLogListByUUIDAndDate(StringUtils.trim(uuid), startDate, endDate);

// 转换为Excel模型
List<VisitLogExcel> excelList = new ArrayList<>();
for (int i = 0; i < visitLogList.size(); i++) {
VisitLog visitLog = visitLogList.get(i);
VisitLogExcel excel = new VisitLogExcel();
excel.setSerialNumber(i + 1);
excel.setUuid(visitLog.getUuid());
excel.setBehavior(visitLog.getBehavior());
excel.setContent(visitLog.getContent());
excel.setIp(visitLog.getIp());
excel.setIpSource(visitLog.getIpSource());
excel.setOs(visitLog.getOs());
excel.setBrowser(visitLog.getBrowser());
excel.setCreateTime(DateFormatUtils.format(visitLog.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
excelList.add(excel);
}

// 设置响应头
String fileName = "访问日志_" + DateFormatUtils.format(new Date(), "yyyyMMddHHmmss") + ".xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);

// 写入Excel
EasyExcel.write(response.getOutputStream(), VisitLogExcel.class).sheet("访问日志").doWrite(excelList);
}
}
53 changes: 53 additions & 0 deletions blog-api/src/main/java/top/naccl/model/vo/VisitLogExcel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package top.naccl.model.vo;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;

/**
* @Description: 访问日志Excel导出模型
* @Author: Naccl
* @Date: 2025-01-25
*/
@Getter
@Setter
public class VisitLogExcel {
@ExcelProperty(value = "序号", index = 0)
@ColumnWidth(10)
private Integer serialNumber;

@ExcelProperty(value = "访客标识", index = 1)
@ColumnWidth(32)
private String uuid;

@ExcelProperty(value = "访问行为", index = 2)
@ColumnWidth(20)
private String behavior;

@ExcelProperty(value = "访问内容", index = 3)
@ColumnWidth(50)
private String content;

@ExcelProperty(value = "IP", index = 4)
@ColumnWidth(15)
private String ip;

@ExcelProperty(value = "IP来源", index = 5)
@ColumnWidth(20)
private String ipSource;

@ExcelProperty(value = "操作系统", index = 6)
@ColumnWidth(20)
private String os;

@ExcelProperty(value = "浏览器", index = 7)
@ColumnWidth(20)
private String browser;

@ExcelProperty(value = "访问时间", index = 8)
@ColumnWidth(20)
private String createTime;
}
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
53 changes: 50 additions & 3 deletions 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="exportVisitLog">导出</el-button>
</el-form-item>
</el-form>

<el-table :data="logList">
Expand Down Expand Up @@ -70,9 +73,9 @@
</template>

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

export default {
name: "VisitLog",
Expand Down Expand Up @@ -132,6 +135,50 @@
setDate(value) {
this.queryInfo.date = value
},
exportVisitLog() {
// 使用axios下载文件,避免a标签跨域问题
let params = {
uuid: this.queryInfo.uuid
}
if (this.queryInfo.date && this.queryInfo.date.length === 2) {
params.date = this.queryInfo.date
}

// 创建临时axios实例,绕过响应拦截器
const axios = require('axios')
const token = window.localStorage.getItem('token')

axios.get('http://localhost:8090/admin/visitLog/export', {
params: params,
headers: {
'Authorization': token
},
responseType: 'blob'
}).then(response => {
// 处理下载
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition']
let fileName = '访问日志.xlsx'
if (contentDisposition) {
const matches = contentDisposition.match(/filename\*=utf-8''(.*)/)
if (matches && matches.length > 1) {
fileName = decodeURIComponent(matches[1])
}
}
a.download = fileName
a.click()
window.URL.revokeObjectURL(url)
}).catch(error => {
console.error('导出失败:', error)
this.$message.error('导出失败,请稍后重试')
})
}
}
}
</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,
},
},
}
}
}
}