#星光不负 码向未来# 从“写页面”到“做能力”:一个大学生自学上岸的鸿蒙实战复盘 原创

杰阔尔娜
发布于 2025-10-29 22:05
浏览
0收藏


一、我的转向:从前端的“页面交付”到鸿蒙的“能力交付”

大学时我靠着网课入门前端,做过 H5、小程序、Flutter 外包。那时我的目标是“把页面做出来”;进入 HarmonyOS 之后,我第一次把目标改成了“把能力做出来”——比如把预约、设备发现、能力开放、数据一致性、故障自愈这些“系统级事情”真正落到业务。


#星光不负 码向未来# 从“写页面”到“做能力”:一个大学生自学上岸的鸿蒙实战复盘-鸿蒙开发者社区


我长得比较老成,当然啦(=゚ω゚)ノ这篇文章复盘一个落地项目:「校园自习室预约」原子化服务(元服务)。它不是“把网页搬到端上”,而是面向 快进快出、无学习成本、可离线缓存、云端对账能力组合拳。也顺便聊聊我如何一边做、一边把 Java 补起来,完成从“前端视角”到“全链路视角”的跃迁。


顺便秀一下,下面我最近刚做的外包项目( ´ ▽ ` )ノ。

#星光不负 码向未来# 从“写页面”到“做能力”:一个大学生自学上岸的鸿蒙实战复盘-鸿蒙开发者社区


二、业务场景与目标

场景痛点

  • 入口碎片化:同学在群里点链接、扫海报码、看活动页,层层跳转。
  • 高峰抢占:饭点 & 晚自习 10 分钟内流量陡增,容易超卖。
  • 使用心智:预约只是临时动作,用户不想“打开 app → 找入口 → 登录 → 搜索房间”。

目标设计

  1. 原子化服务(元服务)+ 桌面卡片实现0 学习成本即点即用
  2. AGC Cloud DB + 云函数并发校验一致性
  3. App Linking 统一入口(H5/海报/短信一键直达预约详情);
  4. 关键链路毫秒级反馈:首交互 < 300ms,提交排队有可视化进度。

三、架构与能力选型

  • 入口层:二维码 / H5 链接(App Linking) → 拉起元服务或 App 指定 Page
  • 端侧数据层:Cloud DB 本地缓存(弱网读缓存)
  • 服务编排:Cloud Functions(并发校验、配额控制、可撤销队列)
  • 呈现形态元服务(Atomic Service) + 服务卡片(Form) + 常规页面
  • 埋点:应用分析(打点预约成功率、冲突占比、P95 延时)

与我上一次做的项目完全不同:本项目不依赖近场/软总线,主打元服务 + 云开发的“轻入口、强后端校验”路径。


四、关键实现

4.1 App Linking:海报/短信一键直达预约详情

module.json5​(片段,按你项目实际域名与字段适配)

{
  "module": {
    "abilities": [{
      "name": "EntryAbility",
      "skills": [{
        "actions": [ "action.view" ],
        "uris": [{
          "scheme": "https",
          "host": "study.自己的网页.com",
          "paths": [ "/room/*" ]
        }]
      }]
    }]
  }
}

解析:二维码指向 ​​https://study.自己的网页.com/room/ROOM_1203?slot=20-22​​ 系统校验通过后,直达对应房间与时段的预约页/元服务,无需“先找入口”。


4.2 元服务(原子化服务)+ 服务卡片:不打开 App 也能预约/取消

卡片 UI(ArkTS 示意,核心是动作事件透传到 FormExtensionAbility)

// form/Card.ets
@Component
export struct RoomCard {
  @Prop roomId: string
  @State slot: string = ''
  @State brief: string = '点击预约 / 查看状态'

  build() {
    Column({ space: 8 }) {
      Text(`自习室 ${this.roomId}`).fontSize(18).fontWeight(FontWeight.Medium)
      Text(this.brief).fontSize(14)
      Row({ space: 12 }) {
        Button('预约').onClick(() => postCardEvent('reserve', { roomId: this.roomId, slot: this.slot }))
        Button('取消').onClick(() => postCardEvent('cancel', { roomId: this.roomId, slot: this.slot }))
      }
    }.padding(16)
  }
}

// 将事件抛给卡片宿主(由 FormExtensionAbility 接收)
function postCardEvent(type: string, payload: Record<string, string | number>) {
  // 实际用 FormExtensionAbility 的 onEvent 接收,以下为示意
  (globalThis as any).__FORM_EVENT__?.({ type, payload })
}

卡片扩展(FormExtensionAbility 处理事件 → 调用云函数)

// form/FormExtAbility.ets(示意)
export default class FormExtAbility extends FormExtensionAbility {
  async onAddForm(formId: string, want: Want): Promise<void> {
    // 初始化卡片数据:最近可预约时间段等
    await this.updateForm(formId, { slot: nextAvailableSlot() })
  }

  async onEvent(formId: string, message: string): Promise<void> {
    const evt = JSON.parse(message) // { type, payload }
    if (evt.type === 'reserve') {
      const ok = await reserveRoom(evt.payload.roomId, evt.payload.slot) // 调云函数
      await this.updateForm(formId, { brief: ok ? '预约成功' : '名额已满' })
    } else if (evt.type === 'cancel') {
      const ok = await cancelRoom(evt.payload.roomId, evt.payload.slot)
      await this.updateForm(formId, { brief: ok ? '已取消' : '取消失败' })
    }
  }
}

4.3 端侧数据读写:Cloud DB(弱网下走本地缓存)

// data/clouddb.ts(示意,以实际 AGC 接口为准)
import agconnect from '@hw-agconnect/core'
import clouddb from '@hw-agconnect/clouddb'

let zone: any

export async function initCloudDB() {
  await agconnect.instance().config({ /* your agc options */ })
  const cloudDB = clouddb.AGConnectCloudDB.getInstance(agconnect.instance())
  await cloudDB.initialize()
  zone = await cloudDB.openCloudDBZone('room_booking', {
    syncMode: clouddb.CloudDBZoneConfig.CloudDBZoneSyncProperty.CLOUDDBZONE_CLOUD_CACHE,
    accessProperty: clouddb.CloudDBZoneConfig.CloudDBZoneAccessProperty.CLOUDDBZONE_PUBLIC
  })
}

export async function queryRoomStatus(roomId: string, slot: string) {
  const query = clouddb.CloudDBZoneQuery.where('Booking')
    .equalTo('roomId', roomId).and().equalTo('slot', slot)
  const result = await zone.executeQuery(query)
  return result.getSnapshotObjects() // 本地有缓存,弱网也能读
}

4.4 并发校验与超卖防护:云函数里的“单时段互斥”

我最初只在端上做“剩余名额计算”,结果高峰期并发冲突仍然发生。后来把校验挪到云函数里,用事务(或乐观锁)保证同一 ​​roomId+slot​​ 的原子更新

Node.js 云函数(逻辑示意)

// functions/reserveRoom.js
const { getDB, withTransaction } = require('./_db') // 你自己的封装

exports.main = async (event, context) => {
  const { roomId, slot, uid } = event
  const db = await getDB()

  return withTransaction(db, async (tx) => {
    const key = `${roomId}@${slot}`
    const quota = await tx.get('Quota', key) // { total, used, version }
    if (!quota || quota.used >= quota.total) {
      return { ok: false, reason: 'FULL' }
    }
    // 幂等:同一人重复提交直接返回成功
    const existing = await tx.get('Booking', `${key}#${uid}`)
    if (existing) return { ok: true, dup: true }

    // 乐观锁:版本号或 compare-and-set
    await tx.update('Quota', key, { used: quota.used + 1, version: quota.version + 1 }, { matchVersion: quota.version })
    await tx.put('Booking', `${key}#${uid}`, { roomId, slot, uid, ts: Date.now() })
    return { ok: true }
  })
}

这一步是我“补 Java/后端思维”的分水岭:先用 Node 实现事务校验,随后我又用 Java 写了一个管理端小服务(导出报表、手动解锁异常名额、查看冲突日志),从只写 UI 的我,走到了能把业务闭环跑起来

Java(管理端控制器示例,Spring Web 轮廓)

@RestController
@RequestMapping("/admin/booking")
public class BookingAdminController {
  private final BookingService bookingService;

  public BookingAdminController(BookingService bookingService) {
    this.bookingService = bookingService;
  }

  @PostMapping("/unlock")
  public Resp unlock(@RequestParam String roomId, @RequestParam String slot) {
    bookingService.unlockQuota(roomId, slot); // 释放异常卡死的占用
    return Resp.ok();
  }

  @GetMapping("/export")
  public void export(@RequestParam String date, HttpServletResponse res) {
    res.setHeader("Content-Disposition", "attachment; filename=booking-"+date+".csv");
    bookingService.exportCsv(date, res);
  }
}

五、上线与指标

  • 入口:App Linking 直达落地页/元服务,点开即见可预约时段
  • 峰值并发:晚间高峰 10 分钟内请求数较上月提升 3.2 倍,但超卖为 0
  • 端侧体验:弱网下读本地缓存,首交互 P95 < 280ms
  • 订单一致性:云函数事务化校验 + 定时对账(Java 管理端),异常单自动回收;
  • 用户反馈:学生会说“像点开闹钟卡片一样快”,辅导员的投诉量下降。

六、我的成长曲线(给还在观望的你)

  1. 从组件到系统:不再停留在页面美观,而是会问“这件事应该由系统能力承担到哪一层?”
  2. 从单端到闭环:端侧缓存、云端事务、对账回收、管理端导出——缺一环都落不到“可用”。
  3. 从 JS 到 Java:哪怕只是做了“导出报表 + 异常解锁”,也让我真正理解“业务的最终形态是流程”,而流程强依赖后端治理。
  4. 从 KPI 到体验指标:不再口说“很快”,而是用 P95/P99、成功率、冲突率来驱动优化。

七、经验与坑

  • 元服务/卡片设计要“只做一件事”:预约/取消之外的复杂流程,跳转 App 页面完成。
  • App Linking 的域验证提前做(证书、跳转校验),否则 H5 无法直达端内能力。
  • 云端防超卖必须放在事务里(或乐观锁更新),端上只能提示缓存
  • 弱网体验靠缓存与延迟同步:读缓存 + 后台刷新,用可见的加载状态降低焦虑。
  • 埋点分层:系统层(冷启动、ANR)与业务层(预约成功率、冲突率)分开看。

八、最后小总结

“以二进制刻录世界,用代码编织未来”。
这段从“学生—自学前端—自学鸿蒙—补 Java”的路,让我真正理解 HarmonyOS 的价值:把能力交到终端、把复杂留在云端、把入口做成“触手可及”的元服务。
如果你也在观望,不妨先做一个一屏一事的小原子化服务——当你把“快、准、稳”交付给用户的那一刻,你也会和我一样,喜欢上鸿蒙

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐