# SMS 服务完整实现与技术栈合规性优化 **日期**: 2026-01-29 **类型**: 功能实现 + 代码优化 **影响范围**: `server/app/services/sms_service.py`, `server/app/repositories/sms_repository.py`, `server/app/schemas/sms.py`, `server/app/api/v1/auth.py` --- ## 变更概述 完成短信验证码服务的完整实现,并优化代码以符合 jointo-tech-stack 技术栈规范,包括依赖注入、异常处理、日志记录、时区处理和统一响应格式。 --- ## 主要变更 ### 1. Service 层优化 (`sms_service.py`) #### 1.1 依赖注入优化 **问题**: Service 层直接创建 Redis 客户端,违反依赖注入原则 **修正前**: ```python def __init__(self, session: AsyncSession): self.redis_client: Optional[Redis] = None self.http_client = httpx.AsyncClient(timeout=10.0) async def _get_redis(self) -> Redis: if self.redis_client is None: self.redis_client = Redis.from_url(settings.REDIS_URL) return self.redis_client ``` **修正后**: ```python def __init__(self, session: AsyncSession, redis_client: Redis): """ Args: session: 异步数据库会话 redis_client: 异步 Redis 客户端(由依赖注入提供) """ self.redis_client = redis_client self._http_client: Optional[httpx.AsyncClient] = None @property def http_client(self) -> httpx.AsyncClient: """懒加载 HTTP 客户端""" if self._http_client is None: self._http_client = httpx.AsyncClient(timeout=10.0) return self._http_client ``` #### 1.2 时区处理修正 **问题**: 使用 `datetime.utcnow()` 而非 `datetime.now(timezone.utc)` **修正**: ```python # 修正前 expires_at=datetime.utcnow() + timedelta(minutes=10) # 修正后 from datetime import datetime, timezone expires_at=datetime.now(timezone.utc) + timedelta(minutes=10) ``` #### 1.3 日志系统统一 **问题**: 使用 loguru 而非项目标准的 logging 模块 **修正**: ```python # 修正前 from loguru import logger # 修正后 from app.core.logging import logger ``` #### 1.4 增强错误处理和日志 **Service 层方法增强**: ```python async def send_verification_code(...) -> dict: try: # 业务逻辑 logger.info( f"验证码已发送", extra={ "phone": phone, "country_code": country_code, "purpose": purpose.to_string(), "ip_address": ip_address } ) return result except (RateLimitError, ValidationError): raise except Exception as e: logger.error(f"发送验证码失败: {str(e)}", exc_info=True) raise ValidationError("发送验证码失败,请稍后重试") ``` #### 1.5 资源管理优化 **问题**: 使用 `close()` 方法手动清理资源 **修正**: 实现异步上下文管理器 ```python async def __aenter__(self): """异步上下文管理器入口""" return self async def __aexit__(self, exc_type, exc_val, exc_tb): """异步上下文管理器退出,清理资源""" if self._http_client is not None: await self._http_client.aclose() ``` #### 1.6 测试验证码优化 **修正**: 统一测试验证码为 4 位 ```python # 修正前 if code == "666666": # 修正后 if code == "6666": ``` ### 2. Repository 层优化 (`sms_repository.py`) #### 2.1 添加异常处理 **所有数据库操作增加异常处理**: ```python async def create(self, code: SmsVerificationCode) -> SmsVerificationCode: try: self.session.add(code) await self.session.commit() await self.session.refresh(code) return code except SQLAlchemyError as e: await self.session.rollback() logger.error(f"创建验证码失败: {str(e)}", exc_info=True) raise DatabaseError("创建验证码失败") ``` #### 2.2 时区处理统一 **修正所有时间比较**: ```python # 修正前 SmsVerificationCode.expires_at > datetime.utcnow() code.updated_at = datetime.utcnow() # 修正后 from datetime import datetime, timezone SmsVerificationCode.expires_at > datetime.now(timezone.utc) code.updated_at = datetime.now(timezone.utc) ``` #### 2.3 软删除优化 **批量软删除时使用统一时间戳**: ```python async def delete_expired(self, cutoff_time: datetime) -> int: try: # ... count = 0 now = datetime.now(timezone.utc) # 统一时间戳 for code in codes: code.deleted_at = now count += 1 # ... except SQLAlchemyError as e: await self.session.rollback() logger.error(f"删除过期验证码失败: {str(e)}", exc_info=True) return 0 ``` ### 3. Schema 层优化 (`sms.py`) #### 3.1 添加 ConfigDict **所有 Schema 添加配置**: ```python from pydantic import BaseModel, Field, field_validator, ConfigDict class SmsCodeSendRequest(BaseModel): model_config = ConfigDict(populate_by_name=True) phone: str = Field(..., min_length=11, max_length=11, description="手机号") country_code: str = Field(default="+86", alias="countryCode", description="国家区号") purpose: SmsPurposeEnum = Field(default=SmsPurposeEnum.LOGIN, description="用途") ``` #### 3.2 响应 Schema 优化 **统一字段命名**: ```python class SmsCodeVerifyResponse(BaseModel): model_config = ConfigDict(populate_by_name=True) verified: bool = Field(..., description="验证是否成功") # 修正前: success ``` ### 4. API 路由层优化 (`auth.py`) #### 4.1 依赖注入更新 **添加 Redis 依赖注入**: ```python from redis.asyncio import Redis from app.api.deps import get_redis @router.post("/sms/send", ...) async def send_sms_code( request: Request, body: SmsCodeSendRequest, session: AsyncSession = Depends(get_session), redis_client: Redis = Depends(get_redis) # 新增 ): sms_service = SmsService(session, redis_client) # 传入 redis_client ``` #### 4.2 统一响应格式 **使用 ApiResponse 包装器**: ```python from app.schemas.response import ApiResponse from app.schemas.sms import SmsCodeSendResponse @router.post( "/sms/send", response_model=ApiResponse[SmsCodeSendResponse], # 统一响应格式 summary="发送短信验证码" ) async def send_sms_code(...): result = await sms_service.send_verification_code(...) return ApiResponse.success( data=SmsCodeSendResponse( message=result["message"], expires_in=result["expires_in"] ) ) ``` #### 4.3 枚举转换优化 **简化枚举转换逻辑**: ```python # 修正前 purpose_map = { 'login': SmsPurpose.LOGIN, 'bind_phone': SmsPurpose.BIND_PHONE, 'reset_password': SmsPurpose.RESET_PASSWORD } purpose_enum = purpose_map.get(request.purpose, SmsPurpose.LOGIN) # 修正后 purpose_enum = SmsPurpose.from_string(body.purpose.value) ``` --- ## 技术栈合规性检查 ### ✅ 已符合规范 - [x] 异步编程(`async/await`、`AsyncSession`、`asyncpg`) - [x] UUID v7 主键(`generate_uuid`) - [x] 枚举类型(`IntEnum` + `SMALLINT`) - [x] 仓储模式(Service/Repository 分层) - [x] 时间戳字段(`TIMESTAMPTZ` + `timezone.utc`) - [x] 索引设计(单列、组合、条件索引) - [x] 无物理外键 - [x] Redis 异步客户端 - [x] HTTP 异步客户端 ### ✅ 新增合规项 - [x] 依赖注入(Redis 客户端由 FastAPI 提供) - [x] 资源管理(异步上下文管理器) - [x] 错误处理(`try-except` + 异常转换) - [x] 日志记录(结构化日志 + `extra` 字段) - [x] Schema 配置(`ConfigDict` + `populate_by_name`) - [x] 字段验证(`@field_validator`) - [x] 统一响应格式(`ApiResponse` + `timestamp`) --- ## 文件变更清单 ### 修改的文件 1. **server/app/services/sms_service.py** - 优化依赖注入(Redis 客户端) - 修正时区处理 - 增强错误处理和日志 - 实现异步上下文管理器 - 统一测试验证码 2. **server/app/repositories/sms_repository.py** - 添加异常处理 - 修正时区处理 - 优化软删除逻辑 - 添加日志记录 3. **server/app/schemas/sms.py** - 添加 `ConfigDict` - 优化响应 Schema 字段命名 4. **server/app/api/v1/auth.py** - 添加 Redis 依赖注入 - 使用统一响应格式 - 简化枚举转换逻辑 --- ## 测试建议 ### 单元测试 ```bash # 运行 SMS 服务单元测试 docker exec jointo-server-app pytest tests/unit/test_sms_service.py -v # 运行 SMS Repository 单元测试 docker exec jointo-server-app pytest tests/unit/test_sms_repository.py -v ``` ### 集成测试 ```bash # 运行 SMS API 集成测试 docker exec jointo-server-app pytest tests/integration/test_sms_api.py -v ``` ### 测试覆盖率 ```bash # 生成覆盖率报告 docker exec jointo-server-app pytest tests/ -v \ --cov=app/services/sms_service \ --cov=app/repositories/sms_repository \ --cov-report=html ``` --- ## 后续优化建议 ### 1. 性能优化 - 考虑使用 Redis Pipeline 批量操作 - 添加验证码发送成功率监控 - 优化数据库查询索引 ### 2. 安全增强 - 添加验证码尝试次数限制 - 实现验证码加密存储 - 添加异常 IP 黑名单机制 ### 3. 功能扩展 - 支持多短信服务商(腾讯云、华为云) - 添加短信模板管理 - 实现短信发送队列 --- ## 相关文档 - [SMS 服务文档](../../requirements/backend/04-services/user/sms-service.md) - [jointo-tech-stack Skill](../../.claude/skills/jointo-tech-stack/SKILL.md) - [后端测试规范](../../.claude/skills/jointo-tech-stack/references/testing.md) - [API 设计规范](../../.claude/skills/jointo-tech-stack/references/api-design.md) --- **变更作者**: Kiro AI **审核状态**: 待审核 **文档版本**: v1.0