hyapp-server/docs/每日任务系统架构.md
2026-05-12 20:07:49 +08:00

18 KiB
Raw Blame History

每日任务系统架构方案

本文定义 App 每日任务和专属任务的产品边界、服务归属、事件来源、数据模型、后台管理和关键异常处理。

目标

  • 后台增加 活动管理 一级菜单,每日任务 作为活动管理下的二级菜单。
  • 每日任务 页面包含两个 Tab每日任务专属任务
  • 每日任务按 UTC 自然日刷新,当天只能领取一次,超额完成不结转到明天。
  • 专属任务是用户维度终身只能完成并领取一次的任务。
  • 任务奖励统一发金币,金币发放必须走 wallet-service 账本。
  • 任务进度由服务端事实事件驱动,客户端不能上报任务进度。

非目标

  • 首版不做客户端自定义任务排序。
  • 首版不做多奖励类型;奖励只支持金币。
  • 首版不做任务进度回滚退款、撤销、CP 解除等后续事件进入风控和补偿,不直接减已完成任务。
  • 首版不让 room-service、wallet-service、user-service 同步调用任务系统更新进度。

服务边界

模块 责任 不负责
server/admin 菜单、后台 HTTP API、任务配置管理、启停、排序、查询统计 不直接写 app 业务库;通过 protobuf client 或明确集成接口访问后端
activity-service 任务定义、任务版本、任务进度、完成状态、领奖状态、任务事件消费幂等 不直接改钱包余额,不拥有房间/钱包/用户基础事实
wallet-service 金币奖励入账、账本幂等、奖励流水 不判断任务是否完成
user-service CP 关系事实、用户麦上时长事实 不判断任务奖励
room-service 房间和麦位状态 owner产生房间 outbox 不同步更新任务

建议首版把任务系统放在 activity-service因为它已经承接活动、消息、fanout 等低频活动能力。后续如果任务规模独立膨胀,再拆 task-service

总体架构

flowchart LR
    Admin["admin 活动管理 > 每日任务"] --> AdminAPI["admin-server task APIs"]
    AdminAPI --> ActivityAdmin["activity-service AdminTaskService"]

    Room["room-service outbox"] --> UserMic["user-service mic time consumer"]
    UserMic --> UserOutbox["user-service user_outbox"]
    Wallet["wallet-service outbox"] --> TaskConsumer["activity-service task consumers"]
    UserOutbox --> TaskConsumer
    CP["user-service CP outbox"] --> TaskConsumer

    TaskConsumer --> TaskDB["activity MySQL task tables"]
    App["App task page"] --> Gateway["gateway-service"]
    Gateway --> ActivityQuery["activity-service TaskQueryService"]
    Gateway --> ActivityClaim["activity-service TaskClaimService"]
    ActivityClaim --> WalletGrant["wallet-service CreditTaskReward"]

核心原则:

  • 任务系统只消费事实事件不参与房间、钱包、CP 主链路。
  • 金币奖励只通过钱包账本发放。
  • 每个事件必须有稳定 event_id,任务消费必须幂等。
  • 任务完成和领奖是两个状态,完成不等于已经发奖。

任务类型

分类 进度指标 事件来源 口径
CP 任务 组成 CP 次数 user-service CP 关系创建事件 CpRelationshipCreated 为准,解除 CP 不回滚
上麦任务 麦上有效分钟数 user-service 麦位时长聚合事件 使用 mic_online_ms,不是单纯占麦时间
游戏任务 游戏消耗金币数 wallet-service 游戏扣费事件 只统计成功扣费的 COIN
送礼任务 送礼消耗金币数 wallet-service 送礼扣费事件 使用钱包扣费金额,不能用客户端礼物价格
幸运礼物任务 幸运礼物消耗金币数或中奖金币数 wallet-service 幸运礼物结算事件 首版一个任务只配置一种指标:spend_coinwin_coin
充值任务 充值金币数 wallet-service 充值成功事件 Google、Apple、三方支付、币商转普通金币都可以按配置纳入口径

上麦任务口径

上麦任务默认读取 user_mic_daily_stats.mic_online_ms 或消费 UserMicSessionClosed 派生事件。mic_online_ms 只从确认发流开始累计未发声、只占麦、pending_publish 不计入有效麦上时间。

跨天上麦必须按 UTC 自然日拆分到不同 task_day,不能把整段时长归到下麦当天。

幸运礼物任务口径

幸运礼物任务需要避免把“消耗”和“中奖”混在同一个字段:

指标 含义
lucky_gift_spend_coin 用户购买/发送幸运礼物实际消耗金币
lucky_gift_win_coin 用户因幸运礼物中奖获得的金币价值

如果运营需要“消耗 n 金币且中奖 n 金币”,应在第二阶段支持多条件 AND。首版建议一个任务一个指标减少配置歧义。

任务周期

每日任务

  • 周期 key 使用 UTC 自然日计算,例如 UTC 下的 2026-05-09
  • 任务周期不读取机器本地时区、用户手机时区或请求 IP 所在时区。
  • 事件归属日按 occurred_at_ms 转 UTC 计算,不按消费时间。
  • 用户当天完成后只能领取一次。
  • 当天超额完成只保留当天进度,不结转到明天。
  • 每日刷新不需要批量清空用户数据;查询时按当前 task_day 读取或懒创建进度。

专属任务

  • 周期 key 固定为 lifetime
  • 用户终身只能完成一次、领取一次。
  • 首版可作为全体用户可见的 lifetime 任务。
  • 如果后续要“指定用户专属”,再增加任务分配表,不要把用户列表塞进任务定义 JSON。

后台管理

后台菜单:

活动管理
  - 每日任务
      Tab 1: 每日任务
      Tab 2: 专属任务

列表字段

字段 说明
任务 ID 系统生成
任务名称 App 展示名
任务分类 CP / 上麦 / 游戏 / 送礼 / 幸运礼物 / 充值
任务类型 每日任务 / 专属任务
目标值 n 次、n 分钟、n 金币
奖励金币 完成后领取的金币数
状态 draft / active / paused / archived
生效时间 可选
结束时间 可选
排序值 App 列表排序
当前版本 配置版本
创建人/更新时间 审计

新增/编辑任务

后台表单:

字段 必填 说明
任务类型 daily / exclusive
任务分类 固定枚举
任务名称 App 展示
任务描述 App 展示
指标类型 按分类收敛,例如 mic_online_minutesgift_spend_coin
目标值 必须大于 0
奖励金币 必须大于 0
排序值 小值靠前
生效/结束时间 控制任务上下架窗口
状态 draft 保存草稿active 对 App 生效

编辑 active 任务时建议生成新版本。每日任务的新版本默认从下一个 task_day 生效,避免当天用户看到目标值突然变化。

App 接口

查询任务列表

GET /api/v1/tasks/tabs

返回两个分区:

  • daily
  • exclusive

每个任务返回:

字段 说明
task_id 任务 ID
task_type daily / exclusive
category 任务分类
title 展示名称
description 展示描述
target_value 目标值
progress_value 当前进度
reward_coin_amount 奖励金币
status in_progress / completed / claimed / expired
claimable 是否可领取
task_day daily 任务返回当前日期
server_time_ms 服务端时间
next_refresh_at_ms 下次刷新时间

领取奖励

POST /api/v1/tasks/claim

请求:

{
  "task_id": "123",
  "task_type": "daily",
  "task_day": "2026-05-09",
  "command_id": "cmd_task_claim_550e8400"
}

规则:

  • 必须已完成。
  • 必须未领取。
  • daily 只能领取当前 task_day 的任务。
  • exclusive 只能领取 lifetime 任务。
  • command_id 由客户端按领取动作生成,同一次动作重试必须复用,重复请求返回同一结果。

可选第二阶段增加:

POST /api/v1/tasks/claim-all

数据模型

task_definitions

任务定义当前版本的读模型。

字段 说明
task_id 主键
app_code App 租户
task_type daily / exclusive
category cp / mic / game / gift / lucky_gift / recharge
metric_type 具体指标
title 展示名称
description 展示描述
target_value 目标值
target_unit count / minute / coin
reward_coin_amount 奖励金币
status draft / active / paused / archived
sort_order 排序
version 当前版本
effective_from_ms 生效时间
effective_to_ms 结束时间
created_by_admin_id 创建人
updated_by_admin_id 更新人
created_at_ms 创建时间
updated_at_ms 更新时间

task_definition_versions

保存历史版本,保证事件和进度可以追溯。

字段 说明
task_version_id 主键
task_id 任务 ID
version 版本号
snapshot_json 任务配置快照
effective_from_cycle 从哪个 daily cycle 或 lifetime 生效
created_at_ms 创建时间

user_task_progress

用户任务进度。

字段 说明
app_code App 租户
user_id 用户 ID
task_id 任务 ID
task_version_id 任务版本
cycle_key daily 使用 yyyy-mm-ddexclusive 使用 lifetime
progress_value 当前进度,可超过目标值
target_value 进度创建时的目标快照
status in_progress / completed / claimed / expired
completed_at_ms 完成时间
claimed_at_ms 领取时间
updated_at_ms 更新时间

唯一键:

UNIQUE(app_code, user_id, task_id, cycle_key)

task_event_consumption

事件幂等表。

字段 说明
app_code App 租户
event_id 来源事件 ID
event_type 来源事件类型
source_service room / wallet / user
user_id 事件归属用户
task_day UTC 日期
status consumed / skipped / failed
skip_reason 跳过原因
created_at_ms 创建时间
consumed_at_ms 消费时间

主键:

PRIMARY KEY(app_code, event_id)

task_reward_claims

领奖幂等和钱包发奖状态。

字段 说明
claim_id 主键
app_code App 租户
command_id 领奖幂等键
user_id 用户 ID
task_id 任务 ID
cycle_key daily 日期或 lifetime
reward_coin_amount 奖励金币快照
wallet_command_id 钱包发奖幂等键
status pending / granted / failed
failure_reason 失败原因
created_at_ms 创建时间
updated_at_ms 更新时间

唯一键:

UNIQUE(app_code, command_id)
UNIQUE(app_code, user_id, task_id, cycle_key)

事件消费流程

sequenceDiagram
    participant Source as Source outbox
    participant Task as activity task consumer
    participant DB as activity MySQL

    Source->>Task: event(event_id, user_id, metric, value, occurred_at_ms)
    Task->>DB: INSERT task_event_consumption(event_id)
    alt event_id 已存在
        DB-->>Task: duplicate
        Task-->>Source: ack
    else 新事件
        Task->>DB: 查 active task definitions
        Task->>DB: upsert user_task_progress
        Task->>DB: progress >= target 时标记 completed
        Task->>DB: task_event_consumption = consumed
        Task-->>Source: ack
    end

进度更新必须在一个数据库事务内完成:

  1. 写入 task_event_consumption
  2. 找到事件命中的 active 任务。
  3. user_id + task_id + cycle_key 锁定或创建进度行。
  4. 增加进度。
  5. 如果达到目标,标记 completed
  6. 标记事件已消费。

如果事件没有匹配任何任务,写 skipped,避免重复消费同一个无效事件。

领奖流程

sequenceDiagram
    participant App
    participant Gateway
    participant Activity as activity-service
    participant DB as activity MySQL
    participant Wallet as wallet-service

    App->>Gateway: POST /tasks/claim
    Gateway->>Activity: ClaimTaskReward
    Activity->>DB: 锁 user_task_progress
    DB-->>Activity: completed and not claimed
    Activity->>DB: 创建 task_reward_claims(pending)
    Activity->>Wallet: CreditTaskReward(wallet_command_id)
    Wallet-->>Activity: granted
    Activity->>DB: progress=claimed, claim=granted
    Activity-->>Gateway: reward granted
    Gateway-->>App: OK

钱包调用失败时:

  • task_reward_claims 保持 pendingfailed
  • user_task_progress 不应标记 claimed
  • 用户重试同一 command_id 返回当前状态。
  • 后台可以扫描 pending/failed claim 重试。

边界和补充规则

1. UTC 周期

所有 daily 周期都按 UTC 计算:

  • task_day
  • cycle_start_ms
  • cycle_end_ms
  • next_refresh_at_ms

不要使用用户手机时区、请求 IP 所在时区或服务器部署所在地时区。

2. 任务定义修改

active 任务修改目标值或奖励时,必须生成新版本。

建议规则:

  • 文案、排序可以立即生效。
  • 目标值、奖励金币、指标类型从下一个 daily cycle 生效。
  • 专属任务修改后只影响未开始用户,已完成或已领取用户不重算。

3. 延迟事件

事件按 occurred_at_ms 归属日期。

建议首版:

  • 当前 task_day 之前的迟到事件可以记录到历史进度。
  • App 只展示当前 daily 任务。
  • daily 奖励只允许在当前 task_day 领取。
  • 是否允许“昨日补领”不要默认支持,后续作为后台补偿能力单独设计。

4. 超额完成

progress_value 可以超过 target_value,便于展示真实进度,例如 120/100

但:

  • daily 超额不结转。
  • reward 仍只发一次。
  • exclusive 完成后不再重复累计领取。

5. 任务可见性

首版所有 active 任务对当前 App 全体用户可见。

后续可扩展:

  • 地区可见。
  • 新用户可见。
  • VIP 可见。
  • 指定用户可见。
  • 主播/Agency/BD 身份可见。

这些应通过 task_audience_rules 扩展,不要把复杂人群条件塞进基础任务表。

6. CP 任务解除关系

CP 任务以成功组成 CP 的事件为准。后续解除 CP 不回滚已完成进度,避免用户完成后被动丢奖励。

如果运营需要“当前仍保持 CP”应该设计状态型任务不要复用事件计数任务。

7. 上麦任务跨天

跨天麦位 session 必须拆分到不同 task_day

例如 UTC 23:50 上麦,次日 UTC 00:10 下麦:

前一天 +10 分钟
后一天 +10 分钟

8. 充值退款和拒付

充值任务按成功充值事件完成。Google/Apple 拒付、退款、风控追回不直接回滚已领任务奖励。

如果后续需要严格财务反作弊,应新增:

  • 充值奖励冻结期。
  • 领奖后拒付追扣。
  • 风控黑名单。

首版不建议把这些复杂规则放进每日任务主链路。

9. 金币奖励发放

activity-service 不能直接写钱包余额。

钱包发奖命令必须包含:

字段 说明
wallet_command_id 幂等键
user_id 领取用户
asset_type COIN
amount 奖励金币
reason daily_task_reward / exclusive_task_reward
biz_id task_id + cycle_key

10. 幂等

必须有三层幂等:

  • 来源事件:task_event_consumption(event_id)
  • 用户进度:user_task_progress(user_id, task_id, cycle_key)
  • 领奖:task_reward_claims(command_id)task_reward_claims(user_id, task_id, cycle_key)

11. 删除任务

后台不做物理删除。

状态流转:

draft -> active -> paused -> archived

archived 后 App 不展示,但历史进度、领奖记录仍可审计。

12. 任务排序

建议排序:

  1. 可领取任务。
  2. 已完成未领取任务。
  3. 进行中任务。
  4. 已领取任务。
  5. 同状态按 sort_order

13. 任务进度查询性能

App 查询任务列表时不要实时扫钱包流水或房间事件。

查询路径只读:

task_definitions + user_task_progress

没有进度行时返回 0不必提前为所有用户初始化每日任务。

14. 多 App 隔离

所有任务定义、进度、事件消费、领奖记录都必须带 app_code

任务 ID 不能跨 App 复用语义。

15. 管理端审计

后台每次创建、启停、修改任务都要写审计:

  • operator_admin_id
  • request_id
  • before_json
  • after_json
  • created_at_ms

任务配置影响发奖,必须可追溯。

推荐实现顺序

  1. Admin 菜单和权限:活动管理 一级菜单下增加 每日任务 二级菜单。
  2. activity-service 增加任务定义表、版本表、进度表、事件消费表、领奖表。
  3. Admin API每日任务/专属任务列表、新增、编辑、启停、排序。
  4. App 查询接口:返回 daily/exclusive 两个分区和用户进度。
  5. 钱包金币奖励接口:CreditTaskReward 幂等入账。
  6. 领奖接口完成校验、claim 幂等、调用钱包发奖。
  7. 接入充值、送礼、游戏、幸运礼物事件。
  8. 接入上麦时长事件或 user-service 麦时长 read model。
  9. 接入 CP 创建事件。
  10. 增加补偿任务:扫描 pending claim、failed event、消费延迟。

需要提前确认的问题

  1. 幸运礼物任务首版是“消耗金币”或“中奖金币”二选一,还是需要 AND 条件。
  2. 充值任务是否包含币商转金币、后台补偿、活动赠币;建议首版只包含真实充值和币商转普通金币。
  3. daily 任务是否允许次日补领;建议首版不允许。
  4. 专属任务是否全体用户可见,还是需要后台指定用户或人群。
  5. 游戏消耗金币的事件来源和业务类型枚举需要先统一,否则任务无法区分普通扣费和游戏扣费。