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

706 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 七日签到奖励架构
本文定义 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 推断时区。
## 总体架构
```mermaid
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.GrantResourceGroup``grant_source=seven_day_checkin``reason=seven_day_checkin`
- wallet-service 在发放事务内重新校验资源组 active、资源 active、钱包资产合法gateway 和 admin 的前端筛选不能替代 owner 校验。
## 后台管理
后台菜单:
```text
活动管理
- 每日任务
- 注册奖励
- 成就配置
- 七日签到
```
建议新增权限:
| 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` 对前端暴露:
```http
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
```
更新配置请求:
```json
{
"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_json``after_json`、操作人、request_id。
- `activity-service` 保存配置时只保存资源组 ID资源组是否 active 由 wallet-service 在选择展示和最终发放时确认。
## App 接口
App 只通过 gateway HTTP 访问:
```http
GET /api/v1/activities/seven-day-checkin
POST /api/v1/activities/seven-day-checkin/sign
```
### 查询签到面板
```http
GET /api/v1/activities/seven-day-checkin
Authorization: Bearer <access_token>
```
响应 data
```json
{
"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` 表示本轮最近已完成的天数,从未签到时返回 `0``next_day_index` 表示今天如果签到会领取第几天奖励。
`reason` 建议枚举:
| reason | 含义 |
| --- | --- |
| `eligible` | 今天可签到 |
| `disabled` | 活动关闭 |
| `not_configured` | 未配置完整 7 天奖励 |
| `already_signed` | 今天已签到 |
| `pending_reward` | 今天签到已接受,奖励仍在处理中 |
| `invalid_config` | 配置不可发放 |
### 执行签到
```http
POST /api/v1/activities/seven-day-checkin/sign
Authorization: Bearer <access_token>
```
请求:
```json
{
"command_id": "cmd_checkin_550e8400"
}
```
规则:
- `command_id` 由客户端按本次点击动作生成,同一次重试必须复用。
- gateway 不补 `command_id`,只裁剪空白并透传当前用户 ID。
- activity-service 决定今天是第几天、是否已签到、应发哪个资源组。
- 同一用户同一 UTC 日期最多一个签到 claim。
- 同一 `command_id` 重试且 payload 一致,返回同一 claim 结果。
响应 data
```json
{
"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` 增加:
```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
```proto
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 后必须运行:
```bash
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` | 更新时间 |
唯一键:
```text
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` | 创建时间 |
唯一键:
```text
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` | 更新时间 |
唯一键:
```text
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` | 更新时间 |
唯一键:
```text
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)` 保证同一轮同一天位不会被并发重复发放。
## 签到流程
```mermaid
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 天资源组没有发出的状态。
## 查询流程
```mermaid
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_id``user_id``cycle_no``checkin_day``day_index``resource_group_id``wallet_grant_id` |
| `SevenDayCheckInRewardFailed` | `claim_id``user_id``checkin_day``failure_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.999``00:00:00.000` 分属不同 checkin_day |
| 开关关闭 | 查询可见但不能签到 |
| 配置缺失 | 不能打开或不能签到 |
| 修改配置版本 | 已开始轮次继续旧版本,新轮次使用新版本 |
| wallet GrantResourceGroup 失败 | claim failed账户不前进可重试 |
| finalize 失败后重试 | wallet 幂等返回后 activity 完成 finalize |
| admin 权限 | 无 `seven-day-checkin:update` 不能保存配置 |
涉及 proto 和代码实现后运行:
```bash
make proto
make test
go test ./...
```
涉及后台菜单 migration 或配置时额外运行:
```bash
docker compose config
```
## 待确认产品项
1. 是否需要补签。当前架构明确首版不支持。
2. 第 7 天之后是否循环回第 1 天。当前架构按循环处理。
3. 中断后是否重置到第 1 天。当前架构按重置处理。
4. 是否需要领取记录导出。当前只设计分页查询。
5. 是否需要发奖成功后的 App 活动消息。当前只预留 outbox不默认投递。