日期: 2026-04-13
作者: Claude
版本: v0.3
状态: 已批准
在 WMS 系统中添加 MES 接口调用日志查看页面,提供综合性的日志查询、统计和管理功能。
文件路径: WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Mes/MesLogController.cs
注意: 此控制器不继承 ApiBaseController,因为 MES 日志是只读记录,不需要完整的 CRUD 操作。仅提供查询、统计和导出功能。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/MesLog/page |
分页查询,支持所有筛选条件 |
| GET | /api/MesLog/{id} |
获取单条日志完整信息 |
| GET | /api/MesLog/statistics |
获取统计数据 |
| POST | /api/MesLog/export |
导出查询结果为 Excel (使用框架内置导出) |
namespace WIDESEA_DTO.MES
{
/// <summary>
/// MES日志查询请求DTO
/// </summary>
public class MesLogQueryDto
{
/// <summary>
/// 接口类型: BindContainer, UnBindContainer, ContainerNgReport, InboundInContainer, OutboundInContainer
/// </summary>
public string ApiType { get; set; }
/// <summary>
/// 成功状态: null-全部, true-成功, false-失败
/// </summary>
public bool? IsSuccess { get; set; }
/// <summary>
/// 开始时间(前端 dateRange 字段映射:dateRange[0] → StartTime)
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间(前端 dateRange 字段映射:dateRange[1] → EndTime)
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 创建人(操作人)
/// </summary>
public string Creator { get; set; }
/// <summary>
/// 最小耗时(毫秒)(前端 elapsedRange 字段映射:elapsedRange[0] → MinElapsedMs)
/// </summary>
public int? MinElapsedMs { get; set; }
/// <summary>
/// 最大耗时(毫秒)(前端 elapsedRange 字段映射:elapsedRange[1] → MaxElapsedMs)
/// </summary>
public int? MaxElapsedMs { get; set; }
/// <summary>
/// 错误信息关键字
/// </summary>
public string ErrorKeyword { get; set; }
/// <summary>
/// 请求JSON关键字
/// </summary>
public string JsonRequestKeyword { get; set; }
/// <summary>
/// 响应JSON关键字
/// </summary>
public string JsonResponseKeyword { get; set; }
}
}
namespace WIDESEA_DTO.MES
{
/// <summary>
/// MES日志统计数据DTO
/// </summary>
public class MesLogStatisticsDto
{
/// <summary>
/// 总调用次数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 成功次数
/// </summary>
public int SuccessCount { get; set; }
/// <summary>
/// 成功率(百分比)
/// </summary>
public double SuccessRate { get; set; }
/// <summary>
/// 失败次数
/// </summary>
public int FailedCount { get; set; }
/// <summary>
/// 平均耗时(毫秒)
/// </summary>
public double AvgElapsedMs { get; set; }
/// <summary>
/// 最大耗时(毫秒)
/// </summary>
public int MaxElapsedMs { get; set; }
/// <summary>
/// 今日调用次数
/// </summary>
public int TodayCount { get; set; }
/// <summary>
/// 各接口类型调用次数统计
/// </summary>
public Dictionary<string, int> ApiTypeCounts { get; set; }
}
}
namespace WIDESEA_DTO.MES
{
/// <summary>
/// MES日志列表项DTO
/// </summary>
public class MesLogListItemDto
{
public long Id { get; set; }
public string ApiType { get; set; }
public bool IsSuccess { get; set; }
public string RequestJsonPreview { get; set; } // 前200字符预览
public string ResponseJsonPreview { get; set; } // 前200字符预览
public string ErrorMessage { get; set; }
public int ElapsedMs { get; set; }
public DateTime CreateDate { get; set; }
public string Creator { get; set; }
}
/// <summary>
/// MES日志详情DTO
/// </summary>
public class MesLogDetailDto : MesLogListItemDto
{
public string RequestJson { get; set; } // 完整JSON
public string ResponseJson { get; set; } // 完整JSON
public DateTime? ModifyDate { get; set; }
public string Modifier { get; set; }
}
}
文件路径: WMS/WIDESEA_WMSServer/WIDESEA_IMesService/IMesLogService.cs
namespace WIDESEA_IMesService
{
public interface IMesLogService : IDependency
{
// 现有方法
Task<bool> LogAsync(MesApiLogDto log);
Task<List<MesApiLogDto>> GetRecentLogsAsync(string apiType, int count = 50);
// 新增方法
/// <summary>
/// 分页查询MES日志
/// </summary>
Task<(List<MesLogListItemDto> items, int total)> GetPageAsync(MesLogQueryDto query, int page, int pageSize);
/// <summary>
/// 获取单条日志详情
/// </summary>
Task<MesLogDetailDto> GetDetailAsync(long id);
/// <summary>
/// 获取统计数据
/// </summary>
Task<MesLogStatisticsDto> GetStatisticsAsync(MesLogQueryDto query);
/// <summary>
/// 导出日志数据
/// </summary>
Task<byte[]> ExportAsync(MesLogQueryDto query);
}
}
namespace WIDESEA_WMSServer.Controllers.Mes
{
[Route("api/MesLog")]
[ApiController]
public class MesLogController : ControllerBase
{
private readonly IMesLogService _mesLogService;
public MesLogController(IMesLogService mesLogService)
{
_mesLogService = mesLogService;
}
/// <summary>
/// 分页查询MES日志
/// </summary>
[HttpPost("page")]
public async Task<WebResponseContent> GetPage([FromBody] MesLogQueryDto query, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
// 实现分页查询
}
/// <summary>
/// 获取日志详情
/// </summary>
[HttpGet("{id}")]
public async Task<WebResponseContent> GetDetail(long id)
{
// 实现详情查询
}
/// <summary>
/// 获取统计数据
/// </summary>
[HttpGet("statistics")]
public async Task<WebResponseContent> GetStatistics([FromQuery] MesLogQueryDto query)
{
// 实现统计查询
}
/// <summary>
/// 导出日志 - 使用框架内置 ServiceBase.Export() 方法
/// 通过 HttpHelper.Post() 调用框架的通用导出接口
/// </summary>
[HttpPost("export")]
public async Task<IActionResult> Export([FromBody] MesLogQueryDto query)
{
// 调用 _mesLogService.ExportAsync() 生成数据
// 使用框架内置的 ExcelExporter 导出
}
}
}
注意: IMesLogService 已实现 IDependency 接口,无需手动注册。框架会通过 AutofacModuleRegister 自动注册所有 IDependency 实现。
src/
├── views/
│ └── system/
│ └── Mes_Log.vue # 主页面
├── extension/
│ └── system/
│ └── Mes_Log.jsx # 业务扩展逻辑
├── components/
│ └── MesJsonViewer.vue # JSON 详情查看器组件
└── router/
└── viewGird.js # 添加路由配置
注意: view-grid 组件不支持 toolbar 插槽,统计卡片需要放在 view-grid 外部。
<template>
<div class="mes-log-page">
<!-- 统计卡片区域(位于 view-grid 上方) -->
<mes-log-statistics ref="statistics" @refresh="onStatsRefresh" />
<!-- 日志列表 -->
<view-grid
ref="grid"
:columns="columns"
:detail="detail"
:editFormFields="editFormFields"
:editFormOptions="editFormOptions"
:searchFormFields="searchFormFields"
:searchFormOptions="searchFormOptions"
:table="table"
:extend="extend"
/>
</div>
</template>
<script>
import extend from "@/extension/system/Mes_Log.jsx";
import { ref, defineComponent } from "vue";
export default defineComponent({
setup() {
const table = ref({
key: "Id",
cnName: "MES接口日志",
name: "Mes_Log",
url: "/api/MesLog/",
sortName: "Id DESC",
});
const columns = ref([
{ field: "id", title: "ID", width: 80, hidden: true },
{
field: "apiType",
title: "接口类型",
width: 130,
bind: { key: "mesApiType", data: [] }
},
{
field: "isSuccess",
title: "状态",
width: 80,
bind: { key: "mesApiStatus", data: [] }
},
{
field: "requestJson",
title: "请求内容",
width: 200,
link: true,
formatter: (row) => previewJson(row.requestJson)
},
{
field: "responseJson",
title: "响应内容",
width: 200,
link: true,
formatter: (row) => previewJson(row.responseJson)
},
{ field: "errorMessage", title: "错误信息", width: 200 },
{ field: "elapsedMs", title: "耗时(ms)", width: 100, sortable: true },
{ field: "createDate", title: "调用时间", width: 160, sortable: true },
{ field: "creator", title: "操作人", width: 100 }
]);
// JSON 内容预览辅助函数(在 Mes_Log.jsx 中实现)
const previewJson = (jsonStr) => {
if (!jsonStr) return '-';
try {
const obj = JSON.parse(jsonStr);
return JSON.stringify(obj, null, 2).substring(0, 200) + '...';
} catch {
return String(jsonStr).substring(0, 200) + '...';
}
};
const searchFormOptions = ref([
[
{ field: "apiType", title: "接口类型", type: "select" },
{ field: "isSuccess", title: "状态", type: "select" },
{ field: "dateRange", title: "时间范围", type: "datetimeRange" }
],
[
{ field: "creator", title: "操作人", type: "text" },
{
field: "elapsedRange",
title: "耗时范围(ms)",
type: "numberRange",
placeholder: ["最小", "最大"]
}
],
[
{ field: "errorKeyword", title: "错误关键字", type: "text" },
{ field: "jsonKeyword", title: "JSON内容关键字", type: "text" }
]
]);
// ... 其他配置
return { table, columns, searchFormOptions, extend, /* ... */ };
}
});
</script>
注意: 不使用外部 JSON 查看器库,采用原生 <pre> 标签 + JSON.stringify() 格式化显示。
<template>
<el-dialog
v-model="visible"
:title="title"
width="800px"
:close-on-click-modal="false"
>
<el-tabs v-model="activeTab">
<el-tab-pane label="请求" name="request">
<div class="json-container">
<el-button
size="small"
class="copy-btn"
@click="copyToClipboard(formattedRequest)"
>
复制
</el-button>
<pre class="json-content">{{ formattedRequest }}</pre>
</div>
</el-tab-pane>
<el-tab-pane label="响应" name="response">
<div class="json-container">
<el-button
size="small"
class="copy-btn"
@click="copyToClipboard(formattedResponse)"
>
复制
</el-button>
<pre class="json-content">{{ formattedResponse }}</pre>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script>
import { computed, ref } from 'vue';
export default {
name: "MesJsonViewer",
props: {
modelValue: Boolean,
title: String,
requestJson: [String, Object],
responseJson: [String, Object]
},
emits: ["update:modelValue"],
setup(props) {
const activeTab = ref('request');
const formattedRequest = computed(() => {
try {
const obj = typeof props.requestJson === 'string'
? JSON.parse(props.requestJson)
: props.requestJson;
return JSON.stringify(obj, null, 2);
} catch {
return props.requestJson || '{}';
}
});
const formattedResponse = computed(() => {
try {
const obj = typeof props.responseJson === 'string'
? JSON.parse(props.responseJson)
: props.responseJson;
return JSON.stringify(obj, null, 2);
} catch {
return props.responseJson || '{}';
}
});
const copyToClipboard = (text) => {
// 优先使用现代 Clipboard API(需要 HTTPS 或 localhost)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {
// 如果失败,使用降级方案
fallbackCopy(text);
});
} else {
// 降级方案:兼容 HTTP 环境和旧浏览器
fallbackCopy(text);
}
};
const fallbackCopy = (text) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('复制失败:', err);
}
document.body.removeChild(textarea);
};
return {
visible: props.modelValue,
activeTab,
formattedRequest,
formattedResponse,
copyToClipboard
};
}
};
</script>
<style scoped>
.json-container {
position: relative;
}
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.json-content {
background: #f5f7fa;
padding: 16px;
border-radius: 4px;
max-height: 500px;
overflow: auto;
font-size: 12px;
line-height: 1.5;
}
</style>
文件路径: src/components/MesLogStatistics.vue
注意: 当前项目 Element Plus 版本为 2.2.14,不支持 el-statistic 组件(需要 2.3+)。采用自定义卡片实现。
<template>
<div class="mes-log-statistics">
<el-row :gutter="16">
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-primary">
<div class="stat-content">
<div class="stat-label">总调用次数</div>
<div class="stat-value">{{ statistics.totalCount }}</div>
<div class="stat-unit">次</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card" :class="statistics.successRate >= 90 ? 'stat-success' : 'stat-warning'">
<div class="stat-content">
<div class="stat-label">成功率</div>
<div class="stat-value">{{ statistics.successRate }}%</div>
<div class="stat-sub">
成功: {{ statistics.successCount }} / 失败: {{ statistics.failedCount }}
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-info">
<div class="stat-content">
<div class="stat-label">平均耗时</div>
<div class="stat-value">{{ Math.round(statistics.avgElapsedMs) }}</div>
<div class="stat-unit">ms</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card stat-secondary">
<div class="stat-content">
<div class="stat-label">今日调用</div>
<div class="stat-value">{{ statistics.todayCount }}</div>
<div class="stat-unit">次</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import axios from '@/api/axios';
export default {
name: "MesLogStatistics",
setup() {
const statistics = ref({
totalCount: 0,
successCount: 0,
failedCount: 0,
successRate: 0,
avgElapsedMs: 0,
todayCount: 0
});
const fetchStatistics = async () => {
const res = await axios.post('/api/MesLog/statistics', {});
if (res.status) {
statistics.value = res.data;
}
};
onMounted(() => {
fetchStatistics();
});
return { statistics, fetchStatistics };
}
};
</script>
<style scoped>
.mes-log-statistics {
margin-bottom: 16px;
}
.stat-card {
text-align: center;
}
.stat-content {
padding: 8px 0;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 4px;
}
.stat-unit {
font-size: 12px;
color: #909399;
}
.stat-sub {
font-size: 12px;
color: #606266;
margin-top: 4px;
}
.stat-primary .stat-value { color: #409EFF; }
.stat-success .stat-value { color: #67C23A; }
.stat-warning .stat-value { color: #E6A23C; }
.stat-info .stat-value { color: #909399; }
.stat-secondary .stat-value { color: #909399; }
</style>
UX 说明:
- 刷新控制位于页面右上角,与查询条件并列
- 当用户打开筛选面板、编辑查询条件或查看详情时,自动刷新暂停
- 用户手动点击查询/刷新按钮后,自动刷新重新计时
// 在 Mes_Log.jsx 中实现
const refreshOptions = [
{ label: "关闭", value: 0 },
{ label: "10秒", value: 10 },
{ label: "30秒", value: 30 },
{ label: "1分钟", value: 60 },
{ label: "5分钟", value: 300 }
];
let refreshTimer = null;
const setAutoRefresh = (interval) => {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
if (interval > 0) {
refreshTimer = setInterval(() => {
// 仅在用户未交互时刷新列表数据
if (!isUserInteracting) {
refresh();
}
}, interval * 1000);
}
};
// 用户交互时暂停刷新
const onUserInteractionStart = () => {
isUserInteracting = true;
};
const onUserInteractionEnd = () => {
isUserInteracting = false;
};
| 值 | 显示文本 | 说明 |
|---|---|---|
| BindContainer | 电芯绑定 | 托盘电芯绑定操作 |
| UnBindContainer | 电芯解绑 | 托盘电芯解绑操作 |
| ContainerNgReport | NG上报 | 托盘NG电芯上报 |
| InboundInContainer | 托盘进站 | 托盘进站操作 |
| OutboundInContainer | 托盘出站 | 托盘出站操作 |
| 值 | 显示文本 | 颜色 |
|---|---|---|
| true | 成功 | 绿色 |
| false | 失败 | 红色 |
在 Dt_Menu 表中添加:
| 字段 | 值 |
|---|---|
| ParentId | (系统管理菜单的 ID) |
| MenuName | MES接口日志 |
| Url | /Mes_Log |
| Component | views/system/Mes_Log |
| Permission | Mes_Log:view |
| Sort | (排在 Sys_Log 之后) |
| Icon | el-icon-document |
Mes_Log:view - 查看日志列表Mes_Log:detail - 查看日志详情Mes_Log:export - 导出日志IMesLogService 接口(添加分页、统计、导出方法)MesLogService 的扩展方法MesLogQueryDto.cs, MesLogStatisticsDto.cs, MesLogListItemDto.cs)MesLogController.csIMesLogService 已实现 IDependency)MesJsonViewer.vue 组件(使用原生 <pre> 标签,无外部依赖)MesLogStatistics.vue 组件(使用 el-card 自定义实现)Mes_Log.jsx 扩展逻辑Mes_Log.vue 页面viewGird.js 添加路由extension 目录添加扩展文件Dt_MesApiLog 表已存在(参见 Database/Scripts/20260412_MesApiLog.sql)Dt_Menu 表Dt_Dictionary 表执行以下 SQL 创建索引以优化查询性能:
-- 接口类型索引(用于按类型筛选)
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_MesApiLog_ApiType' AND object_id = OBJECT_ID('Dt_MesApiLog'))
BEGIN
CREATE INDEX IX_MesApiLog_ApiType ON Dt_MesApiLog(ApiType);
END
-- 创建时间索引(用于时间范围查询和排序)
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_MesApiLog_CreateDate' AND object_id = OBJECT_ID('Dt_MesApiLog'))
BEGIN
CREATE INDEX IX_MesApiLog_CreateDate ON Dt_MesApiLog(CreateDate DESC);
END
-- 成功状态索引(用于成功/失败筛选)
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_MesApiLog_IsSuccess' AND object_id = OBJECT_ID('Dt_MesApiLog'))
BEGIN
CREATE INDEX IX_MesApiLog_IsSuccess ON Dt_MesApiLog(IsSuccess);
END
-- 创建人索引(用于按操作人筛选)
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_MesApiLog_Creator' AND object_id = OBJECT_ID('Dt_MesApiLog'))
BEGIN
CREATE INDEX IX_MesApiLog_Creator ON Dt_MesApiLog(Creator);
END
由于 SQL Server 对 JSON 字段支持有限,采用 LIKE 搜索:
- 适用于关键字搜索
- 注意性能影响,建议配合时间范围筛选
- 搜索字段:RequestJson、ResponseJson、ErrorMessage
TOP 1000 限制防止全表扫描ServiceBase.Export() 方法Magicodes.ExporterAndImporter.Excel 库实现MES接口日志_YYYYMMDD_HHMMSS.xlsx| 前端字段 | 后端 DTO 字段 | 说明 |
|---|---|---|
| dateRange[0] | StartTime | 开始时间 |
| dateRange[1] | EndTime | 结束时间 |
| elapsedRange[0] | MinElapsedMs | 最小耗时 |
| elapsedRange[1] | MaxElapsedMs | 最大耗时 |
| jsonKeyword | JsonRequestKeyword, JsonResponseKeyword | 同时搜索请求和响应 |