10 KiB
10 KiB
Day 15 - 手机号登录迁移 + 账户设置功能
日期:2026-02-02
🔐 认证系统迁移:邮箱 → 手机号
背景
根据业务需求,将用户认证从邮箱登录迁移到手机号登录(11位中国手机号)。
变更范围
| 组件 | 变更内容 |
|---|---|
| 数据库 Schema | email 字段替换为 phone |
| 后端 API | 注册/登录/获取用户信息接口使用 phone |
| 前端页面 | 登录/注册页面改为手机号输入框 |
| 管理员配置 | ADMIN_EMAIL 改为 ADMIN_PHONE |
📦 后端修改
1. 数据库 Schema (schema.sql)
文件: backend/database/schema.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
请求模型更新
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
新增修改密码接口
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
# 管理员配置
ADMIN_PHONE: str = "" # 原 ADMIN_EMAIL
ADMIN_PASSWORD: str = ""
文件: backend/.env
ADMIN_PHONE=15549380526
ADMIN_PASSWORD=lam1988324
4. 管理员初始化 (main.py)
文件: backend/app/main.py
@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
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
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/lib/auth.ts
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
将原来的"退出"按钮改为账户设置下拉菜单:
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
interface UserListItem {
id: string;
phone: string; // 原 email
// ...
}
// 显示手机号而非邮箱
<div className="text-gray-400 text-sm">{user.phone}</div>
🗄️ 数据库迁移
迁移脚本
文件: backend/database/migrate_to_phone.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);
执行方式
# 方式一: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 -> 粘贴执行
✅ 部署步骤
# 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.5秒后调用登出 API
- 跳转到登录页面
🔗 相关文档
- task_complete.md - 任务总览
- Day14.md - 模型升级 + AI 标题标签
- AUTH_DEPLOY.md - 认证系统部署指南
🤖 模型与功能增强 (Day 15 晚)
1. GLM-4.7-Flash 升级
文件: backend/app/services/glm_service.py
将文案洗稿模型从 glm-4-flash 升级为 glm-4.7-flash:
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) 中为上传的视频添加预览功能:
- 点击缩略图弹出视频播放模态框
- 支持下载与发布快捷跳转
📝 任务清单更新
- 认证系统迁移 (手机号)
- 账户管理 (密码修改/有效期)
- GLM-4.7 模型升级
- 独立文案提取助手 (B站/抖音支持)
- 视频预览功能