feat: 添加预算统计服务增强和日志系统改进
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s

1. 新增 BudgetStatsService:将预算统计逻辑从 BudgetService 中提取为独立服务,支持月度和年度统计,包含归档数据支持和硬性预算调整算法
2. 日志系统增强:添加请求ID追踪功能,支持通过请求ID查询关联日志,新增类名筛选功能
3. 日志解析优化:修复类名解析逻辑,正确提取 SourceContext 中的类名信息
4. 代码清理:移除不需要的方法名相关代码,简化日志筛选逻辑
This commit is contained in:
SunCheng
2026-01-22 19:06:58 +08:00
parent e2c0ab5389
commit 9e14849014
11 changed files with 2013 additions and 494 deletions

View File

@@ -1,4 +1,4 @@
import request from './request'
import request from './request'
/**
* 日志相关 API
@@ -12,6 +12,7 @@
* @param {string} [params.searchKeyword] - 搜索关键词
* @param {string} [params.logLevel] - 日志级别
* @param {string} [params.date] - 日期 (yyyyMMdd)
* @param {string} [params.className] - 类名
* @returns {Promise<{success: boolean, data: Array, total: number}>}
*/
export const getLogList = (params = {}) => {
@@ -32,3 +33,34 @@ export const getAvailableDates = () => {
method: 'get'
})
}
/**
* 获取可用的类名列表
* @param {Object} params - 查询参数
* @param {string} [params.date] - 日期 (yyyyMMdd)
* @returns {Promise<{success: boolean, data: Array}>}
*/
export const getAvailableClassNames = (params = {}) => {
return request({
url: '/Log/GetAvailableClassNames',
method: 'get',
params
})
}
/**
* 根据请求ID查询关联日志
* @param {Object} params - 查询参数
* @param {string} params.requestId - 请求ID
* @param {number} [params.pageIndex=1] - 页码
* @param {number} [params.pageSize=50] - 每页条数
* @param {string} [params.date] - 日期 (yyyyMMdd)
* @returns {Promise<{success: boolean, data: Array, total: number}>}
*/
export const getLogsByRequestId = (params = {}) => {
return request({
url: '/Log/GetLogsByRequestId',
method: 'get',
params
})
}

View File

@@ -1,4 +1,4 @@
import axios from 'axios'
import axios from 'axios'
import { showToast } from 'vant'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'
@@ -12,6 +12,15 @@ const request = axios.create({
}
})
// 生成请求ID
const generateRequestId = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
// 请求拦截器
request.interceptors.request.use(
(config) => {
@@ -20,6 +29,11 @@ request.interceptors.request.use(
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
// 添加请求ID
const requestId = generateRequestId()
config.headers['X-Request-ID'] = requestId
return config
},
(error) => {

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="page-container-flex log-view">
<van-nav-bar
title="查看日志"
@@ -28,7 +28,12 @@
<van-dropdown-item
v-model="selectedDate"
:options="dateOptions"
@change="handleSearch"
@change="handleDateChange"
/>
<van-dropdown-item
v-model="selectedClassName"
:options="classNameOptions"
@change="handleClassNameChange"
/>
</van-dropdown-menu>
</div>
@@ -66,6 +71,31 @@
<span class="log-level">{{ log.level }}</span>
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
</div>
<div
v-if="log.className || log.methodName"
class="log-source"
>
<span
v-if="log.className"
class="source-class"
>{{ log.className }}</span>
<span
v-if="log.methodName"
class="source-method"
>.{{ log.methodName }}</span>
</div>
<div
v-if="log.requestId"
class="log-request-id"
>
<span class="request-id-label">请求ID:</span>
<span
class="request-id-value"
@click="handleRequestIdClick(log.requestId)"
>
{{ log.requestId }}
</span>
</div>
<div class="log-message">
{{ log.message }}
</div>
@@ -90,7 +120,7 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { getLogList, getAvailableDates } from '@/api/log'
import { getLogList, getAvailableDates, getAvailableClassNames, getLogsByRequestId } from '@/api/log'
const router = useRouter()
@@ -110,6 +140,11 @@ const total = ref(0)
const searchKeyword = ref('')
const selectedLevel = ref('')
const selectedDate = ref('')
const selectedClassName = ref('')
// requestId 查询模式
const isRequestIdMode = ref(false)
const currentRequestId = ref('')
// 日志级别选项
const levelOptions = ref([
@@ -125,6 +160,9 @@ const levelOptions = ref([
// 日期选项
const dateOptions = ref([{ text: '全部日期', value: '' }])
// 类名选项
const classNameOptions = ref([{ text: '全部类名', value: '' }])
/**
* 返回上一页
*/
@@ -177,22 +215,37 @@ const loadLogs = async (reset = false) => {
finished.value = false
}
const params = {
pageIndex: pageIndex.value,
pageSize: pageSize.value
}
let response
if (searchKeyword.value) {
params.searchKeyword = searchKeyword.value
}
if (selectedLevel.value) {
params.logLevel = selectedLevel.value
}
if (selectedDate.value) {
params.date = selectedDate.value
}
if (isRequestIdMode.value) {
// requestId 查询模式
response = await getLogsByRequestId({
requestId: currentRequestId.value,
pageIndex: pageIndex.value,
pageSize: pageSize.value
})
} else {
// 普通查询模式
const params = {
pageIndex: pageIndex.value,
pageSize: pageSize.value
}
const response = await getLogList(params)
if (searchKeyword.value) {
params.searchKeyword = searchKeyword.value
}
if (selectedLevel.value) {
params.logLevel = selectedLevel.value
}
if (selectedDate.value) {
params.date = selectedDate.value
}
if (selectedClassName.value) {
params.className = selectedClassName.value
}
response = await getLogList(params)
}
if (response.success) {
const newLogs = response.data || []
@@ -206,12 +259,9 @@ const loadLogs = async (reset = false) => {
total.value = response.total
// 判断是否还有更多数据
// total = -1 表示总数未知,此时只根据返回数据量判断
if (total.value === -1) {
// 如果返回的数据少于请求的数量,说明没有更多了
finished.value = newLogs.length < pageSize.value
} else {
// 如果有明确的总数,则判断是否已加载完全部数据
if (logList.value.length >= total.value || newLogs.length < pageSize.value) {
finished.value = true
} else {
@@ -237,7 +287,12 @@ const loadLogs = async (reset = false) => {
* 下拉刷新
*/
const onRefresh = async () => {
await loadLogs(true)
if (isRequestIdMode.value) {
// requestId 模式下刷新,重置为第一页
await loadLogs(true)
} else {
await loadLogs(true)
}
}
/**
@@ -262,6 +317,8 @@ const onLoad = async () => {
* 搜索处理
*/
const handleSearch = () => {
isRequestIdMode.value = false
currentRequestId.value = ''
loadLogs(true)
}
@@ -291,6 +348,44 @@ const loadAvailableDates = async () => {
}
}
/**
* 加载可用类名列表
*/
const loadAvailableClassNames = async () => {
try {
const params = {}
if (selectedDate.value) {
params.date = selectedDate.value
}
const response = await getAvailableClassNames(params)
if (response.success && response.data) {
const classNames = response.data.map((name) => ({
text: name,
value: name
}))
classNameOptions.value = [{ text: '全部类名', value: '' }, ...classNames]
}
} catch (error) {
console.error('加载类名列表失败:', error)
}
}
/**
* 日期改变时重新加载类名
*/
const handleDateChange = async () => {
selectedClassName.value = ''
await loadAvailableClassNames()
handleSearch()
}
/**
* 类名改变时重新搜索
*/
const handleClassNameChange = () => {
handleSearch()
}
/**
* 格式化日期显示
*/
@@ -302,9 +397,50 @@ const formatDate = (dateStr) => {
return dateStr
}
/**
* 处理请求ID点击
*/
const handleRequestIdClick = async (requestId) => {
try {
showToast('正在查询关联日志...')
isRequestIdMode.value = true
currentRequestId.value = requestId
const response = await getLogsByRequestId({
requestId,
pageIndex: 1,
pageSize: 100
})
if (response.success && response.data && response.data.length > 0) {
logList.value = response.data
total.value = response.total
pageIndex.value = 1
// 根据返回数据量判断是否还有更多
if (response.data.length < 100) {
finished.value = true
} else {
finished.value = false
}
showToast(`找到 ${response.total} 条关联日志`)
} else {
showToast('未找到关联日志')
logList.value = []
finished.value = true
}
} catch (error) {
console.error('查询关联日志失败:', error)
showToast('查询失败')
}
}
// 组件挂载时加载数据
onMounted(() => {
loadAvailableDates()
loadAvailableClassNames()
// 不在这里调用 loadLogs让 van-list 的 @load 事件自动触发
})
</script>
@@ -399,6 +535,43 @@ onMounted(() => {
}
}
.log-request-id {
margin: 2px 0;
font-size: 10px;
color: #666;
display: flex;
align-items: center;
}
.log-source {
margin: 2px 0;
font-size: 10px;
color: #666;
display: flex;
align-items: center;
}
.source-class {
font-weight: bold;
color: var(--van-primary-color);
}
.request-id-label {
margin-right: 4px;
font-weight: bold;
}
.request-id-value {
cursor: pointer;
color: var(--van-primary-color);
text-decoration: underline;
word-break: break-all;
}
.request-id-value:hover {
opacity: 0.8;
}
.log-message {
color: #323233;
line-height: 1.4;