开春那会儿帮一个种番茄的大棚用户调试App,他跟我抱怨了句特别实在的话:气象站、土壤传感器、水肥一体机都装上了,但每天早上还是要靠微信群喊人,该浇水的地没人浇,该打的药拖到下午。设备数据再准,最后一步落不到人身上,全都白搭。这就是智慧农业里最典型的调度断层,也是我在这篇教程里要解决的痛点:HarmonyOS智慧农业管理应用中的任务管理与提醒系统。
这套系统要做的,就是把“人追着事跑”变成“事追着人跑”:农事任务创建后能自动派给对应的人,系统到点提醒,逾期自动升级,执行结果回流成农事档案。本篇覆盖任务数据模型设计、状态机定义、HarmonyOS端的Alarm/位置围栏/通知联动、基于AGC云函数的定时扫描与超时变更、ArkTS工程落地,以及我在这套模块上真机联调踩过的几个坑。
1. 为什么智慧农业App一定要有任务管理和提醒
1.1 从“人追事”到“事找人”
传统农业管理里,任务调度基本靠两条:一条是口头的,早上上工前说一句“今天把东边三号棚的番茄点了花”,另一条是微信群的,编辑一段文字@所有人。这两条路子在小规模家庭农场勉强能转,一旦涉及到雇佣工人、临时工、多地块协同,就开始漏。
我调研过几个实际农场场景,漏任务的原因高度集中:
- 口头布置没有书面记录,工人干着干着就忘了优先级。
- 微信群消息被聊天刷掉,重要农事沉底。
- 没有“截止时间”概念,今天干和明天干都一样,但农事恰恰最讲究窗口期。比如病虫害防治,最佳防治窗口就两三天,错过了后面要用十倍成本补救。
- 管理者不在现场时,无法确认任务到底做完没有。
所以任务管理系统的第一个价值,是把“调度”变成一条有状态、有时间、有责任人的流水线,而不是一个单纯的待办清单。
1.2 任务系统不只是“待办”,它是农事档案的入口
很多人会把任务管理做成一个轻量级Todo List,字段就是标题、时间、完成状态,做完就完了。但在智慧农业里,任务数据本身就是生产档案的一部分,埋点价值远大于“提醒”本身。
举个例子,我们在这套系统里设计了taskType字段,取值包括灌溉、施肥、施药、整枝、采收、巡检等。每次任务完成,系统会记录完成时间、操作人、关联地块、农资用量。这些数据沉淀下来以后,可以做三件事:
- 按地块统计年度用工量,辅助下一年度种植计划。
- 按作物统计农药、肥料使用记录,形成生产履历,将来对接农产品溯源,直接有数据支撑。
- 跟环境传感器数据打通,比如某块地在低温过后出现了病害预警,系统自动生成一条“病害巡查”任务,推给对应技术员,这就是从“被动记录”到“主动决策”的升级。
所以这个模块的设计,不能只站在“提醒”角度,必须同时考虑数据结构和后续统计分析模块的衔接。我在下文的数据模型设计里会重点体现这一点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 任务管理的数据模型与状态机设计
2.1 任务表结构设计:字段拆解与设计理由
先给出一版我在实际项目中使用过的任务表结构,基于AGC云数据库。表名建议叫Task,字段如下:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| taskId | String | 是 | 任务ID,主键,服务端生成 |
| title | String | 是 | 任务标题,如“三号棚番茄点花” |
| description | String | 否 | 任务详细描述,可附带农事指导 |
| fieldId | String | 是 | 关联地块ID |
| taskType | String | 是 | 农事类型:irrigation/fertilize/pesticide/prune/harvest/inspect |
| priority | String | 是 | 优先级:high/medium/low |
| status | String | 是 | 状态:pending/ongoing/completed/overdue |
| assigneeId | String | 是 | 责任人用户ID |
| assigneeName | String | 是 | 责任人姓名快照 |
| creatorId | String | 是 | 创建人ID |
| startTime | DateTime | 是 | 计划开始时间 |
| deadlineTime | DateTime | 是 | 截止时间 |
| actualFinishTime | DateTime | 否 | 实际完成时间 |
| notifyStrategy | String | 否 | 提醒策略,如 “15分钟前提醒” |
| attachments | String | 否 | 附件URI,可存图片,如现场病害照片 |
| createTime | DateTime | 是 | 创建时间 |
| updateTime | DateTime | 是 | 最后更新时间 |
几个字段的设计理由我要特别说明:
fieldId 必须单独存,不要用字符串拼接在地块描述里。后续按地块维度统计用工和农资,直接走fieldId索引就能出数。如果耦合在文本里,统计模块写起来会非常痛苦。
assigneeName 在很多人看来是冗余字段,因为通过assigneeId可以关联用户表查出姓名。但在农事现场,网络经常不稳定,列表页需要第一时间展示责任人名字。做一次联表查询就会多一次网络往返,体验差距明显。所以我在写入任务时直接冗余存一份assigneeName,前端列表直接渲染,不用再等联表。
status 里的 overdue 字段,这个不是算出来的,是真正落库的状态。原因后面状态机章节细说。
2.2 任务状态机定义:为什么“已逾期”必须是一个状态
我设计的状态流转如下:
- pending(待接单):任务已创建,尚未有人认领。这是多人协作场景的起点。如果只有一个管理员一个工人,可能觉得“待接单”多余,但农场里有多个工人时,必须让工人从池子里主动认领,或者管理员指定,否则任务落不到具体责任人的头上责任就会扯皮。
- ongoing(进行中):责任人认领或系统直接指派后进入。
- completed(已完成):责任人标记完成,或管理员代操作完成。
- overdue(已逾期):到达deadlineTime时任务仍未完成。这个状态由后端定时扫描自动写入。
这里有一个很关键的决策:为什么overdue不通过前端计算status = ongoing且currentTime > deadlineTime来判断,而非要单独落库?
两个原因。第一,首页“今日需处理”报表需要一眼看到逾期任务数量。如果每次查询都对每一条超时任务做实时比较,数据量大了之后查询效率很低。第二,逾期状态一旦落库,系统就可以在状态变更的瞬间触发一系列动作:给责任人发升级提醒、给管理者发汇总报告、在统计模块里单独归入“逾期任务”维度。这些动作只需要在状态变更时执行一次,而不是每次查询都重复判断。所以从这个角度看,逾期状态本质上是“事件标记”,不是单纯的逻辑推导。
状态流转的触发与通知策略我用表格整理出来:
| 动作 | 原状态 | 新状态 | 是否通知 | 通知对象 |
|---|---|---|---|---|
| 创建任务 | - | pending | 是 | assigneeId(可选) |
| 认领任务 | pending | ongoing | 是 | 创建人 |
| 开始执行 | pending | ongoing | 是 | 创建人 |
| 标记完成 | ongoing | completed | 是 | 创建人和管理员 |
| 超时扫描 | ongoing | overdue | 是 | 责任人和管理员 |
| 逾期后完成 | overdue | completed | 是 | 创建人和管理员 |
另外注意,pending状态的任务如果到达某个时间点仍无人认领,我也会让云函数把它降级为“高优先级待办”,推送给管理员的汇总消息里。这一步是提升任务响应率的关键,虽然看起来不起眼,但实际跑下来效果明显。
2.3 AGC云数据库中的索引与权限设计
表结构确定后,索引设计不能省。任务管理模块用得最多的查询是:
- 按当前用户查询“我负责的未完成任务”:WHERE assigneeId = ? AND status IN ('pending', 'ongoing')
- 查询即将到期的任务:WHERE status = 'ongoing' AND deadlineTime < ? AND deadlineTime > ?
- 查询逾期任务:WHERE status = 'overdue'
所以必须建立两个联合索引:
- (assigneeId, status)
- (status, deadlineTime)
这两个索引能覆盖绝大多数任务查询场景。云数据库的索引建好后,实测列表查询在数据量几千条量级时是毫秒级响应,完全够用。
权限设计上,任务表的读写权限要基于用户认证态。AGC云数据库支持按用户维度配置权限,我们把任务表的读写权限设为“仅创建者和责任人可读写”,管理员角色可通过一个自定义claim字段放行。这个权限配置不要省,否则任务数据会被任意用户遍历到,这是农事隐私,不能裸奔。
3. 提醒系统核心机制:时间提醒、位置提醒与人机交互
3.1 时间提醒:HarmonyOS中的Alarm API用法
HarmonyOS端做定时提醒,第一反应是写在业务代码里setTimeout,到点再发通知。这个方案在我最开始的原型里就是这么做的,但很快发现一个严重问题:App一旦被用户滑掉、或者系统为了省电把进程回收,setTimeout就没了,提醒永远不会触发。
正确姿势是用HarmonyOS的提醒代理服务,也就是@ohos.alarm能力。它的特点是:提醒任务注册到系统层面,由系统统一调度,即使App进程不在,到点也能弹提醒。
我用的核心代码大致长这样:
typescript复制import alarm from '@ohos.alarm';
import { BusinessError } from '@ohos.base';
function setTaskAlarm(taskId: string, remindTime: number) {
let alarmInfo: alarm.AlarmInfo = {
alarmType: alarm.AlarmType.ALARM_TYPE_REMINDER,
triggerTime: remindTime, // 毫秒时间戳
repeatType: alarm.RepeatType.REPEAT_TYPE_NONE,
title: '农事任务提醒',
content: `您有一个任务即将到期:${taskId}`,
// 可选:配置跳转的WantAgent,点击提醒后直接进任务详情
};
try {
alarm.setAlarm(alarmInfo, (err: BusinessError, data: number) => {
if (err) {
console.error(`setAlarm failed, code=${err.code}, message=${err.message}`);
return;
}
console.info(`setAlarm success, alarmId=${data}`);
});
} catch (e) {
console.error(`setAlarm exception: ${JSON.stringify(e)}`);
}
}
这里注意触发时机。任务创建时,前端根据notifyStrategy计算出提醒时间,比如“截止时间前15分钟”,然后调用setAlarm注册。我建议服务端在任务数据里返回一个remindAt字段,客户端直接用它注册提醒,不要在客户端自己算,避免时区、服务器时间不一致的问题。
还有一个细节:提醒的title和content不要写具体任务ID,用户端看起来没有信息量。应该从任务数据里带出“三号棚番茄点花”这样的标题。后端推送或构造提醒时直接将任务标题拼进去。
3.2 位置围栏:让系统在用户走到大棚附近时主动提醒
时间提醒能解决“什么时候该做”,但农事场景里还有一种提醒更实用:人员到了指定地块附近,系统告诉他这块地有什么待办任务。
这个需求对应的是位置围栏(Geofence)能力。HarmonyOS的@ohos.geoLocation支持位置围栏监听。我们在用户进入某个地块半径200米范围时,触发一次任务提醒。
实现思路:
- 每个地块在系统里有经纬度坐标,从数据库读出。
- 前端对有任务的地块注册围栏。
- 进入围栏后,拉取该地块的待办任务列表,弹出应用内通知。
关键代码片段:
typescript复制import geoLocation from '@ohos.geoLocation';
import { BusinessError } from '@ohos.base';
function addTaskGeofence(fieldId: string, lat: number, lng: number, radius: number) {
let request: geoLocation.GeofenceRequest = {
scenario: geoLocation.GeofenceScenario.GEOFENCE_SCENARIO_DAILY,
geofence: {
latitude: lat,
longitude: lng,
radius: radius,
},
};
try {
geoLocation.on('geofence', request, {
onEnter: (geofence) => {
// 进入围栏,拉取并展示该地块的任务
fetchFieldTasks(fieldId);
},
onExit: (geofence) => {
// 可选:退出围栏时清理状态
},
});
} catch (e) {
console.error(`addGeofence exception: ${JSON.stringify(e)}`);
}
}
位置提醒的权限比通知权限还要敏感,所以要选择在用户进入“地块详情”或“任务地图”页面时再申请位置权限,而不是App启动时就弹窗。否则用户一旦拒绝,后面所有围栏功能都无法使用。
3.3 通知与弹窗协作:如何避免“提醒疲劳”
提醒系统最容易被忽略的问题不是“怎么触发”,而是“触发得太频繁”。农事人员一天要在地里走好几个小时,如果每次溜达到地块旁边都弹一次提醒,不出三天他就会把通知权限关了。
我最终落地的是分级提醒策略:
- 临近提醒:任务截止前15分钟,发一条应用内通知,不锁屏、不响铃。
- 到期提醒:到截止时间,任务状态从ongoing切换为overdue,通知升级为锁屏提醒+响铃。
- 每日汇总:每天早上7点推送一条当日任务汇总,而不是逐个任务提醒。
这样做的目的,是把通知的“频率”降下来,把“信息量”提上去。用户一天只需要看两三次通知,就能掌握全天安排。
在HarmonyOS里,通知级别通过notification模块的NotificationRequest来设置:
typescript复制import notificationManager from '@ohos.notificationManager';
let request: notificationManager.NotificationRequest = {
id: 1001,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '今日农事安排',
text: '三号棚浇水、五号棚采收,共2项待处理',
},
},
// 设置通知分组、渠道、重要性等
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
wantAgent: {
// 点击通知跳转到任务列表页
},
};
notificationManager.publish(request).then(() => {
console.info('publish notification success');
});
这里notificationSlotType选SOCIAL_COMMUNICATION还是OTHER_TYPES,要根据提醒重要性来。普通汇总用OTHER_TYPES,可以降低干扰;到期升级提醒用SOCIAL_COMMUNICATION,保证有响铃和横幅。
4. 任务分发与状态流转:后端定时扫描方案
4.1 定时任务的实现:用云函数定时扫描任务表
提醒系统不只有前端这一侧,后端还要承担两个脏活:定时扫描任务表,把到期的任务状态翻转,并向所有关联客户端推送提醒。
为什么要放在云端而不是客户端?因为客户端不可能保证7x24小时在线,尤其手机端,晚上息屏就可能退出后台。但云端是常驻的,可以保证漏不掉任何一次扫描。
在AGC里,我们可以创建一个定时触发的云函数,配置成每分钟执行一次。云函数逻辑如下:
typescript复制import cloud from '@hw-agconnect/cloud';
interface TaskRecord {
taskId: string;
title: string;
status: string;
deadlineTime: number;
assigneeId: string;
assigneeName: string;
}
export async function scanTaskDeadlines() {
const db = cloud.database();
const now = Date.now();
// 每次扫描只处理窗口期内的任务,限制扫描量
const windowEnd = now + 60 * 1000;
// 查询1:进行中的任务到期需要翻转为逾期
const ongoingTasks: TaskRecord[] = await db.collection('Task')
.where({
status: 'ongoing',
deadlineTime: { $lt: now },
})
.limit(100)
.get();
for (const task of ongoingTasks) {
await db.collection('Task').doc(task.taskId).update({
status: 'overdue',
updateTime: new Date(),
});
await sendOverdueNotification(task);
}
// 查询2:待认领的任务超时未接单,提升提醒级别
const stalePendingTasks: TaskRecord[] = await db.collection('Task')
.where({
status: 'pending',
deadlineTime: { $gt: now, $lt: now + 60 * 60 * 1000 },
})
.limit(50)
.get();
for (const task of stalePendingTasks) {
await sendPendingEscalationMessage(task);
}
return { scanned: ongoingTasks.length + stalePendingTasks.length };
}
窗口期设计这里说下我的经验。每次扫描如果一次性查询所有 overdue + all pending 数据,数据量大的时候云函数的执行时间和资源消耗都会飙升。加窗口期后,每次只处理未来60秒内到期的任务,分摊扫描压力,对用户没有任何感知差异。
4.2 状态超时自动变更:从“正在干”到“已逾期”
云函数扫描到 overdue 状态翻转时,需要一个机制保证幂等,避免同一任务被重复翻转为overdue、重复发送通知。
我采用的做法是,在任务表的数据模型里加一个overdueNotified布尔字段,或者更通用一点,加一个statusHistory子表。
我的做法:
- 状态翻转前,先查询Task记录中是否存在statusHistory里最近一条状态为overdue的记录。
- 如果有,直接跳过更新和通知。
- 如果没有,先写入statusHistory记录,再更新Task的status字段。
这里要注意,云函数的并发执行。多个定时触发器同时运行时,两条并发请求可能同时读到“没有overdue记录”,然后同时写入,导致重复。解决办法是给状态变更加上事务,或在Task表的基础上再建一个唯一索引(statusChangedTaskId+status)。实际生产中最稳妥的方案是使用云数据库的事务能力,把检查+更新包在一个事务里。
状态变更日志表的字段设计:
| 字段名 | 类型 | 说明 |
|---|---|---|
| logId | String | 日志ID |
| taskId | String | 任务ID |
| fromStatus | String | 原状态 |
| toStatus | String | 新状态 |
| operatorId | String | 操作者ID,系统操作时填’system’ |
| changeReason | String | 变更原因:user_action/timeout/create |
| changeTime | DateTime | 变更时间 |
这个日志表后续做任务报表、统计平均完成时长时直接按它聚合,非常方便。
4.3 与AGC Push Kit的推送链路打通
云函数检测到任务状态变更后,需要把提醒推到用户手机。这里用的是AGC的Push Kit服务。
流程拆开是三步:
- 客户端启动时向Push Kit申请推送Token,拿到后上传到后端。
- 云端在用户表User中存该用户的pushToken。
- 云函数根据任务记录的assigneeId查出pushToken,组装消息发送。
云函数端的发送消息代码大致如下:
typescript复制import cloud from '@hw-agconnect/cloud';
async function sendOverdueNotification(task: TaskRecord) {
const user = await db.collection('User').doc(task.assigneeId).get();
if (!user || !user.pushToken) {
console.warn(`User ${task.assigneeId} has no push token`);
return;
}
const message = {
token: user.pushToken,
android: {
notification: {
title: '任务超时提醒',
body: `任务「${task.title}」已逾期,请尽快处理`,
},
data: {
taskId: task.taskId,
redirectUrl: 'pages/TaskDetailPage',
},
},
};
await cloud.push().send(message);
}
实际开发中,pushToken会周期刷新。要处理token失效的情况,发送失败后从用户表移除旧token,App下次启动再次上报新token。如果不做这个清理,连续发送失败不仅浪费资源,用户换机或卸载重装后,消息还会推给旧token。
推送到达率这块,我单独放最后一章说,因为真机联调的坑实在太多了。
5. 任务模块的HarmonyOS工程落地(ArkTS示例)
5.1 页面结构:任务列表页+任务详情页+新建任务弹窗
任务模块在HarmonyOS应用里我拆成了三个页面:
- TaskListPage:任务列表页,默认展示“我负责的”所有未完成任务,按截止时间升序、优先级降序排列。
- TaskDetailPage:任务详情页,展示完整字段、操作按钮(认领、开始执行、完成)。
- TaskCreatePage:新建任务页,表单有title、description、fieldId、taskType、priority、deadlineTime等。
列表页的UI建议做成卡片式,每条任务一张卡片,卡片上用不同颜色块区分优先级:高优先级红色、中优先级橙色、低优先级绿色。已逾期任务在右上角加一个“已逾期”标签。之所以用颜色而不是只靠排序,是因为农事人员在户外阳光下看手机,亮度高、眼睛容易疲劳,颜色识别比文字识别快得多。
卡片上还需要展示:任务标题、地块名称(通过fieldId关联查询)、责任人、截止时间。不要堆太多字段,用户扫一眼能抓到关键信息就够了。
5.2 核心代码片段:任务列表的@State数据绑定
ArkTS这边的数据绑定,我用@State定义一个TaskItem数组,页面加载时请求数据并赋值。
typescript复制@Entry
@Component
struct TaskListPage {
@State tasks: TaskItem[] = [];
@State loading: boolean = false;
async aboutToAppear() {
await this.loadTasks();
}
async loadTasks() {
this.loading = true;
try {
const taskService = new TaskService();
const taskList = await taskService.fetchMyTasks('ongoing');
this.tasks = taskList;
} finally {
this.loading = false;
}
}
Build() {
Column({ space: 12 }) {
if (this.loading) {
LoadingProgress().width(300).height(300)
} else if (this.tasks.length === 0) {
Text('暂无待办任务')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 80 })
} else {
List() {
ForEach(this.tasks, (task: TaskItem) => {
ListItem() {
TaskCard({ task: task })
}
}, (task: TaskItem) => task.taskId)
}
.width('100%')
.layoutWeight(1)
.onRefresh(() => {
this.loadTasks();
})
}
}
.width('100%')
.height('100%')
}
}
这里的TaskService是封装好网络请求的服务类,通过fetchMyTasks拉取任务列表。在列表项key的选择上,我踩过一次坑:直接用数组索引作为key,会导致列表项复用错乱,比如滑动后任务卡片内容串位。所以必须用task.taskId作为唯一key。
5.3 网络层封装:从云端拉取任务列表、更新状态
任务模块的网络层,我做了一个简洁的TaskService类,统一处理任务相关请求。
typescript复制import http from '@ohos.net.http';
const BASE_URL = 'https://your-server.example.com/api';
class TaskService {
async fetchMyTasks(status: string): Promise<TaskItem[]> {
const httpRequest = http.createHttp();
const response = await httpRequest.request(
`${BASE_URL}/tasks?status=${status}`,
{
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
'Authorization': getStoredUserToken(),
},
connectTimeout: 10000,
readTimeout: 10000,
}
);
const result = JSON.parse(response.result as string);
return result.data.map((item: any) => new TaskItem(item));
}
async updateTaskStatus(taskId: string, newStatus: string): Promise<boolean> {
const httpRequest = http.createHttp();
const response = await httpRequest.request(
`${BASE_URL}/tasks/${taskId}/status`,
{
method: http.RequestMethod.PUT,
header: {
'Content-Type': 'application/json',
'Authorization': getStoredUserToken(),
},
extraData: JSON.stringify({ status: newStatus }),
}
);
const result = JSON.parse(response.result as string);
return result.success;
}
}
在网络层要注意超时和错误处理。农事现场网络信号不稳定,超时时间设太短会导致请求频繁失败,设太长会让用户感觉卡死。我实测下来,connectTimeout设10秒、readTimeout设10秒比较合适。另外,更新任务状态后,最好做一次状态回读,或用接口返回的最新任务数据刷新本地数组,避免出现“我点了完成,列表里还是显示进行中”的界面不一致问题。
6. 联调与避坑:这套系统在真机上最常见的几个问题
6.1 Alarm API在App被杀死后的失效问题
理论上,Alarm API注册的提醒由系统调度,App进程没了也能触发。但真机上我遇到过一次诡异的现象:App从最近任务列表滑掉之后,个别机型还是不能准时弹提醒,反而是在用户重新打开App时一次性弹出多条错过的提醒。
后来排查发现,问题出在部分国产ROM的省电策略上。系统会智能判断“这个App长期不活跃”,然后冻结它的所有后台能力,包括已经注册的提醒。这不是HarmonyOS框架的问题,而是厂商定制ROM的策略。
应对方案有三个层次:
- 引导用户在系统设置中将App的电池优化设置为“不限制”。
- 对特别重要的任务,同时注册系统日历提醒作为兜底。
- 云函数推送当天早上汇总提醒,即使本地Alarm失效,用户至少能通过Push知道当天安排。
不要试图完全绕过系统策略,那是不可能的。做提醒系统,一定要接受“移动端提醒不可能100%准时”这个现实,设计上要通过多种渠道交叉保障。
6.2 位置权限与后台权限的申请时机
位置围栏依赖两个权限:定位权限和后台位置权限。后台位置权限属于敏感权限,在应用商店审核时会被重点检查,所以不要在启动时一股脑申请。
我的做法是分场景、分步骤申请:
- 用户首次打开App时,只申请通知权限,不申请位置权限。
- 用户第一次进入“地块地图”页面或第一次开启围栏功能时,弹窗申请模糊定位权限。
- 用户在围栏设置里开启“进入地块提醒”时,再弹窗申请精确位置和后台位置权限。
这样做的原因是权限申请的时机和上下文决定了用户的接受度。上来就弹三个权限框,用户大概率全部拒绝,后面再想申请回来很麻烦。
6.3 消息推送到达率:为什么没收到提醒
Push Kit消息发送成功不等于用户一定收到。这一点在真机联调时非常折磨人。
我在开发期就遇到过:云函数返回send success,但手机端什么动静都没有。排查路径如下:
- 先从APICloud/AAGC控制台的推送记录看消息状态,是已发送还是已读状态。
- 再从客户端日志里确认推送Token有没有变化,如果Token更新了,旧Token发的消息当然到不了。
- 再检查手机通知设置,App是否被关闭了通知权限。
- 最后检查手机是否开启了免打扰模式。
这么一通排查下来,大概率能定位到问题。但有一个坑不好查:部分真机在开发者模式下,Push通道的建立会延迟。我建议联调阶段用一台日常使用的手机,而不是专用测试机,并且保持熄屏一段时间后再点亮,模拟真实使用状态。
6.4 我踩过的HarmonyOS任务模块开发坑
这一节分享几个代码层面的坑。
第一个是时区问题。我在云函数里生成deadlineTime时使用的是服务器UTC时间,但HarmonyOS端Alarm API接收的是本地时间戳。时间戳本身不受时区影响,但我一开始在测试时直接把服务端返回的“2025-06-01T10:00:00Z”字符串交给Date.parse,再传给setAlarm,结果差出8个小时。解决方案是:任务数据里deadlineTime统一用毫秒时间戳,前端不要做任何字符串转换,直接调Alarm。
第二个是云数据库比较类型问题。AGC云数据库的字段类型定义后,查询条件里的值如果类型不一致,查出来是空的。比如deadlineTime存的是Number类型,查询时传了字符串“20250601100000”,结果一条都查不出来。排查半天才意识到是类型问题,后面把所有时间字段统一定义成毫秒时间戳Number,从此没再犯过。
第三个是并发更新问题。两个用户同时点击“完成”按钮,后端的update会造成状态覆盖或者重复日志。如果前端不做防抖,后端又不用事务,任务状态可能从“已完成”被改回“进行中”。我的做法是:前端按钮点击后置灰,后端云函数里对状态变更统一走“先查后改带事务”。
第四个是列表下拉刷新与状态更新的配合。用户在地里执行完一个任务,回到列表页刷新,如果接口返回的列表里还带着刚完成的任务,用户体验很差。我在这里做了一个前端过滤:本地保存一个completedTaskIds集合,渲染时过滤掉已经标记完成的任务,等下次全量刷新时再移除。这个方案简单实用,比单纯依赖服务端实时性更可靠。
最后再多说一句,我在把这一整套任务状态机跑通之后才发现,真正在农场里维持这套系统运转的,不是技术,而是管理规则的落实。任务管理模块上线之前,一定要跟农场管理者把两个问题谈透:第一,任务在什么条件下可以被自动转给别人;第二,逾期任务的追责机制是怎样的。系统能做的只是把规则固化下来,规则本身要靠人去定。这也是我在农业项目里最重要的经验——技术方案永远要跟着真正的业务流程走,而不是让业务流程去迁就技术设计。后面有时间,我再把这个模块跟传感器联动、语音播报结合的部分整理出来。
