923ff63a3d
后端: - security.py: 新增 bcrypt 密码哈希/校验工具 - auth_service.py: 新增 login_with_password、reset_password_with_sms - auth.py: 新增 /login-password、/has-password、/set-password、/reset-password 接口 - schemas/auth.py: 新增 PasswordLoginRequest、SetPasswordRequest、ResetPasswordRequest、CheckPasswordResponse - crud/user.py: 新增 update_password 前端: - Login.tsx: 支持验证码/密码切换登录,密码模式下显示忘记密码入口 - Login.css: 新增登录方式切换标签、密码输入框样式 - authStore.ts: 新增 loginWithPassword - Settings.tsx: 新增账号安全区块,显示密码状态,打开设置/修改密码弹窗 - SetPasswordModal.tsx: 设置/修改密码弹窗(旧密码校验、密码显示切换、表单验证) - ResetPasswordModal.tsx: 忘记密码弹窗(手机号+验证码+新密码重置) 兼容: - 零数据库迁移,password_hash 字段已存在(nullable) - 现有接口不变,完全向后兼容旧版本
105 lines
2.7 KiB
Python
105 lines
2.7 KiB
Python
"""
|
||
用户 CRUD 操作
|
||
==============
|
||
|
||
用户认证相关的数据访问。
|
||
"""
|
||
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.crud.base import CRUDBase
|
||
from app.models.user import User
|
||
|
||
|
||
class UserCRUD(CRUDBase[User]):
|
||
"""用户数据访问对象"""
|
||
|
||
def __init__(self) -> None:
|
||
super().__init__(User)
|
||
|
||
async def get_by_mobile(self, db: AsyncSession, *, mobile: str) -> User | None:
|
||
"""根据手机号获取用户"""
|
||
result = await db.execute(select(User).where(User.mobile == mobile))
|
||
return result.scalar_one_or_none()
|
||
|
||
async def get_or_create_by_mobile(
|
||
self, db: AsyncSession, *, mobile: str, nickname: str | None = None, source: str = "unknown"
|
||
) -> User:
|
||
"""
|
||
根据手机号获取或创建用户
|
||
|
||
Returns:
|
||
已存在或新创建的用户
|
||
"""
|
||
user = await self.get_by_mobile(db, mobile=mobile)
|
||
|
||
if user is None:
|
||
# 创建新用户
|
||
user = await self.create(
|
||
db,
|
||
obj_in={
|
||
"mobile": mobile,
|
||
"nickname": nickname or f"用户_{mobile[-4:]}",
|
||
"source": source,
|
||
},
|
||
)
|
||
|
||
return user
|
||
|
||
async def update_login_info(
|
||
self, db: AsyncSession, *, user_id: str, ip: str | None = None
|
||
) -> User | None:
|
||
"""
|
||
更新用户最后登录信息
|
||
"""
|
||
from datetime import UTC, datetime
|
||
|
||
user = await self.get(db, id=user_id)
|
||
if user is None:
|
||
return None
|
||
|
||
user.last_login_at = datetime.now(UTC)
|
||
if ip:
|
||
user.last_login_ip = ip
|
||
|
||
await db.commit()
|
||
await db.refresh(user)
|
||
return user
|
||
|
||
async def update_password(
|
||
self, db: AsyncSession, *, user_id: str, password_hash: str
|
||
) -> User | None:
|
||
"""更新用户密码"""
|
||
user = await self.get(db, id=user_id)
|
||
if user is None:
|
||
return None
|
||
|
||
user.password_hash = password_hash
|
||
await db.commit()
|
||
await db.refresh(user)
|
||
return user
|
||
|
||
async def update_extra(
|
||
self, db: AsyncSession, *, user_id: str, extra: dict
|
||
) -> bool:
|
||
"""
|
||
原子更新用户 extra 字段(JSONB)
|
||
|
||
使用 SQLAlchemy 的 update 语句避免读-改-写的竞态条件。
|
||
"""
|
||
from sqlalchemy import update
|
||
|
||
stmt = (
|
||
update(User)
|
||
.where(User.id == user_id)
|
||
.values(extra=extra)
|
||
)
|
||
result = await db.execute(stmt)
|
||
await db.commit()
|
||
return result.rowcount > 0
|
||
|
||
|
||
# 导出实例
|
||
user = UserCRUD()
|