refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态

This commit is contained in:
ddCat
2026-09-09 22:02:10 +08:00
parent bacd502f5c
commit b5f3413ded
298 changed files with 12681 additions and 26428 deletions

View File

@@ -113,7 +113,7 @@ export default function ChannelsPage() {
const sequence = ++requestSequence.current;
setLoading(true);
try {
const result = await listAdminChannels({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, limit: pageSize });
const result = await listAdminChannels({ keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, pageSize });
if (sequence !== requestSequence.current) return;
setChannels(result.channels);
setTotal(result.total);

View File

@@ -121,7 +121,7 @@ export default function AdminAnnouncementsPanel({
keyword: queryKeyword || undefined,
status: queryStatus === "all" ? undefined : queryStatus,
page: targetPage,
limit: targetPageSize,
pageSize: targetPageSize,
}),
targetPage,
targetPageSize,
@@ -1024,13 +1024,13 @@ function assertAnnouncementMutationResult(
}
}
function assertAnnouncementListResult(result: unknown, expectedPage: number, expectedLimit: number) {
if (!isRecord(result) || !Array.isArray(result.announcements) || !Number.isInteger(result.total) || (result.total as number) < 0 || result.page !== expectedPage || result.limit !== expectedLimit) {
function assertAnnouncementListResult(result: unknown, expectedPage: number, expectedPageSize: number) {
if (!isRecord(result) || !Array.isArray(result.announcements) || !Number.isInteger(result.total) || (result.total as number) < 0 || result.page !== expectedPage || result.pageSize !== expectedPageSize) {
throw new Error("公告列表返回格式不完整");
}
const announcements = result.announcements.map((value) => normalizeAnnouncementListItem(value));
if ((result.total as number) < announcements.length) throw new Error("公告列表总数与当前页数据不一致");
return { announcements, total: result.total as number, page: expectedPage, limit: expectedLimit };
return { announcements, total: result.total as number, page: expectedPage, pageSize: expectedPageSize };
}
function normalizeAnnouncementListItem(value: unknown): SystemAnnouncement {
@@ -1076,9 +1076,9 @@ async function inspectPendingReview(review: AnnouncementPendingReview) {
for (const title of queryTitles) {
let targetPage = 1;
while (targetPage <= 50) {
const data = assertAnnouncementListResult(await listAdminAnnouncements({ keyword: title, page: targetPage, limit: 100 }), targetPage, 100);
const data = assertAnnouncementListResult(await listAdminAnnouncements({ keyword: title, page: targetPage, pageSize: 100 }), targetPage, 100);
data.announcements.forEach((announcement) => candidates.set(announcement.id, announcement));
if (targetPage >= Math.max(1, Math.ceil(data.total / data.limit))) break;
if (targetPage >= Math.max(1, Math.ceil(data.total / data.pageSize))) break;
if (targetPage === 50) throw new Error("同名匹配记录过多,无法安全定位目标;请稍后重试。");
targetPage += 1;
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { App, Button, Descriptions, Drawer, Progress, Skeleton, Tabs } from "antd";
import { App, Button, Descriptions, Progress, Skeleton, Tabs } from "antd";
import { AppDrawer } from "@/components/ui/product/app-drawer";
import { IconButton } from "@/components/ui/base/buttons";
import { EmptyState } from "@/components/ui/product/empty-state";
import { ChevronLeft, ChevronRight } from "lucide-react";
@@ -46,7 +47,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
useEffect(() => {
if (!userId) return;
let active = true;
void listAdminUserLedger(userId, { page: ledgerPage, limit: 20 })
void listAdminUserLedger(userId, { page: ledgerPage, pageSize: 20 })
.then((result) => {
if (active) {
setLedger(result.entries);
@@ -61,7 +62,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
useEffect(() => {
if (!userId) return;
let active = true;
void listAdminUserTasks(userId, { page: taskPage, limit: 20 })
void listAdminUserTasks(userId, { page: taskPage, pageSize: 20 })
.then((result) => {
if (active) {
setTasks(result.tasks);
@@ -76,7 +77,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
useEffect(() => {
if (!userId) return;
let active = true;
void listAdminUserAuditEvents(userId, { page: auditPage, limit: 20 })
void listAdminUserAuditEvents(userId, { page: auditPage, pageSize: 20 })
.then((result) => {
if (active) {
setEvents(result.events);
@@ -90,12 +91,11 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
}, [auditPage, message, userId]);
return (
<Drawer
<AppDrawer
title={detail ? `${detail.user.displayName || detail.user.username} · 用户详情` : "用户详情"}
open={Boolean(userId)}
onClose={onClose}
size="min(920px, 100vw)"
destroyOnHidden
rootClassName="admin-drawer"
extra={onNavigate ? (
<div className="flex items-center gap-1">
@@ -229,7 +229,7 @@ export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUse
) : (
<EmptyState size="compact" title="没有用户详情" />
)}
</Drawer>
</AppDrawer>
);
}

View File

@@ -134,7 +134,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
const searchUsers = async (keyword: string) => {
setSearchingUsers(true);
try {
const result = await listAdminUsers({ keyword: keyword.trim() || undefined, page: 1, limit: 50 });
const result = await listAdminUsers({ keyword: keyword.trim() || undefined, page: 1, pageSize: 50 });
setUserOptions(result.users);
} catch (error) {
message.error(error instanceof Error ? error.message : "搜索用户失败");

View File

@@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { Alert, App, Button, Form, Input, Modal, Segmented, Switch, Tabs } from "antd";
import { Alert, App, Button, Form, Input, Segmented, Switch, Tabs } from "antd";
import { AppModal } from "@/components/ui/product/app-modal";
import { FlaskConical, Plus } from "lucide-react";
import { ModelIconPicker } from "@/components/model-logo";
import { ModelIcon } from "@/components/model-picker";
@@ -166,7 +167,7 @@ export function ChannelModelEditor({
};
return (
<Modal
<AppModal
open
title={
<div className="admin-model-editor-title">
@@ -183,7 +184,7 @@ export function ChannelModelEditor({
keyboard={!busy}
closable={!busy}
onCancel={requestClose}
styles={{ container: { padding: 0, overflow: "hidden" }, body: { padding: 0, minHeight: 0, flex: 1 }, header: { margin: 0 }, footer: { margin: 0 } }}
flush styles={{ body: { minHeight: 0, flex: 1 }, header: { margin: 0 }, footer: { margin: 0 } }}
footer={
<div className="admin-model-editor-footer-actions">
<div className="admin-model-editor-footer-status">
@@ -396,7 +397,7 @@ export function ChannelModelEditor({
]}
/>
</Form>
</Modal>
</AppModal>
);
}

View File

@@ -1,6 +1,7 @@
import { App, Button, Empty, Modal, Spin } from "antd";
import { App, Button, Modal, Spin } from "antd";
import { ArrowDown, ArrowUp, GripVertical, ListOrdered } from "lucide-react";
import { useRef, useState } from "react";
import { EmptyState } from "@/components/ui/product/empty-state";
import { getChannelOrder, saveChannelOrder, type ChannelOrderItem } from "@/services/api/channel-order";
export function moveOrderItem<T extends { id: string }>(items: T[], id: string, target: number): T[] {
@@ -86,7 +87,7 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
<Spin spinning={loading}>
<div className="max-h-[55vh] overflow-y-auto space-y-2" role="list" aria-label="展示顺序">
{!loading && !items.length ? (
<Empty description="暂无可排序条目" />
<EmptyState size="compact" title="暂无可排序条目" />
) : (
items.map((item, index) => (
<div

View File

@@ -73,7 +73,7 @@ export default function CreditOperationsPanel({ users, activeOperation, onOperat
keyword: debouncedKeyword || undefined,
status: orderStatus,
page: targetPage,
limit: targetPageSize,
pageSize: targetPageSize,
});
if (requestId !== ordersRequestRef.current) return;
if (targetPage > 1 && result.total > 0 && result.orders.length === 0) {
@@ -134,7 +134,7 @@ export default function CreditOperationsPanel({ users, activeOperation, onOperat
if (activeOperation !== "adjustment") return;
const requestId = ++userSearchRequestRef.current;
setSearchingUsers(true);
void listAdminUsers({ keyword: debouncedAdjustmentSearch.trim() || undefined, page: 1, limit: 50 })
void listAdminUsers({ keyword: debouncedAdjustmentSearch.trim() || undefined, page: 1, pageSize: 50 })
.then((result) => {
if (requestId !== userSearchRequestRef.current) return;
const selectedId = adjustmentForm.getFieldValue("userId");

View File

@@ -79,7 +79,7 @@ export default function RedemptionCodesPanel({ createOpen, onCreateOpenChange, o
keyword: queryKeyword || undefined,
validity: queryValidity === "all" ? undefined : queryValidity,
page: targetPage,
limit: targetPageSize,
pageSize: targetPageSize,
});
if (requestId !== listRequestRef.current) return false;
const lastPage = Math.max(1, Math.ceil(result.total / targetPageSize));
@@ -641,7 +641,7 @@ function RedeemBatchCodesModal({ batch, onClose, onBatchChanged }: { batch: Rede
setCodes([]);
setTotal(0);
try {
const result = await listAdminRedeemBatchCodes(batch.id, { status: targetStatus === "all" ? undefined : targetStatus, page: targetPage, limit: targetPageSize });
const result = await listAdminRedeemBatchCodes(batch.id, { status: targetStatus === "all" ? undefined : targetStatus, page: targetPage, pageSize: targetPageSize });
if (requestId !== requestRef.current) return false;
const lastPage = Math.max(1, Math.ceil(result.total / targetPageSize));
if (targetPage > lastPage) {

View File

@@ -69,7 +69,7 @@ export default function StorageResourcesPanel() {
provider: provider === "all" ? undefined : provider,
userId: debouncedUserId || undefined,
page,
limit: pageSize,
pageSize: pageSize,
},
controller.signal,
)

View File

@@ -93,8 +93,8 @@ export default function LogicalModelsPage() {
const reload = async () => {
setLoading(true);
try {
const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, limit: 100 })]);
const remainingChannelPages = await Promise.all(Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.limit) - 1) }, (_, index) => listAdminChannels({ page: index + 2, limit: firstChannelPage.limit })));
const [modelResult, firstChannelPage] = await Promise.all([listAdminLogicalModels(), listAdminChannels({ page: 1, pageSize: 100 })]);
const remainingChannelPages = await Promise.all(Array.from({ length: Math.max(0, Math.ceil(firstChannelPage.total / firstChannelPage.pageSize) - 1) }, (_, index) => listAdminChannels({ page: index + 2, pageSize: firstChannelPage.pageSize })));
const channels = [firstChannelPage, ...remainingChannelPages].flatMap((result) => result.channels);
const channelModelResults = await Promise.all(channels.map((channel) => listAdminChannelModels(channel.id)));
setModels(modelResult.models);

View File

@@ -46,7 +46,7 @@ export default function LogsPage() {
useEffect(() => {
const sequence = ++requestSequence.current;
setLoading(true);
void listAdminApiLogs({ recordType, keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, limit: pageSize })
void listAdminApiLogs({ recordType, keyword: debouncedKeyword || undefined, status: status === "all" ? undefined : status, page, pageSize })
.then((result) => {
if (sequence !== requestSequence.current) return;
setLogs(result.logs);

View File

@@ -121,14 +121,14 @@ export default function AdminPaymentsPage() {
}
};
const loadOrders = async (page = orderPage, limit = orderPageSize) => {
const loadOrders = async (page = orderPage, pageSize = orderPageSize) => {
setOrdersLoading(true);
try {
const result = await listAdminPaymentOrders({ status: orderStatusFilter === "all" ? undefined : orderStatusFilter, keyword: orderKeyword.trim() || undefined, page, limit });
const result = await listAdminPaymentOrders({ status: orderStatusFilter === "all" ? undefined : orderStatusFilter, keyword: orderKeyword.trim() || undefined, page, pageSize });
setOrders(result.orders);
setOrderTotal(result.total);
setOrderPage(result.page);
setOrderPageSize(result.limit);
setOrderPageSize(result.pageSize);
} catch (error) {
message.error(error instanceof Error ? error.message : "读取支付订单失败");
} finally {
@@ -136,14 +136,14 @@ export default function AdminPaymentsPage() {
}
};
const loadRuns = async (page = runPage, limit = runPageSize) => {
const loadRuns = async (page = runPage, pageSize = runPageSize) => {
setRunsLoading(true);
try {
const result = await listAdminPaymentReconciliations({ providerId: runProviderFilter === "all" ? undefined : runProviderFilter, page, limit });
const result = await listAdminPaymentReconciliations({ providerId: runProviderFilter === "all" ? undefined : runProviderFilter, page, pageSize });
setRuns(result.runs);
setRunTotal(result.total);
setRunPage(result.page);
setRunPageSize(result.limit);
setRunPageSize(result.pageSize);
} catch (error) {
message.error(error instanceof Error ? error.message : "读取对账记录失败");
} finally {
@@ -287,16 +287,16 @@ export default function AdminPaymentsPage() {
}
};
const openRunDetails = async (run: PaymentReconciliationRun, page = 1, limit = detailPageSize, result = detailResult) => {
const openRunDetails = async (run: PaymentReconciliationRun, page = 1, pageSize = detailPageSize, result = detailResult) => {
setDetailRun(run);
setDetailLoading(true);
try {
const response = await listAdminPaymentReconciliationItems(run.id, { result: result === "all" ? undefined : result, page, limit });
const response = await listAdminPaymentReconciliationItems(run.id, { result: result === "all" ? undefined : result, page, pageSize });
setDetailRun(response.run);
setDetailItems(response.items);
setDetailTotal(response.total);
setDetailPage(response.page);
setDetailPageSize(response.limit);
setDetailPageSize(response.pageSize);
} catch (error) {
message.error(error instanceof Error ? error.message : "读取对账明细失败");
} finally {

View File

@@ -1,4 +1,4 @@
import { App, Button, Drawer, Form, Input, Popconfirm, Select, Tabs } from "antd";
import { App, Button, Form, Input, Popconfirm, Select, Tabs } from "antd";
import { StatusBadge } from "@/components/ui/base/badges";
import { Callout } from "@/components/ui/product/callout";
import type { ColumnsType } from "antd/es/table";
@@ -7,6 +7,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useSearchParams } from "react-router";
import { PaginationBar } from "@/components/layout/workspace-page";
import { AppDrawer } from "@/components/ui/product/app-drawer/app-drawer";
import { PromptCodeEditor, type PromptCodeEditorHandle } from "@/components/prompt/prompt-code-editor";
import {
createAdminPromptTemplate,
@@ -187,7 +188,8 @@ export default function StoryboardPromptsPage() {
footer={<PaginationBar alwaysShow current={page} pageSize={pageSize} total={filtered.length} onChange={(nextPage, nextPageSize) => { setPage(nextPageSize !== pageSize ? 1 : nextPage); setPageSize(nextPageSize); }} />}
/>
<Drawer
<AppDrawer
flush
title={baseTemplate ? `基于 v${baseTemplate.version} 新建版本` : "新建提示词版本"}
open={drawerOpen}
size="min(1180px, 100vw)"
@@ -196,8 +198,6 @@ export default function StoryboardPromptsPage() {
closable={false}
mask={{ closable: false }}
keyboard={false}
destroyOnHidden
styles={{ body: { padding: 0 } }}
extra={<div className="flex gap-2"><Popconfirm disabled={!dirty} title="放弃模板修改?" description="尚未保存的新版本内容将丢失。" okText="放弃修改" cancelText="继续编辑" okButtonProps={{ danger: true }} onConfirm={closeDrawer}><Button disabled={saving} onClick={() => { if (!dirty) closeDrawer(); }}>关闭</Button></Popconfirm><Button type="primary" loading={saving} disabled={!draftOperation || !draftName.trim() || !editorContent.trim()} onClick={() => void save()}>保存版本</Button></div>}
>
<Form form={form} layout="vertical" requiredMark={false} className="flex min-h-full flex-col">
@@ -253,7 +253,7 @@ export default function StoryboardPromptsPage() {
</aside>
</div>
</Form>
</Drawer>
</AppDrawer>
</AdminPageFrame>
);
}

View File

@@ -57,7 +57,7 @@ export default function UsersPanel({ onUserChanged }: { onUserChanged?: (user: L
role: state.role === "all" ? undefined : state.role,
status: state.status === "all" ? undefined : state.status,
page: state.page,
limit: state.pageSize,
pageSize: state.pageSize,
})
.then((result) => {
if (sequence !== requestSequence.current) return;