房间踢人列表增加
This commit is contained in:
parent
38f0a90f48
commit
8fe09044d3
File diff suppressed because it is too large
Load Diff
@ -299,6 +299,15 @@ message AdminCancelRoomPinResponse {
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
// RoomBanState 表达单个房间 ban 的治理状态。
|
||||
message RoomBanState {
|
||||
int64 user_id = 1;
|
||||
int64 actor_user_id = 2;
|
||||
int64 created_at_ms = 3;
|
||||
// expires_at_ms 为 0 表示永久 ban;非 0 表示该 UTC 毫秒后可重新进房。
|
||||
int64 expires_at_ms = 4;
|
||||
}
|
||||
|
||||
// RoomSnapshot 把 room-service 对外需要暴露的房间投影集中返回。
|
||||
message RoomSnapshot {
|
||||
string room_id = 1;
|
||||
@ -322,6 +331,8 @@ message RoomSnapshot {
|
||||
bool locked = 19;
|
||||
// treasure 是语音房宝箱当前状态;配置物料通过 GetRoomTreasure 单独读取,避免快照过大。
|
||||
RoomTreasureState treasure = 20;
|
||||
// ban_states 保存 ban 的过期边界;ban_user_ids 仍只服务简单集合判断。
|
||||
repeated RoomBanState ban_states = 21;
|
||||
}
|
||||
|
||||
// RoomBackgroundImage 是房间维度保存过的背景图素材。
|
||||
@ -707,6 +718,8 @@ message MuteUserResponse {
|
||||
message KickUserRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 target_user_id = 2;
|
||||
// duration_ms 为 0 表示永久 ban;大于 0 表示从服务端当前 UTC 时间起禁止进房的时长。
|
||||
int64 duration_ms = 3;
|
||||
}
|
||||
|
||||
// KickUserResponse 返回最新房间快照。
|
||||
@ -972,6 +985,34 @@ message ListRoomOnlineUsersResponse {
|
||||
repeated RoomOnlineUser items = 6;
|
||||
}
|
||||
|
||||
// RoomBannedUser 是房间黑名单列表的轻量治理读模型;用户展示资料由 gateway 批量补齐。
|
||||
message RoomBannedUser {
|
||||
int64 user_id = 1;
|
||||
int64 actor_user_id = 2;
|
||||
int64 created_at_ms = 3;
|
||||
// unban_at_ms 为 0 表示永久 ban;非 0 表示该 UTC 毫秒后自动允许重新进房。
|
||||
int64 unban_at_ms = 4;
|
||||
int64 remaining_ms = 5;
|
||||
bool permanent = 6;
|
||||
}
|
||||
|
||||
// ListRoomBannedUsersRequest 分页查询房间当前仍有效的黑名单。
|
||||
message ListRoomBannedUsersRequest {
|
||||
RequestMeta meta = 1;
|
||||
string room_id = 2;
|
||||
int64 viewer_user_id = 3;
|
||||
int32 page = 4;
|
||||
int32 page_size = 5;
|
||||
}
|
||||
|
||||
message ListRoomBannedUsersResponse {
|
||||
repeated RoomBannedUser items = 1;
|
||||
int64 total = 2;
|
||||
int32 page = 3;
|
||||
int32 page_size = 4;
|
||||
int64 server_time_ms = 5;
|
||||
}
|
||||
|
||||
// FollowRoomRequest 建立当前用户对房间的关注关系。
|
||||
// 该关系是 room-service 的用户-房间低频关系事实,不进入 Room Cell command log。
|
||||
message FollowRoomRequest {
|
||||
@ -1057,4 +1098,5 @@ service RoomQueryService {
|
||||
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
|
||||
rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse);
|
||||
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
|
||||
rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse);
|
||||
}
|
||||
|
||||
@ -1423,6 +1423,7 @@ const (
|
||||
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
|
||||
RoomQueryService_GetRoomTreasure_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomTreasure"
|
||||
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
|
||||
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
|
||||
)
|
||||
|
||||
// RoomQueryServiceClient is the client API for RoomQueryService service.
|
||||
@ -1445,6 +1446,7 @@ type RoomQueryServiceClient interface {
|
||||
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
|
||||
GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error)
|
||||
ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error)
|
||||
ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error)
|
||||
}
|
||||
|
||||
type roomQueryServiceClient struct {
|
||||
@ -1595,6 +1597,16 @@ func (c *roomQueryServiceClient) ListRoomOnlineUsers(ctx context.Context, in *Li
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListRoomBannedUsersResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomBannedUsers_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RoomQueryServiceServer is the server API for RoomQueryService service.
|
||||
// All implementations must embed UnimplementedRoomQueryServiceServer
|
||||
// for forward compatibility.
|
||||
@ -1615,6 +1627,7 @@ type RoomQueryServiceServer interface {
|
||||
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
|
||||
GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error)
|
||||
ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error)
|
||||
ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error)
|
||||
mustEmbedUnimplementedRoomQueryServiceServer()
|
||||
}
|
||||
|
||||
@ -1667,6 +1680,9 @@ func (UnimplementedRoomQueryServiceServer) GetRoomTreasure(context.Context, *Get
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
|
||||
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -1940,6 +1956,24 @@ func _RoomQueryService_ListRoomOnlineUsers_Handler(srv interface{}, ctx context.
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_ListRoomBannedUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListRoomBannedUsersRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomQueryServiceServer).ListRoomBannedUsers(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomQueryService_ListRoomBannedUsers_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomQueryServiceServer).ListRoomBannedUsers(ctx, req.(*ListRoomBannedUsersRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// RoomQueryService_ServiceDesc is the grpc.ServiceDesc for RoomQueryService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -2003,6 +2037,10 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "ListRoomOnlineUsers",
|
||||
Handler: _RoomQueryService_ListRoomOnlineUsers_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRoomBannedUsers",
|
||||
Handler: _RoomQueryService_ListRoomBannedUsers_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "proto/room/v1/room.proto",
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
|
||||
`POST /api/v1/rooms/user/kick`
|
||||
|
||||
`GET /api/v1/rooms/{room_id}/banned-users`
|
||||
|
||||
`POST /api/v1/rooms/user/unban`
|
||||
|
||||
本地开发地址:
|
||||
@ -19,6 +21,7 @@ http://127.0.0.1:13000
|
||||
| 方法 | 接口 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `POST` | `/api/v1/rooms/user/kick` | 踢出房间用户,并写入房间 ban 集合。 |
|
||||
| `GET` | `/api/v1/rooms/{room_id}/banned-users` | 查询房间维度当前仍有效的踢人列表,也就是房间黑名单列表。 |
|
||||
| `POST` | `/api/v1/rooms/user/unban` | 解除房间 ban,允许用户后续重新 JoinRoom。 |
|
||||
|
||||
## 请求头
|
||||
@ -39,6 +42,7 @@ http://127.0.0.1:13000
|
||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||
| `command_id` | 是 | string | `cmd_kick_room_10001_20001_1710000000` | 房间命令幂等键;同一次踢人重试必须复用。 |
|
||||
| `target_user_id` | 是 | int64 | `20001` | 被踢用户长 ID。 |
|
||||
| `duration_ms` | 否 | int64 | `600000` | 禁止重新进房的毫秒时长;`0` 或不传表示永久,直到房间 owner 调用解封。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
@ -51,7 +55,8 @@ Content-Type: application/json
|
||||
{
|
||||
"room_id": "room_10001",
|
||||
"command_id": "cmd_kick_room_10001_20001_1710000000",
|
||||
"target_user_id": 20001
|
||||
"target_user_id": 20001,
|
||||
"duration_ms": 600000
|
||||
}
|
||||
```
|
||||
|
||||
@ -72,6 +77,14 @@ Content-Type: application/json
|
||||
"room_id": "room_10001",
|
||||
"status": "active",
|
||||
"ban_user_ids": [20001],
|
||||
"ban_states": [
|
||||
{
|
||||
"user_id": 20001,
|
||||
"actor_user_id": 10001,
|
||||
"created_at_ms": 1710000000456,
|
||||
"expires_at_ms": 1710000600456
|
||||
}
|
||||
],
|
||||
"online_users": [],
|
||||
"mic_seats": [],
|
||||
"version": 18
|
||||
@ -86,17 +99,106 @@ Content-Type: application/json
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复踢已 ban 且无残留状态的用户可能为 `false`。 |
|
||||
| `data.result.applied` | bool | 本次命令是否产生状态变更;同一个 `command_id` 重试返回同一命令结果,新的 `command_id` 再踢同一用户会刷新 ban 状态。 |
|
||||
| `data.room.ban_user_ids` | int64[] | 当前房间 ban 用户集合;被踢用户会进入该集合。 |
|
||||
| `data.room.ban_states` | array | 当前房间 ban 的时间边界;`expires_at_ms=0` 表示永久,非 0 表示该 UTC 毫秒后可重新进房。 |
|
||||
| `data.room.online_users` | array | 当前业务 presence;被踢用户会被移除。 |
|
||||
| `data.room.mic_seats` | array | 如果被踢用户在麦上,对应麦位会被释放。 |
|
||||
| `data.rtc_kicked` | bool | 服务端是否已调用 TRTC `RemoveUserByStrRoomId` 移除目标用户。 |
|
||||
| `data.rtc_kick_error` | string | TRTC 服务端踢人失败原因;踢人事实已生效,客户端仍按被踢处理。 |
|
||||
|
||||
## 房间踢人列表
|
||||
|
||||
该接口用于房主或房管查看当前房间仍有效的踢人列表。列表只返回当前仍会阻止用户进房的记录;限时踢人已过期后不再返回。被解除的用户也不再返回。
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
GET /api/v1/rooms/room_10001/banned-users?page=1&page_size=20
|
||||
Authorization: Bearer <access_token>
|
||||
X-App-Code: lalu
|
||||
```
|
||||
|
||||
路径参数:
|
||||
|
||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 字段 | 必填 | 类型 | 默认值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `page` | 否 | int32 | `1` | 页码,从 1 开始。 |
|
||||
| `page_size` | 否 | int32 | `20` | 每页数量;建议 App 使用 20,后端最大值按实现收敛。 |
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"room_id": "room_10001",
|
||||
"items": [
|
||||
{
|
||||
"user_id": "20001",
|
||||
"display_user_id": "90020001",
|
||||
"username": "Alice",
|
||||
"avatar": "https://example.com/avatar.png",
|
||||
"country": "US",
|
||||
"country_name": "United States",
|
||||
"country_display_name": "United States",
|
||||
"country_flag": "🇺🇸",
|
||||
"actor_user_id": "10001",
|
||||
"created_at_ms": 1710000000456,
|
||||
"unban_at_ms": 1710000600456,
|
||||
"remaining_ms": 599000,
|
||||
"permanent": false
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"server_time_ms": 1710000001456
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `data.room_id` | string | 当前查询的房间 ID。 |
|
||||
| `data.items[].user_id` | string | 被踢用户长 ID;Flutter 侧按字符串保存,避免大整数精度问题。 |
|
||||
| `data.items[].display_user_id` | string | 用户展示 ID;可能为空,端上需要兜底展示 `user_id`。 |
|
||||
| `data.items[].username` | string | 用户昵称;可能为空,端上可兜底展示 `display_user_id` 或 `user_id`。 |
|
||||
| `data.items[].avatar` | string | 用户头像 URL;可能为空,端上展示默认头像。 |
|
||||
| `data.items[].country` | string | 用户国家二位码,例如 `US`。 |
|
||||
| `data.items[].country_name` | string | 用户国家标准名。 |
|
||||
| `data.items[].country_display_name` | string | App 展示优先使用的国家名。 |
|
||||
| `data.items[].country_flag` | string | 国家国旗 emoji;为空时端上可按 `country` 本地兜底。 |
|
||||
| `data.items[].actor_user_id` | string | 执行踢人的用户 ID;系统驱逐时可能为 `"0"`。 |
|
||||
| `data.items[].created_at_ms` | int64 | 踢人状态写入时间,UTC epoch milliseconds。 |
|
||||
| `data.items[].unban_at_ms` | int64 | 自动解封时间;`0` 表示永久,需要手动解除。 |
|
||||
| `data.items[].remaining_ms` | int64 | 距离自动解除的剩余毫秒;永久踢人为 `0`。 |
|
||||
| `data.items[].permanent` | bool | 是否永久禁止进房。 |
|
||||
| `data.total` | int64 | 当前房间有效黑名单总数。 |
|
||||
| `data.server_time_ms` | int64 | 服务端当前 UTC 毫秒;倒计时 UI 以它校正本地时间。 |
|
||||
|
||||
## 解除房间 ban
|
||||
|
||||
解除 ban 只恢复用户后续重新进房资格,不恢复历史 presence、麦位、管理员身份或 RTC 连接。
|
||||
|
||||
请求体:
|
||||
|
||||
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
|
||||
| `command_id` | 是 | string | `cmd_unban_room_10001_20001_1710000100` | 房间命令幂等键;同一次解除操作重试必须复用。 |
|
||||
| `target_user_id` | 是 | int64 | `20001` | 要解除踢人状态的用户长 ID。 |
|
||||
|
||||
请求示例:
|
||||
|
||||
```http
|
||||
@ -112,7 +214,37 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
成功响应字段和踢人一致,`room.ban_user_ids` 中不再包含目标用户。
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "OK",
|
||||
"message": "ok",
|
||||
"request_id": "req_abc",
|
||||
"data": {
|
||||
"result": {
|
||||
"applied": true,
|
||||
"room_version": 19,
|
||||
"server_time_ms": 1710000100456
|
||||
},
|
||||
"room": {
|
||||
"room_id": "room_10001",
|
||||
"status": "active",
|
||||
"ban_user_ids": [],
|
||||
"ban_states": [],
|
||||
"version": 19
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `data.result.applied` | bool | 是否实际删除了 ban 状态;目标用户本来不在黑名单时为 `false`。 |
|
||||
| `data.room.ban_user_ids` | int64[] | 当前房间仍有效的 ban 用户集合;成功解除后不再包含目标用户。 |
|
||||
| `data.room.ban_states` | array | 当前房间仍有效的 ban 状态;成功解除后不再包含目标用户对应状态。 |
|
||||
|
||||
## IM 和 RTC 交互
|
||||
|
||||
@ -211,12 +343,22 @@ C2C 通知和房间群消息使用同一个 `event_id`。Flutter 侧把 `room_us
|
||||
| --- | --- |
|
||||
| 同一个 `command_id` 重试同一次踢人 | 返回同一命令结果,不重复变更房间。 |
|
||||
| 同一个 `command_id` 换另一个 `target_user_id` | `409 CONFLICT`。 |
|
||||
| 重复踢已 ban 且无残留状态的用户 | `200 OK`,`applied=false`。 |
|
||||
| 新的 `command_id` 再次踢同一用户 | `200 OK`,刷新 ban 状态和 `duration_ms`。 |
|
||||
| 解封未 ban 用户 | `200 OK`,`applied=false`。 |
|
||||
|
||||
## Flutter 解析示例
|
||||
|
||||
```dart
|
||||
int parseIntLike(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(value?.toString() ?? '') ?? 0;
|
||||
}
|
||||
|
||||
class RoomCommandResult {
|
||||
RoomCommandResult({
|
||||
required this.applied,
|
||||
@ -243,12 +385,14 @@ class KickUserResult {
|
||||
required this.rtcKicked,
|
||||
required this.rtcKickError,
|
||||
required this.banUserIds,
|
||||
required this.banStates,
|
||||
});
|
||||
|
||||
final RoomCommandResult result;
|
||||
final bool rtcKicked;
|
||||
final String rtcKickError;
|
||||
final List<int> banUserIds;
|
||||
final List<RoomBanState> banStates;
|
||||
|
||||
factory KickUserResult.fromJson(Map<String, dynamic> json) {
|
||||
final room = json['room'] as Map<String, dynamic>? ?? const {};
|
||||
@ -259,8 +403,119 @@ class KickUserResult {
|
||||
rtcKicked: json['rtc_kicked'] as bool? ?? false,
|
||||
rtcKickError: json['rtc_kick_error'] as String? ?? '',
|
||||
banUserIds: (room['ban_user_ids'] as List<dynamic>? ?? const [])
|
||||
.map((value) => (value as num).toInt())
|
||||
.map(parseIntLike)
|
||||
.toList(),
|
||||
banStates: (room['ban_states'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(RoomBanState.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomBanState {
|
||||
RoomBanState({
|
||||
required this.userId,
|
||||
required this.actorUserId,
|
||||
required this.createdAtMs,
|
||||
required this.expiresAtMs,
|
||||
});
|
||||
|
||||
final int userId;
|
||||
final int actorUserId;
|
||||
final int createdAtMs;
|
||||
final int expiresAtMs;
|
||||
|
||||
bool get permanent => expiresAtMs == 0;
|
||||
|
||||
factory RoomBanState.fromJson(Map<String, dynamic> json) {
|
||||
return RoomBanState(
|
||||
userId: parseIntLike(json['user_id']),
|
||||
actorUserId: parseIntLike(json['actor_user_id']),
|
||||
createdAtMs: parseIntLike(json['created_at_ms']),
|
||||
expiresAtMs: parseIntLike(json['expires_at_ms']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomBannedUsersResult {
|
||||
RoomBannedUsersResult({
|
||||
required this.roomId,
|
||||
required this.items,
|
||||
required this.total,
|
||||
required this.page,
|
||||
required this.pageSize,
|
||||
required this.serverTimeMs,
|
||||
});
|
||||
|
||||
final String roomId;
|
||||
final List<RoomBannedUserItem> items;
|
||||
final int total;
|
||||
final int page;
|
||||
final int pageSize;
|
||||
final int serverTimeMs;
|
||||
|
||||
factory RoomBannedUsersResult.fromJson(Map<String, dynamic> json) {
|
||||
return RoomBannedUsersResult(
|
||||
roomId: json['room_id'] as String? ?? '',
|
||||
items: (json['items'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(RoomBannedUserItem.fromJson)
|
||||
.toList(),
|
||||
total: parseIntLike(json['total']),
|
||||
page: parseIntLike(json['page']),
|
||||
pageSize: parseIntLike(json['page_size']),
|
||||
serverTimeMs: parseIntLike(json['server_time_ms']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoomBannedUserItem {
|
||||
RoomBannedUserItem({
|
||||
required this.userId,
|
||||
required this.displayUserId,
|
||||
required this.username,
|
||||
required this.avatar,
|
||||
required this.country,
|
||||
required this.countryName,
|
||||
required this.countryDisplayName,
|
||||
required this.countryFlag,
|
||||
required this.actorUserId,
|
||||
required this.createdAtMs,
|
||||
required this.unbanAtMs,
|
||||
required this.remainingMs,
|
||||
required this.permanent,
|
||||
});
|
||||
|
||||
final int userId;
|
||||
final String displayUserId;
|
||||
final String username;
|
||||
final String avatar;
|
||||
final String country;
|
||||
final String countryName;
|
||||
final String countryDisplayName;
|
||||
final String countryFlag;
|
||||
final int actorUserId;
|
||||
final int createdAtMs;
|
||||
final int unbanAtMs;
|
||||
final int remainingMs;
|
||||
final bool permanent;
|
||||
|
||||
factory RoomBannedUserItem.fromJson(Map<String, dynamic> json) {
|
||||
return RoomBannedUserItem(
|
||||
userId: parseIntLike(json['user_id']),
|
||||
displayUserId: json['display_user_id'] as String? ?? '',
|
||||
username: json['username'] as String? ?? '',
|
||||
avatar: json['avatar'] as String? ?? '',
|
||||
country: json['country'] as String? ?? '',
|
||||
countryName: json['country_name'] as String? ?? '',
|
||||
countryDisplayName: json['country_display_name'] as String? ?? '',
|
||||
countryFlag: json['country_flag'] as String? ?? '',
|
||||
actorUserId: parseIntLike(json['actor_user_id']),
|
||||
createdAtMs: parseIntLike(json['created_at_ms']),
|
||||
unbanAtMs: parseIntLike(json['unban_at_ms']),
|
||||
remainingMs: parseIntLike(json['remaining_ms']),
|
||||
permanent: json['permanent'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1069,6 +1069,51 @@ paths:
|
||||
$ref: "#/responses/Conflict"
|
||||
"502":
|
||||
$ref: "#/responses/UpstreamError"
|
||||
/api/v1/rooms/{room_id}/banned-users:
|
||||
get:
|
||||
tags:
|
||||
- rooms
|
||||
summary: 查询房间黑名单列表
|
||||
operationId: listRoomBannedUsers
|
||||
description: 房主或房管分页查看当前仍有效的房间踢人列表;已过期和已解封用户不返回。
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- name: room_id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
pattern: "^[A-Za-z0-9_-]{1,48}$"
|
||||
maxLength: 48
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
type: integer
|
||||
format: int32
|
||||
default: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
required: false
|
||||
type: integer
|
||||
format: int32
|
||||
default: 20
|
||||
responses:
|
||||
"200":
|
||||
description: 返回房间当前仍有效的黑名单用户及展示资料。
|
||||
schema:
|
||||
$ref: "#/definitions/RoomBannedUsersEnvelope"
|
||||
"400":
|
||||
$ref: "#/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/responses/NotFound"
|
||||
"409":
|
||||
$ref: "#/responses/Conflict"
|
||||
"502":
|
||||
$ref: "#/responses/UpstreamError"
|
||||
/api/v1/rooms/{room_id}/follow:
|
||||
post:
|
||||
tags:
|
||||
@ -4208,6 +4253,10 @@ definitions:
|
||||
target_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
duration_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久禁止进房;大于 0 表示从服务端当前 UTC 时间起禁止进房的毫秒时长。
|
||||
UnbanUserRequest:
|
||||
type: object
|
||||
properties:
|
||||
@ -4342,6 +4391,11 @@ definitions:
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
ban_states:
|
||||
type: array
|
||||
description: 房间 ban 的时间边界;expires_at_ms 为 0 表示永久。
|
||||
items:
|
||||
$ref: "#/definitions/RoomBanState"
|
||||
mute_user_ids:
|
||||
type: array
|
||||
items:
|
||||
@ -4362,6 +4416,76 @@ definitions:
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
RoomBanState:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
actor_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
created_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
expires_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久;非 0 表示该 UTC 毫秒后可重新进房。
|
||||
RoomBannedUserData:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: string
|
||||
display_user_id:
|
||||
type: string
|
||||
username:
|
||||
type: string
|
||||
avatar:
|
||||
type: string
|
||||
country:
|
||||
type: string
|
||||
country_name:
|
||||
type: string
|
||||
country_display_name:
|
||||
type: string
|
||||
country_flag:
|
||||
type: string
|
||||
actor_user_id:
|
||||
type: string
|
||||
created_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
unban_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久,需要手动解除;非 0 表示自动解封 UTC 毫秒。
|
||||
remaining_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
permanent:
|
||||
type: boolean
|
||||
RoomBannedUsersData:
|
||||
type: object
|
||||
properties:
|
||||
room_id:
|
||||
type: string
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/RoomBannedUserData"
|
||||
total:
|
||||
type: integer
|
||||
format: int64
|
||||
page:
|
||||
type: integer
|
||||
format: int32
|
||||
page_size:
|
||||
type: integer
|
||||
format: int32
|
||||
server_time_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
RoomListItemData:
|
||||
type: object
|
||||
required:
|
||||
@ -5201,6 +5325,13 @@ definitions:
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/UnbanUserResponse"
|
||||
RoomBannedUsersEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: "#/definitions/RoomBannedUsersData"
|
||||
SendGiftEnvelope:
|
||||
allOf:
|
||||
- $ref: "#/definitions/GatewayOKEnvelopeBase"
|
||||
|
||||
@ -411,6 +411,27 @@ paths:
|
||||
$ref: "#/definitions/GetRoomSnapshotResponse"
|
||||
default:
|
||||
$ref: "#/responses/GRPCError"
|
||||
/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers:
|
||||
post:
|
||||
tags:
|
||||
- room-query
|
||||
summary: 查询房间黑名单列表
|
||||
operationId: roomListRoomBannedUsers
|
||||
description: 读取 Room Cell 当前仍有效的 ban 状态;viewer 必须是当前房间 owner/admin。
|
||||
x-grpc-full-method: /hyapp.room.v1.RoomQueryService/ListRoomBannedUsers
|
||||
parameters:
|
||||
- name: body
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ListRoomBannedUsersRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: 返回当前仍有效的房间黑名单。
|
||||
schema:
|
||||
$ref: "#/definitions/ListRoomBannedUsersResponse"
|
||||
default:
|
||||
$ref: "#/responses/GRPCError"
|
||||
/grpc.health.v1.Health/Check:
|
||||
post:
|
||||
tags:
|
||||
@ -587,6 +608,11 @@ definitions:
|
||||
items:
|
||||
type: integer
|
||||
format: int64
|
||||
ban_states:
|
||||
type: array
|
||||
description: 房间 ban 的时间边界;expires_at_ms 为 0 表示永久。
|
||||
items:
|
||||
$ref: "#/definitions/RoomBanState"
|
||||
mute_user_ids:
|
||||
type: array
|
||||
items:
|
||||
@ -607,6 +633,22 @@ definitions:
|
||||
version:
|
||||
type: integer
|
||||
format: int64
|
||||
RoomBanState:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
actor_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
created_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
expires_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久;非 0 表示该 UTC 毫秒后可重新进房。
|
||||
CreateRoomRequest:
|
||||
type: object
|
||||
description: 同一个 `meta.actor_user_id` 只能作为 owner 创建一个房间;owner/host 不接受外部字段自报。
|
||||
@ -882,6 +924,10 @@ definitions:
|
||||
target_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
duration_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久禁止进房;大于 0 表示从服务端当前 UTC 时间起禁止进房的毫秒时长。
|
||||
KickUserResponse:
|
||||
type: object
|
||||
properties:
|
||||
@ -1052,6 +1098,63 @@ definitions:
|
||||
server_time_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
RoomBannedUser:
|
||||
type: object
|
||||
properties:
|
||||
user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
actor_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
created_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
unban_at_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 0 表示永久;非 0 表示自动解封 UTC 毫秒。
|
||||
remaining_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
permanent:
|
||||
type: boolean
|
||||
ListRoomBannedUsersRequest:
|
||||
type: object
|
||||
properties:
|
||||
meta:
|
||||
$ref: "#/definitions/RequestMeta"
|
||||
room_id:
|
||||
type: string
|
||||
viewer_user_id:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 由 gateway 从 token user_id 注入,客户端不能自报。
|
||||
page:
|
||||
type: integer
|
||||
format: int32
|
||||
page_size:
|
||||
type: integer
|
||||
format: int32
|
||||
ListRoomBannedUsersResponse:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/RoomBannedUser"
|
||||
total:
|
||||
type: integer
|
||||
format: int64
|
||||
page:
|
||||
type: integer
|
||||
format: int32
|
||||
page_size:
|
||||
type: integer
|
||||
format: int32
|
||||
server_time_ms:
|
||||
type: integer
|
||||
format: int64
|
||||
RoomSystemMessage:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@ -52,6 +52,7 @@ type RoomQueryClient interface {
|
||||
ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error)
|
||||
GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error)
|
||||
ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error)
|
||||
ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error)
|
||||
}
|
||||
|
||||
type grpcRoomClient struct {
|
||||
@ -222,3 +223,7 @@ func (c *grpcRoomQueryClient) GetRoomTreasure(ctx context.Context, req *roomv1.G
|
||||
func (c *grpcRoomQueryClient) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
|
||||
return c.client.ListRoomOnlineUsers(ctx, req)
|
||||
}
|
||||
|
||||
func (c *grpcRoomQueryClient) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
return c.client.ListRoomBannedUsers(ctx, req)
|
||||
}
|
||||
|
||||
@ -120,6 +120,14 @@ func (f *fakeUserLeaderboardRoomQueryClient) GetRoomTreasure(context.Context, *r
|
||||
return &roomv1.GetRoomTreasureResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBackgrounds(context.Context, *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
|
||||
return &roomv1.ListRoomBackgroundsResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomOnlineUsers(context.Context, *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
|
||||
return &roomv1.ListRoomOnlineUsersResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBannedUsers(context.Context, *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
return &roomv1.ListRoomBannedUsersResponse{}, nil
|
||||
}
|
||||
|
||||
@ -100,6 +100,7 @@ type RoomHandlers struct {
|
||||
GetRoomSnapshot http.HandlerFunc
|
||||
GetRoomDetail http.HandlerFunc
|
||||
ListRoomOnlineUsers http.HandlerFunc
|
||||
ListRoomBannedUsers http.HandlerFunc
|
||||
GetRoomGiftPanel http.HandlerFunc
|
||||
GetRoomTreasure http.HandlerFunc
|
||||
FollowRoom http.HandlerFunc
|
||||
@ -312,6 +313,7 @@ func (r routes) registerRoomRoutes() {
|
||||
r.profile("/rooms/snapshot", http.MethodGet, h.GetRoomSnapshot)
|
||||
r.profile("/rooms/{room_id}/detail", http.MethodGet, h.GetRoomDetail)
|
||||
r.profile("/rooms/{room_id}/online-users", http.MethodGet, h.ListRoomOnlineUsers)
|
||||
r.profile("/rooms/{room_id}/banned-users", http.MethodGet, h.ListRoomBannedUsers)
|
||||
r.profile("/rooms/{room_id}/gift-panel", http.MethodGet, h.GetRoomGiftPanel)
|
||||
r.profile("/rooms/{room_id}/treasure", http.MethodGet, h.GetRoomTreasure)
|
||||
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom)
|
||||
|
||||
@ -344,6 +344,7 @@ type fakeRoomQueryClient struct {
|
||||
lastBackgrounds *roomv1.ListRoomBackgroundsRequest
|
||||
lastTreasure *roomv1.GetRoomTreasureRequest
|
||||
lastOnline *roomv1.ListRoomOnlineUsersRequest
|
||||
lastBannedUsers *roomv1.ListRoomBannedUsersRequest
|
||||
resp *roomv1.ListRoomsResponse
|
||||
feedsResp *roomv1.ListRoomsResponse
|
||||
myRoomResp *roomv1.GetMyRoomResponse
|
||||
@ -352,6 +353,7 @@ type fakeRoomQueryClient struct {
|
||||
backgroundsResp *roomv1.ListRoomBackgroundsResponse
|
||||
treasureResp *roomv1.GetRoomTreasureResponse
|
||||
onlineResp *roomv1.ListRoomOnlineUsersResponse
|
||||
bannedUsersResp *roomv1.ListRoomBannedUsersResponse
|
||||
err error
|
||||
feedsErr error
|
||||
myRoomErr error
|
||||
@ -360,6 +362,7 @@ type fakeRoomQueryClient struct {
|
||||
backgroundsErr error
|
||||
treasureErr error
|
||||
onlineErr error
|
||||
bannedUsersErr error
|
||||
}
|
||||
|
||||
type fakeUserDeviceClient struct {
|
||||
@ -840,6 +843,17 @@ func (f *fakeRoomQueryClient) ListRoomOnlineUsers(_ context.Context, req *roomv1
|
||||
return &roomv1.ListRoomOnlineUsersResponse{Page: req.GetPage(), PageSize: req.GetPageSize()}, nil
|
||||
}
|
||||
|
||||
func (f *fakeRoomQueryClient) ListRoomBannedUsers(_ context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
f.lastBannedUsers = req
|
||||
if f.bannedUsersErr != nil {
|
||||
return nil, f.bannedUsersErr
|
||||
}
|
||||
if f.bannedUsersResp != nil {
|
||||
return f.bannedUsersResp, nil
|
||||
}
|
||||
return &roomv1.ListRoomBannedUsersResponse{Page: req.GetPage(), PageSize: req.GetPageSize()}, nil
|
||||
}
|
||||
|
||||
func (f *fakeUserDeviceClient) BindPushToken(_ context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
|
||||
f.lastBind = req
|
||||
if f.bindErr != nil {
|
||||
@ -1619,7 +1633,7 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
||||
{
|
||||
name: "kick",
|
||||
path: "/api/v1/rooms/user/kick",
|
||||
body: `{"room_id":"room-1","command_id":"cmd-kick-1","target_user_id":43}`,
|
||||
body: `{"room_id":"room-1","command_id":"cmd-kick-1","target_user_id":43,"duration_ms":600000}`,
|
||||
commandID: "cmd-kick-1",
|
||||
extractMeta: func(client *fakeRoomClient) *roomv1.RequestMeta {
|
||||
return client.lastKick.GetMeta()
|
||||
@ -1674,6 +1688,9 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
|
||||
if meta.GetActorUserId() != 42 {
|
||||
t.Fatalf("actor user mismatch: %+v", meta)
|
||||
}
|
||||
if test.name == "kick" && client.lastKick.GetDurationMs() != 600000 {
|
||||
t.Fatalf("kick duration_ms was not forwarded: %+v", client.lastKick)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -2347,6 +2364,57 @@ func TestListRoomOnlineUsersIncludesProfileGenderAndAge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomBannedUsersIncludesProfileCountryAndUnbanTime(t *testing.T) {
|
||||
queryClient := &fakeRoomQueryClient{bannedUsersResp: &roomv1.ListRoomBannedUsersResponse{
|
||||
Items: []*roomv1.RoomBannedUser{
|
||||
{
|
||||
UserId: 43,
|
||||
ActorUserId: 42,
|
||||
CreatedAtMs: 1000,
|
||||
UnbanAtMs: 7000,
|
||||
RemainingMs: 6000,
|
||||
Permanent: false,
|
||||
},
|
||||
},
|
||||
Total: 1,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
ServerTimeMs: 2000,
|
||||
}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetRoomQueryClient(queryClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/room-banned/banned-users?page=1&page_size=20", nil)
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-room-banned-users")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if queryClient.lastBannedUsers == nil || queryClient.lastBannedUsers.GetRoomId() != "room-banned" || queryClient.lastBannedUsers.GetViewerUserId() != 42 || queryClient.lastBannedUsers.GetPageSize() != 20 {
|
||||
t.Fatalf("banned users request mismatch: %+v", queryClient.lastBannedUsers)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode banned users response failed: %v", err)
|
||||
}
|
||||
data, ok := response.Data.(map[string]any)
|
||||
items, itemsOK := data["items"].([]any)
|
||||
if response.Code != httpkit.CodeOK || !ok || !itemsOK || len(items) != 1 {
|
||||
t.Fatalf("banned users response mismatch: %+v", response)
|
||||
}
|
||||
first, ok := items[0].(map[string]any)
|
||||
if !ok || first["user_id"] != "43" || first["actor_user_id"] != "42" || first["unban_at_ms"] != float64(7000) || first["remaining_ms"] != float64(6000) {
|
||||
t.Fatalf("banned user item mismatch: %+v", first)
|
||||
}
|
||||
if first["username"] != "user-43" || first["avatar"] != "https://cdn.example/avatar.png" || first["country"] != "US" || first["country_flag"] != "🇺🇸" {
|
||||
t.Fatalf("banned user profile fields mismatch: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) {
|
||||
queryClient := &fakeRoomQueryClient{}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
|
||||
@ -51,6 +51,7 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
|
||||
GetRoomSnapshot: h.getRoomSnapshot,
|
||||
GetRoomDetail: h.getRoomDetail,
|
||||
ListRoomOnlineUsers: h.listRoomOnlineUsers,
|
||||
ListRoomBannedUsers: h.listRoomBannedUsers,
|
||||
GetRoomGiftPanel: h.getRoomGiftPanel,
|
||||
GetRoomTreasure: h.getRoomTreasure,
|
||||
FollowRoom: h.handleRoomFollow,
|
||||
|
||||
@ -475,6 +475,46 @@ func (h *Handler) listRoomOnlineUsers(writer http.ResponseWriter, request *http.
|
||||
httpkit.WriteOK(writer, request, roomOnlineUserDataFromProto(resp, h.roomDisplayProfileMap(request, userIDs)))
|
||||
}
|
||||
|
||||
// listRoomBannedUsers 分页返回房间治理面板使用的当前有效黑名单。
|
||||
func (h *Handler) listRoomBannedUsers(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.roomQueryClient == nil {
|
||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||
return
|
||||
}
|
||||
roomID := strings.TrimSpace(request.PathValue("room_id"))
|
||||
if !roomid.ValidStringID(roomID) {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
|
||||
if !ok {
|
||||
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.roomQueryClient.ListRoomBannedUsers(request.Context(), &roomv1.ListRoomBannedUsersRequest{
|
||||
Meta: httpkit.RoomMeta(request, roomID, ""),
|
||||
RoomId: roomID,
|
||||
ViewerUserId: auth.UserIDFromContext(request.Context()),
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
userIDs := make([]int64, 0, len(resp.GetItems()))
|
||||
for _, item := range resp.GetItems() {
|
||||
userIDs = append(userIDs, item.GetUserId())
|
||||
}
|
||||
httpkit.WriteOK(writer, request, roomBannedUsersDataFromProto(roomID, resp, h.roomDisplayProfileMap(request, userIDs)))
|
||||
}
|
||||
|
||||
// getRoomGiftPanel 返回房间送礼面板初始化数据。
|
||||
func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.roomQueryClient == nil || h.walletClient == nil {
|
||||
@ -1159,6 +1199,7 @@ func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
|
||||
RoomID string `json:"room_id"`
|
||||
CommandID string `json:"command_id"`
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
if !httpkit.Decode(writer, request, &body) {
|
||||
@ -1168,6 +1209,7 @@ func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
|
||||
resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
|
||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||
TargetUserId: body.TargetUserID,
|
||||
DurationMs: body.DurationMS,
|
||||
})
|
||||
httpkit.Write(writer, request, resp, err)
|
||||
}
|
||||
|
||||
@ -92,6 +92,31 @@ type roomOnlineUserData struct {
|
||||
Profile *roomDisplayProfileData `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
type roomBannedUsersData struct {
|
||||
RoomID string `json:"room_id"`
|
||||
Items []roomBannedUserData `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int32 `json:"page"`
|
||||
PageSize int32 `json:"page_size"`
|
||||
ServerTimeMS int64 `json:"server_time_ms"`
|
||||
}
|
||||
|
||||
type roomBannedUserData struct {
|
||||
UserID string `json:"user_id"`
|
||||
DisplayUserID string `json:"display_user_id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
Country string `json:"country"`
|
||||
CountryName string `json:"country_name"`
|
||||
CountryDisplayName string `json:"country_display_name"`
|
||||
CountryFlag string `json:"country_flag"`
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
CreatedAtMS int64 `json:"created_at_ms"`
|
||||
UnbanAtMS int64 `json:"unban_at_ms"`
|
||||
RemainingMS int64 `json:"remaining_ms"`
|
||||
Permanent bool `json:"permanent"`
|
||||
}
|
||||
|
||||
type setRoomAdminData struct {
|
||||
Result roomCommandResultData `json:"result"`
|
||||
TargetUserID string `json:"target_user_id"`
|
||||
@ -783,6 +808,41 @@ func roomOnlineUserDataFromProto(resp *roomv1.ListRoomOnlineUsersResponse, profi
|
||||
}
|
||||
}
|
||||
|
||||
func roomBannedUsersDataFromProto(roomID string, resp *roomv1.ListRoomBannedUsersResponse, profiles map[int64]roomDisplayProfileData) roomBannedUsersData {
|
||||
if resp == nil {
|
||||
return roomBannedUsersData{RoomID: roomID}
|
||||
}
|
||||
items := make([]roomBannedUserData, 0, len(resp.GetItems()))
|
||||
for _, user := range resp.GetItems() {
|
||||
item := roomBannedUserData{
|
||||
UserID: formatOptionalUserID(user.GetUserId()),
|
||||
ActorUserID: formatOptionalUserID(user.GetActorUserId()),
|
||||
CreatedAtMS: user.GetCreatedAtMs(),
|
||||
UnbanAtMS: user.GetUnbanAtMs(),
|
||||
RemainingMS: user.GetRemainingMs(),
|
||||
Permanent: user.GetPermanent(),
|
||||
}
|
||||
if profile, ok := profiles[user.GetUserId()]; ok {
|
||||
item.DisplayUserID = profile.DisplayUserID
|
||||
item.Username = profile.Username
|
||||
item.Avatar = profile.Avatar
|
||||
item.Country = profile.Country
|
||||
item.CountryName = profile.CountryName
|
||||
item.CountryDisplayName = profile.CountryDisplayName
|
||||
item.CountryFlag = profile.CountryFlag
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return roomBannedUsersData{
|
||||
RoomID: roomID,
|
||||
Items: items,
|
||||
Total: resp.GetTotal(),
|
||||
Page: resp.GetPage(),
|
||||
PageSize: resp.GetPageSize(),
|
||||
ServerTimeMS: resp.GetServerTimeMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func roomOnlineUsersFromLegacyUsers(users []*roomv1.RoomUser) []*roomv1.RoomOnlineUser {
|
||||
items := make([]*roomv1.RoomOnlineUser, 0, len(users))
|
||||
for _, user := range users {
|
||||
|
||||
@ -320,6 +320,8 @@ type KickUser struct {
|
||||
Base
|
||||
// TargetUserID 是被踢出并加入 ban 集合的用户。
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
// DurationMS 为 0 表示永久 ban;大于 0 表示从命令提交时间起禁止进房的时长。
|
||||
DurationMS int64 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
// Type 返回命令类型。
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/roomid"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/room-service/internal/room/state"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultRoomBannedUsersPageSize 是房间黑名单面板默认页大小。
|
||||
defaultRoomBannedUsersPageSize = 20
|
||||
// maxRoomBannedUsersPageSize 限制单次查询量,避免管理面板一次拉完整治理状态。
|
||||
maxRoomBannedUsersPageSize = 100
|
||||
)
|
||||
|
||||
// ListRoomBannedUsers 分页读取房间当前仍有效的黑名单。
|
||||
// 黑名单状态属于 Room Cell,用户头像、昵称、国家等展示资料由 gateway 批量查询 user-service。
|
||||
func (s *Service) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
|
||||
roomID := req.GetRoomId()
|
||||
viewerUserID := req.GetViewerUserId()
|
||||
if !roomid.ValidStringID(roomID) {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "room_id is invalid")
|
||||
}
|
||||
if viewerUserID <= 0 {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
|
||||
}
|
||||
|
||||
snapshot, err := s.currentSnapshot(ctx, roomID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if snapshot == nil || snapshot.GetRoomId() == "" {
|
||||
return nil, xerr.New(xerr.NotFound, "room not found")
|
||||
}
|
||||
if snapshot.GetStatus() != state.RoomStatusActive {
|
||||
return nil, xerr.New(xerr.RoomClosed, "room closed")
|
||||
}
|
||||
|
||||
current := state.FromProto(snapshot)
|
||||
if err := requireManagerPresent(current, viewerUserID); err != nil {
|
||||
// 黑名单是房间治理数据,只允许当前仍在房间内的 owner/admin 查看。
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nowMS := s.clock.Now().UnixMilli()
|
||||
items := activeBannedUsersFromState(current, nowMS)
|
||||
page, pageSize := normalizeRoomBannedUsersPage(req.GetPage(), req.GetPageSize())
|
||||
total := int64(len(items))
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(items) {
|
||||
items = nil
|
||||
} else {
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
items = items[start:end]
|
||||
}
|
||||
|
||||
return &roomv1.ListRoomBannedUsersResponse{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: int32(page),
|
||||
PageSize: int32(pageSize),
|
||||
ServerTimeMs: nowMS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func activeBannedUsersFromState(current *state.RoomState, nowMS int64) []*roomv1.RoomBannedUser {
|
||||
if current == nil || len(current.BanUsers) == 0 {
|
||||
return nil
|
||||
}
|
||||
items := make([]*roomv1.RoomBannedUser, 0, len(current.BanUsers))
|
||||
for _, ban := range current.BanUsers {
|
||||
if !ban.ActiveAt(nowMS) {
|
||||
continue
|
||||
}
|
||||
remainingMS := int64(0)
|
||||
if ban.ExpiresAtMS > 0 {
|
||||
remainingMS = ban.ExpiresAtMS - nowMS
|
||||
}
|
||||
items = append(items, &roomv1.RoomBannedUser{
|
||||
UserId: ban.UserID,
|
||||
ActorUserId: ban.ActorUserID,
|
||||
CreatedAtMs: ban.CreatedAtMS,
|
||||
UnbanAtMs: ban.ExpiresAtMS,
|
||||
RemainingMs: remainingMS,
|
||||
Permanent: ban.ExpiresAtMS == 0,
|
||||
})
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].GetCreatedAtMs() != items[j].GetCreatedAtMs() {
|
||||
return items[i].GetCreatedAtMs() > items[j].GetCreatedAtMs()
|
||||
}
|
||||
return items[i].GetUserId() < items[j].GetUserId()
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
func normalizeRoomBannedUsersPage(rawPage int32, rawPageSize int32) (int, int) {
|
||||
page := int(rawPage)
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := int(rawPageSize)
|
||||
if pageSize <= 0 {
|
||||
pageSize = defaultRoomBannedUsersPageSize
|
||||
}
|
||||
if pageSize > maxRoomBannedUsersPageSize {
|
||||
pageSize = maxRoomBannedUsersPageSize
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
@ -41,7 +41,7 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !currentRoomSnapshotRestorable(snapshot, userID) {
|
||||
if !currentRoomSnapshotRestorable(snapshot, userID, serverTimeMS) {
|
||||
// 查询语义必须保持只读;读模型滞后由后续命令投影或专项补偿修复。
|
||||
return emptyCurrentRoomResponse(serverTimeMS), nil
|
||||
}
|
||||
@ -81,12 +81,12 @@ func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse
|
||||
}
|
||||
|
||||
// currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。
|
||||
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool {
|
||||
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
|
||||
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
|
||||
// 非 active 房间不能恢复,即使用户级 presence 投影还没被清理。
|
||||
return false
|
||||
}
|
||||
if containsUserID(snapshot.GetBanUserIds(), userID) {
|
||||
if snapshotUserBanned(snapshot, userID, nowMS) {
|
||||
// ban 优先于旧 presence,避免被踢用户通过本地恢复入口重新收消息。
|
||||
return false
|
||||
}
|
||||
|
||||
@ -35,8 +35,10 @@ func (s *Service) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpe
|
||||
}, nil
|
||||
}
|
||||
|
||||
nowMS := s.clock.Now().UnixMilli()
|
||||
banned := snapshotUserBanned(snapshot, req.GetUserId(), nowMS)
|
||||
// 允许发言必须同时满足未 ban、未 mute、房间开启聊天、用户在业务 presence 内。
|
||||
if !containsID(snapshot.GetBanUserIds(), req.GetUserId()) && !containsID(snapshot.GetMuteUserIds(), req.GetUserId()) && snapshot.GetChatEnabled() && findProtoUser(snapshot, req.GetUserId()) != nil {
|
||||
if !banned && !containsID(snapshot.GetMuteUserIds(), req.GetUserId()) && snapshot.GetChatEnabled() && findProtoUser(snapshot, req.GetUserId()) != nil {
|
||||
return &roomv1.CheckSpeakPermissionResponse{
|
||||
Allowed: true,
|
||||
RoomVersion: snapshot.GetVersion(),
|
||||
@ -45,7 +47,7 @@ func (s *Service) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpe
|
||||
|
||||
reason := "not_in_room"
|
||||
switch {
|
||||
case containsID(snapshot.GetBanUserIds(), req.GetUserId()):
|
||||
case banned:
|
||||
// ban 优先级最高,被踢用户即使仍在腾讯云 IM 群内也不能发言。
|
||||
reason = "user_banned"
|
||||
case containsID(snapshot.GetMuteUserIds(), req.GetUserId()):
|
||||
@ -89,7 +91,7 @@ func (s *Service) VerifyRoomPresence(ctx context.Context, req *roomv1.VerifyRoom
|
||||
}, nil
|
||||
}
|
||||
|
||||
if containsID(snapshot.GetBanUserIds(), req.GetUserId()) {
|
||||
if snapshotUserBanned(snapshot, req.GetUserId(), s.clock.Now().UnixMilli()) {
|
||||
// ban 用户不能订阅,即使快照里还有旧 presence 也以 ban 为准。
|
||||
return &roomv1.VerifyRoomPresenceResponse{
|
||||
Present: false,
|
||||
|
||||
@ -154,6 +154,21 @@ func containsID(values []int64, target int64) bool {
|
||||
return slices.Contains(values, target)
|
||||
}
|
||||
|
||||
func snapshotUserBanned(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
|
||||
if snapshot == nil || userID <= 0 {
|
||||
return false
|
||||
}
|
||||
for _, ban := range snapshot.GetBanStates() {
|
||||
if ban.GetUserId() != userID {
|
||||
continue
|
||||
}
|
||||
// ban_states 是带时间边界的权威状态;过期后即使 ban_user_ids 还没刷新也不能继续拦截。
|
||||
return ban.GetExpiresAtMs() == 0 || ban.GetExpiresAtMs() > nowMS
|
||||
}
|
||||
// 旧快照没有 ban_states 时只能按永久 ban 解释 ban_user_ids,避免恢复路径放宽权限。
|
||||
return containsID(snapshot.GetBanUserIds(), userID)
|
||||
}
|
||||
|
||||
func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
|
||||
// SendGift 响应需要 protobuf 榜单项,不能直接暴露内部 state.RankItem。
|
||||
result := make([]*roomv1.RankItem, 0, len(items))
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/room-service/internal/integration"
|
||||
roomoutbox "hyapp/services/room-service/internal/room/outbox"
|
||||
roomservice "hyapp/services/room-service/internal/room/service"
|
||||
@ -100,6 +101,160 @@ func TestKickUserRemovesPresenceBanAndRTCConnection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKickUserDurationExpiresAndAllowsJoin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 28, 8, 0, 0, 0, time.UTC)}
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
NodeID: "node-kick-duration-test",
|
||||
LeaseTTL: 10 * time.Second,
|
||||
RankLimit: 20,
|
||||
SnapshotEveryN: 1,
|
||||
Clock: now,
|
||||
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||
|
||||
roomID := "room-kick-duration"
|
||||
ownerID := int64(7301)
|
||||
viewerID := int64(7401)
|
||||
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||
SeatCount: 10,
|
||||
Mode: "voice",
|
||||
VisibleRegionId: 8101,
|
||||
RoomName: "Kick Duration",
|
||||
RoomShortId: "kick-duration",
|
||||
}); err != nil {
|
||||
t.Fatalf("create duration room fixture failed: %v", err)
|
||||
}
|
||||
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||
Role: "audience",
|
||||
}); err != nil {
|
||||
t.Fatalf("join duration target failed: %v", err)
|
||||
}
|
||||
|
||||
kickResp, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||
TargetUserId: viewerID,
|
||||
DurationMs: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("duration kick failed: %v", err)
|
||||
}
|
||||
if len(kickResp.GetRoom().GetBanStates()) != 1 || kickResp.GetRoom().GetBanStates()[0].GetExpiresAtMs() != now.Now().Add(time.Second).UnixMilli() {
|
||||
t.Fatalf("duration kick must persist ban expiration: %+v", kickResp.GetRoom().GetBanStates())
|
||||
}
|
||||
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||
Role: "audience",
|
||||
}); err == nil {
|
||||
t.Fatalf("target must not join before kick duration expires")
|
||||
}
|
||||
|
||||
presenceResp, err := svc.VerifyRoomPresence(ctx, &roomv1.VerifyRoomPresenceRequest{
|
||||
AppCode: appcode.Default,
|
||||
RoomId: roomID,
|
||||
UserId: viewerID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("verify presence before expiration failed: %v", err)
|
||||
}
|
||||
if presenceResp.GetPresent() || presenceResp.GetReason() != "user_banned" {
|
||||
t.Fatalf("active duration ban must reject IM guard: %+v", presenceResp)
|
||||
}
|
||||
|
||||
now.now = now.now.Add(2 * time.Second)
|
||||
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||
Role: "audience",
|
||||
}); err != nil {
|
||||
t.Fatalf("target must join after kick duration expires: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomBannedUsersReturnsActiveOnlyForManagers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)}
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
NodeID: "node-banned-users-test",
|
||||
LeaseTTL: 10 * time.Second,
|
||||
RankLimit: 20,
|
||||
SnapshotEveryN: 1,
|
||||
Clock: now,
|
||||
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||
|
||||
roomID := "room-banned-users"
|
||||
ownerID := int64(7501)
|
||||
expiredID := int64(7502)
|
||||
activeID := int64(7503)
|
||||
normalID := int64(7504)
|
||||
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||
SeatCount: 10,
|
||||
Mode: "voice",
|
||||
VisibleRegionId: 8101,
|
||||
RoomName: "Banned Users",
|
||||
RoomShortId: "banned-users",
|
||||
}); err != nil {
|
||||
t.Fatalf("create banned users room failed: %v", err)
|
||||
}
|
||||
for _, userID := range []int64{expiredID, activeID, normalID} {
|
||||
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, userID),
|
||||
Role: "audience",
|
||||
}); err != nil {
|
||||
t.Fatalf("join fixture user %d failed: %v", userID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||
TargetUserId: expiredID,
|
||||
DurationMs: 1000,
|
||||
}); err != nil {
|
||||
t.Fatalf("kick expired fixture user failed: %v", err)
|
||||
}
|
||||
now.now = now.now.Add(2 * time.Second)
|
||||
activeCreatedAt := now.Now().UnixMilli()
|
||||
activeUnbanAt := now.Now().Add(10 * time.Second).UnixMilli()
|
||||
if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
|
||||
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||
TargetUserId: activeID,
|
||||
DurationMs: 10_000,
|
||||
}); err != nil {
|
||||
t.Fatalf("kick active fixture user failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := svc.ListRoomBannedUsers(ctx, &roomv1.ListRoomBannedUsersRequest{
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||
RoomId: roomID,
|
||||
ViewerUserId: normalID,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}); !xerr.IsCode(err, xerr.PermissionDenied) {
|
||||
t.Fatalf("non-manager list banned users must be denied: %v", err)
|
||||
}
|
||||
|
||||
resp, err := svc.ListRoomBannedUsers(ctx, &roomv1.ListRoomBannedUsersRequest{
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
|
||||
RoomId: roomID,
|
||||
ViewerUserId: ownerID,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("owner list banned users failed: %v", err)
|
||||
}
|
||||
if resp.GetTotal() != 1 || len(resp.GetItems()) != 1 {
|
||||
t.Fatalf("only active ban must be returned: %+v", resp)
|
||||
}
|
||||
item := resp.GetItems()[0]
|
||||
if item.GetUserId() != activeID || item.GetActorUserId() != ownerID || item.GetCreatedAtMs() != activeCreatedAt || item.GetUnbanAtMs() != activeUnbanAt || item.GetRemainingMs() != 10_000 || item.GetPermanent() {
|
||||
t.Fatalf("active ban item mismatch: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseRoomByAdminEvictsCurrentUsersThroughOutboxAndRTC(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
|
||||
@ -94,6 +94,7 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
|
||||
cmd := command.KickUser{
|
||||
Base: baseFromMeta(req.GetMeta()),
|
||||
TargetUserID: req.GetTargetUserId(),
|
||||
DurationMS: req.GetDurationMs(),
|
||||
}
|
||||
|
||||
shouldKickRTC := false
|
||||
@ -107,6 +108,9 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
|
||||
if cmd.TargetUserID == cmd.ActorUserID() {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot kick self")
|
||||
}
|
||||
if cmd.DurationMS < 0 {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms must be non-negative")
|
||||
}
|
||||
if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
@ -116,10 +120,6 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
|
||||
|
||||
_, inRoom := current.OnlineUsers[cmd.TargetUserID]
|
||||
_, onSeat := current.SeatByUser(cmd.TargetUserID)
|
||||
if current.BanUsers[cmd.TargetUserID] && !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] {
|
||||
// 已处于 ban 且没有残留 presence、麦位或管理员身份时,重复 KickUser 不产生新事件。
|
||||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||
}
|
||||
// 只有目标确实还在业务房间或麦位上,才需要请求 TRTC 服务端移除实时音频连接。
|
||||
shouldKickRTC = inRoom || onSeat
|
||||
|
||||
@ -136,7 +136,20 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
|
||||
// ban 集合会让后续 JoinRoom 和 VerifyRoomPresence 都失败。
|
||||
delete(current.OnlineUsers, cmd.TargetUserID)
|
||||
delete(current.AdminUsers, cmd.TargetUserID)
|
||||
current.BanUsers[cmd.TargetUserID] = true
|
||||
expiresAtMS := int64(0)
|
||||
nowMS := now.UnixMilli()
|
||||
if cmd.DurationMS > 0 {
|
||||
expiresAtMS = nowMS + cmd.DurationMS
|
||||
if expiresAtMS <= nowMS {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms is too large")
|
||||
}
|
||||
}
|
||||
current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
|
||||
UserID: cmd.TargetUserID,
|
||||
ActorUserID: cmd.ActorUserID(),
|
||||
CreatedAtMS: nowMS,
|
||||
ExpiresAtMS: expiresAtMS,
|
||||
}
|
||||
current.Version++
|
||||
|
||||
kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
|
||||
@ -207,7 +220,7 @@ func (s *Service) UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (
|
||||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
if !current.BanUsers[cmd.TargetUserID] {
|
||||
if _, exists := current.BanUsers[cmd.TargetUserID]; !exists {
|
||||
// 重复 unban 是 no-op,仍写 command log 确保 command_id 幂等可判定。
|
||||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||
}
|
||||
|
||||
@ -38,9 +38,13 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
|
||||
}
|
||||
|
||||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||||
if current.BanUsers[cmd.ActorUserID()] {
|
||||
// 被踢或封禁用户不能重新进入业务 presence。
|
||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
|
||||
if ban, exists := current.BanUsers[cmd.ActorUserID()]; exists {
|
||||
if ban.ActiveAt(now.UnixMilli()) {
|
||||
// 被踢或封禁用户在 ban 未过期前不能重新进入业务 presence。
|
||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
|
||||
}
|
||||
// 限时 ban 过期后由下一次进房命令清理,避免定时任务介入 Room Cell owner 状态。
|
||||
delete(current.BanUsers, cmd.ActorUserID())
|
||||
}
|
||||
|
||||
if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists {
|
||||
@ -138,9 +142,13 @@ func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRe
|
||||
if err := requireActiveRoom(current); err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
if current.BanUsers[cmd.ActorUserID()] {
|
||||
// ban 用户不能通过心跳续住业务 presence;KickUser 已经清理 presence,这里是防御脏状态。
|
||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
|
||||
if ban, exists := current.BanUsers[cmd.ActorUserID()]; exists {
|
||||
if ban.ActiveAt(now.UnixMilli()) {
|
||||
// ban 用户不能通过心跳续住业务 presence;KickUser 已经清理 presence,这里是防御脏状态。
|
||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
|
||||
}
|
||||
// 过期 ban 不再阻断心跳;保守清理后继续按 presence 是否存在判断。
|
||||
delete(current.BanUsers, cmd.ActorUserID())
|
||||
}
|
||||
|
||||
existing, exists := current.OnlineUsers[cmd.ActorUserID()]
|
||||
|
||||
@ -108,7 +108,7 @@ func (s *Service) recoverRoom(ctx context.Context, roomMeta RoomMeta) (*state.Ro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := replay(recovered, cmd); err != nil {
|
||||
if err := replay(recovered, cmd, record.CreatedAtMS); err != nil {
|
||||
// replay 失败说明命令日志无法被当前状态机解释,不能继续装载错误 Cell。
|
||||
return nil, err
|
||||
}
|
||||
@ -118,7 +118,7 @@ func (s *Service) recoverRoom(ctx context.Context, roomMeta RoomMeta) (*state.Ro
|
||||
}
|
||||
|
||||
// replay 只重放房间内确定性状态变更,不重复执行钱包扣费或腾讯云 IM 投递。
|
||||
func replay(current *state.RoomState, cmd command.Command) error {
|
||||
func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) error {
|
||||
// replay 只重建房间内状态,不重复写 outbox、不重复调用 wallet、不重复同步腾讯云 IM。
|
||||
switch typed := cmd.(type) {
|
||||
case *command.CreateRoom:
|
||||
@ -340,7 +340,20 @@ func replay(current *state.RoomState, cmd command.Command) error {
|
||||
}
|
||||
delete(current.OnlineUsers, typed.TargetUserID)
|
||||
delete(current.AdminUsers, typed.TargetUserID)
|
||||
current.BanUsers[typed.TargetUserID] = true
|
||||
createdAtMS := committedAtMS
|
||||
if createdAtMS <= 0 {
|
||||
createdAtMS = typed.SentAtMS
|
||||
}
|
||||
expiresAtMS := int64(0)
|
||||
if typed.DurationMS > 0 && createdAtMS > 0 {
|
||||
expiresAtMS = createdAtMS + typed.DurationMS
|
||||
}
|
||||
current.BanUsers[typed.TargetUserID] = state.UserModerationState{
|
||||
UserID: typed.TargetUserID,
|
||||
ActorUserID: typed.ActorUserID(),
|
||||
CreatedAtMS: createdAtMS,
|
||||
ExpiresAtMS: expiresAtMS,
|
||||
}
|
||||
current.Version++
|
||||
case *command.UnbanUser:
|
||||
// Unban 只解除 ban,不恢复被 KickUser 删除的 presence、麦位或管理员身份。
|
||||
|
||||
@ -71,7 +71,7 @@ func (s *Service) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasu
|
||||
if snapshot.GetStatus() != state.RoomStatusActive {
|
||||
return nil, xerr.New(xerr.RoomClosed, "room closed")
|
||||
}
|
||||
if containsUserID(snapshot.GetBanUserIds(), viewerUserID) || findProtoUser(snapshot, viewerUserID) == nil {
|
||||
if snapshotUserBanned(snapshot, viewerUserID, s.clock.Now().UnixMilli()) || findProtoUser(snapshot, viewerUserID) == nil {
|
||||
// 宝箱状态属于房间内活动信息,只允许仍在业务 presence 内的用户读取。
|
||||
return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room")
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ func (s *Service) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapsh
|
||||
if snapshot.GetStatus() != state.RoomStatusActive {
|
||||
return nil, xerr.New(xerr.RoomClosed, "room closed")
|
||||
}
|
||||
if containsUserID(snapshot.GetBanUserIds(), viewerUserID) || findProtoUser(snapshot, viewerUserID) == nil {
|
||||
if snapshotUserBanned(snapshot, viewerUserID, s.clock.Now().UnixMilli()) || findProtoUser(snapshot, viewerUserID) == nil {
|
||||
// 完整快照包含在线用户、麦位和房管集合,必须要求 viewer 仍在业务 presence 内。
|
||||
return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room")
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUs
|
||||
|
||||
_, inRoom := current.OnlineUsers[cmd.TargetUserID]
|
||||
_, onSeat := current.SeatByUser(cmd.TargetUserID)
|
||||
alreadyBanned := current.BanUsers[cmd.TargetUserID]
|
||||
_, alreadyBanned := current.BanUsers[cmd.TargetUserID]
|
||||
if !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] && (!cmd.BanFromRoom || alreadyBanned) {
|
||||
// 重复系统驱逐保持幂等;没有残留状态时只返回当前快照。
|
||||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||
@ -89,7 +89,11 @@ func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUs
|
||||
delete(current.AdminUsers, cmd.TargetUserID)
|
||||
if cmd.BanFromRoom {
|
||||
// BanFromRoom 由治理入口决定;只驱逐不封房间时,用户后续可按产品策略重新进房。
|
||||
current.BanUsers[cmd.TargetUserID] = true
|
||||
current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
|
||||
UserID: cmd.TargetUserID,
|
||||
ActorUserID: 0,
|
||||
CreatedAtMS: now.UnixMilli(),
|
||||
}
|
||||
}
|
||||
current.Version++
|
||||
|
||||
|
||||
@ -98,6 +98,23 @@ type RoomUserState struct {
|
||||
LastSeenAtMS int64
|
||||
}
|
||||
|
||||
// UserModerationState 保存房间内 ban/mute 这类治理状态的时间边界。
|
||||
type UserModerationState struct {
|
||||
// UserID 是被治理用户。
|
||||
UserID int64
|
||||
// ActorUserID 是触发治理命令的房间内操作者;系统治理入口使用 0。
|
||||
ActorUserID int64
|
||||
// CreatedAtMS 是治理状态写入 Room Cell 的 UTC epoch milliseconds。
|
||||
CreatedAtMS int64
|
||||
// ExpiresAtMS 为 0 表示永久有效;非 0 表示该 UTC 毫秒后自动失效。
|
||||
ExpiresAtMS int64
|
||||
}
|
||||
|
||||
// ActiveAt 判断治理状态在指定 UTC 毫秒是否仍然生效。
|
||||
func (m UserModerationState) ActiveAt(nowMS int64) bool {
|
||||
return m.UserID > 0 && (m.ExpiresAtMS == 0 || m.ExpiresAtMS > nowMS)
|
||||
}
|
||||
|
||||
// RankItem 表达房间内本地礼物榜的单个用户累计值。
|
||||
type RankItem struct {
|
||||
// UserID 是榜单归属用户。
|
||||
@ -189,8 +206,8 @@ type RoomState struct {
|
||||
OnlineUsers map[int64]*RoomUserState
|
||||
// AdminUsers 是房间管理权限集合。
|
||||
AdminUsers map[int64]bool
|
||||
// BanUsers 是被踢或封禁的房间用户集合。
|
||||
BanUsers map[int64]bool
|
||||
// BanUsers 是被踢或封禁的房间用户状态,过期时间由 Room Cell 持久化和恢复。
|
||||
BanUsers map[int64]UserModerationState
|
||||
// MuteUsers 是被禁言用户集合。
|
||||
MuteUsers map[int64]bool
|
||||
// Heat 是房间热度,当前主要由送礼累计。
|
||||
@ -223,7 +240,7 @@ func NewRoomState(roomID string, ownerUserID int64, seatCount int32, mode string
|
||||
MicSeats: seats,
|
||||
OnlineUsers: make(map[int64]*RoomUserState),
|
||||
AdminUsers: make(map[int64]bool),
|
||||
BanUsers: make(map[int64]bool),
|
||||
BanUsers: make(map[int64]UserModerationState),
|
||||
MuteUsers: make(map[int64]bool),
|
||||
RoomExt: make(map[string]string),
|
||||
}
|
||||
@ -248,7 +265,7 @@ func (s *RoomState) Clone() *RoomState {
|
||||
MicSeats: append([]MicSeat(nil), s.MicSeats...),
|
||||
OnlineUsers: make(map[int64]*RoomUserState, len(s.OnlineUsers)),
|
||||
AdminUsers: make(map[int64]bool, len(s.AdminUsers)),
|
||||
BanUsers: make(map[int64]bool, len(s.BanUsers)),
|
||||
BanUsers: make(map[int64]UserModerationState, len(s.BanUsers)),
|
||||
MuteUsers: make(map[int64]bool, len(s.MuteUsers)),
|
||||
Heat: s.Heat,
|
||||
GiftRank: append([]RankItem(nil), s.GiftRank...),
|
||||
@ -387,7 +404,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
|
||||
MicSeats: seats,
|
||||
OnlineUsers: users,
|
||||
AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID),
|
||||
BanUserIds: sortedSetIDs(s.BanUsers),
|
||||
BanUserIds: sortedModerationIDs(s.BanUsers),
|
||||
MuteUserIds: sortedSetIDs(s.MuteUsers),
|
||||
GiftRank: rankItems,
|
||||
RoomExt: cloneStringMap(s.RoomExt),
|
||||
@ -396,6 +413,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
|
||||
RoomShortId: s.RoomExt["room_short_id"],
|
||||
Locked: s.RoomPasswordHash != "",
|
||||
Treasure: treasureStateToProto(s.Treasure),
|
||||
BanStates: sortedModerationStates(s.BanUsers),
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,7 +435,7 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
|
||||
MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())),
|
||||
OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())),
|
||||
AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())),
|
||||
BanUsers: make(map[int64]bool, len(snapshot.GetBanUserIds())),
|
||||
BanUsers: make(map[int64]UserModerationState, len(snapshot.GetBanStates())+len(snapshot.GetBanUserIds())),
|
||||
MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())),
|
||||
GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())),
|
||||
RoomExt: cloneStringMap(snapshot.GetRoomExt()),
|
||||
@ -460,8 +478,26 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
|
||||
restored.AdminUsers[userID] = true
|
||||
}
|
||||
|
||||
for _, ban := range snapshot.GetBanStates() {
|
||||
if ban.GetUserId() <= 0 {
|
||||
continue
|
||||
}
|
||||
restored.BanUsers[ban.GetUserId()] = UserModerationState{
|
||||
UserID: ban.GetUserId(),
|
||||
ActorUserID: ban.GetActorUserId(),
|
||||
CreatedAtMS: ban.GetCreatedAtMs(),
|
||||
ExpiresAtMS: ban.GetExpiresAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
for _, userID := range snapshot.GetBanUserIds() {
|
||||
restored.BanUsers[userID] = true
|
||||
if userID <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := restored.BanUsers[userID]; !exists {
|
||||
// 旧快照只有 ban_user_ids 时按永久 ban 恢复,避免恢复后放宽权限。
|
||||
restored.BanUsers[userID] = UserModerationState{UserID: userID}
|
||||
}
|
||||
}
|
||||
|
||||
for _, userID := range snapshot.GetMuteUserIds() {
|
||||
@ -495,6 +531,32 @@ func sortedSetIDs(values map[int64]bool) []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
func sortedModerationIDs(values map[int64]UserModerationState) []int64 {
|
||||
ids := make([]int64, 0, len(values))
|
||||
for userID, moderation := range values {
|
||||
if moderation.UserID > 0 {
|
||||
ids = append(ids, userID)
|
||||
}
|
||||
}
|
||||
slices.Sort(ids)
|
||||
return ids
|
||||
}
|
||||
|
||||
func sortedModerationStates(values map[int64]UserModerationState) []*roomv1.RoomBanState {
|
||||
ids := sortedModerationIDs(values)
|
||||
states := make([]*roomv1.RoomBanState, 0, len(ids))
|
||||
for _, userID := range ids {
|
||||
moderation := values[userID]
|
||||
states = append(states, &roomv1.RoomBanState{
|
||||
UserId: moderation.UserID,
|
||||
ActorUserId: moderation.ActorUserID,
|
||||
CreatedAtMs: moderation.CreatedAtMS,
|
||||
ExpiresAtMs: moderation.ExpiresAtMS,
|
||||
})
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
func sortedSetIDsExcept(values map[int64]bool, excluded int64) []int64 {
|
||||
ids := make([]int64, 0, len(values))
|
||||
for userID, enabled := range values {
|
||||
|
||||
@ -507,6 +507,18 @@ func (s *Server) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOn
|
||||
return mapServiceResult(s.svc.ListRoomOnlineUsers(ctx, req))
|
||||
}
|
||||
|
||||
// ListRoomBannedUsers 代理到房间黑名单分页读模型。
|
||||
func (s *Server) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
// 黑名单来自 Room Cell,必须按 room owner 路由读取最新治理状态。
|
||||
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||
if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.ListRoomBannedUsersResponse, error) {
|
||||
return client.ListRoomBannedUsers(callCtx, req)
|
||||
}); forwarded {
|
||||
return resp, err
|
||||
}
|
||||
return mapServiceResult(s.svc.ListRoomBannedUsers(ctx, req))
|
||||
}
|
||||
|
||||
func contextWithMetaApp(ctx context.Context, meta *roomv1.RequestMeta) context.Context {
|
||||
if meta == nil {
|
||||
return appcode.WithContext(ctx, "")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user