""" 微信登录 + 绑定手机号邮箱 Use Case """ from __future__ import annotations import logging from datetime import datetime, timezone from typing import Optional from packages.application.auth.verification_code_service import ( CODE_TYPE_EMAIL_BIND, CODE_TYPE_PHONE_BIND, normalize_phone, validate_email, validate_phone, ) from packages.domain.entities import User logger = logging.getLogger(__name__) class BindContactRequest: """绑定联系方式请求""" def __init__( self, user_id: str, phone: str = "", phone_code: str = "", email: str = "", email_code: str = "", ): self.user_id = user_id self.phone = normalize_phone(phone) if phone else "" self.phone_code = phone_code.strip() if phone_code else "" self.email = email.strip().lower() if email else "" self.email_code = email_code.strip() if email_code else "" class BindContactResponse: """绑定响应""" def __init__(self, user: User): self.user = user def to_dict(self) -> dict: return { "user": { "id": self.user.id, "email": self.user.email, "phone": self.user.phone, "phone_verified": self.user.phone_verified, "display_name": self.user.display_name, "binding_complete": self.user.binding_completed_at is not None, } } class BindContactUseCase: """绑定手机+邮箱用例""" def __init__( self, user_repository, verification_code_service, email_service=None, ): self.user_repo = user_repository self.verification_service = verification_code_service self.email_service = email_service def execute(self, request: BindContactRequest) -> tuple[Optional[BindContactResponse], Optional[str]]: try: # 1. 校验参数 if not request.phone and not request.email: return None, "至少填写手机号或邮箱" # 2. 查找用户 user = self.user_repo.find_by_id(request.user_id) if not user: return None, "用户不存在" # 3. 手机号绑定 if request.phone: ok, err = validate_phone(request.phone) if not ok: return None, err if not request.phone_code: return None, "请输入手机验证码" # 校验手机号未被其他账号绑定 existing = self.user_repo.find_by_phone(request.phone) if existing and existing.id != user.id: return None, "该手机号已被其他账号绑定" # 校验验证码 ok, err = self.verification_service.verify( recipient=request.phone, code_type=CODE_TYPE_PHONE_BIND, code_value=request.phone_code, ) if not ok: return None, f"手机验证码错误:{err}" user.phone = request.phone user.phone_verified = True # 4. 邮箱绑定 if request.email: ok, err = validate_email(request.email) if not ok: return None, err if not request.email_code: return None, "请输入邮箱验证码" # 校验邮箱未被其他账号绑定 existing = self.user_repo.find_by_email(request.email) if existing and existing.id != user.id: return None, "该邮箱已被其他账号绑定" # 校验验证码 ok, err = self.verification_service.verify( recipient=request.email, code_type=CODE_TYPE_EMAIL_BIND, code_value=request.email_code, ) if not ok: return None, f"邮箱验证码错误:{err}" user.email = request.email user.email_verified = True # 5. 判断是否完成绑定 if user.phone_verified and user.email_verified and "@wechat.local" not in user.email: user.binding_completed_at = datetime.now(timezone.utc) # 6. 保存 self.user_repo.save(user) return BindContactResponse(user=user), None except Exception as e: logger.error("绑定联系方式失败: %s", e, exc_info=True) return None, f"绑定失败: {str(e)}" class SendVerificationCodeRequest: """发送验证码请求""" def __init__(self, target: str, value: str, purpose: str): self.target = target # phone / email self.value = value.strip() self.purpose = purpose # bind / login / reset_password class SendVerificationCodeResponse: """发送验证码响应""" def __init__(self, expires_in: int, resend_after: int): self.expires_in = expires_in self.resend_after = resend_after def to_dict(self) -> dict: return { "expires_in": self.expires_in, "resend_after": self.resend_after, } class SendVerificationCodeUseCase: """发送验证码用例""" def __init__( self, verification_code_service, sms_service=None, email_service=None, ): self.verification_service = verification_code_service self.sms_service = sms_service self.email_service = email_service def execute( self, request: SendVerificationCodeRequest ) -> tuple[Optional[SendVerificationCodeResponse], Optional[str]]: try: # 1. 确定 code_type if request.target == "phone": ok, err = validate_phone(request.value) if not ok: return None, err code_type = f"{request.target}_{request.purpose}" recipient = normalize_phone(request.value) elif request.target == "email": ok, err = validate_email(request.value) if not ok: return None, err code_type = f"{request.target}_{request.purpose}" recipient = request.value.lower() else: return None, f"不支持的目标类型: {request.target}" # 2. 生成验证码 code_obj, err = self.verification_service.generate(recipient, code_type) if err: return None, err # 3. 发送 if request.target == "phone" and self.sms_service: self.sms_service.send_verification_code(recipient, code_obj.code) elif request.target == "email" and self.email_service: subject = "验证码 - 小应剪辑" body = f"您的验证码是:{code_obj.code},5分钟内有效。" self.email_service.send_email(recipient, subject, body) # 4. 返回 ttl = (code_obj.expires_at - code_obj.created_at).total_seconds() return ( SendVerificationCodeResponse( expires_in=int(ttl), resend_after=60, ), None, ) except Exception as e: logger.error("发送验证码失败: %s", e, exc_info=True) return None, f"发送失败: {str(e)}"