编辑
2025-11-19
SQLSERVER
00

在工业制造、设备管理等业务场景中,我们经常遇到这样的痛点:设备有上下级关系、工艺流程呈树状结构,如何用SQL优雅地查询出完整的层级关系?

传统的表连接查询面对多层嵌套时显得力不从心,递归存储过程又过于复杂。今天就来揭秘SQL Server的CTE递归查询这个利器,让你轻松处理任何深度的树形数据结构!

无论你是要查询某个设备下的所有子设备,还是要追溯工艺流程的完整路径,本文将通过实战案例,让你彻底掌握CTE递归查询的精髓。


🔍 问题分析:树形数据查询的三大痛点

痛点1:层级深度不确定

在实际业务中,设备层级可能有3层、5层,甚至更深。用传统JOIN方式需要写N个表连接,代码冗长且不灵活。

痛点2:向上向下查询需求并存

既要能从叶子节点向上追溯到根节点,又要能从根节点向下展开所有子节点,单一查询方式无法满足。

痛点3:性能与可读性难以兼顾

递归存储过程性能好但代码复杂,简单查询可读性强但性能差,需要找到平衡点。


🛠️ 解决方案:CTE递归查询的5大实战技巧

📋 数据准备:构建测试环境

首先创建设备层级表和工艺流程表:

SQL
-- 创建设备层级表 CREATE TABLE Equipment ( EquipmentID INT PRIMARY KEY, EquipmentName NVARCHAR(100), ParentID INT, Level INT, CreateDate DATETIME DEFAULT GETDATE() ); -- 插入测试数据 INSERT INTO Equipment VALUES (1, '生产线A', NULL, 1, '2024-01-01'), (2, '工作站A1', 1, 2, '2024-01-02'), (3, '工作站A2', 1, 2, '2024-01-03'), (4, '设备A1-1', 2, 3, '2024-01-04'), (5, '设备A1-2', 2, 3, '2024-01-05'), (6, '传感器A1-1-1', 4, 4, '2024-01-06'), (7, '传感器A1-1-2', 4, 4, '2024-01-07'); -- 创建工艺流程表 CREATE TABLE ProcessFlow ( ProcessID INT PRIMARY KEY, ProcessName NVARCHAR(100), ParentProcessID INT, Sequence INT, Duration INT -- 工序耗时(分钟) ); -- 插入工艺流程数据 INSERT INTO ProcessFlow VALUES (1, '产品制造', NULL, 1, 0), (2, '原料准备', 1, 1, 30), (3, '加工处理', 1, 2, 60), (4, '质量检测', 1, 3, 20), (5, '物料投入', 2, 1, 10), (6, '预处理', 2, 2, 20), (7, '粗加工', 3, 1, 30), (8, '精加工', 3, 2, 30);

🔥 技巧一:向下递归查询子设备

场景:查询某个设备及其所有下级设备

SQL
-- 查询设备ID=1的所有下级设备 WITH EquipmentHierarchy AS ( -- 锚点:找到起始设备 SELECT EquipmentID, EquipmentName, ParentID, Level, 0 as Depth, CAST(EquipmentName AS NVARCHAR(500)) as HierarchyPath FROM Equipment WHERE EquipmentID = 1 UNION ALL -- 递归:找到所有子设备 SELECT e.EquipmentID, e.EquipmentName, e.ParentID, e.Level, eh.Depth + 1, CAST(eh.HierarchyPath + ' -> ' + e.EquipmentName AS NVARCHAR(500)) FROM Equipment e INNER JOIN EquipmentHierarchy eh ON e.ParentID = eh.EquipmentID ) SELECT EquipmentID, REPLICATE(' ', Depth) + EquipmentName as TreeView, Depth, HierarchyPath FROM EquipmentHierarchy ORDER BY Depth, EquipmentID;

image.png

编辑
2025-11-18
C#
00

在C#图形编程领域,SkiaSharp作为.NET平台上的强大2D图形库,为开发者提供了丰富的图形处理能力。而旋转变换作为基础且常用的图形操作,在UI设计、动画效果和图像处理中扮演着重要角色。本文将深入探讨SkiaSharp中的旋转变换原理及实现方法,通过详细的代码示例帮助你掌握这一技能。

SkiaSharp简介

SkiaSharp是Google Skia图形引擎的C#/.NET绑定,提供跨平台的2D图形API。它被广泛应用于Xamarin.Forms、.NET MAUI、WPF等平台的图形渲染,支持矢量图形、文本渲染、图像处理等功能。

使用SkiaSharp需要安装以下NuGet包:

C#
Install-Package SkiaSharp Install-Package SkiaSharp.Views.WindowsForms

旋转变换基础知识

什么是旋转变换?

旋转变换是指将图形或图像围绕某个点按照指定角度进行旋转的过程。在SkiaSharp中,旋转通常围绕指定的原点进行,角度以顺时针方向为正。

数学原理

旋转变换在数学上通过旋转矩阵实现,这一操作可表示为:

Python
x' = x·cos(θ) - y·sin(θ) y' = x·sin(θ) + y·cos(θ)

其中,(x, y)是原始坐标,(x', y')是旋转后的坐标,θ是旋转角度。

SkiaSharp中实现旋转变换

基本方法:使用Canvas.RotateDegrees()

最直接的旋转方法是使用SKCanvasRotateDegrees()RotateRadians()方法:

C#
using SkiaSharp.Views.Desktop; using SkiaSharp; namespace AppRotate { public partial class Form1 : Form { public Form1() { InitializeComponent(); SKControl skControl = new SKControl(); skControl.Dock = DockStyle.Fill; skControl.PaintSurface += OnPaintSurface; Controls.Add(skControl); } private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e) { // 获取画布 SKCanvas canvas = e.Surface.Canvas; // 清除背景 canvas.Clear(SKColors.White); // 创建画笔 using SKPaint paint = new SKPaint { Color = SKColors.Blue, StrokeWidth = 5, IsAntialias = true, Style = SKPaintStyle.Stroke }; // 保存当前画布状态 canvas.Save(); // 执行旋转变换 (45度,围绕点(100, 100)) canvas.Translate(100, 100); // 移动原点到旋转中心 canvas.RotateDegrees(45); // 旋转45度 canvas.Translate(-100, -100); // 移回原来位置 // 绘制一个矩形 canvas.DrawRect(50, 50, 100, 100, paint); // 恢复画布状态 canvas.Restore(); // 使用不同颜色绘制一个未旋转的矩形作为对比 using SKPaint paint2 = new SKPaint { Color = SKColors.Red, StrokeWidth = 5, IsAntialias = true, Style = SKPaintStyle.Stroke }; canvas.DrawRect(200, 50, 100, 100, paint2); } } }

image.png

编辑
2025-11-16
C#
00

还在为复杂的数据列表展示而头疼吗?每次需要显示表格数据时都要重新造轮子?作为WinForms开发中的核心控件,ListView不仅能优雅地处理各种列表展示需求,更是构建专业用户界面的基石。

许多C#开发者在使用ListView时往往停留在基础应用层面,错失了它的强大功能。本文将从实战角度出发,带你深度掌握ListView的高级特性,让你的应用界面瞬间专业起来!

🎯 ListView核心能力解析

📊 五种视图模式全攻略

ListView提供了5种灵活的显示模式,每种都有其独特的应用场景:

  • LargeIcon:适合文件管理器、图片库等场景
  • SmallIcon:适合紧凑型列表显示
  • List:经典的垂直列表布局
  • Details:表格形式,企业应用首选
  • Tile:现代化的磁贴显示
编辑
2025-11-15
C#
00

在工业4.0的浪潮下,越来越多的C#开发者需要构建工业监控系统。但很多人在项目中遇到这样的困扰:界面逻辑和业务逻辑耦合严重,代码维护困难,测试覆盖率低。今天,我将通过一个完整的工业设备监控系统案例,带你掌握MVP架构模式的精髓,让你的C#项目结构更清晰,代码更易维护!

本文将解决三个核心问题:如何设计清晰的MVP架构、如何实现实时数据更新、如何处理异步操作中的异常。不过这个模式在Winform中我应用极少,我记得的也就是以前写过一个通讯工具。

本文将解决三个核心问题:如何设计清晰的MVP架构、如何实现实时数据更新、如何处理异步操作中的异常。不过这个模式在Winform中我应用极少,我记得的也就是以前写过一个通讯工具。

🎯 问题分析:传统开发模式的痛点

在传统的WinForms开发中,我们经常会遇到以下问题:

  1. 代码耦合度高:界面逻辑、业务逻辑、数据访问混杂在一起
  2. 难以测试:UI控件和业务逻辑绑定,单元测试困难
  3. 维护成本高:需求变更时,涉及多个层面的修改

MVP模式正是解决这些问题的利器!

🔥 解决方案一:构建清晰的MVP架构

核心思想

MVP模式将应用程序分为三个核心组件:

  • Model(模型):数据和业务逻辑
  • View(视图):用户界面
  • Presenter(展示器):连接Model和View,处理用户交互

MVP架构组件关系图

image.png

编辑
2025-11-14
C#
00

你是否还在为项目中频繁的对象序列化操作拖慢系统性能而头疼?传统的JSON序列化在高并发场景下捉襟见肘,Protobuf配置复杂让人望而却步?今天我要为大家介绍一个C#序列化领域的"性能怪兽"——MemoryPack

作为由微软MVP Yoshifumi Kawai开发的新一代序列化库,MemoryPack在保持极简API的同时,性能竟然比System.Text.Json快10-50倍!更令人兴奋的是,它支持版本容错、循环引用处理,还能与Unity完美兼容。

本文将通过实战案例,手把手教你如何在项目中应用MemoryPack,让你的应用性能实现质的飞跃!

🔍 为什么选择MemoryPack?

传统序列化方案的痛点

在日常C#开发中,我们经常遇到这些序列化难题:

  • 性能瓶颈:System.Text.Json在大对象序列化时性能不理想
  • 内存开销:频繁的GC压力影响应用响应速度
  • 配置复杂:Protobuf需要编写.proto文件,学习成本高
  • 版本兼容:字段变更时容易出现反序列化异常

MemoryPack的核心优势

C#
// 传统JSON序列化 var json = JsonSerializer.Serialize(data); var bytes = Encoding.UTF8.GetBytes(json); // MemoryPack:一行代码搞定 var bytes = MemoryPackSerializer.Serialize(data);

三大核心优势

  • 极致性能:零分配的二进制格式,比JSON快10-50倍
  • 🛡️ 版本容错:自动处理字段增减,向后兼容无忧
  • 🎯 零配置:Source Generator自动生成代码,开箱即用

📦 快速上手:第一个MemoryPack程序

安装配置

Bash
dotnet add package MemoryPack

基础示例

C#
using MemoryPack; namespace AppMemoryPack { [MemoryPackable] public partial class UserInfo { public int Id { get; set; } public string Name { get; set; } public DateTime CreateTime { get; set; } public List<string> Tags { get; set; } } class Program { static void Main() { var user = new UserInfo { Id = 1001, Name = "张三", CreateTime = DateTime.Now, Tags = new List<string> { "开发者", "技术爱好者" } }; // 序列化:对象 → 字节数组 byte[] bytes = MemoryPackSerializer.Serialize(user); Console.WriteLine($"序列化后大小: {bytes.Length} bytes"); // 反序列化:字节数组 → 对象 var deserializedUser = MemoryPackSerializer.Deserialize<UserInfo>(bytes); Console.WriteLine($"姓名: {deserializedUser.Name}, ID: {deserializedUser.Id}"); } } }

image.png