尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ethers事件监听全指南:原理、过滤器、实战与坑
写区块链开发的人迟早会遇到“事件监听”这个坎。刚开始写合约交互时大多数人都只是调一下读方法、发一笔交易觉得ethers这个库也就这么回事。但等你真正开始做链上监控、做数据同步、做自动交易提醒或者哪怕只是写一个展示最新转账记录的页面你就会发现ethers的事件监听才是把“链上发生的事”和“你的后端逻辑”真正连接起来的那根线。这篇文章我打算从零梳理一遍ethers事件监听的完整思路包括它背后的原理、常用的API、过滤器的写法、历史事件的回放以及我在实际项目里踩过的一堆坑。内容会比较长但读完你基本可以照着写出一套能上生产的监听服务而不是停留在复制粘贴demo的水平。1. 事件监听到底在监听什么1.1 先搞清楚链上事件的产生过程很多刚接触的人会把“事件监听”想得很玄以为像WebSocket推送消息一样以太坊会主动把数据发给你。实际上完全不是这么回事。以太坊本身是个“哑巴”它只负责把数据打包进区块没有人给它打电话它不会主动通知任何人。合约里用event Transfer(address indexed from, address indexed to, uint256 value)声明一个事件时这个事件并不会在链上“发生”然后广播出来。它只是被写入了交易收据transaction receipt里的logs字段。当一笔交易被打包进区块后logs中会保存一串结构化数据其中包括合约地址、事件签名的哈希topic0、以及带indexed标记的参数的哈希topic1、topic2等。所以“事件”的实质是交易日志里的一行记录。ethers做的事情就是不断监听新的区块拿到新交易收据里的logs然后根据你传入的事件签名去匹配、解码最终把解码完的漂亮对象回调给你。理解这一点至关重要因为很多监听问题的排查思路本质都是在问ethers到底有没有拿到那条log、拿到之后有没有正确解码。1.2 ethers在事件监听里的角色定位ethers这个库在事件监听里承担了三层工作作为JSON-RPC客户端向节点订阅或轮询“新区块”消息作为日志解码器把合约定义的事件对象转换成可以匹配的签名和topic作为事件分发器维护一堆监听回调函数在匹配时调用它们也就是说ethers不只是个“接口封装工具”它里面对监听做了完整的抽象。比如你调用contract.on(Transfer, handler)时它并不是简单地把一个字符串丢给节点而是利用合约的ABI来生成事件签名、计算topic0、校验过滤器参数然后在收到新区块时去比对logs。这一套逻辑如果你自己用原生JSON-RPC去实现会遇到很多细节问题比如事件签名的Keccak哈希计算、indexed参数怎么处理、地址填充怎么对齐等等。用ethers能省掉这些但代价是你要理解它的封装边界否则出错了也不知道去哪里排查。注意事件监听依赖的是节点能力。如果你连接的是一个不支持eth_subscribe的公共RPC节点ethers会退化成轮询模式靠不断调用eth_getLogs来模拟订阅。两种模式对性能和实时性影响很大后面我会展开讲。2. 监听API选型与核心思路2.1 on、once、off三个基础方法别用混ethers的Contract对象为我们提供了几个事件监听方法最常用的就是on、once、off和removeAllListeners。它们的区别听起来简单但实际项目里用错场景的情况非常多。on(eventName, listener)持续监听每次事件触发都调用listener。once(eventName, listener)只触发一次触发后自动移除。off(eventName, listener)手动移除某个特定的监听器如果不传listener则移除该事件的所有监听器。removeAllListeners(eventName?)把该合约实例上所有监听都清掉不传参数则清空全部。我见过不少人在做“等待一笔交易确认”这个场景时用了on结果回调被反复触发又因为没有及时off导致逻辑重复执行甚至内存泄漏。而反过来有一些人想持续监控某个价格变化时却用了once触发一次之后整个监控就静默失效了。正确的做法是一次性等待某个事件比如等质押成功、等某个提现确认用once持续的数据流监控比如记录所有转账、监听预言机价格更新用on组件销毁或服务停止时用removeAllListeners做清理另外要特别提一点on方法返回的是合约实例本身所以可以链式调用。这在注册多个事件监听时很顺手。但链式调用在维护时容易漏掉引用如果你需要在其他地方off掉某个监听器最好还是把监听器函数保存在一个命名变量里。2.2 provider层和合约层两层监听别搞混ethers里其实有两层监听一层挂在Provider上监听的是区块、交易、网络状态另一层挂在Contract上监听的是合约事件。很多人一开始会混淆觉得监听合约事件也可以直接写在Provider上或者用provider.on去监听转账。Provider层能监听的事件类型大概有这些block每个新区块产生时触发回调参数是区块号transactionmempool里出现新交易时触发依赖节点支持error底层连接或轮询出错时触发network网络切换时触发Contract层才是我们监听具体合约事件的地方。contract.on(Transfer, ...)这种写法的内部逻辑是ethers会往provider上注册一个log监听器然后对所有logs做过滤匹配到符合当前合约地址、符合当前事件签名的log再执行你的回调。所以合约层监听是构建在provider层监听之上的。理解这一点后你就明白为什么同一时间监听几十个合约事件理论上并不会在provider层新增几十个订阅连接。但是这里有一个很关键的问题如果你直接用provider.on(block)去手动遍历区块再解析logs就绕过了ethers对logs过滤和事件解码的优化性能会差很多正确性也更容易出问题。我的建议是能用Contract监听就不要手动去处理provider.getLogs除非你对性能有极特殊的要求。2.3 过滤器的写法决定了你能不能精准收到事件事件监听里最容易写错的就是过滤器。过滤器本质上是通过topics数组来定位日志。topic0固定是事件签名的keccak哈希topic1、topic2等对应带indexed的参数。在ethers v6里contract.on(contract.filters.Transfer(from, to), handler)这种写法最为稳妥。它不要求你手写哈希或主题直接传参数给filters方法就行。但你要理解这些参数的位置和规则contract.filters.Transfer(from, to)里from和to都是可选参数不写或传null表示不限制这个位置。如果想监听某个特定地址作为转出方就在第一个参数传那个地址。参数必须传正确的类型。地址类型的参数如果传错了格式比如少了校验和ethers可能会在内部帮你转换但有时候会直接匹配不上。其中有一个很隐蔽的坑对address类型的indexed参数节点存储的是40位十六进制字符ethers在解码时会把它们转成40位带0x的地址格式。如果你过滤时传入了带大小写混合的checksum地址ethers会统一转成小写或规范格式去匹配一般情况下没问题。但如果你的地址字符串前后有多余空格、或者看起来像地址但实际不符合长度就会静默地匹配不到任何日志。还有一个关于uint256的坑topic里存储的uint256是32字节左填充的完整数值ethers的过滤器方法会帮你处理但如果你非要手动拼接topics忘记左填充得到的log就是错的。所以我的原则是能用filters方法就绝不用手动字符串既省事又降低出错概率。3. 实操从零监听一个转账事件3.1 先搭好Provider和合约实例不管你有多少年没看过ethers的代码事件监听的起点永远是provider。我们需要一个指向某个节点的连接再通过合约地址和ABI创建合约实例。import { ethers } from ethers; // 这里用公共RPC或者你自己的节点地址 const RPC_URL https://your-rpc-endpoint.example; const provider new ethers.JsonRpcProvider(RPC_URL); const TOKEN_ADDRESS 0xTokenAddressHere; const TOKEN_ABI [ event Transfer(address indexed from, address indexed to, uint256 value), function name() view returns (string), function symbol() view returns (string), function decimals() view returns (uint8) ]; const tokenContract new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, provider);这段代码看起来平平无奇但有三个细节我会反复确认。第一JsonRpcProvider的URL必须支持eth_subscribe或者至少支持高频率的eth_getLogs否则监听会有不同程度的延迟。第二ABI里的事件定义必须和链上合约完全一致尤其是indexed标记多一个少一个都会导致解码出乱数据。第三读取事件监听的合约实例不需要signer但如果后续你要在同一实例上发交易需要传一个有私钥的wallet。很多人在这一步就容易翻车因为复制了一个ABI片段但事件名称拼写和链上不一致或者参数类型写错导致监听一直不触发。排查方法很简单把事件签名拿去算一下keccak然后和真实log里的topic0比对确认一致再往下走。3.2 监听Transfer事件的最简姿势搭建好合约实例后监听事件的操作就一行tokenContract.on(Transfer, (from, to, value, event) { console.log(${from} - ${to}: ${value.toString()}); console.log(区块高度:, event.blockNumber); console.log(交易哈希:, event.log.transactionHash); });这里有几个容易误解的点。回调参数里前几个是事件里声明的参数按照顺序排列。最后一个参数是EventLog对象它包含了这条日志的元信息blockNumber、blockHash、transactionHash、logIndex、transactionIndex以及合约地址等。注意你拿到的金额是一个bigint类型需要用.toString()或相关工具转成普通数字才能展示否则控制台打印出来会是123n这种形式看着也别扭。你可能注意到回调里我用的是value.toString()没有做小数位转换。因为value是原始链上数值不除以decimals的话显示的是最小单位的数量。比如一个18位小数代币转出1.5个链上日志里的数值是1500000000000000000。实际展示时应该除以10^decimals这个转换逻辑建议放在业务层不要在监听回调里反复做。如果想要停止监听调用tokenContract.off(Transfer)即可。如果只移除某个特定回调要把函数引用传给off。这在大型项目里是必须养成的习惯不然回调会越积越多。3.3 带过滤条件的监听只收想要的事件大多数场景下我们并不需要所有Transfer事件只关心和自己业务相关的地址。比如某个DEX的流动性池用户只关心自己的地址有没有被人转走代币。这时候就可以给监听加过滤条件。const MY_ADDRESS 0xMyAddressHere; // 监听转给自己 tokenContract.on(tokenContract.filters.Transfer(null, MY_ADDRESS), (from, to, value, event) { console.log(收到转账: ${value.toString()} wei); }); // 监听从自己转出 tokenContract.on(tokenContract.filters.Transfer(MY_ADDRESS, null), (from, to, value, event) { console.log(转出代币: ${value.toString()} wei); }); // 两个位置都精确指定 tokenContract.on(tokenContract.filters.Transfer(MY_ADDRESS, MY_ADDRESS), (from, to, value, event) { console.log(自己转自己? 少见但技术上存在: ${value.toString()}); });这里使用的filters.Transfer(null, MY_ADDRESS)中第一个null表示对from不限制第二个参数明确锁定接收方。这个API最直观的好处是在RPC层就把多余的log过滤掉了不会有无关流量浪费你的处理时间和带宽。一个比较反直觉的知识点多个topic条件之间是AND关系不是OR关系。如果你想监听“要么是从我转出要么是转给我”的事件不能用filters.Transfer(MY_ADDRESS, MY_ADDRESS)来实现OR因为这种写法是同时满足两个条件也就是自己转自己。正确的做法是注册两个监听器或者收到事件后在回调里自行判断二选一。还有一点过滤参数除了支持地址也可以支持其他indexed参数。比如某些NFT合约的Transfer事件里indexed了tokenId那就可以按tokenId过滤。你可以想象成给数据库查询加索引好处是节点默认可高效处理但前提是字段必须是indexed普通非indexed参数没法在topics层过滤。3.4 历史事件怎么补queryFilter回放监听事件只能收到注册之后新产生的日志但这在业务中往往不够。比如你的服务早上挂了下午修复后中间这段空窗期的事件如果丢掉数据就对不上账了。这时候需要用queryFilter去回放某个区块范围内的事件。// 查询最近1000个区块内的所有Transfer事件 const fromBlock await provider.getBlockNumber() - 1000; const toBlock await provider.getBlockNumber(); const logs await tokenContract.queryFilter( tokenContract.filters.Transfer(), fromBlock, toBlock ); for (const log of logs) { console.log(区块 ${log.blockNumber}: ${log.args.from} - ${log.args.to} ${log.args.value.toString()}); }queryFilter返回的是EventLog数组和on回调里的event对象结构基本一致。注意EventLog.args是个Result对象既可以用log.args[0]按下标访问也可以用log.args.from按参数名访问但具体能否按名访问取决于合约ABI里事件参数是否命名。在实际项目里“先回放历史再启动实时监听”是个很常见的增量同步方案。但这里有个同步窗口问题如果你先查queryFilter查到了块100到块1050然后才调用contract.on开始监听那从1050到监听生效之间的事件可能重复也可能缺失取决于节点subscription的实现缓存情况。稳妥的做法是记录下queryFilter查到的最后一个区块号启动监听后收到新区块事件时先对比区块号如果在回放范围内就跳过再做后续处理。let lastSyncedBlock 0; async function syncHistorical(untilBlock) { const logs await tokenContract.queryFilter(tokenContract.filters.Transfer(), 0, untilBlock); for (const log of logs) { await processLog(log); } lastSyncedBlock untilBlock; console.log(历史同步完成最高区块: ${untilBlock}); } tokenContract.on(Transfer, async (from, to, value, event) { if (event.blockNumber lastSyncedBlock) { return; // 已经处理过的历史事件跳过 } await processLog(event); });这里的lastSyncedBlock作为水位线能防止历史回放和实时监听之间的重叠。不过还要想清楚多实例部署时的并发问题如果同时跑两个实例做同样的监听事件会被处理两遍。这个问题我在后面的章节专门讲。4. 常见问题与排查技巧实录4.1 监听收不到事件从哪一步开始查“为什么我监听到不到事件”可以说是事件监听领域排第一的问题。我见过太多人在这里卡住然后一遍遍重启程序。其实排查思路可以非常有条理地逐层展开。第一步确认事件是否真的发生了。最简单的方式是用区块浏览器看一下目标合约地址有没有对应的转账记录。如果链上压根没发生还监听个什么。第二步检查ABI。事件签名必须和合约代码完全一致尤其是indexed标记少写一个indexedtopics错位数据就对不上。第三步检查合约地址。别笑真的有人把两个不同链的地址填在一起或者复制地址时遗漏了几个字符导致监听了一个不存在的代币合约。第四步检查过滤条件。如果用了filters.Transfer(from, to)要确认参数没有错误地限制死。第五步检查节点连接。如果一个公共RPC节点不支持eth_subscribe有些ethers版本的可靠监听会有问题甚至需要轮询才能收到事件。我把这五步整理成一个快速排查表排查步骤核心检查点常见问题事件是否发生浏览器查看合约logs目标合约根本没有该事件ABI签名indexed标记、参数类型签名不匹配导致解码失败合约地址地址完整、链与地址匹配地址粘贴错误或链搞错过滤条件参数位置、null含义过滤参数限制错了位置节点连通订阅支持、RPC稳定性节点不支持订阅或连接中断这一套排查下来大部分问题都能定位。如果还是查不到那就该怀疑ethers的被动轮询和节点缓存了这时候可以考虑用provider.getLogs直接验证能不能查到该范围内的日志。4.2 回调触发多次或内存一直涨哪里泄漏了另一类高频问题是内存泄漏和回调重复执行。这类问题几乎都是监听器注册和销毁管理混乱导致的。最典型的情况是在一个循环里反复创建合约实例并给每个实例调用on又从来不清理。每次循环都会往provider上注册新的logs处理器旧的回调永远留在内存里。解决的思路很简单如果监听同一个合约的同一个事件只创建一次实例、注册一次回调其他地方需要触发时共享这个事件源而不是反复创建。如果用的是Node.js后端服务尤其要注意进程退出或模块重载时的资源清理。我习惯在应用退出前或者连接失效时调用tokenContract.removeAllListeners(Transfer)和provider.removeAllListeners()把订阅彻底关掉。回调触发多次还有一个容易被忽视的场景你可能同时监听了不同实例上的同一事件。比如在模块A里new Contract后监听了Transfer在模块B里又new了一个Contract实例也在监听Transfer这两个实例的过滤器是独立的但底层都连同一个provider所以事件会被两个filter同时捕获导致回调执行两次。这个问题的本质是监听器的“多点注册”在架构上应该做成单例模式的事件总线。4.3 事件数据解析出来是乱码或字段对不上有一个很典型的错误合约里事件参数顺序是(address from, address to, uint256 value)但我在ABI里写成了(address to, address from, uint256 value)。ABI的定义顺序错了监听回调里的from和to参数就会互换这种错误很难察觉因为数值看起来都正常只有仔细核对地址才会发现方向反了。解决办法就是拿着真实链上日志跟ABI对一遍。具体做法是// 通过交易哈希查看原始日志 const receipt await provider.getTransactionReceipt(0xYourTxHash); console.log(receipt.logs[0].topics); console.log(receipt.logs[0].data);然后再看ethers解码出来的args手动逐项比对确认每一个参数的位置和值都正确。如果topics的长度、内容和预期不符几乎可以断定ABI写错了或者合约地址不是目标合约。另一个常见问题是bigint转换。以太坊的数值类型都比较大ethers v6里统一用bigint表示。有些开发者直接把这个值塞进JSON、写入数据库或者做加减运算结果要么序列化失败要么精度丢失。正确的做法是在最外层逻辑里统一转成字符串或十进制数字。处理时建议写一个工具函数专门把bigint值除以10^decimals并保留指定位数这样整个项目里展示金额的逻辑才能统一。5. 工程化进阶让监听服务稳定跑在生产环境5.1 断线重连与心跳检查线上环境里监听的进程跑着跑着可能静默失效。常见的原因有节点主动断开WebSocket连接、公共RPC限流、网络闪断。而ethers在某些错误下不会自动恢复你会看到进程还在内存还占着但事件再也不来了。要想稳定生产至少要做三件事。第一监听error事件。provider.on(error, (error) { ... })能让你第一时间发现底层连接问题。第二做区块心跳检查。每10到30秒用provider.getBlockNumber()拉一次当前高度如果拿不到或者高度长时间不变就说明连接异常应该重连。第三实现主动重连逻辑销毁旧的provider和合约监听重新创建连接再重新注册所有业务事件监听器。let heartBeatTimer; async function startHeartbeat(provider, onRestart) { heartBeatTimer setInterval(async () { try { const blockNumber await provider.getBlockNumber(); lastSeenBlock blockNumber; } catch (error) { console.error(心跳检测失败准备重连, error); clearInterval(heartBeatTimer); await onRestart(); } }, 15000); }重连逻辑里有一个必须注意的坑旧provider的监听器不会因为你创建了新provider就自动清理如果不手动removeAllListeners会同时存在两套订阅事件处理就会重复。正确的重连流程是“先清理再重建后注册”。5.2 高吞吐场景下的批量处理与去重如果你的业务是监控全网某个大热合约或者同时监听几百个地址的转账事件处理就不能一条条同步慢慢来了。这里我通常采用两个策略。第一个策略是批量处理。拿到一个新区块之后先等几十毫秒把区块内所有相关log攒起来然后用Promise.all并发处理而不是在回调里一个个地顺序等待数据库写入。第二个策略是幂等去重。由于区块链具有重组reorg可能性极少数情况下你会收到被回滚的区块事件或者同一个log在重连后又被重放一次。解决思路是给每条日志生成一个唯一键比如${transactionHash}_${logIndex}在数据库里加唯一索引。写入时如果发现主键冲突就判断当前区块高度是否比已有数据更高如果更高就覆盖否则跳过。这样做可以容忍重复写入不会造成数据重复或错乱。const uniqueKey ${log.transactionHash}_${log.index}; async function processLogWithDeduplication(log) { const existing await db.findByUniqueKey(uniqueKey); if (existing existing.blockNumber log.blockNumber) { return; // 已处理且不是更新数据 } await db.upsert({ uniqueKey, blockNumber: log.blockNumber, from: log.args.from, to: log.args.to, value: log.args.value.toString(), }); }这个策略看起来简单但在生产环境里能救命的。尤其是服务重启后历史回放和实时监听重叠的那段时间幂等逻辑就是数据准确性的最后一道防线。5.3 多合约、多链监听的统一管理当你的项目从监听一个合约变成监听几十个合约甚至跨多条链时代码量会爆炸。最常见的坏味道是每监听一个合约就复制一大段初始化代码监听器散落在各个模块里清理时根本想不起有哪些。我比较推荐的做法是做一个监听管理器把所有合约地址、ABI、事件过滤器、回调函数放在一张配置表里统一管理。启动时遍历配置注册退出时统一清理。const listenersConfig [ { name: tokenA, address: 0xTokenAAddress, abi: TOKEN_ABI, events: [ { signature: Transfer, filter: [null, MY_ADDRESS], handler: handleMyIncomingTransfer } ] }, { name: tokenB, address: 0xTokenBAddress, abi: TOKEN_ABI, events: [ { signature: Transfer, filter: [MY_ADDRESS, null], handler: handleMyOutgoingTransfer } ] } ];这种配置化方式的好处有几个新增一个监听对象只改配置不动代码事件处理器可以按业务拆分文件利于测试清理时遍历配置即可不用担心漏掉某个off。跨链监听其实思路相同只是要给每个链分别创建provider然后把同一个配置表应用到不同provider上。但要注意不同链的区块时间和最终性规则不同监听服务不能用一个固定的延迟去处理所有链。比如某些链出块块很快但重组概率高你要么接受更高的临时数据修正率要么加长确认延迟。这一步没有银弹只能根据业务风险偏好取舍。6. 最后一个我踩过无数次坑的建议如果你问我在ethers事件监听里最值得记住的一条经验是什么我会说永远不要把监听回调写成不可重入的长耗时逻辑。ethers的日志分发是串行的一个回调里的await如果卡住几秒钟后续所有事件的执行都会被阻塞。我早期写过一个同步脚本在监听回调里直接去做复杂的数据聚合结果事件一多整个进程的日志处理队列就堵死事件延迟从几百毫秒涨到几分钟。后面我改成“监听回调只负责把原始事件丢进队列业务处理全部异步消费”问题迎刃而解。具体实现可以用Node.js的事件队列、消息队列或者简单点用一个异步任务列表。总之监听和业务解耦你的服务才算真正活了。再配合上幂等去重、断线重连、配置化管理这套基于ethers的监听方案拿到多数业务场景里都能稳定跑住。
RELATED

相关推荐

Java NIO连接池实战:解决微信API高并发下的粘包与保活问题

Java NIO连接池实战:解决微信API高并发下的粘包与保活问题

1. 为什么我要用Java NIO重写微信API的连接管理先交代一下背景。我之前维护一个微信公众号后台服务,消息推送接口的调用量不算夸张,但有个很头疼的问题:每次请求都新建TCP连接,高峰期连接数直接冲到几千,服务器文件描述…

📅 2026/10/16 7:02:07
警卫岗的IT基本功:从监控系统到故障排查的数字化指南

警卫岗的IT基本功:从监控系统到故障排查的数字化指南

半夜的值班室里,监控墙忽然有三四路画面同时变成蓝屏。对讲机里传来前场巡逻队员的声音:“整个系统都不对劲了,大门那台摄像机也看不了。”你要是以前的警卫班长,大概率会直接打电话叫设备厂家的技术员。但这两年越来越多的单位把…

📅 2026/10/16 7:02:07
Java SpringBoot+SSM幼儿园管理系统:架构、数据库与实战解析

Java SpringBoot+SSM幼儿园管理系统:架构、数据库与实战解析

先说一个背景:很多读者看到"SSM SpringBoot"这种组合会觉得有点怪,因为正常理解里 SSM 是 Spring SpringMVC MyBatis,SpringBoot 是简化配置的一站式框架,两者更像是"二选一"的技术栈,而不是并…

📅 2026/10/16 7:02:07
MORE NEWS

更多资讯

📰

技能包+任务图:三小时上线邮件自动分类与回复服务

1. 需求拆解:一句话怎么变成Agent能执行的“任务图”1.1 先把模糊需求拆成确定性步骤那天下午三点,业务方在群里丢过来一句话:“每天几百封客户邮件进来,能不能自动分个类、标个优先级,再顺手生成个回复草稿&#xff0…

📰

AI应用落地实战:从RAG到Agent架构的工程化演进

做AI应用落地这段时间,我一直保持一个习惯:每隔几周,把手头项目相关的外部动态、内部复盘和技术选型变化整理成一份情报式的笔记。今天这份“AI情报 | 20260930”,正好赶上我们一个智能问答系统从原型转向生产环境的关键节点&…

📰

办公软件AI助手涨价背后:订阅制、数据飞轮与生态护城河的博弈

最近这段时间,我前后收到不少读者来信,问的问题几乎一模一样:自己公司用的办公套件续费怎么突然贵了这么多?仔细一看,涨价的不是主软件,而是后面多出来的一个叫“AI 助手”的按人头服务。有人当场就取消订阅…

📰

LeetCode 383 赎金信:从哈希表到定长数组的频次判定优化

刷题刷到 383 这道“赎金信”,基本是躲不过去的。它被放在哈希表分类里,题目短、场景有意思,核心就一句话:判断字符串 ransomNote 能不能用字符串 magazine 里的字符拼出来,而且 magazine 里每个字符只能用一次。难吗&…

📰

Odoo视图层进阶:看板、图表与视图继承实战指南

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

📰

可编辑Word版阿奇舒勒矛盾矩阵表:TRIZ工程落地核心工具

简介:本资源为TRIZ理论核心工具——阿奇舒勒矛盾矩阵表的标准化Word版本,面向工程师、创新方法研究者及高校工科师生,专用于快速识别与解决技术系统中的工程参数冲突。资源以单个78KB的.docx文件呈现,结构完整,含3939全…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬