feat: 添加MSW模拟服务和数据源集成

refactor: 重构页面组件移除冗余Layout组件

feat: 实现WebSocket和事件总线系统

feat: 添加队列和调度系统

docs: 更新架构文档和服务映射

style: 清理重复接口定义使用数据源

chore: 更新依赖项配置

feat: 添加运行时系统和领域引导

ci: 配置ESLint边界检查规则

build: 添加Redis和WebSocket依赖

test: 添加MSW浏览器环境入口

perf: 优化数据获取逻辑使用统一数据源

fix: 修复类型定义和状态管理问题
This commit is contained in:
2026-03-19 01:39:34 +08:00
parent cd55097dbf
commit 0dac26d781
176 changed files with 47075 additions and 8404 deletions

View File

@@ -0,0 +1,277 @@
import React, { useState, useEffect } from 'react';
import { Table, Tag, Spin, Alert, Typography, Card, Button, Modal, Descriptions } from 'antd';
import { useRequest } from 'umi';
const { Title, Text } = Typography;
interface BillingRecord {
id: string;
merchantId: string;
totalAmount: number;
status: string;
createdAt: Date;
paidAt?: Date;
}
interface BillingItem {
id: string;
billingId: string;
feature: string;
amount: number;
quantity: number;
unitPrice: number;
}
const statusColorMap: Record<string, string> = {
pending: 'orange',
paid: 'green'
};
const statusTextMap: Record<string, string> = {
pending: '待支付',
paid: '已支付'
};
const featureMap: Record<string, string> = {
AI_OPTIMIZE: 'AI优化',
ADS_AUTO: '自动广告',
SYNC_INVENTORY: '库存同步',
CALCULATE_PROFIT: '利润计算'
};
export default function Billing() {
const [bills, setBills] = useState<BillingRecord[]>([]);
const [loading, setLoading] = useState(true);
const [selectedBill, setSelectedBill] = useState<BillingRecord | null>(null);
const [billItems, setBillItems] = useState<BillingItem[]>([]);
const [itemsLoading, setItemsLoading] = useState(false);
const [modalVisible, setModalVisible] = useState(false);
// 获取账单列表
const { run: fetchBills } = useRequest(async () => {
try {
// 从本地存储获取merchantId实际应用中应该从认证信息中获取
const merchantId = localStorage.getItem('merchantId') || 'anonymous';
const response = await fetch(`/api/billing?merchantId=${merchantId}`);
const data = await response.json();
setBills(data.list || []);
} catch (error) {
console.error('Error fetching bills:', error);
} finally {
setLoading(false);
}
}, {
manual: true
});
// 获取账单明细
const { run: fetchBillItems } = useRequest(async (billingId: string) => {
try {
setItemsLoading(true);
const response = await fetch(`/api/billing/items?billingId=${billingId}`);
const data = await response.json();
setBillItems(data.list || []);
} catch (error) {
console.error('Error fetching bill items:', error);
} finally {
setItemsLoading(false);
}
}, {
manual: true
});
// 初始加载账单列表
useEffect(() => {
fetchBills();
}, [fetchBills]);
// 查看账单明细
const handleViewDetails = async (bill: BillingRecord) => {
setSelectedBill(bill);
await fetchBillItems(bill.id);
setModalVisible(true);
};
// 标记账单为已支付
const handleMarkAsPaid = async (bill: BillingRecord) => {
try {
const response = await fetch(`/api/billing/paid`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ billingId: bill.id })
});
if (response.ok) {
// 刷新账单列表
fetchBills();
}
} catch (error) {
console.error('Error marking bill as paid:', error);
}
};
// 列定义
const columns = [
{
title: '账单ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '总金额',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (amount: number) => `$${amount.toFixed(2)}`,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={statusColorMap[status]}>
{statusTextMap[status]}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: Date) => new Date(date).toLocaleString(),
},
{
title: '支付时间',
dataIndex: 'paidAt',
key: 'paidAt',
render: (date: Date) => date ? new Date(date).toLocaleString() : '-',
},
{
title: '操作',
key: 'action',
render: (_: any, record: BillingRecord) => (
<div>
<Button
type="link"
onClick={() => handleViewDetails(record)}
style={{ marginRight: '8px' }}
>
</Button>
{record.status === 'pending' && (
<Button
type="primary"
onClick={() => handleMarkAsPaid(record)}
>
</Button>
)}
</div>
),
},
];
// 计算总金额
const totalAmount = bills.reduce((sum, bill) => sum + bill.totalAmount, 0);
const pendingAmount = bills.filter(bill => bill.status === 'pending').reduce((sum, bill) => sum + bill.totalAmount, 0);
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Card style={{ marginBottom: '24px' }}>
<Descriptions bordered>
<Descriptions.Item label="总账单金额">${totalAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="待支付金额">${pendingAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="已支付金额">${(totalAmount - pendingAmount).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="账单数量">{bills.length}</Descriptions.Item>
</Descriptions>
</Card>
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<Spin size="large" />
</div>
) : bills.length === 0 ? (
<Alert message="暂无账单记录" type="info" />
) : (
<Table
rowKey="id"
columns={columns}
dataSource={bills}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 1000 }}
/>
)}
{/* 账单明细 modal */}
<Modal
title="账单明细"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={[
<Button key="close" onClick={() => setModalVisible(false)}>
</Button>
]}
width={800}
>
{selectedBill && (
<div>
<Descriptions bordered style={{ marginBottom: '24px' }}>
<Descriptions.Item label="账单ID">{selectedBill.id}</Descriptions.Item>
<Descriptions.Item label="总金额">${selectedBill.totalAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={statusColorMap[selectedBill.status]}>
{statusTextMap[selectedBill.status]}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="创建时间">{new Date(selectedBill.createdAt).toLocaleString()}</Descriptions.Item>
<Descriptions.Item label="支付时间">{selectedBill.paidAt ? new Date(selectedBill.paidAt).toLocaleString() : '-'}</Descriptions.Item>
</Descriptions>
<Title level={4}></Title>
{itemsLoading ? (
<Spin />
) : billItems.length === 0 ? (
<Alert message="暂无明细记录" type="info" />
) : (
<Table
rowKey="id"
columns={[
{
title: '功能',
dataIndex: 'feature',
render: (feature: string) => featureMap[feature] || feature,
},
{
title: '数量',
dataIndex: 'quantity',
},
{
title: '单价',
dataIndex: 'unitPrice',
render: (price: number) => `$${price.toFixed(2)}`,
},
{
title: '金额',
dataIndex: 'amount',
render: (amount: number) => `$${amount.toFixed(2)}`,
},
]}
dataSource={billItems}
pagination={false}
/>
)}
</div>
)}
</Modal>
</div>
);
}

View File

@@ -0,0 +1,176 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Spin, Alert, Typography, Card } from 'antd';
import { useRequest } from 'umi';
const { Title, Text } = Typography;
interface Event {
id: string;
type: string;
payload: any;
timestamp: number;
source: string;
merchantId?: string;
}
const eventTypeMap: Record<string, string> = {
PRODUCT_CREATED: '商品创建',
PRODUCT_UPDATED: '商品更新',
ORDER_CREATED: '订单创建',
ORDER_PAID: '订单支付',
ORDER_COMPLETED: '订单完成',
INVENTORY_LOW: '库存不足',
INVENTORY_UPDATED: '库存更新',
FEATURE_ENABLED: '功能启用',
FEATURE_DISABLED: '功能禁用',
AI_TASK_CREATED: 'AI任务创建',
AI_TASK_COMPLETED: 'AI任务完成',
AD_STARTED: '广告开始',
AD_PERFORMANCE_UPDATED: '广告性能更新',
JOB_CREATED: '任务创建',
JOB_UPDATED: '任务更新',
JOB_COMPLETED: '任务完成',
JOB_FAILED: '任务失败',
BILLING_GENERATED: '账单生成'
};
export default function EventLog() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [ws, setWs] = useState<WebSocket | null>(null);
// 获取事件日志
const { run: fetchEvents } = useRequest(async () => {
try {
const response = await fetch('/api/events');
const data = await response.json();
setEvents(data.list || []);
} catch (error) {
console.error('Error fetching events:', error);
} finally {
setLoading(false);
}
}, {
manual: true
});
// 初始化WebSocket连接
useEffect(() => {
// 从本地存储获取merchantId实际应用中应该从认证信息中获取
const merchantId = localStorage.getItem('merchantId') || 'anonymous';
// 建立WebSocket连接
const socket = new WebSocket(`ws://localhost:8080?merchantId=${merchantId}`);
socket.onopen = () => {
console.log('WebSocket connected');
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 处理任何事件,刷新事件列表
if (data.type !== 'CONNECTION_SUCCESS') {
fetchEvents();
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
socket.onclose = () => {
console.log('WebSocket disconnected');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
setWs(socket);
// 组件卸载时关闭WebSocket连接
return () => {
socket.close();
};
}, [fetchEvents]);
// 初始加载事件列表
useEffect(() => {
fetchEvents();
}, [fetchEvents]);
// 列定义
const columns = [
{
title: '事件类型',
dataIndex: 'type',
key: 'type',
render: (type: string) => eventTypeMap[type] || type,
},
{
title: '来源',
dataIndex: 'source',
key: 'source',
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
render: (timestamp: number) => new Date(timestamp).toLocaleString(),
sorter: (a: Event, b: Event) => a.timestamp - b.timestamp,
defaultSortOrder: 'descend',
},
{
title: '商户ID',
dataIndex: 'merchantId',
key: 'merchantId',
render: (merchantId: string) => merchantId || '-',
},
{
title: '详情',
key: 'details',
render: (_: any, record: Event) => (
<Text
onClick={() => {
// 查看事件详情
console.log('Event details:', record);
}}
style={{ color: '#1890ff', cursor: 'pointer' }}
>
</Text>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Card style={{ marginBottom: '24px' }}>
<Text></Text>
</Card>
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<Spin size="large" />
</div>
) : events.length === 0 ? (
<Alert message="暂无事件日志" type="info" />
) : (
<Table
rowKey="id"
columns={columns}
dataSource={events}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 800 }}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,233 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Spin, Alert, Typography, Card, Row, Col } from 'antd';
import { useRequest } from 'umi';
const { Title, Text } = Typography;
interface Job {
id: string;
type: string;
status: 'pending' | 'running' | 'success' | 'failed';
payload: any;
result?: any;
retryCount: number;
merchantId?: string;
createdAt: number;
updatedAt: number;
}
const statusColorMap: Record<string, string> = {
pending: 'orange',
running: 'blue',
success: 'green',
failed: 'red'
};
const statusTextMap: Record<string, string> = {
pending: '待处理',
running: '处理中',
success: '已完成',
failed: '失败'
};
const jobTypeMap: Record<string, string> = {
AI_OPTIMIZE_PRODUCT: 'AI优化商品',
SYNC_INVENTORY: '同步库存',
RUN_ADS: '运行广告',
CALCULATE_PROFIT: '计算利润',
GENERATE_BILL: '生成账单',
UPDATE_AD_BUDGET: '更新广告预算',
STOP_AD: '停止广告'
};
export default function TaskCenter() {
const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(true);
const [ws, setWs] = useState<WebSocket | null>(null);
// 获取任务列表
const { run: fetchJobs } = useRequest(async () => {
try {
const response = await fetch('/api/jobs');
const data = await response.json();
setJobs(data.list || []);
} catch (error) {
console.error('Error fetching jobs:', error);
} finally {
setLoading(false);
}
}, {
manual: true
});
// 初始化WebSocket连接
useEffect(() => {
// 从本地存储获取merchantId实际应用中应该从认证信息中获取
const merchantId = localStorage.getItem('merchantId') || 'anonymous';
// 建立WebSocket连接
const socket = new WebSocket(`ws://localhost:8080?merchantId=${merchantId}`);
socket.onopen = () => {
console.log('WebSocket connected');
};
socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 处理任务更新事件
if (data.type === 'JOB_UPDATED' || data.type === 'JOB_COMPLETED' || data.type === 'JOB_FAILED') {
fetchJobs();
}
// 处理AI任务完成事件
if (data.type === 'AI_TASK_COMPLETED') {
fetchJobs();
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
socket.onclose = () => {
console.log('WebSocket disconnected');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
setWs(socket);
// 组件卸载时关闭WebSocket连接
return () => {
socket.close();
};
}, [fetchJobs]);
// 初始加载任务列表
useEffect(() => {
fetchJobs();
}, [fetchJobs]);
// 列定义
const columns = [
{
title: '任务ID',
dataIndex: 'id',
key: 'id',
ellipsis: true,
},
{
title: '任务类型',
dataIndex: 'type',
key: 'type',
render: (type: string) => jobTypeMap[type] || type,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={statusColorMap[status]}>
{statusTextMap[status]}
</Tag>
),
},
{
title: '重试次数',
dataIndex: 'retryCount',
key: 'retryCount',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (timestamp: number) => new Date(timestamp).toLocaleString(),
},
{
title: '更新时间',
dataIndex: 'updatedAt',
key: 'updatedAt',
render: (timestamp: number) => new Date(timestamp).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: Job) => (
<Text
onClick={() => {
// 查看任务详情
console.log('Job details:', record);
}}
style={{ color: '#1890ff', cursor: 'pointer' }}
>
</Text>
),
},
];
// 统计数据
const stats = {
total: jobs.length,
pending: jobs.filter(job => job.status === 'pending').length,
running: jobs.filter(job => job.status === 'running').length,
success: jobs.filter(job => job.status === 'success').length,
failed: jobs.filter(job => job.status === 'failed').length,
};
return (
<div style={{ padding: '24px' }}>
<Title level={2}></Title>
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
<Col span={6}>
<Card>
<Text strong></Text>
<div style={{ fontSize: '24px', marginTop: '8px' }}>{stats.total}</div>
</Card>
</Col>
<Col span={6}>
<Card>
<Text strong></Text>
<div style={{ fontSize: '24px', marginTop: '8px', color: statusColorMap.pending }}>{stats.pending}</div>
</Card>
</Col>
<Col span={6}>
<Card>
<Text strong></Text>
<div style={{ fontSize: '24px', marginTop: '8px', color: statusColorMap.running }}>{stats.running}</div>
</Card>
</Col>
<Col span={6}>
<Card>
<Text strong></Text>
<div style={{ fontSize: '24px', marginTop: '8px', color: statusColorMap.success }}>{stats.success}</div>
</Card>
</Col>
</Row>
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<Spin size="large" />
</div>
) : jobs.length === 0 ? (
<Alert message="暂无任务" type="info" />
) : (
<Table
rowKey="id"
columns={columns}
dataSource={jobs}
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
}}
scroll={{ x: 1000 }}
/>
)}
</div>
);
}