feat: 新增分类服务层,实现分类的创建、查询及层级管理功能。
This commit is contained in:
@@ -115,6 +115,18 @@ func (s *CategoryService) GetAllCategories(userID uint) ([]models.Category, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get categories: %w", err)
|
return nil, fmt.Errorf("failed to get categories: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-seed if empty
|
||||||
|
if len(categories) == 0 {
|
||||||
|
if err := s.initDefaultCategories(userID); err != nil {
|
||||||
|
// Log error but return empty list to avoid crashing
|
||||||
|
fmt.Printf("Failed to seed default categories for user %d: %v\n", userID, err)
|
||||||
|
return []models.Category{}, nil
|
||||||
|
}
|
||||||
|
// Fetch again after seeding
|
||||||
|
return s.repo.GetAll(userID)
|
||||||
|
}
|
||||||
|
|
||||||
return categories, nil
|
return categories, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +136,12 @@ func (s *CategoryService) GetCategoriesByType(userID uint, categoryType models.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get categories by type: %w", err)
|
return nil, fmt.Errorf("failed to get categories by type: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We don't auto-seed here because the user might just have no categories of this specific type
|
||||||
|
// but might have categories of the other type.
|
||||||
|
// But if they have absolutely no categories, GetAllCategories would have handled it.
|
||||||
|
// We can trust GetAllCategories or just leave this as is.
|
||||||
|
|
||||||
return categories, nil
|
return categories, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +152,16 @@ func (s *CategoryService) GetCategoryTree(userID uint) ([]models.Category, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get category tree: %w", err)
|
return nil, fmt.Errorf("failed to get category tree: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-seed if empty
|
||||||
|
if len(categories) == 0 {
|
||||||
|
if err := s.initDefaultCategories(userID); err != nil {
|
||||||
|
fmt.Printf("Failed to seed default categories for user %d: %v\n", userID, err)
|
||||||
|
return []models.Category{}, nil
|
||||||
|
}
|
||||||
|
return s.repo.GetAllWithChildren(userID)
|
||||||
|
}
|
||||||
|
|
||||||
return categories, nil
|
return categories, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,3 +339,133 @@ func (s *CategoryService) GetCategoryPath(userID, id uint) ([]models.Category, e
|
|||||||
|
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initDefaultCategories seeds default categories for a user
|
||||||
|
func (s *CategoryService) initDefaultCategories(userID uint) error {
|
||||||
|
defaults := []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
Children []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
// Expenses
|
||||||
|
{
|
||||||
|
Name: "餐饮", Type: models.CategoryTypeExpense, Icon: "restaurant", SortOrder: 1,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "早餐", Type: models.CategoryTypeExpense, Icon: "breakfast_dining", SortOrder: 1},
|
||||||
|
{Name: "午餐", Type: models.CategoryTypeExpense, Icon: "lunch_dining", SortOrder: 2},
|
||||||
|
{Name: "晚餐", Type: models.CategoryTypeExpense, Icon: "dinner_dining", SortOrder: 3},
|
||||||
|
{Name: "零食", Type: models.CategoryTypeExpense, Icon: "icecream", SortOrder: 4},
|
||||||
|
{Name: "饮料", Type: models.CategoryTypeExpense, Icon: "local_cafe", SortOrder: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "交通", Type: models.CategoryTypeExpense, Icon: "directions_bus", SortOrder: 2,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "地铁", Type: models.CategoryTypeExpense, Icon: "subway", SortOrder: 1},
|
||||||
|
{Name: "公交", Type: models.CategoryTypeExpense, Icon: "directions_bus", SortOrder: 2},
|
||||||
|
{Name: "打车", Type: models.CategoryTypeExpense, Icon: "local_taxi", SortOrder: 3},
|
||||||
|
{Name: "加油", Type: models.CategoryTypeExpense, Icon: "local_gas_station", SortOrder: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "购物", Type: models.CategoryTypeExpense, Icon: "shopping_bag", SortOrder: 3,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "服饰", Type: models.CategoryTypeExpense, Icon: "checkroom", SortOrder: 1},
|
||||||
|
{Name: "日用", Type: models.CategoryTypeExpense, Icon: "soap", SortOrder: 2},
|
||||||
|
{Name: "电子数码", Type: models.CategoryTypeExpense, Icon: "devices", SortOrder: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "居住", Type: models.CategoryTypeExpense, Icon: "home", SortOrder: 4,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "房租", Type: models.CategoryTypeExpense, Icon: "house", SortOrder: 1},
|
||||||
|
{Name: "水电煤", Type: models.CategoryTypeExpense, Icon: "lightbulb", SortOrder: 2},
|
||||||
|
{Name: "物业", Type: models.CategoryTypeExpense, Icon: "security", SortOrder: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "娱乐", Type: models.CategoryTypeExpense, Icon: "sports_esports", SortOrder: 5,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "游戏", Type: models.CategoryTypeExpense, Icon: "gamepad", SortOrder: 1},
|
||||||
|
{Name: "电影", Type: models.CategoryTypeExpense, Icon: "movie", SortOrder: 2},
|
||||||
|
{Name: "运动", Type: models.CategoryTypeExpense, Icon: "fitness_center", SortOrder: 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Income
|
||||||
|
{
|
||||||
|
Name: "收入", Type: models.CategoryTypeIncome, Icon: "payments", SortOrder: 10,
|
||||||
|
Children: []struct {
|
||||||
|
Name string
|
||||||
|
Type models.CategoryType
|
||||||
|
Icon string
|
||||||
|
SortOrder int
|
||||||
|
}{
|
||||||
|
{Name: "工资", Type: models.CategoryTypeIncome, Icon: "work", SortOrder: 1},
|
||||||
|
{Name: "奖金", Type: models.CategoryTypeIncome, Icon: "star", SortOrder: 2},
|
||||||
|
{Name: "兼职", Type: models.CategoryTypeIncome, Icon: "access_time", SortOrder: 3},
|
||||||
|
{Name: "理财", Type: models.CategoryTypeIncome, Icon: "trending_up", SortOrder: 4},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cat := range defaults {
|
||||||
|
parent := &models.Category{
|
||||||
|
UserID: userID,
|
||||||
|
Name: cat.Name,
|
||||||
|
Type: cat.Type,
|
||||||
|
Icon: cat.Icon,
|
||||||
|
SortOrder: cat.SortOrder,
|
||||||
|
}
|
||||||
|
if err := s.repo.Create(parent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range cat.Children {
|
||||||
|
c := &models.Category{
|
||||||
|
UserID: userID,
|
||||||
|
Name: child.Name,
|
||||||
|
Type: child.Type,
|
||||||
|
Icon: child.Icon,
|
||||||
|
SortOrder: child.SortOrder,
|
||||||
|
ParentID: &parent.ID,
|
||||||
|
}
|
||||||
|
if err := s.repo.Create(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user