2、所有权模型设计:数据结构与状态机

所有权模型,说白了就是回答三个问题:谁拥有什么设备?什么时候发生的?现在处于什么状态?

我在做第一个物联网溯源项目时,把所有权想得太简单了。结果上线第三天就出了事故——一台设备同时被两个人认领,数据库里出现了两条 Owner 记录。嗯,从那以后我学乖了,所有权模型必须从数据结构到状态流转都设计清楚。

2.1 所有权数据结构

先看最核心的三个字段。我个人习惯把所有权记录设计成不可变的「事件」而非可变的「状态」。你想想看,如果直接修改 Owner 字段,历史记录就丢了。一旦发生纠纷,你拿什么举证?

核心原则:所有权变更 = 插入新记录,绝不原地修改。

下面是我在多个项目中沉淀下来的数据结构:

// 所有权记录结构
{
  "owner":    "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",  // 当前所有者地址
  "deviceId": "SN-2024-A003",                                // 设备唯一标识
  "timestamp": 1704067200,                                   // Unix 时间戳(秒)
  "signature": "0x..."                                       // 可选:数字签名,防篡改
}

这里我特别想强调 timestamp 的精度问题。我在项目中遇到过,有人用毫秒级时间戳,结果同一秒内产生两条记录,排序时出了歧义。我的建议是:用秒级时间戳 + 自增序号,或者直接用区块链的区块高度 + 交易索引,天然有序。

字段 类型 说明 避坑点
owner address (20字节) 当前所有者的区块链地址 不要用字符串,用校验和地址格式
deviceId string (32字节) 设备唯一标识,如序列号或芯片ID 建议加前缀区分设备类型
timestamp uint256 所有权变更的Unix时间戳 统一用秒级,避免时区问题
signature bytes (65字节) 可选,前所有者对本次转移的签名 不强制,但强烈推荐用于防抵赖

我的小技巧:在 owner 字段里,我习惯用 address(0) 表示「未认领」状态。这样在智能合约里判断起来特别方便:if (record.owner == address(0)) 就是未认领。

2.2 所有权状态机

数据结构定好了,接下来是状态怎么流转。我曾经在一个项目中把状态机设计得太复杂,搞了 7 个状态,结果开发团队自己都记不住。后来我砍到 4 个,清晰多了。

四个核心状态:未认领 → 已认领 → 待转移 → 已转移。注意,这是一个循环,不是终点。

重要提醒:「已转移」之后,设备应该回到「未认领」状态吗?不一定。我建议保留「已转移」作为终态,新所有者重新走认领流程。这样每条设备都有完整的生命周期记录。

下面是我用 SVG 画的状态流转图,你看一眼就明白了:

所有权状态机流转图 所有权状态机流转图 未认领 Unclaimed 已认领 Claimed 待转移 Pending 已转移 Transferred 认领 发起转移 确认转移 新所有者重新认领(新周期) 💡 虚线表示设备进入新生命周期,所有权记录链不断增长,但状态机始终在这四个状态间循环。

这张图里有个细节你可能注意到了:「已转移」到「未认领」用的是虚线。为什么?因为这不是同一个所有权的延续,而是新周期的开始。我在项目中用了一个技巧——给每条记录加一个 version 字段,每次新周期 version +1。这样查询时按 deviceId + version 分组,就能拿到完整的所有权变更历史。

2.3 状态转换规则

光有状态图还不够,你得把转换规则写清楚。我习惯用一张表来约束:

当前状态 触发动作 下一状态 前置条件
未认领 用户发起认领 已认领 设备未被认领;用户签名有效
已认领 所有者发起转移 待转移 仅当前所有者可操作;指定接收方地址
待转移 接收方确认接收 已转移 接收方签名确认;转移未过期(建议设24小时超时)
待转移 所有者取消转移 已认领 仅发起者可取消;转移未完成
已转移 新所有者认领 未认领(新周期) 设备已处于「已转移」状态超过确认期

避坑指南:我曾经在「待转移」状态上吃过亏。用户 A 发起转移给 B,但 B 一直没确认。结果 A 想把设备卖给 C,却发现设备卡在「待转移」状态动不了。后来我加了一个 24 小时超时取消 机制,超过时间自动回退到「已认领」。这个设计后来被好几个项目借鉴了。

2.4 代码实现要点

最后,我贴一段智能合约里的核心逻辑。这不是完整代码,但能让你看到状态机是怎么落地的:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract OwnershipManager {
    enum State { Unclaimed, Claimed, Pending, Transferred }
    
    struct OwnershipRecord {
        address owner;
        string deviceId;
        uint256 timestamp;
        State state;
    }
    
    mapping(string => OwnershipRecord) public records;
    
    // 认领设备
    function claimDevice(string memory deviceId) external {
        OwnershipRecord storage rec = records[deviceId];
        require(rec.state == State.Unclaimed, "设备已被认领");
        
        rec.owner = msg.sender;
        rec.timestamp = block.timestamp;
        rec.state = State.Claimed;
    }
    
    // 发起转移
    function initiateTransfer(string memory deviceId, address newOwner) external {
        OwnershipRecord storage rec = records[deviceId];
        require(rec.owner == msg.sender, "你不是所有者");
        require(rec.state == State.Claimed, "状态不允许转移");
        
        rec.state = State.Pending;
        // 存储接收方地址到临时变量
    }
}

你看,状态机用枚举 enum State 来定义,每个函数里先检查当前状态是否允许转换。这就是所谓的 「防御式编程」——不让任何非法状态流转发生。

核心总结:所有权模型 = 不可变的数据结构 + 严格的状态机。数据结构保证你能追溯历史,状态机保证你不会出现逻辑漏洞。两者缺一不可。