421 lines
13 KiB
Python
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,
|
|
)
|
|
|