diff --git a/backend/internal/api/handlers/payment_channel.go b/backend/internal/api/handlers/payment_channel.go new file mode 100644 index 0000000..d2775b0 --- /dev/null +++ b/backend/internal/api/handlers/payment_channel.go @@ -0,0 +1,167 @@ +package handlers + +import ( + "net/http" + "strconv" + + "sale/internal/models" + "sale/internal/utils" + + "github.com/gin-gonic/gin" +) + +type PaymentChannelHandler struct{} + +func NewPaymentChannelHandler() *PaymentChannelHandler { + return &PaymentChannelHandler{} +} + +func (h *PaymentChannelHandler) List(c *gin.Context) { + var channels []models.PaymentChannel + utils.DB.Order("sort_order ASC, id ASC").Find(&channels) + c.JSON(http.StatusOK, gin.H{"data": channels}) +} + +func (h *PaymentChannelHandler) GetByID(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var channel models.PaymentChannel + if err := utils.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment channel not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"data": channel}) +} + +func (h *PaymentChannelHandler) Create(c *gin.Context) { + var req struct { + Name string `json:"name"` + Code string `json:"code"` + Type string `json:"type"` + Icon string `json:"icon"` + FeeRate float64 `json:"fee_rate"` + MinAmount float64 `json:"min_amount"` + MaxAmount float64 `json:"max_amount"` + Config string `json:"config"` + IsEnabled bool `json:"is_enabled"` + SortOrder int `json:"sort_order"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var existing models.PaymentChannel + if err := utils.DB.Where("code = ?", req.Code).First(&existing).Error; err == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "支付通道代码已存在"}) + return + } + + channel := models.PaymentChannel{ + Name: req.Name, + Code: req.Code, + Type: req.Type, + Icon: req.Icon, + FeeRate: req.FeeRate, + MinAmount: req.MinAmount, + MaxAmount: req.MaxAmount, + Config: req.Config, + IsEnabled: req.IsEnabled, + SortOrder: req.SortOrder, + } + + if err := utils.DB.Create(&channel).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create payment channel"}) + return + } + + c.JSON(http.StatusCreated, gin.H{"data": channel}) +} + +func (h *PaymentChannelHandler) Update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var channel models.PaymentChannel + if err := utils.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment channel not found"}) + return + } + + var req struct { + Name *string `json:"name"` + Code *string `json:"code"` + Type *string `json:"type"` + Icon *string `json:"icon"` + FeeRate *float64 `json:"fee_rate"` + MinAmount *float64 `json:"min_amount"` + MaxAmount *float64 `json:"max_amount"` + Config *string `json:"config"` + IsEnabled *bool `json:"is_enabled"` + SortOrder *int `json:"sort_order"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + updates := make(map[string]interface{}) + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Code != nil && *req.Code != channel.Code { + var existing models.PaymentChannel + if err := utils.DB.Where("code = ? AND id != ?", *req.Code, id).First(&existing).Error; err == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "支付通道代码已存在"}) + return + } + updates["code"] = *req.Code + } + if req.Type != nil { + updates["type"] = *req.Type + } + if req.Icon != nil { + updates["icon"] = *req.Icon + } + if req.FeeRate != nil { + updates["fee_rate"] = *req.FeeRate + } + if req.MinAmount != nil { + updates["min_amount"] = *req.MinAmount + } + if req.MaxAmount != nil { + updates["max_amount"] = *req.MaxAmount + } + if req.Config != nil { + updates["config"] = *req.Config + } + if req.IsEnabled != nil { + updates["is_enabled"] = *req.IsEnabled + } + if req.SortOrder != nil { + updates["sort_order"] = *req.SortOrder + } + + utils.DB.Model(&channel).Updates(updates) + c.JSON(http.StatusOK, gin.H{"data": channel}) +} + +func (h *PaymentChannelHandler) Delete(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + if err := utils.DB.Delete(&models.PaymentChannel{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete payment channel"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Payment channel deleted successfully"}) +} + +func (h *PaymentChannelHandler) Toggle(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var channel models.PaymentChannel + if err := utils.DB.First(&channel, id).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Payment channel not found"}) + return + } + channel.IsEnabled = !channel.IsEnabled + utils.DB.Save(&channel) + c.JSON(http.StatusOK, gin.H{"data": channel}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index f23be5a..f373b3a 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -23,6 +23,7 @@ func SetupRoutes(r *gin.Engine) { articleHandler := handlers.NewArticleHandler() uploadHandler := handlers.NewUploadHandler() bannerHandler := handlers.NewBannerHandler() + paymentChannelHandler := handlers.NewPaymentChannelHandler() r.Use(middlewares.CORSMiddleware()) @@ -220,6 +221,15 @@ func SetupRoutes(r *gin.Engine) { adminBanners.DELETE("/:id", bannerHandler.Delete) adminBanners.PUT("/:id/toggle", bannerHandler.ToggleActive) } + + adminPaymentChannels := admin.Group("/payment-channels") + { + adminPaymentChannels.GET("", paymentChannelHandler.List) + adminPaymentChannels.POST("", paymentChannelHandler.Create) + adminPaymentChannels.PUT("/:id", paymentChannelHandler.Update) + adminPaymentChannels.DELETE("/:id", paymentChannelHandler.Delete) + adminPaymentChannels.PUT("/:id/toggle", paymentChannelHandler.Toggle) + } } } } diff --git a/backend/internal/models/payment_channel.go b/backend/internal/models/payment_channel.go new file mode 100644 index 0000000..cb0dda4 --- /dev/null +++ b/backend/internal/models/payment_channel.go @@ -0,0 +1,28 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +type PaymentChannel struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:50;not null" json:"name"` + Code string `gorm:"size:30;not null;uniqueIndex" json:"code"` + Type string `gorm:"size:20;not null" json:"type"` + Icon string `gorm:"size:255" json:"icon"` + FeeRate float64 `gorm:"default:0" json:"fee_rate"` + MinAmount float64 `gorm:"default:0" json:"min_amount"` + MaxAmount float64 `gorm:"default:0" json:"max_amount"` + Config string `gorm:"type:text" json:"config"` + IsEnabled bool `gorm:"default:true" json:"is_enabled"` + 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:"-"` +} + +func (PaymentChannel) TableName() string { + return "payment_channels" +} diff --git a/backend/internal/utils/database.go b/backend/internal/utils/database.go index 9fccfd4..c97779b 100644 --- a/backend/internal/utils/database.go +++ b/backend/internal/utils/database.go @@ -77,6 +77,7 @@ func AutoMigrate() { &models.SupplierAuthorization{}, &models.Article{}, &models.Banner{}, + &models.PaymentChannel{}, ) if err != nil { log.Fatalf("Failed to migrate database: %v", err) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 2151d5c..0fc8aa0 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -168,4 +168,10 @@ export const adminApi = { updateBanner: (id: number, data: any) => api.put(`/admin/banners/${id}`, data), deleteBanner: (id: number) => api.delete(`/admin/banners/${id}`), toggleBanner: (id: number) => api.put(`/admin/banners/${id}/toggle`), + + getPaymentChannels: () => api.get('/admin/payment-channels'), + createPaymentChannel: (data: any) => api.post('/admin/payment-channels', data), + updatePaymentChannel: (id: number, data: any) => api.put(`/admin/payment-channels/${id}`, data), + deletePaymentChannel: (id: number) => api.delete(`/admin/payment-channels/${id}`), + togglePaymentChannel: (id: number) => api.put(`/admin/payment-channels/${id}/toggle`), } diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 821e5b1..a63c9ab 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -126,6 +126,7 @@ "ticketManagement": "Ticket Management", "articleManagement": "Article Management", "bannerManagement": "Banner Management", + "paymentManagement": "Payment", "systemSettings": "System Settings", "menuManagement": "Menu Management", "smtpSettings": "SMTP Settings", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 13c6ac8..c83b260 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -126,6 +126,7 @@ "ticketManagement": "チケット管理", "articleManagement": "記事管理", "bannerManagement": "バナー管理", + "paymentManagement": "支払い管理", "systemSettings": "システム設定", "menuManagement": "メニュー管理", "smtpSettings": "SMTP設定", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 4f59544..9c97350 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -126,6 +126,7 @@ "ticketManagement": "工单管理", "articleManagement": "资讯管理", "bannerManagement": "轮播图管理", + "paymentManagement": "支付管理", "systemSettings": "系统设置", "menuManagement": "菜单管理", "smtpSettings": "SMTP设置", diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index d2b000f..6c6e8b3 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -57,6 +57,10 @@ {{ $t('admin.bannerManagement') }} + + + {{ $t('admin.paymentManagement') }} + {{ $t('admin.systemSettings') }} @@ -156,7 +160,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 } from '@element-plus/icons-vue' +import { DataAnalysis, User, Folder, Stamp, Goods, List, Van, Trophy, ChatDotSquare, Setting, Document, Picture, Compass, Fold, Expand, HomeFilled, FullScreen, ArrowDown, SwitchButton, ArrowRight, Wallet } from '@element-plus/icons-vue' import { useUserStore } from '../store/user' import { useCartStore } from '../store/cart' @@ -182,6 +186,7 @@ const pageTitles: Record = { '/admin/tickets': '工单管理', '/admin/articles': '资讯管理', '/admin/banners': '轮播图管理', + '/admin/payment': '支付管理', '/admin/settings': '系统设置' } diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index c587651..5410b69 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -49,6 +49,7 @@ const routes = [ { path: 'settings', name: 'AdminSettings', component: () => import('../views/admin/Settings.vue') }, { path: 'articles', name: 'AdminArticles', component: () => import('../views/admin/Articles.vue') }, { path: 'banners', name: 'AdminBanners', component: () => import('../views/admin/Banners.vue') }, + { path: 'payment', name: 'AdminPayment', component: () => import('../views/admin/PaymentChannels.vue') }, ], }, { diff --git a/frontend/src/views/admin/PaymentChannels.vue b/frontend/src/views/admin/PaymentChannels.vue new file mode 100644 index 0000000..8d90101 --- /dev/null +++ b/frontend/src/views/admin/PaymentChannels.vue @@ -0,0 +1,305 @@ + + + + +