Compare commits

..

3 Commits

Author SHA1 Message Date
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
2386 changed files with 676572 additions and 315465 deletions

View File

@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(xargs ls:*)",
"Bash(awk:*)",
"Bash(npm start)",
"Bash(python3:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -1,63 +0,0 @@
# 部署配置文件
# 首次使用请复制此文件为 .env.deploy 并填写真实配置
# ==================== 服务器配置 ====================
# 服务器 IP 或域名
SERVER_HOST=your-server-ip-or-domain
# SSH 用户名
SERVER_USER=ubuntu
# SSH 端口
SERVER_PORT=22
# SSH 密钥路径(留空使用默认 ~/.ssh/id_rsa
SSH_KEY_PATH=
# ==================== 路径配置 ====================
# 服务器上的 Git 仓库路径
REMOTE_PROJECT_PATH=/home/ubuntu/vf_react
# 生产环境部署路径
PRODUCTION_PATH=/var/www/valuefrontier.cn
# 部署备份目录
BACKUP_DIR=/home/ubuntu/deployments
# 部署日志目录
LOG_DIR=/home/ubuntu/deploy-logs
# ==================== Git 配置 ====================
# 部署分支
DEPLOY_BRANCH=feature
# ==================== 备份配置 ====================
# 保留备份数量
KEEP_BACKUPS=5
# ==================== 企业微信通知配置 ====================
# 是否启用企业微信通知 (true/false)
ENABLE_WECHAT_NOTIFY=false
# 企业微信机器人 Webhook URL
WECHAT_WEBHOOK_URL=
# 通知提及的用户(@all 或 手机号/userid
WECHAT_MENTIONED_LIST=
# ==================== 部署配置 ====================
# 是否在部署前运行 npm install (true/false)
RUN_NPM_INSTALL=true
# 是否在部署前运行 npm test (true/false)
RUN_NPM_TEST=false
# 构建命令
BUILD_COMMAND=npm run build
# ==================== 高级配置 ====================
# SSH 连接超时时间(秒)
SSH_TIMEOUT=30
# 部署超时时间(秒)
DEPLOY_TIMEOUT=600

View File

@@ -1,25 +0,0 @@
# 开发环境配置(连接真实后端)
# 使用方式: npm run start:dev
# React 构建优化配置
GENERATE_SOURCEMAP=false
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=true
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096
# API 配置
# 后端 API 地址(开发环境会代理到这个地址)
REACT_APP_API_URL=http://49.232.185.254:5001
# 禁用 Mock 数据使用真实API
REACT_APP_ENABLE_MOCK=false
# 开发环境标识
REACT_APP_ENV=development
# 性能监控配置
REACT_APP_ENABLE_PERFORMANCE_MONITOR=true
REACT_APP_ENABLE_PERFORMANCE_PANEL=true
REACT_APP_REPORT_TO_POSTHOG=false

View File

@@ -1,41 +0,0 @@
# ========================================
# Mock 测试环境配置
# ========================================
# 使用方式: npm run start:mock
#
# 工作原理:
# 1. 通过 env-cmd 加载此配置文件
# 2. REACT_APP_ENABLE_MOCK=true 会在 src/index.js 中启动 MSW (Mock Service Worker)
# 3. MSW 在浏览器层面拦截所有 HTTP 请求
# 4. 根据 src/mocks/handlers/* 中定义的规则返回 mock 数据
# 5. 未定义 mock 的接口会继续请求真实后端
#
# 适用场景:
# - 前端独立开发,无需后端支持
# - 测试特定接口的 UI 表现
# - 后端接口未就绪时的快速原型开发
# ========================================
# React 构建优化配置
GENERATE_SOURCEMAP=false
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=true
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096
# API 配置
# Mock 模式下使用空字符串,让请求使用相对路径
# MSW 会在浏览器层拦截这些请求,不需要真实的后端地址
REACT_APP_API_URL=
# Socket.IO 连接地址Mock 模式下连接生产环境)
# 注意WebSocket 不被 MSW 拦截,可以独立配置
REACT_APP_SOCKET_URL=https://valuefrontier.cn
# 启用 Mock 数据(核心配置)
# 此配置会触发 src/index.js 中的 MSW 初始化
REACT_APP_ENABLE_MOCK=true
# Mock 环境标识
REACT_APP_ENV=mock

View File

@@ -1,48 +0,0 @@
# ========================================
# 生产环境配置
# ========================================
# 环境标识
REACT_APP_ENV=production
NODE_ENV=production
# Mock 配置(生产环境禁用 Mock
REACT_APP_ENABLE_MOCK=false
# 🔧 调试模式(生产环境临时调试用)
# 开启后会在全局暴露 window.__DEBUG__
REACT_APP_ENABLE_DEBUG=false
# 后端 API 地址(生产环境)
# 使用单独的 API 域名,静态资源走 CDNAPI 走专用域名
REACT_APP_API_URL=https://api.valuefrontier.cn
# PostHog 分析配置(生产环境)
# PostHog API Key从 PostHog 项目设置中获取)
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
# PostHog API Host使用 PostHog Cloud
REACT_APP_POSTHOG_HOST=https://app.posthog.com
# 启用会话录制Session Recording用于回放用户操作、排查问题
REACT_APP_ENABLE_SESSION_RECORDING=true
# React 构建优化配置
# 禁用 source map 生成(生产环境不需要,提升打包速度和安全性)
GENERATE_SOURCEMAP=false
# 跳过预检查(加快启动速度)
SKIP_PREFLIGHT_CHECK=true
# 禁用 ESLint 检查(生产构建时不需要)
DISABLE_ESLINT_PLUGIN=true
# TypeScript 编译错误时继续
TSC_COMPILE_ON_ERROR=true
# 图片内联大小限制
IMAGE_INLINE_SIZE_LIMIT=10000
# Node.js 内存限制(适用于大型项目)
NODE_OPTIONS=--max_old_space_size=4096
# 性能监控配置(生产环境)
# 启用性能监控
REACT_APP_ENABLE_PERFORMANCE_MONITOR=true
# 禁用性能面板(仅开发环境)
REACT_APP_ENABLE_PERFORMANCE_PANEL=false
# 启用 PostHog 性能数据上报
REACT_APP_REPORT_TO_POSTHOG=true

View File

@@ -1,42 +0,0 @@
# ========================================
# 本地测试环境(前后端都在本地)
# ========================================
# 使用方式: npm run start:test
#
# 工作原理:
# 1. concurrently 同时启动前端和后端
# 2. 前端: localhost:3000
# 3. 后端: localhost:5001 (python app_2.py)
# 4. 数据: 本地数据库
#
# 适用场景:
# - 调试后端代码
# - 性能测试
# - 离线开发
# - 数据库调试
# ========================================
# 环境标识
REACT_APP_ENV=test
NODE_ENV=development
# Mock 配置(关闭 MSW
REACT_APP_ENABLE_MOCK=false
# 后端 API 地址(本地后端)
REACT_APP_API_URL=http://localhost:5001
# PostHog 配置(测试环境)
# 留空 = 仅控制台 debug
# 填入 Key = 控制台 + PostHog Cloud 双模式
REACT_APP_POSTHOG_KEY=
REACT_APP_POSTHOG_HOST=https://app.posthog.com
REACT_APP_ENABLE_SESSION_RECORDING=false
# React 构建优化配置
GENERATE_SOURCEMAP=true # 测试环境保留 sourcemap 便于调试
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=false # 测试环境开启 ESLint
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096

View File

@@ -1,94 +0,0 @@
module.exports = {
root: true,
/* 环境配置 */
env: {
browser: true,
es2021: true,
node: true,
},
/* 扩展配置 */
extends: [
'react-app', // Create React App 默认规则
'react-app/jest', // Jest 测试规则
'eslint:recommended', // ESLint 推荐规则
'plugin:react/recommended', // React 推荐规则
'plugin:react-hooks/recommended', // React Hooks 规则
'plugin:prettier/recommended', // Prettier 集成
],
/* 解析器选项 */
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
/* 插件 */
plugins: ['react', 'react-hooks', 'prettier'],
/* 规则配置 */
rules: {
// React
'react/react-in-jsx-scope': 'off', // React 17+ 不需要导入 React
'react/prop-types': 'off', // 使用 TypeScript 类型检查,不需要 PropTypes
'react/display-name': 'off', // 允许匿名组件
// 通用
'no-console': ['warn', { allow: ['warn', 'error'] }], // 仅警告 console.log
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_', // 忽略以 _ 开头的未使用参数
varsIgnorePattern: '^_', // 忽略以 _ 开头的未使用变量
}],
'prettier/prettier': ['warn', {}, { usePrettierrc: true }], // 使用项目的 Prettier 配置
},
/* 设置 */
settings: {
react: {
version: 'detect', // 自动检测 React 版本
},
},
/* TypeScript 文件特殊配置 */
// 注意react-app 已包含完整的 @typescript-eslint 配置
// 此处仅覆盖特定规则,不重复加载插件
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
// TypeScript 特定规则覆盖
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-non-null-assertion': 'warn',
'no-unused-vars': 'off',
},
},
// Company 视图主题硬编码检测
{
files: ['src/views/Company/**/*.{ts,tsx,js,jsx}'],
excludedFiles: ['**/theme/**', '**/*.test.*', '**/*.spec.*'],
plugins: ['local-rules'],
rules: {
// warning 级别:提醒开发者但不阻塞构建
'local-rules/no-hardcoded-fui-colors': 'warn',
},
},
],
/* 忽略文件(与 .eslintignore 等效)*/
ignorePatterns: [
'node_modules/',
'build/',
'dist/',
'*.config.js',
'public/mockServiceWorker.js',
],
};

25
.gitignore vendored
View File

@@ -22,10 +22,6 @@ node_modules/
.env.test.local
.env.production.local
# 部署配置(包含密钥,不提交)
.env.cos
.env.deploy
# 日志
npm-debug.log*
yarn-debug.log*
@@ -39,27 +35,8 @@ pnpm-debug.log*
*.swo
*~
# Claude Code 配置
.claude/settings.local.json
# macOS
.DS_Store
# Windows
Thumbs.db
# Documentation
*.md
!README.md
!CLAUDE.md
# 忽略 docs 目录(开发文档不提交到 Git
docs/
src/assets/img/original-backup/
# 涨停分析静态数据(由 export_zt_data.py 生成,不提交到 Git
public/data/zt/
# 概念涨跌幅静态数据(由 export_concept_data.py 生成,不提交到 Git
public/data/concept/
Thumbs.db

255
CLAUDE.md
View File

@@ -1,208 +1,91 @@
# CLAUDE.md
> **🌐 语言偏好**: 请始终使用中文与用户交流,包括所有解释、分析、文档编写和代码注释。
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
本文件为 Claude Code 提供在此代码库中工作的指导说明。
## 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.
## 项目概览
### Frontend (React + Chakra UI)
- **Framework**: React 18.3.1 with Chakra UI 2.8.2
- **Styling**: Tailwind CSS + custom Chakra theme
- **Build Tool**: React Scripts with custom Gulp tasks
- **Charts**: ApexCharts, ECharts, and custom visualization components
混合式 React 仪表板,用于金融/交易分析,采用 Flask 后端。基于 Argon Dashboard Chakra PRO 模板构建。
### Backend (Flask/Python)
- **Framework**: Flask with SQLAlchemy ORM
- **Database**: ClickHouse for analytics + MySQL/PostgreSQL
- **Features**: Real-time data processing, trading analysis, user authentication
- **Task Queue**: Celery for background processing
### 技术栈
**前端**
- **核心**: React 18.3.1 + TypeScript 5.9.3(渐进式迁移中)
- **UI**: Chakra UI 2.10.9(主要)+ Ant Design 5.27.4(表格/表单)+ HeroUI 3.0.0-betaAgentChat
- **状态**: Redux Toolkit 2.9.2
- **路由**: React Router v6.30.1 + React.lazy() 代码分割
- **构建**: CRACO 7.1.0 + webpack 5 优化
- **图表**: ECharts 5.6.0、ApexCharts、Recharts、D3、Visx
- **其他**: Framer Motion、FullCalendar、Socket.IO Client、MSW、PostHog
**后端**
- Flask + SQLAlchemy ORM
- ClickHouse分析型+ MySQL事务型+ Redis缓存
- Flask-SocketIOWebSocket+ Celery后台任务
---
## 开发命令
## Development Commands
### Frontend Development
```bash
# 前端开发
npm start # Mock 模式启动(默认)
npm run start:real # 真实后端
npm run build # 生产构建
npm run lint:check # ESLint 检查
npm run type-check # TypeScript 类型检查
# 后端开发
python app.py # Flask 服务器
python simulation_background_processor.py # Celery 后台处理
# 部署
npm run deploy # 部署到生产环境
npm start # Start development server (port 3000, proxies to localhost:5001)
npm run build # Production build with license headers
npm test # Run React test suite
npm run lint:check # Check ESLint rules
npm run lint:fix # Auto-fix ESLint issues
npm run install:clean # Clean install (removes node_modules and package-lock)
```
---
## 架构
### 应用入口流程
```
src/index.js → App.js
├── AppProviders (Redux → Chakra → Ant ConfigProvider → Notification → Auth)
├── AppRoutes (src/routes/index.js)
└── GlobalComponents
```
### 路由保护模式
- `PUBLIC` - 无需认证
- `MODAL` - 未登录显示认证模态框
- `REDIRECT` - 未登录重定向到登录页
### 目录结构速查
| 目录 | 用途 |
|------|------|
| `src/views/` | 页面组件(按功能模块组织) |
| `src/components/` | 可复用 UI 组件 |
| `src/services/` | API 服务层 |
| `src/store/slices/` | Redux 状态 |
| `src/hooks/` | 自定义 Hooks |
| `src/contexts/` | React Context |
| `src/utils/` | 工具函数 |
| `src/constants/` | 常量定义 |
| `src/types/` | TypeScript 类型 |
| `src/theme/` | Chakra UI 主题 |
| `src/routes/` | 路由配置 |
| `src/mocks/` | MSW Mock 数据 |
| `src/layouts/` | 页面布局模板 |
### 路径别名
```
@/ → src/
@components/ → src/components/
@views/ → src/views/
@services/ → src/services/
@store/ → src/store/
@hooks/ → src/hooks/
@utils/ → src/utils/
@types/ → src/types/
@constants/ → src/constants/
```
---
## 开发工作流
### 添加新路由
1. `src/routes/lazy-components.js` - 添加 lazy import
2. `src/routes/routeConfig.js` - 配置路由path、component、protection、layout
### 添加新 API
1. `src/services/` - 创建服务函数
2. `src/mocks/handlers/` - 添加 MSW handlerMock 模式)
### 添加新 Redux Slice
1. `src/store/slices/yourSlice.ts` - 创建 slice
2. `src/store/index.js` - 导入并添加到 store
### 组件组织
- **原子设计**: Atoms → Molecules → Organisms
- **页面专属组件**: 放在 `views/{PageName}/components/`
- **可复用组件**: 放在 `src/components/`
---
## 配置
### 环境文件
- `.env.mock` - Mock 模式默认REACT_APP_ENABLE_MOCK=true
- `.env.development` - 开发模式
- `.env.production` - 生产环境
### MSW Mock
- 激活: `REACT_APP_ENABLE_MOCK=true`
- Handlers: `src/mocks/handlers/`
- 数据: `src/mocks/data/`
---
## TypeScript 接入
**状态**: 渐进式迁移中,支持 JS/TS 混合开发
**类型定义位置**: `src/types/`
- `api.ts` - ApiResponse、PaginatedResponse、ApiError
- `stock.ts` - StockInfo、StockQuote、KLineData
- `user.ts` - UserInfo、AuthInfo
**开发规范**:
- 新代码必须使用 TypeScript
- 避免使用 `any`
- 组件 Props 使用 `interface` 定义
**命令**:
### Backend Development
```bash
npm run type-check # 类型检查
npm run type-check:watch # 监听模式
python app_2.py # Start Flask server (main backend)
python simulation_background_processor.py # Background data processor
```
详细指南参考: [TYPESCRIPT_MIGRATION.md](./TYPESCRIPT_MIGRATION.md)
---
## 后端架构
### 核心文件
- `app.py` - Flask 主应用API + WebSocket
- `simulation_background_processor.py` - Celery 后台任务
- `concept_api.py` - 概念分析独立服务
### 数据库
| 数据库 | 用途 |
|--------|------|
| ClickHouse | 时序数据(股票日线、分钟线) |
| MySQL | 事务数据(用户、订单、持仓) |
| Redis | 缓存 + Celery 消息队列 |
### 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": {...} }
### Python Dependencies
Install from requirements.txt:
```bash
pip install -r requirements.txt
```
---
## Architecture
## 代码规范
### Frontend Structure
- `src/layouts/` - Main layout components (Admin, Auth, Home)
- `src/views/` - Page components organized by feature (Dashboard, Company, Community, etc.)
- `src/components/` - Reusable UI components (Charts, Cards, Buttons, etc.)
- `src/theme/` - Chakra UI theme customization
- `src/routes.js` - Application routing configuration
- `src/contexts/` - React context providers
- `src/services/` - API service layer
### 命名约定
- 组件/目录: PascalCase (`EventCard`)
- 文件/函数: camelCase (`formatPrice.js`)
- 常量: SCREAMING_SNAKE_CASE (`API_BASE_URL`)
- 路由路径: kebab-case (`/trading-simulation`)
### Backend Structure
- `app_2.py` - Main Flask application with routes and business logic
- `simulation_background_processor.py` - Background data processing service
- `wechat_pay.py` / `wechat_pay_config.py` - Payment integration
- `tdays.csv` - Trading days data
### 最佳实践
- 组件不直接调用 axios通过 service 层
- 使用 `getApiBase()` 获取 API 基础 URL
- 页面超过 500 行考虑拆分
- 复用组件需在 2+ 个页面使用
### Key Integrations
- ClickHouse for high-performance analytics queries
- Celery + Redis for background task processing
- Flask-SocketIO for real-time data updates
- Tencent Cloud services (SMS, etc.)
- WeChat Pay integration
---
## Configuration
## 更新本文档
### Proxy Setup
The React dev server proxies API calls to `http://localhost:5001` (see package.json).
在以下情况下更新此文档:
- 添加新的架构模式
- 做出重要技术决策
- 进行重要代码重构
### Environment Files
- `.env` - Environment variables for both frontend and backend
### Build Process
The build process includes custom Gulp tasks that add Creative Tim license headers to JS, CSS, and HTML files.
### Styling Architecture
- Tailwind CSS for utility classes
- Custom Chakra UI theme with extended color palette
- Component-specific SCSS files in `src/assets/scss/`
## Testing
- React Testing Library setup for frontend components
- Test command: `npm test`
## Deployment
- Build: `npm run build`
- Deploy: `npm run deploy` (builds the project)

13
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,13 @@
<!--
IMPORTANT: Please use the following link to create a new issue:
https://www.creative-tim.com/new-issue/argon-dashboard-chakra-pro
**If your issue was not created using the app above, it will be closed immediately.**
-->
<!--
Love Creative Tim? Do you need Angular, React, Vuejs or HTML? You can visit:
👉 https://www.creative-tim.com/bundles
👉 https://www.creative-tim.com
-->

View File

@@ -1 +0,0 @@
17Fo4JhapMw6vtNa

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# vf_react
前端

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkBfIjOiu8vfmOSq1BXcjDsAqQ+xtwGO0aCn0VrhVzc0T70nDchaW/TJ4nW8qlRMBlgfTi00jDGFiW4ND9JHc4aES8yiDSNeaBW4gLQhC1isvpOndyu4YgDc+2lMfghv9+6D8uFl9VS8Vk6o7D+5SiE7F8aBn49rrLyvsmdN5i6eLxIuY9NM58/o0xVG5f3ktGqfFKzhtclPbt8ej39EgziCiNFbIk2FnZp9dB56vtmCKht4t3STDpM0RfC8YlQ2WpGu9okLJYSy1rfynhh0hlOy/9y4cYl50wthoQVxH/Hm9abiTMo2u6xWESreavtdDZ8ByKVltnUrRvzDQ4tVkYwIDAQAB

View File

@@ -1 +0,0 @@
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCQF8iM6K7y9+Y5KrUFdyMOwCpD7G3AY7RoKfRWuFXNzRPvScNyFpb9MnidbyqVEwGWB9OLTSMMYWJbg0P0kdzhoRLzKINI15oFbiAtCELWKy+k6d3K7hiANz7aUx+CG/37oPy4WX1VLxWTqjsP7lKITsXxoGfj2usvK+yZ03mLp4vEi5j00znz+jTFUbl/eS0ap8UrOG1yU9u3x6Pf0SDOIKI0VsiTYWdmn10Hnq+2YIqG3i3dJMOkzRF8LxiVDZaka72iQslhLLWt/KeGHSGU7L/3LhxiXnTC2GhBXEf8eb1puJMyja7rFYRKt5q+10NnwHIpWW2dStG/MNDi1WRjAgMBAAECggEAHQ8+2fQfPE70djj/su94+YOVwocPB0rUWmGDrm2UmGGwkISezwZxQvUH0DBYNSJVIo3HgwN2ewu0y2HotY0pL7PNX46fE3Sv0kKIaKyO1iR1glvL6B4mgM0jduJmq1W73iB0dzVNCn3paxNcv/S/XlAMqZNBAHnpDmVcXRWCIMDG1yRN3l5NbfgPoQZI4MfAdthjIcIQmEVjNWy69swsdNndj8yrIu9E9RlWly/xqB/BJ6O53i0n+jyviy2grgHxo9VgWKv89qZiczioLT7aAJITpcMofUkGImZy+DHlf+6GU762UkwbLykYN2RRkw5TPvWt4ZUXeON4flr3ig02yQKBgQDMh82rc3TklOZSCw3lwYE58eeYxe9PXpZzcOyrSZZhCs0y528dmwYbm0mPlpVti1MGKnn2eGVKeGQ8a5CbJCi2T+VwIILWM9U2+h82nJW5KD6CYaE86KK/PlzhTGwmpecv8hafkpn51zvyjI3UkKbv/Ep+Mfy89PLumvh5Ze+EfwKBgQC0Wn1o0JCjgCt3K39tahavBuCPDvk7oLA6JLp2W9437YFeuh9L/gYdAtJU79WpmOfgr228cOlExdGGpHqLPSDFpN3ssx1pkUJ6RlTN8YInc+dbAkC6gsm5XR0Jvj4YqghyWHKhxXErnFGDof2EQq7ghHK9pokpBFPowwlzkpOeHQKBgFbqVwJG/COvCvlObUd3pbzECdEoO/wUjAbetBROHzN57Z12MAf6uuu8X9Q+/50fmdaC8nVE0HaHFsF+TGNBSHPBHBU8G51/RVopjF4eyJl4eqfZaTWC/rYagEnVuhfqZIZBcE+7cudzCayXAiaUmfxd0CI0h9yckyfGf1THdrNtAoGASMtNWwTznEqbQJpZ8HuldDe+Y3+TsTGGb7FrYWJrKv+9+9H719xL82G0K3wyLSX+UX39ONYKESwXCdVRcOnXVG7a9DLHaFitEFVa3VThR7NMajtajO1FJoAivFABGEto5V41xn2+0+9gJ1U20i9oDk7nUQzqx5drlsNCCVfcJTECgYEAlEYIQ3AiVqx0RqZjfbg+nyZQmoUfHPASY2Hu9pJWvLwXsQWSPh1gf03galXzZ46wrCaeLygGaoHXW7W8WwsYBR1tG7voQeSe8mmlWgiscmvmvqSowJ10BnsdWwpOTZ8O6rKy8HdyI8gFJyfJgfz+6KekcdGEnQbmwCvB8j+Y7IQ=

View File

@@ -1 +0,0 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyULL5tuD2cjfNvgn9fvVfn+WbLhoP3wk3bcYb9AabsZc1GCBlnG579Socc3U9dG5IR7C3KHD4kYHnH4tbK2pEWmmfjbVKXWguLqL5K3Dggnl5KVOlVVjrcsmLPqTj7JdeO0AQolmgdr/o6TlhQdsqINQAK5F5wWwIwwcSoJsWZ6zlPPX/Q/eMIN4zGgK2taMhx656zSxsYE5OKRYkTJhVrMktxQdwbUzoFSID++dTpjxF4w5k5qeVW/1WZaaswMFWh2IcJ5oyc+VjTRqZvtQt4gB0Fa0EblfmSJaozhoWHwzwF+1qtv7gp/TcMYj/Ah2+tY0UPxucEcTqY/i7PPfwIDAQAB

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付配置文件
电脑网站支付 (alipay.trade.page.pay)
"""
import os
# 获取当前文件所在目录
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 支付宝支付配置
ALIPAY_CONFIG = {
# 应用ID - 从支付宝开放平台获取
'app_id': '2021005183650009',
# 支付宝网关 - 正式环境
'gateway_url': 'https://openapi.alipay.com/gateway.do',
# 沙箱环境网关(测试用)
# 'gateway_url': 'https://openapi-sandbox.dl.alipaydev.com/gateway.do',
# 签名方式 - 必须使用RSA2
'sign_type': 'RSA2',
# 编码格式
'charset': 'utf-8',
# 返回格式
'format': 'json',
# 密钥文件路径
'app_private_key_path': os.path.join(_BASE_DIR, 'alipay', '应用私钥.txt'),
'alipay_public_key_path': os.path.join(_BASE_DIR, 'alipay', '支付宝公钥.txt'),
# 回调地址 - 替换为你的实际域名
'notify_url': 'https://api.valuefrontier.cn/api/payment/alipay/callback', # 异步通知地址(后端)
'return_url': 'https://valuefrontier.cn/pricing?payment_return=alipay', # 同步跳转地址(前端页面)
# 产品码 - 电脑网站支付固定为此值
'product_code': 'FAST_INSTANT_TRADE_PAY',
}
def load_key_from_file(file_path):
"""从文件读取密钥内容"""
import sys
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read().strip()
except FileNotFoundError:
print(f"[AlipayConfig] Key file not found: {file_path}", file=sys.stderr)
return None
except Exception as e:
print(f"[AlipayConfig] Read key file failed: {e}", file=sys.stderr)
return None
def get_app_private_key():
"""获取应用私钥"""
return load_key_from_file(ALIPAY_CONFIG['app_private_key_path'])
def get_alipay_public_key():
"""获取支付宝公钥"""
return load_key_from_file(ALIPAY_CONFIG['alipay_public_key_path'])
def validate_config():
"""验证配置是否完整"""
issues = []
# 检查 app_id
if not ALIPAY_CONFIG.get('app_id') or ALIPAY_CONFIG['app_id'].startswith('your_'):
issues.append("app_id not configured")
# 检查密钥文件
if not os.path.exists(ALIPAY_CONFIG['app_private_key_path']):
issues.append(f"Private key file not found: {ALIPAY_CONFIG['app_private_key_path']}")
else:
key = get_app_private_key()
if not key or len(key) < 100:
issues.append("Private key content invalid")
if not os.path.exists(ALIPAY_CONFIG['alipay_public_key_path']):
issues.append(f"Alipay public key file not found: {ALIPAY_CONFIG['alipay_public_key_path']}")
else:
key = get_alipay_public_key()
if not key or len(key) < 100:
issues.append("Alipay public key content invalid")
return len(issues) == 0, issues
if __name__ == "__main__":
print("Alipay Payment Config Validation")
print("=" * 50)
is_valid, issues = validate_config()
if is_valid:
print("[OK] Config validation passed!")
print(f" App ID: {ALIPAY_CONFIG['app_id']}")
print(f" Gateway: {ALIPAY_CONFIG['gateway_url']}")
print(f" Notify URL: {ALIPAY_CONFIG['notify_url']}")
print(f" Return URL: {ALIPAY_CONFIG['return_url']}")
else:
print("[ERROR] Config has issues:")
for issue in issues:
print(f" - {issue}")
print("\nSetup steps:")
print("1. Login to Alipay Open Platform (open.alipay.com)")
print("2. Create app and get App ID")
print("3. Configure RSA2 keys in development settings")
print("4. Put private key and Alipay public key in ./alipay/ folder")
print("5. Update config in this file")
print("=" * 50)

View File

@@ -1,523 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付集成模块
电脑网站支付 (alipay.trade.page.pay)
"""
import json
import time
import base64
import hashlib
from datetime import datetime
from urllib.parse import quote_plus, urlencode
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
class AlipayPay:
"""支付宝电脑网站支付类"""
def __init__(self, app_id, app_private_key, alipay_public_key, notify_url, return_url,
gateway_url='https://openapi.alipay.com/gateway.do',
sign_type='RSA2', charset='utf-8'):
"""
初始化支付宝支付配置
Args:
app_id: 支付宝应用ID
app_private_key: 应用私钥Base64编码的字符串
alipay_public_key: 支付宝公钥Base64编码的字符串
notify_url: 异步通知地址
return_url: 同步跳转地址
gateway_url: 支付宝网关URL
sign_type: 签名类型默认RSA2
charset: 编码默认utf-8
"""
self.app_id = app_id
self.notify_url = notify_url
self.return_url = return_url
self.gateway_url = gateway_url
self.sign_type = sign_type
self.charset = charset
# 加载密钥
self.app_private_key = self._load_private_key(app_private_key)
self.alipay_public_key = self._load_public_key(alipay_public_key)
# 注意:不要在这里使用 print会影响 subprocess 的 JSON 输出
def _load_private_key(self, key_str):
"""加载RSA私钥支持 PKCS#1 和 PKCS#8 格式)"""
try:
# 如果密钥不包含头尾尝试添加PEM格式头尾
if '-----BEGIN' not in key_str:
# 支付宝通常使用 PKCS#8 格式MIIEv 开头)
# 先尝试 PKCS#8 格式
key_str_pkcs8 = f"-----BEGIN PRIVATE KEY-----\n{key_str}\n-----END PRIVATE KEY-----"
try:
private_key = serialization.load_pem_private_key(
key_str_pkcs8.encode('utf-8'),
password=None,
backend=default_backend()
)
return private_key
except Exception:
# 如果 PKCS#8 失败,尝试 PKCS#1 格式
key_str = f"-----BEGIN RSA PRIVATE KEY-----\n{key_str}\n-----END RSA PRIVATE KEY-----"
private_key = serialization.load_pem_private_key(
key_str.encode('utf-8'),
password=None,
backend=default_backend()
)
return private_key
except Exception as e:
import sys
print(f"[AlipayPay] Load private key failed: {e}", file=sys.stderr)
raise
def _load_public_key(self, key_str):
"""加载RSA公钥"""
import sys
try:
# 如果密钥不包含头尾添加PEM格式头尾
if '-----BEGIN' not in key_str:
key_str = f"-----BEGIN PUBLIC KEY-----\n{key_str}\n-----END PUBLIC KEY-----"
public_key = serialization.load_pem_public_key(
key_str.encode('utf-8'),
backend=default_backend()
)
return public_key
except Exception as e:
print(f"[AlipayPay] Load public key failed: {e}", file=sys.stderr)
raise
def _sign(self, unsigned_string):
"""
RSA2签名
Args:
unsigned_string: 待签名字符串
Returns:
Base64编码的签名
"""
import sys
try:
signature = self.app_private_key.sign(
unsigned_string.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode('utf-8')
except Exception as e:
print(f"[AlipayPay] Sign failed: {e}", file=sys.stderr)
raise
def _verify(self, message, signature):
"""
验证支付宝签名
Args:
message: 原始消息
signature: Base64编码的签名
Returns:
bool: 验证是否通过
"""
import sys
try:
self.alipay_public_key.verify(
base64.b64decode(signature),
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception as e:
print(f"[AlipayPay] Verify signature failed: {e}", file=sys.stderr)
return False
def _get_sign_content(self, params):
"""
获取待签名字符串
Args:
params: 参数字典
Returns:
排序后的参数字符串
"""
# 过滤空值和sign参数按key排序
filtered_params = {k: v for k, v in params.items()
if v is not None and v != '' and k != 'sign'}
sorted_params = sorted(filtered_params.items())
# 拼接字符串
sign_content = '&'.join([f'{k}={v}' for k, v in sorted_params])
return sign_content
def create_page_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m'):
"""
创建电脑网站支付URL
Args:
out_trade_no: 商户订单号
total_amount: 订单总金额(元,精确到小数点后两位)
subject: 订单标题
body: 订单描述(可选)
timeout_express: 订单超时时间默认30分钟
Returns:
dict: 包含支付URL的响应
"""
try:
# 金额格式化为两位小数(支付宝要求)
formatted_amount = f"{float(total_amount):.2f}"
# 业务参数
biz_content = {
'out_trade_no': out_trade_no,
'total_amount': formatted_amount,
'subject': subject,
'product_code': 'FAST_INSTANT_TRADE_PAY', # 电脑网站支付固定值
'timeout_express': timeout_express,
}
if body:
biz_content['body'] = body
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.page.pay',
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'notify_url': self.notify_url,
'return_url': self.return_url,
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 构建完整的支付URL
pay_url = f"{self.gateway_url}?{urlencode(params)}"
# 日志输出到 stderr避免影响 subprocess JSON 输出
import sys
print(f"[AlipayPay] Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr)
return {
'success': True,
'pay_url': pay_url,
'order_no': out_trade_no
}
except Exception as e:
import sys
print(f"[AlipayPay] Create order failed: {e}", file=sys.stderr)
return {
'success': False,
'error': str(e)
}
def create_wap_pay_url(self, out_trade_no, total_amount, subject, body=None, timeout_express='30m', quit_url=None):
"""
创建手机网站支付URL (H5支付可调起手机支付宝APP)
Args:
out_trade_no: 商户订单号
total_amount: 订单总金额(元,精确到小数点后两位)
subject: 订单标题
body: 订单描述(可选)
timeout_express: 订单超时时间默认30分钟
quit_url: 用户付款中途退出返回商户网站的地址(可选)
Returns:
dict: 包含支付URL的响应
"""
try:
# 金额格式化为两位小数(支付宝要求)
formatted_amount = f"{float(total_amount):.2f}"
# 业务参数
biz_content = {
'out_trade_no': out_trade_no,
'total_amount': formatted_amount,
'subject': subject,
'product_code': 'QUICK_WAP_WAY', # 手机网站支付固定值
'timeout_express': timeout_express,
}
if body:
biz_content['body'] = body
if quit_url:
biz_content['quit_url'] = quit_url
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.wap.pay', # 手机网站支付接口
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'notify_url': self.notify_url,
'return_url': self.return_url,
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 构建完整的支付URL
pay_url = f"{self.gateway_url}?{urlencode(params)}"
# 日志输出到 stderr避免影响 subprocess JSON 输出
import sys
print(f"[AlipayPay] WAP Order created: {out_trade_no}, amount: {formatted_amount}", file=sys.stderr)
return {
'success': True,
'pay_url': pay_url,
'order_no': out_trade_no,
'pay_type': 'wap' # 标识为手机网站支付
}
except Exception as e:
import sys
print(f"[AlipayPay] Create WAP order failed: {e}", file=sys.stderr)
return {
'success': False,
'error': str(e)
}
def query_order(self, out_trade_no=None, trade_no=None):
"""
查询订单状态
Args:
out_trade_no: 商户订单号
trade_no: 支付宝交易号
Returns:
dict: 订单状态信息
"""
import requests
try:
if not out_trade_no and not trade_no:
return {'success': False, 'error': '订单号和交易号不能同时为空'}
# 业务参数
biz_content = {}
if out_trade_no:
biz_content['out_trade_no'] = out_trade_no
if trade_no:
biz_content['trade_no'] = trade_no
# 公共请求参数
params = {
'app_id': self.app_id,
'method': 'alipay.trade.query',
'format': 'json',
'charset': self.charset,
'sign_type': self.sign_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'version': '1.0',
'biz_content': json.dumps(biz_content, ensure_ascii=False),
}
# 生成签名
sign_content = self._get_sign_content(params)
params['sign'] = self._sign(sign_content)
# 发送请求
response = requests.get(self.gateway_url, params=params, timeout=30)
result = response.json()
# 日志输出到 stderr
import sys
print(f"[AlipayPay] Query response: {result}", file=sys.stderr)
# 解析响应
query_response = result.get('alipay_trade_query_response', {})
if query_response.get('code') == '10000':
trade_status = query_response.get('trade_status')
return {
'success': True,
'trade_status': trade_status,
'trade_no': query_response.get('trade_no'),
'out_trade_no': query_response.get('out_trade_no'),
'total_amount': query_response.get('total_amount'),
'buyer_logon_id': query_response.get('buyer_logon_id'),
'send_pay_date': query_response.get('send_pay_date'),
# 状态映射
'is_paid': trade_status == 'TRADE_SUCCESS' or trade_status == 'TRADE_FINISHED',
'is_closed': trade_status == 'TRADE_CLOSED',
'is_waiting': trade_status == 'WAIT_BUYER_PAY',
}
else:
error_msg = query_response.get('sub_msg') or query_response.get('msg', '查询失败')
return {
'success': False,
'error': error_msg,
'code': query_response.get('code'),
'sub_code': query_response.get('sub_code')
}
except requests.RequestException as e:
import sys
print(f"[AlipayPay] API request failed: {e}", file=sys.stderr)
return {'success': False, 'error': f'网络请求失败: {e}'}
except Exception as e:
import sys
print(f"[AlipayPay] Query order error: {e}", file=sys.stderr)
return {'success': False, 'error': str(e)}
def verify_callback(self, params):
"""
验证支付宝异步回调
Args:
params: 回调参数字典
Returns:
dict: 验证结果
"""
try:
# 获取签名
sign = params.pop('sign', None)
sign_type = params.pop('sign_type', 'RSA2')
if not sign:
return {'success': False, 'error': '缺少签名参数'}
# 构建待验签字符串
sign_content = self._get_sign_content(params)
# 验证签名
import sys
if self._verify(sign_content, sign):
print(f"[AlipayPay] Callback signature verified", file=sys.stderr)
return {
'success': True,
'data': params
}
else:
print(f"[AlipayPay] Callback signature verification failed", file=sys.stderr)
return {
'success': False,
'error': '签名验证失败'
}
except Exception as e:
print(f"[AlipayPay] Verify callback error: {e}", file=sys.stderr)
return {'success': False, 'error': str(e)}
def verify_return(self, params):
"""
验证支付宝同步返回(用户支付后跳转回来)
Args:
params: 同步返回参数字典
Returns:
dict: 验证结果
"""
# 同步返回的验签逻辑与异步回调相同
return self.verify_callback(params)
# 工厂函数
def create_alipay_instance():
"""创建支付宝支付实例"""
try:
from alipay_config import ALIPAY_CONFIG, get_app_private_key, get_alipay_public_key, validate_config
# 验证配置
is_valid, issues = validate_config()
if not is_valid:
raise ValueError(f"支付宝配置错误: {'; '.join(issues)}")
# 获取密钥
app_private_key = get_app_private_key()
alipay_public_key = get_alipay_public_key()
if not app_private_key or not alipay_public_key:
raise ValueError("密钥加载失败")
return AlipayPay(
app_id=ALIPAY_CONFIG['app_id'],
app_private_key=app_private_key,
alipay_public_key=alipay_public_key,
notify_url=ALIPAY_CONFIG['notify_url'],
return_url=ALIPAY_CONFIG['return_url'],
gateway_url=ALIPAY_CONFIG['gateway_url'],
sign_type=ALIPAY_CONFIG['sign_type'],
charset=ALIPAY_CONFIG['charset']
)
except ImportError as e:
raise ValueError(f"无法导入支付宝配置: {e}")
except Exception as e:
raise ValueError(f"创建支付宝实例失败: {e}")
def check_alipay_ready():
"""检查支付宝支付是否就绪"""
try:
instance = create_alipay_instance()
return True, "支付宝支付配置正确"
except Exception as e:
return False, str(e)
if __name__ == '__main__':
"""测试代码"""
import sys
print("=" * 60)
print("Alipay Payment Test")
print("=" * 60)
try:
# 检查配置
is_ready, message = check_alipay_ready()
print(f"\nConfig status: {'READY' if is_ready else 'NOT READY'}")
print(f"Details: {message}")
if is_ready:
# 创建实例
alipay = create_alipay_instance()
# 测试创建订单
test_order_no = f"TEST{int(time.time())}"
result = alipay.create_page_pay_url(
out_trade_no=test_order_no,
total_amount='0.01',
subject='Test Product',
body='This is a test order'
)
print(f"\nCreate order result:")
print(json.dumps(result, indent=2, ensure_ascii=False))
if result['success']:
print(f"\nPayment URL (open in browser):")
print(result['pay_url'][:200] + '...')
except Exception as e:
print(f"\nTest failed: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 60)

View File

@@ -1,163 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
支付宝支付工作脚本
用于在 subprocess 中执行,绕过 eventlet DNS 问题
用法:
python alipay_pay_worker.py check # 检查配置
python alipay_pay_worker.py create <order_no> <amount> <subject> [body] [pay_type] # 创建订单
pay_type: page=电脑网站支付(默认), wap=手机网站支付
python alipay_pay_worker.py query <order_no> # 查询订单
"""
import sys
import json
def check_config():
"""检查支付宝配置"""
try:
from alipay_pay import check_alipay_ready
is_ready, message = check_alipay_ready()
return {
'success': is_ready,
'message': message if is_ready else None,
'error': None if is_ready else message
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
def create_order(order_no, amount, subject, body=None, pay_type='page'):
"""创建支付宝订单
Args:
order_no: 订单号
amount: 金额
subject: 标题
body: 描述
pay_type: 支付类型 'page'=电脑网站支付, 'wap'=手机网站支付
"""
try:
from alipay_pay import create_alipay_instance
alipay = create_alipay_instance()
if pay_type == 'wap':
# 手机网站支付
result = alipay.create_wap_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m',
quit_url='https://valuefrontier.cn/pricing' # 用户取消支付时返回的页面
)
else:
# 电脑网站支付(默认)
result = alipay.create_page_pay_url(
out_trade_no=order_no,
total_amount=str(amount),
subject=subject,
body=body,
timeout_express='30m'
)
return result
except Exception as e:
return {
'success': False,
'error': str(e)
}
def query_order(order_no):
"""查询支付宝订单"""
try:
from alipay_pay import create_alipay_instance
alipay = create_alipay_instance()
result = alipay.query_order(out_trade_no=order_no)
# 转换状态为统一格式
if result.get('success'):
trade_status = result.get('trade_status')
# 映射支付宝状态到统一状态
if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
result['trade_state'] = 'SUCCESS'
elif trade_status == 'WAIT_BUYER_PAY':
result['trade_state'] = 'NOTPAY'
elif trade_status == 'TRADE_CLOSED':
result['trade_state'] = 'CLOSED'
else:
result['trade_state'] = trade_status
return result
except Exception as e:
return {
'success': False,
'error': str(e)
}
def main():
"""主函数"""
if len(sys.argv) < 2:
print(json.dumps({
'success': False,
'error': '缺少命令参数。用法: check | create <order_no> <amount> <subject> | query <order_no>'
}))
sys.exit(1)
command = sys.argv[1].lower()
try:
if command == 'check':
result = check_config()
elif command == 'create':
if len(sys.argv) < 5:
result = {
'success': False,
'error': '创建订单需要参数: order_no, amount, subject'
}
else:
order_no = sys.argv[2]
amount = sys.argv[3]
subject = sys.argv[4]
body = sys.argv[5] if len(sys.argv) > 5 else None
# pay_type: page=电脑网站支付, wap=手机网站支付
pay_type = sys.argv[6] if len(sys.argv) > 6 else 'page'
result = create_order(order_no, amount, subject, body, pay_type)
elif command == 'query':
if len(sys.argv) < 3:
result = {
'success': False,
'error': '查询订单需要参数: order_no'
}
else:
order_no = sys.argv[2]
result = query_order(order_no)
else:
result = {
'success': False,
'error': f'未知命令: {command}'
}
print(json.dumps(result, ensure_ascii=False))
except Exception as e:
print(json.dumps({
'success': False,
'error': str(e)
}, ensure_ascii=False))
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -1,262 +0,0 @@
"""
APNs 推送相关的数据库模型和 API 端点
将以下代码添加到 app.py 中
=== 步骤 1: 添加数据库模型(放在其他模型定义附近)===
"""
# ==================== 数据库模型 ====================
class UserDeviceToken(db.Model):
"""用户设备推送 Token"""
__tablename__ = 'user_device_tokens'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # 可选,未登录也能注册
device_token = db.Column(db.String(255), unique=True, nullable=False)
platform = db.Column(db.String(20), default='ios') # ios / android
app_version = db.Column(db.String(20), nullable=True)
is_active = db.Column(db.Boolean, default=True)
# 推送订阅设置
subscribe_all = db.Column(db.Boolean, default=True) # 订阅所有事件
subscribe_important = db.Column(db.Boolean, default=True) # 订阅重要事件 (S/A级)
subscribe_types = db.Column(db.Text, nullable=True) # 订阅的事件类型JSON 数组
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
user = db.relationship('User', backref='device_tokens')
"""
=== 步骤 2: 添加 API 端点(放在其他 API 定义附近)===
"""
# ==================== Device Token API ====================
@app.route('/api/users/device-token', methods=['POST'])
def register_device_token():
"""
注册设备推送 Token
App 启动时调用,保存设备 Token 用于推送
"""
try:
data = request.get_json()
device_token = data.get('device_token')
platform = data.get('platform', 'ios')
app_version = data.get('app_version', '1.0.0')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
# 查找是否已存在
existing = UserDeviceToken.query.filter_by(device_token=device_token).first()
if existing:
# 更新现有记录
existing.platform = platform
existing.app_version = app_version
existing.is_active = True
existing.updated_at = datetime.now()
# 如果用户已登录,关联用户
if current_user.is_authenticated:
existing.user_id = current_user.id
else:
# 创建新记录
new_token = UserDeviceToken(
device_token=device_token,
platform=platform,
app_version=app_version,
user_id=current_user.id if current_user.is_authenticated else None,
is_active=True
)
db.session.add(new_token)
db.session.commit()
return jsonify({
'success': True,
'message': 'Device token 注册成功'
})
except Exception as e:
db.session.rollback()
print(f"[API] 注册 device token 失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/users/device-token', methods=['DELETE'])
def unregister_device_token():
"""
取消注册设备 Token用户登出时调用
"""
try:
data = request.get_json()
device_token = data.get('device_token')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
if token_record:
token_record.is_active = False
token_record.updated_at = datetime.now()
db.session.commit()
return jsonify({
'success': True,
'message': 'Device token 已取消注册'
})
except Exception as e:
db.session.rollback()
print(f"[API] 取消注册 device token 失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
@app.route('/api/users/push-subscription', methods=['POST'])
def update_push_subscription():
"""
更新推送订阅设置
"""
try:
data = request.get_json()
device_token = data.get('device_token')
if not device_token:
return jsonify({'success': False, 'message': '缺少 device_token'}), 400
token_record = UserDeviceToken.query.filter_by(device_token=device_token).first()
if not token_record:
return jsonify({'success': False, 'message': 'Device token 不存在'}), 404
# 更新订阅设置
if 'subscribe_all' in data:
token_record.subscribe_all = data['subscribe_all']
if 'subscribe_important' in data:
token_record.subscribe_important = data['subscribe_important']
if 'subscribe_types' in data:
token_record.subscribe_types = json.dumps(data['subscribe_types'])
token_record.updated_at = datetime.now()
db.session.commit()
return jsonify({
'success': True,
'message': '订阅设置已更新'
})
except Exception as e:
db.session.rollback()
print(f"[API] 更新推送订阅失败: {e}")
return jsonify({'success': False, 'message': str(e)}), 500
"""
=== 步骤 3: 修改 broadcast_new_event 函数,添加 APNs 推送 ===
"""
# 在文件顶部导入
from apns_push import send_event_notification
# 修改 broadcast_new_event 函数
def broadcast_new_event_with_apns(event):
"""
广播新事件到所有订阅的客户端WebSocket + APNs
"""
try:
# === 原有的 WebSocket 推送代码 ===
print(f'\n[WebSocket DEBUG] ========== 广播新事件 ==========')
print(f'[WebSocket DEBUG] 事件ID: {event.id}')
print(f'[WebSocket DEBUG] 事件标题: {event.title}')
print(f'[WebSocket DEBUG] 重要性: {event.importance}')
event_data = {
'id': event.id,
'title': event.title,
'importance': event.importance,
'event_type': event.event_type,
'created_at': event.created_at.isoformat() if event.created_at else None,
'source': event.source,
'related_avg_chg': event.related_avg_chg,
'related_max_chg': event.related_max_chg,
'hot_score': event.hot_score,
'keywords': event.keywords_list if hasattr(event, 'keywords_list') else event.keywords,
}
# 发送到 WebSocket 房间
socketio.emit('new_event', event_data, room='events_all', namespace='/')
if event.event_type:
room_name = f"events_{event.event_type}"
socketio.emit('new_event', event_data, room=room_name, namespace='/')
# === 新增APNs 推送 ===
# 只推送重要事件 (S/A 级)
if event.importance in ['S', 'A']:
try:
# 获取所有活跃的设备 token
# 可以根据订阅设置过滤
device_tokens_query = db.session.query(UserDeviceToken.device_token).filter(
UserDeviceToken.is_active == True,
db.or_(
UserDeviceToken.subscribe_all == True,
UserDeviceToken.subscribe_important == True
)
).all()
tokens = [t[0] for t in device_tokens_query]
if tokens:
print(f'[APNs] 准备推送到 {len(tokens)} 个设备')
result = send_event_notification(event, tokens)
print(f'[APNs] 推送结果: 成功 {result["success"]}, 失败 {result["failed"]}')
else:
print('[APNs] 没有活跃的设备 token')
except Exception as apns_error:
print(f'[APNs ERROR] 推送失败: {apns_error}')
import traceback
traceback.print_exc()
# 清除事件列表缓存
clear_events_cache()
print(f'[WebSocket DEBUG] ========== 广播完成 ==========\n')
except Exception as e:
print(f'[WebSocket ERROR] 推送新事件失败: {e}')
import traceback
traceback.print_exc()
"""
=== 步骤 4: 创建数据库表(在 MySQL 中执行)===
"""
SQL_CREATE_TABLE = """
CREATE TABLE IF NOT EXISTS user_device_tokens (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
device_token VARCHAR(255) NOT NULL UNIQUE,
platform VARCHAR(20) DEFAULT 'ios',
app_version VARCHAR(20) NULL,
is_active BOOLEAN DEFAULT TRUE,
subscribe_all BOOLEAN DEFAULT TRUE,
subscribe_important BOOLEAN DEFAULT TRUE,
subscribe_types TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_device_token (device_token),
INDEX idx_user_id (user_id),
INDEX idx_is_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
print("请在 MySQL 中执行以下 SQL 创建表:")
print(SQL_CREATE_TABLE)

View File

@@ -1,225 +0,0 @@
"""
APNs 推送通知模块
用于向 iOS 设备发送推送通知
使用前需要安装:
pip install PyAPNs2
配置:
1. 将 AuthKey_HSF578B626.p8 文件放到服务器安全目录
2. 设置环境变量或修改下面的配置
"""
import os
from datetime import datetime
from typing import List, Optional
from apns2.client import APNsClient, NotificationPriority
from apns2.payload import Payload, PayloadAlert
from apns2.credentials import TokenCredentials
# ==================== APNs 配置 ====================
# APNs 认证配置
APNS_KEY_ID = 'HSF578B626'
APNS_TEAM_ID = '6XML2LHR2J'
APNS_BUNDLE_ID = 'com.valuefrontier.meagent'
# Auth Key 文件路径(基于当前文件位置)
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APNS_AUTH_KEY_PATH = os.environ.get(
'APNS_AUTH_KEY_PATH',
os.path.join(_BASE_DIR, 'cert', 'AuthKey_HSF578B626.p8')
)
# 是否使用沙盒环境(开发测试用 True生产用 False
APNS_USE_SANDBOX = os.environ.get('APNS_USE_SANDBOX', 'false').lower() == 'true'
# ==================== APNs 客户端初始化 ====================
_apns_client = None
def get_apns_client():
"""获取 APNs 客户端(单例)"""
global _apns_client
if _apns_client is None:
if not os.path.exists(APNS_AUTH_KEY_PATH):
print(f"[APNs] 警告: Auth Key 文件不存在: {APNS_AUTH_KEY_PATH}")
return None
try:
credentials = TokenCredentials(
auth_key_path=APNS_AUTH_KEY_PATH,
auth_key_id=APNS_KEY_ID,
team_id=APNS_TEAM_ID
)
_apns_client = APNsClient(
credentials=credentials,
use_sandbox=APNS_USE_SANDBOX
)
print(f"[APNs] 客户端初始化成功 (sandbox={APNS_USE_SANDBOX})")
except Exception as e:
print(f"[APNs] 客户端初始化失败: {e}")
return None
return _apns_client
# ==================== 推送函数 ====================
def send_push_notification(
device_tokens: List[str],
title: str,
body: str,
data: Optional[dict] = None,
badge: int = 1,
sound: str = "default",
priority: int = 10
) -> dict:
"""
发送推送通知到多个设备
Args:
device_tokens: 设备 Token 列表
title: 通知标题
body: 通知内容
data: 自定义数据(会传递给 App
badge: 角标数字
sound: 提示音
priority: 优先级 (10=立即, 5=省电)
Returns:
dict: {success: int, failed: int, errors: list}
"""
client = get_apns_client()
if not client:
return {'success': 0, 'failed': len(device_tokens), 'errors': ['APNs 客户端未初始化']}
# 构建通知内容
alert = PayloadAlert(title=title, body=body)
payload = Payload(
alert=alert,
sound=sound,
badge=badge,
custom=data or {}
)
# 设置优先级
notification_priority = NotificationPriority.Immediate if priority == 10 else NotificationPriority.PowerConsideration
results = {'success': 0, 'failed': 0, 'errors': []}
for token in device_tokens:
try:
client.send_notification(
token_hex=token,
notification=payload,
topic=APNS_BUNDLE_ID,
priority=notification_priority
)
results['success'] += 1
print(f"[APNs] 推送成功: {token[:20]}...")
except Exception as e:
results['failed'] += 1
results['errors'].append(f"{token[:20]}...: {str(e)}")
print(f"[APNs] 推送失败 {token[:20]}...: {e}")
return results
def send_event_notification(event, device_tokens: List[str]) -> dict:
"""
发送事件推送通知
Args:
event: Event 模型实例
device_tokens: 设备 Token 列表
Returns:
dict: 推送结果
"""
if not device_tokens:
return {'success': 0, 'failed': 0, 'errors': ['没有设备 Token']}
# 根据重要性设置标题
importance_labels = {
'S': '重大事件',
'A': '重要事件',
'B': '一般事件',
'C': '普通事件'
}
title = f"{importance_labels.get(event.importance, '新事件')}"
# 通知内容(截断过长的标题)
body = event.title[:100] + ('...' if len(event.title) > 100 else '')
# 自定义数据
data = {
'event_id': event.id,
'importance': event.importance,
'title': event.title,
'event_type': event.event_type,
'created_at': event.created_at.isoformat() if event.created_at else None
}
return send_push_notification(
device_tokens=device_tokens,
title=title,
body=body,
data=data,
badge=1,
priority=10 if event.importance in ['S', 'A'] else 5
)
# ==================== 数据库操作(需要在 app.py 中实现) ====================
def get_all_device_tokens(db_session) -> List[str]:
"""
获取所有活跃的设备 Token
需要在 app.py 中实现实际的数据库查询
"""
# 示例 SQL:
# SELECT device_token FROM user_device_tokens WHERE is_active = 1
pass
def get_subscribed_device_tokens(db_session, importance_filter: List[str] = None) -> List[str]:
"""
获取订阅了推送的设备 Token
Args:
importance_filter: 重要性过滤,如 ['S', 'A'] 表示只获取订阅了重要事件的用户
"""
# 示例 SQL:
# SELECT device_token FROM user_device_tokens
# WHERE is_active = 1
# AND (subscribe_all = 1 OR subscribe_importance IN ('S', 'A'))
pass
# ==================== 在 app.py 中调用示例 ====================
"""
在 app.py 的 broadcast_new_event() 函数中添加:
from apns_push import send_event_notification
def broadcast_new_event(event):
# ... 现有的 WebSocket 推送代码 ...
# 添加 APNs 推送(只推送重要事件)
if event.importance in ['S', 'A']:
try:
# 获取所有设备 token
device_tokens = db.session.query(UserDeviceToken.device_token).filter(
UserDeviceToken.is_active == True
).all()
tokens = [t[0] for t in device_tokens]
if tokens:
result = send_event_notification(event, tokens)
print(f"[APNs] 事件推送结果: {result}")
except Exception as e:
print(f"[APNs] 推送失败: {e}")
"""

10377
app.py

File diff suppressed because it is too large Load Diff

45
app/__init__.py Normal file
View File

@@ -0,0 +1,45 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from datetime import datetime
import pytz
import os
# 创建Flask应用
app = Flask(__name__)
# 配置
config_name = os.environ.get('FLASK_ENV', 'development')
from config import config
app.config.from_object(config[config_name])
# 初始化扩展
db = SQLAlchemy(app)
CORS(app, resources={r"/api/*": {"origins": "*"}})
# 时区设置
def beijing_now():
"""获取北京时间"""
tz = pytz.timezone('Asia/Shanghai')
return datetime.now(tz)
# 导入模型
from app.models import *
# 创建数据库表
with app.app_context():
db.create_all()
# 注册路由
from app.routes import events, stocks, limitanalyse, calendar, industries
app.register_blueprint(events.bp)
app.register_blueprint(stocks.bp)
app.register_blueprint(limitanalyse.bp)
app.register_blueprint(calendar.bp)
app.register_blueprint(industries.bp)
if __name__ == '__main__':
print("=== Value Frontier React 架构启动 ===")
app.run(host='0.0.0.0', port=5001, debug=True)

Binary file not shown.

Binary file not shown.

Binary file not shown.

30
app/extensions.py Normal file
View File

@@ -0,0 +1,30 @@
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_compress import Compress
from flask_cors import CORS
from clickhouse_driver import Client as Cclient
from sqlalchemy import create_engine
# Database instances
db = SQLAlchemy()
# Other extensions
login_manager = LoginManager()
compress = Compress()
cors = CORS()
# Database engines (如果仍然需要直接使用 engine)
engine = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/stock", echo=False)
engine_med = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/med", echo=False)
engine_2 = create_engine("mysql+pymysql://root:Zzl33818!@111.198.58.126:33060/valuefrontier", echo=False)
# ClickHouse client factory
def get_clickhouse_client():
return Cclient(
host='111.198.58.126',
port=18778,
user='default',
password='Zzl5588161!',
database='stock'
)

504
app/models.py Normal file
View File

@@ -0,0 +1,504 @@
from app import db
from datetime import datetime
import pytz
import json
def beijing_now():
"""获取北京时间"""
tz = pytz.timezone('Asia/Shanghai')
return datetime.now(tz)
class Post(db.Model):
"""帖子模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# 内容
title = db.Column(db.String(200)) # 标题(可选)
content = db.Column(db.Text, nullable=False) # 内容
content_type = db.Column(db.String(20), default='text') # 内容类型:text/rich_text/link
# 时间
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# 统计
likes_count = db.Column(db.Integer, default=0)
comments_count = db.Column(db.Integer, default=0)
view_count = db.Column(db.Integer, default=0)
# 状态
status = db.Column(db.String(20), default='active') # active/hidden/deleted
is_top = db.Column(db.Boolean, default=False) # 是否置顶
# 关系
user = db.relationship('User', backref='posts')
likes = db.relationship('PostLike', backref='post', lazy='dynamic')
comments = db.relationship('Comment', backref='post', lazy='dynamic')
class User(db.Model):
"""用户模型"""
id = db.Column(db.Integer, primary_key=True)
# 基础账号信息(注册时必填)
username = db.Column(db.String(80), unique=True, nullable=False) # 用户名
email = db.Column(db.String(120), unique=True, nullable=False) # 邮箱
password_hash = db.Column(db.String(128), nullable=False) # 密码哈希
email_confirmed = db.Column(db.Boolean, default=False) # 邮箱是否验证
# 账号状态
created_at = db.Column(db.DateTime, default=beijing_now) # 注册时间
last_seen = db.Column(db.DateTime, default=beijing_now) # 最后活跃时间
status = db.Column(db.String(20), default='active') # 账号状态 active/banned/deleted
# 个人资料(可选,后续在个人中心完善)
nickname = db.Column(db.String(30)) # 社区昵称
avatar_url = db.Column(db.String(200)) # 头像URL
banner_url = db.Column(db.String(200)) # 个人主页背景图
bio = db.Column(db.String(200)) # 个人简介
gender = db.Column(db.String(10)) # 性别
birth_date = db.Column(db.Date) # 生日
location = db.Column(db.String(100)) # 所在地
# 联系方式(可选)
phone = db.Column(db.String(20)) # 手机号
wechat_id = db.Column(db.String(80)) # 微信号
# 实名认证信息(可选)
real_name = db.Column(db.String(30)) # 真实姓名
id_number = db.Column(db.String(18)) # 身份证号(加密存储)
is_verified = db.Column(db.Boolean, default=False) # 是否实名认证
verify_time = db.Column(db.DateTime) # 实名认证时间
# 投资相关信息(可选)
trading_experience = db.Column(db.Integer) # 炒股年限
investment_style = db.Column(db.String(50)) # 投资风格
risk_preference = db.Column(db.String(20)) # 风险偏好
investment_amount = db.Column(db.String(20)) # 投资规模
preferred_markets = db.Column(db.String(200), default='[]') # 偏好市场 JSON
# 社区信息(系统自动更新)
user_level = db.Column(db.Integer, default=1) # 用户等级
reputation_score = db.Column(db.Integer, default=0) # 信用积分
contribution_point = db.Column(db.Integer, default=0) # 贡献点数
post_count = db.Column(db.Integer, default=0) # 发帖数
comment_count = db.Column(db.Integer, default=0) # 评论数
follower_count = db.Column(db.Integer, default=0) # 粉丝数
following_count = db.Column(db.Integer, default=0) # 关注数
# 创作者信息(可选)
is_creator = db.Column(db.Boolean, default=False) # 是否创作者
creator_type = db.Column(db.String(20)) # 创作者类型
creator_tags = db.Column(db.String(200), default='[]') # 创作者标签 JSON
# 系统设置
email_notifications = db.Column(db.Boolean, default=True) # 邮件通知
sms_notifications = db.Column(db.Boolean, default=False) # 短信通知
wechat_notifications = db.Column(db.Boolean, default=False) # 微信通知
notification_preferences = db.Column(db.String(500), default='{}') # 通知偏好 JSON
privacy_level = db.Column(db.String(20), default='public') # 隐私级别
theme_preference = db.Column(db.String(20), default='light') # 主题偏好
blocked_keywords = db.Column(db.String(500), default='[]') # 屏蔽关键词 JSON
# 手机号验证
phone_confirmed = db.Column(db.Boolean, default=False) # 手机是否验证
phone_confirm_time = db.Column(db.DateTime) # 手机验证时间
def __init__(self, username, email=None, password=None, phone=None):
self.username = username
if email:
self.email = email
if password:
self.set_password(password)
if phone:
self.phone = phone
def set_password(self, password):
from werkzeug.security import generate_password_hash
self.password_hash = generate_password_hash(password)
def check_password(self, password):
from werkzeug.security import check_password_hash
return check_password_hash(self.password_hash, password)
def update_last_seen(self):
self.last_seen = beijing_now()
db.session.commit()
def get_preferred_markets(self):
try:
return json.loads(self.preferred_markets)
except (json.JSONDecodeError, TypeError):
return []
def get_blocked_keywords(self):
try:
return json.loads(self.blocked_keywords)
except (json.JSONDecodeError, TypeError):
return []
def get_notification_preferences(self):
try:
return json.loads(self.notification_preferences)
except (json.JSONDecodeError, TypeError):
return {}
def get_creator_tags(self):
try:
return json.loads(self.creator_tags)
except (json.JSONDecodeError, TypeError):
return []
def set_preferred_markets(self, markets):
self.preferred_markets = json.dumps(markets)
def set_blocked_keywords(self, keywords):
self.blocked_keywords = json.dumps(keywords)
def set_notification_preferences(self, preferences):
self.notification_preferences = json.dumps(preferences)
def set_creator_tags(self, tags):
self.creator_tags = json.dumps(tags)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'nickname': self.nickname,
'avatar_url': self.avatar_url,
'bio': self.bio,
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_seen': self.last_seen.isoformat() if self.last_seen else None,
'status': self.status,
'user_level': self.user_level,
'reputation_score': self.reputation_score,
'post_count': self.post_count,
'follower_count': self.follower_count,
'following_count': self.following_count
}
def __repr__(self):
return f'<User {self.username}>'
class Comment(db.Model):
"""评论"""
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
content = db.Column(db.Text, nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('comment.id')) # 父评论ID,用于回复
created_at = db.Column(db.DateTime, default=beijing_now)
status = db.Column(db.String(20), default='active')
user = db.relationship('User', backref='comments')
replies = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]))
class CommentLike(db.Model):
"""评论点赞记录基于session_id以兼容匿名点赞"""
__tablename__ = 'comment_like'
id = db.Column(db.Integer, primary_key=True)
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
session_id = db.Column(db.String(100), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
__table_args__ = (db.UniqueConstraint('comment_id', 'session_id'),)
class EventFollow(db.Model):
"""事件关注"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
user = db.relationship('User', backref='event_follows')
__table_args__ = (db.UniqueConstraint('user_id', 'event_id'),)
class PostLike(db.Model):
"""帖子点赞"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
created_at = db.Column(db.DateTime, default=beijing_now)
user = db.relationship('User', backref='post_likes')
__table_args__ = (db.UniqueConstraint('user_id', 'post_id'),)
class Event(db.Model):
"""事件模型"""
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
# 事件类型与状态
event_type = db.Column(db.String(50))
status = db.Column(db.String(20), default='active')
# 时间相关
start_time = db.Column(db.DateTime, default=beijing_now)
end_time = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now)
# 热度与统计
hot_score = db.Column(db.Float, default=0)
view_count = db.Column(db.Integer, default=0)
trending_score = db.Column(db.Float, default=0)
post_count = db.Column(db.Integer, default=0)
follower_count = db.Column(db.Integer, default=0)
# 关联信息
related_industries = db.Column(db.JSON)
keywords = db.Column(db.JSON)
files = db.Column(db.JSON)
importance = db.Column(db.String(20))
related_avg_chg = db.Column(db.Float, default=0)
related_max_chg = db.Column(db.Float, default=0)
related_week_chg = db.Column(db.Float, default=0)
# 新增字段
invest_score = db.Column(db.Integer) # 超预期得分
expectation_surprise_score = db.Column(db.Integer)
# 创建者信息
creator_id = db.Column(db.Integer, db.ForeignKey('user.id'))
creator = db.relationship('User', backref='created_events')
# 关系
posts = db.relationship('Post', backref='event', lazy='dynamic')
followers = db.relationship('EventFollow', backref='event', lazy='dynamic')
related_stocks = db.relationship('RelatedStock', backref='event', lazy='dynamic')
historical_events = db.relationship('HistoricalEvent', backref='event', lazy='dynamic')
related_data = db.relationship('RelatedData', backref='event', lazy='dynamic')
related_concepts = db.relationship('RelatedConcepts', backref='event', lazy='dynamic')
@property
def keywords_list(self):
if isinstance(self.keywords, list):
return self.keywords
elif isinstance(self.keywords, str):
try:
return json.loads(self.keywords)
except (json.JSONDecodeError, TypeError):
return []
return []
def set_keywords(self, keywords):
if isinstance(keywords, list):
self.keywords = keywords
elif isinstance(keywords, str):
try:
self.keywords = json.loads(keywords)
except json.JSONDecodeError:
self.keywords = [keywords]
else:
self.keywords = []
class RelatedStock(db.Model):
"""相关标的模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
stock_code = db.Column(db.String(20)) # 股票代码
stock_name = db.Column(db.String(100)) # 股票名称
sector = db.Column(db.String(100)) # 关联类型
relation_desc = db.Column(db.String(1024)) # 关联原因描述
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
correlation = db.Column(db.Float())
momentum = db.Column(db.String(1024)) #动量
class RelatedData(db.Model):
"""关联数据模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
title = db.Column(db.String(200)) # 数据标题
data_type = db.Column(db.String(50)) # 数据类型
data_content = db.Column(db.JSON) # 数据内容(JSON格式)
description = db.Column(db.Text) # 数据描述
created_at = db.Column(db.DateTime, default=beijing_now)
class RelatedConcepts(db.Model):
"""关联数据模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
concept_code = db.Column(db.String(20)) # 数据标题
concept = db.Column(db.String(100)) # 数据类型
reason = db.Column(db.Text) # 数据描述
image_paths = db.Column(db.JSON) # 数据内容(JSON格式)
created_at = db.Column(db.DateTime, default=beijing_now)
@property
def image_paths_list(self):
if isinstance(self.image_paths, list):
return self.image_paths
elif isinstance(self.image_paths, str):
try:
return json.loads(self.image_paths)
except (json.JSONDecodeError, TypeError):
return []
return []
def set_image_paths(self, image_paths):
if isinstance(image_paths, list):
self.image_paths = image_paths
elif isinstance(image_paths, str):
try:
self.image_paths = json.loads(image_paths)
except json.JSONDecodeError:
self.image_paths = [image_paths]
else:
self.image_paths = []
def get_first_image_path(self):
paths = self.image_paths_list
return paths[0] if paths else None
class EventHotHistory(db.Model):
"""事件热度历史记录"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
score = db.Column(db.Float) # 总分
interaction_score = db.Column(db.Float) # 互动分数
follow_score = db.Column(db.Float) # 关注度分数
view_score = db.Column(db.Float) # 浏览量分数
recent_activity_score = db.Column(db.Float) # 最近活跃度分数
time_decay = db.Column(db.Float) # 时间衰减因子
created_at = db.Column(db.DateTime, default=beijing_now)
event = db.relationship('Event', backref='hot_history')
class EventTransmissionNode(db.Model):
"""事件传导节点模型"""
__tablename__ = 'event_transmission_nodes'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
node_type = db.Column(db.Enum('company', 'industry', 'policy', 'technology',
'market', 'event', 'other'), nullable=False)
node_name = db.Column(db.String(200), nullable=False)
node_description = db.Column(db.Text)
importance_score = db.Column(db.Integer, default=50)
stock_code = db.Column(db.String(20))
is_main_event = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# Relationships
event = db.relationship('Event', backref='transmission_nodes')
outgoing_edges = db.relationship('EventTransmissionEdge',
foreign_keys='EventTransmissionEdge.from_node_id',
backref='from_node', cascade='all, delete-orphan')
incoming_edges = db.relationship('EventTransmissionEdge',
foreign_keys='EventTransmissionEdge.to_node_id',
backref='to_node', cascade='all, delete-orphan')
__table_args__ = (
db.Index('idx_event_node_type', 'event_id', 'node_type'),
db.Index('idx_node_name', 'node_name'),
)
class EventTransmissionEdge(db.Model):
"""事件传导边模型"""
__tablename__ = 'event_transmission_edges'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
from_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
to_node_id = db.Column(db.Integer, db.ForeignKey('event_transmission_nodes.id'), nullable=False)
transmission_type = db.Column(db.Enum('supply_chain', 'competition', 'policy',
'technology', 'capital_flow', 'expectation',
'cyclic_effect', 'other'), nullable=False)
transmission_mechanism = db.Column(db.Text)
direction = db.Column(db.Enum('positive', 'negative', 'neutral', 'mixed'), default='neutral')
strength = db.Column(db.Integer, default=50)
impact = db.Column(db.Text)
is_circular = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# Relationship
event = db.relationship('Event', backref='transmission_edges')
__table_args__ = (
db.Index('idx_event_edge_type', 'event_id', 'transmission_type'),
db.Index('idx_from_to_nodes', 'from_node_id', 'to_node_id'),
)
class EventSankeyFlow(db.Model):
"""事件桑基流模型"""
__tablename__ = 'event_sankey_flows'
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=False)
# 流的基本信息
source_node = db.Column(db.String(200), nullable=False)
source_type = db.Column(db.Enum('event', 'policy', 'technology', 'industry',
'company', 'product'), nullable=False)
source_level = db.Column(db.Integer, nullable=False, default=0)
target_node = db.Column(db.String(200), nullable=False)
target_type = db.Column(db.Enum('policy', 'technology', 'industry',
'company', 'product'), nullable=False)
target_level = db.Column(db.Integer, nullable=False, default=1)
# 流量信息
flow_value = db.Column(db.Numeric(10, 2), nullable=False)
flow_ratio = db.Column(db.Numeric(5, 4), nullable=False)
# 传导机制
transmission_path = db.Column(db.String(500))
impact_description = db.Column(db.Text)
evidence_strength = db.Column(db.Integer, default=50)
# 时间戳
created_at = db.Column(db.DateTime, default=beijing_now)
updated_at = db.Column(db.DateTime, default=beijing_now, onupdate=beijing_now)
# 关系
event = db.relationship('Event', backref='sankey_flows')
__table_args__ = (
db.Index('idx_event_flow', 'event_id'),
db.Index('idx_source_target', 'source_node', 'target_node'),
)
class HistoricalEvent(db.Model):
"""历史事件模型"""
id = db.Column(db.Integer, primary_key=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
title = db.Column(db.String(200))
content = db.Column(db.Text)
event_date = db.Column(db.DateTime)
relevance = db.Column(db.Integer) # 相关性
importance = db.Column(db.Integer) # 重要程度
related_stock = db.Column(db.JSON) # 保留JSON字段
created_at = db.Column(db.DateTime, default=beijing_now)
# 新增关系
stocks = db.relationship('HistoricalEventStock', backref='historical_event', lazy='dynamic',
cascade='all, delete-orphan')
class HistoricalEventStock(db.Model):
"""历史事件相关股票模型"""
__tablename__ = 'historical_event_stocks'
id = db.Column(db.Integer, primary_key=True)
historical_event_id = db.Column(db.Integer, db.ForeignKey('historical_event.id'), nullable=False)
stock_code = db.Column(db.String(20), nullable=False)
stock_name = db.Column(db.String(50))
relation_desc = db.Column(db.Text)
correlation = db.Column(db.Float, default=0.5)
sector = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=beijing_now)
__table_args__ = (
db.Index('idx_historical_event_stock', 'historical_event_id', 'stock_code'),
)

1
app/routes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# 路由包初始化文件

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

121
app/routes/calendar.py Normal file
View File

@@ -0,0 +1,121 @@
from flask import Blueprint, request, jsonify
from datetime import datetime, timedelta
import json
bp = Blueprint('calendar', __name__, url_prefix='/api/v1/calendar')
@bp.route('/event-counts', methods=['GET'])
def get_event_counts():
"""获取事件数量统计"""
try:
year = request.args.get('year', '2027')
month = request.args.get('month', '10')
# 模拟事件数量数据
event_counts = []
for day in range(1, 32):
count = (day % 7) + 1 # 模拟每天1-7个事件
event_counts.append({
'date': f'{year}-{month.zfill(2)}-{day:02d}',
'count': count
})
return jsonify({
'success': True,
'data': event_counts
})
except Exception as e:
print(f"Error getting event counts: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/events', methods=['GET'])
def get_calendar_events():
"""获取日历事件"""
try:
year = request.args.get('year', '2027')
month = request.args.get('month', '10')
event_type = request.args.get('type', 'all')
# 模拟日历事件数据
events = []
for day in range(1, 32):
for i in range((day % 7) + 1):
event = {
'id': f'{year}{month.zfill(2)}{day:02d}{i}',
'title': f'事件{day}-{i+1}',
'date': f'{year}-{month.zfill(2)}-{day:02d}',
'type': ['政策', '技术', '产业', '公司'][i % 4],
'importance': ['', '', ''][i % 3],
'status': 'active'
}
events.append(event)
# 根据类型过滤
if event_type != 'all':
events = [e for e in events if e['type'] == event_type]
return jsonify({
'success': True,
'data': events
})
except Exception as e:
print(f"Error getting calendar events: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/events/<int:event_id>', methods=['GET'])
def get_calendar_event_detail(event_id):
"""获取日历事件详情"""
try:
# 模拟事件详情
event_detail = {
'id': event_id,
'title': f'事件{event_id}详情',
'description': f'这是事件{event_id}的详细描述',
'date': '2027-10-15',
'type': '政策',
'importance': '',
'status': 'active',
'related_stocks': [
{'code': '000001', 'name': '股票A'},
{'code': '000002', 'name': '股票B'}
],
'keywords': ['政策', '改革', '创新'],
'files': [
{'name': '报告.pdf', 'url': '/files/report.pdf'},
{'name': '数据.xlsx', 'url': '/files/data.xlsx'}
]
}
return jsonify({
'success': True,
'data': event_detail
})
except Exception as e:
print(f"Error getting calendar event detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_event_class(count):
"""根据事件数量获取CSS类"""
if count == 0:
return 'no-events'
elif count <= 3:
return 'few-events'
elif count <= 6:
return 'medium-events'
else:
return 'many-events'
def parse_json_field(field_value):
"""解析JSON字段"""
if isinstance(field_value, str):
try:
return json.loads(field_value)
except (json.JSONDecodeError, TypeError):
return []
elif isinstance(field_value, (list, dict)):
return field_value
else:
return []

385
app/routes/events.py Normal file
View File

@@ -0,0 +1,385 @@
from flask import Blueprint, request, jsonify
from app import db
from app.models import Event, RelatedStock, RelatedConcepts, HistoricalEvent, EventTransmissionNode, EventTransmissionEdge, EventSankeyFlow
from datetime import datetime
import json
bp = Blueprint('events', __name__, url_prefix='/api/events')
@bp.route('/<int:event_id>', methods=['GET'])
def get_event_detail(event_id):
"""获取事件详情"""
try:
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
# 获取相关股票
related_stocks = RelatedStock.query.filter_by(event_id=event_id).all()
stocks_data = []
for stock in related_stocks:
stocks_data.append({
'id': stock.id,
'stock_code': stock.stock_code,
'stock_name': stock.stock_name,
'sector': stock.sector,
'relation_desc': stock.relation_desc,
'correlation': stock.correlation,
'momentum': stock.momentum,
'created_at': stock.created_at.isoformat() if stock.created_at else None
})
# 获取相关概念
related_concepts = RelatedConcepts.query.filter_by(event_id=event_id).all()
concepts_data = []
for concept in related_concepts:
concepts_data.append({
'id': concept.id,
'concept_code': concept.concept_code,
'concept': concept.concept,
'reason': concept.reason,
'image_paths': concept.image_paths_list,
'created_at': concept.created_at.isoformat() if concept.created_at else None
})
event_data = {
'id': event.id,
'title': event.title,
'description': event.description,
'event_type': event.event_type,
'status': event.status,
'start_time': event.start_time.isoformat() if event.start_time else None,
'end_time': event.end_time.isoformat() if event.end_time else None,
'created_at': event.created_at.isoformat() if event.created_at else None,
'updated_at': event.updated_at.isoformat() if event.updated_at else None,
'hot_score': event.hot_score,
'view_count': event.view_count,
'trending_score': event.trending_score,
'post_count': event.post_count,
'follower_count': event.follower_count,
'related_industries': event.related_industries,
'keywords': event.keywords_list,
'files': event.files,
'importance': event.importance,
'related_avg_chg': event.related_avg_chg,
'related_max_chg': event.related_max_chg,
'related_week_chg': event.related_week_chg,
'invest_score': event.invest_score,
'expectation_surprise_score': event.expectation_surprise_score,
'related_stocks': stocks_data,
'related_concepts': concepts_data
}
return jsonify({
'success': True,
'data': event_data
})
except Exception as e:
print(f"Error getting event detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/stocks', methods=['GET'])
def get_related_stocks(event_id):
"""获取事件相关股票"""
try:
stocks = RelatedStock.query.filter_by(event_id=event_id).all()
stocks_data = []
for stock in stocks:
stocks_data.append({
'id': stock.id,
'stock_code': stock.stock_code,
'stock_name': stock.stock_name,
'sector': stock.sector,
'relation_desc': stock.relation_desc,
'correlation': stock.correlation,
'momentum': stock.momentum,
'created_at': stock.created_at.isoformat() if stock.created_at else None
})
return jsonify({
'success': True,
'data': stocks_data
})
except Exception as e:
print(f"Error getting related stocks: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/stocks', methods=['POST'])
def add_related_stock(event_id):
"""添加相关股票"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '请提供数据'}), 400
# 检查事件是否存在
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
# 创建新的相关股票记录
new_stock = RelatedStock(
event_id=event_id,
stock_code=data['stock_code'],
stock_name=data.get('stock_name', ''),
sector=data.get('sector', ''),
relation_desc=data['relation_desc'],
correlation=data.get('correlation', 0.5),
momentum=data.get('momentum', '')
)
db.session.add(new_stock)
db.session.commit()
return jsonify({
'success': True,
'message': '相关股票添加成功',
'data': {
'id': new_stock.id,
'stock_code': new_stock.stock_code,
'stock_name': new_stock.stock_name,
'sector': new_stock.sector,
'relation_desc': new_stock.relation_desc,
'correlation': new_stock.correlation,
'momentum': new_stock.momentum
}
})
except Exception as e:
db.session.rollback()
print(f"Error adding related stock: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/stocks/<int:stock_id>', methods=['DELETE'])
def delete_related_stock(stock_id):
"""删除相关股票"""
try:
stock = RelatedStock.query.get(stock_id)
if not stock:
return jsonify({'success': False, 'error': '相关股票不存在'}), 404
db.session.delete(stock)
db.session.commit()
return jsonify({
'success': True,
'message': '相关股票删除成功'
})
except Exception as e:
db.session.rollback()
print(f"Error deleting related stock: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/concepts', methods=['GET'])
def get_related_concepts(event_id):
"""获取事件相关概念"""
try:
concepts = RelatedConcepts.query.filter_by(event_id=event_id).all()
concepts_data = []
for concept in concepts:
concepts_data.append({
'id': concept.id,
'concept_code': concept.concept_code,
'concept': concept.concept,
'reason': concept.reason,
'image_paths': concept.image_paths_list,
'created_at': concept.created_at.isoformat() if concept.created_at else None
})
return jsonify({
'success': True,
'data': concepts_data
})
except Exception as e:
print(f"Error getting related concepts: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/historical', methods=['GET'])
def get_historical_events(event_id):
"""获取历史事件"""
try:
historical_events = HistoricalEvent.query.filter_by(event_id=event_id).all()
events_data = []
for event in historical_events:
events_data.append({
'id': event.id,
'title': event.title,
'content': event.content,
'event_date': event.event_date.isoformat() if event.event_date else None,
'relevance': event.relevance,
'importance': event.importance,
'related_stock': event.related_stock,
'created_at': event.created_at.isoformat() if event.created_at else None
})
return jsonify({
'success': True,
'data': events_data
})
except Exception as e:
print(f"Error getting historical events: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/expectation-score', methods=['GET'])
def get_expectation_score(event_id):
"""获取超预期得分"""
try:
event = Event.query.get(event_id)
if not event:
return jsonify({'success': False, 'error': '事件不存在'}), 404
return jsonify({
'success': True,
'data': {
'invest_score': event.invest_score,
'expectation_surprise_score': event.expectation_surprise_score
}
})
except Exception as e:
print(f"Error getting expectation score: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/follow', methods=['POST'])
def toggle_event_follow(event_id):
"""关注/取消关注事件"""
try:
# 这里需要用户认证,暂时返回成功
return jsonify({
'success': True,
'message': '关注状态更新成功'
})
except Exception as e:
print(f"Error toggling event follow: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/transmission', methods=['GET'])
def get_transmission_chain(event_id):
"""获取事件传导链"""
try:
# 获取传导节点
nodes = EventTransmissionNode.query.filter_by(event_id=event_id).all()
nodes_data = []
for node in nodes:
nodes_data.append({
'id': node.id,
'node_type': node.node_type,
'node_name': node.node_name,
'node_description': node.node_description,
'importance_score': node.importance_score,
'stock_code': node.stock_code,
'is_main_event': node.is_main_event
})
# 获取传导边
edges = EventTransmissionEdge.query.filter_by(event_id=event_id).all()
edges_data = []
for edge in edges:
edges_data.append({
'id': edge.id,
'from_node_id': edge.from_node_id,
'to_node_id': edge.to_node_id,
'transmission_type': edge.transmission_type,
'transmission_mechanism': edge.transmission_mechanism,
'direction': edge.direction,
'strength': edge.strength,
'impact': edge.impact,
'is_circular': edge.is_circular
})
return jsonify({
'success': True,
'data': {
'nodes': nodes_data,
'edges': edges_data
}
})
except Exception as e:
print(f"Error getting transmission chain: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/sankey-data')
def get_event_sankey_data(event_id):
"""获取事件桑基图数据"""
try:
flows = EventSankeyFlow.query.filter_by(event_id=event_id).all()
flows_data = []
for flow in flows:
flows_data.append({
'id': flow.id,
'source_node': flow.source_node,
'source_type': flow.source_type,
'source_level': flow.source_level,
'target_node': flow.target_node,
'target_type': flow.target_type,
'target_level': flow.target_level,
'flow_value': float(flow.flow_value),
'flow_ratio': float(flow.flow_ratio),
'transmission_path': flow.transmission_path,
'impact_description': flow.impact_description,
'evidence_strength': flow.evidence_strength
})
return jsonify({
'success': True,
'data': flows_data
})
except Exception as e:
print(f"Error getting sankey data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/chain-analysis')
def get_event_chain_analysis(event_id):
"""获取事件链分析"""
try:
# 这里可以添加更复杂的链分析逻辑
return jsonify({
'success': True,
'data': {
'event_id': event_id,
'analysis': '链分析数据'
}
})
except Exception as e:
print(f"Error getting chain analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<int:event_id>/chain-node/<int:node_id>', methods=['GET'])
def get_chain_node_detail(event_id, node_id):
"""获取链节点详情"""
try:
node = EventTransmissionNode.query.filter_by(
event_id=event_id,
id=node_id
).first()
if not node:
return jsonify({'success': False, 'error': '节点不存在'}), 404
return jsonify({
'success': True,
'data': {
'id': node.id,
'node_type': node.node_type,
'node_name': node.node_name,
'node_description': node.node_description,
'importance_score': node.importance_score,
'stock_code': node.stock_code,
'is_main_event': node.is_main_event
}
})
except Exception as e:
print(f"Error getting chain node detail: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

511
app/routes/industries.py Normal file
View File

@@ -0,0 +1,511 @@
from flask import Blueprint, request, jsonify
import json
bp = Blueprint('industries', __name__, url_prefix='/api')
@bp.route('/classifications', methods=['GET'])
def get_classifications():
"""获取行业分类"""
try:
# 模拟行业分类数据
classifications = [
{
'id': 1,
'name': '申万一级行业',
'description': '申万一级行业分类标准',
'levels': [
{'id': 1, 'name': '农林牧渔'},
{'id': 2, 'name': '采掘'},
{'id': 3, 'name': '化工'},
{'id': 4, 'name': '钢铁'},
{'id': 5, 'name': '有色金属'},
{'id': 6, 'name': '建筑材料'},
{'id': 7, 'name': '建筑装饰'},
{'id': 8, 'name': '电气设备'},
{'id': 9, 'name': '国防军工'},
{'id': 10, 'name': '汽车'},
{'id': 11, 'name': '家用电器'},
{'id': 12, 'name': '纺织服装'},
{'id': 13, 'name': '轻工制造'},
{'id': 14, 'name': '医药生物'},
{'id': 15, 'name': '公用事业'},
{'id': 16, 'name': '交通运输'},
{'id': 17, 'name': '房地产'},
{'id': 18, 'name': '商业贸易'},
{'id': 19, 'name': '休闲服务'},
{'id': 20, 'name': '银行'},
{'id': 21, 'name': '非银金融'},
{'id': 22, 'name': '综合'},
{'id': 23, 'name': '计算机'},
{'id': 24, 'name': '传媒'},
{'id': 25, 'name': '通信'},
{'id': 26, 'name': '电子'},
{'id': 27, 'name': '机械设备'},
{'id': 28, 'name': '食品饮料'}
]
}
]
return jsonify({
'success': True,
'data': classifications
})
except Exception as e:
print(f"Error getting classifications: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/levels', methods=['GET'])
def get_industry_levels():
"""获取行业层级"""
try:
classification_id = request.args.get('classification_id', '1')
# 模拟行业层级数据
levels = [
{
'id': 1,
'name': '农林牧渔',
'code': '801010',
'description': '农业、林业、畜牧业、渔业',
'stock_count': 45,
'avg_change': 1.2,
'total_market_cap': 500000000000,
'sub_industries': [
{'id': 101, 'name': '种植业', 'stock_count': 20},
{'id': 102, 'name': '林业', 'stock_count': 8},
{'id': 103, 'name': '畜牧业', 'stock_count': 12},
{'id': 104, 'name': '渔业', 'stock_count': 5}
]
},
{
'id': 2,
'name': '采掘',
'code': '801020',
'description': '煤炭、石油、天然气、有色金属矿采选',
'stock_count': 38,
'avg_change': 0.8,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 201, 'name': '煤炭开采', 'stock_count': 15},
{'id': 202, 'name': '石油开采', 'stock_count': 8},
{'id': 203, 'name': '有色金属矿采选', 'stock_count': 15}
]
},
{
'id': 3,
'name': '化工',
'code': '801030',
'description': '化学原料、化学制品、化学纤维',
'stock_count': 156,
'avg_change': 1.5,
'total_market_cap': 1200000000000,
'sub_industries': [
{'id': 301, 'name': '化学原料', 'stock_count': 45},
{'id': 302, 'name': '化学制品', 'stock_count': 78},
{'id': 303, 'name': '化学纤维', 'stock_count': 33}
]
},
{
'id': 4,
'name': '钢铁',
'code': '801040',
'description': '钢铁冶炼、钢铁制品',
'stock_count': 32,
'avg_change': 0.6,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 401, 'name': '钢铁冶炼', 'stock_count': 18},
{'id': 402, 'name': '钢铁制品', 'stock_count': 14}
]
},
{
'id': 5,
'name': '有色金属',
'code': '801050',
'description': '有色金属冶炼、有色金属制品',
'stock_count': 67,
'avg_change': 1.8,
'total_market_cap': 900000000000,
'sub_industries': [
{'id': 501, 'name': '有色金属冶炼', 'stock_count': 35},
{'id': 502, 'name': '有色金属制品', 'stock_count': 32}
]
},
{
'id': 6,
'name': '建筑材料',
'code': '801060',
'description': '水泥、玻璃、陶瓷、其他建材',
'stock_count': 89,
'avg_change': 1.1,
'total_market_cap': 700000000000,
'sub_industries': [
{'id': 601, 'name': '水泥', 'stock_count': 25},
{'id': 602, 'name': '玻璃', 'stock_count': 18},
{'id': 603, 'name': '陶瓷', 'stock_count': 12},
{'id': 604, 'name': '其他建材', 'stock_count': 34}
]
},
{
'id': 7,
'name': '建筑装饰',
'code': '801070',
'description': '房屋建设、装修装饰、园林工程',
'stock_count': 45,
'avg_change': 0.9,
'total_market_cap': 400000000000,
'sub_industries': [
{'id': 701, 'name': '房屋建设', 'stock_count': 15},
{'id': 702, 'name': '装修装饰', 'stock_count': 20},
{'id': 703, 'name': '园林工程', 'stock_count': 10}
]
},
{
'id': 8,
'name': '电气设备',
'code': '801080',
'description': '电机、电气自动化设备、电源设备',
'stock_count': 134,
'avg_change': 2.1,
'total_market_cap': 1500000000000,
'sub_industries': [
{'id': 801, 'name': '电机', 'stock_count': 25},
{'id': 802, 'name': '电气自动化设备', 'stock_count': 45},
{'id': 803, 'name': '电源设备', 'stock_count': 64}
]
},
{
'id': 9,
'name': '国防军工',
'code': '801090',
'description': '航天装备、航空装备、地面兵装',
'stock_count': 28,
'avg_change': 1.6,
'total_market_cap': 300000000000,
'sub_industries': [
{'id': 901, 'name': '航天装备', 'stock_count': 8},
{'id': 902, 'name': '航空装备', 'stock_count': 12},
{'id': 903, 'name': '地面兵装', 'stock_count': 8}
]
},
{
'id': 10,
'name': '汽车',
'code': '801100',
'description': '汽车整车、汽车零部件',
'stock_count': 78,
'avg_change': 1.3,
'total_market_cap': 1100000000000,
'sub_industries': [
{'id': 1001, 'name': '汽车整车', 'stock_count': 25},
{'id': 1002, 'name': '汽车零部件', 'stock_count': 53}
]
},
{
'id': 11,
'name': '家用电器',
'code': '801110',
'description': '白色家电、小家电、家电零部件',
'stock_count': 56,
'avg_change': 1.0,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 1101, 'name': '白色家电', 'stock_count': 20},
{'id': 1102, 'name': '小家电', 'stock_count': 18},
{'id': 1103, 'name': '家电零部件', 'stock_count': 18}
]
},
{
'id': 12,
'name': '纺织服装',
'code': '801120',
'description': '纺织制造、服装家纺',
'stock_count': 67,
'avg_change': 0.7,
'total_market_cap': 500000000000,
'sub_industries': [
{'id': 1201, 'name': '纺织制造', 'stock_count': 35},
{'id': 1202, 'name': '服装家纺', 'stock_count': 32}
]
},
{
'id': 13,
'name': '轻工制造',
'code': '801130',
'description': '造纸、包装印刷、家用轻工',
'stock_count': 89,
'avg_change': 0.9,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 1301, 'name': '造纸', 'stock_count': 25},
{'id': 1302, 'name': '包装印刷', 'stock_count': 30},
{'id': 1303, 'name': '家用轻工', 'stock_count': 34}
]
},
{
'id': 14,
'name': '医药生物',
'code': '801140',
'description': '化学制药、中药、生物制品、医疗器械',
'stock_count': 234,
'avg_change': 1.9,
'total_market_cap': 2500000000000,
'sub_industries': [
{'id': 1401, 'name': '化学制药', 'stock_count': 78},
{'id': 1402, 'name': '中药', 'stock_count': 45},
{'id': 1403, 'name': '生物制品', 'stock_count': 56},
{'id': 1404, 'name': '医疗器械', 'stock_count': 55}
]
},
{
'id': 15,
'name': '公用事业',
'code': '801150',
'description': '电力、燃气、水务',
'stock_count': 78,
'avg_change': 0.5,
'total_market_cap': 900000000000,
'sub_industries': [
{'id': 1501, 'name': '电力', 'stock_count': 45},
{'id': 1502, 'name': '燃气', 'stock_count': 18},
{'id': 1503, 'name': '水务', 'stock_count': 15}
]
},
{
'id': 16,
'name': '交通运输',
'code': '801160',
'description': '港口、公路、铁路、航空',
'stock_count': 67,
'avg_change': 0.8,
'total_market_cap': 800000000000,
'sub_industries': [
{'id': 1601, 'name': '港口', 'stock_count': 15},
{'id': 1602, 'name': '公路', 'stock_count': 20},
{'id': 1603, 'name': '铁路', 'stock_count': 12},
{'id': 1604, 'name': '航空', 'stock_count': 20}
]
},
{
'id': 17,
'name': '房地产',
'code': '801170',
'description': '房地产开发、房地产服务',
'stock_count': 89,
'avg_change': 0.6,
'total_market_cap': 1200000000000,
'sub_industries': [
{'id': 1701, 'name': '房地产开发', 'stock_count': 65},
{'id': 1702, 'name': '房地产服务', 'stock_count': 24}
]
},
{
'id': 18,
'name': '商业贸易',
'code': '801180',
'description': '贸易、零售',
'stock_count': 78,
'avg_change': 0.7,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 1801, 'name': '贸易', 'stock_count': 35},
{'id': 1802, 'name': '零售', 'stock_count': 43}
]
},
{
'id': 19,
'name': '休闲服务',
'code': '801190',
'description': '景点、酒店、旅游综合',
'stock_count': 34,
'avg_change': 1.2,
'total_market_cap': 300000000000,
'sub_industries': [
{'id': 1901, 'name': '景点', 'stock_count': 12},
{'id': 1902, 'name': '酒店', 'stock_count': 15},
{'id': 1903, 'name': '旅游综合', 'stock_count': 7}
]
},
{
'id': 20,
'name': '银行',
'code': '801200',
'description': '银行',
'stock_count': 28,
'avg_change': 0.4,
'total_market_cap': 8000000000000,
'sub_industries': [
{'id': 2001, 'name': '银行', 'stock_count': 28}
]
},
{
'id': 21,
'name': '非银金融',
'code': '801210',
'description': '保险、证券、多元金融',
'stock_count': 45,
'avg_change': 0.8,
'total_market_cap': 2000000000000,
'sub_industries': [
{'id': 2101, 'name': '保险', 'stock_count': 8},
{'id': 2102, 'name': '证券', 'stock_count': 25},
{'id': 2103, 'name': '多元金融', 'stock_count': 12}
]
},
{
'id': 22,
'name': '综合',
'code': '801220',
'description': '综合',
'stock_count': 23,
'avg_change': 0.6,
'total_market_cap': 200000000000,
'sub_industries': [
{'id': 2201, 'name': '综合', 'stock_count': 23}
]
},
{
'id': 23,
'name': '计算机',
'code': '801230',
'description': '计算机设备、计算机应用',
'stock_count': 156,
'avg_change': 2.3,
'total_market_cap': 1800000000000,
'sub_industries': [
{'id': 2301, 'name': '计算机设备', 'stock_count': 45},
{'id': 2302, 'name': '计算机应用', 'stock_count': 111}
]
},
{
'id': 24,
'name': '传媒',
'code': '801240',
'description': '文化传媒、营销传播',
'stock_count': 78,
'avg_change': 1.4,
'total_market_cap': 700000000000,
'sub_industries': [
{'id': 2401, 'name': '文化传媒', 'stock_count': 45},
{'id': 2402, 'name': '营销传播', 'stock_count': 33}
]
},
{
'id': 25,
'name': '通信',
'code': '801250',
'description': '通信设备、通信运营',
'stock_count': 45,
'avg_change': 1.7,
'total_market_cap': 600000000000,
'sub_industries': [
{'id': 2501, 'name': '通信设备', 'stock_count': 30},
{'id': 2502, 'name': '通信运营', 'stock_count': 15}
]
},
{
'id': 26,
'name': '电子',
'code': '801260',
'description': '半导体、电子制造、光学光电子',
'stock_count': 178,
'avg_change': 2.0,
'total_market_cap': 2000000000000,
'sub_industries': [
{'id': 2601, 'name': '半导体', 'stock_count': 45},
{'id': 2602, 'name': '电子制造', 'stock_count': 78},
{'id': 2603, 'name': '光学光电子', 'stock_count': 55}
]
},
{
'id': 27,
'name': '机械设备',
'code': '801270',
'description': '通用机械、专用设备、仪器仪表',
'stock_count': 234,
'avg_change': 1.1,
'total_market_cap': 1500000000000,
'sub_industries': [
{'id': 2701, 'name': '通用机械', 'stock_count': 89},
{'id': 2702, 'name': '专用设备', 'stock_count': 98},
{'id': 2703, 'name': '仪器仪表', 'stock_count': 47}
]
},
{
'id': 28,
'name': '食品饮料',
'code': '801280',
'description': '食品加工、饮料制造',
'stock_count': 67,
'avg_change': 1.3,
'total_market_cap': 1000000000000,
'sub_industries': [
{'id': 2801, 'name': '食品加工', 'stock_count': 35},
{'id': 2802, 'name': '饮料制造', 'stock_count': 32}
]
}
]
return jsonify({
'success': True,
'data': levels
})
except Exception as e:
print(f"Error getting industry levels: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/info', methods=['GET'])
def get_industry_info():
"""获取行业信息"""
try:
industry_id = request.args.get('industry_id')
if not industry_id:
return jsonify({'success': False, 'error': '请提供行业ID'}), 400
# 模拟行业信息
industry_info = {
'id': industry_id,
'name': f'行业{industry_id}',
'code': f'801{industry_id.zfill(3)}',
'description': f'这是行业{industry_id}的详细描述',
'stock_count': 50,
'avg_change': 1.5,
'total_market_cap': 800000000000,
'pe_ratio': 15.6,
'pb_ratio': 2.3,
'roe': 8.5,
'top_stocks': [
{'code': '000001', 'name': '龙头股A', 'weight': 0.15},
{'code': '000002', 'name': '龙头股B', 'weight': 0.12},
{'code': '000003', 'name': '龙头股C', 'weight': 0.10}
],
'sub_industries': [
{'id': 1, 'name': '子行业A', 'stock_count': 20},
{'id': 2, 'name': '子行业B', 'stock_count': 18},
{'id': 3, 'name': '子行业C', 'stock_count': 12}
],
'performance': {
'daily': 1.5,
'weekly': 3.2,
'monthly': 8.5,
'quarterly': 12.3,
'yearly': 25.6
},
'trend': {
'direction': 'up',
'strength': 'medium',
'duration': '3 months'
}
}
return jsonify({
'success': True,
'data': industry_info
})
except Exception as e:
print(f"Error getting industry info: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

469
app/routes/limitanalyse.py Normal file
View File

@@ -0,0 +1,469 @@
from flask import Blueprint, request, jsonify
import pandas as pd
import json
from datetime import datetime
bp = Blueprint('limitanalyse', __name__, url_prefix='/api/limit-analyse')
@bp.route('/available-dates', methods=['GET'])
def get_available_dates():
"""获取可用日期列表"""
try:
# 模拟可用日期
dates = [
'2025-07-16',
'2025-07-15',
'2025-07-14',
'2025-07-11',
'2025-07-10'
]
return jsonify({
'success': True,
'data': dates
})
except Exception as e:
print(f"Error getting available dates: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def load_stock_data(datestr):
"""加载股票数据"""
try:
# 模拟股票数据
data = []
for i in range(100):
data.append({
'code': f'00000{i:03d}',
'name': f'股票{i}',
'price': 10.0 + i * 0.1,
'change': (i % 10 - 5) * 0.5,
'sector': f'板块{i % 5}',
'limit_type': '涨停' if i % 10 == 0 else '正常',
'volume': 1000000 + i * 50000,
'amount': 10000000 + i * 500000
})
return pd.DataFrame(data)
except Exception as e:
print(f"Error loading stock data: {e}")
return pd.DataFrame()
@bp.route('/data', methods=['GET'])
def get_analysis_data():
"""获取分析数据"""
try:
date = request.args.get('date', '2025-07-16')
# 加载数据
df = load_stock_data(date)
if df.empty:
return jsonify({'success': False, 'error': '数据加载失败'}), 500
# 统计信息
total_stocks = len(df)
limit_up_stocks = len(df[df['limit_type'] == '涨停'])
limit_down_stocks = len(df[df['limit_type'] == '跌停'])
# 板块统计
sector_stats = df.groupby('sector').agg({
'code': 'count',
'change': 'mean',
'volume': 'sum'
}).reset_index()
sector_data = []
for _, row in sector_stats.iterrows():
sector_data.append({
'sector': row['sector'],
'stock_count': int(row['code']),
'avg_change': round(row['change'], 2),
'total_volume': int(row['volume'])
})
return jsonify({
'success': True,
'data': {
'date': date,
'total_stocks': total_stocks,
'limit_up_stocks': limit_up_stocks,
'limit_down_stocks': limit_down_stocks,
'sector_stats': sector_data
}
})
except Exception as e:
print(f"Error getting analysis data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/sector-data', methods=['GET'])
def get_sector_data():
"""获取板块数据"""
try:
date = request.args.get('date', '2025-07-16')
# 加载数据
df = load_stock_data(date)
if df.empty:
return jsonify({'success': False, 'error': '数据加载失败'}), 500
# 板块统计
sector_stats = df.groupby('sector').agg({
'code': 'count',
'change': 'mean',
'volume': 'sum',
'amount': 'sum'
}).reset_index()
sector_data = []
for _, row in sector_stats.iterrows():
sector_data.append({
'sector': row['sector'],
'stock_count': int(row['code']),
'avg_change': round(row['change'], 2),
'total_volume': int(row['volume']),
'total_amount': int(row['amount'])
})
return jsonify({
'success': True,
'data': sector_data
})
except Exception as e:
print(f"Error getting sector data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/word-cloud', methods=['GET'])
def get_word_cloud_data():
"""获取词云数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟词云数据
word_data = [
{'word': '科技', 'value': 100},
{'word': '新能源', 'value': 85},
{'word': '医药', 'value': 70},
{'word': '消费', 'value': 65},
{'word': '金融', 'value': 50},
{'word': '地产', 'value': 45},
{'word': '制造', 'value': 40},
{'word': '农业', 'value': 35},
{'word': '传媒', 'value': 30},
{'word': '环保', 'value': 25}
]
return jsonify({
'success': True,
'data': word_data
})
except Exception as e:
print(f"Error getting word cloud data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/chart-data', methods=['GET'])
def get_chart_data():
"""获取图表数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟图表数据
chart_data = {
'limit_up_distribution': [
{'sector': '科技', 'count': 15},
{'sector': '新能源', 'count': 12},
{'sector': '医药', 'count': 10},
{'sector': '消费', 'count': 8},
{'sector': '金融', 'count': 6}
],
'sector_performance': [
{'sector': '科技', 'change': 2.5},
{'sector': '新能源', 'change': 1.8},
{'sector': '医药', 'change': 1.2},
{'sector': '消费', 'change': 0.8},
{'sector': '金融', 'change': 0.5}
]
}
return jsonify({
'success': True,
'data': chart_data
})
except Exception as e:
print(f"Error getting chart data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/stock-details', methods=['GET'])
def get_stock_details():
"""获取股票详情"""
try:
code = request.args.get('code')
date = request.args.get('date', '2025-07-16')
if not code:
return jsonify({'success': False, 'error': '请提供股票代码'}), 400
# 模拟股票详情
stock_detail = {
'code': code,
'name': f'股票{code}',
'price': 15.50,
'change': 2.5,
'sector': '科技',
'volume': 1500000,
'amount': 23250000,
'limit_type': '涨停',
'turnover_rate': 3.2,
'market_cap': 15500000000
}
return jsonify({
'success': True,
'data': stock_detail
})
except Exception as e:
print(f"Error getting stock details: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/sector-analysis', methods=['GET'])
def get_sector_analysis():
"""获取板块分析"""
try:
sector = request.args.get('sector')
date = request.args.get('date', '2025-07-16')
if not sector:
return jsonify({'success': False, 'error': '请提供板块名称'}), 400
# 模拟板块分析数据
sector_analysis = {
'sector': sector,
'stock_count': 25,
'avg_change': 1.8,
'limit_up_count': 8,
'limit_down_count': 2,
'total_volume': 50000000,
'total_amount': 750000000,
'top_stocks': [
{'code': '000001', 'name': '股票A', 'change': 10.0},
{'code': '000002', 'name': '股票B', 'change': 9.5},
{'code': '000003', 'name': '股票C', 'change': 8.8}
]
}
return jsonify({
'success': True,
'data': sector_analysis
})
except Exception as e:
print(f"Error getting sector analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/trend-analysis', methods=['GET'])
def get_trend_analysis():
"""获取趋势分析"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟趋势分析数据
trend_data = {
'limit_up_trend': [
{'date': '2025-07-10', 'count': 45},
{'date': '2025-07-11', 'count': 52},
{'date': '2025-07-14', 'count': 48},
{'date': '2025-07-15', 'count': 55},
{'date': '2025-07-16', 'count': 51}
],
'sector_trend': [
{'sector': '科技', 'trend': 'up'},
{'sector': '新能源', 'trend': 'up'},
{'sector': '医药', 'trend': 'stable'},
{'sector': '消费', 'trend': 'down'},
{'sector': '金融', 'trend': 'stable'}
]
}
return jsonify({
'success': True,
'data': trend_data
})
except Exception as e:
print(f"Error getting trend analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/heat-map', methods=['GET'])
def get_heat_map_data():
"""获取热力图数据"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟热力图数据
heat_map_data = []
sectors = ['科技', '新能源', '医药', '消费', '金融', '地产', '制造', '农业']
for i, sector in enumerate(sectors):
for j in range(8):
heat_map_data.append({
'sector': sector,
'metric': f'指标{j+1}',
'value': (i + j) % 10 + 1
})
return jsonify({
'success': True,
'data': heat_map_data
})
except Exception as e:
print(f"Error getting heat map data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/correlation-analysis', methods=['GET'])
def get_correlation_analysis():
"""获取相关性分析"""
try:
date = request.args.get('date', '2025-07-16')
# 模拟相关性分析数据
correlation_data = {
'sector_correlations': [
{'sector1': '科技', 'sector2': '新能源', 'correlation': 0.85},
{'sector1': '医药', 'sector2': '消费', 'correlation': 0.72},
{'sector1': '金融', 'sector2': '地产', 'correlation': 0.68},
{'sector1': '科技', 'sector2': '医药', 'correlation': 0.45},
{'sector1': '新能源', 'sector2': '制造', 'correlation': 0.78}
],
'stock_correlations': [
{'stock1': '000001', 'stock2': '000002', 'correlation': 0.92},
{'stock1': '000003', 'stock2': '000004', 'correlation': 0.88},
{'stock1': '000005', 'stock2': '000006', 'correlation': 0.76}
]
}
return jsonify({
'success': True,
'data': correlation_data
})
except Exception as e:
print(f"Error getting correlation analysis: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/export-data', methods=['POST'])
def export_data():
"""导出数据"""
try:
data = request.get_json()
date = data.get('date', '2025-07-16')
export_type = data.get('type', 'excel')
# 模拟导出
filename = f'limit_analyse_{date}.{export_type}'
return jsonify({
'success': True,
'message': '数据导出成功',
'data': {
'filename': filename,
'download_url': f'/downloads/{filename}'
}
})
except Exception as e:
print(f"Error exporting data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/high-position-stocks', methods=['GET'])
def get_high_position_stocks():
"""获取高位股统计数据"""
try:
date = request.args.get('date', datetime.now().strftime('%Y%m%d'))
# 模拟高位股数据 - 实际使用时需要连接真实的数据库
# 根据用户提供的表结构,查询连续涨停天数较多的股票
high_position_stocks = [
{
'stock_code': '000001',
'stock_name': '平安银行',
'price': 15.68,
'increase_rate': 10.02,
'limit_up_days': 5,
'continuous_limit_up': 3,
'industry': '银行',
'turnover_rate': 3.45,
'market_cap': 32000000000
},
{
'stock_code': '000002',
'stock_name': '万科A',
'price': 18.92,
'increase_rate': 9.98,
'limit_up_days': 4,
'continuous_limit_up': 2,
'industry': '房地产',
'turnover_rate': 5.67,
'market_cap': 21000000000
},
{
'stock_code': '600036',
'stock_name': '招商银行',
'price': 42.15,
'increase_rate': 8.45,
'limit_up_days': 6,
'continuous_limit_up': 4,
'industry': '银行',
'turnover_rate': 2.89,
'market_cap': 105000000000
},
{
'stock_code': '000858',
'stock_name': '五粮液',
'price': 168.50,
'increase_rate': 7.23,
'limit_up_days': 3,
'continuous_limit_up': 2,
'industry': '白酒',
'turnover_rate': 1.56,
'market_cap': 650000000000
},
{
'stock_code': '002415',
'stock_name': '海康威视',
'price': 35.68,
'increase_rate': 6.89,
'limit_up_days': 4,
'continuous_limit_up': 3,
'industry': '安防',
'turnover_rate': 4.12,
'market_cap': 33000000000
}
]
# 统计信息
total_count = len(high_position_stocks)
avg_continuous_days = sum(stock['continuous_limit_up'] for stock in high_position_stocks) / total_count if total_count > 0 else 0
# 按连续涨停天数排序
high_position_stocks.sort(key=lambda x: x['continuous_limit_up'], reverse=True)
return jsonify({
'success': True,
'data': {
'stocks': high_position_stocks,
'statistics': {
'total_count': total_count,
'avg_continuous_days': round(avg_continuous_days, 2),
'max_continuous_days': max([stock['continuous_limit_up'] for stock in high_position_stocks], default=0),
'industry_distribution': {}
}
}
})
except Exception as e:
print(f"Error getting high position stocks: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

241
app/routes/stocks.py Normal file
View File

@@ -0,0 +1,241 @@
from flask import Blueprint, request, jsonify
from app import db
from clickhouse_driver import Client
import pandas as pd
from datetime import datetime, timedelta
import pytz
bp = Blueprint('stocks', __name__, url_prefix='/api/stock')
def get_clickhouse_client():
"""获取ClickHouse客户端"""
return Client('localhost', port=9000, user='default', password='', database='default')
@bp.route('/quotes', methods=['GET', 'POST'])
def get_stock_quotes():
"""获取股票实时报价"""
try:
if request.method == 'GET':
# GET 请求从 URL 参数获取数据
codes = request.args.get('codes', '').split(',')
event_time_str = request.args.get('event_time')
else:
# POST 请求从 JSON 获取数据
codes = request.json.get('codes', [])
event_time_str = request.json.get('event_time')
if not codes:
return jsonify({'success': False, 'error': '请提供股票代码'}), 400
# 过滤空字符串
codes = [code.strip() for code in codes if code.strip()]
if not codes:
return jsonify({'success': False, 'error': '请提供有效的股票代码'}), 400
# 解析事件时间
event_time = None
if event_time_str:
try:
event_time = datetime.fromisoformat(event_time_str.replace('Z', '+00:00'))
except ValueError:
return jsonify({'success': False, 'error': '事件时间格式错误'}), 400
# 获取当前时间
now = datetime.now(pytz.timezone('Asia/Shanghai'))
# 如果提供了事件时间,使用事件时间;否则使用当前时间
target_time = event_time if event_time else now
# 获取交易日和交易时间
def get_trading_day_and_times(event_datetime):
"""获取交易日和交易时间列表"""
# 这里简化处理,实际应该查询交易日历
trading_day = event_datetime.strftime('%Y-%m-%d')
# 生成交易时间列表 (9:30-11:30, 13:00-15:00)
morning_times = [f"{trading_day} {hour:02d}:{minute:02d}"
for hour in range(9, 12)
for minute in range(0, 60, 1)
if not (hour == 9 and minute < 30) and not (hour == 11 and minute > 30)]
afternoon_times = [f"{trading_day} {hour:02d}:{minute:02d}"
for hour in range(13, 16)
for minute in range(0, 60, 1)]
return trading_day, morning_times + afternoon_times
trading_day, trading_times = get_trading_day_and_times(target_time)
# 模拟股票数据
results = {}
for code in codes:
# 这里应该从ClickHouse或其他数据源获取真实数据
# 现在使用模拟数据
import random
base_price = 10.0 + random.random() * 20.0
change = (random.random() - 0.5) * 2.0
results[code] = {
'price': round(base_price, 2),
'change': round(change, 2),
'name': f'股票{code}'
}
return jsonify({
'success': True,
'data': results
})
except Exception as e:
print(f"Error getting stock quotes: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/<stock_code>/kline')
def get_stock_kline(stock_code):
"""获取股票K线数据"""
try:
chart_type = request.args.get('type', 'daily')
event_time_str = request.args.get('event_time')
if not event_time_str:
return jsonify({'success': False, 'error': '请提供事件时间'}), 400
try:
event_datetime = datetime.fromisoformat(event_time_str.replace('Z', '+00:00'))
except ValueError:
return jsonify({'success': False, 'error': '事件时间格式错误'}), 400
# 获取股票名称(这里简化处理)
stock_name = f'股票{stock_code}'
if chart_type == 'daily':
return get_daily_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'minute':
return get_minute_kline(stock_code, event_datetime, stock_name)
elif chart_type == 'timeline':
return get_timeline_data(stock_code, event_datetime, stock_name)
else:
return jsonify({'error': f'Unsupported chart type: {chart_type}'}), 400
except Exception as e:
print(f"Error getting stock kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_daily_kline(stock_code, event_datetime, stock_name):
"""获取日K线数据"""
try:
# 模拟日K线数据
data = []
base_price = 10.0
for i in range(30):
date = (event_datetime - timedelta(days=30-i)).strftime('%Y-%m-%d')
open_price = base_price + (i * 0.1) + (i % 3 - 1) * 0.5
close_price = open_price + (i % 5 - 2) * 0.3
high_price = max(open_price, close_price) + 0.2
low_price = min(open_price, close_price) - 0.2
volume = 1000000 + i * 50000
data.append({
'date': date,
'open': round(open_price, 2),
'close': round(close_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting daily kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_minute_kline(stock_code, event_datetime, stock_name):
"""获取分钟K线数据"""
try:
# 模拟分钟K线数据
data = []
base_price = 10.0
trading_times = []
# 生成交易时间
for hour in range(9, 16):
if hour == 12:
continue
for minute in range(0, 60):
if (hour == 9 and minute < 30) or (hour == 11 and minute > 30):
continue
trading_times.append(f"{hour:02d}:{minute:02d}")
for i, time in enumerate(trading_times):
open_price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02
close_price = open_price + (i % 7 - 3) * 0.01
high_price = max(open_price, close_price) + 0.01
low_price = min(open_price, close_price) - 0.01
volume = 50000 + i * 1000
data.append({
'time': time,
'open': round(open_price, 2),
'close': round(close_price, 2),
'high': round(high_price, 2),
'low': round(low_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting minute kline: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def get_timeline_data(stock_code, event_datetime, stock_name):
"""获取分时图数据"""
try:
# 模拟分时图数据
data = []
base_price = 10.0
trading_times = []
# 生成交易时间
for hour in range(9, 16):
if hour == 12:
continue
for minute in range(0, 60):
if (hour == 9 and minute < 30) or (hour == 11 and minute > 30):
continue
trading_times.append(f"{hour:02d}:{minute:02d}")
for i, time in enumerate(trading_times):
price = base_price + (i * 0.01) + (i % 10 - 5) * 0.02
avg_price = price + (i % 5 - 2) * 0.01
volume = 50000 + i * 1000
data.append({
'time': time,
'price': round(price, 2),
'avg_price': round(avg_price, 2),
'volume': volume
})
return jsonify({
'code': stock_code,
'name': stock_name,
'trade_date': event_datetime.strftime('%Y-%m-%d'),
'data': data
})
except Exception as e:
print(f"Error getting timeline data: {e}")
return jsonify({'success': False, 'error': str(e)}), 500

7417
app_vx.py

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
node_modules/**/*
.expo/*
npm-debug.*
package-lock.json
yarn.lock
*.jks
*.p12
*.key
*.mobileprovision

View File

@@ -1 +0,0 @@
{}

View File

@@ -1,101 +0,0 @@
import React, { useCallback, useEffect, useState } from "react";
import * as SplashScreen from "expo-splash-screen";
import * as Font from "expo-font";
import { Asset } from "expo-asset";
import { Block, GalioProvider } from "galio-framework";
import { NavigationContainer } from "@react-navigation/native";
import { Image } from "react-native";
import { Provider } from "react-redux";
import { NativeBaseProvider } from "native-base";
// Keep the splash screen visible while we fetch resources
SplashScreen.preventAutoHideAsync();
// Before rendering any navigation stack
import { enableScreens } from "react-native-screens";
enableScreens();
import Screens from "./navigation/Screens";
import { Images, articles, argonTheme } from "./constants";
import store from "./src/store";
import nativeBaseTheme from "./src/theme";
import { AuthProvider } from "./src/contexts/AuthContext";
// cache app images
const assetImages = [
Images.Onboarding,
Images.LogoOnboarding,
Images.Logo,
Images.Pro,
Images.ArgonLogo,
Images.iOSLogo,
Images.androidLogo,
];
// cache product images
articles.map((article) => assetImages.push(article.image));
function cacheImages(images) {
return images.map((image) => {
if (typeof image === "string") {
return Image.prefetch(image);
} else {
return Asset.fromModule(image).downloadAsync();
}
});
}
export default function App() {
const [appIsReady, setAppIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
//Load Resources
await _loadResourcesAsync();
// Pre-load fonts, make any API calls you need to do here
await Font.loadAsync({
"open-sans-regular": require("./assets/font/OpenSans-Regular.ttf"),
"open-sans-light": require("./assets/font/OpenSans-Light.ttf"),
"open-sans-bold": require("./assets/font/OpenSans-Bold.ttf"),
});
} catch (e) {
console.warn(e);
} finally {
// Tell the application to render
setAppIsReady(true);
}
}
prepare();
}, []);
const _loadResourcesAsync = async () => {
return Promise.all([...cacheImages(assetImages)]);
};
const onLayoutRootView = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync();
}
}, [appIsReady]);
if (!appIsReady) {
return null;
}
return (
<Provider store={store}>
<NativeBaseProvider theme={nativeBaseTheme}>
<AuthProvider>
<NavigationContainer onReady={onLayoutRootView}>
<GalioProvider theme={argonTheme}>
<Block flex>
<Screens />
</Block>
</GalioProvider>
</NavigationContainer>
</AuthProvider>
</NativeBaseProvider>
</Provider>
);
}

View File

@@ -1,6 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLshiAgsJoiJegXNC
55cF2MHBnQCi2AaObrf/qgEavcmgCgYIKoZIzj0DAQehRANCAAQoIgTclBUyCDU2
gFaphqK1I4n1VAkEad144GMKxrdjwfAXbOenkDkUis/6LBEMoOI8tBTcwP1qlY7s
V7zdIhb4
-----END PRIVATE KEY-----

View File

@@ -1,220 +0,0 @@
# [Argon PRO React Native](https://creativetimofficial.github.io/argon-pro-react-native/docs/#) [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](https://twitter.com/intent/tweet?text=Start%20Your%20Development%20With%20A%20Badass%20React%20Native%20app%20inspired%20by%20Argon%20Design%20System.%0Ahttps%3A//demos.creative-tim.com/argon-pro-react-native/)
![version](https://img.shields.io/badge/version-1.6.0-blue.svg) [![GitHub issues open](https://img.shields.io/github/issues/creativetimofficial/ct-argon-pro-react-native.svg?style=flat)](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aopen+is%3Aissue) [![GitHub issues closed](https://img.shields.io/github/issues-closed-raw/creativetimofficial/ct-argon-pro-react-native.svg?maxAge=2592000)](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aissue+is%3Aclosed)
![Product Gif](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-pro-react-native/argp-rn-thumbnail.jpg)
Argon PRO React Native is a fully coded app template built over [Galio.io](https://galio.io/?ref=creativetim), [React Native](https://facebook.github.io/react-native/?ref=creativetim) and [Expo](https://expo.io/?ref=creativetim) to allow you to create powerful and beautiful e-commerce mobile applications. We have redesigned all the usual components in Galio to make it look like Argon's Design System, minimalistic and easy to use.
Start your development with a badass Design System for React Native inspired by Argon Design System. If you like Argon's Design System, you will love this react native app template! It features a huge number of components and screens built to fit together and look amazing.
### FULLY CODED COMPONENTS
Argon PRO React Native features over 200 variations of components like buttons, inputs, cards, navigations etc, giving you the freedom of choosing and combining. All components can take variations in colour, that you can easily modify inside our theme file.
You will save a lot of time going from prototyping to full-functional code, because all elements are implemented. We wanted the design process to be seamless, so switching from image to the real page is very easy to do.
### Components & Cards
Argon PRO React Native comes packed with a large number of components and cards. Putting together a mobile app has never been easier than matching together different components. From the profile screen to a settings screen, you can easily customise and build your screens. We have created multiple options for you to put together and customise into pixel perfect screens.
View [ all components/cards here](https://demos.creative-tim.com/argon-pro-react-native/index.html#cards).
### Example Screens
If you want to get inspiration or just show something directly to your clients, you can jump start your development with our pre-built example screens. From onboarding screens to profile or discover screens, you will be able to quickly set up the basic structure for your React Native mobile project.
View [all screens here](https://demos.creative-tim.com/argon-pro-react-native/index.html#screens).
Let us know your thoughts below. And good luck with development!
## Table of Contents
* [Versions](#versions)
* [Demo](#demo)
* [Quick Start](#quick-start)
* [Documentation](#documentation)
* [File Structure](#file-structure)
* [OS Support](#os-support)
* [Resources](#resources)
* [Reporting Issues](#reporting-issues)
* [Technical Support or Questions](#technical-support-or-questions)
* [Licensing](#licensing)
* [Useful Links](#useful-links)
## Versions
[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/html-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-design-system)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/vue-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/vue-argon-design-system)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/react-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-design-system-react)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/react-native-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-pro-react-native)[<img src="https://github.com/creativetimofficial/public-assets/blob/master/logos/angular-logo.jpg?raw=true" width="60" height="60" />](https://www.creative-tim.com/product/argon-dashboard-angular)
| HTML | React | Angular |
| --- | --- | --- |
| [![Argon Design System](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system/argon-design-system.jpg)](https://www.creative-tim.com/product/argon-design-system) | [![Argon Design System React](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system-react/argon-design-system-react.jpg)](https://www.creative-tim.com/product/argon-design-system-react) | [![Argon Design System Angular](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-design-system-angular/argon-design-system-angular.jpg)](https://www.creative-tim.com/product/argon-design-system-angular)
## Demo
| Home Screen | Profile Screen | Onboarding Screen | Register Screen |
| --- | --- | --- | --- |
| [![Home Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/home-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Profile Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/profile-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Onboarding Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-pro-react-native/onboarding_screen.PNG)](https://demos.creative-tim.com/argon-pro-react-native/) | [![Register Screen](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-react-native/register-screen.png)](https://demos.creative-tim.com/argon-pro-react-native/) |
- [Start page](https://demos.creative-tim.com/argon-pro-react-native)
- [How to install our product](https://demos.creative-tim.com/argon-pro-react-native/docs/#/install)
[View more](https://demos.creative-tim.com/argon-pro-react-native)
## Quick start
- Buy from [Creative Tim](https://www.creative-tim.com/product/argon-pro-react-native)
## Documentation
The documentation for the Argon PRO React Native is hosted at our [website](https://demos.creative-tim.com/argon-pro-react-native/docs/).
## File Structure
Within the download you'll find the following directories and files:
```
argon-pro-react-native/
├── App.js
├── CHANGELOG.md
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── README.md
├── app.json
├── assets
│ ├── font
│ ├── imgs
│ └── nucleo\ icons
├── babel.config.js
├── components
│ ├── Button.js
│ ├── Card.js
│ ├── DrawerItem.js
│ ├── Header.js
│ ├── Icon.js
│ ├── Input.js
│ ├── Notification.js
│ ├── Select.js
│ ├── Switch.js
│ ├── Tabs.js
│ └── index.js
├── constants
│ ├── Images.js
│ ├── Theme.js
│ ├── articles.js
│ ├── cart.js
│ ├── categories.js
│ ├── deals.js
│ ├── index.js
│ ├── tabs.js
│ └── utils.js
├── navigation
│ ├── Menu.js
│ └── Screens.js
├── package.json
└── screens
├── About.js
├── Agreement.js
├── Articles.js
├── Beauty.js
├── Cart.js
├── Category.js
├── Chat.js
├── Elements.js
├── Fashion.js
├── Gallery.js
├── Home.js
├── Notifications.js
├── Onboarding.js
├── PersonalNotifications.js
├── Privacy.js
├── Pro.js
├── Product.js
├── Profile.js
├── Register.js
├── Search.js
├── Settings.js
└── SystemNotifications.js
```
## OS Support
At present, we officially aim to support the last two versions of the following operating systems:
[<img src="https://raw.githubusercontent.com/creativetimofficial/ct-material-kit-pro-react-native/master/assets/android-logo.png" width="60" height="60" />](https://www.creative-tim.com/product/material-kit-pro-react-native)[<img src="https://raw.githubusercontent.com/creativetimofficial/ct-material-kit-pro-react-native/master/assets/apple-logo.png" width="60" height="60" />](https://www.creative-tim.com/product/material-kit-pro-react-native)
## Resources
- Demo: <https://demos.creative-tim.com/argon-pro-react-native>
- Download Page: <https://www.creative-tim.com/product/argon-pro-react-native>
- Documentation: <https://demos.creative-tim.com/argon-pro-react-native/docs>
- License Agreement: <https://www.creative-tim.com/license>
- Support: <https://www.creative-tim.com/contact-us>
- Issues: [Github Issues Page](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues)
- [Argon Design System](https://www.creative-tim.com/product/argon-design-system?ref=argonrn-readme) - For Front End Development
- **Dashboards:**
| HTML | React | Vue |
| --- | --- | --- |
| [![Argon HTML](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro/argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro) | [![Argon Dashboard React](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-react/argon-dashboard-pro-react.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-react) | [![Argon Dashboard Vue](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/vue-argon-dashboard-pro/vue-argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/vue-argon-dashboard-pro)
| Node.js | Nuxt | Laravel |
| --- | --- | --- |
| [![Argon Dashboard PRO NodeJS](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-nodejs/argon-dashboard-pro-nodejs.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-nodejs) | [![Argon Dashboard PRO Nuxt](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/nuxt-argon-dashboard-pro/nuxt-argon-dashboard-pro.jpg)](https://www.creative-tim.com/product/nuxt-argon-dashboard-pro) | [![Argon Dashboard PRO Laravel](https://raw.githubusercontent.com/creativetimofficial/public-assets/master/argon-dashboard-pro-laravel/argon-dashboard-pro-laravel.jpg)](https://www.creative-tim.com/product/argon-dashboard-pro-laravel)
## Reporting Issues
We use GitHub Issues as the official bug tracker for the Argon PRO React Native. Here are some advices for our users that want to report an issue:
1. Make sure that you are using the latest version of the Argon PRO React Native.
2. Providing us reproducible steps for the issue will shorten the time it takes for it to be fixed.
3. Some issues may be platform specific, so specifying on what platform you encountered the issue might help.
### Technical Support or Questions
If you have questions or need help integrating the product please [contact us](https://www.creative-tim.com/contact-us) instead of opening an issue.
## Licensing
- Copyright 2019 Creative Tim (https://www.creative-tim.com/)
- Creative Tim [license](https://creative-tim.com/license)
## Useful Links
- [Tutorials](https://www.youtube.com/channel/UCVyTG4sCw-rOvB9oHkzZD1w)
- [Affiliate Program](https://www.creative-tim.com/affiliates/new) (earn money)
- [Blog Creative Tim](http://blog.creative-tim.com/)
- [Free Products](https://www.creative-tim.com/bootstrap-themes/free) from Creative Tim
- [Premium Products](https://www.creative-tim.com/bootstrap-themes/premium) from Creative Tim
- [React Products](https://www.creative-tim.com/bootstrap-themes/react-themes) from Creative Tim
- [Angular Products](https://www.creative-tim.com/bootstrap-themes/angular-themes) from Creative Tim
- [VueJS Products](https://www.creative-tim.com/bootstrap-themes/vuejs-themes) from Creative Tim
- [More products](https://www.creative-tim.com/bootstrap-themes) from Creative Tim
- Check our Bundles [here](https://www.creative-tim.com/bundles?ref="argon-github-readme")
### Social Media
Twitter: <https://twitter.com/CreativeTim>
Facebook: <https://www.facebook.com/CreativeTim>
Dribbble: <https://dribbble.com/creativetim>
Google+: <https://plus.google.com/+CreativetimPage>
Instagram: <https://www.instagram.com/CreativeTimOfficial>

View File

@@ -1,50 +0,0 @@
{
"expo": {
"name": "价值前沿",
"slug": "valuefrontier",
"privacy": "public",
"platforms": [
"ios",
"android"
],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/logo.jpg",
"splash": {
"image": "./assets/logo.jpg",
"resizeMode": "contain",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.valuefrontier.meagent",
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
},
"android": {
"package": "com.valuefrontier.meagent",
"adaptiveIcon": {
"foregroundImage": "./assets/logo.jpg",
"backgroundColor": "#000000"
}
},
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/logo.jpg",
"color": "#D4AF37",
"sounds": []
}
]
],
"description": "价值前沿 - 智能投资助手"
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M20,0H4C2.3,0,1,1.3,1,3v20c0,0.6,0.4,1,1,1h20c0.6,0,1-0.4,1-1V3C23,1.3,21.7,0,20,0z M12,16 c-3.3,0-6-2.7-6-6c0-0.6,0.4-1,1-1s1,0.4,1,1c0,2.2,1.8,4,4,4s4-1.8,4-4c0-0.6,0.4-1,1-1s1,0.4,1,1C18,13.3,15.3,16,12,16z M20,4H4 C3.4,4,3,3.6,3,3s0.4-1,1-1h16c0.6,0,1,0.4,1,1S20.6,4,20,4z"/></g></svg>

Before

Width:  |  Height:  |  Size: 497 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><path d="M1,10.5A1.5,1.5,0,0,0,2.5,12h7A1.5,1.5,0,0,0,11,10.5V7H1Z" fill="#444444"/> <path d="M9.838,4,8.171.665a.75.75,0,0,0-1.342.67L8.162,4H3.838L5.171,1.335A.75.75,0,0,0,3.829.665L2.162,4H0V6H12V4Z" fill="#444444" data-color="color-2"/></g></svg>

Before

Width:  |  Height:  |  Size: 434 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path d="M20,10V8A8,8,0,0,0,4,8v2a4.441,4.441,0,0,1-1.547,3.193A4.183,4.183,0,0,0,1,16c0,2.5,4.112,4,11,4s11-1.5,11-4a4.183,4.183,0,0,0-1.453-2.807A4.441,4.441,0,0,1,20,10Z" fill="#444444"/> <path data-color="color-2" d="M9.145,21.9a2.992,2.992,0,0,0,5.71,0c-.894.066-1.844.1-2.855.1S10.039,21.968,9.145,21.9Z" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 521 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><rect data-color="color-2" x="4" y="10" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="10" y="10" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="4" y="15" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="10" y="15" width="4" height="3" fill="#444444"/> <rect data-color="color-2" x="16" y="10" width="4" height="3" fill="#444444"/> <path d="M23,3H18V1a1,1,0,0,0-2,0V3H8V1A1,1,0,0,0,6,1V3H1A1,1,0,0,0,0,4V22a1,1,0,0,0,1,1H23a1,1,0,0,0,1-1V4A1,1,0,0,0,23,3ZM22,21H2V7H22Z" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 742 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path data-color="color-2" fill="#444444" d="M13,11h10.949C23.466,5.181,18.819,0.534,13,0.051V11z"/> <path fill="#444444" d="M12.414,13l-8.155,8.155C6.351,22.926,9.051,24,12,24c6.279,0,11.438-4.851,11.949-11H12.414z"/> <path fill="#444444" d="M11,11.586V0.051C4.851,0.562,0,5.721,0,12c0,2.949,1.074,5.649,2.845,7.741L11,11.586z"/></g></svg>

Before

Width:  |  Height:  |  Size: 524 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M18.768,1.36C18.578,1.132,18.297,1,18,1H6C5.703,1,5.422,1.132,5.232,1.36l-5,6 c-0.294,0.353-0.31,0.861-0.039,1.231l11,15C11.382,23.848,11.682,24,12,24s0.618-0.152,0.807-0.409l11-15 c0.271-0.371,0.256-0.878-0.039-1.231L18.768,1.36z M19,9H5V7h14V9z"/></g></svg>

Before

Width:  |  Height:  |  Size: 467 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16" width="16" height="16"><g class="nc-icon-wrapper" fill="#444444"><path d="M8,15A7,7,0,0,1,3.333,2.783l1.334,1.49a5,5,0,1,0,6.666,0l1.333-1.49A7,7,0,0,1,8,15Z" fill="#444444"/> <rect x="7" width="2" height="7" fill="#444444" data-color="color-2"/></g></svg>

Before

Width:  |  Height:  |  Size: 375 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 16 16" width="16" height="16"><g class="nc-icon-wrapper" fill="#444444"><path fill="#444444" d="M16,3.6L15.2,2C8.3,4,4.8,8.4,4.8,8.4L1.6,6L0,7.6L4.8,14C8.5,7.1,16,3.6,16,3.6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 299 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><rect x="22" y="11" fill="#444444" width="2" height="6"/> <path data-color="color-2" fill="#444444" d="M13.241,15.73C12.847,15.91,12.43,16,12,16s-0.847-0.09-1.24-0.269L4,12.658V18 c0,2.626,4.024,4,8,4s8-1.374,8-4v-5.341L13.241,15.73z"/> <path fill="#444444" d="M23.414,7.09l-11-5c-0.263-0.119-0.564-0.119-0.827,0l-11,5C0.229,7.252,0,7.607,0,8s0.229,0.748,0.586,0.91 l11,5C11.718,13.97,11.859,14,12,14s0.282-0.03,0.414-0.09l11-5C23.771,8.748,24,8.393,24,8S23.771,7.252,23.414,7.09z"/></g></svg>

Before

Width:  |  Height:  |  Size: 677 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<g class="nc-icon-wrapper" fill="#444444">
<path d="M20 24c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 16c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm24-16c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-8 16c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm-8-8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 639 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g class="nc-icon-wrapper" fill="#444444">
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24" width="24" height="24"><g class="nc-icon-wrapper" fill="#444444"><polygon fill="#444444" points="17,1.382 13,3.382 13,22.618 17,20.618 "/> <polygon data-color="color-2" fill="#444444" points="11,3.382 7,1.382 7,20.618 11,22.618 "/> <path fill="#444444" d="M5,1.434L0.485,4.143C0.185,4.323,0,4.648,0,5v19l5-3.234V1.434z"/> <path data-color="color-2" fill="#444444" d="M23.515,4.143L19,1.434v19.332L24,24V5C24,4.648,23.815,4.323,23.515,4.143z"/></g></svg>

Before

Width:  |  Height:  |  Size: 572 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><path d="M11,9H1a1,1,0,0,0,0,2H11a1,1,0,0,0,0-2Z" fill="#444444"/> <path d="M11,1H1A1,1,0,0,0,1,3H11a1,1,0,0,0,0-2Z" fill="#444444"/> <path d="M11,5H1A1,1,0,0,0,1,7H11a1,1,0,0,0,0-2Z" fill="#444444" data-color="color-2"/></g></svg>

Before

Width:  |  Height:  |  Size: 415 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><polygon points="6 5.882 2.148 2.03 0.074 4.104 6 10.03 11.926 4.104 9.852 2.03 6 5.882" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 299 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><polygon points="7.92 0 1.92 6 7.92 12 10.02 9.9 6.12 6 10.02 2.1 7.92 0" fill="#444444"></polygon></g></svg>

Before

Width:  |  Height:  |  Size: 293 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 12 12" width="12" height="12"><g class="nc-icon-wrapper" fill="#444444"><polygon points="1.98 2.1 5.88 6 1.98 9.9 4.08 12 10.08 6 4.08 0 1.98 2.1" fill="#444444"/></g></svg>

Before

Width:  |  Height:  |  Size: 285 B

Some files were not shown because too many files have changed in this diff Show More