
鸿蒙实战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?跳转 HomePageLoginPage选择登录方式HMS Account Kit生产: AccountAuthService调试: Mock TokenPhoneLoginPage验证码 SMS本地模式open_idlocalUserDao.ensureLocalUserDemoSeeder.seedIfNeeded播种演示数据AppStorage.setOrCreateuser_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_TABLECREATE 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.etsexportinterfaceUserEntity{id:numberopen_id:stringname:stringavatar?:stringcreated_at:number}avatar用?标记为可选字段与 SQL 中的NULL语义对应。三、UserDao 完整实现3.1 整体结构83 行精简 DAO// entry/src/main/ets/features/data/UserDao.etsimportrelationalStorefromohos.data.relationalStoreimport{DatabaseService}from./DatabaseServiceimport{UserEntity}from./Modelsimport{hilog}fromkit.PerformanceAnalysisKitconstTAGUserDaoconstTABLEuserexportclassUserDao{privatestaticgetStore():relationalStore.RdbStore{returnDatabaseService.getInstance().getStore()}staticasyncensureLocalUser(name:string):PromiseUserEntitystaticasyncfindByOpenId(openId:string):PromiseUserEntity|nullstaticasyncupdateName(id:number,name:string):Promisevoid}三个方法的职责划分ensureLocalUser— 创建或获取幂等findByOpenId— 按 openId 查询支持三路登录的统一查询口updateName— 更新昵称个人中心设置页调用3.2 ensureLocalUser幂等创建staticasyncensureLocalUser(name:string):PromiseUserEntity{try{conststoreUserDao.getStore()// ① 先查询是否已存在constexistingawaitUserDao.findByOpenId(local)if(existing){returnexisting// 幂等已存在则直接返回}// ② 不存在则插入constnowDate.now()constvalues:relationalStore.ValuesBucket{open_id:local,name,created_at:now}constidawaitstore.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):PromiseUserEntity|null{try{conststoreUserDao.getStore()constpredicatesnewrelationalStore.RdbPredicates(TABLE)predicates.equalTo(open_id,openId)constrsawaitstore.query(predicates,[id,open_id,name,avatar,created_at])letresult:UserEntity|nullnullif(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):Promisevoid{try{conststoreUserDao.getStore()constpredicatesnewrelationalStore.RdbPredicates(TABLE)predicates.equalTo(id,id)constaffectedawaitstore.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.ValuesBucket是ArkTS 类型断言——字面量对象{ name }的推断类型是{ name: string }不完全匹配ValuesBucket索引签名需要手动断言。四、与登录页的联动4.1 完整初始化链登录成功后的初始化调用顺序严格有序// LoginPage / PhoneLoginPage 登录成功回调asyncfunctionafterLoginSuccess(openId:string,userName:string){// ① 初始化 RDBawaitDatabaseService.getInstance().init(getContext(this)ascommon.UIAbilityContext)// ② 确保用户记录存在constuserawaitUserDao.ensureLocalUser(userName)// ③ 播种演示数据仅首次awaitDemoSeeder.seedIfNeeded(user.id)// ④ 写入 AppStorage内存态AppStorage.setOrCreatenumber(user_id,user.id)AppStorage.setOrCreatestring(user_token,LU_MOCK_Date.now())AppStorage.setOrCreatestring(user_name,userName)// ⑤ 跳转首页this.getUIContext().getRouter().replaceUrl({url:pages/HomePage})}顺序不可颠倒DatabaseService.init()必须在所有 DAO 调用之前否则getStore()返回 undefined。4.2 登录态预检每次进入LoginPage时会先检查是否已有 TokenaboutToAppear():void{consttokenAppStorage.getstring(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 构建隐藏不推荐行为依赖构建类型最佳实践涉及用户name、open_id等身份信息时应使用%{private}s调试时可临时改为%{public}s发布前务必还原。六、DAO 设计模式对比鹿鹿共有 5 个 DAO所有 DAO 遵循相同的四步模式DAO表名特殊方法行数UserDaouserensureLocalUser幂等创建83ArchiveDaoarchiveincrementRecord计数更新~180HandwritingDaohandwritingbatchUpdateArchive批量迁移~200ReportDaoreportqueryRecentByUser聚合查询~220RelationDaorelationfindByUser关联查询~150四步统一模式所有 DAO 方法均遵循try{conststoreXxxDao.getStore()// ① 获取 RDB 实例constrsawaitstore.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()始终执行constrsawaitstore.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 模型 AbilityContextAppStorage 文档鸿蒙开发者文档首页系列文章第87篇 DemoSeeder 数据播种器如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力UserDao 设计精髓83 行代码实现三路登录的底层持久化核心在于「幂等性设计」ensureUser和「双重用户态」内存 AppStorage 持久化 RDB的分工协作。