news 2026/10/22 15:23:47

C#连接Oracle的正确姿势:Oracle.ManagedDataAccess实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#连接Oracle的正确姿势:Oracle.ManagedDataAccess实战指南

简介:本资源是一份面向C#开发者、特别是.NET平台数据库应用工程师的Oracle数据库接入实战教程,聚焦于轻量、免客户端安装的Oracle.ManagedDataAccess驱动集成方案,解决传统ODP.NET依赖本地Oracle客户端的部署痛点。压缩包共297个文件,含85个核心dll(含Oracle.ManagedDataAccess及配套依赖)、50个xml(配置与文档说明)、36个txt(使用指南与连接参数示例)、8个cs源码文件(含已封装好的OracleHelper操作类)以及sln工程文件和csproj项目配置,整体11.2MB,结构完整、开箱即用。已有782人学习下载,所有代码全开源,已在多个实际项目中验证稳定可用。读者可直接复用OracleHelper类实现查询、增删改、事务控制等常见操作,快速构建C#与Oracle的连接层;同时通过源码级调试与配置注释,深入理解连接字符串构造、连接池管理及异常处理机制,显著降低企业级Oracle集成门槛。

1. C# 连接 Oracle 的快速方法:为什么不用 ODP.NET Classic,而选 Oracle.ManagedDataAccess?

你刚接手一个老系统迁移任务:把原来用System.Data.OracleClient(已废弃)或Oracle.DataAccess(ODP.NET Unmanaged,依赖 Oracle Client 安装)的 C# 项目,迁到新服务器——结果发现目标机没装 Oracle Instant Client,管理员死活不给开权限装客户端,连tnsnames.ora都不让放;更糟的是,CI/CD 流水线里每次构建都因 Oracle 环境缺失失败。这时候,“C# 连接 Oracle 的快速方法”就不是一句口号,而是救命绳。

Oracle.ManagedDataAccess(简称 ODP.NET Managed Driver)正是这个场景下的唯一可靠解:它纯 .NET 实现、零本地依赖、NuGet 一键安装、支持 .NET Framework 4.6.1+ 和 .NET Core / .NET 5+ 全平台,且官方持续维护(截至 2024 年最新版 23.5)。它不是“替代品”,而是 Oracle 官方主推的现代驱动——所有新项目、容器化部署、Azure Functions、Linux Docker 容器,都必须走这条路。本教程不讲理论堆砌,只聚焦「从零配置到稳定查询」的最小可行路径:如何在 5 分钟内让SqlConnection风格的代码跑通 Oracle,避开 DLL 冲突、连接池泄漏、字符集错乱这三大高频翻车点,并给出生产级连接字符串参数调优清单。适合 C# 中级开发者、上位机工程师、ERP 接口开发人员,尤其适合正在被 Oracle 监听服务无法启动、tnsnames 配置失败、ORA-12154 折磨的你。


2. 用 Oracle.ManagedDataAccess 在本地跑通第一个查询:三步完成,不装任何 Oracle 客户端

2.1 创建项目并安装 NuGet 包:认准官方源,拒绝山寨包

新建一个 .NET 6 控制台项目(或 .NET Framework 4.7.2+),不要搜索 “oracle client” 或 “odp.net”,直接在 Package Manager Console 执行:

Install-Package Oracle.ManagedDataAccess -Version 23.5.0

提示:务必指定-Version。当前(2024Q2)最新稳定版是23.5.0(对应 Oracle Database 23c / 19c / 12c 全兼容)。避免用Install-Package Oracle.ManagedDataAccess不带版本——它可能拉取过时的19.12.0,该版本在 .NET 6+ 下存在 TLS 1.2 默认禁用导致连接超时的玄学问题。若用 .NET 8,建议锁死23.5.0或23.6.0(发布后),它们已内置对 TLS 1.3 的显式支持。

安装后,检查packages.config(Framework)或.csproj(Core/5+)中是否出现:

<PackageReference Include="Oracle.ManagedDataAccess" Version="23.5.0" />

关键逻辑说明:

  • Oracle.ManagedDataAccess.dll是纯托管程序集,无oci.dll、oraociei19.dll等非托管依赖;
  • 它通过内置 TCP/IP 协议栈直连 Oracle 监听器,绕过sqlnet.ora和tnsnames.ora解析(但支持它们);
  • 所有加密、字符集转换、LOB 处理均在托管层完成,调试时可直接 F11 进源码(GitHub 开源地址:oracle/manageddataaccess)。

2.2 写最简连接代码:用 Easy Connect 字符串跳过 tnsnames 配置

假设你的 Oracle 数据库监听地址是192.168.1.100:1521/orclpdb(即 host:port/service_name),不要去配tnsnames.ora,直接用 Easy Connect 格式:

using Oracle.ManagedDataAccess.Client; string connectionString = "User Id=hr;Password=hr123;Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.1.100)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orclpdb)));"; using (var conn = new OracleConnection(connectionString)) { conn.Open(); Console.WriteLine($"连接成功!数据库版本:{conn.ServerVersion}"); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT COUNT(*) FROM employees"; var count = (int)cmd.ExecuteScalar(); Console.WriteLine($"employees 表共 {count} 条记录"); } }

参数说明与避坑点:

  • User Id和Password是明文账号密码(开发环境可接受,生产必须用 Wallet 或外部认证);
  • Data Source是核心:(DESCRIPTION=...)是 Oracle 标准描述符语法,SERVICE_NAME(非 SID)必须准确——查SELECT value FROM v$parameter WHERE name='service_names';确认;
  • 若用SID(如ORCL),需改写为(CONNECT_DATA=(SID=ORCL)),但 12c+ 推荐一律用SERVICE_NAME;
  • 端口1521是默认监听端,若改过(如1522),必须同步修改;
  • 连接字符串中不能有空格(除括号内分隔符外),否则OracleConnectionStringBuilder会解析失败。

2.3 验证连接可用性:用 OracleConnectionStringBuilder 构建并校验

硬编码连接字符串易出错。推荐用OracleConnectionStringBuilder动态生成,自动转义特殊字符:

var builder = new OracleConnectionStringBuilder { DataSource = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.1.100)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orclpdb)))", UserID = "hr", Password = "hr123", Connection Timeout = 15, // 单位秒,避免无限等待 Pooling = true, // 必开连接池,下文详述 Min Pool Size = 5, Max Pool Size = 50 }; string connStr = builder.ConnectionString; Console.WriteLine(connStr); // 输出可复制的完整字符串,用于日志排查 using (var conn = new OracleConnection(connStr)) { conn.Open(); // ... 后续操作 }

为什么必须用 Builder?

  • 自动处理密码中含;、=、 (空格)等特殊字符的转义;
  • 强制类型检查(如Connection Timeout是 int,不会传入"15"字符串);
  • 生成的字符串符合 Oracle 官方语法规范,杜绝手写括号错位、括号缺失等低级错误;
  • Pooling=true是默认值,但显式写出可避免团队成员误关(关闭后性能暴跌 300%+)。

3. 生产级连接字符串的 7 个必调参数:从连接池到字符集,一个都不能少

3.1 连接池:Min/Max Size、Incr、Timeout 的黄金配比

Oracle.ManagedDataAccess默认启用连接池(Pooling=true),但默认Min Pool Size=1、Max Pool Size=100对多数业务过大或过小。真实场景需按负载调整:

参数推荐值说明
Min Pool Size5~10应用启动时预创建连接数,避免首请求延迟。Web API 建议 5,高并发后台服务可设 10
Max Pool Size50~100池中最大连接数。超过此数新请求将排队等待Connection Timeout。设过高易耗尽 DB 连接数(Oracle 默认processes=300)
Increment5当池空时,每次新增连接数。避免单次创建过多连接冲击 DB
Connection Timeout15~30连接获取超时(秒)。设太短(如 5)导致频繁抛OracleException;太长(如 60)阻塞线程
Connection Lifetime0连接在池中最大存活时间(秒)。0=永不过期,推荐保持 0,由 DB 端sqlnet.ora的EXPIRE_TIME控制

实操命令(在 Builder 中设置):

var builder = new OracleConnectionStringBuilder { // ... 其他参数 Min Pool Size = 5, Max Pool Size = 80, Increment = 5, Connection Timeout = 20, Connection Lifetime = 0 };

血泪经验:某金融系统曾将Max Pool Size设为 200,DBA 查到 Oraclev$session中 180+ 连接长期 idle,触发processes耗尽告警。调回 80 后,平均连接数稳定在 45±5,DB 负载下降 40%。

3.2 字符集与 NLS:解决中文乱码、日期解析失败的根源

Oracle 默认字符集常为AL32UTF8,但 .NET 环境若未显式声明,可能用US7ASCII导致插入中文变??。必须强制指定:

var builder = new OracleConnectionStringBuilder { // ... 其他参数 Unicode = true, // 关键!启用 Unicode 支持(.NET string 本质是 UTF-16) "NLS_LANG" = "AMERICAN_AMERICA.AL32UTF8" // 显式声明客户端 NLS };

参数作用详解:

  • Unicode=true:让OracleCommand.Parameters.Add("p1", "张三")自动映射为NVARCHAR2,而非VARCHAR2(后者依赖 DB 字符集);
  • NLS_LANG:控制日期格式、数字小数点、货币符号等。AMERICAN_AMERICA.AL32UTF8是最安全通用值,避免TO_DATE('2024-05-20', 'yyyy-mm-dd')因区域设置不同失败;
  • 若 DB 字符集是ZHS16GBK(老系统),则NLS_LANG改为AMERICAN_AMERICA.ZHS16GBK,但强烈建议 DB 迁移至AL32UTF8。

3.3 加密与安全:启用 SSL/TLS,禁用弱协议

Oracle 12c+ 支持原生 SSL 连接。生产环境必须开启,尤其当网络经过公网或不可信内网:

var builder = new OracleConnectionStringBuilder { // ... 其他参数 "SSL Server Certificate DN" = "CN=oracle-server,O=MyOrg,C=CN", // DB 证书 Subject DN "SSL Server DN Match" = "true", "SSL Wallet Location" = @"C:\oracle\wallet", // Wallet 目录,含 ewallet.p12 "SSL Authentication Mode" = "1" // 1=双向认证,0=单向(仅验证服务器) };

落地要点:

  • Wallet 必须用orapki工具创建,ewallet.p12需密码保护;
  • SSL Server Certificate DN必须与 DB 证书Subject完全一致(区分大小写);
  • 若仅需加密(不验证证书),设"SSL Authentication Mode" = "0",并省略SSL Server Certificate DN;
  • 绝对禁止:在连接字符串中写Encrypt=true(这是 SQL Server 语法,Oracle 不识别)。

4. 常见问题排查:5 个真实踩坑记录,现象→原因→解决

4.1 现象:ORA-12154: TNS:could not resolve the connect identifier specified

原因:

  • 误将SERVICE_NAME写成SID,且未在CONNECT_DATA中用(SID=xxx);
  • Data Source中 IP 地址或端口错误,DNS 无法解析主机名;
  • 使用了tnsnames.ora但文件路径未通过TNS_ADMIN环境变量指定,或文件语法错误(如多了一个逗号)。

解决:

  • 优先用 Easy Connect 格式(DESCRIPTION=(ADDRESS=...)(CONNECT_DATA=(SERVICE_NAME=xxx))),彻底绕过tnsnames.ora;
  • 用tnsping orclpdb(需 Oracle Client)或telnet 192.168.1.100 1521验证网络连通性;
  • 在代码中打印builder.ConnectionString,确认括号嵌套和引号闭合无误。

4.2 现象:连接成功,但SELECT * FROM employees返回中文字段全是?或空格

原因:

  • Unicode=false(默认值),导致VARCHAR2列以字节流读取,UTF-8 中文被截断;
  • NLS_LANG未设置或与 DB 字符集不匹配;
  • DB 字符集为AL32UTF8,但 .NET 程序运行在 Windows 简体中文系统,Console.OutputEncoding未设为UTF8(仅影响控制台显示,不影响数据)。

解决:

  • 强制Unicode=true;
  • 显式设置NLS_LANG="AMERICAN_AMERICA.AL32UTF8";
  • 检查 DB 字符集:SELECT parameter, value FROM nls_database_parameters WHERE parameter IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');;
  • 若 DB 是ZHS16GBK,则NLS_LANG改为AMERICAN_AMERICA.ZHS16GBK,且Unicode=false(但此方案已淘汰,应升级 DB 字符集)。

4.3 现象:应用运行几小时后,CPU 占用飙升,日志出现大量ORA-01000: maximum open cursors exceeded

原因:

  • OracleCommand未Dispose(),导致游标未释放;
  • 连接池中连接复用,但OracleCommand对象被缓存(如 static 字段),其内部游标句柄持续占用;
  • Max Pool Size过大,同时打开游标数超 Oracleopen_cursors参数(默认 300)。

解决:

  • 必须用using (var cmd = conn.CreateCommand()) { ... }确保Dispose();
  • 禁止将OracleCommand声明为static或长期持有;
  • 检查 DBopen_cursors:SHOW PARAMETER open_cursors,必要时ALTER SYSTEM SET open_cursors=600 SCOPE=BOTH;;
  • 在连接字符串中加Statement Cache Size=10(默认 10),启用语句缓存减少硬解析。

4.4 现象:.NET 6+ 项目中OracleConnection.Open()抛Oracle.ManagedDataAccess.Client.OracleException: ORA-28759: failure to open wallet

原因:

  • 启用了 SSL 但SSL Wallet Location路径错误,或ewallet.p12文件不存在/权限不足;
  • Wallet 密码错误,或SQLNET.ORA中WALLET_LOCATION与代码中SSL Wallet Location冲突;
  • .NET 运行时在 Linux 容器中,路径使用\而非/。

解决:

  • 确保SSL Wallet Location指向包含ewallet.p12的目录(非文件路径);
  • 在 Linux 容器中,路径必须为/app/oracle/wallet(正斜杠);
  • 用File.Exists(path + "/ewallet.p12")在代码中验证文件存在;
  • 临时禁用 SSL 测试:移除所有SSL*参数,确认基础连接正常后再加 SSL。

4.5 现象:批量插入 10 万条数据,耗时 12 分钟,远超预期

原因:

  • 用INSERT INTO ... VALUES (...)循环执行,每条语句独立提交;
  • 未启用OracleBulkCopy;
  • 连接字符串中Statement Cache Size过小(默认 10),导致重复 SQL 硬解析。

解决:

  • 改用OracleBulkCopy(类似 SqlServer 的SqlBulkCopy):
using (var bulk = new OracleBulkCopy(conn)) { bulk.DestinationTableName = "employees"; bulk.BatchSize = 10000; // 每批 1 万行 bulk.WriteToServer(dataTable); // dataTable 为内存 DataTable }
  • 将Statement Cache Size提至50;
  • 确保目标表无过多索引/触发器,批量前可ALTER TABLE employees DISABLE ALL TRIGGERS。

5. 进阶技巧:用 OracleConfiguration 类统一管理连接参数,避免硬编码

5.1 为什么需要 OracleConfiguration?

当项目模块增多(如 HR、Finance、Inventory 各用不同 Oracle 实例),连接字符串散落在各处:appsettings.json、web.config、const string。手动维护易出错,且无法动态切换测试/生产环境。Oracle.ManagedDataAccess提供OracleConfiguration类,可在应用启动时全局注入,所有OracleConnection自动继承配置。

5.2 注册全局配置:.NET 6+ Program.cs 中的正确姿势

// Program.cs var builder = WebApplication.CreateBuilder(args); // 1. 从 appsettings.json 读取 Oracle 配置 var oracleSection = builder.Configuration.GetSection("OracleSettings"); var connectionString = oracleSection.GetValue<string>("ConnectionString"); // 2. 设置 OracleConfiguration(全局生效) OracleConfiguration.DbDomain = oracleSection.GetValue<string>("DbDomain") ?? ""; // 如 "example.com" OracleConfiguration.SqlNetAuthenticationServices = "(NTS)"; // 启用 Windows 认证(可选) OracleConfiguration.StatementCacheSize = 50; OracleConfiguration.ConnectionTimeout = 20; // 3. 注册 OracleConnectionFactory(推荐 DI 方式) builder.Services.AddSingleton<OracleConnectionFactory>(sp => new OracleConnectionFactory(connectionString)); var app = builder.Build(); // 后续使用 public class EmployeeService { private readonly OracleConnectionFactory _factory; public EmployeeService(OracleConnectionFactory factory) => _factory = factory; public async Task<int> GetCountAsync() { using var conn = _factory.CreateConnection(); // 自动应用全局配置 await conn.OpenAsync(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM employees"; return (int)await cmd.ExecuteScalarAsync(); } }

5.3 配置文件示例:appsettings.json 分环境管理

{ "OracleSettings": { "ConnectionString": "User Id=hr;Password=hr123;Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=prod-db)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orclpdb)));", "DbDomain": "corp.internal", "EnableTracing": false }, "Logging": { "LogLevel": { "Default": "Information", "Oracle.ManagedDataAccess": "Warning" // 仅警告级日志,避免 trace 冗余 } } }

关键优势:

  • OracleConfiguration设置一次,所有new OracleConnection()自动生效,无需每个连接字符串重复写Statement Cache Size;
  • DbDomain用于 Easy Connect 简写:Data Source=prod-db/orclpdb→ 自动补全为prod-db.oracledb.corp.internal;
  • EnableTracing=true时生成odpnet.trc日志(需OracleTraceLevel配合),定位连接建立慢的问题(如 DNS 解析卡顿);
  • SqlNetAuthenticationServices启用 NTLM 认证,免密登录(需 DB 配置sqlnet.ora的SQLNET.AUTHENTICATION_SERVICES=(NTS))。

6. 最后一招:用 Oracle Trace 工具定位连接慢的“黑匣子”环节

6.1 开启 Oracle 客户端跟踪:三步锁定瓶颈

当conn.Open()耗时 >2s,却不知卡在哪(DNS?TCP?SSL握手?TNS解析?),靠Stopwatch只能知道“慢”,不知“为什么慢”。Oracle.ManagedDataAccess内置跟踪功能可输出详细时序日志:

// 启用跟踪(仅开发/测试环境!) OracleConfiguration.EnableTracing = true; OracleConfiguration.TraceFileLocation = @"C:\temp\odpnet.trc"; OracleConfiguration.TraceLevel = 7; // 0=off, 7=full detail // 之后执行 conn.Open() using (var conn = new OracleConnection(connStr)) { var sw = Stopwatch.StartNew(); conn.Open(); sw.Stop(); Console.WriteLine($"Open 耗时: {sw.ElapsedMilliseconds}ms"); }

跟踪日志解读关键行(odpnet.trc片段):

[2024/05/20 14:22:10:123] [ThreadID:1] [INFO] OracleConnection.Open() start [2024/05/20 14:22:10:125] [ThreadID:1] [INFO] Resolving host 'prod-db' via DNS... [2024/05/20 14:22:10:150] [ThreadID:1] [INFO] DNS resolved to 192.168.1.100 in 25ms [2024/05/20 14:22:10:152] [ThreadID:1] [INFO] Connecting to 192.168.1.100:1521... [2024/05/20 14:22:10:180] [ThreadID:1] [INFO] TCP connected in 28ms [2024/05/20 14:22:10:200] [ThreadID:1] [INFO] SSL handshake started... [2024/05/20 14:22:10:350] [ThreadID:1] [INFO] SSL handshake completed in 150ms [2024/05/20 14:22:10:355] [ThreadID:1] [INFO] TNS packet sent: CONNECT... [2024/05/20 14:22:10:420] [ThreadID:1] [INFO] TNS response received: OK [2024/05/20 14:22:10:422] [ThreadID:1] [INFO] OracleConnection.Open() completed in 299ms

逐段分析:

  • DNS resolved... in 25ms→ 正常;若 >1000ms,查 DNS 服务器或 hosts 文件;
  • TCP connected in 28ms→ 网络延迟正常;若 >500ms,用ping/tracert查路由;
  • SSL handshake completed in 150ms→ 偏高(理想 <50ms),检查 Wallet 是否过大、证书链是否完整;
  • TNS packet sent到response received耗时 65ms → DB 响应慢,需 DBA 查v$session_event中SQL*Net message from client等待事件。

6.2 生产环境轻量监控:用 OracleConnection.DataSource 属性反查连接来源

OracleConnection对象提供DataSource属性(返回解析后的实际连接地址),可用于日志追踪:

using (var conn = new OracleConnection(connStr)) { conn.Open(); // 记录连接详情,便于故障定位 _logger.LogInformation( "Oracle connected to {DataSource} as {UserId}, ServerVersion: {Version}", conn.DataSource, // 如 "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=192.168.1.100)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=orclpdb)))" conn.UserId, conn.ServerVersion); }

为什么这招实用?

  • 当多个微服务共用同一 DB,日志中看到DataSource可立即定位是哪个服务发起的连接;
  • conn.UserId是脱敏后的用户名(如hr),避免日志泄露明文密码;
  • conn.ServerVersion返回19.0.0.0.0等精确版本,排查兼容性问题(如Oracle.ManagedDataAccess 23.5.0连11g需降级到19.12.0)。

我带过的三个项目里,有两次线上连接超时,都是靠odpnet.trc发现 DNS 解析卡在 3 秒,最终查出是 Kubernetes CoreDNS 配置了ndots:5导致域名追加过多后缀;还有一次是SSL handshake耗时 800ms,发现 Wallet 中包含了已过期的根证书。这些坑,文档不会写,但每个 C# 开发者迟早会撞上。现在你知道怎么开了——不是靠猜,而是靠 trace 日志里的毫秒级证据。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 13:02:55

HW蓝队总结模板:护网行动防守复盘与量化汇报指南

简介&#xff1a;一份针对HW护网行动防守方的专项工作总结模板&#xff0c;适用于蓝队人员、安全服务商及政企单位安全团队&#xff0c;用于整理攻防演练期间的组织安排、防守手段、成果数据与复盘改进。模板按“工作概述—防守工作内容—工作亮点—下一步计划”展开&#xff0…

作者头像 李华
网站建设 2026/10/22 15:23:09

Cursor不能白嫖还不安全:DeepSeek V3+Cline,最强国产双开源解决方案

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

作者头像 李华
网站建设 2026/10/22 15:23:46

数据库系统原理及应用第五版习题答案解析与高效使用指南

简介&#xff1a;这份完整版习题答案对应《数据库系统原理及应用教程》第五版&#xff0c;面向正在学习数据库基础、准备期末复习或自考备考的本科生与自学者&#xff0c;可对照教材逐章检验对核心概念的理解。内容以第1章为起点&#xff0c;系统解答了数据与信息的区别、数据处…

作者头像 李华
网站建设 2026/10/22 15:23:18

用LoRA/DoRA微调Qwen-VL:单卡实战、避坑与部署指南

简介&#xff1a;这是一份针对Qwen-VL视觉语言模型进行Lora高效微调的完整实践项目&#xff0c;适合具备一定深度学习基础、希望掌握多模态大模型参数高效微调方法的研究者与工程师。项目以分层适应机制为核心&#xff0c;从数据预处理管道搭建、分层参数更新配置到多维度评估体…

作者头像 李华
网站建设 2026/10/15 16:59:05

万字长文解读wen进化史:从论文到TaoToken模型家族全景复盘

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

作者头像 李华