refactor(admin): drop the SPA credits asset record browser

「用户近期资产记录」卡片在 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) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-01 14:20:07 +08:00
1 parent 324bcfdadc
commit 1c95f94bca
4 files changed
+15 -179

No files matched your search

-4
View File
@@ -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}` : ''))
},
}
-100
View File
@@ -23,33 +23,6 @@
</n-form>
</n-card>
<n-card title="用户近期资产记录" class="aksrt-section-card">
<n-space vertical :size="12">
<n-space wrap>
<n-input v-model:value="ledgerFilters.user_id" placeholder="用户 ID" clearable style="width: 130px" />
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.date_from" type="date" placeholder="开始日期" style="width: 160px" />
<n-input v-model:value="ledgerFilters.date_to" type="date" placeholder="结束日期" style="width: 160px" />
<n-button type="primary" :loading="ledgerLoading" @click="searchLedger">查询</n-button>
<n-button @click="resetLedger">重置</n-button>
</n-space>
<n-data-table
:columns="ledgerColumns"
:data="ledgerRows"
:loading="ledgerLoading"
:row-key="row => row.id"
:scroll-x="1100"
size="small"
/>
<n-pagination
v-model:page="ledgerPage"
:page-count="ledgerPageCount"
:page-size="10"
@update:page="loadLedger"
/>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>每日签到</h3></div>
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="aksrt-form">
@@ -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 {
-64
View File
@@ -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;
}
}
@@ -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', () => {