完成基础模型和数据库

This commit is contained in:
ddshi 2025-10-23 09:07:24 +08:00
parent 5137c80d11
commit c9410395a7
14 changed files with 2615 additions and 10 deletions

379
VSCODE_ANDROID_COMMANDS.md Normal file
View File

@ -0,0 +1,379 @@
# VSCode Android 开发常用指令汇总
> 别摇小鸡App开发调试指令集合 - 适用于VSCode终端
## 📋 快速导航
- [🔨 构建指令](#-构建指令)
- [📱 安装卸载](#-安装卸载)
- [🐛 调试日志](#-调试日志)
- [⚡ 快速测试流程](#-快速测试流程)
- [🔍 应用状态检查](#-应用状态检查)
- [📊 性能监控](#-性能监控)
- [🧪 单元测试](#-单元测试)
- [🛠️ 开发工具](#-开发工具)
---
## 🔨 构建指令
### 基础构建
```bash
# 完整构建Debug版本
./gradlew assembleDebug
# 快速增量构建 (推荐日常使用)
./gradlew build
# 清理后重新构建
./gradlew clean build
# 构建Release版本
./gradlew assembleRelease
```
### 构建信息查看
```bash
# 查看构建详情
./gradlew assembleDebug --info
# 查看构建错误堆栈
./gradlew assembleDebug --stacktrace
# 构建并运行测试
./gradlew build connectedDebugAndroidTest
```
---
## 📱 安装卸载
### 安装应用
```bash
# 安装Debug APK (最常用)
adb install -r "E:\chick_mood\app\build\outputs\apk\debug\app-debug.apk"
# 强制安装 (覆盖现有版本)
adb install -r -d "app\build\outputs\apk\debug\app-debug.apk"
# 安装Release APK
adb install -r "E:\chick_mood\app\build\outputs\apk\release\app-release.apk"
```
### 卸载应用
```bash
# 完全卸载应用
adb uninstall com.piyomood
# 卸载并清除数据
adb uninstall -k com.piyomood
```
### APK文件路径
```bash
# Debug APK位置
E:\chick_mood\app\build\outputs\apk\debug\app-debug.apk
# Release APK位置
E:\chick_mood\app\build\outputs\apk\release\app-release.apk
```
---
## 🐛 调试日志
### 日志查看
```bash
# 清除日志缓存 (测试前执行)
adb logcat -c
# 实时查看所有日志
adb logcat
# 查看应用相关日志
adb logcat | grep -E "(PiyoMood|com.piyomood)"
# 查看崩溃日志
adb logcat | grep -E "(AndroidRuntime|FATAL|ERROR)"
# 查看最近的50条应用日志
adb logcat -d | grep -E "(PiyoMood|com.piyomood)" | tail -50
```
### 崩溃调试
```bash
# 启动应用并捕获崩溃
adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1 && sleep 3 && adb logcat -d | grep -E "(AndroidRuntime|FATAL)" | tail -20
# 查看详细崩溃信息
adb logcat -d | grep -A 30 -B 5 "FATAL EXCEPTION"
```
### 传感器调试
```bash
# 查看传感器相关日志
adb logcat | grep -E "(Sensor|Accelerometer)"
# 查看动画相关日志
adb logcat | grep -E "(Lottie|Animation)"
```
---
## ⚡ 快速测试流程
### 完整测试流程 (推荐)
```bash
# 1. 构建并安装
./gradlew assembleDebug && adb install -r "app\build\outputs\apk\debug\app-debug.apk"
# 2. 启动应用测试
adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1
# 3. 查看启动结果
sleep 5 && adb logcat -d | grep -E "(AndroidRuntime|FATAL|PiyoMood)" | tail -20
```
### 快速迭代测试
```bash
# 一键测试脚本 (复制到VSCode终端使用)
./gradlew assembleDebug && adb install -r "app\build\outputs\apk\debug\app-debug.apk" && adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1
```
### 应用启动测试
```bash
# 清除日志并启动应用
adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1
# 检查应用是否正常启动
sleep 3 && adb logcat -d | grep -E "(ActivityTaskManager.*Displayed|Activity idle)"
```
---
## 🔍 应用状态检查
### Activity状态检查
```bash
# 查看当前运行的Activities
adb shell dumpsys activity activities | grep "com.piyomood"
# 查看顶部Activity
adb shell dumpsys activity top | grep -A 10 -B 10 "PiyoMood"
# 查看应用进程状态
adb shell ps | grep piyomood
```
### 应用信息查看
```bash
# 查看应用包信息
adb shell dumpsys package com.piyomood
# 查看应用权限
adb shell dumpsys package com.piyomood | grep "declared permissions"
# 查看应用版本信息
adb shell dumpsys package com.piyomood | grep -A 5 -B 5 "versionName"
```
---
## 📊 性能监控
### 内存使用
```bash
# 查看应用内存使用情况
adb shell dumpsys meminfo com.piyomood
# 实时监控内存使用
adb shell top | grep piyomood
```
### CPU使用
```bash
# 查看应用CPU使用情况
adb shell top | grep piyomood
# 查看系统整体性能
adb shell dumpsys cpuinfo | grep piyomood
```
### 电池使用
```bash
# 查看电池使用情况
adb shell dumpsys batterystats | grep piyomood
# 重置电池统计数据
adb shell dumpsys batterystats --reset
```
---
## 🧪 单元测试
### 运行测试
```bash
# 运行所有单元测试
./gradlew test
# 运行特定测试类
./gradlew test --tests "com.piyomood.data.model.*"
# 运行Debug测试
./gradlew connectedDebugAndroidTest
# 查看测试报告
./gradlew test --continue
```
### 测试报告位置
```bash
# 单元测试报告
app/build/reports/tests/testDebugUnitTest/index.html
# Android测试报告
app/build/reports/androidTests/connected/index.html
```
---
## 🛠️ 开发工具
### 设备管理
```bash
# 查看连接的设备
adb devices
# 查看设备详细信息
adb shell getprop
# 查看设备屏幕密度
adb shell wm density
# 截屏
adb shell screencap -p /sdcard/screenshot.png && adb pull /sdcard/screenshot.png
```
### 文件操作
```bash
# 推送文件到设备
adb push local_file.txt /sdcard/
# 从设备拉取文件
adb pull /sdcard/device_file.txt
# 查看设备文件
adb shell ls /sdcard/
# 查看应用数据目录
adb shell run-as com.piyomood ls -la /data/data/com.piyomood/
```
### 数据库调试
```bash
# 查看应用数据库文件
adb shell run-as com.piyomood find /data/data/com.piyomood/databases -name "*.db"
# 导出数据库文件
adb shell run-as com.piyomood cat /data/data/com.piyomood/databases/your_database.db > local_database.db
```
---
## 🚨 常见问题解决
### 构建问题
```bash
# 清理构建缓存
./gradlew clean
# 清理Gradle缓存
./gradlew --refresh-dependencies
# 重置项目
./gradlew clean build --refresh-dependencies
```
### 安装问题
```bash
# 安装失败时尝试
adb uninstall com.piyomood && adb install -r "app\build\outputs\apk\debug\app-debug.apk"
# 签名问题 (Release构建)
./gradlew assembleRelease
```
### 连接问题
```bash
# 重启ADB服务
adb kill-server && adb start-server
# 检查设备连接
adb devices -l
# 重新连接设备
adb reconnect
```
---
## 📝 VSCode集成提示
### 集成终端快捷键
- `Ctrl + ` (反引号) - 打开集成终端
- `Ctrl + Shift + ` - 新建终端
- `Ctrl + C` - 终止当前命令
### 常用组合指令 (可保存为VSCode任务)
```json
// tasks.json 示例
{
"version": "2.0.0",
"tasks": [
{
"label": "Build and Install",
"type": "shell",
"command": "./gradlew assembleDebug && adb install -r \"app\\build\\outputs\\apk\\debug\\app-debug.apk\"",
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
},
{
"label": "Start App and Check Logs",
"type": "shell",
"command": "adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1 && sleep 3 && adb logcat -d | grep -E \"(AndroidRuntime|FATAL|PiyoMood)\" | tail -20",
"group": "test"
}
]
}
```
---
## 🎯 推荐工作流程
### 日常开发流程
1. **代码修改** → 2. **快速构建测试** → 3. **安装运行** → 4. **查看日志**
```bash
# 推荐的一键命令
./gradlew assembleDebug && adb install -r "app\build\outputs\apk\debug\app-debug.apk" && adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1
```
### 问题调试流程
1. **复现问题** → 2. **查看崩溃日志** → 3. **定位问题代码** → 4. **修复** → 5. **验证**
```bash
# 调试专用命令
adb logcat -c && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1 && sleep 5 && adb logcat -d | grep -E "(AndroidRuntime|FATAL|ERROR)" | tail -30
```
---
*最后更新: 2025-10-22*
*适用于项目: 别摇小鸡心情记录App (Chick_Mood)*

188
VSCODE_QUICK_START.md Normal file
View File

@ -0,0 +1,188 @@
# VSCode Android 开发快速开始指南
## 📋 前置条件
1. **安装VSCode扩展**
- Android
- Gradle for Java
- Android iOS Emulator (可选)
2. **环境准备**
- Android SDK已安装
- ADB设备已连接
- 项目已在VSCode中打开
---
## 🚀 快速使用
### 1. 构建和运行应用
**方法一:使用命令面板 (推荐)**
- `Ctrl + Shift + P` 打开命令面板
- 输入 `Tasks: Run Task`
- 选择任务:
- `🔨 构建Debug版本` - 仅构建
- `📱 构建并安装应用` - 构建并安装
- `⚡ 完整测试流程` - 构建→安装→启动→检查
**方法二:使用快捷键**
- `Ctrl + Shift + B` - 构建项目
- `F5` - 启动调试
**方法三:使用终端**
- `Ctrl + `` 打开集成终端
- 直接输入命令(参考 VSCODE_ANDROID_COMMANDS.md
### 2. 调试应用启动
**使用调试面板**
- `Ctrl + Shift + D` 打开调试面板
- 选择配置:
- `🚀 启动应用并调试`
- `📱 安装并启动应用`
- `🐛 调试应用启动过程`
- 点击绿色播放按钮开始
**查看调试信息**
- 调试控制台:`Ctrl + Shift + Y`
- 集成终端:`Ctrl + `
### 3. 常用任务列表
#### 开发流程任务
```
🔨 构建Debug版本 # 仅构建APK
📱 构建并安装应用 # 构建并安装到设备
🚀 启动应用测试 # 启动已安装的应用
⚡ 完整测试流程 # 一键:构建→安装→启动→检查
```
#### 调试任务
```
🐛 查看应用崩溃日志 # 查看最近的崩溃信息
清除日志缓存 # 清空logcat缓存
🧪 运行单元测试 # 运行所有单元测试
```
#### 维护任务
```
🧹 清理项目 # 清理构建缓存
📱 卸载应用 # 从设备卸载应用
📊 查看应用内存使用 # 监控内存使用情况
🔍 查看应用进程状态 # 查看应用运行状态
```
---
## 🎯 推荐工作流程
### 日常开发
1. **修改代码**
2. `Ctrl + Shift + P``Tasks: Run Task``🔨 构建Debug版本`
3. 等待构建完成
4. `Ctrl + Shift + P``Tasks: Run Task``📱 构建并安装应用`
5. 在设备上测试功能
### 问题调试
1. **复现问题**
2. `Ctrl + Shift + P``Tasks: Run Task``🐛 查看应用崩溃日志`
3. 分析日志,定位问题
4. 修复代码
5. 重新构建测试
### 快速测试
1. **一键测试**`Ctrl + Shift + P``Tasks: Run Task``⚡ 完整测试流程`
2. 这会自动执行:构建→安装→启动→检查日志
---
## 🔧 自定义配置
### 添加新任务
1. 编辑 `.vscode/tasks.json`
2. 在 `tasks` 数组中添加新任务配置
3. 重启VSCode或重新加载窗口 (`Ctrl+Shift+P` → "Developer: Reload Window")
### 修改快捷键
1. 编辑 `.vscode/keybindings.json` (如果不存在则创建)
2. 添加自定义快捷键绑定
### 示例:添加快速日志查看快捷键
```json
[
{
"key": "ctrl+shift+l",
"command": "workbench.action.terminal.sendSequence",
"args": {
"text": "adb logcat -d | grep -E \"(PiyoMood|AndroidRuntime)\" | tail -20\u000D"
}
}
]
```
---
## 📱 设备连接检查
### 检查设备状态
```bash
# 在终端中运行
adb devices
```
### 如果设备未显示
1. 确保USB调试已开启
2. 检查USB连接
3. 重启ADB服务`adb kill-server && adb start-server`
4. 重新授权设备
---
## 🚨 常见问题解决
### 构建失败
1. 运行 `🧹 清理项目` 任务
2. 检查网络连接(下载依赖)
3. 查看构建错误信息
### 安装失败
1. 运行 `📱 卸载应用` 任务
2. 重新运行 `📱 构建并安装应用`
3. 检查设备存储空间
### 调试无响应
1. 检查设备连接状态
2. 重启VSCode
3. 重启ADB服务
---
## 💡 实用技巧
### 批量操作
- 可以在终端中同时运行多个命令
- 使用 `&&` 连接命令:`./gradlew build && adb install -r ...`
### 日志过滤
- 应用相关:`adb logcat | grep piyomood`
- 崩溃信息:`adb logcat | grep -E "(FATAL|AndroidRuntime)"`
- 传感器:`adb logcat | grep -i sensor`
### 快速重启
- 重启应用:`adb shell am force-stop com.piyomood && adb shell monkey -p com.piyomood -c android.intent.category.LAUNCHER 1`
- 重启ADB`adb kill-server && adb start-server`
---
## 📚 相关文件
- `VSCODE_ANDROID_COMMANDS.md` - 完整命令参考
- `.vscode/tasks.json` - VSCode任务配置
- `.vscode/launch.json` - VSCode调试配置
---
*祝你开发愉快!如有问题,请查看完整命令参考或联系开发团队。*
**快速测试快捷键**: `Ctrl + Shift + P``Tasks: Run Task``⚡ 完整测试流程`

View File

@ -40,6 +40,14 @@ android {
buildFeatures {
viewBinding = true
}
// Room schema导出配置
kapt {
correctErrorTypes = true
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -64,6 +72,7 @@ dependencies {
// Room数据库
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-paging:2.6.1")
kapt("androidx.room:room-compiler:2.6.1")
// ViewPager2和分页

View File

@ -0,0 +1,224 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "cf2039a505c0b4236bf1f858cc1de4e3",
"entities": [
{
"tableName": "mood_records",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `emotion` TEXT NOT NULL, `moodIntensity` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `textContent` TEXT, `imagePath` TEXT, `isFavorite` INTEGER NOT NULL, `shakeDuration` REAL NOT NULL, `maxAcceleration` REAL NOT NULL, `deviceModel` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emotion",
"columnName": "emotion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "moodIntensity",
"columnName": "moodIntensity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "textContent",
"columnName": "textContent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "imagePath",
"columnName": "imagePath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isFavorite",
"columnName": "isFavorite",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "shakeDuration",
"columnName": "shakeDuration",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "maxAcceleration",
"columnName": "maxAcceleration",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "deviceModel",
"columnName": "deviceModel",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `nickname` TEXT, `avatarPath` TEXT, `isDarkMode` INTEGER NOT NULL, `isSoundEnabled` INTEGER NOT NULL, `isVibrationEnabled` INTEGER NOT NULL, `isReminderEnabled` INTEGER NOT NULL, `reminderHour` INTEGER NOT NULL, `reminderMinute` INTEGER NOT NULL, `appVersion` TEXT NOT NULL, `dataVersion` INTEGER NOT NULL, `firstUseTime` INTEGER NOT NULL, `totalUsageCount` INTEGER NOT NULL, `lastUsedTime` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "nickname",
"columnName": "nickname",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "avatarPath",
"columnName": "avatarPath",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDarkMode",
"columnName": "isDarkMode",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSoundEnabled",
"columnName": "isSoundEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isVibrationEnabled",
"columnName": "isVibrationEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isReminderEnabled",
"columnName": "isReminderEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reminderHour",
"columnName": "reminderHour",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reminderMinute",
"columnName": "reminderMinute",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "appVersion",
"columnName": "appVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "dataVersion",
"columnName": "dataVersion",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstUseTime",
"columnName": "firstUseTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "totalUsageCount",
"columnName": "totalUsageCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastUsedTime",
"columnName": "lastUsedTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "user_config_entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `config_key` TEXT NOT NULL, `config_value` TEXT NOT NULL, `updated_at` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "configKey",
"columnName": "config_key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "configValue",
"columnName": "config_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updated_at",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cf2039a505c0b4236bf1f858cc1de4e3')"
]
}
}

View File

@ -0,0 +1,238 @@
package com.chick_mood.data.database
import androidx.room.*
import androidx.sqlite.db.SupportSQLiteDatabase
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.chick_mood.data.model.MoodRecord
import com.chick_mood.data.model.UserConfig
import com.chick_mood.data.model.UserConfigEntry
/**
* 应用主数据库
*
* 使用Room数据库框架管理应用的本地数据存储
* 包含心情记录和用户配置两个主要数据表
* 支持数据库迁移和预填充数据
*
* @author Claude
* @date 2025-10-22
*/
@Database(
entities = [
MoodRecord::class,
UserConfig::class,
UserConfigEntry::class
],
version = 2,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
/**
* 心情记录数据访问对象
*/
abstract fun moodRecordDao(): MoodRecordDao
/**
* 用户配置键值对数据访问对象
*/
abstract fun userConfigEntryDao(): UserConfigEntryDao
companion object {
/**
* 数据库名称
*/
const val DATABASE_NAME = "chick_mood_database"
/**
* 数据库版本
*/
const val DATABASE_VERSION = 1
/**
* 数据库实例单例
*/
@Volatile
private var INSTANCE: AppDatabase? = null
/**
* 获取数据库实例
*
* 使用单例模式确保全局只有一个数据库实例
* 如果实例不存在则创建新的实例
*
* @param context 应用上下文
* @return 数据库实例
*/
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
)
.addCallback(DatabaseCallback())
.fallbackToDestructiveMigration() // 开发阶段使用,生产环境应提供具体的迁移策略
.build()
INSTANCE = instance
instance
}
}
/**
* 关闭数据库实例
* 主要用于测试场景
*/
fun closeDatabase() {
INSTANCE?.close()
INSTANCE = null
}
}
/**
* 数据库回调
*
* 处理数据库创建和打开时的初始化操作
*/
private class DatabaseCallback : RoomDatabase.Callback() {
/**
* 数据库创建时调用
* 初始化默认的用户配置数据
*/
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// 在IO线程中执行数据库初始化操作
CoroutineScope(Dispatchers.IO).launch {
populateDatabase(db)
}
}
/**
* 数据库打开时调用
* 可以在这里执行数据验证或迁移后的清理工作
*/
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
// 启用外键约束
db.execSQL("PRAGMA foreign_keys=ON")
}
/**
* 填充初始数据
*
* @param database 数据库实例
*/
private suspend fun populateDatabase(database: SupportSQLiteDatabase) {
try {
// 插入默认用户配置
insertDefaultUserConfigs(database)
// 可以在这里添加其他初始化数据
// 例如:示例心情记录、默认设置等
} catch (e: Exception) {
// 记录初始化错误,但不影响应用启动
e.printStackTrace()
}
}
/**
* 插入默认用户配置
*
* @param db 数据库实例
*/
private fun insertDefaultUserConfigs(db: SupportSQLiteDatabase) {
val currentTime = System.currentTimeMillis() / 1000
// 应用版本
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('app_version', '1.0.0', $currentTime)
""".trimIndent()
)
// 首次使用时间
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('first_use_time', $currentTime, $currentTime)
""".trimIndent()
)
// 使用天数初始化
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('usage_days', 1, $currentTime)
""".trimIndent()
)
// 心情记录总数初始化
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('total_mood_records', 0, $currentTime)
""".trimIndent()
)
// 最后活跃时间
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('last_active_time', $currentTime, $currentTime)
""".trimIndent()
)
// 默认用户昵称
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('user_nickname', '小鸡主人', $currentTime)
""".trimIndent()
)
// 默认提醒设置
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('morning_reminder', '09:00', $currentTime)
""".trimIndent()
)
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('evening_reminder', '21:00', $currentTime)
""".trimIndent()
)
// 默认功能开关
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('notification_enabled', 1, $currentTime)
""".trimIndent()
)
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('sound_enabled', 1, $currentTime)
""".trimIndent()
)
db.execSQL(
"""
INSERT OR REPLACE INTO user_config (config_key, config_value, updated_at)
VALUES ('vibration_enabled', 1, $currentTime)
""".trimIndent()
)
}
}
}

View File

@ -0,0 +1,129 @@
package com.chick_mood.data.database
import androidx.room.TypeConverter
import com.chick_mood.data.model.Emotion
import java.util.Date
/**
* Room数据库类型转换器
*
* 提供Room数据库无法直接处理的数据类型转换
* 包括枚举类型日期类型等的序列化和反序列化
*
* @author Claude
* @date 2025-10-22
*/
class Converters {
/**
* 将Emotion枚举转换为字符串
*
* @param emotion 情绪枚举
* @return 枚举名称字符串
*/
@TypeConverter
fun fromEmotion(emotion: Emotion): String {
return emotion.name
}
/**
* 将字符串转换为Emotion枚举
*
* @param emotionString 枚举名称字符串
* @return 情绪枚举如果转换失败则返回HAPPY作为默认值
*/
@TypeConverter
fun toEmotion(emotionString: String): Emotion {
return try {
Emotion.valueOf(emotionString)
} catch (e: IllegalArgumentException) {
// 如果枚举值不存在,返回默认值
Emotion.HAPPY
}
}
/**
* 将时间戳转换为Date对象
*
* @param value 时间戳毫秒
* @return Date对象如果值为null则返回当前时间
*/
@TypeConverter
fun fromDate(value: Long?): Date {
return Date(value ?: System.currentTimeMillis())
}
/**
* 将Date对象转换为时间戳
*
* @param date Date对象
* @return 时间戳毫秒如果date为null则返回当前时间
*/
@TypeConverter
fun toDate(date: Date?): Long {
return date?.time ?: System.currentTimeMillis()
}
/**
* 将字符串列表转换为JSON字符串
* 用于存储图片路径列表等复杂数据
*
* @param list 字符串列表
* @return JSON字符串
*/
@TypeConverter
fun fromStringList(list: List<String>?): String {
return if (list.isNullOrEmpty()) {
"[]"
} else {
// 简单的JSON格式转换
list.joinToString(prefix = "[", postfix = "]", separator = ",") { "\"$it\"" }
}
}
/**
* 将JSON字符串转换为字符串列表
*
* @param jsonString JSON字符串
* @return 字符串列表
*/
@TypeConverter
fun toStringList(jsonString: String?): List<String> {
return if (jsonString.isNullOrEmpty() || jsonString == "[]") {
emptyList()
} else {
try {
// 简单的JSON解析仅处理标准格式
jsonString
.removeSurrounding("[", "]")
.split(",")
.map { it.removeSurrounding("\"").trim() }
.filter { it.isNotEmpty() }
} catch (e: Exception) {
emptyList()
}
}
}
/**
* 将布尔值转换为整数
*
* @param value 布尔值
* @return 1表示true0表示false
*/
@TypeConverter
fun fromBoolean(value: Boolean?): Int {
return if (value == true) 1 else 0
}
/**
* 将整数转换为布尔值
*
* @param value 整数值
* @return 1表示true其他值表示false
*/
@TypeConverter
fun toBoolean(value: Int?): Boolean {
return value == 1
}
}

View File

@ -0,0 +1,155 @@
package com.chick_mood.data.database
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import com.chick_mood.data.model.Emotion
import com.chick_mood.data.model.UserConfigEntry
/**
* 数据库测试辅助类
*
* 用于在应用启动时测试数据库功能
* 验证基本的CRUD操作是否正常工作
*
* @author Claude
* @date 2025-10-22
*/
class DatabaseTestHelper private constructor() {
companion object {
private const val TAG = "DatabaseTestHelper"
/**
* 在应用启动时运行数据库测试
*/
fun runDatabaseTests(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
try {
Log.d(TAG, "开始数据库测试...")
val dbManager = SimpleDatabaseManager.getInstance(context)
// 测试1: 创建心情记录
testCreateMoodRecord(dbManager)
// 测试2: 查询心情记录
testQueryMoodRecords(dbManager)
// 测试3: 用户配置操作
testUserConfig(dbManager)
// 测试4: 初始化默认配置
testInitializeDefaultConfig(dbManager)
Log.d(TAG, "所有数据库测试完成!")
} catch (e: Exception) {
Log.e(TAG, "数据库测试失败", e)
}
}
}
/**
* 测试创建心情记录
*/
private suspend fun testCreateMoodRecord(dbManager: SimpleDatabaseManager) {
Log.d(TAG, "测试创建心情记录...")
val recordId = dbManager.createMoodRecord(
emotion = Emotion.HAPPY,
intensity = 85,
noteText = "数据库测试记录",
imagePaths = emptyList()
)
Log.d(TAG, "创建的心情记录ID: $recordId")
assertTrue("记录ID应该大于0", recordId > 0)
}
/**
* 测试查询心情记录
*/
private suspend fun testQueryMoodRecords(dbManager: SimpleDatabaseManager) {
Log.d(TAG, "测试查询心情记录...")
val recentRecords = dbManager.getRecentMoodRecords(5)
Log.d(TAG, "查询到 ${recentRecords.size} 条最近记录")
val totalCount = dbManager.getMoodRecordCount()
Log.d(TAG, "心情记录总数: $totalCount")
assertTrue("记录数应该大于等于0", totalCount >= 0)
}
/**
* 测试用户配置操作
*/
private suspend fun testUserConfig(dbManager: SimpleDatabaseManager) {
Log.d(TAG, "测试用户配置操作...")
// 设置测试昵称
dbManager.setUserNickname("数据库测试用户")
val nickname = dbManager.getUserNickname()
Log.d(TAG, "用户昵称: $nickname")
assertEquals("昵称应该正确", "数据库测试用户", nickname)
// 设置测试配置
dbManager.setConfigValue("test_key", "test_value")
val value = dbManager.getConfigValue("test_key")
Log.d(TAG, "测试配置值: $value")
assertEquals("配置值应该正确", "test_value", value)
}
/**
* 测试初始化默认配置
*/
private suspend fun testInitializeDefaultConfig(dbManager: SimpleDatabaseManager) {
Log.d(TAG, "测试初始化默认配置...")
dbManager.initializeDefaultConfig()
// 验证默认配置是否设置成功
val nickname = dbManager.getUserNickname()
Log.d(TAG, "默认昵称: $nickname")
assertNotNull("应该有默认昵称", nickname)
val version = dbManager.getConfigValue(UserConfigEntry.KEY_APP_VERSION)
Log.d(TAG, "应用版本: $version")
assertNotNull("应该有版本信息", version)
val soundEnabled = dbManager.getConfigValue(UserConfigEntry.KEY_SOUND_ENABLED)
Log.d(TAG, "声音开关: $soundEnabled")
assertNotNull("应该有声音开关设置", soundEnabled)
}
/**
* 简单的断言方法
*/
private fun assertTrue(message: String, condition: Boolean) {
if (condition) {
Log.d(TAG, "$message")
} else {
Log.e(TAG, "$message")
}
}
private fun assertEquals(message: String, expected: String?, actual: String?) {
if (expected == actual) {
Log.d(TAG, "$message - 期望: $expected, 实际: $actual")
} else {
Log.e(TAG, "$message - 期望: $expected, 实际: $actual")
}
}
private fun assertNotNull(message: String, value: Any?) {
if (value != null) {
Log.d(TAG, "$message - 值: $value")
} else {
Log.e(TAG, "$message - 值为null")
}
}
}
}

View File

@ -0,0 +1,207 @@
package com.chick_mood.data.database
import androidx.room.*
import androidx.paging.PagingSource
import kotlinx.coroutines.flow.Flow
import com.chick_mood.data.model.MoodRecord
import com.chick_mood.data.model.Emotion
/**
* 心情记录数据访问对象
*
* 提供心情记录的数据库操作接口包括增删改查和统计功能
* 支持分页查询和流式数据更新
*
* @author Claude
* @date 2025-10-22
*/
@Dao
interface MoodRecordDao {
/**
* 插入新的心情记录
*
* @param moodRecord 要插入的心情记录
* @return 插入记录的行ID
*/
@Insert
suspend fun insertMoodRecord(moodRecord: MoodRecord): Long
/**
* 批量插入心情记录
*
* @param moodRecords 要插入的心情记录列表
* @return 插入记录的行ID数组
*/
@Insert
suspend fun insertMoodRecords(moodRecords: List<MoodRecord>): List<Long>
/**
* 更新心情记录
*
* @param moodRecord 要更新的心情记录
* @return 受影响的行数
*/
@Update
suspend fun updateMoodRecord(moodRecord: MoodRecord): Int
/**
* 删除心情记录
*
* @param moodRecord 要删除的心情记录
* @return 受影响的行数
*/
@Delete
suspend fun deleteMoodRecord(moodRecord: MoodRecord): Int
/**
* 根据ID删除心情记录
*
* @param id 记录ID
* @return 受影响的行数
*/
@Query("DELETE FROM mood_records WHERE id = :id")
suspend fun deleteMoodRecordById(id: Long): Int
/**
* 根据ID获取心情记录
*
* @param id 记录ID
* @return 心情记录如果不存在则返回null
*/
@Query("SELECT * FROM mood_records WHERE id = :id")
suspend fun getMoodRecordById(id: Long): MoodRecord?
/**
* 获取所有心情记录按时间倒序
*
* @return 心情记录列表按创建时间倒序排列
*/
@Query("SELECT * FROM mood_records ORDER BY timestamp DESC")
fun getAllMoodRecords(): Flow<List<MoodRecord>>
/**
* 获取分页的心情记录按时间倒序
*
* @return PagingSource用于分页加载
*/
@Query("SELECT * FROM mood_records ORDER BY timestamp DESC")
fun getPagedMoodRecords(): PagingSource<Int, MoodRecord>
/**
* 获取最近的N条心情记录
*
* @param limit 记录数量限制
* @return 心情记录列表按时间倒序排列
*/
@Query("SELECT * FROM mood_records ORDER BY timestamp DESC LIMIT :limit")
suspend fun getRecentMoodRecords(limit: Int): List<MoodRecord>
/**
* 根据情绪类型获取心情记录
*
* @param emotion 情绪类型
* @return 该情绪类型的心情记录列表按时间倒序排列
*/
@Query("SELECT * FROM mood_records WHERE emotion = :emotion ORDER BY timestamp DESC")
suspend fun getMoodRecordsByEmotion(emotion: Emotion): List<MoodRecord>
/**
* 获取指定日期范围内的心情记录
*
* @param startDate 开始日期时间戳
* @param endDate 结束日期时间戳
* @return 日期范围内的心情记录列表按时间倒序排列
*/
@Query("SELECT * FROM mood_records WHERE timestamp BETWEEN :startDate AND :endDate ORDER BY timestamp DESC")
suspend fun getMoodRecordsByDateRange(startDate: Long, endDate: Long): List<MoodRecord>
/**
* 获取今天的心情记录
*
* @param startOfDay 今天开始时间戳
* @param endOfDay 今天结束时间戳
* @return 今天的心情记录列表
*/
@Query("SELECT * FROM mood_records WHERE timestamp BETWEEN :startOfDay AND :endOfDay ORDER BY timestamp DESC")
suspend fun getTodayMoodRecords(startOfDay: Long, endOfDay: Long): List<MoodRecord>
/**
* 获取心情记录总数
*
* @return 心情记录总数
*/
@Query("SELECT COUNT(*) FROM mood_records")
suspend fun getMoodRecordCount(): Int
/**
* 根据情绪类型统计记录数量
*
* @return 情绪统计结果列表
*/
@Query("SELECT emotion, COUNT(*) as count FROM mood_records GROUP BY emotion")
suspend fun getMoodStatistics(): List<EmotionStatistic>
/**
* 获取最近的7天心情记录统计
*
* @param sevenDaysAgo 7天前的时间戳
* @return 7天内的心情记录列表
*/
@Query("SELECT * FROM mood_records WHERE timestamp >= :sevenDaysAgo ORDER BY timestamp DESC")
suspend fun getRecentWeekMoodRecords(sevenDaysAgo: Long): List<MoodRecord>
/**
* 搜索心情记录按文字内容
*
* @param query 搜索关键词
* @return 包含关键词的心情记录列表
*/
@Query("SELECT * FROM mood_records WHERE textContent LIKE '%' || :query || '%' ORDER BY timestamp DESC")
suspend fun searchMoodRecords(query: String): List<MoodRecord>
/**
* 获取心情强度平均值
*
* @param emotion 情绪类型可选
* @return 平均心情强度值
*/
@Query("SELECT AVG(moodIntensity) FROM mood_records WHERE (:emotion IS NULL OR emotion = :emotion)")
suspend fun getAverageIntensity(emotion: Emotion? = null): Float?
/**
* 清除所有心情记录
*
* @return 删除的记录数量
*/
@Query("DELETE FROM mood_records")
suspend fun clearAllMoodRecords(): Int
/**
* 删除指定日期之前的记录
*
* @param beforeDate 日期时间戳
* @return 删除的记录数量
*/
@Query("DELETE FROM mood_records WHERE timestamp < :beforeDate")
suspend fun deleteOldMoodRecords(beforeDate: Long): Int
}
/**
* 情绪统计数据类
* 用于Room查询结果映射
*
* @author Claude
* @date 2025-10-22
*/
data class EmotionStatistic(
/**
* 情绪类型
*/
val emotion: Emotion,
/**
* 该情绪类型的记录数量
*/
val count: Int
)

View File

@ -0,0 +1,254 @@
package com.chick_mood.data.database
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.chick_mood.data.model.MoodRecord
import com.chick_mood.data.model.Emotion
import com.chick_mood.data.model.UserConfigEntry
import java.util.Date
/**
* 简化的数据库管理器
*
* 提供基础的数据库操作用于测试和验证数据库功能
* 包含心情记录和用户配置的基本CRUD操作
*
* @author Claude
* @date 2025-10-22
*/
class SimpleDatabaseManager private constructor(private val context: Context) {
/**
* 数据库实例
*/
private val database = AppDatabase.getDatabase(context)
/**
* 协程作用域
*/
private val scope = CoroutineScope(Dispatchers.IO)
companion object {
/**
* 单例实例
*/
@Volatile
private var INSTANCE: SimpleDatabaseManager? = null
/**
* 获取数据库管理器实例
*
* @param context 应用上下文
* @return 数据库管理器实例
*/
fun getInstance(context: Context): SimpleDatabaseManager {
return INSTANCE ?: synchronized(this) {
val instance = SimpleDatabaseManager(context.applicationContext)
INSTANCE = instance
instance
}
}
}
// ==================== 心情记录操作 ====================
/**
* 创建心情记录
*/
suspend fun createMoodRecord(
emotion: Emotion,
intensity: Int,
noteText: String = "",
imagePaths: List<String> = emptyList()
): Long {
return withContext(Dispatchers.IO) {
val moodRecord = MoodRecord(
emotion = emotion,
moodIntensity = intensity,
timestamp = System.currentTimeMillis(),
textContent = noteText,
imagePath = imagePaths.firstOrNull()
)
database.moodRecordDao().insertMoodRecord(moodRecord)
}
}
/**
* 获取心情记录详情
*/
suspend fun getMoodRecord(id: Long): MoodRecord? {
return withContext(Dispatchers.IO) {
database.moodRecordDao().getMoodRecordById(id)
}
}
/**
* 获取最近的心情记录
*/
suspend fun getRecentMoodRecords(limit: Int = 10): List<MoodRecord> {
return withContext(Dispatchers.IO) {
database.moodRecordDao().getRecentMoodRecords(limit)
}
}
/**
* 获取心情记录总数
*/
suspend fun getMoodRecordCount(): Int {
return withContext(Dispatchers.IO) {
database.moodRecordDao().getMoodRecordCount()
}
}
// ==================== 用户配置操作 ====================
/**
* 获取用户昵称
*/
suspend fun getUserNickname(): String? {
return withContext(Dispatchers.IO) {
database.userConfigEntryDao().getUserNickname()
}
}
/**
* 设置用户昵称
*/
suspend fun setUserNickname(nickname: String) {
scope.launch {
database.userConfigEntryDao().setUserNickname(nickname)
}
}
/**
* 设置配置值
*/
suspend fun setConfigValue(key: String, value: String) {
scope.launch {
database.userConfigEntryDao().setConfigValue(key, value)
}
}
/**
* 获取配置值
*/
suspend fun getConfigValue(key: String): String? {
return withContext(Dispatchers.IO) {
database.userConfigEntryDao().getConfigValue(key)
}
}
/**
* 初始化默认配置
*/
suspend fun initializeDefaultConfig() {
withContext(Dispatchers.IO) {
val dao = database.userConfigEntryDao()
// 设置默认昵称
if (dao.getUserNickname().isNullOrEmpty()) {
dao.setUserNickname("小鸡主人")
}
// 设置默认版本
if (dao.getConfigValue(UserConfigEntry.KEY_APP_VERSION).isNullOrEmpty()) {
dao.setAppVersion("1.0.0")
}
// 初始化使用天数
if (dao.getUsageDays() == null) {
dao.initializeUsageDays()
}
// 初始化心情记录计数
if (dao.getTotalMoodRecords() == null) {
dao.setTotalMoodRecords(0)
}
// 设置提醒时间
if (dao.getReminderTime(UserConfigEntry.KEY_MORNING_REMINDER).isNullOrEmpty()) {
dao.setReminderTime(UserConfigEntry.KEY_MORNING_REMINDER, "09:00")
}
if (dao.getReminderTime(UserConfigEntry.KEY_EVENING_REMINDER).isNullOrEmpty()) {
dao.setReminderTime(UserConfigEntry.KEY_EVENING_REMINDER, "21:00")
}
// 设置功能开关
if (dao.getConfigValue(UserConfigEntry.KEY_NOTIFICATION_ENABLED).isNullOrEmpty()) {
dao.setFeatureEnabled(UserConfigEntry.KEY_NOTIFICATION_ENABLED, true)
}
if (dao.getConfigValue(UserConfigEntry.KEY_SOUND_ENABLED).isNullOrEmpty()) {
dao.setFeatureEnabled(UserConfigEntry.KEY_SOUND_ENABLED, true)
}
if (dao.getConfigValue(UserConfigEntry.KEY_VIBRATION_ENABLED).isNullOrEmpty()) {
dao.setFeatureEnabled(UserConfigEntry.KEY_VIBRATION_ENABLED, true)
}
}
}
// ==================== 测试方法 ====================
/**
* 插入测试数据
*/
suspend fun insertTestData() {
withContext(Dispatchers.IO) {
// 插入测试心情记录
val testRecords = listOf(
MoodRecord(
emotion = Emotion.HAPPY,
moodIntensity = 85,
timestamp = System.currentTimeMillis(),
textContent = "今天心情很好!",
imagePath = null
),
MoodRecord(
emotion = Emotion.SAD,
moodIntensity = 60,
timestamp = System.currentTimeMillis(),
textContent = "有点难过",
imagePath = null
),
MoodRecord(
emotion = Emotion.ANGRY,
moodIntensity = 90,
timestamp = System.currentTimeMillis(),
textContent = "很生气!",
imagePath = null
)
)
testRecords.forEach { record ->
database.moodRecordDao().insertMoodRecord(record)
}
// 设置测试配置
database.userConfigEntryDao().setUserNickname("测试用户")
database.userConfigEntryDao().setFeatureEnabled(UserConfigEntry.KEY_SOUND_ENABLED, true)
}
}
/**
* 清理测试数据
*/
suspend fun clearTestData() {
withContext(Dispatchers.IO) {
database.moodRecordDao().clearAllMoodRecords()
database.userConfigEntryDao().clearAllUserConfigs()
}
}
/**
* 关闭数据库连接
*/
fun close() {
AppDatabase.closeDatabase()
}
}

View File

@ -0,0 +1,249 @@
package com.chick_mood.data.database
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import com.chick_mood.data.model.UserConfigEntry
/**
* 用户配置键值对数据访问对象
*
* 提供用户配置的数据库操作接口包括配置的增删改查
* 支持流式数据更新和配置版本管理
*
* @author Claude
* @date 2025-10-22
*/
@Dao
interface UserConfigEntryDao {
/**
* 插入或更新用户配置
* 使用Replace策略如果配置已存在则更新不存在则插入
*
* @param userConfig 用户配置对象
* @return 受影响的行数
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrUpdateUserConfig(userConfig: UserConfigEntry): Long
/**
* 更新用户配置
*
* @param userConfig 要更新的用户配置
* @return 受影响的行数
*/
@Update
suspend fun updateUserConfig(userConfig: UserConfigEntry): Int
/**
* 根据配置键获取用户配置
*
* @param configKey 配置键
* @return 用户配置如果不存在则返回null
*/
@Query("SELECT * FROM user_config_entries WHERE config_key = :configKey")
suspend fun getUserConfig(configKey: String): UserConfigEntry?
/**
* 获取所有用户配置
*
* @return 所有用户配置的流式数据
*/
@Query("SELECT * FROM user_config_entries")
fun getAllUserConfigs(): Flow<List<UserConfigEntry>>
/**
* 获取用户昵称
*
* @return 用户昵称如果未设置则返回null
*/
@Query("SELECT config_value FROM user_config_entries WHERE config_key = :configKey")
suspend fun getConfigValue(configKey: String): String?
/**
* 设置配置值
*
* @param configKey 配置键
* @param configValue 配置值
* @param updatedAt 更新时间
*/
@Query("INSERT OR REPLACE INTO user_config_entries (config_key, config_value, updated_at) VALUES (:configKey, :configValue, :updatedAt)")
suspend fun setConfigValue(configKey: String, configValue: String, updatedAt: Long = System.currentTimeMillis())
// ==================== 便捷方法 ====================
/**
* 获取用户昵称
*/
suspend fun getUserNickname(): String? {
return getConfigValue(UserConfigEntry.KEY_USER_NICKNAME)
}
/**
* 设置用户昵称
*/
suspend fun setUserNickname(nickname: String) {
setConfigValue(UserConfigEntry.KEY_USER_NICKNAME, nickname)
}
/**
* 获取用户头像路径
*/
suspend fun getUserAvatar(): String? {
return getConfigValue(UserConfigEntry.KEY_USER_AVATAR)
}
/**
* 设置用户头像路径
*/
suspend fun setUserAvatar(avatarPath: String?) {
setConfigValue(UserConfigEntry.KEY_USER_AVATAR, avatarPath ?: "")
}
/**
* 获取应用版本号
*/
suspend fun getAppVersion(): String? {
return getConfigValue(UserConfigEntry.KEY_APP_VERSION)
}
/**
* 设置应用版本号
*/
suspend fun setAppVersion(version: String) {
setConfigValue(UserConfigEntry.KEY_APP_VERSION, version)
}
/**
* 获取首次使用时间
*/
suspend fun getFirstUseTime(): Long? {
return getConfigValue(UserConfigEntry.KEY_FIRST_USE_TIME)?.toLongOrNull()
}
/**
* 设置首次使用时间
*/
suspend fun setFirstUseTime(timestamp: Long) {
setConfigValue(UserConfigEntry.KEY_FIRST_USE_TIME, timestamp.toString())
}
/**
* 获取使用天数
*/
suspend fun getUsageDays(): Int? {
return getConfigValue(UserConfigEntry.KEY_USAGE_DAYS)?.toIntOrNull()
}
/**
* 增加使用天数
*/
suspend fun incrementUsageDays() {
val currentDays = getUsageDays() ?: 0
setConfigValue(UserConfigEntry.KEY_USAGE_DAYS, (currentDays + 1).toString())
}
/**
* 初始化使用天数
*/
suspend fun initializeUsageDays() {
setConfigValue(UserConfigEntry.KEY_USAGE_DAYS, "1")
}
/**
* 获取心情记录总数
*/
suspend fun getTotalMoodRecords(): Int? {
return getConfigValue(UserConfigEntry.KEY_TOTAL_MOOD_RECORDS)?.toIntOrNull()
}
/**
* 设置心情记录总数
*/
suspend fun setTotalMoodRecords(count: Int) {
setConfigValue(UserConfigEntry.KEY_TOTAL_MOOD_RECORDS, count.toString())
}
/**
* 增加心情记录计数
*/
suspend fun incrementMoodRecordCount() {
val currentCount = getTotalMoodRecords() ?: 0
setConfigValue(UserConfigEntry.KEY_TOTAL_MOOD_RECORDS, (currentCount + 1).toString())
}
/**
* 获取最后活跃时间
*/
suspend fun getLastActiveTime(): Long? {
return getConfigValue(UserConfigEntry.KEY_LAST_ACTIVE_TIME)?.toLongOrNull()
}
/**
* 更新最后活跃时间
*/
suspend fun updateLastActiveTime() {
setConfigValue(UserConfigEntry.KEY_LAST_ACTIVE_TIME, System.currentTimeMillis().toString())
}
/**
* 获取提醒设置
*/
suspend fun getReminderTime(reminderType: String): String? {
return getConfigValue(reminderType)
}
/**
* 设置提醒时间
*/
suspend fun setReminderTime(reminderType: String, time: String) {
setConfigValue(reminderType, time)
}
/**
* 获取是否启用了某项功能
*/
suspend fun isFeatureEnabled(featureKey: String): Boolean {
val value = getConfigValue(featureKey)
return value.equals("1", ignoreCase = true) || value.equals("true", ignoreCase = true)
}
/**
* 设置功能开关
*/
suspend fun setFeatureEnabled(featureKey: String, enabled: Boolean) {
setConfigValue(featureKey, if (enabled) "1" else "0")
}
// ==================== 批量操作 ====================
/**
* 批量插入配置
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertConfigs(configs: List<UserConfigEntry>): List<Long>
/**
* 删除指定配置
*/
@Query("DELETE FROM user_config_entries WHERE config_key = :configKey")
suspend fun deleteUserConfig(configKey: String): Int
/**
* 清除所有用户配置
*/
@Query("DELETE FROM user_config_entries")
suspend fun clearAllUserConfigs(): Int
/**
* 获取配置更新时间
*/
@Query("SELECT updated_at FROM user_config_entries WHERE config_key = :configKey")
suspend fun getConfigUpdateTime(configKey: String): Long?
/**
* 获取所有配置键
*/
@Query("SELECT DISTINCT config_key FROM user_config_entries")
suspend fun getAllConfigKeys(): List<String>
}

View File

@ -0,0 +1,151 @@
package com.chick_mood.data.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
/**
* 用户配置键值对数据模型
* 使用键值对方式存储用户的各种配置信息
* 支持灵活的配置管理和扩展
*
* @author Claude
* @date 2025-10-22
*/
@Entity(tableName = "user_config_entries")
data class UserConfigEntry(
/**
* 主键ID自动生成
*/
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
/**
* 配置键名
*/
@ColumnInfo(name = "config_key")
val configKey: String,
/**
* 配置值
*/
@ColumnInfo(name = "config_value")
val configValue: String,
/**
* 配置更新时间
*/
@ColumnInfo(name = "updated_at")
val updatedAt: Long = System.currentTimeMillis()
) {
companion object {
// 配置键常量
const val KEY_USER_NICKNAME = "user_nickname"
const val KEY_USER_AVATAR = "user_avatar"
const val KEY_APP_VERSION = "app_version"
const val KEY_FIRST_USE_TIME = "first_use_time"
const val KEY_USAGE_DAYS = "usage_days"
const val KEY_TOTAL_MOOD_RECORDS = "total_mood_records"
const val KEY_LAST_ACTIVE_TIME = "last_active_time"
const val KEY_MORNING_REMINDER = "morning_reminder"
const val KEY_EVENING_REMINDER = "evening_reminder"
const val KEY_NOTIFICATION_ENABLED = "notification_enabled"
const val KEY_SOUND_ENABLED = "sound_enabled"
const val KEY_VIBRATION_ENABLED = "vibration_enabled"
const val KEY_DARK_MODE = "dark_mode"
}
/**
* 检查配置是否有效
*/
fun isValid(): Boolean {
return configKey.isNotEmpty() &&
configValue.isNotEmpty() &&
updatedAt > 0
}
/**
* 检查是否为布尔类型配置
*/
fun isBooleanConfig(): Boolean {
return configKey.endsWith("_enabled") ||
configKey.startsWith("is_") ||
configKey.contains("flag")
}
/**
* 获取布尔值
*/
fun getBooleanValue(): Boolean {
return configValue.equals("1", ignoreCase = true) ||
configValue.equals("true", ignoreCase = true)
}
/**
* 检查是否为时间配置
*/
fun isTimeConfig(): Boolean {
return configKey.endsWith("_reminder") ||
configKey.endsWith("_time") ||
configValue.matches(Regex("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$"))
}
/**
* 检查是否为数字配置
*/
fun isNumericConfig(): Boolean {
return configKey.endsWith("_days") ||
configKey.endsWith("_count") ||
configKey.endsWith("_total") ||
configValue.matches(Regex("^\\d+$"))
}
/**
* 获取整数值
*/
fun getIntValue(): Int {
return try {
configValue.toInt()
} catch (e: NumberFormatException) {
0
}
}
/**
* 获取长整数值
*/
fun getLongValue(): Long {
return try {
configValue.toLong()
} catch (e: NumberFormatException) {
0L
}
}
/**
* 获取配置类型描述
*/
fun getConfigTypeDescription(): String {
return when {
isBooleanConfig() -> "布尔配置"
isTimeConfig() -> "时间配置"
isNumericConfig() -> "数字配置"
else -> "文本配置"
}
}
/**
* 获取格式化的配置值
*/
fun getFormattedValue(): String {
return when {
isBooleanConfig() -> if (getBooleanValue()) "启用" else "禁用"
isTimeConfig() -> configValue
isNumericConfig() -> configValue
configKey == KEY_USER_NICKNAME && configValue.isNotEmpty() -> configValue
configKey == KEY_APP_VERSION -> "v$configValue"
else -> configValue
}
}
}

View File

@ -1,9 +1,11 @@
package com.daodaoshi.chick_mood
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.daodaoshi.chick_mood.databinding.ActivityMainBinding
import com.chick_mood.data.database.DatabaseTestHelper
import java.text.SimpleDateFormat
import java.util.*
@ -39,6 +41,9 @@ class MainActivity : AppCompatActivity() {
// 设置初始时间显示(当前时间)
updateTimeIndicator(System.currentTimeMillis())
// 运行数据库测试
runDatabaseTest()
}
/**
@ -143,6 +148,18 @@ class MainActivity : AppCompatActivity() {
// 使用 BottomSheetDialog 实现
}
/**
* 运行数据库测试
*/
private fun runDatabaseTest() {
try {
Log.d("MainActivity", "开始运行数据库测试...")
DatabaseTestHelper.runDatabaseTests(this)
} catch (e: Exception) {
Log.e("MainActivity", "数据库测试运行失败", e)
}
}
/**
* 获取当前页面状态 - 用于测试
* @return true表示显示空状态false表示显示历史记录

View File

@ -0,0 +1,302 @@
package com.chick_mood.data.database
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import com.chick_mood.data.model.MoodRecord
import com.chick_mood.data.model.Emotion
import com.chick_mood.data.model.UserConfigEntry
import java.util.Date
/**
* 简化的数据库测试
*
* 测试基本的数据库功能和数据完整性
* 专注于验证Room数据库的核心功能
*
* @author Claude
* @date 2025-10-22
*/
@RunWith(AndroidJUnit4::class)
class SimpleDatabaseTest {
/**
* 测试数据库实例
*/
private lateinit var database: AppDatabase
/**
* 心情记录DAO
*/
private lateinit var moodRecordDao: MoodRecordDao
/**
* 用户配置DAO
*/
private lateinit var userConfigEntryDao: UserConfigEntryDao
/**
* 测试前准备
*/
@Before
fun createDb() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).allowMainThreadQueries().build()
moodRecordDao = database.moodRecordDao()
userConfigEntryDao = database.userConfigEntryDao()
}
/**
* 测试后清理
*/
@After
fun closeDb() {
database.close()
}
// ==================== 心情记录基础测试 ====================
/**
* 测试插入和查询心情记录
*/
@Test
fun testInsertAndGetMoodRecord() = runBlocking {
// 创建测试记录
val testRecord = MoodRecord(
emotion = Emotion.HAPPY,
intensity = 85,
noteText = "测试心情记录",
imagePaths = emptyList(),
timestamp = Date()
)
// 插入记录
val insertedId = moodRecordDao.insertMoodRecord(testRecord)
assertTrue("插入记录应该返回有效的ID", insertedId > 0)
// 查询记录
val retrievedRecord = moodRecordDao.getMoodRecordById(insertedId)
assertNotNull("应该能查询到插入的记录", retrievedRecord)
assertEquals("情绪类型应该正确", Emotion.HAPPY, retrievedRecord?.emotion)
assertEquals("强度值应该正确", 85, retrievedRecord?.intensity)
assertEquals("文字内容应该正确", "测试心情记录", retrievedRecord?.noteText)
}
/**
* 测试获取所有心情记录
*/
@Test
fun testGetAllMoodRecords() = runBlocking {
// 插入多条记录
val testRecords = listOf(
MoodRecord(emotion = Emotion.HAPPY, intensity = 85, noteText = "开心", imagePaths = emptyList(), timestamp = Date()),
MoodRecord(emotion = Emotion.SAD, intensity = 60, noteText = "难过", imagePaths = emptyList(), timestamp = Date()),
MoodRecord(emotion = Emotion.ANGRY, intensity = 90, noteText = "生气", imagePaths = emptyList(), timestamp = Date())
)
testRecords.forEach { record ->
moodRecordDao.insertMoodRecord(record)
}
// 验证记录总数
val totalCount = moodRecordDao.getMoodRecordCount()
assertEquals("应该有3条记录", 3, totalCount)
}
/**
* 测试删除心情记录
*/
@Test
fun testDeleteMoodRecord() = runBlocking {
// 插入记录
val testRecord = MoodRecord(emotion = Emotion.HAPPY, intensity = 85, noteText = "测试", imagePaths = emptyList(), timestamp = Date())
val insertedId = moodRecordDao.insertMoodRecord(testRecord)
assertEquals("插入后应该有1条记录", 1, moodRecordDao.getMoodRecordCount())
// 删除记录
val affectedRows = moodRecordDao.deleteMoodRecordById(insertedId)
assertEquals("应该影响1行", 1, affectedRows)
assertEquals("删除后应该没有记录", 0, moodRecordDao.getMoodRecordCount())
}
// ==================== 用户配置基础测试 ====================
/**
* 测试插入和查询用户配置
*/
@Test
fun testInsertAndGetUserConfig() = runBlocking {
// 创建配置
val testConfig = UserConfigEntry(
configKey = "test_key",
configValue = "test_value",
updatedAt = System.currentTimeMillis()
)
// 插入配置
val insertedId = userConfigEntryDao.insertOrUpdateUserConfig(testConfig)
assertTrue("插入配置应该返回有效的ID", insertedId > 0)
// 查询配置
val retrievedConfig = userConfigEntryDao.getUserConfig("test_key")
assertNotNull("应该能查询到配置", retrievedConfig)
assertEquals("配置键应该正确", "test_key", retrievedConfig?.configKey)
assertEquals("配置值应该正确", "test_value", retrievedConfig?.configValue)
}
/**
* 测试用户昵称设置和获取
*/
@Test
fun testUserNickname() = runBlocking {
// 设置昵称
userConfigEntryDao.setUserNickname("测试用户")
// 获取昵称
val nickname = userConfigEntryDao.getUserNickname()
assertEquals("昵称应该正确", "测试用户", nickname)
}
/**
* 测试配置值设置和获取
*/
@Test
fun testConfigValue() = runBlocking {
// 设置配置值
userConfigEntryDao.setConfigValue("test_key", "test_value")
// 获取配置值
val value = userConfigEntryDao.getConfigValue("test_key")
assertEquals("配置值应该正确", "test_value", value)
}
/**
* 测试功能开关
*/
@Test
fun testFeatureEnabled() = runBlocking {
// 启用功能
userConfigEntryDao.setFeatureEnabled("test_feature", true)
assertTrue("功能应该被启用", userConfigEntryDao.isFeatureEnabled("test_feature"))
// 禁用功能
userConfigEntryDao.setFeatureEnabled("test_feature", false)
assertFalse("功能应该被禁用", userConfigEntryDao.isFeatureEnabled("test_feature"))
}
// ==================== 类型转换测试 ====================
/**
* 测试日期类型转换
*/
@Test
fun testDateConversion() {
val converters = Converters()
val testDate = Date()
val testTimestamp = testDate.time
// 测试日期转时间戳
val timestamp = converters.toDate(testDate)
assertEquals("日期应该转换为正确的时间戳", testTimestamp, timestamp)
// 测试时间戳转日期
val date = converters.fromDate(testTimestamp)
assertEquals("时间戳应该转换为正确的日期", testTimestamp, date.time)
}
/**
* 测试枚举类型转换
*/
@Test
fun testEmotionConversion() {
val converters = Converters()
// 测试枚举转字符串
val emotionString = converters.fromEmotion(Emotion.HAPPY)
assertEquals("枚举应该转换为字符串名称", "HAPPY", emotionString)
// 测试字符串转枚举
val emotion = converters.toEmotion("HAPPY")
assertEquals("字符串应该转换为对应枚举", Emotion.HAPPY, emotion)
// 测试无效枚举值
val defaultEmotion = converters.toEmotion("INVALID_EMOTION")
assertEquals("无效枚举应该返回默认值", Emotion.HAPPY, defaultEmotion)
}
/**
* 测试布尔类型转换
*/
@Test
fun testBooleanConversion() {
val converters = Converters()
// 测试布尔值转整数
assertEquals("true应该转换为1", 1, converters.fromBoolean(true))
assertEquals("false应该转换为0", 0, converters.fromBoolean(false))
// 测试整数转布尔值
assertTrue("1应该转换为true", converters.toBoolean(1))
assertFalse("0应该转换为false", converters.toBoolean(0))
assertFalse("其他数字应该转换为false", converters.toBoolean(2))
}
// ==================== 数据完整性测试 ====================
/**
* 测试数据库版本
*/
@Test
fun testDatabaseVersion() {
assertEquals("数据库版本应该是2", 2, AppDatabase.DATABASE_VERSION)
assertEquals("数据库名称应该正确", "chick_mood_database", AppDatabase.DATABASE_NAME)
}
/**
* 测试空数据库状态
*/
@Test
fun testEmptyDatabaseState() = runBlocking {
// 初始状态应该没有记录
assertEquals("初始应该没有心情记录", 0, moodRecordDao.getMoodRecordCount())
// 查询不存在的记录应该返回null
assertNull("查询不存在的记录应该返回null", moodRecordDao.getMoodRecordById(999L))
// 查询不存在的配置应该返回null
assertNull("查询不存在的配置应该返回null", userConfigEntryDao.getUserConfig("nonexistent_key"))
}
/**
* 测试批量操作
*/
@Test
fun testBatchOperations() = runBlocking {
// 批量插入配置
val configs = listOf(
UserConfigEntry(configKey = "key1", configValue = "value1", updatedAt = System.currentTimeMillis()),
UserConfigEntry(configKey = "key2", configValue = "value2", updatedAt = System.currentTimeMillis()),
UserConfigEntry(configKey = "key3", configValue = "value3", updatedAt = System.currentTimeMillis())
)
val insertedIds = userConfigEntryDao.insertConfigs(configs)
assertEquals("应该插入3个配置", 3, insertedIds.size)
assertTrue("所有插入ID都应该有效", insertedIds.all { it > 0 })
// 验证所有配置都能查询到
for (config in configs) {
val retrieved = userConfigEntryDao.getUserConfig(config.configKey)
assertNotNull("应该能查询到配置: ${config.configKey}", retrieved)
assertEquals("配置值应该正确", config.configValue, retrieved?.configValue)
}
}
}

123
claude.md
View File

@ -317,26 +317,63 @@
### ✅ 已完成工作
- **模块1.1**: MainActivity基础布局结构已完成
- **模块1.2**: 数据模型定义MoodRecord, Emotion枚举UserConfig
- **Bug修复**: 语法错误、依赖冲突、主题配置问题(已修复)
- **项目架构**: 从Compose成功转换为View系统
- **测试覆盖**: 完整的单元测试和测试数据生成器
- **测试覆盖**: 完整的单元测试25个测试用例全部通过
- **Git初始化**: Git仓库初始化和版本控制配置
### 🔄 当前状态
- **编译状态**: ✅ 编译成功
- **安装状态**: ✅ 可安装到设备
- **运行状态**: 🔄 主题修复待验证需重启IDE
- **测试状态**: ✅ 25个单元测试全部通过
- **构建状态**: ✅ APK构建成功
- **安装状态**: ✅ 可安装到设备app-debug.apk
- **Git状态**: ✅ 仓库初始化完成,.gitignore配置完成
### 📋 下一步计划
1. **立即任务**: 验证主题修复效果,确保应用正常启动
2. **模块1.2**: 数据模型定义MoodRecord, Emotion枚举
3. **模块1.3**: Room数据库设计和实现
4. **模块1.4**: ViewModel和Repository架构
1. **立即任务**: 测试APK在设备上的运行情况
2. **模块1.3**: Room数据库设计和实现
3. **模块1.4**: ViewModel和Repository架构
4. **模块2.1**: 空白状态页面实现
### ⚠️ 待解决问题
- **文件锁定**: Gradle build目录锁定问题需手动重启IDE解决
- **模块名称**: 已修复settings.gradle.kts中的项目名称不一致问题
- **主题验证**: 修复后的主题需要在设备上验证
- **模块名称**: ✅ 已修复settings.gradle.kts中的项目名称不一致问题
- **主题验证**: ✅ 主题配置已修复并验证成功
- **图标资源**: 当前使用占位图标,需要设计师提供正式资源
- **Git提交**: 需要用户配置Git信息并提交第一个版本
### 🎯 项目文件结构
```
Chick_Mood/
├── app/
│ ├── src/main/
│ │ ├── java/com/chick_mood/
│ │ │ ├── data/model/
│ │ │ │ ├── Emotion.kt ✅
│ │ │ │ ├── MoodRecord.kt ✅
│ │ │ │ └── UserConfig.kt ✅
│ │ │ └── daodaoshi/chick_mood/
│ │ │ └── MainActivity.kt ✅
│ │ ├── res/
│ │ │ ├── layout/
│ │ │ │ └── activity_main.xml ✅
│ │ │ ├── values/
│ │ │ │ ├── colors.xml ✅
│ │ │ │ ├── strings.xml ✅
│ │ │ │ └── themes.xml ✅
│ │ │ └── drawable/ ✅
│ │ └── test/
│ │ └── java/com/chick_mood/data/model/
│ │ ├── EmotionTest.kt ✅
│ │ ├── MoodRecordTest.kt ✅
│ │ ├── UserConfigTest.kt ✅
│ │ └── TestDataGenerator.kt ✅
│ └── build.gradle.kts ✅
├── build.gradle.kts ✅
├── settings.gradle.kts ✅
├── .gitignore ✅
└── CLAUDE.md ✅
```
**2025-10-22 (Gradle构建错误修复):**
- 🐛 **问题**: Module entity with name: Chick_Mood should be available
@ -435,6 +472,72 @@ class ClassName {
- ✅ 实现ViewBinding和基础组件初始化
- ✅ 添加完整的单元测试覆盖
**2025-10-22 (模块1.2开发完成):**
- ✅ **Emotion.kt**: 六种情绪枚举定义(开心、生气、悲伤、烦恼、孤单、害怕)
- 支持情绪显示名称、颜色值、小鸡表情映射
- 提供颜色资源ID获取、数值转换等方法
- ✅ **MoodRecord.kt**: 心情记录数据模型
- 包含情绪类型、强度值、时间戳、文字内容、图片路径
- 支持心情强度描述(轻微/一般/强烈/极度)和表情符号
- 提供时间格式化、内容检查、边界值处理等实用方法
- ✅ **UserConfig.kt**: 用户配置数据模型
- 存储用户昵称、头像、应用设置、使用统计等
- 支持提醒时间格式化、配置验证、使用天数计算
- ✅ **完整测试覆盖**:
- EmotionTest: 7个测试用例验证情绪枚举功能
- MoodRecordTest: 8个测试用例验证心情记录功能
- UserConfigTest: 9个测试用例验证用户配置功能
- TestDataGenerator: 完整的测试数据生成工具,支持多种场景
- ✅ **测试结果**: 25个单元测试全部通过100%成功率
- ✅ **代码质量**: 遵循开发规范包含完整的KDoc注释和错误处理
**2025-10-22 (Git初始化完成):**
- ✅ 初始化Git仓库 (`git init`)
- ✅ 配置完整的.gitignore文件排除构建文件、IDE文件、临时文件等
- ✅ 准备提交第一个稳定版本
**Git提交准备:**
```bash
# 配置Git用户信息需要用户手动配置
git config user.name "Your Name"
git config user.email "your.email@example.com"
# 添加所有文件到暂存区
git add .
# 提交第一个版本
git commit -m "$(cat <<'EOF'
feat: 实现别摇小鸡App基础架构和数据模型(V1.0 MVP)
## 🎯 核心功能
- ✅ 六种基础情绪支持(开心、生气、悲伤、烦恼、孤单、害怕)
- ✅ 完整的心情记录数据模型(情绪、强度、时间、文字、图片)
- ✅ 用户配置管理(昵称、头像、设置、统计)
## 🏗️ 技术架构
- ✅ Android原生View系统架构
- ✅ MainActivity基础布局结构
- ✅ ViewBinding和Material Design主题
- ✅ 完整的单元测试覆盖25个测试用例
## 📦 依赖库
- Room数据库准备中
- ViewPager2历史记录滑动
- LiveData & ViewModel
- Material Design组件
## 🧪 测试
- ✅ 25个单元测试全部通过
- ✅ 数据模型完整测试覆盖
- ✅ 测试数据生成器
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
```
**2025-10-22 (Bug修复记录):**
- 🐛 **问题1**: MainActivity语法错误时间格式化缺少右括号
- ✅ 修复:修正`timeFormatter.format(Date(timestamp))`语法