411 lines
10 KiB
Markdown
411 lines
10 KiB
Markdown
# 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;
|
||
}
|
||
|
||
<input
|
||
type="tel"
|
||
value={phone}
|
||
onChange={(e) => 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/shared/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<AuthResponse> { ... }
|
||
export async function register(phone: string, password: string, username?: string): Promise<AuthResponse> { ... }
|
||
export async function changePassword(oldPassword: string, newPassword: string): Promise<AuthResponse> { ... }
|
||
```
|
||
|
||
### 4. 首页账户设置下拉菜单 (`page.tsx`)
|
||
|
||
**文件**: `frontend/src/app/page.tsx`
|
||
|
||
将原来的"退出"按钮改为账户设置下拉菜单:
|
||
|
||
```tsx
|
||
function AccountSettingsDropdown() {
|
||
const [isOpen, setIsOpen] = useState(false);
|
||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||
// ...
|
||
|
||
return (
|
||
<div className="relative">
|
||
<button onClick={() => setIsOpen(!isOpen)}>
|
||
⚙️ 账户
|
||
</button>
|
||
|
||
{/* 下拉菜单 */}
|
||
{isOpen && (
|
||
<div className="absolute right-0 mt-2 w-40 bg-gray-800 ...">
|
||
<button onClick={() => setShowPasswordModal(true)}>
|
||
🔐 修改密码
|
||
</button>
|
||
<button onClick={handleLogout} className="text-red-300">
|
||
🚪 退出登录
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* 修改密码弹窗 */}
|
||
{showPasswordModal && (
|
||
<div className="fixed inset-0 z-50 ...">
|
||
<form onSubmit={handleChangePassword}>
|
||
<input placeholder="当前密码" />
|
||
<input placeholder="新密码" />
|
||
<input placeholder="确认新密码" />
|
||
</form>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 5. 管理员页面 (`admin/page.tsx`)
|
||
|
||
**文件**: `frontend/src/app/admin/page.tsx`
|
||
|
||
```tsx
|
||
interface UserListItem {
|
||
id: string;
|
||
phone: string; // 原 email
|
||
// ...
|
||
}
|
||
|
||
// 显示手机号而非邮箱
|
||
<div className="text-gray-400 text-sm">{user.phone}</div>
|
||
```
|
||
|
||
---
|
||
|
||
## 🗄️ 数据库迁移
|
||
|
||
### 迁移脚本
|
||
|
||
**文件**: `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/shared/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) - 认证系统部署指南
|
||
|
||
---
|
||
|
||
## 🤖 模型与功能增强 (Day 15 晚)
|
||
|
||
### 1. GLM-4.7-Flash 升级
|
||
|
||
**文件**: `backend/app/services/glm_service.py`
|
||
|
||
将文案洗稿模型从 `glm-4-flash` 升级为 `glm-4.7-flash`:
|
||
|
||
```python
|
||
response = client.chat.completions.create(
|
||
model="glm-4.7-flash", # Upgrade from glm-4-flash
|
||
messages=[...],
|
||
# ...
|
||
)
|
||
```
|
||
|
||
**改进**:
|
||
- 响应速度提升
|
||
- 洗稿文案的流畅度和逻辑性增强
|
||
|
||
### 2. 独立文案提取助手
|
||
|
||
实现了独立的文案提取工具,支持从视频/音频文件或 URL 提取文字。
|
||
|
||
#### 后端实现 (`backend/app/api/tools.py`)
|
||
|
||
- **多源支持**: 文件上传 (MP4/MP3/WAV) 或 URL 下载
|
||
- **智能下载**:
|
||
- `yt-dlp`: 通用下载 (Douyin/TikTok/Bilibili)
|
||
- `Playwright`: 智能回退机制 (Bilibili Dashboard API, Douyin Cookie Bypass)
|
||
- **URL 自动清洗**: 正则提取分享文本中的 HTTP 链接
|
||
- **流程**: 下载 -> FFmpeg 转 WAV (16k) -> Whisper 识别 -> GLM-4.7 洗稿
|
||
|
||
#### 前端实现 (`frontend/src/components/ScriptExtractionModal.tsx`)
|
||
|
||
- **独立模态框**: 通过顶部导航栏打开
|
||
- **功能**:
|
||
- 链接粘贴 / 文件拖拽
|
||
- 实时进度显示 (下载 -> 识别 -> 洗稿)
|
||
- **一键填入**: 将提取结果直接填充到主输入框
|
||
- **自动识别**: 自动区分平台与链接
|
||
- **交互优化**:
|
||
- 防止误触背景关闭
|
||
- 复制功能兼容 HTTP 环境 (Fallback textArea)
|
||
|
||
### 3. 上传视频预览功能
|
||
|
||
在素材列表 (`frontend/src/app/page.tsx`) 中为上传的视频添加预览功能:
|
||
- 点击缩略图弹出视频播放模态框
|
||
- 支持下载与发布快捷跳转
|
||
|
||
---
|
||
|
||
## 📝 任务清单更新
|
||
|
||
- [x] 认证系统迁移 (手机号)
|
||
- [x] 账户管理 (密码修改/有效期)
|
||
- [x] GLM-4.7 模型升级
|
||
- [x] 独立文案提取助手 (B站/抖音支持)
|
||
- [x] 视频预览功能
|