一、Order结构体:订单的灵魂

做链上订单簿,第一个要解决的问题就是:订单长什么样?

我个人习惯,先把结构体定义清楚。结构体就是订单的骨架,后面所有逻辑都围着它转。

// 限价单结构体
struct Order {
    uint256 id;           // 订单ID,全局唯一
    address trader;       // 交易者地址
    bool isBuy;           // true=买单,false=卖单
    uint256 price;        // 限价价格,单位:wei
    uint256 amount;       // 原始数量
    uint256 filled;       // 已成交数量
    uint256 timestamp;    // 创建时间戳
    uint256 expireTime;   // 过期时间(0=永不过期)
    bool cancelled;       // 是否已撤销
}

嗯,这里要注意几个细节:

  • filled字段:记录已成交数量,而不是用剩余数量。为什么?因为做清算时,用filled累加更安全,不容易出现精度误差
  • expireTime:我建议加上。我在做永续合约时,遇到过僵尸订单一直挂在链上,gas费白烧
  • cancelled:软删除标记。链上数据不能真删,标记一下就行

核心原则:结构体字段要够用,但别冗余。每个字段都要有明确的业务含义,别为了"以后可能用到"就乱加字段。

二、OrderBook合约:订单簿的大脑

结构体定义好了,接下来就是怎么存、怎么查、怎么改。

我设计合约时,喜欢把存储层和逻辑层分开。但初学者可以先放一起,好理解。

contract OrderBook {
    // 订单计数器
    uint256 public nextOrderId = 1;
    
    // 核心存储:订单ID → Order结构体
    mapping(uint256 => Order) public orders;
    
    // 用户维度的订单列表
    mapping(address => uint256[]) public userOrders;
    
    // 价格维度的订单列表(用于撮合)
    mapping(uint256 => uint256[]) public priceLevelOrders;
    
    // 所有订单ID列表(用于遍历)
    uint256[] public allOrderIds;
}

你看,这里用了三个mapping加一个array。为什么这么设计?

设计思路

  • orders mapping:主存储,O(1)查询。这是最核心的
  • userOrders mapping:方便用户查自己的订单。我在做DEX时,用户经常问"我的单子呢?"——有这个就快多了
  • priceLevelOrders mapping:按价格聚合,撮合时直接扫这个列表,不用遍历所有订单
  • allOrderIds array:全局遍历用。虽然gas贵,但管理员做数据统计时有用

三、mapping vs array:怎么选?

说实话,这是Solidity里最让人纠结的选择之一。

特性 mapping array
查询速度 O(1),极快 O(n),慢
遍历能力 不能遍历 可以遍历
存储成本 低(只存有值的key) 高(连续存储)
删除操作 直接delete,简单 需要移位或标记删除
适用场景 按ID/地址查询 需要顺序访问

我的经验是:能用mapping就别用array。为什么?

有一次我写一个做市商机器人,用array存订单列表。结果订单量到5000笔时,遍历一次要烧掉0.1 ETH的gas。老板脸都绿了。

但array也不是一无是处。比如你要展示"最近10笔成交",用array就很自然。

四、实战:订单CRUD操作

光说不练假把式。我们来看看实际怎么操作这些数据结构。

4.1 创建订单

function placeOrder(
    bool _isBuy,
    uint256 _price,
    uint256 _amount,
    uint256 _expireTime
) external returns (uint256) {
    require(_amount > 0, "Amount must be > 0");
    require(_price > 0, "Price must be > 0");
    
    uint256 orderId = nextOrderId++;
    
    orders[orderId] = Order({
        id: orderId,
        trader: msg.sender,
        isBuy: _isBuy,
        price: _price,
        amount: _amount,
        filled: 0,
        timestamp: block.timestamp,
        expireTime: _expireTime,
        cancelled: false
    });
    
    // 同步更新索引
    userOrders[msg.sender].push(orderId);
    priceLevelOrders[_price].push(orderId);
    allOrderIds.push(orderId);
    
    emit OrderPlaced(orderId, msg.sender, _isBuy, _price, _amount);
    
    return orderId;
}

避坑指南:我曾经犯过一个错——创建订单时忘了更新priceLevelOrders。结果撮合引擎扫不到新订单,用户挂单半小时都没成交。后来加了事件监听才补上。

4.2 撤销订单

function cancelOrder(uint256 _orderId) external {
    Order storage order = orders[_orderId];
    require(order.trader == msg.sender, "Not your order");
    require(!order.cancelled, "Already cancelled");
    require(order.filled < order.amount, "Already filled");
    
    order.cancelled = true;
    
    emit OrderCancelled(_orderId);
}

注意这里用的是storage引用,不是memory。直接修改storage,gas更省。我刚开始写Solidity时老用memory,后来发现多花了好多gas。

4.3 查询活跃订单

function getActiveOrders(address _trader) 
    external 
    view 
    returns (Order[] memory) 
{
    uint256[] storage orderIds = userOrders[_trader];
    
    // 先统计活跃订单数量
    uint256 activeCount = 0;
    for(uint256 i = 0; i < orderIds.length; i++) {
        Order storage order = orders[orderIds[i]];
        if(!order.cancelled && order.filled < order.amount) {
            activeCount++;
        }
    }
    
    // 再填充结果数组
    Order[] memory activeOrders = new Order[](activeCount);
    uint256 index = 0;
    for(uint256 i = 0; i < orderIds.length; i++) {
        Order storage order = orders[orderIds[i]];
        if(!order.cancelled && order.filled < order.amount) {
            activeOrders[index] = order;
            index++;
        }
    }
    
    return activeOrders;
}

你想想看,为什么这里要遍历两次?

因为Solidity的数组长度是固定的。你不知道活跃订单有多少个,就得先数一遍。虽然gas多花点,但这是最安全的做法。

五、知识体系总览

下面这张图,是我做这个章节时画的。它把整个订单簿的数据结构串起来了:

链上订单簿数据结构总览 Order 结构体 id, trader isBuy, price amount, filled timestamp, expire cancelled 存储层:mapping + array 组合 orders mapping orderId → Order userOrders mapping address → orderId[] priceLevel mapping price → orderId[] allOrderIds array 全局遍历用 核心原则:mapping主存 + array辅助索引,兼顾查询效率和遍历需求

六、一些心里话

数据结构设计这事,说白了就是trade-off的艺术。

mapping快但不能遍历,array能遍历但慢。你想想看,如果订单量上到10万笔,用array遍历一次,gas费够你吃一顿火锅了。

我个人的建议是:

  • 核心数据用mapping:订单详情、用户余额这些
  • 索引数据用array:但控制长度,别超过1万
  • 聚合数据用mapping of array:比如按价格分组,既快又好查

嗯,差不多就这些。数据结构是地基,地基稳了,上面的撮合逻辑、清算逻辑才能跑得顺。