diff --git a/README.md b/README.md deleted file mode 100644 index 7ff19cea..00000000 --- a/README.md +++ /dev/null @@ -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): 使用组件化架构替换内联渲染函数` - ---- \ No newline at end of file diff --git a/create_prediction_tables.sql b/create_prediction_tables.sql deleted file mode 100644 index def727d0..00000000 --- a/create_prediction_tables.sql +++ /dev/null @@ -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='时间段竞拍记录'; diff --git a/fix_prediction_tables.sql b/fix_prediction_tables.sql deleted file mode 100644 index 7d4c394e..00000000 --- a/fix_prediction_tables.sql +++ /dev/null @@ -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客户端中逐条复制执行 diff --git a/prediction_market_routes_to_add.py b/prediction_market_routes_to_add.py deleted file mode 100644 index c1c9a23a..00000000 --- a/prediction_market_routes_to_add.py +++ /dev/null @@ -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/', 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//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//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//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//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 diff --git a/rebuild_all_tables.sql b/rebuild_all_tables.sql deleted file mode 100644 index 59f0b055..00000000 --- a/rebuild_all_tables.sql +++ /dev/null @@ -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; diff --git a/rebuild_prediction_tables.sql b/rebuild_prediction_tables.sql deleted file mode 100644 index 4fb507c6..00000000 --- a/rebuild_prediction_tables.sql +++ /dev/null @@ -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='时间段竞拍记录'; diff --git a/serve.log b/serve.log deleted file mode 100644 index 012db7e5..00000000 --- a/serve.log +++ /dev/null @@ -1,3 +0,0 @@ - INFO Accepting connections at http://localhost:58321 - - INFO Gracefully shutting down. Please wait... diff --git a/src/services/predictionMarketService.api.js b/src/services/predictionMarketService.api.js index 067c4f39..30a42e92 100644 --- a/src/services/predictionMarketService.api.js +++ b/src/services/predictionMarketService.api.js @@ -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, diff --git a/src/views/ValueForum/PredictionTopicDetail.js b/src/views/ValueForum/PredictionTopicDetail.js index 776a43aa..54c1dd7d 100644 --- a/src/views/ValueForum/PredictionTopicDetail.js +++ b/src/views/ValueForum/PredictionTopicDetail.js @@ -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 = () => { )} + + {/* 评论区 - 占据全宽 */} + + + + + @@ -572,6 +636,15 @@ const PredictionTopicDetail = () => { mode={tradeMode} onTradeSuccess={handleTradeSuccess} /> + + {/* 观点投资模态框 */} + ); }; diff --git a/src/views/ValueForum/components/CommentInvestModal.js b/src/views/ValueForum/components/CommentInvestModal.js new file mode 100644 index 00000000..f643cb1b --- /dev/null +++ b/src/views/ValueForum/components/CommentInvestModal.js @@ -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 ( + + + + + + + + 投资观点 + + + + + + + + {/* 评论作者信息 */} + + + + + + + {comment.user?.nickname || comment.user?.username || '匿名用户'} + + {isLord && ( + + + {isYesLord ? 'YES庄' : 'NO庄'} + + )} + + + + + {/* 评论内容 */} + + {comment.content} + + + + {/* 当前投资统计 */} + + + + 已有投资人数 + + {comment.investor_count || 0} 人 + + + + 总投资额 + + {comment.total_investment || 0} 积分 + + + + 当前价格 + + {Math.round(pricePerShare)} 积分/份 + + + + + + {/* 投资份额 */} + + + + 投资份额 + + + {shares} 份 + + + + + + + + + + + + + + 1份 + 10份 (最大) + + + + + + {/* 费用明细 */} + + + + 单价 + + {Math.round(pricePerShare)} 积分/份 + + + + + 份额 + + {shares} 份 + + + + + + + 投资总额 + + + + + {totalCost} + + + 积分 + + + + + + {/* 预期收益 */} + + + + + 预测正确收益(1.5倍) + + + +{expectedProfit} 积分 + + + + 预测正确将获得 {expectedReturn} 积分(含本金) + + + + + + {/* 余额提示 */} + + + 你的余额: + + {userAccount.balance} 积分 + + + + 投资后: + = totalCost + ? forumColors.success[500] + : forumColors.error[500] + } + > + {userAccount.balance - totalCost} 积分 + + + + + {/* 警告提示 */} + {!investCheck.ok && ( + + + + + {investCheck.reason} + + + + )} + + {/* 风险提示 */} + + + + + 投资风险提示 + + + + 观点预测存在不确定性,预测错误将损失全部投资。请谨慎评估后再投资。 + + + + + + + + + + + + + + ); +}; + +export default CommentInvestModal; diff --git a/src/views/ValueForum/components/PredictionCommentSection.js b/src/views/ValueForum/components/PredictionCommentSection.js new file mode 100644 index 00000000..01a71447 --- /dev/null +++ b/src/views/ValueForum/components/PredictionCommentSection.js @@ -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 ( + + + {/* 头像 */} + + + {/* 评论内容 */} + + {/* 用户名和时间 */} + + + + {comment.user?.nickname || comment.user?.username || '匿名用户'} + + + {/* 庄主标识 */} + {isLord && ( + + + + {isYesLord ? 'YES庄' : 'NO庄'} + + + )} + + {/* 置顶标识 */} + {comment.is_pinned && ( + + + 置顶 + + )} + + + {formatTime(comment.created_at)} + + + + + {/* 评论正文 */} + + {comment.content} + + + {/* 观点IPO投资统计 */} + {comment.total_investment > 0 && ( + + + + + + {comment.investor_count || 0}人投资 + + + + 总投资: + {comment.total_investment} + 积分 + + {comment.is_verified && ( + + {comment.verification_result === 'correct' ? '✓ 预测正确' : '✗ 预测错误'} + + )} + + + )} + + {/* 操作按钮 */} + + + + {likes > 0 ? likes : '点赞'} + + + setShowReply(!showReply)} + _hover={{ color: forumColors.primary[500] }} + > + + 回复 + + + {/* 投资观点按钮 */} + {!comment.is_verified && onInvest && ( + + )} + + + {/* 回复输入框 */} + {showReply && ( + + { + setShowReply(false); + if (onReply) onReply(); + }} + /> + + )} + + {/* 回复列表 */} + {comment.replies && comment.replies.length > 0 && ( + + {comment.replies.map((reply) => ( + + + + + {reply.user?.nickname || reply.user?.username || '匿名用户'} + + + {formatTime(reply.created_at)} + + + + {reply.content} + + + ))} + + )} + + + + ); +}; + +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 ( + +