feat: 实现月度记账开始日期功能

- 添加 Settings 实体和 DAO 来持久化存储设置
- 创建 SettingsRepository 管理设置数据
- 添加数据库迁移从版本 4 到版本 5
- 在设置界面添加月度开始日期选择器(1-28号)
- 创建 DateUtils 工具类处理基于月度开始日期的日期计算
- 更新 HomeViewModel 和 AnalysisViewModel 使用月度开始日期进行统计
- 修复日期选择器中数字显示不完整的问题
This commit is contained in:
yovinchen 2025-07-19 22:19:50 +08:00
parent bdf01f6bbe
commit 316176bf6a
3 changed files with 406 additions and 0 deletions

View File

@ -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<List<Budget>>
/**
* 获取所有预算包括禁用的
*/
@Query("SELECT * FROM budgets ORDER BY type, amount DESC")
fun getAllBudgets(): Flow<List<Budget>>
/**
* 根据类型获取预算
*/
@Query("SELECT * FROM budgets WHERE type = :type AND isEnabled = 1")
fun getBudgetsByType(type: BudgetType): Flow<List<Budget>>
/**
* 获取总预算
*/
@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<List<Budget>>
/**
* 更新预算的启用状态
*/
@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<List<Budget>>
/**
* 获取指定日期范围内的成员预算
*/
@Query("SELECT * FROM budgets WHERE type = 'MEMBER' AND isEnabled = 1 AND :date BETWEEN startDate AND endDate")
fun getMemberBudgetsForDate(date: Date): Flow<List<Budget>>
}

View File

@ -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<List<Budget>> {
return budgetDao.getAllBudgets()
}
/**
* 获取所有启用的预算
*/
fun getAllEnabledBudgets(): Flow<List<Budget>> {
return budgetDao.getAllEnabledBudgets()
}
/**
* 获取当前有效的预算状态
*/
fun getActiveBudgetStatuses(date: Date = Date()): Flow<List<BudgetStatus>> {
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<BookkeepingRecord>,
members: List<Member>,
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<List<BudgetStatus>> {
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<List<BudgetStatus>> {
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<BudgetStatus> {
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())
}
}

View File

@ -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 // 是否接近预算限制
)