588 lines
17 KiB
Go
588 lines
17 KiB
Go
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
|
||
}
|
||
}
|