This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,587 @@
package service
import (
"errors"
"fmt"
"accounting-app/internal/models"
"accounting-app/internal/repository"
"gorm.io/gorm"
)
// 分配规则服务层错误定义
var (
ErrAllocationRuleNotFound = errors.New("分配规则不存在")
ErrAllocationRuleInUse = errors.New("分配规则正在使用中,无法删除")
ErrInvalidTriggerType = errors.New("无效的触发类型")
ErrInvalidTargetType = errors.New("无效的目标类型")
ErrInvalidAllocationPercentage = errors.New("分配百分比必须在0-100之间")
ErrInvalidAllocationAmount = errors.New("分配金额必须为正数")
ErrInvalidAllocationTarget = errors.New("分配目标必须有百分比或固定金额")
ErrTotalPercentageExceeds100 = errors.New("分配百分比总和超过100%")
ErrTargetNotFound = errors.New("目标账户或存钱罐不存在")
ErrInsufficientAmount = errors.New("分配金额不足")
)
// AllocationRuleInput 创建或更新分配规则的输入数据
type AllocationRuleInput struct {
UserID uint `json:"user_id"`
Name string `json:"name" binding:"required"`
TriggerType models.TriggerType `json:"trigger_type" binding:"required"`
SourceAccountID *uint `json:"source_account_id,omitempty"` // 触发分配的源账户
IsActive bool `json:"is_active"`
Targets []AllocationTargetInput `json:"targets" binding:"required,min=1"`
}
// AllocationTargetInput 分配目标的输入数据
type AllocationTargetInput struct {
TargetType models.TargetType `json:"target_type" binding:"required"`
TargetID uint `json:"target_id" binding:"required"`
Percentage *float64 `json:"percentage,omitempty"`
FixedAmount *float64 `json:"fixed_amount,omitempty"`
}
// AllocationResult 应用分配规则的结果
type AllocationResult struct {
RuleID uint `json:"rule_id"`
RuleName string `json:"rule_name"`
TotalAmount float64 `json:"total_amount"`
AllocatedAmount float64 `json:"allocated_amount"`
Remaining float64 `json:"remaining"`
Allocations []AllocationDetail `json:"allocations"`
}
// AllocationDetail 单个分配目标的详情
type AllocationDetail struct {
TargetType models.TargetType `json:"target_type"`
TargetID uint `json:"target_id"`
TargetName string `json:"target_name"`
Amount float64 `json:"amount"`
Percentage *float64 `json:"percentage,omitempty"`
FixedAmount *float64 `json:"fixed_amount,omitempty"`
}
// ApplyAllocationInput 应用分配规则的输入数据
type ApplyAllocationInput struct {
Amount float64 `json:"amount" binding:"required,gt=0"`
FromAccountID *uint `json:"from_account_id,omitempty"`
Note string `json:"note,omitempty"`
}
// AllocationRuleService 分配规则业务逻辑服务
type AllocationRuleService struct {
repo *repository.AllocationRuleRepository
recordRepo *repository.AllocationRecordRepository
accountRepo *repository.AccountRepository
piggyBankRepo *repository.PiggyBankRepository
db *gorm.DB
}
// NewAllocationRuleService 创建分配规则服务实例
func NewAllocationRuleService(
repo *repository.AllocationRuleRepository,
recordRepo *repository.AllocationRecordRepository,
accountRepo *repository.AccountRepository,
piggyBankRepo *repository.PiggyBankRepository,
db *gorm.DB,
) *AllocationRuleService {
return &AllocationRuleService{
repo: repo,
recordRepo: recordRepo,
accountRepo: accountRepo,
piggyBankRepo: piggyBankRepo,
db: db,
}
}
// CreateAllocationRule 创建新的分配规则(带业务逻辑验证)
func (s *AllocationRuleService) CreateAllocationRule(input AllocationRuleInput) (*models.AllocationRule, error) {
// 验证触发类型
if !isValidTriggerType(input.TriggerType) {
return nil, ErrInvalidTriggerType
}
// 验证分配目标
if err := s.validateTargets(input.UserID, input.Targets); err != nil {
return nil, err
}
// 创建分配规则模型
rule := &models.AllocationRule{
UserID: input.UserID,
Name: input.Name,
TriggerType: input.TriggerType,
SourceAccountID: input.SourceAccountID,
IsActive: input.IsActive,
}
// 开始数据库事务
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 保存规则到数据库
if err := tx.Create(rule).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建分配规则失败: %w", err)
}
// 创建分配目标
for _, targetInput := range input.Targets {
target := &models.AllocationTarget{
RuleID: rule.ID,
TargetType: targetInput.TargetType,
TargetID: targetInput.TargetID,
Percentage: targetInput.Percentage,
FixedAmount: targetInput.FixedAmount,
}
if err := tx.Create(target).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建分配目标失败: %w", err)
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("提交事务失败: %w", err)
}
// 重新加载规则(包含目标)
// Re-fetch the rule to include targets
var err error
rule, err = s.repo.GetByID(input.UserID, rule.ID)
if err != nil {
return nil, fmt.Errorf("重新加载分配规则失败: %w", err)
}
return rule, nil
}
// GetAllocationRule 根据ID获取分配规则
func (s *AllocationRuleService) GetAllocationRule(userID, id uint) (*models.AllocationRule, error) {
rule, err := s.repo.GetByID(userID, id)
if err != nil {
if errors.Is(err, repository.ErrAllocationRuleNotFound) {
return nil, ErrAllocationRuleNotFound
}
return nil, fmt.Errorf("获取分配规则失败: %w", err)
}
// userID check handled by repo
return rule, nil
}
// GetAllAllocationRules 获取所有分配规则
func (s *AllocationRuleService) GetAllAllocationRules(userID uint) ([]models.AllocationRule, error) {
rules, err := s.repo.GetAll(userID)
if err != nil {
return nil, fmt.Errorf("获取分配规则列表失败: %w", err)
}
return rules, nil
}
// GetActiveAllocationRules 获取所有启用的分配规则
func (s *AllocationRuleService) GetActiveAllocationRules(userID uint) ([]models.AllocationRule, error) {
rules, err := s.repo.GetActive(userID)
if err != nil {
return nil, fmt.Errorf("获取启用的分配规则失败: %w", err)
}
return rules, nil
}
// UpdateAllocationRule 更新现有的分配规则
func (s *AllocationRuleService) UpdateAllocationRule(userID, id uint, input AllocationRuleInput) (*models.AllocationRule, error) {
// 获取现有规则
rule, err := s.repo.GetByID(userID, id)
if err != nil {
if errors.Is(err, repository.ErrAllocationRuleNotFound) {
return nil, ErrAllocationRuleNotFound
}
return nil, fmt.Errorf("获取分配规则失败: %w", err)
}
// userID check handled by repo
// 验证触发类型
if !isValidTriggerType(input.TriggerType) {
return nil, ErrInvalidTriggerType
}
// 验证分配目标
if err := s.validateTargets(userID, input.Targets); err != nil {
return nil, err
}
// 开始数据库事务
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 更新规则字段
rule.Name = input.Name
rule.TriggerType = input.TriggerType
rule.SourceAccountID = input.SourceAccountID
rule.IsActive = input.IsActive
// 保存规则
if err := tx.Save(rule).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("更新分配规则失败: %w", err)
}
// 删除现有目标
if err := tx.Where("rule_id = ?", id).Delete(&models.AllocationTarget{}).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("删除现有目标失败: %w", err)
}
// 创建新目标
for _, targetInput := range input.Targets {
target := &models.AllocationTarget{
RuleID: rule.ID,
TargetType: targetInput.TargetType,
TargetID: targetInput.TargetID,
Percentage: targetInput.Percentage,
FixedAmount: targetInput.FixedAmount,
}
if err := tx.Create(target).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建分配目标失败: %w", err)
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("提交事务失败: %w", err)
}
// 重新加载规则(包含目标)
rule, err = s.repo.GetByID(userID, rule.ID)
if err != nil {
return nil, fmt.Errorf("重新加载分配规则失败: %w", err)
}
return rule, nil
}
// DeleteAllocationRule 根据ID删除分配规则
func (s *AllocationRuleService) DeleteAllocationRule(userID, id uint) error {
_, err := s.repo.GetByID(userID, id)
if err != nil {
if errors.Is(err, repository.ErrAllocationRuleNotFound) {
return ErrAllocationRuleNotFound
}
return err
}
// userID check handled by repo
err = s.repo.Delete(userID, id)
if err != nil {
if errors.Is(err, repository.ErrAllocationRuleNotFound) {
return ErrAllocationRuleNotFound
}
if errors.Is(err, repository.ErrAllocationRuleInUse) {
return ErrAllocationRuleInUse
}
return fmt.Errorf("删除分配规则失败: %w", err)
}
return nil
}
// ApplyAllocationRule 应用分配规则到指定金额
// 根据规则的目标分配金额
func (s *AllocationRuleService) ApplyAllocationRule(userID, id uint, input ApplyAllocationInput) (*AllocationResult, error) {
// 验证金额
if input.Amount <= 0 {
return nil, ErrInsufficientAmount
}
// 获取分配规则
rule, err := s.repo.GetByID(userID, id)
if err != nil {
if errors.Is(err, repository.ErrAllocationRuleNotFound) {
return nil, ErrAllocationRuleNotFound
}
return nil, fmt.Errorf("获取分配规则失败: %w", err)
}
// userID check handled by repo
// 检查规则是否启用
if !rule.IsActive {
return nil, errors.New("分配规则未启用")
}
// 开始数据库事务
tx := s.db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 如果提供了源账户ID验证账户是否存在且余额充足
if input.FromAccountID != nil {
var account models.Account
if err := tx.First(&account, *input.FromAccountID).Error; err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAccountNotFound
}
return nil, fmt.Errorf("获取账户失败: %w", err)
}
// 检查账户余额是否充足(非信用账户)
if !account.IsCredit && account.Balance < input.Amount {
tx.Rollback()
return nil, ErrInsufficientBalance
}
// 从源账户扣除金额
account.Balance -= input.Amount
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("更新源账户余额失败: %w", err)
}
}
// 计算分配
result := &AllocationResult{
RuleID: rule.ID,
RuleName: rule.Name,
TotalAmount: input.Amount,
Allocations: []AllocationDetail{},
}
totalAllocated := 0.0
// 处理每个目标
for _, target := range rule.Targets {
var allocatedAmount float64
// 计算分配金额
if target.Percentage != nil {
allocatedAmount = input.Amount * (*target.Percentage / 100.0)
} else if target.FixedAmount != nil {
allocatedAmount = *target.FixedAmount
} else {
tx.Rollback()
return nil, ErrInvalidAllocationTarget
}
// 四舍五入到2位小数
allocatedAmount = float64(int(allocatedAmount*100+0.5)) / 100
// 获取目标名称
targetName := ""
// 根据目标类型执行分配
switch target.TargetType {
case models.TargetTypeAccount:
var account models.Account
if err := tx.First(&account, target.TargetID).Error; err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTargetNotFound
}
return nil, fmt.Errorf("获取目标账户失败: %w", err)
}
targetName = account.Name
// 增加目标账户余额
account.Balance += allocatedAmount
if err := tx.Save(&account).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("更新目标账户余额失败: %w", err)
}
case models.TargetTypePiggyBank:
var piggyBank models.PiggyBank
if err := tx.First(&piggyBank, target.TargetID).Error; err != nil {
tx.Rollback()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTargetNotFound
}
return nil, fmt.Errorf("获取目标存钱罐失败: %w", err)
}
targetName = piggyBank.Name
// 增加存钱罐金额
piggyBank.CurrentAmount += allocatedAmount
if err := tx.Save(&piggyBank).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("更新存钱罐余额失败: %w", err)
}
default:
tx.Rollback()
return nil, ErrInvalidTargetType
}
// 添加到结果
result.Allocations = append(result.Allocations, AllocationDetail{
TargetType: target.TargetType,
TargetID: target.TargetID,
TargetName: targetName,
Amount: allocatedAmount,
Percentage: target.Percentage,
FixedAmount: target.FixedAmount,
})
totalAllocated += allocatedAmount
}
result.AllocatedAmount = totalAllocated
result.Remaining = input.Amount - totalAllocated
// 确定分配记录的源账户ID
var sourceAccountID uint
if input.FromAccountID != nil {
sourceAccountID = *input.FromAccountID
} else {
// 如果未指定源账户使用0或适当处理
// 正常流程中不应该发生这种情况,但需要处理
sourceAccountID = 0
}
// 保存分配记录
allocationRecord := &models.AllocationRecord{
UserID: userID,
RuleID: rule.ID,
RuleName: rule.Name,
SourceAccountID: sourceAccountID,
TotalAmount: input.Amount,
AllocatedAmount: totalAllocated,
RemainingAmount: result.Remaining,
Note: input.Note,
}
if err := tx.Create(allocationRecord).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建分配记录失败: %w", err)
}
// 保存分配记录详情
for _, allocation := range result.Allocations {
detail := &models.AllocationRecordDetail{
RecordID: allocationRecord.ID,
TargetType: allocation.TargetType,
TargetID: allocation.TargetID,
TargetName: allocation.TargetName,
Amount: allocation.Amount,
Percentage: allocation.Percentage,
FixedAmount: allocation.FixedAmount,
}
if err := tx.Create(detail).Error; err != nil {
tx.Rollback()
return nil, fmt.Errorf("创建分配记录详情失败: %w", err)
}
}
// 提交事务
if err := tx.Commit().Error; err != nil {
return nil, fmt.Errorf("提交事务失败: %w", err)
}
return result, nil
}
// SuggestAllocationForIncome 为指定收入金额和账户建议分配规则
// 返回所有匹配源账户的已启用收入触发分配规则
func (s *AllocationRuleService) SuggestAllocationForIncome(userID uint, amount float64, accountID uint) ([]models.AllocationRule, error) {
rules, err := s.repo.GetActiveByTriggerTypeAndAccount(userID, models.TriggerTypeIncome, accountID)
if err != nil {
return nil, fmt.Errorf("获取收入分配规则失败: %w", err)
}
return rules, nil
}
// validateTargets 验证分配目标
func (s *AllocationRuleService) validateTargets(userID uint, targets []AllocationTargetInput) error {
if len(targets) == 0 {
return errors.New("至少需要一个分配目标")
}
totalPercentage := 0.0
for _, target := range targets {
// 验证目标类型
if !isValidTargetType(target.TargetType) {
return ErrInvalidTargetType
}
// 验证目标必须有百分比或固定金额,但不能同时有
if target.Percentage == nil && target.FixedAmount == nil {
return ErrInvalidAllocationTarget
}
if target.Percentage != nil && target.FixedAmount != nil {
return errors.New("分配目标不能同时有百分比和固定金额")
}
// 验证百分比
if target.Percentage != nil {
if *target.Percentage < 0 || *target.Percentage > 100 {
return ErrInvalidAllocationPercentage
}
totalPercentage += *target.Percentage
}
// 验证固定金额
if target.FixedAmount != nil {
if *target.FixedAmount <= 0 {
return ErrInvalidAllocationAmount
}
}
// 验证目标是否存在
switch target.TargetType {
case models.TargetTypeAccount:
exists, err := s.accountRepo.ExistsByID(userID, target.TargetID)
if err != nil {
return fmt.Errorf("检查账户是否存在失败: %w", err)
}
if !exists {
return ErrTargetNotFound
}
case models.TargetTypePiggyBank:
exists, err := s.piggyBankRepo.ExistsByID(userID, target.TargetID)
if err != nil {
return fmt.Errorf("检查存钱罐是否存在失败: %w", err)
}
if !exists {
return ErrTargetNotFound
}
}
}
// 检查百分比总和是否超过100%
if totalPercentage > 100 {
return ErrTotalPercentageExceeds100
}
return nil
}
// isValidTriggerType 检查触发类型是否有效
func isValidTriggerType(triggerType models.TriggerType) bool {
switch triggerType {
case models.TriggerTypeIncome, models.TriggerTypeManual:
return true
default:
return false
}
}
// isValidTargetType 检查目标类型是否有效
func isValidTargetType(targetType models.TargetType) bool {
switch targetType {
case models.TargetTypeAccount, models.TargetTypePiggyBank:
return true
default:
return false
}
}