消费者对生鲜产品的来源与流通过程日益关注,对生鲜产品的信任需求已从简单的"合格认证"转向"全流程透明化"。
传统的生鲜溯源系统面临数据真实性存疑、信息展示不直观、各环节数据孤岛三大核心痛点,难以满足普通消费者的认知需求。
于是,我们开始思考如何构建一套全流程可验证、数据可视化、信息不可篡改的生鲜溯源系统。通过扫描商品二维码,消费者可直观查看产品从养殖/种植到运输的全过程环境数据(温湿度等)与流转轨迹。
本文将详细介绍如何建一个高交互性的生鲜溯源可视化系统,该系统能够:
系统采用分层架构设计,确保各模块高内聚、低耦合:
各层核心功能:
模块 | 技术栈 | 优势说明 |
---|---|---|
前端框架 | React 18 | 组件化开发,虚拟DOM高效渲染 |
状态管理 | Redux Toolkit | 时间旅行调试,中央化状态管理 |
可视化 | ECharts + D3.js | 丰富的图表类型,流畅动画效果 |
区块链 | Ethereum + Solidity | 智能合约自动执行,数据不可篡改 |
通信协议 | MQTT + HTTPS | 低延迟数据传输,双向通信支持 |
环境数据采集代码示例(树莓派+DHT22):
import Adafruit_DHT
import time
import requests
# 传感器配置
DHT_SENSOR = Adafruit_DHT.DHT22
DHT_PIN = 4
def read_sensor():
"""
读取DHT22温湿度传感器数据并发送到区块链网关
该函数通过Adafruit_DHT库读取连接到指定引脚的DHT22传感器的温度和湿度数据,
如果读取成功,则将数据封装成JSON格式并发送到区块链网关API。
返回值:
int: HTTP响应状态码,如果数据发送成功则返回相应的状态码(如200, 201等)
None: 如果传感器读取失败或数据无效则返回None
"""
# 读取温湿度传感器数据,使用retry方法提高读取成功率
humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)
# 检查传感器数据是否有效
if humidity is not None and temperature is not None:
# 构造要发送的数据载荷
payload = {
"device_id": "RPI-001",
"timestamp": int(time.time()),
"temp": round(temperature, 1),
"humidity": round(humidity, 1),
"location": "养殖场A区-3号棚"
}
# 发送至区块链网关
response = requests.post("https://gateway.blockchain/api/data",
json=payload,
headers={"Authorization": "Bearer API_KEY"})
return response.status_code
return None
关键参数解析:
参数 | 类型 | 说明 |
---|---|---|
read_retry | 方法 | 尝试15次读取,避免硬件通信失败 |
round(x,1) | 数值处理 | 保留1位小数,降低数据噪声 |
device_id | 字符串 | 设备唯一标识,绑定物理位置 |
timestamp | Unix时间 | 精确到秒的时间戳,全球统一标准 |
Solidity合约核心逻辑:
pragma solidity ^0.8.0;
/**
* @title FreshTrace
* @dev 合约用于追踪产品的温湿度等数据信息
*/
contract FreshTrace {
/**
* @dev 数据点结构体,存储产品在某个时间点的环境数据
* @param timestamp 时间戳
* @param temperature 温度数据,实际值需要除以100,因为存储时扩大了100倍
* @param humidity 湿度数据,实际值需要除以100,因为存储时扩大了100倍
* @param location 位置信息
* @param deviceId 设备ID
*/
struct DataPoint {
uint timestamp;
int16 temperature; // 扩大100倍存储
uint16 humidity; // 扩大100倍存储
string location;
string deviceId;
}
// 产品ID到数据点数组的映射,用于存储各产品的追踪数据
mapping(string => DataPoint[]) public productData;
// 合约所有者地址
address private owner;
/**
* @dev 构造函数,设置合约所有者为部署者
*/
constructor() {
owner = msg.sender;
}
/**
* @dev 修饰符,限制只有合约所有者才能调用某些函数
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev 向指定产品添加数据点
* @param productId 产品ID
* @param timestamp 时间戳
* @param temperature 温度数据(扩大100倍存储)
* @param humidity 湿度数据(扩大100倍存储)
* @param location 位置信息
* @param deviceId 设备ID
*/
function addData(
string memory productId,
uint timestamp,
int16 temperature,
uint16 humidity,
string memory location,
string memory deviceId
) public onlyOwner {
productData[productId].push(DataPoint(
timestamp,
temperature,
humidity,
location,
deviceId
));
}
/**
* @dev 获取指定产品的数据点数量
* @param productId 产品ID
* @return uint 返回数据点的数量
*/
function getDataCount(string memory productId) public view returns (uint) {
return productData[productId].length;
}
}
数据存储优化:
temperature
使用int16
存储,实际值=原始值×100。mapping
结构实现O(1)时间复杂度查询。核心功能需求:
核心实现:
import { Timeline, Divider } from 'antd';
import { format } from 'date-fns';
/**
* 渲染追踪时间线组件,用于显示设备的温湿度监测数据
* @param {Object} props - 组件属性
* @param {Array} props.data - 监测数据数组,每个元素包含timestamp、location、deviceId、temperature、humidity等字段
* @returns {JSX.Element} 时间线组件JSX元素
*/
const TraceTimeline = ({ data }) => {
/**
* 渲染单个时间线项
* @param {Object} item - 单条监测数据
* @param {number} item.timestamp - 时间戳(秒)
* @param {string} item.location - 位置信息
* @param {string} item.deviceId - 设备ID
* @param {number} item.temperature - 温度值
* @param {number} item.humidity - 湿度值
* @returns {JSX.Element} 时间线项组件
*/
const renderItem = item => {
// 判断环境数据是否异常:温度超过25°C或湿度超过80%
const isAbnormal = item.temperature > 25 || item.humidity > 80;
return (
<Timeline.Item color={isAbnormal ? 'red' : 'green'} label={format(item.timestamp * 1000, 'yyyy-MM-dd HH:mm')}>
<div className='p-4 bg-white rounded shadow'>
<h3 className={`font-bold ${isAbnormal ? 'text-red-500' : ''}`}>{item.location}</h3>
<p>设备: {item.deviceId}</p>
<div className='grid grid-cols-2 gap-2 mt-2'>
<span>🌡️ 温度: {item.temperature}°C</span>
<span>💧 湿度: {item.humidity}%</span>
</div>
{isAbnormal && <Alert message='环境异常!超过安全阈值' type='warning' />}
</div>
</Timeline.Item>
);
};
// 按时间戳降序排列数据并渲染时间线
return (
<div className='overflow-auto h-[500px]'>
<Timeline mode='alternate'>{data.sort((a, b) => b.timestamp - a.timestamp).map(renderItem)}</Timeline>
</div>
);
};
性能优化点:
overflow-auto
和固定高度实现大数据量下的流畅滚动。sort
),避免渲染过程中计算。Alert
组件,减少DOM节点数量。结合ECharts实现温度/湿度趋势图:
import ReactECharts from 'echarts-for-react';
/**
* 环境数据图表组件
* 用于展示温度和湿度随时间变化的折线图
* @param {Object} props - 组件属性
* @param {Array} props.data - 环境数据数组,每个元素包含timestamp、temperature、humidity字段
* @returns {JSX.Element} 返回渲染的ECharts图表组件
*/
const EnvironmentChart = ({ data }) => {
const option = {
// 配置图表提示框和图例
tooltip: { trigger: 'axis' },
legend: { data: ['温度', '湿度'] },
// 配置X轴为时间轴,设置时间格式化显示
xAxis: {
type: 'time',
axisLabel: { formatter: '{yyyy}-{MM}-{dd} {hh}:{mm}' },
},
// 配置双Y轴,分别显示温度和湿度的数据范围
yAxis: [
{ name: '温度(°C)', max: 40 },
{ name: '湿度(%)', max: 100 },
],
// 配置数据系列,包括温度和湿度两条折线
series: [
{
name: '温度',
type: 'line',
showSymbol: false,
// 将时间戳转换为毫秒并映射温度数据
data: data.map(d => [d.timestamp * 1000, d.temperature]),
markLine: {
// 添加平均温度标记线
data: [{ type: 'average', name: '平均温度' }],
lineStyle: { color: '#f50' },
},
},
{
name: '湿度',
type: 'line',
yAxisIndex: 1,
showSymbol: false,
// 将时间戳转换为毫秒并映射湿度数据
data: data.map(d => [d.timestamp * 1000, d.humidity]),
},
],
};
return <ReactECharts option={option} style={{ height: 400 }} />;
};
交互特性:
基于Leaflet实现运输路径动态展示:
import { MapContainer, TileLayer, Polyline, Marker } from 'react-leaflet';
/**
* 轨迹地图组件,用于显示位置轨迹和标记点
* @param {Object} props - 组件属性
* @param {Array} props.locations - 位置信息数组,每个元素包含lat(纬度)、lng(经度)、timestamp(时间戳)、temp(温度)等属性
* @returns {JSX.Element} 返回包含轨迹线和标记点的地图组件
*/
const TraceMap = ({ locations }) => {
// 将位置信息转换为地图所需的坐标格式
const positions = locations.map(loc => [loc.lat, loc.lng]);
return (
<MapContainer center={positions[0]} zoom={5} scrollWheelZoom={true}>
{/* 添加地图瓦片图层 */}
<TileLayer url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' />
{/* 绘制轨迹线 */}
<Polyline positions={positions} color='#1890ff' weight={4} dashArray='5,10' />
{/* 遍历位置信息,为每个位置添加标记点 */}
{locations.map((loc, i) => (
<Marker
key={i}
position={[loc.lat, loc.lng]}
icon={L.icon({
iconUrl: i === 0 ? 'start.png' : 'point.png',
iconSize: [24, 24],
})}
>
{/* 为标记点添加信息弹窗 */}
<Popup>
停留时间: {format(loc.timestamp, 'MM/dd HH:mm')}
<br />
温度: {loc.temp}°C
</Popup>
</Marker>
))}
</MapContainer>
);
};
地图优化策略:
扫码流程控制:
核心扫码组件:
// 二维码扫描组件
import QrReader from 'react-qr-reader';
/**
* 溯源二维码扫描组件
* 用于扫描特定格式的二维码并获取对应的溯源信息
*/
const TraceabilityScanner = () => {
const [data, setData] = useState(null);
/**
* 处理二维码扫描结果
* @param {string} result - 扫描到的二维码内容
*/
const handleScan = result => {
if (result) {
// 验证二维码有效性
if (/^trace:\d{20}$/.test(result)) {
fetchData(result.split(':')[1]);
}
}
};
/**
* 根据ID获取溯源数据
* @param {string} id - 20位数字的溯源ID
*/
const fetchData = async id => {
try {
const res = await axios.get(`/api/trace/${id}`);
setData(res.data);
} catch (err) {
alert('溯源信息获取失败');
}
};
return (
<div className='scanner-wrapper'>
<QrReader delay={300} onError={console.error} onScan={handleScan} style={{ width: '100%' }} />
{data && <VisualizationPanel data={data} />}
</div>
);
};
设计思路:
react-qr-reader
库实现摄像头调用。trace:20位数字
的规范格式。重点逻辑:
当单件商品数据点超过1000时,采用以下策略保证性能:
数据分页加载:
/**
* 获取产品追溯数据
* @param {string|number} productId - 产品ID
* @param {number} [page=1] - 页码,默认为第1页
* @returns {Promise<Object>} 返回API响应的数据部分
*/
const fetchData = async (productId, page = 1) => {
const res = await axios.get(`/api/trace/${productId}?page=${page}`);
return res.data;
};
// 使用无限滚动组件来实现数据的分页加载
<InfiniteScroll loadMore={fetchNextPage} hasMore={hasMore}>
<Timeline data={currentData} />
</InfiniteScroll>;
Web Worker预处理:
// 在worker中进行数据排序
const worker = new Worker('data-worker.js');
worker.postMessage(rawData);
worker.onmessage = e => setSortedData(e.data);
Canvas替代SVG:
<ReactECharts
option={option}
style={{ height: 400 }}
opts={{ renderer: 'canvas' }} // 强制使用Canvas渲染
/>
WebSocket集成代码:
/**
* 建立WebSocket连接以接收实时数据更新
*
* 该effect hook负责:
* 1. 建立到实时数据API的WebSocket连接
* 2. 监听来自服务器的消息
* 3. 根据产品ID过滤数据并更新状态
* 4. 在组件卸载时清理WebSocket连接
*
* @param {string|number} currentProductId - 当前关注的产品ID,用于过滤接收到的数据
* @param {function} setData - 状态更新函数,用于将新数据添加到现有数据列表的开头
*
* @returns {function} 清理函数,用于关闭WebSocket连接
*/
useEffect(() => {
// 创建WebSocket连接到实时数据API
const ws = new WebSocket('wss://api.fresh-trace.com/real-time');
// 设置消息处理回调函数
ws.onmessage = e => {
// 解析接收到的JSON数据
const newData = JSON.parse(e.data);
// 只处理与当前产品ID匹配的数据
if (newData.productId === currentProductId) {
// 将新数据添加到数据列表的开头
setData(prev => [newData, ...prev]);
}
};
// 返回清理函数,在effect重新执行或组件卸载时关闭WebSocket连接
return () => ws.close();
}, [currentProductId]);
区块链哈希校验:
/**
* 验证数据完整性函数
* 通过计算数据的哈希值并与区块链上存储的哈希值进行比较来验证数据是否被篡改
* @param {Object} data - 需要验证的数据对象,应包含productId属性
* @returns {Promise<boolean>} 返回Promise,resolve时返回布尔值,true表示数据完整,false表示数据被篡改
*/
const verifyData = async data => {
// 计算传入数据的SHA256哈希值
const hash = crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
// 获取区块链合约实例
const contract = blockchain.getContract();
// 从区块链合约中获取对应产品的已存储哈希值
const storedHash = await contract.methods.getHash(data.productId).call();
// 比较计算出的哈希值与存储的哈希值,返回比较结果
return hash === storedHash;
};
设备签名验证:
/**
* 设备端发送数据前签名
* 使用SHA256算法对数据进行签名,确保数据完整性和身份认证
* @param {Object} data - 需要签名的数据对象
* @param {string} privateKey - 用于签名的私钥
* @returns {string} 返回base64编码的签名结果
*/
const signData = (data, privateKey) => {
// 创建SHA256签名对象
const sign = crypto.createSign('SHA256');
// 将数据转换为JSON字符串并更新到签名对象中
sign.update(JSON.stringify(data));
// 使用私钥进行签名,并以base64格式返回签名结果
return sign.sign(privateKey, 'base64');
};
时间戳连续性检查:
/**
* 验证时间线数组中时间戳的顺序是否正确
* @param {Array} timeline - 时间线数组,每个元素应包含timestamp属性
* @throws {Error} 当时间戳顺序异常时抛出错误
*/
const validateTimeline = timeline => {
// 遍历时间线数组,检查相邻元素的时间戳顺序
for (let i = 1; i < timeline.length; i++) {
// 如果当前元素的时间戳小于前一个元素的时间戳,则抛出异常
if (timeline[i].timestamp < timeline[i - 1].timestamp) {
throw new Error('时间戳顺序异常');
}
}
};
按需加载策略
/**
* Visualizer组件
*
* 该组件用于渲染时间线可视化组件,使用React的懒加载机制来动态导入Timeline组件,
* 并在加载过程中显示Spinner加载指示器。
*
* @returns {JSX.Element} 返回包含Suspense边界和Timeline组件的JSX元素
*/
const Timeline = React.lazy(() => import('./Timeline'));
const Visualizer = () => (
<Suspense fallback={<Spinner />}>
<Timeline />
</Suspense>
);
代码分割:
本文详细介绍了构建生鲜溯源可视化系统的完整方案。我们从系统架构设计开始,深入剖析了核心组件的实现细节,包括产品溯源主组件、时间轴可视化组件和传感器数据图表组件。通过模块化的设计思路和清晰的代码结构,我们构建了一个功能完整、用户体验良好的溯源可视化平台。
核心创新点在于:
通过本系统的实施,企业能够为消费者提供透明可信的食品供应链信息,增强品牌信任度,同时也为监管部门提供了有效的溯源工具。系统的模块化设计使其具备良好的扩展性,可以轻松适应不同类型的生鲜产品溯源需求。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。