Add youth mode restriction labels
This commit is contained in:
+5
-5
@@ -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}` |
|
||||
|
||||
+43
-1
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user