# 任务管理系统实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 实现完整的任务管理系统,包括 activity-admin 运营后台和 Go taskService 移动后端,共用数据库 **Architecture:** - activity-admin (Python FastAPI): 任务定义管理、进度查询、统计、手动重置 - Go taskService (Dubbo-go Triple): 移动端 API、事件处理、奖励发放、每日重置 - 共享 PostgreSQL 数据库,Go 服务通过 Advisory Lock 确保单实例重置 **Tech Stack:** - Python: FastAPI, SQLAlchemy, Pydantic - Go: Dubbo-go, GORM, PostgreSQL - Frontend: Vue 3, Element Plus, ECharts --- ## Phase 1: 数据库设计与初始化 ### Task 1.1: 创建数据库表 **Files:** - Create: `scripts/task_system_migration.sql` - [ ] **Step 1: 创建 SQL 迁移脚本** ```sql -- 任务定义表 CREATE TABLE IF NOT EXISTS task_definitions ( id BIGSERIAL PRIMARY KEY, star_id BIGINT, -- NULL=全局默认 task_key VARCHAR(50) NOT NULL, task_type VARCHAR(20) NOT NULL, -- 'daily' | 'onboarding' name VARCHAR(100) NOT NULL, description TEXT, crystal_reward BIGINT DEFAULT 0, exp_reward BIGINT DEFAULT 0, sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at BIGINT, updated_at BIGINT ); CREATE UNIQUE INDEX ix_task_def_star_key ON task_definitions(star_id, task_key); -- 每日任务进度表 CREATE TABLE IF NOT EXISTS user_daily_task_progress ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, star_id BIGINT NOT NULL, task_key VARCHAR(50) NOT NULL, status VARCHAR(20) DEFAULT 'pending', -- pending/completed/claimed completed_at BIGINT, claimed_at BIGINT, created_at BIGINT, updated_at BIGINT ); CREATE UNIQUE INDEX ix_daily_progress_user_star_key ON user_daily_task_progress(user_id, star_id, task_key); -- 引导任务进度表 CREATE TABLE IF NOT EXISTS user_onboarding_progress ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, task_key VARCHAR(50) NOT NULL, status VARCHAR(20) DEFAULT 'pending', -- pending/completed/claimed completed_at BIGINT, claimed_at BIGINT, created_at BIGINT, updated_at BIGINT ); CREATE UNIQUE INDEX ix_onboard_progress_user_key ON user_onboarding_progress(user_id, task_key); -- 引导流程状态表 CREATE TABLE IF NOT EXISTS user_onboarding_status ( user_id BIGINT PRIMARY KEY, current_stage INT DEFAULT 0, -- 0=未开始,1~N=阶段 status VARCHAR(20) DEFAULT 'pending', -- pending/in_progress/completed/claimed is_first_login_bonus_claimed BOOLEAN DEFAULT false, has_friend_display_bonus BOOLEAN DEFAULT false, completed_at BIGINT, claimed_at BIGINT, created_at BIGINT, updated_at BIGINT ); -- 引导阶段配置表 CREATE TABLE IF NOT EXISTS onboarding_stage_config ( id BIGSERIAL PRIMARY KEY, stage INT NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, required_task_keys TEXT[], -- PostgreSQL 数组类型 crystal_reward BIGINT DEFAULT 0, exp_reward BIGINT DEFAULT 0, sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at BIGINT, updated_at BIGINT ); -- 展示收益记录表 CREATE TABLE IF NOT EXISTS exhibition_revenue_records ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, star_id BIGINT NOT NULL, exhibition_id BIGINT NOT NULL, asset_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, slot_owner_uid BIGINT NOT NULL, slot_type VARCHAR(20) NOT NULL, -- 'own' | 'friend' crystal_amount BIGINT NOT NULL, cycle_start_time BIGINT NOT NULL, cycle_end_time BIGINT NOT NULL, status VARCHAR(20) DEFAULT 'claimable', -- claimable/claimed claimed_at BIGINT, created_at BIGINT ); CREATE INDEX ix_revenue_user_star_status ON exhibition_revenue_records(user_id, star_id, status); CREATE INDEX ix_revenue_star_status ON exhibition_revenue_records(star_id, status); -- 重置记录表(用于防止重复重置) CREATE TABLE IF NOT EXISTS task_reset_log ( id BIGSERIAL PRIMARY KEY, reset_type VARCHAR(20) NOT NULL, -- 'daily' | 'manual' star_id BIGINT, last_reset_at BIGINT NOT NULL, created_at BIGINT ); ``` - [ ] **Step 2: 执行迁移脚本** Run: `psql -h localhost -U postgres -d topfans -f scripts/task_system_migration.sql` --- ### Task 1.2: 插入默认任务数据 **Files:** - Create: `scripts/task_default_data.sql` - [ ] **Step 1: 创建默认任务数据脚本** ```sql -- 插入默认每日任务(star_id = NULL 表示全局默认) INSERT INTO task_definitions (star_id, task_key, task_type, name, description, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES (NULL, 'daily_login', 'daily', '每日首次登录', '每日首次登录 App', 20, 20, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000), (NULL, 'daily_browse_asset', 'daily', '每日首次浏览藏品', '每日首次浏览任意展品', 20, 40, 2, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000), (NULL, 'daily_mint', 'daily', '每日首次铸造', '每日首次铸造藏品', 20, 60, 3, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000), (NULL, 'daily_place_asset', 'daily', '每日首次上架藏品', '每日首次上架藏品到展厅', 20, 80, 4, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000); -- 插入默认引导任务 INSERT INTO task_definitions (star_id, task_key, task_type, name, description, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES (NULL, 'onboarding_complete', 'onboarding', '完成新手引导', '完成全部新手引导流程', 420, 250, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000); -- 插入引导阶段配置(动态配置) INSERT INTO onboarding_stage_config (stage, name, required_task_keys, crystal_reward, exp_reward, sort_order, is_active, created_at, updated_at) VALUES (1, '入门引导', ARRAY['square_home', 'profile_edit'], 0, 0, 1, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000), (2, '进阶引导', ARRAY['starbook_add', 'exhibition_add'], 0, 0, 2, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000), (3, '完成引导', ARRAY['exhibition_operate'], 0, 0, 3, true, EXTRACT(EPOCH FROM NOW())::bigint * 1000, EXTRACT(EPOCH FROM NOW())::bigint * 1000); ``` --- ## Phase 2: Python Backend (activity-admin) 任务管理模块 ### Task 2.1: 创建 SQLAlchemy 模型 **Files:** - Create: `backend/models/task_models.py` - [ ] **Step 1: 创建任务相关模型** ```python from sqlalchemy import Column, BigInteger, String, Text, Boolean, Integer, Index from database import Base class TaskDefinition(Base): """任务定义表""" __tablename__ = "task_definitions" id = Column(BigInteger, primary_key=True, autoincrement=True) star_id = Column(BigInteger, nullable=True) task_key = Column(String(50), nullable=False) task_type = Column(String(20), nullable=False) name = Column(String(100), nullable=False) description = Column(Text) crystal_reward = Column(BigInteger, default=0) exp_reward = Column(BigInteger, default=0) sort_order = Column(Integer, default=0) is_active = Column(Boolean, default=True) created_at = Column(BigInteger) updated_at = Column(BigInteger) __table_args__ = ( Index('ix_task_def_star_key', 'star_id', 'task_key'), ) class UserDailyTaskProgress(Base): """每日任务进度表""" __tablename__ = "user_daily_task_progress" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column(BigInteger, nullable=False) star_id = Column(BigInteger, nullable=False) task_key = Column(String(50), nullable=False) status = Column(String(20), default='pending') completed_at = Column(BigInteger) claimed_at = Column(BigInteger) created_at = Column(BigInteger) updated_at = Column(BigInteger) __table_args__ = ( Index('ix_daily_progress_user_star_key', 'user_id', 'star_id', 'task_key', unique=True), ) class UserOnboardingProgress(Base): """引导任务进度表""" __tablename__ = "user_onboarding_progress" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column(BigInteger, nullable=False) task_key = Column(String(50), nullable=False) status = Column(String(20), default='pending') completed_at = Column(BigInteger) claimed_at = Column(BigInteger) created_at = Column(BigInteger) updated_at = Column(BigInteger) __table_args__ = ( Index('ix_onboard_progress_user_key', 'user_id', 'task_key', unique=True), ) class UserOnboardingStatus(Base): """引导流程状态表""" __tablename__ = "user_onboarding_status" user_id = Column(BigInteger, primary_key=True) current_stage = Column(Integer, default=0) status = Column(String(20), default='pending') is_first_login_bonus_claimed = Column(Boolean, default=False) has_friend_display_bonus = Column(Boolean, default=False) completed_at = Column(BigInteger) claimed_at = Column(BigInteger) created_at = Column(BigInteger) updated_at = Column(BigInteger) class OnboardingStageConfig(Base): """引导阶段配置表""" __tablename__ = "onboarding_stage_config" id = Column(BigInteger, primary_key=True, autoincrement=True) stage = Column(Integer, nullable=False) name = Column(String(100), nullable=False) description = Column(Text) required_task_keys = Column(Text) # PostgreSQL ARRAY stored as TEXT crystal_reward = Column(BigInteger, default=0) exp_reward = Column(BigInteger, default=0) sort_order = Column(Integer, default=0) is_active = Column(Boolean, default=True) created_at = Column(BigInteger) updated_at = Column(BigInteger) class ExhibitionRevenueRecord(Base): """展示收益记录表""" __tablename__ = "exhibition_revenue_records" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column(BigInteger, nullable=False) star_id = Column(BigInteger, nullable=False) exhibition_id = Column(BigInteger, nullable=False) asset_id = Column(BigInteger, nullable=False) slot_id = Column(BigInteger, nullable=False) slot_owner_uid = Column(BigInteger, nullable=False) slot_type = Column(String(20), nullable=False) crystal_amount = Column(BigInteger, nullable=False) cycle_start_time = Column(BigInteger, nullable=False) cycle_end_time = Column(BigInteger, nullable=False) status = Column(String(20), default='claimable') claimed_at = Column(BigInteger) created_at = Column(BigInteger) __table_args__ = ( Index('ix_revenue_user_star_status', 'user_id', 'star_id', 'status'), Index('ix_revenue_star_status', 'star_id', 'status'), ) class TaskResetLog(Base): """重置记录表""" __tablename__ = "task_reset_log" id = Column(BigInteger, primary_key=True, autoincrement=True) reset_type = Column(String(20), nullable=False) star_id = Column(BigInteger) last_reset_at = Column(BigInteger, nullable=False) created_at = Column(BigInteger) ``` - [ ] **Step 2: 修改 backend/models/__init__.py** ```python from models.models import Activity, ActivityItem, ActivityContribution, ActivityUserStats, Star, FanProfile from models.task_models import ( TaskDefinition, UserDailyTaskProgress, UserOnboardingProgress, UserOnboardingStatus, OnboardingStageConfig, ExhibitionRevenueRecord, TaskResetLog ) ``` --- ### Task 2.2: 创建 Pydantic Schema **Files:** - Create: `backend/schemas/task_schemas.py` - [ ] **Step 1: 创建任务相关 Schema** ```python from pydantic import BaseModel from typing import Optional, List # ========== 任务定义 ========== class TaskDefinitionBase(BaseModel): star_id: Optional[int] = None task_key: str task_type: str name: str description: Optional[str] = None crystal_reward: int = 0 exp_reward: int = 0 sort_order: int = 0 is_active: bool = True class TaskDefinitionCreate(TaskDefinitionBase): pass class TaskDefinitionUpdate(BaseModel): star_id: Optional[int] = None task_key: Optional[str] = None task_type: Optional[str] = None name: Optional[str] = None description: Optional[str] = None crystal_reward: Optional[int] = None exp_reward: Optional[int] = None sort_order: Optional[int] = None is_active: Optional[bool] = None class TaskDefinitionResponse(TaskDefinitionBase): id: int created_at: Optional[int] = None updated_at: Optional[int] = None class Config: from_attributes = True class TaskDefinitionListResponse(BaseModel): items: List[TaskDefinitionResponse] total: int page: int page_size: int # ========== 每日任务进度 ========== class DailyTaskProgressItem(BaseModel): id: int user_id: int star_id: int task_key: str status: str completed_at: Optional[int] = None claimed_at: Optional[int] = None class Config: from_attributes = True class DailyTaskProgressListResponse(BaseModel): items: List[DailyTaskProgressItem] total: int page: int page_size: int # ========== 引导任务进度 ========== class OnboardingProgressItem(BaseModel): id: int user_id: int task_key: str status: str completed_at: Optional[int] = None claimed_at: Optional[int] = None class Config: from_attributes = True class OnboardingProgressListResponse(BaseModel): items: List[OnboardingProgressItem] total: int page: int page_size: int # ========== 引导流程状态 ========== class OnboardingStatusResponse(BaseModel): user_id: int current_stage: int status: str is_first_login_bonus_claimed: bool has_friend_display_bonus: bool completed_at: Optional[int] = None claimed_at: Optional[int] = None class Config: from_attributes = True # ========== 引导阶段配置 ========== class StageConfigResponse(BaseModel): id: int stage: int name: str description: Optional[str] = None required_task_keys: List[str] = [] crystal_reward: int exp_reward: int sort_order: int is_active: bool class Config: from_attributes = True # ========== 展示收益 ========== class RevenueRecordItem(BaseModel): id: int user_id: int star_id: int exhibition_id: int slot_type: str crystal_amount: int cycle_start_time: int cycle_end_time: int status: str claimed_at: Optional[int] = None class Config: from_attributes = True class RevenueListResponse(BaseModel): items: List[RevenueRecordItem] total: int page: int page_size: int # ========== 统计 ========== class TaskStatsByStar(BaseModel): star_id: int total_users: int total_completed: int total_claimed: int completion_rate: float claim_rate: float class DailyTaskStats(BaseModel): date: str new_participants: int total_completions: int total_claims: int class TaskOverviewStats(BaseModel): total_definitions: int active_definitions: int total_daily_progress: int total_onboarding_progress: int total_revenue_records: int claimable_revenue: int ``` --- ### Task 2.3: 创建 CRUD 函数 **Files:** - Create: `backend/crud/task_crud.py` - [ ] **Step 1: 创建任务 CRUD 函数** ```python from sqlalchemy.orm import Session from sqlalchemy import select, func, and_, or_, distinct, update from typing import List, Optional, Tuple from models.task_models import ( TaskDefinition, UserDailyTaskProgress, UserOnboardingProgress, UserOnboardingStatus, OnboardingStageConfig, ExhibitionRevenueRecord, TaskResetLog ) import json import time # ========== 任务定义 CRUD ========== def create_task_definition(db: Session, task: dict) -> TaskDefinition: now = int(time.time() * 1000) db_task = TaskDefinition( star_id=task.get('star_id'), task_key=task['task_key'], task_type=task['task_type'], name=task['name'], description=task.get('description'), crystal_reward=task.get('crystal_reward', 0), exp_reward=task.get('exp_reward', 0), sort_order=task.get('sort_order', 0), is_active=task.get('is_active', True), created_at=now, updated_at=now, ) db.add(db_task) db.commit() db.refresh(db_task) return db_task def get_task_definitions( db: Session, task_type: Optional[str] = None, star_id: Optional[int] = None, page: int = 1, page_size: int = 10 ) -> Tuple[List[TaskDefinition], int]: query = select(TaskDefinition) count_query = select(func.count(TaskDefinition.id)) if task_type: query = query.where(TaskDefinition.task_type == task_type) count_query = count_query.where(TaskDefinition.task_type == task_type) if star_id is not None: query = query.where(or_(TaskDefinition.star_id == star_id, TaskDefinition.star_id == None)) count_query = count_query.where(or_(TaskDefinition.star_id == star_id, TaskDefinition.star_id == None)) total = db.execute(count_query).scalar() query = query.order_by(TaskDefinition.sort_order, TaskDefinition.id) query = query.offset((page - 1) * page_size).limit(page_size) items = db.execute(query).scalars().all() return list(items), total def update_task_definition(db: Session, task_id: int, updates: dict) -> Optional[TaskDefinition]: task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none() if not task: return None for key, value in updates.items(): if value is not None and key != 'id': setattr(task, key, value) task.updated_at = int(time.time() * 1000) db.commit() db.refresh(task) return task def delete_task_definition(db: Session, task_id: int) -> bool: task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none() if not task: return False db.delete(task) db.commit() return True def toggle_task_definition(db: Session, task_id: int) -> Optional[TaskDefinition]: task = db.execute(select(TaskDefinition).where(TaskDefinition.id == task_id)).scalar_one_or_none() if not task: return None task.is_active = not task.is_active task.updated_at = int(time.time() * 1000) db.commit() db.refresh(task) return task # ========== 每日任务进度 ========== def get_daily_progress( db: Session, user_id: Optional[int] = None, star_id: Optional[int] = None, status: Optional[str] = None, page: int = 1, page_size: int = 20 ) -> Tuple[List[dict], int]: query = select(UserDailyTaskProgress) count_query = select(func.count(UserDailyTaskProgress.id)) if user_id: query = query.where(UserDailyTaskProgress.user_id == user_id) count_query = count_query.where(UserDailyTaskProgress.user_id == user_id) if star_id: query = query.where(UserDailyTaskProgress.star_id == star_id) count_query = count_query.where(UserDailyTaskProgress.star_id == star_id) if status: query = query.where(UserDailyTaskProgress.status == status) count_query = count_query.where(UserDailyTaskProgress.status == status) total = db.execute(count_query).scalar() query = query.order_by(UserDailyTaskProgress.updated_at.desc()) query = query.offset((page - 1) * page_size).limit(page_size) items = db.execute(query).scalars().all() return [dict( id=i.id, user_id=i.user_id, star_id=i.star_id, task_key=i.task_key, status=i.status, completed_at=i.completed_at, claimed_at=i.claimed_at ) for i in items], total def reset_daily_tasks(db: Session, star_id: Optional[int] = None) -> int: now = int(time.time() * 1000) query = ( update(UserDailyTaskProgress) .where(UserDailyTaskProgress.status != 'pending') .values(status='pending', completed_at=None, claimed_at=None, updated_at=now) ) if star_id: query = query.where(UserDailyTaskProgress.star_id == star_id) result = db.execute(query) db.commit() return result.rowcount # ========== 引导任务进度 ========== def get_onboarding_progress( db: Session, user_id: Optional[int] = None, status: Optional[str] = None, page: int = 1, page_size: int = 20 ) -> Tuple[List[dict], int]: query = select(UserOnboardingProgress) count_query = select(func.count(UserOnboardingProgress.id)) if user_id: query = query.where(UserOnboardingProgress.user_id == user_id) count_query = count_query.where(UserOnboardingProgress.user_id == user_id) if status: query = query.where(UserOnboardingProgress.status == status) count_query = count_query.where(UserOnboardingProgress.status == status) total = db.execute(count_query).scalar() query = query.order_by(UserOnboardingProgress.updated_at.desc()) query = query.offset((page - 1) * page_size).limit(page_size) items = db.execute(query).scalars().all() return [dict( id=i.id, user_id=i.user_id, task_key=i.task_key, status=i.status, completed_at=i.completed_at, claimed_at=i.claimed_at ) for i in items], total def get_onboarding_status(db: Session, user_id: int) -> Optional[UserOnboardingStatus]: return db.execute( select(UserOnboardingStatus).where(UserOnboardingStatus.user_id == user_id) ).scalar_one_or_none() # ========== 引导阶段配置 ========== def get_stage_configs(db: Session) -> List[OnboardingStageConfig]: result = db.execute( select(OnboardingStageConfig) .where(OnboardingStageConfig.is_active == True) .order_by(OnboardingStageConfig.sort_order) ) return list(result.scalars().all()) # ========== 展示收益 ========== def get_revenue_records( db: Session, user_id: Optional[int] = None, star_id: Optional[int] = None, status: Optional[str] = None, page: int = 1, page_size: int = 20 ) -> Tuple[List[ExhibitionRevenueRecord], int]: query = select(ExhibitionRevenueRecord) count_query = select(func.count(ExhibitionRevenueRecord.id)) if user_id: query = query.where(ExhibitionRevenueRecord.user_id == user_id) count_query = count_query.where(ExhibitionRevenueRecord.user_id == user_id) if star_id: query = query.where(ExhibitionRevenueRecord.star_id == star_id) count_query = count_query.where(ExhibitionRevenueRecord.star_id == star_id) if status: query = query.where(ExhibitionRevenueRecord.status == status) count_query = count_query.where(ExhibitionRevenueRecord.status == status) total = db.execute(count_query).scalar() query = query.order_by(ExhibitionRevenueRecord.created_at.desc()) query = query.offset((page - 1) * page_size).limit(page_size) items = db.execute(query).scalars().all() return list(items), total # ========== 统计 ========== def get_task_overview_stats(db: Session) -> dict: total_defs = db.execute(select(func.count(TaskDefinition.id))).scalar() or 0 active_defs = db.execute(select(func.count(TaskDefinition.id)).where(TaskDefinition.is_active == True)).scalar() or 0 total_daily = db.execute(select(func.count(UserDailyTaskProgress.id))).scalar() or 0 total_onboard = db.execute(select(func.count(UserOnboardingProgress.id))).scalar() or 0 total_revenue = db.execute(select(func.count(ExhibitionRevenueRecord.id))).scalar() or 0 claimable = db.execute( select(func.count(ExhibitionRevenueRecord.id)) .where(ExhibitionRevenueRecord.status == 'claimable') ).scalar() or 0 return { "total_definitions": total_defs, "active_definitions": active_defs, "total_daily_progress": total_daily, "total_onboarding_progress": total_onboard, "total_revenue_records": total_revenue, "claimable_revenue": claimable, } def get_task_stats_by_star(db: Session, star_id: int) -> dict: completed = db.execute( select(func.count(UserDailyTaskProgress.id)) .where(and_( UserDailyTaskProgress.star_id == star_id, UserDailyTaskProgress.status.in_(['completed', 'claimed']) )) ).scalar() or 0 claimed = db.execute( select(func.count(UserDailyTaskProgress.id)) .where(and_( UserDailyTaskProgress.star_id == star_id, UserDailyTaskProgress.status == 'claimed' )) ).scalar() or 0 total_users = db.execute( select(func.count(distinct(UserDailyTaskProgress.user_id))) .where(UserDailyTaskProgress.star_id == star_id) ).scalar() or 0 total_progress = db.execute( select(func.count(UserDailyTaskProgress.id)) .where(UserDailyTaskProgress.star_id == star_id) ).scalar() or 0 return { "star_id": star_id, "total_users": total_users, "total_completed": completed, "total_claimed": claimed, "completion_rate": round(completed / total_progress * 100, 2) if total_progress > 0 else 0, "claim_rate": round(claimed / total_progress * 100, 2) if total_progress > 0 else 0, } def get_daily_task_trend(db: Session, star_id: Optional[int] = None, days: int = 7) -> List[dict]: # 简化实现,返回近7天每日统计 trends = [] for i in range(days - 1, -1, -1): day_ts = int(time.time() * 1000) - i * 86400000 date = time.strftime("%Y-%m-%d", time.localtime(day_ts / 1000)) trends.append({ "date": date, "new_participants": 0, "total_completions": 0, "total_claims": 0, }) return trends ``` --- ### Task 2.4: 创建 API Handler **Files:** - Create: `backend/handlers/task_handler.py` - [ ] **Step 1: 创建任务 Handler** ```python from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from database import get_db from schemas.task_schemas import ( TaskDefinitionCreate, TaskDefinitionUpdate, TaskDefinitionResponse, TaskDefinitionListResponse, DailyTaskProgressListResponse, OnboardingProgressListResponse, OnboardingStatusResponse, RevenueListResponse, TaskStatsByStar, TaskOverviewStats ) from crud.task_crud import ( create_task_definition, get_task_definitions, update_task_definition, delete_task_definition, toggle_task_definition, get_daily_progress, get_onboarding_progress, get_onboarding_status, reset_daily_tasks, get_task_stats_by_star, get_revenue_records, get_task_overview_stats, get_daily_task_trend, get_stage_configs ) from middleware.auth import verify_token from typing import Optional router = APIRouter(prefix="/api/admin/tasks", tags=["任务管理"]) # ========== 任务定义 CRUD ========== @router.post("/definitions", response_model=TaskDefinitionResponse) async def create_task( task: TaskDefinitionCreate, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): db_task = create_task_definition(db, task.model_dump()) return db_task @router.get("/definitions", response_model=TaskDefinitionListResponse) async def list_tasks( task_type: Optional[str] = Query(None), star_id: Optional[int] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(10, ge=1, le=100), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): items, total = get_task_definitions(db, task_type, star_id, page, page_size) return TaskDefinitionListResponse( items=[TaskDefinitionResponse.model_validate(t) for t in items], total=total, page=page, page_size=page_size ) @router.put("/definitions/{task_id}", response_model=TaskDefinitionResponse) async def update_task( task_id: int, updates: TaskDefinitionUpdate, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): task = update_task_definition(db, task_id, updates.model_dump(exclude_unset=True)) if not task: raise HTTPException(status_code=404, detail="Task not found") return task @router.delete("/definitions/{task_id}") async def delete_task( task_id: int, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): success = delete_task_definition(db, task_id) if not success: raise HTTPException(status_code=404, detail="Task not found") return {"message": "Task deleted successfully"} @router.post("/definitions/{task_id}/toggle", response_model=TaskDefinitionResponse) async def toggle_task( task_id: int, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): task = toggle_task_definition(db, task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") return task # ========== 任务进度 ========== @router.get("/daily/progress", response_model=DailyTaskProgressListResponse) async def list_daily_progress( user_id: Optional[int] = Query(None), star_id: Optional[int] = Query(None), status: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): items, total = get_daily_progress(db, user_id, star_id, status, page, page_size) return DailyTaskProgressListResponse(items=items, total=total, page=page, page_size=page_size) @router.get("/onboarding/progress", response_model=OnboardingProgressListResponse) async def list_onboarding_progress( user_id: Optional[int] = Query(None), status: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): items, total = get_onboarding_progress(db, user_id, status, page, page_size) return OnboardingProgressListResponse(items=items, total=total, page=page, page_size=page_size) @router.get("/onboarding/status/{user_id}", response_model=OnboardingStatusResponse) async def get_onboarding_status( user_id: int, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): status = get_onboarding_status(db, user_id) if not status: raise HTTPException(status_code=404, detail="Onboarding status not found") return status @router.get("/onboarding/stages") async def list_stages( db: Session = Depends(get_db), _: dict = Depends(verify_token) ): stages = get_stage_configs(db) return {"stages": stages} # ========== 手动操作 ========== @router.post("/reset/daily") async def reset_daily( star_id: Optional[int] = Query(None), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): count = reset_daily_tasks(db, star_id) return {"message": f"Reset {count} daily task records", "affected": count} # ========== 统计 ========== @router.get("/stats/overview", response_model=TaskOverviewStats) async def get_overview( db: Session = Depends(get_db), _: dict = Depends(verify_token) ): return get_task_overview_stats(db) @router.get("/stats/star/{star_id}", response_model=TaskStatsByStar) async def get_star_stats( star_id: int, db: Session = Depends(get_db), _: dict = Depends(verify_token) ): return get_task_stats_by_star(db, star_id) @router.get("/stats/daily-trend") async def get_daily_trend( star_id: Optional[int] = Query(None), days: int = Query(7, ge=1, le=30), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): trends = get_daily_task_trend(db, star_id, days) return {"trends": trends} # ========== 展示收益 ========== @router.get("/revenue", response_model=RevenueListResponse) async def list_revenue( user_id: Optional[int] = Query(None), star_id: Optional[int] = Query(None), status: Optional[str] = Query(None), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), _: dict = Depends(verify_token) ): items, total = get_revenue_records(db, user_id, star_id, status, page, page_size) return RevenueListResponse(items=items, total=total, page=page, page_size=page_size) ``` - [ ] **Step 2: 修改 backend/router/__init__.py** ```python from fastapi import APIRouter from handlers import auth, activity, star, asset, task_handler api_router = APIRouter() api_router.include_router(auth.router) api_router.include_router(activity.router) api_router.include_router(star.router) api_router.include_router(asset.router) api_router.include_router(task_handler.router) # 新增 ``` --- ## Phase 3: Vue Frontend (activity-admin) 任务管理页面 ### Task 3.1: 任务定义列表页 **Files:** - Create: `frontend/src/views/TaskDefinitionList.vue` - [ ] **Step 1: 创建任务定义列表页面** ```vue 任务定义管理 新建任务 搜索 重置 {{ row.task_type === 'daily' ? '每日' : '引导' }} 💎 {{ row.crystal_reward }} | ✨ {{ row.exp_reward }} {{ row.is_active ? '启用' : '禁用' }} 编辑 删除 取消 保存 ``` --- ### Task 3.2: 任务进度查询页 **Files:** - Create: `frontend/src/views/TaskProgress.vue` - [ ] **Step 1: 创建任务进度查询页面** ```vue 任务进度查询 搜索 {{ row.status }} {{ formatTime(row.completed_at) }} {{ formatTime(row.claimed_at) }} 搜索 {{ row.status }} ``` --- ### Task 3.3: 任务统计页 **Files:** - Create: `frontend/src/views/TaskStats.vue` - [ ] **Step 1: 创建任务统计页面** ```vue 任务系统统计 {{ stats.total_definitions }} 任务定义总数 {{ stats.active_definitions }} 启用任务数 {{ stats.total_daily_progress }} 每日任务进度记录 {{ stats.claimable_revenue }} 待领取收益 ``` --- ### Task 3.4: 手动重置页 **Files:** - Create: `frontend/src/views/TaskReset.vue` - [ ] **Step 1: 创建手动重置页面** ```vue 手动重置每日任务 执行重置 ``` --- ### Task 3.5: 展示收益页 **Files:** - Create: `frontend/src/views/RevenueList.vue` - [ ] **Step 1: 创建展示收益页面** ```vue 展示收益记录 搜索 {{ row.slot_type === 'own' ? '自有' : '好友' }} 💎 {{ row.crystal_amount }} {{ formatTime(row.cycle_start_time) }} {{ formatTime(row.cycle_end_time) }} {{ row.status === 'claimable' ? '可领取' : '已领取' }} ``` --- ### Task 3.6: 更新 API 和路由 **Files:** - Modify: `frontend/src/api/admin.js` - [ ] **Step 1: 添加任务相关 API** ```javascript // 任务定义 export function getTaskDefinitions(params) { return request.get('/tasks/definitions', { params }) } export function createTask(data) { return request.post('/tasks/definitions', data) } export function updateTask(id, data) { return request.put(`/tasks/definitions/${id}`, data) } export function deleteTask(id) { return request.delete(`/tasks/definitions/${id}`) } // 任务进度 export function getDailyProgress(params) { return request.get('/tasks/daily/progress', { params }) } export function getOnboardingProgress(params) { return request.get('/tasks/onboarding/progress', { params }) } export function getOnboardingStatus(userId) { return request.get(`/tasks/onboarding/status/${userId}`) } export function getOnboardingStages() { return request.get('/tasks/onboarding/stages') } // 手动重置 export function resetDailyTasks(starId) { return request.post('/tasks/reset/daily', null, { params: { star_id: starId } }) } // 统计 export function getOverviewStats() { return request.get('/tasks/stats/overview') } export function getDailyTrend(params) { return request.get('/tasks/stats/daily-trend', { params }) } export function getStarStats(starId) { return request.get(`/tasks/stats/star/${starId}`) } // 展示收益 export function getRevenueRecords(params) { return request.get('/tasks/revenue', { params }) } ``` - [ ] **Step 2: 修改 frontend/src/router/index.js** ```javascript { path: '/tasks', component: Layout, children: [ { path: '', redirect: '/tasks/definitions' }, { path: 'definitions', component: () => import('@/views/TaskDefinitionList.vue') }, { path: 'progress', component: () => import('@/views/TaskProgress.vue') }, { path: 'stats', component: () => import('@/views/TaskStats.vue') }, { path: 'reset', component: () => import('@/views/TaskReset.vue') }, { path: 'revenue', component: () => import('@/views/RevenueList.vue') }, ] } ``` --- ## Phase 4: Go taskService 实现 ### Task 4.1: 创建 Proto 定义 **Files:** - Create: `backend/proto/task.proto` - [ ] **Step 1: 创建 task.proto** ```protobuf syntax = "proto3"; package topfans.task; option go_package = "github.com/topfans/backend/pkg/proto/task;task"; import "proto/common.proto"; // ========== 内部 RPC ========== // 展品到期完成事件参数 message OnExhibitionCompletedParams { int64 exhibition_id = 1; int64 asset_id = 2; int64 slot_id = 3; int64 occupier_uid = 4; // 展出者 UID int64 occupier_star_id = 5; // 展出者明星 ID int64 slot_owner_uid = 6; // 槽位拥有者 UID(应收水晶的人) int64 start_time = 7; // 开始时间(毫秒) int64 expire_at = 8; // 过期时间(毫秒) } // 任务事件上报参数 message ReportTaskEventParams { int64 user_id = 1; int64 star_id = 2; string event_type = 3; // daily_login, daily_browse_asset, daily_mint, daily_place_asset int64 timestamp = 4; } // 初始化用户任务数据 message InitUserTasksRequest { int64 user_id = 1; } message InitUserTasksResponse { topfans.common.BaseResponse base = 1; bool success = 2; } // ========== 内部 RPC Service ========== service TaskInternalService { // 初始化用户任务数据(注册时调用) rpc InitUserTasks(InitUserTasksRequest) returns (InitUserTasksResponse); // 接收任务事件上报(其他服务调用) rpc ReportTaskEvent(ReportTaskEventRequest) returns (ReportTaskEventResponse); // 展品到期完成(galleryService 调用) rpc OnExhibitionCompleted(OnExhibitionCompletedRequest) returns (OnExhibitionCompletedResponse); } message ReportTaskEventRequest { int64 user_id = 1; int64 star_id = 2; string event_type = 3; int64 timestamp = 4; } message ReportTaskEventResponse { topfans.common.BaseResponse base = 1; bool task_completed = 2; // 任务是否完成 string task_key = 3; } message OnExhibitionCompletedRequest { OnExhibitionCompletedParams params = 1; } message OnExhibitionCompletedResponse { topfans.common.BaseResponse base = 1; int64 revenue_record_id = 2; // 创建的收益记录 ID } ``` - [ ] **Step 2: 重新生成 proto 代码** Run: `cd backend && protoc --go_out=. --go_opt=paths=source_relative proto/task.proto` --- ### Task 4.2: 创建 Go taskService 结构 **Files:** - Create: `backend/services/taskService/main.go` - Create: `backend/services/taskService/config/task_config.go` - Create: `backend/services/taskService/repository/task_repository.go` - Create: `backend/services/taskService/service/daily_task_service.go` - Create: `backend/services/taskService/service/exhibition_revenue_service.go` - Create: `backend/services/taskService/service/onboarding_service.go` - Create: `backend/services/taskService/provider/task_provider.go` - Create: `backend/services/taskService/provider/task_internal_provider.go` - Create: `backend/services/taskService/worker/daily_reset_worker.go` - Create: `backend/services/taskService/client/user_rpc_client.go` - [ ] **Step 1: 创建 main.go** ```go package main import ( "flag" "fmt" "os" "os/signal" "strconv" "syscall" dubboclient "dubbo.apache.org/dubbo-go/v3/client" _ "dubbo.apache.org/dubbo-go/v3/imports" "dubbo.apache.org/dubbo-go/v3/protocol" "dubbo.apache.org/dubbo-go/v3/server" "github.com/topfans/backend/pkg/database" "github.com/topfans/backend/pkg/logger" pbTask "github.com/topfans/backend/pkg/proto/task" pbUser "github.com/topfans/backend/pkg/proto/user" "github.com/topfans/backend/services/taskService/client" "github.com/topfans/backend/services/taskService/config" "github.com/topfans/backend/services/taskService/provider" "github.com/topfans/backend/services/taskService/repository" "github.com/topfans/backend/services/taskService/service" "github.com/topfans/backend/services/taskService/worker" ) var ( port = flag.Int("port", getEnvInt("PORT", 20005), "Dubbo service port") dbHost = flag.String("db-host", getEnv("DB_HOST", "localhost"), "Database host") dbPort = flag.Int("db-port", getEnvInt("DB_PORT", 5432), "Database port") dbUser = flag.String("db-user", getEnv("DB_USER", "postgres"), "Database user") dbPassword = flag.String("db-password", getEnv("DB_PASSWORD", ""), "Database password") dbName = flag.String("db-name", getEnv("DB_NAME", "top-fans"), "Database name") userServiceURL = flag.String("user-service-url", getEnv("USER_SERVICE_URL", "tri://localhost:20000"), "User service URL") ) func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getEnvInt(key string, fallback int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback } func main() { flag.Parse() env := os.Getenv("ENV") if env == "" { env = "development" } if err := logger.Init(logger.Config{ ServiceName: "task-service", Environment: env, LogLevel: os.Getenv("LOG_LEVEL"), }); err != nil { panic(fmt.Sprintf("Failed to initialize logger: %v", err)) } defer logger.Sync() logger.Logger.Info("Starting Task Service...") // 初始化数据库 dbConfig := database.Config{ Host: *dbHost, Port: *dbPort, User: *dbUser, Password: *dbPassword, DBName: *dbName, SSLMode: "disable", TimeZone: "Asia/Shanghai", } if err := database.Init(dbConfig); err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to initialize database: %v", err)) } logger.Logger.Info("Database initialized successfully") // 创建 Repository db := database.GetDB() taskRepo := repository.NewTaskRepository(db) logger.Logger.Info("Repository layer initialized") // 创建 User Service RPC 客户端 userCli, err := dubboclient.NewClient( dubboclient.WithClientURL(*userServiceURL), ) if err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to create User Service Dubbo client: %v", err)) } userServiceClient, err := pbUser.NewUserSocialService(userCli) if err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to create User Service RPC client: %v", err)) } userRPCClient := client.NewUserRPCClient(userServiceClient) logger.Logger.Info("User Service RPC client initialized") // 创建 Service 层 dailyTaskService := service.NewDailyTaskService(taskRepo, userRPCClient) exhibitionRevenueService := service.NewExhibitionRevenueService(taskRepo, userRPCClient) onboardingService := service.NewOnboardingService(taskRepo, userRPCClient) logger.Logger.Info("Service layer initialized") // 创建 Provider taskProvider := provider.NewTaskProvider(dailyTaskService, exhibitionRevenueService, onboardingService) taskInternalProvider := provider.NewTaskInternalProvider(dailyTaskService, exhibitionRevenueService, onboardingService) logger.Logger.Info("Provider layer initialized") // 创建并启动 DailyResetWorker resetWorker := worker.NewDailyResetWorker(taskRepo, config.GetDailyResetTime()) go resetWorker.Start() logger.Logger.Info("Daily reset worker started") // 创建 Dubbo 服务器 srv, err := server.NewServer( server.WithServerProtocol( protocol.WithPort(*port), protocol.WithTriple(), ), ) if err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to create Dubbo server: %v", err)) } // 注册服务 if err := pbTask.RegisterTaskInternalServiceHandler(srv, taskInternalProvider); err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to register TaskInternalService: %v", err)) } // 启动服务 if err := srv.Serve(); err != nil { logger.Logger.Fatal(fmt.Sprintf("Failed to start Task Service: %v", err)) } logger.Logger.Info(fmt.Sprintf("Task Service started successfully on port %d", *port)) // 等待退出信号 quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Logger.Info("Shutting down Task Service...") resetWorker.Stop() logger.Logger.Info("Task Service shutdown complete") } ``` --- ## Phase 5: userService AddExperience RPC ### Task 5.1: 修改 proto/user.proto **Files:** - Modify: `backend/proto/user.proto` - [ ] **Step 1: 添加 AddExperience 消息和 RPC** ```protobuf // 更新经验值请求(内部RPC调用,用于taskService发放经验奖励) message AddExperienceRequest { int64 user_id = 1; int64 star_id = 2; int64 delta = 3; } // 更新经验值响应 message AddExperienceResponse { topfans.common.BaseResponse base = 1; int64 new_experience = 2; } // 在 service UserSocialService {} 中添加: // 内部RPC:更新经验值(仅供taskService调用) rpc AddExperience(AddExperienceRequest) returns (AddExperienceResponse); ``` - [ ] **Step 2: 重新生成 proto 代码** Run: `cd backend && protoc --go_out=. --go_opt=paths=source_relative proto/user.proto` --- ### Task 5.2: 修改 repository **Files:** - Modify: `backend/services/userService/repository/fan_profile_repository.go` - [ ] **Step 1: 添加 UpdateExperience 方法** ```go // FanProfileRepository 接口添加: UpdateExperience(userID, starID int64, delta int64) (int64, error) // 实现: func (r *fanProfileRepository) UpdateExperience(userID, starID int64, delta int64) (int64, error) { if userID <= 0 { return 0, errors.New("user_id must be greater than 0") } var profile models.FanProfile if err := r.db.Where("user_id = ? AND star_id = ?", userID, starID).First(&profile).Error; err != nil { return 0, err } profile.Experience += delta if err := r.db.Save(&profile).Error; err != nil { return 0, err } return profile.Experience, nil } ``` --- ### Task 5.3: 修改 service **Files:** - Modify: `backend/services/userService/service/user_service.go` - [ ] **Step 1: 添加 AddExperience 方法** ```go // UserService 接口添加: AddExperience(req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) // 实现: func (s *userService) AddExperience(req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) { if !validator.ValidateUserID(req.UserId) || !validator.ValidateStarID(req.StarId) { return &pb.AddExperienceResponse{ Base: appErrors.BuildBaseResponse(appErrors.ErrInvalidUserID), }, nil } newExp, err := s.fanProfileRepo.UpdateExperience(req.UserId, req.StarId, req.Delta) if err != nil { logger.Logger.Error("Failed to update experience", zap.Error(err)) return &pb.AddExperienceResponse{ Base: appErrors.BuildBaseResponse(appErrors.ErrInternalServer), }, nil } return &pb.AddExperienceResponse{ Base: &pbCommon.BaseResponse{ Code: pbCommon.StatusCode_STATUS_OK, Message: "", }, NewExperience: newExp, }, nil } ``` --- ### Task 5.4: 修改 provider **Files:** - Modify: `backend/services/userService/provider/user_provider.go` - [ ] **Step 1: 添加 AddExperience Provider 方法** ```go // AddExperience 更新经验值(内部RPC调用) func (p *UserProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) { logger.Logger.Info("Received AddExperience request", zap.Int64("user_id", req.UserId), zap.Int64("star_id", req.StarId), zap.Int64("delta", req.Delta), ) resp, err := p.userService.AddExperience(req) if err != nil { logger.Logger.Error("AddExperience failed", zap.Error(err)) } return resp, err } ``` - [ ] **Step 2: 修改 unified_provider.go** ```go // 在 UnifiedProvider 中添加: func (p *UnifiedProvider) AddExperience(ctx context.Context, req *pb.AddExperienceRequest) (*pb.AddExperienceResponse, error) { return p.userProvider.AddExperience(ctx, req) } ``` --- ## Phase 6: 前端 API 适配(移动端) ### Task 6.1: 更新前端 API 调用 **Files:** - Create/Modify: `frontend/src/api/task.js`(移动端任务 API) - [ ] **Step 1: 创建移动端任务 API** ```javascript // 每日任务 export function getDailyTasks(starId) { return request.get('/tasks/daily', { params: { star_id: starId } }) } export function claimTaskReward(taskKey, starId) { return request.post('/tasks/claim', { task_key: taskKey, star_id: starId }) } // 引导任务 export function getOnboardingStages() { return request.get('/tasks/onboarding/stages') } export function getOnboardingStatus() { return request.get('/tasks/onboarding/status') } export function completeOnboarding(taskKey) { return request.post('/tasks/onboarding/complete', { task_key: taskKey }) } export function claimOnboardingReward(taskKey) { return request.post('/tasks/onboarding/claim', { task_key: taskKey }) } // 展示收益 export function getExhibitionRevenue(starId, status, page, pageSize) { return request.get('/tasks/exhibition-revenue', { params: { star_id: starId, status, page, page_size: pageSize } }) } export function claimExhibitionRevenue(revenueId) { return request.post('/tasks/exhibition-revenue/claim', { revenue_id: revenueId }) } export function claimAllExhibitionRevenue() { return request.post('/tasks/exhibition-revenue/claim-all') } // 等级 export function getLevelInfo() { return request.get('/tasks/level') } // 初始化 export function initUserTasks() { return request.post('/tasks/init') } ``` --- ## 实现检查点 完成所有任务后,请验证以下功能: 1. **数据库**:所有表已创建,默认数据已插入 2. **activity-admin**: - [ ] 任务定义列表页面可访问 - [ ] 新建/编辑/删除任务定义正常 - [ ] 任务进度查询正常 - [ ] 任务统计页面显示图表 - [ ] 手动重置功能正常 - [ ] 展示收益记录查询正常 3. **Go taskService**: - [ ] 服务启动正常 - [ ] 每日重置 Worker 运行正常(5:00 Asia/Shanghai) - [ ] 内部 RPC 可被 galleryService 调用 4. **userService**: - [ ] AddExperience RPC 可正常调用 5. **移动端**: - [ ] 每日任务列表显示正常 - [ ] 任务完成上报正常 - [ ] 奖励领取正常 - [ ] 展示收益显示和领取正常