update pay function
This commit is contained in:
198
README.md
198
README.md
@@ -1,198 +0,0 @@
|
||||
# vf_react
|
||||
|
||||
前端
|
||||
|
||||
---
|
||||
|
||||
## 📚 重构记录
|
||||
|
||||
### 2025-10-30: EventList.js 组件化重构
|
||||
|
||||
#### 🎯 重构目标
|
||||
将 Community 社区页面的 `EventList.js` 组件(1095行)拆分为多个可复用的子组件,提高代码可维护性和复用性。
|
||||
|
||||
#### 📊 重构成果
|
||||
- **重构前**: 1095 行
|
||||
- **重构后**: 497 行
|
||||
- **减少**: 598 行 (-54.6%)
|
||||
|
||||
---
|
||||
|
||||
### 📁 新增目录结构
|
||||
|
||||
```
|
||||
src/views/Community/components/EventCard/
|
||||
├── index.js (60行) - EventCard 统一入口,智能路由紧凑/详细模式
|
||||
│
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│ 原子组件 (Atoms) - 7个基础UI组件
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│
|
||||
├── EventTimeline.js (60行) - 时间轴显示组件
|
||||
│ └── Props: createdAt, timelineStyle, borderColor, minHeight
|
||||
│
|
||||
├── EventImportanceBadge.js (100行) - 重要性等级标签 (S/A/B/C/D)
|
||||
│ └── Props: importance, showTooltip, showIcon, size
|
||||
│
|
||||
├── EventStats.js (60行) - 统计信息 (浏览/帖子/关注)
|
||||
│ └── Props: viewCount, postCount, followerCount, size, spacing
|
||||
│
|
||||
├── EventFollowButton.js (40行) - 关注按钮
|
||||
│ └── Props: isFollowing, followerCount, onToggle, size, showCount
|
||||
│
|
||||
├── EventPriceDisplay.js (130行) - 价格变动显示 (平均/最大/周)
|
||||
│ └── Props: avgChange, maxChange, weekChange, compact, inline
|
||||
│
|
||||
├── EventDescription.js (60行) - 描述文本 (支持展开/收起)
|
||||
│ └── Props: description, textColor, minLength, noOfLines
|
||||
│
|
||||
├── EventHeader.js (100行) - 事件标题头部
|
||||
│ └── Props: title, importance, onTitleClick, linkColor, compact
|
||||
│
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│ 组合组件 (Molecules) - 2个卡片组件
|
||||
├── ──────────────────────────────────────────────────────────
|
||||
│
|
||||
├── CompactEventCard.js (160行) - 紧凑模式事件卡片
|
||||
│ ├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton
|
||||
│ └── Props: event, index, isFollowing, followerCount, callbacks...
|
||||
│
|
||||
└── DetailedEventCard.js (170行) - 详细模式事件卡片
|
||||
├── 使用: EventTimeline, EventHeader, EventStats, EventFollowButton,
|
||||
│ EventPriceDisplay, EventDescription
|
||||
└── Props: event, isFollowing, followerCount, callbacks...
|
||||
```
|
||||
|
||||
**总计**: 10个文件,940行代码
|
||||
|
||||
---
|
||||
|
||||
### 🔧 重构的文件
|
||||
|
||||
#### `src/views/Community/components/EventList.js`
|
||||
|
||||
**移除的内容**:
|
||||
- ❌ `renderPriceChange` 函数 (~60行)
|
||||
- ❌ `renderCompactEvent` 函数 (~200行)
|
||||
- ❌ `renderDetailedEvent` 函数 (~300行)
|
||||
- ❌ `expandedDescriptions` state(展开状态管理移至子组件)
|
||||
- ❌ 冗余的 Chakra UI 导入
|
||||
|
||||
**保留的功能**:
|
||||
- ✅ WebSocket 实时推送
|
||||
- ✅ 浏览器原生通知
|
||||
- ✅ 关注状态管理 (followingMap, followCountMap)
|
||||
- ✅ 分页控制
|
||||
- ✅ 视图模式切换(紧凑/详细)
|
||||
- ✅ 推送权限管理
|
||||
|
||||
**新增引入**:
|
||||
```javascript
|
||||
import EventCard from './EventCard';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🏗️ 架构改进
|
||||
|
||||
#### 重构前(单体架构)
|
||||
```
|
||||
EventList.js (1095行)
|
||||
├── 业务逻辑 (WebSocket, 关注, 通知)
|
||||
├── renderCompactEvent (200行)
|
||||
│ └── 所有UI代码内联
|
||||
├── renderDetailedEvent (300行)
|
||||
│ └── 所有UI代码内联
|
||||
└── renderPriceChange (60行)
|
||||
```
|
||||
|
||||
#### 重构后(组件化架构)
|
||||
```
|
||||
EventList.js (497行) - 容器组件
|
||||
├── 业务逻辑 (WebSocket, 关注, 通知)
|
||||
└── 渲染逻辑
|
||||
└── EventCard (智能路由)
|
||||
├── CompactEventCard (紧凑模式)
|
||||
│ ├── EventTimeline
|
||||
│ ├── EventHeader (compact)
|
||||
│ ├── EventStats
|
||||
│ └── EventFollowButton
|
||||
└── DetailedEventCard (详细模式)
|
||||
├── EventTimeline
|
||||
├── EventHeader (detailed)
|
||||
├── EventStats
|
||||
├── EventFollowButton
|
||||
├── EventPriceDisplay
|
||||
└── EventDescription
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✨ 优势
|
||||
|
||||
1. **可维护性** ⬆️
|
||||
- 每个组件职责单一(单一职责原则)
|
||||
- 代码行数减少 54.6%
|
||||
- 组件边界清晰,易于理解
|
||||
|
||||
2. **可复用性** ⬆️
|
||||
- 原子组件可在其他页面复用
|
||||
- 例如:EventImportanceBadge 可用于任何需要显示事件等级的地方
|
||||
|
||||
3. **可测试性** ⬆️
|
||||
- 小组件更容易编写单元测试
|
||||
- 可独立测试每个组件的渲染和交互
|
||||
|
||||
4. **性能优化** ⬆️
|
||||
- React 可以更精确地追踪变化
|
||||
- 减少不必要的重渲染
|
||||
- 每个子组件可独立优化(useMemo, React.memo)
|
||||
|
||||
5. **开发效率** ⬆️
|
||||
- 新增功能时只需修改对应的子组件
|
||||
- 代码审查更高效
|
||||
- 降低了代码冲突的概率
|
||||
|
||||
---
|
||||
|
||||
### 📦 依赖工具函数
|
||||
|
||||
本次重构使用了之前提取的工具函数:
|
||||
|
||||
```
|
||||
src/utils/priceFormatters.js (105行)
|
||||
├── getPriceChangeColor(value) - 获取价格变化文字颜色
|
||||
├── getPriceChangeBg(value) - 获取价格变化背景颜色
|
||||
├── getPriceChangeBorderColor(value) - 获取价格变化边框颜色
|
||||
├── formatPriceChange(value) - 格式化价格为字符串
|
||||
└── PriceArrow({ value }) - 价格涨跌箭头组件
|
||||
|
||||
src/constants/animations.js (72行)
|
||||
├── pulseAnimation - 脉冲动画(S/A级标签)
|
||||
├── fadeIn - 渐入动画
|
||||
├── slideInUp - 从下往上滑入
|
||||
├── scaleIn - 缩放进入
|
||||
└── spin - 旋转动画(Loading)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🚀 下一步优化计划
|
||||
|
||||
Phase 1 已完成,后续可继续优化:
|
||||
|
||||
- **Phase 2**: 拆分 StockDetailPanel.js (1067行 → ~250行)
|
||||
- **Phase 3**: 拆分 InvestmentCalendar.js (827行 → ~200行)
|
||||
- **Phase 4**: 拆分 MidjourneyHeroSection.js (813行 → ~200行)
|
||||
- **Phase 5**: 拆分 UnifiedSearchBox.js (679行 → ~180行)
|
||||
|
||||
---
|
||||
|
||||
### 🔗 相关提交
|
||||
|
||||
- `feat: 拆分 EventList.js/提取价格相关工具函数到 utils/priceFormatters.js`
|
||||
- `feat(EventList): 创建事件卡片原子组件`
|
||||
- `feat(EventList): 创建事件卡片组合组件`
|
||||
- `refactor(EventList): 使用组件化架构替换内联渲染函数`
|
||||
|
||||
---
|
||||
@@ -1,218 +0,0 @@
|
||||
-- 预测市场数据库表创建脚本
|
||||
-- 数据库: stock (222.128.1.157:33060)
|
||||
-- 执行时间: 2025-11-23
|
||||
|
||||
-- 1. 用户积分账户表
|
||||
CREATE TABLE IF NOT EXISTS user_credit_account (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE,
|
||||
balance FLOAT DEFAULT 10000.0 NOT NULL COMMENT '积分余额',
|
||||
frozen_balance FLOAT DEFAULT 0.0 NOT NULL COMMENT '冻结积分',
|
||||
total_earned FLOAT DEFAULT 0.0 NOT NULL COMMENT '累计获得',
|
||||
total_spent FLOAT DEFAULT 0.0 NOT NULL COMMENT '累计消费',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
last_daily_bonus_at DATETIME COMMENT '最后一次领取每日奖励时间',
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户积分账户';
|
||||
|
||||
-- 2. 预测话题表
|
||||
CREATE TABLE IF NOT EXISTS prediction_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
creator_id INT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
category VARCHAR(50) DEFAULT 'stock' COMMENT '分类: stock/index/concept/policy/event/other',
|
||||
|
||||
-- 市场数据
|
||||
yes_total_shares INT DEFAULT 0 NOT NULL COMMENT 'YES方总份额',
|
||||
no_total_shares INT DEFAULT 0 NOT NULL COMMENT 'NO方总份额',
|
||||
yes_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'YES方价格(0-1000)',
|
||||
no_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'NO方价格(0-1000)',
|
||||
|
||||
-- 奖池
|
||||
total_pool FLOAT DEFAULT 0.0 NOT NULL COMMENT '总奖池(2%交易税累积)',
|
||||
|
||||
-- 领主信息
|
||||
yes_lord_id INT COMMENT 'YES方领主',
|
||||
no_lord_id INT COMMENT 'NO方领主',
|
||||
|
||||
-- 状态
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
result VARCHAR(10) COMMENT '结算结果: yes/no/draw',
|
||||
|
||||
-- 时间
|
||||
deadline DATETIME NOT NULL COMMENT '截止时间',
|
||||
settled_at DATETIME COMMENT '结算时间',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 统计
|
||||
views_count INT DEFAULT 0,
|
||||
comments_count INT DEFAULT 0,
|
||||
participants_count INT DEFAULT 0,
|
||||
|
||||
FOREIGN KEY (creator_id) REFERENCES user(id),
|
||||
FOREIGN KEY (yes_lord_id) REFERENCES user(id),
|
||||
FOREIGN KEY (no_lord_id) REFERENCES user(id),
|
||||
INDEX idx_creator_id (creator_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预测话题';
|
||||
|
||||
-- 3. 用户持仓表
|
||||
CREATE TABLE IF NOT EXISTS prediction_position (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
shares INT DEFAULT 0 NOT NULL COMMENT '持有份额',
|
||||
avg_cost FLOAT DEFAULT 0.0 NOT NULL COMMENT '平均成本',
|
||||
total_invested FLOAT DEFAULT 0.0 NOT NULL COMMENT '总投入',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_position (user_id, topic_id, direction),
|
||||
INDEX idx_user_topic (user_id, topic_id),
|
||||
INDEX idx_topic (topic_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户持仓';
|
||||
|
||||
-- 4. 交易记录表
|
||||
CREATE TABLE IF NOT EXISTS prediction_transaction (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
trade_type VARCHAR(10) NOT NULL COMMENT '交易类型: buy/sell',
|
||||
shares INT NOT NULL COMMENT '交易份额',
|
||||
price FLOAT NOT NULL COMMENT '交易价格',
|
||||
amount FLOAT NOT NULL COMMENT '交易金额',
|
||||
tax FLOAT DEFAULT 0.0 NOT NULL COMMENT '交易税',
|
||||
total_cost FLOAT NOT NULL COMMENT '总成本(含税)',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录';
|
||||
|
||||
-- 5. 话题评论表
|
||||
CREATE TABLE IF NOT EXISTS topic_comment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
content TEXT NOT NULL COMMENT '评论内容',
|
||||
direction VARCHAR(3) COMMENT '预测方向: yes/no',
|
||||
is_published BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已发布',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 观点IPO 相关
|
||||
total_investment INT DEFAULT 0 NOT NULL COMMENT '总投资额',
|
||||
investor_count INT DEFAULT 0 NOT NULL COMMENT '投资人数',
|
||||
is_verified BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已验证',
|
||||
verification_result VARCHAR(20) COMMENT '验证结果: correct/incorrect',
|
||||
position_rank INT COMMENT '评论位置排名',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position_rank (position_rank)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话题评论';
|
||||
|
||||
-- 6. 评论投资记录表(观点IPO)
|
||||
CREATE TABLE IF NOT EXISTS comment_investment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
comment_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
shares INT NOT NULL COMMENT '投资份额',
|
||||
amount INT NOT NULL COMMENT '投资金额',
|
||||
avg_price FLOAT NOT NULL COMMENT '平均价格',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (comment_id) REFERENCES topic_comment(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_comment_id (comment_id),
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论投资记录';
|
||||
|
||||
-- 7. 评论位置竞拍记录(首发权拍卖)
|
||||
CREATE TABLE IF NOT EXISTS comment_position_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
position INT NOT NULL COMMENT '位置:1/2/3',
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'pending' NOT NULL COMMENT '状态: pending/won/outbid',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL COMMENT '过期时间',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position (topic_id, position)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论位置竞拍记录';
|
||||
|
||||
-- 8. 时间胶囊话题表
|
||||
CREATE TABLE IF NOT EXISTS time_capsule_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
encrypted_content TEXT COMMENT '加密的预测内容',
|
||||
encryption_key VARCHAR(500) COMMENT '加密密钥(后端存储)',
|
||||
start_year INT NOT NULL COMMENT '开始年份',
|
||||
end_year INT NOT NULL COMMENT '结束年份',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
is_decrypted BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已解密',
|
||||
actual_happened_year INT COMMENT '实际发生年份',
|
||||
total_pool INT DEFAULT 0 NOT NULL COMMENT '总奖池',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_year_range (start_year, end_year)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊话题';
|
||||
|
||||
-- 9. 时间胶囊时间段表
|
||||
CREATE TABLE IF NOT EXISTS time_capsule_time_slot (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
year_start INT NOT NULL COMMENT '年份区间开始',
|
||||
year_end INT NOT NULL COMMENT '年份区间结束',
|
||||
current_holder_id INT COMMENT '当前持有者',
|
||||
current_price INT DEFAULT 100 NOT NULL COMMENT '当前价格',
|
||||
total_bids INT DEFAULT 0 NOT NULL COMMENT '总竞拍次数',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES time_capsule_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (current_holder_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_holder (current_holder_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊时间段';
|
||||
|
||||
-- 10. 时间段竞拍记录表
|
||||
CREATE TABLE IF NOT EXISTS time_slot_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'outbid' NOT NULL COMMENT '状态: current/outbid/won',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (slot_id) REFERENCES time_capsule_time_slot(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_slot_id (slot_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间段竞拍记录';
|
||||
@@ -1,134 +0,0 @@
|
||||
-- 修复预测市场表结构 - 完整版
|
||||
-- 添加所有缺失的字段
|
||||
-- 如果字段已存在会报错,可以忽略
|
||||
-- 执行前请备份数据库!
|
||||
|
||||
USE stock;
|
||||
|
||||
-- ==================== 完整修复 prediction_topic 表 ====================
|
||||
|
||||
-- 基本信息字段
|
||||
ALTER TABLE prediction_topic ADD COLUMN creator_id INT NOT NULL COMMENT '创建者ID' AFTER id;
|
||||
ALTER TABLE prediction_topic ADD COLUMN title VARCHAR(200) NOT NULL COMMENT '话题标题' AFTER creator_id;
|
||||
ALTER TABLE prediction_topic ADD COLUMN description TEXT COMMENT '话题描述' AFTER title;
|
||||
ALTER TABLE prediction_topic ADD COLUMN category VARCHAR(50) DEFAULT 'stock' COMMENT '分类' AFTER description;
|
||||
|
||||
-- 市场数据字段
|
||||
ALTER TABLE prediction_topic ADD COLUMN yes_total_shares INT DEFAULT 0 NOT NULL COMMENT 'YES方总份额' AFTER category;
|
||||
ALTER TABLE prediction_topic ADD COLUMN no_total_shares INT DEFAULT 0 NOT NULL COMMENT 'NO方总份额' AFTER yes_total_shares;
|
||||
ALTER TABLE prediction_topic ADD COLUMN yes_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'YES方价格(0-1000)' AFTER no_total_shares;
|
||||
ALTER TABLE prediction_topic ADD COLUMN no_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'NO方价格(0-1000)' AFTER yes_price;
|
||||
|
||||
-- 奖池
|
||||
ALTER TABLE prediction_topic ADD COLUMN total_pool FLOAT DEFAULT 0.0 NOT NULL COMMENT '总奖池(2%交易税累积)' AFTER no_price;
|
||||
|
||||
-- 领主信息
|
||||
ALTER TABLE prediction_topic ADD COLUMN yes_lord_id INT COMMENT 'YES方领主' AFTER total_pool;
|
||||
ALTER TABLE prediction_topic ADD COLUMN no_lord_id INT COMMENT 'NO方领主' AFTER yes_lord_id;
|
||||
|
||||
-- 状态
|
||||
ALTER TABLE prediction_topic ADD COLUMN status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled' AFTER no_lord_id;
|
||||
ALTER TABLE prediction_topic ADD COLUMN result VARCHAR(10) COMMENT '结算结果: yes/no/draw' AFTER status;
|
||||
|
||||
-- 时间
|
||||
ALTER TABLE prediction_topic ADD COLUMN deadline DATETIME NOT NULL COMMENT '截止时间' AFTER result;
|
||||
ALTER TABLE prediction_topic ADD COLUMN settled_at DATETIME COMMENT '结算时间' AFTER deadline;
|
||||
ALTER TABLE prediction_topic ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER settled_at;
|
||||
ALTER TABLE prediction_topic ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at;
|
||||
|
||||
-- 统计
|
||||
ALTER TABLE prediction_topic ADD COLUMN views_count INT DEFAULT 0 AFTER updated_at;
|
||||
ALTER TABLE prediction_topic ADD COLUMN comments_count INT DEFAULT 0 AFTER views_count;
|
||||
ALTER TABLE prediction_topic ADD COLUMN participants_count INT DEFAULT 0 AFTER comments_count;
|
||||
|
||||
-- 添加外键约束
|
||||
ALTER TABLE prediction_topic ADD CONSTRAINT fk_prediction_creator FOREIGN KEY (creator_id) REFERENCES user(id);
|
||||
ALTER TABLE prediction_topic ADD CONSTRAINT fk_prediction_yes_lord FOREIGN KEY (yes_lord_id) REFERENCES user(id);
|
||||
ALTER TABLE prediction_topic ADD CONSTRAINT fk_prediction_no_lord FOREIGN KEY (no_lord_id) REFERENCES user(id);
|
||||
|
||||
-- 添加索引
|
||||
ALTER TABLE prediction_topic ADD INDEX idx_creator_id (creator_id);
|
||||
ALTER TABLE prediction_topic ADD INDEX idx_status (status);
|
||||
ALTER TABLE prediction_topic ADD INDEX idx_category (category);
|
||||
ALTER TABLE prediction_topic ADD INDEX idx_created_at (created_at);
|
||||
|
||||
|
||||
-- ==================== 完整修复 prediction_position 表(用户持仓)====================
|
||||
|
||||
ALTER TABLE prediction_position ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY FIRST;
|
||||
ALTER TABLE prediction_position ADD COLUMN user_id INT NOT NULL AFTER id;
|
||||
ALTER TABLE prediction_position ADD COLUMN topic_id INT NOT NULL AFTER user_id;
|
||||
ALTER TABLE prediction_position ADD COLUMN direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no' AFTER topic_id;
|
||||
ALTER TABLE prediction_position ADD COLUMN shares INT DEFAULT 0 NOT NULL COMMENT '持有份额' AFTER direction;
|
||||
ALTER TABLE prediction_position ADD COLUMN avg_cost FLOAT DEFAULT 0.0 NOT NULL COMMENT '平均成本' AFTER shares;
|
||||
ALTER TABLE prediction_position ADD COLUMN total_invested FLOAT DEFAULT 0.0 NOT NULL COMMENT '总投入' AFTER avg_cost;
|
||||
ALTER TABLE prediction_position ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER total_invested;
|
||||
ALTER TABLE prediction_position ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at;
|
||||
|
||||
-- 外键和唯一约束
|
||||
ALTER TABLE prediction_position ADD CONSTRAINT fk_position_user FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE;
|
||||
ALTER TABLE prediction_position ADD CONSTRAINT fk_position_topic FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE;
|
||||
ALTER TABLE prediction_position ADD UNIQUE KEY unique_position (user_id, topic_id, direction);
|
||||
ALTER TABLE prediction_position ADD INDEX idx_user_topic (user_id, topic_id);
|
||||
ALTER TABLE prediction_position ADD INDEX idx_topic (topic_id);
|
||||
|
||||
|
||||
-- ==================== 完整修复 prediction_transaction 表(交易记录)====================
|
||||
|
||||
ALTER TABLE prediction_transaction ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY FIRST;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN user_id INT NOT NULL AFTER id;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN topic_id INT NOT NULL AFTER user_id;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no' AFTER topic_id;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN trade_type VARCHAR(10) NOT NULL COMMENT '交易类型: buy/sell' AFTER direction;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN shares INT NOT NULL COMMENT '交易份额' AFTER trade_type;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN price FLOAT NOT NULL COMMENT '交易价格' AFTER shares;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN amount FLOAT NOT NULL COMMENT '交易金额' AFTER price;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN tax FLOAT DEFAULT 0.0 NOT NULL COMMENT '交易税' AFTER amount;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN total_cost FLOAT NOT NULL COMMENT '总成本(含税)' AFTER tax;
|
||||
ALTER TABLE prediction_transaction ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER total_cost;
|
||||
|
||||
-- 外键和索引
|
||||
ALTER TABLE prediction_transaction ADD CONSTRAINT fk_transaction_user FOREIGN KEY (user_id) REFERENCES user(id);
|
||||
ALTER TABLE prediction_transaction ADD CONSTRAINT fk_transaction_topic FOREIGN KEY (topic_id) REFERENCES prediction_topic(id);
|
||||
ALTER TABLE prediction_transaction ADD INDEX idx_user_id (user_id);
|
||||
ALTER TABLE prediction_transaction ADD INDEX idx_topic_id (topic_id);
|
||||
ALTER TABLE prediction_transaction ADD INDEX idx_created_at (created_at);
|
||||
|
||||
|
||||
-- ==================== 完整修复 topic_comment 表(话题评论 + 观点IPO)====================
|
||||
|
||||
ALTER TABLE topic_comment ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY FIRST;
|
||||
ALTER TABLE topic_comment ADD COLUMN topic_id INT NOT NULL AFTER id;
|
||||
ALTER TABLE topic_comment ADD COLUMN user_id INT NOT NULL AFTER topic_id;
|
||||
ALTER TABLE topic_comment ADD COLUMN content TEXT NOT NULL COMMENT '评论内容' AFTER user_id;
|
||||
ALTER TABLE topic_comment ADD COLUMN direction VARCHAR(3) COMMENT '预测方向: yes/no' AFTER content;
|
||||
ALTER TABLE topic_comment ADD COLUMN is_published BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已发布' AFTER direction;
|
||||
ALTER TABLE topic_comment ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER is_published;
|
||||
ALTER TABLE topic_comment ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER created_at;
|
||||
|
||||
-- 观点IPO相关字段
|
||||
ALTER TABLE topic_comment ADD COLUMN total_investment INT DEFAULT 0 NOT NULL COMMENT '总投资额' AFTER updated_at;
|
||||
ALTER TABLE topic_comment ADD COLUMN investor_count INT DEFAULT 0 NOT NULL COMMENT '投资人数' AFTER total_investment;
|
||||
ALTER TABLE topic_comment ADD COLUMN is_verified BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已验证' AFTER investor_count;
|
||||
ALTER TABLE topic_comment ADD COLUMN verification_result VARCHAR(20) COMMENT '验证结果: correct/incorrect' AFTER is_verified;
|
||||
ALTER TABLE topic_comment ADD COLUMN position_rank INT COMMENT '评论位置排名' AFTER verification_result;
|
||||
|
||||
-- 外键和索引
|
||||
ALTER TABLE topic_comment ADD CONSTRAINT fk_comment_topic FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE;
|
||||
ALTER TABLE topic_comment ADD CONSTRAINT fk_comment_user FOREIGN KEY (user_id) REFERENCES user(id);
|
||||
ALTER TABLE topic_comment ADD INDEX idx_topic_id (topic_id);
|
||||
ALTER TABLE topic_comment ADD INDEX idx_user_id (user_id);
|
||||
ALTER TABLE topic_comment ADD INDEX idx_position_rank (position_rank);
|
||||
|
||||
|
||||
-- ==================== 执行说明 ====================
|
||||
--
|
||||
-- 重要提示:
|
||||
-- 1. 这个脚本会尝试添加所有字段,如果字段已存在会报错
|
||||
-- 2. 报错是正常的,可以忽略继续执行
|
||||
-- 3. 建议使用MySQL客户端逐条执行,跳过已存在字段的语句
|
||||
--
|
||||
-- 执行方式:
|
||||
-- mysql -h 222.128.1.157 -P 33060 -u your_username -p stock < fix_prediction_tables.sql
|
||||
--
|
||||
-- 或者在MySQL客户端中逐条复制执行
|
||||
@@ -1,836 +0,0 @@
|
||||
# ===========================
|
||||
# 预测市场 API 路由
|
||||
# 请将此文件内容插入到 app.py 的 `if __name__ == '__main__':` 之前
|
||||
# ===========================
|
||||
|
||||
# --- 积分系统 API ---
|
||||
|
||||
@app.route('/api/prediction/credit/account', methods=['GET'])
|
||||
@login_required
|
||||
def get_credit_account():
|
||||
"""获取用户积分账户"""
|
||||
try:
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
|
||||
# 如果账户不存在,自动创建
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'balance': float(account.balance),
|
||||
'frozen_balance': float(account.frozen_balance),
|
||||
'available_balance': float(account.balance - account.frozen_balance),
|
||||
'total_earned': float(account.total_earned),
|
||||
'total_spent': float(account.total_spent),
|
||||
'last_daily_bonus_at': account.last_daily_bonus_at.isoformat() if account.last_daily_bonus_at else None
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/credit/daily-bonus', methods=['POST'])
|
||||
@login_required
|
||||
def claim_daily_bonus():
|
||||
"""领取每日奖励(100积分)"""
|
||||
try:
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
|
||||
# 检查是否已领取今日奖励
|
||||
today = beijing_now().date()
|
||||
if account.last_daily_bonus_at and account.last_daily_bonus_at.date() == today:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '今日奖励已领取'
|
||||
}), 400
|
||||
|
||||
# 发放奖励
|
||||
bonus_amount = 100.0
|
||||
account.balance += bonus_amount
|
||||
account.total_earned += bonus_amount
|
||||
account.last_daily_bonus_at = beijing_now()
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='daily_bonus',
|
||||
amount=bonus_amount,
|
||||
balance_after=account.balance,
|
||||
description='每日登录奖励'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'领取成功,获得 {bonus_amount} 积分',
|
||||
'data': {
|
||||
'bonus_amount': bonus_amount,
|
||||
'new_balance': float(account.balance)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 预测话题 API ---
|
||||
|
||||
@app.route('/api/prediction/topics', methods=['POST'])
|
||||
@login_required
|
||||
def create_prediction_topic():
|
||||
"""创建预测话题(消耗100积分)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
title = data.get('title', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
category = data.get('category', 'stock')
|
||||
deadline_str = data.get('deadline')
|
||||
|
||||
# 验证参数
|
||||
if not title or len(title) < 5:
|
||||
return jsonify({'success': False, 'error': '标题至少5个字符'}), 400
|
||||
|
||||
if not deadline_str:
|
||||
return jsonify({'success': False, 'error': '请设置截止时间'}), 400
|
||||
|
||||
# 解析截止时间
|
||||
deadline = datetime.fromisoformat(deadline_str.replace('Z', '+00:00'))
|
||||
if deadline <= beijing_now():
|
||||
return jsonify({'success': False, 'error': '截止时间必须在未来'}), 400
|
||||
|
||||
# 检查积分账户
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
if not account or account.balance < 100:
|
||||
return jsonify({'success': False, 'error': '积分不足(需要100积分)'}), 400
|
||||
|
||||
# 扣除创建费用
|
||||
create_cost = 100.0
|
||||
account.balance -= create_cost
|
||||
account.total_spent += create_cost
|
||||
|
||||
# 创建话题
|
||||
topic = PredictionTopic(
|
||||
creator_id=current_user.id,
|
||||
title=title,
|
||||
description=description,
|
||||
category=category,
|
||||
deadline=deadline
|
||||
)
|
||||
db.session.add(topic)
|
||||
|
||||
# 记录积分交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='create_topic',
|
||||
amount=-create_cost,
|
||||
balance_after=account.balance,
|
||||
description=f'创建预测话题:{title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '话题创建成功',
|
||||
'data': {
|
||||
'topic_id': topic.id,
|
||||
'title': topic.title,
|
||||
'new_balance': float(account.balance)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics', methods=['GET'])
|
||||
def get_prediction_topics():
|
||||
"""获取预测话题列表"""
|
||||
try:
|
||||
status = request.args.get('status', 'active')
|
||||
category = request.args.get('category')
|
||||
sort_by = request.args.get('sort_by', 'created_at')
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# 构建查询
|
||||
query = PredictionTopic.query
|
||||
|
||||
if status:
|
||||
query = query.filter_by(status=status)
|
||||
if category:
|
||||
query = query.filter_by(category=category)
|
||||
|
||||
# 排序
|
||||
if sort_by == 'hot':
|
||||
query = query.order_by(desc(PredictionTopic.views_count))
|
||||
elif sort_by == 'participants':
|
||||
query = query.order_by(desc(PredictionTopic.participants_count))
|
||||
else:
|
||||
query = query.order_by(desc(PredictionTopic.created_at))
|
||||
|
||||
# 分页
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
topics = pagination.items
|
||||
|
||||
# 格式化返回数据
|
||||
topics_data = []
|
||||
for topic in topics:
|
||||
# 计算市场倾向
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
|
||||
|
||||
topics_data.append({
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'description': topic.description,
|
||||
'category': topic.category,
|
||||
'status': topic.status,
|
||||
'yes_price': float(topic.yes_price),
|
||||
'no_price': float(topic.no_price),
|
||||
'yes_probability': round(yes_prob, 1),
|
||||
'total_pool': float(topic.total_pool),
|
||||
'yes_lord': {
|
||||
'id': topic.yes_lord.id,
|
||||
'username': topic.yes_lord.username,
|
||||
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
|
||||
'avatar_url': topic.yes_lord.avatar_url
|
||||
} if topic.yes_lord else None,
|
||||
'no_lord': {
|
||||
'id': topic.no_lord.id,
|
||||
'username': topic.no_lord.username,
|
||||
'nickname': topic.no_lord.nickname or topic.no_lord.username,
|
||||
'avatar_url': topic.no_lord.avatar_url
|
||||
} if topic.no_lord else None,
|
||||
'deadline': topic.deadline.isoformat(),
|
||||
'created_at': topic.created_at.isoformat(),
|
||||
'views_count': topic.views_count,
|
||||
'comments_count': topic.comments_count,
|
||||
'participants_count': topic.participants_count,
|
||||
'creator': {
|
||||
'id': topic.creator.id,
|
||||
'username': topic.creator.username,
|
||||
'nickname': topic.creator.nickname or topic.creator.username
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': topics_data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages,
|
||||
'has_next': pagination.has_next,
|
||||
'has_prev': pagination.has_prev
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>', methods=['GET'])
|
||||
def get_prediction_topic_detail(topic_id):
|
||||
"""获取预测话题详情"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
# 增加浏览量
|
||||
topic.views_count += 1
|
||||
db.session.commit()
|
||||
|
||||
# 计算市场倾向
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
yes_prob = (topic.yes_total_shares / total_shares * 100) if total_shares > 0 else 50.0
|
||||
|
||||
# 获取 TOP 5 持仓(YES 和 NO 各5个)
|
||||
yes_top_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='yes'
|
||||
).order_by(desc(PredictionPosition.shares)).limit(5).all()
|
||||
|
||||
no_top_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='no'
|
||||
).order_by(desc(PredictionPosition.shares)).limit(5).all()
|
||||
|
||||
def format_position(position):
|
||||
return {
|
||||
'user': {
|
||||
'id': position.user.id,
|
||||
'username': position.user.username,
|
||||
'nickname': position.user.nickname or position.user.username,
|
||||
'avatar_url': position.user.avatar_url
|
||||
},
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost),
|
||||
'total_invested': float(position.total_invested),
|
||||
'is_lord': (topic.yes_lord_id == position.user_id and position.direction == 'yes') or
|
||||
(topic.no_lord_id == position.user_id and position.direction == 'no')
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'description': topic.description,
|
||||
'category': topic.category,
|
||||
'status': topic.status,
|
||||
'result': topic.result,
|
||||
'yes_price': float(topic.yes_price),
|
||||
'no_price': float(topic.no_price),
|
||||
'yes_total_shares': topic.yes_total_shares,
|
||||
'no_total_shares': topic.no_total_shares,
|
||||
'yes_probability': round(yes_prob, 1),
|
||||
'no_probability': round(100 - yes_prob, 1),
|
||||
'total_pool': float(topic.total_pool),
|
||||
'yes_lord': {
|
||||
'id': topic.yes_lord.id,
|
||||
'username': topic.yes_lord.username,
|
||||
'nickname': topic.yes_lord.nickname or topic.yes_lord.username,
|
||||
'avatar_url': topic.yes_lord.avatar_url
|
||||
} if topic.yes_lord else None,
|
||||
'no_lord': {
|
||||
'id': topic.no_lord.id,
|
||||
'username': topic.no_lord.username,
|
||||
'nickname': topic.no_lord.nickname or topic.no_lord.username,
|
||||
'avatar_url': topic.no_lord.avatar_url
|
||||
} if topic.no_lord else None,
|
||||
'yes_top_positions': [format_position(p) for p in yes_top_positions],
|
||||
'no_top_positions': [format_position(p) for p in no_top_positions],
|
||||
'deadline': topic.deadline.isoformat(),
|
||||
'settled_at': topic.settled_at.isoformat() if topic.settled_at else None,
|
||||
'created_at': topic.created_at.isoformat(),
|
||||
'views_count': topic.views_count,
|
||||
'comments_count': topic.comments_count,
|
||||
'participants_count': topic.participants_count,
|
||||
'creator': {
|
||||
'id': topic.creator.id,
|
||||
'username': topic.creator.username,
|
||||
'nickname': topic.creator.nickname or topic.creator.username,
|
||||
'avatar_url': topic.creator.avatar_url
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/settle', methods=['POST'])
|
||||
@login_required
|
||||
def settle_prediction_topic(topic_id):
|
||||
"""结算预测话题(仅创建者可操作)"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
# 验证权限
|
||||
if topic.creator_id != current_user.id:
|
||||
return jsonify({'success': False, 'error': '只有创建者可以结算'}), 403
|
||||
|
||||
# 验证状态
|
||||
if topic.status != 'active':
|
||||
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
|
||||
|
||||
# 验证截止时间
|
||||
if beijing_now() < topic.deadline:
|
||||
return jsonify({'success': False, 'error': '未到截止时间'}), 400
|
||||
|
||||
# 获取结算结果
|
||||
data = request.get_json()
|
||||
result = data.get('result') # 'yes', 'no', 'draw'
|
||||
|
||||
if result not in ['yes', 'no', 'draw']:
|
||||
return jsonify({'success': False, 'error': '无效的结算结果'}), 400
|
||||
|
||||
# 更新话题状态
|
||||
topic.status = 'settled'
|
||||
topic.result = result
|
||||
topic.settled_at = beijing_now()
|
||||
|
||||
# 获取获胜方的所有持仓
|
||||
if result == 'draw':
|
||||
# 平局:所有人按投入比例分配奖池
|
||||
all_positions = PredictionPosition.query.filter_by(topic_id=topic_id).all()
|
||||
total_invested = sum(p.total_invested for p in all_positions)
|
||||
|
||||
for position in all_positions:
|
||||
if total_invested > 0:
|
||||
share_ratio = position.total_invested / total_invested
|
||||
prize = topic.total_pool * share_ratio
|
||||
|
||||
# 发放奖金
|
||||
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
|
||||
if account:
|
||||
account.balance += prize
|
||||
account.total_earned += prize
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=position.user_id,
|
||||
transaction_type='settle_win',
|
||||
amount=prize,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
description=f'预测平局,获得奖池分红:{topic.title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
else:
|
||||
# YES 或 NO 获胜
|
||||
winning_direction = result
|
||||
winning_positions = PredictionPosition.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
direction=winning_direction
|
||||
).all()
|
||||
|
||||
if winning_positions:
|
||||
total_winning_shares = sum(p.shares for p in winning_positions)
|
||||
|
||||
for position in winning_positions:
|
||||
# 按份额比例分配奖池
|
||||
share_ratio = position.shares / total_winning_shares
|
||||
prize = topic.total_pool * share_ratio
|
||||
|
||||
# 发放奖金
|
||||
account = UserCreditAccount.query.filter_by(user_id=position.user_id).first()
|
||||
if account:
|
||||
account.balance += prize
|
||||
account.total_earned += prize
|
||||
|
||||
# 记录交易
|
||||
transaction = CreditTransaction(
|
||||
user_id=position.user_id,
|
||||
transaction_type='settle_win',
|
||||
amount=prize,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
description=f'预测正确,获得奖金:{topic.title}'
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'话题已结算,结果为:{result}',
|
||||
'data': {
|
||||
'topic_id': topic.id,
|
||||
'result': result,
|
||||
'total_pool': float(topic.total_pool),
|
||||
'settled_at': topic.settled_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 交易 API ---
|
||||
|
||||
@app.route('/api/prediction/trade/buy', methods=['POST'])
|
||||
@login_required
|
||||
def buy_prediction_shares():
|
||||
"""买入预测份额"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
topic_id = data.get('topic_id')
|
||||
direction = data.get('direction') # 'yes' or 'no'
|
||||
shares = data.get('shares', 0)
|
||||
|
||||
# 验证参数
|
||||
if not topic_id or direction not in ['yes', 'no'] or shares <= 0:
|
||||
return jsonify({'success': False, 'error': '参数错误'}), 400
|
||||
|
||||
if shares > 1000:
|
||||
return jsonify({'success': False, 'error': '单次最多买入1000份额'}), 400
|
||||
|
||||
# 获取话题
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
if topic.status != 'active':
|
||||
return jsonify({'success': False, 'error': '话题已结算或已取消'}), 400
|
||||
|
||||
if beijing_now() >= topic.deadline:
|
||||
return jsonify({'success': False, 'error': '话题已截止'}), 400
|
||||
|
||||
# 获取积分账户
|
||||
account = UserCreditAccount.query.filter_by(user_id=current_user.id).first()
|
||||
if not account:
|
||||
account = UserCreditAccount(user_id=current_user.id)
|
||||
db.session.add(account)
|
||||
db.session.flush()
|
||||
|
||||
# 计算价格
|
||||
current_price = topic.yes_price if direction == 'yes' else topic.no_price
|
||||
|
||||
# 简化的AMM定价:price = (对应方份额 / 总份额) * 1000
|
||||
total_shares = topic.yes_total_shares + topic.no_total_shares
|
||||
if total_shares > 0:
|
||||
if direction == 'yes':
|
||||
current_price = (topic.yes_total_shares / total_shares) * 1000
|
||||
else:
|
||||
current_price = (topic.no_total_shares / total_shares) * 1000
|
||||
else:
|
||||
current_price = 500.0 # 初始价格
|
||||
|
||||
# 买入后价格会上涨,使用平均价格
|
||||
after_total = total_shares + shares
|
||||
if direction == 'yes':
|
||||
after_yes_shares = topic.yes_total_shares + shares
|
||||
after_price = (after_yes_shares / after_total) * 1000
|
||||
else:
|
||||
after_no_shares = topic.no_total_shares + shares
|
||||
after_price = (after_no_shares / after_total) * 1000
|
||||
|
||||
avg_price = (current_price + after_price) / 2
|
||||
|
||||
# 计算费用
|
||||
amount = avg_price * shares
|
||||
tax = amount * 0.02 # 2% 手续费
|
||||
total_cost = amount + tax
|
||||
|
||||
# 检查余额
|
||||
if account.balance < total_cost:
|
||||
return jsonify({'success': False, 'error': '积分不足'}), 400
|
||||
|
||||
# 扣除费用
|
||||
account.balance -= total_cost
|
||||
account.total_spent += total_cost
|
||||
|
||||
# 更新话题数据
|
||||
if direction == 'yes':
|
||||
topic.yes_total_shares += shares
|
||||
topic.yes_price = after_price
|
||||
else:
|
||||
topic.no_total_shares += shares
|
||||
topic.no_price = after_price
|
||||
|
||||
topic.total_pool += tax # 手续费进入奖池
|
||||
|
||||
# 更新或创建持仓
|
||||
position = PredictionPosition.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
direction=direction
|
||||
).first()
|
||||
|
||||
if position:
|
||||
# 更新平均成本
|
||||
old_cost = position.avg_cost * position.shares
|
||||
new_cost = avg_price * shares
|
||||
position.shares += shares
|
||||
position.avg_cost = (old_cost + new_cost) / position.shares
|
||||
position.total_invested += total_cost
|
||||
else:
|
||||
position = PredictionPosition(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
direction=direction,
|
||||
shares=shares,
|
||||
avg_cost=avg_price,
|
||||
total_invested=total_cost
|
||||
)
|
||||
db.session.add(position)
|
||||
topic.participants_count += 1
|
||||
|
||||
# 更新领主
|
||||
if direction == 'yes':
|
||||
# 找到YES方持仓最多的用户
|
||||
top_yes = db.session.query(PredictionPosition).filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='yes'
|
||||
).order_by(desc(PredictionPosition.shares)).first()
|
||||
if top_yes:
|
||||
topic.yes_lord_id = top_yes.user_id
|
||||
else:
|
||||
# 找到NO方持仓最多的用户
|
||||
top_no = db.session.query(PredictionPosition).filter_by(
|
||||
topic_id=topic_id,
|
||||
direction='no'
|
||||
).order_by(desc(PredictionPosition.shares)).first()
|
||||
if top_no:
|
||||
topic.no_lord_id = top_no.user_id
|
||||
|
||||
# 记录交易
|
||||
transaction = PredictionTransaction(
|
||||
user_id=current_user.id,
|
||||
topic_id=topic_id,
|
||||
transaction_type='buy',
|
||||
direction=direction,
|
||||
shares=shares,
|
||||
price=avg_price,
|
||||
amount=amount,
|
||||
tax=tax,
|
||||
total_cost=total_cost
|
||||
)
|
||||
db.session.add(transaction)
|
||||
|
||||
# 记录积分交易
|
||||
credit_transaction = CreditTransaction(
|
||||
user_id=current_user.id,
|
||||
transaction_type='prediction_buy',
|
||||
amount=-total_cost,
|
||||
balance_after=account.balance,
|
||||
related_topic_id=topic_id,
|
||||
related_transaction_id=transaction.id,
|
||||
description=f'买入 {direction.upper()} 份额:{topic.title}'
|
||||
)
|
||||
db.session.add(credit_transaction)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '买入成功',
|
||||
'data': {
|
||||
'transaction_id': transaction.id,
|
||||
'shares': shares,
|
||||
'price': round(avg_price, 2),
|
||||
'total_cost': round(total_cost, 2),
|
||||
'tax': round(tax, 2),
|
||||
'new_balance': float(account.balance),
|
||||
'new_position': {
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/positions', methods=['GET'])
|
||||
@login_required
|
||||
def get_user_positions():
|
||||
"""获取用户的所有持仓"""
|
||||
try:
|
||||
positions = PredictionPosition.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
positions_data = []
|
||||
for position in positions:
|
||||
topic = position.topic
|
||||
|
||||
# 计算当前市值(如果话题还在进行中)
|
||||
current_value = 0
|
||||
profit = 0
|
||||
profit_rate = 0
|
||||
|
||||
if topic.status == 'active':
|
||||
current_price = topic.yes_price if position.direction == 'yes' else topic.no_price
|
||||
current_value = current_price * position.shares
|
||||
profit = current_value - position.total_invested
|
||||
profit_rate = (profit / position.total_invested * 100) if position.total_invested > 0 else 0
|
||||
|
||||
positions_data.append({
|
||||
'id': position.id,
|
||||
'topic': {
|
||||
'id': topic.id,
|
||||
'title': topic.title,
|
||||
'status': topic.status,
|
||||
'result': topic.result,
|
||||
'deadline': topic.deadline.isoformat()
|
||||
},
|
||||
'direction': position.direction,
|
||||
'shares': position.shares,
|
||||
'avg_cost': float(position.avg_cost),
|
||||
'total_invested': float(position.total_invested),
|
||||
'current_value': round(current_value, 2),
|
||||
'profit': round(profit, 2),
|
||||
'profit_rate': round(profit_rate, 2),
|
||||
'created_at': position.created_at.isoformat(),
|
||||
'is_lord': (topic.yes_lord_id == current_user.id and position.direction == 'yes') or
|
||||
(topic.no_lord_id == current_user.id and position.direction == 'no')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': positions_data,
|
||||
'count': len(positions_data)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- 评论 API ---
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['POST'])
|
||||
@login_required
|
||||
def create_topic_comment(topic_id):
|
||||
"""发表话题评论"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
data = request.get_json()
|
||||
content = data.get('content', '').strip()
|
||||
parent_id = data.get('parent_id')
|
||||
|
||||
if not content or len(content) < 2:
|
||||
return jsonify({'success': False, 'error': '评论内容至少2个字符'}), 400
|
||||
|
||||
# 创建评论
|
||||
comment = TopicComment(
|
||||
topic_id=topic_id,
|
||||
user_id=current_user.id,
|
||||
content=content,
|
||||
parent_id=parent_id
|
||||
)
|
||||
|
||||
# 如果是领主评论,自动置顶
|
||||
is_lord = (topic.yes_lord_id == current_user.id) or (topic.no_lord_id == current_user.id)
|
||||
if is_lord:
|
||||
comment.is_pinned = True
|
||||
|
||||
db.session.add(comment)
|
||||
|
||||
# 更新话题评论数
|
||||
topic.comments_count += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': '评论成功',
|
||||
'data': {
|
||||
'comment_id': comment.id,
|
||||
'content': comment.content,
|
||||
'is_pinned': comment.is_pinned,
|
||||
'created_at': comment.created_at.isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/topics/<int:topic_id>/comments', methods=['GET'])
|
||||
def get_topic_comments(topic_id):
|
||||
"""获取话题评论列表"""
|
||||
try:
|
||||
topic = PredictionTopic.query.get_or_404(topic_id)
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
|
||||
# 置顶评论在前,然后按时间倒序
|
||||
query = TopicComment.query.filter_by(
|
||||
topic_id=topic_id,
|
||||
status='active',
|
||||
parent_id=None # 只获取顶级评论
|
||||
).order_by(
|
||||
desc(TopicComment.is_pinned),
|
||||
desc(TopicComment.created_at)
|
||||
)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
comments = pagination.items
|
||||
|
||||
def format_comment(comment):
|
||||
# 获取回复
|
||||
replies = TopicComment.query.filter_by(
|
||||
parent_id=comment.id,
|
||||
status='active'
|
||||
).order_by(TopicComment.created_at).limit(5).all()
|
||||
|
||||
return {
|
||||
'id': comment.id,
|
||||
'content': comment.content,
|
||||
'is_pinned': comment.is_pinned,
|
||||
'likes_count': comment.likes_count,
|
||||
'created_at': comment.created_at.isoformat(),
|
||||
'user': {
|
||||
'id': comment.user.id,
|
||||
'username': comment.user.username,
|
||||
'nickname': comment.user.nickname or comment.user.username,
|
||||
'avatar_url': comment.user.avatar_url
|
||||
},
|
||||
'is_lord': (topic.yes_lord_id == comment.user_id) or (topic.no_lord_id == comment.user_id),
|
||||
'replies': [{
|
||||
'id': reply.id,
|
||||
'content': reply.content,
|
||||
'created_at': reply.created_at.isoformat(),
|
||||
'user': {
|
||||
'id': reply.user.id,
|
||||
'username': reply.user.username,
|
||||
'nickname': reply.user.nickname or reply.user.username,
|
||||
'avatar_url': reply.user.avatar_url
|
||||
}
|
||||
} for reply in replies]
|
||||
}
|
||||
|
||||
comments_data = [format_comment(comment) for comment in comments]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': comments_data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prediction/comments/<int:comment_id>/like', methods=['POST'])
|
||||
@login_required
|
||||
def like_topic_comment(comment_id):
|
||||
"""点赞/取消点赞评论"""
|
||||
try:
|
||||
comment = TopicComment.query.get_or_404(comment_id)
|
||||
|
||||
# 检查是否已点赞
|
||||
existing_like = TopicCommentLike.query.filter_by(
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# 取消点赞
|
||||
db.session.delete(existing_like)
|
||||
comment.likes_count = max(0, comment.likes_count - 1)
|
||||
action = 'unliked'
|
||||
else:
|
||||
# 点赞
|
||||
like = TopicCommentLike(
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id
|
||||
)
|
||||
db.session.add(like)
|
||||
comment.likes_count += 1
|
||||
action = 'liked'
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'action': action,
|
||||
'likes_count': comment.likes_count
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
@@ -1,261 +0,0 @@
|
||||
-- 完整重建预测市场所有表
|
||||
-- 会删除旧表并重新创建
|
||||
-- ⚠️ 警告:会删除所有现有数据!
|
||||
-- 执行前请确认是测试环境或数据可以丢失
|
||||
|
||||
USE stock;
|
||||
|
||||
-- ==================== 删除所有旧表 ====================
|
||||
-- 按依赖关系倒序删除
|
||||
|
||||
DROP TABLE IF EXISTS time_slot_bid;
|
||||
DROP TABLE IF EXISTS time_capsule_time_slot;
|
||||
DROP TABLE IF EXISTS time_capsule_topic;
|
||||
DROP TABLE IF EXISTS comment_position_bid;
|
||||
DROP TABLE IF EXISTS comment_investment;
|
||||
DROP TABLE IF EXISTS topic_comment;
|
||||
DROP TABLE IF EXISTS prediction_transaction;
|
||||
DROP TABLE IF EXISTS prediction_position;
|
||||
DROP TABLE IF EXISTS prediction_topic;
|
||||
DROP TABLE IF EXISTS user_credit_account;
|
||||
|
||||
-- ==================== 重新创建所有表 ====================
|
||||
-- 按依赖关系正序创建
|
||||
|
||||
-- 1. 用户积分账户表
|
||||
CREATE TABLE user_credit_account (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE COMMENT '用户ID',
|
||||
|
||||
-- 积分余额
|
||||
balance FLOAT DEFAULT 10000.0 NOT NULL COMMENT '积分余额(初始10000)',
|
||||
frozen_balance FLOAT DEFAULT 0.0 NOT NULL COMMENT '冻结积分',
|
||||
total_earned FLOAT DEFAULT 0.0 NOT NULL COMMENT '累计获得',
|
||||
total_spent FLOAT DEFAULT 0.0 NOT NULL COMMENT '累计消费',
|
||||
|
||||
-- 时间
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
last_daily_bonus_at DATETIME COMMENT '最后一次领取每日奖励时间',
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户积分账户';
|
||||
|
||||
|
||||
-- 2. 预测话题表
|
||||
CREATE TABLE prediction_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
creator_id INT NOT NULL COMMENT '创建者ID',
|
||||
|
||||
-- 基本信息
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
category VARCHAR(50) DEFAULT 'stock' COMMENT '分类: stock/index/concept/policy/event/other',
|
||||
|
||||
-- 市场数据
|
||||
yes_total_shares INT DEFAULT 0 NOT NULL COMMENT 'YES方总份额',
|
||||
no_total_shares INT DEFAULT 0 NOT NULL COMMENT 'NO方总份额',
|
||||
yes_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'YES方价格(0-1000)',
|
||||
no_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'NO方价格(0-1000)',
|
||||
|
||||
-- 奖池
|
||||
total_pool FLOAT DEFAULT 0.0 NOT NULL COMMENT '总奖池(2%交易税累积)',
|
||||
|
||||
-- 领主信息
|
||||
yes_lord_id INT COMMENT 'YES方领主(持有最多份额的用户)',
|
||||
no_lord_id INT COMMENT 'NO方领主(持有最多份额的用户)',
|
||||
|
||||
-- 状态
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
result VARCHAR(10) COMMENT '结算结果: yes/no/draw',
|
||||
|
||||
-- 时间
|
||||
deadline DATETIME NOT NULL COMMENT '截止时间',
|
||||
settled_at DATETIME COMMENT '结算时间',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 统计
|
||||
views_count INT DEFAULT 0 COMMENT '浏览次数',
|
||||
comments_count INT DEFAULT 0 COMMENT '评论数',
|
||||
participants_count INT DEFAULT 0 COMMENT '参与人数',
|
||||
|
||||
FOREIGN KEY (creator_id) REFERENCES user(id),
|
||||
FOREIGN KEY (yes_lord_id) REFERENCES user(id),
|
||||
FOREIGN KEY (no_lord_id) REFERENCES user(id),
|
||||
INDEX idx_creator_id (creator_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预测话题';
|
||||
|
||||
|
||||
-- 3. 用户持仓表
|
||||
CREATE TABLE prediction_position (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
topic_id INT NOT NULL COMMENT '话题ID',
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
shares INT DEFAULT 0 NOT NULL COMMENT '持有份额',
|
||||
avg_cost FLOAT DEFAULT 0.0 NOT NULL COMMENT '平均成本',
|
||||
total_invested FLOAT DEFAULT 0.0 NOT NULL COMMENT '总投入',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_position (user_id, topic_id, direction),
|
||||
INDEX idx_user_topic (user_id, topic_id),
|
||||
INDEX idx_topic (topic_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户持仓';
|
||||
|
||||
|
||||
-- 4. 交易记录表
|
||||
CREATE TABLE prediction_transaction (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
topic_id INT NOT NULL COMMENT '话题ID',
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
trade_type VARCHAR(10) NOT NULL COMMENT '交易类型: buy/sell',
|
||||
shares INT NOT NULL COMMENT '交易份额',
|
||||
price FLOAT NOT NULL COMMENT '交易价格',
|
||||
amount FLOAT NOT NULL COMMENT '交易金额',
|
||||
tax FLOAT DEFAULT 0.0 NOT NULL COMMENT '交易税(2%)',
|
||||
total_cost FLOAT NOT NULL COMMENT '总成本(含税)',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录';
|
||||
|
||||
|
||||
-- 5. 话题评论表
|
||||
CREATE TABLE topic_comment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL COMMENT '话题ID',
|
||||
user_id INT NOT NULL COMMENT '用户ID',
|
||||
content TEXT NOT NULL COMMENT '评论内容',
|
||||
direction VARCHAR(3) COMMENT '预测方向: yes/no',
|
||||
is_published BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已发布',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 观点IPO相关字段
|
||||
total_investment INT DEFAULT 0 NOT NULL COMMENT '总投资额',
|
||||
investor_count INT DEFAULT 0 NOT NULL COMMENT '投资人数',
|
||||
is_verified BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已验证',
|
||||
verification_result VARCHAR(20) COMMENT '验证结果: correct/incorrect',
|
||||
position_rank INT COMMENT '评论位置排名(1/2/3)',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position_rank (position_rank)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话题评论';
|
||||
|
||||
|
||||
-- 6. 评论投资记录表(观点IPO)
|
||||
CREATE TABLE comment_investment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
comment_id INT NOT NULL COMMENT '评论ID',
|
||||
user_id INT NOT NULL COMMENT '投资者ID',
|
||||
shares INT NOT NULL COMMENT '投资份额',
|
||||
amount INT NOT NULL COMMENT '投资金额',
|
||||
avg_price FLOAT NOT NULL COMMENT '平均价格',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (comment_id) REFERENCES topic_comment(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_comment_id (comment_id),
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论投资记录(观点IPO)';
|
||||
|
||||
|
||||
-- 7. 评论位置竞拍记录表(首发权拍卖)
|
||||
CREATE TABLE comment_position_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL COMMENT '话题ID',
|
||||
user_id INT NOT NULL COMMENT '竞拍者ID',
|
||||
position INT NOT NULL COMMENT '位置:1/2/3',
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'pending' NOT NULL COMMENT '状态: pending/won/outbid',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL COMMENT '过期时间',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position (topic_id, position)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论位置竞拍记录(首发权拍卖)';
|
||||
|
||||
|
||||
-- 8. 时间胶囊话题表
|
||||
CREATE TABLE time_capsule_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL COMMENT '创建者ID',
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
encrypted_content TEXT COMMENT '加密的预测内容(前端AES加密)',
|
||||
encryption_key VARCHAR(500) COMMENT '加密密钥(后端存储)',
|
||||
start_year INT NOT NULL COMMENT '开始年份',
|
||||
end_year INT NOT NULL COMMENT '结束年份',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
is_decrypted BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已解密',
|
||||
actual_happened_year INT COMMENT '实际发生年份',
|
||||
total_pool INT DEFAULT 0 NOT NULL COMMENT '总奖池',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_year_range (start_year, end_year)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊话题(长期预测)';
|
||||
|
||||
|
||||
-- 9. 时间胶囊时间段表
|
||||
CREATE TABLE time_capsule_time_slot (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL COMMENT '话题ID',
|
||||
year_start INT NOT NULL COMMENT '年份区间开始',
|
||||
year_end INT NOT NULL COMMENT '年份区间结束',
|
||||
current_holder_id INT COMMENT '当前持有者ID',
|
||||
current_price INT DEFAULT 100 NOT NULL COMMENT '当前价格',
|
||||
total_bids INT DEFAULT 0 NOT NULL COMMENT '总竞拍次数',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES time_capsule_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (current_holder_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_holder (current_holder_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊时间段';
|
||||
|
||||
|
||||
-- 10. 时间段竞拍记录表
|
||||
CREATE TABLE time_slot_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_id INT NOT NULL COMMENT '时间段ID',
|
||||
user_id INT NOT NULL COMMENT '竞拍者ID',
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'outbid' NOT NULL COMMENT '状态: current/outbid/won',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (slot_id) REFERENCES time_capsule_time_slot(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_slot_id (slot_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间段竞拍记录';
|
||||
|
||||
|
||||
-- ==================== 执行完成提示 ====================
|
||||
SELECT '✅ 所有表创建成功!' AS status;
|
||||
SELECT '📊 共创建了 10 个表' AS info;
|
||||
SELECT '💰 用户注册时将自动获得 10000 初始积分' AS credit_info;
|
||||
SELECT '🎮 预测市场系统已就绪' AS market_status;
|
||||
@@ -1,221 +0,0 @@
|
||||
-- 重建预测市场表(完全删除后重新创建)
|
||||
-- 警告:会删除所有现有数据!
|
||||
-- 仅在测试环境或数据可以丢失时使用
|
||||
-- 执行前请备份数据库!
|
||||
|
||||
USE stock;
|
||||
|
||||
-- 删除旧表(按依赖关系倒序删除)
|
||||
DROP TABLE IF EXISTS time_slot_bid;
|
||||
DROP TABLE IF EXISTS time_capsule_time_slot;
|
||||
DROP TABLE IF EXISTS time_capsule_topic;
|
||||
DROP TABLE IF EXISTS comment_position_bid;
|
||||
DROP TABLE IF EXISTS comment_investment;
|
||||
DROP TABLE IF EXISTS topic_comment;
|
||||
DROP TABLE IF EXISTS prediction_transaction;
|
||||
DROP TABLE IF EXISTS prediction_position;
|
||||
DROP TABLE IF EXISTS prediction_topic;
|
||||
|
||||
-- 重新创建表(按依赖关系正序创建)
|
||||
|
||||
-- 1. 预测话题表
|
||||
CREATE TABLE prediction_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
creator_id INT NOT NULL,
|
||||
|
||||
-- 基本信息
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
category VARCHAR(50) DEFAULT 'stock' COMMENT '分类: stock/index/concept/policy/event/other',
|
||||
|
||||
-- 市场数据
|
||||
yes_total_shares INT DEFAULT 0 NOT NULL COMMENT 'YES方总份额',
|
||||
no_total_shares INT DEFAULT 0 NOT NULL COMMENT 'NO方总份额',
|
||||
yes_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'YES方价格(0-1000)',
|
||||
no_price FLOAT DEFAULT 500.0 NOT NULL COMMENT 'NO方价格(0-1000)',
|
||||
|
||||
-- 奖池
|
||||
total_pool FLOAT DEFAULT 0.0 NOT NULL COMMENT '总奖池(2%交易税累积)',
|
||||
|
||||
-- 领主信息
|
||||
yes_lord_id INT COMMENT 'YES方领主',
|
||||
no_lord_id INT COMMENT 'NO方领主',
|
||||
|
||||
-- 状态
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
result VARCHAR(10) COMMENT '结算结果: yes/no/draw',
|
||||
|
||||
-- 时间
|
||||
deadline DATETIME NOT NULL COMMENT '截止时间',
|
||||
settled_at DATETIME COMMENT '结算时间',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 统计
|
||||
views_count INT DEFAULT 0,
|
||||
comments_count INT DEFAULT 0,
|
||||
participants_count INT DEFAULT 0,
|
||||
|
||||
FOREIGN KEY (creator_id) REFERENCES user(id),
|
||||
FOREIGN KEY (yes_lord_id) REFERENCES user(id),
|
||||
FOREIGN KEY (no_lord_id) REFERENCES user(id),
|
||||
INDEX idx_creator_id (creator_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预测话题';
|
||||
|
||||
-- 2. 用户持仓表
|
||||
CREATE TABLE prediction_position (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
shares INT DEFAULT 0 NOT NULL COMMENT '持有份额',
|
||||
avg_cost FLOAT DEFAULT 0.0 NOT NULL COMMENT '平均成本',
|
||||
total_invested FLOAT DEFAULT 0.0 NOT NULL COMMENT '总投入',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_position (user_id, topic_id, direction),
|
||||
INDEX idx_user_topic (user_id, topic_id),
|
||||
INDEX idx_topic (topic_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户持仓';
|
||||
|
||||
-- 3. 交易记录表
|
||||
CREATE TABLE prediction_transaction (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
direction VARCHAR(3) NOT NULL COMMENT '方向: yes/no',
|
||||
trade_type VARCHAR(10) NOT NULL COMMENT '交易类型: buy/sell',
|
||||
shares INT NOT NULL COMMENT '交易份额',
|
||||
price FLOAT NOT NULL COMMENT '交易价格',
|
||||
amount FLOAT NOT NULL COMMENT '交易金额',
|
||||
tax FLOAT DEFAULT 0.0 NOT NULL COMMENT '交易税',
|
||||
total_cost FLOAT NOT NULL COMMENT '总成本(含税)',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易记录';
|
||||
|
||||
-- 4. 话题评论表
|
||||
CREATE TABLE topic_comment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
content TEXT NOT NULL COMMENT '评论内容',
|
||||
direction VARCHAR(3) COMMENT '预测方向: yes/no',
|
||||
is_published BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已发布',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
-- 观点IPO相关
|
||||
total_investment INT DEFAULT 0 NOT NULL COMMENT '总投资额',
|
||||
investor_count INT DEFAULT 0 NOT NULL COMMENT '投资人数',
|
||||
is_verified BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已验证',
|
||||
verification_result VARCHAR(20) COMMENT '验证结果: correct/incorrect',
|
||||
position_rank INT COMMENT '评论位置排名',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position_rank (position_rank)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='话题评论';
|
||||
|
||||
-- 5. 评论投资记录表(观点IPO)
|
||||
CREATE TABLE comment_investment (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
comment_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
shares INT NOT NULL COMMENT '投资份额',
|
||||
amount INT NOT NULL COMMENT '投资金额',
|
||||
avg_price FLOAT NOT NULL COMMENT '平均价格',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (comment_id) REFERENCES topic_comment(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_comment_id (comment_id),
|
||||
INDEX idx_user_id (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论投资记录';
|
||||
|
||||
-- 6. 评论位置竞拍记录(首发权拍卖)
|
||||
CREATE TABLE comment_position_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
position INT NOT NULL COMMENT '位置:1/2/3',
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'pending' NOT NULL COMMENT '状态: pending/won/outbid',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL COMMENT '过期时间',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES prediction_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_position (topic_id, position)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评论位置竞拍记录';
|
||||
|
||||
-- 7. 时间胶囊话题表
|
||||
CREATE TABLE time_capsule_topic (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL COMMENT '话题标题',
|
||||
description TEXT COMMENT '话题描述',
|
||||
encrypted_content TEXT COMMENT '加密的预测内容',
|
||||
encryption_key VARCHAR(500) COMMENT '加密密钥(后端存储)',
|
||||
start_year INT NOT NULL COMMENT '开始年份',
|
||||
end_year INT NOT NULL COMMENT '结束年份',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled/cancelled',
|
||||
is_decrypted BOOLEAN DEFAULT FALSE NOT NULL COMMENT '是否已解密',
|
||||
actual_happened_year INT COMMENT '实际发生年份',
|
||||
total_pool INT DEFAULT 0 NOT NULL COMMENT '总奖池',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_year_range (start_year, end_year)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊话题';
|
||||
|
||||
-- 8. 时间胶囊时间段表
|
||||
CREATE TABLE time_capsule_time_slot (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
topic_id INT NOT NULL,
|
||||
year_start INT NOT NULL COMMENT '年份区间开始',
|
||||
year_end INT NOT NULL COMMENT '年份区间结束',
|
||||
current_holder_id INT COMMENT '当前持有者',
|
||||
current_price INT DEFAULT 100 NOT NULL COMMENT '当前价格',
|
||||
total_bids INT DEFAULT 0 NOT NULL COMMENT '总竞拍次数',
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL COMMENT '状态: active/settled',
|
||||
|
||||
FOREIGN KEY (topic_id) REFERENCES time_capsule_topic(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (current_holder_id) REFERENCES user(id),
|
||||
INDEX idx_topic_id (topic_id),
|
||||
INDEX idx_holder (current_holder_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间胶囊时间段';
|
||||
|
||||
-- 9. 时间段竞拍记录表
|
||||
CREATE TABLE time_slot_bid (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slot_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
bid_amount INT NOT NULL COMMENT '竞拍金额',
|
||||
status VARCHAR(20) DEFAULT 'outbid' NOT NULL COMMENT '状态: current/outbid/won',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (slot_id) REFERENCES time_capsule_time_slot(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES user(id),
|
||||
INDEX idx_slot_id (slot_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时间段竞拍记录';
|
||||
@@ -1,3 +0,0 @@
|
||||
INFO Accepting connections at http://localhost:58321
|
||||
|
||||
INFO Gracefully shutting down. Please wait...
|
||||
@@ -174,6 +174,52 @@ export const likeComment = async (commentId) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 观点IPO API ====================
|
||||
|
||||
/**
|
||||
* 投资评论(观点IPO)
|
||||
* @param {number} commentId - 评论ID
|
||||
* @param {number} shares - 投资份额
|
||||
*/
|
||||
export const investComment = async (commentId, shares) => {
|
||||
try {
|
||||
const response = await api.post(`/api/prediction/comments/${commentId}/invest`, { shares });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('投资评论失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取评论的投资列表
|
||||
* @param {number} commentId - 评论ID
|
||||
*/
|
||||
export const getCommentInvestments = async (commentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/prediction/comments/${commentId}/investments`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取投资列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证评论结果(仅创建者可操作)
|
||||
* @param {number} commentId - 评论ID
|
||||
* @param {string} result - 'correct' | 'incorrect'
|
||||
*/
|
||||
export const verifyComment = async (commentId, result) => {
|
||||
try {
|
||||
const response = await api.post(`/api/prediction/comments/${commentId}/verify`, { result });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('验证评论失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 工具函数(价格计算保留在前端,用于实时预览)====================
|
||||
|
||||
export const MARKET_CONFIG = {
|
||||
@@ -266,6 +312,11 @@ export default {
|
||||
getComments,
|
||||
likeComment,
|
||||
|
||||
// 观点IPO
|
||||
investComment,
|
||||
getCommentInvestments,
|
||||
verifyComment,
|
||||
|
||||
// 工具函数
|
||||
calculatePrice,
|
||||
calculateTax,
|
||||
|
||||
@@ -39,6 +39,8 @@ import { forumColors } from '@theme/forumTheme';
|
||||
import { getTopicDetail, getUserAccount } from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import TradeModal from './components/TradeModal';
|
||||
import PredictionCommentSection from './components/PredictionCommentSection';
|
||||
import CommentInvestModal from './components/CommentInvestModal';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
@@ -52,6 +54,8 @@ const PredictionTopicDetail = () => {
|
||||
const [topic, setTopic] = useState(null);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
const [tradeMode, setTradeMode] = useState('buy');
|
||||
const [selectedComment, setSelectedComment] = useState(null);
|
||||
const [commentSectionKey, setCommentSectionKey] = useState(0);
|
||||
|
||||
// 模态框
|
||||
const {
|
||||
@@ -60,6 +64,12 @@ const PredictionTopicDetail = () => {
|
||||
onClose: onTradeModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isInvestModalOpen,
|
||||
onOpen: onInvestModalOpen,
|
||||
onClose: onInvestModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载话题数据
|
||||
useEffect(() => {
|
||||
const loadTopic = async () => {
|
||||
@@ -136,6 +146,44 @@ const PredictionTopicDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 打开投资弹窗
|
||||
const handleOpenInvest = (comment) => {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedComment(comment);
|
||||
onInvestModalOpen();
|
||||
};
|
||||
|
||||
// 投资成功回调
|
||||
const handleInvestSuccess = async () => {
|
||||
// 刷新账户数据
|
||||
try {
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
|
||||
// 刷新评论区(通过更新key触发重新加载)
|
||||
setCommentSectionKey((prev) => prev + 1);
|
||||
|
||||
toast({
|
||||
title: '投资成功',
|
||||
description: '评论列表已刷新',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!topic) {
|
||||
return null;
|
||||
}
|
||||
@@ -561,6 +609,22 @@ const PredictionTopicDetail = () => {
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 评论区 - 占据全宽 */}
|
||||
<Box gridColumn={{ base: "1", lg: "1 / -1" }}>
|
||||
<MotionBox
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<PredictionCommentSection
|
||||
key={commentSectionKey}
|
||||
topicId={topicId}
|
||||
topic={topic}
|
||||
onInvest={handleOpenInvest}
|
||||
/>
|
||||
</MotionBox>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
@@ -572,6 +636,15 @@ const PredictionTopicDetail = () => {
|
||||
mode={tradeMode}
|
||||
onTradeSuccess={handleTradeSuccess}
|
||||
/>
|
||||
|
||||
{/* 观点投资模态框 */}
|
||||
<CommentInvestModal
|
||||
isOpen={isInvestModalOpen}
|
||||
onClose={onInvestModalClose}
|
||||
comment={selectedComment}
|
||||
topic={topic}
|
||||
onInvestSuccess={handleInvestSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
464
src/views/ValueForum/components/CommentInvestModal.js
Normal file
464
src/views/ValueForum/components/CommentInvestModal.js
Normal file
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* 观点IPO投资弹窗组件
|
||||
* 用于投资评论观点
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
Flex,
|
||||
useToast,
|
||||
Avatar,
|
||||
Badge,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { TrendingUp, DollarSign, AlertCircle, Lightbulb, Crown } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import {
|
||||
investComment,
|
||||
getUserAccount,
|
||||
} from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const CommentInvestModal = ({ isOpen, onClose, comment, topic, onInvestSuccess }) => {
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [shares, setShares] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
|
||||
// 异步获取用户账户
|
||||
useEffect(() => {
|
||||
const fetchAccount = async () => {
|
||||
if (!user || !isOpen) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户失败:', error);
|
||||
}
|
||||
};
|
||||
fetchAccount();
|
||||
}, [user, isOpen]);
|
||||
|
||||
// 重置状态
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShares(1);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!comment || !userAccount) return null;
|
||||
|
||||
// 计算投资成本(后端算法:基础价格100 + 已有投资额/10)
|
||||
const basePrice = 100;
|
||||
const priceIncrease = (comment.total_investment || 0) / 10;
|
||||
const pricePerShare = basePrice + priceIncrease;
|
||||
const totalCost = Math.round(pricePerShare * shares);
|
||||
|
||||
// 预期收益(如果预测正确,获得1.5倍回报)
|
||||
const expectedReturn = Math.round(totalCost * 1.5);
|
||||
const expectedProfit = expectedReturn - totalCost;
|
||||
|
||||
// 检查是否可以投资
|
||||
const canInvest = () => {
|
||||
// 检查余额
|
||||
if (userAccount.balance < totalCost) {
|
||||
return { ok: false, reason: '积分不足' };
|
||||
}
|
||||
|
||||
// 不能投资自己的评论
|
||||
if (comment.user?.id === user?.id) {
|
||||
return { ok: false, reason: '不能投资自己的评论' };
|
||||
}
|
||||
|
||||
// 检查是否已结算
|
||||
if (comment.is_verified) {
|
||||
return { ok: false, reason: '该评论已结算' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
const investCheck = canInvest();
|
||||
|
||||
// 处理投资
|
||||
const handleInvest = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const response = await investComment(comment.id, shares);
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: '投资成功!',
|
||||
description: `投资${totalCost}积分,剩余 ${response.data.new_balance} 积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
// 刷新账户数据
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
|
||||
// 通知父组件刷新
|
||||
if (onInvestSuccess) {
|
||||
onInvestSuccess(response.data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('投资失败:', error);
|
||||
toast({
|
||||
title: '投资失败',
|
||||
description: error.response?.data?.error || error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 判断是否是庄主
|
||||
const isYesLord = comment.user?.id === topic?.yes_lord_id;
|
||||
const isNoLord = comment.user?.id === topic?.no_lord_id;
|
||||
const isLord = isYesLord || isNoLord;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size={{ base: "full", sm: "lg" }} isCentered>
|
||||
<ModalOverlay backdropFilter="blur(4px)" />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius={{ base: "0", sm: "xl" }}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
maxH={{ base: "100vh", sm: "90vh" }}
|
||||
m={{ base: "0", sm: "4" }}
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
borderTopRadius={{ base: "0", sm: "xl" }}
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py={{ base: "4", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon
|
||||
as={Lightbulb}
|
||||
boxSize={{ base: "18px", sm: "20px" }}
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.primary} fontSize={{ base: "md", sm: "lg" }}>
|
||||
投资观点
|
||||
</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.text.primary} />
|
||||
|
||||
<ModalBody py={{ base: "4", sm: "6" }} px={{ base: "4", sm: "6" }}>
|
||||
<VStack spacing="5" align="stretch">
|
||||
{/* 评论作者信息 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="3" mb="2">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={comment.user?.nickname || comment.user?.username}
|
||||
src={comment.user?.avatar_url}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
/>
|
||||
<VStack align="start" spacing="0" flex="1">
|
||||
<HStack spacing="2">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.user?.nickname || comment.user?.username || '匿名用户'}
|
||||
</Text>
|
||||
{isLord && (
|
||||
<Badge
|
||||
bg={isYesLord ? 'green.500' : 'red.500'}
|
||||
color="white"
|
||||
fontSize="2xs"
|
||||
px="2"
|
||||
py="0.5"
|
||||
borderRadius="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
>
|
||||
<Crown size={10} />
|
||||
{isYesLord ? 'YES庄' : 'NO庄'}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 评论内容 */}
|
||||
<Text fontSize={{ base: "sm", sm: "md" }} color={forumColors.text.secondary} lineHeight="1.6">
|
||||
{comment.content}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 当前投资统计 */}
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>已有投资人数</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.investor_count || 0} 人
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>总投资额</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.total_investment || 0} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>当前价格</Text>
|
||||
<Text fontWeight="600" color={forumColors.primary[500]}>
|
||||
{Math.round(pricePerShare)} 积分/份
|
||||
</Text>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 投资份额 */}
|
||||
<Box>
|
||||
<Flex justify="space-between" mb={{ base: "2", sm: "3" }}>
|
||||
<Text fontSize={{ base: "sm", sm: "sm" }} fontWeight="600" color={forumColors.text.primary}>
|
||||
投资份额
|
||||
</Text>
|
||||
<Text fontSize={{ base: "sm", sm: "sm" }} color={forumColors.text.secondary}>
|
||||
{shares} 份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Slider
|
||||
value={shares}
|
||||
onChange={setShares}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
focusThumbOnChange={false}
|
||||
>
|
||||
<SliderTrack bg={forumColors.background.hover} h={{ base: "2", sm: "1.5" }}>
|
||||
<SliderFilledTrack bg={forumColors.gradients.goldPrimary} />
|
||||
</SliderTrack>
|
||||
<SliderThumb boxSize={{ base: "7", sm: "6" }} bg={forumColors.primary[500]}>
|
||||
<Box as={Icon} as={DollarSign} boxSize={{ base: "14px", sm: "12px" }} color="white" />
|
||||
</SliderThumb>
|
||||
</Slider>
|
||||
|
||||
<HStack justify="space-between" mt="2" fontSize={{ base: "2xs", sm: "xs" }} color={forumColors.text.tertiary}>
|
||||
<Text>1份</Text>
|
||||
<Text>10份 (最大)</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<Divider borderColor={forumColors.border.default} />
|
||||
|
||||
{/* 费用明细 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
>
|
||||
<VStack spacing={{ base: "1.5", sm: "2" }} align="stretch">
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>单价</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{Math.round(pricePerShare)} 积分/份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>份额</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{shares} 份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt={{ base: "1.5", sm: "2" }} mt="1">
|
||||
<Flex justify="space-between">
|
||||
<Text fontWeight="bold" color={forumColors.text.primary} fontSize={{ base: "sm", sm: "md" }}>
|
||||
投资总额
|
||||
</Text>
|
||||
<HStack spacing="1">
|
||||
<Icon as={DollarSign} boxSize={{ base: "16px", sm: "20px" }} color={forumColors.primary[500]} />
|
||||
<Text fontSize={{ base: "xl", sm: "2xl" }} fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{totalCost}
|
||||
</Text>
|
||||
<Text fontSize={{ base: "xs", sm: "sm" }} color={forumColors.text.secondary}>
|
||||
积分
|
||||
</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 预期收益 */}
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt={{ base: "1.5", sm: "2" }}>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color="green.600" fontWeight="600">
|
||||
<Icon as={TrendingUp} size={14} display="inline" mr="1" />
|
||||
预测正确收益(1.5倍)
|
||||
</Text>
|
||||
<Text fontWeight="600" color="green.600">
|
||||
+{expectedProfit} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text fontSize="2xs" color={forumColors.text.muted} mt="1">
|
||||
预测正确将获得 {expectedReturn} 积分(含本金)
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 余额提示 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>你的余额:</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{userAccount.balance} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }} mt="1">
|
||||
<Text color={forumColors.text.secondary}>投资后:</Text>
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={
|
||||
userAccount.balance >= totalCost
|
||||
? forumColors.success[500]
|
||||
: forumColors.error[500]
|
||||
}
|
||||
>
|
||||
{userAccount.balance - totalCost} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 警告提示 */}
|
||||
{!investCheck.ok && (
|
||||
<Box
|
||||
bg="red.50"
|
||||
border="1px solid"
|
||||
borderColor="red.200"
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={AlertCircle} boxSize={{ base: "14px", sm: "16px" }} color="red.500" />
|
||||
<Text fontSize={{ base: "xs", sm: "sm" }} color="red.600" fontWeight="600">
|
||||
{investCheck.reason}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 风险提示 */}
|
||||
<Box
|
||||
bg="orange.50"
|
||||
border="1px solid"
|
||||
borderColor="orange.200"
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2" mb="1">
|
||||
<Icon as={AlertCircle} boxSize="14px" color="orange.500" />
|
||||
<Text fontSize="xs" color="orange.700" fontWeight="600">
|
||||
投资风险提示
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="2xs" color="orange.600" lineHeight="1.5">
|
||||
观点预测存在不确定性,预测错误将损失全部投资。请谨慎评估后再投资。
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
borderTop="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py={{ base: "3", sm: "4" }}
|
||||
px={{ base: "4", sm: "6" }}
|
||||
>
|
||||
<HStack spacing={{ base: "2", sm: "3" }} w="full" justify="flex-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color={forumColors.text.secondary}
|
||||
_hover={{ bg: forumColors.background.hover }}
|
||||
h={{ base: "10", sm: "auto" }}
|
||||
fontSize={{ base: "sm", sm: "md" }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
onClick={handleInvest}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="投资中..."
|
||||
isDisabled={!investCheck.ok}
|
||||
_hover={{
|
||||
opacity: 0.9,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
h={{ base: "11", sm: "auto" }}
|
||||
fontSize={{ base: "sm", sm: "md" }}
|
||||
px={{ base: "6", sm: "4" }}
|
||||
>
|
||||
投资 {shares} 份
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentInvestModal;
|
||||
475
src/views/ValueForum/components/PredictionCommentSection.js
Normal file
475
src/views/ValueForum/components/PredictionCommentSection.js
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* 预测话题评论区组件
|
||||
* 支持发布评论、嵌套回复、点赞、庄主标识、观点IPO投资
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Avatar,
|
||||
Textarea,
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Divider,
|
||||
useToast,
|
||||
Badge,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Heart, MessageCircle, Send, TrendingUp, Crown, Pin } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import {
|
||||
createComment,
|
||||
getComments,
|
||||
likeComment,
|
||||
} from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const CommentItem = ({ comment, topicId, topic, onReply, onInvest }) => {
|
||||
const [isLiked, setIsLiked] = useState(false);
|
||||
const [likes, setLikes] = useState(comment.likes_count || 0);
|
||||
const [showReply, setShowReply] = useState(false);
|
||||
|
||||
// 处理点赞
|
||||
const handleLike = async () => {
|
||||
try {
|
||||
const response = await likeComment(comment.id);
|
||||
if (response.success) {
|
||||
setLikes(response.likes_count);
|
||||
setIsLiked(response.action === 'like');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return '刚刚';
|
||||
if (minutes < 60) return `${minutes}分钟前`;
|
||||
if (hours < 24) return `${hours}小时前`;
|
||||
if (days < 7) return `${days}天前`;
|
||||
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
// 判断是否是庄主
|
||||
const isYesLord = comment.user?.id === topic?.yes_lord_id;
|
||||
const isNoLord = comment.user?.id === topic?.no_lord_id;
|
||||
const isLord = isYesLord || isNoLord;
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Flex gap={{ base: "2", sm: "3" }} py={{ base: "3", sm: "4" }}>
|
||||
{/* 头像 */}
|
||||
<Avatar
|
||||
size={{ base: "sm", sm: "md" }}
|
||||
name={comment.user?.nickname || comment.user?.username}
|
||||
src={comment.user?.avatar_url}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
/>
|
||||
|
||||
{/* 评论内容 */}
|
||||
<VStack align="stretch" flex="1" spacing={{ base: "1.5", sm: "2" }}>
|
||||
{/* 用户名和时间 */}
|
||||
<HStack justify="space-between" flexWrap="wrap">
|
||||
<HStack spacing="2">
|
||||
<Text fontSize={{ base: "sm", sm: "md" }} fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.user?.nickname || comment.user?.username || '匿名用户'}
|
||||
</Text>
|
||||
|
||||
{/* 庄主标识 */}
|
||||
{isLord && (
|
||||
<Tooltip label={isYesLord ? 'YES方庄主' : 'NO方庄主'} placement="top">
|
||||
<Badge
|
||||
bg={isYesLord ? 'green.500' : 'red.500'}
|
||||
color="white"
|
||||
fontSize="2xs"
|
||||
px="2"
|
||||
py="0.5"
|
||||
borderRadius="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
>
|
||||
<Crown size={10} />
|
||||
{isYesLord ? 'YES庄' : 'NO庄'}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* 置顶标识 */}
|
||||
{comment.is_pinned && (
|
||||
<Badge
|
||||
bg={forumColors.primary[500]}
|
||||
color="white"
|
||||
fontSize="2xs"
|
||||
px="2"
|
||||
py="0.5"
|
||||
borderRadius="full"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
>
|
||||
<Pin size={10} />
|
||||
置顶
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Text fontSize={{ base: "2xs", sm: "xs" }} color={forumColors.text.muted}>
|
||||
{formatTime(comment.created_at)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* 评论正文 */}
|
||||
<Text fontSize={{ base: "sm", sm: "md" }} color={forumColors.text.secondary} lineHeight="1.6">
|
||||
{comment.content}
|
||||
</Text>
|
||||
|
||||
{/* 观点IPO投资统计 */}
|
||||
{comment.total_investment > 0 && (
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
borderRadius="md"
|
||||
px="3"
|
||||
py="2"
|
||||
mt="1"
|
||||
>
|
||||
<HStack spacing="4" fontSize="xs">
|
||||
<HStack spacing="1">
|
||||
<TrendingUp size={12} color={forumColors.primary[500]} />
|
||||
<Text color={forumColors.text.secondary}>
|
||||
{comment.investor_count || 0}人投资
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text color={forumColors.text.secondary}>
|
||||
总投资:<Text as="span" fontWeight="600" color={forumColors.primary[500]}>
|
||||
{comment.total_investment}
|
||||
</Text> 积分
|
||||
</Text>
|
||||
{comment.is_verified && (
|
||||
<Badge
|
||||
colorScheme={comment.verification_result === 'correct' ? 'green' : 'red'}
|
||||
fontSize="2xs"
|
||||
>
|
||||
{comment.verification_result === 'correct' ? '✓ 预测正确' : '✗ 预测错误'}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<HStack spacing={{ base: "3", sm: "4" }} fontSize="xs" color={forumColors.text.tertiary} flexWrap="wrap">
|
||||
<HStack
|
||||
spacing="1"
|
||||
cursor="pointer"
|
||||
onClick={handleLike}
|
||||
_hover={{ color: forumColors.primary[500] }}
|
||||
color={isLiked ? forumColors.primary[500] : forumColors.text.tertiary}
|
||||
>
|
||||
<Heart size={14} fill={isLiked ? 'currentColor' : 'none'} />
|
||||
<Text>{likes > 0 ? likes : '点赞'}</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
spacing="1"
|
||||
cursor="pointer"
|
||||
onClick={() => setShowReply(!showReply)}
|
||||
_hover={{ color: forumColors.primary[500] }}
|
||||
>
|
||||
<MessageCircle size={14} />
|
||||
<Text>回复</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 投资观点按钮 */}
|
||||
{!comment.is_verified && onInvest && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
leftIcon={<TrendingUp size={14} />}
|
||||
color={forumColors.primary[500]}
|
||||
_hover={{ bg: forumColors.gradients.goldSubtle }}
|
||||
onClick={() => onInvest(comment)}
|
||||
>
|
||||
投资观点
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* 回复输入框 */}
|
||||
{showReply && (
|
||||
<MotionBox
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
mt="2"
|
||||
>
|
||||
<ReplyInput
|
||||
topicId={topicId}
|
||||
parentId={comment.id}
|
||||
placeholder={`回复 @${comment.user?.nickname || comment.user?.username || '匿名用户'}`}
|
||||
onSubmit={() => {
|
||||
setShowReply(false);
|
||||
if (onReply) onReply();
|
||||
}}
|
||||
/>
|
||||
</MotionBox>
|
||||
)}
|
||||
|
||||
{/* 回复列表 */}
|
||||
{comment.replies && comment.replies.length > 0 && (
|
||||
<VStack
|
||||
align="stretch"
|
||||
spacing="2"
|
||||
pl={{ base: "3", sm: "4" }}
|
||||
mt="2"
|
||||
borderLeft="2px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
{comment.replies.map((reply) => (
|
||||
<Box key={reply.id}>
|
||||
<HStack spacing="2" mb="1">
|
||||
<Avatar
|
||||
size="xs"
|
||||
name={reply.user?.nickname || reply.user?.username}
|
||||
src={reply.user?.avatar_url}
|
||||
/>
|
||||
<Text fontSize="xs" fontWeight="600" color={forumColors.text.primary}>
|
||||
{reply.user?.nickname || reply.user?.username || '匿名用户'}
|
||||
</Text>
|
||||
<Text fontSize="2xs" color={forumColors.text.muted}>
|
||||
{formatTime(reply.created_at)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary} pl="6">
|
||||
{reply.content}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Flex>
|
||||
</MotionBox>
|
||||
);
|
||||
};
|
||||
|
||||
const ReplyInput = ({ topicId, parentId = null, placeholder, onSubmit }) => {
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [content, setContent] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!content.trim()) {
|
||||
toast({
|
||||
title: '请输入评论内容',
|
||||
status: 'warning',
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await createComment(topicId, {
|
||||
content: content.trim(),
|
||||
parent_id: parentId,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: '评论成功',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
setContent('');
|
||||
if (onSubmit) onSubmit();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('评论失败:', error);
|
||||
toast({
|
||||
title: '评论失败',
|
||||
description: error.response?.data?.error || error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap="2" align="end">
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={placeholder || '写下你的评论...'}
|
||||
size="sm"
|
||||
bg={forumColors.background.secondary}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_placeholder={{ color: forumColors.text.tertiary }}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
minH={{ base: "70px", sm: "80px" }}
|
||||
resize="vertical"
|
||||
fontSize={{ base: "sm", sm: "md" }}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<Send size={18} />}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isSubmitting}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color={forumColors.background.main}
|
||||
_hover={{ opacity: 0.9 }}
|
||||
size="sm"
|
||||
h={{ base: "9", sm: "10" }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const PredictionCommentSection = ({ topicId, topic, onInvest }) => {
|
||||
const [comments, setComments] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
// 加载评论
|
||||
const loadComments = async (pageNum = 1) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getComments(topicId, { page: pageNum, per_page: 20 });
|
||||
|
||||
if (response.success) {
|
||||
if (pageNum === 1) {
|
||||
setComments(response.data);
|
||||
} else {
|
||||
setComments((prev) => [...prev, ...response.data]);
|
||||
}
|
||||
setTotal(response.pagination?.total || response.data.length);
|
||||
setHasMore(response.pagination?.has_next || false);
|
||||
setPage(pageNum);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载评论失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadComments();
|
||||
}, [topicId]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
p={{ base: "4", sm: "6" }}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Flex justify="space-between" align="center" mb={{ base: "4", sm: "6" }}>
|
||||
<HStack spacing="2">
|
||||
<MessageCircle size={20} color={forumColors.primary[500]} />
|
||||
<Text fontSize={{ base: "md", sm: "lg" }} fontWeight="bold" color={forumColors.text.primary}>
|
||||
评论
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Text fontSize="sm" color={forumColors.text.tertiary}>
|
||||
共 {total} 条
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* 发表评论 */}
|
||||
<Box mb={{ base: "4", sm: "6" }}>
|
||||
<ReplyInput topicId={topicId} onSubmit={() => loadComments(1)} />
|
||||
</Box>
|
||||
|
||||
<Divider borderColor={forumColors.border.default} mb="4" />
|
||||
|
||||
{/* 评论列表 */}
|
||||
{loading && page === 1 ? (
|
||||
<Text color={forumColors.text.secondary} textAlign="center" py="8">
|
||||
加载中...
|
||||
</Text>
|
||||
) : comments.length === 0 ? (
|
||||
<Text color={forumColors.text.secondary} textAlign="center" py="8">
|
||||
暂无评论,快来抢沙发吧!
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<VStack align="stretch" spacing="0" divider={<Divider borderColor={forumColors.border.default} />}>
|
||||
<AnimatePresence>
|
||||
{comments.map((comment) => (
|
||||
<CommentItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
topicId={topicId}
|
||||
topic={topic}
|
||||
onReply={() => loadComments(1)}
|
||||
onInvest={onInvest}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</VStack>
|
||||
|
||||
{/* 加载更多 */}
|
||||
{hasMore && (
|
||||
<Flex justify="center" mt="6">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => loadComments(page + 1)}
|
||||
isLoading={loading}
|
||||
color={forumColors.text.secondary}
|
||||
_hover={{ bg: forumColors.background.hover }}
|
||||
>
|
||||
加载更多
|
||||
</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PredictionCommentSection;
|
||||
Reference in New Issue
Block a user