vip相关
This commit is contained in:
parent
f89aa37318
commit
b4655c1326
@ -18288,7 +18288,7 @@ func (x *VipTrialCard) GetUpdatedAtMs() int64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet
|
// VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet
|
||||||
// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
// 默认开启进房/上线通知、关闭三项隐私能力,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||||
type VipUserSettings struct {
|
type VipUserSettings struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
|
||||||
|
|||||||
@ -2119,7 +2119,7 @@ message VipTrialCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet
|
// VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet
|
||||||
// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
// 默认开启进房/上线通知、关闭三项隐私能力,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||||
message VipUserSettings {
|
message VipUserSettings {
|
||||||
string app_code = 1;
|
string app_code = 1;
|
||||||
int64 user_id = 2;
|
int64 user_id = 2;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
## 0. 当前稳定契约
|
## 0. 当前稳定契约
|
||||||
|
|
||||||
- `GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡、卸下体验卡的 `state` 完全同构,固定包含 `paid_vip`、`equipped_trial_card`、`effective_vip`、`effective_source`、`effective_benefits`、`evaluated_at_ms`、完整 `program_config`、完整 `user_settings`。写成功后整体替换 store,禁止局部合并。
|
- `GET /vip/me`、`GET /vip/packages`、购买、佩戴体验卡、卸下体验卡的 `state` 完全同构,固定包含 `paid_vip`、`equipped_trial_card`、`effective_vip`、`effective_source`、`effective_benefits`、`evaluated_at_ms`、完整 `program_config`、完整 `user_settings`。写成功后整体替换 store,禁止局部合并。
|
||||||
- `user_settings` 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离、无记录默认开启。
|
- `user_settings` 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离,无记录时前两项通知默认开启、后三项隐私能力默认关闭。
|
||||||
- 权益富展示读取结构化 `benefits[].presentation`。它覆盖说明文案、多项静态/动态预览、用户头像合成方式和数值奖励;Flutter 不解析 `metadata_json` 生成 UI。
|
- 权益富展示读取结构化 `benefits[].presentation`。它覆盖说明文案、多项静态/动态预览、用户头像合成方式和数值奖励;Flutter 不解析 `metadata_json` 生成 UI。
|
||||||
- VIP 购买后的余额读取带 `version` 的 `coin_balance`;`coin_balance_after` 仅兼容旧客户端。历史幂等回放若 `version=0`,必须重新请求钱包余额。
|
- VIP 购买后的余额读取带 `version` 的 `coin_balance`;`coin_balance_after` 仅兼容旧客户端。历史幂等回放若 `version=0`,必须重新请求钱包余额。
|
||||||
- 业务分支只判断稳定 `code`,不得解析 `message`。精确 JSON、`presentation` Schema 和错误码矩阵以 [新版 VIP 权限策略 Flutter 客户端对接文档](./新版VIP权限策略_Flutter客户端对接文档.md) 为唯一协议定义;本文后续较短 JSON 只解释场景,不定义裁剪响应。
|
- 业务分支只判断稳定 `code`,不得解析 `message`。精确 JSON、`presentation` Schema 和错误码矩阵以 [新版 VIP 权限策略 Flutter 客户端对接文档](./新版VIP权限策略_Flutter客户端对接文档.md) 为唯一协议定义;本文后续较短 JSON 只解释场景,不定义裁剪响应。
|
||||||
@ -340,9 +340,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -468,9 +468,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -492,7 +492,7 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
|||||||
- 当体验卡被佩戴时,不限制卡等级高低,它会覆盖付费 VIP 成为 `effective_vip`;卸下或过期后自然回落到仍有效的 `paid_vip`。
|
- 当体验卡被佩戴时,不限制卡等级高低,它会覆盖付费 VIP 成为 `effective_vip`;卸下或过期后自然回落到仍有效的 `paid_vip`。
|
||||||
- 体验卡状态下,`daily_coin_rebate` 不会进入 `effective_benefits`。Flutter 不要只看卡等级猜测权益。
|
- 体验卡状态下,`daily_coin_rebate` 不会进入 `effective_benefits`。Flutter 不要只看卡等级猜测权益。
|
||||||
- 无 VIP 时,`effective_source="none"`、`effective_vip.active=false`、`effective_benefits=[]`。
|
- 无 VIP 时,`effective_source="none"`、`effective_vip.active=false`、`effective_benefits=[]`。
|
||||||
- `user_settings.updated_at_ms=0` 表示用户尚未修改,5 个开关使用默认开启值;设置在无 VIP 时也保留,但不会单独授权。
|
- `user_settings.updated_at_ms=0` 表示用户尚未修改;进房/上线通知默认为 `true`,隐藏个人数据/匿名访问/榜单隐身默认为 `false`。设置在无 VIP 时也保留,但不会单独授权。
|
||||||
|
|
||||||
## 8. 购买、续期和升级
|
## 8. 购买、续期和升级
|
||||||
|
|
||||||
@ -580,9 +580,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -784,9 +784,9 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -884,9 +884,9 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1070,6 +1070,8 @@ Fami(`trial_card`)发一张 30 天体验卡到背包,不自动佩戴:
|
|||||||
|
|
||||||
`GET /api/v1/vip/me` 的 `data.state.user_settings` 与 GET 接口是同一份事实。VIP 页面已经拉取 `/vip/me` 时,不必再为展示开关多发一次请求。
|
`GET /api/v1/vip/me` 的 `data.state.user_settings` 与 GET 接口是同一份事实。VIP 页面已经拉取 `/vip/me` 时,不必再为展示开关多发一次请求。
|
||||||
|
|
||||||
|
`GET /api/v1/users/me/overview` 也在 `data.vip.user_settings` 返回同一份完整设置,它复用聚合接口已有的 `GetMyVip` 回包,不会增加 wallet RPC。该字段只属于当前用户私有信息,查看他人的公开 profile 不返回。
|
||||||
|
|
||||||
### 参数
|
### 参数
|
||||||
|
|
||||||
GET 无参数。PATCH 是局部更新,5 个字段至少传一个;显式 `false` 不能被当成未传:
|
GET 无参数。PATCH 是局部更新,5 个字段至少传一个;显式 `false` 不能被当成未传:
|
||||||
@ -1134,7 +1136,7 @@ PATCH:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
用户从未修改过时,5 项均默认为 `true`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖其他项。
|
用户从未修改过时,进房/上线通知默认为 `true`,隐藏个人数据/匿名访问/榜单隐身默认为 `false`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖其他项。
|
||||||
|
|
||||||
### 相关 IM
|
### 相关 IM
|
||||||
|
|
||||||
@ -1426,7 +1428,7 @@ Flutter 先把 `action_param` 再解析为 JSON。点击前可将页面上所有
|
|||||||
| 同级续期 | 不在本地计算,以响应为准 | 已强制,从旧截止时间累加 |
|
| 同级续期 | 不在本地计算,以响应为准 | 已强制,从旧截止时间累加 |
|
||||||
| 体验卡自由切换 | 按 `entitlement_id` 佩戴,每次直接替换全局 `state` | 已强制,不限等级,不作废旧卡,不改绝对截止时间 |
|
| 体验卡自由切换 | 按 `entitlement_id` 佩戴,每次直接替换全局 `state` | 已强制,不限等级,不作废旧卡,不改绝对截止时间 |
|
||||||
| 体验卡权益 | 只渲染 `effective_benefits` 资格 | 已强制过滤 `trial_enabled=false`,金币返现不给体验卡 |
|
| 体验卡权益 | 只渲染 `effective_benefits` 资格 | 已强制过滤 `trial_enabled=false`,金币返现不给体验卡 |
|
||||||
| VIP 功能开关 | 读 `state.user_settings`,用 PATCH 局部修改 | 已按 App 持久化,无记录默认 5 项开启,开关不单独授权 |
|
| VIP 功能开关 | 读 `state.user_settings`,用 PATCH 局部修改 | 已按 App 持久化,无记录时通知开、隐私关,开关不单独授权 |
|
||||||
| 进房高亮通知 | 本人首屏读 Join HTTP `effective_vip`,其他成员消费当前房间 `room_user_joined` | 后端已同时校验权益与用户开关,并透传 Join HTTP 和当前房间 IM;不发全服 |
|
| 进房高亮通知 | 本人首屏读 Join HTTP `effective_vip`,其他成员消费当前房间 `room_user_joined` | 后端已同时校验权益与用户开关,并透传 Join HTTP 和当前房间 IM;不发全服 |
|
||||||
| 防踢 | 房管操作返回 `VIP_ANTI_KICK` 时显示不可踢,不做本地预判 | 已在 room-service 强制;只阻止普通房主/管理员的 KickUser |
|
| 防踢 | 房管操作返回 `VIP_ANTI_KICK` 时显示不可踢,不做本地预判 | 已在 room-service 强制;只阻止普通房主/管理员的 KickUser |
|
||||||
| 防禁言 | 房管操作返回 `VIP_ANTI_MUTE` 时显示不可禁言,解禁始终可发起 | 已在 room-service 强制;只阻止普通房主/管理员新增禁言 |
|
| 防禁言 | 房管操作返回 `VIP_ANTI_MUTE` 时显示不可禁言,解禁始终可发起 | 已在 room-service 强制;只阻止普通房主/管理员新增禁言 |
|
||||||
|
|||||||
@ -93,7 +93,7 @@ Fami 的 VIP1–VIP9 初始全部为 `disabled`。30 天只是不产生零时长
|
|||||||
- `anonymous_profile_visit_enabled`:匿名访问主页;
|
- `anonymous_profile_visit_enabled`:匿名访问主页;
|
||||||
- `leaderboard_invisible_enabled`:榜单隐身。
|
- `leaderboard_invisible_enabled`:榜单隐身。
|
||||||
|
|
||||||
无记录时 5 项默认为 `true`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好;Fami 与 Lalu 互不影响。
|
无记录时,进房/上线通知默认为 `true`,隐藏个人数据/匿名访问/榜单隐身默认为 `false`,首次修改才写行。偏好不授予权益:无 VIP 或对应等级没有权益时,即使开关为开也不会放行。用户可在无 VIP、VIP 过期或切换体验卡时保留偏好;Fami 与 Lalu 互不影响。
|
||||||
|
|
||||||
### 权益展示投影
|
### 权益展示投影
|
||||||
|
|
||||||
|
|||||||
@ -282,9 +282,9 @@ Authorization: Bearer {access_token}
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -191,6 +191,7 @@ Flutter 建议放在 body 里,避免调用层漏拼 query。
|
|||||||
| `profiles[].avatar_frame` | 头像框后置拉。 |
|
| `profiles[].avatar_frame` | 头像框后置拉。 |
|
||||||
| `profiles[].profile_card` | 资料卡后置拉。 |
|
| `profiles[].profile_card` | 资料卡后置拉。 |
|
||||||
| `profiles[].vehicle` | 座驾后置拉。 |
|
| `profiles[].vehicle` | 座驾后置拉。 |
|
||||||
|
| `profiles[].chat_bubble` | 聊天气泡后置拉。 |
|
||||||
| `profiles[].mic_seat_animation` | 麦位动画后置拉。 |
|
| `profiles[].mic_seat_animation` | 麦位动画后置拉。 |
|
||||||
| `profiles[].badges` | 等级/成就徽章后置拉。 |
|
| `profiles[].badges` | 等级/成就徽章后置拉。 |
|
||||||
| `profiles[].vip` | VIP 展示后置拉。 |
|
| `profiles[].vip` | VIP 展示后置拉。 |
|
||||||
@ -249,6 +250,7 @@ Authorization: Bearer <access_token>
|
|||||||
"avatar_frame": {},
|
"avatar_frame": {},
|
||||||
"profile_card": {},
|
"profile_card": {},
|
||||||
"vehicle": {},
|
"vehicle": {},
|
||||||
|
"chat_bubble": {},
|
||||||
"mic_seat_animation": {},
|
"mic_seat_animation": {},
|
||||||
"badges": [],
|
"badges": [],
|
||||||
"vip": {},
|
"vip": {},
|
||||||
|
|||||||
@ -790,7 +790,7 @@ paths:
|
|||||||
- users
|
- users
|
||||||
summary: 我的页首屏聚合摘要
|
summary: 我的页首屏聚合摘要
|
||||||
operationId: getMyOverview
|
operationId: getMyOverview
|
||||||
description: 聚合当前用户资料、顶部计数、金币钱包卡片、VIP、Host/Agency/BD/币商身份和入口显隐。该接口不查消息红点、邀请码、钻石、美元余额、流水、成员或工资;钱包、VIP、计数和角色上游失败时按字段 `unavailable=true` 局部降级。
|
description: 聚合当前用户资料、顶部计数、金币钱包卡片、VIP、Host/Agency/BD/币商身份和入口显隐。`data.vip.user_settings` 复用本次 VIP 状态查询,不另外请求 wallet,且只在当前用户私有接口返回。该接口不查消息红点、邀请码、钻石、美元余额、流水、成员或工资;钱包、VIP、计数和角色上游失败时按字段 `unavailable=true` 局部降级。
|
||||||
security:
|
security:
|
||||||
- BearerAuth: []
|
- BearerAuth: []
|
||||||
parameters:
|
parameters:
|
||||||
@ -3782,6 +3782,8 @@ definitions:
|
|||||||
description: 钱包上游不可用时为 true。
|
description: 钱包上游不可用时为 true。
|
||||||
MyOverviewVIPData:
|
MyOverviewVIPData:
|
||||||
type: object
|
type: object
|
||||||
|
required:
|
||||||
|
- user_settings
|
||||||
properties:
|
properties:
|
||||||
level:
|
level:
|
||||||
type: integer
|
type: integer
|
||||||
@ -3796,6 +3798,9 @@ definitions:
|
|||||||
expires_at_ms:
|
expires_at_ms:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
user_settings:
|
||||||
|
$ref: "#/definitions/VIPUserSettingsData"
|
||||||
|
description: 当前 App 的完整 VIP 用户偏好;它不会出现在他人公开资料中。
|
||||||
unavailable:
|
unavailable:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: VIP 上游不可用时为 true。
|
description: VIP 上游不可用时为 true。
|
||||||
@ -6045,6 +6050,9 @@ definitions:
|
|||||||
$ref: "#/definitions/AppearanceResourceData"
|
$ref: "#/definitions/AppearanceResourceData"
|
||||||
vehicle:
|
vehicle:
|
||||||
$ref: "#/definitions/AppearanceResourceData"
|
$ref: "#/definitions/AppearanceResourceData"
|
||||||
|
chat_bubble:
|
||||||
|
$ref: "#/definitions/AppearanceResourceData"
|
||||||
|
description: 当前聊天气泡佩戴快照;空对象表示未佩戴,供公屏按发送人资料渲染。
|
||||||
mic_seat_animation:
|
mic_seat_animation:
|
||||||
$ref: "#/definitions/AppearanceResourceData"
|
$ref: "#/definitions/AppearanceResourceData"
|
||||||
description: 当前麦位动效佩戴快照;空对象表示未佩戴,format 缺省表示后台没有配置受支持 renderer。
|
description: 当前麦位动效佩戴快照;空对象表示未佩戴,format 缺省表示后台没有配置受支持 renderer。
|
||||||
@ -7281,14 +7289,24 @@ definitions:
|
|||||||
format: int64
|
format: int64
|
||||||
room_entry_notice_enabled:
|
room_entry_notice_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: 无持久化记录时默认开启。
|
||||||
online_global_notice_enabled:
|
online_global_notice_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: 无持久化记录时默认开启。
|
||||||
hide_profile_data_enabled:
|
hide_profile_data_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: 无持久化记录时默认关闭。
|
||||||
anonymous_profile_visit_enabled:
|
anonymous_profile_visit_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: 无持久化记录时默认关闭。
|
||||||
leaderboard_invisible_enabled:
|
leaderboard_invisible_enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: 无持久化记录时默认关闭。
|
||||||
updated_at_ms:
|
updated_at_ms:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
|||||||
@ -291,9 +291,9 @@ class VipEntitlementState {
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,9 +416,9 @@ class VipEntitlementState {
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -542,9 +542,9 @@ class VipEntitlementState {
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -728,9 +728,9 @@ class VipEntitlementState {
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -816,9 +816,9 @@ class VipEntitlementState {
|
|||||||
"user_id": 10001,
|
"user_id": 10001,
|
||||||
"room_entry_notice_enabled": true,
|
"room_entry_notice_enabled": true,
|
||||||
"online_global_notice_enabled": true,
|
"online_global_notice_enabled": true,
|
||||||
"hide_profile_data_enabled": true,
|
"hide_profile_data_enabled": false,
|
||||||
"anonymous_profile_visit_enabled": true,
|
"anonymous_profile_visit_enabled": false,
|
||||||
"leaderboard_invisible_enabled": true,
|
"leaderboard_invisible_enabled": false,
|
||||||
"updated_at_ms": 0
|
"updated_at_ms": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -910,7 +910,7 @@ class VipEntitlementState {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
5 个字段都使用 PATCH 语义:不传表示保持原值,显式 `false` 表示关闭。没有持久记录时均默认 `true`;设置只限制用户主动启用对应效果,不会单独授予 VIP 权益。服务端执行时使用“当前有效权益 AND 用户设置”判定,Flutter 不能只看开关。
|
5 个字段都使用 PATCH 语义:不传表示保持原值,显式 `false` 表示关闭。没有持久记录时,进房/上线通知默认 `true`,隐藏个人数据/匿名访问/榜单隐身默认 `false`;设置只限制用户主动启用对应效果,不会单独授予 VIP 权益。服务端执行时使用“当前有效权益 AND 用户设置”判定,Flutter 不能只看开关。
|
||||||
|
|
||||||
## 8. 每日 VIP 金币返现
|
## 8. 每日 VIP 金币返现
|
||||||
|
|
||||||
|
|||||||
@ -371,6 +371,18 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
|
|||||||
"entitlement_id": "ent-vehicle",
|
"entitlement_id": "ent-vehicle",
|
||||||
"expires_at_ms": 1999999999999
|
"expires_at_ms": 1999999999999
|
||||||
},
|
},
|
||||||
|
"chat_bubble": {
|
||||||
|
"resource_id": 8103,
|
||||||
|
"resource_code": "bubble_gold",
|
||||||
|
"resource_type": "chat_bubble",
|
||||||
|
"name": "Gold Chat Bubble",
|
||||||
|
"asset_url": "https://cdn.example/chat-bubble.png",
|
||||||
|
"preview_url": "",
|
||||||
|
"animation_url": "",
|
||||||
|
"metadata_json": "{}",
|
||||||
|
"entitlement_id": "ent-chat-bubble",
|
||||||
|
"expires_at_ms": 1999999999999
|
||||||
|
},
|
||||||
"mic_seat_animation": {
|
"mic_seat_animation": {
|
||||||
"resource_id": 8106,
|
"resource_id": 8106,
|
||||||
"resource_code": "mic_seat_wave_gold",
|
"resource_code": "mic_seat_wave_gold",
|
||||||
@ -394,6 +406,7 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
|
|||||||
- 房间、公屏、在线列表展示头像框用 `avatar_frame`。
|
- 房间、公屏、在线列表展示头像框用 `avatar_frame`。
|
||||||
- 房间资料卡展示用 `profile_card`,从 `metadata_json.profile_card_layout` 解析 `content_top` 和 `content_height`。
|
- 房间资料卡展示用 `profile_card`,从 `metadata_json.profile_card_layout` 解析 `content_top` 和 `content_height`。
|
||||||
- 入场座驾展示用 `vehicle`。
|
- 入场座驾展示用 `vehicle`。
|
||||||
|
- 公屏聊天气泡用 `chat_bubble`;未佩戴时固定返回空对象,普通文本消息按发送人 ID 批量补齐该字段。
|
||||||
- 麦位声波展示用 `mic_seat_animation`。
|
- 麦位声波展示用 `mic_seat_animation`。
|
||||||
- 徽章条展示用 `badges`。
|
- 徽章条展示用 `badges`。
|
||||||
- 过期佩戴不会返回。
|
- 过期佩戴不会返回。
|
||||||
@ -524,5 +537,5 @@ Flutter 处理:
|
|||||||
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
||||||
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
||||||
5. 个人主页信息卡背景使用个人资料返回的 `profile_bg_img`;该字段是用户资料字段,和 `profile_card` 装扮资源无关。
|
5. 个人主页信息卡背景使用个人资料返回的 `profile_bg_img`;该字段是用户资料字段,和 `profile_card` 装扮资源无关。
|
||||||
6. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`badges`。
|
6. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch` 的 `avatar_frame`、`profile_card`、`vehicle`、`chat_bubble`、`badges`。
|
||||||
7. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
7. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。
|
||||||
|
|||||||
@ -382,6 +382,7 @@ Response `data`:
|
|||||||
"metadata_json": "{\"profile_card_layout\":{\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}"
|
"metadata_json": "{\"profile_card_layout\":{\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}"
|
||||||
},
|
},
|
||||||
"vehicle": {},
|
"vehicle": {},
|
||||||
|
"chat_bubble": {},
|
||||||
"charm": 10
|
"charm": 10
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -391,7 +392,7 @@ Response `data`:
|
|||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
- 该接口专门服务房间麦位、公屏和礼物动效展示;不要复用“我的页”或用户主页重接口。
|
- 该接口专门服务房间麦位、公屏和礼物动效展示;不要复用“我的页”或用户主页重接口。
|
||||||
- 当前已接入用户基础资料,`vip/level/badges/avatar_frame/profile_card/vehicle/charm` 保持稳定字段;资料卡高度从 `profile_card.metadata_json.profile_card_layout` 解析。
|
- 当前已接入用户基础资料,`vip/level/badges/avatar_frame/profile_card/vehicle/chat_bubble/charm` 保持稳定字段;资料卡高度从 `profile_card.metadata_json.profile_card_layout` 解析,公屏气泡按发送人资料里的 `chat_bubble` 渲染。
|
||||||
|
|
||||||
### IM UserSig
|
### IM UserSig
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ INCREMENTAL_MIGRATION_FILES=(
|
|||||||
"services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql"
|
"services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql"
|
||||||
"services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql"
|
"services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql"
|
||||||
"services/wallet-service/deploy/mysql/migrations/010_fami_agency_host_income_share.sql"
|
"services/wallet-service/deploy/mysql/migrations/010_fami_agency_host_income_share.sql"
|
||||||
|
"services/wallet-service/deploy/mysql/migrations/011_vip_user_settings_defaults.sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep MySQL as the only required dependency for this script. Business services
|
# Keep MySQL as the only required dependency for this script. Business services
|
||||||
|
|||||||
@ -2,16 +2,16 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|||||||
|
|
||||||
USE hyapp_wallet;
|
USE hyapp_wallet;
|
||||||
|
|
||||||
-- 用户级通知开关只保存偏好,不改变 VIP 资格或 effective_benefits;历史用户不批量补行,
|
-- 用户级开关只保存偏好,不改变 VIP 资格或 effective_benefits;历史用户不批量补行,
|
||||||
-- 查询无记录时由 wallet-service 统一返回默认开启。
|
-- 查询无记录时由 wallet-service 统一返回“通知开、隐私关”的产品默认。
|
||||||
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||||
room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知',
|
room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知',
|
||||||
online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知',
|
online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知',
|
||||||
hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启隐藏个人数据',
|
hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启隐藏个人数据',
|
||||||
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页',
|
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启匿名访问主页',
|
||||||
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启榜单隐身',
|
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启榜单隐身',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (app_code, user_id)
|
PRIMARY KEY (app_code, user_id)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ USE hyapp_wallet;
|
|||||||
-- 显式 LOCK 子句,因此只保留算法硬约束,避免语法失败且禁止静默退化为 COPY/INPLACE。
|
-- 显式 LOCK 子句,因此只保留算法硬约束,避免语法失败且禁止静默退化为 COPY/INPLACE。
|
||||||
SET @ddl := IF(
|
SET @ddl := IF(
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'hide_profile_data_enabled') = 0,
|
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'hide_profile_data_enabled') = 0,
|
||||||
'ALTER TABLE user_vip_settings ADD COLUMN hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启隐藏个人数据'', ALGORITHM=INSTANT',
|
'ALTER TABLE user_vip_settings ADD COLUMN hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT ''是否开启隐藏个人数据'', ALGORITHM=INSTANT',
|
||||||
'SELECT 1'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE stmt FROM @ddl;
|
PREPARE stmt FROM @ddl;
|
||||||
@ -16,7 +16,7 @@ DEALLOCATE PREPARE stmt;
|
|||||||
|
|
||||||
SET @ddl := IF(
|
SET @ddl := IF(
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'anonymous_profile_visit_enabled') = 0,
|
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'anonymous_profile_visit_enabled') = 0,
|
||||||
'ALTER TABLE user_vip_settings ADD COLUMN anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启匿名访问主页'', ALGORITHM=INSTANT',
|
'ALTER TABLE user_vip_settings ADD COLUMN anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT ''是否开启匿名访问主页'', ALGORITHM=INSTANT',
|
||||||
'SELECT 1'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE stmt FROM @ddl;
|
PREPARE stmt FROM @ddl;
|
||||||
@ -25,7 +25,7 @@ DEALLOCATE PREPARE stmt;
|
|||||||
|
|
||||||
SET @ddl := IF(
|
SET @ddl := IF(
|
||||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'leaderboard_invisible_enabled') = 0,
|
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_settings' AND COLUMN_NAME = 'leaderboard_invisible_enabled') = 0,
|
||||||
'ALTER TABLE user_vip_settings ADD COLUMN leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否开启榜单隐身'', ALGORITHM=INSTANT',
|
'ALTER TABLE user_vip_settings ADD COLUMN leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT ''是否开启榜单隐身'', ALGORITHM=INSTANT',
|
||||||
'SELECT 1'
|
'SELECT 1'
|
||||||
);
|
);
|
||||||
PREPARE stmt FROM @ddl;
|
PREPARE stmt FROM @ddl;
|
||||||
|
|||||||
30
scripts/mysql/073_vip_user_settings_defaults.sql
Normal file
30
scripts/mysql/073_vip_user_settings_defaults.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE hyapp_wallet;
|
||||||
|
|
||||||
|
-- 067 已可能在生产执行,因此本迁移只修改后续 INSERT 使用的列默认,
|
||||||
|
-- 不 UPDATE 任何现有行。五列在一条 INSTANT DDL 中更新,只改数据字典,
|
||||||
|
-- 也不会把用户已明确选择的 true/false 重置成新产品默认。
|
||||||
|
SET @ddl := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user_vip_settings'
|
||||||
|
AND COLUMN_NAME IN (
|
||||||
|
'room_entry_notice_enabled',
|
||||||
|
'online_global_notice_enabled',
|
||||||
|
'hide_profile_data_enabled',
|
||||||
|
'anonymous_profile_visit_enabled',
|
||||||
|
'leaderboard_invisible_enabled'
|
||||||
|
)) = 5,
|
||||||
|
'ALTER TABLE user_vip_settings
|
||||||
|
ALTER COLUMN room_entry_notice_enabled SET DEFAULT TRUE,
|
||||||
|
ALTER COLUMN online_global_notice_enabled SET DEFAULT TRUE,
|
||||||
|
ALTER COLUMN hide_profile_data_enabled SET DEFAULT FALSE,
|
||||||
|
ALTER COLUMN anonymous_profile_visit_enabled SET DEFAULT FALSE,
|
||||||
|
ALTER COLUMN leaderboard_invisible_enabled SET DEFAULT FALSE,
|
||||||
|
ALGORITHM=INSTANT',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -38,7 +38,14 @@ func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T)
|
|||||||
}
|
}
|
||||||
walletClient := &fakeWalletClient{
|
walletClient := &fakeWalletClient{
|
||||||
valueSummaryResp: &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{CoinAmount: 12000, UpdatedAtMs: 8000}},
|
valueSummaryResp: &walletv1.GetWalletValueSummaryResponse{Summary: &walletv1.WalletValueSummary{CoinAmount: 12000, UpdatedAtMs: 8000}},
|
||||||
myVipResp: &walletv1.GetMyVipResponse{Vip: &walletv1.UserVip{UserId: 42, Level: 3, Name: "VIP3", Active: true, StartedAtMs: 9000, ExpiresAtMs: 19000}},
|
myVipResp: &walletv1.GetMyVipResponse{
|
||||||
|
Vip: &walletv1.UserVip{UserId: 42, Level: 3, Name: "VIP3", Active: true, StartedAtMs: 9000, ExpiresAtMs: 19000},
|
||||||
|
State: &walletv1.VipState{UserSettings: &walletv1.VipUserSettings{
|
||||||
|
AppCode: "lalu", UserId: 42, RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: false,
|
||||||
|
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: true, LeaderboardInvisibleEnabled: false,
|
||||||
|
UpdatedAtMs: 8500,
|
||||||
|
}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
hostClient := &fakeUserHostClient{
|
hostClient := &fakeUserHostClient{
|
||||||
roleSummary: &userv1.UserRoleSummary{
|
roleSummary: &userv1.UserRoleSummary{
|
||||||
@ -104,6 +111,13 @@ func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T)
|
|||||||
if vip["level"] != float64(3) || vip["name"] != "VIP3" || vip["active"] != true {
|
if vip["level"] != float64(3) || vip["name"] != "VIP3" || vip["active"] != true {
|
||||||
t.Fatalf("vip summary mismatch: %+v", vip)
|
t.Fatalf("vip summary mismatch: %+v", vip)
|
||||||
}
|
}
|
||||||
|
settings := vip["user_settings"].(map[string]any)
|
||||||
|
if settings["app_code"] != "lalu" || settings["user_id"] != float64(42) ||
|
||||||
|
settings["room_entry_notice_enabled"] != true || settings["online_global_notice_enabled"] != false ||
|
||||||
|
settings["hide_profile_data_enabled"] != false || settings["anonymous_profile_visit_enabled"] != true ||
|
||||||
|
settings["leaderboard_invisible_enabled"] != false || settings["updated_at_ms"] != float64(8500) {
|
||||||
|
t.Fatalf("vip user settings mismatch: %+v", settings)
|
||||||
|
}
|
||||||
if _, ok := data["invite"]; ok {
|
if _, ok := data["invite"]; ok {
|
||||||
t.Fatalf("my overview must not include invite: %+v", data["invite"])
|
t.Fatalf("my overview must not include invite: %+v", data["invite"])
|
||||||
}
|
}
|
||||||
@ -155,6 +169,14 @@ func TestGetMyOverviewProjectsOrdinaryUserEntriesAndZeroWallet(t *testing.T) {
|
|||||||
if roles["is_host"] != false || roles["is_manager"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false {
|
if roles["is_host"] != false || roles["is_manager"] != false || roles["is_bd"] != false || roles["is_coin_seller"] != false {
|
||||||
t.Fatalf("ordinary roles mismatch: %+v", roles)
|
t.Fatalf("ordinary roles mismatch: %+v", roles)
|
||||||
}
|
}
|
||||||
|
vip := data["vip"].(map[string]any)
|
||||||
|
settings := vip["user_settings"].(map[string]any)
|
||||||
|
if settings["app_code"] != "lalu" || settings["user_id"] != float64(42) ||
|
||||||
|
settings["room_entry_notice_enabled"] != true || settings["online_global_notice_enabled"] != true ||
|
||||||
|
settings["hide_profile_data_enabled"] != false || settings["anonymous_profile_visit_enabled"] != false ||
|
||||||
|
settings["leaderboard_invisible_enabled"] != false {
|
||||||
|
t.Fatalf("missing wallet state must use notice-on privacy-off defaults: %+v", settings)
|
||||||
|
}
|
||||||
entries := data["entries"].([]any)
|
entries := data["entries"].([]any)
|
||||||
if !overviewEntryVisible(entries, "apply_host") || overviewEntryVisible(entries, "host_center") {
|
if !overviewEntryVisible(entries, "apply_host") || overviewEntryVisible(entries, "host_center") {
|
||||||
t.Fatalf("ordinary entries mismatch: %+v", entries)
|
t.Fatalf("ordinary entries mismatch: %+v", entries)
|
||||||
|
|||||||
@ -3719,7 +3719,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
|||||||
}
|
}
|
||||||
hasRoomDisplayResourceQuery := false
|
hasRoomDisplayResourceQuery := false
|
||||||
for _, req := range walletClient.batchEquippedRequests {
|
for _, req := range walletClient.batchEquippedRequests {
|
||||||
if strings.Join(req.GetResourceTypes(), ",") == "avatar_frame,profile_card,vehicle,badge,mic_seat_animation" {
|
if strings.Join(req.GetResourceTypes(), ",") == "avatar_frame,profile_card,vehicle,chat_bubble,badge,mic_seat_animation" {
|
||||||
hasRoomDisplayResourceQuery = true
|
hasRoomDisplayResourceQuery = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const (
|
|||||||
roomAppearanceResourceAvatarFrame = "avatar_frame"
|
roomAppearanceResourceAvatarFrame = "avatar_frame"
|
||||||
roomAppearanceResourceProfileCard = "profile_card"
|
roomAppearanceResourceProfileCard = "profile_card"
|
||||||
roomAppearanceResourceVehicle = "vehicle"
|
roomAppearanceResourceVehicle = "vehicle"
|
||||||
|
roomAppearanceResourceChatBubble = "chat_bubble"
|
||||||
roomAppearanceResourceBadge = "badge"
|
roomAppearanceResourceBadge = "badge"
|
||||||
roomAppearanceResourceMicSeatAnimation = "mic_seat_animation"
|
roomAppearanceResourceMicSeatAnimation = "mic_seat_animation"
|
||||||
|
|
||||||
@ -2457,7 +2458,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
|||||||
// 房间资料是展示增强,失败时保留主房间数据,让客户端后续重试 batch 接口。
|
// 房间资料是展示增强,失败时保留主房间数据,让客户端后续重试 batch 接口。
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// 房间资料高频读取,头像框/资料卡/座驾/当前佩戴徽章用 wallet 批量查;
|
// 房间资料高频读取,头像框/资料卡/座驾/聊天气泡/当前佩戴徽章用 wallet 批量查;
|
||||||
// activity 的 badge 投影再补展示槽位,两个来源合并后客户端才能同时看到长短徽章。
|
// activity 的 badge 投影再补展示槽位,两个来源合并后客户端才能同时看到长短徽章。
|
||||||
appearanceMap := h.roomAppearanceResourceMap(request, userIDs)
|
appearanceMap := h.roomAppearanceResourceMap(request, userIDs)
|
||||||
badgeMap := h.roomBadgeDisplayMap(request, userIDs)
|
badgeMap := h.roomBadgeDisplayMap(request, userIDs)
|
||||||
@ -2490,6 +2491,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
|||||||
AvatarFrame: appearance.AvatarFrame,
|
AvatarFrame: appearance.AvatarFrame,
|
||||||
ProfileCard: appearance.ProfileCard,
|
ProfileCard: appearance.ProfileCard,
|
||||||
Vehicle: appearance.Vehicle,
|
Vehicle: appearance.Vehicle,
|
||||||
|
ChatBubble: appearance.ChatBubble,
|
||||||
MicSeatAnimation: appearance.MicSeatAnimation,
|
MicSeatAnimation: appearance.MicSeatAnimation,
|
||||||
Charm: 0,
|
Charm: 0,
|
||||||
}
|
}
|
||||||
@ -2519,6 +2521,12 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
|||||||
profile.Vehicle = map[string]any{}
|
profile.Vehicle = map[string]any{}
|
||||||
profiles[userID] = profile
|
profiles[userID] = profile
|
||||||
}
|
}
|
||||||
|
if profiles[userID].ChatBubble == nil {
|
||||||
|
// 公屏普通文本只携带发送人 ID;空对象保持稳定 JSON 契约,观众端无需再区分 null/缺字段。
|
||||||
|
profile := profiles[userID]
|
||||||
|
profile.ChatBubble = map[string]any{}
|
||||||
|
profiles[userID] = profile
|
||||||
|
}
|
||||||
if profiles[userID].MicSeatAnimation == nil {
|
if profiles[userID].MicSeatAnimation == nil {
|
||||||
profile := profiles[userID]
|
profile := profiles[userID]
|
||||||
profile.MicSeatAnimation = map[string]any{}
|
profile.MicSeatAnimation = map[string]any{}
|
||||||
@ -2537,6 +2545,7 @@ type roomAppearanceResources struct {
|
|||||||
AvatarFrame map[string]any
|
AvatarFrame map[string]any
|
||||||
ProfileCard map[string]any
|
ProfileCard map[string]any
|
||||||
Vehicle map[string]any
|
Vehicle map[string]any
|
||||||
|
ChatBubble map[string]any
|
||||||
MicSeatAnimation map[string]any
|
MicSeatAnimation map[string]any
|
||||||
Badges []map[string]any
|
Badges []map[string]any
|
||||||
}
|
}
|
||||||
@ -2551,7 +2560,7 @@ func (h *Handler) roomAppearanceResourceMap(request *http.Request, userIDs []int
|
|||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
UserIds: userIDs,
|
UserIds: userIDs,
|
||||||
ResourceTypes: []string{roomAppearanceResourceAvatarFrame, roomAppearanceResourceProfileCard, roomAppearanceResourceVehicle, roomAppearanceResourceBadge, roomAppearanceResourceMicSeatAnimation},
|
ResourceTypes: []string{roomAppearanceResourceAvatarFrame, roomAppearanceResourceProfileCard, roomAppearanceResourceVehicle, roomAppearanceResourceChatBubble, roomAppearanceResourceBadge, roomAppearanceResourceMicSeatAnimation},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 装扮是展示增强,失败不影响房间主数据返回。
|
// 装扮是展示增强,失败不影响房间主数据返回。
|
||||||
@ -2572,6 +2581,8 @@ func (h *Handler) roomAppearanceResourceMap(request *http.Request, userIDs []int
|
|||||||
current.ProfileCard = resource
|
current.ProfileCard = resource
|
||||||
case roomAppearanceResourceVehicle:
|
case roomAppearanceResourceVehicle:
|
||||||
current.Vehicle = resource
|
current.Vehicle = resource
|
||||||
|
case roomAppearanceResourceChatBubble:
|
||||||
|
current.ChatBubble = resource
|
||||||
case roomAppearanceResourceBadge:
|
case roomAppearanceResourceBadge:
|
||||||
current.Badges = append(current.Badges, roomBadgeItemFromEquippedResource(resource))
|
current.Badges = append(current.Badges, roomBadgeItemFromEquippedResource(resource))
|
||||||
case roomAppearanceResourceMicSeatAnimation:
|
case roomAppearanceResourceMicSeatAnimation:
|
||||||
|
|||||||
@ -219,6 +219,7 @@ type roomDisplayProfileData struct {
|
|||||||
AvatarFrame map[string]any `json:"avatar_frame"`
|
AvatarFrame map[string]any `json:"avatar_frame"`
|
||||||
ProfileCard map[string]any `json:"profile_card"`
|
ProfileCard map[string]any `json:"profile_card"`
|
||||||
Vehicle map[string]any `json:"vehicle"`
|
Vehicle map[string]any `json:"vehicle"`
|
||||||
|
ChatBubble map[string]any `json:"chat_bubble"`
|
||||||
MicSeatAnimation map[string]any `json:"mic_seat_animation"`
|
MicSeatAnimation map[string]any `json:"mic_seat_animation"`
|
||||||
Charm int64 `json:"charm"`
|
Charm int64 `json:"charm"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,12 @@ type agencyCenterSalaryData struct {
|
|||||||
DisplayAmount float64 `json:"display_amount"`
|
DisplayAmount float64 `json:"display_amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type agencyCenterDiamondProgressData struct {
|
||||||
|
CycleKey string `json:"cycle_key"`
|
||||||
|
TotalDiamonds int64 `json:"total_diamonds"`
|
||||||
|
ActiveHostCount int32 `json:"active_host_count"`
|
||||||
|
}
|
||||||
|
|
||||||
type agencyCenterUserData struct {
|
type agencyCenterUserData struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
DisplayUserID string `json:"display_user_id"`
|
DisplayUserID string `json:"display_user_id"`
|
||||||
@ -120,24 +126,50 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
cycleKey := now.Format("2006-01")
|
||||||
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
||||||
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
AppCode: appcode.FromContext(request.Context()),
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
RegionId: agency.GetRegionId(),
|
RegionId: agency.GetRegionId(),
|
||||||
SettlementTriggerMode: "",
|
SettlementTriggerMode: "",
|
||||||
CycleKey: time.Now().UTC().Format("2006-01"),
|
CycleKey: cycleKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// H5 需要完整政策明细;hostCenterPolicyFromProto 会保留政策元数据和所有等级行,agency 端不计算主播等级进度。
|
// Agency 卡片展示的是本工资周期内归属该 owner 的主播钻石合计。聚合依据入账时固化的
|
||||||
|
// agency_owner_user_id 快照,既不会把 owner 的用户 COIN 钱包混进来,也不会因主播后来换公会改写历史归属。
|
||||||
|
statsResp, err := h.walletClient.GetTeamHostSalaryStats(request.Context(), &walletv1.GetTeamHostSalaryStatsRequest{
|
||||||
|
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||||
|
AppCode: appcode.FromContext(request.Context()),
|
||||||
|
AgencyOwnerUserIds: []int64{agency.GetOwnerUserId()},
|
||||||
|
CycleKeys: []string{cycleKey},
|
||||||
|
NowMs: now.UnixMilli(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
progress := agencyCenterDiamondProgressData{CycleKey: cycleKey}
|
||||||
|
for _, stat := range statsResp.GetStats() {
|
||||||
|
if stat.GetCycleKey() != cycleKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
progress.TotalDiamonds = stat.GetTotalDiamonds()
|
||||||
|
progress.ActiveHostCount = stat.GetActiveHostCount()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// H5 需要完整政策明细和 Agency 周期合计;Agency 没有单主播等级,因此不返回 host-center 的 level_progress。
|
||||||
httpkit.WriteOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"found": resp.GetFound(),
|
"found": resp.GetFound(),
|
||||||
"agency_region_id": agency.GetRegionId(),
|
"agency_region_id": agency.GetRegionId(),
|
||||||
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
||||||
|
"progress": progress,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -72,9 +72,23 @@ type overviewVIPData struct {
|
|||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
StartedAtMS int64 `json:"started_at_ms"`
|
StartedAtMS int64 `json:"started_at_ms"`
|
||||||
ExpiresAtMS int64 `json:"expires_at_ms"`
|
ExpiresAtMS int64 `json:"expires_at_ms"`
|
||||||
|
UserSettings overviewVIPUserSettingsData `json:"user_settings"`
|
||||||
Unavailable bool `json:"unavailable,omitempty"`
|
Unavailable bool `json:"unavailable,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overviewVIPUserSettingsData 只出现在当前用户的私有聚合接口。不能把这些 wallet
|
||||||
|
// 偏好塞进公共 userProfileData,否则查看他人资料和批量用户读取会泄露私有设置。
|
||||||
|
type overviewVIPUserSettingsData struct {
|
||||||
|
AppCode string `json:"app_code"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"`
|
||||||
|
OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"`
|
||||||
|
HideProfileDataEnabled bool `json:"hide_profile_data_enabled"`
|
||||||
|
AnonymousProfileVisitEnabled bool `json:"anonymous_profile_visit_enabled"`
|
||||||
|
LeaderboardInvisibleEnabled bool `json:"leaderboard_invisible_enabled"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
// getMyOverview 聚合 App 我的页首屏摘要;gateway 只做 RPC 编排和入口显隐规则,不查数据库。
|
// getMyOverview 聚合 App 我的页首屏摘要;gateway 只做 RPC 编排和入口显隐规则,不查数据库。
|
||||||
func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userProfileClient == nil {
|
if h.userProfileClient == nil {
|
||||||
@ -154,7 +168,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
|||||||
roles = overviewRolesData{Unavailable: true}
|
roles = overviewRolesData{Unavailable: true}
|
||||||
}
|
}
|
||||||
wallet := overviewWallet(walletResp, walletErr)
|
wallet := overviewWallet(walletResp, walletErr)
|
||||||
vip := overviewVIP(vipResp, vipErr)
|
vip := overviewVIP(vipResp, vipErr, appcode.FromContext(request.Context()), userID)
|
||||||
profileStats := overviewStats(statsResp, statsErr)
|
profileStats := overviewStats(statsResp, statsErr)
|
||||||
|
|
||||||
httpkit.WriteOK(writer, request, myOverviewData{
|
httpkit.WriteOK(writer, request, myOverviewData{
|
||||||
@ -208,9 +222,12 @@ func overviewWallet(resp *walletv1.GetWalletValueSummaryResponse, err error) ove
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overviewVIP(resp *walletv1.GetMyVipResponse, err error) overviewVIPData {
|
func overviewVIP(resp *walletv1.GetMyVipResponse, err error, appCode string, userID int64) overviewVIPData {
|
||||||
|
settings := overviewVIPUserSettings(resp.GetState().GetUserSettings(), appCode, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return overviewVIPData{Unavailable: true}
|
// 聚合接口保持稳定 JSON shape,但 unavailable=true 明确表示本次默认值
|
||||||
|
// 不是已确认的持久化事实;客户端不应在降级时覆盖本地最后一次成功状态。
|
||||||
|
return overviewVIPData{UserSettings: settings, Unavailable: true}
|
||||||
}
|
}
|
||||||
vip := resp.GetVip()
|
vip := resp.GetVip()
|
||||||
return overviewVIPData{
|
return overviewVIPData{
|
||||||
@ -219,9 +236,37 @@ func overviewVIP(resp *walletv1.GetMyVipResponse, err error) overviewVIPData {
|
|||||||
Active: vip.GetActive(),
|
Active: vip.GetActive(),
|
||||||
StartedAtMS: vip.GetStartedAtMs(),
|
StartedAtMS: vip.GetStartedAtMs(),
|
||||||
ExpiresAtMS: vip.GetExpiresAtMs(),
|
ExpiresAtMS: vip.GetExpiresAtMs(),
|
||||||
|
UserSettings: settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// overviewVIPUserSettings 复用当前 GetMyVip 回包,不增加 RPC。旧 wallet 实例缺少
|
||||||
|
// user_settings 时与 wallet owner 的缺行语义保持一致:通知开启,三项隐私能力关闭。
|
||||||
|
func overviewVIPUserSettings(settings *walletv1.VipUserSettings, appCode string, userID int64) overviewVIPUserSettingsData {
|
||||||
|
result := overviewVIPUserSettingsData{
|
||||||
|
AppCode: appCode, UserID: userID,
|
||||||
|
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||||
|
}
|
||||||
|
if settings == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.AppCode = settings.GetAppCode()
|
||||||
|
if result.AppCode == "" {
|
||||||
|
result.AppCode = appCode
|
||||||
|
}
|
||||||
|
result.UserID = settings.GetUserId()
|
||||||
|
if result.UserID == 0 {
|
||||||
|
result.UserID = userID
|
||||||
|
}
|
||||||
|
result.RoomEntryNoticeEnabled = settings.GetRoomEntryNoticeEnabled()
|
||||||
|
result.OnlineGlobalNoticeEnabled = settings.GetOnlineGlobalNoticeEnabled()
|
||||||
|
result.HideProfileDataEnabled = settings.GetHideProfileDataEnabled()
|
||||||
|
result.AnonymousProfileVisitEnabled = settings.GetAnonymousProfileVisitEnabled()
|
||||||
|
result.LeaderboardInvisibleEnabled = settings.GetLeaderboardInvisibleEnabled()
|
||||||
|
result.UpdatedAtMS = settings.GetUpdatedAtMs()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func overviewStats(resp *userv1.GetMyProfileStatsResponse, err error) overviewProfileStats {
|
func overviewStats(resp *userv1.GetMyProfileStatsResponse, err error) overviewProfileStats {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return overviewProfileStats{Unavailable: true}
|
return overviewProfileStats{Unavailable: true}
|
||||||
|
|||||||
@ -489,6 +489,13 @@ func TestGetMyVIPKeepsLegacyFieldsAndAddsEffectiveState(t *testing.T) {
|
|||||||
EffectiveBenefits []struct {
|
EffectiveBenefits []struct {
|
||||||
BenefitCode string `json:"benefit_code"`
|
BenefitCode string `json:"benefit_code"`
|
||||||
} `json:"effective_benefits"`
|
} `json:"effective_benefits"`
|
||||||
|
UserSettings struct {
|
||||||
|
RoomEntryNoticeEnabled bool `json:"room_entry_notice_enabled"`
|
||||||
|
OnlineGlobalNoticeEnabled bool `json:"online_global_notice_enabled"`
|
||||||
|
HideProfileDataEnabled bool `json:"hide_profile_data_enabled"`
|
||||||
|
AnonymousProfileVisitEnabled bool `json:"anonymous_profile_visit_enabled"`
|
||||||
|
LeaderboardInvisibleEnabled bool `json:"leaderboard_invisible_enabled"`
|
||||||
|
} `json:"user_settings"`
|
||||||
} `json:"state"`
|
} `json:"state"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
@ -507,6 +514,10 @@ func TestGetMyVIPKeepsLegacyFieldsAndAddsEffectiveState(t *testing.T) {
|
|||||||
if len(envelope.Data.State.EffectiveBenefits) != 1 || envelope.Data.State.EffectiveBenefits[0].BenefitCode != "room_entry_notice" {
|
if len(envelope.Data.State.EffectiveBenefits) != 1 || envelope.Data.State.EffectiveBenefits[0].BenefitCode != "room_entry_notice" {
|
||||||
t.Fatalf("effective benefits mismatch: %+v", envelope.Data.State.EffectiveBenefits)
|
t.Fatalf("effective benefits mismatch: %+v", envelope.Data.State.EffectiveBenefits)
|
||||||
}
|
}
|
||||||
|
settings := envelope.Data.State.UserSettings
|
||||||
|
if !settings.RoomEntryNoticeEnabled || !settings.OnlineGlobalNoticeEnabled || settings.HideProfileDataEnabled || settings.AnonymousProfileVisitEnabled || settings.LeaderboardInvisibleEnabled {
|
||||||
|
t.Fatalf("legacy wallet fallback must keep notice-on privacy-off defaults: %+v", settings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListVIPPackagesAddsProgramStateAndLevelBenefits(t *testing.T) {
|
func TestListVIPPackagesAddsProgramStateAndLevelBenefits(t *testing.T) {
|
||||||
|
|||||||
@ -505,8 +505,8 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData {
|
|||||||
EffectiveBenefits: []vipBenefitData{},
|
EffectiveBenefits: []vipBenefitData{},
|
||||||
UserSettings: vipUserSettingsData{
|
UserSettings: vipUserSettingsData{
|
||||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||||
LeaderboardInvisibleEnabled: true,
|
LeaderboardInvisibleEnabled: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -524,11 +524,12 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData {
|
|||||||
|
|
||||||
func vipUserSettingsFromProto(settings *walletv1.VipUserSettings) vipUserSettingsData {
|
func vipUserSettingsFromProto(settings *walletv1.VipUserSettings) vipUserSettingsData {
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
// wallet 滚动升级期间旧实例没有 tag 8;与 wallet owner 的缺行语义一致,按默认开启兼容。
|
// wallet 滚动升级期间旧实例没有 tag 8;这里必须与 wallet owner 的
|
||||||
|
// 缺行语义一致,否则同一用户会因命中不同 gateway 实例而看到不同的隐私默认。
|
||||||
return vipUserSettingsData{
|
return vipUserSettingsData{
|
||||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||||
LeaderboardInvisibleEnabled: true,
|
LeaderboardInvisibleEnabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vipUserSettingsData{
|
return vipUserSettingsData{
|
||||||
|
|||||||
@ -1834,16 +1834,17 @@ CREATE TABLE IF NOT EXISTS user_vip_memberships (
|
|||||||
KEY idx_user_vip_expiry (app_code, status, expires_at_ms)
|
KEY idx_user_vip_expiry (app_code, status, expires_at_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 VIP 成员关系表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 VIP 成员关系表';
|
||||||
|
|
||||||
-- 用户偏好和 VIP 资格分离:没有记录时运行时默认两项开启;首次 PATCH 才物化。
|
-- 用户偏好和 VIP 资格分离:没有记录时通知默认开启、三项隐私能力默认关闭;
|
||||||
|
-- 首次 PATCH 才物化,且未提交字段必须保留该默认。
|
||||||
-- 主键包含 app_code,确保同一 user_id 在 Fami/Lalu 可独立设置。
|
-- 主键包含 app_code,确保同一 user_id 在 Fami/Lalu 可独立设置。
|
||||||
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
||||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||||
room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知',
|
room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知',
|
||||||
online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知',
|
online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知',
|
||||||
hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启隐藏个人数据',
|
hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启隐藏个人数据',
|
||||||
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页',
|
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启匿名访问主页',
|
||||||
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启榜单隐身',
|
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启榜单隐身',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (app_code, user_id)
|
PRIMARY KEY (app_code, user_id)
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE hyapp_wallet;
|
||||||
|
|
||||||
|
-- 与顶层生产迁移 073 保持同一事实。旧迁移已可能物化用户选择,
|
||||||
|
-- 所以这里只用 INSTANT DDL 修改后续 INSERT 的列默认,绝不更新现有行。
|
||||||
|
SET @ddl := IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'user_vip_settings'
|
||||||
|
AND COLUMN_NAME IN (
|
||||||
|
'room_entry_notice_enabled',
|
||||||
|
'online_global_notice_enabled',
|
||||||
|
'hide_profile_data_enabled',
|
||||||
|
'anonymous_profile_visit_enabled',
|
||||||
|
'leaderboard_invisible_enabled'
|
||||||
|
)) = 5,
|
||||||
|
'ALTER TABLE user_vip_settings
|
||||||
|
ALTER COLUMN room_entry_notice_enabled SET DEFAULT TRUE,
|
||||||
|
ALTER COLUMN online_global_notice_enabled SET DEFAULT TRUE,
|
||||||
|
ALTER COLUMN hide_profile_data_enabled SET DEFAULT FALSE,
|
||||||
|
ALTER COLUMN anonymous_profile_visit_enabled SET DEFAULT FALSE,
|
||||||
|
ALTER COLUMN leaderboard_invisible_enabled SET DEFAULT FALSE,
|
||||||
|
ALGORITHM=INSTANT',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @ddl;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -273,13 +273,14 @@ type VipUserSettings struct {
|
|||||||
UpdatedAtMS int64
|
UpdatedAtMS int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultVipUserSettings 返回尚未物化时的产品默认:五项能力均开启,UpdatedAtMS=0。
|
// DefaultVipUserSettings 返回尚未物化时的产品默认:通知默认开启,
|
||||||
|
// 隐藏个人数据、匿名访问和榜单隐身默认关闭,UpdatedAtMS=0。
|
||||||
func DefaultVipUserSettings(appCode string, userID int64) VipUserSettings {
|
func DefaultVipUserSettings(appCode string, userID int64) VipUserSettings {
|
||||||
return VipUserSettings{
|
return VipUserSettings{
|
||||||
AppCode: appCode, UserID: userID,
|
AppCode: appCode, UserID: userID,
|
||||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||||
LeaderboardInvisibleEnabled: true,
|
LeaderboardInvisibleEnabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,9 +25,9 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
|||||||
t.Fatalf("get default VIP settings failed: %v", err)
|
t.Fatalf("get default VIP settings failed: %v", err)
|
||||||
}
|
}
|
||||||
if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled ||
|
if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled ||
|
||||||
!defaultState.UserSettings.HideProfileDataEnabled || !defaultState.UserSettings.AnonymousProfileVisitEnabled ||
|
defaultState.UserSettings.HideProfileDataEnabled || defaultState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||||
!defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 {
|
defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 {
|
||||||
t.Fatalf("missing settings row must project default enabled: %+v", defaultState.UserSettings)
|
t.Fatalf("missing settings row must project notice-on privacy-off defaults: %+v", defaultState.UserSettings)
|
||||||
}
|
}
|
||||||
if got := repository.CountRows("user_vip_settings", "app_code = ? AND user_id = ?", "fami", paidUserID); got != 0 {
|
if got := repository.CountRows("user_vip_settings", "app_code = ? AND user_id = ?", "fami", paidUserID); got != 0 {
|
||||||
t.Fatalf("read must not materialize default settings row, got %d", got)
|
t.Fatalf("read must not materialize default settings row, got %d", got)
|
||||||
@ -37,14 +37,16 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
|||||||
settings, err := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
|
settings, err := service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
|
||||||
AppCode: "fami", UserID: paidUserID, RoomEntryNoticeEnabled: &roomOff,
|
AppCode: "fami", UserID: paidUserID, RoomEntryNoticeEnabled: &roomOff,
|
||||||
})
|
})
|
||||||
if err != nil || settings.RoomEntryNoticeEnabled || !settings.OnlineGlobalNoticeEnabled || settings.UpdatedAtMS <= 0 {
|
if err != nil || settings.RoomEntryNoticeEnabled || !settings.OnlineGlobalNoticeEnabled ||
|
||||||
|
settings.HideProfileDataEnabled || settings.AnonymousProfileVisitEnabled || settings.LeaderboardInvisibleEnabled ||
|
||||||
|
settings.UpdatedAtMS <= 0 {
|
||||||
t.Fatalf("first partial settings update mismatch: settings=%+v err=%v", settings, err)
|
t.Fatalf("first partial settings update mismatch: settings=%+v err=%v", settings, err)
|
||||||
}
|
}
|
||||||
hideProfileOff := false
|
anonymousVisitOn := true
|
||||||
settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
|
settings, err = service.UpdateVipUserSettings(famiCtx, ledger.UpdateVipUserSettingsCommand{
|
||||||
AppCode: "fami", UserID: paidUserID, HideProfileDataEnabled: &hideProfileOff,
|
AppCode: "fami", UserID: paidUserID, AnonymousProfileVisitEnabled: &anonymousVisitOn,
|
||||||
})
|
})
|
||||||
if err != nil || settings.HideProfileDataEnabled || !settings.AnonymousProfileVisitEnabled || !settings.LeaderboardInvisibleEnabled {
|
if err != nil || settings.HideProfileDataEnabled || !settings.AnonymousProfileVisitEnabled || settings.LeaderboardInvisibleEnabled {
|
||||||
t.Fatalf("privacy partial update must preserve other switches: settings=%+v err=%v", settings, err)
|
t.Fatalf("privacy partial update must preserve other switches: settings=%+v err=%v", settings, err)
|
||||||
}
|
}
|
||||||
onlineOff := false
|
onlineOff := false
|
||||||
@ -56,8 +58,8 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
|||||||
}
|
}
|
||||||
laluState, err := service.GetVipState(laluCtx, paidUserID)
|
laluState, err := service.GetVipState(laluCtx, paidUserID)
|
||||||
if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled ||
|
if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled ||
|
||||||
!laluState.UserSettings.HideProfileDataEnabled || !laluState.UserSettings.AnonymousProfileVisitEnabled ||
|
laluState.UserSettings.HideProfileDataEnabled || laluState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||||
!laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 {
|
laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 {
|
||||||
t.Fatalf("same user settings must remain App scoped: state=%+v err=%v", laluState.UserSettings, err)
|
t.Fatalf("same user settings must remain App scoped: state=%+v err=%v", laluState.UserSettings, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +86,9 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
|||||||
t.Fatalf("concurrent partial settings update failed: left=%v right=%v", leftErr, rightErr)
|
t.Fatalf("concurrent partial settings update failed: left=%v right=%v", leftErr, rightErr)
|
||||||
}
|
}
|
||||||
concurrentState, err := service.GetVipState(famiCtx, concurrentUserID)
|
concurrentState, err := service.GetVipState(famiCtx, concurrentUserID)
|
||||||
if err != nil || concurrentState.UserSettings.RoomEntryNoticeEnabled || concurrentState.UserSettings.OnlineGlobalNoticeEnabled {
|
if err != nil || concurrentState.UserSettings.RoomEntryNoticeEnabled || concurrentState.UserSettings.OnlineGlobalNoticeEnabled ||
|
||||||
|
concurrentState.UserSettings.HideProfileDataEnabled || concurrentState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||||
|
concurrentState.UserSettings.LeaderboardInvisibleEnabled {
|
||||||
t.Fatalf("concurrent partial updates overwrote each other: settings=%+v err=%v", concurrentState.UserSettings, err)
|
t.Fatalf("concurrent partial updates overwrote each other: settings=%+v err=%v", concurrentState.UserSettings, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -423,14 +423,16 @@ func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fami tiered 的三项房间视觉权益是稳定跨服务契约,不能由后台把 code 改成
|
// Fami tiered 的固定房间视觉权益是稳定跨服务契约,不能由后台把 code 改成
|
||||||
// function 或绑定到近似类型。disabled 可以暂不选素材,但一旦携带 resource_id,
|
// function 或绑定到近似类型。disabled 可以暂不选素材,但一旦携带 resource_id,
|
||||||
// 下方仍会校验资源 active 且类型精确匹配,避免脏绑定在之后切状态时直接生效。
|
// 下方仍会校验资源 active 且类型精确匹配,避免脏绑定在之后切状态时直接生效。
|
||||||
expectedResourceType := ""
|
expectedResourceType := ""
|
||||||
if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 && appcode.FromContext(ctx) == "fami" {
|
if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 && appcode.FromContext(ctx) == "fami" {
|
||||||
switch benefit.BenefitCode {
|
switch benefit.BenefitCode {
|
||||||
case ledger.VipBenefitCodeMicSkin:
|
case ledger.VipBenefitCodeMicSkin:
|
||||||
expectedResourceType = resourcedomain.TypeMicSeatAnimation
|
// 麦克风皮肤替换麦位中心图标;mic_seat_animation 留给 voice_wave 声波动效,
|
||||||
|
// 不能因两者都显示在麦位周围就复用同一目录类型。
|
||||||
|
expectedResourceType = resourcedomain.TypeMicSeatIcon
|
||||||
case ledger.VipBenefitCodeRoomBorder:
|
case ledger.VipBenefitCodeRoomBorder:
|
||||||
expectedResourceType = resourcedomain.TypeRoomBorder
|
expectedResourceType = resourcedomain.TypeRoomBorder
|
||||||
case ledger.VipBenefitCodeColoredRoomName:
|
case ledger.VipBenefitCodeColoredRoomName:
|
||||||
|
|||||||
@ -12,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// UpdateVipUserSettings 用一条 partial upsert 原子修改用户明确提交的开关。
|
// UpdateVipUserSettings 用一条 partial upsert 原子修改用户明确提交的开关。
|
||||||
// INSERT 分支给未提交字段使用默认 true;UPDATE 分支则保留原值,避免两个并发 PATCH 互相覆盖。
|
// INSERT 分支必须使用与无行读取完全相同的产品默认;UPDATE 分支则保留原值,
|
||||||
|
// 避免用户首次只改通知时意外开启三项隐私权益,也避免两个并发 PATCH 互相覆盖。
|
||||||
func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.UpdateVipUserSettingsCommand) (ledger.VipUserSettings, error) {
|
func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.UpdateVipUserSettingsCommand) (ledger.VipUserSettings, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.VipUserSettings{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return ledger.VipUserSettings{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
@ -41,17 +42,17 @@ func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.U
|
|||||||
if onlineProvided {
|
if onlineProvided {
|
||||||
onlineValue = *command.OnlineGlobalNoticeEnabled
|
onlineValue = *command.OnlineGlobalNoticeEnabled
|
||||||
}
|
}
|
||||||
hideProfileValue := true
|
hideProfileValue := false
|
||||||
hideProfileProvided := command.HideProfileDataEnabled != nil
|
hideProfileProvided := command.HideProfileDataEnabled != nil
|
||||||
if hideProfileProvided {
|
if hideProfileProvided {
|
||||||
hideProfileValue = *command.HideProfileDataEnabled
|
hideProfileValue = *command.HideProfileDataEnabled
|
||||||
}
|
}
|
||||||
anonymousVisitValue := true
|
anonymousVisitValue := false
|
||||||
anonymousVisitProvided := command.AnonymousProfileVisitEnabled != nil
|
anonymousVisitProvided := command.AnonymousProfileVisitEnabled != nil
|
||||||
if anonymousVisitProvided {
|
if anonymousVisitProvided {
|
||||||
anonymousVisitValue = *command.AnonymousProfileVisitEnabled
|
anonymousVisitValue = *command.AnonymousProfileVisitEnabled
|
||||||
}
|
}
|
||||||
leaderboardInvisibleValue := true
|
leaderboardInvisibleValue := false
|
||||||
leaderboardInvisibleProvided := command.LeaderboardInvisibleEnabled != nil
|
leaderboardInvisibleProvided := command.LeaderboardInvisibleEnabled != nil
|
||||||
if leaderboardInvisibleProvided {
|
if leaderboardInvisibleProvided {
|
||||||
leaderboardInvisibleValue = *command.LeaderboardInvisibleEnabled
|
leaderboardInvisibleValue = *command.LeaderboardInvisibleEnabled
|
||||||
@ -87,7 +88,7 @@ func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.U
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// queryVipUserSettings 统一处理“无行即默认开启”。调用方可以传事务,从而让 VipState 的
|
// queryVipUserSettings 统一处理“无行即产品默认”。调用方可以传事务,从而让 VipState 的
|
||||||
// program、会员、体验卡、benefits 和 settings 保持在同一 repeatable-read 快照。
|
// program、会员、体验卡、benefits 和 settings 保持在同一 repeatable-read 快照。
|
||||||
func (r *Repository) queryVipUserSettings(ctx context.Context, querier interface {
|
func (r *Repository) queryVipUserSettings(ctx context.Context, querier interface {
|
||||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user