hyapp-server/docs/七日签到奖励架构.md
2026-05-20 10:22:35 +08:00

24 KiB
Raw Permalink Blame History

七日签到奖励架构

本文定义 App 七日签到奖励的产品规则、服务边界、后台管理、接口契约、数据模型和落地顺序。七日签到属于活动事实,签到状态和发奖状态由 activity-service 拥有;奖励内容统一引用 wallet-service 资源组,不能把资源明细复制进签到配置。

目标

  • 后台 活动管理 下新增二级菜单 七日签到
  • 后台可以配置 1 到 7 天的奖励资源组,每天必须选择一个 resource_group_id
  • 后台可以打开或关闭七日签到;关闭后 App 只读状态,不允许新签到。
  • App 提供七日签到面板查询接口和签到接口。
  • 每个用户按 UTC 自然日每日最多签到一次。
  • 连续签到按 7 天为一轮:第 7 天签到后,下一次签到开启新一轮第 1 天。
  • 中断连续签到后下一次签到从第 1 天重新开始。
  • 签到奖励发放必须通过 wallet-service.GrantResourceGroup,资源组中可以包含金币、钻石和装扮权益。
  • 签到事实、奖励发放状态和幂等命令必须可审计、可恢复、可重试。

非目标

  • 首版不做补签、签到卡、广告补领或客户端本地时区签到。
  • 首版不支持多套并行签到活动;每个 app_code 同一时间只有一套七日签到配置。
  • 首版不做人群定向、地区定向、新老用户差异奖励。
  • 首版不让客户端上报连续天数、奖励内容、资源组明细或服务端时间。
  • 首版不把七日签到建成每日任务。每日任务依赖事实事件进度,签到是用户主动命令和每日幂等发奖。
  • 首版不把签到奖励拆成金币字段;所有奖励都只表达为资源组。

服务边界

模块 责任 不负责
gateway-service App HTTP 入口、鉴权、业务 envelope、生成并透传 request_id、调用 activity gRPC、按需向 wallet 查询资源组展示信息 不判断连续天数,不写签到表,不发资源组
activity-service 七日签到配置、配置版本、用户连续签到状态、签到 claim、发奖幂等、失败重试状态 不拥有资源组内容,不直接修改钱包余额或用户权益
wallet-service 资源组 catalog、资源组有效性校验、GrantResourceGroup 原子展开资源和钱包资产、钱包/资源 outbox 不判断用户今天能否签到,不保存签到连续天数
server/admin 后台菜单、权限、后台 HTTP API、操作审计、调用 activity 管理 RPC并复用资源组选择能力 不直接写 activity/wallet 业务库,不复制 app 服务内部包
Admin 前端 活动管理 > 七日签到 页面、开关、7 行资源组选择、保存发布、领取记录列表 不自行拼资源组明细,不绕过后台权限
notice-service 后续如需私有通知,从 activity outbox 消费签到奖励事实 不参与签到主链路

核心原则:

  • activity-service 是七日签到状态 owner。
  • 资源组内容永远以 wallet-service 为准;签到配置只保存 resource_group_id 和发放时必要快照。
  • 发奖是房间外副作用,必须通过幂等命令调用 wallet-service。
  • 所有日期、周期、刷新时间使用 UTC不读取服务器本地时区、用户手机时区或 IP 推断时区。

总体架构

flowchart LR
  AdminFE["admin 前端\n活动管理 > 七日签到"] --> AdminServer["server/admin\n七日签到后台 API"]
  AdminServer --> AdminCheckIn["activity-service\nAdminSevenDayCheckInService"]
  AdminServer --> WalletRead["wallet-service\n资源组查询"]

  App["App 七日签到页"] --> Gateway["gateway-service\n/api/v1"]
  Gateway --> CheckInSvc["activity-service\nSevenDayCheckInService"]
  Gateway --> WalletGroup["wallet-service\n资源组展示查询"]

  CheckInSvc --> ActivityDB[("hyapp_activity\ncheckin tables")]
  CheckInSvc --> WalletGrant["wallet-service\nGrantResourceGroup"]
  WalletGrant --> WalletDB[("hyapp_wallet\nresource grants / wallet ledger")]
  CheckInSvc --> ActivityOutbox[("activity_outbox\noptional reward event")]

产品规则

签到周期

签到日使用 UTC 日期字符串 yyyy-mm-dd

场景 结果
用户从未签到 今天签到为第 1 天
上次签到日是今天 返回已签到,不重复发奖
上次签到日是昨天,且上次是第 1 到第 6 天 今天签到为下一天
上次签到日是昨天,且上次是第 7 天 今天开启新一轮第 1 天
上次签到日早于昨天 连续中断,今天重新从第 1 天开始

第 7 天之后不保持在第 7 天;下一次有效签到必须进入新一轮第 1 天,避免一个用户无限领取第 7 天奖励。

配置版本

后台每次保存 7 天奖励或开关都生成新的 config_version

  • App 查询总是展示当前配置版本。
  • 用户开始一轮签到时绑定当时的 config_version
  • 同一轮后续签到继续使用该轮绑定版本,避免运营改配置后用户第 3 天突然看到另一套奖励。
  • 第 7 天完成后下一轮读取最新 enabled 配置版本。
  • 如果旧版本对应资源组后来被 wallet 下架,发奖时以 wallet-service 校验结果为准,失败 claim 进入可重试/待处理状态。

配置版本不是历史兼容负担,而是发奖审计和用户体验一致性的最小事实。

开关

enabled=false 时:

  • App 查询返回当前配置和 can_sign=false, reason=disabled
  • 新的签到命令被拒绝。
  • 已经创建的 pending/failed claim 仍允许后台或服务端补偿重试,避免开关关闭导致已接受签到无法收尾。
  • 后台可以继续编辑 7 天奖励并保存新版本。

奖励

  • 每天奖励只允许一个资源组 ID。
  • 资源组必须由后台从资源组列表选择;不允许手写资源 ID、金币数量、钻石数量或有效期。
  • 资源组内容展示由 wallet-service 查询并返回activity-service 只保存 ID 和发放时返回的 grant_id
  • 发放时调用 wallet-service.GrantResourceGroupgrant_source=seven_day_checkinreason=seven_day_checkin
  • wallet-service 在发放事务内重新校验资源组 active、资源 active、钱包资产合法gateway 和 admin 的前端筛选不能替代 owner 校验。

后台管理

后台菜单:

活动管理
  - 每日任务
  - 注册奖励
  - 成就配置
  - 七日签到

建议新增权限:

Permission Kind Usage
seven-day-checkin:view menu 查看七日签到配置和领取记录
seven-day-checkin:update button 更新开关和 7 天奖励资源组

建议菜单:

字段
title 七日签到
code seven-day-checkin
path /activities/seven-day-checkin
icon event_available
permission_code seven-day-checkin:view
sort 72

页面结构

页面保留两个 Tab

Tab 内容
配置 顶部开关、7 行奖励配置、当前版本、更新时间、保存按钮
领取记录 用户、签到日、第几天、轮次、配置版本、资源组、状态、wallet grant id、失败原因

配置 Tab 字段:

字段 必填 说明
开关 保存到 enabled
第 1 天资源组 resource_group_id > 0
第 2 天资源组 resource_group_id > 0
第 3 天资源组 resource_group_id > 0
第 4 天资源组 resource_group_id > 0
第 5 天资源组 resource_group_id > 0
第 6 天资源组 resource_group_id > 0
第 7 天资源组 resource_group_id > 0

领取记录筛选:

字段 说明
keyword 用户 ID、展示 ID 或昵称,由 server/admin 解析成 user_id
status pending / granted / failed
checkin_day UTC 日期
day_index 1 到 7

后台 API

server/admin 对前端暴露:

GET /api/v1/admin/activity/seven-day-checkin/config
PUT /api/v1/admin/activity/seven-day-checkin/config
GET /api/v1/admin/activity/seven-day-checkin/claims

更新配置请求:

{
  "enabled": true,
  "rewards": [
    {"day_index": 1, "resource_group_id": 101},
    {"day_index": 2, "resource_group_id": 102},
    {"day_index": 3, "resource_group_id": 103},
    {"day_index": 4, "resource_group_id": 104},
    {"day_index": 5, "resource_group_id": 105},
    {"day_index": 6, "resource_group_id": 106},
    {"day_index": 7, "resource_group_id": 107}
  ]
}

规则:

  • rewards 必须刚好包含 1 到 7 天,不能缺失、重复或越界。
  • resource_group_id 必须大于 0。
  • Admin 前端资源组选择器复用现有资源组列表能力,只展示 active 资源组。
  • server/admin 必须写操作审计,记录 before_jsonafter_json、操作人、request_id。
  • activity-service 保存配置时只保存资源组 ID资源组是否 active 由 wallet-service 在选择展示和最终发放时确认。

App 接口

App 只通过 gateway HTTP 访问:

GET  /api/v1/activities/seven-day-checkin
POST /api/v1/activities/seven-day-checkin/sign

查询签到面板

GET /api/v1/activities/seven-day-checkin
Authorization: Bearer <access_token>

响应 data

{
  "enabled": true,
  "can_sign": true,
  "reason": "eligible",
  "cycle_no": 3,
  "current_day_index": 2,
  "next_day_index": 3,
  "checkin_day": "2026-05-16",
  "last_checkin_day": "2026-05-15",
  "config_version": 8,
  "server_time_ms": 1778889600000,
  "next_refresh_at_ms": 1778976000000,
  "rewards": [
    {
      "day_index": 1,
      "resource_group_id": 101,
      "status": "granted",
      "resource_group": {}
    },
    {
      "day_index": 2,
      "resource_group_id": 102,
      "status": "granted",
      "resource_group": {}
    },
    {
      "day_index": 3,
      "resource_group_id": 103,
      "status": "available",
      "resource_group": {}
    }
  ]
}

resource_group 由 gateway 按 resource_group_id 调 wallet-service 查询后嵌入,结构沿用现有资源组 HTTP DTO。activity-service gRPC 只需要返回资源组 ID 和用户签到状态。

current_day_index 表示本轮最近已完成的天数,从未签到时返回 0next_day_index 表示今天如果签到会领取第几天奖励。

reason 建议枚举:

reason 含义
eligible 今天可签到
disabled 活动关闭
not_configured 未配置完整 7 天奖励
already_signed 今天已签到
pending_reward 今天签到已接受,奖励仍在处理中
invalid_config 配置不可发放

执行签到

POST /api/v1/activities/seven-day-checkin/sign
Authorization: Bearer <access_token>

请求:

{
  "command_id": "cmd_checkin_550e8400"
}

规则:

  • command_id 由客户端按本次点击动作生成,同一次重试必须复用。
  • gateway 不补 command_id,只裁剪空白并透传当前用户 ID。
  • activity-service 决定今天是第几天、是否已签到、应发哪个资源组。
  • 同一用户同一 UTC 日期最多一个签到 claim。
  • 同一 command_id 重试且 payload 一致,返回同一 claim 结果。

响应 data

{
  "claim_id": "checkin_claim_123",
  "cycle_no": 3,
  "checkin_day": "2026-05-16",
  "day_index": 3,
  "resource_group_id": 103,
  "status": "granted",
  "wallet_grant_id": "resource_grant_456",
  "granted_at_ms": 1778918500000,
  "server_time_ms": 1778918500000
}

内部 gRPC 契约

建议在 api/proto/activity/v1/activity.proto 增加:

message SevenDayCheckInReward {
  int32 day_index = 1;
  int64 resource_group_id = 2;
}

message SevenDayCheckInConfig {
  string app_code = 1;
  bool enabled = 2;
  int64 config_version = 3;
  repeated SevenDayCheckInReward rewards = 4;
  int64 updated_by_admin_id = 5;
  int64 created_at_ms = 6;
  int64 updated_at_ms = 7;
}

message SevenDayCheckInRewardStatus {
  int32 day_index = 1;
  int64 resource_group_id = 2;
  string status = 3;
  string claim_id = 4;
  string wallet_grant_id = 5;
  int64 granted_at_ms = 6;
}

message GetSevenDayCheckInStatusRequest {
  RequestMeta meta = 1;
  int64 user_id = 2;
}

message GetSevenDayCheckInStatusResponse {
  bool enabled = 1;
  bool can_sign = 2;
  string reason = 3;
  int64 cycle_no = 4;
  int32 current_day_index = 5;
  int32 next_day_index = 6;
  string checkin_day = 7;
  string last_checkin_day = 8;
  int64 config_version = 9;
  repeated SevenDayCheckInRewardStatus rewards = 10;
  int64 server_time_ms = 11;
  int64 next_refresh_at_ms = 12;
}

message SignSevenDayCheckInRequest {
  RequestMeta meta = 1;
  int64 user_id = 2;
  string command_id = 3;
}

message SignSevenDayCheckInResponse {
  string claim_id = 1;
  int64 cycle_no = 2;
  string checkin_day = 3;
  int32 day_index = 4;
  int64 resource_group_id = 5;
  string status = 6;
  string wallet_grant_id = 7;
  int64 granted_at_ms = 8;
  int64 server_time_ms = 9;
}

service SevenDayCheckInService {
  rpc GetSevenDayCheckInStatus(GetSevenDayCheckInStatusRequest) returns (GetSevenDayCheckInStatusResponse);
  rpc SignSevenDayCheckIn(SignSevenDayCheckInRequest) returns (SignSevenDayCheckInResponse);
}

后台管理 RPC

message GetSevenDayCheckInConfigRequest {
  RequestMeta meta = 1;
}

message GetSevenDayCheckInConfigResponse {
  SevenDayCheckInConfig config = 1;
}

message UpdateSevenDayCheckInConfigRequest {
  RequestMeta meta = 1;
  bool enabled = 2;
  repeated SevenDayCheckInReward rewards = 3;
  int64 operator_admin_id = 4;
}

message UpdateSevenDayCheckInConfigResponse {
  SevenDayCheckInConfig config = 1;
}

message SevenDayCheckInClaim {
  string claim_id = 1;
  string command_id = 2;
  int64 user_id = 3;
  int64 cycle_no = 4;
  string checkin_day = 5;
  int32 day_index = 6;
  int64 config_version = 7;
  int64 resource_group_id = 8;
  string wallet_command_id = 9;
  string wallet_grant_id = 10;
  string status = 11;
  string failure_reason = 12;
  int64 signed_at_ms = 13;
  int64 granted_at_ms = 14;
  int64 created_at_ms = 15;
  int64 updated_at_ms = 16;
}

message ListSevenDayCheckInClaimsRequest {
  RequestMeta meta = 1;
  string status = 2;
  int64 user_id = 3;
  string checkin_day = 4;
  int32 day_index = 5;
  int32 page = 6;
  int32 page_size = 7;
}

message ListSevenDayCheckInClaimsResponse {
  repeated SevenDayCheckInClaim claims = 1;
  int64 total = 2;
}

service AdminSevenDayCheckInService {
  rpc GetSevenDayCheckInConfig(GetSevenDayCheckInConfigRequest) returns (GetSevenDayCheckInConfigResponse);
  rpc UpdateSevenDayCheckInConfig(UpdateSevenDayCheckInConfigRequest) returns (UpdateSevenDayCheckInConfigResponse);
  rpc ListSevenDayCheckInClaims(ListSevenDayCheckInClaimsRequest) returns (ListSevenDayCheckInClaimsResponse);
}

修改 proto 后必须运行:

make proto
go test ./...

数据模型

所有表位于 hyapp_activity,字段使用 UTC epoch milliseconds 和 UTC 日期字符串。

seven_day_checkin_configs

当前配置入口。

字段 说明
app_code App 租户,主键
enabled 活动开关
config_version 当前配置版本
updated_by_admin_id 最近操作人
created_at_ms 创建时间
updated_at_ms 更新时间

seven_day_checkin_rewards

当前版本 7 天奖励读模型。

字段 说明
app_code App 租户
day_index 1 到 7
config_version 当前版本
resource_group_id wallet-service 资源组 ID
created_at_ms 创建时间
updated_at_ms 更新时间

唯一键:

PRIMARY KEY(app_code, day_index)

seven_day_checkin_config_versions

配置版本快照。

字段 说明
app_code App 租户
config_version 版本号
enabled 当时开关
snapshot_json 7 天奖励资源组快照
created_by_admin_id 操作人
created_at_ms 创建时间

唯一键:

PRIMARY KEY(app_code, config_version)

user_seven_day_checkin_accounts

用户连续签到账户。

字段 说明
app_code App 租户
user_id 用户 ID
cycle_no 当前轮次,从 1 开始
cycle_start_day 本轮第 1 天 UTC 日期
last_checkin_day 最近成功签到 UTC 日期
last_day_index 最近成功签到是第几天
config_version 本轮绑定配置版本
updated_at_ms 更新时间

唯一键:

PRIMARY KEY(app_code, user_id)

seven_day_checkin_claims

每次签到和资源组发放事实。

字段 说明
claim_id 主键
command_id 客户端签到动作幂等键
app_code App 租户
user_id 用户 ID
cycle_no 轮次
checkin_day UTC 签到日
day_index 1 到 7
config_version 发奖使用的配置版本
resource_group_id 发奖资源组快照
wallet_command_id 调用 wallet-service 的幂等键
wallet_grant_id wallet-service 返回的资源组 grant ID
status pending / granted / failed
failure_reason 失败原因
signed_at_ms 签到命令接受时间
granted_at_ms 奖励发放完成时间
created_at_ms 创建时间
updated_at_ms 更新时间

唯一键:

PRIMARY KEY(app_code, claim_id)
UNIQUE(app_code, command_id)
UNIQUE(app_code, user_id, checkin_day)
UNIQUE(app_code, user_id, cycle_no, day_index)

UNIQUE(app_code, user_id, checkin_day) 保证自然日最多一次;UNIQUE(app_code, user_id, cycle_no, day_index) 保证同一轮同一天位不会被并发重复发放。

签到流程

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

  App->>Gateway: POST /activities/seven-day-checkin/sign(command_id)
  Gateway->>Activity: SignSevenDayCheckIn(user_id, command_id)
  Activity->>DB: 锁 config + user account计算 checkin_day/day_index
  Activity->>DB: INSERT claim pending(wallet_command_id)
  Activity->>Wallet: GrantResourceGroup(wallet_command_id, resource_group_id)
  Wallet-->>Activity: grant_id
  Activity->>DB: 锁 claim + account更新 account 和 claim=granted
  Activity-->>Gateway: granted
  Gateway-->>App: OK

关键事务边界:

  1. Prepare 事务只创建或复用 pending claim不更新连续签到账户。
  2. wallet-service 发放资源组使用稳定 wallet_command_id,重复调用必须幂等。
  3. Finalize 事务在 wallet 成功后更新 user_seven_day_checkin_accounts 和 claim 状态。
  4. 如果 wallet 成功但 finalize 失败,用户重试同一 command_id 时 wallet 返回幂等结果activity 再完成 finalize。
  5. 如果 wallet 失败claim 标记 failed,账户不前进;同一 command_id 重试应重新尝试发放该 claim后台补偿也只能重试同一个 claim。

这样做的原因是签到连续天数必须和奖励成功保持一致。不能出现 activity 已经把用户推进到第 3 天,但第 3 天资源组没有发出的状态。

查询流程

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

  App->>Gateway: GET /activities/seven-day-checkin
  Gateway->>Activity: GetSevenDayCheckInStatus(user_id)
  Activity-->>Gateway: config ids + user status
  Gateway->>Wallet: GetResourceGroup for visible reward ids
  Wallet-->>Gateway: resource group DTOs
  Gateway-->>App: envelope(data)

查询只读 activity 配置、用户账户和最近 claims不实时扫描钱包流水。资源组展示信息由 gateway 使用 wallet 查询结果补齐。

幂等和并发

  • command_id 是客户端点击动作幂等键。
  • wallet_command_id 建议格式:seven_day_checkin:{claim_id}
  • 同一 command_id 重试返回同一 claim。
  • 同一用户同一 checkin_day 并发请求只能创建一个 claim。
  • 同一用户同一 cycle_no + day_index 只能有一个 granted/pending claim。
  • claim payload 冲突时返回 CONFLICT,不能静默重写历史 claim。
  • 查询接口不产生签到事实。

时间规则

  • checkin_day 按 UTC 日期计算。
  • next_refresh_at_ms 是下一天 UTC 00:00:00.000 的 epoch milliseconds。
  • 所有业务时间字段使用 *_ms
  • 领取记录筛选的日期字段是 UTC 日期,不接受本地时区字符串。
  • MySQL DSN、Docker TZ 和数据库默认时区继续保持 UTC。

错误和状态

状态或错误 场景 处理
disabled 活动关闭 查询可见,签到拒绝
not_configured 未配置完整 7 天奖励 查询提示不可签,签到拒绝
already_signed 当天已有 granted/pending claim 返回已有状态
pending claim 已创建,奖励未完成 可重试或后台补偿
failed wallet 发放失败或配置不可发放 记录原因,账户不前进
CONFLICT 同一 command_id payload 不一致 返回冲突
INVALID_ARGUMENT command_id 空、day_index 越界、资源组 ID 无效 返回参数错误

Outbox 和通知

签到发奖成功后activity-service 可以写 activity_outbox

Event Payload
SevenDayCheckInRewardGranted claim_iduser_idcycle_nocheckin_dayday_indexresource_group_idwallet_grant_id
SevenDayCheckInRewardFailed claim_iduser_idcheckin_dayfailure_reason

首版不要求实时私信通知。后续如果产品需要奖励到账通知,由 notice-service 消费 activity outbox 并投递,不让 activity-service 直接耦合私有通知实现。

实现顺序

  1. 文档和契约:确认本架构后修改 api/proto/activity/v1/activity.proto,运行 make proto
  2. activity-service增加 domain/service/repository/gRPC server 和 MySQL initdb 表。
  3. wallet 集成:复用 GrantResourceGroup,补齐七日签到发放 reason/source 和测试。
  4. gateway-service增加签到查询和签到 HTTP接入 activity client并按资源组 ID 补齐展示 DTO。
  5. server/admin增加后台模块、路由、权限、菜单 migration、审计和 activity 管理 client。
  6. Admin 前端:增加 /activities/seven-day-checkin 页面,复用资源组选择器。
  7. 补偿:增加 pending/failed claim 重试批处理;由 cron-service 触发 activity-service owner 批处理。
  8. 验证:补 UTC 边界、并发幂等、wallet 失败、配置版本切换和后台权限测试。

测试要求

至少覆盖:

场景 验证
UTC 当天首次签到 创建 claim发资源组账户推进到第 1 天
同一天重复签到 不重复发奖,返回已有 claim
连续 7 天签到 day_index 从 1 到 7下一天回到第 1 天并 cycle_no + 1
中断一天后签到 day_index 重置为 1新 cycle_no
UTC 边界 23:59:59.99900:00:00.000 分属不同 checkin_day
开关关闭 查询可见但不能签到
配置缺失 不能打开或不能签到
修改配置版本 已开始轮次继续旧版本,新轮次使用新版本
wallet GrantResourceGroup 失败 claim failed账户不前进可重试
finalize 失败后重试 wallet 幂等返回后 activity 完成 finalize
admin 权限 seven-day-checkin:update 不能保存配置

涉及 proto 和代码实现后运行:

make proto
make test
go test ./...

涉及后台菜单 migration 或配置时额外运行:

docker compose config

待确认产品项

  1. 是否需要补签。当前架构明确首版不支持。
  2. 第 7 天之后是否循环回第 1 天。当前架构按循环处理。
  3. 中断后是否重置到第 1 天。当前架构按重置处理。
  4. 是否需要领取记录导出。当前只设计分页查询。
  5. 是否需要发奖成功后的 App 活动消息。当前只预留 outbox不默认投递。