feat(admin): add bulk actions and pending-record deletion to creator income

- Always render the checkbox column and bulk bar; disable with a reason
  instead of hiding when settlement is off or the page has no pending rows
- Replace the batch-settle button with a bulk action select (settle / delete)
  plus apply, matching the charge cards and invite codes pages
- Add per-row delete for pending records and Revenue::delete_pending(),
  guarded by status = 'pending' so settled ledger-backed rows are never removed
- Report done / failed counts for bulk settle and delete

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-31 19:51:55 +08:00
1 parent b25ebc5fff
commit 1d6bc8f7cc
5 files changed
+146 -48

No files matched your search

+10 -8
View File
@@ -12,11 +12,12 @@
.aksrt-income-page .aksrt-income-filters__id { width: 96px; } .aksrt-income-page .aksrt-income-filters__id { width: 96px; }
.aksrt-income-filters__submit { display: inline-flex; gap: 8px; } .aksrt-income-filters__submit { display: inline-flex; gap: 8px; }
/* ---------- 批量结算 ---------- */ /* ---------- 批量操作 ---------- */
.aksrt-income-batch { margin: 0; } .aksrt-income-bulk { margin: 0; }
.aksrt-income-batch__count { color: var(--an-muted); font-size: 12px; } .aksrt-income-bulk__count { color: var(--an-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.aksrt-income-batch__count.is-active { color: var(--an-text); font-weight: 500; } .aksrt-income-bulk__count.is-active { color: var(--an-text); font-weight: 500; }
.aksrt-income-page .aksrt-income-batch .button:disabled { opacity: .6; cursor: not-allowed; } .aksrt-income-page .aksrt-income-bulk .button:disabled { opacity: .6; cursor: not-allowed; }
.aksrt-income-table .check-column input:disabled { cursor: not-allowed; opacity: .45; }
/* ---------- 分成表 ---------- */ /* ---------- 分成表 ---------- */
.aksrt-income-table { min-width: 1040px; table-layout: fixed; } .aksrt-income-table { min-width: 1040px; table-layout: fixed; }
@@ -25,8 +26,8 @@
.aksrt-income-table .column-source { width: 24%; } .aksrt-income-table .column-source { width: 24%; }
.aksrt-income-table .column-base { width: 14%; } .aksrt-income-table .column-base { width: 14%; }
.aksrt-income-table .column-amount { width: 12%; } .aksrt-income-table .column-amount { width: 12%; }
.aksrt-income-table .column-status { width: 13%; } .aksrt-income-table .column-status { width: 12%; }
.aksrt-income-table .column-actions { width: 9%; } .aksrt-income-table .column-actions { width: 13%; }
.aksrt-income-table td, .aksrt-income-table td,
.aksrt-income-table tbody th { vertical-align: top; } .aksrt-income-table tbody th { vertical-align: top; }
.aksrt-income-id { color: var(--an-text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 600; } .aksrt-income-id { color: var(--an-text); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; font-weight: 600; }
@@ -40,7 +41,8 @@ a.aksrt-income-post:focus { color: var(--an-primary); }
.aksrt-income-table .column-base, .aksrt-income-table .column-base,
.aksrt-income-table .column-amount { font-variant-numeric: tabular-nums; } .aksrt-income-table .column-amount { font-variant-numeric: tabular-nums; }
/* 行内结算:单按钮 POST 表单 */ /* 行内操作:每个按钮是一个独立 POST 表单 */
.aksrt-income-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.aksrt-income-action { display: inline-flex; margin: 0; } .aksrt-income-action { display: inline-flex; margin: 0; }
.aksrt-income-action .button[disabled] { opacity: .6; cursor: progress; } .aksrt-income-action .button[disabled] { opacity: .6; cursor: progress; }
+16 -5
View File
@@ -1,4 +1,4 @@
/** 创作分成:批量勾选计数、全选、结算二次确认 + 防重复提交。仅本页入队。 */ /** 创作分成:勾选计数、全选、批量操作(结算 / 删除)、二次确认 + 防重复提交。仅本页入队。 */
(function () { (function () {
'use strict'; 'use strict';
@@ -7,7 +7,8 @@
var checkAll = root.querySelector('[data-aksrt-income-check-all]'); var checkAll = root.querySelector('[data-aksrt-income-check-all]');
var checks = Array.from(root.querySelectorAll('[data-aksrt-income-check]')); var checks = Array.from(root.querySelectorAll('[data-aksrt-income-check]'));
var batchSubmit = root.querySelector('[data-aksrt-income-batch-submit]'); var bulkAction = root.querySelector('[data-aksrt-income-bulk-action]');
var bulkSubmit = root.querySelector('[data-aksrt-income-bulk-submit]');
var selected = root.querySelector('[data-aksrt-income-selected]'); var selected = root.querySelector('[data-aksrt-income-selected]');
var emptyText = selected ? selected.textContent : ''; var emptyText = selected ? selected.textContent : '';
@@ -17,12 +18,12 @@
function sync() { function sync() {
var count = checkedCount(); var count = checkedCount();
if (batchSubmit) batchSubmit.disabled = count === 0; if (bulkSubmit) bulkSubmit.disabled = count === 0 || !bulkAction || bulkAction.value === '';
if (selected) { if (selected) {
selected.textContent = count > 0 ? '已选 ' + count + ' 条' : emptyText; selected.textContent = count > 0 ? '已选 ' + count + ' 条' : emptyText;
selected.classList.toggle('is-active', count > 0); selected.classList.toggle('is-active', count > 0);
} }
if (checkAll) { if (checkAll && !checkAll.disabled) {
checkAll.checked = count > 0 && count === checks.length; checkAll.checked = count > 0 && count === checks.length;
checkAll.indeterminate = count > 0 && count < checks.length; checkAll.indeterminate = count > 0 && count < checks.length;
} }
@@ -35,8 +36,18 @@
}); });
} }
checks.forEach(function (box) { box.addEventListener('change', sync); }); checks.forEach(function (box) { box.addEventListener('change', sync); });
if (bulkAction) bulkAction.addEventListener('change', sync);
sync(); sync();
/** 单行表单的确认文案在 form 上;批量表单按所选操作取 option 上的文案。 */
function confirmMessage(form) {
if (bulkAction && form.contains(bulkAction)) {
var option = bulkAction.selectedOptions[0];
return option ? option.getAttribute('data-confirm') : '';
}
return form.getAttribute('data-aksrt-confirm');
}
root.addEventListener('submit', function (event) { root.addEventListener('submit', function (event) {
var form = event.target.closest('form'); var form = event.target.closest('form');
if (!form) return; if (!form) return;
@@ -44,7 +55,7 @@
event.preventDefault(); event.preventDefault();
return; return;
} }
var message = form.getAttribute('data-aksrt-confirm'); var message = confirmMessage(form);
if (message && !window.confirm(message.replace('{count}', String(checkedCount())))) { if (message && !window.confirm(message.replace('{count}', String(checkedCount())))) {
event.preventDefault(); event.preventDefault();
return; return;
+50 -11
View File
@@ -3,8 +3,8 @@
* 支付中心「创作分成」:分成记录列表、单条与批量结算。 * 支付中心「创作分成」:分成记录列表、单条与批量结算。
* *
* 列表渲染在 PayIncomeList(flat sibling trait,PSR-4 要求与宿主类同目录);这里保留入口、 * 列表渲染在 PayIncomeList(flat sibling trait,PSR-4 要求与宿主类同目录);这里保留入口、
* admin-post 处理器与共用的展示助手。页面样式在 assets/css/admin/income.css, * admin-post 处理器(结算 / 删除 / 批量操作)与共用的展示助手。页面样式在 assets/css/admin/income.css,
* 结算确认、全选与防重复提交在 assets/js/admin-income.js。 * 批量操作、二次确认、全选与防重复提交在 assets/js/admin-income.js。
* *
* @package AKSRT * @package AKSRT
*/ */
@@ -35,7 +35,8 @@ class PayIncome {
} }
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']); add_action('admin_post_aksrt_pay_settle_income', [$this, 'handle_settle_income']);
add_action('admin_post_aksrt_pay_batch_settle_income', [$this, 'handle_batch_settle_income']); add_action('admin_post_aksrt_pay_delete_income', [$this, 'handle_delete_income']);
add_action('admin_post_aksrt_pay_bulk_income', [$this, 'handle_bulk_income']);
} }
public function enqueue_assets(): void { public function enqueue_assets(): void {
@@ -79,18 +80,43 @@ class PayIncome {
$this->redirect_back(['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']); $this->redirect_back(['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']);
} }
public function handle_batch_settle_income(): void { /** 单条删除:只删待结算记录,已结算记录由 Revenue::delete_pending() 的状态条件挡住。 */
public function handle_delete_income(): void {
$this->ensure_cap(); $this->ensure_cap();
check_admin_referer('aksrt_pay_batch_settle_income'); $id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
check_admin_referer('aksrt_pay_delete_income_' . $id);
$deleted = $id > 0 ? \AKSRT\Payment\Revenue::get_instance()->delete_pending([$id]) : 0;
$this->redirect_back(['aksrt_pay_msg' => $deleted > 0 ? 'income_deleted' : 'income_delete_failed', 'done' => $deleted]);
}
/** 批量操作:bulk_action = settle | delete,作用于勾选的记录。 */
public function handle_bulk_income(): void {
$this->ensure_cap();
check_admin_referer('aksrt_pay_bulk_income');
$action = isset($_POST['bulk_action']) ? sanitize_key((string) wp_unslash($_POST['bulk_action'])) : '';
$raw_ids = isset($_POST['revenue_ids']) ? wp_unslash($_POST['revenue_ids']) : []; $raw_ids = isset($_POST['revenue_ids']) ? wp_unslash($_POST['revenue_ids']) : [];
$ids = is_array($raw_ids) $ids = is_array($raw_ids)
? array_values(array_unique(array_filter(array_map('intval', $raw_ids), static fn (int $id): bool => $id > 0))) ? array_values(array_unique(array_filter(array_map('intval', $raw_ids), static fn (int $id): bool => $id > 0)))
: []; : [];
if (!in_array($action, ['settle', 'delete'], true)) {
$this->redirect_back(['aksrt_pay_msg' => 'income_bulk_invalid']);
}
if ($ids === []) { if ($ids === []) {
$this->redirect_back(['aksrt_pay_msg' => 'income_none_selected']); $this->redirect_back(['aksrt_pay_msg' => 'income_none_selected']);
} }
if ($action === 'delete') {
$deleted = \AKSRT\Payment\Revenue::get_instance()->delete_pending($ids);
$failed = count($ids) - $deleted;
$this->redirect_back([
'aksrt_pay_msg' => $deleted === 0 ? 'income_delete_failed' : ($failed > 0 ? 'income_delete_partial' : 'income_deleted'),
'done' => $deleted,
'failed' => $failed,
]);
}
$operation_key = $this->request_operation_key('admin-revenue-settle-batch'); $operation_key = $this->request_operation_key('admin-revenue-settle-batch');
$result = $operation_key !== '' $result = $operation_key !== ''
? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id(), $operation_key) ? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id(), $operation_key)
@@ -100,7 +126,7 @@ class PayIncome {
$this->redirect_back([ $this->redirect_back([
'aksrt_pay_msg' => $settled === 0 ? 'income_failed' : ($failed > 0 ? 'income_batch_partial' : 'income_batch_settled'), 'aksrt_pay_msg' => $settled === 0 ? 'income_failed' : ($failed > 0 ? 'income_batch_partial' : 'income_batch_settled'),
'settled' => $settled, 'done' => $settled,
'failed' => $failed, 'failed' => $failed,
]); ]);
} }
@@ -124,7 +150,7 @@ class PayIncome {
if (is_string($referer) && $referer !== '') { if (is_string($referer) && $referer !== '') {
parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed); parse_str((string) wp_parse_url($referer, PHP_URL_QUERY), $parsed);
if (($parsed['page'] ?? '') === self::PAGE_SLUG) { if (($parsed['page'] ?? '') === self::PAGE_SLUG) {
unset($parsed['page'], $parsed['aksrt_pay_msg'], $parsed['settled'], $parsed['failed']); unset($parsed['page'], $parsed['aksrt_pay_msg'], $parsed['done'], $parsed['failed']);
$query = $parsed; $query = $parsed;
} }
} }
@@ -179,7 +205,7 @@ class PayIncome {
return; return;
} }
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$settled = max(0, (int) ($_GET['settled'] ?? 0)); $done = max(0, (int) ($_GET['done'] ?? 0));
$failed = max(0, (int) ($_GET['failed'] ?? 0)); $failed = max(0, (int) ($_GET['failed'] ?? 0));
// 码 => [文案, notice 类型] // 码 => [文案, notice 类型]
$map = [ $map = [
@@ -187,16 +213,29 @@ class PayIncome {
'income_batch_settled' => [sprintf( 'income_batch_settled' => [sprintf(
/* translators: %d settled count */ /* translators: %d settled count */
__('已结算 %d 条分成记录。', 'aksrt'), __('已结算 %d 条分成记录。', 'aksrt'),
$settled $done
), 'success'], ), 'success'],
'income_batch_partial' => [sprintf( 'income_batch_partial' => [sprintf(
/* translators: 1 settled count 2 failed count */ /* translators: 1 settled count 2 failed count */
__('已结算 %1$d 条,%2$d 条失败(记录状态可能已变化),可刷新后重试。', 'aksrt'), __('已结算 %1$d 条,%2$d 条失败(记录状态可能已变化),可刷新后重试。', 'aksrt'),
$settled, $done,
$failed $failed
), 'warning'], ), 'warning'],
'income_none_selected' => [__('请先勾选要结算的待结算记录。', 'aksrt'), 'warning'], 'income_none_selected' => [__('请先勾选待结算记录。', 'aksrt'), 'warning'],
'income_bulk_invalid' => [__('请先选择批量操作。', 'aksrt'), 'warning'],
'income_failed' => [__('分成结算失败:记录可能已被结算,或分成功能未启用。', 'aksrt'), 'error'], 'income_failed' => [__('分成结算失败:记录可能已被结算,或分成功能未启用。', 'aksrt'), 'error'],
'income_deleted' => [sprintf(
/* translators: %d deleted count */
__('已删除 %d 条待结算记录。', 'aksrt'),
$done
), 'success'],
'income_delete_partial' => [sprintf(
/* translators: 1 deleted count 2 skipped count */
__('已删除 %1$d 条,%2$d 条未删除(仅待结算记录可删除,可能已被结算)。', 'aksrt'),
$done,
$failed
), 'warning'],
'income_delete_failed' => [__('删除失败:仅待结算记录可删除,记录可能已被结算或不存在。', 'aksrt'), 'error'],
]; ];
if (!isset($map[$code])) { if (!isset($map[$code])) {
return; return;
+48 -24
View File
@@ -61,17 +61,16 @@ trait PayIncomeList {
$this->render_status_views($filters, $query_args, $counts); $this->render_status_views($filters, $query_args, $counts);
$this->render_filter_card($filters); $this->render_filter_card($filters);
$can_settle = $policy['enabled']; $can_settle = $policy['enabled'];
$has_pending = $can_settle && in_array('pending', array_column($items, 'status'), true); $has_pending = in_array('pending', array_column($items, 'status'), true);
NativeUi::table_card_open(__('分成记录', 'aksrt'), $has_pending ? $this->batch_form_html() : ''); // 复选框列与批量栏始终渲染;不可用时禁用并用 title 说明原因,而不是整块隐藏。
NativeUi::table_card_open(__('分成记录', 'aksrt'), $this->bulk_form_html($can_settle, $has_pending));
echo '<table class="wp-list-table widefat striped aksrt-income-table">'; echo '<table class="wp-list-table widefat striped aksrt-income-table">';
echo '<caption class="screen-reader-text">' . esc_html__('分成记录', 'aksrt') . '</caption><thead><tr>'; echo '<caption class="screen-reader-text">' . esc_html__('分成记录', 'aksrt') . '</caption><thead><tr>';
echo '<td class="manage-column column-cb check-column">'; echo '<td class="manage-column column-cb check-column">'
if ($has_pending) { . '<input type="checkbox" data-aksrt-income-check-all aria-label="' . esc_attr__('全选本页待结算记录', 'aksrt') . '"'
echo '<input type="checkbox" data-aksrt-income-check-all aria-label="' . esc_attr__('全选本页待结算记录', 'aksrt') . '" />'; . ($has_pending ? '' : ' disabled title="' . esc_attr__('本页没有待结算记录', 'aksrt') . '"') . ' /></td>';
}
echo '</td>';
foreach ([ foreach ([
'record' => __('记录 / 产生时间', 'aksrt'), 'record' => __('记录 / 产生时间', 'aksrt'),
'author' => __('作者', 'aksrt'), 'author' => __('作者', 'aksrt'),
@@ -111,12 +110,11 @@ trait PayIncomeList {
$pending = $status === 'pending'; $pending = $status === 'pending';
echo '<tr>'; echo '<tr>';
echo '<th scope="row" class="check-column">'; // 已结算记录已入账到钱包,不可结算也不可删除:复选框禁用占位,保持列对齐。
if ($pending && $can_settle) { echo '<th scope="row" class="check-column">'
echo '<input type="checkbox" form="aksrt-income-batch" name="revenue_ids[]" value="' . esc_attr((string) $id) . '" data-aksrt-income-check aria-label="' . '<input type="checkbox" form="aksrt-income-bulk" name="revenue_ids[]" value="' . esc_attr((string) $id) . '"'
. esc_attr(sprintf(__('选择分成记录 #%d', 'aksrt'), $id)) . '" />'; . ($pending ? ' data-aksrt-income-check' : ' disabled title="' . esc_attr__('已结算记录不可操作', 'aksrt') . '"')
} . ' aria-label="' . esc_attr(sprintf(__('选择分成记录 #%d', 'aksrt'), $id)) . '" /></th>';
echo '</th>';
echo '<td class="column-record"><span class="aksrt-income-id">#' . esc_html((string) $id) . '</span>' echo '<td class="column-record"><span class="aksrt-income-id">#' . esc_html((string) $id) . '</span>'
. '<div class="aksrt-income-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>'; . '<div class="aksrt-income-sub">' . esc_html((string) ($row['created_at'] ?? '')) . '</div></td>';
@@ -148,8 +146,11 @@ trait PayIncomeList {
. $this->status_hint($row, $cycle_days) . '</td>'; . $this->status_hint($row, $cycle_days) . '</td>';
echo '<td class="column-actions">'; echo '<td class="column-actions">';
if ($pending && $can_settle) { if ($pending) {
echo $this->settle_form_html($id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo '<div class="aksrt-income-actions">'
. ($can_settle ? $this->settle_form_html($id) : '')
. $this->delete_form_html($id)
. '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else { } else {
echo '<span class="aksrt-native-muted">—</span>'; echo '<span class="aksrt-native-muted">—</span>';
} }
@@ -300,18 +301,41 @@ trait PayIncomeList {
return ['' => __('全部订单状态', 'aksrt')] + \AKSRT\Core\DomainModel::order_status_labels(); return ['' => __('全部订单状态', 'aksrt')] + \AKSRT\Core\DomainModel::order_status_labels();
} }
/** 批量结算表单只放提交按钮;表格里的复选框通过 form="aksrt-income-batch" 关联进来。 */ /**
private function batch_form_html(): string { * 批量操作栏:已选计数 + 「批量操作」下拉 + 应用。表格里的复选框通过 form="aksrt-income-bulk" 关联进来。
return '<form id="aksrt-income-batch" class="aksrt-native-bulk aksrt-income-batch" method="post" action="' . esc_url(admin_url('admin-post.php')) . '"' * 各操作的确认文案挂在 option 的 data-confirm 上,由 admin-income.js 按所选操作取用。
. ' data-aksrt-confirm="' . esc_attr__('确认立即结算选中的 {count} 条分成?金额会转入作者钱包,不可撤销。', 'aksrt') . '">' */
. '<input type="hidden" name="action" value="aksrt_pay_batch_settle_income" />' private function bulk_form_html(bool $can_settle, bool $has_pending): string {
$settle_attrs = $can_settle
? ' data-confirm="' . esc_attr__('确认立即结算选中的 {count} 条分成?金额会转入作者钱包,不可撤销。', 'aksrt') . '"'
: ' disabled';
$settle_label = $can_settle ? __('立即结算', 'aksrt') : __('立即结算(分成功能未启用)', 'aksrt');
return '<form id="aksrt-income-bulk" class="aksrt-native-bulk aksrt-income-bulk" method="post" action="' . esc_url(admin_url('admin-post.php')) . '">'
. '<input type="hidden" name="action" value="aksrt_pay_bulk_income" />'
. $this->operation_key_field() . $this->operation_key_field()
. wp_nonce_field('aksrt_pay_batch_settle_income', '_wpnonce', true, false) . wp_nonce_field('aksrt_pay_bulk_income', '_wpnonce', true, false)
. '<span class="aksrt-income-batch__count" data-aksrt-income-selected aria-live="polite">' . esc_html__('未选择记录', 'aksrt') . '</span>' . '<span class="aksrt-income-bulk__count" data-aksrt-income-selected aria-live="polite">'
. '<button type="submit" class="button button-primary" disabled data-aksrt-income-batch-submit>' . esc_html__('结算选中', 'aksrt') . '</button>' . esc_html($has_pending ? __('未选择记录', 'aksrt') : __('本页没有待结算记录', 'aksrt')) . '</span>'
. '<label class="screen-reader-text" for="aksrt-income-bulk-action">' . esc_html__('批量操作', 'aksrt') . '</label>'
. '<select name="bulk_action" id="aksrt-income-bulk-action" data-aksrt-income-bulk-action>'
. '<option value="">' . esc_html__('批量操作', 'aksrt') . '</option>'
. '<option value="settle"' . $settle_attrs . '>' . esc_html($settle_label) . '</option>'
. '<option value="delete" data-confirm="' . esc_attr__('确认删除选中的 {count} 条待结算分成?删除后作者不会获得这些分成,不可恢复。', 'aksrt') . '">' . esc_html__('删除', 'aksrt') . '</option>'
. '</select>'
. '<button type="submit" class="button" disabled data-aksrt-income-bulk-submit>' . esc_html__('应用', 'aksrt') . '</button>'
. '</form>'; . '</form>';
} }
private function delete_form_html(int $id): string {
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-income-action"'
. ' data-aksrt-confirm="' . esc_attr(sprintf(__('确认删除待结算分成记录 #%d?删除后作者不会获得这笔分成,不可恢复。', 'aksrt'), $id)) . '">'
. '<input type="hidden" name="action" value="aksrt_pay_delete_income" />'
. '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />'
. wp_nonce_field('aksrt_pay_delete_income_' . $id, '_wpnonce', true, false)
. '<button type="submit" class="button button-small is-danger">' . esc_html__('删除', 'aksrt') . '</button></form>';
}
private function settle_form_html(int $id): string { private function settle_form_html(int $id): string {
return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-income-action"' return '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="aksrt-income-action"'
. ' data-aksrt-confirm="' . esc_attr(sprintf(__('确认立即结算分成记录 #%d?金额会转入作者钱包,不可撤销。', 'aksrt'), $id)) . '">' . ' data-aksrt-confirm="' . esc_attr(sprintf(__('确认立即结算分成记录 #%d?金额会转入作者钱包,不可撤销。', 'aksrt'), $id)) . '">'
+22
View File
@@ -171,6 +171,28 @@ class Revenue {
return ['settled' => $settled, 'failed' => $failed]; return ['settled' => $settled, 'failed' => $failed];
} }
/**
* Delete pending records. Settled records are never deleted: their amount is already
* credited to the wallet ledger, which references the record id. The status guard in
* the DELETE makes a concurrent settlement win (settle_one locks the row first).
*
* @param array<int,int> $record_ids
* @return int Number of records deleted.
*/
public function delete_pending(array $record_ids): int {
global $wpdb;
$ids = array_slice(array_values(array_unique(array_filter(array_map('intval', $record_ids), static fn (int $id): bool => $id > 0))), 0, 100);
if (!$ids) {
return 0;
}
$placeholders = implode(', ', array_fill(0, count($ids), '%d'));
$deleted = $wpdb->query($wpdb->prepare(
"DELETE FROM {$this->table()} WHERE status = %s AND id IN ({$placeholders})",
array_merge(['pending'], $ids)
));
return max(0, (int) $deleted);
}
/** /**
* Settle all pending records whose rolling earning cycle has ended. * Settle all pending records whose rolling earning cycle has ended.
* *