18 KiB
每日任务系统架构方案
本文定义 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_coin 或 win_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_minutes、gift_spend_coin |
| 目标值 | 是 | 必须大于 0 |
| 奖励金币 | 是 | 必须大于 0 |
| 排序值 | 是 | 小值靠前 |
| 生效/结束时间 | 否 | 控制任务上下架窗口 |
| 状态 | 是 | draft 保存草稿,active 对 App 生效 |
编辑 active 任务时建议生成新版本。每日任务的新版本默认从下一个 task_day 生效,避免当天用户看到目标值突然变化。
App 接口
查询任务列表
GET /api/v1/tasks/tabs
返回两个分区:
dailyexclusive
每个任务返回:
| 字段 | 说明 |
|---|---|
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-dd,exclusive 使用 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
进度更新必须在一个数据库事务内完成:
- 写入
task_event_consumption。 - 找到事件命中的 active 任务。
- 按
user_id + task_id + cycle_key锁定或创建进度行。 - 增加进度。
- 如果达到目标,标记
completed。 - 标记事件已消费。
如果事件没有匹配任何任务,写 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保持pending或failed。user_task_progress不应标记claimed。- 用户重试同一
command_id返回当前状态。 - 后台可以扫描
pending/failedclaim 重试。
边界和补充规则
1. UTC 周期
所有 daily 周期都按 UTC 计算:
task_daycycle_start_mscycle_end_msnext_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. 任务排序
建议排序:
- 可领取任务。
- 已完成未领取任务。
- 进行中任务。
- 已领取任务。
- 同状态按
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
任务配置影响发奖,必须可追溯。
推荐实现顺序
- Admin 菜单和权限:
活动管理一级菜单下增加每日任务二级菜单。 - activity-service 增加任务定义表、版本表、进度表、事件消费表、领奖表。
- Admin API:每日任务/专属任务列表、新增、编辑、启停、排序。
- App 查询接口:返回 daily/exclusive 两个分区和用户进度。
- 钱包金币奖励接口:
CreditTaskReward幂等入账。 - 领奖接口:完成校验、claim 幂等、调用钱包发奖。
- 接入充值、送礼、游戏、幸运礼物事件。
- 接入上麦时长事件或 user-service 麦时长 read model。
- 接入 CP 创建事件。
- 增加补偿任务:扫描 pending claim、failed event、消费延迟。
需要提前确认的问题
- 幸运礼物任务首版是“消耗金币”或“中奖金币”二选一,还是需要 AND 条件。
- 充值任务是否包含币商转金币、后台补偿、活动赠币;建议首版只包含真实充值和币商转普通金币。
- daily 任务是否允许次日补领;建议首版不允许。
- 专属任务是否全体用户可见,还是需要后台指定用户或人群。
- 游戏消耗金币的事件来源和业务类型枚举需要先统一,否则任务无法区分普通扣费和游戏扣费。