Compare commits

..

1 Commits

Author SHA1 Message Date
linkeyu aff250f9fd feat(admin): support backend bulk reward reviews 2026-07-23 17:55:47 +08:00
3 changed files with 40 additions and 46 deletions
+16 -11
View File
@@ -25,7 +25,8 @@ import { api } from '@/lib/api';
import { canDo } from '@/lib/auth';
import { formatWallTime } from '@/lib/format';
import { refreshReviewBadge } from '@/lib/reviewBadge';
import { runBulkAction } from '@/lib/bulkAction';
import { failedReviewIds } from '@/lib/bulkAction';
import type { BulkReviewResult } from '@/lib/bulkAction';
import { usePagedList } from '@/lib/usePagedList';
import type { Feedback, FeedbackSummary } from '@/lib/types';
import FeedbackHandleDrawer from './FeedbackHandleDrawer';
@@ -269,33 +270,37 @@ export default function FeedbacksPage() {
const targets = selectedFeedbacks;
setBulkSubmitting(true);
try {
const result = await runBulkAction(targets, (item) =>
const { data } = await api.post<BulkReviewResult>(
`/admin/api/feedbacks/bulk/${bulkAction}`,
bulkAction === 'approve'
? api.post(`/admin/api/feedbacks/${item.id}/approve`, {
? {
ids: targets.map((item) => item.id),
reward_coins: values.reward_coins,
note: values.note?.trim() || null,
reply: values.reply?.trim() || null,
})
: api.post(`/admin/api/feedbacks/${item.id}/reject`, {
}
: {
ids: targets.map((item) => item.id),
reason: values.reason.trim(),
note: values.note?.trim() || null,
reply: values.reply?.trim() || null,
}),
},
);
setSelectedRowKeys(result.failedIds);
const failedIds = failedReviewIds(data);
setSelectedRowKeys(failedIds);
setBulkAction(null);
bulkForm.resetFields();
if (result.failedIds.length) reload();
if (failedIds.length) reload();
else onPageChange(1, pageSize);
loadSummary();
refreshReviewBadge('/feedbacks');
const label = bulkAction === 'approve' ? '批量采纳' : '批量拒绝';
if (result.failedIds.length) {
if (failedIds.length) {
message.warning(
`${label}完成:成功 ${result.succeededIds.length} 条,失败 ${result.failedIds.length} 条;失败项已保留勾选`,
`${label}完成:成功 ${data.success} 条,失败 ${failedIds.length} 条;失败项已保留勾选`,
);
} else {
message.success(`${label}完成:成功 ${result.succeededIds.length}`);
message.success(`${label}完成:成功 ${data.success}`);
}
} finally {
setBulkSubmitting(false);
+10 -7
View File
@@ -23,7 +23,8 @@ import { api, errMsg } from '@/lib/api';
import { canDo } from '@/lib/auth';
import { mediaUrl } from '@/lib/media';
import { refreshReviewBadge } from '@/lib/reviewBadge';
import { runBulkAction } from '@/lib/bulkAction';
import { failedReviewIds } from '@/lib/bulkAction';
import type { BulkReviewResult } from '@/lib/bulkAction';
import { usePagedList } from '@/lib/usePagedList';
import type { PriceReport, PriceReportSummary } from '@/lib/types';
import UserRecordsDrawer from '@/components/UserRecordsDrawer';
@@ -219,10 +220,11 @@ export default function PriceReportsPage() {
content: `通过后将向每位用户发放 1000 金币,共发放 ${targets.length * 1000} 金币。请确认已逐条核实截图。`,
okText: '批量通过并发金币',
onOk: async () => {
const result = await runBulkAction(targets, (item) =>
api.post(`/admin/api/price-reports/${item.id}/approve`),
const { data } = await api.post<BulkReviewResult>(
'/admin/api/price-reports/bulk/approve',
{ ids: targets.map((item) => item.id) },
);
reportBulkResult('批量通过', result.succeededIds.length, result.failedIds);
reportBulkResult('批量通过', data.success, failedReviewIds(data));
},
});
};
@@ -268,10 +270,11 @@ export default function PriceReportsPage() {
setRejectSubmitting(true);
try {
if (bulkRejecting.length) {
const result = await runBulkAction(targets, (item) =>
api.post(`/admin/api/price-reports/${item.id}/reject`, { reason }),
const { data } = await api.post<BulkReviewResult>(
'/admin/api/price-reports/bulk/reject',
{ ids: targets.map((item) => item.id), reason },
);
reportBulkResult('批量拒绝', result.succeededIds.length, result.failedIds);
reportBulkResult('批量拒绝', data.success, failedReviewIds(data));
} else {
await api.post(`/admin/api/price-reports/${targets[0].id}/reject`, { reason });
refreshReviewBadge('/price-reports');
+14 -28
View File
@@ -1,31 +1,17 @@
export interface BulkActionResult {
succeededIds: number[];
failedIds: number[];
export interface BulkReviewItemResult {
id: number;
ok: boolean;
status: string | null;
error: string | null;
}
/**
* Run per-record write endpoints with bounded concurrency.
*
* The review endpoints already own transaction, audit and idempotency semantics,
* so the admin UI deliberately keeps using them instead of duplicating that logic.
*/
export async function runBulkAction<T extends { id: number }>(
targets: T[],
action: (target: T) => Promise<unknown>,
concurrency = 5,
): Promise<BulkActionResult> {
const succeededIds: number[] = [];
const failedIds: number[] = [];
for (let index = 0; index < targets.length; index += concurrency) {
const chunk = targets.slice(index, index + concurrency);
const settled = await Promise.allSettled(chunk.map(action));
settled.forEach((result, resultIndex) => {
const id = chunk[resultIndex].id;
if (result.status === 'fulfilled') succeededIds.push(id);
else failedIds.push(id);
});
}
return { succeededIds, failedIds };
export interface BulkReviewResult {
total: number;
success: number;
failed: number;
items: BulkReviewItemResult[];
}
export function failedReviewIds(result: BulkReviewResult): number[] {
return result.items.filter((item) => !item.ok).map((item) => item.id);
}