养殖 SaaS 笔记:乐橙 IoT 物模型管环境,视频 OpenAPI 管回看

养殖 SaaS 笔记:乐橙 IoT 物模型管环境,视频 OpenAPI 管回看

eo.png` · 栏舍传感 + 摄像 → 一体看板


凌晨两点,养殖场值班群炸了:「3 号栋氨气超标,赶紧开风机!」

环控同事在传感平台里看到数值飙红,却要再切到乐橙 App / 另一套监控客户端找画面------SN 对不上、栋号对不上,十分钟后才对准镜头 。真正的问题不是没有传感器、也不是没有摄像机,是 环境数据与视频回传不在同一条业务台账里

后来我们在 乐橙开放平台listDeviceDetailsByPageproductId 分轨 :有值走 IoT 物模型 读温湿度/控风机;无值走视频 OpenAPI / 轻应用 出画。下面是可落地的「智慧养殖一体化」笔记。


养殖现场的两类设备,两种云协议

现场资产 业务诉求 云侧路径 列表线索
温湿度 / 氨气 / 料位 / 继电器 读数、阈值、告警、远程启停 IoT 物模型 productId 有值
栏舍 / 通道 IPC(乐橙摄像机) 复核现场、留证、夜间巡查 视频 OpenAPI / 轻应用 无 productId;能力集含动检等

文档约定(见 IoT 物模型概述):productId 有值 = IoT 设备,可调:

接口 用途
getProductModel 拉 Property / Service / Event 模板
getIotDeviceProperties 读属性(参数名 properties = ref 列表)
setIotDeviceProperties 写属性
iotDeviceControl 调 Service(如启停风机)

视频侧仍用 bindDeviceLive / getKitToken 等现行接口出画;动检走 setMessageCallbackalarm,物模型事件订 iot ------两条推送可并存

一体化不是「一个 MQTT 打天下」

错误做法 后果
摄像机也硬调物模型 productId,接口失败
传感器另写私有 HTTP,视频另写一套 栋号映射永久分叉
只做告警短信,不回传画面 值班不敢远程决策
只做九宫格监控,不读环境 看起来「智慧」,其实是传统安防

正确姿势:

text 复制代码
一条设备台账(栋号 / 栏舍)
  ├─ iot 轨:物模型 Property / Service / Event
  └─ video 轨:直播 / 回放 / 动检
同一告警中心:氨气超标 → 关联同栋摄像机 kitToken/HLS

架构

text 复制代码
┌─ 栏舍传感(温湿度/氨气/继电器)─┐   ┌─ 栏舍 IPC ─┐
│ productId 有值 → IoT 物模型      │   │ 视频 OpenAPI │
└────────────────┬────────────────┘   └──────┬─────┘
                 ▼                           ▼
          listDeviceDetailsByPage(统一同步)
                 │
                 ▼
            分轨:productId?
           /                \
        iot 轨              video 轨
   getProductModel       bindDeviceLive
   get/set Properties    或 getKitToken
   iotDeviceControl      setMessageCallback(alarm)
   callbackFlag=iot
           \                /
                 ▼
         养殖看板:环境曲线 + 一键回看
能力 谁负责 说明
设备进池 绑定 / 配网 乐橙 App 能看 ≠ 开发者资产池有设备
是否 IoT productId 有值才调物模型
ref 从哪来 getProductModel 用数字字符串 ref,不用英文 identifier
布尔值 物模型约定 统一 0 / 1,不是 JSON true/false
栋号映射 你的系统 云不替你建「3 号栋」

边界 :物模型 不能 替代 IPC 出画;视频接口 不能 读写温湿度。一体化发生在 你的 BFF 与看板,不是强行揉成一个 API。


Step 0 · 调用壳

javascript 复制代码
// lib/platform-call.js
import crypto from 'node:crypto';
import { randomUUID } from 'node:crypto';

export function calcSign(time, nonce, appSecret) {
  return crypto
    .createHash('md5')
    .update(`time:${time},nonce:${nonce},appSecret:${appSecret}`, 'utf8')
    .digest('hex');
}

export async function platformCall(method, params = {}) {
  const time = Math.floor(Date.now() / 1000);
  const nonce = randomUUID();
  const body = {
    system: {
      ver: '1.0',
      appId: process.env.APP_ID,
      sign: calcSign(time, nonce, process.env.APP_SECRET),
      time,
      nonce,
    },
    id: randomUUID(),
    params,
  };
  const res = await fetch(`${process.env.OPENAPI_BASE}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (json.result?.code !== '0') {
    throw new Error(`[${json.result?.code}] ${json.result?.msg}`);
  }
  return json.result.data;
}

let cached = { token: null, exp: 0 };
export async function adminToken() {
  if (Date.now() < cached.exp) return cached.token;
  const { accessToken, expireTime } = await platformCall('accessToken', {});
  cached = { token: accessToken, exp: Date.now() + (expireTime - 300) * 1000 };
  return accessToken;
}
bash 复制代码
# .env.example
APP_ID=your_app_id
APP_SECRET=your_secret
OPENAPI_BASE=https://openapi.lechange.cn/openapi
CALLBACK_URL=https://farm.example.com/hooks/events
# 氨气告警阈值(示例):超过则关联同栋视频
AMMONIA_THRESHOLD=25

乐橙开放平台 创建应用后,控制台可拿到 appId / appSecret。传感类 IoT 设备与乐橙摄像机均需进入同一开发者资产池。


Step 1 · 栋号台账 + productId 分轨

javascript 复制代码
// config/barns.js
/** deviceId → 栋号;传感与摄像必须进同一张表 */
export const deviceBarnMap = {
  // 'SENSOR_SN_001': 'BARN_03',
  // 'CAM_SN_001': 'BARN_03',
};

export function resolveBarn(deviceId, deviceName) {
  if (deviceBarnMap[deviceId]) return deviceBarnMap[deviceId];
  const m = String(deviceName || '').match(/^(BARN_\d+)/i);
  return m ? m[1].toUpperCase() : 'UNMAPPED';
}
javascript 复制代码
// services/classify.js
export function classifyDevice(dev) {
  const isIot = Boolean(dev.productId);
  const ability = [
    dev.deviceAbility,
    ...(dev.channelList || []).map((c) => c.channelAbility),
  ]
    .filter(Boolean)
    .join(',');

  let kind = 'unknown';
  if (isIot) {
    // 养殖侧常见:环控传感 / 继电器;以物模型 name 再细分
    kind = /relay|fan|风机|开关/i.test(`${dev.deviceModel} ${dev.deviceName}`)
      ? 'actuator'
      : 'sensor';
  } else if (/AlarmMD|AudioTalk|PTZ|WLAN/i.test(ability) || !isIot) {
    kind = 'camera';
  }

  return {
    deviceId: dev.deviceId,
    name: dev.deviceName,
    productId: dev.productId || null,
    status: dev.deviceStatus,
    channelId: String(dev.channelList?.[0]?.channelId ?? 0),
    track: isIot ? 'iot' : 'video',
    kind,
    ability,
  };
}
javascript 复制代码
// services/sync-farm-devices.js
import { platformCall, adminToken } from '../lib/platform-call.js';
import { classifyDevice } from './classify.js';
import { resolveBarn } from '../config/barns.js';

export async function syncFarmDevices({ pageSize = 50 } = {}) {
  if (pageSize < 1 || pageSize > 50) throw new Error('pageSize 1..50');
  const token = await adminToken();
  const rows = [];
  let page = 1;

  for (;;) {
    const data = await platformCall('listDeviceDetailsByPage', {
      token,
      page,
      pageSize,
      source: 'bindAndShare',
    });
    const list = data.deviceList ?? [];
    if (!list.length) break;

    for (const d of list) {
      const base = classifyDevice(d);
      rows.push({
        ...base,
        barnCode: resolveBarn(d.deviceId, d.deviceName),
      });
    }
    if (list.length < pageSize) break;
    page += 1;
    await new Promise((r) => setTimeout(r, 200));
  }
  return rows;
}
javascript 复制代码
// scripts/run-sync-farm.js
import 'dotenv/config';
import { syncFarmDevices } from '../services/sync-farm-devices.js';

const rows = await syncFarmDevices();
console.table(
  rows.map((r) => ({
    barn: r.barnCode,
    id: r.deviceId,
    track: r.track,
    kind: r.kind,
    productId: r.productId,
    status: r.status,
  })),
);
console.log(
  'UNMAPPED',
  rows.filter((r) => r.barnCode === 'UNMAPPED').map((r) => r.deviceId),
);

踩坑 A :全是摄像机、无 productId ------ 正常 ,别对视频设备调 getProductModel

踩坑 B :传感与摄像栋号分属两张 Excel ------ 告警永远对不准镜头;必须同一 barnCode

踩坑 C :乐橙 App 能看、列表为空 ------ 未按接入指南绑进开发者资产池。


Step 2 · IoT 轨:拉物模型,用 name 找 ref

接口见 getProductModel / getIotDeviceProperties

属性/服务通信用的是 数字字符串 ref ,不是英文 identifier。养殖产品各异,禁止写死别人家的 ref ------先 getProductModel,再按中文名模糊匹配。

javascript 复制代码
// services/iot-thing.js
import { platformCall, adminToken } from '../lib/platform-call.js';

const modelCache = new Map();

export async function loadProductModel(productId) {
  if (modelCache.has(productId)) return modelCache.get(productId);
  const model = await platformCall('getProductModel', {
    token: await adminToken(),
    productId,
  });
  modelCache.set(productId, model);
  return model;
}

/** 按名称关键字找 Property ref;找不到返回 null */
export function findPropertyRef(model, keywords = []) {
  const props = model.properties || [];
  for (const kw of keywords) {
    const hit = props.find((p) =>
      String(p.name || p.description || p.identifier || '').includes(kw),
    );
    if (hit?.ref != null) return String(hit.ref);
  }
  return null;
}

export function findServiceRef(model, keywords = []) {
  const services = model.services || [];
  for (const kw of keywords) {
    const hit = services.find((s) =>
      String(s.name || s.description || s.identifier || '').includes(kw),
    );
    if (hit?.ref != null) return String(hit.ref);
  }
  return null;
}

/** 官方参数名是 properties(ref 数组),不是 refs */
export async function readProperties(deviceId, productId, propertyRefs) {
  return platformCall('getIotDeviceProperties', {
    token: await adminToken(),
    deviceId,
    productId,
    properties: propertyRefs.map(String),
  });
}

export async function writeProperties(deviceId, productId, content) {
  return platformCall('setIotDeviceProperties', {
    token: await adminToken(),
    deviceId,
    productId,
    content, // { "3301": 1 } ------ key 为 ref 字符串
  });
}

export async function invokeService(deviceId, productId, ref, content = {}) {
  return platformCall('iotDeviceControl', {
    token: await adminToken(),
    deviceId,
    productId,
    ref: String(ref),
    content,
  });
}
javascript 复制代码
// scripts/probe-sensor.js
import 'dotenv/config';
import {
  loadProductModel,
  findPropertyRef,
  readProperties,
} from '../services/iot-thing.js';

const productId = process.env.DEMO_PRODUCT_ID;
const deviceId = process.env.DEMO_SENSOR_SN;

const model = await loadProductModel(productId);
const tempRef = findPropertyRef(model, ['温度', '温']);
const humiRef = findPropertyRef(model, ['湿度', '湿']);
const ammoRef = findPropertyRef(model, ['氨', '氨气']);

const refs = [tempRef, humiRef, ammoRef].filter(Boolean);
if (!refs.length) {
  console.log('未匹配到环境属性,打印全部 properties.name/ref 供人工配置:');
  console.table(
    (model.properties || []).map((p) => ({
      ref: p.ref,
      name: p.name,
      access: p.accessMode,
    })),
  );
  process.exit(1);
}

const state = await readProperties(deviceId, productId, refs);
console.log({
  status: state.status,
  properties: state.properties,
  mapped: { tempRef, humiRef, ammoRef },
});

物模型三维(见 IoT 物模型概述):

text 复制代码
Property --- 运行状态(温度、湿度、开关)
Service  --- 可调用方法(启停风机、投料一次)
Event    --- 运行事件(可经 setMessageCallback 的 iot 推送)

踩坑 Dproperties 传 identifier 英文名 → 读失败;必须传 ref

踩坑 E :bool 写成 true → 应按文档传 0 / 1

踩坑 F :不同 productId 共用一套 ref 缓存 ------ 风机和温湿度探头模板不同,按 productId 分缓存


Step 3 · IoT 轨:阈值与远程启停(先确认再下发)

写属性 / 调服务见 setIotDeviceProperties · iotDeviceControl

javascript 复制代码
// services/farm-control.js
import {
  loadProductModel,
  findServiceRef,
  findPropertyRef,
  writeProperties,
  invokeService,
} from './iot-thing.js';

/**
 * 示例:打开风机。
 * 有的产品是写 bool 属性,有的是调 Service------以 getProductModel 为准。
 */
export async function startFan({ deviceId, productId, confirmToken }) {
  if (confirmToken !== process.env.CONTROL_CONFIRM_TOKEN) {
    throw new Error('二次确认失败:拒绝下发');
  }

  const model = await loadProductModel(productId);
  const svc = findServiceRef(model, ['风机', '通风', '开启', '启动']);
  if (svc) {
    return invokeService(deviceId, productId, svc, {});
  }

  const sw = findPropertyRef(model, ['风机', '继电器', '开关']);
  if (!sw) throw new Error('物模型中未找到风机相关 Service/Property');
  // bool:1 开 0 关
  return writeProperties(deviceId, productId, { [sw]: 1 });
}
javascript 复制代码
// scripts/start-fan.js
import 'dotenv/config';
import { startFan } from '../services/farm-control.js';

const r = await startFan({
  deviceId: process.env.DEMO_ACTUATOR_SN,
  productId: process.env.DEMO_ACTUATOR_PRODUCT_ID,
  confirmToken: process.env.CONTROL_CONFIRM_TOKEN,
});
console.log(r);

踩坑 G :看板做「一键开风机」无审计 ------ 养殖现场误触代价高,BFF 必须 二次确认 + 操作日志(谁、哪栋、何时)


Step 4 · 视频轨:同栋回传(HLS 最小闭环)

传感告警要「点开即看」,视频轨用现行 bindDeviceLive 即可;Web 控件场景可换成 getKitToken + ImouPlayer

javascript 复制代码
// services/video-live.js
import { platformCall, adminToken } from '../lib/platform-call.js';

export async function getBarnLive(deviceId, channelId = '0', streamId = 1) {
  return platformCall('bindDeviceLive', {
    token: await adminToken(),
    deviceId,
    channelId: String(channelId),
    streamId, // 1 标清:场区弱网更稳
    liveMode: 'proxy',
  });
}
javascript 复制代码
// services/barn-link.js
/** 同一 barnCode 下关联摄像,供告警页一键出画 */
export function camerasOfBarn(devices, barnCode) {
  return devices.filter(
    (d) => d.barnCode === barnCode && d.track === 'video' && d.status === 'online',
  );
}

export function sensorsOfBarn(devices, barnCode) {
  return devices.filter(
    (d) => d.barnCode === barnCode && d.track === 'iot',
  );
}

Step 5 · 推送并轨:iot + alarm,先 200 再处理

登记回调见 setMessageCallback;流程说明见 事件消息推送

javascript 复制代码
// scripts/enable-farm-callback.js
import 'dotenv/config';
import { platformCall, adminToken } from '../lib/platform-call.js';

const token = await adminToken();
await platformCall('setMessageCallback', {
  token,
  status: 'on',
  callbackUrl: process.env.CALLBACK_URL,
  // 物模型事件 + 摄像动检/人形;上下线可按需加 deviceStatus
  callbackFlag: 'iot,alarm',
  basePush: '2',
});
console.log(await platformCall('getMessageCallback', { token }));
javascript 复制代码
// server/farm-bridge.js
import express from 'express';
import 'dotenv/config';
import { syncFarmDevices } from '../services/sync-farm-devices.js';
import { camerasOfBarn } from '../services/barn-link.js';
import { getBarnLive } from '../services/video-live.js';
import {
  loadProductModel,
  findPropertyRef,
  readProperties,
} from '../services/iot-thing.js';

const app = express();
app.use(express.json({ limit: '1mb' }));

let devices = [];
async function refresh() {
  devices = await syncFarmDevices();
}
await refresh();
setInterval(() => refresh().catch(console.error), 10 * 60 * 1000);

app.post('/hooks/events', (req, res) => {
  res.status(200).send('ok'); // ★ 必须先应答
  queueMicrotask(() => handle(req.body).catch(console.error));
});

async function handle(msg) {
  const msgType = msg?.msgType;
  if (!msgType) return;

  // 视频动检:记日志即可;养殖白天噪声大,别当氨气告警
  if (msgType === 'videoMotion' || msgType === 'human') {
    console.log('VIDEO_EVENT', msg.did || msg.deviceId, msgType);
    return;
  }

  // 物模型事件(字段以事件消息格式为准,常见 msgType=iotEvent)
  if (msgType === 'iotEvent' || msg.pid) {
    console.log('IOT_EVENT', JSON.stringify({
      did: msg.did || msg.deviceId,
      pid: msg.pid || msg.productId,
      event: msg.content?.event,
      time: msg.time || msg.utcTime,
    }));
  }
}

/** 值班看板:某栋环境快照 + 可播摄像头列表 */
app.get('/api/v1/barns/:barnCode/dashboard', async (req, res) => {
  const { barnCode } = req.params;
  const cams = camerasOfBarn(devices, barnCode);
  const sensors = devices.filter(
    (d) => d.barnCode === barnCode && d.track === 'iot' && d.kind === 'sensor',
  );

  const readings = [];
  for (const s of sensors) {
    if (!s.productId || s.status !== 'online') continue;
    const model = await loadProductModel(s.productId);
    const refs = [
      findPropertyRef(model, ['温度', '温']),
      findPropertyRef(model, ['湿度', '湿']),
      findPropertyRef(model, ['氨', '氨气']),
    ].filter(Boolean);
    if (!refs.length) continue;
    const state = await readProperties(s.deviceId, s.productId, refs);
    readings.push({
      deviceId: s.deviceId,
      status: state.status,
      properties: state.properties,
    });
  }

  res.json({
    barnCode,
    readings,
    cameras: cams.map((c) => ({
      deviceId: c.deviceId,
      name: c.name,
      channelId: c.channelId,
    })),
  });
});

/** 告警联动:取同栋第一路在线摄像直播 */
app.get('/api/v1/barns/:barnCode/live', async (req, res) => {
  const cams = camerasOfBarn(devices, req.params.barnCode);
  if (!cams.length) return res.status(404).json({ msg: '该栋无在线摄像机' });
  const cam = cams[0];
  const live = await getBarnLive(cam.deviceId, cam.channelId, 1);
  res.json({ deviceId: cam.deviceId, live });
});

app.listen(8791, () => console.log('farm bridge :8791'));

氨气阈值示例(轮询补偿;实时仍以 iot 事件为准):

javascript 复制代码
// jobs/ammonia-watch.js
import 'dotenv/config';
import { syncFarmDevices } from '../services/sync-farm-devices.js';
import {
  loadProductModel,
  findPropertyRef,
  readProperties,
} from '../services/iot-thing.js';
import { camerasOfBarn } from '../services/barn-link.js';

const THRESH = Number(process.env.AMMONIA_THRESHOLD || 25);

export async function scanAmmonia() {
  const devices = await syncFarmDevices();
  const sensors = devices.filter((d) => d.track === 'iot' && d.productId);

  for (const s of sensors) {
    const model = await loadProductModel(s.productId);
    const ammoRef = findPropertyRef(model, ['氨', '氨气']);
    if (!ammoRef) continue;
    const state = await readProperties(s.deviceId, s.productId, [ammoRef]);
    const val = Number(state.properties?.[ammoRef]);
    if (Number.isNaN(val) || val < THRESH) continue;

    const cams = camerasOfBarn(devices, s.barnCode);
    console.warn('AMMONIA_ALERT', {
      barn: s.barnCode,
      sensor: s.deviceId,
      value: val,
      suggestCameras: cams.map((c) => c.deviceId),
      // 下一步:推值班群 + 打开 /api/v1/barns/:barn/live
    });
  }
}

// 定时:setInterval(() => scanAmmonia().catch(console.error), 60_000)
// 或 CLI:node jobs/ammonia-watch.js(入口自行调用 scanAmmonia)

Step 6 · 验收清单

text 复制代码
① run-sync-farm.js:传感 track=iot 且 productId 有值;摄像 track=video
② 同栋传感与摄像 barnCode 一致;UNMAPPED 清零
③ probe-sensor.js:温度/湿度/氨气 ref 匹配成功并读出数值
④ start-fan.js:二次确认后风机动作;无 token 被拒绝
⑤ enable-farm-callback.js:callbackFlag 含 iot(及按需 alarm)
⑥ /dashboard 返回 readings + cameras;/live 返回可播地址
⑦ 故意对摄像机调 getProductModel → 应失败或跳过(分轨校验)

踩坑 H :先写库再 200 → 物模型事件停推。

踩坑 I :把动检 videoMotion 当氨气告警 ------ 白天料线抖动会刷爆群。

踩坑 JbindDeviceLive 地址短时有效,勿当永久 URL 入库。


能力边界

text 复制代码
□ 物模型:环境量、开关量、Service 控制;不出 HLS
□ 视频:预览/回放/动检;不读氨气
□ 一体化:栋号台账 + 告警关联摄像
□ 档案 Excel / 兽药批号:另一条数据链,别塞进物模型 ref

性能与配额

text 复制代码
□ getProductModel 按 productId 缓存;固件/物模型变更再失效
□ 环境读数:事件推送为主,轮询为辅(如 1~5 分钟)
□ 视频默认标清;告警复核再切高清
□ 列表同步 10~15 分钟;pageSize ≤ 50

安全与生产

text 复制代码
□ 远程启停必须 RBAC + 二次确认 + 审计
□ 场区账号按养殖单元隔离,加盟场互不可见
□ appSecret / CONTROL_CONFIRM_TOKEN 仅 BFF
□ 弱网栏舍:直播失败时降级为最近云录像(另接回放接口)

和档案系统怎么拼

text 复制代码
智慧养殖最小闭环(本文)
  ├─ 物模型:环境与控制
  ├─ 视频:复核与留证
  └─ 档案/日报:生产数据(Excel/ERP)------ 用 barnCode + 日期对齐,勿混协议

本文结论

智慧养殖一体化的最小正确姿势:

text 复制代码
listDeviceDetailsByPage 统一台账(barnCode)
  → productId?iot 轨 : video 轨
  → 物模型读环境 / 控设备 + 视频回传复核
  → setMessageCallback(iot,alarm) 进同一值班流

只有传感没有画面 ,值班不敢远程拍板;只有监控没有物模型 ,氨气超了还靠人闻;两套后台两套 SN,三分钟对不齐镜头。

注册与下一步

如果你正在做养殖 SaaS / 场区数字化,先在 乐橙开放平台 open.imou.com 创建应用,把环控 IoT 设备与乐橙摄像机绑进同一开发者资产池;按本文用 productId 分轨,跑通「氨气告警 → 同栋一键回看」,再叠加档案与投喂业务。

乐橙开放平台以视频技术与安全 为核心,开放 OpenAPI、IoT 物模型、轻应用、OpenSDK 等低代码开发组件,一站式帮助第三方厂商与个人开发者快速、低成本落地视频与物联网场景应用------环境数据与画面并进同一台账,智慧养殖才算真正「可值班」。

注册入口:https://open.imou.com

今晚先让 3 号栋的告警页能点开直播,比再买一套独立环控大屏更有用。

相关推荐
草莓熊Lotso9 小时前
【LangChain】输出解析器全解:让大模型输出从 “聊天” 变 “机器可读”
服务器·数据库·python·langchain·pip
GuWenyue9 小时前
放弃云端API!5套技术栈实战WebGPU端侧AI,前端独立跑本地大模型,省成本还保隐私
前端·数据库·人工智能
鸽芷咕11 小时前
MySQL/PostgreSQL 迁移金仓 KES:LEFT JOIN 丢数据排查与避坑指南
数据库·mysql·postgresql
love530love18 小时前
【笔记】AutoClaw NSIS 安装器卡死、进程杀不掉、目录删不了?我是这么解决的
人工智能·windows·笔记·agent
谙弆悕博士20 小时前
系统集成项目管理工程师教程(第3版)笔记——第2章:信息技术发展
笔记·计算机网络·信息安全·学习方法·业界资讯·软考·系统分析师
吃着火锅x唱着歌21 小时前
Effective C++ 学习笔记 条款37 绝不重新定义继承而来的缺省参数值
c++·笔记·学习
2401_8414956421 小时前
【数据结构】B+树
数据结构·数据库·c++·b+树·概念·结构·操作原理
风曦Kisaki21 小时前
# Linux笔记:操作系统优化与资源管理
linux·运维·服务器·笔记
阿米亚波1 天前
【C++】流式数据输入处理(不完全整理)
开发语言·c++·笔记