鸿蒙应用开发实战【38】— 数据导出与备份:JSON 序列化

鸿蒙应用开发实战【38】--- 数据导出与备份:JSON 序列化

本文是「号码助手全栈开发系列」第 38 篇,持续更新中...

开源社区:https://openharmonycrossplatform.csdn.net


前言

数据是用户最宝贵的资产。号码助手提供了备份导出功能,让用户可以将数据库中的卡号、绑定关系导出为 JSON 文件,保存到本地或云盘。当换机或重装时,可以通过导入备份文件一键恢复。本篇将实现数据导出与备份的完整链路。

本篇涵盖:数据导出流程图、BackupData 备份数据模型设计、三张表的 JSON 序列化、使用 @ohos.file.fs 写入文件、文件保存到沙盒与分享导出、增量备份策略、备份文件格式版本管理。


一、备份数据模型

1.1 BackupData 结构

typescript 复制代码
export interface BackupData {
  // 备份元信息
  version: number;          // 备份文件格式版本
  app: string;              // 应用标识
  exported_at: string;      // 导出时间

  // 核心数据
  cards: CardEntity[];
  app_bindings: AppBindingEntity[];
  sms_candidates: SmsCandidateEntity[];

  // 可选的统计摘要
  summary?: {
    totalCards: number;
    totalBindings: number;
    totalCandidates: number;
  };
}

1.2 版本管理

typescript 复制代码
export const BACKUP_VERSION = 1;

未来数据库结构变更时,备份格式版本号递增,导入时可以按版本做兼容处理。


二、导出流程

2.1 BackupService

typescript 复制代码
import { relationalStore } from '@kit.ArkData';
import { fileIo } from '@kit.CoreFileKit';
import { CardDao } from '../dao/CardDao';
import { AppBindingDao } from '../dao/AppBindingDao';
import { SmsCandidateDao } from '../dao/SmsCandidateDao';

export class BackupService {
  private cardDao: CardDao;
  private bindingDao: AppBindingDao;
  private candidateDao: SmsCandidateDao;

  constructor(store: relationalStore.RdbStore) {
    this.cardDao = new CardDao(store);
    this.bindingDao = new AppBindingDao(store);
    this.candidateDao = new SmsCandidateDao(store);
  }

  /**
   * 导出全量数据
   */
  async exportAll(): Promise<BackupData> {
    // 并行查询所有数据
    const [cards, app_bindings, sms_candidates] = await Promise.all([
      this.cardDao.listAll(),
      this.bindingDao.listAll(),
      this.candidateDao.listAll(),
    ]);

    const backup: BackupData = {
      version: BACKUP_VERSION,
      app: 'haomazhushou',
      exported_at: new Date().toISOString(),
      cards,
      app_bindings,
      sms_candidates,
      summary: {
        totalCards: cards.length,
        totalBindings: app_bindings.length,
        totalCandidates: sms_candidates.length,
      },
    };

    return backup;
  }
}

2.2 序列化为 JSON 字符串

typescript 复制代码
function backupToJson(backup: BackupData): string {
  return JSON.stringify(backup, null, 2);
}

使用 JSON.stringify 的第三个参数 2 格式化输出,让导出的 JSON 文件可读:

json 复制代码
{
  "version": 1,
  "app": "haomazhushou",
  "exported_at": "2026-07-15T10:30:00.000Z",
  "cards": [
    {
      "id": 1,
      "label": "主卡",
      "phone": "13800138000",
      "operator": "中国移动",
      "color": "#4F7CFF",
      "sort_order": 0,
      "created_at": "2026-07-15T10:00:00",
      "updated_at": "2026-07-15T10:00:00"
    }
  ],
  "app_bindings": [],
  "sms_candidates": []
}

三、写入文件

3.1 保存到应用沙盒

typescript 复制代码
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

/**
 * 将备份 JSON 写入沙盒文件
 */
async function saveBackupToFile(
  context: common.UIAbilityContext,
  backupData: BackupData
): Promise<string> {
  const json = backupToJson(backupData);

  // 生成文件名:haomazhushou_backup_20260715_103000.json
  const timestamp = new Date()
    .toISOString()
    .replace(/[:.]/g, '')
    .replace('T', '_')
    .slice(0, 15);
  const fileName = `haomazhushou_backup_${timestamp}.json`;

  // 获取沙盒缓存目录
  const cacheDir = context.cacheDir;
  const filePath = `${cacheDir}/${fileName}`;

  // 写入文件
  const file = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
  fileIo.writeSync(file.fd, json);
  fileIo.closeSync(file);

  console.info(`Backup saved: ${filePath} (${json.length} bytes)`);
  return filePath;
}

3.2 分享导出文件

使用系统分享能力让用户将备份文件保存到云盘或发送给他人:

typescript 复制代码
import { common, UIAbilityContext } from '@kit.AbilityKit';
import { wantConstant } from '@kit.AbilityKit';

/**
 * 分享备份文件
 */
async function shareBackupFile(context: UIAbilityContext, filePath: string): Promise<void> {
  const uri = fileIo.uri.getUriFromPath(filePath);

  try {
    const want: Want = {
      action: wantConstant.Action.SEND_DATA,
      type: 'application/json',
      uri: [uri],
      parameters: {
        'ability.params.stream': [uri],
      },
    };

    await context.startAbility(want);
    console.info('Share intent launched');
  } catch (error) {
    console.error('Failed to share backup', error);
    throw error;
  }
}

四、完整导出流程

typescript 复制代码
async function performExport(context: UIAbilityContext, store: relationalStore.RdbStore): Promise<void> {
  try {
    // 1. 查询数据
    const service = new BackupService(store);
    const backupData = await service.exportAll();

    // 2. 写入文件
    const filePath = await saveBackupToFile(context, backupData);

    // 3. 提示成功
    AlertDialog.show({
      title: '备份成功',
      message: `已导出 ${backupData.summary?.totalCards} 张卡、${backupData.summary?.totalBindings} 条绑定`,
      primaryButton: {
        value: '分享文件',
        action: () => shareBackupFile(context, filePath),
      },
      secondaryButton: {
        value: '完成',
      },
    });
  } catch (error) {
    console.error('Export failed', error);
    AlertDialog.show({ title: '备份失败', message: error.message });
  }
}

五、增量备份策略

全量备份每次导出所有数据,数据量大时耗时增加。可以考虑增量备份:

typescript 复制代码
/**
 * 增量备份:只导出指定日期之后修改的记录
 */
async exportIncremental(since: Date): Promise<BackupData> {
  const sinceStr = since.toISOString();

  // 只查询 updated_at > since 的记录
  // 需要在 DAO 中新增查询方法
  const cards = await this.cardDao.listUpdatedSince(sinceStr);
  const bindings = await this.bindingDao.listUpdatedSince(sinceStr);
  const candidates = await this.candidateDao.listCreatedSince(sinceStr);

  return {
    version: BACKUP_VERSION,
    app: 'haomazhushou',
    exported_at: new Date().toISOString(),
    cards,
    app_bindings: bindings,
    sms_candidates: candidates,
  };
}

DAO 中的增量查询方法:

typescript 复制代码
// CardDao
async listUpdatedSince(since: string): Promise<CardEntity[]> {
  let predicates = new relationalStore.RdbPredicates('card');
  predicates.greaterThan('updated_at', since);
  return await this.queryList(predicates);
}

六、备份文件验证

导出后需要对备份文件做完整性验证:

typescript 复制代码
function validateBackup(data: unknown): data is BackupData {
  if (!data || typeof data !== 'object') return false;

  const backup = data as Record<string, unknown>;

  // 必填字段检查
  if (typeof backup.version !== 'number') return false;
  if (typeof backup.app !== 'string') return false;
  if (typeof backup.exported_at !== 'string') return false;

  // 数组字段检查
  if (!Array.isArray(backup.cards)) return false;
  if (!Array.isArray(backup.app_bindings)) return false;
  if (!Array.isArray(backup.sms_candidates)) return false;

  return true;
}

七、文件管理

7.1 列出所有备份文件

typescript 复制代码
async function listBackupFiles(context: UIAbilityContext): Promise<string[]> {
  const cacheDir = context.cacheDir;
  const files = fileIo.listFileSync(cacheDir);

  return files
    .filter(name => name.startsWith('haomazhushou_backup_') && name.endsWith('.json'))
    .map(name => `${cacheDir}/${name}`);
}

7.2 删除旧备份

typescript 复制代码
async function cleanOldBackups(context: UIAbilityContext, keepCount: number = 5): Promise<void> {
  const files = await listBackupFiles(context);

  if (files.length <= keepCount) return;

  // 按文件名排序(时间戳按字典序排列)
  files.sort();

  // 删除最旧的
  const toDelete = files.slice(0, files.length - keepCount);
  for (const filePath of toDelete) {
    try {
      fileIo.unlinkSync(filePath);
    } catch (error) {
      console.warn(`Failed to delete old backup: ${filePath}`, error);
    }
  }
}

小结

本篇完成了数据导出与备份的完整实现:

模块 关键点
BackupData 模型 version + app + 三张表数据 + summary
JSON 序列化 JSON.stringify 带格式化输出
文件写入 fileIo.openSync + writeSync
文件分享 startAbility SEND_DATA
增量备份 listUpdatedSince 按时间过滤
备份验证 类型守卫 + 字段完整性检查
文件管理 列出/清理旧备份文件

7.1 备份策略对比

策略 数据量 耗时 存储空间 适用场景
全量备份 全部数据 较长 较大 首次备份/恢复
增量备份 变更数据 日常定期备份

7.2 fileIo 核心方法

方法 功能 参数
openSync(path, mode) 打开/创建文件 文件路径、打开模式
writeSync(fd, data) 写入数据 文件描述符、字符串/ArrayBuffer
readSync(fd, buffer) 读取数据 文件描述符、缓冲区
closeSync(file) 关闭文件 文件描述符
unlinkSync(path) 删除文件 文件路径
statSync(path) 获取文件信息 文件路径
listFileSync(path) 列出目录文件 目录路径

下一篇将实现「数据导入与恢复批量插入策略」,读取备份 JSON 并安全恢复数据。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
2301_768103491 小时前
HarmonyOS趣味相机实战第11篇:从单摄切换升级前后双摄同开、能力探测与降级设计
harmonyos·arkts·camerakit·双摄同开·能力降级
拥抱太阳06162 小时前
HarmonyOS 应用开发《掌上英语》第12篇:组件通信模式:从父子传参到跨层级状态共享
华为·harmonyos
最爱老式锅包肉2 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦
数码相机·华为·harmonyos
前网易架构师-高司机2 小时前
带标注的SDD-SAR遥感船舶识别数据集,识别率92.9%,7000张图,支持yolo,coco json,voc xml,文末有模型训练代码
yolo·json·数据集·遥感·sar·船舶·sdd
2501_919749032 小时前
华为鸿蒙免费口算闯关APP—小羊口算
华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第14篇:@Extend 扩展函数 — 告别重复样式代码
华为·harmonyos
蓝速科技2 小时前
蓝速科技鸿蒙广告机替代安卓实测:信创系统下的性能与边界评估
android·科技·harmonyos
2301_768103493 小时前
HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机
harmonyos·arkts·稳定性·错误处理·camerakit
最爱老式锅包肉5 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第8篇:曝光控制深入解析
数码相机·华为·harmonyos
Helen_cai6 小时前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos