以 1024 之名,这是我与代码的「双向奔赴」

这个项目,源于一次回家

国庆假期回老家,看到爷爷又在为麦地的事发愁。手里拿着一瓶农药,眯着眼睛看说明书上的小字,嘴里念叨着"这一亩地到底要兑多少水"。我凑过去看了看,心想这不就是个简单的数学计算吗?

回到学校后,这个画面一直在我脑子里转。恰好这学期要做移动应用开发的课程设计,我就跟导师提了个想法——做一个帮助老年人管理麦田的App。导师说可以试试,但提醒我HarmonyOS开发坑比较多,让我做好心理准备。

没想到,这一做就是两个多月。

第一次被ArkTS虐哭

刚开始的时候还挺兴奋的,下载了DevEco Studio,跟着官方文档开始搭项目。结果第一天就被ArkTS的类型系统劝退了。

我之前学的是JavaScript和Python,类型这东西一直都是随便写写就过了。但ArkTS不一样,它的类型检查严格到变态。写个对象必须先定义接口,函数参数必须标注类型,连map函数的返回值都要明确声明。

举个最简单的例子,我想把任务列表转成简化版本,在JavaScript里可以这么写:

// 我一开始的写法(报错!)
return tasks.map(task => ({
  id: task.id,
  name: task.taskName,
  date: task.dueDate
}))

结果编译器直接给我报红线:Object literal must correspond to some explicitly declared class or interface。我当时一脸懵,心想"这不就是个普通的map操作吗,咋还报错了"。

查了半天文档才知道,ArkTS要求所有对象字面量都必须有明确的类型。得这么写:

// 定义返回类型接口
interface SimpleTask {
  id: number;
  name: string;
  date: string;
}

// 明确指定返回类型
return tasks.map((task): SimpleTask => {
  return {
    id: task.id,
    name: task.taskName,
    date: task.dueDate
  }
})

我记得特别清楚,有一天晚上在宿舍写代码,舍友都睡了,我还在那儿跟编译器较劲。光是这种类型声明的问题,就改了快一个小时。当时真的很崩溃,差点想换回做Android原生开发。

后来去图书馆借了本《TypeScript权威指南》,又在B站上找了些视频教程,慢慢才摸到点门道。现在想想,那段时间虽然痛苦,但确实让我对类型系统的理解深了很多。期中考试那会儿,数据结构的题目我都觉得简单了不少。

宿舍熄灯后的数据库设计

做这个项目最大的挑战,其实是数据库设计。

我之前只在课堂上学过MySQL的基本操作,写几条简单的增删改查。但这个项目要用HarmonyOS的关系型数据库,而且业务逻辑比想象中复杂得多。

地块信息、任务计划、农事记录、销售流水、病虫害库……光是理清这些表之间的关系就花了我一周。我在草稿纸上画了无数遍ER图,问了数据库课的老师好几次,才最终敲定了方案。

比如任务计划表task_schedule,一开始我只设计了几个基本字段:

-- 我最初的设计(太简单了)
CREATE TABLE task_schedule (
    id INTEGER PRIMARY KEY,
    task_name TEXT,
    due_date TEXT
);

后来发现根本不够用。任务得关联到具体的地块,得有状态标记(待完成/已完成/已取消),得记录生育期,还得支持提前提醒。最后改成了这样:

CREATE TABLE task_schedule (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    field_id INTEGER NOT NULL,              -- 关联地块
    task_name TEXT NOT NULL,                -- 任务名称
    task_type TEXT NOT NULL,                -- 类型(浇水/施肥/打药等)
    growth_stage TEXT,                      -- 生育期
    due_date TEXT NOT NULL,                 -- 到期日期
    status TEXT DEFAULT 'pending',          -- 状态
    description TEXT,                       -- 详细说明
    reminder_days INTEGER DEFAULT 3,        -- 提前提醒天数
    completed_at TEXT,                      -- 完成时间
    created_at TEXT NOT NULL,
    FOREIGN KEY(field_id) REFERENCES field_info(id)
);

-- 还要加索引提升查询效率
CREATE INDEX idx_task_due_date ON task_schedule(due_date);
CREATE INDEX idx_task_status ON task_schedule(status);

最头疼的是任务自动生成功能。用户输入播种日期后,系统要自动生成整个生长周期的任务。这需要了解小麦从播种到收割的整个过程,什么出苗期、返青期、拔节期、灌浆期……

我在知网上下了好几篇农学论文,还专门打电话问爷爷小麦的种植流程。爷爷在电话那头说了半天,我拿着笔记本边听边记。舍友还笑我说"学计算机的怎么开始研究农业了"。

有天晚上宿舍11点熄灯了,我还在想这个算法怎么实现。就着台灯的光,在床上用笔记本写代码。突然灵感来了,把日期计算和节气判断结合起来,终于把逻辑理顺了:

// 根据播种日期生成冬小麦任务的核心逻辑
static generateWheatTasks(sowingDate: Date): Task[] {
  const tasks: Task[] = [];
  const year = sowingDate.getFullYear();
  
  // 出苗期:播后7-10天
  tasks.push({
    taskName: '出苗期检查',
    dueDate: this.addDays(sowingDate, 7),
    description: '检查出苗情况,缺苗及时补种',
    reminderDays: 1
  });
  
  // 越冬期:固定在11月下旬
  tasks.push({
    taskName: '冬灌作业',
    dueDate: new Date(year, 10, 25), // 11月25日
    description: '日平均气温3-5℃时进行冬灌',
    reminderDays: 5
  });
  
  // 返青期:次年2月底
  tasks.push({
    taskName: '返青期追肥浇水',
    dueDate: new Date(year + 1, 1, 28),
    description: '追施尿素10-15kg/亩,浇返青水',
    reminderDays: 5
  });
  
  // ... 还有6个生育期的任务
  
  return tasks;
}

那一刻真的很有成就感,感觉自己不只是在写代码,而是在把几千年的农业经验用数字化的方式传承下来。

实验室里的900多行DAO代码

写DAO层的时候,我基本都泡在实验室。

说实话,那段时间挺枯燥的。FieldDao、TaskDao,一个个方法写下来——insert、update、delete、query,每个方法都要考虑异常处理、事务管理、数据转换。

最开始我对HarmonyOS的数据库操作完全不熟。官方文档看得云里雾里,RelationalStore、RdbPredicates、ValuesBucket这些类都不知道怎么用。写第一个查询方法的时候,光是把ResultSet转成对象就卡了半天:

// 查询今日任务 - 一开始写得很乱
async getTodayTasks(): Promise<Task[]> {
  const today = new Date().toISOString().split('T')[0]; // 2024-10-28
  
  // 设置查询条件
  const predicates = new relationalStore.RdbPredicates('task_schedule');
  predicates.equalTo('due_date', today)
            .equalTo('status', 'pending');
  
  // 执行查询
  const resultSet = await this.store.query(predicates);
  
  // 这里卡了很久 - 怎么把ResultSet转成Task数组?
  const tasks: Task[] = [];
  while (resultSet.goToNextRow()) {
    // 一开始我不知道要用getColumnIndex,直接写索引号,结果数据全乱了
    const task: Task = {
      id: resultSet.getLong(0),  // 这样写太脆弱了
      fieldId: resultSet.getLong(1),
      taskName: resultSet.getString(2),
      // ...
    };
    tasks.push(task);
  }
  resultSet.close(); // 差点忘了关闭ResultSet,导致内存泄漏
  
  return tasks;
}

后来学长看了我的代码,说"这样写维护性太差,字段顺序一变就全乱了"。他教我用getColumnIndex方法:

// 改进后的版本
private parseTaskFromResultSet(resultSet: relationalStore.ResultSet): Task {
  return {
    id: resultSet.getLong(resultSet.getColumnIndex('id')),
    fieldId: resultSet.getLong(resultSet.getColumnIndex('field_id')),
    taskName: resultSet.getString(resultSet.getColumnIndex('task_name')),
    taskType: resultSet.getString(resultSet.getColumnIndex('task_type')),
    dueDate: resultSet.getString(resultSet.getColumnIndex('due_date')),
    status: resultSet.getString(resultSet.getColumnIndex('status')),
    // ... 其他字段
  };
}

写着写着就觉得很烦,想着"这些重复性的代码有什么意思"。但学长跟我说,这些基础的代码虽然无聊,却是整个系统的地基。地基打不牢,上层建筑都是空中楼阁。

我听了之后,还是坚持把每个方法都认真写完。现在光DAO层就有900多行代码,每个方法都加了详细的错误处理:

async insertTask(task: Task): Promise<number> {
  try {
    const valueBucket: relationalStore.ValuesBucket = {
      field_id: task.fieldId,
      task_name: task.taskName,
      task_type: task.taskType,
      due_date: task.dueDate,
      status: task.status || 'pending',
      created_at: new Date().toISOString()
    };
    
    const rowId = await this.store.insert('task_schedule', valueBucket);
    console.info(`任务插入成功,ID: ${rowId}`);
    return rowId;
    
  } catch (error) {
    console.error('插入任务失败:', error);
    throw new Error(`Failed to insert task: ${error.message}`);
  }
}

有次导师来实验室检查进度,看到我的代码,说"写得还挺规范,继续保持"。那是我第一次觉得,原来写代码不只是为了实现功能,代码质量本身也很重要。

一个让我调试了两天的Bug

说到调试,有个Bug让我印象特别深刻。

我在写农药配比计算器的时候,逻辑很简单:输入面积和稀释倍数,计算需要的农药量和水量。公式也很简单:

// 农药用量(毫升) = (总水量 / 稀释倍数) * 1000
static calculatePesticide(area: number, dilution: number): CalcResult {
  const sprayerCapacity = 15; // 喷雾器容量15升
  const sprayerCount = Math.ceil(area); // 每亩一个喷雾器
  const totalWater = sprayerCount * sprayerCapacity;
  const pesticideAmount = (totalWater / dilution) * 1000;
  
  return {
    totalWater,
    pesticideAmount,
    sprayerCount
  };
}

结果测试的时候发现,输入5亩地、1500倍稀释,计算出来的农药量居然是负数!我一脸懵逼,这怎么可能?

打了一堆console.log调试:

console.log('面积:', area);           // 5
console.log('稀释倍数:', dilution);    // 1500
console.log('喷雾器数量:', sprayerCount); // 5
console.log('总水量:', totalWater);     // 75
console.log('农药量:', pesticideAmount); // -2147483648 ???

折腾了两天才发现,问题出在数值溢出上。我用的是整数类型,计算过程中超出了范围。最后改成浮点数才解决:

// 修复后的版本
const pesticideAmount = (totalWater / dilution) * 1000.0; // 注意这里的1000.0

这个Bug让我明白,写代码真的要考虑各种边界情况。课本上学的理论知识,实际用起来完全不一样。

最让我触动的适老化设计

这个项目让我变化最大的,是对"用户体验"的理解。

以前做课程作业,总想着功能越炫越好,界面越酷越好。但这次不一样,用户是我爷爷那样的老年人。他们不会用复杂的界面,看不清小字,有些甚至不太会拼音输入。

我开始认真思考:如果是爷爷用这个App,他会遇到什么困难?

于是我把所有文字字号调到48sp以上,按钮做到80×80dp。每个功能都加上了语音播报:

// 语音播报工具类的简单封装
export class VoiceUtil {
  private static ttsEngine: textToSpeech.TextToSpeechEngine;
  
  static async speak(content: string): Promise<void> {
    try {
      if (!this.ttsEngine) {
        this.ttsEngine = await textToSpeech.createEngine({
          language: 'zh-CN',
          speed: 1.0,  // 正常语速,老人可能需要慢一点
          volume: 1.0
        });
      }
      
      await this.ttsEngine.speak(content);
    } catch (error) {
      console.error('语音播报失败:', error);
    }
  }
}

// 使用示例 - 在任务卡片上
@Component
struct TaskCard {
  @Prop task: Task;
  
  build() {
    Row() {
      Text(this.task.taskName)
        .fontSize(48)  // 超大字号
        .fontColor('#333333')
      
      // 语音播报按钮
      Button() {
        Image($r('app.media.voice_icon')).width(40).height(40)
      }
      .width(80)
      .height(80)
      .onClick(() => {
        VoiceUtil.speak(`${this.task.taskName}${this.task.description}`);
      })
    }
  }
}

最纠结的是紧急求助功能。我想做一个一键呼救的按钮,老人在地里出事的时候能及时求助。但怎么设计才能既防止误触,又不会太复杂?

试了好几种方案:

  • 单击:太容易误触
  • 双击:紧急情况下可能反应不过来
  • 长按2秒:感觉比较合适

最后实现的代码:

@Component
struct SOSButton {
  @State isActivated: boolean = false;
  
  build() {
    Button('紧急求助')
      .width(80)
      .height(80)
      .backgroundColor(this.isActivated ? '#FF0000' : '#FF6B6B')
      .fontSize(32)
      .position({ x: '85%', y: '90%' })
      .gesture(
        LongPressGesture({ duration: 2000 })
          .onAction(() => {
            this.triggerSOS();
          })
      )
  }
  
  triggerSOS() {
    this.isActivated = true;
    // 最大音量循环播报
    VoiceUtil.speak('紧急求助!请帮助我!');
    // 振动提醒
    vibrator.startVibration({ type: 'time', duration: 3000 });
    // 显示紧急联系人
    // ...
  }
}

国庆回家的时候,我拿着原型给爷爷看。他戴上老花镜,按着手机屏幕,仔细听语音播报。当他成功操作完一个功能后,脸上的笑容让我觉得,这两个月的熬夜都值了。

现在才75%,后面还有硬仗

到今天为止,项目完成度大概75%。

看一眼代码统计:

类别          文件数    代码行数
-------------------------------
常量类          2        200+
枚举类          5        100+
实体类          4        600+
数据库核心      1        400+
DAO层          2        900+
工具类          3        300+
-------------------------------
总计           17       2500+

基础框架搭好了,数据库设计完了,核心的实体类和DAO层也写完了。但距离真正能用,还差很多。农事日历的界面还没做,病虫害诊断功能刚开了个头,语音播报还在调试。

这周答辩在即,我还得赶紧把PPT做出来。导师说后面可以继续完善,甚至可以作为毕业设计的基础。我挺想把它做完的,不只是为了拿个好成绩,更多的是想真正帮到爷爷他们。

现在每天的状态基本是:上完课就去实验室写代码,写累了就去操场跑两圈,回来继续写。舍友说我最近有点魔怔,但我觉得还好,这种为了一个目标持续努力的感觉,挺充实的。

写在程序员节

今天是1024程序员节,室友们约着去吃火锅庆祝。我看了眼进度条,想了想还是先把这篇文章写完。

从9月初开始做这个项目到现在,差不多两个月。这是我第一次独立完成一个相对完整的项目,也是第一次觉得代码不只是作业,而是能真正帮到别人的东西。

HarmonyOS的坑确实多,ArkTS的类型系统确实严格,适老化设计确实费脑子。但正是这些挑战,让我从一个只会照着教程敲代码的学生,慢慢学会了独立思考、查阅文档、解决问题。

现在项目还没做完,不知道答辩能不能过,也不知道最后能做成什么样。但至少,我已经不再是那个只为了交差而写代码的我了。

感谢这个项目,感谢那些被编译器虐的夜晚,感谢导师的指导,也感谢爷爷给我的灵感。

希望每个像我一样的学生程序员,都能找到属于自己的那个"值得熬夜的项目"。

节日快乐。


2025年10月28日 晚
写于学校实验室
BGM: 《夜曲》- 周杰伦(单曲循环第37遍)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐