简介一份以JavaScript与Solidity为主、结合Java技术栈背景的去中心化拍卖系统完整源码面向计算机相关专业学生、区块链开发者及毕业设计创作者。项目采用Truffle框架组织涵盖拍卖合约、托管账户与Web交互层适合作为课程设计、毕业设计或项目初期演示使用。压缩包共24个文件整体约203KB核心文件包括10个JavaScript文件前端交互与逻辑、4个Solidity合约链上拍卖、托管与迁移、3个HTML页面界面展示以及JSON、README等辅助配置与说明目录结构清晰便于对照学习。目前平台显示已有98人学习下载具有一定参考价值。源码内含EcommerceStore、EcommerceStoreV2、Escrow、Migrations等合约配置了truffle-config.js与webpack打包脚本可按文档说明直接运行代码均测试通过答辩评审平均分达96分作者表示可远程教学。读者既能整体理解拍卖上链流程也可在现有基础上扩展功能适用于从入门到中高级的区块链项目实践。1. 去中心化拍卖系统把Ebay式竞拍搬上区块链很多人问我拍卖系统为什么非要上区块链传统电商拍卖出价记录、成交价格、资金托管全捏在平台手里买家怕改数据卖家怕赖账平台沾上资金就是最大风险点。用Solidity写拍卖合约把上架、出价、成交、Escrow托管整条链路部署到链上记录不可篡改结算交给托管合约自动执行正好解决毕设和中小团队开发里的信任痛点。这份资源是完整跑通的Truffle项目包含EcommerceStore.sol拍卖主合约、EcommerceStoreV2.sol升级版合约、Escrow.sol托管合约、Truffle迁移脚本、webpack前端工程和种子脚本JavaScript与Solidity负责链上交互Java后端可以对接合约事件做订单同步适合照着部署复现也适合直接改造成自己的拍卖dApp。2. Solidity合约设计从存储布局看懂拍卖的状态机2.1 目录结构与合约职责划分打开项目根目录先别急着跑把文件结构过一遍心里才有底。典型的Truffle项目结构是这样的ebay-truffle-auction-master/ ├── contracts/ │ ├── EcommerceStore.sol │ ├── EcommerceStoreV2.sol │ ├── Escrow.sol │ └── Migrations.sol ├── migrations/ │ ├── 1_initial_migration.js │ ├── 2_deploy_contracts.js │ └── 3_upgrade_ecommerceStore.js ├── app/ │ └── src/ │ ├── index.html │ └── main.js ├── truffle-config.js ├── webpack.config.js ├── seed.js └── package.json其中Migrations.sol是Truffle框架用来记录迁移版本的合约每次执行迁移脚本前Truffle会先读它防止同一脚本被重复执行。EcommerceStore.sol是拍卖主合约定义商品结构体、出价映射和竞拍流程Escrow.sol承担资金托管买家付款后资金先锁在托管合约里确认收货后再放款给卖家。EcommerceStoreV2.sol属于升级版本在原合约基础上改了部分业务逻辑我们放到第六章单独讲。从存储布局角度来看EcommerceStore.sol里最核心的存储变量是items映射和itemCount计数器。Solidity中mapping(uint256 Item) public items会自动生成一个同名的getter函数前端web3可以直接通过items(0)读取第一条商品数据这是调试时最方便的入口不需要额外写查询函数。2.2 拍卖主流程商品上架、出价、成交与资金托管拍卖合约的核心业务可以拆成四个动作上架、出价、成交、确认收货。第一个是上架代码通常长这样function addItem( string memory _name, uint256 _startingPrice, uint256 _duration ) public returns (uint256) { items[itemCount] Item({ id: itemCount, name: _name, seller: msg.sender, startingPrice: _startingPrice, currentPrice: _startingPrice, endTime: block.timestamp _duration, isSold: false, highestBidder: address(0) }); itemCount; emit ItemAdded(itemCount - 1, _name, _startingPrice, _duration); return itemCount - 1; }每次上架都会生成一个新的item id起始价就是当前价截止时间是当前区块时间戳加上拍卖持续时间。这里有个细节itemCount先赋值再自增事件里记录的ID是itemCount - 1也就是刚刚创建的那条记录的下标。如果你在事件监听里直接用itemCount去读数据读到的会是下一条不存在的记录前端列表就会多出一个空白项。出价逻辑是拍卖合约里最容易写错的地方function bid(uint256 _itemId) public payable { Item storage item items[_itemId]; require(block.timestamp item.endTime, 拍卖已结束); require(msg.value item.currentPrice, 出价必须高于当前价); // 退款给上一个出价者 if (item.highestBidder ! address(0)) { payable(item.highestBidder).transfer(item.currentPrice); } item.currentPrice msg.value; item.highestBidder msg.sender; emit BidPlaced(_itemId, msg.sender, msg.value); }这段逻辑的关键在于新出价进入后先把上一次最高价退回给之前的出价者再把当前价更新为新出价。这样做的好处是资金不经过合约长期沉淀每个失败的出价者都能及时拿回款项。但注意如果上一个出价者地址是合约比如某些代理合约transfer可能触发重入风险实际生产环境里通常会用Pull Payment模式改成“先记账、后提取”。做毕设的话用transfer问题不大但答辩时能说出这个改进点分数会明显不一样。成交确认环节一般会用一个finalizePurchase函数让赢家在拍卖结束后确认收货function finalizePurchase(uint256 _itemId) public { Item storage item items[_itemId]; require(block.timestamp item.endTime, 拍卖还未结束); require(msg.sender item.highestBidder, 只有赢家可以确认); require(item.highestBidder ! address(0), 无人出价无法成交); require(!item.isSold, 商品已售出); item.isSold true; emit PurchaseFinalized(_itemId, item.highestBidder, item.currentPrice); }写这个函数时有三个校验缺一不可时间必须已经超过endTime调用者必须是最高出价者商品不能是已售状态。很多人做课程设计只写前两个结果同一个商品被重复确认成交链上数据就出现矛盾。商品状态流转可以概括为下表状态触发条件下一个状态拍卖中addItem上架已成交 / 流拍已成交finalizePurchase确认待资金结算待资金结算Escrow.deposit托管入金已释放已释放Escrow.release放款完成2.3 Escrow托管合约的资金流转路径Escrow.sol扮演的是“中间账户”它的deposit和release两个函数撑起了资金托管的核心逻辑contract Escrow { struct EscrowItem { address buyer; address seller; uint256 amount; bool released; } mapping(uint256 EscrowItem) public escrowItems; function deposit(uint256 _itemId, address _seller) public payable { require(msg.value 0, 托管金额必须大于0); escrowItems[_itemId] EscrowItem({ buyer: msg.sender, seller: _seller, amount: msg.value, released: false }); } function release(uint256 _itemId) public { EscrowItem storage item escrowItems[_itemId]; require(item.buyer msg.sender, 只有买家可以确认收货); require(!item.released, 资金已释放); item.released true; payable(item.seller).transfer(item.amount); } }逻辑不复杂但参数细节值得注意买家调用deposit时必须用msg.sender作为buyer不能传参让别的地址代付release时校验msg.sender item.buyer也就是只有付款方本人能确认收货这样卖家不能自己给自己放款。如果你要在毕设里加“卖家发货”状态可以在中间追加一个ship()函数把状态推进到待确认同时给合约加一个enum状态机变量。3. Truffle环境与迁移部署从配置文件到链上合约地址3.1 truffle-config.js的网络与编译器配置Truffle项目的入口配置文件是truffle-config.js先看常见写法module.exports { networks: { development: { host: 127.0.0.1, port: 7545, network_id: * }, ganache: { host: 127.0.0.1, port: 8545, network_id: 5777 } }, compilers: { solc: { version: 0.8.0, settings: { optimizer: { enabled: true, runs: 200 } } } } };这里的network_id用来匹配目标网络。Ganache CLI默认是5777Ganache GUI的默认端口是7545如果你本机两个都装了建议明确指定network_id否则Truffle可能连错实例。solc版本0.8.0和合约里require、payable的语法是配套的如果换成0.4.x的旧编译器会直接报语法错误。如果要把合约部署到测试网常见做法是在networks里追加一层配置使用HDWalletProvider注入私钥和BSC或Sepolia的RPC节点地址const HDWalletProvider require(truffle/hdwallet-provider); const mnemonic 你的助记词; networks: { sepolia: { provider: () new HDWalletProvider(mnemonic, https://rpc.sepolia.org), network_id: 11155111, gas: 3000000, confirmations: 2, timeoutBlocks: 200 } }gas字段建议手动设置而不是依赖自动估算测试网Dapp浏览器里经常出现估算结果偏差导致部署卡死的情况。confirmations表示等待多少个区块确认后才认为部署成功测试网设2就够主网建议5以上。3.2 逐个迁移脚本的部署顺序与运行逻辑Truffle迁移脚本按文件名前缀数字排序执行。第一步1_initial_migration.js部署Migrations.sol注册表const Migrations artifacts.require(Migrations); module.exports function (deployer) { deployer.deploy(Migrations); };第二步2_deploy_contracts.js部署业务合约const EcommerceStore artifacts.require(EcommerceStore); const Escrow artifacts.require(Escrow); module.exports async function (deployer, network, accounts) { await deployer.deploy(EcommerceStore); await deployer.deploy(Escrow); const storeInstance await EcommerceStore.deployed(); const escrowInstance await Escrow.deployed(); await storeInstance.setEscrowAddress(escrowInstance.address); };部署顺序值得仔细看先部署两个合约拿到各自的deployed地址再把Escrow地址写回EcommerceStore。如果直接在EcommerceStore构造函数里传入Escrow地址一旦Escrow部署失败整个迁移都要回滚重来。Truffle的deployer.deploy返回的是Promise必须await完成之后才能调用deployed()获取地址这里漏掉await是最容易犯的错结果就是拿到一个空地址还定位不到原因。第三步3_upgrade_ecommerceStore.js处理V2升级const EcommerceStoreV2 artifacts.require(EcommerceStoreV2); module.exports async function (deployer, network, accounts) { const escrowInstance await Escrow.deployed(); await deployer.deploy(EcommerceStoreV2, escrowInstance.address); };如果EcommerceStoreV2的构造函数直接接收Escrow地址迁移脚本就是一行部署加一行参数传递。这种升级策略是在链上新增一个合约实例旧数据如果需要保留你得在脚本里手动写数据迁移逻辑具体做法见第六章。3.3 部署完成后的验证与种子数据初始化部署完成后我习惯性跑以下几组命令truffle migrate --network development --reset truffle console --network development--reset会强制从Migrations.sol开始重新执行所有迁移脚本适合合约代码改了之后需要干净重来一遍的场景。如果只是改了前端代码不需要重置链上数据直接重启webpack devServer就行。在console里执行EcommerceStore.deployed()能直接拿到合约实例。合约里有items公共状态变量可以直接读const store await EcommerceStore.deployed(); const item await store.items(0); console.log(item.name, item.currentPrice.toNumber());这里的item.currentPrice返回的是BigNumber对象打印前记得调.toNumber()不然控制台输出带n后缀写进前端状态管理容易出类型问题。seed.js是项目自带的初始化脚本作用是在部署完成后往链上塞一批测试商品省得手动调addItem交易truffle exec seed.js --network developmenttruffle exec和直接node seed.js不一样它会在Truffle环境中执行脚本脚本内部可以通过artifacts.require拿到编译后的合约抽象自动读取已部署地址。如果你用node直接跑会遇到artifacts is not defined报错这是Truffle项目常见的启动误区。4. 前端JavaScript集成webpack构建与拍卖页面交互4.1 webpack.config.js与项目入口前端部分我单独把webpack.config.js拿出来说是因为很多人在这一步翻车。项目里前端源码放在app/src下通过webpack打包成浏览器可执行的bundle。核心配置const path require(path); const CopyWebpackPlugin require(copy-webpack-plugin); module.exports { mode: development, entry: ./app/src/index.js, output: { path: path.resolve(__dirname, dist), filename: bundle.js }, devServer: { static: { directory: path.join(__dirname, app), }, port: 8080, hot: true }, plugins: [ new CopyWebpackPlugin({ patterns: [ { from: ./app/src/index.html, to: index.html } ] }) ] };这里的entry指向app/src/index.js如果项目里入口文件名是main.js或app.js你得先改这里或者把文件重命名。devServer的static配置在webpack 5里改成了对象格式老教程里contentBase已经废弃照旧文章抄配置会直接报contentBase不存在的错。CopyWebpackPlugin的patterns数组写法也是webpack 5之后的规范v4版本用的from/to参数已经不能用了。启动前端开发服务器的命令是npm install npx webpack serve --open4.2 页面调用合约的完整链路前端和合约交互的路径是浏览器 - MetaMask注入的window.ethereum - web3.js - RPC节点 - 链上合约。项目里常见的初始化代码import Web3 from web3; import EcommerceStore from ../../build/contracts/EcommerceStore.json; let web3; let accounts; let storeInstance; if (window.ethereum) { web3 new Web3(window.ethereum); await window.ethereum.enable(); } else { console.error(请先安装MetaMask); } const networkId await web3.eth.net.getId(); const deployedNetwork EcommerceStore.networks[networkId]; storeInstance new web3.eth.Contract( EcommerceStore.abi, deployedNetwork.address );把合约JSON里的abi和deployedAddress读出来再实例化合约对象。这里有个细节build/contracts目录下的JSON是Truffle编译时生成的它包含了networks字段记录的是每个网络上的部署地址。如果你在Ganache上部署后找不到地址先确认网络ID能对上。如果deployedNetwork是undefined最常见的现象是truffle migrate部署成功后没有重新编译或者前端webpack缓存了旧的JSON文件清一下缓存重跑即可。出价按钮的交互逻辑大概是async function placeBid(itemId, bidAmount) { const gasEstimate await storeInstance.methods .bid(itemId) .estimateGas({ from: accounts[0], value: bidAmount }); const result await storeInstance.methods.bid(itemId).send({ from: accounts[0], value: bidAmount, gas: Math.floor(gasEstimate * 1.2) }); console.log(出价成功, result.transactionHash); }参数说明estimateGas先估算gas再乘上1.2倍作为gasLimit防止实际执行时因为链上状态变化导致out of gas。value字段是wei单位如果input框读的用户输入是按以太币来的记得用web3.utils.toWei转换。如果你用Java后端做订单系统可以在这一步监听合约的BidPlaced事件通过web3j的flowable或轮询方式同步到数据库这样前端只负责展示业务数据落库交给Java服务处理。4.3 前端联调时的常见报错处理最常见的报错是Returned values arent serializable。这类问题出现在合约返回了address数组或struct数组web3在解码时格式对不上。我一般会先在合约里给每个字段加独立的getter函数前端一个字段一个字段读不要一次性拉整个结构体。另一个高频问题是Invalid number of arguments for Solidity ABI多半是合约函数签名和前端调用参数个数不匹配。比如合约里bid(uint256 _itemId)只接收一个参数前端传了两个对象进去。排查方式是把ABI打印出来对着函数入口逐个核对。链上特有的问题是nonce冲突用户连续点击出价按钮MetaMask还没广播完上一笔交易第二笔就带着同样的nonce发出去导致其中一笔被拒绝。解法是在前端提交前禁用按钮等交易receipt返回后再放开。async function placeBid(itemId, bidAmount) { const btn document.getElementById(bidBtn); btn.disabled true; try { const result await storeInstance.methods.bid(itemId).send({ from: accounts[0], value: bidAmount }); // 监听事件刷新当前最高价 storeInstance.events.BidPlaced({ filter: { itemId } }) .on(data, event { updateCurrentPrice(event.returnValues.currentPrice); }); } catch (err) { console.error(交易失败, err.message); } finally { btn.disabled false; } }这里用storeInstance.events.BidPlaced做事件监听可以实现多个用户同时竞价时页面价格自动刷新不需要手动轮询。注意事件监听的filter参数是可选的但加上之后可以减少网络传输数据量测试环境下差别不明显主网下能明显降低RPC请求压力。5. 避坑指南区块链拍卖项目最容易翻车的五个地方5.1 迁移脚本执行顺序导致合约地址错乱现象执行完truffle migrate后EcommerceStore里的Escrow地址是0x0调用release时报“地址未设置”。原因第二个迁移脚本里使用deployer.deploy时没有等待部署完成就读取地址或者Truffle回滚后继续执行旧迁移产生了脏状态。解决在迁移脚本里改用async/await确保Escrow部署完成再调用deployed()必要时加--reset强制从Migrations.sol开始重跑。我一般会在脚本开头加一行console.log打印当前网络和账户信息确认连对了再继续。5.2 出价函数没有校验起始价下限导致低价成交现象有用户用0.001 ETH拍下一件标价0.01 ETH的商品前端显示成交价低于卖家设置的起始价。原因bid逻辑只判断msg.value item.currentPrice没有检查起始价下限首次出价时currentPrice还等于startingPrice理论上没问题但如果起始价是0就出现了0元成交。解决在addItem里加require(_startingPrice 0)并在bid函数加require(msg.value item.startingPrice)兜底。这种防御性校验虽然是冗余的但能防止前端bug导致异常数据上链。5.3 webpack 5与copy-webpack-plugin版本不兼容现象启动devServer后页面白屏控制台报Cannot read property tap of undefined。原因webpack 5中copy-webpack-plugin必须用v11及以上版本项目lock文件里锁的还是v6老版本插件API不兼容。解决在package.json里把copy-webpack-plugin升级到^11.0.0删除node_modules和package-lock.json重装。改完仍然白屏的话检查bundle.js是否被正确注入到index.html有时候devServer的静态目录配错导致html加载了旧路径的js。5.4 合约状态变量用memory而非storage数据没存进链上现象调用addItem返回成功但再读items数组发现数据全为空或者只有最后一条数据。原因Item storage item items[itemCount]误写成Item memory item items[itemCount]memory只是临时拷贝修改不会写回链上存储。解决在需要持久化修改时一律使用storage引用。排查时看Solidity编译警告memory的赋值位置编译器会给出提示。我自己的习惯是声明变量时先写修饰符再写类型养成肌肉记忆就不会错。5.5 Ganache账号余额不足导致交易反复失败现象MetaMask注入账号后调用send交易报insufficient funds for gas * price value但Ganache明明显示有100 ETH。原因当前选中账号不是Ganache解锁的账号而是浏览器里MetaMask的另一个空地址或者是Ganache端口连错前端实际连接到了本地其他RPC节点。解决确认MetaMask当前账号与Ganache第一个解锁地址一致检查truffle-config.js的端口号。用web3.eth.getAccounts()拿到的是浏览器选中的账号拿不到就检查window.ethereum的权限是否成功enable。如果页面加载时MetaMask弹窗没出现刷新页面后再试一次。6. 合约升级V2把旧拍卖数据无损迁移的三条实操经验EcommerceStoreV2的存在是该资源里最有学习价值的部分因为它完整演示了合约升级路径。V2相比V1常见改动方向是增加交易手续费、增加卖家黑名单、修改成交规则。这里我不展开具体代码只讲通用做法。第一条经验是“先部署后迁移”。部署V2之前先把V1的items数组全部导出用脚本把每条item的状态读出来写入V2的对应映射。导出时可以批量调用gettertruffle exec scripts/exportItems.js --network developmenttruffle exec可以直接在脚本里用artifacts获取已部署合约实例遍历items把数组转成JSON落盘。导入时在迁移脚本里逐条调用V2的importItem函数注意函数要加onlyAdmin或onlyOwner权限控制防止别人调用导入接口覆盖数据。第二条经验是“代理模式优于复制数据”。如果业务场景允许可以用OpenZeppelin的TransparentUpgradeableProxy方案让所有用户继续访问同一个代理地址只有实现合约地址在升级时变化。但这个项目是教学向的毕设资源直接复制数据成本更低理解起来也更直观。二选一之前要想清楚代理模式省Gas但增加可靠性风险复制数据模式更简单但链上存储成本高。第三条经验是“升级后必须做回归验证”。部署V2后我会从头走一遍完整流程用seed.js初始化一批拍卖商品模拟三个账号分别出价跑到成交确认观察Escrow合约的资金是否按预期释放。把这三步都验证通过才敢说升级没有问题。从那以后我每次部署升级版合约都会强制走一遍“导出—导入—回归”的完整流程哪怕只是改了一个常量也不敢跳过验证。这个习惯帮我避掉了不少线上事故希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
华为eSight路由交换运维:设备纳管、告警管理与配置备份 1. 从“设备台账”说起:eSight 到底在解决什么层面的问题早些年我接手过一个园区网的运维活儿,核心交换机、汇聚、接入加起来四十多台,品牌还不统一。最原始的“管理”方式说出来你可能不信:一张 Excel 台账,记着每台设… · 2026/11/3 12:53:44
一阶齐次线性微分方程解法与应用详解 1. 一阶齐次线性微分方程的基本概念 微分方程是描述变量之间变化关系的数学工具,在物理学、工程学和经济学等领域有广泛应用。一阶齐次线性微分方程是最基础也最重要的微分方程类型之一,其标准形式为: dy/dx P(x)y 0 这个方程被称为"… · 2026/11/3 2:21:08
一次性在线工具使用指南:文本编码、图片压缩与本地处理安全 1. 收藏夹里那堆"奇怪"的在线工具,到底解决了什么问题有段时间我整理浏览器书签,发现一个很奇怪的现象:真正每天用的工具类网站不超过五个,剩下几十个全都是"奇怪的在线工具"——有的能把一张图压到原体积的十… · 2026/11/3 10:24:28
只把中国当工厂,是个伪命题 《只把中国当工厂,是个伪命题》——市场决定企业在哪里卖货,创新效率决定企业在哪里下注过去三十年,外资来中国是为了卖;未来十年,外资来中国是为了想。先说一件有点反常识的事。今年前三季度,高技术产业实… · 2026/11/6 2:03:32
百智办公最好的用法,可能是让你少用它 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 2:03:32
海外市场调研怎么做?从需求梳理、问卷设计到海外样本回收的完整流程 随着企业海外业务的持续推进,市场调研正在成为越来越重要的决策工具。无论是了解目标用户画像、收集产品反馈、评估价格接受度,还是测试广告卖点、比较竞品表现,企业都需要通过系统化的调研方式,获得来自海外消费者的真实反馈。一… · 2026/11/6 2:03:32
把百智办公接进客服,最难的是一句「不知道」 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 2:03:32
笔记++23 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 2:03:32
#11.4 认识time和rand,自己尝试写小游戏(未果) /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 2:02:32
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
三菱PLC数据采集到数据库的时序设计:Python+MC协议实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
ASIC主模式RTL设计:状态机与三态驱动的流片级实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
STM32嵌入式C++开发:GDB调试与Renode仿真实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07