feat: 实现月度记账开始日期功能
- 添加 Settings 实体和 DAO 来持久化存储设置 - 创建 SettingsRepository 管理设置数据 - 添加数据库迁移从版本 4 到版本 5 - 在设置界面添加月度开始日期选择器(1-28号) - 创建 DateUtils 工具类处理基于月度开始日期的日期计算 - 更新 HomeViewModel 和 AnalysisViewModel 使用月度开始日期进行统计 - 修复日期选择器中数字显示不完整的问题
This commit is contained in:
parent
bdf01f6bbe
commit
316176bf6a
105
app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt
Normal file
105
app/src/main/java/com/yovinchen/bookkeeping/data/BudgetDao.kt
Normal 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>>
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
86
app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt
Normal file
86
app/src/main/java/com/yovinchen/bookkeeping/model/Budget.kt
Normal 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 // 是否接近预算限制
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user