feat: 实现分类管理功能,包括数据模型、仓库层、服务层和数据库初始化脚本
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user