mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-28 12:46:41 +08:00
feat(clients): preserve traffic counters in portable export/import (#6469)
* feat(clients): preserve traffic counters in portable export/import ExportAll now attaches client_traffics up/down (plus resetCount and last-seen fields) on each portable payload, and ImportClients restores them only for newly created emails so skipped/existing clients keep their live counters. Fixes #5858. * fix(clients): restore imported traffic only onto rows the import created Review of the portable-traffic export/import (#5858) found four defects: - An orphan's restored row was hand-built, dropping reset_weekday and forcing enable=true; a row kept by a keepTraffic delete kept the old client's limits. depletedClientsClause then matched a weekly-renewing over-quota orphan and DelDepleted deleted it. Orphan rows now go through AddClientStat, whose upsert refreshes config and keeps counters, so the unused traffic.total field is dropped from the export. - Created clients were inferred from Skipped emails, so a duplicate email in the file left the created copy with zero counters. bulkCreate now reports which payloads inserted a record, and only those are restored. - Each client took its own serialized-writer commit: 2000 clients spent 3.66s instead of 0.52s. Counters now apply in batched transactions (0.51s). - importClients discarded needRestart when the late restore step failed after clients were committed; it now flags and notifies first, as create already does. The /clients/export and /clients/import API docs now describe traffic. * fix(groups): keep imported traffic out of group totals Group totals keep a deleted client's usage (#5675), and the portable import restores that same usage onto the re-created client. Export, delete, re-import therefore counted it twice in ListGroups, and a fresh panel showed the migrated usage as consumption of its groups. Restored counters are usage from before the import, so the import now shifts each group's baseline up by what it restored, in the same transaction. A group total no longer moves at import time; only traffic consumed afterwards counts. The baseline shift reuses the #5675 helper, now signed. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -81,21 +81,27 @@ _openapi:
|
||||
Returns the deleted count. Cannot be undone.
|
||||
url: '#delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone'
|
||||
- depth: 2
|
||||
title: Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
||||
(copy / download); programmatic callers get the array in obj.
|
||||
url: '#return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj'
|
||||
title: Return every client as a {client, inboundIds, traffic} array — the shape
|
||||
/import accepts — so the payload round-trips straight back through
|
||||
/import. traffic carries the usage counters (up, down, resetCount,
|
||||
lastOnline, lastSubFetch) and is omitted for a client with no traffic
|
||||
row; the quota itself stays in client.totalGB. Clients with no inbound
|
||||
attachment are included with an empty inboundIds list. The UI shows this
|
||||
in a CodeMirror viewer (copy / download); programmatic callers get the
|
||||
array in obj.
|
||||
url: '#return-every-client-as-a-client-inboundids-traffic-array--the-shape-import-accepts--so-the-payload-round-trips-straight-back-through-import-traffic-carries-the-usage-counters-up-down-resetcount-lastonline-lastsubfetch-and-is-omitted-for-a-client-with-no-traffic-row-the-quota-itself-stays-in-clienttotalgb-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj'
|
||||
- depth: 2
|
||||
title: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
||||
with inboundIds are created and attached to those inbounds; items with
|
||||
an empty inboundIds list are restored as unattached client records.
|
||||
Existing emails are never overwritten — they are returned in skipped.
|
||||
Triggers a single Xray restart at the end if any target inbound was
|
||||
running.'
|
||||
url: '#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running'
|
||||
string-encoded array produced by /export ([{client, inboundIds,
|
||||
traffic}]). Items with inboundIds are created and attached to those
|
||||
inbounds; items with an empty inboundIds list are restored as unattached
|
||||
client records. An optional traffic object restores the usage counters,
|
||||
only for clients this import creates. Existing emails are never
|
||||
overwritten — they are returned in skipped, and their live counters are
|
||||
left untouched. Triggers a single Xray restart at the end if any target
|
||||
inbound was running; a failure while restoring counters still reports
|
||||
success=false after the clients were created.'
|
||||
url: '#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-traffic-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-an-optional-traffic-object-restores-the-usage-counters-only-for-clients-this-import-creates-existing-emails-are-never-overwritten--they-are-returned-in-skipped-and-their-live-counters-are-left-untouched-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running-a-failure-while-restoring-counters-still-reports-successfalse-after-the-clients-were-created'
|
||||
- depth: 2
|
||||
title: 'Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
@@ -356,20 +362,26 @@ _openapi:
|
||||
clearing clients left unattached after their inbounds were removed.
|
||||
Returns the deleted count. Cannot be undone.
|
||||
id: delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
- content: Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror
|
||||
viewer (copy / download); programmatic callers get the array in obj.
|
||||
id: return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- content: Return every client as a {client, inboundIds, traffic} array — the
|
||||
shape /import accepts — so the payload round-trips straight back
|
||||
through /import. traffic carries the usage counters (up, down,
|
||||
resetCount, lastOnline, lastSubFetch) and is omitted for a client with
|
||||
no traffic row; the quota itself stays in client.totalGB. Clients with
|
||||
no inbound attachment are included with an empty inboundIds list. The
|
||||
UI shows this in a CodeMirror viewer (copy / download); programmatic
|
||||
callers get the array in obj.
|
||||
id: return-every-client-as-a-client-inboundids-traffic-array--the-shape-import-accepts--so-the-payload-round-trips-straight-back-through-import-traffic-carries-the-usage-counters-up-down-resetcount-lastonline-lastsubfetch-and-is-omitted-for-a-client-with-no-traffic-row-the-quota-itself-stays-in-clienttotalgb-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- content: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]).
|
||||
Items with inboundIds are created and attached to those inbounds;
|
||||
items with an empty inboundIds list are restored as unattached client
|
||||
records. Existing emails are never overwritten — they are returned in
|
||||
skipped. Triggers a single Xray restart at the end if any target
|
||||
inbound was running.'
|
||||
id: import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
string-encoded array produced by /export ([{client, inboundIds,
|
||||
traffic}]). Items with inboundIds are created and attached to those
|
||||
inbounds; items with an empty inboundIds list are restored as
|
||||
unattached client records. An optional traffic object restores the
|
||||
usage counters, only for clients this import creates. Existing emails
|
||||
are never overwritten — they are returned in skipped, and their live
|
||||
counters are left untouched. Triggers a single Xray restart at the end
|
||||
if any target inbound was running; a failure while restoring counters
|
||||
still reports success=false after the clients were created.'
|
||||
id: import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-traffic-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-an-optional-traffic-object-restores-the-usage-counters-only-for-clients-this-import-creates-existing-emails-are-never-overwritten--they-are-returned-in-skipped-and-their-live-counters-are-left-untouched-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running-a-failure-while-restoring-counters-still-reports-successfalse-after-the-clients-were-created
|
||||
- content: 'Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||
|
||||
@@ -9130,7 +9130,7 @@
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Return every client as a {client, inboundIds} array — the same shape /bulkCreate and /import accept — so the payload round-trips straight back through /import. Clients with no inbound attachment are included with an empty inboundIds list. The UI shows this in a CodeMirror viewer (copy / download); programmatic callers get the array in obj.",
|
||||
"summary": "Return every client as a {client, inboundIds, traffic} array — the shape /import accepts — so the payload round-trips straight back through /import. traffic carries the usage counters (up, down, resetCount, lastOnline, lastSubFetch) and is omitted for a client with no traffic row; the quota itself stays in client.totalGB. Clients with no inbound attachment are included with an empty inboundIds list. The UI shows this in a CodeMirror viewer (copy / download); programmatic callers get the array in obj.",
|
||||
"operationId": "get_panel_api_clients_export",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -9165,7 +9165,13 @@
|
||||
"inboundIds": [
|
||||
7,
|
||||
9
|
||||
]
|
||||
],
|
||||
"traffic": {
|
||||
"up": 1048576,
|
||||
"down": 2097152,
|
||||
"resetCount": 0,
|
||||
"lastOnline": 1735680000000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9180,7 +9186,7 @@
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Import clients from a JSON body { \"data\": \"<json>\" }, where data is a string-encoded array produced by /export ([{client, inboundIds}]). Items with inboundIds are created and attached to those inbounds; items with an empty inboundIds list are restored as unattached client records. Existing emails are never overwritten — they are returned in skipped. Triggers a single Xray restart at the end if any target inbound was running.",
|
||||
"summary": "Import clients from a JSON body { \"data\": \"<json>\" }, where data is a string-encoded array produced by /export ([{client, inboundIds, traffic}]). Items with inboundIds are created and attached to those inbounds; items with an empty inboundIds list are restored as unattached client records. An optional traffic object restores the usage counters, only for clients this import creates. Existing emails are never overwritten — they are returned in skipped, and their live counters are left untouched. Triggers a single Xray restart at the end if any target inbound was running; a failure while restoring counters still reports success=false after the clients were created.",
|
||||
"operationId": "post_panel_api_clients_import",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
|
||||
Reference in New Issue
Block a user