鸿蒙实战:报告与雷达图 ReportDao

鸿蒙实战:报告与雷达图 ReportDao

前言(必读)

图:鸿蒙实战:报告与雷达图 ReportDao(第 86 篇) 运行效果截图(HarmonyOS NEXT)

report 表是 AI 分析结果的主要载体------它将端侧推理产出的人格类型、雷达数据、情绪标签和 AI 摘要持久化到本地。本文详解 ReportDao 的 8 个方法以及 JSON 字段的序列化/反序列化策略,并附带 RelationDao 伴侣关系的实现。

图:ReportDao + RelationDao 数据结构------JSON 字段序列化与雷达分数存储策略
#mermaid-svg-NJ5IBBFAjaYOjQYB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NJ5IBBFAjaYOjQYB .error-icon{fill:#552222;}#mermaid-svg-NJ5IBBFAjaYOjQYB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NJ5IBBFAjaYOjQYB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .marker.cross{stroke:#333333;}#mermaid-svg-NJ5IBBFAjaYOjQYB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NJ5IBBFAjaYOjQYB p{margin:0;}#mermaid-svg-NJ5IBBFAjaYOjQYB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster-label text{fill:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster-label span{color:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster-label span p{background-color:transparent;}#mermaid-svg-NJ5IBBFAjaYOjQYB .label text,#mermaid-svg-NJ5IBBFAjaYOjQYB span{fill:#333;color:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .node rect,#mermaid-svg-NJ5IBBFAjaYOjQYB .node circle,#mermaid-svg-NJ5IBBFAjaYOjQYB .node ellipse,#mermaid-svg-NJ5IBBFAjaYOjQYB .node polygon,#mermaid-svg-NJ5IBBFAjaYOjQYB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .rough-node .label text,#mermaid-svg-NJ5IBBFAjaYOjQYB .node .label text,#mermaid-svg-NJ5IBBFAjaYOjQYB .image-shape .label,#mermaid-svg-NJ5IBBFAjaYOjQYB .icon-shape .label{text-anchor:middle;}#mermaid-svg-NJ5IBBFAjaYOjQYB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .rough-node .label,#mermaid-svg-NJ5IBBFAjaYOjQYB .node .label,#mermaid-svg-NJ5IBBFAjaYOjQYB .image-shape .label,#mermaid-svg-NJ5IBBFAjaYOjQYB .icon-shape .label{text-align:center;}#mermaid-svg-NJ5IBBFAjaYOjQYB .node.clickable{cursor:pointer;}#mermaid-svg-NJ5IBBFAjaYOjQYB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .arrowheadPath{fill:#333333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NJ5IBBFAjaYOjQYB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NJ5IBBFAjaYOjQYB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NJ5IBBFAjaYOjQYB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster text{fill:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB .cluster span{color:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NJ5IBBFAjaYOjQYB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NJ5IBBFAjaYOjQYB rect.text{fill:none;stroke-width:0;}#mermaid-svg-NJ5IBBFAjaYOjQYB .icon-shape,#mermaid-svg-NJ5IBBFAjaYOjQYB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NJ5IBBFAjaYOjQYB .icon-shape p,#mermaid-svg-NJ5IBBFAjaYOjQYB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NJ5IBBFAjaYOjQYB .icon-shape .label rect,#mermaid-svg-NJ5IBBFAjaYOjQYB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NJ5IBBFAjaYOjQYB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NJ5IBBFAjaYOjQYB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NJ5IBBFAjaYOjQYB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1:1
reports 表
personality_type TEXT
emotion_tags TEXT

JSON 数组
ai_summary TEXT
radar_data TEXT

JSON 对象
created_at INTEGER
radar_scores 表
emotion_stability REAL
self_awareness REAL
social_tendency REAL
creativity REAL
responsibility REAL
empathy REAL


一、ReportEntity 模型

typescript 复制代码
export interface ReportEntity {
  id?: number
  handwriting_id: number        // FK → handwriting.id(1:1 关系)
  archive_id?: number           // 冗余 FK,方便按档案查询
  personality_type?: string     // 人格类型文字
  summary?: string              // AI 摘要
  radar_json?: string           // JSON: 6 维雷达分数
  tags_json?: string            // JSON: 情绪标签数组
  created_at: number
}

JSON 字段说明

字段 存储格式 示例 序列化工具
radar_json RadarScores 对象的 JSON 字符串 {"energy":62,"thinking":68,...} JSON.stringify / JSON.parse
tags_json string[] 数组的 JSON 字符串 ["💛 平静偏暖","能量 62"] JSON.stringify / JSON.parse

二、ReportDao 方法全景

方法 用途 调用方
create(item) 写入分析报告 AnalyzingPage
findById(id) 按报告 ID 加载 ReportDetailPage
findByHandwriting(handwritingId) 按笔迹 ID 查报告 ReportDetailPage
listByArchive(archiveId) 档案下全部报告 TrendPage / ArchivePage
listRecent(limit) 全局最近 N 条 HomePage(最近分析)
findLatestByArchive(archiveId) 档案最新 1 条 ArchivePage 卡片
parseRadar(report) 反序列化雷达 JSON 各页面工具方法
parseTags(report) 反序列化标签 JSON 各页面工具方法
countAll() 全局计数 MePage

三、核心查询实现

3.1 findByHandwriting

typescript 复制代码
// 由 handwriting_id 查找对应的报告
static async findByHandwriting(handwritingId: number): Promise<ReportEntity | null> {
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('handwriting_id', handwritingId)
  predicates.orderByDesc('created_at')
  const rs = await store.query(predicates, COLS)
  let result: ReportEntity | null = null
  if (rs.goToFirstRow()) {
    result = ReportDao.parseRow(rs)
  }
  rs.close()
  return result
}

3.2 findLatestByArchive(每档案最新一条)

typescript 复制代码
// ArchivePage 每个档案卡片显示最新人格类型
static async findLatestByArchive(archiveId: number): Promise<ReportEntity | null> {
  const rs = await store.querySql(
    `SELECT * FROM ${TABLE} WHERE archive_id = ?
     ORDER BY created_at DESC LIMIT 1`,
    [archiveId]
  )
  let result: ReportEntity | null = null
  if (rs.goToFirstRow()) {
    result = ReportDao.parseRow(rs)
  }
  rs.close()
  return result
}

为什么用 querySql 而非 predicates? 因为需要 LIMIT 1RdbPredicateslimit() 在某些版本中不如原生 SQL 直观。

3.3 listByArchive(档案时间线)

typescript 复制代码
// TrendPage 使用 --- 展示某个档案下的情绪变化趋势
static async listByArchive(archiveId: number): Promise<ReportEntity[]> {
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('archive_id', archiveId)
  predicates.orderByDesc('created_at')
  const rs = await store.query(predicates, COLS)
  const result: ReportEntity[] = []
  while (rs.goToNextRow()) {
    result.push(ReportDao.parseRow(rs))
  }
  rs.close()
  return result
}

四、JSON 字段序列化

4.1 写入

typescript 复制代码
// AnalyzingPage 中写入时
await ReportDao.create({
  handwriting_id: hwId,
  archive_id: archiveId,
  personality_type: '温和理性型',
  summary: 'TA 是一个细腻、克制的人...',
  radar_json: JSON.stringify({             // 序列化为 JSON 字符串
    energy: 62, thinking: 68,
    action: 55, emotion: 71,
    social: 60, detail: 75
  }),
  tags_json: JSON.stringify([              // 序列化为 JSON 字符串
    '💛 平静偏暖', '能量 62', '细腻克制'
  ]),
  created_at: Date.now()
})

4.2 读取

typescript 复制代码
// 反序列化雷达分数
static parseRadar(report: ReportEntity): RadarScores {
  if (!report.radar_json) {
    return { energy: 0, thinking: 0, action: 0, emotion: 0,
             social: 0, detail: 0 }
  }
  try {
    return JSON.parse(report.radar_json) as RadarScores
  } catch (e) {
    return { energy: 0, thinking: 0, action: 0, emotion: 0,
             social: 0, detail: 0 }
  }
}

// 反序列化标签
static parseTags(report: ReportEntity): string[] {
  if (!report.tags_json) return []
  try {
    const t = JSON.parse(report.tags_json) as string[]
    return Array.isArray(t) ? t : []
  } catch (e) {
    return []
  }
}

反序列化的安全策略:

异常情况 处理方式
radar_json 为空字符串 返回全 0 的 RadarScores
JSON.parse 抛出异常 catch 后返回默认值
parseTags 结果不是数组 Array.isArray 校验

五、RelationDao 伴侣关系

5.1 模型

typescript 复制代码
export interface RelationEntity {
  id?: number
  user_id: number               // 当前用户
  partner_archive_id: number    // 伴侣的 archive_id
  bound_method?: string         // 'tap' | 'qrcode' | 'phone'
  match_score?: number          // 合盘匹配度
  created_at: number
}

5.2 方法

方法 用途 调用方
create(item) 创建关系绑定 BindRelationPage
findByUser(userId) 查询当前用户的关系 几乎所有页面
listByUser(userId) 列出所有关系 ---
updateMatchScore(id, score) 更新匹配度 DuetReportPage
delete(id) 解绑 MePage

5.3 findByUser 实现

typescript 复制代码
// 查询当前用户最新的关系绑定
static async findByUser(userId: number): Promise<RelationEntity | null> {
  const predicates = new relationalStore.RdbPredicates(TABLE)
  predicates.equalTo('user_id', userId)
  predicates.orderByDesc('created_at')
  const rs = await store.query(predicates, COLS)
  let result: RelationEntity | null = null
  if (rs.goToFirstRow()) {
    result = RelationDao.parseRow(rs)
  }
  rs.close()
  return result
}

这个方法是全项目中调用频次最高的 DAO 查询------HomePage、ArchivePage、MePage、CoupleHomePage、DuetReportPage 都在用。


六、数据完整性

6.1 1:1 约束

report 与 handwriting 是 1:1 关系(一条笔迹只能有 1 份报告),但 SQLite 中通过 handwriting_id 外键实现,没有创建 UNIQUE 约束:

typescript 复制代码
// 如果某条笔迹已存在报告,不要重复创建
const existing = await ReportDao.findByHandwriting(handwritingId)
if (existing) {
  hilog.warn(0x0000, TAG, '该笔迹已存在报告,跳过')
  return existing.id!
}

6.2 级联清理

ArchiveDao.delete 中会清理 report 和 handwriting 的 archive_id 引用,但不会删除 report 行本身

复制代码
删除档案 → report.archive_id = 0(数据保留)
删除笔迹 → 目前没有级联删除 report

七、完整数据写入流程

复制代码
AnalyzingPage 完成推理
    ↓
HandwritingDao.create({
  archive_id, source, ocr_text,
  feature_json, energy_score
})
    ↓
ArchiveDao.incrementRecord(
  archiveId, now
)
    ↓
ReportDao.create({
  handwriting_id, archive_id,
  personality_type, summary,
  radar_json, tags_json
})
    ↓
AppStorage.setOrCreate(
  'analysis_result', result
)
    ↓
router.pushUrl('pages/ReportDetailPage')
    ↓
ReportDetailPage 从 AppStorage 读取 result

八、注意事项与常见问题

8.1 开发注意事项

在正式开发前,建议按以下步骤完成环境准备与前置检查:

  1. 版本确认:检查 DevEco Studio 与 SDK 版本,确保满足目标 API Level 要求
  2. 权限声明 :在 module.json5requestPermissions 字段中提前声明所有需要的系统权限
  3. 设备能力检查:调用前验证设备是否支持目标能力(相机、NFC、传感器等)
  4. 异步封装 :所有耗时操作(数据库、文件 I/O、网络请求)统一使用 async/await 处理
  5. 资源释放 :在组件 aboutToDisappear() 生命周期钩子中及时释放系统资源,防止内存泄漏

8.2 常见错误与解决方案

常见问题快速排查表:

问题类型 排查方向 参考方法
应用崩溃 查看 hilog 错误日志 hilog.error(TAG, "...", e.message)
状态丢失 检查 AppStorage 键名拼写 统一使用常量管理键名
动画不流畅 避免在 animateTo 回调中执行 I/O 动画与数据操作分离

总结

  • report 表:8 字段,handwriting_id 为 FK,1:1 关系
  • 8 个 DAO 方法:覆盖创建/查询/列表/统计全部场景
  • JSON 序列化:radar_json / tags_json 使用 JSON.stringify/parse
  • 安全反序列化:空值/异常/类型校验三重保护
  • RelationDao:5 个方法管理伴侣绑定关系
  • 完整写入流程:handwriting → archive count → report → AppStorage

下一篇将讲解DemoSeeder 模拟数据初始化------数据库模块的最后一篇。


📌 收藏提示:JSON 字段是 SQLite 灵活性的体现------在关系型数据库中灵活存储非结构化数据,适合雷达图和标签这类长度可变的数据。

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


相关资源:


七、ReportDao 核心方法

7.1 JSON 字段的序列化策略

报告中的情绪标签和雷达数据使用 JSON 字符串存储:

typescript 复制代码
// 写入:对象 → JSON 字符串
const valuesBucket = {
  personality_type: report.personalityType,
  emotion_tags: JSON.stringify(report.emotionTags),  // string[] → JSON
  radar_data: JSON.stringify(report.radarData),       // RadarData → JSON
  ai_summary: report.aiSummary,
  archive_id: report.archiveId,
  created_at: Date.now()
};

// 读取:JSON 字符串 → 对象
const emotionTags = JSON.parse(
  resultSet.getString(resultSet.getColumnIndex('emotion_tags'))
) as string[];

7.2 查询最近 N 条报告

typescript 复制代码
async queryRecent(limit: number = 5): Promise<ReportInfo[]> {
  const store = await DatabaseService.getInstance().getStore();
  const sql = `
    SELECT r.*, a.name AS archive_name
    FROM reports r
    JOIN archives a ON r.archive_id = a.id
    ORDER BY r.created_at DESC
    LIMIT ?
  `;
  const resultSet = await store.querySql(sql, [limit]);
  const reports: ReportInfo[] = [];
  while (resultSet.goToNextRow()) {
    reports.push(this.fromResultSet(resultSet));
  }
  resultSet.close();
  return reports;
}

7.3 RelationDao 伴侣关系存储

typescript 复制代码
class RelationDao {
  async save(userId: number, partnerCode: string): Promise<void> {
    const store = await DatabaseService.getInstance().getStore();
    const pred = new relationalStore.RdbPredicates('relations');
    pred.equalTo('user_id', userId);
    await store.delete(pred);  // 先删除旧的(一个用户只能有一个伴侣)
    await store.insert('relations', {
      user_id: userId,
      partner_code: partnerCode,
      created_at: Date.now()
    });
  }

  async findByUser(userId: number): Promise<RelationInfo | null> {
    const store = await DatabaseService.getInstance().getStore();
    const pred = new relationalStore.RdbPredicates('relations');
    pred.equalTo('user_id', userId);
    const result = await store.query(pred);
    if (!result.goToFirstRow()) { result.close(); return null; }
    const info = this.fromResultSet(result);
    result.close();
    return info;
  }
}

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

相关推荐
anling_li1 小时前
《图片华容道》一、背景设置使用指南
ui·华为·harmonyos
花开彼岸天~1 小时前
鸿蒙实战:字体令牌系统——AppFonts 与排版规范
华为·harmonyos·鸿蒙系统
xd1855785551 小时前
药箱顾问-药品说明书解读的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙
在书中成长1 小时前
HarmonyOS 小游戏《对战五子棋》开发第34篇 - ArkTS中复用UI的方法
ui·harmonyos
木木子221 小时前
# [特殊字符] 健身模拟器 — 鸿蒙ArkTS完整技术解析
华为·harmonyos
爱笑鱼2 小时前
Handler(二):MessageQueue 为什么不是普通队列?
android
爸爸6192 小时前
鸿蒙实战:@State 本地状态与 UI 刷新
ui·华为·harmonyos·鸿蒙系统
阿巴斯甜2 小时前
Android Studio 新版 Logcat
android
念雨思2 小时前
每日穿搭助手:鸿蒙AI应用开发实战——AI衣橱,每日穿搭不再愁
人工智能·华为·powerpoint·harmonyos·鸿蒙