# 设计文档:Weibo-HotSign 多用户签到系统 ## 概述 本设计文档描述 Weibo-HotSign 系统的架构重构与核心功能实现方案。核心目标是: 1. 引入 `backend/shared/` 共享模块,统一 ORM 模型、数据库连接和加密工具,消除各服务间的代码重复 2. 完善 `auth_service`,实现 Refresh Token 机制 3. 从零实现 `api_service`,提供微博账号 CRUD、任务配置和签到日志查询 API 4. 将 `signin_executor` 和 `task_scheduler` 中的 Mock 实现替换为真实数据库交互 5. 所有 API 遵循统一响应格式 技术栈:Python 3.11 + FastAPI + SQLAlchemy (async) + Celery + MySQL (aiomysql) + Redis ## 架构 ### 重构后的服务架构 ```mermaid graph TD subgraph "客户端" FE[Web Frontend / API Client] end subgraph "后端服务层" API[API_Service :8000
账号/任务/日志管理] AUTH[Auth_Service :8001
注册/登录/Token刷新] SCHED[Task_Scheduler
Celery Beat] EXEC[Signin_Executor
Celery Worker] end subgraph "共享层" SHARED[shared/
ORM Models + DB Session
+ Crypto Utils + Response Format] end subgraph "基础设施" MYSQL[(MySQL)] REDIS[(Redis
Cache + Message Queue)] PROXY[Proxy Pool] end FE -->|REST API| API FE -->|REST API| AUTH API -->|导入| SHARED AUTH -->|导入| SHARED SCHED -->|导入| SHARED EXEC -->|导入| SHARED SHARED -->|aiomysql| MYSQL SCHED -->|发布任务| REDIS EXEC -->|消费任务| REDIS EXEC -->|获取代理| PROXY EXEC -->|签到请求| WEIBO[Weibo.com] ``` ### 关键架构决策 1. **共享模块而非微服务间 RPC**:各服务通过 Python 包导入 `shared/` 模块访问数据库,而非通过 HTTP 调用其他服务查询数据。这简化了部署,减少了网络延迟,适合当前规模。 2. **API_Service 作为唯一对外网关**:所有账号管理、任务配置、日志查询 API 集中在 `api_service` 中,`auth_service` 仅负责认证。 3. **Celery 同时承担调度和执行**:`task_scheduler` 运行 Celery Beat(调度),`signin_executor` 运行 Celery Worker(执行),通过 Redis 消息队列解耦。 4. **Dockerfile 多阶段构建**:保持现有的多阶段 Dockerfile 结构,新增 `shared/` 目录的 COPY 步骤。 ### 目录结构(重构后) ``` backend/ ├── shared/ # 新增:共享模块 │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ ├── base.py # SQLAlchemy Base + engine + session │ │ ├── user.py # User ORM model │ │ ├── account.py # Account ORM model │ │ ├── task.py # Task ORM model │ │ └── signin_log.py # SigninLog ORM model │ ├── crypto.py # AES-256-GCM 加密/解密工具 │ ├── response.py # 统一响应格式工具 │ └── config.py # 共享配置(DB URL, Redis URL 等) ├── auth_service/ │ └── app/ │ ├── main.py # 重构:使用 shared models │ ├── config.py │ ├── schemas/ │ │ └── user.py # 增加 RefreshToken schema │ ├── services/ │ │ └── auth_service.py # 增加 refresh token 逻辑 │ └── utils/ │ └── security.py # 增加 refresh token 生成/验证 ├── api_service/ │ └── app/ │ ├── __init__.py │ ├── main.py # 新增:FastAPI 应用入口 │ ├── config.py │ ├── dependencies.py # JWT 认证依赖 │ ├── schemas/ │ │ ├── __init__.py │ │ ├── account.py # 账号请求/响应 schema │ │ ├── task.py # 任务请求/响应 schema │ │ └── signin_log.py # 签到日志响应 schema │ └── routers/ │ ├── __init__.py │ ├── accounts.py # 账号 CRUD 路由 │ ├── tasks.py # 任务 CRUD 路由 │ └── signin_logs.py # 签到日志查询路由 ├── signin_executor/ │ └── app/ │ ├── main.py │ ├── config.py │ ├── services/ │ │ ├── signin_service.py # 重构:使用 shared models 查询真实数据 │ │ └── weibo_client.py # 重构:实现真实加密/解密 │ └── models/ │ └── signin_models.py # 保留 Pydantic 请求/响应模型 ├── task_scheduler/ │ └── app/ │ ├── celery_app.py # 重构:从 DB 动态加载任务 │ ├── config.py │ └── tasks/ │ └── signin_tasks.py # 重构:使用真实 DB 查询 ├── Dockerfile # 更新:各阶段 COPY shared/ └── requirements.txt ``` ## 组件与接口 ### 1. shared 模块 #### 1.1 数据库连接管理 (`shared/models/base.py`) ```python # 提供异步 engine 和 session factory # 所有服务通过 get_db() 获取 AsyncSession async def get_db() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: try: yield session finally: await session.close() ``` #### 1.2 加密工具 (`shared/crypto.py`) ```python def encrypt_cookie(plaintext: str, key: bytes) -> tuple[str, str]: """AES-256-GCM 加密,返回 (密文base64, iv_base64)""" def decrypt_cookie(ciphertext_b64: str, iv_b64: str, key: bytes) -> str: """AES-256-GCM 解密,返回原始 Cookie 字符串""" ``` #### 1.3 统一响应格式 (`shared/response.py`) ```python def success_response(data: Any, message: str = "Operation successful") -> dict def error_response(message: str, code: str, details: list = None, status_code: int = 400) -> JSONResponse ``` ### 2. Auth_Service 接口 | 方法 | 路径 | 描述 | 需求 | |------|------|------|------| | POST | `/auth/register` | 用户注册 | 1.1, 1.2, 1.8 | | POST | `/auth/login` | 用户登录,返回 access_token + refresh_token | 1.3, 1.4 | | POST | `/auth/refresh` | 刷新 Token | 1.5, 1.6 | | GET | `/auth/me` | 获取当前用户信息 | 1.7 | ### 3. API_Service 接口 | 方法 | 路径 | 描述 | 需求 | |------|------|------|------| | POST | `/api/v1/accounts` | 添加微博账号 | 2.1, 2.7, 2.8 | | GET | `/api/v1/accounts` | 获取账号列表 | 2.2, 2.8 | | GET | `/api/v1/accounts/{id}` | 获取账号详情 | 2.3, 2.6, 2.8 | | PUT | `/api/v1/accounts/{id}` | 更新账号信息 | 2.4, 2.6, 2.8 | | DELETE | `/api/v1/accounts/{id}` | 删除账号 | 2.5, 2.6, 2.8 | | POST | `/api/v1/accounts/{id}/tasks` | 创建签到任务 | 4.1, 4.2, 4.6 | | GET | `/api/v1/accounts/{id}/tasks` | 获取任务列表 | 4.3 | | PUT | `/api/v1/tasks/{id}` | 启用/禁用任务 | 4.4 | | DELETE | `/api/v1/tasks/{id}` | 删除任务 | 4.5 | | GET | `/api/v1/accounts/{id}/signin-logs` | 查询签到日志 | 8.1, 8.2, 8.3, 8.4, 8.5 | ### 4. Task_Scheduler 内部接口 Task_Scheduler 不对外暴露 HTTP 接口,通过以下方式工作: - **启动时**:从 DB 加载 `is_enabled=True` 的任务,注册到 Celery Beat - **运行时**:根据 Cron 表达式触发 `execute_signin_task` Celery task - **动态更新**:通过 Redis pub/sub 接收任务变更通知,动态更新调度 ### 5. Signin_Executor 内部流程 ```mermaid sequenceDiagram participant Queue as Redis Queue participant Exec as Signin_Executor participant DB as MySQL participant Weibo as Weibo.com participant Proxy as Proxy Pool Queue->>Exec: 消费签到任务 (task_id, account_id) Exec->>DB: 查询 Account (by account_id) Exec->>Exec: 解密 Cookie (AES-256-GCM) Exec->>Weibo: 验证 Cookie 有效性 alt Cookie 无效 Exec->>DB: 更新 account.status = "invalid_cookie" Exec->>DB: 写入失败日志 else Cookie 有效 Exec->>Weibo: 获取超话列表 loop 每个未签到超话 Exec->>Proxy: 获取代理 IP Exec->>Exec: 随机延迟 (1-3s) Exec->>Weibo: 执行签到请求 Exec->>DB: 写入 signin_log end end ``` ## 数据模型 ### ORM 模型定义(shared/models/) #### User 模型 ```python class User(Base): __tablename__ = "users" id = Column(String(36), primary_key=True, default=lambda: str(uuid4())) username = Column(String(50), unique=True, nullable=False, index=True) email = Column(String(255), unique=True, nullable=False, index=True) hashed_password = Column(String(255), nullable=False) created_at = Column(DateTime, server_default=func.now()) is_active = Column(Boolean, default=True) # Relationships accounts = relationship("Account", back_populates="user", cascade="all, delete-orphan") ``` #### Account 模型 ```python class Account(Base): __tablename__ = "accounts" id = Column(String(36), primary_key=True, default=lambda: str(uuid4())) user_id = Column(String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False) weibo_user_id = Column(String(20), nullable=False) remark = Column(String(100)) encrypted_cookies = Column(Text, nullable=False) iv = Column(String(32), nullable=False) status = Column(String(20), default="pending") # pending, active, invalid_cookie, banned last_checked_at = Column(DateTime, nullable=True) created_at = Column(DateTime, server_default=func.now()) # Relationships user = relationship("User", back_populates="accounts") tasks = relationship("Task", back_populates="account", cascade="all, delete-orphan") signin_logs = relationship("SigninLog", back_populates="account") ``` #### Task 模型 ```python class Task(Base): __tablename__ = "tasks" id = Column(String(36), primary_key=True, default=lambda: str(uuid4())) account_id = Column(String(36), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False) cron_expression = Column(String(50), nullable=False) is_enabled = Column(Boolean, default=True) created_at = Column(DateTime, server_default=func.now()) # Relationships account = relationship("Account", back_populates="tasks") ``` #### SigninLog 模型 ```python class SigninLog(Base): __tablename__ = "signin_logs" id = Column(BigInteger, primary_key=True, autoincrement=True) account_id = Column(String(36), ForeignKey("accounts.id"), nullable=False) topic_title = Column(String(100)) status = Column(String(20), nullable=False) # success, failed_already_signed, failed_network, failed_banned reward_info = Column(JSON, nullable=True) error_message = Column(Text, nullable=True) signed_at = Column(DateTime, server_default=func.now()) # Relationships account = relationship("Account", back_populates="signin_logs") ``` ### Refresh Token 存储 Refresh Token 使用 Redis 存储,key 格式为 `refresh_token:{token_hash}`,value 为 `user_id`,TTL 为 7 天。这避免了在数据库中增加额外的表,同时利用 Redis 的自动过期机制。 ```python # 存储 await redis.setex(f"refresh_token:{token_hash}", 7 * 24 * 3600, user_id) # 验证 user_id = await redis.get(f"refresh_token:{token_hash}") # 刷新时删除旧 token,生成新 token(Token Rotation) await redis.delete(f"refresh_token:{old_token_hash}") await redis.setex(f"refresh_token:{new_token_hash}", 7 * 24 * 3600, user_id) ``` ## 正确性属性 (Correctness Properties) *属性(Property)是指在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。* ### Property 1: Cookie 加密 Round-trip *For any* 有效的 Cookie 字符串,使用 AES-256-GCM 加密后再用相同密钥和 IV 解密,应产生与原始字符串完全相同的结果。 **Validates: Requirements 3.1, 3.2, 3.3** ### Property 2: 用户注册后可登录获取信息 *For any* 有效的注册信息(用户名、邮箱、符合强度要求的密码),注册后使用相同邮箱和密码登录应成功返回 Token,使用该 Token 调用 `/auth/me` 应返回与注册时一致的用户名和邮箱。 **Validates: Requirements 1.1, 1.3, 1.7** ### Property 3: 用户名/邮箱唯一性约束 *For any* 已注册的用户,使用相同用户名或相同邮箱再次注册应返回 409 Conflict 错误。 **Validates: Requirements 1.2** ### Property 4: 无效凭证登录拒绝 *For any* 不存在的邮箱或错误的密码,登录请求应返回 401 Unauthorized 错误。 **Validates: Requirements 1.4** ### Property 5: Refresh Token 轮换 *For any* 已登录用户的有效 Refresh Token,刷新操作应返回新的 Access Token 和新的 Refresh Token,且旧的 Refresh Token 应失效(再次使用应返回 401)。 **Validates: Requirements 1.5, 1.6** ### Property 6: 弱密码拒绝 *For any* 不满足强度要求的密码(缺少大写字母、小写字母、数字或特殊字符,或长度不足8位),注册请求应返回 400 Bad Request。 **Validates: Requirements 1.8** ### Property 7: 账号创建与列表一致性 *For any* 用户和任意数量的有效微博账号数据,创建 N 个账号后查询列表应返回恰好 N 条记录,每条记录的状态应为 "pending",且响应中不应包含解密后的 Cookie 明文。 **Validates: Requirements 2.1, 2.2, 2.7** ### Property 8: 账号详情 Round-trip *For any* 已创建的微博账号,通过详情接口查询应返回与创建时一致的备注和微博用户 ID。 **Validates: Requirements 2.3** ### Property 9: 账号更新反映 *For any* 已创建的微博账号和任意新的备注字符串,更新备注后再次查询应返回更新后的值。 **Validates: Requirements 2.4** ### Property 10: 账号删除级联 *For any* 拥有关联 Task 和 SigninLog 的账号,删除该账号后,查询该账号的 Task 列表和 SigninLog 列表应返回空结果。 **Validates: Requirements 2.5** ### Property 11: 跨用户资源隔离 *For any* 两个不同用户 A 和 B,用户 A 尝试访问、修改或删除用户 B 的账号、任务或签到日志时,应返回 403 Forbidden。 **Validates: Requirements 2.6, 4.6, 8.5** ### Property 12: 受保护接口认证要求 *For any* 受保护的 API 端点(账号管理、任务管理、日志查询),不携带 JWT Token 或携带无效 Token 的请求应返回 401 Unauthorized。 **Validates: Requirements 2.8, 8.4, 9.4** ### Property 13: 有效 Cron 表达式创建任务 *For any* 有效的 Cron 表达式和已存在的账号,创建任务应成功,且查询该账号的任务列表应包含新创建的任务。 **Validates: Requirements 4.1, 4.3** ### Property 14: 无效 Cron 表达式拒绝 *For any* 无效的 Cron 表达式字符串,创建任务请求应返回 400 Bad Request。 **Validates: Requirements 4.2** ### Property 15: 任务启用/禁用切换 *For any* 已创建的任务,切换 `is_enabled` 状态后查询应反映新的状态值。 **Validates: Requirements 4.4** ### Property 16: 任务删除 *For any* 已创建的任务,删除后查询该任务应返回 404 或不在列表中出现。 **Validates: Requirements 4.5** ### Property 17: 调度器加载已启用任务 *For any* 数据库中的任务集合,Task_Scheduler 启动时加载的任务数量应等于 `is_enabled=True` 的任务数量。 **Validates: Requirements 5.1** ### Property 18: 分布式锁防重复调度 *For any* 签到任务,同一时刻并发触发两次应只产生一次实际执行。 **Validates: Requirements 5.5** ### Property 19: 签到结果持久化 *For any* 签到执行结果(成功或失败),`signin_logs` 表中应存在对应的记录,且记录的 `account_id`、`status` 和 `topic_title` 与执行结果一致。 **Validates: Requirements 6.1, 6.4** ### Property 20: Cookie 失效时更新账号状态 *For any* Cookie 已失效的账号,执行签到时应将账号状态更新为 "invalid_cookie"。 **Validates: Requirements 6.5, 3.4** ### Property 21: 随机延迟范围 *For any* 调用反爬虫延迟函数的结果,延迟值应在配置的 `[min, max]` 范围内。 **Validates: Requirements 7.1** ### Property 22: User-Agent 来源 *For any* 调用 User-Agent 选择函数的结果,返回的 UA 字符串应属于预定义列表中的某一个。 **Validates: Requirements 7.2** ### Property 23: 签到日志时间倒序 *For any* 包含多条签到日志的账号,查询返回的日志列表应按 `signed_at` 降序排列。 **Validates: Requirements 8.1** ### Property 24: 签到日志分页 *For any* 包含 N 条日志的账号和分页参数 (page, size),返回的记录数应等于 `min(size, N - (page-1)*size)` 且总记录数应等于 N。 **Validates: Requirements 8.2** ### Property 25: 签到日志状态过滤 *For any* 状态过滤参数,返回的所有日志记录的 `status` 字段应与过滤参数一致。 **Validates: Requirements 8.3** ### Property 26: 统一响应格式 *For any* API 调用,成功响应应包含 `success=true` 和 `data` 字段;错误响应应包含 `success=false`、`data=null` 和 `error` 字段。 **Validates: Requirements 9.1, 9.2, 9.3** ## 错误处理 ### 错误分类与处理策略 | 错误类型 | HTTP 状态码 | 错误码 | 处理策略 | |----------|------------|--------|----------| | 请求参数校验失败 | 400 | VALIDATION_ERROR | 返回字段级错误详情 | | 未认证 | 401 | UNAUTHORIZED | 返回标准 401 响应 | | 权限不足 | 403 | FORBIDDEN | 返回资源不可访问提示 | | 资源不存在 | 404 | NOT_FOUND | 返回资源未找到提示 | | 资源冲突 | 409 | CONFLICT | 返回冲突字段说明 | | 服务器内部错误 | 500 | INTERNAL_ERROR | 记录详细日志,返回通用错误提示 | ### 签到执行错误处理 - **Cookie 解密失败**:标记账号为 `invalid_cookie`,记录错误日志,终止该账号签到 - **Cookie 验证失败**(微博返回未登录):同上 - **网络超时/连接错误**:记录 `failed_network` 日志,不更改账号状态(可能是临时问题) - **微博返回封禁**:标记账号为 `banned`,记录日志,发送通知 - **代理池不可用**:降级为直连,记录警告日志 - **Celery 任务失败**:自动重试最多3次,间隔60秒,最终失败记录日志 ### 全局异常处理 所有 FastAPI 服务注册统一的异常处理器: ```python @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): return error_response(exc.detail, f"HTTP_{exc.status_code}", status_code=exc.status_code) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): details = [{"field": e["loc"][-1], "message": e["msg"]} for e in exc.errors()] return error_response("Validation failed", "VALIDATION_ERROR", details, 400) ``` ## 测试策略 ### 测试框架选择 - **单元测试**:`pytest` + `pytest-asyncio`(异步测试支持) - **属性测试**:`hypothesis`(Python 属性测试库) - **HTTP 测试**:`httpx` + FastAPI `TestClient` - **数据库测试**:使用 SQLite in-memory 或测试专用 MySQL 实例 ### 测试分层 #### 1. 单元测试 - 加密/解密函数的边界情况 - 密码强度验证的各种组合 - Cron 表达式验证 - 响应格式化函数 - 具体的错误场景(网络超时、解密失败等) #### 2. 属性测试(Property-Based Testing) - 使用 `hypothesis` 库,每个属性测试至少运行 100 次迭代 - 每个测试用注释标注对应的设计文档属性编号 - 标注格式:`# Feature: multi-user-signin, Property {N}: {property_text}` - 每个正确性属性对应一个独立的属性测试函数 #### 3. 集成测试 - API 端点的完整请求/响应流程 - 数据库 CRUD 操作的正确性 - 服务间通过 Redis 消息队列的交互 - Celery 任务的调度和执行 ### 属性测试配置 ```python from hypothesis import given, settings, strategies as st @settings(max_examples=100) @given(cookie=st.text(min_size=1, max_size=1000)) def test_cookie_encryption_roundtrip(cookie): """Feature: multi-user-signin, Property 1: Cookie 加密 Round-trip""" key = generate_test_key() ciphertext, iv = encrypt_cookie(cookie, key) decrypted = decrypt_cookie(ciphertext, iv, key) assert decrypted == cookie ``` ### 测试目录结构 ``` backend/ ├── tests/ │ ├── conftest.py # 共享 fixtures(DB session, test client 等) │ ├── unit/ │ │ ├── test_crypto.py # 加密/解密单元测试 │ │ ├── test_password.py # 密码验证单元测试 │ │ └── test_cron.py # Cron 表达式验证单元测试 │ ├── property/ │ │ ├── test_crypto_props.py # Property 1 │ │ ├── test_auth_props.py # Property 2-6 │ │ ├── test_account_props.py # Property 7-12 │ │ ├── test_task_props.py # Property 13-18 │ │ ├── test_signin_props.py # Property 19-22 │ │ └── test_log_props.py # Property 23-26 │ └── integration/ │ ├── test_auth_flow.py # 完整认证流程 │ ├── test_account_flow.py # 账号管理流程 │ └── test_signin_flow.py # 签到执行流程 ```