huoyan-enterprise/backend/services/user_service.py

421 lines
13 KiB
Python

"""
用户服务层
"""
from datetime import datetime, timezone
from typing import Optional
import asyncpg
from models.user import User, UserCreate, UserResponse, UserDepartmentInfo
from services.department_service import DepartmentService
from core.security import get_password_hash, verify_password
from logger.logging import get_logger
logger = get_logger(__name__)
class UserService:
"""用户服务类"""
@staticmethod
async def get_user_by_id(conn: asyncpg.Connection, user_id: int) -> Optional[User]:
"""根据用户 ID 获取用户"""
row = await conn.fetchrow(
"""
SELECT * FROM user_list WHERE id = $1
""",
user_id
)
if row:
return User(**dict(row))
return None
@staticmethod
async def get_user_by_username(conn: asyncpg.Connection, username: str) -> Optional[User]:
"""根据用户名获取用户"""
row = await conn.fetchrow(
"""
SELECT * FROM user_list WHERE username = $1
""",
username
)
if row:
return User(**dict(row))
return None
@staticmethod
async def get_user_by_email(conn: asyncpg.Connection, email: str) -> Optional[User]:
"""根据邮箱获取用户"""
row = await conn.fetchrow(
"""
SELECT * FROM user_list WHERE email = $1
""",
email
)
if row:
return User(**dict(row))
return None
@staticmethod
async def create_user(conn: asyncpg.Connection, user_data: UserCreate) -> User:
"""创建新用户"""
hashed_password = get_password_hash(user_data.password)
row = await conn.fetchrow(
"""
INSERT INTO user_list (
username, email, phone, hashed_password, display_name,
created_at, updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *
""",
user_data.username,
user_data.email,
user_data.phone,
hashed_password,
user_data.display_name or user_data.username,
datetime.now(timezone.utc),
datetime.now(timezone.utc)
)
return User(**dict(row))
@staticmethod
async def authenticate_user(
conn: asyncpg.Connection,
username: str,
password: str
) -> Optional[User]:
"""验证用户登录"""
user = await UserService.get_user_by_username(conn, username)
if not user:
return None
if not user.hashed_password:
return None
if not verify_password(password, user.hashed_password):
return None
# 更新最后登录时间
await conn.execute(
"""
UPDATE user_list
SET last_login_at = $1
WHERE id = $2
""",
datetime.now(timezone.utc),
user.id
)
return user
@staticmethod
async def update_last_login(conn: asyncpg.Connection, user_id: int):
"""更新用户最后登录时间"""
await conn.execute(
"""
UPDATE user_list
SET last_login_at = $1
WHERE id = $2
""",
datetime.now(timezone.utc),
user_id
)
@staticmethod
async def get_user_by_phone(conn: asyncpg.Connection, phone: str) -> Optional[User]:
"""根据手机号获取用户"""
row = await conn.fetchrow(
"SELECT * FROM user_list WHERE phone = $1",
phone
)
if row:
return User(**dict(row))
return None
@staticmethod
async def create_user_by_phone(
conn: asyncpg.Connection,
phone: str,
password: str,
username: Optional[str] = None
) -> User:
"""通过手机号创建用户"""
from core.security import get_password_hash
hashed_password = get_password_hash(password)
# 生成用户名
if not username:
username = f"user_{phone[-4:]}"
counter = 1
while await UserService.get_user_by_username(conn, username):
username = f"user_{phone[-4:]}_{counter}"
counter += 1
else:
# 检查用户名是否已存在
if await UserService.get_user_by_username(conn, username):
raise ValueError("用户名已存在")
# 生成邮箱
email = f"{phone}@phone.user"
counter = 1
while await UserService.get_user_by_email(conn, email):
email = f"{phone}_{counter}@phone.user"
counter += 1
row = await conn.fetchrow(
"""
INSERT INTO user_list (
username, email, phone, hashed_password, display_name,
is_active, created_at, updated_at, last_login_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
""",
username,
email,
phone,
hashed_password,
username,
True,
datetime.now(timezone.utc),
datetime.now(timezone.utc),
datetime.now(timezone.utc)
)
return User(**dict(row))
@staticmethod
async def create_user_by_phone_without_password(
conn: asyncpg.Connection,
phone: str
) -> User:
"""通过手机号创建用户(不设置密码,用于验证码登录自动注册)"""
# 生成用户名
username = f"user_{phone[-4:]}"
counter = 1
while await UserService.get_user_by_username(conn, username):
username = f"user_{phone[-4:]}_{counter}"
counter += 1
# 生成邮箱
email = f"{phone}@phone.user"
counter = 1
while await UserService.get_user_by_email(conn, email):
email = f"{phone}_{counter}@phone.user"
counter += 1
row = await conn.fetchrow(
"""
INSERT INTO user_list (
username, email, phone, display_name,
is_active, created_at, updated_at, last_login_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
""",
username,
email,
phone,
username,
True,
datetime.now(timezone.utc),
datetime.now(timezone.utc),
datetime.now(timezone.utc)
)
return User(**dict(row))
@staticmethod
async def authenticate_by_phone_password(
conn: asyncpg.Connection,
phone: str,
password: str
) -> Optional[User]:
"""通过手机号和密码验证用户"""
user = await UserService.get_user_by_phone(conn, phone)
if not user:
return None
if not user.hashed_password:
return None
if not verify_password(password, user.hashed_password):
return None
# 更新最后登录时间
await UserService.update_last_login(conn, user.id)
return user
@staticmethod
async def get_user_by_wechat_openid(conn: asyncpg.Connection, openid: str) -> Optional[User]:
"""根据微信 OpenID 获取用户"""
row = await conn.fetchrow(
"SELECT * FROM user_list WHERE wechat_openid = $1",
openid
)
if row:
return User(**dict(row))
return None
@staticmethod
async def create_or_update_wechat_user(
conn: asyncpg.Connection,
openid: str,
unionid: Optional[str] = None,
nickname: Optional[str] = None,
avatar_url: Optional[str] = None,
phone: Optional[str] = None
) -> User:
"""
创建或更新微信用户
账号合并逻辑:
1. 如果 openid 已存在,直接更新
2. 如果 phone 是真实手机号且已有用户,绑定到该用户
3. 否则创建新用户
"""
# 1. 先检查 openid 是否已存在
existing_user = await UserService.get_user_by_wechat_openid(conn, openid)
if existing_user:
# 更新现有用户
row = await conn.fetchrow(
"""
UPDATE user_list
SET wechat_unionid = COALESCE($1, wechat_unionid),
wechat_nickname = COALESCE($2, wechat_nickname),
wechat_avatar_url = COALESCE($3, wechat_avatar_url),
updated_at = $4,
last_login_at = $5
WHERE wechat_openid = $6
RETURNING *
""",
unionid,
nickname,
avatar_url,
datetime.now(timezone.utc),
datetime.now(timezone.utc),
openid
)
return User(**dict(row))
# 2. 检查 phone 是否是真实手机号,且已有用户
import re
if phone and re.match(r'^1[3-9]\d{9}$', phone):
phone_user = await UserService.get_user_by_phone(conn, phone)
if phone_user and not phone_user.wechat_openid:
# 绑定到已有用户
return await UserService.link_wechat_to_existing_user(
conn, phone_user.id, openid, unionid, nickname, avatar_url
)
# 3. 创建新用户
username = f"wx_{openid[:8]}"
counter = 1
while await UserService.get_user_by_username(conn, username):
username = f"wx_{openid[:8]}_{counter}"
counter += 1
email = f"{openid[:16]}@wechat.user"
counter = 1
while await UserService.get_user_by_email(conn, email):
email = f"{openid[:16]}_{counter}@wechat.user"
counter += 1
# 如果有真实手机号则使用,否则生成占位符
user_phone = phone if phone and re.match(r'^1[3-9]\d{9}$', phone) else f"wx_{openid[:11]}"
row = await conn.fetchrow(
"""
INSERT INTO user_list (
username, email, phone, wechat_openid, wechat_unionid,
wechat_nickname, wechat_avatar_url, display_name, avatar_url,
is_active, created_at, updated_at, last_login_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING *
""",
username,
email,
user_phone,
openid,
unionid,
nickname,
avatar_url,
nickname or username,
avatar_url,
True,
datetime.now(timezone.utc),
datetime.now(timezone.utc),
datetime.now(timezone.utc)
)
return User(**dict(row))
@staticmethod
async def link_wechat_to_existing_user(
conn: asyncpg.Connection,
user_id: int,
openid: str,
unionid: Optional[str] = None,
nickname: Optional[str] = None,
avatar_url: Optional[str] = None
) -> User:
"""将微信账号绑定到已有用户"""
row = await conn.fetchrow(
"""
UPDATE user_list
SET wechat_openid = $1,
wechat_unionid = $2,
wechat_nickname = $3,
wechat_avatar_url = $4,
updated_at = $5,
last_login_at = $6
WHERE id = $7
RETURNING *
""",
openid,
unionid,
nickname,
avatar_url,
datetime.now(timezone.utc),
datetime.now(timezone.utc),
user_id
)
return User(**dict(row))
@staticmethod
async def build_user_response(conn: asyncpg.Connection, user: User) -> UserResponse:
"""构建含部门信息的用户响应"""
department = None
department_name = None
if user.department_id is not None and user.enterprise_id is not None:
dept = await DepartmentService.get_by_id(
conn, user.department_id, user.enterprise_id
)
if dept:
department_name = dept["name"]
department = UserDepartmentInfo(
id=dept["id"],
name=dept["name"],
parent_id=dept.get("parent_id"),
leader_user_id=dept.get("leader_user_id"),
leader_name=dept.get("leader_name"),
)
data = user.model_dump(exclude={"hashed_password"})
return UserResponse(
**data,
department_name=department_name,
department=department,
)