网银支付Java接口实战:从demo到生产的签名与回调避坑
简介面向Java开发者的农行Web端网银支付集成资源包同时提供官方接口文档与可直接运行的Demo适合电商或在线服务企业中负责支付模块的Java工程师需具备Web开发基础与支付接口联调经验。压缩包共147个文件约5.1MB内含54个class核心类交易请求、商户配置、签名服务等、19个jar依赖库、34个jsp与28个html演示页面另有4个properties配置文件、cer/truststore安全证书及少量jpg示意图目录结构清晰导入项目后可按模块对照学习。已有2074人学习下载。内容覆盖交易请求构造、RSA签名验证、银行响应解析、异常处理与回调通知完整链路通过运行Demo可直观看到初始化支付、传递参数、接收响应、验证回调等步骤接口文档对参数格式和返回码也有明确说明能帮助开发者快速完成农行网银支付集成减少联调排错时间。1. 银行web端网银支付Java接口文件及demo从拿到文档到跑通第一笔支付的完整路径拿到这套银行web端网银支付Java接口文件及demo时我的第一反应是松了口气——终于不用对着接口文档PDF盲猜字段含义了。这份资源包有完整的接口文档、可直接运行的Java示例工程、证书配置说明覆盖了下单、跳转、回调、查询四个核心环节。它解决的实际问题是你不必从零去拼报文和签名逻辑照着demo把参数填对跑起来后就能看到完整的请求与响应日志。适合刚接手银行支付需求、想快速跑通流程的新手也适合在联调中被证书和中文编码折腾到怀疑人生的熟手。我按实战顺序拆一遍重点是哪些参数改完就能用、哪些坑会让你白忙两天。2. 支付接口的四个环节下单、跳转、回调、查询的全链路理解2.1 为什么先理解机制再动手改代码银行web端网银支付和微信、支付宝的最大差异在于整个过程是浏览器跳转驱动的不是纯API调用。用户在你的网站下单后端把订单参数签名后拼到银行支付页面上用户输入银行卡信息完成支付银行异步通知你的回调地址你再拿着回调结果去查订单状态。四个环节缺一不可理解顺序决定了调试顺序下单请求后端把订单金额、订单号、商品描述、回调地址等参数按银行要求的字段顺序整理用商户私钥签名然后通过表单或重定向把请求发到银行网关地址。跳转支付用户浏览器在银行页面完成认证和支付操作。这个环节你基本不能干预能做的就是保证生成跳转页面的参数和签名正确。异步回调支付完成后银行服务器直接请求你配置的回调地址通知支付结果。这个请求不是你发起请求后同步返回的而是银行“主动送上门”的所以回调地址必须公网可达。订单查询处理回调后主动调用查询接口核对订单状态和金额作为最终落库依据。我一般会让新手先打印完所有环节的入参和出参日志再把demo里的测试商户号替换成真实商户信息。这样哪一步出错日志里一目了然。2.2 接口文档与demo资源包的内容结构这套资源的目录结构与典型银行支付SDK包类似拿到手先看这几个部分文件/目录作用接口文档接口字段说明、签名规则、参数示例、错误码表PDF或网页格式demo工程可直接导入IDE的Java项目包含下单、回调、查询的示例代码证书工具生成密钥对、转换证书格式、查看证书信息的脚本或工具类配置文件商户号、证书路径、回调地址、网关地址等参数的成对配置示例README运行环境要求、快速开始步骤、常见问题demo工程通常是Maven结构的普通JavaWeb项目依赖很少核心就是几个工具类签名工具、证书加载工具、HttpClient封装、参数组装类。入口一般是OrderServlet、NotifyServlet、QueryServlet三个类。下单后跳到银行页面银行回调打到NotifyServlet查询订单走QueryServlet流程相当直白。我建议先把demo工程完整看一遍再动生产代码。前期花两小时搞懂它怎么组织参数后面省下的调试时间不止两小时。3. 把demo跑起来从环境准备到第一笔真实支付3.1 环境配置和工程启动先把项目跑起来再谈业务改造。这套demo基于JavaWeb环境以下是我在本地跑通的标准步骤# 1. 检查JDK版本银行支付demo一般要求JDK1.6以上 java -version # 2. 解压资源包确认demo目录结构 unzip bank_pay_demo.zip ls -la bank_pay_demo/ # 3. 启动Tomcat假设解压到 /opt 目录 cd /opt/apache-tomcat-8.5.xx/bin ./startup.shJDK版本需要注意多个项目同时存在时确认当前环境用的确实是目标版本否则签名算法可能因加密强度限制而报错。Tomcat版本没有太多硬性要求8.5系列基本通用。导入IDEA或Eclipse后优先看资源包里的demo.properties文件。这里全部是运行的关键参数注意先不要动签名算法和证书格式参数优先改测试环境网关地址和回调地址。# 商户号测试环境分配 merchant.no123456789012345 # 证书密码一般在证书申请表里提供 merchant.cert.passbank_cert_password # 证书文件路径 merchant.cert.path/path/to/merchant.p12 # 测试网关地址 pay.gateway.urlhttps://gateway.test.bank.pay/pay/gateway # 回调地址测试时可临时填写本机外网映射地址 notify.urlhttp://yourdomain.com:8080/bank_pay_demo/notify # 页面编码做中文参数时统一用UTF-8 input.charsetUTF-8配置项看着不多但每一行都有讲究。商户号是银行分配的唯一身份标识证书密码和证书文件必须匹配网关地址测试和生产是两个不同域名——我见过有人把测试地址配到了生产环境结果线下验证一切正常上线时请求全部超时。3.2 第一笔支付的处理流程验证启动成功后访问demo工程的下单页面。下单请求的核心代码通常长这个样子// 组装下单请求参数 MapString, String params new HashMapString, String(); params.put(merchantNo, merchantNo); params.put(orderNo, orderNo); params.put(orderAmount, String.valueOf(amountInFen)); params.put(productName, productName); params.put(notifyUrl, notifyUrl); params.put(returnUrl, returnUrl); params.put(inputCharset, UTF-8); // 按字典序排序后拼接签名原文 String signSrc SignTool.sortAndJoin(params); String sign SignTool.sign(signSrc, merchantPrivateKey); params.put(sign, sign); // 生成自动提交的HTML表单 StringBuilder html new StringBuilder(); html.append(form idpayForm action).append(gatewayUrl).append( methodpost); for (EntryString, String entry : params.entrySet()) { html.append(input typehidden name).append(entry.getKey()) .append( value).append(entry.getValue()).append( /); } html.append(/form); html.append(scriptdocument.getElementById(payForm).submit();/script);这段代码完成了三个关键动作。第一把下单参数放进Map第二对所有参数按字典序排序、拼接成签名源串再用商户私钥生成签名第三动态生成一个自动提交的HTML表单把参数通过POST方式传到银行网关。amountInFen这个字段值得专门强调订单金额的单位是“分”。如果你直接传“100.00”元银行可能直接拒绝或者按100分处理最后账对不上。正确的做法是金额转成整数分再塞进去千万不要用float或double存金额。跑通后浏览器会跳到银行测试支付页面输入测试卡号完成支付然后自动跳到returnUrl同时银行后台向notifyUrl发异步通知。此时看到NotifyServlet日志输出“验签成功”四个字说明第一笔支付基本跑通。4. 签名、证书、编码联调中最容易翻车的三处细节4.1 签名生成与验签的具体逻辑银行支付接口最核心的技术点是RSA签名。签名的作用不是加密而是防止参数在传输过程中被篡改。商家请求银行时用商家私钥签名银行用商家公钥验签银行回调商家时用银行私钥签名商家用银行公钥验签。签名生成过程通常是这样的public static String sign(String content, PrivateKey privateKey) throws Exception { Signature signature Signature.getInstance(SHA1withRSA); signature.initSign(privateKey); signature.update(content.getBytes(UTF-8)); byte[] signed signature.sign(); return Base64.encodeBase64String(signed); } // 验签 public static boolean verify(String content, String signValue, PublicKey publicKey) throws Exception { Signature signature Signature.getInstance(SHA1withRSA); signature.initVerify(publicKey); signature.update(content.getBytes(UTF-8)); return signature.verify(Base64.decodeBase64(signValue)); }签名源串的组装规则是整个联调中出错率最高的环节。银行通常要求把所有需要签名的参数名按字典序排序用keyvalue形式拼接再用连接不能包含sign字段本身也不能包含空值。这里有个细节如果参数值是空字符串有些银行要求跳过有些银行则要求拼进去。以接口文档为准不确定时先把空值参数过滤掉。验签失败时我会优先检查三个点签名源串和银行那边拼接的是否完全一致、字符集是否统一、Base64解码方式是否正确。多数“验签失败”不是算法问题而是这三项里某一样不一致。4.2 证书格式和密钥管理的两个常用方案证书文件是整个支付对接里最像“黑匣子”的部分。这套demo支持的证书一般是两套商家证书P12格式内含商家私钥和商家公钥用于生成签名文件后缀通常为.p12或.pfx有访问密码。银行公钥证书DER格式内含银行公钥用于验证银行回调消息的签名通常是.cer后缀的文件。加载证书的代码一般长这样KeyStore ks KeyStore.getInstance(PKCS12); InputStream is new FileInputStream(certPath); ks.load(is, certPassword.toCharArray()); String alias ks.aliases().nextElement(); PrivateKey privateKey (PrivateKey) ks.getKey(alias, certPassword.toCharArray());证书加载碰到的第一道坎是密码。证书密码不是商户登录密码而是银行发证书时一并给你的独立密码复制时注意别带空格。第二道坎是格式银行给的证书格式偶尔是DER编码的直接用KeyStore加载会报“invalid keystore format”。处理方式是用keytool或资源包里的证书转换工具转成PKCS12格式。我一般会先把证书文件用文本编辑器打开看一眼出现“------BEGIN--”就是PEM编码纯二进制则是DER编码心里有数再动手。4.3 回调通知的参数解析与验签顺序回调是整个支付环节中逻辑最密集的步骤顺序很重要先验签再核对金额再更新订单状态。验签应该在解析业务参数之前完成否则攻击者可以伪造参数。// 1. 接收回调参数不包含sign字段 MapString, String callbackParams parseNotifyRequest(request); String bankSign request.getParameter(sign); String signSource SignTool.sortAndJoin(callbackParams); // 2. 用银行公钥验签 boolean passed SignTool.verify(signSource, bankSign, bankPublicKey); if (!passed) { // 记录日志不修改订单状态 return failResult(); } // 3. 验签通过后查订单原始金额 OrderModel order orderService.getByOrderNo(callbackParams.get(orderNo)); long originalFen order.getAmountInFen(); long callbackFen Long.parseLong(callbackParams.get(orderAmount)); if (originalFen ! callbackFen) { // 金额不一致直接拒绝并告警 return failResult(); } // 4. 更新订单状态为已支付 orderService.markPaid(order.getOrderNo()); return successResult();很多初学的人在回调里只验签不核金额这是个容易出大事的疏漏。银行回调参数里的金额会以分为单位的字符串传过来而本地订单表可能用元做单位存储直接比较数值会出问题。正确做法是两边统一成“分”后再比较或者用BigDecimal的方式精确比价。回调处理完成后必须返回给银行一个确认字符串不同银行要求不同有的要求返回SUCCESS有的要求返回纯文本ok。格式不对的话银行会认为回调未送达服务器会重复通知多次后面就出现“订单被重复更新”的问题。5. 避坑排查五条最常见的联调事故记录5.1 证书加载失败报“invalid keystore format”现象启动demo或发起下单请求时日志直接抛出IOException: invalid keystore format证书加载不到。原因证书文件编码格式和代码预期不一致。银行证书下发时可能是DER编码而代码按PKCS12格式去加载。另一种可能是证书文件在下载过程中被损坏字节数不对。解决先确认证书扩展名和文件大小下载一份原始证书比对MD5值。如果确认是DER格式用证书工具转成PKCS12后再重新配置如果文件本身损坏联系银行技术支持重新下发。代码里可以加一个try分支先尝试PKCS12失败再尝试其他格式但生产环境不建议这种“猜格式”的处理方式明确已知格式更稳。5.2 测试环境一切正常生产环境所有请求超时现象测试网关下单跳转流畅回调也正常。换到生产配置后支付页面打不开、请求长时间无响应。原因根因通常不是代码问题而是生产服务器到银行网关的网络不通。生产环境一般有出网限制策略服务器无法访问银行网关的IP或域名。反向排查时发现是防火墙规则把HTTPS流量拦了。解决把生产环境需要访问的银行网关IP和端口清单发给运维做白名单放行。我习惯在demo里加一个连通性检查功能用一个简单HTTP请求访问网关地址的health路径连不通就立刻打日志并告警避免上线后才发现网络不通。5.3 支付金额总是差几分钱现象订单金额是100元回调返回的金额是9999分数据库里记的却是100.00元对账差0.01元。原因金额在前端或后端用float类型做了运算和转换。100.0f * 100在二进制浮点运算中结果接近于10000取整或转字符串时会变成9999或10001。这是Java浮点精度导致的典型问题。解决所有金额相关的字段一律用整数“分”存储和计算单位转换只在展示层做。签名参数里的金额也必须用分单位的纯数字字符串拼接。从那以后我凡是看到double类型存金额的代码一律改成long或BigDecimal。5.4 中文商品名在支付页面显示乱码现象下单参数里商品名称是中文跳转到银行支付页面后显示成问号或乱码签名却验证通过。原因参数在拼签名原文时使用了getBytes()无参方法默认使用了运行环境的字符集可能是GBK。而HTML表单提交和银行网关解析时要求UTF-8编码两边不一致导致显示乱码。解决签名原文拼接时显式指定getBytes(UTF-8)页面设置charsetUTF-8。同时检查Tomcat的Connector配置给URIEncodingUTF-8加上。这个坑在编码上属于玄学范畴排查时最省事的方式就是统一全链路UTF-8不接受任何默认值。5.5 订单重复支付通知导致状态被覆盖现象同一笔订单收到两次回调第二次回调把第一次的已支付状态改回了“待支付”或者多扣了一笔库存。原因回调没有做幂等处理。银行可能因网络原因重发通知也可能用户支付成功后手动刷新页面导致多次触发。解决订单状态更新必须基于当前状态做条件更新。SQL层面用UPDATE order SET statusPAID WHERE order_no? AND statusUNPAID的方式写更新行数为0就说明已处理过直接返回成功确认给银行。逻辑上要区分“订单不存在”和“订单已处理”两种情况后者的响应也是成功。6. 回调验签细节与对账脚本把这套资源用得更稳跑通demo只算入门真正要放生产建议把验证框架搭得更完整一些。我自己的习惯是在回调处理里做成“三段式校验”第一段验签第二段校验金额和订单号匹配第三段校验订单状态是否可流转。前两段失败直接返回失败应答第三段失败返回成功应答且不改数据。这样即使银行重发通知也不会出现重复操作。另外建议把整个支付流程的请求日志集中到一个地方。下单请求的入参值、签名原文、签名结果、回调的入参值、验签结果、数据库操作结果全按traceId串起来。排查问题时一条链路从下单到回调全部串在一起比翻多个日志文件高效太多。资源包里的demo通常只涉及实时支付但生产建议加一个定时对账逻辑。每天凌晨拉取银行交易流水和自己订单表逐笔比对金额、状态、时间都要对得上。这一步能弥补回调丢失、网络异常等实时链路覆盖不到的问题。我还会把订单金额分成“前端展示元”和“后端存储分”两层概念写进入职文档防止后面接手的人用错单位。证书密码统一放到配置中心的加密项里不和源码头文件一起提交。这套资源里给的证书工具和签名工具类在对接完支付后还可以复用做加密退款、批量转账这类业务时签名逻辑几乎是一样的只是接口地址和字段不同。所以第一次对接时不着急删代码把工具类沉淀好后面每次对接至少省三分之一时间。从那次证书加载三天没解决之后我每次拿到银行接口包都强制走一遍验签、对账脚本、日志链路一个都不少。希望这套资源和这篇拆解能帮你少踩一轮坑。本文还有配套的精品资源点击获取

相关新闻

I3D视频行为识别模型拆解:从膨胀初始化到训练调参

I3D视频行为识别模型拆解:从膨胀初始化到训练调参

做视频行为识别的朋友,应该都绕不开一个经典模型:I3D(Two-Stream Inflated 3D ConvNets)。它是CVPR 2017上提出的方法,也是很多后来工作的骨架和基准。当时它在UCF101和HMDB51这两个公开数据集上把准确率推到新的高度&…

2026/10/12 9:04:34 阅读更多 →
OpenCV图像旋转实战:从旋转矩阵到坐标映射的完整解析

OpenCV图像旋转实战:从旋转矩阵到坐标映射的完整解析

简介:面向 OpenCV 初学者的图像旋转示例工程,内含完整 Visual C 项目源码、可执行程序与示例图片,用于快速理解仿射变换与图像旋转实现。工程演示 cv::getRotationMatrix2D 与 cv::warpAffine 的配合用法,也展示 cv::rotate 便捷调…

2026/10/12 9:04:34 阅读更多 →
用claude-mem打造Claude跨会话长期记忆:本地向量检索与提示词注入实战

用claude-mem打造Claude跨会话长期记忆:本地向量检索与提示词注入实战

我平时用 Claude 最烦的一件事,就是每次开新会话它就把我之前聊过的项目背景、技术方案、甚至我反复交代过的偏好忘得一干二净。你只能把一个 4000 字的需求背景重新复制粘贴一遍,对方读完还要假装惊讶地说“原来你做过这个”。这也是我最初看到claude-m…

2026/10/12 9:04:34 阅读更多 →

最新新闻

哈希算法在系统安全中的关键作用:从密码存储到完整性校验

哈希算法在系统安全中的关键作用:从密码存储到完整性校验

你有没有遇到过这样的情况:下载一个软件安装包时,官网页面会贴出一串长长的十六进制字符,比如e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。大多数人都把这串字符当成可有可无的“官网装饰”,但你有没有想…

2026/10/12 9:43:27 阅读更多 →
Java函数复写深度解析:从语法规则到Android实战避坑

Java函数复写深度解析:从语法规则到Android实战避坑

1. 从一次编译报错说起:为什么函数复写值得单独拎出来讲刚接触Java4Android那会儿,我写了一个继承自父类的子类,想当然地给某个方法换了个参数类型,以为这就是"复写"。结果运行起来,程序走的还是父类的逻辑&…

2026/10/12 9:43:27 阅读更多 →
JSP+MySQL构建BBS论坛:核心架构与完整开发实战

JSP+MySQL构建BBS论坛:核心架构与完整开发实战

简介:基于JSP与MySQL构建的BBS论坛项目源码包,面向正在学习Java Web开发的学生与开发者,完整演示了论坛系统的核心业务流程:用户注册登录、板块管理、主题发帖、回复互动、关键词搜索以及后台权限控制等。包内共493个文件&#xf…

2026/10/12 9:43:27 阅读更多 →
5G下行数据传输流程图形化:从GTP-U隧道到空口调度完整解析

5G下行数据传输流程图形化:从GTP-U隧道到空口调度完整解析

简介:5G NR下行数据传输完整流程的图解资料,以图形化方式呈现从应用层到物理层的端到端数据传递过程,面向5G网络工程师、通信专业学生或初识5G NR协议的读者。内容以独立基站为场景,基于CU-DU高层切分架构展示用户平面协议栈&…

2026/10/12 9:43:27 阅读更多 →
LeetCode 406 贪心与排序实战:C语言重建队列

LeetCode 406 贪心与排序实战:C语言重建队列

1. 从一道题看贪心与排序的配合逻辑Leetcode 406 这道题,标题叫“根据身高重建队列”,乍一看像是数据结构题,实际上它是一道非常典型的排序 贪心 插入的综合题。我当年第一次做的时候,第一反应是“这题是不是得用拓扑排序”&…

2026/10/12 9:43:27 阅读更多 →
用AI精准生成健身动作画册:从姿态控制到批量出图

用AI精准生成健身动作画册:从姿态控制到批量出图

做这类“给AI出题”的项目,最怕的不是AI翻车,而是你的需求本身就模糊。Stretchbook这个标题看起来简单——用AI做一本健身动作画册,但它背后其实藏着一条相当完整的技术链路:动作姿态怎么保持准确?风格怎么统一&#x…

2026/10/12 9:42:25 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/11 14:36:54 阅读更多 →