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)}
+ >
+
+
+
) : '-',
},
]