style: auto-format with black + isort + prettier [ci skip]
This commit is contained in:
@@ -509,17 +509,18 @@ async def send_verification_code(
|
||||
request: SendVerificationCodeRequest,
|
||||
) -> SendVerificationCodeResponse:
|
||||
"""发送验证码(手机或邮箱)"""
|
||||
from app.dependencies import get_db
|
||||
|
||||
from packages.adapters.sms.sms_service import get_sms_service
|
||||
from packages.adapters.smtp import get_email_service
|
||||
from packages.adapters.sqlalchemy_impl.verification_code_repository import (
|
||||
SQLAlchemyVerificationCodeRepository,
|
||||
)
|
||||
from packages.application.auth.bind_contact_use_case import SendVerificationCodeRequest as UseCaseRequest
|
||||
from packages.application.auth.bind_contact_use_case import (
|
||||
SendVerificationCodeRequest as UseCaseRequest,
|
||||
SendVerificationCodeUseCase,
|
||||
)
|
||||
from packages.application.auth.bind_contact_use_case import SendVerificationCodeUseCase
|
||||
from packages.application.auth.verification_code_service import VerificationCodeService
|
||||
from app.dependencies import get_db
|
||||
|
||||
db = next(get_db())
|
||||
repo = SQLAlchemyVerificationCodeRepository(db)
|
||||
@@ -554,15 +555,16 @@ async def bind_contact(
|
||||
user_repository: UserRepository = Depends(get_user_repository),
|
||||
) -> BindContactResponse:
|
||||
"""绑定手机号和/或邮箱(需登录态)"""
|
||||
from app.dependencies import get_db
|
||||
|
||||
from packages.adapters.sqlalchemy_impl.verification_code_repository import (
|
||||
SQLAlchemyVerificationCodeRepository,
|
||||
)
|
||||
from packages.application.auth.bind_contact_use_case import BindContactRequest as UseCaseRequest
|
||||
from packages.application.auth.bind_contact_use_case import (
|
||||
BindContactRequest as UseCaseRequest,
|
||||
BindContactUseCase,
|
||||
)
|
||||
from packages.application.auth.bind_contact_use_case import BindContactUseCase
|
||||
from packages.application.auth.verification_code_service import VerificationCodeService
|
||||
from app.dependencies import get_db
|
||||
|
||||
db = next(get_db())
|
||||
vc_repo = SQLAlchemyVerificationCodeRepository(db)
|
||||
|
||||
@@ -35,19 +35,18 @@ class AliyunSmsService:
|
||||
self.access_key_id = access_key_id or os.environ.get("ALIYUN_SMS_ACCESS_KEY_ID", "")
|
||||
self.access_key_secret = access_key_secret or os.environ.get("ALIYUN_SMS_ACCESS_KEY_SECRET", "")
|
||||
self.sign_name = sign_name or os.environ.get("ALIYUN_SMS_SIGN_NAME", "小应剪辑")
|
||||
self.verify_template_id = verify_template_id or os.environ.get(
|
||||
"ALIYUN_SMS_VERIFY_TEMPLATE_ID", "SMS_123456789"
|
||||
)
|
||||
self.verify_template_id = verify_template_id or os.environ.get("ALIYUN_SMS_VERIFY_TEMPLATE_ID", "SMS_123456789")
|
||||
|
||||
def send_verification_code(self, phone: str, code: str) -> bool:
|
||||
return self.send_template_sms(phone, self.verify_template_id, {"code": code})
|
||||
|
||||
def send_template_sms(self, phone: str, template_id: str, params: dict) -> bool:
|
||||
try:
|
||||
import json
|
||||
|
||||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
|
||||
from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
|
||||
from alibabacloud_tea_openapi import models as open_api_models
|
||||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
|
||||
import json
|
||||
|
||||
config = open_api_models.Config(
|
||||
access_key_id=self.access_key_id,
|
||||
|
||||
@@ -573,4 +573,4 @@ class VerificationCodeModel(Base):
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
used_at = Column(DateTime, nullable=True)
|
||||
attempts = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -189,7 +189,9 @@ class SendVerificationCodeUseCase:
|
||||
self.sms_service = sms_service
|
||||
self.email_service = email_service
|
||||
|
||||
def execute(self, request: SendVerificationCodeRequest) -> tuple[Optional[SendVerificationCodeResponse], Optional[str]]:
|
||||
def execute(
|
||||
self, request: SendVerificationCodeRequest
|
||||
) -> tuple[Optional[SendVerificationCodeResponse], Optional[str]]:
|
||||
try:
|
||||
# 1. 确定 code_type
|
||||
if request.target == "phone":
|
||||
@@ -222,10 +224,13 @@ class SendVerificationCodeUseCase:
|
||||
|
||||
# 4. 返回
|
||||
ttl = (code_obj.expires_at - code_obj.created_at).total_seconds()
|
||||
return SendVerificationCodeResponse(
|
||||
expires_in=int(ttl),
|
||||
resend_after=60,
|
||||
), None
|
||||
return (
|
||||
SendVerificationCodeResponse(
|
||||
expires_in=int(ttl),
|
||||
resend_after=60,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("发送验证码失败: %s", e, exc_info=True)
|
||||
|
||||
@@ -18,10 +18,10 @@ from packages.ports.verification_code_repository import VerificationCodeReposito
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 频控参数
|
||||
RESEND_COOLDOWN_SECONDS = 60 # 重发冷却时间
|
||||
DAILY_LIMIT = 10 # 每日发送上限
|
||||
MAX_ATTEMPTS = 5 # 单验证码最大尝试次数
|
||||
DEFAULT_TTL_SECONDS = 300 # 默认有效期 5 分钟
|
||||
RESEND_COOLDOWN_SECONDS = 60 # 重发冷却时间
|
||||
DAILY_LIMIT = 10 # 每日发送上限
|
||||
MAX_ATTEMPTS = 5 # 单验证码最大尝试次数
|
||||
DEFAULT_TTL_SECONDS = 300 # 默认有效期 5 分钟
|
||||
|
||||
# 验证码类型
|
||||
CODE_TYPE_EMAIL_BIND = "email_bind"
|
||||
|
||||
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
|
||||
@dataclass
|
||||
class WechatUserInfo:
|
||||
"""微信用户信息"""
|
||||
|
||||
openid: str
|
||||
unionid: str = ""
|
||||
nickname: str = ""
|
||||
@@ -39,9 +40,7 @@ class WechatOAuthService:
|
||||
):
|
||||
self.app_id = app_id or os.environ.get("WECHAT_OPEN_APP_ID", "")
|
||||
self.app_secret = app_secret or os.environ.get("WECHAT_OPEN_APP_SECRET", "")
|
||||
self.redirect_uri = redirect_uri or os.environ.get(
|
||||
"WECHAT_OPEN_REDIRECT_URI", ""
|
||||
)
|
||||
self.redirect_uri = redirect_uri or os.environ.get("WECHAT_OPEN_REDIRECT_URI", "")
|
||||
self._state_store = state_store # 可选:state 存储(Redis/内存),用于 CSRF 防护
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
@@ -59,12 +58,14 @@ class WechatOAuthService:
|
||||
|
||||
if not self.is_configured():
|
||||
# 未配置时返回 mock URL,方便前端联调
|
||||
mock_params = urllib.parse.urlencode({
|
||||
"app_id": "mock",
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"scope": scope,
|
||||
"state": state,
|
||||
})
|
||||
mock_params = urllib.parse.urlencode(
|
||||
{
|
||||
"app_id": "mock",
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"scope": scope,
|
||||
"state": state,
|
||||
}
|
||||
)
|
||||
return f"/mock/wechat/auth?{mock_params}", state
|
||||
|
||||
params = {
|
||||
@@ -94,12 +95,15 @@ class WechatOAuthService:
|
||||
if not self.is_configured():
|
||||
# 开发模式:返回 mock 用户信息
|
||||
logger.info("微信未配置,使用 mock 用户信息")
|
||||
return WechatUserInfo(
|
||||
openid=f"mock_{code[:20]}",
|
||||
unionid=f"mock_union_{code[:16]}",
|
||||
nickname="微信测试用户",
|
||||
avatar_url="",
|
||||
), None
|
||||
return (
|
||||
WechatUserInfo(
|
||||
openid=f"mock_{code[:20]}",
|
||||
unionid=f"mock_union_{code[:16]}",
|
||||
nickname="微信测试用户",
|
||||
avatar_url="",
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. 用 code 换 access_token
|
||||
@@ -135,12 +139,15 @@ class WechatOAuthService:
|
||||
logger.error("微信获取用户信息失败: %s", user_data)
|
||||
return None, f"获取用户信息失败: {user_data.get('errmsg', '未知错误')}"
|
||||
|
||||
return WechatUserInfo(
|
||||
openid=openid,
|
||||
unionid=unionid,
|
||||
nickname=user_data.get("nickname", ""),
|
||||
avatar_url=user_data.get("headimgurl", ""),
|
||||
), None
|
||||
return (
|
||||
WechatUserInfo(
|
||||
openid=openid,
|
||||
unionid=unionid,
|
||||
nickname=user_data.get("nickname", ""),
|
||||
avatar_url=user_data.get("headimgurl", ""),
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error("微信 OAuth 请求异常: %s", e, exc_info=True)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
验证码服务 + 绑定流程单元测试
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.adapters.in_memory.user_repository import InMemoryUserRepository
|
||||
from packages.application.auth.bind_contact_use_case import (
|
||||
BindContactRequest,
|
||||
@@ -16,9 +17,9 @@ from packages.application.auth.verification_code_service import (
|
||||
CODE_TYPE_EMAIL_BIND,
|
||||
CODE_TYPE_PHONE_BIND,
|
||||
VerificationCodeService,
|
||||
normalize_phone,
|
||||
validate_email,
|
||||
validate_phone,
|
||||
normalize_phone,
|
||||
)
|
||||
from packages.domain.entities import User
|
||||
from packages.domain.verification_code import VerificationCode
|
||||
@@ -34,10 +35,7 @@ class InMemoryVerificationCodeRepository:
|
||||
self._codes[code.id] = code
|
||||
|
||||
def find_latest(self, recipient, code_type):
|
||||
candidates = [
|
||||
c for c in self._codes.values()
|
||||
if c.recipient == recipient and c.code_type == code_type
|
||||
]
|
||||
candidates = [c for c in self._codes.values() if c.recipient == recipient and c.code_type == code_type]
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda c: c.created_at)
|
||||
@@ -49,10 +47,9 @@ class InMemoryVerificationCodeRepository:
|
||||
now = datetime.now(timezone.utc)
|
||||
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return sum(
|
||||
1 for c in self._codes.values()
|
||||
if c.recipient == recipient
|
||||
and c.code_type == code_type
|
||||
and c.created_at >= start_of_day
|
||||
1
|
||||
for c in self._codes.values()
|
||||
if c.recipient == recipient and c.code_type == code_type and c.created_at >= start_of_day
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user