Add youth mode restriction labels

This commit is contained in:
richjohnrambo
2026-09-02 13:47:12 +08:00
parent 79947a2c6f
commit a94f60c254
4 changed files with 104 additions and 12 deletions
+5 -5
View File
@@ -2402,7 +2402,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增。**
- `GET /v1/im/settings/youth-mode` 返回 `enabled,policy_version,restrictions,password_set,can_enable`。
- `GET /v1/im/settings/youth-mode` 返回 `enabled,policy_version,restrictions,restriction_items,password_set,can_enable`。当前默认策略项为 `night_quiet`、`content_filter`、`payment_guard` 三条;后端现阶段完成状态持久化、开启/关闭校验和策略返回,业务接口层的深夜免打扰、内容过滤、支付守护尚未实际强制执行,`restriction_items[].enforced=false` 用于明确表达这一点。
- 点击开启先展示 10.15,再按 1.7 完成身份验证和 1.11 设置独立密码;最后调用 `POST /v1/im/settings/youth-mode/enable`,Body `{"policy_version":n,"action_token":"..."}`,其中 `action_token` 使用设置独立密码响应里的 `enable_action_token`。
- 开启链路中,短信校验返回的 `set_independent_password` token 只用于设置独立密码;设置密码成功响应中的 `enable_action_token` 才用于 `youth-mode/enable`。
- 服务端持久化到用户租户范围并推送所有设备,不能仅设置本地标志。
@@ -2413,7 +2413,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:弹窗纯客户端;文案由服务端配置。**
- 使用 10.14 返回的 `restrictions` 与 `policy_version` 渲染;取消不请求,确认进入密码设置/验证。
- 使用 10.14 返回的 `restriction_items` 与 `policy_version` 渲染;取消不请求,确认进入密码设置/验证。`restrictions` 仅作为兼容旧客户端的 code 数组。
- 最终 enable 请求携带用户确认的 `policy_version`;若策略更新返回 `POLICY_RECONFIRM_REQUIRED`,重新展示新内容。
### 10.16 青少年模式已开启(安卓).png
@@ -2423,7 +2423,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
**状态:新增。**
- 页面调用 10.14,`enabled=true` 时展示 `enabled_at,restrictions`。`bootstrap` 同时返回受限模块和动作。
- 受限接口服务端返回 `YOUTH_MODE_RESTRICTED`;前端隐藏只是体验优化,不能作为安全控制。
- 后续某条规则真正接入业务拦截后,受限接口服务端返回 `YOUTH_MODE_RESTRICTED`,并把对应 `restriction_items[].enforced` 改为 `true`;前端隐藏只是体验优化,不能作为安全控制。
- 登录、App 重启和另一设备均从服务端恢复状态;收到 `youth_mode.updated` 即刷新。
### 10.17 关闭青少年模式(安卓).png
@@ -2492,10 +2492,10 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| `PATCH /v1/im/me/profile` | `{"nickname":"新昵称","signature":"新签名","version":1}` | `{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true}}` |
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"status":"ok","password_set":true,"purpose":"youth_mode","enable_action_token":"enable_token","enable_action_expires_at":"2026-08-20T09:05:00Z","next_action":"enable_youth_mode"}` |
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
| `POST /v1/im/settings/youth-mode/enable` | `{"policy_version":1,"action_token":"enable_action_token"}` | `{"enabled":true,"policy_version":1,"password_set":true,"restrictions":["night_quiet","content_filter","payment_guard"],"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/settings/youth-mode/enable` | `{"policy_version":1,"action_token":"enable_action_token"}` | `{"enabled":true,"policy_version":1,"password_set":true,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/settings/youth-mode/disable` | Header `X-Action-Token: action_token`;Body `{}` | `{"enabled":false,"policy_version":1,"password_set":true,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/security/challenges` 开启青少年模式 | Header `Idempotency-Key`;Body `{"scene":"set_independent_password","channel":"sms"}` | `{"challenge_id":"sec_chg_1","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60}` |
| `POST /v1/im/security/challenges` 关闭青少年模式 | Header `Idempotency-Key`;Body `{"scene":"close_youth_mode","channel":"sms"}` | `{"challenge_id":"sec_chg_2","phone_mask":"+861****000","expires_in_seconds":300,"resend_after_seconds":60}` |
@@ -23,6 +23,8 @@ Idempotency-Key: <uuid>
测试环境验证码固定为 `123456`。`action_token` 为 5 分钟有效、单次消费,只能用于指定动作,不能在多个接口之间重复使用。
当前默认策略项为 `night_quiet`、`content_filter`、`payment_guard` 三条。现阶段后端已完成状态持久化、开启/关闭校验和策略返回;业务接口层的深夜消息免打扰、内容过滤、支付拦截尚未实际强制执行,响应中的 `restriction_items[].enforced=false` 用于明确表达这一点。后续某条规则真正接入后台策略和业务拦截后,应把对应项改为 `true`。
## 2. 开启青少年模式
### 2.1 查询当前状态
@@ -40,6 +42,26 @@ GET /v1/im/settings/youth-mode
"enabled": false,
"policy_version": 1,
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
"restriction_items": [
{
"code": "night_quiet",
"title": "深夜免打扰",
"description": "夜间减少消息提醒打扰,帮助用户保持健康作息。",
"enforced": false
},
{
"code": "content_filter",
"title": "内容过滤",
"description": "对不适合青少年的内容进行识别和限制展示。",
"enforced": false
},
{
"code": "payment_guard",
"title": "支付守护",
"description": "限制或确认支付、充值等敏感消费行为。",
"enforced": false
}
],
"password_set": false,
"can_enable": false,
"updated_at": "2026-08-20T09:00:00Z"
@@ -47,7 +69,7 @@ GET /v1/im/settings/youth-mode
}
```
前端使用 `policy_version` 和 `restrictions` 渲染开启确认页。最终开启时需要提交用户确认过的 `policy_version`。
前端优先使用 `restriction_items` 渲染开启确认页,`restrictions` 保留为兼容旧客户端的 code 数组。最终开启时需要提交用户确认过的 `policy_version`。
### 2.2 发起短信身份验证
@@ -175,6 +197,26 @@ POST /v1/im/settings/youth-mode/enable
"policy_version": 1,
"password_set": true,
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
"restriction_items": [
{
"code": "night_quiet",
"title": "深夜免打扰",
"description": "夜间减少消息提醒打扰,帮助用户保持健康作息。",
"enforced": false
},
{
"code": "content_filter",
"title": "内容过滤",
"description": "对不适合青少年的内容进行识别和限制展示。",
"enforced": false
},
{
"code": "payment_guard",
"title": "支付守护",
"description": "限制或确认支付、充值等敏感消费行为。",
"enforced": false
}
],
"updated_at": "2026-08-20T09:00:00Z"
}
}
+48 -6
View File
@@ -8166,12 +8166,13 @@ func appReleasePayload(release *types.AppRelease) map[string]any {
func youthModePayload(settings *types.YouthModeSettings) map[string]any {
data := map[string]any{
"enabled": settings.Enabled,
"policy_version": settings.PolicyVersion,
"restrictions": settings.Restrictions,
"password_set": settings.PasswordSet,
"can_enable": settings.CanEnable,
"updated_at": settings.UpdatedAt.UTC().Format(time.RFC3339Nano),
"enabled": settings.Enabled,
"policy_version": settings.PolicyVersion,
"restrictions": settings.Restrictions,
"restriction_items": youthModeRestrictionItems(settings.Restrictions),
"password_set": settings.PasswordSet,
"can_enable": settings.CanEnable,
"updated_at": settings.UpdatedAt.UTC().Format(time.RFC3339Nano),
}
if settings.EnabledAt != nil {
data["enabled_at"] = settings.EnabledAt.UTC().Format(time.RFC3339Nano)
@@ -8179,6 +8180,47 @@ func youthModePayload(settings *types.YouthModeSettings) map[string]any {
return data
}
func youthModeRestrictionItems(restrictions []string) []map[string]any {
catalog := map[string]map[string]any{
"night_quiet": {
"code": "night_quiet",
"title": "深夜免打扰",
"description": "夜间减少消息提醒打扰,帮助用户保持健康作息。",
"enforced": false,
},
"content_filter": {
"code": "content_filter",
"title": "内容过滤",
"description": "对不适合青少年的内容进行识别和限制展示。",
"enforced": false,
},
"payment_guard": {
"code": "payment_guard",
"title": "支付守护",
"description": "限制或确认支付、充值等敏感消费行为。",
"enforced": false,
},
}
items := make([]map[string]any, 0, len(restrictions))
for _, code := range restrictions {
code = strings.TrimSpace(code)
if code == "" {
continue
}
if item, ok := catalog[code]; ok {
items = append(items, item)
continue
}
items = append(items, map[string]any{
"code": code,
"title": code,
"description": "",
"enforced": false,
})
}
return items
}
func devicePushTokenPayload(token *types.DevicePushToken) map[string]any {
return map[string]any{
"device_id": token.DeviceID,
+8
View File
@@ -3186,6 +3186,14 @@ func TestServeIMYouthModeEnableDisable(t *testing.T) {
if data["enabled"] != true || data["policy_version"].(float64) != 2 {
t.Fatalf("unexpected youth enable payload: %#v", data)
}
items := data["restriction_items"].([]any)
if len(items) != 3 {
t.Fatalf("restriction_items: got %#v", items)
}
firstItem := items[0].(map[string]any)
if firstItem["code"] != "night_quiet" || firstItem["title"] != "深夜免打扰" || firstItem["enforced"] != false {
t.Fatalf("unexpected restriction item: %#v", firstItem)
}
security.actionToken = &types.ActionToken{Action: "close_youth_mode", Target: "youth_mode"}
expectIMBearer(t, ctrl, "token-disable", 7, 42)