refactor: 优化代码结构和类型定义

feat(types): 添加express.d.ts类型引用
style: 格式化express.d.ts中的接口定义
refactor: 移除未使用的AntFC类型导入
chore: 删除自动生成的.umi-production文件
feat: 添加店铺管理相关表和初始化脚本
docs: 更新安全规则和交互指南文档
refactor: 统一使用FC类型替代React.FC
perf: 优化图表组件导入方式
style: 添加.prettierrc配置文件
refactor: 调整组件导入顺序和结构
feat: 添加平台库存管理路由
fix: 修复订单同步时的库存检查逻辑
docs: 更新RBAC设计和租户管理文档
refactor: 优化部门控制器代码
This commit is contained in:
2026-03-30 01:20:57 +08:00
parent d327706087
commit 1b14947e7b
106 changed files with 11251 additions and 38565 deletions

View File

@@ -0,0 +1,517 @@
/**
* [FE-MON003] 用户行为分析服务
* @description 事件追踪、页面访问追踪、用户行为分析
* @version 1.0
*/
import { http } from './http';
export interface UserBehaviorEvent {
tenantId: string;
shopId: string;
userId: string;
sessionId: string;
eventType: EventType;
eventName: string;
eventData: Record<string, unknown>;
timestamp: string;
url: string;
referrer: string;
userAgent: string;
viewport: { width: number; height: number };
language: string;
}
export type EventType =
| 'page_view'
| 'page_exit'
| 'click'
| 'scroll'
| 'form_submit'
| 'form_change'
| 'search'
| 'error'
| 'custom'
| 'api_call'
| 'user_action';
export interface PageViewEvent {
path: string;
title: string;
duration?: number;
scrollDepth?: number;
}
export interface ClickEvent {
element: string;
text?: string;
x: number;
y: number;
}
export interface SearchEvent {
query: string;
results?: number;
filters?: Record<string, unknown>;
}
export interface UserSession {
sessionId: string;
startTime: string;
endTime?: string;
pageViews: number;
events: number;
device: DeviceInfo;
}
export interface DeviceInfo {
type: 'desktop' | 'tablet' | 'mobile';
os: string;
browser: string;
screenWidth: number;
screenHeight: number;
}
interface BehaviorAnalyticsConfig {
enabled: boolean;
trackPageViews: boolean;
trackClicks: boolean;
trackScrolls: boolean;
trackForms: boolean;
trackSearch: boolean;
trackApiCalls: boolean;
scrollThreshold: number;
reportInterval: number;
batchSize: number;
samplingRate: number;
}
const DEFAULT_CONFIG: BehaviorAnalyticsConfig = {
enabled: true,
trackPageViews: true,
trackClicks: true,
trackScrolls: true,
trackForms: true,
trackSearch: true,
trackApiCalls: true,
scrollThreshold: 0.25,
reportInterval: 15000,
batchSize: 20,
samplingRate: 1.0,
};
class UserBehaviorAnalyticsService {
private config: BehaviorAnalyticsConfig;
private eventQueue: UserBehaviorEvent[] = [];
private sessionId: string;
private sessionStartTime: string;
private currentPageViewStart: number = 0;
private maxScrollDepth: number = 0;
private isInitialized = false;
private reportInterval: NodeJS.Timeout | null = null;
private scrollThresholds: Set<number> = new Set();
private lastPagePath: string = '';
constructor(config: Partial<BehaviorAnalyticsConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.sessionId = this.generateSessionId();
this.sessionStartTime = new Date().toISOString();
}
private generateSessionId(): string {
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
init(): void {
if (this.isInitialized) return;
this.isInitialized = true;
if (Math.random() > this.config.samplingRate) {
this.config.enabled = false;
return;
}
if (this.config.trackPageViews) {
this.trackPageViews();
}
if (this.config.trackClicks) {
this.trackClicks();
}
if (this.config.trackScrolls) {
this.trackScrolls();
}
if (this.config.trackForms) {
this.trackForms();
}
if (this.config.trackSearch) {
this.trackSearch();
}
this.startPeriodicReport();
window.addEventListener('beforeunload', () => {
this.trackPageExit();
this.flushEvents();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.trackPageExit();
this.flushEvents();
} else if (document.visibilityState === 'visible') {
this.currentPageViewStart = Date.now();
}
});
console.log('[BehaviorAnalytics] Initialized');
}
private trackPageViews(): void {
this.currentPageViewStart = Date.now();
this.lastPagePath = window.location.pathname;
this.trackEvent('page_view', 'page_load', {
path: window.location.pathname,
title: document.title,
search: window.location.search,
hash: window.location.hash,
});
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
const self = this;
history.pushState = function(...args) {
self.trackPageExit();
originalPushState.apply(this, args);
self.handleRouteChange();
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
self.handleRouteChange();
};
window.addEventListener('popstate', () => {
this.trackPageExit();
this.handleRouteChange();
});
}
private handleRouteChange(): void {
this.maxScrollDepth = 0;
this.scrollThresholds.clear();
this.currentPageViewStart = Date.now();
this.lastPagePath = window.location.pathname;
this.trackEvent('page_view', 'route_change', {
path: window.location.pathname,
title: document.title,
search: window.location.search,
});
}
private trackPageExit(): void {
const duration = Date.now() - this.currentPageViewStart;
this.trackEvent('page_exit', 'page_unload', {
path: this.lastPagePath,
duration,
scrollDepth: this.maxScrollDepth,
});
}
private trackClicks(): void {
document.addEventListener('click', (event) => {
const target = event.target as Element;
const clickData = {
element: this.getElementPath(target),
tagName: target.tagName.toLowerCase(),
text: target.textContent?.substring(0, 100),
className: target.className,
id: target.id,
href: (target as HTMLAnchorElement).href,
x: event.clientX,
y: event.clientY,
timestamp: Date.now(),
};
this.trackEvent('click', 'element_click', clickData);
}, true);
}
private trackScrolls(): void {
let scrollTimeout: NodeJS.Timeout | null = null;
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = docHeight > 0 ? (scrollTop / docHeight) : 0;
if (scrollPercent > this.maxScrollDepth) {
this.maxScrollDepth = scrollPercent;
}
const threshold = Math.floor(scrollPercent / this.config.scrollThreshold) * this.config.scrollThreshold;
if (!this.scrollThresholds.has(threshold) && threshold > 0) {
this.scrollThresholds.add(threshold);
this.trackEvent('scroll', 'scroll_depth', {
depth: threshold,
scrollTop,
docHeight,
});
}
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(() => {
this.trackEvent('scroll', 'scroll_end', {
maxScrollDepth: this.maxScrollDepth,
finalScrollTop: scrollTop,
});
}, 500);
};
window.addEventListener('scroll', handleScroll, { passive: true });
}
private trackForms(): void {
document.addEventListener('submit', (event) => {
const form = event.target as HTMLFormElement;
this.trackEvent('form_submit', 'form_submission', {
formId: form.id,
formName: form.name,
formAction: form.action,
formMethod: form.method,
fieldCount: form.elements.length,
});
}, true);
document.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
if (target.tagName === 'INPUT' || target.tagName === 'SELECT' || target.tagName === 'TEXTAREA') {
this.trackEvent('form_change', 'field_change', {
fieldType: target.type,
fieldName: target.name,
fieldId: target.id,
formId: (target.form as HTMLFormElement)?.id,
});
}
}, true);
}
private trackSearch(): void {
const searchInputs = document.querySelectorAll('input[type="search"], input[placeholder*="搜索"], input[placeholder*="search"]');
searchInputs.forEach((input) => {
let searchTimeout: NodeJS.Timeout | null = null;
input.addEventListener('input', (event) => {
const target = event.target as HTMLInputElement;
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(() => {
if (target.value.length >= 2) {
this.trackEvent('search', 'search_input', {
query: target.value,
inputId: target.id,
inputName: target.name,
});
}
}, 500);
});
});
}
private getElementPath(element: Element): string {
const path: string[] = [];
let current: Element | null = element;
while (current && current !== document.body) {
let selector = current.tagName.toLowerCase();
if (current.id) {
selector += `#${current.id}`;
} else if (current.className) {
const classes = current.className.split(' ').filter(Boolean).slice(0, 2);
if (classes.length > 0) {
selector += `.${classes.join('.')}`;
}
}
path.unshift(selector);
current = current.parentElement;
}
return path.join(' > ');
}
trackEvent(eventType: EventType, eventName: string, eventData: Record<string, unknown> = {}): void {
if (!this.config.enabled) return;
const tenantId = localStorage.getItem('tenantId') || 'default';
const shopId = localStorage.getItem('shopId') || 'default';
const userId = localStorage.getItem('userId') || 'anonymous';
const event: UserBehaviorEvent = {
tenantId,
shopId,
userId,
sessionId: this.sessionId,
eventType,
eventName,
eventData,
timestamp: new Date().toISOString(),
url: window.location.href,
referrer: document.referrer,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
language: navigator.language,
};
this.eventQueue.push(event);
if (this.eventQueue.length >= this.config.batchSize) {
this.flushEvents();
}
}
trackCustomEvent(eventName: string, eventData: Record<string, unknown> = {}): void {
this.trackEvent('custom', eventName, eventData);
}
trackUserAction(action: string, details: Record<string, unknown> = {}): void {
this.trackEvent('user_action', action, details);
}
trackApiCall(endpoint: string, method: string, duration: number, status: number): void {
if (!this.config.trackApiCalls) return;
this.trackEvent('api_call', 'api_request', {
endpoint,
method,
duration,
status,
success: status >= 200 && status < 300,
});
}
private startPeriodicReport(): void {
this.reportInterval = setInterval(() => {
if (this.eventQueue.length > 0) {
this.flushEvents();
}
}, this.config.reportInterval);
}
private async flushEvents(): Promise<void> {
if (this.eventQueue.length === 0) return;
const events = [...this.eventQueue];
this.eventQueue = [];
try {
await http.post('/api/telemetry/behavior/batch', {
events,
session: {
sessionId: this.sessionId,
startTime: this.sessionStartTime,
pageViews: events.filter(e => e.eventType === 'page_view').length,
events: events.length,
},
});
console.log('[BehaviorAnalytics] Events reported successfully');
} catch (error) {
console.error('[BehaviorAnalytics] Failed to report events:', error);
this.eventQueue.unshift(...events);
}
}
getSession(): UserSession {
return {
sessionId: this.sessionId,
startTime: this.sessionStartTime,
pageViews: this.eventQueue.filter(e => e.eventType === 'page_view').length,
events: this.eventQueue.length,
device: this.getDeviceInfo(),
};
}
private getDeviceInfo(): DeviceInfo {
const ua = navigator.userAgent;
let type: DeviceInfo['type'] = 'desktop';
let os = 'unknown';
let browser = 'unknown';
if (/mobile/i.test(ua)) {
type = 'mobile';
} else if (/tablet/i.test(ua)) {
type = 'tablet';
}
if (/windows/i.test(ua)) os = 'Windows';
else if (/mac/i.test(ua)) os = 'MacOS';
else if (/linux/i.test(ua)) os = 'Linux';
else if (/android/i.test(ua)) os = 'Android';
else if (/ios|iphone|ipad/i.test(ua)) os = 'iOS';
if (/chrome/i.test(ua)) browser = 'Chrome';
else if (/firefox/i.test(ua)) browser = 'Firefox';
else if (/safari/i.test(ua)) browser = 'Safari';
else if (/edge/i.test(ua)) browser = 'Edge';
return {
type,
os,
browser,
screenWidth: screen.width,
screenHeight: screen.height,
};
}
getEventQueue(): UserBehaviorEvent[] {
return [...this.eventQueue];
}
setUserId(userId: string): void {
localStorage.setItem('userId', userId);
}
destroy(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
this.trackPageExit();
this.flushEvents();
this.isInitialized = false;
}
}
export const behaviorAnalytics = new UserBehaviorAnalyticsService();
export const initBehaviorAnalytics = (config?: Partial<BehaviorAnalyticsConfig>): void => {
if (config) {
Object.assign(behaviorAnalytics['config'], config);
}
behaviorAnalytics.init();
};
export default behaviorAnalytics;

View File

@@ -0,0 +1,477 @@
/**
* [FE-MON002] 错误监控和上报服务
* @description 全局错误捕获、ErrorBoundary集成、错误上报
* @version 1.0
*/
import { http } from './http';
export interface ErrorReport {
tenantId: string;
shopId: string;
userId: string;
traceId: string;
errorType: 'javascript' | 'promise' | 'resource' | 'react' | 'api';
message: string;
stack?: string;
componentStack?: string;
url: string;
lineNumber?: number;
columnNumber?: number;
fileName?: string;
timestamp: string;
userAgent: string;
breadcrumbs: Breadcrumb[];
context: Record<string, unknown>;
severity: 'low' | 'medium' | 'high' | 'critical';
tags: string[];
}
export interface Breadcrumb {
type: 'navigation' | 'click' | 'xhr' | 'console' | 'error' | 'user';
message: string;
timestamp: string;
data?: Record<string, unknown>;
}
interface ErrorMonitorConfig {
maxBreadcrumbs: number;
reportInterval: number;
enableConsoleCapture: boolean;
enableXhrCapture: boolean;
enableClickCapture: boolean;
enableNavigationCapture: boolean;
ignoredErrors: RegExp[];
samplingRate: number;
}
const DEFAULT_CONFIG: ErrorMonitorConfig = {
maxBreadcrumbs: 50,
reportInterval: 10000,
enableConsoleCapture: true,
enableXhrCapture: true,
enableClickCapture: true,
enableNavigationCapture: true,
ignoredErrors: [
/ResizeObserver loop/,
/Network request failed/,
/Loading chunk/,
/Script error/,
],
samplingRate: 1.0,
};
class ErrorMonitorService {
private config: ErrorMonitorConfig;
private breadcrumbs: Breadcrumb[] = [];
private errorQueue: ErrorReport[] = [];
private isInitialized = false;
private reportInterval: NodeJS.Timeout | null = null;
private originalConsole: Record<string, (...args: unknown[]) => void> = {};
private originalXhr: {
open: typeof XMLHttpRequest.prototype.open;
send: typeof XMLHttpRequest.prototype.send;
} | null = null;
constructor(config: Partial<ErrorMonitorConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
init(): void {
if (this.isInitialized) return;
this.isInitialized = true;
this.captureGlobalErrors();
this.capturePromiseRejections();
this.captureResourceErrors();
if (this.config.enableConsoleCapture) {
this.captureConsoleErrors();
}
if (this.config.enableXhrCapture) {
this.captureXhrErrors();
}
if (this.config.enableClickCapture) {
this.captureClickEvents();
}
if (this.config.enableNavigationCapture) {
this.captureNavigationEvents();
}
this.startPeriodicReport();
window.addEventListener('beforeunload', () => {
this.flushReports();
});
console.log('[ErrorMonitor] Initialized');
}
private captureGlobalErrors(): void {
window.addEventListener('error', (event) => {
if (this.shouldIgnoreError(event.message)) return;
this.addBreadcrumb({
type: 'error',
message: event.message,
timestamp: new Date().toISOString(),
data: {
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
},
});
this.reportError({
errorType: 'javascript',
message: event.message,
stack: event.error?.stack,
fileName: event.filename,
lineNumber: event.lineno,
columnNumber: event.colno,
severity: this.determineSeverity(event.message),
});
});
}
private capturePromiseRejections(): void {
window.addEventListener('unhandledrejection', (event) => {
const message = event.reason?.message || String(event.reason);
if (this.shouldIgnoreError(message)) return;
this.addBreadcrumb({
type: 'error',
message: `Unhandled Promise Rejection: ${message}`,
timestamp: new Date().toISOString(),
data: {
reason: event.reason,
},
});
this.reportError({
errorType: 'promise',
message,
stack: event.reason?.stack,
severity: 'high',
});
});
}
private captureResourceErrors(): void {
window.addEventListener('error', (event) => {
if (event.target && (event.target as Element).tagName) {
const target = event.target as Element;
const tagName = target.tagName.toLowerCase();
if (['img', 'script', 'link', 'video', 'audio'].includes(tagName)) {
const src = (target as HTMLImageElement).src || (target as HTMLLinkElement).href;
this.reportError({
errorType: 'resource',
message: `Failed to load resource: ${src}`,
severity: 'medium',
tags: ['resource', tagName],
});
}
}
}, true);
}
private captureConsoleErrors(): void {
const methods = ['error', 'warn'] as const;
methods.forEach((method) => {
this.originalConsole[method] = console[method];
console[method] = (...args: unknown[]) => {
this.originalConsole[method](...args);
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
this.addBreadcrumb({
type: 'console',
message: `[${method.toUpperCase()}] ${message}`,
timestamp: new Date().toISOString(),
data: { args },
});
if (method === 'error') {
this.reportError({
errorType: 'javascript',
message,
severity: 'low',
});
}
};
});
}
private captureXhrErrors(): void {
this.originalXhr = {
open: XMLHttpRequest.prototype.open,
send: XMLHttpRequest.prototype.send,
};
const self = this;
XMLHttpRequest.prototype.open = function(method: string, url: string, ...args: unknown[]) {
(this as any)._errorMonitorUrl = url;
(this as any)._errorMonitorMethod = method;
return self.originalXhr!.open.apply(this, [method, url, ...args] as [string, string, ...unknown[]]);
};
XMLHttpRequest.prototype.send = function(...args: unknown[]) {
const xhr = this;
const startTime = Date.now();
xhr.addEventListener('loadend', () => {
const duration = Date.now() - startTime;
const status = xhr.status;
self.addBreadcrumb({
type: 'xhr',
message: `${(xhr as any)._errorMonitorMethod || 'GET'} ${(xhr as any)._errorMonitorUrl}`,
timestamp: new Date().toISOString(),
data: {
status,
duration,
responseSize: xhr.responseText?.length,
},
});
if (status >= 400) {
self.reportError({
errorType: 'api',
message: `API Error: ${(xhr as any)._errorMonitorUrl} returned ${status}`,
severity: status >= 500 ? 'high' : 'medium',
tags: ['api', `status-${status}`],
context: {
url: (xhr as any)._errorMonitorUrl,
method: (xhr as any)._errorMonitorMethod,
status,
response: xhr.responseText?.substring(0, 500),
},
});
}
});
xhr.addEventListener('error', () => {
self.reportError({
errorType: 'api',
message: `Network Error: ${(xhr as any)._errorMonitorUrl}`,
severity: 'high',
tags: ['api', 'network-error'],
});
});
return self.originalXhr!.send.apply(this, args);
};
}
private captureClickEvents(): void {
document.addEventListener('click', (event) => {
const target = event.target as Element;
const selector = this.getElementSelector(target);
this.addBreadcrumb({
type: 'click',
message: `Click on ${selector}`,
timestamp: new Date().toISOString(),
data: {
tagName: target.tagName,
className: target.className,
id: target.id,
text: target.textContent?.substring(0, 50),
},
});
}, true);
}
private captureNavigationEvents(): void {
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
const self = this;
history.pushState = function(...args) {
originalPushState.apply(this, args);
self.addNavigationBreadcrumb('pushState');
};
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
self.addNavigationBreadcrumb('replaceState');
};
window.addEventListener('popstate', () => {
this.addNavigationBreadcrumb('popstate');
});
window.addEventListener('hashchange', () => {
this.addNavigationBreadcrumb('hashchange');
});
}
private addNavigationBreadcrumb(type: string): void {
this.addBreadcrumb({
type: 'navigation',
message: `Navigation: ${type} -> ${window.location.pathname}`,
timestamp: new Date().toISOString(),
data: {
type,
url: window.location.href,
pathname: window.location.pathname,
},
});
}
private getElementSelector(element: Element): string {
if (element.id) return `#${element.id}`;
if (element.className) {
const classes = element.className.split(' ').filter(Boolean).join('.');
return `${element.tagName.toLowerCase()}.${classes}`;
}
return element.tagName.toLowerCase();
}
addBreadcrumb(breadcrumb: Breadcrumb): void {
this.breadcrumbs.push(breadcrumb);
if (this.breadcrumbs.length > this.config.maxBreadcrumbs) {
this.breadcrumbs.shift();
}
}
private shouldIgnoreError(message: string): boolean {
if (Math.random() > this.config.samplingRate) return true;
return this.config.ignoredErrors.some(pattern => pattern.test(message));
}
private determineSeverity(message: string): 'low' | 'medium' | 'high' | 'critical' {
const criticalPatterns = [/fatal/i, /crash/i, /security/i];
const highPatterns = [/error/i, /fail/i, /timeout/i];
const mediumPatterns = [/warn/i, /deprecated/i];
if (criticalPatterns.some(p => p.test(message))) return 'critical';
if (highPatterns.some(p => p.test(message))) return 'high';
if (mediumPatterns.some(p => p.test(message))) return 'medium';
return 'low';
}
reportError(params: {
errorType: ErrorReport['errorType'];
message: string;
stack?: string;
componentStack?: string;
fileName?: string;
lineNumber?: number;
columnNumber?: number;
severity?: ErrorReport['severity'];
tags?: string[];
context?: Record<string, unknown>;
}): void {
const tenantId = localStorage.getItem('tenantId') || 'default';
const shopId = localStorage.getItem('shopId') || 'default';
const userId = localStorage.getItem('userId') || 'anonymous';
const errorReport: ErrorReport = {
tenantId,
shopId,
userId,
traceId: `trace-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
errorType: params.errorType,
message: params.message,
stack: params.stack,
componentStack: params.componentStack,
url: window.location.href,
lineNumber: params.lineNumber,
columnNumber: params.columnNumber,
fileName: params.fileName,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
breadcrumbs: [...this.breadcrumbs],
context: params.context || {},
severity: params.severity || 'medium',
tags: params.tags || [],
};
this.errorQueue.push(errorReport);
console.error('[ErrorMonitor] Error captured:', errorReport);
}
reportReactError(error: Error, componentStack: string): void {
this.reportError({
errorType: 'react',
message: error.message,
stack: error.stack,
componentStack,
severity: 'high',
tags: ['react', 'error-boundary'],
});
}
private startPeriodicReport(): void {
this.reportInterval = setInterval(() => {
if (this.errorQueue.length > 0) {
this.flushReports();
}
}, this.config.reportInterval);
}
private async flushReports(): Promise<void> {
if (this.errorQueue.length === 0) return;
const errors = [...this.errorQueue];
this.errorQueue = [];
try {
await http.post('/api/telemetry/errors/batch', { errors });
console.log('[ErrorMonitor] Errors reported successfully');
} catch (error) {
console.error('[ErrorMonitor] Failed to report errors:', error);
this.errorQueue.unshift(...errors);
}
}
getBreadcrumbs(): Breadcrumb[] {
return [...this.breadcrumbs];
}
getErrorQueue(): ErrorReport[] {
return [...this.errorQueue];
}
destroy(): void {
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
Object.entries(this.originalConsole).forEach(([method, fn]) => {
(console as any)[method] = fn;
});
if (this.originalXhr) {
XMLHttpRequest.prototype.open = this.originalXhr.open;
XMLHttpRequest.prototype.send = this.originalXhr.send;
}
this.flushReports();
this.isInitialized = false;
}
}
export const errorMonitor = new ErrorMonitorService();
export const initErrorMonitor = (config?: Partial<ErrorMonitorConfig>): void => {
if (config) {
Object.assign(errorMonitor['config'], config);
}
errorMonitor.init();
};
export default errorMonitor;

View File

@@ -0,0 +1,141 @@
/**
* [FE-MON] 前端监控初始化模块
* @description 统一初始化所有前端监控服务
* @version 1.0
*/
import { performanceMonitor, initPerformanceMonitor } from './performanceMonitorService';
import { errorMonitor, initErrorMonitor } from './errorMonitorService';
import { behaviorAnalytics, initBehaviorAnalytics } from './behaviorAnalyticsService';
export interface MonitoringConfig {
enabled: boolean;
performance: {
enabled: boolean;
reportInterval: number;
};
error: {
enabled: boolean;
reportInterval: number;
samplingRate: number;
};
behavior: {
enabled: boolean;
reportInterval: number;
samplingRate: number;
trackPageViews: boolean;
trackClicks: boolean;
trackScrolls: boolean;
trackForms: boolean;
};
}
const DEFAULT_CONFIG: MonitoringConfig = {
enabled: true,
performance: {
enabled: true,
reportInterval: 30000,
},
error: {
enabled: true,
reportInterval: 10000,
samplingRate: 1.0,
},
behavior: {
enabled: true,
reportInterval: 15000,
samplingRate: 1.0,
trackPageViews: true,
trackClicks: true,
trackScrolls: true,
trackForms: true,
},
};
let isInitialized = false;
export function initMonitoring(config: Partial<MonitoringConfig> = {}): void {
if (isInitialized) {
console.warn('[Monitoring] Already initialized');
return;
}
const finalConfig: MonitoringConfig = {
...DEFAULT_CONFIG,
...config,
performance: { ...DEFAULT_CONFIG.performance, ...config.performance },
error: { ...DEFAULT_CONFIG.error, ...config.error },
behavior: { ...DEFAULT_CONFIG.behavior, ...config.behavior },
};
if (!finalConfig.enabled) {
console.log('[Monitoring] Monitoring is disabled');
return;
}
console.log('[Monitoring] Initializing monitoring services...');
if (finalConfig.performance.enabled) {
initPerformanceMonitor();
console.log('[Monitoring] Performance monitoring initialized');
}
if (finalConfig.error.enabled) {
initErrorMonitor({
reportInterval: finalConfig.error.reportInterval,
samplingRate: finalConfig.error.samplingRate,
});
console.log('[Monitoring] Error monitoring initialized');
}
if (finalConfig.behavior.enabled) {
initBehaviorAnalytics({
reportInterval: finalConfig.behavior.reportInterval,
samplingRate: finalConfig.behavior.samplingRate,
trackPageViews: finalConfig.behavior.trackPageViews,
trackClicks: finalConfig.behavior.trackClicks,
trackScrolls: finalConfig.behavior.trackScrolls,
trackForms: finalConfig.behavior.trackForms,
});
console.log('[Monitoring] Behavior analytics initialized');
}
isInitialized = true;
console.log('[Monitoring] All monitoring services initialized');
}
export function destroyMonitoring(): void {
if (!isInitialized) return;
performanceMonitor.destroy();
errorMonitor.destroy();
behaviorAnalytics.destroy();
isInitialized = false;
console.log('[Monitoring] All monitoring services destroyed');
}
export function getMonitoringStatus(): {
initialized: boolean;
performance: boolean;
error: boolean;
behavior: boolean;
} {
return {
initialized: isInitialized,
performance: (performanceMonitor as any).isInitialized || false,
error: (errorMonitor as any).isInitialized || false,
behavior: (behaviorAnalytics as any).isInitialized || false,
};
}
export { performanceMonitor, errorMonitor, behaviorAnalytics };
export default {
init: initMonitoring,
destroy: destroyMonitoring,
getStatus: getMonitoringStatus,
performanceMonitor,
errorMonitor,
behaviorAnalytics,
};

View File

@@ -0,0 +1,215 @@
/**
* 通知服务
* 支持多渠道通知发送EMAIL、SMS、WEBHOOK
*/
import { notificationStorage } from './notificationStorage';
export type NotificationChannel = 'EMAIL' | 'SMS' | 'WEBHOOK';
export interface NotificationOptions {
title: string;
message: string;
channels?: NotificationChannel[];
recipient?: string;
data?: Record<string, any>;
}
export interface NotificationResponse {
success: boolean;
channel: NotificationChannel;
message?: string;
error?: string;
}
class NotificationService {
/**
* 发送通知
* @param options 通知选项
* @returns 通知发送结果
*/
async sendNotification(options: NotificationOptions): Promise<NotificationResponse[]> {
const { title, message, channels = ['EMAIL'], recipient, data } = options;
const results: NotificationResponse[] = [];
for (const channel of channels) {
try {
let result: NotificationResponse;
switch (channel) {
case 'EMAIL':
result = await this.sendEmail(title, message, recipient, data);
break;
case 'SMS':
result = await this.sendSMS(title, message, recipient, data);
break;
case 'WEBHOOK':
result = await this.sendWebhook(title, message, data);
break;
default:
result = {
success: false,
channel,
error: '不支持的通知渠道',
};
}
results.push(result);
// 保存通知到本地存储
if (result.success) {
notificationStorage.saveNotification({
title,
message,
type: 'info',
channel,
data,
});
}
} catch (error) {
results.push({
success: false,
channel,
error: error instanceof Error ? error.message : '发送失败',
});
}
}
return results;
}
/**
* 发送邮件通知
* @param title 标题
* @param message 消息内容
* @param recipient 收件人
* @param data 附加数据
* @returns 发送结果
*/
private async sendEmail(
title: string,
message: string,
recipient?: string,
data?: Record<string, any>
): Promise<NotificationResponse> {
// 模拟邮件发送
console.log('发送邮件通知:', {
title,
message,
recipient,
data,
});
// 实际项目中这里应该调用邮件服务API
return {
success: true,
channel: 'EMAIL',
message: '邮件发送成功',
};
}
/**
* 发送短信通知
* @param title 标题
* @param message 消息内容
* @param recipient 收件人
* @param data 附加数据
* @returns 发送结果
*/
private async sendSMS(
title: string,
message: string,
recipient?: string,
data?: Record<string, any>
): Promise<NotificationResponse> {
// 模拟短信发送
console.log('发送短信通知:', {
title,
message,
recipient,
data,
});
// 实际项目中这里应该调用短信服务API
return {
success: true,
channel: 'SMS',
message: '短信发送成功',
};
}
/**
* 发送Webhook通知
* @param title 标题
* @param message 消息内容
* @param data 附加数据
* @returns 发送结果
*/
private async sendWebhook(
title: string,
message: string,
data?: Record<string, any>
): Promise<NotificationResponse> {
// 模拟Webhook发送
console.log('发送Webhook通知:', {
title,
message,
data,
});
// 实际项目中这里应该调用Webhook服务API
return {
success: true,
channel: 'WEBHOOK',
message: 'Webhook发送成功',
};
}
/**
* 获取通知历史
* @param limit 限制数量
* @returns 通知历史列表
*/
async getNotificationHistory(limit: number = 50) {
// 从本地存储获取通知历史
console.log('获取通知历史:', { limit });
const notifications = notificationStorage.getNotifications();
return notifications.slice(0, limit);
}
/**
* 标记通知为已读
* @param notificationId 通知ID
* @returns 操作结果
*/
async markAsRead(notificationId: string) {
// 使用存储服务标记为已读
console.log('标记通知为已读:', { notificationId });
const success = notificationStorage.markAsRead(notificationId);
return {
success,
message: success ? '标记成功' : '标记失败',
};
}
/**
* 删除通知
* @param notificationId 通知ID
* @returns 操作结果
*/
async deleteNotification(notificationId: string) {
// 使用存储服务删除通知
console.log('删除通知:', { notificationId });
const success = notificationStorage.deleteNotification(notificationId);
return {
success,
message: success ? '删除成功' : '删除失败',
};
}
}
export const notificationService = new NotificationService();

View File

@@ -0,0 +1,165 @@
/**
* 通知存储服务
* 提供通知的本地存储和持久化功能
*/
interface StoredNotification {
id: string;
title: string;
message: string;
type: 'success' | 'error' | 'warning' | 'info';
read: boolean;
createdAt: string;
channel?: string;
data?: Record<string, any>;
}
class NotificationStorage {
private readonly STORAGE_KEY = 'makemd_notifications';
/**
* 保存通知到本地存储
* @param notification 通知对象
* @returns 保存的通知
*/
saveNotification(notification: Omit<StoredNotification, 'id' | 'createdAt' | 'read'>): StoredNotification {
const notifications = this.getNotifications();
const newNotification: StoredNotification = {
...notification,
id: `notification_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
createdAt: new Date().toISOString(),
read: false,
};
notifications.unshift(newNotification);
this.setNotifications(notifications);
return newNotification;
}
/**
* 获取所有通知
* @returns 通知列表
*/
getNotifications(): StoredNotification[] {
try {
const stored = localStorage.getItem(this.STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (error) {
console.error('读取通知存储失败:', error);
return [];
}
}
/**
* 获取未读通知数量
* @returns 未读通知数量
*/
getUnreadCount(): number {
const notifications = this.getNotifications();
return notifications.filter(n => !n.read).length;
}
/**
* 标记通知为已读
* @param notificationId 通知ID
* @returns 是否标记成功
*/
markAsRead(notificationId: string): boolean {
try {
const notifications = this.getNotifications();
const updated = notifications.map(n =>
n.id === notificationId ? { ...n, read: true } : n
);
this.setNotifications(updated);
return true;
} catch (error) {
console.error('标记通知已读失败:', error);
return false;
}
}
/**
* 标记所有通知为已读
* @returns 是否标记成功
*/
markAllAsRead(): boolean {
try {
const notifications = this.getNotifications();
const updated = notifications.map(n => ({ ...n, read: true }));
this.setNotifications(updated);
return true;
} catch (error) {
console.error('标记所有通知已读失败:', error);
return false;
}
}
/**
* 删除通知
* @param notificationId 通知ID
* @returns 是否删除成功
*/
deleteNotification(notificationId: string): boolean {
try {
const notifications = this.getNotifications();
const updated = notifications.filter(n => n.id !== notificationId);
this.setNotifications(updated);
return true;
} catch (error) {
console.error('删除通知失败:', error);
return false;
}
}
/**
* 清空所有通知
* @returns 是否清空成功
*/
clearAllNotifications(): boolean {
try {
localStorage.removeItem(this.STORAGE_KEY);
return true;
} catch (error) {
console.error('清空通知失败:', error);
return false;
}
}
/**
* 保存通知列表到本地存储
* @param notifications 通知列表
*/
private setNotifications(notifications: StoredNotification[]): void {
try {
// 限制存储的通知数量最多保存100条
const limited = notifications.slice(0, 100);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(limited));
} catch (error) {
console.error('保存通知存储失败:', error);
}
}
/**
* 清理过期通知
* @param days 保留天数
*/
cleanupOldNotifications(days: number = 30): void {
try {
const notifications = this.getNotifications();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const updated = notifications.filter(n => {
const notificationDate = new Date(n.createdAt);
return notificationDate >= cutoffDate;
});
this.setNotifications(updated);
} catch (error) {
console.error('清理过期通知失败:', error);
}
}
}
export const notificationStorage = new NotificationStorage();

View File

@@ -0,0 +1,333 @@
/**
* [FE-MON001] 前端性能监控服务
* @description 收集和上报 Web Vitals 性能指标
* @version 1.0
*/
import { http } from './http';
export interface PerformanceMetric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
delta: number;
id: string;
navigationType: string;
timestamp: number;
}
export interface WebVitalsReport {
tenantId: string;
shopId: string;
userId: string;
url: string;
userAgent: string;
timestamp: string;
metrics: PerformanceMetric[];
connection?: {
effectiveType: string;
downlink: number;
rtt: number;
};
memory?: {
usedJSHeapSize: number;
totalJSHeapSize: number;
jsHeapSizeLimit: number;
};
}
type MetricRatingThresholds = {
good: number;
poor: number;
};
const METRIC_THRESHOLDS: Record<string, MetricRatingThresholds> = {
LCP: { good: 2500, poor: 4000 },
FID: { good: 100, poor: 300 },
CLS: { good: 0.1, poor: 0.25 },
FCP: { good: 1800, poor: 3000 },
TTFB: { good: 800, poor: 1800 },
INP: { good: 200, poor: 500 },
};
const getRating = (name: string, value: number): 'good' | 'needs-improvement' | 'poor' => {
const thresholds = METRIC_THRESHOLDS[name];
if (!thresholds) return 'good';
if (value <= thresholds.good) return 'good';
if (value <= thresholds.poor) return 'needs-improvement';
return 'poor';
};
class PerformanceMonitorService {
private metrics: Map<string, PerformanceMetric> = new Map();
private observers: PerformanceObserver[] = [];
private reportQueue: PerformanceMetric[] = [];
private isInitialized = false;
private reportInterval: NodeJS.Timeout | null = null;
private sessionId: string;
constructor() {
this.sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
init(): void {
if (this.isInitialized) return;
this.isInitialized = true;
this.observePerformance();
this.collectWebVitals();
this.startPeriodicReport();
window.addEventListener('beforeunload', () => {
this.flushReports();
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flushReports();
}
});
console.log('[PerformanceMonitor] Initialized');
}
private observePerformance(): void {
if (typeof PerformanceObserver === 'undefined') return;
const observeTypes = [
'largest-contentful-paint',
'first-input',
'layout-shift',
'paint',
'navigation',
'longtask',
];
observeTypes.forEach((type) => {
try {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
this.processEntry(entry);
});
});
observer.observe({ type, buffered: true });
this.observers.push(observer);
} catch (e) {
console.debug(`[PerformanceMonitor] Observer type ${type} not supported`);
}
});
}
private processEntry(entry: PerformanceEntry): void {
switch (entry.entryType) {
case 'largest-contentful-paint':
this.recordMetric('LCP', entry.startTime, entry.duration);
break;
case 'first-input':
this.recordMetric('FID', (entry as PerformanceEventTiming).processingStart - entry.startTime, 0);
break;
case 'layout-shift':
if (!(entry as any).hadRecentInput) {
const clsValue = (entry as any).value || 0;
this.recordMetric('CLS', clsValue * 1000, 0);
}
break;
case 'paint':
if (entry.name === 'first-contentful-paint') {
this.recordMetric('FCP', entry.startTime, 0);
}
break;
case 'navigation':
this.processNavigationTiming(entry as PerformanceNavigationTiming);
break;
case 'longtask':
this.recordMetric('LongTask', entry.duration, 0);
break;
}
}
private processNavigationTiming(entry: PerformanceNavigationTiming): void {
this.recordMetric('TTFB', entry.responseStart - entry.requestStart, 0);
this.recordMetric('DOMLoad', entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart, 0);
this.recordMetric('WindowLoad', entry.loadEventEnd - entry.loadEventStart, 0);
}
private collectWebVitals(): void {
if (typeof window === 'undefined' || !('performance' in window)) return;
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
this.processNavigationTiming(navigation);
}
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach((entry) => {
if (entry.name === 'first-contentful-paint') {
this.recordMetric('FCP', entry.startTime, 0);
}
});
const memory = (performance as any).memory;
if (memory) {
this.recordMetric('MemoryUsed', memory.usedJSHeapSize / 1048576, 0);
this.recordMetric('MemoryLimit', memory.jsHeapSizeLimit / 1048576, 0);
}
}
recordMetric(name: string, value: number, delta: number): void {
const metric: PerformanceMetric = {
name,
value: Math.round(value * 100) / 100,
rating: getRating(name, value),
delta,
id: `${this.sessionId}-${name}`,
navigationType: this.getNavigationType(),
timestamp: Date.now(),
};
this.metrics.set(name, metric);
this.reportQueue.push(metric);
}
private getNavigationType(): string {
const entries = performance.getEntriesByType('navigation');
if (entries.length > 0) {
return (entries[0] as PerformanceNavigationTiming).type;
}
return 'navigate';
}
private startPeriodicReport(): void {
this.reportInterval = setInterval(() => {
if (this.reportQueue.length > 0) {
this.flushReports();
}
}, 30000);
}
private async flushReports(): Promise<void> {
if (this.reportQueue.length === 0) return;
const metrics = [...this.reportQueue];
this.reportQueue = [];
const report = this.buildReport(metrics);
await this.sendReport(report);
}
private buildReport(metrics: PerformanceMetric[]): WebVitalsReport {
const tenantId = localStorage.getItem('tenantId') || 'default';
const shopId = localStorage.getItem('shopId') || 'default';
const userId = localStorage.getItem('userId') || 'anonymous';
const report: WebVitalsReport = {
tenantId,
shopId,
userId,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
metrics,
};
const connection = (navigator as any).connection;
if (connection) {
report.connection = {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
};
}
const memory = (performance as any).memory;
if (memory) {
report.memory = {
usedJSHeapSize: memory.usedJSHeapSize,
totalJSHeapSize: memory.totalJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
};
}
return report;
}
private async sendReport(report: WebVitalsReport): Promise<void> {
try {
await http.post('/api/telemetry/performance', report);
console.log('[PerformanceMonitor] Report sent successfully');
} catch (error) {
console.error('[PerformanceMonitor] Failed to send report:', error);
}
}
getMetrics(): Map<string, PerformanceMetric> {
return new Map(this.metrics);
}
getMetric(name: string): PerformanceMetric | undefined {
return this.metrics.get(name);
}
getSummary(): Record<string, { value: number; rating: string }> {
const summary: Record<string, { value: number; rating: string }> = {};
this.metrics.forEach((metric, name) => {
summary[name] = {
value: metric.value,
rating: metric.rating,
};
});
return summary;
}
markMeasure(name: string): void {
try {
performance.mark(`${name}-start`);
} catch (e) {
console.debug(`[PerformanceMonitor] Mark failed: ${name}`);
}
}
endMeasure(name: string): number | null {
try {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const entries = performance.getEntriesByName(name, 'measure');
const duration = entries[entries.length - 1]?.duration;
if (duration) {
this.recordMetric(`Custom_${name}`, duration, 0);
}
performance.clearMarks(`${name}-start`);
performance.clearMarks(`${name}-end`);
performance.clearMeasures(name);
return duration || null;
} catch (e) {
console.debug(`[PerformanceMonitor] Measure failed: ${name}`);
return null;
}
}
destroy(): void {
this.observers.forEach((observer) => observer.disconnect());
this.observers = [];
if (this.reportInterval) {
clearInterval(this.reportInterval);
this.reportInterval = null;
}
this.flushReports();
this.isInitialized = false;
}
}
export const performanceMonitor = new PerformanceMonitorService();
export const initPerformanceMonitor = (): void => {
performanceMonitor.init();
};
export default performanceMonitor;

View File

@@ -0,0 +1,134 @@
import { http } from './http';
import { DataSourceFactory } from './baseDataSource';
export interface Shop {
id: string;
tenantId: string;
name: string;
platform: string;
platformAccountId?: string;
departmentId?: string;
status: 'ACTIVE' | 'INACTIVE' | 'EXPIRED' | 'ERROR';
config?: Record<string, any>;
lastSyncAt?: string;
createdAt: string;
updatedAt: string;
}
export interface ShopMember {
id: string;
shopId: string;
userId: string;
userName?: string;
userEmail?: string;
role: 'owner' | 'admin' | 'operator' | 'viewer';
permissions: string[];
assignedBy: string;
assignedAt: string;
createdAt: string;
updatedAt: string;
}
export interface ShopStats {
total: number;
active: number;
inactive: number;
expired: number;
error: number;
}
export interface ShopMemberCreate {
shopId: string;
userId: string;
role: 'owner' | 'admin' | 'operator' | 'viewer';
permissions: string[];
assignedBy: string;
}
export interface ShopUpdate {
name?: string;
platformAccountId?: string;
departmentId?: string;
config?: Record<string, any>;
}
export interface ShopDataSource {
getMyShops(): Promise<Shop[]>;
getById(id: string): Promise<Shop | null>;
getShopMembers(shopId: string): Promise<ShopMember[]>;
getUserShops(userId: string): Promise<ShopMember[]>;
createShop(data: { name: string; platform: string; platformAccountId?: string; departmentId?: string; config?: Record<string, any> }): Promise<Shop>;
updateShop(id: string, data: ShopUpdate): Promise<Shop>;
refreshAuth(id: string): Promise<{ success: boolean; message: string }>;
deleteShop(id: string): Promise<void>;
addMember(data: ShopMemberCreate): Promise<ShopMember>;
removeMember(shopId: string, userId: string): Promise<void>;
updateMemberRole(shopId: string, userId: string, role: 'owner' | 'admin' | 'operator' | 'viewer', permissions: string[]): Promise<ShopMember>;
getStats(): Promise<ShopStats>;
}
export class ShopApiDataSource implements ShopDataSource {
async getMyShops(): Promise<Shop[]> {
const response = await http.get('/api/shops/my-shops');
return response.data.data;
}
async getById(id: string): Promise<Shop | null> {
const response = await http.get(`/api/shops/${id}`);
return response.data.data;
}
async getShopMembers(shopId: string): Promise<ShopMember[]> {
const response = await http.get(`/api/shops/${shopId}/members`);
return response.data.data;
}
async getUserShops(userId: string): Promise<ShopMember[]> {
const response = await http.get(`/api/shops/user/${userId}/shops`);
return response.data.data;
}
async createShop(data: { name: string; platform: string; platformAccountId?: string; departmentId?: string; config?: Record<string, any> }): Promise<Shop> {
const response = await http.post('/api/shops', data);
return response.data.data;
}
async updateShop(id: string, data: ShopUpdate): Promise<Shop> {
const response = await http.put(`/api/shops/${id}`, data);
return response.data.data;
}
async refreshAuth(id: string): Promise<{ success: boolean; message: string }> {
const response = await http.post(`/api/shops/${id}/refresh-auth`);
return response.data.data;
}
async deleteShop(id: string): Promise<void> {
await http.delete(`/api/shops/${id}`);
}
async addMember(data: ShopMemberCreate): Promise<ShopMember> {
const response = await http.post('/api/shops/members', data);
return response.data.data;
}
async removeMember(shopId: string, userId: string): Promise<void> {
await http.delete(`/api/shops/members/${shopId}/${userId}`);
}
async updateMemberRole(shopId: string, userId: string, role: 'owner' | 'admin' | 'operator' | 'viewer', permissions: string[]): Promise<ShopMember> {
const response = await http.put(`/api/shops/members/${shopId}/${userId}`, { role, permissions });
return response.data.data;
}
async getStats(): Promise<ShopStats> {
const response = await http.get('/api/shops/stats');
return response.data.data;
}
}
// 直接创建实例
export const shopDataSource = new ShopApiDataSource();
// 注册到 DataSourceFactory
DataSourceFactory.register('shop', shopDataSource);

View File

@@ -0,0 +1,368 @@
/**
* [FE-MON004] 系统状态监控数据源
* @description 获取系统健康状态、性能指标、服务状态
* @version 1.0
*/
import { http } from './http';
export interface SystemHealthStatus {
status: 'healthy' | 'degraded' | 'unhealthy';
timestamp: string;
components: Record<string, ComponentHealth>;
metrics: SystemMetrics;
}
export interface ComponentHealth {
status: 'up' | 'down' | 'degraded';
responseTime: number;
lastCheck: string;
details?: Record<string, unknown>;
}
export interface SystemMetrics {
cpu: number;
memory: number;
disk: number;
uptime: number;
connections: number;
requestRate: number;
errorRate: number;
avgResponseTime: number;
}
export interface ServiceStatus {
id: string;
name: string;
status: 'running' | 'stopped' | 'error' | 'pending';
uptime: number;
lastHeartbeat: string;
version: string;
cpu: number;
memory: number;
port?: number;
pid?: number;
}
export interface AlertInfo {
id: string;
type: 'error' | 'warning' | 'info';
message: string;
source: string;
timestamp: string;
acknowledged: boolean;
}
export interface PerformanceDataPoint {
timestamp: string;
cpu: number;
memory: number;
requestRate: number;
responseTime: number;
errorRate: number;
}
export interface MonitoringDashboardData {
health: SystemHealthStatus;
services: ServiceStatus[];
alerts: AlertInfo[];
performanceHistory: PerformanceDataPoint[];
activeConnections: number;
queueSize: number;
cacheHitRate: number;
}
class SystemStatusDataSource {
private baseUrl = '/api/system';
private ws: WebSocket | null = null;
private listeners: Set<(data: MonitoringDashboardData) => void> = new Set();
async getHealthStatus(): Promise<SystemHealthStatus> {
const response = await http.get(`${this.baseUrl}/health`);
return response.data;
}
async getServices(): Promise<ServiceStatus[]> {
const response = await http.get(`${this.baseUrl}/services`);
return response.data.data || [];
}
async getAlerts(params?: {
limit?: number;
type?: AlertInfo['type'];
acknowledged?: boolean;
}): Promise<AlertInfo[]> {
const response = await http.get(`${this.baseUrl}/alerts`, { params });
return response.data.data || [];
}
async getPerformanceHistory(params?: {
start?: string;
end?: string;
interval?: string;
}): Promise<PerformanceDataPoint[]> {
const response = await http.get(`${this.baseUrl}/performance/history`, { params });
return response.data.data || [];
}
async getDashboardData(): Promise<MonitoringDashboardData> {
const [health, services, alerts, performanceHistory] = await Promise.all([
this.getHealthStatus(),
this.getServices(),
this.getAlerts({ limit: 10 }),
this.getPerformanceHistory(),
]);
return {
health,
services,
alerts,
performanceHistory,
activeConnections: (health.metrics as any).connections || 0,
queueSize: (health.components.queue as any)?.details?.queueSize || 0,
cacheHitRate: (health.components.cache as any)?.details?.hitRate || 0,
};
}
async acknowledgeAlert(alertId: string): Promise<void> {
await http.post(`${this.baseUrl}/alerts/${alertId}/acknowledge`);
}
async restartService(serviceId: string): Promise<{ success: boolean; message: string }> {
const response = await http.post(`${this.baseUrl}/services/${serviceId}/restart`);
return response.data;
}
subscribeToUpdates(callback: (data: MonitoringDashboardData) => void): () => void {
this.listeners.add(callback);
if (!this.ws && typeof WebSocket !== 'undefined') {
this.connectWebSocket();
}
return () => {
this.listeners.delete(callback);
if (this.listeners.size === 0) {
this.disconnectWebSocket();
}
};
}
private connectWebSocket(): void {
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${this.baseUrl}/ws`;
try {
this.ws = new WebSocket(wsUrl);
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as MonitoringDashboardData;
this.listeners.forEach(callback => callback(data));
} catch (error) {
console.error('[SystemStatus] Failed to parse WebSocket message:', error);
}
};
this.ws.onerror = (error) => {
console.error('[SystemStatus] WebSocket error:', error);
};
this.ws.onclose = () => {
console.log('[SystemStatus] WebSocket closed, reconnecting...');
setTimeout(() => this.connectWebSocket(), 5000);
};
} catch (error) {
console.error('[SystemStatus] Failed to connect WebSocket:', error);
}
}
private disconnectWebSocket(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
class MockSystemStatusDataSource {
private listeners: Set<(data: MonitoringDashboardData) => void> = new Set();
private interval: NodeJS.Timeout | null = null;
async getHealthStatus(): Promise<SystemHealthStatus> {
return {
status: Math.random() > 0.1 ? 'healthy' : 'degraded',
timestamp: new Date().toISOString(),
components: {
database: {
status: 'up',
responseTime: Math.random() * 50 + 10,
lastCheck: new Date().toISOString(),
},
redis: {
status: 'up',
responseTime: Math.random() * 10 + 1,
lastCheck: new Date().toISOString(),
},
eventBus: {
status: 'up',
responseTime: Math.random() * 20 + 5,
lastCheck: new Date().toISOString(),
},
queue: {
status: 'up',
responseTime: Math.random() * 5 + 1,
lastCheck: new Date().toISOString(),
details: { queueSize: Math.floor(Math.random() * 100) },
},
websocket: {
status: 'up',
responseTime: Math.random() * 10 + 2,
lastCheck: new Date().toISOString(),
},
api: {
status: 'up',
responseTime: Math.random() * 30 + 10,
lastCheck: new Date().toISOString(),
},
},
metrics: {
cpu: Math.random() * 60 + 20,
memory: Math.random() * 50 + 30,
disk: Math.random() * 40 + 40,
uptime: Date.now() - 86400000 * 7,
connections: Math.floor(Math.random() * 100) + 50,
requestRate: Math.random() * 100 + 50,
errorRate: Math.random() * 2,
avgResponseTime: Math.random() * 200 + 50,
},
};
}
async getServices(): Promise<ServiceStatus[]> {
return [
{
id: 'api-server',
name: 'API Server',
status: 'running',
uptime: 604800,
lastHeartbeat: new Date().toISOString(),
version: '1.0.0',
cpu: Math.random() * 40 + 10,
memory: Math.random() * 30 + 20,
port: 3001,
pid: 12345,
},
{
id: 'worker-1',
name: 'Background Worker',
status: 'running',
uptime: 604800,
lastHeartbeat: new Date().toISOString(),
version: '1.0.0',
cpu: Math.random() * 30 + 5,
memory: Math.random() * 25 + 15,
},
{
id: 'scheduler',
name: 'Task Scheduler',
status: 'running',
uptime: 604800,
lastHeartbeat: new Date().toISOString(),
version: '1.0.0',
cpu: Math.random() * 10 + 2,
memory: Math.random() * 10 + 5,
},
];
}
async getAlerts(): Promise<AlertInfo[]> {
return [
{
id: '1',
type: 'warning',
message: 'High memory usage detected on API Server',
source: 'api-server',
timestamp: new Date(Date.now() - 3600000).toISOString(),
acknowledged: false,
},
{
id: '2',
type: 'info',
message: 'Scheduled maintenance completed',
source: 'system',
timestamp: new Date(Date.now() - 7200000).toISOString(),
acknowledged: true,
},
];
}
async getPerformanceHistory(): Promise<PerformanceDataPoint[]> {
const points: PerformanceDataPoint[] = [];
const now = Date.now();
for (let i = 24; i >= 0; i--) {
points.push({
timestamp: new Date(now - i * 3600000).toISOString(),
cpu: Math.random() * 60 + 20,
memory: Math.random() * 50 + 30,
requestRate: Math.random() * 100 + 50,
responseTime: Math.random() * 200 + 50,
errorRate: Math.random() * 2,
});
}
return points;
}
async getDashboardData(): Promise<MonitoringDashboardData> {
const [health, services, alerts, performanceHistory] = await Promise.all([
this.getHealthStatus(),
this.getServices(),
this.getAlerts(),
this.getPerformanceHistory(),
]);
return {
health,
services,
alerts,
performanceHistory,
activeConnections: health.metrics.connections,
queueSize: (health.components.queue.details?.queueSize as number) || 0,
cacheHitRate: 0.85,
};
}
async acknowledgeAlert(): Promise<void> {}
async restartService(): Promise<{ success: boolean; message: string }> {
return { success: true, message: 'Service restarted' };
}
subscribeToUpdates(callback: (data: MonitoringDashboardData) => void): () => void {
this.listeners.add(callback);
if (!this.interval) {
this.interval = setInterval(() => {
this.getDashboardData().then(data => {
this.listeners.forEach(cb => cb(data));
});
}, 5000);
}
return () => {
this.listeners.delete(callback);
if (this.listeners.size === 0 && this.interval) {
clearInterval(this.interval);
this.interval = null;
}
};
}
}
const useMock = process.env.NODE_ENV === 'development' || process.env.REACT_APP_USE_MOCK === 'true';
export const systemStatusDataSource = useMock
? new MockSystemStatusDataSource()
: new SystemStatusDataSource();
export default systemStatusDataSource;