diff --git a/API_ENDPOINTS.md b/API_ENDPOINTS.md new file mode 100644 index 00000000..632cbc54 --- /dev/null +++ b/API_ENDPOINTS.md @@ -0,0 +1,415 @@ +# API 接口文档 + +本文档记录了项目中所有 API 接口的详细信息。 + +## 目录 +- [认证相关 API](#认证相关-api) +- [个人中心相关 API](#个人中心相关-api) +- [事件相关 API](#事件相关-api) +- [股票相关 API](#股票相关-api) +- [公司相关 API](#公司相关-api) +- [订阅/支付相关 API](#订阅支付相关-api) + +--- + +## 认证相关 API + +### POST /api/auth/send-verification-code +发送验证码到手机号或邮箱 + +**请求参数**: +```json +{ + "credential": "13800138000", // 手机号或邮箱 + "type": "phone", // 'phone' | 'email' + "purpose": "login" // 'login' | 'register' +} +``` + +**响应示例**: +```json +{ + "success": true, + "message": "验证码已发送到 13800138000", + "dev_code": "123456" // 仅开发环境返回 +} +``` + +**错误响应**: +```json +{ + "success": false, + "error": "发送验证码失败" +} +``` + +**Mock 数据**: ✅ `src/mocks/handlers/auth.js` 行 21-44 + +**涉及文件**: +- `src/components/Auth/AuthFormContent.js` 行 164-207 + +--- + +### POST /api/auth/login-with-code +使用验证码登录(支持自动注册新用户) + +**请求参数**: +```json +{ + "credential": "13800138000", + "verification_code": "123456", + "login_type": "phone" // 'phone' | 'email' +} +``` + +**响应示例**: +```json +{ + "success": true, + "message": "登录成功", + "isNewUser": false, + "user": { + "id": 1, + "phone": "13800138000", + "nickname": "用户昵称", + "email": null, + "avatar_url": "https://...", + "has_wechat": false + }, + "token": "mock_token_1_1234567890" +} +``` + +**错误响应**: +```json +{ + "success": false, + "error": "验证码错误" +} +``` + +**Mock 数据**: ✅ `src/mocks/handlers/auth.js` 行 47-115 + +**涉及文件**: +- `src/components/Auth/AuthFormContent.js` 行 252-327 + +**注意事项**: +- 后端需要支持自动注册新用户(当用户不存在时) +- 前端已添加 `.trim()` 防止空格问题 + +--- + +### GET /api/auth/session +检查当前登录状态 + +**响应示例**: +```json +{ + "success": true, + "isAuthenticated": true, + "user": { + "id": 1, + "phone": "13800138000", + "nickname": "用户昵称" + } +} +``` + +**Mock 数据**: ✅ `src/mocks/handlers/auth.js` 行 269-290 + +--- + +### POST /api/auth/logout +退出登录 + +**响应示例**: +```json +{ + "success": true, + "message": "退出成功" +} +``` + +**Mock 数据**: ✅ `src/mocks/handlers/auth.js` 行 317-329 + +--- + +## 个人中心相关 API + +### GET /api/account/watchlist +获取用户自选股列表 + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "stock_code": "000001.SZ", + "stock_name": "平安银行", + "added_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +**Mock 数据**: ❌ 待创建 `src/mocks/handlers/account.js` + +**涉及文件**: +- `src/views/Dashboard/Center.js` 行 94 + +--- + +### GET /api/account/watchlist/realtime +获取自选股实时行情 + +**响应示例**: +```json +{ + "success": true, + "data": { + "000001.SZ": { + "price": 12.34, + "change": 0.56, + "change_percent": 4.76, + "volume": 123456789 + } + } +} +``` + +**Mock 数据**: ❌ 待创建 + +**涉及文件**: +- `src/views/Dashboard/Center.js` 行 133 + +--- + +### GET /api/account/events/following +获取用户关注的事件列表 + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "title": "事件标题", + "followed_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +**Mock 数据**: ❌ 待创建 + +**涉及文件**: +- `src/views/Dashboard/Center.js` 行 95 + +--- + +### GET /api/account/events/comments +获取用户的事件评论 + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "event_id": 123, + "content": "评论内容", + "created_at": "2024-01-01T00:00:00Z" + } + ] +} +``` + +**Mock 数据**: ❌ 待创建 + +**涉及文件**: +- `src/views/Dashboard/Center.js` 行 96 + +--- + +### GET /api/subscription/current +获取当前订阅信息 + +**响应示例**: +```json +{ + "success": true, + "data": { + "plan": "premium", + "expires_at": "2025-01-01T00:00:00Z", + "auto_renew": true + } +} +``` + +**Mock 数据**: ❌ 待创建 `src/mocks/handlers/subscription.js` + +**涉及文件**: +- `src/views/Dashboard/Center.js` 行 97 + +--- + +## 事件相关 API + +### GET /api/events +获取事件列表 + +**查询参数**: +- `page`: 页码(默认 1) +- `per_page`: 每页数量(默认 10) +- `sort`: 排序方式 ('new' | 'hot' | 'returns') +- `importance`: 重要性筛选 ('all' | 'high' | 'medium' | 'low') +- `date_range`: 日期范围 +- `q`: 搜索关键词 +- `industry_classification`: 行业分类 +- `industry_code`: 行业代码 + +**响应示例**: +```json +{ + "success": true, + "data": { + "events": [ + { + "id": 1, + "title": "事件标题", + "importance": "high", + "created_at": "2024-01-01T00:00:00Z" + } + ], + "pagination": { + "page": 1, + "per_page": 10, + "total": 100 + } + } +} +``` + +**Mock 数据**: ⚠️ 部分实现(需完善) + +**涉及文件**: +- `src/views/Community/index.js` 行 148 + +--- + +### GET /api/events/:id +获取事件详情 + +**响应示例**: +```json +{ + "success": true, + "data": { + "id": 1, + "title": "事件标题", + "content": "事件内容", + "importance": "high", + "created_at": "2024-01-01T00:00:00Z" + } +} +``` + +**Mock 数据**: ❌ 待创建 + +--- + +### GET /api/events/:id/stocks +获取事件相关股票 + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "stock_code": "000001.SZ", + "stock_name": "平安银行", + "correlation": 0.85 + } + ] +} +``` + +**Mock 数据**: ✅ `src/mocks/handlers/event.js` 行 12-38 + +--- + +### GET /api/events/popular-keywords +获取热门关键词 + +**查询参数**: +- `limit`: 返回数量(默认 20) + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "keyword": "人工智能", + "count": 123, + "trend": "up" + } + ] +} +``` + +**Mock 数据**: ❌ 待创建 + +**涉及文件**: +- `src/views/Community/index.js` 行 180 + +--- + +### GET /api/events/hot +获取热点事件 + +**查询参数**: +- `days`: 天数范围(默认 5) +- `limit`: 返回数量(默认 4) + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "id": 1, + "title": "热点事件标题", + "heat_score": 95.5 + } + ] +} +``` + +**Mock 数据**: ❌ 待创建 + +**涉及文件**: +- `src/views/Community/index.js` 行 192 + +--- + +## 待补充 API + +以下 API 将在重构其他文件时逐步添加: + +- 股票相关 API +- 公司相关 API +- 订阅/支付相关 API +- 用户资料相关 API +- 行业分类相关 API + +--- + +## 更新日志 + +- 2024-XX-XX: 创建文档,记录认证和个人中心相关 API diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js index ba2efd7e..1a06c4db 100644 --- a/src/components/Auth/AuthFormContent.js +++ b/src/components/Auth/AuthFormContent.js @@ -35,6 +35,7 @@ import AuthHeader from './AuthHeader'; import VerificationCodeInput from './VerificationCodeInput'; import WechatRegister from './WechatRegister'; import { setCurrentUser } from '../../mocks/data/users'; +import { logger } from '../../utils/logger'; // 统一配置对象 const AUTH_CONFIG = { @@ -151,17 +152,22 @@ export default function AuthFormContent() { try { setSendingCode(true); + + const requestData = { + credential: credential.trim(), // 添加 trim() 防止空格 + type: 'phone', + purpose: config.api.purpose + }; + + logger.api.request('POST', '/api/auth/send-verification-code', requestData); + const response = await fetch('/api/auth/send-verification-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - credentials: 'include', // 必须包含以支持跨域 session cookie - body: JSON.stringify({ - credential, - type: 'phone', - purpose: config.api.purpose // 根据模式使用不同的purpose - }), + credentials: 'include', + body: JSON.stringify(requestData), }); if (!response) { @@ -170,6 +176,8 @@ export default function AuthFormContent() { const data = await response.json(); + logger.api.response('POST', '/api/auth/send-verification-code', response.status, data); + if (!isMountedRef.current) return; if (!data) { @@ -177,11 +185,10 @@ export default function AuthFormContent() { } if (response.ok && data.success) { - toast({ - title: "验证码已发送", - description: "验证码已发送到您的手机号", - status: "success", - duration: 3000, + // ❌ 移除成功 toast,静默处理 + logger.info('AuthFormContent', '验证码发送成功', { + credential: credential.substring(0, 3) + '****' + credential.substring(7), + dev_code: data.dev_code }); setVerificationCodeSent(true); setCountdown(60); @@ -189,14 +196,10 @@ export default function AuthFormContent() { throw new Error(data.error || '发送验证码失败'); } } catch (error) { - if (isMountedRef.current) { - toast({ - title: "发送验证码失败", - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - }); - } + // ❌ 移除错误 toast,仅 console 输出 + logger.api.error('POST', '/api/auth/send-verification-code', error, { + credential: credential.substring(0, 3) + '****' + credential.substring(7) + }); } finally { if (isMountedRef.current) { setSendingCode(false); @@ -234,18 +237,24 @@ export default function AuthFormContent() { // 构建请求体 const requestBody = { - credential: phone, - verification_code: verificationCode, + credential: phone.trim(), // 添加 trim() 防止空格 + verification_code: verificationCode.trim(), // 添加 trim() 防止空格 login_type: 'phone', }; + logger.api.request('POST', '/api/auth/login-with-code', { + credential: phone.substring(0, 3) + '****' + phone.substring(7), + verification_code: verificationCode.substring(0, 2) + '****', + login_type: 'phone' + }); + // 调用API(根据模式选择不同的endpoint const response = await fetch('/api/auth/login-with-code', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - credentials: 'include', // 必须包含以支持跨域 session cookie + credentials: 'include', body: JSON.stringify(requestBody), }); @@ -255,6 +264,11 @@ export default function AuthFormContent() { const data = await response.json(); + logger.api.response('POST', '/api/auth/login-with-code', response.status, { + ...data, + user: data.user ? { id: data.user.id, phone: data.user.phone } : null + }); + if (!isMountedRef.current) return; if (!data) { @@ -271,13 +285,19 @@ export default function AuthFormContent() { // 更新session await checkSession(); + // ✅ 保留登录成功 toast(关键操作提示) toast({ - title: config.successTitle, + title: data.isNewUser ? '注册成功' : '登录成功', description: config.successDescription, status: "success", duration: 2000, }); + logger.info('AuthFormContent', '登录成功', { + isNewUser: data.isNewUser, + userId: data.user?.id + }); + // 检查是否为新注册用户 if (data.isNewUser) { // 新注册用户,延迟后显示昵称设置引导 @@ -295,15 +315,12 @@ export default function AuthFormContent() { throw new Error(data.error || `${config.errorTitle}`); } } catch (error) { - console.error('Auth error:', error); - if (isMountedRef.current) { - toast({ - title: config.errorTitle, - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - }); - } + // ❌ 移除错误 toast,仅 console 输出 + const { phone, verificationCode } = formData; + logger.error('AuthFormContent', 'handleSubmit', error, { + phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A', + hasVerificationCode: !!verificationCode + }); } finally { if (isMountedRef.current) { setIsLoading(false); diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js index 9566616a..8dc81e1e 100755 --- a/src/contexts/AuthContext.js +++ b/src/contexts/AuthContext.js @@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useToast } from '@chakra-ui/react'; +import { logger } from '../utils/logger'; // 创建认证上下文 const AuthContext = createContext(); @@ -26,7 +27,7 @@ export const AuthProvider = ({ children }) => { // 检查Session状态 const checkSession = async () => { try { - console.log('🔍 检查Session状态...'); + logger.debug('AuthContext', '检查Session状态'); // 创建超时控制器 const controller = new AbortController(); @@ -34,11 +35,11 @@ export const AuthProvider = ({ children }) => { const response = await fetch(`/api/auth/session`, { method: 'GET', - credentials: 'include', // 重要:包含cookie + credentials: 'include', headers: { 'Content-Type': 'application/json', }, - signal: controller.signal // 添加超时信号 + signal: controller.signal }); clearTimeout(timeoutId); @@ -48,7 +49,10 @@ export const AuthProvider = ({ children }) => { } const data = await response.json(); - console.log('📦 Session数据:', data); + logger.debug('AuthContext', 'Session数据', { + isAuthenticated: data.isAuthenticated, + userId: data.user?.id + }); if (data.isAuthenticated && data.user) { setUser(data.user); @@ -58,12 +62,11 @@ export const AuthProvider = ({ children }) => { setIsAuthenticated(false); } } catch (error) { - console.error('❌ Session检查错误:', error); + logger.error('AuthContext', 'checkSession', error); // 网络错误或超时,设置为未登录状态 setUser(null); setIsAuthenticated(false); } finally { - // ⚡ Session 检查完成后,停止加载状态 setIsLoading(false); } }; @@ -97,7 +100,7 @@ export const AuthProvider = ({ children }) => { const login = async (credential, password, loginType = 'email') => { try { setIsLoading(true); - console.log('🔐 开始登录流程:', { credential, loginType }); + logger.debug('AuthContext', '开始登录流程', { credential: credential.substring(0, 3) + '***', loginType }); const formData = new URLSearchParams(); formData.append('password', password); @@ -110,11 +113,9 @@ export const AuthProvider = ({ children }) => { formData.append('username', credential); } - console.log('📤 发送登录请求到:', `/api/auth/login`); - console.log('📝 请求数据:', { - credential, - loginType, - formData: formData.toString() + logger.api.request('POST', '/api/auth/login', { + credential: credential.substring(0, 3) + '***', + loginType }); const response = await fetch(`/api/auth/login`, { @@ -122,24 +123,19 @@ export const AuthProvider = ({ children }) => { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - credentials: 'include', // 包含cookie + credentials: 'include', body: formData }); - console.log('📨 响应状态:', response.status, response.statusText); - console.log('📨 响应头:', Object.fromEntries(response.headers.entries())); - // 获取响应文本,然后尝试解析JSON const responseText = await response.text(); - console.log('📨 响应原始内容:', responseText); let data; try { data = JSON.parse(responseText); - console.log('✅ JSON解析成功:', data); + logger.api.response('POST', '/api/auth/login', response.status, data); } catch (parseError) { - console.error('❌ JSON解析失败:', parseError); - console.error('📄 响应内容:', responseText); + logger.error('AuthContext', 'login', parseError, { responseText: responseText.substring(0, 100) }); throw new Error(`服务器响应格式错误: ${responseText.substring(0, 100)}...`); } @@ -163,17 +159,7 @@ export const AuthProvider = ({ children }) => { return { success: true }; } catch (error) { - console.error('❌ 登录错误:', error); - - // ⚡ 移除toast,让调用者处理错误显示,避免重复toast和并发更新 - // toast({ - // title: "登录失败", - // description: error.message || "请检查您的登录信息", - // status: "error", - // duration: 3000, - // isClosable: true, - // }); - + logger.error('AuthContext', 'login', error, { loginType }); return { success: false, error: error.message }; } finally { setIsLoading(false); @@ -220,18 +206,11 @@ export const AuthProvider = ({ children }) => { return { success: true }; } catch (error) { - console.error('注册错误:', error); - - toast({ - title: "注册失败", - description: error.message || "注册失败,请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); + logger.error('AuthContext', 'register', error); + // ❌ 移除错误 toast,静默失败 return { success: false, error: error.message }; - } finally { + } finally{ setIsLoading(false); } }; @@ -276,16 +255,7 @@ export const AuthProvider = ({ children }) => { return { success: true }; } catch (error) { - console.error('手机注册错误:', error); - - toast({ - title: "注册失败", - description: error.message || "注册失败,请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); - + logger.error('AuthContext', 'registerWithPhone', error, { phone: phone.substring(0, 3) + '****' }); return { success: false, error: error.message }; } finally { setIsLoading(false); @@ -332,16 +302,7 @@ export const AuthProvider = ({ children }) => { return { success: true }; } catch (error) { - console.error('邮箱注册错误:', error); - - toast({ - title: "注册失败", - description: error.message || "注册失败,请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); - + logger.error('AuthContext', 'registerWithEmail', error); return { success: false, error: error.message }; } finally { setIsLoading(false); @@ -356,7 +317,7 @@ export const AuthProvider = ({ children }) => { headers: { 'Content-Type': 'application/json', }, - credentials: 'include', // 必须包含以支持跨域 session cookie + credentials: 'include', body: JSON.stringify({ phone }) }); @@ -366,27 +327,13 @@ export const AuthProvider = ({ children }) => { throw new Error(data.error || '发送失败'); } - toast({ - title: "验证码已发送", - description: "请查收短信", - status: "success", - duration: 3000, - isClosable: true, - }); - + // ❌ 移除成功 toast + logger.info('AuthContext', '验证码已发送', { phone: phone.substring(0, 3) + '****' }); return { success: true }; } catch (error) { - console.error('SMS code error:', error); - - toast({ - title: "发送失败", - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); - + // ❌ 移除错误 toast + logger.error('AuthContext', 'sendSmsCode', error, { phone: phone.substring(0, 3) + '****' }); return { success: false, error: error.message }; } }; @@ -399,7 +346,7 @@ export const AuthProvider = ({ children }) => { headers: { 'Content-Type': 'application/json', }, - credentials: 'include', // 必须包含以支持跨域 session cookie + credentials: 'include', body: JSON.stringify({ email }) }); @@ -409,27 +356,13 @@ export const AuthProvider = ({ children }) => { throw new Error(data.error || '发送失败'); } - toast({ - title: "验证码已发送", - description: "请查收邮件", - status: "success", - duration: 3000, - isClosable: true, - }); - + // ❌ 移除成功 toast + logger.info('AuthContext', '邮箱验证码已发送', { email: email.substring(0, 3) + '***@***' }); return { success: true }; } catch (error) { - console.error('Email code error:', error); - - toast({ - title: "发送失败", - description: error.message || "请稍后重试", - status: "error", - duration: 3000, - isClosable: true, - }); - + // ❌ 移除错误 toast + logger.error('AuthContext', 'sendEmailCode', error); return { success: false, error: error.message }; } }; @@ -447,6 +380,7 @@ export const AuthProvider = ({ children }) => { setUser(null); setIsAuthenticated(false); + // ✅ 保留登出成功 toast(关键操作提示) toast({ title: "已登出", description: "您已成功退出登录", @@ -455,14 +389,11 @@ export const AuthProvider = ({ children }) => { isClosable: true, }); - // 不再跳转,用户留在当前页面 - } catch (error) { - console.error('Logout error:', error); + logger.error('AuthContext', 'logout', error); // 即使API调用失败也清除本地状态 setUser(null); setIsAuthenticated(false); - // 不再跳转,用户留在当前页面 } }; diff --git a/src/mocks/handlers/account.js b/src/mocks/handlers/account.js index 650faaef..fc57a9b4 100644 --- a/src/mocks/handlers/account.js +++ b/src/mocks/handlers/account.js @@ -5,6 +5,133 @@ import { getCurrentUser } from '../data/users'; // 模拟网络延迟(毫秒) const NETWORK_DELAY = 300; +// ==================== Mock 数据 ==================== + +// 模拟自选股数据 +const mockWatchlist = [ + { + id: 1, + stock_code: '000001.SZ', + stock_name: '平安银行', + added_at: '2024-01-15T10:30:00Z', + industry: '银行', + market_cap: 3200000000000 + }, + { + id: 2, + stock_code: '600519.SH', + stock_name: '贵州茅台', + added_at: '2024-01-10T14:20:00Z', + industry: '白酒', + market_cap: 2500000000000 + }, + { + id: 3, + stock_code: '000858.SZ', + stock_name: '五粮液', + added_at: '2024-01-08T09:15:00Z', + industry: '白酒', + market_cap: 800000000000 + } +]; + +// 模拟实时行情数据 +const mockRealtimeQuotes = { + '000001.SZ': { + price: 12.34, + change: 0.56, + change_percent: 4.76, + volume: 123456789, + turnover: 1523456789.12, + high: 12.50, + low: 11.80, + open: 11.90, + prev_close: 11.78, + timestamp: new Date().toISOString() + }, + '600519.SH': { + price: 1680.50, + change: -12.30, + change_percent: -0.73, + volume: 2345678, + turnover: 3945678901.23, + high: 1695.00, + low: 1675.00, + open: 1692.80, + prev_close: 1692.80, + timestamp: new Date().toISOString() + }, + '000858.SZ': { + price: 156.78, + change: 2.34, + change_percent: 1.52, + volume: 45678901, + turnover: 7123456789.45, + high: 158.00, + low: 154.50, + open: 155.00, + prev_close: 154.44, + timestamp: new Date().toISOString() + } +}; + +// 模拟关注的事件 +const mockFollowingEvents = [ + { + id: 1, + title: '央行降准0.5个百分点', + importance: 'high', + followed_at: '2024-01-12T08:00:00Z', + event_date: '2024-01-10T00:00:00Z', + category: '宏观政策' + }, + { + id: 2, + title: 'ChatGPT-5 即将发布', + importance: 'medium', + followed_at: '2024-01-11T15:30:00Z', + event_date: '2024-01-09T00:00:00Z', + category: '科技创新' + } +]; + +// 模拟事件评论 +const mockEventComments = [ + { + id: 1, + event_id: 1, + content: '这次降准对银行股是重大利好,建议关注四大行', + created_at: '2024-01-12T10:30:00Z', + likes: 15, + event_title: '央行降准0.5个百分点' + }, + { + id: 2, + event_id: 2, + content: 'AI 板块又要起飞了,重点关注算力概念股', + created_at: '2024-01-11T16:45:00Z', + likes: 8, + event_title: 'ChatGPT-5 即将发布' + } +]; + +// 模拟订阅信息(当前订阅) +const mockSubscriptionCurrent = { + plan: 'premium', + plan_name: '专业版', + expires_at: '2025-12-31T23:59:59Z', + auto_renew: true, + features: [ + '无限事件查看', + '实时行情推送', + '专业分析报告', + '优先客服支持' + ], + price: 299, + currency: 'CNY', + billing_cycle: 'monthly' +}; + export const accountHandlers = [ // ==================== 用户资料管理 ==================== @@ -244,5 +371,172 @@ export const accountHandlers = [ permissions } }); - }) + }), + + // ==================== 自选股管理 ==================== + + // 6. 获取自选股列表 + http.get('/api/account/watchlist', async () => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + console.log('[Mock] 获取自选股列表'); + + return HttpResponse.json({ + success: true, + data: mockWatchlist + }); + }), + + // 7. 获取自选股实时行情 + http.get('/api/account/watchlist/realtime', async () => { + await delay(200); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + console.log('[Mock] 获取自选股实时行情'); + + return HttpResponse.json({ + success: true, + data: mockRealtimeQuotes + }); + }), + + // 8. 添加自选股 + http.post('/api/account/watchlist/add', async ({ request }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + const body = await request.json(); + const { stock_code, stock_name } = body; + + console.log('[Mock] 添加自选股:', { stock_code, stock_name }); + + const newItem = { + id: mockWatchlist.length + 1, + stock_code, + stock_name, + added_at: new Date().toISOString(), + industry: '未知', + market_cap: 0 + }; + + mockWatchlist.push(newItem); + + return HttpResponse.json({ + success: true, + message: '添加成功', + data: newItem + }); + }), + + // 9. 删除自选股 + http.delete('/api/account/watchlist/:id', async ({ params }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + const { id } = params; + console.log('[Mock] 删除自选股:', id); + + const index = mockWatchlist.findIndex(item => item.id === parseInt(id)); + if (index !== -1) { + mockWatchlist.splice(index, 1); + } + + return HttpResponse.json({ + success: true, + message: '删除成功' + }); + }), + + // ==================== 事件关注管理 ==================== + + // 10. 获取关注的事件 + http.get('/api/account/events/following', async () => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + console.log('[Mock] 获取关注的事件'); + + return HttpResponse.json({ + success: true, + data: mockFollowingEvents + }); + }), + + // 11. 获取事件评论 + http.get('/api/account/events/comments', async () => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + console.log('[Mock] 获取事件评论'); + + return HttpResponse.json({ + success: true, + data: mockEventComments + }); + }), + + // ==================== 订阅信息 ==================== + + // 12. 获取当前订阅信息 + http.get('/api/subscription/current', async () => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + if (!currentUser) { + return HttpResponse.json( + { success: false, error: '未登录' }, + { status: 401 } + ); + } + + console.log('[Mock] 获取当前订阅信息'); + + return HttpResponse.json({ + success: true, + data: mockSubscriptionCurrent + }); + }), ]; diff --git a/src/services/authService.js b/src/services/authService.js index b7e55a8f..86d4c48d 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -3,6 +3,8 @@ * 认证服务层 - 处理所有认证相关的 API 调用 */ +import { logger } from '../utils/logger'; + const isProduction = process.env.NODE_ENV === 'production'; const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; @@ -13,6 +15,12 @@ const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; * @returns {Promise} - 响应数据 */ const apiRequest = async (url, options = {}) => { + const method = options.method || 'GET'; + const requestData = options.body ? JSON.parse(options.body) : null; + + // 记录请求日志 + logger.api.request(method, url, requestData); + try { const response = await fetch(`${API_BASE_URL}${url}`, { ...options, @@ -20,7 +28,7 @@ const apiRequest = async (url, options = {}) => { 'Content-Type': 'application/json', ...options.headers, }, - credentials: 'include', // 包含 cookies + credentials: 'include', }); // 检查响应是否为 JSON @@ -33,8 +41,10 @@ const apiRequest = async (url, options = {}) => { try { const errorData = await response.json(); errorMessage = errorData.error || errorData.message || errorMessage; + // 记录错误响应 + logger.api.error(method, url, new Error(errorMessage), requestData); } catch (parseError) { - console.warn('Failed to parse error response as JSON'); + logger.warn('authService', 'Failed to parse error response as JSON', { url }); } } throw new Error(errorMessage); @@ -43,16 +53,21 @@ const apiRequest = async (url, options = {}) => { // 安全地解析 JSON 响应 if (isJson) { try { - return await response.json(); + const data = await response.json(); + // 记录成功响应 + logger.api.response(method, url, response.status, data); + return data; } catch (parseError) { - console.error('Failed to parse response as JSON:', parseError); + logger.error('authService', 'apiRequest', parseError, { url }); throw new Error('服务器响应格式错误'); } } else { throw new Error('服务器响应不是 JSON 格式'); } } catch (error) { - console.error(`Auth API request failed for ${url}:`, error); + // ❌ 移除 console.error,使用 logger + logger.api.error(method, url, error, requestData); + // 如果是网络错误,提供更友好的提示 if (error.message === 'Failed to fetch' || error.name === 'TypeError') { throw new Error('网络连接失败,请检查网络设置'); diff --git a/src/services/eventService.js b/src/services/eventService.js index f035e3d1..6f365d70 100755 --- a/src/services/eventService.js +++ b/src/services/eventService.js @@ -1,30 +1,36 @@ // src/services/eventService.js -const apiRequest = async (url, options = {}) => { - try { - console.log(`Making API request to: ${url}`); +import { logger } from '../utils/logger'; +const apiRequest = async (url, options = {}) => { + const method = options.method || 'GET'; + const requestData = options.body ? JSON.parse(options.body) : null; + + logger.api.request(method, url, requestData); + + try { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, - credentials: 'include', // 包含 cookies,以便后端识别登录状态 + credentials: 'include', }); if (!response.ok) { const errorText = await response.text(); - console.error(`API request failed: ${response.status} - ${errorText}`); - throw new Error(`HTTP error! status: ${response.status}`); + const error = new Error(`HTTP error! status: ${response.status}`); + logger.api.error(method, url, error, { errorText, ...requestData }); + throw error; } const data = await response.json(); - console.log(`API response from ${url}:`, data); + logger.api.response(method, url, response.status, data); return data; } catch (error) { - console.error(`API request failed for ${url}:`, error); + logger.api.error(method, url, error, requestData); throw error; } }; @@ -160,7 +166,7 @@ export const eventService = { try { return await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=${page}&per_page=${perPage}`); } catch (error) { - console.error('获取帖子失败:', error); + logger.error('eventService', 'getPosts', error, { eventId, sortType, page }); return { success: false, data: [], pagination: {} }; } }, @@ -172,7 +178,7 @@ export const eventService = { body: JSON.stringify(postData) }); } catch (error) { - console.error('创建帖子失败:', error); + logger.error('eventService', 'createPost', error, { eventId }); return { success: false, message: '创建帖子失败' }; } }, @@ -183,7 +189,7 @@ export const eventService = { method: 'DELETE' }); } catch (error) { - console.error('删除帖子失败:', error); + logger.error('eventService', 'deletePost', error, { postId }); return { success: false, message: '删除帖子失败' }; } }, @@ -194,7 +200,7 @@ export const eventService = { method: 'POST' }); } catch (error) { - console.error('点赞失败:', error); + logger.error('eventService', 'likePost', error, { postId }); return { success: false, message: '点赞失败' }; } }, @@ -204,7 +210,7 @@ export const eventService = { try { return await apiRequest(`/api/posts/${postId}/comments?sort=${sortType}`); } catch (error) { - console.error('获取评论失败:', error); + logger.error('eventService', 'getPostComments', error, { postId, sortType }); return { success: false, data: [] }; } }, @@ -216,7 +222,7 @@ export const eventService = { body: JSON.stringify(commentData) }); } catch (error) { - console.error('添加评论失败:', error); + logger.error('eventService', 'addPostComment', error, { postId }); return { success: false, message: '添加评论失败' }; } }, @@ -227,7 +233,7 @@ export const eventService = { method: 'DELETE' }); } catch (error) { - console.error('删除评论失败:', error); + logger.error('eventService', 'deleteComment', error, { commentId }); return { success: false, message: '删除评论失败' }; } }, @@ -238,33 +244,31 @@ export const eventService = { method: 'POST' }); } catch (error) { - console.error('点赞失败:', error); + logger.error('eventService', 'likeComment', error, { commentId }); return { success: false, message: '点赞失败' }; } }, // 兼容旧版本的评论API getComments: async (eventId, sortType = 'latest') => { - console.warn('getComments 已废弃,请使用 getPosts'); - // 直接调用 getPosts 的实现,避免循环引用 + logger.warn('eventService', 'getComments 已废弃,请使用 getPosts'); try { return await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=1&per_page=20`); } catch (error) { - console.error('获取帖子失败:', error); + logger.error('eventService', 'getComments', error, { eventId, sortType }); return { success: false, data: [], pagination: {} }; } }, addComment: async (eventId, commentData) => { - console.warn('addComment 已废弃,请使用 createPost'); - // 直接调用 createPost 的实现,避免循环引用 + logger.warn('eventService', 'addComment 已废弃,请使用 createPost'); try { return await apiRequest(`/api/events/${eventId}/posts`, { method: 'POST', body: JSON.stringify(commentData) }); } catch (error) { - console.error('创建帖子失败:', error); + logger.error('eventService', 'addComment', error, { eventId }); return { success: false, message: '创建帖子失败' }; } }, @@ -281,26 +285,26 @@ export const stockService = { requestBody.event_time = eventTime; } - console.log(`获取股票报价,请求体:`, requestBody); + logger.debug('stockService', '获取股票报价', requestBody); const response = await apiRequest(`/api/stock/quotes`, { method: 'POST', body: JSON.stringify(requestBody) }); - console.log('股票报价原始响应:', response); - console.log('response.success:', response.success); - console.log('response.data:', response.data); + logger.debug('stockService', '股票报价响应', { + success: response.success, + dataKeys: response.data ? Object.keys(response.data) : [] + }); if (response.success && response.data) { - console.log('返回股票报价数据:', response.data); return response.data; } else { - console.warn('股票报价响应格式异常:', response); + logger.warn('stockService', '股票报价响应格式异常', response); return {}; } } catch (error) { - console.error('获取股票报价失败:', error); + logger.error('stockService', 'getQuotes', error, { codes, eventTime }); throw error; } }, @@ -317,18 +321,18 @@ export const stockService = { } const url = `/api/stock/${stockCode}/kline?${params.toString()}`; - console.log(`获取K线数据: ${url}`); + logger.debug('stockService', '获取K线数据', { stockCode, chartType, eventTime }); const response = await apiRequest(url); if (response.error) { - console.warn('K线数据响应包含错误:', response.error); + logger.warn('stockService', 'K线数据响应包含错误', response.error); return response; } return response; } catch (error) { - console.error('获取K线数据失败:', error); + logger.error('stockService', 'getKlineData', error, { stockCode, chartType }); throw error; } }, @@ -343,21 +347,10 @@ export const stockService = { node_level: nodeInfo.level }); - const response = await fetch(`/api/events/${eventId}/sankey-node-detail?${params}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return data; + const url = `/api/events/${eventId}/sankey-node-detail?${params}`; + return await apiRequest(url); } catch (error) { - console.error('Error fetching sankey node detail:', error); + logger.error('stockService', 'getSankeyNodeDetail', error, { eventId, nodeInfo }); throw error; } }, @@ -382,11 +375,11 @@ export const indexService = { } const url = `/api/index/${indexCode}/kline?${params.toString()}`; - console.log(`获取指数K线数据: ${url}`); + logger.debug('indexService', '获取指数K线数据', { indexCode, chartType, eventTime }); const response = await apiRequest(url); return response; } catch (error) { - console.error('获取指数K线数据失败:', error); + logger.error('indexService', 'getKlineData', error, { indexCode, chartType }); throw error; } }, diff --git a/src/utils/axiosConfig.js b/src/utils/axiosConfig.js new file mode 100644 index 00000000..929de312 --- /dev/null +++ b/src/utils/axiosConfig.js @@ -0,0 +1,64 @@ +// src/utils/axiosConfig.js +// Axios 全局配置和拦截器 + +import axios from 'axios'; +import { logger } from './logger'; + +// 判断当前是否是生产环境 +const isProduction = process.env.NODE_ENV === 'production'; + +// 配置基础 URL +const API_BASE_URL = isProduction ? '' : process.env.REACT_APP_API_URL; + +// 配置 axios 默认值 +axios.defaults.baseURL = API_BASE_URL; +axios.defaults.withCredentials = true; +axios.defaults.headers.common['Content-Type'] = 'application/json'; + +/** + * 请求拦截器 + * 自动记录所有请求日志 + */ +axios.interceptors.request.use( + (config) => { + const method = config.method?.toUpperCase() || 'GET'; + const url = config.url || ''; + const data = config.data || config.params || null; + + logger.api.request(method, url, data); + + return config; + }, + (error) => { + logger.api.error('REQUEST', 'Interceptor', error); + return Promise.reject(error); + } +); + +/** + * 响应拦截器 + * 自动记录所有响应/错误日志 + */ +axios.interceptors.response.use( + (response) => { + const method = response.config.method?.toUpperCase() || 'GET'; + const url = response.config.url || ''; + const status = response.status; + const data = response.data; + + logger.api.response(method, url, status, data); + + return response; + }, + (error) => { + const method = error.config?.method?.toUpperCase() || 'UNKNOWN'; + const url = error.config?.url || 'UNKNOWN'; + const requestData = error.config?.data || error.config?.params || null; + + logger.api.error(method, url, error, requestData); + + return Promise.reject(error); + } +); + +export default axios; diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 00000000..10c24644 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,144 @@ +// src/utils/logger.js +// 统一日志工具 + +const isDevelopment = process.env.NODE_ENV === 'development'; + +/** + * 统一日志工具 + * 开发环境:输出详细日志 + * 生产环境:仅输出错误日志 + */ +export const logger = { + /** + * API 相关日志 + */ + api: { + /** + * 记录 API 请求 + * @param {string} method - 请求方法 (GET, POST, etc.) + * @param {string} url - 请求 URL + * @param {object} data - 请求参数/body + */ + request: (method, url, data = null) => { + if (isDevelopment) { + console.group(`🌐 API Request: ${method} ${url}`); + console.log('Timestamp:', new Date().toISOString()); + if (data) console.log('Data:', data); + console.groupEnd(); + } + }, + + /** + * 记录 API 响应成功 + * @param {string} method - 请求方法 + * @param {string} url - 请求 URL + * @param {number} status - HTTP 状态码 + * @param {any} data - 响应数据 + */ + response: (method, url, status, data) => { + if (isDevelopment) { + console.group(`✅ API Response: ${method} ${url}`); + console.log('Status:', status); + console.log('Data:', data); + console.log('Timestamp:', new Date().toISOString()); + console.groupEnd(); + } + }, + + /** + * 记录 API 错误 + * @param {string} method - 请求方法 + * @param {string} url - 请求 URL + * @param {Error|any} error - 错误对象 + * @param {object} requestData - 请求参数(可选) + */ + error: (method, url, error, requestData = null) => { + console.group(`❌ API Error: ${method} ${url}`); + console.error('Error:', error); + console.error('Message:', error?.message || error); + if (error?.response) { + console.error('Response Status:', error.response.status); + console.error('Response Data:', error.response.data); + } + if (requestData) console.error('Request Data:', requestData); + console.error('Timestamp:', new Date().toISOString()); + if (error?.stack) console.error('Stack:', error.stack); + console.groupEnd(); + } + }, + + /** + * 组件错误日志 + * @param {string} component - 组件名称 + * @param {string} method - 方法名称 + * @param {Error|any} error - 错误对象 + * @param {object} context - 上下文信息(可选) + */ + error: (component, method, error, context = {}) => { + console.group(`🔴 Error in ${component}.${method}`); + console.error('Error:', error); + console.error('Message:', error?.message || error); + if (Object.keys(context).length > 0) { + console.error('Context:', context); + } + console.error('Timestamp:', new Date().toISOString()); + if (error?.stack) console.error('Stack:', error.stack); + console.groupEnd(); + }, + + /** + * 警告日志 + * @param {string} component - 组件名称 + * @param {string} message - 警告信息 + * @param {object} data - 相关数据(可选) + */ + warn: (component, message, data = {}) => { + if (isDevelopment) { + console.group(`⚠️ Warning: ${component}`); + console.warn('Message:', message); + if (Object.keys(data).length > 0) { + console.warn('Data:', data); + } + console.warn('Timestamp:', new Date().toISOString()); + console.groupEnd(); + } + }, + + /** + * 调试日志(仅开发环境) + * @param {string} component - 组件名称 + * @param {string} message - 调试信息 + * @param {object} data - 相关数据(可选) + */ + debug: (component, message, data = {}) => { + if (isDevelopment) { + console.group(`🐛 Debug: ${component}`); + console.log('Message:', message); + if (Object.keys(data).length > 0) { + console.log('Data:', data); + } + console.log('Timestamp:', new Date().toISOString()); + console.groupEnd(); + } + }, + + /** + * 信息日志(仅开发环境) + * @param {string} component - 组件名称 + * @param {string} message - 信息内容 + * @param {object} data - 相关数据(可选) + */ + info: (component, message, data = {}) => { + if (isDevelopment) { + console.group(`ℹ️ Info: ${component}`); + console.log('Message:', message); + if (Object.keys(data).length > 0) { + console.log('Data:', data); + } + console.log('Timestamp:', new Date().toISOString()); + console.groupEnd(); + } + } +}; + +export default logger; diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 4381a20f..f57e10d3 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -16,7 +16,6 @@ import { HStack, Badge, Spinner, - useToast, Flex, Tag, TagLabel, @@ -71,6 +70,7 @@ import HotEvents from './components/HotEvents'; import ImportanceLegend from './components/ImportanceLegend'; import InvestmentCalendar from './components/InvestmentCalendar'; import { eventService } from '../../services/eventService'; +import { logger } from '../../utils/logger'; // 导航栏已由 MainLayout 提供,无需在此导入 @@ -86,8 +86,7 @@ const filterLabelMap = { const Community = () => { const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); - const toast = useToast(); - + // Chakra UI hooks const bgColor = useColorModeValue('gray.50', 'gray.900'); const cardBg = useColorModeValue('white', 'gray.800'); @@ -161,18 +160,15 @@ const Community = () => { setLastUpdateTime(new Date()); } } catch (error) { - console.error('Failed to load events:', error); - toast({ - title: '加载失败', - description: '无法加载事件列表', - status: 'error', - duration: 3000, - isClosable: true, + // ❌ 移除 toast,仅 console 输出 + logger.error('Community', 'loadEvents', error, { + page, + filters: getFiltersFromUrl() }); } finally { setLoading(false); } - }, [getFiltersFromUrl, pagination.pageSize, toast]); + }, [getFiltersFromUrl, pagination.pageSize]); // ✅ 移除 toast 依赖 // 加载热门关键词 const loadPopularKeywords = useCallback(async () => { diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js index 7c3dbf5b..6d80548f 100644 --- a/src/views/Dashboard/Center.js +++ b/src/views/Dashboard/Center.js @@ -1,5 +1,6 @@ // src/views/Dashboard/Center.js import React, { useEffect, useState, useCallback } from 'react'; +import { logger } from '../../utils/logger'; import { Box, Flex, @@ -111,19 +112,16 @@ export default function CenterDashboard() { if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []); if (js.success) setSubscriptionInfo(js.data); } catch (err) { - console.warn('加载个人中心数据失败', err); - toast({ - title: '数据加载失败', - description: '请检查网络连接后重试', - status: 'error', - duration: 3000, - isClosable: true, + // ❌ 移除 toast,仅 console 输出 + logger.error('Center', 'loadData', err, { + userId: user?.id, + timestamp: new Date().toISOString() }); } finally { setLoading(false); setRefreshing(false); } - }, [user, toast]); + }, [user]); // ✅ 移除 toast 依赖 // 加载实时行情 const loadRealtimeQuotes = useCallback(async () => {