topfans/docs/superpowers/plans/2026-04-08-task-management-system-implementation-plan.md
2026-04-10 16:17:45 +08:00

73 KiB
Raw Blame History

任务管理系统实现计划

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 迁移脚本

-- 任务定义表
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: 创建默认任务数据脚本

-- 插入默认每日任务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: 创建任务相关模型

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

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 函数

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

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
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: 创建任务定义列表页面

<template>
  <div class="task-definition-list">
    <div class="header">
      <h2>任务定义管理</h2>
      <el-button type="primary" @click="handleCreate">新建任务</el-button>
    </div>

    <el-card class="filter-card">
      <el-form :inline="true">
        <el-form-item label="任务类型">
          <el-select v-model="filters.task_type" clearable>
            <el-option label="每日任务" value="daily" />
            <el-option label="引导任务" value="onboarding" />
          </el-select>
        </el-form-item>
        <el-form-item label="明星">
          <el-select v-model="filters.star_id" placeholder="全部(全局)" clearable>
            <el-option label="全局" :value="null" />
            <el-option v-for="star in stars" :key="star.star_id" 
              :label="star.name" :value="star.star_id" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="fetchTasks">搜索</el-button>
          <el-button @click="resetFilters">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <el-card>
      <el-table :data="tasks" v-loading="loading" row-key="id">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="task_type" label="类型" width="100">
          <template #default="{ row }">
            <el-tag :type="row.task_type === 'daily' ? 'success' : 'warning'">
              {{ row.task_type === 'daily' ? '每日' : '引导' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="task_key" label="Task Key" width="180" />
        <el-table-column prop="name" label="任务名称" min-width="150" />
        <el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
        <el-table-column label="奖励" width="120">
          <template #default="{ row }">
            💎 {{ row.crystal_reward }} | ✨ {{ row.exp_reward }}
          </template>
        </el-table-column>
        <el-table-column prop="sort_order" label="排序" width="80" />
        <el-table-column prop="is_active" label="状态" width="80">
          <template #default="{ row }">
            <el-tag :type="row.is_active ? 'success' : 'info'">
              {{ row.is_active ? '启用' : '禁用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="{ row }">
            <el-button size="small" @click="handleEdit(row)">编辑</el-button>
            <el-button size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>

      <el-pagination
        v-model:current-page="pagination.page"
        v-model:page-size="pagination.page_size"
        :total="pagination.total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next"
        @size-change="fetchTasks" @current-change="fetchTasks"
        style="margin-top: 20px; justify-content: flex-end"
      />
    </el-card>

    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
      <el-form :model="form" label-width="100px">
        <el-form-item label="任务类型" required>
          <el-select v-model="form.task_type">
            <el-option label="每日任务" value="daily" />
            <el-option label="引导任务" value="onboarding" />
          </el-select>
        </el-form-item>
        <el-form-item label="Task Key" required>
          <el-input v-model="form.task_key" placeholder="如 daily_login" />
        </el-form-item>
        <el-form-item label="任务名称" required>
          <el-input v-model="form.name" placeholder="如 每日首次登录" />
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="form.description" type="textarea" />
        </el-form-item>
        <el-form-item label="水晶奖励">
          <el-input-number v-model="form.crystal_reward" :min="0" />
        </el-form-item>
        <el-form-item label="经验奖励">
          <el-input-number v-model="form.exp_reward" :min="0" />
        </el-form-item>
        <el-form-item label="排序">
          <el-input-number v-model="form.sort_order" :min="0" />
        </el-form-item>
        <el-form-item label="启用">
          <el-switch v-model="form.is_active" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="handleSave">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getTaskDefinitions, createTask, updateTask, deleteTask, getStars } from '@/api/admin'
import { ElMessage, ElMessageBox } from 'element-plus'

const tasks = ref([])
const stars = ref([])
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新建任务')
const isEdit = ref(false)

const filters = reactive({ task_type: '', star_id: null })
const pagination = reactive({ page: 1, page_size: 10, total: 0 })

const form = reactive({
  task_type: 'daily',
  task_key: '',
  name: '',
  description: '',
  crystal_reward: 0,
  exp_reward: 0,
  sort_order: 0,
  is_active: true,
  star_id: null,
})

const fetchTasks = async () => {
  loading.value = true
  try {
    const res = await getTaskDefinitions({
      task_type: filters.task_type || undefined,
      star_id: filters.star_id,
      page: pagination.page,
      page_size: pagination.page_size,
    })
    tasks.value = res.items
    pagination.total = res.total
  } finally {
    loading.value = false
  }
}

const fetchStars = async () => {
  try {
    const res = await getStars()
    stars.value = res
  } catch (e) { console.error(e) }
}

const resetFilters = () => {
  filters.task_type = ''
  filters.star_id = null
  pagination.page = 1
  fetchTasks()
}

const handleCreate = () => {
  isEdit.value = false
  dialogTitle.value = '新建任务'
  Object.assign(form, {
    task_type: 'daily', task_key: '', name: '', description: '',
    crystal_reward: 0, exp_reward: 0, sort_order: 0, is_active: true, star_id: null,
  })
  dialogVisible.value = true
}

const handleEdit = (row) => {
  isEdit.value = true
  dialogTitle.value = '编辑任务'
  Object.assign(form, row)
  dialogVisible.value = true
}

const handleSave = async () => {
  try {
    if (isEdit.value) {
      await updateTask(form.id, form)
      ElMessage.success('更新成功')
    } else {
      await createTask(form)
      ElMessage.success('创建成功')
    }
    dialogVisible.value = false
    fetchTasks()
  } catch (e) {
    ElMessage.error('保存失败')
  }
}

const handleDelete = async (id) => {
  await ElMessageBox.confirm('确定要删除此任务定义吗?', '警告', { type: 'error' })
  await deleteTask(id)
  ElMessage.success('删除成功')
  fetchTasks()
}

onMounted(() => { fetchTasks(); fetchStars() })
</script>

Task 3.2: 任务进度查询页

Files:

  • Create: frontend/src/views/TaskProgress.vue

  • Step 1: 创建任务进度查询页面

<template>
  <div class="task-progress">
    <div class="header">
      <h2>任务进度查询</h2>
    </div>

    <el-tabs v-model="activeTab">
      <el-tab-pane label="每日任务" name="daily">
        <el-card>
          <el-form :inline="true">
            <el-form-item label="用户ID">
              <el-input v-model="filters.user_id" placeholder="用户ID" clearable />
            </el-form-item>
            <el-form-item label="明星ID">
              <el-input v-model="filters.star_id" placeholder="明星ID" clearable />
            </el-form-item>
            <el-form-item label="状态">
              <el-select v-model="filters.status" clearable>
                <el-option label="待完成" value="pending" />
                <el-option label="已完成" value="completed" />
                <el-option label="已领取" value="claimed" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="fetchDailyProgress">搜索</el-button>
            </el-form-item>
          </el-form>

          <el-table :data="dailyItems" v-loading="loading" style="margin-top: 20px">
            <el-table-column prop="id" label="ID" width="80" />
            <el-table-column prop="user_id" label="用户ID" width="100" />
            <el-table-column prop="star_id" label="明星ID" width="100" />
            <el-table-column prop="task_key" label="任务Key" width="180" />
            <el-table-column prop="status" label="状态" width="100">
              <template #default="{ row }">
                <el-tag>{{ row.status }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="completed_at" label="完成时间" width="180">
              <template #default="{ row }">
                {{ formatTime(row.completed_at) }}
              </template>
            </el-table-column>
            <el-table-column prop="claimed_at" label="领取时间" width="180">
              <template #default="{ row }">
                {{ formatTime(row.claimed_at) }}
              </template>
            </el-table-column>
          </el-table>

          <el-pagination
            v-model:current-page="pagination.page"
            v-model:page-size="pagination.page_size"
            :total="pagination.total"
            :page-sizes="[10, 20, 50, 100]"
            layout="total, sizes, prev, pager, next"
            @size-change="fetchDailyProgress" @current-change="fetchDailyProgress"
            style="margin-top: 20px; justify-content: flex-end"
          />
        </el-card>
      </el-tab-pane>

      <el-tab-pane label="引导任务" name="onboarding">
        <el-card>
          <el-form :inline="true">
            <el-form-item label="用户ID">
              <el-input v-model="onboardFilters.user_id" placeholder="用户ID" clearable />
            </el-form-item>
            <el-form-item label="状态">
              <el-select v-model="onboardFilters.status" clearable>
                <el-option label="待完成" value="pending" />
                <el-option label="已完成" value="completed" />
                <el-option label="已领取" value="claimed" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="fetchOnboardingProgress">搜索</el-button>
            </el-form-item>
          </el-form>

          <el-table :data="onboardItems" v-loading="onboardLoading" style="margin-top: 20px">
            <el-table-column prop="id" label="ID" width="80" />
            <el-table-column prop="user_id" label="用户ID" width="100" />
            <el-table-column prop="task_key" label="任务Key" width="180" />
            <el-table-column prop="status" label="状态" width="100">
              <template #default="{ row }">
                <el-tag>{{ row.status }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="completed_at" label="完成时间" width="180" />
            <el-table-column prop="claimed_at" label="领取时间" width="180" />
          </el-table>

          <el-pagination
            v-model:current-page="onboardPagination.page"
            v-model:page-size="onboardPagination.page_size"
            :total="onboardPagination.total"
            :page-sizes="[10, 20, 50, 100]"
            layout="total, sizes, prev, pager, next"
            @size-change="fetchOnboardingProgress" @current-change="fetchOnboardingProgress"
            style="margin-top: 20px; justify-content: flex-end"
          />
        </el-card>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getDailyProgress, getOnboardingProgress } from '@/api/admin'

const activeTab = ref('daily')
const loading = ref(false)
const onboardLoading = ref(false)

const filters = reactive({ user_id: null, star_id: null, status: '' })
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
const dailyItems = ref([])

const onboardFilters = reactive({ user_id: null, status: '' })
const onboardPagination = reactive({ page: 1, page_size: 20, total: 0 })
const onboardItems = ref([])

const formatTime = (ts) => {
  if (!ts) return '-'
  return new Date(ts).toLocaleString()
}

const fetchDailyProgress = async () => {
  loading.value = true
  try {
    const res = await getDailyProgress({
      user_id: filters.user_id || undefined,
      star_id: filters.star_id || undefined,
      status: filters.status || undefined,
      page: pagination.page,
      page_size: pagination.page_size,
    })
    dailyItems.value = res.items
    pagination.total = res.total
  } finally {
    loading.value = false
  }
}

const fetchOnboardingProgress = async () => {
  onboardLoading.value = true
  try {
    const res = await getOnboardingProgress({
      user_id: onboardFilters.user_id || undefined,
      status: onboardFilters.status || undefined,
      page: onboardPagination.page,
      page_size: onboardPagination.page_size,
    })
    onboardItems.value = res.items
    onboardPagination.total = res.total
  } finally {
    onboardLoading.value = false
  }
}

onMounted(() => {
  fetchDailyProgress()
  fetchOnboardingProgress()
})
</script>

Task 3.3: 任务统计页

Files:

  • Create: frontend/src/views/TaskStats.vue

  • Step 1: 创建任务统计页面

<template>
  <div class="task-stats">
    <div class="header">
      <h2>任务系统统计</h2>
      <el-select v-model="selectedStarId" placeholder="选择明星" clearable @change="fetchStats">
        <el-option v-for="star in stars" :key="star.star_id" 
          :label="star.name" :value="star.star_id" />
      </el-select>
    </div>

    <el-row :gutter="20" class="overview-cards">
      <el-col :span="6">
        <el-card>
          <div class="stat-card">
            <div class="stat-value">{{ stats.total_definitions }}</div>
            <div class="stat-label">任务定义总数</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div class="stat-card">
            <div class="stat-value">{{ stats.active_definitions }}</div>
            <div class="stat-label">启用任务数</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div class="stat-card">
            <div class="stat-value">{{ stats.total_daily_progress }}</div>
            <div class="stat-label">每日任务进度记录</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card>
          <div class="stat-card">
            <div class="stat-value">{{ stats.claimable_revenue }}</div>
            <div class="stat-label">待领取收益</div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="20" class="charts-row">
      <el-col :span="12">
        <el-card title="每日任务完成率趋势">
          <div ref="completionChartRef" style="height: 300px"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card title="任务类型分布">
          <div ref="distributionChartRef" style="height: 300px"></div>
        </el-card>
      </el-col>
    </el-row>

    <el-row :gutter="20" class="charts-row">
      <el-col :span="12">
        <el-card title="每日任务领取率趋势">
          <div ref="claimChartRef" style="height: 300px"></div>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card title="热门任务 TOP10">
          <div ref="hotTasksChartRef" style="height: 300px"></div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getOverviewStats, getDailyTrend, getStars } from '@/api/admin'

const selectedStarId = ref(null)
const stars = ref([])
const stats = reactive({
  total_definitions: 0,
  active_definitions: 0,
  total_daily_progress: 0,
  claimable_revenue: 0,
})

const completionChartRef = ref(null)
const claimChartRef = ref(null)
const distributionChartRef = ref(null)
const hotTasksChartRef = ref(null)

let completionChart, claimChart, distributionChart, hotTasksChart

const fetchStats = async () => {
  try {
    const overview = await getOverviewStats()
    Object.assign(stats, overview)

    const trendRes = await getDailyTrend({ star_id: selectedStarId.value, days: 7 })
    updateCompletionChart(trendRes.trends)
    updateClaimChart(trendRes.trends)
    updateDistributionChart()
  } catch (e) {
    console.error(e)
  }
}

const updateCompletionChart = (trends) => {
  const xData = trends.map(t => t.date)
  const yData = trends.map(t => t.completion_rate || 0)
  
  completionChart.setOption({
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: xData },
    yAxis: { type: 'value', name: '完成率%', max: 100 },
    series: [{
      name: '完成率',
      type: 'line',
      smooth: true,
      data: yData,
      areaStyle: { color: 'rgba(82, 196, 26, 0.2)' },
      lineStyle: { color: '#52c41a' },
    }]
  })
}

const updateClaimChart = (trends) => {
  const xData = trends.map(t => t.date)
  const yData = trends.map(t => t.claim_rate || 0)
  
  claimChart.setOption({
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: xData },
    yAxis: { type: 'value', name: '领取率%', max: 100 },
    series: [{
      name: '领取率',
      type: 'bar',
      data: yData,
      itemStyle: { color: '#1890ff' },
    }]
  })
}

const updateDistributionChart = () => {
  distributionChart.setOption({
    tooltip: { trigger: 'item' },
    legend: { bottom: 0 },
    series: [{
      type: 'pie',
      radius: ['40%', '70%'],
      label: { show: true, formatter: '{b}: {c} ({d}%)' },
      data: [
        { value: 4, name: '每日任务' },
        { value: 5, name: '引导任务' },
      ]
    }]
  })
}

onMounted(async () => {
  await nextTick()
  
  completionChart = echarts.init(completionChartRef.value)
  claimChart = echarts.init(claimChartRef.value)
  distributionChart = echarts.init(distributionChartRef.value)
  hotTasksChart = echarts.init(hotTasksChartRef.value)

  const starRes = await getStars()
  stars.value = starRes

  await fetchStats()
})
</script>

Task 3.4: 手动重置页

Files:

  • Create: frontend/src/views/TaskReset.vue

  • Step 1: 创建手动重置页面

<template>
  <div class="task-reset">
    <div class="header">
      <h2>手动重置每日任务</h2>
    </div>

    <el-card>
      <el-alert
        title="警告"
        type="warning"
        description="重置操作会将所有用户的每日任务进度恢复为待完成状态,已领取的奖励不会回退。"
        :closable="false"
        style="margin-bottom: 20px"
      />

      <el-form :inline="true">
        <el-form-item label="明星">
          <el-select v-model="selectedStarId" placeholder="全部(全局重置)" clearable>
            <el-option label="全局" :value="null" />
            <el-option v-for="star in stars" :key="star.star_id" 
              :label="star.name" :value="star.star_id" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="danger" @click="handleReset" :loading="loading">
            执行重置
          </el-button>
        </el-form-item>
      </el-form>

      <div v-if="resetResult" style="margin-top: 20px">
        <el-alert
          :title="`重置成功,影响 ${resetResult} 条记录`"
          type="success"
          show-icon
        />
      </div>
    </el-card>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { resetDailyTasks, getStars } from '@/api/admin'
import { ElMessage } from 'element-plus'

const selectedStarId = ref(null)
const stars = ref([])
const loading = ref(false)
const resetResult = ref(null)

const fetchStars = async () => {
  try {
    const res = await getStars()
    stars.value = res
  } catch (e) {
    console.error(e)
  }
}

const handleReset = async () => {
  loading.value = true
  resetResult.value = null
  try {
    const res = await resetDailyTasks(selectedStarId.value)
    resetResult.value = res.affected
    ElMessage.success(`重置成功,影响 ${res.affected} 条记录`)
  } catch (e) {
    ElMessage.error('重置失败')
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchStars()
})
</script>

Task 3.5: 展示收益页

Files:

  • Create: frontend/src/views/RevenueList.vue

  • Step 1: 创建展示收益页面

<template>
  <div class="revenue-list">
    <div class="header">
      <h2>展示收益记录</h2>
    </div>

    <el-card>
      <el-form :inline="true">
        <el-form-item label="用户ID">
          <el-input v-model="filters.user_id" placeholder="用户ID" clearable />
        </el-form-item>
        <el-form-item label="明星ID">
          <el-input v-model="filters.star_id" placeholder="明星ID" clearable />
        </el-form-item>
        <el-form-item label="状态">
          <el-select v-model="filters.status" clearable>
            <el-option label="可领取" value="claimable" />
            <el-option label="已领取" value="claimed" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="fetchRevenue">搜索</el-button>
        </el-form-item>
      </el-form>

      <el-table :data="items" v-loading="loading" style="margin-top: 20px">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="user_id" label="用户ID" width="100" />
        <el-table-column prop="star_id" label="明星ID" width="100" />
        <el-table-column prop="slot_type" label="展位类型" width="100">
          <template #default="{ row }">
            <el-tag :type="row.slot_type === 'own' ? 'success' : 'warning'">
              {{ row.slot_type === 'own' ? '自有' : '好友' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="crystal_amount" label="水晶数量" width="120">
          <template #default="{ row }">
            💎 {{ row.crystal_amount }}
          </template>
        </el-table-column>
        <el-table-column prop="cycle_start_time" label="周期开始" width="180">
          <template #default="{ row }">
            {{ formatTime(row.cycle_start_time) }}
          </template>
        </el-table-column>
        <el-table-column prop="cycle_end_time" label="周期结束" width="180">
          <template #default="{ row }">
            {{ formatTime(row.cycle_end_time) }}
          </template>
        </el-table-column>
        <el-table-column prop="status" label="状态" width="100">
          <template #default="{ row }">
            <el-tag :type="row.status === 'claimable' ? 'success' : 'info'">
              {{ row.status === 'claimable' ? '可领取' : '已领取' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>

      <el-pagination
        v-model:current-page="pagination.page"
        v-model:page-size="pagination.page_size"
        :total="pagination.total"
        :page-sizes="[10, 20, 50, 100]"
        layout="total, sizes, prev, pager, next"
        @size-change="fetchRevenue" @current-change="fetchRevenue"
        style="margin-top: 20px; justify-content: flex-end"
      />
    </el-card>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getRevenueRecords } from '@/api/admin'

const loading = ref(false)
const filters = reactive({ user_id: null, star_id: null, status: '' })
const pagination = reactive({ page: 1, page_size: 20, total: 0 })
const items = ref([])

const formatTime = (ts) => {
  if (!ts) return '-'
  return new Date(ts).toLocaleString()
}

const fetchRevenue = async () => {
  loading.value = true
  try {
    const res = await getRevenueRecords({
      user_id: filters.user_id || undefined,
      star_id: filters.star_id || undefined,
      status: filters.status || undefined,
      page: pagination.page,
      page_size: pagination.page_size,
    })
    items.value = res.items
    pagination.total = res.total
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  fetchRevenue()
})
</script>

Task 3.6: 更新 API 和路由

Files:

  • Modify: frontend/src/api/admin.js

  • Step 1: 添加任务相关 API

// 任务定义
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
{
  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

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

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

// 更新经验值请求内部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 方法

// 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 方法

// 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 方法

// 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
// 在 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

// 每日任务
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. 移动端
    • 每日任务列表显示正常
    • 任务完成上报正常
    • 奖励领取正常
    • 展示收益显示和领取正常