21
Some checks reported errors
continuous-integration/drone/push Build encountered an error

This commit is contained in:
zpc 2026-04-21 21:57:28 +08:00
parent be824e7f06
commit 996e1abc24
20 changed files with 2350 additions and 468 deletions

View File

@ -0,0 +1 @@
{"specId": "a3f7c2e1-8d4b-4f9a-b6e3-1c5d8a2f7e90", "workflowType": "requirements-first", "specType": "feature"}

View File

@ -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<string, object>` 序列化为紧凑 JSON自动格式化 DateTime
```csharp
/// <summary>
/// 将字段字典序列化为审计 JSON用于所有操作类型
/// </summary>
protected static string BuildAuditJson(Dictionary<string, object> fields)
{
var result = new Dictionary<string, object>();
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<string, object>`,通过 `BuildAuditJson` 序列化。不使用任何通用序列化 + 字段映射字典的方式。
### 2. 每个实体的中文字段名映射字典
每个控制器文件中定义一个 `static readonly Dictionary<string, string>` 作为该实体的字段映射。也可以统一放在 `BaseController` 或一个独立的静态类中。推荐放在各自控制器中,保持就近原则。
#### OdfCableFaults 映射
```csharp
private static readonly Dictionary<string, string> 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<string, string> CablesFieldMapping = new()
{
["id"] = "Id",
["cableName"] = "光缆名称",
["deptId"] = "部门ID",
["deptName"] = "部门名称",
["createdAt"] = "创建时间",
["updatedAt"] = "更新时间",
};
```
#### OdfMarkerPoles 映射
```csharp
private static readonly Dictionary<string, string> 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<string, string> RacksFieldMapping = new()
{
["id"] = "Id",
["roomId"] = "机房ID",
["sequenceNumber"] = "序号",
["rackName"] = "ODF名称",
["frameCount"] = "框数量",
["createdAt"] = "创建时间",
["updatedAt"] = "修改时间",
["deptId"] = "部门ID",
["rackType"] = "机架类型",
};
```
#### OdfPorts 映射
```csharp
private static readonly Dictionary<string, string> 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<string, string> FramesFieldMapping = new()
{
["id"] = "Id",
["rackId"] = "机架ID",
["portsName"] = "名称",
["deptId"] = "部门ID",
["sequenceNumber"] = "序号",
["portsCount"] = "端口数量",
["portsCol"] = "列数量",
["portsRow"] = "行数量",
["createdAt"] = "创建时间",
["updateAt"] = "修改时间",
};
```
#### OdfRooms 映射
```csharp
private static readonly Dictionary<string, string> 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<string, object>
{
["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<string, object>
{
["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<string, object>` 序列化为紧凑 JSON并自动格式化 DateTime
```csharp
/// <summary>
/// 将字段字典序列化为审计 JSON用于 UPDATE 差量记录)
/// </summary>
protected static string BuildAuditJson(Dictionary<string, object> fields)
{
var result = new Dictionary<string, object>();
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<string, object>
{
["故障发生频次"] = oldFault.FaultCount,
["更新时间"] = oldFault.UpdatedAt,
});
// 执行更新
var response = _OdfCableFaultsService.IncrementFaultCount(id);
// 更新后
var newFault = _OdfCableFaultsService.GetFirst(f => f.Id == id);
string newData = BuildAuditJson(new Dictionary<string, object>
{
["故障发生频次"] = 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<string, object>
{
["连接状态"] = 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<string, object>
{
["连接状态"] = 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` 函数 |

View File

@ -0,0 +1,141 @@
# 需求文档:审计日志中文字段名
## 简介
当前系统的审计日志odf_audit_logsOldData/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 表
## 需求
### 需求 1INSERT 操作使用中文字段名全量序列化
**用户故事:** 作为系统管理员,我希望 INSERT 审计日志的 NewData 使用中文字段名作为 JSON 键,以便在审计日志查看界面直接看懂每个字段的含义。
#### 验收标准
1. WHEN 任意控制器方法执行 INSERT 操作并写入审计日志时THE 审计日志系统 SHALL 将 NewData 的 JSON 键名全部替换为对应的中文字段名
2. THE 审计日志系统 SHALL 在 INSERT 操作中记录实体的所有字段全量OldData 为 null
3. WHEN OdfCableFaultsController.Add 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→Id, CableId→关联光缆ID, FaultTime→故障时间, Personnel→人员, FaultReason→故障原因, Mileage→表显故障里程, MileageCorrection→表显里程矫正, Location→地点描述, Latitude→纬度, Longitude→经度, Remark→备注, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间, FaultCount→故障发生频次
4. WHEN OdfCablesController.Add 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→Id, CableName→光缆名称, DeptId→部门ID, DeptName→部门名称, CreatedAt→创建时间, UpdatedAt→更新时间
5. WHEN OdfMarkerPolesController.Add 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→Id, CableId→关联光缆ID, Name→名称, RecordTime→记录时间, Personnel→责任人, Latitude→纬度, Longitude→经度, ActualMileage→实际里程, DeptId→部门ID, DeptName→部门名称, UserId→提交人用户ID, CreatedAt→创建时间, UpdatedAt→更新时间
6. WHEN OdfRacksController.AddOdfRacks 或 AddOdfRacksExpert 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→Id, RoomId→机房ID, SequenceNumber→序号, RackName→ODF名称, FrameCount→框数量, CreatedAt→创建时间, UpdatedAt→修改时间, DeptId→部门ID, RackType→机架类型
7. WHEN OdfPortsController.AddOdfPorts 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→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 使用以下中文字段名映射生成 NewDataId→Id, RackId→机架ID, PortsName→名称, DeptId→部门ID, SequenceNumber→序号, PortsCount→端口数量, PortsCol→列数量, PortsRow→行数量, CreatedAt→创建时间, UpdateAt→修改时间
9. WHEN OdfRoomsController.AddOdfRooms 或 AddExpertOdfRooms 执行 INSERT 时THE 审计日志系统 SHALL 使用以下中文字段名映射生成 NewDataId→Id, DeptId→部门ID, DeptName→部门名称, RoomName→机房名称, RoomAddress→机房位置, RacksCount→机架数量, Remarks→备注, CreatedAt→创建时间, UpdatedAt→修改时间
### 需求 2DELETE 操作使用中文字段名全量序列化
**用户故事:** 作为系统管理员,我希望 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
### 需求 3UPDATE 操作仅记录实际修改字段并使用中文字段名
**用户故事:** 作为系统管理员,我希望 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 个方法AddINSERT、IncrementFaultCountUPDATE、UpdateMileageCorrectionUPDATE、DeleteDELETE
2. THE 审计日志系统 SHALL 修改 OdfCablesController 的以下 3 个方法AddINSERT、UpdateUPDATE、DeleteDELETE
3. THE 审计日志系统 SHALL 修改 OdfMarkerPolesController 的以下 3 个方法AddINSERT、UpdateUPDATE、DeleteDELETE
4. THE 审计日志系统 SHALL 修改 OdfRacksController 的以下 4 个方法AddOdfRacksINSERT、AddOdfRacksExpertINSERT、UpdateOdfRacksUPDATE、DeleteOdfRacksDELETE
5. THE 审计日志系统 SHALL 修改 OdfPortsController 的以下 7 个方法GetOdfPortsStatusUPDATE、AddOdfPortsINSERT、UpdateOdfPortsUPDATE、SaveMOdfPortsUPDATE、GetOdfPortsEmptyUPDATE、DeleteOdfPortsDELETE、DeleteAllDELETE
6. THE 审计日志系统 SHALL 修改 OdfFramesController 的以下 3 个方法AddOdfFramesINSERT、UpdateOdfFramesUPDATE、DeleteOdfFramesDELETE
7. THE 审计日志系统 SHALL 修改 OdfRoomsController 的以下 4 个方法AddOdfRoomsINSERT、AddExpertOdfRoomsINSERT、UpdateOdfRoomsUPDATE、DeleteOdfRoomsDELETE
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` 方法

View File

@ -0,0 +1,115 @@
# 实施计划:审计日志中文字段名
## 概述
将审计日志系统的 OldData/NewData JSON 键名从 camelCase 替换为中文字段名。所有操作INSERT/DELETE/UPDATE都在每个方法内部手动构造 `Dictionary<string, object>`,通过 `BuildAuditJson` 序列化。不使用通用序列化方法,不使用字段映射字典。前端移除 `translateJson``fieldLabelMap`,直接用 `formatJson` 展示。
## 任务
- [x] 1. BaseController 新增 BuildAuditJson 方法
- [x] 1.1 在 `server/Infrastructure/Controllers/BaseController.cs` 中新增 `BuildAuditJson(Dictionary<string, object> 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` 编译通过
- 前端无语法错误

View File

@ -0,0 +1,5 @@
{
"specType": "feature",
"workflowType": "requirements-first",
"featureName": "audit-log-manual"
}

View File

@ -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<OdfAuditLogFilter>()` |
## 组件与接口
### 审计日志服务接口
已有 `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<int>(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
{
// ... 现有代码 ...
/// <summary>
/// 审计日志 JSON 序列化选项
/// </summary>
protected static readonly JsonSerializerOptions AuditJsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// 获取操作来源客户端标识
/// </summary>
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";
}
/// <summary>
/// 构造审计日志对象
/// </summary>
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,
};
}
/// <summary>
/// 将实体序列化为审计日志 JSON
/// </summary>
protected static string SerializeForAudit<T>(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. 全量测试验证

View File

@ -0,0 +1,81 @@
# 需求:审计日志手动化改造
## 背景
当前系统通过 `OdfAuditLogFilter`ActionFilter 拦截器)自动记录业务数据的增删改审计日志。该方案存在以下问题:
1. 对于只传简单类型参数(如 `int id`)的接口,拦截器无法序列化 NewData导致审计日志中修改后数据为空
2. 批量删除接口(传 `string ids`)只能记录第一条记录的 OldData
3. 拦截器逻辑对不同接口的参数结构有隐式依赖,不够透明
4. 部分 ControllerOdfFrames、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签到记录
- OdfAppUpdatesControllerApp版本管理
- OdfUserModulesController用户模块权限
- OdfPortFaultController错误日志只读
- CosUploadController文件上传
- OdfAuditLogsController审计日志本身只读

View File

@ -0,0 +1,62 @@
# 审计日志手动化改造 — 任务列表
## Task 1: 扩展 BaseController 公共方法
- [x] 在 `BaseController` 中添加 `AuditJsonOptions`、`GetSourceClient()`、`BuildAuditLog()`、`SerializeForAudit<T>()` 方法
- [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<OdfAuditLogFilter>()` 注册
- [x] 全局搜索确认无其他文件引用 `OdfAuditLogFilter`
- [x] 编译验证通过

View File

@ -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";
/// <summary>
/// 审计日志 JSON 序列化选项
/// </summary>
protected static readonly JsonSerializerOptions AuditJsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// 获取操作来源客户端标识
/// </summary>
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";
}
/// <summary>
/// 构造审计日志对象
/// </summary>
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,
};
}
/// <summary>
/// 将实体序列化为审计日志 JSON
/// </summary>
protected static string SerializeForAudit<T>(T entity) where T : class
{
return entity != null ? System.Text.Json.JsonSerializer.Serialize(entity, AuditJsonOptions) : null;
}
/// <summary>
/// 将字段字典序列化为审计 JSON用于所有操作类型
/// </summary>
protected static string BuildAuditJson(Dictionary<string, object> fields)
{
var result = new Dictionary<string, object>();
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 });
}
/// <summary>
/// 返回成功封装
/// </summary>

View File

@ -10,6 +10,10 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZR.Model\ZR.Model.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AspectCore.Abstractions" Version="2.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.2" />

View File

@ -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
/// 干线故障接口
/// </summary>
private readonly IOdfCableFaultsService _OdfCableFaultsService;
private readonly IOdfAuditLogsService _auditLogsService;
public OdfCableFaultsController(IOdfCableFaultsService OdfCableFaultsService)
public OdfCableFaultsController(
IOdfCableFaultsService OdfCableFaultsService,
IOdfAuditLogsService auditLogsService)
{
_OdfCableFaultsService = OdfCableFaultsService;
_auditLogsService = auditLogsService;
}
/// <summary>
@ -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<IActionResult> 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<string, object>
{
["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<string, object>
{
["故障发生频次"] = oldFault.FaultCount,
["更新时间"] = oldFault.UpdatedAt,
});
// 执行更新
var response = _OdfCableFaultsService.IncrementFaultCount(id);
// 记录新数据
var newFault = _OdfCableFaultsService.GetFirst(f => f.Id == id);
string newData = BuildAuditJson(new Dictionary<string, object>
{
["故障发生频次"] = 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<string, object>
{
["表显里程矫正"] = oldRecord.MileageCorrection,
["更新时间"] = oldRecord.UpdatedAt,
});
// 执行更新
var response = _OdfCableFaultsService.UpdateMileageCorrection(id, dto.MileageCorrection);
// 查新数据
var newRecord = _OdfCableFaultsService.GetFirst(f => f.Id == id);
string newData = BuildAuditJson(new Dictionary<string, object>
{
["表显里程矫正"] = 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<string, object>
{
["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)
{

View File

@ -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
/// 光缆管理接口
/// </summary>
private readonly IOdfCablesService _OdfCablesService;
private readonly IOdfAuditLogsService _auditLogsService;
public OdfCablesController(IOdfCablesService OdfCablesService)
public OdfCablesController(
IOdfCablesService OdfCablesService,
IOdfAuditLogsService auditLogsService)
{
_OdfCablesService = OdfCablesService;
_auditLogsService = auditLogsService;
}
/// <summary>
@ -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<string, object>
{
["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<string, object>
{
["光缆名称"] = 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<string, object>
{
["光缆名称"] = 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<string, object>
{
["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);
}

View File

@ -27,15 +27,18 @@ namespace ZR.Admin.WebApi.Controllers.Business
/// </summary>
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;
}
/// <summary>
@ -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<string, object>
{
["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<IActionResult> UpdateOdfFrames([FromBody] OdfFramesDto parm)
{
var modal = parm.Adapt<OdfFrames>().ToUpdate(HttpContext);
// 查旧数据
var oldRecord = _OdfFramesService.GetFirst(f => f.Id == parm.Id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["名称"] = 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<string, object>
{
["名称"] = 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<int>(ids);
// 循环每条记录分别记录 DELETE 审计日志
foreach (var id in idArr)
{
var oldRecord = _OdfFramesService.GetFirst(f => f.Id == id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["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, "删除框-信息"));
}

View File

@ -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
/// 标石/杆号牌接口
/// </summary>
private readonly IOdfMarkerPolesService _OdfMarkerPolesService;
private readonly IOdfAuditLogsService _auditLogsService;
public OdfMarkerPolesController(IOdfMarkerPolesService OdfMarkerPolesService)
public OdfMarkerPolesController(
IOdfMarkerPolesService OdfMarkerPolesService,
IOdfAuditLogsService auditLogsService)
{
_OdfMarkerPolesService = OdfMarkerPolesService;
_auditLogsService = auditLogsService;
}
/// <summary>
@ -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<string, object>
{
["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<string, object>
{
["名称"] = 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<string, object>
{
["名称"] = 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<string, object>
{
["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)
{

View File

@ -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
/// </summary>
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;
}
/// <summary>
@ -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<string, object>
{
["连接状态"] = 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<string, object>
{
["连接状态"] = 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<IActionResult> AddOdfPorts([FromBody] OdfPortsDto parm)
{
var modal = parm.Adapt<OdfPorts>().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<string, object>
{
["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<IActionResult> UpdateOdfPorts([FromBody] OdfPortsDto parm)
{
// 查旧数据
var oldRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["端口名称"] = oldRecord.Name,
["连接状态"] = oldRecord.Status,
["备注"] = oldRecord.Remarks,
["光衰值"] = oldRecord.OpticalAttenuation,
["历史故障"] = oldRecord.HistoryRemarks,
["光缆断信息"] = oldRecord.OpticalCableOffRemarks,
["设备型号"] = oldRecord.EquipmentModel,
["业务类型"] = oldRecord.BusinessType,
["修改时间"] = oldRecord.UpdatedAt,
});
var modal = parm.Adapt<OdfPorts>().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<string, object>
{
["端口名称"] = 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<IActionResult> SaveMOdfPorts([FromBody] OdfPortsMMDto parm)
{
// 查旧数据
var oldRecord = _OdfPortsService.GetFirst(f => f.Id == parm.Id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["连接状态"] = 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<string, object>
{
["连接状态"] = 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<string, object>
{
["连接状态"] = 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<string, object>
{
["连接状态"] = 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<int>(ids);
// 循环每条记录分别记录 DELETE 审计日志
foreach (var id in idArr)
{
var oldRecord = _OdfPortsService.GetFirst(f => f.Id == id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["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<IActionResult> 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<string, object>
{
["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<string, object>
{
["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<string, object>
{
["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);

View File

@ -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
/// 端口
/// </summary>
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;
}
/// <summary>
@ -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<OdfRacks>().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<string, object>
{
["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<IActionResult> AddOdfRacksExpert([FromBody] OdfRacksExpertDto parm)
{
var modal = parm.Adapt<OdfRacks>().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<string, object>
{
["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<IActionResult> UpdateOdfRacks([FromBody] OdfRacksDto parm)
{
var modal = parm.Adapt<OdfRacks>().ToUpdate(HttpContext);
// 查旧数据
var oldRecord = _OdfRacksService.GetFirst(f => f.Id == parm.Id);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["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<string, object>
{
["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<IActionResult> DeleteOdfRacks([FromRoute] string ids)
{
var idArr = Tools.SplitAndConvert<int>(ids);
// 循环每条记录分别记录 DELETE 审计日志
foreach (var racksId in idArr)
{
var oldRecord = _OdfRacksService.GetFirst(f => f.Id == racksId);
string oldData = BuildAuditJson(new Dictionary<string, object>
{
["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);

View File

@ -53,13 +53,15 @@ namespace ZR.Admin.WebApi.Controllers.Business
/// 端口
/// </summary>
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;
}
/// <summary>
@ -201,6 +204,29 @@ namespace ZR.Admin.WebApi.Controllers.Business
var modal = parm.Adapt<OdfRooms>().ToCreate(HttpContext);
var response = _OdfRoomsService.AddOdfRooms(modal);
// INSERT 审计日志
try
{
var newRecord = _OdfRoomsService.GetFirst(f => f.Id == response.Id);
string newData = BuildAuditJson(new Dictionary<string, object>
{
["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);
}
/// <summary>
@ -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<string, object>
{
["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<string, object>
{
["机房名称"] = oldModel.RoomName,
["机房位置"] = oldModel.RoomAddress,
["部门ID"] = oldModel.DeptId,
["部门名称"] = oldModel.DeptName,
["机架数量"] = oldModel.RacksCount,
["备注"] = oldModel.Remarks,
["修改时间"] = oldModel.UpdatedAt,
});
var modal = parm.Adapt<OdfRooms>().ToUpdate(HttpContext);
var response = _OdfRoomsService.UpdateOdfRooms(modal);
// UPDATE 审计日志
try
{
var newRecord = _OdfRoomsService.GetFirst(f => f.Id == parm.Id);
string newData = BuildAuditJson(new Dictionary<string, object>
{
["机房名称"] = 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<string, object>
{
["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 )

View File

@ -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
{
/// <summary>
/// ODF 审计日志 ActionFilter
/// 拦截标石/ODF/干线相关 Controller 的增删改操作,自动记录审计日志。
/// 使用方式:在 Action 上添加 [ServiceFilter(typeof(OdfAuditLogFilter))]
/// </summary>
public class OdfAuditLogFilter : IAsyncActionFilter
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IOdfAuditLogsService _auditLogsService;
/// <summary>
/// 控制器名称到数据库表名的映射
/// </summary>
private static readonly Dictionary<string, string> 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, "审计日志:写入审计日志失败");
}
}
/// <summary>
/// 从 [Log] 特性获取 BusinessType
/// </summary>
private static BusinessType? GetBusinessType(ActionExecutingContext context)
{
if (context.ActionDescriptor is not ControllerActionDescriptor descriptor)
return null;
var logAttr = descriptor.MethodInfo
.GetCustomAttributes(inherit: true)
.OfType<LogAttribute>()
.FirstOrDefault();
return logAttr?.BusinessType;
}
/// <summary>
/// 从控制器名称映射到数据库表名
/// </summary>
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";
}
/// <summary>
/// 从路由参数或请求体中提取记录 ID
/// </summary>
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;
}
/// <summary>
/// 从 Action 参数中获取新数据(请求体 DTO 序列化为 JSON
/// </summary>
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;
}
/// <summary>
/// 从 Action 返回结果中尝试获取新记录 ID
/// </summary>
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;
}
/// <summary>
/// 判断操作来源App 端或管理后台端
/// 优先从自定义 Header "X-Source-Client" 判断,
/// 其次从 User-Agent 判断(包含 uni-app 或移动端标识则为 App
/// </summary>
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";
}
/// <summary>
/// 查询现有记录数据并序列化为 JSON用于 OldData
/// 通过 SqlSugar 直接查询对应表
/// </summary>
private async Task<string> GetExistingDataJson(string tableName, int recordId)
{
try
{
var db = DbScoped.SugarScope;
if (db == null) return null;
var data = await db.Queryable<dynamic>()
.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;
}
}
}
}

View File

@ -62,8 +62,6 @@ builder.Services.AddJwt();
builder.Services.AddSingleton(new AppSettings(builder.Configuration));
//app服务注册
builder.Services.AddAppService();
// 注册 ODF 审计日志 FilterServiceFilter 需要 DI 注入)
builder.Services.AddScoped<ZR.Admin.WebApi.Filters.OdfAuditLogFilter>();
var wxMp = builder.Configuration.GetSection("WxMp");
var mp_appid = wxMp.GetSection("AppID").Value ?? "";//默认值
var mp_appSecret = wxMp.GetSection("AppSecret").Value ?? "";//默认值

View File

@ -92,7 +92,7 @@
<el-input
type="textarea"
:rows="15"
:model-value="translateJson(detailRow.oldData, detailRow.tableName)"
:model-value="formatJson(detailRow.oldData)"
readonly />
</el-col>
<el-col :span="12">
@ -100,7 +100,7 @@
<el-input
type="textarea"
:rows="15"
:model-value="translateJson(detailRow.newData, detailRow.tableName)"
:model-value="formatJson(detailRow.newData)"
readonly />
</el-col>
</el-row>
@ -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 '(无数据)'