# Day 15 - 手机号登录迁移 + 账户设置功能 **日期**:2026-02-02 --- ## 🔐 认证系统迁移:邮箱 → 手机号 ### 背景 根据业务需求,将用户认证从邮箱登录迁移到手机号登录(11位中国手机号)。 ### 变更范围 | 组件 | 变更内容 | |------|----------| | 数据库 Schema | `email` 字段替换为 `phone` | | 后端 API | 注册/登录/获取用户信息接口使用 `phone` | | 前端页面 | 登录/注册页面改为手机号输入框 | | 管理员配置 | `ADMIN_EMAIL` 改为 `ADMIN_PHONE` | --- ## 📦 后端修改 ### 1. 数据库 Schema (`schema.sql`) **文件**: `backend/database/schema.sql` ```sql CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), phone TEXT UNIQUE NOT NULL, -- 原 email 改为 phone password_hash TEXT NOT NULL, username TEXT, role TEXT DEFAULT 'pending' CHECK (role IN ('pending', 'user', 'admin')), is_active BOOLEAN DEFAULT FALSE, expires_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_users_phone ON users(phone); ``` ### 2. 认证 API (`auth.py`) **文件**: `backend/app/api/auth.py` #### 请求模型更新 ```python class RegisterRequest(BaseModel): phone: str password: str username: Optional[str] = None @field_validator('phone') @classmethod def validate_phone(cls, v): if not re.match(r'^\d{11}$', v): raise ValueError('手机号必须是11位数字') return v ``` #### 新增修改密码接口 ```python class ChangePasswordRequest(BaseModel): old_password: str new_password: str @field_validator('new_password') @classmethod def validate_new_password(cls, v): if len(v) < 6: raise ValueError('新密码长度至少6位') return v @router.post("/change-password") async def change_password(request: ChangePasswordRequest, req: Request, response: Response): """修改密码,验证当前密码后更新""" # 1. 验证当前密码 # 2. 更新密码 hash # 3. 重新生成 session token # 4. 返回新的 JWT Cookie ``` ### 3. 配置更新 **文件**: `backend/app/core/config.py` ```python # 管理员配置 ADMIN_PHONE: str = "" # 原 ADMIN_EMAIL ADMIN_PASSWORD: str = "" ``` **文件**: `backend/.env` ```bash ADMIN_PHONE=15549380526 ADMIN_PASSWORD=lam1988324 ``` ### 4. 管理员初始化 (`main.py`) **文件**: `backend/app/main.py` ```python @app.on_event("startup") async def init_admin(): admin_phone = settings.ADMIN_PHONE # 原 ADMIN_EMAIL # ... 使用 phone 字段创建管理员 ``` ### 5. 管理员 API (`admin.py`) **文件**: `backend/app/api/admin.py` ```python class UserListItem(BaseModel): id: str phone: str # 原 email username: Optional[str] role: str is_active: bool expires_at: Optional[str] created_at: str ``` --- ## 🖥️ 前端修改 ### 1. 登录页面 (`login/page.tsx`) **文件**: `frontend/src/app/login/page.tsx` ```tsx const [phone, setPhone] = useState(''); // 验证手机号格式 if (!/^\d{11}$/.test(phone)) { setError('请输入正确的11位手机号'); return; } setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} maxLength={11} placeholder="请输入11位手机号" /> ``` ### 2. 注册页面 (`register/page.tsx`) 同样使用手机号输入,增加 11 位数字验证。 ### 3. Auth 工具函数 (`auth.ts`) **文件**: `frontend/src/lib/auth.ts` ```typescript export interface User { id: string; phone: string; // 原 email username: string | null; role: string; is_active: boolean; } export async function login(phone: string, password: string): Promise { ... } export async function register(phone: string, password: string, username?: string): Promise { ... } export async function changePassword(oldPassword: string, newPassword: string): Promise { ... } ``` ### 4. 首页账户设置下拉菜单 (`page.tsx`) **文件**: `frontend/src/app/page.tsx` 将原来的"退出"按钮改为账户设置下拉菜单: ```tsx function AccountSettingsDropdown() { const [isOpen, setIsOpen] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); // ... return ( setIsOpen(!isOpen)}> ⚙️ 账户 {/* 下拉菜单 */} {isOpen && ( setShowPasswordModal(true)}> 🔐 修改密码 🚪 退出登录 )} {/* 修改密码弹窗 */} {showPasswordModal && ( )} ); } ``` ### 5. 管理员页面 (`admin/page.tsx`) **文件**: `frontend/src/app/admin/page.tsx` ```tsx interface UserListItem { id: string; phone: string; // 原 email // ... } // 显示手机号而非邮箱 {user.phone} ``` --- ## 🗄️ 数据库迁移 ### 迁移脚本 **文件**: `backend/database/migrate_to_phone.sql` ```sql -- 删除旧表 (CASCADE 处理外键依赖) DROP TABLE IF EXISTS user_sessions CASCADE; DROP TABLE IF EXISTS social_accounts CASCADE; DROP TABLE IF EXISTS users CASCADE; -- 重新创建使用 phone 字段的表 CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), phone TEXT UNIQUE NOT NULL, -- ... ); -- 重新创建依赖表和索引 CREATE TABLE user_sessions (...); CREATE TABLE social_accounts (...); CREATE INDEX idx_users_phone ON users(phone); ``` ### 执行方式 ```bash # 方式一:Docker 命令 docker exec -i supabase-db psql -U postgres < backend/database/migrate_to_phone.sql # 方式二:Supabase Studio SQL Editor # 打开 https://supabase.hbyrkj.top -> SQL Editor -> 粘贴执行 ``` --- ## ✅ 部署步骤 ```bash # 1. 执行数据库迁移 docker exec -i supabase-db psql -U postgres < backend/database/migrate_to_phone.sql # 2. 重新构建前端 cd frontend && npm run build # 3. 重启服务 pm2 restart vigent2-backend vigent2-frontend ``` --- ## 📁 今日修改文件清单 | 文件 | 变更类型 | 说明 | |------|----------|------| | `backend/database/schema.sql` | 修改 | email → phone | | `backend/database/migrate_to_phone.sql` | 新增 | 数据库迁移脚本 | | `backend/app/api/auth.py` | 修改 | 手机号验证 + 修改密码 API | | `backend/app/api/admin.py` | 修改 | UserListItem.email → phone | | `backend/app/core/config.py` | 修改 | ADMIN_EMAIL → ADMIN_PHONE | | `backend/app/main.py` | 修改 | 管理员初始化使用 phone | | `backend/.env` | 修改 | ADMIN_PHONE=15549380526 | | `frontend/src/app/login/page.tsx` | 修改 | 手机号登录 + 11位验证 | | `frontend/src/app/register/page.tsx` | 修改 | 手机号注册 + 11位验证 | | `frontend/src/lib/auth.ts` | 修改 | phone 参数 + changePassword 函数 | | `frontend/src/app/page.tsx` | 修改 | AccountSettingsDropdown 组件 | | `frontend/src/app/admin/page.tsx` | 修改 | 用户列表显示手机号 | | `frontend/src/contexts/AuthContext.tsx` | 修改 | 存储完整用户信息含 expires_at | --- ## 🆕 后续完善 (Day 15 下午) ### 账户有效期显示 在账户下拉菜单中显示用户的有效期: | 显示情况 | 格式 | |----------|------| | 有设置 expires_at | `2026-03-15` | | NULL | `永久有效` | **相关修改**: - `backend/app/api/auth.py`: UserResponse 新增 `expires_at` 字段 - `frontend/src/contexts/AuthContext.tsx`: 存储完整用户对象 - `frontend/src/app/page.tsx`: 格式化并显示有效期 ### 点击外部关闭下拉菜单 使用 `useRef` + `useEffect` 监听全局点击事件,点击菜单外部自动关闭。 ### 修改密码后强制重新登录 密码修改成功后: 1. 显示"密码修改成功,正在跳转登录页..." 2. 1.5秒后调用登出 API 3. 跳转到登录页面 --- ## 🔗 相关文档 - [task_complete.md](../task_complete.md) - 任务总览 - [Day14.md](./Day14.md) - 模型升级 + AI 标题标签 - [AUTH_DEPLOY.md](../AUTH_DEPLOY.md) - 认证系统部署指南