Compare commits
14 Commits
feature_20
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df41ef2e61 | ||
| 0d84ffe87f | |||
|
|
cb84b0238a | ||
|
|
376b5f66cd | ||
|
|
f3c7e016ac | ||
| 8417ab17be | |||
| dd59cb6385 | |||
| 512aca16d8 | |||
|
|
6a51fc3c88 | ||
|
|
7cca5e73c0 | ||
|
|
112fbbd42d | ||
|
|
3a4dade8ec | ||
|
|
6f81259f8c | ||
|
|
864844a52b |
133
CLAUDE.md
133
CLAUDE.md
@@ -4,40 +4,61 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## 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)
|
### Frontend (React + Chakra UI)
|
||||||
- **Framework**: React 18.3.1 with Chakra UI 2.8.2
|
- **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
|
- **Styling**: Tailwind CSS + custom Chakra theme
|
||||||
- **Build Tool**: React Scripts with custom Gulp tasks
|
- **Build Tool**: CRACO (Create React App Configuration Override) with custom webpack optimizations
|
||||||
- **Charts**: ApexCharts, ECharts, and custom visualization components
|
- **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)
|
### Backend (Flask/Python)
|
||||||
- **Framework**: Flask with SQLAlchemy ORM
|
- **Framework**: Flask with SQLAlchemy ORM
|
||||||
- **Database**: ClickHouse for analytics + MySQL/PostgreSQL
|
- **Database**: ClickHouse for analytics queries + MySQL/PostgreSQL
|
||||||
- **Features**: Real-time data processing, trading analysis, user authentication
|
- **Real-time**: Flask-SocketIO for WebSocket connections
|
||||||
- **Task Queue**: Celery for background processing
|
- **Task Queue**: Celery with Redis for background processing
|
||||||
|
- **External APIs**: Tencent Cloud SMS, WeChat Pay integration
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
### Frontend Development
|
### Frontend Development
|
||||||
```bash
|
```bash
|
||||||
npm start # Start development server (port 3000, proxies to localhost:5001)
|
npm start # Start with mock data (.env.mock), proxies to localhost:5001
|
||||||
npm run build # Production build with license headers
|
npm run start:real # Start with real backend (.env.local)
|
||||||
npm test # Run React test suite
|
npm run start:dev # Start with development config (.env.development)
|
||||||
npm run lint:check # Check ESLint rules
|
npm run start:test # Starts both backend (app_2.py) and frontend (.env.test) concurrently
|
||||||
npm run lint:fix # Auto-fix ESLint issues
|
npm run dev # Alias for 'npm start'
|
||||||
npm run install:clean # Clean install (removes node_modules and package-lock)
|
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
|
### Backend Development
|
||||||
```bash
|
```bash
|
||||||
python app_2.py # Start Flask server (main backend)
|
python app.py # Main Flask server (newer version)
|
||||||
python simulation_background_processor.py # Background data processor
|
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
|
### Python Dependencies
|
||||||
Install from requirements.txt:
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
@@ -45,47 +66,69 @@ pip install -r requirements.txt
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Frontend Structure
|
### Frontend Structure
|
||||||
- `src/layouts/` - Main layout components (Admin, Auth, Home)
|
- **src/App.js** - Main application entry with route definitions (routing moved from src/routes.js)
|
||||||
- `src/views/` - Page components organized by feature (Dashboard, Company, Community, etc.)
|
- **src/layouts/** - Layout wrappers (Auth, Home, MainLayout)
|
||||||
- `src/components/` - Reusable UI components (Charts, Cards, Buttons, etc.)
|
- **src/views/** - Page components (Community, Company, TradingSimulation, etc.)
|
||||||
- `src/theme/` - Chakra UI theme customization
|
- **src/components/** - Reusable UI components
|
||||||
- `src/routes.js` - Application routing configuration
|
- **src/contexts/** - React contexts (AuthContext, NotificationContext, IndustryContext)
|
||||||
- `src/contexts/` - React context providers
|
- **src/store/** - Redux store with slices (posthogSlice, etc.)
|
||||||
- `src/services/` - API service layer
|
- **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
|
### Backend Structure
|
||||||
- `app_2.py` - Main Flask application with routes and business logic
|
- **app.py / app_2.py** - Main Flask application with routes, authentication, and business logic
|
||||||
- `simulation_background_processor.py` - Background data processing service
|
- **simulation_background_processor.py** - Background processor for trading simulations
|
||||||
- `wechat_pay.py` / `wechat_pay_config.py` - Payment integration
|
- **wechat_pay.py / wechat_pay_config.py** - WeChat payment integration
|
||||||
- `tdays.csv` - Trading days data
|
- **concept_api.py** - API for concept/industry analysis
|
||||||
|
- **tdays.csv** - Trading days calendar data (loaded into memory at startup)
|
||||||
|
|
||||||
### Key Integrations
|
### Key Integrations
|
||||||
- ClickHouse for high-performance analytics queries
|
- **ClickHouse** - High-performance analytics queries
|
||||||
- Celery + Redis for background task processing
|
- **Celery + Redis** - Background task processing
|
||||||
- Flask-SocketIO for real-time data updates
|
- **Flask-SocketIO** - Real-time data updates via WebSocket
|
||||||
- Tencent Cloud services (SMS, etc.)
|
- **Tencent Cloud** - SMS services
|
||||||
- WeChat Pay integration
|
- **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
|
## Configuration
|
||||||
|
|
||||||
### Proxy Setup
|
|
||||||
The React dev server proxies API calls to `http://localhost:5001` (see package.json).
|
|
||||||
|
|
||||||
### Environment Files
|
### 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
|
### Build Configuration (craco.config.js)
|
||||||
The build process includes custom Gulp tasks that add Creative Tim license headers to JS, CSS, and HTML files.
|
- **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
|
### Important Build Notes
|
||||||
- Tailwind CSS for utility classes
|
- Uses NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' for Node compatibility
|
||||||
- Custom Chakra UI theme with extended color palette
|
- Gulp task adds Creative Tim license headers post-build
|
||||||
- Component-specific SCSS files in `src/assets/scss/`
|
- Bundle analyzer available via `ANALYZE=true npm run build:analyze`
|
||||||
|
- Pre-build: kills any process on port 3000
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
- React Testing Library setup for frontend components
|
- **React Testing Library** for component tests
|
||||||
- Test command: `npm test`
|
- **MSW** (Mock Service Worker) for API mocking during tests
|
||||||
|
- Run tests: `npm test`
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
- Build: `npm run build`
|
- Deployment scripts in **scripts/** directory
|
||||||
- Deploy: `npm run deploy` (builds the project)
|
- Build output processed by Gulp for licensing
|
||||||
|
- Supports rollback via scripts/rollback-from-local.sh
|
||||||
282
app.py
282
app.py
@@ -97,6 +97,38 @@ def get_trading_day_near_date(target_date):
|
|||||||
return trading_days[-1] if trading_days else None
|
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()
|
load_trading_days()
|
||||||
|
|
||||||
@@ -1849,6 +1881,15 @@ def send_verification_code():
|
|||||||
if not credential or not code_type:
|
if not credential or not code_type:
|
||||||
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
||||||
|
|
||||||
|
# 清理格式字符(空格、横线、括号等)
|
||||||
|
if code_type == 'phone':
|
||||||
|
# 移除手机号中的空格、横线、括号、加号等格式字符
|
||||||
|
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
|
||||||
|
print(f"📱 清理后的手机号: {credential}")
|
||||||
|
elif code_type == 'email':
|
||||||
|
# 邮箱只移除空格
|
||||||
|
credential = credential.strip()
|
||||||
|
|
||||||
# 生成验证码
|
# 生成验证码
|
||||||
verification_code = generate_verification_code()
|
verification_code = generate_verification_code()
|
||||||
|
|
||||||
@@ -1907,6 +1948,17 @@ def login_with_verification_code():
|
|||||||
if not credential or not verification_code or not login_type:
|
if not credential or not verification_code or not login_type:
|
||||||
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
|
||||||
|
|
||||||
|
# 清理格式字符(空格、横线、括号等)
|
||||||
|
if login_type == 'phone':
|
||||||
|
# 移除手机号中的空格、横线、括号、加号等格式字符
|
||||||
|
original_credential = credential
|
||||||
|
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
|
||||||
|
if original_credential != credential:
|
||||||
|
print(f"📱 登录时清理手机号: {original_credential} -> {credential}")
|
||||||
|
elif login_type == 'email':
|
||||||
|
# 邮箱只移除前后空格
|
||||||
|
credential = credential.strip()
|
||||||
|
|
||||||
# 检查验证码
|
# 检查验证码
|
||||||
session_key = f'verification_code_{login_type}_{credential}_login'
|
session_key = f'verification_code_{login_type}_{credential}_login'
|
||||||
stored_code_info = session.get(session_key)
|
stored_code_info = session.get(session_key)
|
||||||
@@ -1968,12 +2020,51 @@ def login_with_verification_code():
|
|||||||
username = f"{base_username}_{counter}"
|
username = f"{base_username}_{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
# 如果用户不存在,自动创建新用户
|
||||||
|
if not user:
|
||||||
|
try:
|
||||||
|
# 生成用户名
|
||||||
|
if login_type == 'phone':
|
||||||
|
# 使用手机号生成用户名
|
||||||
|
base_username = f"用户{credential[-4:]}"
|
||||||
|
elif login_type == 'email':
|
||||||
|
# 使用邮箱前缀生成用户名
|
||||||
|
base_username = credential.split('@')[0]
|
||||||
|
else:
|
||||||
|
base_username = "新用户"
|
||||||
|
|
||||||
|
# 确保用户名唯一
|
||||||
|
username = base_username
|
||||||
|
counter = 1
|
||||||
|
while User.is_username_taken(username):
|
||||||
|
username = f"{base_username}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
# 创建新用户
|
# 创建新用户
|
||||||
user = User(username=username, email=credential)
|
user = User(username=username)
|
||||||
user.email_confirmed = True
|
|
||||||
|
# 设置手机号或邮箱
|
||||||
|
if login_type == 'phone':
|
||||||
|
user.phone = credential
|
||||||
|
elif login_type == 'email':
|
||||||
|
user.email = credential
|
||||||
|
|
||||||
|
# 设置默认密码(使用随机密码,用户后续可以修改)
|
||||||
|
user.set_password(uuid.uuid4().hex)
|
||||||
|
user.status = 'active'
|
||||||
|
user.nickname = username
|
||||||
|
|
||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
is_new_user = True
|
||||||
|
print(f"✅ 自动创建新用户: {username}, {login_type}: {credential}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 创建用户失败: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'success': False, 'error': '创建用户失败'}), 500
|
||||||
|
|
||||||
# 清除验证码
|
# 清除验证码
|
||||||
session.pop(session_key, None)
|
session.pop(session_key, None)
|
||||||
|
|
||||||
@@ -1989,10 +2080,13 @@ def login_with_verification_code():
|
|||||||
# 更新最后登录时间
|
# 更新最后登录时间
|
||||||
user.update_last_seen()
|
user.update_last_seen()
|
||||||
|
|
||||||
|
# 根据是否为新用户返回不同的消息
|
||||||
|
message = '注册成功,欢迎加入!' if is_new_user else '登录成功'
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': '注册成功' if is_new_user else '登录成功',
|
'message': message,
|
||||||
'isNewUser': is_new_user,
|
'is_new_user': is_new_user,
|
||||||
'user': {
|
'user': {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'username': user.username,
|
'username': user.username,
|
||||||
@@ -2004,6 +2098,59 @@ def login_with_verification_code():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"验证码登录错误: {e}")
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'success': False, 'error': '登录失败'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/register', methods=['POST'])
|
||||||
|
def register():
|
||||||
|
"""用户注册 - 使用Session"""
|
||||||
|
username = request.form.get('username')
|
||||||
|
email = request.form.get('email')
|
||||||
|
password = request.form.get('password')
|
||||||
|
|
||||||
|
# 验证输入
|
||||||
|
if not all([username, email, password]):
|
||||||
|
return jsonify({'success': False, 'error': '所有字段都是必填的'}), 400
|
||||||
|
|
||||||
|
# 检查用户名和邮箱是否已存在
|
||||||
|
if User.is_username_taken(username):
|
||||||
|
return jsonify({'success': False, 'error': '用户名已存在'}), 400
|
||||||
|
|
||||||
|
if User.is_email_taken(email):
|
||||||
|
return jsonify({'success': False, 'error': '邮箱已被使用'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 创建新用户
|
||||||
|
user = User(username=username, email=email)
|
||||||
|
user.set_password(password)
|
||||||
|
user.email_confirmed = True # 暂时默认已确认
|
||||||
|
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# 自动登录
|
||||||
|
session.permanent = True
|
||||||
|
session['user_id'] = user.id
|
||||||
|
session['username'] = user.username
|
||||||
|
session['logged_in'] = True
|
||||||
|
|
||||||
|
# Flask-Login 登录
|
||||||
|
login_user(user, remember=True)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '注册成功',
|
||||||
|
'user': {
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'nickname': user.nickname or user.username,
|
||||||
|
'email': user.email
|
||||||
|
}
|
||||||
|
}), 201
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
print(f"验证码登录/注册错误: {e}")
|
print(f"验证码登录/注册错误: {e}")
|
||||||
@@ -2619,11 +2766,19 @@ def wechat_callback():
|
|||||||
state = request.args.get('state')
|
state = request.args.get('state')
|
||||||
error = request.args.get('error')
|
error = request.args.get('error')
|
||||||
|
|
||||||
print(f"🎯 [CALLBACK] 微信回调被调用!code={code[:10] if code else None}..., state={state[:8] if state else None}..., error={error}")
|
# 错误处理:用户拒绝授权
|
||||||
|
if error:
|
||||||
|
if state in wechat_qr_sessions:
|
||||||
|
wechat_qr_sessions[state]['status'] = 'auth_denied'
|
||||||
|
wechat_qr_sessions[state]['error'] = '用户拒绝授权'
|
||||||
|
print(f"❌ 用户拒绝授权: state={state}")
|
||||||
|
return redirect('/auth/signin?error=wechat_auth_denied')
|
||||||
|
|
||||||
# 错误处理
|
# 参数验证
|
||||||
if error or not code or not state:
|
if not code or not state:
|
||||||
print(f"❌ [CALLBACK] 参数错误: error={error}, has_code={bool(code)}, has_state={bool(state)}")
|
if state in wechat_qr_sessions:
|
||||||
|
wechat_qr_sessions[state]['status'] = 'auth_failed'
|
||||||
|
wechat_qr_sessions[state]['error'] = '授权参数缺失'
|
||||||
return redirect('/auth/signin?error=wechat_auth_failed')
|
return redirect('/auth/signin?error=wechat_auth_failed')
|
||||||
|
|
||||||
# 验证state
|
# 验证state
|
||||||
@@ -2643,20 +2798,28 @@ def wechat_callback():
|
|||||||
return redirect('/auth/signin?error=session_expired')
|
return redirect('/auth/signin?error=session_expired')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 获取access_token
|
# 步骤1: 用户已扫码并授权(微信回调过来说明用户已完成扫码+授权)
|
||||||
print(f"🔑 [CALLBACK] 开始获取 access_token...")
|
session_data['status'] = 'scanned'
|
||||||
|
print(f"✅ 微信扫码回调: state={state}, code={code[:10]}...")
|
||||||
|
|
||||||
|
# 步骤2: 获取access_token
|
||||||
token_data = get_wechat_access_token(code)
|
token_data = get_wechat_access_token(code)
|
||||||
if not token_data:
|
if not token_data:
|
||||||
print(f"❌ [CALLBACK] 获取 access_token 失败")
|
session_data['status'] = 'auth_failed'
|
||||||
|
session_data['error'] = '获取访问令牌失败'
|
||||||
|
print(f"❌ 获取微信access_token失败: state={state}")
|
||||||
return redirect('/auth/signin?error=token_failed')
|
return redirect('/auth/signin?error=token_failed')
|
||||||
|
|
||||||
print(f"✅ [CALLBACK] 获取 access_token 成功, openid={token_data.get('openid', '')[:8]}...")
|
# 步骤3: Token获取成功,标记为已授权
|
||||||
|
session_data['status'] = 'authorized'
|
||||||
|
print(f"✅ 微信授权成功: openid={token_data['openid']}")
|
||||||
|
|
||||||
# 获取用户信息
|
# 步骤4: 获取用户信息
|
||||||
print(f"👤 [CALLBACK] 开始获取用户信息...")
|
|
||||||
user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
|
user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
|
||||||
if not user_info:
|
if not user_info:
|
||||||
print(f"❌ [CALLBACK] 获取用户信息失败")
|
session_data['status'] = 'auth_failed'
|
||||||
|
session_data['error'] = '获取用户信息失败'
|
||||||
|
print(f"❌ 获取微信用户信息失败: openid={token_data['openid']}")
|
||||||
return redirect('/auth/signin?error=userinfo_failed')
|
return redirect('/auth/signin?error=userinfo_failed')
|
||||||
|
|
||||||
print(f"✅ [CALLBACK] 获取用户信息成功, nickname={user_info.get('nickname', 'N/A')}")
|
print(f"✅ [CALLBACK] 获取用户信息成功, nickname={user_info.get('nickname', 'N/A')}")
|
||||||
@@ -2736,23 +2899,31 @@ def wechat_callback():
|
|||||||
db.session.add(user)
|
db.session.add(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# 更新 wechat_qr_sessions 状态,供前端轮询检测
|
is_new_user = True
|
||||||
print(f"🔍 [DEBUG] state={state}, state in wechat_qr_sessions: {state in wechat_qr_sessions}")
|
print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}")
|
||||||
|
|
||||||
|
# 更新最后登录时间
|
||||||
|
user.update_last_seen()
|
||||||
|
|
||||||
|
# 设置session
|
||||||
|
session.permanent = True
|
||||||
|
session['user_id'] = user.id
|
||||||
|
session['username'] = user.username
|
||||||
|
session['logged_in'] = True
|
||||||
|
session['wechat_login'] = True # 标记是微信登录
|
||||||
|
|
||||||
|
# Flask-Login 登录
|
||||||
|
login_user(user, remember=True)
|
||||||
|
|
||||||
|
# 更新微信session状态,供前端轮询检测
|
||||||
if state in wechat_qr_sessions:
|
if state in wechat_qr_sessions:
|
||||||
session_item = wechat_qr_sessions[state]
|
session_item = wechat_qr_sessions[state]
|
||||||
mode = session_item.get('mode')
|
# 仅处理登录/注册流程,不处理绑定流程
|
||||||
print(f"🔍 [DEBUG] session_item mode: {mode}, is_new_user: {is_new_user}")
|
if not session_item.get('mode'):
|
||||||
# 不是绑定模式才更新为登录状态
|
# 更新状态和用户信息
|
||||||
if not mode:
|
session_item['status'] = 'register_ready' if is_new_user else 'login_ready'
|
||||||
new_status = 'register_success' if is_new_user else 'login_success'
|
session_item['user_info'] = {'user_id': user.id}
|
||||||
session_item['status'] = new_status
|
print(f"✅ 微信扫码状态已更新: {session_item['status']}, user_id: {user.id}")
|
||||||
session_item['user_info'] = {
|
|
||||||
'user_id': user.id,
|
|
||||||
'is_new_user': is_new_user
|
|
||||||
}
|
|
||||||
print(f"✅ [DEBUG] 更新 wechat_qr_sessions 状态: {new_status}, user_id: {user.id}")
|
|
||||||
else:
|
|
||||||
print(f"⚠️ [DEBUG] 跳过状态更新,因为 mode={mode}")
|
|
||||||
|
|
||||||
# 返回一个简单的成功页面(前端轮询会检测到状态变化)
|
# 返回一个简单的成功页面(前端轮询会检测到状态变化)
|
||||||
return '''
|
return '''
|
||||||
@@ -2772,10 +2943,16 @@ def wechat_callback():
|
|||||||
''', 200
|
''', 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ [CALLBACK] 微信登录失败: {e}")
|
print(f"❌ 微信登录失败: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
print(f"❌ [CALLBACK] 错误堆栈:\n{traceback.format_exc()}")
|
traceback.print_exc()
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
|
# 更新session状态为失败
|
||||||
|
if state in wechat_qr_sessions:
|
||||||
|
wechat_qr_sessions[state]['status'] = 'auth_failed'
|
||||||
|
wechat_qr_sessions[state]['error'] = str(e)
|
||||||
|
|
||||||
return redirect('/auth/signin?error=login_failed')
|
return redirect('/auth/signin?error=login_failed')
|
||||||
|
|
||||||
|
|
||||||
@@ -6450,6 +6627,9 @@ def api_get_events():
|
|||||||
event_status = request.args.get('status', 'active')
|
event_status = request.args.get('status', 'active')
|
||||||
importance = request.args.get('importance', 'all')
|
importance = request.args.get('importance', 'all')
|
||||||
|
|
||||||
|
# 交易日筛选参数
|
||||||
|
tday = request.args.get('tday') # 交易日,格式:YYYY-MM-DD 或 YYYY/M/D
|
||||||
|
|
||||||
# 日期筛选参数
|
# 日期筛选参数
|
||||||
start_date = request.args.get('start_date')
|
start_date = request.args.get('start_date')
|
||||||
end_date = request.args.get('end_date')
|
end_date = request.args.get('end_date')
|
||||||
@@ -6535,7 +6715,41 @@ def api_get_events():
|
|||||||
text(f"JSON_SEARCH(keywords, 'one', '%{search_query}%') IS NOT NULL")
|
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
|
from datetime import datetime, timedelta
|
||||||
cutoff_date = datetime.now() - timedelta(days=recent_days)
|
cutoff_date = datetime.now() - timedelta(days=recent_days)
|
||||||
query = query.filter(Event.created_at >= cutoff_date)
|
query = query.filter(Event.created_at >= cutoff_date)
|
||||||
@@ -6641,6 +6855,8 @@ def api_get_events():
|
|||||||
applied_filters['type'] = event_type
|
applied_filters['type'] = event_type
|
||||||
if importance != 'all':
|
if importance != 'all':
|
||||||
applied_filters['importance'] = importance
|
applied_filters['importance'] = importance
|
||||||
|
if tday:
|
||||||
|
applied_filters['tday'] = tday
|
||||||
if start_date:
|
if start_date:
|
||||||
applied_filters['start_date'] = start_date
|
applied_filters['start_date'] = start_date
|
||||||
if end_date:
|
if end_date:
|
||||||
|
|||||||
@@ -160,11 +160,12 @@ export default function AuthFormContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(credential)) {
|
// 清理手机号格式字符(空格、横线、括号等)
|
||||||
// 追踪手机号验证失败
|
const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, '');
|
||||||
|
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) {
|
||||||
authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format');
|
authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format');
|
||||||
authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号');
|
authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "请输入有效的手机号",
|
title: "请输入有效的手机号",
|
||||||
status: "warning",
|
status: "warning",
|
||||||
@@ -180,7 +181,7 @@ export default function AuthFormContent() {
|
|||||||
setSendingCode(true);
|
setSendingCode(true);
|
||||||
|
|
||||||
const requestData = {
|
const requestData = {
|
||||||
credential: credential.trim(), // 添加 trim() 防止空格
|
credential: cleanedCredential, // 使用清理后的手机号
|
||||||
type: 'phone',
|
type: 'phone',
|
||||||
purpose: config.api.purpose
|
purpose: config.api.purpose
|
||||||
};
|
};
|
||||||
@@ -221,13 +222,13 @@ export default function AuthFormContent() {
|
|||||||
|
|
||||||
// ❌ 移除成功 toast,静默处理
|
// ❌ 移除成功 toast,静默处理
|
||||||
logger.info('AuthFormContent', '验证码发送成功', {
|
logger.info('AuthFormContent', '验证码发送成功', {
|
||||||
credential: credential.substring(0, 3) + '****' + credential.substring(7),
|
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
|
||||||
dev_code: data.dev_code
|
dev_code: data.dev_code
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 开发环境下在控制台显示验证码
|
// ✅ 开发环境下在控制台显示验证码
|
||||||
if (data.dev_code) {
|
if (data.dev_code) {
|
||||||
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
|
console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
|
||||||
}
|
}
|
||||||
|
|
||||||
setVerificationCodeSent(true);
|
setVerificationCodeSent(true);
|
||||||
@@ -244,7 +245,7 @@ export default function AuthFormContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.api.error('POST', '/api/auth/send-verification-code', error, {
|
logger.api.error('POST', '/api/auth/send-verification-code', error, {
|
||||||
credential: credential.substring(0, 3) + '****' + credential.substring(7)
|
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7)
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ 显示错误提示给用户
|
// ✅ 显示错误提示给用户
|
||||||
@@ -286,7 +287,10 @@ export default function AuthFormContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
// 清理手机号格式字符(空格、横线、括号等)
|
||||||
|
const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, '');
|
||||||
|
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) {
|
||||||
toast({
|
toast({
|
||||||
title: "请输入有效的手机号",
|
title: "请输入有效的手机号",
|
||||||
status: "warning",
|
status: "warning",
|
||||||
@@ -300,13 +304,13 @@ export default function AuthFormContent() {
|
|||||||
|
|
||||||
// 构建请求体
|
// 构建请求体
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
credential: phone.trim(), // 添加 trim() 防止空格
|
credential: cleanedPhone, // 使用清理后的手机号
|
||||||
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
|
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
|
||||||
login_type: 'phone',
|
login_type: 'phone',
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.api.request('POST', '/api/auth/login-with-code', {
|
logger.api.request('POST', '/api/auth/login-with-code', {
|
||||||
credential: phone.substring(0, 3) + '****' + phone.substring(7),
|
credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7),
|
||||||
verification_code: verificationCode.substring(0, 2) + '****',
|
verification_code: verificationCode.substring(0, 2) + '****',
|
||||||
login_type: 'phone'
|
login_type: 'phone'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ const getStatusColor = (status) => {
|
|||||||
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
|
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
|
||||||
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
|
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
|
||||||
case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600";
|
case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600";
|
||||||
|
case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字
|
||||||
|
case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字
|
||||||
default: return "gray.600";
|
default: return "gray.600";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -248,6 +250,33 @@ export default function WechatRegister() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 处理用户拒绝授权
|
||||||
|
else if (status === WECHAT_STATUS.AUTH_DENIED) {
|
||||||
|
clearTimers();
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
toast({
|
||||||
|
title: "授权已取消",
|
||||||
|
description: "您已取消微信授权登录",
|
||||||
|
status: "warning",
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 处理授权失败
|
||||||
|
else if (status === WECHAT_STATUS.AUTH_FAILED) {
|
||||||
|
clearTimers();
|
||||||
|
if (isMountedRef.current) {
|
||||||
|
const errorMsg = response.error || "授权过程出现错误";
|
||||||
|
toast({
|
||||||
|
title: "授权失败",
|
||||||
|
description: errorMsg,
|
||||||
|
status: "error",
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
|
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
|
||||||
// 轮询过程中的错误不显示给用户,避免频繁提示
|
// 轮询过程中的错误不显示给用户,避免频繁提示
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export const WECHAT_STATUS = {
|
|||||||
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
||||||
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
|
||||||
EXPIRED: 'expired',
|
EXPIRED: 'expired',
|
||||||
|
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
|
||||||
|
AUTH_FAILED: 'auth_failed', // 授权失败
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,6 +159,8 @@ export const STATUS_MESSAGES = {
|
|||||||
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
|
||||||
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
|
||||||
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
|
||||||
|
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
|
||||||
|
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default authService;
|
export default authService;
|
||||||
|
|||||||
@@ -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)
|
|
||||||
147
test_events_api.py
Normal file
147
test_events_api.py
Normal file
@@ -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}")
|
||||||
Reference in New Issue
Block a user