From 1c95f94bca17b0d3723b25b0438ea8080b4614b8 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 1 Nov 2026 14:20:07 +0800 Subject: [PATCH] refactor(admin): drop the SPA credits asset record browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「用户近期资产记录」卡片在 SPA 积分配置页里按用户 ID + 资产类型 + 日期查 user meta 历史,再伪装成统一账本行(entries / operation_key / ref_key)。SPA 只承载配置,运营记录归原生 用户 → 积分/余额 屏幕 (CreditsAdmin / BalanceAdmin),这条平行路径删除,不留双读或迁移。 - CmsCredits.vue: 移除卡片、表格、分页及 ledger* 状态与 formatAssetValue/loadLedger/searchLedger/resetLedger - users.js: 删除仅服务该卡片的 getCreditLog() - UserController: 删除 /assets/transactions 路由、rest_get_credits_log() 及其唯一调用方 asset_value_to_minor() - 契约测试改为断言该能力不得回归 points_record / balance_record 本身保留,前端账本与原生后台仍在消费。 Co-Authored-By: Claude Opus 5 (1M context) --- assets/admin/src/api/modules/users.js | 4 - assets/admin/src/pages/CmsCredits.vue | 100 ------------------ src/Admin/UserController.php | 64 ----------- .../asset-ledger-contract.test.cjs | 26 +++-- 4 files changed, 15 insertions(+), 179 deletions(-) diff --git a/assets/admin/src/api/modules/users.js b/assets/admin/src/api/modules/users.js index 39fa3c68..34818810 100644 --- a/assets/admin/src/api/modules/users.js +++ b/assets/admin/src/api/modules/users.js @@ -27,8 +27,4 @@ export const usersApiMethods = { 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('/assets/transactions' + (qs ? `?${qs}` : '')) - }, } diff --git a/assets/admin/src/pages/CmsCredits.vue b/assets/admin/src/pages/CmsCredits.vue index 56437018..4c5ddac7 100644 --- a/assets/admin/src/pages/CmsCredits.vue +++ b/assets/admin/src/pages/CmsCredits.vue @@ -23,33 +23,6 @@ - - - - - - - - 查询 - 重置 - - - - - -

每日签到

@@ -162,79 +135,6 @@ const cfg = reactive({ }) const streakJson = ref('{}') const saving = ref(false) -const ledgerLoading = ref(false) -const ledgerRows = ref([]) -const ledgerPage = ref(1) -const ledgerPageCount = ref(1) -const ledgerFilters = reactive({ - user_id: '', - asset: 'points', - date_from: '', - date_to: '', -}) -const assetOptions = [ - { label: '积分', value: 'points' }, - { label: '余额', value: 'balance' }, -] -const ledgerColumns = [ - { title: '类型', key: 'reason', width: 150 }, - { title: '订单号', key: 'ref_key', width: 200 }, - { title: '变动 / 变动后', key: 'delta_label', width: 190 }, - { title: '说明', key: 'remark', width: 240, ellipsis: { tooltip: true } }, - { title: '时间', key: 'created_at', width: 170 }, -] - -function formatAssetValue(asset, value) { - const raw = Number(value ?? 0) - if (!Number.isFinite(raw)) return asset === 'balance' ? '0.00' : '0' - if (asset !== 'balance') return String(raw) - const negative = raw < 0 - const digits = String(Math.abs(Math.trunc(raw))).padStart(3, '0') - const whole = digits.slice(0, -2).replace(/^0+(?=\d)/, '') - return (negative ? '-' : '') + whole + '.' + digits.slice(-2) -} - -async function loadLedger() { - ledgerLoading.value = true - try { - if (!/^\d+$/.test(String(ledgerFilters.user_id).trim())) { - message.warning('请输入用户 ID 后查询该用户近期记录') - return - } - const params = { page: ledgerPage.value, per_page: 10 } - Object.entries(ledgerFilters).forEach(([key, value]) => { - if (value !== null && String(value).trim() !== '') params[key] = value - }) - const response = await api.getCreditLog(params) - const data = response?.data || response || {} - ledgerRows.value = (data.items || []).map((row) => { - const entries = Array.isArray(row.entries) ? row.entries : [] - const entry = entries[0] || {} - return { - ...row, - delta_label: `${formatAssetValue(entry.asset, entry.delta)} / ${formatAssetValue(entry.asset, entry.balance_after)}`, - remark: row.remark || '—', - } - }) - ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 10))) - } catch (e) { - message.error(e.message || '加载资产记录失败') - } finally { - ledgerLoading.value = false - } -} - -function searchLedger() { - ledgerPage.value = 1 - loadLedger() -} - -function resetLedger() { - Object.assign(ledgerFilters, { user_id: '', asset: 'points', date_from: '', date_to: '' }) - ledgerRows.value = [] - ledgerPage.value = 1 - ledgerPageCount.value = 1 -} async function loadCfg() { try { diff --git a/src/Admin/UserController.php b/src/Admin/UserController.php index 620b735a..8979736e 100644 --- a/src/Admin/UserController.php +++ b/src/Admin/UserController.php @@ -135,20 +135,6 @@ class UserController extends ControllerBase { ], ]); - register_rest_route(self::API_NAMESPACE, '/assets/transactions', [ - 'methods' => 'GET', - 'callback' => [$this, 'rest_get_credits_log'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => [ - 'user_id' => ['type' => 'integer', 'minimum' => 1, 'required' => true], - 'asset' => ['type' => 'string', 'enum' => ['points', 'balance'], 'required' => true], - 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1], - 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 50, 'default' => 10], - 'date_from' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'], - 'date_to' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'], - ], - ]); - // Report ticket list (admin) register_rest_route(self::API_NAMESPACE, '/users/reports', [ 'methods' => 'GET', @@ -740,54 +726,4 @@ class UserController extends ControllerBase { ], 'message' => __('资产调整已保存', 'aksrt')]); } - public function rest_get_credits_log(\WP_REST_Request $request): \WP_REST_Response { - $user_id = max(0, (int) $request->get_param('user_id')); - $asset = sanitize_key((string) $request->get_param('asset')); - if ($user_id <= 0 || !get_userdata($user_id) || !in_array($asset, ['points', 'balance'], true)) { - return $this->error(__('用户或资产类型无效', 'aksrt'), 400); - } - $meta_key = $asset === 'points' ? 'points_record' : 'balance_record'; - $records = get_user_meta($user_id, $meta_key, true); - $records = is_array($records) ? array_slice($records, 0, 50) : []; - $date_from = sanitize_text_field((string) $request->get_param('date_from')); - $date_to = sanitize_text_field((string) $request->get_param('date_to')); - $records = array_values(array_filter($records, static function ($record) use ($date_from, $date_to): bool { - if (!is_array($record)) return false; - $day = substr((string) ($record['time'] ?? ''), 0, 10); - return ($date_from === '' || $day >= $date_from) && ($date_to === '' || $day <= $date_to); - })); - $page = max(1, (int) $request->get_param('page')); - $per_page = min(50, max(1, (int) $request->get_param('per_page'))); - $items = []; - foreach (array_slice($records, ($page - 1) * $per_page, $per_page) as $index => $record) { - $value = $asset === 'points' ? (int) ($record['value'] ?? 0) : $this->asset_value_to_minor((string) ($record['value'] ?? '0')); - $after = $asset === 'points' ? (int) ($record['points'] ?? 0) : $this->asset_value_to_minor((string) ($record['balance'] ?? '0')); - $items[] = [ - 'id' => (($page - 1) * $per_page) + $index + 1, - 'operation_key' => (string) ($record['operation_key'] ?? ''), - 'reason' => (string) ($record['type'] ?? ''), - 'ref_type' => !empty($record['order_num']) ? 'order' : '', - 'ref_key' => (string) ($record['order_num'] ?? ''), - 'actor_user_id' => 0, - 'assets' => [$asset], - 'created_at' => (string) ($record['time'] ?? ''), - 'remark' => (string) ($record['desc'] ?? ''), - 'entries' => [[ - 'user_id' => $user_id, 'asset' => $asset, 'entry_type' => $value < 0 ? 'debit' : 'credit', - 'delta' => $value, 'balance_after' => $after, 'remark' => (string) ($record['desc'] ?? ''), - ]], - ]; - } - $data = ['items' => $items, 'total' => count($records), 'page' => $page, 'per_page' => $per_page]; - return $this->success(['data' => $data]); - } - - private function asset_value_to_minor(string $value): int { - $value = trim($value); - $negative = str_starts_with($value, '-'); - if ($negative) $value = substr($value, 1); - try { $minor = \AKSRT\Ledger\AssetAmount::balance($value)->minor(); } catch (\InvalidArgumentException $e) { return 0; } - return $negative ? -$minor : $minor; - } - } diff --git a/tests/widget-system/asset-ledger-contract.test.cjs b/tests/widget-system/asset-ledger-contract.test.cjs index 53a04da0..bd33db36 100644 --- a/tests/widget-system/asset-ledger-contract.test.cjs +++ b/tests/widget-system/asset-ledger-contract.test.cjs @@ -324,18 +324,22 @@ test('VIP card redemption is one locked transaction with bounded retry and repla assert.match(method, /vip_card_replay_result/) }) -test('admin recent asset records read the user-meta histories', () => { - const controller = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8') +test('the SPA credits page is configuration only, with no asset record browser', () => { + // Recent per-user asset records live on the native 用户 → 积分/余额 screens + // (src/User/CreditsAdmin.php, src/User/BalanceAdmin.php). The SPA keeps + // configuration only, so the record table, its REST route and its API method + // must not come back. const page = fs.readFileSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'CmsCredits.vue'), 'utf8') - assert.match(controller, /'balance_record'/) - assert.match(controller, /'points_record'/) - assert.match(controller, /'balance_value'/) - assert.match(controller, /'points_value'/) - assert.match(page, /用户近期资产记录/) - assert.match(page, /api\.getCreditLog\(params\)/) - assert.doesNotMatch(page, /统一资产账本|transaction_id|operation_key|toFixed|number_format/) - assert.match(page, /Math\.trunc\(raw\)/) - assert.match(page, /padStart\(3, '0'\)/) + const api = fs.readFileSync(path.join(root, 'assets', 'admin', 'src', 'api', 'modules', 'users.js'), 'utf8') + const controller = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8') + + assert.match(page, /积分配置/) + assert.match(page, /api\.getCreditSettings\(\)/) + assert.doesNotMatch(page, /用户近期资产记录/) + assert.doesNotMatch(page, /getCreditLog|ledgerRows|ledgerFilters|assetOptions/) + assert.doesNotMatch(api, /getCreditLog|assets\/transactions/) + assert.doesNotMatch(controller, /assets\/transactions/) + assert.doesNotMatch(controller, /rest_get_credits_log|asset_value_to_minor/) }) test('asset purchases share the order center and refresh every wallet surface', () => {