添加开发者日志功能
This commit is contained in:
34
Web/src/api/log.js
Normal file
34
Web/src/api/log.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import request from './request'
|
||||
|
||||
/**
|
||||
* 日志相关 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取日志列表(分页)
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} [params.pageIndex=1] - 页码
|
||||
* @param {number} [params.pageSize=50] - 每页条数
|
||||
* @param {string} [params.searchKeyword] - 搜索关键词
|
||||
* @param {string} [params.logLevel] - 日志级别
|
||||
* @param {string} [params.date] - 日期 (yyyyMMdd)
|
||||
* @returns {Promise<{success: boolean, data: Array, total: number}>}
|
||||
*/
|
||||
export const getLogList = (params = {}) => {
|
||||
return request({
|
||||
url: '/Log/GetList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的日志日期列表
|
||||
* @returns {Promise<{success: boolean, data: Array}>}
|
||||
*/
|
||||
export const getAvailableDates = () => {
|
||||
return request({
|
||||
url: '/Log/GetAvailableDates',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -81,6 +81,12 @@ const router = createRouter({
|
||||
name: 'periodic-record',
|
||||
component: () => import('../views/PeriodicRecord.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/log',
|
||||
name: 'log',
|
||||
component: () => import('../views/LogView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
456
Web/src/views/LogView.vue
Normal file
456
Web/src/views/LogView.vue
Normal file
@@ -0,0 +1,456 @@
|
||||
<template>
|
||||
<div class="page-container-flex log-view">
|
||||
<van-nav-bar
|
||||
title="查看日志"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="handleBack"
|
||||
placeholder
|
||||
/>
|
||||
|
||||
<div class="scroll-content">
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-section">
|
||||
<van-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="输入关键词筛选日志"
|
||||
@search="handleSearch"
|
||||
@clear="handleClear"
|
||||
/>
|
||||
|
||||
<div class="filter-row">
|
||||
<van-dropdown-menu>
|
||||
<van-dropdown-item v-model="selectedLevel" :options="levelOptions" @change="handleSearch" />
|
||||
<van-dropdown-item v-model="selectedDate" :options="dateOptions" @change="handleSearch" />
|
||||
</van-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下拉刷新区域 -->
|
||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||
<!-- 加载提示 -->
|
||||
<van-loading v-if="loading && !logList.length" vertical style="padding: 50px 0">
|
||||
加载中...
|
||||
</van-loading>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
class="log-list"
|
||||
>
|
||||
<div
|
||||
v-for="(log, index) in logList"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
:class="getLevelClass(log.level)"
|
||||
>
|
||||
<div class="log-header">
|
||||
<span class="log-level">{{ log.level }}</span>
|
||||
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="log-message">{{ log.message }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<van-empty
|
||||
v-if="!loading && !logList.length"
|
||||
description="暂无日志"
|
||||
image="search"
|
||||
/>
|
||||
</van-list>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div style="height: 20px"></div>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { getLogList, getAvailableDates } from '@/api/log'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 数据状态
|
||||
const logList = ref([])
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const fetching = ref(false)
|
||||
|
||||
// 分页参数
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(100)
|
||||
const total = ref(0)
|
||||
|
||||
// 筛选参数
|
||||
const searchKeyword = ref('')
|
||||
const selectedLevel = ref('')
|
||||
const selectedDate = ref('')
|
||||
|
||||
// 日志级别选项
|
||||
const levelOptions = ref([
|
||||
{ text: '全部级别', value: '' },
|
||||
{ text: 'VRB', value: 'VRB' },
|
||||
{ text: 'DBG', value: 'DBG' },
|
||||
{ text: 'INF', value: 'INF' },
|
||||
{ text: 'WRN', value: 'WRN' },
|
||||
{ text: 'ERR', value: 'ERR' },
|
||||
{ text: 'FTL', value: 'FTL' }
|
||||
])
|
||||
|
||||
// 日期选项
|
||||
const dateOptions = ref([
|
||||
{ text: '全部日期', value: '' }
|
||||
])
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志级别对应的样式类
|
||||
*/
|
||||
const getLevelClass = (level) => {
|
||||
const levelMap = {
|
||||
'ERR': 'level-error',
|
||||
'FTL': 'level-fatal',
|
||||
'WRN': 'level-warning',
|
||||
'INF': 'level-info',
|
||||
'DBG': 'level-debug',
|
||||
'VRB': 'level-verbose'
|
||||
}
|
||||
return levelMap[level] || 'level-default'
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间显示
|
||||
*/
|
||||
const formatTime = (timestamp) => {
|
||||
// 提取时间部分,去掉时区
|
||||
const match = timestamp.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})/)
|
||||
return match ? match[1] : timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载日志数据
|
||||
*/
|
||||
const loadLogs = async (reset = false) => {
|
||||
if (fetching.value) return
|
||||
|
||||
fetching.value = true
|
||||
|
||||
try {
|
||||
if (reset) {
|
||||
pageIndex.value = 1
|
||||
logList.value = []
|
||||
finished.value = false
|
||||
}
|
||||
|
||||
const params = {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
|
||||
if (searchKeyword.value) {
|
||||
params.searchKeyword = searchKeyword.value
|
||||
}
|
||||
if (selectedLevel.value) {
|
||||
params.logLevel = selectedLevel.value
|
||||
}
|
||||
if (selectedDate.value) {
|
||||
params.date = selectedDate.value
|
||||
}
|
||||
|
||||
const response = await getLogList(params)
|
||||
|
||||
if (response.success) {
|
||||
const newLogs = response.data || []
|
||||
|
||||
if (reset) {
|
||||
logList.value = newLogs
|
||||
} else {
|
||||
logList.value = [...logList.value, ...newLogs]
|
||||
}
|
||||
|
||||
total.value = response.total
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (logList.value.length >= total.value || newLogs.length < pageSize.value) {
|
||||
finished.value = true
|
||||
} else {
|
||||
finished.value = false
|
||||
}
|
||||
} else {
|
||||
showToast(response.message || '获取日志失败')
|
||||
finished.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日志失败:', error)
|
||||
showToast('加载日志失败')
|
||||
finished.value = true
|
||||
} finally {
|
||||
fetching.value = false
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下拉刷新
|
||||
*/
|
||||
const onRefresh = async () => {
|
||||
await loadLogs(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
*/
|
||||
const onLoad = async () => {
|
||||
if (finished.value || fetching.value) return
|
||||
|
||||
// 如果是第一次加载
|
||||
if (pageIndex.value === 1 && logList.value.length === 0) {
|
||||
await loadLogs(false)
|
||||
} else {
|
||||
// 后续分页加载
|
||||
pageIndex.value++
|
||||
await loadLogs(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索处理
|
||||
*/
|
||||
const handleSearch = () => {
|
||||
loadLogs(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空搜索
|
||||
*/
|
||||
const handleClear = () => {
|
||||
searchKeyword.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载可用日期列表
|
||||
*/
|
||||
const loadAvailableDates = async () => {
|
||||
try {
|
||||
const response = await getAvailableDates()
|
||||
if (response.success && response.data) {
|
||||
const dates = response.data.map(date => ({
|
||||
text: formatDate(date),
|
||||
value: date
|
||||
}))
|
||||
dateOptions.value = [
|
||||
{ text: '全部日期', value: '' },
|
||||
...dates
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载日期列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期显示
|
||||
*/
|
||||
const formatDate = (dateStr) => {
|
||||
// dateStr 格式: 20251229
|
||||
if (dateStr.length === 8) {
|
||||
return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`
|
||||
}
|
||||
return dateStr
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadAvailableDates()
|
||||
// 不在这里调用 loadLogs,让 van-list 的 @load 事件自动触发
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-view {
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.log-view {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background-color: #ffffff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.filter-section {
|
||||
background-color: #2c2c2c;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
background-color: #ffffff;
|
||||
border-left: 3px solid #1989fa;
|
||||
margin-bottom: 4px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.log-item {
|
||||
background-color: #2c2c2c;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.log-item:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #969799;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.log-time {
|
||||
color: #858585;
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #323233;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.log-message {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 不同日志级别的颜色 */
|
||||
.level-verbose .log-level {
|
||||
background-color: #b0b0b0;
|
||||
}
|
||||
|
||||
.level-debug .log-level {
|
||||
background-color: #1989fa;
|
||||
}
|
||||
|
||||
.level-info .log-level {
|
||||
background-color: #07c160;
|
||||
}
|
||||
|
||||
.level-warning .log-level {
|
||||
background-color: #ff976a;
|
||||
border-left-color: #ff976a;
|
||||
}
|
||||
|
||||
.level-error .log-level {
|
||||
background-color: #ee0a24;
|
||||
}
|
||||
|
||||
.level-fatal .log-level {
|
||||
background-color: #8b0000;
|
||||
}
|
||||
|
||||
.level-default .log-level {
|
||||
background-color: #646566;
|
||||
}
|
||||
|
||||
.level-verbose {
|
||||
border-left-color: #b0b0b0;
|
||||
}
|
||||
|
||||
.level-debug {
|
||||
border-left-color: #1989fa;
|
||||
}
|
||||
|
||||
.level-info {
|
||||
border-left-color: #07c160;
|
||||
}
|
||||
|
||||
.level-warning {
|
||||
border-left-color: #ff976a;
|
||||
}
|
||||
|
||||
.level-error {
|
||||
border-left-color: #ee0a24;
|
||||
}
|
||||
|
||||
.level-fatal {
|
||||
border-left-color: #8b0000;
|
||||
}
|
||||
|
||||
.level-default {
|
||||
border-left-color: #646566;
|
||||
}
|
||||
|
||||
/* 优化下拉菜单样式 */
|
||||
:deep(.van-dropdown-menu) {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:deep(.van-dropdown-menu) {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,12 @@
|
||||
<van-cell title="智能分类" is-link @click="handleSmartClassification" />
|
||||
<van-cell title="自然语言分类" is-link @click="handleNaturalLanguageClassification" />
|
||||
</van-cell-group>
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<p>开发者</p>
|
||||
</div>
|
||||
<van-cell-group inset>
|
||||
<van-cell title="查看日志" is-link @click="handleLogView" />
|
||||
</van-cell-group>
|
||||
|
||||
<div class="detail-header" style="padding-bottom: 5px;">
|
||||
<p>账户</p>
|
||||
@@ -145,6 +151,13 @@ const handleLogout = async () => {
|
||||
showToast('已取消退出登录')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查看日志
|
||||
*/
|
||||
const handleLogView = () => {
|
||||
router.push({ name: 'log' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
228
WebApi/Controllers/LogController.cs
Normal file
228
WebApi/Controllers/LogController.cs
Normal file
@@ -0,0 +1,228 @@
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class LogController(ILogger<LogController> logger) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取日志列表(分页)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<PagedResponse<LogEntry>> GetListAsync(
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? searchKeyword = null,
|
||||
[FromQuery] string? logLevel = null,
|
||||
[FromQuery] string? date = null
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取日志目录
|
||||
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
if (!Directory.Exists(logDirectory))
|
||||
{
|
||||
return new PagedResponse<LogEntry>
|
||||
{
|
||||
Success = true,
|
||||
Data = [],
|
||||
Total = 0,
|
||||
Message = "日志目录不存在"
|
||||
};
|
||||
}
|
||||
|
||||
// 确定要读取的日志文件
|
||||
string logFilePath;
|
||||
if (!string.IsNullOrEmpty(date))
|
||||
{
|
||||
logFilePath = Path.Combine(logDirectory, $"log-{date}.txt");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认读取今天的日志
|
||||
var today = DateTime.Now.ToString("yyyyMMdd");
|
||||
logFilePath = Path.Combine(logDirectory, $"log-{today}.txt");
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!System.IO.File.Exists(logFilePath))
|
||||
{
|
||||
return new PagedResponse<LogEntry>
|
||||
{
|
||||
Success = true,
|
||||
Data = [],
|
||||
Total = 0,
|
||||
Message = "日志文件不存在"
|
||||
};
|
||||
}
|
||||
|
||||
// 读取所有日志行(使用共享读取模式,允许其他进程写入)
|
||||
var allLines = await ReadAllLinesAsync(logFilePath);
|
||||
var logEntries = new List<LogEntry>();
|
||||
|
||||
foreach (var line in allLines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var logEntry = ParseLogLine(line);
|
||||
if (logEntry != null)
|
||||
{
|
||||
// 应用筛选条件
|
||||
if (!string.IsNullOrEmpty(searchKeyword) &&
|
||||
!logEntry.Message.Contains(searchKeyword, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(logLevel) &&
|
||||
!logEntry.Level.Equals(logLevel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
logEntries.Add(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
// 倒序排列(最新的在前面)
|
||||
logEntries.Reverse();
|
||||
|
||||
var total = logEntries.Count;
|
||||
var skip = (pageIndex - 1) * pageSize;
|
||||
var pagedData = logEntries.Skip(skip).Take(pageSize).ToList();
|
||||
|
||||
return new PagedResponse<LogEntry>
|
||||
{
|
||||
Success = true,
|
||||
Data = pagedData.ToArray(),
|
||||
Total = total,
|
||||
Message = "获取日志成功"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取日志失败");
|
||||
return new PagedResponse<LogEntry>
|
||||
{
|
||||
Success = false,
|
||||
Data = [],
|
||||
Total = 0,
|
||||
Message = $"获取日志失败: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取可用的日志日期列表
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public IActionResult GetAvailableDates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var logDirectory = Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
if (!Directory.Exists(logDirectory))
|
||||
{
|
||||
return Ok(new { success = true, data = new List<string>() });
|
||||
}
|
||||
|
||||
var logFiles = Directory.GetFiles(logDirectory, "log-*.txt");
|
||||
var dates = logFiles
|
||||
.Select(f => Path.GetFileNameWithoutExtension(f))
|
||||
.Select(name => name.Replace("log-", ""))
|
||||
.OrderByDescending(d => d)
|
||||
.ToList();
|
||||
|
||||
return Ok(new { success = true, data = dates });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "获取日志日期列表失败");
|
||||
return Ok(new { success = false, message = $"获取日志日期列表失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单行日志
|
||||
/// </summary>
|
||||
private LogEntry? ParseLogLine(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 日志格式示例: [2025-12-29 16:38:45.730 +08:00] [INF] Message here
|
||||
// 使用正则表达式解析
|
||||
var match = System.Text.RegularExpressions.Regex.Match(
|
||||
line,
|
||||
@"^\[(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3}\s+[+-]\d{2}:\d{2})\]\s+\[(\w+)\]\s+(.*)$"
|
||||
);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
return new LogEntry
|
||||
{
|
||||
Timestamp = match.Groups[1].Value,
|
||||
Level = match.Groups[2].Value,
|
||||
Message = match.Groups[3].Value
|
||||
};
|
||||
}
|
||||
|
||||
// 如果不匹配标准格式,将整行作为消息
|
||||
return new LogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"),
|
||||
Level = "LOG",
|
||||
Message = line
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取文件所有行(支持共享读取)
|
||||
/// </summary>
|
||||
private async Task<string[]> ReadAllLinesAsync(string path)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
|
||||
using (var fileStream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite))
|
||||
using (var streamReader = new StreamReader(fileStream))
|
||||
{
|
||||
string? line;
|
||||
while ((line = await streamReader.ReadLineAsync()) != null)
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志条目
|
||||
/// </summary>
|
||||
public class LogEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间戳
|
||||
/// </summary>
|
||||
public string Timestamp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 日志级别
|
||||
/// </summary>
|
||||
public string Level { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 日志消息
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -17,8 +17,8 @@ public static class Expand
|
||||
q.AddTrigger(opts => opts
|
||||
.ForJob(emailJobKey)
|
||||
.WithIdentity("EmailSyncTrigger")
|
||||
.WithCronSchedule("0 0/20 * * * ?") // 每20分钟执行
|
||||
.WithDescription("每20分钟同步一次邮件"));
|
||||
.WithCronSchedule("0 0/10 * * * ?") // 每10分钟执行
|
||||
.WithDescription("每10分钟同步一次邮件"));
|
||||
|
||||
// 配置周期性账单任务 - 每天早上6点执行
|
||||
var periodicBillJobKey = new JobKey("PeriodicBillJob");
|
||||
|
||||
Reference in New Issue
Block a user