Compare commits

..

14 Commits

Author SHA1 Message Date
zdl
df41ef2e61 Merge branch 'feature' into develop
* feature: (33 commits)
  修改总结
  feat: API优化
  feat: mock数据添加
  feat: 修改内容:添加风险提示到K线图弹窗
  feat:修复mock数据
  feat:  访问"概念中心"页面      2. 点击任意概念卡片进入概念详情      3. 点击"历史时间轴"按钮(需要Max会员权限)      4. 查看弹窗底部是否显示风险提示 & mock数据处理
  feat: 事件中心股票详情添加风险提示
  feat: 涨停分析/股票详情弹窗 添加风险提示
  feat: 添加mock数据
  feat: 事件中心 事件详情底部添加风险提示
  feat: 添加mock数据
  feat: 核心页面添加风险提示
  feat: 创建风险提示通用组件
  feat: bugfix
  feat: 优化packge.json
  feat: package.json 优化方案
  feat:  任务 1: 集成 TradingSimulation 追踪事件任务 2: 传递 tradingEvents 到子组件
  feat: 统一的Hook架构
  feat: 集成导航上报
  feat: 已完成的工作:   -  创建了4个P1优先级Hook(搜索、导航、个人资料、订阅)   -  将其中3个Hook集成到5个组件中   -  在个人资料、设置、搜索、订阅流程中添加了15+个追踪点   -  覆盖了完整的收入漏斗(支付发起 → 成功 → 订阅创建)   -  添加了留存追踪(个人资料更新、设置修改、搜索查询)
  ...
2025-11-03 17:41:12 +08:00
0d84ffe87f 修改总结 2025-11-03 16:10:35 +08:00
zdl
cb84b0238a Merge branch 'feature_2025/251029_legal_notice' into feature
* feature_2025/251029_legal_notice: (32 commits)
  feat: API优化
  feat: mock数据添加
  feat: 修改内容:添加风险提示到K线图弹窗
  feat:修复mock数据
  feat:  访问"概念中心"页面      2. 点击任意概念卡片进入概念详情      3. 点击"历史时间轴"按钮(需要Max会员权限)      4. 查看弹窗底部是否显示风险提示 & mock数据处理
  feat: 事件中心股票详情添加风险提示
  feat: 涨停分析/股票详情弹窗 添加风险提示
  feat: 添加mock数据
  feat: 事件中心 事件详情底部添加风险提示
  feat: 添加mock数据
  feat: 核心页面添加风险提示
  feat: 创建风险提示通用组件
  feat: bugfix
  feat: 优化packge.json
  feat: package.json 优化方案
  feat:  任务 1: 集成 TradingSimulation 追踪事件任务 2: 传递 tradingEvents 到子组件
  feat: 统一的Hook架构
  feat: 集成导航上报
  feat: 已完成的工作:   -  创建了4个P1优先级Hook(搜索、导航、个人资料、订阅)   -  将其中3个Hook集成到5个组件中   -  在个人资料、设置、搜索、订阅流程中添加了15+个追踪点   -  覆盖了完整的收入漏斗(支付发起 → 成功 → 订阅创建)   -  添加了留存追踪(个人资料更新、设置修改、搜索查询)
  feat: P1通用功能:4个Hook创建完成(待集成)现在您可以追踪:
  ...
2025-10-30 10:41:24 +08:00
zdl
376b5f66cd Merge branch 'feature' into develop
* feature:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  pref: 去除坏味道
  feat: 微信登陆逻辑调整
  feat: 微信mock数据调整
  整合register端口进入login端口
  整合register端口进入login端口
  整合register端口进入login端口
  feat: 文案调整
  整合register端口进入login端口
  feat: 修复首页新闻中心卡片布局跳变问题
  整合register端口进入login端口
  fix: 修复首页路由跳转失败的问题
  修改个股详情中桑基图提示Stack: Error: Sankey is a DAG
  bugfix:调整
  feat: 路由链接调整
  bugfix:修复警告错误
  feat: 错误logger 不在被error页面捕获
2025-10-29 16:27:53 +08:00
zdl
f3c7e016ac Merge branch '1028_bugfix' into feature
* 1028_bugfix:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  整合register端口进入login端口
2025-10-29 16:27:31 +08:00
8417ab17be 手机号格式适配-前端修改 2025-10-29 11:20:41 +08:00
dd59cb6385 添加微信扫码的几种其他状态 2025-10-29 07:33:44 +08:00
512aca16d8 整合register端口进入login端口 2025-10-28 15:47:50 +08:00
zdl
6a51fc3c88 Merge branch 'feature' into develop 2025-10-27 18:03:17 +08:00
zdl
7cca5e73c0 Merge branch 'feature' into develop 2025-10-21 11:02:02 +08:00
zdl
112fbbd42d Merge branch 'feature' into develop 2025-10-17 15:01:54 +08:00
zdl
3a4dade8ec Merge branch 'main' into develop 2025-10-15 21:01:59 +08:00
zdl
6f81259f8c feat: 解决导航跳转失效的问题 2025-10-15 11:57:28 +08:00
zdl
864844a52b feat: 10.10线上最新代码提交 2025-10-15 11:56:34 +08:00
7 changed files with 531 additions and 197 deletions

131
CLAUDE.md
View File

@@ -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 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 lint:fix # Auto-fix ESLint issues
npm run install:clean # Clean install (removes node_modules and package-lock) 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
View File

@@ -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:

View File

@@ -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'
}); });

View File

@@ -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 });
// 轮询过程中的错误不显示给用户,避免频繁提示 // 轮询过程中的错误不显示给用户,避免频繁提示

View File

@@ -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;

View File

@@ -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
View 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}")