From 42acc8fac099d64cd1bd7545fbb655b07263bfe5 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 16 Oct 2025 17:50:07 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E6=BF=80=E6=B4=BB=E7=8A=B6=E6=80=81=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 React Router 的 useLocation 钩子检测当前路径 - 为顶级导航菜单添加激活状态样式(蓝色背景 + 底部边框) - 为下拉菜单项添加激活状态样式(蓝色背景 + 左侧边框) - 支持桌面端和移动端抽屉菜单 - 解决用户无法感知当前导航位置的 UX 问题 激活路由映射: - 高频跟踪: /community, /concepts - 行情复盘: /limit-analyse, /stocks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/Navbars/HomeNavbar.js | 67 ++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index a106fb07..f9bcf1a0 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -34,7 +34,7 @@ import { } from '@chakra-ui/react'; import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; import { FiStar, FiCalendar } from 'react-icons/fi'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { useAuthModal } from '../../contexts/AuthModalContext'; @@ -45,12 +45,28 @@ import { useAuthModal } from '../../contexts/AuthModalContext'; */ const NavItems = ({ isAuthenticated, user }) => { const navigate = useNavigate(); + const location = useLocation(); + + // 辅助函数:判断导航项是否激活 + const isActive = (paths) => { + return paths.some(path => location.pathname.includes(path)); + }; if (isAuthenticated && user) { return ( - }> + } + bg={isActive(['/community', '/concepts']) ? 'blue.50' : 'transparent'} + color={isActive(['/community', '/concepts']) ? 'blue.600' : 'inherit'} + fontWeight={isActive(['/community', '/concepts']) ? 'bold' : 'normal'} + borderBottom={isActive(['/community', '/concepts']) ? '2px solid' : 'none'} + borderColor="blue.600" + _hover={{ bg: isActive(['/community', '/concepts']) ? 'blue.100' : 'gray.50' }} + > 高频跟踪 @@ -62,6 +78,10 @@ const NavItems = ({ isAuthenticated, user }) => { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/community') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/community') ? 'bold' : 'normal'} > 新闻催化分析 @@ -78,6 +98,10 @@ const NavItems = ({ isAuthenticated, user }) => { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/concepts') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/concepts') ? 'bold' : 'normal'} > 概念中心 @@ -89,7 +113,17 @@ const NavItems = ({ isAuthenticated, user }) => { - }> + } + bg={isActive(['/limit-analyse', '/stocks']) ? 'blue.50' : 'transparent'} + color={isActive(['/limit-analyse', '/stocks']) ? 'blue.600' : 'inherit'} + fontWeight={isActive(['/limit-analyse', '/stocks']) ? 'bold' : 'normal'} + borderBottom={isActive(['/limit-analyse', '/stocks']) ? '2px solid' : 'none'} + borderColor="blue.600" + _hover={{ bg: isActive(['/limit-analyse', '/stocks']) ? 'blue.100' : 'gray.50' }} + > 行情复盘 @@ -101,6 +135,10 @@ const NavItems = ({ isAuthenticated, user }) => { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/limit-analyse') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/limit-analyse') ? 'bold' : 'normal'} > 涨停分析 @@ -114,6 +152,10 @@ const NavItems = ({ isAuthenticated, user }) => { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/stocks') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/stocks') ? 'bold' : 'normal'} > 个股中心 @@ -809,6 +851,9 @@ export default function HomeNavbar() { cursor="pointer" color="blue.500" fontWeight="bold" + bg={location.pathname === '/home' ? 'blue.50' : 'transparent'} + borderLeft={location.pathname === '/home' ? '3px solid' : 'none'} + borderColor="blue.600" > 🏠 首页 @@ -826,6 +871,10 @@ export default function HomeNavbar() { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/community') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/community') ? 'bold' : 'normal'} > 新闻催化分析 @@ -845,6 +894,10 @@ export default function HomeNavbar() { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/concepts') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/concepts') ? 'bold' : 'normal'} > 概念中心 @@ -867,6 +920,10 @@ export default function HomeNavbar() { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/limit-analyse') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/limit-analyse') ? 'bold' : 'normal'} > 涨停分析 @@ -883,6 +940,10 @@ export default function HomeNavbar() { borderRadius="md" _hover={{ bg: 'gray.100' }} cursor="pointer" + bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/stocks') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/stocks') ? 'bold' : 'normal'} > 个股中心 From bc407d2a35dd222c06835153bac5119e4dae3e0a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 16 Oct 2025 17:52:56 +0800 Subject: [PATCH 2/8] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E7=B3=BB=E7=BB=9F=E5=AE=8C=E6=95=B4=E6=8C=87=E5=8D=97?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 详细的认证系统架构说明 - 三种认证方式的实现细节(手机验证码、微信PC、微信H5) - API 接口文档 - 组件架构说明 - 调试和故障排查指南 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- AUTHENTICATION_SYSTEM_GUIDE.md | 1879 ++++++++++++++++++++++++++++++++ 1 file changed, 1879 insertions(+) create mode 100644 AUTHENTICATION_SYSTEM_GUIDE.md diff --git a/AUTHENTICATION_SYSTEM_GUIDE.md b/AUTHENTICATION_SYSTEM_GUIDE.md new file mode 100644 index 00000000..c01738c4 --- /dev/null +++ b/AUTHENTICATION_SYSTEM_GUIDE.md @@ -0,0 +1,1879 @@ +# 价值前沿认证系统完整文档 + +> **版本**: 2.0 +> **更新日期**: 2025-01-16 +> **作者**: Claude Code +> **适用范围**: 前端 React + 后端 Flask + +--- + +## 📖 目录 + +1. [系统架构概览](#1-系统架构概览) +2. [认证流程详解](#2-认证流程详解) + - [2.1 手机验证码登录](#21-手机验证码登录) + - [2.2 微信PC扫码登录](#22-微信pc扫码登录) + - [2.3 微信H5网页授权](#23-微信h5网页授权) +3. [路由配置与跳转逻辑](#3-路由配置与跳转逻辑) +4. [API接口文档](#4-api接口文档) +5. [前端组件架构](#5-前端组件架构) +6. [状态管理机制](#6-状态管理机制) +7. [Session持久化](#7-session持久化) +8. [错误处理策略](#8-错误处理策略) +9. [安全机制](#9-安全机制) +10. [调试指南](#10-调试指南) + +--- + +## 1. 系统架构概览 + +### 1.1 技术栈 + +**前端**: +- React 18.3.1 +- Chakra UI 2.8.2 +- React Router 6.x +- Context API (状态管理) + +**后端**: +- Flask (Python) +- Session-based Authentication +- HttpOnly Cookies +- Flask-Session + +**认证方式**: +1. **手机验证码登录** (短信验证码) +2. **微信PC扫码登录** (二维码扫码) +3. **微信H5网页授权** (移动端跳转授权) + +### 1.2 架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户界面层 (UI) │ +│ ┌───────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ HomeNavbar │ │ AuthModal │ │ProtectedRoute│ │ +│ │ (登录按钮) │ │ (认证弹窗) │ │ (路由保护) │ │ +│ └───────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 状态管理层 (Context) │ +│ ┌──────────────────────┐ ┌───────────────────────┐ │ +│ │ AuthContext │ │ AuthModalContext │ │ +│ │ - user │ │ - isAuthModalOpen │ │ +│ │ - isAuthenticated │ │ - openAuthModal() │ │ +│ │ - checkSession() │ │ - handleLoginSuccess()│ │ +│ └──────────────────────┘ └───────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 业务逻辑层 (Components) │ +│ ┌──────────────────┐ ┌─────────────┐ ┌────────────┐ │ +│ │ AuthFormContent │ │WechatRegister│ │VerifyCodeInput│ │ +│ │ (认证表单) │ │ (微信扫码) │ │ (验证码输入)│ │ +│ └──────────────────┘ └─────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 服务层 (API Service) │ +│ ┌──────────────┐ │ +│ │ authService │ │ +│ │ - getWechatQRCode() │ +│ │ - checkWechatStatus() │ +│ │ - loginWithWechat() │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 后端 API (Flask) │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ /api/auth/session - Session检查 │ │ +│ │ /api/auth/send-verification-code - 发送验证码 │ │ +│ │ /api/auth/login-with-code - 验证码登录 │ │ +│ │ /api/auth/wechat/qrcode - 获取微信二维码 │ │ +│ │ /api/auth/wechat/check - 检查扫码状态 │ │ +│ │ /api/auth/login/wechat - 微信登录 │ │ +│ │ /api/auth/wechat/h5-auth - 微信H5授权 │ │ +│ │ /api/auth/wechat/h5-callback - 微信H5回调 │ │ +│ │ /api/auth/logout - 退出登录 │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 数据存储层 (Database) │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ User表 │ │ Session存储 │ │ 验证码缓存 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 认证流程详解 + +### 2.1 手机验证码登录 + +**适用场景**: PC端和移动端通用登录方式 + +#### 流程图 + +``` +用户点击"登录/注册"按钮 + ↓ +打开 AuthModal 弹窗 + ↓ +输入手机号 → 点击"获取验证码" + ↓ +前端: AuthFormContent.sendVerificationCode() + ↓ +POST /api/auth/send-verification-code + Body: { + credential: "13800138000", + type: "phone", + purpose: "login" + } + ↓ +后端: 生成6位验证码 → 发送短信 → 存入Session/Redis (5分钟有效期) + ↓ +返回: { success: true } + ↓ +前端显示: "验证码已发送" + 60秒倒计时 + ↓ +用户输入验证码 → 点击"登录/注册" + ↓ +前端: AuthFormContent.handleSubmit() + ↓ +POST /api/auth/login-with-code + Body: { + credential: "13800138000", + verification_code: "123456", + login_type: "phone" + } + ↓ +后端验证: + 1. 验证码是否存在 ✓ + 2. 验证码是否过期 ✓ + 3. 验证码是否匹配 ✓ + 4. 查询用户是否存在 + - 存在: 登录 (isNewUser: false) + - 不存在: 自动注册 (isNewUser: true) + ↓ +设置 Session Cookie (HttpOnly) + ↓ +返回: { + success: true, + isNewUser: true/false, + user: { id, phone, nickname, ... } +} + ↓ +前端: + 1. checkSession() 更新全局状态 + 2. 显示成功提示 + 3. 如果 isNewUser=true → 显示昵称设置引导 + 4. 关闭弹窗,留在当前页面 + ↓ +登录完成 ✅ +``` + +#### 关键代码位置 + +**前端**: +- `src/components/Auth/AuthFormContent.js:130` - 发送验证码 +- `src/components/Auth/AuthFormContent.js:207` - 提交登录 +- `src/components/Auth/VerificationCodeInput.js` - 验证码输入组件 + +**后端**: +- `app.py:1826` - POST /api/auth/send-verification-code +- `app.py:1884` - POST /api/auth/login-with-code + +--- + +### 2.2 微信PC扫码登录 + +**适用场景**: PC端桌面浏览器 + +#### 流程图 + +``` +用户打开登录弹窗 (桌面端) + ↓ +右侧显示微信二维码区域 (WechatRegister组件) + ↓ +初始状态: 灰色二维码图标 + "获取二维码"按钮 + ↓ +用户点击"获取二维码" + ↓ +前端: WechatRegister.getWechatQRCode() + ↓ +GET /api/auth/wechat/qrcode + ↓ +后端: + 1. 生成唯一 session_id (UUID) + 2. 构建微信开放平台授权URL + 3. 存储到临时状态 (5分钟有效期) + ↓ +返回: { + code: 0, + data: { + auth_url: "https://open.weixin.qq.com/connect/qrconnect?...", + session_id: "uuid-xxxxx" + } +} + ↓ +前端: + 1. 在 iframe 中显示微信二维码 + 2. 启动轮询: 每2秒检查扫码状态 + 3. 启动备用轮询: 每3秒检查 (防止丢失) + 4. 设置超时: 5分钟后二维码过期 + ↓ +【轮询检查】 +POST /api/auth/wechat/check + Body: { session_id: "uuid-xxxxx" } + ↓ +后端返回状态: + - waiting: 等待扫码 + - scanned: 已扫码,等待确认 + - authorized: 已授权 + - login_success: 登录成功 (老用户) + - register_success: 注册成功 (新用户) + - expired: 二维码过期 + ↓ +如果状态 = login_success / register_success: + ↓ +前端: WechatRegister.handleLoginSuccess() + ↓ +POST /api/auth/login/wechat + Body: { session_id: "uuid-xxxxx" } + ↓ +后端: + 1. 从临时状态获取微信用户信息 + 2. 查询数据库是否存在该微信用户 + - 存在: 登录 + - 不存在: 自动注册 + 3. 设置 Session Cookie + ↓ +返回: { + success: true, + user: { id, nickname, avatar_url, ... }, + token: "optional-token" +} + ↓ +前端: + 1. 停止轮询 + 2. checkSession() 更新状态 + 3. 显示成功提示 + 4. 1秒后跳转 /home + ↓ +登录完成 ✅ +``` + +#### 关键代码位置 + +**前端**: +- `src/components/Auth/WechatRegister.js:199` - 获取二维码 +- `src/components/Auth/WechatRegister.js:120` - 检查扫码状态 +- `src/components/Auth/WechatRegister.js:85` - 登录成功处理 +- `src/services/authService.js:69` - API服务 + +**后端**: +- `app.py:2487` - GET /api/auth/wechat/qrcode +- `app.py:2560` - POST /api/auth/wechat/check +- `app.py:2743` - POST /api/auth/login/wechat + +--- + +### 2.3 微信H5网页授权 + +**适用场景**: 移动端浏览器中打开 + +#### 流程图 + +``` +移动端用户点击验证码输入框下方的微信图标 + ↓ +前端: AuthFormContent.handleWechatH5Login() + ↓ +构建回调URL: https://yourdomain.com/home/wechat-callback + ↓ +POST /api/auth/wechat/h5-auth + Body: { redirect_url: "https://..." } + ↓ +后端: + 1. 构建微信网页授权URL (snsapi_userinfo) + 2. 生成 state 参数防止CSRF + ↓ +返回: { + auth_url: "https://open.weixin.qq.com/connect/oauth2/authorize?..." +} + ↓ +前端: 延迟500ms后跳转到微信授权页面 + ↓ +window.location.href = auth_url + ↓ +【用户在微信授权页面确认】 + ↓ +微信回调: https://yourdomain.com/home/wechat-callback?code=xxx&state=yyy + ↓ +前端: WechatCallback 组件接收回调 + ↓ +POST /api/auth/wechat/h5-callback + Body: { code: "xxx", state: "yyy" } + ↓ +后端: + 1. 验证 state 参数 + 2. 使用 code 换取 access_token + 3. 使用 access_token 获取微信用户信息 + 4. 查询数据库 + - 存在: 登录 + - 不存在: 自动注册 + 5. 设置 Session Cookie + ↓ +返回: { + success: true, + user: { ... }, + token: "optional" +} + ↓ +前端: + 1. 存储 token (可选) + 2. checkSession() 更新状态 + 3. 显示"登录成功" + 4. 1.5秒后跳转 /home + ↓ +登录完成 ✅ +``` + +#### 关键代码位置 + +**前端**: +- `src/components/Auth/AuthFormContent.js:308` - 发起H5授权 +- `src/views/Pages/WechatCallback.js:34` - 处理回调 +- `src/services/authService.js:78` - H5授权API +- `src/services/authService.js:91` - H5回调API + +**后端**: +- `app.py:2487+` - POST /api/auth/wechat/h5-auth (需确认) +- `app.py:2610+` - POST /api/auth/wechat/h5-callback (需确认) + +--- + +## 3. 路由配置与跳转逻辑 + +### 3.1 路由结构 + +```javascript +// src/App.js + + + {/* 公开路由 - 无需登录 */} + } /> + } /> + + {/* 受保护路由 - 需要登录 */} + } /> + + + +// src/layouts/Home.js (公开路由) + + } /> + } /> + } /> + } /> + + {/* 需要登录的页面 */} + + + + } /> + + + + } /> + +``` + +### 3.2 路由保护机制 + +**ProtectedRoute 组件** (`src/components/ProtectedRoute.js`) + +```javascript +const ProtectedRoute = ({ children }) => { + const { isAuthenticated, isLoading, user } = useAuth(); + const { openAuthModal, isAuthModalOpen } = useAuthModal(); + + // 未登录时自动弹出认证窗口 + useEffect(() => { + if (!isLoading && !isAuthenticated && !user && !isAuthModalOpen) { + openAuthModal(currentPath); // 记录当前路径 + } + }, [isAuthenticated, user, isLoading]); + + // 加载中: 显示 Spinner + if (isLoading) { + return ; + } + + // 未登录: 显示页面 + 自动打开弹窗 (非阻断式) + // 已登录: 正常显示页面 + return children; +}; +``` + +**特点**: +- ✅ 非阻断式保护 (弹窗而非重定向) +- ✅ 记录原始路径 (登录后可返回) +- ✅ 避免白屏 (先显示页面骨架) + +### 3.3 跳转逻辑总结 + +| 场景 | 触发位置 | 跳转目标 | 说明 | +|------|---------|---------|------| +| 未登录访问受保护页面 | ProtectedRoute | 留在当前页 + 弹窗 | 非阻断式 | +| 点击导航栏登录按钮 | HomeNavbar:740 | 打开 AuthModal | 弹窗形式 | +| 手机验证码登录成功 | AuthFormContent:284 | 留在当前页 | 关闭弹窗即可 | +| 微信扫码登录成功 | WechatRegister:106 | 跳转 /home | 1秒延迟 | +| 微信H5授权成功 | WechatCallback:69 | 跳转 /home | 1.5秒延迟 | +| 用户点击退出登录 | HomeNavbar:227 | 跳转 /home | 清除Session | +| 关闭弹窗未登录 | AuthModalContext:58 | 跳转 /home | 防止停留受保护页 | + +--- + +## 4. API接口文档 + +### 4.1 Session检查 + +**接口**: `GET /api/auth/session` + +**用途**: 检查当前用户登录状态 + +**请求**: +```http +GET /api/auth/session HTTP/1.1 +Cookie: session=xxx +``` + +**响应**: +```json +{ + "isAuthenticated": true, + "user": { + "id": 123, + "phone": "13800138000", + "nickname": "价小前用户", + "email": "user@example.com", + "avatar_url": "https://...", + "has_wechat": true + } +} +``` + +**前端调用**: +```javascript +// src/contexts/AuthContext.js:85 +const checkSession = async () => { + const response = await fetch('/api/auth/session', { + credentials: 'include' + }); + const data = await response.json(); + if (data.isAuthenticated) { + setUser(data.user); + setIsAuthenticated(true); + } +}; +``` + +--- + +### 4.2 发送验证码 + +**接口**: `POST /api/auth/send-verification-code` + +**用途**: 发送手机验证码 + +**请求**: +```http +POST /api/auth/send-verification-code HTTP/1.1 +Content-Type: application/json + +{ + "credential": "13800138000", + "type": "phone", + "purpose": "login" +} +``` + +**响应**: +```json +{ + "success": true, + "message": "验证码已发送" +} +``` + +**错误响应**: +```json +{ + "success": false, + "error": "手机号格式不正确" +} +``` + +**前端调用**: +```javascript +// src/components/Auth/AuthFormContent.js:153 +const response = await fetch('/api/auth/send-verification-code', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + credential: phone, + type: 'phone', + purpose: 'login' + }) +}); +``` + +**限流规则**: +- 同一手机号: 60秒内只能发送1次 +- 验证码有效期: 5分钟 + +--- + +### 4.3 验证码登录 + +**接口**: `POST /api/auth/login-with-code` + +**用途**: 使用验证码登录/注册 + +**请求**: +```http +POST /api/auth/login-with-code HTTP/1.1 +Content-Type: application/json + +{ + "credential": "13800138000", + "verification_code": "123456", + "login_type": "phone" +} +``` + +**响应 (登录成功)**: +```json +{ + "success": true, + "isNewUser": false, + "user": { + "id": 123, + "phone": "13800138000", + "nickname": "价小前用户" + } +} +``` + +**响应 (注册成功)**: +```json +{ + "success": true, + "isNewUser": true, + "user": { + "id": 124, + "phone": "13900139000", + "nickname": "用户13900139000" + } +} +``` + +**错误响应**: +```json +{ + "success": false, + "error": "验证码已过期或不存在" +} +``` + +**自动注册逻辑**: +- 如果手机号不存在 → 自动创建新用户 +- 默认昵称: `用户{手机号}` +- isNewUser 标记用于前端引导设置昵称 + +--- + +### 4.4 获取微信二维码 + +**接口**: `GET /api/auth/wechat/qrcode` + +**用途**: 获取微信PC扫码登录二维码 + +**请求**: +```http +GET /api/auth/wechat/qrcode HTTP/1.1 +``` + +**响应**: +```json +{ + "code": 0, + "data": { + "auth_url": "https://open.weixin.qq.com/connect/qrconnect?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_login&state=yyy#wechat_redirect", + "session_id": "550e8400-e29b-41d4-a716-446655440000" + }, + "message": "success" +} +``` + +**前端调用**: +```javascript +// src/services/authService.js:69 +getWechatQRCode: async () => { + return await apiRequest('/api/auth/wechat/qrcode'); +} +``` + +**二维码有效期**: 5分钟 + +--- + +### 4.5 检查微信扫码状态 + +**接口**: `POST /api/auth/wechat/check` + +**用途**: 轮询检查微信扫码状态 + +**请求**: +```http +POST /api/auth/wechat/check HTTP/1.1 +Content-Type: application/json + +{ + "session_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**响应状态**: + +| status | 说明 | 前端行为 | +|--------|------|---------| +| waiting | 等待扫码 | 继续轮询 | +| scanned | 已扫码,等待确认 | 显示提示 + 继续轮询 | +| authorized | 已授权 | 继续轮询 | +| login_success | 登录成功 (老用户) | 停止轮询 + 调用登录接口 | +| register_success | 注册成功 (新用户) | 停止轮询 + 调用登录接口 | +| expired | 二维码过期 | 停止轮询 + 显示刷新按钮 | + +**响应示例**: +```json +{ + "status": "login_success", + "user_info": { + "openid": "xxx", + "nickname": "微信用户", + "avatar_url": "https://..." + } +} +``` + +**前端轮询**: +```javascript +// src/components/Auth/WechatRegister.js:180 +pollIntervalRef.current = setInterval(() => { + checkWechatStatus(); +}, 2000); // 每2秒检查一次 +``` + +--- + +### 4.6 微信登录 + +**接口**: `POST /api/auth/login/wechat` + +**用途**: 使用微信 session_id 完成登录 + +**请求**: +```http +POST /api/auth/login/wechat HTTP/1.1 +Content-Type: application/json + +{ + "session_id": "550e8400-e29b-41d4-a716-446655440000" +} +``` + +**响应**: +```json +{ + "success": true, + "user": { + "id": 123, + "nickname": "微信用户", + "avatar_url": "https://...", + "has_wechat": true + }, + "token": "optional-token" +} +``` + +**前端调用**: +```javascript +// src/components/Auth/WechatRegister.js:87 +const response = await authService.loginWithWechat(sessionId); +if (response?.success) { + // 存储用户信息 + if (response.user) { + localStorage.setItem('user', JSON.stringify(response.user)); + } + // 跳转首页 + navigate('/home'); +} +``` + +--- + +### 4.7 微信H5授权 + +**接口**: `POST /api/auth/wechat/h5-auth` + +**用途**: 获取微信H5网页授权链接 + +**请求**: +```http +POST /api/auth/wechat/h5-auth HTTP/1.1 +Content-Type: application/json + +{ + "redirect_url": "https://yourdomain.com/home/wechat-callback" +} +``` + +**响应**: +```json +{ + "auth_url": "https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx&redirect_uri=xxx&response_type=code&scope=snsapi_userinfo&state=yyy#wechat_redirect" +} +``` + +**前端调用**: +```javascript +// src/components/Auth/AuthFormContent.js:323 +const response = await authService.getWechatH5AuthUrl(redirectUrl); +if (response?.auth_url) { + window.location.href = response.auth_url; +} +``` + +--- + +### 4.8 微信H5回调 + +**接口**: `POST /api/auth/wechat/h5-callback` + +**用途**: 处理微信H5授权回调 + +**请求**: +```http +POST /api/auth/wechat/h5-callback HTTP/1.1 +Content-Type: application/json + +{ + "code": "wechat-code-xxx", + "state": "csrf-state-yyy" +} +``` + +**响应**: +```json +{ + "success": true, + "user": { + "id": 123, + "nickname": "微信用户", + "avatar_url": "https://..." + }, + "token": "optional-token" +} +``` + +**前端调用**: +```javascript +// src/views/Pages/WechatCallback.js:46 +const response = await authService.handleWechatH5Callback(code, state); +if (response?.success) { + await checkSession(); + navigate('/home'); +} +``` + +--- + +### 4.9 退出登录 + +**接口**: `POST /api/auth/logout` + +**用途**: 清除用户Session + +**请求**: +```http +POST /api/auth/logout HTTP/1.1 +Cookie: session=xxx +``` + +**响应**: +```json +{ + "success": true, + "message": "已退出登录" +} +``` + +**前端调用**: +```javascript +// src/contexts/AuthContext.js:120 +const logout = async () => { + await fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include' + }); + setUser(null); + setIsAuthenticated(false); + navigate('/home'); +}; +``` + +--- + +## 5. 前端组件架构 + +### 5.1 核心组件关系图 + +``` +App.js (根组件) + └── AuthProvider (全局认证状态) + └── AuthModalProvider (弹窗管理) + ├── HomeNavbar (导航栏) + │ └── Button onClick={openAuthModal} (登录按钮) + │ + ├── AuthModalManager (弹窗管理器) + │ └── Modal + │ └── AuthFormContent (认证表单) + │ ├── VerificationCodeInput (验证码输入) + │ └── WechatRegister (微信二维码) + │ + └── ProtectedRoute (路由保护) + └── 受保护的页面组件 +``` + +### 5.2 组件详解 + +#### AuthContext (`src/contexts/AuthContext.js`) + +**职责**: 全局认证状态管理 + +**状态**: +```javascript +{ + user: { + id: 123, + phone: "13800138000", + nickname: "价小前用户", + email: "user@example.com", + avatar_url: "https://...", + has_wechat: true + }, + isAuthenticated: true, + isLoading: false +} +``` + +**方法**: +- `checkSession()` - 检查当前登录状态 +- `logout()` - 退出登录 + +**使用**: +```javascript +import { useAuth } from '../../contexts/AuthContext'; + +const { user, isAuthenticated, isLoading, checkSession, logout } = useAuth(); +``` + +--- + +#### AuthModalContext (`src/contexts/AuthModalContext.js`) + +**职责**: 认证弹窗状态管理 + +**状态**: +```javascript +{ + isAuthModalOpen: false, + redirectUrl: null, // 登录成功后跳转的URL (可选) +} +``` + +**方法**: +- `openAuthModal(url, callback)` - 打开认证弹窗 +- `closeModal()` - 关闭弹窗 (未登录则跳转首页) +- `handleLoginSuccess(user)` - 登录成功处理 + +**使用**: +```javascript +import { useAuthModal } from '../../contexts/AuthModalContext'; + +const { isAuthModalOpen, openAuthModal, handleLoginSuccess } = useAuthModal(); + +// 打开弹窗 +openAuthModal(); + +// 登录成功 +handleLoginSuccess(userData); +``` + +--- + +#### AuthModalManager (`src/components/Auth/AuthModalManager.js`) + +**职责**: 渲染认证弹窗 UI + +**特点**: +- 响应式尺寸 (移动端 md, 桌面端 xl) +- 条件渲染 (仅在打开时渲染,避免不必要的Portal) +- 半透明背景 + 模糊效果 +- 禁止点击背景关闭 (防止误操作) + +**响应式配置**: +```javascript +const modalSize = useBreakpointValue({ + base: "md", // 移动端 + md: "lg", // 中屏 + lg: "xl" // 大屏 +}); + +const modalMaxW = useBreakpointValue({ + base: "90%", // 移动端占90%宽度 + md: "700px" // 桌面端固定700px +}); +``` + +--- + +#### AuthFormContent (`src/components/Auth/AuthFormContent.js`) + +**职责**: 认证表单主体内容 + +**功能**: +1. 手机号输入 +2. 验证码输入 + 发送验证码 +3. 登录/注册按钮 +4. 微信扫码 (桌面端右侧) +5. 微信H5登录 (移动端验证码下方图标) +6. 隐私协议链接 + +**布局**: +``` +桌面端 (Desktop): +┌─────────────────────────────────────┐ +│ 价值前沿 - 开启您的投资之旅 │ +├──────────────────┬──────────────────┤ +│ 登陆/注册 │ 微信扫码 │ +│ │ │ +│ 手机号输入框 │ [二维码区域] │ +│ 验证码输入框 │ │ +│ [获取验证码] │ 请使用微信扫码 │ +│ │ │ +│ [登录/注册按钮] │ │ +│ │ │ +│ 《隐私政策》 │ │ +└──────────────────┴──────────────────┘ + +移动端 (Mobile): +┌─────────────────────┐ +│ 价值前沿 │ +│ 开启您的投资之旅 │ +├─────────────────────┤ +│ 登陆/注册 │ +│ │ +│ 手机号输入框 │ +│ │ +│ 验证码输入框 │ +│ [获取验证码] │ +│ 其他登录方式: [微信] │ +│ │ +│ [登录/注册按钮] │ +│ │ +│ 《隐私政策》 │ +└─────────────────────┘ +``` + +**关键逻辑**: +```javascript +// 发送验证码 (60秒倒计时) +const sendVerificationCode = async () => { + const response = await fetch('/api/auth/send-verification-code', { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + credential: phone, + type: 'phone', + purpose: 'login' + }) + }); + + if (response.ok) { + setCountdown(60); // 启动60秒倒计时 + } +}; + +// 提交登录 +const handleSubmit = async (e) => { + e.preventDefault(); + + const response = await fetch('/api/auth/login-with-code', { + method: 'POST', + credentials: 'include', + body: JSON.stringify({ + credential: phone, + verification_code: verificationCode, + login_type: 'phone' + }) + }); + + if (response.ok) { + await checkSession(); // 更新全局状态 + + if (data.isNewUser) { + // 新用户引导设置昵称 + setShowNicknamePrompt(true); + } else { + handleLoginSuccess(); // 关闭弹窗 + } + } +}; +``` + +--- + +#### WechatRegister (`src/components/Auth/WechatRegister.js`) + +**职责**: 微信PC扫码登录组件 + +**状态机**: +``` +NONE (初始) → 点击"获取二维码" + ↓ + WAITING (显示二维码 + 轮询) + ↓ + SCANNED (提示"请在手机上确认") + ↓ + LOGIN_SUCCESS / REGISTER_SUCCESS + ↓ + 调用登录接口 → 跳转首页 + + EXPIRED (二维码过期 → 显示"点击刷新") +``` + +**轮询机制**: +```javascript +// 主轮询: 每2秒检查 +pollIntervalRef.current = setInterval(() => { + checkWechatStatus(); +}, 2000); + +// 备用轮询: 每3秒检查 (防止主轮询失败) +backupPollIntervalRef.current = setInterval(() => { + checkWechatStatus().catch(error => { + console.warn('备用轮询检查失败(静默处理):', error); + }); +}, 3000); + +// 超时机制: 5分钟后停止轮询 +timeoutRef.current = setTimeout(() => { + clearTimers(); + setWechatStatus(WECHAT_STATUS.EXPIRED); +}, 300000); +``` + +**二维码缩放**: +```javascript +// 使用 ResizeObserver 监听容器尺寸变化 +const calculateScale = () => { + const { width, height } = containerRef.current.getBoundingClientRect(); + + const scaleX = width / 300; // 二维码原始宽度300px + const scaleY = height / 350; // 二维码原始高度350px + const newScale = Math.min(scaleX, scaleY, 1.0); + + setScale(Math.max(newScale, 0.3)); // 最小0.3,最大1.0 +}; + +// iframe 缩放 +