diff --git a/.gitignore b/.gitignore index f3b6baed..7b3eb20c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ !/tests/widget-system/auth-source-contract.test.cjs !/tests/widget-system/user-badges-contract.test.cjs !/tests/widget-system/user-features-contract.test.cjs +!/tests/widget-system/asset-ledger-contract.test.cjs +!/tests/asset-ledger/ +!/tests/asset-ledger/*.php !/tests/visual/docker-compose.zibll.yml !/tests/visual/zibll-Caddyfile !/tests/visual/README.md @@ -48,6 +51,7 @@ assets/admin/.vite-temp/ # Temporary / recovery helpers restore-helper-monoliths.php +/.phpunit.cache/ # OS / editor noise .DS_Store diff --git a/assets/admin/src/api/index.js b/assets/admin/src/api/index.js index be925358..c8fbbf28 100644 --- a/assets/admin/src/api/index.js +++ b/assets/admin/src/api/index.js @@ -12,6 +12,21 @@ import { apiMethods } from './modules/index.js' const API_ROOT = window.aksrtAdminData?.apiUrl || '/wp-json/aksrt/v1' const NONCE = window.aksrtAdminData?.nonce || '' +function createIdempotencyKey() { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID() + } + if (typeof crypto === 'undefined' || typeof crypto.getRandomValues !== 'function') { + throw new Error('当前浏览器无法生成安全操作标识') + } + const bytes = new Uint8Array(16) + crypto.getRandomValues(bytes) + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('') + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} + function normalizeDomainKey(domain) { return String(domain || 'global').trim().replace(/-/g, '_') } @@ -85,6 +100,7 @@ function joinRestUrl(baseUrl, endpoint) { class ApiClient { constructor() { this.baseUrl = API_ROOT + this.pendingIdempotencyKeys = new Map() } normalizeDomainKey(domain) { @@ -138,11 +154,22 @@ class ApiClient { finalBody = JSON.stringify(body) } + const isWrite = method !== 'GET' && method !== 'HEAD' + const suppliedKey = headers?.['Idempotency-Key'] || headers?.['idempotency-key'] || '' + const idempotencySignature = isWrite ? `${method}:${url}:${finalBody ?? ''}` : '' + const idempotencyKey = isWrite && !suppliedKey + ? (this.pendingIdempotencyKeys.get(idempotencySignature) || createIdempotencyKey()) + : suppliedKey + if (isWrite && !suppliedKey) { + this.pendingIdempotencyKeys.set(idempotencySignature, idempotencyKey) + } + const config = { method, headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE, + ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}), ...headers, }, ...rest, @@ -174,6 +201,9 @@ class ApiClient { throw new Error(msg) } + if (idempotencySignature !== '') { + this.pendingIdempotencyKeys.delete(idempotencySignature) + } return data } catch (error) { console.error(`[API Error] ${endpoint}:`, error) diff --git a/assets/admin/src/api/modules/users.js b/assets/admin/src/api/modules/users.js index b9aed5e6..60f2f5c7 100644 --- a/assets/admin/src/api/modules/users.js +++ b/assets/admin/src/api/modules/users.js @@ -28,17 +28,17 @@ export const usersApiMethods = { return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) }) }, - getCreditSettings() { return this.request('/credits/settings') }, - saveCreditSettings(data) { return this.request('/credits/settings', { method: 'POST', body: JSON.stringify(data) }) }, + getCreditSettings() { return this.request('/assets/settings') }, + saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) }, listCreditUsers(params = {}) { const qs = new URLSearchParams(params).toString() - return this.request('/credits/users' + (qs ? `?${qs}` : '')) + return this.request('/assets/accounts' + (qs ? `?${qs}` : '')) }, - adjustUserCredits(id, delta, reason) { - return this.request(`/credits/users/${id}/adjust`, { method: 'POST', body: JSON.stringify({ delta, reason }) }) + adjustUserCredits(id, asset, delta, remark) { + return this.request(`/assets/accounts/${id}/adjust`, { method: 'POST', body: JSON.stringify({ asset, delta, remark }) }) }, getCreditLog(params = {}) { const qs = new URLSearchParams(params).toString() - return this.request('/credits/log' + (qs ? `?${qs}` : '')) + return this.request('/assets/transactions' + (qs ? `?${qs}` : '')) }, } diff --git a/assets/admin/src/pages/CmsCredits.vue b/assets/admin/src/pages/CmsCredits.vue index f77b92e0..34933637 100644 --- a/assets/admin/src/pages/CmsCredits.vue +++ b/assets/admin/src/pages/CmsCredits.vue @@ -1,8 +1,8 @@