# 任务管理系统设计文档 > **创建日期:** 2026-04-08 > **项目:** TopFans 任务管理系统 > **服务:** activity-admin (Python FastAPI) + taskService (Go Dubbo-go) + 共享 PostgreSQL --- ## 一、整体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 移动端 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ 每日任务页面 │ │ 引导任务页面 │ │ 展示收益/奖励领取 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────┘ │ └─────────┼─────────────────┼─────────────────────┼───────────────┘ │ │ │ │ Triple/HTTP │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ taskService (新服务,端口 20005) │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Mobile API Handler (HTTP/Triple) │ │ │ │ /api/tasks/daily/* /api/tasks/guide/* /api/tasks/* │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ DailyTaskService│ │OnboardingService│ │ RevenueService │ │ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ │ ┌────────▼─────────────────────▼─────────────────────▼────────┐ │ │ │ Repository Layer (GORM) │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────▼─────────────┐ │ │ │ DailyResetWorker │ │ │ │ + RevenueAutoClaimWorker │ │ │ │ (PostgreSQL Advisory │ │ │ │ Lock,05:00 Asia/Shanghai)│ │ │ └───────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▲ ▲ │ Triple RPC │ │ ▼ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ galleryService │ │ userService │ │ activity-admin │ │ (OnExhibition │ │ (UpdateCrystal │ │ (任务定义管理) │ │ Completed RPC)│ │ Balance + │ │ 直接操作DB │ │ │ │ AddExperience)│ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▲ ▲ └────────────────────▼────────────────────┘ PostgreSQL ``` --- ## 二、设计决策汇总 ### 2.1 服务职责划分 | 服务 | 职责 | |------|------| | activity-admin (Python) | 任务定义管理、进度查询、统计、手动重置 | | taskService (Go) | 移动端 API、内部 RPC、奖励发放、定时任务 | | PostgreSQL | 共用数据库 | ### 2.2 核心设计决策 | 决策项 | 选择 | |--------|------| | 移动端 API 提供方 | 新建独立 taskService 微服务 | | 引导完成上报 | 前端每步完成立即调用 `/api/tasks/guide/complete` | | 每日任务重置 | PostgreSQL Advisory Lock,每天 05:00 Asia/Shanghai 执行 | | 引导阶段推进 | 前端主动调用 `/api/tasks/onboarding/advance-stage` | | 每日任务奖励 | 手动领取(单个或一键领取) | | 引导任务奖励 | 阶段切换时统一发放 | | 展示收益发放 | 统一到次日 05:00 自动发放,支持手动领取 | | 展示收益计算 | 由调用方(galleryService)计算后传入 | | 每日任务 scope | 每个 (user_id, star_id) 各自独立 | | InitUserTasks | 创建 user_onboarding_status + 该 star_id 下的每日任务进度;新增粉丝身份时创建对应进度 | | 重置日志 | 记录到 task_reset_log 表 | | 旧字段处理 | is_first_login_bonus_claimed、has_friend_display_bonus 标记废弃 | | 奖励发放失败 | 记录日志 + 本地重试 N 次(可配置),仍失败标记 failed | | 重置时区判断 | 按自然天,每天 05:00 执行一次 | | 重置范围 | 全局所有用户所有 star_id | | 引导上报 key | 动态配置,数据库存什么 key,前端就上报什么 key | | 阶段切换检查 | 后端检查当前阶段所有任务是否已完成,未完成拒绝切换 | | 引导任务 | 不允许重复完成,不重置 | | claimed 状态 | 每日重置时也重置回 pending | | 收益自动发放 | 分批发放,每条记录结果保存,失败重试 N 次(可配置) | | 统计页面 | 多指标(完成率、领取率等)、多维度(star_id、时间范围) | | Worker 设计 | 共用一个 Worker,05:00 先重置再发收益 | | 日志监控 | 接入现有日志和监控体系 | | 健康检查 | 不实现,复用 Dubbo-go 框架自带能力 | | 错误码 | 复用现有 BaseResponse.code 体系 | | 分页规范 | page + page_size + total | | 引导状态返回 | POST `/api/tasks/guide/complete` 返回当前引导完整状态 | | 锁 key 设计 | pg_advisory_lock(YYYYMMDD) | | taskService 端口 | 20005 | | taskService 调用 userService | 直接调用(UpdateCrystalBalance + AddExperience) | | activity-admin | 直接操作数据库,不通过 taskService API | --- ## 三、数据库设计 ### 3.1 ER 图 ``` ┌─────────────────────────┐ ┌─────────────────────────────┐ │ task_definitions │ │ onboarding_stage_config │ ├─────────────────────────┤ ├─────────────────────────────┤ │ id (PK) │ │ id (PK) │ │ star_id │ │ stage │ │ task_key │────▶│ required_task_keys (TEXT[]) │ │ task_type │ │ crystal_reward │ │ name │ │ exp_reward │ │ description │ │ ... │ │ crystal_reward │ └─────────────────────────────┘ │ exp_reward │ ▲ │ ... │ │ └─────────────────────────┘ │ │ │ │ 1:N 1:N │ ▼ │ ┌─────────────────────────┐ ┌─────────────────────────────┐ │ user_daily_task_progress│ │ user_onboarding_progress │ ├─────────────────────────┤ ├─────────────────────────────┤ │ id (PK) │ │ id (PK) │ │ user_id │ │ user_id │ │ star_id │ │ task_key ───────────────────┘ │ task_key │◀────│ │ status │ │ ┌─────────────────────────┐ │ completed_at │ │ │user_onboarding_status │ │ claimed_at │ │ ├─────────────────────────┤ │ ... │ │ │ user_id (PK) │ └─────────────────────────┘ │ │ current_stage │ │ │ status │ │ │ is_first_login_bonus_ │ │ │ claimed (废弃) │ │ │ has_friend_display_bonus│ │ │ (废弃) │ │ └─────────────────────────┘ │ ┌─────────────────────────────┐ │ │ exhibition_revenue_records │ │ ├─────────────────────────────┤ │ │ id (PK) │ │ │ user_id │ │ │ star_id │ │ │ exhibition_id │ │ │ asset_id │ │ │ slot_id │ │ │ slot_owner_uid │ │ │ slot_type ('own'/'friend') │ │ │ crystal_amount │ │ │ cycle_start_time │ │ │ cycle_end_time │ │ │ status ('claimable'/'...') │ │ │ claimed_at │ │ │ created_at │ │ └─────────────────────────────┘ ┌─────────────────────────────┐ │ task_reset_log │ ├─────────────────────────────┤ │ id (PK) │ │ reset_type │ │ last_reset_at │ │ created_at │ └─────────────────────────────┘ ``` ### 3.2 表结构 #### task_definitions(任务定义表) ```sql CREATE TABLE 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); ``` #### user_daily_task_progress(每日任务进度表) ```sql CREATE TABLE 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); ``` #### user_onboarding_progress(引导任务进度表) ```sql CREATE TABLE user_onboarding_progress ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, task_key VARCHAR(50) NOT NULL, status VARCHAR(20) DEFAULT 'pending', 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); ``` #### user_onboarding_status(引导流程状态表) ```sql CREATE TABLE user_onboarding_status ( user_id BIGINT PRIMARY KEY, current_stage INT DEFAULT 0, -- 0=未开始,1~N=阶段 status VARCHAR(20) DEFAULT 'pending', -- 以下字段标记废弃,但仍保留 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 ); ``` #### onboarding_stage_config(引导阶段配置表) ```sql CREATE TABLE 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 ); ``` #### exhibition_revenue_records(展示收益记录表) ```sql CREATE TABLE 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', 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); ``` #### task_reset_log(重置日志表) ```sql CREATE TABLE task_reset_log ( id BIGSERIAL PRIMARY KEY, reset_type VARCHAR(20) NOT NULL, -- 'daily' last_reset_at BIGINT NOT NULL, created_at BIGINT ); ``` --- ## 四、API 设计 ### 4.1 移动端 API(taskService HTTP/Triple) #### 每日任务 | 方法 | 路径 | 请求 | 说明 | |------|------|------|------| | GET | `/api/tasks/daily` | `star_id` | 获取每日任务列表及进度 | | POST | `/api/tasks/report-event` | `{ event_type, star_id }` | 上报任务事件(如浏览展品、铸造等) | | POST | `/api/tasks/daily/claim` | `{ task_key, star_id }` | 领取单个任务奖励 | | POST | `/api/tasks/daily/claim-all` | `star_id` | 一键领取所有已完成任务奖励 | **GET /api/tasks/daily 响应:** ```json { "star_id": 1, "tasks": [ { "task_key": "daily_login", "star_id": 1, "name": "每日首次登录", "description": "每日首次登录 App", "crystal_reward": 20, "exp_reward": 20, "status": "pending", "can_claim": false } ] } ``` **POST /api/tasks/report-event 请求:** ```json { "event_type": "daily_browse_asset", "star_id": 1 } ``` **POST /api/tasks/report-event 响应:** ```json { "success": true, "task_key": "daily_browse_asset", "task_completed": true, "message": "任务完成" } ``` #### 引导任务 | 方法 | 路径 | 请求 | 说明 | |------|------|------|------| | POST | `/api/tasks/guide/complete` | `{ task_key }` | 上报引导步骤完成 | | GET | `/api/tasks/onboarding/status` | - | 获取引导状态 | | POST | `/api/tasks/onboarding/advance-stage` | `{ target_stage }` | 切换阶段 | | POST | `/api/tasks/onboarding/claim-reward` | `{ stage }` | 领取当前阶段奖励 | **POST /api/tasks/guide/complete 响应:** ```json { "user_id": 123, "current_stage": 1, "status": "in_progress", "stages": [ { "stage": 1, "name": "入门引导", "required_task_keys": ["square_home", "profile_edit"], "crystal_reward": 0, "exp_reward": 0 } ] } ``` **GET /api/tasks/onboarding/status 响应:** ```json { "user_id": 123, "current_stage": 1, "status": "in_progress", "stages": [ { "stage": 1, "name": "入门引导", "required_task_keys": ["square_home", "profile_edit"], "crystal_reward": 0, "exp_reward": 0 } ] } ``` #### 展示收益 | 方法 | 路径 | 请求 | 说明 | |------|------|------|------| | GET | `/api/tasks/exhibition-revenue` | `star_id`, `status`, `page`, `page_size` | 获取收益记录列表 | | POST | `/api/tasks/exhibition-revenue/claim` | `{ revenue_id, star_id }` | 领取单个收益 | | POST | `/api/tasks/exhibition-revenue/claim-all` | `star_id` | 一键领取所有可领取收益 | **GET /api/tasks/exhibition-revenue 响应:** ```json { "items": [ { "id": 123, "star_id": 1, "exhibition_id": 456, "asset_id": 789, "slot_id": 1, "slot_type": "own", "crystal_amount": 100, "cycle_start_time": 1712607600000, "cycle_end_time": 1712694000000, "status": "claimable", "can_claim": true } ], "total": 10, "page": 1, "page_size": 20 } ``` ### 4.2 Dubbo 内部 RPC #### taskService 作为服务端(被调用) **TaskInternalService:** ```protobuf service TaskInternalService { // 用户注册时调用,创建 onboarding_status + 该 star_id 下的每日任务进度 rpc InitUserTasks(InitUserTasksRequest) returns (InitUserTasksResponse); // 展品到期完成(galleryService 调用) rpc OnExhibitionCompleted(OnExhibitionCompletedRequest) returns (OnExhibitionCompletedResponse); } ``` **消息定义:** ```protobuf message InitUserTasksRequest { int64 user_id = 1; int64 star_id = 2; // 注册时选择的第一个 star_id } message InitUserTasksResponse { topfans.common.BaseResponse base = 1; bool success = 2; } message OnExhibitionCompletedRequest { int64 exhibition_id = 1; int64 asset_id = 2; int64 slot_id = 3; int64 occupier_uid = 4; int64 occupier_star_id = 5; int64 slot_owner_uid = 6; int64 crystal_amount = 7; int64 start_time = 8; int64 expire_at = 9; } message OnExhibitionCompletedResponse { topfans.common.BaseResponse base = 1; int64 revenue_record_id = 2; } ``` #### taskService 作为客户端(调用 userService) **userService 新增 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 AddExperience(AddExperienceRequest) returns (AddExperienceResponse); ``` --- ## 五、业务流程 ### 5.1 每日任务流程 ``` 1. 用户注册 → userService 调用 taskService.InitUserTasks - 创建 user_onboarding_status(新用户标记) - 创建该 star_id 下的每日任务进度记录(status=pending) 2. 用户登录 → 前端调用 GET /api/tasks/daily?star_id=xxx 3. 后端返回该用户的每日任务列表(含 status) 4. 用户完成行为(如浏览展品)→ 前端调用 POST /api/tasks/report-event 5. 后端自动标记任务完成(status=completed) 6. 用户点击"领取" → POST /api/tasks/daily/claim 7. 后端验证任务已完成 → 调用 userService.UpdateCrystalBalance → 调用 AddExperience → 更新状态为 claimed 8. 每日 05:00 Asia/Shanghai: a. Advisory Lock 获取锁(pg_advisory_lock(YYYYMMDD)) b. 查询 task_reset_log,如今天已有记录则跳过 c. 所有非 pending 状态的记录恢复为 pending d. 记录 task_reset_log e. 释放锁 ``` ### 5.2 引导任务流程 ``` 1. 用户首次登录 → 前端调用 GET /api/tasks/onboarding/status 2. 返回 { current_stage: 0, status: 'pending', stages: [...] } 3. 前端根据 stage 配置显示引导步骤 4. 用户完成引导步骤(如 square_home) 5. → POST /api/tasks/guide/complete { task_key: "square_home" } 6. 后端更新 user_onboarding_progress.status = 'completed' 7. 前端引导流程中,用户点击"进入下一阶段" 8. → POST /api/tasks/onboarding/advance-stage { target_stage: 2 } 9. 后端检查 stage 1 所有 task_key 是否都已 completed: - 如未全部完成,返回错误 - 如全部完成,current_stage++,返回最新状态 10. 前端调用 POST /api/tasks/onboarding/claim-reward 领取阶段奖励 11. 重复 3-10,直到所有引导完成 ``` ### 5.3 展示收益流程 ``` 1. galleryService 的 CleanupWorker 扫描到过期展品 2. → 调用 taskService.OnExhibitionCompleted RPC 3. taskService 创建 exhibition_revenue_records(status=claimable) 4. 每日 05:00(紧接重置后): a. 查询所有 status=claimable 的记录 b. 分批处理(每批 N 条,可配置) c. 调用 userService.UpdateCrystalBalance 发放水晶 d. 成功则更新 status=claimed,失败则重试 N 次 e. 仍失败则标记 status=failed(对用户不可见,运营可查) 5. 用户打开移动端 → GET /api/tasks/exhibition-revenue - 前端只显示 claimable 和 claimed 状态的记录 - failed 记录对用户不可见 6. 用户点击领取 → POST /api/tasks/exhibition-revenue/claim 7. 后端调用 userService.UpdateCrystalBalance → 更新状态 ``` --- ## 六、分页规范 所有列表查询统一分页参数: | 参数 | 类型 | 说明 | |------|------|------| | page | int | 页码,从 1 开始 | | page_size | int | 每页条数,最大 100 | 响应结构: ```json { "items": [...], "total": 100, "page": 1, "page_size": 20 } ``` --- ## 七、Worker 设计 ### 7.1 DailyResetWorker + RevenueAutoClaimWorker **共用一个 Worker,每天 05:00 Asia/Shanghai 执行。** ```go // 伪代码 func (w *Worker) run() { // 尝试获取锁(日期作为锁ID) lockID := time.Now().Format("20060102") // 如 "20230409" if !w.acquireAdvisoryLock(lockID) { return // 其他实例在执行 } defer w.releaseAdvisoryLock(lockID) // 1. 执行每日任务重置 w.resetDailyTasks() // 2. 执行展示收益自动发放 w.autoClaimExhibitionRevenue() } func (w *Worker) resetDailyTasks() { // 查询 task_reset_log,如今天已有记录则跳过 // 更新所有 status != 'pending' 的记录为 'pending' // 记录 task_reset_log } func (w *Worker) autoClaimExhibitionRevenue() { // 查询所有 status='claimable' 的记录 // 分批处理(batch_size 可配置) // 每批:调用 userService.UpdateCrystalBalance // - 成功:更新 status='claimed' // - 失败:重试 N 次(可配置),仍失败标记 status='failed' // 每条记录处理结果保存到日志 } ``` ### 7.2 Advisory Lock 设计 - **锁 key:** 日期格式 `YYYYMMDD`(如 `20230409`) - **获取锁:** `pg_try_advisory_lock(lockID)` - **释放锁:** `pg_advisory_unlock(lockID)` - **作用:** 保证多实例环境下每天只执行一次重置和自动发放 --- ## 八、activity-admin 页面 | 页面 | 路由 | 功能 | |------|------|------| | 任务定义列表 | `/tasks/definitions` | 查看、新建、编辑、删除、启用/禁用任务定义 | | 每日任务进度 | `/tasks/daily-progress` | 查询各用户的每日任务完成情况 | | 引导任务进度 | `/tasks/onboarding-progress` | 查询各用户的引导任务完成情况 | | 任务统计 | `/tasks/stats` | 任务完成率/领取率统计图表,支持 star_id、时间范围筛选 | | 手动重置 | `/tasks/reset` | 手动触发每日任务重置 | | 收益记录 | `/tasks/revenue` | 查看展示收益记录(含 failed),支持手动补发 | **统计页面指标:** - 每日任务:完成人数、领取人数、完成率、领取率 - 引导任务:各 stage 完成人数、总体完成率 - 收益统计:总发放水晶数、待领取水晶数、已领取水晶数 **筛选维度:** - star_id - 时间范围(近7天、近30天、自定义) - 任务类型(daily/onboarding) --- ## 九、项目结构 ### 9.1 目录结构 ``` backend/ ├── proto/ │ ├── user.proto # 修改:新增 AddExperience RPC │ └── task.proto # 新增:TaskInternalService │ ├── services/ │ ├── taskService/ # 新增 │ │ ├── main.go │ │ ├── config/ │ │ │ └── task_config.go │ │ ├── repository/ │ │ │ └── task_repository.go │ │ ├── service/ │ │ │ ├── daily_task_service.go │ │ │ ├── onboarding_service.go │ │ │ └── exhibition_revenue_service.go │ │ ├── provider/ │ │ │ ├── task_mobile_provider.go # 移动端 API │ │ │ └── task_internal_provider.go # 内部 RPC │ │ ├── worker/ │ │ │ └── daily_reset_worker.go │ │ └── client/ │ │ └── user_rpc_client.go │ │ │ └── userService/ # 修改 │ ├── repository/ │ │ └── fan_profile_repository.go # 新增 UpdateExperience │ ├── service/ │ │ └── user_service.go # 新增 AddExperience │ └── provider/ │ ├── user_provider.go # 新增 AddExperience │ └── unified_provider.go # 新增 AddExperience │ ├── pkg/ │ └── proto/ │ ├── user/ │ │ └── *.go # 重新生成 │ └── task/ │ └── *.go # 新生成 │ └── scripts/ └── v001_init_task_tables.sql ``` ### 9.2 前端文件(activity-admin) ``` frontend/src/ ├── api/ │ └── admin.js # 修改:新增任务相关 API ├── views/ │ ├── TaskDefinitionList.vue │ ├── TaskProgress.vue │ ├── TaskStats.vue │ ├── TaskReset.vue │ └── RevenueList.vue └── router/ └── index.js # 修改:新增路由 ``` --- ## 十、后续迭代项 以下内容在当前设计中预留扩展点,后续可根据业务需求添加: 1. **引导任务细分** - `square_home` 等 key 是否需要与 star_id 关联 2. **任务事件扩展** - ReportTaskEvent 的 event_type 可继续扩展 3. **收益周期细分** - 当前 cycle_start/end_time 为基础字段,可扩展 cycle_name 等 4. **统计图表扩展** - 当前预留 ECharts 位置,可扩展更多图表类型 5. **手动重置扩展** - 当前支持全局重置,可扩展按 star_id 重置 --- ## 十一、错误码规范 复用现有 `BaseResponse.code` 体系,不新增自定义业务错误码。 | 场景 | 处理方式 | |------|----------| | 领取未完成任务 | 静默返回成功,前端根据 status 阻止 | | 领取已领取任务 | 静默返回成功,前端根据 status 阻止 | | 阶段切换但任务未完成 | 返回错误,前端提示 | | RPC 超时 | 记录日志,不自动重试(可手动补发) | | 重置时其他实例在执行 | 跳过本次执行 |