mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
feat(clients): add Delete depleted action
Mirrors the legacy delDepletedClients action that lived under the inbounds page, but as a first-class /panel/api/clients/delDepleted endpoint backed by ClientService. The new path goes through ClientService.Delete for each depleted email, so the new clients + client_inbounds + xray_client_traffic tables stay consistent. Adds a danger-styled toolbar button on the Clients page (next to Reset all client traffic) with a confirm dialog and a toast reporting the deleted count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c5217b9a78
commit
c84799ea2b
6 changed files with 100 additions and 1 deletions
|
|
@ -597,6 +597,12 @@ export const sections = [
|
||||||
summary: 'Reset the up/down counters for every client globally. Quotas and expiry are not affected. Triggers an Xray restart if any counter actually moved.',
|
summary: 'Reset the up/down counters for every client globally. Quotas and expiry are not affected. Triggers an Xray restart if any counter actually moved.',
|
||||||
response: '{\n "success": true\n}',
|
response: '{\n "success": true\n}',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
path: '/panel/api/clients/delDepleted',
|
||||||
|
summary: 'Delete every client whose traffic quota is exhausted (used >= total, when reset is disabled) or whose expiry has passed. Returns the deleted count and triggers an Xray restart when any client was on a running inbound.',
|
||||||
|
response: '{\n "success": true,\n "obj": {\n "deleted": 0\n }\n}',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
QrcodeOutlined,
|
QrcodeOutlined,
|
||||||
RetweetOutlined,
|
RetweetOutlined,
|
||||||
|
RestOutlined,
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
|
@ -40,6 +41,7 @@ const {
|
||||||
detach,
|
detach,
|
||||||
resetTraffic,
|
resetTraffic,
|
||||||
resetAllTraffics,
|
resetAllTraffics,
|
||||||
|
delDepleted,
|
||||||
setEnable,
|
setEnable,
|
||||||
} = useClients();
|
} = useClients();
|
||||||
|
|
||||||
|
|
@ -140,6 +142,25 @@ async function onBulkAddSaved() {
|
||||||
bulkAddOpen.value = false;
|
bulkAddOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDelDepleted() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('pages.clients.delDepletedConfirmTitle') || 'Delete depleted clients?',
|
||||||
|
content: t('pages.clients.delDepletedConfirmContent')
|
||||||
|
|| 'Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.',
|
||||||
|
okText: t('delete'),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: t('cancel'),
|
||||||
|
onOk: async () => {
|
||||||
|
const msg = await delDepleted();
|
||||||
|
if (msg?.success) {
|
||||||
|
const deleted = msg.obj?.deleted ?? 0;
|
||||||
|
message.success(t('pages.clients.toasts.delDepleted', { count: deleted })
|
||||||
|
|| `${deleted} depleted clients deleted`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const onlineSet = computed(() => new Set(onlines.value || []));
|
const onlineSet = computed(() => new Set(onlines.value || []));
|
||||||
const inboundsById = computed(() => {
|
const inboundsById = computed(() => {
|
||||||
const out = {};
|
const out = {};
|
||||||
|
|
@ -343,6 +364,12 @@ const columns = computed(() => [
|
||||||
</template>
|
</template>
|
||||||
<template v-if="!isMobile">{{ t('pages.clients.resetAllTraffics') }}</template>
|
<template v-if="!isMobile">{{ t('pages.clients.resetAllTraffics') }}</template>
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button size="small" danger @click="onDelDepleted">
|
||||||
|
<template #icon>
|
||||||
|
<RestOutlined />
|
||||||
|
</template>
|
||||||
|
<template v-if="!isMobile">{{ t('pages.clients.delDepleted') || 'Delete depleted' }}</template>
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,12 @@ export function useClients() {
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function delDepleted() {
|
||||||
|
const msg = await HttpUtil.post('/panel/api/clients/delDepleted');
|
||||||
|
if (msg?.success) await refresh();
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
async function setEnable(client, enable) {
|
async function setEnable(client, enable) {
|
||||||
if (!client?.id) return null;
|
if (!client?.id) return null;
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
@ -142,6 +148,7 @@ export function useClients() {
|
||||||
detach,
|
detach,
|
||||||
resetTraffic,
|
resetTraffic,
|
||||||
resetAllTraffics,
|
resetAllTraffics,
|
||||||
|
delDepleted,
|
||||||
setEnable,
|
setEnable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/:id/attach", a.attach)
|
g.POST("/:id/attach", a.attach)
|
||||||
g.POST("/:id/detach", a.detach)
|
g.POST("/:id/detach", a.detach)
|
||||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||||
|
g.POST("/delDepleted", a.delDepleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ClientController) list(c *gin.Context) {
|
func (a *ClientController) list(c *gin.Context) {
|
||||||
|
|
@ -155,6 +156,18 @@ func (a *ClientController) resetAllTraffics(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ClientController) delDepleted(c *gin.Context) {
|
||||||
|
deleted, needRestart, err := a.clientService.DelDepleted(&a.inboundService)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, gin.H{"deleted": deleted}, nil)
|
||||||
|
if needRestart {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ClientController) detach(c *gin.Context) {
|
func (a *ClientController) detach(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,48 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
|
||||||
return needRestart, nil
|
return needRestart, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) DelDepleted(inboundSvc *InboundService) (int, bool, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
depletedClause := "reset = 0 and ((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?))"
|
||||||
|
|
||||||
|
var rows []xray.ClientTraffic
|
||||||
|
if err := db.Where(depletedClause, now).Find(&rows).Error; err != nil {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
emails := make(map[string]struct{}, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
if r.Email != "" {
|
||||||
|
emails[r.Email] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needRestart := false
|
||||||
|
deleted := 0
|
||||||
|
for email := range emails {
|
||||||
|
var rec model.ClientRecord
|
||||||
|
if err := db.Where("email = ?", email).First(&rec).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return deleted, needRestart, err
|
||||||
|
}
|
||||||
|
nr, err := s.Delete(inboundSvc, rec.Id, false)
|
||||||
|
if err != nil {
|
||||||
|
return deleted, needRestart, err
|
||||||
|
}
|
||||||
|
if nr {
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
return deleted, needRestart, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ClientService) ResetAllTraffics() (bool, error) {
|
func (s *ClientService) ResetAllTraffics() (bool, error) {
|
||||||
res := database.GetDB().Model(&xray.ClientTraffic{}).
|
res := database.GetDB().Model(&xray.ClientTraffic{}).
|
||||||
Where("1 = 1").
|
Where("1 = 1").
|
||||||
|
|
|
||||||
|
|
@ -440,12 +440,16 @@
|
||||||
"deleteSelected": "Delete ({count})",
|
"deleteSelected": "Delete ({count})",
|
||||||
"bulkDeleteConfirmTitle": "Delete {count} clients?",
|
"bulkDeleteConfirmTitle": "Delete {count} clients?",
|
||||||
"bulkDeleteConfirmContent": "Each selected client is removed from every attached inbound and its traffic record is dropped. This cannot be undone.",
|
"bulkDeleteConfirmContent": "Each selected client is removed from every attached inbound and its traffic record is dropped. This cannot be undone.",
|
||||||
|
"delDepleted": "Delete depleted",
|
||||||
|
"delDepletedConfirmTitle": "Delete depleted clients?",
|
||||||
|
"delDepletedConfirmContent": "Removes every client whose traffic quota is exhausted or whose expiry has passed. This cannot be undone.",
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"deleted": "Client deleted",
|
"deleted": "Client deleted",
|
||||||
"trafficReset": "Traffic reset",
|
"trafficReset": "Traffic reset",
|
||||||
"allTrafficsReset": "All client traffic reset",
|
"allTrafficsReset": "All client traffic reset",
|
||||||
"bulkDeleted": "{count} clients deleted",
|
"bulkDeleted": "{count} clients deleted",
|
||||||
"bulkCreated": "{count} clients created"
|
"bulkCreated": "{count} clients created",
|
||||||
|
"delDepleted": "{count} depleted clients deleted"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue