feat: 实现分类管理功能,包括数据模型、仓库层、服务层和数据库初始化脚本

This commit is contained in:
2026-01-28 09:55:29 +08:00
parent 1b6feae015
commit 4d024eba8e
6 changed files with 447 additions and 30 deletions

View File

@@ -24,6 +24,7 @@ type CategoryInput struct {
UserID uint `json:"user_id"`
Name string `json:"name" binding:"required"`
Icon string `json:"icon"`
Color string `json:"color"` // HEX color code (e.g., #FF6B35)
Type models.CategoryType `json:"type" binding:"required"`
ParentID *uint `json:"parent_id,omitempty"`
SortOrder int `json:"sort_order"`
@@ -70,6 +71,7 @@ func (s *CategoryService) CreateCategory(input CategoryInput) (*models.Category,
UserID: input.UserID,
Name: input.Name,
Icon: input.Icon,
Color: input.Color,
Type: input.Type,
ParentID: input.ParentID,
SortOrder: input.SortOrder,
@@ -340,102 +342,129 @@ func (s *CategoryService) GetCategoryPath(userID, id uint) ([]models.Category, e
return path, nil
}
// initDefaultCategories seeds default categories for a user
// initDefaultCategories seeds default categories for a user from the default_categories template table
// If no templates are found in the database, falls back to hardcoded defaults
func (s *CategoryService) initDefaultCategories(userID uint) error {
db := s.repo.GetDB()
// Try to copy from default_categories template table first
err := models.CopyDefaultCategoriesToUser(db, userID)
if err == nil {
// Check if any categories were actually created
count, countErr := s.repo.CountByType(userID, models.CategoryTypeExpense)
if countErr == nil && count > 0 {
return nil // Successfully copied from template
}
}
// Fallback to hardcoded defaults if template table is empty or failed
return s.initHardcodedDefaults(userID)
}
// initHardcodedDefaults seeds hardcoded default categories for a user (fallback)
func (s *CategoryService) initHardcodedDefaults(userID uint) error {
defaults := []struct {
Name string
Type models.CategoryType
Icon string
Color string
SortOrder int
Children []struct {
Name string
Type models.CategoryType
Icon string
Color string
SortOrder int
}
}{
// Expenses
{
Name: "餐饮", Type: models.CategoryTypeExpense, Icon: "restaurant", SortOrder: 1,
Name: "餐饮", Type: models.CategoryTypeExpense, Icon: "mdi:silverware-fork-knife", Color: "#FF6B35", SortOrder: 1,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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: "mdi:food-croissant", Color: "#FBBF24", SortOrder: 1},
{Name: "午餐", Type: models.CategoryTypeExpense, Icon: "mdi:food", Color: "#FB923C", SortOrder: 2},
{Name: "晚餐", Type: models.CategoryTypeExpense, Icon: "mdi:food-turkey", Color: "#F97316", SortOrder: 3},
{Name: "零食", Type: models.CategoryTypeExpense, Icon: "mdi:cookie", Color: "#FDE047", SortOrder: 4},
{Name: "饮料", Type: models.CategoryTypeExpense, Icon: "mdi:coffee", Color: "#A16207", SortOrder: 5},
},
},
{
Name: "交通", Type: models.CategoryTypeExpense, Icon: "directions_bus", SortOrder: 2,
Name: "交通", Type: models.CategoryTypeExpense, Icon: "mdi:bus", Color: "#3B82F6", SortOrder: 2,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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: "mdi:subway-variant", Color: "#3B82F6", SortOrder: 1},
{Name: "公交", Type: models.CategoryTypeExpense, Icon: "mdi:bus", Color: "#60A5FA", SortOrder: 2},
{Name: "打车", Type: models.CategoryTypeExpense, Icon: "mdi:taxi", Color: "#FBBF24", SortOrder: 3},
{Name: "加油", Type: models.CategoryTypeExpense, Icon: "mdi:gas-station", Color: "#EF4444", SortOrder: 4},
},
},
{
Name: "购物", Type: models.CategoryTypeExpense, Icon: "shopping_bag", SortOrder: 3,
Name: "购物", Type: models.CategoryTypeExpense, Icon: "mdi:shopping", Color: "#EC4899", SortOrder: 3,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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: "mdi:tshirt-crew", Color: "#EC4899", SortOrder: 1},
{Name: "日用", Type: models.CategoryTypeExpense, Icon: "mdi:basket", Color: "#F472B6", SortOrder: 2},
{Name: "电子数码", Type: models.CategoryTypeExpense, Icon: "mdi:laptop", Color: "#3B82F6", SortOrder: 3},
},
},
{
Name: "居住", Type: models.CategoryTypeExpense, Icon: "home", SortOrder: 4,
Name: "居住", Type: models.CategoryTypeExpense, Icon: "mdi:home", Color: "#92400E", SortOrder: 4,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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: "mdi:home-city", Color: "#92400E", SortOrder: 1},
{Name: "水电煤", Type: models.CategoryTypeExpense, Icon: "mdi:lightbulb-on", Color: "#FBBF24", SortOrder: 2},
{Name: "物业", Type: models.CategoryTypeExpense, Icon: "mdi:office-building", Color: "#64748B", SortOrder: 3},
},
},
{
Name: "娱乐", Type: models.CategoryTypeExpense, Icon: "sports_esports", SortOrder: 5,
Name: "娱乐", Type: models.CategoryTypeExpense, Icon: "mdi:gamepad-variant", Color: "#8B5CF6", SortOrder: 5,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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},
{Name: "游戏", Type: models.CategoryTypeExpense, Icon: "mdi:controller-classic", Color: "#8B5CF6", SortOrder: 1},
{Name: "电影", Type: models.CategoryTypeExpense, Icon: "mdi:movie-open", Color: "#EF4444", SortOrder: 2},
{Name: "运动", Type: models.CategoryTypeExpense, Icon: "mdi:dumbbell", Color: "#22C55E", SortOrder: 3},
},
},
// Income
{
Name: "收入", Type: models.CategoryTypeIncome, Icon: "payments", SortOrder: 10,
Name: "工作收入", Type: models.CategoryTypeIncome, Icon: "mdi:briefcase", Color: "#10B981", SortOrder: 10,
Children: []struct {
Name string
Type models.CategoryType
Icon string
Color 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},
{Name: "工资", Type: models.CategoryTypeIncome, Icon: "mdi:briefcase", Color: "#10B981", SortOrder: 1},
{Name: "奖金", Type: models.CategoryTypeIncome, Icon: "mdi:trophy", Color: "#FBBF24", SortOrder: 2},
{Name: "兼职", Type: models.CategoryTypeIncome, Icon: "mdi:briefcase-clock", Color: "#8B5CF6", SortOrder: 3},
{Name: "理财", Type: models.CategoryTypeIncome, Icon: "mdi:chart-line", Color: "#F59E0B", SortOrder: 4},
},
},
}
@@ -446,6 +475,7 @@ func (s *CategoryService) initDefaultCategories(userID uint) error {
Name: cat.Name,
Type: cat.Type,
Icon: cat.Icon,
Color: cat.Color,
SortOrder: cat.SortOrder,
}
if err := s.repo.Create(parent); err != nil {
@@ -458,6 +488,7 @@ func (s *CategoryService) initDefaultCategories(userID uint) error {
Name: child.Name,
Type: child.Type,
Icon: child.Icon,
Color: child.Color,
SortOrder: child.SortOrder,
ParentID: &parent.ID,
}