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
|
|||
|
|
}
|
|||
|
|
}
|