编辑 | blame | 历史 | 原始文档

MES 接口调用日志页面设计文档

日期: 2026-04-13
作者: Claude
版本: v0.3
状态: 已批准


1. 概述

1.1 目标

在 WMS 系统中添加 MES 接口调用日志查看页面,提供综合性的日志查询、统计和管理功能。

1.2 范围

  • 后端:API 接口、服务层扩展
  • 前端:日志列表页面、统计卡片、JSON 详情查看器
  • 数据库:菜单配置、数据字典

2. 后端设计

2.1 Controller 接口

文件路径: 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 (使用框架内置导出)

2.2 数据传输对象

2.2.1 查询请求 DTO

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; }
    }
}

2.2.2 统计数据 DTO

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; }
    }
}

2.2.3 分页响应 DTO

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; }
    }
}

2.3 Service 接口扩展

文件路径: 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);
    }
}

2.4 Controller 实现框架

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 导出
        }
    }
}

2.5 服务注册

注意: IMesLogService 已实现 IDependency 接口,无需手动注册。框架会通过 AutofacModuleRegister 自动注册所有 IDependency 实现。


3. 前端设计

3.1 文件结构

src/
├── views/
│   └── system/
│       └── Mes_Log.vue              # 主页面
├── extension/
│   └── system/
│       └── Mes_Log.jsx              # 业务扩展逻辑
├── components/
│   └── MesJsonViewer.vue            # JSON 详情查看器组件
└── router/
    └── viewGird.js                  # 添加路由配置

3.2 Mes_Log.vue 配置

注意: 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>

3.3 MesJsonViewer.vue 组件

注意: 不使用外部 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>

3.4 统计卡片组件

文件路径: 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>

3.5 自动刷新功能

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;
};

4. 数据字典配置

4.1 接口类型字典 (mesApiType)

显示文本 说明
BindContainer 电芯绑定 托盘电芯绑定操作
UnBindContainer 电芯解绑 托盘电芯解绑操作
ContainerNgReport NG上报 托盘NG电芯上报
InboundInContainer 托盘进站 托盘进站操作
OutboundInContainer 托盘出站 托盘出站操作

4.2 调用状态字典 (mesApiStatus)

显示文本 颜色
true 成功 绿色
false 失败 红色

5. 权限与菜单

5.1 菜单配置

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

5.2 权限点

  • Mes_Log:view - 查看日志列表
  • Mes_Log:detail - 查看日志详情
  • Mes_Log:export - 导出日志

6. 实现步骤

6.1 后端实现

  1. 扩展 IMesLogService 接口(添加分页、统计、导出方法)
  2. 实现 MesLogService 的扩展方法
  3. 创建 DTO 文件(MesLogQueryDto.cs, MesLogStatisticsDto.cs, MesLogListItemDto.cs
  4. 创建 MesLogController.cs
  5. 注意: 服务注册自动完成(IMesLogService 已实现 IDependency

6.2 前端实现

  1. 创建 MesJsonViewer.vue 组件(使用原生 <pre> 标签,无外部依赖)
  2. 创建 MesLogStatistics.vue 组件(使用 el-card 自定义实现)
  3. 创建 Mes_Log.jsx 扩展逻辑
  4. 创建 Mes_Log.vue 页面
  5. viewGird.js 添加路由
  6. extension 目录添加扩展文件

6.3 数据库配置

  1. 确认 Dt_MesApiLog 表已存在(参见 Database/Scripts/20260412_MesApiLog.sql
  2. 创建数据库索引(参见 6.4 节)
  3. 插入菜单记录到 Dt_Menu
  4. 插入数据字典记录到 Dt_Dictionary
  5. 分配权限给角色

6.4 数据库索引

执行以下 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

7. 技术要点

7.1 JSON 内容搜索

由于 SQL Server 对 JSON 字段支持有限,采用 LIKE 搜索:
- 适用于关键字搜索
- 注意性能影响,建议配合时间范围筛选
- 搜索字段:RequestJsonResponseJsonErrorMessage

7.2 分页性能

  • 利用索引(参见 6.4 节数据库索引)
  • 大数据量时建议增加时间范围限制(默认显示最近 7 天)
  • 考虑添加 TOP 1000 限制防止全表扫描

7.3 导出功能

  • 使用框架内置的 ServiceBase.Export() 方法
  • 通过 Magicodes.ExporterAndImporter.Excel 库实现
  • 导出文件命名:MES接口日志_YYYYMMDD_HHMMSS.xlsx
  • 支持与当前查询条件一致的导出

7.4 前端字段映射

前端字段 后端 DTO 字段 说明
dateRange[0] StartTime 开始时间
dateRange[1] EndTime 结束时间
elapsedRange[0] MinElapsedMs 最小耗时
elapsedRange[1] MaxElapsedMs 最大耗时
jsonKeyword JsonRequestKeyword, JsonResponseKeyword 同时搜索请求和响应

8. 测试要点

  1. 分页查询 - 验证各种筛选条件的组合
  2. 统计准确性 - 验证统计数据与实际数据一致
  3. JSON展示 - 验证格式化、折叠、复制功能
  4. 自动刷新 - 验证定时刷新功能正常工作
  5. 导出功能 - 验证 Excel 文件内容正确
  6. 权限控制 - 验证无权限用户无法访问

9. 后续优化建议

  1. 日志归档 - 考虑定期归档旧日志,保持主表性能
  2. 实时监控 - 集成 SignalR 实现实时日志推送
  3. 异常告警 - 失败率超过阈值时发送告警
  4. 性能分析 - 添加耗时趋势图表
  5. 接口对比 - 支持同类接口调用的对比分析