diff --git a/.kiro/specs/audit-log-chinese-fields/.config.kiro b/.kiro/specs/audit-log-chinese-fields/.config.kiro new file mode 100644 index 0000000..61ffb73 --- /dev/null +++ b/.kiro/specs/audit-log-chinese-fields/.config.kiro @@ -0,0 +1 @@ +{"specId": "a3f7c2e1-8d4b-4f9a-b6e3-1c5d8a2f7e90", "workflowType": "requirements-first", "specType": "feature"} \ No newline at end of file diff --git a/.kiro/specs/audit-log-chinese-fields/design.md b/.kiro/specs/audit-log-chinese-fields/design.md new file mode 100644 index 0000000..11d8af1 --- /dev/null +++ b/.kiro/specs/audit-log-chinese-fields/design.md @@ -0,0 +1,528 @@ +# 设计文档:审计日志中文字段名 + +## 概述 + +本设计将审计日志系统的 OldData/NewData JSON 键名从 C# camelCase 属性名替换为中文字段名。核心改动分为两部分: + +1. **后端**:修改 `BaseController` 的序列化辅助方法 + 每个 UPDATE 方法手动构造差量 JSON +2. **前端**:修改 `translateJson()` 方法,兼容新格式(中文 key)和旧格式(camelCase key) + +设计原则:INSERT/DELETE 使用通用辅助方法(字典映射)做全量中文序列化;UPDATE 由每个方法手动构造仅包含修改字段的 JSON。 + +## 架构 + +```mermaid +flowchart TD + subgraph Backend["后端 - 控制器层"] + BC[BaseController] + BC -->|提供| SFA["SerializeForAudit(entity, mapping)"] + BC -->|提供| BAL["BuildAuditLog()"] + + C1[OdfCableFaultsController] + C2[OdfCablesController] + C3[OdfMarkerPolesController] + C4[OdfRacksController] + C5[OdfPortsController] + C6[OdfFramesController] + C7[OdfRoomsController] + + C1 & C2 & C3 & C4 & C5 & C6 & C7 -->|INSERT/DELETE 调用| SFA + C1 & C2 & C3 & C4 & C5 & C6 & C7 -->|UPDATE 手动构造 JSON| ManualJSON["手动构造差量 JSON"] + end + + subgraph Frontend["前端 - OdfAuditLogs.vue"] + TJ["translateJson()"] + TJ -->|检测第一个 key| Detect{是否中文?} + Detect -->|是| Direct["直接格式化展示"] + Detect -->|否| Translate["使用 fieldLabelMap 翻译"] + end + + Backend -->|写入 odf_audit_logs| DB[(数据库)] + DB -->|读取| Frontend +``` + +## 组件与接口 + +### 1. BaseController 改造 + +#### 1.1 新增 `BuildAuditJson` 方法 + +唯一的辅助方法,用于将 `Dictionary` 序列化为紧凑 JSON,自动格式化 DateTime: + +```csharp +/// +/// 将字段字典序列化为审计 JSON(用于所有操作类型) +/// +protected static string BuildAuditJson(Dictionary fields) +{ + var result = new Dictionary(); + foreach (var kv in fields) + { + if (kv.Value is DateTime dt) + { + result[kv.Key] = dt.ToString("yyyy-MM-dd HH:mm:ss"); + } + else + { + result[kv.Key] = kv.Value; + } + } + return System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); +} +``` + +#### 1.2 保留 `BuildAuditLog()` 不变 + +`BuildAuditLog()` 方法逻辑完全不变,仍然负责构造 `OdfAuditLogs` 对象。 + +#### 1.3 不新增 `SerializeForAuditChinese` 方法 + +所有操作(INSERT/DELETE/UPDATE)都在每个方法内部手动构造 `Dictionary`,通过 `BuildAuditJson` 序列化。不使用任何通用序列化 + 字段映射字典的方式。 + +### 2. 每个实体的中文字段名映射字典 + +每个控制器文件中定义一个 `static readonly Dictionary` 作为该实体的字段映射。也可以统一放在 `BaseController` 或一个独立的静态类中。推荐放在各自控制器中,保持就近原则。 + +#### OdfCableFaults 映射 +```csharp +private static readonly Dictionary CableFaultsFieldMapping = new() +{ + ["id"] = "Id", + ["cableId"] = "关联光缆ID", + ["faultTime"] = "故障时间", + ["personnel"] = "人员", + ["faultReason"] = "故障原因", + ["mileage"] = "表显故障里程", + ["mileageCorrection"] = "表显里程矫正", + ["location"] = "地点描述", + ["latitude"] = "纬度", + ["longitude"] = "经度", + ["remark"] = "备注", + ["userId"] = "提交人用户ID", + ["createdAt"] = "创建时间", + ["updatedAt"] = "更新时间", + ["faultCount"] = "故障发生频次", +}; +``` + +#### OdfCables 映射 +```csharp +private static readonly Dictionary CablesFieldMapping = new() +{ + ["id"] = "Id", + ["cableName"] = "光缆名称", + ["deptId"] = "部门ID", + ["deptName"] = "部门名称", + ["createdAt"] = "创建时间", + ["updatedAt"] = "更新时间", +}; +``` + +#### OdfMarkerPoles 映射 +```csharp +private static readonly Dictionary MarkerPolesFieldMapping = new() +{ + ["id"] = "Id", + ["cableId"] = "关联光缆ID", + ["name"] = "名称", + ["recordTime"] = "记录时间", + ["personnel"] = "责任人", + ["latitude"] = "纬度", + ["longitude"] = "经度", + ["actualMileage"] = "实际里程", + ["deptId"] = "部门ID", + ["deptName"] = "部门名称", + ["userId"] = "提交人用户ID", + ["createdAt"] = "创建时间", + ["updatedAt"] = "更新时间", +}; +``` + +#### OdfRacks 映射 +```csharp +private static readonly Dictionary RacksFieldMapping = new() +{ + ["id"] = "Id", + ["roomId"] = "机房ID", + ["sequenceNumber"] = "序号", + ["rackName"] = "ODF名称", + ["frameCount"] = "框数量", + ["createdAt"] = "创建时间", + ["updatedAt"] = "修改时间", + ["deptId"] = "部门ID", + ["rackType"] = "机架类型", +}; +``` + +#### OdfPorts 映射 +```csharp +private static readonly Dictionary PortsFieldMapping = new() +{ + ["id"] = "Id", + ["name"] = "端口名称", + ["roomId"] = "机房ID", + ["roomName"] = "机房名称", + ["rackId"] = "机架ID", + ["rackName"] = "机架名称", + ["frameId"] = "框ID", + ["frameName"] = "框名称", + ["deptId"] = "部门ID", + ["deptName"] = "部门名称", + ["rowNumber"] = "行号", + ["portNumber"] = "端口号", + ["status"] = "连接状态", + ["remarks"] = "备注", + ["opticalAttenuation"] = "光衰值", + ["historyRemarks"] = "历史故障", + ["createdAt"] = "创建时间", + ["updatedAt"] = "修改时间", + ["opticalCableOffRemarks"] = "光缆断信息", + ["equipmentModel"] = "设备型号", + ["businessType"] = "业务类型", + ["portSide"] = "端口侧", +}; +``` + +#### OdfFrames 映射 +```csharp +private static readonly Dictionary FramesFieldMapping = new() +{ + ["id"] = "Id", + ["rackId"] = "机架ID", + ["portsName"] = "名称", + ["deptId"] = "部门ID", + ["sequenceNumber"] = "序号", + ["portsCount"] = "端口数量", + ["portsCol"] = "列数量", + ["portsRow"] = "行数量", + ["createdAt"] = "创建时间", + ["updateAt"] = "修改时间", +}; +``` + +#### OdfRooms 映射 +```csharp +private static readonly Dictionary RoomsFieldMapping = new() +{ + ["id"] = "Id", + ["deptId"] = "部门ID", + ["deptName"] = "部门名称", + ["roomName"] = "机房名称", + ["roomAddress"] = "机房位置", + ["racksCount"] = "机架数量", + ["remarks"] = "备注", + ["createdAt"] = "创建时间", + ["updatedAt"] = "修改时间", +}; +``` + +### 3. INSERT/DELETE 操作模式 + +INSERT 和 DELETE 也使用 `BuildAuditJson` 手动构造,列出实体的所有字段。 + +#### INSERT 模式(以 OdfCableFaultsController.Add 为例) + +```csharp +// 查询插入后的完整记录 +var newRecord = _OdfCableFaultsService.GetFirst(f => f.Id == response); +string newData = BuildAuditJson(new Dictionary +{ + ["Id"] = newRecord.Id, + ["关联光缆ID"] = newRecord.CableId, + ["故障时间"] = newRecord.FaultTime, + ["人员"] = newRecord.Personnel, + ["故障原因"] = newRecord.FaultReason, + ["表显故障里程"] = newRecord.Mileage, + ["表显里程矫正"] = newRecord.MileageCorrection, + ["地点描述"] = newRecord.Location, + ["纬度"] = newRecord.Latitude, + ["经度"] = newRecord.Longitude, + ["备注"] = newRecord.Remark, + ["提交人用户ID"] = newRecord.UserId, + ["创建时间"] = newRecord.CreatedAt, + ["更新时间"] = newRecord.UpdatedAt, + ["故障发生频次"] = newRecord.FaultCount, +}); +_auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", newRecord.Id, "INSERT", null, newData)); +``` + +#### DELETE 模式(以 OdfCableFaultsController.Delete 为例) + +```csharp +var oldRecord = _OdfCableFaultsService.GetFirst(f => f.Id == id); +string oldData = BuildAuditJson(new Dictionary +{ + ["Id"] = oldRecord.Id, + ["关联光缆ID"] = oldRecord.CableId, + ["故障时间"] = oldRecord.FaultTime, + // ... 所有字段 +}); +_auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "DELETE", oldData, null)); +``` + +### 4. UPDATE 操作模式 — 手动构造差量 JSON + +每个 UPDATE 方法需要: +1. 更新前:读取旧记录中该方法会修改的特定字段 +2. 更新后:读取新记录中相同字段 +3. 手动构造包含中文键名的 JSON + +#### 辅助方法:`BuildAuditJson` + +在 `BaseController` 中新增一个轻量辅助方法,用于将 `Dictionary` 序列化为紧凑 JSON,并自动格式化 DateTime: + +```csharp +/// +/// 将字段字典序列化为审计 JSON(用于 UPDATE 差量记录) +/// +protected static string BuildAuditJson(Dictionary fields) +{ + var result = new Dictionary(); + foreach (var kv in fields) + { + if (kv.Value is DateTime dt) + { + result[kv.Key] = dt.ToString("yyyy-MM-dd HH:mm:ss"); + } + else + { + result[kv.Key] = kv.Value; + } + } + return System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); +} +``` + +#### 示例:OdfCableFaultsController.IncrementFaultCount + +需求 3.3 要求仅记录:故障发生频次, 更新时间 + +```csharp +// 更新前 +var oldFault = _OdfCableFaultsService.GetFirst(f => f.Id == id); +string oldData = BuildAuditJson(new Dictionary +{ + ["故障发生频次"] = oldFault.FaultCount, + ["更新时间"] = oldFault.UpdatedAt, +}); + +// 执行更新 +var response = _OdfCableFaultsService.IncrementFaultCount(id); + +// 更新后 +var newFault = _OdfCableFaultsService.GetFirst(f => f.Id == id); +string newData = BuildAuditJson(new Dictionary +{ + ["故障发生频次"] = newFault.FaultCount, + ["更新时间"] = newFault.UpdatedAt, +}); + +_auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "UPDATE", oldData, newData)); +``` + +#### 示例:OdfPortsController.SaveMOdfPorts + +需求 3.10 要求仅记录:连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 + +```csharp +// 更新前 +var oldRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); +string oldData = BuildAuditJson(new Dictionary +{ + ["连接状态"] = oldRecord.Status, + ["备注"] = oldRecord.Remarks, + ["光衰值"] = oldRecord.OpticalAttenuation, + ["历史故障"] = oldRecord.HistoryRemarks, + ["光缆断信息"] = oldRecord.OpticalCableOffRemarks, + ["设备型号"] = oldRecord.EquipmentModel, + ["业务类型"] = oldRecord.BusinessType, + ["修改时间"] = oldRecord.UpdatedAt, +}); + +// ... 执行更新逻辑 ... + +// 更新后 +var newRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); +string newData = BuildAuditJson(new Dictionary +{ + ["连接状态"] = newRecord.Status, + ["备注"] = newRecord.Remarks, + ["光衰值"] = newRecord.OpticalAttenuation, + ["历史故障"] = newRecord.HistoryRemarks, + ["光缆断信息"] = newRecord.OpticalCableOffRemarks, + ["设备型号"] = newRecord.EquipmentModel, + ["业务类型"] = newRecord.BusinessType, + ["修改时间"] = newRecord.UpdatedAt, +}); + +_auditLogsService.AddLog(BuildAuditLog("odf_ports", parm.Id, "UPDATE", oldData, newData)); +``` + +### 5. 每个 UPDATE 方法的字段清单 + +| 控制器 | 方法 | 记录字段(中文键名) | +|--------|------|---------------------| +| OdfCableFaultsController | IncrementFaultCount | 故障发生频次, 更新时间 | +| OdfCableFaultsController | UpdateMileageCorrection | 表显里程矫正, 更新时间 | +| OdfCablesController | Update | 光缆名称, 部门ID, 部门名称, 更新时间 | +| OdfMarkerPolesController | Update | 名称, 记录时间, 责任人, 纬度, 经度, 实际里程, 更新时间 | +| OdfRacksController | UpdateOdfRacks | ODF名称, 序号, 框数量, 机架类型, 修改时间 | +| OdfPortsController | GetOdfPortsStatus | 连接状态, 修改时间 | +| OdfPortsController | UpdateOdfPorts | 端口名称, 连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 | +| OdfPortsController | SaveMOdfPorts | 连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 | +| OdfPortsController | GetOdfPortsEmpty | 连接状态, 备注, 历史故障, 光衰值, 光缆断信息, 修改时间 | +| OdfFramesController | UpdateOdfFrames | 名称, 序号, 端口数量, 列数量, 行数量, 修改时间 | +| OdfRoomsController | UpdateOdfRooms | 机房名称, 机房位置, 部门ID, 部门名称, 机架数量, 备注, 修改时间 | + + +### 6. 前端 OdfAuditLogs.vue 适配 + +#### 6.1 简化 JSON 展示逻辑 + +后端已直接存储中文 key 的 JSON,前端不再需要翻译。移除 `translateJson` 方法和 `fieldLabelMap`,改为直接格式化展示。 + +```javascript +// 格式化 JSON 字符串用于展示 +function formatJson(jsonStr) { + if (!jsonStr) return '(无数据)' + try { + return JSON.stringify(JSON.parse(jsonStr), null, 2) + } catch { + return jsonStr + } +} +``` + +对话框中的 `:model-value="translateJson(detailRow.oldData, detailRow.tableName)"` 改为 `:model-value="formatJson(detailRow.oldData)"`,NewData 同理。 + +#### 6.2 移除的代码 + +- 移除 `fieldLabelMap` 对象(整个字段映射定义) +- 移除 `translateJson` 方法 +- 移除 `isChinese` 方法(如果有) + +#### 6.3 保持不变的部分 + +- `tableNameMap` 保留(表名中文映射) +- 对话框布局(左右对比)保留 +- 查询、分页、排序逻辑保留 + +## 数据模型 + +本需求不涉及数据库表结构变更。`odf_audit_logs` 表的 `OldData` 和 `NewData` 列仍为 JSON 字符串,仅 JSON 内容的键名从 camelCase 变为中文。 + +### JSON 格式对比 + +**INSERT 改造前:** +```json +{"id":1,"cableId":5,"faultTime":"2025-01-15T10:30:00","faultReason":"光缆断裂","mileage":"12.5"} +``` + +**INSERT 改造后:** +```json +{"Id":1,"关联光缆ID":5,"故障时间":"2025-01-15 10:30:00","故障原因":"光缆断裂","表显故障里程":"12.5"} +``` + +**UPDATE 改造前(全量):** +```json +{"id":1,"cableId":5,"faultTime":"...","personnel":"...","faultReason":"...","mileage":"...","mileageCorrection":"...","location":"...","latitude":0,"longitude":0,"remark":"...","userId":1,"createdAt":"...","updatedAt":"...","faultCount":1} +``` + +**UPDATE 改造后(差量,IncrementFaultCount):** +```json +{"故障发生频次":2,"更新时间":"2025-01-15 10:30:00"} +``` + +## 正确性属性 + +*正确性属性是在系统所有有效执行中都应成立的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。* + +### Property 1: 中文键名序列化正确性 + +*For any* 实体对象和对应的字段映射字典,调用 `SerializeForAuditChinese(entity, mapping)` 生成的 JSON 字符串中,所有键名都应存在于映射字典的值集合中(即全部为中文或 "Id"),且不应包含任何 camelCase 属性名。 + +**Validates: Requirements 1.1, 2.1, 3.2** + +### Property 2: INSERT/DELETE 全量字段覆盖 + +*For any* 实体对象和对应的字段映射字典,调用 `SerializeForAuditChinese(entity, mapping)` 生成的 JSON 字符串中,键的数量应等于映射字典的条目数,且每个映射字典中的中文字段名都应作为键出现在 JSON 中。 + +**Validates: Requirements 1.2, 2.2, 6.1-6.7** + +### Property 3: JSON 紧凑格式 + +*For any* 通过 `SerializeForAuditChinese` 或 `BuildAuditJson` 生成的 JSON 字符串,该字符串不应包含换行符(`\n`)或连续空格缩进。 + +**Validates: Requirements 5.1** + +### Property 4: DateTime 格式化与 null 处理 + +*For any* 包含 DateTime 类型字段的实体,序列化后的 JSON 中对应值应匹配 `yyyy-MM-dd HH:mm:ss` 格式(正则 `^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)。*For any* 值为 null 的字段,序列化后 JSON 中该键的值应为 `null`。 + +**Validates: Requirements 5.4, 5.5** + +### Property 5: 前端直接展示中文 key JSON + +*For any* 审计日志的 OldData/NewData JSON 字符串,前端 `formatJson` 方法应直接解析并格式化展示,不做任何键名翻译。 + +**Validates: Requirements 8.1, 8.2** + +## 错误处理 + +- 所有审计日志写入操作保持现有的 try-catch 模式,失败时通过 NLog 记录错误,不影响主业务流程 +- `SerializeForAuditChinese` 在 entity 为 null 时返回 null +- `BuildAuditJson` 在字典值为 null 时保留 null 值 +- 前端 `translateJson` 在 JSON 解析失败时返回原始字符串 + +## 测试策略 + +### 单元测试 + +针对以下具体场景编写单元测试: + +1. **SerializeForAuditChinese 方法**: + - 测试每个实体类型(OdfPorts, OdfCableFaults 等)的序列化输出键名是否正确 + - 测试 null 字段的序列化 + - 测试 DateTime 字段的格式化 + +2. **BuildAuditJson 方法**: + - 测试 DateTime 值的格式化 + - 测试 null 值的处理 + - 测试输出为紧凑 JSON + +3. **每个 UPDATE 方法的字段覆盖**: + - 验证每个 UPDATE 方法构造的 JSON 仅包含需求 3 中指定的字段 + +4. **前端 translateJson**: + - 测试中文键名 JSON 直接展示 + - 测试 camelCase 键名 JSON 翻译展示 + - 测试空 JSON 和异常 JSON 的处理 + +### 属性测试 + +使用 **FsCheck**(C# 属性测试库)和 **fast-check**(JavaScript 属性测试库)。 + +- 每个属性测试至少运行 100 次迭代 +- 每个测试用注释标注对应的设计属性编号 +- 标签格式:**Feature: audit-log-chinese-fields, Property {number}: {property_text}** + +属性测试覆盖: +- Property 1: 生成随机实体 → 序列化 → 验证所有键为中文 +- Property 2: 生成随机实体 → 序列化 → 验证键数量等于映射字典大小 +- Property 3: 生成随机实体 → 序列化 → 验证无换行无缩进 +- Property 4: 生成包含随机 DateTime 和 null 的实体 → 序列化 → 验证格式 +- Property 5: 生成随机中文键/英文键 JSON → 调用 translateJson → 验证行为 + +## 需要修改的文件清单 + +| 文件 | 改动类型 | +|------|---------| +| `server/Infrastructure/Controllers/BaseController.cs` | 新增 `SerializeForAuditChinese` 和 `BuildAuditJson` 方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs` | 新增映射字典 + 改造 4 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs` | 新增映射字典 + 改造 3 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs` | 新增映射字典 + 改造 3 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs` | 新增映射字典 + 改造 4 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs` | 新增映射字典 + 改造 7 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs` | 新增映射字典 + 改造 3 个方法 | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs` | 新增映射字典 + 改造 4 个方法 | +| `server/ZR.Vue/src/views/business/OdfAuditLogs.vue` | 修改 `translateJson` 方法 + 新增 `isChinese` 函数 | diff --git a/.kiro/specs/audit-log-chinese-fields/requirements.md b/.kiro/specs/audit-log-chinese-fields/requirements.md new file mode 100644 index 0000000..3611111 --- /dev/null +++ b/.kiro/specs/audit-log-chinese-fields/requirements.md @@ -0,0 +1,141 @@ +# 需求文档:审计日志中文字段名 + +## 简介 + +当前系统的审计日志(odf_audit_logs)中,OldData/NewData 的 JSON 使用 C# 属性的 camelCase 命名作为键名。本需求要求将所有审计日志的 JSON 键名替换为中文字段名(来源于 Model 的 XML 注释),并且 UPDATE 操作仅记录该方法实际修改的字段,而非整个实体。每个控制器方法需手动构造自己的审计 JSON,不使用通用序列化方法。 + +## 术语表 + +- **审计日志系统(Audit_Log_System)**: 负责记录 odf_audit_logs 表中 OldData/NewData JSON 数据的子系统 +- **中文字段名映射(Chinese_Field_Mapping)**: C# 属性名到中文名称的对应关系,来源于 Model 类的 XML 注释 +- **全量中文序列化(Full_Chinese_Serialization)**: 将实体的所有字段以中文键名序列化为 JSON 的操作,用于 INSERT 和 DELETE +- **差量中文序列化(Diff_Chinese_Serialization)**: 仅将方法实际修改的字段以中文键名序列化为 JSON 的操作,用于 UPDATE +- **OdfPorts**: 端口实体,对应 odf_ports 表 +- **OdfCableFaults**: 干线故障实体,对应 odf_cable_faults 表 +- **OdfCables**: 光缆实体,对应 odf_cables 表 +- **OdfRacks**: 机架实体,对应 odf_racks 表 +- **OdfFrames**: 框实体,对应 odf_frames 表 +- **OdfRooms**: 机房实体,对应 odf_rooms 表 +- **OdfMarkerPoles**: 标石/杆号牌实体,对应 odf_marker_poles 表 + +## 需求 + +### 需求 1:INSERT 操作使用中文字段名全量序列化 + +**用户故事:** 作为系统管理员,我希望 INSERT 审计日志的 NewData 使用中文字段名作为 JSON 键,以便在审计日志查看界面直接看懂每个字段的含义。 + +#### 验收标准 + +1. WHEN 任意控制器方法执行 INSERT 操作并写入审计日志时,THE 审计日志系统 SHALL 将 NewData 的 JSON 键名全部替换为对应的中文字段名 +2. THE 审计日志系统 SHALL 在 INSERT 操作中记录实体的所有字段(全量),OldData 为 null +3. WHEN OdfCableFaultsController.Add 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, CableId→关联光缆ID, FaultTime→故障时间, Personnel→人员, FaultReason→故障原因, Mileage→表显故障里程, MileageCorrection→表显里程矫正, Location→地点描述, Latitude→纬度, Longitude→经度, Remark→备注, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间, FaultCount→故障发生频次 +4. WHEN OdfCablesController.Add 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, CableName→光缆名称, DeptId→部门ID, DeptName→部门名称, CreatedAt→创建时间, UpdatedAt→更新时间 +5. WHEN OdfMarkerPolesController.Add 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, CableId→关联光缆ID, Name→名称, RecordTime→记录时间, Personnel→责任人, Latitude→纬度, Longitude→经度, ActualMileage→实际里程, DeptId→部门ID, DeptName→部门名称, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间 +6. WHEN OdfRacksController.AddOdfRacks 或 AddOdfRacksExpert 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, RoomId→机房ID, SequenceNumber→序号, RackName→ODF名称, FrameCount→框数量, CreatedAt→创建时间, UpdatedAt→修改时间, DeptId→部门ID, RackType→机架类型 +7. WHEN OdfPortsController.AddOdfPorts 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, Name→端口名称, RoomId→机房ID, RoomName→机房名称, RackId→机架ID, RackName→机架名称, FrameId→框ID, FrameName→框名称, DeptId→部门ID, DeptName→部门名称, RowNumber→行号, PortNumber→端口号, Status→连接状态, Remarks→备注, OpticalAttenuation→光衰值, HistoryRemarks→历史故障, CreatedAt→创建时间, UpdatedAt→修改时间, OpticalCableOffRemarks→光缆断信息, EquipmentModel→设备型号, BusinessType→业务类型, PortSide→端口侧 +8. WHEN OdfFramesController.AddOdfFrames 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, RackId→机架ID, PortsName→名称, DeptId→部门ID, SequenceNumber→序号, PortsCount→端口数量, PortsCol→列数量, PortsRow→行数量, CreatedAt→创建时间, UpdateAt→修改时间 +9. WHEN OdfRoomsController.AddOdfRooms 或 AddExpertOdfRooms 执行 INSERT 时,THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewData:Id→Id, DeptId→部门ID, DeptName→部门名称, RoomName→机房名称, RoomAddress→机房位置, RacksCount→机架数量, Remarks→备注, CreatedAt→创建时间, UpdatedAt→修改时间 + + +### 需求 2:DELETE 操作使用中文字段名全量序列化 + +**用户故事:** 作为系统管理员,我希望 DELETE 审计日志的 OldData 使用中文字段名作为 JSON 键,以便在审计日志中清晰看到被删除记录的完整内容。 + +#### 验收标准 + +1. WHEN 任意控制器方法执行 DELETE 操作并写入审计日志时,THE 审计日志系统 SHALL 将 OldData 的 JSON 键名全部替换为对应的中文字段名 +2. THE 审计日志系统 SHALL 在 DELETE 操作中记录实体的所有字段(全量),NewData 为 null +3. WHEN OdfCableFaultsController.Delete 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfCableFaults 的中文字段名映射生成 OldData +4. WHEN OdfCablesController.Delete 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfCables 的中文字段名映射生成 OldData +5. WHEN OdfMarkerPolesController.Delete 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfMarkerPoles 的中文字段名映射生成 OldData +6. WHEN OdfRacksController.DeleteOdfRacks 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfRacks 的中文字段名映射生成 OldData +7. WHEN OdfPortsController.DeleteOdfPorts 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfPorts 的中文字段名映射生成 OldData +8. WHEN OdfPortsController.DeleteAll 执行 DELETE 时,THE 审计日志系统 SHALL 对每个被删除的端口使用 OdfPorts 的中文字段名映射生成 OldData +9. WHEN OdfFramesController.DeleteOdfFrames 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfFrames 的中文字段名映射生成 OldData +10. WHEN OdfRoomsController.DeleteOdfRooms 执行 DELETE 时,THE 审计日志系统 SHALL 使用 OdfRooms 的中文字段名映射生成 OldData + +### 需求 3:UPDATE 操作仅记录实际修改字段并使用中文字段名 + +**用户故事:** 作为系统管理员,我希望 UPDATE 审计日志的 OldData/NewData 仅包含该方法实际修改的字段(而非整个实体),并使用中文字段名,以便快速定位每次操作具体改了什么。 + +#### 验收标准 + +1. WHEN 任意控制器方法执行 UPDATE 操作并写入审计日志时,THE 审计日志系统 SHALL 仅在 OldData 和 NewData 中包含该方法实际修改的字段 +2. THE 审计日志系统 SHALL 在 UPDATE 操作的 OldData 和 NewData 中使用中文字段名作为 JSON 键 +3. WHEN OdfCableFaultsController.IncrementFaultCount 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:故障发生频次, 更新时间 +4. WHEN OdfCableFaultsController.UpdateMileageCorrection 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:表显里程矫正, 更新时间 +5. WHEN OdfCablesController.Update 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:光缆名称, 部门ID, 部门名称, 更新时间 +6. WHEN OdfMarkerPolesController.Update 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:名称, 记录时间, 责任人, 纬度, 经度, 实际里程, 更新时间 +7. WHEN OdfRacksController.UpdateOdfRacks 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:ODF名称, 序号, 框数量, 机架类型, 修改时间 +8. WHEN OdfPortsController.GetOdfPortsStatus 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:连接状态, 修改时间 +9. WHEN OdfPortsController.UpdateOdfPorts 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:端口名称, 连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 +10. WHEN OdfPortsController.SaveMOdfPorts 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 +11. WHEN OdfPortsController.GetOdfPortsEmpty 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:连接状态, 备注, 历史故障, 光衰值, 光缆断信息, 修改时间 +12. WHEN OdfFramesController.UpdateOdfFrames 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:名称, 序号, 端口数量, 列数量, 行数量, 修改时间 +13. WHEN OdfRoomsController.UpdateOdfRooms 执行 UPDATE 时,THE 审计日志系统 SHALL 仅记录以下字段:机房名称, 机房位置, 部门ID, 部门名称, 机架数量, 备注, 修改时间 + +### 需求 4:每个方法手动构造审计 JSON + +**用户故事:** 作为开发者,我希望每个控制器方法手动构造自己的审计 JSON(而非使用通用序列化方法),因为每个方法修改的字段和业务逻辑各不相同。 + +#### 验收标准 + +1. THE 审计日志系统 SHALL 要求每个控制器方法在自身代码中手动构造 OldData/NewData 的 JSON 字符串 +2. THE 审计日志系统 SHALL 保留 BaseController 中的 BuildAuditLog 方法用于构造 OdfAuditLogs 对象 +3. THE 审计日志系统 SHALL 允许 INSERT 和 DELETE 操作使用辅助方法(如改造后的 SerializeForAudit 或新的中文序列化辅助方法)进行全量中文序列化 +4. THE 审计日志系统 SHALL 要求 UPDATE 操作在每个方法内部手动构造仅包含修改字段的 JSON +5. IF 某个 UPDATE 方法的业务逻辑发生变化导致修改字段增减,THEN THE 审计日志系统 SHALL 要求开发者同步更新该方法的审计 JSON 构造代码 + +### 需求 5:审计日志 JSON 格式规范 + +**用户故事:** 作为系统管理员,我希望审计日志的 JSON 格式统一规范,以便前端展示和数据分析。 + +#### 验收标准 + +1. THE 审计日志系统 SHALL 生成的 JSON 字符串不包含缩进(紧凑格式) +2. THE 审计日志系统 SHALL 保持 odf_audit_logs 表的 OldData 和 NewData 列存储格式为 JSON 字符串 +3. THE 审计日志系统 SHALL 保持 odf_audit_logs 表的 TableName、RecordId、OperationType、OperatorId、OperatorName、SourceClient、OperationTime、DeptId、Remark 等字段的写入逻辑不变 +4. WHEN 实体字段值为 null 时,THE 审计日志系统 SHALL 在 JSON 中将该字段值序列化为 null +5. WHEN 实体字段值为 DateTime 类型时,THE 审计日志系统 SHALL 将其序列化为 "yyyy-MM-dd HH:mm:ss" 格式的字符串 + +### 需求 6:中文字段名映射完整性 + +**用户故事:** 作为开发者,我希望所有 7 个实体的所有字段都有明确的中文字段名映射,以确保审计日志的一致性。 + +#### 验收标准 + +1. THE 审计日志系统 SHALL 为 OdfPorts 的 22 个字段提供完整的中文字段名映射:Id→Id, Name→端口名称, RoomId→机房ID, RoomName→机房名称, RackId→机架ID, RackName→机架名称, FrameId→框ID, FrameName→框名称, DeptId→部门ID, DeptName→部门名称, RowNumber→行号, PortNumber→端口号, Status→连接状态, Remarks→备注, OpticalAttenuation→光衰值, HistoryRemarks→历史故障, CreatedAt→创建时间, UpdatedAt→修改时间, OpticalCableOffRemarks→光缆断信息, EquipmentModel→设备型号, BusinessType→业务类型, PortSide→端口侧 +2. THE 审计日志系统 SHALL 为 OdfCableFaults 的 15 个字段提供完整的中文字段名映射:Id→Id, CableId→关联光缆ID, FaultTime→故障时间, Personnel→人员, FaultReason→故障原因, Mileage→表显故障里程, MileageCorrection→表显里程矫正, Location→地点描述, Latitude→纬度, Longitude→经度, Remark→备注, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间, FaultCount→故障发生频次 +3. THE 审计日志系统 SHALL 为 OdfCables 的 6 个字段提供完整的中文字段名映射:Id→Id, CableName→光缆名称, DeptId→部门ID, DeptName→部门名称, CreatedAt→创建时间, UpdatedAt→更新时间 +4. THE 审计日志系统 SHALL 为 OdfRacks 的 8 个字段提供完整的中文字段名映射:Id→Id, RoomId→机房ID, SequenceNumber→序号, RackName→ODF名称, FrameCount→框数量, CreatedAt→创建时间, UpdatedAt→修改时间, DeptId→部门ID, RackType→机架类型 +5. THE 审计日志系统 SHALL 为 OdfFrames 的 10 个字段提供完整的中文字段名映射:Id→Id, RackId→机架ID, PortsName→名称, DeptId→部门ID, SequenceNumber→序号, PortsCount→端口数量, PortsCol→列数量, PortsRow→行数量, CreatedAt→创建时间, UpdateAt→修改时间 +6. THE 审计日志系统 SHALL 为 OdfRooms 的 9 个字段提供完整的中文字段名映射:Id→Id, DeptId→部门ID, DeptName→部门名称, RoomName→机房名称, RoomAddress→机房位置, RacksCount→机架数量, Remarks→备注, CreatedAt→创建时间, UpdatedAt→修改时间 +7. THE 审计日志系统 SHALL 为 OdfMarkerPoles 的 13 个字段提供完整的中文字段名映射:Id→Id, CableId→关联光缆ID, Name→名称, RecordTime→记录时间, Personnel→责任人, Latitude→纬度, Longitude→经度, ActualMileage→实际里程, DeptId→部门ID, DeptName→部门名称, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间 + +### 需求 7:涉及的控制器方法完整清单 + +**用户故事:** 作为开发者,我希望有一份完整的需要修改的控制器方法清单,以确保不遗漏任何审计日志调用点。 + +#### 验收标准 + +1. THE 审计日志系统 SHALL 修改 OdfCableFaultsController 的以下 4 个方法:Add(INSERT)、IncrementFaultCount(UPDATE)、UpdateMileageCorrection(UPDATE)、Delete(DELETE) +2. THE 审计日志系统 SHALL 修改 OdfCablesController 的以下 3 个方法:Add(INSERT)、Update(UPDATE)、Delete(DELETE) +3. THE 审计日志系统 SHALL 修改 OdfMarkerPolesController 的以下 3 个方法:Add(INSERT)、Update(UPDATE)、Delete(DELETE) +4. THE 审计日志系统 SHALL 修改 OdfRacksController 的以下 4 个方法:AddOdfRacks(INSERT)、AddOdfRacksExpert(INSERT)、UpdateOdfRacks(UPDATE)、DeleteOdfRacks(DELETE) +5. THE 审计日志系统 SHALL 修改 OdfPortsController 的以下 7 个方法:GetOdfPortsStatus(UPDATE)、AddOdfPorts(INSERT)、UpdateOdfPorts(UPDATE)、SaveMOdfPorts(UPDATE)、GetOdfPortsEmpty(UPDATE)、DeleteOdfPorts(DELETE)、DeleteAll(DELETE) +6. THE 审计日志系统 SHALL 修改 OdfFramesController 的以下 3 个方法:AddOdfFrames(INSERT)、UpdateOdfFrames(UPDATE)、DeleteOdfFrames(DELETE) +7. THE 审计日志系统 SHALL 修改 OdfRoomsController 的以下 4 个方法:AddOdfRooms(INSERT)、AddExpertOdfRooms(INSERT)、UpdateOdfRooms(UPDATE)、DeleteOdfRooms(DELETE) +8. THE 审计日志系统 SHALL 总计修改 7 个控制器中的 28 个方法 + + +### 需求 8:前端"修改统计"页面适配 + +**用户故事:** 作为系统管理员,我希望管理后台的"修改统计"页面直接展示后端存储的中文 key JSON,不再需要前端翻译。 + +#### 验收标准 + +1. THE 前端 SHALL 移除 `translateJson` 方法,改为直接格式化展示 JSON(仅做 `JSON.stringify(obj, null, 2)`) +2. THE 前端 SHALL 移除 `fieldLabelMap` 字段映射对象(不再需要前端翻译) +3. THE 前端 SHALL 保持现有的表名中文映射(tableNameMap)不变 +4. THE 前端 SHALL 保持现有的对话框布局(左右对比 OldData/NewData)不变 +5. THE 前端 SHALL 移除未使用的 `formatJson` 方法 diff --git a/.kiro/specs/audit-log-chinese-fields/tasks.md b/.kiro/specs/audit-log-chinese-fields/tasks.md new file mode 100644 index 0000000..af57dcc --- /dev/null +++ b/.kiro/specs/audit-log-chinese-fields/tasks.md @@ -0,0 +1,115 @@ +# 实施计划:审计日志中文字段名 + +## 概述 + +将审计日志系统的 OldData/NewData JSON 键名从 camelCase 替换为中文字段名。所有操作(INSERT/DELETE/UPDATE)都在每个方法内部手动构造 `Dictionary`,通过 `BuildAuditJson` 序列化。不使用通用序列化方法,不使用字段映射字典。前端移除 `translateJson` 和 `fieldLabelMap`,直接用 `formatJson` 展示。 + +## 任务 + +- [x] 1. BaseController 新增 BuildAuditJson 方法 + - [x] 1.1 在 `server/Infrastructure/Controllers/BaseController.cs` 中新增 `BuildAuditJson(Dictionary fields)` 方法 + - 遍历字典,DateTime 值格式化为 `yyyy-MM-dd HH:mm:ss` + - 返回紧凑 JSON 字符串 + - _需求: 1.1, 3.1, 4.2, 5.1, 5.4, 5.5_ + +- [x] 2. OdfCableFaultsController 改造(4 个方法) + - [x] 2.1 改造 `Add` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 15 个字段(中文键名) + - 字段:Id, 关联光缆ID, 故障时间, 人员, 故障原因, 表显故障里程, 表显里程矫正, 地点描述, 纬度, 经度, 备注, 提交人用户ID, 创建时间, 更新时间, 故障发生频次 + - _需求: 1.3, 7.1_ + - [x] 2.2 改造 `IncrementFaultCount` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:故障发生频次, 更新时间 + - _需求: 3.3, 7.1_ + - [x] 2.3 改造 `UpdateMileageCorrection` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:表显里程矫正, 更新时间 + - _需求: 3.4, 7.1_ + - [x] 2.4 改造 `Delete` 方法(DELETE):循环中用 `BuildAuditJson` 手动构造 oldData,列出全部 15 个字段 + - _需求: 2.3, 7.1_ + +- [x] 3. OdfCablesController 改造(3 个方法) + - [x] 3.1 改造 `Add` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 6 个字段 + - 字段:Id, 光缆名称, 部门ID, 部门名称, 创建时间, 更新时间 + - _需求: 1.4, 7.2_ + - [x] 3.2 改造 `Update` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:光缆名称, 部门ID, 部门名称, 更新时间 + - _需求: 3.5, 7.2_ + - [x] 3.3 改造 `Delete` 方法(DELETE):用 `BuildAuditJson` 手动构造 oldData,列出全部 6 个字段 + - _需求: 2.4, 7.2_ + +- [x] 4. OdfMarkerPolesController 改造(3 个方法) + - [x] 4.1 改造 `Add` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 13 个字段 + - 字段:Id, 关联光缆ID, 名称, 记录时间, 责任人, 纬度, 经度, 实际里程, 部门ID, 部门名称, 提交人用户ID, 创建时间, 更新时间 + - _需求: 1.5, 7.3_ + - [x] 4.2 改造 `Update` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:名称, 记录时间, 责任人, 纬度, 经度, 实际里程, 更新时间 + - _需求: 3.6, 7.3_ + - [x] 4.3 改造 `Delete` 方法(DELETE):循环中用 `BuildAuditJson` 手动构造 oldData,列出全部 13 个字段 + - _需求: 2.5, 7.3_ + +- [x] 5. OdfRacksController 改造(4 个方法) + - [x] 5.1 改造 `AddOdfRacks` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 9 个字段 + - 字段:Id, 机房ID, 序号, ODF名称, 框数量, 创建时间, 修改时间, 部门ID, 机架类型 + - _需求: 1.6, 7.4_ + - [x] 5.2 改造 `AddOdfRacksExpert` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 9 个字段 + - _需求: 1.6, 7.4_ + - [x] 5.3 改造 `UpdateOdfRacks` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:ODF名称, 序号, 框数量, 机架类型, 修改时间 + - _需求: 3.7, 7.4_ + - [x] 5.4 改造 `DeleteOdfRacks` 方法(DELETE):循环中用 `BuildAuditJson` 手动构造 oldData,列出全部 9 个字段 + - _需求: 2.6, 7.4_ + +- [x] 6. OdfPortsController 改造(7 个方法) + - [x] 6.1 改造 `AddOdfPorts` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 22 个字段 + - 字段:Id, 端口名称, 机房ID, 机房名称, 机架ID, 机架名称, 框ID, 框名称, 部门ID, 部门名称, 行号, 端口号, 连接状态, 备注, 光衰值, 历史故障, 创建时间, 修改时间, 光缆断信息, 设备型号, 业务类型, 端口侧 + - _需求: 1.7, 7.5_ + - [x] 6.2 改造 `GetOdfPortsStatus` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:连接状态, 修改时间 + - _需求: 3.8, 7.5_ + - [x] 6.3 改造 `UpdateOdfPorts` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:端口名称, 连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 + - _需求: 3.9, 7.5_ + - [x] 6.4 改造 `SaveMOdfPorts` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:连接状态, 备注, 光衰值, 历史故障, 光缆断信息, 设备型号, 业务类型, 修改时间 + - _需求: 3.10, 7.5_ + - [x] 6.5 改造 `GetOdfPortsEmpty` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:连接状态, 备注, 历史故障, 光衰值, 光缆断信息, 修改时间 + - _需求: 3.11, 7.5_ + - [x] 6.6 改造 `DeleteOdfPorts` 方法(DELETE):循环中用 `BuildAuditJson` 手动构造 oldData,列出全部 22 个字段 + - _需求: 2.7, 7.5_ + - [x] 6.7 改造 `DeleteAll` 方法(DELETE):三个分支(Level 2/3/4)中循环用 `BuildAuditJson` 手动构造 oldData,列出全部 22 个字段 + - _需求: 2.8, 7.5_ + +- [x] 7. OdfFramesController 改造(3 个方法) + - [x] 7.1 改造 `AddOdfFrames` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 10 个字段 + - 字段:Id, 机架ID, 名称, 部门ID, 序号, 端口数量, 列数量, 行数量, 创建时间, 修改时间 + - _需求: 1.8, 7.6_ + - [x] 7.2 改造 `UpdateOdfFrames` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:名称, 序号, 端口数量, 列数量, 行数量, 修改时间 + - _需求: 3.12, 7.6_ + - [x] 7.3 改造 `DeleteOdfFrames` 方法(DELETE):循环中用 `BuildAuditJson` 手动构造 oldData,列出全部 10 个字段 + - _需求: 2.9, 7.6_ + +- [x] 8. OdfRoomsController 改造(4 个方法) + - [x] 8.1 改造 `AddOdfRooms` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 9 个字段 + - 字段:Id, 部门ID, 部门名称, 机房名称, 机房位置, 机架数量, 备注, 创建时间, 修改时间 + - _需求: 1.9, 7.7_ + - [x] 8.2 改造 `AddExpertOdfRooms` 方法(INSERT):用 `BuildAuditJson` 手动构造 newData,列出全部 9 个字段 + - _需求: 1.9, 7.7_ + - [x] 8.3 改造 `UpdateOdfRooms` 方法(UPDATE):用 `BuildAuditJson` 手动构造 oldData/newData + - 仅记录字段:机房名称, 机房位置, 部门ID, 部门名称, 机架数量, 备注, 修改时间 + - _需求: 3.13, 7.7_ + - [x] 8.4 改造 `DeleteOdfRooms` 方法(DELETE):用 `BuildAuditJson` 手动构造 oldData,列出全部 9 个字段 + - _需求: 2.10, 7.7_ + +- [x] 9. 前端 OdfAuditLogs.vue 适配 + - [x] 9.1 移除 `fieldLabelMap` 对象 + - _需求: 8.2_ + - [x] 9.2 移除 `translateJson` 方法 + - _需求: 8.1_ + - [x] 9.3 将对话框中 `translateJson(...)` 调用替换为 `formatJson(...)` 调用 + - _需求: 8.1_ + - [x] 9.4 保留 `tableNameMap` 和 `formatJson` 方法不变 + - _需求: 8.3_ + +- [x] 10. 编译验证 + - 后端 `dotnet build` 编译通过 + - 前端无语法错误 diff --git a/.kiro/specs/audit-log-manual/.config.kiro b/.kiro/specs/audit-log-manual/.config.kiro new file mode 100644 index 0000000..9a22f99 --- /dev/null +++ b/.kiro/specs/audit-log-manual/.config.kiro @@ -0,0 +1,5 @@ +{ + "specType": "feature", + "workflowType": "requirements-first", + "featureName": "audit-log-manual" +} diff --git a/.kiro/specs/audit-log-manual/design.md b/.kiro/specs/audit-log-manual/design.md new file mode 100644 index 0000000..cf46b53 --- /dev/null +++ b/.kiro/specs/audit-log-manual/design.md @@ -0,0 +1,400 @@ +# 技术设计文档 — 审计日志手动化改造 + +## 概述 + +将 ODF 系统中所有业务数据的审计日志记录方式从 `OdfAuditLogFilter` 拦截器自动模式改为 Controller 中手动记录。改造涉及 7 个 Controller、27 个接口,改造完成后移除拦截器。 + +## 架构 + +### 当前架构(拦截器模式) + +``` +Controller Action + ↓ +[ServiceFilter(typeof(OdfAuditLogFilter))] + ↓ 操作前:GetExistingDataJson() 查旧数据 + ↓ 执行 Action + ↓ 操作后:GetNewDataFromActionArguments() 从请求参数序列化新数据 + ↓ 写入 odf_audit_logs +``` + +问题:`GetNewDataFromActionArguments` 只能序列化复杂类型 DTO,对 `int id`、`string ids` 等简单类型参数返回 null。 + +### 目标架构(手动模式) + +``` +Controller Action 内部 + ↓ 操作前:查询旧数据 → JSON 序列化为 OldData + ↓ 执行业务操作 + ↓ 操作后:查询新数据 → JSON 序列化为 NewData + ↓ 构造 OdfAuditLogs 对象 → _auditLogsService.AddLog() +``` + +## 涉及文件 + +### 需要修改的文件 + +| 文件 | 改动内容 | +|------|---------| +| `server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs` | 去掉拦截器,手动审计(IncrementFaultCount 已完成,剩余 Add/UpdateMileageCorrection/Delete) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs` | 去掉拦截器,手动审计(Add/Update/Delete) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs` | 去掉拦截器,手动审计(Add/AddExpert/Update/Delete) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs` | 去掉拦截器,手动审计(Status/Add/Update/SaveM/Empty/Delete/DeleteAll) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs` | 去掉拦截器,手动审计(Add/Update/Delete) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs` | 新增审计日志(Add/Update/Delete) | +| `server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs` | 新增审计日志(Add/AddExpert/Update/Delete) | + +### 需要删除的文件 + +| 文件 | 原因 | +|------|------| +| `server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs` | 拦截器不再使用 | + +### 需要修改的配置 + +| 文件 | 改动内容 | +|------|---------| +| `server/ZR.Admin.WebApi/Program.cs` | 移除 `builder.Services.AddScoped()` | + +## 组件与接口 + +### 审计日志服务接口 + +已有 `IOdfAuditLogsService`,提供 `AddLog(OdfAuditLogs log)` 方法,无需修改。 + +### Controller 注入变更 + +每个需要审计的 Controller 需注入 `IOdfAuditLogsService`: + +```csharp +private readonly IOdfAuditLogsService _auditLogsService; + +public XxxController( + IXxxService xxxService, + IOdfAuditLogsService auditLogsService) +{ + _xxxService = xxxService; + _auditLogsService = auditLogsService; +} +``` + +### JSON 序列化配置 + +每个 Controller 中定义统一的序列化选项(复用): + +```csharp +private static readonly JsonSerializerOptions _jsonOptions = new() +{ + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase +}; +``` + +### 操作来源判断 + +从 `HttpContext.Request.Headers["X-Source-Client"]` 获取,逻辑与原拦截器一致: + +```csharp +private string GetSourceClient() +{ + if (HttpContext.Request.Headers.TryGetValue("X-Source-Client", out var src) + && !string.IsNullOrWhiteSpace(src.ToString())) + { + return src.ToString().Equals("App", StringComparison.OrdinalIgnoreCase) ? "App" : "Admin"; + } + return "Admin"; +} +``` + +建议将此方法放入 `BaseController` 中,避免每个 Controller 重复定义。同理 `_jsonOptions` 也可放入 `BaseController`。 + +## 各操作类型的审计模式 + +### INSERT(新增) + +```csharp +// 执行插入 +var entity = service.Add(model); + +// 查询插入后的完整记录 +var newRecord = service.GetFirst(f => f.Id == entity.Id); +string newData = newRecord != null ? JsonSerializer.Serialize(newRecord, _jsonOptions) : null; + +_auditLogsService.AddLog(new OdfAuditLogs +{ + TableName = "表名", + RecordId = entity.Id, + OperationType = "INSERT", + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = null, + NewData = newData, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, +}); +``` + +### UPDATE(修改) + +```csharp +// 查旧数据 +var oldRecord = service.GetFirst(f => f.Id == id); +string oldData = oldRecord != null ? JsonSerializer.Serialize(oldRecord, _jsonOptions) : null; + +// 执行更新 +service.Update(model); + +// 查新数据 +var newRecord = service.GetFirst(f => f.Id == id); +string newData = newRecord != null ? JsonSerializer.Serialize(newRecord, _jsonOptions) : null; + +_auditLogsService.AddLog(new OdfAuditLogs +{ + TableName = "表名", + RecordId = id, + OperationType = "UPDATE", + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = oldData, + NewData = newData, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, +}); +``` + +### DELETE(删除) + +```csharp +// 查旧数据(删除前) +var oldRecord = service.GetFirst(f => f.Id == id); +string oldData = oldRecord != null ? JsonSerializer.Serialize(oldRecord, _jsonOptions) : null; + +// 执行删除 +service.Delete(id); + +_auditLogsService.AddLog(new OdfAuditLogs +{ + TableName = "表名", + RecordId = id, + OperationType = "DELETE", + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = oldData, + NewData = null, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, +}); +``` + +### 批量删除 + +对于传 `string ids` 的批量删除接口,需要循环每条记录分别记录审计日志: + +```csharp +var idArr = Tools.SplitAndConvert(ids); + +// 批量查旧数据 +foreach (var id in idArr) +{ + var oldRecord = service.GetFirst(f => f.Id == id); + string oldData = oldRecord != null ? JsonSerializer.Serialize(oldRecord, _jsonOptions) : null; + + // 记录每条的审计日志 + _auditLogsService.AddLog(new OdfAuditLogs + { + TableName = "表名", + RecordId = id, + OperationType = "DELETE", + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = oldData, + NewData = null, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, + }); +} + +// 执行批量删除 +service.Delete(idArr); +``` + +## 各 Controller 接口与表名映射 + +### OdfCableFaultsController + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| Add | INSERT | odf_cable_faults | 异步方法,插入后从返回的 faultId 查新数据 | +| IncrementFaultCount | UPDATE | odf_cable_faults | ✅ 已完成 | +| UpdateMileageCorrection | UPDATE | odf_cable_faults | 有 DTO 参数,但改为手动更可靠 | +| Delete | DELETE | odf_cable_faults | 批量删除 | + +### OdfCablesController + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| Add | INSERT | odf_cables | 参数为 OdfCables 实体 | +| Update | UPDATE | odf_cables | 参数为 OdfCables 实体 | +| Delete | DELETE | odf_cables | 单条删除,int id | + +### OdfRacksController + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| AddOdfRacks | INSERT | odf_racks | | +| AddOdfRacksExpert | INSERT | odf_racks | 专家模式,同时创建框和端口 | +| UpdateOdfRacks | UPDATE | odf_racks | | +| DeleteOdfRacks | DELETE | odf_racks | 批量删除 | + +### OdfPortsController + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| GetOdfPortsStatus | UPDATE | odf_ports | 参数为简单类型 int Id, int status | +| AddOdfPorts | INSERT | odf_ports | | +| UpdateOdfPorts | UPDATE | odf_ports | | +| SaveMOdfPorts | UPDATE | odf_ports | APP端修改端口 | +| GetOdfPortsEmpty | UPDATE | odf_ports | 清空端口数据,参数为 int Id | +| DeleteOdfPorts | DELETE | odf_ports | 批量删除 | +| DeleteAll | DELETE | odf_ports | 按条件批量删除,参数为 DTO | + +### OdfMarkerPolesController + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| Add | INSERT | odf_marker_poles | | +| Update | UPDATE | odf_marker_poles | | +| Delete | DELETE | odf_marker_poles | 批量删除 | + +### OdfFramesController(新增审计) + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| AddOdfFrames | INSERT | odf_frames | 同时批量插入端口,仅记录框的审计 | +| UpdateOdfFrames | UPDATE | odf_frames | 可能级联更新端口 FrameName | +| DeleteOdfFrames | DELETE | odf_frames | 批量删除 | + +### OdfRoomsController(新增审计) + +| 方法 | 操作类型 | 表名 | 备注 | +|------|---------|------|------| +| AddOdfRooms | INSERT | odf_rooms | | +| AddExpertOdfRooms | INSERT | odf_rooms | 专家模式,同时创建机架/框/端口,仅记录机房的审计 | +| UpdateOdfRooms | UPDATE | odf_rooms | | +| DeleteOdfRooms | DELETE | odf_rooms | 批量删除 | + +## BaseController 扩展 + +在 `BaseController` 中添加审计日志相关的公共方法和配置,避免各 Controller 重复代码: + +```csharp +public class BaseController : ControllerBase +{ + // ... 现有代码 ... + + /// + /// 审计日志 JSON 序列化选项 + /// + protected static readonly JsonSerializerOptions AuditJsonOptions = new() + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// 获取操作来源客户端标识 + /// + protected string GetSourceClient() + { + if (HttpContext.Request.Headers.TryGetValue("X-Source-Client", out var src) + && !string.IsNullOrWhiteSpace(src.ToString())) + { + return src.ToString().Equals("App", StringComparison.OrdinalIgnoreCase) ? "App" : "Admin"; + } + return "Admin"; + } + + /// + /// 构造审计日志对象 + /// + protected OdfAuditLogs BuildAuditLog(string tableName, int? recordId, string operationType, string oldData, string newData) + { + return new OdfAuditLogs + { + TableName = tableName, + RecordId = recordId, + OperationType = operationType, + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = oldData, + NewData = newData, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, + }; + } + + /// + /// 将实体序列化为审计日志 JSON + /// + protected static string SerializeForAudit(T entity) where T : class + { + return entity != null ? JsonSerializer.Serialize(entity, AuditJsonOptions) : null; + } +} +``` + +### 使用 BaseController 后的简化写法 + +```csharp +// UPDATE 示例 +var oldRecord = _service.GetFirst(f => f.Id == id); +string oldData = SerializeForAudit(oldRecord); + +_service.Update(model); + +var newRecord = _service.GetFirst(f => f.Id == id); +string newData = SerializeForAudit(newRecord); + +_auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "UPDATE", oldData, newData)); +``` + +## 错误处理 + +| 场景 | 处理方式 | +|------|---------| +| 审计日志写入失败 | try-catch 包裹,记录 NLog 错误日志,不影响主业务返回 | +| 查询旧数据失败 | OldData 设为 null,继续执行主业务 | +| 查询新数据失败 | NewData 设为 null,继续执行主业务 | +| JSON 序列化失败 | 对应字段设为 null,记录警告日志 | + +每个审计日志记录操作都应包裹在 try-catch 中: + +```csharp +try +{ + _auditLogsService.AddLog(BuildAuditLog(...)); +} +catch (Exception ex) +{ + // 审计日志写入失败不影响主业务 + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); +} +``` + +## 改造顺序建议 + +1. 先扩展 `BaseController`,添加公共方法 +2. 改造 `OdfCableFaultsController`(剩余 3 个接口) +3. 改造 `OdfCablesController`(3 个接口) +4. 改造 `OdfMarkerPolesController`(3 个接口) +5. 改造 `OdfRacksController`(4 个接口) +6. 改造 `OdfPortsController`(7 个接口,最多) +7. 新增 `OdfFramesController` 审计(3 个接口) +8. 新增 `OdfRoomsController` 审计(4 个接口) +9. 移除 `OdfAuditLogFilter` 及 Program.cs 注册 +10. 全量测试验证 diff --git a/.kiro/specs/audit-log-manual/requirements.md b/.kiro/specs/audit-log-manual/requirements.md new file mode 100644 index 0000000..fd554a0 --- /dev/null +++ b/.kiro/specs/audit-log-manual/requirements.md @@ -0,0 +1,81 @@ +# 需求:审计日志手动化改造 + +## 背景 + +当前系统通过 `OdfAuditLogFilter`(ActionFilter 拦截器)自动记录业务数据的增删改审计日志。该方案存在以下问题: + +1. 对于只传简单类型参数(如 `int id`)的接口,拦截器无法序列化 NewData,导致审计日志中修改后数据为空 +2. 批量删除接口(传 `string ids`)只能记录第一条记录的 OldData +3. 拦截器逻辑对不同接口的参数结构有隐式依赖,不够透明 +4. 部分 Controller(OdfFrames、OdfRooms)有增删改操作但完全没有审计日志 + +## 目标 + +将所有业务数据的审计日志记录从拦截器自动模式改为 Controller 中手动记录,确保每个增删改操作都能正确记录完整的 OldData 和 NewData。改造完成后移除 `OdfAuditLogFilter`。 + +## 需求列表 + +### 1. 干线故障模块(OdfCableFaultsController) + +- 1.1 `IncrementFaultCount`(增加频次):手动记录审计日志,包含完整 OldData 和 NewData ✅ 已完成 +- 1.2 `Add`(新增故障):去掉拦截器,手动记录审计日志,NewData 为插入后的完整记录 +- 1.3 `UpdateMileageCorrection`(更新里程矫正):去掉拦截器,手动记录 OldData 和 NewData +- 1.4 `Delete`(删除故障):去掉拦截器,手动记录 OldData,支持批量删除时每条记录单独记录 + +### 2. 光缆管理模块(OdfCablesController) + +- 2.1 `Add`(新增光缆):去掉拦截器,手动记录审计日志 +- 2.2 `Update`(修改光缆):去掉拦截器,手动记录 OldData 和 NewData +- 2.3 `Delete`(删除光缆):去掉拦截器,手动记录 OldData + +### 3. 机架模块(OdfRacksController) + +- 3.1 `AddOdfRacks`(新增机架):去掉拦截器,手动记录审计日志 +- 3.2 `AddOdfRacksExpert`(专家模式新增机架):去掉拦截器,手动记录审计日志 +- 3.3 `UpdateOdfRacks`(更新机架):去掉拦截器,手动记录 OldData 和 NewData +- 3.4 `DeleteOdfRacks`(删除机架):去掉拦截器,手动记录 OldData,支持批量 + +### 4. 端口模块(OdfPortsController) + +- 4.1 `GetOdfPortsStatus`(更新端口状态):去掉拦截器,手动记录 OldData 和 NewData +- 4.2 `AddOdfPorts`(新增端口):去掉拦截器,手动记录审计日志 +- 4.3 `UpdateOdfPorts`(更新端口):去掉拦截器,手动记录 OldData 和 NewData +- 4.4 `SaveMOdfPorts`(APP修改端口):去掉拦截器,手动记录 OldData 和 NewData +- 4.5 `GetOdfPortsEmpty`(清空端口数据):去掉拦截器,手动记录 OldData 和 NewData +- 4.6 `DeleteOdfPorts`(删除端口):去掉拦截器,手动记录 OldData,支持批量 +- 4.7 `DeleteAll`(批量删除端口):去掉拦截器,手动记录审计日志 + +### 5. 标石/杆号牌模块(OdfMarkerPolesController) + +- 5.1 `Add`(新增标石):去掉拦截器,手动记录审计日志 +- 5.2 `Update`(编辑标石):去掉拦截器,手动记录 OldData 和 NewData +- 5.3 `Delete`(删除标石):去掉拦截器,手动记录 OldData,支持批量 + +### 6. 框模块(OdfFramesController)— 新增审计 + +- 6.1 `AddOdfFrames`(新增框):新增手动审计日志记录 +- 6.2 `UpdateOdfFrames`(更新框):新增手动审计日志记录,包含 OldData 和 NewData +- 6.3 `DeleteOdfFrames`(删除框):新增手动审计日志记录,包含 OldData + +### 7. 机房模块(OdfRoomsController)— 新增审计 + +- 7.1 `AddOdfRooms`(新增机房):新增手动审计日志记录 +- 7.2 `AddExpertOdfRooms`(专家模式新增机房):新增手动审计日志记录 +- 7.3 `UpdateOdfRooms`(更新机房):新增手动审计日志记录,包含 OldData 和 NewData +- 7.4 `DeleteOdfRooms`(删除机房):新增手动审计日志记录,包含 OldData + +### 8. 清理工作 + +- 8.1 所有接口改造完成后,移除 `OdfAuditLogFilter.cs` 文件 +- 8.2 移除 `Program.cs` 中 `OdfAuditLogFilter` 的 DI 注册 +- 8.3 确认无其他文件引用 `OdfAuditLogFilter` + +### 9. 不纳入审计范围的模块 + +以下模块不需要审计日志,不做改造: +- OdfCheckinController(签到记录) +- OdfAppUpdatesController(App版本管理) +- OdfUserModulesController(用户模块权限) +- OdfPortFaultController(错误日志,只读) +- CosUploadController(文件上传) +- OdfAuditLogsController(审计日志本身,只读) diff --git a/.kiro/specs/audit-log-manual/tasks.md b/.kiro/specs/audit-log-manual/tasks.md new file mode 100644 index 0000000..3cffd5c --- /dev/null +++ b/.kiro/specs/audit-log-manual/tasks.md @@ -0,0 +1,62 @@ +# 审计日志手动化改造 — 任务列表 + +## Task 1: 扩展 BaseController 公共方法 +- [x] 在 `BaseController` 中添加 `AuditJsonOptions`、`GetSourceClient()`、`BuildAuditLog()`、`SerializeForAudit()` 方法 +- [x] 添加 `using System.Text.Json` 和 `using ZR.Model.Business` 引用 +- [x] 验证编译通过 + +## Task 2: 改造 OdfCableFaultsController(干线故障) +- [x] `IncrementFaultCount` — 已完成 +- [x] `Add` — 去掉 `[ServiceFilter(typeof(OdfAuditLogFilter))]`,手动记录 INSERT 审计日志(表名: odf_cable_faults) +- [x] `UpdateMileageCorrection` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `Delete` — 去掉拦截器,手动记录 DELETE 审计日志,循环每条记录分别记录 +- [x] 注入 `IOdfAuditLogsService`(已注入) +- [x] 移除 `IncrementFaultCount` 中的 `_jsonOptions` 和 sourceClient 内联逻辑,改用 BaseController 公共方法 + +## Task 3: 改造 OdfCablesController(光缆管理) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `Add` — 去掉拦截器,手动记录 INSERT 审计日志(表名: odf_cables) +- [x] `Update` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `Delete` — 去掉拦截器,手动记录 DELETE 审计日志 + +## Task 4: 改造 OdfMarkerPolesController(标石/杆号牌) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `Add` — 去掉拦截器,手动记录 INSERT 审计日志(表名: odf_marker_poles) +- [x] `Update` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `Delete` — 去掉拦截器,手动记录 DELETE 审计日志,循环每条记录 + +## Task 5: 改造 OdfRacksController(机架) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `AddOdfRacks` — 去掉拦截器,手动记录 INSERT 审计日志(表名: odf_racks) +- [x] `AddOdfRacksExpert` — 去掉拦截器,手动记录 INSERT 审计日志 +- [x] `UpdateOdfRacks` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `DeleteOdfRacks` — 去掉拦截器,手动记录 DELETE 审计日志,循环每条记录 + +## Task 6: 改造 OdfPortsController(端口) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `GetOdfPortsStatus` — 去掉拦截器,手动记录 UPDATE 审计日志(表名: odf_ports) +- [x] `AddOdfPorts` — 去掉拦截器,手动记录 INSERT 审计日志 +- [x] `UpdateOdfPorts` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `SaveMOdfPorts` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `GetOdfPortsEmpty` — 去掉拦截器,手动记录 UPDATE 审计日志 +- [x] `DeleteOdfPorts` — 去掉拦截器,手动记录 DELETE 审计日志,循环每条记录 +- [x] `DeleteAll` — 去掉拦截器,手动记录 DELETE 审计日志 + +## Task 7: 新增 OdfFramesController 审计(框) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `AddOdfFrames` — 新增 INSERT 审计日志(表名: odf_frames) +- [x] `UpdateOdfFrames` — 新增 UPDATE 审计日志 +- [x] `DeleteOdfFrames` — 新增 DELETE 审计日志,循环每条记录 + +## Task 8: 新增 OdfRoomsController 审计(机房) +- [x] 构造函数注入 `IOdfAuditLogsService` +- [x] `AddOdfRooms` — 新增 INSERT 审计日志(表名: odf_rooms) +- [x] `AddExpertOdfRooms` — 新增 INSERT 审计日志 +- [x] `UpdateOdfRooms` — 新增 UPDATE 审计日志 +- [x] `DeleteOdfRooms` — 新增 DELETE 审计日志,循环每条记录 + +## Task 9: 清理拦截器 +- [x] 删除 `server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs` +- [x] 移除 `Program.cs` 中 `builder.Services.AddScoped()` 注册 +- [x] 全局搜索确认无其他文件引用 `OdfAuditLogFilter` +- [x] 编译验证通过 diff --git a/server/Infrastructure/Controllers/BaseController.cs b/server/Infrastructure/Controllers/BaseController.cs index d840c9a..ad1f79b 100644 --- a/server/Infrastructure/Controllers/BaseController.cs +++ b/server/Infrastructure/Controllers/BaseController.cs @@ -9,8 +9,10 @@ using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using System.Threading.Tasks; using System.Web; +using ZR.Model.Business; namespace Infrastructure.Controllers { @@ -22,6 +24,76 @@ namespace Infrastructure.Controllers { public static string TIME_FORMAT_FULL = "yyyy-MM-dd HH:mm:ss"; + /// + /// 审计日志 JSON 序列化选项 + /// + protected static readonly JsonSerializerOptions AuditJsonOptions = new() + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// 获取操作来源客户端标识 + /// + protected string GetSourceClient() + { + if (HttpContext.Request.Headers.TryGetValue("X-Source-Client", out var src) + && !string.IsNullOrWhiteSpace(src.ToString())) + { + return src.ToString().Equals("App", StringComparison.OrdinalIgnoreCase) ? "App" : "Admin"; + } + return "Admin"; + } + + /// + /// 构造审计日志对象 + /// + protected OdfAuditLogs BuildAuditLog(string tableName, int? recordId, string operationType, string oldData, string newData) + { + return new OdfAuditLogs + { + TableName = tableName, + RecordId = recordId, + OperationType = operationType, + OperatorId = HttpContext.GetUId(), + OperatorName = HttpContext.GetName(), + SourceClient = GetSourceClient(), + OldData = oldData, + NewData = newData, + OperationTime = DateTime.Now, + DeptId = HttpContext.GetDeptId() > 0 ? HttpContext.GetDeptId() : null, + }; + } + + /// + /// 将实体序列化为审计日志 JSON + /// + protected static string SerializeForAudit(T entity) where T : class + { + return entity != null ? System.Text.Json.JsonSerializer.Serialize(entity, AuditJsonOptions) : null; + } + + /// + /// 将字段字典序列化为审计 JSON(用于所有操作类型) + /// + protected static string BuildAuditJson(Dictionary fields) + { + var result = new Dictionary(); + foreach (var kv in fields) + { + if (kv.Value is DateTime dt) + { + result[kv.Key] = dt.ToString("yyyy-MM-dd HH:mm:ss"); + } + else + { + result[kv.Key] = kv.Value; + } + } + return System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = false }); + } + /// /// 返回成功封装 /// diff --git a/server/Infrastructure/ZR.Infrastructure.csproj b/server/Infrastructure/ZR.Infrastructure.csproj index 998f38e..c5fa9db 100644 --- a/server/Infrastructure/ZR.Infrastructure.csproj +++ b/server/Infrastructure/ZR.Infrastructure.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs index 22fdca1..27be2e2 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfCableFaultsController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; -using ZR.Admin.WebApi.Filters; +using ZR.Model.Business; using ZR.Model.Business.Dto; using ZR.Service.Business.IBusinessService; @@ -17,10 +17,14 @@ namespace ZR.Admin.WebApi.Controllers.Business /// 干线故障接口 /// private readonly IOdfCableFaultsService _OdfCableFaultsService; + private readonly IOdfAuditLogsService _auditLogsService; - public OdfCableFaultsController(IOdfCableFaultsService OdfCableFaultsService) + public OdfCableFaultsController( + IOdfCableFaultsService OdfCableFaultsService, + IOdfAuditLogsService auditLogsService) { _OdfCableFaultsService = OdfCableFaultsService; + _auditLogsService = auditLogsService; } /// @@ -57,11 +61,40 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("add")] [ActionPermissionFilter(Permission = "odfcablefaults:add")] [Log(Title = "干线故障", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task Add([FromBody] OdfCableFaultAddDto dto) { dto.UserId = HttpContext.GetUId(); var response = await _OdfCableFaultsService.AddFault(dto); + + // 手动记录 INSERT 审计日志 + try + { + var newRecord = _OdfCableFaultsService.GetFirst(f => f.Id == response); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["关联光缆ID"] = newRecord.CableId, + ["故障时间"] = newRecord.FaultTime, + ["人员"] = newRecord.Personnel, + ["故障原因"] = newRecord.FaultReason, + ["表显故障里程"] = newRecord.Mileage, + ["表显里程矫正"] = newRecord.MileageCorrection, + ["地点描述"] = newRecord.Location, + ["纬度"] = newRecord.Latitude, + ["经度"] = newRecord.Longitude, + ["备注"] = newRecord.Remark, + ["提交人用户ID"] = newRecord.UserId, + ["创建时间"] = newRecord.CreatedAt, + ["更新时间"] = newRecord.UpdatedAt, + ["故障发生频次"] = newRecord.FaultCount, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", response, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -71,10 +104,37 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("incrementFaultCount/{id}")] [ActionPermissionFilter(Permission = "odfcablefaults:edit")] [Log(Title = "干线故障", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult IncrementFaultCount(int id) { + // 记录旧数据 + var oldFault = _OdfCableFaultsService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["故障发生频次"] = oldFault.FaultCount, + ["更新时间"] = oldFault.UpdatedAt, + }); + + // 执行更新 var response = _OdfCableFaultsService.IncrementFaultCount(id); + + // 记录新数据 + var newFault = _OdfCableFaultsService.GetFirst(f => f.Id == id); + string newData = BuildAuditJson(new Dictionary + { + ["故障发生频次"] = newFault.FaultCount, + ["更新时间"] = newFault.UpdatedAt, + }); + + // 写入审计日志 + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -84,10 +144,37 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("updateMileageCorrection/{id}")] [ActionPermissionFilter(Permission = "odfcablefaults:edit")] [Log(Title = "干线故障", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult UpdateMileageCorrection(int id, [FromBody] MileageCorrectionDto dto) { + // 查旧数据 + var oldRecord = _OdfCableFaultsService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["表显里程矫正"] = oldRecord.MileageCorrection, + ["更新时间"] = oldRecord.UpdatedAt, + }); + + // 执行更新 var response = _OdfCableFaultsService.UpdateMileageCorrection(id, dto.MileageCorrection); + + // 查新数据 + var newRecord = _OdfCableFaultsService.GetFirst(f => f.Id == id); + string newData = BuildAuditJson(new Dictionary + { + ["表显里程矫正"] = newRecord.MileageCorrection, + ["更新时间"] = newRecord.UpdatedAt, + }); + + // 写入审计日志 + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -98,7 +185,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("delete/{ids}")] [ActionPermissionFilter(Permission = "odfcablefaults:delete")] [Log(Title = "干线故障", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Delete(string ids) { var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -111,6 +197,38 @@ namespace ZR.Admin.WebApi.Controllers.Business return ToResponse(ResultCode.FAIL, "请选择要删除的数据"); } + // 循环每条记录分别记录 DELETE 审计日志 + foreach (var id in idList) + { + var oldRecord = _OdfCableFaultsService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["关联光缆ID"] = oldRecord.CableId, + ["故障时间"] = oldRecord.FaultTime, + ["人员"] = oldRecord.Personnel, + ["故障原因"] = oldRecord.FaultReason, + ["表显故障里程"] = oldRecord.Mileage, + ["表显里程矫正"] = oldRecord.MileageCorrection, + ["地点描述"] = oldRecord.Location, + ["纬度"] = oldRecord.Latitude, + ["经度"] = oldRecord.Longitude, + ["备注"] = oldRecord.Remark, + ["提交人用户ID"] = oldRecord.UserId, + ["创建时间"] = oldRecord.CreatedAt, + ["更新时间"] = oldRecord.UpdatedAt, + ["故障发生频次"] = oldRecord.FaultCount, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_cable_faults", id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + int total = 0; foreach (var id in idList) { diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs index 53b02a0..e2924f1 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfCablesController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using ZR.Admin.WebApi.Filters; using ZR.Model.Business; using ZR.Model.Business.Dto; using ZR.Service.Business.IBusinessService; @@ -17,10 +16,14 @@ namespace ZR.Admin.WebApi.Controllers.Business /// 光缆管理接口 /// private readonly IOdfCablesService _OdfCablesService; + private readonly IOdfAuditLogsService _auditLogsService; - public OdfCablesController(IOdfCablesService OdfCablesService) + public OdfCablesController( + IOdfCablesService OdfCablesService, + IOdfAuditLogsService auditLogsService) { _OdfCablesService = OdfCablesService; + _auditLogsService = auditLogsService; } /// @@ -56,12 +59,32 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost] [ActionPermissionFilter(Permission = "odfcables:add")] [Log(Title = "光缆管理", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Add([FromBody] OdfCables parm) { parm.CreatedAt = DateTime.Now; parm.UpdatedAt = DateTime.Now; var response = _OdfCablesService.Add(parm); + + // 手动记录 INSERT 审计日志 + try + { + var newRecord = _OdfCablesService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["光缆名称"] = newRecord.CableName, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["创建时间"] = newRecord.CreatedAt, + ["更新时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_cables", parm.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -72,11 +95,39 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPut] [ActionPermissionFilter(Permission = "odfcables:edit")] [Log(Title = "光缆管理", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Update([FromBody] OdfCables parm) { + // 查旧数据(仅记录 UPDATE 实际修改的字段) + var oldRecord = _OdfCablesService.GetFirst(f => f.Id == parm.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["光缆名称"] = oldRecord.CableName, + ["部门ID"] = oldRecord.DeptId, + ["部门名称"] = oldRecord.DeptName, + ["更新时间"] = oldRecord.UpdatedAt, + }); + parm.UpdatedAt = DateTime.Now; var response = _OdfCablesService.Update(parm); + + // 查新数据并记录 UPDATE 审计日志 + try + { + var newRecord = _OdfCablesService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["光缆名称"] = newRecord.CableName, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["更新时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_cables", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -100,9 +151,28 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("delete/{id}")] [ActionPermissionFilter(Permission = "odfcables:delete")] [Log(Title = "光缆管理", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Delete(int id) { + // 删除前记录 DELETE 审计日志 + var oldRecord = _OdfCablesService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["光缆名称"] = oldRecord.CableName, + ["部门ID"] = oldRecord.DeptId, + ["部门名称"] = oldRecord.DeptName, + ["创建时间"] = oldRecord.CreatedAt, + ["更新时间"] = oldRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_cables", id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + var response = _OdfCablesService.Delete(id); return ToResponse(response); } diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs index 8e51665..9491164 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfFramesController.cs @@ -27,15 +27,18 @@ namespace ZR.Admin.WebApi.Controllers.Business /// private readonly IOdfRacksService _OdfRacksService; private readonly IOdfRoomsService _odfRooms; + private readonly IOdfAuditLogsService _auditLogsService; public OdfFramesController(IOdfRacksService OdfRacksService, IOdfRoomsService odfRooms, IOdfFramesService odfFramesService, - IOdfPortsService odfPortsService) + IOdfPortsService odfPortsService, + IOdfAuditLogsService auditLogsService) { _OdfRacksService = OdfRacksService; _odfRooms = odfRooms; _OdfFramesService = odfFramesService; _OdfPortsService = odfPortsService; + _auditLogsService = auditLogsService; } /// @@ -90,6 +93,31 @@ namespace ZR.Admin.WebApi.Controllers.Business modal.UpdateAt = DateTime.Now; modal.CreatedAt = DateTime.Now; var response = _OdfFramesService.AddOdfFrames(modal); + + // INSERT 审计日志(仅记录框) + try + { + var newRecord = _OdfFramesService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["机架ID"] = newRecord.RackId, + ["名称"] = newRecord.PortsName, + ["部门ID"] = newRecord.DeptId, + ["序号"] = newRecord.SequenceNumber, + ["端口数量"] = newRecord.PortsCount, + ["列数量"] = newRecord.PortsCol, + ["行数量"] = newRecord.PortsRow, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdateAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_frames", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + var roomId = rooms.Id; var roomName = rooms.RoomName; @@ -155,8 +183,42 @@ namespace ZR.Admin.WebApi.Controllers.Business public async Task UpdateOdfFrames([FromBody] OdfFramesDto parm) { var modal = parm.Adapt().ToUpdate(HttpContext); + + // 查旧数据 + var oldRecord = _OdfFramesService.GetFirst(f => f.Id == parm.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["名称"] = oldRecord.PortsName, + ["序号"] = oldRecord.SequenceNumber, + ["端口数量"] = oldRecord.PortsCount, + ["列数量"] = oldRecord.PortsCol, + ["行数量"] = oldRecord.PortsRow, + ["修改时间"] = oldRecord.UpdateAt, + }); + var oldModel = _OdfFramesService.GetById(parm.Id); var response = _OdfFramesService.UpdateOdfFrames(modal); + + // UPDATE 审计日志(仅记录框) + try + { + var newRecord = _OdfFramesService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["名称"] = newRecord.PortsName, + ["序号"] = newRecord.SequenceNumber, + ["端口数量"] = newRecord.PortsCount, + ["列数量"] = newRecord.PortsCol, + ["行数量"] = newRecord.PortsRow, + ["修改时间"] = newRecord.UpdateAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_frames", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + if (response > 0) { var rortsName = oldModel.PortsName; @@ -189,6 +251,33 @@ namespace ZR.Admin.WebApi.Controllers.Business { var idArr = Tools.SplitAndConvert(ids); + // 循环每条记录分别记录 DELETE 审计日志 + foreach (var id in idArr) + { + var oldRecord = _OdfFramesService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["机架ID"] = oldRecord.RackId, + ["名称"] = oldRecord.PortsName, + ["部门ID"] = oldRecord.DeptId, + ["序号"] = oldRecord.SequenceNumber, + ["端口数量"] = oldRecord.PortsCount, + ["列数量"] = oldRecord.PortsCol, + ["行数量"] = oldRecord.PortsRow, + ["创建时间"] = oldRecord.CreatedAt, + ["修改时间"] = oldRecord.UpdateAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_frames", id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + return ToResponse(_OdfFramesService.Delete(idArr, "删除框-信息")); } diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs index b8bcab7..deb788a 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfMarkerPolesController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using ZR.Admin.WebApi.Filters; using ZR.Model.Business.Dto; using ZR.Service.Business.IBusinessService; @@ -16,10 +15,14 @@ namespace ZR.Admin.WebApi.Controllers.Business /// 标石/杆号牌接口 /// private readonly IOdfMarkerPolesService _OdfMarkerPolesService; + private readonly IOdfAuditLogsService _auditLogsService; - public OdfMarkerPolesController(IOdfMarkerPolesService OdfMarkerPolesService) + public OdfMarkerPolesController( + IOdfMarkerPolesService OdfMarkerPolesService, + IOdfAuditLogsService auditLogsService) { _OdfMarkerPolesService = OdfMarkerPolesService; + _auditLogsService = auditLogsService; } /// @@ -56,12 +59,39 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("add")] [ActionPermissionFilter(Permission = "odfmarkerpoles:add")] [Log(Title = "标石/杆号牌", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Add([FromBody] OdfMarkerPoleAddDto dto) { dto.UserId = HttpContext.GetUId(); var deptId = HttpContext.GetDeptId(); var response = _OdfMarkerPolesService.Add(dto, deptId); + + // 手动记录 INSERT 审计日志 + try + { + var newRecord = _OdfMarkerPolesService.GetFirst(f => f.Id == response); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["关联光缆ID"] = newRecord.CableId, + ["名称"] = newRecord.Name, + ["记录时间"] = newRecord.RecordTime, + ["责任人"] = newRecord.Personnel, + ["纬度"] = newRecord.Latitude, + ["经度"] = newRecord.Longitude, + ["实际里程"] = newRecord.ActualMileage, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["提交人用户ID"] = newRecord.UserId, + ["创建时间"] = newRecord.CreatedAt, + ["更新时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_marker_poles", response, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -72,10 +102,47 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPut("edit")] [ActionPermissionFilter(Permission = "odfmarkerpoles:edit")] [Log(Title = "标石/杆号牌", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Update([FromBody] OdfMarkerPoleEditDto dto) { + // 查旧数据 + var oldRecord = _OdfMarkerPolesService.GetFirst(f => f.Id == dto.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["名称"] = oldRecord.Name, + ["记录时间"] = oldRecord.RecordTime, + ["责任人"] = oldRecord.Personnel, + ["纬度"] = oldRecord.Latitude, + ["经度"] = oldRecord.Longitude, + ["实际里程"] = oldRecord.ActualMileage, + ["更新时间"] = oldRecord.UpdatedAt, + }); + + // 执行更新 var response = _OdfMarkerPolesService.Update(dto); + + // 查新数据 + var newRecord = _OdfMarkerPolesService.GetFirst(f => f.Id == dto.Id); + string newData = BuildAuditJson(new Dictionary + { + ["名称"] = newRecord.Name, + ["记录时间"] = newRecord.RecordTime, + ["责任人"] = newRecord.Personnel, + ["纬度"] = newRecord.Latitude, + ["经度"] = newRecord.Longitude, + ["实际里程"] = newRecord.ActualMileage, + ["更新时间"] = newRecord.UpdatedAt, + }); + + // 写入审计日志 + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_marker_poles", dto.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -86,7 +153,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpDelete("delete/{ids}")] [ActionPermissionFilter(Permission = "odfmarkerpoles:delete")] [Log(Title = "标石/杆号牌", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult Delete(string ids) { var idList = ids.Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -99,6 +165,36 @@ namespace ZR.Admin.WebApi.Controllers.Business return ToResponse(ResultCode.FAIL, "请选择要删除的数据"); } + // 循环每条记录分别记录 DELETE 审计日志 + foreach (var id in idList) + { + var oldRecord = _OdfMarkerPolesService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["关联光缆ID"] = oldRecord.CableId, + ["名称"] = oldRecord.Name, + ["记录时间"] = oldRecord.RecordTime, + ["责任人"] = oldRecord.Personnel, + ["纬度"] = oldRecord.Latitude, + ["经度"] = oldRecord.Longitude, + ["实际里程"] = oldRecord.ActualMileage, + ["部门ID"] = oldRecord.DeptId, + ["部门名称"] = oldRecord.DeptName, + ["提交人用户ID"] = oldRecord.UserId, + ["创建时间"] = oldRecord.CreatedAt, + ["更新时间"] = oldRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_marker_poles", id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + int total = 0; foreach (var id in idList) { diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs index 515a9e2..87ead2b 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfPortsController.cs @@ -14,7 +14,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using ZR.Admin.WebApi.Filters; using ZR.Model.Business; using ZR.Model.Business.Dto; using ZR.Model.System.Dto; @@ -67,6 +66,8 @@ namespace ZR.Admin.WebApi.Controllers.Business /// private readonly IOdfPortFaultService _OdfPortFaultService; + private readonly IOdfAuditLogsService _auditLogsService; + public OdfPortsController(IOdfRoomsService OdfRoomsService, ISysDeptService sysDeptService, @@ -74,8 +75,8 @@ namespace ZR.Admin.WebApi.Controllers.Business IOdfPortsService odfPortsService, IOdfFramesService odfFramesService, IOdfRacksService odfRacksService, - IOdfPortFaultService odfPortFaultService - + IOdfPortFaultService odfPortFaultService, + IOdfAuditLogsService auditLogsService ) { _OdfRoomsService = OdfRoomsService; @@ -85,6 +86,7 @@ namespace ZR.Admin.WebApi.Controllers.Business _OdfRacksService = odfRacksService; _SysDeptService = sysDeptService; _OdfPortFaultService = odfPortFaultService; + _auditLogsService = auditLogsService; } /// @@ -362,13 +364,35 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpGet("status/{Id}/{status}")] [ActionPermissionFilter(Permission = "odfports:edit")] [Log(Title = "更新端口状态", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult GetOdfPortsStatus(int Id, int status) { + var oldRecord = _OdfPortsService.GetFirst(f => f.Id == Id); + string oldData = BuildAuditJson(new Dictionary + { + ["连接状态"] = oldRecord.Status, + ["修改时间"] = oldRecord.UpdatedAt, + }); + var response = _OdfPortsService.GetInfo(Id); response.Status = status; response.UpdatedAt = DateTime.Now; var s = _OdfPortsService.Update(response); + + var newRecord = _OdfPortsService.GetFirst(f => f.Id == Id); + string newData = BuildAuditJson(new Dictionary + { + ["连接状态"] = newRecord.Status, + ["修改时间"] = newRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(s); } @@ -390,7 +414,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost] [ActionPermissionFilter(Permission = "odfports:add")] [Log(Title = "端口", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task AddOdfPorts([FromBody] OdfPortsDto parm) { var modal = parm.Adapt().ToCreate(HttpContext); @@ -412,6 +435,43 @@ namespace ZR.Admin.WebApi.Controllers.Business await _OdfPortFaultService.InsertAsync(o); } } + + // 手动记录 INSERT 审计日志 + try + { + var newRecord = _OdfPortsService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["端口名称"] = newRecord.Name, + ["机房ID"] = newRecord.RoomId, + ["机房名称"] = newRecord.RoomName, + ["机架ID"] = newRecord.RackId, + ["机架名称"] = newRecord.RackName, + ["框ID"] = newRecord.FrameId, + ["框名称"] = newRecord.FrameName, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["行号"] = newRecord.RowNumber, + ["端口号"] = newRecord.PortNumber, + ["连接状态"] = newRecord.Status, + ["备注"] = newRecord.Remarks, + ["光衰值"] = newRecord.OpticalAttenuation, + ["历史故障"] = newRecord.HistoryRemarks, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdatedAt, + ["光缆断信息"] = newRecord.OpticalCableOffRemarks, + ["设备型号"] = newRecord.EquipmentModel, + ["业务类型"] = newRecord.BusinessType, + ["端口侧"] = newRecord.PortSide, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_ports", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -422,9 +482,23 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPut] [ActionPermissionFilter(Permission = "odfports:edit")] [Log(Title = "端口", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task UpdateOdfPorts([FromBody] OdfPortsDto parm) { + // 查旧数据 + var oldRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["端口名称"] = oldRecord.Name, + ["连接状态"] = oldRecord.Status, + ["备注"] = oldRecord.Remarks, + ["光衰值"] = oldRecord.OpticalAttenuation, + ["历史故障"] = oldRecord.HistoryRemarks, + ["光缆断信息"] = oldRecord.OpticalCableOffRemarks, + ["设备型号"] = oldRecord.EquipmentModel, + ["业务类型"] = oldRecord.BusinessType, + ["修改时间"] = oldRecord.UpdatedAt, + }); + var modal = parm.Adapt().ToUpdate(HttpContext); modal.UpdatedAt = DateTime.Now; modal.HistoryRemarks = ToHistoryString(parm.HistoryFault); @@ -448,6 +522,30 @@ namespace ZR.Admin.WebApi.Controllers.Business await _OdfPortFaultService.InsertAsync(o); } } + + // 查新数据 + var newRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["端口名称"] = newRecord.Name, + ["连接状态"] = newRecord.Status, + ["备注"] = newRecord.Remarks, + ["光衰值"] = newRecord.OpticalAttenuation, + ["历史故障"] = newRecord.HistoryRemarks, + ["光缆断信息"] = newRecord.OpticalCableOffRemarks, + ["设备型号"] = newRecord.EquipmentModel, + ["业务类型"] = newRecord.BusinessType, + ["修改时间"] = newRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -458,9 +556,22 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("save")] [ActionPermissionFilter(Permission = "odfports:edit")] [Log(Title = "APP修改端口", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task SaveMOdfPorts([FromBody] OdfPortsMMDto parm) { + // 查旧数据 + var oldRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["连接状态"] = oldRecord.Status, + ["备注"] = oldRecord.Remarks, + ["光衰值"] = oldRecord.OpticalAttenuation, + ["历史故障"] = oldRecord.HistoryRemarks, + ["光缆断信息"] = oldRecord.OpticalCableOffRemarks, + ["设备型号"] = oldRecord.EquipmentModel, + ["业务类型"] = oldRecord.BusinessType, + ["修改时间"] = oldRecord.UpdatedAt, + }); + var port = _OdfPortsService.GetById(parm.Id); if (port == null) { @@ -528,6 +639,29 @@ namespace ZR.Admin.WebApi.Controllers.Business await _OdfPortFaultService.InsertAsync(o); } } + + // 查新数据 + var newRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["连接状态"] = newRecord.Status, + ["备注"] = newRecord.Remarks, + ["光衰值"] = newRecord.OpticalAttenuation, + ["历史故障"] = newRecord.HistoryRemarks, + ["光缆断信息"] = newRecord.OpticalCableOffRemarks, + ["设备型号"] = newRecord.EquipmentModel, + ["业务类型"] = newRecord.BusinessType, + ["修改时间"] = newRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return ToResponse(response); } @@ -567,9 +701,19 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpGet("empty/{Id}")] [ActionPermissionFilter(Permission = "odfports:edit")] [Log(Title = "清空数据", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult GetOdfPortsEmpty(int Id) { + var oldRecord = _OdfPortsService.GetFirst(f => f.Id == Id); + string oldData = BuildAuditJson(new Dictionary + { + ["连接状态"] = oldRecord.Status, + ["备注"] = oldRecord.Remarks, + ["历史故障"] = oldRecord.HistoryRemarks, + ["光衰值"] = oldRecord.OpticalAttenuation, + ["光缆断信息"] = oldRecord.OpticalCableOffRemarks, + ["修改时间"] = oldRecord.UpdatedAt, + }); + var response = _OdfPortsService.GetInfo(Id); response.Status = 0; response.Remarks = ""; @@ -578,6 +722,26 @@ namespace ZR.Admin.WebApi.Controllers.Business response.UpdatedAt = DateTime.Now; response.OpticalCableOffRemarks = ""; var s = _OdfPortsService.Update(response); + + var newRecord = _OdfPortsService.GetFirst(f => f.Id == Id); + string newData = BuildAuditJson(new Dictionary + { + ["连接状态"] = newRecord.Status, + ["备注"] = newRecord.Remarks, + ["历史故障"] = newRecord.HistoryRemarks, + ["光衰值"] = newRecord.OpticalAttenuation, + ["光缆断信息"] = newRecord.OpticalCableOffRemarks, + ["修改时间"] = newRecord.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(s); } @@ -588,11 +752,49 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("delete/{ids}")] [ActionPermissionFilter(Permission = "odfports:delete")] [Log(Title = "端口", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult DeleteOdfPorts([FromRoute] string ids) { var idArr = Tools.SplitAndConvert(ids); + // 循环每条记录分别记录 DELETE 审计日志 + foreach (var id in idArr) + { + var oldRecord = _OdfPortsService.GetFirst(f => f.Id == id); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["端口名称"] = oldRecord.Name, + ["机房ID"] = oldRecord.RoomId, + ["机房名称"] = oldRecord.RoomName, + ["机架ID"] = oldRecord.RackId, + ["机架名称"] = oldRecord.RackName, + ["框ID"] = oldRecord.FrameId, + ["框名称"] = oldRecord.FrameName, + ["部门ID"] = oldRecord.DeptId, + ["部门名称"] = oldRecord.DeptName, + ["行号"] = oldRecord.RowNumber, + ["端口号"] = oldRecord.PortNumber, + ["连接状态"] = oldRecord.Status, + ["备注"] = oldRecord.Remarks, + ["光衰值"] = oldRecord.OpticalAttenuation, + ["历史故障"] = oldRecord.HistoryRemarks, + ["创建时间"] = oldRecord.CreatedAt, + ["修改时间"] = oldRecord.UpdatedAt, + ["光缆断信息"] = oldRecord.OpticalCableOffRemarks, + ["设备型号"] = oldRecord.EquipmentModel, + ["业务类型"] = oldRecord.BusinessType, + ["端口侧"] = oldRecord.PortSide, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + return ToResponse(_OdfPortsService.Delete(idArr, "删除端口")); } @@ -1023,7 +1225,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("deleteAll")] [ActionPermissionFilter(Permission = "odfports:delete")] [Log(Title = "删除端口数据", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task DeleteAll([FromBody] OdfRoomsTreeDto treeDto) { if (treeDto.Level == 2) @@ -1034,6 +1235,46 @@ namespace ZR.Admin.WebApi.Controllers.Business { return ToResponse(ResultCode.FAIL, "删除失败"); } + + // 记录机房下所有端口的 DELETE 审计日志 + var ports = await _OdfPortsService.AsQueryable().Where(it => it.RoomId == room.Id).ToListAsync(); + foreach (var port in ports) + { + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = port.Id, + ["端口名称"] = port.Name, + ["机房ID"] = port.RoomId, + ["机房名称"] = port.RoomName, + ["机架ID"] = port.RackId, + ["机架名称"] = port.RackName, + ["框ID"] = port.FrameId, + ["框名称"] = port.FrameName, + ["部门ID"] = port.DeptId, + ["部门名称"] = port.DeptName, + ["行号"] = port.RowNumber, + ["端口号"] = port.PortNumber, + ["连接状态"] = port.Status, + ["备注"] = port.Remarks, + ["光衰值"] = port.OpticalAttenuation, + ["历史故障"] = port.HistoryRemarks, + ["创建时间"] = port.CreatedAt, + ["修改时间"] = port.UpdatedAt, + ["光缆断信息"] = port.OpticalCableOffRemarks, + ["设备型号"] = port.EquipmentModel, + ["业务类型"] = port.BusinessType, + ["端口侧"] = port.PortSide, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", port.Id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + //删除机房下的所有端口 await _OdfPortsService.AsDeleteable().Where(it => it.RoomId == room.Id).ExecuteCommandAsync(); // delete odf_frames where exists ( select id from odf_racks where roomid=1 and id=odf_frames.RackId ) @@ -1056,6 +1297,46 @@ namespace ZR.Admin.WebApi.Controllers.Business { return ToResponse(ResultCode.FAIL, "删除失败"); } + + // 记录机架下所有端口的 DELETE 审计日志 + var ports = await _OdfPortsService.AsQueryable().Where(it => it.RackId == racks.Id).ToListAsync(); + foreach (var port in ports) + { + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = port.Id, + ["端口名称"] = port.Name, + ["机房ID"] = port.RoomId, + ["机房名称"] = port.RoomName, + ["机架ID"] = port.RackId, + ["机架名称"] = port.RackName, + ["框ID"] = port.FrameId, + ["框名称"] = port.FrameName, + ["部门ID"] = port.DeptId, + ["部门名称"] = port.DeptName, + ["行号"] = port.RowNumber, + ["端口号"] = port.PortNumber, + ["连接状态"] = port.Status, + ["备注"] = port.Remarks, + ["光衰值"] = port.OpticalAttenuation, + ["历史故障"] = port.HistoryRemarks, + ["创建时间"] = port.CreatedAt, + ["修改时间"] = port.UpdatedAt, + ["光缆断信息"] = port.OpticalCableOffRemarks, + ["设备型号"] = port.EquipmentModel, + ["业务类型"] = port.BusinessType, + ["端口侧"] = port.PortSide, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", port.Id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + //删除机房下的所有端口 await _OdfPortsService.AsDeleteable().Where(it => it.RackId == racks.Id).ExecuteCommandAsync(); //删除机房下所有的框 @@ -1070,6 +1351,46 @@ namespace ZR.Admin.WebApi.Controllers.Business { return ToResponse(ResultCode.FAIL, "删除失败"); } + + // 记录框下所有端口的 DELETE 审计日志 + var ports = await _OdfPortsService.AsQueryable().Where(it => it.FrameId == frames.Id).ToListAsync(); + foreach (var port in ports) + { + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = port.Id, + ["端口名称"] = port.Name, + ["机房ID"] = port.RoomId, + ["机房名称"] = port.RoomName, + ["机架ID"] = port.RackId, + ["机架名称"] = port.RackName, + ["框ID"] = port.FrameId, + ["框名称"] = port.FrameName, + ["部门ID"] = port.DeptId, + ["部门名称"] = port.DeptName, + ["行号"] = port.RowNumber, + ["端口号"] = port.PortNumber, + ["连接状态"] = port.Status, + ["备注"] = port.Remarks, + ["光衰值"] = port.OpticalAttenuation, + ["历史故障"] = port.HistoryRemarks, + ["创建时间"] = port.CreatedAt, + ["修改时间"] = port.UpdatedAt, + ["光缆断信息"] = port.OpticalCableOffRemarks, + ["设备型号"] = port.EquipmentModel, + ["业务类型"] = port.BusinessType, + ["端口侧"] = port.PortSide, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_ports", port.Id, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + //删除机房下的所有端口 await _OdfPortsService.AsDeleteable().Where(it => it.FrameId == frames.Id).ExecuteCommandAsync(); _OdfFramesService.Delete(frames); diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs index 78c8872..481ed6a 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfRacksController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using ZR.Admin.WebApi.Filters; using ZR.Model.Business.Dto; using ZR.Model.Business; using ZR.Service.Business.IBusinessService; @@ -29,15 +28,18 @@ namespace ZR.Admin.WebApi.Controllers.Business /// 端口 /// private readonly IOdfPortsService _OdfPortsService; + private readonly IOdfAuditLogsService _auditLogsService; public OdfRacksController(IOdfRacksService OdfRacksService, IOdfRoomsService odfRooms, IOdfFramesService odfFramesService, - IOdfPortsService odfPortsService) + IOdfPortsService odfPortsService, + IOdfAuditLogsService auditLogsService) { _OdfRacksService = OdfRacksService; _odfRooms = odfRooms; _OdfFramesService = odfFramesService; _OdfPortsService = odfPortsService; + _auditLogsService = auditLogsService; } /// @@ -76,7 +78,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost] [ActionPermissionFilter(Permission = "odfracks:add")] [Log(Title = "机架列表", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public IActionResult AddOdfRacks([FromBody] OdfRacksDto parm) { var modal = parm.Adapt().ToCreate(HttpContext); @@ -91,6 +92,29 @@ namespace ZR.Admin.WebApi.Controllers.Business } var response = _OdfRacksService.AddOdfRacks(modal); + // 手动记录 INSERT 审计日志 + try + { + var newRecord = _OdfRacksService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["机房ID"] = newRecord.RoomId, + ["序号"] = newRecord.SequenceNumber, + ["ODF名称"] = newRecord.RackName, + ["框数量"] = newRecord.FrameCount, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdatedAt, + ["部门ID"] = newRecord.DeptId, + ["机架类型"] = newRecord.RackType, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_racks", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -102,7 +126,6 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("expert")] [ActionPermissionFilter(Permission = "odfracks:add")] [Log(Title = "机架列表", BusinessType = BusinessType.INSERT)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task AddOdfRacksExpert([FromBody] OdfRacksExpertDto parm) { var modal = parm.Adapt().ToCreate(HttpContext); @@ -271,6 +294,29 @@ namespace ZR.Admin.WebApi.Controllers.Business } } + // 手动记录 INSERT 审计日志(仅记录机架) + try + { + var newRecord = _OdfRacksService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["机房ID"] = newRecord.RoomId, + ["序号"] = newRecord.SequenceNumber, + ["ODF名称"] = newRecord.RackName, + ["框数量"] = newRecord.FrameCount, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdatedAt, + ["部门ID"] = newRecord.DeptId, + ["机架类型"] = newRecord.RackType, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_racks", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } @@ -281,12 +327,43 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPut] [ActionPermissionFilter(Permission = "odfracks:edit")] [Log(Title = "机架列表", BusinessType = BusinessType.UPDATE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task UpdateOdfRacks([FromBody] OdfRacksDto parm) { var modal = parm.Adapt().ToUpdate(HttpContext); + + // 查旧数据 + var oldRecord = _OdfRacksService.GetFirst(f => f.Id == parm.Id); + string oldData = BuildAuditJson(new Dictionary + { + ["ODF名称"] = oldRecord.RackName, + ["序号"] = oldRecord.SequenceNumber, + ["框数量"] = oldRecord.FrameCount, + ["机架类型"] = oldRecord.RackType, + ["修改时间"] = oldRecord.UpdatedAt, + }); + var oldModel = _OdfRacksService.GetById(parm.Id); var response = _OdfRacksService.UpdateOdfRacks(modal); + + // 查新数据并记录 UPDATE 审计日志 + try + { + var newRecord = _OdfRacksService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["ODF名称"] = newRecord.RackName, + ["序号"] = newRecord.SequenceNumber, + ["框数量"] = newRecord.FrameCount, + ["机架类型"] = newRecord.RackType, + ["修改时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_racks", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + if (response > 0) { var rackName = oldModel.RackName; @@ -315,10 +392,36 @@ namespace ZR.Admin.WebApi.Controllers.Business [HttpPost("delete/{ids}")] [ActionPermissionFilter(Permission = "odfracks:delete")] [Log(Title = "机架列表", BusinessType = BusinessType.DELETE)] - [ServiceFilter(typeof(OdfAuditLogFilter))] public async Task DeleteOdfRacks([FromRoute] string ids) { var idArr = Tools.SplitAndConvert(ids); + + // 循环每条记录分别记录 DELETE 审计日志 + foreach (var racksId in idArr) + { + var oldRecord = _OdfRacksService.GetFirst(f => f.Id == racksId); + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = oldRecord.Id, + ["机房ID"] = oldRecord.RoomId, + ["序号"] = oldRecord.SequenceNumber, + ["ODF名称"] = oldRecord.RackName, + ["框数量"] = oldRecord.FrameCount, + ["创建时间"] = oldRecord.CreatedAt, + ["修改时间"] = oldRecord.UpdatedAt, + ["部门ID"] = oldRecord.DeptId, + ["机架类型"] = oldRecord.RackType, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_racks", racksId, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + } + foreach (var racksId in idArr) { var racks = _OdfRacksService.GetById(racksId); diff --git a/server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs b/server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs index 76668f7..6d942e2 100644 --- a/server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs +++ b/server/ZR.Admin.WebApi/Controllers/Business/OdfRoomsController.cs @@ -53,13 +53,15 @@ namespace ZR.Admin.WebApi.Controllers.Business /// 端口 /// private readonly IOdfPortsService _OdfPortsService; + private readonly IOdfAuditLogsService _auditLogsService; public OdfRoomsController(IOdfRoomsService OdfRoomsService, ISysDeptService sysDeptService, ISysUserService sysUserService, IOdfPortsService odfPortsService, IOdfFramesService odfFramesService, - IOdfRacksService odfRacksService) + IOdfRacksService odfRacksService, + IOdfAuditLogsService auditLogsService) { _OdfRoomsService = OdfRoomsService; this.sysDeptService = sysDeptService; @@ -67,6 +69,7 @@ namespace ZR.Admin.WebApi.Controllers.Business _OdfPortsService = odfPortsService; _OdfFramesService = odfFramesService; _OdfRacksService = odfRacksService; + _auditLogsService = auditLogsService; } /// @@ -201,6 +204,29 @@ namespace ZR.Admin.WebApi.Controllers.Business var modal = parm.Adapt().ToCreate(HttpContext); var response = _OdfRoomsService.AddOdfRooms(modal); + // INSERT 审计日志 + try + { + var newRecord = _OdfRoomsService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["机房名称"] = newRecord.RoomName, + ["机房位置"] = newRecord.RoomAddress, + ["机架数量"] = newRecord.RacksCount, + ["备注"] = newRecord.Remarks, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_rooms", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + return SUCCESS(response); } /// @@ -276,6 +302,30 @@ namespace ZR.Admin.WebApi.Controllers.Business modal.DeptId = parm.DeptId; modal.DeptName = parm.DeptName; var response = _OdfRoomsService.AddOdfRooms(modal); + + // INSERT 审计日志(仅记录机房) + try + { + var newRecord = _OdfRoomsService.GetFirst(f => f.Id == response.Id); + string newData = BuildAuditJson(new Dictionary + { + ["Id"] = newRecord.Id, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["机房名称"] = newRecord.RoomName, + ["机房位置"] = newRecord.RoomAddress, + ["机架数量"] = newRecord.RacksCount, + ["备注"] = newRecord.Remarks, + ["创建时间"] = newRecord.CreatedAt, + ["修改时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_rooms", response.Id, "INSERT", null, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + if (parm.RacksCount > 0) { var roomId = response.Id; @@ -477,8 +527,43 @@ namespace ZR.Admin.WebApi.Controllers.Business parm.DeptName = dept.DeptName; } var oldModel = _OdfRoomsService.GetById(parm.Id); + + // 查旧数据 + string oldData = BuildAuditJson(new Dictionary + { + ["机房名称"] = oldModel.RoomName, + ["机房位置"] = oldModel.RoomAddress, + ["部门ID"] = oldModel.DeptId, + ["部门名称"] = oldModel.DeptName, + ["机架数量"] = oldModel.RacksCount, + ["备注"] = oldModel.Remarks, + ["修改时间"] = oldModel.UpdatedAt, + }); + var modal = parm.Adapt().ToUpdate(HttpContext); var response = _OdfRoomsService.UpdateOdfRooms(modal); + + // UPDATE 审计日志 + try + { + var newRecord = _OdfRoomsService.GetFirst(f => f.Id == parm.Id); + string newData = BuildAuditJson(new Dictionary + { + ["机房名称"] = newRecord.RoomName, + ["机房位置"] = newRecord.RoomAddress, + ["部门ID"] = newRecord.DeptId, + ["部门名称"] = newRecord.DeptName, + ["机架数量"] = newRecord.RacksCount, + ["备注"] = newRecord.Remarks, + ["修改时间"] = newRecord.UpdatedAt, + }); + _auditLogsService.AddLog(BuildAuditLog("odf_rooms", parm.Id, "UPDATE", oldData, newData)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + if (response > 0) { var deptId = modal.DeptId ?? 0; @@ -549,6 +634,29 @@ namespace ZR.Admin.WebApi.Controllers.Business { return ToResponse(ResultCode.FAIL, "删除失败,未找到机房数据"); } + + // DELETE 审计日志 + string oldData = BuildAuditJson(new Dictionary + { + ["Id"] = room.Id, + ["部门ID"] = room.DeptId, + ["部门名称"] = room.DeptName, + ["机房名称"] = room.RoomName, + ["机房位置"] = room.RoomAddress, + ["机架数量"] = room.RacksCount, + ["备注"] = room.Remarks, + ["创建时间"] = room.CreatedAt, + ["修改时间"] = room.UpdatedAt, + }); + try + { + _auditLogsService.AddLog(BuildAuditLog("odf_rooms", roomId, "DELETE", oldData, null)); + } + catch (Exception ex) + { + NLog.LogManager.GetCurrentClassLogger().Error(ex, "审计日志写入失败"); + } + //删除机房下的所有端口 await _OdfPortsService.AsDeleteable().Where(it => it.RoomId == room.Id).ExecuteCommandAsync(); // delete odf_frames where exists ( select id from odf_racks where roomid=1 and id=odf_frames.RackId ) diff --git a/server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs b/server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs deleted file mode 100644 index 9218e7c..0000000 --- a/server/ZR.Admin.WebApi/Filters/OdfAuditLogFilter.cs +++ /dev/null @@ -1,337 +0,0 @@ -using Infrastructure; -using Infrastructure.Attribute; -using Infrastructure.Enums; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using SqlSugar; -using SqlSugar.IOC; -using System.Text.Json; -using ZR.Model.Business; -using ZR.Service.Business.IBusinessService; - -namespace ZR.Admin.WebApi.Filters -{ - /// - /// ODF 审计日志 ActionFilter - /// 拦截标石/ODF/干线相关 Controller 的增删改操作,自动记录审计日志。 - /// 使用方式:在 Action 上添加 [ServiceFilter(typeof(OdfAuditLogFilter))] - /// - public class OdfAuditLogFilter : IAsyncActionFilter - { - private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); - private readonly IOdfAuditLogsService _auditLogsService; - - /// - /// 控制器名称到数据库表名的映射 - /// - private static readonly Dictionary ControllerTableMap = new(StringComparer.OrdinalIgnoreCase) - { - { "OdfMarkerPoles", "odf_marker_poles" }, - { "OdfCables", "odf_cables" }, - { "OdfCableFaults", "odf_cable_faults" }, - { "OdfPorts", "odf_ports" }, - { "OdfRacks", "odf_racks" }, - { "OdfFrames", "odf_frames" }, - { "OdfRooms", "odf_rooms" }, - }; - - public OdfAuditLogFilter(IOdfAuditLogsService auditLogsService) - { - _auditLogsService = auditLogsService; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - string oldData = null; - string operationType = null; - string tableName = null; - int? recordId = null; - - try - { - // 从 [Log] 特性获取 BusinessType - var businessType = GetBusinessType(context); - operationType = businessType switch - { - BusinessType.INSERT => "INSERT", - BusinessType.UPDATE => "UPDATE", - BusinessType.DELETE => "DELETE", - _ => null - }; - - // 仅拦截增删改操作 - if (operationType == null) - { - await next(); - return; - } - - // 获取表名 - tableName = GetTableName(context); - - // 获取记录 ID(从路由参数或请求体) - recordId = GetRecordId(context); - - // 操作前:对于 UPDATE/DELETE,尝试读取现有数据作为 OldData - if ((operationType == "UPDATE" || operationType == "DELETE") && recordId.HasValue && recordId.Value > 0) - { - oldData = await GetExistingDataJson(tableName, recordId.Value); - } - } - catch (Exception ex) - { - logger.Error(ex, "审计日志:操作前记录数据失败"); - } - - // 执行实际的 Action - var resultContext = await next(); - - // 操作后:记录审计日志 - try - { - // 如果 Action 执行出错,不记录审计日志 - if (resultContext.Exception != null && !resultContext.ExceptionHandled) - { - return; - } - - if (operationType == null) - { - return; - } - - // 获取新数据 - string newData = null; - if (operationType == "INSERT" || operationType == "UPDATE") - { - newData = GetNewDataFromActionArguments(context); - } - - // 对于 INSERT,尝试从返回结果获取新记录 ID - if (operationType == "INSERT" && (!recordId.HasValue || recordId.Value <= 0)) - { - recordId = GetRecordIdFromResult(resultContext); - } - - // 获取用户信息 - var operatorId = context.HttpContext.GetUId(); - var operatorName = context.HttpContext.GetName(); - var deptId = context.HttpContext.GetDeptId(); - - // 判断操作来源 - var sourceClient = GetSourceClient(context.HttpContext); - - var auditLog = new OdfAuditLogs - { - TableName = tableName ?? "unknown", - RecordId = recordId, - OperationType = operationType, - OperatorId = operatorId, - OperatorName = operatorName, - SourceClient = sourceClient, - OldData = oldData, - NewData = newData, - OperationTime = DateTime.Now, - DeptId = deptId > 0 ? deptId : null, - }; - - _auditLogsService.AddLog(auditLog); - } - catch (Exception ex) - { - // 审计日志写入失败不影响主业务 - logger.Error(ex, "审计日志:写入审计日志失败"); - } - } - - /// - /// 从 [Log] 特性获取 BusinessType - /// - private static BusinessType? GetBusinessType(ActionExecutingContext context) - { - if (context.ActionDescriptor is not ControllerActionDescriptor descriptor) - return null; - - var logAttr = descriptor.MethodInfo - .GetCustomAttributes(inherit: true) - .OfType() - .FirstOrDefault(); - - return logAttr?.BusinessType; - } - - /// - /// 从控制器名称映射到数据库表名 - /// - private static string GetTableName(ActionExecutingContext context) - { - if (context.ActionDescriptor is ControllerActionDescriptor descriptor) - { - var controllerName = descriptor.ControllerName; - if (ControllerTableMap.TryGetValue(controllerName, out var table)) - { - return table; - } - // 回退:使用控制器名称 - return controllerName; - } - return "unknown"; - } - - /// - /// 从路由参数或请求体中提取记录 ID - /// - private static int? GetRecordId(ActionExecutingContext context) - { - // 尝试从路由参数获取 id - if (context.ActionArguments.TryGetValue("id", out var idObj)) - { - if (idObj is int intId) return intId; - if (int.TryParse(idObj?.ToString(), out var parsed)) return parsed; - } - - // 尝试从路由参数获取 ids(批量删除场景,取第一个) - if (context.ActionArguments.TryGetValue("ids", out var idsObj) && idsObj is string idsStr) - { - var first = idsStr.Split(',', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - if (int.TryParse(first?.Trim(), out var firstId)) return firstId; - } - - // 尝试从请求体 DTO 中获取 Id 属性 - foreach (var arg in context.ActionArguments.Values) - { - if (arg == null) continue; - var idProp = arg.GetType().GetProperty("Id"); - if (idProp != null && idProp.PropertyType == typeof(int)) - { - var val = (int)(idProp.GetValue(arg) ?? 0); - if (val > 0) return val; - } - } - - return null; - } - - /// - /// 从 Action 参数中获取新数据(请求体 DTO 序列化为 JSON) - /// - private static string GetNewDataFromActionArguments(ActionExecutingContext context) - { - try - { - // 查找请求体中的 DTO 对象(排除简单类型) - foreach (var arg in context.ActionArguments) - { - if (arg.Value == null) continue; - var type = arg.Value.GetType(); - if (type.IsClass && type != typeof(string) && !type.IsPrimitive) - { - return JsonSerializer.Serialize(arg.Value, new JsonSerializerOptions - { - WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - } - } - } - catch (Exception ex) - { - logger.Warn(ex, "审计日志:序列化新数据失败"); - } - return null; - } - - /// - /// 从 Action 返回结果中尝试获取新记录 ID - /// - private static int? GetRecordIdFromResult(ActionExecutedContext context) - { - try - { - if (context.Result is ObjectResult objResult && objResult.Value != null) - { - // 尝试从返回对象中获取 data 或 result 属性 - var resultType = objResult.Value.GetType(); - var dataProp = resultType.GetProperty("Data") ?? resultType.GetProperty("data"); - if (dataProp != null) - { - var dataVal = dataProp.GetValue(objResult.Value); - if (dataVal is int intVal && intVal > 0) return intVal; - if (int.TryParse(dataVal?.ToString(), out var parsed) && parsed > 0) return parsed; - } - } - } - catch - { - // 忽略 - } - return null; - } - - /// - /// 判断操作来源:App 端或管理后台端 - /// 优先从自定义 Header "X-Source-Client" 判断, - /// 其次从 User-Agent 判断(包含 uni-app 或移动端标识则为 App) - /// - private static string GetSourceClient(HttpContext httpContext) - { - // 优先检查自定义 Header - if (httpContext.Request.Headers.TryGetValue("X-Source-Client", out var sourceHeader)) - { - var val = sourceHeader.ToString().Trim(); - if (!string.IsNullOrEmpty(val)) - { - return val.Equals("App", StringComparison.OrdinalIgnoreCase) ? "App" : "Admin"; - } - } - - // 从 User-Agent 判断 - var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); - if (!string.IsNullOrEmpty(userAgent)) - { - if (userAgent.Contains("uni-app", StringComparison.OrdinalIgnoreCase) - || userAgent.Contains("Android", StringComparison.OrdinalIgnoreCase) - || userAgent.Contains("iPhone", StringComparison.OrdinalIgnoreCase) - || userAgent.Contains("iPad", StringComparison.OrdinalIgnoreCase)) - { - return "App"; - } - } - - // 默认为管理后台 - return "Admin"; - } - - /// - /// 查询现有记录数据并序列化为 JSON(用于 OldData) - /// 通过 SqlSugar 直接查询对应表 - /// - private async Task GetExistingDataJson(string tableName, int recordId) - { - try - { - var db = DbScoped.SugarScope; - if (db == null) return null; - - var data = await db.Queryable() - .AS(tableName) - .Where("Id = @id", new { id = recordId }) - .FirstAsync(); - - if (data == null) return null; - - return JsonSerializer.Serialize(data, new JsonSerializerOptions - { - WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - } - catch (Exception ex) - { - logger.Warn(ex, $"审计日志:查询表 {tableName} 记录 {recordId} 的旧数据失败"); - return null; - } - } - } -} diff --git a/server/ZR.Admin.WebApi/Program.cs b/server/ZR.Admin.WebApi/Program.cs index e5301d8..29f3e89 100644 --- a/server/ZR.Admin.WebApi/Program.cs +++ b/server/ZR.Admin.WebApi/Program.cs @@ -62,8 +62,6 @@ builder.Services.AddJwt(); builder.Services.AddSingleton(new AppSettings(builder.Configuration)); //app服务注册 builder.Services.AddAppService(); -// 注册 ODF 审计日志 Filter(ServiceFilter 需要 DI 注入) -builder.Services.AddScoped(); var wxMp = builder.Configuration.GetSection("WxMp"); var mp_appid = wxMp.GetSection("AppID").Value ?? "";//默认值 var mp_appSecret = wxMp.GetSection("AppSecret").Value ?? "";//默认值 diff --git a/server/ZR.Vue/src/views/business/OdfAuditLogs.vue b/server/ZR.Vue/src/views/business/OdfAuditLogs.vue index db09154..4ed8d80 100644 --- a/server/ZR.Vue/src/views/business/OdfAuditLogs.vue +++ b/server/ZR.Vue/src/views/business/OdfAuditLogs.vue @@ -92,7 +92,7 @@ @@ -100,7 +100,7 @@ @@ -197,81 +197,6 @@ function handleViewDetail(row) { detailVisible.value = true } -// 字段名中文映射(按表名分组,通用字段放 _common) -const fieldLabelMap = { - _common: { - id: 'ID', - name: '名称', - deptId: '部门ID', - deptName: '所属公司', - createdAt: '创建时间', - updatedAt: '修改时间', - remarks: '备注说明', - status: '连接状态', - latitude: '纬度', - longitude: '经度', - userId: '用户ID', - personnel: '责任人', - }, - odf_ports: { - roomId: '机房ID', - roomName: '机房名称', - rackId: '机架ID', - rackName: '机架名称', - frameId: '框ID', - frameName: '框名称', - rowNumber: '行号', - portNumber: '端口号', - opticalAttenuation: '光衰值(dB)', - historyRemarks: '历史故障', - opticalCableOffRemarks: '光缆断信息', - equipmentModel: '设备型号', - businessType: '业务类型', - portSide: '端口侧', - rackType: '机架类型', - }, - odf_rooms: { - roomName: '机房名称', - roomAddress: '机房位置', - racksCount: '机架数量', - orderby: '排序', - }, - odf_racks: { - roomId: '机房ID', - sequenceNumber: '序号', - rackName: '机架名称', - frameCount: '框数量', - rackType: '机架类型', - }, - odf_frames: { - rackId: '机架ID', - portsName: '框名称', - sequenceNumber: '序号', - portsCount: '端口数量', - portsCol: '列数量', - portsRow: '行数量', - updateAt: '修改时间', - }, - odf_cables: { - cableName: '光缆名称', - }, - odf_cable_faults: { - cableId: '光缆ID', - faultTime: '故障时间', - faultReason: '故障原因', - mileage: '表显故障里程', - mileageCorrection: '表显里程矫正', - location: '地点描述', - remark: '备注', - faultCount: '故障频次', - }, - odf_marker_poles: { - cableId: '光缆ID', - recordTime: '记录时间', - actualMileage: '实际里程', - }, -} - // 表名中文映射 const tableNameMap = { odf_ports: '端口', @@ -283,24 +208,6 @@ const tableNameMap = { odf_marker_poles: '标石/杆号牌', } -// 将 JSON 中的字段名替换为中文标签 -function translateJson(jsonStr, tableName) { - if (!jsonStr) return '(无数据)' - try { - const obj = JSON.parse(jsonStr) - const tableMap = fieldLabelMap[tableName] || {} - const commonMap = fieldLabelMap._common - const translated = {} - for (const [key, value] of Object.entries(obj)) { - const label = tableMap[key] || commonMap[key] || key - translated[label] = value - } - return JSON.stringify(translated, null, 2) - } catch { - return jsonStr - } -} - // 格式化 JSON 字符串 function formatJson(jsonStr) { if (!jsonStr) return '(无数据)'