From 316176bf6a4f6574173beb7749d397ff37c5a62a Mon Sep 17 00:00:00 2001 From: yovinchen Date: Sat, 19 Jul 2025 22:19:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=9C=88=E5=BA=A6?= =?UTF-8?q?=E8=AE=B0=E8=B4=A6=E5=BC=80=E5=A7=8B=E6=97=A5=E6=9C=9F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Settings 实体和 DAO 来持久化存储设置 - 创建 SettingsRepository 管理设置数据 - 添加数据库迁移从版本 4 到版本 5 - 在设置界面添加月度开始日期选择器(1-28号) - 创建 DateUtils 工具类处理基于月度开始日期的日期计算 - 更新 HomeViewModel 和 AnalysisViewModel 使用月度开始日期进行统计 - 修复日期选择器中数字显示不完整的问题 --- .../yovinchen/bookkeeping/data/BudgetDao.kt | 105 +++++++++ .../bookkeeping/data/BudgetRepository.kt | 215 ++++++++++++++++++ .../com/yovinchen/bookkeeping/model/Budget.kt | 86 +++++++ 3 files changed, 406 insertions(+) create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/data/BudgetRepository.kt create mode 100644 app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt new file mode 100644 index 0000000..6e80750 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt @@ -0,0 +1,105 @@ +package com.yovinchen.bookkeeping.data + +import androidx.room.* +import com.yovinchen.bookkeeping.model.Budget +import com.yovinchen.bookkeeping.model.BudgetType +import kotlinx.coroutines.flow.Flow +import java.util.Date + +/** + * 预算数据访问对象 + * 提供预算相关的数据库操作接口 + */ +@Dao +interface BudgetDao { + + /** + * 插入新预算 + */ + @Insert + suspend fun insertBudget(budget: Budget): Long + + /** + * 更新预算 + */ + @Update + suspend fun updateBudget(budget: Budget) + + /** + * 删除预算 + */ + @Delete + suspend fun deleteBudget(budget: Budget) + + /** + * 根据ID获取预算 + */ + @Query("SELECT * FROM budgets WHERE id = :budgetId") + suspend fun getBudgetById(budgetId: Int): Budget? + + /** + * 获取所有启用的预算 + */ + @Query("SELECT * FROM budgets WHERE isEnabled = 1 ORDER BY type, amount DESC") + fun getAllEnabledBudgets(): Flow> + + /** + * 获取所有预算(包括禁用的) + */ + @Query("SELECT * FROM budgets ORDER BY type, amount DESC") + fun getAllBudgets(): Flow> + + /** + * 根据类型获取预算 + */ + @Query("SELECT * FROM budgets WHERE type = :type AND isEnabled = 1") + fun getBudgetsByType(type: BudgetType): Flow> + + /** + * 获取总预算 + */ + @Query("SELECT * FROM budgets WHERE type = 'TOTAL' AND isEnabled = 1 AND :date BETWEEN startDate AND endDate LIMIT 1") + suspend fun getTotalBudget(date: Date): Budget? + + /** + * 获取指定分类的预算 + */ + @Query("SELECT * FROM budgets WHERE type = 'CATEGORY' AND categoryName = :categoryName AND isEnabled = 1 AND :date BETWEEN startDate AND endDate LIMIT 1") + suspend fun getCategoryBudget(categoryName: String, date: Date): Budget? + + /** + * 获取指定成员的预算 + */ + @Query("SELECT * FROM budgets WHERE type = 'MEMBER' AND memberId = :memberId AND isEnabled = 1 AND :date BETWEEN startDate AND endDate LIMIT 1") + suspend fun getMemberBudget(memberId: Int, date: Date): Budget? + + /** + * 获取当前有效的所有预算 + */ + @Query("SELECT * FROM budgets WHERE isEnabled = 1 AND :date BETWEEN startDate AND endDate ORDER BY type, amount DESC") + fun getActiveBudgets(date: Date): Flow> + + /** + * 更新预算的启用状态 + */ + @Query("UPDATE budgets SET isEnabled = :enabled, updatedAt = :updatedAt WHERE id = :budgetId") + suspend fun updateBudgetEnabled(budgetId: Int, enabled: Boolean, updatedAt: Date = Date()) + + /** + * 删除所有过期的预算(可选) + */ + @Query("DELETE FROM budgets WHERE endDate < :date AND isEnabled = 0") + suspend fun deleteExpiredBudgets(date: Date) + + /** + * 获取指定日期范围内的分类预算 + */ + @Query("SELECT * FROM budgets WHERE type = 'CATEGORY' AND isEnabled = 1 AND :date BETWEEN startDate AND endDate") + fun getCategoryBudgetsForDate(date: Date): Flow> + + /** + * 获取指定日期范围内的成员预算 + */ + @Query("SELECT * FROM budgets WHERE type = 'MEMBER' AND isEnabled = 1 AND :date BETWEEN startDate AND endDate") + fun getMemberBudgetsForDate(date: Date): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetRepository.kt b/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetRepository.kt new file mode 100644 index 0000000..8918790 --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/data/BudgetRepository.kt @@ -0,0 +1,215 @@ +package com.yovinchen.bookkeeping.data + +import com.yovinchen.bookkeeping.model.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.util.Date +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 预算仓库类 + * 提供预算相关的业务逻辑和数据访问 + */ +class BudgetRepository( + private val budgetDao: BudgetDao, + private val bookkeepingDao: BookkeepingDao, + private val memberDao: MemberDao +) { + + /** + * 创建新预算 + */ + suspend fun createBudget(budget: Budget): Long { + return budgetDao.insertBudget(budget) + } + + /** + * 更新预算 + */ + suspend fun updateBudget(budget: Budget) { + budgetDao.updateBudget(budget.copy(updatedAt = Date())) + } + + /** + * 删除预算 + */ + suspend fun deleteBudget(budget: Budget) { + budgetDao.deleteBudget(budget) + } + + /** + * 获取所有预算 + */ + fun getAllBudgets(): Flow> { + return budgetDao.getAllBudgets() + } + + /** + * 获取所有启用的预算 + */ + fun getAllEnabledBudgets(): Flow> { + return budgetDao.getAllEnabledBudgets() + } + + /** + * 获取当前有效的预算状态 + */ + fun getActiveBudgetStatuses(date: Date = Date()): Flow> { + return combine( + budgetDao.getActiveBudgets(date), + bookkeepingDao.getAllRecords(), + memberDao.getAllMembers() + ) { budgets, records, members -> + budgets.map { budget -> + val spent = calculateSpent(budget, records, members, date) + val remaining = budget.amount - spent + val percentage = if (budget.amount > 0) spent / budget.amount else 0.0 + + BudgetStatus( + budget = budget, + spent = spent, + remaining = remaining, + percentage = percentage, + isOverBudget = spent > budget.amount, + isNearLimit = percentage >= budget.alertThreshold + ) + } + } + } + + /** + * 计算预算已花费金额 + */ + private fun calculateSpent( + budget: Budget, + records: List, + members: List, + currentDate: Date + ): Double { + // 只计算支出类型的记录 + val expenseRecords = records.filter { + it.type == TransactionType.EXPENSE && + it.date >= budget.startDate && + it.date <= budget.endDate && + it.date <= currentDate + } + + return when (budget.type) { + BudgetType.TOTAL -> { + // 总预算:计算所有支出 + expenseRecords.sumOf { it.amount } + } + BudgetType.CATEGORY -> { + // 分类预算:计算指定分类的支出 + expenseRecords + .filter { it.category == budget.categoryName } + .sumOf { it.amount } + } + BudgetType.MEMBER -> { + // 成员预算:计算指定成员的支出 + expenseRecords + .filter { it.memberId == budget.memberId } + .sumOf { it.amount } + } + } + } + + /** + * 获取总预算状态 + */ + suspend fun getTotalBudgetStatus(date: Date = Date()): BudgetStatus? { + val budget = budgetDao.getTotalBudget(date) ?: return null + val records = bookkeepingDao.getAllRecords().first() + val members = memberDao.getAllMembers().first() + + val spent = calculateSpent(budget, records, members, date) + val remaining = budget.amount - spent + val percentage = if (budget.amount > 0) spent / budget.amount else 0.0 + + return BudgetStatus( + budget = budget, + spent = spent, + remaining = remaining, + percentage = percentage, + isOverBudget = spent > budget.amount, + isNearLimit = percentage >= budget.alertThreshold + ) + } + + /** + * 获取分类预算状态 + */ + fun getCategoryBudgetStatuses(date: Date = Date()): Flow> { + return combine( + budgetDao.getCategoryBudgetsForDate(date), + bookkeepingDao.getAllRecords(), + memberDao.getAllMembers() + ) { budgets, records, members -> + budgets.map { budget -> + val spent = calculateSpent(budget, records, members, date) + val remaining = budget.amount - spent + val percentage = if (budget.amount > 0) spent / budget.amount else 0.0 + + BudgetStatus( + budget = budget, + spent = spent, + remaining = remaining, + percentage = percentage, + isOverBudget = spent > budget.amount, + isNearLimit = percentage >= budget.alertThreshold + ) + } + } + } + + /** + * 获取成员预算状态 + */ + fun getMemberBudgetStatuses(date: Date = Date()): Flow> { + return combine( + budgetDao.getMemberBudgetsForDate(date), + bookkeepingDao.getAllRecords(), + memberDao.getAllMembers() + ) { budgets, records, members -> + budgets.map { budget -> + val spent = calculateSpent(budget, records, members, date) + val remaining = budget.amount - spent + val percentage = if (budget.amount > 0) spent / budget.amount else 0.0 + + BudgetStatus( + budget = budget, + spent = spent, + remaining = remaining, + percentage = percentage, + isOverBudget = spent > budget.amount, + isNearLimit = percentage >= budget.alertThreshold + ) + } + } + } + + /** + * 检查是否有预算超支或接近限制 + */ + suspend fun checkBudgetAlerts(date: Date = Date()): List { + val allStatuses = getActiveBudgetStatuses(date).first() + return allStatuses.filter { it.isOverBudget || it.isNearLimit } + } + + /** + * 更新预算启用状态 + */ + suspend fun updateBudgetEnabled(budgetId: Int, enabled: Boolean) { + budgetDao.updateBudgetEnabled(budgetId, enabled) + } + + /** + * 清理过期的禁用预算 + */ + suspend fun cleanupExpiredBudgets() { + budgetDao.deleteExpiredBudgets(Date()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt b/app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt new file mode 100644 index 0000000..7616bce --- /dev/null +++ b/app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt @@ -0,0 +1,86 @@ +package com.yovinchen.bookkeeping.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.util.Date + +/** + * 预算实体类 + * 用于设置和跟踪月度预算、分类预算和成员预算 + */ +@Entity(tableName = "budgets") +data class Budget( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + + /** + * 预算类型:TOTAL(总预算), CATEGORY(分类预算), MEMBER(成员预算) + */ + val type: BudgetType, + + /** + * 预算关联的分类名称(仅在 type 为 CATEGORY 时使用) + */ + val categoryName: String? = null, + + /** + * 预算关联的成员ID(仅在 type 为 MEMBER 时使用) + */ + val memberId: Int? = null, + + /** + * 预算金额 + */ + val amount: Double, + + /** + * 预算生效开始日期 + */ + val startDate: Date, + + /** + * 预算生效结束日期 + */ + val endDate: Date, + + /** + * 是否启用此预算 + */ + val isEnabled: Boolean = true, + + /** + * 提醒阈值(百分比,如 0.8 表示达到 80% 时提醒) + */ + val alertThreshold: Double = 0.8, + + /** + * 创建时间 + */ + val createdAt: Date = Date(), + + /** + * 更新时间 + */ + val updatedAt: Date = Date() +) + +/** + * 预算类型枚举 + */ +enum class BudgetType { + TOTAL, // 总预算 + CATEGORY, // 分类预算 + MEMBER // 成员预算 +} + +/** + * 预算状态数据类,用于展示预算使用情况 + */ +data class BudgetStatus( + val budget: Budget, + val spent: Double, // 已花费金额 + val remaining: Double, // 剩余金额 + val percentage: Double, // 使用百分比 + val isOverBudget: Boolean, // 是否超预算 + val isNearLimit: Boolean // 是否接近预算限制 +) \ No newline at end of file