diff --git a/CLAUDE.md b/CLAUDE.md index 2106e3bd..31a10853 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,40 +4,61 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a hybrid React dashboard application with a Flask/Python backend. The project is built on the Argon Dashboard Chakra PRO template and includes financial/trading analysis features. +This is a hybrid React dashboard application with a Flask/Python backend for financial/trading analysis. Built on the Argon Dashboard Chakra PRO template with extensive customization. ### Frontend (React + Chakra UI) - **Framework**: React 18.3.1 with Chakra UI 2.8.2 +- **State Management**: Redux Toolkit (@reduxjs/toolkit) +- **Routing**: React Router DOM v6 with lazy loading for code splitting - **Styling**: Tailwind CSS + custom Chakra theme -- **Build Tool**: React Scripts with custom Gulp tasks -- **Charts**: ApexCharts, ECharts, and custom visualization components +- **Build Tool**: CRACO (Create React App Configuration Override) with custom webpack optimizations +- **Charts**: ApexCharts, ECharts, Recharts, D3 +- **UI Components**: Ant Design (antd) alongside Chakra UI +- **Other Libraries**: Three.js (@react-three), FullCalendar, Leaflet maps ### Backend (Flask/Python) - **Framework**: Flask with SQLAlchemy ORM -- **Database**: ClickHouse for analytics + MySQL/PostgreSQL -- **Features**: Real-time data processing, trading analysis, user authentication -- **Task Queue**: Celery for background processing +- **Database**: ClickHouse for analytics queries + MySQL/PostgreSQL +- **Real-time**: Flask-SocketIO for WebSocket connections +- **Task Queue**: Celery with Redis for background processing +- **External APIs**: Tencent Cloud SMS, WeChat Pay integration ## Development Commands ### Frontend Development ```bash -npm start # Start development server (port 3000, proxies to localhost:5001) -npm run build # Production build with license headers -npm test # Run React test suite -npm run lint:check # Check ESLint rules -npm run lint:fix # Auto-fix ESLint issues -npm run install:clean # Clean install (removes node_modules and package-lock) +npm start # Start with mock data (.env.mock), proxies to localhost:5001 +npm run start:real # Start with real backend (.env.local) +npm run start:dev # Start with development config (.env.development) +npm run start:test # Starts both backend (app_2.py) and frontend (.env.test) concurrently +npm run dev # Alias for 'npm start' +npm run backend # Start Flask server only (python app_2.py) + +npm run build # Production build with Gulp license headers +npm run build:analyze # Build with webpack bundle analyzer +npm test # Run React test suite with CRACO + +npm run lint:check # Check ESLint rules (exits 0) +npm run lint:fix # Auto-fix ESLint issues +npm run clean # Remove node_modules and package-lock.json +npm run reinstall # Clean install (runs clean + install) ``` ### Backend Development ```bash -python app_2.py # Start Flask server (main backend) -python simulation_background_processor.py # Background data processor +python app.py # Main Flask server (newer version) +python app_2.py # Flask server (appears to be current main) +python simulation_background_processor.py # Background data processor for simulations +``` + +### Deployment +```bash +npm run deploy # Executes scripts/deploy-from-local.sh +npm run deploy:setup # Setup deployment (scripts/setup-deployment.sh) +npm run rollback # Rollback deployment (scripts/rollback-from-local.sh) ``` ### Python Dependencies -Install from requirements.txt: ```bash pip install -r requirements.txt ``` @@ -45,47 +66,69 @@ pip install -r requirements.txt ## Architecture ### Frontend Structure -- `src/layouts/` - Main layout components (Admin, Auth, Home) -- `src/views/` - Page components organized by feature (Dashboard, Company, Community, etc.) -- `src/components/` - Reusable UI components (Charts, Cards, Buttons, etc.) -- `src/theme/` - Chakra UI theme customization -- `src/routes.js` - Application routing configuration -- `src/contexts/` - React context providers -- `src/services/` - API service layer +- **src/App.js** - Main application entry with route definitions (routing moved from src/routes.js) +- **src/layouts/** - Layout wrappers (Auth, Home, MainLayout) +- **src/views/** - Page components (Community, Company, TradingSimulation, etc.) +- **src/components/** - Reusable UI components +- **src/contexts/** - React contexts (AuthContext, NotificationContext, IndustryContext) +- **src/store/** - Redux store with slices (posthogSlice, etc.) +- **src/services/** - API service layer +- **src/theme/** - Chakra UI theme customization +- **src/mocks/** - MSW (Mock Service Worker) handlers for development + - src/mocks/handlers/ - Request handlers by domain + - src/mocks/data/ - Mock data files + - src/mocks/browser.js - MSW browser setup ### Backend Structure -- `app_2.py` - Main Flask application with routes and business logic -- `simulation_background_processor.py` - Background data processing service -- `wechat_pay.py` / `wechat_pay_config.py` - Payment integration -- `tdays.csv` - Trading days data +- **app.py / app_2.py** - Main Flask application with routes, authentication, and business logic +- **simulation_background_processor.py** - Background processor for trading simulations +- **wechat_pay.py / wechat_pay_config.py** - WeChat payment integration +- **concept_api.py** - API for concept/industry analysis +- **tdays.csv** - Trading days calendar data (loaded into memory at startup) ### Key Integrations -- ClickHouse for high-performance analytics queries -- Celery + Redis for background task processing -- Flask-SocketIO for real-time data updates -- Tencent Cloud services (SMS, etc.) -- WeChat Pay integration +- **ClickHouse** - High-performance analytics queries +- **Celery + Redis** - Background task processing +- **Flask-SocketIO** - Real-time data updates via WebSocket +- **Tencent Cloud** - SMS services +- **WeChat Pay** - Payment processing +- **PostHog** - Analytics (initialized in Redux) +- **MSW** - API mocking for development/testing + +### Routing & Code Splitting +- Routing is defined in **src/App.js** (not src/routes.js - that file is deprecated) +- Heavy components use React.lazy() for code splitting (Community, TradingSimulation, etc.) +- Protected routes use ProtectedRoute and ProtectedRouteRedirect components ## Configuration -### Proxy Setup -The React dev server proxies API calls to `http://localhost:5001` (see package.json). - ### Environment Files -- `.env` - Environment variables for both frontend and backend +Multiple environment configurations available: +- **.env.mock** - Mock data mode (default for `npm start`) +- **.env.local** - Real backend connection +- **.env.development** - Development environment +- **.env.test** - Test environment -### Build Process -The build process includes custom Gulp tasks that add Creative Tim license headers to JS, CSS, and HTML files. +### Build Configuration (craco.config.js) +- **Webpack caching**: Filesystem cache for faster rebuilds (50-80% improvement) +- **Code splitting**: Aggressive chunk splitting by library (react-vendor, charts-lib, chakra-ui, antd-lib, three-lib, etc.) +- **Path aliases**: `@` → src/, `@components` → src/components/, `@views` → src/views/, `@assets` → src/assets/, `@contexts` → src/contexts/ +- **Optimizations**: ESLint plugin removed from build for speed, Babel caching enabled, moment locale stripping +- **Source maps**: Disabled in production, eval-cheap-module-source-map in development +- **Dev server proxy**: `/api` requests proxy to http://49.232.185.254:5001 -### Styling Architecture -- Tailwind CSS for utility classes -- Custom Chakra UI theme with extended color palette -- Component-specific SCSS files in `src/assets/scss/` +### Important Build Notes +- Uses NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' for Node compatibility +- Gulp task adds Creative Tim license headers post-build +- Bundle analyzer available via `ANALYZE=true npm run build:analyze` +- Pre-build: kills any process on port 3000 ## Testing -- React Testing Library setup for frontend components -- Test command: `npm test` +- **React Testing Library** for component tests +- **MSW** (Mock Service Worker) for API mocking during tests +- Run tests: `npm test` ## Deployment -- Build: `npm run build` -- Deploy: `npm run deploy` (builds the project) \ No newline at end of file +- Deployment scripts in **scripts/** directory +- Build output processed by Gulp for licensing +- Supports rollback via scripts/rollback-from-local.sh \ No newline at end of file diff --git a/app.py b/app.py index 26d89f64..bb4bce50 100755 --- a/app.py +++ b/app.py @@ -97,6 +97,38 @@ def get_trading_day_near_date(target_date): return trading_days[-1] if trading_days else None +def get_previous_trading_day(target_date): + """ + 获取指定日期的上一个交易日 + 如果目标日期不是交易日,先找到对应的交易日,然后返回前一个交易日 + """ + if not trading_days: + load_trading_days() + + if not trading_days: + return None + + # 如果目标日期是datetime,转换为date + if isinstance(target_date, datetime): + target_date = target_date.date() + + # 确保目标日期是交易日 + if target_date not in trading_days_set: + target_date = get_trading_day_near_date(target_date) + if not target_date: + return None + + # 查找上一个交易日 + try: + index = trading_days.index(target_date) + if index > 0: + return trading_days[index - 1] + else: + return None # 没有上一个交易日 + except ValueError: + return None + + # 应用启动时加载交易日数据 load_trading_days() @@ -6595,6 +6627,9 @@ def api_get_events(): event_status = request.args.get('status', 'active') importance = request.args.get('importance', 'all') + # 交易日筛选参数 + tday = request.args.get('tday') # 交易日,格式:YYYY-MM-DD 或 YYYY/M/D + # 日期筛选参数 start_date = request.args.get('start_date') end_date = request.args.get('end_date') @@ -6680,7 +6715,41 @@ def api_get_events(): text(f"JSON_SEARCH(keywords, 'one', '%{search_query}%') IS NOT NULL") ) ) - if recent_days: + + # 交易日筛选逻辑 + if tday: + from datetime import datetime, timedelta, time + try: + # 解析交易日参数,支持 YYYY-MM-DD 和 YYYY/M/D 格式 + if '/' in tday: + target_tday = datetime.strptime(tday, '%Y/%m/%d').date() + else: + target_tday = datetime.strptime(tday, '%Y-%m-%d').date() + + # 获取该交易日的上一个交易日 + prev_tday = get_previous_trading_day(target_tday) + + if prev_tday: + # 计算时间范围:[前一个交易日 15:00, 当前交易日 15:00] + start_datetime = datetime.combine(prev_tday, time(15, 0, 0)) + end_datetime = datetime.combine(target_tday, time(15, 0, 0)) + + query = query.filter( + Event.created_at >= start_datetime, + Event.created_at <= end_datetime + ) + else: + # 如果没有上一个交易日,则筛选当天的事件 + start_datetime = datetime.combine(target_tday, time(0, 0, 0)) + end_datetime = datetime.combine(target_tday, time(15, 0, 0)) + query = query.filter( + Event.created_at >= start_datetime, + Event.created_at <= end_datetime + ) + except (ValueError, TypeError) as e: + # 日期格式错误,忽略该参数 + app.logger.warning(f"无效的交易日参数: {tday}, 错误: {e}") + elif recent_days: from datetime import datetime, timedelta cutoff_date = datetime.now() - timedelta(days=recent_days) query = query.filter(Event.created_at >= cutoff_date) @@ -6786,6 +6855,8 @@ def api_get_events(): applied_filters['type'] = event_type if importance != 'all': applied_filters['importance'] = importance + if tday: + applied_filters['tday'] = tday if start_date: applied_filters['start_date'] = start_date if end_date: diff --git a/test_create_event.py b/test_create_event.py deleted file mode 100644 index 5a475cc9..00000000 --- a/test_create_event.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -测试脚本:手动创建事件到数据库 -用于测试 WebSocket 实时推送功能 -""" - -import sys -from datetime import datetime -from sqlalchemy import create_engine, Column, Integer, String, Text, Float, DateTime -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -# 数据库连接(从 app.py 复制) -DATABASE_URI = 'mysql+pymysql://root:Zzl5588161!@111.198.58.126:33060/stock?charset=utf8mb4' - -engine = create_engine(DATABASE_URI, echo=False) -Session = sessionmaker(bind=engine) -session = Session() - -Base = declarative_base() - -# Event 模型(简化版,只包含必要字段) -class Event(Base): - __tablename__ = 'events' - - id = Column(Integer, primary_key=True) - title = Column(String(500), nullable=False) - description = Column(Text) - event_type = Column(String(100)) - importance = Column(String(10)) - status = Column(String(50), default='active') - hot_score = Column(Float, default=0) - view_count = Column(Integer, default=0) - created_at = Column(DateTime, default=datetime.now) - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) - - -def create_test_event(): - """创建一个测试事件""" - - import random - - event_types = ['policy', 'market', 'tech', 'industry', 'finance'] - importances = ['S', 'A', 'B', 'C'] - - test_event = Event( - title=f'测试事件 - {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', - description=f'这是一个用于测试 WebSocket 实时推送的事件,创建于 {datetime.now()}', - event_type=random.choice(event_types), - importance=random.choice(importances), - status='active', - hot_score=round(random.uniform(50, 100), 2), - view_count=random.randint(100, 1000) - ) - - try: - session.add(test_event) - session.commit() - - print("✅ 测试事件创建成功!") - print(f" ID: {test_event.id}") - print(f" 标题: {test_event.title}") - print(f" 类型: {test_event.event_type}") - print(f" 重要性: {test_event.importance}") - print(f" 热度: {test_event.hot_score}") - print(f"\n💡 提示: 轮询将在 2 分钟内检测到此事件并推送到前端") - print(f" (如果需要立即推送,请将轮询间隔改为更短)") - - return test_event.id - - except Exception as e: - session.rollback() - print(f"❌ 创建事件失败: {e}") - return None - finally: - session.close() - - -def create_multiple_events(count=3): - """创建多个测试事件""" - print(f"正在创建 {count} 个测试事件...\n") - - for i in range(count): - event_id = create_test_event() - if event_id: - print(f"[{i+1}/{count}] 事件 #{event_id} 创建成功\n") - else: - print(f"[{i+1}/{count}] 创建失败\n") - - print(f"\n✅ 完成!共创建 {count} 个事件") - - -if __name__ == '__main__': - print("=" * 60) - print("WebSocket 事件推送测试 - 手动创建事件") - print("=" * 60) - print() - - if len(sys.argv) > 1: - try: - count = int(sys.argv[1]) - create_multiple_events(count) - except ValueError: - print("❌ 参数必须是数字") - print("用法: python test_create_event.py [数量]") - else: - # 默认创建 1 个事件 - create_test_event() - - print("\n" + "=" * 60) diff --git a/test_events_api.py b/test_events_api.py new file mode 100644 index 00000000..4d6e5c65 --- /dev/null +++ b/test_events_api.py @@ -0,0 +1,147 @@ +""" +测试 /api/events 接口的分页和交易日筛选功能 +""" +import requests +from datetime import datetime, timedelta + +# 接口地址 +BASE_URL = "http://localhost:5001" +EVENTS_API = f"{BASE_URL}/api/events" + +def test_pagination(): + """测试分页功能""" + print("\n=== 测试分页功能 ===") + + # 测试第一页 + params = { + 'page': 1, + 'per_page': 5 + } + response = requests.get(EVENTS_API, params=params) + data = response.json() + + if data.get('success'): + pagination = data['data']['pagination'] + print(f"✓ 第一页请求成功") + print(f" - 当前页: {pagination['page']}") + print(f" - 每页数量: {pagination['per_page']}") + print(f" - 总记录数: {pagination['total']}") + print(f" - 总页数: {pagination['pages']}") + print(f" - 是否有下一页: {pagination['has_next']}") + print(f" - 本页事件数: {len(data['data']['events'])}") + + # 测试第二页 + if pagination['has_next']: + params['page'] = 2 + response = requests.get(EVENTS_API, params=params) + data = response.json() + if data.get('success'): + print(f"✓ 第二页请求成功,返回 {len(data['data']['events'])} 个事件") + else: + print(f"✗ 请求失败: {data.get('error')}") + +def test_trading_day_filter(): + """测试交易日筛选功能""" + print("\n=== 测试交易日筛选功能 ===") + + # 测试使用 YYYY-MM-DD 格式 + tday = "2024-11-01" # 使用一个交易日 + params = { + 'tday': tday, + 'per_page': 10 + } + response = requests.get(EVENTS_API, params=params) + data = response.json() + + if data.get('success'): + print(f"✓ 交易日筛选成功 (格式: YYYY-MM-DD)") + print(f" - 交易日: {tday}") + print(f" - 筛选到的事件数: {len(data['data']['events'])}") + print(f" - 总记录数: {data['data']['pagination']['total']}") + + if data['data']['events']: + print(f" - 第一个事件创建时间: {data['data']['events'][0]['created_at']}") + + # 检查 applied_filters + filters = data['data']['filters']['applied_filters'] + if 'tday' in filters: + print(f" - 应用的交易日筛选: {filters['tday']}") + else: + print(f"✗ 请求失败: {data.get('error')}") + + # 测试使用 YYYY/M/D 格式 + tday2 = "2024/11/1" + params['tday'] = tday2 + response = requests.get(EVENTS_API, params=params) + data = response.json() + + if data.get('success'): + print(f"✓ 交易日筛选成功 (格式: YYYY/M/D)") + print(f" - 交易日: {tday2}") + print(f" - 筛选到的事件数: {len(data['data']['events'])}") + else: + print(f"✗ 请求失败: {data.get('error')}") + +def test_combined_filters(): + """测试组合筛选功能""" + print("\n=== 测试组合筛选 (分页 + 交易日) ===") + + params = { + 'tday': '2024-10-31', + 'page': 1, + 'per_page': 3, + 'status': 'active' + } + response = requests.get(EVENTS_API, params=params) + data = response.json() + + if data.get('success'): + print(f"✓ 组合筛选成功") + print(f" - 筛选到的事件数: {len(data['data']['events'])}") + print(f" - 应用的筛选条件: {data['data']['filters']['applied_filters']}") + + if data['data']['events']: + for i, event in enumerate(data['data']['events'], 1): + print(f" - 事件{i}: {event['title'][:30]}... (创建时间: {event['created_at']})") + else: + print(f"✗ 请求失败: {data.get('error')}") + +def test_latest_trading_day(): + """测试获取最新数据(不传 tday 参数)""" + print("\n=== 测试获取最新数据 ===") + + params = { + 'page': 1, + 'per_page': 5, + 'sort': 'new' + } + response = requests.get(EVENTS_API, params=params) + data = response.json() + + if data.get('success'): + print(f"✓ 获取最新数据成功") + print(f" - 返回事件数: {len(data['data']['events'])}") + if data['data']['events']: + print(f" - 最新事件: {data['data']['events'][0]['title'][:50]}") + print(f" - 创建时间: {data['data']['events'][0]['created_at']}") + else: + print(f"✗ 请求失败: {data.get('error')}") + +if __name__ == "__main__": + print("开始测试 /api/events 接口") + print("=" * 50) + + try: + # 测试各项功能 + test_pagination() + test_trading_day_filter() + test_combined_filters() + test_latest_trading_day() + + print("\n" + "=" * 50) + print("测试完成!") + + except requests.exceptions.ConnectionError: + print("\n✗ 连接失败:请确保后端服务正在运行 (python app.py)") + except Exception as e: + print(f"\n✗ 测试出错: {e}")