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 画的状态流转图,你看一眼就明白了:
这张图里有个细节你可能注意到了:「已转移」到「未认领」用的是虚线。为什么?因为这不是同一个所有权的延续,而是新周期的开始。我在项目中用了一个技巧——给每条记录加一个 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 来定义,每个函数里先检查当前状态是否允许转换。这就是所谓的 「防御式编程」——不让任何非法状态流转发生。
核心总结:所有权模型 = 不可变的数据结构 + 严格的状态机。数据结构保证你能追溯历史,状态机保证你不会出现逻辑漏洞。两者缺一不可。