鸿蒙实战:UserDao 用户本地管理与三路登录体系
2026/7/12 5:49:05 网站建设 项目流程

鸿蒙实战: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 三路登录 × 双层状态

"鹿鹿"的用户体系采用“三路登录入口 + 双层状态管理”架构:

华为一键

手机号

游客模式

用户进入 App

已有 Token?

跳转 HomePage

LoginPage

选择登录方式

HMS Account Kit
生产: AccountAuthService
调试: Mock Token

PhoneLoginPage
验证码 + SMS

本地模式
open_id='local'

UserDao.ensureLocalUser

DemoSeeder.seedIfNeeded
播种演示数据

AppStorage.setOrCreate
user_id / user_token / user_name

登录方式open_id存储来源线上/调试
华为 HMSHMS 返回的 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中定义,结构如下:

constCREATE_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_idUNIQUE约束,防止重复插入时产生脏数据
  • avatar允许 NULL,首次注册时可为空
  • created_at存 Unix 毫秒时间戳(Date.now()

2.2 对应实体类

// features/data/Models.etsexportinterfaceUserEntity{id:numberopen_id:stringname:stringavatar?:stringcreated_at:number}

avatar?标记为可选字段,与 SQL 中的NULL语义对应。


三、UserDao 完整实现

3.1 整体结构(83 行精简 DAO)

// entry/src/main/ets/features/data/UserDao.etsimportrelationalStorefrom'@ohos.data.relationalStore'import{DatabaseService}from'./DatabaseService'import{UserEntity}from'./Models'import{hilog}from'@kit.PerformanceAnalysisKit'constTAG='UserDao'constTABLE='user'exportclassUserDao{privatestaticgetStore():relationalStore.RdbStore{returnDatabaseService.getInstance().getStore()}staticasyncensureLocalUser(name:string):Promise<UserEntity>staticasyncfindByOpenId(openId:string):Promise<UserEntity|null>staticasyncupdateName(id:number,name:string):Promise<void>}

三个方法的职责划分:

  1. ensureLocalUser— 创建或获取(幂等)
  2. findByOpenId— 按 openId 查询(支持三路登录的统一查询口)
  3. updateName— 更新昵称(个人中心设置页调用)

3.2 ensureLocalUser:幂等创建

staticasyncensureLocalUser(name:string):Promise<UserEntity>{try{conststore=UserDao.getStore()// ① 先查询是否已存在constexisting=awaitUserDao.findByOpenId('local')if(existing){returnexisting// 幂等:已存在则直接返回}// ② 不存在则插入constnow=Date.now()constvalues:relationalStore.ValuesBucket={open_id:'local',name,created_at:now}constid=awaitstore.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))thrownewError('ensureLocalUser 失败: '+JSON.stringify(e))}}

设计亮点

  • 先查后插避免了open_id UNIQUE约束导致的插入异常
  • 'local'固定字符串是本地模式的约定标识
  • 函数命名哲学ensure前缀代表"保证存在",调用者无需判断是创建还是获取

3.3 findByOpenId:统一查询入口

staticasyncfindByOpenId(openId:string):Promise<UserEntity|null>{try{conststore=UserDao.getStore()constpredicates=newrelationalStore.RdbPredicates(TABLE)predicates.equalTo('open_id',openId)constrs=awaitstore.query(predicates,['id','open_id','name','avatar','created_at'])letresult:UserEntity|null=nullif(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()// ⚠️ 必须关闭,否则连接泄漏returnresult}catch(e){hilog.error(0x0000,TAG,'findByOpenId 失败: %{public}s',JSON.stringify(e))returnnull}}

关键模式

  • store.query(predicates, columns)— 第二参数指定列名,避免SELECT *
  • rs.goToFirstRow()— 返回false则无记录,无需 while 循环(单条记录查询)
  • rs.close()非常重要,RDB ResultSet 持有底层数据库连接,不关闭会导致连接池耗尽

3.4 updateName:条件更新

staticasyncupdateName(id:number,name:string):Promise<void>{try{conststore=UserDao.getStore()constpredicates=newrelationalStore.RdbPredicates(TABLE)predicates.equalTo('id',id)constaffected=awaitstore.update({name}asrelationalStore.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.ValuesBucketArkTS 类型断言——字面量对象{ name }的推断类型是{ name: string },不完全匹配ValuesBucket(索引签名),需要手动断言。


四、与登录页的联动

4.1 完整初始化链

登录成功后的初始化调用顺序严格有序

// LoginPage / PhoneLoginPage 登录成功回调asyncfunctionafterLoginSuccess(openId:string,userName:string){// ① 初始化 RDBawaitDatabaseService.getInstance().init(getContext(this)ascommon.UIAbilityContext)// ② 确保用户记录存在constuser=awaitUserDao.ensureLocalUser(userName)// ③ 播种演示数据(仅首次)awaitDemoSeeder.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{consttoken=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}svs%{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 构建隐藏不推荐,行为依赖构建类型

最佳实践:涉及用户nameopen_id等身份信息时应使用%{private}s;调试时可临时改为%{public}s,发布前务必还原。


六、DAO 设计模式对比

"鹿鹿"共有 5 个 DAO,所有 DAO 遵循相同的四步模式:

DAO表名特殊方法行数
UserDaouserensureLocalUser(幂等创建)83
ArchiveDaoarchiveincrementRecord(计数更新)~180
HandwritingDaohandwritingbatchUpdateArchive(批量迁移)~200
ReportDaoreportqueryRecentByUser(聚合查询)~220
RelationDaorelationfindByUser(关联查询)~150

四步统一模式(所有 DAO 方法均遵循):

try{conststore=XxxDao.getStore()// ① 获取 RDB 实例constrs=awaitstore.query(...)// ② 执行操作// rs.goToFirstRow() + getXxx() // ③ 遍历结果rs.close()// ④ 关闭 ResultSet}catch(e){hilog.error(TAG,'...',JSON.stringify(e))}

七、常见问题排查

7.1getStore()返回 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()始终执行:

constrs=awaitstore.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)的分工协作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询