""" 用户服务层 """ 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, )