当前位置: 首页 > news >正文

.NET自定义数据操作日志

文章目录

  • 一、前言
  • 二、效果
  • 三、数据库设计
    • 核心思路
    • 实现方案
      • 1、创建两张表:
      • 2、创建视图vw_Tables查询表及字段信息:
      • 3、在ODBC操作(Insert/Update/Delete)时增加LogSet检查机制
      • 4、数据示例1
      • 5、数据示例2
  • 四、.NET实现
      • 1、日志辅助类实现:LogHelper.cs
      • 2、主要业务逻辑实现 :_logs.cs
      • 3、主要实体类: log.cs
  • 五、ODBC实现
      • 1、链接数据库实现
      • 2、调用实现:Access.cs
      • 3、Json转换类实现: JsonHelper.cs
  • 六、前端实现
      • 1、设置页面 LogSet.vue
      • 2、日志查看页面 Logview.vue

一、前言

在实际开发中,对数据库表的字段进行增删改查操作时,记录操作日志往往很有必要。虽然SQL Server提供了触发器机制来实现这一功能,但存在编写复杂、需要为每个表单独编写SQL、且任何改动都会触发执行等问题,导致资源占用较大。

基于此,我采用VUE3、WebAPI(.net 8)和SQLServer技术栈,实现了一套更高效的日志记录方案。

二、效果

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

三、数据库设计

核心思路

取表和字段
=> 前端设置哪些需要记录
=>表数据变动的时候加一个检查
=>检查成功写入日志表

实现方案

1、创建两张表:

  • A表:日志配置表(LogSet)
  • B表:日志记录表

日志配置表示例
需要记录的字段配置在LogSet表中

日志记录表示例
日志记录原理:将新旧数据转换为JSON格式,分别存入oldVal和newVal字段

2、创建视图vw_Tables查询表及字段信息:

通过SQL Server系统视图[INFORMATION_SCHEMA].[COLUMNS]和[INFORMATION_SCHEMA].[TABLES]获取表结构信息

CREATE VIEW [dbo].[vw_Tables]
AS
SELECT A.TABLE_NAME AS tname, A.COLUMN_NAME AS cname
FROM INFORMATION_SCHEMA.COLUMNS AS A 
LEFT JOIN INFORMATION_SCHEMA.TABLES AS B 
ON A.TABLE_NAME = B.TABLE_NAME
WHERE B.TABLE_TYPE = 'BASE TABLE' 
AND B.TABLE_NAME <> 'sysdiagrams'

视图查询结果示例

3、在ODBC操作(Insert/Update/Delete)时增加LogSet检查机制

4、数据示例1

在这里插入图片描述

5、数据示例2

在这里插入图片描述

四、.NET实现

1、日志辅助类实现:LogHelper.cs

using Data;
using Model.Data;
using NPOI.OpenXmlFormats;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;namespace Datas
{/// <summary>/// 日志需求检查、及写入/// </summary>public class LogHelper{private static List<LogSet> _logset = new List<LogSet>();/// <summary>/// 添加字段修改日志  对表的增删改查_根据设置_进行日志写入/// </summary>public static async void main<T>(T newData, string sql) where T : new(){string Operation = "";if (sql.Contains("Insert")) Operation = "add";if (sql.Contains("Update")) Operation = "update";if (sql.Contains("Delete")) Operation = "delete";//1、获取表名Type type = typeof(T);string tableName = type.Name;if (tableName == "Logs") return;//2、设置为空 读取设置if (_logset.Count == 0)_logset = await Access.GetModelList<LogSet>("");//3、查询设置 本表改动是否要日志var list = _logset.Where(p => p.tableName == tableName).ToList();if (list.Count == 0) return;//表不在记录之内//4、查询设置 本列改动是否要日志  否的话就清掉PropertyInfo[] ps = type.GetProperties();int id = (int)typeof(T).GetProperty("id").GetValue(newData);//-旧数据T oldData = new T();if (id > 0){//查询数据oldData = await Access.GetModel<T>(id);}//-新数据(改动记录)if (Operation != "delete"){ps.ToList().ForEach(p =>{int idx = list.FindIndex(x => x.columnsName == p.Name);if (idx == -1) p.SetValue(newData, null);});}//5、存储主体 日志信息写入Model.Data.Logs m = new Model.Data.Logs();m.tableName = tableName;m.operation = Operation;if (id > 0)m.oldVal = JsonHelper.ToStr(oldData);else m.oldVal = null;//新增为空if (Operation != "delete")m.newVal = JsonHelper.ToStr(newData);else m.newVal = null;//删除为空m.createtime = DateTime.Now;//6、写入日志await Access.Add<Model.Data.Logs>(m);}}
}

2、主要业务逻辑实现 :_logs.cs

using Datas;
using Model.Data;
using Model.Tran;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;namespace Logics
{public class _logs{/// <summary>/// 获取所有表/// </summary>public static async Task<List<tTable>> GetTable(){List<vw_Tables> list = await basic.GetModelList<vw_Tables>("");List<tTable> rst = new List<tTable>();int i = 1;list.ForEach(p =>{int index = rst.FindIndex(x => x.name == p.tname);if (index == -1)//表分组{tTable t = new tTable();t.name = p.tname;//表rst.Add(t);}i++;});return rst;}/// <summary>/// 获取所有表、字段/// </summary>public static async Task<List<tTables>> GetTables(){List<vw_Tables> list = await basic.GetModelList<vw_Tables>("");List<tTables> rst = new List<tTables>();int i = 1;list.ForEach(p =>{int index = rst.FindIndex(x => x.name == p.tname);if (index == -1)//表分组{tTables t = new tTables();t.name = p.tname;//表t.sign = p.tname;List<tempModel> temps = new List<tempModel>();//表下列 循环写入list.Where(y => y.tname == t.name).Select(z => z.cname).ToList().ForEach(m =>{tempModel d = new tempModel();d.sign = t.name + "_" + m;d.name = m; ;temps.Add(d);});t.children = temps;//列rst.Add(t);}i++;});return rst;}/// <summary>/// 获取设置/// </summary>public static async Task<string> GetSet(){List<LogSet> list = await basic.GetModelList<LogSet>("");List<string> rst = new List<string>();list.ForEach(p =>{rst.Add(p.tableName + "_" + p.columnsName);});return string.Join(",", rst);}/// <summary>/// 保存设置/// </summary>/// <returns></returns>public static async Task<bool> SaveSet(string SetStr){if (string.IsNullOrEmpty(SetStr)) return false;string[] items = SetStr.Split(',');List<LogSet> list = await basic.GetModelList<LogSet>("");List<string> temps = new List<string>();foreach (var m in list){temps.Add(m.tableName + "_" + m.columnsName);}//校对  '新写入'没有就删除foreach (var m in temps){if (!items.Contains(m)){//删除var n = list.Where(x => x.tableName + "_" + x.columnsName == m).FirstOrDefault();if (n != null)await basic.Delete<LogSet>(n);}}//校对  '数据库'没有就添加foreach (var m in items){if (!temps.Contains(m)){//添加LogSet n = new LogSet();n.tableName = m.Split('_')[0];n.columnsName = m.Split('_')[1];await basic.Add<LogSet>(n);}}return true;}/// <summary>/// 获取某表日志/// </summary>/// <returns></returns>public static async Task<List<Logs>> GetLogsByTab(string TableName){if (string.IsNullOrEmpty(TableName)) return null;List<Logs> list = await basic.GetModelList<Logs>($"tableName='{TableName}'");return list;}}
}

3、主要实体类: log.cs

// 视图vw_Tables对象
public partial class vw_Tables
{public string tname { get; set; }public string cname { get; set; }
}
//表名 对象
public partial class tTable
{public string name { get; set; }
}
//表和对应列对象  母行
public partial class tTables
{public string sign { get; set; }public string name { get; set; }public List<tempModel> children { get; set; }
}
//表和对应列对象 子行
public class tempModel
{public string sign { get; set; }public string name { get; set; }
}

五、ODBC实现

在这里插入图片描述

1、链接数据库实现

 /// <summary>/// 非查询  =>条数  --存储过程 Istran=0/// </summary>/// <returns></returns>public static async Task<object> Edit<T>(SqlConnection connection, CommandType cmdType, List<CmdData> list, bool IsTran, T entity) where T : new(){SqlTransaction trans = null;if (IsTran) //是否开启事务trans = connection.BeginTransaction();try{int result = 0;for (int i = 0; i < list.Count; i++)//批量删除{using (SqlCommand cmd = new SqlCommand(list[i].sql, connection)){if (IsTran) cmd.Transaction = trans;//日志记录LogHelper.main(entity, list[i].sql);//传入实体model、执行sql//日志记录 Endcmd.CommandType = cmdType;if (list[i].parameter != null) cmd.Parameters.AddRange(list[i].parameter);cmd.CommandTimeout = timeout; // 超时定义int rst = await cmd.ExecuteNonQueryAsync();if (rst == 0){  //没执行成功if (IsTran) await trans.RollbackAsync();break;}else result += rst;}if (IsTran) await (trans).CommitAsync();}return result;}catch (Exception e){if (IsTran) await trans.RollbackAsync();return _empty;}}

2、调用实现:Access.cs

(参考.NET8关于ORM的一次思考)

 /// <summary>/// 单增/// </summary>public static async Task<bool> Add<T>(T entity) where T : new(){string SqlStr = SqlBuilder.InsertSQL<T>(entity);//语句SqlParameter[] parameter = SqlParameters.NoID(entity);//参数List<CmdData> list = new List<CmdData>();list.Add(new CmdData(SqlStr, parameter));int rst = (int)await SqlHelper.MyTran_EX(list, "Edit", true,"", entity);return rst > 0;}/// <summary>/// 单改/// </summary>public static async Task<bool> Update<T>(T entity) where T : new(){string SqlStr = SqlBuilder.UpdateSQL<T>(entity);SqlParameter[] parameters = SqlParameters.HaveId(entity);List<CmdData> list = new List<CmdData>();list.Add(new CmdData(SqlStr, parameters));int rst = (int)await SqlHelper.MyTran_EX(list, "Edit", true, "", entity);return rst > 0;}/// <summary>/// 单删/// </summary>public static async Task<bool> Delete<T>(T entity) where T : new(){string SqlStr = SqlBuilder.DeleteSQL<T>(entity);SqlParameter[] parameters = SqlParameters.OnlyID(entity);List<CmdData> list = new List<CmdData>();list.Add(new CmdData(SqlStr, parameters));int rst = (int)await SqlHelper.MyTran_EX(list, "Edit", true, "", entity);return rst > 0;}

3、Json转换类实现: JsonHelper.cs

using Newtonsoft.Json;
using System.IO;public static class JsonHelper
{// 将对象序列化为JSON字符串public static string ToStr(object obj){return JsonConvert.SerializeObject(obj);}// 将JSON字符串反序列化为指定类型的对象public static T ToObj<T>(string json){return JsonConvert.DeserializeObject<T>(json);}// 将对象序列化到文件public static void SerializeToFile(string filePath, object obj){string json = ToStr(obj);File.WriteAllText(filePath, json);}// 从文件读取JSON并反序列化为指定类型的对象public static T DeserializeFromFile<T>(string filePath){string json = File.ReadAllText(filePath);return ToObj<T>(json);}
}

六、前端实现

其实就两页面,我用的element-plus

1、设置页面 LogSet.vue

使用的 el-tree ,先读取表和列,初始化,
再使用 TreeRef.value.setCheckedKeys(res.data.split(',')) 读取设置 并赋值
勾选后 TreeRef.value.getCheckedKeys(true, true) 获取勾选的值
设置格式: Users_password,Users_email

<template><ContentWrap class="pagenow"><h1>设置需要报错日志的字段</h1><el-divider /><el-card><el-treestyle="max-width: 600px":data="Datas"show-checkbox:expand-on-click-node="false"accordionnode-key="sign"ref="TreeRef":props="{label: 'name'}"@node-click="nodeClick"/><template #footer><el-button type="primary" @click="submit"> {{ t('button.ok') }} </el-button></template></el-card></ContentWrap>
</template><script lang="ts" setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
//自写组件
import { ContentWrap } from '@/components/ContentWrap'
//双语
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
//实体
import { TableCols } from '@/api/table/types'
//接口
import { GetTables, GetLogsSets, SaveLogsSets } from '@/api/table/index'//数据初始化
const Datas = ref<TableCols[]>([])
const Checkeds = ref<string[]>([])
//获取表和列
const getdata = async () => {const res = await GetTables() //条数.catch(() => {}).finally(() => {})if (res) {Datas.value = res.datagetSet()}
}getdata()
//获取设置
const getSet = async () => {const res = await GetLogsSets() //条数.catch(() => {}).finally(() => {})if (res) {//字符串转数组,然后赋值if (res.data != null) TreeRef.value.setCheckedKeys(res.data.split(','))}
}const TreeRef = ref()
//点击
const nodeClick = (data: TableCols) => {//测试输出  Users_password,Users_emailconst xx = TreeRef.value.getCheckedKeys(true, true).toString()console.log(xx.value)
}
//提交
const submit = async () => {//勾选值  Users_password,Users_emailconst val = TreeRef.value.getCheckedKeys(true, true).toString()const params = {Str: val}const res = await SaveLogsSets(params) //条数.catch(() => {}).finally(() => {})if (res) {if (res.data) ElMessage.success(t('msg.del1'))getdata()} else ElMessage.error(t('msg.del2'))
}
</script>
// 数据在后端拼凑成这种格式
export type TableCols = {sign:string  //作为id使用name: stringchildren?: TableCols
}:[ {"sign": "Departments","name": "Departments","children": [{"sign": "Departments_id","name": "id"},{"sign": "Departments_name","name": "name"},...其他字段]},...其他表]

2、日志查看页面 Logview.vue

使用 el-tabs 显示表名
内嵌 el-table 显示各表下的日志
外部 el-dialog 实现单条日志 的改动显示

<template><ContentWrap class="pagenow"><el-tabs type="border-card" class="demo-tabs" @tab-click="handleClick"><el-tab-panev-for="(item, index) in TableNames":label="item.name":name="item.name":key="index"><el-table:data="LogsData":border="true":preserve-expanded-content="true"style="width: 98%"><el-table-column :label="t('col.Operations')" prop="operation" /><el-table-column :label="t('col.date')" prop="createtime" /><!-- 操作 --><el-table-column :label="t('col.Operations')" width="250" class="dialog"><template #default="props"><el-button type="" @click="itemDetail(props.row)">{{ t('button.view') }}</el-button></template></el-table-column></el-table></el-tab-pane></el-tabs></ContentWrap><!-- 查看 --><el-dialog v-model="showDetail" :title="t('button.view')" width="700" :maxHeight="350"><el-form class="dialog detailFrom"><el-row :gutter="20"><!-- el-col合计24  --><el-col :span="8"> 列名 </el-col><el-col :span="8"> 修改后 </el-col><el-col :span="8"> 修改前 </el-col></el-row><el-row :gutter="20" v-for="(item, index) in nowdata" :key="index"><!-- el-col合计24  --><el-col :span="8">{{ item.keyName }}</el-col><el-col :span="8">{{ item.newValue }}</el-col><el-col :span="8">{{ item.oldValue }}</el-col></el-row></el-form><template #footer><div class="dialog-footer"><el-button @click="showDetail = false">{{ t('button.close') }}</el-button></div></template></el-dialog>
</template><script lang="ts" setup>
import { ref } from 'vue'
//自写组件
import { ContentWrap } from '@/components/ContentWrap'
//双语
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
//实体
import { Tables, Logs } from '@/api/table/types'
//接口
import { GetTable, GetLogs } from '@/api/table/index'const TableNames = ref<Tables[]>([])
const LogsData = ref<Logs[]>([])//取表名给tab
const getdata = async () => {const res = await GetTable() //条数.catch(() => {}).finally(() => {})if (res) {TableNames.value = res.data}
}
getdata()
//tab切换
const handleClick = async (tab) => {//取该表日志const params = {Str: tab.props.name}const res = await GetLogs(params).catch(() => {}).finally(() => {})if (res) {LogsData.value = res.data}
}
//弹窗
const showDetail = ref(false)interface TableRow {keyName: stringoldValue: anynewValue: any
}
const nowdata = ref<TableRow[]>()
//显示弹窗 
const itemDetail = (row: Logs) => {let json1, json2if (row.newVal != null) json1 = JSON.parse(row.newVal)if (row.oldVal != null) json2 = JSON.parse(row.oldVal)showDetail.value = true// 合并两个对象的所有键,并去重 // 形成 : '列名  旧值  新增'  这样的数据列const keys = Array.from(new Set([...Object.keys(json1), ...Object.keys(json2)]))//数据重组nowdata.value = keys.map((key) => ({keyName: key,newValue: json1[key],oldValue: json2[key]}))
}
</script><style scoped lang="scss">
.demo-tabs > .el-tabs__content {padding: 32px;color: #6b778c;font-size: 32px;font-weight: 600;
}
.demo-tabs .custom-tabs-label .el-icon {vertical-align: middle;
}
.demo-tabs .custom-tabs-label span {vertical-align: middle;margin-left: 4px;
}
.el-row {margin: 0px !important;
}
.el-col {padding: 5px;border: solid #d0d1d3 1px;
}
</style>
http://www.dtcms.com/a/390703.html

相关文章:

  • 从“连不上网”到“玩转路由”:路由器配置与静态路由实战(小白也能轻松掌握)
  • R语言 生物信息如何解读geo数据集的说明,如何知道样本分类, MDA PCa 79(n = 3)n的含义
  • 你的第一个Node.js应用:Hello World
  • 【LVS入门宝典】LVS核心原理与实战:Real Server(后端服务器)高可用配置指南
  • TPAMI 25 ICML 25 Oral | 顶刊顶会双认证!SparseTSF以稀疏性革新长期时序预测!
  • rep()函数在 R 中的用途详解
  • 在Windows中的Docker与WSL2的关系,以及与WSL2中安装的Ubuntu等其它实例的关系
  • 编辑器Vim
  • 数字推理笔记——基础数列
  • 如何使用 FinalShell 连接本地 WSL Ubuntu
  • Node.js 进程生命周期核心笔记
  • 低空网络安全防护核心:管理平台安全体系构建与实践
  • 站内信通知功能websoket+锁+重试机制+多线程
  • Vue 3 <script setup> 语法详解
  • Redis三种服务架构详解:主从复制、哨兵模式与Cluster集群
  • 复习1——IP网络基础
  • MATLAB中借助pdetool 实现有限元求解Possion方程
  • string::c_str()写入导致段错误?const指针的只读特性与正确用法
  • 深度解析 CopyOnWriteArrayList:并发编程中的读写分离利器
  • 直接看 rstudio里面的 rds 数据 无法看到 expr 表达矩阵的详细数据 ,有什么办法呢
  • 【示例】通义千问Qwen大模型解析本地pdf文档,转换成markdown格式文档
  • 企业级容器技术Docker 20250919总结
  • 微信小程序-隐藏自定义 tabbar
  • leetcode15.三数之和
  • 强化学习Gym库的常用API
  • ✅ Python微博舆情分析系统 Flask+SnowNLP情感分析 词云可视化 爬虫大数据 爬虫+机器学习+可视化
  • 红队渗透实战
  • 基于MATLAB的NSCT(非下采样轮廓波变换)实现
  • 创建vue3项目,npm install后,运行报错,已解决
  • 设计模式(C++)详解—外观模式(1)