Files
social-app/docs/plans/2026-03-30-calendar-detail-show-subscribers.md
T

5.7 KiB

Calendar Detail - Show Subscribers Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 在日历详情页显示已订阅此日历的用户列表

Architecture:

  • 后端:在 GET /api/v1/schedule-items/{id} 响应中返回 subscribers 列表
  • 前端:在详情页渲染订阅者列表组件

Tech Stack: Flutter, FastAPI, PostgreSQL


Task 1: 后端 - 返回订阅者列表

Files:

  • Modify: backend/src/v1/schedule_items/schemas.py

  • Modify: backend/src/v1/schedule_items/service.py

  • Verify: backend/tests/integration/v1/test_schedule_items_routes.py

  • Step 1: 新增 SubscriberInfo Schema

# backend/src/v1/schedule_items/schemas.py

class SubscriberInfo(BaseModel):
    """订阅者信息"""
    user_id: UUID
    username: str
    avatar_url: str | None = None
    permission: int  # 位标志: 1=view, 2=invite, 4=edit
    status: str  # active, pending, paused, unsubscribed
    subscribed_at: datetime
  • Step 2: 修改 ScheduleItemResponse
class ScheduleItemResponse(BaseModel):
    # ... existing fields ...
    subscribers: List[SubscriberInfo] = []  # 新增
  • Step 3: 修改 service.get_by_id 填充 subscribers
# backend/src/v1/schedule_items/service.py

async def get_by_id(self, item_id: UUID) -> ScheduleItemResponse:
    item = await self._repository.get_by_id(item_id)
    if not item:
        raise ScheduleItemNotFoundError(item_id)
    
    # 获取订阅者列表
    subscriptions = await self._repository.get_subscriptions_by_item_id(item_id)
    subscribers = []
    for sub in subscriptions:
        if sub.status == 'active':  # 只返回活跃订阅者
            user = await self._user_repo.get_by_id(sub.subscriber_id)
            if user:
                subscribers.append(SubscriberInfo(
                    user_id=user.id,
                    username=user.username,
                    avatar_url=user.avatar_url,
                    permission=sub.permission,
                    status=sub.status,
                    subscribed_at=sub.created_at,
                ))
    
    return ScheduleItemResponse(
        # ... existing fields ...,
        subscribers=subscribers,
    )
  • Step 4: 编写集成测试
# backend/tests/integration/v1/test_schedule_items_routes.py

async def test_get_item_returns_subscribers(self):
    """测试获取日程时返回订阅者列表"""
    # Arrange: 创建日程,添加订阅者
    # Act: GET /api/v1/schedule-items/{id}
    # Assert: 响应包含 subscribers 字段

Task 2: 前端 - Model 更新

Files:

  • Modify: apps/lib/features/calendar/data/models/schedule_item_model.dart

  • Step 1: 新增 Subscriber 模型

class Subscriber {
  final String userId;
  final String username;
  final String? avatarUrl;
  final int permission;
  final String status;
  final DateTime subscribedAt;
  
  bool get canView => (permission & 1) != 0;
  bool get canEdit => (permission & 4) != 0;
  bool get canInvite => (permission & 2) != 0;
}
  • Step 2: 在 ScheduleItemModel 中添加 subscribers 字段
class ScheduleItemModel {
  // ... existing fields ...
  final List<Subscriber> subscribers;
}
  • Step 3: 更新 fromJson
factory ScheduleItemModel.fromJson(Map<String, dynamic> json) {
  return ScheduleItemModel(
    // ... existing fields ...,
    subscribers: (json['subscribers'] as List<dynamic>?)
        ?.map((s) => Subscriber.fromJson(s as Map<String, dynamic>))
        .toList() ?? [],
  );
}

Task 3: 前端 - 详情页渲染订阅者

Files:

  • Modify: apps/lib/features/calendar/presentation/screens/calendar_event_detail_screen.dart

  • Step 1: 在 _buildMetaSurface 或新方法中渲染订阅者

Widget _buildSubscribersSurface(ScheduleItemModel event) {
  if (event.subscribers.isEmpty) {
    return const SizedBox.shrink();
  }
  
  return Container(
    padding: EdgeInsets.all(AppSpacing.lg),
    decoration: BoxDecoration(
      color: _colorScheme.surface,
      borderRadius: BorderRadius.circular(AppRadius.xl),
      border: Border.all(color: _colorScheme.outlineVariant),
    ),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          context.l10n.calendarDetailSubscribers,
          style: TextStyle(
            fontSize: 13,
            fontWeight: FontWeight.w600,
            color: _colorScheme.onSurfaceVariant,
          ),
        ),
        const SizedBox(height: AppSpacing.md),
        ...event.subscribers.map((sub) => _buildSubscriberRow(sub)),
      ],
    ),
  );
}

Widget _buildSubscriberRow(Subscriber subscriber) {
  return Padding(
    padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
    child: Row(
      children: [
        Avatar(url: subscriber.avatarUrl, name: subscriber.username),
        const SizedBox(width: AppSpacing.sm),
        Expanded(child: Text(subscriber.username)),
        if (subscriber.canEdit) Icon(Icons.edit, size: 16),
        if (subscriber.canInvite) Icon(Icons.person_add, size: 16),
      ],
    ),
  );
}
  • Step 2: 在 build 方法中添加
// 在 _buildExtraSurface 之后添加
if (event.subscribers.isNotEmpty) [
  const SizedBox(height: AppSpacing.md),
  _buildSubscribersSurface(event),
],
  • Step 3: 添加 l10n 文本
// apps/lib/l10n/app_zh.arb
"calendarDetailSubscribers": "已订阅 ({count}人)"

Task 4: 验证

  • 运行后端测试: cd backend && uv run pytest -v -k "subscriber"
  • 运行前端分析: cd apps && flutter analyze
  • 手动测试分享流程