智慧农业

水肥一体化怎么对账:配方、计划、执行三层分离

行业背景:这个问题现在为什么被反复问到

水肥一体化从“能手调”走到“要结算”的那一刻,问题的性质就变了。此前关心的是灌得匀不匀、肥有没有跟上;一旦要按方按肥结算,或者要在项目台账里写清投入,同一块地一季用了多少水、多少肥、按哪个配方用的,就必须有据可查。变化不在精度要求上,而在每个数字开始有人负责。

一条水肥系统的构成并不复杂:水源与首部(泵、过滤、施肥装置)、田间管网(主管、支管、滴灌带或微喷)、控制层(控制器、阀门、传感器)、计量层(水表、流量计、压力表、EC 与 pH 探头)。施肥装置常见两类,比例泵按进水比例吸入母液,注肥泵按设定流量或时长注入;两者的实际注入量推算方式并不相同,前期不留心,后期就很难对齐。

这个问题被反复问到,是因为至少三类主体各自记着一份账:园区业主看投入成本,托管服务方按用水用肥收费,项目验收要一份投入台账。同一季、同一块地,三份数字常常对不上,而每一份在自己的口径下又都能自圆其说。争议往往不是从某方算错开始,而是从口径没被写下来开始。

多数对账失败的直接原因不是数据太少,而是三件不同的事被记进了同一行记录:“应该配什么”(配方)、“打算灌多少”(计划)、“实际放了多少”(执行)。混在一张表里,事后既证明不了当时的计划合理,也定位不到执行偏在哪一步;偏差出现时只能退回到“反正这块地就灌了这么多”,把本可归因的问题变成口径之争。

下面讨论的就是这三层怎么分开、对账口径怎么写死。不给作物施肥参数,也不给用量建议——配方数值必须由农技人员按作物、土壤和水质现场确定,本文只处理记录结构与引用关系。

痛点:现场到底卡在哪几步

卡在第一步:配方在变,执行记录却不写版本。同一作物在不同生育期配方不同,现场还会因为水源水质波动、EC 偏高临时调整母液配比。记录里通常只写“按配方施肥”,没写用的是哪个版本、哪一天生效,事后就无从判断当时该按哪一套算。更麻烦的是就地手改:控制器里把注入浓度改了,台账没动,账面与实际从此分成两条线,而且谁也说不出是从哪一天开始分的。

卡在第二步:计划按地块排,执行按阀门开。计划一般写成“地块 × 轮灌组 × 时间窗”,实际动作却发生在泵、阀门、施肥装置这一层。地块与阀门的对应关系常常只存在老技术员的脑子里,换人、改管、加阀之后关系跟着漂移,分摊就断了。而且一次计划可能被拆成多次运行(中途停泵、压力不足补灌),两次计划也可能合并成一次运行,“一条计划对一次运行”的假设根本不成立。

卡在第三步:计量口径不统一,来源还不同源。水量有三种算法:水表累计读数差、流量计瞬时值乘时长、控制器记录的阀开时长乘额定流量,三者给出的数必然不一样。肥量同样有母液体积乘浓度、注肥泵冲程乘标定流量、桶余量前后差三套算法。时间口径也没统一:设备时钟、平台接收时间、人工填表时间可以差几分钟到几小时,跨零点的运行归属哪一天,事先没有人定。

卡在第四步:对账口径要到结算时才定。平时只记“灌了水、施了肥”,等结算才问每亩多少方、每方折多少肥。这时候缺的是分母:面积没核、轮灌组面积与实际种植面积不一致、空棚也摊了水费。口径没在记录产生之前定下来,事后就只能补出一份各方都不认的账。

方案:可落地的结构与取舍

把三层分开,在数据上就是三份记录、三套写权限,而不是把三样东西塞进同一行再派生出几个视图。每层只回答一个问题,也只允许一类角色去改它;边界一旦模糊,对账就会退化成事后解释。

层回答的问题写入规则
配方层该按什么配只读,调整出新版本
计划层这一轮打算灌多少可变更,每次留版本与原因
执行层实际放了多少只追加,错了用冲正

配方层写清作物与生育期、目标 EC 与 pH 区间、各母液配比、单位水量注入量、生效时间与适用区域。它一经启用即冻结,要调整就出下一个版本,旧版本原样保留。理由很朴素:配方是对账的依据,依据被事后改写,任何偏差都无法归因到具体某一天。

计划层写时间窗、地块与轮灌组、核定面积、目标灌水量,以及由某个配方版本推导出的计划肥量,附审批状态。天气、长势、临时停水都会让计划变,所以它必须可改;但每次改都留版本与原因,旧计划不删。同时要守住一件事:计划是意图,不是事实,不能被拿去当执行结果统计。

执行层写一次运行的起止时间、实际水量、实际注入肥量、计量来源与偏离原因。这一层只追加,写错了用一条冲正记录改正,不原地覆盖。执行记录不回写计划,也不改动配方。

三层之间只靠标识关联:运行记计划号与配方版本号,计划记配方版本号。上游改版就新增记录,靠引用链回溯,不在下游复制上游字段。复制出去的两份迟早各自演化,对账时又变成两个口径。

代价要提前说清。录入比一张表麻烦,配方得先建档、计划得先走审批;现场临时调整必须填偏离原因,否则执行层会攒下一堆解释不了的数;这套结构也不提升计量精度,它只让偏差可归因,谁把它当成“自动算准”,第一轮结算就会翻车。规模小的场景可以简化,固定配方、单一地块、一人操作时计划层先手工记也行,但三层的数据结构不要合并——合并容易,拆回来就难了。

技术实现:字段、接口与数据口径

落到字段上,三层的边界靠“谁写、谁不许改”体现,而不是靠命名空间。五份记录各自独立:配方版本只追加新版本,计划留修订链,运行只追加,原始读数单独存,修正单独记。

对象必需字段校验重点
recipe_version 配方版本作物、生育期、目标 EC 与 pH 区间、单位水量注入量、生效时间、状态同一作物同一天只能有一个生效版本
plan 计划地块、轮灌组、核定面积、计划水量、配方版本、时间窗、审批状态、修订来源计划水量能按面积口径复算
run 运行计划号、配方版本号、起止时间、实际水量、实际肥量、计量来源、偏离原因起止时间落在计划窗口内,越界必须填原因
meter_reading 原始读数计量来源、设备标识、采样时间、累计值、单位采样时间与接收时间分开存,只追加
run_adjust 冲正原运行号、冲正类型、水量与肥量增量、原因、操作人不改原记录,冲正后能还原净额

三处口径必须写死,否则对账时还要再吵一遍。水量以水表累计读数差为主口径,流量乘时长只作辅证;两者偏差超阈值就标“待核”,不自动取平均——平均出来的数两边都不认。肥量按母液体积乘浓度折算,折算系数要注明来自哪一次标定;比例泵按进水总量乘配比折算,同样写清配比出自哪个配方版本。时间上采样时间与接收时间分开存,跨零点的运行按阀门开启区间切分归属日期,设备时钟偏差超阈值的那一段标“时效不确定”,不进结算。

幂等和补传是现场网络不稳带来的硬需求,不是可选项。run_id 由现场端生成,重复提交直接返回首次结果,不会多出一条运行;断网补传只补接收时间,起止时间一律不动;撤回错误记录不原地改,追加一条冲正。这样一条链上的每个数字都是追加出来的,不存在“谁把数改了”却查不出改前值的情况。

把口径落成一段能跑的对账函数,比写二十页规范有用:


#!/usr/bin/env python3
# 三层对账:配方版本 / 计划 / 执行,只用标准库
RECIPE = {
    "recipe_version_id": "R-03",
    "ec_range": (1.8, 2.4),   # 目标 EC 区间,由农技按现场定
    "inject_l_per_m3": 1.2,   # 每立方米水应注入母液升数(来自最近一次标定)
}

PLANS = [
    {"plan_id": "P-0715", "recipe_version_id": "R-03", "plan_water_m3": 40.0},
    {"plan_id": "P-0716", "recipe_version_id": "R-03", "plan_water_m3": 25.0},
]

# 执行层:只追加。一次计划拆成两次运行,两条都挂同一个 plan_id
RUNS = [
    {"run_id": "U-1", "plan_id": "P-0715", "water_m3": 20.0, "fert_l": 24.8,
     "ec_samples": [2.0, 2.2, 2.6], "deviation_reason": ""},
    {"run_id": "U-2", "plan_id": "P-0715", "water_m3": 17.5, "fert_l": 21.6,
     "ec_samples": [2.1, 1.9, 1.7], "deviation_reason": "中途停泵补灌,少灌 2.5 方"},
]

plan = {p["plan_id"]: p for p in PLANS}
done_plans = {r["plan_id"] for r in RUNS}

# 口径一:水量偏差率,分母恒为计划量;计划为 0 的计划不计入
water_plan = sum(p["plan_water_m3"] for p in PLANS if p["plan_id"] in done_plans)
water_run = sum(r["water_m3"] for r in RUNS)
water_dev = (water_run - water_plan) / water_plan * 100

# 口径二:肥量按母液体积折算,折算系数来自配方版本里记录的那次标定
fert_plan = water_run * RECIPE["inject_l_per_m3"]
fert_run = sum(r["fert_l"] for r in RUNS)
fert_dev = (fert_run - fert_plan) / fert_plan * 100

# 口径三:浓度按采样点判合规,只数超限次数,不用平均值掩盖
low, high = RECIPE["ec_range"]
samples = [v for r in RUNS for v in r["ec_samples"]]
over = [v for v in samples if not low <= v <= high]

# 计划执行率:分母按计划数算,取消的计划单独计,不并入分母
rate = len(done_plans) / len(PLANS) * 100

print(f"水量偏差 {water_dev:+.1f}%(计划 {water_plan:.1f} 方,执行 {water_run:.1f} 方)")
print(f"肥量偏差 {fert_dev:+.1f}%(应注 {fert_plan:.1f} 升,实注 {fert_run:.1f} 升)")
print(f"浓度超限 {len(over)} 次,占 {len(over)}/{len(samples)} 次采样")
print(f"计划执行率 {len(done_plans)}/{len(PLANS)},{rate:.0f}%")

# expect: 水量偏差 -6.2%(计划 40.0 方,执行 37.5 方)
# expect: 肥量偏差 +3.1%(应注 45.0 升,实注 46.4 升)
# expect: 浓度超限 2 次,占 2/6 次采样
# expect: 计划执行率 1/2,50%
assert round(water_dev, 1) == -6.2
assert round(fert_dev, 1) == 3.1
assert len(over) == 2
for r in RUNS:
    if r["water_m3"] < plan[r["plan_id"]]["plan_water_m3"] * 0.5:
        assert r["deviation_reason"], f"{r['run_id']} 水量显著偏低却没有偏离原因"
print("对账口径校验通过:每笔偏差都能追到原因")

采集端到平台这一段只需要示意:字段映射做在网关上,别让控制器直接对平台字段;补传按 run_id 去重,重试用指数退避,失败落本地队列,等下次联网再抬。


# 非可运行代码,仅作设计示意(字段映射与补传重试的结构,不含实现)
# 现场端 -> 网关 -> 平台,网关做三件事:改字段名、补设备缺口、去重
POST_RUN = {
    "run_id": "<现场生成,全局唯一>",
    "plan_id": "<现场从下发任务里带过来的计划号>",
    "start_at": "<阀门开启时刻,设备时钟>",
    "end_at": "<阀门关闭时刻>",
    "water_m3": "<水表累计差,保留两位>",
    "meter_source": "water_meter|flow_totalizer|valve_duration",
    "received_at": "<网关接收时刻,平台写入时补>",
}

def push(run, retry=5):
    for attempt in range(retry):
        code = platform.post("/runs", run)       # 幂等键 = run_id
        if code == 200:
            return True                          # 首次与重复提交都走这里
        sleep(2 ** attempt)                      # 指数退避,不并发、不刷接口
    queue.append_local(run)                      # 重试用尽落本地队列,联网后再传
    return False

本节的验收口径一句话:任一笔结算数字,都要能顺着 run → plan → recipe_version 回到那台表的原始读数。

项目实践:一次真实落地怎么走

本节是方案推演,没有真实项目记录可引用,下面的顺序与判据可以照做,成效不作承诺。

落地按六步走,每一步先定“做完就算过”的判据。一核台账:地块、核定面积、作物、阀门与轮灌组逐一摊开对一遍,对不上是常态——一块地挂两个阀、一个阀管半个棚、面积按亩还是按棚,都得现场定死;判据是任取一条运行都能落到唯一地块与唯一面积,否则先修关系表。二建配方版本:现行配方整理成带生效时间的版本,历史补不出的写“无记录”而不靠回忆,判据是任一条历史运行都能指向某个版本或“未知”。三选一个轮灌组试点两周,只记录不考核,这一段要暴露的是计量缺口和人工习惯,动了考核数字只会填得好看。四做差异归因:水量差、肥量差逐条落到计量口径、计划变更、执行偏离、录入错误,归不了因的单列“未解释”并计数。五与业主、服务方、农技三方书面确认口径,面积、水量主口径、肥量折算依据写进对账说明。六扩组,每扩一组重做一次台账核对,漏了等于把上一组的关系错误原样复制。

下表是扩组前的门槛,判断只看“预期结果”那一列:

场景预期结果应留证据
计划中途换版本运行仍指旧版本,能查出换版时点版本记录 + 运行引用
一次计划拆两次运行都挂同一计划号,水量可相加计划号 + 两条 run
两种水量口径偏差超阈值标“待核”,不自动取平均两种口径原始值

以上都是待执行项:偏差未归因前不扩大范围,也不据此宣称节水节肥成效。

价值总结:判断标准与复用边界

一套水肥对账能不能用,不看报表做得多漂亮,只看它能不能当场回答几个问题:这一笔水是从哪台表、哪次标定、哪段时间的读数里来的;当时该按哪个配方版本执行,为什么是那一个;计划与执行之间的差额,哪些是有意变更、哪些是操作偏差;记录被改过没有,改之前长什么样。四个问题里有一个答不上,这套账就还停在“各方都能自圆其说”的阶段,结算时照样得靠人现场喊数。

指标要分层算,而且分母先写死在前面。水量偏差率拿执行减计划作分子,分母恒为计划量——分母一换成执行量,同一场运行既能报节水也能报超灌;计划量为零的运行不并入这个指标,否则除出来是无意义的数。肥量偏差同一套算法,但要注明折算口径:按母液体积乘浓度,还是按注肥泵冲程乘标定流量,两者不能混算。浓度合规率数的是采样点落在配方区间内的比例,采样频率一并写清;用一句“平均浓度合格”盖过超限次数,等于把偶发的严重偏移抹平。计划执行率按完成的计划数除以计划数,取消的计划单列,不并入分母。

机制可以复用,参数必须重定。三层结构、版本化、只追加与冲正、幂等补传,这些换个项目能整段搬过去;配方数值、标定系数、轮灌制度、面积台账只能现场重新确认。把上一套参数直接复制过去,机制越完整,错得越整齐。

反过来也值得说一句:把三层压成一张表确实最省事,代价是所有争议都变成说不清,而且事后补不出来——没有版本就没有当时的依据,不追加就没有当时的原样。所以对账真正交付的不是一份报表,而是一条从配方版本一直走到那台表原始读数都能走通的链,报表只是这条链的打印件。

Smart Agriculture

把智慧农业落到你的现场

围绕种植、设施农业与水产养殖场景,构建环境监测、智能控制与生产数据一体的数字农业体系,把经验种植变成可量化、可复制的数据化生产。