From 4e86a9a6adeaeb8ba73a62ffd5235bef49339dcd Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 5 Jul 2026 21:51:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E5=88=86=E7=BB=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=E8=B7=A8=E5=88=86=E7=B1=BB=E8=B4=AD=E7=89=A9=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/api/handlers/product.go | 26 ++--- .../internal/api/handlers/purchase_group.go | 105 ++++++++++++++++++ backend/internal/api/routes/routes.go | 9 ++ backend/internal/models/category.go | 35 +++--- backend/internal/models/purchase_group.go | 22 ++++ backend/internal/schemas/product.go | 56 ++++++---- backend/internal/utils/database.go | 1 + frontend/src/api/index.ts | 5 + frontend/src/layouts/AdminLayout.vue | 7 +- frontend/src/router/index.ts | 1 + frontend/src/views/admin/Categories.vue | 38 ++++--- frontend/src/views/admin/PurchaseGroups.vue | 98 ++++++++++++++++ 12 files changed, 336 insertions(+), 67 deletions(-) create mode 100644 backend/internal/api/handlers/purchase_group.go create mode 100644 backend/internal/models/purchase_group.go create mode 100644 frontend/src/views/admin/PurchaseGroups.vue diff --git a/backend/internal/api/handlers/product.go b/backend/internal/api/handlers/product.go index 43c7813..ed852ce 100644 --- a/backend/internal/api/handlers/product.go +++ b/backend/internal/api/handlers/product.go @@ -49,17 +49,17 @@ func (h *CategoryHandler) Create(c *gin.Context) { } category := models.Category{ - Name: req.Name, - Description: req.Description, - ParentID: req.ParentID, - MinAmount: req.MinAmount, - MaxAmount: req.MaxAmount, - MinQuantity: req.MinQuantity, - MaxQuantity: req.MaxQuantity, - MinWeight: req.MinWeight, - MaxWeight: req.MaxWeight, - AllowCrossCategory: req.AllowCrossCategory != nil && *req.AllowCrossCategory, - SortOrder: req.SortOrder, + Name: req.Name, + Description: req.Description, + ParentID: req.ParentID, + PurchaseGroupID: req.PurchaseGroupID, + MinAmount: req.MinAmount, + MaxAmount: req.MaxAmount, + MinQuantity: req.MinQuantity, + MaxQuantity: req.MaxQuantity, + MinWeight: req.MinWeight, + MaxWeight: req.MaxWeight, + SortOrder: req.SortOrder, } if err := utils.DB.Create(&category).Error; err != nil { @@ -123,8 +123,8 @@ func (h *CategoryHandler) Update(c *gin.Context) { if req.SortOrder != nil { updates["sort_order"] = *req.SortOrder } - if req.AllowCrossCategory != nil { - updates["allow_cross_category"] = *req.AllowCrossCategory + if req.PurchaseGroupID != nil { + updates["purchase_group_id"] = *req.PurchaseGroupID } utils.DB.Model(&category).Updates(updates) diff --git a/backend/internal/api/handlers/purchase_group.go b/backend/internal/api/handlers/purchase_group.go new file mode 100644 index 0000000..ab35570 --- /dev/null +++ b/backend/internal/api/handlers/purchase_group.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "net/http" + "strconv" + + "sale/internal/models" + "sale/internal/schemas" + "sale/internal/utils" + + "github.com/gin-gonic/gin" +) + +type PurchaseGroupHandler struct{} + +func NewPurchaseGroupHandler() *PurchaseGroupHandler { + return &PurchaseGroupHandler{} +} + +func (h *PurchaseGroupHandler) List(c *gin.Context) { + var groups []models.PurchaseGroup + utils.DB.Order("sort_order ASC, id ASC").Preload("Categories").Find(&groups) + c.JSON(http.StatusOK, gin.H{"data": groups}) +} + +func (h *PurchaseGroupHandler) Create(c *gin.Context) { + var req schemas.CreatePurchaseGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var existing models.PurchaseGroup + if err := utils.DB.Where("name = ?", req.Name).First(&existing).Error; err == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "分组名称已存在"}) + return + } + + group := models.PurchaseGroup{ + Name: req.Name, + Description: req.Description, + SortOrder: req.SortOrder, + } + + if err := utils.DB.Create(&group).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create purchase group"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"data": group}) +} + +func (h *PurchaseGroupHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var group models.PurchaseGroup + if err := utils.DB.First(&group, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Purchase group not found"}) + return + } + + var req schemas.UpdatePurchaseGroupRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if req.Name != nil && *req.Name != group.Name { + var existing models.PurchaseGroup + if err := utils.DB.Where("name = ? AND id != ?", *req.Name, id).First(&existing).Error; err == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "分组名称已存在"}) + return + } + } + + updates := make(map[string]interface{}) + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.SortOrder != nil { + updates["sort_order"] = *req.SortOrder + } + + utils.DB.Model(&group).Updates(updates) + c.JSON(http.StatusOK, gin.H{"data": group}) +} + +func (h *PurchaseGroupHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + + var categoryCount int64 + utils.DB.Model(&models.Category{}).Where("purchase_group_id = ?", id).Count(&categoryCount) + if categoryCount > 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "该分组下有分类,无法删除"}) + return + } + + if err := utils.DB.Delete(&models.PurchaseGroup{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete purchase group"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Purchase group deleted successfully"}) +} \ No newline at end of file diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 9794b71..d79c651 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -26,6 +26,7 @@ func SetupRoutes(r *gin.Engine) { paymentChannelHandler := handlers.NewPaymentChannelHandler() paymentHandler := handlers.NewPaymentHandler() logHandler := handlers.NewLogHandler() + purchaseGroupHandler := handlers.NewPurchaseGroupHandler() r.Use(middlewares.CORSMiddleware()) @@ -154,6 +155,14 @@ func SetupRoutes(r *gin.Engine) { adminCategories.DELETE("/:id", categoryHandler.Delete) } + adminPurchaseGroups := admin.Group("/purchase-groups") + { + adminPurchaseGroups.GET("", purchaseGroupHandler.List) + adminPurchaseGroups.POST("", purchaseGroupHandler.Create) + adminPurchaseGroups.PUT("/:id", purchaseGroupHandler.Update) + adminPurchaseGroups.DELETE("/:id", purchaseGroupHandler.Delete) + } + adminBrands := admin.Group("/brands") { adminBrands.GET("", brandHandler.List) diff --git a/backend/internal/models/category.go b/backend/internal/models/category.go index d35ba24..6a22742 100644 --- a/backend/internal/models/category.go +++ b/backend/internal/models/category.go @@ -7,23 +7,24 @@ import ( ) type Category struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` - Description string `json:"description"` - ParentID *uint `json:"parent_id"` - MinAmount *float64 `json:"min_amount"` - MaxAmount *float64 `json:"max_amount"` - MinQuantity *int `json:"min_quantity"` - MaxQuantity *int `json:"max_quantity"` - MinWeight *float64 `json:"min_weight"` - MaxWeight *float64 `json:"max_weight"` - AllowCrossCategory bool `gorm:"default:false" json:"allow_cross_category"` - SortOrder int `gorm:"default:0" json:"sort_order"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - ProductCount int64 `gorm:"-" json:"product_count"` - Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"` + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` + Description string `json:"description"` + ParentID *uint `json:"parent_id"` + PurchaseGroupID *uint `json:"purchase_group_id"` + PurchaseGroup *PurchaseGroup `gorm:"foreignKey:PurchaseGroupID" json:"purchase_group,omitempty"` + MinAmount *float64 `json:"min_amount"` + MaxAmount *float64 `json:"max_amount"` + MinQuantity *int `json:"min_quantity"` + MaxQuantity *int `json:"max_quantity"` + MinWeight *float64 `json:"min_weight"` + MaxWeight *float64 `json:"max_weight"` + SortOrder int `gorm:"default:0" json:"sort_order"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ProductCount int64 `gorm:"-" json:"product_count"` + Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"` } func (Category) TableName() string { diff --git a/backend/internal/models/purchase_group.go b/backend/internal/models/purchase_group.go new file mode 100644 index 0000000..11406c8 --- /dev/null +++ b/backend/internal/models/purchase_group.go @@ -0,0 +1,22 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type PurchaseGroup struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null;uniqueIndex" json:"name"` + Description string `json:"description"` + SortOrder int `gorm:"default:0" json:"sort_order"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Categories []Category `gorm:"foreignKey:PurchaseGroupID" json:"categories,omitempty"` +} + +func (PurchaseGroup) TableName() string { + return "purchase_groups" +} \ No newline at end of file diff --git a/backend/internal/schemas/product.go b/backend/internal/schemas/product.go index 0de40e7..4783da4 100644 --- a/backend/internal/schemas/product.go +++ b/backend/internal/schemas/product.go @@ -1,31 +1,43 @@ package schemas type CreateCategoryRequest struct { - Name string `json:"name" binding:"required"` - Description string `json:"description"` - ParentID *uint `json:"parent_id"` - MinAmount *float64 `json:"min_amount"` - MaxAmount *float64 `json:"max_amount"` - MinQuantity *int `json:"min_quantity"` - MaxQuantity *int `json:"max_quantity"` - MinWeight *float64 `json:"min_weight"` - MaxWeight *float64 `json:"max_weight"` - AllowCrossCategory *bool `json:"allow_cross_category"` - SortOrder int `json:"sort_order"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + ParentID *uint `json:"parent_id"` + PurchaseGroupID *uint `json:"purchase_group_id"` + MinAmount *float64 `json:"min_amount"` + MaxAmount *float64 `json:"max_amount"` + MinQuantity *int `json:"min_quantity"` + MaxQuantity *int `json:"max_quantity"` + MinWeight *float64 `json:"min_weight"` + MaxWeight *float64 `json:"max_weight"` + SortOrder int `json:"sort_order"` } type UpdateCategoryRequest struct { - Name *string `json:"name"` - Description *string `json:"description"` - ParentID *uint `json:"parent_id"` - MinAmount *float64 `json:"min_amount"` - MaxAmount *float64 `json:"max_amount"` - MinQuantity *int `json:"min_quantity"` - MaxQuantity *int `json:"max_quantity"` - MinWeight *float64 `json:"min_weight"` - MaxWeight *float64 `json:"max_weight"` - AllowCrossCategory *bool `json:"allow_cross_category"` - SortOrder *int `json:"sort_order"` + Name *string `json:"name"` + Description *string `json:"description"` + ParentID *uint `json:"parent_id"` + PurchaseGroupID *uint `json:"purchase_group_id"` + MinAmount *float64 `json:"min_amount"` + MaxAmount *float64 `json:"max_amount"` + MinQuantity *int `json:"min_quantity"` + MaxQuantity *int `json:"max_quantity"` + MinWeight *float64 `json:"min_weight"` + MaxWeight *float64 `json:"max_weight"` + SortOrder *int `json:"sort_order"` +} + +type CreatePurchaseGroupRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + SortOrder int `json:"sort_order"` +} + +type UpdatePurchaseGroupRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + SortOrder *int `json:"sort_order"` } type CreateProductRequest struct { diff --git a/backend/internal/utils/database.go b/backend/internal/utils/database.go index bc43ee4..9433d41 100644 --- a/backend/internal/utils/database.go +++ b/backend/internal/utils/database.go @@ -63,6 +63,7 @@ func AutoMigrate() { err := DB.AutoMigrate( &models.User{}, &models.Category{}, + &models.PurchaseGroup{}, &models.Brand{}, &models.Product{}, &models.ProductCustomField{}, diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 9eb661e..fbf4b6d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -124,6 +124,11 @@ export const adminApi = { updateCategory: (id: number, data: any) => api.put(`/admin/categories/${id}`, data), deleteCategory: (id: number) => api.delete(`/admin/categories/${id}`), + getPurchaseGroups: () => api.get('/admin/purchase-groups'), + createPurchaseGroup: (data: any) => api.post('/admin/purchase-groups', data), + updatePurchaseGroup: (id: number, data: any) => api.put(`/admin/purchase-groups/${id}`, data), + deletePurchaseGroup: (id: number) => api.delete(`/admin/purchase-groups/${id}`), + getBrands: () => api.get('/admin/brands'), createBrand: (data: any) => api.post('/admin/brands', data), updateBrand: (id: number, data: any) => api.put(`/admin/brands/${id}`, data), diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 320b474..dba8af9 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -25,6 +25,10 @@ {{ $t('admin.categoryManagement') }} + + + 购买分组 + {{ $t('admin.brandManagement') }} @@ -164,7 +168,7 @@ import { ref, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' -import { DataAnalysis, User, Folder, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook } from '@element-plus/icons-vue' +import { DataAnalysis, User, Folder, Grid, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet, Notebook } from '@element-plus/icons-vue' import { useUserStore } from '../store/user' import { useCartStore } from '../store/cart' @@ -182,6 +186,7 @@ const pageTitles: Record = { '/admin': '控制台', '/admin/users': '用户管理', '/admin/categories': '分类管理', + '/admin/purchase-groups': '购买分组', '/admin/brands': '品牌管理', '/admin/products': '商品管理', '/admin/orders': '订单管理', diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 4d19cea..af947b9 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -41,6 +41,7 @@ const routes = [ { path: '', name: 'AdminDashboard', component: () => import('../views/admin/Dashboard.vue') }, { path: 'users', name: 'AdminUsers', component: () => import('../views/admin/Users.vue') }, { path: 'categories', name: 'AdminCategories', component: () => import('../views/admin/Categories.vue') }, + { path: 'purchase-groups', name: 'AdminPurchaseGroups', component: () => import('../views/admin/PurchaseGroups.vue') }, { path: 'brands', name: 'AdminBrands', component: () => import('../views/admin/Brands.vue') }, { path: 'products', name: 'AdminProducts', component: () => import('../views/admin/Products.vue') }, { path: 'orders', name: 'AdminOrders', component: () => import('../views/admin/Orders.vue') }, diff --git a/frontend/src/views/admin/Categories.vue b/frontend/src/views/admin/Categories.vue index 51cbe02..cc9bf0b 100644 --- a/frontend/src/views/admin/Categories.vue +++ b/frontend/src/views/admin/Categories.vue @@ -8,19 +8,18 @@ + + + - - -