Files
weibo_signin/.kiro/specs/multi-user-signin/design.md
2026-03-09 14:05:00 +08:00

21 KiB
Raw Blame History

设计文档Weibo-HotSign 多用户签到系统

概述

本设计文档描述 Weibo-HotSign 系统的架构重构与核心功能实现方案。核心目标是:

  1. 引入 backend/shared/ 共享模块,统一 ORM 模型、数据库连接和加密工具,消除各服务间的代码重复
  2. 完善 auth_service,实现 Refresh Token 机制
  3. 从零实现 api_service,提供微博账号 CRUD、任务配置和签到日志查询 API
  4. signin_executortask_scheduler 中的 Mock 实现替换为真实数据库交互
  5. 所有 API 遵循统一响应格式

技术栈Python 3.11 + FastAPI + SQLAlchemy (async) + Celery + MySQL (aiomysql) + Redis

架构

重构后的服务架构

graph TD
    subgraph "客户端"
        FE[Web Frontend / API Client]
    end

    subgraph "后端服务层"
        API[API_Service :8000<br/>账号/任务/日志管理]
        AUTH[Auth_Service :8001<br/>注册/登录/Token刷新]
        SCHED[Task_Scheduler<br/>Celery Beat]
        EXEC[Signin_Executor<br/>Celery Worker]
    end

    subgraph "共享层"
        SHARED[shared/<br/>ORM Models + DB Session<br/>+ Crypto Utils + Response Format]
    end

    subgraph "基础设施"
        MYSQL[(MySQL)]
        REDIS[(Redis<br/>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)

# 提供异步 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)

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)

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 内部流程

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 模型

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 模型

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 模型

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 模型

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_idTTL 为 7 天。这避免了在数据库中增加额外的表,同时利用 Redis 的自动过期机制。

# 存储
await redis.setex(f"refresh_token:{token_hash}", 7 * 24 * 3600, user_id)

# 验证
user_id = await redis.get(f"refresh_token:{token_hash}")

# 刷新时删除旧 token生成新 tokenToken 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是指在系统所有有效执行中都应成立的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规格说明与机器可验证正确性保证之间的桥梁。

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_idstatustopic_title 与执行结果一致。

Validates: Requirements 6.1, 6.4

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=truedata 字段;错误响应应包含 success=falsedata=nullerror 字段。

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 服务注册统一的异常处理器:

@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(异步测试支持)
  • 属性测试hypothesisPython 属性测试库)
  • 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 任务的调度和执行

属性测试配置

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              # 共享 fixturesDB 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  # 签到执行流程