首页/新闻资讯/正文详情

sql-server-samples 中的 SmoSamples:基于单元测试的 SMO 性能优化与网络度量实战

发布时间:2026/9/25 2:40:54 来源:云帆数科 栏目:资讯中心
sql-server-samples 中的 SmoSamples:基于单元测试的 SMO 性能优化与网络度量实战
示例工程数据库教程后端【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址https://gitcode.com/gh_mirrors/sq/sql-server-samples点击查看免费下载本文围绕 sql-server-samples 仓库中samples/features/sql-management-objects下的 SmoSamples 样例展开,该样例是一个用 C# 编写的单元测试工程,专门演示 SQL Server Management Objects(SMO)框架的核心特性,并帮助开发者优化其 SMO 应用的性能。读完后,你将理解如何用SetDefaultInitFields减少集合枚举时的往返查询、如何用本地 TCP 代理精确度量 SMO 应用的查询次数与网络流量,以及 URN 转义、事件订阅等 SMO 开发中的关键实践。样例定位:面向 SMO 应用性能优化的单元测试工程SmoSamples 的定位在 README.md 中开宗明义:This unit test project is meant to demonstrate features of the Sql Management Objects framework and to help developers optimize performance of their SMO-based applications.也就是说,它不是一个功能演示型应用,而是一个可运行的最佳实践库:每个单元测试都演示 SMO 应用开发的某个具体方面,并针对一个真实可连接的 SQL Server 实例执行,同时通过度量框架量化优化前后的差异。样例的关键信息如下(与原文档保持一致):适用对象:SQL Server 2016(或更高版本)、Azure SQL Database、Azure SQL Data Warehouse关键特性:针对可运行 SQL Server 实例的 SMO 特性演示,附带单元测试与 Docker 文件编程语言:C#整个目录的组织方式清晰体现了测试 环境准备两条主线:samples/features/sql-management-objects/ ├── README.md # 本文的主体文档 ├── runtests.sh # Linux/macOS 一键运行脚本(Docker) ├── runtests.cmd # Windows 一键运行脚本(Docker) ├── prep/ # Docker 环境准备 │ ├── dockerfile # 基于 mssql 2017 镜像,下载并恢复 WideWorldImporters │ ├── entrypoint.sh # 启动 sqlservr 并执行备份恢复 │ ├── restore.sh # 等待 SQL Server 就绪后执行 sqlcmd 恢复 │ └── restore.sql # RESTORE DATABASE WideWorldImporters ... └── src/ # 被测工程(单元测试 度量基础设施) ├── SmoSamples.csproj ├── SmoSamples.sln ├── localhost.runsettings ├── CollectionSamples.cs # 集合高效遍历测试 ├── Urn.cs # URN 转义测试 ├── ConnectionHelpers.cs # 连接构造与测试上下文辅助 ├── ConnectionMetrics.cs # 网络/查询度量 └── GenericSqlProxy.cs # 本地内存 TCP 代理前提条件按 README.md 的 Before you begin 一节,运行该样例需要:装有完整 WideWorldImporters 样例数据库的 SQL Server 2016(或更高版本),或 Azure SQL Database;或Docker至少 dotnet 2.2 SDK,或 Visual Studio 2017从 SmoSamples.csproj 可以确认实际的依赖版本,这也是理解运行环境约束的关键依据:目标框架为netcoreapp2.1;SMO 包引用为Microsoft.SqlServer.SqlManagementObjects150.18118.0(150.x 对应 SQL Server 2016/2017 时代的管理对象版本);System.Data.SqlClient4.8.6;测试框架采用 MSTest(MSTest.TestAdapter/MSTest.TestFramework1.4.0)提供[TestClass]、[TestMethod]与TestContext,同时引入NUnit3.11.0 的Assert(源码中以using Assert NUnit.Framework.Assert;的方式混用)。运行方式一:Docker 一键脚本如果本地已有独立的 SQL Server 2016 或 Azure SQL Database,可以跳过本节;否则最简单的方式是使用 runtests.sh(Linux/macOS)或 runtests.cmd(Windows),两个脚本的完整流程一致,以 sh 版为例:pwdPwd$RANDOM # 生成随机 SA 密码 echo Building the SQL Linux Docker container docker pull mcr.microsoft.com/mssql/server:2017-latest docker build -t sqllinux prep # 基于 prep/dockerfile 构建 echo Running the SQL linux docker image docker run -e ACCEPT_EULAY -e SA_PASSWORD$pwd -e MSSQL_SA_PASSWORD$pwd \ -h sqlserver --name sqlserver -p:1433:1433 -d --rm sqllinux echo Waiting 2 minutes for SQL server to restore WideWorldImporters sleep 120 echo running tests against SQL 2017 database WideWorldImporters export TEST_PASSWORD$pwd # 传给 runsettings 中的 [password] 占位符 dotnet publish src dotnet vstest src/bin/Debug/netcoreapp2.1/SmoSamples.dll \ --logger:console --Settings:src/localhost.runsettings echo Terminating docker container docker kill sqlserver几个值得注意的细节:prep/目录下的 dockerfile 基于mcr.microsoft.com/mssql/server:2017-latest镜像,构建阶段就用wget下载 WideWorldImporters 的完整备份WideWorldImporters-Full.bak并放入镜像,因此镜像本身就是带库的;entrypoint.sh 采用后台启动 恢复的方式:先以后台启动/opt/mssql/bin/sqlservr,再执行 restore.sh(sleep 35s等待实例就绪后用sqlcmd -S . -U sa -P $SA_PASSWORD执行恢复脚本),最后tail -f /dev/null保持容器存活;restore.sql 中的RESTORE DATABASE WideWorldImporters FROM DISK /tmp/backup/WideWorldImporters-Full.bak用WITH MOVE将WWI_Primary、WWI_Userdata、WWI_Log、WWI_InMemory_Data_1四个文件组分别落盘到/var/opt/mssql/data/下,这也是 WideWorldImporters 包含内存优化数据库文件这一特点所要求的;脚本把随机 SA 密码导出为TEST_PASSWORD环境变量,这正是后面.runsettings连接串中占位符的取值来源(Windows 版 runtests.cmd 用setlocal/endlocal将密码限制在当前脚本作用域内,避免污染用户环境)。运行方式二:连接已有的 SQL Server 实例如果目标是独立的 SQL Server 实例或 Azure SQL Database,README 给出的做法是:创建一份自己的.runsettings文件,然后使用 Visual Studio 或dotnet vstest运行单元测试。仓库内置的 localhost.runsettings 就是模板:?xml version1.0 encodingutf-8? RunSettings TestRunParameters Parameter nameconnectionString valueserverlocalhost;User Idsa;Password[password];Timeout60 / Parameter nametestDatabase valueWideWorldImporters / Parameter nameproxyPort value0 / /TestRunParameters /RunSettings这三个参数的含义,需要结合 ConnectionHelpers.cs 与 ConnectionMetrics.cs 才能说清:参数消费位置说明connectionStringTestContext.GetConnectionString()SMO 连接用的 ADO.NET 连接串。支持[hostname]、[username]、[password]、[database]四个占位符,运行时分别用环境变量TEST_HOSTNAME、TEST_USERNAME、TEST_PASSWORD、TEST_DATABASE替换。这就是 runtests 脚本只导出TEST_PASSWORD即可跑通的机制testDatabaseTestContext.GetTestDatabaseName()测试所用数据库名,优先取环境变量TEST_DATABASE,取不到才回落到该参数,两者都缺时断言失败proxyPortConnectionMetrics.SetupMeasuredConnection本地代理监听端口,0表示由操作系统分配随机端口。源码注释特别提示:在容器内运行时可能需要在 dockerfile 中暴露指定端口GetTestConnection()还会根据ConnectionType枚举(Default/Integrated/SqlAuth/SqlConnection)构造不同认证方式的ServerConnection,SQL 认证缺用户名或密码时会抛出ArgumentException,避免静默降级为错误连接。特性一:集合的高效遍历(SetDefaultInitFields)README 的 Sample details 列出的第一个特性领域是Efficient use of collections(高效使用集合),对应测试类 CollectionSamples.cs。它演示的正是 SMO 应用最常见、也最容易被忽视的性能陷阱:SMO 对象属性是延迟初始化的,首次访问才会触发一次元数据查询。测试方法Collection_iteration_is_faster_with_SetDefaultInitFields的做法:建立带度量的连接后,先做一次朴素遍历:var server new Management.Smo.Server(connectionMetrics.ServerConnection); var database server.Databases[TestContext.GetTestDatabaseName()]; connectionMetrics.Reset(); foreach (Table table in database.Tables) { // 访问 FileGroup 会触发一次额外的查询 Trace.TraceInformation( $Unoptimized table Name: {table.Name}\tSchema:{table.Schema}\tFileGroup:{table.FileGroup}); }SMO 默认只初始化集合枚举所需的最小字段集,循环体内每次访问table.FileGroup都会对单张表发起一次查询——即俗称的 N1 问题。调用SetDefaultInitFields预取所需属性后,再做同样的遍历:server.SetDefaultInitFields(typeof(Table), Name, Schema, FileGroup); database.Tables.Refresh(); // 让集合按新的初始化字段重新加载 foreach (Table table in database.Tables) { Trace.TraceInformation( $Optimized table Name: {table.Name}\tSchema:{table.Schema}\tFileGroup:{table.FileGroup}); }最后用 NUnit 断言优化后的度量严格更优:Assert.That(optimizedMetrics.BytesRead, Is.LessThan(unoptimizedMetrics.BytesRead)); Assert.That(optimizedMetrics.BytesSent, Is.LessThan(unoptimizedMetrics.BytesSent)); Assert.That(optimizedMetrics.ConnectionCount, Is.AtMost(unoptimizedMetrics.ConnectionCount)); Assert.That(optimizedMetrics.QueryCount, Is.LessThan(unoptimizedMetrics.QueryCount));这里有两个源码级细节值得学习:SetDefaultInitFields设置的是该 Server 对象上 Table 类型的默认初始化字段集,所以调用之后必须database.Tables.Refresh()让已加载的集合按新规则重新初始化,否则旧集合仍持有旧的加载状态;测试没有硬编码任何行数或字节数阈值,而是对同一环境做前后对照断言,这让测试在任意规模的 WideWorldImporters 恢复结果上都成立——这正是用单元测试固化性能优化的思路。特性二:SQL 查询捕获与网络度量(StatementExecuted 本地 TCP 代理)README 列出的Sql query capture与Events两个特性领域,在源码中由 ConnectionMetrics.cs 和 GenericSqlProxy.cs 共同实现,它们构成了一套可复用的网络层探针。ConnectionMetrics维护四个计数器,全部来自事件的回调:计数器事件来源触发时机QueryCountServerConnection.StatementExecutedSMO 每向服务端执行一条语句(T-SQL 或 DDL)时触发,直接反映 SMO 产生的往返查询数BytesSent代理OnWriteHost代理把客户端数据转发给 SQL Server 之前BytesRead代理OnWriteClient代理把服务端响应转发回客户端之前ConnectionCount代理OnConnect每接受一个新 TCP 连接SetupMeasuredConnection的组装过程(见 ConnectionMetrics.cs 第 68–84 行)是理解事件 代理协作的关键:public static ConnectionMetrics SetupMeasuredConnection(TestContext testContext, int latencyPaddingMs 0) { var connectionString testContext.GetConnectionString(); var proxy new GenericSqlProxy(connectionString); if (latencyPaddingMs 0) { // 在每次向客户端写数据前注入固定延迟,模拟高延迟网络 proxy.OnWriteClient (o,e) DelayWrite(latencyPaddingMs, e); } var port testContext.Properties.ContainsKey(proxyPort) ? Convert.ToInt32(testContext.Properties[proxyPort]) : 0; var sqlConnection new SqlConnection(proxy.Initialize(port)); var serverConnection new ServerConnection(sqlConnection); return new ConnectionMetrics(serverConnection, proxy); }注意ServerConnection是直接建立在SqlConnection之上的——即让 SMO复用一条已经指向代理的 ADO.NET 连接,因此 SMO 发出的所有 TDS 流量都会经过代理,字节数统计才能闭环。latencyPaddingMs参数(默认 0)则通过OnWriteClient事件在每次响应回传前Thread.Sleep,用于人为放大网络延迟,观察优化策略在高延迟环境下的收益。支撑这一切的 GenericSqlProxy 是一个纯内存实现的 TCP 转发代理,源码结构上可以归纳为:Initialize(int localPort 0)在回环地址上启动TcpListener(port0时随机分配),把原始连接串的DataSource解析出目标主机/端口(默认 1433),然后返回一条指向tcp:127.0.0.1,代理端口的新连接串;每接受一个客户端连接,就同时建立到真实 SQL Server 的上游连接,并分别启动ForwardToSql(客户端→服务端)与ForwardToClient(服务端→客户端)两个转发循环;两个转发循环在每次写缓冲区之前触发OnWriteHost/OnWriteClient事件,事件参数StreamWriteEventArgs携带本次写入的BytesWritten,这正是BytesSent/BytesRead的统计口径;缓冲大小固定为 128 KB,源码注释说明原因是足够容纳大多数单条响应,避免过度注入延迟;Dispose()通过CancellationTokenSource取消接受循环并停止监听器,测试侧的ConnectionMetrics.Dispose()同时解绑四个事件、释放底层SqlConnection与代理,保证测试之间互不串扰。这套事件订阅 透明代理的组合拳,展示了 SMO 开发者在不修改被测代码的前提下,如何精确量化一个 SMO 操作序列的真实网络开销——这比只看执行时间更能定位问题,因为本地环境的时间差异可能完全掩盖查询次数差异,而在高延迟的 Azure 场景中则会被成倍放大。特性三:URN 与转义README 列出的URNs领域对应 Urn.cs 中的UrnSamples测试类,它演示了 SMO 的 URN(URN,用于定位对象的路径式标识)三个要点:URN 值包含引号时必须转义。测试创建一个名为NameWithQuotes的表,验证table.Urn.GetNameForType(Table.UrnSuffix)会原样返回带引号的名称;随后证明:直接用未转义的名称拼 URN 调用server.GetSmoObject会抛出FailedOperationException,而用Urn.EscapeString(NameWithQuotes)转义后则能正确取回对象:table (Table)server.GetSmoObject( $Server/Database[Name{database.Name}]/Table[Name{Urn.EscapeString(NameWithQuotes)}]); Assert.That(table.Name, Is.EqualTo(NameWithQuotes), Table with escaped name);这对把用户输入或对象名拼进 URN的管理类工具是必备的安全与正确性实践。Server 对象的 URN Name 与实例真名一致:server.Urn.Value应等于ServerNameconnection.TrueName,测试借此校验 URN 语义与ServerConnection.TrueName(解析后的真实实例名,而非连接串里写的别名)的对应关系。URN 的类型是路径的最后一节:new Urn(Server[Nameserver]/Database[Namedatabase]/Table[Nametable]).Type应等于Table.UrnSuffix。另外,UrnSamples依赖的ExecuteWithDbDrop辅助方法(位于 ConnectionHelpers.cs 第 117–140 行)也值得一提:它用TestName Random.Next()生成唯一库名,Create()后执行被测逻辑,finally中尽力Drop()(失败只记 Trace 不抛出),保证每个 URN 测试都在干净的临时库上进行,互不污染。其余特性领域与工程结构说明README 的 Sample details 共列出五个特性领域:1) 集合高效使用;2) SQL 查询捕获;3) 事件;4) URN;5) 脚本生成(Script generation)。从当前提交到仓库的源码结构看,前四个领域在src/下都有明确的对应实现(集合与 URN 为独立测试类,查询捕获与事件体现为ConnectionMetrics/GenericSqlProxy度量基础设施,且其本身即被各测试类复用);第五个领域脚本生成目前未见独立的测试类,属于 README 声明的特性范围,阅读时建议以 README 为纲、以现有源码为实证。依赖与延伸阅读SMO 的官方 NuGet 包为Microsoft.SqlServer.SqlManagementObjects,本仓库锁定的版本是 150.18118.0(见 SmoSamples.csproj 第 23 行);如需升级,注意大版本号与 SQL Server 版本代的对应关系;WideWorldImporters 完整备份是测试数据基础,Docker 路线由 prep/dockerfile 在构建期下载,自行部署时可使用 WideWorldImporters 官方发布渠道获取WideWorldImporters-Full.bak并执行与 restore.sql 等价的RESTORE ... WITH MOVE语句(四个文件组缺一不可,否则会因找不到WWI_InMemory_Data_1报错);若要在 CI 中复用本样例的度量手法,可参考ConnectionMetrics.SetupMeasuredConnection的组装模式:先建代理、再建SqlConnection、最后用该SqlConnection构造ServerConnection,三步顺序不可颠倒。小结SmoSamples 样例的价值在于可度量:它把 SMO 开发中三类高频问题——集合延迟初始化导致的 N1 查询、对服务端语句数量不可见、URN 拼接不转义——各自转化为一个可在真实实例上运行、带断言的单元测试,并提供了SetDefaultInitFields、本地 TCP 代理 StatementExecuted事件、Urn.EscapeString三个可直接迁移到自己 SMO 应用中的解法。配合 runtests.sh/runtests.cmd 与 prep 目录下的 Docker 文件,整套环境可以在几分钟内从零搭起,是学习 SMO 性能工程化的现成参照。赞分享示例工程数据库教程后端【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址https://gitcode.com/gh_mirrors/sq/sql-server-samples点击查看免费下载相关推荐UAAppReviewManager源码解析iOS应用评分弹窗的智能实现原理UAAppReviewManager源码解析iOS应用评分弹窗的智能实现原理 你是否曾经想过为什么优秀的iOS应用总能恰好在最合适的时机请求用户评分SQL Server 容器化实战基于 docker-compose 的复制拓扑与 tSQLt GitHub Actions 单元测试自动化SQL Server 容器化实战基于 docker compose 的复制拓扑与 tSQLt GitHub Actions 单元测试自动化 导读 本篇文章示例工程数据库教程后端SQL Server 批量数据导入导出实战基于 sql-server-samples 的 BCP 与 BULK INSERT 订单数据装载指南SQL Server 批量数据导入导出实战基于 sql server samples 的 BCP 与 BULK INSERT 订单数据装载指南 导读 本文以示例工程数据库教程后端上一篇React-Markdown项目中图片渲染被段落组件覆盖的问题解析下一篇PythonOCC-core中禁用STEP文件导出日志的方法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

Tftpd64从安装到首次传文件:10分钟搭好Windows轻量级TFTP服务器教程
Tftpd64从安装到首次传文件:10分钟搭好Windows轻量级TFTP服务器教程

Tftpd64从安装到首次传文件:10分钟搭好Windows轻量级TFTP服务器教程 【免费下载链接】tftpd64 The working repository of the famous TFTP server. 项目地址: https://gitcode.com/gh_mirrors/tf/tftpd64 Tftpd64 是一款轻量级、多线程的 Windows 服务器软件… · 2026/9/25 2:40:54

Spark Link 插件测试套件深度解析:从测试架构设计到源码级验证
Spark Link 插件测试套件深度解析:从测试架构设计到源码级验证

人工智能AI AgentAgent 编排RPA后端前端企业应用 【免费下载链接】astron-agent Enterprise-grade, commercial-friendly agentic workflow platform for building next-generation SuperAgents. 项目地址: https://gitcode.com/gh_mirrors/as/astron-agent 点击查看… · 2026/9/25 2:40:48

Amazon RDS 的 Java 2.x SDK 实战:从 Hello RDS 到完整数据库生命周期场景
Amazon RDS 的 Java 2.x SDK 实战:从 Hello RDS 到完整数据库生命周期场景

示例工程教程后端 【免费下载链接】aws-doc-sdk-examples Welcome to the AWS Code Examples Repository. This repo contains code examples used in the AWS documentation, AWS SDK Developer Guides, and more. For more information, see the Readme.md file below. 项目地… · 2026/9/25 2:40:48

Quip Node Manager 架构解析:Tauri v2 + Rust 如何编排 Docker 节点堆栈
Quip Node Manager 架构解析:Tauri v2 + Rust 如何编排 Docker 节点堆栈

Quip Node Manager 架构解析:Tauri v2 Rust 如何编排 Docker 节点堆栈 【免费下载链接】quip-node-manager A simple GUI client to manage a Quip Network node 项目地址: https://gitcode.com/gh_mirrors/qu/quip-node-manager Quip Node Manager 是一款基… · 2026/9/25 3:04:48

链表核心操作实战:单链表与双向链表的插入删除及逆序详解
链表核心操作实战:单链表与双向链表的插入删除及逆序详解

数组和链表哪个更好?这是我在带学生做数据结构实验时,被问得最多的问题。单链表和双向链表作为数据结构课程里的绝对主角,不仅考研408年年考,期末试卷次次出,更是后续栈、队列、图论的基础。我见过太多同学拿着严蔚敏的… · 2026/9/25 3:04:47

10个必看的AI Agent Harness开源项目:Awesome Harness Engineering 38个运行时与参考实现实战指南
10个必看的AI Agent Harness开源项目:Awesome Harness Engineering 38个运行时与参考实现实战指南

10个必看的AI Agent Harness开源项目:Awesome Harness Engineering 38个运行时与参考实现实战指南 【免费下载链接】awesome-harness-engineering 🛠️ Awesome tools & guides for harness engineering. 项目地址: https://gitcode.com/gh_mirror… · 2026/9/25 3:04:47

WeChatMsg 微信聊天记录导出完整教程:备份、检索、年度报告一次理清
WeChatMsg 微信聊天记录导出完整教程:备份、检索、年度报告一次理清

WeChatMsg 微信聊天记录导出完整教程:备份、检索、年度报告一次理清 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Tren… · 2026/9/25 3:04:41

NgRx ComponentStore 初始化机制详解:构造函数初始化与惰性初始化(Lazy Initialization)
NgRx ComponentStore 初始化机制详解:构造函数初始化与惰性初始化(Lazy Initialization)

前端状态管理 【免费下载链接】platform Reactive State for Angular 项目地址: https://gitcode.com/gh_mirrors/pl/platform 点击查看 免费下载 本文基于 NgRx platform 仓库中 ComponentStore 指南的 Initialization 章节展开,系统讲解 ngrx/compone… · 2026/9/25 3:04:41

索引详解:从B+树到倒排索引,覆盖数据库、搜索与流媒体
索引详解:从B+树到倒排索引,覆盖数据库、搜索与流媒体

“索引”这个词,在技术圈里出现得远比你想象中频繁。一说数据库,MySQL、PostgreSQL、Oracle 里靠索引把查询从几秒压到几毫秒;聊到搜索引擎,又有倒排索引这个词;前端表格排序时行号错乱、HLS 视频流的 m3u8 切片清单、… · 2026/9/25 3:04:41

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码