init
This commit is contained in:
587
internal/service/allocation_rule_service.go
Normal file
587
internal/service/allocation_rule_service.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user