鸿蒙实战:UserDao 用户本地管理与三路登录体系
鸿蒙实战:UserDao 用户本地管理与三路登录体系
前言

图:鸿蒙实战第88篇:UserDao 用户本地管理与三路登录体系 运行效果截图(HarmonyOS NEXT)
用户系统是几乎所有 App 的基础设施。本篇聚焦 UserDao——"鹿鹿"最轻量的数据访问层组件,仅 83 行代码,却支撑起三路登录体系(华为 HMS 一键登录 / 手机验证码 / 纯本地模式)的完整底层持久化。通过拆解这个 83 行精简 DAO,你将掌握:
relationalStoreRDB 的 CRUD 完整套路(增/查/改三操作)- 幂等性设计(
ensureXxx命名约定的含义与实践) - AppStorage + RDB 双重用户态(内存态 + 持久化态的分工)
%{public}shilog 日志标记的安全含义
系列文章导航:00 开篇与项目总览 · 87 DemoSeeder 数据播种器 · 89 LoginPage 华为一键登录
官方文档参考:relationalStore API · hilog API
一、用户系统整体架构
1.1 三路登录 × 双层状态
"鹿鹿"的用户体系采用 “三路登录入口 + 双层状态管理” 架构:
| 登录方式 | open_id 值 |
存储来源 | 线上/调试 |
|---|---|---|---|
| 华为 HMS | HMS 返回的 openId 字符串 | AccountAuthService.signIn() |
预留注释 |
| 手机号 | phone_ + 手机号后 4 位 |
SMS 云端验证 | 预留注释 |
| 本地/游客 | 'local' 字面量 |
无外部依赖 | ✅ 当前可用 |
1.2 AppStorage vs RDB 分工
内存态(AppStorage)
└── user_id ← 快速读取,无需查 DB
└── user_token ← 登录态判断
└── user_name ← 首页展示
持久化(RDB user 表)
└── open_id ← 唯一标识
└── name ← 可修改的昵称
└── avatar ← 头像 URI
└── created_at ← 首次登录时间
设计原则:AppStorage 只存"当次会话"所需的最小状态集,RDB 存全量用户数据。App 重启时通过
findByOpenId重建 AppStorage。
二、数据表结构
2.1 user 表定义
user 表在 DatabaseService.ets 中定义,结构如下:
const CREATE_USER_TABLE = `
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
open_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL DEFAULT '鹿鹿用户',
avatar TEXT,
created_at INTEGER NOT NULL
)
`
open_id带UNIQUE约束,防止重复插入时产生脏数据avatar允许 NULL,首次注册时可为空created_at存 Unix 毫秒时间戳(Date.now())
2.2 对应实体类
// features/data/Models.ets
export interface UserEntity {
id: number
open_id: string
name: string
avatar?: string
created_at: number
}
avatar 用 ? 标记为可选字段,与 SQL 中的 NULL 语义对应。
三、UserDao 完整实现
3.1 整体结构(83 行精简 DAO)
// entry/src/main/ets/features/data/UserDao.ets
import relationalStore from '@ohos.data.relationalStore'
import { DatabaseService } from './DatabaseService'
import { UserEntity } from './Models'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG = 'UserDao'
const TABLE = 'user'
export class UserDao {
private static getStore(): relationalStore.RdbStore {
return DatabaseService.getInstance().getStore()
}
static async ensureLocalUser(name: string): Promise<UserEntity>
static async findByOpenId(openId: string): Promise<UserEntity | null>
static async updateName(id: number, name: string): Promise<void>
}
三个方法的职责划分:
ensureLocalUser— 创建或获取(幂等)findByOpenId— 按 openId 查询(支持三路登录的统一查询口)updateName— 更新昵称(个人中心设置页调用)
3.2 ensureLocalUser:幂等创建
static async ensureLocalUser(name: string): Promise<UserEntity> {
try {
const store = UserDao.getStore()
// ① 先查询是否已存在
const existing = await UserDao.findByOpenId('local')
if (existing) {
return existing // 幂等:已存在则直接返回
}
// ② 不存在则插入
const now = Date.now()
const values: relationalStore.ValuesBucket = {
open_id: 'local',
name,
created_at: now
}
const id = await store.insert(TABLE, values)
hilog.info(0x0000, TAG, '本地用户创建成功 id=%{public}d', id)
return { id, open_id: 'local', name, created_at: now }
} catch (e) {
hilog.error(0x0000, TAG, 'ensureLocalUser 失败: %{public}s', JSON.stringify(e))
throw new Error('ensureLocalUser 失败: ' + JSON.stringify(e))
}
}
设计亮点:
- 先查后插避免了
open_id UNIQUE约束导致的插入异常 'local'固定字符串是本地模式的约定标识- 函数命名哲学:
ensure前缀代表"保证存在",调用者无需判断是创建还是获取
3.3 findByOpenId:统一查询入口
static async findByOpenId(openId: string): Promise<UserEntity | null> {
try {
const store = UserDao.getStore()
const predicates = new relationalStore.RdbPredicates(TABLE)
predicates.equalTo('open_id', openId)
const rs = await store.query(
predicates,
['id', 'open_id', 'name', 'avatar', 'created_at']
)
let result: UserEntity | null = null
if (rs.goToFirstRow()) {
result = {
id: rs.getLong(rs.getColumnIndex('id')),
open_id: rs.getString(rs.getColumnIndex('open_id')),
name: rs.getString(rs.getColumnIndex('name')),
avatar: rs.getString(rs.getColumnIndex('avatar')),
created_at: rs.getLong(rs.getColumnIndex('created_at'))
}
}
rs.close() // ⚠️ 必须关闭,否则连接泄漏
return result
} catch (e) {
hilog.error(0x0000, TAG, 'findByOpenId 失败: %{public}s', JSON.stringify(e))
return null
}
}
关键模式:
store.query(predicates, columns)— 第二参数指定列名,避免SELECT *rs.goToFirstRow()— 返回false则无记录,无需 while 循环(单条记录查询)rs.close()— 非常重要,RDB ResultSet 持有底层数据库连接,不关闭会导致连接池耗尽
3.4 updateName:条件更新
static async updateName(id: number, name: string): Promise<void> {
try {
const store = UserDao.getStore()
const predicates = new relationalStore.RdbPredicates(TABLE)
predicates.equalTo('id', id)
const affected = await store.update(
{ name } as relationalStore.ValuesBucket,
predicates
)
hilog.info(0x0000, TAG, 'updateName 影响行数: %{public}d', affected)
} catch (e) {
hilog.error(0x0000, TAG, 'updateName 失败: %{public}s', JSON.stringify(e))
}
}
{ name } as relationalStore.ValuesBucket 是 ArkTS 类型断言——字面量对象 { name } 的推断类型是 { name: string },不完全匹配 ValuesBucket(索引签名),需要手动断言。
四、与登录页的联动
4.1 完整初始化链
登录成功后的初始化调用顺序严格有序:
// LoginPage / PhoneLoginPage 登录成功回调
async function afterLoginSuccess(openId: string, userName: string) {
// ① 初始化 RDB
await DatabaseService.getInstance().init(
getContext(this) as common.UIAbilityContext
)
// ② 确保用户记录存在
const user = await UserDao.ensureLocalUser(userName)
// ③ 播种演示数据(仅首次)
await DemoSeeder.seedIfNeeded(user.id)
// ④ 写入 AppStorage(内存态)
AppStorage.setOrCreate<number>('user_id', user.id)
AppStorage.setOrCreate<string>('user_token', 'LU_MOCK_' + Date.now())
AppStorage.setOrCreate<string>('user_name', userName)
// ⑤ 跳转首页
this.getUIContext().getRouter().replaceUrl({ url: 'pages/HomePage' })
}
顺序不可颠倒:DatabaseService.init() 必须在所有 DAO 调用之前,否则 getStore() 返回 undefined。
4.2 登录态预检
每次进入 LoginPage 时会先检查是否已有 Token:
aboutToAppear(): void {
const token = AppStorage.get<string>('user_token')
if (token) {
// 已登录:跳过登录页直接进首页
this.getUIContext().getRouter().replaceUrl({ url: 'pages/HomePage' })
return
}
// 未登录:开始入场动画
this.startEntranceAnimation()
}
注意:此处 Token 是 AppStorage 的内存值。App 重启后 AppStorage 清空,即使 RDB 中有用户记录,也需要重新登录写入 Token。这是有意的设计:保证每次冷启动都经过登录验证。
五、hilog 安全日志标记
5.1 %{public}s vs %{private}s
项目所有 hilog 调用都使用 %{public}s 格式化符:
hilog.info(0x0000, TAG, '用户查询: %{public}s', openId)
hilog.error(0x0000, TAG, '错误详情: %{public}s', JSON.stringify(e))
| 标记 | 含义 | 适用场景 |
|---|---|---|
%{public}s |
日志内容可见于日志抓取工具 | 非隐私字段:错误消息、操作类型 |
%{private}s |
日志内容隐藏(显示为 <private>) |
隐私字段:手机号、身份证、密码 |
%s(无标记) |
默认行为,Release 构建隐藏 | 不推荐,行为依赖构建类型 |
最佳实践:涉及用户
name、open_id等身份信息时应使用%{private}s;调试时可临时改为%{public}s,发布前务必还原。
六、DAO 设计模式对比
"鹿鹿"共有 5 个 DAO,所有 DAO 遵循相同的四步模式:
| DAO | 表名 | 特殊方法 | 行数 |
|---|---|---|---|
| UserDao | user | ensureLocalUser(幂等创建) |
83 |
| ArchiveDao | archive | incrementRecord(计数更新) |
~180 |
| HandwritingDao | handwriting | batchUpdateArchive(批量迁移) |
~200 |
| ReportDao | report | queryRecentByUser(聚合查询) |
~220 |
| RelationDao | relation | findByUser(关联查询) |
~150 |
四步统一模式(所有 DAO 方法均遵循):
try {
const store = XxxDao.getStore() // ① 获取 RDB 实例
const rs = await store.query(...) // ② 执行操作
// rs.goToFirstRow() + getXxx() // ③ 遍历结果
rs.close() // ④ 关闭 ResultSet
} catch (e) {
hilog.error(TAG, '...', JSON.stringify(e))
}
七、常见问题排查
7.1 getStore() 返回 undefined
现象:调用 DAO 方法时报 Cannot read property 'insert' of undefined
原因:DatabaseService.init() 尚未调用或 await 未完成就调用了 DAO
解决:确保在 EntryAbility.onCreate() 或登录成功后第一步调用 DatabaseService.getInstance().init(context)
7.2 用户记录重复插入
现象:数据库中出现多条 open_id = 'local' 的记录
原因:并发调用 ensureLocalUser,两次都通过了 findByOpenId 的判断
解决:已由 open_id UNIQUE 约束兜底,第二次 insert 会抛异常被 catch 捕获;如需更严格的幂等,可加分布式锁或在应用入口串行化调用
7.3 ResultSet 未关闭导致内存泄漏
现象:长时间使用后 App 报"数据库连接数达上限"错误
原因:rs.close() 忘记调用或在异常路径未执行
解决:使用 try-finally 确保 rs.close() 始终执行:
const rs = await store.query(predicates, columns)
try {
if (rs.goToFirstRow()) {
// ... 读取数据
}
} finally {
rs.close() // finally 保证执行
}
八、注意事项与常见问题
8.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期:合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
总结
本篇拆解了 UserDao 的 83 行代码,核心收获:
ensureLocalUser幂等设计:先查后插 +UNIQUE约束双重保障- 三路登录统一底层:
open_id字段屏蔽登录方式差异,DAO 层完全无感 - AppStorage + RDB 职责分离:内存态(快速判断)+ 持久化态(数据恢复)
rs.close()不可省略:RDB ResultSet 持有连接资源,必须手动释放- hilog 安全标记:
%{public}svs%{private}s是生产日志的隐私保护机制
下一篇预告:第89篇 LoginPage——华为一键登录与 Canvas 手绘插画
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- relationalStore API 文档
- hilog API 文档
- HMS Account Kit 集成指南
- ArkTS 装饰器参考
- Stage 模型 AbilityContext
- AppStorage 文档
- 鸿蒙开发者文档首页
- 系列文章:第87篇 DemoSeeder 数据播种器
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
UserDao 设计精髓:83 行代码实现三路登录的底层持久化,核心在于「幂等性设计」(ensureUser)和「双重用户态」(内存 AppStorage + 持久化 RDB)的分工协作。
更多推荐




所有评论(0)