【共创季稿事节】毕业季·鸿蒙同行:我用 HarmonyOS 做了一个「鸿蒙求职罗盘」,陪自己走完校招这一程

投稿方向:毕业季·鸿蒙同行

一、写在前面:毕设做完了,求职才刚刚开始

几个月前,我完成了自己的毕业设计——一款叫「鸿蒙毕设伴侣」的应用,用它管理毕设进度、润色论文、演练答辩。写完那篇复盘文章时,我以为自己已经把"毕业季"这件事讲完整了。

但很快我意识到:毕设只是毕业季的上半场,求职才是下半场,而且往往更漫长、更煎熬。 投了简历石沉大海、面试被问住哑口无言、offer 迟迟不来的焦虑,比赶论文的焦虑更磨人——因为论文的截止日期是确定的,而求职的"什么时候能上岸"是不确定的。

我投了不少公司,投递记录散落在各个招聘 App 里,笔试通知在邮箱,面试安排在微信,复盘的心得只存在脑子里,过几天就忘了当时具体是怎么答的、哪里答得不好。我又想起了做毕设伴侣时的那个念头:我是一个正在学鸿蒙的学生,为什么不用鸿蒙做点什么,来解决我正在经历的问题?
在这里插入图片描述

于是就有了这款「鸿蒙求职罗盘」——一个专门陪应届生走完校招这一程的鸿蒙应用。它管理我的每一次投递,帮我把简历里模糊的项目经历改得更专业,陪我模拟面试官可能问的每一个刁钻问题,也记录下每一次面试后我自己的复盘和反思。

这款应用和「鸿蒙毕设伴侣」是姊妹项目——一个负责"毕业设计"这条线,一个负责"求职、实习与成长"这条线,两个应用合在一起,正好覆盖了「毕业季·鸿蒙同行」这个方向里我最想讲的故事。

先看一眼成品。这是应用在 DevEco Studio 里,运行在 Mate 80 Pro(HarmonyOS 6.1.0) 模拟器上的样子——投递看板页:

求职罗盘投递看板,运行在 HarmonyOS 6.1.0 模拟器上

三张统计卡片一目了然:总投递、进行中、Offer。下面是具体的投递记录——华为(内推,二面)、某互联网大厂(笔试)、某创业公司(已投递)。这是我自己求职季最真实的状态截面:几家在谈、几家在等、一家还没消息。

二、需求梳理:求职季最真实的四个痛点

在写代码之前,我先诚实地梳理了自己求职这段时间遇到的四个具体痛点:

  1. 投递记录到处乱记,进度记不清。 投了十几家公司,每家进展到哪一步(笔试?一面?二面?)全靠脑子记,经常搞混。
  2. 简历写不出彩,项目经历流水账。 我的项目经历描述总是像"做了一个xx功能",缺乏量化和亮点,不知道怎么改。
  3. 面试没底气,不知道会被问什么。 尤其是技术面,总担心被问到自己没准备的知识点,一慌就语无伦次。
  4. 面完就忘,同样的问题下次还是答不好。 每次面试完只有一个模糊的"感觉还行/感觉不太好",没有具体记录,下次遇到类似问题还是老样子。

这四个痛点,对应了应用的四个 Tab:看板、简历、面试、我的。和做毕设伴侣时一样,我坚持"先想清楚自己真实需要什么,再决定做什么功能"——这是我做这两个项目学到的最重要的一课。

三、架构设计:复用经验,快速搭建

3.1 和毕设伴侣同构的技术路线

做完毕设伴侣之后,我发现"AI 出题→作答→点评"这套模式几乎可以套用在任何"需要陪练"的场景——答辩演练是这样,模拟面试也是这样。所以这次开发,我大量复用了已验证过的架构模式:

entry/src/main/ets/
├── entryability/EntryAbility.ets   入口:初始化、沉浸式全屏、安全区
├── common/
│   ├── Theme.ets              深空蓝罗盘风主题
│   ├── AIConfig.ets           模型配置 + 求职顾问系统提示词
│   └── TimeUtil.ets           相对时间格式化
├── model/
│   ├── JobStore.ets           数据模型 + 持久化 + 状态机配色
│   └── AIService.ets          AI 服务:多轮对话 + 单轮任务
└── pages/
    ├── Index.ets              四 Tab 主壳
    ├── BoardTab.ets           投递看板
    ├── ResumeTab.ets          AI 简历顾问
    ├── InterviewTab.ets       模拟面试
    └── MyTab.ets              复盘日记 + 我的

这次开发速度明显比第一次做毕设伴侣快很多——因为架构分层的思路已经跑通过一次,AIService 的多轮对话封装、任务模板法、JSON/文本解析容错,这些方法论都可以直接照搬。这让我真切体会到"复用架构经验"这件事的价值:第一个项目投入的心血,会在第二个项目里加速兑现,这也是成长的一种具体体现。
在这里插入图片描述

3.2 四个 Tab 对应四个痛点

  • 🧭 看板:投递状态机管理,解决"记不清进度"
  • 📄 简历:AI 简历顾问,解决"写不出彩"
  • 🎤 面试:模拟面试陪练,解决"没底气"
  • 👤 我的:复盘日记,解决"面完就忘"

四、投递看板:把"记不清"变成"看得见"

看板要解决的核心问题是:十几家公司的投递进度,要在一个地方一眼看清

4.1 投递状态机

我把求职投递抽象成一个七态状态机:

export const STAGES: string[] = ['已投递', '笔试', '一面', '二面', '三面', 'Offer', '已挂'];

export interface JobApp {
  id: string;
  company: string;       // 公司名
  position: string;      // 岗位
  stage: string;         // 当前状态(STAGES 之一)
  note: string;          // 备注(渠道/内推人等)
  updateTime: number;
}

每条投递记录点击状态标签,会弹出底部选择框,选一个新状态就完成流转:

static async updateStage(id: string, stage: string): Promise<JobApp[]> {
  const cur = await JobStore.listApps();
  for (const a of cur) { if (a.id === id) { a.stage = stage; a.updateTime = Date.now(); } }
  const sorted = cur.sort((a: JobApp, b: JobApp) => b.updateTime - a.updateTime);   // 更新后重新按时间排序
  await JobStore.saveApps(sorted);
  return sorted;
}

状态更新的同时刷新 updateTime 并重新排序,这样最近有动态的投递会自动排到列表最前面——这是个小细节,但对求职者来说很实用:你最关心的永远是"最近有进展的那几家",而不是按添加时间死板排列的列表。

4.2 状态配色,一眼分辨紧急程度

每个状态对应一个颜色,从"已投递"的中性灰,到"二面/三面"的warning色,再到"Offer"的成功绿、"已挂"的警示红:

export function stageColor(stage: string): string {
  switch (stage) {
    case '已投递': return '#647291';
    case '笔试': return '#3E8EF7';
    case '一面': return '#9E7BFF';
    case '二面': return '#F5B93E';
    case '三面': return '#FF8A5B';
    case 'Offer': return '#2FD9A8';
    case '已挂': return '#F76A6A';
    default: return '#647291';
  }
}

配合概览区的三张统计卡(总投递、进行中、Offer 数),打开应用第一眼就能看到自己求职的"战况"——这种"数字化自己进度"的感觉,比脑子里模糊记着"投了好多家,进展不知道"要踏实得多。

五、AI 简历顾问:把流水账改成有亮点的表达

这是我认为整个应用里,对求职最直接有用的一个功能。

5.1 四类任务模板

简历问题千差万别,但常见的求助场景可以归纳成四类:润色经历、写自我介绍、STAR 法则改写、岗位匹配分析。我依然沿用"任务模板 + 用户输入"的套路:

private tasks: QuickTask[] = [
  { label: '润色经历', tpl: '请帮我润色下面这段项目经历描述,使其更专业、有量化亮点,并指出改进点:\n{X}' },
  { label: '自我介绍', tpl: '我的背景是:{X}。请帮我写一段 1 分钟左右的求职自我介绍(技术岗)。' },
  { label: 'STAR改写', tpl: '请用 STAR 法则(情境-任务-行动-结果)改写下面这段经历,突出个人贡献与量化成果:\n{X}' },
  { label: '岗位匹配', tpl: '这是招聘岗位要求:{X}。请分析我应该在简历中突出哪些技能与项目,并给出建议。' }
];

5.2 真实效果:AI 一针见血地指出问题

下图是我把自己简历里一段"项目经历"贴给 AI 润色的真实效果——我原本写的是一段更像"个人博客简介"的描述(“关注 HarmonyOS、Flutter、人工智能及跨端开发技术,喜欢分享技术文章”),AI 直接指出了三个问题:

AI 简历顾问指出经历描述的三个具体问题

AI 反馈非常直接:定位模糊(像博客简介而非简历内容)、缺乏量化成果与技术深度、未体现工程能力与业务价值。这三条反馈,说实话,比我自己反复读十遍都更能一针见血地戳中问题。这也是为什么我把系统提示词里 AI 的人设定得很明确:

static readonly systemPrompt: string =
  '你是一位资深的校招/实习求职顾问兼技术面试官,熟悉简历优化、自我介绍、' +
  '技术面试与 HR 面试的常见套路。请用专业、具体、鼓励性的中文帮助应届生求职,' +
  '回答要可操作、有针对性,避免空泛的套话。';

“专业、具体、鼓励性”、“可操作、有针对性、避免空泛套话”——这几个关键词是我反复调试后确定的,第一版提示词让 AI 给出的建议比较笼统("建议增加更多细节"这类),加上"避免空泛的套话"这句限制后,AI 的反馈明显更犀利、更具体了。提示词里明确禁止什么,有时候比说明白要求什么更有效。

5.3 多轮追问:把改到满意为止

首轮任务模板发送后,后续的对话就自动进入自由追问模式:

const prompt = this.messages.length === 0
  ? this.tasks[this.activeTask].tpl.replace('{X}', raw)   // 首轮:套用任务模板
  : raw;                                                  // 后续:自由追问

拿到 AI 指出问题后,我可以接着问"那你帮我改一版",或者"我这个项目具体是做了xx功能,怎么量化",一路把简历改到自己满意为止。这种多轮打磨的过程,本身就是一次很好的自我梳理。

六、模拟面试:技术面 + HR 面双模式陪练

面试焦虑的核心是"未知"——不知道会被问什么。这个功能就是要把未知变成已知,提前练。

6.1 技术面 / HR 面切换

我把面试拆成两种模式,因为技术面和 HR 面的问题画像完全不同:

private modes: string[] = ['技术面', 'HR面'];

const modeDesc = this.mode === 0 ? '技术面试官' : 'HR 面试官';
const prompt = `我的求职背景是:「${this.topic.trim()}」。` +
  `请站在${modeDesc}角度,预测 5 个最可能被问到的面试问题。` +
  `只输出问题列表,每行一个问题,用数字编号,不要额外说明。`;

6.2 真实效果:Java 技术栈的面试题预测

我输入"java"作为技术方向,切到"技术面"模式,AI 生成的问题相当扎实:

模拟面试·技术面,AI 生成 Java 相关面试题

三个问题分别是——HashMap 的底层实现与 JDK 8 优化点synchronized 与 ReentrantLock 的区别Java 内存模型与 volatile 的可见性/有序性/原子性。这几乎就是 Java 后端面试的高频三连问,专业度完全不输真实的面试官。

切到"HR面"模式,同样输入"java",AI 生成的问题风格完全不同:

模拟面试·HR面,AI 生成的问题更偏软技能与动机

自我介绍、为什么选 Java(技术选型动机)、遇到过最棘手的问题及如何解决——明显能感受到 HR 面更关注表达能力、职业动机和解决问题的思路,而不是纯技术细节。同一个技术方向,两种模式下 AI 给出的问题画像截然不同,这背后靠的就是提示词里"站在技术面试官角度"和"站在 HR 面试官角度"这一句话的差异。这让我意识到,在提示词里明确身份视角,是让大模型输出符合场景预期的一个简单却极其有效的手段。

6.3 出题解析与容错

和毕设伴侣的答辩演练一样,我要求 AI"只输出编号问题列表",然后用正则清洗:

private parseQuestions(text: string): string[] {
  const lines = text.split('\n');
  const out: string[] = [];
  for (const raw of lines) {
    const line = raw.trim();
    if (line === '') { continue; }
    const cleaned = line.replace(/^[Qq]?\s*\d+\s*[\.\、\:\))\.]?\s*/, '').trim();   // 去除各种编号格式
    if (cleaned !== '') { out.push(cleaned); }
  }
  return out.slice(0, 8);
}

这套解析逻辑我在两个项目里几乎原封不动地复用了——这也印证了前面说的,方法论层面的经验,会随着项目积累越来越好用

6.4 作答与点评闭环

每道题下面有"练习作答",写完提交后 AI 会点评:

const prompt = `${modeDesc}面试问题:${this.questions[i]}\n` +
  `候选人的回答:${this.answer.trim()}\n` +
  `请作为面试官,简要点评这个回答(优点、不足、改进建议),控制在 150 字内。`;

"出题→作答→点评"形成完整闭环,相当于把面试现场提前搬到自己房间里反复练习。我练完 Java 并发那道题之后,发现自己对 synchronized 底层锁升级机制讲得含糊,这正是复盘价值所在——练习中暴露的漏洞,比面试现场暴露的漏洞成本低得多。

七、复盘日记:把"面完就忘"变成持续的成长记录

这是我认为整个应用里,最能体现"成长"这个主题的功能。

7.1 为什么复盘比刷题更重要

求职期间我发现一个现象:很多人(包括曾经的我)面试完只会有一个模糊的印象——“感觉还行"或"感觉挂了”,但具体问了什么、哪里答得不好、下次该怎么改进,都随着时间被遗忘。没有记录的经验,等于没有发生过的经验。

所以我设计了「复盘日记」这个模块,鼓励自己每场面试后花 5 分钟,把具体问题和反思记下来:

export interface ReviewLog {
  id: string;
  company: string;
  title: string;         // 简述,如"一面复盘"
  content: string;
  time: number;
}

7.2 一条真实的复盘记录

下图是「我的」页面,展示了一条我自己写的真实复盘——华为一面之后的记录:

我的页面:复盘日记与求职同行者主题

复盘内容是这样写的:“问了 ArkTS 与 TypeScript 的区别、@State/@Prop 状态管理原理,还问了项目里怎么处理大模型输出不稳定的问题。答得还算流畅,但对分布式能力细节回答不够深入,需要补一下。”

这条记录很朴素,但价值巨大——它精确指出了我在"分布式能力"这个知识点上的薄弱环节。如果没有这条记录,我可能过几天就忘了这个具体的薄弱点,下一场面试遇到类似问题还是会卡壳。 有了它,我在准备下一场面试前,会专门去补一下鸿蒙分布式能力的细节,这就是"记录"带来的实际改进。

页面顶部我特意起了个标题——“求职同行者”,配文"每一次面试,都是一次成长"。这不是一句空洞的口号,而是我做这个功能时最真实的心态:求职路上每一次挫折,只要认真复盘,都能转化成下一次的底气。

7.3 时间线可视化

复盘记录用竖向时间线展示,每条记录带公司名、标题、内容摘要和相对时间:

Row({ space: 10 }) {
  Column() {
    Circle({ width: 10, height: 10 }).fill(C.primary)
    if (i < this.reviews.length - 1) {
      Divider().vertical(true).color(C.stroke).strokeWidth(2).height(40)
    }
  }.width(14).alignItems(HorizontalAlign.Center)
  Column({ space: 4 }) {
    Row() {
      Text(r.company + ' · ' + r.title).fontWeight(FontWeight.Bold).layoutWeight(1)
      Text('✕').onClick(() => { this.onDelete(r.id); })
    }.width('100%')
    Text(r.content).fontSize(13).fontColor(C.textSub).width('100%')
    Text(fmtRelative(r.time)).fontSize(11).fontColor(C.textDim)
  }
}

随着求职季推进,这条时间线会越来越长,回头看时能清楚感受到自己"面试能力在一次次复盘中不断被打磨"的过程——这和毕设伴侣里的"成长里程碑时间线"是同一套设计思路,都是在用最朴素的方式,让"成长"这件抽象的事情变得可视化、可回顾。

八、开发环境:在 HarmonyOS 6 全系设备上验证

这次开发依然是在最新的 HarmonyOS 6(API 23) 环境下完成。打开 DevEco Studio 的设备选择器,能看到从 Mate 80 Pro、Mate X7、Pura 90 到 MatePad Pro 13、MateBook Pro 的全系列 HarmonyOS 6.1.0 设备:

DevEco Studio 设备选择器,README 展示求职罗盘完整功能表

我最终选择 Mate 80 Pro(HarmonyOS 6.1.0) 作为主力调试设备。能在这么完整的一套设备矩阵下验证应用的运行效果,对一个还在学习阶段的开发者来说是很难得的资源——这也从侧面印证了鸿蒙生态这几年发展的速度。

九、AI 服务复用:架构经验的复利效应

这一节我想单独展开讲讲"复用"这件事,因为它是这次开发速度明显加快的核心原因。

9.1 几乎照搬的 AIService

对比「毕设伴侣」和「求职罗盘」的 AIService.ets,两者的多轮对话封装几乎是一模一样的结构:

export class AIService {
  static async chat(history: ChatMessage[]): Promise<string> {
    const messages: ChatMessage[] = [{ role: 'system', content: AIConfig.systemPrompt }];
    for (const m of history) { messages.push(m); }
    // ... 发起请求、超时控制、异常兜底、finally 释放资源
  }

  static async task(prompt: string): Promise<string> {
    return AIService.chat([{ role: 'user', content: prompt }]);
  }
}

唯一的差异只是 AIConfig.systemPrompt 里的人设描述——一个是"毕业设计指导老师",一个是"校招求职顾问兼技术面试官"。核心的网络请求逻辑、超时控制、异常兜底、资源释放,全部可以直接复用。

9.2 复用带来的真实收益

这次开发,从搭工程骨架到四个 Tab 全部功能跑通,明显比第一次做毕设伴侣快了不少。具体体现在:

  • 架构分层不用重新设计:直接照搬"入口层不碰网络、AI 交互收敛在 model 层"的分层原则。
  • AI 服务封装可以复制粘贴AIService.chat/task 几乎原样搬过来,只改提示词。
  • 交互模式可以复用:"任务模板+多轮追问"用在了简历顾问,"出题-作答-点评"用在了模拟面试,都是从毕设伴侣里搬过来的成熟模式。
  • 踩过的坑不会再踩@Prop 装饰器不能漏、自定义组件属性不能跟内置属性方法同名、build() 方法只能有一个根容器……这些坑在第一个项目里踩过一次,这次写代码时会本能地避开。

这让我真切体会到一件事:技术成长不是线性的,而是有复利效应的。第一个项目投入的时间和踩坑成本,会在后续项目里持续兑现。这也是我想对同样在学鸿蒙的同学说的——不要怕做第一个项目慢,方法论和踩坑经验积累起来后,第二个、第三个项目会明显加速。

十、这次踩到的新坑:ArkUI 保留字与容器根节点

虽然大部分架构是复用的,但求职罗盘的开发过程也不是一帆风顺,编译时遇到了两个新的报错,记录下来。

10.1 自定义方法名与框架保留字冲突

Methods, properties and accessors in structures decorated by '@Component' 
and '@ComponentV2' cannot have name as 'activeCount'.

我在 BoardTab 组件里写了一个统计"进行中投递数"的方法,随手命名为 activeCount,结果编译报错——activeCount 是 ArkUI 框架内部保留的名字(大概率与 Tabs/动画等内部活跃状态计数机制相关),不能被自定义组件的方法或属性使用。解决很简单,换个名字:

// 之前:activeCount() —— 与框架保留字冲突
private notFailedCount(): number {
  return this.list.filter((a: JobApp) => a.stage !== '已挂').length;
}

这个坑告诉我一个经验:ArkUI 组件内部方法命名,最好避开一些"看起来很像框架内置概念"的通用词(如 activestatecurrent 这类),命名时稍微具体一点(比如 notFailedCount 而不是 activeCount),能降低撞上保留字的概率。

10.2 build() 方法的根容器规则

In an '@Entry' decorated component, the 'build' method can have only one 
root node, which must be a container component.

我在「我的」页面想在 Scroll 滚动区域之外叠加一个条件渲染的底部弹窗(写复盘的表单),最初写成了这样:

// 错误写法:build() 里出现了两个平级根节点
build() {
  Scroll() { /* ... */ }
  if (this.showAdd) { this.addSheet() }   // 和 Scroll 是平级的第二个根节点!
}

build() 方法的规则是有且仅有一个根容器组件,不能出现两个平级的顶层节点。修复方式是用 Stack 把两者包裹成一个整体:

build() {
  Stack({ alignContent: Alignment.Bottom }) {
    Scroll() { /* ... */ }
    if (this.showAdd) { this.addSheet() }   // 现在是 Stack 内部的子节点,合法
  }
  .width('100%').height('100%')
}

这个坑其实我在毕设伴侣的看板页里已经用 Stack 处理过一次(当时是为了叠加"新增任务"的底部弹窗),但这次写「我的」页时因为切换了心思专注在复盘日记的逻辑上,一时疏忽又漏掉了这个包裹。这提醒我,即使是已经验证过的模式,写新页面时也要对照检查一遍,不能凭记忆想当然。

十一、总结:求职是一场值得被认真记录的修行

回顾这个项目,它对我的意义和「鸿蒙毕设伴侣」一样——技术上是练手,情感上是陪伴

从技术角度,这次开发验证了一件事:当你把一个项目的架构、方法论沉淀下来,第二个类似的项目会明显更快、更稳。从投递看板的状态机设计,到 AI 顾问的提示词调优,再到面试陪练的出题-点评闭环,几乎每一处都能看到上一个项目的影子,但又结合求职场景做了针对性调整。
在这里插入图片描述

从意义角度,这个应用陪我走过了求职季最焦虑的那段时间。它让我看到自己投递的全貌,不再被"记不清进度"的焦虑困扰;它帮我把简历里模糊的表达改得更专业;它让我在真正的面试前,先在自己的应用里练了一遍又一遍;它记录下每一次面试后的反思,让"这次没答好"不再是白白经历的挫折,而是变成了下一次的底气。

如果你也是一名正在求职的鸿蒙方向应届生,我想把这段经历里最真实的几点体会留给你:

  1. 求职的焦虑,往往来自"不确定",而"记录"是对抗不确定感最好的工具。 把投递进度、复盘心得都写下来,你会发现自己其实比想象中更有掌控感。
  2. 让 AI 帮你换个角度看自己。 简历也好,面试作答也好,我们对自己的表达往往有盲区,AI 的第三方视角能帮你更快发现问题。
  3. 技术积累是有复利的。 如果你也在做鸿蒙项目,不要怕第一个项目做得慢、踩坑多——那些经验会在你做第二个项目时兑现成实实在在的效率提升。

毕业季,鸿蒙同行。愿每一位正在投简历、赶面试的同学,都能把这段求职路走得踏实、走得心里有数,最终找到属于自己的方向。

如果你对这个项目、对鸿蒙开发、或者对求职这件事本身有任何想聊的,欢迎交流——我们都是这条路上的同行者。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐