diff --git a/backend/internal/handler/merchant.go b/backend/internal/handler/merchant.go index 28b16c1..648bd10 100644 --- a/backend/internal/handler/merchant.go +++ b/backend/internal/handler/merchant.go @@ -302,6 +302,16 @@ func (h *MerchantHandler) UpdateAPIClientStatus(c *gin.Context) { response.OK(c, nil) } +// DeleteAPIClient DELETE /api/merchant/api-clients/:id +func (h *MerchantHandler) DeleteAPIClient(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 64) + if err := h.merchantSvc.DeleteAPIClient(middleware.GetMerchantID(c), uint(id), middleware.GetUserID(c)); err != nil { + response.BadRequest(c, err.Error()) + return + } + response.OK(c, nil) +} + func (h *MerchantHandler) ListCallbacks(c *gin.Context) { list, err := h.callbackSvc.ListSubscriptions(middleware.GetMerchantID(c)) if err != nil { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index b4ca4c0..fb927c6 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -113,9 +113,10 @@ func Setup(h *Handlers) *gin.Engine { merchant.GET("/wallet", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator, model.MemberRoleFinance), h.Merchant.GetWallet) merchant.GET("/wallet/ledger", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Merchant.ListWalletLedger) merchant.POST("/wallet/adjust", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureWallet), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleFinance), h.Merchant.AdjustWallet) - merchant.GET("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.ListAPIClients) + merchant.GET("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), h.Merchant.ListAPIClients) merchant.POST("/api-clients", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateAPIClient) merchant.PATCH("/api-clients/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateAPIClientStatus) + merchant.DELETE("/api-clients/:id", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureAPI), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.DeleteAPIClient) merchant.GET("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.ListCallbacks) merchant.POST("/callbacks", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.CreateCallback) merchant.PATCH("/callbacks/:id/status", middleware.RequireMerchantFeature(h.OpenDB, model.MerchantFeatureCallbacks), middleware.RequireMerchantRole(model.MemberRoleOwner, model.MemberRoleOperator), h.Merchant.UpdateCallbackStatus) diff --git a/backend/internal/service/merchant.go b/backend/internal/service/merchant.go index 51197b8..eddc625 100644 --- a/backend/internal/service/merchant.go +++ b/backend/internal/service/merchant.go @@ -506,7 +506,9 @@ func (s *MerchantService) CreateAPIClient(merchantID uint, in CreateAPIClientInp return errors.New("商户不存在或已禁用") } var clientCount int64 - if err := tx.Model(&model.APIClient{}).Where("merchant_id = ?", merchantID).Count(&clientCount).Error; err != nil { + if err := tx.Model(&model.APIClient{}). + Where("merchant_id = ? AND status = ?", merchantID, model.APIClientStatusActive). + Count(&clientCount).Error; err != nil { return err } if clientCount >= MaxAPIClientsPerMerchant { @@ -545,6 +547,27 @@ func (s *MerchantService) UpdateAPIClientStatus(merchantID, id uint, status stri }) } +// DeleteAPIClient 删除商户的 API 密钥(物理删除,不可恢复)。 +// 删除前要求先停用,避免在用的密钥被误删。 +func (s *MerchantService) DeleteAPIClient(merchantID, id uint, actorUserID uint) error { + return s.db.Transaction(func(tx *gorm.DB) error { + var client model.APIClient + if err := tx.Where("id = ? AND merchant_id = ?", id, merchantID).First(&client).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("API 客户端不存在") + } + return err + } + if client.Status == model.APIClientStatusActive { + return errors.New("请先停用该密钥再删除") + } + if err := tx.Delete(&client).Error; err != nil { + return err + } + return writeAudit(tx, &merchantID, &actorUserID, nil, "api_client.delete", "api_client", fmt.Sprint(id), map[string]string{"name": client.Name, "app_key": client.AppKey}) + }) +} + // ProductCatalogItem 是商品目录(自营商户可售商品)的展示项,供平台管理员分配商品时勾选。 type ProductCatalogItem struct { ID uint `json:"id"` diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 827be0e..fc3b856 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -91,6 +91,8 @@ export const merchantApi = { }) => request.post('/merchant/api-clients', data).then((r) => r.data.data as ApiCredential), updateApiClientStatus: (id: number, status: ApiClient['status']) => request.patch(`/merchant/api-clients/${id}/status`, { status }).then((r) => r.data.data), + deleteApiClient: (id: number) => + request.delete(`/merchant/api-clients/${id}`).then((r) => r.data.data), callbacks: () => request.get('/merchant/callbacks').then((r) => r.data.data as CallbackSubscription[]), createCallback: (data: { name: string; url: string; events: string }) => diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx index a8aa4ac..59ec91a 100644 --- a/frontend/src/layouts/MainLayout.tsx +++ b/frontend/src/layouts/MainLayout.tsx @@ -76,7 +76,7 @@ const adminSections: SidebarSection[] = [ }, { key: 'funds', - label: '资金', + label: '积分', icon: , children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }], }, @@ -114,7 +114,7 @@ const merchantSections: SidebarSection[] = [ }, { key: 'funds', - label: '资金', + label: '积分', icon: , children: [{ key: 'merchant-wallet', label: '积分明细', path: '/merchant-wallet' }], }, diff --git a/frontend/src/pages/MerchantCenter.tsx b/frontend/src/pages/MerchantCenter.tsx index 16eda37..b084159 100644 --- a/frontend/src/pages/MerchantCenter.tsx +++ b/frontend/src/pages/MerchantCenter.tsx @@ -8,6 +8,7 @@ import { Input, InputNumber, Modal, + Popconfirm, Select, Space, Table, @@ -206,14 +207,10 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer } }, [ledger.page, ledger.size, merchantRole]) - const loadAPIClients = useCallback(async (role = merchantRole) => { - if (role !== 'owner' && role !== 'operator') { - setApiClients([]) - return - } + const loadAPIClients = useCallback(async () => { const clientData = await merchantApi.apiClients() setApiClients(clientData || []) - }, [merchantRole]) + }, []) const loadCallbacks = useCallback(async (role = merchantRole) => { if (role !== 'owner' && role !== 'operator') { @@ -245,7 +242,7 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer await loadWallet(undefined, undefined, role) return case 'api': - await loadAPIClients(role) + await loadAPIClients() return case 'callbacks': await loadCallbacks(role) @@ -457,6 +454,16 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer } } + const deleteAPIClient = async (record: ApiClient) => { + try { + await merchantApi.deleteApiClient(record.id) + message.success('密钥已删除') + loadAPIClients() + } catch (e) { + message.error(e instanceof Error ? e.message : '删除失败') + } + } + const toggleCallback = async (record: CallbackSubscription) => { try { await merchantApi.updateCallbackStatus(record.id, record.status === 'active' ? 'disabled' : 'active') @@ -550,11 +557,24 @@ export default function MerchantCenter({ fixedTab, title = '商户中心' }: Mer { title: '操作', key: 'action', - width: 100, + width: 150, render: (_, record) => canManage ? ( - + + + deleteAPIClient(record)} + > + + + ) : '-', }, ]