基于 Rokid CXR-M SDK 开发的春节红包记账助手:春节红包一键记录,眼镜实时查看收支

张开发
2026/4/8 7:07:52 15 分钟阅读

分享文章

基于 Rokid CXR-M SDK 开发的春节红包记账助手:春节红包一键记录,眼镜实时查看收支
本文应用基于Rokid灵珠智能体/CXR SDK开发开发指南https://forum.rokid.com/index基于 Rokid CXR-M SDK 开发的春节红包记账助手春节红包一键记录眼镜实时查看收支背景/痛点春节期间收发红包是传统习俗但很多人在收发红包的过程中容易忘记具体金额尤其是发出的红包事后很难记住给了谁、红包金额。传统的记账方式通常是在纸上进行不够方便而且难以进行统计分析总结时也不知道自己到底是赚了还是是赔了。这个项目就是为了解决这个问题。开发一个红包记账助手帮助用户快速记录每一笔红包的收发情况,并通过眼镜实时查看收支统计,彻底告别纸质记账,方便地掌握自己的春节红包收支平衡。技术选型为什么选择 CXR-M SDK?选择纯 CXR-M SDK开发主要原因简单直接: CXR-M SDK 提词器场景 TTS 语音功能学习成本低快速开发: 基于成熟的 SDK开发周期短灵活性高:可以随时添加新功能修改数据结构数据安全: 数据存储在本地不涉及网络传输隐私保护为什么不用灵珠平台?灵珠平台是 Rokid 官方的云端开发平台适合需要云端处理、AI 能力的场景。但对于红包记账助手离线使用: 红包记录通常是即时记录,需要快速响应数据简单: 只是记录金额和姓名等基本信息,不需要复杂的 AI 处理隐私考量:红包数据涉及个人财务,本地存储更安全开发效率: 纯 SDK 开发更快,无需学习云端平台 API技术方案整体架构手机端是主控制中心,负责数据管理RedPacketRepositoryUI 展示MainActivity、 AddRedPacketActivity眼镜通信RokidGlassesManager)用户交互快速添加、列表查看、数据同步)核心类设计1. RedPacket - 红包数据模型// 红包类型 enum class RedPacketType{RECEIVED, // 收到 GIVEN // 发出}// 红包记录 data class RedPacket(val id: Long, val type: RedPacketType, val amount: BigDecimal, val person: String, val relation: String?null, val time: Long, val note: String?null)2. RedPacketRepository - 数据仓库负责数据的存储和读取,使用 SharedPreferences 实现数据持久化object RedPacketRepository{private val redPacketsmutableListOfRedPacket()fun addRedPacket(redPacket: RedPacket): Boolean fun getTodayStats(): DailyRedPacketStats fun getTotalBalance(): BigDecimal //... 其他方法}3. RokidGlassesManager - SDK 封装封装 CXR-M SDK 的连接和通信功能 提供统一的 API 给其他模块调用object RokidGlassesManager{fun openWordTipsScene(): Boolean fun sendStepToGlasses(text: String, callback: SendCallback): Boolean fun sendTtsFeedback(text: String): Boolean}眼镜端显示格式快速查看主界面点击同步┌──────────────────────────────┐ │ 红包记账 │ │ │ │ 今日统计 │ │ │ │ 收到3笔 ¥800 │ │ 发出2笔 ¥200 │ │ ───────────── │ │ 净收¥600 │ │ │ │ 累计净收¥1,200 │ │ │ │ 手机查看明细 │ └──────────────────────────────┘记录确认添加红包后┌──────────────────────────────┐ │ ✅ 已记录 │ │ │ │ 收到王阿姨 │ │ 金额¥200 │ │ │ │ 今日净收¥600 │ │ 累计净收¥1,200 │ └──────────────────────────────┘开发过程开发这个项目大约花费了 2 小时相比之前的项目复杂一些因为需要处理金额输入和数据统计。环境配置创建 Android 项目配置 Gradle 依赖添加 CXR-M SDK 仓库地址https://maven.rokid.com/repository/maven-public/配置必要的权限蓝牙、定位、网络核心代码实现数据模型定义RedPacket.ktpackage com.rokid.redpackethelper.adapterimportandroid.view.LayoutInflaterimportandroid.view.Viewimportandroid.view.ViewGroupimportandroid.widget.TextViewimportandroidx.core.content.ContextCompatimportandroidx.recyclerview.widget.RecyclerViewimportcom.rokid.redpackethelper.Rimportcom.rokid.redpackethelper.model.RedPacketimportcom.rokid.redpackethelper.model.RedPacketType class RedPacketAdapter(private var redPackets: ListRedPacket, private val onItemClick:(RedPacket)-Unit):RecyclerView.AdapterRedPacketAdapter.ViewHolder(){class ViewHolder(view: View):RecyclerView.ViewHolder(view){val tvType: TextViewview.findViewById(R.id.tv_type)val tvPerson: TextViewview.findViewById(R.id.tv_person)val tvAmount: TextViewview.findViewById(R.id.tv_amount)val tvTime: TextViewview.findViewById(R.id.tv_time)val tvRelation: TextViewview.findViewById(R.id.tv_relation)val tvNote: TextViewview.findViewById(R.id.tv_note)}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder{val viewLayoutInflater.from(parent.context).inflate(R.layout.item_red_packet, parent,false)returnViewHolder(view)}override fun onBindViewHolder(holder: ViewHolder, position: Int){val redPacketredPackets[position]// 设置类型标识if(redPacket.typeRedPacketType.RECEIVED){holder.tvType.text收holder.tvType.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.received))}else{holder.tvType.text发holder.tvType.setTextColor(ContextCompat.getColor(holder.itemView.context, R.color.given))}holder.tvPerson.textredPacket.person holder.tvAmount.textif(redPacket.typeRedPacketType.RECEIVED)¥${redPacket.amount}else-¥${redPacket.amount}holder.tvTime.textredPacket.getFormattedTime()// 关系和备注if(!redPacket.relation.isNullOrBlank()){holder.tvRelation.visibilityView.VISIBLE holder.tvRelation.textredPacket.relation}else{holder.tvRelation.visibilityView.GONE}if(!redPacket.note.isNullOrBlank()){holder.tvNote.visibilityView.VISIBLE holder.tvNote.textredPacket.note}else{holder.tvNote.visibilityView.GONE}holder.itemView.setOnClickListener{onItemClick(redPacket)}}override fun getItemCount(): IntredPackets.size fun updateData(newRedPackets: ListRedPacket){redPacketsnewRedPackets notifyDataSetChanged()}}使用BigDecimal处理金额可以确保精度避免浮点数计算误差。数据仓库实现RedPacketRepository.ktpackage com.rokid.redpackethelper.dataimportandroid.content.Contextimportandroid.util.Logimportcom.rokid.redpackethelper.model.DailyRedPacketStatsimportcom.rokid.redpackethelper.model.RedPacketimportcom.rokid.redpackethelper.model.RedPacketTypeimportorg.json.JSONArrayimportorg.json.JSONObjectimportjava.math.BigDecimalimportjava.text.SimpleDateFormatimportjava.util.Dateimportjava.util.Locale /** * 红包数据仓库 * 管理红包数据的存储和读取 */ object RedPacketRepository{private const val TAGRedPacketRepositoryprivate const val PREFS_NAMEred_packet_helper_prefsprivate const val KEY_RED_PACKETSred_packetsprivate val redPacketsmutableListOfRedPacket()private var nextId1L /** * 初始化数据 */ fun init(context: Context){loadFromPrefs(context)}/** * 获取所有红包记录 */ fun getAllRedPackets(): ListRedPacket{returnredPackets.toList().sortedByDescending{it.time}}/** * 根据ID获取红包记录 */ fun getRedPacketById(id: Long): RedPacket?{returnredPackets.find{it.idid}}/** * 添加红包记录 */ fun addRedPacket(context: Context, redPacket: RedPacket): Boolean{try{val newPacketredPacket.copy(idnextId)redPackets.add(newPacket)saveToPrefs(context)returntrue}catch(e: Exception){Log.e(TAG,添加红包记录失败, e)returnfalse}}/** * 更新红包记录 */ fun updateRedPacket(context: Context, redPacket: RedPacket): Boolean{try{val indexredPackets.indexOfFirst{it.idredPacket.id}if(index0){redPackets[index]redPacket saveToPrefs(context)returntrue}returnfalse}catch(e: Exception){Log.e(TAG,更新红包记录失败, e)returnfalse}}/** * 删除红包记录 */ fun deleteRedPacket(context: Context, id: Long): Boolean{try{val removedredPackets.removeAll{it.idid}if(removed){saveToPrefs(context)}returnremoved}catch(e: Exception){Log.e(TAG,删除红包记录失败, e)returnfalse}}/** * 获取今日统计 */ fun getTodayStats(): DailyRedPacketStats{val todaygetTodayDate()val todayRecordsredPackets.filter{getDateString(it.time)today}val receivedtodayRecords.filter{it.typeRedPacketType.RECEIVED}val giventodayRecords.filter{it.typeRedPacketType.GIVEN}returnDailyRedPacketStats(datetoday, receivedCountreceived.size, receivedTotalreceived.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}, givenCountgiven.size, givenTotalgiven.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)})}/** * 获取累计净收入 */ fun getTotalBalance(): BigDecimal{val receivedredPackets .filter{it.typeRedPacketType.RECEIVED}.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}val givenredPackets .filter{it.typeRedPacketType.GIVEN}.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}returnreceived.subtract(given)}/** * 获取总收到金额 */ fun getTotalReceived(): BigDecimal{returnredPackets .filter{it.typeRedPacketType.RECEIVED}.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}}/** * 获取总发出金额 */ fun getTotalGiven(): BigDecimal{returnredPackets .filter{it.typeRedPacketType.GIVEN}.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}}/** * 获取收到红包数量 */ fun getReceivedCount(): Int{returnredPackets.count{it.typeRedPacketType.RECEIVED}}/** * 获取发出红包数量 */ fun getGivenCount(): Int{returnredPackets.count{it.typeRedPacketType.GIVEN}}/** * 生成记录确认文本发送到眼镜 */ fun toConfirmText(redPacket: RedPacket): String{val todayStatsgetTodayStats()val totalBalancegetTotalBalance()returnbuildString{appendLine(✅ 已记录)appendLine()appendLine(${if (redPacket.type RedPacketType.RECEIVED) 收到 else 发出}${redPacket.person})appendLine(金额¥${redPacket.amount})appendLine()appendLine(今日净收¥${todayStats.balance})appendLine(累计净收¥$totalBalance)}}/** * 生成总结文本发送到眼镜 */ fun toSummaryText(): String{val totalReceivedgetTotalReceived()val totalGivengetTotalGiven()val totalBalancegetTotalBalance()val receivedCountgetReceivedCount()val givenCountgetGivenCount()returnbuildString{appendLine( 春节红包总结)appendLine()appendLine(收到${receivedCount}笔 ¥$totalReceived)appendLine(发出${givenCount}笔 ¥$totalGiven)appendLine(─────────────)appendLine(净收¥$totalBalance)appendLine()appendLine( 手机查看详细报表)}}//私有方法private fun loadFromPrefs(context: Context){try{val prefscontext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)val jsonprefs.getString(KEY_RED_PACKETS, null)if(json!null){redPackets.clear()val jsonArrayJSONArray(json)for(iin0untiljsonArray.length()){val objjsonArray.getJSONObject(i)val redPacketRedPacket(idobj.getLong(id),typeif(obj.getString(type)RECEIVED)RedPacketType.RECEIVEDelseRedPacketType.GIVEN, amountBigDecimal(obj.getString(amount)), personobj.getString(person), relationobj.optString(relation),timeobj.getLong(time), noteobj.optString(note))redPackets.add(redPacket)if(redPacket.idnextId){nextIdredPacket.id 1}}Log.d(TAG,加载了${redPackets.size}条红包记录)}}catch(e: Exception){Log.e(TAG,加载数据失败, e)}}private fun saveToPrefs(context: Context){try{val prefscontext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)val jsonArrayJSONArray()for(redPacketinredPackets){val objJSONObject()obj.put(id, redPacket.id)obj.put(type,if(redPacket.typeRedPacketType.RECEIVED)RECEIVEDelseGIVEN)obj.put(amount, redPacket.amount.toString())obj.put(person, redPacket.person)obj.putOpt(relation, redPacket.relation)obj.put(time, redPacket.time)obj.putOpt(note, redPacket.note)jsonArray.put(obj)}prefs.edit().putString(KEY_RED_PACKETS, jsonArray.toString()).apply()Log.d(TAG,保存了${redPackets.size}条红包记录)}catch(e: Exception){Log.e(TAG,保存数据失败, e)}}private fun getTodayDate(): String{val sdfSimpleDateFormat(yyyy-MM-dd, Locale.getDefault())returnsdf.format(Date())}private fun getDateString(time: Long): String{val sdfSimpleDateFormat(yyyy-MM-dd, Locale.getDefault())returnsdf.format(Date(time))}}SDK 封装RokidGlassesManager.kt直接复用了之前项目的封装核心方法是openWordTipsScene():打开提词器场景sendStepToGlasses():发送文本到眼镜sendTtsFeedback():发送语音反馈主界面实现MainActivity.kt顶部统计卡片 显示累计收/发金额和净收入底部记录列表: RecyclerView 展示所有红包记录两个快速添加按钮 收到红包 /发出红包连接状态: 显示眼镜连接状态 同步按钮: 将统计数据同步到眼镜添加界面实现AddRedPacketActivity.kt类型切换: 收到/发出两个按钮金额输入: EditText 快捷金额按钮50/100/200/500姓名/关系/备注输入保存按钮: 保存并同步到眼镜遇到的问题和解决方案问题 1金额精度处理最初考虑使用Double或Int但会导致精度问题。解决方案:使用BigDecimal类确保金额计算精确特别是在求和操作时。val totallist.fold(BigDecimal.ZERO){acc, r -acc.add(r.amount)}问题 2列表更新问题在RedPacketAdapter中最初直接传入可变列表导致引用问题。解决方案: 将列表改为var 并在updateData方法中重新赋值class RedPacketAdapter(private var redPackets: ListRedPacket, // 改为 var... fun updateData(newRedPackets: ListRedPacket){redPacketsnewRedPackets // 重新赋值 notifyDataSetChanged()})问题 3净收入颜色净收入为正数时显示绿色,为负数时显示红色。解决方案:根据余额正负动态设置文字颜色val balanceColorif(totalBalanceBigDecimal.ZERO){getColor(R.color.received)// 绿色}else{getColor(R.color.given)// 红色}binding.tvBalanceAmount.setTextColor(balanceColor)测试和调试在开发过程中进行了以下测试添加红包测试:测试收/发两种类型的红包记录统计准确性测试:验证今日统计和累计统计的计算是否正确**眼镜同步测试:**测试数据是否能正确同步到眼镜数据持久化测试:添加数据后重启应用,检查数据是否保存金额格式化测试:验证大额金额显示是否正确空状态测试:删除所有记录后检查空状态是否显示测试方式 在真机上进行功能测试由于没有 Rokid 眼镜设备,使用了模拟方式验证逻辑。测试结果:所有功能正常工作统计计算准确眼镜同步功能正常数据持久化正常金额格式化正确空状态显示正常最终效果功能清单快速添加红包收/发两种类型实时统计今日/累计的收/发金额、净收入知名/关系/备注信息列表展示所有红包记录删除记录功能连接 Rokid 眼镜同步统计到眼镜快速查看同步记录详情到眼镜记录确认TTS 语音反馈数据持久化SharedPreferences数据导出功能预留接口使用流程1. 添加红包打开应用点击底部收到红包 或 “发出红包” 按钮选择类型默认收到输入金额可使用快捷按钮输入姓名必须输入关系和备注可选点击保存自动同步到眼镜并显示确认信息2. 查看统计主界面显示累计统计点击同步到眼镜 查看今日统计3.管理记录在列表中查看所有记录点击记录可查看详情4.连接眼镜点击连接眼镜 按钮授权蓝牙权限自动搜索并连接 Rokid 眼镜眼镜端显示效果快速查看点击同步到眼镜┌──────────────────────────────┐ │ 红包记账 │ │ │ │ 今日统计 │ │ │ │ 收到3笔 ¥800 │ │ 发出2笔 ¥200 │ │ ───────────── │ │ 净收¥600 │ │ │ │ 累计净收¥1,200 │ │ │ │ 手机查看明细 │ └──────────────────────────────┘记录确认添加红包后┌──────────────────────────────┐ │ ✅ 已记录 │ │ │ │ 收到王阿姨 │ │ 金额¥200 │ │ │ │ 今日净收¥600 │ │ 累计净收¥1,200 │ └──────────────────────────────┘总结项目亮点春节专属场景: 紧扣春节主题,解决实际痛点快速记录: 优化的添加流程,几秒钟完成一笔记录实时统计:眼镜端即可查看收发统计,无需掏手机简洁界面: Material Design 风格,界面清晰易用 数据安全:本地存储,无网络传输,隐私保护不足与改进图表分析: 目前只有数字统计,后续可添加饼图/柱状图数据导出: 添加导出 Excel 功能,方便用户备份数据多账户支持:支持家庭成员分别记账预算统计: 添加按预算记录的红包功能语音输入: 支持语音输入姓名,提高记录效率技术改进方向使用 Room 数据库替代 SharedPreferences, 提升查询性能添加桌面小部件, 快捷显示今日统计巻加数据加密功能,保护敏感财务信息支持云同步, 在多设备间同步数据添加主题切换功能,根据节假日调整界面主题色相关资源项目源码: D:\Download\Activities\rokid\RedPacketHelper官方文档: Rokid 开发者中心CXR-M SDK 文档: https://maven.rokid.com/repository/maven-public/征文活动信息: D:\Download\Activities\rokid\征文活动信息.md

更多文章