vip相关
This commit is contained in:
parent
f89aa37318
commit
b4655c1326
@ -18288,7 +18288,7 @@ func (x *VipTrialCard) GetUpdatedAtMs() int64 {
|
||||
}
|
||||
|
||||
// VipUserSettings 是用户按 App 隔离的可关闭 VIP 功能偏好。没有持久记录时 wallet
|
||||
// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||
// 默认开启进房/上线通知、关闭三项隐私能力,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||
type VipUserSettings struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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
|
||||
// 仍返回五个默认开启值,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||
// 默认开启进房/上线通知、关闭三项隐私能力,updated_at_ms=0;设置本身不授予任何 VIP 权益。
|
||||
message VipUserSettings {
|
||||
string app_code = 1;
|
||||
int64 user_id = 2;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
## 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,禁止局部合并。
|
||||
- `user_settings` 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离、无记录默认开启。
|
||||
- `user_settings` 固定包含 5 项开关:进房通知、上线全服通知、隐藏个人数据、匿名访问主页、榜单隐身;均按 App 隔离,无记录时前两项通知默认开启、后三项隐私能力默认关闭。
|
||||
- 权益富展示读取结构化 `benefits[].presentation`。它覆盖说明文案、多项静态/动态预览、用户头像合成方式和数值奖励;Flutter 不解析 `metadata_json` 生成 UI。
|
||||
- VIP 购买后的余额读取带 `version` 的 `coin_balance`;`coin_balance_after` 仅兼容旧客户端。历史幂等回放若 `version=0`,必须重新请求钱包余额。
|
||||
- 业务分支只判断稳定 `code`,不得解析 `message`。精确 JSON、`presentation` Schema 和错误码矩阵以 [新版 VIP 权限策略 Flutter 客户端对接文档](./新版VIP权限策略_Flutter客户端对接文档.md) 为唯一协议定义;本文后续较短 JSON 只解释场景,不定义裁剪响应。
|
||||
@ -340,9 +340,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -468,9 +468,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -492,7 +492,7 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
||||
- 当体验卡被佩戴时,不限制卡等级高低,它会覆盖付费 VIP 成为 `effective_vip`;卸下或过期后自然回落到仍有效的 `paid_vip`。
|
||||
- 体验卡状态下,`daily_coin_rebate` 不会进入 `effective_benefits`。Flutter 不要只看卡等级猜测权益。
|
||||
- 无 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. 购买、续期和升级
|
||||
|
||||
@ -580,9 +580,9 @@ final benefitHandlers = <String, VipBenefitHandler>{
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -784,9 +784,9 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
},
|
||||
@ -884,9 +884,9 @@ Flutter 收到后用 `event_id` 做会话内去重,再调 `GET /vip/me`。不
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"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/users/me/overview` 也在 `data.vip.user_settings` 返回同一份完整设置,它复用聚合接口已有的 `GetMyVip` 回包,不会增加 wallet RPC。该字段只属于当前用户私有信息,查看他人的公开 profile 不返回。
|
||||
|
||||
### 参数
|
||||
|
||||
GET 无参数。PATCH 是局部更新,5 个字段至少传一个;显式 `false` 不能被当成未传:
|
||||
@ -1134,7 +1136,7 @@ PATCH:
|
||||
}
|
||||
```
|
||||
|
||||
用户从未修改过时,5 项均默认为 `true`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖其他项。
|
||||
用户从未修改过时,进房/上线通知默认为 `true`,隐藏个人数据/匿名访问/榜单隐身默认为 `false`,`updated_at_ms=0`。PATCH 只改本次提交的字段,不会覆盖其他项。
|
||||
|
||||
### 相关 IM
|
||||
|
||||
@ -1426,7 +1428,7 @@ Flutter 先把 `action_param` 再解析为 JSON。点击前可将页面上所有
|
||||
| 同级续期 | 不在本地计算,以响应为准 | 已强制,从旧截止时间累加 |
|
||||
| 体验卡自由切换 | 按 `entitlement_id` 佩戴,每次直接替换全局 `state` | 已强制,不限等级,不作废旧卡,不改绝对截止时间 |
|
||||
| 体验卡权益 | 只渲染 `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;不发全服 |
|
||||
| 防踢 | 房管操作返回 `VIP_ANTI_KICK` 时显示不可踢,不做本地预判 | 已在 room-service 强制;只阻止普通房主/管理员的 KickUser |
|
||||
| 防禁言 | 房管操作返回 `VIP_ANTI_MUTE` 时显示不可禁言,解禁始终可发起 | 已在 room-service 强制;只阻止普通房主/管理员新增禁言 |
|
||||
|
||||
@ -93,7 +93,7 @@ Fami 的 VIP1–VIP9 初始全部为 `disabled`。30 天只是不产生零时长
|
||||
- `anonymous_profile_visit_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,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,6 +191,7 @@ Flutter 建议放在 body 里,避免调用层漏拼 query。
|
||||
| `profiles[].avatar_frame` | 头像框后置拉。 |
|
||||
| `profiles[].profile_card` | 资料卡后置拉。 |
|
||||
| `profiles[].vehicle` | 座驾后置拉。 |
|
||||
| `profiles[].chat_bubble` | 聊天气泡后置拉。 |
|
||||
| `profiles[].mic_seat_animation` | 麦位动画后置拉。 |
|
||||
| `profiles[].badges` | 等级/成就徽章后置拉。 |
|
||||
| `profiles[].vip` | VIP 展示后置拉。 |
|
||||
@ -249,6 +250,7 @@ Authorization: Bearer <access_token>
|
||||
"avatar_frame": {},
|
||||
"profile_card": {},
|
||||
"vehicle": {},
|
||||
"chat_bubble": {},
|
||||
"mic_seat_animation": {},
|
||||
"badges": [],
|
||||
"vip": {},
|
||||
|
||||
@ -790,7 +790,7 @@ paths:
|
||||
- users
|
||||
summary: 我的页首屏聚合摘要
|
||||
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:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
@ -3782,6 +3782,8 @@ definitions:
|
||||
description: 钱包上游不可用时为 true。
|
||||
MyOverviewVIPData:
|
||||
type: object
|
||||
required:
|
||||
- user_settings
|
||||
properties:
|
||||
level:
|
||||
type: integer
|
||||
@ -3796,6 +3798,9 @@ definitions:
|
||||
expires_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
user_settings:
|
||||
$ref: "#/definitions/VIPUserSettingsData"
|
||||
description: 当前 App 的完整 VIP 用户偏好;它不会出现在他人公开资料中。
|
||||
unavailable:
|
||||
type: boolean
|
||||
description: VIP 上游不可用时为 true。
|
||||
@ -6045,6 +6050,9 @@ definitions:
|
||||
$ref: "#/definitions/AppearanceResourceData"
|
||||
vehicle:
|
||||
$ref: "#/definitions/AppearanceResourceData"
|
||||
chat_bubble:
|
||||
$ref: "#/definitions/AppearanceResourceData"
|
||||
description: 当前聊天气泡佩戴快照;空对象表示未佩戴,供公屏按发送人资料渲染。
|
||||
mic_seat_animation:
|
||||
$ref: "#/definitions/AppearanceResourceData"
|
||||
description: 当前麦位动效佩戴快照;空对象表示未佩戴,format 缺省表示后台没有配置受支持 renderer。
|
||||
@ -7281,14 +7289,24 @@ definitions:
|
||||
format: int64
|
||||
room_entry_notice_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 无持久化记录时默认开启。
|
||||
online_global_notice_enabled:
|
||||
type: boolean
|
||||
default: true
|
||||
description: 无持久化记录时默认开启。
|
||||
hide_profile_data_enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 无持久化记录时默认关闭。
|
||||
anonymous_profile_visit_enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 无持久化记录时默认关闭。
|
||||
leaderboard_invisible_enabled:
|
||||
type: boolean
|
||||
default: false
|
||||
description: 无持久化记录时默认关闭。
|
||||
updated_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
|
||||
@ -291,9 +291,9 @@ class VipEntitlementState {
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -416,9 +416,9 @@ class VipEntitlementState {
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -542,9 +542,9 @@ class VipEntitlementState {
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
}
|
||||
@ -728,9 +728,9 @@ class VipEntitlementState {
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"updated_at_ms": 0
|
||||
}
|
||||
},
|
||||
@ -816,9 +816,9 @@ class VipEntitlementState {
|
||||
"user_id": 10001,
|
||||
"room_entry_notice_enabled": true,
|
||||
"online_global_notice_enabled": true,
|
||||
"hide_profile_data_enabled": true,
|
||||
"anonymous_profile_visit_enabled": true,
|
||||
"leaderboard_invisible_enabled": true,
|
||||
"hide_profile_data_enabled": false,
|
||||
"anonymous_profile_visit_enabled": false,
|
||||
"leaderboard_invisible_enabled": false,
|
||||
"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 金币返现
|
||||
|
||||
|
||||
@ -371,6 +371,18 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
|
||||
"entitlement_id": "ent-vehicle",
|
||||
"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": {
|
||||
"resource_id": 8106,
|
||||
"resource_code": "mic_seat_wave_gold",
|
||||
@ -394,6 +406,7 @@ GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
|
||||
- 房间、公屏、在线列表展示头像框用 `avatar_frame`。
|
||||
- 房间资料卡展示用 `profile_card`,从 `metadata_json.profile_card_layout` 解析 `content_top` 和 `content_height`。
|
||||
- 入场座驾展示用 `vehicle`。
|
||||
- 公屏聊天气泡用 `chat_bubble`;未佩戴时固定返回空对象,普通文本消息按发送人 ID 批量补齐该字段。
|
||||
- 麦位声波展示用 `mic_seat_animation`。
|
||||
- 徽章条展示用 `badges`。
|
||||
- 过期佩戴不会返回。
|
||||
@ -524,5 +537,5 @@ Flutter 处理:
|
||||
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`。
|
||||
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`。
|
||||
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` 播放入场座驾。
|
||||
|
||||
@ -382,6 +382,7 @@ Response `data`:
|
||||
"metadata_json": "{\"profile_card_layout\":{\"source_height\":1680,\"content_top\":143,\"content_height\":1525}}"
|
||||
},
|
||||
"vehicle": {},
|
||||
"chat_bubble": {},
|
||||
"charm": 10
|
||||
}
|
||||
]
|
||||
@ -391,7 +392,7 @@ Response `data`:
|
||||
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
|
||||
|
||||
|
||||
@ -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/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/011_vip_user_settings_defaults.sql"
|
||||
)
|
||||
|
||||
# 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;
|
||||
|
||||
-- 用户级通知开关只保存偏好,不改变 VIP 资格或 effective_benefits;历史用户不批量补行,
|
||||
-- 查询无记录时由 wallet-service 统一返回默认开启。
|
||||
-- 用户级开关只保存偏好,不改变 VIP 资格或 effective_benefits;历史用户不批量补行,
|
||||
-- 查询无记录时由 wallet-service 统一返回“通知开、隐私关”的产品默认。
|
||||
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
room_entry_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 '是否开启隐藏个人数据',
|
||||
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页',
|
||||
leaderboard_invisible_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 FALSE COMMENT '是否开启匿名访问主页',
|
||||
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启榜单隐身',
|
||||
created_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)
|
||||
|
||||
@ -7,7 +7,7 @@ USE hyapp_wallet;
|
||||
-- 显式 LOCK 子句,因此只保留算法硬约束,避免语法失败且禁止静默退化为 COPY/INPLACE。
|
||||
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,
|
||||
'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'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
@ -16,7 +16,7 @@ DEALLOCATE PREPARE stmt;
|
||||
|
||||
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,
|
||||
'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'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
@ -25,7 +25,7 @@ DEALLOCATE PREPARE stmt;
|
||||
|
||||
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,
|
||||
'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'
|
||||
);
|
||||
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{
|
||||
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{
|
||||
roleSummary: &userv1.UserRoleSummary{
|
||||
@ -104,6 +111,13 @@ func TestGetMyOverviewComposesProfileWalletVIPStatsRolesAndEntries(t *testing.T)
|
||||
if vip["level"] != float64(3) || vip["name"] != "VIP3" || vip["active"] != true {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
if !overviewEntryVisible(entries, "apply_host") || overviewEntryVisible(entries, "host_center") {
|
||||
t.Fatalf("ordinary entries mismatch: %+v", entries)
|
||||
|
||||
@ -3719,7 +3719,7 @@ func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T)
|
||||
}
|
||||
hasRoomDisplayResourceQuery := false
|
||||
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
|
||||
break
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ const (
|
||||
roomAppearanceResourceAvatarFrame = "avatar_frame"
|
||||
roomAppearanceResourceProfileCard = "profile_card"
|
||||
roomAppearanceResourceVehicle = "vehicle"
|
||||
roomAppearanceResourceChatBubble = "chat_bubble"
|
||||
roomAppearanceResourceBadge = "badge"
|
||||
roomAppearanceResourceMicSeatAnimation = "mic_seat_animation"
|
||||
|
||||
@ -2457,7 +2458,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
// 房间资料是展示增强,失败时保留主房间数据,让客户端后续重试 batch 接口。
|
||||
return nil
|
||||
}
|
||||
// 房间资料高频读取,头像框/资料卡/座驾/当前佩戴徽章用 wallet 批量查;
|
||||
// 房间资料高频读取,头像框/资料卡/座驾/聊天气泡/当前佩戴徽章用 wallet 批量查;
|
||||
// activity 的 badge 投影再补展示槽位,两个来源合并后客户端才能同时看到长短徽章。
|
||||
appearanceMap := h.roomAppearanceResourceMap(request, userIDs)
|
||||
badgeMap := h.roomBadgeDisplayMap(request, userIDs)
|
||||
@ -2490,6 +2491,7 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
AvatarFrame: appearance.AvatarFrame,
|
||||
ProfileCard: appearance.ProfileCard,
|
||||
Vehicle: appearance.Vehicle,
|
||||
ChatBubble: appearance.ChatBubble,
|
||||
MicSeatAnimation: appearance.MicSeatAnimation,
|
||||
Charm: 0,
|
||||
}
|
||||
@ -2519,6 +2521,12 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
|
||||
profile.Vehicle = map[string]any{}
|
||||
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 {
|
||||
profile := profiles[userID]
|
||||
profile.MicSeatAnimation = map[string]any{}
|
||||
@ -2537,6 +2545,7 @@ type roomAppearanceResources struct {
|
||||
AvatarFrame map[string]any
|
||||
ProfileCard map[string]any
|
||||
Vehicle map[string]any
|
||||
ChatBubble map[string]any
|
||||
MicSeatAnimation 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()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
UserIds: userIDs,
|
||||
ResourceTypes: []string{roomAppearanceResourceAvatarFrame, roomAppearanceResourceProfileCard, roomAppearanceResourceVehicle, roomAppearanceResourceBadge, roomAppearanceResourceMicSeatAnimation},
|
||||
ResourceTypes: []string{roomAppearanceResourceAvatarFrame, roomAppearanceResourceProfileCard, roomAppearanceResourceVehicle, roomAppearanceResourceChatBubble, roomAppearanceResourceBadge, roomAppearanceResourceMicSeatAnimation},
|
||||
})
|
||||
if err != nil {
|
||||
// 装扮是展示增强,失败不影响房间主数据返回。
|
||||
@ -2572,6 +2581,8 @@ func (h *Handler) roomAppearanceResourceMap(request *http.Request, userIDs []int
|
||||
current.ProfileCard = resource
|
||||
case roomAppearanceResourceVehicle:
|
||||
current.Vehicle = resource
|
||||
case roomAppearanceResourceChatBubble:
|
||||
current.ChatBubble = resource
|
||||
case roomAppearanceResourceBadge:
|
||||
current.Badges = append(current.Badges, roomBadgeItemFromEquippedResource(resource))
|
||||
case roomAppearanceResourceMicSeatAnimation:
|
||||
|
||||
@ -219,6 +219,7 @@ type roomDisplayProfileData struct {
|
||||
AvatarFrame map[string]any `json:"avatar_frame"`
|
||||
ProfileCard map[string]any `json:"profile_card"`
|
||||
Vehicle map[string]any `json:"vehicle"`
|
||||
ChatBubble map[string]any `json:"chat_bubble"`
|
||||
MicSeatAnimation map[string]any `json:"mic_seat_animation"`
|
||||
Charm int64 `json:"charm"`
|
||||
}
|
||||
|
||||
@ -28,6 +28,12 @@ type agencyCenterSalaryData struct {
|
||||
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 {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
@ -120,24 +126,50 @@ func (h *Handler) getAgencyCenterPlatformPolicy(writer http.ResponseWriter, requ
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
cycleKey := now.Format("2006-01")
|
||||
// Agency Center 不是主播身份,不能复用 host-center 的 active host 校验;这里固定用当前 agency 的区域读取完整工资政策。
|
||||
resp, err := h.walletClient.GetActiveHostSalaryPolicy(request.Context(), &walletv1.GetActiveHostSalaryPolicyRequest{
|
||||
RequestId: httpkit.RequestIDFromContext(request.Context()),
|
||||
AppCode: appcode.FromContext(request.Context()),
|
||||
RegionId: agency.GetRegionId(),
|
||||
SettlementTriggerMode: "",
|
||||
CycleKey: time.Now().UTC().Format("2006-01"),
|
||||
CycleKey: cycleKey,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
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{
|
||||
"found": resp.GetFound(),
|
||||
"agency_region_id": agency.GetRegionId(),
|
||||
"policy": hostCenterPolicyFromProto(resp.GetPolicy()),
|
||||
"progress": progress,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -67,12 +67,26 @@ type overviewEntryData struct {
|
||||
}
|
||||
|
||||
type overviewVIPData struct {
|
||||
Level int32 `json:"level"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
StartedAtMS int64 `json:"started_at_ms"`
|
||||
ExpiresAtMS int64 `json:"expires_at_ms"`
|
||||
Unavailable bool `json:"unavailable,omitempty"`
|
||||
Level int32 `json:"level"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
StartedAtMS int64 `json:"started_at_ms"`
|
||||
ExpiresAtMS int64 `json:"expires_at_ms"`
|
||||
UserSettings overviewVIPUserSettingsData `json:"user_settings"`
|
||||
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 编排和入口显隐规则,不查数据库。
|
||||
@ -154,7 +168,7 @@ func (h *Handler) getMyOverview(writer http.ResponseWriter, request *http.Reques
|
||||
roles = overviewRolesData{Unavailable: true}
|
||||
}
|
||||
wallet := overviewWallet(walletResp, walletErr)
|
||||
vip := overviewVIP(vipResp, vipErr)
|
||||
vip := overviewVIP(vipResp, vipErr, appcode.FromContext(request.Context()), userID)
|
||||
profileStats := overviewStats(statsResp, statsErr)
|
||||
|
||||
httpkit.WriteOK(writer, request, myOverviewData{
|
||||
@ -208,20 +222,51 @@ 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 {
|
||||
return overviewVIPData{Unavailable: true}
|
||||
// 聚合接口保持稳定 JSON shape,但 unavailable=true 明确表示本次默认值
|
||||
// 不是已确认的持久化事实;客户端不应在降级时覆盖本地最后一次成功状态。
|
||||
return overviewVIPData{UserSettings: settings, Unavailable: true}
|
||||
}
|
||||
vip := resp.GetVip()
|
||||
return overviewVIPData{
|
||||
Level: vip.GetLevel(),
|
||||
Name: vip.GetName(),
|
||||
Active: vip.GetActive(),
|
||||
StartedAtMS: vip.GetStartedAtMs(),
|
||||
ExpiresAtMS: vip.GetExpiresAtMs(),
|
||||
Level: vip.GetLevel(),
|
||||
Name: vip.GetName(),
|
||||
Active: vip.GetActive(),
|
||||
StartedAtMS: vip.GetStartedAtMs(),
|
||||
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 {
|
||||
if err != nil {
|
||||
return overviewProfileStats{Unavailable: true}
|
||||
|
||||
@ -489,6 +489,13 @@ func TestGetMyVIPKeepsLegacyFieldsAndAddsEffectiveState(t *testing.T) {
|
||||
EffectiveBenefits []struct {
|
||||
BenefitCode string `json:"benefit_code"`
|
||||
} `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:"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" {
|
||||
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) {
|
||||
|
||||
@ -505,8 +505,8 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData {
|
||||
EffectiveBenefits: []vipBenefitData{},
|
||||
UserSettings: vipUserSettingsData{
|
||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
||||
LeaderboardInvisibleEnabled: true,
|
||||
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||
LeaderboardInvisibleEnabled: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -524,11 +524,12 @@ func vipStateFromProto(state *walletv1.VipState) vipStateData {
|
||||
|
||||
func vipUserSettingsFromProto(settings *walletv1.VipUserSettings) vipUserSettingsData {
|
||||
if settings == nil {
|
||||
// wallet 滚动升级期间旧实例没有 tag 8;与 wallet owner 的缺行语义一致,按默认开启兼容。
|
||||
// wallet 滚动升级期间旧实例没有 tag 8;这里必须与 wallet owner 的
|
||||
// 缺行语义一致,否则同一用户会因命中不同 gateway 实例而看到不同的隐私默认。
|
||||
return vipUserSettingsData{
|
||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
||||
LeaderboardInvisibleEnabled: true,
|
||||
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||
LeaderboardInvisibleEnabled: false,
|
||||
}
|
||||
}
|
||||
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)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 VIP 成员关系表';
|
||||
|
||||
-- 用户偏好和 VIP 资格分离:没有记录时运行时默认两项开启;首次 PATCH 才物化。
|
||||
-- 用户偏好和 VIP 资格分离:没有记录时通知默认开启、三项隐私能力默认关闭;
|
||||
-- 首次 PATCH 才物化,且未提交字段必须保留该默认。
|
||||
-- 主键包含 app_code,确保同一 user_id 在 Fami/Lalu 可独立设置。
|
||||
CREATE TABLE IF NOT EXISTS user_vip_settings (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL COMMENT '用户 ID',
|
||||
room_entry_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 '是否开启隐藏个人数据',
|
||||
anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启匿名访问主页',
|
||||
leaderboard_invisible_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 FALSE COMMENT '是否开启匿名访问主页',
|
||||
leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启榜单隐身',
|
||||
created_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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
// DefaultVipUserSettings 返回尚未物化时的产品默认:五项能力均开启,UpdatedAtMS=0。
|
||||
// DefaultVipUserSettings 返回尚未物化时的产品默认:通知默认开启,
|
||||
// 隐藏个人数据、匿名访问和榜单隐身默认关闭,UpdatedAtMS=0。
|
||||
func DefaultVipUserSettings(appCode string, userID int64) VipUserSettings {
|
||||
return VipUserSettings{
|
||||
AppCode: appCode, UserID: userID,
|
||||
RoomEntryNoticeEnabled: true, OnlineGlobalNoticeEnabled: true,
|
||||
HideProfileDataEnabled: true, AnonymousProfileVisitEnabled: true,
|
||||
LeaderboardInvisibleEnabled: true,
|
||||
HideProfileDataEnabled: false, AnonymousProfileVisitEnabled: false,
|
||||
LeaderboardInvisibleEnabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,9 +25,9 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
||||
t.Fatalf("get default VIP settings failed: %v", err)
|
||||
}
|
||||
if !defaultState.UserSettings.RoomEntryNoticeEnabled || !defaultState.UserSettings.OnlineGlobalNoticeEnabled ||
|
||||
!defaultState.UserSettings.HideProfileDataEnabled || !defaultState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||
!defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 {
|
||||
t.Fatalf("missing settings row must project default enabled: %+v", defaultState.UserSettings)
|
||||
defaultState.UserSettings.HideProfileDataEnabled || defaultState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||
defaultState.UserSettings.LeaderboardInvisibleEnabled || defaultState.UserSettings.UpdatedAtMS != 0 {
|
||||
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 {
|
||||
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{
|
||||
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)
|
||||
}
|
||||
hideProfileOff := false
|
||||
anonymousVisitOn := true
|
||||
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)
|
||||
}
|
||||
onlineOff := false
|
||||
@ -56,8 +58,8 @@ func TestVipUserSettingsDefaultPartialUpdateAndAppScope(t *testing.T) {
|
||||
}
|
||||
laluState, err := service.GetVipState(laluCtx, paidUserID)
|
||||
if err != nil || !laluState.UserSettings.RoomEntryNoticeEnabled || !laluState.UserSettings.OnlineGlobalNoticeEnabled ||
|
||||
!laluState.UserSettings.HideProfileDataEnabled || !laluState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||
!laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 {
|
||||
laluState.UserSettings.HideProfileDataEnabled || laluState.UserSettings.AnonymousProfileVisitEnabled ||
|
||||
laluState.UserSettings.LeaderboardInvisibleEnabled || laluState.UserSettings.UpdatedAtMS != 0 {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
// 下方仍会校验资源 active 且类型精确匹配,避免脏绑定在之后切状态时直接生效。
|
||||
expectedResourceType := ""
|
||||
if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 && appcode.FromContext(ctx) == "fami" {
|
||||
switch benefit.BenefitCode {
|
||||
case ledger.VipBenefitCodeMicSkin:
|
||||
expectedResourceType = resourcedomain.TypeMicSeatAnimation
|
||||
// 麦克风皮肤替换麦位中心图标;mic_seat_animation 留给 voice_wave 声波动效,
|
||||
// 不能因两者都显示在麦位周围就复用同一目录类型。
|
||||
expectedResourceType = resourcedomain.TypeMicSeatIcon
|
||||
case ledger.VipBenefitCodeRoomBorder:
|
||||
expectedResourceType = resourcedomain.TypeRoomBorder
|
||||
case ledger.VipBenefitCodeColoredRoomName:
|
||||
|
||||
@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// UpdateVipUserSettings 用一条 partial upsert 原子修改用户明确提交的开关。
|
||||
// INSERT 分支给未提交字段使用默认 true;UPDATE 分支则保留原值,避免两个并发 PATCH 互相覆盖。
|
||||
// INSERT 分支必须使用与无行读取完全相同的产品默认;UPDATE 分支则保留原值,
|
||||
// 避免用户首次只改通知时意外开启三项隐私权益,也避免两个并发 PATCH 互相覆盖。
|
||||
func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.UpdateVipUserSettingsCommand) (ledger.VipUserSettings, error) {
|
||||
if r == nil || r.db == nil {
|
||||
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 {
|
||||
onlineValue = *command.OnlineGlobalNoticeEnabled
|
||||
}
|
||||
hideProfileValue := true
|
||||
hideProfileValue := false
|
||||
hideProfileProvided := command.HideProfileDataEnabled != nil
|
||||
if hideProfileProvided {
|
||||
hideProfileValue = *command.HideProfileDataEnabled
|
||||
}
|
||||
anonymousVisitValue := true
|
||||
anonymousVisitValue := false
|
||||
anonymousVisitProvided := command.AnonymousProfileVisitEnabled != nil
|
||||
if anonymousVisitProvided {
|
||||
anonymousVisitValue = *command.AnonymousProfileVisitEnabled
|
||||
}
|
||||
leaderboardInvisibleValue := true
|
||||
leaderboardInvisibleValue := false
|
||||
leaderboardInvisibleProvided := command.LeaderboardInvisibleEnabled != nil
|
||||
if leaderboardInvisibleProvided {
|
||||
leaderboardInvisibleValue = *command.LeaderboardInvisibleEnabled
|
||||
@ -87,7 +88,7 @@ func (r *Repository) UpdateVipUserSettings(ctx context.Context, command ledger.U
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// queryVipUserSettings 统一处理“无行即默认开启”。调用方可以传事务,从而让 VipState 的
|
||||
// queryVipUserSettings 统一处理“无行即产品默认”。调用方可以传事务,从而让 VipState 的
|
||||
// program、会员、体验卡、benefits 和 settings 保持在同一 repeatable-read 快照。
|
||||
func (r *Repository) queryVipUserSettings(ctx context.Context, querier interface {
|
||||
QueryRowContext(context.Context, string, ...any) *sql.Row
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user