新闻详情

新闻详情

首页 / 资讯中心 / 详情

PHP对接EOS区块链:PHP开发包实现RPC调用与离线签名实战

发布时间:2026/9/25 7:56:21来源:尧图网络
PHP对接EOS区块链:PHP开发包实现RPC调用与离线签名实战
很多人第一次看到“php eos”这个标题第一反应是PHP里的heredoc字符串语法第二反应才可能是EOS区块链。两个理解其实都对这个项目的核心就是用PHP通过开发包对接EOS区块链——而eos那种“向EOS输出一段内容”的语义放在链上开发里居然也贴切PHP把构造好的交易数据“喂”给EOS节点节点再把结果吐回来。我这次把实际业务中基于PHP开发包对接EOS链上API、完成余额查询、离线签名、合约调用的完整过程整理出来包括踩过的坑和可以直接抄的代码。对正在用PHP做区块链服务端、或者准备接入EOS的团队来说这篇应该能帮你省下一两周的试错时间。1. EOS区块链与PHP开发包的整体认知1.1 EOS是什么为什么PHP能参与其中EOS是一条采用DPoS委托权益证明共识机制的公链核心卖点是高吞吐和低延迟。它不像比特币那样用PoW拼算力也不像以太坊那样所有节点跑同一套EVM而是靠一组经过投票选举的超级节点轮流出块配合并行交易执行来提升性能。对开发者来说最直接的感受是转账、合约调用这类操作确认速度很快而且手续费不是按Gas竞价的而是通过抵押代币换取CPU、NET、RAM资源。那PHP在这条链里能干什么很多人觉得区块链开发必须是C、Go、Rust这个刻板印象得改改。EOS节点对外暴露的是一套JSON-RPC接口任何能发HTTP请求的语言都可以接入。PHP做服务端这么多年HTTP、JSON、加密相关的生态都非常成熟用PHP去调EOS节点接口、处理ABI、构造签名交易技术上没有额外的门槛。我在团队里就是负责用PHP搭EOS钱包后端服务的从查询余额到代币转账再到合约交互都是靠PHP开发包一层层封装出来的。1.2 PHP开发包要解决的核心问题所谓PHP开发包本质上就是个“中间层”把EOS节点RPC接口和PHP业务代码之间那些繁琐的细节包起来。我觉得一个好用的EOS PHP开发包至少要回答四个问题怎么连节点RPC地址、版本兼容、超时处理。怎么管私钥不能把私钥硬编码在代码里得考虑加密存储和内存安全。怎么构造和签名交易EOS的转账不是简单拼个JSON就行的要序列化、算摘要、做ECDSA签名然后把签名塞回去。怎么处理响应和异常节点返回的错误码各不相同比如eosio_assert_message_exception代表合约断言失败tx_cpu_net说明资源不足开发包得把这些错误转成业务能识别的异常信息。为什么这些事不能每次自己写因为真正常踩的坑全在细节里。比如EOS的交易有个expiration字段超过这个时间广播就会被拒时间格式要用2023-01-01T00:00:00这样的ISO8601ref_block_num和ref_block_prefix要从最新区块拿拿错了哪怕是签名正确的交易也会被节点判为无效。这些细节靠临时查阅文档去怼效率太低交给开发包统一处理才靠谱。1.3 选一个开发包前先想清楚边界市面上确实有一些EOS的PHP库但老实讲大部分都处在“能用但别指望面面俱到”的状态。有的只封装了查询接口不支持交易构造有的支持交易构造但ABI序列化做得不完整复杂合约参数就报错还有的维护停滞EOS的链API版本向前向后一变动就没法用了。我的建议是不要指望一个包解决所有问题开发包的边界应该用到“把RPC通信和基本序列化做稳”为止再往上的业务逻辑、异常重试、私钥方案最好自己控制。这样即使底层包不维护了替换成本也低。2. 开发环境准备与包选型2.1 基础环境依赖PHP版本与扩展的黄金组合我当前在用的环境是PHP 8.1最低建议PHP 7.4低于这个版本很多现代语法和函数都不支持跟EOS交互的库也没法跑。除了PHP本身以下几个扩展是必须的curl所有RPC请求都是通过curl发的这个不用多说。openssl处理密钥对和签名的基础库。gmpEOS的签名算法是基于secp256k1椭圆曲线的涉及大整数运算GMP扩展能提供高效的大数计算支持。sodiumEOS的一些加密操作跟libsodium相关虽然不是每个包都用但装上有备无患。如果你只想搭个最小可运行环境用Docker是最快的。我不建议在开发机上直接装一堆扩展污染环境用docker起一个PHP容器里面塞好扩展再起一个EOS本地测试节点就能把开发和测试完全隔离。2.2 包选型自己封装还是用开源的权衡这里我交代一下我的选择查询类接口我用现成的RPC封装交易签名部分我自研了一个轻量封装。为什么这么折腾因为试过几个开源包后发现几个共性问题对比维度开源包自研轻量封装ABI序列化完整性部分包对嵌套结构支持差按需实现配合离线ABI文件链API版本兼容维护不及时会失效跟随官方RPC文档维护私钥处理方式有的包要求明文传入可用自定义密钥管理器可维护性依赖原作者更新完全自己掌控上手速度composer一条命令搞定需要一点编码量选择标准就三条一是看包是否支持EOS链API的当前版本二是看ABI序列化对复杂参数的覆盖度三是看维护活跃度。如果你只做查询和固定几种合约调用用一个维护状态不错的开源包完全够。如果业务复杂、要频繁对接不同类型的合约自研封装反而更省心。我当时就是被一个合约的map类型参数序列化问题卡了整整一天最后索性自己写了序列化层。2.3 用Docker搭一个本地EOS测试节点开发EOS不搭本地节点调试体验会非常痛苦。主网账户、私钥、代币都是真金白银不能拿来乱试。我的做法是在本地用容器跑一个EOS节点专门用测试账号做接口调试和签名验证。docker run --name eos-dev -p 8888:8888 -d eosio/eos:v2.0.0启动后本地就会有一个监听8888端口的EOS节点。接下来需要创建测试账户、给账户充值测试代币、部署合约等。EOS有一个好用的一键开发环境配置脚本eosio_bash.sh旧版本中通过它可以在容器里完成建钱包、导入私钥、创建账户的全套配置。我在开发环境里通常还会装一个cleos命令行工具需要快速验证某个接口或查询某笔交易时用命令行比写PHP快得多。3. 核心原理解析PHP与EOS链上交互的关键机制3.1 RPC API调用链从HTTP请求到区块数据EOS节点对外提供的API分几类最常用的是链APIChain API和历史APIHistory API。链API负责查询链上状态、发布交易历史API负责查询账户历史操作。举个例子查账户余额最基本的两个接口是POST /v1/chain/get_account POST /v1/chain/get_currency_balanceget_account返回账户详细信息包括资源抵押量、权限结构get_currency_balance返回指定合约代币的余额。这些接口的请求和响应基本都是JSONPHP里写一个基础的RPC客户端非常容易?php class EosRpcClient { private string $baseUrl; private float $timeout; public function __construct(string $baseUrl, float $timeout 5.0) { $this-baseUrl rtrim($baseUrl, /); $this-timeout $timeout; } public function call(string $path, array $params []): array { $ch curl_init($this-baseUrl . / . $path); curl_setopt_array($ch, [ CURLOPT_POST true, CURLOPT_POSTFIELDS json_encode($params), CURLOPT_HTTPHEADER [Content-Type: application/json], CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT $this-timeout, ]); $response curl_exec($ch); if (curl_errno($ch)) { throw new RuntimeException(RPC request failed: . curl_error($ch)); } curl_close($ch); return json_decode($response, true, 512, JSON_THROW_ON_ERROR); } }3.2 交易签名一笔转账的完整生命周期EOS的交易签名比普通API请求复杂得多一张图就能看明白的流程用文字描述是这样先通过get_info拿到链ID和最新区块信息然后从最新区块中提取ref_block_num和ref_block_prefix确定交易的过期时间当前时间一个合理窗口比如60秒。交易体包含一个或多个动作Action每个动作包含account合约账户、name动作名、authorization权限声明和data参数数据。所有字段拼起来后要按ABI规则序列化成二进制然后再做SHA256摘要最后用私钥对摘要做secp256k1签名。签名结果要转成EOS认可的格式通常是Base64或数组形式塞回原始交易结构中。最终通过push_transaction把整个交易JSON发给节点。PHP里做secp256k1签名建议用kornrunner/secp256k1这个库纯PHP实现配合GMP扩展不需要装额外的C扩展。核心签名代码大致是这样use kornrunner\Secp256k1; $secp256k1 new Secp256k1(); $privateKey str_pad(gmp_strval(gmp_init(hex2bin($privHex), 256), 10), 64, 0, STR_PAD_LEFT); $signed $secp256k1-sign($signatureHash, $privateKey);这里有个细节要提醒$signatureHash必须是32字节的二进制字符串不是明文字符串。很多人第一次写都死在这直接把交易摘要的十六进制字符串传给签名函数得到的签名节点永远校验不过。3.3 ABI序列化合约参数的通关密码EOS的合约参数不是直接传JSON的节点要求的是按ABI规则序列化后的二进制数据。举个例子如果合约定义的转账动作是transfer(from, to, quantity, memo)其中quantity是asset类型它内部是一个数字加一个符号比如1.0000 EOS序列化时要把余额拆成整数部分和精度部分分别存储。PHP开发包里的ABI序列化器要做的事就是加载合约的ABI JSON文件包含各动作的字段名、类型、嵌套结构把PHP数组映射成二进制字符串。比如$data $abi-serializeActionData(transfer, [ from account1, to account2, quantity 1.0000 EOS, memo hello, ]);这里序列化顺序、类型转换、字符串长度编码都有严格规范一个字节错都不行。我的经验是开始做之前先把EOS的ABI文档读透特别是asset、symbol、name这几个特殊类型shenme都别想当然。我自己就吃过亏name类型转账时我以为直接传字符串就行结果序列化按变长字符串处理节点报解码错误排查了一下午才发现是类型处理不对。4. 实操篇PHP查询与转账开发全流程4.1 初始化客户端配置节点、链ID与HTTP超时开始写代码前先封装一个EOS客户端类把节点地址、超时时间、链ID等基础配置管理起来。链ID很关键它用于构造交易时标识是在哪条链上主网和测试网的链ID完全不同。不要手动拼写链ID一定要从节点接口拿。?php class EosClient { private string $baseUrl; private string $chainId; private EosRpcClient $rpc; public function __construct(string $baseUrl) { $this-baseUrl $baseUrl; $this-rpc new EosRpcClient($baseUrl); $info $this-rpc-call(v1/chain/get_info); $this-chainId $info[chain_id]; } public function getChainId(): string { return $this-chainId; } public function getInfo(): array { return $this-rpc-call(v1/chain/get_info); } public function getAccount(string $account): array { return $this-rpc-call(v1/chain/get_account, [account_name $account]); } public function getCurrencyBalance(string $account, string $code eosio.token): array { return $this-rpc-call(v1/chain/get_currency_balance, [ account $account, code $code, symbol EOS, ]); } }初始化时自动获取链ID是我反复强调的做法不要在代码里写死。因为一旦你切换网络从主网切到测试网、或者从测试网切到本地节点写死的链ID会导致交易全部无效。自动获取看似多一次HTTP请求但换来的是不同环境间的无缝切换。4.2 余额查询JSON响应解析的健壮性处理余额查询最简单的就是调get_currency_balance响应格式是一个字符串数组比如[12.3456 EOS]注意是数组不是单值。因为同一个账户可能持有同一个合约下的多个不同资产符号。我在接入时踩过一个不小的坑如果把symbol参数留空接口返回的是该账户在指定合约下的全部代币余额如果指定EOS则只返回EOS一个。业务如果只关心EOS最好明确传symbol参数否则遇到同时持有EOS和某个粉丝代币的账户响应会多出元素解析逻辑就得写循环了。余额查询还有一个容易踩的点即使拿到了字符串余额做算术运算也要小心。EOS的asset格式是金额 符号金额部分可能带小数点不能直接当整数用。保险的做法是把字符串先拆分再按小数精度转成最小单位整数比如精度4位1.0000 EOS转成10000运算后再格式化成字符串。我封装了一个小工具类专门做这个转换避免精度丢失。4.3 离线签名并广播一笔真实转账的实现这是整个开发包的核心功能——离线构造交易、签名、广播上链整个过程不需要把私钥交给节点。先看代码然后我逐个关键点解释?php use kornrunner\Secp256k1; use kornrunner\Keccak; class EosTransactionBuilder { private EosClient $client; private string $privateKeyHex; private string $publicKeyHex; public function __construct(EosClient $client, string $privateKeyHex) { $this-client $client; $this-privateKeyHex $privateKeyHex; } public function transfer(string $from, string $to, string $quantity, string $memo ): string { $info $this-client-getInfo(); $headBlockNum $info[head_block_num]; $lastIrreversibleBlockNum $info[last_irreversible_block_num]; $expiration gmdate(Y-m-d\TH:i:s, time() 120); $tx [ expiration $expiration, ref_block_num $lastIrreversibleBlockNum 0xFFFF, ref_block_prefix $info[ref_block_prefix], max_net_usage_words 0, max_cpu_usage_ms 0, delay_sec 0, context_free_actions [], actions [[ account eosio.token, name transfer, authorization [[ actor $from, permission active, ]], data [ from $from, to $to, quantity $quantity, memo $memo, ], ]], ]; $serializedTx $this-serializeTransaction($tx); $signature $this-signTransaction($serializedTx); $pushData [ signatures [$signature], compression none, packed_context_free_data , packed_trx bin2hex($serializedTx), ]; return $this-client-pushTransaction($pushData); } private function serializeTransaction(array $tx): string { // 这里需要把交易体按EOS协议序列化尤其是actions里的data需要ABI序列化 // 实际操作中可用开发包的transaction serializer $serializer new EosTransactionSerializer($this-abiProvider); return $serializer-serialize($tx); } private function signTransaction(string $serializedTx): string { $hash hash(sha256, $serializedTx, true); $secp256k1 new Secp256k1(); $signature $secp256k1-sign($hash, $this-privateKeyHex); return $this-signatureToString($signature); } }关键细节我逐条说。过期时间设置120秒是我实测比较稳的一个窗口。太短交易容易在广播前排期超时太长则容易被网络攻击重放。EOS的push_transaction要求交易未过期才能打包但同时节点也会检查是否引用有效区块。引用区块ref_block_num和ref_block_prefix必须来自最新的不可逆区块或接近最新的头区块。我建议用last_irreversible_block_num而不是head_block_num因为引用不可逆区块可以避免极少数情况下链回滚导致的交易失效。注意ref_block_num只需要16位所以代码里做了 0xFFFF。签名内容签名是对整个序列化后的交易二进制做SHA256后进行的不是对JSON字符串。所以传输给签名函数前必须先序列化。EOS的签名格式是SIG_K1_开头的Base58字符串但内部还要包含恢复IDrecid。kornrunner\Secp256k1的sign方法返回的对象里有r、s和recid要组合后转成EOS的签名格式。我封装了一个signatureToString函数来处理转换此处就不贴完整代码了但要点是recid不能丢否则节点无法恢复出公钥来验证签名。节点验签通过后交易被广播到P2P网络确认后资产就完成转移。整个过程PHP只负责构造与签名最关键的一步“验证签名”发生在节点侧所以私钥始终没离开过你的服务器。4.4 查询交易结果确认上链的可靠方式广播交易后返回的是一个交易ID但返回并不代表最终成功。EOS的交易处理是最终一致性的广播后可能进入内存池也可能因为某些原因被丢弃。所以我一般在广播后做三件事等1~3秒调用v1/history/get_transaction查询交易状态。通过v1/chain/get_info对比当前不可逆区块高度看这笔交易确认深度是否足够。如果失败比如CPU资源不足捕获异常并整理成业务可读的提示。查询交易的PHP代码public function getTransaction(string $txId): array { return $this-client-call(v1/history/get_transaction, [ id $txId, ]); }如果拿到trx里有receipt.status字段值有executed、soft_fail、hard_fail几种。executed代表被成功执行hard_fail代表合约执行失败但交易本身被收取了资源费用。业务上不要只看返回状态码200一定要检查receipt.status。5. 实战排错笔记那些坑与对策5.1 RPC错误清单从eosio_assert到tx_cpu_net我在开发过程中整理了一张很实用的错误对照表遇到异常直接看表就能定位方向。错误类型典型报错信息原因处理建议合约断言失败eosio_assert_message_exception合约内部断言条件不满足比如余额不足、参数不合法中文提示往往在message里直接取message展示给用户CPU资源不足tx_cpu_net账户抵押的CPU资源不足提示用户抵押更多EOS或升级Stake方案交易过期expired transaction从构造到广播之间超过了expiration检查服务器时钟构造交易时使用动态过期时间序列化错误bad_deserializeABI序列化/types不匹配比对合约ABI字段重点检查asset、name类型签名错误signature is invalid签名数据、私钥、signature hash填错核对签名是否基于序列化后的交易二进制而不是原JSON网络不可达unable to connect to node节点地址配错或节点服务挂了检查节点可用性做多节点故障转移5.2 签名失败、交易过期、序列化错位的典型修法这几个问题我每一个都实打实遇到过说下排查过程。签名失败有一次测试环境换了一套新密钥代码里私钥写入方式变了结果发出去的交易全部报signature is invalid。排查方法是在本地用一个已知私钥、构造一笔最简单的转账用离线方式比对节点验签结果。最后发现是我在读取私钥时把十六进制字符串转成了十进制大整数再转回二进制时前面补位出了问题私钥长度不对签名自然无效。解决办法是私钥统一按64位十六进制字符串保存和传递不要在中间环节做进制转换导致信息丢失。交易过期服务器UTC时区配置错误gmdate产生的过期时间比节点时间慢了8小时交易一广播就提示过期。这个坑很隐蔽因为代码本身没毛病纯环境问题。后来我在初始化客户端时增加了一次时间同步校验用get_info返回的head_block_time跟本地时间做差值超过5分钟就报警。生产环境强烈建议启用NTP时间同步。序列化错位最头疼的问题。转移一个自定义合约资产时合约动作有两个asset参数我漏看了ABI里字段顺序把两个值传反了。结果交易居然在签名环节通过、广播环节也返回成功最终链上执行时才发现逻辑完全反了。从那之后我做了一个强制约定合约字段的赋值不许靠记忆必须在代码里直接读合约ABI JSON按字段顺序逐个填充。即便多写几行代码也不允许凭经验硬拼。5.3 生产环境部署建议PHP-FPM下的连接池与重试策略开发包最终是要上生产的不只是本地跑通。在PHP-FPM模式下每个请求会经历完整的生命周期这跟常驻进程的场景差异很大有几个点需要提前设计好。节点连接复用PHP-FPM每个请求结束后连接就释放了如果每次请求都新建curl handle节点压力会很大而且握手时间也会拖慢响应。我用了curl_multi做一个轻量连接池在进程生命周期内复用连接尤其是批量查询场景下收益非常明显。超时与重试EOS节点偶尔会出现短时不可用直接抛异常给前端体验很差。我在封装层里加了重试逻辑对查询类接口超时后最多重试2次间隔递增对广播类接口不盲目重试因为重复广播可能触发重复交易虽然EOS的expiration和引用区块机制能防止最坏情况但重试前要检查是否已经上链。多节点故障转移生产环境至少配置两个节点一个为主一个为备。每次调用前先做get_info探活主节点连续失败3次就切到备节点同时把主节点标记为不可用10分钟后再尝试恢复。我用一个简单的静态数组存节点状态崩溃率从原来的每周几次降到了几乎为零。另外要提醒的是不要把私钥放在配置文件里明文存储。更稳妥的是用独立的密钥管理服务PHP进程通过环境变量或加密文件读取。虽然EOS的私钥用于签名不需要参与RPC通信但一旦服务器被入侵私钥泄露就意味着资产被转移。开发包只做签名密钥的生命周期管理必须单独拉出来做。5.4 开发包性能调优大了照样跑得动如果业务量上来PHP做EOS查询的瓶颈通常在RPC请求耗时上。一个账户的完整余额查询要调用好几个接口账户信息、代币余额、资源信息每个接口一次网络往返。优化思路有两个一是批量并行请求。把多个独立的RPC请求用curl_multi同时发比如同时查20个账户的余额总耗时才和查一个差不多。这套逻辑我封装成了EosRpcClient::batchCall(array $requests)传入多个[path, params]数组返回对应响应。二是本地缓存。对于账户资源信息这类变化频率不高的数据加一层内存缓存或Redis缓存TTL设30秒就能显著降低节点压力。链上数据虽然理论上是最新的但在绝大多数业务场景里30秒的延迟完全可接受。缓存过期后回源拉取新数据既能保证新鲜度又能避免请求风暴。我做了一个简单的缓存类键名用节点地址接口路径参数做MD5命中率提升后节点负载直接降了一个量级。写在最后的一点经验折腾这套EOS PHP开发包的过程中我最大的体会是区块链开发真正的难点从来不是语言本身而是对协议细节的精读能力。PHP作为一门“老牌”Web语言在对接EOS这种偏底层的公链时完全够用前提是你愿意花时间去读EOS的Nodeos RPC文档和ABI规范。最后再分享一个小技巧把EOS官方提供的cleos命令当作你的“参考答案”。当PHP代码行为跟预期不符时先用cleos手动执行同样的操作对比请求参数和响应体很快就能定位到是封装层的bug还是自己的逻辑bug。这套开发包我用到现在已经支撑了包括余额批量查询、代币转账、合约交互在内的多个线上功能稳定性比我预想的要好。后续如果再接入其他EOS系链比如测试网或者侧链只需要替换节点地址和链ID就可以复用整套逻辑。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Atlas 300V 24G部署YOLOv5全流程:环境搭建、模型转换与性能调优 2026/9/25 9:44:09

Atlas 300V 24G部署YOLOv5全流程:环境搭建、模型转换与性能调优

前两天看到有人在搜“atlas 300v 24g 是运算加速卡吗”,紧接着还有一条是“atlas部署yolo”。这两个问题拼在一起,基本就是一张昇腾推理卡从“这玩意到底能不能用”到“怎么把它跑起来”的全过程心态写照。我最近正好在Atlas 300V 24G这张卡上把YOLOv5检…

阅读更多 →
C# 项目接入 OpenClaw 的配置骨架:TaoToken 统一 Key 与 settings.json 实战 2026/9/25 9:44:03

C# 项目接入 OpenClaw 的配置骨架:TaoToken 统一 Key 与 settings.json 实战

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

阅读更多 →
如何用AI Agent实现日均万行可用代码:工作流与实战指南 2026/9/25 9:44:03

如何用AI Agent实现日均万行可用代码:工作流与实战指南

1. 当CEO把AI当成"结对程序员"而不是"代码补全器"第一次看到"日均产出一万行可用代码"这个说法,我的反应和大多数人一样:要么是标题党,要么是把AI生成的垃圾代码也算进去了。但仔细拆解这个数字背后的工作模式…

阅读更多 →
Atlas 300V 24G推理卡详解:从入门到YOLO部署实战 2026/9/25 9:43:56

Atlas 300V 24G推理卡详解:从入门到YOLO部署实战

在边缘AI推理这个圈子里,Atlas这个名字最近几年出现的频率越来越高。尤其当“atlas部署yolo”和“atlas 300v 24g 是运算加速卡吗”这两个问题被反复问到的时候,我就知道很多人其实已经拿到了卡,或者正在选型阶段,但对这套工具链还…

阅读更多 →
Atlas 300V 24G推理加速卡部署YOLO完整实战:从环境配置到模型转换与调优 2026/9/25 9:43:56

Atlas 300V 24G推理加速卡部署YOLO完整实战:从环境配置到模型转换与调优

最近收到好几条私信,都是同一个问题:“Atlas 300V 24G 是运算加速卡吗?能不能拿来部署 YOLO?” 问的人多了,我干脆把之前折腾过的整套流程整理出来。这篇文章不是官方文档,是我自己从装卡、配驱动、转模型到…

阅读更多 →
程序员用AI写AI代码:TaoToken统一Key接入Copilot的settings.json配置与验证 2026/9/25 9:43:31

程序员用AI写AI代码:TaoToken统一Key接入Copilot的settings.json配置与验证

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉