diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..3995dbee
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,1398 @@
+# CLAUDE.md
+
+> **🌐 语言偏好**: 请始终使用中文与用户交流,包括所有解释、分析、文档编写和代码注释。
+
+本文件为 Claude Code (claude.ai/code) 提供在此代码库中工作的指导说明。
+
+## 📖 文档结构导航
+
+本文档分为以下主要章节,建议按需查阅:
+
+**基础信息**:
+- [项目概览](#项目概览) - 技术栈、项目定位
+- [开发命令](#开发命令) - 常用命令速查
+- [配置](#配置) - 环境变量、MSW、路径别名、Webpack
+
+**架构设计**:
+- [架构](#架构) - 应用入口流程、路由架构
+- [前端目录结构详解](#前端目录结构详解) - 完整的目录说明与使用指南
+- [后端架构详解](#后端架构详解) - Flask、Celery、数据库架构
+
+**开发指南**:
+- [开发工作流](#开发工作流) - 路由、组件、API、Redux 开发指南
+- [常见开发任务](#常见开发任务) - 5 个详细的开发任务教程
+- [技术路径与开发指南](#技术路径与开发指南) - UI 框架选型、技术栈演进、最佳实践
+
+**技术决策与规范**:
+- [UI 框架选型与使用指南](#ui-框架选型与使用指南) - Ant Design + Chakra UI 混合策略
+- [架构设计原则](#架构设计原则) - 组件设计、状态管理、代码分割、API 层
+- [开发规范与最佳实践](#开发规范与最佳实践) - 命名规范、Git 工作流、代码审查
+
+**技术债务与维护**:
+- [技术债务与已知问题](#技术债务与已知问题) - 技术债务跟踪、优化机会
+- [更新本文档](#更新本文档) - 文档维护指南
+
+---
+
+## 项目概览
+
+混合式 React 仪表板,用于金融/交易分析,采用 Flask 后端。基于 Argon Dashboard Chakra PRO 模板构建。
+
+### 技术栈
+
+**前端**
+- **核心框架**: React 18.3.1
+- **UI 组件库**: Chakra UI 2.8.2(主要) + Ant Design 5.27.4(表格/表单)
+- **状态管理**: Redux Toolkit 2.9.2
+- **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割
+- **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化
+- **图表库**: ECharts 5.6.0、ApexCharts 3.27.3、Recharts 3.1.2、D3 7.9.0、Visx 3.12.0
+- **动画**: Framer Motion 4.1.17
+- **日历**: FullCalendar 5.9.0、React Big Calendar 0.33.2
+- **图标**: Lucide React 0.540.0(推荐)、React Icons 4.12.0、@ant-design/icons 6.0.0
+- **网络请求**: Axios 1.10.0
+- **实时通信**: Socket.IO Client 4.7.4
+- **数据分析**: PostHog 1.281.0
+- **开发工具**: MSW (Mock Service Worker) 用于 API mocking
+- **虚拟化**: @tanstack/react-virtual 3.13.12(性能优化)
+- **其他**: Draft.js(富文本编辑)、React Markdown、React Quill
+
+**后端**
+- Flask + SQLAlchemy ORM
+- ClickHouse(分析型数据库)+ MySQL/PostgreSQL(事务型数据库)
+- Flask-SocketIO 实现 WebSocket 实时更新
+- Celery + Redis 处理后台任务
+- 腾讯云短信 + 微信支付集成
+
+## 开发命令
+
+### 前端开发
+```bash
+npm start # 使用 mock 数据启动(.env.mock),代理到 localhost:5001
+npm run start:real # 使用真实后端启动(.env.local)
+npm run start:dev # 使用开发配置启动(.env.development)
+npm run start:test # 同时启动后端(app.py)和前端(.env.test)
+npm run dev # 'npm start' 的别名
+npm run backend # 仅启动 Flask 服务器(python app.py)
+
+npm run build # 生产环境构建,包含 Gulp 许可证头
+npm run build:analyze # 使用 webpack bundle analyzer 构建
+npm test # 运行 React 测试套件(CRACO)
+
+npm run lint:check # 检查 ESLint 规则(退出码 0)
+npm run lint:fix # 自动修复 ESLint 问题
+npm run clean # 删除 node_modules 和 package-lock.json
+npm run reinstall # 清洁安装(运行 clean + install)
+```
+
+### 后端开发
+```bash
+python app.py # 主 Flask 服务器
+python simulation_background_processor.py # 交易模拟的后台任务处理器
+pip install -r requirements.txt # 安装 Python 依赖
+```
+
+### 部署
+```bash
+npm run deploy # 从本地部署(scripts/deploy-from-local.sh)
+npm run rollback # 回滚到上一个版本
+```
+
+## 架构
+
+### 应用入口流程
+```
+src/index.js
+└── src/App.js(根组件)
+ ├── AppProviders (src/providers/AppProviders.js)
+ │ ├── ReduxProvider(store 来自 src/store/)
+ │ ├── ChakraProvider(主题来自 src/theme/)
+ │ ├── NotificationProvider (src/contexts/NotificationContext.js)
+ │ └── AuthProvider (src/contexts/AuthContext.js)
+ ├── AppRoutes (src/routes/index.js)
+ │ ├── MainLayout 路由(带导航栏/页脚)
+ │ └── 独立路由(认证页面、全屏视图)
+ └── GlobalComponents(模态框覆盖层、全局 UI)
+```
+
+### 路由架构(模块化设计)
+路由采用**声明式**设计,并拆分到 `src/routes/` 中的多个文件:
+
+- **index.js** - 主路由器(组合配置 + 渲染路由)
+- **routeConfig.js** - 路由定义(路径、组件、保护模式、布局、子路由)
+- **lazy-components.js** - React.lazy() 导入,用于代码分割
+- **homeRoutes.js** - 嵌套的首页路由
+- **constants/** - 保护模式、布局映射
+- **utils/** - 路由渲染逻辑(wrapWithProtection、renderRoute)
+
+路由保护模式(PROTECTION_MODES):
+- `PUBLIC` - 无需认证
+- `MODAL` - 未登录时显示认证模态框
+- `REDIRECT` - 未登录时重定向到 /auth/sign-in
+
+### 前端目录结构详解
+
+本项目采用**功能驱动的目录结构**,按职责将代码组织到不同目录中。以下是完整的目录结构及详细说明:
+
+#### 核心目录概览
+
+```
+src/
+├── index.js # 应用入口文件
+├── App.js # 根组件(组合 Providers + Routes)
+│
+├── providers/ # Provider 组合层
+├── routes/ # 路由配置与管理
+├── layouts/ # 页面布局模板
+├── views/ # 页面级组件
+├── components/ # 可复用 UI 组件
+│
+├── contexts/ # React Context 状态管理
+├── store/ # Redux 全局状态管理
+├── hooks/ # 自定义 React Hooks
+│
+├── services/ # API 服务层
+├── utils/ # 工具函数库
+├── constants/ # 全局常量定义
+│
+├── theme/ # UI 主题配置
+├── assets/ # 静态资源
+├── styles/ # 全局样式
+│
+├── mocks/ # 开发环境 Mock 数据
+├── lib/ # 第三方库配置
+└── variables/ # 可配置变量
+```
+
+---
+
+#### 详细目录说明
+
+##### 📁 `src/providers/` - Provider 组合层
+
+**用途**: 集中管理应用的所有 Context Providers,避免在 App.js 中嵌套过深。
+
+**核心文件**:
+- `AppProviders.js` - 所有 Provider 的组合容器
+
+**Provider 嵌套顺序**(从外到内):
+```javascript
+ReduxProvider // 1. Redux 状态管理(最外层)
+ └─ ChakraProvider // 2. Chakra UI 主题系统
+ └─ ConfigProvider // 3. Ant Design 主题配置
+ └─ NotificationProvider // 4. 通知系统
+ └─ AuthProvider // 5. 认证系统(最内层)
+```
+
+**何时修改**:
+- 添加新的全局 Provider(如 i18n、Analytics)
+- 调整 Provider 顺序(注意依赖关系)
+
+---
+
+##### 📁 `src/routes/` - 路由系统
+
+**用途**: 模块化路由配置,采用声明式设计,支持懒加载和路由保护。
+
+**核心文件**:
+- `index.js` - 主路由器(渲染所有路由)
+- `routeConfig.js` - **路由配置中心**(所有路由定义)
+- `lazy-components.js` - React.lazy() 懒加载组件导入
+- `homeRoutes.js` - 首页嵌套路由
+- `constants/protectionModes.js` - 路由保护模式定义
+- `utils/wrapWithProtection.js` - 路由保护逻辑
+- `utils/renderRoute.js` - 路由渲染工具
+
+**路由保护模式**:
+- `PUBLIC` - 公开访问,无需登录
+- `MODAL` - 未登录时显示登录模态框(不跳转)
+- `REDIRECT` - 未登录时重定向到 /auth/sign-in
+
+**添加新路由的步骤**: 参见"常见开发任务 → 如何添加新的页面路由"
+
+**命名约定**:
+- 路由路径使用 kebab-case: `portfolio`, `trading-simulation`
+- 组件名使用 PascalCase: `Portfolio`, `TradingSimulation`
+
+---
+
+##### 📁 `src/layouts/` - 页面布局
+
+**用途**: 定义页面的通用布局结构(导航栏、侧边栏、页脚等),复用于多个页面。
+
+**核心文件**:
+- `MainLayout.js` - 主布局(带顶部导航栏 + 侧边栏 + 页脚)
+- `Auth.js` - 认证页面布局(无导航栏,纯净背景)
+
+**布局特性**:
+- 每个布局包含独立的 ErrorBoundary(错误隔离)
+- 通过 `` 渲染子路由内容
+- 布局内可访问认证状态(AuthContext)
+
+**何时创建新布局**:
+- 需要完全不同的页面结构(如打印页、全屏图表)
+- 特定页面需要自定义导航栏/侧边栏
+
+---
+
+##### 📁 `src/views/` - 页面级组件
+
+**用途**: 存放路由对应的页面组件,每个文件对应一个路由页面。
+
+**组织结构**:
+```
+views/
+├── Community/ # 社区页面(大型功能模块)
+│ ├── index.js # 页面入口
+│ ├── components/ # 页面专属组件
+│ │ ├── EventList/
+│ │ ├── EventCard/
+│ │ └── StockDetailPanel/
+│ ├── hooks/ # 页面专属 Hooks
+│ │ └── useCommunityData.js
+│ └── utils/ # 页面专属工具函数
+│
+├── TradingSimulation/ # 交易模拟页面
+│ ├── index.js
+│ ├── components/
+│ └── ...
+│
+├── Dashboard/ # 仪表板页面
+│ └── index.js
+│
+└── Portfolio/ # 投资组合页面(简单页面)
+ └── index.js
+```
+
+**命名约定**:
+- 目录名使用 PascalCase: `Community`, `TradingSimulation`
+- 主文件始终为 `index.js`(方便导入)
+
+**原则**:
+- 页面组件主要负责**数据获取和布局组合**,不应包含复杂的 UI 逻辑
+- 复杂 UI 逻辑应拆分到 `components/` 子目录
+- 页面超过 500 行时考虑拆分(参见"组件组织模式")
+
+---
+
+##### 📁 `src/components/` - 可复用 UI 组件
+
+**用途**: 存放跨页面复用的通用 UI 组件(按钮、卡片、表格、模态框等)。
+
+**组织结构**(推荐采用原子设计模式):
+```
+components/
+├── Atoms/ # 原子组件(1-50 行)
+│ ├── Button/
+│ ├── Badge/
+│ └── Icon/
+│
+├── Molecules/ # 分子组件(50-150 行)
+│ ├── Card/
+│ ├── StatCard/
+│ └── SearchBar/
+│
+├── Organisms/ # 有机体组件(150-500 行)
+│ ├── Navbar/
+│ ├── Sidebar/
+│ ├── DataTable/
+│ └── FilterPanel/
+│
+└── Templates/ # 模板组件(可选)
+ └── PageTemplate/
+```
+
+**何时添加新组件**:
+- 组件在 2+ 个页面中使用
+- 组件具有明确的职责和边界
+- 组件可独立测试
+
+**命名约定**:
+- 组件目录使用 PascalCase: `EventCard`, `StockTable`
+- 每个组件目录包含:
+ - `index.js` - 主导出文件
+ - `ComponentName.js` - 实现代码(可选,简单组件直接在 index.js)
+ - `ComponentName.test.js` - 测试文件(可选)
+
+**避免**:
+- 将页面特定组件放在这里(应放在 `views/{PageName}/components/`)
+- 过度拆分(不要为了拆分而拆分)
+
+---
+
+##### 📁 `src/contexts/` - React Context 状态管理
+
+**用途**: 存放使用 React Context API 管理的跨组件状态。
+
+**核心 Contexts**:
+- `AuthContext.js` - 认证状态(用户信息、登录状态、登录/登出方法)
+- `NotificationContext.js` - 通知系统(显示 Toast/Alert)
+- `SidebarContext.js` - 侧边栏状态(展开/收起)
+
+**何时使用 Context**:
+- 跨层级传递数据(避免 props drilling)
+- 不频繁变化的数据(主题、语言、认证状态)
+- 依赖注入场景
+
+**何时不使用 Context**:
+- 频繁变化的数据 → 使用 Redux
+- 服务端数据 → 使用 React Query(计划中)
+- 本地 UI 状态 → 使用 useState
+
+**Context 文件结构**:
+```javascript
+// AuthContext.js
+export const AuthContext = createContext();
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ // ...
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => useContext(AuthContext); // 自定义 Hook
+```
+
+---
+
+##### 📁 `src/store/` - Redux 全局状态管理
+
+**用途**: 使用 Redux Toolkit 管理全局状态(UI 状态、跨页面共享数据)。
+
+**目录结构**:
+```
+store/
+├── index.js # Store 配置(combineReducers)
+└── slices/ # Redux Slices
+ ├── authModalSlice.js # 认证模态框状态
+ ├── posthogSlice.js # PostHog 分析配置
+ ├── stockSlice.js # 股票数据
+ ├── industrySlice.js # 行业/概念数据
+ ├── subscriptionSlice.js # 用户订阅状态
+ └── communityDataSlice.js # 社区数据
+```
+
+**何时使用 Redux**:
+- 全局 UI 状态(模态框开关、侧边栏状态)
+- 需要在多个不相关组件间共享的数据
+- 需要持久化的状态(与 redux-persist 结合)
+- 需要中间件处理的复杂逻辑
+
+**Slice 命名约定**:
+- 文件名: `{feature}Slice.js` (如 `authModalSlice.js`)
+- Slice 名称: `{feature}` (如 `name: 'authModal'`)
+- Actions: 动词开头 (如 `openModal`, `closeModal`, `setUser`)
+
+**添加新 Slice**: 参见"常见开发任务 → 如何添加新的 Redux Slice"
+
+---
+
+##### 📁 `src/hooks/` - 自定义 React Hooks
+
+**用途**: 存放可复用的自定义 Hooks,封装常见逻辑模式。
+
+**Hook 类型示例**:
+```
+hooks/
+├── useAuth.js # 认证相关(实际导出自 AuthContext)
+├── useStockData.js # 数据获取 Hook
+├── useDebounce.js # 防抖 Hook
+├── useIntersectionObserver.js # 懒加载/无限滚动
+├── useLocalStorage.js # 本地存储持久化
+├── useMediaQuery.js # 响应式查询
+└── usePrevious.js # 获取前一个值
+```
+
+**命名约定**:
+- 文件名: `use{PascalCase}.js` (如 `useStockData.js`)
+- 必须以 `use` 开头(React Hooks 规则)
+- 一个文件导出一个 Hook
+
+**Hook 设计原则**:
+- 职责单一(只做一件事)
+- 返回对象或数组(不返回原始值)
+- 包含完整的状态管理(loading, error, data)
+
+**何时创建新 Hook**:
+- 逻辑在 2+ 个组件中重复
+- 组件逻辑可以独立测试
+- 需要封装浏览器 API(localStorage, IntersectionObserver)
+
+---
+
+##### 📁 `src/services/` - API 服务层
+
+**用途**: 封装所有后端 API 调用,提供统一的接口供组件调用。
+
+**组织结构**(按业务领域划分):
+```
+services/
+├── authService.js # 认证相关 API(登录、注册、登出)
+├── stockService.js # 股票数据 API
+├── portfolioService.js # 投资组合 API
+├── communityService.js # 社区内容 API
+├── tradingService.js # 交易模拟 API
+└── userService.js # 用户信息 API
+```
+
+**服务函数命名约定**:
+- `fetch{Entity}` - 获取数据 (如 `fetchStockData`)
+- `create{Entity}` - 创建记录 (如 `createOrder`)
+- `update{Entity}` - 更新记录 (如 `updateProfile`)
+- `delete{Entity}` - 删除记录 (如 `deletePost`)
+
+**标准 API 调用结构**:
+```javascript
+// stockService.js
+import axios from 'axios';
+import { getApiBase } from '@utils/apiConfig';
+
+const api = axios.create({
+ baseURL: getApiBase(),
+ timeout: 10000,
+});
+
+export const fetchStockData = async (stockCode) => {
+ try {
+ const response = await api.get(`/api/stocks/${stockCode}`);
+ return response.data;
+ } catch (error) {
+ console.error('fetchStockData error:', error);
+ throw error;
+ }
+};
+```
+
+**原则**:
+- 组件不应直接调用 axios(通过 service 层)
+- 每个 service 函数应包含错误处理
+- 使用 `getApiBase()` 获取 API 基础 URL(支持 Mock 模式)
+
+---
+
+##### 📁 `src/utils/` - 工具函数库
+
+**用途**: 存放纯函数工具、格式化工具、计算逻辑等不依赖 React 的通用代码。
+
+**核心工具文件**:
+```
+utils/
+├── apiConfig.js # API 配置(getApiBase, isMockMode)
+├── priceFormatters.js # 价格/数字格式化函数
+├── dateFormatters.js # 日期格式化函数
+├── logger.js # 统一日志工具
+├── validators.js # 表单验证函数
+├── calculations.js # 金融计算函数
+└── string.js # 字符串处理函数
+```
+
+**命名约定**:
+- 文件名: `camelCase.js` (如 `priceFormatters.js`)
+- 函数名: `camelCase` (如 `formatPrice`, `calculateProfit`)
+- 导出方式: 命名导出 (如 `export const formatPrice = ...`)
+
+**工具函数特点**:
+- **纯函数**: 相同输入始终返回相同输出,无副作用
+- **可测试**: 易于编写单元测试
+- **可复用**: 多处使用的逻辑
+
+**示例**:
+```javascript
+// priceFormatters.js
+export const formatPrice = (price, currency = '¥') => {
+ return `${currency}${price.toFixed(2)}`;
+};
+
+export const formatPercent = (value, decimals = 2) => {
+ return `${(value * 100).toFixed(decimals)}%`;
+};
+```
+
+---
+
+##### 📁 `src/constants/` - 全局常量定义
+
+**用途**: 存放全局常量、枚举值、配置对象等不可变数据。
+
+**常见常量文件**:
+```
+constants/
+├── animations.js # 动画配置(duration、easing)
+├── routes.js # 路由路径常量
+├── apiEndpoints.js # API 端点常量
+├── colors.js # 颜色常量(补充主题)
+├── tradingConfig.js # 交易相关配置
+└── errorMessages.js # 错误消息常量
+```
+
+**命名约定**:
+- 常量名使用 SCREAMING_SNAKE_CASE: `API_BASE_URL`, `MAX_RETRY_COUNT`
+- 配置对象使用 camelCase: `animationConfig`, `chartColors`
+
+**示例**:
+```javascript
+// animations.js
+export const ANIMATION_DURATION = {
+ FAST: 150,
+ NORMAL: 300,
+ SLOW: 500,
+};
+
+export const EASING = {
+ EASE_IN_OUT: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ EASE_OUT: 'cubic-bezier(0, 0, 0.2, 1)',
+};
+```
+
+**原则**:
+- 避免魔法数字/字符串(在代码中硬编码)
+- 方便统一修改(单一数据源)
+- 提高代码可读性
+
+---
+
+##### 📁 `src/theme/` - UI 主题配置
+
+**用途**: 定义 Chakra UI 主题配置(颜色、字体、间距、组件样式等)。
+
+**核心文件**:
+```
+theme/
+├── theme.js # 主题主文件(导出完整主题)
+├── foundations/ # 基础样式(颜色、字体、间距)
+│ ├── colors.js
+│ ├── typography.js
+│ └── spacing.js
+└── components/ # 组件级主题覆盖
+ ├── button.js
+ ├── card.js
+ └── modal.js
+```
+
+**主题配置示例**:
+```javascript
+// theme.js
+import { extendTheme } from '@chakra-ui/react';
+import { colors } from './foundations/colors';
+import { ButtonStyles as Button } from './components/button';
+
+const theme = extendTheme({
+ colors,
+ fonts: {
+ heading: `'Open Sans', sans-serif`,
+ body: `'Raleway', sans-serif`,
+ },
+ components: {
+ Button,
+ },
+});
+
+export default theme;
+```
+
+**何时修改主题**:
+- 添加自定义颜色/渐变
+- 统一修改所有 Button/Card 的默认样式
+- 配置深色模式
+
+---
+
+##### 📁 `src/assets/` - 静态资源
+
+**用途**: 存放图片、字体、图标等静态文件。
+
+**组织结构**:
+```
+assets/
+├── img/ # 图片资源
+│ ├── logo.png
+│ ├── backgrounds/
+│ └── illustrations/
+├── icons/ # SVG 图标
+└── fonts/ # 自定义字体文件(如需要)
+```
+
+**使用方式**:
+```javascript
+import logo from '@assets/img/logo.png';
+
+
+```
+
+---
+
+##### 📁 `src/mocks/` - 开发环境 Mock 数据
+
+**用途**: 使用 MSW (Mock Service Worker) 拦截 API 请求,返回 mock 数据,实现前后端分离开发。
+
+**目录结构**:
+```
+mocks/
+├── browser.js # MSW 初始化(浏览器环境)
+├── handlers/ # API Mock Handlers(按领域划分)
+│ ├── index.js # 汇总所有 handlers
+│ ├── auth.js # 认证相关 API mock
+│ ├── stock.js # 股票数据 API mock
+│ ├── portfolio.js # 投资组合 API mock
+│ └── community.js # 社区内容 API mock
+└── data/ # Mock 数据文件
+ ├── stocks.json
+ ├── users.json
+ └── events.json
+```
+
+**启用 Mock 模式**:
+```bash
+# 方式 1: 使用 .env.mock 配置(默认)
+npm start
+
+# 方式 2: 手动设置环境变量
+REACT_APP_ENABLE_MOCK=true npm start
+```
+
+**添加新 Mock Handler**: 参见"常见开发任务 → 如何添加新的 MSW Mock Handler"
+
+**原则**:
+- Mock 数据应尽量模拟真实数据结构
+- 使用 MSW 的 `http.get/post/put/delete` 定义 handlers
+- Handler 中可模拟延迟、错误响应(测试加载/错误状态)
+
+---
+
+##### 📁 其他目录
+
+**`src/lib/`** - 第三方库配置
+- 存放第三方库的自定义配置(如 PostHog、Analytics)
+
+**`src/styles/`** - 全局样式
+- 存放全局 CSS/SCSS 文件(如 reset.css、global.scss)
+
+**`src/variables/`** - 可配置变量
+- 存放可通过环境变量覆盖的配置(如 API URLs、Feature Flags)
+
+---
+
+#### 目录结构最佳实践
+
+**1. 就近原则(Co-location)**
+- 将相关文件放在一起(组件、样式、测试、工具函数)
+- 示例: `EventCard/` 目录包含 `EventCard.js`、`EventCard.test.js`、`utils.js`
+
+**2. 单一职责**
+- 每个目录只负责一个领域(不要混合业务逻辑和 UI 组件)
+- 示例: `services/` 只包含 API 调用,不包含 UI 组件
+
+**3. 避免深层嵌套**
+- 目录层级不超过 4 层(超过则考虑重构)
+- 示例: `src/views/Community/components/EventCard/` 已经是 4 层
+
+**4. 命名一致性**
+- 组件: PascalCase (`EventCard`)
+- 文件: camelCase (`priceFormatters.js`)
+- 常量: SCREAMING_SNAKE_CASE (`API_BASE_URL`)
+
+**5. 导出规范**
+- 每个目录包含 `index.js` 作为主导出文件
+- 使用命名导出 (`export const ...`) 而非默认导出(除组件外)
+
+---
+
+#### 查找文件指南
+
+**如何快速找到想要修改的文件?**
+
+| 需求 | 目录 | 示例 |
+|------|------|------|
+| 修改页面布局 | `src/layouts/` | `MainLayout.js` |
+| 修改某个页面 | `src/views/{PageName}/` | `Community/index.js` |
+| 修改可复用组件 | `src/components/` | `DataTable/index.js` |
+| 修改 API 调用 | `src/services/` | `stockService.js` |
+| 修改工具函数 | `src/utils/` | `priceFormatters.js` |
+| 修改全局状态 | `src/store/slices/` | `stockSlice.js` |
+| 添加新路由 | `src/routes/routeConfig.js` | - |
+| 修改主题颜色 | `src/theme/foundations/colors.js` | - |
+| 添加 Mock API | `src/mocks/handlers/` | `stock.js` |
+
+**使用路径别名快速导入**:
+```javascript
+// ✅ 使用别名(推荐)
+import { EventCard } from '@components/EventCard';
+import { formatPrice } from '@utils/priceFormatters';
+import { fetchStockData } from '@services/stockService';
+
+// ❌ 使用相对路径(不推荐)
+import { EventCard } from '../../../components/EventCard';
+import { formatPrice } from '../../utils/priceFormatters';
+```
+
+### 后端架构详解
+
+本项目后端采用 **Flask 微服务架构**,结合多种数据库和消息队列,实现高性能金融数据处理和实时通信。
+
+#### 后端目录结构
+
+```
+项目根目录/
+├── app.py # 主 Flask 应用(Web 服务器 + API 路由)
+├── simulation_background_processor.py # Celery 后台处理器(交易模拟任务)
+├── concept_api.py # 概念/行业分析独立 API 服务
+│
+├── wechat_pay.py # 微信支付业务逻辑
+├── wechat_pay_config.py # 微信支付配置(商户号、密钥等)
+│
+├── tdays.csv # 交易日历数据(A 股交易日)
+├── requirements.txt # Python 依赖包清单
+│
+├── models/ # 数据库模型定义(SQLAlchemy)
+│ ├── user.py
+│ ├── stock.py
+│ └── transaction.py
+│
+├── services/ # 业务逻辑服务层
+│ ├── stock_service.py
+│ ├── trading_service.py
+│ └── notification_service.py
+│
+├── utils/ # 后端工具函数
+│ ├── db_utils.py # 数据库连接工具
+│ ├── clickhouse_client.py # ClickHouse 客户端封装
+│ └── date_utils.py # 日期处理工具
+│
+└── config/ # 配置文件
+ ├── development.py
+ ├── production.py
+ └── test.py
+```
+
+---
+
+#### 核心组件详解
+
+##### 1️⃣ `app.py` - 主 Flask 应用
+
+**职责**: Web 服务器、API 路由、认证、会话管理、WebSocket 通信
+
+**核心功能**:
+- **Flask 应用初始化**: 配置 CORS、Session、Logging
+- **API 路由定义**: RESTful API 端点(`/api/stocks`, `/api/portfolio`, `/api/community`)
+- **认证系统**: Flask-Login 集成(登录、登出、权限校验)
+- **WebSocket 服务**: Flask-SocketIO 实现实时推送(股票行情、事件通知)
+- **数据库连接**: SQLAlchemy ORM + ClickHouse Client
+- **交易日历加载**: 启动时从 `tdays.csv` 加载 A 股交易日数据到全局变量
+
+**技术栈**:
+- **Flask** - 轻量级 Web 框架
+- **Flask-Login** - 用户会话管理
+- **Flask-SocketIO** - WebSocket 实时通信
+- **Flask-CORS** - 跨域资源共享配置
+- **SQLAlchemy** - ORM(对象关系映射)
+
+**API 路由示例**:
+```python
+# 获取股票数据
+@app.route('/api/stocks/', methods=['GET'])
+@login_required
+def get_stock_data(stock_code):
+ # 从 ClickHouse 查询历史数据
+ data = clickhouse_client.query(f"SELECT * FROM stocks WHERE code = '{stock_code}'")
+ return jsonify(data)
+
+# 创建交易订单
+@app.route('/api/orders', methods=['POST'])
+@login_required
+def create_order():
+ data = request.json
+ order = Order(user_id=current_user.id, stock_code=data['stock_code'], ...)
+ db.session.add(order)
+ db.session.commit()
+ return jsonify({'order_id': order.id})
+```
+
+**WebSocket 事件示例**:
+```python
+# 客户端订阅股票行情
+@socketio.on('subscribe_stock')
+def handle_subscribe(data):
+ stock_code = data['stock_code']
+ join_room(f'stock_{stock_code}')
+ emit('subscribed', {'stock_code': stock_code})
+
+# 服务端推送行情更新
+def push_quote_update(stock_code, quote_data):
+ socketio.emit('stock_quote', quote_data, room=f'stock_{stock_code}')
+```
+
+**交易日历加载**:
+```python
+# 全局变量存储交易日
+trading_days = []
+
+# 启动时加载 tdays.csv
+def load_trading_days():
+ global trading_days
+ with open('tdays.csv', 'r') as f:
+ trading_days = [line.strip() for line in f.readlines()]
+
+# 判断是否为交易日
+def is_trading_day(date_str):
+ return date_str in trading_days
+```
+
+---
+
+##### 2️⃣ `simulation_background_processor.py` - Celery 后台处理器
+
+**职责**: 处理长时间运行的后台任务(交易模拟、报表生成、数据同步)
+
+**为什么需要后台处理器?**
+- 交易模拟需要执行数百次历史回测计算(耗时 10-60 秒)
+- 避免阻塞 Flask 主线程(影响其他 API 响应)
+- 支持任务队列、重试、失败处理
+
+**技术栈**:
+- **Celery** - 分布式任务队列
+- **Redis** - 消息代理(Broker)和结果存储(Backend)
+- **ClickHouse** - 高性能查询历史股票数据
+
+**任务示例**:
+```python
+from celery import Celery
+
+# Celery 实例
+celery_app = Celery('tasks', broker='redis://localhost:6379/0')
+
+# 交易模拟任务
+@celery_app.task(bind=True)
+def run_simulation(self, user_id, strategy_config):
+ """
+ 执行交易模拟任务
+ :param user_id: 用户 ID
+ :param strategy_config: 策略配置(买入条件、卖出条件、资金管理)
+ :return: 模拟结果(收益率、最大回撤、交易明细)
+ """
+ try:
+ # 1. 从 ClickHouse 获取历史数据
+ stock_data = clickhouse_client.query(...)
+
+ # 2. 执行回测计算
+ for i in range(len(stock_data)):
+ # 模拟交易逻辑
+ if should_buy(stock_data[i]):
+ buy(stock_data[i])
+ if should_sell(stock_data[i]):
+ sell(stock_data[i])
+
+ # 3. 计算绩效指标
+ result = {
+ 'total_return': calculate_return(),
+ 'max_drawdown': calculate_drawdown(),
+ 'trades': get_trade_history(),
+ }
+
+ # 4. 保存结果到 MySQL
+ save_simulation_result(user_id, result)
+
+ return result
+ except Exception as e:
+ # 失败重试(最多 3 次)
+ self.retry(exc=e, countdown=60, max_retries=3)
+```
+
+**从 Flask 调用 Celery 任务**:
+```python
+# app.py
+@app.route('/api/simulations', methods=['POST'])
+@login_required
+def start_simulation():
+ strategy = request.json
+
+ # 异步提交任务到 Celery
+ task = run_simulation.delay(current_user.id, strategy)
+
+ return jsonify({
+ 'task_id': task.id,
+ 'status': 'pending',
+ 'message': '模拟任务已提交,请稍后查看结果'
+ })
+
+# 查询任务状态
+@app.route('/api/simulations/', methods=['GET'])
+def get_simulation_status(task_id):
+ task = run_simulation.AsyncResult(task_id)
+
+ if task.state == 'PENDING':
+ response = {'status': 'pending', 'progress': 0}
+ elif task.state == 'SUCCESS':
+ response = {'status': 'completed', 'result': task.result}
+ elif task.state == 'FAILURE':
+ response = {'status': 'failed', 'error': str(task.info)}
+
+ return jsonify(response)
+```
+
+**启动 Celery Worker**:
+```bash
+# 启动 worker(监听任务队列)
+celery -A simulation_background_processor worker --loglevel=info
+
+# 启动 Flower(Celery 监控工具)
+celery -A simulation_background_processor flower
+```
+
+---
+
+##### 3️⃣ `concept_api.py` - 概念/行业分析 API
+
+**职责**: 独立的概念板块分析服务(涨停分析、热点行业、资金流向)
+
+**为什么独立部署?**
+- 概念分析计算密集(需要遍历数千只股票)
+- 独立扩展性(可部署到多台服务器)
+- 服务隔离(不影响主应用性能)
+
+**部署架构**:
+```
+前端请求 → Nginx 反向代理
+ ├─ /api → Flask 主应用 (端口 5001)
+ └─ /concept-api → Concept API (端口 6801)
+```
+
+**API 示例**:
+```python
+from flask import Flask, jsonify
+import clickhouse_driver
+
+app = Flask(__name__)
+
+# 获取涨停股票列表
+@app.route('/limit_up', methods=['GET'])
+def get_limit_up_stocks():
+ """
+ 获取当日涨停股票列表,按概念板块分组
+ """
+ query = """
+ SELECT
+ stock_code,
+ stock_name,
+ concept,
+ close_price,
+ change_percent
+ FROM stock_daily
+ WHERE trade_date = today()
+ AND change_percent >= 9.9
+ ORDER BY concept, change_percent DESC
+ """
+
+ data = clickhouse_client.query(query)
+
+ # 按概念分组
+ result = {}
+ for row in data:
+ concept = row['concept']
+ if concept not in result:
+ result[concept] = []
+ result[concept].append(row)
+
+ return jsonify(result)
+
+if __name__ == '__main__':
+ app.run(host='0.0.0.0', port=6801)
+```
+
+**前端调用**(通过代理):
+```javascript
+// craco.config.js 配置代理
+proxy: {
+ '/concept-api': {
+ target: 'http://49.232.185.254:6801',
+ pathRewrite: { '^/concept-api': '' },
+ }
+}
+
+// 前端调用
+fetch('/concept-api/limit_up')
+ .then(res => res.json())
+ .then(data => console.log(data));
+```
+
+---
+
+##### 4️⃣ 微信支付集成
+
+**文件**:
+- `wechat_pay.py` - 微信支付业务逻辑(下单、查询、退款)
+- `wechat_pay_config.py` - 配置(商户号、API 密钥、证书路径)
+
+**核心功能**:
+- **统一下单**: 创建支付订单(JSAPI、Native、H5)
+- **支付回调**: 接收微信支付结果通知
+- **订单查询**: 查询订单支付状态
+- **退款处理**: 发起退款请求
+
+**示例代码**:
+```python
+# wechat_pay.py
+from wechatpy.pay import WeChatPay
+from wechat_pay_config import MCHID, API_KEY, CERT_PATH
+
+# 初始化微信支付客户端
+wechat_pay = WeChatPay(
+ appid='your_appid',
+ api_key=API_KEY,
+ mch_id=MCHID,
+ mch_cert=CERT_PATH,
+)
+
+# 创建订单
+def create_order(user_id, amount, description):
+ result = wechat_pay.order.create(
+ trade_type='JSAPI',
+ body=description,
+ total_fee=int(amount * 100), # 单位:分
+ notify_url='https://yourdomain.com/api/wechat/callback',
+ user_id=user_id,
+ )
+ return result
+
+# 支付回调处理
+@app.route('/api/wechat/callback', methods=['POST'])
+def wechat_callback():
+ xml_data = request.data
+ data = wechat_pay.parse_payment_result(xml_data)
+
+ if data['return_code'] == 'SUCCESS':
+ # 更新订单状态
+ order_id = data['out_trade_no']
+ update_order_status(order_id, 'paid')
+
+ return wechat_pay.reply('OK', True)
+```
+
+---
+
+##### 5️⃣ 数据库架构
+
+**多数据库策略**:
+
+| 数据库 | 用途 | 特点 | 示例表 |
+|--------|------|------|--------|
+| **ClickHouse** | 时序数据存储 | OLAP(列式存储、查询速度快) | `stock_daily`(日线数据)
`stock_minute`(分钟数据)
`concept_daily`(概念板块数据) |
+| **MySQL/PostgreSQL** | 事务数据存储 | OLTP(ACID 保证、关系型) | `users`(用户表)
`orders`(订单表)
`portfolios`(持仓表)
`subscriptions`(订阅表) |
+| **Redis** | 缓存 + 消息队列 | 内存数据库(高速读写) | 股票行情缓存
用户 Session
Celery 任务队列 |
+
+**ClickHouse 使用场景**:
+```python
+# 查询某只股票的历史数据(100 万行数据,查询耗时 < 100ms)
+query = """
+ SELECT
+ trade_date,
+ open, high, low, close, volume
+ FROM stock_daily
+ WHERE stock_code = '600000.SH'
+ AND trade_date >= '2020-01-01'
+ ORDER BY trade_date
+"""
+
+data = clickhouse_client.query(query)
+```
+
+**MySQL 使用场景**:
+```python
+# 创建订单(事务保证)
+@app.route('/api/orders', methods=['POST'])
+def create_order():
+ try:
+ # 开启事务
+ order = Order(user_id=user_id, stock_code=stock_code, ...)
+ db.session.add(order)
+
+ # 扣减账户余额
+ account = Account.query.get(user_id)
+ account.balance -= order.total_amount
+
+ # 提交事务
+ db.session.commit()
+ return jsonify({'success': True})
+ except Exception as e:
+ # 回滚事务
+ db.session.rollback()
+ return jsonify({'error': str(e)}), 500
+```
+
+---
+
+#### 后端技术栈详解
+
+**核心框架**:
+- **Flask 2.x** - 轻量级 Web 框架,易于扩展
+- **Flask-Login** - 用户认证与会话管理
+- **Flask-SocketIO** - WebSocket 实时通信(基于 Socket.IO 协议)
+- **Flask-CORS** - 跨域资源共享配置
+
+**数据库与 ORM**:
+- **SQLAlchemy** - Python ORM,支持 MySQL/PostgreSQL
+- **ClickHouse Driver** - ClickHouse Python 客户端
+- **Redis-py** - Redis Python 客户端
+
+**后台任务处理**:
+- **Celery** - 分布式任务队列
+- **Redis** - Celery 消息代理(Broker)
+
+**第三方服务**:
+- **微信支付 SDK** - `wechatpy`
+- **腾讯云短信 SDK** - 发送验证码、通知
+
+---
+
+#### 后端开发最佳实践
+
+**1. API 设计规范**:
+```python
+# ✅ RESTful 风格
+GET /api/stocks # 获取股票列表
+GET /api/stocks/:id # 获取单个股票
+POST /api/stocks # 创建股票
+PUT /api/stocks/:id # 更新股票
+DELETE /api/stocks/:id # 删除股票
+
+# ✅ 统一响应格式
+{
+ "code": 200,
+ "message": "success",
+ "data": { ... }
+}
+
+# ❌ 避免使用动词命名
+POST /api/getStockData # 不推荐
+POST /api/createNewOrder # 不推荐
+```
+
+**2. 错误处理**:
+```python
+# 统一错误处理
+@app.errorhandler(Exception)
+def handle_error(e):
+ if isinstance(e, ValidationError):
+ return jsonify({'error': str(e)}), 400
+ elif isinstance(e, Unauthorized):
+ return jsonify({'error': 'Unauthorized'}), 401
+ else:
+ logger.error(f'Unhandled exception: {e}')
+ return jsonify({'error': 'Internal server error'}), 500
+```
+
+**3. 数据库连接管理**:
+```python
+# 使用连接池
+engine = create_engine(
+ 'mysql://user:pass@localhost/db',
+ pool_size=10,
+ max_overflow=20,
+ pool_recycle=3600,
+)
+
+# 请求结束后自动关闭连接
+@app.teardown_appcontext
+def shutdown_session(exception=None):
+ db.session.remove()
+```
+
+**4. 性能优化**:
+- **数据库索引**: 为常用查询字段添加索引
+- **查询优化**: 避免 N+1 查询,使用 `joinedload`
+- **缓存策略**: 使用 Redis 缓存热点数据(股票行情、用户信息)
+- **异步任务**: 耗时操作使用 Celery 异步处理
+
+---
+
+#### 后端部署架构
+
+**生产环境部署**:
+```
+Nginx (反向代理 + 负载均衡)
+ ├─ Gunicorn (WSGI 服务器) × 4 进程
+ │ └─ Flask 应用 (app.py)
+ │
+ ├─ Celery Worker × 2 进程
+ │ └─ 后台任务处理
+ │
+ ├─ Redis (缓存 + 消息队列)
+ ├─ MySQL (事务数据)
+ └─ ClickHouse (时序数据)
+```
+
+**启动命令**:
+```bash
+# 启动 Flask 应用(Gunicorn)
+gunicorn -w 4 -b 0.0.0.0:5001 app:app
+
+# 启动 Celery Worker
+celery -A simulation_background_processor worker --loglevel=info
+
+# 启动 Concept API
+python concept_api.py
+```
+
+**监控与日志**:
+- **日志**: 使用 Python `logging` 模块记录日志
+- **监控**: Celery Flower 监控任务状态
+- **性能**: 使用 APM 工具(如 New Relic、Datadog)
+
+---
+
+## 配置
+
+### 环境文件
+```
+.env.mock - Mock 模式(默认):MSW 拦截所有 API 调用,无需后端
+.env.development - 开发模式:连接到开发后端
+.env.test - 测试模式:用于 'npm run start:test'(后端 + 前端一起)
+.env.production - 生产环境构建配置
+```
+
+**关键环境变量:**
+- `REACT_APP_ENABLE_MOCK=true` - 启用 MSW mocking
+- `REACT_APP_API_URL` - 后端 URL(空字符串 = 使用相对路径或 MSW)
+
+### MSW (Mock Service Worker) 设置
+MSW 用于开发期间的 API mocking:
+
+1. **激活方式**:在 env 文件中设置 `REACT_APP_ENABLE_MOCK=true`
+2. **Worker 文件**:`public/mockServiceWorker.js`(自动生成)
+3. **Handlers**:`src/mocks/handlers/`(按领域组织:auth、stock、company 等)
+4. **模式**:`onUnhandledRequest: 'warn'` - 未处理的请求会传递到后端
+
+当 MSW 激活时,开发服务器代理被禁用(MSW 优先拦截)。
+
+### 路径别名 (craco.config.js)
+所有别名解析到 `src/` 子目录:
+```
+@/ → src/
+@assets/ → src/assets/
+@components/ → src/components/
+@constants/ → src/constants/
+@contexts/ → src/contexts/
+@data/ → src/data/
+@hooks/ → src/hooks/
+@layouts/ → src/layouts/
+@lib/ → src/lib/
+@mocks/ → src/mocks/
+@providers/ → src/providers/
+@routes/ → src/routes/
+@services/ → src/services/
+@store/ → src/store/
+@styles/ → src/styles/
+@theme/ → src/theme/
+@utils/ → src/utils/
+@variables/ → src/variables/
+@views/ → src/views/
+```
+
+### Webpack 优化 (craco.config.js)
+**性能特性:**
+- 文件系统缓存(重新构建速度提升 50-80%)
+- 按库激进的代码分割:
+ - `react-vendor` - React 核心(优先级 30)
+ - `charts-lib` - echarts、d3、apexcharts、recharts(优先级 25)
+ - `chakra-ui` - Chakra UI + Emotion(优先级 23)
+ - `antd-lib` - Ant Design(优先级 22)
+ - `three-lib` - Three.js(优先级 20)
+ - `calendar-lib` - moment、date-fns、FullCalendar(优先级 18)
+- 从构建中移除 ESLint 插件(速度提升 20-30%)
+- 启用 Babel 缓存
+- moment locale 剥离(IgnorePlugin)
+- Source maps:生产环境禁用,开发环境使用 `eval-cheap-module-source-map`
+
+**开发服务器:**
+- 端口 3000(prestart 时杀死现有进程)
+- 代理(当 MSW 禁用时):`/api` → `http://49.232.185.254:5001`
+- Bundle 分析器:`ANALYZE=true npm run build:analyze`
+
+### 构建流程
+1. `npm run build` 使用 CRACO + webpack 优化编译
+2. Gulp 任务(`gulp licenses`)为 JS/HTML 添加 Creative Tim 许可证头
+3. 输出:`build/` 目录
+
+**Node 兼容性:**
+```bash
+NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096'
+```
+
+## 开发工作流
+
+### 路由开发
+添加新路由的步骤:
+
+1. 在 `src/routes/lazy-components.js` 中添加 lazy import
+2. 在 `src/routes/routeConfig.js` 中添加路由配置:
+ - `path` - URL 路径
+ - `component` - 来自 lazyComponents
+ - `protection` - MODAL/REDIRECT/PUBLIC
+ - `layout` - 'main'(带导航栏)或 'none'(全屏)
+3. 路由自动使用 Suspense + ErrorBoundary 渲染(由 PageTransitionWrapper 处理)
+
+### 组件组织模式
+基于最近的重构(详见 README.md):
+
+**原子设计模式:**
+- **Atoms(原子)** - 基础 UI 元素(按钮、徽章、输入框)
+- **Molecules(分子)** - 原子的组合(卡片、表单)
+- **Organisms(有机体)** - 复杂组件(列表、面板)
+
+**大型组件示例结构(1000+ 行):**
+```
+src/views/Community/components/
+├── EventCard/
+│ ├── index.js - 智能包装器(路由紧凑 vs 详细视图)
+│ ├── CompactEventCard.js - 紧凑视图
+│ ├── DetailedEventCard.js - 详细视图
+│ ├── EventTimeline.js - 原子组件
+│ ├── EventImportanceBadge.js - 原子组件
+│ └── ...
+```
+
+**工具提取:**
+- 将可复用逻辑提取到 `src/utils/`(如 `priceFormatters.js`)
+- 将共享常量提取到 `src/constants/`(如 `animations.js`)
+
+### API 集成
+**服务层**(`src/services/`):
+- 使用 `src/utils/apiConfig.js` 中的 `getApiBase()` 获取基础 URL
+- 示例:`${getApiBase()}/api/endpoint`
+- 在 mock 模式下,MSW 拦截;在开发/生产环境下,请求后端
+
+**添加新的 API 端点:**
+1. 在 `src/services/` 中添加服务函数(或在组件中内联)
+2. 如果使用 MSW:在 `src/mocks/handlers/{domain}.js` 中添加 handler
+3. 在 `src/mocks/handlers/index.js` 中导入 handler
+
+### Redux 状态管理
+**现有 slices**(`src/store/slices/`):
+- `authModalSlice` - 认证模态框状态
+- `posthogSlice` - PostHog 分析
+- `stockSlice` - 股票数据
+- `industrySlice` - 行业/概念数据
+- `subscriptionSlice` - 用户订阅
+- `communityDataSlice` - 社区内容
+
+**添加新 slice:**
+1. 创建 `src/store/slices/yourSlice.js`
+2. 在 `src/store/index.js` 中导入并添加
+3. 通过 `useSelector` 访问,通过 `useDispatch` 派发
+
+---
+
+## 更新本文档
+
+本 CLAUDE.md 文件是一个持续更新的文档。在以下情况下应更新它:
+- 添加新的架构模式或指南
+- 发现新的最佳实践
+- 解决或添加技术债务项
+- 做出重要的技术决策
+- 进行重要的代码清理或重构
+
+所有开发人员应在入职时审查本文档,并在做出架构决策时参考它。
diff --git a/mcp_database.py b/mcp_database.py
index ec2df704..00024b9a 100644
--- a/mcp_database.py
+++ b/mcp_database.py
@@ -544,3 +544,240 @@ async def get_stock_comparison(
"comparison_type": metric,
"stocks": [convert_row(row) for row in results]
}
+
+
+async def get_user_favorite_stocks(user_id: str, limit: int = 100) -> List[Dict[str, Any]]:
+ """
+ 获取用户自选股列表
+
+ Args:
+ user_id: 用户ID
+ limit: 返回条数
+
+ Returns:
+ 自选股列表(包含最新行情数据)
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 查询用户自选股(假设有 user_favorites 表)
+ # 如果没有此表,可以根据实际情况调整
+ query = """
+ SELECT
+ f.user_id,
+ f.stock_code,
+ b.SECNAME as stock_name,
+ b.F030V as industry,
+ t.F007N as current_price,
+ t.F010N as change_pct,
+ t.F012N as turnover_rate,
+ t.F026N as pe_ratio,
+ t.TRADEDATE as latest_trade_date,
+ f.created_at as favorite_time
+ FROM user_favorites f
+ INNER JOIN ea_baseinfo b ON f.stock_code = b.SECCODE
+ LEFT JOIN (
+ SELECT SECCODE, MAX(TRADEDATE) as max_date
+ FROM ea_trade
+ GROUP BY SECCODE
+ ) latest ON b.SECCODE = latest.SECCODE
+ LEFT JOIN ea_trade t ON b.SECCODE = t.SECCODE
+ AND t.TRADEDATE = latest.max_date
+ WHERE f.user_id = %s AND f.is_deleted = 0
+ ORDER BY f.created_at DESC
+ LIMIT %s
+ """
+
+ await cursor.execute(query, [user_id, limit])
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def get_user_favorite_events(user_id: str, limit: int = 100) -> List[Dict[str, Any]]:
+ """
+ 获取用户自选事件列表
+
+ Args:
+ user_id: 用户ID
+ limit: 返回条数
+
+ Returns:
+ 自选事件列表
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 查询用户自选事件(假设有 user_event_favorites 表)
+ query = """
+ SELECT
+ f.user_id,
+ f.event_id,
+ e.title,
+ e.description,
+ e.event_date,
+ e.importance,
+ e.related_stocks,
+ e.category,
+ f.created_at as favorite_time
+ FROM user_event_favorites f
+ INNER JOIN events e ON f.event_id = e.id
+ WHERE f.user_id = %s AND f.is_deleted = 0
+ ORDER BY e.event_date DESC
+ LIMIT %s
+ """
+
+ await cursor.execute(query, [user_id, limit])
+ results = await cursor.fetchall()
+
+ return [convert_row(row) for row in results]
+
+
+async def add_favorite_stock(user_id: str, stock_code: str) -> Dict[str, Any]:
+ """
+ 添加自选股
+
+ Args:
+ user_id: 用户ID
+ stock_code: 股票代码
+
+ Returns:
+ 操作结果
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 检查是否已存在
+ check_query = """
+ SELECT id, is_deleted
+ FROM user_favorites
+ WHERE user_id = %s AND stock_code = %s
+ """
+ await cursor.execute(check_query, [user_id, stock_code])
+ existing = await cursor.fetchone()
+
+ if existing:
+ if existing['is_deleted'] == 1:
+ # 恢复已删除的记录
+ update_query = """
+ UPDATE user_favorites
+ SET is_deleted = 0, updated_at = NOW()
+ WHERE id = %s
+ """
+ await cursor.execute(update_query, [existing['id']])
+ return {"success": True, "message": "已恢复自选股"}
+ else:
+ return {"success": False, "message": "该股票已在自选中"}
+
+ # 插入新记录
+ insert_query = """
+ INSERT INTO user_favorites (user_id, stock_code, created_at, updated_at, is_deleted)
+ VALUES (%s, %s, NOW(), NOW(), 0)
+ """
+ await cursor.execute(insert_query, [user_id, stock_code])
+ return {"success": True, "message": "添加自选股成功"}
+
+
+async def remove_favorite_stock(user_id: str, stock_code: str) -> Dict[str, Any]:
+ """
+ 删除自选股
+
+ Args:
+ user_id: 用户ID
+ stock_code: 股票代码
+
+ Returns:
+ 操作结果
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ UPDATE user_favorites
+ SET is_deleted = 1, updated_at = NOW()
+ WHERE user_id = %s AND stock_code = %s AND is_deleted = 0
+ """
+ result = await cursor.execute(query, [user_id, stock_code])
+
+ if result > 0:
+ return {"success": True, "message": "删除自选股成功"}
+ else:
+ return {"success": False, "message": "未找到该自选股"}
+
+
+async def add_favorite_event(user_id: str, event_id: int) -> Dict[str, Any]:
+ """
+ 添加自选事件
+
+ Args:
+ user_id: 用户ID
+ event_id: 事件ID
+
+ Returns:
+ 操作结果
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ # 检查是否已存在
+ check_query = """
+ SELECT id, is_deleted
+ FROM user_event_favorites
+ WHERE user_id = %s AND event_id = %s
+ """
+ await cursor.execute(check_query, [user_id, event_id])
+ existing = await cursor.fetchone()
+
+ if existing:
+ if existing['is_deleted'] == 1:
+ # 恢复已删除的记录
+ update_query = """
+ UPDATE user_event_favorites
+ SET is_deleted = 0, updated_at = NOW()
+ WHERE id = %s
+ """
+ await cursor.execute(update_query, [existing['id']])
+ return {"success": True, "message": "已恢复自选事件"}
+ else:
+ return {"success": False, "message": "该事件已在自选中"}
+
+ # 插入新记录
+ insert_query = """
+ INSERT INTO user_event_favorites (user_id, event_id, created_at, updated_at, is_deleted)
+ VALUES (%s, %s, NOW(), NOW(), 0)
+ """
+ await cursor.execute(insert_query, [user_id, event_id])
+ return {"success": True, "message": "添加自选事件成功"}
+
+
+async def remove_favorite_event(user_id: str, event_id: int) -> Dict[str, Any]:
+ """
+ 删除自选事件
+
+ Args:
+ user_id: 用户ID
+ event_id: 事件ID
+
+ Returns:
+ 操作结果
+ """
+ pool = await get_pool()
+
+ async with pool.acquire() as conn:
+ async with conn.cursor(aiomysql.DictCursor) as cursor:
+ query = """
+ UPDATE user_event_favorites
+ SET is_deleted = 1, updated_at = NOW()
+ WHERE user_id = %s AND event_id = %s AND is_deleted = 0
+ """
+ result = await cursor.execute(query, [user_id, event_id])
+
+ if result > 0:
+ return {"success": True, "message": "删除自选事件成功"}
+ else:
+ return {"success": False, "message": "未找到该自选事件"}
diff --git a/mcp_elasticsearch.py b/mcp_elasticsearch.py
new file mode 100644
index 00000000..53407f5f
--- /dev/null
+++ b/mcp_elasticsearch.py
@@ -0,0 +1,320 @@
+"""
+Elasticsearch 连接和工具模块
+用于聊天记录存储和向量搜索
+"""
+
+from elasticsearch import Elasticsearch, helpers
+from datetime import datetime
+from typing import List, Dict, Any, Optional
+import logging
+import json
+import openai
+
+logger = logging.getLogger(__name__)
+
+# ==================== 配置 ====================
+
+# ES 配置
+ES_CONFIG = {
+ "host": "http://222.128.1.157:19200",
+ "index_chat_history": "agent_chat_history", # 聊天记录索引
+}
+
+# Embedding 配置
+EMBEDDING_CONFIG = {
+ "api_key": "dummy",
+ "base_url": "http://222.128.1.157:18008/v1",
+ "model": "qwen3-embedding-8b",
+ "dims": 4096, # 向量维度
+}
+
+# ==================== ES 客户端 ====================
+
+class ESClient:
+ """Elasticsearch 客户端封装"""
+
+ def __init__(self):
+ self.es = Elasticsearch([ES_CONFIG["host"]], request_timeout=60)
+ self.chat_index = ES_CONFIG["index_chat_history"]
+
+ # 初始化 OpenAI 客户端用于 embedding
+ self.embedding_client = openai.OpenAI(
+ api_key=EMBEDDING_CONFIG["api_key"],
+ base_url=EMBEDDING_CONFIG["base_url"],
+ )
+ self.embedding_model = EMBEDDING_CONFIG["model"]
+
+ # 初始化索引
+ self.create_chat_history_index()
+
+ def create_chat_history_index(self):
+ """创建聊天记录索引"""
+ if self.es.indices.exists(index=self.chat_index):
+ logger.info(f"索引 {self.chat_index} 已存在")
+ return
+
+ mappings = {
+ "properties": {
+ "session_id": {"type": "keyword"}, # 会话ID
+ "user_id": {"type": "keyword"}, # 用户ID
+ "user_nickname": {"type": "text"}, # 用户昵称
+ "user_avatar": {"type": "keyword"}, # 用户头像URL
+ "message_type": {"type": "keyword"}, # user / assistant
+ "message": {"type": "text"}, # 消息内容
+ "message_embedding": { # 消息向量
+ "type": "dense_vector",
+ "dims": EMBEDDING_CONFIG["dims"],
+ "index": True,
+ "similarity": "cosine"
+ },
+ "plan": {"type": "text"}, # 执行计划(仅 assistant)
+ "steps": {"type": "text"}, # 执行步骤(仅 assistant)
+ "timestamp": {"type": "date"}, # 时间戳
+ "created_at": {"type": "date"}, # 创建时间
+ }
+ }
+
+ self.es.indices.create(index=self.chat_index, body={"mappings": mappings})
+ logger.info(f"创建索引: {self.chat_index}")
+
+ def generate_embedding(self, text: str) -> List[float]:
+ """生成文本向量"""
+ try:
+ if not text or len(text.strip()) == 0:
+ return []
+
+ # 截断过长文本
+ text = text[:16000] if len(text) > 16000 else text
+
+ response = self.embedding_client.embeddings.create(
+ model=self.embedding_model,
+ input=[text]
+ )
+ return response.data[0].embedding
+ except Exception as e:
+ logger.error(f"Embedding 生成失败: {e}")
+ return []
+
+ def save_chat_message(
+ self,
+ session_id: str,
+ user_id: str,
+ user_nickname: str,
+ user_avatar: str,
+ message_type: str, # "user" or "assistant"
+ message: str,
+ plan: Optional[str] = None,
+ steps: Optional[str] = None,
+ ) -> str:
+ """
+ 保存聊天消息
+
+ Args:
+ session_id: 会话ID
+ user_id: 用户ID
+ user_nickname: 用户昵称
+ user_avatar: 用户头像URL
+ message_type: 消息类型 (user/assistant)
+ message: 消息内容
+ plan: 执行计划(可选)
+ steps: 执行步骤(可选)
+
+ Returns:
+ 文档ID
+ """
+ try:
+ # 生成向量
+ embedding = self.generate_embedding(message)
+
+ doc = {
+ "session_id": session_id,
+ "user_id": user_id,
+ "user_nickname": user_nickname,
+ "user_avatar": user_avatar,
+ "message_type": message_type,
+ "message": message,
+ "message_embedding": embedding if embedding else None,
+ "plan": plan,
+ "steps": steps,
+ "timestamp": datetime.now(),
+ "created_at": datetime.now(),
+ }
+
+ result = self.es.index(index=self.chat_index, body=doc)
+ logger.info(f"保存聊天记录: {result['_id']}")
+ return result["_id"]
+
+ except Exception as e:
+ logger.error(f"保存聊天记录失败: {e}")
+ raise
+
+ def get_chat_sessions(self, user_id: str, limit: int = 50) -> List[Dict[str, Any]]:
+ """
+ 获取用户的聊天会话列表
+
+ Args:
+ user_id: 用户ID
+ limit: 返回数量
+
+ Returns:
+ 会话列表,每个会话包含:session_id, last_message, last_timestamp
+ """
+ try:
+ # 聚合查询:按 session_id 分组,获取每个会话的最后一条消息
+ query = {
+ "query": {
+ "term": {"user_id": user_id}
+ },
+ "aggs": {
+ "sessions": {
+ "terms": {
+ "field": "session_id",
+ "size": limit,
+ "order": {"last_message": "desc"}
+ },
+ "aggs": {
+ "last_message": {
+ "max": {"field": "timestamp"}
+ },
+ "last_message_content": {
+ "top_hits": {
+ "size": 1,
+ "sort": [{"timestamp": {"order": "desc"}}],
+ "_source": ["message", "timestamp", "message_type"]
+ }
+ }
+ }
+ }
+ },
+ "size": 0
+ }
+
+ result = self.es.search(index=self.chat_index, body=query)
+
+ sessions = []
+ for bucket in result["aggregations"]["sessions"]["buckets"]:
+ session_data = bucket["last_message_content"]["hits"]["hits"][0]["_source"]
+ sessions.append({
+ "session_id": bucket["key"],
+ "last_message": session_data["message"],
+ "last_timestamp": session_data["timestamp"],
+ "message_count": bucket["doc_count"],
+ })
+
+ return sessions
+
+ except Exception as e:
+ logger.error(f"获取会话列表失败: {e}")
+ return []
+
+ def get_chat_history(
+ self,
+ session_id: str,
+ limit: int = 100
+ ) -> List[Dict[str, Any]]:
+ """
+ 获取指定会话的聊天历史
+
+ Args:
+ session_id: 会话ID
+ limit: 返回数量
+
+ Returns:
+ 聊天记录列表
+ """
+ try:
+ query = {
+ "query": {
+ "term": {"session_id": session_id}
+ },
+ "sort": [{"timestamp": {"order": "asc"}}],
+ "size": limit
+ }
+
+ result = self.es.search(index=self.chat_index, body=query)
+
+ messages = []
+ for hit in result["hits"]["hits"]:
+ doc = hit["_source"]
+ messages.append({
+ "message_type": doc["message_type"],
+ "message": doc["message"],
+ "plan": doc.get("plan"),
+ "steps": doc.get("steps"),
+ "timestamp": doc["timestamp"],
+ })
+
+ return messages
+
+ except Exception as e:
+ logger.error(f"获取聊天历史失败: {e}")
+ return []
+
+ def search_chat_history(
+ self,
+ user_id: str,
+ query_text: str,
+ top_k: int = 10
+ ) -> List[Dict[str, Any]]:
+ """
+ 向量搜索聊天历史
+
+ Args:
+ user_id: 用户ID
+ query_text: 查询文本
+ top_k: 返回数量
+
+ Returns:
+ 相关聊天记录列表
+ """
+ try:
+ # 生成查询向量
+ query_embedding = self.generate_embedding(query_text)
+ if not query_embedding:
+ return []
+
+ # 向量搜索
+ query = {
+ "query": {
+ "bool": {
+ "must": [
+ {"term": {"user_id": user_id}},
+ {
+ "script_score": {
+ "query": {"match_all": {}},
+ "script": {
+ "source": "cosineSimilarity(params.query_vector, 'message_embedding') + 1.0",
+ "params": {"query_vector": query_embedding}
+ }
+ }
+ }
+ ]
+ }
+ },
+ "size": top_k
+ }
+
+ result = self.es.search(index=self.chat_index, body=query)
+
+ messages = []
+ for hit in result["hits"]["hits"]:
+ doc = hit["_source"]
+ messages.append({
+ "session_id": doc["session_id"],
+ "message_type": doc["message_type"],
+ "message": doc["message"],
+ "timestamp": doc["timestamp"],
+ "score": hit["_score"],
+ })
+
+ return messages
+
+ except Exception as e:
+ logger.error(f"向量搜索失败: {e}")
+ return []
+
+
+# ==================== 全局实例 ====================
+
+# 创建全局 ES 客户端
+es_client = ESClient()
diff --git a/mcp_server.py b/mcp_server.py
index 412971e0..35156b5b 100644
--- a/mcp_server.py
+++ b/mcp_server.py
@@ -17,6 +17,8 @@ import mcp_database as db
from openai import OpenAI
import json
import asyncio
+import uuid
+from mcp_elasticsearch import es_client
# 配置日志
logging.basicConfig(level=logging.INFO)
@@ -135,6 +137,10 @@ class AgentChatRequest(BaseModel):
"""聊天请求"""
message: str
conversation_history: List[ConversationMessage] = []
+ user_id: Optional[str] = None # 用户ID
+ user_nickname: Optional[str] = None # 用户昵称
+ user_avatar: Optional[str] = None # 用户头像URL
+ session_id: Optional[str] = None # 会话ID(如果为空则创建新会话)
# ==================== MCP工具定义 ====================
@@ -1023,7 +1029,14 @@ class MCPAgentIntegrated:
for tool in tools
])
- return f"""你是一个专业的金融研究助手。根据用户问题,制定详细的执行计划。
+ return f"""你是"价小前",北京价值前沿科技公司的AI投研聊天助手。
+
+## 你的人格特征
+- **名字**: 价小前
+- **身份**: 北京价值前沿科技公司的专业AI投研助手
+- **专业领域**: 股票投资研究、市场分析、新闻解读、财务分析
+- **性格**: 专业、严谨、友好,擅长用简洁的语言解释复杂的金融概念
+- **服务宗旨**: 帮助投资者做出更明智的投资决策,提供数据驱动的研究支持
## 可用工具
@@ -1040,7 +1053,7 @@ class MCPAgentIntegrated:
- 概念板块: 相同题材股票分类
## 任务
-分析用户问题,制定执行计划。返回 JSON:
+分析用户问题,制定详细的执行计划。返回 JSON:
```json
{{
@@ -1533,7 +1546,32 @@ async def chat(request: ChatRequest):
@app.post("/agent/chat", response_model=AgentResponse)
async def agent_chat(request: AgentChatRequest):
"""智能代理对话端点(非流式)"""
- logger.info(f"Agent chat: {request.message}")
+ logger.info(f"Agent chat: {request.message} (user: {request.user_id})")
+
+ # ==================== 权限检查 ====================
+ # 仅允许 max 用户使用
+ if request.user_id != "max":
+ raise HTTPException(
+ status_code=403,
+ detail="很抱歉,「价小前投研」功能目前仅对特定用户开放。如需使用,请联系管理员。"
+ )
+
+ # ==================== 会话管理 ====================
+ # 如果没有提供 session_id,创建新会话
+ session_id = request.session_id or str(uuid.uuid4())
+
+ # 保存用户消息到 ES
+ try:
+ es_client.save_chat_message(
+ session_id=session_id,
+ user_id=request.user_id or "anonymous",
+ user_nickname=request.user_nickname or "匿名用户",
+ user_avatar=request.user_avatar or "",
+ message_type="user",
+ message=request.message,
+ )
+ except Exception as e:
+ logger.error(f"保存用户消息失败: {e}")
# 获取工具列表
tools = [tool.dict() for tool in TOOLS]
@@ -1565,7 +1603,31 @@ async def agent_chat(request: AgentChatRequest):
tool_handlers=TOOL_HANDLERS,
)
- return response
+ # 保存 Agent 回复到 ES
+ try:
+ # 将执行步骤转换为JSON字符串
+ steps_json = json.dumps(
+ [{"tool": step.tool, "result": step.result} for step in response.steps],
+ ensure_ascii=False
+ )
+
+ es_client.save_chat_message(
+ session_id=session_id,
+ user_id=request.user_id or "anonymous",
+ user_nickname=request.user_nickname or "匿名用户",
+ user_avatar=request.user_avatar or "",
+ message_type="assistant",
+ message=response.final_answer,
+ plan=response.plan,
+ steps=steps_json,
+ )
+ except Exception as e:
+ logger.error(f"保存 Agent 回复失败: {e}")
+
+ # 在响应中返回 session_id
+ response_dict = response.dict()
+ response_dict["session_id"] = session_id
+ return response_dict
@app.post("/agent/chat/stream")
async def agent_chat_stream(request: AgentChatRequest):
@@ -1610,6 +1672,81 @@ async def agent_chat_stream(request: AgentChatRequest):
},
)
+# ==================== 聊天记录管理 API ====================
+
+@app.get("/agent/sessions")
+async def get_chat_sessions(user_id: str, limit: int = 50):
+ """
+ 获取用户的聊天会话列表
+
+ Args:
+ user_id: 用户ID
+ limit: 返回数量(默认50)
+
+ Returns:
+ 会话列表
+ """
+ try:
+ sessions = es_client.get_chat_sessions(user_id, limit)
+ return {
+ "success": True,
+ "data": sessions,
+ "count": len(sessions)
+ }
+ except Exception as e:
+ logger.error(f"获取会话列表失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/agent/history/{session_id}")
+async def get_chat_history(session_id: str, limit: int = 100):
+ """
+ 获取指定会话的聊天历史
+
+ Args:
+ session_id: 会话ID
+ limit: 返回数量(默认100)
+
+ Returns:
+ 聊天记录列表
+ """
+ try:
+ messages = es_client.get_chat_history(session_id, limit)
+ return {
+ "success": True,
+ "data": messages,
+ "count": len(messages)
+ }
+ except Exception as e:
+ logger.error(f"获取聊天历史失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post("/agent/search")
+async def search_chat_history(user_id: str, query: str, top_k: int = 10):
+ """
+ 向量搜索聊天历史
+
+ Args:
+ user_id: 用户ID
+ query: 查询文本
+ top_k: 返回数量(默认10)
+
+ Returns:
+ 相关聊天记录列表
+ """
+ try:
+ results = es_client.search_chat_history(user_id, query, top_k)
+ return {
+ "success": True,
+ "data": results,
+ "count": len(results)
+ }
+ except Exception as e:
+ logger.error(f"向量搜索失败: {e}")
+ raise HTTPException(status_code=500, detail=str(e))
+
+
# ==================== 健康检查 ====================
@app.get("/health")
diff --git a/src/routes/routeConfig.js b/src/routes/routeConfig.js
index 6d632416..9ae9911d 100644
--- a/src/routes/routeConfig.js
+++ b/src/routes/routeConfig.js
@@ -157,8 +157,8 @@ export const routeConfig = [
protection: PROTECTION_MODES.MODAL,
layout: 'main',
meta: {
- title: 'AI投资助手',
- description: '基于MCP的智能投资顾问'
+ title: '价小前投研',
+ description: '北京价值前沿科技公司的AI投研聊天助手'
}
},
];