Compare commits
3 Commits
feature_bu
...
3a4dade8ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4dade8ec | ||
|
|
6f81259f8c | ||
|
|
864844a52b |
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm test:*)",
|
||||
"Bash(xargs ls:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(npm start)",
|
||||
"Bash(python3:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
41
.env.mock
@@ -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
|
||||
@@ -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 域名,静态资源走 CDN,API 走专用域名
|
||||
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
|
||||
42
.env.test
@@ -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
|
||||
94
.eslintrc.js
@@ -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
@@ -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
@@ -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-beta(AgentChat)
|
||||
- **状态**: 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-SocketIO(WebSocket)+ 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 handler(Mock 模式)
|
||||
|
||||
### 添加新 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
@@ -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
|
||||
-->
|
||||
@@ -1 +0,0 @@
|
||||
17Fo4JhapMw6vtNa
|
||||
BIN
__pycache__/config.cpython-311.pyc
Executable file
BIN
__pycache__/wechat_pay.cpython-310.pyc
Normal file
BIN
__pycache__/wechat_pay_config.cpython-310.pyc
Normal file
BIN
about_us.docx
@@ -1 +0,0 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkBfIjOiu8vfmOSq1BXcjDsAqQ+xtwGO0aCn0VrhVzc0T70nDchaW/TJ4nW8qlRMBlgfTi00jDGFiW4ND9JHc4aES8yiDSNeaBW4gLQhC1isvpOndyu4YgDc+2lMfghv9+6D8uFl9VS8Vk6o7D+5SiE7F8aBn49rrLyvsmdN5i6eLxIuY9NM58/o0xVG5f3ktGqfFKzhtclPbt8ej39EgziCiNFbIk2FnZp9dB56vtmCKht4t3STDpM0RfC8YlQ2WpGu9okLJYSy1rfynhh0hlOy/9y4cYl50wthoQVxH/Hm9abiTMo2u6xWESreavtdDZ8ByKVltnUrRvzDQ4tVkYwIDAQAB
|
||||
@@ -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=
|
||||
@@ -1 +0,0 @@
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyULL5tuD2cjfNvgn9fvVfn+WbLhoP3wk3bcYb9AabsZc1GCBlnG579Socc3U9dG5IR7C3KHD4kYHnH4tbK2pEWmmfjbVKXWguLqL5K3Dggnl5KVOlVVjrcsmLPqTj7JdeO0AQolmgdr/o6TlhQdsqINQAK5F5wWwIwwcSoJsWZ6zlPPX/Q/eMIN4zGgK2taMhx656zSxsYE5OKRYkTJhVrMktxQdwbUzoFSID++dTpjxF4w5k5qeVW/1WZaaswMFWh2IcJ5oyc+VjTRqZvtQt4gB0Fa0EblfmSJaozhoWHwzwF+1qtv7gp/TcMYj/Ah2+tY0UPxucEcTqY/i7PPfwIDAQAB
|
||||
118
alipay_config.py
@@ -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)
|
||||
523
alipay_pay.py
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
225
apns_push.py
@@ -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}")
|
||||
"""
|
||||
45
app/__init__.py
Normal 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)
|
||||
BIN
app/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
app/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/__pycache__/models.cpython-311.pyc
Normal file
30
app/extensions.py
Normal 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
@@ -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
@@ -0,0 +1 @@
|
||||
# 路由包初始化文件
|
||||
BIN
app/routes/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/calendar.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/events.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/industries.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/limitanalyse.cpython-311.pyc
Normal file
BIN
app/routes/__pycache__/stocks.cpython-311.pyc
Normal file
121
app/routes/calendar.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
6352
app_vx_copy1.py
9
argon-pro-react-native/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
*.jks
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgLshiAgsJoiJegXNC
|
||||
55cF2MHBnQCi2AaObrf/qgEavcmgCgYIKoZIzj0DAQehRANCAAQoIgTclBUyCDU2
|
||||
gFaphqK1I4n1VAkEad144GMKxrdjwfAXbOenkDkUis/6LBEMoOI8tBTcwP1qlY7s
|
||||
V7zdIhb4
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,220 +0,0 @@
|
||||
# [Argon PRO React Native](https://creativetimofficial.github.io/argon-pro-react-native/docs/#) [](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/)
|
||||
|
||||
|
||||
 [](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aopen+is%3Aissue) [](https://github.com/creativetimofficial/ct-argon-pro-react-native/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-design-system) | [](https://www.creative-tim.com/product/argon-design-system-react) | [](https://www.creative-tim.com/product/argon-design-system-angular)
|
||||
|
||||
## Demo
|
||||
|
||||
| Home Screen | Profile Screen | Onboarding Screen | Register Screen |
|
||||
| --- | --- | --- | --- |
|
||||
| [](https://demos.creative-tim.com/argon-pro-react-native/) | [](https://demos.creative-tim.com/argon-pro-react-native/) | [](https://demos.creative-tim.com/argon-pro-react-native/) | [](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 |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-dashboard-pro) | [](https://www.creative-tim.com/product/argon-dashboard-pro-react) | [](https://www.creative-tim.com/product/vue-argon-dashboard-pro)
|
||||
|
||||
| Node.js | Nuxt | Laravel |
|
||||
| --- | --- | --- |
|
||||
| [](https://www.creative-tim.com/product/argon-dashboard-pro-nodejs) | [](https://www.creative-tim.com/product/nuxt-argon-dashboard-pro) | [](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>
|
||||
|
||||
@@ -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": "价值前沿 - 智能投资助手"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 778 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |